@harness-mix/cli 0.2.4 → 0.3.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/CHANGELOG.md +11 -0
- package/README.md +4 -4
- package/docs/harness-management.md +1 -1
- package/docs/multi-agent-collaboration.md +3 -1
- package/docs/native-acp.md +138 -127
- package/output/native-build/renderer-extension.js +149 -19
- package/package.json +4 -2
- package/scripts/acp-image-test.cjs +69 -0
- package/scripts/adapters-test.cjs +25 -0
- package/scripts/codex-accounts-test.cjs +34 -0
- package/scripts/codex-adapter-test.cjs +77 -1
- package/scripts/collaboration-test.cjs +304 -1
- package/scripts/collaboration-ui-smoke.cjs +25 -3
- package/scripts/e2e-delegate.cjs +1 -1
- package/scripts/e2e-hermes-image.cjs +60 -0
- package/scripts/openclaw-adapter-test.cjs +121 -3
- package/scripts/openclaw-mcp-probe.cjs +100 -0
- package/scripts/openclaw-mcp-tool-probe.cjs +55 -0
- package/scripts/openclaw-thinking-probe.cjs +73 -0
- package/scripts/storage-verification-test.cjs +11 -0
- package/src/main/adapters/claude.js +15 -7
- package/src/main/adapters/codex-app-server.js +29 -11
- package/src/main/adapters/codex.js +39 -5
- package/src/main/adapters/omp.js +7 -2
- package/src/main/adapters/openclaw.js +534 -343
- package/src/main/adapters/pi-family.js +35 -8
- package/src/main/adapters/zcode.js +12 -8
- package/src/main/host/collaboration-tools.js +1 -1
- package/src/main/host/collaboration.js +285 -36
- package/src/main/host/runtime.js +9 -3
- package/src/main/host/verification-gates.js +14 -2
- package/src/main/native/codex-accounts.js +15 -4
- package/src/main/native/protocol.js +6 -0
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js +3 -1
- package/src/native-ui/desktop-control/dist/renderer-cdp-control-session.js.map +1 -1
- package/src/native-ui/desktop-control/dist/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-binding-probe.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts +1 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-collab-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts +29 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-model-client.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts +4 -0
- package/src/native-ui/renderer-extension/dist/types/renderer-team-cards.d.ts.map +1 -1
- package/src/native-ui/renderer-extension/dist/types/tsconfig.tsbuildinfo +1 -1
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +20 -7
- package/src/native-ui/renderer-extension/src/renderer-collab-cards.ts +25 -0
- package/src/native-ui/renderer-extension/src/renderer-model-client.ts +27 -0
- package/src/native-ui/renderer-extension/src/renderer-team-cards.ts +93 -13
- package/src/native-ui/renderer-extension/test/renderer-model-client.test.ts +47 -1
|
@@ -1,343 +1,534 @@
|
|
|
1
|
-
const { execFile } = require("node:child_process");
|
|
2
|
-
const { randomUUID } = require("node:crypto");
|
|
3
|
-
const
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
await
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
async
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
await OpenClawGatewayHost.
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
1
|
+
const { execFile } = require("node:child_process");
|
|
2
|
+
const { randomUUID } = require("node:crypto");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const os = require("node:os");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { cliSpawn } = require("../host/jsonl");
|
|
7
|
+
const { OpenClawGatewayHost, readGatewayConfig, OPENCLAW_CONFIG } = require("./openclaw-gateway");
|
|
8
|
+
const { recordNative } = require("../harness-adapter/fixture-recorder");
|
|
9
|
+
|
|
10
|
+
// 静态思考档位仅作兜底(sessions.describe 失败时);会话级权威目录来自 sessions.describe
|
|
11
|
+
// (thinkingLevels/thinkingDefault,随模型而变)。实测 Gateway 2026.5.12(2026-09-21 复核):
|
|
12
|
+
// sessions.create 后、首turn 前 describe 即返回 thinkingLevels:[off,minimal,low,medium,high](5 档)
|
|
13
|
+
// 与 thinkingDefault:"off",open() 的 refreshSessionState 即可取到;兜底列表对齐该实测值,
|
|
14
|
+
// 证据:output/openclaw-thinking-probe/*.json。
|
|
15
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
16
|
+
const AGENT_TIMEOUT_MS = 600_000; // 与 openclaw agent --timeout 默认值一致
|
|
17
|
+
|
|
18
|
+
const manifest = {
|
|
19
|
+
id: "openclaw",
|
|
20
|
+
name: "OpenClaw",
|
|
21
|
+
icon: "openclaw-color.svg",
|
|
22
|
+
aliases: ["openclaw"],
|
|
23
|
+
// 能力均按 Gateway 2026.5.12 实测声明;usage/contextUsage 取自 sessions.describe 的原生
|
|
24
|
+
// token 统计与上下文窗口,不用累计输入量伪装。thinking 流投影已接线(形状取自上游
|
|
25
|
+
// emitAgentEvent stream:"thinking" 的 {text,delta},会话级 reasoningLevel=stream 已实测
|
|
26
|
+
// 受理),但 Gateway 的 agent RPC 通道未接 onReasoningStream(该回调只在 Telegram 等
|
|
27
|
+
// 消息渠道装配;2026.5.12 实测:reasoningLevel=stream + 推理模型 MiniMax-M3 也只有
|
|
28
|
+
// assistant 流,推理内容并入正文),故能力位保持 false,属上游协议边界。
|
|
29
|
+
// compaction:原生 /compact 实测在 commands.list 目录中,且斜杠命令以消息文本经
|
|
30
|
+
// agent 运行管道执行(压缩结果流形状未实测,结算不伪造 token 前后值)。
|
|
31
|
+
capabilities: { streaming: true, thinking: false, tools: true, approvals: true, questions: false, models: true, thinkingLevels: true, permissionModes: false, resume: true, fork: false, forkFromMessage: false, compaction: true, usage: true, contextUsage: true, attachments: true },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
manifest.integrations = { mcp: true, skills: {
|
|
35
|
+
global: ['.openclaw/skills', '.agents/skills'],
|
|
36
|
+
project: ['skills', '.agents/skills'],
|
|
37
|
+
overrides: { '.openclaw/skills': { env: 'OPENCLAW_STATE_DIR', suffix: 'skills' } },
|
|
38
|
+
} };
|
|
39
|
+
|
|
40
|
+
// 托管 MCP 在 OpenClaw 侧落到 openclaw.json 的 mcp.servers 注册表(机器级,内嵌运行时按
|
|
41
|
+
// 配置消费)。只允许触碰 harness-mix/ 前缀的自有键,用户手写的服务器定义绝不增删改。
|
|
42
|
+
const MCP_OWNED_PREFIX = "harness-mix/";
|
|
43
|
+
const OPENCLAW_MCP_DOC = "mcp.servers 注册表(OpenClaw 官方集中式 MCP 客户端配置)";
|
|
44
|
+
|
|
45
|
+
/** 托管条目(managed-mcp 原始形状)→ OpenClaw 注册表条目;形状对齐 `openclaw mcp set` 的 JSON 值 */
|
|
46
|
+
function toOpenClawServerEntry(server) {
|
|
47
|
+
if (server?.url) return { url: server.url, ...(server.http_headers ? { headers: server.http_headers } : {}) };
|
|
48
|
+
const entry = { command: server.command };
|
|
49
|
+
if (Array.isArray(server.args) && server.args.length) entry.args = server.args;
|
|
50
|
+
if (server.env && typeof server.env === "object" && Object.keys(server.env).length) entry.env = server.env;
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 纯函数:现有 mcp.servers + 托管集合 → 对账计划(新增/更新/移除,仅限自有键)。
|
|
56
|
+
* 返回 { kept, sets: [[name, entry]], unsets: [name] };kept 为不动区(含用户键)。
|
|
57
|
+
*/
|
|
58
|
+
function planMcpReconcile(currentServers, managed) {
|
|
59
|
+
const current = (currentServers && typeof currentServers === "object") ? currentServers : {};
|
|
60
|
+
const desired = {};
|
|
61
|
+
for (const server of Array.isArray(managed) ? managed : []) {
|
|
62
|
+
if (!server?.name) continue;
|
|
63
|
+
desired[`${MCP_OWNED_PREFIX}${server.name}`] = toOpenClawServerEntry(server);
|
|
64
|
+
}
|
|
65
|
+
const sets = [];
|
|
66
|
+
const unsets = [];
|
|
67
|
+
const kept = {};
|
|
68
|
+
for (const [name, entry] of Object.entries(current)) {
|
|
69
|
+
if (!name.startsWith(MCP_OWNED_PREFIX)) { kept[name] = entry; continue; }
|
|
70
|
+
if (!(name in desired)) { unsets.push(name); continue; }
|
|
71
|
+
if (JSON.stringify(entry) !== JSON.stringify(desired[name])) sets.push([name, desired[name]]);
|
|
72
|
+
}
|
|
73
|
+
for (const [name, entry] of Object.entries(desired)) {
|
|
74
|
+
if (!(name in current)) sets.push([name, entry]);
|
|
75
|
+
}
|
|
76
|
+
return { kept, sets, unsets };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 把托管 MCP 服务器对账进 openclaw.json 的 mcp.servers(文件级外科合并)。
|
|
81
|
+
* 返回是否有实际变更。实测(Gateway 2026.5.12):mcp.servers 仅在 Gateway 启动时被
|
|
82
|
+
* agent 运行消费,运行中不热载——调用方应在连接 Gateway 之前对账(本次拉起即生效),
|
|
83
|
+
* 对已在运行的 Gateway 需提示重启后新工具才可用。
|
|
84
|
+
* - 只动 harness-mix/* 自有键;写回前做"去自有键后逐键等值"断言,内容零丢失才落盘;
|
|
85
|
+
* - 原子写(临时文件 + rename);失败只记 diagnostic,不阻塞会话(与 Skills 预建同策略);
|
|
86
|
+
* - 上游 CLI `openclaw mcp set` 的整档规范化写入会触发 size-drop 守卫(本机排版为
|
|
87
|
+
* PowerShell 风格),故不走 CLI/RPC 写入面;首次推送会把排版规整为标准 JSON。
|
|
88
|
+
*/
|
|
89
|
+
async function applyManagedMcpServers(managed, diagnostic = () => {}) {
|
|
90
|
+
let raw;
|
|
91
|
+
try { raw = await fs.promises.readFile(OPENCLAW_CONFIG, "utf8"); }
|
|
92
|
+
catch (error) { diagnostic(`[openclaw] 读取 ${OPENCLAW_MCP_DOC} 失败:${error.message}`); return false; }
|
|
93
|
+
let config;
|
|
94
|
+
try { config = JSON.parse(raw); } catch (error) { diagnostic(`[openclaw] 配置不是合法 JSON,跳过 MCP 对账`); return false; }
|
|
95
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) { diagnostic(`[openclaw] 配置根不是对象,跳过 MCP 对账`); return false; }
|
|
96
|
+
|
|
97
|
+
const plan = planMcpReconcile(config.mcp?.servers, managed);
|
|
98
|
+
if (!plan.sets.length && !plan.unsets.length) return false;
|
|
99
|
+
|
|
100
|
+
// 保留原始形状:用户无自有 servers 键时清理后不残留空键;mcp 原本缺失则不创建
|
|
101
|
+
const nextMcp = { ...config.mcp };
|
|
102
|
+
const keptServers = plan.kept;
|
|
103
|
+
if (Object.keys(keptServers).length || plan.sets.length) {
|
|
104
|
+
nextMcp.servers = { ...keptServers, ...Object.fromEntries(plan.sets) };
|
|
105
|
+
} else {
|
|
106
|
+
delete nextMcp.servers;
|
|
107
|
+
}
|
|
108
|
+
const next = { ...config };
|
|
109
|
+
if (Object.keys(nextMcp).length) next.mcp = nextMcp;
|
|
110
|
+
else delete next.mcp;
|
|
111
|
+
const text = `${JSON.stringify(next, null, 2).replace(/\n/g, "\r\n")}\n`;
|
|
112
|
+
|
|
113
|
+
// 等值断言:去掉自有键后,新旧内容必须逐键一致(防序列化意外丢内容)
|
|
114
|
+
const stripOwned = (servers) => Object.fromEntries(Object.entries(servers ?? {}).filter(([k]) => !k.startsWith(MCP_OWNED_PREFIX)));
|
|
115
|
+
const before = JSON.stringify({ ...config, mcp: { ...config.mcp, servers: stripOwned(config.mcp?.servers) } });
|
|
116
|
+
const after = JSON.stringify({ ...JSON.parse(text), mcp: { ...JSON.parse(text).mcp, servers: stripOwned(JSON.parse(text).mcp?.servers) } });
|
|
117
|
+
if (before !== after) { diagnostic(`[openclaw] MCP 对账等值断言失败,放弃写入`); return false; }
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const tmp = path.join(os.tmpdir(), `harness-mix-openclaw-mcp-${randomUUID()}.json`);
|
|
121
|
+
await fs.promises.writeFile(tmp, text, "utf8");
|
|
122
|
+
await fs.promises.rename(tmp, OPENCLAW_CONFIG);
|
|
123
|
+
diagnostic(`[openclaw] MCP 注册表对账完成:+${plan.sets.length} 更新 / -${plan.unsets.length} 移除(仅自有键)`);
|
|
124
|
+
return true;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
diagnostic(`[openclaw] 写入 ${OPENCLAW_MCP_DOC} 失败:${error.message}`);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const textOfContent = (result) => (result?.content ?? []).map((c) => (c?.type === "text" ? c.text : "")).filter(Boolean).join("\n");
|
|
132
|
+
const stringifyArgs = (args) => {
|
|
133
|
+
if (args == null) return undefined;
|
|
134
|
+
if (typeof args === "string") return args;
|
|
135
|
+
try { return JSON.stringify(args, null, 2); } catch { return String(args); }
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gateway agent 广播事件({runId, stream, data, sessionKey, seq, ts})→ 统一事件投影。
|
|
140
|
+
* 纯映射(运行时与 fixture 回放共用);session 仅用于跨事件累积(assistant/thinking 累计文本、生命周期错误)。
|
|
141
|
+
* thinking 流形状取自上游 emitAgentEvent({text 累计, delta 增量},与 assistant 同构);
|
|
142
|
+
* item/plan/patch/approval/compaction 流形态未逐字段实测,不投影,不伪造。
|
|
143
|
+
*/
|
|
144
|
+
function projectAgentEvent(payload, session) {
|
|
145
|
+
const out = [];
|
|
146
|
+
if (!payload || typeof payload !== "object") return out;
|
|
147
|
+
const data = payload.data ?? {};
|
|
148
|
+
switch (payload.stream) {
|
|
149
|
+
case "assistant": {
|
|
150
|
+
let delta = typeof data.delta === "string" ? data.delta : "";
|
|
151
|
+
if (!delta && typeof data.text === "string") {
|
|
152
|
+
// 缺 delta 时按累计文本差分补齐
|
|
153
|
+
const last = session?.state?.lastAssistantText ?? "";
|
|
154
|
+
delta = data.text.startsWith(last) ? data.text.slice(last.length) : data.text;
|
|
155
|
+
}
|
|
156
|
+
if (typeof data.text === "string" && session) session.state.lastAssistantText = data.text;
|
|
157
|
+
if (delta) out.push({ kind: "text-delta", text: delta });
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
case "thinking": {
|
|
161
|
+
let delta = typeof data.delta === "string" ? data.delta : "";
|
|
162
|
+
if (!delta && typeof data.text === "string") {
|
|
163
|
+
const last = session?.state?.lastThinkingText ?? "";
|
|
164
|
+
delta = data.text.startsWith(last) ? data.text.slice(last.length) : data.text;
|
|
165
|
+
}
|
|
166
|
+
if (typeof data.text === "string" && session) session.state.lastThinkingText = data.text;
|
|
167
|
+
if (delta) out.push({ kind: "thinking-delta", text: delta });
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case "tool": {
|
|
171
|
+
if (!data.toolCallId) break;
|
|
172
|
+
const title = data.meta ? `${data.name ?? "tool"} ${data.meta}` : (data.name ?? "tool");
|
|
173
|
+
if (data.phase === "start") {
|
|
174
|
+
out.push({ kind: "tool", toolCallId: data.toolCallId, title, state: "running", input: stringifyArgs(data.args) });
|
|
175
|
+
} else if (data.phase === "update") {
|
|
176
|
+
const output = textOfContent(data.partialResult);
|
|
177
|
+
if (output) out.push({ kind: "tool", toolCallId: data.toolCallId, title, state: "running", output });
|
|
178
|
+
} else if (data.phase === "result") {
|
|
179
|
+
out.push({ kind: "tool", toolCallId: data.toolCallId, title, state: data.isError ? "error" : "done", output: textOfContent(data.result) || (data.isError ? "原生工具执行失败" : undefined) });
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
case "command_output": {
|
|
184
|
+
// 命令输出与工具卡片同一投影(toolCallId 归并):delta 增量输出,end 携带退出码收尾
|
|
185
|
+
if (!data.toolCallId) break;
|
|
186
|
+
const title = data.title || data.name || "command";
|
|
187
|
+
if (data.phase === "delta") {
|
|
188
|
+
if (typeof data.output === "string" && data.output) out.push({ kind: "tool", toolCallId: data.toolCallId, title, state: "running", output: data.output });
|
|
189
|
+
} else if (data.phase === "end") {
|
|
190
|
+
out.push({ kind: "tool", toolCallId: data.toolCallId, title, state: data.status === "completed" ? "done" : "error", output: data.output, detail: data.exitCode != null ? `exit ${data.exitCode}` : undefined });
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "lifecycle": {
|
|
195
|
+
if (session && data.phase === "error") session.state.lastError = typeof data.error === "string" ? data.error : data.error?.message;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
default:
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** exec/plugin 审批请求广播 → 统一 approval 事件 */
|
|
205
|
+
function projectApproval(eventName, payload, sessionKey) {
|
|
206
|
+
const id = String(payload?.id ?? "");
|
|
207
|
+
const request = payload?.request ?? {};
|
|
208
|
+
const isPlugin = eventName.startsWith("plugin.") || id.startsWith("plugin:");
|
|
209
|
+
const lines = [];
|
|
210
|
+
if (request.command) lines.push(`\`${request.command}\``);
|
|
211
|
+
if (request.cwd) lines.push(`工作目录:${request.cwd}`);
|
|
212
|
+
if (request.ask) lines.push(`原因:${request.ask}`);
|
|
213
|
+
return {
|
|
214
|
+
kind: "approval",
|
|
215
|
+
requestId: `openclaw-${id}`,
|
|
216
|
+
title: isPlugin ? "插件权限审批" : "命令执行审批",
|
|
217
|
+
message: lines.join("\n") || undefined,
|
|
218
|
+
options: [
|
|
219
|
+
{ id: "allowed-once", label: "允许一次" },
|
|
220
|
+
{ id: "allowed-always", label: "始终允许" },
|
|
221
|
+
{ id: "rejected", label: "拒绝", kind: "reject" },
|
|
222
|
+
],
|
|
223
|
+
nativeRef: { sessionId: sessionKey, interactionId: id },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** sessions.describe 行 → 会话状态(生效模型、思考档位目录与当前值、原生用量)。字段随 turn 推进逐渐齐备。 */
|
|
228
|
+
function applySessionDescription(session, row) {
|
|
229
|
+
if (!row || typeof row !== "object") return;
|
|
230
|
+
if (Array.isArray(row.thinkingLevels) && row.thinkingLevels.length) {
|
|
231
|
+
session.state.thinkingLevels = row.thinkingLevels.map((l) => ({ id: String(l.id ?? l), label: String(l.label ?? l.id ?? l) }));
|
|
232
|
+
}
|
|
233
|
+
if (typeof row.thinkingDefault === "string") session.state.thinkingDefault = row.thinkingDefault;
|
|
234
|
+
if (row.model) {
|
|
235
|
+
const id = row.modelProvider ? `${row.modelProvider}/${row.model}` : String(row.model);
|
|
236
|
+
session.model = session.models?.find((m) => m.id === id) ?? { id, name: String(row.model), provider: row.modelProvider };
|
|
237
|
+
}
|
|
238
|
+
if (Number.isFinite(row.totalTokens) && Number.isFinite(row.contextTokens) && row.contextTokens > 0) {
|
|
239
|
+
const usage = { tokens: row.totalTokens, contextWindow: row.contextTokens, contextPercent: Math.round((row.totalTokens / row.contextTokens) * 10000) / 100 };
|
|
240
|
+
if (typeof row.estimatedCostUsd === "number" && row.estimatedCostUsd > 0) usage.cost = row.estimatedCostUsd;
|
|
241
|
+
session.state.usage = usage;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 拉取并应用会话状态;返回是否有可用用量。 */
|
|
246
|
+
async function refreshSessionState(session) {
|
|
247
|
+
const described = await session.host.call("sessions.describe", { key: session.nativeSessionId }).catch(() => null);
|
|
248
|
+
applySessionDescription(session, described?.session);
|
|
249
|
+
return session.state.usage;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** 命令 name → UI 契约 id([A-Za-z0-9._:-]+,≤128);规范化后为空则返回 '',由调用方跳过 */
|
|
253
|
+
function slugifyCommandId(name) {
|
|
254
|
+
if (typeof name !== "string") return "";
|
|
255
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const COMMAND_SOURCES = { native: "原生", skill: "技能", plugin: "插件" };
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* commands.list {scope:'text'} 应答 → 统一命令目录(Gateway 2026.5.12 实测行形状:
|
|
262
|
+
* {name, nativeName, textAliases, description, category, source:'native'|'skill'|'plugin',
|
|
263
|
+
* scope:'both'|'text', acceptsArgs, args:[{name,type,required?,description?,choices?}]})。
|
|
264
|
+
* 斜杠命令以消息文本经 agent 管道执行,只有携带 textAliases[0] 的条目可被 operator
|
|
265
|
+
* 文本面触发,无别名条目诚实跳过;id 取 name 的 slug(与 compact 重复或规范化失败
|
|
266
|
+
* 同样跳过),描述附参数提示(<必选>/[可选])与来源标注。
|
|
267
|
+
*/
|
|
268
|
+
function projectTextCommands(listed) {
|
|
269
|
+
const rows = Array.isArray(listed?.commands) ? listed.commands : Array.isArray(listed) ? listed : [];
|
|
270
|
+
const seen = new Set(["compact"]); // 原生 compact 由专用 execute 条目提供,映射去重
|
|
271
|
+
const out = [];
|
|
272
|
+
for (const row of rows) {
|
|
273
|
+
const alias = row?.textAliases?.[0];
|
|
274
|
+
if (typeof alias !== "string" || !alias) continue;
|
|
275
|
+
const id = slugifyCommandId(row.name);
|
|
276
|
+
if (!id || seen.has(id)) continue;
|
|
277
|
+
seen.add(id);
|
|
278
|
+
const hint = (Array.isArray(row.args) ? row.args : [])
|
|
279
|
+
.filter((arg) => typeof arg?.name === "string" && arg.name)
|
|
280
|
+
.map((arg) => (arg.required === true ? `<${arg.name}>` : `[${arg.name}]`))
|
|
281
|
+
.join(" ");
|
|
282
|
+
out.push({
|
|
283
|
+
id,
|
|
284
|
+
label: alias,
|
|
285
|
+
description: `${[String(row.description ?? "").trim(), hint].filter(Boolean).join(" ")}(${COMMAND_SOURCES[row.source] ?? "原生"})`,
|
|
286
|
+
action: "insert",
|
|
287
|
+
text: `${alias} `,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
function create() {
|
|
293
|
+
const adapter = {
|
|
294
|
+
manifest,
|
|
295
|
+
|
|
296
|
+
async inspect() {
|
|
297
|
+
const cli = cliSpawn("openclaw", ["--version"]);
|
|
298
|
+
const version = await new Promise((resolve) => execFile(cli.command, cli.args, { windowsHide: true, timeout: 10000 }, (error, out) => resolve(error ? null : out.trim())));
|
|
299
|
+
if (!version) return { available: false, detail: "OpenClaw CLI 不可用(npm i -g openclaw)" };
|
|
300
|
+
try {
|
|
301
|
+
const cfg = await readGatewayConfig();
|
|
302
|
+
return { available: true, detail: `${version} · Gateway 127.0.0.1:${cfg.port}` };
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return { available: false, detail: `OpenClaw 配置不可读:${error.message}` };
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
async describe() {
|
|
309
|
+
// 全局模型目录:只读已有 Gateway(不为选择器拉起进程),不可达时交回空目录
|
|
310
|
+
try {
|
|
311
|
+
const host = await OpenClawGatewayHost.acquire(() => {}, { autostart: false });
|
|
312
|
+
try { return { models: await listModels(host), thinkingLevels: thinkingCatalog(), permissionModes: [] }; }
|
|
313
|
+
finally { await OpenClawGatewayHost.release(); }
|
|
314
|
+
} catch { return { models: null, thinkingLevels: [], permissionModes: [] }; }
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
async open({ thread, emit: emitEvent, diagnostic = () => {}, managedMcp = [] }) {
|
|
318
|
+
// 托管 MCP 先落注册表再连 Gateway:Gateway 由本次拉起则启动即消费到新工具;
|
|
319
|
+
// 已在运行的 Gateway 实测不热载 mcp.servers,变更后提示重启生效。
|
|
320
|
+
const mcpChanged = await applyManagedMcpServers(managedMcp, diagnostic);
|
|
321
|
+
const host = await OpenClawGatewayHost.acquire(diagnostic);
|
|
322
|
+
if (mcpChanged && !host.child) diagnostic("[openclaw] 托管 MCP 变更已写入注册表;运行中的 Gateway 需重启后新工具才可用");
|
|
323
|
+
const session = {
|
|
324
|
+
host,
|
|
325
|
+
cwd: thread.cwd,
|
|
326
|
+
nativeSessionId: null,
|
|
327
|
+
state: { lastAssistantText: "", lastThinkingText: "", lastError: null, runId: null },
|
|
328
|
+
pendingApprovals: new Map(), // requestId -> approvalId
|
|
329
|
+
modelOverride: null,
|
|
330
|
+
thinkingOverride: null,
|
|
331
|
+
emit: emitEvent,
|
|
332
|
+
unwatch: null,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
// 新会话显式登记(携带标题),恢复会话直接沿用原生 sessionKey
|
|
337
|
+
if (thread.restore && thread.nativeSessionId) {
|
|
338
|
+
session.nativeSessionId = thread.nativeSessionId;
|
|
339
|
+
} else {
|
|
340
|
+
const label = String(thread.title || "Harness Mix 任务").slice(0, 60);
|
|
341
|
+
const baseKey = `harness-mix-${thread.nativeSessionId || randomUUID()}`;
|
|
342
|
+
let created = await host.call("sessions.create", { key: baseKey, label, cwd: thread.cwd }).catch(() => null);
|
|
343
|
+
if (!created?.key) created = await host.call("sessions.create", { key: baseKey, label }).catch(() => null);
|
|
344
|
+
session.nativeSessionId = created?.key ?? `agent:main:${baseKey}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 思考流开关:会话级 reasoningLevel=stream(2026.5.12 实测受理并回读持久化)。
|
|
348
|
+
// 是否真正出流由 thinkingLevel≠off(用户档位)与上游模型/运行时支持决定。
|
|
349
|
+
await host.call("sessions.patch", { key: session.nativeSessionId, reasoningLevel: "stream" }).catch(() => {});
|
|
350
|
+
|
|
351
|
+
session.unwatch = host.onEvent((frame) => {
|
|
352
|
+
recordNative(manifest.id, frame);
|
|
353
|
+
if (frame?.type === "closed") {
|
|
354
|
+
for (const pending of session.pendingApprovals.values()) pending.reject?.(new Error("OpenClaw Gateway 连接已断开"));
|
|
355
|
+
session.pendingApprovals.clear();
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (frame.type !== "event") return;
|
|
359
|
+
// agent 流:按 runId(本轮)或 sessionKey(本会话)归属
|
|
360
|
+
if (frame.event === "agent") {
|
|
361
|
+
const payload = frame.payload ?? {};
|
|
362
|
+
if (payload.runId !== session.state.runId && payload.sessionKey !== session.nativeSessionId) return;
|
|
363
|
+
for (const event of projectAgentEvent(payload, session)) emitEvent(event);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
// 审批广播:只归集显式携带本会话 sessionKey 的请求,不劫持其他客户端的审批
|
|
367
|
+
if (frame.event === "exec.approval.requested" || frame.event === "plugin.approval.requested") {
|
|
368
|
+
const payload = frame.payload ?? {};
|
|
369
|
+
if (payload.request?.sessionKey !== session.nativeSessionId) return;
|
|
370
|
+
const id = String(payload.id ?? "");
|
|
371
|
+
if (!id) return;
|
|
372
|
+
const projected = projectApproval(frame.event, payload, session.nativeSessionId);
|
|
373
|
+
session.pendingApprovals.set(projected.requestId, { approvalId: id });
|
|
374
|
+
emitEvent(projected);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (frame.event === "exec.approval.resolved" || frame.event === "plugin.approval.resolved") {
|
|
378
|
+
// 他端(如 OpenClaw 自有 UI)已处理的审批:同步收掉卡片
|
|
379
|
+
const requestId = `openclaw-${frame.payload?.id ?? ""}`;
|
|
380
|
+
if (session.pendingApprovals.delete(requestId)) emitEvent({ kind: "interaction-responded", requestId });
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const models = await listModels(host).catch(() => []);
|
|
385
|
+
if (models.length) session.models = models;
|
|
386
|
+
// 生效模型/思考档位/用量以 sessions.describe 为准(2026-09-21 实测:新会话 create 后
|
|
387
|
+
// 首 turn 前 describe 即返回 thinkingLevels/thinkingDefault,open 时即可取到目录)
|
|
388
|
+
await refreshSessionState(session);
|
|
389
|
+
if (thread.options?.model) await adapter.setModel(session, thread.options.model);
|
|
390
|
+
if (thread.options?.thinking) await adapter.setThinkingLevel(session, thread.options.thinking);
|
|
391
|
+
emitEvent({ kind: "session", nativeSessionId: session.nativeSessionId, model: session.model });
|
|
392
|
+
return session;
|
|
393
|
+
} catch (error) {
|
|
394
|
+
try { session.unwatch?.(); } catch { /* 已断开 */ }
|
|
395
|
+
await OpenClawGatewayHost.release();
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
async describeFor(session) {
|
|
401
|
+
const levels = session.state.thinkingLevels ?? thinkingCatalog();
|
|
402
|
+
// 原生 sessions.describe 上报的 thinkingDefault 即当前生效档,标记后由协议层作为安全预选秀下发
|
|
403
|
+
const marked = session.state.thinkingDefault
|
|
404
|
+
? levels.map(l => ({ ...l, default: l.id === session.state.thinkingDefault }))
|
|
405
|
+
: levels;
|
|
406
|
+
return { models: session.models ?? null, thinkingLevels: marked, permissionModes: [] };
|
|
407
|
+
},
|
|
408
|
+
|
|
409
|
+
async listModelsFor(session) { return listModels(session.host); },
|
|
410
|
+
|
|
411
|
+
async send(session, text, hooks, attachments) {
|
|
412
|
+
const params = {
|
|
413
|
+
message: text,
|
|
414
|
+
sessionKey: session.nativeSessionId,
|
|
415
|
+
idempotencyKey: randomUUID(),
|
|
416
|
+
deliver: false, // 回复只走 Desktop 原生流,不向 OpenClaw 渠道投递
|
|
417
|
+
};
|
|
418
|
+
// 图片走 Gateway agent RPC 原生 attachments 字段:{ mimeType, content(base64), fileName }(协议 schema 见附件归一化实现)
|
|
419
|
+
const images = attachments?.images ?? [];
|
|
420
|
+
if (images.length) {
|
|
421
|
+
params.attachments = images.map((img) => ({
|
|
422
|
+
mimeType: img.mime || "image/png",
|
|
423
|
+
content: img.data,
|
|
424
|
+
fileName: img.name || undefined,
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
if (session.modelOverride) params.model = session.modelOverride;
|
|
428
|
+
if (session.thinkingOverride) params.thinking = session.thinkingOverride;
|
|
429
|
+
const accepted = await session.host.call("agent", params);
|
|
430
|
+
if (accepted?.status === "in_flight") throw new Error("该会话已有进行中的原生任务,请稍候");
|
|
431
|
+
const runId = accepted?.runId;
|
|
432
|
+
if (!runId) throw new Error("OpenClaw Gateway 未返回 runId");
|
|
433
|
+
session.state.runId = runId;
|
|
434
|
+
session.state.lastAssistantText = "";
|
|
435
|
+
session.state.lastThinkingText = "";
|
|
436
|
+
session.state.lastError = null;
|
|
437
|
+
try {
|
|
438
|
+
const result = await session.host.call("agent.wait", { runId, timeoutMs: AGENT_TIMEOUT_MS }, AGENT_TIMEOUT_MS + 30_000);
|
|
439
|
+
// turn 结束后回读原生会话状态:生效模型、思考目录、token 用量(真实统计,不用估计值冒充)
|
|
440
|
+
const modelBefore = session.model?.id;
|
|
441
|
+
await refreshSessionState(session);
|
|
442
|
+
if (session.state.usage) hooks.emit({ kind: "usage", usage: session.state.usage });
|
|
443
|
+
if (session.model && session.model.id !== modelBefore) hooks.emit({ kind: "session", nativeSessionId: session.nativeSessionId, model: session.model });
|
|
444
|
+
if (result?.status === "ok") { hooks.emit({ kind: "completed", finalAnswer: true }); return; }
|
|
445
|
+
if (result?.stopReason === "rpc") return; // 用户取消:runtime 已按取消结算
|
|
446
|
+
throw new Error(result?.error || session.state.lastError || `原生任务未正常结束(${result?.status ?? "未知"})`);
|
|
447
|
+
} finally {
|
|
448
|
+
if (session.state.runId === runId) session.state.runId = null;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
async cancel(session) {
|
|
453
|
+
const runId = session.state.runId;
|
|
454
|
+
await session.host.call("sessions.abort", runId ? { key: session.nativeSessionId, runId } : { key: session.nativeSessionId }).catch(() => {});
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
async respond(session, requestId, response) {
|
|
458
|
+
const pending = session.pendingApprovals.get(requestId);
|
|
459
|
+
if (!pending) throw new Error("未知或已过期的原生审批请求");
|
|
460
|
+
const decision = response?.confirmed === true || response?.optionId === "allowed-once" ? "allow-once"
|
|
461
|
+
: response?.optionId === "allowed-always" ? "allow-always"
|
|
462
|
+
: "deny"; // 拒绝/取消统一收敛为 deny,不替用户放行
|
|
463
|
+
const method = pending.approvalId.startsWith("plugin:") ? "plugin.approval.resolve" : "exec.approval.resolve";
|
|
464
|
+
await session.host.call(method, { id: pending.approvalId, decision });
|
|
465
|
+
session.pendingApprovals.delete(requestId);
|
|
466
|
+
},
|
|
467
|
+
|
|
468
|
+
async listCommands(session) {
|
|
469
|
+
const compact = { id: "compact", label: "/compact", action: "execute", description: "由 OpenClaw 原生压缩当前会话上下文" };
|
|
470
|
+
if (session?.host) {
|
|
471
|
+
try {
|
|
472
|
+
const listed = await session.host.call("commands.list", { scope: "text" });
|
|
473
|
+
const commands = [compact, ...projectTextCommands(listed)];
|
|
474
|
+
session.state.commands = commands;
|
|
475
|
+
return commands;
|
|
476
|
+
} catch { return [compact]; } // 目录 RPC 失败时至少保留可执行的压缩指令
|
|
477
|
+
}
|
|
478
|
+
// 无会话时只连已有 Gateway(绝不为命令菜单拉起/恢复进程);连接失败 → 菜单诚实禁用
|
|
479
|
+
try {
|
|
480
|
+
const host = await OpenClawGatewayHost.acquire(() => {}, { autostart: false });
|
|
481
|
+
try { return [compact, ...projectTextCommands(await host.call("commands.list", { scope: "text" }))]; }
|
|
482
|
+
finally { await OpenClawGatewayHost.release(); }
|
|
483
|
+
} catch { return []; }
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
async executeCommand(session, id, { emit }) {
|
|
487
|
+
if (id !== "compact") throw new Error("未知 OpenClaw 指令");
|
|
488
|
+
// /compact 即消息文本,走 agent 运行管道;压缩流形状未实测,不伪造 tokensBefore/After
|
|
489
|
+
await this.send(session, "/compact", { emit });
|
|
490
|
+
emit({ kind: "compaction", state: "completed", outcome: "succeeded", summary: "上下文已由 OpenClaw 压缩。" });
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
async setModel(session, model) {
|
|
494
|
+
// Gateway 按 run 应用 model 覆盖(需 operator.admin,握手已声明);逐轮生效,不写原生配置
|
|
495
|
+
session.modelOverride = model.id;
|
|
496
|
+
session.model = model;
|
|
497
|
+
return model;
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
async setThinkingLevel(session, level) {
|
|
501
|
+
const catalog = session.state.thinkingLevels?.map((l) => l.id) ?? THINKING_LEVELS;
|
|
502
|
+
if (!catalog.includes(level)) throw new Error(`OpenClaw 当前会话不支持思考档位 ${level}`);
|
|
503
|
+
session.thinkingOverride = level;
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
async getContextUsage(session) { return session.state.usage; },
|
|
507
|
+
|
|
508
|
+
async close(session) {
|
|
509
|
+
for (const pending of session.pendingApprovals.values()) pending.reject?.(new Error("会话已关闭"));
|
|
510
|
+
session.pendingApprovals.clear();
|
|
511
|
+
try { session.unwatch?.(); } catch { /* 已断开 */ }
|
|
512
|
+
await OpenClawGatewayHost.release();
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
return adapter;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** models.list → 统一目录(id 带 provider 前缀,与 agent 的 model 覆盖参数同形) */
|
|
519
|
+
async function listModels(host) {
|
|
520
|
+
const result = await host.call("models.list", {});
|
|
521
|
+
const rows = Array.isArray(result?.models) ? result.models : [];
|
|
522
|
+
return rows.filter((m) => m && m.id).map((m) => ({
|
|
523
|
+
id: m.provider ? `${m.provider}/${m.id}` : String(m.id),
|
|
524
|
+
name: m.name || String(m.id),
|
|
525
|
+
provider: m.provider,
|
|
526
|
+
description: m.contextWindow ? `上下文窗口 ${Math.round(m.contextWindow / 1000)}k` : undefined,
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function thinkingCatalog() {
|
|
531
|
+
return THINKING_LEVELS.map((id) => ({ id, label: id }));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
module.exports = { manifest, create, projectAgentEvent, projectApproval, applySessionDescription, planMcpReconcile, toOpenClawServerEntry, applyManagedMcpServers, MCP_OWNED_PREFIX, THINKING_LEVELS };
|