@coclaw/openclaw-coclaw 0.1.6 → 0.1.7
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/README.md +33 -25
- package/index.js +3 -4
- package/package.json +11 -11
- package/src/api.js +13 -21
- package/src/channel-plugin.js +8 -17
- package/src/cli-registrar.js +3 -6
- package/src/cli.js +11 -17
- package/src/common/bot-binding.js +16 -8
- package/src/common/messages.js +5 -7
- package/src/config.js +1 -104
- package/src/message-model.js +2 -0
- package/src/realtime-bridge.js +501 -429
- package/src/session-manager/manager.js +42 -4
- package/src/transport-adapter.js +3 -0
package/README.md
CHANGED
|
@@ -5,51 +5,60 @@ CoClaw 的 OpenClaw 插件(npm: `@coclaw/openclaw-coclaw`,plugin id: `opencl
|
|
|
5
5
|
- **transport bridge** — CoClaw server 与 OpenClaw gateway 之间的实时消息桥接
|
|
6
6
|
- **session-manager** — 会话列表/读取能力(`nativeui.sessions.listAll` / `nativeui.sessions.get`)
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 安装与模式切换
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
插件支持两种安装模式,可随时切换(脚本会自动处理卸载→重装):
|
|
11
|
+
|
|
12
|
+
### 本地开发(link 模式,日常开发推荐)
|
|
11
13
|
|
|
12
14
|
```bash
|
|
13
|
-
pnpm run
|
|
14
|
-
# 或手动:
|
|
15
|
-
openclaw plugins install @coclaw/openclaw-coclaw
|
|
16
|
-
openclaw gateway restart
|
|
15
|
+
pnpm run link
|
|
17
16
|
```
|
|
18
17
|
|
|
19
|
-
|
|
18
|
+
link 后代码更新只需 `openclaw gateway restart`,无需重新安装。
|
|
19
|
+
|
|
20
|
+
### 从 npm 安装
|
|
20
21
|
|
|
21
22
|
```bash
|
|
22
|
-
pnpm run
|
|
23
|
-
# 或手动:
|
|
24
|
-
openclaw plugins install --link /path/to/plugins/openclaw
|
|
25
|
-
openclaw gateway restart
|
|
23
|
+
pnpm run install:npm
|
|
26
24
|
```
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
### 卸载
|
|
29
27
|
|
|
30
28
|
```bash
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
pnpm run unlink # 卸载 link 模式
|
|
30
|
+
pnpm run uninstall:npm # 卸载 npm 模式
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
卸载仅移除插件元数据和代码,不清理绑定信息(`bindings.json` 独立保留)。
|
|
34
|
+
|
|
35
|
+
## 预发布验证与发布
|
|
36
|
+
|
|
37
|
+
### 预发布验证
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
发布前验证 tarball 能正确安装到 OpenClaw 中:
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
|
-
pnpm run
|
|
42
|
+
pnpm run prerelease # 全新安装验证(交互式,含手动功能验证)
|
|
43
|
+
pnpm run prerelease -- --upgrade # 升级验证(先装 npm 旧版,再用本地包覆盖)
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
###
|
|
46
|
+
### 发布到 npm
|
|
44
47
|
|
|
45
48
|
```bash
|
|
46
|
-
pnpm run
|
|
49
|
+
pnpm run release
|
|
47
50
|
```
|
|
48
51
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
发布流程:预发布验证(自动) → npm 凭据检查 → dry-run → 发布 → 轮询确认生效。
|
|
53
|
+
|
|
54
|
+
### 检查发布状态
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pnpm run release:check # 显示各 registry 最新版本
|
|
58
|
+
pnpm run release:check -- 0.1.7 # 对比指定版本
|
|
59
|
+
WAIT=1 pnpm run release:check -- 0.1.7 # 轮询直到版本生效
|
|
60
|
+
pnpm run release:versions # 显示所有已发布版本
|
|
61
|
+
```
|
|
53
62
|
|
|
54
63
|
## 绑定 / 解绑
|
|
55
64
|
|
|
@@ -102,7 +111,6 @@ node ~/.openclaw/extensions/coclaw/src/cli.js unbind --server <url>
|
|
|
102
111
|
说明:
|
|
103
112
|
- 这一设计是为了避免卸载插件后 `channels.coclaw` 节点残留导致 OpenClaw gateway schema 验证失败。
|
|
104
113
|
- `config.js` 是读写绑定信息的唯一入口。
|
|
105
|
-
- 首次读取时会自动从旧位置(`openclaw.json` 的 `channels.coclaw` / `.coclaw-tunnel.json`)迁移。
|
|
106
114
|
- 绑定时不提交 bot `name`;server 通过 gateway WebSocket 获取 OpenClaw 实例名。若未设置实例名,前端回退显示 `OpenClaw`。
|
|
107
115
|
|
|
108
116
|
## 运行与排障日志
|
|
@@ -150,4 +158,4 @@ pnpm coverage # 覆盖率检查
|
|
|
150
158
|
pnpm verify # 完整验证(check → test:standalone → test:plugin → test → coverage)
|
|
151
159
|
```
|
|
152
160
|
|
|
153
|
-
覆盖率阈值:
|
|
161
|
+
覆盖率阈值:lines/statements/functions 100%,branches ≥ 95%。未通过禁止接入 gateway。
|
package/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { bindBot, unbindBot } from './src/common/bot-binding.js';
|
|
2
2
|
import { registerCoclawCli } from './src/cli-registrar.js';
|
|
3
3
|
import { resolveErrorMessage } from './src/common/errors.js';
|
|
4
|
-
import {
|
|
4
|
+
import { notBound, bindOk, unbindOk } from './src/common/messages.js';
|
|
5
5
|
import { coclawChannelPlugin } from './src/channel-plugin.js';
|
|
6
6
|
import { refreshRealtimeBridge, startRealtimeBridge, stopRealtimeBridge } from './src/realtime-bridge.js';
|
|
7
7
|
import { setRuntime } from './src/runtime.js';
|
|
@@ -118,6 +118,8 @@ const plugin = {
|
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
120
|
if (action === 'bind') {
|
|
121
|
+
// 先断开 bridge,避免 unbindWithServer 触发的 bot.unbound 竞态
|
|
122
|
+
await stopRealtimeBridge();
|
|
121
123
|
const serverUrl = options.server ?? api.pluginConfig?.serverUrl;
|
|
122
124
|
const result = await bindBot({
|
|
123
125
|
code: positionals[0],
|
|
@@ -136,9 +138,6 @@ const plugin = {
|
|
|
136
138
|
return { text: buildHelpText() };
|
|
137
139
|
}
|
|
138
140
|
catch (err) {
|
|
139
|
-
if (err.code === 'ALREADY_BOUND') {
|
|
140
|
-
return { text: alreadyBound(err) };
|
|
141
|
-
}
|
|
142
141
|
if (err.code === 'NOT_BOUND') {
|
|
143
142
|
return { text: notBound() };
|
|
144
143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
@@ -45,23 +45,23 @@
|
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"dev": "node src/cli.js --help",
|
|
48
|
-
"build": "echo
|
|
48
|
+
"build": "echo 'No build step needed (pure ES modules)'",
|
|
49
49
|
"lint": "eslint \"**/*.{js,mjs,cjs}\" --no-error-on-unmatched-pattern",
|
|
50
50
|
"typecheck": "echo \"No typecheck configured yet (coclaw openclaw plugin)\"",
|
|
51
51
|
"check": "pnpm lint && pnpm typecheck",
|
|
52
52
|
"test:standalone": "node --test src/standalone-mode.test.js",
|
|
53
53
|
"test:plugin": "node --test src/plugin-mode.test.js",
|
|
54
54
|
"test": "node --test",
|
|
55
|
-
"coverage": "c8 --check-coverage --lines 100 --functions 100 --branches
|
|
55
|
+
"coverage": "c8 --check-coverage --lines 100 --functions 100 --branches 95 --statements 100 --reporter=text --reporter=lcov node --test",
|
|
56
56
|
"verify": "pnpm check && pnpm test:standalone && pnpm test:plugin && pnpm test && pnpm coverage",
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
57
|
+
"link": "bash ./scripts/link.sh",
|
|
58
|
+
"unlink": "bash ./scripts/unlink.sh",
|
|
59
|
+
"install:npm": "bash ./scripts/install-npm.sh",
|
|
60
|
+
"uninstall:npm": "bash ./scripts/uninstall-npm.sh",
|
|
61
|
+
"prerelease": "bash ./scripts/prerelease.sh",
|
|
62
|
+
"release": "bash ./scripts/release.sh",
|
|
63
|
+
"release:check": "bash ./scripts/release-check.sh",
|
|
64
|
+
"release:versions": "npm view @coclaw/openclaw-coclaw versions --json --registry=https://registry.npmjs.org/ && npm view @coclaw/openclaw-coclaw versions --json"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"c8": "^10.1.3",
|
package/src/api.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
async function requestJson(baseUrl, path, { method = 'GET', headers, body } = {}) {
|
|
1
|
+
async function requestJson(baseUrl, path, { method = 'GET', headers, body, timeout } = {}) {
|
|
2
2
|
const url = new URL(path, baseUrl).toString();
|
|
3
|
-
const
|
|
3
|
+
const fetchOpts = {
|
|
4
4
|
method,
|
|
5
5
|
headers,
|
|
6
6
|
body: body == null ? undefined : JSON.stringify(body),
|
|
7
|
-
}
|
|
7
|
+
};
|
|
8
|
+
if (timeout) {
|
|
9
|
+
fetchOpts.signal = AbortSignal.timeout(timeout);
|
|
10
|
+
}
|
|
11
|
+
const res = await fetch(url, fetchOpts);
|
|
8
12
|
let data = null;
|
|
9
13
|
try {
|
|
10
14
|
data = await res.json();
|
|
@@ -21,37 +25,25 @@ async function requestJson(baseUrl, path, { method = 'GET', headers, body } = {}
|
|
|
21
25
|
return data;
|
|
22
26
|
}
|
|
23
27
|
|
|
28
|
+
const BIND_TIMEOUT = 30_000;
|
|
29
|
+
const UNBIND_TIMEOUT = 15_000;
|
|
30
|
+
|
|
24
31
|
export async function bindWithServer({ baseUrl, code, name }) {
|
|
25
32
|
return requestJson(baseUrl, '/api/v1/bots/bind', {
|
|
26
33
|
method: 'POST',
|
|
27
34
|
headers: { 'content-type': 'application/json' },
|
|
28
35
|
body: { code, name },
|
|
36
|
+
timeout: BIND_TIMEOUT,
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
export async function unbindWithServer({ baseUrl, token }) {
|
|
40
|
+
export async function unbindWithServer({ baseUrl, token, timeout = UNBIND_TIMEOUT }) {
|
|
33
41
|
return requestJson(baseUrl, '/api/v1/bots/unbind', {
|
|
34
42
|
method: 'POST',
|
|
35
43
|
headers: {
|
|
36
44
|
Authorization: `Bearer ${token}`,
|
|
37
45
|
},
|
|
46
|
+
timeout,
|
|
38
47
|
});
|
|
39
48
|
}
|
|
40
49
|
|
|
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
|
-
}
|
package/src/channel-plugin.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { DEFAULT_ACCOUNT_ID } from './config.js';
|
|
2
|
-
import { createTransportAdapter } from './transport-adapter.js';
|
|
3
2
|
|
|
4
3
|
function resolveAccount(_cfg, accountId) {
|
|
5
4
|
const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
@@ -11,8 +10,6 @@ function resolveAccount(_cfg, accountId) {
|
|
|
11
10
|
};
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const transport = createTransportAdapter();
|
|
15
|
-
|
|
16
13
|
export const coclawChannelPlugin = {
|
|
17
14
|
id: 'coclaw',
|
|
18
15
|
meta: {
|
|
@@ -44,21 +41,15 @@ export const coclawChannelPlugin = {
|
|
|
44
41
|
},
|
|
45
42
|
outbound: {
|
|
46
43
|
deliveryMode: 'direct',
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
channel: 'coclaw',
|
|
55
|
-
messageId: result.messageId ?? `coclaw-local-${Date.now()}`,
|
|
56
|
-
to,
|
|
57
|
-
text,
|
|
58
|
-
accepted: Boolean(result.accepted),
|
|
59
|
-
};
|
|
60
|
-
},
|
|
44
|
+
// placeholder: CoClaw 消息实际通过 realtime-bridge WebSocket 桥接发送,
|
|
45
|
+
// 此 sendText 仅满足 OpenClaw channel 注册要求。
|
|
46
|
+
sendText: async ({ to }) => ({
|
|
47
|
+
channel: 'coclaw',
|
|
48
|
+
messageId: `coclaw-${Date.now()}`,
|
|
49
|
+
to,
|
|
50
|
+
}),
|
|
61
51
|
},
|
|
52
|
+
// TODO: status.defaultRuntime.running 应反映 realtime-bridge 实际连接状态
|
|
62
53
|
status: {
|
|
63
54
|
defaultRuntime: {
|
|
64
55
|
accountId: DEFAULT_ACCOUNT_ID,
|
package/src/cli-registrar.js
CHANGED
|
@@ -2,7 +2,7 @@ import { bindBot, unbindBot } from './common/bot-binding.js';
|
|
|
2
2
|
import { resolveErrorMessage } from './common/errors.js';
|
|
3
3
|
import { callGatewayMethod } from './common/gateway-notify.js';
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
notBound, bindOk, unbindOk,
|
|
6
6
|
gatewayNotified, gatewayNotifyFailed,
|
|
7
7
|
} from './common/messages.js';
|
|
8
8
|
|
|
@@ -47,17 +47,14 @@ export function registerCoclawCli({ program, config, logger }, deps = {}) {
|
|
|
47
47
|
.option('--server <url>', 'CoClaw server URL')
|
|
48
48
|
.action(async (code, opts) => {
|
|
49
49
|
try {
|
|
50
|
+
// 先断开 bridge,避免 unbindWithServer 触发的 bot.unbound 竞态
|
|
51
|
+
await notifyGateway('coclaw.stopBridge');
|
|
50
52
|
const serverUrl = resolveServerUrl(opts, config);
|
|
51
53
|
const result = await bindBot({ code, serverUrl });
|
|
52
54
|
/* c8 ignore next */
|
|
53
55
|
console.log(bindOk(result));
|
|
54
56
|
await notifyGateway('coclaw.refreshBridge');
|
|
55
57
|
} catch (err) {
|
|
56
|
-
if (err.code === 'ALREADY_BOUND') {
|
|
57
|
-
console.error(alreadyBound(err));
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
58
|
console.error(`Error: ${resolveErrorMessage(err)}`);
|
|
62
59
|
process.exitCode = 1;
|
|
63
60
|
}
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { bindBot, unbindBot } from './common/bot-binding.js';
|
|
|
7
7
|
import { resolveErrorMessage } from './common/errors.js';
|
|
8
8
|
import { callGatewayMethod } from './common/gateway-notify.js';
|
|
9
9
|
import {
|
|
10
|
-
|
|
10
|
+
notBound, bindOk, unbindOk,
|
|
11
11
|
gatewayNotified, gatewayNotifyFailed,
|
|
12
12
|
} from './common/messages.js';
|
|
13
13
|
|
|
@@ -66,22 +66,16 @@ export async function main(argv = process.argv.slice(2), deps = {}) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
if (command === 'bind') {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (err.code === 'ALREADY_BOUND') {
|
|
80
|
-
console.error(alreadyBound(err));
|
|
81
|
-
return 1;
|
|
82
|
-
}
|
|
83
|
-
throw err;
|
|
84
|
-
}
|
|
69
|
+
// 先断开 bridge,避免 unbindWithServer 触发的 bot.unbound 竞态
|
|
70
|
+
await notifyGateway('coclaw.stopBridge', deps);
|
|
71
|
+
const result = await bindBot({
|
|
72
|
+
code: positionals[0],
|
|
73
|
+
serverUrl: options.server,
|
|
74
|
+
});
|
|
75
|
+
/* c8 ignore next */
|
|
76
|
+
console.log(bindOk(result));
|
|
77
|
+
await notifyGateway('coclaw.refreshBridge', deps);
|
|
78
|
+
return 0;
|
|
85
79
|
}
|
|
86
80
|
|
|
87
81
|
if (command === 'unbind') {
|
|
@@ -9,11 +9,20 @@ export async function bindBot({ code, serverUrl }) {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
const config = await readConfig();
|
|
12
|
+
|
|
13
|
+
// 已绑定时自动解绑再重绑(解绑尽力而为,不阻塞新绑定)
|
|
14
|
+
let previousBotId;
|
|
12
15
|
if (config?.token) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
previousBotId = config.botId || 'unknown';
|
|
17
|
+
const oldBaseUrl = config.serverUrl;
|
|
18
|
+
if (oldBaseUrl) {
|
|
19
|
+
try {
|
|
20
|
+
await unbindWithServer({ baseUrl: oldBaseUrl, token: config.token });
|
|
21
|
+
} catch {
|
|
22
|
+
// 尽力而为,忽略解绑错误
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
await clearConfig();
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
/* c8 ignore next */
|
|
@@ -27,18 +36,17 @@ export async function bindBot({ code, serverUrl }) {
|
|
|
27
36
|
throw new Error('invalid bind response');
|
|
28
37
|
}
|
|
29
38
|
|
|
30
|
-
|
|
31
|
-
...config,
|
|
39
|
+
await writeConfig({
|
|
32
40
|
serverUrl: baseUrl,
|
|
33
41
|
botId: data.botId,
|
|
34
42
|
token: data.token,
|
|
35
43
|
boundAt: new Date().toISOString(),
|
|
36
|
-
};
|
|
37
|
-
await writeConfig(next);
|
|
44
|
+
});
|
|
38
45
|
|
|
39
46
|
return {
|
|
40
47
|
botId: data.botId,
|
|
41
48
|
rebound: Boolean(data.rebound),
|
|
49
|
+
previousBotId,
|
|
42
50
|
};
|
|
43
51
|
}
|
|
44
52
|
|
package/src/common/messages.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// bind/unbind CLI 及 command 的用户提示文案(统一出口)
|
|
2
2
|
|
|
3
|
-
export function bindOk({ botId, rebound }) {
|
|
3
|
+
export function bindOk({ botId, rebound, previousBotId }) {
|
|
4
4
|
const action = rebound ? 're-bound' : 'bound';
|
|
5
|
-
|
|
5
|
+
const prev = previousBotId
|
|
6
|
+
? ` (previous binding to bot ${previousBotId} was auto-removed)`
|
|
7
|
+
: '';
|
|
8
|
+
return `OK. Bot (${botId}) ${action} to CoClaw.${prev}`;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export function unbindOk({ botId, serverError }) {
|
|
@@ -13,11 +16,6 @@ export function unbindOk({ botId, serverError }) {
|
|
|
13
16
|
return `OK. Bot (${id}) unbound from CoClaw.${tag}`;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export function alreadyBound({ botId }) {
|
|
17
|
-
const id = botId ?? 'unknown';
|
|
18
|
-
return `Already bound to CoClaw as bot (${id}).\nRun \`openclaw coclaw unbind\` to unbind first.`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
19
|
export function notBound() {
|
|
22
20
|
return 'Not bound. Nothing to unbind.';
|
|
23
21
|
}
|
package/src/config.js
CHANGED
|
@@ -48,112 +48,12 @@ async function writeJson(filePath, value) {
|
|
|
48
48
|
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// --- 旧位置迁移 ---
|
|
52
|
-
|
|
53
|
-
function getOpenclawConfigPath() {
|
|
54
|
-
return process.env.OPENCLAW_CONFIG_PATH
|
|
55
|
-
? nodePath.resolve(process.env.OPENCLAW_CONFIG_PATH)
|
|
56
|
-
: nodePath.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function pickFromOldConfig(rootCfg) {
|
|
60
|
-
const channels = toRecord(rootCfg.channels);
|
|
61
|
-
const coclaw = toRecord(channels.coclaw);
|
|
62
|
-
const accounts = toRecord(coclaw.accounts);
|
|
63
|
-
const account = toRecord(accounts.default);
|
|
64
|
-
return {
|
|
65
|
-
serverUrl: account.serverUrl ?? coclaw.serverUrl,
|
|
66
|
-
botId: account.botId ?? coclaw.botId,
|
|
67
|
-
token: account.token ?? coclaw.token,
|
|
68
|
-
boundAt: account.boundAt ?? coclaw.boundAt,
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async function tryMigrateFromOldLocations() {
|
|
73
|
-
// 1. 尝试从 openclaw.json channels.coclaw 迁移
|
|
74
|
-
const rt = getRuntime();
|
|
75
|
-
let oldData;
|
|
76
|
-
if (rt?.config?.loadConfig) {
|
|
77
|
-
oldData = pickFromOldConfig(rt.config.loadConfig());
|
|
78
|
-
} else {
|
|
79
|
-
const rootCfg = await readJson(getOpenclawConfigPath());
|
|
80
|
-
oldData = pickFromOldConfig(rootCfg);
|
|
81
|
-
}
|
|
82
|
-
if (oldData.token) {
|
|
83
|
-
return oldData;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// 2. 尝试从 legacy 文件迁移
|
|
87
|
-
const legacyPaths = [
|
|
88
|
-
nodePath.resolve(process.cwd(), '.coclaw-tunnel.json'),
|
|
89
|
-
nodePath.join(os.homedir(), '.coclaw-tunnel.json'),
|
|
90
|
-
];
|
|
91
|
-
for (const p of legacyPaths) {
|
|
92
|
-
const legacy = await readJson(p);
|
|
93
|
-
if (legacy?.token) {
|
|
94
|
-
return legacy;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async function cleanOldLocations() {
|
|
102
|
-
// 清理 openclaw.json 中的 channels.coclaw
|
|
103
|
-
const rt = getRuntime();
|
|
104
|
-
if (rt?.config?.loadConfig && rt?.config?.writeConfigFile) {
|
|
105
|
-
const rootCfg = structuredClone(rt.config.loadConfig());
|
|
106
|
-
const channels = toRecord(rootCfg.channels);
|
|
107
|
-
if (channels.coclaw) {
|
|
108
|
-
delete channels.coclaw;
|
|
109
|
-
rootCfg.channels = channels;
|
|
110
|
-
await rt.config.writeConfigFile(rootCfg);
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
const filePath = getOpenclawConfigPath();
|
|
114
|
-
const rootCfg = toRecord(await readJson(filePath));
|
|
115
|
-
const channels = toRecord(rootCfg.channels);
|
|
116
|
-
if (channels.coclaw) {
|
|
117
|
-
delete channels.coclaw;
|
|
118
|
-
rootCfg.channels = channels;
|
|
119
|
-
await writeJson(filePath, rootCfg);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// 清理 legacy 文件
|
|
124
|
-
const legacyPaths = [
|
|
125
|
-
nodePath.resolve(process.cwd(), '.coclaw-tunnel.json'),
|
|
126
|
-
nodePath.join(os.homedir(), '.coclaw-tunnel.json'),
|
|
127
|
-
];
|
|
128
|
-
for (const p of legacyPaths) {
|
|
129
|
-
const legacy = await readJson(p);
|
|
130
|
-
if (legacy?.token) {
|
|
131
|
-
await writeJson(p, {});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
51
|
// --- 公共 API ---
|
|
137
52
|
|
|
138
53
|
export async function readConfig(accountId = DEFAULT_ACCOUNT_ID) {
|
|
139
54
|
const bindingsPath = getBindingsPath();
|
|
140
55
|
const bindings = await readJson(bindingsPath);
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (entry.token) {
|
|
144
|
-
return entry;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// 首次运行:尝试从旧位置迁移
|
|
148
|
-
const migrated = await tryMigrateFromOldLocations();
|
|
149
|
-
if (migrated?.token) {
|
|
150
|
-
const newBindings = { ...bindings, [accountId]: migrated };
|
|
151
|
-
await writeJson(bindingsPath, newBindings);
|
|
152
|
-
await cleanOldLocations();
|
|
153
|
-
return migrated;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return entry;
|
|
56
|
+
return toRecord(bindings[accountId]);
|
|
157
57
|
}
|
|
158
58
|
|
|
159
59
|
export async function writeConfig(nextConfig, accountId = DEFAULT_ACCOUNT_ID) {
|
|
@@ -188,7 +88,4 @@ export async function clearConfig(accountId = DEFAULT_ACCOUNT_ID) {
|
|
|
188
88
|
} else {
|
|
189
89
|
await writeJson(bindingsPath, bindings);
|
|
190
90
|
}
|
|
191
|
-
|
|
192
|
-
// 确保清理旧位置残留
|
|
193
|
-
await cleanOldLocations();
|
|
194
91
|
}
|