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/index.js +627 -154
- package/dist/server.d.ts +4 -0
- package/dist/server.js +605 -138
- package/package.json +11 -5
package/dist/index.js
CHANGED
|
@@ -26,6 +26,254 @@ 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
|
+
wsPort: " WebSocket \u7AEF\u53E3: {{port}}",
|
|
36
|
+
httpPort: " HTTP \u5BA1\u6279\u7AEF\u53E3: {{port}}",
|
|
37
|
+
tokenDisabled: " \u8FDE\u63A5 Token: (\u5DF2\u7981\u7528\uFF0C\u5F00\u53D1\u6A21\u5F0F)",
|
|
38
|
+
token: " \u8FDE\u63A5 Token: {{token}}",
|
|
39
|
+
wsAddress: " WebSocket \u5730\u5740: ws://{{ip}}:{{port}}",
|
|
40
|
+
wsAddressWithToken: " WebSocket \u5730\u5740: ws://{{ip}}:{{port}}?token={{token}}",
|
|
41
|
+
healthCheck: " \u5065\u5EB7\u68C0\u67E5: http://localhost:{{port}}/health",
|
|
42
|
+
devMode: " [\u5F00\u53D1\u6A21\u5F0F] \u65E0\u9700 Token\uFF0C\u624B\u673A\u7AEF\u53EA\u9700\u8F93\u5165 IP:\u7AEF\u53E3 \u5373\u53EF\u8FDE\u63A5",
|
|
43
|
+
autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
|
|
44
|
+
autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
45
|
+
autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
|
|
46
|
+
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
47
|
+
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
48
|
+
shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
|
|
49
|
+
startFailed: "\u542F\u52A8\u5931\u8D25:"
|
|
50
|
+
},
|
|
51
|
+
server: {
|
|
52
|
+
listProjectsFailed: "\u83B7\u53D6\u9879\u76EE\u5217\u8868\u5931\u8D25: {{error}}",
|
|
53
|
+
listSessionsFailed: "\u83B7\u53D6\u9879\u76EE\u4F1A\u8BDD\u5931\u8D25: {{error}}",
|
|
54
|
+
readHistoryFailed: "\u8BFB\u53D6\u4F1A\u8BDD\u5386\u53F2\u5931\u8D25: {{error}}",
|
|
55
|
+
noHistory: "\uFF08\u6682\u65E0\u5BF9\u8BDD\u5386\u53F2\uFF09",
|
|
56
|
+
unknownEvent: "\u672A\u77E5\u7684\u4E8B\u4EF6\u7C7B\u578B: {{type}}",
|
|
57
|
+
clientEventError: "\u5904\u7406\u5BA2\u6237\u7AEF\u4E8B\u4EF6\u5F02\u5E38",
|
|
58
|
+
phoneDisconnected: "\u624B\u673A\u7AEF\u5DF2\u65AD\u5F00",
|
|
59
|
+
approvalRetry: "\u5BA1\u6279\u8BF7\u6C42 {{id}} 60\u79D2\u672A\u5904\u7406\uFF0C\u91CD\u8BD5\u63A8\u9001",
|
|
60
|
+
hookInstalled: "Sessix hook \u5DF2\u5B89\u88C5\u5230 Claude Code",
|
|
61
|
+
hookExists: "Sessix hook \u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7\u5B89\u88C5",
|
|
62
|
+
hookContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08hook \u529F\u80FD\u53EF\u80FD\u4E0D\u53EF\u7528\uFF09",
|
|
63
|
+
hookInstallFailed: "Hook \u5B89\u88C5\u5931\u8D25:",
|
|
64
|
+
shuttingDown: "\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
65
|
+
shutdownComponentError: "\u5173\u95ED {{label}} \u51FA\u9519",
|
|
66
|
+
shutdownWithErrors: "\u5173\u95ED\u5B8C\u6210\uFF0C{{count}} \u4E2A\u9519\u8BEF",
|
|
67
|
+
shutdownComplete: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED",
|
|
68
|
+
portInUse: "\u7AEF\u53E3 {{port}} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u91CA\u653E\u65E7\u8FDB\u7A0B...",
|
|
69
|
+
restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
|
|
70
|
+
activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
|
|
71
|
+
activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
|
|
72
|
+
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
|
|
73
|
+
},
|
|
74
|
+
ws: {
|
|
75
|
+
started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
|
|
76
|
+
serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF"
|
|
77
|
+
},
|
|
78
|
+
mdns: {
|
|
79
|
+
alreadyRunning: "\u670D\u52A1\u5DF2\u5728\u8FD0\u884C\u4E2D",
|
|
80
|
+
started: "mDNS \u5E7F\u64AD\u5DF2\u542F\u52A8: _sessix._tcp \u7AEF\u53E3 {{port}}",
|
|
81
|
+
stopped: "\u670D\u52A1\u5E7F\u64AD\u5DF2\u505C\u6B62",
|
|
82
|
+
closed: "mDNS \u670D\u52A1\u5DF2\u5173\u95ED"
|
|
83
|
+
},
|
|
84
|
+
approval: {
|
|
85
|
+
httpStarted: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
|
|
86
|
+
serverError: "\u670D\u52A1\u8FD0\u884C\u9519\u8BEF",
|
|
87
|
+
yoloMode: "YOLO \u6A21\u5F0F{{status}}",
|
|
88
|
+
yoloEnabled: "\u5DF2\u542F\u7528",
|
|
89
|
+
yoloDisabled: "\u5DF2\u5173\u95ED",
|
|
90
|
+
requestNotFound: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u4E0D\u5B58\u5728\u6216\u5DF2\u8D85\u65F6",
|
|
91
|
+
requestProcessed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u5904\u7406",
|
|
92
|
+
alwaysAllowWritten: "\u5DF2\u5C06 {{entry}} \u5199\u5165 {{label}}",
|
|
93
|
+
settingsWriteFailed: "\u5199\u5165 settings.json \u5931\u8D25",
|
|
94
|
+
autoAllowed: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u81EA\u52A8\u5141\u8BB8{{reason}}",
|
|
95
|
+
serverClosed: "\u670D\u52A1\u5668\u5DF2\u5173\u95ED",
|
|
96
|
+
httpClosed: "HTTP \u5BA1\u6279\u670D\u52A1\u5DF2\u5173\u95ED",
|
|
97
|
+
received: "\u6536\u5230\u5BA1\u6279\u8BF7\u6C42",
|
|
98
|
+
alwaysAllowPassThrough: "{{tool}} \u5DF2\u88AB\u59CB\u7EC8\u5141\u8BB8\uFF0C\u76F4\u63A5\u653E\u884C\uFF08\u4E0D\u901A\u77E5\uFF09",
|
|
99
|
+
yoloAutoAllow: "YOLO \u6A21\u5F0F\uFF0C\u81EA\u52A8\u653E\u884C",
|
|
100
|
+
timeout: "\u5BA1\u6279\u8BF7\u6C42 {{id}} \u5DF2\u8D85\u65F6\uFF0C\u9ED8\u8BA4\u5141\u8BB8",
|
|
101
|
+
processingFailed: "\u5904\u7406\u5BA1\u6279\u8BF7\u6C42\u5931\u8D25",
|
|
102
|
+
forbidden: "Forbidden: \u4EC5\u5141\u8BB8\u672C\u673A\u8BBF\u95EE",
|
|
103
|
+
bodyTooLarge: "\u8BF7\u6C42 body \u8FC7\u5927\uFF08\u8D85\u8FC7 1MB\uFF09",
|
|
104
|
+
invalidJson: "\u65E0\u6548\u7684 JSON body"
|
|
105
|
+
},
|
|
106
|
+
notification: {
|
|
107
|
+
tokenRegistered: "\u5DF2\u6CE8\u518C push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
|
|
108
|
+
tokenRemoved: "\u5DF2\u79FB\u9664 push token\uFF0C\u5F53\u524D\u8BBE\u5907\u6570: {{count}}",
|
|
109
|
+
soundPrefsUpdated: "\u5DF2\u66F4\u65B0\u97F3\u6548\u504F\u597D",
|
|
110
|
+
sendingPush: "\u53D1\u9001\u63A8\u9001\uFF0Ctokens:",
|
|
111
|
+
pushApiError: "Expo Push API \u8FD4\u56DE\u9519\u8BEF:",
|
|
112
|
+
pushApiFormatError: "Expo Push API \u54CD\u5E94\u683C\u5F0F\u5F02\u5E38\uFF0C\u7F3A\u5C11 data \u6570\u7EC4:",
|
|
113
|
+
pushFailed: "\u63A8\u9001\u5931\u8D25:",
|
|
114
|
+
sendFailed: "\u53D1\u9001\u63A8\u9001\u5931\u8D25:",
|
|
115
|
+
pendingApprovals: "{{title}} \u2014 {{count}} \u9879\u5F85\u5BA1\u6279",
|
|
116
|
+
taskComplete: "\u5DF2\u5B8C\u6210\uFF0C\u7B49\u5F85\u4E0B\u4E00\u6B65\u6307\u4EE4",
|
|
117
|
+
taskError: "\u6267\u884C\u51FA\u9519\uFF0C\u8BF7\u67E5\u770B\u8BE6\u60C5",
|
|
118
|
+
questionRetry: "\u63D0\u95EE {{id}} 60\u79D2\u672A\u56DE\u7B54\uFF0C\u91CD\u8BD5\u63A8\u9001"
|
|
119
|
+
},
|
|
120
|
+
tray: {
|
|
121
|
+
tooltip: "Sessix \u2014 AI \u7F16\u7A0B\u79FB\u52A8\u6307\u6325\u4E2D\u5FC3"
|
|
122
|
+
},
|
|
123
|
+
watcher: {
|
|
124
|
+
readError: "\u8BFB\u53D6\u5F02\u5E38 {{sessionId}}",
|
|
125
|
+
startWatching: "\u5F00\u59CB\u76D1\u542C",
|
|
126
|
+
stopWatching: "\u505C\u6B62\u76D1\u542C"
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// src/i18n/locales/en.ts
|
|
131
|
+
var en = {
|
|
132
|
+
startup: {
|
|
133
|
+
banner: " Sessix \u2014 AI Coding Mobile Command Center",
|
|
134
|
+
scanToPair: " Scan to pair:",
|
|
135
|
+
waitingConnection: " Waiting for phone connection...",
|
|
136
|
+
wsPort: " WebSocket port: {{port}}",
|
|
137
|
+
httpPort: " HTTP approval port: {{port}}",
|
|
138
|
+
tokenDisabled: " Token: (disabled, dev mode)",
|
|
139
|
+
token: " Token: {{token}}",
|
|
140
|
+
wsAddress: " WebSocket URL: ws://{{ip}}:{{port}}",
|
|
141
|
+
wsAddressWithToken: " WebSocket URL: ws://{{ip}}:{{port}}?token={{token}}",
|
|
142
|
+
healthCheck: " Health check: http://localhost:{{port}}/health",
|
|
143
|
+
devMode: " [Dev mode] No token required, just enter IP:port on your phone",
|
|
144
|
+
autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
|
|
145
|
+
autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
146
|
+
autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
|
|
147
|
+
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
148
|
+
goodbye: "All services closed, goodbye!",
|
|
149
|
+
shutdownError: "Shutdown error:",
|
|
150
|
+
startFailed: "Startup failed:"
|
|
151
|
+
},
|
|
152
|
+
server: {
|
|
153
|
+
listProjectsFailed: "Failed to list projects: {{error}}",
|
|
154
|
+
listSessionsFailed: "Failed to list project sessions: {{error}}",
|
|
155
|
+
readHistoryFailed: "Failed to read session history: {{error}}",
|
|
156
|
+
noHistory: "(No conversation history)",
|
|
157
|
+
unknownEvent: "Unknown event type: {{type}}",
|
|
158
|
+
clientEventError: "Client event handling error",
|
|
159
|
+
phoneDisconnected: "Phone disconnected",
|
|
160
|
+
approvalRetry: "Approval request {{id}} not handled in 60s, retrying push",
|
|
161
|
+
hookInstalled: "Sessix hook installed to Claude Code",
|
|
162
|
+
hookExists: "Sessix hook already exists, skipping installation",
|
|
163
|
+
hookContinue: "Continuing startup (hook functionality may be unavailable)",
|
|
164
|
+
hookInstallFailed: "Hook installation failed:",
|
|
165
|
+
shuttingDown: "Graceful shutdown in progress...",
|
|
166
|
+
shutdownComponentError: "Error closing {{label}}",
|
|
167
|
+
shutdownWithErrors: "Shutdown complete, {{count}} error(s)",
|
|
168
|
+
shutdownComplete: "All services closed",
|
|
169
|
+
portInUse: "Port {{port}} in use, attempting to release old process...",
|
|
170
|
+
restarting: "Restarting {{label}}...",
|
|
171
|
+
activityPushEnabled: "ActivityKit Push enabled",
|
|
172
|
+
activityPushFailed: "ActivityKit Push init failed:",
|
|
173
|
+
activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
|
|
174
|
+
},
|
|
175
|
+
ws: {
|
|
176
|
+
started: "WebSocket server started on port {{port}}",
|
|
177
|
+
serverError: "Server runtime error"
|
|
178
|
+
},
|
|
179
|
+
mdns: {
|
|
180
|
+
alreadyRunning: "Service is already running",
|
|
181
|
+
started: "mDNS broadcast started: _sessix._tcp port {{port}}",
|
|
182
|
+
stopped: "Service broadcast stopped",
|
|
183
|
+
closed: "mDNS service closed"
|
|
184
|
+
},
|
|
185
|
+
approval: {
|
|
186
|
+
httpStarted: "HTTP approval server started on port {{port}}",
|
|
187
|
+
serverError: "Server runtime error",
|
|
188
|
+
yoloMode: "YOLO mode {{status}}",
|
|
189
|
+
yoloEnabled: "enabled",
|
|
190
|
+
yoloDisabled: "disabled",
|
|
191
|
+
requestNotFound: "Approval request {{id}} not found or timed out",
|
|
192
|
+
requestProcessed: "Approval request {{id}} processed",
|
|
193
|
+
alwaysAllowWritten: "Written {{entry}} to {{label}}",
|
|
194
|
+
settingsWriteFailed: "Failed to write settings.json",
|
|
195
|
+
autoAllowed: "Approval request {{id}} auto-allowed{{reason}}",
|
|
196
|
+
serverClosed: "Server closed",
|
|
197
|
+
httpClosed: "HTTP approval server closed",
|
|
198
|
+
received: "Approval request received",
|
|
199
|
+
alwaysAllowPassThrough: "{{tool}} is always-allowed, passing through (no notification)",
|
|
200
|
+
yoloAutoAllow: "YOLO mode, auto-allowing",
|
|
201
|
+
timeout: "Approval request {{id}} timed out, default allowed",
|
|
202
|
+
processingFailed: "Approval request processing failed",
|
|
203
|
+
forbidden: "Forbidden: localhost access only",
|
|
204
|
+
bodyTooLarge: "Request body too large (>1MB)",
|
|
205
|
+
invalidJson: "Invalid JSON body"
|
|
206
|
+
},
|
|
207
|
+
notification: {
|
|
208
|
+
tokenRegistered: "Push token registered, devices: {{count}}",
|
|
209
|
+
tokenRemoved: "Push token removed, devices: {{count}}",
|
|
210
|
+
soundPrefsUpdated: "Sound preferences updated",
|
|
211
|
+
sendingPush: "Sending push, tokens:",
|
|
212
|
+
pushApiError: "Expo Push API returned error:",
|
|
213
|
+
pushApiFormatError: "Expo Push API response format error, missing data array:",
|
|
214
|
+
pushFailed: "Push failed:",
|
|
215
|
+
sendFailed: "Send push failed:",
|
|
216
|
+
pendingApprovals: "{{title}} \u2014 {{count}} pending approval(s)",
|
|
217
|
+
taskComplete: "Completed, awaiting next instruction",
|
|
218
|
+
taskError: "Execution error, check details",
|
|
219
|
+
questionRetry: "Question {{id}} not answered in 60s, retrying push"
|
|
220
|
+
},
|
|
221
|
+
tray: {
|
|
222
|
+
tooltip: "Sessix \u2014 AI Coding Mobile Command Center"
|
|
223
|
+
},
|
|
224
|
+
watcher: {
|
|
225
|
+
readError: "Read error {{sessionId}}",
|
|
226
|
+
startWatching: "Start watching",
|
|
227
|
+
stopWatching: "Stop watching"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// src/i18n/index.ts
|
|
232
|
+
var locales = { zh, en };
|
|
233
|
+
function detectLocale() {
|
|
234
|
+
const explicit = process.env.SESSIX_LANG;
|
|
235
|
+
if (explicit && explicit in locales) return explicit;
|
|
236
|
+
try {
|
|
237
|
+
const raw = process.env.LANG || process.env.LC_ALL || process.env.LC_MESSAGES || "";
|
|
238
|
+
if (raw.startsWith("zh")) return "zh";
|
|
239
|
+
} catch {
|
|
240
|
+
}
|
|
241
|
+
return "en";
|
|
242
|
+
}
|
|
243
|
+
var currentLocale = detectLocale();
|
|
244
|
+
var currentMessages = locales[currentLocale] ?? en;
|
|
245
|
+
function t(key, params) {
|
|
246
|
+
const parts = key.split(".");
|
|
247
|
+
let val = currentMessages;
|
|
248
|
+
for (const p of parts) {
|
|
249
|
+
if (val && typeof val === "object") {
|
|
250
|
+
val = val[p];
|
|
251
|
+
} else {
|
|
252
|
+
val = void 0;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (typeof val !== "string") {
|
|
257
|
+
let fallback = en;
|
|
258
|
+
for (const p of parts) {
|
|
259
|
+
if (fallback && typeof fallback === "object") {
|
|
260
|
+
fallback = fallback[p];
|
|
261
|
+
} else {
|
|
262
|
+
fallback = void 0;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
val = typeof fallback === "string" ? fallback : key;
|
|
267
|
+
}
|
|
268
|
+
let result = val;
|
|
269
|
+
if (params) {
|
|
270
|
+
for (const [k, v] of Object.entries(params)) {
|
|
271
|
+
result = result.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
}
|
|
276
|
+
|
|
29
277
|
// src/server.ts
|
|
30
278
|
var import_uuid4 = require("uuid");
|
|
31
279
|
var import_promises4 = require("fs/promises");
|
|
@@ -95,12 +343,12 @@ var ProcessProvider = class {
|
|
|
95
343
|
session.pid = proc.pid;
|
|
96
344
|
this.activeSessions.set(sessionId, { session, process: proc, model, permissionMode, effort });
|
|
97
345
|
proc.on("error", (err) => {
|
|
98
|
-
console.error(`[ProcessProvider]
|
|
346
|
+
console.error(`[ProcessProvider] Session ${sessionId} process error:`, err.message);
|
|
99
347
|
this.activeSessions.delete(sessionId);
|
|
100
348
|
const syntheticResult = {
|
|
101
349
|
type: "result",
|
|
102
350
|
subtype: "error",
|
|
103
|
-
result:
|
|
351
|
+
result: `Process spawn failed: ${err.message}`,
|
|
104
352
|
session_id: sessionId,
|
|
105
353
|
duration_ms: 0,
|
|
106
354
|
is_error: true,
|
|
@@ -153,15 +401,17 @@ var ProcessProvider = class {
|
|
|
153
401
|
async sendMessage(sessionId, message, permissionMode, images) {
|
|
154
402
|
const entry = this.activeSessions.get(sessionId);
|
|
155
403
|
if (!entry) {
|
|
156
|
-
throw new Error(
|
|
404
|
+
throw new Error(`Session ${sessionId} not found or already ended`);
|
|
157
405
|
}
|
|
158
406
|
const modeChanged = permissionMode != null && permissionMode !== (entry.permissionMode ?? "default");
|
|
159
407
|
if (!modeChanged && entry.process.exitCode === null && entry.process.signalCode === null && !entry.process.stdin?.destroyed) {
|
|
408
|
+
entry.session.status = "running";
|
|
409
|
+
entry.session.lastActiveAt = Date.now();
|
|
160
410
|
this.writeUserMessage(entry.process, message, sessionId, images);
|
|
161
411
|
return;
|
|
162
412
|
}
|
|
163
413
|
if (modeChanged) {
|
|
164
|
-
console.log(`[ProcessProvider]
|
|
414
|
+
console.log(`[ProcessProvider] Session ${sessionId}: permission mode change ${entry.permissionMode ?? "default"} \u2192 ${permissionMode}, respawn`);
|
|
165
415
|
if (entry.process.exitCode === null && entry.process.signalCode === null) {
|
|
166
416
|
try {
|
|
167
417
|
entry.process.stdin?.end();
|
|
@@ -170,7 +420,7 @@ var ProcessProvider = class {
|
|
|
170
420
|
entry.process.kill("SIGTERM");
|
|
171
421
|
}
|
|
172
422
|
} else {
|
|
173
|
-
console.log(`[ProcessProvider]
|
|
423
|
+
console.log(`[ProcessProvider] Session ${sessionId}: process exited, respawning`);
|
|
174
424
|
}
|
|
175
425
|
const savedPendingQuestion = entry.pendingQuestion;
|
|
176
426
|
const newMode = permissionMode ?? entry.permissionMode;
|
|
@@ -183,12 +433,12 @@ var ProcessProvider = class {
|
|
|
183
433
|
entry.permissionMode = newMode;
|
|
184
434
|
entry.pendingQuestion = savedPendingQuestion;
|
|
185
435
|
proc.on("error", (err) => {
|
|
186
|
-
console.error(`[ProcessProvider]
|
|
436
|
+
console.error(`[ProcessProvider] Session ${sessionId} sendMessage process error:`, err.message);
|
|
187
437
|
this.activeSessions.delete(sessionId);
|
|
188
438
|
const syntheticResult = {
|
|
189
439
|
type: "result",
|
|
190
440
|
subtype: "error",
|
|
191
|
-
result:
|
|
441
|
+
result: `Failed to send message: ${err.message}`,
|
|
192
442
|
session_id: sessionId,
|
|
193
443
|
duration_ms: 0,
|
|
194
444
|
is_error: true,
|
|
@@ -279,16 +529,16 @@ var ProcessProvider = class {
|
|
|
279
529
|
parent_tool_use_id: null
|
|
280
530
|
});
|
|
281
531
|
if (!proc.stdin || proc.stdin.destroyed) {
|
|
282
|
-
console.error(`[ProcessProvider] stdin
|
|
532
|
+
console.error(`[ProcessProvider] stdin unavailable, message lost`);
|
|
283
533
|
if (sessionId) {
|
|
284
|
-
this.emitWriteError(sessionId, "
|
|
534
|
+
this.emitWriteError(sessionId, "Process stdin closed, message not delivered");
|
|
285
535
|
}
|
|
286
536
|
return;
|
|
287
537
|
}
|
|
288
538
|
proc.stdin.write(payload + "\n", (err) => {
|
|
289
539
|
if (err && sessionId) {
|
|
290
|
-
console.error(`[ProcessProvider]
|
|
291
|
-
this.emitWriteError(sessionId,
|
|
540
|
+
console.error(`[ProcessProvider] Session ${sessionId} stdin write failed:`, err.message);
|
|
541
|
+
this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
|
|
292
542
|
}
|
|
293
543
|
});
|
|
294
544
|
}
|
|
@@ -312,7 +562,7 @@ var ProcessProvider = class {
|
|
|
312
562
|
*/
|
|
313
563
|
attachStdoutListener(sessionId, proc) {
|
|
314
564
|
if (!proc.stdout) {
|
|
315
|
-
console.warn(`[ProcessProvider]
|
|
565
|
+
console.warn(`[ProcessProvider] Session ${sessionId}: stdout unavailable`);
|
|
316
566
|
return;
|
|
317
567
|
}
|
|
318
568
|
const rl = (0, import_readline.createInterface)({
|
|
@@ -350,7 +600,7 @@ var ProcessProvider = class {
|
|
|
350
600
|
this.emitter.emit(this.getEventName(sessionId), event);
|
|
351
601
|
} else {
|
|
352
602
|
console.warn(
|
|
353
|
-
`[ProcessProvider]
|
|
603
|
+
`[ProcessProvider] Session ${sessionId}: failed to parse line: ${trimmed.substring(0, 100)}`
|
|
354
604
|
);
|
|
355
605
|
}
|
|
356
606
|
});
|
|
@@ -363,7 +613,7 @@ var ProcessProvider = class {
|
|
|
363
613
|
proc.stderr.on("data", (data) => {
|
|
364
614
|
const text = data.toString().trim();
|
|
365
615
|
if (text) {
|
|
366
|
-
console.error(`[ProcessProvider]
|
|
616
|
+
console.error(`[ProcessProvider] Session ${sessionId} stderr: ${text}`);
|
|
367
617
|
}
|
|
368
618
|
});
|
|
369
619
|
}
|
|
@@ -394,7 +644,7 @@ var ProcessProvider = class {
|
|
|
394
644
|
entry.session.status = isNormal ? "idle" : "error";
|
|
395
645
|
if (!isNormal) {
|
|
396
646
|
console.error(
|
|
397
|
-
`[ProcessProvider]
|
|
647
|
+
`[ProcessProvider] Session ${sessionId}: process exited abnormally code=${code} signal=${signal}`
|
|
398
648
|
);
|
|
399
649
|
}
|
|
400
650
|
const syntheticResult = {
|
|
@@ -402,7 +652,7 @@ var ProcessProvider = class {
|
|
|
402
652
|
subtype: isNormal ? "success" : "error",
|
|
403
653
|
session_id: sessionId,
|
|
404
654
|
is_error: !isNormal,
|
|
405
|
-
result: isNormal ? "" :
|
|
655
|
+
result: isNormal ? "" : `Process exited code=${code} signal=${signal}`,
|
|
406
656
|
duration_ms: 0,
|
|
407
657
|
num_turns: 0
|
|
408
658
|
};
|
|
@@ -450,7 +700,7 @@ var ProcessProvider = class {
|
|
|
450
700
|
* 使用 --output-format text 做一次性调用,返回纯文本结果。
|
|
451
701
|
*/
|
|
452
702
|
async generateSuggestion(context) {
|
|
453
|
-
const prompt =
|
|
703
|
+
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):
|
|
454
704
|
|
|
455
705
|
${context}`;
|
|
456
706
|
return new Promise((resolve, reject) => {
|
|
@@ -470,7 +720,7 @@ ${context}`;
|
|
|
470
720
|
if (code === 0) {
|
|
471
721
|
resolve(output.trim());
|
|
472
722
|
} else {
|
|
473
|
-
reject(new Error(`generateSuggestion
|
|
723
|
+
reject(new Error(`generateSuggestion process exit code: ${code}`));
|
|
474
724
|
}
|
|
475
725
|
});
|
|
476
726
|
proc.once("error", reject);
|
|
@@ -485,10 +735,10 @@ ${context}`;
|
|
|
485
735
|
async answerQuestion(sessionId, toolUseId, answer) {
|
|
486
736
|
const entry = this.activeSessions.get(sessionId);
|
|
487
737
|
if (!entry) {
|
|
488
|
-
throw new Error(
|
|
738
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
489
739
|
}
|
|
490
740
|
if (!entry.process.stdin || entry.process.stdin.destroyed) {
|
|
491
|
-
throw new Error(
|
|
741
|
+
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
492
742
|
}
|
|
493
743
|
const toolResult = JSON.stringify({
|
|
494
744
|
type: "tool_result",
|
|
@@ -501,7 +751,7 @@ ${context}`;
|
|
|
501
751
|
else resolve();
|
|
502
752
|
});
|
|
503
753
|
});
|
|
504
|
-
console.log(`[ProcessProvider]
|
|
754
|
+
console.log(`[ProcessProvider] Session ${sessionId}: AskUserQuestion answered (toolUseId=${toolUseId})`);
|
|
505
755
|
}
|
|
506
756
|
/**
|
|
507
757
|
* 订阅指定会话的 AskUserQuestion 事件
|
|
@@ -540,7 +790,7 @@ var SessionManager = class {
|
|
|
540
790
|
unsubscribeMap = /* @__PURE__ */ new Map();
|
|
541
791
|
/** 每个会话的事件缓冲区(用于新订阅者重放)*/
|
|
542
792
|
sessionEventBuffers = /* @__PURE__ */ new Map();
|
|
543
|
-
/** AskUserQuestion 问题映射:requestId → resolve 回调 */
|
|
793
|
+
/** AskUserQuestion 问题映射:requestId → resolve 回调 + 原始问题内容 */
|
|
544
794
|
pendingQuestions = /* @__PURE__ */ new Map();
|
|
545
795
|
/**
|
|
546
796
|
* 会话状态缓存(用于追踪 status 变化,检测 oldStatus !== newStatus 时广播)
|
|
@@ -555,6 +805,10 @@ var SessionManager = class {
|
|
|
555
805
|
runningStartedAt = /* @__PURE__ */ new Map();
|
|
556
806
|
/** assistant 事件合并缓冲区(30ms 窗口内的 assistant 事件合并为一次发送) */
|
|
557
807
|
pendingAssistantEvents = /* @__PURE__ */ new Map();
|
|
808
|
+
/** 标记哪些会话的缓冲区曾被截断(溢出过 BUFFER_MAX) */
|
|
809
|
+
bufferTruncated = /* @__PURE__ */ new Set();
|
|
810
|
+
/** sessionId → projectPath 映射,用于截断时从 JSONL 补全历史 */
|
|
811
|
+
sessionProjectPaths = /* @__PURE__ */ new Map();
|
|
558
812
|
constructor(provider) {
|
|
559
813
|
this.provider = provider;
|
|
560
814
|
}
|
|
@@ -579,9 +833,10 @@ var SessionManager = class {
|
|
|
579
833
|
images
|
|
580
834
|
});
|
|
581
835
|
this.lastBroadcastStatus.set(session.id, session.status);
|
|
836
|
+
this.sessionProjectPaths.set(session.id, projectPath);
|
|
582
837
|
this.unsubscribeSession(session.id);
|
|
583
838
|
this.subscribeToSession(session.id);
|
|
584
|
-
console.log(`[SessionManager]
|
|
839
|
+
console.log(`[SessionManager] Session created: ${session.id} (project: ${projectPath})`);
|
|
585
840
|
return session;
|
|
586
841
|
}
|
|
587
842
|
/**
|
|
@@ -590,7 +845,7 @@ var SessionManager = class {
|
|
|
590
845
|
async sendMessage(sessionId, message, permissionMode, images) {
|
|
591
846
|
await this.provider.sendMessage(sessionId, message, permissionMode, images);
|
|
592
847
|
this.updateSessionStatus(sessionId, "running");
|
|
593
|
-
console.log(`[SessionManager]
|
|
848
|
+
console.log(`[SessionManager] Message sent to session: ${sessionId}`);
|
|
594
849
|
}
|
|
595
850
|
/**
|
|
596
851
|
* 终止会话
|
|
@@ -600,6 +855,8 @@ var SessionManager = class {
|
|
|
600
855
|
this.clearPendingQuestions(sessionId);
|
|
601
856
|
this.lastBroadcastStatus.delete(sessionId);
|
|
602
857
|
this.sessionEventBuffers.delete(sessionId);
|
|
858
|
+
this.bufferTruncated.delete(sessionId);
|
|
859
|
+
this.sessionProjectPaths.delete(sessionId);
|
|
603
860
|
this.sessionStats.delete(sessionId);
|
|
604
861
|
const pending = this.pendingAssistantEvents.get(sessionId);
|
|
605
862
|
if (pending) {
|
|
@@ -607,7 +864,7 @@ var SessionManager = class {
|
|
|
607
864
|
this.pendingAssistantEvents.delete(sessionId);
|
|
608
865
|
}
|
|
609
866
|
await this.provider.killSession(sessionId);
|
|
610
|
-
console.log(`[SessionManager]
|
|
867
|
+
console.log(`[SessionManager] Session killed: ${sessionId}`);
|
|
611
868
|
}
|
|
612
869
|
/**
|
|
613
870
|
* 获取会话的缓冲事件(用于新订阅者重放)
|
|
@@ -615,19 +872,71 @@ var SessionManager = class {
|
|
|
615
872
|
getSessionEvents(sessionId) {
|
|
616
873
|
return this.sessionEventBuffers.get(sessionId) ?? [];
|
|
617
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* 检查会话的缓冲区是否曾被截断(溢出过 BUFFER_MAX)
|
|
877
|
+
*/
|
|
878
|
+
isBufferTruncated(sessionId) {
|
|
879
|
+
return this.bufferTruncated.has(sessionId);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* 获取会话的项目路径(用于截断时从 JSONL 补全历史)
|
|
883
|
+
*/
|
|
884
|
+
getSessionProjectPath(sessionId) {
|
|
885
|
+
return this.sessionProjectPaths.get(sessionId);
|
|
886
|
+
}
|
|
618
887
|
/**
|
|
619
888
|
* 处理 AskUserQuestion 回答(从手机端传来)
|
|
620
889
|
*/
|
|
621
890
|
handleQuestionResponse(requestId, answer) {
|
|
622
891
|
const pending = this.pendingQuestions.get(requestId);
|
|
623
892
|
if (!pending) {
|
|
624
|
-
console.warn(`[SessionManager]
|
|
893
|
+
console.warn(`[SessionManager] Question request not found: ${requestId}`);
|
|
625
894
|
return;
|
|
626
895
|
}
|
|
627
896
|
this.pendingQuestions.delete(requestId);
|
|
628
897
|
this.updateSessionStatus(pending.sessionId, "running");
|
|
629
898
|
pending.resolve(answer);
|
|
630
|
-
console.log(`[SessionManager]
|
|
899
|
+
console.log(`[SessionManager] Question answered: ${requestId}`);
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* 获取指定会话的所有待回答问题(用于重连时恢复)
|
|
903
|
+
*/
|
|
904
|
+
getPendingQuestionsForSession(sessionId) {
|
|
905
|
+
const result = [];
|
|
906
|
+
for (const [requestId, pending] of this.pendingQuestions) {
|
|
907
|
+
if (pending.sessionId === sessionId) {
|
|
908
|
+
result.push({
|
|
909
|
+
id: requestId,
|
|
910
|
+
sessionId,
|
|
911
|
+
toolUseId: pending.toolUseId,
|
|
912
|
+
question: pending.question,
|
|
913
|
+
options: pending.options,
|
|
914
|
+
createdAt: pending.createdAt
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
}
|
|
920
|
+
/** 检查某个问题是否仍在等待回答 */
|
|
921
|
+
isQuestionPending(requestId) {
|
|
922
|
+
return this.pendingQuestions.has(requestId);
|
|
923
|
+
}
|
|
924
|
+
/**
|
|
925
|
+
* 获取所有待回答问题(用于客户端重连时恢复状态)
|
|
926
|
+
*/
|
|
927
|
+
getAllPendingQuestions() {
|
|
928
|
+
const result = [];
|
|
929
|
+
for (const [requestId, pending] of this.pendingQuestions) {
|
|
930
|
+
result.push({
|
|
931
|
+
id: requestId,
|
|
932
|
+
sessionId: pending.sessionId,
|
|
933
|
+
toolUseId: pending.toolUseId,
|
|
934
|
+
question: pending.question,
|
|
935
|
+
options: pending.options,
|
|
936
|
+
createdAt: pending.createdAt
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
return result;
|
|
631
940
|
}
|
|
632
941
|
/**
|
|
633
942
|
* 获取所有活跃会话(含服务器端统计)
|
|
@@ -661,6 +970,8 @@ var SessionManager = class {
|
|
|
661
970
|
}
|
|
662
971
|
this.unsubscribeMap.clear();
|
|
663
972
|
this.sessionEventBuffers.clear();
|
|
973
|
+
this.bufferTruncated.clear();
|
|
974
|
+
this.sessionProjectPaths.clear();
|
|
664
975
|
this.sessionStats.clear();
|
|
665
976
|
for (const [, pending] of this.pendingAssistantEvents) {
|
|
666
977
|
clearTimeout(pending.timer);
|
|
@@ -669,7 +980,7 @@ var SessionManager = class {
|
|
|
669
980
|
this.pendingQuestions.clear();
|
|
670
981
|
this.lastBroadcastStatus.clear();
|
|
671
982
|
this.eventCallbacks.length = 0;
|
|
672
|
-
console.log("[SessionManager]
|
|
983
|
+
console.log("[SessionManager] Destroyed");
|
|
673
984
|
}
|
|
674
985
|
// ============================================
|
|
675
986
|
// 内部方法
|
|
@@ -714,6 +1025,7 @@ var SessionManager = class {
|
|
|
714
1025
|
buffer.push(event);
|
|
715
1026
|
if (buffer.length > BUFFER_MAX) {
|
|
716
1027
|
buffer.splice(0, buffer.length - BUFFER_MAX);
|
|
1028
|
+
this.bufferTruncated.add(sessionId);
|
|
717
1029
|
}
|
|
718
1030
|
this.sessionEventBuffers.set(sessionId, buffer);
|
|
719
1031
|
if (event.type === "assistant" && Array.isArray(event.message?.content)) {
|
|
@@ -822,7 +1134,7 @@ var SessionManager = class {
|
|
|
822
1134
|
status: newStatus,
|
|
823
1135
|
stats
|
|
824
1136
|
});
|
|
825
|
-
console.log(`[SessionManager]
|
|
1137
|
+
console.log(`[SessionManager] Session ${sessionId} status change: ${lastStatus ?? "(none)"} \u2192 ${newStatus}`);
|
|
826
1138
|
}
|
|
827
1139
|
}
|
|
828
1140
|
/** 获取会话统计(含 runningStartedAt) */
|
|
@@ -851,7 +1163,7 @@ var SessionManager = class {
|
|
|
851
1163
|
createdAt: Date.now()
|
|
852
1164
|
};
|
|
853
1165
|
this.emit({ type: "question_request", request: updatedRequest });
|
|
854
|
-
console.log(`[SessionManager]
|
|
1166
|
+
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion updated (requestId=${existingRequestId})`);
|
|
855
1167
|
return;
|
|
856
1168
|
}
|
|
857
1169
|
const requestId = (0, import_uuid2.v4)();
|
|
@@ -866,16 +1178,16 @@ var SessionManager = class {
|
|
|
866
1178
|
this.updateSessionStatus(sessionId, "waiting_question");
|
|
867
1179
|
this.emit({ type: "question_request", request });
|
|
868
1180
|
const answerPromise = new Promise((resolve) => {
|
|
869
|
-
this.pendingQuestions.set(requestId, { sessionId, toolUseId, resolve });
|
|
1181
|
+
this.pendingQuestions.set(requestId, { sessionId, toolUseId, question, options, createdAt: request.createdAt, resolve });
|
|
870
1182
|
});
|
|
871
1183
|
answerPromise.then(async (answer) => {
|
|
872
1184
|
try {
|
|
873
1185
|
await this.provider.answerQuestion(sessionId, toolUseId, answer);
|
|
874
1186
|
} catch (err) {
|
|
875
|
-
console.error(`[SessionManager] answerQuestion
|
|
1187
|
+
console.error(`[SessionManager] answerQuestion failed (${sessionId}):`, err);
|
|
876
1188
|
}
|
|
877
1189
|
}).catch((err) => console.error("[SessionManager] answerPromise rejected:", err));
|
|
878
|
-
console.log(`[SessionManager]
|
|
1190
|
+
console.log(`[SessionManager] Session ${sessionId}: AskUserQuestion pushed (requestId=${requestId})`);
|
|
879
1191
|
}
|
|
880
1192
|
/**
|
|
881
1193
|
* 清除指定会话的所有待回答问题
|
|
@@ -899,7 +1211,7 @@ var SessionManager = class {
|
|
|
899
1211
|
try {
|
|
900
1212
|
callback(event);
|
|
901
1213
|
} catch (err) {
|
|
902
|
-
console.error("[SessionManager]
|
|
1214
|
+
console.error("[SessionManager] Event callback error:", err);
|
|
903
1215
|
}
|
|
904
1216
|
}
|
|
905
1217
|
}
|
|
@@ -945,13 +1257,13 @@ var SessionFileWatcher = class {
|
|
|
945
1257
|
};
|
|
946
1258
|
watcher.on("change", () => {
|
|
947
1259
|
this.readNewLines(sessionId).catch((err) => {
|
|
948
|
-
console.error(`[SessionFileWatcher]
|
|
1260
|
+
console.error(`[SessionFileWatcher] ${t("watcher.readError", { sessionId })}:`, err);
|
|
949
1261
|
});
|
|
950
1262
|
this.resetIdleTimer(sessionId);
|
|
951
1263
|
});
|
|
952
1264
|
this.watchers.set(sessionId, entry);
|
|
953
1265
|
this.resetIdleTimer(sessionId);
|
|
954
|
-
console.log(`[SessionFileWatcher]
|
|
1266
|
+
console.log(`[SessionFileWatcher] ${t("watcher.startWatching")}: ${sessionId} (offset=${byteOffset})`);
|
|
955
1267
|
}
|
|
956
1268
|
/** 停止监听指定会话 */
|
|
957
1269
|
unwatch(sessionId) {
|
|
@@ -960,7 +1272,7 @@ var SessionFileWatcher = class {
|
|
|
960
1272
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
961
1273
|
void entry.watcher.close();
|
|
962
1274
|
this.watchers.delete(sessionId);
|
|
963
|
-
console.log(`[SessionFileWatcher]
|
|
1275
|
+
console.log(`[SessionFileWatcher] ${t("watcher.stopWatching")}: ${sessionId}`);
|
|
964
1276
|
}
|
|
965
1277
|
/** 停止所有监听(服务关闭时调用) */
|
|
966
1278
|
destroy() {
|
|
@@ -976,7 +1288,7 @@ var SessionFileWatcher = class {
|
|
|
976
1288
|
if (!entry) return;
|
|
977
1289
|
if (entry.idleTimer) clearTimeout(entry.idleTimer);
|
|
978
1290
|
entry.idleTimer = setTimeout(() => {
|
|
979
|
-
console.log(`[SessionFileWatcher]
|
|
1291
|
+
console.log(`[SessionFileWatcher] Idle timeout, stop watching: ${sessionId}`);
|
|
980
1292
|
this.unwatch(sessionId);
|
|
981
1293
|
}, this.IDLE_TIMEOUT_MS);
|
|
982
1294
|
}
|
|
@@ -1083,6 +1395,8 @@ var WsBridge = class _WsBridge {
|
|
|
1083
1395
|
lastPongMap = /* @__PURE__ */ new Map();
|
|
1084
1396
|
/** 每个连接当前正在查看的会话 ID */
|
|
1085
1397
|
viewingSessions = /* @__PURE__ */ new Map();
|
|
1398
|
+
/** 每个连接的消息处理队列(串行化 async handler,防止 create_session/subscribe 竞态) */
|
|
1399
|
+
messageQueues = /* @__PURE__ */ new Map();
|
|
1086
1400
|
constructor(options) {
|
|
1087
1401
|
this.token = options.token;
|
|
1088
1402
|
this.wss = new import_ws.WebSocketServer({
|
|
@@ -1098,7 +1412,7 @@ var WsBridge = class _WsBridge {
|
|
|
1098
1412
|
});
|
|
1099
1413
|
this.wss.on("connection", (ws) => this.handleConnection(ws));
|
|
1100
1414
|
this.startHeartbeat();
|
|
1101
|
-
console.log(`[WsBridge]
|
|
1415
|
+
console.log(`[WsBridge] ${t("ws.started", { port: options.port })}`);
|
|
1102
1416
|
}
|
|
1103
1417
|
/**
|
|
1104
1418
|
* 异步工厂方法:等待端口监听成功后 resolve,端口占用等错误时 reject。
|
|
@@ -1108,7 +1422,7 @@ var WsBridge = class _WsBridge {
|
|
|
1108
1422
|
return new Promise((resolve, reject) => {
|
|
1109
1423
|
const bridge = new _WsBridge(options);
|
|
1110
1424
|
bridge.wss.once("listening", () => {
|
|
1111
|
-
bridge.wss.on("error", (err) => console.error(
|
|
1425
|
+
bridge.wss.on("error", (err) => console.error(`[WsBridge] ${t("ws.serverError")}:`, err));
|
|
1112
1426
|
resolve(bridge);
|
|
1113
1427
|
});
|
|
1114
1428
|
bridge.wss.once("error", reject);
|
|
@@ -1177,7 +1491,7 @@ var WsBridge = class _WsBridge {
|
|
|
1177
1491
|
if (err) {
|
|
1178
1492
|
reject(err);
|
|
1179
1493
|
} else {
|
|
1180
|
-
console.log("[WsBridge] WebSocket
|
|
1494
|
+
console.log("[WsBridge] WebSocket server closed");
|
|
1181
1495
|
resolve();
|
|
1182
1496
|
}
|
|
1183
1497
|
});
|
|
@@ -1200,12 +1514,12 @@ var WsBridge = class _WsBridge {
|
|
|
1200
1514
|
/** 处理新的 WebSocket 连接 */
|
|
1201
1515
|
handleConnection(ws) {
|
|
1202
1516
|
this.lastPongMap.set(ws, Date.now());
|
|
1203
|
-
console.log(`[WsBridge]
|
|
1517
|
+
console.log(`[WsBridge] New client connected, connections: ${this.getConnectionCount()}`);
|
|
1204
1518
|
for (const callback of this.connectionCallbacks) {
|
|
1205
1519
|
try {
|
|
1206
1520
|
callback(ws);
|
|
1207
1521
|
} catch (err) {
|
|
1208
|
-
console.error("[WsBridge]
|
|
1522
|
+
console.error("[WsBridge] Connection callback error:", err);
|
|
1209
1523
|
}
|
|
1210
1524
|
}
|
|
1211
1525
|
ws.on("pong", () => {
|
|
@@ -1216,10 +1530,10 @@ var WsBridge = class _WsBridge {
|
|
|
1216
1530
|
const event = JSON.parse(raw.toString());
|
|
1217
1531
|
this.dispatchClientEvent(event, ws);
|
|
1218
1532
|
} catch (err) {
|
|
1219
|
-
console.error("[WsBridge]
|
|
1533
|
+
console.error("[WsBridge] Message parse error:", err);
|
|
1220
1534
|
this.send(ws, {
|
|
1221
1535
|
type: "error",
|
|
1222
|
-
message: "
|
|
1536
|
+
message: "Invalid message format",
|
|
1223
1537
|
code: "INVALID_MESSAGE"
|
|
1224
1538
|
});
|
|
1225
1539
|
}
|
|
@@ -1227,30 +1541,40 @@ var WsBridge = class _WsBridge {
|
|
|
1227
1541
|
ws.on("close", () => {
|
|
1228
1542
|
this.lastPongMap.delete(ws);
|
|
1229
1543
|
this.viewingSessions.delete(ws);
|
|
1544
|
+
this.messageQueues.delete(ws);
|
|
1230
1545
|
setTimeout(() => {
|
|
1231
|
-
console.log(`[WsBridge]
|
|
1546
|
+
console.log(`[WsBridge] Client disconnected, connections: ${this.getConnectionCount()}`);
|
|
1232
1547
|
for (const cb of this.disconnectCallbacks) {
|
|
1233
1548
|
try {
|
|
1234
1549
|
cb();
|
|
1235
1550
|
} catch (err) {
|
|
1236
|
-
console.error("[WsBridge]
|
|
1551
|
+
console.error("[WsBridge] Disconnect callback error:", err);
|
|
1237
1552
|
}
|
|
1238
1553
|
}
|
|
1239
1554
|
}, 0);
|
|
1240
1555
|
});
|
|
1241
1556
|
ws.on("error", (err) => {
|
|
1242
|
-
console.error("[WsBridge]
|
|
1557
|
+
console.error("[WsBridge] Connection error:", err.message);
|
|
1243
1558
|
});
|
|
1244
1559
|
}
|
|
1245
|
-
/**
|
|
1560
|
+
/**
|
|
1561
|
+
* 分发客户端事件到所有注册的回调
|
|
1562
|
+
*
|
|
1563
|
+
* 使用 per-connection 队列串行化处理,确保 async 回调(如 create_session)
|
|
1564
|
+
* 完成后才处理下一条消息(如 subscribe),避免竞态条件。
|
|
1565
|
+
*/
|
|
1246
1566
|
dispatchClientEvent(event, ws) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1567
|
+
const prev = this.messageQueues.get(ws) ?? Promise.resolve();
|
|
1568
|
+
const next = prev.then(async () => {
|
|
1569
|
+
for (const callback of this.clientEventCallbacks) {
|
|
1570
|
+
try {
|
|
1571
|
+
await callback(event, ws);
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
console.error("[WsBridge] Event callback error:", err);
|
|
1574
|
+
}
|
|
1252
1575
|
}
|
|
1253
|
-
}
|
|
1576
|
+
});
|
|
1577
|
+
this.messageQueues.set(ws, next);
|
|
1254
1578
|
}
|
|
1255
1579
|
/** 启动心跳机制 */
|
|
1256
1580
|
startHeartbeat() {
|
|
@@ -1260,7 +1584,7 @@ var WsBridge = class _WsBridge {
|
|
|
1260
1584
|
for (const ws of this.wss.clients) {
|
|
1261
1585
|
const lastPong = this.lastPongMap.get(ws) ?? 0;
|
|
1262
1586
|
if (now - lastPong > 45e3) {
|
|
1263
|
-
console.log("[WsBridge]
|
|
1587
|
+
console.log("[WsBridge] Dead connection detected, terminating");
|
|
1264
1588
|
ws.terminate();
|
|
1265
1589
|
continue;
|
|
1266
1590
|
}
|
|
@@ -1300,7 +1624,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1300
1624
|
this.handleRequest(req, res);
|
|
1301
1625
|
});
|
|
1302
1626
|
this.server.listen(options.port, () => {
|
|
1303
|
-
console.log(`[ApprovalProxy]
|
|
1627
|
+
console.log(`[ApprovalProxy] ${t("approval.httpStarted", { port: options.port })}`);
|
|
1304
1628
|
});
|
|
1305
1629
|
}
|
|
1306
1630
|
/**
|
|
@@ -1310,7 +1634,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1310
1634
|
return new Promise((resolve, reject) => {
|
|
1311
1635
|
const proxy = new _ApprovalProxy(options);
|
|
1312
1636
|
proxy.server.once("listening", () => {
|
|
1313
|
-
proxy.server.on("error", (err) => console.error(
|
|
1637
|
+
proxy.server.on("error", (err) => console.error(`[ApprovalProxy] ${t("approval.serverError")}:`, err));
|
|
1314
1638
|
resolve(proxy);
|
|
1315
1639
|
});
|
|
1316
1640
|
proxy.server.once("error", reject);
|
|
@@ -1330,7 +1654,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1330
1654
|
/** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
|
|
1331
1655
|
setYoloMode(sessionId, enabled) {
|
|
1332
1656
|
this.yoloSessions.set(sessionId, enabled);
|
|
1333
|
-
console.log(`[ApprovalProxy]
|
|
1657
|
+
console.log(`[ApprovalProxy] ${t("approval.yoloMode", { status: enabled ? t("approval.yoloEnabled") : t("approval.yoloDisabled") })}: ${sessionId}`);
|
|
1334
1658
|
}
|
|
1335
1659
|
/** 检查会话是否处于 YOLO 模式 */
|
|
1336
1660
|
isYoloMode(sessionId) {
|
|
@@ -1345,13 +1669,13 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1345
1669
|
resolveApproval(requestId, decision) {
|
|
1346
1670
|
const pending = this.pendingApprovals.get(requestId);
|
|
1347
1671
|
if (!pending) {
|
|
1348
|
-
console.warn(`[ApprovalProxy]
|
|
1672
|
+
console.warn(`[ApprovalProxy] ${t("approval.requestNotFound", { id: requestId })}`);
|
|
1349
1673
|
return false;
|
|
1350
1674
|
}
|
|
1351
1675
|
clearTimeout(pending.timer);
|
|
1352
1676
|
pending.resolve(decision);
|
|
1353
1677
|
this.pendingApprovals.delete(requestId);
|
|
1354
|
-
console.log(`[ApprovalProxy]
|
|
1678
|
+
console.log(`[ApprovalProxy] ${t("approval.requestProcessed", { id: requestId })}: ${decision.decision}`);
|
|
1355
1679
|
return true;
|
|
1356
1680
|
}
|
|
1357
1681
|
/** 获取当前待处理的审批数量 */
|
|
@@ -1418,11 +1742,11 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1418
1742
|
allow.push(entry);
|
|
1419
1743
|
import_node_fs.default.writeFileSync(targetPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
1420
1744
|
const label = projectPath ? `${projectPath}/.claude/settings.json` : "~/.claude/settings.json";
|
|
1421
|
-
console.log(`[ApprovalProxy]
|
|
1745
|
+
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowWritten", { entry, label })}`);
|
|
1422
1746
|
}
|
|
1423
1747
|
this.alwaysAllowedTools.add(toolName);
|
|
1424
1748
|
} catch (err) {
|
|
1425
|
-
console.error(
|
|
1749
|
+
console.error(`[ApprovalProxy] ${t("approval.settingsWriteFailed")}:`, err);
|
|
1426
1750
|
}
|
|
1427
1751
|
}
|
|
1428
1752
|
/** 获取指定会话的所有 pending approval requests(用于 subscribe 重发) */
|
|
@@ -1435,6 +1759,10 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1435
1759
|
}
|
|
1436
1760
|
return result;
|
|
1437
1761
|
}
|
|
1762
|
+
/** 获取所有 pending approval requests(用于客户端重连时恢复状态) */
|
|
1763
|
+
getAllPendingRequests() {
|
|
1764
|
+
return Array.from(this.pendingApprovals.values()).map(({ request }) => request);
|
|
1765
|
+
}
|
|
1438
1766
|
/**
|
|
1439
1767
|
* 批量允许所有待处理的审批请求(手机端断线时调用)
|
|
1440
1768
|
*/
|
|
@@ -1444,7 +1772,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1444
1772
|
clearTimeout(pending.timer);
|
|
1445
1773
|
pending.resolve({ decision: "allow" });
|
|
1446
1774
|
this.pendingApprovals.delete(requestId);
|
|
1447
|
-
console.log(`[ApprovalProxy]
|
|
1775
|
+
console.log(`[ApprovalProxy] ${t("approval.autoAllowed", { id: requestId, reason: reason ? `\uFF08${reason}\uFF09` : "" })}`);
|
|
1448
1776
|
}
|
|
1449
1777
|
}
|
|
1450
1778
|
/** 优雅关闭 HTTP 服务 */
|
|
@@ -1453,14 +1781,14 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1453
1781
|
const pendingEntries = Array.from(this.pendingApprovals.entries());
|
|
1454
1782
|
for (const [, pending] of pendingEntries) {
|
|
1455
1783
|
clearTimeout(pending.timer);
|
|
1456
|
-
pending.resolve({ decision: "deny", reason: "
|
|
1784
|
+
pending.resolve({ decision: "deny", reason: t("approval.serverClosed") });
|
|
1457
1785
|
}
|
|
1458
1786
|
this.pendingApprovals.clear();
|
|
1459
1787
|
this.server.close((err) => {
|
|
1460
1788
|
if (err) {
|
|
1461
1789
|
reject(err);
|
|
1462
1790
|
} else {
|
|
1463
|
-
console.log(
|
|
1791
|
+
console.log(`[ApprovalProxy] ${t("approval.httpClosed")}`);
|
|
1464
1792
|
resolve();
|
|
1465
1793
|
}
|
|
1466
1794
|
});
|
|
@@ -1515,24 +1843,24 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1515
1843
|
projectPath,
|
|
1516
1844
|
toolName,
|
|
1517
1845
|
toolInput,
|
|
1518
|
-
description: String(payload.description ?? body.description ?? `${toolName}
|
|
1846
|
+
description: String(payload.description ?? body.description ?? `${toolName} tool call request`),
|
|
1519
1847
|
createdAt: Date.now()
|
|
1520
1848
|
};
|
|
1521
|
-
console.log(`[ApprovalProxy]
|
|
1849
|
+
console.log(`[ApprovalProxy] ${t("approval.received")}: ${requestId} (${approvalRequest.toolName})`);
|
|
1522
1850
|
if (this.isToolAlwaysAllowed(approvalRequest.toolName, projectPath !== "unknown" ? projectPath : void 0)) {
|
|
1523
|
-
console.log(`[ApprovalProxy] ${approvalRequest.toolName}
|
|
1851
|
+
console.log(`[ApprovalProxy] ${t("approval.alwaysAllowPassThrough", { tool: approvalRequest.toolName })}`);
|
|
1524
1852
|
this.sendJson(res, 200, { decision: "allow" });
|
|
1525
1853
|
return;
|
|
1526
1854
|
}
|
|
1527
1855
|
if (this.yoloSessions.get(approvalRequest.sessionId)) {
|
|
1528
|
-
console.log(`[ApprovalProxy]
|
|
1856
|
+
console.log(`[ApprovalProxy] ${t("approval.yoloAutoAllow")}: ${approvalRequest.toolName}`);
|
|
1529
1857
|
this.sendJson(res, 200, { decision: "allow" });
|
|
1530
1858
|
return;
|
|
1531
1859
|
}
|
|
1532
1860
|
this.notifyApprovalRequest(approvalRequest);
|
|
1533
1861
|
const decision = await new Promise((resolve) => {
|
|
1534
1862
|
const timer = setTimeout(() => {
|
|
1535
|
-
console.log(`[ApprovalProxy]
|
|
1863
|
+
console.log(`[ApprovalProxy] ${t("approval.timeout", { id: requestId })}`);
|
|
1536
1864
|
this.pendingApprovals.delete(requestId);
|
|
1537
1865
|
resolve({ decision: "allow" });
|
|
1538
1866
|
}, 325e3);
|
|
@@ -1540,8 +1868,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1540
1868
|
});
|
|
1541
1869
|
this.sendJson(res, 200, decision);
|
|
1542
1870
|
} catch (err) {
|
|
1543
|
-
console.error(
|
|
1544
|
-
this.sendJson(res, 200, { decision: "deny", reason: "
|
|
1871
|
+
console.error(`[ApprovalProxy] ${t("approval.processingFailed")}:`, err);
|
|
1872
|
+
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
1545
1873
|
}
|
|
1546
1874
|
}
|
|
1547
1875
|
/** 健康检查端点 */
|
|
@@ -1558,7 +1886,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1558
1886
|
const remoteAddress = req.socket.remoteAddress;
|
|
1559
1887
|
const isLocal = remoteAddress === "127.0.0.1" || remoteAddress === "::1" || remoteAddress === "::ffff:127.0.0.1";
|
|
1560
1888
|
if (!isLocal) {
|
|
1561
|
-
this.sendJson(res, 403, { error: "
|
|
1889
|
+
this.sendJson(res, 403, { error: t("approval.forbidden") });
|
|
1562
1890
|
return;
|
|
1563
1891
|
}
|
|
1564
1892
|
this.sendJson(res, 200, { token: this.token });
|
|
@@ -1569,7 +1897,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1569
1897
|
try {
|
|
1570
1898
|
callback(request);
|
|
1571
1899
|
} catch (err) {
|
|
1572
|
-
console.error("[ApprovalProxy]
|
|
1900
|
+
console.error("[ApprovalProxy] Approval request callback error:", err);
|
|
1573
1901
|
}
|
|
1574
1902
|
}
|
|
1575
1903
|
}
|
|
@@ -1586,7 +1914,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1586
1914
|
if (totalSize > MAX_BODY_SIZE) {
|
|
1587
1915
|
destroyed = true;
|
|
1588
1916
|
req.destroy();
|
|
1589
|
-
return reject(new Error("
|
|
1917
|
+
return reject(new Error(t("approval.bodyTooLarge")));
|
|
1590
1918
|
}
|
|
1591
1919
|
chunks.push(chunk);
|
|
1592
1920
|
});
|
|
@@ -1596,7 +1924,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1596
1924
|
const parsed = JSON.parse(raw);
|
|
1597
1925
|
resolve(parsed);
|
|
1598
1926
|
} catch {
|
|
1599
|
-
reject(new Error("
|
|
1927
|
+
reject(new Error(t("approval.invalidJson")));
|
|
1600
1928
|
}
|
|
1601
1929
|
});
|
|
1602
1930
|
req.on("error", (err) => {
|
|
@@ -1623,17 +1951,19 @@ var MdnsService = class {
|
|
|
1623
1951
|
wsPort;
|
|
1624
1952
|
httpPort;
|
|
1625
1953
|
version;
|
|
1954
|
+
token;
|
|
1626
1955
|
constructor(options) {
|
|
1627
1956
|
this.wsPort = options.wsPort;
|
|
1628
1957
|
this.httpPort = options.httpPort;
|
|
1629
1958
|
this.version = options.version ?? "0.1.0";
|
|
1959
|
+
this.token = options.token ?? "";
|
|
1630
1960
|
}
|
|
1631
1961
|
/**
|
|
1632
1962
|
* 启动 mDNS 广播
|
|
1633
1963
|
*/
|
|
1634
1964
|
start() {
|
|
1635
1965
|
if (this.bonjour) {
|
|
1636
|
-
console.warn(
|
|
1966
|
+
console.warn(`[MdnsService] ${t("mdns.alreadyRunning")}`);
|
|
1637
1967
|
return;
|
|
1638
1968
|
}
|
|
1639
1969
|
this.bonjour = new import_bonjour_service.default();
|
|
@@ -1643,10 +1973,11 @@ var MdnsService = class {
|
|
|
1643
1973
|
port: this.wsPort,
|
|
1644
1974
|
txt: {
|
|
1645
1975
|
version: this.version,
|
|
1646
|
-
httpPort: String(this.httpPort)
|
|
1976
|
+
httpPort: String(this.httpPort),
|
|
1977
|
+
token: this.token
|
|
1647
1978
|
}
|
|
1648
1979
|
});
|
|
1649
|
-
console.log(`[MdnsService]
|
|
1980
|
+
console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
|
|
1650
1981
|
}
|
|
1651
1982
|
/**
|
|
1652
1983
|
* 停止 mDNS 广播
|
|
@@ -1654,7 +1985,7 @@ var MdnsService = class {
|
|
|
1654
1985
|
stop() {
|
|
1655
1986
|
if (this.service) {
|
|
1656
1987
|
this.service.stop?.(() => {
|
|
1657
|
-
console.log(
|
|
1988
|
+
console.log(`[MdnsService] ${t("mdns.stopped")}`);
|
|
1658
1989
|
});
|
|
1659
1990
|
this.service = null;
|
|
1660
1991
|
}
|
|
@@ -1662,7 +1993,7 @@ var MdnsService = class {
|
|
|
1662
1993
|
this.bonjour.destroy();
|
|
1663
1994
|
this.bonjour = null;
|
|
1664
1995
|
}
|
|
1665
|
-
console.log(
|
|
1996
|
+
console.log(`[MdnsService] ${t("mdns.closed")}`);
|
|
1666
1997
|
}
|
|
1667
1998
|
};
|
|
1668
1999
|
|
|
@@ -1744,7 +2075,7 @@ var HookInstaller = class {
|
|
|
1744
2075
|
await (0, import_promises2.chmod)(HOOK_SCRIPT_PATH, 493);
|
|
1745
2076
|
await (0, import_promises2.chmod)(PERMISSION_ACCEPT_PATH, 493);
|
|
1746
2077
|
await this.addHookToSettings();
|
|
1747
|
-
console.log("[HookInstaller] Hook
|
|
2078
|
+
console.log("[HookInstaller] Hook installation complete");
|
|
1748
2079
|
}
|
|
1749
2080
|
/**
|
|
1750
2081
|
* 卸载 hook
|
|
@@ -1754,7 +2085,7 @@ var HookInstaller = class {
|
|
|
1754
2085
|
*/
|
|
1755
2086
|
async uninstall() {
|
|
1756
2087
|
await this.removeHookFromSettings();
|
|
1757
|
-
console.log("[HookInstaller] Hook
|
|
2088
|
+
console.log("[HookInstaller] Hook uninstalled");
|
|
1758
2089
|
}
|
|
1759
2090
|
/**
|
|
1760
2091
|
* 检查 hook 是否已安装
|
|
@@ -1812,7 +2143,7 @@ var HookInstaller = class {
|
|
|
1812
2143
|
if (changed) {
|
|
1813
2144
|
await this.writeClaudeSettings(settings);
|
|
1814
2145
|
} else {
|
|
1815
|
-
console.log("[HookInstaller] Hook
|
|
2146
|
+
console.log("[HookInstaller] Hook config already exists, skipping");
|
|
1816
2147
|
}
|
|
1817
2148
|
}
|
|
1818
2149
|
/**
|
|
@@ -1897,6 +2228,8 @@ var NotificationService = class {
|
|
|
1897
2228
|
yoloModeState = /* @__PURE__ */ new Map();
|
|
1898
2229
|
/** 每个会话的最新 assistant 文本消息(用于通知正文预览) */
|
|
1899
2230
|
latestAssistantText = /* @__PURE__ */ new Map();
|
|
2231
|
+
/** 获取全局待审批总数的回调(跨所有会话) */
|
|
2232
|
+
globalPendingCountProvider = null;
|
|
1900
2233
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
1901
2234
|
addChannel(id, channel, enabled = true) {
|
|
1902
2235
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -1930,6 +2263,14 @@ var NotificationService = class {
|
|
|
1930
2263
|
removeActivityPushToken(sessionId) {
|
|
1931
2264
|
this.activityPushChannel?.removeToken(sessionId);
|
|
1932
2265
|
}
|
|
2266
|
+
/** 设置全局待审批总数提供者 */
|
|
2267
|
+
setGlobalPendingCountProvider(provider) {
|
|
2268
|
+
this.globalPendingCountProvider = provider;
|
|
2269
|
+
}
|
|
2270
|
+
/** 获取全局待审批总数 */
|
|
2271
|
+
getGlobalPendingCount() {
|
|
2272
|
+
return this.globalPendingCountProvider?.() ?? 0;
|
|
2273
|
+
}
|
|
1933
2274
|
/** 更新会话的 YOLO 模式状态 */
|
|
1934
2275
|
setYoloMode(sessionId, enabled) {
|
|
1935
2276
|
this.yoloModeState.set(sessionId, enabled);
|
|
@@ -1938,7 +2279,7 @@ var NotificationService = class {
|
|
|
1938
2279
|
notifyApproval(request, pendingCount) {
|
|
1939
2280
|
if (this.yoloModeState.get(request.sessionId)) return;
|
|
1940
2281
|
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
1941
|
-
const title = pendingCount > 1 ?
|
|
2282
|
+
const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
|
|
1942
2283
|
const body = pendingCount > 1 ? `\u{1F527} \u6700\u65B0: ${request.toolName}: ${request.description}` : `\u{1F527} ${request.toolName}: ${request.description}`;
|
|
1943
2284
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
1944
2285
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
@@ -1948,7 +2289,7 @@ var NotificationService = class {
|
|
|
1948
2289
|
{
|
|
1949
2290
|
status: "waitingApproval",
|
|
1950
2291
|
sessionTitle,
|
|
1951
|
-
latestMessage:
|
|
2292
|
+
latestMessage: "",
|
|
1952
2293
|
approvalInfo: {
|
|
1953
2294
|
requestId: request.id,
|
|
1954
2295
|
toolName: request.toolName,
|
|
@@ -1966,8 +2307,8 @@ var NotificationService = class {
|
|
|
1966
2307
|
this.notify({
|
|
1967
2308
|
title,
|
|
1968
2309
|
body,
|
|
1969
|
-
sound: "
|
|
1970
|
-
badge:
|
|
2310
|
+
sound: "default",
|
|
2311
|
+
badge: this.getGlobalPendingCount(),
|
|
1971
2312
|
data: {
|
|
1972
2313
|
type: "approval_request",
|
|
1973
2314
|
sessionId: request.sessionId,
|
|
@@ -1975,6 +2316,37 @@ var NotificationService = class {
|
|
|
1975
2316
|
}
|
|
1976
2317
|
});
|
|
1977
2318
|
}
|
|
2319
|
+
/** 直接触发提问通知(由 server.ts 在 question_request 事件时调用) */
|
|
2320
|
+
notifyQuestion(request) {
|
|
2321
|
+
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
2322
|
+
const body = `\u2753 ${request.question.slice(0, 80)}`;
|
|
2323
|
+
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
2324
|
+
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
2325
|
+
this.activityPushChannel.updateActivityWithAlert(
|
|
2326
|
+
request.sessionId,
|
|
2327
|
+
{
|
|
2328
|
+
status: "waitingApproval",
|
|
2329
|
+
sessionTitle,
|
|
2330
|
+
latestMessage: request.question.slice(0, 80),
|
|
2331
|
+
isYoloMode,
|
|
2332
|
+
updatedAt: Date.now()
|
|
2333
|
+
},
|
|
2334
|
+
{ title: sessionTitle, body }
|
|
2335
|
+
);
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
this.notify({
|
|
2339
|
+
title: sessionTitle,
|
|
2340
|
+
body,
|
|
2341
|
+
sound: "default",
|
|
2342
|
+
badge: this.getGlobalPendingCount(),
|
|
2343
|
+
data: {
|
|
2344
|
+
type: "question_request",
|
|
2345
|
+
sessionId: request.sessionId,
|
|
2346
|
+
requestId: request.id
|
|
2347
|
+
}
|
|
2348
|
+
});
|
|
2349
|
+
}
|
|
1978
2350
|
/** 简单的工具危险等级判断 */
|
|
1979
2351
|
getDangerLevel(toolName) {
|
|
1980
2352
|
if (toolName === "Bash") return "danger";
|
|
@@ -2007,7 +2379,7 @@ var NotificationService = class {
|
|
|
2007
2379
|
if (event.status === "idle") {
|
|
2008
2380
|
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
2009
2381
|
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
2010
|
-
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : "
|
|
2382
|
+
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
2011
2383
|
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
2012
2384
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
2013
2385
|
this.activityPushChannel.endActivity(event.sessionId, {
|
|
@@ -2021,14 +2393,15 @@ var NotificationService = class {
|
|
|
2021
2393
|
this.notify({
|
|
2022
2394
|
title: sessionTitle,
|
|
2023
2395
|
body,
|
|
2024
|
-
sound: "
|
|
2396
|
+
sound: "default",
|
|
2397
|
+
badge: this.getGlobalPendingCount(),
|
|
2025
2398
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
2026
2399
|
});
|
|
2027
2400
|
}
|
|
2028
2401
|
} else if (event.status === "error") {
|
|
2029
2402
|
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
2030
2403
|
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
2031
|
-
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : "
|
|
2404
|
+
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
|
|
2032
2405
|
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
2033
2406
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
2034
2407
|
this.activityPushChannel.endActivity(event.sessionId, {
|
|
@@ -2042,7 +2415,8 @@ var NotificationService = class {
|
|
|
2042
2415
|
this.notify({
|
|
2043
2416
|
title: sessionTitle,
|
|
2044
2417
|
body,
|
|
2045
|
-
sound: "
|
|
2418
|
+
sound: "default",
|
|
2419
|
+
badge: this.getGlobalPendingCount(),
|
|
2046
2420
|
data: { type: "task_error", sessionId: event.sessionId }
|
|
2047
2421
|
});
|
|
2048
2422
|
}
|
|
@@ -2055,7 +2429,7 @@ var NotificationService = class {
|
|
|
2055
2429
|
for (const { channel, enabled } of this.channelMap.values()) {
|
|
2056
2430
|
if (!enabled) continue;
|
|
2057
2431
|
channel.send(payload).catch((err) => {
|
|
2058
|
-
console.error("[NotificationService]
|
|
2432
|
+
console.error("[NotificationService] Notification send failed:", err);
|
|
2059
2433
|
});
|
|
2060
2434
|
}
|
|
2061
2435
|
}
|
|
@@ -2095,7 +2469,7 @@ var MacNotificationChannel = class {
|
|
|
2095
2469
|
return new Promise((resolve) => {
|
|
2096
2470
|
(0, import_node_child_process.execFile)("osascript", ["-e", script], (err) => {
|
|
2097
2471
|
if (err) {
|
|
2098
|
-
console.warn("[MacNotificationChannel]
|
|
2472
|
+
console.warn("[MacNotificationChannel] Send notification failed:", err.message);
|
|
2099
2473
|
}
|
|
2100
2474
|
resolve();
|
|
2101
2475
|
});
|
|
@@ -2114,19 +2488,19 @@ var ExpoNotificationChannel = class {
|
|
|
2114
2488
|
}
|
|
2115
2489
|
addToken(token) {
|
|
2116
2490
|
this.tokens.add(token);
|
|
2117
|
-
console.log(`[ExpoNotificationChannel]
|
|
2491
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
|
|
2118
2492
|
}
|
|
2119
2493
|
removeToken(token) {
|
|
2120
2494
|
this.tokens.delete(token);
|
|
2121
2495
|
this.soundPreferences.delete(token);
|
|
2122
|
-
console.log(`[ExpoNotificationChannel]
|
|
2496
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
|
|
2123
2497
|
}
|
|
2124
2498
|
/** 更新某个 token 的音效偏好 */
|
|
2125
2499
|
setSoundPreferences(prefs) {
|
|
2126
2500
|
for (const token of this.tokens) {
|
|
2127
2501
|
this.soundPreferences.set(token, prefs);
|
|
2128
2502
|
}
|
|
2129
|
-
console.log(
|
|
2503
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
|
|
2130
2504
|
}
|
|
2131
2505
|
async send(payload) {
|
|
2132
2506
|
if (this.tokens.size === 0) return;
|
|
@@ -2139,17 +2513,18 @@ var ExpoNotificationChannel = class {
|
|
|
2139
2513
|
else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
|
|
2140
2514
|
else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
|
|
2141
2515
|
}
|
|
2516
|
+
const pushSound = sound === "none" ? null : sound;
|
|
2142
2517
|
return {
|
|
2143
2518
|
to,
|
|
2144
2519
|
title: payload.title,
|
|
2145
2520
|
body: payload.body,
|
|
2146
2521
|
badge: payload.badge,
|
|
2147
|
-
sound:
|
|
2522
|
+
sound: pushSound,
|
|
2148
2523
|
data: payload.data ?? {}
|
|
2149
2524
|
};
|
|
2150
2525
|
});
|
|
2151
2526
|
try {
|
|
2152
|
-
console.log(
|
|
2527
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}`, Array.from(this.tokens));
|
|
2153
2528
|
const res = await fetch(EXPO_PUSH_API, {
|
|
2154
2529
|
method: "POST",
|
|
2155
2530
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -2157,20 +2532,20 @@ var ExpoNotificationChannel = class {
|
|
|
2157
2532
|
});
|
|
2158
2533
|
const body = await res.json();
|
|
2159
2534
|
if (!res.ok) {
|
|
2160
|
-
console.warn(
|
|
2535
|
+
console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiError")}`, res.status, JSON.stringify(body));
|
|
2161
2536
|
} else {
|
|
2162
2537
|
if (!Array.isArray(body?.data)) {
|
|
2163
|
-
console.warn(
|
|
2538
|
+
console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
|
|
2164
2539
|
return;
|
|
2165
2540
|
}
|
|
2166
2541
|
for (const ticket of body.data) {
|
|
2167
2542
|
if (ticket.status === "error") {
|
|
2168
|
-
console.error(`[ExpoNotificationChannel]
|
|
2543
|
+
console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
|
|
2169
2544
|
}
|
|
2170
2545
|
}
|
|
2171
2546
|
}
|
|
2172
2547
|
} catch (err) {
|
|
2173
|
-
console.warn(
|
|
2548
|
+
console.warn(`[ExpoNotificationChannel] ${t("notification.sendFailed")}`, err);
|
|
2174
2549
|
}
|
|
2175
2550
|
}
|
|
2176
2551
|
};
|
|
@@ -2195,7 +2570,7 @@ var ActivityPushChannel = class {
|
|
|
2195
2570
|
this.keyId = config.keyId;
|
|
2196
2571
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
2197
2572
|
this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
|
|
2198
|
-
console.log(`[ActivityPushChannel]
|
|
2573
|
+
console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
|
|
2199
2574
|
}
|
|
2200
2575
|
/** 获取或新建 HTTP/2 长连接 */
|
|
2201
2576
|
getHttp2Client() {
|
|
@@ -2204,7 +2579,7 @@ var ActivityPushChannel = class {
|
|
|
2204
2579
|
}
|
|
2205
2580
|
this.http2Client = http2.connect(`https://${this.apnsHost}`);
|
|
2206
2581
|
this.http2Client.on("error", (err) => {
|
|
2207
|
-
console.warn("[ActivityPushChannel] HTTP/2
|
|
2582
|
+
console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
|
|
2208
2583
|
this.http2Client?.destroy();
|
|
2209
2584
|
this.http2Client = null;
|
|
2210
2585
|
});
|
|
@@ -2216,7 +2591,7 @@ var ActivityPushChannel = class {
|
|
|
2216
2591
|
/** 注册 Activity push token */
|
|
2217
2592
|
addToken(sessionId, token) {
|
|
2218
2593
|
this.tokens.set(sessionId, token);
|
|
2219
|
-
console.log(`[ActivityPushChannel]
|
|
2594
|
+
console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
|
|
2220
2595
|
}
|
|
2221
2596
|
/** 移除 Activity push token */
|
|
2222
2597
|
removeToken(sessionId) {
|
|
@@ -2236,7 +2611,7 @@ var ActivityPushChannel = class {
|
|
|
2236
2611
|
try {
|
|
2237
2612
|
await this.sendToAPNs(token, payload);
|
|
2238
2613
|
} catch (err) {
|
|
2239
|
-
console.warn(`[ActivityPushChannel]
|
|
2614
|
+
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
2240
2615
|
}
|
|
2241
2616
|
}
|
|
2242
2617
|
/** 发送带通知的 content-state 更新(审批请求时使用) */
|
|
@@ -2255,7 +2630,7 @@ var ActivityPushChannel = class {
|
|
|
2255
2630
|
try {
|
|
2256
2631
|
await this.sendToAPNs(token, payload);
|
|
2257
2632
|
} catch (err) {
|
|
2258
|
-
console.warn(`[ActivityPushChannel]
|
|
2633
|
+
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
2259
2634
|
}
|
|
2260
2635
|
}
|
|
2261
2636
|
/** 结束指定会话的 Live Activity */
|
|
@@ -2272,7 +2647,7 @@ var ActivityPushChannel = class {
|
|
|
2272
2647
|
try {
|
|
2273
2648
|
await this.sendToAPNs(token, payload);
|
|
2274
2649
|
} catch (err) {
|
|
2275
|
-
console.warn(`[ActivityPushChannel]
|
|
2650
|
+
console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
|
|
2276
2651
|
}
|
|
2277
2652
|
this.tokens.delete(sessionId);
|
|
2278
2653
|
}
|
|
@@ -2319,7 +2694,7 @@ var ActivityPushChannel = class {
|
|
|
2319
2694
|
this.http2Client?.destroy();
|
|
2320
2695
|
this.http2Client = null;
|
|
2321
2696
|
}
|
|
2322
|
-
reject(new Error(`APNs
|
|
2697
|
+
reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
|
|
2323
2698
|
}
|
|
2324
2699
|
});
|
|
2325
2700
|
req.on("error", (err) => {
|
|
@@ -2673,9 +3048,9 @@ async function createWithRetry(label, port, factory) {
|
|
|
2673
3048
|
return await factory();
|
|
2674
3049
|
} catch (err) {
|
|
2675
3050
|
if (err?.code === "EADDRINUSE") {
|
|
2676
|
-
console.warn(`[Server]
|
|
3051
|
+
console.warn(`[Server] ${t("server.portInUse", { port })}`);
|
|
2677
3052
|
await killPortProcess(port);
|
|
2678
|
-
console.log(`[Server]
|
|
3053
|
+
console.log(`[Server] ${t("server.restarting", { label })}`);
|
|
2679
3054
|
return await factory();
|
|
2680
3055
|
}
|
|
2681
3056
|
throw err;
|
|
@@ -2711,10 +3086,10 @@ async function start(opts = {}) {
|
|
|
2711
3086
|
try {
|
|
2712
3087
|
const activityChannel = new ActivityPushChannel(opts.activityPush);
|
|
2713
3088
|
notificationService.setActivityPushChannel(activityChannel);
|
|
2714
|
-
console.log(
|
|
3089
|
+
console.log(`[Server] ${t("server.activityPushEnabled")}`);
|
|
2715
3090
|
} catch (err) {
|
|
2716
|
-
console.warn(
|
|
2717
|
-
console.log(
|
|
3091
|
+
console.warn(`[Server] ${t("server.activityPushFailed")}`, err);
|
|
3092
|
+
console.log(`[Server] ${t("server.activityPushContinue")}`);
|
|
2718
3093
|
}
|
|
2719
3094
|
}
|
|
2720
3095
|
const wsBridge = await createWithRetry(
|
|
@@ -2730,6 +3105,13 @@ async function start(opts = {}) {
|
|
|
2730
3105
|
HTTP_PORT,
|
|
2731
3106
|
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
2732
3107
|
);
|
|
3108
|
+
const unreadSessionIds = /* @__PURE__ */ new Set();
|
|
3109
|
+
notificationService.setGlobalPendingCountProvider(
|
|
3110
|
+
() => approvalProxy.getPendingCount() + unreadSessionIds.size
|
|
3111
|
+
);
|
|
3112
|
+
const broadcastUnreadSessions = () => {
|
|
3113
|
+
wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
3114
|
+
};
|
|
2733
3115
|
wsBridge.onConnection(async (ws) => {
|
|
2734
3116
|
const result = await getProjects();
|
|
2735
3117
|
if (result.ok) {
|
|
@@ -2739,6 +3121,15 @@ async function start(opts = {}) {
|
|
|
2739
3121
|
type: "session_list",
|
|
2740
3122
|
sessions: sessionManager.getActiveSessions()
|
|
2741
3123
|
});
|
|
3124
|
+
for (const req of approvalProxy.getAllPendingRequests()) {
|
|
3125
|
+
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
3126
|
+
}
|
|
3127
|
+
for (const req of sessionManager.getAllPendingQuestions()) {
|
|
3128
|
+
wsBridge.send(ws, { type: "question_request", request: req });
|
|
3129
|
+
}
|
|
3130
|
+
if (unreadSessionIds.size > 0) {
|
|
3131
|
+
wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
3132
|
+
}
|
|
2742
3133
|
});
|
|
2743
3134
|
wsBridge.onClientEvent(async (event, ws) => {
|
|
2744
3135
|
try {
|
|
@@ -2797,7 +3188,32 @@ async function start(opts = {}) {
|
|
|
2797
3188
|
sessions: sessionManager.getActiveSessions()
|
|
2798
3189
|
});
|
|
2799
3190
|
const bufferedEvents = sessionManager.getSessionEvents(event.sessionId);
|
|
2800
|
-
if (
|
|
3191
|
+
if (sessionManager.isBufferTruncated(event.sessionId)) {
|
|
3192
|
+
const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
|
|
3193
|
+
if (projectPath) {
|
|
3194
|
+
const historyResult = await getSessionHistory(projectPath, event.sessionId);
|
|
3195
|
+
if (historyResult.ok && historyResult.value.length > 0) {
|
|
3196
|
+
const merged = [...historyResult.value, ...bufferedEvents];
|
|
3197
|
+
wsBridge.send(ws, {
|
|
3198
|
+
type: "session_history",
|
|
3199
|
+
sessionId: event.sessionId,
|
|
3200
|
+
events: merged
|
|
3201
|
+
});
|
|
3202
|
+
} else if (bufferedEvents.length > 0) {
|
|
3203
|
+
wsBridge.send(ws, {
|
|
3204
|
+
type: "session_history",
|
|
3205
|
+
sessionId: event.sessionId,
|
|
3206
|
+
events: bufferedEvents
|
|
3207
|
+
});
|
|
3208
|
+
}
|
|
3209
|
+
} else if (bufferedEvents.length > 0) {
|
|
3210
|
+
wsBridge.send(ws, {
|
|
3211
|
+
type: "session_history",
|
|
3212
|
+
sessionId: event.sessionId,
|
|
3213
|
+
events: bufferedEvents
|
|
3214
|
+
});
|
|
3215
|
+
}
|
|
3216
|
+
} else if (bufferedEvents.length > 0) {
|
|
2801
3217
|
wsBridge.send(ws, {
|
|
2802
3218
|
type: "session_history",
|
|
2803
3219
|
sessionId: event.sessionId,
|
|
@@ -2807,6 +3223,9 @@ async function start(opts = {}) {
|
|
|
2807
3223
|
for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
|
|
2808
3224
|
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
2809
3225
|
}
|
|
3226
|
+
for (const req of sessionManager.getPendingQuestionsForSession(event.sessionId)) {
|
|
3227
|
+
wsBridge.send(ws, { type: "question_request", request: req });
|
|
3228
|
+
}
|
|
2810
3229
|
break;
|
|
2811
3230
|
}
|
|
2812
3231
|
case "list_projects": {
|
|
@@ -2816,7 +3235,7 @@ async function start(opts = {}) {
|
|
|
2816
3235
|
} else {
|
|
2817
3236
|
wsBridge.send(ws, {
|
|
2818
3237
|
type: "error",
|
|
2819
|
-
message:
|
|
3238
|
+
message: t("server.listProjectsFailed", { error: result.error.message }),
|
|
2820
3239
|
code: "PROJECT_LIST_ERROR"
|
|
2821
3240
|
});
|
|
2822
3241
|
}
|
|
@@ -2842,7 +3261,7 @@ async function start(opts = {}) {
|
|
|
2842
3261
|
} else {
|
|
2843
3262
|
wsBridge.send(ws, {
|
|
2844
3263
|
type: "error",
|
|
2845
|
-
message:
|
|
3264
|
+
message: t("server.listSessionsFailed", { error: histResult.error.message }),
|
|
2846
3265
|
code: "PROJECT_SESSIONS_ERROR"
|
|
2847
3266
|
});
|
|
2848
3267
|
}
|
|
@@ -2853,7 +3272,7 @@ async function start(opts = {}) {
|
|
|
2853
3272
|
if (!historyResult.ok) {
|
|
2854
3273
|
wsBridge.send(ws, {
|
|
2855
3274
|
type: "error",
|
|
2856
|
-
message:
|
|
3275
|
+
message: t("server.readHistoryFailed", { error: historyResult.error.message }),
|
|
2857
3276
|
code: "SESSION_HISTORY_ERROR",
|
|
2858
3277
|
sessionId: event.sessionId
|
|
2859
3278
|
});
|
|
@@ -2878,7 +3297,7 @@ async function start(opts = {}) {
|
|
|
2878
3297
|
}
|
|
2879
3298
|
case "suggest_next_prompt": {
|
|
2880
3299
|
const historyResult = await getSessionHistory(event.projectPath, event.sessionId);
|
|
2881
|
-
let context = "
|
|
3300
|
+
let context = t("server.noHistory");
|
|
2882
3301
|
if (historyResult.ok && historyResult.value.length > 0) {
|
|
2883
3302
|
const recent = historyResult.value.slice(-10);
|
|
2884
3303
|
context = recent.map((e) => {
|
|
@@ -2887,7 +3306,8 @@ async function start(opts = {}) {
|
|
|
2887
3306
|
return `Assistant: ${text.substring(0, 300)}`;
|
|
2888
3307
|
}
|
|
2889
3308
|
if (e.type === "user") {
|
|
2890
|
-
const
|
|
3309
|
+
const content = e.message.content;
|
|
3310
|
+
const text = typeof content === "string" ? content : content.filter((b) => b.type === "text" && !!b.text).map((b) => b.text).join("");
|
|
2891
3311
|
return text ? `User: ${text.substring(0, 300)}` : null;
|
|
2892
3312
|
}
|
|
2893
3313
|
return null;
|
|
@@ -2928,6 +3348,9 @@ async function start(opts = {}) {
|
|
|
2928
3348
|
}
|
|
2929
3349
|
case "viewing_session": {
|
|
2930
3350
|
wsBridge.setViewingSession(ws, event.sessionId);
|
|
3351
|
+
if (unreadSessionIds.delete(event.sessionId)) {
|
|
3352
|
+
broadcastUnreadSessions();
|
|
3353
|
+
}
|
|
2931
3354
|
break;
|
|
2932
3355
|
}
|
|
2933
3356
|
case "left_session": {
|
|
@@ -2941,14 +3364,14 @@ async function start(opts = {}) {
|
|
|
2941
3364
|
default: {
|
|
2942
3365
|
wsBridge.send(ws, {
|
|
2943
3366
|
type: "error",
|
|
2944
|
-
message:
|
|
3367
|
+
message: t("server.unknownEvent", { type: event.type }),
|
|
2945
3368
|
code: "UNKNOWN_EVENT"
|
|
2946
3369
|
});
|
|
2947
3370
|
}
|
|
2948
3371
|
}
|
|
2949
3372
|
} catch (err) {
|
|
2950
3373
|
const message = err instanceof Error ? err.message : String(err);
|
|
2951
|
-
console.error(
|
|
3374
|
+
console.error(`[Server] ${t("server.clientEventError")}:`, message);
|
|
2952
3375
|
const errorCodeMap = {
|
|
2953
3376
|
create_session: "SESSION_CREATE_ERROR",
|
|
2954
3377
|
send_message: "SEND_MESSAGE_ERROR",
|
|
@@ -2964,10 +3387,16 @@ async function start(opts = {}) {
|
|
|
2964
3387
|
});
|
|
2965
3388
|
sessionManager.onEvent((event) => {
|
|
2966
3389
|
wsBridge.broadcast(event);
|
|
3390
|
+
if (event.type === "status_change" && (event.status === "idle" || event.status === "error")) {
|
|
3391
|
+
if (!wsBridge.isViewingSession(event.sessionId)) {
|
|
3392
|
+
unreadSessionIds.add(event.sessionId);
|
|
3393
|
+
broadcastUnreadSessions();
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
2967
3396
|
});
|
|
2968
3397
|
wsBridge.onDisconnect(() => {
|
|
2969
3398
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
2970
|
-
approvalProxy.approveAll("
|
|
3399
|
+
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
2971
3400
|
}
|
|
2972
3401
|
});
|
|
2973
3402
|
approvalProxy.onApprovalRequest((request) => {
|
|
@@ -2983,52 +3412,81 @@ async function start(opts = {}) {
|
|
|
2983
3412
|
if (!approvalProxy.isPending(request.id)) return;
|
|
2984
3413
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
2985
3414
|
if (wsBridge.getConnectionCount() > 0) return;
|
|
2986
|
-
console.log(`[Server]
|
|
3415
|
+
console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
|
|
2987
3416
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
2988
3417
|
notificationService.notifyApproval(request, pendingCount);
|
|
2989
3418
|
}, 6e4);
|
|
2990
3419
|
});
|
|
3420
|
+
sessionManager.onEvent((event) => {
|
|
3421
|
+
if (event.type !== "question_request") return;
|
|
3422
|
+
const { request } = event;
|
|
3423
|
+
setTimeout(() => {
|
|
3424
|
+
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
3425
|
+
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
3426
|
+
if (wsBridge.getConnectionCount() > 0) return;
|
|
3427
|
+
notificationService.notifyQuestion(request);
|
|
3428
|
+
}, 5e3);
|
|
3429
|
+
setTimeout(() => {
|
|
3430
|
+
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
3431
|
+
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
3432
|
+
if (wsBridge.getConnectionCount() > 0) return;
|
|
3433
|
+
console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
|
|
3434
|
+
notificationService.notifyQuestion(request);
|
|
3435
|
+
}, 6e4);
|
|
3436
|
+
});
|
|
2991
3437
|
approvalProxy.setStatusInfoProvider(() => ({
|
|
2992
3438
|
connections: wsBridge.getConnectionCount(),
|
|
2993
3439
|
activeSessions: sessionManager.getActiveSessions().length
|
|
2994
3440
|
}));
|
|
2995
|
-
|
|
2996
|
-
|
|
3441
|
+
let mdnsService = null;
|
|
3442
|
+
const startMdns = () => {
|
|
3443
|
+
if (mdnsService) return;
|
|
3444
|
+
mdnsService = new MdnsService({ wsPort: WS_PORT, httpPort: HTTP_PORT, token });
|
|
3445
|
+
mdnsService.start();
|
|
3446
|
+
};
|
|
3447
|
+
const stopMdns = () => {
|
|
3448
|
+
if (!mdnsService) return;
|
|
3449
|
+
mdnsService.stop();
|
|
3450
|
+
mdnsService = null;
|
|
3451
|
+
};
|
|
3452
|
+
if (opts.enableAutoConnect !== false) {
|
|
3453
|
+
startMdns();
|
|
3454
|
+
}
|
|
2997
3455
|
const hookInstaller = new HookInstaller();
|
|
2998
3456
|
try {
|
|
2999
3457
|
const installed = await hookInstaller.isInstalled();
|
|
3000
3458
|
if (!installed) {
|
|
3001
3459
|
await hookInstaller.install();
|
|
3002
|
-
console.log(
|
|
3460
|
+
console.log(`[Server] ${t("server.hookInstalled")}`);
|
|
3003
3461
|
} else {
|
|
3004
|
-
console.log(
|
|
3462
|
+
console.log(`[Server] ${t("server.hookExists")}`);
|
|
3005
3463
|
}
|
|
3006
3464
|
} catch (err) {
|
|
3007
|
-
console.error(
|
|
3008
|
-
console.log(
|
|
3465
|
+
console.error(`[Server] ${t("server.hookInstallFailed")}`, err);
|
|
3466
|
+
console.log(`[Server] ${t("server.hookContinue")}`);
|
|
3009
3467
|
}
|
|
3010
3468
|
const stop = async () => {
|
|
3011
|
-
console.log(
|
|
3469
|
+
console.log(`[Server] ${t("server.shuttingDown")}`);
|
|
3012
3470
|
const errors = [];
|
|
3013
3471
|
const attempt = async (fn, label) => {
|
|
3014
3472
|
try {
|
|
3015
3473
|
await fn();
|
|
3016
3474
|
} catch (err) {
|
|
3017
|
-
console.error(`[Server]
|
|
3475
|
+
console.error(`[Server] ${t("server.shutdownComponentError", { label })}:`, err);
|
|
3018
3476
|
errors.push(err);
|
|
3019
3477
|
}
|
|
3020
3478
|
};
|
|
3021
|
-
await attempt(() =>
|
|
3479
|
+
await attempt(() => stopMdns(), "mDNS");
|
|
3022
3480
|
await attempt(() => wsBridge.close(), "WebSocket");
|
|
3023
3481
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
3024
3482
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
3025
3483
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
3026
3484
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
3027
3485
|
if (errors.length > 0) {
|
|
3028
|
-
console.error(`[Server]
|
|
3486
|
+
console.error(`[Server] ${t("server.shutdownWithErrors", { count: errors.length })}`);
|
|
3029
3487
|
throw errors[0];
|
|
3030
3488
|
}
|
|
3031
|
-
console.log(
|
|
3489
|
+
console.log(`[Server] ${t("server.shutdownComplete")}`);
|
|
3032
3490
|
};
|
|
3033
3491
|
return {
|
|
3034
3492
|
token,
|
|
@@ -3039,7 +3497,14 @@ async function start(opts = {}) {
|
|
|
3039
3497
|
stop,
|
|
3040
3498
|
setMacNotification: (enabled) => notificationService.setChannelEnabled("mac", enabled),
|
|
3041
3499
|
setExpoPush: (enabled) => notificationService.setChannelEnabled("expo", enabled),
|
|
3042
|
-
onServerEvent: (cb) => sessionManager.onEvent(cb)
|
|
3500
|
+
onServerEvent: (cb) => sessionManager.onEvent(cb),
|
|
3501
|
+
setAutoConnect: (enabled) => {
|
|
3502
|
+
if (enabled) {
|
|
3503
|
+
startMdns();
|
|
3504
|
+
} else {
|
|
3505
|
+
stopMdns();
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3043
3508
|
};
|
|
3044
3509
|
}
|
|
3045
3510
|
|
|
@@ -3047,47 +3512,55 @@ async function start(opts = {}) {
|
|
|
3047
3512
|
var import_qrcode_terminal = __toESM(require("qrcode-terminal"));
|
|
3048
3513
|
async function main() {
|
|
3049
3514
|
console.log("=".repeat(50));
|
|
3050
|
-
console.log("
|
|
3515
|
+
console.log(t("startup.banner"));
|
|
3051
3516
|
console.log("=".repeat(50));
|
|
3052
3517
|
console.log();
|
|
3053
|
-
const
|
|
3518
|
+
const enableAutoConnect = process.env.SESSIX_AUTO_CONNECT !== "false";
|
|
3519
|
+
const server = await start({ enableAutoConnect });
|
|
3054
3520
|
const localIp = getLocalIp();
|
|
3055
3521
|
console.log("-".repeat(50));
|
|
3056
|
-
console.log(
|
|
3057
|
-
console.log(
|
|
3522
|
+
console.log(t("startup.wsPort", { port: server.wsPort }));
|
|
3523
|
+
console.log(t("startup.httpPort", { port: server.httpPort }));
|
|
3058
3524
|
if (server.token === "") {
|
|
3059
|
-
console.log(
|
|
3525
|
+
console.log(t("startup.tokenDisabled"));
|
|
3060
3526
|
console.log();
|
|
3061
|
-
console.log(
|
|
3527
|
+
console.log(t("startup.wsAddress", { ip: localIp, port: server.wsPort }));
|
|
3062
3528
|
} else {
|
|
3063
|
-
console.log(
|
|
3529
|
+
console.log(t("startup.token", { token: server.token }));
|
|
3064
3530
|
console.log();
|
|
3065
|
-
console.log(
|
|
3531
|
+
console.log(t("startup.wsAddressWithToken", { ip: localIp, port: server.wsPort, token: server.token }));
|
|
3066
3532
|
}
|
|
3067
|
-
console.log(
|
|
3533
|
+
console.log(t("startup.healthCheck", { port: server.httpPort }));
|
|
3068
3534
|
console.log("-".repeat(50));
|
|
3069
3535
|
if (server.token === "") {
|
|
3070
3536
|
console.log();
|
|
3071
|
-
console.log("
|
|
3537
|
+
console.log(t("startup.devMode"));
|
|
3072
3538
|
}
|
|
3073
3539
|
console.log();
|
|
3074
3540
|
const qrUrl = buildQrUrl(localIp, server.wsPort, server.token);
|
|
3075
|
-
console.log("
|
|
3541
|
+
console.log(t("startup.scanToPair"));
|
|
3076
3542
|
import_qrcode_terminal.default.generate(qrUrl, { small: true }, (qr) => {
|
|
3077
3543
|
qr.split("\n").forEach((line) => console.log(` ${line}`));
|
|
3078
3544
|
});
|
|
3079
3545
|
console.log();
|
|
3080
|
-
|
|
3546
|
+
if (enableAutoConnect) {
|
|
3547
|
+
console.log(t("startup.autoDiscoveryOn"));
|
|
3548
|
+
console.log(t("startup.autoDiscoveryHint"));
|
|
3549
|
+
} else {
|
|
3550
|
+
console.log(t("startup.autoDiscoveryOff"));
|
|
3551
|
+
}
|
|
3552
|
+
console.log();
|
|
3553
|
+
console.log(t("startup.waitingConnection"));
|
|
3081
3554
|
console.log();
|
|
3082
3555
|
const shutdown = async (signal) => {
|
|
3083
3556
|
console.log(`
|
|
3084
|
-
[Main]
|
|
3557
|
+
[Main] ${t("startup.receivedSignal", { signal })}`);
|
|
3085
3558
|
try {
|
|
3086
3559
|
await server.stop();
|
|
3087
|
-
console.log(
|
|
3560
|
+
console.log(`[Main] ${t("startup.goodbye")}`);
|
|
3088
3561
|
process.exit(0);
|
|
3089
3562
|
} catch (err) {
|
|
3090
|
-
console.error(
|
|
3563
|
+
console.error(`[Main] ${t("startup.shutdownError")}`, err);
|
|
3091
3564
|
process.exit(1);
|
|
3092
3565
|
}
|
|
3093
3566
|
};
|
|
@@ -3110,6 +3583,6 @@ function buildQrUrl(ip, wsPort, token) {
|
|
|
3110
3583
|
return token ? `${base}?token=${token}` : base;
|
|
3111
3584
|
}
|
|
3112
3585
|
main().catch((err) => {
|
|
3113
|
-
console.error(
|
|
3586
|
+
console.error(`[Main] ${t("startup.startFailed")}`, err);
|
|
3114
3587
|
process.exit(1);
|
|
3115
3588
|
});
|