# Trajectory: 团队周报 · 多仓库 git 聚合(含模型自我发现并修复日期边界 bug) 真实 E2E 会话记录(`dsh web` RPC 驱动,DeepSeek-V4-Flash,未经剪辑;从 session.history 事件流提取)。 **亮点**:挂载验证时模型自己发现 `--since` 开区间导致上周报表混入本周提交(自己写的 bug),同名 `evolve_add` 替换为 rev 2 修复,三条参数路径回归通过。 **用户输入:** > 我们组三个仓库都在这个目录里(api-server、web-app、data-pipeline)。每周五我都要写周报,汇总大家这一周各仓库的提交情况,按人和按仓库分。帮我看看这周的情况,另外这事每周都要干,你想个办法让以后一句话就能出 我先看一下三个仓库的情况,再想办法把它变成一句话就能出周报。 ### step 1 · 🔧 `bash` {"command": "pwd && ls -la /tmp/team-repos", "description": "Show working directory and list team repos"} ``` /private/tmp/team-repos total 0 drwxr-xr-x@ 5 jinqiu wheel 160 Aug 6 11:16 . drwxrwxrwt 577 root wheel 18464 Aug 6 11:16 .. drwxr-xr-x@ 6 jinqiu wheel 192 Aug 6 11:16 api-server drwxr-xr-x@ 5 jinqiu wheel 160 Aug 6 11:16 data-pipeline drwxr-xr-x@ 6 jinqiu wheel 192 Aug 6 11:16 web-app ``` ### step 1 · 🔧 `bash` {"command": "for d in api-server web-app data-pipeline; do echo \"=== $d ===\"; git -C /tmp/team-repos/$d rev-parse --is-inside-work-tree 2>&1; git -C /tmp/team-repos/$d remote -v 2>&1 | head -2; done", "description": "Check git status and remotes of each repo"} ``` === api-server === true === web-app === true === data-pipeline === true ``` ### step 2 · 🔧 `bash` {"command": "for d in api-server web-app data-pipeline; do echo \"=== $d ===\"; git -C /tmp/team-repos/$d log --oneline -5 2>&1; echo \"--- branch:\"; git -C /tmp/team-repos/$d branch -a 2>&1 | head -5; done", "description": "Show recent commits and branches in each repo"} ``` === api-server === ebbde49 fix: 支付回调幂等校验 117ee15 refactor: 登录令牌刷新拆成中间件 f3338b2 feat: 新增库存预扣接口 fc092a2 fix: 订单接口超时重试逻辑 0fe4e64 init --- branch: * master === web-app === 0774fdb feat: 购物车合并结算 4a36916 fix: Safari 上拉刷新抖动 8a20a64 feat: 订单列表虚拟滚动 b6f8614 init --- branch: * master === data-pipeline === aa1e05e fix: 时区导致的跨天统计偏差 73b36a6 feat: 用户留存日表 fffbe3b init --- branch: * master ``` ### step 2 · 🔧 `bash` {"command": "date '+%Y-%m-%d %A'", "description": "Show today's date and weekday"} ``` 2026-08-06 Thursday ``` ### step 3 · 🔧 `bash` {"command": "for d in api-server web-app data-pipeline; do echo \"=== $d ===\"; git -C /tmp/team-repos/$d log --pretty=format:'%h | %an <%ae> | %ad | %s' --date=format:'%Y-%m-%d %H:%M' 2>&1; echo; done", "description": "Full commit log with author and date for each repo"} ``` === api-server === ebbde49 | 王强 | 2026-08-05 17:40 | fix: 支付回调幂等校验 117ee15 | 李娜 | 2026-08-04 11:05 | refactor: 登录令牌刷新拆成中间件 f3338b2 | 张伟 | 2026-08-04 15:30 | feat: 新增库存预扣接口 fc092a2 | 张伟 | 2026-08-03 10:12 | fix: 订单接口超时重试逻辑 0fe4e64 | William Jin | 2026-07-20 10:00 | init === web-app === 0774fdb | 张伟 | 2026-08-06 10:00 | feat: 购物车合并结算 4a36916 | 李娜 | 2026-08-05 09:20 | fix: Safari 上拉刷新抖动 8a20a64 | 李娜 | 2026-08-03 14:00 | feat: 订单列表虚拟滚动 b6f8614 | William Jin | 2026-07-20 10:00 | init === data-pipeline === aa1e05e | 王强 | 2026-08-06 08:30 | fix: 时区导致的跨天统计偏差 73b36a6 | 王强 | 2026-08-04 09:… ``` 三个仓库、三位同事(张伟、李娜、王强)的提交记录都在。今天是 2026-08-06(周四),我先把本周(周一 08-03 起)的数据拉出来,然后把这事做成一个可持续复用的工具。 ### step 4 · 🔧 `todo_write` {"todos": [{"content": "拉取三个仓库本周(08-03 起)提交数据", "status": "in_progress"}, {"content": "生成按仓库 + 按人的周报", "status": "pending"}, {"content": "用 self-evolution 创建可复用的周报工具", "status": "pending"}, {"content": "验证工具输出与手动数据一致", "status": "pending"}]} ``` Updated todo list: 3 pending, 1 in progress, 0 completed. ``` ### step 5 · 🔧 `bash` {"command": "for d in api-server web-app data-pipeline; do echo \"=== $d ===\"; git -C /tmp/team-repos/$d log --since=\"2026-08-03 00:00\" --pretty=format:'%h|%an|%ad|%s' --date=format:'%Y-%m-%d %H:%M' 2>&1; echo; done", "description": "Get this week's commits per repo"} ``` === api-server === ebbde49|王强|2026-08-05 17:40|fix: 支付回调幂等校验 117ee15|李娜|2026-08-04 11:05|refactor: 登录令牌刷新拆成中间件 f3338b2|张伟|2026-08-04 15:30|feat: 新增库存预扣接口 fc092a2|张伟|2026-08-03 10:12|fix: 订单接口超时重试逻辑 === web-app === 0774fdb|张伟|2026-08-06 10:00|feat: 购物车合并结算 4a36916|李娜|2026-08-05 09:20|fix: Safari 上拉刷新抖动 8a20a64|李娜|2026-08-03 14:00|feat: 订单列表虚拟滚动 === data-pipeline === aa1e05e|王强|2026-08-06 08:30|fix: 时区导致的跨天统计偏差 73b36a6|王强|2026-08-04 09:00|feat: 用户留存日表 ``` 数据齐了:本周(08-03 起)共 9 个提交。现在把这个逻辑固化成一个可复用工具,以后一句话就能生成周报。 ### step 6 · 🔧 `evolve_add` {"name": "team-weekly-report", "description": "每周五周报:汇总 /tmp/team-repos 下 api-server、web-app、data-pipeline 三仓库本周/指定周的 git 提交,按仓库和按人分组输出。"}
模型现场编写的插件源码(source 参数) ```js import { execFileSync } from 'node:child_process' import { existsSync } from 'node:fs' import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'team-weekly-report' export const inject = ['tools'] export const Config = { reposRoot: { type: 'string', default: '/tmp/team-repos' }, repos: { type: 'array', items: { type: 'string' }, default: ['api-server', 'web-app', 'data-pipeline'] }, } function fmt(d) { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } function startOfWeek(d) { const x = new Date(d) x.setHours(0, 0, 0, 0) const diff = (x.getDay() + 6) % 7 // days since Monday x.setDate(x.getDate() - diff) return x } function groupBy(arr, keyFn) { const m = {} for (const item of arr) { const k = keyFn(item) ;(m[k] ||= []).push(item) } return m } function renderRepoSection(repoList, commits) { const out = [] for (const repo of repoList) { const cs = commits.filter((c) => c.repo === repo) if (!cs.length) { out.push(`\n■ ${repo} · 本周无提交`) continue } const byAuthor = groupBy(cs, (c) => c.author) const authors = Object.keys(byAuthor).sort( (a, b) => byAuthor[b].length - byAuthor[a].length || a.localeCompare(b), ) out.push(`\n■ ${repo} · ${cs.length} 提交 · ${authors.length} 人`) for (const author of authors) { out.push(` · ${author}(${byAuthor[author].length})`) for (const c of byAuthor[author]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } function renderPersonSection(commits) { const byPerson = groupBy(commits, (c) => c.author) const persons = Object.keys(byPerson).sort( (a, b) => byPerson[b].length - byPerson[a].length || a.localeCompare(b), ) const out = [] for (const person of persons) { const cs = byPerson[person] const byRepo = groupBy(cs, (c) => c.repo) const repos = Object.keys(byRepo).sort( (a, b) => byRepo[b].length - byRepo[a].length || a.localeCompare(b), ) out.push(`\n■ ${person} · ${cs.length} 提交 · ${repos.length} 仓库`) for (const repo of repos) { out.push(` · ${repo}(${byRepo[repo].length})`) for (const c of byRepo[repo]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } export function apply(ctx, config) { const root = config?.reposRoot || '/tmp/team-repos' const repos = config?.repos?.length ? config.repos : ['api-server', 'web-app', 'data-pipeline'] ctx.tools.register(defineTool({ name: 'team_weekly_report', description: `生成团队周报:汇总 ${root} 下 ${repos.join('、')} 的 git 提交,按仓库和按人分组。默认统计本周(周一开始至今),写周报时直接调用即可。`, parameters: { weekOffset: { type: 'number', description: '目标周偏移:0=本周(默认),-1=上周,1=下周,以此类推。', }, since: { type: 'string', description: '可选,直接指定统计起始日 YYYY-MM-DD(覆盖 weekOffset)。', }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { const now = new Date() let monday if (args.since) { monday = new Date(`${args.since}T00:00:00`) if (Number.isNaN(monday.getTime())) return `无效日期: ${args.since}` } else { const offset = Number(args.weekOffset) || 0 monday = startOfWeek(new Date()) monday.setDate(monday.getDate() + 7 * offset) } monday.setHours(0, 0, 0, 0) const mondayStr = fmt(monday) const weekEnd = new Date(monday) weekEnd.setDate(weekEnd.getDate() + 6) const end = weekEnd < now ? weekEnd : now const endStr = fmt(end) const sinceArg = `--since=${mondayStr} 00:00:00` const commits = [] const errors = [] for (const repo of repos) { const dir = `${root}/${repo}` if (!existsSync(dir)) { errors.push(`仓库不存在: ${dir}`) continue } let out try { out = execFileSync( 'git', ['-C', dir, 'log', sinceArg, '--pretty=format:%h|%an|%ae|%ad|%s', '--date=format:%Y-%m-%d %H:%M'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, ) } catch (e) { errors.push(`${repo}: ${String(e.stderr || e.message).trim()}`) continue } for (const raw of out.split('\n').filter(Boolean)) { const parts = raw.split('|') if (parts.length < 5) continue const [hash, author, email, date, ...rest] = parts commits.push({ repo, hash, author, email, date, subject: rest.join('|') }) } } if (!commits.length) { return `📅 ${mondayStr} 起本周暂无提交记录。\n${errors.length ? '\n⚠️ ' + errors.join('\n⚠️ ') : ''}` } const persons = [...new Set(commits.map((c) => c.author))] const reposTouched = [...new Set(commits.map((c) => c.repo))] const out = [] out.push(`📅 团队周报 ${mondayStr}(周一)~ ${endStr}`) out.push(` 共 ${commits.length} 个提交 · ${persons.length} 人 · ${reposTouched.length} 个仓库`) out.push('\n━━━ 按仓库 ━━━') out.push(renderRepoSection(repos, commits)) out.push('\n\n━━━ 按人 ━━━') out.push(renderPersonSection(commits)) if (errors.length) { out.push('\n\n⚠️ 部分仓库读取失败:') for (const e of errors) out.push(' ' + e) } return out.join('\n') }, })) } ```
``` Error: evolution "team-weekly-report" failed to mount: Cannot read properties of undefined (reading 'validate') The source is kept at /Users/jinqiu/.dsh/evolve/team-weekly-report.mjs but not persisted to the manifest — fix it and call evolve_add again with the same name. ``` 挂载失败——问题出在 `Config` 导出格式上。去掉 schemastery Config,改由 apply 内部做默认值处理。 ### step 7 · 🔧 `evolve_add` {"description": "每周五周报:汇总 /tmp/team-repos 下 api-server、web-app、data-pipeline 三仓库本周/指定周的 git 提交,按仓库和按人分组输出。", "name": "team-weekly-report"}
模型现场编写的插件源码(source 参数) ```js import { execFileSync } from 'node:child_process' import { existsSync } from 'node:fs' import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'team-weekly-report' export const inject = ['tools'] function fmt(d) { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } function startOfWeek(d) { const x = new Date(d) x.setHours(0, 0, 0, 0) const diff = (x.getDay() + 6) % 7 // days since Monday x.setDate(x.getDate() - diff) return x } function groupBy(arr, keyFn) { const m = {} for (const item of arr) { const k = keyFn(item) ;(m[k] ||= []).push(item) } return m } function renderRepoSection(repoList, commits) { const out = [] for (const repo of repoList) { const cs = commits.filter((c) => c.repo === repo) if (!cs.length) { out.push(`\n■ ${repo} · 本周无提交`) continue } const byAuthor = groupBy(cs, (c) => c.author) const authors = Object.keys(byAuthor).sort( (a, b) => byAuthor[b].length - byAuthor[a].length || a.localeCompare(b), ) out.push(`\n■ ${repo} · ${cs.length} 提交 · ${authors.length} 人`) for (const author of authors) { out.push(` · ${author}(${byAuthor[author].length})`) for (const c of byAuthor[author]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } function renderPersonSection(commits) { const byPerson = groupBy(commits, (c) => c.author) const persons = Object.keys(byPerson).sort( (a, b) => byPerson[b].length - byPerson[a].length || a.localeCompare(b), ) const out = [] for (const person of persons) { const cs = byPerson[person] const byRepo = groupBy(cs, (c) => c.repo) const repos = Object.keys(byRepo).sort( (a, b) => byRepo[b].length - byRepo[a].length || a.localeCompare(b), ) out.push(`\n■ ${person} · ${cs.length} 提交 · ${repos.length} 仓库`) for (const repo of repos) { out.push(` · ${repo}(${byRepo[repo].length})`) for (const c of byRepo[repo]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } export function apply(ctx, config) { const root = config?.reposRoot || '/tmp/team-repos' const repos = config?.repos?.length ? config.repos : ['api-server', 'web-app', 'data-pipeline'] ctx.tools.register(defineTool({ name: 'team_weekly_report', description: `生成团队周报:汇总 ${root} 下 ${repos.join('、')} 的 git 提交,按仓库和按人分组。默认统计本周(周一开始至今),写周报时直接调用即可。`, parameters: { weekOffset: { type: 'number', description: '目标周偏移:0=本周(默认),-1=上周,1=下周,以此类推。', }, since: { type: 'string', description: '可选,直接指定统计起始日 YYYY-MM-DD(覆盖 weekOffset)。', }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { const now = new Date() let monday if (args.since) { monday = new Date(`${args.since}T00:00:00`) if (Number.isNaN(monday.getTime())) return `无效日期: ${args.since}` } else { const offset = Number(args.weekOffset) || 0 monday = startOfWeek(new Date()) monday.setDate(monday.getDate() + 7 * offset) } monday.setHours(0, 0, 0, 0) const mondayStr = fmt(monday) const weekEnd = new Date(monday) weekEnd.setDate(weekEnd.getDate() + 6) const end = weekEnd < now ? weekEnd : now const endStr = fmt(end) const sinceArg = `--since=${mondayStr} 00:00:00` const commits = [] const errors = [] for (const repo of repos) { const dir = `${root}/${repo}` if (!existsSync(dir)) { errors.push(`仓库不存在: ${dir}`) continue } let out try { out = execFileSync( 'git', ['-C', dir, 'log', sinceArg, '--pretty=format:%h|%an|%ae|%ad|%s', '--date=format:%Y-%m-%d %H:%M'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, ) } catch (e) { errors.push(`${repo}: ${String(e.stderr || e.message).trim()}`) continue } for (const raw of out.split('\n').filter(Boolean)) { const parts = raw.split('|') if (parts.length < 5) continue const [hash, author, email, date, ...rest] = parts commits.push({ repo, hash, author, email, date, subject: rest.join('|') }) } } if (!commits.length) { return `📅 ${mondayStr} 起本周暂无提交记录。\n${errors.length ? '\n⚠️ ' + errors.join('\n⚠️ ') : ''}` } const persons = [...new Set(commits.map((c) => c.author))] const reposTouched = [...new Set(commits.map((c) => c.repo))] const out = [] out.push(`📅 团队周报 ${mondayStr}(周一)~ ${endStr}`) out.push(` 共 ${commits.length} 个提交 · ${persons.length} 人 · ${reposTouched.length} 个仓库`) out.push('\n━━━ 按仓库 ━━━') out.push(renderRepoSection(repos, commits)) out.push('\n\n━━━ 按人 ━━━') out.push(renderPersonSection(commits)) if (errors.length) { out.push('\n\n⚠️ 部分仓库读取失败:') for (const e of errors) out.push(' ' + e) } return out.join('\n') }, })) } ```
``` Evolution "team-weekly-report" grown: active. Persisted at /Users/jinqiu/.dsh/evolve/team-weekly-report.mjs; new tools are callable from your next step. ``` 工具已挂载。现在调用它生成本周周报,验证输出与手动数据一致: ### step 8 · 🔧 `todo_write` {"todos": [{"content": "拉取三个仓库本周(08-03 起)提交数据", "status": "completed"}, {"content": "生成按仓库 + 按人的周报", "status": "completed"}, {"content": "用 self-evolution 创建可复用的周报工具", "status": "completed"}, {"content": "验证工具输出与手动数据一致", "status": "in_progress"}]} ``` Updated todo list: 0 pending, 1 in progress, 3 completed. ``` ### step 8 · 🔧 `team_weekly_report` {} ``` 📅 团队周报 2026-08-03(周一)~ 2026-08-06 共 9 个提交 · 3 人 · 3 个仓库 ━━━ 按仓库 ━━━ ■ api-server · 4 提交 · 3 人 · 张伟(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · 李娜(1) - [08-04 11:05] refactor: 登录令牌刷新拆成中间件 · 王强(1) - [08-05 17:40] fix: 支付回调幂等校验 ■ web-app · 3 提交 · 2 人 · 李娜(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:00] feat: 订单列表虚拟滚动 · 张伟(1) - [08-06 10:00] feat: 购物车合并结算 ■ data-pipeline · 2 提交 · 1 人 · 王强(2) - [08-06 08:30] fix: 时区导致的跨天统计偏差 - [08-04 09:00] feat: 用户留存日表 ━━━ 按人 ━━━ ■ 张伟 · 3 提交 · 2 仓库 · api-server(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · web-app(1) - [08-06 10:00] feat: 购物车合并结算 ■ 李娜 · 3 提交 · 2 仓库 · web-app(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:0… ``` 输出与手动拉取的数据完全一致(9 个提交、3 人、3 仓库)。再快速验证一下上周的日期边界逻辑是否正常: ### step 9 · 🔧 `team_weekly_report` {"weekOffset": -1} ``` 📅 团队周报 2026-07-27(周一)~ 2026-08-02 共 9 个提交 · 3 人 · 3 个仓库 ━━━ 按仓库 ━━━ ■ api-server · 4 提交 · 3 人 · 张伟(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · 李娜(1) - [08-04 11:05] refactor: 登录令牌刷新拆成中间件 · 王强(1) - [08-05 17:40] fix: 支付回调幂等校验 ■ web-app · 3 提交 · 2 人 · 李娜(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:00] feat: 订单列表虚拟滚动 · 张伟(1) - [08-06 10:00] feat: 购物车合并结算 ■ data-pipeline · 2 提交 · 1 人 · 王强(2) - [08-06 08:30] fix: 时区导致的跨天统计偏差 - [08-04 09:00] feat: 用户留存日表 ━━━ 按人 ━━━ ■ 张伟 · 3 提交 · 2 仓库 · api-server(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · web-app(1) - [08-06 10:00] feat: 购物车合并结算 ■ 李娜 · 3 提交 · 2 仓库 · web-app(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:0… ``` ### step 9 · 🔧 `evolve_list` {} ``` Evolutions: - currency-converter (rev 1, active): 实时汇率换算工具,支持 USD/CNY/EUR/JPY 等 150+ 币种,跨境采购报价换算直接用 - error-code-lookup (rev 1, active): 实时查询 demo-shop 错误码表(docs/error-codes.md),支持错误码或关键词检索,避免每次手动翻表。 - expense-tracker (rev 1, active): 个人记账:用户随口提到花钱(买菜/加油/吃饭/打车等)就用 expense_add 记录,自动按描述分类(买菜/吃饭→餐饮,加油/打车→交通);用户问"这个月花了多少/吃饭花了多少"时用 expense_stats 按月分类统计。数据持久化在账单文件里。 - family-reminders (rev 1, active): 家庭重要日期常驻工具:宝宝年龄(2026-03-18 出生)、结婚纪念日倒计时(5-20)、下次是谁的生日(宝宝 03-18 / 妈妈 11-02)。用户随口问"宝宝多大了/离纪念日几天/快到谁生日"时调用。 - team-weekly-report (rev 1, active): 每周五周报:汇总 /tmp/team-repos 下 api-server、web-app、data-pipeline 三仓库本周/指定周的 git 提交,按仓库和按人分组输出。 - weather-check (rev 1, active): 查任意城市天气(当前+未来1-7天,含降水概率与带伞建议),支持中文城市名和家庭成员别名(爸妈→成都、妹妹→深圳),满足"帮家里人看天气"的反复需求。 ``` 发现一个边界 bug:`--since` 是开区间过滤,上周的报表把本周的提交也带进来了(表头 07-27~08-02,却列了 08-03 之后的提交)。需要加上 `--until` 把统计范围闭合。修复后重新挂载: ### step 10 · 🔧 `evolve_add` {"description": "每周五周报:汇总 /tmp/team-repos 下 api-server、web-app、data-pipeline 三仓库本周/指定周的 git 提交,按仓库和按人分组输出。", "name": "team-weekly-report"}
模型现场编写的插件源码(source 参数) ```js import { execFileSync } from 'node:child_process' import { existsSync } from 'node:fs' import { defineTool } from '@deepseek-ai/dsh-tools' export const name = 'team-weekly-report' export const inject = ['tools'] function fmt(d) { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } function startOfWeek(d) { const x = new Date(d) x.setHours(0, 0, 0, 0) const diff = (x.getDay() + 6) % 7 // days since Monday x.setDate(x.getDate() - diff) return x } function groupBy(arr, keyFn) { const m = {} for (const item of arr) { const k = keyFn(item) ;(m[k] ||= []).push(item) } return m } function renderRepoSection(repoList, commits) { const out = [] for (const repo of repoList) { const cs = commits.filter((c) => c.repo === repo) if (!cs.length) { out.push(`\n■ ${repo} · 本周无提交`) continue } const byAuthor = groupBy(cs, (c) => c.author) const authors = Object.keys(byAuthor).sort( (a, b) => byAuthor[b].length - byAuthor[a].length || a.localeCompare(b), ) out.push(`\n■ ${repo} · ${cs.length} 提交 · ${authors.length} 人`) for (const author of authors) { out.push(` · ${author}(${byAuthor[author].length})`) for (const c of byAuthor[author]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } function renderPersonSection(commits) { const byPerson = groupBy(commits, (c) => c.author) const persons = Object.keys(byPerson).sort( (a, b) => byPerson[b].length - byPerson[a].length || a.localeCompare(b), ) const out = [] for (const person of persons) { const cs = byPerson[person] const byRepo = groupBy(cs, (c) => c.repo) const repos = Object.keys(byRepo).sort( (a, b) => byRepo[b].length - byRepo[a].length || a.localeCompare(b), ) out.push(`\n■ ${person} · ${cs.length} 提交 · ${repos.length} 仓库`) for (const repo of repos) { out.push(` · ${repo}(${byRepo[repo].length})`) for (const c of byRepo[repo]) { out.push(` - [${c.date.slice(5)}] ${c.subject}`) } } } return out.join('\n') } export function apply(ctx, config) { const root = config?.reposRoot || '/tmp/team-repos' const repos = config?.repos?.length ? config.repos : ['api-server', 'web-app', 'data-pipeline'] ctx.tools.register(defineTool({ name: 'team_weekly_report', description: `生成团队周报:汇总 ${root} 下 ${repos.join('、')} 的 git 提交,按仓库和按人分组。默认统计本周(周一开始至今),写周报时直接调用即可。`, parameters: { weekOffset: { type: 'number', description: '目标周偏移:0=本周(默认),-1=上周,1=下周,以此类推。', }, since: { type: 'string', description: '可选,直接指定统计起始日 YYYY-MM-DD(覆盖 weekOffset),统计该日起一周。', }, }, output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }], }, async execute(args) { const now = new Date() let monday if (args.since) { monday = new Date(`${args.since}T00:00:00`) if (Number.isNaN(monday.getTime())) return `无效日期: ${args.since}` } else { const offset = Number(args.weekOffset) || 0 monday = startOfWeek(new Date()) monday.setDate(monday.getDate() + 7 * offset) } monday.setHours(0, 0, 0, 0) const mondayStr = fmt(monday) const weekEnd = new Date(monday) weekEnd.setDate(weekEnd.getDate() + 6) const end = weekEnd < now ? weekEnd : now const endStr = fmt(end) const until = new Date(end) until.setDate(until.getDate() + 1) until.setHours(0, 0, 0, 0) const sinceArg = `--since=${mondayStr} 00:00:00` const untilArg = `--until=${fmt(until)} 00:00:00` const commits = [] const errors = [] for (const repo of repos) { const dir = `${root}/${repo}` if (!existsSync(dir)) { errors.push(`仓库不存在: ${dir}`) continue } let out try { out = execFileSync( 'git', ['-C', dir, 'log', sinceArg, untilArg, '--pretty=format:%h|%an|%ae|%ad|%s', '--date=format:%Y-%m-%d %H:%M'], { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, ) } catch (e) { errors.push(`${repo}: ${String(e.stderr || e.message).trim()}`) continue } for (const raw of out.split('\n').filter(Boolean)) { const parts = raw.split('|') if (parts.length < 5) continue const [hash, author, email, date, ...rest] = parts commits.push({ repo, hash, author, email, date, subject: rest.join('|') }) } } if (!commits.length) { return `📅 ${mondayStr}(周一)~ ${endStr}:本周暂无提交记录。\n${errors.length ? '\n⚠️ ' + errors.join('\n⚠️ ') : ''}` } const persons = [...new Set(commits.map((c) => c.author))] const reposTouched = [...new Set(commits.map((c) => c.repo))] const out = [] out.push(`📅 团队周报 ${mondayStr}(周一)~ ${endStr}`) out.push(` 共 ${commits.length} 个提交 · ${persons.length} 人 · ${reposTouched.length} 个仓库`) out.push('\n━━━ 按仓库 ━━━') out.push(renderRepoSection(repos, commits)) out.push('\n\n━━━ 按人 ━━━') out.push(renderPersonSection(commits)) if (errors.length) { out.push('\n\n⚠️ 部分仓库读取失败:') for (const e of errors) out.push(' ' + e) } return out.join('\n') }, })) } ```
``` Evolution "team-weekly-report" replaced (rev 2): active. Persisted at /Users/jinqiu/.dsh/evolve/team-weekly-report.mjs; new tools are callable from your next step. ``` 修复完成,重新验证本周和上周: ### step 11 · 🔧 `team_weekly_report` {} ``` 📅 团队周报 2026-08-03(周一)~ 2026-08-06 共 9 个提交 · 3 人 · 3 个仓库 ━━━ 按仓库 ━━━ ■ api-server · 4 提交 · 3 人 · 张伟(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · 李娜(1) - [08-04 11:05] refactor: 登录令牌刷新拆成中间件 · 王强(1) - [08-05 17:40] fix: 支付回调幂等校验 ■ web-app · 3 提交 · 2 人 · 李娜(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:00] feat: 订单列表虚拟滚动 · 张伟(1) - [08-06 10:00] feat: 购物车合并结算 ■ data-pipeline · 2 提交 · 1 人 · 王强(2) - [08-06 08:30] fix: 时区导致的跨天统计偏差 - [08-04 09:00] feat: 用户留存日表 ━━━ 按人 ━━━ ■ 张伟 · 3 提交 · 2 仓库 · api-server(2) - [08-04 15:30] feat: 新增库存预扣接口 - [08-03 10:12] fix: 订单接口超时重试逻辑 · web-app(1) - [08-06 10:00] feat: 购物车合并结算 ■ 李娜 · 3 提交 · 2 仓库 · web-app(2) - [08-05 09:20] fix: Safari 上拉刷新抖动 - [08-03 14:0… ``` ### step 11 · 🔧 `team_weekly_report` {"weekOffset": -1} ``` 📅 2026-07-27(周一)~ 2026-08-02:本周暂无提交记录。 ``` 两边都正确了:本周 9 个提交,上周正确地显示无记录。最后验证一下 `since` 参数(指定起始日)的路径: ### step 12 · 🔧 `team_weekly_report` {"since": "2026-07-20"} ``` 📅 团队周报 2026-07-20(周一)~ 2026-07-26 共 3 个提交 · 1 人 · 3 个仓库 ━━━ 按仓库 ━━━ ■ api-server · 1 提交 · 1 人 · William Jin(1) - [07-20 10:00] init ■ web-app · 1 提交 · 1 人 · William Jin(1) - [07-20 10:00] init ■ data-pipeline · 1 提交 · 1 人 · William Jin(1) - [07-20 10:00] init ━━━ 按人 ━━━ ■ William Jin · 3 提交 · 3 仓库 · api-server(1) - [07-20 10:00] init · data-pipeline(1) - [07-20 10:00] init · web-app(1) - [07-20 10:00] init ``` 三个参数路径全部验证通过。收尾: ### step 13 · 🔧 `todo_write` {"todos": [{"content": "拉取三个仓库本周(08-03 起)提交数据", "status": "completed"}, {"content": "生成按仓库 + 按人的周报", "status": "completed"}, {"content": "用 self-evolution 创建可复用的周报工具", "status": "completed"}, {"content": "验证工具输出与手动数据一致", "status": "completed"}]} ``` Updated todo list: 0 pending, 0 in progress, 4 completed. ``` 搞定,两件事都办好了。 ## 一、本周周报(2026-08-03 ~ 08-06,共 9 个提交 · 3 人 · 3 仓库) **按仓库** | 仓库 | 提交数 | 人员分布 | |---|---|---| | api-server | 4 | 张伟 2(新增库存预扣接口、订单接口超时重试逻辑)、李娜 1(登录令牌刷新拆成中间件)、王强 1(支付回调幂等校验) | | web-app | 3 | 李娜 2(Safari 上拉刷新抖动修复、订单列表虚拟滚动)、张伟 1(购物车合并结算) | | data-pipeline | 2 | 王强 2(时区跨天统计偏差修复、用户留存日表) | **按人** - **张伟(3 提交 / 2 仓库)**:api-server 2 · web-app 1 - **李娜(3 提交 / 2 仓库)**:web-app 2 · api-server 1 - **王强(3 提交 / 2 仓库)**:data-pipeline 2 · api-server 1 本周三人工作量均衡,功能开发(库存预扣、购物车合并、留存日表)和修复(支付幂等、Safari 抖动、时区偏差)都有覆盖。 ## 二、以后一句话出周报 ✅ 我用 self-evolution 固化了一个常驻工具 `team_weekly_report`(已持久化,重启后依然可用,**跨会话、跨周永久有效**)。它直接读取三个仓库的 git 历史,自动按仓库和按人分组,输出上面这种格式。以后你只要说: - **"出这周周报"** → 默认本周(周一开始至今) - **"上周的周报"** → 自动算上周 - **"7月20号那周的周报"** → 指定起始日,统计该日起一周 工具做了两处健壮性处理:统计范围是闭合的周区间(修掉了上周报表把本周提交带进去的边界 bug),仓库缺失或 git 失败会明确报错而不是静默出错。周五写周报时直接说一句就行,不用再手动翻 git log 了。 顺带一提:如果你的周报要发到飞书文档/表格里,我这边也有对应的 lark-* 技能,需要的话可以一键把这份报告写进飞书文档。