@xyagent/cli 0.0.1 → 1.0.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/bin/agentlink +551 -176
- package/bin/agentlink-agent +0 -0
- package/bin/agentlink-mcp-stdio +0 -0
- package/package.json +4 -2
- package/src/core.mjs +26 -0
- package/src-ext/bin.mjs +0 -1
- package/src-ext/commands/agent.mjs +0 -11
- package/src-ext/core/agentlinkToolsBootstrap.mjs +1 -1
- package/src-ext/core/autoTunnelDetector.mjs +6 -1
- package/src-ext/core/autoTunnelWiring.mjs +1 -1
- package/src-ext/core/bridgeSelfUninstall.mjs +1 -1
- package/src-ext/core/relayWorker.mjs +1 -1
- package/src-ext/core/scanDeeplink.mjs +67 -0
- package/src-ext/core/scanPairFlow.mjs +28 -0
- package/src-ext/core/scanQrRenderer.mjs +40 -0
- package/src-ext/core/unifiedDispatchHandler.mjs +3 -3
- package/src-ext/core/usageReporter.mjs +1 -2
- package/src-ext/openclaw-plugin/envelope-builder.cjs +2 -2
- package/src-ext/openclaw-plugin/todoTranslatorUtils.cjs +1 -1
- package/src-ext/runtime/_shared/relayObjectToBlock.mjs +9 -11
- package/src-ext/runtime/_shared/todoTranslatorUtils.mjs +1 -1
- package/src-ext/runtime/claude/launcher.mjs +0 -6
- package/src-ext/runtime/openclaw/buildOpenclawDaemonInput.mjs +0 -6
- package/src-shared/envelope_builder.mjs +2 -2
- package/src-ext/runtime/picoclaw/constants.mjs +0 -39
- package/src-ext/runtime/picoclaw/handleRequest.mjs +0 -289
- package/src-ext/runtime/picoclaw/index.mjs +0 -314
- package/src-ext/runtime/picoclaw/pairFlow.mjs +0 -224
- package/src-ext/runtime/picoclaw/state.mjs +0 -78
- package/src-ext/runtime/picoclaw/todoTranslator.mjs +0 -67
- package/src-ext/runtime/picoclaw/translator.mjs +0 -272
- package/src-ext/runtime/picoclaw/wsClient.mjs +0 -129
package/bin/agentlink-agent
CHANGED
|
File without changes
|
package/bin/agentlink-mcp-stdio
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xyagent/cli",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Relay-first CLI for pairing and bridging OpenClaw gateway traffic",
|
|
6
6
|
"type": "module",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"agentlink-mcp-stdio": "bin/agentlink-mcp-stdio"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"pdfjs-dist": "^4.10.38"
|
|
36
|
+
"pdfjs-dist": "^4.10.38",
|
|
37
|
+
"qrcode": "^1.5.4",
|
|
38
|
+
"qrcode-terminal": "^0.12.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/src/core.mjs
CHANGED
|
@@ -80,6 +80,16 @@ export function normalizeProvidedCode(raw, onInvalid = throwError) {
|
|
|
80
80
|
if (/^\d{4}$/.test(compact)) return compact;
|
|
81
81
|
if (/^\d{6}$/.test(compact)) return compact;
|
|
82
82
|
if (/^[A-Z0-9]{8}$/.test(compact)) return formatCode(compact);
|
|
83
|
+
// High-entropy scan code (`agentlink scan`, unify-scan-pairing): generated
|
|
84
|
+
// locally by the CLI via generateScanPairCode(), never hand-typed by a user
|
|
85
|
+
// (QR/deeplink transport only) — so no dash-grouping is applied. Contract
|
|
86
|
+
// with go_relay (real-device P0 fix): EXACTLY 16 uppercase hex chars
|
|
87
|
+
// (64 bits) — go_relay's `pair_sessions.pair_code` column is `varchar(16)`
|
|
88
|
+
// (no DDL), and go_relay's own validation was tightened to the same
|
|
89
|
+
// `^[0-9A-F]{16}$`. Do not widen this range again without a matching DDL —
|
|
90
|
+
// a wider CLI-side accept here previously let an 18-char/72-bit code
|
|
91
|
+
// through that overflowed the column (HTTP 500 "Data too long").
|
|
92
|
+
if (/^[A-Z0-9]{16}$/.test(compact)) return compact;
|
|
83
93
|
return onInvalid(
|
|
84
94
|
"invalid --code format. expected 4 digits (e.g. 1234), 6 digits (e.g. 123456), " +
|
|
85
95
|
"or 8 alphanumeric characters (e.g. ABCD-1234; case-insensitive, dash optional)",
|
|
@@ -92,6 +102,22 @@ export function generatePairCode(random = Math.random, numericCodeLength = NUMER
|
|
|
92
102
|
return String(min + Math.floor(random() * (max - min)));
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
// High-entropy pair code for `agentlink scan` (unify-scan-pairing spec's "安全
|
|
106
|
+
// 信封" §1: >=64-bit entropy, cryptographic random source, unenumerable).
|
|
107
|
+
// EXACTLY 8 random bytes -> 16 uppercase hex chars = 64 bits — pinned to this
|
|
108
|
+
// exact length to fit go_relay's `pair_sessions.pair_code varchar(16)` column
|
|
109
|
+
// (no DDL, per user decision after a real-device P0: an earlier 9-byte/18-char
|
|
110
|
+
// version overflowed the column with HTTP 500 "Data too long"). 64 bits still
|
|
111
|
+
// clears the spec's >=64-bit floor for a short-TTL(<=5min) + rate-limited
|
|
112
|
+
// (30/min) one-time code. Never hand-typed by a user, so no dash-grouping
|
|
113
|
+
// (contrast generatePairCode()/formatCode() above, which target human entry).
|
|
114
|
+
// Do not change the byte count without re-confirming the column width with
|
|
115
|
+
// go_relay first.
|
|
116
|
+
export function generateScanPairCode(randomBytesFn = randomBytes) {
|
|
117
|
+
const buf = randomBytesFn(8);
|
|
118
|
+
return Buffer.from(buf).toString("hex").toUpperCase();
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
export function parseNonNegativeInt(value, fallback = 0) {
|
|
96
122
|
if (typeof value === "number" && Number.isFinite(value)) {
|
|
97
123
|
return value >= 0 ? Math.trunc(value) : fallback;
|
package/src-ext/bin.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { runClaudeBridge } from "../runtime/claude/index.mjs";
|
|
2
2
|
import { runCodexBridge } from "../runtime/codex/index.mjs";
|
|
3
3
|
import { runHermesBridge } from "../runtime/hermes/index.mjs";
|
|
4
|
-
import { runPicoclawBridge } from "../runtime/picoclaw/index.mjs";
|
|
5
4
|
import { runPairInstall, runUninstall } from "./pair.mjs";
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -17,13 +16,6 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
17
16
|
const action = (positional[0] || "").toLowerCase();
|
|
18
17
|
|
|
19
18
|
if (action === "pair") {
|
|
20
|
-
// picoclaw 不走传统 `pair <code>`,daemon 启动后自动配对(QR 流)。
|
|
21
|
-
if (runtime === "picoclaw") {
|
|
22
|
-
process.stderr.write(
|
|
23
|
-
"picoclaw 不需要 pair 子命令,直接启动 daemon: agentlink-agent picoclaw\n",
|
|
24
|
-
);
|
|
25
|
-
process.exit(2);
|
|
26
|
-
}
|
|
27
19
|
const code = String(positional[1] || options.pair || "").trim();
|
|
28
20
|
if (!code) {
|
|
29
21
|
process.stderr.write(`usage: agentlink pair <code> -r ${runtime}\n`);
|
|
@@ -55,9 +47,6 @@ export async function runAgentCommand({ runtime, options, positional, log }) {
|
|
|
55
47
|
case "hermes":
|
|
56
48
|
await runHermesBridge({ options, positional, log: rtLog });
|
|
57
49
|
return;
|
|
58
|
-
case "picoclaw":
|
|
59
|
-
await runPicoclawBridge({ options, positional, log: rtLog });
|
|
60
|
-
return;
|
|
61
50
|
default:
|
|
62
51
|
rtLog.error("bridge.unknown_runtime", "unsupported runtime", { runtime });
|
|
63
52
|
process.stderr.write(`unsupported runtime: ${runtime}\n`);
|
|
@@ -19,7 +19,7 @@ import { createPathAttachScanner } from "./bridgeAttachAutoScanner.mjs";
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @param {object} opts
|
|
22
|
-
* @param {string} opts.runtime "claude"|"codex"|"hermes"|"openclaw"
|
|
22
|
+
* @param {string} opts.runtime "claude"|"codex"|"hermes"|"openclaw"
|
|
23
23
|
* @param {string} opts.gwId gateway id (= clientUserID 跨 pair_session 反查的键)
|
|
24
24
|
* @param {string} opts.relayUrl relay base URL
|
|
25
25
|
* @param {string} opts.bridgeToken gateway/client token, used by attach 工具调 relay sign/commit
|
|
@@ -22,7 +22,12 @@ const LAN_HOST =
|
|
|
22
22
|
|
|
23
23
|
// URL 尾部:可省略;若有,以 / ? # 起,止于空白或闭合标点。
|
|
24
24
|
// 不包含 ) ] > " ' ` 避免吞 markdown / HTML / 引号边界。
|
|
25
|
-
|
|
25
|
+
// 另排除全角标点/括号(-,含全角 `()`)与 CJK 字符(CJK 标点
|
|
26
|
+
// -ヿ、汉字 一-鿿、谚文 가-):模型常在 URL 后
|
|
27
|
+
// 紧跟中文说明(如 `index.html(返回 200 正常)`),不排除会把中文吞进 URL,
|
|
28
|
+
// 导致穿透 URL 与 markdown label 都带上乱码路径 → WebView 404。
|
|
29
|
+
const URL_TAIL =
|
|
30
|
+
"(?:[/?#][^\\s)\\]>\"'`\\u3000-\\u30FF\\u4E00-\\u9FFF\\uAC00-\\uD7AF\\uFF00-\\uFFEF]*)?";
|
|
26
31
|
|
|
27
32
|
function buildRegex(hostFragment) {
|
|
28
33
|
// 锚定:URL 前必须有非字母数字(或字符串开头)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// 则禁用)选择是否注册到 bridgeHooks 全局 registry。
|
|
9
9
|
// - 多次调用幂等:用模块级 flag 防止重复注册。
|
|
10
10
|
//
|
|
11
|
-
// 调用方:每个 runtime 入口(src-ext/runtime/{claude,codex,hermes
|
|
11
|
+
// 调用方:每个 runtime 入口(src-ext/runtime/{claude,codex,hermes}/index.mjs)
|
|
12
12
|
// 在 bridge 主循环启动前调一次 installAutoTunnel({ relayUrl, log })。
|
|
13
13
|
|
|
14
14
|
import fs from "node:fs";
|
|
@@ -43,7 +43,7 @@ function findCliBinary() {
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* @param {{ runtime: string, log: any }} opts
|
|
46
|
-
* runtime: hermes / claude / codex / openclaw
|
|
46
|
+
* runtime: hermes / claude / codex / openclaw
|
|
47
47
|
* log: bridge 自己的 logger(已经带 cid / gw context)
|
|
48
48
|
*
|
|
49
49
|
* 返回 Promise,不抛错 —— 任一步失败都 best-effort,最差也就是用户手动
|
|
@@ -111,7 +111,7 @@ export function createRelayWorker(opts) {
|
|
|
111
111
|
// App 端用 `meta.online ?? meta.connected ?? meta.alive ?? meta.state` 串行
|
|
112
112
|
// 兜底来判 bridge 是否在线(见 agent_app/matelink-app/lib/src/models/relay_models.dart:477)。
|
|
113
113
|
// legacy openclaw bridge 的 `buildBridgePresenceMeta` 一直会发这套字段,
|
|
114
|
-
// ext runtime(hermes / claude / codex
|
|
114
|
+
// ext runtime(hermes / claude / codex)漏发会导致 onlineHint=null
|
|
115
115
|
// → App UI 兜底显示"离线"(即用户报"配对完显示离线"的根因)。
|
|
116
116
|
// detail === "stopped" 时表示主动下线,其它("ready" / "heartbeat" / "alive")都视为在线。
|
|
117
117
|
async function sendMeta(detail) {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// `agentlink scan` 中性 deeplink 构造 + 本地 QR PNG 渲染。
|
|
2
|
+
//
|
|
3
|
+
// unify-scan-pairing 取代了上一代扫码硬件方案:旧方案的 QR payload/PNG 由**服务端**
|
|
4
|
+
// 在专用 create 端点响应里下发(`qr_payload` + `qr_png_base64`,已随硬件方案一并
|
|
5
|
+
// 删除)。新契约下 `POST /v1/pair-sessions` host-claim 响应不带任何二维码内容——
|
|
6
|
+
// CLI 必须自己构造 payload、自己渲染 PNG。
|
|
7
|
+
//
|
|
8
|
+
// 契约(spec「中性 deeplink 契约」,冻结,App 侧对齐):
|
|
9
|
+
// agentlink://pair/scan?v=1&code=<CODE>&relay=<urlencoded relay base>
|
|
10
|
+
// v = payload 版本号(当前 1)
|
|
11
|
+
// code = 高熵 pair code(generateScanPairCode(),见 src/core.mjs)
|
|
12
|
+
// relay= 可选;缺省时 App 用自己已配置的 relay。
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
const DEFAULT_DEEPLINK_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 构造中性 deeplink payload 字符串。
|
|
21
|
+
*
|
|
22
|
+
* @param {{ code: string, relay?: string, version?: number }} opts
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
export function buildScanDeeplink({ code, relay, version = DEFAULT_DEEPLINK_VERSION } = {}) {
|
|
26
|
+
const trimmedCode = String(code ?? "").trim();
|
|
27
|
+
if (!trimmedCode) {
|
|
28
|
+
throw new Error("buildScanDeeplink: code is required");
|
|
29
|
+
}
|
|
30
|
+
const params = new URLSearchParams();
|
|
31
|
+
params.set("v", String(version));
|
|
32
|
+
params.set("code", trimmedCode);
|
|
33
|
+
const trimmedRelay = String(relay ?? "").trim();
|
|
34
|
+
if (trimmedRelay) {
|
|
35
|
+
params.set("relay", trimmedRelay);
|
|
36
|
+
}
|
|
37
|
+
return `agentlink://pair/scan?${params.toString()}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 把 payload 渲染成 QR PNG 并落地到 outputPath。永不抛错——渲染失败时返回
|
|
42
|
+
* `{ ok: false, error }`,调用方(runScan)据此打印兜底提示,不中断守护循环
|
|
43
|
+
* (spec S5 的兜底精神:QR 展示失败不能中断配对流程;这里同样适用于 PNG 落地失败,
|
|
44
|
+
* 例如磁盘写入被拒)。
|
|
45
|
+
*
|
|
46
|
+
* @param {{
|
|
47
|
+
* payload: string,
|
|
48
|
+
* outputPath: string,
|
|
49
|
+
* generateImpl?: (payload: string, outputPath: string) => Promise<void>,
|
|
50
|
+
* }} opts
|
|
51
|
+
* @returns {Promise<{ ok: boolean, path?: string, error?: string }>}
|
|
52
|
+
*/
|
|
53
|
+
export async function renderScanQrPng({ payload, outputPath, generateImpl }) {
|
|
54
|
+
const generate = generateImpl || defaultGenerateImpl;
|
|
55
|
+
try {
|
|
56
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
57
|
+
await generate(payload, outputPath);
|
|
58
|
+
return { ok: true, path: outputPath };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return { ok: false, error: String(err?.message || err) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function defaultGenerateImpl(payload, outputPath) {
|
|
65
|
+
const { default: QRCode } = await import("qrcode");
|
|
66
|
+
await QRCode.toFile(outputPath, String(payload ?? ""), { type: "png" });
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// `agentlink scan` 二维码 PNG 落地路径 + 清理工具。
|
|
2
|
+
//
|
|
3
|
+
// unify-scan-pairing 全量移除了上一代扫码硬件方案:本模块曾承载其专用
|
|
4
|
+
// pair-code create/poll 客户端(`createPairCode`/`pollPairStatus`/
|
|
5
|
+
// `runScanOutcodeLoop`)、专用命名空间常量、只写不 issue 的
|
|
6
|
+
// `buildScanConfigPatch`,以及一个命名空间归一 hack——这些全部随该硬件方案的
|
|
7
|
+
// 专用端点一并删除,新扫码路径改走 `pair_sessions` host-claim(见 bin/agentlink
|
|
8
|
+
// 的 runScan/runPair pairMode="scan" 分支)+ 本地渲染的中性 deeplink QR
|
|
9
|
+
// (见 scanDeeplink.mjs)。
|
|
10
|
+
//
|
|
11
|
+
// 保留下来的只有与具体硬件方案无关的通用 QR 文件落地/清理工具:CLI 出码循环
|
|
12
|
+
// 每轮把新二维码 PNG 写到约定路径,过期重出码或配对成功后清掉旧文件。
|
|
13
|
+
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
|
|
16
|
+
// 二维码 PNG 落地兜底路径,可用 --qr-path 或 AGENTLINK_SCAN_QR_PNG_PATH 覆盖。
|
|
17
|
+
export const SCAN_QR_PNG_OUTPUT_PATH =
|
|
18
|
+
process.env.AGENTLINK_SCAN_QR_PNG_PATH?.trim() || "/tmp/bubu/scan-pair-qr.png";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 删除 QR 图片(配对成功/重新出码前清理)。不存在时是安全的 no-op。
|
|
22
|
+
*/
|
|
23
|
+
export function clearQRImage(outputPath) {
|
|
24
|
+
const target = outputPath || SCAN_QR_PNG_OUTPUT_PATH;
|
|
25
|
+
try {
|
|
26
|
+
fs.unlinkSync(target);
|
|
27
|
+
} catch {}
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// `agentlink scan` 终端二维码渲染 —— 封装 `qrcode-terminal`(`small: true`)。
|
|
2
|
+
//
|
|
3
|
+
// spec S6(二维码展示兜底):终端不支持 ASCII 二维码渲染时(渲染抛错),
|
|
4
|
+
// SHALL 回退为打印 payload 文本,不因渲染失败中断配对流程。因此本函数
|
|
5
|
+
// **永不抛错**——渲染失败被内部吞掉并转成文本兜底。
|
|
6
|
+
|
|
7
|
+
import qrcodeTerminal from "qrcode-terminal";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 在终端渲染 payload 的 ASCII 二维码;渲染失败时回退打印 payload 文本。
|
|
11
|
+
*
|
|
12
|
+
* @param {string} payload 二维码内容(qr_payload deeplink)
|
|
13
|
+
* @param {{
|
|
14
|
+
* generateImpl?: (input: string, opts: {small: boolean}, cb: (output: string) => void) => void,
|
|
15
|
+
* print?: (s: string) => void,
|
|
16
|
+
* warn?: (s: string) => void,
|
|
17
|
+
* }} [deps]
|
|
18
|
+
* @returns {{ ok: boolean, mode: "qr" | "text", error?: string }}
|
|
19
|
+
*/
|
|
20
|
+
export function renderQrToTerminal(payload, { generateImpl, print, warn } = {}) {
|
|
21
|
+
// NB: must call through `qrcodeTerminal.generate(...)`, not a bare
|
|
22
|
+
// reference to the method — qrcode-terminal's generate() reads `this.error`
|
|
23
|
+
// internally, and a detached reference loses that binding (`this` would be
|
|
24
|
+
// undefined under ESM's implicit strict mode), throwing on every call.
|
|
25
|
+
const generate = generateImpl || ((input, opts, cb) => qrcodeTerminal.generate(input, opts, cb));
|
|
26
|
+
const doPrint = print || ((s) => console.log(s));
|
|
27
|
+
const doWarn = warn || ((s) => console.warn(s));
|
|
28
|
+
const text = payload == null ? "" : String(payload);
|
|
29
|
+
try {
|
|
30
|
+
generate(text, { small: true }, (output) => {
|
|
31
|
+
doPrint(output);
|
|
32
|
+
});
|
|
33
|
+
return { ok: true, mode: "qr" };
|
|
34
|
+
} catch (err) {
|
|
35
|
+
const message = String(err?.message || err);
|
|
36
|
+
doWarn(`⚠ 二维码渲染失败,回退为文本(仍可完成配对):${message}`);
|
|
37
|
+
doPrint(text);
|
|
38
|
+
return { ok: false, mode: "text", error: message };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Injection points for testing:
|
|
11
11
|
* producerFactory({ relayUrl, bridgeToken, threadId }) → { postEvents }
|
|
12
|
-
* openclawInvoke({ input, onChunk }) → async void (default:
|
|
12
|
+
* openclawInvoke({ input, onChunk }) → async void (default: openclaw wsHolder)
|
|
13
13
|
* flushPolicy({ size, timeMs }) → 控制 micro-batched flush 触发阈值(默认 {size:1, timeMs:0} —— 即来即发,保流式体验)
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -29,7 +29,7 @@ import { signGetCosObject } from "./cosUploadClient.mjs";
|
|
|
29
29
|
// Streaming-first policy: size:1 means every envelope (especially
|
|
30
30
|
// content_block.delta) posts immediately, so the App SSE stream receives
|
|
31
31
|
// chunks as the runtime produces them rather than in 8-envelope batches.
|
|
32
|
-
// Tests
|
|
32
|
+
// Tests can override via flushPolicy if they want fewer POSTs.
|
|
33
33
|
const DEFAULT_FLUSH_POLICY = Object.freeze({ size: 1, timeMs: 0 });
|
|
34
34
|
const STREAM_DEBUG = process.env.BUBBO_DEBUG_STREAM === "1";
|
|
35
35
|
|
|
@@ -343,7 +343,7 @@ export function createUnifiedDispatchHandler({
|
|
|
343
343
|
let runPausedFromRuntime = false;
|
|
344
344
|
|
|
345
345
|
if (typeof openclawInvoke === "function") {
|
|
346
|
-
// Injected (test mode or
|
|
346
|
+
// Injected (test mode or ws integration)
|
|
347
347
|
// thread_id 透传给 runtime adapter,让需要"按 thread 维护原生 session
|
|
348
348
|
// 续接"的 runtime(如 claude --resume <sid>)可以做 mapping。
|
|
349
349
|
await openclawInvoke({
|
|
@@ -40,8 +40,7 @@ export function createUsageReporter({ publishEvent, runtime, log, debounceMs = D
|
|
|
40
40
|
if (
|
|
41
41
|
runtime !== "claude" &&
|
|
42
42
|
runtime !== "codex" &&
|
|
43
|
-
runtime !== "hermes"
|
|
44
|
-
runtime !== "picoclaw"
|
|
43
|
+
runtime !== "hermes"
|
|
45
44
|
) {
|
|
46
45
|
throw new Error(`createUsageReporter: bad runtime ${runtime}`);
|
|
47
46
|
}
|
|
@@ -448,7 +448,7 @@ function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
448
448
|
}
|
|
449
449
|
envelopes.push(env);
|
|
450
450
|
} else if (event === "response.function_call_arguments.delta") {
|
|
451
|
-
//
|
|
451
|
+
// legacy event name → maps to tool_use.start + arguments.delta.
|
|
452
452
|
// chunk shape: { call_id, name, arguments } where arguments is a JSON
|
|
453
453
|
// fragment to append. We auto-start a tool_use block on first occurrence.
|
|
454
454
|
const callId = String(chunk.call_id || "");
|
|
@@ -540,7 +540,7 @@ function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
540
540
|
envelopes.push(started, completed);
|
|
541
541
|
} else if (event === "response.output_text.done") {
|
|
542
542
|
// No incremental envelope needed — full text already accumulated via deltas.
|
|
543
|
-
// If runtime carries authoritative full text on .done
|
|
543
|
+
// If the runtime carries authoritative full text on .done, prefer it.
|
|
544
544
|
if (typeof chunk.text === "string" && chunk.text.length > 0) {
|
|
545
545
|
text.content = chunk.text;
|
|
546
546
|
}
|
|
@@ -75,7 +75,7 @@ function normalizeStatus(raw) {
|
|
|
75
75
|
* rawItems 中每项支持字段:
|
|
76
76
|
* - id?: string(有则保留,无则生成 td_{idx})
|
|
77
77
|
* - content?: string(主内容字段,Claude/Hermes 用)
|
|
78
|
-
* - step?: string(Codex
|
|
78
|
+
* - step?: string(Codex 用,作为 content 的 fallback)
|
|
79
79
|
* - status?: string(经 normalizeStatus 归一)
|
|
80
80
|
*
|
|
81
81
|
* @param {Array<{id?: string, content?: string, step?: string, status?: string}>} rawItems
|
|
@@ -12,9 +12,9 @@
|
|
|
12
12
|
* { type: "image"|"attachment", source: { kind: "url", url, media_type, attachment_kind }, name?, mime?, size? }
|
|
13
13
|
*
|
|
14
14
|
* @param {object} opts
|
|
15
|
-
* @param {string} opts.runtimeHint - "claude"|"openclaw"|"
|
|
15
|
+
* @param {string} opts.runtimeHint - "claude"|"openclaw"|"codex"|"hermes"
|
|
16
16
|
* @param {string[]|null} opts.modelCaps - modalities 数组(如 ["text","image","pdf"])。
|
|
17
|
-
* null 表示跳过 caps
|
|
17
|
+
* null 表示跳过 caps 检查。
|
|
18
18
|
* 缺失时默认 ["text"]。
|
|
19
19
|
* @param {Function} opts.fetchAndBase64Encode - async (url, opts) => { base64, mediaType, bytes }
|
|
20
20
|
* @param {Function} [opts.fetchAndDecodeText] - async (url, opts) => string
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
* { type: "input_image", source: { type: "base64", media_type, data } }
|
|
27
27
|
* { type: "input_file", source: { type: "base64", media_type: "application/pdf", filename, data } }
|
|
28
28
|
* { type: "input_text", text }
|
|
29
|
-
* { type: "url_ref", url, media_type } // picoclaw only
|
|
30
29
|
*
|
|
31
30
|
* ErrorResult:
|
|
32
31
|
* { error: { code: "caps_mismatch", message, model?, missing_modality } }
|
|
@@ -70,8 +69,12 @@ function resolveAttachmentKind(block) {
|
|
|
70
69
|
|
|
71
70
|
export async function relayObjectToBlock(block, opts = {}) {
|
|
72
71
|
const {
|
|
73
|
-
runtimeHint
|
|
74
|
-
|
|
72
|
+
// 曾经唯一消费 runtimeHint 的分支(url_ref 直通)是移除硬件方案时删掉的
|
|
73
|
+
// 专属逻辑;所有真实 runtime(openclaw/claude/hermes)现在走同一条
|
|
74
|
+
// fetch+base64 路径,不再需要按 runtime 分叉。保留在签名里是为了不动调用方
|
|
75
|
+
// (openclaw/claude/hermes 三处仍显式传它),留作未来按 runtime 差异化的挂钩点。
|
|
76
|
+
runtimeHint: _runtimeHint,
|
|
77
|
+
modelCaps, // null = skip caps check; undefined/missing = default ["text"]
|
|
75
78
|
fetchAndBase64Encode,
|
|
76
79
|
fetchAndDecodeText,
|
|
77
80
|
} = opts;
|
|
@@ -107,12 +110,7 @@ export async function relayObjectToBlock(block, opts = {}) {
|
|
|
107
110
|
};
|
|
108
111
|
}
|
|
109
112
|
|
|
110
|
-
// 2.
|
|
111
|
-
if (runtimeHint === "picoclaw") {
|
|
112
|
-
return { type: "url_ref", url, media_type: mediaType };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// 3. fetch + base64
|
|
113
|
+
// 2. fetch + base64
|
|
116
114
|
try {
|
|
117
115
|
const { base64, mediaType: resolvedMime, bytes } = await fetchAndBase64Encode(url, {
|
|
118
116
|
expectedMime: mediaType,
|
|
@@ -72,7 +72,7 @@ export function normalizeStatus(raw) {
|
|
|
72
72
|
* rawItems 中每项支持字段:
|
|
73
73
|
* - id?: string(有则保留,无则生成 td_{idx})
|
|
74
74
|
* - content?: string(主内容字段,Claude/Hermes 用)
|
|
75
|
-
* - step?: string(Codex
|
|
75
|
+
* - step?: string(Codex 用,作为 content 的 fallback)
|
|
76
76
|
* - status?: string(经 normalizeStatus 归一)
|
|
77
77
|
*
|
|
78
78
|
* @param {Array<{id?: string, content?: string, step?: string, status?: string}>} rawItems
|
|
@@ -285,12 +285,6 @@ export async function writeUserMessage(child, text, blocks = [], { _fetchAndBase
|
|
|
285
285
|
case "input_text":
|
|
286
286
|
content.push({ type: "text", text: result.text });
|
|
287
287
|
break;
|
|
288
|
-
case "url_ref":
|
|
289
|
-
// picoclaw only — claude runtime never returns url_ref from relayObjectToBlock;
|
|
290
|
-
// if this branch is somehow hit, drop to a safe description with NO URL in LLM context
|
|
291
|
-
// (铁律 #1: signed URL must not appear in LLM input). (CR-003)
|
|
292
|
-
content.push({ type: "text", text: `[image] (preview unavailable for this runtime)` });
|
|
293
|
-
break;
|
|
294
288
|
default:
|
|
295
289
|
break;
|
|
296
290
|
}
|
|
@@ -127,12 +127,6 @@ export async function buildOpenclawDaemonInput(textInput, blocks, {
|
|
|
127
127
|
// text 文件内容 / binary 描述 / document text extraction fallback
|
|
128
128
|
content.push({ type: "input_text", text: result.text });
|
|
129
129
|
break;
|
|
130
|
-
case "url_ref":
|
|
131
|
-
// picoclaw only — openclaw/claude runtimes never return url_ref from relayObjectToBlock;
|
|
132
|
-
// if this branch is somehow hit, drop to a safe description with NO URL in LLM context
|
|
133
|
-
// (铁律 #1: signed URL must not appear in LLM input). (CR-003)
|
|
134
|
-
content.push({ type: "input_text", text: `[image] (preview unavailable for this runtime)` });
|
|
135
|
-
break;
|
|
136
130
|
default:
|
|
137
131
|
break;
|
|
138
132
|
}
|
|
@@ -445,7 +445,7 @@ export function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
445
445
|
}
|
|
446
446
|
envelopes.push(env);
|
|
447
447
|
} else if (event === "response.function_call_arguments.delta") {
|
|
448
|
-
//
|
|
448
|
+
// legacy event name → maps to tool_use.start + arguments.delta.
|
|
449
449
|
// chunk shape: { call_id, name, arguments } where arguments is a JSON
|
|
450
450
|
// fragment to append. We auto-start a tool_use block on first occurrence.
|
|
451
451
|
const callId = String(chunk.call_id || "");
|
|
@@ -537,7 +537,7 @@ export function translateOpenClawChunkToEnvelope(chunk, runId, blockState) {
|
|
|
537
537
|
envelopes.push(started, completed);
|
|
538
538
|
} else if (event === "response.output_text.done") {
|
|
539
539
|
// No incremental envelope needed — full text already accumulated via deltas.
|
|
540
|
-
// If runtime carries authoritative full text on .done
|
|
540
|
+
// If the runtime carries authoritative full text on .done, prefer it.
|
|
541
541
|
if (typeof chunk.text === "string" && chunk.text.length > 0) {
|
|
542
542
|
text.content = chunk.text;
|
|
543
543
|
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// 巴布(PicoClaw)运行时常量。
|
|
2
|
-
//
|
|
3
|
-
// 部署假设:matelink-cli 与 picoclaw daemon 烧录在同一开发板,
|
|
4
|
-
// cli 默认连本地回环 picoclaw WebSocket。token 烧录时固化,cli 端写死同值。
|
|
5
|
-
//
|
|
6
|
-
// 如需改 token,**两处**(开发板 picoclaw config + 此文件)必须同步,否则连接失败。
|
|
7
|
-
|
|
8
|
-
import process from "node:process";
|
|
9
|
-
|
|
10
|
-
// PicoClaw daemon 的 WebSocket 端点(Pico Protocol)。
|
|
11
|
-
// 默认 127.0.0.1:18790/pico/ws,可通过 env 覆盖(开发/调试用)。
|
|
12
|
-
export const PICOCLAW_WS_URL =
|
|
13
|
-
process.env.PICOCLAW_WS_URL?.trim() || "ws://127.0.0.1:18790/pico/ws";
|
|
14
|
-
|
|
15
|
-
// PicoClaw Bearer token —— 与开发板 picoclaw config 里 [pico].token 一致。
|
|
16
|
-
// 生产烧录时双方都写死成下面这个固定值;切换值时必须双侧同步。
|
|
17
|
-
export const PICOCLAW_TOKEN =
|
|
18
|
-
process.env.PICOCLAW_TOKEN?.trim() || "bubu-local-token";
|
|
19
|
-
|
|
20
|
-
// 巴布配对二维码 PNG 输出路径 —— 屏幕显示模块从这个路径加载图片。
|
|
21
|
-
// 默认写到 /tmp/bubu/pair-qr.png,可通过 env 覆盖。
|
|
22
|
-
export const QR_PNG_OUTPUT_PATH =
|
|
23
|
-
process.env.BUBU_QR_PNG_PATH?.trim() || "/tmp/bubu/pair-qr.png";
|
|
24
|
-
|
|
25
|
-
// 配对状态轮询间隔(秒)。服务端建议值在 create 响应里返回,这是兜底。
|
|
26
|
-
export const PAIR_STATUS_POLL_INTERVAL_SEC = 2;
|
|
27
|
-
|
|
28
|
-
// 配对状态最长等待时间(秒)。超过后 cli 重新生成 QR(配对码已过期)。
|
|
29
|
-
export const PAIR_STATUS_POLL_MAX_SEC = 300;
|
|
30
|
-
|
|
31
|
-
// 长轮询拉取请求的等待时长(秒)。
|
|
32
|
-
export const LONG_POLL_WAIT_SEC = 30;
|
|
33
|
-
|
|
34
|
-
// PicoClaw WS 重连退避(毫秒)— 指数退避,上限 30s。
|
|
35
|
-
export const WS_RECONNECT_INITIAL_MS = 1000;
|
|
36
|
-
export const WS_RECONNECT_MAX_MS = 30000;
|
|
37
|
-
|
|
38
|
-
// runtime 名 — 必须与服务端 RuntimeKindPicoclaw 字符串一致。
|
|
39
|
-
export const RUNTIME_NAME = "picoclaw";
|