sessix-server 0.1.3 → 0.2.1
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 +326 -20
- package/dist/server.d.ts +6 -0
- package/dist/server.js +306 -18
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ var zh = {
|
|
|
43
43
|
autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
|
|
44
44
|
autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
45
45
|
autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
|
|
46
|
+
pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
|
|
47
|
+
pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
|
|
46
48
|
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
47
49
|
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
48
50
|
shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
|
|
@@ -69,7 +71,8 @@ var zh = {
|
|
|
69
71
|
restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
|
|
70
72
|
activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
|
|
71
73
|
activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
|
|
72
|
-
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
|
|
74
|
+
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
|
|
75
|
+
noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
|
|
73
76
|
},
|
|
74
77
|
ws: {
|
|
75
78
|
started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
|
|
@@ -144,6 +147,8 @@ var en = {
|
|
|
144
147
|
autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
|
|
145
148
|
autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
146
149
|
autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
|
|
150
|
+
pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
|
|
151
|
+
pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
|
|
147
152
|
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
148
153
|
goodbye: "All services closed, goodbye!",
|
|
149
154
|
shutdownError: "Shutdown error:",
|
|
@@ -170,7 +175,8 @@ var en = {
|
|
|
170
175
|
restarting: "Restarting {{label}}...",
|
|
171
176
|
activityPushEnabled: "ActivityKit Push enabled",
|
|
172
177
|
activityPushFailed: "ActivityKit Push init failed:",
|
|
173
|
-
activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
|
|
178
|
+
activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
|
|
179
|
+
noActiveLoginProcess: "No active login process"
|
|
174
180
|
},
|
|
175
181
|
ws: {
|
|
176
182
|
started: "WebSocket server started on port {{port}}",
|
|
@@ -283,11 +289,14 @@ var import_node_child_process2 = require("child_process");
|
|
|
283
289
|
var import_node_util = require("util");
|
|
284
290
|
|
|
285
291
|
// src/providers/ProcessProvider.ts
|
|
286
|
-
var
|
|
292
|
+
var import_child_process2 = require("child_process");
|
|
287
293
|
var import_readline = require("readline");
|
|
288
294
|
var import_events = require("events");
|
|
289
295
|
var import_node_os = require("os");
|
|
290
296
|
var import_uuid = require("uuid");
|
|
297
|
+
|
|
298
|
+
// src/utils/claudePath.ts
|
|
299
|
+
var import_child_process = require("child_process");
|
|
291
300
|
function findClaudePath() {
|
|
292
301
|
try {
|
|
293
302
|
return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
|
|
@@ -307,6 +316,8 @@ function findClaudePath() {
|
|
|
307
316
|
return "claude";
|
|
308
317
|
}
|
|
309
318
|
}
|
|
319
|
+
|
|
320
|
+
// src/providers/ProcessProvider.ts
|
|
310
321
|
var CLAUDE_PATH = findClaudePath();
|
|
311
322
|
var ProcessProvider = class {
|
|
312
323
|
/** 活跃会话映射表:sessionId -> { session, process } */
|
|
@@ -499,7 +510,7 @@ var ProcessProvider = class {
|
|
|
499
510
|
}
|
|
500
511
|
const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
|
|
501
512
|
delete env.CLAUDECODE;
|
|
502
|
-
const proc = (0,
|
|
513
|
+
const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
|
|
503
514
|
cwd: projectPath,
|
|
504
515
|
env,
|
|
505
516
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -706,7 +717,7 @@ ${context}`;
|
|
|
706
717
|
return new Promise((resolve, reject) => {
|
|
707
718
|
const env = { ...process.env };
|
|
708
719
|
delete env.CLAUDECODE;
|
|
709
|
-
const proc = (0,
|
|
720
|
+
const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
|
|
710
721
|
cwd: (0, import_node_os.homedir)(),
|
|
711
722
|
env,
|
|
712
723
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1617,6 +1628,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1617
1628
|
alwaysAllowedTools = /* @__PURE__ */ new Set();
|
|
1618
1629
|
/** 获取状态信息的回调(由外部注入) */
|
|
1619
1630
|
statusInfoProvider = null;
|
|
1631
|
+
pairingManager = null;
|
|
1620
1632
|
constructor(options) {
|
|
1621
1633
|
this.token = options.token;
|
|
1622
1634
|
this.port = options.port;
|
|
@@ -1651,6 +1663,10 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1651
1663
|
setStatusInfoProvider(provider) {
|
|
1652
1664
|
this.statusInfoProvider = provider;
|
|
1653
1665
|
}
|
|
1666
|
+
/** 设置配对管理器 */
|
|
1667
|
+
setPairingManager(manager) {
|
|
1668
|
+
this.pairingManager = manager;
|
|
1669
|
+
}
|
|
1654
1670
|
/** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
|
|
1655
1671
|
setYoloMode(sessionId, enabled) {
|
|
1656
1672
|
this.yoloSessions.set(sessionId, enabled);
|
|
@@ -1811,6 +1827,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1811
1827
|
const pathname = url.pathname;
|
|
1812
1828
|
if (req.method === "POST" && pathname === "/hook/approval") {
|
|
1813
1829
|
this.handleApprovalHook(req, res);
|
|
1830
|
+
} else if (req.method === "POST" && pathname === "/pair") {
|
|
1831
|
+
this.handlePair(req, res);
|
|
1814
1832
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
1815
1833
|
this.handleHealth(req, res);
|
|
1816
1834
|
} else if (req.method === "GET" && pathname === "/token") {
|
|
@@ -1881,6 +1899,23 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1881
1899
|
activeSessions: info.activeSessions
|
|
1882
1900
|
});
|
|
1883
1901
|
}
|
|
1902
|
+
/** 配对端点:配对窗口开放时返回 token */
|
|
1903
|
+
handlePair(_req, res) {
|
|
1904
|
+
if (!this.pairingManager) {
|
|
1905
|
+
this.sendJson(res, 503, { error: "pairing_unavailable" });
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const result = this.pairingManager.tryPair();
|
|
1909
|
+
if (result) {
|
|
1910
|
+
console.log("[ApprovalProxy] Device paired successfully");
|
|
1911
|
+
this.sendJson(res, 200, result);
|
|
1912
|
+
} else {
|
|
1913
|
+
this.sendJson(res, 403, {
|
|
1914
|
+
error: "pairing_closed",
|
|
1915
|
+
message: "Pairing window is closed. Restart server or press p to reopen."
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1884
1919
|
/** 返回连接 token(仅本机访问) */
|
|
1885
1920
|
handleToken(req, res) {
|
|
1886
1921
|
const remoteAddress = req.socket.remoteAddress;
|
|
@@ -1951,12 +1986,12 @@ var MdnsService = class {
|
|
|
1951
1986
|
wsPort;
|
|
1952
1987
|
httpPort;
|
|
1953
1988
|
version;
|
|
1954
|
-
|
|
1989
|
+
pairing;
|
|
1955
1990
|
constructor(options) {
|
|
1956
1991
|
this.wsPort = options.wsPort;
|
|
1957
1992
|
this.httpPort = options.httpPort;
|
|
1958
1993
|
this.version = options.version ?? "0.1.0";
|
|
1959
|
-
this.
|
|
1994
|
+
this.pairing = options.pairing ?? "closed";
|
|
1960
1995
|
}
|
|
1961
1996
|
/**
|
|
1962
1997
|
* 启动 mDNS 广播
|
|
@@ -1974,7 +2009,8 @@ var MdnsService = class {
|
|
|
1974
2009
|
txt: {
|
|
1975
2010
|
version: this.version,
|
|
1976
2011
|
httpPort: String(this.httpPort),
|
|
1977
|
-
|
|
2012
|
+
wsPort: String(this.wsPort),
|
|
2013
|
+
pairing: this.pairing
|
|
1978
2014
|
}
|
|
1979
2015
|
});
|
|
1980
2016
|
console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
|
|
@@ -1995,6 +2031,29 @@ var MdnsService = class {
|
|
|
1995
2031
|
}
|
|
1996
2032
|
console.log(`[MdnsService] ${t("mdns.closed")}`);
|
|
1997
2033
|
}
|
|
2034
|
+
/**
|
|
2035
|
+
* 更新配对状态(重新发布 mDNS 服务)
|
|
2036
|
+
*/
|
|
2037
|
+
updatePairingState(state) {
|
|
2038
|
+
this.pairing = state;
|
|
2039
|
+
if (!this.bonjour) return;
|
|
2040
|
+
if (this.service) {
|
|
2041
|
+
this.service.stop?.(() => {
|
|
2042
|
+
});
|
|
2043
|
+
this.service = null;
|
|
2044
|
+
}
|
|
2045
|
+
this.service = this.bonjour.publish({
|
|
2046
|
+
name: "Sessix",
|
|
2047
|
+
type: "sessix",
|
|
2048
|
+
port: this.wsPort,
|
|
2049
|
+
txt: {
|
|
2050
|
+
version: this.version,
|
|
2051
|
+
httpPort: String(this.httpPort),
|
|
2052
|
+
wsPort: String(this.wsPort),
|
|
2053
|
+
pairing: state
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
1998
2057
|
};
|
|
1999
2058
|
|
|
2000
2059
|
// src/hooks/HookInstaller.ts
|
|
@@ -2240,8 +2299,8 @@ var NotificationService = class {
|
|
|
2240
2299
|
if (entry) entry.enabled = enabled;
|
|
2241
2300
|
}
|
|
2242
2301
|
/** 注册手机 push token(连接建立时由 WsBridge 调用) */
|
|
2243
|
-
addPushToken(token) {
|
|
2244
|
-
this.expoChannel?.addToken(token);
|
|
2302
|
+
addPushToken(token, ws) {
|
|
2303
|
+
this.expoChannel?.addToken(token, ws);
|
|
2245
2304
|
}
|
|
2246
2305
|
/** 移除手机 push token(断线时或手机主动注销时调用) */
|
|
2247
2306
|
removePushToken(token) {
|
|
@@ -2481,17 +2540,21 @@ var MacNotificationChannel = class {
|
|
|
2481
2540
|
var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
|
|
2482
2541
|
var ExpoNotificationChannel = class {
|
|
2483
2542
|
tokens = /* @__PURE__ */ new Set();
|
|
2543
|
+
/** push token → WebSocket 连接映射,用于前台抑制 */
|
|
2544
|
+
tokenWsMap = /* @__PURE__ */ new Map();
|
|
2484
2545
|
/** per-token 通知音效偏好 */
|
|
2485
2546
|
soundPreferences = /* @__PURE__ */ new Map();
|
|
2486
2547
|
isAvailable() {
|
|
2487
2548
|
return this.tokens.size > 0;
|
|
2488
2549
|
}
|
|
2489
|
-
addToken(token) {
|
|
2550
|
+
addToken(token, ws) {
|
|
2490
2551
|
this.tokens.add(token);
|
|
2552
|
+
if (ws) this.tokenWsMap.set(token, ws);
|
|
2491
2553
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
|
|
2492
2554
|
}
|
|
2493
2555
|
removeToken(token) {
|
|
2494
2556
|
this.tokens.delete(token);
|
|
2557
|
+
this.tokenWsMap.delete(token);
|
|
2495
2558
|
this.soundPreferences.delete(token);
|
|
2496
2559
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
|
|
2497
2560
|
}
|
|
@@ -2504,7 +2567,12 @@ var ExpoNotificationChannel = class {
|
|
|
2504
2567
|
}
|
|
2505
2568
|
async send(payload) {
|
|
2506
2569
|
if (this.tokens.size === 0) return;
|
|
2507
|
-
const
|
|
2570
|
+
const offlineTokens = Array.from(this.tokens).filter((token) => {
|
|
2571
|
+
const ws = this.tokenWsMap.get(token);
|
|
2572
|
+
return !ws || ws.readyState !== ws.OPEN;
|
|
2573
|
+
});
|
|
2574
|
+
if (offlineTokens.length === 0) return;
|
|
2575
|
+
const messages = offlineTokens.map((to) => {
|
|
2508
2576
|
let sound = payload.sound ?? "default";
|
|
2509
2577
|
const prefs = this.soundPreferences.get(to);
|
|
2510
2578
|
if (prefs) {
|
|
@@ -2524,7 +2592,7 @@ var ExpoNotificationChannel = class {
|
|
|
2524
2592
|
};
|
|
2525
2593
|
});
|
|
2526
2594
|
try {
|
|
2527
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}
|
|
2595
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
|
|
2528
2596
|
const res = await fetch(EXPO_PUSH_API, {
|
|
2529
2597
|
method: "POST",
|
|
2530
2598
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -3027,6 +3095,180 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3027
3095
|
}
|
|
3028
3096
|
}
|
|
3029
3097
|
|
|
3098
|
+
// src/pairing/PairingManager.ts
|
|
3099
|
+
var PairingManager = class {
|
|
3100
|
+
_state = "closed";
|
|
3101
|
+
timer = null;
|
|
3102
|
+
deadline = 0;
|
|
3103
|
+
token;
|
|
3104
|
+
serverName;
|
|
3105
|
+
version;
|
|
3106
|
+
defaultDuration;
|
|
3107
|
+
onStateChange;
|
|
3108
|
+
constructor(opts) {
|
|
3109
|
+
this.token = opts.token;
|
|
3110
|
+
this.serverName = opts.serverName;
|
|
3111
|
+
this.version = opts.version;
|
|
3112
|
+
this.defaultDuration = opts.defaultDuration ?? 3e5;
|
|
3113
|
+
this.onStateChange = opts.onStateChange;
|
|
3114
|
+
}
|
|
3115
|
+
get state() {
|
|
3116
|
+
return this._state;
|
|
3117
|
+
}
|
|
3118
|
+
open(duration) {
|
|
3119
|
+
const ms = duration ?? this.defaultDuration;
|
|
3120
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3121
|
+
this._state = "open";
|
|
3122
|
+
this.deadline = Date.now() + ms;
|
|
3123
|
+
this.timer = setTimeout(() => this.close(), ms);
|
|
3124
|
+
this.onStateChange("open");
|
|
3125
|
+
}
|
|
3126
|
+
close() {
|
|
3127
|
+
if (this.timer) {
|
|
3128
|
+
clearTimeout(this.timer);
|
|
3129
|
+
this.timer = null;
|
|
3130
|
+
}
|
|
3131
|
+
if (this._state === "closed") return;
|
|
3132
|
+
this._state = "closed";
|
|
3133
|
+
this.deadline = 0;
|
|
3134
|
+
this.onStateChange("closed");
|
|
3135
|
+
}
|
|
3136
|
+
tryPair() {
|
|
3137
|
+
if (this._state !== "open") return null;
|
|
3138
|
+
const result = { token: this.token, serverName: this.serverName, version: this.version };
|
|
3139
|
+
this.close();
|
|
3140
|
+
return result;
|
|
3141
|
+
}
|
|
3142
|
+
getRemainingSeconds() {
|
|
3143
|
+
if (this._state !== "open") return 0;
|
|
3144
|
+
return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
|
|
3145
|
+
}
|
|
3146
|
+
destroy() {
|
|
3147
|
+
if (this.timer) {
|
|
3148
|
+
clearTimeout(this.timer);
|
|
3149
|
+
this.timer = null;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
};
|
|
3153
|
+
|
|
3154
|
+
// src/auth/AuthManager.ts
|
|
3155
|
+
var import_child_process3 = require("child_process");
|
|
3156
|
+
var import_child_process4 = require("child_process");
|
|
3157
|
+
var import_util = require("util");
|
|
3158
|
+
var import_events2 = require("events");
|
|
3159
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
3160
|
+
var CLAUDE_PATH2 = findClaudePath();
|
|
3161
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3162
|
+
var AuthManager = class extends import_events2.EventEmitter {
|
|
3163
|
+
loginProcess = null;
|
|
3164
|
+
loginTimeout = null;
|
|
3165
|
+
urlSent = false;
|
|
3166
|
+
/** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
|
|
3167
|
+
async checkAuth() {
|
|
3168
|
+
try {
|
|
3169
|
+
const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
|
|
3170
|
+
timeout: 1e4
|
|
3171
|
+
});
|
|
3172
|
+
const parsed = JSON.parse(stdout.trim());
|
|
3173
|
+
return {
|
|
3174
|
+
loggedIn: !!parsed.loggedIn,
|
|
3175
|
+
email: parsed.email,
|
|
3176
|
+
authMethod: parsed.authMethod
|
|
3177
|
+
};
|
|
3178
|
+
} catch {
|
|
3179
|
+
return { loggedIn: false };
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
/** 启动登录流程,捕获 URL 并通过事件推送 */
|
|
3183
|
+
async startLogin() {
|
|
3184
|
+
if (this.loginProcess) {
|
|
3185
|
+
this.loginProcess.kill();
|
|
3186
|
+
this.loginProcess = null;
|
|
3187
|
+
}
|
|
3188
|
+
this.clearLoginTimeout();
|
|
3189
|
+
this.urlSent = false;
|
|
3190
|
+
const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
|
|
3191
|
+
env: { ...process.env, BROWSER: "echo" },
|
|
3192
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3193
|
+
});
|
|
3194
|
+
this.loginProcess = proc;
|
|
3195
|
+
const handleOutput = (data) => {
|
|
3196
|
+
const text = data.toString();
|
|
3197
|
+
console.log(`[AuthManager] login output: ${text.trim()}`);
|
|
3198
|
+
if (!this.urlSent) {
|
|
3199
|
+
const url = this.extractUrl(text);
|
|
3200
|
+
if (url) {
|
|
3201
|
+
this.urlSent = true;
|
|
3202
|
+
console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
|
|
3203
|
+
this.emit("login_url", url);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
};
|
|
3207
|
+
proc.stdout?.on("data", handleOutput);
|
|
3208
|
+
proc.stderr?.on("data", handleOutput);
|
|
3209
|
+
proc.on("exit", (code) => {
|
|
3210
|
+
console.log(`[AuthManager] login process exited with code ${code}`);
|
|
3211
|
+
this.loginProcess = null;
|
|
3212
|
+
this.clearLoginTimeout();
|
|
3213
|
+
this.checkAuth().then((status) => {
|
|
3214
|
+
if (status.loggedIn) {
|
|
3215
|
+
this.emit("login_result", { success: true });
|
|
3216
|
+
} else if (code !== 0) {
|
|
3217
|
+
this.emit("login_result", { success: false, error: `Exit code: ${code}` });
|
|
3218
|
+
}
|
|
3219
|
+
});
|
|
3220
|
+
});
|
|
3221
|
+
proc.on("error", (err) => {
|
|
3222
|
+
console.error(`[AuthManager] login process error:`, err.message);
|
|
3223
|
+
this.loginProcess = null;
|
|
3224
|
+
this.clearLoginTimeout();
|
|
3225
|
+
this.emit("login_result", { success: false, error: err.message });
|
|
3226
|
+
});
|
|
3227
|
+
this.loginTimeout = setTimeout(() => {
|
|
3228
|
+
if (this.loginProcess) {
|
|
3229
|
+
console.warn("[AuthManager] login process timed out, killing");
|
|
3230
|
+
this.loginProcess.kill();
|
|
3231
|
+
this.loginProcess = null;
|
|
3232
|
+
this.emit("login_result", { success: false, error: "Login timed out" });
|
|
3233
|
+
}
|
|
3234
|
+
}, LOGIN_TIMEOUT_MS);
|
|
3235
|
+
}
|
|
3236
|
+
/** 提交授权码到登录进程的 stdin */
|
|
3237
|
+
submitCode(code) {
|
|
3238
|
+
if (!this.loginProcess?.stdin?.writable) {
|
|
3239
|
+
console.warn("[AuthManager] No active login process");
|
|
3240
|
+
return false;
|
|
3241
|
+
}
|
|
3242
|
+
console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
|
|
3243
|
+
this.loginProcess.stdin.write(code + "\n");
|
|
3244
|
+
return true;
|
|
3245
|
+
}
|
|
3246
|
+
/** 是否有登录进程在运行 */
|
|
3247
|
+
get isLoginInProgress() {
|
|
3248
|
+
return this.loginProcess !== null;
|
|
3249
|
+
}
|
|
3250
|
+
/** 清理资源 */
|
|
3251
|
+
destroy() {
|
|
3252
|
+
this.clearLoginTimeout();
|
|
3253
|
+
if (this.loginProcess) {
|
|
3254
|
+
this.loginProcess.kill();
|
|
3255
|
+
this.loginProcess = null;
|
|
3256
|
+
}
|
|
3257
|
+
this.removeAllListeners();
|
|
3258
|
+
}
|
|
3259
|
+
clearLoginTimeout() {
|
|
3260
|
+
if (this.loginTimeout) {
|
|
3261
|
+
clearTimeout(this.loginTimeout);
|
|
3262
|
+
this.loginTimeout = null;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
/** 从文本中提取 URL */
|
|
3266
|
+
extractUrl(text) {
|
|
3267
|
+
const match = text.match(/https?:\/\/[^\s"'<>]+/);
|
|
3268
|
+
return match ? match[0] : null;
|
|
3269
|
+
}
|
|
3270
|
+
};
|
|
3271
|
+
|
|
3030
3272
|
// src/server.ts
|
|
3031
3273
|
var import_promises5 = require("fs/promises");
|
|
3032
3274
|
var WS_PORT = 3745;
|
|
@@ -3105,9 +3347,32 @@ async function start(opts = {}) {
|
|
|
3105
3347
|
HTTP_PORT,
|
|
3106
3348
|
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
3107
3349
|
);
|
|
3350
|
+
let mdnsService = null;
|
|
3351
|
+
const pairingManager = new PairingManager({
|
|
3352
|
+
token,
|
|
3353
|
+
serverName: (0, import_node_os4.hostname)(),
|
|
3354
|
+
version: "0.2.0",
|
|
3355
|
+
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
3356
|
+
});
|
|
3357
|
+
approvalProxy.setPairingManager(pairingManager);
|
|
3358
|
+
if (opts.enablePairing !== false) {
|
|
3359
|
+
pairingManager.open();
|
|
3360
|
+
}
|
|
3361
|
+
const authManager = new AuthManager();
|
|
3362
|
+
authManager.on("login_url", (url) => {
|
|
3363
|
+
wsBridge.broadcast({ type: "auth_login_url", url });
|
|
3364
|
+
});
|
|
3365
|
+
authManager.on("login_result", (result) => {
|
|
3366
|
+
wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
|
|
3367
|
+
if (result.success) {
|
|
3368
|
+
authManager.checkAuth().then((status) => {
|
|
3369
|
+
wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
});
|
|
3108
3373
|
const unreadSessionIds = /* @__PURE__ */ new Set();
|
|
3109
3374
|
notificationService.setGlobalPendingCountProvider(
|
|
3110
|
-
() => approvalProxy.getPendingCount() + unreadSessionIds.size
|
|
3375
|
+
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
3111
3376
|
);
|
|
3112
3377
|
const broadcastUnreadSessions = () => {
|
|
3113
3378
|
wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
@@ -3322,7 +3587,7 @@ async function start(opts = {}) {
|
|
|
3322
3587
|
break;
|
|
3323
3588
|
}
|
|
3324
3589
|
case "register_push_token": {
|
|
3325
|
-
notificationService.addPushToken(event.token);
|
|
3590
|
+
notificationService.addPushToken(event.token, ws);
|
|
3326
3591
|
break;
|
|
3327
3592
|
}
|
|
3328
3593
|
case "unregister_push_token": {
|
|
@@ -3361,6 +3626,22 @@ async function start(opts = {}) {
|
|
|
3361
3626
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
3362
3627
|
break;
|
|
3363
3628
|
}
|
|
3629
|
+
case "check_auth": {
|
|
3630
|
+
const status = await authManager.checkAuth();
|
|
3631
|
+
wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
|
|
3632
|
+
break;
|
|
3633
|
+
}
|
|
3634
|
+
case "start_auth_login": {
|
|
3635
|
+
await authManager.startLogin();
|
|
3636
|
+
break;
|
|
3637
|
+
}
|
|
3638
|
+
case "submit_auth_code": {
|
|
3639
|
+
const submitted = authManager.submitCode(event.code);
|
|
3640
|
+
if (!submitted) {
|
|
3641
|
+
wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
|
|
3642
|
+
}
|
|
3643
|
+
break;
|
|
3644
|
+
}
|
|
3364
3645
|
default: {
|
|
3365
3646
|
wsBridge.send(ws, {
|
|
3366
3647
|
type: "error",
|
|
@@ -3438,10 +3719,13 @@ async function start(opts = {}) {
|
|
|
3438
3719
|
connections: wsBridge.getConnectionCount(),
|
|
3439
3720
|
activeSessions: sessionManager.getActiveSessions().length
|
|
3440
3721
|
}));
|
|
3441
|
-
let mdnsService = null;
|
|
3442
3722
|
const startMdns = () => {
|
|
3443
3723
|
if (mdnsService) return;
|
|
3444
|
-
mdnsService = new MdnsService({
|
|
3724
|
+
mdnsService = new MdnsService({
|
|
3725
|
+
wsPort: WS_PORT,
|
|
3726
|
+
httpPort: HTTP_PORT,
|
|
3727
|
+
pairing: pairingManager.state
|
|
3728
|
+
});
|
|
3445
3729
|
mdnsService.start();
|
|
3446
3730
|
};
|
|
3447
3731
|
const stopMdns = () => {
|
|
@@ -3476,7 +3760,9 @@ async function start(opts = {}) {
|
|
|
3476
3760
|
errors.push(err);
|
|
3477
3761
|
}
|
|
3478
3762
|
};
|
|
3763
|
+
await attempt(() => authManager.destroy(), "AuthManager");
|
|
3479
3764
|
await attempt(() => stopMdns(), "mDNS");
|
|
3765
|
+
await attempt(() => pairingManager.destroy(), "PairingManager");
|
|
3480
3766
|
await attempt(() => wsBridge.close(), "WebSocket");
|
|
3481
3767
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
3482
3768
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
@@ -3504,7 +3790,9 @@ async function start(opts = {}) {
|
|
|
3504
3790
|
} else {
|
|
3505
3791
|
stopMdns();
|
|
3506
3792
|
}
|
|
3507
|
-
}
|
|
3793
|
+
},
|
|
3794
|
+
openPairing: (duration) => pairingManager.open(duration),
|
|
3795
|
+
closePairing: () => pairingManager.close()
|
|
3508
3796
|
};
|
|
3509
3797
|
}
|
|
3510
3798
|
|
|
@@ -3552,6 +3840,8 @@ async function main() {
|
|
|
3552
3840
|
console.log();
|
|
3553
3841
|
console.log(t("startup.waitingConnection"));
|
|
3554
3842
|
console.log();
|
|
3843
|
+
console.log(t("startup.pairingOpen"));
|
|
3844
|
+
console.log();
|
|
3555
3845
|
const shutdown = async (signal) => {
|
|
3556
3846
|
console.log(`
|
|
3557
3847
|
[Main] ${t("startup.receivedSignal", { signal })}`);
|
|
@@ -3564,8 +3854,24 @@ async function main() {
|
|
|
3564
3854
|
process.exit(1);
|
|
3565
3855
|
}
|
|
3566
3856
|
};
|
|
3567
|
-
process.
|
|
3568
|
-
|
|
3857
|
+
if (process.stdin.isTTY) {
|
|
3858
|
+
process.stdin.setRawMode(true);
|
|
3859
|
+
process.stdin.resume();
|
|
3860
|
+
process.stdin.setEncoding("utf8");
|
|
3861
|
+
process.stdin.on("data", (key) => {
|
|
3862
|
+
if (key === "p" || key === "P") {
|
|
3863
|
+
server.openPairing();
|
|
3864
|
+
console.log(`
|
|
3865
|
+
${t("startup.pairingReopened")}`);
|
|
3866
|
+
}
|
|
3867
|
+
if (key === "") {
|
|
3868
|
+
shutdown("SIGINT");
|
|
3869
|
+
}
|
|
3870
|
+
});
|
|
3871
|
+
} else {
|
|
3872
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
3873
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
3874
|
+
}
|
|
3569
3875
|
}
|
|
3570
3876
|
function getLocalIp() {
|
|
3571
3877
|
const interfaces = (0, import_node_os5.networkInterfaces)();
|
package/dist/server.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface ServerInstance {
|
|
|
15
15
|
onServerEvent: (cb: (event: ServerEvent) => void) => () => void;
|
|
16
16
|
/** 运行时切换 mDNS 自动发现 */
|
|
17
17
|
setAutoConnect: (enabled: boolean) => void;
|
|
18
|
+
/** 运行时开启配对窗口 */
|
|
19
|
+
openPairing: (duration?: number) => void;
|
|
20
|
+
/** 运行时关闭配对窗口 */
|
|
21
|
+
closePairing: () => void;
|
|
18
22
|
}
|
|
19
23
|
interface ServerOptions {
|
|
20
24
|
/** 覆盖 token(默认读取 ~/.sessix/token 或自动生成) */
|
|
@@ -30,6 +34,8 @@ interface ServerOptions {
|
|
|
30
34
|
};
|
|
31
35
|
/** 是否启用 mDNS 自动发现(默认 true) */
|
|
32
36
|
enableAutoConnect?: boolean;
|
|
37
|
+
/** 是否启用配对模式(默认 true,Electron 传 false) */
|
|
38
|
+
enablePairing?: boolean;
|
|
33
39
|
}
|
|
34
40
|
declare function start(opts?: ServerOptions): Promise<ServerInstance>;
|
|
35
41
|
|
package/dist/server.js
CHANGED
|
@@ -51,6 +51,8 @@ var zh = {
|
|
|
51
51
|
autoDiscoveryOn: " \u{1F4A1} \u81EA\u52A8\u53D1\u73B0\u5DF2\u542F\u7528\uFF0C\u540C\u7F51\u6BB5\u624B\u673A\u53EF\u81EA\u52A8\u8FDE\u63A5",
|
|
52
52
|
autoDiscoveryHint: " \u5982\u5728\u516C\u5171\u7F51\u7EDC\uFF0C\u5EFA\u8BAE\u5173\u95ED: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
53
53
|
autoDiscoveryOff: " \u2139\uFE0F \u81EA\u52A8\u53D1\u73B0\u5DF2\u5173\u95ED\uFF0C\u624B\u673A\u9700\u624B\u52A8\u8F93\u5165\u5730\u5740\u8FDE\u63A5",
|
|
54
|
+
pairingOpen: " \u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u5F00\u542F\uFF085 \u5206\u949F\u5185\u6709\u6548\uFF09\u2014 \u6309 p \u91CD\u65B0\u5F00\u542F",
|
|
55
|
+
pairingReopened: "\u{1F513} \u914D\u5BF9\u6A21\u5F0F\u5DF2\u91CD\u65B0\u5F00\u542F\uFF085 \u5206\u949F\uFF09",
|
|
54
56
|
receivedSignal: "\u6536\u5230 {{signal}}\uFF0C\u6B63\u5728\u4F18\u96C5\u5173\u95ED...",
|
|
55
57
|
goodbye: "\u6240\u6709\u670D\u52A1\u5DF2\u5173\u95ED\uFF0C\u518D\u89C1\uFF01",
|
|
56
58
|
shutdownError: "\u5173\u95ED\u8FC7\u7A0B\u51FA\u9519:",
|
|
@@ -77,7 +79,8 @@ var zh = {
|
|
|
77
79
|
restarting: "\u91CD\u65B0\u542F\u52A8 {{label}}...",
|
|
78
80
|
activityPushEnabled: "ActivityKit Push \u5DF2\u542F\u7528",
|
|
79
81
|
activityPushFailed: "ActivityKit Push \u521D\u59CB\u5316\u5931\u8D25:",
|
|
80
|
-
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09"
|
|
82
|
+
activityPushContinue: "\u7EE7\u7EED\u542F\u52A8\uFF08Live Activity \u540E\u53F0\u63A8\u9001\u4E0D\u53EF\u7528\uFF09",
|
|
83
|
+
noActiveLoginProcess: "\u6CA1\u6709\u6D3B\u8DC3\u7684\u767B\u5F55\u8FDB\u7A0B"
|
|
81
84
|
},
|
|
82
85
|
ws: {
|
|
83
86
|
started: "WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 {{port}}",
|
|
@@ -152,6 +155,8 @@ var en = {
|
|
|
152
155
|
autoDiscoveryOn: " Auto-discovery enabled, phones on the same network can connect automatically",
|
|
153
156
|
autoDiscoveryHint: " On public networks, disable with: SESSIX_AUTO_CONNECT=false npx sessix-server",
|
|
154
157
|
autoDiscoveryOff: " Auto-discovery disabled, phone must enter address manually",
|
|
158
|
+
pairingOpen: " \u{1F513} Pairing mode open (5 min) \u2014 press p to reopen",
|
|
159
|
+
pairingReopened: "\u{1F513} Pairing mode reopened (5 min)",
|
|
155
160
|
receivedSignal: "Received {{signal}}, graceful shutdown...",
|
|
156
161
|
goodbye: "All services closed, goodbye!",
|
|
157
162
|
shutdownError: "Shutdown error:",
|
|
@@ -178,7 +183,8 @@ var en = {
|
|
|
178
183
|
restarting: "Restarting {{label}}...",
|
|
179
184
|
activityPushEnabled: "ActivityKit Push enabled",
|
|
180
185
|
activityPushFailed: "ActivityKit Push init failed:",
|
|
181
|
-
activityPushContinue: "Continuing startup (Live Activity background push unavailable)"
|
|
186
|
+
activityPushContinue: "Continuing startup (Live Activity background push unavailable)",
|
|
187
|
+
noActiveLoginProcess: "No active login process"
|
|
182
188
|
},
|
|
183
189
|
ws: {
|
|
184
190
|
started: "WebSocket server started on port {{port}}",
|
|
@@ -291,11 +297,14 @@ var import_node_child_process2 = require("child_process");
|
|
|
291
297
|
var import_node_util = require("util");
|
|
292
298
|
|
|
293
299
|
// src/providers/ProcessProvider.ts
|
|
294
|
-
var
|
|
300
|
+
var import_child_process2 = require("child_process");
|
|
295
301
|
var import_readline = require("readline");
|
|
296
302
|
var import_events = require("events");
|
|
297
303
|
var import_node_os = require("os");
|
|
298
304
|
var import_uuid = require("uuid");
|
|
305
|
+
|
|
306
|
+
// src/utils/claudePath.ts
|
|
307
|
+
var import_child_process = require("child_process");
|
|
299
308
|
function findClaudePath() {
|
|
300
309
|
try {
|
|
301
310
|
return (0, import_child_process.execSync)("which claude", { encoding: "utf-8" }).trim();
|
|
@@ -315,6 +324,8 @@ function findClaudePath() {
|
|
|
315
324
|
return "claude";
|
|
316
325
|
}
|
|
317
326
|
}
|
|
327
|
+
|
|
328
|
+
// src/providers/ProcessProvider.ts
|
|
318
329
|
var CLAUDE_PATH = findClaudePath();
|
|
319
330
|
var ProcessProvider = class {
|
|
320
331
|
/** 活跃会话映射表:sessionId -> { session, process } */
|
|
@@ -507,7 +518,7 @@ var ProcessProvider = class {
|
|
|
507
518
|
}
|
|
508
519
|
const env = { ...process.env, SESSIX_SESSION_ID: sessionId };
|
|
509
520
|
delete env.CLAUDECODE;
|
|
510
|
-
const proc = (0,
|
|
521
|
+
const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, args, {
|
|
511
522
|
cwd: projectPath,
|
|
512
523
|
env,
|
|
513
524
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -714,7 +725,7 @@ ${context}`;
|
|
|
714
725
|
return new Promise((resolve, reject) => {
|
|
715
726
|
const env = { ...process.env };
|
|
716
727
|
delete env.CLAUDECODE;
|
|
717
|
-
const proc = (0,
|
|
728
|
+
const proc = (0, import_child_process2.spawn)(CLAUDE_PATH, ["-p", prompt, "--output-format", "text"], {
|
|
718
729
|
cwd: (0, import_node_os.homedir)(),
|
|
719
730
|
env,
|
|
720
731
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -1625,6 +1636,7 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1625
1636
|
alwaysAllowedTools = /* @__PURE__ */ new Set();
|
|
1626
1637
|
/** 获取状态信息的回调(由外部注入) */
|
|
1627
1638
|
statusInfoProvider = null;
|
|
1639
|
+
pairingManager = null;
|
|
1628
1640
|
constructor(options) {
|
|
1629
1641
|
this.token = options.token;
|
|
1630
1642
|
this.port = options.port;
|
|
@@ -1659,6 +1671,10 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1659
1671
|
setStatusInfoProvider(provider) {
|
|
1660
1672
|
this.statusInfoProvider = provider;
|
|
1661
1673
|
}
|
|
1674
|
+
/** 设置配对管理器 */
|
|
1675
|
+
setPairingManager(manager) {
|
|
1676
|
+
this.pairingManager = manager;
|
|
1677
|
+
}
|
|
1662
1678
|
/** 设置会话的 YOLO 模式(服务端拦截,即使手机断连也生效) */
|
|
1663
1679
|
setYoloMode(sessionId, enabled) {
|
|
1664
1680
|
this.yoloSessions.set(sessionId, enabled);
|
|
@@ -1819,6 +1835,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1819
1835
|
const pathname = url.pathname;
|
|
1820
1836
|
if (req.method === "POST" && pathname === "/hook/approval") {
|
|
1821
1837
|
this.handleApprovalHook(req, res);
|
|
1838
|
+
} else if (req.method === "POST" && pathname === "/pair") {
|
|
1839
|
+
this.handlePair(req, res);
|
|
1822
1840
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
1823
1841
|
this.handleHealth(req, res);
|
|
1824
1842
|
} else if (req.method === "GET" && pathname === "/token") {
|
|
@@ -1889,6 +1907,23 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
1889
1907
|
activeSessions: info.activeSessions
|
|
1890
1908
|
});
|
|
1891
1909
|
}
|
|
1910
|
+
/** 配对端点:配对窗口开放时返回 token */
|
|
1911
|
+
handlePair(_req, res) {
|
|
1912
|
+
if (!this.pairingManager) {
|
|
1913
|
+
this.sendJson(res, 503, { error: "pairing_unavailable" });
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const result = this.pairingManager.tryPair();
|
|
1917
|
+
if (result) {
|
|
1918
|
+
console.log("[ApprovalProxy] Device paired successfully");
|
|
1919
|
+
this.sendJson(res, 200, result);
|
|
1920
|
+
} else {
|
|
1921
|
+
this.sendJson(res, 403, {
|
|
1922
|
+
error: "pairing_closed",
|
|
1923
|
+
message: "Pairing window is closed. Restart server or press p to reopen."
|
|
1924
|
+
});
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1892
1927
|
/** 返回连接 token(仅本机访问) */
|
|
1893
1928
|
handleToken(req, res) {
|
|
1894
1929
|
const remoteAddress = req.socket.remoteAddress;
|
|
@@ -1959,12 +1994,12 @@ var MdnsService = class {
|
|
|
1959
1994
|
wsPort;
|
|
1960
1995
|
httpPort;
|
|
1961
1996
|
version;
|
|
1962
|
-
|
|
1997
|
+
pairing;
|
|
1963
1998
|
constructor(options) {
|
|
1964
1999
|
this.wsPort = options.wsPort;
|
|
1965
2000
|
this.httpPort = options.httpPort;
|
|
1966
2001
|
this.version = options.version ?? "0.1.0";
|
|
1967
|
-
this.
|
|
2002
|
+
this.pairing = options.pairing ?? "closed";
|
|
1968
2003
|
}
|
|
1969
2004
|
/**
|
|
1970
2005
|
* 启动 mDNS 广播
|
|
@@ -1982,7 +2017,8 @@ var MdnsService = class {
|
|
|
1982
2017
|
txt: {
|
|
1983
2018
|
version: this.version,
|
|
1984
2019
|
httpPort: String(this.httpPort),
|
|
1985
|
-
|
|
2020
|
+
wsPort: String(this.wsPort),
|
|
2021
|
+
pairing: this.pairing
|
|
1986
2022
|
}
|
|
1987
2023
|
});
|
|
1988
2024
|
console.log(`[MdnsService] ${t("mdns.started", { port: this.wsPort })}`);
|
|
@@ -2003,6 +2039,29 @@ var MdnsService = class {
|
|
|
2003
2039
|
}
|
|
2004
2040
|
console.log(`[MdnsService] ${t("mdns.closed")}`);
|
|
2005
2041
|
}
|
|
2042
|
+
/**
|
|
2043
|
+
* 更新配对状态(重新发布 mDNS 服务)
|
|
2044
|
+
*/
|
|
2045
|
+
updatePairingState(state) {
|
|
2046
|
+
this.pairing = state;
|
|
2047
|
+
if (!this.bonjour) return;
|
|
2048
|
+
if (this.service) {
|
|
2049
|
+
this.service.stop?.(() => {
|
|
2050
|
+
});
|
|
2051
|
+
this.service = null;
|
|
2052
|
+
}
|
|
2053
|
+
this.service = this.bonjour.publish({
|
|
2054
|
+
name: "Sessix",
|
|
2055
|
+
type: "sessix",
|
|
2056
|
+
port: this.wsPort,
|
|
2057
|
+
txt: {
|
|
2058
|
+
version: this.version,
|
|
2059
|
+
httpPort: String(this.httpPort),
|
|
2060
|
+
wsPort: String(this.wsPort),
|
|
2061
|
+
pairing: state
|
|
2062
|
+
}
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2006
2065
|
};
|
|
2007
2066
|
|
|
2008
2067
|
// src/hooks/HookInstaller.ts
|
|
@@ -2248,8 +2307,8 @@ var NotificationService = class {
|
|
|
2248
2307
|
if (entry) entry.enabled = enabled;
|
|
2249
2308
|
}
|
|
2250
2309
|
/** 注册手机 push token(连接建立时由 WsBridge 调用) */
|
|
2251
|
-
addPushToken(token) {
|
|
2252
|
-
this.expoChannel?.addToken(token);
|
|
2310
|
+
addPushToken(token, ws) {
|
|
2311
|
+
this.expoChannel?.addToken(token, ws);
|
|
2253
2312
|
}
|
|
2254
2313
|
/** 移除手机 push token(断线时或手机主动注销时调用) */
|
|
2255
2314
|
removePushToken(token) {
|
|
@@ -2489,17 +2548,21 @@ var MacNotificationChannel = class {
|
|
|
2489
2548
|
var EXPO_PUSH_API = "https://exp.host/--/api/v2/push/send";
|
|
2490
2549
|
var ExpoNotificationChannel = class {
|
|
2491
2550
|
tokens = /* @__PURE__ */ new Set();
|
|
2551
|
+
/** push token → WebSocket 连接映射,用于前台抑制 */
|
|
2552
|
+
tokenWsMap = /* @__PURE__ */ new Map();
|
|
2492
2553
|
/** per-token 通知音效偏好 */
|
|
2493
2554
|
soundPreferences = /* @__PURE__ */ new Map();
|
|
2494
2555
|
isAvailable() {
|
|
2495
2556
|
return this.tokens.size > 0;
|
|
2496
2557
|
}
|
|
2497
|
-
addToken(token) {
|
|
2558
|
+
addToken(token, ws) {
|
|
2498
2559
|
this.tokens.add(token);
|
|
2560
|
+
if (ws) this.tokenWsMap.set(token, ws);
|
|
2499
2561
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRegistered", { count: this.tokens.size })}`);
|
|
2500
2562
|
}
|
|
2501
2563
|
removeToken(token) {
|
|
2502
2564
|
this.tokens.delete(token);
|
|
2565
|
+
this.tokenWsMap.delete(token);
|
|
2503
2566
|
this.soundPreferences.delete(token);
|
|
2504
2567
|
console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
|
|
2505
2568
|
}
|
|
@@ -2512,7 +2575,12 @@ var ExpoNotificationChannel = class {
|
|
|
2512
2575
|
}
|
|
2513
2576
|
async send(payload) {
|
|
2514
2577
|
if (this.tokens.size === 0) return;
|
|
2515
|
-
const
|
|
2578
|
+
const offlineTokens = Array.from(this.tokens).filter((token) => {
|
|
2579
|
+
const ws = this.tokenWsMap.get(token);
|
|
2580
|
+
return !ws || ws.readyState !== ws.OPEN;
|
|
2581
|
+
});
|
|
2582
|
+
if (offlineTokens.length === 0) return;
|
|
2583
|
+
const messages = offlineTokens.map((to) => {
|
|
2516
2584
|
let sound = payload.sound ?? "default";
|
|
2517
2585
|
const prefs = this.soundPreferences.get(to);
|
|
2518
2586
|
if (prefs) {
|
|
@@ -2532,7 +2600,7 @@ var ExpoNotificationChannel = class {
|
|
|
2532
2600
|
};
|
|
2533
2601
|
});
|
|
2534
2602
|
try {
|
|
2535
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")}
|
|
2603
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${offlineTokens.length}/${this.tokens.size} devices)`, offlineTokens);
|
|
2536
2604
|
const res = await fetch(EXPO_PUSH_API, {
|
|
2537
2605
|
method: "POST",
|
|
2538
2606
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -3035,6 +3103,180 @@ async function countJsonlFilesWithMtime(dirPath) {
|
|
|
3035
3103
|
}
|
|
3036
3104
|
}
|
|
3037
3105
|
|
|
3106
|
+
// src/pairing/PairingManager.ts
|
|
3107
|
+
var PairingManager = class {
|
|
3108
|
+
_state = "closed";
|
|
3109
|
+
timer = null;
|
|
3110
|
+
deadline = 0;
|
|
3111
|
+
token;
|
|
3112
|
+
serverName;
|
|
3113
|
+
version;
|
|
3114
|
+
defaultDuration;
|
|
3115
|
+
onStateChange;
|
|
3116
|
+
constructor(opts) {
|
|
3117
|
+
this.token = opts.token;
|
|
3118
|
+
this.serverName = opts.serverName;
|
|
3119
|
+
this.version = opts.version;
|
|
3120
|
+
this.defaultDuration = opts.defaultDuration ?? 3e5;
|
|
3121
|
+
this.onStateChange = opts.onStateChange;
|
|
3122
|
+
}
|
|
3123
|
+
get state() {
|
|
3124
|
+
return this._state;
|
|
3125
|
+
}
|
|
3126
|
+
open(duration) {
|
|
3127
|
+
const ms = duration ?? this.defaultDuration;
|
|
3128
|
+
if (this.timer) clearTimeout(this.timer);
|
|
3129
|
+
this._state = "open";
|
|
3130
|
+
this.deadline = Date.now() + ms;
|
|
3131
|
+
this.timer = setTimeout(() => this.close(), ms);
|
|
3132
|
+
this.onStateChange("open");
|
|
3133
|
+
}
|
|
3134
|
+
close() {
|
|
3135
|
+
if (this.timer) {
|
|
3136
|
+
clearTimeout(this.timer);
|
|
3137
|
+
this.timer = null;
|
|
3138
|
+
}
|
|
3139
|
+
if (this._state === "closed") return;
|
|
3140
|
+
this._state = "closed";
|
|
3141
|
+
this.deadline = 0;
|
|
3142
|
+
this.onStateChange("closed");
|
|
3143
|
+
}
|
|
3144
|
+
tryPair() {
|
|
3145
|
+
if (this._state !== "open") return null;
|
|
3146
|
+
const result = { token: this.token, serverName: this.serverName, version: this.version };
|
|
3147
|
+
this.close();
|
|
3148
|
+
return result;
|
|
3149
|
+
}
|
|
3150
|
+
getRemainingSeconds() {
|
|
3151
|
+
if (this._state !== "open") return 0;
|
|
3152
|
+
return Math.max(0, Math.ceil((this.deadline - Date.now()) / 1e3));
|
|
3153
|
+
}
|
|
3154
|
+
destroy() {
|
|
3155
|
+
if (this.timer) {
|
|
3156
|
+
clearTimeout(this.timer);
|
|
3157
|
+
this.timer = null;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
};
|
|
3161
|
+
|
|
3162
|
+
// src/auth/AuthManager.ts
|
|
3163
|
+
var import_child_process3 = require("child_process");
|
|
3164
|
+
var import_child_process4 = require("child_process");
|
|
3165
|
+
var import_util = require("util");
|
|
3166
|
+
var import_events2 = require("events");
|
|
3167
|
+
var execFileAsync = (0, import_util.promisify)(import_child_process4.execFile);
|
|
3168
|
+
var CLAUDE_PATH2 = findClaudePath();
|
|
3169
|
+
var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3170
|
+
var AuthManager = class extends import_events2.EventEmitter {
|
|
3171
|
+
loginProcess = null;
|
|
3172
|
+
loginTimeout = null;
|
|
3173
|
+
urlSent = false;
|
|
3174
|
+
/** 检查当前 Claude CLI 认证状态(异步,不阻塞事件循环) */
|
|
3175
|
+
async checkAuth() {
|
|
3176
|
+
try {
|
|
3177
|
+
const { stdout } = await execFileAsync(CLAUDE_PATH2, ["auth", "status"], {
|
|
3178
|
+
timeout: 1e4
|
|
3179
|
+
});
|
|
3180
|
+
const parsed = JSON.parse(stdout.trim());
|
|
3181
|
+
return {
|
|
3182
|
+
loggedIn: !!parsed.loggedIn,
|
|
3183
|
+
email: parsed.email,
|
|
3184
|
+
authMethod: parsed.authMethod
|
|
3185
|
+
};
|
|
3186
|
+
} catch {
|
|
3187
|
+
return { loggedIn: false };
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
/** 启动登录流程,捕获 URL 并通过事件推送 */
|
|
3191
|
+
async startLogin() {
|
|
3192
|
+
if (this.loginProcess) {
|
|
3193
|
+
this.loginProcess.kill();
|
|
3194
|
+
this.loginProcess = null;
|
|
3195
|
+
}
|
|
3196
|
+
this.clearLoginTimeout();
|
|
3197
|
+
this.urlSent = false;
|
|
3198
|
+
const proc = (0, import_child_process3.spawn)(CLAUDE_PATH2, ["auth", "login"], {
|
|
3199
|
+
env: { ...process.env, BROWSER: "echo" },
|
|
3200
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
3201
|
+
});
|
|
3202
|
+
this.loginProcess = proc;
|
|
3203
|
+
const handleOutput = (data) => {
|
|
3204
|
+
const text = data.toString();
|
|
3205
|
+
console.log(`[AuthManager] login output: ${text.trim()}`);
|
|
3206
|
+
if (!this.urlSent) {
|
|
3207
|
+
const url = this.extractUrl(text);
|
|
3208
|
+
if (url) {
|
|
3209
|
+
this.urlSent = true;
|
|
3210
|
+
console.log(`[AuthManager] \u6355\u83B7\u5230\u767B\u5F55 URL: ${url}`);
|
|
3211
|
+
this.emit("login_url", url);
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
};
|
|
3215
|
+
proc.stdout?.on("data", handleOutput);
|
|
3216
|
+
proc.stderr?.on("data", handleOutput);
|
|
3217
|
+
proc.on("exit", (code) => {
|
|
3218
|
+
console.log(`[AuthManager] login process exited with code ${code}`);
|
|
3219
|
+
this.loginProcess = null;
|
|
3220
|
+
this.clearLoginTimeout();
|
|
3221
|
+
this.checkAuth().then((status) => {
|
|
3222
|
+
if (status.loggedIn) {
|
|
3223
|
+
this.emit("login_result", { success: true });
|
|
3224
|
+
} else if (code !== 0) {
|
|
3225
|
+
this.emit("login_result", { success: false, error: `Exit code: ${code}` });
|
|
3226
|
+
}
|
|
3227
|
+
});
|
|
3228
|
+
});
|
|
3229
|
+
proc.on("error", (err) => {
|
|
3230
|
+
console.error(`[AuthManager] login process error:`, err.message);
|
|
3231
|
+
this.loginProcess = null;
|
|
3232
|
+
this.clearLoginTimeout();
|
|
3233
|
+
this.emit("login_result", { success: false, error: err.message });
|
|
3234
|
+
});
|
|
3235
|
+
this.loginTimeout = setTimeout(() => {
|
|
3236
|
+
if (this.loginProcess) {
|
|
3237
|
+
console.warn("[AuthManager] login process timed out, killing");
|
|
3238
|
+
this.loginProcess.kill();
|
|
3239
|
+
this.loginProcess = null;
|
|
3240
|
+
this.emit("login_result", { success: false, error: "Login timed out" });
|
|
3241
|
+
}
|
|
3242
|
+
}, LOGIN_TIMEOUT_MS);
|
|
3243
|
+
}
|
|
3244
|
+
/** 提交授权码到登录进程的 stdin */
|
|
3245
|
+
submitCode(code) {
|
|
3246
|
+
if (!this.loginProcess?.stdin?.writable) {
|
|
3247
|
+
console.warn("[AuthManager] No active login process");
|
|
3248
|
+
return false;
|
|
3249
|
+
}
|
|
3250
|
+
console.log(`[AuthManager] \u63D0\u4EA4\u6388\u6743\u7801`);
|
|
3251
|
+
this.loginProcess.stdin.write(code + "\n");
|
|
3252
|
+
return true;
|
|
3253
|
+
}
|
|
3254
|
+
/** 是否有登录进程在运行 */
|
|
3255
|
+
get isLoginInProgress() {
|
|
3256
|
+
return this.loginProcess !== null;
|
|
3257
|
+
}
|
|
3258
|
+
/** 清理资源 */
|
|
3259
|
+
destroy() {
|
|
3260
|
+
this.clearLoginTimeout();
|
|
3261
|
+
if (this.loginProcess) {
|
|
3262
|
+
this.loginProcess.kill();
|
|
3263
|
+
this.loginProcess = null;
|
|
3264
|
+
}
|
|
3265
|
+
this.removeAllListeners();
|
|
3266
|
+
}
|
|
3267
|
+
clearLoginTimeout() {
|
|
3268
|
+
if (this.loginTimeout) {
|
|
3269
|
+
clearTimeout(this.loginTimeout);
|
|
3270
|
+
this.loginTimeout = null;
|
|
3271
|
+
}
|
|
3272
|
+
}
|
|
3273
|
+
/** 从文本中提取 URL */
|
|
3274
|
+
extractUrl(text) {
|
|
3275
|
+
const match = text.match(/https?:\/\/[^\s"'<>]+/);
|
|
3276
|
+
return match ? match[0] : null;
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
|
|
3038
3280
|
// src/server.ts
|
|
3039
3281
|
var import_promises5 = require("fs/promises");
|
|
3040
3282
|
var WS_PORT = 3745;
|
|
@@ -3113,9 +3355,32 @@ async function start(opts = {}) {
|
|
|
3113
3355
|
HTTP_PORT,
|
|
3114
3356
|
() => ApprovalProxy.create({ port: HTTP_PORT, token })
|
|
3115
3357
|
);
|
|
3358
|
+
let mdnsService = null;
|
|
3359
|
+
const pairingManager = new PairingManager({
|
|
3360
|
+
token,
|
|
3361
|
+
serverName: (0, import_node_os4.hostname)(),
|
|
3362
|
+
version: "0.2.0",
|
|
3363
|
+
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
3364
|
+
});
|
|
3365
|
+
approvalProxy.setPairingManager(pairingManager);
|
|
3366
|
+
if (opts.enablePairing !== false) {
|
|
3367
|
+
pairingManager.open();
|
|
3368
|
+
}
|
|
3369
|
+
const authManager = new AuthManager();
|
|
3370
|
+
authManager.on("login_url", (url) => {
|
|
3371
|
+
wsBridge.broadcast({ type: "auth_login_url", url });
|
|
3372
|
+
});
|
|
3373
|
+
authManager.on("login_result", (result) => {
|
|
3374
|
+
wsBridge.broadcast({ type: "auth_login_result", success: result.success, error: result.error });
|
|
3375
|
+
if (result.success) {
|
|
3376
|
+
authManager.checkAuth().then((status) => {
|
|
3377
|
+
wsBridge.broadcast({ type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
|
|
3378
|
+
});
|
|
3379
|
+
}
|
|
3380
|
+
});
|
|
3116
3381
|
const unreadSessionIds = /* @__PURE__ */ new Set();
|
|
3117
3382
|
notificationService.setGlobalPendingCountProvider(
|
|
3118
|
-
() => approvalProxy.getPendingCount() + unreadSessionIds.size
|
|
3383
|
+
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
3119
3384
|
);
|
|
3120
3385
|
const broadcastUnreadSessions = () => {
|
|
3121
3386
|
wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
@@ -3330,7 +3595,7 @@ async function start(opts = {}) {
|
|
|
3330
3595
|
break;
|
|
3331
3596
|
}
|
|
3332
3597
|
case "register_push_token": {
|
|
3333
|
-
notificationService.addPushToken(event.token);
|
|
3598
|
+
notificationService.addPushToken(event.token, ws);
|
|
3334
3599
|
break;
|
|
3335
3600
|
}
|
|
3336
3601
|
case "unregister_push_token": {
|
|
@@ -3369,6 +3634,22 @@ async function start(opts = {}) {
|
|
|
3369
3634
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
3370
3635
|
break;
|
|
3371
3636
|
}
|
|
3637
|
+
case "check_auth": {
|
|
3638
|
+
const status = await authManager.checkAuth();
|
|
3639
|
+
wsBridge.send(ws, { type: "auth_status", loggedIn: status.loggedIn, email: status.email, authMethod: status.authMethod });
|
|
3640
|
+
break;
|
|
3641
|
+
}
|
|
3642
|
+
case "start_auth_login": {
|
|
3643
|
+
await authManager.startLogin();
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
case "submit_auth_code": {
|
|
3647
|
+
const submitted = authManager.submitCode(event.code);
|
|
3648
|
+
if (!submitted) {
|
|
3649
|
+
wsBridge.send(ws, { type: "auth_login_result", success: false, error: t("server.noActiveLoginProcess") });
|
|
3650
|
+
}
|
|
3651
|
+
break;
|
|
3652
|
+
}
|
|
3372
3653
|
default: {
|
|
3373
3654
|
wsBridge.send(ws, {
|
|
3374
3655
|
type: "error",
|
|
@@ -3446,10 +3727,13 @@ async function start(opts = {}) {
|
|
|
3446
3727
|
connections: wsBridge.getConnectionCount(),
|
|
3447
3728
|
activeSessions: sessionManager.getActiveSessions().length
|
|
3448
3729
|
}));
|
|
3449
|
-
let mdnsService = null;
|
|
3450
3730
|
const startMdns = () => {
|
|
3451
3731
|
if (mdnsService) return;
|
|
3452
|
-
mdnsService = new MdnsService({
|
|
3732
|
+
mdnsService = new MdnsService({
|
|
3733
|
+
wsPort: WS_PORT,
|
|
3734
|
+
httpPort: HTTP_PORT,
|
|
3735
|
+
pairing: pairingManager.state
|
|
3736
|
+
});
|
|
3453
3737
|
mdnsService.start();
|
|
3454
3738
|
};
|
|
3455
3739
|
const stopMdns = () => {
|
|
@@ -3484,7 +3768,9 @@ async function start(opts = {}) {
|
|
|
3484
3768
|
errors.push(err);
|
|
3485
3769
|
}
|
|
3486
3770
|
};
|
|
3771
|
+
await attempt(() => authManager.destroy(), "AuthManager");
|
|
3487
3772
|
await attempt(() => stopMdns(), "mDNS");
|
|
3773
|
+
await attempt(() => pairingManager.destroy(), "PairingManager");
|
|
3488
3774
|
await attempt(() => wsBridge.close(), "WebSocket");
|
|
3489
3775
|
await attempt(() => approvalProxy.close(), "ApprovalProxy");
|
|
3490
3776
|
await attempt(() => sessionManager.destroy(), "SessionManager");
|
|
@@ -3512,7 +3798,9 @@ async function start(opts = {}) {
|
|
|
3512
3798
|
} else {
|
|
3513
3799
|
stopMdns();
|
|
3514
3800
|
}
|
|
3515
|
-
}
|
|
3801
|
+
},
|
|
3802
|
+
openPairing: (duration) => pairingManager.open(duration),
|
|
3803
|
+
closePairing: () => pairingManager.close()
|
|
3516
3804
|
};
|
|
3517
3805
|
}
|
|
3518
3806
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sessix-server",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"bin": {
|
|
5
5
|
"sessix-server": "./dist/index.js"
|
|
6
6
|
},
|
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
"start": "node dist/index.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@sessix/shared": "*",
|
|
30
29
|
"ws": "^8.18.0",
|
|
31
30
|
"bonjour-service": "^1.3.0",
|
|
32
31
|
"chokidar": "^4.0.0",
|
|
@@ -34,6 +33,7 @@
|
|
|
34
33
|
"qrcode-terminal": "^0.12.0"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
36
|
+
"@sessix/shared": "*",
|
|
37
37
|
"@types/ws": "^8.5.0",
|
|
38
38
|
"@types/uuid": "^10.0.0",
|
|
39
39
|
"@types/qrcode-terminal": "^0.12.2",
|