sessix-server 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_node_os8 = require("os");
28
28
  var import_node_fs4 = require("fs");
29
29
  var import_node_path7 = require("path");
30
- var import_node_child_process9 = require("child_process");
30
+ var import_node_child_process10 = require("child_process");
31
31
 
32
32
  // src/i18n/locales/zh.ts
33
33
  var zh = {
@@ -306,7 +306,7 @@ var import_uuid6 = require("uuid");
306
306
  var import_promises4 = require("fs/promises");
307
307
  var import_node_os7 = require("os");
308
308
  var import_node_path6 = require("path");
309
- var import_node_child_process8 = require("child_process");
309
+ var import_node_child_process9 = require("child_process");
310
310
  var import_node_util = require("util");
311
311
 
312
312
  // src/providers/ProcessProvider.ts
@@ -2335,7 +2335,7 @@ var WsBridge = class _WsBridge {
2335
2335
  onConnection(callback) {
2336
2336
  this.connectionCallbacks.push(callback);
2337
2337
  }
2338
- /** 注册断开连接回调(任意客户端断开时触发,可通过 getConnectionCount() 判断是否全部断开) */
2338
+ /** 注册断开连接回调(任意客户端断开时触发,传入断开的 ws 对象) */
2339
2339
  onDisconnect(callback) {
2340
2340
  this.disconnectCallbacks.push(callback);
2341
2341
  }
@@ -2449,7 +2449,7 @@ var WsBridge = class _WsBridge {
2449
2449
  console.log(`[WsBridge] Client disconnected, connections: ${this.getConnectionCount()}`);
2450
2450
  for (const cb of this.disconnectCallbacks) {
2451
2451
  try {
2452
- cb();
2452
+ cb(ws);
2453
2453
  } catch (err) {
2454
2454
  console.error("[WsBridge] Disconnect callback error:", err);
2455
2455
  }
@@ -3275,6 +3275,8 @@ var NotificationService = class {
3275
3275
  latestAssistantText = /* @__PURE__ */ new Map();
3276
3276
  /** 获取全局待审批总数的回调(跨所有会话) */
3277
3277
  globalPendingCountProvider = null;
3278
+ /** 获取当前 WS 连接数的回调(用于在线抑制推送) */
3279
+ connectionCountProvider = null;
3278
3280
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3279
3281
  addChannel(id, channel, enabled = true) {
3280
3282
  this.channelMap.set(id, { channel, enabled });
@@ -3312,6 +3314,14 @@ var NotificationService = class {
3312
3314
  setGlobalPendingCountProvider(provider) {
3313
3315
  this.globalPendingCountProvider = provider;
3314
3316
  }
3317
+ /** 设置 WS 连接数提供者(用于 status_change 通知的在线抑制) */
3318
+ setConnectionCountProvider(provider) {
3319
+ this.connectionCountProvider = provider;
3320
+ }
3321
+ /** 移除指定 WebSocket 关联的所有 push token */
3322
+ removeTokensByWs(ws) {
3323
+ this.expoChannel?.removeTokensByWs(ws);
3324
+ }
3315
3325
  /** 获取全局待审批总数 */
3316
3326
  getGlobalPendingCount() {
3317
3327
  return this.globalPendingCountProvider?.() ?? 0;
@@ -3454,7 +3464,7 @@ var NotificationService = class {
3454
3464
  isYoloMode,
3455
3465
  updatedAt: Date.now()
3456
3466
  });
3457
- } else {
3467
+ } else if ((this.connectionCountProvider?.() ?? 0) === 0) {
3458
3468
  this.notify({
3459
3469
  title: sessionTitle,
3460
3470
  body,
@@ -3476,7 +3486,7 @@ var NotificationService = class {
3476
3486
  isYoloMode,
3477
3487
  updatedAt: Date.now()
3478
3488
  });
3479
- } else {
3489
+ } else if ((this.connectionCountProvider?.() ?? 0) === 0) {
3480
3490
  this.notify({
3481
3491
  title: sessionTitle,
3482
3492
  body,
@@ -3564,6 +3574,17 @@ var ExpoNotificationChannel = class {
3564
3574
  this.soundPreferences.delete(token);
3565
3575
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
3566
3576
  }
3577
+ /** 移除指定 WebSocket 关联的所有 token(设备断开时调用) */
3578
+ removeTokensByWs(ws) {
3579
+ for (const [token, tokenWs] of this.tokenWsMap) {
3580
+ if (tokenWs === ws) {
3581
+ this.tokens.delete(token);
3582
+ this.tokenWsMap.delete(token);
3583
+ this.soundPreferences.delete(token);
3584
+ console.log(`[ExpoNotificationChannel] Token removed on WS disconnect, remaining: ${this.tokens.size}`);
3585
+ }
3586
+ }
3587
+ }
3567
3588
  /** 更新某个 token 的音效偏好 */
3568
3589
  setSoundPreferences(prefs) {
3569
3590
  for (const token of this.tokens) {
@@ -4410,10 +4431,50 @@ var TerminalExecutor = class {
4410
4431
  }
4411
4432
  };
4412
4433
 
4434
+ // src/utils/cliCapabilities.ts
4435
+ var import_node_child_process8 = require("child_process");
4436
+ var DEFAULT_CAPABILITIES = {
4437
+ effortLevels: ["low", "medium", "high", "max"]
4438
+ };
4439
+ async function parseCliCapabilities() {
4440
+ const claudePath = findClaudePath();
4441
+ const [helpText, version] = await Promise.all([
4442
+ runCli(claudePath, ["--help"]),
4443
+ runCli(claudePath, ["--version"])
4444
+ ]);
4445
+ const capabilities = { ...DEFAULT_CAPABILITIES };
4446
+ if (version) {
4447
+ capabilities.version = version.trim();
4448
+ }
4449
+ if (helpText) {
4450
+ const effortMatch = helpText.match(/--effort\s.*?\(([^)]+)\)/);
4451
+ if (effortMatch) {
4452
+ const levels = effortMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
4453
+ if (levels.length > 0) {
4454
+ capabilities.effortLevels = levels;
4455
+ }
4456
+ }
4457
+ }
4458
+ console.log(`[CliCapabilities] version=${capabilities.version ?? "unknown"}, effort=[${capabilities.effortLevels.join(", ")}]`);
4459
+ return capabilities;
4460
+ }
4461
+ function runCli(path2, args) {
4462
+ return new Promise((resolve) => {
4463
+ (0, import_node_child_process8.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
4464
+ if (err) {
4465
+ console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
4466
+ resolve(null);
4467
+ } else {
4468
+ resolve(stdout);
4469
+ }
4470
+ });
4471
+ });
4472
+ }
4473
+
4413
4474
  // src/server.ts
4414
4475
  var WS_PORT = 3745;
4415
4476
  var HTTP_PORT = 3746;
4416
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
4477
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4417
4478
  async function killPortProcess(port) {
4418
4479
  try {
4419
4480
  if (isWindows) {
@@ -4535,6 +4596,12 @@ async function start(opts = {}) {
4535
4596
  notificationService.setGlobalPendingCountProvider(
4536
4597
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
4537
4598
  );
4599
+ notificationService.setConnectionCountProvider(() => wsBridge.getConnectionCount());
4600
+ let cliCapabilities = null;
4601
+ parseCliCapabilities().then((caps) => {
4602
+ cliCapabilities = caps;
4603
+ }).catch(() => {
4604
+ });
4538
4605
  const broadcastUnreadSessions = () => {
4539
4606
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
4540
4607
  };
@@ -4556,6 +4623,9 @@ async function start(opts = {}) {
4556
4623
  if (unreadSessionIds.size > 0) {
4557
4624
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
4558
4625
  }
4626
+ if (cliCapabilities) {
4627
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
4628
+ }
4559
4629
  });
4560
4630
  wsBridge.onClientEvent(async (event, ws) => {
4561
4631
  try {
@@ -4891,7 +4961,8 @@ async function start(opts = {}) {
4891
4961
  terminalExecutor.onEvent((event) => {
4892
4962
  wsBridge.broadcast(event);
4893
4963
  });
4894
- wsBridge.onDisconnect(() => {
4964
+ wsBridge.onDisconnect((ws) => {
4965
+ notificationService.removeTokensByWs(ws);
4895
4966
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
4896
4967
  approvalProxy.approveAll(t("server.phoneDisconnected"));
4897
4968
  }
@@ -5076,7 +5147,7 @@ async function autoUpdateIfNeeded() {
5076
5147
  console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
5077
5148
  console.log();
5078
5149
  try {
5079
- (0, import_node_child_process9.execFileSync)("npx", [`sessix-server@${latest}`], {
5150
+ (0, import_node_child_process10.execFileSync)("npx", [`sessix-server@${latest}`], {
5080
5151
  stdio: "inherit",
5081
5152
  env: { ...process.env, __SESSIX_UPDATED: "1" }
5082
5153
  });
package/dist/server.js CHANGED
@@ -311,7 +311,7 @@ var import_uuid6 = require("uuid");
311
311
  var import_promises4 = require("fs/promises");
312
312
  var import_node_os7 = require("os");
313
313
  var import_node_path6 = require("path");
314
- var import_node_child_process8 = require("child_process");
314
+ var import_node_child_process9 = require("child_process");
315
315
  var import_node_util = require("util");
316
316
 
317
317
  // src/providers/ProcessProvider.ts
@@ -2340,7 +2340,7 @@ var WsBridge = class _WsBridge {
2340
2340
  onConnection(callback) {
2341
2341
  this.connectionCallbacks.push(callback);
2342
2342
  }
2343
- /** 注册断开连接回调(任意客户端断开时触发,可通过 getConnectionCount() 判断是否全部断开) */
2343
+ /** 注册断开连接回调(任意客户端断开时触发,传入断开的 ws 对象) */
2344
2344
  onDisconnect(callback) {
2345
2345
  this.disconnectCallbacks.push(callback);
2346
2346
  }
@@ -2454,7 +2454,7 @@ var WsBridge = class _WsBridge {
2454
2454
  console.log(`[WsBridge] Client disconnected, connections: ${this.getConnectionCount()}`);
2455
2455
  for (const cb of this.disconnectCallbacks) {
2456
2456
  try {
2457
- cb();
2457
+ cb(ws);
2458
2458
  } catch (err) {
2459
2459
  console.error("[WsBridge] Disconnect callback error:", err);
2460
2460
  }
@@ -3280,6 +3280,8 @@ var NotificationService = class {
3280
3280
  latestAssistantText = /* @__PURE__ */ new Map();
3281
3281
  /** 获取全局待审批总数的回调(跨所有会话) */
3282
3282
  globalPendingCountProvider = null;
3283
+ /** 获取当前 WS 连接数的回调(用于在线抑制推送) */
3284
+ connectionCountProvider = null;
3283
3285
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3284
3286
  addChannel(id, channel, enabled = true) {
3285
3287
  this.channelMap.set(id, { channel, enabled });
@@ -3317,6 +3319,14 @@ var NotificationService = class {
3317
3319
  setGlobalPendingCountProvider(provider) {
3318
3320
  this.globalPendingCountProvider = provider;
3319
3321
  }
3322
+ /** 设置 WS 连接数提供者(用于 status_change 通知的在线抑制) */
3323
+ setConnectionCountProvider(provider) {
3324
+ this.connectionCountProvider = provider;
3325
+ }
3326
+ /** 移除指定 WebSocket 关联的所有 push token */
3327
+ removeTokensByWs(ws) {
3328
+ this.expoChannel?.removeTokensByWs(ws);
3329
+ }
3320
3330
  /** 获取全局待审批总数 */
3321
3331
  getGlobalPendingCount() {
3322
3332
  return this.globalPendingCountProvider?.() ?? 0;
@@ -3459,7 +3469,7 @@ var NotificationService = class {
3459
3469
  isYoloMode,
3460
3470
  updatedAt: Date.now()
3461
3471
  });
3462
- } else {
3472
+ } else if ((this.connectionCountProvider?.() ?? 0) === 0) {
3463
3473
  this.notify({
3464
3474
  title: sessionTitle,
3465
3475
  body,
@@ -3481,7 +3491,7 @@ var NotificationService = class {
3481
3491
  isYoloMode,
3482
3492
  updatedAt: Date.now()
3483
3493
  });
3484
- } else {
3494
+ } else if ((this.connectionCountProvider?.() ?? 0) === 0) {
3485
3495
  this.notify({
3486
3496
  title: sessionTitle,
3487
3497
  body,
@@ -3569,6 +3579,17 @@ var ExpoNotificationChannel = class {
3569
3579
  this.soundPreferences.delete(token);
3570
3580
  console.log(`[ExpoNotificationChannel] ${t("notification.tokenRemoved", { count: this.tokens.size })}`);
3571
3581
  }
3582
+ /** 移除指定 WebSocket 关联的所有 token(设备断开时调用) */
3583
+ removeTokensByWs(ws) {
3584
+ for (const [token, tokenWs] of this.tokenWsMap) {
3585
+ if (tokenWs === ws) {
3586
+ this.tokens.delete(token);
3587
+ this.tokenWsMap.delete(token);
3588
+ this.soundPreferences.delete(token);
3589
+ console.log(`[ExpoNotificationChannel] Token removed on WS disconnect, remaining: ${this.tokens.size}`);
3590
+ }
3591
+ }
3592
+ }
3572
3593
  /** 更新某个 token 的音效偏好 */
3573
3594
  setSoundPreferences(prefs) {
3574
3595
  for (const token of this.tokens) {
@@ -4415,10 +4436,50 @@ var TerminalExecutor = class {
4415
4436
  }
4416
4437
  };
4417
4438
 
4439
+ // src/utils/cliCapabilities.ts
4440
+ var import_node_child_process8 = require("child_process");
4441
+ var DEFAULT_CAPABILITIES = {
4442
+ effortLevels: ["low", "medium", "high", "max"]
4443
+ };
4444
+ async function parseCliCapabilities() {
4445
+ const claudePath = findClaudePath();
4446
+ const [helpText, version] = await Promise.all([
4447
+ runCli(claudePath, ["--help"]),
4448
+ runCli(claudePath, ["--version"])
4449
+ ]);
4450
+ const capabilities = { ...DEFAULT_CAPABILITIES };
4451
+ if (version) {
4452
+ capabilities.version = version.trim();
4453
+ }
4454
+ if (helpText) {
4455
+ const effortMatch = helpText.match(/--effort\s.*?\(([^)]+)\)/);
4456
+ if (effortMatch) {
4457
+ const levels = effortMatch[1].split(",").map((s) => s.trim()).filter(Boolean);
4458
+ if (levels.length > 0) {
4459
+ capabilities.effortLevels = levels;
4460
+ }
4461
+ }
4462
+ }
4463
+ console.log(`[CliCapabilities] version=${capabilities.version ?? "unknown"}, effort=[${capabilities.effortLevels.join(", ")}]`);
4464
+ return capabilities;
4465
+ }
4466
+ function runCli(path2, args) {
4467
+ return new Promise((resolve) => {
4468
+ (0, import_node_child_process8.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
4469
+ if (err) {
4470
+ console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
4471
+ resolve(null);
4472
+ } else {
4473
+ resolve(stdout);
4474
+ }
4475
+ });
4476
+ });
4477
+ }
4478
+
4418
4479
  // src/server.ts
4419
4480
  var WS_PORT = 3745;
4420
4481
  var HTTP_PORT = 3746;
4421
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
4482
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4422
4483
  async function killPortProcess(port) {
4423
4484
  try {
4424
4485
  if (isWindows) {
@@ -4540,6 +4601,12 @@ async function start(opts = {}) {
4540
4601
  notificationService.setGlobalPendingCountProvider(
4541
4602
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
4542
4603
  );
4604
+ notificationService.setConnectionCountProvider(() => wsBridge.getConnectionCount());
4605
+ let cliCapabilities = null;
4606
+ parseCliCapabilities().then((caps) => {
4607
+ cliCapabilities = caps;
4608
+ }).catch(() => {
4609
+ });
4543
4610
  const broadcastUnreadSessions = () => {
4544
4611
  wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
4545
4612
  };
@@ -4561,6 +4628,9 @@ async function start(opts = {}) {
4561
4628
  if (unreadSessionIds.size > 0) {
4562
4629
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
4563
4630
  }
4631
+ if (cliCapabilities) {
4632
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
4633
+ }
4564
4634
  });
4565
4635
  wsBridge.onClientEvent(async (event, ws) => {
4566
4636
  try {
@@ -4896,7 +4966,8 @@ async function start(opts = {}) {
4896
4966
  terminalExecutor.onEvent((event) => {
4897
4967
  wsBridge.broadcast(event);
4898
4968
  });
4899
- wsBridge.onDisconnect(() => {
4969
+ wsBridge.onDisconnect((ws) => {
4970
+ notificationService.removeTokensByWs(ws);
4900
4971
  if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
4901
4972
  approvalProxy.approveAll(t("server.phoneDisconnected"));
4902
4973
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "bin": {
5
5
  "sessix-server": "dist/index.js"
6
6
  },