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 +174 -6
- package/dist/server.js +174 -6
- package/package.json +1 -1
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
|
-
|
|
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)
|
|
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 (
|
|
4588
|
+
for (let i = 0; i < body.data.length; i++) {
|
|
4589
|
+
const ticket = body.data[i];
|
|
4563
4590
|
if (ticket.status === "error") {
|
|
4564
|
-
|
|
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 =
|
|
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":
|
|
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
|
-
|
|
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)
|
|
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 (
|
|
4593
|
+
for (let i = 0; i < body.data.length; i++) {
|
|
4594
|
+
const ticket = body.data[i];
|
|
4568
4595
|
if (ticket.status === "error") {
|
|
4569
|
-
|
|
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 =
|
|
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":
|
|
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;
|