From fc26970f8a5334675fe01b104186d5403c53a03f Mon Sep 17 00:00:00 2001 From: m0Nst3r873 Date: Tue, 23 Jun 2026 20:20:09 +0800 Subject: [PATCH 1/3] feat(ci): add MR knowledge extraction pipeline with reaction/reject interaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `teamai ci extract-mr` command for CI pipeline integration: - Extract learnings and codebase suggestions from MR/PR via AI - Post each suggestion as individual comment with unique marker - Reviewer rejects unwanted items via reaction (GitHub 👎 / TGit ☝️) - Post-merge: filter out rejected items, write approved knowledge to team repo - Auto-configure git committer from token API (passes TGit committer-check) - Fix TGit API: use ?iid= query and global MR id for Notes API - Fix AI output: strip preamble before YAML frontmatter - CI templates for GitHub Actions and Coding CI (QCI) - Update README with CI Integration section --story=0 --- .gitignore | 1 + README.md | 37 ++++ RELEASE_NOTE_v0.16.8.md | 286 ++++++++++++++++++++++++++ examples/ci/coding-ci-mr-extract.yaml | 4 +- src/__tests__/read-rejections.test.ts | 63 ++++++ src/ci/extract-mr.ts | 124 +++++++++-- src/ci/mr-comment.ts | 165 +++++++++++++++ src/ci/read-rejections.ts | 200 ++++++++++++++++++ src/index.ts | 1 + 9 files changed, 863 insertions(+), 18 deletions(-) create mode 100644 RELEASE_NOTE_v0.16.8.md create mode 100644 src/__tests__/read-rejections.test.ts create mode 100644 src/ci/read-rejections.ts diff --git a/.gitignore b/.gitignore index 7041ac7..644ed48 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ __pycache__/ .gstack/ .worktrees/ .codebuddy/ +.teamai/ # Local-only drafts and scratch notes (kept out of the open-source repo) eval/ diff --git a/README.md b/README.md index 7edfe7f..eb12863 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ The CLI picks a provider automatically from the repo URL: | `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | Inspect and resolve domain-drift signals; `--apply` reassigns the repo to the recommended domain and refreshes the aggregate views | | `teamai digest` | Generate a team AI usage weekly digest (skill leaderboard, new/updated skills, session summaries) | | `teamai hooks` | Manage AI-tool hooks (list / inject / remove) | +| `teamai ci extract-mr --url [--mode comment\|write\|both] [--individual-comments]` | CI pipeline integration: extract knowledge from MR/PR, post as comments, and write to team repo after merge. With `--individual-comments`, each suggestion is posted separately with reaction/reject support (GitHub 👎 / TGit ☝️) | | `teamai uninstall [--force]` | Uninstall teamai: remove hooks, rules, skills, env, docs, and `~/.teamai/` | | `teamai doctor` | Diagnose configuration problems | @@ -361,6 +362,42 @@ Auto-update runs on the Stop hook at the end of a session. It can be controlled The user-level `updatePolicy` always wins over the team-level `autoUpdate`. +## CI Integration + +TeamAI can integrate into your CI pipeline to automatically extract knowledge from every MR/PR: + +``` +MR opened/updated → CI extracts learning + codebase suggestions → posts as comments + → Reviewer rejects unwanted suggestions (GitHub 👎 / TGit ☝️) + → MR merged → CI writes approved items to team knowledge repo +``` + +### Quick Start + +```bash +# Comment mode: post suggestions to MR (run on PR open/update) +teamai ci extract-mr --url "$MR_URL" --mode comment --individual-comments + +# Write mode: write approved items to knowledge repo (run after merge) +teamai ci extract-mr --url "$MR_URL" --mode write --team-repo ./team-repo --individual-comments +``` + +### CI Templates + +Ready-to-use templates in `examples/ci/`: + +| File | Platform | +|------|----------| +| `github-actions-mr-extract.yml` | GitHub Actions | +| `coding-ci-mr-extract.yaml` | Coding CI (TGit + ZhiYan QCI) | + +### Reject Interaction + +| Platform | How to reject | Default | +|----------|--------------|---------| +| GitHub | Add 👎 reaction to the suggestion comment | Write all | +| TGit | Add ☝️ emoji to the suggestion note | Write all | + ## License [MIT](LICENSE) diff --git a/RELEASE_NOTE_v0.16.8.md b/RELEASE_NOTE_v0.16.8.md new file mode 100644 index 0000000..4566274 --- /dev/null +++ b/RELEASE_NOTE_v0.16.8.md @@ -0,0 +1,286 @@ +# teamai-cli v0.16.8 — 团队 AI 知识飞轮 MVP + +本版本实现了团队 AI 知识飞轮的最小闭环:**知识库初始化 → 智能检索 → 贡献反馈**,团队成员从安装到受益只需 5 分钟。 + +--- + +## 快速开始(5 分钟上手) + +```bash +# 安装(内部) +tnpm install -g @tencent/teamai-cli + +# 安装(公网) +npm install -g teamai-cli + +# 初始化(首次,配置团队仓库) +teamai init --repo <团队仓库地址> + +# 同步团队知识到本地(部署 recall agent + 构建索引 + 注入 hooks) +teamai pull + +# 开始编码 — 一切自动生效,无需额外操作 +# recall agent 自动在每个任务前检索团队知识库 +# hooks 自动追踪使用情况和贡献判断 +``` + +--- + +## 核心能力详解 + +### 知识库初始化(Cold Start / import) + +**本地文档导入** + +```bash +teamai import --dir ./docs # 扫描本地 Markdown 文档 +teamai import --from-claude # 导入 Claude/Cursor 规则目录 +teamai import --workspace # 从当前 git 仓库生成 codebase.md +``` + +**远程仓库导入** + +```bash +teamai import --from-repo # 单仓导入 +teamai import --from-repo-list .teamai/repos.yaml # 批量导入 +``` + +**组织级一键初始化** + +```bash +teamai import --from-org <工蜂 group ID> # 工蜂 group +teamai import --from-org # GitHub org +teamai import --from-org --bootstrap # 含交互式域确认 +``` + +- AI 自动聚类仓库为业务域,生成 `domains.yaml` +- 产出仓库白名单 + 业务域字典 + 全量 codebase 文档 +- 认证三层兜底:token → SSH → public + +**iWiki 导入** + +```bash +teamai import --from-iwiki # 需要 TAI_PAT_TOKEN +``` + +**MR 历史提炼** + +```bash +teamai import --from-mr +``` + +从合入的 MR 提取 learning 草稿(confidence: 0.85)和 codebase 更新建议,自动与近期 session learnings 去重。 + +**增量同步** + +```bash +teamai import --from-repo-list .teamai/repos.yaml --incremental +``` + +SHA 未变化时跳过 AI 扫描;章节级 diff,只覆写变化部分,保留人工批注。 + +--- + +### 检索 Subagent(自动可用) + +`teamai pull` 后自动部署 `teamai-recall` subagent。主对话通过 Agent tool 自动调用,CLAUDE.md 注入触发规则,无需手动操作。 + +**检索范围** + +| 知识类型 | 说明 | +|---------|------| +| Learnings | 团队成员贡献的经验总结 | +| Docs | 团队文档 | +| Rules | 编码规则 | +| Skills | 技能/Slash Commands | +| Codebase | `import --from-repo` 产物 | + +**智能增强** + +- Domain 推断加权:technical > neutral > ops > support +- IDF 权重 + Vote 加分排序 +- 错误自动检索:Bash 报错时 hook 自动触发知识库搜索 + +**手动检索** + +```bash +teamai recall "k8s pod crashloop" +``` + +--- + +### 贡献反馈(contribute-check) + +Session 结束时,Stop hook 自动判断是否值得贡献: + +- 知识库空白感知:recall 均未命中(`hitCount=0`)时触发更强贡献提示 +- 低覆盖感知:命中质量低(`topScore < 5.0`)时温和引导 +- git commit 检测:有 commit 的 session 降低触发权重,避免与 MR learning 重复 + +**贡献流程** + +``` +/teamai-share-learnings # AI 生成经验总结 + → teamai contribute --file # 推送到团队仓库 learnings/ 目录 + → 下次 teamai pull 时所有成员可检索到 +``` + +推送失败时自动本地 commit 保护数据,下次 pull 时重试。 + +--- + +### MR 驱动知识沉淀(P4.4) + +每次 MR 合入后双路输出: + +- learning 草稿(confidence: 0.85,已过 code review) +- codebase.md 变更建议(新服务 / 接口变更 / 架构决策) + +SessionStart hook 自动提醒最近合入但未导入的 MR。 + +```bash +teamai import --from-mr +``` + +--- + +## 团队级 Codebase 知识库 + +### 业务域字典 + +域配置存储于 `.teamai/domains.yaml`,支持一键初始化: + +```bash +teamai import --from-org --bootstrap +``` + +AI 聚类仓库为业务域(如"接入层"、"后端服务"、"基础设施"),CLI 交互确认后写入 `domains.yaml`。 + +### 单仓导入 + +```bash +teamai import --from-repo # 不指定域 +teamai import --from-repo --domain 接入层 # 显式指定域 +``` + +### 批量导入 + +```bash +teamai import --from-repo-list .teamai/repo-whitelist.yaml +teamai import --from-repo-list .teamai/repo-whitelist.yaml --concurrency 5 +``` + +默认并发 3 仓,产物按域拆分为 `docs/codebase/domain-*.md`。 + +### 增量同步 + +```bash +teamai import --from-repo-list .teamai/repo-whitelist.yaml --incremental +``` + +仅重扫变更涉及的模块,并自动检测域漂移。 + +--- + +## 运维与治理 + +### 缓存管理 + +```bash +teamai cache --status # 查看缓存状态 +teamai cache --gc # 清理过期缓存(LRU,默认 5GB 上限) +teamai cache --gc --dry-run # 预览清理结果,不实际删除 +``` + +### Codebase 健康度 + +```bash +teamai codebase --lint # 检查文档一致性 +teamai codebase --lint --fix # 自动修复低风险问题 +teamai codebase --lint --severity high # 只报高级别问题 +teamai codebase --lint --json # 机器可读输出(供 CI 消费) +``` + +### Pending Review + +```bash +teamai review # 列出待审核项 +teamai review --apply # 接受并应用 +teamai review --reject # 拒绝 +teamai review --all-apply --max-risk low # 批量接受低风险项 +``` + +### 域漂移 + +```bash +teamai domains drift # 列出漂移建议 +teamai domains drift --apply # 接受指定仓库的重分类 +teamai domains drift --apply-all --threshold 0.8 # 自动应用高置信项 +``` + +--- + +## 会话 Dashboard & Analytics + +```bash +teamai stats # 本地 skill 使用统计 +teamai dashboard # 启动 Web UI(默认端口 3721),实时查看团队编码会话 +teamai digest # 生成团队周活动摘要 +teamai save-session # 保存当前会话摘要 +``` + +- `teamai track` / `teamai track-slash`:hook 自动调用,追踪工具使用事件 +- `teamai dashboard-report`:hook 自动上报会话状态 +- 数据存储路径:`~/.teamai/usage.jsonl` + +--- + +## 支持的 AI 编码工具 + +| 工具 | Recall Agent | Hooks | CLAUDE.md 规则注入 | +|------|:---:|:---:|:---:| +| Claude Code | ✅ | ✅ | ✅ | +| CodeBuddy | ✅ | ✅ | ✅ | +| Cursor | ✅ | ✅ | — | +| Codex | ✅ | ✅ | — | +| OpenClaw | ✅ | — | — | +| WorkBuddy | ✅ | — | — | + +--- + +## 管理员功能 + +```bash +# 角色管理 +teamai roles init / add / remove / set + +# 标签管理 +teamai tags add / remove / subscribe / unsubscribe + +# 跨团队源 +teamai source add / remove / browse + +# 环境变量 +teamai env add / remove / list + +# 成员管理 +teamai members list +``` + +--- + +## 已知限制与注意事项 + +- `--from-org` 需要认证:工蜂使用 `~/.netrc` 或 `gf auth login`;GitHub 使用 `GITHUB_TOKEN` 或 `gh auth login` +- `--from-iwiki` 需要设置环境变量 `TAI_PAT_TOKEN` +- AI codebase 扫描大仓库可能需要数分钟,超时上限为 12 分钟 +- 搜索索引中 codebase 条目的 Score 可能显示为 NaN,不影响排序,后续版本修复 +- Phase 3(Vote 双计数器)和 Phase 4(置信度维护)将在后续版本实现 +- 工蜂的 `--from-org` 支持已通过 TGit listOrgRepos 实现 + +--- + +## 反馈渠道 + +- 内部群聊 / issue 跟踪 +- `teamai doctor` 可自助诊断配置问题 diff --git a/examples/ci/coding-ci-mr-extract.yaml b/examples/ci/coding-ci-mr-extract.yaml index 252fc94..89c3243 100644 --- a/examples/ci/coding-ci-mr-extract.yaml +++ b/examples/ci/coding-ci-mr-extract.yaml @@ -53,8 +53,8 @@ stages: --write-mode direct - | cd team-repo - git config user.name "teamai-ci" - git config user.email "teamai-ci@tencent.com" + # git user.name/email 由 teamai ci extract-mr 自动从 token 对应账号获取 + # 确保 TAI_PAT_TOKEN 对应的账号对知识仓库有 push 权限即可 git add learnings/ docs/ .teamai/ git diff --cached --quiet || \ git commit -m "[teamai] Extract knowledge from MR !${CI_MERGE_REQUEST_IID}" diff --git a/src/__tests__/read-rejections.test.ts b/src/__tests__/read-rejections.test.ts new file mode 100644 index 0000000..eac01d7 --- /dev/null +++ b/src/__tests__/read-rejections.test.ts @@ -0,0 +1,63 @@ +import { describe, it, expect } from 'vitest'; +import { extractMarkerId, shouldWrite } from '../ci/read-rejections.js'; +import type { RejectionResult } from '../ci/read-rejections.js'; + +describe('extractMarkerId', () => { + it('提取 learning marker', () => { + expect(extractMarkerId('\nsome content')).toBe('learning'); + }); + + it('提取 suggestion marker', () => { + expect(extractMarkerId('\ncontent')).toBe('suggestion:1'); + }); + + it('提取 suggestion:2 marker', () => { + expect(extractMarkerId('')).toBe('suggestion:2'); + }); + + it('无 marker 返回 null', () => { + expect(extractMarkerId('普通 comment 内容')).toBeNull(); + }); + + it('其他 teamai marker 不匹配', () => { + expect(extractMarkerId('')).toBeNull(); + }); +}); + +describe('shouldWrite', () => { + const rejections: RejectionResult = { + rejectedIds: new Set(['suggestion:2']), + approvedIds: new Set(['learning', 'suggestion:1']), + allIds: new Set(['learning', 'suggestion:1', 'suggestion:2']), + }; + + describe('GitHub (默认写入,👎 = reject)', () => { + it('不在 rejectedIds 中 → 写入', () => { + expect(shouldWrite('learning', rejections, 'github')).toBe(true); + expect(shouldWrite('suggestion:1', rejections, 'github')).toBe(true); + }); + + it('在 rejectedIds 中 → 不写入', () => { + expect(shouldWrite('suggestion:2', rejections, 'github')).toBe(false); + }); + + it('未知 id(不在任何集合中)→ 写入(默认)', () => { + expect(shouldWrite('suggestion:99', rejections, 'github')).toBe(true); + }); + }); + + describe('TGit (默认写入,🚫 emoji = reject)', () => { + it('不在 rejectedIds 中 → 写入', () => { + expect(shouldWrite('learning', rejections, 'tgit')).toBe(true); + expect(shouldWrite('suggestion:1', rejections, 'tgit')).toBe(true); + }); + + it('在 rejectedIds 中 → 不写入', () => { + expect(shouldWrite('suggestion:2', rejections, 'tgit')).toBe(false); + }); + + it('未知 id(不在任何集合中)→ 写入(默认)', () => { + expect(shouldWrite('suggestion:99', rejections, 'tgit')).toBe(true); + }); + }); +}); diff --git a/src/ci/extract-mr.ts b/src/ci/extract-mr.ts index 424658c..3d63998 100644 --- a/src/ci/extract-mr.ts +++ b/src/ci/extract-mr.ts @@ -18,7 +18,8 @@ import { appendPendingReview } from '../review-store.js'; import { pushRepoDirectly } from '../utils/git.js'; import { log } from '../utils/logger.js'; import type { LearningDraft, CodebaseSuggestion } from '../types.js'; -import { postOrUpdateMrComment } from './mr-comment.js'; +import { postOrUpdateMrComment, postIndividualComments, parseMrUrl } from './mr-comment.js'; +import { readRejections, shouldWrite } from './read-rejections.js'; // ─── 类型 ──────────────────────────────────────────────── @@ -31,6 +32,62 @@ export interface CiExtractMrOptions { writeMode?: 'direct' | 'pending-review'; output?: string; dryRun?: boolean; + individualComments?: boolean; +} + +// ─── Git User 配置 ─────────────────────────────────────── + +/** + * 自动配置 git user.name 和 user.email。 + * + * 通过 provider API 获取当前 token 对应的用户信息: + * - GitHub: GITHUB_TOKEN → GET /user + * - TGit: TAI_PAT_TOKEN → GET /api/v3/user + */ +async function configureGitUser(repoPath: string, provider: 'github' | 'tgit'): Promise { + const { execFileSync } = await import('node:child_process'); + let name = 'teamai-ci'; + let email = 'teamai-ci@noreply'; + + try { + if (provider === 'github') { + const token = process.env['GITHUB_TOKEN']; + if (token) { + const resp = await fetch('https://api.github.com/user', { + headers: { Authorization: `Bearer ${token}`, 'User-Agent': 'teamai-cli' }, + signal: AbortSignal.timeout(8000), + }); + if (resp.ok) { + const user = (await resp.json()) as { login: string; email: string | null }; + name = user.login; + email = user.email ?? `${user.login}@users.noreply.github.com`; + } + } + } else { + const token = process.env['TAI_PAT_TOKEN']; + if (token) { + const resp = await fetch('https://git.woa.com/api/v3/user', { + headers: { Authorization: `Bearer ${token}` }, + signal: AbortSignal.timeout(8000), + }); + if (resp.ok) { + const user = (await resp.json()) as { username: string; email: string }; + name = user.username; + email = user.email; + } + } + } + } catch { + log.debug('无法获取用户信息,使用默认 git user'); + } + + try { + execFileSync('git', ['config', 'user.name', name], { cwd: repoPath, stdio: 'ignore' }); + execFileSync('git', ['config', 'user.email', email], { cwd: repoPath, stdio: 'ignore' }); + log.debug(`Git user: ${name} <${email}>`); + } catch { + log.debug('git config 失败(非 git 仓库),跳过'); + } } // ─── 写入逻辑 ──────────────────────────────────────────── @@ -102,6 +159,8 @@ async function writeKnowledgeToRepo( // 提交并推送 if (!dryRun && changedFiles.length > 0) { try { + const provider = mrUrl.includes('github.com') ? 'github' as const : 'tgit' as const; + await configureGitUser(teamRepo, provider); await pushRepoDirectly(teamRepo, `[teamai] CI extract knowledge from MR`, changedFiles); log.success('已推送到团队仓库'); } catch (err) { @@ -185,29 +244,62 @@ export async function ciExtractMr(opts: CiExtractMrOptions): Promise { // 执行 comment if (opts.mode === 'comment' || opts.mode === 'both') { - const result = await postOrUpdateMrComment( - opts.url, - learning, - suggestions, - opts.commentMarker, - opts.dryRun, - ); - if (result.created) { - log.success('MR comment 已发布'); + if (opts.individualComments) { + const { posted } = await postIndividualComments(opts.url, learning, suggestions, opts.dryRun); + log.success(`已发布 ${posted} 条独立建议 comment`); } else { - log.success('MR comment 已更新'); - } - if (result.url) { - log.info(`Comment URL: ${result.url}`); + const result = await postOrUpdateMrComment( + opts.url, + learning, + suggestions, + opts.commentMarker, + opts.dryRun, + ); + if (result.created) { + log.success('MR comment 已发布'); + } else { + log.success('MR comment 已更新'); + } + if (result.url) { + log.info(`Comment URL: ${result.url}`); + } } } // 执行 write if (opts.mode === 'write' || opts.mode === 'both') { + // 当使用 individual comments 时,读取 rejection 状态进行过滤 + let filteredLearning = learning; + let filteredSuggestions = suggestions; + + if (opts.individualComments && !opts.dryRun) { + const parsed = parseMrUrl(opts.url); + const rejections = await readRejections(opts.url); + + if (rejections.allIds.size > 0) { + // 过滤 learning + if (learning && !shouldWrite('learning', rejections, parsed.provider)) { + log.info('Learning 被 reject,跳过写入'); + filteredLearning = undefined; + } + + // 过滤 suggestions + if (suggestions) { + filteredSuggestions = suggestions.filter((_, i) => + shouldWrite(`suggestion:${i + 1}`, rejections, parsed.provider), + ); + const rejected = suggestions.length - filteredSuggestions.length; + if (rejected > 0) { + log.info(`${rejected} 条 codebase 建议被 reject,已排除`); + } + } + } + } + await writeKnowledgeToRepo( opts.teamRepo!, - learning, - suggestions, + filteredLearning, + filteredSuggestions, opts.writeMode ?? 'direct', opts.url, opts.dryRun, diff --git a/src/ci/mr-comment.ts b/src/ci/mr-comment.ts index 066c86c..d1affc5 100644 --- a/src/ci/mr-comment.ts +++ b/src/ci/mr-comment.ts @@ -340,3 +340,168 @@ export async function postOrUpdateMrComment( } return postTGitComment(projectId, mrGlobalId, body); } + +// ─── 逐条发布(Individual Comments)───────────────────── + +/** + * 格式化单条建议为独立 comment 内容。 + */ +export function formatIndividualComment( + type: 'learning' | 'suggestion', + index: number, + content: { title?: string; body?: string; section?: string; action?: string; preview?: string }, + provider: 'github' | 'tgit', +): string { + const markerId = type === 'learning' ? 'learning' : `suggestion:${index}`; + const marker = ``; + + const lines: string[] = [marker, '']; + + if (type === 'learning') { + lines.push(`## TeamAI Learning`, ''); + lines.push(`**${content.title}**`, ''); + if (content.body) { + lines.push('
', '展开完整内容', ''); + lines.push('```markdown'); + lines.push(content.body); + lines.push('```'); + lines.push('', '
'); + } + } else { + lines.push(`## TeamAI Codebase 建议 #${index}`, ''); + lines.push(`**章节**: ${content.section} | **操作**: ${content.action}`, ''); + if (content.preview) { + lines.push('```'); + lines.push(content.preview.slice(0, 300)); + lines.push('```'); + } + } + + lines.push(''); + if (provider === 'github') { + lines.push('> 💡 不写入此条?请对本条 comment 加 👎 reaction'); + } else { + lines.push('> 💡 不写入此条?请对本条评论加 ☝️ emoji reaction'); + } + lines.push(''); + lines.push('_Auto-generated by `teamai ci extract-mr`_'); + + return lines.join('\n'); +} + +/** + * 逐条发布 learning 和 suggestions 为独立的 comment/note。 + * + * 每条建议带唯一 marker,支持后续 readRejections 读取状态。 + * TGit 上发布为 conversation note,reviewer 通过 ☝️ emoji reaction 来 reject。 + */ +export async function postIndividualComments( + mrUrl: string, + learning: LearningDraft | undefined, + suggestions: CodebaseSuggestion[] | undefined, + dryRun?: boolean, +): Promise<{ posted: number }> { + const parsed = parseMrUrl(mrUrl); + let posted = 0; + + const items: Array<{ markerId: string; body: string }> = []; + + // Learning + if (learning) { + const body = formatIndividualComment( + 'learning', 0, + { title: learning.title, body: learning.content }, + parsed.provider, + ); + items.push({ markerId: 'learning', body }); + } + + // Suggestions + if (suggestions) { + for (let i = 0; i < suggestions.length; i++) { + const s = suggestions[i]; + const body = formatIndividualComment( + 'suggestion', i + 1, + { section: s.section, action: s.action, preview: s.content }, + parsed.provider, + ); + items.push({ markerId: `suggestion:${i + 1}`, body }); + } + } + + if (items.length === 0) return { posted: 0 }; + + if (dryRun) { + for (const item of items) { + log.info(`dry-run [${item.markerId}]:`); + console.log(item.body); + console.log('---'); + } + return { posted: items.length }; + } + + if (parsed.provider === 'github') { + for (const item of items) { + const marker = ``; + const existing = await findGitHubComment(parsed.owner, parsed.repo, parsed.number, marker); + if (existing) { + await updateGitHubComment(parsed.owner, parsed.repo, existing.id, item.body); + } else { + await postGitHubComment(parsed.owner, parsed.repo, parsed.number, item.body); + } + posted++; + } + } else { + // TGit: 作为 code review comment(关联文件),带 resolve 按钮 + const projectId = encodeURIComponent(`${parsed.owner}/${parsed.repo}`); + const mrGlobalId = await getMrGlobalId(projectId, parsed.number); + + // 获取 MR 变更文件列表,取第一个文件作为 code review comment 锚点 + // 使用 /repository/compare API(/changes 对某些 MR 状态不可用) + const mrListResp = await tgitRequest( + `/projects/${projectId}/merge_requests?iid=${parsed.number}`, + 'GET', + ); + let anchorFile = 'README.md'; + if (mrListResp.ok) { + const mrList = (await mrListResp.json()) as Array<{ source_branch: string; target_branch: string }>; + if (mrList.length > 0) { + const { source_branch, target_branch } = mrList[0]; + const compareResp = await tgitRequest( + `/projects/${projectId}/repository/compare?from=${target_branch}&to=${source_branch}`, + 'GET', + ); + if (compareResp.ok) { + const compareData = (await compareResp.json()) as { diffs?: Array<{ new_path: string }> }; + if (compareData.diffs && compareData.diffs.length > 0) { + anchorFile = compareData.diffs[0].new_path; + } + } + } + } + + for (const item of items) { + const marker = ``; + // 检查是否已存在 + const existingNote = await findTGitComment(projectId, mrGlobalId, marker); + if (existingNote) { + await updateTGitComment(projectId, mrGlobalId, existingNote.id, item.body); + } else { + // 创建 code review comment(关联到文件第 1 行) + const resp = await tgitRequest( + `/projects/${projectId}/merge_requests/${mrGlobalId}/notes`, + 'POST', + { body: item.body, path: anchorFile, line: 1, line_type: 'new' }, + ); + if (!resp.ok) { + // fallback: 创建普通 note + await postTGitComment(projectId, mrGlobalId, item.body); + } + } + posted++; + } + } + + log.success(`已发布 ${posted} 条独立建议`); + return { posted }; +} diff --git a/src/ci/read-rejections.ts b/src/ci/read-rejections.ts new file mode 100644 index 0000000..41718ca --- /dev/null +++ b/src/ci/read-rejections.ts @@ -0,0 +1,200 @@ +// -*- coding: utf-8 -*- +/** + * 读取 MR/PR 上 reviewer 对知识建议的交互状态。 + * + * - GitHub: 👎 reaction = reject,无 reaction = approve(默认写入) + * - TGit: resolve_state=1 = approve,resolve_state=0 = reject(默认不写入) + */ + +import { parseMrUrl } from './mr-comment.js'; +import { gfGetOAuthToken } from '../providers/tgit/gf-cli.js'; +import { log } from '../utils/logger.js'; + +const API_TIMEOUT_MS = 15_000; + +// ─── 类型 ──────────────────────────────────────────────── + +export interface RejectionResult { + /** 被 reject 的 marker id 集合 (如 "learning", "suggestion:1") */ + rejectedIds: Set; + /** 被 approve 的 marker id 集合 */ + approvedIds: Set; + /** 所有找到的 marker id */ + allIds: Set; +} + +// ─── Marker 解析 ───────────────────────────────────────── + +const MARKER_REGEX = //; + +/** + * 从 comment body 中提取 marker id。 + */ +export function extractMarkerId(body: string): string | null { + const match = body.match(MARKER_REGEX); + return match ? match[1] : null; +} + +// ─── GitHub ───────────────────────────────────────────── + +interface GitHubComment { + id: number; + body: string; +} + +interface GitHubReaction { + content: string; // "+1", "-1", "laugh", etc. +} + +async function githubRequest(path: string): Promise { + const token = process.env['GITHUB_TOKEN']; + if (!token) throw new Error('未设置 GITHUB_TOKEN'); + return fetch(`https://api.github.com${path}`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'teamai-cli', + }, + signal: AbortSignal.timeout(API_TIMEOUT_MS), + }); +} + +async function readGitHubRejections(owner: string, repo: string, prNumber: string): Promise { + const result: RejectionResult = { rejectedIds: new Set(), approvedIds: new Set(), allIds: new Set() }; + + // 读取所有 comments + const resp = await githubRequest(`/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`); + if (!resp.ok) return result; + const comments = (await resp.json()) as GitHubComment[]; + + for (const comment of comments) { + const markerId = extractMarkerId(comment.body); + if (!markerId) continue; + result.allIds.add(markerId); + + // 读取 reactions + const reactResp = await githubRequest( + `/repos/${owner}/${repo}/issues/comments/${comment.id}/reactions?per_page=100`, + ); + if (!reactResp.ok) { + result.approvedIds.add(markerId); // 读取失败默认 approve + continue; + } + const reactions = (await reactResp.json()) as GitHubReaction[]; + const hasThumbsDown = reactions.some((r) => r.content === '-1'); + + if (hasThumbsDown) { + result.rejectedIds.add(markerId); + } else { + result.approvedIds.add(markerId); + } + } + + return result; +} + +// ─── TGit ─────────────────────────────────────────────── + +/** TGit emoji 编号 8 = ☝️(竖起食指),作为 reject 信号 */ +const TGIT_REJECT_EMOJI = 8; + +interface TGitNoteComment { + comment: number; + author: { username: string }; +} + +interface TGitNote { + id: number; + body: string; + resolve_state: number; + file_path: string | null; + comments: TGitNoteComment[]; +} + +function getTGitToken(): string { + const envToken = process.env['TAI_PAT_TOKEN']; + if (envToken) return envToken; + const oauthToken = gfGetOAuthToken(); + if (oauthToken) return oauthToken; + throw new Error('未设置 TAI_PAT_TOKEN 且无法获取 OAuth token'); +} + +async function tgitRequest(path: string): Promise { + const token = getTGitToken(); + return fetch(`https://git.woa.com/api/v3${path}`, { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + signal: AbortSignal.timeout(API_TIMEOUT_MS), + }); +} + +async function getMrGlobalId(projectId: string, mrIid: string): Promise { + const resp = await tgitRequest(`/projects/${projectId}/merge_requests?iid=${mrIid}`); + if (!resp.ok) throw new Error(`TGit 查询 MR 失败 (${resp.status})`); + const mrs = (await resp.json()) as Array<{ id: number; iid: number }>; + const mr = mrs.find((m) => String(m.iid) === mrIid); + if (!mr) throw new Error(`TGit MR !${mrIid} 不存在`); + return mr.id; +} + +async function readTGitRejections(owner: string, repo: string, mrIid: string): Promise { + const result: RejectionResult = { rejectedIds: new Set(), approvedIds: new Set(), allIds: new Set() }; + + const projectId = encodeURIComponent(`${owner}/${repo}`); + const mrGlobalId = await getMrGlobalId(projectId, mrIid); + + // 读取所有 notes + const resp = await tgitRequest(`/projects/${projectId}/merge_requests/${mrGlobalId}/notes?per_page=100`); + if (!resp.ok) return result; + const notes = (await resp.json()) as TGitNote[]; + + for (const note of notes) { + const markerId = extractMarkerId(note.body); + if (!markerId) continue; + result.allIds.add(markerId); + + // TGit: emoji 编号 8 (☝️) = reject,无 emoji = approve(默认写入) + const hasRejectEmoji = (note.comments ?? []).some((c) => c.comment === TGIT_REJECT_EMOJI); + if (hasRejectEmoji) { + result.rejectedIds.add(markerId); + } else { + result.approvedIds.add(markerId); + } + } + + return result; +} + +// ─── Public API ───────────────────────────────────────── + +/** + * 读取 MR/PR 上 reviewer 对知识建议的交互状态。 + * + * 返回被 reject 和 approve 的 marker id 集合。 + * - GitHub: 默认写入,👎 = reject + * - TGit: 默认不写入,点"解决" = approve + */ +export async function readRejections(mrUrl: string): Promise { + const parsed = parseMrUrl(mrUrl); + + if (parsed.provider === 'github') { + log.debug('读取 GitHub reactions...'); + return readGitHubRejections(parsed.owner, parsed.repo, parsed.number); + } + + log.debug('读取 TGit emoji reactions...'); + return readTGitRejections(parsed.owner, parsed.repo, parsed.number); +} + +/** + * 根据 rejection 结果判断某条建议是否应该写入。 + * + * 两个平台逻辑统一:默认写入,被 reject 的不写入。 + * - GitHub: 👎 reaction = reject + * - TGit: ☝️ emoji (编号 8) = reject + */ +export function shouldWrite(markerId: string, rejections: RejectionResult, _provider: 'github' | 'tgit'): boolean { + return !rejections.rejectedIds.has(markerId); +} diff --git a/src/index.ts b/src/index.ts index 1da9849..2823e71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -710,6 +710,7 @@ ciCmd .option('--comment-marker ', 'HTML comment anchor for idempotent updates', '') .option('--write-mode ', 'Write strategy: direct | pending-review', 'direct') .option('--output ', 'Write artifacts to directory') + .option('--individual-comments', 'Post each suggestion as separate comment with reaction/resolve support') .action(async (cmdOpts) => { const globalOpts = program.opts() as GlobalOptions; const { ciExtractMr } = await import('./ci/extract-mr.js'); From b073d5539e24bb652cea0a0880e477baed912a36 Mon Sep 17 00:00:00 2001 From: Jiahe Geng <146067293+m0Nst3r873@users.noreply.github.com> Date: Tue, 23 Jun 2026 20:32:06 +0800 Subject: [PATCH 2/3] Delete RELEASE_NOTE_v0.16.8.md --- RELEASE_NOTE_v0.16.8.md | 286 ---------------------------------------- 1 file changed, 286 deletions(-) delete mode 100644 RELEASE_NOTE_v0.16.8.md diff --git a/RELEASE_NOTE_v0.16.8.md b/RELEASE_NOTE_v0.16.8.md deleted file mode 100644 index 4566274..0000000 --- a/RELEASE_NOTE_v0.16.8.md +++ /dev/null @@ -1,286 +0,0 @@ -# teamai-cli v0.16.8 — 团队 AI 知识飞轮 MVP - -本版本实现了团队 AI 知识飞轮的最小闭环:**知识库初始化 → 智能检索 → 贡献反馈**,团队成员从安装到受益只需 5 分钟。 - ---- - -## 快速开始(5 分钟上手) - -```bash -# 安装(内部) -tnpm install -g @tencent/teamai-cli - -# 安装(公网) -npm install -g teamai-cli - -# 初始化(首次,配置团队仓库) -teamai init --repo <团队仓库地址> - -# 同步团队知识到本地(部署 recall agent + 构建索引 + 注入 hooks) -teamai pull - -# 开始编码 — 一切自动生效,无需额外操作 -# recall agent 自动在每个任务前检索团队知识库 -# hooks 自动追踪使用情况和贡献判断 -``` - ---- - -## 核心能力详解 - -### 知识库初始化(Cold Start / import) - -**本地文档导入** - -```bash -teamai import --dir ./docs # 扫描本地 Markdown 文档 -teamai import --from-claude # 导入 Claude/Cursor 规则目录 -teamai import --workspace # 从当前 git 仓库生成 codebase.md -``` - -**远程仓库导入** - -```bash -teamai import --from-repo # 单仓导入 -teamai import --from-repo-list .teamai/repos.yaml # 批量导入 -``` - -**组织级一键初始化** - -```bash -teamai import --from-org <工蜂 group ID> # 工蜂 group -teamai import --from-org # GitHub org -teamai import --from-org --bootstrap # 含交互式域确认 -``` - -- AI 自动聚类仓库为业务域,生成 `domains.yaml` -- 产出仓库白名单 + 业务域字典 + 全量 codebase 文档 -- 认证三层兜底:token → SSH → public - -**iWiki 导入** - -```bash -teamai import --from-iwiki # 需要 TAI_PAT_TOKEN -``` - -**MR 历史提炼** - -```bash -teamai import --from-mr -``` - -从合入的 MR 提取 learning 草稿(confidence: 0.85)和 codebase 更新建议,自动与近期 session learnings 去重。 - -**增量同步** - -```bash -teamai import --from-repo-list .teamai/repos.yaml --incremental -``` - -SHA 未变化时跳过 AI 扫描;章节级 diff,只覆写变化部分,保留人工批注。 - ---- - -### 检索 Subagent(自动可用) - -`teamai pull` 后自动部署 `teamai-recall` subagent。主对话通过 Agent tool 自动调用,CLAUDE.md 注入触发规则,无需手动操作。 - -**检索范围** - -| 知识类型 | 说明 | -|---------|------| -| Learnings | 团队成员贡献的经验总结 | -| Docs | 团队文档 | -| Rules | 编码规则 | -| Skills | 技能/Slash Commands | -| Codebase | `import --from-repo` 产物 | - -**智能增强** - -- Domain 推断加权:technical > neutral > ops > support -- IDF 权重 + Vote 加分排序 -- 错误自动检索:Bash 报错时 hook 自动触发知识库搜索 - -**手动检索** - -```bash -teamai recall "k8s pod crashloop" -``` - ---- - -### 贡献反馈(contribute-check) - -Session 结束时,Stop hook 自动判断是否值得贡献: - -- 知识库空白感知:recall 均未命中(`hitCount=0`)时触发更强贡献提示 -- 低覆盖感知:命中质量低(`topScore < 5.0`)时温和引导 -- git commit 检测:有 commit 的 session 降低触发权重,避免与 MR learning 重复 - -**贡献流程** - -``` -/teamai-share-learnings # AI 生成经验总结 - → teamai contribute --file # 推送到团队仓库 learnings/ 目录 - → 下次 teamai pull 时所有成员可检索到 -``` - -推送失败时自动本地 commit 保护数据,下次 pull 时重试。 - ---- - -### MR 驱动知识沉淀(P4.4) - -每次 MR 合入后双路输出: - -- learning 草稿(confidence: 0.85,已过 code review) -- codebase.md 变更建议(新服务 / 接口变更 / 架构决策) - -SessionStart hook 自动提醒最近合入但未导入的 MR。 - -```bash -teamai import --from-mr -``` - ---- - -## 团队级 Codebase 知识库 - -### 业务域字典 - -域配置存储于 `.teamai/domains.yaml`,支持一键初始化: - -```bash -teamai import --from-org --bootstrap -``` - -AI 聚类仓库为业务域(如"接入层"、"后端服务"、"基础设施"),CLI 交互确认后写入 `domains.yaml`。 - -### 单仓导入 - -```bash -teamai import --from-repo # 不指定域 -teamai import --from-repo --domain 接入层 # 显式指定域 -``` - -### 批量导入 - -```bash -teamai import --from-repo-list .teamai/repo-whitelist.yaml -teamai import --from-repo-list .teamai/repo-whitelist.yaml --concurrency 5 -``` - -默认并发 3 仓,产物按域拆分为 `docs/codebase/domain-*.md`。 - -### 增量同步 - -```bash -teamai import --from-repo-list .teamai/repo-whitelist.yaml --incremental -``` - -仅重扫变更涉及的模块,并自动检测域漂移。 - ---- - -## 运维与治理 - -### 缓存管理 - -```bash -teamai cache --status # 查看缓存状态 -teamai cache --gc # 清理过期缓存(LRU,默认 5GB 上限) -teamai cache --gc --dry-run # 预览清理结果,不实际删除 -``` - -### Codebase 健康度 - -```bash -teamai codebase --lint # 检查文档一致性 -teamai codebase --lint --fix # 自动修复低风险问题 -teamai codebase --lint --severity high # 只报高级别问题 -teamai codebase --lint --json # 机器可读输出(供 CI 消费) -``` - -### Pending Review - -```bash -teamai review # 列出待审核项 -teamai review --apply # 接受并应用 -teamai review --reject # 拒绝 -teamai review --all-apply --max-risk low # 批量接受低风险项 -``` - -### 域漂移 - -```bash -teamai domains drift # 列出漂移建议 -teamai domains drift --apply # 接受指定仓库的重分类 -teamai domains drift --apply-all --threshold 0.8 # 自动应用高置信项 -``` - ---- - -## 会话 Dashboard & Analytics - -```bash -teamai stats # 本地 skill 使用统计 -teamai dashboard # 启动 Web UI(默认端口 3721),实时查看团队编码会话 -teamai digest # 生成团队周活动摘要 -teamai save-session # 保存当前会话摘要 -``` - -- `teamai track` / `teamai track-slash`:hook 自动调用,追踪工具使用事件 -- `teamai dashboard-report`:hook 自动上报会话状态 -- 数据存储路径:`~/.teamai/usage.jsonl` - ---- - -## 支持的 AI 编码工具 - -| 工具 | Recall Agent | Hooks | CLAUDE.md 规则注入 | -|------|:---:|:---:|:---:| -| Claude Code | ✅ | ✅ | ✅ | -| CodeBuddy | ✅ | ✅ | ✅ | -| Cursor | ✅ | ✅ | — | -| Codex | ✅ | ✅ | — | -| OpenClaw | ✅ | — | — | -| WorkBuddy | ✅ | — | — | - ---- - -## 管理员功能 - -```bash -# 角色管理 -teamai roles init / add / remove / set - -# 标签管理 -teamai tags add / remove / subscribe / unsubscribe - -# 跨团队源 -teamai source add / remove / browse - -# 环境变量 -teamai env add / remove / list - -# 成员管理 -teamai members list -``` - ---- - -## 已知限制与注意事项 - -- `--from-org` 需要认证:工蜂使用 `~/.netrc` 或 `gf auth login`;GitHub 使用 `GITHUB_TOKEN` 或 `gh auth login` -- `--from-iwiki` 需要设置环境变量 `TAI_PAT_TOKEN` -- AI codebase 扫描大仓库可能需要数分钟,超时上限为 12 分钟 -- 搜索索引中 codebase 条目的 Score 可能显示为 NaN,不影响排序,后续版本修复 -- Phase 3(Vote 双计数器)和 Phase 4(置信度维护)将在后续版本实现 -- 工蜂的 `--from-org` 支持已通过 TGit listOrgRepos 实现 - ---- - -## 反馈渠道 - -- 内部群聊 / issue 跟踪 -- `teamai doctor` 可自助诊断配置问题 From c26bf4fd41e66260b5553613414d28aa8c2e222f Mon Sep 17 00:00:00 2001 From: m0Nst3r873 Date: Wed, 24 Jun 2026 11:06:51 +0800 Subject: [PATCH 3/3] docs: update Chinese README with CI integration section Add `teamai ci extract-mr` command to the command table and add the CI Integration section (quick start, templates, reject interaction) to match the English README updated in PR #39. --- README.zh-CN.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/README.zh-CN.md b/README.zh-CN.md index a3a1fb2..c4ae919 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -89,6 +89,7 @@ CLI 会根据用户传入的 repo URL 自动选择 provider: | `teamai domains drift [url] [--apply \| --lock \| --apply-all]` | 浏览并处理域漂移信号;`--apply` 把仓库重新归类到推荐域并刷新聚合视图 | | `teamai digest` | 生成团队 AI 使用周报(skill 排行、新增/更新 skill、session 摘要) | | `teamai hooks` | 管理 AI 工具 hooks(list / inject / remove) | +| `teamai ci extract-mr --url [--mode comment\|write\|both] [--individual-comments]` | CI 流水线集成:从 MR/PR 中提取知识,发布为评论,合并后写入团队知识仓库。使用 `--individual-comments` 时每条建议单独发布,支持 reaction/reject 交互(GitHub 👎 / TGit ☝️) | | `teamai uninstall [--force]` | 卸载 teamai:移除 hooks、rules、skills、env、docs、~/.teamai/ | | `teamai doctor` | 诊断配置问题 | @@ -361,6 +362,42 @@ npm update -g teamai-cli # 或手动触发 npm 升级 用户级 `updatePolicy` 始终优先于团队级 `autoUpdate`。 +## CI 集成 + +TeamAI 可以集成到 CI 流水线中,从每次 MR/PR 自动提取知识: + +``` +MR 创建/更新 → CI 提取 learning + codebase 建议 → 以评论形式发布 + → Reviewer 拒绝不需要的建议(GitHub 👎 / TGit ☝️) + → MR 合并 → CI 将已通过的条目写入团队知识仓库 +``` + +### 快速开始 + +```bash +# Comment 模式:将建议发布到 MR(在 PR 打开/更新时运行) +teamai ci extract-mr --url "$MR_URL" --mode comment --individual-comments + +# Write 模式:将已通过的条目写入知识仓库(在合并后运行) +teamai ci extract-mr --url "$MR_URL" --mode write --team-repo ./team-repo --individual-comments +``` + +### CI 模板 + +`examples/ci/` 目录下提供了开箱即用的模板: + +| 文件 | 平台 | +|------|------| +| `github-actions-mr-extract.yml` | GitHub Actions | +| `coding-ci-mr-extract.yaml` | Coding CI(TGit + 智研 QCI) | + +### 拒绝交互 + +| 平台 | 拒绝方式 | 默认行为 | +|------|---------|---------| +| GitHub | 对建议评论添加 👎 reaction | 全部写入 | +| TGit | 对建议 note 添加 ☝️ emoji | 全部写入 | + ## 许可证 [MIT](LICENSE)