@coclaw/openclaw-coclaw 0.10.0 → 0.11.2

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 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` 中**。
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
- // 延迟读取 + 缓存:避免模块加载时 package.json 损坏导致插件整体无法注册
20
- let __pluginVersion = null;
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
- api.registerGatewayMethod('coclaw.info', async ({ respond }) => {
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
- respond(true, { version, clawVersion, capabilities: ['topics', 'chatHistory'] });
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.10.0",
3
+ "version": "0.11.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "OpenClaw CoClaw channel plugin for remote chat",
@@ -41,11 +41,7 @@
41
41
  "./index.js"
42
42
  ]
43
43
  },
44
- "bin": {
45
- "coclaw": "src/cli.js"
46
- },
47
44
  "scripts": {
48
- "dev": "node src/cli.js --help",
49
45
  "build": "echo 'No build step needed (pure ES modules)'",
50
46
  "lint": "eslint \"**/*.{js,mjs,cjs}\" --no-error-on-unmatched-pattern",
51
47
  "typecheck": "echo \"No typecheck configured yet (coclaw openclaw plugin)\"",
@@ -15,16 +15,33 @@ export function escapeJsonForCmd(json) {
15
15
  /**
16
16
  * 通过 spawn 调用 `openclaw gateway call <method> --json`
17
17
  *
18
- * 背景:openclaw CLI 在完成 gateway RPC 后,因 WebSocket handle 未清理,
19
- * 进程不会自然退出。execSync 会一直阻塞直到超时,导致误报失败。
18
+ * ## 设计背景
20
19
  *
21
- * 策略:
22
- * 1. spawn 子进程,监听 stdout
23
- * 2. 解析 JSON 输出判断 RPC 是否成功
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
- * @param {string} method - gateway method 名(如 coclaw.refreshBridge)
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] - 总超时毫秒数
@@ -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();
@@ -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; }
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import nodePath from 'node:path';
4
4
 
5
5
  import { clearConfig, getBindingsPath, readConfig } from './config.js';
6
+ import { getHostName, readSettings } from './settings.js';
6
7
  import {
7
8
  loadOrCreateDeviceIdentity,
8
9
  signDevicePayload,
@@ -11,6 +12,7 @@ import {
11
12
  } from './device-identity.js';
12
13
  import { getRuntime } from './runtime.js';
13
14
  import { setSender as setRemoteLogSender, remoteLog } from './remote-log.js';
15
+ import { getPluginVersion } from './plugin-version.js';
14
16
 
15
17
  const DEFAULT_GATEWAY_WS_URL = `ws://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT || '18789'}`;
16
18
  const RECONNECT_MS = 10_000;
@@ -214,7 +216,7 @@ export class RealtimeBridge {
214
216
  throw new Error('No WebRTC implementation available');
215
217
  }
216
218
 
217
- const { WebRtcPeer } = await import('./webrtc-peer.js');
219
+ const { WebRtcPeer } = await import('./webrtc/webrtc-peer.js');
218
220
  const { createFileHandler } = await import('./file-manager/handler.js');
219
221
  this.__fileHandler = createFileHandler({
220
222
  resolveWorkspace: (agentId) => this.__resolveWorkspace(agentId),
@@ -423,6 +425,20 @@ export class RealtimeBridge {
423
425
  }
424
426
  }
425
427
 
428
+ /** 推送实例名到 server 和已连接的 UI */
429
+ async __pushInstanceName() {
430
+ try {
431
+ const settings = await readSettings();
432
+ const name = settings.name ?? null;
433
+ const hostName = getHostName();
434
+ broadcastPluginEvent('coclaw.info.updated', { name, hostName });
435
+ }
436
+ catch (err) {
437
+ /* c8 ignore next 2 -- 防御性兜底 */
438
+ this.logger.warn?.(`[coclaw] pushInstanceName failed: ${String(err?.message ?? err)}`);
439
+ }
440
+ }
441
+
426
442
  /* c8 ignore start -- 仅通过 WebRTC 路径调用,依赖 gateway 连接,集成测试覆盖 */
427
443
  /**
428
444
  * 通过 gateway RPC 获取指定 agent 的 workspace 绝对路径
@@ -519,7 +535,7 @@ export class RealtimeBridge {
519
535
  maxProtocol: 3,
520
536
  client: {
521
537
  id: 'gateway-client',
522
- version: 'dev',
538
+ version: this.__pluginVersion ?? 'unknown',
523
539
  platform: process.platform,
524
540
  mode: 'backend',
525
541
  },
@@ -580,6 +596,7 @@ export class RealtimeBridge {
580
596
  this.__logDebug(`gateway connect ok <- id=${payload.id}`);
581
597
  this.gatewayConnectReqId = null;
582
598
  this.__ensureSessionsPromise = this.__ensureAllAgentSessions();
599
+ this.__pushInstanceName();
583
600
  }
584
601
  else {
585
602
  this.gatewayReady = false;
@@ -912,13 +929,18 @@ export class RealtimeBridge {
912
929
  this.started = true;
913
930
  // 先完成 WebRTC 实现加载,再建立连接,避免 UI 发来 offer 时 RTC 包未就绪
914
931
  const preloadFn = this.__preloadNdc
915
- ?? (await import('./ndc-preloader.js')).preloadNdc;
916
- const preloadResult = await preloadFn()
917
- .catch((err) => {
932
+ ?? (await import('./webrtc/ndc-preloader.js')).preloadNdc;
933
+ // 版本预热与 preload 并行,供 gateway connect 请求同步使用
934
+ const [preloadResult] = await Promise.all([
935
+ preloadFn().catch((err) => {
918
936
  // preloadNdc 设计上永不 throw,此 catch 为纯防御性兜底
919
937
  this.logger.warn?.(`[coclaw] ndc preload unexpected failure: ${err?.message}`);
920
938
  return { PeerConnection: null, cleanup: null, impl: 'none' };
921
- });
939
+ }),
940
+ getPluginVersion()
941
+ .then((v) => { this.__pluginVersion = v; })
942
+ .catch(() => { this.__pluginVersion = 'unknown'; }),
943
+ ]);
922
944
  // 竞态保护:若 preload 期间 stop() 已执行,不再赋值,立即释放 cleanup
923
945
  if (!this.started) {
924
946
  if (preloadResult.cleanup) {
@@ -1048,3 +1070,15 @@ export async function gatewayAgentRpc(method, params, options) {
1048
1070
  }
1049
1071
  return singleton.__gatewayAgentRpc(method, params, options);
1050
1072
  }
1073
+
1074
+ /**
1075
+ * 广播插件自发事件(推送到 server + 广播到所有 UI DC)
1076
+ * @param {string} event - 事件名(如 'coclaw.info.updated')
1077
+ * @param {object} [payload]
1078
+ */
1079
+ export function broadcastPluginEvent(event, payload) {
1080
+ if (!singleton) return;
1081
+ const frame = { type: 'event', event, payload };
1082
+ singleton.__forwardToServer(frame);
1083
+ singleton.webrtcPeer?.broadcast(frame);
1084
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import nodePath from 'node:path';
4
+
5
+ import { resolveStateDir, CHANNEL_ID } from './config.js';
6
+ import { atomicWriteJsonFile } from './utils/atomic-write.js';
7
+ import { createMutex } from './utils/mutex.js';
8
+
9
+ const SETTINGS_FILENAME = 'settings.json';
10
+ export const MAX_NAME_LENGTH = 63;
11
+
12
+ const settingsMutex = createMutex();
13
+
14
+ function getSettingsPath() {
15
+ return nodePath.join(resolveStateDir(), CHANNEL_ID, SETTINGS_FILENAME);
16
+ }
17
+
18
+ async function readJsonSafe(filePath) {
19
+ let raw;
20
+ try {
21
+ raw = await fs.readFile(filePath, 'utf8');
22
+ }
23
+ catch (err) {
24
+ if (err?.code === 'ENOENT') return {};
25
+ /* c8 ignore next 2 */
26
+ throw err;
27
+ }
28
+ if (!String(raw).trim()) return {};
29
+ try {
30
+ return JSON.parse(raw);
31
+ }
32
+ catch {
33
+ // 文件损坏,删除后当空对象处理
34
+ /* c8 ignore next 2 -- ?./?? fallback */
35
+ console.warn?.(`[coclaw] corrupt settings file deleted: ${filePath}`);
36
+ await fs.unlink(filePath).catch(() => {});
37
+ return {};
38
+ }
39
+ }
40
+
41
+ /**
42
+ * 读取插件设置
43
+ * @returns {Promise<{ name?: string }>}
44
+ */
45
+ export async function readSettings() {
46
+ const data = await readJsonSafe(getSettingsPath());
47
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
48
+ }
49
+
50
+ /**
51
+ * 写入 claw name
52
+ * @param {string|null} name - 名称;null/空字符串/纯空白 → 清除
53
+ */
54
+ export async function writeName(name) {
55
+ const trimmed = typeof name === 'string' ? name.trim() : '';
56
+ if (trimmed && trimmed.length > MAX_NAME_LENGTH) {
57
+ throw new Error(`Name exceeds maximum length of ${MAX_NAME_LENGTH} characters`);
58
+ }
59
+ return settingsMutex.withLock(async () => {
60
+ const settingsPath = getSettingsPath();
61
+ const data = await readJsonSafe(settingsPath);
62
+ const settings = data && typeof data === 'object' && !Array.isArray(data) ? data : {};
63
+ if (trimmed) {
64
+ settings.name = trimmed;
65
+ } else {
66
+ delete settings.name;
67
+ }
68
+ await atomicWriteJsonFile(settingsPath, settings);
69
+ });
70
+ }
71
+
72
+ /**
73
+ * 获取 OS 主机名(去 .local 后缀)
74
+ * @returns {string}
75
+ */
76
+ export function getHostName() {
77
+ const raw = os.hostname().trim();
78
+ return raw.replace(/\.local$/i, '') || 'openclaw';
79
+ }
@@ -34,8 +34,10 @@ async function atomicWriteFile(filePath, content, opts) {
34
34
  try {
35
35
  await fs.writeFile(tmp, content, { encoding, mode });
36
36
  // best-effort chmod(部分平台 writeFile 的 mode 可能不生效)
37
+ /* c8 ignore next -- chmod 在正常文件系统上不会失败 */
37
38
  try { await fs.chmod(tmp, mode); } catch { /* ignore */ }
38
39
  await fs.rename(tmp, filePath);
40
+ /* c8 ignore next -- chmod 在正常文件系统上不会失败 */
39
41
  try { await fs.chmod(filePath, mode); } catch { /* ignore */ }
40
42
  } finally {
41
43
  // 确保临时文件不残留
@@ -2,7 +2,7 @@ import { createRequire } from 'module';
2
2
  import nodePath from 'path';
3
3
  import fsSync from 'fs';
4
4
  import fsPromises from 'fs/promises';
5
- import { remoteLog as defaultRemoteLog } from './remote-log.js';
5
+ import { remoteLog as defaultRemoteLog } from '../remote-log.js';
6
6
 
7
7
  const SUPPORTED_PLATFORMS = new Set([
8
8
  'linux-x64',
@@ -1,5 +1,5 @@
1
- import { chunkAndSend, createReassembler } from './utils/dc-chunking.js';
2
- import { remoteLog } from './remote-log.js';
1
+ import { chunkAndSend, createReassembler } from './dc-chunking.js';
2
+ import { remoteLog } from '../remote-log.js';
3
3
 
4
4
  /**
5
5
  * 管理多个 WebRTC PeerConnection(以 connId 为粒度)。
@@ -124,7 +124,7 @@ export class WebRtcPeer {
124
124
  const { urls, username, credential } = msg.turnCreds;
125
125
  for (const url of urls) {
126
126
  const server = { urls: url };
127
- if (url.startsWith('turn:')) {
127
+ if (url.startsWith('turn:') || url.startsWith('turns:')) {
128
128
  server.username = username;
129
129
  server.credential = credential;
130
130
  }
package/src/cli.js DELETED
@@ -1,128 +0,0 @@
1
- #!/usr/bin/env node
2
- import nodePath from 'node:path';
3
- import process from 'node:process';
4
- import { pathToFileURL } from 'node:url';
5
-
6
- import { bindBot, unbindBot } from './common/bot-binding.js';
7
- import { resolveErrorMessage } from './common/errors.js';
8
- import { callGatewayMethod } from './common/gateway-notify.js';
9
- import {
10
- notBound, bindOk, unbindOk,
11
- gatewayNotified, gatewayNotifyFailed,
12
- } from './common/messages.js';
13
-
14
- function parseArgs(argv) {
15
- const [command, ...rest] = argv;
16
- const options = {};
17
- const positionals = [];
18
-
19
- for (let i = 0; i < rest.length; i += 1) {
20
- const token = rest[i];
21
- if (token === '--server' && i + 1 < rest.length) {
22
- options.server = rest[i + 1];
23
- i += 1;
24
- continue;
25
- }
26
- positionals.push(token);
27
- }
28
-
29
- return {
30
- command,
31
- positionals,
32
- options,
33
- };
34
- }
35
-
36
- function printHelp() {
37
- console.log('Usage: coclaw <bind|unbind> [args] [--server <url>]');
38
- console.log('');
39
- console.log('Commands:');
40
- console.log(' bind <binding-code>');
41
- console.log(' unbind');
42
- }
43
-
44
- async function notifyGateway(method, deps) {
45
- const action = method.endsWith('refreshBridge') ? 'refresh' : 'stop';
46
- try {
47
- const result = await callGatewayMethod(method, deps.spawn);
48
- if (result.ok) {
49
- console.log(gatewayNotified(action));
50
- } else {
51
- console.warn(gatewayNotifyFailed());
52
- }
53
- }
54
- /* c8 ignore next 3 -- callGatewayMethod 已内部兜底,此处纯防御 */
55
- catch {
56
- console.warn(gatewayNotifyFailed());
57
- }
58
- }
59
-
60
- export async function main(argv = process.argv.slice(2), deps = {}) {
61
- const { command, positionals, options } = parseArgs(argv);
62
-
63
- if (!command || command === '--help' || command === '-h') {
64
- printHelp();
65
- return 0;
66
- }
67
-
68
- if (command === 'bind') {
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;
79
- }
80
-
81
- if (command === 'unbind') {
82
- try {
83
- const result = await unbindBot({
84
- serverUrl: options.server,
85
- });
86
- /* c8 ignore next */
87
- console.log(unbindOk(result));
88
- await notifyGateway('coclaw.stopBridge', deps);
89
- return 0;
90
- } catch (err) {
91
- if (err.code === 'NOT_BOUND') {
92
- console.error(notBound());
93
- return 1;
94
- }
95
- /* c8 ignore start -- 防御性兜底,unbindBot 主要抛 NOT_BOUND,也可能抛网络/HTTP 错误 */
96
- throw err;
97
- }
98
- /* c8 ignore stop */
99
- }
100
-
101
- throw new Error(`unknown command: ${command}`);
102
- }
103
-
104
- /* c8 ignore start */
105
- function isCliEntrypoint() {
106
- const argvPath = process.argv[1];
107
- if (!argvPath) {
108
- return false;
109
- }
110
- return import.meta.url === pathToFileURL(nodePath.resolve(argvPath)).href;
111
- }
112
- /* c8 ignore stop */
113
-
114
- /* c8 ignore start */
115
- if (isCliEntrypoint()) {
116
- process.on('uncaughtException', (err) => {
117
- console.error('[coclaw] uncaughtException:', err?.stack ?? err);
118
- });
119
- process.on('unhandledRejection', (err) => {
120
- console.error('[coclaw] unhandledRejection:', err?.stack ?? err);
121
- });
122
-
123
- main().catch((err) => {
124
- console.error(`[coclaw] ${resolveErrorMessage(err)}`);
125
- process.exitCode = 1;
126
- });
127
- }
128
- /* c8 ignore stop */
File without changes