@coclaw/openclaw-coclaw 0.14.0 → 0.14.1
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/index.js +81 -3
- package/package.json +1 -1
- package/src/agent-abort.js +2 -73
- package/src/platform-info.js +60 -0
- package/src/realtime-bridge.js +24 -2
package/index.js
CHANGED
|
@@ -14,10 +14,77 @@ import { AutoUpgradeScheduler } from './src/auto-upgrade/updater.js';
|
|
|
14
14
|
import { getPackageInfo } from './src/auto-upgrade/updater-check.js';
|
|
15
15
|
import { createFileHandler } from './src/file-manager/handler.js';
|
|
16
16
|
import { abortAgentRun } from './src/agent-abort.js';
|
|
17
|
+
import { remoteLog } from './src/remote-log.js';
|
|
17
18
|
|
|
18
19
|
import { getPluginVersion, __resetPluginVersion } from './src/plugin-version.js';
|
|
19
20
|
export { getPluginVersion, __resetPluginVersion };
|
|
20
21
|
|
|
22
|
+
// 侧门注册表观测:patch OpenClaw embeddedRunState.activeRuns 的 set/delete,
|
|
23
|
+
// 用于跟踪 sessionId 何时注册/注销(agent 取消流程实际读取的就是这张表)。
|
|
24
|
+
// OpenClaw 侧门形状变化时(缺失 / 抛异常),通过 remoteLog 上报为升级契约变更的早期信号。
|
|
25
|
+
const PATCH_LABELS = [
|
|
26
|
+
['embedded.activeRuns', () => globalThis[Symbol.for('openclaw.embeddedRunState')]?.activeRuns],
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function installAbortRegistryDiag(logger) {
|
|
30
|
+
const installed = [];
|
|
31
|
+
const missing = [];
|
|
32
|
+
try {
|
|
33
|
+
for (const [label, resolve] of PATCH_LABELS) {
|
|
34
|
+
if (patchMapLogging(resolve(), label, logger)) installed.push(label);
|
|
35
|
+
else missing.push(label);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
logger?.warn?.(`[coclaw.diag] installAbortRegistryDiag failed: ${String(err?.message ?? err)}`);
|
|
40
|
+
remoteLog(`abort.patch-failed reason=${String(err?.message ?? err)}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
remoteLog(`abort.patch installed=${installed.join(',') || 'none'} missing=${missing.join(',') || 'none'}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function patchMapLogging(map, label, logger) {
|
|
47
|
+
if (!map || typeof map.set !== 'function' || typeof map.delete !== 'function') return false;
|
|
48
|
+
if (map.__coclawDiagPatched) return true;
|
|
49
|
+
// 先打 idempotent 标记:若 map 是 frozen/sealed/Proxy 致 defineProperty 抛,
|
|
50
|
+
// 立即返回 false 让上层归入 missing;不留下半装好的 wrapper 状态
|
|
51
|
+
try {
|
|
52
|
+
Object.defineProperty(map, '__coclawDiagPatched', { value: true, enumerable: false });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger?.warn?.(`[coclaw.diag] cannot mark ${label} patched: ${String(err?.message ?? err)}`);
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const origSet = map.set.bind(map);
|
|
59
|
+
const origDel = map.delete.bind(map);
|
|
60
|
+
// log 行包 try/catch 兜底:上游若把 Map 换成有 throwing getter(如 Proxy)的对象,
|
|
61
|
+
// 不能让本插件的诊断 log 把 OpenClaw 内部 set/delete 流程带崩
|
|
62
|
+
const safeLog = (msg) => {
|
|
63
|
+
try { logger?.info?.(msg); } catch { /* swallow — diag log 不得影响主流程 */ }
|
|
64
|
+
};
|
|
65
|
+
const safeSize = () => {
|
|
66
|
+
try { return map.size; } catch { return '?'; }
|
|
67
|
+
};
|
|
68
|
+
map.set = (key, value) => {
|
|
69
|
+
const res = origSet(key, value);
|
|
70
|
+
safeLog(`[coclaw.diag] ${label}.set key=${stringifyKey(key)} size=${safeSize()}`);
|
|
71
|
+
return res;
|
|
72
|
+
};
|
|
73
|
+
map.delete = (key) => {
|
|
74
|
+
let had;
|
|
75
|
+
try { had = map.has(key); } catch { had = '?'; }
|
|
76
|
+
const res = origDel(key);
|
|
77
|
+
safeLog(`[coclaw.diag] ${label}.delete key=${stringifyKey(key)} had=${had} size=${safeSize()}`);
|
|
78
|
+
return res;
|
|
79
|
+
};
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stringifyKey(k) {
|
|
84
|
+
if (typeof k === 'string') return k;
|
|
85
|
+
try { return JSON.stringify(k); } catch { return String(k); }
|
|
86
|
+
}
|
|
87
|
+
|
|
21
88
|
/* c8 ignore start */
|
|
22
89
|
function parseCommandArgs(args) {
|
|
23
90
|
const tokens = (args ?? '').split(/\s+/).filter(Boolean);
|
|
@@ -65,6 +132,7 @@ const plugin = {
|
|
|
65
132
|
register(api) {
|
|
66
133
|
setRuntime(api.runtime);
|
|
67
134
|
const logger = api?.logger ?? console;
|
|
135
|
+
installAbortRegistryDiag(logger);
|
|
68
136
|
const manager = createSessionManager({ logger });
|
|
69
137
|
const topicManager = new TopicManager({ logger });
|
|
70
138
|
const chatHistoryManager = new ChatHistoryManager({ logger });
|
|
@@ -460,6 +528,7 @@ const plugin = {
|
|
|
460
528
|
|
|
461
529
|
// 取消正在执行的 embedded agent run(通过 OpenClaw 全局 symbol 侧门)
|
|
462
530
|
// 侧门不存在 / sessionId 未注册 / handle.abort 抛异常时返回 { ok:false, reason } —— UI 静默降级
|
|
531
|
+
// UI 可能在 OpenClaw 注册 sessionId 前点 STOP(注册空窗期),此时返回 not-found;UI 会按 500ms 间隔重试。
|
|
463
532
|
api.registerGatewayMethod('coclaw.agent.abort', ({ params, respond }) => {
|
|
464
533
|
try {
|
|
465
534
|
const sessionId = params?.sessionId;
|
|
@@ -468,9 +537,18 @@ const plugin = {
|
|
|
468
537
|
respondInvalid(respond, 'sessionId is required');
|
|
469
538
|
return;
|
|
470
539
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
540
|
+
const result = abortAgentRun(sessionId);
|
|
541
|
+
// not-found 是 UI 重试期常态(注册空窗),不打日志避免噪音;其余分支保留 info
|
|
542
|
+
if (result.reason !== 'not-found') {
|
|
543
|
+
logger.info?.(`[coclaw.agent.abort] result sessionId=${sessionId} ok=${result.ok}${result.reason ? ` reason=${result.reason}` : ''}${result.error ? ` error=${result.error}` : ''}`);
|
|
544
|
+
}
|
|
545
|
+
if (result.ok) {
|
|
546
|
+
remoteLog(`abort.success sid=${sessionId}`);
|
|
547
|
+
}
|
|
548
|
+
else if (result.reason === 'not-supported') {
|
|
549
|
+
// 侧门缺失或 handle shape 变化:OpenClaw 升级契约变更的早期信号
|
|
550
|
+
remoteLog(`abort.not-supported sid=${sessionId}`);
|
|
551
|
+
}
|
|
474
552
|
respond(true, result);
|
|
475
553
|
}
|
|
476
554
|
catch (err) {
|
package/package.json
CHANGED
package/src/agent-abort.js
CHANGED
|
@@ -9,91 +9,20 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
const EMBEDDED_RUN_STATE_KEY = Symbol.for('openclaw.embeddedRunState');
|
|
12
|
-
const REPLY_RUN_STATE_KEY = Symbol.for('openclaw.replyRunRegistry');
|
|
13
|
-
|
|
14
|
-
/* c8 ignore start */ // 临时诊断代码,定位完问题会删除
|
|
15
|
-
/**
|
|
16
|
-
* 诊断用:从 reply-run-registry 全局单例中解析 sessionId → sessionKey 映射及概览
|
|
17
|
-
* @param {string} sessionId
|
|
18
|
-
* @returns {string}
|
|
19
|
-
*/
|
|
20
|
-
function describeReplyRunRegistry(sessionId) {
|
|
21
|
-
const state = globalThis[REPLY_RUN_STATE_KEY];
|
|
22
|
-
if (!state) return 'reply.state=absent';
|
|
23
|
-
const parts = [];
|
|
24
|
-
const runs = state.activeRunsByKey;
|
|
25
|
-
if (runs && typeof runs.size === 'number') {
|
|
26
|
-
parts.push(`reply.activeRunsByKey.size=${runs.size}`);
|
|
27
|
-
try {
|
|
28
|
-
const ks = [];
|
|
29
|
-
if (typeof runs.keys === 'function') {
|
|
30
|
-
for (const k of runs.keys()) {
|
|
31
|
-
ks.push(k);
|
|
32
|
-
if (ks.length >= 10) break;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
parts.push(`reply.keys=${JSON.stringify(ks)}`);
|
|
36
|
-
}
|
|
37
|
-
catch (e) {
|
|
38
|
-
parts.push(`reply.keysErr=${String(e?.message ?? e)}`);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
42
|
-
parts.push('reply.activeRunsByKey=absent');
|
|
43
|
-
}
|
|
44
|
-
const byId = state.activeKeysBySessionId;
|
|
45
|
-
if (byId && typeof byId.get === 'function') {
|
|
46
|
-
try {
|
|
47
|
-
const mapped = byId.get(sessionId);
|
|
48
|
-
parts.push(`reply.keyForSid=${mapped === undefined ? 'null' : JSON.stringify(mapped)}`);
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
parts.push(`reply.keyForSidErr=${String(e?.message ?? e)}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
else {
|
|
55
|
-
parts.push('reply.activeKeysBySessionId=absent');
|
|
56
|
-
}
|
|
57
|
-
return parts.join(' ');
|
|
58
|
-
}
|
|
59
|
-
/* c8 ignore stop */
|
|
60
12
|
|
|
61
13
|
/**
|
|
62
14
|
* 尝试取消 sessionId 对应的 embedded agent run
|
|
63
15
|
* @param {string} sessionId
|
|
64
|
-
* @param {{ info?: Function }} [logger] - 可选 logger;传入时在 not-found 分支 dump activeRuns 诊断信息
|
|
65
16
|
* @returns {{ ok: true } | { ok: false, reason: 'not-supported' | 'not-found' | 'abort-threw', error?: string }}
|
|
66
17
|
*/
|
|
67
|
-
export function abortAgentRun(sessionId
|
|
18
|
+
export function abortAgentRun(sessionId) {
|
|
68
19
|
const state = globalThis[EMBEDDED_RUN_STATE_KEY];
|
|
69
20
|
if (!state || !state.activeRuns || typeof state.activeRuns.get !== 'function') {
|
|
70
21
|
return { ok: false, reason: 'not-supported' };
|
|
71
22
|
}
|
|
72
23
|
try {
|
|
73
24
|
const handle = state.activeRuns.get(sessionId);
|
|
74
|
-
if (!handle) {
|
|
75
|
-
/* c8 ignore start */ // 临时诊断代码,定位完问题会删除
|
|
76
|
-
if (logger?.info) {
|
|
77
|
-
let diag = `sessionId=${sessionId} embedded.size=${state.activeRuns.size ?? '?'}`;
|
|
78
|
-
try {
|
|
79
|
-
const ks = [];
|
|
80
|
-
if (typeof state.activeRuns.keys === 'function') {
|
|
81
|
-
for (const k of state.activeRuns.keys()) {
|
|
82
|
-
ks.push(k);
|
|
83
|
-
if (ks.length >= 10) break;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
diag += ` embedded.keys=${JSON.stringify(ks)}`;
|
|
87
|
-
}
|
|
88
|
-
catch (e) {
|
|
89
|
-
diag += ` embedded.keysErr=${String(e?.message ?? e)}`;
|
|
90
|
-
}
|
|
91
|
-
diag += ` ${describeReplyRunRegistry(sessionId)}`;
|
|
92
|
-
logger.info(`[coclaw.agent.abort] not-found diag ${diag}`);
|
|
93
|
-
}
|
|
94
|
-
/* c8 ignore stop */
|
|
95
|
-
return { ok: false, reason: 'not-found' };
|
|
96
|
-
}
|
|
25
|
+
if (!handle) return { ok: false, reason: 'not-found' };
|
|
97
26
|
// shape 守卫:abort 字段应为函数;若不是说明 OpenClaw handle 契约变化(归入 not-supported 让 UI 提示升级)
|
|
98
27
|
if (typeof handle.abort !== 'function') return { ok: false, reason: 'not-supported' };
|
|
99
28
|
handle.abort();
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
|
|
4
|
+
// 模块级缓存:所有字段在进程生命周期内不变,缓存后 ws 重连补发零开销。
|
|
5
|
+
let __cachedLine = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 以 "key=value" 形式收集运行环境信息,用于诊断平台相关二进制依赖(如 pion-ipc)问题。
|
|
9
|
+
*
|
|
10
|
+
* 尽力而为:每项独立 try/catch,单项失败不影响其它项;无法获取时该字段省略。
|
|
11
|
+
* 结果在进程生命周期内缓存,重复调用零额外开销。
|
|
12
|
+
*
|
|
13
|
+
* 字段:platform / arch / node / osrel / cpu / cores / mem
|
|
14
|
+
*
|
|
15
|
+
* @returns {string} - 形如 `platform=linux arch=x64 node=v20.11.0 osrel=6.6.87 cpu="Intel Xeon" cores=8 mem=16.0GB`
|
|
16
|
+
*/
|
|
17
|
+
export function getPlatformInfoLine() {
|
|
18
|
+
if (__cachedLine !== null) return __cachedLine;
|
|
19
|
+
const parts = [];
|
|
20
|
+
|
|
21
|
+
tryPush(parts, 'platform', () => process.platform);
|
|
22
|
+
tryPush(parts, 'arch', () => process.arch);
|
|
23
|
+
tryPush(parts, 'node', () => process.version);
|
|
24
|
+
tryPush(parts, 'osrel', () => os.release());
|
|
25
|
+
tryPush(parts, 'cpu', () => {
|
|
26
|
+
const model = os.cpus()?.[0]?.model;
|
|
27
|
+
if (!model) return undefined;
|
|
28
|
+
// 外层包双引号以保留含空格的 model;内部双引号 / C0 控制字符 / DEL 替换为空格后折叠空白
|
|
29
|
+
const cleaned = String(model).replace(/["\x00-\x1F\x7F]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
30
|
+
if (!cleaned) return undefined;
|
|
31
|
+
return `"${cleaned}"`;
|
|
32
|
+
});
|
|
33
|
+
tryPush(parts, 'cores', () => {
|
|
34
|
+
const n = os.cpus()?.length;
|
|
35
|
+
return n > 0 ? n : undefined;
|
|
36
|
+
});
|
|
37
|
+
tryPush(parts, 'mem', () => {
|
|
38
|
+
const bytes = os.totalmem();
|
|
39
|
+
if (!bytes || !Number.isFinite(bytes)) return undefined;
|
|
40
|
+
return `${(bytes / 1024 ** 3).toFixed(1)}GB`;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
__cachedLine = parts.join(' ');
|
|
44
|
+
return __cachedLine;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 测试用:清缓存以便覆盖不同 monkey-patch 场景 */
|
|
48
|
+
export function __resetPlatformInfoCache() {
|
|
49
|
+
__cachedLine = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function tryPush(parts, key, resolver) {
|
|
53
|
+
try {
|
|
54
|
+
const value = resolver();
|
|
55
|
+
if (value === undefined || value === null || value === '') return;
|
|
56
|
+
parts.push(`${key}=${value}`);
|
|
57
|
+
} catch {
|
|
58
|
+
// 单项失败静默跳过,不影响其它字段
|
|
59
|
+
}
|
|
60
|
+
}
|
package/src/realtime-bridge.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { getRuntime } from './runtime.js';
|
|
15
15
|
import { setSender as setRemoteLogSender, remoteLog } from './remote-log.js';
|
|
16
16
|
import { getPluginVersion } from './plugin-version.js';
|
|
17
|
+
import { getPlatformInfoLine } from './platform-info.js';
|
|
17
18
|
|
|
18
19
|
const DEFAULT_GATEWAY_WS_URL = `ws://127.0.0.1:${process.env.OPENCLAW_GATEWAY_PORT || '18789'}`;
|
|
19
20
|
const RECONNECT_MS = 10_000;
|
|
@@ -811,9 +812,14 @@ export class RealtimeBridge {
|
|
|
811
812
|
this.__clearConnectTimer();
|
|
812
813
|
this.logger.info?.(`[coclaw] realtime bridge connected: ${maskedTarget}`);
|
|
813
814
|
remoteLog('ws.connected peer=server');
|
|
815
|
+
// 顺序很重要:先注入 sender 再 remoteLog 环境信息——这样环境信息能随当前 sock
|
|
816
|
+
// 立即 flush;同时 sender 内部仅做 sock.send(不回调 remoteLog),无循环依赖。
|
|
814
817
|
setRemoteLogSender((msg) => {
|
|
815
818
|
if (sock.readyState === 1) sock.send(JSON.stringify(msg));
|
|
816
819
|
});
|
|
820
|
+
// ws 重连后补发环境信息:server 重启重连后能立即看到当前 claw 的运行环境与 webrtc 选型。
|
|
821
|
+
// __buildEnvLine 内部所有读取均为缓存值,无 native syscall。
|
|
822
|
+
remoteLog(this.__buildEnvLine());
|
|
817
823
|
this.__startServerHeartbeat(sock);
|
|
818
824
|
this.__ensureGatewayConnection();
|
|
819
825
|
});
|
|
@@ -967,12 +973,28 @@ export class RealtimeBridge {
|
|
|
967
973
|
this.__ndcPreloadResult = preloadResult;
|
|
968
974
|
this.__ndcCleanup = preloadResult.cleanup;
|
|
969
975
|
const implLabel = preloadResult.impl === 'ndc' ? 'node-datachannel(ndc)' : preloadResult.impl;
|
|
976
|
+
this.__implLabel = implLabel; // 缓存供 ws.open 时发送
|
|
977
|
+
// 启动信息只本地 log;远程发送统一由 ws.open 触发,避免重复
|
|
970
978
|
this.logger.info?.(`[coclaw] WebRTC impl: ${implLabel}`);
|
|
971
|
-
|
|
972
|
-
remoteLog(
|
|
979
|
+
this.logger.info?.(`[coclaw] ${this.__buildEnvLine()}`);
|
|
980
|
+
remoteLog('bridge.started');
|
|
973
981
|
await this.__connectIfNeeded();
|
|
974
982
|
}
|
|
975
983
|
|
|
984
|
+
/**
|
|
985
|
+
* 组装一条覆盖最基础环境信息的 log 行:
|
|
986
|
+
* coclaw.env impl=<...> plugin=<ver> openclaw=<ver> platform=<...> ... mem=<...>
|
|
987
|
+
*
|
|
988
|
+
* 字段值均为已缓存的轻量同步读取,无 native syscall;不调用 remoteLog,无循环依赖。
|
|
989
|
+
*/
|
|
990
|
+
__buildEnvLine() {
|
|
991
|
+
const rt = getRuntime();
|
|
992
|
+
const openclawVer = (rt?.version && rt.version !== 'unknown') ? rt.version : 'unknown';
|
|
993
|
+
const impl = this.__implLabel ?? 'pending';
|
|
994
|
+
const plugin = this.__pluginVersion ?? 'unknown';
|
|
995
|
+
return `coclaw.env impl=${impl} plugin=${plugin} openclaw=${openclawVer} ${getPlatformInfoLine()}`;
|
|
996
|
+
}
|
|
997
|
+
|
|
976
998
|
async refresh() {
|
|
977
999
|
await this.stop();
|
|
978
1000
|
await this.start({
|