@tunnelbox/dsh-tunnelbox 0.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 ADDED
@@ -0,0 +1,127 @@
1
+ # @tunnelbox/dsh-tunnelbox
2
+
3
+ tunnelbox 的 **DeepSeek Harness(dsh)原生 Cordis 插件**——跑在 dsh 进程内,让手机远程驱动完整的 DeepSeek Harness(agent/session/工具/审批/中止)。
4
+
5
+ ```
6
+ 手机 PWA ──WSS──► relay ──WSS──► 本插件(dsh 进程内) ──ctx.agents/session/approval──► DeepSeek Harness
7
+ (dsh --profile tunnelbox)
8
+ ```
9
+
10
+ ## 与旧实现(plugin/dsh 外部桥)的区别
11
+
12
+ | | 旧:`plugin/dsh`(外部进程桥) | 新:`@tunnelbox/dsh-tunnelbox`(原生插件) |
13
+ |---|---|---|
14
+ | 形态 | `node dist/index.js` 独立进程 + JSON-RPC 驱动 `sdk-minimal` | `dsh --profile tunnelbox` 进程内 Cordis 插件 |
15
+ | 能力位 | streaming/thinking ✅;permission ❌;abort ❌ | **全部 true**(permission/abort 原生支持) |
16
+ | 审批 | 无(sdk-minimal 固定 danger-full-access) | `ctx.approval`(approval/request waterfall)→ 手机 `permission.request` |
17
+ | 中止 | 无 | `agent.cancel({kind:"user"})` |
18
+ | 工具 | 黑盒透传 | `ctx.tools`/`ctx.shell`/`ctx.fs` 等 harness 全部服务 |
19
+ | 配置 | 环境变量 | schemastery Schema + cordis.yml,支持 HMR |
20
+ | 会话 | 直读 JSONL | `ctx.sessions` + 持久化;JSONL 仅做列表/历史 |
21
+
22
+ ## 能力位(agent.info.capabilities)
23
+
24
+ | 能力 | 值 | 实现 |
25
+ |---|---|---|
26
+ | streaming | true | `ctx.on("session/event")` → `message.part`(delta) |
27
+ | thinking | true | `reasoning-delta` → thinking 部件 |
28
+ | permission | **true** | `approval/request` → 手机审批 → `allowed-once/rejected`(超时 fail-closed) |
29
+ | commands | true | 适配器命令 `/new` `/help` |
30
+ | abort | **true** | `agent.cancel` → 前端「停止」 |
31
+
32
+ ## 目录结构
33
+
34
+ ```
35
+ plugin/dsh-tunnelbox/
36
+ ├── package.json # dsh.bundle.patch 声明
37
+ ├── cordis.patch.yml # 插件层(install 时并入 profile)
38
+ ├── scripts/build.mjs # esbuild 打包 → dist/index.mjs(external @deepseek-ai/*)
39
+ └── src/
40
+ ├── index.ts # 插件入口(name/inject/Config/apply)
41
+ ├── bridge.ts # relay 连接 + 协议 ↔ harness 映射 + Agent 生命周期
42
+ ├── map.ts # 会话事件 → Part/ChatMessage 映射(复用旧桥逻辑)
43
+ ├── sessions.ts # JSONL 会话列表/历史/删除
44
+ └── shims.d.ts # @deepseek-ai/* 宽松类型声明
45
+ ```
46
+
47
+ ## 构建
48
+
49
+ ```bash
50
+ cd plugin && npm install # workspaces 根(含 dsh-tunnelbox)
51
+ cd dsh-tunnelbox && npm run build # 产出 dist/index.mjs
52
+ ```
53
+
54
+ ## 安装到 dsh profile
55
+
56
+ ```bash
57
+ dsh plugin --profile tunnelbox add ./plugin/dsh-tunnelbox # pnpm link + 追加 bundles
58
+ dsh --profile tunnelbox --dump-config # 确认 tunnelbox-dsh 层
59
+ ```
60
+
61
+ > profile 目录:`$DSH_HOME/profiles/tunnelbox/`,bundles = `@deepseek-ai/dsh-base` + `@tunnelbox/dsh-tunnelbox`。
62
+
63
+ ## 运行
64
+
65
+ ```bash
66
+ export DSH_HOME=... # dsh home(默认 ~/.dsh,含凭证/会话)
67
+ dsh --profile tunnelbox # 启动 harness + 插件 → 连接中继 → 打印二维码
68
+ ```
69
+
70
+ - 凭证:dsh 的 `credentials` 服务自动读 `$DSH_HOME/.credentials.yaml`(无需手动传 key);
71
+ - 模型/工作目录可在 profile 的 cordis.yml 配置,或环境变量覆盖。
72
+
73
+ ## 配置(Schema)
74
+
75
+ | 字段 | 默认 | 说明 |
76
+ |---|---|---|
77
+ | `relayUrl` | `""`(取 remote-state.dsh.json) | tunnelbox 中继地址 |
78
+ | `provider` | `deepseek-official` | LLM provider |
79
+ | `model` | `deepseek-v4-flash` | 模型 |
80
+ | `cwd` | `process.cwd()` | agent 工作目录(会话按 workspace 分目录持久化) |
81
+
82
+ 示例(profile 的 cordis.patch.yml 覆盖):
83
+
84
+ ```yaml
85
+ - id: tunnelbox-dsh
86
+ config:
87
+ relayUrl: 'wss://chat.example.com'
88
+ model: 'deepseek-v4-flash'
89
+ cwd: 'D:/workspace/project'
90
+ ```
91
+
92
+ ## 协议映射(与 adapter 接口规范 v0.2 一致)
93
+
94
+ | 手机消息 | 处理 |
95
+ |---|---|
96
+ | `session.prompt` | `ensureAgent(sessionID)`(无文件则 create / 有则 resume)→ `agent.followup(createUserMessage(text))` |
97
+ | `session.messages` | 活动会话:`eventsToMessages(session.events)`;否则读 JSONL |
98
+ | `session.list` | JSONL 扫描 + 活动会话合并 |
99
+ | `session.create` | 返回占位(首次 prompt 落盘) |
100
+ | `session.delete` | `handle.dispose()` + 删 JSONL |
101
+ | `session.abort` | `agent.cancel({kind:"user"})` |
102
+ | `permission.reply` | 解析 pending 审批 → `allowed-once/rejected` + 回执 `permission.replied` |
103
+ | 流式 | `session/event` → `streamEventTo` → `message.part` |
104
+
105
+ ## 安全
106
+
107
+ - 审批策略由 `@deepseek-ai/dsh-base` 的 `approval`/`sandbox-policy` 决定(默认 `workspace-write` + `ask`);
108
+ 如需全放开:`DSH_PERMISSION_MODE=danger-full-access`(此时审批 policy 为 `never`,手机端不再弹卡)。
109
+ - 手机端发 `session.prompt` 即驱动 dsh 在配置的 `cwd` 内工作,请勿把敏感工程目录暴露给不受信手机。
110
+
111
+ ## 备注
112
+
113
+ - 旧 `plugin/dsh`(sdk-minimal 外部桥)已删除,本插件是唯一的 dsh 集成方式;
114
+ - 若 dsh 线协议/事件结构变化,仅需升级 `@tunnelbox/dsh-tunnelbox`(依赖 harness 原生事件,天然跟随 dsh 版本)。
115
+
116
+ ## 多 agent 隔离
117
+
118
+ 每个 `dsh --profile <name>` 是独立进程,天然隔离(不同 profile 可配不同 cwd/模型/工具集/审批策略)。同机跑多个:
119
+
120
+ ```bash
121
+ # 实例 A:工程目录 + workspace-write 审批
122
+ DSH_HOME=~/.dsh-a dsh --profile tunnelbox --patch ./a.patch.yml
123
+ # 实例 B:另一工程目录 + danger-full-access
124
+ DSH_HOME=~/.dsh-b DSH_PERMISSION_MODE=danger-full-access dsh --profile tunnelbox --patch ./b.patch.yml
125
+ ```
126
+
127
+ 手机端每个实例按 `type=dsh` + 独立 agentID(各自 `remote-state.dsh.json`)区分。
@@ -0,0 +1,3 @@
1
+ - insert:
2
+ - id: tunnelbox-dsh
3
+ name: '@tunnelbox/dsh-tunnelbox'
package/dist/index.mjs ADDED
@@ -0,0 +1,1003 @@
1
+ // tunnelbox dsh plugin — built by scripts/build.mjs
2
+
3
+ // src/index.ts
4
+ import Schema from "@deepseek-ai/schemastery";
5
+
6
+ // src/bridge.ts
7
+ import { randomBytes as randomBytes2 } from "node:crypto";
8
+ import { hostname } from "node:os";
9
+
10
+ // ../core/src/types.ts
11
+ function envelope(type, payload, id) {
12
+ return { id, type, payload, ts: Date.now() };
13
+ }
14
+
15
+ // ../core/src/relay.ts
16
+ var BASE_DELAY = 1e3;
17
+ var MAX_DELAY = 15e3;
18
+ var RelayClient = class {
19
+ ws = null;
20
+ url = "";
21
+ headers = {};
22
+ handlers = null;
23
+ timer = null;
24
+ retry = 0;
25
+ stopped = false;
26
+ connected = false;
27
+ get isConnected() {
28
+ return this.connected;
29
+ }
30
+ connect(url, headers, handlers) {
31
+ this.url = url;
32
+ this.headers = headers;
33
+ this.handlers = handlers;
34
+ this.stopped = false;
35
+ this.retry = 0;
36
+ this.open();
37
+ }
38
+ send(env) {
39
+ if (this.ws && this.connected) {
40
+ try {
41
+ this.ws.send(JSON.stringify(env));
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+ return false;
48
+ }
49
+ close() {
50
+ this.stopped = true;
51
+ if (this.timer) clearTimeout(this.timer);
52
+ this.timer = null;
53
+ if (this.ws) {
54
+ try {
55
+ this.ws.close();
56
+ } catch {
57
+ }
58
+ }
59
+ this.ws = null;
60
+ this.connected = false;
61
+ }
62
+ open() {
63
+ if (this.stopped) return;
64
+ try {
65
+ const ws = new WebSocket(this.url, { headers: this.headers });
66
+ this.ws = ws;
67
+ ws.onopen = () => {
68
+ this.connected = true;
69
+ this.retry = 0;
70
+ this.handlers?.onOpen();
71
+ };
72
+ ws.onmessage = (ev) => {
73
+ try {
74
+ const msg = JSON.parse(String(ev.data));
75
+ this.handlers?.onMessage(msg);
76
+ } catch {
77
+ }
78
+ };
79
+ ws.onerror = (e) => this.handlers?.onError(e);
80
+ ws.onclose = () => {
81
+ this.connected = false;
82
+ this.ws = null;
83
+ this.handlers?.onClose();
84
+ this.scheduleReconnect();
85
+ };
86
+ } catch (e) {
87
+ this.handlers?.onError(e);
88
+ this.scheduleReconnect();
89
+ }
90
+ }
91
+ scheduleReconnect() {
92
+ if (this.stopped) return;
93
+ const delay = Math.min(BASE_DELAY * 2 ** this.retry, MAX_DELAY);
94
+ this.retry++;
95
+ this.timer = setTimeout(() => this.open(), delay);
96
+ }
97
+ };
98
+
99
+ // ../core/src/state.ts
100
+ import { randomBytes } from "node:crypto";
101
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
102
+ import { homedir } from "node:os";
103
+ import { dirname, join } from "node:path";
104
+ var DEFAULT_RELAY_URL = process.env.TUNNELBOX_RELAY_URL || (process.env.NODE_ENV === "production" ? "wss://chat.wxngrok.com" : "ws://127.0.0.1:8080");
105
+ function baseName() {
106
+ return join(homedir(), ".config", "opencode");
107
+ }
108
+ function suffix(type) {
109
+ return type ? `.${type}` : "";
110
+ }
111
+ function statePath(type) {
112
+ return join(baseName(), `remote-state${suffix(type)}.json`);
113
+ }
114
+ function saveState(st, type) {
115
+ try {
116
+ const p = statePath(type);
117
+ mkdirSync(dirname(p), { recursive: true });
118
+ writeFileSync(p, JSON.stringify(st, null, 2), "utf8");
119
+ } catch (e) {
120
+ console.error("[tunnelbox] \u4FDD\u5B58\u72B6\u6001\u5931\u8D25", e);
121
+ }
122
+ }
123
+ function loadState(type) {
124
+ const fallback = {
125
+ agentId: randomBytes(16).toString("hex"),
126
+ relayUrl: DEFAULT_RELAY_URL
127
+ };
128
+ try {
129
+ if (!existsSync(statePath(type))) {
130
+ saveState(fallback, type);
131
+ return fallback;
132
+ }
133
+ const parsed = JSON.parse(readFileSync(statePath(type), "utf8"));
134
+ if (parsed.agentId && parsed.agentId.length >= 16) {
135
+ return {
136
+ agentId: parsed.agentId,
137
+ relayUrl: parsed.relayUrl || DEFAULT_RELAY_URL,
138
+ claimed: parsed.claimed === true
139
+ };
140
+ }
141
+ saveState(fallback, type);
142
+ return fallback;
143
+ } catch {
144
+ saveState(fallback, type);
145
+ return fallback;
146
+ }
147
+ }
148
+ function writePairingFile(link, code, type) {
149
+ const p = join(baseName(), `remote-pairing${suffix(type)}.txt`);
150
+ try {
151
+ mkdirSync(dirname(p), { recursive: true });
152
+ writeFileSync(p, `\u914D\u5BF9\u7801: ${code}
153
+ \u914D\u5BF9\u94FE\u63A5: ${link}
154
+ `, "utf8");
155
+ } catch {
156
+ }
157
+ return p;
158
+ }
159
+ function writePairingJson(info, type) {
160
+ const p = join(baseName(), `remote-pairing${suffix(type)}.json`);
161
+ try {
162
+ mkdirSync(dirname(p), { recursive: true });
163
+ writeFileSync(p, JSON.stringify(info, null, 2), "utf8");
164
+ } catch {
165
+ }
166
+ return p;
167
+ }
168
+
169
+ // ../core/src/qr.ts
170
+ async function printPairing(relayHttpBase, code, type, name2) {
171
+ const p = new URLSearchParams({ code });
172
+ if (type) p.set("type", type);
173
+ if (name2) p.set("name", name2);
174
+ const link = `${relayHttpBase}/app/#/pages/index/index?${p.toString()}`;
175
+ const file = writePairingFile(link, code, type);
176
+ const tag = type ? ` ${type}` : "";
177
+ console.log("");
178
+ console.log("\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
179
+ console.log(`\u2502 tunnelbox${tag} \u624B\u673A\u914D\u5BF9 \u2502`);
180
+ console.log("\u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
181
+ console.log(`\u2502 \u914D\u5BF9\u7801: ${code.padEnd(42)} \u2502`);
182
+ console.log(`\u2502 \u94FE\u63A5 : ${link.padEnd(42)} \u2502`);
183
+ console.log("\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
184
+ console.log(" \u624B\u673A\u6D4F\u89C8\u5668\u6253\u5F00\u94FE\u63A5\uFF0C\u6216\u6253\u5F00 App \u540E\u624B\u52A8\u8F93\u5165\u914D\u5BF9\u7801\u3002");
185
+ try {
186
+ const mod = await import("qrcode");
187
+ const render = mod.default ?? mod;
188
+ const toString = render.toString;
189
+ if (toString) {
190
+ const qr = await toString(link, { type: "terminal", small: true });
191
+ if (qr) console.log(qr);
192
+ }
193
+ } catch {
194
+ console.log(" \uFF08\u672A\u5B89\u88C5 qrcode\uFF0C\u8BF7\u4F7F\u7528\u4E0A\u65B9\u94FE\u63A5\u6216\u914D\u5BF9\u7801\uFF09");
195
+ }
196
+ console.log(` \u914D\u5BF9\u4FE1\u606F\u5DF2\u5199\u5165: ${file}`);
197
+ console.log("");
198
+ }
199
+
200
+ // src/bridge.ts
201
+ import { installModelSelection } from "@deepseek-ai/dsh-agent";
202
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
203
+ import { SessionId } from "@deepseek-ai/dsh-session";
204
+
205
+ // src/map.ts
206
+ function asMessage(v) {
207
+ if (!v || typeof v !== "object") return null;
208
+ const o = v;
209
+ if (typeof o.role !== "string" || !Array.isArray(o.content)) return null;
210
+ return o;
211
+ }
212
+ function asBlock(v) {
213
+ if (!v || typeof v !== "object") return null;
214
+ const o = v;
215
+ if (typeof o.type !== "string") return null;
216
+ return o;
217
+ }
218
+ function mapBlock(block) {
219
+ switch (block.type) {
220
+ case "text":
221
+ return { id: block.id || "t", type: "text", text: block.text || flattenBlocks(block.content) || "" };
222
+ case "reasoning":
223
+ return { id: block.id || "r", type: "thinking", text: block.text || flattenBlocks(block.content) || "" };
224
+ case "tool-call": {
225
+ let args = block.arguments || "";
226
+ if (args && !args.startsWith("{")) {
227
+ try {
228
+ args = JSON.stringify(JSON.parse(args));
229
+ } catch {
230
+ }
231
+ }
232
+ return { id: block.id || "tool", type: "tool", tool: block.name || "tool", args };
233
+ }
234
+ case "tool-result":
235
+ return { id: block.id || "t", type: "text", text: flattenBlocks(block.content) };
236
+ default:
237
+ return null;
238
+ }
239
+ }
240
+ function flattenBlocks(blocks) {
241
+ if (!blocks) return "";
242
+ const out = [];
243
+ for (const b of blocks) {
244
+ const blk = asBlock(b);
245
+ if (!blk) continue;
246
+ if (blk.type === "text" && typeof blk.text === "string") out.push(blk.text);
247
+ else if (blk.type === "tool-result") out.push(flattenBlocks(blk.content));
248
+ }
249
+ return out.join("\n");
250
+ }
251
+ function mapParts(blocks) {
252
+ const parts = [];
253
+ for (const raw of blocks ?? []) {
254
+ const blk = asBlock(raw);
255
+ if (!blk) continue;
256
+ const p = mapBlock(blk);
257
+ if (p) parts.push(p);
258
+ }
259
+ return parts;
260
+ }
261
+ function mapMessage(msg) {
262
+ const role = msg.role === "user" || msg.role === "assistant" || msg.role === "system" ? msg.role : "assistant";
263
+ return {
264
+ id: msg.id || `m-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
265
+ role,
266
+ parts: mapParts(msg.content),
267
+ created: msg.created ?? Date.now()
268
+ };
269
+ }
270
+ function extractMessages(event) {
271
+ if (!event || typeof event !== "object") return [];
272
+ const o = event;
273
+ if (asMessage(o)) return [o];
274
+ if (typeof o.type === "string" && o.data && typeof o.data === "object") {
275
+ const data = o.data;
276
+ const out = [];
277
+ const push = (v) => {
278
+ const m = asMessage(v);
279
+ if (m) out.push(m);
280
+ else if (v && typeof v === "object") out.push(...extractMessages(v));
281
+ };
282
+ if (data.message !== void 0) push(data.message);
283
+ if (Array.isArray(data.inserted)) data.inserted.forEach(push);
284
+ if (Array.isArray(data.messages)) data.messages.forEach(push);
285
+ if (o.type === "user/message" || o.type === "assistant/message") {
286
+ const role = o.type.startsWith("assistant") ? "assistant" : "user";
287
+ const m = asMessage({ role, ...data });
288
+ if (m) out.push(m);
289
+ }
290
+ return out;
291
+ }
292
+ if (Array.isArray(o.messages)) {
293
+ const out = [];
294
+ for (const m of o.messages) {
295
+ const mm = asMessage(m);
296
+ if (mm) out.push(mm);
297
+ }
298
+ return out;
299
+ }
300
+ if (Array.isArray(o.events)) {
301
+ const out = [];
302
+ for (const e of o.events) out.push(...extractMessages(e));
303
+ return out;
304
+ }
305
+ return [];
306
+ }
307
+ function eventsToMessages(events) {
308
+ const seen = /* @__PURE__ */ new Set();
309
+ const out = [];
310
+ for (const ev of events) {
311
+ for (const m of extractMessages(ev)) {
312
+ if (m.id && seen.has(m.id)) continue;
313
+ if (m.id) seen.add(m.id);
314
+ out.push(mapMessage(m));
315
+ }
316
+ }
317
+ return out;
318
+ }
319
+ function toolCallFrom(block) {
320
+ let args = {};
321
+ const raw = block.arguments;
322
+ if (typeof raw === "string" && raw.trim()) {
323
+ try {
324
+ const parsed = JSON.parse(raw);
325
+ if (parsed && typeof parsed === "object") args = parsed;
326
+ } catch {
327
+ }
328
+ }
329
+ return { name: String(block.name ?? ""), args };
330
+ }
331
+ function findToolCall(events, callId) {
332
+ for (const raw of events) {
333
+ const e = raw ?? {};
334
+ if (typeof e.type !== "string" || !e.data || typeof e.data !== "object") continue;
335
+ const data = e.data;
336
+ if (e.type === "assistant/chunk") {
337
+ const chunk = data.chunk;
338
+ if (!chunk || typeof chunk !== "object") continue;
339
+ const block = chunk.block;
340
+ if (chunk.type === "block-end" && block && block.type === "tool-call" && block.id === callId) {
341
+ return toolCallFrom(block);
342
+ }
343
+ }
344
+ if (e.type === "assistant/message") {
345
+ const msg = data.message ?? data;
346
+ if (!msg || !Array.isArray(msg.content)) continue;
347
+ for (const b of msg.content) {
348
+ const block = b;
349
+ if (block && block.type === "tool-call" && block.id === callId) {
350
+ return toolCallFrom(block);
351
+ }
352
+ }
353
+ }
354
+ }
355
+ return null;
356
+ }
357
+ function sessionTitle(messages) {
358
+ for (const m of messages) {
359
+ if (m.role !== "user") continue;
360
+ const text = (m.content ?? []).map((b) => asBlock(b)?.type === "text" ? String(asBlock(b)?.text ?? "") : "").join(" ").trim();
361
+ if (text) return text.length > 30 ? text.slice(0, 30) + "\u2026" : text;
362
+ }
363
+ return "";
364
+ }
365
+ function streamEventTo(sessionID, event, emit, streamed) {
366
+ const e = event ?? {};
367
+ if (typeof e.type !== "string" || !e.data || typeof e.data !== "object") return;
368
+ const data = e.data;
369
+ const turn = typeof data.turn === "number" ? data.turn : Date.now();
370
+ const mid = `m-${sessionID}-${turn}`;
371
+ const key = `${sessionID}:${turn}`;
372
+ switch (e.type) {
373
+ case "assistant/chunk": {
374
+ const chunk = data.chunk ?? {};
375
+ const t = chunk.type || "";
376
+ if (t === "finish") {
377
+ if (chunk.reason?.kind === "error") {
378
+ streamed.add(key);
379
+ const msg = chunk.reason.failure?.message || chunk.reason.error?.message || "dsh \u751F\u6210\u51FA\u9519";
380
+ emit(sessionID, mid, { id: "err", type: "text", text: "[dsh] " + msg, complete: true });
381
+ }
382
+ return;
383
+ }
384
+ if (t === "text-delta" && typeof chunk.text === "string") {
385
+ streamed.add(key);
386
+ emit(sessionID, mid, { id: `t${chunk.index ?? 0}`, type: "text", delta: chunk.text });
387
+ return;
388
+ }
389
+ if (t === "reasoning-delta" && typeof chunk.text === "string") {
390
+ streamed.add(key);
391
+ emit(sessionID, mid, { id: `r${chunk.index ?? 0}`, type: "thinking", delta: chunk.text });
392
+ return;
393
+ }
394
+ const part = mapBlock(chunk);
395
+ if (part) {
396
+ streamed.add(key);
397
+ emit(sessionID, mid, part);
398
+ }
399
+ return;
400
+ }
401
+ case "assistant/message": {
402
+ if (streamed.has(key)) return;
403
+ const m = data.message ?? data;
404
+ if (!m || typeof m !== "object") return;
405
+ for (const part of mapParts(m.content)) {
406
+ emit(sessionID, m.id || mid, part);
407
+ }
408
+ return;
409
+ }
410
+ case "agent/inbox/spliced": {
411
+ for (const m of extractMessages(e)) {
412
+ if (m.role !== "assistant") continue;
413
+ if (streamed.has(key)) return;
414
+ for (const part of mapParts(m.content)) emit(sessionID, m.id || mid, part);
415
+ }
416
+ return;
417
+ }
418
+ case "turn/end": {
419
+ const reason = data.reason ?? {};
420
+ if (reason.kind === "error" && reason.error?.message && !streamed.has(key)) {
421
+ streamed.add(key);
422
+ emit(sessionID, mid, { id: "err", type: "text", text: "[dsh] " + reason.error.message, complete: true });
423
+ }
424
+ return;
425
+ }
426
+ default:
427
+ return;
428
+ }
429
+ }
430
+
431
+ // src/sessions.ts
432
+ import { readdirSync, rmSync, statSync } from "node:fs";
433
+ import { homedir as homedir2 } from "node:os";
434
+ import { join as join2 } from "node:path";
435
+ function dshHome() {
436
+ return process.env.DSH_HOME || join2(homedir2(), ".dsh");
437
+ }
438
+ function release(prep) {
439
+ const p = prep;
440
+ const d = typeof p?.dispose === "function" ? p.dispose : typeof p?.[Symbol.dispose] === "function" ? p[Symbol.dispose] : void 0;
441
+ if (typeof d === "function") {
442
+ try {
443
+ d.call(p);
444
+ } catch {
445
+ }
446
+ }
447
+ }
448
+ function basename(p) {
449
+ return p.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? p;
450
+ }
451
+ function titleOf(events) {
452
+ for (const raw of events) {
453
+ const e = raw ?? {};
454
+ if (e.type === "session/title" && typeof e.data?.title === "string" && e.data.title) return e.data.title;
455
+ }
456
+ const msgs = events.flatMap((e) => extractMessages(e));
457
+ return sessionTitle(msgs) || "";
458
+ }
459
+ async function readSessionEvents(sp, sessionId) {
460
+ const prep = await sp.prepare(sessionId);
461
+ try {
462
+ return [...prep.session.events];
463
+ } finally {
464
+ release(prep);
465
+ }
466
+ }
467
+ async function sessionExists(sp, sessionId) {
468
+ const headers = await sp.list().catch(() => []);
469
+ return headers.some((h) => h.id === sessionId);
470
+ }
471
+ async function sessionInfo(sp, h) {
472
+ let events = [];
473
+ let updated = h.createdAt;
474
+ try {
475
+ const prep = await sp.prepare(h.id);
476
+ events = [...prep.session.events];
477
+ release(prep);
478
+ const last = events[events.length - 1];
479
+ if (typeof last?.time === "number") updated = last.time;
480
+ } catch {
481
+ }
482
+ return {
483
+ id: h.id,
484
+ title: titleOf(events) || "dsh \u4F1A\u8BDD",
485
+ created: h.createdAt,
486
+ updated,
487
+ status: "idle",
488
+ workspace: h.cwd
489
+ };
490
+ }
491
+ async function listSessions(sp, workspace) {
492
+ const headers = await sp.list().catch(() => []);
493
+ const out = [];
494
+ for (const h of headers) {
495
+ if (workspace && h.cwd !== workspace) continue;
496
+ const s = await sessionInfo(sp, h);
497
+ if (s) out.push(s);
498
+ }
499
+ return out.sort((a, b) => b.updated - a.updated);
500
+ }
501
+ async function listWorkspaces(sp, current) {
502
+ const headers = await sp.list().catch(() => []);
503
+ const byPath = /* @__PURE__ */ new Map();
504
+ for (const h of headers) {
505
+ const p = h.cwd;
506
+ if (!p) continue;
507
+ const e = byPath.get(p) ?? { count: 0, updated: 0 };
508
+ e.count++;
509
+ if (h.createdAt > e.updated) e.updated = h.createdAt;
510
+ byPath.set(p, e);
511
+ }
512
+ if (current && !byPath.has(current)) byPath.set(current, { count: 0, updated: 0 });
513
+ return [...byPath.entries()].map(([path, e]) => ({
514
+ path,
515
+ name: basename(path) || path,
516
+ sessionCount: e.count,
517
+ updated: e.updated
518
+ })).sort((a, b) => b.updated - a.updated);
519
+ }
520
+ function deleteSessionDir(home, sessionId) {
521
+ const root = join2(home, "sessions");
522
+ const target = findDir(root, sessionId);
523
+ if (!target) return false;
524
+ try {
525
+ rmSync(target, { recursive: true, force: true });
526
+ return true;
527
+ } catch {
528
+ return false;
529
+ }
530
+ }
531
+ function findDir(dir, name2) {
532
+ let entries;
533
+ try {
534
+ entries = readdirSync(dir);
535
+ } catch {
536
+ return null;
537
+ }
538
+ for (const n of entries) {
539
+ const p = join2(dir, n);
540
+ try {
541
+ const st = statSync(p);
542
+ if (st.isDirectory()) {
543
+ if (n === name2) return p;
544
+ const hit = findDir(p, name2);
545
+ if (hit) return hit;
546
+ }
547
+ } catch {
548
+ }
549
+ }
550
+ return null;
551
+ }
552
+
553
+ // src/bridge.ts
554
+ var VERSION = "0.2.0";
555
+ var AGENT_TYPE = "dsh";
556
+ var APPROVAL_TIMEOUT_MS = 12e4;
557
+ var ADAPTER_COMMANDS = [
558
+ { name: "new", description: "\u65B0\u5EFA\u4F1A\u8BDD" },
559
+ { name: "help", description: "\u663E\u793A\u5E2E\u52A9" }
560
+ ];
561
+ var TunnelboxBridge = class {
562
+ constructor(ctx, cfg) {
563
+ this.ctx = ctx;
564
+ this.cfg = cfg;
565
+ this.currentWorkspace = cfg.cwd || process.cwd();
566
+ this.registerListeners();
567
+ }
568
+ ws = new RelayClient();
569
+ state = loadState(AGENT_TYPE);
570
+ claimed = !!this.state.claimed;
571
+ everConnected = false;
572
+ relayBase = "";
573
+ /** sessionID → 活动 AgentHandle(本进程生命周期)。 */
574
+ handles = /* @__PURE__ */ new Map();
575
+ streamedTurns = /* @__PURE__ */ new Set();
576
+ approvals = /* @__PURE__ */ new Map();
577
+ approvalSeq = 0;
578
+ /** 当前工作区(新会话/新 agent 的 cwd;默认配置 cwd)。 */
579
+ currentWorkspace = "";
580
+ /** session.create 指定工作区但尚未建 agent 的暂存(sessionID → workspace)。 */
581
+ pendingWorkspaces = /* @__PURE__ */ new Map();
582
+ registerListeners() {
583
+ this.ctx.on("session/event", (session, event) => this.onSessionEvent(session.id, event));
584
+ this.ctx.on("approval/request", (req, next) => this.onApproval(req, next));
585
+ }
586
+ get home() {
587
+ return dshHome();
588
+ }
589
+ get relayHttpBase() {
590
+ return this.relayBase.replace(/^ws/, "http").replace(/\/+$/, "");
591
+ }
592
+ async start() {
593
+ await this.ctx.get("loader")?.await();
594
+ const url = (this.cfg.relayUrl || this.state.relayUrl || "").replace(/\/+$/, "");
595
+ this.relayBase = url;
596
+ this.connect(url);
597
+ }
598
+ dispose() {
599
+ this.ws.close();
600
+ for (const h of this.handles.values()) void h.dispose().catch(() => {
601
+ });
602
+ this.handles.clear();
603
+ }
604
+ log(msg) {
605
+ console.log(`[tunnelbox:dsh] ${msg}`);
606
+ }
607
+ send(e) {
608
+ return this.ws.send(e);
609
+ }
610
+ reply(src, type, payload) {
611
+ const msg = envelope(type, payload, src.id);
612
+ msg.clientID = src.clientID;
613
+ this.send(msg);
614
+ }
615
+ pairingLink(code) {
616
+ const p = new URLSearchParams({ code, type: AGENT_TYPE, name: hostname() });
617
+ return `${this.relayHttpBase}/app/#/pages/index/index?${p.toString()}`;
618
+ }
619
+ connect(url) {
620
+ const agentID = this.state.agentId;
621
+ const wsUrl = `${url}/ws/agent?token=${encodeURIComponent(agentID)}`;
622
+ this.ws.connect(
623
+ wsUrl,
624
+ { Authorization: `Bearer ${agentID}` },
625
+ {
626
+ onOpen: () => {
627
+ this.everConnected = true;
628
+ this.log("\u5DF2\u8FDE\u63A5\u4E2D\u7EE7 " + url);
629
+ this.announce();
630
+ },
631
+ onMessage: (env) => {
632
+ void this.onMessage(env).catch((e) => this.log(`\u6D88\u606F\u5904\u7406\u672A\u6355\u83B7\u5F02\u5E38: ${e.message}`));
633
+ },
634
+ onClose: () => {
635
+ if (this.everConnected) {
636
+ this.everConnected = false;
637
+ this.log("\u4E0E\u4E2D\u7EE7\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\u2026");
638
+ }
639
+ },
640
+ onError: () => {
641
+ }
642
+ }
643
+ );
644
+ }
645
+ announce() {
646
+ const info = {
647
+ name: hostname(),
648
+ version: VERSION,
649
+ type: AGENT_TYPE,
650
+ capabilities: {
651
+ streaming: true,
652
+ thinking: true,
653
+ permission: true,
654
+ // 原生审批(approval/request → 手机)
655
+ commands: true,
656
+ abort: true
657
+ // agent.cancel
658
+ },
659
+ platform: process.platform,
660
+ directory: this.currentWorkspace,
661
+ timezone: localTimezone()
662
+ };
663
+ this.send(envelope("agent.info", info));
664
+ if (!this.claimed) this.send(envelope("pair.create", {}));
665
+ }
666
+ getSessionPersistence() {
667
+ return this.ctx.get("sessionPersistence");
668
+ }
669
+ async onMessage(env) {
670
+ try {
671
+ switch (env.type) {
672
+ case "pair.created": {
673
+ const code = env.payload.code;
674
+ const link = this.pairingLink(code);
675
+ writePairingJson({ code, link, relayUrl: this.relayBase, at: Date.now() }, AGENT_TYPE);
676
+ this.log("\u751F\u6210\u914D\u5BF9\u7801: " + code);
677
+ void printPairing(this.relayHttpBase, code, AGENT_TYPE, hostname());
678
+ return;
679
+ }
680
+ case "agent.claimed":
681
+ this.claimed = true;
682
+ this.state.claimed = true;
683
+ saveState(this.state, AGENT_TYPE);
684
+ this.log("\u5DF2\u7ED1\u5B9A\u5230\u624B\u673A\u8D26\u53F7\uFF0C\u4E4B\u540E\u624B\u673A\u4ECE\u300C\u6211\u7684\u7535\u8111\u300D\u76F4\u63A5\u8FDE\u63A5");
685
+ return;
686
+ case "agent.revoked":
687
+ this.resetIdentity();
688
+ return;
689
+ case "session.list":
690
+ return this.sessionList(env);
691
+ case "session.create":
692
+ return this.sessionCreate(env);
693
+ case "session.messages":
694
+ return this.sessionMessages(env);
695
+ case "session.prompt":
696
+ return this.sessionPrompt(env);
697
+ case "session.delete":
698
+ return this.sessionDelete(env);
699
+ case "session.abort":
700
+ return this.sessionAbort(env);
701
+ case "command.list":
702
+ return this.commandList(env);
703
+ case "workspace.list":
704
+ return this.workspaceList(env);
705
+ case "workspace.set":
706
+ return this.workspaceSet(env);
707
+ case "permission.reply":
708
+ return this.permissionReply(env);
709
+ case "error":
710
+ return;
711
+ default:
712
+ this.log("\u672A\u5904\u7406\u7684\u6D88\u606F\u7C7B\u578B: " + env.type);
713
+ }
714
+ } catch (e) {
715
+ this.log(`\u5904\u7406 ${env.type} \u5931\u8D25: ${e.message}`);
716
+ this.reply(env, "error", { code: "HANDLER_ERROR", message: e.message, id: env.id });
717
+ }
718
+ }
719
+ // ---- 会话 ----
720
+ async sessionList(env) {
721
+ const sp = this.getSessionPersistence();
722
+ const payload = env.payload;
723
+ const sessions = /* @__PURE__ */ new Map();
724
+ if (sp) {
725
+ for (const s of await listSessions(sp, payload?.workspace)) sessions.set(s.id, s);
726
+ }
727
+ for (const [id, h] of this.handles) {
728
+ const events = [...h.agent.session.events];
729
+ const msgs = eventsToMessages(events);
730
+ const text = msgs.find((m) => m.role === "user")?.parts.map((p) => p.type === "text" ? p.text || "" : "").join(" ").trim();
731
+ const last = events[events.length - 1];
732
+ sessions.set(id, {
733
+ id,
734
+ title: text ? text.length > 30 ? text.slice(0, 30) + "\u2026" : text : "\u65B0\u4F1A\u8BDD",
735
+ created: events[0]?.time ?? Date.now(),
736
+ updated: last?.time ?? Date.now(),
737
+ status: "idle",
738
+ workspace: this.currentWorkspace
739
+ });
740
+ }
741
+ const list = [...sessions.values()].sort((a, b) => b.updated - a.updated);
742
+ this.reply(env, "sessions", { sessions: list });
743
+ }
744
+ async sessionCreate(env) {
745
+ const payload = env.payload;
746
+ const workspace = payload?.workspace || this.currentWorkspace;
747
+ const id = `s-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
748
+ const now = Date.now();
749
+ this.pendingWorkspaces.set(id, workspace);
750
+ this.reply(env, "session.created", {
751
+ session: { id, title: "\u65B0\u4F1A\u8BDD", created: now, updated: now, status: "idle", workspace }
752
+ });
753
+ }
754
+ async sessionMessages(env) {
755
+ const { sessionID } = env.payload;
756
+ let messages = [];
757
+ const h = this.handles.get(sessionID);
758
+ if (h) {
759
+ messages = eventsToMessages([...h.agent.session.events]);
760
+ } else {
761
+ const sp = this.getSessionPersistence();
762
+ if (sp) {
763
+ try {
764
+ if (await sessionExists(sp, sessionID)) {
765
+ messages = eventsToMessages(await readSessionEvents(sp, sessionID));
766
+ }
767
+ } catch (e) {
768
+ this.log(`\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25(${sessionID}): ${e.message}`);
769
+ }
770
+ }
771
+ }
772
+ this.reply(env, "messages", { sessionID, messages });
773
+ }
774
+ async sessionPrompt(env) {
775
+ const payload = env.payload;
776
+ const text = (payload.text || "").trim();
777
+ if (!text) return;
778
+ if (text.startsWith("/new")) return this.sessionCreate(env);
779
+ if (text.startsWith("/help")) {
780
+ this.reply(env, "message.part", {
781
+ sessionID: payload.sessionID,
782
+ messageID: `m-${Date.now()}`,
783
+ part: { id: "t", type: "text", text: "\u652F\u6301\u7684\u547D\u4EE4\uFF1A\n- /new \u65B0\u5EFA\u4F1A\u8BDD\n- /help \u663E\u793A\u5E2E\u52A9", complete: true }
784
+ });
785
+ return;
786
+ }
787
+ const handle = await this.ensureAgent(payload.sessionID);
788
+ handle.agent.followup(
789
+ createUserMessage({ content: [{ type: "text", text }], source: { kind: "user" } })
790
+ );
791
+ this.log(`\u5DF2\u53D1\u9001\u5230 dsh (session=${payload.sessionID})`);
792
+ }
793
+ async sessionDelete(env) {
794
+ const { sessionID } = env.payload;
795
+ const h = this.handles.get(sessionID);
796
+ if (h) {
797
+ await h.dispose().catch(() => {
798
+ });
799
+ this.handles.delete(sessionID);
800
+ }
801
+ this.pendingWorkspaces.delete(sessionID);
802
+ deleteSessionDir(this.home, sessionID);
803
+ this.reply(env, "session.deleted", { sessionID });
804
+ }
805
+ async sessionAbort(env) {
806
+ const { sessionID } = env.payload;
807
+ const h = this.handles.get(sessionID);
808
+ if (h) h.agent.cancel({ kind: "user" });
809
+ this.send(envelope("session.status", { sessionID, status: "idle" }));
810
+ }
811
+ async commandList(env) {
812
+ this.reply(env, "commands", { commands: ADAPTER_COMMANDS });
813
+ }
814
+ // ---- 工作区 ----
815
+ async workspaceList(env) {
816
+ const sp = this.getSessionPersistence();
817
+ let workspaces = [];
818
+ if (sp) {
819
+ workspaces = await listWorkspaces(sp, this.currentWorkspace);
820
+ } else if (this.currentWorkspace) {
821
+ workspaces = [{ path: this.currentWorkspace, name: workspaceName(this.currentWorkspace), sessionCount: 0, updated: 0 }];
822
+ }
823
+ this.reply(env, "workspaces", { workspaces, current: this.currentWorkspace });
824
+ }
825
+ async workspaceSet(env) {
826
+ const path = (env.payload?.path || "").trim();
827
+ const sp = this.getSessionPersistence();
828
+ if (!path) {
829
+ this.reply(env, "error", { code: "BAD_WORKSPACE", message: "\u7F3A\u5C11\u5DE5\u4F5C\u533A\u8DEF\u5F84", id: env.id });
830
+ return;
831
+ }
832
+ let workspaces = [];
833
+ if (sp) {
834
+ workspaces = await listWorkspaces(sp, this.currentWorkspace);
835
+ }
836
+ if (workspaces.length && !workspaces.some((w) => w.path === path)) {
837
+ this.reply(env, "error", { code: "BAD_WORKSPACE", message: "\u8BE5\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728", id: env.id });
838
+ return;
839
+ }
840
+ this.currentWorkspace = path;
841
+ this.log("\u5207\u6362\u5DE5\u4F5C\u533A: " + path);
842
+ this.announce();
843
+ this.reply(env, "workspaces", { workspaces, current: this.currentWorkspace });
844
+ }
845
+ async permissionReply(env) {
846
+ const p = env.payload;
847
+ const resolver = this.approvals.get(p.permissionID);
848
+ if (resolver) {
849
+ this.approvals.delete(p.permissionID);
850
+ resolver(p.status === "allow" ? "allowed-once" : "rejected");
851
+ }
852
+ this.send(envelope("permission.replied", { permissionID: p.permissionID, status: p.status }));
853
+ }
854
+ // ---- 审批 ----
855
+ async onApproval(req, next) {
856
+ if (!this.ws.isConnected) return next();
857
+ const sessionId = req.agent?.session?.id;
858
+ if (sessionId === void 0 || sessionId === null) return next();
859
+ const sessionID = String(sessionId);
860
+ const id = `p-${++this.approvalSeq}-${Date.now()}`;
861
+ const agent = req.agent;
862
+ const call = req.callId ? findToolCall(agent?.session?.events ? [...agent.session.events] : [], String(req.callId)) : null;
863
+ const tool = call?.name || req.toolName;
864
+ const args = call?.args || {};
865
+ const reason = req.reason || (Object.keys(args).length ? JSON.stringify(args, null, 2) : `dsh \u8BF7\u6C42\u6267\u884C\u5DE5\u5177 ${tool}`);
866
+ this.log(`\u5BA1\u6279\u8BF7\u6C42(tool=${tool}) \u2192 \u53D1\u9001\u5230\u624B\u673A\u6743\u9650\u5361 permissionID=${id}`);
867
+ return new Promise((resolve) => {
868
+ const timer = setTimeout(() => {
869
+ this.approvals.delete(id);
870
+ resolve("rejected");
871
+ }, APPROVAL_TIMEOUT_MS);
872
+ this.approvals.set(id, (outcome) => {
873
+ clearTimeout(timer);
874
+ resolve(outcome);
875
+ });
876
+ req.signal?.addEventListener(
877
+ "abort",
878
+ () => {
879
+ clearTimeout(timer);
880
+ this.approvals.delete(id);
881
+ resolve("cancelled");
882
+ },
883
+ { once: true }
884
+ );
885
+ const ok = this.send(
886
+ envelope("permission.request", {
887
+ id,
888
+ sessionID,
889
+ tool,
890
+ args,
891
+ prompt: reason,
892
+ createdAt: Date.now()
893
+ })
894
+ );
895
+ if (!ok) {
896
+ clearTimeout(timer);
897
+ this.approvals.delete(id);
898
+ resolve("rejected");
899
+ }
900
+ });
901
+ }
902
+ // ---- 流式 ----
903
+ onSessionEvent(sessionID, event) {
904
+ const sid = String(sessionID);
905
+ if (!this.handles.has(sid)) return;
906
+ streamEventTo(sid, event, (s, mid, part) => this.send(envelope("message.part", { sessionID: s, messageID: mid, part })), this.streamedTurns);
907
+ const t = event.type;
908
+ if (t === "turn/start") this.send(envelope("session.status", { sessionID: sid, status: "running" }));
909
+ else if (t === "turn/end") this.send(envelope("session.status", { sessionID: sid, status: "idle" }));
910
+ }
911
+ // ---- Agent 生命周期 ----
912
+ async ensureAgent(sessionID) {
913
+ const existing = this.handles.get(sessionID);
914
+ if (existing) return existing;
915
+ const agents = this.ctx.get("agents");
916
+ if (!agents) throw new Error("agents \u670D\u52A1\u4E0D\u53EF\u7528");
917
+ const dm = this.ctx.get("agentDefaultModel");
918
+ const selection = dm?.currentSelection();
919
+ const provider = this.cfg.provider || selection?.provider || "deepseek-official";
920
+ const model = this.cfg.model || selection?.model || "deepseek-v4-flash";
921
+ const current = { provider, model };
922
+ const presets = this.ctx.get("agentPresets");
923
+ const presetId = this.cfg.agentPreset || presets?.settings?.value?.default || "standard";
924
+ let resolvedId;
925
+ if (presets) {
926
+ try {
927
+ resolvedId = (await presets.resolve(presetId)).id;
928
+ } catch (e) {
929
+ this.log(`agentPreset \u89E3\u6790\u5931\u8D25(${presetId})\uFF0C\u964D\u7EA7\u4E3A\u65E0 preset: ${e.message}`);
930
+ }
931
+ }
932
+ const setup = async (agentCtx) => {
933
+ installModelSelection(agentCtx, { current, assembled: void 0 });
934
+ if (presets && resolvedId) await presets.mount(agentCtx, resolvedId);
935
+ };
936
+ let handle;
937
+ const sp = this.getSessionPersistence();
938
+ const workspace = this.pendingWorkspaces.get(sessionID) || this.currentWorkspace;
939
+ if (sp && await sessionExists(sp, sessionID)) {
940
+ handle = await agents.resume({
941
+ resumeSessionId: SessionId(sessionID),
942
+ agentOptions: { provider, model },
943
+ setup
944
+ });
945
+ } else {
946
+ handle = await agents.create({
947
+ sessionId: SessionId(sessionID),
948
+ meta: { cwd: workspace, ...resolvedId ? { agentPreset: resolvedId } : {} },
949
+ agentOptions: { provider, model },
950
+ setup
951
+ });
952
+ }
953
+ this.pendingWorkspaces.delete(sessionID);
954
+ this.handles.set(sessionID, handle);
955
+ return handle;
956
+ }
957
+ resetIdentity() {
958
+ this.log("\u88AB\u8D26\u53F7\u89E3\u7ED1\uFF0C\u6B63\u5728\u91CD\u7F6E\u8EAB\u4EFD\u5E76\u91CD\u65B0\u751F\u6210\u914D\u5BF9\u7801\u2026");
959
+ this.claimed = false;
960
+ this.state.agentId = randomBytes2(16).toString("hex");
961
+ this.state.claimed = false;
962
+ saveState(this.state, AGENT_TYPE);
963
+ this.ws.close();
964
+ this.connect(this.relayBase);
965
+ }
966
+ };
967
+ function localTimezone() {
968
+ try {
969
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
970
+ if (tz) return tz;
971
+ } catch {
972
+ }
973
+ return "";
974
+ }
975
+ function workspaceName(p) {
976
+ return p.replace(/[\\/]+$/, "").split(/[\\/]/).pop() || p;
977
+ }
978
+
979
+ // src/index.ts
980
+ var name = "tunnelbox-dsh";
981
+ var inject = ["agents", "sessions", "sessionPersistence", "agentDefaultModel", "approval", "loader"];
982
+ var Config = Schema.object({
983
+ relayUrl: Schema.string().default(""),
984
+ provider: Schema.string().default("deepseek-official"),
985
+ model: Schema.string().default("deepseek-v4-flash"),
986
+ cwd: Schema.string().default(process.cwd()),
987
+ agentPreset: Schema.string().default("")
988
+ });
989
+ function apply(ctx, config) {
990
+ const bridge = new TunnelboxBridge(ctx, config);
991
+ void bridge.start().catch((e) => {
992
+ console.error("[tunnelbox:dsh] \u542F\u52A8\u5931\u8D25:", e);
993
+ });
994
+ ctx.effect(() => {
995
+ return () => bridge.dispose();
996
+ });
997
+ }
998
+ export {
999
+ Config,
1000
+ apply,
1001
+ inject,
1002
+ name
1003
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@tunnelbox/dsh-tunnelbox",
3
+ "version": "0.1.0",
4
+ "description": "tunnelbox 的 dsh 原生 Cordis 插件:手机远程驱动 DeepSeek Harness(agent/session/tools/审批/中止)",
5
+ "type": "module",
6
+ "main": "./dist/index.mjs",
7
+ "files": [
8
+ "dist",
9
+ "cordis.patch.yml"
10
+ ],
11
+ "dsh": {
12
+ "bundle": {
13
+ "patch": "./cordis.patch.yml"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "node scripts/build.mjs",
18
+ "type-check": "tsc --noEmit"
19
+ },
20
+ "dependencies": {},
21
+ "peerDependencies": {
22
+ "@deepseek-ai/cordis": "^4.0.1",
23
+ "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
24
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
25
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
26
+ "@deepseek-ai/dsh-user-approval": "^0.1.1-rc.2",
27
+ "@deepseek-ai/schemastery": "^3.18.1"
28
+ },
29
+ "devDependencies": {
30
+ "@tunnelbox/core": "0.1.0",
31
+ "@types/node": "^22.0.0",
32
+ "esbuild": "^0.24.0",
33
+ "typescript": "^5.4.5"
34
+ }
35
+ }