@tunnelbox/core 0.1.0 → 0.1.7
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 +16 -11
- package/README.zh-CN.md +28 -0
- package/dist/index.js +1630 -0
- package/locales/de-DE.json +161 -0
- package/locales/en-US.json +161 -0
- package/locales/es-ES.json +161 -0
- package/locales/fr-FR.json +161 -0
- package/locales/ja-JP.json +161 -0
- package/locales/ko-KR.json +161 -0
- package/locales/zh-CN.json +161 -0
- package/locales/zh-TW.json +161 -0
- package/package.json +18 -4
- package/src/env.ts +15 -0
- package/src/i18n.ts +114 -0
- package/src/index.ts +11 -9
- package/src/qr.ts +84 -18
- package/src/state.ts +101 -105
- package/src/types.ts +156 -127
package/src/qr.ts
CHANGED
|
@@ -1,42 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 终端二维码与配对信息打印。qrcode
|
|
2
|
+
* 终端二维码与配对信息打印。qrcode 为可选依赖,失败时降级为配对码提示。
|
|
3
|
+
* 输出文案经 core i18n 按语言翻译(见 ./i18n)。
|
|
3
4
|
*/
|
|
4
5
|
import { writePairingFile } from "./state";
|
|
6
|
+
import { normalizeLang, resolveLang, t, type Lang } from "./i18n";
|
|
5
7
|
|
|
8
|
+
/** 加载 qrcode 渲染器(可选依赖,加载失败返回 null)。 */
|
|
9
|
+
async function loadQrcode(): Promise<{
|
|
10
|
+
toString?: (text: string, opts: unknown) => Promise<string>;
|
|
11
|
+
} | null> {
|
|
12
|
+
try {
|
|
13
|
+
const mod = await import("qrcode");
|
|
14
|
+
const render = (mod as { default?: unknown }).default ?? mod;
|
|
15
|
+
const toString = (render as { toString?: (text: string, opts: unknown) => Promise<string> }).toString;
|
|
16
|
+
return toString ? { toString } : null;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 生成适合嵌入聊天/等宽代码块的纯文本二维码(无 ANSI 颜色码,半块字符)。
|
|
24
|
+
* 供手机「扫一扫」直接扫码配对。qrcode 不可用时返回空字符串(由调用方降级)。
|
|
25
|
+
*
|
|
26
|
+
* 注意:聊天区无法像终端 printPairing 那样用 ANSI 背景色“直接涂黑白”,只能靠字符,
|
|
27
|
+
* 因此必须强制**反相**输出——白色模块用实心块字符(渲染成亮前景)、黑色模块留空
|
|
28
|
+
* (呈现深色背景),从而在 opencode 默认的深色 TUI 里保持标准“深码浅底”对比度;
|
|
29
|
+
* 若按常规映射(黑码实心、白码空格)在深色主题下会反相,手机无法扫码。
|
|
30
|
+
*/
|
|
31
|
+
export async function renderPairingQrText(text: string): Promise<string> {
|
|
32
|
+
const render = await loadQrcode();
|
|
33
|
+
if (!render?.toString) return "";
|
|
34
|
+
try {
|
|
35
|
+
const qr = await render.toString(text, {
|
|
36
|
+
type: "utf8",
|
|
37
|
+
margin: 4,
|
|
38
|
+
// 令 qrcode 选用反相块字符表(dark=白/light=黑 → INVERTED_BLOCK_CHAR)
|
|
39
|
+
color: { dark: "#ffffff", light: "#000000" },
|
|
40
|
+
});
|
|
41
|
+
return qr || "";
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 近似显示宽度:ASCII 宽 1,其余按宽字符 2 计(用于跨语言的框线对齐)。 */
|
|
48
|
+
function dispWidth(s: string): number {
|
|
49
|
+
let w = 0;
|
|
50
|
+
for (const ch of s) w += ch.charCodeAt(0) > 0xff ? 2 : 1;
|
|
51
|
+
return w;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function padVisual(s: string, width: number, align: "left" | "center" = "left"): string {
|
|
55
|
+
const cur = dispWidth(s);
|
|
56
|
+
if (cur >= width) return s;
|
|
57
|
+
const pad = width - cur;
|
|
58
|
+
if (align === "center") {
|
|
59
|
+
const left = Math.floor(pad / 2);
|
|
60
|
+
return " ".repeat(left) + s + " ".repeat(pad - left);
|
|
61
|
+
}
|
|
62
|
+
return s + " ".repeat(pad);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 打印配对信息框 + 二维码(二维码内容为配对码本身,供 tunnelbox App「扫一扫」配对)。
|
|
67
|
+
*/
|
|
6
68
|
export async function printPairing(
|
|
7
|
-
relayHttpBase: string,
|
|
8
69
|
code: string,
|
|
9
70
|
type?: string,
|
|
10
71
|
name?: string,
|
|
72
|
+
lang?: Lang,
|
|
11
73
|
): Promise<void> {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
74
|
+
const resolved = lang ? normalizeLang(lang) : resolveLang();
|
|
75
|
+
const file = writePairingFile(code, type);
|
|
76
|
+
const tt = (key: Parameters<typeof t>[1], vars?: Parameters<typeof t>[2]) => t(resolved, key, vars);
|
|
77
|
+
|
|
78
|
+
const title = type ? tt("qr.titleWithType", { type }) : tt("qr.titleNoType");
|
|
79
|
+
const codeLine = `${tt("qr.codeLabel")}: ${code}`;
|
|
80
|
+
const hint = tt("qr.openHint");
|
|
81
|
+
|
|
82
|
+
const inner = Math.max(40, dispWidth(codeLine), dispWidth(title));
|
|
83
|
+
const W = Math.min(76, inner + 4);
|
|
84
|
+
const bar = "─".repeat(W);
|
|
18
85
|
|
|
19
86
|
console.log("");
|
|
20
|
-
console.log("
|
|
21
|
-
console.log(
|
|
22
|
-
console.log("
|
|
23
|
-
console.log(
|
|
24
|
-
console.log(
|
|
25
|
-
console.log("
|
|
26
|
-
console.log(" 手机浏览器打开链接,或打开 App 后手动输入配对码。");
|
|
87
|
+
console.log("┌" + bar + "┐");
|
|
88
|
+
console.log("│ " + padVisual(title, W - 2, "center") + " │");
|
|
89
|
+
console.log("├" + bar + "┤");
|
|
90
|
+
console.log("│ " + padVisual(codeLine, W - 2) + " │");
|
|
91
|
+
console.log("└" + bar + "┘");
|
|
92
|
+
console.log(" " + hint);
|
|
27
93
|
|
|
28
94
|
try {
|
|
29
95
|
const mod = await import("qrcode");
|
|
30
96
|
const render = (mod as { default?: unknown }).default ?? mod;
|
|
31
97
|
const toString = (render as { toString?: (text: string, opts: unknown) => Promise<string> }).toString;
|
|
32
98
|
if (toString) {
|
|
33
|
-
const qr = await toString(
|
|
99
|
+
const qr = await toString(code, { type: "terminal", small: true });
|
|
34
100
|
if (qr) console.log(qr);
|
|
35
101
|
}
|
|
36
102
|
} catch {
|
|
37
|
-
console.log("
|
|
103
|
+
console.log(" " + tt("qr.unavailable"));
|
|
38
104
|
}
|
|
39
105
|
|
|
40
|
-
console.log(
|
|
106
|
+
console.log(" " + tt("qr.fileWritten", { path: file }));
|
|
41
107
|
console.log("");
|
|
42
108
|
}
|
package/src/state.ts
CHANGED
|
@@ -1,105 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 本地状态持久化。默认存于
|
|
3
|
-
* (多 agent 同机时各适配器使用独立文件:remote-state.<type>.json)。
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
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
|
-
return
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
saveState(fallback, type);
|
|
69
|
-
return fallback;
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
/* ignore */
|
|
103
|
-
}
|
|
104
|
-
return p;
|
|
105
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* 本地状态持久化。默认存于 tunnelbox 独立根目录 ~/.tunnelbox;支持按智能体类型隔离文件
|
|
3
|
+
* (多 agent 同机时各适配器使用独立文件:remote-state.<type>.json)。
|
|
4
|
+
*
|
|
5
|
+
* 中继地址不再作为连接选择源:环境默认由构建期经 esbuild `__TB_DEFAULT_RELAY__`
|
|
6
|
+
* 注入(配置来自 plugin/env/.env.*),运行期仅接受 TUNNELBOX_RELAY_URL / 显式配置覆盖。
|
|
7
|
+
*/
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { dirname, join } from "node:path";
|
|
12
|
+
import { normalizeLang, type Lang } from "./i18n";
|
|
13
|
+
|
|
14
|
+
export interface State {
|
|
15
|
+
agentId: string;
|
|
16
|
+
/** 遗留字段(信息用途,不再作为连接选择源)。 */
|
|
17
|
+
relayUrl?: string;
|
|
18
|
+
/** 账号体系(Phase 1):是否已被手机账号认领。认领后不再自动申请配对码。 */
|
|
19
|
+
claimed?: boolean;
|
|
20
|
+
/** 语言(install-local 安装时写入,opencode.json options.language / TUNNELBOX_LANG 可覆盖)。 */
|
|
21
|
+
lang?: Lang;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function baseName(): string {
|
|
25
|
+
return join(homedir(), ".tunnelbox");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function suffix(type?: string): string {
|
|
29
|
+
return type ? `.${type}` : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function statePath(type?: string): string {
|
|
33
|
+
return join(baseName(), `remote-state${suffix(type)}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function saveState(st: State, type?: string): void {
|
|
37
|
+
try {
|
|
38
|
+
const p = statePath(type);
|
|
39
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
40
|
+
writeFileSync(p, JSON.stringify(st, null, 2), "utf8");
|
|
41
|
+
} catch (e) {
|
|
42
|
+
console.error("[tunnelbox] 保存状态失败", e);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 读取状态;不存在时生成新 agentID 并持久化。relayUrl 仅信息用途,不参与连接选择。 */
|
|
47
|
+
export function loadState(type?: string): State {
|
|
48
|
+
const fallback: State = {
|
|
49
|
+
agentId: randomBytes(16).toString("hex"),
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(statePath(type))) {
|
|
53
|
+
saveState(fallback, type);
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
const parsed = JSON.parse(readFileSync(statePath(type), "utf8")) as Partial<State>;
|
|
57
|
+
if (parsed.agentId && parsed.agentId.length >= 16) {
|
|
58
|
+
return {
|
|
59
|
+
agentId: parsed.agentId,
|
|
60
|
+
relayUrl: parsed.relayUrl || "",
|
|
61
|
+
claimed: parsed.claimed === true,
|
|
62
|
+
lang: parsed.lang ? normalizeLang(parsed.lang) : undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
saveState(fallback, type);
|
|
66
|
+
return fallback;
|
|
67
|
+
} catch {
|
|
68
|
+
saveState(fallback, type);
|
|
69
|
+
return fallback;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 把配对码写入本地文件,作为终端日志被吞时的兜底。 */
|
|
74
|
+
export function writePairingFile(code: string, type?: string): string {
|
|
75
|
+
const p = join(baseName(), `remote-pairing${suffix(type)}.txt`);
|
|
76
|
+
try {
|
|
77
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
78
|
+
writeFileSync(p, `配对码: ${code}\n`, "utf8");
|
|
79
|
+
} catch {
|
|
80
|
+
/* ignore */
|
|
81
|
+
}
|
|
82
|
+
return p;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface PairingInfo {
|
|
86
|
+
code: string;
|
|
87
|
+
relayUrl: string;
|
|
88
|
+
at: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 把最新配对信息写入 JSON(供配套 UI/工具读取)。 */
|
|
92
|
+
export function writePairingJson(info: PairingInfo, type?: string): string {
|
|
93
|
+
const p = join(baseName(), `remote-pairing${suffix(type)}.json`);
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
96
|
+
writeFileSync(p, JSON.stringify(info, null, 2), "utf8");
|
|
97
|
+
} catch {
|
|
98
|
+
/* ignore */
|
|
99
|
+
}
|
|
100
|
+
return p;
|
|
101
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,127 +1,156 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tunnelbox 统一协议类型(各智能体适配器共享)。
|
|
3
|
-
* 与 relay/internal/proto、web/src/protocol 通过 JSON 互通(镜像维护)。
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export interface Envelope<T = unknown> {
|
|
7
|
-
id?: string;
|
|
8
|
-
type: string;
|
|
9
|
-
payload: T;
|
|
10
|
-
ts: number;
|
|
11
|
-
/** 中继注入/回填:手机端会话 id,用于回路由 */
|
|
12
|
-
clientID?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function envelope<T>(type: string, payload: T, id?: string): Envelope<T> {
|
|
16
|
-
return { id, type, payload, ts: Date.now() };
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface AgentInfo {
|
|
20
|
-
name: string;
|
|
21
|
-
version: string;
|
|
22
|
-
platform?: string;
|
|
23
|
-
directory?: string;
|
|
24
|
-
/** 智能体类型:opencode | claude-code | codex | dsh | openclaw | hermes | cursor */
|
|
25
|
-
type?: string;
|
|
26
|
-
/** IANA 时区名(如 Asia/Shanghai),仅用于"电脑本地时间"展示 */
|
|
27
|
-
timezone?: string;
|
|
28
|
-
/** 能力位 */
|
|
29
|
-
capabilities?: {
|
|
30
|
-
streaming?: boolean;
|
|
31
|
-
thinking?: boolean;
|
|
32
|
-
permission?: boolean;
|
|
33
|
-
commands?: boolean;
|
|
34
|
-
abort?: boolean;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type SessionStatus = "queued" | "running" | "idle" | "error";
|
|
39
|
-
|
|
40
|
-
export interface SessionInfo {
|
|
41
|
-
id: string;
|
|
42
|
-
title: string;
|
|
43
|
-
created: number;
|
|
44
|
-
updated: number;
|
|
45
|
-
status: SessionStatus;
|
|
46
|
-
/** 会话所在工作区目录(dsh 按 workspace 分目录持久化) */
|
|
47
|
-
workspace?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WorkspaceInfo {
|
|
51
|
-
/** 工作区目录绝对路径 */
|
|
52
|
-
path: string;
|
|
53
|
-
/** 展示名(取目录名) */
|
|
54
|
-
name: string;
|
|
55
|
-
/** 该工作区内的会话数 */
|
|
56
|
-
sessionCount: number;
|
|
57
|
-
/** 最近会话更新时间 */
|
|
58
|
-
updated: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface Part {
|
|
62
|
-
id: string;
|
|
63
|
-
type: "text" | "tool" | "thinking";
|
|
64
|
-
text?: string;
|
|
65
|
-
delta?: string;
|
|
66
|
-
tool?: string;
|
|
67
|
-
args?: string;
|
|
68
|
-
complete?: boolean;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ChatMessage {
|
|
72
|
-
id: string;
|
|
73
|
-
role: "user" | "assistant" | "tool" | "system";
|
|
74
|
-
parts: Part[];
|
|
75
|
-
created: number;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
export
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
1
|
+
/**
|
|
2
|
+
* tunnelbox 统一协议类型(各智能体适配器共享)。
|
|
3
|
+
* 与 relay/internal/proto、web/src/protocol 通过 JSON 互通(镜像维护)。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Envelope<T = unknown> {
|
|
7
|
+
id?: string;
|
|
8
|
+
type: string;
|
|
9
|
+
payload: T;
|
|
10
|
+
ts: number;
|
|
11
|
+
/** 中继注入/回填:手机端会话 id,用于回路由 */
|
|
12
|
+
clientID?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function envelope<T>(type: string, payload: T, id?: string): Envelope<T> {
|
|
16
|
+
return { id, type, payload, ts: Date.now() };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface AgentInfo {
|
|
20
|
+
name: string;
|
|
21
|
+
version: string;
|
|
22
|
+
platform?: string;
|
|
23
|
+
directory?: string;
|
|
24
|
+
/** 智能体类型:opencode | claude-code | codex | dsh | openclaw | hermes | cursor */
|
|
25
|
+
type?: string;
|
|
26
|
+
/** IANA 时区名(如 Asia/Shanghai),仅用于"电脑本地时间"展示 */
|
|
27
|
+
timezone?: string;
|
|
28
|
+
/** 能力位 */
|
|
29
|
+
capabilities?: {
|
|
30
|
+
streaming?: boolean;
|
|
31
|
+
thinking?: boolean;
|
|
32
|
+
permission?: boolean;
|
|
33
|
+
commands?: boolean;
|
|
34
|
+
abort?: boolean;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type SessionStatus = "queued" | "running" | "idle" | "error";
|
|
39
|
+
|
|
40
|
+
export interface SessionInfo {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
created: number;
|
|
44
|
+
updated: number;
|
|
45
|
+
status: SessionStatus;
|
|
46
|
+
/** 会话所在工作区目录(dsh 按 workspace 分目录持久化) */
|
|
47
|
+
workspace?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorkspaceInfo {
|
|
51
|
+
/** 工作区目录绝对路径 */
|
|
52
|
+
path: string;
|
|
53
|
+
/** 展示名(取目录名) */
|
|
54
|
+
name: string;
|
|
55
|
+
/** 该工作区内的会话数 */
|
|
56
|
+
sessionCount: number;
|
|
57
|
+
/** 最近会话更新时间 */
|
|
58
|
+
updated: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Part {
|
|
62
|
+
id: string;
|
|
63
|
+
type: "text" | "tool" | "thinking";
|
|
64
|
+
text?: string;
|
|
65
|
+
delta?: string;
|
|
66
|
+
tool?: string;
|
|
67
|
+
args?: string;
|
|
68
|
+
complete?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ChatMessage {
|
|
72
|
+
id: string;
|
|
73
|
+
role: "user" | "assistant" | "tool" | "system";
|
|
74
|
+
parts: Part[];
|
|
75
|
+
created: number;
|
|
76
|
+
/** 消息对应的模式/agent(opencode: build|plan),用于手机端气泡标注 */
|
|
77
|
+
mode?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 权限请求的作答方式:approval=允许/拒绝/总是允许;choice=从选项中选择;input=输入文本。 */
|
|
81
|
+
export type PermissionKind = "approval" | "choice" | "input";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 审批"允许"的作用域(Hermes 审批门 / claude `canUseTool` 等宿主可能提供多种允许粒度):
|
|
85
|
+
* - once 仅本次允许(默认)
|
|
86
|
+
* - session 本会话内允许
|
|
87
|
+
* - always 总是允许(宿主永久 allowlist)
|
|
88
|
+
* 手机端按请求方提供的 allowed 作用域渲染按钮;仅当 status="allow" 时生效,缺省视为 once。
|
|
89
|
+
*/
|
|
90
|
+
export type PermissionScope = "once" | "session" | "always";
|
|
91
|
+
|
|
92
|
+
export interface PermissionRequest {
|
|
93
|
+
id: string;
|
|
94
|
+
sessionID: string;
|
|
95
|
+
tool: string;
|
|
96
|
+
args: Record<string, unknown>;
|
|
97
|
+
prompt?: string;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
/** 作答方式(缺省视为 approval) */
|
|
100
|
+
kind?: PermissionKind;
|
|
101
|
+
/** choice 类型的可选项(显示为按钮列表) */
|
|
102
|
+
options?: string[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** 手机端对权限请求的应答(permission.reply)。status=allow 时可携带 scope 表达允许作用域。 */
|
|
106
|
+
export interface PermissionReply {
|
|
107
|
+
permissionID: string;
|
|
108
|
+
status: "allow" | "deny";
|
|
109
|
+
/** 允许作用域(status=allow 时生效;缺省 once)。宿主仅接受其提供的 allowed 作用域。 */
|
|
110
|
+
scope?: PermissionScope;
|
|
111
|
+
/** 兼容旧字段:是否总是允许(等价 scope="always")。 */
|
|
112
|
+
always?: boolean;
|
|
113
|
+
/** choice/input 型作答的选中值/输入文本。 */
|
|
114
|
+
value?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 结构化提问(opencode question.v2.asked 等)中的单条问题。 */
|
|
118
|
+
export interface QuestionInfo {
|
|
119
|
+
question: string;
|
|
120
|
+
header?: string;
|
|
121
|
+
options: string[];
|
|
122
|
+
multiple?: boolean;
|
|
123
|
+
custom?: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface QuestionRequest {
|
|
127
|
+
id: string;
|
|
128
|
+
sessionID: string;
|
|
129
|
+
prompt?: string;
|
|
130
|
+
questions: QuestionInfo[];
|
|
131
|
+
createdAt: number;
|
|
132
|
+
/**
|
|
133
|
+
* 会话内容锚点(可选):该提问对应会话里哪条 assistant 消息/哪个 tool-call 部件,
|
|
134
|
+
* 前端可据此把选择卡内联到消息下方渲染(如 dsh 的 ask_user_question / exit_plan_mode)。
|
|
135
|
+
*/
|
|
136
|
+
messageID?: string;
|
|
137
|
+
partID?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface CommandInfo {
|
|
141
|
+
name: string;
|
|
142
|
+
description?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** 账号体系(Phase 1):电脑被手机账号认领后 relay 下发。 */
|
|
146
|
+
export interface AgentClaimedMessage {
|
|
147
|
+
email?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 账号体系(Phase 1):电脑被解绑后 relay 下发,适配器应重置并重新生成 agentID。 */
|
|
151
|
+
export interface AgentRevokedMessage {}
|
|
152
|
+
|
|
153
|
+
/** agent 断开后中继推送给已连接手机(在线信号为 agent.info 广播)。 */
|
|
154
|
+
export interface AgentOfflineMessage {
|
|
155
|
+
agentID?: string;
|
|
156
|
+
}
|