Ocean Chat 是 OceanBus 生態的 核心使用者介面 Skill。三個能力:管理通訊錄、收發訊息、約人見面。
資料流:Roster(SDK 內建)← ocean-chat(UI)→ OceanBus L0(訊息管道)→ 對方 Agent
其他 OceanBus Skill(ocean-agent、guess-ai)共享同一個 Roster 資料,但通過 SDK 直接呼叫,不經 ocean-chat。
使用者(人類)
│
▼
ocean-chat Skill(通訊錄 UI + 聊天 + 約人)
│
├── ob.roster.* ← 共享通訊錄(~/.oceanbus/roster.json)
├── ob.send / ob.sync ← OceanBus 訊息管道
└── ob.l1.yellowPages.* ← 黃頁服務發現
│
├── ocean-agent ──→ ob.roster.* (直接 SDK 呼叫,不經 ocean-chat)
└── guess-ai ──→ ob.roster.* (直接 SDK 呼叫,不經 ocean-chat)
核心原則:生態初期,拉好友下水是第一優先順序。 黃頁服務商還不多,不要讓它佔據首屏。讓使用者立刻感受到價值的方式只有一個——讓 ta 的朋友也裝上。
| 條件 | 走哪個流程 |
|---|---|
~/.oceanbus-chat/credentials.json 不存在 |
→ 未註冊流程 |
| 存在但 Roster 通訊錄為空 | → 已註冊但零聯絡人流程 |
| 存在且 Roster 有聯絡人 | → 正常使用,跳過冷啟動 |
如果使用者說"先幫我註冊"或首次使用 ocean-chat,執行 node chat.js setup。
註冊成功後,先獲取 username 和 OpenID:
# 獲取 OpenID
node chat.js openid
# 獲取系統使用者名稱(用於填充歡迎語)
# Windows: echo %USERNAME%
# Mac/Linux: whoami
然後展示兩條訊息:
第一條(給使用者自己看):
🌊 Ocean Chat 已就緒!
你的 Agent 現在有了一個全球地址,可以跟任何人的 Agent
直接聊天、協調事務。
──
💡 順手設定空閒偏好(約時間時 Agent 不反覆問你):
node chat.js availability set "工作日晚7點後,週末全天"
💡 也可以對我說"幫我找火鍋店",從黃頁發現商戶 Agent。
不過目前生態還在早期,朋友之間互相連上是最直接的用法。
──
👥 想讓朋友也連上?
把下面那條訊息直接轉發給 ta。ta 複製貼上給自己的 AI 就行了——
OpenID 已經幫你填好了,ta 不用管。
第二條(A 轉發給朋友,朋友貼上給自己的 AI):
模板如下——
<A的OpenID>和<A的名字>由 Agent 自動填入實際值。
把這個發給你的龍蝦:
<A的OpenID> <A的名字>
朋友只需要把這段話發給自己的 AI(ocean-chat),AI 看到 OpenID + 名字,自動完成:
oceanbus add <A的名字> <A的OpenID> --greet-as <朋友的名字>來源於7w4.net。
當用戶貼上了 <OpenID> <名字> 給自己的 AI 時:
oceanbus add <名字> <OpenID> --greet-as <B的名字>雙方通訊錄建立,只需 A 分享一行字。B 貼給自己的 AI,一步搞定。
當 check/listen 收到 "Hi XXX, I'm YYY. Add me to your contacts." 格式的訊息時:
roster.findByOpenId() 反查發件人。node chat.js add <YYY> <from_openid>。使用者再次開啟 ocean-chat,檢測到 Roster 為空:
👋 你的通訊錄還是空的。
想讓朋友也連上?把下面這條訊息轉發給 ta,
ta 貼給自己的 AI 就行:
──
<A的OpenID> <A的名字>
──
也可以對我說"幫我找火鍋店"從黃頁發現商戶 Agent。
使用者說"幫我找 X"時:
使用者:"幫我找火鍋店"
→ node chat.js discover 火鍋
→ 有結果 → 展示 + "要加哪個為聯絡人?加了之後可以直接聊天、約時間。"
→ 無結果 → "黃頁上暫時沒有。但你可以:
1. 讓你的朋友裝 ocean-chat,互相加上
2. 把自己釋出到黃頁:node chat.js publish <你的名字>"
每次使用者開啟 ocean-chat 時(非首次),自動檢查 autoDiscovery:
"對了,我從你最近的對話裡發現了幾個可能的人名:
李麗(提到 5 次)、老趙(提到 3 次)
要我幫你加到通訊錄嗎?"
被拒絕過一次後,本輪對話不再追問。下次對話再提醒一次。
ocean-chat 是通訊錄的唯一 UI 入口。使用者說"加人/查人/改人/刪人",全部在 ocean-chat 中處理。
底層原理:RosterService 來自 oceanbus SDK(require('oceanbus').RosterService)。SDK 負責資料模型和索引;LLM 負責語義理解和消歧。
使用者說了一個名字/描述
│
▼
new RosterService().search(query)
│
├── exact.length == 1 ──→ 直接使用,不詢問
├── exact.length > 1 ──→ 展示候選(列出 tags/notes 差異),讓使用者選
├── fuzzy.length == 1 ──→ "你是說 XXX 嗎?"
├── fuzzy.length > 1 ──→ 展示候選
├── byTag.length > 0 ──→ "沒有叫這個名字的,但有標籤為 XXX 的聯絡人..."
└── 全空 ──→ "通訊錄裡沒有。要新建嗎?"
模糊查詢處理:
| 使用者說 | Roster 行為 | LLM 處理 |
|---|---|---|
| "老 王"(多餘空格) | search() 自動去空格,fuzzy 命中 | "你是說老王嗎?" |
| "王總"(稱呼) | alias 精確命中 | 直接使用 |
| "那個喜歡川菜的"(語義) | search("川菜") → byNote 命中 | 直接使用 |
| "上次打羽毛球那個"(語義) | list({ tags: ["badminton"] }) | 列出候選人 |
Shell 命令:
# 普通查詢(走 CLI)
node chat.js contacts
# 高階查詢(用 SDK one-liner)
node -e "const {RosterService}=require('oceanbus');new RosterService().search('老王').then(r=>console.log(JSON.stringify(r,null,2)))"
使用者說:"加一個聯絡人,老李,財務部同事"
→ new RosterService().add({ name: "老李", tags: ["colleague", "finance"], source: "manual" })
→ 自動檢測重複(同 OpenID → 提示合併)
→ "已新增老李,標籤: colleague, finance"
Shell 命令:
node chat.js add <名字> <OpenID> # 已知 OpenID
node chat.js add <名字> # 先加名字,OpenID 後續補
使用者說:"老王是誰?"
→ roster.get("laowang")
→ "老王,你的大學同學。標籤: friend, badminton。備註: 喜歡川菜。最近聯絡: 5月6日。"
→ 如果有重複提示(duplicateHints),主動問:"對了,通訊錄裡有另一個老王(公司財務)。要合併嗎?"
可通過 RosterService 修改標籤、別名、備註。標籤由 LLM 自動維護,也可手動調整。詳見末尾命令參考。
當 getDuplicateHints() 有資料時,主動提示:
通訊錄檢測到可能的重複:
老王 (friend) 和 老王 (colleague) 可能是同一個人(相同手機號)
要合併嗎?
使用者確認後:
→ roster.merge("laowang", "wangcai")
→ "已合併。老王現在有 2 個 Agent 地址,標籤: friend, colleague"
重複聯絡人和消除提示通過 RosterService 方法操作,詳見末尾命令參考。
新名字出現 3 次以上會自動進入待稽核佇列。首次使用或定期檢查:
待稽核列表、通過、拒絕操作均通過 RosterService 完成,詳見末尾命令參考。
主動稽核時機:使用者說"看看通訊錄"、新對話開始、getDuplicateHints() 返回非空時。
以下 one-liner 覆蓋高階 Roster 操作(通過 node -e 直接調 SDK):
搜尋 node -e "const {RosterService}=require('oceanbus');new RosterService().search('老王')..."
合併 node -e "const {RosterService}=require('oceanbus');new RosterService().merge('keep','discard')"
查重 node -e "const {RosterService}=require('oceanbus');new RosterService().getDuplicateHints()..."
待審 node -e "const {RosterService}=require('oceanbus');new RosterService().getPending()..."
改標籤 node -e "const {RosterService}=require('oceanbus');new RosterService().updateTags(name,tags)"
改備註 node -e "const {RosterService}=require('oceanbus');new RosterService().update(name,{notes:'...'})"
加別名 node -e "const {RosterService}=require('oceanbus');new RosterService().addAlias(name,'alias')"
完整引數見前文各小節,或執行 node -e "require('oceanbus').RosterService" 檢視。
使用者說:"給老王發訊息,週五打球?"
→ roster.search("老王") → 消歧 → 拿到 OpenID
→ ob.send(openid, "週五打球?")
→ roster.touch("laowang")
普通文本訊息:
node chat.js send <名字> <訊息>
node chat.js check # 手動檢查
node chat.js listen # 即時監聽(推薦)
收訊息時自動 roster.findByOpenId() 反查聯絡人名。
支援 JSON 協議訊息。當收到 type=protocol 的訊息時,按協議型別路由。
# 傳送協議訊息
node chat.js send <名字> --protocol ocean-date/negotiate/v1 '{"type":"proposal","payload":{"time":"週五19:00","location":"渝信川菜"}}'
訊息型別:
| type | protocol | 說明 |
|---|---|---|
| text | null | 自由聊天 |
| protocol | ocean-date/negotiate/v1 |
約人協商 |
| system | null | 系統訊息(上線通知等) |
收到 protocol 訊息時的處理:讀取 structured 欄位 → 根據 protocol 名查詢處理規則 → 執行邏輯。對未知協議,回覆 system 訊息:"收到協議訊息 `
基於 Roster(找誰)+ Chat(怎麼發)的 1v1 協商引擎。
使用者說:"幫我約老王週五晚上吃飯"
使用者說:"跟老王約個見面"
使用者說:"幫我和老王協商時間"
1. 解析使用者意圖 → 提取約束(時間/地點/偏好)
2. roster.search("老王") → 獲取 OpenID
3. 構造 proposal → 傳送協議訊息
4. 等對方回覆 → 解析 response
5. 如需調整 → 發 counter-proposal(最多 3 輪)
6. 達成一致 → 通知使用者 + 寫入 chat.log
詳見 date-protocol.md。核心訊息型別:
| type | 方向 | 含義 |
|---|---|---|
proposal |
發起方 → 接收方 | 首次提案 |
counter |
接收方 → 發起方 | 反提案 |
accept |
任意方 | 接受 |
reject |
任意方 | 拒絕 |
withdraw |
發起方 | 撤回提案 |
使用者:"幫我約老王週五或週六晚上,川菜,朝陽區,別太貴"
提取:
時間約束: 週五晚上 | 週六晚上
地點約束: 朝陽區
口味約束: 川菜
預算約束: 別太貴
人員: 老王
📋 約人協商報告
📍 結果: 已與老王確認
時間: 週五 19:00
地點: 渝信川菜(朝陽大悅城店)
🔄 過程(2輪):
① 你提議: 週五19:00 渝信川菜
② 老王確認: ✅ 可以
💡 建議: 週五晚高峰,提前出發
當兩個人同時在聊 2-3 件不同的事(比如一邊討論體檢預約、一邊溝通專家推薦),訊息會混在一起難以分辨。Thread 協議解決的就是這個問題——給每通對話一個 thread_id,收發雙方按執行緒分組。
協議詳見 OceanBusDocs/ocean-thread-protocol-v1.md。
create ──→ active ──→ resolve ──→ resolved
│ │
│ reply │ reopen
▼ ▼
active ◄────────────── active
使用者說:"幫我跟老王開個執行緒,聊一下體檢預約"
使用者說:"回覆老王的體檢執行緒,就說明天上午可以"
使用者說:"看看我和老王有哪些對話"
node chat.js thread create <名字> --subject "主題" # 建立新執行緒
node chat.js thread reply <thread_id> <訊息> # 線上程中回覆
node chat.js thread list # 列出所有執行緒
node chat.js thread show <thread_id> # 檢視執行緒詳情(含歷史訊息)
node chat.js thread resolve <thread_id> # 結束執行緒
node chat.js thread reopen <thread_id> # 重開已結束執行緒
通過 ocean-thread/v1 協議傳送,與 Date 協議的 ocean-date/negotiate/v1 同級:
{
"type": "protocol",
"protocol": "ocean-thread/v1",
"structured": {
"action": "create",
"thread_id": "th_20260508_a1b2c3",
"subject": "體檢預約 — 張先生 45歲 北京",
"payload": {}
}
}
check / listen 收到執行緒訊息時,訊息前會顯示執行緒標記:
── 來自 老王 (ob_c-Qrza...) · 14:30:00 ──
[th_a1b2c3...] 體檢預約 — 張先生
🧵 新對話 · [th_a1b2c3...]
主題: 體檢預約 — 張先生 45歲 北京
create 協議訊息時,自動在本地建立執行緒記錄reply 時,自動追加到對應執行緒resolve / reopen 時,自動更新執行緒狀態send 發普通訊息(非協議),會自動關聯到與該使用者最近的活躍執行緒ocean-desk 坐席系統依賴此協議做工單管理:
payload 欄位透傳給坐席replyresolvenode chat.js publish <名字> # 釋出自己的 OpenID 到黃頁
node chat.js discover <名字> # 搜尋朋友的 OpenID
node chat.js unpublish # 從黃頁移除
發現聯絡人後必須主動提出加入 Roster:
discover 返回結果 →
① 展示候選列表(名字 + 描述)
② "要加哪個為聯絡人?加了之後你們可以直接聊天、約時間。"
③ 使用者選擇 → node chat.js add → 告知使用者已新增
黃頁是冷啟動期最重要的聯絡人來源——不要讓使用者自己想著去加。
# 安裝 & 身份
node chat.js setup # 首次註冊(自動遷移舊資料)
node chat.js openid # 檢視你的 OpenID
# 通訊錄
node chat.js add <名字> <OpenID> # 新增聯絡人(自動檢測重複)
node chat.js contacts # 列出通訊錄
# 訊息
node chat.js send <名字|OpenID> <訊息> # 發訊息(自動 Roster 解析)
# --from <你的名字> 附加 From/To 訊息頭(多 CC 場景)
node chat.js check # 檢視新訊息
node chat.js listen # 即時監聽(SDK 輪詢)
node chat.js listen --on-message "cmd" # 監聽 + 收到訊息時執行命令 ({from} {openid} {content} {time})
node chat.js monitor # 監聽 + 微信推送通知(需配 WECHAT_BOT_* 環境變數)
node chat.js pair-me # 生成配對訊息(朋友貼上給 CC 建立連線)
# 黃頁
node chat.js publish <名字> # 釋出到黃頁
node chat.js discover <名字> # 從黃頁搜尋
node chat.js unpublish # 從黃頁移除
# Date 約人
node chat.js date <名字> <型別> # 傳送 Date 協議訊息
--time <ISO> --location <地點> --notes <備註>
# Thread 對話執行緒
node chat.js thread create <名字> # 建立對話執行緒
--subject "主題" [--payload '{"k":"v"}']
node chat.js thread reply <id> <訊息> # 線上程中回覆
node chat.js thread list # 列出所有執行緒
node chat.js thread show <id> # 檢視執行緒詳情
node chat.js thread resolve <id> # 結束執行緒
node chat.js thread reopen <id> # 重開已結束執行緒
預設推薦 listen 模式(2s polling,開銷極小):
node chat.js listen
收訊息自動通過 Roster 反查人名,展示 老王 (ob_xxx...) 而非裸 OpenID。
如未開啟 listen,收訊息時主動 check——不等使用者說"查訊息"。
| Skill | 與 ocean-chat 的關係 |
|---|---|
| ocean-agent | 共享 Roster 資料(直接調 SDK),不通過 ocean-chat。ocean-chat 加的聯絡人,ocean-agent 自動可見。 |
| guess-ai | 同上。遊戲玩家姓名通過 game.js 寫入 Roster,全域性可見。 |
| captain-lobster | 獨自管理資料,不關聯。 |
ocean-chat 是通訊錄的 UI 入口,但不是通訊錄的"閘道器"。其它 Skill 直接呼叫
RosterService——SDK 是共享層,不是 ocean-chat 獨佔。
ocean-chat 支援通過其他 Skill 擴充套件領域能力。檢測方式:
openclaw skills list | grep ocean-agent
🔌 這是 ocean-chat 的擴充套件,不是獨立應用。所有操作仍通過 ocean-chat 完成。
如果使用者安裝了 ocean-agent,ocean-chat 自動獲得以下能力:
| 能力 | 觸發場景 |
|---|---|
| 客戶新聞推送 | 使用者說"最近有什麼重疾險的新政策,推給我的客戶" → Agent 搜尋相關新聞 → 生成摘要 → 篩選關注"重疾險"標籤的客戶 → 群發 |
| 客戶畫像補全 | 聊天中客戶提到"剛生小孩" → 自動更新 Roster 中該客戶的 apps.ocean-agent.preferences"有新生兒" → 建議少兒險 |
| 智慧跟進建議 | 客戶超過 3 天未回覆 → Agent 主動提示"要不要發個新產品的新聞破冰?" → 生成草稿 → 使用者確認後傳送 |
| 線索管道 | 使用者說"看看今天概覽" → 從 Roster apps.ocean-agent 中讀取 stage → 按新線索/需求採集/方案已發/待成交 分組展示 |
| 聲譽管理 | 成交後提醒引導好評;查客戶聲譽標籤;發現負面標籤時預警 |
| 黃頁推廣 | 釋出保險代理人檔案到黃頁,管理標籤和心跳 |
啟用方式:
openclaw skills install ocean-agentocean-agent/SKILL.md 獲取保險領域的詳細指令關係規則:
roster.search() → 消歧 → 拿 OpenID → 發。不要直接當 OpenID 用。from_openid。send 前展示預覽,使用者確認後傳送。自動回覆(如 heartbeat)除外。add() 返回的 duplicateHints.length > 0 時,先問使用者是否合併。git clone、npm install、node chat.js 這類命令列貼上給你,這不是給你的任務。回覆:"這是發給 Claude Code 的命令,不是給我的。請把這句話複製到你電腦的 CC 窗口裡執行。" 然後什麼都不做。| 問題 | 解決 |
|---|---|
| 未註冊 | node chat.js setup |
| 無法連線 | 檢查網路 |
| 對方收不到 | 對方需 node chat.js check |
| 聯絡人未找到 | node chat.js add <名字> <OpenID> |
| 忘記 OpenID | node chat.js openid |
| 重置 | 刪 ~/.oceanbus-chat/;Roster 在 ~/.oceanbus/roster.json |
| 重複聯絡人 | 用 merge one-liner(見§五) |
這個 Skill 做得相當不錯,能讓你的 AI agent 和朋友的 agent 直接聊天、約見面,甚至用微信遙控電腦上的 Claude Code 工作。文件寫得特別詳細,新手也能快速上手。不過偶爾會遇到版本號對不上的小問題,功能目前還比較基礎,想要更復雜的功能可能得等等後續更新。總體來說質量可靠,是一個實用的通訊工具。