@winmatrix/agent-sdk 0.1.6 → 1.0.4

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.
Files changed (84) hide show
  1. package/dist/adapters/claude/ClaudeAdapter.d.ts +7 -0
  2. package/dist/adapters/claude/ClaudeAdapter.d.ts.map +1 -1
  3. package/dist/adapters/claude/ClaudeAdapter.js +69 -9
  4. package/dist/adapters/claude/ClaudeAdapter.js.map +1 -1
  5. package/dist/adapters/claude/ClaudeStreamParser.d.ts +7 -2
  6. package/dist/adapters/claude/ClaudeStreamParser.d.ts.map +1 -1
  7. package/dist/adapters/claude/ClaudeStreamParser.js +90 -3
  8. package/dist/adapters/claude/ClaudeStreamParser.js.map +1 -1
  9. package/dist/adapters/claude/claudeCommand.d.ts +2 -1
  10. package/dist/adapters/claude/claudeCommand.d.ts.map +1 -1
  11. package/dist/adapters/claude/claudeCommand.js +88 -7
  12. package/dist/adapters/claude/claudeCommand.js.map +1 -1
  13. package/dist/adapters/codex/CodexAdapter.d.ts +20 -0
  14. package/dist/adapters/codex/CodexAdapter.d.ts.map +1 -0
  15. package/dist/adapters/codex/CodexAdapter.js +286 -0
  16. package/dist/adapters/codex/CodexAdapter.js.map +1 -0
  17. package/dist/adapters/hermes/HermesAdapter.d.ts +1 -0
  18. package/dist/adapters/hermes/HermesAdapter.d.ts.map +1 -1
  19. package/dist/adapters/hermes/HermesAdapter.js +79 -14
  20. package/dist/adapters/hermes/HermesAdapter.js.map +1 -1
  21. package/dist/adapters/openclaw/OpenClawAdapter.d.ts +12 -37
  22. package/dist/adapters/openclaw/OpenClawAdapter.d.ts.map +1 -1
  23. package/dist/adapters/openclaw/OpenClawAdapter.js +168 -634
  24. package/dist/adapters/openclaw/OpenClawAdapter.js.map +1 -1
  25. package/dist/core/AgentAdapter.d.ts +31 -0
  26. package/dist/core/AgentAdapter.d.ts.map +1 -1
  27. package/dist/core/AgentAdapter.js +25 -0
  28. package/dist/core/AgentAdapter.js.map +1 -1
  29. package/dist/core/ClaudeRuntimeDoctor.d.ts +20 -0
  30. package/dist/core/ClaudeRuntimeDoctor.d.ts.map +1 -0
  31. package/dist/core/ClaudeRuntimeDoctor.js +50 -0
  32. package/dist/core/ClaudeRuntimeDoctor.js.map +1 -0
  33. package/dist/core/RuntimeDetector.d.ts.map +1 -1
  34. package/dist/core/RuntimeDetector.js +53 -33
  35. package/dist/core/RuntimeDetector.js.map +1 -1
  36. package/dist/core/WinMatrixAgentClient.d.ts +8 -4
  37. package/dist/core/WinMatrixAgentClient.d.ts.map +1 -1
  38. package/dist/core/WinMatrixAgentClient.js +9 -2
  39. package/dist/core/WinMatrixAgentClient.js.map +1 -1
  40. package/dist/index.d.ts +6 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -7
  43. package/dist/index.js.map +1 -1
  44. package/dist/utils/resumeFallback.d.ts +4 -0
  45. package/dist/utils/resumeFallback.d.ts.map +1 -0
  46. package/dist/utils/resumeFallback.js +55 -0
  47. package/dist/utils/resumeFallback.js.map +1 -0
  48. package/package.json +7 -2
  49. package/dist/adapters/openclaw/unwrapOpenClawPayloads.d.ts +0 -6
  50. package/dist/adapters/openclaw/unwrapOpenClawPayloads.d.ts.map +0 -1
  51. package/dist/adapters/openclaw/unwrapOpenClawPayloads.js +0 -24
  52. package/dist/adapters/openclaw/unwrapOpenClawPayloads.js.map +0 -1
  53. package/dist/runtime/DualRuntimeBridge.d.ts +0 -38
  54. package/dist/runtime/DualRuntimeBridge.d.ts.map +0 -1
  55. package/dist/runtime/DualRuntimeBridge.js +0 -321
  56. package/dist/runtime/DualRuntimeBridge.js.map +0 -1
  57. package/dist/runtime/bridgeConfigLoader.d.ts +0 -14
  58. package/dist/runtime/bridgeConfigLoader.d.ts.map +0 -1
  59. package/dist/runtime/bridgeConfigLoader.js +0 -133
  60. package/dist/runtime/bridgeConfigLoader.js.map +0 -1
  61. package/dist/runtime/bridgeConfigTypes.d.ts +0 -26
  62. package/dist/runtime/bridgeConfigTypes.d.ts.map +0 -1
  63. package/dist/runtime/bridgeConfigTypes.js +0 -6
  64. package/dist/runtime/bridgeConfigTypes.js.map +0 -1
  65. package/dist/runtime/bridgeHome.d.ts +0 -19
  66. package/dist/runtime/bridgeHome.d.ts.map +0 -1
  67. package/dist/runtime/bridgeHome.js +0 -61
  68. package/dist/runtime/bridgeHome.js.map +0 -1
  69. package/dist/runtime/bridgeReconnectPolicy.d.ts +0 -8
  70. package/dist/runtime/bridgeReconnectPolicy.d.ts.map +0 -1
  71. package/dist/runtime/bridgeReconnectPolicy.js +0 -16
  72. package/dist/runtime/bridgeReconnectPolicy.js.map +0 -1
  73. package/dist/runtime/bridgeTaskJournal.d.ts +0 -7
  74. package/dist/runtime/bridgeTaskJournal.d.ts.map +0 -1
  75. package/dist/runtime/bridgeTaskJournal.js +0 -66
  76. package/dist/runtime/bridgeTaskJournal.js.map +0 -1
  77. package/dist/runtime/localRuntimeAvailabilityMonitor.d.ts +0 -22
  78. package/dist/runtime/localRuntimeAvailabilityMonitor.d.ts.map +0 -1
  79. package/dist/runtime/localRuntimeAvailabilityMonitor.js +0 -81
  80. package/dist/runtime/localRuntimeAvailabilityMonitor.js.map +0 -1
  81. package/dist/runtime/runDualRuntimeBridge.d.ts +0 -2
  82. package/dist/runtime/runDualRuntimeBridge.d.ts.map +0 -1
  83. package/dist/runtime/runDualRuntimeBridge.js +0 -32
  84. package/dist/runtime/runDualRuntimeBridge.js.map +0 -1
@@ -1,246 +1,83 @@
1
1
  /**
2
- * OpenClawAdapter — 通过 WebSocket-RPC 连接本地 OpenClaw Gateway (port 18789)
2
+ * OpenClawAdapter — spawn `openclaw agent --json` CLI,通过 Gateway 执行 agent turn。
3
3
  *
4
- * OpenClaw 是 Gateway 架构的智能体运行时,通过 WS-RPC 协议通信。
5
- * 协议:connect.challenge connect 握手 chat.send 发消息 chat/agent 事件流
4
+ * WS-RPC gateway 模式改为 CLI spawn 模式(对齐 CodexAdapter/HermesAdapter):
5
+ * - 绕过 gateway protocol mismatch + operator scope 问题(CLI 内部处理 auth)
6
+ * - `--json` 结构化输出(精确解析,不猜正则)
7
+ * - 非流式(一次性 JSON,等进程退出后 yield result)
6
8
  *
7
- * 使用 client.id: 'gateway-client' + client.mode: 'backend' 作为 loopback backend 客户端,
8
- * 无需 device identity 签名。
9
+ * CLI 命令:`openclaw agent --json --agent <id> [--session-id <sid>] -m <prompt>`
9
10
  */
10
- import { randomUUID } from 'node:crypto';
11
+ import { spawn } from 'node:child_process';
11
12
  import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
12
13
  import { homedir } from 'node:os';
13
14
  import { join, dirname } from 'node:path';
14
- import WebSocket from 'ws';
15
15
  import { parseSkillMd } from '../../utils/parseSkillMd.js';
16
+ import { waitForExit, killProcess } from '../../utils/processUtils.js';
17
+ import { executeWithResumeFallback } from '../../utils/resumeFallback.js';
16
18
  // ── constants ──
17
- const PROTOCOL_VERSION = 3;
18
19
  const OPENCLAW_DIR = join(homedir(), '.openclaw');
19
20
  const OPENCLAW_CONFIG = join(OPENCLAW_DIR, 'openclaw.json');
20
- /** eventQueue 上限,防止 wakeUp 丢失导致的内存泄漏 */
21
- const MAX_EVENT_QUEUE = 10_000;
22
- /** 判断事件帧是否为流终止事件(final/error),终止事件不应被丢弃 */
23
- function isStreamEndEvent(frame) {
24
- if (frame.event === 'chat') {
25
- const state = frame.payload?.state;
26
- return state === 'final' || state === 'error';
27
- }
28
- if (frame.event === 'agent') {
29
- const stream = typeof frame.payload?.stream === 'string'
30
- ? frame.payload.stream
31
- : '';
32
- const data = frame.payload?.data;
33
- if (stream === 'lifecycle') {
34
- const phase = data?.phase;
35
- return phase === 'end' || phase === 'error';
36
- }
37
- if (stream === 'error')
38
- return true;
39
- }
40
- return false;
41
- }
42
- /** 递归提取嵌套对象或数组中的 text 字符串 */
43
- function extractText(obj) {
44
- if (typeof obj === 'string')
45
- return obj;
46
- if (Array.isArray(obj)) {
47
- const parts = [];
48
- for (const item of obj) {
49
- if (typeof item === 'string') {
50
- parts.push(item);
51
- }
52
- else if (item && typeof item === 'object' && typeof item.text === 'string') {
53
- parts.push(item.text);
54
- }
55
- }
56
- return parts.join('');
57
- }
58
- if (!obj || typeof obj !== 'object')
59
- return '';
60
- const o = obj;
61
- if (typeof o.text === 'string')
62
- return o.text;
63
- if (typeof o.content === 'string')
64
- return o.content;
65
- if (typeof o.delta === 'string')
66
- return o.delta;
67
- if (Array.isArray(o.content)) {
68
- const parts = [];
69
- for (const item of o.content) {
70
- if (typeof item === 'string') {
71
- parts.push(item);
72
- }
73
- else if (item && typeof item === 'object' && typeof item.text === 'string') {
74
- parts.push(item.text);
75
- }
76
- }
77
- return parts.join('');
78
- }
79
- return '';
80
- }
81
- /** 从 chat 事件 payload 提取文本 */
82
- function extractTextFromPayload(payload) {
83
- const fromMessage = extractText(payload.message);
84
- if (fromMessage)
85
- return fromMessage;
86
- if (Array.isArray(payload.messages)) {
87
- const parts = [];
88
- for (const m of payload.messages) {
89
- const t = extractText(m);
90
- if (t)
91
- parts.push(t);
92
- }
93
- if (parts.length > 0)
94
- return parts.join('');
95
- }
96
- if (typeof payload.content === 'string')
97
- return payload.content;
98
- if (typeof payload.text === 'string')
99
- return payload.text;
100
- if (typeof payload.delta === 'string')
101
- return payload.delta;
102
- const fromContent = extractText(payload.content);
103
- if (fromContent)
104
- return fromContent;
105
- if (typeof payload.result === 'string')
106
- return payload.result;
107
- if (typeof payload.output === 'string')
108
- return payload.output;
109
- return '';
110
- }
111
- /**
112
- * 从 agent assistant 事件 data 提取文本。
113
- * 参照 server 端 extractAgentAssistantText — 覆盖 text/delta/content/chunk/messages/message/payload/result/response/output。
114
- */
115
- export function extractAgentAssistantText(data) {
116
- if (typeof data.text === 'string')
117
- return data.text;
118
- if (typeof data.delta === 'string')
119
- return data.delta;
120
- if (typeof data.content === 'string')
121
- return data.content;
122
- if (typeof data.chunk === 'string')
123
- return data.chunk;
124
- const fromMessages = extractTextFromMessages(data.messages);
125
- if (fromMessages)
126
- return fromMessages;
127
- const fromMessage = extractText(data.message);
128
- if (fromMessage)
129
- return fromMessage;
130
- const fromContent = extractText(data.content);
131
- if (fromContent)
132
- return fromContent;
133
- const fromDelta = extractText(data.delta);
134
- if (fromDelta)
135
- return fromDelta;
136
- const fromChunk = extractText(data.chunk);
137
- if (fromChunk)
138
- return fromChunk;
139
- const fromPayload = extractText(data.payload);
140
- if (fromPayload)
141
- return fromPayload;
142
- const fromResult = extractText(data.result);
143
- if (fromResult)
144
- return fromResult;
145
- const fromResponse = extractText(data.response);
146
- if (fromResponse)
147
- return fromResponse;
148
- const fromOutput = extractText(data.output);
149
- if (fromOutput)
150
- return fromOutput;
151
- // deep extraction
152
- const fromDeep = extractTextDeep(data);
153
- if (fromDeep)
154
- return fromDeep;
155
- return '';
156
- }
157
- export function extractTextFromMessages(messages) {
158
- if (!Array.isArray(messages))
159
- return '';
160
- for (let i = messages.length - 1; i >= 0; i -= 1) {
161
- const item = messages[i];
162
- if (!item || typeof item !== 'object')
163
- continue;
164
- const msg = item;
165
- const role = typeof msg.role === 'string' ? msg.role.toLowerCase() : '';
166
- if (role !== 'assistant' && role !== 'model')
167
- continue;
168
- const text = extractText(msg.content) || extractTextDeep(msg);
169
- if (text)
170
- return text;
171
- }
172
- return '';
173
- }
174
- export function extractTextDeep(obj, depth = 0) {
175
- if (!obj || typeof obj !== 'object' || depth > 4)
176
- return '';
177
- const o = obj;
178
- for (const key of ['text', 'content', 'delta', 'chunk', 'result', 'output', 'response', 'message']) {
179
- const v = o[key];
180
- if (typeof v === 'string' && v.trim())
181
- return v;
182
- if (v && typeof v === 'object') {
183
- const inner = extractTextDeep(v, depth + 1);
184
- if (inner)
185
- return inner;
186
- }
187
- }
188
- return '';
189
- }
190
- /** 从 ~/.openclaw/openclaw.json 读取 gateway token */
191
- function readGatewayTokenFromConfig() {
21
+ // ── helpers ──
22
+ // ── helpers ──
23
+ /** 解析 openclaw agent --json 的完整输出,提取 answer/sessionId/usage */
24
+ function parseOpenClawCliResult(rawJson) {
192
25
  try {
193
- if (!existsSync(OPENCLAW_CONFIG))
194
- return undefined;
195
- const raw = readFileSync(OPENCLAW_CONFIG, 'utf-8');
196
- const cfg = JSON.parse(raw);
197
- return cfg.gateway?.auth?.token || undefined;
26
+ const parsed = JSON.parse(rawJson);
27
+ const result = parsed.result;
28
+ // answer text: result.payloads[*].text join
29
+ let text = '';
30
+ if (result?.payloads && Array.isArray(result.payloads)) {
31
+ const texts = result.payloads
32
+ .map((p) => p.text ?? '')
33
+ .filter(Boolean);
34
+ if (texts.length > 0)
35
+ text = texts.join('\n');
36
+ }
37
+ // sessionId: result.meta.agentMeta.sessionId
38
+ const meta = result?.meta;
39
+ const agentMeta = meta?.agentMeta;
40
+ const sessionId = typeof agentMeta?.sessionId === 'string' ? agentMeta.sessionId : undefined;
41
+ // usage: result.meta.agentMeta.usage
42
+ const rawUsage = agentMeta?.usage;
43
+ const usage = rawUsage && typeof rawUsage.input === 'number' && typeof rawUsage.output === 'number'
44
+ ? { input: rawUsage.input, output: rawUsage.output }
45
+ : undefined;
46
+ return { text, sessionId, usage };
198
47
  }
199
48
  catch {
200
- return undefined;
49
+ return { text: rawJson.trim() };
201
50
  }
202
51
  }
203
- /**
204
- * sessionKey 编码: agent:<agentId>:direct:<sessionKey>(参照 server toAgentSessionKey)。
205
- * 若 sessionKey 已是 agent: 格式则不重复编码,避免双重包装。
206
- */
207
- export function toAgentSessionKey(sessionKey, agentId) {
208
- if (sessionKey.toLowerCase().startsWith('agent:'))
209
- return sessionKey;
210
- return `agent:${agentId}:direct:${sessionKey}`;
211
- }
212
52
  export class OpenClawAdapter {
213
53
  meta = {
214
54
  agentType: 'openclaw',
215
55
  runtime: 'openclaw',
216
56
  capabilities: {
217
- streaming: true,
57
+ // 非流式(一次性 JSON);CLI 无 --stream-json
58
+ streaming: false,
218
59
  toolCall: true,
219
60
  session: true,
220
- cancellation: false,
61
+ // SIGTERM killProcess 可取消
62
+ cancellation: true,
63
+ supportsCancel: true,
64
+ supportsResume: true,
221
65
  },
222
66
  tools: [],
223
67
  skills: [],
224
68
  toolCategories: {},
225
69
  };
226
- gatewayHost;
227
- gatewayPort;
228
- token;
70
+ openclawPath;
229
71
  mcpBridgeUrl;
230
72
  mcpApiKey;
231
73
  agentId;
232
74
  constructor(config) {
233
75
  const opts = config;
234
- this.gatewayHost = opts?.gatewayHost ?? '127.0.0.1';
235
- this.gatewayPort = opts?.gatewayPort ?? 18789;
236
- this.token = opts?.token ?? readGatewayTokenFromConfig();
76
+ this.openclawPath = opts?.openclawPath ?? 'openclaw';
237
77
  this.mcpBridgeUrl = opts?.mcpBridgeUrl;
238
78
  this.mcpApiKey = opts?.mcpApiKey;
239
79
  this.agentId = opts?.agentId;
240
80
  }
241
- get gatewayUrl() {
242
- return `ws://${this.gatewayHost}:${this.gatewayPort}`;
243
- }
244
81
  // ── MCP config helpers ──
245
82
  /**
246
83
  * Write MCP server config to openclaw.json, merge with existing keys.
@@ -281,230 +118,101 @@ export class OpenClawAdapter {
281
118
  }
282
119
  // ── execute ──
283
120
  async *execute(task, signal) {
284
- const isOneshot = task.mode === 'oneshot';
285
- let sessionId = task.sessionId;
286
- // Write MCP config to openclaw.json before connect (per-task mcpToken takes priority).
287
- // MCP 配置通过文件注入而非 chat.send params,因为 Gateway 从 openclaw.json 中
288
- // mcp.servers.winmatrix 读取插件配置。前提:Gateway 支持热加载或 MCP 插件按需读取。
121
+ yield* executeWithResumeFallback(task, signal, (nextTask, nextSignal) => this.executeOnce(nextTask, nextSignal));
122
+ }
123
+ async *executeOnce(task, signal) {
124
+ const workDir = task.workDir;
125
+ if (workDir && !existsSync(workDir)) {
126
+ yield { type: 'error', message: `Work directory does not exist: ${workDir}` };
127
+ return;
128
+ }
129
+ // MCP 注入:Gateway 从 openclaw.json 读 mcp.servers.winmatrix
289
130
  this.writeMCPConfig(task.mcpToken);
290
- const gatewayHost = task.openclaw?.gatewayHost ?? this.gatewayHost;
291
- const gatewayPort = task.openclaw?.gatewayPort ?? this.gatewayPort;
292
- const gatewayUrl = `ws://${gatewayHost}:${gatewayPort}`;
293
- let ws;
131
+ // 构造 CLI args: openclaw agent --json --agent main [--session-id <sid>] -m <prompt>
132
+ // 注意:openclaw CLI 的 --agent 要的是 openclaw 内部 agent id(如 'main'),
133
+ // 不是 WinMatrix 的外联 Agent id(ea_xxx)。WinMatrix agentId 仅用于 sessionKey 编码/MCP,
134
+ // openclaw agent id 固定用 'main'(用户可在 ~/.openclaw/agents/ 配置多个,默认 main)。
135
+ const openclawAgentId = 'main';
136
+ const prompt = task.systemPrompt
137
+ ? `${task.systemPrompt}\n\n---\n\n${task.input}`
138
+ : task.input;
139
+ const args = ['agent', '--json', '--agent', openclawAgentId, '-m', prompt];
140
+ // resume: --session-id <sessionId>
141
+ if (task.resumeSession && task.sessionId) {
142
+ args.push('--session-id', task.sessionId);
143
+ }
144
+ // model override(临时复用 task.hermes?.model;待 OpenClawTaskOptions 加独立 model 字段后改读 task.openclaw?.model)
145
+ if (task.hermes?.model) {
146
+ args.push('--model', task.hermes.model);
147
+ }
148
+ // Spawn
149
+ let proc;
294
150
  try {
295
- ws = await this.authenticatedConnect(gatewayUrl, gatewayHost, signal);
151
+ proc = spawn(this.openclawPath, args, {
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ cwd: workDir ?? process.cwd(),
154
+ });
296
155
  }
297
156
  catch (err) {
298
- yield { type: 'error', message: `OpenClaw Gateway connection failed: ${err instanceof Error ? err.message : String(err)}` };
157
+ yield {
158
+ type: 'error',
159
+ message: `Failed to spawn openclaw: ${err instanceof Error ? err.message : String(err)}`,
160
+ };
299
161
  return;
300
162
  }
301
- // Build persistent handler infrastructure BEFORE chat.send
302
- const pendingRequests = new Map();
303
- const eventQueue = [];
304
- let wakeUp = null;
305
- let streamDone = false;
306
- let finalized = false;
307
- const persistHandler = (data) => {
308
- try {
309
- const frame = JSON.parse(data.toString());
310
- // Route res frames to pendingRequests
311
- if (frame.type === 'res' && frame.id) {
312
- const pending = pendingRequests.get(frame.id);
313
- if (pending) {
314
- clearTimeout(pending.timer);
315
- pendingRequests.delete(frame.id);
316
- pending.resolve({
317
- ok: frame.ok === true,
318
- payload: frame.payload,
319
- error: frame.error,
320
- });
321
- }
322
- return;
323
- }
324
- // Route event frames to event queue (capped to prevent leaks).
325
- // Never drop final/error events — they're required to terminate the stream loop.
326
- if (frame.type === 'event') {
327
- if (eventQueue.length >= MAX_EVENT_QUEUE) {
328
- let dropIdx = -1;
329
- for (let i = 0; i < eventQueue.length; i++) {
330
- if (!isStreamEndEvent(eventQueue[i])) {
331
- dropIdx = i;
332
- break;
333
- }
334
- }
335
- if (dropIdx >= 0) {
336
- eventQueue.splice(dropIdx, 1);
337
- }
338
- }
339
- eventQueue.push(frame);
340
- wakeUp?.();
341
- }
342
- }
343
- catch {
344
- // skip malformed
345
- }
346
- };
347
- ws.on('message', persistHandler);
348
- const onClose = () => {
349
- streamDone = true;
350
- // Reject all pending requests
351
- for (const [, p] of pendingRequests) {
352
- clearTimeout(p.timer);
353
- p.reject(new Error('WebSocket closed'));
354
- }
355
- pendingRequests.clear();
356
- wakeUp?.();
357
- };
358
- ws.on('close', onClose);
163
+ let completed = false;
359
164
  const onAbort = () => {
360
- // Best-effort abort via ws send
361
- try {
362
- ws.send(JSON.stringify({ type: 'req', id: `abort-${Date.now()}`, method: 'chat.abort', params: { sessionKey: sessionId } }));
363
- }
364
- catch { /* ignore */ }
365
- ws.close();
165
+ if (!completed)
166
+ killProcess(proc);
366
167
  };
367
168
  signal?.addEventListener('abort', onAbort, { once: true });
368
169
  try {
369
- const baseSessionKey = !isOneshot && sessionId ? sessionId : `wm-${randomUUID().slice(0, 8)}`;
370
- const sessionKey = this.agentId ? toAgentSessionKey(baseSessionKey, this.agentId) : baseSessionKey;
371
- const message = task.systemPrompt
372
- ? `${task.systemPrompt}\n\n---\n\n${task.input}`
373
- : task.input;
374
- // Send chat.send via pendingRequests map (not the generic sendRequest)
375
- const chatSendResult = await this.sendRequestViaMap(ws, pendingRequests, 'chat.send', {
376
- sessionKey,
377
- message,
378
- idempotencyKey: `wm-${randomUUID()}`,
170
+ // 累积 stdout(整块 JSON,非流式)
171
+ let stdoutBuffer = '';
172
+ let stderrBuffer = '';
173
+ proc.stderr?.on('data', (chunk) => {
174
+ stderrBuffer += chunk.toString('utf-8');
379
175
  });
380
- if (!chatSendResult.ok) {
381
- const errMsg = typeof chatSendResult.error === 'string'
382
- ? chatSendResult.error
383
- : JSON.stringify(chatSendResult.error);
384
- yield { type: 'error', message: `chat.send failed: ${errMsg}` };
385
- return;
386
- }
387
- const ack = chatSendResult.payload;
388
- const runId = ack?.runId;
389
- sessionId = sessionKey;
390
- // Stream events from the persistent handler's queue
391
- let resultText = '';
392
- let msgIdx = 0;
393
- while (!streamDone) {
176
+ for await (const chunk of proc.stdout) {
394
177
  if (signal?.aborted)
395
178
  break;
396
- while (msgIdx < eventQueue.length) {
397
- const frame = eventQueue[msgIdx++];
398
- if (frame.event === 'chat') {
399
- const payload = frame.payload;
400
- if (!payload)
401
- continue;
402
- if (runId && payload.runId && payload.runId !== runId)
403
- continue;
404
- const state = typeof payload.state === 'string' ? payload.state : undefined;
405
- if (state === 'delta') {
406
- const text = extractTextFromPayload(payload);
407
- if (text) {
408
- resultText += text;
409
- yield { type: 'delta', content: text };
410
- }
411
- }
412
- else if (state === 'final' && !finalized) {
413
- finalized = true;
414
- const text = extractTextFromPayload(payload);
415
- if (text)
416
- resultText += text;
417
- streamDone = true;
418
- }
419
- else if (state === 'error' && !finalized) {
420
- finalized = true;
421
- const errMsg = typeof payload.errorMessage === 'string'
422
- ? payload.errorMessage
423
- : 'chat error';
424
- yield { type: 'error', message: errMsg };
425
- return;
426
- }
427
- }
428
- if (frame.event === 'agent') {
429
- const payload = frame.payload;
430
- if (!payload)
431
- continue;
432
- if (runId && payload.runId && payload.runId !== runId)
433
- continue;
434
- const stream = typeof payload.stream === 'string' ? payload.stream : '';
435
- const data = (payload.data ?? {});
436
- if (stream === 'assistant') {
437
- const delta = extractAgentAssistantText(data);
438
- if (delta) {
439
- resultText += delta;
440
- yield { type: 'delta', content: delta };
441
- }
442
- }
443
- else if (stream === 'tool' && typeof data.name === 'string') {
444
- // Observability only — Gateway executes tools via MCP
445
- const argsStr = typeof data.arguments === 'string'
446
- ? data.arguments
447
- : JSON.stringify(data.arguments ?? {});
448
- yield { type: 'thinking', content: `[工具] ${data.name}(${argsStr.slice(0, 200)})` };
449
- }
450
- else if (stream === 'result') {
451
- const resultTxt = extractAgentAssistantText(data);
452
- if (resultTxt) {
453
- resultText += resultTxt;
454
- yield { type: 'delta', content: resultTxt };
455
- }
456
- }
457
- else if (stream === 'error' && !finalized) {
458
- finalized = true;
459
- const errMsg = typeof data.error === 'string'
460
- ? data.error
461
- : typeof data.message === 'string'
462
- ? data.message
463
- : 'agent error';
464
- yield { type: 'error', message: errMsg };
465
- return;
466
- }
467
- else if (stream === 'lifecycle') {
468
- const phase = data.phase;
469
- if (phase === 'end' && !finalized) {
470
- finalized = true;
471
- streamDone = true;
472
- }
473
- else if (phase === 'error' && !finalized) {
474
- finalized = true;
475
- const errMsg = (typeof data.error === 'string' ? data.error : undefined)
476
- ?? (typeof data.message === 'string' ? data.message : undefined)
477
- ?? 'agent lifecycle error';
478
- yield { type: 'error', message: errMsg };
479
- return;
480
- }
481
- }
482
- }
483
- }
484
- if (ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
485
- break;
179
+ stdoutBuffer += typeof chunk === 'string' ? chunk : chunk.toString('utf-8');
180
+ }
181
+ const exitResult = await waitForExit(proc);
182
+ completed = true;
183
+ if (exitResult.code === 0 && stdoutBuffer.trim()) {
184
+ const { text, sessionId, usage } = parseOpenClawCliResult(stdoutBuffer);
185
+ if (text) {
186
+ yield {
187
+ type: 'result',
188
+ content: text,
189
+ ...(sessionId ? { sessionId } : {}),
190
+ ...(usage ? { usage } : {}),
191
+ };
486
192
  }
487
- if (!streamDone) {
488
- await new Promise((resolve) => {
489
- wakeUp = resolve;
490
- });
491
- wakeUp = null;
193
+ else {
194
+ // JSON 解析成功但 text 为空
195
+ yield {
196
+ type: 'result',
197
+ content: stdoutBuffer.trim().slice(0, 500),
198
+ };
492
199
  }
493
200
  }
494
- yield {
495
- type: 'result',
496
- content: resultText.trim() || (ack?.status === 'started' ? '任务已提交,等待执行...' : ''),
497
- sessionId: isOneshot ? undefined : sessionId,
498
- };
201
+ else {
202
+ // exit≠0:取 stderr 最后非空行(真实错误通常在末尾,前面可能混 plugin warning 噪声)
203
+ const stderrLines = stderrBuffer.trim().split('\n').filter((l) => l.trim());
204
+ const errMsg = stderrLines[stderrLines.length - 1] || exitResult.error || `openclaw exited with code ${exitResult.code}`;
205
+ yield { type: 'error', message: `OpenClaw agent failed: ${errMsg}` };
206
+ }
499
207
  }
500
208
  catch (err) {
501
- yield { type: 'error', message: err instanceof Error ? err.message : String(err) };
209
+ yield {
210
+ type: 'error',
211
+ message: `OpenClaw agent error: ${err instanceof Error ? err.message : String(err)}`,
212
+ };
502
213
  }
503
214
  finally {
504
215
  signal?.removeEventListener('abort', onAbort);
505
- ws.off('message', persistHandler);
506
- ws.off('close', onClose);
507
- ws.close();
508
216
  }
509
217
  }
510
218
  // ── discoverSkills ──
@@ -553,246 +261,72 @@ export class OpenClawAdapter {
553
261
  catch { /* silently skip */ }
554
262
  return skills;
555
263
  }
556
- // ── authenticated WebSocket connect ──
557
- authenticatedConnect(url, host, signal) {
558
- return new Promise((resolve, reject) => {
559
- const ws = new WebSocket(url, {
560
- headers: { Origin: `http://${host}` },
561
- });
562
- let handshakeDone = false;
563
- const onAbort = () => {
564
- ws.close();
565
- if (!handshakeDone)
566
- reject(new Error('Aborted'));
567
- };
568
- signal?.addEventListener('abort', onAbort, { once: true });
569
- const cleanup = () => {
570
- handshakeDone = true;
571
- signal?.removeEventListener('abort', onAbort);
572
- ws.off('message', onMessage);
573
- };
574
- const onMessage = (data) => {
575
- try {
576
- const frame = JSON.parse(data.toString());
577
- if (frame.event === 'connect.challenge') {
578
- const connectId = `connect-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
579
- const role = 'operator';
580
- const scopes = ['operator.admin', 'operator.write', 'operator.read'];
581
- const connectParams = {
582
- minProtocol: PROTOCOL_VERSION,
583
- maxProtocol: PROTOCOL_VERSION,
584
- client: {
585
- // 'gateway-client' + mode:'backend' = loopback backend 客户端(官方支持,无需 device identity)。
586
- // 服务端 workstation 使用 'openclaw-control-ui' + mode:'webchat',两者 client.id 不同是因为
587
- // daemon 走本地 loopback(同机部署),server 走容器远程连接。
588
- id: 'gateway-client',
589
- displayName: 'WinMatrix Agent',
590
- version: '1.0.0',
591
- platform: process.platform,
592
- mode: 'backend',
593
- },
594
- role,
595
- scopes,
596
- };
597
- if (this.token) {
598
- connectParams.auth = { token: this.token };
599
- }
600
- ws.send(JSON.stringify({ type: 'req', id: connectId, method: 'connect', params: connectParams }));
601
- return;
602
- }
603
- if (frame.type === 'res' && frame.id?.startsWith('connect-')) {
604
- if (frame.ok) {
605
- cleanup();
606
- resolve(ws);
607
- }
608
- else {
609
- cleanup();
610
- const errDetail = typeof frame.error === 'string' ? frame.error : JSON.stringify(frame.error);
611
- reject(new Error(`Gateway connect rejected: ${errDetail}`));
612
- }
613
- }
614
- }
615
- catch {
616
- // skip malformed frames during handshake
617
- }
618
- };
619
- ws.on('message', onMessage);
620
- ws.on('error', (err) => {
621
- cleanup();
622
- reject(new Error(`OpenClaw Gateway not reachable at ${url}: ${err.message}`));
623
- });
624
- ws.on('close', () => {
625
- if (!handshakeDone) {
626
- cleanup();
627
- reject(new Error('WebSocket closed before handshake completed'));
628
- }
629
- });
630
- if (signal?.aborted) {
631
- ws.close();
632
- reject(new Error('Aborted'));
633
- }
634
- });
635
- }
636
- // ── RPC helpers ──
637
- /**
638
- * Generic sendRequest — temporary onMessage handler pattern.
639
- * Used by healthCheck/listSessions/getSession (short-lived connections).
640
- */
641
- sendRequest(ws, method, params) {
642
- return new Promise((resolve, reject) => {
643
- const id = `${method}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
644
- let resolved = false;
645
- const timer = setTimeout(() => {
646
- if (resolved)
647
- return;
648
- resolved = true;
649
- ws.off('message', onMessage);
650
- reject(new Error(`OpenClaw request timeout: ${method}`));
651
- }, 30_000);
652
- const onMessage = (data) => {
653
- try {
654
- const frame = JSON.parse(data.toString());
655
- if (frame.type === 'res' && frame.id === id) {
656
- if (resolved)
657
- return;
658
- resolved = true;
659
- clearTimeout(timer);
660
- ws.off('message', onMessage);
661
- resolve({
662
- ok: frame.ok === true,
663
- payload: frame.payload,
664
- error: frame.error,
665
- });
666
- }
667
- }
668
- catch {
669
- // skip malformed frames
670
- }
671
- };
672
- ws.on('message', onMessage);
673
- ws.send(JSON.stringify({ type: 'req', id, method, params }));
674
- });
675
- }
676
- /**
677
- * Send a request using a pendingRequests map (persistent handler multiplexing).
678
- * Used by execute() to multiplex req/res with event streaming on the same ws.
679
- */
680
- sendRequestViaMap(ws, pendingRequests, method, params) {
681
- return new Promise((resolve, reject) => {
682
- const id = `${method}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
683
- const timer = setTimeout(() => {
684
- pendingRequests.delete(id);
685
- reject(new Error(`OpenClaw request timeout: ${method}`));
686
- }, 30_000);
687
- pendingRequests.set(id, { resolve, reject, timer });
688
- ws.send(JSON.stringify({ type: 'req', id, method, params }));
689
- });
690
- }
691
264
  // ── healthCheck ──
692
265
  async healthCheck() {
693
266
  try {
694
- const ws = await this.authenticatedConnect(this.gatewayUrl, this.gatewayHost);
695
- const result = await this.sendRequest(ws, 'health', {});
696
- ws.close();
697
- if (result.ok) {
698
- const version = typeof result.payload?.version === 'string' ? result.payload.version : undefined;
267
+ // openclaw --version 验 CLI 可执行(轻量,不连 gateway)
268
+ const { spawnSync } = await import('node:child_process');
269
+ const result = spawnSync(this.openclawPath, ['--version'], {
270
+ timeout: 5_000,
271
+ stdio: ['ignore', 'pipe', 'pipe'],
272
+ encoding: 'utf-8',
273
+ });
274
+ if (result.status === 0) {
275
+ const version = result.stdout.trim().split('\n')[0].slice(0, 100);
699
276
  return { available: true, version };
700
277
  }
701
- return { available: true };
278
+ return { available: false, error: `openclaw --version exited with code ${result.status}` };
702
279
  }
703
280
  catch (err) {
704
- return { available: false, error: err instanceof Error ? err.message : String(err) };
281
+ return {
282
+ available: false,
283
+ error: err instanceof Error ? err.message : String(err),
284
+ };
705
285
  }
706
286
  }
707
- // ── listSessions ──
287
+ // ── session management ──
708
288
  async listSessions(options) {
289
+ // CLI: openclaw sessions --json --agent <id> --limit <n>
709
290
  try {
710
- const ws = await this.authenticatedConnect(this.gatewayUrl, this.gatewayHost);
711
- const result = await this.sendRequest(ws, 'sessions.list', {
712
- dir: options.dir,
713
- limit: options.limit ?? 50,
291
+ const { spawnSync } = await import('node:child_process');
292
+ const agentId = this.agentId ?? 'main';
293
+ const limit = options.limit ?? 20;
294
+ const result = spawnSync(this.openclawPath, ['sessions', '--json', '--agent', agentId, '--limit', String(limit)], {
295
+ timeout: 10_000,
296
+ stdio: ['ignore', 'pipe', 'pipe'],
297
+ encoding: 'utf-8',
298
+ cwd: options.dir ?? process.cwd(),
714
299
  });
715
- ws.close();
716
- if (!result.ok && result.error) {
717
- return { sessions: [], error: typeof result.error === 'string' ? result.error : JSON.stringify(result.error) };
300
+ if (result.status === 0 && result.stdout.trim()) {
301
+ const parsed = JSON.parse(result.stdout);
302
+ const sessions = (parsed.sessions ?? []).map((s) => ({
303
+ id: s.sessionId ?? s.id ?? '',
304
+ customTitle: s.title,
305
+ createdAt: s.createdAt ?? '',
306
+ lastActivityAt: s.lastActivityAt ?? '',
307
+ messageCount: s.messageCount,
308
+ }));
309
+ return { sessions };
718
310
  }
719
- const data = result.payload;
720
- const raw = data?.sessions ?? [];
721
- const sessions = raw.map((s) => ({
722
- id: String(s.id ?? s.session_id ?? s.sessionKey ?? ''),
723
- cwd: typeof s.cwd === 'string' ? s.cwd : undefined,
724
- summary: typeof s.summary === 'string' ? s.summary : undefined,
725
- createdAt: typeof s.created_at === 'string' ? s.created_at : undefined,
726
- lastActivityAt: typeof s.last_activity_at === 'string' ? s.last_activity_at : undefined,
727
- messageCount: typeof s.message_count === 'number' ? s.message_count : undefined,
728
- status: typeof s.status === 'string' ? s.status : undefined,
729
- }));
730
- return { sessions };
731
- }
732
- catch (err) {
733
- return { sessions: [], error: err instanceof Error ? err.message : String(err) };
734
311
  }
312
+ catch { /* fall through to empty */ }
313
+ return { sessions: [] };
735
314
  }
736
- // ── getSession ──
737
- async getSession(sessionId, options = {}) {
738
- try {
739
- const ws = await this.authenticatedConnect(this.gatewayUrl, this.gatewayHost);
740
- const result = await this.sendRequest(ws, 'sessions.preview', {
741
- sessionKey: sessionId,
742
- messageLimit: options.messageLimit ?? 100,
743
- includeRaw: options.includeRawMessages === true,
744
- });
745
- ws.close();
746
- if (!result.ok && result.error) {
747
- return {
748
- session: { id: sessionId },
749
- messageCount: 0,
750
- truncated: false,
751
- userPrompts: [],
752
- assistantText: '',
753
- lastAssistantText: '',
754
- error: typeof result.error === 'string' ? result.error : JSON.stringify(result.error),
755
- };
756
- }
757
- const data = result.payload;
758
- const messages = data?.messages ?? [];
759
- let assistantText = '';
760
- let lastAssistantText = '';
761
- const userPrompts = [];
762
- for (const msg of messages) {
763
- if (msg.role === 'user' && msg.content) {
764
- userPrompts.push(msg.content);
765
- }
766
- else if (msg.role === 'assistant' && msg.content) {
767
- assistantText += msg.content + '\n';
768
- lastAssistantText = msg.content;
769
- }
770
- }
771
- return {
772
- session: {
773
- id: sessionId,
774
- ...(data?.session ?? {}),
775
- },
776
- messageCount: messages.length,
777
- truncated: messages.length >= (options.messageLimit ?? 100),
778
- userPrompts,
779
- assistantText: assistantText.trim(),
780
- lastAssistantText,
781
- rawMessages: options.includeRawMessages ? messages : undefined,
782
- rawMessagesIncluded: options.includeRawMessages === true,
783
- };
784
- }
785
- catch (err) {
786
- return {
787
- session: { id: sessionId },
788
- messageCount: 0,
789
- truncated: false,
790
- userPrompts: [],
791
- assistantText: '',
792
- lastAssistantText: '',
793
- error: err instanceof Error ? err.message : String(err),
794
- };
795
- }
315
+ async getSession(sessionId, options) {
316
+ // CLI 无直接 get session 命令;返回 stub(daemon AgentProcessManager fallback)
317
+ return {
318
+ session: {
319
+ id: sessionId,
320
+ customTitle: 'N/A',
321
+ createdAt: '',
322
+ lastActivityAt: '',
323
+ },
324
+ messageCount: 0,
325
+ truncated: false,
326
+ userPrompts: [],
327
+ assistantText: '',
328
+ lastAssistantText: '',
329
+ };
796
330
  }
797
331
  }
798
332
  //# sourceMappingURL=OpenClawAdapter.js.map