sessix-server 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.
Files changed (61) hide show
  1. package/dist/approval/ApprovalProxy.d.ts +86 -0
  2. package/dist/approval/ApprovalProxy.d.ts.map +1 -0
  3. package/dist/approval/ApprovalProxy.js +363 -0
  4. package/dist/approval/ApprovalProxy.js.map +1 -0
  5. package/dist/hooks/HookInstaller.d.ts +55 -0
  6. package/dist/hooks/HookInstaller.d.ts.map +1 -0
  7. package/dist/hooks/HookInstaller.js +215 -0
  8. package/dist/hooks/HookInstaller.js.map +1 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +3115 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/mdns/MdnsService.d.ts +36 -0
  14. package/dist/mdns/MdnsService.d.ts.map +1 -0
  15. package/dist/mdns/MdnsService.js +66 -0
  16. package/dist/mdns/MdnsService.js.map +1 -0
  17. package/dist/notification/ActivityPushChannel.d.ts +54 -0
  18. package/dist/notification/ActivityPushChannel.d.ts.map +1 -0
  19. package/dist/notification/ActivityPushChannel.js +235 -0
  20. package/dist/notification/ActivityPushChannel.js.map +1 -0
  21. package/dist/notification/ExpoNotificationChannel.d.ts +17 -0
  22. package/dist/notification/ExpoNotificationChannel.d.ts.map +1 -0
  23. package/dist/notification/ExpoNotificationChannel.js +57 -0
  24. package/dist/notification/ExpoNotificationChannel.js.map +1 -0
  25. package/dist/notification/MacNotificationChannel.d.ts +22 -0
  26. package/dist/notification/MacNotificationChannel.d.ts.map +1 -0
  27. package/dist/notification/MacNotificationChannel.js +33 -0
  28. package/dist/notification/MacNotificationChannel.js.map +1 -0
  29. package/dist/notification/NotificationService.d.ts +50 -0
  30. package/dist/notification/NotificationService.d.ts.map +1 -0
  31. package/dist/notification/NotificationService.js +177 -0
  32. package/dist/notification/NotificationService.js.map +1 -0
  33. package/dist/providers/ExecutionProvider.d.ts +60 -0
  34. package/dist/providers/ExecutionProvider.d.ts.map +1 -0
  35. package/dist/providers/ExecutionProvider.js +3 -0
  36. package/dist/providers/ExecutionProvider.js.map +1 -0
  37. package/dist/providers/ProcessProvider.d.ts +117 -0
  38. package/dist/providers/ProcessProvider.d.ts.map +1 -0
  39. package/dist/providers/ProcessProvider.js +507 -0
  40. package/dist/providers/ProcessProvider.js.map +1 -0
  41. package/dist/server.d.ts +32 -0
  42. package/dist/server.d.ts.map +1 -0
  43. package/dist/server.js +3054 -0
  44. package/dist/server.js.map +1 -0
  45. package/dist/session/ProjectReader.d.ts +44 -0
  46. package/dist/session/ProjectReader.d.ts.map +1 -0
  47. package/dist/session/ProjectReader.js +471 -0
  48. package/dist/session/ProjectReader.js.map +1 -0
  49. package/dist/session/SessionFileWatcher.d.ts +35 -0
  50. package/dist/session/SessionFileWatcher.d.ts.map +1 -0
  51. package/dist/session/SessionFileWatcher.js +207 -0
  52. package/dist/session/SessionFileWatcher.js.map +1 -0
  53. package/dist/session/SessionManager.d.ts +114 -0
  54. package/dist/session/SessionManager.d.ts.map +1 -0
  55. package/dist/session/SessionManager.js +356 -0
  56. package/dist/session/SessionManager.js.map +1 -0
  57. package/dist/ws/WsBridge.d.ts +55 -0
  58. package/dist/ws/WsBridge.d.ts.map +1 -0
  59. package/dist/ws/WsBridge.js +220 -0
  60. package/dist/ws/WsBridge.js.map +1 -0
  61. package/package.json +38 -0
package/dist/index.js ADDED
@@ -0,0 +1,3115 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_node_os5 = require("os");
28
+
29
+ // src/server.ts
30
+ var import_uuid4 = require("uuid");
31
+ var import_promises4 = require("fs/promises");
32
+ var import_node_os4 = require("os");
33
+ var import_node_path4 = require("path");
34
+ var import_node_child_process2 = require("child_process");
35
+ var import_node_util = require("util");
36
+
37
+ // src/providers/ProcessProvider.ts
38
+ var import_child_process = require("child_process");
39
+ var import_readline = require("readline");
40
+ var import_events = require("events");
41
+ var import_node_os = require("os");
42
+ var import_uuid = require("uuid");
43
+ function findClaudePath() {
44
+ try {
45
+ return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
46
+ } catch {
47
+ const candidates = [
48
+ `${process.env.HOME}/.local/bin/claude`,
49
+ "/usr/local/bin/claude",
50
+ "/opt/homebrew/bin/claude"
51
+ ];
52
+ for (const candidate of candidates) {
53
+ try {
54
+ (0, import_child_process.execSync)(`test -x "${candidate}"`);
55
+ return candidate;
56
+ } catch {
57
+ }
58
+ }
59
+ return "claude";
60
+ }
61
+ }
62
+ var CLAUDE_PATH = findClaudePath();
63
+ var ProcessProvider = class {
64
+ /** 活跃会话映射表:sessionId -> { session, process } */
65
+ activeSessions = /* @__PURE__ */ new Map();
66
+ /** 事件发射器,用于分发 Claude 事件流 */
67
+ emitter = new import_events.EventEmitter();
68
+ /** 已发射的 AskUserQuestion toolUseId 集合(避免 partial message 重复触发) */
69
+ emittedQuestionToolUseIds = /* @__PURE__ */ new Set();
70
+ /**
71
+ * 启动新会话或恢复已有会话
72
+ *
73
+ * 会 spawn 一个 `claude` CLI 进程,设置工作目录和环境变量,
74
+ * 并开始监听 stdout 的 NDJSON 输出。
75
+ */
76
+ async startSession(opts) {
77
+ const { projectPath, message, sessionId: existingSessionId, model, permissionMode, effort, images } = opts;
78
+ const sessionId = existingSessionId ?? (0, import_uuid.v4)();
79
+ if (this.activeSessions.has(sessionId)) {
80
+ await this.killSession(sessionId);
81
+ }
82
+ const projectId = projectPath.split("/").filter(Boolean).pop() ?? "unknown";
83
+ const session = {
84
+ id: sessionId,
85
+ projectId,
86
+ projectPath,
87
+ status: "running",
88
+ createdAt: Date.now(),
89
+ lastActiveAt: Date.now(),
90
+ summary: message.slice(0, 80)
91
+ };
92
+ const resume = opts.resume ?? !!existingSessionId;
93
+ const proc = this.spawnClaudeProcess(sessionId, projectPath, resume, model, permissionMode, effort);
94
+ this.writeUserMessage(proc, message, sessionId, images);
95
+ session.pid = proc.pid;
96
+ this.activeSessions.set(sessionId, { session, process: proc, model, permissionMode, effort });
97
+ proc.on("error", (err) => {
98
+ console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} \u8FDB\u7A0B\u9519\u8BEF:`, err.message);
99
+ this.activeSessions.delete(sessionId);
100
+ const syntheticResult = {
101
+ type: "result",
102
+ subtype: "error",
103
+ result: `\u8FDB\u7A0B\u542F\u52A8\u5931\u8D25: ${err.message}`,
104
+ session_id: sessionId,
105
+ duration_ms: 0,
106
+ is_error: true,
107
+ num_turns: 0
108
+ };
109
+ this.emitter.emit(this.getEventName(sessionId), syntheticResult);
110
+ });
111
+ this.attachStdoutListener(sessionId, proc);
112
+ this.attachStderrListener(sessionId, proc);
113
+ this.attachExitListener(sessionId, proc);
114
+ return session;
115
+ }
116
+ /**
117
+ * 终止指定会话
118
+ *
119
+ * kill 进程并从活跃映射中移除。
120
+ */
121
+ async killSession(sessionId) {
122
+ const entry = this.activeSessions.get(sessionId);
123
+ if (!entry) {
124
+ return;
125
+ }
126
+ if (entry.process.exitCode === null && entry.process.signalCode === null) {
127
+ try {
128
+ entry.process.stdin?.end();
129
+ } catch {
130
+ }
131
+ entry.process.kill("SIGTERM");
132
+ await new Promise((resolve) => {
133
+ const timeout = setTimeout(() => {
134
+ if (entry.process.exitCode === null && entry.process.signalCode === null) {
135
+ entry.process.kill("SIGKILL");
136
+ }
137
+ resolve();
138
+ }, 3e3);
139
+ entry.process.once("exit", () => {
140
+ clearTimeout(timeout);
141
+ resolve();
142
+ });
143
+ });
144
+ }
145
+ this.activeSessions.delete(sessionId);
146
+ }
147
+ /**
148
+ * 向已有会话发送新消息
149
+ *
150
+ * 快速路径:进程存活时直接写 stdin(毫秒级响应)。
151
+ * 慢速路径:进程已退出时 respawn 并 --resume。
152
+ */
153
+ async sendMessage(sessionId, message, permissionMode, images) {
154
+ const entry = this.activeSessions.get(sessionId);
155
+ if (!entry) {
156
+ throw new Error(`\u4F1A\u8BDD ${sessionId} \u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F`);
157
+ }
158
+ const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
159
+ if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
160
+ this.writeUserMessage(entry.process, message, sessionId, images);
161
+ return;
162
+ }
163
+ if (modeChanged) {
164
+ console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u6743\u9650\u6A21\u5F0F\u5207\u6362 ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}\uFF0Crespawn`);
165
+ if (entry.process.exitCode === null && entry.process.signalCode === null) {
166
+ try {
167
+ entry.process.stdin?.end();
168
+ } catch {
169
+ }
170
+ entry.process.kill("SIGTERM");
171
+ }
172
+ } else {
173
+ console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF0Crespawn \u91CD\u65B0\u542F\u52A8`);
174
+ }
175
+ const savedPendingQuestion = entry.pendingQuestion;
176
+ const newMode = permissionMode ?? entry.permissionMode;
177
+ const proc = this.spawnClaudeProcess(sessionId, entry.session.projectPath, true, entry.model, newMode, entry.effort);
178
+ this.writeUserMessage(proc, message, sessionId, images);
179
+ entry.session.status = "running";
180
+ entry.session.lastActiveAt = Date.now();
181
+ entry.session.pid = proc.pid;
182
+ entry.process = proc;
183
+ entry.permissionMode = newMode;
184
+ entry.pendingQuestion = savedPendingQuestion;
185
+ proc.on("error", (err) => {
186
+ console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} sendMessage \u8FDB\u7A0B\u9519\u8BEF:`, err.message);
187
+ this.activeSessions.delete(sessionId);
188
+ const syntheticResult = {
189
+ type: "result",
190
+ subtype: "error",
191
+ result: `\u6D88\u606F\u53D1\u9001\u5931\u8D25: ${err.message}`,
192
+ session_id: sessionId,
193
+ duration_ms: 0,
194
+ is_error: true,
195
+ num_turns: 0
196
+ };
197
+ this.emitter.emit(this.getEventName(sessionId), syntheticResult);
198
+ });
199
+ this.attachStdoutListener(sessionId, proc);
200
+ this.attachStderrListener(sessionId, proc);
201
+ this.attachExitListener(sessionId, proc);
202
+ }
203
+ /**
204
+ * 订阅指定会话的 Claude 事件流
205
+ *
206
+ * @returns 取消订阅函数
207
+ */
208
+ onEvent(sessionId, callback) {
209
+ const eventName = this.getEventName(sessionId);
210
+ this.emitter.on(eventName, callback);
211
+ return () => {
212
+ this.emitter.off(eventName, callback);
213
+ };
214
+ }
215
+ /**
216
+ * 获取当前所有活跃会话列表
217
+ */
218
+ getActiveSessions() {
219
+ return Array.from(this.activeSessions.values()).map((entry) => entry.session);
220
+ }
221
+ // ============================================
222
+ // 私有方法
223
+ // ============================================
224
+ /**
225
+ * 启动 claude CLI 进程(持久模式,stdin 保持开放接收多条消息)
226
+ */
227
+ spawnClaudeProcess(sessionId, projectPath, resume = false, model, permissionMode, effort) {
228
+ const args = [
229
+ "--input-format",
230
+ "stream-json",
231
+ "--output-format",
232
+ "stream-json",
233
+ "--verbose",
234
+ "--include-partial-messages"
235
+ ];
236
+ if (resume) {
237
+ args.push("--resume", sessionId);
238
+ } else {
239
+ args.push("--session-id", sessionId);
240
+ }
241
+ if (model) {
242
+ args.push("--model", model);
243
+ }
244
+ if (permissionMode && permissionMode !== "default") {
245
+ args.push("--permission-mode", permissionMode);
246
+ }
247
+ if (effort) {
248
+ args.push("--effort", effort);
249
+ }
250
+ const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
251
+ delete env.CLAUDECODE;
252
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, args, {
253
+ cwd: projectPath,
254
+ env,
255
+ stdio: ["pipe", "pipe", "pipe"]
256
+ });
257
+ return proc;
258
+ }
259
+ /**
260
+ * 向持久进程的 stdin 写入一条用户消息(NDJSON 格式)
261
+ *
262
+ * 写入失败时合成 error result 事件,确保 SessionManager 能感知到失败。
263
+ */
264
+ writeUserMessage(proc, message, sessionId, images) {
265
+ const content = [];
266
+ if (images?.length) {
267
+ for (const img of images) {
268
+ content.push({
269
+ type: "image",
270
+ source: { type: "base64", media_type: img.media_type, data: img.data }
271
+ });
272
+ }
273
+ }
274
+ content.push({ type: "text", text: message });
275
+ const payload = JSON.stringify({
276
+ type: "user",
277
+ session_id: "",
278
+ message: { role: "user", content },
279
+ parent_tool_use_id: null
280
+ });
281
+ if (!proc.stdin || proc.stdin.destroyed) {
282
+ console.error(`[ProcessProvider] stdin \u4E0D\u53EF\u7528\uFF0C\u6D88\u606F\u4E22\u5931`);
283
+ if (sessionId) {
284
+ this.emitWriteError(sessionId, "\u8FDB\u7A0B stdin \u5DF2\u5173\u95ED\uFF0C\u6D88\u606F\u672A\u9001\u8FBE");
285
+ }
286
+ return;
287
+ }
288
+ proc.stdin.write(payload + "\n", (err) => {
289
+ if (err && sessionId) {
290
+ console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} stdin \u5199\u5165\u5931\u8D25:`, err.message);
291
+ this.emitWriteError(sessionId, `\u6D88\u606F\u53D1\u9001\u5931\u8D25: ${err.message}`);
292
+ }
293
+ });
294
+ }
295
+ /**
296
+ * 发出写入失败的合成错误事件
297
+ */
298
+ emitWriteError(sessionId, message) {
299
+ const syntheticResult = {
300
+ type: "result",
301
+ subtype: "error",
302
+ result: message,
303
+ session_id: sessionId,
304
+ duration_ms: 0,
305
+ is_error: true,
306
+ num_turns: 0
307
+ };
308
+ this.emitter.emit(this.getEventName(sessionId), syntheticResult);
309
+ }
310
+ /**
311
+ * 挂载 stdout 监听器,逐行解析 NDJSON
312
+ */
313
+ attachStdoutListener(sessionId, proc) {
314
+ if (!proc.stdout) {
315
+ console.warn(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: stdout \u4E0D\u53EF\u7528`);
316
+ return;
317
+ }
318
+ const rl = (0, import_readline.createInterface)({
319
+ input: proc.stdout,
320
+ crlfDelay: Infinity
321
+ });
322
+ const entry = this.activeSessions.get(sessionId);
323
+ if (entry) {
324
+ entry.rl = rl;
325
+ }
326
+ rl.on("line", (line) => {
327
+ const trimmed = line.trim();
328
+ if (!trimmed) return;
329
+ const result = this.parseLine(trimmed);
330
+ if (result.ok) {
331
+ const event = result.value;
332
+ if (event.type === "assistant") {
333
+ for (const block of event.message.content) {
334
+ if (block.type === "tool_use" && block.name === "AskUserQuestion") {
335
+ const input = block.input;
336
+ const question = input.question ?? "";
337
+ if (!question) continue;
338
+ const prevKey = `${block.id}:${question}:${JSON.stringify(input.options ?? [])}`;
339
+ if (this.emittedQuestionToolUseIds.has(prevKey)) continue;
340
+ this.emittedQuestionToolUseIds.add(prevKey);
341
+ this.emitter.emit(this.getQuestionEventName(sessionId), {
342
+ toolUseId: block.id,
343
+ question,
344
+ options: input.options
345
+ });
346
+ }
347
+ }
348
+ }
349
+ this.updateSessionStatus(sessionId, event);
350
+ this.emitter.emit(this.getEventName(sessionId), event);
351
+ } else {
352
+ console.warn(
353
+ `[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u65E0\u6CD5\u89E3\u6790\u884C: ${trimmed.substring(0, 100)}`
354
+ );
355
+ }
356
+ });
357
+ }
358
+ /**
359
+ * 挂载 stderr 监听器,记录日志
360
+ */
361
+ attachStderrListener(sessionId, proc) {
362
+ if (!proc.stderr) return;
363
+ proc.stderr.on("data", (data) => {
364
+ const text = data.toString().trim();
365
+ if (text) {
366
+ console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} stderr: ${text}`);
367
+ }
368
+ });
369
+ }
370
+ /**
371
+ * 挂载进程退出监听器
372
+ *
373
+ * 当进程退出时发出合成的 result 事件,确保 SessionManager 能感知到退出。
374
+ * 正常退出时 Claude 会先通过 stdout 发送真实 result 事件,
375
+ * updateSessionStatus 会将 session.status 更新为 idle/error。
376
+ * 此时合成事件会重复触发,导致手机端出现两张总结卡。
377
+ * 修复:已收到真实 result(status 已为 idle/error)时跳过合成事件。
378
+ * 异常退出时(crash/OOM/killed)没有真实 result 事件,合成事件确保状态正确广播。
379
+ */
380
+ attachExitListener(sessionId, proc) {
381
+ proc.once("exit", (code, signal) => {
382
+ const entry = this.activeSessions.get(sessionId);
383
+ if (!entry) return;
384
+ if (entry.process !== proc) return;
385
+ if (entry.rl) {
386
+ entry.rl.close();
387
+ entry.rl = void 0;
388
+ }
389
+ entry.session.pid = void 0;
390
+ entry.session.lastActiveAt = Date.now();
391
+ const alreadyHasResult = entry.session.status === "idle" || entry.session.status === "error";
392
+ if (alreadyHasResult) return;
393
+ const isNormal = code === 0 || code === 143 || signal === "SIGTERM";
394
+ entry.session.status = isNormal ? "idle" : "error";
395
+ if (!isNormal) {
396
+ console.error(
397
+ `[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u8FDB\u7A0B\u5F02\u5E38\u9000\u51FA code=${code} signal=${signal}`
398
+ );
399
+ }
400
+ const syntheticResult = {
401
+ type: "result",
402
+ subtype: isNormal ? "success" : "error",
403
+ session_id: sessionId,
404
+ is_error: !isNormal,
405
+ result: isNormal ? "" : `\u8FDB\u7A0B\u9000\u51FA code=${code} signal=${signal}`,
406
+ duration_ms: 0,
407
+ num_turns: 0
408
+ };
409
+ this.emitter.emit(this.getEventName(sessionId), syntheticResult);
410
+ });
411
+ }
412
+ /**
413
+ * 解析一行 NDJSON 文本为 ClaudeStreamEvent
414
+ */
415
+ parseLine(line) {
416
+ try {
417
+ const parsed = JSON.parse(line);
418
+ return { ok: true, value: parsed };
419
+ } catch (err) {
420
+ return {
421
+ ok: false,
422
+ error: err instanceof Error ? err : new Error(String(err))
423
+ };
424
+ }
425
+ }
426
+ /**
427
+ * 根据 Claude 事件更新会话状态
428
+ */
429
+ updateSessionStatus(sessionId, event) {
430
+ const entry = this.activeSessions.get(sessionId);
431
+ if (!entry) return;
432
+ entry.session.lastActiveAt = Date.now();
433
+ switch (event.type) {
434
+ case "system":
435
+ if (event.subtype === "init") {
436
+ entry.session.status = "running";
437
+ }
438
+ break;
439
+ case "assistant":
440
+ entry.session.status = "running";
441
+ break;
442
+ case "result":
443
+ entry.session.status = event.is_error ? "error" : "idle";
444
+ break;
445
+ }
446
+ }
447
+ /**
448
+ * 根据对话上下文生成下一步建议指令
449
+ *
450
+ * 使用 --output-format text 做一次性调用,返回纯文本结果。
451
+ */
452
+ async generateSuggestion(context) {
453
+ const prompt = `\u4F60\u662F\u4E00\u4E2A AI \u7F16\u7A0B\u52A9\u624B\u3002\u6839\u636E\u4EE5\u4E0B Claude Code \u5BF9\u8BDD\u4E0A\u4E0B\u6587\uFF0C\u5EFA\u8BAE\u7528\u6237\u4E0B\u4E00\u6B65\u6700\u6709\u4EF7\u503C\u7684\u4E00\u6761\u6307\u4EE4\uFF08\u76F4\u63A5\u7ED9\u51FA\u6307\u4EE4\u5185\u5BB9\uFF0C\u4E0D\u8981\u89E3\u91CA\uFF0C\u4E0D\u8981\u52A0\u5F15\u53F7\uFF09\uFF1A
454
+
455
+ ${context}`;
456
+ return new Promise((resolve, reject) => {
457
+ const env = { ...process.env };
458
+ delete env.CLAUDECODE;
459
+ const proc = (0, import_child_process.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
460
+ cwd: (0, import_node_os.homedir)(),
461
+ env,
462
+ stdio: ["pipe", "pipe", "pipe"]
463
+ });
464
+ proc.stdin.end();
465
+ let output = "";
466
+ proc.stdout?.on("data", (data) => {
467
+ output += data.toString();
468
+ });
469
+ proc.once("exit", (code) => {
470
+ if (code === 0) {
471
+ resolve(output.trim());
472
+ } else {
473
+ reject(new Error(`generateSuggestion \u8FDB\u7A0B\u9000\u51FA\u7801: ${code}`));
474
+ }
475
+ });
476
+ proc.once("error", reject);
477
+ });
478
+ }
479
+ /**
480
+ * 向正在等待中的 AskUserQuestion 提供答案
481
+ *
482
+ * 将答案写入 Claude 进程的 stdin(作为 tool_result),
483
+ * Claude 收到后继续执行。
484
+ */
485
+ async answerQuestion(sessionId, toolUseId, answer) {
486
+ const entry = this.activeSessions.get(sessionId);
487
+ if (!entry) {
488
+ throw new Error(`\u4F1A\u8BDD ${sessionId} \u4E0D\u5B58\u5728`);
489
+ }
490
+ if (!entry.process.stdin || entry.process.stdin.destroyed) {
491
+ throw new Error(`\u4F1A\u8BDD ${sessionId} stdin \u4E0D\u53EF\u7528`);
492
+ }
493
+ const toolResult = JSON.stringify({
494
+ type: "tool_result",
495
+ tool_use_id: toolUseId,
496
+ content: answer
497
+ });
498
+ await new Promise((resolve, reject) => {
499
+ entry.process.stdin.write(toolResult + "\n", (err) => {
500
+ if (err) reject(err);
501
+ else resolve();
502
+ });
503
+ });
504
+ console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u56DE\u7B54 (toolUseId=${toolUseId})`);
505
+ }
506
+ /**
507
+ * 订阅指定会话的 AskUserQuestion 事件
508
+ *
509
+ * @returns 取消订阅函数
510
+ */
511
+ onQuestion(sessionId, callback) {
512
+ const eventName = this.getQuestionEventName(sessionId);
513
+ this.emitter.on(eventName, callback);
514
+ return () => {
515
+ this.emitter.off(eventName, callback);
516
+ };
517
+ }
518
+ /**
519
+ * 生成事件名称
520
+ */
521
+ getEventName(sessionId) {
522
+ return `claude:${sessionId}`;
523
+ }
524
+ /**
525
+ * 生成 AskUserQuestion 内部事件名称
526
+ */
527
+ getQuestionEventName(sessionId) {
528
+ return `question:${sessionId}`;
529
+ }
530
+ };
531
+
532
+ // src/session/SessionManager.ts
533
+ var import_uuid2 = require("uuid");
534
+ var BUFFER_MAX = 5e3;
535
+ var SessionManager = class {
536
+ provider;
537
+ /** 事件回调列表(事件会被转发到 WsBridge) */
538
+ eventCallbacks = [];
539
+ /** 每个会话的事件流取消订阅函数 */
540
+ unsubscribeMap = /* @__PURE__ */ new Map();
541
+ /** 每个会话的事件缓冲区(用于新订阅者重放)*/
542
+ sessionEventBuffers = /* @__PURE__ */ new Map();
543
+ /** AskUserQuestion 问题映射:requestId → resolve 回调 */
544
+ pendingQuestions = /* @__PURE__ */ new Map();
545
+ /**
546
+ * 会话状态缓存(用于追踪 status 变化,检测 oldStatus !== newStatus 时广播)
547
+ *
548
+ * 这是 status 变化的唯一检测源。ProcessProvider 的 session.status 是实际值,
549
+ * 这里只缓存上次广播的值,用于去重。
550
+ */
551
+ lastBroadcastStatus = /* @__PURE__ */ new Map();
552
+ /** 每个会话的服务器端累计统计 */
553
+ sessionStats = /* @__PURE__ */ new Map();
554
+ /** 每个会话进入 running 状态的 wall-clock 起始时间 */
555
+ runningStartedAt = /* @__PURE__ */ new Map();
556
+ /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
557
+ pendingAssistantEvents = /* @__PURE__ */ new Map();
558
+ constructor(provider) {
559
+ this.provider = provider;
560
+ }
561
+ // ============================================
562
+ // 公开 API
563
+ // ============================================
564
+ /**
565
+ * 创建新会话
566
+ *
567
+ * 调用 provider.startSession(),订阅事件流,
568
+ * 将 ClaudeStreamEvent 包装为 ServerEvent 转发。
569
+ */
570
+ async createSession(projectPath, message, resumeSessionId, newSessionId, model, permissionMode, effort, images) {
571
+ const session = await this.provider.startSession({
572
+ projectPath,
573
+ message,
574
+ sessionId: resumeSessionId ?? newSessionId,
575
+ resume: !!resumeSessionId,
576
+ model,
577
+ permissionMode,
578
+ effort,
579
+ images
580
+ });
581
+ this.lastBroadcastStatus.set(session.id, session.status);
582
+ this.unsubscribeSession(session.id);
583
+ this.subscribeToSession(session.id);
584
+ console.log(`[SessionManager] \u4F1A\u8BDD\u5DF2\u521B\u5EFA: ${session.id} (\u9879\u76EE: ${projectPath})`);
585
+ return session;
586
+ }
587
+ /**
588
+ * 发送消息到已有会话
589
+ */
590
+ async sendMessage(sessionId, message, permissionMode, images) {
591
+ await this.provider.sendMessage(sessionId, message, permissionMode, images);
592
+ this.updateSessionStatus(sessionId, "running");
593
+ console.log(`[SessionManager] \u6D88\u606F\u5DF2\u53D1\u9001\u5230\u4F1A\u8BDD: ${sessionId}`);
594
+ }
595
+ /**
596
+ * 终止会话
597
+ */
598
+ async killSession(sessionId) {
599
+ this.unsubscribeSession(sessionId);
600
+ this.clearPendingQuestions(sessionId);
601
+ this.lastBroadcastStatus.delete(sessionId);
602
+ this.sessionEventBuffers.delete(sessionId);
603
+ this.sessionStats.delete(sessionId);
604
+ const pending = this.pendingAssistantEvents.get(sessionId);
605
+ if (pending) {
606
+ clearTimeout(pending.timer);
607
+ this.pendingAssistantEvents.delete(sessionId);
608
+ }
609
+ await this.provider.killSession(sessionId);
610
+ console.log(`[SessionManager] \u4F1A\u8BDD\u5DF2\u7EC8\u6B62: ${sessionId}`);
611
+ }
612
+ /**
613
+ * 获取会话的缓冲事件(用于新订阅者重放)
614
+ */
615
+ getSessionEvents(sessionId) {
616
+ return this.sessionEventBuffers.get(sessionId) ?? [];
617
+ }
618
+ /**
619
+ * 处理 AskUserQuestion 回答(从手机端传来)
620
+ */
621
+ handleQuestionResponse(requestId, answer) {
622
+ const pending = this.pendingQuestions.get(requestId);
623
+ if (!pending) {
624
+ console.warn(`[SessionManager] \u672A\u627E\u5230\u95EE\u9898\u8BF7\u6C42: ${requestId}`);
625
+ return;
626
+ }
627
+ this.pendingQuestions.delete(requestId);
628
+ this.updateSessionStatus(pending.sessionId, "running");
629
+ pending.resolve(answer);
630
+ console.log(`[SessionManager] \u95EE\u9898\u5DF2\u56DE\u7B54: ${requestId}`);
631
+ }
632
+ /**
633
+ * 获取所有活跃会话(含服务器端统计)
634
+ */
635
+ getActiveSessions() {
636
+ return this.provider.getActiveSessions().map((session) => {
637
+ const stats = this.getSessionStats(session.id);
638
+ return stats ? { ...session, stats } : session;
639
+ });
640
+ }
641
+ /**
642
+ * 注册事件回调(事件会被转发到 WsBridge)
643
+ *
644
+ * @returns 取消注册的函数
645
+ */
646
+ onEvent(callback) {
647
+ this.eventCallbacks.push(callback);
648
+ return () => {
649
+ const index = this.eventCallbacks.indexOf(callback);
650
+ if (index !== -1) {
651
+ this.eventCallbacks.splice(index, 1);
652
+ }
653
+ };
654
+ }
655
+ /**
656
+ * 清理所有资源
657
+ */
658
+ destroy() {
659
+ for (const [, unsub] of this.unsubscribeMap) {
660
+ unsub();
661
+ }
662
+ this.unsubscribeMap.clear();
663
+ this.sessionEventBuffers.clear();
664
+ this.sessionStats.clear();
665
+ for (const [, pending] of this.pendingAssistantEvents) {
666
+ clearTimeout(pending.timer);
667
+ }
668
+ this.pendingAssistantEvents.clear();
669
+ this.pendingQuestions.clear();
670
+ this.lastBroadcastStatus.clear();
671
+ this.eventCallbacks.length = 0;
672
+ console.log("[SessionManager] \u5DF2\u9500\u6BC1");
673
+ }
674
+ // ============================================
675
+ // 内部方法
676
+ // ============================================
677
+ /**
678
+ * 订阅指定会话的事件流(包括 AskUserQuestion 问题事件)
679
+ */
680
+ subscribeToSession(sessionId) {
681
+ const unsubscribeEvent = this.provider.onEvent(sessionId, (event) => {
682
+ this.handleClaudeEvent(sessionId, event);
683
+ });
684
+ const unsubscribeQuestion = this.provider.onQuestion(
685
+ sessionId,
686
+ ({ toolUseId, question, options }) => {
687
+ this.handleAskUserQuestion(sessionId, toolUseId, question, options);
688
+ }
689
+ );
690
+ this.unsubscribeMap.set(sessionId, () => {
691
+ unsubscribeEvent();
692
+ unsubscribeQuestion();
693
+ });
694
+ }
695
+ /**
696
+ * 取消指定会话的事件订阅
697
+ */
698
+ unsubscribeSession(sessionId) {
699
+ const unsub = this.unsubscribeMap.get(sessionId);
700
+ if (unsub) {
701
+ unsub();
702
+ this.unsubscribeMap.delete(sessionId);
703
+ }
704
+ }
705
+ /**
706
+ * 处理来自 provider 的 Claude 事件
707
+ *
708
+ * - 包装为 ServerEvent 转发
709
+ * - assistant 事件在 30ms 窗口内合并后批量发送(减少 WebSocket 帧数)
710
+ * - 检测 status 变化
711
+ */
712
+ handleClaudeEvent(sessionId, event) {
713
+ const buffer = this.sessionEventBuffers.get(sessionId) ?? [];
714
+ buffer.push(event);
715
+ if (buffer.length > BUFFER_MAX) {
716
+ buffer.splice(0, buffer.length - BUFFER_MAX);
717
+ }
718
+ this.sessionEventBuffers.set(sessionId, buffer);
719
+ if (event.type === "assistant" && Array.isArray(event.message?.content)) {
720
+ const thinkingBlocks = event.message.content.filter((b) => b.type === "thinking");
721
+ if (thinkingBlocks.length > 0) {
722
+ console.log(`[SessionManager] \u{1F9E0} thinking block detected in ${sessionId}: msgId=${event.message.id}, blocks=${thinkingBlocks.length}, len=${thinkingBlocks.map((b) => (b.thinking || "").length).join(",")}`);
723
+ }
724
+ }
725
+ switch (event.type) {
726
+ case "assistant":
727
+ this.bufferAssistantEvent(sessionId, event);
728
+ break;
729
+ case "system":
730
+ this.flushPendingAssistant(sessionId);
731
+ this.emit({ type: "claude_event", sessionId, event });
732
+ if (event.subtype === "init") {
733
+ this.updateSessionStatus(sessionId, "running");
734
+ }
735
+ break;
736
+ case "user":
737
+ this.flushPendingAssistant(sessionId);
738
+ this.emit({ type: "claude_event", sessionId, event });
739
+ break;
740
+ case "result": {
741
+ this.flushPendingAssistant(sessionId);
742
+ this.emit({ type: "claude_event", sessionId, event });
743
+ const stats = this.sessionStats.get(sessionId) ?? {
744
+ totalInputTokens: 0,
745
+ totalOutputTokens: 0,
746
+ totalDurationMs: 0
747
+ };
748
+ if (event.usage) {
749
+ stats.totalInputTokens += event.usage.input_tokens ?? 0;
750
+ stats.totalOutputTokens += event.usage.output_tokens ?? 0;
751
+ }
752
+ if (event.total_cost_usd != null) {
753
+ stats.totalCostUsd = (stats.totalCostUsd ?? 0) + event.total_cost_usd;
754
+ }
755
+ this.sessionStats.set(sessionId, stats);
756
+ if (event.is_error) {
757
+ this.updateSessionStatus(sessionId, "error");
758
+ } else {
759
+ this.updateSessionStatus(sessionId, "idle");
760
+ }
761
+ break;
762
+ }
763
+ }
764
+ }
765
+ /**
766
+ * 缓冲 assistant 事件到 30ms 窗口
767
+ */
768
+ bufferAssistantEvent(sessionId, event) {
769
+ let pending = this.pendingAssistantEvents.get(sessionId);
770
+ if (!pending) {
771
+ pending = {
772
+ events: [],
773
+ timer: setTimeout(() => this.flushPendingAssistant(sessionId), 30)
774
+ };
775
+ this.pendingAssistantEvents.set(sessionId, pending);
776
+ }
777
+ pending.events.push(event);
778
+ }
779
+ /**
780
+ * 刷新缓冲的 assistant 事件,批量发送
781
+ */
782
+ flushPendingAssistant(sessionId) {
783
+ const pending = this.pendingAssistantEvents.get(sessionId);
784
+ if (!pending) return;
785
+ clearTimeout(pending.timer);
786
+ this.pendingAssistantEvents.delete(sessionId);
787
+ if (pending.events.length === 1) {
788
+ this.emit({ type: "claude_event", sessionId, event: pending.events[0] });
789
+ } else if (pending.events.length > 1) {
790
+ this.emit({ type: "claude_events", sessionId, events: pending.events });
791
+ }
792
+ }
793
+ /**
794
+ * 更新会话状态,如果状态发生变化则广播通知
795
+ *
796
+ * 使用 lastBroadcastStatus 去重,只在状态实际变化时广播。
797
+ */
798
+ updateSessionStatus(sessionId, newStatus) {
799
+ const lastStatus = this.lastBroadcastStatus.get(sessionId);
800
+ if (lastStatus !== newStatus) {
801
+ if (lastStatus === "running") {
802
+ const startedAt = this.runningStartedAt.get(sessionId);
803
+ if (startedAt) {
804
+ const stats2 = this.sessionStats.get(sessionId) ?? {
805
+ totalInputTokens: 0,
806
+ totalOutputTokens: 0,
807
+ totalDurationMs: 0
808
+ };
809
+ stats2.totalDurationMs += Date.now() - startedAt;
810
+ this.sessionStats.set(sessionId, stats2);
811
+ this.runningStartedAt.delete(sessionId);
812
+ }
813
+ }
814
+ if (newStatus === "running") {
815
+ this.runningStartedAt.set(sessionId, Date.now());
816
+ }
817
+ this.lastBroadcastStatus.set(sessionId, newStatus);
818
+ const stats = this.getSessionStats(sessionId);
819
+ this.emit({
820
+ type: "status_change",
821
+ sessionId,
822
+ status: newStatus,
823
+ stats
824
+ });
825
+ console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId} \u72B6\u6001\u53D8\u5316: ${lastStatus ?? "(\u65E0)"} \u2192 ${newStatus}`);
826
+ }
827
+ }
828
+ /** 获取会话统计(含 runningStartedAt) */
829
+ getSessionStats(sessionId) {
830
+ const runningStartedAt = this.runningStartedAt.get(sessionId);
831
+ const stats = this.sessionStats.get(sessionId);
832
+ if (!stats && !runningStartedAt) return void 0;
833
+ const base = stats ?? { totalInputTokens: 0, totalOutputTokens: 0, totalDurationMs: 0 };
834
+ return runningStartedAt ? { ...base, runningStartedAt } : base;
835
+ }
836
+ /**
837
+ * 处理 AskUserQuestion 事件:广播问题请求到手机,等待用户回答
838
+ */
839
+ handleAskUserQuestion(sessionId, toolUseId, question, options) {
840
+ const existingEntry = Array.from(this.pendingQuestions.entries()).find(
841
+ ([, v]) => v.toolUseId === toolUseId
842
+ );
843
+ if (existingEntry) {
844
+ const [existingRequestId] = existingEntry;
845
+ const updatedRequest = {
846
+ id: existingRequestId,
847
+ sessionId,
848
+ toolUseId,
849
+ question,
850
+ options,
851
+ createdAt: Date.now()
852
+ };
853
+ this.emit({ type: "question_request", request: updatedRequest });
854
+ console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u66F4\u65B0 (requestId=${existingRequestId})`);
855
+ return;
856
+ }
857
+ const requestId = (0, import_uuid2.v4)();
858
+ const request = {
859
+ id: requestId,
860
+ sessionId,
861
+ toolUseId,
862
+ question,
863
+ options,
864
+ createdAt: Date.now()
865
+ };
866
+ this.updateSessionStatus(sessionId, "waiting_question");
867
+ this.emit({ type: "question_request", request });
868
+ const answerPromise = new Promise((resolve) => {
869
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, resolve });
870
+ });
871
+ answerPromise.then(async (answer) => {
872
+ try {
873
+ await this.provider.answerQuestion(sessionId, toolUseId, answer);
874
+ } catch (err) {
875
+ console.error(`[SessionManager] answerQuestion \u5931\u8D25 (${sessionId}):`, err);
876
+ }
877
+ }).catch((err) => console.error("[SessionManager] answerPromise rejected:", err));
878
+ console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u63A8\u9001 (requestId=${requestId})`);
879
+ }
880
+ /**
881
+ * 清除指定会话的所有待回答问题
882
+ */
883
+ clearPendingQuestions(sessionId) {
884
+ const toRemove = [];
885
+ for (const [requestId, pending] of this.pendingQuestions) {
886
+ if (pending.sessionId === sessionId) {
887
+ toRemove.push(requestId);
888
+ }
889
+ }
890
+ for (const requestId of toRemove) {
891
+ this.pendingQuestions.delete(requestId);
892
+ }
893
+ }
894
+ /**
895
+ * 发出 ServerEvent 到所有已注册的回调
896
+ */
897
+ emit(event) {
898
+ for (const callback of this.eventCallbacks) {
899
+ try {
900
+ callback(event);
901
+ } catch (err) {
902
+ console.error("[SessionManager] \u4E8B\u4EF6\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
903
+ }
904
+ }
905
+ }
906
+ };
907
+
908
+ // src/session/SessionFileWatcher.ts
909
+ var import_chokidar = __toESM(require("chokidar"));
910
+ var import_promises = require("fs/promises");
911
+ var import_node_readline = require("readline");
912
+ var SessionFileWatcher = class {
913
+ watchers = /* @__PURE__ */ new Map();
914
+ onEvent;
915
+ /** 文件无变化后自动停止监听的超时时间(10 分钟) */
916
+ IDLE_TIMEOUT_MS = 10 * 60 * 1e3;
917
+ constructor(onEvent) {
918
+ this.onEvent = onEvent;
919
+ }
920
+ /**
921
+ * 开始监听指定会话的 JSONL 文件新增内容
922
+ *
923
+ * @param sessionId 会话 ID
924
+ * @param filePath JSONL 文件绝对路径
925
+ * @param byteOffset 已读到的字节位置(跳过历史内容,只推送新行)
926
+ */
927
+ watch(sessionId, filePath, byteOffset) {
928
+ if (this.watchers.has(sessionId)) return;
929
+ const watcher = import_chokidar.default.watch(filePath, {
930
+ persistent: false,
931
+ // 不阻止进程退出
932
+ usePolling: false,
933
+ // 使用原生 FS 事件,不轮询
934
+ awaitWriteFinish: {
935
+ stabilityThreshold: 300,
936
+ // 写入停止 300ms 后才触发
937
+ pollInterval: 100
938
+ }
939
+ });
940
+ const entry = {
941
+ filePath,
942
+ byteOffset,
943
+ idleTimer: null,
944
+ watcher
945
+ };
946
+ watcher.on("change", () => {
947
+ this.readNewLines(sessionId).catch((err) => {
948
+ console.error(`[SessionFileWatcher] \u8BFB\u53D6\u5F02\u5E38 ${sessionId}:`, err);
949
+ });
950
+ this.resetIdleTimer(sessionId);
951
+ });
952
+ this.watchers.set(sessionId, entry);
953
+ this.resetIdleTimer(sessionId);
954
+ console.log(`[SessionFileWatcher] \u5F00\u59CB\u76D1\u542C: ${sessionId} (offset=${byteOffset})`);
955
+ }
956
+ /** 停止监听指定会话 */
957
+ unwatch(sessionId) {
958
+ const entry = this.watchers.get(sessionId);
959
+ if (!entry) return;
960
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
961
+ void entry.watcher.close();
962
+ this.watchers.delete(sessionId);
963
+ console.log(`[SessionFileWatcher] \u505C\u6B62\u76D1\u542C: ${sessionId}`);
964
+ }
965
+ /** 停止所有监听(服务关闭时调用) */
966
+ destroy() {
967
+ for (const sessionId of [...this.watchers.keys()]) {
968
+ this.unwatch(sessionId);
969
+ }
970
+ }
971
+ // ============================================
972
+ // 内部方法
973
+ // ============================================
974
+ resetIdleTimer(sessionId) {
975
+ const entry = this.watchers.get(sessionId);
976
+ if (!entry) return;
977
+ if (entry.idleTimer) clearTimeout(entry.idleTimer);
978
+ entry.idleTimer = setTimeout(() => {
979
+ console.log(`[SessionFileWatcher] \u7A7A\u95F2\u8D85\u65F6\uFF0C\u505C\u6B62\u76D1\u542C: ${sessionId}`);
980
+ this.unwatch(sessionId);
981
+ }, this.IDLE_TIMEOUT_MS);
982
+ }
983
+ async readNewLines(sessionId) {
984
+ const entry = this.watchers.get(sessionId);
985
+ if (!entry) return;
986
+ let fileHandle;
987
+ let rl;
988
+ try {
989
+ fileHandle = await (0, import_promises.open)(entry.filePath, "r");
990
+ const fileStat = await fileHandle.stat();
991
+ const newSize = fileStat.size;
992
+ if (newSize <= entry.byteOffset) return;
993
+ rl = (0, import_node_readline.createInterface)({
994
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
995
+ input: fileHandle.createReadStream({ start: entry.byteOffset, encoding: "utf-8" }),
996
+ crlfDelay: Infinity
997
+ });
998
+ const newEvents = [];
999
+ let isCompleted = false;
1000
+ let isError = false;
1001
+ for await (const line of rl) {
1002
+ if (!line.trim()) continue;
1003
+ const parsed = parseJSONLLine(line, sessionId);
1004
+ if (parsed.type === "event" && parsed.event) {
1005
+ newEvents.push(parsed.event);
1006
+ } else if (parsed.type === "completed") {
1007
+ isCompleted = true;
1008
+ isError = parsed.isError;
1009
+ }
1010
+ }
1011
+ entry.byteOffset = newSize;
1012
+ for (const event of newEvents) {
1013
+ this.onEvent({ type: "claude_event", sessionId, event });
1014
+ }
1015
+ if (isCompleted) {
1016
+ this.onEvent({ type: "status_change", sessionId, status: isError ? "error" : "idle" });
1017
+ this.unwatch(sessionId);
1018
+ }
1019
+ } finally {
1020
+ rl?.close();
1021
+ await fileHandle?.close();
1022
+ }
1023
+ }
1024
+ };
1025
+ function parseJSONLLine(line, sessionId) {
1026
+ try {
1027
+ const obj = JSON.parse(line);
1028
+ if (obj.type === "user" && obj.message) {
1029
+ const msgContent = obj.message.content;
1030
+ if (typeof msgContent === "string") {
1031
+ if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) {
1032
+ return { type: "skip" };
1033
+ }
1034
+ }
1035
+ const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
1036
+ if (normalizedContent.length === 0) return { type: "skip" };
1037
+ return {
1038
+ type: "event",
1039
+ event: {
1040
+ type: "user",
1041
+ message: { ...obj.message, content: normalizedContent },
1042
+ session_id: sessionId
1043
+ }
1044
+ };
1045
+ }
1046
+ if (obj.type === "assistant" && obj.message) {
1047
+ const content = (obj.message.content ?? []).filter((b) => b.type === "text" || b.type === "tool_use");
1048
+ if (content.length === 0) return { type: "skip" };
1049
+ return {
1050
+ type: "event",
1051
+ event: {
1052
+ type: "assistant",
1053
+ message: {
1054
+ id: obj.message.id ?? obj.uuid ?? "unknown",
1055
+ model: obj.message.model ?? "unknown",
1056
+ role: "assistant",
1057
+ content,
1058
+ stop_reason: obj.message.stop_reason
1059
+ },
1060
+ session_id: sessionId
1061
+ }
1062
+ };
1063
+ }
1064
+ if (obj.type === "result") {
1065
+ return { type: "completed", isError: !!obj.is_error };
1066
+ }
1067
+ return { type: "skip" };
1068
+ } catch {
1069
+ return { type: "skip" };
1070
+ }
1071
+ }
1072
+
1073
+ // src/ws/WsBridge.ts
1074
+ var import_ws = require("ws");
1075
+ var WsBridge = class _WsBridge {
1076
+ wss;
1077
+ token;
1078
+ heartbeatTimer = null;
1079
+ clientEventCallbacks = [];
1080
+ connectionCallbacks = [];
1081
+ disconnectCallbacks = [];
1082
+ /** 每个连接的最后一次 pong 时间 */
1083
+ lastPongMap = /* @__PURE__ */ new Map();
1084
+ /** 每个连接当前正在查看的会话 ID */
1085
+ viewingSessions = /* @__PURE__ */ new Map();
1086
+ constructor(options) {
1087
+ this.token = options.token;
1088
+ this.wss = new import_ws.WebSocketServer({
1089
+ port: options.port,
1090
+ verifyClient: (info, callback) => {
1091
+ const authorized = this.verifyToken(info.req);
1092
+ if (!authorized) {
1093
+ callback(false, 401, "Unauthorized");
1094
+ } else {
1095
+ callback(true);
1096
+ }
1097
+ }
1098
+ });
1099
+ this.wss.on("connection", (ws) => this.handleConnection(ws));
1100
+ this.startHeartbeat();
1101
+ console.log(`[WsBridge] WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1102
+ }
1103
+ /**
1104
+ * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
1105
+ * 使用此方法代替 new WsBridge(),确保 EADDRINUSE 等错误能被调用方的 try-catch 捕获。
1106
+ */
1107
+ static async create(options) {
1108
+ return new Promise((resolve, reject) => {
1109
+ const bridge = new _WsBridge(options);
1110
+ bridge.wss.once("listening", () => {
1111
+ bridge.wss.on("error", (err) => console.error("[WsBridge] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1112
+ resolve(bridge);
1113
+ });
1114
+ bridge.wss.once("error", reject);
1115
+ });
1116
+ }
1117
+ // ============================================
1118
+ // 公开 API
1119
+ // ============================================
1120
+ /** 注册客户端事件回调 */
1121
+ onClientEvent(callback) {
1122
+ this.clientEventCallbacks.push(callback);
1123
+ }
1124
+ /** 注册新连接回调(用于自动推送初始数据) */
1125
+ onConnection(callback) {
1126
+ this.connectionCallbacks.push(callback);
1127
+ }
1128
+ /** 注册断开连接回调(任意客户端断开时触发,可通过 getConnectionCount() 判断是否全部断开) */
1129
+ onDisconnect(callback) {
1130
+ this.disconnectCallbacks.push(callback);
1131
+ }
1132
+ /** 广播事件到所有已连接的客户端 */
1133
+ broadcast(event) {
1134
+ const data = JSON.stringify(event);
1135
+ for (const ws of this.wss.clients) {
1136
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
1137
+ ws.send(data);
1138
+ }
1139
+ }
1140
+ }
1141
+ /** 发送事件到指定客户端 */
1142
+ send(ws, event) {
1143
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
1144
+ ws.send(JSON.stringify(event));
1145
+ }
1146
+ }
1147
+ /** 设置指定连接当前正在查看的会话 */
1148
+ setViewingSession(ws, sessionId) {
1149
+ this.viewingSessions.set(ws, sessionId);
1150
+ }
1151
+ /** 清除指定连接的查看状态 */
1152
+ clearViewingSession(ws) {
1153
+ this.viewingSessions.delete(ws);
1154
+ }
1155
+ /** 检查是否有任意连接正在查看指定会话 */
1156
+ isViewingSession(sessionId) {
1157
+ for (const sid of this.viewingSessions.values()) {
1158
+ if (sid === sessionId) return true;
1159
+ }
1160
+ return false;
1161
+ }
1162
+ /** 获取当前活跃连接数 */
1163
+ getConnectionCount() {
1164
+ return this.wss.clients.size;
1165
+ }
1166
+ /** 优雅关闭 WebSocket 服务 */
1167
+ close() {
1168
+ return new Promise((resolve, reject) => {
1169
+ if (this.heartbeatTimer) {
1170
+ clearInterval(this.heartbeatTimer);
1171
+ this.heartbeatTimer = null;
1172
+ }
1173
+ for (const ws of this.wss.clients) {
1174
+ ws.terminate();
1175
+ }
1176
+ this.wss.close((err) => {
1177
+ if (err) {
1178
+ reject(err);
1179
+ } else {
1180
+ console.log("[WsBridge] WebSocket \u670D\u52A1\u5DF2\u5173\u95ED");
1181
+ resolve();
1182
+ }
1183
+ });
1184
+ });
1185
+ }
1186
+ // ============================================
1187
+ // 内部方法
1188
+ // ============================================
1189
+ /** 验证连接 token(token 为空字符串时跳过验证) */
1190
+ verifyToken(req) {
1191
+ if (this.token === "") return true;
1192
+ try {
1193
+ const url = new URL(req.url ?? "", `http://${req.headers.host}`);
1194
+ const clientToken = url.searchParams.get("token");
1195
+ return clientToken === this.token;
1196
+ } catch {
1197
+ return false;
1198
+ }
1199
+ }
1200
+ /** 处理新的 WebSocket 连接 */
1201
+ handleConnection(ws) {
1202
+ this.lastPongMap.set(ws, Date.now());
1203
+ console.log(`[WsBridge] \u65B0\u5BA2\u6237\u7AEF\u8FDE\u63A5\uFF0C\u5F53\u524D\u8FDE\u63A5\u6570: ${this.getConnectionCount()}`);
1204
+ for (const callback of this.connectionCallbacks) {
1205
+ try {
1206
+ callback(ws);
1207
+ } catch (err) {
1208
+ console.error("[WsBridge] \u8FDE\u63A5\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1209
+ }
1210
+ }
1211
+ ws.on("pong", () => {
1212
+ this.lastPongMap.set(ws, Date.now());
1213
+ });
1214
+ ws.on("message", (raw) => {
1215
+ try {
1216
+ const event = JSON.parse(raw.toString());
1217
+ this.dispatchClientEvent(event, ws);
1218
+ } catch (err) {
1219
+ console.error("[WsBridge] \u6D88\u606F\u89E3\u6790\u5931\u8D25:", err);
1220
+ this.send(ws, {
1221
+ type: "error",
1222
+ message: "\u6D88\u606F\u683C\u5F0F\u65E0\u6548",
1223
+ code: "INVALID_MESSAGE"
1224
+ });
1225
+ }
1226
+ });
1227
+ ws.on("close", () => {
1228
+ this.lastPongMap.delete(ws);
1229
+ this.viewingSessions.delete(ws);
1230
+ setTimeout(() => {
1231
+ console.log(`[WsBridge] \u5BA2\u6237\u7AEF\u65AD\u5F00\uFF0C\u5F53\u524D\u8FDE\u63A5\u6570: ${this.getConnectionCount()}`);
1232
+ for (const cb of this.disconnectCallbacks) {
1233
+ try {
1234
+ cb();
1235
+ } catch (err) {
1236
+ console.error("[WsBridge] \u65AD\u5F00\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1237
+ }
1238
+ }
1239
+ }, 0);
1240
+ });
1241
+ ws.on("error", (err) => {
1242
+ console.error("[WsBridge] \u8FDE\u63A5\u9519\u8BEF:", err.message);
1243
+ });
1244
+ }
1245
+ /** 分发客户端事件到所有注册的回调 */
1246
+ dispatchClientEvent(event, ws) {
1247
+ for (const callback of this.clientEventCallbacks) {
1248
+ try {
1249
+ callback(event, ws);
1250
+ } catch (err) {
1251
+ console.error("[WsBridge] \u4E8B\u4EF6\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1252
+ }
1253
+ }
1254
+ }
1255
+ /** 启动心跳机制 */
1256
+ startHeartbeat() {
1257
+ this.heartbeatTimer = setInterval(() => {
1258
+ const now = Date.now();
1259
+ this.broadcast({ type: "heartbeat", timestamp: now });
1260
+ for (const ws of this.wss.clients) {
1261
+ const lastPong = this.lastPongMap.get(ws) ?? 0;
1262
+ if (now - lastPong > 45e3) {
1263
+ console.log("[WsBridge] \u68C0\u6D4B\u5230\u6B7B\u8FDE\u63A5\uFF0C\u4E3B\u52A8\u65AD\u5F00");
1264
+ ws.terminate();
1265
+ continue;
1266
+ }
1267
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
1268
+ ws.ping();
1269
+ }
1270
+ }
1271
+ }, 15e3);
1272
+ }
1273
+ };
1274
+
1275
+ // src/approval/ApprovalProxy.ts
1276
+ var import_node_http = __toESM(require("http"));
1277
+ var import_node_fs = __toESM(require("fs"));
1278
+ var import_node_path = __toESM(require("path"));
1279
+ var import_node_os2 = __toESM(require("os"));
1280
+ var import_uuid3 = require("uuid");
1281
+ var ApprovalProxy = class _ApprovalProxy {
1282
+ server;
1283
+ token;
1284
+ port;
1285
+ settingsPath = import_node_path.default.join(import_node_os2.default.homedir(), ".claude", "settings.json");
1286
+ /** 待处理的审批请求:requestId -> { resolve, timer, request } */
1287
+ pendingApprovals = /* @__PURE__ */ new Map();
1288
+ /** 审批请求回调(通知外部推送到手机) */
1289
+ approvalRequestCallbacks = [];
1290
+ /** YOLO 模式状态:sessionId -> enabled */
1291
+ yoloSessions = /* @__PURE__ */ new Map();
1292
+ /** 内存缓存:已被"始终允许"的工具名(避免每次读 settings.json) */
1293
+ alwaysAllowedTools = /* @__PURE__ */ new Set();
1294
+ /** 获取状态信息的回调(由外部注入) */
1295
+ statusInfoProvider = null;
1296
+ constructor(options) {
1297
+ this.token = options.token;
1298
+ this.port = options.port;
1299
+ this.server = import_node_http.default.createServer((req, res) => {
1300
+ this.handleRequest(req, res);
1301
+ });
1302
+ this.server.listen(options.port, () => {
1303
+ console.log(`[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1304
+ });
1305
+ }
1306
+ /**
1307
+ * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
1308
+ */
1309
+ static async create(options) {
1310
+ return new Promise((resolve, reject) => {
1311
+ const proxy = new _ApprovalProxy(options);
1312
+ proxy.server.once("listening", () => {
1313
+ proxy.server.on("error", (err) => console.error("[ApprovalProxy] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1314
+ resolve(proxy);
1315
+ });
1316
+ proxy.server.once("error", reject);
1317
+ });
1318
+ }
1319
+ // ============================================
1320
+ // 公开 API
1321
+ // ============================================
1322
+ /** 注册审批请求回调(当有新的审批请求时触发) */
1323
+ onApprovalRequest(callback) {
1324
+ this.approvalRequestCallbacks.push(callback);
1325
+ }
1326
+ /** 设置状态信息提供者(用于 /health 端点) */
1327
+ setStatusInfoProvider(provider) {
1328
+ this.statusInfoProvider = provider;
1329
+ }
1330
+ /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1331
+ setYoloMode(sessionId, enabled) {
1332
+ this.yoloSessions.set(sessionId, enabled);
1333
+ console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F ${enabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u5173\u95ED"}: ${sessionId}`);
1334
+ }
1335
+ /** 检查会话是否处于 YOLO 模式 */
1336
+ isYoloMode(sessionId) {
1337
+ return this.yoloSessions.get(sessionId) ?? false;
1338
+ }
1339
+ /**
1340
+ * 注入审批结果
1341
+ *
1342
+ * 从 pendingApprovals 中取出对应请求,resolve promise,
1343
+ * 让长轮询的 HTTP 响应返回审批结果给 Claude Code hook。
1344
+ */
1345
+ resolveApproval(requestId, decision) {
1346
+ const pending = this.pendingApprovals.get(requestId);
1347
+ if (!pending) {
1348
+ console.warn(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6`);
1349
+ return false;
1350
+ }
1351
+ clearTimeout(pending.timer);
1352
+ pending.resolve(decision);
1353
+ this.pendingApprovals.delete(requestId);
1354
+ console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u5904\u7406: ${decision.decision}`);
1355
+ return true;
1356
+ }
1357
+ /** 获取当前待处理的审批数量 */
1358
+ getPendingCount() {
1359
+ return this.pendingApprovals.size;
1360
+ }
1361
+ /** 检查指定审批请求是否仍在等待用户决策 */
1362
+ isPending(requestId) {
1363
+ return this.pendingApprovals.has(requestId);
1364
+ }
1365
+ /** 检查工具是否已被"始终允许"(内存缓存 + settings.json 双重检查) */
1366
+ isToolAlwaysAllowed(toolName, projectPath) {
1367
+ if (this.alwaysAllowedTools.has(toolName)) return true;
1368
+ return this.isToolInClaudeSettings(toolName, projectPath);
1369
+ }
1370
+ /** 检查工具是否已在 settings.json permissions.allow 中(检查项目级和全局) */
1371
+ isToolInClaudeSettings(toolName, projectPath) {
1372
+ const checkPath = (filepath) => {
1373
+ try {
1374
+ const raw = import_node_fs.default.readFileSync(filepath, "utf-8");
1375
+ const settings = JSON.parse(raw);
1376
+ const allow = settings?.permissions?.allow ?? [];
1377
+ return allow.some((entry) => {
1378
+ if (entry === toolName) return true;
1379
+ if (entry === `${toolName}(*)`) return true;
1380
+ if (toolName.startsWith(`${entry}__`)) return true;
1381
+ return false;
1382
+ });
1383
+ } catch {
1384
+ return false;
1385
+ }
1386
+ };
1387
+ if (projectPath) {
1388
+ const projectSettingsPath = import_node_path.default.join(projectPath, ".claude", "settings.json");
1389
+ if (checkPath(projectSettingsPath)) return true;
1390
+ }
1391
+ return checkPath(this.settingsPath);
1392
+ }
1393
+ /** 将工具写入 settings.json permissions.allow(项目级或全局) */
1394
+ addToClaudeSettings(projectPath, toolName) {
1395
+ const targetPath = projectPath ? import_node_path.default.join(projectPath, ".claude", "settings.json") : this.settingsPath;
1396
+ try {
1397
+ if (projectPath) {
1398
+ const dir = import_node_path.default.dirname(targetPath);
1399
+ if (!import_node_fs.default.existsSync(dir)) {
1400
+ import_node_fs.default.mkdirSync(dir, { recursive: true });
1401
+ }
1402
+ }
1403
+ let settings = {};
1404
+ try {
1405
+ settings = JSON.parse(import_node_fs.default.readFileSync(targetPath, "utf-8"));
1406
+ } catch {
1407
+ }
1408
+ if (!settings.permissions) {
1409
+ settings.permissions = {};
1410
+ }
1411
+ const perms = settings.permissions;
1412
+ if (!Array.isArray(perms.allow)) {
1413
+ perms.allow = [];
1414
+ }
1415
+ const allow = perms.allow;
1416
+ const entry = `${toolName}(*)`;
1417
+ if (!allow.includes(entry)) {
1418
+ allow.push(entry);
1419
+ import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1420
+ const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1421
+ console.log(`[ApprovalProxy] \u5DF2\u5C06 ${entry} \u5199\u5165 ${label}`);
1422
+ }
1423
+ this.alwaysAllowedTools.add(toolName);
1424
+ } catch (err) {
1425
+ console.error("[ApprovalProxy] \u5199\u5165 settings.json \u5931\u8D25:", err);
1426
+ }
1427
+ }
1428
+ /** 获取指定会话的所有 pending approval requests(用于 subscribe 重发) */
1429
+ getPendingRequestsForSession(sessionId) {
1430
+ const result = [];
1431
+ for (const { request } of this.pendingApprovals.values()) {
1432
+ if (request.sessionId === sessionId) {
1433
+ result.push(request);
1434
+ }
1435
+ }
1436
+ return result;
1437
+ }
1438
+ /**
1439
+ * 批量允许所有待处理的审批请求(手机端断线时调用)
1440
+ */
1441
+ approveAll(reason) {
1442
+ const entries = Array.from(this.pendingApprovals.entries());
1443
+ for (const [requestId, pending] of entries) {
1444
+ clearTimeout(pending.timer);
1445
+ pending.resolve({ decision: "allow" });
1446
+ this.pendingApprovals.delete(requestId);
1447
+ console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u81EA\u52A8\u5141\u8BB8${reason ? `\uFF08${reason}\uFF09` : ""}`);
1448
+ }
1449
+ }
1450
+ /** 优雅关闭 HTTP 服务 */
1451
+ close() {
1452
+ return new Promise((resolve, reject) => {
1453
+ const pendingEntries = Array.from(this.pendingApprovals.entries());
1454
+ for (const [, pending] of pendingEntries) {
1455
+ clearTimeout(pending.timer);
1456
+ pending.resolve({ decision: "deny", reason: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED" });
1457
+ }
1458
+ this.pendingApprovals.clear();
1459
+ this.server.close((err) => {
1460
+ if (err) {
1461
+ reject(err);
1462
+ } else {
1463
+ console.log("[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED");
1464
+ resolve();
1465
+ }
1466
+ });
1467
+ });
1468
+ }
1469
+ // ============================================
1470
+ // 内部方法
1471
+ // ============================================
1472
+ /** 路由请求 */
1473
+ handleRequest(req, res) {
1474
+ res.setHeader("Access-Control-Allow-Origin", "*");
1475
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
1476
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
1477
+ if (req.method === "OPTIONS") {
1478
+ res.writeHead(204);
1479
+ res.end();
1480
+ return;
1481
+ }
1482
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1483
+ const pathname = url.pathname;
1484
+ if (req.method === "POST" && pathname === "/hook/approval") {
1485
+ this.handleApprovalHook(req, res);
1486
+ } else if (req.method === "GET" && pathname === "/health") {
1487
+ this.handleHealth(req, res);
1488
+ } else if (req.method === "GET" && pathname === "/token") {
1489
+ this.handleToken(req, res);
1490
+ } else {
1491
+ this.sendJson(res, 404, { error: "Not Found" });
1492
+ }
1493
+ }
1494
+ /**
1495
+ * 核心端点:处理 Claude Code hook 的审批请求
1496
+ *
1497
+ * 长轮询实现:
1498
+ * 1. 解析请求 body
1499
+ * 2. 创建 ApprovalRequest 对象
1500
+ * 3. 通知外部(推到手机)
1501
+ * 4. 创建 Promise 并 hold 住 response
1502
+ * 5. 等待 resolveApproval() 被调用或超时
1503
+ */
1504
+ async handleApprovalHook(req, res) {
1505
+ try {
1506
+ const body = await this.parseJsonBody(req);
1507
+ const payload = body.payload ?? body;
1508
+ const requestId = (0, import_uuid3.v4)();
1509
+ const projectPath = String(body.projectPath ?? "unknown");
1510
+ const toolName = String(payload.tool_name ?? body.tool_name ?? "unknown");
1511
+ const toolInput = payload.tool_input ?? body.tool_input ?? {};
1512
+ const approvalRequest = {
1513
+ id: requestId,
1514
+ sessionId: String(body.sessionId ?? "unknown"),
1515
+ projectPath,
1516
+ toolName,
1517
+ toolInput,
1518
+ description: String(payload.description ?? body.description ?? `${toolName} \u5DE5\u5177\u8C03\u7528\u8BF7\u6C42`),
1519
+ createdAt: Date.now()
1520
+ };
1521
+ console.log(`[ApprovalProxy] \u6536\u5230\u5BA1\u6279\u8BF7\u6C42: ${requestId} (${approvalRequest.toolName})`);
1522
+ if (this.isToolAlwaysAllowed(approvalRequest.toolName, projectPath !== "unknown" ? projectPath : void 0)) {
1523
+ console.log(`[ApprovalProxy] ${approvalRequest.toolName} \u5DF2\u88AB\u59CB\u7EC8\u5141\u8BB8\uFF0C\u76F4\u63A5\u653E\u884C\uFF08\u4E0D\u901A\u77E5\uFF09`);
1524
+ this.sendJson(res, 200, { decision: "allow" });
1525
+ return;
1526
+ }
1527
+ if (this.yoloSessions.get(approvalRequest.sessionId)) {
1528
+ console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C: ${approvalRequest.toolName}`);
1529
+ this.sendJson(res, 200, { decision: "allow" });
1530
+ return;
1531
+ }
1532
+ this.notifyApprovalRequest(approvalRequest);
1533
+ const decision = await new Promise((resolve) => {
1534
+ const timer = setTimeout(() => {
1535
+ console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8`);
1536
+ this.pendingApprovals.delete(requestId);
1537
+ resolve({ decision: "allow" });
1538
+ }, 325e3);
1539
+ this.pendingApprovals.set(requestId, { resolve, timer, request: approvalRequest });
1540
+ });
1541
+ this.sendJson(res, 200, decision);
1542
+ } catch (err) {
1543
+ console.error("[ApprovalProxy] \u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25:", err);
1544
+ this.sendJson(res, 200, { decision: "deny", reason: "\u670D\u52A1\u5668\u5904\u7406\u8BF7\u6C42\u5931\u8D25" });
1545
+ }
1546
+ }
1547
+ /** 健康检查端点 */
1548
+ handleHealth(_req, res) {
1549
+ const info = this.statusInfoProvider?.() ?? { connections: 0, activeSessions: 0 };
1550
+ this.sendJson(res, 200, {
1551
+ status: "ok",
1552
+ connections: info.connections,
1553
+ activeSessions: info.activeSessions
1554
+ });
1555
+ }
1556
+ /** 返回连接 token(仅本机访问) */
1557
+ handleToken(req, res) {
1558
+ const remoteAddress = req.socket.remoteAddress;
1559
+ const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
1560
+ if (!isLocal) {
1561
+ this.sendJson(res, 403, { error: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE" });
1562
+ return;
1563
+ }
1564
+ this.sendJson(res, 200, { token: this.token });
1565
+ }
1566
+ /** 通知所有注册的审批请求回调 */
1567
+ notifyApprovalRequest(request) {
1568
+ for (const callback of this.approvalRequestCallbacks) {
1569
+ try {
1570
+ callback(request);
1571
+ } catch (err) {
1572
+ console.error("[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1573
+ }
1574
+ }
1575
+ }
1576
+ /** 手动解析请求的 JSON body(限制最大 1MB 防止滥用) */
1577
+ parseJsonBody(req) {
1578
+ const MAX_BODY_SIZE = 1024 * 1024;
1579
+ return new Promise((resolve, reject) => {
1580
+ const chunks = [];
1581
+ let totalSize = 0;
1582
+ let destroyed = false;
1583
+ req.on("data", (chunk) => {
1584
+ if (destroyed) return;
1585
+ totalSize += chunk.length;
1586
+ if (totalSize > MAX_BODY_SIZE) {
1587
+ destroyed = true;
1588
+ req.destroy();
1589
+ return reject(new Error("\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09"));
1590
+ }
1591
+ chunks.push(chunk);
1592
+ });
1593
+ req.on("end", () => {
1594
+ try {
1595
+ const raw = Buffer.concat(chunks).toString("utf-8");
1596
+ const parsed = JSON.parse(raw);
1597
+ resolve(parsed);
1598
+ } catch {
1599
+ reject(new Error("\u65E0\u6548\u7684 JSON body"));
1600
+ }
1601
+ });
1602
+ req.on("error", (err) => {
1603
+ reject(err);
1604
+ });
1605
+ });
1606
+ }
1607
+ /** 发送 JSON 响应的辅助方法 */
1608
+ sendJson(res, statusCode, data) {
1609
+ const body = JSON.stringify(data);
1610
+ res.writeHead(statusCode, {
1611
+ "Content-Type": "application/json; charset=utf-8",
1612
+ "Content-Length": Buffer.byteLength(body)
1613
+ });
1614
+ res.end(body);
1615
+ }
1616
+ };
1617
+
1618
+ // src/mdns/MdnsService.ts
1619
+ var import_bonjour_service = __toESM(require("bonjour-service"));
1620
+ var MdnsService = class {
1621
+ bonjour = null;
1622
+ service = null;
1623
+ wsPort;
1624
+ httpPort;
1625
+ version;
1626
+ constructor(options) {
1627
+ this.wsPort = options.wsPort;
1628
+ this.httpPort = options.httpPort;
1629
+ this.version = options.version ?? "0.1.0";
1630
+ }
1631
+ /**
1632
+ * 启动 mDNS 广播
1633
+ */
1634
+ start() {
1635
+ if (this.bonjour) {
1636
+ console.warn("[MdnsService] \u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D");
1637
+ return;
1638
+ }
1639
+ this.bonjour = new import_bonjour_service.default();
1640
+ this.service = this.bonjour.publish({
1641
+ name: "Sessix",
1642
+ type: "sessix",
1643
+ port: this.wsPort,
1644
+ txt: {
1645
+ version: this.version,
1646
+ httpPort: String(this.httpPort)
1647
+ }
1648
+ });
1649
+ console.log(`[MdnsService] mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 ${this.wsPort}`);
1650
+ }
1651
+ /**
1652
+ * 停止 mDNS 广播
1653
+ */
1654
+ stop() {
1655
+ if (this.service) {
1656
+ this.service.stop?.(() => {
1657
+ console.log("[MdnsService] \u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62");
1658
+ });
1659
+ this.service = null;
1660
+ }
1661
+ if (this.bonjour) {
1662
+ this.bonjour.destroy();
1663
+ this.bonjour = null;
1664
+ }
1665
+ console.log("[MdnsService] mDNS \u670D\u52A1\u5DF2\u5173\u95ED");
1666
+ }
1667
+ };
1668
+
1669
+ // src/hooks/HookInstaller.ts
1670
+ var import_promises2 = require("fs/promises");
1671
+ var import_node_path2 = require("path");
1672
+ var import_node_os3 = require("os");
1673
+ var SESSIX_HOOKS_DIR = (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".sessix", "hooks");
1674
+ var HOOK_SCRIPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "approval-hook.sh");
1675
+ var PERMISSION_ACCEPT_PATH = (0, import_node_path2.join)(SESSIX_HOOKS_DIR, "permission-accept.sh");
1676
+ var CLAUDE_SETTINGS_PATH = (0, import_node_path2.join)((0, import_node_os3.homedir)(), ".claude", "settings.json");
1677
+ var HOOK_COMMAND = "~/.sessix/hooks/approval-hook.sh";
1678
+ var PERMISSION_ACCEPT_COMMAND = "~/.sessix/hooks/permission-accept.sh";
1679
+ var HOOK_SCRIPT_TEMPLATE = `#!/bin/bash
1680
+ # Sessix Approval Hook
1681
+ # \u4EC5\u5728 Sessix \u7BA1\u7406\u7684\u4F1A\u8BDD\u4E2D\u6FC0\u6D3B
1682
+
1683
+ if [ -z "$SESSIX_SESSION_ID" ]; then
1684
+ exit 0
1685
+ fi
1686
+
1687
+ # \u4ECE stdin \u8BFB\u53D6 hook payload
1688
+ PAYLOAD=$(cat)
1689
+
1690
+ # \u83B7\u53D6\u9879\u76EE\u8DEF\u5F84\uFF08\u5F53\u524D\u5DE5\u4F5C\u76EE\u5F55\uFF09
1691
+ PROJECT_PATH="$PWD"
1692
+
1693
+ # \u53D1\u9001\u5BA1\u6279\u8BF7\u6C42\u5230 Sessix \u670D\u52A1\u5668\uFF08\u957F\u8F6E\u8BE2\uFF0C\u8D85\u65F6\u65F6\u95F4 > 300s\uFF09
1694
+ RESPONSE=$(curl -s -X POST "http://localhost:3746/hook/approval" \\
1695
+ -H "Content-Type: application/json" \\
1696
+ -d "{\\"sessionId\\": \\"$SESSIX_SESSION_ID\\", \\"projectPath\\": \\"$PROJECT_PATH\\", \\"payload\\": $PAYLOAD}" \\
1697
+ --max-time 330 \\
1698
+ 2>/dev/null)
1699
+
1700
+ if [ $? -ne 0 ] || [ -z "$RESPONSE" ]; then
1701
+ # \u5982\u679C Sessix \u670D\u52A1\u5668\u4E0D\u53EF\u7528\uFF0C\u9ED8\u8BA4\u653E\u884C\uFF08exit 0 = \u6279\u51C6\uFF09
1702
+ exit 0
1703
+ fi
1704
+
1705
+ # \u89E3\u6790\u670D\u52A1\u5668\u54CD\u5E94
1706
+ DECISION=$(echo "$RESPONSE" | grep -o '\\"decision\\":\\"[^"]*\\"' | cut -d'"' -f4)
1707
+
1708
+ if [ "$DECISION" = "allow" ]; then
1709
+ # \u7528\u6237\u6279\u51C6\u6216\u670D\u52A1\u5668\u8D85\u65F6\u81EA\u52A8\u6279\u51C6
1710
+ exit 0
1711
+ elif [ "$DECISION" = "deny" ]; then
1712
+ # \u7528\u6237\u660E\u786E\u62D2\u7EDD
1713
+ exit 1
1714
+ else
1715
+ # \u672A\u77E5\u54CD\u5E94\uFF0C\u9ED8\u8BA4\u653E\u884C
1716
+ exit 0
1717
+ fi
1718
+ `;
1719
+ var PERMISSION_ACCEPT_TEMPLATE = `#!/bin/bash
1720
+ # Sessix PermissionRequest \u515C\u5E95
1721
+ # \u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42\uFF0C\u907F\u514D Sessix \u4F1A\u8BDD\u963B\u585E
1722
+
1723
+ if [ -z "$SESSIX_SESSION_ID" ]; then
1724
+ exit 0
1725
+ fi
1726
+
1727
+ # \u8F93\u51FA JSON \u51B3\u7B56\uFF0C\u81EA\u52A8\u63A5\u53D7\u6743\u9650\u8BF7\u6C42
1728
+ echo '{"decision":"allow"}'
1729
+ exit 0
1730
+ `;
1731
+ var HookInstaller = class {
1732
+ /**
1733
+ * 安装 hook
1734
+ *
1735
+ * 1. 创建 ~/.sessix/hooks/ 目录
1736
+ * 2. 写入 approval-hook.sh 脚本
1737
+ * 3. 赋予执行权限
1738
+ * 4. 更新 Claude Code settings.json 添加 hook 配置
1739
+ */
1740
+ async install() {
1741
+ await (0, import_promises2.mkdir)(SESSIX_HOOKS_DIR, { recursive: true });
1742
+ await (0, import_promises2.writeFile)(HOOK_SCRIPT_PATH, HOOK_SCRIPT_TEMPLATE, "utf-8");
1743
+ await (0, import_promises2.writeFile)(PERMISSION_ACCEPT_PATH, PERMISSION_ACCEPT_TEMPLATE, "utf-8");
1744
+ await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
1745
+ await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
1746
+ await this.addHookToSettings();
1747
+ console.log("[HookInstaller] Hook \u5B89\u88C5\u5B8C\u6210");
1748
+ }
1749
+ /**
1750
+ * 卸载 hook
1751
+ *
1752
+ * 从 Claude Code settings.json 中移除 Sessix hook 配置。
1753
+ * 注意:不删除 hook 脚本文件(保持幂等性,避免误删)。
1754
+ */
1755
+ async uninstall() {
1756
+ await this.removeHookFromSettings();
1757
+ console.log("[HookInstaller] Hook \u5DF2\u5378\u8F7D");
1758
+ }
1759
+ /**
1760
+ * 检查 hook 是否已安装
1761
+ * 脚本文件和 settings.json 配置必须同时存在才算已安装
1762
+ */
1763
+ async isInstalled() {
1764
+ let approvalScriptExists = false;
1765
+ let permissionScriptExists = false;
1766
+ try {
1767
+ await (0, import_promises2.access)(HOOK_SCRIPT_PATH);
1768
+ approvalScriptExists = true;
1769
+ } catch {
1770
+ }
1771
+ try {
1772
+ await (0, import_promises2.access)(PERMISSION_ACCEPT_PATH);
1773
+ permissionScriptExists = true;
1774
+ } catch {
1775
+ }
1776
+ const settings = await this.readClaudeSettings();
1777
+ const configExists = this.hasHookConfig(settings);
1778
+ return approvalScriptExists && permissionScriptExists && configExists;
1779
+ }
1780
+ // ============================================
1781
+ // 内部方法
1782
+ // ============================================
1783
+ /**
1784
+ * 向 Claude Code settings.json 添加 Sessix hook 配置
1785
+ */
1786
+ async addHookToSettings() {
1787
+ let settings = await this.readClaudeSettings();
1788
+ let changed = false;
1789
+ if (!settings.hooks) {
1790
+ settings.hooks = {};
1791
+ }
1792
+ if (!this.hasPreToolUseConfig(settings)) {
1793
+ if (!settings.hooks.PreToolUse) {
1794
+ settings.hooks.PreToolUse = [];
1795
+ }
1796
+ settings.hooks.PreToolUse.push({
1797
+ matcher: "",
1798
+ hooks: [{ type: "command", command: HOOK_COMMAND }]
1799
+ });
1800
+ changed = true;
1801
+ }
1802
+ if (!this.hasPermissionRequestConfig(settings)) {
1803
+ if (!settings.hooks.PermissionRequest) {
1804
+ settings.hooks.PermissionRequest = [];
1805
+ }
1806
+ settings.hooks.PermissionRequest.push({
1807
+ matcher: "",
1808
+ hooks: [{ type: "command", command: PERMISSION_ACCEPT_COMMAND }]
1809
+ });
1810
+ changed = true;
1811
+ }
1812
+ if (changed) {
1813
+ await this.writeClaudeSettings(settings);
1814
+ } else {
1815
+ console.log("[HookInstaller] Hook \u914D\u7F6E\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7");
1816
+ }
1817
+ }
1818
+ /**
1819
+ * 从 Claude Code settings.json 移除 Sessix hook 配置
1820
+ */
1821
+ async removeHookFromSettings() {
1822
+ let settings = await this.readClaudeSettings();
1823
+ if (!settings.hooks) return;
1824
+ this.removeHookCommand(settings, "PreToolUse", HOOK_COMMAND);
1825
+ this.removeHookCommand(settings, "PermissionRequest", PERMISSION_ACCEPT_COMMAND);
1826
+ if (Object.keys(settings.hooks).length === 0) {
1827
+ delete settings.hooks;
1828
+ }
1829
+ await this.writeClaudeSettings(settings);
1830
+ }
1831
+ /** 从指定 hook 事件数组中移除包含指定命令的条目 */
1832
+ removeHookCommand(settings, event, command) {
1833
+ if (!Array.isArray(settings.hooks?.[event])) return;
1834
+ settings.hooks[event] = settings.hooks[event].filter(
1835
+ (entry) => !entry?.hooks?.some?.((h) => h.type === "command" && h.command === command)
1836
+ );
1837
+ if (settings.hooks[event].length === 0) {
1838
+ delete settings.hooks[event];
1839
+ }
1840
+ }
1841
+ /**
1842
+ * 读取 Claude Code settings.json
1843
+ */
1844
+ async readClaudeSettings() {
1845
+ try {
1846
+ const content = await (0, import_promises2.readFile)(CLAUDE_SETTINGS_PATH, "utf-8");
1847
+ return JSON.parse(content);
1848
+ } catch {
1849
+ return {};
1850
+ }
1851
+ }
1852
+ /**
1853
+ * 写入 Claude Code settings.json
1854
+ */
1855
+ async writeClaudeSettings(settings) {
1856
+ await (0, import_promises2.mkdir)((0, import_node_path2.join)((0, import_node_os3.homedir)(), ".claude"), { recursive: true });
1857
+ await (0, import_promises2.writeFile)(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2) + "\n", "utf-8");
1858
+ }
1859
+ /**
1860
+ * 检查 settings 中是否已包含所有 Sessix hook 配置
1861
+ */
1862
+ hasHookConfig(settings) {
1863
+ return this.hasPreToolUseConfig(settings) && this.hasPermissionRequestConfig(settings);
1864
+ }
1865
+ /** 检查 PreToolUse 中是否有 approval-hook.sh */
1866
+ hasPreToolUseConfig(settings) {
1867
+ return this.hasHookEntry(settings?.hooks?.PreToolUse, HOOK_COMMAND);
1868
+ }
1869
+ /** 检查 PermissionRequest 中是否有 permission-accept.sh */
1870
+ hasPermissionRequestConfig(settings) {
1871
+ return this.hasHookEntry(settings?.hooks?.PermissionRequest, PERMISSION_ACCEPT_COMMAND);
1872
+ }
1873
+ /** 检查 hook 数组中是否包含指定命令 */
1874
+ hasHookEntry(hookArray, command) {
1875
+ if (!Array.isArray(hookArray)) return false;
1876
+ return hookArray.some(
1877
+ (entry) => entry?.hooks?.some?.((hook) => hook.type === "command" && hook.command === command)
1878
+ );
1879
+ }
1880
+ };
1881
+
1882
+ // src/notification/NotificationService.ts
1883
+ var import_node_path3 = require("path");
1884
+ var NotificationService = class {
1885
+ constructor(sessionManager, expoChannel = null) {
1886
+ this.sessionManager = sessionManager;
1887
+ this.expoChannel = expoChannel;
1888
+ this.unsubscribe = sessionManager.onEvent((event) => this.handleEvent(event));
1889
+ if (expoChannel) {
1890
+ this.channelMap.set("expo", { channel: expoChannel, enabled: true });
1891
+ }
1892
+ }
1893
+ channelMap = /* @__PURE__ */ new Map();
1894
+ unsubscribe = null;
1895
+ activityPushChannel = null;
1896
+ /** YOLO 模式状态映射:sessionId -> isYoloMode */
1897
+ yoloModeState = /* @__PURE__ */ new Map();
1898
+ /** 每个会话的最新 assistant 文本消息(用于通知正文预览) */
1899
+ latestAssistantText = /* @__PURE__ */ new Map();
1900
+ /** 添加通知渠道(id 唯一,可用于后续动态开关) */
1901
+ addChannel(id, channel, enabled = true) {
1902
+ this.channelMap.set(id, { channel, enabled });
1903
+ }
1904
+ /** 运行时切换指定渠道的启用状态 */
1905
+ setChannelEnabled(id, enabled) {
1906
+ const entry = this.channelMap.get(id);
1907
+ if (entry) entry.enabled = enabled;
1908
+ }
1909
+ /** 注册手机 push token(连接建立时由 WsBridge 调用) */
1910
+ addPushToken(token) {
1911
+ this.expoChannel?.addToken(token);
1912
+ }
1913
+ /** 移除手机 push token(断线时或手机主动注销时调用) */
1914
+ removePushToken(token) {
1915
+ this.expoChannel?.removeToken(token);
1916
+ }
1917
+ /** 更新通知音效偏好 */
1918
+ setSoundPreferences(prefs) {
1919
+ this.expoChannel?.setSoundPreferences(prefs);
1920
+ }
1921
+ /** 设置 ActivityKit Push 渠道(可选,需要 APNs 认证配置) */
1922
+ setActivityPushChannel(channel) {
1923
+ this.activityPushChannel = channel;
1924
+ }
1925
+ /** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
1926
+ addActivityPushToken(sessionId, token) {
1927
+ this.activityPushChannel?.addToken(sessionId, token);
1928
+ }
1929
+ /** 移除 ActivityKit push token */
1930
+ removeActivityPushToken(sessionId) {
1931
+ this.activityPushChannel?.removeToken(sessionId);
1932
+ }
1933
+ /** 更新会话的 YOLO 模式状态 */
1934
+ setYoloMode(sessionId, enabled) {
1935
+ this.yoloModeState.set(sessionId, enabled);
1936
+ }
1937
+ /** 直接触发审批通知(由 ApprovalProxy 回调调用) */
1938
+ notifyApproval(request, pendingCount) {
1939
+ if (this.yoloModeState.get(request.sessionId)) return;
1940
+ const sessionTitle = this.getSessionTitle(request.sessionId);
1941
+ const title = pendingCount > 1 ? `${sessionTitle} \u2014 ${pendingCount} \u9879\u5F85\u5BA1\u6279` : sessionTitle;
1942
+ const body = pendingCount > 1 ? `\u{1F527} \u6700\u65B0: ${request.toolName}: ${request.description}` : `\u{1F527} ${request.toolName}: ${request.description}`;
1943
+ if (this.activityPushChannel?.hasToken(request.sessionId)) {
1944
+ const dangerLevel = this.getDangerLevel(request.toolName);
1945
+ const isYoloMode = this.getYoloMode(request.sessionId);
1946
+ this.activityPushChannel.updateActivityWithAlert(
1947
+ request.sessionId,
1948
+ {
1949
+ status: "waitingApproval",
1950
+ sessionTitle,
1951
+ latestMessage: `${request.toolName}: ${request.description}`,
1952
+ approvalInfo: {
1953
+ requestId: request.id,
1954
+ toolName: request.toolName,
1955
+ description: request.description.slice(0, 80),
1956
+ dangerLevel,
1957
+ pendingCount
1958
+ },
1959
+ isYoloMode,
1960
+ updatedAt: Date.now()
1961
+ },
1962
+ { title, body }
1963
+ );
1964
+ return;
1965
+ }
1966
+ this.notify({
1967
+ title,
1968
+ body,
1969
+ sound: "Funk",
1970
+ badge: pendingCount,
1971
+ data: {
1972
+ type: "approval_request",
1973
+ sessionId: request.sessionId,
1974
+ requestId: request.id
1975
+ }
1976
+ });
1977
+ }
1978
+ /** 简单的工具危险等级判断 */
1979
+ getDangerLevel(toolName) {
1980
+ if (toolName === "Bash") return "danger";
1981
+ if (["Write", "Edit", "NotebookEdit"].includes(toolName)) return "write";
1982
+ return "safe";
1983
+ }
1984
+ /** 清理资源 */
1985
+ destroy() {
1986
+ this.unsubscribe?.();
1987
+ this.unsubscribe = null;
1988
+ this.yoloModeState.clear();
1989
+ this.latestAssistantText.clear();
1990
+ }
1991
+ // ============================================
1992
+ // 内部方法
1993
+ // ============================================
1994
+ handleEvent(event) {
1995
+ switch (event.type) {
1996
+ case "claude_event": {
1997
+ this.trackAssistantText(event.sessionId, event.event);
1998
+ break;
1999
+ }
2000
+ case "claude_events": {
2001
+ for (const e of event.events) {
2002
+ this.trackAssistantText(event.sessionId, e);
2003
+ }
2004
+ break;
2005
+ }
2006
+ case "status_change": {
2007
+ if (event.status === "idle") {
2008
+ const sessionTitle = this.getSessionTitle(event.sessionId);
2009
+ const latestMsg = this.latestAssistantText.get(event.sessionId);
2010
+ const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : "\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B65\u6307\u4EE4";
2011
+ const isYoloMode = this.getYoloMode(event.sessionId);
2012
+ if (this.activityPushChannel?.hasToken(event.sessionId)) {
2013
+ this.activityPushChannel.endActivity(event.sessionId, {
2014
+ status: "idle",
2015
+ sessionTitle,
2016
+ latestMessage: body,
2017
+ isYoloMode,
2018
+ updatedAt: Date.now()
2019
+ });
2020
+ } else {
2021
+ this.notify({
2022
+ title: sessionTitle,
2023
+ body,
2024
+ sound: "Glass",
2025
+ data: { type: "task_complete", sessionId: event.sessionId }
2026
+ });
2027
+ }
2028
+ } else if (event.status === "error") {
2029
+ const sessionTitle = this.getSessionTitle(event.sessionId);
2030
+ const latestMsg = this.latestAssistantText.get(event.sessionId);
2031
+ const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : "\u6267\u884C\u51FA\u9519\uFF0C\u8BF7\u67E5\u770B\u8BE6\u60C5";
2032
+ const isYoloMode = this.getYoloMode(event.sessionId);
2033
+ if (this.activityPushChannel?.hasToken(event.sessionId)) {
2034
+ this.activityPushChannel.endActivity(event.sessionId, {
2035
+ status: "error",
2036
+ sessionTitle,
2037
+ latestMessage: body,
2038
+ isYoloMode,
2039
+ updatedAt: Date.now()
2040
+ });
2041
+ } else {
2042
+ this.notify({
2043
+ title: sessionTitle,
2044
+ body,
2045
+ sound: "Basso",
2046
+ data: { type: "task_error", sessionId: event.sessionId }
2047
+ });
2048
+ }
2049
+ }
2050
+ break;
2051
+ }
2052
+ }
2053
+ }
2054
+ notify(payload) {
2055
+ for (const { channel, enabled } of this.channelMap.values()) {
2056
+ if (!enabled) continue;
2057
+ channel.send(payload).catch((err) => {
2058
+ console.error("[NotificationService] \u901A\u77E5\u53D1\u9001\u5931\u8D25:", err);
2059
+ });
2060
+ }
2061
+ }
2062
+ /** 从 assistant 事件中提取最新文本消息 */
2063
+ trackAssistantText(sessionId, event) {
2064
+ if (event.type !== "assistant") return;
2065
+ const textBlocks = event.message.content.filter((b) => b.type === "text");
2066
+ const lastText = textBlocks[textBlocks.length - 1];
2067
+ if (lastText && lastText.type === "text" && lastText.text.trim()) {
2068
+ this.latestAssistantText.set(sessionId, lastText.text.trim());
2069
+ }
2070
+ }
2071
+ /** 获取会话标题:优先 summary,fallback 到项目名 */
2072
+ getSessionTitle(sessionId) {
2073
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
2074
+ if (!session) return "Unknown";
2075
+ return session.summary ?? (0, import_node_path3.basename)(session.projectPath);
2076
+ }
2077
+ /** 获取会话的 YOLO 模式状态 */
2078
+ getYoloMode(sessionId) {
2079
+ return this.yoloModeState.get(sessionId) ?? false;
2080
+ }
2081
+ };
2082
+
2083
+ // src/notification/MacNotificationChannel.ts
2084
+ var import_node_child_process = require("child_process");
2085
+ var MacNotificationChannel = class {
2086
+ isAvailable() {
2087
+ return process.platform === "darwin";
2088
+ }
2089
+ send(payload) {
2090
+ if (!this.isAvailable()) return Promise.resolve();
2091
+ const title = payload.title.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2092
+ const body = payload.body.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
2093
+ const sound = payload.sound ?? "Ping";
2094
+ const script = `display notification "${body}" with title "${title}" sound name "${sound}"`;
2095
+ return new Promise((resolve) => {
2096
+ (0, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
2097
+ if (err) {
2098
+ console.warn("[MacNotificationChannel] \u53D1\u9001\u901A\u77E5\u5931\u8D25:", err.message);
2099
+ }
2100
+ resolve();
2101
+ });
2102
+ });
2103
+ }
2104
+ };
2105
+
2106
+ // src/notification/ExpoNotificationChannel.ts
2107
+ var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
2108
+ var ExpoNotificationChannel = class {
2109
+ tokens = /* @__PURE__ */ new Set();
2110
+ /** per-token 通知音效偏好 */
2111
+ soundPreferences = /* @__PURE__ */ new Map();
2112
+ isAvailable() {
2113
+ return this.tokens.size > 0;
2114
+ }
2115
+ addToken(token) {
2116
+ this.tokens.add(token);
2117
+ console.log(`[ExpoNotificationChannel] \u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2118
+ }
2119
+ removeToken(token) {
2120
+ this.tokens.delete(token);
2121
+ this.soundPreferences.delete(token);
2122
+ console.log(`[ExpoNotificationChannel] \u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2123
+ }
2124
+ /** 更新某个 token 的音效偏好 */
2125
+ setSoundPreferences(prefs) {
2126
+ for (const token of this.tokens) {
2127
+ this.soundPreferences.set(token, prefs);
2128
+ }
2129
+ console.log("[ExpoNotificationChannel] \u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D");
2130
+ }
2131
+ async send(payload) {
2132
+ if (this.tokens.size === 0) return;
2133
+ const messages = Array.from(this.tokens).map((to) => {
2134
+ let sound = payload.sound ?? "default";
2135
+ const prefs = this.soundPreferences.get(to);
2136
+ if (prefs) {
2137
+ const notifType = payload.data?.type ?? "";
2138
+ if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
2139
+ else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
2140
+ else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
2141
+ }
2142
+ return {
2143
+ to,
2144
+ title: payload.title,
2145
+ body: payload.body,
2146
+ badge: payload.badge,
2147
+ sound: sound === "none" ? null : sound,
2148
+ data: payload.data ?? {}
2149
+ };
2150
+ });
2151
+ try {
2152
+ console.log("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\uFF0Ctokens:", Array.from(this.tokens));
2153
+ const res = await fetch(EXPO_PUSH_API, {
2154
+ method: "POST",
2155
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
2156
+ body: JSON.stringify(messages)
2157
+ });
2158
+ const body = await res.json();
2159
+ if (!res.ok) {
2160
+ console.warn("[ExpoNotificationChannel] Expo Push API \u8FD4\u56DE\u9519\u8BEF:", res.status, JSON.stringify(body));
2161
+ } else {
2162
+ if (!Array.isArray(body?.data)) {
2163
+ console.warn("[ExpoNotificationChannel] Expo Push API \u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF0C\u7F3A\u5C11 data \u6570\u7EC4:", JSON.stringify(body));
2164
+ return;
2165
+ }
2166
+ for (const ticket of body.data) {
2167
+ if (ticket.status === "error") {
2168
+ console.error(`[ExpoNotificationChannel] \u63A8\u9001\u5931\u8D25: ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
2169
+ }
2170
+ }
2171
+ }
2172
+ } catch (err) {
2173
+ console.warn("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\u5931\u8D25:", err);
2174
+ }
2175
+ }
2176
+ };
2177
+
2178
+ // src/notification/ActivityPushChannel.ts
2179
+ var http2 = __toESM(require("http2"));
2180
+ var fs2 = __toESM(require("fs"));
2181
+ var crypto = __toESM(require("crypto"));
2182
+ var ActivityPushChannel = class {
2183
+ /** sessionId -> activityPushToken */
2184
+ tokens = /* @__PURE__ */ new Map();
2185
+ teamId;
2186
+ keyId;
2187
+ authKey;
2188
+ apnsHost;
2189
+ /** 缓存的 JWT token + 过期时间 */
2190
+ cachedJwt = null;
2191
+ /** 复用的 HTTP/2 长连接 */
2192
+ http2Client = null;
2193
+ constructor(config) {
2194
+ this.teamId = config.teamId;
2195
+ this.keyId = config.keyId;
2196
+ this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
2197
+ this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
2198
+ console.log(`[ActivityPushChannel] \u5DF2\u521D\u59CB\u5316 (${config.sandbox ? "\u6C99\u7BB1" : "\u751F\u4EA7"}\u6A21\u5F0F)`);
2199
+ }
2200
+ /** 获取或新建 HTTP/2 长连接 */
2201
+ getHttp2Client() {
2202
+ if (this.http2Client && !this.http2Client.destroyed && !this.http2Client.closed) {
2203
+ return this.http2Client;
2204
+ }
2205
+ this.http2Client = http2.connect(`https://${this.apnsHost}`);
2206
+ this.http2Client.on("error", (err) => {
2207
+ console.warn("[ActivityPushChannel] HTTP/2 \u8FDE\u63A5\u9519\u8BEF\uFF0C\u5C06\u5728\u4E0B\u6B21\u8BF7\u6C42\u65F6\u91CD\u5EFA:", err.message);
2208
+ this.http2Client?.destroy();
2209
+ this.http2Client = null;
2210
+ });
2211
+ this.http2Client.on("close", () => {
2212
+ this.http2Client = null;
2213
+ });
2214
+ return this.http2Client;
2215
+ }
2216
+ /** 注册 Activity push token */
2217
+ addToken(sessionId, token) {
2218
+ this.tokens.set(sessionId, token);
2219
+ console.log(`[ActivityPushChannel] \u5DF2\u6CE8\u518C token: session=${sessionId}`);
2220
+ }
2221
+ /** 移除 Activity push token */
2222
+ removeToken(sessionId) {
2223
+ this.tokens.delete(sessionId);
2224
+ }
2225
+ /** 发送 content-state 更新到指定会话的 Live Activity */
2226
+ async updateActivity(sessionId, contentState) {
2227
+ const token = this.tokens.get(sessionId);
2228
+ if (!token) return;
2229
+ const payload = {
2230
+ aps: {
2231
+ timestamp: Math.floor(Date.now() / 1e3),
2232
+ event: "update",
2233
+ "content-state": contentState
2234
+ }
2235
+ };
2236
+ try {
2237
+ await this.sendToAPNs(token, payload);
2238
+ } catch (err) {
2239
+ console.warn(`[ActivityPushChannel] \u66F4\u65B0\u5931\u8D25 session=${sessionId}:`, err);
2240
+ }
2241
+ }
2242
+ /** 发送带通知的 content-state 更新(审批请求时使用) */
2243
+ async updateActivityWithAlert(sessionId, contentState, alert) {
2244
+ const token = this.tokens.get(sessionId);
2245
+ if (!token) return;
2246
+ const payload = {
2247
+ aps: {
2248
+ timestamp: Math.floor(Date.now() / 1e3),
2249
+ event: "update",
2250
+ "content-state": contentState,
2251
+ alert,
2252
+ sound: "default"
2253
+ }
2254
+ };
2255
+ try {
2256
+ await this.sendToAPNs(token, payload);
2257
+ } catch (err) {
2258
+ console.warn(`[ActivityPushChannel] \u5E26\u63D0\u9192\u66F4\u65B0\u5931\u8D25 session=${sessionId}:`, err);
2259
+ }
2260
+ }
2261
+ /** 结束指定会话的 Live Activity */
2262
+ async endActivity(sessionId, contentState) {
2263
+ const token = this.tokens.get(sessionId);
2264
+ if (!token) return;
2265
+ const payload = {
2266
+ aps: {
2267
+ timestamp: Math.floor(Date.now() / 1e3),
2268
+ event: "end",
2269
+ "content-state": contentState
2270
+ }
2271
+ };
2272
+ try {
2273
+ await this.sendToAPNs(token, payload);
2274
+ } catch (err) {
2275
+ console.warn(`[ActivityPushChannel] \u7ED3\u675F\u5931\u8D25 session=${sessionId}:`, err);
2276
+ }
2277
+ this.tokens.delete(sessionId);
2278
+ }
2279
+ /** 检查是否有指定会话的 token */
2280
+ hasToken(sessionId) {
2281
+ return this.tokens.has(sessionId);
2282
+ }
2283
+ /** 发送 APNs HTTP/2 请求 */
2284
+ async sendToAPNs(deviceToken, payload) {
2285
+ const topic = "com.kachun.sessix.push-type.liveactivity";
2286
+ const jwt = this.getJWT();
2287
+ const payloadStr = JSON.stringify(payload);
2288
+ return new Promise((resolve, reject) => {
2289
+ let client;
2290
+ try {
2291
+ client = this.getHttp2Client();
2292
+ } catch (err) {
2293
+ return reject(err);
2294
+ }
2295
+ const req = client.request({
2296
+ ":method": "POST",
2297
+ ":path": `/3/device/${deviceToken}`,
2298
+ "authorization": `bearer ${jwt}`,
2299
+ "apns-topic": topic,
2300
+ "apns-push-type": "liveactivity",
2301
+ "apns-priority": "10",
2302
+ "apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
2303
+ "content-type": "application/json",
2304
+ "content-length": Buffer.byteLength(payloadStr)
2305
+ });
2306
+ let statusCode = 0;
2307
+ let responseData = "";
2308
+ req.on("response", (headers) => {
2309
+ statusCode = Number(headers[":status"] ?? 0);
2310
+ });
2311
+ req.on("data", (chunk) => {
2312
+ responseData += chunk;
2313
+ });
2314
+ req.on("end", () => {
2315
+ if (statusCode === 200) {
2316
+ resolve();
2317
+ } else {
2318
+ if (statusCode === 0) {
2319
+ this.http2Client?.destroy();
2320
+ this.http2Client = null;
2321
+ }
2322
+ reject(new Error(`APNs \u8FD4\u56DE ${statusCode}: ${responseData}`));
2323
+ }
2324
+ });
2325
+ req.on("error", (err) => {
2326
+ reject(err);
2327
+ });
2328
+ req.write(payloadStr);
2329
+ req.end();
2330
+ });
2331
+ }
2332
+ /** 生成或获取缓存的 APNs JWT token */
2333
+ getJWT() {
2334
+ const now = Math.floor(Date.now() / 1e3);
2335
+ if (this.cachedJwt && this.cachedJwt.expiresAt > now) {
2336
+ return this.cachedJwt.token;
2337
+ }
2338
+ const header = Buffer.from(JSON.stringify({
2339
+ alg: "ES256",
2340
+ kid: this.keyId
2341
+ })).toString("base64url");
2342
+ const claims = Buffer.from(JSON.stringify({
2343
+ iss: this.teamId,
2344
+ iat: now
2345
+ })).toString("base64url");
2346
+ const signingInput = `${header}.${claims}`;
2347
+ const sign = crypto.createSign("SHA256");
2348
+ sign.update(signingInput);
2349
+ const signature = sign.sign(this.authKey, "base64url");
2350
+ const token = `${signingInput}.${signature}`;
2351
+ this.cachedJwt = { token, expiresAt: now + 3e3 };
2352
+ return token;
2353
+ }
2354
+ };
2355
+
2356
+ // src/session/ProjectReader.ts
2357
+ var import_promises3 = require("fs/promises");
2358
+ var import_readline2 = require("readline");
2359
+ var import_path = require("path");
2360
+ var import_os = require("os");
2361
+ var CLAUDE_PROJECTS_DIR = (0, import_path.join)((0, import_os.homedir)(), ".claude", "projects");
2362
+ function getSessionFilePath(projectPath, sessionId) {
2363
+ return (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodeDirName(projectPath), `${sessionId}.jsonl`);
2364
+ }
2365
+ async function getProjects() {
2366
+ try {
2367
+ const dirExists = await directoryExists(CLAUDE_PROJECTS_DIR);
2368
+ if (!dirExists) {
2369
+ return { ok: true, value: [] };
2370
+ }
2371
+ const entries = await (0, import_promises3.readdir)(CLAUDE_PROJECTS_DIR, { withFileTypes: true });
2372
+ const projects = [];
2373
+ for (const entry of entries) {
2374
+ if (!entry.isDirectory() || entry.name.startsWith(".")) {
2375
+ continue;
2376
+ }
2377
+ const encodedPath = entry.name;
2378
+ const decodedPath = decodeDirName(encodedPath);
2379
+ const name = decodedPath.split("/").filter(Boolean).pop() ?? encodedPath;
2380
+ const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
2381
+ const { count: sessionCount, latestMtime } = await countJsonlFilesWithMtime(projectDir);
2382
+ projects.push({
2383
+ id: encodedPath,
2384
+ path: decodedPath,
2385
+ name,
2386
+ sessionCount,
2387
+ lastActiveAt: latestMtime
2388
+ });
2389
+ }
2390
+ projects.sort((a, b) => a.name.localeCompare(b.name));
2391
+ return { ok: true, value: projects };
2392
+ } catch (err) {
2393
+ return {
2394
+ ok: false,
2395
+ error: err instanceof Error ? err : new Error(String(err))
2396
+ };
2397
+ }
2398
+ }
2399
+ async function getHistoricalSessions(projectPath) {
2400
+ try {
2401
+ const encodedPath = encodeDirName(projectPath);
2402
+ const projectDir = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath);
2403
+ const dirExists = await directoryExists(projectDir);
2404
+ if (!dirExists) {
2405
+ return { ok: true, value: [] };
2406
+ }
2407
+ const entries = await (0, import_promises3.readdir)(projectDir, { withFileTypes: true });
2408
+ const jsonlFiles = entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl"));
2409
+ const mtimeMap = /* @__PURE__ */ new Map();
2410
+ for (const entry of jsonlFiles) {
2411
+ const sessionId = entry.name.slice(0, -6);
2412
+ const filePath = (0, import_path.join)(projectDir, entry.name);
2413
+ try {
2414
+ const fileStat = await (0, import_promises3.stat)(filePath);
2415
+ mtimeMap.set(sessionId, fileStat.mtimeMs);
2416
+ } catch {
2417
+ mtimeMap.set(sessionId, 0);
2418
+ }
2419
+ }
2420
+ const uuidDirs = entries.filter(
2421
+ (e) => e.isDirectory() && UUID_RE.test(e.name) && !mtimeMap.has(e.name)
2422
+ );
2423
+ for (const entry of uuidDirs) {
2424
+ try {
2425
+ const fileStat = await (0, import_promises3.stat)((0, import_path.join)(projectDir, entry.name));
2426
+ mtimeMap.set(entry.name, fileStat.mtimeMs);
2427
+ } catch {
2428
+ mtimeMap.set(entry.name, 0);
2429
+ }
2430
+ }
2431
+ const indexPath = (0, import_path.join)(projectDir, "sessions-index.json");
2432
+ const sessionMap = /* @__PURE__ */ new Map();
2433
+ try {
2434
+ const indexContent = await (0, import_promises3.readFile)(indexPath, "utf-8");
2435
+ const indexData = JSON.parse(indexContent);
2436
+ if (indexData.version === 1 && Array.isArray(indexData.entries)) {
2437
+ for (const entry of indexData.entries) {
2438
+ const mtime = mtimeMap.get(entry.sessionId) ?? entry.fileMtime ?? (entry.modified ? new Date(entry.modified).getTime() : 0);
2439
+ sessionMap.set(entry.sessionId, {
2440
+ sessionId: entry.sessionId,
2441
+ lastModified: mtime,
2442
+ summary: entry.summary,
2443
+ firstPrompt: entry.firstPrompt,
2444
+ messageCount: entry.messageCount
2445
+ });
2446
+ }
2447
+ await Promise.all(
2448
+ Array.from(sessionMap.values()).filter((s) => (s.messageCount ?? 0) > 0 && !s.summary && !s.firstPrompt).map(async (s) => {
2449
+ const filePath = (0, import_path.join)(projectDir, `${s.sessionId}.jsonl`);
2450
+ const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
2451
+ if (firstPrompt) s.firstPrompt = firstPrompt;
2452
+ })
2453
+ );
2454
+ }
2455
+ } catch {
2456
+ }
2457
+ const uuidDirSet = new Set(uuidDirs.map((e) => e.name));
2458
+ for (const [sessionId, mtime] of mtimeMap) {
2459
+ if (!sessionMap.has(sessionId)) {
2460
+ if (uuidDirSet.has(sessionId)) {
2461
+ sessionMap.set(sessionId, { sessionId, lastModified: mtime, messageCount: -1 });
2462
+ } else {
2463
+ const filePath = (0, import_path.join)(projectDir, `${sessionId}.jsonl`);
2464
+ const firstPrompt = await extractFirstPrompt(filePath).catch(() => void 0);
2465
+ sessionMap.set(sessionId, { sessionId, lastModified: mtime, firstPrompt });
2466
+ }
2467
+ }
2468
+ }
2469
+ const sessions = Array.from(sessionMap.values()).filter((s) => {
2470
+ if (s.messageCount === 0) return false;
2471
+ if (s.messageCount === -1) return true;
2472
+ if (s.firstPrompt === void 0 && s.messageCount === void 0) return false;
2473
+ return true;
2474
+ });
2475
+ sessions.sort((a, b) => b.lastModified - a.lastModified);
2476
+ return { ok: true, value: sessions };
2477
+ } catch (err) {
2478
+ return {
2479
+ ok: false,
2480
+ error: err instanceof Error ? err : new Error(String(err))
2481
+ };
2482
+ }
2483
+ }
2484
+ async function getSessionHistory(projectPath, sessionId) {
2485
+ try {
2486
+ const encodedPath = encodeDirName(projectPath);
2487
+ const filePath = (0, import_path.join)(CLAUDE_PROJECTS_DIR, encodedPath, `${sessionId}.jsonl`);
2488
+ const raw = await (0, import_promises3.readFile)(filePath, "utf-8").catch((err) => {
2489
+ if (err.code === "ENOENT") return null;
2490
+ throw err;
2491
+ });
2492
+ if (raw === null) return { ok: false, error: new Error("ENOENT") };
2493
+ const lines = raw.split("\n").filter((l) => l.trim());
2494
+ const events = [];
2495
+ for (const line of lines) {
2496
+ try {
2497
+ const obj = JSON.parse(line);
2498
+ const type = obj.type;
2499
+ if (type === "user" && obj.message) {
2500
+ const msgContent = obj.message.content;
2501
+ if (typeof msgContent === "string") {
2502
+ if (msgContent.includes("<local-command") || msgContent.includes("<command-name>")) continue;
2503
+ } else if (Array.isArray(msgContent)) {
2504
+ const hasText = msgContent.some(
2505
+ (b) => b.type === "text" && !b.text?.includes("<local-command") && !b.text?.includes("<command-name>")
2506
+ );
2507
+ if (!hasText) continue;
2508
+ }
2509
+ const normalizedContent = typeof msgContent === "string" ? [{ type: "text", text: msgContent }] : Array.isArray(msgContent) ? msgContent.filter((b) => b.type === "text" && typeof b.text === "string") : [];
2510
+ if (normalizedContent.length === 0) continue;
2511
+ events.push({
2512
+ type: "user",
2513
+ message: {
2514
+ ...obj.message,
2515
+ content: normalizedContent
2516
+ },
2517
+ session_id: sessionId
2518
+ });
2519
+ } else if (type === "assistant" && obj.message) {
2520
+ const content = (obj.message.content ?? []).filter(
2521
+ (b) => b.type === "text" || b.type === "tool_use" || b.type === "thinking"
2522
+ );
2523
+ if (content.length === 0) continue;
2524
+ events.push({
2525
+ type: "assistant",
2526
+ message: {
2527
+ id: obj.message.id ?? obj.uuid ?? `hist-${events.length}`,
2528
+ model: obj.message.model ?? "unknown",
2529
+ role: "assistant",
2530
+ content,
2531
+ stop_reason: obj.message.stop_reason,
2532
+ usage: obj.message.usage
2533
+ },
2534
+ session_id: sessionId
2535
+ });
2536
+ }
2537
+ } catch {
2538
+ }
2539
+ }
2540
+ if (events.length > 0) {
2541
+ let totalInputTokens = 0;
2542
+ let totalOutputTokens = 0;
2543
+ for (const ev of events) {
2544
+ if (ev.type === "assistant" && ev.message.usage) {
2545
+ totalInputTokens += ev.message.usage.input_tokens ?? 0;
2546
+ totalOutputTokens += ev.message.usage.output_tokens ?? 0;
2547
+ }
2548
+ }
2549
+ if (totalInputTokens > 0 || totalOutputTokens > 0) {
2550
+ events.push({
2551
+ type: "result",
2552
+ subtype: "success",
2553
+ is_error: false,
2554
+ duration_ms: 0,
2555
+ num_turns: events.filter((e) => e.type === "user").length,
2556
+ result: "",
2557
+ session_id: sessionId,
2558
+ usage: { input_tokens: totalInputTokens, output_tokens: totalOutputTokens }
2559
+ });
2560
+ }
2561
+ }
2562
+ return { ok: true, value: events };
2563
+ } catch (err) {
2564
+ return {
2565
+ ok: false,
2566
+ error: err instanceof Error ? err : new Error(String(err))
2567
+ };
2568
+ }
2569
+ }
2570
+ async function extractFirstPrompt(filePath) {
2571
+ let fileHandle;
2572
+ try {
2573
+ fileHandle = await (0, import_promises3.open)(filePath, "r");
2574
+ const rl = (0, import_readline2.createInterface)({
2575
+ input: fileHandle.createReadStream({ encoding: "utf-8" }),
2576
+ crlfDelay: Infinity
2577
+ });
2578
+ let lineCount = 0;
2579
+ for await (const line of rl) {
2580
+ if (++lineCount > 20) break;
2581
+ if (!line.trim()) continue;
2582
+ try {
2583
+ const obj = JSON.parse(line);
2584
+ if (obj.type === "user" && obj.message) {
2585
+ const msgContent = obj.message.content;
2586
+ let text = "";
2587
+ if (typeof msgContent === "string") {
2588
+ text = msgContent;
2589
+ } else if (Array.isArray(msgContent)) {
2590
+ const textBlock = msgContent.find((b) => b.type === "text" && typeof b.text === "string");
2591
+ text = textBlock?.text ?? "";
2592
+ }
2593
+ if (text && !text.includes("<local-command") && !text.includes("<command-name>")) {
2594
+ text = text.replace(/<ide_opened_file>[\s\S]*?<\/ide_opened_file>/gi, "");
2595
+ text = text.replace(/<[^>]+>/g, "").trim();
2596
+ rl.close();
2597
+ return text.length > 80 ? text.slice(0, 80) + "..." : text;
2598
+ }
2599
+ }
2600
+ } catch {
2601
+ }
2602
+ }
2603
+ } catch {
2604
+ } finally {
2605
+ await fileHandle?.close();
2606
+ }
2607
+ return void 0;
2608
+ }
2609
+ function decodeDirName(dirName) {
2610
+ const placeholder = "\0";
2611
+ const escaped = dirName.replace(/--/g, placeholder);
2612
+ const decoded = escaped.replace(/-/g, "/");
2613
+ return decoded.replace(new RegExp(placeholder, "g"), "-");
2614
+ }
2615
+ function encodeDirName(path2) {
2616
+ const escaped = path2.replace(/-/g, "--");
2617
+ return escaped.replace(/\//g, "-");
2618
+ }
2619
+ async function directoryExists(dirPath) {
2620
+ try {
2621
+ const s = await (0, import_promises3.stat)(dirPath);
2622
+ return s.isDirectory();
2623
+ } catch {
2624
+ return false;
2625
+ }
2626
+ }
2627
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
2628
+ async function countJsonlFilesWithMtime(dirPath) {
2629
+ try {
2630
+ const entries = await (0, import_promises3.readdir)(dirPath, { withFileTypes: true });
2631
+ const jsonlNames = new Set(
2632
+ entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")).map((e) => e.name.slice(0, -6))
2633
+ );
2634
+ const uuidDirs = entries.filter(
2635
+ (e) => e.isDirectory() && UUID_RE.test(e.name) && !jsonlNames.has(e.name)
2636
+ );
2637
+ let latestMtime = 0;
2638
+ const allEntries = [
2639
+ ...entries.filter((e) => e.isFile() && e.name.endsWith(".jsonl")),
2640
+ ...uuidDirs
2641
+ ];
2642
+ for (const entry of allEntries) {
2643
+ try {
2644
+ const fileStat = await (0, import_promises3.stat)((0, import_path.join)(dirPath, entry.name));
2645
+ if (fileStat.mtimeMs > latestMtime) latestMtime = fileStat.mtimeMs;
2646
+ } catch {
2647
+ }
2648
+ }
2649
+ return { count: jsonlNames.size + uuidDirs.length, latestMtime };
2650
+ } catch {
2651
+ return { count: 0, latestMtime: 0 };
2652
+ }
2653
+ }
2654
+
2655
+ // src/server.ts
2656
+ var import_promises5 = require("fs/promises");
2657
+ var WS_PORT = 3745;
2658
+ var HTTP_PORT = 3746;
2659
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process2.exec);
2660
+ async function killPortProcess(port) {
2661
+ try {
2662
+ const { stdout } = await execAsync(`lsof -ti :${port}`);
2663
+ const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
2664
+ if (pids.length > 0) {
2665
+ await execAsync(`kill -9 ${pids.join(" ")}`);
2666
+ await new Promise((resolve) => setTimeout(resolve, 600));
2667
+ }
2668
+ } catch {
2669
+ }
2670
+ }
2671
+ async function createWithRetry(label, port, factory) {
2672
+ try {
2673
+ return await factory();
2674
+ } catch (err) {
2675
+ if (err?.code === "EADDRINUSE") {
2676
+ console.warn(`[Server] \u7AEF\u53E3 ${port} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u91CA\u653E\u65E7\u8FDB\u7A0B...`);
2677
+ await killPortProcess(port);
2678
+ console.log(`[Server] \u91CD\u65B0\u542F\u52A8 ${label}...`);
2679
+ return await factory();
2680
+ }
2681
+ throw err;
2682
+ }
2683
+ }
2684
+ async function start(opts = {}) {
2685
+ const configDir = (0, import_node_path4.join)((0, import_node_os4.homedir)(), ".sessix");
2686
+ const tokenFile = (0, import_node_path4.join)(configDir, "token");
2687
+ let token;
2688
+ if (opts.token !== void 0) {
2689
+ token = opts.token;
2690
+ } else {
2691
+ const envToken = process.env.SESSIX_TOKEN;
2692
+ if (envToken !== void 0) {
2693
+ token = envToken;
2694
+ } else {
2695
+ try {
2696
+ token = (await (0, import_promises4.readFile)(tokenFile, "utf8")).trim();
2697
+ } catch {
2698
+ token = (0, import_uuid4.v4)();
2699
+ await (0, import_promises4.mkdir)(configDir, { recursive: true });
2700
+ await (0, import_promises4.writeFile)(tokenFile, token, "utf8");
2701
+ }
2702
+ }
2703
+ }
2704
+ const provider = new ProcessProvider();
2705
+ const sessionManager = new SessionManager(provider);
2706
+ const expoChannel = new ExpoNotificationChannel();
2707
+ const notificationService = new NotificationService(sessionManager, expoChannel);
2708
+ notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
2709
+ notificationService.addChannel("mac", new MacNotificationChannel(), opts.enableMacNotification !== false);
2710
+ if (opts.activityPush) {
2711
+ try {
2712
+ const activityChannel = new ActivityPushChannel(opts.activityPush);
2713
+ notificationService.setActivityPushChannel(activityChannel);
2714
+ console.log("[Server] ActivityKit Push \u5DF2\u542F\u7528");
2715
+ } catch (err) {
2716
+ console.warn("[Server] ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:", err);
2717
+ console.log("[Server] \u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09");
2718
+ }
2719
+ }
2720
+ const wsBridge = await createWithRetry(
2721
+ "WsBridge",
2722
+ WS_PORT,
2723
+ () => WsBridge.create({ port: WS_PORT, token })
2724
+ );
2725
+ const sessionFileWatcher = new SessionFileWatcher((event) => {
2726
+ wsBridge.broadcast(event);
2727
+ });
2728
+ const approvalProxy = await createWithRetry(
2729
+ "ApprovalProxy",
2730
+ HTTP_PORT,
2731
+ () => ApprovalProxy.create({ port: HTTP_PORT, token })
2732
+ );
2733
+ wsBridge.onConnection(async (ws) => {
2734
+ const result = await getProjects();
2735
+ if (result.ok) {
2736
+ wsBridge.send(ws, { type: "project_list", projects: result.value });
2737
+ }
2738
+ wsBridge.send(ws, {
2739
+ type: "session_list",
2740
+ sessions: sessionManager.getActiveSessions()
2741
+ });
2742
+ });
2743
+ wsBridge.onClientEvent(async (event, ws) => {
2744
+ try {
2745
+ switch (event.type) {
2746
+ case "create_session": {
2747
+ await (0, import_promises4.mkdir)(event.projectPath, { recursive: true });
2748
+ await sessionManager.createSession(
2749
+ event.projectPath,
2750
+ event.message,
2751
+ event.resumeSessionId,
2752
+ event.newSessionId,
2753
+ event.model,
2754
+ event.permissionMode,
2755
+ event.effort,
2756
+ event.images
2757
+ );
2758
+ wsBridge.broadcast({
2759
+ type: "session_list",
2760
+ sessions: sessionManager.getActiveSessions()
2761
+ });
2762
+ break;
2763
+ }
2764
+ case "send_message": {
2765
+ await sessionManager.sendMessage(event.sessionId, event.message, event.permissionMode, event.images);
2766
+ wsBridge.broadcast({
2767
+ type: "session_list",
2768
+ sessions: sessionManager.getActiveSessions()
2769
+ });
2770
+ break;
2771
+ }
2772
+ case "kill_session": {
2773
+ wsBridge.broadcast({ type: "status_change", sessionId: event.sessionId, status: "idle" });
2774
+ await sessionManager.killSession(event.sessionId);
2775
+ wsBridge.broadcast({
2776
+ type: "session_list",
2777
+ sessions: sessionManager.getActiveSessions()
2778
+ });
2779
+ break;
2780
+ }
2781
+ case "approve": {
2782
+ approvalProxy.resolveApproval(event.requestId, { decision: "allow" });
2783
+ break;
2784
+ }
2785
+ case "reject": {
2786
+ const decision = { decision: "deny", reason: event.reason };
2787
+ approvalProxy.resolveApproval(event.requestId, decision);
2788
+ break;
2789
+ }
2790
+ case "answer_question": {
2791
+ sessionManager.handleQuestionResponse(event.requestId, event.answer);
2792
+ break;
2793
+ }
2794
+ case "subscribe": {
2795
+ wsBridge.send(ws, {
2796
+ type: "session_list",
2797
+ sessions: sessionManager.getActiveSessions()
2798
+ });
2799
+ const bufferedEvents = sessionManager.getSessionEvents(event.sessionId);
2800
+ if (bufferedEvents.length > 0) {
2801
+ wsBridge.send(ws, {
2802
+ type: "session_history",
2803
+ sessionId: event.sessionId,
2804
+ events: bufferedEvents
2805
+ });
2806
+ }
2807
+ for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
2808
+ wsBridge.send(ws, { type: "approval_request", request: req });
2809
+ }
2810
+ break;
2811
+ }
2812
+ case "list_projects": {
2813
+ const result = await getProjects();
2814
+ if (result.ok) {
2815
+ wsBridge.send(ws, { type: "project_list", projects: result.value });
2816
+ } else {
2817
+ wsBridge.send(ws, {
2818
+ type: "error",
2819
+ message: `\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: ${result.error.message}`,
2820
+ code: "PROJECT_LIST_ERROR"
2821
+ });
2822
+ }
2823
+ break;
2824
+ }
2825
+ case "list_sessions": {
2826
+ wsBridge.send(ws, {
2827
+ type: "session_list",
2828
+ sessions: sessionManager.getActiveSessions().filter(
2829
+ (s) => s.projectPath === event.projectPath
2830
+ )
2831
+ });
2832
+ break;
2833
+ }
2834
+ case "list_project_sessions": {
2835
+ const histResult = await getHistoricalSessions(event.projectPath);
2836
+ if (histResult.ok) {
2837
+ wsBridge.send(ws, {
2838
+ type: "project_sessions",
2839
+ projectPath: event.projectPath,
2840
+ sessions: histResult.value
2841
+ });
2842
+ } else {
2843
+ wsBridge.send(ws, {
2844
+ type: "error",
2845
+ message: `\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: ${histResult.error.message}`,
2846
+ code: "PROJECT_SESSIONS_ERROR"
2847
+ });
2848
+ }
2849
+ break;
2850
+ }
2851
+ case "load_session_history": {
2852
+ const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
2853
+ if (!historyResult.ok) {
2854
+ wsBridge.send(ws, {
2855
+ type: "error",
2856
+ message: `\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: ${historyResult.error.message}`,
2857
+ code: "SESSION_HISTORY_ERROR",
2858
+ sessionId: event.sessionId
2859
+ });
2860
+ } else if (historyResult.value.length > 0) {
2861
+ wsBridge.send(ws, {
2862
+ type: "session_history",
2863
+ sessionId: event.sessionId,
2864
+ events: historyResult.value
2865
+ });
2866
+ const activeSession = sessionManager.getActiveSessions().find((s) => s.id === event.sessionId);
2867
+ const isStreaming = activeSession?.status === "running" || activeSession?.status === "waiting_approval";
2868
+ if (!isStreaming) {
2869
+ const filePath = getSessionFilePath(event.projectPath, event.sessionId);
2870
+ try {
2871
+ const fileStat = await (0, import_promises5.stat)(filePath);
2872
+ sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
2873
+ } catch {
2874
+ }
2875
+ }
2876
+ }
2877
+ break;
2878
+ }
2879
+ case "suggest_next_prompt": {
2880
+ const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
2881
+ let context = "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09";
2882
+ if (historyResult.ok && historyResult.value.length > 0) {
2883
+ const recent = historyResult.value.slice(-10);
2884
+ context = recent.map((e) => {
2885
+ if (e.type === "assistant") {
2886
+ const text = e.message.content.filter((b) => b.type === "text").map((b) => b.text).join("");
2887
+ return `Assistant: ${text.substring(0, 300)}`;
2888
+ }
2889
+ if (e.type === "user") {
2890
+ const text = e.message.content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
2891
+ return text ? `User: ${text.substring(0, 300)}` : null;
2892
+ }
2893
+ return null;
2894
+ }).filter(Boolean).join("\n");
2895
+ }
2896
+ const suggestion = await provider.generateSuggestion(context);
2897
+ wsBridge.send(ws, {
2898
+ type: "prompt_suggestion",
2899
+ sessionId: event.sessionId,
2900
+ suggestion
2901
+ });
2902
+ break;
2903
+ }
2904
+ case "register_push_token": {
2905
+ notificationService.addPushToken(event.token);
2906
+ break;
2907
+ }
2908
+ case "unregister_push_token": {
2909
+ notificationService.removePushToken(event.token);
2910
+ break;
2911
+ }
2912
+ case "update_notification_sounds": {
2913
+ notificationService.setSoundPreferences(event.preferences);
2914
+ break;
2915
+ }
2916
+ case "register_activity_push_token": {
2917
+ notificationService.addActivityPushToken(event.sessionId, event.token);
2918
+ break;
2919
+ }
2920
+ case "unregister_activity_push_token": {
2921
+ notificationService.removeActivityPushToken(event.sessionId);
2922
+ break;
2923
+ }
2924
+ case "set_yolo_mode": {
2925
+ notificationService.setYoloMode(event.sessionId, event.enabled);
2926
+ approvalProxy.setYoloMode(event.sessionId, event.enabled);
2927
+ break;
2928
+ }
2929
+ case "viewing_session": {
2930
+ wsBridge.setViewingSession(ws, event.sessionId);
2931
+ break;
2932
+ }
2933
+ case "left_session": {
2934
+ wsBridge.clearViewingSession(ws);
2935
+ break;
2936
+ }
2937
+ case "always_allow_tool": {
2938
+ approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
2939
+ break;
2940
+ }
2941
+ default: {
2942
+ wsBridge.send(ws, {
2943
+ type: "error",
2944
+ message: `\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: ${event.type}`,
2945
+ code: "UNKNOWN_EVENT"
2946
+ });
2947
+ }
2948
+ }
2949
+ } catch (err) {
2950
+ const message = err instanceof Error ? err.message : String(err);
2951
+ console.error("[Server] \u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38:", message);
2952
+ const errorCodeMap = {
2953
+ create_session: "SESSION_CREATE_ERROR",
2954
+ send_message: "SEND_MESSAGE_ERROR",
2955
+ kill_session: "KILL_SESSION_ERROR",
2956
+ approve: "APPROVE_ERROR",
2957
+ reject: "REJECT_ERROR",
2958
+ answer_question: "ANSWER_QUESTION_ERROR",
2959
+ suggest_next_prompt: "SUGGEST_PROMPT_ERROR"
2960
+ };
2961
+ const code = errorCodeMap[event.type] ?? "INTERNAL_ERROR";
2962
+ wsBridge.send(ws, { type: "error", message, code });
2963
+ }
2964
+ });
2965
+ sessionManager.onEvent((event) => {
2966
+ wsBridge.broadcast(event);
2967
+ });
2968
+ wsBridge.onDisconnect(() => {
2969
+ if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
2970
+ approvalProxy.approveAll("\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00");
2971
+ }
2972
+ });
2973
+ approvalProxy.onApprovalRequest((request) => {
2974
+ wsBridge.broadcast({ type: "approval_request", request });
2975
+ setTimeout(() => {
2976
+ if (!approvalProxy.isPending(request.id)) return;
2977
+ if (wsBridge.isViewingSession(request.sessionId)) return;
2978
+ if (wsBridge.getConnectionCount() > 0) return;
2979
+ const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
2980
+ notificationService.notifyApproval(request, pendingCount);
2981
+ }, 5e3);
2982
+ setTimeout(() => {
2983
+ if (!approvalProxy.isPending(request.id)) return;
2984
+ if (wsBridge.isViewingSession(request.sessionId)) return;
2985
+ if (wsBridge.getConnectionCount() > 0) return;
2986
+ console.log(`[Server] \u5BA1\u6279\u8BF7\u6C42 ${request.id} 60\u79D2\u672A\u5904\u7406\uFF0C\u91CD\u8BD5\u63A8\u9001`);
2987
+ const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
2988
+ notificationService.notifyApproval(request, pendingCount);
2989
+ }, 6e4);
2990
+ });
2991
+ approvalProxy.setStatusInfoProvider(() => ({
2992
+ connections: wsBridge.getConnectionCount(),
2993
+ activeSessions: sessionManager.getActiveSessions().length
2994
+ }));
2995
+ const mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT });
2996
+ mdnsService.start();
2997
+ const hookInstaller = new HookInstaller();
2998
+ try {
2999
+ const installed = await hookInstaller.isInstalled();
3000
+ if (!installed) {
3001
+ await hookInstaller.install();
3002
+ console.log("[Server] Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code");
3003
+ } else {
3004
+ console.log("[Server] Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5");
3005
+ }
3006
+ } catch (err) {
3007
+ console.error("[Server] Hook \u5B89\u88C5\u5931\u8D25:", err);
3008
+ console.log("[Server] \u7EE7\u7EED\u542F\u52A8\uFF08hook \u529F\u80FD\u53EF\u80FD\u4E0D\u53EF\u7528\uFF09");
3009
+ }
3010
+ const stop = async () => {
3011
+ console.log("[Server] \u6B63\u5728\u4F18\u96C5\u5173\u95ED...");
3012
+ const errors = [];
3013
+ const attempt = async (fn, label) => {
3014
+ try {
3015
+ await fn();
3016
+ } catch (err) {
3017
+ console.error(`[Server] \u5173\u95ED ${label} \u51FA\u9519:`, err);
3018
+ errors.push(err);
3019
+ }
3020
+ };
3021
+ await attempt(() => mdnsService.stop(), "mDNS");
3022
+ await attempt(() => wsBridge.close(), "WebSocket");
3023
+ await attempt(() => approvalProxy.close(), "ApprovalProxy");
3024
+ await attempt(() => sessionManager.destroy(), "SessionManager");
3025
+ await attempt(() => notificationService.destroy(), "NotificationService");
3026
+ await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3027
+ if (errors.length > 0) {
3028
+ console.error(`[Server] \u5173\u95ED\u5B8C\u6210\uFF0C${errors.length} \u4E2A\u9519\u8BEF`);
3029
+ throw errors[0];
3030
+ }
3031
+ console.log("[Server] \u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED");
3032
+ };
3033
+ return {
3034
+ token,
3035
+ wsPort: WS_PORT,
3036
+ httpPort: HTTP_PORT,
3037
+ getActiveSessions: () => sessionManager.getActiveSessions(),
3038
+ getConnectionCount: () => wsBridge.getConnectionCount(),
3039
+ stop,
3040
+ setMacNotification: (enabled) => notificationService.setChannelEnabled("mac", enabled),
3041
+ setExpoPush: (enabled) => notificationService.setChannelEnabled("expo", enabled),
3042
+ onServerEvent: (cb) => sessionManager.onEvent(cb)
3043
+ };
3044
+ }
3045
+
3046
+ // src/index.ts
3047
+ var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
3048
+ async function main() {
3049
+ console.log("=".repeat(50));
3050
+ console.log(" Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3");
3051
+ console.log("=".repeat(50));
3052
+ console.log();
3053
+ const server = await start();
3054
+ const localIp = getLocalIp();
3055
+ console.log("-".repeat(50));
3056
+ console.log(` WebSocket \u7AEF\u53E3: ${server.wsPort}`);
3057
+ console.log(` HTTP \u5BA1\u6279\u7AEF\u53E3: ${server.httpPort}`);
3058
+ if (server.token === "") {
3059
+ console.log(` \u8FDE\u63A5 Token: (\u5DF2\u7981\u7528\uFF0C\u5F00\u53D1\u6A21\u5F0F)`);
3060
+ console.log();
3061
+ console.log(` WebSocket \u5730\u5740: ws://${localIp}:${server.wsPort}`);
3062
+ } else {
3063
+ console.log(` \u8FDE\u63A5 Token: ${server.token}`);
3064
+ console.log();
3065
+ console.log(` WebSocket \u5730\u5740: ws://${localIp}:${server.wsPort}?token=${server.token}`);
3066
+ }
3067
+ console.log(` \u5065\u5EB7\u68C0\u67E5: http://localhost:${server.httpPort}/health`);
3068
+ console.log("-".repeat(50));
3069
+ if (server.token === "") {
3070
+ console.log();
3071
+ console.log(" [\u5F00\u53D1\u6A21\u5F0F] \u65E0\u9700 Token\uFF0C\u624B\u673A\u7AEF\u53EA\u9700\u8F93\u5165 IP:\u7AEF\u53E3 \u5373\u53EF\u8FDE\u63A5");
3072
+ }
3073
+ console.log();
3074
+ const qrUrl = buildQrUrl(localIp, server.wsPort, server.token);
3075
+ console.log(" \u626B\u7801\u914D\u5BF9\uFF1A");
3076
+ import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
3077
+ qr.split("\n").forEach((line) => console.log(` ${line}`));
3078
+ });
3079
+ console.log();
3080
+ console.log(" \u7B49\u5F85\u624B\u673A\u8FDE\u63A5...");
3081
+ console.log();
3082
+ const shutdown = async (signal) => {
3083
+ console.log(`
3084
+ [Main] \u6536\u5230 ${signal}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...`);
3085
+ try {
3086
+ await server.stop();
3087
+ console.log("[Main] \u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01");
3088
+ process.exit(0);
3089
+ } catch (err) {
3090
+ console.error("[Main] \u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:", err);
3091
+ process.exit(1);
3092
+ }
3093
+ };
3094
+ process.on("SIGINT", () => shutdown("SIGINT"));
3095
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
3096
+ }
3097
+ function getLocalIp() {
3098
+ const interfaces = (0, import_node_os5.networkInterfaces)();
3099
+ for (const iface of Object.values(interfaces)) {
3100
+ for (const addr of iface ?? []) {
3101
+ if (addr.family === "IPv4" && !addr.internal) {
3102
+ return addr.address;
3103
+ }
3104
+ }
3105
+ }
3106
+ return "<your-mac-ip>";
3107
+ }
3108
+ function buildQrUrl(ip, wsPort, token) {
3109
+ const base = `sessix://${ip}:${wsPort}`;
3110
+ return token ? `${base}?token=${token}` : base;
3111
+ }
3112
+ main().catch((err) => {
3113
+ console.error("[Main] \u542F\u52A8\u5931\u8D25:", err);
3114
+ process.exit(1);
3115
+ });