@tunnelbox/claude-code 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 +120 -0
- package/dist/index.mjs +1351 -0
- package/official-plugin/.claude-plugin/plugin.json +10 -0
- package/official-plugin/bin/tunnelbox +4 -0
- package/official-plugin/bin/tunnelbox.cmd +3 -0
- package/official-plugin/bin/tunnelbox.mjs +274 -0
- package/official-plugin/commands/pair.md +10 -0
- package/official-plugin/commands/status.md +9 -0
- package/official-plugin/hooks/hooks.json +16 -0
- package/official-plugin/skills/pair/SKILL.md +12 -0
- package/package.json +32 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1351 @@
|
|
|
1
|
+
// tunnelbox claude-code adapter — built by scripts/build.mjs
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { resolve as resolve2 } from "node:path";
|
|
5
|
+
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
6
|
+
|
|
7
|
+
// src/bridge.ts
|
|
8
|
+
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
9
|
+
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
10
|
+
import { hostname } from "node:os";
|
|
11
|
+
import { isAbsolute, join as join2, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import {
|
|
14
|
+
query
|
|
15
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
16
|
+
|
|
17
|
+
// ../core/src/types.ts
|
|
18
|
+
function envelope(type, payload, id) {
|
|
19
|
+
return { id, type, payload, ts: Date.now() };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ../core/src/relay.ts
|
|
23
|
+
var BASE_DELAY = 1e3;
|
|
24
|
+
var MAX_DELAY = 15e3;
|
|
25
|
+
var RelayClient = class {
|
|
26
|
+
ws = null;
|
|
27
|
+
url = "";
|
|
28
|
+
headers = {};
|
|
29
|
+
handlers = null;
|
|
30
|
+
timer = null;
|
|
31
|
+
retry = 0;
|
|
32
|
+
stopped = false;
|
|
33
|
+
connected = false;
|
|
34
|
+
get isConnected() {
|
|
35
|
+
return this.connected;
|
|
36
|
+
}
|
|
37
|
+
connect(url, headers, handlers) {
|
|
38
|
+
this.url = url;
|
|
39
|
+
this.headers = headers;
|
|
40
|
+
this.handlers = handlers;
|
|
41
|
+
this.stopped = false;
|
|
42
|
+
this.retry = 0;
|
|
43
|
+
this.open();
|
|
44
|
+
}
|
|
45
|
+
send(env) {
|
|
46
|
+
if (this.ws && this.connected) {
|
|
47
|
+
try {
|
|
48
|
+
this.ws.send(JSON.stringify(env));
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
close() {
|
|
57
|
+
this.stopped = true;
|
|
58
|
+
if (this.timer) clearTimeout(this.timer);
|
|
59
|
+
this.timer = null;
|
|
60
|
+
if (this.ws) {
|
|
61
|
+
try {
|
|
62
|
+
this.ws.close();
|
|
63
|
+
} catch {
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.ws = null;
|
|
67
|
+
this.connected = false;
|
|
68
|
+
}
|
|
69
|
+
open() {
|
|
70
|
+
if (this.stopped) return;
|
|
71
|
+
try {
|
|
72
|
+
const ws = new WebSocket(this.url, { headers: this.headers });
|
|
73
|
+
this.ws = ws;
|
|
74
|
+
ws.onopen = () => {
|
|
75
|
+
this.connected = true;
|
|
76
|
+
this.retry = 0;
|
|
77
|
+
this.handlers?.onOpen();
|
|
78
|
+
};
|
|
79
|
+
ws.onmessage = (ev) => {
|
|
80
|
+
try {
|
|
81
|
+
const msg = JSON.parse(String(ev.data));
|
|
82
|
+
this.handlers?.onMessage(msg);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
ws.onerror = (e) => this.handlers?.onError(e);
|
|
87
|
+
ws.onclose = () => {
|
|
88
|
+
this.connected = false;
|
|
89
|
+
this.ws = null;
|
|
90
|
+
this.handlers?.onClose();
|
|
91
|
+
this.scheduleReconnect();
|
|
92
|
+
};
|
|
93
|
+
} catch (e) {
|
|
94
|
+
this.handlers?.onError(e);
|
|
95
|
+
this.scheduleReconnect();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
scheduleReconnect() {
|
|
99
|
+
if (this.stopped) return;
|
|
100
|
+
const delay = Math.min(BASE_DELAY * 2 ** this.retry, MAX_DELAY);
|
|
101
|
+
this.retry++;
|
|
102
|
+
this.timer = setTimeout(() => this.open(), delay);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ../core/src/state.ts
|
|
107
|
+
import { randomBytes } from "node:crypto";
|
|
108
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
109
|
+
import { homedir } from "node:os";
|
|
110
|
+
import { dirname, join } from "node:path";
|
|
111
|
+
var DEFAULT_RELAY_URL = process.env.TUNNELBOX_RELAY_URL || (process.env.NODE_ENV === "production" ? "wss://chat.wxngrok.com" : "ws://127.0.0.1:8080");
|
|
112
|
+
function baseName() {
|
|
113
|
+
return join(homedir(), ".config", "opencode");
|
|
114
|
+
}
|
|
115
|
+
function suffix(type) {
|
|
116
|
+
return type ? `.${type}` : "";
|
|
117
|
+
}
|
|
118
|
+
function statePath(type) {
|
|
119
|
+
return join(baseName(), `remote-state${suffix(type)}.json`);
|
|
120
|
+
}
|
|
121
|
+
function saveState(st, type) {
|
|
122
|
+
try {
|
|
123
|
+
const p = statePath(type);
|
|
124
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
125
|
+
writeFileSync(p, JSON.stringify(st, null, 2), "utf8");
|
|
126
|
+
} catch (e) {
|
|
127
|
+
console.error("[tunnelbox] \u4FDD\u5B58\u72B6\u6001\u5931\u8D25", e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function loadState(type) {
|
|
131
|
+
const fallback = {
|
|
132
|
+
agentId: randomBytes(16).toString("hex"),
|
|
133
|
+
relayUrl: DEFAULT_RELAY_URL
|
|
134
|
+
};
|
|
135
|
+
try {
|
|
136
|
+
if (!existsSync(statePath(type))) {
|
|
137
|
+
saveState(fallback, type);
|
|
138
|
+
return fallback;
|
|
139
|
+
}
|
|
140
|
+
const parsed = JSON.parse(readFileSync(statePath(type), "utf8"));
|
|
141
|
+
if (parsed.agentId && parsed.agentId.length >= 16) {
|
|
142
|
+
return {
|
|
143
|
+
agentId: parsed.agentId,
|
|
144
|
+
relayUrl: parsed.relayUrl || DEFAULT_RELAY_URL,
|
|
145
|
+
claimed: parsed.claimed === true
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
saveState(fallback, type);
|
|
149
|
+
return fallback;
|
|
150
|
+
} catch {
|
|
151
|
+
saveState(fallback, type);
|
|
152
|
+
return fallback;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function writePairingFile(link, code, type) {
|
|
156
|
+
const p = join(baseName(), `remote-pairing${suffix(type)}.txt`);
|
|
157
|
+
try {
|
|
158
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
159
|
+
writeFileSync(p, `\u914D\u5BF9\u7801: ${code}
|
|
160
|
+
\u914D\u5BF9\u94FE\u63A5: ${link}
|
|
161
|
+
`, "utf8");
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
return p;
|
|
165
|
+
}
|
|
166
|
+
function writePairingJson(info, type) {
|
|
167
|
+
const p = join(baseName(), `remote-pairing${suffix(type)}.json`);
|
|
168
|
+
try {
|
|
169
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
170
|
+
writeFileSync(p, JSON.stringify(info, null, 2), "utf8");
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ../core/src/qr.ts
|
|
177
|
+
async function printPairing(relayHttpBase, code, type, name) {
|
|
178
|
+
const p = new URLSearchParams({ code });
|
|
179
|
+
if (type) p.set("type", type);
|
|
180
|
+
if (name) p.set("name", name);
|
|
181
|
+
const link = `${relayHttpBase}/app/#/pages/index/index?${p.toString()}`;
|
|
182
|
+
const file = writePairingFile(link, code, type);
|
|
183
|
+
const tag = type ? ` ${type}` : "";
|
|
184
|
+
console.log("");
|
|
185
|
+
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");
|
|
186
|
+
console.log(`\u2502 tunnelbox${tag} \u624B\u673A\u914D\u5BF9 \u2502`);
|
|
187
|
+
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");
|
|
188
|
+
console.log(`\u2502 \u914D\u5BF9\u7801: ${code.padEnd(42)} \u2502`);
|
|
189
|
+
console.log(`\u2502 \u94FE\u63A5 : ${link.padEnd(42)} \u2502`);
|
|
190
|
+
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");
|
|
191
|
+
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");
|
|
192
|
+
try {
|
|
193
|
+
const mod = await import("qrcode");
|
|
194
|
+
const render = mod.default ?? mod;
|
|
195
|
+
const toString = render.toString;
|
|
196
|
+
if (toString) {
|
|
197
|
+
const qr = await toString(link, { type: "terminal", small: true });
|
|
198
|
+
if (qr) console.log(qr);
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
console.log(" \uFF08\u672A\u5B89\u88C5 qrcode\uFF0C\u8BF7\u4F7F\u7528\u4E0A\u65B9\u94FE\u63A5\u6216\u914D\u5BF9\u7801\uFF09");
|
|
202
|
+
}
|
|
203
|
+
console.log(` \u914D\u5BF9\u4FE1\u606F\u5DF2\u5199\u5165: ${file}`);
|
|
204
|
+
console.log("");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/map.ts
|
|
208
|
+
function rec(v) {
|
|
209
|
+
return v && typeof v === "object" ? v : {};
|
|
210
|
+
}
|
|
211
|
+
function safeJson(v, indent = 2) {
|
|
212
|
+
if (v === void 0 || v === null) return "";
|
|
213
|
+
try {
|
|
214
|
+
return typeof v === "string" ? v : JSON.stringify(v, null, indent);
|
|
215
|
+
} catch {
|
|
216
|
+
return String(v);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function blockToPart(block, index) {
|
|
220
|
+
const b = rec(block);
|
|
221
|
+
const id = `c${index}`;
|
|
222
|
+
switch (b.type) {
|
|
223
|
+
case "text":
|
|
224
|
+
return typeof b.text === "string" && b.text ? { id, type: "text", text: b.text } : null;
|
|
225
|
+
case "thinking":
|
|
226
|
+
case "redacted_thinking":
|
|
227
|
+
return { id, type: "thinking", text: typeof b.text === "string" ? b.text : typeof b.thinking === "string" ? b.thinking : "" };
|
|
228
|
+
case "tool_use":
|
|
229
|
+
return {
|
|
230
|
+
id,
|
|
231
|
+
type: "tool",
|
|
232
|
+
tool: String(b.name ?? "tool"),
|
|
233
|
+
args: safeJson(b.input) || "{}",
|
|
234
|
+
complete: true
|
|
235
|
+
};
|
|
236
|
+
default:
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function userTextParts(content) {
|
|
241
|
+
const out = [];
|
|
242
|
+
if (typeof content === "string") {
|
|
243
|
+
if (content) out.push({ id: "u0", type: "text", text: content });
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
if (!Array.isArray(content)) return out;
|
|
247
|
+
let i = 0;
|
|
248
|
+
for (const raw of content) {
|
|
249
|
+
const b = rec(raw);
|
|
250
|
+
if (b.type === "text" && typeof b.text === "string" && b.text) {
|
|
251
|
+
out.push({ id: `u${i}`, type: "text", text: b.text });
|
|
252
|
+
}
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
var RESULT_MAX_CHARS = 2e3;
|
|
258
|
+
function toolResultText(content) {
|
|
259
|
+
const parts = [];
|
|
260
|
+
const push = (v) => {
|
|
261
|
+
if (typeof v === "string") {
|
|
262
|
+
if (v) parts.push(v);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(v)) return;
|
|
266
|
+
for (const raw of v) {
|
|
267
|
+
const b = rec(raw);
|
|
268
|
+
if (b.type === "text" && typeof b.text === "string" && b.text) parts.push(b.text);
|
|
269
|
+
else if (b.type === "tool_result") push(b.content);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
push(content);
|
|
273
|
+
const text = parts.join("\n").trim();
|
|
274
|
+
if (text.length <= RESULT_MAX_CHARS) return text;
|
|
275
|
+
return text.slice(0, RESULT_MAX_CHARS) + "\n\u2026\uFF08\u7ED3\u679C\u8FC7\u957F\u5DF2\u622A\u65AD\uFF09";
|
|
276
|
+
}
|
|
277
|
+
function transcriptToMessages(entries) {
|
|
278
|
+
const out = [];
|
|
279
|
+
for (const e of entries) {
|
|
280
|
+
const m = rec(e.message);
|
|
281
|
+
if (e.type === "user") {
|
|
282
|
+
const userParts = userTextParts(m.content ?? "");
|
|
283
|
+
if (userParts.length) {
|
|
284
|
+
out.push({ id: e.uuid || genId(), role: "user", parts: userParts, created: Date.now() });
|
|
285
|
+
}
|
|
286
|
+
const resultParts = [];
|
|
287
|
+
const content = m.content;
|
|
288
|
+
if (Array.isArray(content)) {
|
|
289
|
+
content.forEach((b, i) => {
|
|
290
|
+
const blk = rec(b);
|
|
291
|
+
if (blk.type !== "tool_result") return;
|
|
292
|
+
const text = toolResultText(blk.content);
|
|
293
|
+
if (text) resultParts.push({ id: `r${i}`, type: "text", text });
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
if (resultParts.length) {
|
|
297
|
+
out.push({ id: `${e.uuid || genId()}-tool`, role: "tool", parts: resultParts, created: Date.now() });
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (e.type === "assistant") {
|
|
302
|
+
const content = m.content;
|
|
303
|
+
const parts = [];
|
|
304
|
+
if (Array.isArray(content)) {
|
|
305
|
+
content.forEach((b, i) => {
|
|
306
|
+
const p = blockToPart(b, i);
|
|
307
|
+
if (p) parts.push(p);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
if (!parts.length) continue;
|
|
311
|
+
out.push({ id: e.uuid || genId(), role: "assistant", parts, created: Date.now() });
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
var TurnStreamer = class {
|
|
318
|
+
constructor(sessionID, emit, setStatus) {
|
|
319
|
+
this.sessionID = sessionID;
|
|
320
|
+
this.emit = emit;
|
|
321
|
+
this.setStatus = setStatus;
|
|
322
|
+
}
|
|
323
|
+
/** 当前 assistant 消息 id(message_start 或 assistant.uuid 提供) */
|
|
324
|
+
mid = "";
|
|
325
|
+
/** 已投递的内容块(`${mid}:${index}`),避免流事件后再投整块造成重复 */
|
|
326
|
+
seen = /* @__PURE__ */ new Set();
|
|
327
|
+
/** 内容块下标 → 类型(text/thinking/tool_use) */
|
|
328
|
+
kinds = /* @__PURE__ */ new Map();
|
|
329
|
+
/** 工具名 / 累积的 input JSON */
|
|
330
|
+
toolNames = /* @__PURE__ */ new Map();
|
|
331
|
+
argBuf = /* @__PURE__ */ new Map();
|
|
332
|
+
seq = 0;
|
|
333
|
+
fallbackId() {
|
|
334
|
+
return `m-${Date.now()}-${++this.seq}`;
|
|
335
|
+
}
|
|
336
|
+
key(index) {
|
|
337
|
+
return `${this.mid}:${index}`;
|
|
338
|
+
}
|
|
339
|
+
/** 处理一条 SDKMessage 帧。 */
|
|
340
|
+
feed(raw) {
|
|
341
|
+
const f = rec(raw);
|
|
342
|
+
const type = typeof f.type === "string" ? f.type : "";
|
|
343
|
+
if (type === "stream_event") return this.onStreamEvent(f);
|
|
344
|
+
if (type === "assistant") return this.onAssistant(f.message);
|
|
345
|
+
if (type === "system") {
|
|
346
|
+
if (f.subtype === "session_state_changed") {
|
|
347
|
+
const state = typeof f.state === "string" ? f.state : "";
|
|
348
|
+
if (state === "running") this.setStatus(this.sessionID, "running");
|
|
349
|
+
else if (state === "idle") this.setStatus(this.sessionID, "idle");
|
|
350
|
+
else if (state === "requires_action") this.setStatus(this.sessionID, "running");
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
onStreamEvent(f) {
|
|
356
|
+
const ev = rec(f.event);
|
|
357
|
+
const t = typeof ev.type === "string" ? ev.type : "";
|
|
358
|
+
switch (t) {
|
|
359
|
+
case "message_start": {
|
|
360
|
+
const m = rec(ev.message);
|
|
361
|
+
this.mid = typeof m.id === "string" && m.id ? String(m.id) : this.fallbackId();
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "content_block_start": {
|
|
365
|
+
const index = String(ev.index ?? 0);
|
|
366
|
+
const block = rec(ev.content_block);
|
|
367
|
+
const btype = block.type;
|
|
368
|
+
if (btype === "text") this.kinds.set(index, "text");
|
|
369
|
+
else if (btype === "thinking" || btype === "redacted_thinking") this.kinds.set(index, "thinking");
|
|
370
|
+
else if (btype === "tool_use") {
|
|
371
|
+
this.kinds.set(index, "tool_use");
|
|
372
|
+
this.toolNames.set(index, String(block.name ?? "tool"));
|
|
373
|
+
this.argBuf.set(index, "");
|
|
374
|
+
this.send({ id: `c${index}`, type: "tool", tool: String(block.name ?? "tool"), args: "", complete: false });
|
|
375
|
+
}
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
case "content_block_delta": {
|
|
379
|
+
const index = String(ev.index ?? 0);
|
|
380
|
+
const delta = rec(ev.delta);
|
|
381
|
+
const dtype = delta.type;
|
|
382
|
+
if (dtype === "text_delta" && typeof delta.text === "string" && delta.text) {
|
|
383
|
+
this.mark(index);
|
|
384
|
+
this.send({ id: `c${index}`, type: "text", delta: delta.text });
|
|
385
|
+
} else if (dtype === "thinking_delta" && typeof delta.thinking === "string" && delta.thinking) {
|
|
386
|
+
this.mark(index);
|
|
387
|
+
this.send({ id: `c${index}`, type: "thinking", delta: delta.thinking });
|
|
388
|
+
} else if (dtype === "input_json_delta" && typeof delta.partial_json === "string") {
|
|
389
|
+
this.argBuf.set(index, (this.argBuf.get(index) || "") + delta.partial_json);
|
|
390
|
+
}
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
case "content_block_stop": {
|
|
394
|
+
const index = String(ev.index ?? 0);
|
|
395
|
+
const kind = this.kinds.get(index);
|
|
396
|
+
if (kind === "tool_use") {
|
|
397
|
+
const raw = this.argBuf.get(index) || "";
|
|
398
|
+
let args = raw;
|
|
399
|
+
if (raw) {
|
|
400
|
+
try {
|
|
401
|
+
args = JSON.stringify(JSON.parse(raw), null, 2);
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
this.mark(index);
|
|
406
|
+
this.send({
|
|
407
|
+
id: `c${index}`,
|
|
408
|
+
type: "tool",
|
|
409
|
+
tool: this.toolNames.get(index) || "tool",
|
|
410
|
+
args: args || "{}",
|
|
411
|
+
complete: true
|
|
412
|
+
});
|
|
413
|
+
this.kinds.delete(index);
|
|
414
|
+
this.toolNames.delete(index);
|
|
415
|
+
this.argBuf.delete(index);
|
|
416
|
+
} else {
|
|
417
|
+
this.mark(index);
|
|
418
|
+
}
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
case "message_stop":
|
|
422
|
+
this.end();
|
|
423
|
+
break;
|
|
424
|
+
case "error": {
|
|
425
|
+
const err = rec(ev.error);
|
|
426
|
+
const message = typeof err.message === "string" && err.message ? err.message : "claude \u6D41\u5F0F\u51FA\u9519";
|
|
427
|
+
this.send({ id: "err", type: "text", text: `[claude] ${message}`, complete: true });
|
|
428
|
+
this.end();
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
default:
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
mark(index) {
|
|
436
|
+
this.seen.add(this.key(index));
|
|
437
|
+
}
|
|
438
|
+
/** 完整 assistant 消息兜底:整块内容投递(已流式投递的块跳过)。 */
|
|
439
|
+
onAssistant(message) {
|
|
440
|
+
const m = rec(message);
|
|
441
|
+
const id = typeof m.id === "string" && m.id ? String(m.id) : this.fallbackId();
|
|
442
|
+
this.mid = id;
|
|
443
|
+
const content = m.content;
|
|
444
|
+
if (!Array.isArray(content)) return;
|
|
445
|
+
content.forEach((b, i) => {
|
|
446
|
+
const index = String(i);
|
|
447
|
+
if (this.seen.has(this.key(index))) return;
|
|
448
|
+
const part = blockToPart(b, i);
|
|
449
|
+
if (part) {
|
|
450
|
+
this.seen.add(this.key(index));
|
|
451
|
+
this.send(part);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
end() {
|
|
456
|
+
this.mid = "";
|
|
457
|
+
this.seen.clear();
|
|
458
|
+
}
|
|
459
|
+
send(part) {
|
|
460
|
+
if (!this.mid) this.mid = this.fallbackId();
|
|
461
|
+
this.emit(this.sessionID, this.mid, part);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
function genId() {
|
|
465
|
+
return `m-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/dialog.ts
|
|
469
|
+
function asRec(v) {
|
|
470
|
+
return v && typeof v === "object" ? v : {};
|
|
471
|
+
}
|
|
472
|
+
function firstText(v) {
|
|
473
|
+
for (const x of [v]) {
|
|
474
|
+
if (typeof x === "string" && x.trim()) return x.trim();
|
|
475
|
+
}
|
|
476
|
+
const r = asRec(v);
|
|
477
|
+
for (const key of ["prompt", "message", "question", "header", "text", "description", "title"]) {
|
|
478
|
+
const val = r[key];
|
|
479
|
+
if (typeof val === "string" && val.trim()) return val.trim();
|
|
480
|
+
}
|
|
481
|
+
return "";
|
|
482
|
+
}
|
|
483
|
+
function asLabels(v) {
|
|
484
|
+
if (!Array.isArray(v)) return [];
|
|
485
|
+
const out = [];
|
|
486
|
+
for (const x of v) {
|
|
487
|
+
if (typeof x === "string") {
|
|
488
|
+
if (x.trim()) out.push(x.trim());
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
const r = asRec(x);
|
|
492
|
+
const label = r.label ?? r.title ?? r.value;
|
|
493
|
+
if (typeof label === "string" && label.trim()) out.push(label.trim());
|
|
494
|
+
}
|
|
495
|
+
return out;
|
|
496
|
+
}
|
|
497
|
+
function tryMapDialog(dialogKind, payload) {
|
|
498
|
+
const p = asRec(payload);
|
|
499
|
+
const choices = p.choices !== void 0 ? asLabels(p.choices) : asLabels(p.options);
|
|
500
|
+
const prompt = firstText(payload) || `Claude Code \u8BF7\u6C42\u786E\u8BA4\uFF08${dialogKind}\uFF09`;
|
|
501
|
+
if (choices.length) {
|
|
502
|
+
return { kind: "choice", tool: dialogKind, prompt, options: choices };
|
|
503
|
+
}
|
|
504
|
+
const inputHint = p.freeform === true || p.type === "input" || typeof p.input === "string" || Array.isArray(p.input) || typeof p.requested === "string";
|
|
505
|
+
if (inputHint) {
|
|
506
|
+
return { kind: "input", tool: dialogKind, prompt };
|
|
507
|
+
}
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/warm.ts
|
|
512
|
+
import { startup } from "@anthropic-ai/claude-agent-sdk";
|
|
513
|
+
var WarmPool = class {
|
|
514
|
+
ready = /* @__PURE__ */ new Map();
|
|
515
|
+
starting = /* @__PURE__ */ new Map();
|
|
516
|
+
closed = false;
|
|
517
|
+
/** 取走某会话已就绪的预热句柄;没有(未就绪/无)返回 null。 */
|
|
518
|
+
take(sessionID) {
|
|
519
|
+
const w = this.ready.get(sessionID);
|
|
520
|
+
if (!w) return null;
|
|
521
|
+
this.ready.delete(sessionID);
|
|
522
|
+
return w;
|
|
523
|
+
}
|
|
524
|
+
/** 为该会话异步预 spawn 一个就绪进程(已有就绪/进行中则不重复)。 */
|
|
525
|
+
replenish(sessionID, options) {
|
|
526
|
+
if (this.closed || this.ready.has(sessionID) || this.starting.has(sessionID)) {
|
|
527
|
+
return Promise.resolve();
|
|
528
|
+
}
|
|
529
|
+
let done;
|
|
530
|
+
const p = new Promise((r) => done = r);
|
|
531
|
+
this.starting.set(sessionID, p);
|
|
532
|
+
void (async () => {
|
|
533
|
+
try {
|
|
534
|
+
const w = await startup({ options });
|
|
535
|
+
if (this.closed) {
|
|
536
|
+
w.close();
|
|
537
|
+
} else {
|
|
538
|
+
this.ready.set(sessionID, w);
|
|
539
|
+
}
|
|
540
|
+
} catch {
|
|
541
|
+
} finally {
|
|
542
|
+
this.starting.delete(sessionID);
|
|
543
|
+
done();
|
|
544
|
+
}
|
|
545
|
+
})();
|
|
546
|
+
return p;
|
|
547
|
+
}
|
|
548
|
+
/** 会话删除/结束等场景清理该会话的预热资源。 */
|
|
549
|
+
drop(sessionID) {
|
|
550
|
+
const w = this.ready.get(sessionID);
|
|
551
|
+
if (w) {
|
|
552
|
+
this.ready.delete(sessionID);
|
|
553
|
+
try {
|
|
554
|
+
w.close();
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/** 关闭全部预热进程。 */
|
|
560
|
+
dispose() {
|
|
561
|
+
this.closed = true;
|
|
562
|
+
for (const w of this.ready.values()) {
|
|
563
|
+
try {
|
|
564
|
+
w.close();
|
|
565
|
+
} catch {
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
this.ready.clear();
|
|
569
|
+
this.starting.clear();
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/sessions.ts
|
|
574
|
+
import {
|
|
575
|
+
deleteSession,
|
|
576
|
+
getSessionInfo,
|
|
577
|
+
getSessionMessages,
|
|
578
|
+
listSessions
|
|
579
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
580
|
+
var DEFAULT_TITLE = "Claude Code \u4F1A\u8BDD";
|
|
581
|
+
function toSessionInfo(s) {
|
|
582
|
+
return {
|
|
583
|
+
id: s.sessionId,
|
|
584
|
+
title: s.customTitle?.trim() || s.summary?.trim() || s.firstPrompt?.trim() || DEFAULT_TITLE,
|
|
585
|
+
created: s.createdAt ?? s.lastModified,
|
|
586
|
+
updated: s.lastModified,
|
|
587
|
+
status: "idle",
|
|
588
|
+
workspace: s.cwd
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function fetchSessionInfos(workspace) {
|
|
592
|
+
const list = await listSessions().catch((e) => {
|
|
593
|
+
throw new Error(`\u8BFB\u53D6 Claude \u4F1A\u8BDD\u5217\u8868\u5931\u8D25: ${e.message}`);
|
|
594
|
+
});
|
|
595
|
+
return list.map(toSessionInfo).filter((s) => !workspace || s.workspace === workspace).sort((a, b) => b.updated - a.updated);
|
|
596
|
+
}
|
|
597
|
+
async function sessionExists(sessionId, dir) {
|
|
598
|
+
try {
|
|
599
|
+
const info = await getSessionInfo(sessionId, dir ? { dir } : void 0);
|
|
600
|
+
return !!info;
|
|
601
|
+
} catch {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
async function readHistory(sessionId, dir) {
|
|
606
|
+
const rows = await getSessionMessages(sessionId, dir ? { dir } : void 0).catch((e) => {
|
|
607
|
+
throw new Error(`\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25(${sessionId}): ${e.message}`);
|
|
608
|
+
});
|
|
609
|
+
const entries = rows.map((r) => ({
|
|
610
|
+
type: r.type,
|
|
611
|
+
uuid: r.uuid,
|
|
612
|
+
message: r.message ?? {}
|
|
613
|
+
}));
|
|
614
|
+
return transcriptToMessages(entries);
|
|
615
|
+
}
|
|
616
|
+
async function removeSession(sessionId, dir) {
|
|
617
|
+
try {
|
|
618
|
+
await deleteSession(sessionId, dir ? { dir } : void 0);
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
function basename(p) {
|
|
623
|
+
return p.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? p;
|
|
624
|
+
}
|
|
625
|
+
async function fetchWorkspaceList(current) {
|
|
626
|
+
const list = await listSessions().catch(() => []);
|
|
627
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
628
|
+
for (const s of list) {
|
|
629
|
+
const p = s.cwd;
|
|
630
|
+
if (!p) continue;
|
|
631
|
+
const e = byPath.get(p) ?? { count: 0, updated: 0 };
|
|
632
|
+
e.count++;
|
|
633
|
+
if (s.lastModified > e.updated) e.updated = s.lastModified;
|
|
634
|
+
byPath.set(p, e);
|
|
635
|
+
}
|
|
636
|
+
if (current && !byPath.has(current)) byPath.set(current, { count: 0, updated: 0 });
|
|
637
|
+
return [...byPath.entries()].map(([path, e]) => ({
|
|
638
|
+
path,
|
|
639
|
+
name: basename(path) || path,
|
|
640
|
+
sessionCount: e.count,
|
|
641
|
+
updated: e.updated
|
|
642
|
+
})).sort((a, b) => b.updated - a.updated);
|
|
643
|
+
}
|
|
644
|
+
function newSessionId() {
|
|
645
|
+
if (typeof globalThis.crypto?.randomUUID === "function") {
|
|
646
|
+
return globalThis.crypto.randomUUID();
|
|
647
|
+
}
|
|
648
|
+
return genId();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/bridge.ts
|
|
652
|
+
var VERSION = "0.1.0";
|
|
653
|
+
var AGENT_TYPE = "claude-code";
|
|
654
|
+
var APPROVAL_TIMEOUT_MS = 12e4;
|
|
655
|
+
var ADAPTER_COMMANDS = [
|
|
656
|
+
{ name: "new", description: "\u65B0\u5EFA\u4F1A\u8BDD" },
|
|
657
|
+
{ name: "help", description: "\u663E\u793A\u5E2E\u52A9" }
|
|
658
|
+
];
|
|
659
|
+
function defaultOfficialPluginDir() {
|
|
660
|
+
try {
|
|
661
|
+
const here = fileURLToPath(new URL("../official-plugin", import.meta.url));
|
|
662
|
+
return existsSync2(join2(here, ".claude-plugin", "plugin.json")) ? here : void 0;
|
|
663
|
+
} catch {
|
|
664
|
+
return void 0;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
var ClaudeBridge = class {
|
|
668
|
+
constructor(cfg) {
|
|
669
|
+
this.cfg = cfg;
|
|
670
|
+
this.currentWorkspace = resolve(cfg.cwd || process.cwd());
|
|
671
|
+
if (cfg.warm) this.warmPool = new WarmPool();
|
|
672
|
+
}
|
|
673
|
+
ws = new RelayClient();
|
|
674
|
+
state = loadState(AGENT_TYPE);
|
|
675
|
+
claimed = !!this.state.claimed;
|
|
676
|
+
everConnected = false;
|
|
677
|
+
relayBase = "";
|
|
678
|
+
/** 当前工作区(新建会话的默认 cwd)。 */
|
|
679
|
+
currentWorkspace;
|
|
680
|
+
/** 新会话暂存的工作区(session.create 到首次 prompt 之间)。 */
|
|
681
|
+
pendingWorkspaces = /* @__PURE__ */ new Map();
|
|
682
|
+
/** 已知会话 → cwd(从 SDK 列表/首次 prompt 维护)。 */
|
|
683
|
+
sessionCwd = /* @__PURE__ */ new Map();
|
|
684
|
+
/** 进行中的回合(sessionID → AbortController)。 */
|
|
685
|
+
activeTurns = /* @__PURE__ */ new Map();
|
|
686
|
+
/** 待手机作答的请求(工具审批 / onUserDialog 选择输入共用一张表)。 */
|
|
687
|
+
approvals = /* @__PURE__ */ new Map();
|
|
688
|
+
approvalSeq = 0;
|
|
689
|
+
/** 会话级 mode 覆盖(session.prompt.agent 映射,如 plan)。 */
|
|
690
|
+
sessionModes = /* @__PURE__ */ new Map();
|
|
691
|
+
/** P4 预热池(cfg.warm 开启时启用)。 */
|
|
692
|
+
warmPool = null;
|
|
693
|
+
get home() {
|
|
694
|
+
return this.currentWorkspace;
|
|
695
|
+
}
|
|
696
|
+
get relayHttpBase() {
|
|
697
|
+
return this.relayBase.replace(/^ws/, "http").replace(/\/+$/, "");
|
|
698
|
+
}
|
|
699
|
+
async start() {
|
|
700
|
+
const url = (this.cfg.relayUrl || this.state.relayUrl || "").replace(/\/+$/, "");
|
|
701
|
+
this.relayBase = url;
|
|
702
|
+
this.connect(url);
|
|
703
|
+
}
|
|
704
|
+
dispose() {
|
|
705
|
+
this.ws.close();
|
|
706
|
+
for (const c of this.activeTurns.values()) c.abort();
|
|
707
|
+
this.activeTurns.clear();
|
|
708
|
+
this.warmPool?.dispose();
|
|
709
|
+
}
|
|
710
|
+
log(msg) {
|
|
711
|
+
console.log(`[tunnelbox:claude] ${msg}`);
|
|
712
|
+
}
|
|
713
|
+
send(e) {
|
|
714
|
+
return this.ws.send(e);
|
|
715
|
+
}
|
|
716
|
+
reply(src, type, payload) {
|
|
717
|
+
const msg = envelope(type, payload, src.id);
|
|
718
|
+
msg.clientID = src.clientID;
|
|
719
|
+
this.send(msg);
|
|
720
|
+
}
|
|
721
|
+
pairingLink(code) {
|
|
722
|
+
const p = new URLSearchParams({ code, type: AGENT_TYPE, name: hostname() });
|
|
723
|
+
return `${this.relayHttpBase}/app/#/pages/index/index?${p.toString()}`;
|
|
724
|
+
}
|
|
725
|
+
connect(url) {
|
|
726
|
+
const agentID = this.state.agentId;
|
|
727
|
+
const wsUrl = `${url}/ws/agent?token=${encodeURIComponent(agentID)}`;
|
|
728
|
+
this.ws.connect(
|
|
729
|
+
wsUrl,
|
|
730
|
+
{ Authorization: `Bearer ${agentID}` },
|
|
731
|
+
{
|
|
732
|
+
onOpen: () => {
|
|
733
|
+
this.everConnected = true;
|
|
734
|
+
this.log("\u5DF2\u8FDE\u63A5\u4E2D\u7EE7 " + url);
|
|
735
|
+
this.announce();
|
|
736
|
+
},
|
|
737
|
+
onMessage: (env) => {
|
|
738
|
+
void this.onMessage(env).catch((e) => this.log(`\u6D88\u606F\u5904\u7406\u672A\u6355\u83B7\u5F02\u5E38: ${e.message}`));
|
|
739
|
+
},
|
|
740
|
+
onClose: () => {
|
|
741
|
+
this.flushApprovals("\u4E2D\u7EE7\u65AD\u5F00\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08fail-closed\uFF09");
|
|
742
|
+
if (this.everConnected) {
|
|
743
|
+
this.everConnected = false;
|
|
744
|
+
this.log("\u4E0E\u4E2D\u7EE7\u65AD\u5F00\uFF0C\u6B63\u5728\u81EA\u52A8\u91CD\u8FDE\u2026");
|
|
745
|
+
}
|
|
746
|
+
},
|
|
747
|
+
onError: () => {
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
/** 会话基础 permissionMode 是否会产生工具询问(决定 permission 能力位/canUseTool)。 */
|
|
753
|
+
permissionEnabled() {
|
|
754
|
+
return this.turnModeFor("", void 0) !== "bypassPermissions";
|
|
755
|
+
}
|
|
756
|
+
announce() {
|
|
757
|
+
const permission = this.permissionEnabled();
|
|
758
|
+
const info = {
|
|
759
|
+
name: hostname(),
|
|
760
|
+
version: VERSION,
|
|
761
|
+
type: AGENT_TYPE,
|
|
762
|
+
capabilities: {
|
|
763
|
+
streaming: true,
|
|
764
|
+
thinking: true,
|
|
765
|
+
// 仅当模型/会话开启思考时会下发 thinking 部件
|
|
766
|
+
permission,
|
|
767
|
+
// bypassPermissions 下为 false(不询问、不注册 canUseTool)
|
|
768
|
+
commands: true,
|
|
769
|
+
abort: true
|
|
770
|
+
// AbortController
|
|
771
|
+
},
|
|
772
|
+
platform: process.platform,
|
|
773
|
+
directory: this.currentWorkspace,
|
|
774
|
+
timezone: localTimezone()
|
|
775
|
+
};
|
|
776
|
+
this.send(envelope("agent.info", info));
|
|
777
|
+
if (!this.claimed) this.send(envelope("pair.create", {}));
|
|
778
|
+
}
|
|
779
|
+
/** 计算某回合生效的 permissionMode:agent 参数 > 会话级 > 配置默认。 */
|
|
780
|
+
turnModeFor(sessionID, agent) {
|
|
781
|
+
if (agent === "plan") return "plan";
|
|
782
|
+
if (agent === "build") return "default";
|
|
783
|
+
return this.sessionModes.get(sessionID) || this.cfg.mode || "default";
|
|
784
|
+
}
|
|
785
|
+
// ---- 消息路由 ----
|
|
786
|
+
async onMessage(env) {
|
|
787
|
+
try {
|
|
788
|
+
switch (env.type) {
|
|
789
|
+
case "pair.created": {
|
|
790
|
+
const code = env.payload.code;
|
|
791
|
+
const link = this.pairingLink(code);
|
|
792
|
+
writePairingJson({ code, link, relayUrl: this.relayBase, at: Date.now() }, AGENT_TYPE);
|
|
793
|
+
this.log("\u751F\u6210\u914D\u5BF9\u7801: " + code);
|
|
794
|
+
void printPairing(this.relayHttpBase, code, AGENT_TYPE, hostname());
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
case "agent.claimed":
|
|
798
|
+
this.claimed = true;
|
|
799
|
+
this.state.claimed = true;
|
|
800
|
+
saveState(this.state, AGENT_TYPE);
|
|
801
|
+
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");
|
|
802
|
+
return;
|
|
803
|
+
case "agent.revoked":
|
|
804
|
+
this.resetIdentity();
|
|
805
|
+
return;
|
|
806
|
+
case "session.list":
|
|
807
|
+
return this.sessionList(env);
|
|
808
|
+
case "session.create":
|
|
809
|
+
return this.sessionCreate(env);
|
|
810
|
+
case "session.messages":
|
|
811
|
+
return this.sessionMessages(env);
|
|
812
|
+
case "session.prompt":
|
|
813
|
+
return this.sessionPrompt(env);
|
|
814
|
+
case "session.abort":
|
|
815
|
+
return this.sessionAbort(env);
|
|
816
|
+
case "session.delete":
|
|
817
|
+
return this.sessionDelete(env);
|
|
818
|
+
case "command.list":
|
|
819
|
+
return this.commandList(env);
|
|
820
|
+
case "workspace.list":
|
|
821
|
+
return this.workspaceList(env);
|
|
822
|
+
case "workspace.set":
|
|
823
|
+
return this.workspaceSet(env);
|
|
824
|
+
case "permission.reply":
|
|
825
|
+
return this.permissionReply(env);
|
|
826
|
+
case "error":
|
|
827
|
+
return;
|
|
828
|
+
default:
|
|
829
|
+
this.log("\u672A\u5904\u7406\u7684\u6D88\u606F\u7C7B\u578B: " + env.type);
|
|
830
|
+
}
|
|
831
|
+
} catch (e) {
|
|
832
|
+
this.log(`\u5904\u7406 ${env.type} \u5931\u8D25: ${e.message}`);
|
|
833
|
+
this.reply(env, "error", { code: "HANDLER_ERROR", message: e.message, id: env.id });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
// ---- 会话 ----
|
|
837
|
+
async sessionList(env) {
|
|
838
|
+
const payload = env.payload;
|
|
839
|
+
const sessions = (await fetchSessionInfos(payload?.workspace)).map((s) => ({
|
|
840
|
+
...s,
|
|
841
|
+
status: this.activeTurns.has(s.id) ? "running" : s.status
|
|
842
|
+
}));
|
|
843
|
+
this.reply(env, "sessions", { sessions });
|
|
844
|
+
}
|
|
845
|
+
async sessionCreate(env) {
|
|
846
|
+
const payload = env.payload;
|
|
847
|
+
const workspace = payload?.workspace || this.currentWorkspace;
|
|
848
|
+
const id = newSessionId();
|
|
849
|
+
const now = Date.now();
|
|
850
|
+
this.pendingWorkspaces.set(id, workspace);
|
|
851
|
+
this.reply(env, "session.created", {
|
|
852
|
+
session: { id, title: "\u65B0\u4F1A\u8BDD", created: now, updated: now, status: "idle", workspace }
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
async sessionMessages(env) {
|
|
856
|
+
const { sessionID } = env.payload;
|
|
857
|
+
const dir = await this.dirOf(sessionID);
|
|
858
|
+
let messages = [];
|
|
859
|
+
if (dir && await sessionExists(sessionID, dir)) {
|
|
860
|
+
messages = await readHistory(sessionID, dir);
|
|
861
|
+
}
|
|
862
|
+
this.reply(env, "messages", { sessionID, messages });
|
|
863
|
+
}
|
|
864
|
+
async sessionPrompt(env) {
|
|
865
|
+
const payload = env.payload;
|
|
866
|
+
const text = (payload.text || "").trim();
|
|
867
|
+
if (!text) return;
|
|
868
|
+
if (text.startsWith("/new")) return this.sessionCreate(env);
|
|
869
|
+
if (text.startsWith("/help")) {
|
|
870
|
+
this.reply(env, "message.part", {
|
|
871
|
+
sessionID: payload.sessionID,
|
|
872
|
+
messageID: `m-${Date.now()}`,
|
|
873
|
+
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 }
|
|
874
|
+
});
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
const agent = (payload.agent || "").trim();
|
|
878
|
+
if (agent === "plan") this.sessionModes.set(payload.sessionID, "plan");
|
|
879
|
+
else if (agent === "build") this.sessionModes.delete(payload.sessionID);
|
|
880
|
+
if (this.activeTurns.has(payload.sessionID)) {
|
|
881
|
+
this.reply(env, "error", { code: "BUSY", message: "\u8BE5\u4F1A\u8BDD\u6B63\u5728\u751F\u6210\u4E2D\uFF0C\u8BF7\u7B49\u5F85\u5B8C\u6210\u6216\u5148\u505C\u6B62", id: env.id });
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
try {
|
|
885
|
+
await this.runTurn(payload.sessionID, text);
|
|
886
|
+
} catch (e) {
|
|
887
|
+
const msg = e.message || String(e);
|
|
888
|
+
this.log(`session.prompt \u5931\u8D25: ${msg}`);
|
|
889
|
+
this.reply(env, "error", { code: "PROMPT_ERROR", message: msg, id: env.id });
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async sessionAbort(env) {
|
|
893
|
+
const { sessionID } = env.payload;
|
|
894
|
+
const c = this.activeTurns.get(sessionID);
|
|
895
|
+
if (c) {
|
|
896
|
+
c.abort();
|
|
897
|
+
this.log(`\u5DF2\u53D1\u9001\u4E2D\u6B62 (session=${sessionID})`);
|
|
898
|
+
}
|
|
899
|
+
this.send(envelope("session.status", { sessionID, status: "idle" }));
|
|
900
|
+
}
|
|
901
|
+
async sessionDelete(env) {
|
|
902
|
+
const { sessionID } = env.payload;
|
|
903
|
+
const c = this.activeTurns.get(sessionID);
|
|
904
|
+
if (c) c.abort();
|
|
905
|
+
this.activeTurns.delete(sessionID);
|
|
906
|
+
const dir = await this.dirOf(sessionID);
|
|
907
|
+
await removeSession(sessionID, dir);
|
|
908
|
+
this.sessionCwd.delete(sessionID);
|
|
909
|
+
this.pendingWorkspaces.delete(sessionID);
|
|
910
|
+
this.sessionModes.delete(sessionID);
|
|
911
|
+
this.warmPool?.drop(sessionID);
|
|
912
|
+
this.reply(env, "session.deleted", { sessionID });
|
|
913
|
+
}
|
|
914
|
+
/** 定位会话所在工作区:内存缓存 → SDK 列表 → undefined。 */
|
|
915
|
+
async dirOf(sessionID) {
|
|
916
|
+
const cached = this.sessionCwd.get(sessionID);
|
|
917
|
+
if (cached) return cached;
|
|
918
|
+
const list = await fetchSessionInfos().catch(() => []);
|
|
919
|
+
const found = list.find((s) => s.id === sessionID);
|
|
920
|
+
if (found?.workspace) {
|
|
921
|
+
this.sessionCwd.set(sessionID, found.workspace);
|
|
922
|
+
return found.workspace;
|
|
923
|
+
}
|
|
924
|
+
return void 0;
|
|
925
|
+
}
|
|
926
|
+
async commandList(env) {
|
|
927
|
+
this.reply(env, "commands", { commands: ADAPTER_COMMANDS });
|
|
928
|
+
}
|
|
929
|
+
// ---- 工作区 ----
|
|
930
|
+
async workspaceList(env) {
|
|
931
|
+
const workspaces = await fetchWorkspaceList(this.currentWorkspace);
|
|
932
|
+
this.reply(env, "workspaces", { workspaces, current: this.currentWorkspace });
|
|
933
|
+
}
|
|
934
|
+
async workspaceSet(env) {
|
|
935
|
+
const raw = (env.payload?.path || "").trim();
|
|
936
|
+
if (!raw) {
|
|
937
|
+
this.reply(env, "error", { code: "BAD_WORKSPACE", message: "\u7F3A\u5C11\u5DE5\u4F5C\u533A\u8DEF\u5F84", id: env.id });
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
const path = isAbsolute(raw) ? raw : resolve(raw);
|
|
941
|
+
if (!existsSync2(path) || !statSync(path).isDirectory()) {
|
|
942
|
+
this.reply(env, "error", { code: "BAD_WORKSPACE", message: "\u8BE5\u76EE\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u53EF\u7528", id: env.id });
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
this.currentWorkspace = path;
|
|
946
|
+
this.log("\u5207\u6362\u5DE5\u4F5C\u533A: " + path);
|
|
947
|
+
this.announce();
|
|
948
|
+
const workspaces = await fetchWorkspaceList(this.currentWorkspace);
|
|
949
|
+
this.reply(env, "workspaces", { workspaces, current: this.currentWorkspace });
|
|
950
|
+
}
|
|
951
|
+
// ---- 权限 ----
|
|
952
|
+
async permissionReply(env) {
|
|
953
|
+
const p = env.payload;
|
|
954
|
+
const entry = this.approvals.get(p.permissionID);
|
|
955
|
+
if (entry) {
|
|
956
|
+
entry.resolve({ allow: p.status === "allow", always: !!p.always, value: p.value });
|
|
957
|
+
}
|
|
958
|
+
this.send(envelope("permission.replied", { permissionID: p.permissionID, status: p.status }));
|
|
959
|
+
}
|
|
960
|
+
/** 断线/重置时把所有待作答请求 resolve 为 deny(fail-closed),避免挂满超时。 */
|
|
961
|
+
flushApprovals(reason) {
|
|
962
|
+
const entries = [...this.approvals.values()];
|
|
963
|
+
this.approvals.clear();
|
|
964
|
+
for (const entry of entries) {
|
|
965
|
+
entry.resolve({ allow: false, reason });
|
|
966
|
+
}
|
|
967
|
+
if (entries.length) this.log(`\u5DF2\u62D2\u7EDD ${entries.length} \u4E2A\u5F85\u4F5C\u7B54\u8BF7\u6C42: ${reason}`);
|
|
968
|
+
}
|
|
969
|
+
// ---- 回合驱动 ----
|
|
970
|
+
/** 每个手机消息 = 一次 query(resume 既有会话 / sessionId 固定新会话),跨会话并行。 */
|
|
971
|
+
async runTurn(sessionID, text) {
|
|
972
|
+
const dir = this.pendingWorkspaces.get(sessionID) || await this.dirOf(sessionID) || this.currentWorkspace;
|
|
973
|
+
if (!existsSync2(dir) || !statSync(dir).isDirectory()) {
|
|
974
|
+
throw new Error(`\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728: ${dir}`);
|
|
975
|
+
}
|
|
976
|
+
this.pendingWorkspaces.delete(sessionID);
|
|
977
|
+
const known = await sessionExists(sessionID, dir);
|
|
978
|
+
const controller = new AbortController();
|
|
979
|
+
this.activeTurns.set(sessionID, controller);
|
|
980
|
+
this.send(envelope("session.status", { sessionID, status: "running" }));
|
|
981
|
+
const streamer = new TurnStreamer(
|
|
982
|
+
sessionID,
|
|
983
|
+
(s, messageID, part) => this.send(envelope("message.part", { sessionID: s, messageID, part })),
|
|
984
|
+
(s, status) => this.send(envelope("session.status", { sessionID: s, status }))
|
|
985
|
+
);
|
|
986
|
+
const mode = this.turnModeFor(sessionID, void 0);
|
|
987
|
+
const withPermission = mode !== "bypassPermissions";
|
|
988
|
+
const options = {
|
|
989
|
+
cwd: dir,
|
|
990
|
+
includePartialMessages: true,
|
|
991
|
+
permissionMode: mode,
|
|
992
|
+
abortController: controller,
|
|
993
|
+
// 对话框白名单(默认空 = 不发任何 dialog,fail-closed);onElicitation 见下
|
|
994
|
+
supportedDialogKinds: this.cfg.dialogKinds ?? [],
|
|
995
|
+
onUserDialog: (req, info) => this.onUserDialog(sessionID, req, info),
|
|
996
|
+
onElicitation: (req, info) => this.onElicitation(sessionID, req, info)
|
|
997
|
+
};
|
|
998
|
+
if (withPermission) {
|
|
999
|
+
const canUseTool = (toolName, input, info) => this.askPermission(sessionID, toolName, input, info);
|
|
1000
|
+
options.canUseTool = canUseTool;
|
|
1001
|
+
}
|
|
1002
|
+
if (this.cfg.model) options.model = this.cfg.model;
|
|
1003
|
+
if (this.cfg.thinking) options.thinking = this.cfg.thinking;
|
|
1004
|
+
if (this.cfg.maxThinkingTokens != null) options.maxThinkingTokens = this.cfg.maxThinkingTokens;
|
|
1005
|
+
const pluginDir = this.cfg.pluginDir || defaultOfficialPluginDir();
|
|
1006
|
+
if (pluginDir) {
|
|
1007
|
+
options.plugins = [
|
|
1008
|
+
{ type: "local", path: pluginDir }
|
|
1009
|
+
];
|
|
1010
|
+
options.env = { ...this.cfg.env || {}, TUNNELBOX_SDK_SESSION: "1" };
|
|
1011
|
+
} else if (this.cfg.env && Object.keys(this.cfg.env).length) {
|
|
1012
|
+
options.env = this.cfg.env;
|
|
1013
|
+
}
|
|
1014
|
+
if (known) options.resume = sessionID;
|
|
1015
|
+
else options.sessionId = sessionID;
|
|
1016
|
+
let last = null;
|
|
1017
|
+
try {
|
|
1018
|
+
let gen;
|
|
1019
|
+
const warm = this.warmPool?.take(sessionID);
|
|
1020
|
+
if (warm) {
|
|
1021
|
+
gen = warm.query(text);
|
|
1022
|
+
} else {
|
|
1023
|
+
gen = query({ prompt: text, options });
|
|
1024
|
+
}
|
|
1025
|
+
for await (const msg of gen) {
|
|
1026
|
+
last = msg;
|
|
1027
|
+
streamer.feed(msg);
|
|
1028
|
+
}
|
|
1029
|
+
if (last && last.type === "result" && last.is_error) {
|
|
1030
|
+
const subtype = last.subtype;
|
|
1031
|
+
const text2 = last.result;
|
|
1032
|
+
this.send(
|
|
1033
|
+
envelope("message.part", {
|
|
1034
|
+
sessionID,
|
|
1035
|
+
messageID: `m-err-${Date.now()}`,
|
|
1036
|
+
part: { id: "err", type: "text", text: `[claude] ${text2 || subtype || "\u751F\u6210\u51FA\u9519"}`, complete: true }
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
if (controller.signal.aborted) {
|
|
1042
|
+
this.log(`\u4F1A\u8BDD\u5DF2\u4E2D\u6B62 (session=${sessionID})`);
|
|
1043
|
+
} else {
|
|
1044
|
+
const msg = e.message || String(e);
|
|
1045
|
+
this.log(`\u56DE\u5408\u51FA\u9519 (session=${sessionID}): ${msg}`);
|
|
1046
|
+
this.send(
|
|
1047
|
+
envelope("message.part", {
|
|
1048
|
+
sessionID,
|
|
1049
|
+
messageID: `m-err-${Date.now()}`,
|
|
1050
|
+
part: { id: "err", type: "text", text: `[claude] ${msg}`, complete: true }
|
|
1051
|
+
})
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
} finally {
|
|
1055
|
+
this.activeTurns.delete(sessionID);
|
|
1056
|
+
if (!this.sessionCwd.has(sessionID)) this.sessionCwd.set(sessionID, dir);
|
|
1057
|
+
this.send(envelope("session.status", { sessionID, status: "idle" }));
|
|
1058
|
+
if (this.warmPool) void this.warmPool.replenish(sessionID, options).catch(() => {
|
|
1059
|
+
});
|
|
1060
|
+
void this.broadcastSessionUpdate(sessionID);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
/** 广播 session.updated(最新 listSessions 信息)。 */
|
|
1064
|
+
async broadcastSessionUpdate(sessionID) {
|
|
1065
|
+
try {
|
|
1066
|
+
if (!this.ws.isConnected) return;
|
|
1067
|
+
const list = await fetchSessionInfos().catch(() => []);
|
|
1068
|
+
const found = list.find((s) => s.id === sessionID);
|
|
1069
|
+
if (!found) return;
|
|
1070
|
+
if (found.workspace) this.sessionCwd.set(sessionID, found.workspace);
|
|
1071
|
+
this.send(envelope("session.updated", { session: found }));
|
|
1072
|
+
} catch {
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
/** canUseTool → permission.request;中继在线才推送,离线/超时/中止一律 fail-closed 拒绝。 */
|
|
1076
|
+
askPermission(sessionID, toolName, input, info) {
|
|
1077
|
+
return new Promise((resolve3) => {
|
|
1078
|
+
if (!this.ws.isConnected) {
|
|
1079
|
+
this.log(`\u4E2D\u7EE7\u79BB\u7EBF\uFF0C\u81EA\u52A8\u62D2\u7EDD\u5DE5\u5177 ${toolName}`);
|
|
1080
|
+
resolve3({ behavior: "deny", message: "\u4E2D\u7EE7\u79BB\u7EBF\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08fail-closed\uFF09" });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const tool = toolName || "tool";
|
|
1084
|
+
const id = `p-${++this.approvalSeq}-${Date.now()}`;
|
|
1085
|
+
const prompt = info.title || info.description || (info.decisionReason ? `Claude Code: ${info.decisionReason}` : `Claude Code \u8BF7\u6C42\u6267\u884C ${tool}`);
|
|
1086
|
+
const timer = setTimeout(() => {
|
|
1087
|
+
this.approvals.delete(id);
|
|
1088
|
+
resolve3({ behavior: "deny", message: "\u5BA1\u6279\u8D85\u65F6\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08fail-closed\uFF09" });
|
|
1089
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
1090
|
+
this.approvals.set(id, {
|
|
1091
|
+
tool,
|
|
1092
|
+
kind: "approval",
|
|
1093
|
+
resolve: (o) => {
|
|
1094
|
+
clearTimeout(timer);
|
|
1095
|
+
this.approvals.delete(id);
|
|
1096
|
+
if (!o.allow) {
|
|
1097
|
+
resolve3({ behavior: "deny", message: o.reason || "\u7528\u6237\u62D2\u7EDD" });
|
|
1098
|
+
} else if (o.always && info.suggestions?.length) {
|
|
1099
|
+
resolve3({ behavior: "allow", updatedPermissions: info.suggestions });
|
|
1100
|
+
} else {
|
|
1101
|
+
resolve3({ behavior: "allow" });
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
const signal = info.signal;
|
|
1106
|
+
signal?.addEventListener(
|
|
1107
|
+
"abort",
|
|
1108
|
+
() => {
|
|
1109
|
+
clearTimeout(timer);
|
|
1110
|
+
this.approvals.delete(id);
|
|
1111
|
+
resolve3({ behavior: "deny", message: "\u8BF7\u6C42\u5DF2\u4E2D\u6B62" });
|
|
1112
|
+
},
|
|
1113
|
+
{ once: true }
|
|
1114
|
+
);
|
|
1115
|
+
this.log(`\u5BA1\u6279\u8BF7\u6C42(tool=${tool}) \u2192 \u53D1\u9001\u5230\u624B\u673A\u6743\u9650\u5361 permissionID=${id}`);
|
|
1116
|
+
const ok = this.send(
|
|
1117
|
+
envelope("permission.request", {
|
|
1118
|
+
id,
|
|
1119
|
+
sessionID,
|
|
1120
|
+
tool,
|
|
1121
|
+
args: input ?? {},
|
|
1122
|
+
prompt,
|
|
1123
|
+
createdAt: Date.now()
|
|
1124
|
+
})
|
|
1125
|
+
);
|
|
1126
|
+
if (!ok) {
|
|
1127
|
+
clearTimeout(timer);
|
|
1128
|
+
this.approvals.delete(id);
|
|
1129
|
+
resolve3({ behavior: "deny", message: "\u4E2D\u7EE7\u4E0D\u53EF\u8FBE\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08fail-closed\uFF09" });
|
|
1130
|
+
}
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
/**
|
|
1134
|
+
* 向手机发起选择/输入型请求(onUserDialog 映射用),返回是否完成及用户提交值。
|
|
1135
|
+
* 离线/超时/中止返回 null(调用方据此回 cancelled)。
|
|
1136
|
+
*/
|
|
1137
|
+
askUserChoice(sessionID, tool, kind, prompt, options, signal) {
|
|
1138
|
+
return new Promise((resolve3) => {
|
|
1139
|
+
if (!this.ws.isConnected) {
|
|
1140
|
+
this.log(`\u4E2D\u7EE7\u79BB\u7EBF\uFF0C\u5BF9\u8BDD\u6846\u81EA\u52A8\u53D6\u6D88\uFF08${tool}\uFF09`);
|
|
1141
|
+
resolve3(null);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
const id = `p-${++this.approvalSeq}-${Date.now()}`;
|
|
1145
|
+
const timer = setTimeout(() => {
|
|
1146
|
+
this.approvals.delete(id);
|
|
1147
|
+
resolve3(null);
|
|
1148
|
+
}, APPROVAL_TIMEOUT_MS);
|
|
1149
|
+
this.approvals.set(id, {
|
|
1150
|
+
tool,
|
|
1151
|
+
kind,
|
|
1152
|
+
resolve: (o) => {
|
|
1153
|
+
clearTimeout(timer);
|
|
1154
|
+
this.approvals.delete(id);
|
|
1155
|
+
if (!o.allow) {
|
|
1156
|
+
resolve3(null);
|
|
1157
|
+
} else {
|
|
1158
|
+
resolve3({ value: o.value });
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
signal?.addEventListener(
|
|
1163
|
+
"abort",
|
|
1164
|
+
() => {
|
|
1165
|
+
clearTimeout(timer);
|
|
1166
|
+
this.approvals.delete(id);
|
|
1167
|
+
resolve3(null);
|
|
1168
|
+
},
|
|
1169
|
+
{ once: true }
|
|
1170
|
+
);
|
|
1171
|
+
this.log(`\u5BF9\u8BDD\u6846\u8BF7\u6C42(kind=${kind} tool=${tool}) \u2192 \u53D1\u9001\u5230\u624B\u673A\u6743\u9650\u5361 permissionID=${id}`);
|
|
1172
|
+
const ok = this.send(
|
|
1173
|
+
envelope("permission.request", {
|
|
1174
|
+
id,
|
|
1175
|
+
sessionID,
|
|
1176
|
+
tool,
|
|
1177
|
+
args: { dialogKind: tool },
|
|
1178
|
+
prompt,
|
|
1179
|
+
createdAt: Date.now(),
|
|
1180
|
+
...kind === "approval" ? {} : kind === "choice" && options?.length ? { kind, options } : { kind }
|
|
1181
|
+
})
|
|
1182
|
+
);
|
|
1183
|
+
if (!ok) {
|
|
1184
|
+
clearTimeout(timer);
|
|
1185
|
+
this.approvals.delete(id);
|
|
1186
|
+
resolve3(null);
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* request_user_dialog:把白名单内的 kind 映射为手机 choice/input 卡。
|
|
1192
|
+
* 仅当 kind 在 cfg.dialogKinds(运行时白名单)内时才会被 SDK 发射并走到这里;
|
|
1193
|
+
* 未映射/离线/超时 → cancelled(走 CLI 默认路径)。
|
|
1194
|
+
* 注意:payload/result 为 kind 相关结构,映射为实验性,需装 claude 后实测校正。
|
|
1195
|
+
*/
|
|
1196
|
+
async onUserDialog(sessionID, req, info) {
|
|
1197
|
+
const mapped = tryMapDialog(req.dialogKind, req.payload);
|
|
1198
|
+
if (!mapped) {
|
|
1199
|
+
this.log(`request_user_dialog(kind=${req.dialogKind}) \u672A\u63A5\u5165\u6620\u5C04\uFF0C\u4FDD\u6301\u9ED8\u8BA4\u884C\u4E3A\uFF08session=${sessionID}\uFF09`);
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const ans = await this.askUserChoice(
|
|
1203
|
+
sessionID,
|
|
1204
|
+
req.dialogKind,
|
|
1205
|
+
mapped.kind,
|
|
1206
|
+
mapped.prompt,
|
|
1207
|
+
mapped.options,
|
|
1208
|
+
info.signal
|
|
1209
|
+
);
|
|
1210
|
+
if (ans && ans.value !== void 0 && ans.value !== "") {
|
|
1211
|
+
return { behavior: "completed", result: ans.value };
|
|
1212
|
+
}
|
|
1213
|
+
return { behavior: "cancelled" };
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* MCP elicitation:mode='url' → 手机审批卡(允许=accept);form / 其它保持 decline。
|
|
1217
|
+
* 均为实验性映射,需装 claude 后按真实 elicitation 语义校正。
|
|
1218
|
+
*/
|
|
1219
|
+
async onElicitation(sessionID, req, info) {
|
|
1220
|
+
if (req.mode === "url" && this.ws.isConnected) {
|
|
1221
|
+
const tool = `mcp.auth:${req.serverName}`;
|
|
1222
|
+
const ans = await this.askUserChoice(
|
|
1223
|
+
sessionID,
|
|
1224
|
+
tool,
|
|
1225
|
+
"approval",
|
|
1226
|
+
`MCP\u300C${req.serverName}\u300D\u8BF7\u6C42\u6388\u6743
|
|
1227
|
+
|
|
1228
|
+
${req.message || ""}${req.url ? `
|
|
1229
|
+
|
|
1230
|
+
URL: ${req.url}` : ""}`,
|
|
1231
|
+
void 0,
|
|
1232
|
+
info.signal
|
|
1233
|
+
);
|
|
1234
|
+
if (ans) return { action: "accept" };
|
|
1235
|
+
return { action: "decline" };
|
|
1236
|
+
}
|
|
1237
|
+
this.log(`MCP elicitation(server=${req.serverName}, mode=${req.mode ?? "-"}) \u672A\u63A5\u5165\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08session=${sessionID}\uFF09`);
|
|
1238
|
+
return { action: "decline" };
|
|
1239
|
+
}
|
|
1240
|
+
resetIdentity() {
|
|
1241
|
+
this.log("\u88AB\u8D26\u53F7\u89E3\u7ED1\uFF0C\u6B63\u5728\u91CD\u7F6E\u8EAB\u4EFD\u5E76\u91CD\u65B0\u751F\u6210\u914D\u5BF9\u7801\u2026");
|
|
1242
|
+
this.flushApprovals("\u88AB\u8D26\u53F7\u89E3\u7ED1\uFF0C\u81EA\u52A8\u62D2\u7EDD\uFF08fail-closed\uFF09");
|
|
1243
|
+
this.claimed = false;
|
|
1244
|
+
this.state.agentId = randomBytes2(16).toString("hex");
|
|
1245
|
+
this.state.claimed = false;
|
|
1246
|
+
saveState(this.state, AGENT_TYPE);
|
|
1247
|
+
this.ws.close();
|
|
1248
|
+
this.connect(this.relayBase);
|
|
1249
|
+
}
|
|
1250
|
+
};
|
|
1251
|
+
function localTimezone() {
|
|
1252
|
+
try {
|
|
1253
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1254
|
+
if (tz) return tz;
|
|
1255
|
+
} catch {
|
|
1256
|
+
}
|
|
1257
|
+
return "";
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// src/index.ts
|
|
1261
|
+
var ENV_PREFIX = "TUNNELBOX_CLAUDE_ENV_";
|
|
1262
|
+
var VALID_MODES = /* @__PURE__ */ new Set(["default", "plan", "acceptEdits", "bypassPermissions", "dontAsk", "auto"]);
|
|
1263
|
+
function envOn(name) {
|
|
1264
|
+
return /^(1|true|yes|on)$/i.test(process.env[name] || "");
|
|
1265
|
+
}
|
|
1266
|
+
function parseMode(raw) {
|
|
1267
|
+
const m = (raw || "").trim().toLowerCase();
|
|
1268
|
+
if (!m) return void 0;
|
|
1269
|
+
if (!VALID_MODES.has(m)) {
|
|
1270
|
+
console.warn(`[tunnelbox:claude] \u5FFD\u7565\u65E0\u6548 TUNNELBOX_CLAUDE_MODE: ${raw}\uFF08\u5141\u8BB8 ${[...VALID_MODES].join("/")}\uFF09`);
|
|
1271
|
+
return void 0;
|
|
1272
|
+
}
|
|
1273
|
+
return m;
|
|
1274
|
+
}
|
|
1275
|
+
function parseThinking(raw) {
|
|
1276
|
+
const s = (raw || "").trim().toLowerCase();
|
|
1277
|
+
if (!s) return void 0;
|
|
1278
|
+
const as = (o) => o;
|
|
1279
|
+
if (s === "off" || s === "disabled") return as({ type: "disabled" });
|
|
1280
|
+
if (s === "adaptive") return as({ type: "adaptive" });
|
|
1281
|
+
if (s === "on" || s === "enabled") return as({ type: "enabled" });
|
|
1282
|
+
const m = s.match(/^enabled:(\d+)$/);
|
|
1283
|
+
if (m) return as({ type: "enabled", budgetTokens: Number(m[1]) });
|
|
1284
|
+
console.warn(`[tunnelbox:claude] \u5FFD\u7565\u65E0\u6548 TUNNELBOX_CLAUDE_THINKING: ${raw}\uFF08off|adaptive|enabled[:budget]\uFF09`);
|
|
1285
|
+
return void 0;
|
|
1286
|
+
}
|
|
1287
|
+
function parseEnvPassthrough() {
|
|
1288
|
+
const out = {};
|
|
1289
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
1290
|
+
if (k.startsWith(ENV_PREFIX) && v !== void 0 && v !== "") {
|
|
1291
|
+
out[k.slice(ENV_PREFIX.length)] = v;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return Object.keys(out).length ? out : void 0;
|
|
1295
|
+
}
|
|
1296
|
+
function parseDialogKinds(raw) {
|
|
1297
|
+
const kinds = (raw || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1298
|
+
return kinds.length ? kinds : void 0;
|
|
1299
|
+
}
|
|
1300
|
+
function start(opts = {}) {
|
|
1301
|
+
const cfg = {
|
|
1302
|
+
relayUrl: (process.env.TUNNELBOX_RELAY_URL || opts.relayUrl || "").trim(),
|
|
1303
|
+
cwd: process.env.TUNNELBOX_CWD || opts.cwd || process.cwd(),
|
|
1304
|
+
mode: parseMode(process.env.TUNNELBOX_CLAUDE_MODE),
|
|
1305
|
+
model: process.env.TUNNELBOX_CLAUDE_MODEL?.trim() || void 0,
|
|
1306
|
+
thinking: parseThinking(process.env.TUNNELBOX_CLAUDE_THINKING),
|
|
1307
|
+
maxThinkingTokens: parseMaxThinking(process.env.TUNNELBOX_CLAUDE_MAX_THINKING_TOKENS),
|
|
1308
|
+
env: parseEnvPassthrough(),
|
|
1309
|
+
warm: envOn("TUNNELBOX_CLAUDE_WARM"),
|
|
1310
|
+
dialogKinds: parseDialogKinds(process.env.TUNNELBOX_CLAUDE_DIALOG_KINDS),
|
|
1311
|
+
pluginDir: process.env.TUNNELBOX_CLAUDE_PLUGIN_DIR?.trim() || void 0
|
|
1312
|
+
};
|
|
1313
|
+
const bridge = new ClaudeBridge(cfg);
|
|
1314
|
+
void bridge.start().catch((e) => {
|
|
1315
|
+
console.error("[tunnelbox:claude] \u542F\u52A8\u5931\u8D25:", e);
|
|
1316
|
+
});
|
|
1317
|
+
let stopped = false;
|
|
1318
|
+
return {
|
|
1319
|
+
stop() {
|
|
1320
|
+
if (stopped) return;
|
|
1321
|
+
stopped = true;
|
|
1322
|
+
bridge.dispose();
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
function parseMaxThinking(raw) {
|
|
1327
|
+
if (!raw) return void 0;
|
|
1328
|
+
const n = Number(raw);
|
|
1329
|
+
if (!Number.isFinite(n) || n <= 0) return void 0;
|
|
1330
|
+
return Math.floor(n);
|
|
1331
|
+
}
|
|
1332
|
+
var isEntry = (() => {
|
|
1333
|
+
try {
|
|
1334
|
+
return !!process.argv[1] && resolve2(process.argv[1]) === fileURLToPath2(import.meta.url);
|
|
1335
|
+
} catch {
|
|
1336
|
+
return false;
|
|
1337
|
+
}
|
|
1338
|
+
})();
|
|
1339
|
+
if (isEntry) {
|
|
1340
|
+
const { stop } = start();
|
|
1341
|
+
const onSignal = () => {
|
|
1342
|
+
stop();
|
|
1343
|
+
process.exit(0);
|
|
1344
|
+
};
|
|
1345
|
+
process.once("SIGINT", onSignal);
|
|
1346
|
+
process.once("SIGTERM", onSignal);
|
|
1347
|
+
console.log("[tunnelbox:claude] Claude Code \u9002\u914D\u5668\u5DF2\u542F\u52A8\uFF08Ctrl+C \u9000\u51FA\uFF09");
|
|
1348
|
+
}
|
|
1349
|
+
export {
|
|
1350
|
+
start
|
|
1351
|
+
};
|