sessix-server 0.1.0 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -33,6 +33,256 @@ __export(server_exports, {
33
33
  start: () => start
34
34
  });
35
35
  module.exports = __toCommonJS(server_exports);
36
+
37
+ // src/i18n/locales/zh.ts
38
+ var zh = {
39
+ startup: {
40
+ banner: " Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3",
41
+ scanToPair: " \u626B\u7801\u914D\u5BF9\uFF1A",
42
+ waitingConnection: " \u7B49\u5F85\u624B\u673A\u8FDE\u63A5...",
43
+ wsPort: " WebSocket \u7AEF\u53E3: {{port}}",
44
+ httpPort: " HTTP \u5BA1\u6279\u7AEF\u53E3: {{port}}",
45
+ tokenDisabled: " \u8FDE\u63A5 Token: (\u5DF2\u7981\u7528\uFF0C\u5F00\u53D1\u6A21\u5F0F)",
46
+ token: " \u8FDE\u63A5 Token: {{token}}",
47
+ wsAddress: " WebSocket \u5730\u5740: ws://{{ip}}:{{port}}",
48
+ wsAddressWithToken: " WebSocket \u5730\u5740: ws://{{ip}}:{{port}}?token={{token}}",
49
+ healthCheck: " \u5065\u5EB7\u68C0\u67E5: http://localhost:{{port}}/health",
50
+ devMode: " [\u5F00\u53D1\u6A21\u5F0F] \u65E0\u9700 Token\uFF0C\u624B\u673A\u7AEF\u53EA\u9700\u8F93\u5165 IP:\u7AEF\u53E3 \u5373\u53EF\u8FDE\u63A5",
51
+ autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
52
+ autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
53
+ autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
54
+ receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
55
+ goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
56
+ shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
57
+ startFailed: "\u542F\u52A8\u5931\u8D25:"
58
+ },
59
+ server: {
60
+ listProjectsFailed: "\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: {{error}}",
61
+ listSessionsFailed: "\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: {{error}}",
62
+ readHistoryFailed: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: {{error}}",
63
+ noHistory: "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09",
64
+ unknownEvent: "\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: {{type}}",
65
+ clientEventError: "\u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38",
66
+ phoneDisconnected: "\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00",
67
+ approvalRetry: "\u5BA1\u6279\u8BF7\u6C42 {{id}} 60\u79D2\u672A\u5904\u7406\uFF0C\u91CD\u8BD5\u63A8\u9001",
68
+ hookInstalled: "Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code",
69
+ hookExists: "Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5",
70
+ hookContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08hook \u529F\u80FD\u53EF\u80FD\u4E0D\u53EF\u7528\uFF09",
71
+ hookInstallFailed: "Hook \u5B89\u88C5\u5931\u8D25:",
72
+ shuttingDown: "\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
73
+ shutdownComponentError: "\u5173\u95ED {{label}} \u51FA\u9519",
74
+ shutdownWithErrors: "\u5173\u95ED\u5B8C\u6210\uFF0C{{count}} \u4E2A\u9519\u8BEF",
75
+ shutdownComplete: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED",
76
+ portInUse: "\u7AEF\u53E3 {{port}} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u91CA\u653E\u65E7\u8FDB\u7A0B...",
77
+ restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
78
+ activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
79
+ activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
80
+ activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
81
+ },
82
+ ws: {
83
+ started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
84
+ serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF"
85
+ },
86
+ mdns: {
87
+ alreadyRunning: "\u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D",
88
+ started: "mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 {{port}}",
89
+ stopped: "\u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62",
90
+ closed: "mDNS \u670D\u52A1\u5DF2\u5173\u95ED"
91
+ },
92
+ approval: {
93
+ httpStarted: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
94
+ serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF",
95
+ yoloMode: "YOLO \u6A21\u5F0F{{status}}",
96
+ yoloEnabled: "\u5DF2\u542F\u7528",
97
+ yoloDisabled: "\u5DF2\u5173\u95ED",
98
+ requestNotFound: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6",
99
+ requestProcessed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u5904\u7406",
100
+ alwaysAllowWritten: "\u5DF2\u5C06 {{entry}} \u5199\u5165 {{label}}",
101
+ settingsWriteFailed: "\u5199\u5165 settings.json \u5931\u8D25",
102
+ autoAllowed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u81EA\u52A8\u5141\u8BB8{{reason}}",
103
+ serverClosed: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED",
104
+ httpClosed: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED",
105
+ received: "\u6536\u5230\u5BA1\u6279\u8BF7\u6C42",
106
+ alwaysAllowPassThrough: "{{tool}} \u5DF2\u88AB\u59CB\u7EC8\u5141\u8BB8\uFF0C\u76F4\u63A5\u653E\u884C\uFF08\u4E0D\u901A\u77E5\uFF09",
107
+ yoloAutoAllow: "YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C",
108
+ timeout: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8",
109
+ processingFailed: "\u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25",
110
+ forbidden: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE",
111
+ bodyTooLarge: "\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09",
112
+ invalidJson: "\u65E0\u6548\u7684 JSON body"
113
+ },
114
+ notification: {
115
+ tokenRegistered: "\u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
116
+ tokenRemoved: "\u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
117
+ soundPrefsUpdated: "\u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D",
118
+ sendingPush: "\u53D1\u9001\u63A8\u9001\uFF0Ctokens:",
119
+ pushApiError: "Expo Push API \u8FD4\u56DE\u9519\u8BEF:",
120
+ pushApiFormatError: "Expo Push API \u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF0C\u7F3A\u5C11 data \u6570\u7EC4:",
121
+ pushFailed: "\u63A8\u9001\u5931\u8D25:",
122
+ sendFailed: "\u53D1\u9001\u63A8\u9001\u5931\u8D25:",
123
+ pendingApprovals: "{{title}} \u2014 {{count}} \u9879\u5F85\u5BA1\u6279",
124
+ taskComplete: "\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B65\u6307\u4EE4",
125
+ taskError: "\u6267\u884C\u51FA\u9519\uFF0C\u8BF7\u67E5\u770B\u8BE6\u60C5",
126
+ questionRetry: "\u63D0\u95EE {{id}} 60\u79D2\u672A\u56DE\u7B54\uFF0C\u91CD\u8BD5\u63A8\u9001"
127
+ },
128
+ tray: {
129
+ tooltip: "Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3"
130
+ },
131
+ watcher: {
132
+ readError: "\u8BFB\u53D6\u5F02\u5E38 {{sessionId}}",
133
+ startWatching: "\u5F00\u59CB\u76D1\u542C",
134
+ stopWatching: "\u505C\u6B62\u76D1\u542C"
135
+ }
136
+ };
137
+
138
+ // src/i18n/locales/en.ts
139
+ var en = {
140
+ startup: {
141
+ banner: " Sessix \u2014 AI Coding Mobile Command Center",
142
+ scanToPair: " Scan to pair:",
143
+ waitingConnection: " Waiting for phone connection...",
144
+ wsPort: " WebSocket port: {{port}}",
145
+ httpPort: " HTTP approval port: {{port}}",
146
+ tokenDisabled: " Token: (disabled, dev mode)",
147
+ token: " Token: {{token}}",
148
+ wsAddress: " WebSocket URL: ws://{{ip}}:{{port}}",
149
+ wsAddressWithToken: " WebSocket URL: ws://{{ip}}:{{port}}?token={{token}}",
150
+ healthCheck: " Health check: http://localhost:{{port}}/health",
151
+ devMode: " [Dev mode] No token required, just enter IP:port on your phone",
152
+ autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
153
+ autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
154
+ autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
155
+ receivedSignal: "Received {{signal}}, graceful shutdown...",
156
+ goodbye: "All services closed, goodbye!",
157
+ shutdownError: "Shutdown error:",
158
+ startFailed: "Startup failed:"
159
+ },
160
+ server: {
161
+ listProjectsFailed: "Failed to list projects: {{error}}",
162
+ listSessionsFailed: "Failed to list project sessions: {{error}}",
163
+ readHistoryFailed: "Failed to read session history: {{error}}",
164
+ noHistory: "(No conversation history)",
165
+ unknownEvent: "Unknown event type: {{type}}",
166
+ clientEventError: "Client event handling error",
167
+ phoneDisconnected: "Phone disconnected",
168
+ approvalRetry: "Approval request {{id}} not handled in 60s, retrying push",
169
+ hookInstalled: "Sessix hook installed to Claude Code",
170
+ hookExists: "Sessix hook already exists, skipping installation",
171
+ hookContinue: "Continuing startup (hook functionality may be unavailable)",
172
+ hookInstallFailed: "Hook installation failed:",
173
+ shuttingDown: "Graceful shutdown in progress...",
174
+ shutdownComponentError: "Error closing {{label}}",
175
+ shutdownWithErrors: "Shutdown complete, {{count}} error(s)",
176
+ shutdownComplete: "All services closed",
177
+ portInUse: "Port {{port}} in use, attempting to release old process...",
178
+ restarting: "Restarting {{label}}...",
179
+ activityPushEnabled: "ActivityKit Push enabled",
180
+ activityPushFailed: "ActivityKit Push init failed:",
181
+ activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
182
+ },
183
+ ws: {
184
+ started: "WebSocket server started on port {{port}}",
185
+ serverError: "Server runtime error"
186
+ },
187
+ mdns: {
188
+ alreadyRunning: "Service is already running",
189
+ started: "mDNS broadcast started: _sessix._tcp port {{port}}",
190
+ stopped: "Service broadcast stopped",
191
+ closed: "mDNS service closed"
192
+ },
193
+ approval: {
194
+ httpStarted: "HTTP approval server started on port {{port}}",
195
+ serverError: "Server runtime error",
196
+ yoloMode: "YOLO mode {{status}}",
197
+ yoloEnabled: "enabled",
198
+ yoloDisabled: "disabled",
199
+ requestNotFound: "Approval request {{id}} not found or timed out",
200
+ requestProcessed: "Approval request {{id}} processed",
201
+ alwaysAllowWritten: "Written {{entry}} to {{label}}",
202
+ settingsWriteFailed: "Failed to write settings.json",
203
+ autoAllowed: "Approval request {{id}} auto-allowed{{reason}}",
204
+ serverClosed: "Server closed",
205
+ httpClosed: "HTTP approval server closed",
206
+ received: "Approval request received",
207
+ alwaysAllowPassThrough: "{{tool}} is always-allowed, passing through (no notification)",
208
+ yoloAutoAllow: "YOLO mode, auto-allowing",
209
+ timeout: "Approval request {{id}} timed out, default allowed",
210
+ processingFailed: "Approval request processing failed",
211
+ forbidden: "Forbidden: localhost access only",
212
+ bodyTooLarge: "Request body too large (>1MB)",
213
+ invalidJson: "Invalid JSON body"
214
+ },
215
+ notification: {
216
+ tokenRegistered: "Push token registered, devices: {{count}}",
217
+ tokenRemoved: "Push token removed, devices: {{count}}",
218
+ soundPrefsUpdated: "Sound preferences updated",
219
+ sendingPush: "Sending push, tokens:",
220
+ pushApiError: "Expo Push API returned error:",
221
+ pushApiFormatError: "Expo Push API response format error, missing data array:",
222
+ pushFailed: "Push failed:",
223
+ sendFailed: "Send push failed:",
224
+ pendingApprovals: "{{title}} \u2014 {{count}} pending approval(s)",
225
+ taskComplete: "Completed, awaiting next instruction",
226
+ taskError: "Execution error, check details",
227
+ questionRetry: "Question {{id}} not answered in 60s, retrying push"
228
+ },
229
+ tray: {
230
+ tooltip: "Sessix \u2014 AI Coding Mobile Command Center"
231
+ },
232
+ watcher: {
233
+ readError: "Read error {{sessionId}}",
234
+ startWatching: "Start watching",
235
+ stopWatching: "Stop watching"
236
+ }
237
+ };
238
+
239
+ // src/i18n/index.ts
240
+ var locales = { zh, en };
241
+ function detectLocale() {
242
+ const explicit = process.env.SESSIX_LANG;
243
+ if (explicit && explicit in locales) return explicit;
244
+ try {
245
+ const raw = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
246
+ if (raw.startsWith("zh")) return "zh";
247
+ } catch {
248
+ }
249
+ return "en";
250
+ }
251
+ var currentLocale = detectLocale();
252
+ var currentMessages = locales[currentLocale] ?? en;
253
+ function t(key, params) {
254
+ const parts = key.split(".");
255
+ let val = currentMessages;
256
+ for (const p of parts) {
257
+ if (val && typeof val === "object") {
258
+ val = val[p];
259
+ } else {
260
+ val = void 0;
261
+ break;
262
+ }
263
+ }
264
+ if (typeof val !== "string") {
265
+ let fallback = en;
266
+ for (const p of parts) {
267
+ if (fallback && typeof fallback === "object") {
268
+ fallback = fallback[p];
269
+ } else {
270
+ fallback = void 0;
271
+ break;
272
+ }
273
+ }
274
+ val = typeof fallback === "string" ? fallback : key;
275
+ }
276
+ let result = val;
277
+ if (params) {
278
+ for (const [k, v] of Object.entries(params)) {
279
+ result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
280
+ }
281
+ }
282
+ return result;
283
+ }
284
+
285
+ // src/server.ts
36
286
  var import_uuid4 = require("uuid");
37
287
  var import_promises4 = require("fs/promises");
38
288
  var import_node_os4 = require("os");
@@ -101,12 +351,12 @@ var ProcessProvider = class {
101
351
  session.pid = proc.pid;
102
352
  this.activeSessions.set(sessionId, { session, process: proc, model, permissionMode, effort });
103
353
  proc.on("error", (err) => {
104
- console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} \u8FDB\u7A0B\u9519\u8BEF:`, err.message);
354
+ console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
105
355
  this.activeSessions.delete(sessionId);
106
356
  const syntheticResult = {
107
357
  type: "result",
108
358
  subtype: "error",
109
- result: `\u8FDB\u7A0B\u542F\u52A8\u5931\u8D25: ${err.message}`,
359
+ result: `Process spawn failed: ${err.message}`,
110
360
  session_id: sessionId,
111
361
  duration_ms: 0,
112
362
  is_error: true,
@@ -159,15 +409,17 @@ var ProcessProvider = class {
159
409
  async sendMessage(sessionId, message, permissionMode, images) {
160
410
  const entry = this.activeSessions.get(sessionId);
161
411
  if (!entry) {
162
- throw new Error(`\u4F1A\u8BDD ${sessionId} \u4E0D\u5B58\u5728\u6216\u5DF2\u7ED3\u675F`);
412
+ throw new Error(`Session ${sessionId} not found or already ended`);
163
413
  }
164
414
  const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
165
415
  if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
416
+ entry.session.status = "running";
417
+ entry.session.lastActiveAt = Date.now();
166
418
  this.writeUserMessage(entry.process, message, sessionId, images);
167
419
  return;
168
420
  }
169
421
  if (modeChanged) {
170
- console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u6743\u9650\u6A21\u5F0F\u5207\u6362 ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}\uFF0Crespawn`);
422
+ console.log(`[ProcessProvider] Session ${sessionId}: permission mode change ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}, respawn`);
171
423
  if (entry.process.exitCode === null && entry.process.signalCode === null) {
172
424
  try {
173
425
  entry.process.stdin?.end();
@@ -176,7 +428,7 @@ var ProcessProvider = class {
176
428
  entry.process.kill("SIGTERM");
177
429
  }
178
430
  } else {
179
- console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u8FDB\u7A0B\u5DF2\u9000\u51FA\uFF0Crespawn \u91CD\u65B0\u542F\u52A8`);
431
+ console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
180
432
  }
181
433
  const savedPendingQuestion = entry.pendingQuestion;
182
434
  const newMode = permissionMode ?? entry.permissionMode;
@@ -189,12 +441,12 @@ var ProcessProvider = class {
189
441
  entry.permissionMode = newMode;
190
442
  entry.pendingQuestion = savedPendingQuestion;
191
443
  proc.on("error", (err) => {
192
- console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} sendMessage \u8FDB\u7A0B\u9519\u8BEF:`, err.message);
444
+ console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
193
445
  this.activeSessions.delete(sessionId);
194
446
  const syntheticResult = {
195
447
  type: "result",
196
448
  subtype: "error",
197
- result: `\u6D88\u606F\u53D1\u9001\u5931\u8D25: ${err.message}`,
449
+ result: `Failed to send message: ${err.message}`,
198
450
  session_id: sessionId,
199
451
  duration_ms: 0,
200
452
  is_error: true,
@@ -285,16 +537,16 @@ var ProcessProvider = class {
285
537
  parent_tool_use_id: null
286
538
  });
287
539
  if (!proc.stdin || proc.stdin.destroyed) {
288
- console.error(`[ProcessProvider] stdin \u4E0D\u53EF\u7528\uFF0C\u6D88\u606F\u4E22\u5931`);
540
+ console.error(`[ProcessProvider] stdin unavailable, message lost`);
289
541
  if (sessionId) {
290
- this.emitWriteError(sessionId, "\u8FDB\u7A0B stdin \u5DF2\u5173\u95ED\uFF0C\u6D88\u606F\u672A\u9001\u8FBE");
542
+ this.emitWriteError(sessionId, "Process stdin closed, message not delivered");
291
543
  }
292
544
  return;
293
545
  }
294
546
  proc.stdin.write(payload + "\n", (err) => {
295
547
  if (err && sessionId) {
296
- console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} stdin \u5199\u5165\u5931\u8D25:`, err.message);
297
- this.emitWriteError(sessionId, `\u6D88\u606F\u53D1\u9001\u5931\u8D25: ${err.message}`);
548
+ console.error(`[ProcessProvider] Session ${sessionId} stdin write failed:`, err.message);
549
+ this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
298
550
  }
299
551
  });
300
552
  }
@@ -318,7 +570,7 @@ var ProcessProvider = class {
318
570
  */
319
571
  attachStdoutListener(sessionId, proc) {
320
572
  if (!proc.stdout) {
321
- console.warn(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: stdout \u4E0D\u53EF\u7528`);
573
+ console.warn(`[ProcessProvider] Session ${sessionId}: stdout unavailable`);
322
574
  return;
323
575
  }
324
576
  const rl = (0, import_readline.createInterface)({
@@ -356,7 +608,7 @@ var ProcessProvider = class {
356
608
  this.emitter.emit(this.getEventName(sessionId), event);
357
609
  } else {
358
610
  console.warn(
359
- `[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u65E0\u6CD5\u89E3\u6790\u884C: ${trimmed.substring(0, 100)}`
611
+ `[ProcessProvider] Session ${sessionId}: failed to parse line: ${trimmed.substring(0, 100)}`
360
612
  );
361
613
  }
362
614
  });
@@ -369,7 +621,7 @@ var ProcessProvider = class {
369
621
  proc.stderr.on("data", (data) => {
370
622
  const text = data.toString().trim();
371
623
  if (text) {
372
- console.error(`[ProcessProvider] \u4F1A\u8BDD ${sessionId} stderr: ${text}`);
624
+ console.error(`[ProcessProvider] Session ${sessionId} stderr: ${text}`);
373
625
  }
374
626
  });
375
627
  }
@@ -400,7 +652,7 @@ var ProcessProvider = class {
400
652
  entry.session.status = isNormal ? "idle" : "error";
401
653
  if (!isNormal) {
402
654
  console.error(
403
- `[ProcessProvider] \u4F1A\u8BDD ${sessionId}: \u8FDB\u7A0B\u5F02\u5E38\u9000\u51FA code=${code} signal=${signal}`
655
+ `[ProcessProvider] Session ${sessionId}: process exited abnormally code=${code} signal=${signal}`
404
656
  );
405
657
  }
406
658
  const syntheticResult = {
@@ -408,7 +660,7 @@ var ProcessProvider = class {
408
660
  subtype: isNormal ? "success" : "error",
409
661
  session_id: sessionId,
410
662
  is_error: !isNormal,
411
- result: isNormal ? "" : `\u8FDB\u7A0B\u9000\u51FA code=${code} signal=${signal}`,
663
+ result: isNormal ? "" : `Process exited code=${code} signal=${signal}`,
412
664
  duration_ms: 0,
413
665
  num_turns: 0
414
666
  };
@@ -456,7 +708,7 @@ var ProcessProvider = class {
456
708
  * 使用 --output-format text 做一次性调用,返回纯文本结果。
457
709
  */
458
710
  async generateSuggestion(context) {
459
- 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
711
+ const prompt = `You are an AI coding assistant. Based on the following Claude Code conversation context, suggest the most valuable next instruction for the user (give the instruction directly, no explanation, no quotes):
460
712
 
461
713
  ${context}`;
462
714
  return new Promise((resolve, reject) => {
@@ -476,7 +728,7 @@ ${context}`;
476
728
  if (code === 0) {
477
729
  resolve(output.trim());
478
730
  } else {
479
- reject(new Error(`generateSuggestion \u8FDB\u7A0B\u9000\u51FA\u7801: ${code}`));
731
+ reject(new Error(`generateSuggestion process exit code: ${code}`));
480
732
  }
481
733
  });
482
734
  proc.once("error", reject);
@@ -491,10 +743,10 @@ ${context}`;
491
743
  async answerQuestion(sessionId, toolUseId, answer) {
492
744
  const entry = this.activeSessions.get(sessionId);
493
745
  if (!entry) {
494
- throw new Error(`\u4F1A\u8BDD ${sessionId} \u4E0D\u5B58\u5728`);
746
+ throw new Error(`Session ${sessionId} not found`);
495
747
  }
496
748
  if (!entry.process.stdin || entry.process.stdin.destroyed) {
497
- throw new Error(`\u4F1A\u8BDD ${sessionId} stdin \u4E0D\u53EF\u7528`);
749
+ throw new Error(`Session ${sessionId} stdin unavailable`);
498
750
  }
499
751
  const toolResult = JSON.stringify({
500
752
  type: "tool_result",
@@ -507,7 +759,7 @@ ${context}`;
507
759
  else resolve();
508
760
  });
509
761
  });
510
- console.log(`[ProcessProvider] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u56DE\u7B54 (toolUseId=${toolUseId})`);
762
+ console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
511
763
  }
512
764
  /**
513
765
  * 订阅指定会话的 AskUserQuestion 事件
@@ -546,7 +798,7 @@ var SessionManager = class {
546
798
  unsubscribeMap = /* @__PURE__ */ new Map();
547
799
  /** 每个会话的事件缓冲区(用于新订阅者重放)*/
548
800
  sessionEventBuffers = /* @__PURE__ */ new Map();
549
- /** AskUserQuestion 问题映射:requestId → resolve 回调 */
801
+ /** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
550
802
  pendingQuestions = /* @__PURE__ */ new Map();
551
803
  /**
552
804
  * 会话状态缓存(用于追踪 status 变化,检测 oldStatus !== newStatus 时广播)
@@ -561,6 +813,10 @@ var SessionManager = class {
561
813
  runningStartedAt = /* @__PURE__ */ new Map();
562
814
  /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
563
815
  pendingAssistantEvents = /* @__PURE__ */ new Map();
816
+ /** 标记哪些会话的缓冲区曾被截断(溢出过 BUFFER_MAX) */
817
+ bufferTruncated = /* @__PURE__ */ new Set();
818
+ /** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
819
+ sessionProjectPaths = /* @__PURE__ */ new Map();
564
820
  constructor(provider) {
565
821
  this.provider = provider;
566
822
  }
@@ -585,9 +841,10 @@ var SessionManager = class {
585
841
  images
586
842
  });
587
843
  this.lastBroadcastStatus.set(session.id, session.status);
844
+ this.sessionProjectPaths.set(session.id, projectPath);
588
845
  this.unsubscribeSession(session.id);
589
846
  this.subscribeToSession(session.id);
590
- console.log(`[SessionManager] \u4F1A\u8BDD\u5DF2\u521B\u5EFA: ${session.id} (\u9879\u76EE: ${projectPath})`);
847
+ console.log(`[SessionManager] Session created: ${session.id} (project: ${projectPath})`);
591
848
  return session;
592
849
  }
593
850
  /**
@@ -596,7 +853,7 @@ var SessionManager = class {
596
853
  async sendMessage(sessionId, message, permissionMode, images) {
597
854
  await this.provider.sendMessage(sessionId, message, permissionMode, images);
598
855
  this.updateSessionStatus(sessionId, "running");
599
- console.log(`[SessionManager] \u6D88\u606F\u5DF2\u53D1\u9001\u5230\u4F1A\u8BDD: ${sessionId}`);
856
+ console.log(`[SessionManager] Message sent to session: ${sessionId}`);
600
857
  }
601
858
  /**
602
859
  * 终止会话
@@ -606,6 +863,8 @@ var SessionManager = class {
606
863
  this.clearPendingQuestions(sessionId);
607
864
  this.lastBroadcastStatus.delete(sessionId);
608
865
  this.sessionEventBuffers.delete(sessionId);
866
+ this.bufferTruncated.delete(sessionId);
867
+ this.sessionProjectPaths.delete(sessionId);
609
868
  this.sessionStats.delete(sessionId);
610
869
  const pending = this.pendingAssistantEvents.get(sessionId);
611
870
  if (pending) {
@@ -613,7 +872,7 @@ var SessionManager = class {
613
872
  this.pendingAssistantEvents.delete(sessionId);
614
873
  }
615
874
  await this.provider.killSession(sessionId);
616
- console.log(`[SessionManager] \u4F1A\u8BDD\u5DF2\u7EC8\u6B62: ${sessionId}`);
875
+ console.log(`[SessionManager] Session killed: ${sessionId}`);
617
876
  }
618
877
  /**
619
878
  * 获取会话的缓冲事件(用于新订阅者重放)
@@ -621,19 +880,71 @@ var SessionManager = class {
621
880
  getSessionEvents(sessionId) {
622
881
  return this.sessionEventBuffers.get(sessionId) ?? [];
623
882
  }
883
+ /**
884
+ * 检查会话的缓冲区是否曾被截断(溢出过 BUFFER_MAX)
885
+ */
886
+ isBufferTruncated(sessionId) {
887
+ return this.bufferTruncated.has(sessionId);
888
+ }
889
+ /**
890
+ * 获取会话的项目路径(用于截断时从 JSONL 补全历史)
891
+ */
892
+ getSessionProjectPath(sessionId) {
893
+ return this.sessionProjectPaths.get(sessionId);
894
+ }
624
895
  /**
625
896
  * 处理 AskUserQuestion 回答(从手机端传来)
626
897
  */
627
898
  handleQuestionResponse(requestId, answer) {
628
899
  const pending = this.pendingQuestions.get(requestId);
629
900
  if (!pending) {
630
- console.warn(`[SessionManager] \u672A\u627E\u5230\u95EE\u9898\u8BF7\u6C42: ${requestId}`);
901
+ console.warn(`[SessionManager] Question request not found: ${requestId}`);
631
902
  return;
632
903
  }
633
904
  this.pendingQuestions.delete(requestId);
634
905
  this.updateSessionStatus(pending.sessionId, "running");
635
906
  pending.resolve(answer);
636
- console.log(`[SessionManager] \u95EE\u9898\u5DF2\u56DE\u7B54: ${requestId}`);
907
+ console.log(`[SessionManager] Question answered: ${requestId}`);
908
+ }
909
+ /**
910
+ * 获取指定会话的所有待回答问题(用于重连时恢复)
911
+ */
912
+ getPendingQuestionsForSession(sessionId) {
913
+ const result = [];
914
+ for (const [requestId, pending] of this.pendingQuestions) {
915
+ if (pending.sessionId === sessionId) {
916
+ result.push({
917
+ id: requestId,
918
+ sessionId,
919
+ toolUseId: pending.toolUseId,
920
+ question: pending.question,
921
+ options: pending.options,
922
+ createdAt: pending.createdAt
923
+ });
924
+ }
925
+ }
926
+ return result;
927
+ }
928
+ /** 检查某个问题是否仍在等待回答 */
929
+ isQuestionPending(requestId) {
930
+ return this.pendingQuestions.has(requestId);
931
+ }
932
+ /**
933
+ * 获取所有待回答问题(用于客户端重连时恢复状态)
934
+ */
935
+ getAllPendingQuestions() {
936
+ const result = [];
937
+ for (const [requestId, pending] of this.pendingQuestions) {
938
+ result.push({
939
+ id: requestId,
940
+ sessionId: pending.sessionId,
941
+ toolUseId: pending.toolUseId,
942
+ question: pending.question,
943
+ options: pending.options,
944
+ createdAt: pending.createdAt
945
+ });
946
+ }
947
+ return result;
637
948
  }
638
949
  /**
639
950
  * 获取所有活跃会话(含服务器端统计)
@@ -667,6 +978,8 @@ var SessionManager = class {
667
978
  }
668
979
  this.unsubscribeMap.clear();
669
980
  this.sessionEventBuffers.clear();
981
+ this.bufferTruncated.clear();
982
+ this.sessionProjectPaths.clear();
670
983
  this.sessionStats.clear();
671
984
  for (const [, pending] of this.pendingAssistantEvents) {
672
985
  clearTimeout(pending.timer);
@@ -675,7 +988,7 @@ var SessionManager = class {
675
988
  this.pendingQuestions.clear();
676
989
  this.lastBroadcastStatus.clear();
677
990
  this.eventCallbacks.length = 0;
678
- console.log("[SessionManager] \u5DF2\u9500\u6BC1");
991
+ console.log("[SessionManager] Destroyed");
679
992
  }
680
993
  // ============================================
681
994
  // 内部方法
@@ -720,6 +1033,7 @@ var SessionManager = class {
720
1033
  buffer.push(event);
721
1034
  if (buffer.length > BUFFER_MAX) {
722
1035
  buffer.splice(0, buffer.length - BUFFER_MAX);
1036
+ this.bufferTruncated.add(sessionId);
723
1037
  }
724
1038
  this.sessionEventBuffers.set(sessionId, buffer);
725
1039
  if (event.type === "assistant" && Array.isArray(event.message?.content)) {
@@ -828,7 +1142,7 @@ var SessionManager = class {
828
1142
  status: newStatus,
829
1143
  stats
830
1144
  });
831
- console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId} \u72B6\u6001\u53D8\u5316: ${lastStatus ?? "(\u65E0)"} \u2192 ${newStatus}`);
1145
+ console.log(`[SessionManager] Session ${sessionId} status change: ${lastStatus ?? "(none)"} \u2192 ${newStatus}`);
832
1146
  }
833
1147
  }
834
1148
  /** 获取会话统计(含 runningStartedAt) */
@@ -857,7 +1171,7 @@ var SessionManager = class {
857
1171
  createdAt: Date.now()
858
1172
  };
859
1173
  this.emit({ type: "question_request", request: updatedRequest });
860
- console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u66F4\u65B0 (requestId=${existingRequestId})`);
1174
+ console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
861
1175
  return;
862
1176
  }
863
1177
  const requestId = (0, import_uuid2.v4)();
@@ -872,16 +1186,16 @@ var SessionManager = class {
872
1186
  this.updateSessionStatus(sessionId, "waiting_question");
873
1187
  this.emit({ type: "question_request", request });
874
1188
  const answerPromise = new Promise((resolve) => {
875
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, resolve });
1189
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
876
1190
  });
877
1191
  answerPromise.then(async (answer) => {
878
1192
  try {
879
1193
  await this.provider.answerQuestion(sessionId, toolUseId, answer);
880
1194
  } catch (err) {
881
- console.error(`[SessionManager] answerQuestion \u5931\u8D25 (${sessionId}):`, err);
1195
+ console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
882
1196
  }
883
1197
  }).catch((err) => console.error("[SessionManager] answerPromise rejected:", err));
884
- console.log(`[SessionManager] \u4F1A\u8BDD ${sessionId}: AskUserQuestion \u5DF2\u63A8\u9001 (requestId=${requestId})`);
1198
+ console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion pushed (requestId=${requestId})`);
885
1199
  }
886
1200
  /**
887
1201
  * 清除指定会话的所有待回答问题
@@ -905,7 +1219,7 @@ var SessionManager = class {
905
1219
  try {
906
1220
  callback(event);
907
1221
  } catch (err) {
908
- console.error("[SessionManager] \u4E8B\u4EF6\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1222
+ console.error("[SessionManager] Event callback error:", err);
909
1223
  }
910
1224
  }
911
1225
  }
@@ -951,13 +1265,13 @@ var SessionFileWatcher = class {
951
1265
  };
952
1266
  watcher.on("change", () => {
953
1267
  this.readNewLines(sessionId).catch((err) => {
954
- console.error(`[SessionFileWatcher] \u8BFB\u53D6\u5F02\u5E38 ${sessionId}:`, err);
1268
+ console.error(`[SessionFileWatcher] ${t("watcher.readError", { sessionId })}:`, err);
955
1269
  });
956
1270
  this.resetIdleTimer(sessionId);
957
1271
  });
958
1272
  this.watchers.set(sessionId, entry);
959
1273
  this.resetIdleTimer(sessionId);
960
- console.log(`[SessionFileWatcher] \u5F00\u59CB\u76D1\u542C: ${sessionId} (offset=${byteOffset})`);
1274
+ console.log(`[SessionFileWatcher] ${t("watcher.startWatching")}: ${sessionId} (offset=${byteOffset})`);
961
1275
  }
962
1276
  /** 停止监听指定会话 */
963
1277
  unwatch(sessionId) {
@@ -966,7 +1280,7 @@ var SessionFileWatcher = class {
966
1280
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
967
1281
  void entry.watcher.close();
968
1282
  this.watchers.delete(sessionId);
969
- console.log(`[SessionFileWatcher] \u505C\u6B62\u76D1\u542C: ${sessionId}`);
1283
+ console.log(`[SessionFileWatcher] ${t("watcher.stopWatching")}: ${sessionId}`);
970
1284
  }
971
1285
  /** 停止所有监听(服务关闭时调用) */
972
1286
  destroy() {
@@ -982,7 +1296,7 @@ var SessionFileWatcher = class {
982
1296
  if (!entry) return;
983
1297
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
984
1298
  entry.idleTimer = setTimeout(() => {
985
- console.log(`[SessionFileWatcher] \u7A7A\u95F2\u8D85\u65F6\uFF0C\u505C\u6B62\u76D1\u542C: ${sessionId}`);
1299
+ console.log(`[SessionFileWatcher] Idle timeout, stop watching: ${sessionId}`);
986
1300
  this.unwatch(sessionId);
987
1301
  }, this.IDLE_TIMEOUT_MS);
988
1302
  }
@@ -1089,6 +1403,8 @@ var WsBridge = class _WsBridge {
1089
1403
  lastPongMap = /* @__PURE__ */ new Map();
1090
1404
  /** 每个连接当前正在查看的会话 ID */
1091
1405
  viewingSessions = /* @__PURE__ */ new Map();
1406
+ /** 每个连接的消息处理队列(串行化 async handler,防止 create_session/subscribe 竞态) */
1407
+ messageQueues = /* @__PURE__ */ new Map();
1092
1408
  constructor(options) {
1093
1409
  this.token = options.token;
1094
1410
  this.wss = new import_ws.WebSocketServer({
@@ -1104,7 +1420,7 @@ var WsBridge = class _WsBridge {
1104
1420
  });
1105
1421
  this.wss.on("connection", (ws) => this.handleConnection(ws));
1106
1422
  this.startHeartbeat();
1107
- console.log(`[WsBridge] WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1423
+ console.log(`[WsBridge] ${t("ws.started", { port: options.port })}`);
1108
1424
  }
1109
1425
  /**
1110
1426
  * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
@@ -1114,7 +1430,7 @@ var WsBridge = class _WsBridge {
1114
1430
  return new Promise((resolve, reject) => {
1115
1431
  const bridge = new _WsBridge(options);
1116
1432
  bridge.wss.once("listening", () => {
1117
- bridge.wss.on("error", (err) => console.error("[WsBridge] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1433
+ bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
1118
1434
  resolve(bridge);
1119
1435
  });
1120
1436
  bridge.wss.once("error", reject);
@@ -1183,7 +1499,7 @@ var WsBridge = class _WsBridge {
1183
1499
  if (err) {
1184
1500
  reject(err);
1185
1501
  } else {
1186
- console.log("[WsBridge] WebSocket \u670D\u52A1\u5DF2\u5173\u95ED");
1502
+ console.log("[WsBridge] WebSocket server closed");
1187
1503
  resolve();
1188
1504
  }
1189
1505
  });
@@ -1206,12 +1522,12 @@ var WsBridge = class _WsBridge {
1206
1522
  /** 处理新的 WebSocket 连接 */
1207
1523
  handleConnection(ws) {
1208
1524
  this.lastPongMap.set(ws, Date.now());
1209
- console.log(`[WsBridge] \u65B0\u5BA2\u6237\u7AEF\u8FDE\u63A5\uFF0C\u5F53\u524D\u8FDE\u63A5\u6570: ${this.getConnectionCount()}`);
1525
+ console.log(`[WsBridge] New client connected, connections: ${this.getConnectionCount()}`);
1210
1526
  for (const callback of this.connectionCallbacks) {
1211
1527
  try {
1212
1528
  callback(ws);
1213
1529
  } catch (err) {
1214
- console.error("[WsBridge] \u8FDE\u63A5\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1530
+ console.error("[WsBridge] Connection callback error:", err);
1215
1531
  }
1216
1532
  }
1217
1533
  ws.on("pong", () => {
@@ -1222,10 +1538,10 @@ var WsBridge = class _WsBridge {
1222
1538
  const event = JSON.parse(raw.toString());
1223
1539
  this.dispatchClientEvent(event, ws);
1224
1540
  } catch (err) {
1225
- console.error("[WsBridge] \u6D88\u606F\u89E3\u6790\u5931\u8D25:", err);
1541
+ console.error("[WsBridge] Message parse error:", err);
1226
1542
  this.send(ws, {
1227
1543
  type: "error",
1228
- message: "\u6D88\u606F\u683C\u5F0F\u65E0\u6548",
1544
+ message: "Invalid message format",
1229
1545
  code: "INVALID_MESSAGE"
1230
1546
  });
1231
1547
  }
@@ -1233,30 +1549,40 @@ var WsBridge = class _WsBridge {
1233
1549
  ws.on("close", () => {
1234
1550
  this.lastPongMap.delete(ws);
1235
1551
  this.viewingSessions.delete(ws);
1552
+ this.messageQueues.delete(ws);
1236
1553
  setTimeout(() => {
1237
- console.log(`[WsBridge] \u5BA2\u6237\u7AEF\u65AD\u5F00\uFF0C\u5F53\u524D\u8FDE\u63A5\u6570: ${this.getConnectionCount()}`);
1554
+ console.log(`[WsBridge] Client disconnected, connections: ${this.getConnectionCount()}`);
1238
1555
  for (const cb of this.disconnectCallbacks) {
1239
1556
  try {
1240
1557
  cb();
1241
1558
  } catch (err) {
1242
- console.error("[WsBridge] \u65AD\u5F00\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1559
+ console.error("[WsBridge] Disconnect callback error:", err);
1243
1560
  }
1244
1561
  }
1245
1562
  }, 0);
1246
1563
  });
1247
1564
  ws.on("error", (err) => {
1248
- console.error("[WsBridge] \u8FDE\u63A5\u9519\u8BEF:", err.message);
1565
+ console.error("[WsBridge] Connection error:", err.message);
1249
1566
  });
1250
1567
  }
1251
- /** 分发客户端事件到所有注册的回调 */
1568
+ /**
1569
+ * 分发客户端事件到所有注册的回调
1570
+ *
1571
+ * 使用 per-connection 队列串行化处理,确保 async 回调(如 create_session)
1572
+ * 完成后才处理下一条消息(如 subscribe),避免竞态条件。
1573
+ */
1252
1574
  dispatchClientEvent(event, ws) {
1253
- for (const callback of this.clientEventCallbacks) {
1254
- try {
1255
- callback(event, ws);
1256
- } catch (err) {
1257
- console.error("[WsBridge] \u4E8B\u4EF6\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1575
+ const prev = this.messageQueues.get(ws) ?? Promise.resolve();
1576
+ const next = prev.then(async () => {
1577
+ for (const callback of this.clientEventCallbacks) {
1578
+ try {
1579
+ await callback(event, ws);
1580
+ } catch (err) {
1581
+ console.error("[WsBridge] Event callback error:", err);
1582
+ }
1258
1583
  }
1259
- }
1584
+ });
1585
+ this.messageQueues.set(ws, next);
1260
1586
  }
1261
1587
  /** 启动心跳机制 */
1262
1588
  startHeartbeat() {
@@ -1266,7 +1592,7 @@ var WsBridge = class _WsBridge {
1266
1592
  for (const ws of this.wss.clients) {
1267
1593
  const lastPong = this.lastPongMap.get(ws) ?? 0;
1268
1594
  if (now - lastPong > 45e3) {
1269
- console.log("[WsBridge] \u68C0\u6D4B\u5230\u6B7B\u8FDE\u63A5\uFF0C\u4E3B\u52A8\u65AD\u5F00");
1595
+ console.log("[WsBridge] Dead connection detected, terminating");
1270
1596
  ws.terminate();
1271
1597
  continue;
1272
1598
  }
@@ -1306,7 +1632,7 @@ var ApprovalProxy = class _ApprovalProxy {
1306
1632
  this.handleRequest(req, res);
1307
1633
  });
1308
1634
  this.server.listen(options.port, () => {
1309
- console.log(`[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1635
+ console.log(`[ApprovalProxy] ${t("approval.httpStarted", { port: options.port })}`);
1310
1636
  });
1311
1637
  }
1312
1638
  /**
@@ -1316,7 +1642,7 @@ var ApprovalProxy = class _ApprovalProxy {
1316
1642
  return new Promise((resolve, reject) => {
1317
1643
  const proxy = new _ApprovalProxy(options);
1318
1644
  proxy.server.once("listening", () => {
1319
- proxy.server.on("error", (err) => console.error("[ApprovalProxy] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1645
+ proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
1320
1646
  resolve(proxy);
1321
1647
  });
1322
1648
  proxy.server.once("error", reject);
@@ -1336,7 +1662,7 @@ var ApprovalProxy = class _ApprovalProxy {
1336
1662
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1337
1663
  setYoloMode(sessionId, enabled) {
1338
1664
  this.yoloSessions.set(sessionId, enabled);
1339
- console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F ${enabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u5173\u95ED"}: ${sessionId}`);
1665
+ console.log(`[ApprovalProxy] ${t("approval.yoloMode", { status: enabled ? t("approval.yoloEnabled") : t("approval.yoloDisabled") })}: ${sessionId}`);
1340
1666
  }
1341
1667
  /** 检查会话是否处于 YOLO 模式 */
1342
1668
  isYoloMode(sessionId) {
@@ -1351,13 +1677,13 @@ var ApprovalProxy = class _ApprovalProxy {
1351
1677
  resolveApproval(requestId, decision) {
1352
1678
  const pending = this.pendingApprovals.get(requestId);
1353
1679
  if (!pending) {
1354
- console.warn(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6`);
1680
+ console.warn(`[ApprovalProxy] ${t("approval.requestNotFound", { id: requestId })}`);
1355
1681
  return false;
1356
1682
  }
1357
1683
  clearTimeout(pending.timer);
1358
1684
  pending.resolve(decision);
1359
1685
  this.pendingApprovals.delete(requestId);
1360
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u5904\u7406: ${decision.decision}`);
1686
+ console.log(`[ApprovalProxy] ${t("approval.requestProcessed", { id: requestId })}: ${decision.decision}`);
1361
1687
  return true;
1362
1688
  }
1363
1689
  /** 获取当前待处理的审批数量 */
@@ -1424,11 +1750,11 @@ var ApprovalProxy = class _ApprovalProxy {
1424
1750
  allow.push(entry);
1425
1751
  import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1426
1752
  const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1427
- console.log(`[ApprovalProxy] \u5DF2\u5C06 ${entry} \u5199\u5165 ${label}`);
1753
+ console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
1428
1754
  }
1429
1755
  this.alwaysAllowedTools.add(toolName);
1430
1756
  } catch (err) {
1431
- console.error("[ApprovalProxy] \u5199\u5165 settings.json \u5931\u8D25:", err);
1757
+ console.error(`[ApprovalProxy] ${t("approval.settingsWriteFailed")}:`, err);
1432
1758
  }
1433
1759
  }
1434
1760
  /** 获取指定会话的所有 pending approval requests(用于 subscribe 重发) */
@@ -1441,6 +1767,10 @@ var ApprovalProxy = class _ApprovalProxy {
1441
1767
  }
1442
1768
  return result;
1443
1769
  }
1770
+ /** 获取所有 pending approval requests(用于客户端重连时恢复状态) */
1771
+ getAllPendingRequests() {
1772
+ return Array.from(this.pendingApprovals.values()).map(({ request }) => request);
1773
+ }
1444
1774
  /**
1445
1775
  * 批量允许所有待处理的审批请求(手机端断线时调用)
1446
1776
  */
@@ -1450,7 +1780,7 @@ var ApprovalProxy = class _ApprovalProxy {
1450
1780
  clearTimeout(pending.timer);
1451
1781
  pending.resolve({ decision: "allow" });
1452
1782
  this.pendingApprovals.delete(requestId);
1453
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u81EA\u52A8\u5141\u8BB8${reason ? `\uFF08${reason}\uFF09` : ""}`);
1783
+ console.log(`[ApprovalProxy] ${t("approval.autoAllowed", { id: requestId, reason: reason ? `\uFF08${reason}\uFF09` : "" })}`);
1454
1784
  }
1455
1785
  }
1456
1786
  /** 优雅关闭 HTTP 服务 */
@@ -1459,14 +1789,14 @@ var ApprovalProxy = class _ApprovalProxy {
1459
1789
  const pendingEntries = Array.from(this.pendingApprovals.entries());
1460
1790
  for (const [, pending] of pendingEntries) {
1461
1791
  clearTimeout(pending.timer);
1462
- pending.resolve({ decision: "deny", reason: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED" });
1792
+ pending.resolve({ decision: "deny", reason: t("approval.serverClosed") });
1463
1793
  }
1464
1794
  this.pendingApprovals.clear();
1465
1795
  this.server.close((err) => {
1466
1796
  if (err) {
1467
1797
  reject(err);
1468
1798
  } else {
1469
- console.log("[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED");
1799
+ console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
1470
1800
  resolve();
1471
1801
  }
1472
1802
  });
@@ -1521,24 +1851,24 @@ var ApprovalProxy = class _ApprovalProxy {
1521
1851
  projectPath,
1522
1852
  toolName,
1523
1853
  toolInput,
1524
- description: String(payload.description ?? body.description ?? `${toolName} \u5DE5\u5177\u8C03\u7528\u8BF7\u6C42`),
1854
+ description: String(payload.description ?? body.description ?? `${toolName} tool call request`),
1525
1855
  createdAt: Date.now()
1526
1856
  };
1527
- console.log(`[ApprovalProxy] \u6536\u5230\u5BA1\u6279\u8BF7\u6C42: ${requestId} (${approvalRequest.toolName})`);
1857
+ console.log(`[ApprovalProxy] ${t("approval.received")}: ${requestId} (${approvalRequest.toolName})`);
1528
1858
  if (this.isToolAlwaysAllowed(approvalRequest.toolName, projectPath !== "unknown" ? projectPath : void 0)) {
1529
- console.log(`[ApprovalProxy] ${approvalRequest.toolName} \u5DF2\u88AB\u59CB\u7EC8\u5141\u8BB8\uFF0C\u76F4\u63A5\u653E\u884C\uFF08\u4E0D\u901A\u77E5\uFF09`);
1859
+ console.log(`[ApprovalProxy] ${t("approval.alwaysAllowPassThrough", { tool: approvalRequest.toolName })}`);
1530
1860
  this.sendJson(res, 200, { decision: "allow" });
1531
1861
  return;
1532
1862
  }
1533
1863
  if (this.yoloSessions.get(approvalRequest.sessionId)) {
1534
- console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C: ${approvalRequest.toolName}`);
1864
+ console.log(`[ApprovalProxy] ${t("approval.yoloAutoAllow")}: ${approvalRequest.toolName}`);
1535
1865
  this.sendJson(res, 200, { decision: "allow" });
1536
1866
  return;
1537
1867
  }
1538
1868
  this.notifyApprovalRequest(approvalRequest);
1539
1869
  const decision = await new Promise((resolve) => {
1540
1870
  const timer = setTimeout(() => {
1541
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8`);
1871
+ console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
1542
1872
  this.pendingApprovals.delete(requestId);
1543
1873
  resolve({ decision: "allow" });
1544
1874
  }, 325e3);
@@ -1546,8 +1876,8 @@ var ApprovalProxy = class _ApprovalProxy {
1546
1876
  });
1547
1877
  this.sendJson(res, 200, decision);
1548
1878
  } catch (err) {
1549
- console.error("[ApprovalProxy] \u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25:", err);
1550
- this.sendJson(res, 200, { decision: "deny", reason: "\u670D\u52A1\u5668\u5904\u7406\u8BF7\u6C42\u5931\u8D25" });
1879
+ console.error(`[ApprovalProxy] ${t("approval.processingFailed")}:`, err);
1880
+ this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
1551
1881
  }
1552
1882
  }
1553
1883
  /** 健康检查端点 */
@@ -1564,7 +1894,7 @@ var ApprovalProxy = class _ApprovalProxy {
1564
1894
  const remoteAddress = req.socket.remoteAddress;
1565
1895
  const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
1566
1896
  if (!isLocal) {
1567
- this.sendJson(res, 403, { error: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE" });
1897
+ this.sendJson(res, 403, { error: t("approval.forbidden") });
1568
1898
  return;
1569
1899
  }
1570
1900
  this.sendJson(res, 200, { token: this.token });
@@ -1575,7 +1905,7 @@ var ApprovalProxy = class _ApprovalProxy {
1575
1905
  try {
1576
1906
  callback(request);
1577
1907
  } catch (err) {
1578
- console.error("[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1908
+ console.error("[ApprovalProxy] Approval request callback error:", err);
1579
1909
  }
1580
1910
  }
1581
1911
  }
@@ -1592,7 +1922,7 @@ var ApprovalProxy = class _ApprovalProxy {
1592
1922
  if (totalSize > MAX_BODY_SIZE) {
1593
1923
  destroyed = true;
1594
1924
  req.destroy();
1595
- return reject(new Error("\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09"));
1925
+ return reject(new Error(t("approval.bodyTooLarge")));
1596
1926
  }
1597
1927
  chunks.push(chunk);
1598
1928
  });
@@ -1602,7 +1932,7 @@ var ApprovalProxy = class _ApprovalProxy {
1602
1932
  const parsed = JSON.parse(raw);
1603
1933
  resolve(parsed);
1604
1934
  } catch {
1605
- reject(new Error("\u65E0\u6548\u7684 JSON body"));
1935
+ reject(new Error(t("approval.invalidJson")));
1606
1936
  }
1607
1937
  });
1608
1938
  req.on("error", (err) => {
@@ -1629,17 +1959,19 @@ var MdnsService = class {
1629
1959
  wsPort;
1630
1960
  httpPort;
1631
1961
  version;
1962
+ token;
1632
1963
  constructor(options) {
1633
1964
  this.wsPort = options.wsPort;
1634
1965
  this.httpPort = options.httpPort;
1635
1966
  this.version = options.version ?? "0.1.0";
1967
+ this.token = options.token ?? "";
1636
1968
  }
1637
1969
  /**
1638
1970
  * 启动 mDNS 广播
1639
1971
  */
1640
1972
  start() {
1641
1973
  if (this.bonjour) {
1642
- console.warn("[MdnsService] \u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D");
1974
+ console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
1643
1975
  return;
1644
1976
  }
1645
1977
  this.bonjour = new import_bonjour_service.default();
@@ -1649,10 +1981,11 @@ var MdnsService = class {
1649
1981
  port: this.wsPort,
1650
1982
  txt: {
1651
1983
  version: this.version,
1652
- httpPort: String(this.httpPort)
1984
+ httpPort: String(this.httpPort),
1985
+ token: this.token
1653
1986
  }
1654
1987
  });
1655
- console.log(`[MdnsService] mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 ${this.wsPort}`);
1988
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
1656
1989
  }
1657
1990
  /**
1658
1991
  * 停止 mDNS 广播
@@ -1660,7 +1993,7 @@ var MdnsService = class {
1660
1993
  stop() {
1661
1994
  if (this.service) {
1662
1995
  this.service.stop?.(() => {
1663
- console.log("[MdnsService] \u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62");
1996
+ console.log(`[MdnsService] ${t("mdns.stopped")}`);
1664
1997
  });
1665
1998
  this.service = null;
1666
1999
  }
@@ -1668,7 +2001,7 @@ var MdnsService = class {
1668
2001
  this.bonjour.destroy();
1669
2002
  this.bonjour = null;
1670
2003
  }
1671
- console.log("[MdnsService] mDNS \u670D\u52A1\u5DF2\u5173\u95ED");
2004
+ console.log(`[MdnsService] ${t("mdns.closed")}`);
1672
2005
  }
1673
2006
  };
1674
2007
 
@@ -1750,7 +2083,7 @@ var HookInstaller = class {
1750
2083
  await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
1751
2084
  await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
1752
2085
  await this.addHookToSettings();
1753
- console.log("[HookInstaller] Hook \u5B89\u88C5\u5B8C\u6210");
2086
+ console.log("[HookInstaller] Hook installation complete");
1754
2087
  }
1755
2088
  /**
1756
2089
  * 卸载 hook
@@ -1760,7 +2093,7 @@ var HookInstaller = class {
1760
2093
  */
1761
2094
  async uninstall() {
1762
2095
  await this.removeHookFromSettings();
1763
- console.log("[HookInstaller] Hook \u5DF2\u5378\u8F7D");
2096
+ console.log("[HookInstaller] Hook uninstalled");
1764
2097
  }
1765
2098
  /**
1766
2099
  * 检查 hook 是否已安装
@@ -1818,7 +2151,7 @@ var HookInstaller = class {
1818
2151
  if (changed) {
1819
2152
  await this.writeClaudeSettings(settings);
1820
2153
  } else {
1821
- console.log("[HookInstaller] Hook \u914D\u7F6E\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7");
2154
+ console.log("[HookInstaller] Hook config already exists, skipping");
1822
2155
  }
1823
2156
  }
1824
2157
  /**
@@ -1903,6 +2236,8 @@ var NotificationService = class {
1903
2236
  yoloModeState = /* @__PURE__ */ new Map();
1904
2237
  /** 每个会话的最新 assistant 文本消息(用于通知正文预览) */
1905
2238
  latestAssistantText = /* @__PURE__ */ new Map();
2239
+ /** 获取全局待审批总数的回调(跨所有会话) */
2240
+ globalPendingCountProvider = null;
1906
2241
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
1907
2242
  addChannel(id, channel, enabled = true) {
1908
2243
  this.channelMap.set(id, { channel, enabled });
@@ -1936,6 +2271,14 @@ var NotificationService = class {
1936
2271
  removeActivityPushToken(sessionId) {
1937
2272
  this.activityPushChannel?.removeToken(sessionId);
1938
2273
  }
2274
+ /** 设置全局待审批总数提供者 */
2275
+ setGlobalPendingCountProvider(provider) {
2276
+ this.globalPendingCountProvider = provider;
2277
+ }
2278
+ /** 获取全局待审批总数 */
2279
+ getGlobalPendingCount() {
2280
+ return this.globalPendingCountProvider?.() ?? 0;
2281
+ }
1939
2282
  /** 更新会话的 YOLO 模式状态 */
1940
2283
  setYoloMode(sessionId, enabled) {
1941
2284
  this.yoloModeState.set(sessionId, enabled);
@@ -1944,7 +2287,7 @@ var NotificationService = class {
1944
2287
  notifyApproval(request, pendingCount) {
1945
2288
  if (this.yoloModeState.get(request.sessionId)) return;
1946
2289
  const sessionTitle = this.getSessionTitle(request.sessionId);
1947
- const title = pendingCount > 1 ? `${sessionTitle} \u2014 ${pendingCount} \u9879\u5F85\u5BA1\u6279` : sessionTitle;
2290
+ const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
1948
2291
  const body = pendingCount > 1 ? `\u{1F527} \u6700\u65B0: ${request.toolName}: ${request.description}` : `\u{1F527} ${request.toolName}: ${request.description}`;
1949
2292
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
1950
2293
  const dangerLevel = this.getDangerLevel(request.toolName);
@@ -1954,7 +2297,7 @@ var NotificationService = class {
1954
2297
  {
1955
2298
  status: "waitingApproval",
1956
2299
  sessionTitle,
1957
- latestMessage: `${request.toolName}: ${request.description}`,
2300
+ latestMessage: "",
1958
2301
  approvalInfo: {
1959
2302
  requestId: request.id,
1960
2303
  toolName: request.toolName,
@@ -1972,8 +2315,8 @@ var NotificationService = class {
1972
2315
  this.notify({
1973
2316
  title,
1974
2317
  body,
1975
- sound: "Funk",
1976
- badge: pendingCount,
2318
+ sound: "default",
2319
+ badge: this.getGlobalPendingCount(),
1977
2320
  data: {
1978
2321
  type: "approval_request",
1979
2322
  sessionId: request.sessionId,
@@ -1981,6 +2324,37 @@ var NotificationService = class {
1981
2324
  }
1982
2325
  });
1983
2326
  }
2327
+ /** 直接触发提问通知(由 server.ts 在 question_request 事件时调用) */
2328
+ notifyQuestion(request) {
2329
+ const sessionTitle = this.getSessionTitle(request.sessionId);
2330
+ const body = `\u2753 ${request.question.slice(0, 80)}`;
2331
+ if (this.activityPushChannel?.hasToken(request.sessionId)) {
2332
+ const isYoloMode = this.getYoloMode(request.sessionId);
2333
+ this.activityPushChannel.updateActivityWithAlert(
2334
+ request.sessionId,
2335
+ {
2336
+ status: "waitingApproval",
2337
+ sessionTitle,
2338
+ latestMessage: request.question.slice(0, 80),
2339
+ isYoloMode,
2340
+ updatedAt: Date.now()
2341
+ },
2342
+ { title: sessionTitle, body }
2343
+ );
2344
+ return;
2345
+ }
2346
+ this.notify({
2347
+ title: sessionTitle,
2348
+ body,
2349
+ sound: "default",
2350
+ badge: this.getGlobalPendingCount(),
2351
+ data: {
2352
+ type: "question_request",
2353
+ sessionId: request.sessionId,
2354
+ requestId: request.id
2355
+ }
2356
+ });
2357
+ }
1984
2358
  /** 简单的工具危险等级判断 */
1985
2359
  getDangerLevel(toolName) {
1986
2360
  if (toolName === "Bash") return "danger";
@@ -2013,7 +2387,7 @@ var NotificationService = class {
2013
2387
  if (event.status === "idle") {
2014
2388
  const sessionTitle = this.getSessionTitle(event.sessionId);
2015
2389
  const latestMsg = this.latestAssistantText.get(event.sessionId);
2016
- const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : "\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B65\u6307\u4EE4";
2390
+ const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
2017
2391
  const isYoloMode = this.getYoloMode(event.sessionId);
2018
2392
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
2019
2393
  this.activityPushChannel.endActivity(event.sessionId, {
@@ -2027,14 +2401,15 @@ var NotificationService = class {
2027
2401
  this.notify({
2028
2402
  title: sessionTitle,
2029
2403
  body,
2030
- sound: "Glass",
2404
+ sound: "default",
2405
+ badge: this.getGlobalPendingCount(),
2031
2406
  data: { type: "task_complete", sessionId: event.sessionId }
2032
2407
  });
2033
2408
  }
2034
2409
  } else if (event.status === "error") {
2035
2410
  const sessionTitle = this.getSessionTitle(event.sessionId);
2036
2411
  const latestMsg = this.latestAssistantText.get(event.sessionId);
2037
- const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : "\u6267\u884C\u51FA\u9519\uFF0C\u8BF7\u67E5\u770B\u8BE6\u60C5";
2412
+ const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
2038
2413
  const isYoloMode = this.getYoloMode(event.sessionId);
2039
2414
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
2040
2415
  this.activityPushChannel.endActivity(event.sessionId, {
@@ -2048,7 +2423,8 @@ var NotificationService = class {
2048
2423
  this.notify({
2049
2424
  title: sessionTitle,
2050
2425
  body,
2051
- sound: "Basso",
2426
+ sound: "default",
2427
+ badge: this.getGlobalPendingCount(),
2052
2428
  data: { type: "task_error", sessionId: event.sessionId }
2053
2429
  });
2054
2430
  }
@@ -2061,7 +2437,7 @@ var NotificationService = class {
2061
2437
  for (const { channel, enabled } of this.channelMap.values()) {
2062
2438
  if (!enabled) continue;
2063
2439
  channel.send(payload).catch((err) => {
2064
- console.error("[NotificationService] \u901A\u77E5\u53D1\u9001\u5931\u8D25:", err);
2440
+ console.error("[NotificationService] Notification send failed:", err);
2065
2441
  });
2066
2442
  }
2067
2443
  }
@@ -2101,7 +2477,7 @@ var MacNotificationChannel = class {
2101
2477
  return new Promise((resolve) => {
2102
2478
  (0, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
2103
2479
  if (err) {
2104
- console.warn("[MacNotificationChannel] \u53D1\u9001\u901A\u77E5\u5931\u8D25:", err.message);
2480
+ console.warn("[MacNotificationChannel] Send notification failed:", err.message);
2105
2481
  }
2106
2482
  resolve();
2107
2483
  });
@@ -2120,19 +2496,19 @@ var ExpoNotificationChannel = class {
2120
2496
  }
2121
2497
  addToken(token) {
2122
2498
  this.tokens.add(token);
2123
- console.log(`[ExpoNotificationChannel] \u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2499
+ console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2124
2500
  }
2125
2501
  removeToken(token) {
2126
2502
  this.tokens.delete(token);
2127
2503
  this.soundPreferences.delete(token);
2128
- console.log(`[ExpoNotificationChannel] \u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2504
+ console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2129
2505
  }
2130
2506
  /** 更新某个 token 的音效偏好 */
2131
2507
  setSoundPreferences(prefs) {
2132
2508
  for (const token of this.tokens) {
2133
2509
  this.soundPreferences.set(token, prefs);
2134
2510
  }
2135
- console.log("[ExpoNotificationChannel] \u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D");
2511
+ console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
2136
2512
  }
2137
2513
  async send(payload) {
2138
2514
  if (this.tokens.size === 0) return;
@@ -2145,17 +2521,18 @@ var ExpoNotificationChannel = class {
2145
2521
  else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
2146
2522
  else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
2147
2523
  }
2524
+ const pushSound = sound === "none" ? null : sound;
2148
2525
  return {
2149
2526
  to,
2150
2527
  title: payload.title,
2151
2528
  body: payload.body,
2152
2529
  badge: payload.badge,
2153
- sound: sound === "none" ? null : sound,
2530
+ sound: pushSound,
2154
2531
  data: payload.data ?? {}
2155
2532
  };
2156
2533
  });
2157
2534
  try {
2158
- console.log("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\uFF0Ctokens:", Array.from(this.tokens));
2535
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2159
2536
  const res = await fetch(EXPO_PUSH_API, {
2160
2537
  method: "POST",
2161
2538
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -2163,20 +2540,20 @@ var ExpoNotificationChannel = class {
2163
2540
  });
2164
2541
  const body = await res.json();
2165
2542
  if (!res.ok) {
2166
- console.warn("[ExpoNotificationChannel] Expo Push API \u8FD4\u56DE\u9519\u8BEF:", res.status, JSON.stringify(body));
2543
+ console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiError")}`, res.status, JSON.stringify(body));
2167
2544
  } else {
2168
2545
  if (!Array.isArray(body?.data)) {
2169
- console.warn("[ExpoNotificationChannel] Expo Push API \u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF0C\u7F3A\u5C11 data \u6570\u7EC4:", JSON.stringify(body));
2546
+ console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
2170
2547
  return;
2171
2548
  }
2172
2549
  for (const ticket of body.data) {
2173
2550
  if (ticket.status === "error") {
2174
- console.error(`[ExpoNotificationChannel] \u63A8\u9001\u5931\u8D25: ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
2551
+ console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
2175
2552
  }
2176
2553
  }
2177
2554
  }
2178
2555
  } catch (err) {
2179
- console.warn("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\u5931\u8D25:", err);
2556
+ console.warn(`[ExpoNotificationChannel] ${t("notification.sendFailed")}`, err);
2180
2557
  }
2181
2558
  }
2182
2559
  };
@@ -2201,7 +2578,7 @@ var ActivityPushChannel = class {
2201
2578
  this.keyId = config.keyId;
2202
2579
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
2203
2580
  this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
2204
- console.log(`[ActivityPushChannel] \u5DF2\u521D\u59CB\u5316 (${config.sandbox ? "\u6C99\u7BB1" : "\u751F\u4EA7"}\u6A21\u5F0F)`);
2581
+ console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
2205
2582
  }
2206
2583
  /** 获取或新建 HTTP/2 长连接 */
2207
2584
  getHttp2Client() {
@@ -2210,7 +2587,7 @@ var ActivityPushChannel = class {
2210
2587
  }
2211
2588
  this.http2Client = http2.connect(`https://${this.apnsHost}`);
2212
2589
  this.http2Client.on("error", (err) => {
2213
- console.warn("[ActivityPushChannel] HTTP/2 \u8FDE\u63A5\u9519\u8BEF\uFF0C\u5C06\u5728\u4E0B\u6B21\u8BF7\u6C42\u65F6\u91CD\u5EFA:", err.message);
2590
+ console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
2214
2591
  this.http2Client?.destroy();
2215
2592
  this.http2Client = null;
2216
2593
  });
@@ -2222,7 +2599,7 @@ var ActivityPushChannel = class {
2222
2599
  /** 注册 Activity push token */
2223
2600
  addToken(sessionId, token) {
2224
2601
  this.tokens.set(sessionId, token);
2225
- console.log(`[ActivityPushChannel] \u5DF2\u6CE8\u518C token: session=${sessionId}`);
2602
+ console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
2226
2603
  }
2227
2604
  /** 移除 Activity push token */
2228
2605
  removeToken(sessionId) {
@@ -2242,7 +2619,7 @@ var ActivityPushChannel = class {
2242
2619
  try {
2243
2620
  await this.sendToAPNs(token, payload);
2244
2621
  } catch (err) {
2245
- console.warn(`[ActivityPushChannel] \u66F4\u65B0\u5931\u8D25 session=${sessionId}:`, err);
2622
+ console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
2246
2623
  }
2247
2624
  }
2248
2625
  /** 发送带通知的 content-state 更新(审批请求时使用) */
@@ -2261,7 +2638,7 @@ var ActivityPushChannel = class {
2261
2638
  try {
2262
2639
  await this.sendToAPNs(token, payload);
2263
2640
  } catch (err) {
2264
- console.warn(`[ActivityPushChannel] \u5E26\u63D0\u9192\u66F4\u65B0\u5931\u8D25 session=${sessionId}:`, err);
2641
+ console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
2265
2642
  }
2266
2643
  }
2267
2644
  /** 结束指定会话的 Live Activity */
@@ -2278,7 +2655,7 @@ var ActivityPushChannel = class {
2278
2655
  try {
2279
2656
  await this.sendToAPNs(token, payload);
2280
2657
  } catch (err) {
2281
- console.warn(`[ActivityPushChannel] \u7ED3\u675F\u5931\u8D25 session=${sessionId}:`, err);
2658
+ console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
2282
2659
  }
2283
2660
  this.tokens.delete(sessionId);
2284
2661
  }
@@ -2325,7 +2702,7 @@ var ActivityPushChannel = class {
2325
2702
  this.http2Client?.destroy();
2326
2703
  this.http2Client = null;
2327
2704
  }
2328
- reject(new Error(`APNs \u8FD4\u56DE ${statusCode}: ${responseData}`));
2705
+ reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
2329
2706
  }
2330
2707
  });
2331
2708
  req.on("error", (err) => {
@@ -2679,9 +3056,9 @@ async function createWithRetry(label, port, factory) {
2679
3056
  return await factory();
2680
3057
  } catch (err) {
2681
3058
  if (err?.code === "EADDRINUSE") {
2682
- console.warn(`[Server] \u7AEF\u53E3 ${port} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u91CA\u653E\u65E7\u8FDB\u7A0B...`);
3059
+ console.warn(`[Server] ${t("server.portInUse", { port })}`);
2683
3060
  await killPortProcess(port);
2684
- console.log(`[Server] \u91CD\u65B0\u542F\u52A8 ${label}...`);
3061
+ console.log(`[Server] ${t("server.restarting", { label })}`);
2685
3062
  return await factory();
2686
3063
  }
2687
3064
  throw err;
@@ -2717,10 +3094,10 @@ async function start(opts = {}) {
2717
3094
  try {
2718
3095
  const activityChannel = new ActivityPushChannel(opts.activityPush);
2719
3096
  notificationService.setActivityPushChannel(activityChannel);
2720
- console.log("[Server] ActivityKit Push \u5DF2\u542F\u7528");
3097
+ console.log(`[Server] ${t("server.activityPushEnabled")}`);
2721
3098
  } catch (err) {
2722
- console.warn("[Server] ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:", err);
2723
- console.log("[Server] \u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09");
3099
+ console.warn(`[Server] ${t("server.activityPushFailed")}`, err);
3100
+ console.log(`[Server] ${t("server.activityPushContinue")}`);
2724
3101
  }
2725
3102
  }
2726
3103
  const wsBridge = await createWithRetry(
@@ -2736,6 +3113,13 @@ async function start(opts = {}) {
2736
3113
  HTTP_PORT,
2737
3114
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
2738
3115
  );
3116
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3117
+ notificationService.setGlobalPendingCountProvider(
3118
+ () => approvalProxy.getPendingCount() + unreadSessionIds.size
3119
+ );
3120
+ const broadcastUnreadSessions = () => {
3121
+ wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
3122
+ };
2739
3123
  wsBridge.onConnection(async (ws) => {
2740
3124
  const result = await getProjects();
2741
3125
  if (result.ok) {
@@ -2745,6 +3129,15 @@ async function start(opts = {}) {
2745
3129
  type: "session_list",
2746
3130
  sessions: sessionManager.getActiveSessions()
2747
3131
  });
3132
+ for (const req of approvalProxy.getAllPendingRequests()) {
3133
+ wsBridge.send(ws, { type: "approval_request", request: req });
3134
+ }
3135
+ for (const req of sessionManager.getAllPendingQuestions()) {
3136
+ wsBridge.send(ws, { type: "question_request", request: req });
3137
+ }
3138
+ if (unreadSessionIds.size > 0) {
3139
+ wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
3140
+ }
2748
3141
  });
2749
3142
  wsBridge.onClientEvent(async (event, ws) => {
2750
3143
  try {
@@ -2803,7 +3196,32 @@ async function start(opts = {}) {
2803
3196
  sessions: sessionManager.getActiveSessions()
2804
3197
  });
2805
3198
  const bufferedEvents = sessionManager.getSessionEvents(event.sessionId);
2806
- if (bufferedEvents.length > 0) {
3199
+ if (sessionManager.isBufferTruncated(event.sessionId)) {
3200
+ const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
3201
+ if (projectPath) {
3202
+ const historyResult = await getSessionHistory(projectPath, event.sessionId);
3203
+ if (historyResult.ok && historyResult.value.length > 0) {
3204
+ const merged = [...historyResult.value, ...bufferedEvents];
3205
+ wsBridge.send(ws, {
3206
+ type: "session_history",
3207
+ sessionId: event.sessionId,
3208
+ events: merged
3209
+ });
3210
+ } else if (bufferedEvents.length > 0) {
3211
+ wsBridge.send(ws, {
3212
+ type: "session_history",
3213
+ sessionId: event.sessionId,
3214
+ events: bufferedEvents
3215
+ });
3216
+ }
3217
+ } else if (bufferedEvents.length > 0) {
3218
+ wsBridge.send(ws, {
3219
+ type: "session_history",
3220
+ sessionId: event.sessionId,
3221
+ events: bufferedEvents
3222
+ });
3223
+ }
3224
+ } else if (bufferedEvents.length > 0) {
2807
3225
  wsBridge.send(ws, {
2808
3226
  type: "session_history",
2809
3227
  sessionId: event.sessionId,
@@ -2813,6 +3231,9 @@ async function start(opts = {}) {
2813
3231
  for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
2814
3232
  wsBridge.send(ws, { type: "approval_request", request: req });
2815
3233
  }
3234
+ for (const req of sessionManager.getPendingQuestionsForSession(event.sessionId)) {
3235
+ wsBridge.send(ws, { type: "question_request", request: req });
3236
+ }
2816
3237
  break;
2817
3238
  }
2818
3239
  case "list_projects": {
@@ -2822,7 +3243,7 @@ async function start(opts = {}) {
2822
3243
  } else {
2823
3244
  wsBridge.send(ws, {
2824
3245
  type: "error",
2825
- message: `\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: ${result.error.message}`,
3246
+ message: t("server.listProjectsFailed", { error: result.error.message }),
2826
3247
  code: "PROJECT_LIST_ERROR"
2827
3248
  });
2828
3249
  }
@@ -2848,7 +3269,7 @@ async function start(opts = {}) {
2848
3269
  } else {
2849
3270
  wsBridge.send(ws, {
2850
3271
  type: "error",
2851
- message: `\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: ${histResult.error.message}`,
3272
+ message: t("server.listSessionsFailed", { error: histResult.error.message }),
2852
3273
  code: "PROJECT_SESSIONS_ERROR"
2853
3274
  });
2854
3275
  }
@@ -2859,7 +3280,7 @@ async function start(opts = {}) {
2859
3280
  if (!historyResult.ok) {
2860
3281
  wsBridge.send(ws, {
2861
3282
  type: "error",
2862
- message: `\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: ${historyResult.error.message}`,
3283
+ message: t("server.readHistoryFailed", { error: historyResult.error.message }),
2863
3284
  code: "SESSION_HISTORY_ERROR",
2864
3285
  sessionId: event.sessionId
2865
3286
  });
@@ -2884,7 +3305,7 @@ async function start(opts = {}) {
2884
3305
  }
2885
3306
  case "suggest_next_prompt": {
2886
3307
  const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
2887
- let context = "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09";
3308
+ let context = t("server.noHistory");
2888
3309
  if (historyResult.ok && historyResult.value.length > 0) {
2889
3310
  const recent = historyResult.value.slice(-10);
2890
3311
  context = recent.map((e) => {
@@ -2893,7 +3314,8 @@ async function start(opts = {}) {
2893
3314
  return `Assistant: ${text.substring(0, 300)}`;
2894
3315
  }
2895
3316
  if (e.type === "user") {
2896
- const text = e.message.content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
3317
+ const content = e.message.content;
3318
+ const text = typeof content === "string" ? content : content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
2897
3319
  return text ? `User: ${text.substring(0, 300)}` : null;
2898
3320
  }
2899
3321
  return null;
@@ -2934,6 +3356,9 @@ async function start(opts = {}) {
2934
3356
  }
2935
3357
  case "viewing_session": {
2936
3358
  wsBridge.setViewingSession(ws, event.sessionId);
3359
+ if (unreadSessionIds.delete(event.sessionId)) {
3360
+ broadcastUnreadSessions();
3361
+ }
2937
3362
  break;
2938
3363
  }
2939
3364
  case "left_session": {
@@ -2947,14 +3372,14 @@ async function start(opts = {}) {
2947
3372
  default: {
2948
3373
  wsBridge.send(ws, {
2949
3374
  type: "error",
2950
- message: `\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: ${event.type}`,
3375
+ message: t("server.unknownEvent", { type: event.type }),
2951
3376
  code: "UNKNOWN_EVENT"
2952
3377
  });
2953
3378
  }
2954
3379
  }
2955
3380
  } catch (err) {
2956
3381
  const message = err instanceof Error ? err.message : String(err);
2957
- console.error("[Server] \u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38:", message);
3382
+ console.error(`[Server] ${t("server.clientEventError")}:`, message);
2958
3383
  const errorCodeMap = {
2959
3384
  create_session: "SESSION_CREATE_ERROR",
2960
3385
  send_message: "SEND_MESSAGE_ERROR",
@@ -2970,10 +3395,16 @@ async function start(opts = {}) {
2970
3395
  });
2971
3396
  sessionManager.onEvent((event) => {
2972
3397
  wsBridge.broadcast(event);
3398
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3399
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3400
+ unreadSessionIds.add(event.sessionId);
3401
+ broadcastUnreadSessions();
3402
+ }
3403
+ }
2973
3404
  });
2974
3405
  wsBridge.onDisconnect(() => {
2975
3406
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
2976
- approvalProxy.approveAll("\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00");
3407
+ approvalProxy.approveAll(t("server.phoneDisconnected"));
2977
3408
  }
2978
3409
  });
2979
3410
  approvalProxy.onApprovalRequest((request) => {
@@ -2989,52 +3420,81 @@ async function start(opts = {}) {
2989
3420
  if (!approvalProxy.isPending(request.id)) return;
2990
3421
  if (wsBridge.isViewingSession(request.sessionId)) return;
2991
3422
  if (wsBridge.getConnectionCount() > 0) return;
2992
- console.log(`[Server] \u5BA1\u6279\u8BF7\u6C42 ${request.id} 60\u79D2\u672A\u5904\u7406\uFF0C\u91CD\u8BD5\u63A8\u9001`);
3423
+ console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
2993
3424
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
2994
3425
  notificationService.notifyApproval(request, pendingCount);
2995
3426
  }, 6e4);
2996
3427
  });
3428
+ sessionManager.onEvent((event) => {
3429
+ if (event.type !== "question_request") return;
3430
+ const { request } = event;
3431
+ setTimeout(() => {
3432
+ if (!sessionManager.isQuestionPending(request.id)) return;
3433
+ if (wsBridge.isViewingSession(request.sessionId)) return;
3434
+ if (wsBridge.getConnectionCount() > 0) return;
3435
+ notificationService.notifyQuestion(request);
3436
+ }, 5e3);
3437
+ setTimeout(() => {
3438
+ if (!sessionManager.isQuestionPending(request.id)) return;
3439
+ if (wsBridge.isViewingSession(request.sessionId)) return;
3440
+ if (wsBridge.getConnectionCount() > 0) return;
3441
+ console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
3442
+ notificationService.notifyQuestion(request);
3443
+ }, 6e4);
3444
+ });
2997
3445
  approvalProxy.setStatusInfoProvider(() => ({
2998
3446
  connections: wsBridge.getConnectionCount(),
2999
3447
  activeSessions: sessionManager.getActiveSessions().length
3000
3448
  }));
3001
- const mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT });
3002
- mdnsService.start();
3449
+ let mdnsService = null;
3450
+ const startMdns = () => {
3451
+ if (mdnsService) return;
3452
+ mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3453
+ mdnsService.start();
3454
+ };
3455
+ const stopMdns = () => {
3456
+ if (!mdnsService) return;
3457
+ mdnsService.stop();
3458
+ mdnsService = null;
3459
+ };
3460
+ if (opts.enableAutoConnect !== false) {
3461
+ startMdns();
3462
+ }
3003
3463
  const hookInstaller = new HookInstaller();
3004
3464
  try {
3005
3465
  const installed = await hookInstaller.isInstalled();
3006
3466
  if (!installed) {
3007
3467
  await hookInstaller.install();
3008
- console.log("[Server] Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code");
3468
+ console.log(`[Server] ${t("server.hookInstalled")}`);
3009
3469
  } else {
3010
- console.log("[Server] Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5");
3470
+ console.log(`[Server] ${t("server.hookExists")}`);
3011
3471
  }
3012
3472
  } catch (err) {
3013
- console.error("[Server] Hook \u5B89\u88C5\u5931\u8D25:", err);
3014
- console.log("[Server] \u7EE7\u7EED\u542F\u52A8\uFF08hook \u529F\u80FD\u53EF\u80FD\u4E0D\u53EF\u7528\uFF09");
3473
+ console.error(`[Server] ${t("server.hookInstallFailed")}`, err);
3474
+ console.log(`[Server] ${t("server.hookContinue")}`);
3015
3475
  }
3016
3476
  const stop = async () => {
3017
- console.log("[Server] \u6B63\u5728\u4F18\u96C5\u5173\u95ED...");
3477
+ console.log(`[Server] ${t("server.shuttingDown")}`);
3018
3478
  const errors = [];
3019
3479
  const attempt = async (fn, label) => {
3020
3480
  try {
3021
3481
  await fn();
3022
3482
  } catch (err) {
3023
- console.error(`[Server] \u5173\u95ED ${label} \u51FA\u9519:`, err);
3483
+ console.error(`[Server] ${t("server.shutdownComponentError", { label })}:`, err);
3024
3484
  errors.push(err);
3025
3485
  }
3026
3486
  };
3027
- await attempt(() => mdnsService.stop(), "mDNS");
3487
+ await attempt(() => stopMdns(), "mDNS");
3028
3488
  await attempt(() => wsBridge.close(), "WebSocket");
3029
3489
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3030
3490
  await attempt(() => sessionManager.destroy(), "SessionManager");
3031
3491
  await attempt(() => notificationService.destroy(), "NotificationService");
3032
3492
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3033
3493
  if (errors.length > 0) {
3034
- console.error(`[Server] \u5173\u95ED\u5B8C\u6210\uFF0C${errors.length} \u4E2A\u9519\u8BEF`);
3494
+ console.error(`[Server] ${t("server.shutdownWithErrors", { count: errors.length })}`);
3035
3495
  throw errors[0];
3036
3496
  }
3037
- console.log("[Server] \u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED");
3497
+ console.log(`[Server] ${t("server.shutdownComplete")}`);
3038
3498
  };
3039
3499
  return {
3040
3500
  token,
@@ -3045,7 +3505,14 @@ async function start(opts = {}) {
3045
3505
  stop,
3046
3506
  setMacNotification: (enabled) => notificationService.setChannelEnabled("mac", enabled),
3047
3507
  setExpoPush: (enabled) => notificationService.setChannelEnabled("expo", enabled),
3048
- onServerEvent: (cb) => sessionManager.onEvent(cb)
3508
+ onServerEvent: (cb) => sessionManager.onEvent(cb),
3509
+ setAutoConnect: (enabled) => {
3510
+ if (enabled) {
3511
+ startMdns();
3512
+ } else {
3513
+ stopMdns();
3514
+ }
3515
+ }
3049
3516
  };
3050
3517
  }
3051
3518
  // Annotate the CommonJS export names for ESM import in node: