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