sessix-server 0.4.3 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +868 -149
- package/dist/server.js +866 -147
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -311,7 +311,7 @@ var import_uuid9 = require("uuid");
|
|
|
311
311
|
var import_promises7 = require("fs/promises");
|
|
312
312
|
var import_node_os9 = require("os");
|
|
313
313
|
var import_node_path9 = require("path");
|
|
314
|
-
var
|
|
314
|
+
var import_node_child_process12 = require("child_process");
|
|
315
315
|
var import_node_util3 = require("util");
|
|
316
316
|
|
|
317
317
|
// src/providers/ProcessProvider.ts
|
|
@@ -667,7 +667,24 @@ var ProcessProvider = class {
|
|
|
667
667
|
writeUserMessage(proc, message, sessionId, images) {
|
|
668
668
|
const content = [];
|
|
669
669
|
if (images?.length) {
|
|
670
|
-
|
|
670
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
|
671
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
672
|
+
for (let i = 0; i < images.length; i++) {
|
|
673
|
+
const img = images[i];
|
|
674
|
+
if (!ALLOWED_TYPES.has(img.media_type)) {
|
|
675
|
+
if (sessionId) {
|
|
676
|
+
this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
|
|
677
|
+
}
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const sizeBytes = Math.floor(img.data.length * 0.75);
|
|
681
|
+
if (sizeBytes > MAX_IMAGE_BYTES) {
|
|
682
|
+
if (sessionId) {
|
|
683
|
+
const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
|
|
684
|
+
this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
671
688
|
content.push({
|
|
672
689
|
type: "image",
|
|
673
690
|
source: { type: "base64", media_type: img.media_type, data: img.data }
|
|
@@ -694,6 +711,14 @@ var ProcessProvider = class {
|
|
|
694
711
|
this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
|
|
695
712
|
}
|
|
696
713
|
});
|
|
714
|
+
if (sessionId) {
|
|
715
|
+
const syntheticUser = {
|
|
716
|
+
type: "user",
|
|
717
|
+
session_id: sessionId,
|
|
718
|
+
message: { role: "user", content }
|
|
719
|
+
};
|
|
720
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticUser);
|
|
721
|
+
}
|
|
697
722
|
}
|
|
698
723
|
/**
|
|
699
724
|
* 发出写入失败的合成错误事件
|
|
@@ -927,9 +952,13 @@ ${context}`;
|
|
|
927
952
|
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
928
953
|
}
|
|
929
954
|
const toolResult = JSON.stringify({
|
|
930
|
-
type: "
|
|
931
|
-
|
|
932
|
-
|
|
955
|
+
type: "user",
|
|
956
|
+
session_id: "",
|
|
957
|
+
message: {
|
|
958
|
+
role: "user",
|
|
959
|
+
content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
|
|
960
|
+
},
|
|
961
|
+
parent_tool_use_id: toolUseId
|
|
933
962
|
});
|
|
934
963
|
await new Promise((resolve, reject) => {
|
|
935
964
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
@@ -2940,6 +2969,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2940
2969
|
this.handleApprovalHook(req, res);
|
|
2941
2970
|
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2942
2971
|
this.handleHookNotify(req, res);
|
|
2972
|
+
} else if (req.method === "POST" && pathname === "/api/resolve") {
|
|
2973
|
+
this.handleApiResolve(req, res);
|
|
2943
2974
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2944
2975
|
this.handlePair(req, res);
|
|
2945
2976
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -3005,6 +3036,34 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3005
3036
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
3006
3037
|
}
|
|
3007
3038
|
}
|
|
3039
|
+
/**
|
|
3040
|
+
* 移动端 API 端点:Widget Extension / Watch 直接提交审批决策
|
|
3041
|
+
*
|
|
3042
|
+
* 绕过 WebSocket 链路,让 iOS Widget Extension 的 AppIntent 在 App 挂起时
|
|
3043
|
+
* 仍能直接将审批结果提交到服务端。使用 Bearer token 鉴权(与 WS 同一 token)。
|
|
3044
|
+
*/
|
|
3045
|
+
async handleApiResolve(req, res) {
|
|
3046
|
+
const authHeader = req.headers.authorization ?? "";
|
|
3047
|
+
const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
3048
|
+
if (bearerToken !== this.token) {
|
|
3049
|
+
this.sendJson(res, 401, { ok: false, error: "Unauthorized" });
|
|
3050
|
+
return;
|
|
3051
|
+
}
|
|
3052
|
+
try {
|
|
3053
|
+
const body = await this.parseJsonBody(req);
|
|
3054
|
+
const requestId = String(body.requestId ?? "").trim();
|
|
3055
|
+
const decision = String(body.decision ?? "").trim();
|
|
3056
|
+
if (!requestId || decision !== "allow" && decision !== "deny") {
|
|
3057
|
+
this.sendJson(res, 400, { ok: false, error: "requestId and decision (allow|deny) required" });
|
|
3058
|
+
return;
|
|
3059
|
+
}
|
|
3060
|
+
const resolved = this.resolveApproval(requestId, { decision });
|
|
3061
|
+
this.sendJson(res, 200, { ok: resolved });
|
|
3062
|
+
} catch (err) {
|
|
3063
|
+
console.error("[ApprovalProxy] /api/resolve error:", err);
|
|
3064
|
+
this.sendJson(res, 500, { ok: false, error: "Internal error" });
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3008
3067
|
/**
|
|
3009
3068
|
* 非阻塞 hook 通知端点
|
|
3010
3069
|
*
|
|
@@ -3715,6 +3774,8 @@ var HookInstaller = class {
|
|
|
3715
3774
|
|
|
3716
3775
|
// src/notification/NotificationService.ts
|
|
3717
3776
|
var import_node_path5 = require("path");
|
|
3777
|
+
var RECENT_ACTIVITY_MAX = 6;
|
|
3778
|
+
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3718
3779
|
var NotificationService = class {
|
|
3719
3780
|
constructor(sessionManager, expoChannel = null) {
|
|
3720
3781
|
this.sessionManager = sessionManager;
|
|
@@ -3733,6 +3794,30 @@ var NotificationService = class {
|
|
|
3733
3794
|
latestAssistantText = /* @__PURE__ */ new Map();
|
|
3734
3795
|
/** 获取全局待审批总数的回调(跨所有会话) */
|
|
3735
3796
|
globalPendingCountProvider = null;
|
|
3797
|
+
/** sessionId → 最近活动状态(用于 LA content push) */
|
|
3798
|
+
recentActivityState = /* @__PURE__ */ new Map();
|
|
3799
|
+
/** sessionId → 节流定时器(LA content push) */
|
|
3800
|
+
activityPushTimers = /* @__PURE__ */ new Map();
|
|
3801
|
+
/** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
|
|
3802
|
+
lastActivityPushAt = /* @__PURE__ */ new Map();
|
|
3803
|
+
/** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
|
|
3804
|
+
pendingPriority = /* @__PURE__ */ new Map();
|
|
3805
|
+
/** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
|
|
3806
|
+
activityCounters = /* @__PURE__ */ new Map();
|
|
3807
|
+
/** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
|
|
3808
|
+
pendingApprovalsProvider = null;
|
|
3809
|
+
/**
|
|
3810
|
+
* sessionId → idle 结束定时器。
|
|
3811
|
+
* 会话变为 idle 时启动(30 秒);用户发新消息重回 running 时取消。
|
|
3812
|
+
* 30 秒内无新消息 → 调 endActivity 关闭 LA,同时发横幅通知告知完成。
|
|
3813
|
+
*/
|
|
3814
|
+
idleEndTimers = /* @__PURE__ */ new Map();
|
|
3815
|
+
/**
|
|
3816
|
+
* sessionId → LA 心跳定时器(setInterval)。
|
|
3817
|
+
* 确保 Agent 子任务等长时间无 claude_event 的场景下 LA 仍持续更新。
|
|
3818
|
+
* token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
|
|
3819
|
+
*/
|
|
3820
|
+
laHeartbeatTimers = /* @__PURE__ */ new Map();
|
|
3736
3821
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3737
3822
|
addChannel(id, channel, enabled = true) {
|
|
3738
3823
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3760,11 +3845,27 @@ var NotificationService = class {
|
|
|
3760
3845
|
}
|
|
3761
3846
|
/** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
|
|
3762
3847
|
addActivityPushToken(sessionId, token) {
|
|
3763
|
-
this.activityPushChannel
|
|
3848
|
+
if (!this.activityPushChannel) {
|
|
3849
|
+
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`);
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3852
|
+
this.activityPushChannel.addToken(sessionId, token);
|
|
3853
|
+
console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
|
|
3854
|
+
this.scheduleActivityPush(sessionId, true);
|
|
3855
|
+
this.startLaHeartbeat(sessionId);
|
|
3764
3856
|
}
|
|
3765
3857
|
/** 移除 ActivityKit push token */
|
|
3766
3858
|
removeActivityPushToken(sessionId) {
|
|
3859
|
+
this.stopLaHeartbeat(sessionId);
|
|
3767
3860
|
this.activityPushChannel?.removeToken(sessionId);
|
|
3861
|
+
this.clearActivityPushTimer(sessionId);
|
|
3862
|
+
this.recentActivityState.delete(sessionId);
|
|
3863
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
3864
|
+
this.activityCounters.delete(sessionId);
|
|
3865
|
+
}
|
|
3866
|
+
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3867
|
+
setPendingApprovalsProvider(fn) {
|
|
3868
|
+
this.pendingApprovalsProvider = fn;
|
|
3768
3869
|
}
|
|
3769
3870
|
/** 设置全局待审批总数提供者 */
|
|
3770
3871
|
setGlobalPendingCountProvider(provider) {
|
|
@@ -3779,7 +3880,7 @@ var NotificationService = class {
|
|
|
3779
3880
|
this.yoloModeState.set(sessionId, enabled);
|
|
3780
3881
|
}
|
|
3781
3882
|
/** 直接触发审批通知(由 ApprovalProxy 回调调用) */
|
|
3782
|
-
notifyApproval(request, pendingCount) {
|
|
3883
|
+
async notifyApproval(request, pendingCount) {
|
|
3783
3884
|
if (this.yoloModeState.get(request.sessionId)) return;
|
|
3784
3885
|
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
3785
3886
|
const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
|
|
@@ -3787,12 +3888,16 @@ var NotificationService = class {
|
|
|
3787
3888
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3788
3889
|
const dangerLevel2 = this.getDangerLevel(request.toolName);
|
|
3789
3890
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3790
|
-
this.
|
|
3891
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3892
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
|
|
3893
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3894
|
+
const sent = await this.activityPushChannel.updateActivityWithAlert(
|
|
3791
3895
|
request.sessionId,
|
|
3792
3896
|
{
|
|
3793
3897
|
status: "waitingApproval",
|
|
3794
3898
|
sessionTitle,
|
|
3795
|
-
latestMessage
|
|
3899
|
+
latestMessage,
|
|
3900
|
+
recentActivity,
|
|
3796
3901
|
approvalInfo: {
|
|
3797
3902
|
requestId: request.id,
|
|
3798
3903
|
toolName: request.toolName,
|
|
@@ -3801,11 +3906,22 @@ var NotificationService = class {
|
|
|
3801
3906
|
pendingCount
|
|
3802
3907
|
},
|
|
3803
3908
|
isYoloMode,
|
|
3804
|
-
updatedAt: Date.now()
|
|
3909
|
+
updatedAt: Date.now(),
|
|
3910
|
+
displayMode: "summary",
|
|
3911
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3912
|
+
startedAt: session?.createdAt,
|
|
3913
|
+
stats: this.buildStatsPayload(session)
|
|
3805
3914
|
},
|
|
3806
3915
|
{ title, body }
|
|
3807
3916
|
);
|
|
3808
|
-
|
|
3917
|
+
if (sent) {
|
|
3918
|
+
console.log(`[NotificationService] \u{1F4E1} approval via ActivityKit push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3919
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3920
|
+
return;
|
|
3921
|
+
}
|
|
3922
|
+
console.warn(`[NotificationService] \u26A0\uFE0F ActivityKit push \u5931\u8D25\uFF0C\u964D\u7EA7\u5230 Expo push session=${request.sessionId.slice(0, 8)}\u2026`);
|
|
3923
|
+
} else {
|
|
3924
|
+
console.log(`[NotificationService] \u{1F4F2} approval via Expo push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3809
3925
|
}
|
|
3810
3926
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
3811
3927
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
@@ -3838,17 +3954,25 @@ var NotificationService = class {
|
|
|
3838
3954
|
const body = `\u2753 ${request.question.slice(0, 80)}`;
|
|
3839
3955
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3840
3956
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3957
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3958
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3841
3959
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3842
3960
|
request.sessionId,
|
|
3843
3961
|
{
|
|
3844
|
-
status: "
|
|
3962
|
+
status: "waitingQuestion",
|
|
3845
3963
|
sessionTitle,
|
|
3846
3964
|
latestMessage: request.question.slice(0, 80),
|
|
3965
|
+
recentActivity,
|
|
3847
3966
|
isYoloMode,
|
|
3848
|
-
updatedAt: Date.now()
|
|
3967
|
+
updatedAt: Date.now(),
|
|
3968
|
+
displayMode: "summary",
|
|
3969
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3970
|
+
startedAt: session?.createdAt,
|
|
3971
|
+
stats: this.buildStatsPayload(session)
|
|
3849
3972
|
},
|
|
3850
3973
|
{ title: sessionTitle, body }
|
|
3851
3974
|
);
|
|
3975
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3852
3976
|
return;
|
|
3853
3977
|
}
|
|
3854
3978
|
this.notify({
|
|
@@ -3882,6 +4006,12 @@ var NotificationService = class {
|
|
|
3882
4006
|
this.unsubscribe = null;
|
|
3883
4007
|
this.yoloModeState.clear();
|
|
3884
4008
|
this.latestAssistantText.clear();
|
|
4009
|
+
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
4010
|
+
this.activityPushTimers.clear();
|
|
4011
|
+
this.recentActivityState.clear();
|
|
4012
|
+
this.lastActivityPushAt.clear();
|
|
4013
|
+
this.pendingPriority.clear();
|
|
4014
|
+
this.activityCounters.clear();
|
|
3885
4015
|
}
|
|
3886
4016
|
// ============================================
|
|
3887
4017
|
// 内部方法
|
|
@@ -3890,29 +4020,29 @@ var NotificationService = class {
|
|
|
3890
4020
|
switch (event.type) {
|
|
3891
4021
|
case "claude_event": {
|
|
3892
4022
|
this.trackAssistantText(event.sessionId, event.event);
|
|
4023
|
+
this.updateRecentActivity(event.sessionId, event.event);
|
|
4024
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3893
4025
|
break;
|
|
3894
4026
|
}
|
|
3895
4027
|
case "claude_events": {
|
|
3896
4028
|
for (const e of event.events) {
|
|
3897
4029
|
this.trackAssistantText(event.sessionId, e);
|
|
4030
|
+
this.updateRecentActivity(event.sessionId, e);
|
|
3898
4031
|
}
|
|
4032
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3899
4033
|
break;
|
|
3900
4034
|
}
|
|
3901
4035
|
case "status_change": {
|
|
4036
|
+
this.clearActivityPushTimer(event.sessionId);
|
|
3902
4037
|
if (event.status === "idle") {
|
|
3903
|
-
|
|
3904
|
-
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
3905
|
-
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3906
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
4038
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
3907
4039
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3908
|
-
this.
|
|
3909
|
-
|
|
3910
|
-
sessionTitle,
|
|
3911
|
-
latestMessage: body,
|
|
3912
|
-
isYoloMode,
|
|
3913
|
-
updatedAt: Date.now()
|
|
3914
|
-
});
|
|
4040
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
4041
|
+
this.scheduleIdleEnd(event.sessionId, 3e4);
|
|
3915
4042
|
} else {
|
|
4043
|
+
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
4044
|
+
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
4045
|
+
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3916
4046
|
this.notify({
|
|
3917
4047
|
title: sessionTitle,
|
|
3918
4048
|
body,
|
|
@@ -3921,28 +4051,12 @@ var NotificationService = class {
|
|
|
3921
4051
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
3922
4052
|
});
|
|
3923
4053
|
}
|
|
4054
|
+
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4055
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4056
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
3924
4057
|
} else if (event.status === "error") {
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
|
|
3928
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
3929
|
-
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3930
|
-
this.activityPushChannel.endActivity(event.sessionId, {
|
|
3931
|
-
status: "error",
|
|
3932
|
-
sessionTitle,
|
|
3933
|
-
latestMessage: body,
|
|
3934
|
-
isYoloMode,
|
|
3935
|
-
updatedAt: Date.now()
|
|
3936
|
-
});
|
|
3937
|
-
} else {
|
|
3938
|
-
this.notify({
|
|
3939
|
-
title: sessionTitle,
|
|
3940
|
-
body,
|
|
3941
|
-
sound: "default",
|
|
3942
|
-
badge: this.getGlobalPendingCount(),
|
|
3943
|
-
data: { type: "task_error", sessionId: event.sessionId }
|
|
3944
|
-
});
|
|
3945
|
-
}
|
|
4058
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4059
|
+
this.flushActivityEnd(event.sessionId, "error");
|
|
3946
4060
|
}
|
|
3947
4061
|
break;
|
|
3948
4062
|
}
|
|
@@ -3975,6 +4089,383 @@ var NotificationService = class {
|
|
|
3975
4089
|
getYoloMode(sessionId) {
|
|
3976
4090
|
return this.yoloModeState.get(sessionId) ?? false;
|
|
3977
4091
|
}
|
|
4092
|
+
// ============================================
|
|
4093
|
+
// Live Activity 内容推送(后台 LA 实时刷新)
|
|
4094
|
+
// ============================================
|
|
4095
|
+
/**
|
|
4096
|
+
* 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
|
|
4097
|
+
* 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
|
|
4098
|
+
* 切换 message.id 视为新 turn,旧条目沉淀到 history。
|
|
4099
|
+
*/
|
|
4100
|
+
updateRecentActivity(sessionId, event) {
|
|
4101
|
+
if (event.type === "result") {
|
|
4102
|
+
const state2 = this.recentActivityState.get(sessionId);
|
|
4103
|
+
if (state2 && state2.currentEntries.length > 0) {
|
|
4104
|
+
state2.history.push(...state2.currentEntries);
|
|
4105
|
+
while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
|
|
4106
|
+
state2.currentEntries = [];
|
|
4107
|
+
state2.currentMessageId = null;
|
|
4108
|
+
}
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
if (event.type !== "assistant") return;
|
|
4112
|
+
const msg = event.message;
|
|
4113
|
+
if (!Array.isArray(msg.content)) return;
|
|
4114
|
+
let state = this.recentActivityState.get(sessionId);
|
|
4115
|
+
if (!state) {
|
|
4116
|
+
state = { history: [], currentMessageId: null, currentEntries: [] };
|
|
4117
|
+
this.recentActivityState.set(sessionId, state);
|
|
4118
|
+
}
|
|
4119
|
+
if (state.currentMessageId !== msg.id) {
|
|
4120
|
+
if (state.currentEntries.length > 0) {
|
|
4121
|
+
state.history.push(...state.currentEntries);
|
|
4122
|
+
while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
|
|
4123
|
+
}
|
|
4124
|
+
state.currentEntries = [];
|
|
4125
|
+
state.currentMessageId = msg.id;
|
|
4126
|
+
}
|
|
4127
|
+
const next = [];
|
|
4128
|
+
for (const block of msg.content) {
|
|
4129
|
+
if (block.type === "text") {
|
|
4130
|
+
const line = this.summarizeText(block.text);
|
|
4131
|
+
if (line.length >= 4) next.push(line);
|
|
4132
|
+
} else if (block.type === "tool_use") {
|
|
4133
|
+
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4134
|
+
if (line) next.push(line);
|
|
4135
|
+
this.incrementCounter(sessionId, block.name);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
state.currentEntries = next;
|
|
4139
|
+
}
|
|
4140
|
+
/** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
|
|
4141
|
+
getRecentActivity(sessionId) {
|
|
4142
|
+
const state = this.recentActivityState.get(sessionId);
|
|
4143
|
+
if (!state) return [];
|
|
4144
|
+
const combined = [...state.history, ...state.currentEntries];
|
|
4145
|
+
return combined.slice(-RECENT_ACTIVITY_MAX);
|
|
4146
|
+
}
|
|
4147
|
+
/** 工具名 → 计数器类别映射 */
|
|
4148
|
+
incrementCounter(sessionId, toolName) {
|
|
4149
|
+
let c = this.activityCounters.get(sessionId);
|
|
4150
|
+
if (!c) {
|
|
4151
|
+
c = { filesEdited: 0, commandsRun: 0, searches: 0, filesRead: 0, messagesReceived: 0 };
|
|
4152
|
+
this.activityCounters.set(sessionId, c);
|
|
4153
|
+
}
|
|
4154
|
+
switch (toolName) {
|
|
4155
|
+
case "Edit":
|
|
4156
|
+
case "MultiEdit":
|
|
4157
|
+
case "Write":
|
|
4158
|
+
case "NotebookEdit":
|
|
4159
|
+
c.filesEdited++;
|
|
4160
|
+
break;
|
|
4161
|
+
case "Bash":
|
|
4162
|
+
c.commandsRun++;
|
|
4163
|
+
break;
|
|
4164
|
+
case "Grep":
|
|
4165
|
+
case "Glob":
|
|
4166
|
+
case "WebSearch":
|
|
4167
|
+
case "WebFetch":
|
|
4168
|
+
c.searches++;
|
|
4169
|
+
break;
|
|
4170
|
+
case "Read":
|
|
4171
|
+
c.filesRead++;
|
|
4172
|
+
break;
|
|
4173
|
+
default:
|
|
4174
|
+
break;
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
/** 把累计计数器格式化为可读摘要(如"已编辑 3 个文件 · 运行 5 条命令") */
|
|
4178
|
+
buildActivitySummary(sessionId) {
|
|
4179
|
+
const c = this.activityCounters.get(sessionId);
|
|
4180
|
+
if (!c) return "";
|
|
4181
|
+
const parts = [];
|
|
4182
|
+
if (c.filesEdited > 0) parts.push(`\u5DF2\u7F16\u8F91 ${c.filesEdited} \u4E2A\u6587\u4EF6`);
|
|
4183
|
+
if (c.commandsRun > 0) parts.push(`\u8FD0\u884C ${c.commandsRun} \u6761\u547D\u4EE4`);
|
|
4184
|
+
if (c.searches > 0) parts.push(`\u641C\u7D22 ${c.searches} \u6B21`);
|
|
4185
|
+
if (c.filesRead > 0) parts.push(`\u9605\u8BFB ${c.filesRead} \u4E2A\u6587\u4EF6`);
|
|
4186
|
+
return parts.join(" \xB7 ") || "\u4F1A\u8BDD\u8FDB\u884C\u4E2D\u2026";
|
|
4187
|
+
}
|
|
4188
|
+
buildStatsPayload(session) {
|
|
4189
|
+
return {
|
|
4190
|
+
totalInputTokens: session?.stats?.totalInputTokens ?? 0,
|
|
4191
|
+
totalOutputTokens: session?.stats?.totalOutputTokens ?? 0,
|
|
4192
|
+
totalCostUsd: session?.stats?.totalCostUsd,
|
|
4193
|
+
totalDurationMs: session?.stats?.totalDurationMs,
|
|
4194
|
+
runningStartedAt: session?.stats?.runningStartedAt
|
|
4195
|
+
};
|
|
4196
|
+
}
|
|
4197
|
+
/** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
|
|
4198
|
+
scheduleActivityPush(sessionId, force = false, priority) {
|
|
4199
|
+
if (!this.activityPushChannel) return;
|
|
4200
|
+
if (!this.activityPushChannel.hasToken(sessionId)) {
|
|
4201
|
+
console.warn(`[NotificationService] \u26A0\uFE0F skip LA push: session=${sessionId.slice(0, 8)}\u2026 token \u672A\u6CE8\u518C`);
|
|
4202
|
+
return;
|
|
4203
|
+
}
|
|
4204
|
+
if (priority === "10") {
|
|
4205
|
+
this.pendingPriority.set(sessionId, "10");
|
|
4206
|
+
}
|
|
4207
|
+
const now = Date.now();
|
|
4208
|
+
const last = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4209
|
+
const elapsed = now - last;
|
|
4210
|
+
if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
|
|
4211
|
+
this.clearActivityPushTimer(sessionId);
|
|
4212
|
+
this.flushActivityPush(sessionId);
|
|
4213
|
+
return;
|
|
4214
|
+
}
|
|
4215
|
+
if (this.activityPushTimers.has(sessionId)) return;
|
|
4216
|
+
const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
|
|
4217
|
+
this.activityPushTimers.set(
|
|
4218
|
+
sessionId,
|
|
4219
|
+
setTimeout(() => {
|
|
4220
|
+
this.activityPushTimers.delete(sessionId);
|
|
4221
|
+
this.flushActivityPush(sessionId);
|
|
4222
|
+
}, wait)
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
clearActivityPushTimer(sessionId) {
|
|
4226
|
+
const timer = this.activityPushTimers.get(sessionId);
|
|
4227
|
+
if (timer) {
|
|
4228
|
+
clearTimeout(timer);
|
|
4229
|
+
this.activityPushTimers.delete(sessionId);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
/**
|
|
4233
|
+
* 启动 LA 心跳(每 ACTIVITY_PUSH_THROTTLE_MS 触发一次 scheduleActivityPush)。
|
|
4234
|
+
* 确保 Agent 子会话等长时间无 claude_event 的情况下 LA 仍持续更新。
|
|
4235
|
+
* 心跳只在会话活跃状态(running/waitingApproval/waitingQuestion)下发推送;
|
|
4236
|
+
* scheduleActivityPush 自带节流,心跳与正常事件驱动不冲突。
|
|
4237
|
+
*/
|
|
4238
|
+
startLaHeartbeat(sessionId) {
|
|
4239
|
+
if (this.laHeartbeatTimers.has(sessionId)) return;
|
|
4240
|
+
const timer = setInterval(() => {
|
|
4241
|
+
if (!this.activityPushChannel?.hasToken(sessionId)) {
|
|
4242
|
+
this.stopLaHeartbeat(sessionId);
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4246
|
+
if (!session) {
|
|
4247
|
+
this.stopLaHeartbeat(sessionId);
|
|
4248
|
+
return;
|
|
4249
|
+
}
|
|
4250
|
+
if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
|
|
4251
|
+
this.scheduleActivityPush(sessionId);
|
|
4252
|
+
}
|
|
4253
|
+
}, ACTIVITY_PUSH_THROTTLE_MS);
|
|
4254
|
+
this.laHeartbeatTimers.set(sessionId, timer);
|
|
4255
|
+
}
|
|
4256
|
+
/** 停止 LA 心跳 */
|
|
4257
|
+
stopLaHeartbeat(sessionId) {
|
|
4258
|
+
const timer = this.laHeartbeatTimers.get(sessionId);
|
|
4259
|
+
if (timer) {
|
|
4260
|
+
clearInterval(timer);
|
|
4261
|
+
this.laHeartbeatTimers.delete(sessionId);
|
|
4262
|
+
}
|
|
4263
|
+
}
|
|
4264
|
+
/** 取消 idle 结束定时器 */
|
|
4265
|
+
cancelIdleEndTimer(sessionId) {
|
|
4266
|
+
const timer = this.idleEndTimers.get(sessionId);
|
|
4267
|
+
if (timer) {
|
|
4268
|
+
clearTimeout(timer);
|
|
4269
|
+
this.idleEndTimers.delete(sessionId);
|
|
4270
|
+
}
|
|
4271
|
+
}
|
|
4272
|
+
/**
|
|
4273
|
+
* 启动 idle 结束定时器:delayMs 后若会话仍 idle,调 endActivity + 发通知。
|
|
4274
|
+
* 保证多轮对话期间(idle→running→idle)LA 不会过早消失。
|
|
4275
|
+
*/
|
|
4276
|
+
scheduleIdleEnd(sessionId, delayMs) {
|
|
4277
|
+
const timer = setTimeout(() => {
|
|
4278
|
+
this.idleEndTimers.delete(sessionId);
|
|
4279
|
+
this.flushActivityEnd(sessionId, "idle");
|
|
4280
|
+
}, delayMs);
|
|
4281
|
+
this.idleEndTimers.set(sessionId, timer);
|
|
4282
|
+
}
|
|
4283
|
+
/**
|
|
4284
|
+
* 结束 LA 并发完成通知。有 LA token → APNs event:end(带横幅);
|
|
4285
|
+
* 无 token → 普通 Expo push。清理所有相关状态。
|
|
4286
|
+
*/
|
|
4287
|
+
flushActivityEnd(sessionId, reason) {
|
|
4288
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4289
|
+
const latestMsg = this.latestAssistantText.get(sessionId);
|
|
4290
|
+
const isError = reason === "error";
|
|
4291
|
+
const body = isError ? `\u274C ${latestMsg?.slice(0, 80) ?? t("notification.taskError")}` : `\u2705 ${latestMsg?.slice(0, 80) ?? t("notification.taskComplete")}`;
|
|
4292
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4293
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4294
|
+
this.notify({
|
|
4295
|
+
title: sessionTitle,
|
|
4296
|
+
body,
|
|
4297
|
+
sound: "default",
|
|
4298
|
+
badge: this.getGlobalPendingCount(),
|
|
4299
|
+
data: { type: isError ? "task_error" : "task_complete", sessionId }
|
|
4300
|
+
});
|
|
4301
|
+
if (this.activityPushChannel?.hasToken(sessionId)) {
|
|
4302
|
+
this.activityPushChannel.endActivity(
|
|
4303
|
+
sessionId,
|
|
4304
|
+
{
|
|
4305
|
+
status: isError ? "error" : "completed",
|
|
4306
|
+
sessionTitle,
|
|
4307
|
+
latestMessage: body,
|
|
4308
|
+
recentActivity: this.getRecentActivity(sessionId),
|
|
4309
|
+
isYoloMode,
|
|
4310
|
+
updatedAt: Date.now(),
|
|
4311
|
+
displayMode: "summary",
|
|
4312
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4313
|
+
startedAt: session?.createdAt,
|
|
4314
|
+
stats: this.buildStatsPayload(session)
|
|
4315
|
+
}
|
|
4316
|
+
// 不传 alert——Expo push 已处理通知,event:end 仅用于关闭 LA
|
|
4317
|
+
).catch((err) => {
|
|
4318
|
+
console.warn("[NotificationService] endActivity (close LA) failed, LA may linger:", err);
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
this.stopLaHeartbeat(sessionId);
|
|
4322
|
+
this.recentActivityState.delete(sessionId);
|
|
4323
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4324
|
+
this.activityCounters.delete(sessionId);
|
|
4325
|
+
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4326
|
+
}
|
|
4327
|
+
/** 真正发送一次 LA content push(无 alert) */
|
|
4328
|
+
flushActivityPush(sessionId) {
|
|
4329
|
+
const channel = this.activityPushChannel;
|
|
4330
|
+
if (!channel?.hasToken(sessionId)) return;
|
|
4331
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4332
|
+
if (!session) return;
|
|
4333
|
+
const recentActivity = this.getRecentActivity(sessionId);
|
|
4334
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
|
|
4335
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4336
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4337
|
+
const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
|
|
4338
|
+
const latestApproval = pendingApprovals[pendingApprovals.length - 1];
|
|
4339
|
+
const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
|
|
4340
|
+
const contentState = {
|
|
4341
|
+
status,
|
|
4342
|
+
sessionTitle,
|
|
4343
|
+
latestMessage,
|
|
4344
|
+
recentActivity,
|
|
4345
|
+
isYoloMode,
|
|
4346
|
+
updatedAt: Date.now(),
|
|
4347
|
+
displayMode: "summary",
|
|
4348
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4349
|
+
startedAt: session.createdAt
|
|
4350
|
+
};
|
|
4351
|
+
if (latestApproval) {
|
|
4352
|
+
contentState.approvalInfo = {
|
|
4353
|
+
requestId: latestApproval.id,
|
|
4354
|
+
toolName: latestApproval.toolName,
|
|
4355
|
+
description: String(latestApproval.description ?? "").slice(0, 80),
|
|
4356
|
+
dangerLevel: this.getDangerLevel(latestApproval.toolName),
|
|
4357
|
+
pendingCount: pendingApprovals.length
|
|
4358
|
+
};
|
|
4359
|
+
}
|
|
4360
|
+
contentState.stats = this.buildStatsPayload(session);
|
|
4361
|
+
const priority = this.pendingPriority.get(sessionId) ?? "5";
|
|
4362
|
+
this.pendingPriority.delete(sessionId);
|
|
4363
|
+
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4364
|
+
const lineCount = recentActivity.length;
|
|
4365
|
+
channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
|
|
4366
|
+
if (ok) {
|
|
4367
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
|
|
4368
|
+
}
|
|
4369
|
+
}).catch((err) => {
|
|
4370
|
+
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
4371
|
+
});
|
|
4372
|
+
}
|
|
4373
|
+
/** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
|
|
4374
|
+
mapSessionStatus(status) {
|
|
4375
|
+
switch (status) {
|
|
4376
|
+
case "running":
|
|
4377
|
+
return "running";
|
|
4378
|
+
case "waiting_approval":
|
|
4379
|
+
return "waitingApproval";
|
|
4380
|
+
case "waiting_question":
|
|
4381
|
+
return "waitingQuestion";
|
|
4382
|
+
case "idle":
|
|
4383
|
+
return "completed";
|
|
4384
|
+
case "completed":
|
|
4385
|
+
return "completed";
|
|
4386
|
+
case "error":
|
|
4387
|
+
return "error";
|
|
4388
|
+
default:
|
|
4389
|
+
return "idle";
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4392
|
+
/** 文本块清洗:去多余空白 + 截断到 70 字符 */
|
|
4393
|
+
summarizeText(raw) {
|
|
4394
|
+
if (typeof raw !== "string") return "";
|
|
4395
|
+
const cleaned = raw.replace(/\s+/g, " ").trim();
|
|
4396
|
+
return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
|
|
4397
|
+
}
|
|
4398
|
+
/** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
|
|
4399
|
+
summarizeToolCall(name, input) {
|
|
4400
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
4401
|
+
const baseName = (p) => {
|
|
4402
|
+
const cleaned = p.split(/[?#]/)[0];
|
|
4403
|
+
const parts = cleaned.split("/");
|
|
4404
|
+
return parts[parts.length - 1] || cleaned;
|
|
4405
|
+
};
|
|
4406
|
+
const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
|
|
4407
|
+
switch (name) {
|
|
4408
|
+
case "Bash": {
|
|
4409
|
+
const cmd = str(input.command).split("\n")[0];
|
|
4410
|
+
return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
|
|
4411
|
+
}
|
|
4412
|
+
case "Edit": {
|
|
4413
|
+
const fp = baseName(str(input.file_path));
|
|
4414
|
+
return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
|
|
4415
|
+
}
|
|
4416
|
+
case "MultiEdit": {
|
|
4417
|
+
const fp = baseName(str(input.file_path));
|
|
4418
|
+
return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
|
|
4419
|
+
}
|
|
4420
|
+
case "Write": {
|
|
4421
|
+
const fp = baseName(str(input.file_path));
|
|
4422
|
+
return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
|
|
4423
|
+
}
|
|
4424
|
+
case "Read":
|
|
4425
|
+
case "NotebookEdit": {
|
|
4426
|
+
const fp = baseName(str(input.file_path) || str(input.notebook_path));
|
|
4427
|
+
return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
|
|
4428
|
+
}
|
|
4429
|
+
case "Grep": {
|
|
4430
|
+
const p = str(input.pattern);
|
|
4431
|
+
return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
|
|
4432
|
+
}
|
|
4433
|
+
case "Glob": {
|
|
4434
|
+
const p = str(input.pattern);
|
|
4435
|
+
return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
|
|
4436
|
+
}
|
|
4437
|
+
case "WebFetch": {
|
|
4438
|
+
const url = str(input.url);
|
|
4439
|
+
let host = url;
|
|
4440
|
+
try {
|
|
4441
|
+
host = new URL(url).hostname;
|
|
4442
|
+
} catch {
|
|
4443
|
+
}
|
|
4444
|
+
return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
|
|
4445
|
+
}
|
|
4446
|
+
case "WebSearch": {
|
|
4447
|
+
const q = str(input.query);
|
|
4448
|
+
return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
|
|
4449
|
+
}
|
|
4450
|
+
case "TodoWrite":
|
|
4451
|
+
return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
|
|
4452
|
+
case "Task":
|
|
4453
|
+
case "Agent": {
|
|
4454
|
+
const desc = str(input.description) || str(input.subagent_type);
|
|
4455
|
+
return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
|
|
4456
|
+
}
|
|
4457
|
+
case "ExitPlanMode":
|
|
4458
|
+
return "\u63D0\u4EA4\u8BA1\u5212";
|
|
4459
|
+
case "Skill": {
|
|
4460
|
+
const skill = str(input.skill);
|
|
4461
|
+
return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
|
|
4462
|
+
}
|
|
4463
|
+
default: {
|
|
4464
|
+
const summary = trunc(JSON.stringify(input), 50);
|
|
4465
|
+
return name ? `${name}: ${summary}` : summary;
|
|
4466
|
+
}
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
3978
4469
|
};
|
|
3979
4470
|
|
|
3980
4471
|
// src/notification/DesktopNotificationChannel.ts
|
|
@@ -4031,12 +4522,13 @@ var ExpoNotificationChannel = class {
|
|
|
4031
4522
|
}
|
|
4032
4523
|
async send(payload) {
|
|
4033
4524
|
if (this.tokens.size === 0) return;
|
|
4034
|
-
const
|
|
4525
|
+
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
4526
|
+
const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
|
|
4035
4527
|
const ws = this.tokenWsMap.get(token);
|
|
4036
4528
|
return !ws || ws.readyState !== ws.OPEN;
|
|
4037
4529
|
});
|
|
4038
|
-
if (
|
|
4039
|
-
const messages =
|
|
4530
|
+
if (targetTokens.length === 0) return;
|
|
4531
|
+
const messages = targetTokens.map((to) => {
|
|
4040
4532
|
let sound = payload.sound ?? "default";
|
|
4041
4533
|
const prefs = this.soundPreferences.get(to);
|
|
4042
4534
|
if (prefs) {
|
|
@@ -4058,7 +4550,7 @@ var ExpoNotificationChannel = class {
|
|
|
4058
4550
|
};
|
|
4059
4551
|
});
|
|
4060
4552
|
try {
|
|
4061
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${
|
|
4553
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
|
|
4062
4554
|
const res = await fetch(EXPO_PUSH_API, {
|
|
4063
4555
|
method: "POST",
|
|
4064
4556
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -4088,100 +4580,151 @@ var ExpoNotificationChannel = class {
|
|
|
4088
4580
|
var http2 = __toESM(require("http2"));
|
|
4089
4581
|
var fs2 = __toESM(require("fs"));
|
|
4090
4582
|
var crypto = __toESM(require("crypto"));
|
|
4583
|
+
var APNS_HOSTS = {
|
|
4584
|
+
production: "api.push.apple.com",
|
|
4585
|
+
sandbox: "api.sandbox.push.apple.com"
|
|
4586
|
+
};
|
|
4091
4587
|
var ActivityPushChannel = class {
|
|
4092
4588
|
/** sessionId -> activityPushToken */
|
|
4093
4589
|
tokens = /* @__PURE__ */ new Map();
|
|
4590
|
+
/**
|
|
4591
|
+
* 每个 token 已确认工作的 APNs 环境。
|
|
4592
|
+
* Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
|
|
4593
|
+
* Release build (aps-environment=production) 的 token 仅在 production 端有效。
|
|
4594
|
+
* 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
|
|
4595
|
+
*/
|
|
4596
|
+
tokenEnv = /* @__PURE__ */ new Map();
|
|
4597
|
+
/**
|
|
4598
|
+
* 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
|
|
4599
|
+
* 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
|
|
4600
|
+
* 当同一 session 注册新 token 时自动清除旧 token 的标记。
|
|
4601
|
+
*/
|
|
4602
|
+
deadTokens = /* @__PURE__ */ new Set();
|
|
4603
|
+
/** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
|
|
4604
|
+
probeOrder;
|
|
4094
4605
|
teamId;
|
|
4095
4606
|
keyId;
|
|
4096
4607
|
authKey;
|
|
4097
|
-
apnsHost;
|
|
4098
4608
|
/** 缓存的 JWT token + 过期时间 */
|
|
4099
4609
|
cachedJwt = null;
|
|
4100
|
-
/**
|
|
4101
|
-
|
|
4610
|
+
/** 每个环境一条 HTTP/2 长连接 */
|
|
4611
|
+
http2Clients = {};
|
|
4102
4612
|
constructor(config) {
|
|
4103
4613
|
this.teamId = config.teamId;
|
|
4104
4614
|
this.keyId = config.keyId;
|
|
4105
4615
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
4106
|
-
this.
|
|
4107
|
-
console.log(`[ActivityPushChannel] Initialized (${
|
|
4108
|
-
}
|
|
4109
|
-
/**
|
|
4110
|
-
getHttp2Client() {
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4616
|
+
this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
|
|
4617
|
+
console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
|
|
4618
|
+
}
|
|
4619
|
+
/** 获取或新建指定环境的 HTTP/2 长连接 */
|
|
4620
|
+
getHttp2Client(env) {
|
|
4621
|
+
const existing = this.http2Clients[env];
|
|
4622
|
+
if (existing && !existing.destroyed && !existing.closed) {
|
|
4623
|
+
return existing;
|
|
4624
|
+
}
|
|
4625
|
+
const client = http2.connect(`https://${APNS_HOSTS[env]}`);
|
|
4626
|
+
client.on("error", (err) => {
|
|
4627
|
+
console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
|
|
4628
|
+
client.destroy();
|
|
4629
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4119
4630
|
});
|
|
4120
|
-
|
|
4121
|
-
this.
|
|
4631
|
+
client.on("close", () => {
|
|
4632
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4122
4633
|
});
|
|
4123
|
-
|
|
4634
|
+
this.http2Clients[env] = client;
|
|
4635
|
+
return client;
|
|
4124
4636
|
}
|
|
4125
4637
|
/** 注册 Activity push token */
|
|
4126
4638
|
addToken(sessionId, token) {
|
|
4639
|
+
const oldToken = this.tokens.get(sessionId);
|
|
4640
|
+
if (oldToken && oldToken !== token) {
|
|
4641
|
+
this.tokenEnv.delete(oldToken);
|
|
4642
|
+
this.deadTokens.delete(oldToken);
|
|
4643
|
+
}
|
|
4644
|
+
const existed = this.tokens.has(sessionId);
|
|
4127
4645
|
this.tokens.set(sessionId, token);
|
|
4128
|
-
console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
|
|
4646
|
+
console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
|
|
4129
4647
|
}
|
|
4130
4648
|
/** 移除 Activity push token */
|
|
4131
4649
|
removeToken(sessionId) {
|
|
4650
|
+
const tok = this.tokens.get(sessionId);
|
|
4132
4651
|
this.tokens.delete(sessionId);
|
|
4652
|
+
if (tok) {
|
|
4653
|
+
this.tokenEnv.delete(tok);
|
|
4654
|
+
this.deadTokens.delete(tok);
|
|
4655
|
+
}
|
|
4133
4656
|
}
|
|
4134
|
-
/** 发送 content-state 更新到指定会话的 Live Activity */
|
|
4135
|
-
async updateActivity(sessionId, contentState) {
|
|
4657
|
+
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4658
|
+
async updateActivity(sessionId, contentState, opts) {
|
|
4136
4659
|
const token = this.tokens.get(sessionId);
|
|
4137
|
-
if (!token) return;
|
|
4660
|
+
if (!token) return false;
|
|
4661
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4662
|
+
const priority = opts?.priority ?? "5";
|
|
4138
4663
|
const payload = {
|
|
4139
4664
|
aps: {
|
|
4140
|
-
timestamp:
|
|
4665
|
+
timestamp: now,
|
|
4141
4666
|
event: "update",
|
|
4142
|
-
"content-state": contentState
|
|
4667
|
+
"content-state": contentState,
|
|
4668
|
+
"stale-date": now + 600
|
|
4143
4669
|
}
|
|
4144
4670
|
};
|
|
4145
4671
|
try {
|
|
4146
|
-
await this.sendToAPNs(token, payload
|
|
4672
|
+
await this.sendToAPNs(token, payload, {
|
|
4673
|
+
priority,
|
|
4674
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4675
|
+
});
|
|
4676
|
+
return true;
|
|
4147
4677
|
} catch (err) {
|
|
4148
4678
|
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4679
|
+
return false;
|
|
4149
4680
|
}
|
|
4150
4681
|
}
|
|
4151
|
-
/** 发送带通知的 content-state
|
|
4682
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4152
4683
|
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4153
4684
|
const token = this.tokens.get(sessionId);
|
|
4154
|
-
if (!token) return;
|
|
4685
|
+
if (!token) return false;
|
|
4686
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4155
4687
|
const payload = {
|
|
4156
4688
|
aps: {
|
|
4157
|
-
timestamp:
|
|
4689
|
+
timestamp: now,
|
|
4158
4690
|
event: "update",
|
|
4159
4691
|
"content-state": contentState,
|
|
4692
|
+
"stale-date": now + 600,
|
|
4160
4693
|
alert,
|
|
4161
4694
|
sound: "default"
|
|
4162
4695
|
}
|
|
4163
4696
|
};
|
|
4164
4697
|
try {
|
|
4165
|
-
await this.sendToAPNs(token, payload);
|
|
4698
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4699
|
+
return true;
|
|
4166
4700
|
} catch (err) {
|
|
4167
4701
|
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4702
|
+
return false;
|
|
4168
4703
|
}
|
|
4169
4704
|
}
|
|
4170
|
-
/**
|
|
4171
|
-
|
|
4705
|
+
/**
|
|
4706
|
+
* 结束指定会话的 Live Activity。
|
|
4707
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4708
|
+
*/
|
|
4709
|
+
async endActivity(sessionId, contentState, opts) {
|
|
4172
4710
|
const token = this.tokens.get(sessionId);
|
|
4173
4711
|
if (!token) return;
|
|
4174
|
-
const
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
}
|
|
4712
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4713
|
+
const aps = {
|
|
4714
|
+
timestamp: now,
|
|
4715
|
+
event: "end",
|
|
4716
|
+
"content-state": contentState
|
|
4180
4717
|
};
|
|
4718
|
+
if (opts?.alert) {
|
|
4719
|
+
aps.alert = opts.alert;
|
|
4720
|
+
aps.sound = "default";
|
|
4721
|
+
}
|
|
4722
|
+
const payload = { aps };
|
|
4181
4723
|
try {
|
|
4182
|
-
await this.sendToAPNs(token, payload);
|
|
4724
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4183
4725
|
} catch (err) {
|
|
4184
|
-
|
|
4726
|
+
this.tokens.delete(sessionId);
|
|
4727
|
+
throw err;
|
|
4185
4728
|
}
|
|
4186
4729
|
this.tokens.delete(sessionId);
|
|
4187
4730
|
}
|
|
@@ -4189,33 +4732,81 @@ var ActivityPushChannel = class {
|
|
|
4189
4732
|
hasToken(sessionId) {
|
|
4190
4733
|
return this.tokens.has(sessionId);
|
|
4191
4734
|
}
|
|
4192
|
-
/**
|
|
4193
|
-
|
|
4735
|
+
/**
|
|
4736
|
+
* 发送 APNs,自动处理环境探测。
|
|
4737
|
+
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4738
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4739
|
+
* 并把成功的环境绑定到该 token。
|
|
4740
|
+
*/
|
|
4741
|
+
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4742
|
+
if (this.deadTokens.has(deviceToken)) {
|
|
4743
|
+
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
4744
|
+
}
|
|
4745
|
+
const known = this.tokenEnv.get(deviceToken);
|
|
4746
|
+
if (known) {
|
|
4747
|
+
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4748
|
+
}
|
|
4749
|
+
const short = deviceToken.slice(0, 16);
|
|
4750
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4751
|
+
let lastErr = null;
|
|
4752
|
+
for (const env of this.probeOrder) {
|
|
4753
|
+
try {
|
|
4754
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4755
|
+
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4756
|
+
this.tokenEnv.set(deviceToken, env);
|
|
4757
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4758
|
+
return;
|
|
4759
|
+
} catch (err) {
|
|
4760
|
+
lastErr = err;
|
|
4761
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
4762
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
4763
|
+
if (!isBadDeviceTokenError(err)) {
|
|
4764
|
+
throw err;
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
}
|
|
4768
|
+
this.deadTokens.add(deviceToken);
|
|
4769
|
+
for (const [sid, tok] of this.tokens) {
|
|
4770
|
+
if (tok === deviceToken) {
|
|
4771
|
+
this.tokens.delete(sid);
|
|
4772
|
+
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
4773
|
+
break;
|
|
4774
|
+
}
|
|
4775
|
+
}
|
|
4776
|
+
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4777
|
+
}
|
|
4778
|
+
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
4779
|
+
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
4194
4780
|
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
4195
4781
|
const jwt = this.getJWT();
|
|
4196
4782
|
const payloadStr = JSON.stringify(payload);
|
|
4783
|
+
const priority = opts.priority ?? "10";
|
|
4197
4784
|
return new Promise((resolve, reject) => {
|
|
4198
4785
|
let client;
|
|
4199
4786
|
try {
|
|
4200
|
-
client = this.getHttp2Client();
|
|
4787
|
+
client = this.getHttp2Client(env);
|
|
4201
4788
|
} catch (err) {
|
|
4202
4789
|
return reject(err);
|
|
4203
4790
|
}
|
|
4204
|
-
const
|
|
4791
|
+
const headers = {
|
|
4205
4792
|
":method": "POST",
|
|
4206
4793
|
":path": `/3/device/${deviceToken}`,
|
|
4207
4794
|
"authorization": `bearer ${jwt}`,
|
|
4208
4795
|
"apns-topic": topic,
|
|
4209
4796
|
"apns-push-type": "liveactivity",
|
|
4210
|
-
"apns-priority":
|
|
4211
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) +
|
|
4797
|
+
"apns-priority": priority,
|
|
4798
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4212
4799
|
"content-type": "application/json",
|
|
4213
4800
|
"content-length": Buffer.byteLength(payloadStr)
|
|
4214
|
-
}
|
|
4801
|
+
};
|
|
4802
|
+
if (opts.collapseId) {
|
|
4803
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
4804
|
+
}
|
|
4805
|
+
const req = client.request(headers);
|
|
4215
4806
|
let statusCode = 0;
|
|
4216
4807
|
let responseData = "";
|
|
4217
|
-
req.on("response", (
|
|
4218
|
-
statusCode = Number(
|
|
4808
|
+
req.on("response", (headers2) => {
|
|
4809
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
4219
4810
|
});
|
|
4220
4811
|
req.on("data", (chunk) => {
|
|
4221
4812
|
responseData += chunk;
|
|
@@ -4225,10 +4816,11 @@ var ActivityPushChannel = class {
|
|
|
4225
4816
|
resolve();
|
|
4226
4817
|
} else {
|
|
4227
4818
|
if (statusCode === 0) {
|
|
4228
|
-
this.
|
|
4229
|
-
|
|
4819
|
+
const c = this.http2Clients[env];
|
|
4820
|
+
c?.destroy();
|
|
4821
|
+
delete this.http2Clients[env];
|
|
4230
4822
|
}
|
|
4231
|
-
reject(new
|
|
4823
|
+
reject(new ApnsError(statusCode, responseData));
|
|
4232
4824
|
}
|
|
4233
4825
|
});
|
|
4234
4826
|
req.on("error", (err) => {
|
|
@@ -4261,6 +4853,24 @@ var ActivityPushChannel = class {
|
|
|
4261
4853
|
return token;
|
|
4262
4854
|
}
|
|
4263
4855
|
};
|
|
4856
|
+
var ApnsError = class extends Error {
|
|
4857
|
+
constructor(statusCode, responseBody) {
|
|
4858
|
+
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
4859
|
+
this.statusCode = statusCode;
|
|
4860
|
+
this.responseBody = responseBody;
|
|
4861
|
+
this.name = "ApnsError";
|
|
4862
|
+
}
|
|
4863
|
+
};
|
|
4864
|
+
function isBadDeviceTokenError(err) {
|
|
4865
|
+
if (!(err instanceof ApnsError)) return false;
|
|
4866
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4867
|
+
try {
|
|
4868
|
+
const parsed = JSON.parse(err.responseBody);
|
|
4869
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4870
|
+
} catch {
|
|
4871
|
+
return false;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4264
4874
|
|
|
4265
4875
|
// src/session/ProjectReader.ts
|
|
4266
4876
|
var import_promises3 = require("fs/promises");
|
|
@@ -4660,6 +5270,45 @@ var PairingManager = class {
|
|
|
4660
5270
|
}
|
|
4661
5271
|
};
|
|
4662
5272
|
|
|
5273
|
+
// src/utils/shellPath.ts
|
|
5274
|
+
var import_node_child_process7 = require("child_process");
|
|
5275
|
+
var fixed = false;
|
|
5276
|
+
function fixShellPath() {
|
|
5277
|
+
if (fixed || isWindows) {
|
|
5278
|
+
fixed = true;
|
|
5279
|
+
return;
|
|
5280
|
+
}
|
|
5281
|
+
fixed = true;
|
|
5282
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
5283
|
+
const isFish = /\/fish$/.test(shell);
|
|
5284
|
+
const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
|
|
5285
|
+
let raw;
|
|
5286
|
+
try {
|
|
5287
|
+
raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
|
|
5288
|
+
encoding: "utf8",
|
|
5289
|
+
timeout: 3e3,
|
|
5290
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5291
|
+
});
|
|
5292
|
+
} catch (err) {
|
|
5293
|
+
console.warn("[fixShellPath] failed to read login shell PATH:", err);
|
|
5294
|
+
return;
|
|
5295
|
+
}
|
|
5296
|
+
const fromShell = raw.trim();
|
|
5297
|
+
if (!fromShell) return;
|
|
5298
|
+
process.env.PATH = mergePath(fromShell, process.env.PATH || "");
|
|
5299
|
+
}
|
|
5300
|
+
function mergePath(primary, secondary) {
|
|
5301
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5302
|
+
const out = [];
|
|
5303
|
+
for (const seg of primary.split(":").concat(secondary.split(":"))) {
|
|
5304
|
+
if (!seg) continue;
|
|
5305
|
+
if (seen.has(seg)) continue;
|
|
5306
|
+
seen.add(seg);
|
|
5307
|
+
out.push(seg);
|
|
5308
|
+
}
|
|
5309
|
+
return out.join(":");
|
|
5310
|
+
}
|
|
5311
|
+
|
|
4663
5312
|
// src/auth/AuthManager.ts
|
|
4664
5313
|
var import_child_process3 = require("child_process");
|
|
4665
5314
|
var import_child_process4 = require("child_process");
|
|
@@ -4778,13 +5427,10 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4778
5427
|
}
|
|
4779
5428
|
};
|
|
4780
5429
|
|
|
4781
|
-
// src/server.ts
|
|
4782
|
-
var import_promises8 = require("fs/promises");
|
|
4783
|
-
|
|
4784
5430
|
// src/terminal/TerminalExecutor.ts
|
|
4785
|
-
var
|
|
5431
|
+
var import_node_child_process8 = require("child_process");
|
|
4786
5432
|
var import_uuid5 = require("uuid");
|
|
4787
|
-
var EXEC_TIMEOUT_MS =
|
|
5433
|
+
var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4788
5434
|
var TerminalExecutor = class {
|
|
4789
5435
|
processes = /* @__PURE__ */ new Map();
|
|
4790
5436
|
eventCallbacks = [];
|
|
@@ -4806,9 +5452,9 @@ var TerminalExecutor = class {
|
|
|
4806
5452
|
}
|
|
4807
5453
|
exec(sessionId, command, cwd) {
|
|
4808
5454
|
const execId = (0, import_uuid5.v4)();
|
|
4809
|
-
const shell = isWindows ? "powershell" : "
|
|
4810
|
-
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
4811
|
-
const proc = (0,
|
|
5455
|
+
const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
|
|
5456
|
+
const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
|
|
5457
|
+
const proc = (0, import_node_child_process8.spawn)(shell, args, {
|
|
4812
5458
|
cwd,
|
|
4813
5459
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4814
5460
|
env: { ...process.env }
|
|
@@ -4845,6 +5491,14 @@ var TerminalExecutor = class {
|
|
|
4845
5491
|
});
|
|
4846
5492
|
const timer = setTimeout(() => {
|
|
4847
5493
|
if (this.processes.has(execId)) {
|
|
5494
|
+
this.emit({
|
|
5495
|
+
type: "terminal_output",
|
|
5496
|
+
sessionId,
|
|
5497
|
+
execId,
|
|
5498
|
+
stream: "stderr",
|
|
5499
|
+
data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
|
|
5500
|
+
`
|
|
5501
|
+
});
|
|
4848
5502
|
killProcessCrossPlatform(proc);
|
|
4849
5503
|
}
|
|
4850
5504
|
}, EXEC_TIMEOUT_MS);
|
|
@@ -4869,13 +5523,13 @@ var TerminalExecutor = class {
|
|
|
4869
5523
|
};
|
|
4870
5524
|
|
|
4871
5525
|
// src/xcode/XcodeBuildExecutor.ts
|
|
4872
|
-
var
|
|
5526
|
+
var import_node_child_process9 = require("child_process");
|
|
4873
5527
|
var import_node_util = require("util");
|
|
4874
5528
|
var import_promises4 = require("fs/promises");
|
|
4875
5529
|
var import_node_path6 = require("path");
|
|
4876
5530
|
var import_node_os7 = require("os");
|
|
4877
5531
|
var import_uuid6 = require("uuid");
|
|
4878
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
5532
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
4879
5533
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4880
5534
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4881
5535
|
var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
|
|
@@ -5064,7 +5718,7 @@ ${e.stderr ?? ""}`);
|
|
|
5064
5718
|
if (override) await this.saveConfig(projectPath, override);
|
|
5065
5719
|
const buildId = (0, import_uuid6.v4)();
|
|
5066
5720
|
const args = buildArgs(config);
|
|
5067
|
-
const proc = (0,
|
|
5721
|
+
const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
|
|
5068
5722
|
cwd: projectPath,
|
|
5069
5723
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5070
5724
|
env: { ...process.env, NSUnbufferedIO: "YES" }
|
|
@@ -5160,7 +5814,7 @@ ${e.stderr ?? ""}`);
|
|
|
5160
5814
|
|
|
5161
5815
|
`
|
|
5162
5816
|
});
|
|
5163
|
-
const proc = (0,
|
|
5817
|
+
const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
|
|
5164
5818
|
cwd: projectPath,
|
|
5165
5819
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5166
5820
|
});
|
|
@@ -5567,15 +6221,16 @@ var CommandDiscovery = class {
|
|
|
5567
6221
|
const cmd = sanitizeBashLine(rawLine);
|
|
5568
6222
|
if (!cmd) continue;
|
|
5569
6223
|
const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
|
|
5570
|
-
const
|
|
6224
|
+
const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
|
|
6225
|
+
const title = synthesizeTitle(finalCmd);
|
|
5571
6226
|
out.push(makeCommand({
|
|
5572
6227
|
title,
|
|
5573
|
-
command:
|
|
5574
|
-
cwd:
|
|
6228
|
+
command: finalCmd,
|
|
6229
|
+
cwd: cdCwd,
|
|
5575
6230
|
source,
|
|
5576
6231
|
sourceFile: fileName,
|
|
5577
6232
|
description: inlineComment ?? blockHeading,
|
|
5578
|
-
category: classifyByCommand(
|
|
6233
|
+
category: classifyByCommand(finalCmd)
|
|
5579
6234
|
}));
|
|
5580
6235
|
}
|
|
5581
6236
|
}
|
|
@@ -5620,6 +6275,19 @@ function synthesizeTitle(cmd) {
|
|
|
5620
6275
|
const head = tokens.slice(0, 3).join(" ");
|
|
5621
6276
|
return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
|
|
5622
6277
|
}
|
|
6278
|
+
function splitCdPrefix(cmd) {
|
|
6279
|
+
const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
|
|
6280
|
+
if (!m) return { cwd: "", command: cmd };
|
|
6281
|
+
const path2 = m[1];
|
|
6282
|
+
if (!path2) return { cwd: "", command: cmd };
|
|
6283
|
+
if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
|
|
6284
|
+
return { cwd: "", command: cmd };
|
|
6285
|
+
}
|
|
6286
|
+
if (path2.split("/").some((seg) => seg === "..")) {
|
|
6287
|
+
return { cwd: "", command: cmd };
|
|
6288
|
+
}
|
|
6289
|
+
return { cwd: path2, command: m[2].trim() };
|
|
6290
|
+
}
|
|
5623
6291
|
function splitInlineComment(line) {
|
|
5624
6292
|
let inSingle = false;
|
|
5625
6293
|
let inDouble = false;
|
|
@@ -5681,10 +6349,10 @@ function sourceWeight(s) {
|
|
|
5681
6349
|
}
|
|
5682
6350
|
|
|
5683
6351
|
// src/git/GitExecutor.ts
|
|
5684
|
-
var
|
|
6352
|
+
var import_node_child_process10 = require("child_process");
|
|
5685
6353
|
var import_node_util2 = require("util");
|
|
5686
6354
|
var import_uuid7 = require("uuid");
|
|
5687
|
-
var execAsync2 = (0, import_node_util2.promisify)(
|
|
6355
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
5688
6356
|
var STATUS_TIMEOUT_MS = 15e3;
|
|
5689
6357
|
var COMMIT_TIMEOUT_MS = 6e4;
|
|
5690
6358
|
var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -5857,7 +6525,7 @@ var GitExecutor = class {
|
|
|
5857
6525
|
});
|
|
5858
6526
|
let proc;
|
|
5859
6527
|
try {
|
|
5860
|
-
proc = (0,
|
|
6528
|
+
proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
|
|
5861
6529
|
cwd: projectPath,
|
|
5862
6530
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5863
6531
|
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
@@ -6037,9 +6705,15 @@ function isValidTask(value) {
|
|
|
6037
6705
|
}
|
|
6038
6706
|
|
|
6039
6707
|
// src/utils/cliCapabilities.ts
|
|
6040
|
-
var
|
|
6708
|
+
var import_node_child_process11 = require("child_process");
|
|
6709
|
+
var DEFAULT_MODELS = [
|
|
6710
|
+
{ value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
|
|
6711
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
|
|
6712
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
|
|
6713
|
+
];
|
|
6041
6714
|
var DEFAULT_CAPABILITIES = {
|
|
6042
|
-
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
6715
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
6716
|
+
models: DEFAULT_MODELS
|
|
6043
6717
|
};
|
|
6044
6718
|
async function parseCliCapabilities() {
|
|
6045
6719
|
const claudePath = findClaudePath();
|
|
@@ -6065,7 +6739,7 @@ async function parseCliCapabilities() {
|
|
|
6065
6739
|
}
|
|
6066
6740
|
function runCli(path2, args) {
|
|
6067
6741
|
return new Promise((resolve) => {
|
|
6068
|
-
(0,
|
|
6742
|
+
(0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
6069
6743
|
if (err) {
|
|
6070
6744
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
6071
6745
|
resolve(null);
|
|
@@ -6079,7 +6753,7 @@ function runCli(path2, args) {
|
|
|
6079
6753
|
// src/server.ts
|
|
6080
6754
|
var WS_PORT = 3745;
|
|
6081
6755
|
var HTTP_PORT = 3746;
|
|
6082
|
-
var execAsync3 = (0, import_node_util3.promisify)(
|
|
6756
|
+
var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
|
|
6083
6757
|
async function killPortProcess(port) {
|
|
6084
6758
|
try {
|
|
6085
6759
|
if (isWindows) {
|
|
@@ -6107,6 +6781,39 @@ async function killPortProcess(port) {
|
|
|
6107
6781
|
} catch {
|
|
6108
6782
|
}
|
|
6109
6783
|
}
|
|
6784
|
+
async function loadApnsConfigFromFile() {
|
|
6785
|
+
const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
|
|
6786
|
+
try {
|
|
6787
|
+
const raw = await (0, import_promises7.readFile)(path2, "utf8");
|
|
6788
|
+
const cfg = JSON.parse(raw);
|
|
6789
|
+
if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
|
|
6790
|
+
console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
|
|
6791
|
+
return null;
|
|
6792
|
+
}
|
|
6793
|
+
try {
|
|
6794
|
+
await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
|
|
6795
|
+
} catch (err) {
|
|
6796
|
+
console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
|
|
6797
|
+
return null;
|
|
6798
|
+
}
|
|
6799
|
+
console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
|
|
6800
|
+
console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
|
|
6801
|
+
return {
|
|
6802
|
+
teamId: cfg.teamId,
|
|
6803
|
+
keyId: cfg.keyId,
|
|
6804
|
+
authKeyPath: cfg.authKeyPath,
|
|
6805
|
+
sandbox: cfg.sandbox === true
|
|
6806
|
+
};
|
|
6807
|
+
} catch (err) {
|
|
6808
|
+
const code = err.code;
|
|
6809
|
+
if (code === "ENOENT") {
|
|
6810
|
+
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`);
|
|
6811
|
+
} else {
|
|
6812
|
+
console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
|
|
6813
|
+
}
|
|
6814
|
+
return null;
|
|
6815
|
+
}
|
|
6816
|
+
}
|
|
6110
6817
|
async function createWithRetry(label, port, factory) {
|
|
6111
6818
|
try {
|
|
6112
6819
|
return await factory();
|
|
@@ -6121,6 +6828,7 @@ async function createWithRetry(label, port, factory) {
|
|
|
6121
6828
|
}
|
|
6122
6829
|
}
|
|
6123
6830
|
async function start(opts = {}) {
|
|
6831
|
+
fixShellPath();
|
|
6124
6832
|
const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
|
|
6125
6833
|
const tokenFile = (0, import_node_path9.join)(configDir, "token");
|
|
6126
6834
|
let token;
|
|
@@ -6169,9 +6877,10 @@ async function start(opts = {}) {
|
|
|
6169
6877
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
6170
6878
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
6171
6879
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
6172
|
-
|
|
6880
|
+
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
6881
|
+
if (activityPushOpts) {
|
|
6173
6882
|
try {
|
|
6174
|
-
const activityChannel = new ActivityPushChannel(
|
|
6883
|
+
const activityChannel = new ActivityPushChannel(activityPushOpts);
|
|
6175
6884
|
notificationService.setActivityPushChannel(activityChannel);
|
|
6176
6885
|
console.log(`[Server] ${t("server.activityPushEnabled")}`);
|
|
6177
6886
|
} catch (err) {
|
|
@@ -6206,6 +6915,9 @@ async function start(opts = {}) {
|
|
|
6206
6915
|
notificationService.setGlobalPendingCountProvider(
|
|
6207
6916
|
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
6208
6917
|
);
|
|
6918
|
+
notificationService.setPendingApprovalsProvider(
|
|
6919
|
+
(sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
|
|
6920
|
+
);
|
|
6209
6921
|
let cliCapabilities = null;
|
|
6210
6922
|
parseCliCapabilities().then((caps) => {
|
|
6211
6923
|
cliCapabilities = caps;
|
|
@@ -6218,7 +6930,8 @@ async function start(opts = {}) {
|
|
|
6218
6930
|
onFire: async (task) => {
|
|
6219
6931
|
const p = task.payload;
|
|
6220
6932
|
if (p.kind === "create") {
|
|
6221
|
-
await (0, import_promises7.
|
|
6933
|
+
const dirExists = await (0, import_promises7.stat)(p.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
6934
|
+
if (!dirExists) await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
|
|
6222
6935
|
const session = await sessionManager.createSession(
|
|
6223
6936
|
p.projectPath,
|
|
6224
6937
|
p.message,
|
|
@@ -6276,7 +6989,7 @@ async function start(opts = {}) {
|
|
|
6276
6989
|
wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
6277
6990
|
}
|
|
6278
6991
|
if (cliCapabilities) {
|
|
6279
|
-
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
|
|
6992
|
+
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
|
|
6280
6993
|
}
|
|
6281
6994
|
if (scheduledManager) {
|
|
6282
6995
|
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
@@ -6286,7 +6999,8 @@ async function start(opts = {}) {
|
|
|
6286
6999
|
try {
|
|
6287
7000
|
switch (event.type) {
|
|
6288
7001
|
case "create_session": {
|
|
6289
|
-
await (0, import_promises7.
|
|
7002
|
+
const dirExists = await (0, import_promises7.stat)(event.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
7003
|
+
if (!dirExists) await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
|
|
6290
7004
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
6291
7005
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
6292
7006
|
await sessionManager.createSession(
|
|
@@ -6346,38 +7060,44 @@ async function start(opts = {}) {
|
|
|
6346
7060
|
sessions: sessionManager.getActiveSessions()
|
|
6347
7061
|
});
|
|
6348
7062
|
sessionManager.flushPendingAssistant(event.sessionId);
|
|
6349
|
-
const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6350
7063
|
if (sessionManager.isBufferTruncated(event.sessionId)) {
|
|
6351
7064
|
const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
|
|
6352
7065
|
if (projectPath) {
|
|
6353
7066
|
const historyResult = await getSessionHistory(projectPath, event.sessionId);
|
|
7067
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6354
7068
|
if (historyResult.ok && historyResult.value.length > 0) {
|
|
6355
|
-
const merged = [...historyResult.value, ...
|
|
7069
|
+
const merged = [...historyResult.value, ...buffered];
|
|
6356
7070
|
wsBridge.send(ws, {
|
|
6357
7071
|
type: "session_history",
|
|
6358
7072
|
sessionId: event.sessionId,
|
|
6359
7073
|
events: merged
|
|
6360
7074
|
});
|
|
6361
|
-
} else if (
|
|
7075
|
+
} else if (buffered.length > 0) {
|
|
6362
7076
|
wsBridge.send(ws, {
|
|
6363
7077
|
type: "session_history",
|
|
6364
7078
|
sessionId: event.sessionId,
|
|
6365
|
-
events:
|
|
7079
|
+
events: buffered
|
|
6366
7080
|
});
|
|
6367
7081
|
}
|
|
6368
|
-
} else
|
|
7082
|
+
} else {
|
|
7083
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7084
|
+
if (buffered.length > 0) {
|
|
7085
|
+
wsBridge.send(ws, {
|
|
7086
|
+
type: "session_history",
|
|
7087
|
+
sessionId: event.sessionId,
|
|
7088
|
+
events: buffered
|
|
7089
|
+
});
|
|
7090
|
+
}
|
|
7091
|
+
}
|
|
7092
|
+
} else {
|
|
7093
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7094
|
+
if (buffered.length > 0) {
|
|
6369
7095
|
wsBridge.send(ws, {
|
|
6370
7096
|
type: "session_history",
|
|
6371
7097
|
sessionId: event.sessionId,
|
|
6372
|
-
events:
|
|
7098
|
+
events: buffered
|
|
6373
7099
|
});
|
|
6374
7100
|
}
|
|
6375
|
-
} else if (bufferedEvents.length > 0) {
|
|
6376
|
-
wsBridge.send(ws, {
|
|
6377
|
-
type: "session_history",
|
|
6378
|
-
sessionId: event.sessionId,
|
|
6379
|
-
events: bufferedEvents
|
|
6380
|
-
});
|
|
6381
7101
|
}
|
|
6382
7102
|
for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
|
|
6383
7103
|
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
@@ -6474,7 +7194,7 @@ async function start(opts = {}) {
|
|
|
6474
7194
|
if (!isStreaming) {
|
|
6475
7195
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
6476
7196
|
try {
|
|
6477
|
-
const fileStat = await (0,
|
|
7197
|
+
const fileStat = await (0, import_promises7.stat)(filePath);
|
|
6478
7198
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
6479
7199
|
} catch {
|
|
6480
7200
|
}
|
|
@@ -6558,6 +7278,9 @@ async function start(opts = {}) {
|
|
|
6558
7278
|
wsBridge.clearViewingSession(ws);
|
|
6559
7279
|
break;
|
|
6560
7280
|
}
|
|
7281
|
+
case "approval_displayed": {
|
|
7282
|
+
break;
|
|
7283
|
+
}
|
|
6561
7284
|
case "always_allow_tool": {
|
|
6562
7285
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
6563
7286
|
break;
|
|
@@ -6755,14 +7478,12 @@ async function start(opts = {}) {
|
|
|
6755
7478
|
setTimeout(() => {
|
|
6756
7479
|
if (!approvalProxy.isPending(request.id)) return;
|
|
6757
7480
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6758
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6759
7481
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
6760
7482
|
notificationService.notifyApproval(request, pendingCount);
|
|
6761
|
-
},
|
|
7483
|
+
}, 3e3);
|
|
6762
7484
|
setTimeout(() => {
|
|
6763
7485
|
if (!approvalProxy.isPending(request.id)) return;
|
|
6764
7486
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6765
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6766
7487
|
console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
|
|
6767
7488
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
6768
7489
|
notificationService.notifyApproval(request, pendingCount);
|
|
@@ -6774,13 +7495,11 @@ async function start(opts = {}) {
|
|
|
6774
7495
|
setTimeout(() => {
|
|
6775
7496
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
6776
7497
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6777
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6778
7498
|
notificationService.notifyQuestion(request);
|
|
6779
7499
|
}, 5e3);
|
|
6780
7500
|
setTimeout(() => {
|
|
6781
7501
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
6782
7502
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6783
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6784
7503
|
console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
|
|
6785
7504
|
notificationService.notifyQuestion(request);
|
|
6786
7505
|
}, 6e4);
|