Skip to content

PR-16 · Discord 与 Notion 两个采集渠道 - #178

Open
titanwings wants to merge 5 commits into
ds/15-identityfrom
ds/16-discord-notion
Open

titanwings wants to merge 5 commits into
ds/15-identityfrom
ds/16-discord-notion

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/15-identity 原因:本链每一环的内容都已包含在 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/16-discord-notionds/15-identity,1 个提交,321 insertions(+)。

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

PR-16 · Discord 与 Notion 两个采集渠道

  • 分支:ds/16-discord-notion(本地,已合并进 dot-skill-test
  • 交付:src/collect/kit.mjssrc/collect/discord.mjssrc/collect/notion.mjs
    src/parse/chat.mjs(透传 method / 凭据来源 / 认 author.username)、
    src/commands/credentialed.mjstests/collect-discord-notion.test.mjs

1. 为什么

CONTRACT.md §1 的采集命令行写着 8 个渠道,此前只实现 4 个(飞书/Slack/钉钉/X)。
未实现的两个渠道此前回 collect/planned-channel 并写明所需凭据——诚实,但仍是缺口。
这一轮把 Discord(bot token)与 Notion(内部集成 token)补上,PENDING_CHANNELS
从 4 条缩到 2 条(reddit / gmail)。

2. 三条纪律,与已有渠道一致

纪律 实现
只读 Discord 读 API 全是 GET,白名单就只列 GET;Notion 的读路径几乎全是 POST(search / 数据库查询),白名单显式列出这两个 POST 并拒绝其它一切。越权动词在建立连接之前被拒
凭据不进回执 只允许出现配置文件名(discord_config.json / notion_config.json);redact/scrub 覆盖回执的每个字段(含嵌套数组)
逐字入库 + 可派生 原始页 verbatim 落 knowledge/raw/<channel>/,同一次采集把它变成带锚点正文(Discord 交给已有的导出解析器,Notion 走段落化),一页/一页一个账本条目

分页与限流:Discord 用 before 游标(一页不足 page size 即结束),429 读 body 里的
retry_after)退避——测试断言睡的是 1500ms;Notion 用 next_cursor
两处都支持断点续采(游标写在 $DISTILLY_HOME/state/)。

3. 顺带修好的两处解析器透传

  • parseChat 现在尊重调用方的 methodcredentialed/credential_source/credential_file
    导出来的是 user-export,API 采集来的是 api-bot-token——否则采集来的页面会在账本里
    冒充用户导出。
  • 说话人取名认 REST 的 author.username(导出用 name,API 用 username):
    同一批消息无论是下载的还是采集的,人名一致(林工 而不是 u1)。
  • 顺带的一致性变化:chat 的正文文件名现在随 harvest 的 source 标签(默认取输入目录名),
    与其它格式一致;--source chat 可恢复旧名。

4. 怎么验

node --test tests/collect-discord-notion.test.mjs   # 10 条
node --test tests/*.test.mjs                        # 364 pass / 0 fail
node bin/distilly.mjs doctor                        # Planned channels: collect reddit, collect gmail

测试覆盖:两页分页(游标串接 + 逐字字节 + 锚点正文 + 一页一条目)、429 退避毫秒数、
缺凭据零写入、写动词被拒、Notion 页面 → 段落(跳过块点名)、Notion 白名单(两个 POST 放行、
POST /v1/pagesPATCH 拒绝)、页面 id 从 URL 解析与坏 id 响亮失败、空页面不算空文档、
kit 的脱敏与凭据查找(环境变量 / 配置文件 / legacy 路径 / 非法 JSON)。

5. 已知缺口

  • reddit / gmail 仍未实现(各自需要什么写在 PENDING_CHANNELSdoctor 会列出)。
  • Discord 的线程/回复关系、Notion 的数据库行查询(白名单已放行 POST /v1/databases/{id}/query,但 CLI 尚未暴露 --database-id)留待后续。
  • Discord 附件、Notion 的图片与嵌入块不anchored,按类型计入 warnings。

重建说明:本提交由会话转录重建,提交信息取自原分支(ds/16-discord-notion)。
文件内容为集成分支上的最终态,不是当时那一刻的中间态——原分支的 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