@coclaw/openclaw-coclaw 0.9.2 → 0.11.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/README.md +21 -13
- package/index.js +41 -47
- package/package.json +5 -9
- package/src/auto-upgrade/updater.js +5 -0
- package/src/common/gateway-notify.js +25 -8
- package/src/common/messages.js +0 -10
- package/src/config.js +5 -3
- package/src/device-identity.js +4 -2
- package/src/plugin-version.js +18 -0
- package/src/realtime-bridge.js +118 -14
- package/src/remote-log.js +70 -0
- package/src/settings.js +79 -0
- package/src/webrtc/ndc-preloader.js +191 -0
- package/src/{webrtc-peer.js → webrtc/webrtc-peer.js} +45 -6
- package/vendor/ndc-prebuilds/darwin-arm64/node_datachannel.node +0 -0
- package/vendor/ndc-prebuilds/darwin-x64/node_datachannel.node +0 -0
- package/vendor/ndc-prebuilds/linux-arm64/node_datachannel.node +0 -0
- package/vendor/ndc-prebuilds/linux-x64/node_datachannel.node +0 -0
- package/vendor/ndc-prebuilds/win32-x64/node_datachannel.node +0 -0
- package/src/cli.js +0 -128
- /package/src/{utils → webrtc}/dc-chunking.js +0 -0
package/README.md
CHANGED
|
@@ -88,15 +88,6 @@ openclaw coclaw enroll [--server <url>]
|
|
|
88
88
|
|
|
89
89
|
需要 gateway 运行中。
|
|
90
90
|
|
|
91
|
-
### 方式三:独立 CLI(遗留)
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
node ~/.openclaw/extensions/coclaw/src/cli.js bind <binding-code> --server <url>
|
|
95
|
-
node ~/.openclaw/extensions/coclaw/src/cli.js unbind --server <url>
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
> 注意:独立 CLI 不走 gateway RPC,直接在 CLI 进程中执行 bind/unbind 并通过 `coclaw.refreshBridge`/`coclaw.stopBridge` 通知 gateway。此路径不具备瘦 CLI 的架构保证(所有 config 操作在同一进程内)。推荐使用方式一。
|
|
99
|
-
|
|
100
91
|
## 配置存储
|
|
101
92
|
|
|
102
93
|
绑定信息存储在 `~/.openclaw/coclaw/bindings.json`(通过 `resolveStateDir()` + channel ID 组合路径),**不存储在 `openclaw.json` 中**。
|
|
@@ -137,6 +128,25 @@ openclaw gateway call coclaw.upgradeHealth --json
|
|
|
137
128
|
|
|
138
129
|
详见设计文档 `docs/auto-upgrade.md`。
|
|
139
130
|
|
|
131
|
+
## WebRTC 实现
|
|
132
|
+
|
|
133
|
+
插件支持两个 WebRTC 实现,运行时自动选择:
|
|
134
|
+
|
|
135
|
+
1. **node-datachannel**(首选)— 基于 libdatachannel 的工业级实现,通过 vendor 预编译 native binary 部署。
|
|
136
|
+
2. **werift**(回退)— 纯 JavaScript 实现,作为 node-datachannel 加载失败时的兜底。
|
|
137
|
+
|
|
138
|
+
选择结果通过 remoteLog 上报(`ndc.loaded` 或 `ndc.using-werift`)。
|
|
139
|
+
|
|
140
|
+
### vendor 预编译包
|
|
141
|
+
|
|
142
|
+
由于 OpenClaw 使用 `--ignore-scripts` 安装插件,node-datachannel 的 native binary 需通过 vendor 预编译包提供:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
bash scripts/download-ndc-prebuilds.sh # 下载 5 平台预编译包到 vendor/ndc-prebuilds/
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
支持的平台:linux-x64、linux-arm64、darwin-x64、darwin-arm64、win32-x64。vendor 目录不入 git,通过 npm publish 的 `files` 字段包含在发布包中。
|
|
149
|
+
|
|
140
150
|
## 运行与排障日志
|
|
141
151
|
|
|
142
152
|
### 日志级别建议
|
|
@@ -176,10 +186,8 @@ openclaw logs --limit 300 --plain | rg -n "ui->server req|bot->server res|bot->s
|
|
|
176
186
|
## 测试门禁
|
|
177
187
|
|
|
178
188
|
```bash
|
|
179
|
-
pnpm check
|
|
180
|
-
pnpm test #
|
|
181
|
-
pnpm coverage # 覆盖率检查
|
|
182
|
-
pnpm verify # 完整验证(check → test:standalone → test:plugin → test → coverage)
|
|
189
|
+
pnpm check # lint + typecheck
|
|
190
|
+
pnpm test # 测试 + 覆盖率检查
|
|
183
191
|
```
|
|
184
192
|
|
|
185
193
|
覆盖率阈值:lines/statements/functions 100%,branches ≥ 95%。未通过禁止接入 gateway。
|
package/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import nodePath from 'node:path';
|
|
3
|
-
|
|
4
1
|
import { bindBot, unbindBot, enrollBot, waitForClaimAndSave } from './src/common/bot-binding.js';
|
|
5
2
|
import { registerCoclawCli } from './src/cli-registrar.js';
|
|
6
3
|
import { resolveErrorMessage } from './src/common/errors.js';
|
|
7
4
|
import { notBound, bindOk, unbindOk, claimCodeCreated } from './src/common/messages.js';
|
|
8
5
|
import { coclawChannelPlugin } from './src/channel-plugin.js';
|
|
9
|
-
import { ensureAgentSession, gatewayAgentRpc, restartRealtimeBridge, stopRealtimeBridge, waitForSessionsReady } from './src/realtime-bridge.js';
|
|
6
|
+
import { ensureAgentSession, gatewayAgentRpc, restartRealtimeBridge, stopRealtimeBridge, waitForSessionsReady, broadcastPluginEvent } from './src/realtime-bridge.js';
|
|
7
|
+
import { getHostName, readSettings, writeName, MAX_NAME_LENGTH } from './src/settings.js';
|
|
10
8
|
import { setRuntime } from './src/runtime.js';
|
|
11
9
|
import { createSessionManager } from './src/session-manager/manager.js';
|
|
12
10
|
import { TopicManager } from './src/topic-manager/manager.js';
|
|
@@ -16,27 +14,12 @@ import { AutoUpgradeScheduler } from './src/auto-upgrade/updater.js';
|
|
|
16
14
|
import { getPackageInfo } from './src/auto-upgrade/updater-check.js';
|
|
17
15
|
import { createFileHandler } from './src/file-manager/handler.js';
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
export async function getPluginVersion() {
|
|
22
|
-
if (__pluginVersion) return __pluginVersion;
|
|
23
|
-
try {
|
|
24
|
-
const pkgPath = nodePath.resolve(import.meta.dirname, 'package.json');
|
|
25
|
-
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
26
|
-
__pluginVersion = JSON.parse(raw).version ?? 'unknown';
|
|
27
|
-
} catch {
|
|
28
|
-
return 'unknown';
|
|
29
|
-
}
|
|
30
|
-
return __pluginVersion;
|
|
31
|
-
}
|
|
32
|
-
// 测试用:重置缓存
|
|
33
|
-
export function __resetPluginVersion() { __pluginVersion = null; }
|
|
34
|
-
|
|
17
|
+
import { getPluginVersion, __resetPluginVersion } from './src/plugin-version.js';
|
|
18
|
+
export { getPluginVersion, __resetPluginVersion };
|
|
35
19
|
|
|
20
|
+
/* c8 ignore start */
|
|
36
21
|
function parseCommandArgs(args) {
|
|
37
|
-
/* c8 ignore next */
|
|
38
22
|
const tokens = (args ?? '').split(/\s+/).filter(Boolean);
|
|
39
|
-
/* c8 ignore next */
|
|
40
23
|
const action = tokens[0] ?? 'help';
|
|
41
24
|
const options = {};
|
|
42
25
|
const positionals = [];
|
|
@@ -67,7 +50,6 @@ function buildHelpText() {
|
|
|
67
50
|
function respondError(respond, err) {
|
|
68
51
|
respond(false, undefined, {
|
|
69
52
|
code: err?.code ?? 'INTERNAL_ERROR',
|
|
70
|
-
/* c8 ignore next */
|
|
71
53
|
message: String(err?.message ?? err),
|
|
72
54
|
});
|
|
73
55
|
}
|
|
@@ -75,8 +57,6 @@ function respondError(respond, err) {
|
|
|
75
57
|
function respondInvalid(respond, message) {
|
|
76
58
|
respond(false, undefined, { code: 'INVALID_INPUT', message });
|
|
77
59
|
}
|
|
78
|
-
|
|
79
|
-
/* c8 ignore start */
|
|
80
60
|
const plugin = {
|
|
81
61
|
id: 'openclaw-coclaw',
|
|
82
62
|
name: 'CoClaw',
|
|
@@ -130,26 +110,6 @@ const plugin = {
|
|
|
130
110
|
},
|
|
131
111
|
});
|
|
132
112
|
|
|
133
|
-
api.registerGatewayMethod('coclaw.refreshBridge', async ({ respond }) => {
|
|
134
|
-
try {
|
|
135
|
-
await restartRealtimeBridge({ logger, pluginConfig: api.pluginConfig });
|
|
136
|
-
respond(true, { status: 'refreshed' });
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
respondError(respond, err);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
api.registerGatewayMethod('coclaw.stopBridge', async ({ respond }) => {
|
|
144
|
-
try {
|
|
145
|
-
await stopRealtimeBridge();
|
|
146
|
-
respond(true, { status: 'stopped' });
|
|
147
|
-
}
|
|
148
|
-
catch (err) {
|
|
149
|
-
respondError(respond, err);
|
|
150
|
-
}
|
|
151
|
-
});
|
|
152
|
-
|
|
153
113
|
// --- bind/unbind 共享逻辑(RPC handler + 斜杠命令共用) ---
|
|
154
114
|
|
|
155
115
|
async function doBind({ code, serverUrl }) {
|
|
@@ -290,14 +250,48 @@ const plugin = {
|
|
|
290
250
|
}
|
|
291
251
|
});
|
|
292
252
|
|
|
293
|
-
|
|
253
|
+
async function handleInfoGet({ respond }) {
|
|
294
254
|
try {
|
|
295
255
|
await waitForSessionsReady();
|
|
296
256
|
const version = await getPluginVersion();
|
|
297
257
|
const rawClawVersion = api.runtime?.version;
|
|
298
258
|
// OpenClaw 打包后 resolveVersion() 路径失配导致返回 'unknown',此时不传该字段
|
|
299
259
|
const clawVersion = (rawClawVersion && rawClawVersion !== 'unknown') ? rawClawVersion : undefined;
|
|
300
|
-
|
|
260
|
+
const settings = await readSettings();
|
|
261
|
+
const name = settings.name ?? null;
|
|
262
|
+
const hostName = getHostName();
|
|
263
|
+
respond(true, { version, clawVersion, capabilities: ['topics', 'chatHistory'], name, hostName });
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
respondError(respond, err);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
api.registerGatewayMethod('coclaw.info', handleInfoGet);
|
|
271
|
+
api.registerGatewayMethod('coclaw.info.get', handleInfoGet);
|
|
272
|
+
|
|
273
|
+
api.registerGatewayMethod('coclaw.info.patch', async ({ params, respond }) => {
|
|
274
|
+
try {
|
|
275
|
+
const rawName = params?.name;
|
|
276
|
+
if (rawName === undefined) {
|
|
277
|
+
respondInvalid(respond, 'name field is required');
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (rawName !== null && typeof rawName !== 'string') {
|
|
281
|
+
respondInvalid(respond, 'name must be a string or null');
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const trimmed = typeof rawName === 'string' ? rawName.trim() : '';
|
|
285
|
+
if (trimmed.length > MAX_NAME_LENGTH) {
|
|
286
|
+
respondInvalid(respond, `name exceeds maximum length of ${MAX_NAME_LENGTH} characters`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const nameToSave = trimmed || null;
|
|
290
|
+
await writeName(nameToSave);
|
|
291
|
+
const hostName = getHostName();
|
|
292
|
+
respond(true, { name: nameToSave, hostName });
|
|
293
|
+
// 异步广播变更事件到 server 和其他 UI 实例
|
|
294
|
+
broadcastPluginEvent('coclaw.info.updated', { name: nameToSave, hostName });
|
|
301
295
|
}
|
|
302
296
|
catch (err) {
|
|
303
297
|
respondError(respond, err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coclaw/openclaw-coclaw",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"description": "OpenClaw CoClaw channel plugin for remote chat",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"!src/**/*.test.js",
|
|
33
33
|
"!src/mock-server.helper.js",
|
|
34
34
|
"openclaw.plugin.json",
|
|
35
|
+
"vendor/ndc-prebuilds/**",
|
|
35
36
|
"LICENSE"
|
|
36
37
|
],
|
|
37
38
|
"main": "index.js",
|
|
@@ -40,20 +41,14 @@
|
|
|
40
41
|
"./index.js"
|
|
41
42
|
]
|
|
42
43
|
},
|
|
43
|
-
"bin": {
|
|
44
|
-
"coclaw": "src/cli.js"
|
|
45
|
-
},
|
|
46
44
|
"scripts": {
|
|
47
|
-
"dev": "node src/cli.js --help",
|
|
48
45
|
"build": "echo 'No build step needed (pure ES modules)'",
|
|
49
46
|
"lint": "eslint \"**/*.{js,mjs,cjs}\" --no-error-on-unmatched-pattern",
|
|
50
47
|
"typecheck": "echo \"No typecheck configured yet (coclaw openclaw plugin)\"",
|
|
51
48
|
"check": "pnpm lint && pnpm typecheck",
|
|
52
|
-
"test:standalone": "node --test src/standalone-mode.test.js",
|
|
53
49
|
"test:plugin": "node --test src/plugin-mode.test.js",
|
|
54
|
-
"test": "node --test",
|
|
55
|
-
"
|
|
56
|
-
"verify": "pnpm check && pnpm coverage",
|
|
50
|
+
"test": "c8 --check-coverage --lines 100 --functions 100 --branches 94 --statements 100 --reporter=text --reporter=lcov bash -c 'for f in src/**/*.test.js src/*.test.js index.test.js; do node --test \"$f\" || exit 1; done'",
|
|
51
|
+
"verify": "pnpm check && pnpm test",
|
|
57
52
|
"link": "bash ./scripts/link.sh",
|
|
58
53
|
"unlink": "bash ./scripts/unlink.sh",
|
|
59
54
|
"install:npm": "bash ./scripts/install-npm.sh",
|
|
@@ -64,6 +59,7 @@
|
|
|
64
59
|
"release:versions": "npm view @coclaw/openclaw-coclaw versions --json --registry=https://registry.npmjs.org/ && npm view @coclaw/openclaw-coclaw versions --json"
|
|
65
60
|
},
|
|
66
61
|
"dependencies": {
|
|
62
|
+
"node-datachannel": "0.32.1",
|
|
67
63
|
"werift": "^0.19.0"
|
|
68
64
|
},
|
|
69
65
|
"devDependencies": {
|
|
@@ -5,6 +5,7 @@ import { checkForUpdate } from './updater-check.js';
|
|
|
5
5
|
import { spawnUpgradeWorker } from './updater-spawn.js';
|
|
6
6
|
import { resolveStateDir } from './state.js';
|
|
7
7
|
import { getRuntime } from '../runtime.js';
|
|
8
|
+
import { remoteLog } from '../remote-log.js';
|
|
8
9
|
|
|
9
10
|
const INITIAL_DELAY_MS = 5 * 60 * 1000; // 5 分钟
|
|
10
11
|
const CHECK_INTERVAL_MS = 60 * 60 * 1000; // 1 小时
|
|
@@ -203,6 +204,7 @@ export class AutoUpgradeScheduler {
|
|
|
203
204
|
// 若上一次 spawn 的 worker 仍在运行,跳过本次检查
|
|
204
205
|
const isLocked = this.__opts.isUpgradeLockedFn ?? isUpgradeLocked;
|
|
205
206
|
if (await isLocked({ logger: this.__logger })) {
|
|
207
|
+
remoteLog('upgrade.worker-locked');
|
|
206
208
|
this.__logger.info?.('[auto-upgrade] Upgrade worker still running, skipping check');
|
|
207
209
|
return;
|
|
208
210
|
}
|
|
@@ -221,11 +223,13 @@ export class AutoUpgradeScheduler {
|
|
|
221
223
|
return;
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
remoteLog(`upgrade.available from=${result.currentVersion} to=${result.latestVersion}`);
|
|
224
227
|
this.__logger.info?.(`[auto-upgrade] Update available: ${result.currentVersion} → ${result.latestVersion}`);
|
|
225
228
|
|
|
226
229
|
const getInstallPath = this.__opts.getPluginInstallPathFn ?? getPluginInstallPath;
|
|
227
230
|
const pluginDir = getInstallPath(this.__pluginId);
|
|
228
231
|
if (!pluginDir) {
|
|
232
|
+
remoteLog('upgrade.no-install-path');
|
|
229
233
|
this.__logger.warn?.('[auto-upgrade] Cannot determine plugin install path');
|
|
230
234
|
return;
|
|
231
235
|
}
|
|
@@ -245,6 +249,7 @@ export class AutoUpgradeScheduler {
|
|
|
245
249
|
await writeLock(child.pid);
|
|
246
250
|
}
|
|
247
251
|
catch (err) {
|
|
252
|
+
remoteLog(`upgrade.check-failed msg=${err.message}`);
|
|
248
253
|
this.__logger.warn?.(`[auto-upgrade] Check failed: ${err.message}`);
|
|
249
254
|
}
|
|
250
255
|
finally {
|
|
@@ -15,16 +15,33 @@ export function escapeJsonForCmd(json) {
|
|
|
15
15
|
/**
|
|
16
16
|
* 通过 spawn 调用 `openclaw gateway call <method> --json`
|
|
17
17
|
*
|
|
18
|
-
*
|
|
19
|
-
* 进程不会自然退出。execSync 会一直阻塞直到超时,导致误报失败。
|
|
18
|
+
* ## 设计背景
|
|
20
19
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* 3. 检测到输出后延迟 KILL_DELAY_MS 再 kill(给进程自然退出的机会)
|
|
25
|
-
* 4. 无论成功失败,最终都主动 kill 子进程
|
|
20
|
+
* openclaw CLI 完成 gateway RPC 后,因 GatewayClient(WebSocket)handle 未完全销毁,
|
|
21
|
+
* 事件循环仍活跃,进程不会自然退出。早期使用 execSync 会阻塞等待进程退出而非输出完成,
|
|
22
|
+
* 导致 RPC 实际在 ~2s 内成功,但 10s 超时后误报 100% 失败。
|
|
26
23
|
*
|
|
27
|
-
*
|
|
24
|
+
* ## 策略
|
|
25
|
+
*
|
|
26
|
+
* 1. spawn 子进程执行 `openclaw gateway call <method> --json`
|
|
27
|
+
* 2. 监听 stdout,解析 JSON 输出判断 RPC 成功/失败
|
|
28
|
+
* 3. 检测到完整 JSON 后启动 KILL_DELAY_MS grace period 等待自然退出
|
|
29
|
+
* 4. 总超时默认 NOTIFY_TIMEOUT_MS(注册 CLI 路径覆盖为 30s)
|
|
30
|
+
* 5. 无论成功失败,最终都主动 kill 子进程
|
|
31
|
+
*
|
|
32
|
+
* grace period 设计:openclaw 进程因 WS handle 滞留可能 10s+ 才退出,
|
|
33
|
+
* 延迟 kill 是为兼容未来 OpenClaw 修复 WS 清理后进程能优雅退出的场景。
|
|
34
|
+
*
|
|
35
|
+
* ## stdout 判断策略
|
|
36
|
+
*
|
|
37
|
+
* `openclaw gateway call --json` 直接输出 method 的 result payload(respond 第二参数),
|
|
38
|
+
* 而非 gateway 协议层 { ok, result, error } 包装:
|
|
39
|
+
* - 有 stdout + 可解析 JSON → RPC 成功
|
|
40
|
+
* - 有 stdout + 非 JSON → 也视为成功(兜底)
|
|
41
|
+
* - 无 stdout + 非零退出码 → RPC 失败
|
|
42
|
+
* - 无 stdout + 超时 → RPC 失败
|
|
43
|
+
*
|
|
44
|
+
* @param {string} method - gateway method 名(如 coclaw.bind)
|
|
28
45
|
* @param {Function} [spawnFn] - 可注入的 spawn 函数(测试用)
|
|
29
46
|
* @param {object} [opts] - 可选配置(测试用)
|
|
30
47
|
* @param {number} [opts.timeoutMs] - 总超时毫秒数
|
package/src/common/messages.js
CHANGED
|
@@ -17,16 +17,6 @@ export function notBound() {
|
|
|
17
17
|
return 'Not bound. Nothing to unbind.';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function gatewayNotified(action) {
|
|
21
|
-
return action === 'refresh'
|
|
22
|
-
? 'Bridge connection refreshed.'
|
|
23
|
-
: 'Bridge connection stopped.';
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function gatewayNotifyFailed() {
|
|
27
|
-
return 'Note: could not notify the running gateway. If it is running, restart it manually.';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
20
|
export function claimCodeCreated({ code, appUrl, expiresMinutes }) {
|
|
31
21
|
return [
|
|
32
22
|
`Claim code: ${code}`,
|
package/src/config.js
CHANGED
|
@@ -7,10 +7,10 @@ import { atomicWriteJsonFile } from './utils/atomic-write.js';
|
|
|
7
7
|
import { createMutex } from './utils/mutex.js';
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_ACCOUNT_ID = 'default';
|
|
10
|
-
const CHANNEL_ID = 'coclaw';
|
|
10
|
+
export const CHANNEL_ID = 'coclaw';
|
|
11
11
|
const BINDINGS_FILENAME = 'bindings.json';
|
|
12
12
|
|
|
13
|
-
function resolveStateDir() {
|
|
13
|
+
export function resolveStateDir() {
|
|
14
14
|
const rt = getRuntime();
|
|
15
15
|
if (rt?.state?.resolveStateDir) {
|
|
16
16
|
return rt.state.resolveStateDir();
|
|
@@ -42,8 +42,10 @@ async function readJson(filePath) {
|
|
|
42
42
|
try {
|
|
43
43
|
return JSON.parse(raw);
|
|
44
44
|
}
|
|
45
|
-
catch {
|
|
45
|
+
catch (err) {
|
|
46
46
|
// 文件损坏,删除后当空文件处理
|
|
47
|
+
/* c8 ignore next -- ?./?? fallback */
|
|
48
|
+
console.warn?.(`[coclaw] corrupt bindings file deleted: ${filePath} (${String(err?.message ?? err)})`);
|
|
47
49
|
await fs.unlink(filePath).catch(() => {});
|
|
48
50
|
return {};
|
|
49
51
|
}
|
package/src/device-identity.js
CHANGED
|
@@ -115,8 +115,10 @@ export function loadOrCreateDeviceIdentity(filePath) {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
-
catch {
|
|
119
|
-
//
|
|
118
|
+
catch (err) {
|
|
119
|
+
// 读取/解析失败时重新生成(将产生新 deviceId,需重新 enroll)
|
|
120
|
+
/* c8 ignore next -- ?./?? fallback */
|
|
121
|
+
console.warn?.(`[coclaw] device identity read failed, regenerating: ${String(err?.message ?? err)}`);
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
const identity = generateIdentity();
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import nodePath from 'node:path';
|
|
3
|
+
|
|
4
|
+
// 延迟读取 + 缓存:避免模块加载时 package.json 损坏导致插件整体无法注册
|
|
5
|
+
let __pluginVersion = null;
|
|
6
|
+
export async function getPluginVersion() {
|
|
7
|
+
if (__pluginVersion) return __pluginVersion;
|
|
8
|
+
try {
|
|
9
|
+
const pkgPath = nodePath.resolve(import.meta.dirname, '..', 'package.json');
|
|
10
|
+
const raw = await fs.readFile(pkgPath, 'utf8');
|
|
11
|
+
__pluginVersion = JSON.parse(raw).version ?? 'unknown';
|
|
12
|
+
} catch {
|
|
13
|
+
return 'unknown';
|
|
14
|
+
}
|
|
15
|
+
return __pluginVersion;
|
|
16
|
+
}
|
|
17
|
+
// 测试用:重置缓存
|
|
18
|
+
export function __resetPluginVersion() { __pluginVersion = null; }
|