@coclaw/openclaw-coclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -0
- package/README.md +153 -0
- package/index.js +156 -0
- package/openclaw.plugin.json +18 -0
- package/package.json +70 -0
- package/src/api.js +57 -0
- package/src/channel-plugin.js +71 -0
- package/src/cli-registrar.js +87 -0
- package/src/cli.js +132 -0
- package/src/common/bot-binding.js +76 -0
- package/src/common/errors.js +23 -0
- package/src/common/gateway-notify.js +104 -0
- package/src/common/messages.js +33 -0
- package/src/config.js +194 -0
- package/src/message-model.js +67 -0
- package/src/realtime-bridge.js +546 -0
- package/src/runtime.js +10 -0
- package/src/session-manager/manager.js +274 -0
- package/src/transport-adapter.js +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Chengdu Gongyan Technology Co., Ltd.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @coclaw/openclaw-coclaw
|
|
2
|
+
|
|
3
|
+
CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `openclaw-coclaw`),包含:
|
|
4
|
+
|
|
5
|
+
- **transport bridge** — CoClaw server 与 OpenClaw gateway 之间的实时消息桥接
|
|
6
|
+
- **session-manager** — 会话列表/读取能力(`nativeui.sessions.listAll` / `nativeui.sessions.get`)
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
### 从 npm 安装(生产推荐)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm run plugin:npm:install
|
|
14
|
+
# 或手动:
|
|
15
|
+
openclaw plugins install @coclaw/openclaw-coclaw
|
|
16
|
+
openclaw gateway restart
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 本地开发安装(--link)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm run plugin:dev:link
|
|
23
|
+
# 或手动:
|
|
24
|
+
openclaw plugins install --link /path/to/plugins/openclaw
|
|
25
|
+
openclaw gateway restart
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
安装后确认:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
openclaw plugins doctor
|
|
32
|
+
openclaw gateway status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 卸载
|
|
36
|
+
|
|
37
|
+
### npm 安装的卸载
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pnpm run plugin:npm:uninstall
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 本地开发安装的卸载
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pnpm run plugin:dev:unlink
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
卸载脚本会自动清理:
|
|
50
|
+
- `plugins.entries` / `plugins.installs` 等插件元数据
|
|
51
|
+
- `~/.openclaw/coclaw/bindings.json`(绑定信息)
|
|
52
|
+
- `openclaw.json` 中可能残留的 `channels.coclaw` 节点(旧版兼容)
|
|
53
|
+
|
|
54
|
+
## 绑定 / 解绑
|
|
55
|
+
|
|
56
|
+
绑定码从 CoClaw Web 端生成,有效期有限。
|
|
57
|
+
|
|
58
|
+
### 方式一:OpenClaw CLI 子命令(推荐)
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
openclaw coclaw bind <binding-code> [--server <url>]
|
|
62
|
+
openclaw coclaw unbind [--server <url>]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
bind/unbind 成功后会通过 gateway RPC 通知插件刷新/停止 bridge 连接(无需重启 gateway)。若 gateway 未运行,通知会失败但不影响绑定结果。
|
|
66
|
+
|
|
67
|
+
### 方式二:IM 渠道命令
|
|
68
|
+
|
|
69
|
+
在已注册 CoClaw channel 的 IM 渠道中发送:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
/coclaw bind <binding-code> [--server <url>]
|
|
73
|
+
/coclaw unbind [--server <url>]
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
需要 gateway 运行中。
|
|
77
|
+
|
|
78
|
+
### 方式三:独立 CLI(兼容)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node ~/.openclaw/extensions/openclaw-coclaw/src/cli.js bind <binding-code> --server <url>
|
|
82
|
+
node ~/.openclaw/extensions/openclaw-coclaw/src/cli.js unbind --server <url>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## 配置存储
|
|
86
|
+
|
|
87
|
+
绑定信息存储在 `~/.openclaw/coclaw/bindings.json`(通过 `resolveStateDir()` + channel ID 组合路径),**不存储在 `openclaw.json` 中**。
|
|
88
|
+
|
|
89
|
+
文件结构:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"default": {
|
|
94
|
+
"serverUrl": "https://coclaw.net",
|
|
95
|
+
"botId": "bot-xxx",
|
|
96
|
+
"token": "token-xxx",
|
|
97
|
+
"boundAt": "2026-03-05T..."
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
说明:
|
|
103
|
+
- 这一设计是为了避免卸载插件后 `channels.coclaw` 节点残留导致 OpenClaw gateway schema 验证失败。
|
|
104
|
+
- `config.js` 是读写绑定信息的唯一入口。
|
|
105
|
+
- 首次读取时会自动从旧位置(`openclaw.json` 的 `channels.coclaw` / `.coclaw-tunnel.json`)迁移。
|
|
106
|
+
- 绑定时不提交 bot `name`;server 通过 gateway WebSocket 获取 OpenClaw 实例名。若未设置实例名,前端回退显示 `OpenClaw`。
|
|
107
|
+
|
|
108
|
+
## 运行与排障日志
|
|
109
|
+
|
|
110
|
+
### 日志级别建议
|
|
111
|
+
|
|
112
|
+
- 默认(生产推荐):仅保留 `info/warn`,用于连接、绑定、解绑、鉴权失败等关键事件。
|
|
113
|
+
- 深度排障:开启 `COCLAW_WS_DEBUG=1`,输出 rpc/event 透传细节。
|
|
114
|
+
|
|
115
|
+
### 开启 `COCLAW_WS_DEBUG`
|
|
116
|
+
|
|
117
|
+
临时开启(当前 shell):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
COCLAW_WS_DEBUG=1 openclaw gateway start
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
若通过 systemd/user service 运行 gateway,可在服务环境中追加:
|
|
124
|
+
|
|
125
|
+
```ini
|
|
126
|
+
Environment=COCLAW_WS_DEBUG=1
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
然后重启 gateway 生效。
|
|
130
|
+
|
|
131
|
+
### 常用排障命令
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 看 plugin 与 server 连接状态
|
|
135
|
+
openclaw logs --limit 300 --plain | rg -n "realtime bridge|coclaw/ws|bind success|unbind success" -i
|
|
136
|
+
|
|
137
|
+
# 看 gateway 握手/协议问题
|
|
138
|
+
openclaw logs --limit 300 --plain | rg -n "gateway connect failed|protocol mismatch|closed before connect|auth failed" -i
|
|
139
|
+
|
|
140
|
+
# 看 rpc/event 透传(需先开启 COCLAW_WS_DEBUG=1)
|
|
141
|
+
openclaw logs --limit 300 --plain | rg -n "ui->server req|bot->server res|bot->server event|gateway req" -i
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 测试门禁
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
pnpm check # lint + typecheck
|
|
148
|
+
pnpm test # 全部单测
|
|
149
|
+
pnpm coverage # 覆盖率检查
|
|
150
|
+
pnpm verify # 完整验证(check → test:standalone → test:plugin → test → coverage)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
覆盖率阈值:100%(lines/functions/branches/statements),未通过禁止接入 gateway。
|
package/index.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { bindBot, unbindBot } from './src/common/bot-binding.js';
|
|
2
|
+
import { registerCoclawCli } from './src/cli-registrar.js';
|
|
3
|
+
import { resolveErrorMessage } from './src/common/errors.js';
|
|
4
|
+
import { alreadyBound, notBound, bindOk, unbindOk } from './src/common/messages.js';
|
|
5
|
+
import { coclawChannelPlugin } from './src/channel-plugin.js';
|
|
6
|
+
import { refreshRealtimeBridge, startRealtimeBridge, stopRealtimeBridge } from './src/realtime-bridge.js';
|
|
7
|
+
import { setRuntime } from './src/runtime.js';
|
|
8
|
+
import { createSessionManager } from './src/session-manager/manager.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
function parseCommandArgs(args) {
|
|
12
|
+
/* c8 ignore next */
|
|
13
|
+
const tokens = (args ?? '').split(/\s+/).filter(Boolean);
|
|
14
|
+
/* c8 ignore next */
|
|
15
|
+
const action = tokens[0] ?? 'help';
|
|
16
|
+
const options = {};
|
|
17
|
+
const positionals = [];
|
|
18
|
+
|
|
19
|
+
for (let i = 1; i < tokens.length; i += 1) {
|
|
20
|
+
const token = tokens[i];
|
|
21
|
+
if (token === '--server' && i + 1 < tokens.length) {
|
|
22
|
+
options.server = tokens[i + 1];
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
positionals.push(token);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { action, positionals, options };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildHelpText() {
|
|
33
|
+
return [
|
|
34
|
+
'CoClaw command:',
|
|
35
|
+
'',
|
|
36
|
+
'/coclaw bind <binding-code> [--server <url>]',
|
|
37
|
+
'/coclaw unbind [--server <url>]',
|
|
38
|
+
].join('\n');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function respondError(respond, err) {
|
|
42
|
+
respond(false, {
|
|
43
|
+
/* c8 ignore next */
|
|
44
|
+
error: String(err?.message ?? err),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* c8 ignore start */
|
|
49
|
+
const plugin = {
|
|
50
|
+
id: 'openclaw-coclaw',
|
|
51
|
+
name: 'CoClaw',
|
|
52
|
+
description: 'OpenClaw CoClaw channel plugin for remote chat',
|
|
53
|
+
register(api) {
|
|
54
|
+
setRuntime(api.runtime);
|
|
55
|
+
const logger = api?.logger ?? console;
|
|
56
|
+
const manager = createSessionManager({ logger });
|
|
57
|
+
|
|
58
|
+
api.registerChannel({ plugin: coclawChannelPlugin });
|
|
59
|
+
api.registerService({
|
|
60
|
+
id: 'coclaw-realtime-bridge',
|
|
61
|
+
async start() {
|
|
62
|
+
await startRealtimeBridge({ logger, pluginConfig: api.pluginConfig });
|
|
63
|
+
},
|
|
64
|
+
async stop() {
|
|
65
|
+
await stopRealtimeBridge();
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
api.registerGatewayMethod('coclaw.refreshBridge', async ({ respond }) => {
|
|
70
|
+
try {
|
|
71
|
+
await refreshRealtimeBridge();
|
|
72
|
+
respond(true, { status: 'refreshed' });
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
respondError(respond, err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
api.registerGatewayMethod('coclaw.stopBridge', async ({ respond }) => {
|
|
80
|
+
try {
|
|
81
|
+
await stopRealtimeBridge();
|
|
82
|
+
respond(true, { status: 'stopped' });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
respondError(respond, err);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
api.registerGatewayMethod('nativeui.sessions.listAll', ({ params, respond }) => {
|
|
90
|
+
try {
|
|
91
|
+
respond(true, manager.listAll(params ?? {}));
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
respondError(respond, err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
api.registerGatewayMethod('nativeui.sessions.get', ({ params, respond }) => {
|
|
99
|
+
try {
|
|
100
|
+
respond(true, manager.get(params ?? {}));
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
respondError(respond, err);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
api.registerCli(registerCoclawCli, { commands: ['coclaw'] });
|
|
108
|
+
|
|
109
|
+
api.registerCommand({
|
|
110
|
+
name: 'coclaw',
|
|
111
|
+
description: 'CoClaw bind/unbind command',
|
|
112
|
+
acceptsArgs: true,
|
|
113
|
+
handler: async (ctx) => {
|
|
114
|
+
const { action, positionals, options } = parseCommandArgs(ctx.args);
|
|
115
|
+
if (action === 'help') {
|
|
116
|
+
return { text: buildHelpText() };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const serverUrl = options.server ?? api.pluginConfig?.serverUrl;
|
|
121
|
+
if (action === 'bind') {
|
|
122
|
+
const result = await bindBot({
|
|
123
|
+
code: positionals[0],
|
|
124
|
+
serverUrl,
|
|
125
|
+
});
|
|
126
|
+
await refreshRealtimeBridge();
|
|
127
|
+
return { text: bindOk(result) };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (action === 'unbind') {
|
|
131
|
+
const result = await unbindBot({ serverUrl });
|
|
132
|
+
await stopRealtimeBridge();
|
|
133
|
+
return { text: unbindOk(result) };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { text: buildHelpText() };
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
if (err.code === 'ALREADY_BOUND') {
|
|
140
|
+
return { text: alreadyBound(err) };
|
|
141
|
+
}
|
|
142
|
+
if (err.code === 'NOT_BOUND') {
|
|
143
|
+
return { text: notBound() };
|
|
144
|
+
}
|
|
145
|
+
logger.warn?.(`[coclaw] command failed: ${String(err?.message ?? err)}`);
|
|
146
|
+
return {
|
|
147
|
+
text: `Error: ${resolveErrorMessage(err)}`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
/* c8 ignore stop */
|
|
155
|
+
|
|
156
|
+
export default plugin;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "openclaw-coclaw",
|
|
3
|
+
"name": "CoClaw",
|
|
4
|
+
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
5
|
+
"channels": ["coclaw"],
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"serverUrl": {
|
|
11
|
+
"type": "string"
|
|
12
|
+
},
|
|
13
|
+
"gatewayWsUrl": {
|
|
14
|
+
"type": "string"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/coclaw/coclaw.git",
|
|
10
|
+
"directory": "plugins/openclaw"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://coclaw.net",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"openclaw",
|
|
15
|
+
"openclaw-plugin",
|
|
16
|
+
"coclaw",
|
|
17
|
+
"channel",
|
|
18
|
+
"chatbot",
|
|
19
|
+
"bridge",
|
|
20
|
+
"webchat"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public",
|
|
27
|
+
"registry": "https://registry.npmjs.org/"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"src/**/*.js",
|
|
32
|
+
"!src/**/*.test.js",
|
|
33
|
+
"!src/mock-server.helper.js",
|
|
34
|
+
"openclaw.plugin.json",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"main": "index.js",
|
|
38
|
+
"openclaw": {
|
|
39
|
+
"extensions": [
|
|
40
|
+
"./index.js"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"bin": {
|
|
44
|
+
"coclaw": "src/cli.js"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"dev": "node src/cli.js --help",
|
|
48
|
+
"build": "echo \"TODO: build coclaw openclaw plugin\"",
|
|
49
|
+
"lint": "eslint \"**/*.{js,mjs,cjs}\" --no-error-on-unmatched-pattern",
|
|
50
|
+
"typecheck": "echo \"No typecheck configured yet (coclaw openclaw plugin)\"",
|
|
51
|
+
"check": "pnpm lint && pnpm typecheck",
|
|
52
|
+
"test:standalone": "node --test src/standalone-mode.test.js",
|
|
53
|
+
"test:plugin": "node --test src/plugin-mode.test.js",
|
|
54
|
+
"test": "node --test",
|
|
55
|
+
"coverage": "c8 --check-coverage --lines 100 --functions 100 --branches 100 --statements 100 --reporter=text --reporter=lcov node --test",
|
|
56
|
+
"verify": "pnpm check && pnpm test:standalone && pnpm test:plugin && pnpm test && pnpm coverage",
|
|
57
|
+
"pub:verify": "pnpm verify",
|
|
58
|
+
"pub:check": "npm whoami --registry=https://registry.npmjs.org/ && npm ping --registry=https://registry.npmjs.org/ && npm publish --dry-run --access public --registry=https://registry.npmjs.org/",
|
|
59
|
+
"pub:versions": "npm view @coclaw/openclaw-coclaw versions --json --registry=https://registry.npmjs.org/ && npm view @coclaw/openclaw-coclaw versions --json",
|
|
60
|
+
"pub:release": "bash ./scripts/publish-npm.sh",
|
|
61
|
+
"plugin:dev:link": "bash ./scripts/dev-link-install.sh",
|
|
62
|
+
"plugin:dev:unlink": "bash ./scripts/dev-link-uninstall.sh",
|
|
63
|
+
"plugin:npm:install": "bash ./scripts/npm-install.sh",
|
|
64
|
+
"plugin:npm:uninstall": "bash ./scripts/npm-uninstall.sh"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"c8": "^10.1.3",
|
|
68
|
+
"eslint": "^9.39.2"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/api.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
async function requestJson(baseUrl, path, { method = 'GET', headers, body } = {}) {
|
|
2
|
+
const url = new URL(path, baseUrl).toString();
|
|
3
|
+
const res = await fetch(url, {
|
|
4
|
+
method,
|
|
5
|
+
headers,
|
|
6
|
+
body: body == null ? undefined : JSON.stringify(body),
|
|
7
|
+
});
|
|
8
|
+
let data = null;
|
|
9
|
+
try {
|
|
10
|
+
data = await res.json();
|
|
11
|
+
}
|
|
12
|
+
/* c8 ignore next 3 */
|
|
13
|
+
catch {
|
|
14
|
+
data = null;
|
|
15
|
+
}
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const err = new Error(data?.message || `HTTP ${res.status}`);
|
|
18
|
+
err.response = { status: res.status, data };
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function bindWithServer({ baseUrl, code, name }) {
|
|
25
|
+
return requestJson(baseUrl, '/api/v1/bots/bind', {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { 'content-type': 'application/json' },
|
|
28
|
+
body: { code, name },
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function unbindWithServer({ baseUrl, token }) {
|
|
33
|
+
return requestJson(baseUrl, '/api/v1/bots/unbind', {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${token}`,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function listBotsWithServer({ baseUrl, cookie }) {
|
|
42
|
+
return requestJson(baseUrl, '/api/v1/bots', {
|
|
43
|
+
headers: cookie
|
|
44
|
+
? {
|
|
45
|
+
Cookie: cookie,
|
|
46
|
+
}
|
|
47
|
+
: undefined,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function getBotSelfWithServer({ baseUrl, token }) {
|
|
52
|
+
return requestJson(baseUrl, '/api/v1/bots/self', {
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: `Bearer ${token}`,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from './config.js';
|
|
2
|
+
import { createTransportAdapter } from './transport-adapter.js';
|
|
3
|
+
|
|
4
|
+
function resolveAccount(_cfg, accountId) {
|
|
5
|
+
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
6
|
+
return {
|
|
7
|
+
accountId: resolvedAccountId,
|
|
8
|
+
enabled: true,
|
|
9
|
+
configured: true,
|
|
10
|
+
name: 'CoClaw',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const transport = createTransportAdapter();
|
|
15
|
+
|
|
16
|
+
export const coclawChannelPlugin = {
|
|
17
|
+
id: 'coclaw',
|
|
18
|
+
meta: {
|
|
19
|
+
id: 'coclaw',
|
|
20
|
+
label: 'CoClaw',
|
|
21
|
+
selectionLabel: 'CoClaw',
|
|
22
|
+
docsPath: 'https://docs.coclaw.net',
|
|
23
|
+
blurb: 'CoClaw channel plugin for remote chat',
|
|
24
|
+
},
|
|
25
|
+
capabilities: {
|
|
26
|
+
chatTypes: ['direct'],
|
|
27
|
+
nativeCommands: true,
|
|
28
|
+
media: false,
|
|
29
|
+
reactions: false,
|
|
30
|
+
threads: false,
|
|
31
|
+
polls: false,
|
|
32
|
+
},
|
|
33
|
+
config: {
|
|
34
|
+
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
35
|
+
resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
|
|
36
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
37
|
+
isConfigured: () => true,
|
|
38
|
+
describeAccount: (account) => ({
|
|
39
|
+
accountId: account.accountId,
|
|
40
|
+
name: account.name,
|
|
41
|
+
enabled: account.enabled,
|
|
42
|
+
configured: true,
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
outbound: {
|
|
46
|
+
deliveryMode: 'direct',
|
|
47
|
+
sendText: async ({ to, text }) => {
|
|
48
|
+
const result = await transport.safeDispatchOutbound({
|
|
49
|
+
channel: 'coclaw',
|
|
50
|
+
to,
|
|
51
|
+
text,
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
channel: 'coclaw',
|
|
55
|
+
messageId: result.messageId ?? `coclaw-local-${Date.now()}`,
|
|
56
|
+
to,
|
|
57
|
+
text,
|
|
58
|
+
accepted: Boolean(result.accepted),
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
status: {
|
|
63
|
+
defaultRuntime: {
|
|
64
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
65
|
+
running: true,
|
|
66
|
+
lastStartAt: null,
|
|
67
|
+
lastStopAt: null,
|
|
68
|
+
lastError: null,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { bindBot, unbindBot } from './common/bot-binding.js';
|
|
2
|
+
import { resolveErrorMessage } from './common/errors.js';
|
|
3
|
+
import { callGatewayMethod } from './common/gateway-notify.js';
|
|
4
|
+
import {
|
|
5
|
+
alreadyBound, notBound, bindOk, unbindOk,
|
|
6
|
+
gatewayNotified, gatewayNotifyFailed,
|
|
7
|
+
} from './common/messages.js';
|
|
8
|
+
|
|
9
|
+
function resolveServerUrl(opts, config) {
|
|
10
|
+
return opts?.server
|
|
11
|
+
?? config?.plugins?.entries?.['openclaw-coclaw']?.config?.serverUrl
|
|
12
|
+
?? process.env.COCLAW_SERVER_URL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 注册 `openclaw coclaw bind/unbind` CLI 子命令
|
|
17
|
+
* @param {object} ctx - OpenClaw CLI 注册上下文
|
|
18
|
+
* @param {import('commander').Command} ctx.program - Commander.js Command 实例
|
|
19
|
+
* @param {object} ctx.config - OpenClaw 配置
|
|
20
|
+
* @param {object} ctx.logger - 日志实例
|
|
21
|
+
* @param {object} [deps] - 可注入依赖(测试用)
|
|
22
|
+
*/
|
|
23
|
+
export function registerCoclawCli({ program, config, logger }, deps = {}) {
|
|
24
|
+
const notifyGateway = async (method) => {
|
|
25
|
+
const action = method.endsWith('refreshBridge') ? 'refresh' : 'stop';
|
|
26
|
+
try {
|
|
27
|
+
const result = await callGatewayMethod(method, deps.spawn);
|
|
28
|
+
if (result.ok) {
|
|
29
|
+
logger.info?.(`[coclaw] ${gatewayNotified(action)}`);
|
|
30
|
+
} else {
|
|
31
|
+
logger.warn?.(`[coclaw] ${gatewayNotifyFailed()}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/* c8 ignore next 3 -- callGatewayMethod 已内部兜底,此处纯防御 */
|
|
35
|
+
catch {
|
|
36
|
+
logger.warn?.(`[coclaw] ${gatewayNotifyFailed()}`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const coclaw = program
|
|
41
|
+
.command('coclaw')
|
|
42
|
+
.description('CoClaw bind/unbind commands');
|
|
43
|
+
|
|
44
|
+
coclaw
|
|
45
|
+
.command('bind <code>')
|
|
46
|
+
.description('Bind this OpenClaw instance to CoClaw')
|
|
47
|
+
.option('--server <url>', 'CoClaw server URL')
|
|
48
|
+
.action(async (code, opts) => {
|
|
49
|
+
try {
|
|
50
|
+
const serverUrl = resolveServerUrl(opts, config);
|
|
51
|
+
const result = await bindBot({ code, serverUrl });
|
|
52
|
+
/* c8 ignore next */
|
|
53
|
+
console.log(bindOk(result));
|
|
54
|
+
await notifyGateway('coclaw.refreshBridge');
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (err.code === 'ALREADY_BOUND') {
|
|
57
|
+
console.error(alreadyBound(err));
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
62
|
+
process.exitCode = 1;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
coclaw
|
|
67
|
+
.command('unbind')
|
|
68
|
+
.description('Unbind this OpenClaw instance from CoClaw')
|
|
69
|
+
.option('--server <url>', 'CoClaw server URL')
|
|
70
|
+
.action(async (opts) => {
|
|
71
|
+
try {
|
|
72
|
+
const serverUrl = resolveServerUrl(opts, config);
|
|
73
|
+
const result = await unbindBot({ serverUrl });
|
|
74
|
+
/* c8 ignore next */
|
|
75
|
+
console.log(unbindOk(result));
|
|
76
|
+
await notifyGateway('coclaw.stopBridge');
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (err.code === 'NOT_BOUND') {
|
|
79
|
+
console.error(notBound());
|
|
80
|
+
process.exitCode = 1;
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|