Skip to content

PR-17 · Reddit 与 Gmail:契约的采集渠道全部实现 - #179

Open
titanwings wants to merge 5 commits into
ds/16-discord-notionfrom
ds/17-reddit-gmail
Open

titanwings wants to merge 5 commits into
ds/16-discord-notionfrom
ds/17-reddit-gmail

Conversation

@titanwings

@titanwings titanwings commented Sep 15, 2026

Copy link
Copy Markdown
Owner

状态 · 2026-09-15CI 全绿(Node 20 / Node 22 / Acceptance)。本分支已并入集成分支
dot-skill-test 上的 3 个修复提交(Node 20 上跑不起来的 npm test、目标审计在 CI 里抛异常的
推送行、测试读开发机真实 home 与一条依赖大小写不敏感文件系统的断言)。

背景:本分支是重建快照——原始 165 提交的历史在一次事故中丢失,这 19 条分支是按功能重建的,
内容 = 该功能的最终状态,能恢复的提交信息已尽量恢复。它的 base 是上一条 ds/* 分支(堆叠),
不是 dot-skill-test

权威状态与已知缺口:docs/v2/STATUS.md


这是一次等价重建,不是原分支的还原。

  • 基底不是 dot-skill-test,而是 ds/16-discord-notion 原因:本链每一环的内容都已包含在 dot-skill-test 里,
    若以它为 base,diff 会是空的(原计划文档 dst-evidence/PR-BODIES/PR-PLAN.md 早已预判这一点)。
    链式 base 才能让每个 PR 只显示这一个功能的改动。
  • 提交信息:能从会话转录里恢复的原样保留;恢复不到的原提交(130 条里有 50 条,多为单文件文档提交)
    chore(<区>): 重建 …(原提交信息未记录) 标注,不冒充原文。
  • 文件内容是集成分支上的最终态,不是历史中间态。原分支的 per-commit 文件树随 /tmp 清空丢失,
    转录只保留了提交信息与每次 git add 的路径清单。
  • 自检:19 条分支从基线依次应用后,链尾树与 dot-skill-test 逐字节相同(空 diff),
    所以没有任何文件被漏掉或重复归属。
  • 原版 ds/01-node-coreds/04-prompts(丢失前已推送的幸存原件)未改动,本链的同名环节以 -rebuilt 后缀区分。

本 PRds/17-reddit-gmailds/16-discord-notion,1 个提交,313 insertions(+)。

本 PR 的提交清单
  • chore(evidence): 重建 2 个文件(原提交信息未记录)

PR-17 · Reddit 与 Gmail:契约的采集渠道全部实现

  • 分支:ds/17-reddit-gmail(本地,已合并进 dot-skill-test
  • 交付:src/collect/reddit.mjssrc/collect/gmail.mjssrc/parse/email.mjs(透传 method/凭据来源)、
    src/commands/credentialed.mjs(渠道注册 + 待实现表清空)、tests/collect-reddit-gmail.test.mjs
  • 依赖:src/collect/kit.mjs(PR-16 抽出的共享管道)

1. 结果

CONTRACT.md §1 列了 8 个采集渠道,此前 6 个。这一轮补上最后两个,
PENDING_CHANNELS 清空doctor 打印 Planned channels:none
审计的渠道行从"仍未移植"变成"契约的采集渠道全部实现"。

渠道 鉴权 只读保证 分页 归一化
collect reddit --target <subreddit|username> [--kind user] OAuth client credential(POST /api/v1/access_token,Basic 头) 白名单只有这一个 POST + 四个 GET 前缀;/api/submit/api/vote 一类在建连前被拒 after 游标(Listing 的 data.after 评论 → ISO 作者:正文 锚点段落
collect gmail [--query <搜索>] OAuth refresh token(POST /token 换 access token) 白名单只有这一个 POST + /gmail/v1/users/ 的 GET nextPageToken--max-messages 封顶 raw MIME 交给已有的邮件解析器

2. 两处值得记录的设计

  • Gmail 不重写邮件解析:取回 format=raw 的 MIME 字节,直接构造 SourceFile 交给
    parseEmail——头部 RFC 2047 解码、按段 charset、text/plain 优先回退 HTML、
    附件只登记并进 warnings,全部与本地 .eml 同一条实现。access token 中途过期时
    自动再换一次并继续(测试断言换了两次、运行未中断)。
  • Reddit 的占位符不当成发言[deleted] / [removed] / more 逐类跳过并在 warnings 点名,
    绝不把平台占位符锚成"某人说过的话"。

3. 顺带修好的一处透传

parseEmail 现在像 parseChat 一样尊重调用方的 method
credentialed/credential_source/credential_file:本地 .emllocal-file
Gmail 取回的是 api-oauth-refresh,账本条目因此不会互相冒充。

4. 怎么验

node --test tests/collect-reddit-gmail.test.mjs   # 7 条
node --test tests/*.test.mjs                      # 371 pass / 0 fail
node bin/distilly.mjs doctor                      # Planned channels: none
node scripts/audit-objective.mjs --skip-acceptance # 16/16(渠道行不再是缺口)

测试覆盖:两页 after 游标 + Basic/Bearer 两条鉴权路径 + 逐字字节 + 锚点段落、
占位符与 more 跳过、401 响亮失败且零写入、写动词被拒、Gmail 刷新流程与 raw MIME
交给邮件解析器(断言 subject / 发件人 / 正文都进了正文)、access token 过期自动重换、
凭据被拒零写入、渠道表已满。

5. 已知缺口

  • 推送与 PR 仍未执行(用户冻结);解冻后的清单在 dst-evidence/PR-BODIES/PR-PLAN.md
  • 各渠道的细粒度能力仍有取舍:Reddit 未取 submission(只有评论),
    Gmail 未处理 format=full 的结构化正文(raw 已覆盖),
    Discord 线程/回复关系、Notion 数据库行查询的 CLI 入口(白名单已放行)留待后续。
  • 采集到的语料仍受渠道本身限制(例如飞书开放平台页只有 sender.id);跨渠道身份靠
    identity.json 显式声明(PR-15)。

重建说明:本提交由会话转录重建,提交信息取自原分支(ds/17-reddit-gmail)。
文件内容为集成分支上的最终态,不是当时那一刻的中间态——原分支的 per-commit
文件树随 /tmp 清空丢失,转录只保留了提交信息与 git add 的路径清单。

原提交信息:(未记录)
dsh-agent and others added 4 commits September 15, 2026 22:59
这 19 个 PR 是堆叠的,base 是上一条 ds/* 分支,而 CI 的触发名单只有
[dot-skill-test, dot-skill, main],所以本分支的 push / PR 都不会触发工作流,
PR 页面永远显示 no checks reported。这里只改触发条件,不动任何产品代码。
CI 从建立起**没有一次是绿的**(30 次运行全部 failure),原因有两个,都不是产品
代码的问题,而是门禁自己坏了:

一、`npm test` 在 Node 20 上根本跑不起来

  "test": "node --test \"tests/*.test.mjs\""

  引号挡住了 shell 的 glob 展开,而 Node 20 的 `--test` 只接受字面路径(不支持
  glob),于是 Node 20 那条腿报 `Could not find '…/tests/*.test.mjs'` 直接退出;
  Node 22 支持 glob,所以本机永远是绿的。矩阵里同时有 20 和 22,CI 必红。

  更要命的是有一条测试**把这个坏字符串钉死了**:
  `assert.equal(manifest.scripts.test, 'node --test "tests/*.test.mjs"')`。
  断言一条命令的**文本**,不等于断言它能跑 —— 这正是它没拦住的原因。

  改成 `node --test tests/*.test.mjs`(不加引号):shell 先展开,Node 20 拿到真实
  路径;Node 22 拿到同样的路径也认。(试过 `node --test tests/`:Node 20 认目录,
  Node 22 把位置参数当 glob、拿目录当模块,报 `Cannot find module '…/tests'`。)
  新测试改为断言:命令不是裸 `node --test`、参数里没有引号、**每个参数都能对上
  真实存在的文件**。

二、目标审计在 CI 里直接崩,而不是报告

  推送行的 `rev-parse --verify --quiet` 包在会抛异常的 `git()` 里:ref 不存在时
  git 以 1 退出且无输出,于是这一行 —— 本该报告"这个分支不在任何远端上"的那一行
  —— 把整个审计抛崩(CI 日志里 stdout/stderr 全空)。CI 的 checkout 本来就没有
  `origin/dot-skill-test` 跟踪 ref,所以每次必崩。

  改动:ref 探测用不抛异常的 `gitRef()`;CI 环境里这一行记为**缺口**并说明理由
  (CI 的源码本来就来自远端,这条要求在这里无法验证),而不是假装验证过;本地
  仍严格,且把"工作树干净"这句标题真正纳入判定(此前标题这么写,检查只看
  ahead,脏工作树照样绿)。

三、验收红行不再空着理由

  visual-check 缺少 playwright 的提示打在 **stderr**,而这一行只引用 stdout,
  于是失败信息是空的。现在缺 playwright 时单独报一行并给出该设的变量。

本地核对:Node 20 与 22 都收集 383 项。

(cherry picked from commit 2c90893)
CI 终于能跑测试之后(前一个提交修好了 npm test),Node 20 与 22 两条腿都稳定
红在同一条上:

  not ok 123 - cli: --mode mcp routes to the MCP client and needs a target

本机复现的办法很直接 —— 把 `~/.colleague-skill/` 移走,这条立刻变红。原来
`credentialPaths()` 除了 `DISTILLY_HOME` 还会回落到 `~/.colleague-skill/<file>`,
而 feishu-mcp 的两条 CLI 路由测试调 `runCollectCli(..., { transport })` 时不传
env,于是 `loadCredential` 拿的是 `process.env` —— **我笔记本上那份改名前的旧
凭据**。CI 上没有那个文件,所以必红。同一个文件里"缺凭据要响亮失败"的那条测试
早就为此把 HOME 指到空目录并写了注释,这两条却漏了。

改动:
1. `tests/feishu-mcp.test.mjs` 的 sandbox 现在把凭据写进自己的 home,并带一份
   `{ DISTILLY_HOME, HOME }`;两条路由测试显式传它。移走真实旧凭据后 8/8 通过。
2. `src/collect/slack.mjs` 的 `credentialPaths()` 改为 `env.HOME ?? homedir()`,
   与 `kit.mjs` 一致:旧路径回落必须跟着被覆盖的 HOME 走。此前它无视 `env.HOME`,
   一次"隔离"的运行照样能读到真实 home 的凭据,两个渠道对"刚读的是哪个文件"
   也会给出不同答案。
3. `tests/collect.test.mjs` 里两条只会打印 `false !== true` 的断言补上现场信息
   (收据、stderr、work 目录清单)—— 这条 CI-only 失败此前从日志里根本
   看不出是"没写文件"还是"写到别处去了"。

全量:无真实旧凭据时 383/385(另 2 条是本机未提交/未推送时的审计行)。

(cherry picked from commit 2551c9b)
CI 现在给得出失败现场了,于是这条一看就清楚:

  the page was not written; receipt={… "path":"…/knowledge/raw/slack/c0123-p001.json" …}
  tree=knowledge/index.json, knowledge/raw/slack/c0123-p001.json

文件就在那里,只是名字是小写。`writeRaw` 会把名字过一遍 `slug()`,所以频道
`C0123` 落盘成 `c0123-p001.json`(收据里的 `channel_id` 仍然是 `C0123`)。断言
写的是 `C0123-p001.json`,在 macOS 上 APFS 大小写不敏感所以"通过",在 Linux
runner 上就红了 —— 又是一条"取决于在哪台机器上跑"的测试。

断言改成真实文件名,并在注释里写明为什么是小写,免得下次又被"修"回去。

(cherry picked from commit a467934)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant