sessix-server 0.4.3 → 0.4.5

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.
Files changed (3) hide show
  1. package/dist/index.js +533 -68
  2. package/dist/server.js +531 -66
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_node_os10 = require("os");
28
28
  var import_node_fs4 = require("fs");
29
29
  var import_node_path10 = require("path");
30
- var import_node_child_process12 = require("child_process");
30
+ var import_node_child_process13 = require("child_process");
31
31
 
32
32
  // src/i18n/locales/zh.ts
33
33
  var zh = {
@@ -306,7 +306,7 @@ var import_uuid9 = require("uuid");
306
306
  var import_promises7 = require("fs/promises");
307
307
  var import_node_os9 = require("os");
308
308
  var import_node_path9 = require("path");
309
- var import_node_child_process11 = require("child_process");
309
+ var import_node_child_process12 = require("child_process");
310
310
  var import_node_util3 = require("util");
311
311
 
312
312
  // src/providers/ProcessProvider.ts
@@ -662,7 +662,24 @@ var ProcessProvider = class {
662
662
  writeUserMessage(proc, message, sessionId, images) {
663
663
  const content = [];
664
664
  if (images?.length) {
665
- for (const img of images) {
665
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
666
+ const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
667
+ for (let i = 0; i < images.length; i++) {
668
+ const img = images[i];
669
+ if (!ALLOWED_TYPES.has(img.media_type)) {
670
+ if (sessionId) {
671
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
672
+ }
673
+ return;
674
+ }
675
+ const sizeBytes = Math.floor(img.data.length * 0.75);
676
+ if (sizeBytes > MAX_IMAGE_BYTES) {
677
+ if (sessionId) {
678
+ const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
679
+ this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
680
+ }
681
+ return;
682
+ }
666
683
  content.push({
667
684
  type: "image",
668
685
  source: { type: "base64", media_type: img.media_type, data: img.data }
@@ -689,6 +706,14 @@ var ProcessProvider = class {
689
706
  this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
690
707
  }
691
708
  });
709
+ if (sessionId) {
710
+ const syntheticUser = {
711
+ type: "user",
712
+ session_id: sessionId,
713
+ message: { role: "user", content }
714
+ };
715
+ this.emitter.emit(this.getEventName(sessionId), syntheticUser);
716
+ }
692
717
  }
693
718
  /**
694
719
  * 发出写入失败的合成错误事件
@@ -3710,6 +3735,8 @@ var HookInstaller = class {
3710
3735
 
3711
3736
  // src/notification/NotificationService.ts
3712
3737
  var import_node_path5 = require("path");
3738
+ var RECENT_ACTIVITY_MAX = 6;
3739
+ var ACTIVITY_PUSH_THROTTLE_MS = 2500;
3713
3740
  var NotificationService = class {
3714
3741
  constructor(sessionManager, expoChannel = null) {
3715
3742
  this.sessionManager = sessionManager;
@@ -3728,6 +3755,14 @@ var NotificationService = class {
3728
3755
  latestAssistantText = /* @__PURE__ */ new Map();
3729
3756
  /** 获取全局待审批总数的回调(跨所有会话) */
3730
3757
  globalPendingCountProvider = null;
3758
+ /** sessionId → 最近活动状态(用于 LA content push) */
3759
+ recentActivityState = /* @__PURE__ */ new Map();
3760
+ /** sessionId → 节流定时器(LA content push) */
3761
+ activityPushTimers = /* @__PURE__ */ new Map();
3762
+ /** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
3763
+ lastActivityPushAt = /* @__PURE__ */ new Map();
3764
+ /** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
3765
+ pendingApprovalsProvider = null;
3731
3766
  /** 添加通知渠道(id 唯一,可用于后续动态开关) */
3732
3767
  addChannel(id, channel, enabled = true) {
3733
3768
  this.channelMap.set(id, { channel, enabled });
@@ -3755,11 +3790,24 @@ var NotificationService = class {
3755
3790
  }
3756
3791
  /** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
3757
3792
  addActivityPushToken(sessionId, token) {
3758
- this.activityPushChannel?.addToken(sessionId, token);
3793
+ if (!this.activityPushChannel) {
3794
+ console.warn(`[NotificationService] \u26A0\uFE0F \u6536\u5230 LA push token \u4F46 ActivityPushChannel \u672A\u521D\u59CB\u5316 (session=${sessionId.slice(0, 8)}\u2026) \u2014 \u68C0\u67E5 ~/.sessix/apns.json`);
3795
+ return;
3796
+ }
3797
+ this.activityPushChannel.addToken(sessionId, token);
3798
+ console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
3799
+ this.scheduleActivityPush(sessionId, true);
3759
3800
  }
3760
3801
  /** 移除 ActivityKit push token */
3761
3802
  removeActivityPushToken(sessionId) {
3762
3803
  this.activityPushChannel?.removeToken(sessionId);
3804
+ this.clearActivityPushTimer(sessionId);
3805
+ this.recentActivityState.delete(sessionId);
3806
+ this.lastActivityPushAt.delete(sessionId);
3807
+ }
3808
+ /** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
3809
+ setPendingApprovalsProvider(fn) {
3810
+ this.pendingApprovalsProvider = fn;
3763
3811
  }
3764
3812
  /** 设置全局待审批总数提供者 */
3765
3813
  setGlobalPendingCountProvider(provider) {
@@ -3782,12 +3830,15 @@ var NotificationService = class {
3782
3830
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3783
3831
  const dangerLevel2 = this.getDangerLevel(request.toolName);
3784
3832
  const isYoloMode = this.getYoloMode(request.sessionId);
3833
+ const recentActivity = this.getRecentActivity(request.sessionId);
3834
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
3785
3835
  this.activityPushChannel.updateActivityWithAlert(
3786
3836
  request.sessionId,
3787
3837
  {
3788
3838
  status: "waitingApproval",
3789
3839
  sessionTitle,
3790
- latestMessage: "",
3840
+ latestMessage,
3841
+ recentActivity,
3791
3842
  approvalInfo: {
3792
3843
  requestId: request.id,
3793
3844
  toolName: request.toolName,
@@ -3800,6 +3851,7 @@ var NotificationService = class {
3800
3851
  },
3801
3852
  { title, body }
3802
3853
  );
3854
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3803
3855
  return;
3804
3856
  }
3805
3857
  const dangerLevel = this.getDangerLevel(request.toolName);
@@ -3833,17 +3885,20 @@ var NotificationService = class {
3833
3885
  const body = `\u2753 ${request.question.slice(0, 80)}`;
3834
3886
  if (this.activityPushChannel?.hasToken(request.sessionId)) {
3835
3887
  const isYoloMode = this.getYoloMode(request.sessionId);
3888
+ const recentActivity = this.getRecentActivity(request.sessionId);
3836
3889
  this.activityPushChannel.updateActivityWithAlert(
3837
3890
  request.sessionId,
3838
3891
  {
3839
3892
  status: "waitingApproval",
3840
3893
  sessionTitle,
3841
3894
  latestMessage: request.question.slice(0, 80),
3895
+ recentActivity,
3842
3896
  isYoloMode,
3843
3897
  updatedAt: Date.now()
3844
3898
  },
3845
3899
  { title: sessionTitle, body }
3846
3900
  );
3901
+ this.lastActivityPushAt.set(request.sessionId, Date.now());
3847
3902
  return;
3848
3903
  }
3849
3904
  this.notify({
@@ -3877,6 +3932,10 @@ var NotificationService = class {
3877
3932
  this.unsubscribe = null;
3878
3933
  this.yoloModeState.clear();
3879
3934
  this.latestAssistantText.clear();
3935
+ for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
3936
+ this.activityPushTimers.clear();
3937
+ this.recentActivityState.clear();
3938
+ this.lastActivityPushAt.clear();
3880
3939
  }
3881
3940
  // ============================================
3882
3941
  // 内部方法
@@ -3885,15 +3944,20 @@ var NotificationService = class {
3885
3944
  switch (event.type) {
3886
3945
  case "claude_event": {
3887
3946
  this.trackAssistantText(event.sessionId, event.event);
3947
+ this.updateRecentActivity(event.sessionId, event.event);
3948
+ this.scheduleActivityPush(event.sessionId);
3888
3949
  break;
3889
3950
  }
3890
3951
  case "claude_events": {
3891
3952
  for (const e of event.events) {
3892
3953
  this.trackAssistantText(event.sessionId, e);
3954
+ this.updateRecentActivity(event.sessionId, e);
3893
3955
  }
3956
+ this.scheduleActivityPush(event.sessionId);
3894
3957
  break;
3895
3958
  }
3896
3959
  case "status_change": {
3960
+ this.clearActivityPushTimer(event.sessionId);
3897
3961
  if (event.status === "idle") {
3898
3962
  const sessionTitle = this.getSessionTitle(event.sessionId);
3899
3963
  const latestMsg = this.latestAssistantText.get(event.sessionId);
@@ -3904,9 +3968,12 @@ var NotificationService = class {
3904
3968
  status: "idle",
3905
3969
  sessionTitle,
3906
3970
  latestMessage: body,
3971
+ recentActivity: this.getRecentActivity(event.sessionId),
3907
3972
  isYoloMode,
3908
3973
  updatedAt: Date.now()
3909
3974
  });
3975
+ this.recentActivityState.delete(event.sessionId);
3976
+ this.lastActivityPushAt.delete(event.sessionId);
3910
3977
  } else {
3911
3978
  this.notify({
3912
3979
  title: sessionTitle,
@@ -3926,9 +3993,12 @@ var NotificationService = class {
3926
3993
  status: "error",
3927
3994
  sessionTitle,
3928
3995
  latestMessage: body,
3996
+ recentActivity: this.getRecentActivity(event.sessionId),
3929
3997
  isYoloMode,
3930
3998
  updatedAt: Date.now()
3931
3999
  });
4000
+ this.recentActivityState.delete(event.sessionId);
4001
+ this.lastActivityPushAt.delete(event.sessionId);
3932
4002
  } else {
3933
4003
  this.notify({
3934
4004
  title: sessionTitle,
@@ -3970,6 +4040,229 @@ var NotificationService = class {
3970
4040
  getYoloMode(sessionId) {
3971
4041
  return this.yoloModeState.get(sessionId) ?? false;
3972
4042
  }
4043
+ // ============================================
4044
+ // Live Activity 内容推送(后台 LA 实时刷新)
4045
+ // ============================================
4046
+ /**
4047
+ * 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
4048
+ * 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
4049
+ * 切换 message.id 视为新 turn,旧条目沉淀到 history。
4050
+ */
4051
+ updateRecentActivity(sessionId, event) {
4052
+ if (event.type === "result") {
4053
+ const state2 = this.recentActivityState.get(sessionId);
4054
+ if (state2 && state2.currentEntries.length > 0) {
4055
+ state2.history.push(...state2.currentEntries);
4056
+ while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
4057
+ state2.currentEntries = [];
4058
+ state2.currentMessageId = null;
4059
+ }
4060
+ return;
4061
+ }
4062
+ if (event.type !== "assistant") return;
4063
+ const msg = event.message;
4064
+ if (!Array.isArray(msg.content)) return;
4065
+ let state = this.recentActivityState.get(sessionId);
4066
+ if (!state) {
4067
+ state = { history: [], currentMessageId: null, currentEntries: [] };
4068
+ this.recentActivityState.set(sessionId, state);
4069
+ }
4070
+ if (state.currentMessageId !== msg.id) {
4071
+ if (state.currentEntries.length > 0) {
4072
+ state.history.push(...state.currentEntries);
4073
+ while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
4074
+ }
4075
+ state.currentEntries = [];
4076
+ state.currentMessageId = msg.id;
4077
+ }
4078
+ const next = [];
4079
+ for (const block of msg.content) {
4080
+ if (block.type === "text") {
4081
+ const line = this.summarizeText(block.text);
4082
+ if (line.length >= 4) next.push(line);
4083
+ } else if (block.type === "tool_use") {
4084
+ const line = this.summarizeToolCall(block.name, block.input ?? {});
4085
+ if (line) next.push(line);
4086
+ }
4087
+ }
4088
+ state.currentEntries = next;
4089
+ }
4090
+ /** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
4091
+ getRecentActivity(sessionId) {
4092
+ const state = this.recentActivityState.get(sessionId);
4093
+ if (!state) return [];
4094
+ const combined = [...state.history, ...state.currentEntries];
4095
+ return combined.slice(-RECENT_ACTIVITY_MAX);
4096
+ }
4097
+ /** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
4098
+ scheduleActivityPush(sessionId, force = false) {
4099
+ if (!this.activityPushChannel?.hasToken(sessionId)) return;
4100
+ const now = Date.now();
4101
+ const last = this.lastActivityPushAt.get(sessionId) ?? 0;
4102
+ const elapsed = now - last;
4103
+ if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
4104
+ this.clearActivityPushTimer(sessionId);
4105
+ this.flushActivityPush(sessionId);
4106
+ return;
4107
+ }
4108
+ if (this.activityPushTimers.has(sessionId)) return;
4109
+ const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
4110
+ this.activityPushTimers.set(
4111
+ sessionId,
4112
+ setTimeout(() => {
4113
+ this.activityPushTimers.delete(sessionId);
4114
+ this.flushActivityPush(sessionId);
4115
+ }, wait)
4116
+ );
4117
+ }
4118
+ clearActivityPushTimer(sessionId) {
4119
+ const timer = this.activityPushTimers.get(sessionId);
4120
+ if (timer) {
4121
+ clearTimeout(timer);
4122
+ this.activityPushTimers.delete(sessionId);
4123
+ }
4124
+ }
4125
+ /** 真正发送一次 LA content push(无 alert) */
4126
+ flushActivityPush(sessionId) {
4127
+ const channel = this.activityPushChannel;
4128
+ if (!channel?.hasToken(sessionId)) return;
4129
+ const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
4130
+ if (!session) return;
4131
+ const recentActivity = this.getRecentActivity(sessionId);
4132
+ const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
4133
+ const sessionTitle = this.getSessionTitle(sessionId);
4134
+ const isYoloMode = this.getYoloMode(sessionId);
4135
+ const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
4136
+ const latestApproval = pendingApprovals[pendingApprovals.length - 1];
4137
+ const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
4138
+ const contentState = {
4139
+ status,
4140
+ sessionTitle,
4141
+ latestMessage,
4142
+ recentActivity,
4143
+ isYoloMode,
4144
+ updatedAt: Date.now()
4145
+ };
4146
+ if (latestApproval) {
4147
+ contentState.approvalInfo = {
4148
+ requestId: latestApproval.id,
4149
+ toolName: latestApproval.toolName,
4150
+ description: String(latestApproval.description ?? "").slice(0, 80),
4151
+ dangerLevel: this.getDangerLevel(latestApproval.toolName),
4152
+ pendingCount: pendingApprovals.length
4153
+ };
4154
+ }
4155
+ if (session.stats) {
4156
+ contentState.stats = {
4157
+ totalInputTokens: session.stats.totalInputTokens,
4158
+ totalOutputTokens: session.stats.totalOutputTokens,
4159
+ totalCostUsd: session.stats.totalCostUsd
4160
+ };
4161
+ }
4162
+ this.lastActivityPushAt.set(sessionId, Date.now());
4163
+ const lineCount = recentActivity.length;
4164
+ channel.updateActivity(sessionId, contentState).then(() => {
4165
+ console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} lines=${lineCount}`);
4166
+ }).catch((err) => {
4167
+ console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
4168
+ });
4169
+ }
4170
+ /** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
4171
+ mapSessionStatus(status) {
4172
+ switch (status) {
4173
+ case "running":
4174
+ return "running";
4175
+ case "waiting_approval":
4176
+ return "waitingApproval";
4177
+ case "waiting_question":
4178
+ return "waitingQuestion";
4179
+ case "idle":
4180
+ return "completed";
4181
+ case "completed":
4182
+ return "completed";
4183
+ case "error":
4184
+ return "error";
4185
+ default:
4186
+ return "idle";
4187
+ }
4188
+ }
4189
+ /** 文本块清洗:去多余空白 + 截断到 70 字符 */
4190
+ summarizeText(raw) {
4191
+ if (typeof raw !== "string") return "";
4192
+ const cleaned = raw.replace(/\s+/g, " ").trim();
4193
+ return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
4194
+ }
4195
+ /** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
4196
+ summarizeToolCall(name, input) {
4197
+ const str = (v) => typeof v === "string" ? v : "";
4198
+ const baseName = (p) => {
4199
+ const cleaned = p.split(/[?#]/)[0];
4200
+ const parts = cleaned.split("/");
4201
+ return parts[parts.length - 1] || cleaned;
4202
+ };
4203
+ const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
4204
+ switch (name) {
4205
+ case "Bash": {
4206
+ const cmd = str(input.command).split("\n")[0];
4207
+ return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
4208
+ }
4209
+ case "Edit": {
4210
+ const fp = baseName(str(input.file_path));
4211
+ return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
4212
+ }
4213
+ case "MultiEdit": {
4214
+ const fp = baseName(str(input.file_path));
4215
+ return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
4216
+ }
4217
+ case "Write": {
4218
+ const fp = baseName(str(input.file_path));
4219
+ return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
4220
+ }
4221
+ case "Read":
4222
+ case "NotebookEdit": {
4223
+ const fp = baseName(str(input.file_path) || str(input.notebook_path));
4224
+ return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
4225
+ }
4226
+ case "Grep": {
4227
+ const p = str(input.pattern);
4228
+ return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
4229
+ }
4230
+ case "Glob": {
4231
+ const p = str(input.pattern);
4232
+ return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
4233
+ }
4234
+ case "WebFetch": {
4235
+ const url = str(input.url);
4236
+ let host = url;
4237
+ try {
4238
+ host = new URL(url).hostname;
4239
+ } catch {
4240
+ }
4241
+ return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
4242
+ }
4243
+ case "WebSearch": {
4244
+ const q = str(input.query);
4245
+ return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
4246
+ }
4247
+ case "TodoWrite":
4248
+ return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
4249
+ case "Task":
4250
+ case "Agent": {
4251
+ const desc = str(input.description) || str(input.subagent_type);
4252
+ return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
4253
+ }
4254
+ case "ExitPlanMode":
4255
+ return "\u63D0\u4EA4\u8BA1\u5212";
4256
+ case "Skill": {
4257
+ const skill = str(input.skill);
4258
+ return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
4259
+ }
4260
+ default: {
4261
+ const summary = trunc(JSON.stringify(input), 50);
4262
+ return name ? `${name}: ${summary}` : summary;
4263
+ }
4264
+ }
4265
+ }
3973
4266
  };
3974
4267
 
3975
4268
  // src/notification/DesktopNotificationChannel.ts
@@ -4083,62 +4376,82 @@ var ExpoNotificationChannel = class {
4083
4376
  var http2 = __toESM(require("http2"));
4084
4377
  var fs2 = __toESM(require("fs"));
4085
4378
  var crypto = __toESM(require("crypto"));
4379
+ var APNS_HOSTS = {
4380
+ production: "api.push.apple.com",
4381
+ sandbox: "api.sandbox.push.apple.com"
4382
+ };
4086
4383
  var ActivityPushChannel = class {
4087
4384
  /** sessionId -> activityPushToken */
4088
4385
  tokens = /* @__PURE__ */ new Map();
4386
+ /**
4387
+ * 每个 token 已确认工作的 APNs 环境。
4388
+ * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
4389
+ * Release build (aps-environment=production) 的 token 仅在 production 端有效。
4390
+ * 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
4391
+ */
4392
+ tokenEnv = /* @__PURE__ */ new Map();
4393
+ /** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
4394
+ probeOrder;
4089
4395
  teamId;
4090
4396
  keyId;
4091
4397
  authKey;
4092
- apnsHost;
4093
4398
  /** 缓存的 JWT token + 过期时间 */
4094
4399
  cachedJwt = null;
4095
- /** 复用的 HTTP/2 长连接 */
4096
- http2Client = null;
4400
+ /** 每个环境一条 HTTP/2 长连接 */
4401
+ http2Clients = {};
4097
4402
  constructor(config) {
4098
4403
  this.teamId = config.teamId;
4099
4404
  this.keyId = config.keyId;
4100
4405
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4101
- this.apnsHost = config.sandbox ? "api.sandbox.push.apple.com" : "api.push.apple.com";
4102
- console.log(`[ActivityPushChannel] Initialized (${config.sandbox ? "sandbox" : "production"} mode)`);
4103
- }
4104
- /** 获取或新建 HTTP/2 长连接 */
4105
- getHttp2Client() {
4106
- if (this.http2Client && !this.http2Client.destroyed && !this.http2Client.closed) {
4107
- return this.http2Client;
4108
- }
4109
- this.http2Client = http2.connect(`https://${this.apnsHost}`);
4110
- this.http2Client.on("error", (err) => {
4111
- console.warn("[ActivityPushChannel] HTTP/2 connection error, will reconnect on next request:", err.message);
4112
- this.http2Client?.destroy();
4113
- this.http2Client = null;
4406
+ this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4407
+ console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4408
+ }
4409
+ /** 获取或新建指定环境的 HTTP/2 长连接 */
4410
+ getHttp2Client(env) {
4411
+ const existing = this.http2Clients[env];
4412
+ if (existing && !existing.destroyed && !existing.closed) {
4413
+ return existing;
4414
+ }
4415
+ const client = http2.connect(`https://${APNS_HOSTS[env]}`);
4416
+ client.on("error", (err) => {
4417
+ console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
4418
+ client.destroy();
4419
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4114
4420
  });
4115
- this.http2Client.on("close", () => {
4116
- this.http2Client = null;
4421
+ client.on("close", () => {
4422
+ if (this.http2Clients[env] === client) delete this.http2Clients[env];
4117
4423
  });
4118
- return this.http2Client;
4424
+ this.http2Clients[env] = client;
4425
+ return client;
4119
4426
  }
4120
4427
  /** 注册 Activity push token */
4121
4428
  addToken(sessionId, token) {
4429
+ const existed = this.tokens.has(sessionId);
4122
4430
  this.tokens.set(sessionId, token);
4123
- console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
4431
+ console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
4124
4432
  }
4125
4433
  /** 移除 Activity push token */
4126
4434
  removeToken(sessionId) {
4435
+ const tok = this.tokens.get(sessionId);
4127
4436
  this.tokens.delete(sessionId);
4437
+ if (tok) this.tokenEnv.delete(tok);
4128
4438
  }
4129
- /** 发送 content-state 更新到指定会话的 Live Activity */
4439
+ /** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知) */
4130
4440
  async updateActivity(sessionId, contentState) {
4131
4441
  const token = this.tokens.get(sessionId);
4132
4442
  if (!token) return;
4443
+ const now = Math.floor(Date.now() / 1e3);
4133
4444
  const payload = {
4134
4445
  aps: {
4135
- timestamp: Math.floor(Date.now() / 1e3),
4446
+ timestamp: now,
4136
4447
  event: "update",
4137
- "content-state": contentState
4448
+ "content-state": contentState,
4449
+ // 2 分钟没新内容就让 LA 进入 stale 状态(系统自动灰化),避免显示陈旧数据
4450
+ "stale-date": now + 120
4138
4451
  }
4139
4452
  };
4140
4453
  try {
4141
- await this.sendToAPNs(token, payload);
4454
+ await this.sendToAPNs(token, payload, { priority: "5" });
4142
4455
  } catch (err) {
4143
4456
  console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
4144
4457
  }
@@ -4147,17 +4460,19 @@ var ActivityPushChannel = class {
4147
4460
  async updateActivityWithAlert(sessionId, contentState, alert) {
4148
4461
  const token = this.tokens.get(sessionId);
4149
4462
  if (!token) return;
4463
+ const now = Math.floor(Date.now() / 1e3);
4150
4464
  const payload = {
4151
4465
  aps: {
4152
- timestamp: Math.floor(Date.now() / 1e3),
4466
+ timestamp: now,
4153
4467
  event: "update",
4154
4468
  "content-state": contentState,
4469
+ "stale-date": now + 120,
4155
4470
  alert,
4156
4471
  sound: "default"
4157
4472
  }
4158
4473
  };
4159
4474
  try {
4160
- await this.sendToAPNs(token, payload);
4475
+ await this.sendToAPNs(token, payload, { priority: "10" });
4161
4476
  } catch (err) {
4162
4477
  console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
4163
4478
  }
@@ -4166,15 +4481,16 @@ var ActivityPushChannel = class {
4166
4481
  async endActivity(sessionId, contentState) {
4167
4482
  const token = this.tokens.get(sessionId);
4168
4483
  if (!token) return;
4484
+ const now = Math.floor(Date.now() / 1e3);
4169
4485
  const payload = {
4170
4486
  aps: {
4171
- timestamp: Math.floor(Date.now() / 1e3),
4487
+ timestamp: now,
4172
4488
  event: "end",
4173
4489
  "content-state": contentState
4174
4490
  }
4175
4491
  };
4176
4492
  try {
4177
- await this.sendToAPNs(token, payload);
4493
+ await this.sendToAPNs(token, payload, { priority: "10" });
4178
4494
  } catch (err) {
4179
4495
  console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
4180
4496
  }
@@ -4184,15 +4500,44 @@ var ActivityPushChannel = class {
4184
4500
  hasToken(sessionId) {
4185
4501
  return this.tokens.has(sessionId);
4186
4502
  }
4187
- /** 发送 APNs HTTP/2 请求 */
4188
- async sendToAPNs(deviceToken, payload) {
4503
+ /**
4504
+ * 发送 APNs,自动处理环境探测。
4505
+ * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
4506
+ * 收到 BadDeviceToken 自动切到另一个环境,并把成功的环境绑定到该 token。
4507
+ */
4508
+ async sendToAPNs(deviceToken, payload, opts = {}) {
4509
+ const known = this.tokenEnv.get(deviceToken);
4510
+ if (known) {
4511
+ return this.sendToAPNsOnce(deviceToken, payload, opts, known);
4512
+ }
4513
+ let lastErr = null;
4514
+ for (const env of this.probeOrder) {
4515
+ try {
4516
+ await this.sendToAPNsOnce(deviceToken, payload, opts, env);
4517
+ this.tokenEnv.set(deviceToken, env);
4518
+ if (env !== this.probeOrder[0]) {
4519
+ console.log(`[ActivityPushChannel] Token bound to ${env} after probe (token=${deviceToken.slice(0, 16)}\u2026)`);
4520
+ }
4521
+ return;
4522
+ } catch (err) {
4523
+ lastErr = err;
4524
+ if (!isBadDeviceTokenError(err)) {
4525
+ throw err;
4526
+ }
4527
+ }
4528
+ }
4529
+ throw lastErr ?? new Error("APNs send failed: all environments rejected token");
4530
+ }
4531
+ /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4532
+ async sendToAPNsOnce(deviceToken, payload, opts, env) {
4189
4533
  const topic = "com.kachun.sessix.push-type.liveactivity";
4190
4534
  const jwt = this.getJWT();
4191
4535
  const payloadStr = JSON.stringify(payload);
4536
+ const priority = opts.priority ?? "10";
4192
4537
  return new Promise((resolve, reject) => {
4193
4538
  let client;
4194
4539
  try {
4195
- client = this.getHttp2Client();
4540
+ client = this.getHttp2Client(env);
4196
4541
  } catch (err) {
4197
4542
  return reject(err);
4198
4543
  }
@@ -4202,7 +4547,7 @@ var ActivityPushChannel = class {
4202
4547
  "authorization": `bearer ${jwt}`,
4203
4548
  "apns-topic": topic,
4204
4549
  "apns-push-type": "liveactivity",
4205
- "apns-priority": "10",
4550
+ "apns-priority": priority,
4206
4551
  "apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
4207
4552
  "content-type": "application/json",
4208
4553
  "content-length": Buffer.byteLength(payloadStr)
@@ -4220,10 +4565,11 @@ var ActivityPushChannel = class {
4220
4565
  resolve();
4221
4566
  } else {
4222
4567
  if (statusCode === 0) {
4223
- this.http2Client?.destroy();
4224
- this.http2Client = null;
4568
+ const c = this.http2Clients[env];
4569
+ c?.destroy();
4570
+ delete this.http2Clients[env];
4225
4571
  }
4226
- reject(new Error(`APNs returned ${statusCode}: ${responseData}`));
4572
+ reject(new ApnsError(statusCode, responseData));
4227
4573
  }
4228
4574
  });
4229
4575
  req.on("error", (err) => {
@@ -4256,6 +4602,24 @@ var ActivityPushChannel = class {
4256
4602
  return token;
4257
4603
  }
4258
4604
  };
4605
+ var ApnsError = class extends Error {
4606
+ constructor(statusCode, responseBody) {
4607
+ super(`APNs returned ${statusCode}: ${responseBody}`);
4608
+ this.statusCode = statusCode;
4609
+ this.responseBody = responseBody;
4610
+ this.name = "ApnsError";
4611
+ }
4612
+ };
4613
+ function isBadDeviceTokenError(err) {
4614
+ if (!(err instanceof ApnsError)) return false;
4615
+ if (err.statusCode !== 400 && err.statusCode !== 410) return false;
4616
+ try {
4617
+ const parsed = JSON.parse(err.responseBody);
4618
+ return parsed.reason === "BadDeviceToken" || parsed.reason === "Unregistered";
4619
+ } catch {
4620
+ return false;
4621
+ }
4622
+ }
4259
4623
 
4260
4624
  // src/session/ProjectReader.ts
4261
4625
  var import_promises3 = require("fs/promises");
@@ -4655,6 +5019,45 @@ var PairingManager = class {
4655
5019
  }
4656
5020
  };
4657
5021
 
5022
+ // src/utils/shellPath.ts
5023
+ var import_node_child_process7 = require("child_process");
5024
+ var fixed = false;
5025
+ function fixShellPath() {
5026
+ if (fixed || isWindows) {
5027
+ fixed = true;
5028
+ return;
5029
+ }
5030
+ fixed = true;
5031
+ const shell = process.env.SHELL || "/bin/zsh";
5032
+ const isFish = /\/fish$/.test(shell);
5033
+ const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
5034
+ let raw;
5035
+ try {
5036
+ raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
5037
+ encoding: "utf8",
5038
+ timeout: 3e3,
5039
+ stdio: ["ignore", "pipe", "ignore"]
5040
+ });
5041
+ } catch (err) {
5042
+ console.warn("[fixShellPath] failed to read login shell PATH:", err);
5043
+ return;
5044
+ }
5045
+ const fromShell = raw.trim();
5046
+ if (!fromShell) return;
5047
+ process.env.PATH = mergePath(fromShell, process.env.PATH || "");
5048
+ }
5049
+ function mergePath(primary, secondary) {
5050
+ const seen = /* @__PURE__ */ new Set();
5051
+ const out = [];
5052
+ for (const seg of primary.split(":").concat(secondary.split(":"))) {
5053
+ if (!seg) continue;
5054
+ if (seen.has(seg)) continue;
5055
+ seen.add(seg);
5056
+ out.push(seg);
5057
+ }
5058
+ return out.join(":");
5059
+ }
5060
+
4658
5061
  // src/auth/AuthManager.ts
4659
5062
  var import_child_process3 = require("child_process");
4660
5063
  var import_child_process4 = require("child_process");
@@ -4777,9 +5180,9 @@ var AuthManager = class extends import_events3.EventEmitter {
4777
5180
  var import_promises8 = require("fs/promises");
4778
5181
 
4779
5182
  // src/terminal/TerminalExecutor.ts
4780
- var import_node_child_process7 = require("child_process");
5183
+ var import_node_child_process8 = require("child_process");
4781
5184
  var import_uuid5 = require("uuid");
4782
- var EXEC_TIMEOUT_MS = 5 * 60 * 1e3;
5185
+ var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
4783
5186
  var TerminalExecutor = class {
4784
5187
  processes = /* @__PURE__ */ new Map();
4785
5188
  eventCallbacks = [];
@@ -4801,9 +5204,9 @@ var TerminalExecutor = class {
4801
5204
  }
4802
5205
  exec(sessionId, command, cwd) {
4803
5206
  const execId = (0, import_uuid5.v4)();
4804
- const shell = isWindows ? "powershell" : "bash";
4805
- const args = isWindows ? ["-Command", command] : ["-c", command];
4806
- const proc = (0, import_node_child_process7.spawn)(shell, args, {
5207
+ const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
5208
+ const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
5209
+ const proc = (0, import_node_child_process8.spawn)(shell, args, {
4807
5210
  cwd,
4808
5211
  stdio: ["ignore", "pipe", "pipe"],
4809
5212
  env: { ...process.env }
@@ -4840,6 +5243,14 @@ var TerminalExecutor = class {
4840
5243
  });
4841
5244
  const timer = setTimeout(() => {
4842
5245
  if (this.processes.has(execId)) {
5246
+ this.emit({
5247
+ type: "terminal_output",
5248
+ sessionId,
5249
+ execId,
5250
+ stream: "stderr",
5251
+ data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
5252
+ `
5253
+ });
4843
5254
  killProcessCrossPlatform(proc);
4844
5255
  }
4845
5256
  }, EXEC_TIMEOUT_MS);
@@ -4864,13 +5275,13 @@ var TerminalExecutor = class {
4864
5275
  };
4865
5276
 
4866
5277
  // src/xcode/XcodeBuildExecutor.ts
4867
- var import_node_child_process8 = require("child_process");
5278
+ var import_node_child_process9 = require("child_process");
4868
5279
  var import_node_util = require("util");
4869
5280
  var import_promises4 = require("fs/promises");
4870
5281
  var import_node_path6 = require("path");
4871
5282
  var import_node_os7 = require("os");
4872
5283
  var import_uuid6 = require("uuid");
4873
- var execAsync = (0, import_node_util.promisify)(import_node_child_process8.exec);
5284
+ var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
4874
5285
  var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
4875
5286
  var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
4876
5287
  var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
@@ -5059,7 +5470,7 @@ ${e.stderr ?? ""}`);
5059
5470
  if (override) await this.saveConfig(projectPath, override);
5060
5471
  const buildId = (0, import_uuid6.v4)();
5061
5472
  const args = buildArgs(config);
5062
- const proc = (0, import_node_child_process8.spawn)("xcodebuild", args, {
5473
+ const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
5063
5474
  cwd: projectPath,
5064
5475
  stdio: ["ignore", "pipe", "pipe"],
5065
5476
  env: { ...process.env, NSUnbufferedIO: "YES" }
@@ -5155,7 +5566,7 @@ ${e.stderr ?? ""}`);
5155
5566
 
5156
5567
  `
5157
5568
  });
5158
- const proc = (0, import_node_child_process8.spawn)(installCmd[0], installCmd.slice(1), {
5569
+ const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
5159
5570
  cwd: projectPath,
5160
5571
  stdio: ["ignore", "pipe", "pipe"]
5161
5572
  });
@@ -5562,15 +5973,16 @@ var CommandDiscovery = class {
5562
5973
  const cmd = sanitizeBashLine(rawLine);
5563
5974
  if (!cmd) continue;
5564
5975
  const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
5565
- const title = synthesizeTitle(cleanCmd);
5976
+ const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
5977
+ const title = synthesizeTitle(finalCmd);
5566
5978
  out.push(makeCommand({
5567
5979
  title,
5568
- command: cleanCmd,
5569
- cwd: "",
5980
+ command: finalCmd,
5981
+ cwd: cdCwd,
5570
5982
  source,
5571
5983
  sourceFile: fileName,
5572
5984
  description: inlineComment ?? blockHeading,
5573
- category: classifyByCommand(cleanCmd)
5985
+ category: classifyByCommand(finalCmd)
5574
5986
  }));
5575
5987
  }
5576
5988
  }
@@ -5615,6 +6027,19 @@ function synthesizeTitle(cmd) {
5615
6027
  const head = tokens.slice(0, 3).join(" ");
5616
6028
  return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
5617
6029
  }
6030
+ function splitCdPrefix(cmd) {
6031
+ const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
6032
+ if (!m) return { cwd: "", command: cmd };
6033
+ const path2 = m[1];
6034
+ if (!path2) return { cwd: "", command: cmd };
6035
+ if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
6036
+ return { cwd: "", command: cmd };
6037
+ }
6038
+ if (path2.split("/").some((seg) => seg === "..")) {
6039
+ return { cwd: "", command: cmd };
6040
+ }
6041
+ return { cwd: path2, command: m[2].trim() };
6042
+ }
5618
6043
  function splitInlineComment(line) {
5619
6044
  let inSingle = false;
5620
6045
  let inDouble = false;
@@ -5676,10 +6101,10 @@ function sourceWeight(s) {
5676
6101
  }
5677
6102
 
5678
6103
  // src/git/GitExecutor.ts
5679
- var import_node_child_process9 = require("child_process");
6104
+ var import_node_child_process10 = require("child_process");
5680
6105
  var import_node_util2 = require("util");
5681
6106
  var import_uuid7 = require("uuid");
5682
- var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process9.exec);
6107
+ var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
5683
6108
  var STATUS_TIMEOUT_MS = 15e3;
5684
6109
  var COMMIT_TIMEOUT_MS = 6e4;
5685
6110
  var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
@@ -5852,7 +6277,7 @@ var GitExecutor = class {
5852
6277
  });
5853
6278
  let proc;
5854
6279
  try {
5855
- proc = (0, import_node_child_process9.spawn)(cmd[0], cmd.slice(1), {
6280
+ proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
5856
6281
  cwd: projectPath,
5857
6282
  stdio: ["ignore", "pipe", "pipe"],
5858
6283
  env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
@@ -6032,9 +6457,15 @@ function isValidTask(value) {
6032
6457
  }
6033
6458
 
6034
6459
  // src/utils/cliCapabilities.ts
6035
- var import_node_child_process10 = require("child_process");
6460
+ var import_node_child_process11 = require("child_process");
6461
+ var DEFAULT_MODELS = [
6462
+ { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6463
+ { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6464
+ { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6465
+ ];
6036
6466
  var DEFAULT_CAPABILITIES = {
6037
- effortLevels: ["low", "medium", "high", "xhigh", "max"]
6467
+ effortLevels: ["low", "medium", "high", "xhigh", "max"],
6468
+ models: DEFAULT_MODELS
6038
6469
  };
6039
6470
  async function parseCliCapabilities() {
6040
6471
  const claudePath = findClaudePath();
@@ -6060,7 +6491,7 @@ async function parseCliCapabilities() {
6060
6491
  }
6061
6492
  function runCli(path2, args) {
6062
6493
  return new Promise((resolve) => {
6063
- (0, import_node_child_process10.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6494
+ (0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
6064
6495
  if (err) {
6065
6496
  console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
6066
6497
  resolve(null);
@@ -6074,7 +6505,7 @@ function runCli(path2, args) {
6074
6505
  // src/server.ts
6075
6506
  var WS_PORT = 3745;
6076
6507
  var HTTP_PORT = 3746;
6077
- var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process11.exec);
6508
+ var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
6078
6509
  async function killPortProcess(port) {
6079
6510
  try {
6080
6511
  if (isWindows) {
@@ -6102,6 +6533,39 @@ async function killPortProcess(port) {
6102
6533
  } catch {
6103
6534
  }
6104
6535
  }
6536
+ async function loadApnsConfigFromFile() {
6537
+ const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
6538
+ try {
6539
+ const raw = await (0, import_promises7.readFile)(path2, "utf8");
6540
+ const cfg = JSON.parse(raw);
6541
+ if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
6542
+ console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
6543
+ return null;
6544
+ }
6545
+ try {
6546
+ await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
6547
+ } catch (err) {
6548
+ console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
6549
+ return null;
6550
+ }
6551
+ console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
6552
+ console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
6553
+ return {
6554
+ teamId: cfg.teamId,
6555
+ keyId: cfg.keyId,
6556
+ authKeyPath: cfg.authKeyPath,
6557
+ sandbox: cfg.sandbox === true
6558
+ };
6559
+ } catch (err) {
6560
+ const code = err.code;
6561
+ if (code === "ENOENT") {
6562
+ console.log(`[Server] \u2139\uFE0F ${path2} \u4E0D\u5B58\u5728\uFF0CLA \u540E\u53F0\u63A8\u9001\u672A\u542F\u7528\uFF08\u524D\u53F0 App \u4ECD\u80FD\u7528\u672C\u5730 Activity.update\uFF09`);
6563
+ } else {
6564
+ console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
6565
+ }
6566
+ return null;
6567
+ }
6568
+ }
6105
6569
  async function createWithRetry(label, port, factory) {
6106
6570
  try {
6107
6571
  return await factory();
@@ -6116,6 +6580,7 @@ async function createWithRetry(label, port, factory) {
6116
6580
  }
6117
6581
  }
6118
6582
  async function start(opts = {}) {
6583
+ fixShellPath();
6119
6584
  const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
6120
6585
  const tokenFile = (0, import_node_path9.join)(configDir, "token");
6121
6586
  let token;
@@ -6164,9 +6629,10 @@ async function start(opts = {}) {
6164
6629
  const notificationService = new NotificationService(sessionManager, expoChannel);
6165
6630
  notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
6166
6631
  notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
6167
- if (opts.activityPush) {
6632
+ const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
6633
+ if (activityPushOpts) {
6168
6634
  try {
6169
- const activityChannel = new ActivityPushChannel(opts.activityPush);
6635
+ const activityChannel = new ActivityPushChannel(activityPushOpts);
6170
6636
  notificationService.setActivityPushChannel(activityChannel);
6171
6637
  console.log(`[Server] ${t("server.activityPushEnabled")}`);
6172
6638
  } catch (err) {
@@ -6201,6 +6667,9 @@ async function start(opts = {}) {
6201
6667
  notificationService.setGlobalPendingCountProvider(
6202
6668
  () => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
6203
6669
  );
6670
+ notificationService.setPendingApprovalsProvider(
6671
+ (sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
6672
+ );
6204
6673
  let cliCapabilities = null;
6205
6674
  parseCliCapabilities().then((caps) => {
6206
6675
  cliCapabilities = caps;
@@ -6271,7 +6740,7 @@ async function start(opts = {}) {
6271
6740
  wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
6272
6741
  }
6273
6742
  if (cliCapabilities) {
6274
- wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
6743
+ wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
6275
6744
  }
6276
6745
  if (scheduledManager) {
6277
6746
  wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
@@ -6750,14 +7219,12 @@ async function start(opts = {}) {
6750
7219
  setTimeout(() => {
6751
7220
  if (!approvalProxy.isPending(request.id)) return;
6752
7221
  if (wsBridge.isViewingSession(request.sessionId)) return;
6753
- if (wsBridge.getConnectionCount() > 0) return;
6754
7222
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6755
7223
  notificationService.notifyApproval(request, pendingCount);
6756
7224
  }, 5e3);
6757
7225
  setTimeout(() => {
6758
7226
  if (!approvalProxy.isPending(request.id)) return;
6759
7227
  if (wsBridge.isViewingSession(request.sessionId)) return;
6760
- if (wsBridge.getConnectionCount() > 0) return;
6761
7228
  console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
6762
7229
  const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
6763
7230
  notificationService.notifyApproval(request, pendingCount);
@@ -6769,13 +7236,11 @@ async function start(opts = {}) {
6769
7236
  setTimeout(() => {
6770
7237
  if (!sessionManager.isQuestionPending(request.id)) return;
6771
7238
  if (wsBridge.isViewingSession(request.sessionId)) return;
6772
- if (wsBridge.getConnectionCount() > 0) return;
6773
7239
  notificationService.notifyQuestion(request);
6774
7240
  }, 5e3);
6775
7241
  setTimeout(() => {
6776
7242
  if (!sessionManager.isQuestionPending(request.id)) return;
6777
7243
  if (wsBridge.isViewingSession(request.sessionId)) return;
6778
- if (wsBridge.getConnectionCount() > 0) return;
6779
7244
  console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
6780
7245
  notificationService.notifyQuestion(request);
6781
7246
  }, 6e4);
@@ -6964,7 +7429,7 @@ async function autoUpdateIfNeeded() {
6964
7429
  console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
6965
7430
  console.log();
6966
7431
  try {
6967
- (0, import_node_child_process12.execFileSync)("npx", [`sessix-server@${latest}`], {
7432
+ (0, import_node_child_process13.execFileSync)("npx", [`sessix-server@${latest}`], {
6968
7433
  stdio: "inherit",
6969
7434
  env: { ...process.env, __SESSIX_UPDATED: "1" }
6970
7435
  });