Community写作与编辑github.com

ChanMeng666/slack-waterline-skill

读取 Slack 工作区中的每条消息,确保没有遗漏任何内容 — 一款 Claude Code 技能,帮你补上进度、每日/每周摘要直接 DM 给你,并草拟回复。零依赖,一次安装支持多个工作区。

slack-waterline-skill 是什么?

Slack Waterline 技能专为希望掌握 Slack 工作区所有动态但时间有限的开发者设计。它模拟人类阅读方式:扫描每个频道、直接消息、线程回复、置顶消息、附件以及 Google Sheets 链接,然后记录已读内容,确保下次运行从上一次结束的地方开始。该技能支持 Claude Code,核心原则是“已读回执仅描述已送达的内容,而非存在的内容”,通过 state.json 追踪最新的读取时间戳(watermarkTs)和扫描时间戳(scannedTs),防止重复获取和遗漏。它提供发现、获取、渲染、记录、审计和报告等模块化脚本,用户可通过 catchup.mjs 按工作区、时间范围或频道类型进行阅读,通过 record.mjs 记录阅读内容,通过 audit.mjs 和 coverage.mjs 双重验证完整性。技能强调安全,要求通过剪贴板而非命令行传递令牌,并支持多个工作区并行管理。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add ChanMeng666/slack-waterline-skill

Installed? Explore more 写作与编辑 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

Slack Waterline

如同一个有时间的人那样阅读 Slack 工作区:每个频道、每个 DM、每个线程回复、置顶消息、附件,以及人们经常链接的 Google Sheets。然后精确记录已读内容——并且只记录已读内容——这样下一次运行就从这里结束的地方开始。

Waterline 是一条线,在这条线以下,没有任何东西会遗漏。 此技能中的所有内容都是为了确保这条线是诚实的。

唯一规则

已读回执只能描述已送达的内容,而非已存在的内容。

获取不等于阅读。如果一个负载已构建但无人查看,它就不算已读,将其记录为已读并不会暂时丢失一条消息——而是会永久丢失,因为下一次运行会找不到任何东西。

以下所有内容都源于此。


从零开始设置

如果任何内容未配置——没有工作区、没有令牌、缺少作用域——首先运行诊断工具并按照其提示操作。 不要猜测状态。

cd skills/slack-waterline/scripts
node setup.mjs                       # 或 --workspace acme

它会检查 Node、工作区、令牌、每个作用域以及是否已读取内容,然后在第一个真正的阻碍处停止,并打印清除它所需的确切命令或点击。每次修复后再次运行。完整教程请参考 references/setup.md。

全新工作区的正常顺序:

node init.mjs  --workspace acme --dir /path/to/acme-slack-archive
node setup.mjs --workspace acme --manifest --out slack-app.yaml
# 用户根据该清单创建应用并点击复制令牌
node setup.mjs --workspace acme --token-from-clipboard
node setup.mjs --workspace acme       # 确认;然后发现并补上进度

清单很重要:它一次性设置所有十二个作用域,而不是让用户去勾选复选框,并且将它们置于用户令牌作用域下,否则产生的令牌无法看到任何 DM。

一个技能,多个工作区。 注册表位于 ~/.slack-waterline/config.json,仅保存路径;令牌存储在每个工作区自己的 .env 文件中。node init.mjs --list 显示已注册的内容。

切勿亲自处理令牌

已生效的令牌绝不能进入终端、shell 历史记录或聊天记录。

它只需从 Slack 的页面传送到一个文件中,因此通过剪贴板移动:

node setup.mjs --workspace acme --token-from-clipboard

这将验证它、写入它、向 Slack 确认它,然后仅打印它属于哪个团队和用户。切勿接受或传递令牌作为命令行参数,切勿要求用户将其粘贴到聊天中,切勿亲自将其写入文件。

在浏览器中进行设置引导

当用户要求在 Chrome 中逐步引导时,references/setup.md 提供了详细的步骤和准确的工具调用。其形式:

  1. 先生成清单,这样在需要之前就拥有了 YAML。
  2. 在一个 ToolSearch 调用中加载浏览器工具,然后在创建标签页之前使用 tabs_context_mcp。
  3. 引导:根据清单创建应用 → 安装 → 进入 OAuth 与权限页面。
  4. 在点击“允许”之前,将作用域列表读回给用户。这是他们的工作区和他们的同意。
  5. 在令牌处停止。 要求他们点击复制,然后运行 setup.mjs --token-from-clipboard。

不要从页面上读取令牌——不要使用 read_page、get_page_text、find、javascript_tool 或针对该字段的截图,也不要点击显示来取消屏蔽。Slack 默认屏蔽令牌;捕获它会将实时凭据写入到会超出会话寿命且更难撤销的聊天记录中。第 5 步的存在就是为了避免这种情况。

切勿接触客户端密钥、签名密钥或应用级令牌——此技能不需要它们。如果用户已注销,要求他们登录;切勿操作登录表单。

令牌

SLACK_USER_TOKEN (xoxp-)SLACK_BOT_TOKEN (xoxb-)
公共频道你能看到的所有频道,无需加入仅已加入的频道
私有频道你所在的每个频道仅被 /invite 的频道
DM / 群组 DM你所有的永远不能
线程、置顶、书签、文件可以可以

用户令牌首选(只要存在)——用于读取时它是严格的超集。机器人令牌无法看到任何 DM,因此对于个人进度补全工具来说,它几乎毫无用处。

用户令牌作用域:channels:history channels:read groups:history groups:read im:history im:read mpim:history mpim:read users:read files:read pins:read bookmarks:read。仅当需要发送摘要 DM 和回复时才添加 chat:write。

请参阅 references/tokens-and-scopes.md 了解如何生成一个。

两个无法修复的盲点

直接说明,而不是承诺不存在的覆盖范围:

  1. 其他两人之间的 DM。 需要 Slack 的 Discovery API——仅限 Enterprise Grid。
  2. 没有任何持有令牌的用户所在的私有频道。 人类必须 /invite 某人。conversations.join 直接拒绝私有频道。

补上进度——正常路径

node catchup.mjs --workspace acme --unread

这将运行分诊、获取所有可操作内容、渲染每个对话,并打印一个已渲染增量的索引以及一个准备好的记录批次。

然后你阅读它们。 catchup.mjs 特意不记录任何内容。

  1. 读取索引中列出的每个已渲染的 .md 文件。对于大量积压,可以将子代理分配到批次中——每个子代理仅报告发现,绝不将原始消息文本带回主上下文。
  2. 从 tmp/record-batch.json 中删除任何你未打开其增量的条目。
  3. 在理解尚清晰时记录你阅读的内容并附上摘要:
node record.mjs --workspace acme --batch tmp/record-batch.json
node record.mjs --workspace acme --from tmp/<id>.json \
  --digest "Sprint scope settled Tuesday. Open: Olga's PR needs review."

摘要是沉淀物。水位标记阻止下一次运行重新获取旧消息;摘要阻止它重新推导它们的含义。

  1. 证明没有隐藏的内容:
node audit.mjs --workspace acme
node coverage.mjs --workspace acme --enumerate-only

限定阅读范围

node catchup.mjs --workspace acme --all              # 每个对话
node catchup.mjs --workspace acme --since 7d         # 一个时间窗口
node catchup.mjs --workspace acme --channels general,eng-backend
node catchup.mjs --workspace acme --dms-only
node catchup.mjs --workspace acme --all --full       # 首次归档遍历

层级

层级 A — discover.mjs 扫描整个工作区并为每个对话打印一行,绝不包含消息正文,因此模型在一次小读取中就能了解工作区的结构。列:新消息数、优先级、操作。

层级 B — fetch.mjs → render.mjs 打开需要的内容。

使用 render.mjs 读取负载。切勿使用 head、tail 或 jq ——这些命令会给你一个 JSON 文件的前缀,但这并非对话的前缀,并且它们会静默地丢弃线程回复。

render.mjs 有意地输出排序:先显示之前的摘要,然后显示置顶(规范的,无论水位标记如何总是获取),然后是书签,然后是运行表和文档,最后是新的消息。文档优于聊天——决定通常出现在电子表格中,而频道只是人们在讨论它。


读取状态——三个位置,每个一个写入者

state.json,以频道 ID 为键,因为频道 ID 在重命名后仍然有效。

字段含义唯一写入者
watermarkTs已读——送达给读者的最新顶层时间戳record.mjs --from
scannedTs分诊——分诊评分的最新时间戳discover.mjs,仅限空行
pendingTsSlack——分诊无法清除的行上 Slack 持有的内容discover.mjs
readAt该 实际负载已送达record.mjs --from
readAtSource缩小该声明的附带说明--read-source

如果你添加一个写入者:它要么移动 scannedTs,要么向读者传递内容。绝不能两者都做或都不做。

在更改 lib/state.mjs 中的任何内容之前,请阅读 references/read-state.md,其中解释了每个字段存在的原因,以及在此之前哪些内容未被读取。

线程是消息隐藏的地方

回复永远不会移动其父级。 一个频道如果整个实时对话都发生在一个较旧的线程内,从顶层看起来会完全静默。这就是为什么 state.json 存储每个线程的 replyCount + latestReplyTs,以及为什么一行可以显示 incremental (thread replies)——用眼睛检查该对话不会显示新内容;你必须打开线程。

信号门决定优先级,绝不决定是否已读

分数对表格进行排序。它们永远不会使一行变得确定。一个安静的频道最终说了重要的事情时,不能因为它的分数是 1 就被跳过。


两个失效方式不同的关口

每次都运行两个。

node audit.mjs --workspace acme                      # 离线,瞬间
node coverage.mjs --workspace acme --enumerate-only  # 网络,几秒

一个遍历记录的关口只能确认记录是自洽的。

audit.mjs 遍历 state.json——因此一个从未被列举过的对话对它来说是不可见的。不是“干净”,而是不可见。coverage.mjs --enumerate-only 通过 conversations.list 并设置 exclude_archived: false 来问另一个问题,而遗忘的频道就藏在这里。

积压不是构建失败。这些命令会以发现的数量退出,以便脚本可以分支;而一个定时运行如果因为有人在 Slack 上发帖就变红,会让所有人忽视红色。


报告

node report.mjs --workspace acme --period day
node report.mjs --workspace acme --period week --send

章节:需要回复、提到你但你已经回复、上下文以及此摘要无法看到的内容。--send 会通过 DM 发送给你,如果内容较长,会分块并作为线程发送。

报告是视图,不是已读。 report.mjs 从不触碰 state.json。在摘要中被告知一条消息与阅读了该对话是不同的,将两者混为一谈就是积压在无人清除的情况下消失的方式。报告在其自己的页脚中说明了这一点。

调度:assets/schedule-task.ps1 注册 Windows 计划任务;assets/github-workflow.tmpl 是云端等效项。任何自动运行都会向分诊传递 --no-record——机器绝不能推进读取位置。


向 Slack 写入

send.mjs 以你的身份发布,而不是以机器人身份。两个不可选的护栏:

  1. 始终先进行 --dry-run,并在发送前向用户显示已解析的目标和准确的文本。 切勿自行主动发送。
  2. Slack 没有撤回功能。 在 API 调用之前和之后,每次尝试都会记录到 sent.log.jsonl 中。
node send.mjs --workspace acme --to '@Saba' --file draft.md --dry-run
node send.mjs --workspace acme --to '#general' --text '…' --thread 1785…
node send.mjs --workspace acme --to '@me' --text '…'

--update <ts> 原地编辑消息并需要 --reason,该原因会写入日志;dry-run 会在替换文本上方打印当前的远程文本,以便你看到正在覆盖的内容。含糊的 @Name 会被拒绝,绝不会猜测。

草拟回复

首先读取线程(fetch.mjs + render.mjs),将草稿写入文件,向用户展示,然后发送。将草稿写入工作区目录,而不是技能仓库。


附件、表格、文档

node download-file.mjs F0ABC123 out/deck.pdf --workspace acme
node extract-docx.mjs out/bio.docx
node fetch-sheet.mjs 'https://docs.google.com/spreadsheets/d/…' --workspace acme

链接中的 gid 不是作用域。

#gid= 片段记录的是某人复制 URL 时其浏览器恰好显示的标签页。fetch-sheet.mjs 读取每个标签页,仅将链接的那个排在前面。使用时有意地用 --tab 缩小范围,切勿意外。

当响应是 HTML 页面而非数据时,download-file.mjs 和 fetch-sheet.mjs 都会明确拒绝——Slack 和 Google 都会返回带有 HTTP 200 的登录页和错误页,因此一个天真的阅读器会将登录页面以 .pdf 名称写入磁盘并称之为成功。


归档

node fetch.mjs --workspace acme --many ids.txt --out-dir raw/ --skip-existing
node archive.mjs --workspace acme

使用 Markdown,而非 JSON——一个搜索谁同意了什么事的人不应该需要解析任何东西。机械消息会被丢弃并计数。conversations/ 每次运行都会被清空并重建,因为重命名或新的日历年度会留下孤立文件,而 grep 会找到两个副本却无法说明哪个是当前的。

--skip-existing 是可恢复性,但在过时的列表上它是一个陷阱:每个过时的负载都已经完整,因此该标志使得那次运行完全没有任何操作。仅用于“新对话”遍历。


你在此添加任何内容时的约定

  • 标准输出是负载;诊断信息发送到标准错误。 fetch.mjs 向标准输出写入 JSON。共享该通道的日志记录器是在等待发生的错误。
  • 使用 --out,而不是 >。 Shell 重定向会在脚本运行之前截断目标,因此失败的获取会破坏它正在替换的负载。
  • 原子写入。 临时文件,然后重命名。只写了一半的清单会声称某些内容已被读取。
  • 散文内容通过文件传递。 --digest-file、--reason-file、--batch:PowerShell 会加倍引用引号,而 bash 不会,并且文件根本没有引用规则。
  • 拒绝模糊性。 读取错误人的 DM 是不可恢复的。

此技能不做什么

  • 不自行主动发布任何内容。每次写入都会先显示。
  • 不为无人阅读的内容推进读取位置。
  • 不因对话分数低就标记为已解决。
  • 不声称覆盖上述两个盲点。
  • 不复制密钥。切勿将凭据、访问代码或私密邀请链接移出 Slack。

相关技能