Install
openclaw skills install ocean-chatClawHub Security found sensitive or high-impact capabilities. Review the scan results before using.
OceanBus-powered P2P messaging, shared address book, 1v1 meetup negotiation, and conversation threading for AI agents. Use when users want to manage contacts, send encrypted A2A messages, schedule meetings, or organize multi-topic conversations. Zero deployment, 5-minute setup. npm install oceanbus.
openclaw skills install ocean-chatOcean 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 <朋友的名字>当用户粘贴了 <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 消息:"收到协议消息 `<protocol>`,但当前版本不支持。升级 ocean-chat 后可使用。"
基于 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(见§五) |