sessix-server 0.4.7 → 0.4.9

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
@@ -3830,9 +3830,21 @@ var NotificationService = class {
3830
3830
  removePushToken(token) {
3831
3831
  this.expoChannel?.removeToken(token);
3832
3832
  }
3833
+ /** 注册原生 APNs device token(直连 APNs,优先于 Expo 推送服务) */
3834
+ addNativePushToken(token, ws) {
3835
+ this.activityPushChannel?.addAlertToken(token, ws);
3836
+ if (this.activityPushChannel?.hasAlertTokens()) {
3837
+ console.log("[NotificationService] \u2705 \u76F4\u8FDE APNs alert token \u5DF2\u6CE8\u518C\uFF0CExpo \u63A8\u9001\u6E20\u9053\u964D\u7EA7\u5907\u7528");
3838
+ }
3839
+ }
3840
+ /** 移除原生 APNs device token */
3841
+ removeNativePushToken(token) {
3842
+ this.activityPushChannel?.removeAlertToken(token);
3843
+ }
3833
3844
  /** 更新通知音效偏好 */
3834
3845
  setSoundPreferences(prefs) {
3835
3846
  this.expoChannel?.setSoundPreferences(prefs);
3847
+ this.activityPushChannel?.setAlertSoundPreferences(prefs);
3836
3848
  }
3837
3849
  /** 设置 ActivityKit Push 渠道(可选,需要 APNs 认证配置) */
3838
3850
  setActivityPushChannel(channel) {
@@ -4058,12 +4070,19 @@ var NotificationService = class {
4058
4070
  }
4059
4071
  }
4060
4072
  notify(payload) {
4061
- for (const { channel, enabled } of this.channelMap.values()) {
4073
+ const hasDirectApns = this.activityPushChannel?.hasAlertTokens() === true;
4074
+ for (const [id, { channel, enabled }] of this.channelMap.entries()) {
4062
4075
  if (!enabled) continue;
4076
+ if (id === "expo" && hasDirectApns) continue;
4063
4077
  channel.send(payload).catch((err) => {
4064
4078
  console.error("[NotificationService] Notification send failed:", err);
4065
4079
  });
4066
4080
  }
4081
+ if (hasDirectApns && this.activityPushChannel) {
4082
+ this.activityPushChannel.send(payload).catch((err) => {
4083
+ console.error("[NotificationService] Direct APNs push failed, no fallback:", err);
4084
+ });
4085
+ }
4067
4086
  }
4068
4087
  /** 从 assistant 事件中提取最新文本消息 */
4069
4088
  trackAssistantText(sessionId, event) {
@@ -4280,6 +4299,7 @@ var NotificationService = class {
4280
4299
  * 无 token → 普通 Expo push。清理所有相关状态。
4281
4300
  */
4282
4301
  flushActivityEnd(sessionId, reason) {
4302
+ console.log(`[NotificationService] \u{1F514} flushActivityEnd(${reason}) session=${sessionId.slice(0, 8)}\u2026 expoAvailable=${this.expoChannel?.isAvailable() ?? false}`);
4283
4303
  const sessionTitle = this.getSessionTitle(sessionId);
4284
4304
  const latestMsg = this.latestAssistantText.get(sessionId);
4285
4305
  const isError = reason === "error";
@@ -4516,7 +4536,13 @@ var ExpoNotificationChannel = class {
4516
4536
  console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
4517
4537
  }
4518
4538
  async send(payload) {
4519
- if (this.tokens.size === 0) return;
4539
+ if (this.tokens.size === 0) {
4540
+ const isCompletion = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4541
+ if (isCompletion) {
4542
+ console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u65E0 Expo push token\uFF0C\u5B8C\u6210\u901A\u77E5\u65E0\u6CD5\u63A8\u9001\u3002\u8BF7\u786E\u8BA4\u624B\u673A\u7AEF register_push_token \u5DF2\u53D1\u9001\uFF08\u91CD\u542F App \u53EF\u5F3A\u5236\u91CD\u65B0\u6CE8\u518C\uFF09");
4543
+ }
4544
+ return;
4545
+ }
4520
4546
  const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4521
4547
  const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
4522
4548
  const ws = this.tokenWsMap.get(token);
@@ -4559,9 +4585,18 @@ var ExpoNotificationChannel = class {
4559
4585
  console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
4560
4586
  return;
4561
4587
  }
4562
- for (const ticket of body.data) {
4588
+ for (let i = 0; i < body.data.length; i++) {
4589
+ const ticket = body.data[i];
4563
4590
  if (ticket.status === "error") {
4564
- console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
4591
+ const errorCode = ticket.details?.error ?? "unknown";
4592
+ console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${errorCode})`);
4593
+ if (errorCode === "DeviceNotRegistered" && targetTokens[i]) {
4594
+ const staleToken = targetTokens[i];
4595
+ this.tokens.delete(staleToken);
4596
+ this.tokenWsMap.delete(staleToken);
4597
+ this.soundPreferences.delete(staleToken);
4598
+ console.warn(`[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08DeviceNotRegistered\uFF09\u3002\u82E5\u901A\u77E5\u672A\u6062\u590D\uFF0C\u8BF7\u91CD\u542F App \u91CD\u65B0\u6CE8\u518C push token\u3002`);
4599
+ }
4565
4600
  }
4566
4601
  }
4567
4602
  }
@@ -4582,6 +4617,14 @@ var APNS_HOSTS = {
4582
4617
  var ActivityPushChannel = class {
4583
4618
  /** sessionId -> activityPushToken */
4584
4619
  tokens = /* @__PURE__ */ new Map();
4620
+ /** 原生 device token 集合(用于普通 alert push,绕过 Expo 推送服务) */
4621
+ alertTokens = /* @__PURE__ */ new Set();
4622
+ /** alert token -> WebSocket 映射(用于前台在线过滤) */
4623
+ alertTokenWsMap = /* @__PURE__ */ new Map();
4624
+ /** alert token 已确认的 APNs 环境(独立于 LA token 的探测结果) */
4625
+ alertTokenEnv = /* @__PURE__ */ new Map();
4626
+ /** per-alert-token 通知音效偏好 */
4627
+ alertSoundPreferences = /* @__PURE__ */ new Map();
4585
4628
  /**
4586
4629
  * 每个 token 已确认工作的 APNs 环境。
4587
4630
  * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
@@ -4600,6 +4643,7 @@ var ActivityPushChannel = class {
4600
4643
  teamId;
4601
4644
  keyId;
4602
4645
  authKey;
4646
+ bundleId;
4603
4647
  /** 缓存的 JWT token + 过期时间 */
4604
4648
  cachedJwt = null;
4605
4649
  /** 每个环境一条 HTTP/2 长连接 */
@@ -4608,6 +4652,7 @@ var ActivityPushChannel = class {
4608
4652
  this.teamId = config.teamId;
4609
4653
  this.keyId = config.keyId;
4610
4654
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4655
+ this.bundleId = config.bundleId ?? "com.kachun.sessix";
4611
4656
  this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4612
4657
  console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4613
4658
  }
@@ -4727,6 +4772,103 @@ var ActivityPushChannel = class {
4727
4772
  hasToken(sessionId) {
4728
4773
  return this.tokens.has(sessionId);
4729
4774
  }
4775
+ // ============================================
4776
+ // 普通 Alert Push(直连 APNs,绕过 Expo 推送服务)
4777
+ // ============================================
4778
+ /** 注册原生 APNs device token,用于发送普通 alert 通知 */
4779
+ addAlertToken(token, ws) {
4780
+ this.alertTokens.add(token);
4781
+ if (ws) this.alertTokenWsMap.set(token, ws);
4782
+ console.log(`[ActivityPushChannel] Alert token registered (${this.alertTokens.size} device(s))`);
4783
+ }
4784
+ /** 移除原生 APNs device token */
4785
+ removeAlertToken(token) {
4786
+ this.alertTokens.delete(token);
4787
+ this.alertTokenWsMap.delete(token);
4788
+ this.alertTokenEnv.delete(token);
4789
+ this.alertSoundPreferences.delete(token);
4790
+ }
4791
+ /** 是否有可用的 alert token */
4792
+ hasAlertTokens() {
4793
+ return this.alertTokens.size > 0;
4794
+ }
4795
+ /** 更新 alert token 音效偏好 */
4796
+ setAlertSoundPreferences(prefs) {
4797
+ for (const token of this.alertTokens) {
4798
+ this.alertSoundPreferences.set(token, prefs);
4799
+ }
4800
+ }
4801
+ /**
4802
+ * 发送普通 alert 推送通知(直连 APNs,sandbox/production 自动探测)。
4803
+ * 实现 NotificationChannel.send 接口,可注册到 NotificationService。
4804
+ */
4805
+ async send(payload) {
4806
+ if (this.alertTokens.size === 0) return;
4807
+ const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4808
+ const targetTokens = isCompletionNotif ? Array.from(this.alertTokens) : Array.from(this.alertTokens).filter((token) => {
4809
+ const ws = this.alertTokenWsMap.get(token);
4810
+ return !ws || ws.readyState !== ws.OPEN;
4811
+ });
4812
+ if (targetTokens.length === 0) return;
4813
+ console.log(`[ActivityPushChannel] Alert push \u2192 ${targetTokens.length}/${this.alertTokens.size} device(s)${isCompletionNotif ? " (forced)" : ""}`);
4814
+ await Promise.all(targetTokens.map(async (deviceToken) => {
4815
+ let sound = payload.sound ?? "default";
4816
+ const prefs = this.alertSoundPreferences.get(deviceToken);
4817
+ if (prefs) {
4818
+ const notifType = payload.data?.type ?? "";
4819
+ if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
4820
+ else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
4821
+ else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
4822
+ }
4823
+ const apnsPayload = {
4824
+ aps: {
4825
+ alert: { title: payload.title, ...payload.subtitle ? { subtitle: payload.subtitle } : {}, body: payload.body },
4826
+ ...payload.badge !== void 0 ? { badge: payload.badge } : {},
4827
+ ...sound && sound !== "none" ? { sound } : {},
4828
+ ...payload.categoryId ? { category: payload.categoryId } : {}
4829
+ },
4830
+ ...payload.data ?? {}
4831
+ };
4832
+ try {
4833
+ await this.sendAlertToAPNs(deviceToken, apnsPayload);
4834
+ } catch (err) {
4835
+ console.warn(`[ActivityPushChannel] Alert push failed for token ${deviceToken.slice(0, 16)}\u2026:`, err instanceof Error ? err.message : err);
4836
+ }
4837
+ }));
4838
+ }
4839
+ /** isAvailable 实现(NotificationChannel 接口) */
4840
+ isAvailable() {
4841
+ return this.alertTokens.size > 0;
4842
+ }
4843
+ /**
4844
+ * 发送 alert 通知到指定 device token,自动探测 sandbox/production。
4845
+ * 使用独立的 alertTokenEnv 映射(与 LA token 环境探测隔离)。
4846
+ */
4847
+ async sendAlertToAPNs(deviceToken, payload) {
4848
+ const known = this.alertTokenEnv.get(deviceToken);
4849
+ if (known) {
4850
+ return this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, known);
4851
+ }
4852
+ const short = deviceToken.slice(0, 16);
4853
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
4854
+ let lastErr = null;
4855
+ for (const env of this.probeOrder) {
4856
+ try {
4857
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe try ${env} token=${short}\u2026`);
4858
+ await this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, env);
4859
+ this.alertTokenEnv.set(deviceToken, env);
4860
+ console.log(`[ActivityPushChannel] \u2705 alert probe bound to ${env} (token=${short}\u2026)`);
4861
+ return;
4862
+ } catch (err) {
4863
+ lastErr = err;
4864
+ const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4865
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe ${env} failed: ${reason}`);
4866
+ if (isProviderTokenError(err)) throw err;
4867
+ if (!isBadDeviceTokenError(err)) throw err;
4868
+ }
4869
+ }
4870
+ throw lastErr ?? new Error("APNs alert send failed: all environments rejected token");
4871
+ }
4730
4872
  /**
4731
4873
  * 发送 APNs,自动处理环境探测。
4732
4874
  * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
@@ -4755,6 +4897,12 @@ var ActivityPushChannel = class {
4755
4897
  lastErr = err;
4756
4898
  const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4757
4899
  console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
4900
+ if (isProviderTokenError(err)) {
4901
+ console.error(
4902
+ `[ActivityPushChannel] \u274C APNs Auth Key \u5DF2\u5931\u6548\uFF08${reason}\uFF09\u3002\u8BF7\u5230 Apple Developer \u4E0B\u8F7D\u65B0\u7684 .p8 \u6587\u4EF6\uFF0C\u5E76\u66F4\u65B0 ~/.sessix/apns.json \u4E2D\u7684 keyId \u548C authKeyPath\uFF0C\u7136\u540E\u91CD\u542F\u670D\u52A1\u7AEF\u3002`
4903
+ );
4904
+ throw err;
4905
+ }
4758
4906
  if (!isBadDeviceTokenError(err)) {
4759
4907
  throw err;
4760
4908
  }
@@ -4772,7 +4920,8 @@ var ActivityPushChannel = class {
4772
4920
  }
4773
4921
  /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4774
4922
  async sendToAPNsOnce(deviceToken, payload, opts, env) {
4775
- const topic = "com.kachun.sessix.push-type.liveactivity";
4923
+ const topic = opts.topic ?? `${this.bundleId}.push-type.liveactivity`;
4924
+ const pushType = opts.pushType ?? "liveactivity";
4776
4925
  const jwt = this.getJWT();
4777
4926
  const payloadStr = JSON.stringify(payload);
4778
4927
  const priority = opts.priority ?? "10";
@@ -4788,7 +4937,7 @@ var ActivityPushChannel = class {
4788
4937
  ":path": `/3/device/${deviceToken}`,
4789
4938
  "authorization": `bearer ${jwt}`,
4790
4939
  "apns-topic": topic,
4791
- "apns-push-type": "liveactivity",
4940
+ "apns-push-type": pushType,
4792
4941
  "apns-priority": priority,
4793
4942
  "apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
4794
4943
  "content-type": "application/json",
@@ -4856,6 +5005,16 @@ var ApnsError = class extends Error {
4856
5005
  this.name = "ApnsError";
4857
5006
  }
4858
5007
  };
5008
+ function isProviderTokenError(err) {
5009
+ if (!(err instanceof ApnsError)) return false;
5010
+ if (err.statusCode !== 403) return false;
5011
+ try {
5012
+ const parsed = JSON.parse(err.responseBody);
5013
+ return parsed.reason === "InvalidProviderToken" || parsed.reason === "ExpiredProviderToken";
5014
+ } catch {
5015
+ return false;
5016
+ }
5017
+ }
4859
5018
  function isBadDeviceTokenError(err) {
4860
5019
  if (!(err instanceof ApnsError)) return false;
4861
5020
  if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
@@ -6703,6 +6862,7 @@ function isValidTask(value) {
6703
6862
  var import_node_child_process11 = require("child_process");
6704
6863
  var DEFAULT_MODELS = [
6705
6864
  { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6865
+ { value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Previous generation flagship" },
6706
6866
  { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6707
6867
  { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6708
6868
  ];
@@ -7231,6 +7391,14 @@ async function start(opts = {}) {
7231
7391
  notificationService.removePushToken(event.token);
7232
7392
  break;
7233
7393
  }
7394
+ case "register_native_push_token": {
7395
+ notificationService.addNativePushToken(event.token, ws);
7396
+ break;
7397
+ }
7398
+ case "unregister_native_push_token": {
7399
+ notificationService.removeNativePushToken(event.token);
7400
+ break;
7401
+ }
7234
7402
  case "update_notification_sounds": {
7235
7403
  notificationService.setSoundPreferences(event.preferences);
7236
7404
  break;
package/dist/server.js CHANGED
@@ -3835,9 +3835,21 @@ var NotificationService = class {
3835
3835
  removePushToken(token) {
3836
3836
  this.expoChannel?.removeToken(token);
3837
3837
  }
3838
+ /** 注册原生 APNs device token(直连 APNs,优先于 Expo 推送服务) */
3839
+ addNativePushToken(token, ws) {
3840
+ this.activityPushChannel?.addAlertToken(token, ws);
3841
+ if (this.activityPushChannel?.hasAlertTokens()) {
3842
+ console.log("[NotificationService] \u2705 \u76F4\u8FDE APNs alert token \u5DF2\u6CE8\u518C\uFF0CExpo \u63A8\u9001\u6E20\u9053\u964D\u7EA7\u5907\u7528");
3843
+ }
3844
+ }
3845
+ /** 移除原生 APNs device token */
3846
+ removeNativePushToken(token) {
3847
+ this.activityPushChannel?.removeAlertToken(token);
3848
+ }
3838
3849
  /** 更新通知音效偏好 */
3839
3850
  setSoundPreferences(prefs) {
3840
3851
  this.expoChannel?.setSoundPreferences(prefs);
3852
+ this.activityPushChannel?.setAlertSoundPreferences(prefs);
3841
3853
  }
3842
3854
  /** 设置 ActivityKit Push 渠道(可选,需要 APNs 认证配置) */
3843
3855
  setActivityPushChannel(channel) {
@@ -4063,12 +4075,19 @@ var NotificationService = class {
4063
4075
  }
4064
4076
  }
4065
4077
  notify(payload) {
4066
- for (const { channel, enabled } of this.channelMap.values()) {
4078
+ const hasDirectApns = this.activityPushChannel?.hasAlertTokens() === true;
4079
+ for (const [id, { channel, enabled }] of this.channelMap.entries()) {
4067
4080
  if (!enabled) continue;
4081
+ if (id === "expo" && hasDirectApns) continue;
4068
4082
  channel.send(payload).catch((err) => {
4069
4083
  console.error("[NotificationService] Notification send failed:", err);
4070
4084
  });
4071
4085
  }
4086
+ if (hasDirectApns && this.activityPushChannel) {
4087
+ this.activityPushChannel.send(payload).catch((err) => {
4088
+ console.error("[NotificationService] Direct APNs push failed, no fallback:", err);
4089
+ });
4090
+ }
4072
4091
  }
4073
4092
  /** 从 assistant 事件中提取最新文本消息 */
4074
4093
  trackAssistantText(sessionId, event) {
@@ -4285,6 +4304,7 @@ var NotificationService = class {
4285
4304
  * 无 token → 普通 Expo push。清理所有相关状态。
4286
4305
  */
4287
4306
  flushActivityEnd(sessionId, reason) {
4307
+ console.log(`[NotificationService] \u{1F514} flushActivityEnd(${reason}) session=${sessionId.slice(0, 8)}\u2026 expoAvailable=${this.expoChannel?.isAvailable() ?? false}`);
4288
4308
  const sessionTitle = this.getSessionTitle(sessionId);
4289
4309
  const latestMsg = this.latestAssistantText.get(sessionId);
4290
4310
  const isError = reason === "error";
@@ -4521,7 +4541,13 @@ var ExpoNotificationChannel = class {
4521
4541
  console.log(`[ExpoNotificationChannel] ${t("notification.soundPrefsUpdated")}`);
4522
4542
  }
4523
4543
  async send(payload) {
4524
- if (this.tokens.size === 0) return;
4544
+ if (this.tokens.size === 0) {
4545
+ const isCompletion = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4546
+ if (isCompletion) {
4547
+ console.warn("[ExpoNotificationChannel] \u26A0\uFE0F \u65E0 Expo push token\uFF0C\u5B8C\u6210\u901A\u77E5\u65E0\u6CD5\u63A8\u9001\u3002\u8BF7\u786E\u8BA4\u624B\u673A\u7AEF register_push_token \u5DF2\u53D1\u9001\uFF08\u91CD\u542F App \u53EF\u5F3A\u5236\u91CD\u65B0\u6CE8\u518C\uFF09");
4548
+ }
4549
+ return;
4550
+ }
4525
4551
  const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4526
4552
  const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
4527
4553
  const ws = this.tokenWsMap.get(token);
@@ -4564,9 +4590,18 @@ var ExpoNotificationChannel = class {
4564
4590
  console.warn(`[ExpoNotificationChannel] ${t("notification.pushApiFormatError")}`, JSON.stringify(body));
4565
4591
  return;
4566
4592
  }
4567
- for (const ticket of body.data) {
4593
+ for (let i = 0; i < body.data.length; i++) {
4594
+ const ticket = body.data[i];
4568
4595
  if (ticket.status === "error") {
4569
- console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${ticket.details?.error ?? "unknown"})`);
4596
+ const errorCode = ticket.details?.error ?? "unknown";
4597
+ console.error(`[ExpoNotificationChannel] ${t("notification.pushFailed")} ${ticket.message} (${errorCode})`);
4598
+ if (errorCode === "DeviceNotRegistered" && targetTokens[i]) {
4599
+ const staleToken = targetTokens[i];
4600
+ this.tokens.delete(staleToken);
4601
+ this.tokenWsMap.delete(staleToken);
4602
+ this.soundPreferences.delete(staleToken);
4603
+ console.warn(`[ExpoNotificationChannel] \u26A0\uFE0F \u5DF2\u79FB\u9664\u5931\u6548 token\uFF08DeviceNotRegistered\uFF09\u3002\u82E5\u901A\u77E5\u672A\u6062\u590D\uFF0C\u8BF7\u91CD\u542F App \u91CD\u65B0\u6CE8\u518C push token\u3002`);
4604
+ }
4570
4605
  }
4571
4606
  }
4572
4607
  }
@@ -4587,6 +4622,14 @@ var APNS_HOSTS = {
4587
4622
  var ActivityPushChannel = class {
4588
4623
  /** sessionId -> activityPushToken */
4589
4624
  tokens = /* @__PURE__ */ new Map();
4625
+ /** 原生 device token 集合(用于普通 alert push,绕过 Expo 推送服务) */
4626
+ alertTokens = /* @__PURE__ */ new Set();
4627
+ /** alert token -> WebSocket 映射(用于前台在线过滤) */
4628
+ alertTokenWsMap = /* @__PURE__ */ new Map();
4629
+ /** alert token 已确认的 APNs 环境(独立于 LA token 的探测结果) */
4630
+ alertTokenEnv = /* @__PURE__ */ new Map();
4631
+ /** per-alert-token 通知音效偏好 */
4632
+ alertSoundPreferences = /* @__PURE__ */ new Map();
4590
4633
  /**
4591
4634
  * 每个 token 已确认工作的 APNs 环境。
4592
4635
  * Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
@@ -4605,6 +4648,7 @@ var ActivityPushChannel = class {
4605
4648
  teamId;
4606
4649
  keyId;
4607
4650
  authKey;
4651
+ bundleId;
4608
4652
  /** 缓存的 JWT token + 过期时间 */
4609
4653
  cachedJwt = null;
4610
4654
  /** 每个环境一条 HTTP/2 长连接 */
@@ -4613,6 +4657,7 @@ var ActivityPushChannel = class {
4613
4657
  this.teamId = config.teamId;
4614
4658
  this.keyId = config.keyId;
4615
4659
  this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
4660
+ this.bundleId = config.bundleId ?? "com.kachun.sessix";
4616
4661
  this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
4617
4662
  console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
4618
4663
  }
@@ -4732,6 +4777,103 @@ var ActivityPushChannel = class {
4732
4777
  hasToken(sessionId) {
4733
4778
  return this.tokens.has(sessionId);
4734
4779
  }
4780
+ // ============================================
4781
+ // 普通 Alert Push(直连 APNs,绕过 Expo 推送服务)
4782
+ // ============================================
4783
+ /** 注册原生 APNs device token,用于发送普通 alert 通知 */
4784
+ addAlertToken(token, ws) {
4785
+ this.alertTokens.add(token);
4786
+ if (ws) this.alertTokenWsMap.set(token, ws);
4787
+ console.log(`[ActivityPushChannel] Alert token registered (${this.alertTokens.size} device(s))`);
4788
+ }
4789
+ /** 移除原生 APNs device token */
4790
+ removeAlertToken(token) {
4791
+ this.alertTokens.delete(token);
4792
+ this.alertTokenWsMap.delete(token);
4793
+ this.alertTokenEnv.delete(token);
4794
+ this.alertSoundPreferences.delete(token);
4795
+ }
4796
+ /** 是否有可用的 alert token */
4797
+ hasAlertTokens() {
4798
+ return this.alertTokens.size > 0;
4799
+ }
4800
+ /** 更新 alert token 音效偏好 */
4801
+ setAlertSoundPreferences(prefs) {
4802
+ for (const token of this.alertTokens) {
4803
+ this.alertSoundPreferences.set(token, prefs);
4804
+ }
4805
+ }
4806
+ /**
4807
+ * 发送普通 alert 推送通知(直连 APNs,sandbox/production 自动探测)。
4808
+ * 实现 NotificationChannel.send 接口,可注册到 NotificationService。
4809
+ */
4810
+ async send(payload) {
4811
+ if (this.alertTokens.size === 0) return;
4812
+ const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
4813
+ const targetTokens = isCompletionNotif ? Array.from(this.alertTokens) : Array.from(this.alertTokens).filter((token) => {
4814
+ const ws = this.alertTokenWsMap.get(token);
4815
+ return !ws || ws.readyState !== ws.OPEN;
4816
+ });
4817
+ if (targetTokens.length === 0) return;
4818
+ console.log(`[ActivityPushChannel] Alert push \u2192 ${targetTokens.length}/${this.alertTokens.size} device(s)${isCompletionNotif ? " (forced)" : ""}`);
4819
+ await Promise.all(targetTokens.map(async (deviceToken) => {
4820
+ let sound = payload.sound ?? "default";
4821
+ const prefs = this.alertSoundPreferences.get(deviceToken);
4822
+ if (prefs) {
4823
+ const notifType = payload.data?.type ?? "";
4824
+ if (notifType === "approval_request" && prefs.approval) sound = prefs.approval;
4825
+ else if (notifType === "task_complete" && prefs.taskComplete) sound = prefs.taskComplete;
4826
+ else if (notifType === "task_error" && prefs.taskError) sound = prefs.taskError;
4827
+ }
4828
+ const apnsPayload = {
4829
+ aps: {
4830
+ alert: { title: payload.title, ...payload.subtitle ? { subtitle: payload.subtitle } : {}, body: payload.body },
4831
+ ...payload.badge !== void 0 ? { badge: payload.badge } : {},
4832
+ ...sound && sound !== "none" ? { sound } : {},
4833
+ ...payload.categoryId ? { category: payload.categoryId } : {}
4834
+ },
4835
+ ...payload.data ?? {}
4836
+ };
4837
+ try {
4838
+ await this.sendAlertToAPNs(deviceToken, apnsPayload);
4839
+ } catch (err) {
4840
+ console.warn(`[ActivityPushChannel] Alert push failed for token ${deviceToken.slice(0, 16)}\u2026:`, err instanceof Error ? err.message : err);
4841
+ }
4842
+ }));
4843
+ }
4844
+ /** isAvailable 实现(NotificationChannel 接口) */
4845
+ isAvailable() {
4846
+ return this.alertTokens.size > 0;
4847
+ }
4848
+ /**
4849
+ * 发送 alert 通知到指定 device token,自动探测 sandbox/production。
4850
+ * 使用独立的 alertTokenEnv 映射(与 LA token 环境探测隔离)。
4851
+ */
4852
+ async sendAlertToAPNs(deviceToken, payload) {
4853
+ const known = this.alertTokenEnv.get(deviceToken);
4854
+ if (known) {
4855
+ return this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, known);
4856
+ }
4857
+ const short = deviceToken.slice(0, 16);
4858
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
4859
+ let lastErr = null;
4860
+ for (const env of this.probeOrder) {
4861
+ try {
4862
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe try ${env} token=${short}\u2026`);
4863
+ await this.sendToAPNsOnce(deviceToken, payload, { priority: "10", pushType: "alert", topic: this.bundleId }, env);
4864
+ this.alertTokenEnv.set(deviceToken, env);
4865
+ console.log(`[ActivityPushChannel] \u2705 alert probe bound to ${env} (token=${short}\u2026)`);
4866
+ return;
4867
+ } catch (err) {
4868
+ lastErr = err;
4869
+ const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4870
+ console.log(`[ActivityPushChannel] \u{1F50D} alert probe ${env} failed: ${reason}`);
4871
+ if (isProviderTokenError(err)) throw err;
4872
+ if (!isBadDeviceTokenError(err)) throw err;
4873
+ }
4874
+ }
4875
+ throw lastErr ?? new Error("APNs alert send failed: all environments rejected token");
4876
+ }
4735
4877
  /**
4736
4878
  * 发送 APNs,自动处理环境探测。
4737
4879
  * 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
@@ -4760,6 +4902,12 @@ var ActivityPushChannel = class {
4760
4902
  lastErr = err;
4761
4903
  const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
4762
4904
  console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
4905
+ if (isProviderTokenError(err)) {
4906
+ console.error(
4907
+ `[ActivityPushChannel] \u274C APNs Auth Key \u5DF2\u5931\u6548\uFF08${reason}\uFF09\u3002\u8BF7\u5230 Apple Developer \u4E0B\u8F7D\u65B0\u7684 .p8 \u6587\u4EF6\uFF0C\u5E76\u66F4\u65B0 ~/.sessix/apns.json \u4E2D\u7684 keyId \u548C authKeyPath\uFF0C\u7136\u540E\u91CD\u542F\u670D\u52A1\u7AEF\u3002`
4908
+ );
4909
+ throw err;
4910
+ }
4763
4911
  if (!isBadDeviceTokenError(err)) {
4764
4912
  throw err;
4765
4913
  }
@@ -4777,7 +4925,8 @@ var ActivityPushChannel = class {
4777
4925
  }
4778
4926
  /** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
4779
4927
  async sendToAPNsOnce(deviceToken, payload, opts, env) {
4780
- const topic = "com.kachun.sessix.push-type.liveactivity";
4928
+ const topic = opts.topic ?? `${this.bundleId}.push-type.liveactivity`;
4929
+ const pushType = opts.pushType ?? "liveactivity";
4781
4930
  const jwt = this.getJWT();
4782
4931
  const payloadStr = JSON.stringify(payload);
4783
4932
  const priority = opts.priority ?? "10";
@@ -4793,7 +4942,7 @@ var ActivityPushChannel = class {
4793
4942
  ":path": `/3/device/${deviceToken}`,
4794
4943
  "authorization": `bearer ${jwt}`,
4795
4944
  "apns-topic": topic,
4796
- "apns-push-type": "liveactivity",
4945
+ "apns-push-type": pushType,
4797
4946
  "apns-priority": priority,
4798
4947
  "apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
4799
4948
  "content-type": "application/json",
@@ -4861,6 +5010,16 @@ var ApnsError = class extends Error {
4861
5010
  this.name = "ApnsError";
4862
5011
  }
4863
5012
  };
5013
+ function isProviderTokenError(err) {
5014
+ if (!(err instanceof ApnsError)) return false;
5015
+ if (err.statusCode !== 403) return false;
5016
+ try {
5017
+ const parsed = JSON.parse(err.responseBody);
5018
+ return parsed.reason === "InvalidProviderToken" || parsed.reason === "ExpiredProviderToken";
5019
+ } catch {
5020
+ return false;
5021
+ }
5022
+ }
4864
5023
  function isBadDeviceTokenError(err) {
4865
5024
  if (!(err instanceof ApnsError)) return false;
4866
5025
  if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
@@ -6708,6 +6867,7 @@ function isValidTask(value) {
6708
6867
  var import_node_child_process11 = require("child_process");
6709
6868
  var DEFAULT_MODELS = [
6710
6869
  { value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
6870
+ { value: "claude-opus-4-6", label: "Opus 4.6", sublabel: "Previous generation flagship" },
6711
6871
  { value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
6712
6872
  { value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
6713
6873
  ];
@@ -7236,6 +7396,14 @@ async function start(opts = {}) {
7236
7396
  notificationService.removePushToken(event.token);
7237
7397
  break;
7238
7398
  }
7399
+ case "register_native_push_token": {
7400
+ notificationService.addNativePushToken(event.token, ws);
7401
+ break;
7402
+ }
7403
+ case "unregister_native_push_token": {
7404
+ notificationService.removeNativePushToken(event.token);
7405
+ break;
7406
+ }
7239
7407
  case "update_notification_sounds": {
7240
7408
  notificationService.setSoundPreferences(event.preferences);
7241
7409
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessix-server",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "bin": {
5
5
  "sessix-server": "dist/index.js"
6
6
  },