sessix-server 0.1.0 → 0.1.2

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/index.js CHANGED
@@ -26,6 +26,210 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/index.ts
27
27
  var import_node_os5 = require("os");
28
28
 
29
+ // src/i18n/locales/zh.ts
30
+ var zh = {
31
+ startup: {
32
+ banner: " Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3",
33
+ scanToPair: " \u626B\u7801\u914D\u5BF9\uFF1A",
34
+ waitingConnection: " \u7B49\u5F85\u624B\u673A\u8FDE\u63A5..."
35
+ },
36
+ server: {
37
+ listProjectsFailed: "\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: {{error}}",
38
+ listSessionsFailed: "\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: {{error}}",
39
+ readHistoryFailed: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: {{error}}",
40
+ noHistory: "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09",
41
+ unknownEvent: "\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: {{type}}",
42
+ clientEventError: "\u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38",
43
+ phoneDisconnected: "\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00",
44
+ approvalRetry: "\u5BA1\u6279\u8BF7\u6C42 {{id}} 60\u79D2\u672A\u5904\u7406\uFF0C\u91CD\u8BD5\u63A8\u9001",
45
+ hookInstalled: "Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code",
46
+ hookExists: "Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5",
47
+ hookContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08hook \u529F\u80FD\u53EF\u80FD\u4E0D\u53EF\u7528\uFF09",
48
+ shuttingDown: "\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
49
+ shutdownComponentError: "\u5173\u95ED {{label}} \u51FA\u9519",
50
+ shutdownWithErrors: "\u5173\u95ED\u5B8C\u6210\uFF0C{{count}} \u4E2A\u9519\u8BEF",
51
+ shutdownComplete: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED"
52
+ },
53
+ ws: {
54
+ started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
55
+ serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF"
56
+ },
57
+ mdns: {
58
+ alreadyRunning: "\u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D",
59
+ started: "mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 {{port}}",
60
+ stopped: "\u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62",
61
+ closed: "mDNS \u670D\u52A1\u5DF2\u5173\u95ED"
62
+ },
63
+ approval: {
64
+ httpStarted: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
65
+ serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF",
66
+ yoloMode: "YOLO \u6A21\u5F0F{{status}}",
67
+ yoloEnabled: "\u5DF2\u542F\u7528",
68
+ yoloDisabled: "\u5DF2\u5173\u95ED",
69
+ requestNotFound: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6",
70
+ requestProcessed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u5904\u7406",
71
+ alwaysAllowWritten: "\u5DF2\u5C06 {{entry}} \u5199\u5165 {{label}}",
72
+ settingsWriteFailed: "\u5199\u5165 settings.json \u5931\u8D25",
73
+ autoAllowed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u81EA\u52A8\u5141\u8BB8{{reason}}",
74
+ serverClosed: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED",
75
+ httpClosed: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED",
76
+ received: "\u6536\u5230\u5BA1\u6279\u8BF7\u6C42",
77
+ alwaysAllowPassThrough: "{{tool}} \u5DF2\u88AB\u59CB\u7EC8\u5141\u8BB8\uFF0C\u76F4\u63A5\u653E\u884C\uFF08\u4E0D\u901A\u77E5\uFF09",
78
+ yoloAutoAllow: "YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C",
79
+ timeout: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8",
80
+ processingFailed: "\u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25",
81
+ forbidden: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE",
82
+ bodyTooLarge: "\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09",
83
+ invalidJson: "\u65E0\u6548\u7684 JSON body"
84
+ },
85
+ notification: {
86
+ tokenRegistered: "\u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
87
+ tokenRemoved: "\u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
88
+ soundPrefsUpdated: "\u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D",
89
+ sendingPush: "\u53D1\u9001\u63A8\u9001\uFF0Ctokens:",
90
+ pushApiError: "Expo Push API \u8FD4\u56DE\u9519\u8BEF:",
91
+ pushApiFormatError: "Expo Push API \u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF0C\u7F3A\u5C11 data \u6570\u7EC4:",
92
+ pushFailed: "\u63A8\u9001\u5931\u8D25:",
93
+ sendFailed: "\u53D1\u9001\u63A8\u9001\u5931\u8D25:",
94
+ pendingApprovals: "{{title}} \u2014 {{count}} \u9879\u5F85\u5BA1\u6279",
95
+ taskComplete: "\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B65\u6307\u4EE4",
96
+ taskError: "\u6267\u884C\u51FA\u9519\uFF0C\u8BF7\u67E5\u770B\u8BE6\u60C5",
97
+ questionRetry: "\u63D0\u95EE {{id}} 60\u79D2\u672A\u56DE\u7B54\uFF0C\u91CD\u8BD5\u63A8\u9001"
98
+ },
99
+ tray: {
100
+ tooltip: "Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3"
101
+ },
102
+ watcher: {
103
+ readError: "\u8BFB\u53D6\u5F02\u5E38 {{sessionId}}",
104
+ startWatching: "\u5F00\u59CB\u76D1\u542C",
105
+ stopWatching: "\u505C\u6B62\u76D1\u542C"
106
+ }
107
+ };
108
+
109
+ // src/i18n/locales/en.ts
110
+ var en = {
111
+ startup: {
112
+ banner: " Sessix \u2014 AI Coding Mobile Command Center",
113
+ scanToPair: " Scan to pair:",
114
+ waitingConnection: " Waiting for phone connection..."
115
+ },
116
+ server: {
117
+ listProjectsFailed: "Failed to list projects: {{error}}",
118
+ listSessionsFailed: "Failed to list project sessions: {{error}}",
119
+ readHistoryFailed: "Failed to read session history: {{error}}",
120
+ noHistory: "(No conversation history)",
121
+ unknownEvent: "Unknown event type: {{type}}",
122
+ clientEventError: "Client event handling error",
123
+ phoneDisconnected: "Phone disconnected",
124
+ approvalRetry: "Approval request {{id}} not handled in 60s, retrying push",
125
+ hookInstalled: "Sessix hook installed to Claude Code",
126
+ hookExists: "Sessix hook already exists, skipping installation",
127
+ hookContinue: "Continuing startup (hook functionality may be unavailable)",
128
+ shuttingDown: "Graceful shutdown in progress...",
129
+ shutdownComponentError: "Error closing {{label}}",
130
+ shutdownWithErrors: "Shutdown complete, {{count}} error(s)",
131
+ shutdownComplete: "All services closed"
132
+ },
133
+ ws: {
134
+ started: "WebSocket server started on port {{port}}",
135
+ serverError: "Server runtime error"
136
+ },
137
+ mdns: {
138
+ alreadyRunning: "Service is already running",
139
+ started: "mDNS broadcast started: _sessix._tcp port {{port}}",
140
+ stopped: "Service broadcast stopped",
141
+ closed: "mDNS service closed"
142
+ },
143
+ approval: {
144
+ httpStarted: "HTTP approval server started on port {{port}}",
145
+ serverError: "Server runtime error",
146
+ yoloMode: "YOLO mode {{status}}",
147
+ yoloEnabled: "enabled",
148
+ yoloDisabled: "disabled",
149
+ requestNotFound: "Approval request {{id}} not found or timed out",
150
+ requestProcessed: "Approval request {{id}} processed",
151
+ alwaysAllowWritten: "Written {{entry}} to {{label}}",
152
+ settingsWriteFailed: "Failed to write settings.json",
153
+ autoAllowed: "Approval request {{id}} auto-allowed{{reason}}",
154
+ serverClosed: "Server closed",
155
+ httpClosed: "HTTP approval server closed",
156
+ received: "Approval request received",
157
+ alwaysAllowPassThrough: "{{tool}} is always-allowed, passing through (no notification)",
158
+ yoloAutoAllow: "YOLO mode, auto-allowing",
159
+ timeout: "Approval request {{id}} timed out, default allowed",
160
+ processingFailed: "Approval request processing failed",
161
+ forbidden: "Forbidden: localhost access only",
162
+ bodyTooLarge: "Request body too large (>1MB)",
163
+ invalidJson: "Invalid JSON body"
164
+ },
165
+ notification: {
166
+ tokenRegistered: "Push token registered, devices: {{count}}",
167
+ tokenRemoved: "Push token removed, devices: {{count}}",
168
+ soundPrefsUpdated: "Sound preferences updated",
169
+ sendingPush: "Sending push, tokens:",
170
+ pushApiError: "Expo Push API returned error:",
171
+ pushApiFormatError: "Expo Push API response format error, missing data array:",
172
+ pushFailed: "Push failed:",
173
+ sendFailed: "Send push failed:",
174
+ pendingApprovals: "{{title}} \u2014 {{count}} pending approval(s)",
175
+ taskComplete: "Completed, awaiting next instruction",
176
+ taskError: "Execution error, check details",
177
+ questionRetry: "Question {{id}} not answered in 60s, retrying push"
178
+ },
179
+ tray: {
180
+ tooltip: "Sessix \u2014 AI Coding Mobile Command Center"
181
+ },
182
+ watcher: {
183
+ readError: "Read error {{sessionId}}",
184
+ startWatching: "Start watching",
185
+ stopWatching: "Stop watching"
186
+ }
187
+ };
188
+
189
+ // src/i18n/index.ts
190
+ function detectLocale() {
191
+ try {
192
+ const raw = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
193
+ if (raw.startsWith("zh")) return "zh";
194
+ } catch {
195
+ }
196
+ return "en";
197
+ }
198
+ var locales = { zh, en };
199
+ var currentLocale = detectLocale();
200
+ var currentMessages = locales[currentLocale] ?? en;
201
+ function t(key, params) {
202
+ const parts = key.split(".");
203
+ let val = currentMessages;
204
+ for (const p of parts) {
205
+ if (val && typeof val === "object") {
206
+ val = val[p];
207
+ } else {
208
+ val = void 0;
209
+ break;
210
+ }
211
+ }
212
+ if (typeof val !== "string") {
213
+ let fallback = en;
214
+ for (const p of parts) {
215
+ if (fallback && typeof fallback === "object") {
216
+ fallback = fallback[p];
217
+ } else {
218
+ fallback = void 0;
219
+ break;
220
+ }
221
+ }
222
+ val = typeof fallback === "string" ? fallback : key;
223
+ }
224
+ let result = val;
225
+ if (params) {
226
+ for (const [k, v] of Object.entries(params)) {
227
+ result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
228
+ }
229
+ }
230
+ return result;
231
+ }
232
+
29
233
  // src/server.ts
30
234
  var import_uuid4 = require("uuid");
31
235
  var import_promises4 = require("fs/promises");
@@ -157,6 +361,8 @@ var ProcessProvider = class {
157
361
  }
158
362
  const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
159
363
  if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
364
+ entry.session.status = "running";
365
+ entry.session.lastActiveAt = Date.now();
160
366
  this.writeUserMessage(entry.process, message, sessionId, images);
161
367
  return;
162
368
  }
@@ -540,7 +746,7 @@ var SessionManager = class {
540
746
  unsubscribeMap = /* @__PURE__ */ new Map();
541
747
  /** 每个会话的事件缓冲区(用于新订阅者重放)*/
542
748
  sessionEventBuffers = /* @__PURE__ */ new Map();
543
- /** AskUserQuestion 问题映射:requestId → resolve 回调 */
749
+ /** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
544
750
  pendingQuestions = /* @__PURE__ */ new Map();
545
751
  /**
546
752
  * 会话状态缓存(用于追踪 status 变化,检测 oldStatus !== newStatus 时广播)
@@ -555,6 +761,10 @@ var SessionManager = class {
555
761
  runningStartedAt = /* @__PURE__ */ new Map();
556
762
  /** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
557
763
  pendingAssistantEvents = /* @__PURE__ */ new Map();
764
+ /** 标记哪些会话的缓冲区曾被截断(溢出过 BUFFER_MAX) */
765
+ bufferTruncated = /* @__PURE__ */ new Set();
766
+ /** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
767
+ sessionProjectPaths = /* @__PURE__ */ new Map();
558
768
  constructor(provider) {
559
769
  this.provider = provider;
560
770
  }
@@ -579,6 +789,7 @@ var SessionManager = class {
579
789
  images
580
790
  });
581
791
  this.lastBroadcastStatus.set(session.id, session.status);
792
+ this.sessionProjectPaths.set(session.id, projectPath);
582
793
  this.unsubscribeSession(session.id);
583
794
  this.subscribeToSession(session.id);
584
795
  console.log(`[SessionManager] \u4F1A\u8BDD\u5DF2\u521B\u5EFA: ${session.id} (\u9879\u76EE: ${projectPath})`);
@@ -600,6 +811,8 @@ var SessionManager = class {
600
811
  this.clearPendingQuestions(sessionId);
601
812
  this.lastBroadcastStatus.delete(sessionId);
602
813
  this.sessionEventBuffers.delete(sessionId);
814
+ this.bufferTruncated.delete(sessionId);
815
+ this.sessionProjectPaths.delete(sessionId);
603
816
  this.sessionStats.delete(sessionId);
604
817
  const pending = this.pendingAssistantEvents.get(sessionId);
605
818
  if (pending) {
@@ -615,6 +828,18 @@ var SessionManager = class {
615
828
  getSessionEvents(sessionId) {
616
829
  return this.sessionEventBuffers.get(sessionId) ?? [];
617
830
  }
831
+ /**
832
+ * 检查会话的缓冲区是否曾被截断(溢出过 BUFFER_MAX)
833
+ */
834
+ isBufferTruncated(sessionId) {
835
+ return this.bufferTruncated.has(sessionId);
836
+ }
837
+ /**
838
+ * 获取会话的项目路径(用于截断时从 JSONL 补全历史)
839
+ */
840
+ getSessionProjectPath(sessionId) {
841
+ return this.sessionProjectPaths.get(sessionId);
842
+ }
618
843
  /**
619
844
  * 处理 AskUserQuestion 回答(从手机端传来)
620
845
  */
@@ -629,6 +854,46 @@ var SessionManager = class {
629
854
  pending.resolve(answer);
630
855
  console.log(`[SessionManager] \u95EE\u9898\u5DF2\u56DE\u7B54: ${requestId}`);
631
856
  }
857
+ /**
858
+ * 获取指定会话的所有待回答问题(用于重连时恢复)
859
+ */
860
+ getPendingQuestionsForSession(sessionId) {
861
+ const result = [];
862
+ for (const [requestId, pending] of this.pendingQuestions) {
863
+ if (pending.sessionId === sessionId) {
864
+ result.push({
865
+ id: requestId,
866
+ sessionId,
867
+ toolUseId: pending.toolUseId,
868
+ question: pending.question,
869
+ options: pending.options,
870
+ createdAt: pending.createdAt
871
+ });
872
+ }
873
+ }
874
+ return result;
875
+ }
876
+ /** 检查某个问题是否仍在等待回答 */
877
+ isQuestionPending(requestId) {
878
+ return this.pendingQuestions.has(requestId);
879
+ }
880
+ /**
881
+ * 获取所有待回答问题(用于客户端重连时恢复状态)
882
+ */
883
+ getAllPendingQuestions() {
884
+ const result = [];
885
+ for (const [requestId, pending] of this.pendingQuestions) {
886
+ result.push({
887
+ id: requestId,
888
+ sessionId: pending.sessionId,
889
+ toolUseId: pending.toolUseId,
890
+ question: pending.question,
891
+ options: pending.options,
892
+ createdAt: pending.createdAt
893
+ });
894
+ }
895
+ return result;
896
+ }
632
897
  /**
633
898
  * 获取所有活跃会话(含服务器端统计)
634
899
  */
@@ -661,6 +926,8 @@ var SessionManager = class {
661
926
  }
662
927
  this.unsubscribeMap.clear();
663
928
  this.sessionEventBuffers.clear();
929
+ this.bufferTruncated.clear();
930
+ this.sessionProjectPaths.clear();
664
931
  this.sessionStats.clear();
665
932
  for (const [, pending] of this.pendingAssistantEvents) {
666
933
  clearTimeout(pending.timer);
@@ -714,6 +981,7 @@ var SessionManager = class {
714
981
  buffer.push(event);
715
982
  if (buffer.length > BUFFER_MAX) {
716
983
  buffer.splice(0, buffer.length - BUFFER_MAX);
984
+ this.bufferTruncated.add(sessionId);
717
985
  }
718
986
  this.sessionEventBuffers.set(sessionId, buffer);
719
987
  if (event.type === "assistant" && Array.isArray(event.message?.content)) {
@@ -866,7 +1134,7 @@ var SessionManager = class {
866
1134
  this.updateSessionStatus(sessionId, "waiting_question");
867
1135
  this.emit({ type: "question_request", request });
868
1136
  const answerPromise = new Promise((resolve) => {
869
- this.pendingQuestions.set(requestId, { sessionId, toolUseId, resolve });
1137
+ this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
870
1138
  });
871
1139
  answerPromise.then(async (answer) => {
872
1140
  try {
@@ -945,13 +1213,13 @@ var SessionFileWatcher = class {
945
1213
  };
946
1214
  watcher.on("change", () => {
947
1215
  this.readNewLines(sessionId).catch((err) => {
948
- console.error(`[SessionFileWatcher] \u8BFB\u53D6\u5F02\u5E38 ${sessionId}:`, err);
1216
+ console.error(`[SessionFileWatcher] ${t("watcher.readError", { sessionId })}:`, err);
949
1217
  });
950
1218
  this.resetIdleTimer(sessionId);
951
1219
  });
952
1220
  this.watchers.set(sessionId, entry);
953
1221
  this.resetIdleTimer(sessionId);
954
- console.log(`[SessionFileWatcher] \u5F00\u59CB\u76D1\u542C: ${sessionId} (offset=${byteOffset})`);
1222
+ console.log(`[SessionFileWatcher] ${t("watcher.startWatching")}: ${sessionId} (offset=${byteOffset})`);
955
1223
  }
956
1224
  /** 停止监听指定会话 */
957
1225
  unwatch(sessionId) {
@@ -960,7 +1228,7 @@ var SessionFileWatcher = class {
960
1228
  if (entry.idleTimer) clearTimeout(entry.idleTimer);
961
1229
  void entry.watcher.close();
962
1230
  this.watchers.delete(sessionId);
963
- console.log(`[SessionFileWatcher] \u505C\u6B62\u76D1\u542C: ${sessionId}`);
1231
+ console.log(`[SessionFileWatcher] ${t("watcher.stopWatching")}: ${sessionId}`);
964
1232
  }
965
1233
  /** 停止所有监听(服务关闭时调用) */
966
1234
  destroy() {
@@ -1083,6 +1351,8 @@ var WsBridge = class _WsBridge {
1083
1351
  lastPongMap = /* @__PURE__ */ new Map();
1084
1352
  /** 每个连接当前正在查看的会话 ID */
1085
1353
  viewingSessions = /* @__PURE__ */ new Map();
1354
+ /** 每个连接的消息处理队列(串行化 async handler,防止 create_session/subscribe 竞态) */
1355
+ messageQueues = /* @__PURE__ */ new Map();
1086
1356
  constructor(options) {
1087
1357
  this.token = options.token;
1088
1358
  this.wss = new import_ws.WebSocketServer({
@@ -1098,7 +1368,7 @@ var WsBridge = class _WsBridge {
1098
1368
  });
1099
1369
  this.wss.on("connection", (ws) => this.handleConnection(ws));
1100
1370
  this.startHeartbeat();
1101
- console.log(`[WsBridge] WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1371
+ console.log(`[WsBridge] ${t("ws.started", { port: options.port })}`);
1102
1372
  }
1103
1373
  /**
1104
1374
  * 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
@@ -1108,7 +1378,7 @@ var WsBridge = class _WsBridge {
1108
1378
  return new Promise((resolve, reject) => {
1109
1379
  const bridge = new _WsBridge(options);
1110
1380
  bridge.wss.once("listening", () => {
1111
- bridge.wss.on("error", (err) => console.error("[WsBridge] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1381
+ bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
1112
1382
  resolve(bridge);
1113
1383
  });
1114
1384
  bridge.wss.once("error", reject);
@@ -1227,6 +1497,7 @@ var WsBridge = class _WsBridge {
1227
1497
  ws.on("close", () => {
1228
1498
  this.lastPongMap.delete(ws);
1229
1499
  this.viewingSessions.delete(ws);
1500
+ this.messageQueues.delete(ws);
1230
1501
  setTimeout(() => {
1231
1502
  console.log(`[WsBridge] \u5BA2\u6237\u7AEF\u65AD\u5F00\uFF0C\u5F53\u524D\u8FDE\u63A5\u6570: ${this.getConnectionCount()}`);
1232
1503
  for (const cb of this.disconnectCallbacks) {
@@ -1242,15 +1513,24 @@ var WsBridge = class _WsBridge {
1242
1513
  console.error("[WsBridge] \u8FDE\u63A5\u9519\u8BEF:", err.message);
1243
1514
  });
1244
1515
  }
1245
- /** 分发客户端事件到所有注册的回调 */
1516
+ /**
1517
+ * 分发客户端事件到所有注册的回调
1518
+ *
1519
+ * 使用 per-connection 队列串行化处理,确保 async 回调(如 create_session)
1520
+ * 完成后才处理下一条消息(如 subscribe),避免竞态条件。
1521
+ */
1246
1522
  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);
1523
+ const prev = this.messageQueues.get(ws) ?? Promise.resolve();
1524
+ const next = prev.then(async () => {
1525
+ for (const callback of this.clientEventCallbacks) {
1526
+ try {
1527
+ await callback(event, ws);
1528
+ } catch (err) {
1529
+ console.error("[WsBridge] \u4E8B\u4EF6\u56DE\u8C03\u6267\u884C\u5F02\u5E38:", err);
1530
+ }
1252
1531
  }
1253
- }
1532
+ });
1533
+ this.messageQueues.set(ws, next);
1254
1534
  }
1255
1535
  /** 启动心跳机制 */
1256
1536
  startHeartbeat() {
@@ -1300,7 +1580,7 @@ var ApprovalProxy = class _ApprovalProxy {
1300
1580
  this.handleRequest(req, res);
1301
1581
  });
1302
1582
  this.server.listen(options.port, () => {
1303
- console.log(`[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${options.port}`);
1583
+ console.log(`[ApprovalProxy] ${t("approval.httpStarted", { port: options.port })}`);
1304
1584
  });
1305
1585
  }
1306
1586
  /**
@@ -1310,7 +1590,7 @@ var ApprovalProxy = class _ApprovalProxy {
1310
1590
  return new Promise((resolve, reject) => {
1311
1591
  const proxy = new _ApprovalProxy(options);
1312
1592
  proxy.server.once("listening", () => {
1313
- proxy.server.on("error", (err) => console.error("[ApprovalProxy] \u670D\u52A1\u8FD0\u884C\u9519\u8BEF:", err));
1593
+ proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
1314
1594
  resolve(proxy);
1315
1595
  });
1316
1596
  proxy.server.once("error", reject);
@@ -1330,7 +1610,7 @@ var ApprovalProxy = class _ApprovalProxy {
1330
1610
  /** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
1331
1611
  setYoloMode(sessionId, enabled) {
1332
1612
  this.yoloSessions.set(sessionId, enabled);
1333
- console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F ${enabled ? "\u5DF2\u542F\u7528" : "\u5DF2\u5173\u95ED"}: ${sessionId}`);
1613
+ console.log(`[ApprovalProxy] ${t("approval.yoloMode", { status: enabled ? t("approval.yoloEnabled") : t("approval.yoloDisabled") })}: ${sessionId}`);
1334
1614
  }
1335
1615
  /** 检查会话是否处于 YOLO 模式 */
1336
1616
  isYoloMode(sessionId) {
@@ -1345,13 +1625,13 @@ var ApprovalProxy = class _ApprovalProxy {
1345
1625
  resolveApproval(requestId, decision) {
1346
1626
  const pending = this.pendingApprovals.get(requestId);
1347
1627
  if (!pending) {
1348
- console.warn(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6`);
1628
+ console.warn(`[ApprovalProxy] ${t("approval.requestNotFound", { id: requestId })}`);
1349
1629
  return false;
1350
1630
  }
1351
1631
  clearTimeout(pending.timer);
1352
1632
  pending.resolve(decision);
1353
1633
  this.pendingApprovals.delete(requestId);
1354
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u5904\u7406: ${decision.decision}`);
1634
+ console.log(`[ApprovalProxy] ${t("approval.requestProcessed", { id: requestId })}: ${decision.decision}`);
1355
1635
  return true;
1356
1636
  }
1357
1637
  /** 获取当前待处理的审批数量 */
@@ -1418,11 +1698,11 @@ var ApprovalProxy = class _ApprovalProxy {
1418
1698
  allow.push(entry);
1419
1699
  import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
1420
1700
  const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
1421
- console.log(`[ApprovalProxy] \u5DF2\u5C06 ${entry} \u5199\u5165 ${label}`);
1701
+ console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
1422
1702
  }
1423
1703
  this.alwaysAllowedTools.add(toolName);
1424
1704
  } catch (err) {
1425
- console.error("[ApprovalProxy] \u5199\u5165 settings.json \u5931\u8D25:", err);
1705
+ console.error(`[ApprovalProxy] ${t("approval.settingsWriteFailed")}:`, err);
1426
1706
  }
1427
1707
  }
1428
1708
  /** 获取指定会话的所有 pending approval requests(用于 subscribe 重发) */
@@ -1435,6 +1715,10 @@ var ApprovalProxy = class _ApprovalProxy {
1435
1715
  }
1436
1716
  return result;
1437
1717
  }
1718
+ /** 获取所有 pending approval requests(用于客户端重连时恢复状态) */
1719
+ getAllPendingRequests() {
1720
+ return Array.from(this.pendingApprovals.values()).map(({ request }) => request);
1721
+ }
1438
1722
  /**
1439
1723
  * 批量允许所有待处理的审批请求(手机端断线时调用)
1440
1724
  */
@@ -1444,7 +1728,7 @@ var ApprovalProxy = class _ApprovalProxy {
1444
1728
  clearTimeout(pending.timer);
1445
1729
  pending.resolve({ decision: "allow" });
1446
1730
  this.pendingApprovals.delete(requestId);
1447
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u81EA\u52A8\u5141\u8BB8${reason ? `\uFF08${reason}\uFF09` : ""}`);
1731
+ console.log(`[ApprovalProxy] ${t("approval.autoAllowed", { id: requestId, reason: reason ? `\uFF08${reason}\uFF09` : "" })}`);
1448
1732
  }
1449
1733
  }
1450
1734
  /** 优雅关闭 HTTP 服务 */
@@ -1453,14 +1737,14 @@ var ApprovalProxy = class _ApprovalProxy {
1453
1737
  const pendingEntries = Array.from(this.pendingApprovals.entries());
1454
1738
  for (const [, pending] of pendingEntries) {
1455
1739
  clearTimeout(pending.timer);
1456
- pending.resolve({ decision: "deny", reason: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED" });
1740
+ pending.resolve({ decision: "deny", reason: t("approval.serverClosed") });
1457
1741
  }
1458
1742
  this.pendingApprovals.clear();
1459
1743
  this.server.close((err) => {
1460
1744
  if (err) {
1461
1745
  reject(err);
1462
1746
  } else {
1463
- console.log("[ApprovalProxy] HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED");
1747
+ console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
1464
1748
  resolve();
1465
1749
  }
1466
1750
  });
@@ -1518,21 +1802,21 @@ var ApprovalProxy = class _ApprovalProxy {
1518
1802
  description: String(payload.description ?? body.description ?? `${toolName} \u5DE5\u5177\u8C03\u7528\u8BF7\u6C42`),
1519
1803
  createdAt: Date.now()
1520
1804
  };
1521
- console.log(`[ApprovalProxy] \u6536\u5230\u5BA1\u6279\u8BF7\u6C42: ${requestId} (${approvalRequest.toolName})`);
1805
+ console.log(`[ApprovalProxy] ${t("approval.received")}: ${requestId} (${approvalRequest.toolName})`);
1522
1806
  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`);
1807
+ console.log(`[ApprovalProxy] ${t("approval.alwaysAllowPassThrough", { tool: approvalRequest.toolName })}`);
1524
1808
  this.sendJson(res, 200, { decision: "allow" });
1525
1809
  return;
1526
1810
  }
1527
1811
  if (this.yoloSessions.get(approvalRequest.sessionId)) {
1528
- console.log(`[ApprovalProxy] YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C: ${approvalRequest.toolName}`);
1812
+ console.log(`[ApprovalProxy] ${t("approval.yoloAutoAllow")}: ${approvalRequest.toolName}`);
1529
1813
  this.sendJson(res, 200, { decision: "allow" });
1530
1814
  return;
1531
1815
  }
1532
1816
  this.notifyApprovalRequest(approvalRequest);
1533
1817
  const decision = await new Promise((resolve) => {
1534
1818
  const timer = setTimeout(() => {
1535
- console.log(`[ApprovalProxy] \u5BA1\u6279\u8BF7\u6C42 ${requestId} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8`);
1819
+ console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
1536
1820
  this.pendingApprovals.delete(requestId);
1537
1821
  resolve({ decision: "allow" });
1538
1822
  }, 325e3);
@@ -1540,7 +1824,7 @@ var ApprovalProxy = class _ApprovalProxy {
1540
1824
  });
1541
1825
  this.sendJson(res, 200, decision);
1542
1826
  } catch (err) {
1543
- console.error("[ApprovalProxy] \u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25:", err);
1827
+ console.error(`[ApprovalProxy] ${t("approval.processingFailed")}:`, err);
1544
1828
  this.sendJson(res, 200, { decision: "deny", reason: "\u670D\u52A1\u5668\u5904\u7406\u8BF7\u6C42\u5931\u8D25" });
1545
1829
  }
1546
1830
  }
@@ -1558,7 +1842,7 @@ var ApprovalProxy = class _ApprovalProxy {
1558
1842
  const remoteAddress = req.socket.remoteAddress;
1559
1843
  const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
1560
1844
  if (!isLocal) {
1561
- this.sendJson(res, 403, { error: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE" });
1845
+ this.sendJson(res, 403, { error: t("approval.forbidden") });
1562
1846
  return;
1563
1847
  }
1564
1848
  this.sendJson(res, 200, { token: this.token });
@@ -1586,7 +1870,7 @@ var ApprovalProxy = class _ApprovalProxy {
1586
1870
  if (totalSize > MAX_BODY_SIZE) {
1587
1871
  destroyed = true;
1588
1872
  req.destroy();
1589
- return reject(new Error("\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09"));
1873
+ return reject(new Error(t("approval.bodyTooLarge")));
1590
1874
  }
1591
1875
  chunks.push(chunk);
1592
1876
  });
@@ -1596,7 +1880,7 @@ var ApprovalProxy = class _ApprovalProxy {
1596
1880
  const parsed = JSON.parse(raw);
1597
1881
  resolve(parsed);
1598
1882
  } catch {
1599
- reject(new Error("\u65E0\u6548\u7684 JSON body"));
1883
+ reject(new Error(t("approval.invalidJson")));
1600
1884
  }
1601
1885
  });
1602
1886
  req.on("error", (err) => {
@@ -1623,17 +1907,19 @@ var MdnsService = class {
1623
1907
  wsPort;
1624
1908
  httpPort;
1625
1909
  version;
1910
+ token;
1626
1911
  constructor(options) {
1627
1912
  this.wsPort = options.wsPort;
1628
1913
  this.httpPort = options.httpPort;
1629
1914
  this.version = options.version ?? "0.1.0";
1915
+ this.token = options.token ?? "";
1630
1916
  }
1631
1917
  /**
1632
1918
  * 启动 mDNS 广播
1633
1919
  */
1634
1920
  start() {
1635
1921
  if (this.bonjour) {
1636
- console.warn("[MdnsService] \u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D");
1922
+ console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
1637
1923
  return;
1638
1924
  }
1639
1925
  this.bonjour = new import_bonjour_service.default();
@@ -1643,10 +1929,11 @@ var MdnsService = class {
1643
1929
  port: this.wsPort,
1644
1930
  txt: {
1645
1931
  version: this.version,
1646
- httpPort: String(this.httpPort)
1932
+ httpPort: String(this.httpPort),
1933
+ token: this.token
1647
1934
  }
1648
1935
  });
1649
- console.log(`[MdnsService] mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 ${this.wsPort}`);
1936
+ console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
1650
1937
  }
1651
1938
  /**
1652
1939
  * 停止 mDNS 广播
@@ -1654,7 +1941,7 @@ var MdnsService = class {
1654
1941
  stop() {
1655
1942
  if (this.service) {
1656
1943
  this.service.stop?.(() => {
1657
- console.log("[MdnsService] \u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62");
1944
+ console.log(`[MdnsService] ${t("mdns.stopped")}`);
1658
1945
  });
1659
1946
  this.service = null;
1660
1947
  }
@@ -1662,7 +1949,7 @@ var MdnsService = class {
1662
1949
  this.bonjour.destroy();
1663
1950
  this.bonjour = null;
1664
1951
  }
1665
- console.log("[MdnsService] mDNS \u670D\u52A1\u5DF2\u5173\u95ED");
1952
+ console.log(`[MdnsService] ${t("mdns.closed")}`);
1666
1953
  }
1667
1954
  };
1668
1955
 
@@ -1897,6 +2184,8 @@ var NotificationService = class {
1897
2184
  yoloModeState = /* @__PURE__ */ new Map();
1898
2185
  /** 每个会话的最新 assistant 文本消息(用于通知正文预览) */
1899
2186
  latestAssistantText = /* @__PURE__ */ new Map();
2187
+ /** 获取全局待审批总数的回调(跨所有会话) */
2188
+ globalPendingCountProvider = null;
1900
2189
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
1901
2190
  addChannel(id, channel, enabled = true) {
1902
2191
  this.channelMap.set(id, { channel, enabled });
@@ -1930,6 +2219,14 @@ var NotificationService = class {
1930
2219
  removeActivityPushToken(sessionId) {
1931
2220
  this.activityPushChannel?.removeToken(sessionId);
1932
2221
  }
2222
+ /** 设置全局待审批总数提供者 */
2223
+ setGlobalPendingCountProvider(provider) {
2224
+ this.globalPendingCountProvider = provider;
2225
+ }
2226
+ /** 获取全局待审批总数 */
2227
+ getGlobalPendingCount() {
2228
+ return this.globalPendingCountProvider?.() ?? 0;
2229
+ }
1933
2230
  /** 更新会话的 YOLO 模式状态 */
1934
2231
  setYoloMode(sessionId, enabled) {
1935
2232
  this.yoloModeState.set(sessionId, enabled);
@@ -1938,7 +2235,7 @@ var NotificationService = class {
1938
2235
  notifyApproval(request, pendingCount) {
1939
2236
  if (this.yoloModeState.get(request.sessionId)) return;
1940
2237
  const sessionTitle = this.getSessionTitle(request.sessionId);
1941
- const title = pendingCount > 1 ? `${sessionTitle} \u2014 ${pendingCount} \u9879\u5F85\u5BA1\u6279` : sessionTitle;
2238
+ const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
1942
2239
  const body = pendingCount > 1 ? `\u{1F527} \u6700\u65B0: ${request.toolName}: ${request.description}` : `\u{1F527} ${request.toolName}: ${request.description}`;
1943
2240
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
1944
2241
  const dangerLevel = this.getDangerLevel(request.toolName);
@@ -1948,7 +2245,7 @@ var NotificationService = class {
1948
2245
  {
1949
2246
  status: "waitingApproval",
1950
2247
  sessionTitle,
1951
- latestMessage: `${request.toolName}: ${request.description}`,
2248
+ latestMessage: "",
1952
2249
  approvalInfo: {
1953
2250
  requestId: request.id,
1954
2251
  toolName: request.toolName,
@@ -1966,8 +2263,8 @@ var NotificationService = class {
1966
2263
  this.notify({
1967
2264
  title,
1968
2265
  body,
1969
- sound: "Funk",
1970
- badge: pendingCount,
2266
+ sound: "default",
2267
+ badge: this.getGlobalPendingCount(),
1971
2268
  data: {
1972
2269
  type: "approval_request",
1973
2270
  sessionId: request.sessionId,
@@ -1975,6 +2272,37 @@ var NotificationService = class {
1975
2272
  }
1976
2273
  });
1977
2274
  }
2275
+ /** 直接触发提问通知(由 server.ts 在 question_request 事件时调用) */
2276
+ notifyQuestion(request) {
2277
+ const sessionTitle = this.getSessionTitle(request.sessionId);
2278
+ const body = `\u2753 ${request.question.slice(0, 80)}`;
2279
+ if (this.activityPushChannel?.hasToken(request.sessionId)) {
2280
+ const isYoloMode = this.getYoloMode(request.sessionId);
2281
+ this.activityPushChannel.updateActivityWithAlert(
2282
+ request.sessionId,
2283
+ {
2284
+ status: "waitingApproval",
2285
+ sessionTitle,
2286
+ latestMessage: request.question.slice(0, 80),
2287
+ isYoloMode,
2288
+ updatedAt: Date.now()
2289
+ },
2290
+ { title: sessionTitle, body }
2291
+ );
2292
+ return;
2293
+ }
2294
+ this.notify({
2295
+ title: sessionTitle,
2296
+ body,
2297
+ sound: "default",
2298
+ badge: this.getGlobalPendingCount(),
2299
+ data: {
2300
+ type: "question_request",
2301
+ sessionId: request.sessionId,
2302
+ requestId: request.id
2303
+ }
2304
+ });
2305
+ }
1978
2306
  /** 简单的工具危险等级判断 */
1979
2307
  getDangerLevel(toolName) {
1980
2308
  if (toolName === "Bash") return "danger";
@@ -2007,7 +2335,7 @@ var NotificationService = class {
2007
2335
  if (event.status === "idle") {
2008
2336
  const sessionTitle = this.getSessionTitle(event.sessionId);
2009
2337
  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";
2338
+ const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
2011
2339
  const isYoloMode = this.getYoloMode(event.sessionId);
2012
2340
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
2013
2341
  this.activityPushChannel.endActivity(event.sessionId, {
@@ -2021,14 +2349,15 @@ var NotificationService = class {
2021
2349
  this.notify({
2022
2350
  title: sessionTitle,
2023
2351
  body,
2024
- sound: "Glass",
2352
+ sound: "default",
2353
+ badge: this.getGlobalPendingCount(),
2025
2354
  data: { type: "task_complete", sessionId: event.sessionId }
2026
2355
  });
2027
2356
  }
2028
2357
  } else if (event.status === "error") {
2029
2358
  const sessionTitle = this.getSessionTitle(event.sessionId);
2030
2359
  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";
2360
+ const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
2032
2361
  const isYoloMode = this.getYoloMode(event.sessionId);
2033
2362
  if (this.activityPushChannel?.hasToken(event.sessionId)) {
2034
2363
  this.activityPushChannel.endActivity(event.sessionId, {
@@ -2042,7 +2371,8 @@ var NotificationService = class {
2042
2371
  this.notify({
2043
2372
  title: sessionTitle,
2044
2373
  body,
2045
- sound: "Basso",
2374
+ sound: "default",
2375
+ badge: this.getGlobalPendingCount(),
2046
2376
  data: { type: "task_error", sessionId: event.sessionId }
2047
2377
  });
2048
2378
  }
@@ -2114,19 +2444,19 @@ var ExpoNotificationChannel = class {
2114
2444
  }
2115
2445
  addToken(token) {
2116
2446
  this.tokens.add(token);
2117
- console.log(`[ExpoNotificationChannel] \u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2447
+ console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
2118
2448
  }
2119
2449
  removeToken(token) {
2120
2450
  this.tokens.delete(token);
2121
2451
  this.soundPreferences.delete(token);
2122
- console.log(`[ExpoNotificationChannel] \u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: ${this.tokens.size}`);
2452
+ console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
2123
2453
  }
2124
2454
  /** 更新某个 token 的音效偏好 */
2125
2455
  setSoundPreferences(prefs) {
2126
2456
  for (const token of this.tokens) {
2127
2457
  this.soundPreferences.set(token, prefs);
2128
2458
  }
2129
- console.log("[ExpoNotificationChannel] \u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D");
2459
+ console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
2130
2460
  }
2131
2461
  async send(payload) {
2132
2462
  if (this.tokens.size === 0) return;
@@ -2139,17 +2469,18 @@ var ExpoNotificationChannel = class {
2139
2469
  else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
2140
2470
  else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
2141
2471
  }
2472
+ const pushSound = sound === "none" ? null : sound;
2142
2473
  return {
2143
2474
  to,
2144
2475
  title: payload.title,
2145
2476
  body: payload.body,
2146
2477
  badge: payload.badge,
2147
- sound: sound === "none" ? null : sound,
2478
+ sound: pushSound,
2148
2479
  data: payload.data ?? {}
2149
2480
  };
2150
2481
  });
2151
2482
  try {
2152
- console.log("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\uFF0Ctokens:", Array.from(this.tokens));
2483
+ console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
2153
2484
  const res = await fetch(EXPO_PUSH_API, {
2154
2485
  method: "POST",
2155
2486
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -2157,20 +2488,20 @@ var ExpoNotificationChannel = class {
2157
2488
  });
2158
2489
  const body = await res.json();
2159
2490
  if (!res.ok) {
2160
- console.warn("[ExpoNotificationChannel] Expo Push API \u8FD4\u56DE\u9519\u8BEF:", res.status, JSON.stringify(body));
2491
+ console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiError")}`, res.status, JSON.stringify(body));
2161
2492
  } else {
2162
2493
  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));
2494
+ console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
2164
2495
  return;
2165
2496
  }
2166
2497
  for (const ticket of body.data) {
2167
2498
  if (ticket.status === "error") {
2168
- console.error(`[ExpoNotificationChannel] \u63A8\u9001\u5931\u8D25: ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
2499
+ console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
2169
2500
  }
2170
2501
  }
2171
2502
  }
2172
2503
  } catch (err) {
2173
- console.warn("[ExpoNotificationChannel] \u53D1\u9001\u63A8\u9001\u5931\u8D25:", err);
2504
+ console.warn(`[ExpoNotificationChannel] ${t("notification.sendFailed")}`, err);
2174
2505
  }
2175
2506
  }
2176
2507
  };
@@ -2730,6 +3061,13 @@ async function start(opts = {}) {
2730
3061
  HTTP_PORT,
2731
3062
  () => ApprovalProxy.create({ port: HTTP_PORT, token })
2732
3063
  );
3064
+ const unreadSessionIds = /* @__PURE__ */ new Set();
3065
+ notificationService.setGlobalPendingCountProvider(
3066
+ () => approvalProxy.getPendingCount() + unreadSessionIds.size
3067
+ );
3068
+ const broadcastUnreadSessions = () => {
3069
+ wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
3070
+ };
2733
3071
  wsBridge.onConnection(async (ws) => {
2734
3072
  const result = await getProjects();
2735
3073
  if (result.ok) {
@@ -2739,6 +3077,15 @@ async function start(opts = {}) {
2739
3077
  type: "session_list",
2740
3078
  sessions: sessionManager.getActiveSessions()
2741
3079
  });
3080
+ for (const req of approvalProxy.getAllPendingRequests()) {
3081
+ wsBridge.send(ws, { type: "approval_request", request: req });
3082
+ }
3083
+ for (const req of sessionManager.getAllPendingQuestions()) {
3084
+ wsBridge.send(ws, { type: "question_request", request: req });
3085
+ }
3086
+ if (unreadSessionIds.size > 0) {
3087
+ wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
3088
+ }
2742
3089
  });
2743
3090
  wsBridge.onClientEvent(async (event, ws) => {
2744
3091
  try {
@@ -2797,7 +3144,32 @@ async function start(opts = {}) {
2797
3144
  sessions: sessionManager.getActiveSessions()
2798
3145
  });
2799
3146
  const bufferedEvents = sessionManager.getSessionEvents(event.sessionId);
2800
- if (bufferedEvents.length > 0) {
3147
+ if (sessionManager.isBufferTruncated(event.sessionId)) {
3148
+ const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
3149
+ if (projectPath) {
3150
+ const historyResult = await getSessionHistory(projectPath, event.sessionId);
3151
+ if (historyResult.ok && historyResult.value.length > 0) {
3152
+ const merged = [...historyResult.value, ...bufferedEvents];
3153
+ wsBridge.send(ws, {
3154
+ type: "session_history",
3155
+ sessionId: event.sessionId,
3156
+ events: merged
3157
+ });
3158
+ } else if (bufferedEvents.length > 0) {
3159
+ wsBridge.send(ws, {
3160
+ type: "session_history",
3161
+ sessionId: event.sessionId,
3162
+ events: bufferedEvents
3163
+ });
3164
+ }
3165
+ } else if (bufferedEvents.length > 0) {
3166
+ wsBridge.send(ws, {
3167
+ type: "session_history",
3168
+ sessionId: event.sessionId,
3169
+ events: bufferedEvents
3170
+ });
3171
+ }
3172
+ } else if (bufferedEvents.length > 0) {
2801
3173
  wsBridge.send(ws, {
2802
3174
  type: "session_history",
2803
3175
  sessionId: event.sessionId,
@@ -2807,6 +3179,9 @@ async function start(opts = {}) {
2807
3179
  for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
2808
3180
  wsBridge.send(ws, { type: "approval_request", request: req });
2809
3181
  }
3182
+ for (const req of sessionManager.getPendingQuestionsForSession(event.sessionId)) {
3183
+ wsBridge.send(ws, { type: "question_request", request: req });
3184
+ }
2810
3185
  break;
2811
3186
  }
2812
3187
  case "list_projects": {
@@ -2816,7 +3191,7 @@ async function start(opts = {}) {
2816
3191
  } else {
2817
3192
  wsBridge.send(ws, {
2818
3193
  type: "error",
2819
- message: `\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: ${result.error.message}`,
3194
+ message: t("server.listProjectsFailed", { error: result.error.message }),
2820
3195
  code: "PROJECT_LIST_ERROR"
2821
3196
  });
2822
3197
  }
@@ -2842,7 +3217,7 @@ async function start(opts = {}) {
2842
3217
  } else {
2843
3218
  wsBridge.send(ws, {
2844
3219
  type: "error",
2845
- message: `\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: ${histResult.error.message}`,
3220
+ message: t("server.listSessionsFailed", { error: histResult.error.message }),
2846
3221
  code: "PROJECT_SESSIONS_ERROR"
2847
3222
  });
2848
3223
  }
@@ -2853,7 +3228,7 @@ async function start(opts = {}) {
2853
3228
  if (!historyResult.ok) {
2854
3229
  wsBridge.send(ws, {
2855
3230
  type: "error",
2856
- message: `\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: ${historyResult.error.message}`,
3231
+ message: t("server.readHistoryFailed", { error: historyResult.error.message }),
2857
3232
  code: "SESSION_HISTORY_ERROR",
2858
3233
  sessionId: event.sessionId
2859
3234
  });
@@ -2878,7 +3253,7 @@ async function start(opts = {}) {
2878
3253
  }
2879
3254
  case "suggest_next_prompt": {
2880
3255
  const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
2881
- let context = "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09";
3256
+ let context = t("server.noHistory");
2882
3257
  if (historyResult.ok && historyResult.value.length > 0) {
2883
3258
  const recent = historyResult.value.slice(-10);
2884
3259
  context = recent.map((e) => {
@@ -2887,7 +3262,8 @@ async function start(opts = {}) {
2887
3262
  return `Assistant: ${text.substring(0, 300)}`;
2888
3263
  }
2889
3264
  if (e.type === "user") {
2890
- const text = e.message.content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
3265
+ const content = e.message.content;
3266
+ const text = typeof content === "string" ? content : content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
2891
3267
  return text ? `User: ${text.substring(0, 300)}` : null;
2892
3268
  }
2893
3269
  return null;
@@ -2928,6 +3304,9 @@ async function start(opts = {}) {
2928
3304
  }
2929
3305
  case "viewing_session": {
2930
3306
  wsBridge.setViewingSession(ws, event.sessionId);
3307
+ if (unreadSessionIds.delete(event.sessionId)) {
3308
+ broadcastUnreadSessions();
3309
+ }
2931
3310
  break;
2932
3311
  }
2933
3312
  case "left_session": {
@@ -2941,14 +3320,14 @@ async function start(opts = {}) {
2941
3320
  default: {
2942
3321
  wsBridge.send(ws, {
2943
3322
  type: "error",
2944
- message: `\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: ${event.type}`,
3323
+ message: t("server.unknownEvent", { type: event.type }),
2945
3324
  code: "UNKNOWN_EVENT"
2946
3325
  });
2947
3326
  }
2948
3327
  }
2949
3328
  } catch (err) {
2950
3329
  const message = err instanceof Error ? err.message : String(err);
2951
- console.error("[Server] \u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38:", message);
3330
+ console.error(`[Server] ${t("server.clientEventError")}:`, message);
2952
3331
  const errorCodeMap = {
2953
3332
  create_session: "SESSION_CREATE_ERROR",
2954
3333
  send_message: "SEND_MESSAGE_ERROR",
@@ -2964,10 +3343,16 @@ async function start(opts = {}) {
2964
3343
  });
2965
3344
  sessionManager.onEvent((event) => {
2966
3345
  wsBridge.broadcast(event);
3346
+ if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
3347
+ if (!wsBridge.isViewingSession(event.sessionId)) {
3348
+ unreadSessionIds.add(event.sessionId);
3349
+ broadcastUnreadSessions();
3350
+ }
3351
+ }
2967
3352
  });
2968
3353
  wsBridge.onDisconnect(() => {
2969
3354
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
2970
- approvalProxy.approveAll("\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00");
3355
+ approvalProxy.approveAll(t("server.phoneDisconnected"));
2971
3356
  }
2972
3357
  });
2973
3358
  approvalProxy.onApprovalRequest((request) => {
@@ -2983,52 +3368,81 @@ async function start(opts = {}) {
2983
3368
  if (!approvalProxy.isPending(request.id)) return;
2984
3369
  if (wsBridge.isViewingSession(request.sessionId)) return;
2985
3370
  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`);
3371
+ console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
2987
3372
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
2988
3373
  notificationService.notifyApproval(request, pendingCount);
2989
3374
  }, 6e4);
2990
3375
  });
3376
+ sessionManager.onEvent((event) => {
3377
+ if (event.type !== "question_request") return;
3378
+ const { request } = event;
3379
+ setTimeout(() => {
3380
+ if (!sessionManager.isQuestionPending(request.id)) return;
3381
+ if (wsBridge.isViewingSession(request.sessionId)) return;
3382
+ if (wsBridge.getConnectionCount() > 0) return;
3383
+ notificationService.notifyQuestion(request);
3384
+ }, 5e3);
3385
+ setTimeout(() => {
3386
+ if (!sessionManager.isQuestionPending(request.id)) return;
3387
+ if (wsBridge.isViewingSession(request.sessionId)) return;
3388
+ if (wsBridge.getConnectionCount() > 0) return;
3389
+ console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
3390
+ notificationService.notifyQuestion(request);
3391
+ }, 6e4);
3392
+ });
2991
3393
  approvalProxy.setStatusInfoProvider(() => ({
2992
3394
  connections: wsBridge.getConnectionCount(),
2993
3395
  activeSessions: sessionManager.getActiveSessions().length
2994
3396
  }));
2995
- const mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT });
2996
- mdnsService.start();
3397
+ let mdnsService = null;
3398
+ const startMdns = () => {
3399
+ if (mdnsService) return;
3400
+ mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
3401
+ mdnsService.start();
3402
+ };
3403
+ const stopMdns = () => {
3404
+ if (!mdnsService) return;
3405
+ mdnsService.stop();
3406
+ mdnsService = null;
3407
+ };
3408
+ if (opts.enableAutoConnect !== false) {
3409
+ startMdns();
3410
+ }
2997
3411
  const hookInstaller = new HookInstaller();
2998
3412
  try {
2999
3413
  const installed = await hookInstaller.isInstalled();
3000
3414
  if (!installed) {
3001
3415
  await hookInstaller.install();
3002
- console.log("[Server] Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code");
3416
+ console.log(`[Server] ${t("server.hookInstalled")}`);
3003
3417
  } else {
3004
- console.log("[Server] Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5");
3418
+ console.log(`[Server] ${t("server.hookExists")}`);
3005
3419
  }
3006
3420
  } catch (err) {
3007
3421
  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");
3422
+ console.log(`[Server] ${t("server.hookContinue")}`);
3009
3423
  }
3010
3424
  const stop = async () => {
3011
- console.log("[Server] \u6B63\u5728\u4F18\u96C5\u5173\u95ED...");
3425
+ console.log(`[Server] ${t("server.shuttingDown")}`);
3012
3426
  const errors = [];
3013
3427
  const attempt = async (fn, label) => {
3014
3428
  try {
3015
3429
  await fn();
3016
3430
  } catch (err) {
3017
- console.error(`[Server] \u5173\u95ED ${label} \u51FA\u9519:`, err);
3431
+ console.error(`[Server] ${t("server.shutdownComponentError", { label })}:`, err);
3018
3432
  errors.push(err);
3019
3433
  }
3020
3434
  };
3021
- await attempt(() => mdnsService.stop(), "mDNS");
3435
+ await attempt(() => stopMdns(), "mDNS");
3022
3436
  await attempt(() => wsBridge.close(), "WebSocket");
3023
3437
  await attempt(() => approvalProxy.close(), "ApprovalProxy");
3024
3438
  await attempt(() => sessionManager.destroy(), "SessionManager");
3025
3439
  await attempt(() => notificationService.destroy(), "NotificationService");
3026
3440
  await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
3027
3441
  if (errors.length > 0) {
3028
- console.error(`[Server] \u5173\u95ED\u5B8C\u6210\uFF0C${errors.length} \u4E2A\u9519\u8BEF`);
3442
+ console.error(`[Server] ${t("server.shutdownWithErrors", { count: errors.length })}`);
3029
3443
  throw errors[0];
3030
3444
  }
3031
- console.log("[Server] \u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED");
3445
+ console.log(`[Server] ${t("server.shutdownComplete")}`);
3032
3446
  };
3033
3447
  return {
3034
3448
  token,
@@ -3039,7 +3453,14 @@ async function start(opts = {}) {
3039
3453
  stop,
3040
3454
  setMacNotification: (enabled) => notificationService.setChannelEnabled("mac", enabled),
3041
3455
  setExpoPush: (enabled) => notificationService.setChannelEnabled("expo", enabled),
3042
- onServerEvent: (cb) => sessionManager.onEvent(cb)
3456
+ onServerEvent: (cb) => sessionManager.onEvent(cb),
3457
+ setAutoConnect: (enabled) => {
3458
+ if (enabled) {
3459
+ startMdns();
3460
+ } else {
3461
+ stopMdns();
3462
+ }
3463
+ }
3043
3464
  };
3044
3465
  }
3045
3466
 
@@ -3047,10 +3468,11 @@ async function start(opts = {}) {
3047
3468
  var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
3048
3469
  async function main() {
3049
3470
  console.log("=".repeat(50));
3050
- console.log(" Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3");
3471
+ console.log(t("startup.banner"));
3051
3472
  console.log("=".repeat(50));
3052
3473
  console.log();
3053
- const server = await start();
3474
+ const enableAutoConnect = process.env.SESSIX_AUTO_CONNECT !== "false";
3475
+ const server = await start({ enableAutoConnect });
3054
3476
  const localIp = getLocalIp();
3055
3477
  console.log("-".repeat(50));
3056
3478
  console.log(` WebSocket \u7AEF\u53E3: ${server.wsPort}`);
@@ -3072,12 +3494,19 @@ async function main() {
3072
3494
  }
3073
3495
  console.log();
3074
3496
  const qrUrl = buildQrUrl(localIp, server.wsPort, server.token);
3075
- console.log(" \u626B\u7801\u914D\u5BF9\uFF1A");
3497
+ console.log(t("startup.scanToPair"));
3076
3498
  import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
3077
3499
  qr.split("\n").forEach((line) => console.log(` ${line}`));
3078
3500
  });
3079
3501
  console.log();
3080
- console.log(" \u7B49\u5F85\u624B\u673A\u8FDE\u63A5...");
3502
+ if (enableAutoConnect) {
3503
+ console.log(` \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5`);
3504
+ console.log(` \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server`);
3505
+ } else {
3506
+ console.log(` \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5`);
3507
+ }
3508
+ console.log();
3509
+ console.log(t("startup.waitingConnection"));
3081
3510
  console.log();
3082
3511
  const shutdown = async (signal) => {
3083
3512
  console.log(`