@xyagent/cli 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/bin/agentlink +468 -86
- package/package.json +2 -2
- package/src/tunnel_service.mjs +17 -1
- package/src-ext/bin.mjs +12 -11
- package/src-ext/commands/agent.mjs +34 -0
- package/src-ext/commands/pair.mjs +122 -23
- package/src-ext/commands/service.mjs +1 -1
- package/src-ext/core/activeRuns.mjs +26 -9
- package/src-ext/core/defaultWorkspace.mjs +43 -13
- package/src-ext/core/defaultWorkspaceSync.mjs +20 -0
- package/src-ext/core/installationIdentity.mjs +94 -0
- package/src-ext/core/mcpRuntimeFanout.mjs +23 -1
- package/src-ext/core/pairCodeClient.mjs +48 -8
- package/src-ext/core/pairInventory.mjs +31 -6
- package/src-ext/core/relayWorker.mjs +21 -1
- package/src-ext/core/runtimeRegistry.mjs +180 -0
- package/src-ext/core/scanWorkspaces.mjs +163 -23
- package/src-ext/core/unifiedDispatchHandler.mjs +9 -2
- package/src-ext/runtime/_shared/bridgedSessionLedger.mjs +60 -0
- package/src-ext/runtime/_shared/claudeSchemaEvent.mjs +49 -0
- package/src-ext/runtime/_shared/headlessCliBridge.mjs +168 -0
- package/src-ext/runtime/_shared/jsonMcpConfigAdapter.mjs +70 -0
- package/src-ext/runtime/_shared/ndjsonProcess.mjs +141 -0
- package/src-ext/runtime/_shared/resolveWorkspaceCwd.mjs +47 -0
- package/src-ext/runtime/_shared/slashCommandRouter.mjs +10 -0
- package/src-ext/runtime/claude/handleRequest.mjs +15 -37
- package/src-ext/runtime/claude/stdoutParser.mjs +8 -3
- package/src-ext/runtime/codebuddy/index.mjs +41 -0
- package/src-ext/runtime/codex/handleRequest.mjs +13 -35
- package/src-ext/runtime/cursor/index.mjs +46 -0
- package/src-ext/runtime/cursor/mcpConfigAdapter.mjs +15 -0
- package/src-ext/runtime/deepagents/preflight.mjs +57 -0
- package/src-ext/runtime/hermes/envSetup.mjs +22 -8
- package/src-ext/runtime/hermes/gatewayManager.mjs +239 -3
- package/src-ext/runtime/hermes/handleRequest.mjs +13 -0
- package/src-ext/runtime/hermes/httpBackend.mjs +12 -1
- package/src-ext/runtime/hermes/index.mjs +1 -1
- package/src-ext/runtime/hermes/preflight.mjs +2 -1
- package/src-ext/runtime/kimi/index.mjs +100 -0
- package/src-ext/runtime/openclaw/workspaceContext.mjs +58 -0
- package/src-ext/runtime/opencode/index.mjs +48 -0
- package/src-ext/runtime/opencode/mcpConfigAdapter.mjs +15 -0
- package/src-ext/runtime/opencode/preflight.mjs +72 -0
- package/src-ext/runtime/qwen/index.mjs +42 -0
- package/src-ext/service/serviceManager.mjs +120 -42
package/bin/agentlink
CHANGED
|
@@ -62,18 +62,30 @@ import { createLogger as _createBubLogger } from "../src-shared/logger.mjs";
|
|
|
62
62
|
import { scanWorkspacesByRuntime } from "../src-ext/core/scanWorkspaces.mjs";
|
|
63
63
|
import { createWorkspace } from "../src-ext/core/createWorkspace.mjs";
|
|
64
64
|
import { normalizeHostName } from "../src-ext/core/pairCodeClient.mjs";
|
|
65
|
+
import { getOrCreateInstallationId } from "../src-ext/core/installationIdentity.mjs";
|
|
65
66
|
import { buildPairSessionResults } from "../src-ext/core/pairInventory.mjs";
|
|
67
|
+
// runtime 能力分层的**单一真相**(bin 与 src-ext 共用,防两边各持一份名单漂移)。
|
|
68
|
+
// 语义分层的完整说明见该模块头注释 —— 改分层前先读它。
|
|
69
|
+
import {
|
|
70
|
+
PAIRABLE_RUNTIME_KINDS,
|
|
71
|
+
BRIDGE_CAPABLE_RUNTIME_KINDS,
|
|
72
|
+
PAIR_ONLY_RUNTIME_KINDS,
|
|
73
|
+
EXT_AGENT_RUNTIME_KINDS,
|
|
74
|
+
RUNTIME_CAPABILITIES,
|
|
75
|
+
RUNTIME_BINARY_CANDIDATES,
|
|
76
|
+
pairingCapabilitiesForRuntime,
|
|
77
|
+
} from "../src-ext/core/runtimeRegistry.mjs";
|
|
66
78
|
import { SCAN_QR_PNG_OUTPUT_PATH, clearQRImage } from "../src-ext/core/scanPairFlow.mjs";
|
|
67
79
|
import { renderQrToTerminal } from "../src-ext/core/scanQrRenderer.mjs";
|
|
68
80
|
import { buildScanDeeplink, renderScanQrPng } from "../src-ext/core/scanDeeplink.mjs";
|
|
69
81
|
|
|
70
|
-
//
|
|
71
|
-
|
|
82
|
+
// Keep this aligned with Desktop's embedded CLI compatibility floor.
|
|
83
|
+
const MIN_SUPPORTED_NODE_MAJOR = 18;
|
|
72
84
|
{
|
|
73
85
|
const major = Number.parseInt((process.versions?.node ?? "0").split(".")[0], 10);
|
|
74
|
-
if (major <
|
|
86
|
+
if (major < MIN_SUPPORTED_NODE_MAJOR) {
|
|
75
87
|
process.stderr.write(
|
|
76
|
-
`Error: agentlink requires Node.js >=
|
|
88
|
+
`Error: agentlink requires Node.js >= ${MIN_SUPPORTED_NODE_MAJOR}, current: ${process.version}\nPlease upgrade: https://nodejs.org/\n`,
|
|
77
89
|
);
|
|
78
90
|
process.exit(1);
|
|
79
91
|
}
|
|
@@ -104,26 +116,139 @@ const CLI_COMMAND_NAME = "agentlink";
|
|
|
104
116
|
const SERVICE_LABEL = "com.agentlink.bridge";
|
|
105
117
|
const SERVICE_UNIT_NAME = "agentlink-bridge.service";
|
|
106
118
|
const SERVICE_TASK_NAME = "Agentlink Bridge";
|
|
119
|
+
// 非默认状态根(例如 Desktop Debug 的 ~/.agentlink-debug)必须有独立服务名,
|
|
120
|
+
// 否则会覆盖正式 ~/.agentlink 的 launchd/systemd/schtasks 服务。
|
|
121
|
+
const SERVICE_SCOPE_HASH_LENGTH = 12;
|
|
107
122
|
const RUNTIME_KIND_OPENCLAW = "openclaw";
|
|
108
123
|
const RUNTIME_KIND_HERMES = "hermes";
|
|
124
|
+
const RUNTIME_KIND_CLAUDE = "claude";
|
|
125
|
+
const RUNTIME_KIND_CODEX = "codex";
|
|
126
|
+
const RUNTIME_KIND_CURSOR = "cursor";
|
|
127
|
+
// add-deepagents-opencode-runtime-enum: 已知 runtime 枚举扩到 6 个。
|
|
128
|
+
// opencode 自 devagent-m-20260807-cli-runtime-opencode-pairable 起**有了配对实现**
|
|
129
|
+
// (PATH 探测 + 握手 + 凭证落地),但仍**没有** bridge / mcpConfigAdapter;
|
|
130
|
+
// deepagents 至今只入枚举。两者的差别由下面的分层集合承载,不靠人记。
|
|
131
|
+
const RUNTIME_KIND_OPENCODE = "opencode";
|
|
132
|
+
const RUNTIME_KIND_DEEPAGENTS = "deepagents";
|
|
133
|
+
const RUNTIME_KIND_QWEN = "qwen";
|
|
134
|
+
const RUNTIME_KIND_KIMI = "kimi";
|
|
135
|
+
const RUNTIME_KIND_CODEBUDDY = "codebuddy";
|
|
136
|
+
|
|
137
|
+
// ─── runtime 种类登记表(cli-runtime-kind-registry) ─────────────────────────
|
|
138
|
+
// 本仓有 **多种语义不同** 的 runtime 集合。早期它们恰好都等于同一批 4 个字符串,
|
|
139
|
+
// 于是被当成"4 种 runtime"散落成字面量;新增 opencode/deepagents 打破了这个巧合,
|
|
140
|
+
// 所以这里把每一层显式列出来 —— 以后加 runtime 只需在对应集合里加一行,
|
|
141
|
+
// 「这一层要不要带上它」被逼成一次显式选择,不再靠人记得满仓 grep。
|
|
142
|
+
//
|
|
143
|
+
// ① 能力分层(PAIRABLE / BRIDGE_CAPABLE / PAIR_ONLY / EXT_AGENT)已上移到
|
|
144
|
+
// src-ext/core/runtimeRegistry.mjs(见文件顶部 import)—— 因为 agentlink-agent
|
|
145
|
+
// 那侧(src-ext/**)也要用同一套判定,各持一份必然漂移。
|
|
146
|
+
// · PAIRABLE = 有 pair/reset 实现(openclaw/hermes/claude/codex/opencode)
|
|
147
|
+
// · BRIDGE_CAPABLE= 有 bridge/service/MCP 实现(前四个,**不含 opencode**)
|
|
148
|
+
// · PAIR_ONLY = 配得上但跑不起来(= opencode),命令层必须显式拦停
|
|
149
|
+
// · EXT_AGENT = 凭证存 ext-<rt>-last.json(claude/codex/opencode)
|
|
150
|
+
// ② 走 `which <bin>`(win `where`)两阶段 PATH 探测的 runtime。
|
|
151
|
+
// openclaw 不在此列 —— 它以 openclaw.json 是否存在作为"已安装"信号。
|
|
152
|
+
const PATH_PROBED_RUNTIME_KINDS = Object.freeze([
|
|
153
|
+
RUNTIME_KIND_HERMES, RUNTIME_KIND_CLAUDE, RUNTIME_KIND_CODEX,
|
|
154
|
+
RUNTIME_KIND_CURSOR, RUNTIME_KIND_OPENCODE, RUNTIME_KIND_DEEPAGENTS,
|
|
155
|
+
RUNTIME_KIND_QWEN, RUNTIME_KIND_KIMI, RUNTIME_KIND_CODEBUDDY,
|
|
156
|
+
]);
|
|
157
|
+
// ③ 本机探测集 = detectInstalledRuntimes() 的返回顺序,也是 host-inventory
|
|
158
|
+
// 上报(Phase 1 runtimes / Phase 2 results)的口径。
|
|
159
|
+
// DeepAgents 以 `agentlink-deepagents` 独立 sidecar 命令作为 PATH 探测信号。
|
|
160
|
+
const HOST_DETECTED_RUNTIME_KINDS = Object.freeze([
|
|
161
|
+
RUNTIME_KIND_OPENCLAW, ...PATH_PROBED_RUNTIME_KINDS,
|
|
162
|
+
]);
|
|
163
|
+
// ④ 已知 runtime 枚举 = `--runtime` 参数解析层承认的全集(不含 `all` 别名)。
|
|
164
|
+
const KNOWN_RUNTIME_KINDS = Object.freeze([
|
|
165
|
+
...HOST_DETECTED_RUNTIME_KINDS,
|
|
166
|
+
]);
|
|
167
|
+
// ⑤ 已知但连配对都没有的 runtime。必须显式拦停:
|
|
168
|
+
// runPair/runReset/runBridge 都以 normalizeRuntimeKind(runtime, "openclaw")
|
|
169
|
+
// 开头,而它只认 openclaw/hermes、其余一律回落 fallback —— 不拦停就会把
|
|
170
|
+
// 非 openclaw 的配对静默写进 openclaw 的凭证槽位(relayGatewayId/Token),
|
|
171
|
+
// 用户还以为配的是自己选的那个 runtime。
|
|
172
|
+
const ENUM_ONLY_RUNTIME_KINDS = Object.freeze(
|
|
173
|
+
KNOWN_RUNTIME_KINDS.filter((k) => !PAIRABLE_RUNTIME_KINDS.includes(k)),
|
|
174
|
+
);
|
|
109
175
|
// scan-capable runtimes (unify-scan-pairing): openclaw/hermes read their gateway
|
|
110
176
|
// credentials locally (~/.agentlink/config.json via ensureRelayBridgeCredentials)
|
|
111
177
|
// and can host-claim immediately. claude/codex credentials are issued by the
|
|
112
178
|
// server at consume-time — no host-claim path exists for them this cycle (see
|
|
113
179
|
// spec "术语" + Non-goals; S2b covers the explicit unsupported-runtime notice).
|
|
180
|
+
// 🔴 opencode **不得**加进来:它没有本机 host-claim 凭证路径,走 pair 路径。
|
|
114
181
|
const SCAN_CAPABLE_RUNTIME_KINDS = Object.freeze([RUNTIME_KIND_OPENCLAW, RUNTIME_KIND_HERMES]);
|
|
115
182
|
|
|
183
|
+
/**
|
|
184
|
+
* enum-only runtime(已知枚举 ∖ 可配对集,当前 = deepagents)撞上任何实现路径时
|
|
185
|
+
* 显式失败退出,绝不静默回落成 openclaw。
|
|
186
|
+
*
|
|
187
|
+
* 必须在**所有命令分派之前**调用:runPair / runReset / runBridge / runService /
|
|
188
|
+
* runScan 都以 normalizeRuntimeKind(runtime, RUNTIME_KIND_OPENCLAW) 起手,而
|
|
189
|
+
* src/core.mjs 的 normalizeRuntimeKind **只认 openclaw/hermes**、其余一律回落
|
|
190
|
+
* fallback。不拦停就会把 `-r <未实现 runtime>` 静默当成 openclaw 执行(配对结果
|
|
191
|
+
* 写进 openclaw 的 relayGatewayId/Token 槽位),用户看到的却是「配对成功」
|
|
192
|
+
* = 静默错配。
|
|
193
|
+
*
|
|
194
|
+
* ⚠️ 这与「取值非法」是两类失败:取值域错误由 parseArgs 给 `must be one of ...`,
|
|
195
|
+
* 这里给的是「取值合法但本轮没有实现」。两条文案不得混用(测试有断言)。
|
|
196
|
+
*
|
|
197
|
+
* @param {string|undefined} runtime options.runtime 原值(未 normalize)
|
|
198
|
+
*/
|
|
199
|
+
function assertRuntimeOperableOrFail(runtime) {
|
|
200
|
+
if (runtime && ENUM_ONLY_RUNTIME_KINDS.includes(runtime)) {
|
|
201
|
+
fail(t("runtime_known_but_unsupported", {
|
|
202
|
+
runtime,
|
|
203
|
+
cli: CLI_COMMAND_NAME,
|
|
204
|
+
pairable: PAIRABLE_RUNTIME_KINDS.join(" / "),
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* PAIR_ONLY runtime(可配对、但本轮没有 bridge/service 实现,当前 = opencode)
|
|
211
|
+
* 撞上「需要真的把进程跑起来 / 展示运行态」的命令时显式失败退出。
|
|
212
|
+
*
|
|
213
|
+
* 🔴 这是 assertRuntimeOperableOrFail 让出的那半边防护,不是可选的收尾:
|
|
214
|
+
* opencode 进了可配对集之后就不再被上面那道拦停命中,而 runBridge / runService /
|
|
215
|
+
* runReload / runScan / runStatus 依然全都以 normalizeRuntimeKind(x, "openclaw")
|
|
216
|
+
* 起手 —— 少了这道闸,`agentlink status -r opencode` 会把 **openclaw 的**
|
|
217
|
+
* 凭证与服务态当成 opencode 的展示给用户(同一个静默错配家族的另一种形态)。
|
|
218
|
+
*
|
|
219
|
+
* pair / reset 刻意**不**调这个:它们对 opencode 是真实现(转发 agentlink-agent)。
|
|
220
|
+
*
|
|
221
|
+
* @param {string|undefined} runtime options.runtime 原值(未 normalize)
|
|
222
|
+
* @param {string} command 触发的命令名,仅用于文案
|
|
223
|
+
*/
|
|
224
|
+
function assertRuntimeBridgeSupportedOrFail(runtime, command) {
|
|
225
|
+
if (runtime && PAIR_ONLY_RUNTIME_KINDS.includes(runtime)) {
|
|
226
|
+
fail(t("runtime_paired_but_no_bridge", {
|
|
227
|
+
runtime,
|
|
228
|
+
command,
|
|
229
|
+
cli: CLI_COMMAND_NAME,
|
|
230
|
+
bridgeable: BRIDGE_CAPABLE_RUNTIME_KINDS.join(" / "),
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
116
235
|
function serviceNames(runtimeKind) {
|
|
117
236
|
const kind = normalizeRuntimeKind(runtimeKind, RUNTIME_KIND_OPENCLAW);
|
|
237
|
+
const scope = serviceScopeSuffix();
|
|
118
238
|
if (kind === RUNTIME_KIND_OPENCLAW) {
|
|
119
|
-
return {
|
|
239
|
+
return {
|
|
240
|
+
label: `${SERVICE_LABEL}${scope}`,
|
|
241
|
+
unit: scope ? `agentlink-bridge${scope}.service` : SERVICE_UNIT_NAME,
|
|
242
|
+
task: `${SERVICE_TASK_NAME}${scope}`,
|
|
243
|
+
logPrefix: `bridge${scope}`,
|
|
244
|
+
};
|
|
120
245
|
}
|
|
121
246
|
const capitalized = kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
122
247
|
return {
|
|
123
|
-
label: `${SERVICE_LABEL}-${kind}`,
|
|
124
|
-
unit: `agentlink-bridge-${kind}.service`,
|
|
125
|
-
task: `${SERVICE_TASK_NAME} ${capitalized}`,
|
|
126
|
-
logPrefix: `bridge-${kind}`,
|
|
248
|
+
label: `${SERVICE_LABEL}-${kind}${scope}`,
|
|
249
|
+
unit: `agentlink-bridge-${kind}${scope}.service`,
|
|
250
|
+
task: `${SERVICE_TASK_NAME} ${capitalized}${scope}`,
|
|
251
|
+
logPrefix: `bridge-${kind}${scope}`,
|
|
127
252
|
};
|
|
128
253
|
}
|
|
129
254
|
// Mirror the WS bridge's operator scopes for local HTTP proxy calls as well.
|
|
@@ -367,7 +492,21 @@ const CLI_I18N = {
|
|
|
367
492
|
pair_all_detecting_runtimes: "\n检测本机已安装的 AI runtime ...",
|
|
368
493
|
pair_all_runtime_installed_extra: "({detail})",
|
|
369
494
|
pair_all_runtime_not_installed: " · {kind} 未安装,跳过{hint}",
|
|
370
|
-
|
|
495
|
+
// {kinds} 由 HOST_DETECTED_RUNTIME_KINDS 渲染 —— 文案本身不再持有 runtime 名,
|
|
496
|
+
// 加 runtime 时中英两侧自动跟随,从结构上消灭「两份枚举串漂移」。
|
|
497
|
+
pair_all_no_runtimes: "本机未检测到任何可用 runtime({kinds})。",
|
|
498
|
+
// 已知 runtime 枚举里、但本轮还没有 pair/bridge 实现的 runtime。
|
|
499
|
+
runtime_known_but_unsupported:
|
|
500
|
+
"runtime `{runtime}` 已被识别,但本版 {cli} 还没有它的配对/桥接实现,暂不支持该命令。\n"
|
|
501
|
+
+ "当前可操作的 runtime:{pairable}。",
|
|
502
|
+
// 可配对、但本轮没有 bridge/service 实现的 runtime(PAIR_ONLY)。
|
|
503
|
+
// 与上一条是**两类**失败:这条的配对是通的,只是跑不起来。
|
|
504
|
+
runtime_paired_but_no_bridge:
|
|
505
|
+
"runtime `{runtime}` 本版 {cli} 只支持配对(pair / reset),还没有 bridge 实现,`{command}` 暂不可用。\n"
|
|
506
|
+
+ "支持 bridge/service 的 runtime:{bridgeable}。",
|
|
507
|
+
pair_all_runtime_pair_unsupported: " · {kind} 已安装但本版暂无配对实现,跳过配对(已如实上报本机清单)",
|
|
508
|
+
pair_all_summary_pair_unsupported: "ℹ️ 已装但暂无配对实现 ({count}): {kinds}",
|
|
509
|
+
pair_all_summary_nothing_pairable: "\n本机没有可配对的 runtime;已把探测结果如实上报给 App。",
|
|
371
510
|
pair_all_host_inventory_report_failed: " ⚠️ host-inventory 上报失败 (HTTP {status}),继续配对 ...",
|
|
372
511
|
pair_all_host_inventory_report_error: " ⚠️ host-inventory 上报异常 ({error}),继续配对 ...",
|
|
373
512
|
pair_all_host_inventory_prepare_failed: " ⚠️ host-inventory 准备失败 ({error})",
|
|
@@ -395,6 +534,8 @@ const CLI_I18N = {
|
|
|
395
534
|
reset_all_detecting_runtimes: "\n检测本机已安装的 AI runtime(逐个解绑)...",
|
|
396
535
|
reset_all_no_runtimes: "本机未检测到任何已安装 runtime,也无可解绑的本地残留。",
|
|
397
536
|
reset_all_runtime_state_only: " ⚠ {kind} 未安装,但检测到本地绑定残留,将清理",
|
|
537
|
+
// 已装、但本轮没有配对实现 → 不可能存在可解绑的绑定,不进解绑目标集。
|
|
538
|
+
reset_all_runtime_no_binding: " · {kind} 已安装但本版暂无配对实现,无可解绑绑定,跳过",
|
|
398
539
|
reset_all_resetting_runtime: "\n→ 正在解绑 {kind} ...",
|
|
399
540
|
reset_all_runtime_reset_failed: " ⚠️ {kind} 解绑失败 (退出码 {status}),继续下一个 ...",
|
|
400
541
|
reset_all_summary_header: "\n──────────────── 解绑结果 ────────────────",
|
|
@@ -612,7 +753,21 @@ const CLI_I18N = {
|
|
|
612
753
|
pair_all_detecting_runtimes: "\nDetecting installed AI runtimes on this host ...",
|
|
613
754
|
pair_all_runtime_installed_extra: " ({detail})",
|
|
614
755
|
pair_all_runtime_not_installed: " · {kind} not installed, skipping{hint}",
|
|
615
|
-
|
|
756
|
+
// {kinds} rendered from HOST_DETECTED_RUNTIME_KINDS — see the zh table note:
|
|
757
|
+
// the text no longer holds runtime names, so both locales can never drift.
|
|
758
|
+
pair_all_no_runtimes: "No usable runtime detected on this host ({kinds}).",
|
|
759
|
+
// Pairable but no bridge/service implementation yet (PAIR_ONLY tier).
|
|
760
|
+
// A *different* failure class from runtime_known_but_unsupported below:
|
|
761
|
+
// pairing does work here, the runtime just cannot be run yet.
|
|
762
|
+
runtime_paired_but_no_bridge:
|
|
763
|
+
"runtime `{runtime}` only supports pairing (pair / reset) in this {cli} build; there is no bridge implementation yet, so `{command}` is unavailable.\n"
|
|
764
|
+
+ "Runtimes with bridge/service support: {bridgeable}.",
|
|
765
|
+
runtime_known_but_unsupported:
|
|
766
|
+
"runtime `{runtime}` is recognized, but this {cli} build has no pairing/bridge implementation for it yet.\n"
|
|
767
|
+
+ "Currently operable runtimes: {pairable}.",
|
|
768
|
+
pair_all_runtime_pair_unsupported: " · {kind} installed but no pairing implementation in this build, skipping pairing (still reported in the host inventory)",
|
|
769
|
+
pair_all_summary_pair_unsupported: "ℹ️ Installed, pairing not implemented yet ({count}): {kinds}",
|
|
770
|
+
pair_all_summary_nothing_pairable: "\nNo pairable runtime on this host; the detection result was reported to the App as-is.",
|
|
616
771
|
pair_all_host_inventory_report_failed: " ⚠️ host-inventory report failed (HTTP {status}), continuing pairing ...",
|
|
617
772
|
pair_all_host_inventory_report_error: " ⚠️ host-inventory report error ({error}), continuing pairing ...",
|
|
618
773
|
pair_all_host_inventory_prepare_failed: " ⚠️ host-inventory prepare failed ({error})",
|
|
@@ -640,6 +795,7 @@ const CLI_I18N = {
|
|
|
640
795
|
reset_all_detecting_runtimes: "\nDetecting installed AI runtimes on this host (unbind each) ...",
|
|
641
796
|
reset_all_no_runtimes: "No installed runtime and no local binding trace on this host; nothing to unbind.",
|
|
642
797
|
reset_all_runtime_state_only: " ⚠ {kind} not installed, but a local binding trace was found — will clean it up",
|
|
798
|
+
reset_all_runtime_no_binding: " · {kind} installed but pairing is not implemented in this build; no binding to unbind, skipping",
|
|
643
799
|
reset_all_resetting_runtime: "\n→ Resetting {kind} ...",
|
|
644
800
|
reset_all_runtime_reset_failed: " ⚠️ {kind} reset failed (exit {status}), continuing ...",
|
|
645
801
|
reset_all_summary_header: "\n──────────────── Reset Results ────────────────",
|
|
@@ -781,12 +937,15 @@ function printHelp() {
|
|
|
781
937
|
"",
|
|
782
938
|
t("help_usage"),
|
|
783
939
|
` ${CLI_COMMAND_NAME} setup [--relay <url>] [--public-base <url>] [--mode <relay>] [--account <id>] [--restart|--no-restart] [--strict-plugin] [--json]`,
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
` ${CLI_COMMAND_NAME}
|
|
940
|
+
// pair / reset 的取值域 = 可配对集(含 opencode),由登记表渲染,
|
|
941
|
+
// 不再手抄第二份枚举串(加 runtime 时用法行自动跟随)。
|
|
942
|
+
` ${CLI_COMMAND_NAME} pair <4-digit|6-digit|8-alnum> [-r|--runtime <${PAIRABLE_RUNTIME_KINDS.join("|")}|all>] [--mode <relay>] [--strict-plugin] # default: all`,
|
|
943
|
+
` ${CLI_COMMAND_NAME} reset [-r|--runtime <${PAIRABLE_RUNTIME_KINDS.join("|")}|all>] [--relay <url>] [--gateway <url>] [--json] # default: all`,
|
|
944
|
+
// bridge / reload 的取值域 = 可桥接集(**不含** opencode:本轮它只能配对)。
|
|
945
|
+
` ${CLI_COMMAND_NAME} bridge [--relay <url>] [--gateway <url>] [-r|--runtime <${BRIDGE_CAPABLE_RUNTIME_KINDS.join("|")}>] [--json]`,
|
|
787
946
|
` ${CLI_COMMAND_NAME} scan [-r|--runtime <openclaw|hermes>] [--relay <url>] [--json] [--no-serve] [--qr-path <path>] [--strict-plugin] # 扫码配对:出二维码 → App 扫码 → 本机 scan-capable runtime(openclaw/hermes)收发;claude/codex 不支持,请改用 pair`,
|
|
788
947
|
` ${CLI_COMMAND_NAME} service <status|install|stop|uninstall|restart> [--relay <url>] [--gateway <url>] [--runtime <openclaw|hermes>] [--json]`,
|
|
789
|
-
` ${CLI_COMMAND_NAME} reload [-r|--runtime
|
|
948
|
+
` ${CLI_COMMAND_NAME} reload [-r|--runtime <${BRIDGE_CAPABLE_RUNTIME_KINDS.join("|")}>] [--json]`,
|
|
790
949
|
` ${CLI_COMMAND_NAME} pair-url [<4-digit|6-digit|8-alnum>] [--account <id>] [--code <4-digit|6-digit|8-alnum>] [--json]`,
|
|
791
950
|
` ${CLI_COMMAND_NAME} tunnel <on|off> <port> [--relay <url>] [--ws|--tcp] [--json]`,
|
|
792
951
|
` ${CLI_COMMAND_NAME} tunnel ls [--json]`,
|
|
@@ -851,6 +1010,9 @@ function parseArgs(argv) {
|
|
|
851
1010
|
mcpRuntime: undefined,
|
|
852
1011
|
noServe: false,
|
|
853
1012
|
qrPath: undefined,
|
|
1013
|
+
// Debug Desktop passes an isolated state root here. Keep it internal so normal
|
|
1014
|
+
// users do not accidentally split their existing runtime pairing state.
|
|
1015
|
+
agentlinkHome: undefined,
|
|
854
1016
|
// Internal plumbing flag — not advertised in --help. Set by `runScan`'s
|
|
855
1017
|
// per-runtime host-claim child spawn (`--pair-mode scan`), never expected
|
|
856
1018
|
// from a human-typed `agentlink pair` invocation. See runPair()'s
|
|
@@ -937,6 +1099,14 @@ function parseArgs(argv) {
|
|
|
937
1099
|
continue;
|
|
938
1100
|
}
|
|
939
1101
|
|
|
1102
|
+
if (token === "--agentlink-home") {
|
|
1103
|
+
const value = args.shift();
|
|
1104
|
+
if (!value || value.startsWith("--")) fail(t("err_missing_value", { flag: "--agentlink-home" }));
|
|
1105
|
+
if (!path.isAbsolute(value.trim())) fail("--agentlink-home must be an absolute path");
|
|
1106
|
+
options.agentlinkHome = value.trim();
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
940
1110
|
if (token === "--relay") {
|
|
941
1111
|
const value = args.shift();
|
|
942
1112
|
if (!value || value.startsWith("--")) fail(t("err_missing_value", { flag: "--relay" }));
|
|
@@ -991,18 +1161,25 @@ function parseArgs(argv) {
|
|
|
991
1161
|
const value = args.shift();
|
|
992
1162
|
if (!value || value.startsWith("--")) fail(`missing value for ${token}`);
|
|
993
1163
|
const raw = value.trim().toLowerCase();
|
|
994
|
-
//
|
|
995
|
-
//
|
|
996
|
-
//
|
|
997
|
-
|
|
1164
|
+
// 解析层只做「取值在不在已知枚举里」这一件事(KNOWN_RUNTIME_KINDS,见常量区):
|
|
1165
|
+
// · openclaw / hermes → bin 内 native 路径
|
|
1166
|
+
// · claude / codex / opencode → main() 转发 agentlink-agent(shim)。
|
|
1167
|
+
// opencode 自 opencode-pairable 起进了可配对集:pair / reset / uninstall
|
|
1168
|
+
// 是真实现,assertRuntimeOperableOrFail() 对它**不再触发**;
|
|
1169
|
+
// bridge / service / reload / scan / status 另由
|
|
1170
|
+
// assertRuntimeBridgeSupportedOrFail() 显式拦停(两道闸分工见其文档注释)
|
|
1171
|
+
// · deepagents → 本轮只有枚举,真执行由 main() 的
|
|
1172
|
+
// assertRuntimeOperableOrFail() 给显式「暂不支持」错误
|
|
1173
|
+
// · all → 仅 pair / reset 支持,逐 runtime 串行
|
|
1174
|
+
// 这里刻意 **不** 再走 normalizeRuntimeKind —— 它只认 openclaw/hermes、其余
|
|
1175
|
+
// 静默回落 fallback,会把新 runtime 悄悄变成 openclaw(静默错配)。
|
|
1176
|
+
// raw 已 trim + toLowerCase,与 normalizeRuntimeKind 的归一化口径一致。
|
|
1177
|
+
if (raw === "all" || KNOWN_RUNTIME_KINDS.includes(raw)) {
|
|
998
1178
|
options.runtime = raw;
|
|
999
1179
|
continue;
|
|
1000
1180
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
fail(`${token} must be openclaw / hermes / claude / codex / all`);
|
|
1004
|
-
}
|
|
1005
|
-
options.runtime = normalizedRuntime;
|
|
1181
|
+
// 取值域文案由常量渲染 —— 加 runtime 时无需再改这行(防两处真相漂移)。
|
|
1182
|
+
fail(`${token} must be one of ${KNOWN_RUNTIME_KINDS.join(" / ")} / all`);
|
|
1006
1183
|
continue;
|
|
1007
1184
|
}
|
|
1008
1185
|
if (token === "--hermes-python") {
|
|
@@ -1216,8 +1393,8 @@ function resolveAgentlinkStatePath() {
|
|
|
1216
1393
|
// reset 用:某 runtime 是否有「可解绑的本地痕迹」(已配对的凭证 / state 文件)。
|
|
1217
1394
|
// 与「二进制是否安装」解耦 —— 卸了 CLI 但 binding 还在的 runtime 仍须能被 reset
|
|
1218
1395
|
// 清掉(否则 server 端绑定 + 本地 state 永久泄漏)。读 reset 实际操作的同一批文件:
|
|
1219
|
-
// openclaw / hermes
|
|
1220
|
-
// claude / codex
|
|
1396
|
+
// openclaw / hermes → ~/.agentlink/config.json(relayGatewayId / hermesRelayGatewayId)
|
|
1397
|
+
// claude / codex / opencode → ~/.agentlink/ext-<rt>-last.json(gw_id)
|
|
1221
1398
|
function runtimeHasLocalState(kind) {
|
|
1222
1399
|
// 1) 残留的 bridge service 也算「有痕迹」—— 凭证 id 已被上一次部分 reset 清掉、
|
|
1223
1400
|
// 但 service 还在的孤儿,必须仍纳入 reset 清理,否则后台服务空转、卸不掉。
|
|
@@ -1225,9 +1402,19 @@ function runtimeHasLocalState(kind) {
|
|
|
1225
1402
|
// linux ~/.config/systemd/user),**不**走 `launchctl print`/`systemctl --user`
|
|
1226
1403
|
// 那种用户级全局探测 —— 否则 reset 的扇出决策会依赖跨 HOME 的全局服务状态,
|
|
1227
1404
|
// 破坏隔离、并可能误删别的上下文的服务。(win 用 schtasks 无文件,跳过该信号。)
|
|
1405
|
+
//
|
|
1406
|
+
// 🔴 只对 **native 服务** (openclaw/hermes)做这个探测:resolveMacLaunchAgentPath /
|
|
1407
|
+
// resolveLinuxUserUnitPath 都经 serviceNames() → normalizeRuntimeKind(x,"openclaw"),
|
|
1408
|
+
// 而后者只认 openclaw/hermes —— 对 ext-agent runtime(claude/codex/opencode)会
|
|
1409
|
+
// **别名到 openclaw 自己的 plist/unit**,于是"装了 openclaw 的 bridge 服务"会被
|
|
1410
|
+
// 误判成"claude/opencode 有本地痕迹"。ext-agent 的服务单元命名另在
|
|
1411
|
+
// src-ext/service/serviceManager.mjs(com.agentlink.agent.<rt>),不由这里定位;
|
|
1412
|
+
// 它们的痕迹判定完全走下面的 ext-<rt>-last.json 凭证文件,够用且不会串台。
|
|
1228
1413
|
try {
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1414
|
+
if (!EXT_AGENT_RUNTIME_KINDS.includes(kind)) {
|
|
1415
|
+
if (process.platform === "darwin" && fs.existsSync(resolveMacLaunchAgentPath(kind))) return true;
|
|
1416
|
+
if (process.platform === "linux" && fs.existsSync(resolveLinuxUserUnitPath(kind))) return true;
|
|
1417
|
+
}
|
|
1231
1418
|
} catch { /* 文件探测失败不影响下面的凭证判定 */ }
|
|
1232
1419
|
try {
|
|
1233
1420
|
if (kind === RUNTIME_KIND_OPENCLAW || kind === RUNTIME_KIND_HERMES) {
|
|
@@ -1236,7 +1423,7 @@ function runtimeHasLocalState(kind) {
|
|
|
1236
1423
|
const idKey = kind === RUNTIME_KIND_OPENCLAW ? "relayGatewayId" : "hermesRelayGatewayId";
|
|
1237
1424
|
return Boolean(String(cfg[idKey] ?? "").trim());
|
|
1238
1425
|
}
|
|
1239
|
-
if (kind
|
|
1426
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(kind)) {
|
|
1240
1427
|
// ext state(ext-<rt>-last.json)由 pairCodeClient.stateDir() 定位,现已统一
|
|
1241
1428
|
// 优先 AGENTLINK_HOME(见该文件注释),与此处 resolveAgentlinkHome() 一致 ——
|
|
1242
1429
|
// 探测与实际清理读写同一份文件,无 AGENTLINK_HOME 分叉。
|
|
@@ -1249,7 +1436,15 @@ function runtimeHasLocalState(kind) {
|
|
|
1249
1436
|
}
|
|
1250
1437
|
|
|
1251
1438
|
function resolveMatecliRuntimeDir() {
|
|
1252
|
-
return
|
|
1439
|
+
return resolveAgentlinkHome();
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function serviceScopeSuffix() {
|
|
1443
|
+
const currentHome = path.resolve(resolveAgentlinkHome());
|
|
1444
|
+
const defaultHome = path.resolve(path.join(os.homedir(), ".agentlink"));
|
|
1445
|
+
if (currentHome === defaultHome) return "";
|
|
1446
|
+
const digest = createHash("sha256").update(currentHome).digest("hex");
|
|
1447
|
+
return `-${digest.slice(0, SERVICE_SCOPE_HASH_LENGTH)}`;
|
|
1253
1448
|
}
|
|
1254
1449
|
|
|
1255
1450
|
function resolveBridgeLogsDir() {
|
|
@@ -2690,6 +2885,34 @@ function ensureRelayBridgeCredentials({
|
|
|
2690
2885
|
}
|
|
2691
2886
|
}
|
|
2692
2887
|
|
|
2888
|
+
// 🔴 把 relay 地址一起落盘。缺了它,openclaw / hermes 的常驻服务重启后**静默连到
|
|
2889
|
+
// 生产** —— 这不是理论问题,2026-08-13 在真机上坐实过:
|
|
2890
|
+
//
|
|
2891
|
+
// · 桌面端配对时确实传了 `--relay <测试环境>`(pairing.rs pair_args)
|
|
2892
|
+
// · 但 launchd plist 的 ProgramArguments 只有 [node, agentlink-agent, <rt>],
|
|
2893
|
+
// 没有 --relay(plutil 解析确认)
|
|
2894
|
+
// · config.json 里只有 gateway 凭据、**没有任何 URL**
|
|
2895
|
+
// → pairCodeClient.readPairStateFromConfig 读 parsed.relayUrl 读到 undefined
|
|
2896
|
+
// → resolveRelayUrl 回落到 DEFAULT_RELAY_URL = https://go-relay.xyagent.com(生产)
|
|
2897
|
+
// → `agentlink doctor` 实测:relay.reachable = https://go-relay.xyagent.com
|
|
2898
|
+
//
|
|
2899
|
+
// 后果对测试用户最恶劣:配对当时一切正常,**重启电脑后全部悄悄连到生产**,表现是
|
|
2900
|
+
// 「配对好了但会话和消息都不见了」——极难排查。
|
|
2901
|
+
//
|
|
2902
|
+
// 读侧(readPairStateFromConfig)早就在读 `parsed.relayUrl` 了,只有写侧一直没写;
|
|
2903
|
+
// 本函数连 relayUrl 参数都收了、没有它还直接早退,却从不落盘。属纯遗漏。
|
|
2904
|
+
//
|
|
2905
|
+
// 用单个 `relayUrl` 而非 runtime 分槽(hermesRelayUrl/relayUrl):读侧对 hermes 与
|
|
2906
|
+
// openclaw 读的是**同一个 key**,写成两个反而对不上。四端本来同属一台机器、同一个
|
|
2907
|
+
// 桌面端,relay 地址天然相同;真出现要分开的需求时再连读侧一起改。
|
|
2908
|
+
const normalizedRelayUrl = String(relayUrl).trim();
|
|
2909
|
+
if (normalizedRelayUrl && section.relayUrl !== normalizedRelayUrl) {
|
|
2910
|
+
section.relayUrl = normalizedRelayUrl;
|
|
2911
|
+
// 记进 changes 才会触发下面的落盘 —— 只有 URL 变化(凭据都已存在)时,
|
|
2912
|
+
// changes 为空就会静默不写,这一步等于没做。
|
|
2913
|
+
changes.push(`state.relayUrl=${normalizedRelayUrl}`);
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2693
2916
|
if (changes.length > 0) {
|
|
2694
2917
|
writeJsonFile(resolveAgentlinkStatePath(), section);
|
|
2695
2918
|
}
|
|
@@ -3438,6 +3661,21 @@ function writeBridgeLaunchScript(installConfig) {
|
|
|
3438
3661
|
const appData =
|
|
3439
3662
|
(process.env.APPDATA ?? "").trim() ||
|
|
3440
3663
|
path.join(capturedHome, "AppData", "Roaming");
|
|
3664
|
+
// ScheduledTask 的 PATH 是注册表值,看不到安装时进程里的增量(尤其是桌面端
|
|
3665
|
+
// 私有 Node 的目录)—— 与 POSIX 版 `export PATH=<defaultPath>:$PATH` 同口径,
|
|
3666
|
+
// 把 node/openclaw 兄弟目录 + 常见安装位快照进脚本,`%` 转义防意外展开。
|
|
3667
|
+
// 兄弟目录解析显式用 win32 语义、分隔符显式 `;`(生产 win 上与默认等价;
|
|
3668
|
+
// 保证非 win 环境下生成的脚本形态也正确,便于测试与排查)。
|
|
3669
|
+
const defaultPath = buildPathString(
|
|
3670
|
+
[
|
|
3671
|
+
...resolveSiblingBinDirs(installConfig.nodePath, path.win32),
|
|
3672
|
+
...resolveSiblingBinDirs(installConfig.openclawPath, path.win32),
|
|
3673
|
+
path.join(capturedHome, ".volta", "bin"),
|
|
3674
|
+
path.join(appData, "npm"),
|
|
3675
|
+
"C:\\Program Files\\nodejs",
|
|
3676
|
+
],
|
|
3677
|
+
";",
|
|
3678
|
+
);
|
|
3441
3679
|
const script = [
|
|
3442
3680
|
"@echo off",
|
|
3443
3681
|
`set "HOME=${capturedHome}"`,
|
|
@@ -3445,6 +3683,7 @@ function writeBridgeLaunchScript(installConfig) {
|
|
|
3445
3683
|
`set "APPDATA=${appData}"`,
|
|
3446
3684
|
`set "OPENCLAW_HOME=${capturedOpenClawHome}"`,
|
|
3447
3685
|
`set "AGENTLINK_HOME=${capturedAgentlinkHome}"`,
|
|
3686
|
+
`set "PATH=${defaultPath.replaceAll("%", "%%")};%PATH%"`,
|
|
3448
3687
|
`"${installConfig.nodePath}" ${nodeArg}`,
|
|
3449
3688
|
"",
|
|
3450
3689
|
].join("\r\n");
|
|
@@ -4252,6 +4491,7 @@ async function publishPairToRelay({
|
|
|
4252
4491
|
ttlSeconds = 600,
|
|
4253
4492
|
}) {
|
|
4254
4493
|
const url = `${relayUrl}/v1/pair-sessions`;
|
|
4494
|
+
const normalizedRuntime = normalizeRuntimeKind(runtimeKind, RUNTIME_KIND_OPENCLAW);
|
|
4255
4495
|
const headers = {
|
|
4256
4496
|
"content-type": "application/json",
|
|
4257
4497
|
...relayAuthHeaders(),
|
|
@@ -4261,7 +4501,7 @@ async function publishPairToRelay({
|
|
|
4261
4501
|
headers,
|
|
4262
4502
|
body: JSON.stringify({
|
|
4263
4503
|
code,
|
|
4264
|
-
runtimeKind:
|
|
4504
|
+
runtimeKind: normalizedRuntime,
|
|
4265
4505
|
transportMode: normalizeChatTransportMode(transportMode, "relay"),
|
|
4266
4506
|
...(clientUserId ? { clientUserId } : {}),
|
|
4267
4507
|
bindUrl,
|
|
@@ -4274,6 +4514,10 @@ async function publishPairToRelay({
|
|
|
4274
4514
|
ttlSeconds,
|
|
4275
4515
|
source: "agentlink-cli",
|
|
4276
4516
|
hostName: normalizeHostName(os.hostname()),
|
|
4517
|
+
// /v1/pair-sessions 是 OpenClaw/Hermes 的旧配对入口;必须同步 v2
|
|
4518
|
+
// 工作区身份,避免 relay 把已配对 gateway 当作 legacy bridge。
|
|
4519
|
+
installation_id: getOrCreateInstallationId(),
|
|
4520
|
+
capabilities: pairingCapabilitiesForRuntime(normalizedRuntime),
|
|
4277
4521
|
...(pairMode ? { pairMode } : {}),
|
|
4278
4522
|
}),
|
|
4279
4523
|
});
|
|
@@ -5496,11 +5740,12 @@ async function handleUnifiedRunDispatch({
|
|
|
5496
5740
|
gatewayAuthToken,
|
|
5497
5741
|
bridgeToken,
|
|
5498
5742
|
defaultModel,
|
|
5743
|
+
openclawConfig,
|
|
5499
5744
|
request,
|
|
5500
5745
|
json: jsonMode,
|
|
5501
5746
|
}) {
|
|
5502
5747
|
const inner = request?.request ?? {};
|
|
5503
|
-
const { thread_id, run_id, input, blocks: rawBlocks = [] } = inner;
|
|
5748
|
+
const { thread_id, run_id, input, blocks: rawBlocks = [], workspace_path } = inner;
|
|
5504
5749
|
|
|
5505
5750
|
if (!thread_id || !run_id) {
|
|
5506
5751
|
if (!jsonMode) {
|
|
@@ -5607,6 +5852,28 @@ async function handleUnifiedRunDispatch({
|
|
|
5607
5852
|
}
|
|
5608
5853
|
}
|
|
5609
5854
|
|
|
5855
|
+
// OpenClaw's strict /v1/responses schema has no request-level cwd. Select
|
|
5856
|
+
// the workspace through agent_id and verify that agent's configured
|
|
5857
|
+
// workspace before dispatch; a mismatch must never fall back to main.
|
|
5858
|
+
const agentId = String(inner.agent_id || request.agentId || "main").trim() || "main";
|
|
5859
|
+
if (workspace_path) {
|
|
5860
|
+
try {
|
|
5861
|
+
const contextUrl = new URL("../src-ext/runtime/openclaw/workspaceContext.mjs", import.meta.url).href;
|
|
5862
|
+
const { assertOpenClawWorkspaceContext } = await import(contextUrl);
|
|
5863
|
+
assertOpenClawWorkspaceContext({ config: openclawConfig, agentId, workspacePath: workspace_path });
|
|
5864
|
+
} catch (workspaceError) {
|
|
5865
|
+
try {
|
|
5866
|
+
await postEnvelopes([_buildRunFailed(
|
|
5867
|
+
run_id,
|
|
5868
|
+
"workspace_context_unavailable",
|
|
5869
|
+
String(workspaceError?.message || "openclaw workspace context is unavailable"),
|
|
5870
|
+
)]);
|
|
5871
|
+
} catch { /* best-effort */ }
|
|
5872
|
+
cleanupActiveRunContext(thread_id, run_id);
|
|
5873
|
+
return;
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5876
|
+
|
|
5610
5877
|
// bridge-auto-tunnel: chat-side `/tunnel ls` / `/tunnel off <port>` 拦截。
|
|
5611
5878
|
// 命中 → 直接 reply bridge-origin text block + run.completed,跳过本地
|
|
5612
5879
|
// OpenClaw 网关派发。失败 / 未命中 → 兜底 pass-through 继续走 runtime。
|
|
@@ -5700,7 +5967,6 @@ async function handleUnifiedRunDispatch({
|
|
|
5700
5967
|
// unify-scan-pairing 全量移除了上一代扫码硬件的命名空间归一 hack:扫码路径
|
|
5701
5968
|
// 现在与 pair code 共用同一套真实 runtime binding(openclaw/hermes),不再有
|
|
5702
5969
|
// App 以合成命名空间 agent 绑定的场景,agentId 直接原样使用。
|
|
5703
|
-
const agentId = String(inner.agent_id || request.agentId || "main").trim() || "main";
|
|
5704
5970
|
const headers = {
|
|
5705
5971
|
"content-type": "application/json",
|
|
5706
5972
|
"accept": "text/event-stream",
|
|
@@ -6396,6 +6662,7 @@ async function runRelayBridge({
|
|
|
6396
6662
|
gatewayAuthToken,
|
|
6397
6663
|
bridgeToken: gatewayToken, // server lookupBridgeToken accepts relay_gateway_token
|
|
6398
6664
|
defaultModel: bridgeMeta?.openclawConfig?.agents?.defaults?.model?.primary || null,
|
|
6665
|
+
openclawConfig: bridgeMeta?.openclawConfig || null,
|
|
6399
6666
|
request,
|
|
6400
6667
|
json,
|
|
6401
6668
|
});
|
|
@@ -6481,7 +6748,10 @@ function formatServiceStatusOutput(result) {
|
|
|
6481
6748
|
.join("\n");
|
|
6482
6749
|
}
|
|
6483
6750
|
|
|
6484
|
-
// 探测本机已安装的
|
|
6751
|
+
// 探测本机已安装的 runtime。集合与返回顺序 = HOST_DETECTED_RUNTIME_KINDS
|
|
6752
|
+
// (openclaw / hermes / claude / codex / opencode)。
|
|
6753
|
+
// 🔴 deepagents 不在这里:它只入 runtime 枚举,本机没有任何探测手段 —— 没探测过
|
|
6754
|
+
// 就不能出现在探测表/host-inventory 里谎报 not_installed。
|
|
6485
6755
|
// 设计:每项探测要快(≤3s),失败时给出可读 hint。
|
|
6486
6756
|
// opts.probeVersion=false:只判 installed(which / 文件存在),跳过 `--version`
|
|
6487
6757
|
// 子进程探测 —— reset 不需要版本号,省去 hermes 最长 60s 的 ls-remote 宽限延迟。
|
|
@@ -6518,7 +6788,11 @@ function detectInstalledRuntimes({ probeVersion = true } = {}) {
|
|
|
6518
6788
|
});
|
|
6519
6789
|
}
|
|
6520
6790
|
|
|
6521
|
-
// hermes / claude / codex:检测 PATH 中的 CLI 二进制。
|
|
6791
|
+
// hermes / claude / codex / opencode:检测 PATH 中的 CLI 二进制。
|
|
6792
|
+
// opencode(add-deepagents-opencode-runtime-enum)复用**完全同一套** installed
|
|
6793
|
+
// 判定,不另造探测逻辑;自 opencode-pairable 起它同时进入配对扇出目标
|
|
6794
|
+
// (单 runtime 的 `pair -r opencode` 另有 src-ext/runtime/opencode/preflight.mjs
|
|
6795
|
+
// 在消费配对码前再确认一次二进制可用,同构自 codex 的 preflight)。
|
|
6522
6796
|
//
|
|
6523
6797
|
// hermes = Nous Research `hermes-agent`(https://github.com/nousresearch/hermes-agent),
|
|
6524
6798
|
// 装好后会在 PATH 暴露 `hermes` 命令,`hermes --version` 首行形如
|
|
@@ -6536,27 +6810,37 @@ function detectInstalledRuntimes({ probeVersion = true } = {}) {
|
|
|
6536
6810
|
// (banner.py:140 timeout=10s),网络慢时整条 spawnSync 会 ETIMEDOUT 被
|
|
6537
6811
|
// 上层误判"未安装";现在不依赖它。
|
|
6538
6812
|
// ② 拿到 binary 后再跑 `--version` 取版本字符串,超时不影响 installed 判断;
|
|
6539
|
-
// hermes 给 60s 宽限(GitHub ls-remote + git fetch 可能慢),
|
|
6540
|
-
// 是 native 二进制保留 10s。
|
|
6541
|
-
for (const
|
|
6813
|
+
// hermes 给 60s 宽限(GitHub ls-remote + git fetch 可能慢),
|
|
6814
|
+
// claude/codex/opencode 是 native 二进制保留 10s。
|
|
6815
|
+
for (const kind of PATH_PROBED_RUNTIME_KINDS) {
|
|
6542
6816
|
const whichCmd = process.platform === "win32" ? "where" : "which";
|
|
6543
|
-
const
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6817
|
+
const candidates = RUNTIME_BINARY_CANDIDATES[kind] ?? [kind];
|
|
6818
|
+
let bin = candidates[0];
|
|
6819
|
+
let which = null;
|
|
6820
|
+
let binPath = "";
|
|
6821
|
+
for (const candidate of candidates) {
|
|
6822
|
+
const probeWhich = spawnSync(whichCmd, [candidate], {
|
|
6823
|
+
encoding: "utf8", timeout: 2000, windowsHide: true,
|
|
6824
|
+
});
|
|
6825
|
+
if (!probeWhich.error && probeWhich.status === 0) {
|
|
6826
|
+
bin = candidate;
|
|
6827
|
+
which = probeWhich;
|
|
6828
|
+
binPath = String(probeWhich.stdout ?? "").trim().split(/\r?\n/)[0];
|
|
6829
|
+
break;
|
|
6830
|
+
}
|
|
6831
|
+
which = probeWhich;
|
|
6832
|
+
}
|
|
6549
6833
|
if (!binPath) {
|
|
6550
6834
|
const code = which.error?.code || "";
|
|
6551
6835
|
const hint = code && code !== "ENOENT"
|
|
6552
6836
|
? t("pair_all_hint_which_failed", { cmd: whichCmd, bin, code })
|
|
6553
6837
|
: t("pair_all_hint_bin_not_in_path", { bin });
|
|
6554
|
-
results.push({ kind
|
|
6838
|
+
results.push({ kind, installed: false, hint });
|
|
6555
6839
|
continue;
|
|
6556
6840
|
}
|
|
6557
6841
|
if (!probeVersion) {
|
|
6558
6842
|
// reset 路径:只需 installed 布尔,跳过 `--version`(省 hermes 最长 60s 宽限)。
|
|
6559
|
-
results.push({ kind
|
|
6843
|
+
results.push({ kind, installed: true });
|
|
6560
6844
|
continue;
|
|
6561
6845
|
}
|
|
6562
6846
|
// ② 取版本(best-effort)。hermes 第一次跑会做 git ls-remote 检查更新,
|
|
@@ -6569,7 +6853,7 @@ function detectInstalledRuntimes({ probeVersion = true } = {}) {
|
|
|
6569
6853
|
if (!probe.error && probe.status === 0) {
|
|
6570
6854
|
version = String(probe.stdout ?? "").trim().split(/\r?\n/)[0] || "";
|
|
6571
6855
|
}
|
|
6572
|
-
const entry = { kind
|
|
6856
|
+
const entry = { kind, installed: true, version: version || t("pair_all_version_unknown") };
|
|
6573
6857
|
if (!version) {
|
|
6574
6858
|
// 记一下为啥没拿到版本,方便用户/我们之后定位
|
|
6575
6859
|
const code = probe.error?.code;
|
|
@@ -6595,15 +6879,26 @@ function detectInstalledRuntimes({ probeVersion = true } = {}) {
|
|
|
6595
6879
|
// openclaw + hermes 的 pair state 都存在 ~/.agentlink/config.json:
|
|
6596
6880
|
// openclaw → relayGatewayId/relayGatewayToken
|
|
6597
6881
|
// hermes → hermesRelayGatewayId/hermesRelayGatewayToken
|
|
6598
|
-
// claude/codex 存在 ~/.agentlink/ext-<runtime>-last.json。
|
|
6882
|
+
// claude/codex/opencode 存在 ~/.agentlink/ext-<runtime>-last.json。
|
|
6883
|
+
//
|
|
6884
|
+
// 🔴 目录一律走 resolveAgentlinkHome()(优先 AGENTLINK_HOME),**不得**用
|
|
6885
|
+
// `process.env.HOME + "/.agentlink"` 自己拼:写方(ext 侧 pairCodeClient.stateDir())
|
|
6886
|
+
// 与探方(runtimeHasLocalState)都优先认 AGENTLINK_HOME,这里拼裸 HOME 会造成
|
|
6887
|
+
// 「写在 A、探在 A、读/清在 B」的三方分叉 —— 已配对的 ext-agent runtime 被判成未
|
|
6888
|
+
// 配对而重复 consume(服务端多一条 orphan binding),stale 清理又会去删另一个
|
|
6889
|
+
// HOME 下的同名凭证(误删别人的)。launchd/systemd 单元本身就注入 AGENTLINK_HOME
|
|
6890
|
+
// (见 3600/3636/3841 行),所以这不是理论风险。
|
|
6599
6891
|
//
|
|
6600
6892
|
// 返回 { gw_id, token, relay_url } 或 null。
|
|
6601
6893
|
function readLocalPairState(kind) {
|
|
6602
6894
|
try {
|
|
6603
|
-
const
|
|
6604
|
-
if (!
|
|
6605
|
-
|
|
6606
|
-
|
|
6895
|
+
const agentlinkHome = resolveAgentlinkHome();
|
|
6896
|
+
if (!agentlinkHome) return null;
|
|
6897
|
+
// ext-agent 家族(claude / codex / opencode):凭证在 ext-<kind>-last.json。
|
|
6898
|
+
// 🔴 判据走登记表而不是字面量 —— 漏一个就会掉进下面的 config.json 分支,
|
|
6899
|
+
// 把 openclaw 的 relayGatewayId 当成它的凭证读出来(静默错配的读方向)。
|
|
6900
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(kind)) {
|
|
6901
|
+
const p = path.join(agentlinkHome, `ext-${kind}-last.json`);
|
|
6607
6902
|
if (!fs.existsSync(p)) return null;
|
|
6608
6903
|
const j = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
6609
6904
|
const gw = j?.gw_id || "";
|
|
@@ -6612,7 +6907,7 @@ function readLocalPairState(kind) {
|
|
|
6612
6907
|
return { gw_id: gw, token: tok, relay_url: j?.relay_url || "" };
|
|
6613
6908
|
}
|
|
6614
6909
|
if (kind === "openclaw" || kind === "hermes") {
|
|
6615
|
-
const p =
|
|
6910
|
+
const p = path.join(agentlinkHome, "config.json");
|
|
6616
6911
|
if (!fs.existsSync(p)) return null;
|
|
6617
6912
|
let j;
|
|
6618
6913
|
try { j = JSON.parse(fs.readFileSync(p, "utf8")); }
|
|
@@ -6633,17 +6928,20 @@ function readLocalPairState(kind) {
|
|
|
6633
6928
|
// 删本机 state 文件(App 删除后 server 没了,CLI 需要清干净本机才能 re-pair)。
|
|
6634
6929
|
function clearLocalPairState(kind) {
|
|
6635
6930
|
try {
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6931
|
+
// 与 readLocalPairState 同一判据(登记表驱动)**且同一目录解析**
|
|
6932
|
+
// (resolveAgentlinkHome / 优先 AGENTLINK_HOME)—— 读写必须落在同一个文件上,
|
|
6933
|
+
// 否则"清理"是空操作、真正被删的却是另一个 HOME 下的同名凭证。
|
|
6934
|
+
const agentlinkHome = resolveAgentlinkHome();
|
|
6935
|
+
if (!agentlinkHome) return;
|
|
6936
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(kind)) {
|
|
6937
|
+
const p = path.join(agentlinkHome, `ext-${kind}-last.json`);
|
|
6640
6938
|
if (fs.existsSync(p)) {
|
|
6641
6939
|
try { fs.unlinkSync(p); } catch {}
|
|
6642
6940
|
}
|
|
6643
6941
|
return;
|
|
6644
6942
|
}
|
|
6645
6943
|
if (kind === "openclaw" || kind === "hermes") {
|
|
6646
|
-
const p =
|
|
6944
|
+
const p = path.join(agentlinkHome, "config.json");
|
|
6647
6945
|
if (!fs.existsSync(p)) return;
|
|
6648
6946
|
let j;
|
|
6649
6947
|
try { j = JSON.parse(fs.readFileSync(p, "utf8")); } catch { return; }
|
|
@@ -6739,7 +7037,8 @@ async function probeRelayBinding(state) {
|
|
|
6739
7037
|
|
|
6740
7038
|
// 对应 runtime 的"重置后重新配对"命令提示。
|
|
6741
7039
|
function resetHintFor(kind, code) {
|
|
6742
|
-
|
|
7040
|
+
// ext-agent 家族没有 reset 子命令,其"解绑"即 uninstall(见 forwardToAgent)。
|
|
7041
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(kind)) {
|
|
6743
7042
|
return `agentlink uninstall -r ${kind} && agentlink pair ${code} -r ${kind}`;
|
|
6744
7043
|
}
|
|
6745
7044
|
// openclaw / hermes 走 reset 子命令
|
|
@@ -6759,9 +7058,12 @@ function spawnPairChildForRuntime({ argvTail, runtimeKind, env }) {
|
|
|
6759
7058
|
|
|
6760
7059
|
// `agentlink pair <code>` 默认批量配对所有已安装 runtime。
|
|
6761
7060
|
// 也兼容 `agentlink pair <code> --runtime all` 显式写法。
|
|
6762
|
-
// 检测本机安装的
|
|
6763
|
-
//
|
|
7061
|
+
// 检测本机安装的 runtime(HOST_DETECTED_RUNTIME_KINDS),对每个「已安装 **且**
|
|
7062
|
+
// 本轮有配对实现」的 runtime 串行 spawn 一次自身(替换 --runtime <kind>),
|
|
6764
7063
|
// 互不影响 fail(),最后汇总结果。
|
|
7064
|
+
// opencode 自 devagent-m-20260807-cli-runtime-opencode-pairable 起**已进扇出**
|
|
7065
|
+
// (它有了真配对实现)。已装但仍无配对实现的 runtime 才只进 host-inventory 上报、
|
|
7066
|
+
// 不进扇出 —— 见 runPairAll 内 pairTargetKinds 处注释。
|
|
6765
7067
|
async function runPairAll(options) {
|
|
6766
7068
|
if (!options.code || !String(options.code).trim()) {
|
|
6767
7069
|
fail(t("pair_usage_missing"));
|
|
@@ -6783,7 +7085,21 @@ async function runPairAll(options) {
|
|
|
6783
7085
|
|
|
6784
7086
|
const installedKinds = detected.filter((d) => d.installed).map((d) => d.kind);
|
|
6785
7087
|
if (installedKinds.length === 0) {
|
|
6786
|
-
fail(t("pair_all_no_runtimes"));
|
|
7088
|
+
fail(t("pair_all_no_runtimes", { kinds: HOST_DETECTED_RUNTIME_KINDS.join(" / ") }));
|
|
7089
|
+
}
|
|
7090
|
+
|
|
7091
|
+
// 扇出目标 = 已安装 ∩ 可配对集。已装但没有配对实现的 runtime **必须**排除在外:
|
|
7092
|
+
// 给它 spawn 一次 `--runtime <kind>` 只会撞 assertRuntimeOperableOrFail() 白拿
|
|
7093
|
+
// 一个非 0 退出,把整批 pair-all 拖成失败(纯回归)。
|
|
7094
|
+
// 但它仍留在 installedKinds 里上报给 relay —— 「本机装了它」是事实,App 需要知道。
|
|
7095
|
+
//
|
|
7096
|
+
// 现状:本机探测集 ⊆ 可配对集(opencode 自本轮起有了配对实现),所以
|
|
7097
|
+
// pairUnsupportedKinds 实际恒空。**保留**这条分支而不是删掉:它是由登记表
|
|
7098
|
+
// 派生的通用兜底,下一个"只入探测、还没配对实现"的 runtime 会立刻用上。
|
|
7099
|
+
const pairTargetKinds = installedKinds.filter((k) => PAIRABLE_RUNTIME_KINDS.includes(k));
|
|
7100
|
+
const pairUnsupportedKinds = installedKinds.filter((k) => !PAIRABLE_RUNTIME_KINDS.includes(k));
|
|
7101
|
+
for (const kind of pairUnsupportedKinds) {
|
|
7102
|
+
console.log(t("pair_all_runtime_pair_unsupported", { kind }));
|
|
6787
7103
|
}
|
|
6788
7104
|
|
|
6789
7105
|
// host-inventory side channel —— 在 spawn 子配对命令之前,先把本机装好的
|
|
@@ -6852,7 +7168,10 @@ async function runPairAll(options) {
|
|
|
6852
7168
|
continue;
|
|
6853
7169
|
}
|
|
6854
7170
|
if (a === "--allow-rebind") {
|
|
6855
|
-
|
|
7171
|
+
// 刻意**不**转发给子进程:pair-all 的语义是「把没配对的都配上」,已配对的
|
|
7172
|
+
// 由下面的 probeRelayBinding 判定后跳过,不做覆盖兜底(见下一段注释)。
|
|
7173
|
+
// ⚠️ 注释曾写「后面会显式加一次」——那是早期实现,现已不再回注;别照着补回去。
|
|
7174
|
+
continue;
|
|
6856
7175
|
}
|
|
6857
7176
|
forwardedArgv.push(a);
|
|
6858
7177
|
}
|
|
@@ -6872,7 +7191,7 @@ async function runPairAll(options) {
|
|
|
6872
7191
|
// 已提到顶层(供 runScan 复用,见其上方注释),行为不变。
|
|
6873
7192
|
|
|
6874
7193
|
const summary = [];
|
|
6875
|
-
for (const kind of
|
|
7194
|
+
for (const kind of pairTargetKinds) {
|
|
6876
7195
|
const state = readLocalPairState(kind);
|
|
6877
7196
|
if (state) {
|
|
6878
7197
|
const probe = await probeRelayBinding(state);
|
|
@@ -6932,7 +7251,7 @@ async function runPairAll(options) {
|
|
|
6932
7251
|
}
|
|
6933
7252
|
}
|
|
6934
7253
|
|
|
6935
|
-
//
|
|
7254
|
+
// 汇总:一行一类,一眼扫完。
|
|
6936
7255
|
const newlyPaired = summary.filter((s) => s.ok && !s.skipped).map((s) => s.kind);
|
|
6937
7256
|
const alreadyPaired = summary.filter((s) => s.skipped).map((s) => s.kind);
|
|
6938
7257
|
const failList = summary.filter((s) => !s.ok);
|
|
@@ -6951,13 +7270,23 @@ async function runPairAll(options) {
|
|
|
6951
7270
|
detail: failList.map((s) => `${s.kind}[exit ${s.status}]`).join("、"),
|
|
6952
7271
|
}));
|
|
6953
7272
|
}
|
|
7273
|
+
if (pairUnsupportedKinds.length > 0) {
|
|
7274
|
+
console.log(t("pair_all_summary_pair_unsupported", {
|
|
7275
|
+
count: pairUnsupportedKinds.length,
|
|
7276
|
+
kinds: pairUnsupportedKinds.join("、"),
|
|
7277
|
+
}));
|
|
7278
|
+
}
|
|
6954
7279
|
if (notInstalledList.length > 0) {
|
|
6955
7280
|
console.log(t("pair_all_summary_not_installed", { count: notInstalledList.length, kinds: notInstalledList.join("、") }));
|
|
6956
7281
|
}
|
|
6957
|
-
if (failList.length
|
|
7282
|
+
if (failList.length > 0) {
|
|
7283
|
+
console.log(t("pair_all_summary_failure_hint"));
|
|
7284
|
+
} else if (newlyPaired.length > 0 || alreadyPaired.length > 0) {
|
|
6958
7285
|
console.log(t("pair_all_summary_ok_hint"));
|
|
6959
7286
|
} else {
|
|
6960
|
-
|
|
7287
|
+
// 本机只装了「已知但无配对实现」的 runtime(如只装了 opencode):
|
|
7288
|
+
// 一个都没配上,别报「回 App 看已配对的智能体」那种假成功。
|
|
7289
|
+
console.log(t("pair_all_summary_nothing_pairable"));
|
|
6961
7290
|
}
|
|
6962
7291
|
console.log("");
|
|
6963
7292
|
|
|
@@ -6986,13 +7315,19 @@ async function runResetAll(options) {
|
|
|
6986
7315
|
// reset 不需要版本号 → 跳过 `--version` 探测(省 hermes 最长 60s 宽限)。
|
|
6987
7316
|
const detected = detectInstalledRuntimes({ probeVersion: false });
|
|
6988
7317
|
const installedKinds = detected.filter((d) => d.installed).map((d) => d.kind);
|
|
7318
|
+
// 只有「可配对集」的 runtime 会留下可解绑痕迹(config.json 的凭证 / ext-<rt>-last.json)。
|
|
7319
|
+
// 无配对实现的 runtime 不可能存在绑定,装了也没什么可解绑的 ——
|
|
7320
|
+
// 放进目标集只会让子进程撞 assertRuntimeOperableOrFail()、把整批 reset 拖成非 0 退出。
|
|
7321
|
+
// opencode 自本轮起在可配对集内:它会正常进入解绑目标(走 agentlink-agent uninstall)。
|
|
7322
|
+
const installedPairableKinds = installedKinds.filter((k) => PAIRABLE_RUNTIME_KINDS.includes(k));
|
|
7323
|
+
const noBindingKinds = installedKinds.filter((k) => !PAIRABLE_RUNTIME_KINDS.includes(k));
|
|
6989
7324
|
// 未安装但有本地痕迹(卸了 CLI / 删了 openclaw.json 但 binding 还在)的 runtime
|
|
6990
7325
|
// 也要纳入清理。读 reset 实际操作的同一批文件(config.json / ext-<rt>-last.json)。
|
|
6991
|
-
const ALL_KINDS =
|
|
7326
|
+
const ALL_KINDS = PAIRABLE_RUNTIME_KINDS;
|
|
6992
7327
|
const stateOnlyKinds = ALL_KINDS.filter(
|
|
6993
|
-
(k) => !
|
|
7328
|
+
(k) => !installedPairableKinds.includes(k) && runtimeHasLocalState(k),
|
|
6994
7329
|
);
|
|
6995
|
-
const targetKinds = [...
|
|
7330
|
+
const targetKinds = [...installedPairableKinds, ...stateOnlyKinds];
|
|
6996
7331
|
// 真正「无事可做」的 runtime(既没装、也没痕迹)才算 not-installed。
|
|
6997
7332
|
const notInstalledList = detected
|
|
6998
7333
|
.filter((d) => !d.installed && !stateOnlyKinds.includes(d.kind))
|
|
@@ -7000,7 +7335,9 @@ async function runResetAll(options) {
|
|
|
7000
7335
|
|
|
7001
7336
|
if (!json) {
|
|
7002
7337
|
for (const entry of detected) {
|
|
7003
|
-
if (entry.installed) {
|
|
7338
|
+
if (entry.installed && noBindingKinds.includes(entry.kind)) {
|
|
7339
|
+
console.log(t("reset_all_runtime_no_binding", { kind: entry.kind }));
|
|
7340
|
+
} else if (entry.installed) {
|
|
7004
7341
|
console.log(` ✓ ${entry.kind}`);
|
|
7005
7342
|
} else if (stateOnlyKinds.includes(entry.kind)) {
|
|
7006
7343
|
console.log(t("reset_all_runtime_state_only", { kind: entry.kind }));
|
|
@@ -7012,6 +7349,8 @@ async function runResetAll(options) {
|
|
|
7012
7349
|
}
|
|
7013
7350
|
log.info("reset_all.targets", "resolved reset targets", {
|
|
7014
7351
|
installed: installedKinds, stateOnly: stateOnlyKinds, notInstalled: notInstalledList,
|
|
7352
|
+
// 已装但无配对实现 → 无绑定可解,不进目标集(诊断时能一眼看出为何被跳过)
|
|
7353
|
+
noBinding: noBindingKinds,
|
|
7015
7354
|
});
|
|
7016
7355
|
|
|
7017
7356
|
if (targetKinds.length === 0) {
|
|
@@ -8144,8 +8483,10 @@ async function startScanWorkers({ kinds, relayUrl, gateway, json }) {
|
|
|
8144
8483
|
}
|
|
8145
8484
|
|
|
8146
8485
|
async function runScan({ json, relay, noRelay, gateway, qrPath, noServe, strictPlugin, runtime }) {
|
|
8147
|
-
// S2b(显式 -r
|
|
8148
|
-
|
|
8486
|
+
// S2b(显式 -r 到非 scan-capable 的 ext-agent runtime):不出码、不联系 relay,
|
|
8487
|
+
// 直接提示 + 非零退出。(opencode 更早已被 assertRuntimeBridgeSupportedOrFail
|
|
8488
|
+
// 拦停;这里保留登记表判据作双保险。)
|
|
8489
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(runtime)) {
|
|
8149
8490
|
const message = scanUnsupportedRuntimeMessage([runtime]);
|
|
8150
8491
|
if (json) {
|
|
8151
8492
|
printScanEvent({ phase: "unsupported_runtime", unsupportedRuntimes: [runtime], hint: message });
|
|
@@ -8171,14 +8512,19 @@ async function runScan({ json, relay, noRelay, gateway, qrPath, noServe, strictP
|
|
|
8171
8512
|
// ── 探测本机已装 runtime,划出「scan-capable 候选集」(S1/S2)───────────
|
|
8172
8513
|
const detected = detectInstalledRuntimes();
|
|
8173
8514
|
const installedKinds = new Set(detected.filter((d) => d.installed).map((d) => d.kind));
|
|
8515
|
+
// 已装、但不在 scan-capable 白名单里的 runtime:提示改走 pair(ext-agent 家族
|
|
8516
|
+
// 的凭证由服务端在 consume 时下发,本机没有 host-claim 路径)。判据走登记表,
|
|
8517
|
+
// 加 runtime 时不会漏提示。
|
|
8174
8518
|
const unsupportedDetected = detected
|
|
8175
|
-
.filter((d) => d.installed && (d.kind
|
|
8519
|
+
.filter((d) => d.installed && EXT_AGENT_RUNTIME_KINDS.includes(d.kind))
|
|
8176
8520
|
.map((d) => d.kind);
|
|
8177
8521
|
|
|
8178
8522
|
let candidateKinds;
|
|
8179
8523
|
if (runtime) {
|
|
8180
8524
|
// -r openclaw|hermes:只配这一个(S2)。校验值域已在 CLI 参数解析阶段完成
|
|
8181
|
-
// (openclaw/hermes/claude/codex/all
|
|
8525
|
+
// (openclaw/hermes/claude/codex/opencode/deepagents/all);ext-agent 家族
|
|
8526
|
+
// (claude/codex/opencode)已在上面提前返回,enum-only 的 deepagents 与
|
|
8527
|
+
// pair-only 的 opencode 更早就被命令分派前的两道显式拦停挡下,走不到这里。
|
|
8182
8528
|
if (!installedKinds.has(runtime)) {
|
|
8183
8529
|
fail(`${CLI_COMMAND_NAME} scan -r ${runtime}: runtime not installed on this host`);
|
|
8184
8530
|
}
|
|
@@ -8187,7 +8533,11 @@ async function runScan({ json, relay, noRelay, gateway, qrPath, noServe, strictP
|
|
|
8187
8533
|
candidateKinds = SCAN_CAPABLE_RUNTIME_KINDS.filter((k) => installedKinds.has(k));
|
|
8188
8534
|
}
|
|
8189
8535
|
if (candidateKinds.length === 0) {
|
|
8190
|
-
|
|
8536
|
+
// 🔴 kinds 必须显式传:该文案自 add-deepagents-opencode-runtime-enum 起改成
|
|
8537
|
+
// {kinds} 占位(不再把 runtime 名写死进文案),漏传会把字面 "{kinds}" 打给用户。
|
|
8538
|
+
// 这里的语境是 scan,所以报的是 **scan-capable 集合**(openclaw/hermes),
|
|
8539
|
+
// 不是 HOST_DETECTED 全集 —— 装了 opencode 也不构成"可扫码配对的 runtime"。
|
|
8540
|
+
fail(t("pair_all_no_runtimes", { kinds: SCAN_CAPABLE_RUNTIME_KINDS.join(" / ") }));
|
|
8191
8541
|
}
|
|
8192
8542
|
|
|
8193
8543
|
if (unsupportedDetected.length > 0) {
|
|
@@ -8585,10 +8935,11 @@ async function runBridge({ json, relay, noRelay, gateway, runtime }) {
|
|
|
8585
8935
|
}
|
|
8586
8936
|
|
|
8587
8937
|
function runStatus({ json, runtime }) {
|
|
8588
|
-
//
|
|
8938
|
+
// ext-agent runtime 不走 bin 的 openclaw/hermes 状态视图(normalizeRuntimeKind 会把
|
|
8589
8939
|
// 它们静默回退成 openclaw,导致展示错 runtime 的状态)。显式拦下并指向正确命令,
|
|
8590
|
-
//
|
|
8591
|
-
|
|
8940
|
+
// 而非静默误导。(opencode 更早已被 assertRuntimeBridgeSupportedOrFail 拦停,
|
|
8941
|
+
// 这里保留登记表判据是双保险 —— 两道闸的判据不同、都不能漏。)
|
|
8942
|
+
if (EXT_AGENT_RUNTIME_KINDS.includes(runtime)) {
|
|
8592
8943
|
fail(t("status_ext_runtime_unsupported", { runtime, cli: CLI_COMMAND_NAME }));
|
|
8593
8944
|
}
|
|
8594
8945
|
const configPath = resolveOpenClawConfigPath();
|
|
@@ -8639,14 +8990,23 @@ function runStatus({ json, runtime }) {
|
|
|
8639
8990
|
: null,
|
|
8640
8991
|
};
|
|
8641
8992
|
|
|
8642
|
-
//
|
|
8993
|
+
// 多端汇总(不指定 -r 时也能一眼看全**可配对** runtime 的配对 + 服务态),
|
|
8643
8994
|
// 对称 pair/reset 的「无 -r = 全部」。paired 走 HOME 隔离的凭证判定;service 态走
|
|
8644
8995
|
// 实际服务管理器(展示用途,反映真实情况)。best-effort,不让汇总失败影响主输出。
|
|
8645
|
-
|
|
8996
|
+
// 🔴 这里刻意用 **可配对集** 而不是已知枚举全集:本视图展示的是「配对态 + 服务态」,
|
|
8997
|
+
// 而 enum-only runtime(deepagents)永远没有配对、也没有 service,
|
|
8998
|
+
// 加进来只会长期多出恒为空的行 = 噪音。它们的"装没装"由 pair 的探测清单负责。
|
|
8999
|
+
status.runtimes = PAIRABLE_RUNTIME_KINDS.map((k) => {
|
|
8646
9000
|
let paired = false;
|
|
8647
9001
|
try { paired = runtimeHasLocalState(k); } catch { /* ignore */ }
|
|
8648
9002
|
let svc = { installed: false, running: false };
|
|
8649
|
-
|
|
9003
|
+
// 🔴 无 bridge 实现的 runtime(PAIR_ONLY,当前 = opencode)**不探服务态**:
|
|
9004
|
+
// getBridgeServiceStatus → serviceNames() → normalizeRuntimeKind(x,"openclaw")
|
|
9005
|
+
// 会把它别名到 openclaw 的 plist/unit,于是「openclaw 的 bridge 在跑」会被
|
|
9006
|
+
// 展示成「opencode 的服务在跑」= 静默错配。它压根没有 service,恒 false 才是实话。
|
|
9007
|
+
if (BRIDGE_CAPABLE_RUNTIME_KINDS.includes(k)) {
|
|
9008
|
+
try { svc = getBridgeServiceStatus(k); } catch { /* ignore */ }
|
|
9009
|
+
}
|
|
8650
9010
|
return { kind: k, paired, serviceInstalled: !!svc.installed, serviceRunning: !!svc.running };
|
|
8651
9011
|
});
|
|
8652
9012
|
|
|
@@ -8867,6 +9227,15 @@ async function forwardToAgent(command, options) {
|
|
|
8867
9227
|
|
|
8868
9228
|
async function main() {
|
|
8869
9229
|
const { command, options } = parseArgs(process.argv.slice(2));
|
|
9230
|
+
// Must happen before any pairing/runtime dispatch: every downstream resolver
|
|
9231
|
+
// reads AGENTLINK_HOME, and external-agent child processes inherit it.
|
|
9232
|
+
if (options.agentlinkHome) {
|
|
9233
|
+
process.env.AGENTLINK_HOME = options.agentlinkHome;
|
|
9234
|
+
}
|
|
9235
|
+
if (command === "capabilities") {
|
|
9236
|
+
process.stdout.write(`${JSON.stringify(RUNTIME_CAPABILITIES)}\n`);
|
|
9237
|
+
return;
|
|
9238
|
+
}
|
|
8870
9239
|
// `agentlink pair <code>` / `agentlink reset` 默认分别为本机已安装的 4 种
|
|
8871
9240
|
// runtime 各 spawn 一次(不写死默认 openclaw —— 四端互不依赖:没装 openclaw
|
|
8872
9241
|
// 的机器也能整机 reset)。`--runtime all` 保持为兼容别名。
|
|
@@ -8882,13 +9251,26 @@ async function main() {
|
|
|
8882
9251
|
if (command !== "pair" && command !== "reset" && options.runtime === "all") {
|
|
8883
9252
|
fail(`--runtime all 仅 \`${CLI_COMMAND_NAME} pair\` / \`${CLI_COMMAND_NAME} reset\` 命令支持`);
|
|
8884
9253
|
}
|
|
8885
|
-
//
|
|
8886
|
-
//
|
|
8887
|
-
|
|
9254
|
+
// 🔴 已知枚举 ∖ 可配对集(deepagents):显式拦停,绝不放进实现路径。
|
|
9255
|
+
// 判据与理由都在 assertRuntimeOperableOrFail 的文档注释里(单一事实源)。
|
|
9256
|
+
assertRuntimeOperableOrFail(options.runtime);
|
|
9257
|
+
// 🔴 可配对 ∖ 可桥接(opencode):pair / reset 放行,其余"要把进程跑起来 / 展示
|
|
9258
|
+
// 运行态"的命令显式拦停 —— 见 assertRuntimeBridgeSupportedOrFail 的文档注释。
|
|
9259
|
+
// 放在分派之前统一做,避免每个 run* 各自记得判一次(漏一个就退化成静默回落)。
|
|
9260
|
+
if (command === "bridge" || command === "service" || command === "reload" ||
|
|
9261
|
+
command === "scan" || command === "status" || command === "setup" ||
|
|
9262
|
+
command === "prepare") {
|
|
9263
|
+
assertRuntimeBridgeSupportedOrFail(options.runtime, command);
|
|
9264
|
+
}
|
|
9265
|
+
// External Agents (Claude / Codex / OpenCode) shim ——
|
|
9266
|
+
// 统一把 `agentlink pair <code> -r <ext>`、`bridge -r <ext>`、
|
|
9267
|
+
// `reset -r <ext>`(→ uninstall)转发到 agentlink-agent。避免 external
|
|
8888
9268
|
// 平行世界代码污染老 bin,也防止 reset 被 normalizeRuntimeKind 回退成 openclaw。
|
|
9269
|
+
// 目标集用 EXT_AGENT_RUNTIME_KINDS(登记表),不再写死 claude/codex 字面量 ——
|
|
9270
|
+
// opencode 的凭证同样存 ext-<rt>-last.json,必须走同一条转发路径。
|
|
8889
9271
|
if ((command === "pair" || command === "bridge" || command === "service" ||
|
|
8890
9272
|
command === "uninstall" || command === "reset") &&
|
|
8891
|
-
(options.runtime
|
|
9273
|
+
EXT_AGENT_RUNTIME_KINDS.includes(options.runtime)) {
|
|
8892
9274
|
await forwardToAgent(command, options);
|
|
8893
9275
|
return;
|
|
8894
9276
|
}
|