Slack 吃水線工具
以人類有時間時的方式讀取 Slack 工作區:每個頻道、每則直接訊息、每個執行緒回覆、已釘選的訊息、附件,以及人們一直連結的 Google Sheets。然後它精確記錄了哪些被讀取——而且只記錄那些被讀取的——因此下一次執行會從這次停止的地方開始。
吃水線就是那條線,線以下的任何東西都不會被錯過。 這個技能中的一切都在於保持那條線的真實性。
一條規則
讀取收據只能描述已傳送的內容,絕不能描述存在的內容。
擷取不算是閱讀。如果一個負載被建立但沒有人看過它,它就不算被閱讀,且記錄為已閱讀並不會暫時失去一則訊息——而是永久失去它,因為下一次執行就沒有東西可找了。
以下的所有內容都源於此。
從零開始設定
如果任何東西未設定——沒有工作區、沒有權杖、缺少權限範圍——請先執行診斷工具並按照其指示操作。 不要猜測狀態。
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
# 使用者從那個 manifest 建立應用程式,並點擊複製權杖
node setup.mjs --workspace acme --token-from-clipboard
node setup.mjs --workspace acme # 確認;然後探索 + 追趕
Manifest 很重要:它一次貼上設定所有十二個權限範圍,而不是讓使用者尋找核取方塊,並將它們放在使用者權杖範圍下,這是否則會產生一個無法看到任何直接訊息之權杖的錯誤。
一個技能,多個工作區。 位於 ~/.slack-waterline/config.json 的註冊表只保存路徑;權杖存放在每個工作區自己的 .env 檔案中。
node init.mjs --list 顯示已註冊的內容。
絕不自己處理權杖
一個有效的權杖絕不能進入終端機、shell 歷史記錄或聊天記錄。
它只需要從 Slack 頁面傳輸到一個檔案中,所以透過剪貼簿移動它:
node setup.mjs --workspace acme --token-from-clipboard
這會驗證它、寫入它、向 Slack 驗證它,並只印出它所屬的團隊和使用者。絕不接受或傳遞權杖作為命令列參數,絕不要求使用者將權杖貼到聊天中,也絕不自己將權杖寫入檔案。
在瀏覽器中引導設定
當使用者要求在 Chrome 中逐步引導時,references/setup.md 有逐步操作說明及確切的工具呼叫。形式如下:
- 先產生 manifest,這樣你在需要它之前就有了 YAML 檔案。
- 在一次
ToolSearch呼叫中載入瀏覽器工具,然後在建立分頁前使用tabs_context_mcp。 - 引導:從 manifest 建立應用程式 → 安裝 → 到達 OAuth & Permissions。
- 在點擊允許之前向使用者讀取權限範圍清單。這是他們的工作區,他們的同意。
- 在權杖處停止。 要求他們點擊複製,然後執行
setup.mjs --token-from-clipboard。
不要從頁面上讀取權杖——不要使用 read_page、get_page_text、find、javascript_tool 或針對該欄位的螢幕截圖,也不要點擊顯示來揭露它。Slack 預設會遮罩它;擷取它會將一個有效的憑證寫入比會話長壽得多的記錄中,且撤銷它比洩漏它困難得多。第 5 步的存在就是為了避免這種情況。
絕不要碰客戶端密鑰、簽署密鑰或應用層級權杖——這個技能不需要這些。如果使用者已登出,請要求他們登入;絕不要操作登入表單。
權杖
SLACK_USER_TOKEN (xoxp-) | SLACK_BOT_TOKEN (xoxb-) | |
|---|---|---|
| 公開頻道 | 你可見的所有內容,無需加入 | 僅它已加入的頻道 |
| 私人頻道 | 你所在的每一個 | 僅已被 /invite 的頻道 |
| 直接訊息 / 群組直接訊息 | 你所有的 | 從不 |
| 執行緒、釘選、書籤、檔案 | 是 | 是 |
使用者權杖在存在時是首選——對於閱讀來說,它是嚴格的超集。機器人權杖無法看到任何直接訊息,因此對於個人追趕工具來說,它本身幾乎是無用的。
使用者權杖範圍:channels:history channels:read groups:history groups:read im:history im:read mpim:history mpim:read users:read files:read pins:read bookmarks:read。僅在需要摘要直接訊息和發送回覆時才新增 chat:write。
請參閱 references/tokens-and-scopes.md 以了解如何建立一個。
兩個無法修復的盲點
明確說明這些,而不是承諾不存在的覆蓋範圍:
- 兩人之間的直接訊息。 需要 Slack 的 Discovery API——僅限 Enterprise Grid。
- 沒有持有權杖的使用者所在的私人頻道。 人類必須
/invite某人。conversations.join會直接拒絕私人頻道。
追趕進度——正常路徑
node catchup.mjs --workspace acme --unread
這會執行分類、擷取所有可操作的內容、渲染每個對話,並印出已渲染差異的索引以及一個準備好的記錄批次。
然後你閱讀它們。 catchup.mjs 刻意不記錄任何東西。
- 閱讀索引中列出的每個已渲染
.md檔案。對於大量的待辦事項,將子代理程式分配到各個批次——每個僅報告發現,絕不將原始訊息文字帶回主上下文。 - 從
tmp/record-batch.json中刪除任何你未開啟的差異條目。 - 在你記憶猶新時記錄你閱讀的內容,並附上摘要:
node record.mjs --workspace acme --batch tmp/record-batch.json
node record.mjs --workspace acme --from tmp/<id>.json \
--digest "週二確定了 Sprint 範圍。待辦事項:Olga 的 PR 需要審閱。"
摘要是沉澱物。水位標記阻止下次執行重新擷取舊訊息;摘要阻止它重新推導其含義。
- 證明沒有遺漏:
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 為鍵,因為這能經得起重新命名。
| 欄位 | 含義 | 唯一寫入者 |
|---|---|---|
watermarkTs | 已讀取——傳送給讀者的最新頂層時間戳 | record.mjs --from |
scannedTs | 已分類——分類器評分過的最新時間戳 | discover.mjs,且僅針對安靜的列 |
pendingTs | SLACK——分類器無法清除的列上,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 會以直接訊息傳送給你,如果內容較長則會分段並在第一條訊息下串接回覆。
報告是一個視圖,不是一次閱讀。 report.mjs 從不碰 state.json。在摘要中被告知一則訊息,並不等同於閱讀了那個對話,將這兩者混為一談,正是待辦事項在沒有人清除它的情況下消失的原因。報告在其自己的頁尾中說明了這一點。
排程:assets/schedule-task.ps1 註冊一個 Windows 排程工作;assets/github-workflow.tmpl 是雲端等效方案。任何自動化執行都必須將 --no-record 傳遞給分類工具——機器絕不能推進讀取位置。
寫入 Slack
send.mjs 以你的身份發文,而不是以機器人的身份。兩個非選擇性的防護措施:
- 始終先執行
--dry-run,並在發送前向使用者顯示已解析的目標和確切的文字。 絕不自行主動發送。 - 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,這會寫入日誌;乾執行會在上述替換文字之前顯示當前的遠端文字,以便你看到你要覆寫的內容。模糊的 @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 刻意縮小範圍,而不是意外地。
download-file.mjs 和 fetch-sheet.mjs 在回應是 HTML 頁面而不是資料時會大聲拒絕——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——一個正在 grep 誰同意了什麼的人不應該需要解析任何東西。機械式訊息會被丟棄並計數。conversations/ 每次執行都會被清除並重建,因為重新命名或新的日曆年會留下一個孤立檔案,而 grep 會找到兩個副本,卻無法說明哪個是當前的。
--skip-existing 是為了可續傳性,但對於一個過時的清單來說是一個陷阱:每個過時的負載都已經完整,因此該標誌會使那次執行完全無效。僅在「新對話」階段使用它。
你在此新增任何內容時應遵守的慣例
- 標準輸出是負載;診斷資訊傳到標準錯誤輸出。
fetch.mjs將 JSON 寫入標準輸出。共用該通道的記錄器是一個等待發生的錯誤。 - 使用
--out,絕不使用>。 Shell 重新導向會在腳本執行之前截斷目標,因此失敗的擷取會破壞它正在取代的負載。 - 原子寫入。 暫存檔,然後重新命名。一個寫了一半的 manifest 會聲稱某些東西已被讀取。
- 散文內容透過檔案傳遞。
--digest-file、--reason-file、--batch:PowerShell 會在 bash 沒有的地方加倍引號,而檔案根本沒有引號規則。 - 拒絕模糊性。 讀取錯誤的人的直接訊息是無法回復的。
這個技能不做的事
- 自行主動發佈任何內容。每次寫入都會先顯示。
- 為沒有人閱讀的內容推進讀取位置。
- 因為一個對話得分低就將其標記為已處理。
- 聲稱覆蓋了上述兩個盲點。
- 複製機密。絕不將憑證、存取碼或私人邀請連結移出 Slack。
...
[為翻譯而截斷]