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