团队自动日报 — 架构图scripts/ops/daily-report/

v5 — Gitea workflow + 中心化 SSH 拉取 每天 Beijing 00:05 压缩 ~1000× (raw → LLM input) Claude Code + Codex CLI 双数据源 (follow-up PR)

核心目的:不是汇报,是从每天每个团队成员跟 AI Coding CLI 的对话里挖工作流可优化点。

§总流程图

高层概览(6 个阶段)

STEP ①
触发
Gitea cron
Beijing 00:05
Pin to dedicated-runner-1
STEP ②
Dispatcher
run_all.py
parse users config
per-user 循环
STEP ③
SSH 拉数据
opt-out gate
Claude + Codex
tar stream
STEP ④
压缩管线
7 阶段 + A 兜底
无 LLM
压缩 ~1000×
STEP ⑤
LLM 生成
Haiku 4.5
~106 KB prompt
~50 秒 / 2 KB md
STEP ⑥
落地
git rolling 分支
+ Gitea 每周 issue
append comment

数据流细节

flowchart LR cron["① Gitea cron
00:05 Beijing"] pin["Pin to
dedicated-runner-1"] dispatch["② run_all.py
per-user loop"] optout{"opt-out?"} skip(["跳过该 user"]) ssh["③ SSH
tar stream"] compress["④ extract_signals.py
7 阶段压缩 + A 兜底"] gather["gather.sh
commits/PR/learnings"] prompt["⑤ run.py
组装 prompt"] haiku["claude --print
Haiku 4.5"] md["markdown 日报"] git["⑥a git push
rolling 分支"] issue["⑥b Gitea
聚合 issue"] cron --> pin --> dispatch --> optout optout -->|存在| skip optout -->|不存在| ssh ssh --> compress compress --> prompt gather --> prompt prompt --> haiku --> md md --> git md --> issue classDef trigger fill:#1e3a8a,stroke:#60a5fa,stroke-width:2px,color:#dbeafe classDef gate fill:#713f12,stroke:#fbbf24,stroke-width:2px,color:#fef3c7 classDef compute fill:#14532d,stroke:#4ade80,stroke-width:2px,color:#dcfce7 classDef llm fill:#831843,stroke:#ec4899,stroke-width:2px,color:#fce7f3 classDef land fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#e0e7ff class cron,pin trigger class optout,skip gate class compress,gather,prompt compute class haiku,md llm class git,issue land

压缩管线细节(STEP ④ 展开)

flowchart LR raw["raw jsonl
~110 MB"] s2["阶段 2
type + 日期过滤
~3.7×"] sA["★ 阶段 A
session 元统计
(基于完整 turn)
兜底未知"] s3["阶段 3
字段裁剪
~6×"] s4["阶段 4
敏感词丢"] s5["阶段 5
L2 信号筛
~150×"] s6["阶段 6
去重"] s7["阶段 7
±3 邻域"] out["~100 KB
进 LLM
(元统计 + 信号点)"] raw --> s2 s2 --> sA s2 --> s3 --> s4 --> s5 --> s6 --> s7 sA --> out s7 --> out classDef rule fill:#14532d,stroke:#4ade80,stroke-width:2px,color:#dcfce7 classDef warn fill:#7f1d1d,stroke:#f87171,stroke-width:2px,color:#fee2e2 classDef bonus fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fef3c7 classDef io fill:#312e81,stroke:#818cf8,stroke-width:2px,color:#e0e7ff class s5 rule class s4 warn class sA bonus class raw,out io

§① 触发 — Gitea workflow + Runner Pin

定时 + Runner 选择

Workflow 文件
.gitea/workflows/daily-report.yml
cron
'5 16 * * *' UTC = Beijing 00:05("刚结束的当天")
runs-on
ubuntu-latest + Pin step 强制 dedicated-runner-1
手动触发
workflow_dispatch:date / only-user / dry-run 三参
关键 env
TZ=Asia/Shanghai + Gitea secrets (DAILY_REPORT_USERS_CONFIG / AI_REVIEW_GITEA_TOKEN)

Pin 机制:本仓库 ubuntu-latest 匹配多台 runner(dedicated-runner-1 online + workspace-runner offline),yaml 层无法 pin 机器名 → 加 hostname -s 检查 step,调度到非 dedicated-runner-1 立刻 exit 1,避免静默跑空。

§② Dispatcher — run_all.py 中心调度

用户解析 + fail-soft 循环

入口
scripts/ops/daily-report/run_all.py
config 来源(优先级)
1. --users-file  →  2. env DAILY_REPORT_USERS_CONFIG  →  3. users.local.json
JSON 格式
[{"name", "email", "ssh_host"}]workspace_path 已删,扫整个 projects/)
错误处理
fail-soft:单 user 失败 log + skip + 继续;总 exit 0 除非全失败

§③ 数据拉取 — SSH + opt-out gate

远端两步:opt-out 检查 + tar stream

opt-out 在远端第一步执行(PR #499 ai-review 抓的 risk 已修),命中即 exit 0,jsonl 一行不流回 runner:

[ -f "$HOME/.claude-insight-optout" ] && { echo OPTED_OUT >&2; exit 0; }

双数据源 find(mtime ≥ DATE 00:00,无上界)

Claude Code
find ~/.claude/projects/*/ \
  -maxdepth 2 -name '*.jsonl' \
  -newermt "$DATE 00:00:00"

扫全部项目(含 worktree / slot 路径)—— PR #499 的 workspace_path 限定已废弃。

Codex CLI
find ~/.codex/sessions/*/*/*/ \
  -name 'rollout-*.jsonl' \
  -newermt "$DATE 00:00:00"

Codex 按 YYYY/MM/DD 路径分目录,但跨天 turn 写在原文件 → 跟 Claude 同款 mtime 筛即可。

跨天处理:mtime ≥ DATE 00:00 自动包含跨午夜 session(mtime 在今天但 turn 时间在昨天),extract_signals.pyturn.timestamp.date() == target_date 行级精筛。

§④ 压缩管线 — extract_signals.py(无 LLM)

无 LLM 全程序代码,确定性。实测压缩比 ~1000×:110 MB raw → ~100 KB 进 LLM。

阶段 2:type 过滤 + 行级日期精筛 ~3.7×

Claude type处理原因
user / assistant保留对话主体
attachmenthook 注入(system-reminder 重复几十次)
file-history-snapshot编辑器内部状态
queue-operation队列 metadata
ai-title / permission-mode / last-promptUI / 配置 metadata
systemsubtype 都是 hook 事件

行级日期:if ts.date().isoformat() != target_date: continue —— 跨天 session 的非目标日 turn 直接弃

★ 阶段 A:session 元统计 — 兜底未知模式 先全局后局部

位置关键:阶段 A 紧跟阶段 2(type+日期过滤),**先于** 阶段 3-7 跑——基于"未压缩的完整 turn 列表"算 session-level 全局统计。如果放在阶段 7 之后,元统计会基于"已被信号筛过滤的子集",违背"兜底未知"的目的(被筛掉的 turn 的 tool/file 也不进 histogram)。

每 session 输出(跟阶段 7 的 L2 信号点并列,一起喂 LLM):

{
  "session_id": "...",
  "turn_count": 1462,
  "tool_histogram": {"Edit": 87, "Bash": 234, "WebFetch": 52, ...},
  "files_touched": {"foo.py": 23, "bar.ts": 18, ...},
  "external_apis_called": {"gitea.x.x.x": 14, ...},
  "long_assistant_msgs": [/* top 3 turn 索引 */],
  "session_duration_min": 287
}

5 类 L2 规则是白名单——只覆盖已知模式(同 file 反复改 / tool 连续报错 / 长 gap / frustration / ask cluster),未知模式(团队没列规则的)会漏:

→ LLM 在 prompt 里 先看到 全局元统计("这天调了什么工具、动了什么文件"),再看到 信号点 + 邻域上下文("这些 turn 特别值得看")—— 全局 → 局部,是 LLM 自然的认知路径。

阶段 3:字段裁剪 ~6×

tool_result.content
裁前 RESULT_HEAD_CHARS = 200 字 + 总长 + status
tool_call.input
不留全 args,只留 sig(fingerprint):
· Edit/Read/Write → name::file_path
· Bash → name::<前 80 字 command>
· Grep → name::<前 60 字 pattern>
· 其他 → name::<md5(args)[:8]>
assistant.thinking
不进 prompt

这一步压缩最猛:Bash find / 的 100 KB 输出 → 200 字。

阶段 4:敏感词整 turn 丢弃

REDACT_PATTERNS = [
    r"sk-[a-zA-Z0-9]{32,}",           # Anthropic API key
    r"ghp_[a-zA-Z0-9]{36,}",          # GitHub PAT
    r"ghs_[a-zA-Z0-9]{36,}",          # GitHub server token
    r"ffai_[A-Za-z0-9_-]{20,}",       # FFAI personal token
    r"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+",  # JWT
    r"-----BEGIN (RSA |EC |OPENSSH |)PRIVATE KEY-----",     # PEM private key
]

命中即整 turn 丢弃(不是 mask 字符串)—— 第二道保险,opt-out 是第一道。

阶段 5:L2 信号筛 — 5 类规则 ~150×

规则阈值含义
edit_repeatEDIT_REPEAT_WINDOW=10 / THRESHOLD=3同 file 10 turn 内改 ≥ 3 次 → 缺 helper / lint / 文档
tool_error_streakTOOL_ERROR_STREAK=2连续 ≥ 2 次 tool_use_error → 卡点 / 预检 step 缺失
long_gapTURN_GAP_LONG_SECONDS=1800turn_gap > 30 min → 用户思考 / 切走 / 决策点
frustration正则 FRUSTRATION_PATTERNS"不对/重做/再想想..." → 用户挑战方向 / 真实工作流痛点
ask_clusterASK_USER_QUESTION_THRESHOLD=25 turn 内 ≥ 2 次 AskUserQuestion → 决策密集 / 需求模糊

阶段 6:信号去重(按主体)

类型去重策略
edit_repeat同 file_path 取 count 最高的一次
tool_error_streak同一连续段(turn_index 差 ≤ 1)取 max streak
long_gap每 session 留 1 个最长的
frustration每 session 至多 3 条
ask_cluster每 session 1 条

实测:去重前 231 信号 → 去重后 44 信号(关键避免连续 turn 重复触发)

阶段 7:±3 turn 邻域上下文

窗口
SIGNAL_CONTEXT_TURNS = 3 → 每信号点附 7 turn(前 3 + 自己 + 后 3)
text
裁到 300 字(比 L1 更狠)
tools
只 sig 不 args
results
head 再裁到 80 字(L1 是 200)

§⑤ LLM 生成 — Haiku 4.5

组装 + 调用

模型
claude-haiku-4-5-20251001(全名 pin,不用别名)
调用方式
claude --print --output-format json(OAuth via act_runner,不带 --bare)
Prompt 来源
L2 信号 + 元统计 + gather.sh(commits / PR / .learnings)+ 模板 + system prompt
Prompt 大小
~106 KB / ~25k tokens
耗时
~50 秒 / session 批
成本
~$0.005 / user / 天 · 10 人月 ~$9
输出
markdown 日报 ~2 KB

System prompt 硬性约束:核心目的是"挖工作流可优化点"不是汇报;「对话洞察」每条 ≤ 3 行;整篇 ≤ 100 行;规则匹配翻译成人话而非原样列。

§⑥ 落地 — git + Gitea issue

8a: git 事实归档

分支
chore/daily-reports-rolling(永不 merge develop)
路径
daily-reports/<user>/YYYY-MM-DD.md
API
Gitea contents API(PUT 更新 / POST 创建)+ sha race 一次幂等重试
raw URL
跟 issue comment 链回的入口

8b: Gitea issue 讨论入口

每周聚合
每人每周一个 issue:[daily-report] user · YYYY-W##
labels
daily-report + user/<name>
每日 comment
cron append 当日 markdown + raw URL + @ 自己(push 通知)
体量
10 人 × 52 周 = ~520 issue/年(vs 每日独立 issue 的 3650/年)

§兜底未知信号 — 设计权衡 + 未来扩展

为什么需要兜底

压缩管线的核心是阶段 5 的 5 类信号筛规则——这是白名单只匹配已知模式。"团队还没意识到的模式" = 规则没列 = 信号丢失。阶段 A 的实现细节见上 §④ 压缩管线,这里讲为什么这么设计 + 未来如何继续兜底

三层兜底架构

方向机制状态
A. 全局元统计 每 session 算 tool histogram / file 热点 / API 频次 / 长 msg 索引 + 时长——基于未压缩完整 turn,跟阶段 5 白名单并行喂 LLM follow-up PR 实现
B. 随机抽样 L2 信号筛之外,随机抽 N 个非信号 turn 喂 LLM,让 LLM 看到"普通对话"判断是否真的没信号 未来扩展
C. 反馈循环 LLM 周聚合时标"哪些信号有用 / 哪些 false positive",统计调阈值,漏掉的模式补规则 攒 4-6 周数据后启动

"先全局后局部"的设计哲学

阶段 A 位置在阶段 2 之后、阶段 3 之前,理由不只是技术(基于未压缩数据),更是认知顺序:

如果阶段 A 放在阶段 7 之后(基于已被信号筛过滤的子集),元统计就漏掉了"被规则筛掉的 turn 的信息"——等于"兜底"做成了"叠加",违背设计初衷。