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/index.js
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_node_os10 = require("os");
|
|
28
28
|
var import_node_fs4 = require("fs");
|
|
29
29
|
var import_node_path10 = require("path");
|
|
30
|
-
var
|
|
30
|
+
var import_node_child_process13 = require("child_process");
|
|
31
31
|
|
|
32
32
|
// src/i18n/locales/zh.ts
|
|
33
33
|
var zh = {
|
|
@@ -306,7 +306,7 @@ var import_uuid9 = require("uuid");
|
|
|
306
306
|
var import_promises7 = require("fs/promises");
|
|
307
307
|
var import_node_os9 = require("os");
|
|
308
308
|
var import_node_path9 = require("path");
|
|
309
|
-
var
|
|
309
|
+
var import_node_child_process12 = require("child_process");
|
|
310
310
|
var import_node_util3 = require("util");
|
|
311
311
|
|
|
312
312
|
// src/providers/ProcessProvider.ts
|
|
@@ -662,7 +662,24 @@ var ProcessProvider = class {
|
|
|
662
662
|
writeUserMessage(proc, message, sessionId, images) {
|
|
663
663
|
const content = [];
|
|
664
664
|
if (images?.length) {
|
|
665
|
-
|
|
665
|
+
const ALLOWED_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
|
666
|
+
const MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
667
|
+
for (let i = 0; i < images.length; i++) {
|
|
668
|
+
const img = images[i];
|
|
669
|
+
if (!ALLOWED_TYPES.has(img.media_type)) {
|
|
670
|
+
if (sessionId) {
|
|
671
|
+
this.emitWriteError(sessionId, `Image #${i + 1} rejected: unsupported media_type "${img.media_type}". Only JPEG/PNG/WebP/GIF are accepted.`);
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const sizeBytes = Math.floor(img.data.length * 0.75);
|
|
676
|
+
if (sizeBytes > MAX_IMAGE_BYTES) {
|
|
677
|
+
if (sessionId) {
|
|
678
|
+
const sizeMb = (sizeBytes / (1024 * 1024)).toFixed(1);
|
|
679
|
+
this.emitWriteError(sessionId, `Image #${i + 1} rejected: ${sizeMb}MB exceeds 5MB per-image limit.`);
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
666
683
|
content.push({
|
|
667
684
|
type: "image",
|
|
668
685
|
source: { type: "base64", media_type: img.media_type, data: img.data }
|
|
@@ -689,6 +706,14 @@ var ProcessProvider = class {
|
|
|
689
706
|
this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
|
|
690
707
|
}
|
|
691
708
|
});
|
|
709
|
+
if (sessionId) {
|
|
710
|
+
const syntheticUser = {
|
|
711
|
+
type: "user",
|
|
712
|
+
session_id: sessionId,
|
|
713
|
+
message: { role: "user", content }
|
|
714
|
+
};
|
|
715
|
+
this.emitter.emit(this.getEventName(sessionId), syntheticUser);
|
|
716
|
+
}
|
|
692
717
|
}
|
|
693
718
|
/**
|
|
694
719
|
* 发出写入失败的合成错误事件
|
|
@@ -922,9 +947,13 @@ ${context}`;
|
|
|
922
947
|
throw new Error(`Session ${sessionId} stdin unavailable`);
|
|
923
948
|
}
|
|
924
949
|
const toolResult = JSON.stringify({
|
|
925
|
-
type: "
|
|
926
|
-
|
|
927
|
-
|
|
950
|
+
type: "user",
|
|
951
|
+
session_id: "",
|
|
952
|
+
message: {
|
|
953
|
+
role: "user",
|
|
954
|
+
content: [{ type: "tool_result", tool_use_id: toolUseId, content: answer }]
|
|
955
|
+
},
|
|
956
|
+
parent_tool_use_id: toolUseId
|
|
928
957
|
});
|
|
929
958
|
await new Promise((resolve, reject) => {
|
|
930
959
|
entry.process.stdin.write(toolResult + "\n", (err) => {
|
|
@@ -2935,6 +2964,8 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
2935
2964
|
this.handleApprovalHook(req, res);
|
|
2936
2965
|
} else if (req.method === "POST" && pathname === "/hook/notify") {
|
|
2937
2966
|
this.handleHookNotify(req, res);
|
|
2967
|
+
} else if (req.method === "POST" && pathname === "/api/resolve") {
|
|
2968
|
+
this.handleApiResolve(req, res);
|
|
2938
2969
|
} else if (req.method === "POST" && pathname === "/pair") {
|
|
2939
2970
|
this.handlePair(req, res);
|
|
2940
2971
|
} else if (req.method === "GET" && pathname === "/health") {
|
|
@@ -3000,6 +3031,34 @@ var ApprovalProxy = class _ApprovalProxy {
|
|
|
3000
3031
|
this.sendJson(res, 200, { decision: "deny", reason: "Server failed to process request" });
|
|
3001
3032
|
}
|
|
3002
3033
|
}
|
|
3034
|
+
/**
|
|
3035
|
+
* 移动端 API 端点:Widget Extension / Watch 直接提交审批决策
|
|
3036
|
+
*
|
|
3037
|
+
* 绕过 WebSocket 链路,让 iOS Widget Extension 的 AppIntent 在 App 挂起时
|
|
3038
|
+
* 仍能直接将审批结果提交到服务端。使用 Bearer token 鉴权(与 WS 同一 token)。
|
|
3039
|
+
*/
|
|
3040
|
+
async handleApiResolve(req, res) {
|
|
3041
|
+
const authHeader = req.headers.authorization ?? "";
|
|
3042
|
+
const bearerToken = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
3043
|
+
if (bearerToken !== this.token) {
|
|
3044
|
+
this.sendJson(res, 401, { ok: false, error: "Unauthorized" });
|
|
3045
|
+
return;
|
|
3046
|
+
}
|
|
3047
|
+
try {
|
|
3048
|
+
const body = await this.parseJsonBody(req);
|
|
3049
|
+
const requestId = String(body.requestId ?? "").trim();
|
|
3050
|
+
const decision = String(body.decision ?? "").trim();
|
|
3051
|
+
if (!requestId || decision !== "allow" && decision !== "deny") {
|
|
3052
|
+
this.sendJson(res, 400, { ok: false, error: "requestId and decision (allow|deny) required" });
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
const resolved = this.resolveApproval(requestId, { decision });
|
|
3056
|
+
this.sendJson(res, 200, { ok: resolved });
|
|
3057
|
+
} catch (err) {
|
|
3058
|
+
console.error("[ApprovalProxy] /api/resolve error:", err);
|
|
3059
|
+
this.sendJson(res, 500, { ok: false, error: "Internal error" });
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3003
3062
|
/**
|
|
3004
3063
|
* 非阻塞 hook 通知端点
|
|
3005
3064
|
*
|
|
@@ -3710,6 +3769,8 @@ var HookInstaller = class {
|
|
|
3710
3769
|
|
|
3711
3770
|
// src/notification/NotificationService.ts
|
|
3712
3771
|
var import_node_path5 = require("path");
|
|
3772
|
+
var RECENT_ACTIVITY_MAX = 6;
|
|
3773
|
+
var ACTIVITY_PUSH_THROTTLE_MS = 4e3;
|
|
3713
3774
|
var NotificationService = class {
|
|
3714
3775
|
constructor(sessionManager, expoChannel = null) {
|
|
3715
3776
|
this.sessionManager = sessionManager;
|
|
@@ -3728,6 +3789,30 @@ var NotificationService = class {
|
|
|
3728
3789
|
latestAssistantText = /* @__PURE__ */ new Map();
|
|
3729
3790
|
/** 获取全局待审批总数的回调(跨所有会话) */
|
|
3730
3791
|
globalPendingCountProvider = null;
|
|
3792
|
+
/** sessionId → 最近活动状态(用于 LA content push) */
|
|
3793
|
+
recentActivityState = /* @__PURE__ */ new Map();
|
|
3794
|
+
/** sessionId → 节流定时器(LA content push) */
|
|
3795
|
+
activityPushTimers = /* @__PURE__ */ new Map();
|
|
3796
|
+
/** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
|
|
3797
|
+
lastActivityPushAt = /* @__PURE__ */ new Map();
|
|
3798
|
+
/** 挂起的优先级提升请求(状态变化时设为 '10',flush 后清除) */
|
|
3799
|
+
pendingPriority = /* @__PURE__ */ new Map();
|
|
3800
|
+
/** sessionId → 累计活动计数器(用于 summary 模式的 activitySummary) */
|
|
3801
|
+
activityCounters = /* @__PURE__ */ new Map();
|
|
3802
|
+
/** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
|
|
3803
|
+
pendingApprovalsProvider = null;
|
|
3804
|
+
/**
|
|
3805
|
+
* sessionId → idle 结束定时器。
|
|
3806
|
+
* 会话变为 idle 时启动(30 秒);用户发新消息重回 running 时取消。
|
|
3807
|
+
* 30 秒内无新消息 → 调 endActivity 关闭 LA,同时发横幅通知告知完成。
|
|
3808
|
+
*/
|
|
3809
|
+
idleEndTimers = /* @__PURE__ */ new Map();
|
|
3810
|
+
/**
|
|
3811
|
+
* sessionId → LA 心跳定时器(setInterval)。
|
|
3812
|
+
* 确保 Agent 子任务等长时间无 claude_event 的场景下 LA 仍持续更新。
|
|
3813
|
+
* token 注册时启动,flushActivityEnd / removeActivityPushToken 时停止。
|
|
3814
|
+
*/
|
|
3815
|
+
laHeartbeatTimers = /* @__PURE__ */ new Map();
|
|
3731
3816
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3732
3817
|
addChannel(id, channel, enabled = true) {
|
|
3733
3818
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3755,11 +3840,27 @@ var NotificationService = class {
|
|
|
3755
3840
|
}
|
|
3756
3841
|
/** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
|
|
3757
3842
|
addActivityPushToken(sessionId, token) {
|
|
3758
|
-
this.activityPushChannel
|
|
3843
|
+
if (!this.activityPushChannel) {
|
|
3844
|
+
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`);
|
|
3845
|
+
return;
|
|
3846
|
+
}
|
|
3847
|
+
this.activityPushChannel.addToken(sessionId, token);
|
|
3848
|
+
console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
|
|
3849
|
+
this.scheduleActivityPush(sessionId, true);
|
|
3850
|
+
this.startLaHeartbeat(sessionId);
|
|
3759
3851
|
}
|
|
3760
3852
|
/** 移除 ActivityKit push token */
|
|
3761
3853
|
removeActivityPushToken(sessionId) {
|
|
3854
|
+
this.stopLaHeartbeat(sessionId);
|
|
3762
3855
|
this.activityPushChannel?.removeToken(sessionId);
|
|
3856
|
+
this.clearActivityPushTimer(sessionId);
|
|
3857
|
+
this.recentActivityState.delete(sessionId);
|
|
3858
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
3859
|
+
this.activityCounters.delete(sessionId);
|
|
3860
|
+
}
|
|
3861
|
+
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3862
|
+
setPendingApprovalsProvider(fn) {
|
|
3863
|
+
this.pendingApprovalsProvider = fn;
|
|
3763
3864
|
}
|
|
3764
3865
|
/** 设置全局待审批总数提供者 */
|
|
3765
3866
|
setGlobalPendingCountProvider(provider) {
|
|
@@ -3774,7 +3875,7 @@ var NotificationService = class {
|
|
|
3774
3875
|
this.yoloModeState.set(sessionId, enabled);
|
|
3775
3876
|
}
|
|
3776
3877
|
/** 直接触发审批通知(由 ApprovalProxy 回调调用) */
|
|
3777
|
-
notifyApproval(request, pendingCount) {
|
|
3878
|
+
async notifyApproval(request, pendingCount) {
|
|
3778
3879
|
if (this.yoloModeState.get(request.sessionId)) return;
|
|
3779
3880
|
const sessionTitle = this.getSessionTitle(request.sessionId);
|
|
3780
3881
|
const title = pendingCount > 1 ? t("notification.pendingApprovals", { title: sessionTitle, count: pendingCount }) : sessionTitle;
|
|
@@ -3782,12 +3883,16 @@ var NotificationService = class {
|
|
|
3782
3883
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3783
3884
|
const dangerLevel2 = this.getDangerLevel(request.toolName);
|
|
3784
3885
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3785
|
-
this.
|
|
3886
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3887
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
|
|
3888
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3889
|
+
const sent = await this.activityPushChannel.updateActivityWithAlert(
|
|
3786
3890
|
request.sessionId,
|
|
3787
3891
|
{
|
|
3788
3892
|
status: "waitingApproval",
|
|
3789
3893
|
sessionTitle,
|
|
3790
|
-
latestMessage
|
|
3894
|
+
latestMessage,
|
|
3895
|
+
recentActivity,
|
|
3791
3896
|
approvalInfo: {
|
|
3792
3897
|
requestId: request.id,
|
|
3793
3898
|
toolName: request.toolName,
|
|
@@ -3796,11 +3901,22 @@ var NotificationService = class {
|
|
|
3796
3901
|
pendingCount
|
|
3797
3902
|
},
|
|
3798
3903
|
isYoloMode,
|
|
3799
|
-
updatedAt: Date.now()
|
|
3904
|
+
updatedAt: Date.now(),
|
|
3905
|
+
displayMode: "summary",
|
|
3906
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3907
|
+
startedAt: session?.createdAt,
|
|
3908
|
+
stats: this.buildStatsPayload(session)
|
|
3800
3909
|
},
|
|
3801
3910
|
{ title, body }
|
|
3802
3911
|
);
|
|
3803
|
-
|
|
3912
|
+
if (sent) {
|
|
3913
|
+
console.log(`[NotificationService] \u{1F4E1} approval via ActivityKit push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3914
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
console.warn(`[NotificationService] \u26A0\uFE0F ActivityKit push \u5931\u8D25\uFF0C\u964D\u7EA7\u5230 Expo push session=${request.sessionId.slice(0, 8)}\u2026`);
|
|
3918
|
+
} else {
|
|
3919
|
+
console.log(`[NotificationService] \u{1F4F2} approval via Expo push session=${request.sessionId.slice(0, 8)}\u2026 tool=${request.toolName}`);
|
|
3804
3920
|
}
|
|
3805
3921
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
3806
3922
|
const isDangerous = dangerLevel === "danger" || dangerLevel === "write";
|
|
@@ -3833,17 +3949,25 @@ var NotificationService = class {
|
|
|
3833
3949
|
const body = `\u2753 ${request.question.slice(0, 80)}`;
|
|
3834
3950
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3835
3951
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3952
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3953
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === request.sessionId);
|
|
3836
3954
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3837
3955
|
request.sessionId,
|
|
3838
3956
|
{
|
|
3839
|
-
status: "
|
|
3957
|
+
status: "waitingQuestion",
|
|
3840
3958
|
sessionTitle,
|
|
3841
3959
|
latestMessage: request.question.slice(0, 80),
|
|
3960
|
+
recentActivity,
|
|
3842
3961
|
isYoloMode,
|
|
3843
|
-
updatedAt: Date.now()
|
|
3962
|
+
updatedAt: Date.now(),
|
|
3963
|
+
displayMode: "summary",
|
|
3964
|
+
activitySummary: this.buildActivitySummary(request.sessionId),
|
|
3965
|
+
startedAt: session?.createdAt,
|
|
3966
|
+
stats: this.buildStatsPayload(session)
|
|
3844
3967
|
},
|
|
3845
3968
|
{ title: sessionTitle, body }
|
|
3846
3969
|
);
|
|
3970
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3847
3971
|
return;
|
|
3848
3972
|
}
|
|
3849
3973
|
this.notify({
|
|
@@ -3877,6 +4001,12 @@ var NotificationService = class {
|
|
|
3877
4001
|
this.unsubscribe = null;
|
|
3878
4002
|
this.yoloModeState.clear();
|
|
3879
4003
|
this.latestAssistantText.clear();
|
|
4004
|
+
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
4005
|
+
this.activityPushTimers.clear();
|
|
4006
|
+
this.recentActivityState.clear();
|
|
4007
|
+
this.lastActivityPushAt.clear();
|
|
4008
|
+
this.pendingPriority.clear();
|
|
4009
|
+
this.activityCounters.clear();
|
|
3880
4010
|
}
|
|
3881
4011
|
// ============================================
|
|
3882
4012
|
// 内部方法
|
|
@@ -3885,29 +4015,29 @@ var NotificationService = class {
|
|
|
3885
4015
|
switch (event.type) {
|
|
3886
4016
|
case "claude_event": {
|
|
3887
4017
|
this.trackAssistantText(event.sessionId, event.event);
|
|
4018
|
+
this.updateRecentActivity(event.sessionId, event.event);
|
|
4019
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3888
4020
|
break;
|
|
3889
4021
|
}
|
|
3890
4022
|
case "claude_events": {
|
|
3891
4023
|
for (const e of event.events) {
|
|
3892
4024
|
this.trackAssistantText(event.sessionId, e);
|
|
4025
|
+
this.updateRecentActivity(event.sessionId, e);
|
|
3893
4026
|
}
|
|
4027
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3894
4028
|
break;
|
|
3895
4029
|
}
|
|
3896
4030
|
case "status_change": {
|
|
4031
|
+
this.clearActivityPushTimer(event.sessionId);
|
|
3897
4032
|
if (event.status === "idle") {
|
|
3898
|
-
|
|
3899
|
-
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
3900
|
-
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3901
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
4033
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
3902
4034
|
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3903
|
-
this.
|
|
3904
|
-
|
|
3905
|
-
sessionTitle,
|
|
3906
|
-
latestMessage: body,
|
|
3907
|
-
isYoloMode,
|
|
3908
|
-
updatedAt: Date.now()
|
|
3909
|
-
});
|
|
4035
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
4036
|
+
this.scheduleIdleEnd(event.sessionId, 3e4);
|
|
3910
4037
|
} else {
|
|
4038
|
+
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
4039
|
+
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
4040
|
+
const body = latestMsg ? `\u2705 ${latestMsg.slice(0, 80)}` : t("notification.taskComplete");
|
|
3911
4041
|
this.notify({
|
|
3912
4042
|
title: sessionTitle,
|
|
3913
4043
|
body,
|
|
@@ -3916,28 +4046,12 @@ var NotificationService = class {
|
|
|
3916
4046
|
data: { type: "task_complete", sessionId: event.sessionId }
|
|
3917
4047
|
});
|
|
3918
4048
|
}
|
|
4049
|
+
} else if (event.status === "running" || event.status === "waiting_approval" || event.status === "waiting_question") {
|
|
4050
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4051
|
+
this.scheduleActivityPush(event.sessionId, true, "10");
|
|
3919
4052
|
} else if (event.status === "error") {
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
const body = latestMsg ? `\u274C ${latestMsg.slice(0, 80)}` : t("notification.taskError");
|
|
3923
|
-
const isYoloMode = this.getYoloMode(event.sessionId);
|
|
3924
|
-
if (this.activityPushChannel?.hasToken(event.sessionId)) {
|
|
3925
|
-
this.activityPushChannel.endActivity(event.sessionId, {
|
|
3926
|
-
status: "error",
|
|
3927
|
-
sessionTitle,
|
|
3928
|
-
latestMessage: body,
|
|
3929
|
-
isYoloMode,
|
|
3930
|
-
updatedAt: Date.now()
|
|
3931
|
-
});
|
|
3932
|
-
} else {
|
|
3933
|
-
this.notify({
|
|
3934
|
-
title: sessionTitle,
|
|
3935
|
-
body,
|
|
3936
|
-
sound: "default",
|
|
3937
|
-
badge: this.getGlobalPendingCount(),
|
|
3938
|
-
data: { type: "task_error", sessionId: event.sessionId }
|
|
3939
|
-
});
|
|
3940
|
-
}
|
|
4053
|
+
this.cancelIdleEndTimer(event.sessionId);
|
|
4054
|
+
this.flushActivityEnd(event.sessionId, "error");
|
|
3941
4055
|
}
|
|
3942
4056
|
break;
|
|
3943
4057
|
}
|
|
@@ -3970,6 +4084,383 @@ var NotificationService = class {
|
|
|
3970
4084
|
getYoloMode(sessionId) {
|
|
3971
4085
|
return this.yoloModeState.get(sessionId) ?? false;
|
|
3972
4086
|
}
|
|
4087
|
+
// ============================================
|
|
4088
|
+
// Live Activity 内容推送(后台 LA 实时刷新)
|
|
4089
|
+
// ============================================
|
|
4090
|
+
/**
|
|
4091
|
+
* 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
|
|
4092
|
+
* 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
|
|
4093
|
+
* 切换 message.id 视为新 turn,旧条目沉淀到 history。
|
|
4094
|
+
*/
|
|
4095
|
+
updateRecentActivity(sessionId, event) {
|
|
4096
|
+
if (event.type === "result") {
|
|
4097
|
+
const state2 = this.recentActivityState.get(sessionId);
|
|
4098
|
+
if (state2 && state2.currentEntries.length > 0) {
|
|
4099
|
+
state2.history.push(...state2.currentEntries);
|
|
4100
|
+
while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
|
|
4101
|
+
state2.currentEntries = [];
|
|
4102
|
+
state2.currentMessageId = null;
|
|
4103
|
+
}
|
|
4104
|
+
return;
|
|
4105
|
+
}
|
|
4106
|
+
if (event.type !== "assistant") return;
|
|
4107
|
+
const msg = event.message;
|
|
4108
|
+
if (!Array.isArray(msg.content)) return;
|
|
4109
|
+
let state = this.recentActivityState.get(sessionId);
|
|
4110
|
+
if (!state) {
|
|
4111
|
+
state = { history: [], currentMessageId: null, currentEntries: [] };
|
|
4112
|
+
this.recentActivityState.set(sessionId, state);
|
|
4113
|
+
}
|
|
4114
|
+
if (state.currentMessageId !== msg.id) {
|
|
4115
|
+
if (state.currentEntries.length > 0) {
|
|
4116
|
+
state.history.push(...state.currentEntries);
|
|
4117
|
+
while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
|
|
4118
|
+
}
|
|
4119
|
+
state.currentEntries = [];
|
|
4120
|
+
state.currentMessageId = msg.id;
|
|
4121
|
+
}
|
|
4122
|
+
const next = [];
|
|
4123
|
+
for (const block of msg.content) {
|
|
4124
|
+
if (block.type === "text") {
|
|
4125
|
+
const line = this.summarizeText(block.text);
|
|
4126
|
+
if (line.length >= 4) next.push(line);
|
|
4127
|
+
} else if (block.type === "tool_use") {
|
|
4128
|
+
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4129
|
+
if (line) next.push(line);
|
|
4130
|
+
this.incrementCounter(sessionId, block.name);
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
state.currentEntries = next;
|
|
4134
|
+
}
|
|
4135
|
+
/** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
|
|
4136
|
+
getRecentActivity(sessionId) {
|
|
4137
|
+
const state = this.recentActivityState.get(sessionId);
|
|
4138
|
+
if (!state) return [];
|
|
4139
|
+
const combined = [...state.history, ...state.currentEntries];
|
|
4140
|
+
return combined.slice(-RECENT_ACTIVITY_MAX);
|
|
4141
|
+
}
|
|
4142
|
+
/** 工具名 → 计数器类别映射 */
|
|
4143
|
+
incrementCounter(sessionId, toolName) {
|
|
4144
|
+
let c = this.activityCounters.get(sessionId);
|
|
4145
|
+
if (!c) {
|
|
4146
|
+
c = { filesEdited: 0, commandsRun: 0, searches: 0, filesRead: 0, messagesReceived: 0 };
|
|
4147
|
+
this.activityCounters.set(sessionId, c);
|
|
4148
|
+
}
|
|
4149
|
+
switch (toolName) {
|
|
4150
|
+
case "Edit":
|
|
4151
|
+
case "MultiEdit":
|
|
4152
|
+
case "Write":
|
|
4153
|
+
case "NotebookEdit":
|
|
4154
|
+
c.filesEdited++;
|
|
4155
|
+
break;
|
|
4156
|
+
case "Bash":
|
|
4157
|
+
c.commandsRun++;
|
|
4158
|
+
break;
|
|
4159
|
+
case "Grep":
|
|
4160
|
+
case "Glob":
|
|
4161
|
+
case "WebSearch":
|
|
4162
|
+
case "WebFetch":
|
|
4163
|
+
c.searches++;
|
|
4164
|
+
break;
|
|
4165
|
+
case "Read":
|
|
4166
|
+
c.filesRead++;
|
|
4167
|
+
break;
|
|
4168
|
+
default:
|
|
4169
|
+
break;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
4172
|
+
/** 把累计计数器格式化为可读摘要(如"已编辑 3 个文件 · 运行 5 条命令") */
|
|
4173
|
+
buildActivitySummary(sessionId) {
|
|
4174
|
+
const c = this.activityCounters.get(sessionId);
|
|
4175
|
+
if (!c) return "";
|
|
4176
|
+
const parts = [];
|
|
4177
|
+
if (c.filesEdited > 0) parts.push(`\u5DF2\u7F16\u8F91 ${c.filesEdited} \u4E2A\u6587\u4EF6`);
|
|
4178
|
+
if (c.commandsRun > 0) parts.push(`\u8FD0\u884C ${c.commandsRun} \u6761\u547D\u4EE4`);
|
|
4179
|
+
if (c.searches > 0) parts.push(`\u641C\u7D22 ${c.searches} \u6B21`);
|
|
4180
|
+
if (c.filesRead > 0) parts.push(`\u9605\u8BFB ${c.filesRead} \u4E2A\u6587\u4EF6`);
|
|
4181
|
+
return parts.join(" \xB7 ") || "\u4F1A\u8BDD\u8FDB\u884C\u4E2D\u2026";
|
|
4182
|
+
}
|
|
4183
|
+
buildStatsPayload(session) {
|
|
4184
|
+
return {
|
|
4185
|
+
totalInputTokens: session?.stats?.totalInputTokens ?? 0,
|
|
4186
|
+
totalOutputTokens: session?.stats?.totalOutputTokens ?? 0,
|
|
4187
|
+
totalCostUsd: session?.stats?.totalCostUsd,
|
|
4188
|
+
totalDurationMs: session?.stats?.totalDurationMs,
|
|
4189
|
+
runningStartedAt: session?.stats?.runningStartedAt
|
|
4190
|
+
};
|
|
4191
|
+
}
|
|
4192
|
+
/** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
|
|
4193
|
+
scheduleActivityPush(sessionId, force = false, priority) {
|
|
4194
|
+
if (!this.activityPushChannel) return;
|
|
4195
|
+
if (!this.activityPushChannel.hasToken(sessionId)) {
|
|
4196
|
+
console.warn(`[NotificationService] \u26A0\uFE0F skip LA push: session=${sessionId.slice(0, 8)}\u2026 token \u672A\u6CE8\u518C`);
|
|
4197
|
+
return;
|
|
4198
|
+
}
|
|
4199
|
+
if (priority === "10") {
|
|
4200
|
+
this.pendingPriority.set(sessionId, "10");
|
|
4201
|
+
}
|
|
4202
|
+
const now = Date.now();
|
|
4203
|
+
const last = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4204
|
+
const elapsed = now - last;
|
|
4205
|
+
if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
|
|
4206
|
+
this.clearActivityPushTimer(sessionId);
|
|
4207
|
+
this.flushActivityPush(sessionId);
|
|
4208
|
+
return;
|
|
4209
|
+
}
|
|
4210
|
+
if (this.activityPushTimers.has(sessionId)) return;
|
|
4211
|
+
const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
|
|
4212
|
+
this.activityPushTimers.set(
|
|
4213
|
+
sessionId,
|
|
4214
|
+
setTimeout(() => {
|
|
4215
|
+
this.activityPushTimers.delete(sessionId);
|
|
4216
|
+
this.flushActivityPush(sessionId);
|
|
4217
|
+
}, wait)
|
|
4218
|
+
);
|
|
4219
|
+
}
|
|
4220
|
+
clearActivityPushTimer(sessionId) {
|
|
4221
|
+
const timer = this.activityPushTimers.get(sessionId);
|
|
4222
|
+
if (timer) {
|
|
4223
|
+
clearTimeout(timer);
|
|
4224
|
+
this.activityPushTimers.delete(sessionId);
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
/**
|
|
4228
|
+
* 启动 LA 心跳(每 ACTIVITY_PUSH_THROTTLE_MS 触发一次 scheduleActivityPush)。
|
|
4229
|
+
* 确保 Agent 子会话等长时间无 claude_event 的情况下 LA 仍持续更新。
|
|
4230
|
+
* 心跳只在会话活跃状态(running/waitingApproval/waitingQuestion)下发推送;
|
|
4231
|
+
* scheduleActivityPush 自带节流,心跳与正常事件驱动不冲突。
|
|
4232
|
+
*/
|
|
4233
|
+
startLaHeartbeat(sessionId) {
|
|
4234
|
+
if (this.laHeartbeatTimers.has(sessionId)) return;
|
|
4235
|
+
const timer = setInterval(() => {
|
|
4236
|
+
if (!this.activityPushChannel?.hasToken(sessionId)) {
|
|
4237
|
+
this.stopLaHeartbeat(sessionId);
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4241
|
+
if (!session) {
|
|
4242
|
+
this.stopLaHeartbeat(sessionId);
|
|
4243
|
+
return;
|
|
4244
|
+
}
|
|
4245
|
+
if (session.status === "running" || session.status === "waiting_approval" || session.status === "waiting_question") {
|
|
4246
|
+
this.scheduleActivityPush(sessionId);
|
|
4247
|
+
}
|
|
4248
|
+
}, ACTIVITY_PUSH_THROTTLE_MS);
|
|
4249
|
+
this.laHeartbeatTimers.set(sessionId, timer);
|
|
4250
|
+
}
|
|
4251
|
+
/** 停止 LA 心跳 */
|
|
4252
|
+
stopLaHeartbeat(sessionId) {
|
|
4253
|
+
const timer = this.laHeartbeatTimers.get(sessionId);
|
|
4254
|
+
if (timer) {
|
|
4255
|
+
clearInterval(timer);
|
|
4256
|
+
this.laHeartbeatTimers.delete(sessionId);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
/** 取消 idle 结束定时器 */
|
|
4260
|
+
cancelIdleEndTimer(sessionId) {
|
|
4261
|
+
const timer = this.idleEndTimers.get(sessionId);
|
|
4262
|
+
if (timer) {
|
|
4263
|
+
clearTimeout(timer);
|
|
4264
|
+
this.idleEndTimers.delete(sessionId);
|
|
4265
|
+
}
|
|
4266
|
+
}
|
|
4267
|
+
/**
|
|
4268
|
+
* 启动 idle 结束定时器:delayMs 后若会话仍 idle,调 endActivity + 发通知。
|
|
4269
|
+
* 保证多轮对话期间(idle→running→idle)LA 不会过早消失。
|
|
4270
|
+
*/
|
|
4271
|
+
scheduleIdleEnd(sessionId, delayMs) {
|
|
4272
|
+
const timer = setTimeout(() => {
|
|
4273
|
+
this.idleEndTimers.delete(sessionId);
|
|
4274
|
+
this.flushActivityEnd(sessionId, "idle");
|
|
4275
|
+
}, delayMs);
|
|
4276
|
+
this.idleEndTimers.set(sessionId, timer);
|
|
4277
|
+
}
|
|
4278
|
+
/**
|
|
4279
|
+
* 结束 LA 并发完成通知。有 LA token → APNs event:end(带横幅);
|
|
4280
|
+
* 无 token → 普通 Expo push。清理所有相关状态。
|
|
4281
|
+
*/
|
|
4282
|
+
flushActivityEnd(sessionId, reason) {
|
|
4283
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4284
|
+
const latestMsg = this.latestAssistantText.get(sessionId);
|
|
4285
|
+
const isError = reason === "error";
|
|
4286
|
+
const body = isError ? `\u274C ${latestMsg?.slice(0, 80) ?? t("notification.taskError")}` : `\u2705 ${latestMsg?.slice(0, 80) ?? t("notification.taskComplete")}`;
|
|
4287
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4288
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4289
|
+
this.notify({
|
|
4290
|
+
title: sessionTitle,
|
|
4291
|
+
body,
|
|
4292
|
+
sound: "default",
|
|
4293
|
+
badge: this.getGlobalPendingCount(),
|
|
4294
|
+
data: { type: isError ? "task_error" : "task_complete", sessionId }
|
|
4295
|
+
});
|
|
4296
|
+
if (this.activityPushChannel?.hasToken(sessionId)) {
|
|
4297
|
+
this.activityPushChannel.endActivity(
|
|
4298
|
+
sessionId,
|
|
4299
|
+
{
|
|
4300
|
+
status: isError ? "error" : "completed",
|
|
4301
|
+
sessionTitle,
|
|
4302
|
+
latestMessage: body,
|
|
4303
|
+
recentActivity: this.getRecentActivity(sessionId),
|
|
4304
|
+
isYoloMode,
|
|
4305
|
+
updatedAt: Date.now(),
|
|
4306
|
+
displayMode: "summary",
|
|
4307
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4308
|
+
startedAt: session?.createdAt,
|
|
4309
|
+
stats: this.buildStatsPayload(session)
|
|
4310
|
+
}
|
|
4311
|
+
// 不传 alert——Expo push 已处理通知,event:end 仅用于关闭 LA
|
|
4312
|
+
).catch((err) => {
|
|
4313
|
+
console.warn("[NotificationService] endActivity (close LA) failed, LA may linger:", err);
|
|
4314
|
+
});
|
|
4315
|
+
}
|
|
4316
|
+
this.stopLaHeartbeat(sessionId);
|
|
4317
|
+
this.recentActivityState.delete(sessionId);
|
|
4318
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
4319
|
+
this.activityCounters.delete(sessionId);
|
|
4320
|
+
console.log(`[NotificationService] \u{1F3C1} LA end (${reason}) session=${sessionId.slice(0, 8)}\u2026`);
|
|
4321
|
+
}
|
|
4322
|
+
/** 真正发送一次 LA content push(无 alert) */
|
|
4323
|
+
flushActivityPush(sessionId) {
|
|
4324
|
+
const channel = this.activityPushChannel;
|
|
4325
|
+
if (!channel?.hasToken(sessionId)) return;
|
|
4326
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4327
|
+
if (!session) return;
|
|
4328
|
+
const recentActivity = this.getRecentActivity(sessionId);
|
|
4329
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
|
|
4330
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4331
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4332
|
+
const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
|
|
4333
|
+
const latestApproval = pendingApprovals[pendingApprovals.length - 1];
|
|
4334
|
+
const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
|
|
4335
|
+
const contentState = {
|
|
4336
|
+
status,
|
|
4337
|
+
sessionTitle,
|
|
4338
|
+
latestMessage,
|
|
4339
|
+
recentActivity,
|
|
4340
|
+
isYoloMode,
|
|
4341
|
+
updatedAt: Date.now(),
|
|
4342
|
+
displayMode: "summary",
|
|
4343
|
+
activitySummary: this.buildActivitySummary(sessionId),
|
|
4344
|
+
startedAt: session.createdAt
|
|
4345
|
+
};
|
|
4346
|
+
if (latestApproval) {
|
|
4347
|
+
contentState.approvalInfo = {
|
|
4348
|
+
requestId: latestApproval.id,
|
|
4349
|
+
toolName: latestApproval.toolName,
|
|
4350
|
+
description: String(latestApproval.description ?? "").slice(0, 80),
|
|
4351
|
+
dangerLevel: this.getDangerLevel(latestApproval.toolName),
|
|
4352
|
+
pendingCount: pendingApprovals.length
|
|
4353
|
+
};
|
|
4354
|
+
}
|
|
4355
|
+
contentState.stats = this.buildStatsPayload(session);
|
|
4356
|
+
const priority = this.pendingPriority.get(sessionId) ?? "5";
|
|
4357
|
+
this.pendingPriority.delete(sessionId);
|
|
4358
|
+
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4359
|
+
const lineCount = recentActivity.length;
|
|
4360
|
+
channel.updateActivity(sessionId, contentState, { priority }).then((ok) => {
|
|
4361
|
+
if (ok) {
|
|
4362
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} p=${priority} lines=${lineCount}`);
|
|
4363
|
+
}
|
|
4364
|
+
}).catch((err) => {
|
|
4365
|
+
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
/** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
|
|
4369
|
+
mapSessionStatus(status) {
|
|
4370
|
+
switch (status) {
|
|
4371
|
+
case "running":
|
|
4372
|
+
return "running";
|
|
4373
|
+
case "waiting_approval":
|
|
4374
|
+
return "waitingApproval";
|
|
4375
|
+
case "waiting_question":
|
|
4376
|
+
return "waitingQuestion";
|
|
4377
|
+
case "idle":
|
|
4378
|
+
return "completed";
|
|
4379
|
+
case "completed":
|
|
4380
|
+
return "completed";
|
|
4381
|
+
case "error":
|
|
4382
|
+
return "error";
|
|
4383
|
+
default:
|
|
4384
|
+
return "idle";
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
/** 文本块清洗:去多余空白 + 截断到 70 字符 */
|
|
4388
|
+
summarizeText(raw) {
|
|
4389
|
+
if (typeof raw !== "string") return "";
|
|
4390
|
+
const cleaned = raw.replace(/\s+/g, " ").trim();
|
|
4391
|
+
return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
|
|
4392
|
+
}
|
|
4393
|
+
/** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
|
|
4394
|
+
summarizeToolCall(name, input) {
|
|
4395
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
4396
|
+
const baseName = (p) => {
|
|
4397
|
+
const cleaned = p.split(/[?#]/)[0];
|
|
4398
|
+
const parts = cleaned.split("/");
|
|
4399
|
+
return parts[parts.length - 1] || cleaned;
|
|
4400
|
+
};
|
|
4401
|
+
const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
|
|
4402
|
+
switch (name) {
|
|
4403
|
+
case "Bash": {
|
|
4404
|
+
const cmd = str(input.command).split("\n")[0];
|
|
4405
|
+
return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
|
|
4406
|
+
}
|
|
4407
|
+
case "Edit": {
|
|
4408
|
+
const fp = baseName(str(input.file_path));
|
|
4409
|
+
return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
|
|
4410
|
+
}
|
|
4411
|
+
case "MultiEdit": {
|
|
4412
|
+
const fp = baseName(str(input.file_path));
|
|
4413
|
+
return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
|
|
4414
|
+
}
|
|
4415
|
+
case "Write": {
|
|
4416
|
+
const fp = baseName(str(input.file_path));
|
|
4417
|
+
return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
|
|
4418
|
+
}
|
|
4419
|
+
case "Read":
|
|
4420
|
+
case "NotebookEdit": {
|
|
4421
|
+
const fp = baseName(str(input.file_path) || str(input.notebook_path));
|
|
4422
|
+
return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
|
|
4423
|
+
}
|
|
4424
|
+
case "Grep": {
|
|
4425
|
+
const p = str(input.pattern);
|
|
4426
|
+
return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
|
|
4427
|
+
}
|
|
4428
|
+
case "Glob": {
|
|
4429
|
+
const p = str(input.pattern);
|
|
4430
|
+
return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
|
|
4431
|
+
}
|
|
4432
|
+
case "WebFetch": {
|
|
4433
|
+
const url = str(input.url);
|
|
4434
|
+
let host = url;
|
|
4435
|
+
try {
|
|
4436
|
+
host = new URL(url).hostname;
|
|
4437
|
+
} catch {
|
|
4438
|
+
}
|
|
4439
|
+
return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
|
|
4440
|
+
}
|
|
4441
|
+
case "WebSearch": {
|
|
4442
|
+
const q = str(input.query);
|
|
4443
|
+
return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
|
|
4444
|
+
}
|
|
4445
|
+
case "TodoWrite":
|
|
4446
|
+
return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
|
|
4447
|
+
case "Task":
|
|
4448
|
+
case "Agent": {
|
|
4449
|
+
const desc = str(input.description) || str(input.subagent_type);
|
|
4450
|
+
return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
|
|
4451
|
+
}
|
|
4452
|
+
case "ExitPlanMode":
|
|
4453
|
+
return "\u63D0\u4EA4\u8BA1\u5212";
|
|
4454
|
+
case "Skill": {
|
|
4455
|
+
const skill = str(input.skill);
|
|
4456
|
+
return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
|
|
4457
|
+
}
|
|
4458
|
+
default: {
|
|
4459
|
+
const summary = trunc(JSON.stringify(input), 50);
|
|
4460
|
+
return name ? `${name}: ${summary}` : summary;
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
3973
4464
|
};
|
|
3974
4465
|
|
|
3975
4466
|
// src/notification/DesktopNotificationChannel.ts
|
|
@@ -4026,12 +4517,13 @@ var ExpoNotificationChannel = class {
|
|
|
4026
4517
|
}
|
|
4027
4518
|
async send(payload) {
|
|
4028
4519
|
if (this.tokens.size === 0) return;
|
|
4029
|
-
const
|
|
4520
|
+
const isCompletionNotif = payload.data?.type === "task_complete" || payload.data?.type === "task_error";
|
|
4521
|
+
const targetTokens = isCompletionNotif ? Array.from(this.tokens) : Array.from(this.tokens).filter((token) => {
|
|
4030
4522
|
const ws = this.tokenWsMap.get(token);
|
|
4031
4523
|
return !ws || ws.readyState !== ws.OPEN;
|
|
4032
4524
|
});
|
|
4033
|
-
if (
|
|
4034
|
-
const messages =
|
|
4525
|
+
if (targetTokens.length === 0) return;
|
|
4526
|
+
const messages = targetTokens.map((to) => {
|
|
4035
4527
|
let sound = payload.sound ?? "default";
|
|
4036
4528
|
const prefs = this.soundPreferences.get(to);
|
|
4037
4529
|
if (prefs) {
|
|
@@ -4053,7 +4545,7 @@ var ExpoNotificationChannel = class {
|
|
|
4053
4545
|
};
|
|
4054
4546
|
});
|
|
4055
4547
|
try {
|
|
4056
|
-
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${
|
|
4548
|
+
console.log(`[ExpoNotificationChannel] ${t("notification.sendingPush")} (${targetTokens.length}/${this.tokens.size} devices${isCompletionNotif ? ", forced" : ""})`, targetTokens);
|
|
4057
4549
|
const res = await fetch(EXPO_PUSH_API, {
|
|
4058
4550
|
method: "POST",
|
|
4059
4551
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
@@ -4083,100 +4575,151 @@ var ExpoNotificationChannel = class {
|
|
|
4083
4575
|
var http2 = __toESM(require("http2"));
|
|
4084
4576
|
var fs2 = __toESM(require("fs"));
|
|
4085
4577
|
var crypto = __toESM(require("crypto"));
|
|
4578
|
+
var APNS_HOSTS = {
|
|
4579
|
+
production: "api.push.apple.com",
|
|
4580
|
+
sandbox: "api.sandbox.push.apple.com"
|
|
4581
|
+
};
|
|
4086
4582
|
var ActivityPushChannel = class {
|
|
4087
4583
|
/** sessionId -> activityPushToken */
|
|
4088
4584
|
tokens = /* @__PURE__ */ new Map();
|
|
4585
|
+
/**
|
|
4586
|
+
* 每个 token 已确认工作的 APNs 环境。
|
|
4587
|
+
* Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
|
|
4588
|
+
* Release build (aps-environment=production) 的 token 仅在 production 端有效。
|
|
4589
|
+
* 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
|
|
4590
|
+
*/
|
|
4591
|
+
tokenEnv = /* @__PURE__ */ new Map();
|
|
4592
|
+
/**
|
|
4593
|
+
* 两个环境都拒绝的 token(Live Activity 已销毁/过期)。
|
|
4594
|
+
* 标记后跳过所有发送尝试,避免无意义的 HTTP/2 请求。
|
|
4595
|
+
* 当同一 session 注册新 token 时自动清除旧 token 的标记。
|
|
4596
|
+
*/
|
|
4597
|
+
deadTokens = /* @__PURE__ */ new Set();
|
|
4598
|
+
/** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
|
|
4599
|
+
probeOrder;
|
|
4089
4600
|
teamId;
|
|
4090
4601
|
keyId;
|
|
4091
4602
|
authKey;
|
|
4092
|
-
apnsHost;
|
|
4093
4603
|
/** 缓存的 JWT token + 过期时间 */
|
|
4094
4604
|
cachedJwt = null;
|
|
4095
|
-
/**
|
|
4096
|
-
|
|
4605
|
+
/** 每个环境一条 HTTP/2 长连接 */
|
|
4606
|
+
http2Clients = {};
|
|
4097
4607
|
constructor(config) {
|
|
4098
4608
|
this.teamId = config.teamId;
|
|
4099
4609
|
this.keyId = config.keyId;
|
|
4100
4610
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
4101
|
-
this.
|
|
4102
|
-
console.log(`[ActivityPushChannel] Initialized (${
|
|
4103
|
-
}
|
|
4104
|
-
/**
|
|
4105
|
-
getHttp2Client() {
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4611
|
+
this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
|
|
4612
|
+
console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
|
|
4613
|
+
}
|
|
4614
|
+
/** 获取或新建指定环境的 HTTP/2 长连接 */
|
|
4615
|
+
getHttp2Client(env) {
|
|
4616
|
+
const existing = this.http2Clients[env];
|
|
4617
|
+
if (existing && !existing.destroyed && !existing.closed) {
|
|
4618
|
+
return existing;
|
|
4619
|
+
}
|
|
4620
|
+
const client = http2.connect(`https://${APNS_HOSTS[env]}`);
|
|
4621
|
+
client.on("error", (err) => {
|
|
4622
|
+
console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
|
|
4623
|
+
client.destroy();
|
|
4624
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4114
4625
|
});
|
|
4115
|
-
|
|
4116
|
-
this.
|
|
4626
|
+
client.on("close", () => {
|
|
4627
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4117
4628
|
});
|
|
4118
|
-
|
|
4629
|
+
this.http2Clients[env] = client;
|
|
4630
|
+
return client;
|
|
4119
4631
|
}
|
|
4120
4632
|
/** 注册 Activity push token */
|
|
4121
4633
|
addToken(sessionId, token) {
|
|
4634
|
+
const oldToken = this.tokens.get(sessionId);
|
|
4635
|
+
if (oldToken && oldToken !== token) {
|
|
4636
|
+
this.tokenEnv.delete(oldToken);
|
|
4637
|
+
this.deadTokens.delete(oldToken);
|
|
4638
|
+
}
|
|
4639
|
+
const existed = this.tokens.has(sessionId);
|
|
4122
4640
|
this.tokens.set(sessionId, token);
|
|
4123
|
-
console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
|
|
4641
|
+
console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
|
|
4124
4642
|
}
|
|
4125
4643
|
/** 移除 Activity push token */
|
|
4126
4644
|
removeToken(sessionId) {
|
|
4645
|
+
const tok = this.tokens.get(sessionId);
|
|
4127
4646
|
this.tokens.delete(sessionId);
|
|
4647
|
+
if (tok) {
|
|
4648
|
+
this.tokenEnv.delete(tok);
|
|
4649
|
+
this.deadTokens.delete(tok);
|
|
4650
|
+
}
|
|
4128
4651
|
}
|
|
4129
|
-
/** 发送 content-state 更新到指定会话的 Live Activity */
|
|
4130
|
-
async updateActivity(sessionId, contentState) {
|
|
4652
|
+
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知)。返回 true 表示实际发出 */
|
|
4653
|
+
async updateActivity(sessionId, contentState, opts) {
|
|
4131
4654
|
const token = this.tokens.get(sessionId);
|
|
4132
|
-
if (!token) return;
|
|
4655
|
+
if (!token) return false;
|
|
4656
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4657
|
+
const priority = opts?.priority ?? "5";
|
|
4133
4658
|
const payload = {
|
|
4134
4659
|
aps: {
|
|
4135
|
-
timestamp:
|
|
4660
|
+
timestamp: now,
|
|
4136
4661
|
event: "update",
|
|
4137
|
-
"content-state": contentState
|
|
4662
|
+
"content-state": contentState,
|
|
4663
|
+
"stale-date": now + 600
|
|
4138
4664
|
}
|
|
4139
4665
|
};
|
|
4140
4666
|
try {
|
|
4141
|
-
await this.sendToAPNs(token, payload
|
|
4667
|
+
await this.sendToAPNs(token, payload, {
|
|
4668
|
+
priority,
|
|
4669
|
+
collapseId: `state-${sessionId.slice(0, 54)}`
|
|
4670
|
+
});
|
|
4671
|
+
return true;
|
|
4142
4672
|
} catch (err) {
|
|
4143
4673
|
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4674
|
+
return false;
|
|
4144
4675
|
}
|
|
4145
4676
|
}
|
|
4146
|
-
/** 发送带通知的 content-state
|
|
4677
|
+
/** 发送带通知的 content-state 更新(审批请求时使用),返回是否发送成功 */
|
|
4147
4678
|
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4148
4679
|
const token = this.tokens.get(sessionId);
|
|
4149
|
-
if (!token) return;
|
|
4680
|
+
if (!token) return false;
|
|
4681
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4150
4682
|
const payload = {
|
|
4151
4683
|
aps: {
|
|
4152
|
-
timestamp:
|
|
4684
|
+
timestamp: now,
|
|
4153
4685
|
event: "update",
|
|
4154
4686
|
"content-state": contentState,
|
|
4687
|
+
"stale-date": now + 600,
|
|
4155
4688
|
alert,
|
|
4156
4689
|
sound: "default"
|
|
4157
4690
|
}
|
|
4158
4691
|
};
|
|
4159
4692
|
try {
|
|
4160
|
-
await this.sendToAPNs(token, payload);
|
|
4693
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4694
|
+
return true;
|
|
4161
4695
|
} catch (err) {
|
|
4162
4696
|
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4697
|
+
return false;
|
|
4163
4698
|
}
|
|
4164
4699
|
}
|
|
4165
|
-
/**
|
|
4166
|
-
|
|
4700
|
+
/**
|
|
4701
|
+
* 结束指定会话的 Live Activity。
|
|
4702
|
+
* 可选 alert:APNs event=end 时同时推送横幅通知 + 声音,用于在会话完成时提醒用户。
|
|
4703
|
+
*/
|
|
4704
|
+
async endActivity(sessionId, contentState, opts) {
|
|
4167
4705
|
const token = this.tokens.get(sessionId);
|
|
4168
4706
|
if (!token) return;
|
|
4169
|
-
const
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
}
|
|
4707
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4708
|
+
const aps = {
|
|
4709
|
+
timestamp: now,
|
|
4710
|
+
event: "end",
|
|
4711
|
+
"content-state": contentState
|
|
4175
4712
|
};
|
|
4713
|
+
if (opts?.alert) {
|
|
4714
|
+
aps.alert = opts.alert;
|
|
4715
|
+
aps.sound = "default";
|
|
4716
|
+
}
|
|
4717
|
+
const payload = { aps };
|
|
4176
4718
|
try {
|
|
4177
|
-
await this.sendToAPNs(token, payload);
|
|
4719
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4178
4720
|
} catch (err) {
|
|
4179
|
-
|
|
4721
|
+
this.tokens.delete(sessionId);
|
|
4722
|
+
throw err;
|
|
4180
4723
|
}
|
|
4181
4724
|
this.tokens.delete(sessionId);
|
|
4182
4725
|
}
|
|
@@ -4184,33 +4727,81 @@ var ActivityPushChannel = class {
|
|
|
4184
4727
|
hasToken(sessionId) {
|
|
4185
4728
|
return this.tokens.has(sessionId);
|
|
4186
4729
|
}
|
|
4187
|
-
/**
|
|
4188
|
-
|
|
4730
|
+
/**
|
|
4731
|
+
* 发送 APNs,自动处理环境探测。
|
|
4732
|
+
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4733
|
+
* 收到 BadDeviceToken / BadEnvironmentKeyInToken 自动切到另一个环境,
|
|
4734
|
+
* 并把成功的环境绑定到该 token。
|
|
4735
|
+
*/
|
|
4736
|
+
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4737
|
+
if (this.deadTokens.has(deviceToken)) {
|
|
4738
|
+
throw new Error(`token permanently dead (Activity ended): ${deviceToken.slice(0, 16)}\u2026`);
|
|
4739
|
+
}
|
|
4740
|
+
const known = this.tokenEnv.get(deviceToken);
|
|
4741
|
+
if (known) {
|
|
4742
|
+
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4743
|
+
}
|
|
4744
|
+
const short = deviceToken.slice(0, 16);
|
|
4745
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe start token=${short}\u2026 order=[${this.probeOrder.join(",")}]`);
|
|
4746
|
+
let lastErr = null;
|
|
4747
|
+
for (const env of this.probeOrder) {
|
|
4748
|
+
try {
|
|
4749
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe try ${env} token=${short}\u2026`);
|
|
4750
|
+
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4751
|
+
this.tokenEnv.set(deviceToken, env);
|
|
4752
|
+
console.log(`[ActivityPushChannel] \u2705 probe bound to ${env} (token=${short}\u2026)`);
|
|
4753
|
+
return;
|
|
4754
|
+
} catch (err) {
|
|
4755
|
+
lastErr = err;
|
|
4756
|
+
const reason = err instanceof ApnsError ? JSON.parse(err.responseBody || "{}")?.reason ?? err.statusCode : String(err);
|
|
4757
|
+
console.log(`[ActivityPushChannel] \u{1F50D} probe ${env} failed: ${reason}`);
|
|
4758
|
+
if (!isBadDeviceTokenError(err)) {
|
|
4759
|
+
throw err;
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
this.deadTokens.add(deviceToken);
|
|
4764
|
+
for (const [sid, tok] of this.tokens) {
|
|
4765
|
+
if (tok === deviceToken) {
|
|
4766
|
+
this.tokens.delete(sid);
|
|
4767
|
+
console.warn(`[ActivityPushChannel] \u2620\uFE0F dead token, session removed: session=${sid.slice(0, 8)}\u2026 token=${short}\u2026 (Activity ended/iOS reinstalled)`);
|
|
4768
|
+
break;
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4771
|
+
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4772
|
+
}
|
|
4773
|
+
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
4774
|
+
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
4189
4775
|
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
4190
4776
|
const jwt = this.getJWT();
|
|
4191
4777
|
const payloadStr = JSON.stringify(payload);
|
|
4778
|
+
const priority = opts.priority ?? "10";
|
|
4192
4779
|
return new Promise((resolve, reject) => {
|
|
4193
4780
|
let client;
|
|
4194
4781
|
try {
|
|
4195
|
-
client = this.getHttp2Client();
|
|
4782
|
+
client = this.getHttp2Client(env);
|
|
4196
4783
|
} catch (err) {
|
|
4197
4784
|
return reject(err);
|
|
4198
4785
|
}
|
|
4199
|
-
const
|
|
4786
|
+
const headers = {
|
|
4200
4787
|
":method": "POST",
|
|
4201
4788
|
":path": `/3/device/${deviceToken}`,
|
|
4202
4789
|
"authorization": `bearer ${jwt}`,
|
|
4203
4790
|
"apns-topic": topic,
|
|
4204
4791
|
"apns-push-type": "liveactivity",
|
|
4205
|
-
"apns-priority":
|
|
4206
|
-
"apns-expiration": String(Math.floor(Date.now() / 1e3) +
|
|
4792
|
+
"apns-priority": priority,
|
|
4793
|
+
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 300),
|
|
4207
4794
|
"content-type": "application/json",
|
|
4208
4795
|
"content-length": Buffer.byteLength(payloadStr)
|
|
4209
|
-
}
|
|
4796
|
+
};
|
|
4797
|
+
if (opts.collapseId) {
|
|
4798
|
+
headers["apns-collapse-id"] = opts.collapseId;
|
|
4799
|
+
}
|
|
4800
|
+
const req = client.request(headers);
|
|
4210
4801
|
let statusCode = 0;
|
|
4211
4802
|
let responseData = "";
|
|
4212
|
-
req.on("response", (
|
|
4213
|
-
statusCode = Number(
|
|
4803
|
+
req.on("response", (headers2) => {
|
|
4804
|
+
statusCode = Number(headers2[":status"] ?? 0);
|
|
4214
4805
|
});
|
|
4215
4806
|
req.on("data", (chunk) => {
|
|
4216
4807
|
responseData += chunk;
|
|
@@ -4220,10 +4811,11 @@ var ActivityPushChannel = class {
|
|
|
4220
4811
|
resolve();
|
|
4221
4812
|
} else {
|
|
4222
4813
|
if (statusCode === 0) {
|
|
4223
|
-
this.
|
|
4224
|
-
|
|
4814
|
+
const c = this.http2Clients[env];
|
|
4815
|
+
c?.destroy();
|
|
4816
|
+
delete this.http2Clients[env];
|
|
4225
4817
|
}
|
|
4226
|
-
reject(new
|
|
4818
|
+
reject(new ApnsError(statusCode, responseData));
|
|
4227
4819
|
}
|
|
4228
4820
|
});
|
|
4229
4821
|
req.on("error", (err) => {
|
|
@@ -4256,6 +4848,24 @@ var ActivityPushChannel = class {
|
|
|
4256
4848
|
return token;
|
|
4257
4849
|
}
|
|
4258
4850
|
};
|
|
4851
|
+
var ApnsError = class extends Error {
|
|
4852
|
+
constructor(statusCode, responseBody) {
|
|
4853
|
+
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
4854
|
+
this.statusCode = statusCode;
|
|
4855
|
+
this.responseBody = responseBody;
|
|
4856
|
+
this.name = "ApnsError";
|
|
4857
|
+
}
|
|
4858
|
+
};
|
|
4859
|
+
function isBadDeviceTokenError(err) {
|
|
4860
|
+
if (!(err instanceof ApnsError)) return false;
|
|
4861
|
+
if (err.statusCode !== 400 && err.statusCode !== 403 && err.statusCode !== 410) return false;
|
|
4862
|
+
try {
|
|
4863
|
+
const parsed = JSON.parse(err.responseBody);
|
|
4864
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "BadEnvironmentKeyInToken" || parsed.reason === "Unregistered";
|
|
4865
|
+
} catch {
|
|
4866
|
+
return false;
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4259
4869
|
|
|
4260
4870
|
// src/session/ProjectReader.ts
|
|
4261
4871
|
var import_promises3 = require("fs/promises");
|
|
@@ -4655,6 +5265,45 @@ var PairingManager = class {
|
|
|
4655
5265
|
}
|
|
4656
5266
|
};
|
|
4657
5267
|
|
|
5268
|
+
// src/utils/shellPath.ts
|
|
5269
|
+
var import_node_child_process7 = require("child_process");
|
|
5270
|
+
var fixed = false;
|
|
5271
|
+
function fixShellPath() {
|
|
5272
|
+
if (fixed || isWindows) {
|
|
5273
|
+
fixed = true;
|
|
5274
|
+
return;
|
|
5275
|
+
}
|
|
5276
|
+
fixed = true;
|
|
5277
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
5278
|
+
const isFish = /\/fish$/.test(shell);
|
|
5279
|
+
const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
|
|
5280
|
+
let raw;
|
|
5281
|
+
try {
|
|
5282
|
+
raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
|
|
5283
|
+
encoding: "utf8",
|
|
5284
|
+
timeout: 3e3,
|
|
5285
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5286
|
+
});
|
|
5287
|
+
} catch (err) {
|
|
5288
|
+
console.warn("[fixShellPath] failed to read login shell PATH:", err);
|
|
5289
|
+
return;
|
|
5290
|
+
}
|
|
5291
|
+
const fromShell = raw.trim();
|
|
5292
|
+
if (!fromShell) return;
|
|
5293
|
+
process.env.PATH = mergePath(fromShell, process.env.PATH || "");
|
|
5294
|
+
}
|
|
5295
|
+
function mergePath(primary, secondary) {
|
|
5296
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5297
|
+
const out = [];
|
|
5298
|
+
for (const seg of primary.split(":").concat(secondary.split(":"))) {
|
|
5299
|
+
if (!seg) continue;
|
|
5300
|
+
if (seen.has(seg)) continue;
|
|
5301
|
+
seen.add(seg);
|
|
5302
|
+
out.push(seg);
|
|
5303
|
+
}
|
|
5304
|
+
return out.join(":");
|
|
5305
|
+
}
|
|
5306
|
+
|
|
4658
5307
|
// src/auth/AuthManager.ts
|
|
4659
5308
|
var import_child_process3 = require("child_process");
|
|
4660
5309
|
var import_child_process4 = require("child_process");
|
|
@@ -4773,13 +5422,10 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4773
5422
|
}
|
|
4774
5423
|
};
|
|
4775
5424
|
|
|
4776
|
-
// src/server.ts
|
|
4777
|
-
var import_promises8 = require("fs/promises");
|
|
4778
|
-
|
|
4779
5425
|
// src/terminal/TerminalExecutor.ts
|
|
4780
|
-
var
|
|
5426
|
+
var import_node_child_process8 = require("child_process");
|
|
4781
5427
|
var import_uuid5 = require("uuid");
|
|
4782
|
-
var EXEC_TIMEOUT_MS =
|
|
5428
|
+
var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4783
5429
|
var TerminalExecutor = class {
|
|
4784
5430
|
processes = /* @__PURE__ */ new Map();
|
|
4785
5431
|
eventCallbacks = [];
|
|
@@ -4801,9 +5447,9 @@ var TerminalExecutor = class {
|
|
|
4801
5447
|
}
|
|
4802
5448
|
exec(sessionId, command, cwd) {
|
|
4803
5449
|
const execId = (0, import_uuid5.v4)();
|
|
4804
|
-
const shell = isWindows ? "powershell" : "
|
|
4805
|
-
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
4806
|
-
const proc = (0,
|
|
5450
|
+
const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
|
|
5451
|
+
const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
|
|
5452
|
+
const proc = (0, import_node_child_process8.spawn)(shell, args, {
|
|
4807
5453
|
cwd,
|
|
4808
5454
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4809
5455
|
env: { ...process.env }
|
|
@@ -4840,6 +5486,14 @@ var TerminalExecutor = class {
|
|
|
4840
5486
|
});
|
|
4841
5487
|
const timer = setTimeout(() => {
|
|
4842
5488
|
if (this.processes.has(execId)) {
|
|
5489
|
+
this.emit({
|
|
5490
|
+
type: "terminal_output",
|
|
5491
|
+
sessionId,
|
|
5492
|
+
execId,
|
|
5493
|
+
stream: "stderr",
|
|
5494
|
+
data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
|
|
5495
|
+
`
|
|
5496
|
+
});
|
|
4843
5497
|
killProcessCrossPlatform(proc);
|
|
4844
5498
|
}
|
|
4845
5499
|
}, EXEC_TIMEOUT_MS);
|
|
@@ -4864,13 +5518,13 @@ var TerminalExecutor = class {
|
|
|
4864
5518
|
};
|
|
4865
5519
|
|
|
4866
5520
|
// src/xcode/XcodeBuildExecutor.ts
|
|
4867
|
-
var
|
|
5521
|
+
var import_node_child_process9 = require("child_process");
|
|
4868
5522
|
var import_node_util = require("util");
|
|
4869
5523
|
var import_promises4 = require("fs/promises");
|
|
4870
5524
|
var import_node_path6 = require("path");
|
|
4871
5525
|
var import_node_os7 = require("os");
|
|
4872
5526
|
var import_uuid6 = require("uuid");
|
|
4873
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
5527
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
4874
5528
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4875
5529
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4876
5530
|
var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
|
|
@@ -5059,7 +5713,7 @@ ${e.stderr ?? ""}`);
|
|
|
5059
5713
|
if (override) await this.saveConfig(projectPath, override);
|
|
5060
5714
|
const buildId = (0, import_uuid6.v4)();
|
|
5061
5715
|
const args = buildArgs(config);
|
|
5062
|
-
const proc = (0,
|
|
5716
|
+
const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
|
|
5063
5717
|
cwd: projectPath,
|
|
5064
5718
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5065
5719
|
env: { ...process.env, NSUnbufferedIO: "YES" }
|
|
@@ -5155,7 +5809,7 @@ ${e.stderr ?? ""}`);
|
|
|
5155
5809
|
|
|
5156
5810
|
`
|
|
5157
5811
|
});
|
|
5158
|
-
const proc = (0,
|
|
5812
|
+
const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
|
|
5159
5813
|
cwd: projectPath,
|
|
5160
5814
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5161
5815
|
});
|
|
@@ -5562,15 +6216,16 @@ var CommandDiscovery = class {
|
|
|
5562
6216
|
const cmd = sanitizeBashLine(rawLine);
|
|
5563
6217
|
if (!cmd) continue;
|
|
5564
6218
|
const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
|
|
5565
|
-
const
|
|
6219
|
+
const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
|
|
6220
|
+
const title = synthesizeTitle(finalCmd);
|
|
5566
6221
|
out.push(makeCommand({
|
|
5567
6222
|
title,
|
|
5568
|
-
command:
|
|
5569
|
-
cwd:
|
|
6223
|
+
command: finalCmd,
|
|
6224
|
+
cwd: cdCwd,
|
|
5570
6225
|
source,
|
|
5571
6226
|
sourceFile: fileName,
|
|
5572
6227
|
description: inlineComment ?? blockHeading,
|
|
5573
|
-
category: classifyByCommand(
|
|
6228
|
+
category: classifyByCommand(finalCmd)
|
|
5574
6229
|
}));
|
|
5575
6230
|
}
|
|
5576
6231
|
}
|
|
@@ -5615,6 +6270,19 @@ function synthesizeTitle(cmd) {
|
|
|
5615
6270
|
const head = tokens.slice(0, 3).join(" ");
|
|
5616
6271
|
return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
|
|
5617
6272
|
}
|
|
6273
|
+
function splitCdPrefix(cmd) {
|
|
6274
|
+
const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
|
|
6275
|
+
if (!m) return { cwd: "", command: cmd };
|
|
6276
|
+
const path2 = m[1];
|
|
6277
|
+
if (!path2) return { cwd: "", command: cmd };
|
|
6278
|
+
if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
|
|
6279
|
+
return { cwd: "", command: cmd };
|
|
6280
|
+
}
|
|
6281
|
+
if (path2.split("/").some((seg) => seg === "..")) {
|
|
6282
|
+
return { cwd: "", command: cmd };
|
|
6283
|
+
}
|
|
6284
|
+
return { cwd: path2, command: m[2].trim() };
|
|
6285
|
+
}
|
|
5618
6286
|
function splitInlineComment(line) {
|
|
5619
6287
|
let inSingle = false;
|
|
5620
6288
|
let inDouble = false;
|
|
@@ -5676,10 +6344,10 @@ function sourceWeight(s) {
|
|
|
5676
6344
|
}
|
|
5677
6345
|
|
|
5678
6346
|
// src/git/GitExecutor.ts
|
|
5679
|
-
var
|
|
6347
|
+
var import_node_child_process10 = require("child_process");
|
|
5680
6348
|
var import_node_util2 = require("util");
|
|
5681
6349
|
var import_uuid7 = require("uuid");
|
|
5682
|
-
var execAsync2 = (0, import_node_util2.promisify)(
|
|
6350
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
5683
6351
|
var STATUS_TIMEOUT_MS = 15e3;
|
|
5684
6352
|
var COMMIT_TIMEOUT_MS = 6e4;
|
|
5685
6353
|
var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
@@ -5852,7 +6520,7 @@ var GitExecutor = class {
|
|
|
5852
6520
|
});
|
|
5853
6521
|
let proc;
|
|
5854
6522
|
try {
|
|
5855
|
-
proc = (0,
|
|
6523
|
+
proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
|
|
5856
6524
|
cwd: projectPath,
|
|
5857
6525
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5858
6526
|
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
@@ -6032,9 +6700,15 @@ function isValidTask(value) {
|
|
|
6032
6700
|
}
|
|
6033
6701
|
|
|
6034
6702
|
// src/utils/cliCapabilities.ts
|
|
6035
|
-
var
|
|
6703
|
+
var import_node_child_process11 = require("child_process");
|
|
6704
|
+
var DEFAULT_MODELS = [
|
|
6705
|
+
{ value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
|
|
6706
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
|
|
6707
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
|
|
6708
|
+
];
|
|
6036
6709
|
var DEFAULT_CAPABILITIES = {
|
|
6037
|
-
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
6710
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
6711
|
+
models: DEFAULT_MODELS
|
|
6038
6712
|
};
|
|
6039
6713
|
async function parseCliCapabilities() {
|
|
6040
6714
|
const claudePath = findClaudePath();
|
|
@@ -6060,7 +6734,7 @@ async function parseCliCapabilities() {
|
|
|
6060
6734
|
}
|
|
6061
6735
|
function runCli(path2, args) {
|
|
6062
6736
|
return new Promise((resolve) => {
|
|
6063
|
-
(0,
|
|
6737
|
+
(0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
6064
6738
|
if (err) {
|
|
6065
6739
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
6066
6740
|
resolve(null);
|
|
@@ -6074,7 +6748,7 @@ function runCli(path2, args) {
|
|
|
6074
6748
|
// src/server.ts
|
|
6075
6749
|
var WS_PORT = 3745;
|
|
6076
6750
|
var HTTP_PORT = 3746;
|
|
6077
|
-
var execAsync3 = (0, import_node_util3.promisify)(
|
|
6751
|
+
var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
|
|
6078
6752
|
async function killPortProcess(port) {
|
|
6079
6753
|
try {
|
|
6080
6754
|
if (isWindows) {
|
|
@@ -6102,6 +6776,39 @@ async function killPortProcess(port) {
|
|
|
6102
6776
|
} catch {
|
|
6103
6777
|
}
|
|
6104
6778
|
}
|
|
6779
|
+
async function loadApnsConfigFromFile() {
|
|
6780
|
+
const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
|
|
6781
|
+
try {
|
|
6782
|
+
const raw = await (0, import_promises7.readFile)(path2, "utf8");
|
|
6783
|
+
const cfg = JSON.parse(raw);
|
|
6784
|
+
if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
|
|
6785
|
+
console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
|
|
6786
|
+
return null;
|
|
6787
|
+
}
|
|
6788
|
+
try {
|
|
6789
|
+
await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
|
|
6790
|
+
} catch (err) {
|
|
6791
|
+
console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
|
|
6792
|
+
return null;
|
|
6793
|
+
}
|
|
6794
|
+
console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
|
|
6795
|
+
console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
|
|
6796
|
+
return {
|
|
6797
|
+
teamId: cfg.teamId,
|
|
6798
|
+
keyId: cfg.keyId,
|
|
6799
|
+
authKeyPath: cfg.authKeyPath,
|
|
6800
|
+
sandbox: cfg.sandbox === true
|
|
6801
|
+
};
|
|
6802
|
+
} catch (err) {
|
|
6803
|
+
const code = err.code;
|
|
6804
|
+
if (code === "ENOENT") {
|
|
6805
|
+
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`);
|
|
6806
|
+
} else {
|
|
6807
|
+
console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
|
|
6808
|
+
}
|
|
6809
|
+
return null;
|
|
6810
|
+
}
|
|
6811
|
+
}
|
|
6105
6812
|
async function createWithRetry(label, port, factory) {
|
|
6106
6813
|
try {
|
|
6107
6814
|
return await factory();
|
|
@@ -6116,6 +6823,7 @@ async function createWithRetry(label, port, factory) {
|
|
|
6116
6823
|
}
|
|
6117
6824
|
}
|
|
6118
6825
|
async function start(opts = {}) {
|
|
6826
|
+
fixShellPath();
|
|
6119
6827
|
const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
|
|
6120
6828
|
const tokenFile = (0, import_node_path9.join)(configDir, "token");
|
|
6121
6829
|
let token;
|
|
@@ -6164,9 +6872,10 @@ async function start(opts = {}) {
|
|
|
6164
6872
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
6165
6873
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
6166
6874
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
6167
|
-
|
|
6875
|
+
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
6876
|
+
if (activityPushOpts) {
|
|
6168
6877
|
try {
|
|
6169
|
-
const activityChannel = new ActivityPushChannel(
|
|
6878
|
+
const activityChannel = new ActivityPushChannel(activityPushOpts);
|
|
6170
6879
|
notificationService.setActivityPushChannel(activityChannel);
|
|
6171
6880
|
console.log(`[Server] ${t("server.activityPushEnabled")}`);
|
|
6172
6881
|
} catch (err) {
|
|
@@ -6201,6 +6910,9 @@ async function start(opts = {}) {
|
|
|
6201
6910
|
notificationService.setGlobalPendingCountProvider(
|
|
6202
6911
|
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
6203
6912
|
);
|
|
6913
|
+
notificationService.setPendingApprovalsProvider(
|
|
6914
|
+
(sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
|
|
6915
|
+
);
|
|
6204
6916
|
let cliCapabilities = null;
|
|
6205
6917
|
parseCliCapabilities().then((caps) => {
|
|
6206
6918
|
cliCapabilities = caps;
|
|
@@ -6213,7 +6925,8 @@ async function start(opts = {}) {
|
|
|
6213
6925
|
onFire: async (task) => {
|
|
6214
6926
|
const p = task.payload;
|
|
6215
6927
|
if (p.kind === "create") {
|
|
6216
|
-
await (0, import_promises7.
|
|
6928
|
+
const dirExists = await (0, import_promises7.stat)(p.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
6929
|
+
if (!dirExists) await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
|
|
6217
6930
|
const session = await sessionManager.createSession(
|
|
6218
6931
|
p.projectPath,
|
|
6219
6932
|
p.message,
|
|
@@ -6271,7 +6984,7 @@ async function start(opts = {}) {
|
|
|
6271
6984
|
wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
6272
6985
|
}
|
|
6273
6986
|
if (cliCapabilities) {
|
|
6274
|
-
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
|
|
6987
|
+
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
|
|
6275
6988
|
}
|
|
6276
6989
|
if (scheduledManager) {
|
|
6277
6990
|
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
@@ -6281,7 +6994,8 @@ async function start(opts = {}) {
|
|
|
6281
6994
|
try {
|
|
6282
6995
|
switch (event.type) {
|
|
6283
6996
|
case "create_session": {
|
|
6284
|
-
await (0, import_promises7.
|
|
6997
|
+
const dirExists = await (0, import_promises7.stat)(event.projectPath).then((s) => s.isDirectory()).catch(() => false);
|
|
6998
|
+
if (!dirExists) await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
|
|
6285
6999
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
6286
7000
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
6287
7001
|
await sessionManager.createSession(
|
|
@@ -6341,38 +7055,44 @@ async function start(opts = {}) {
|
|
|
6341
7055
|
sessions: sessionManager.getActiveSessions()
|
|
6342
7056
|
});
|
|
6343
7057
|
sessionManager.flushPendingAssistant(event.sessionId);
|
|
6344
|
-
const bufferedEvents = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6345
7058
|
if (sessionManager.isBufferTruncated(event.sessionId)) {
|
|
6346
7059
|
const projectPath = sessionManager.getSessionProjectPath(event.sessionId);
|
|
6347
7060
|
if (projectPath) {
|
|
6348
7061
|
const historyResult = await getSessionHistory(projectPath, event.sessionId);
|
|
7062
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
6349
7063
|
if (historyResult.ok && historyResult.value.length > 0) {
|
|
6350
|
-
const merged = [...historyResult.value, ...
|
|
7064
|
+
const merged = [...historyResult.value, ...buffered];
|
|
6351
7065
|
wsBridge.send(ws, {
|
|
6352
7066
|
type: "session_history",
|
|
6353
7067
|
sessionId: event.sessionId,
|
|
6354
7068
|
events: merged
|
|
6355
7069
|
});
|
|
6356
|
-
} else if (
|
|
7070
|
+
} else if (buffered.length > 0) {
|
|
7071
|
+
wsBridge.send(ws, {
|
|
7072
|
+
type: "session_history",
|
|
7073
|
+
sessionId: event.sessionId,
|
|
7074
|
+
events: buffered
|
|
7075
|
+
});
|
|
7076
|
+
}
|
|
7077
|
+
} else {
|
|
7078
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7079
|
+
if (buffered.length > 0) {
|
|
6357
7080
|
wsBridge.send(ws, {
|
|
6358
7081
|
type: "session_history",
|
|
6359
7082
|
sessionId: event.sessionId,
|
|
6360
|
-
events:
|
|
7083
|
+
events: buffered
|
|
6361
7084
|
});
|
|
6362
7085
|
}
|
|
6363
|
-
}
|
|
7086
|
+
}
|
|
7087
|
+
} else {
|
|
7088
|
+
const buffered = [...sessionManager.getSessionEvents(event.sessionId)];
|
|
7089
|
+
if (buffered.length > 0) {
|
|
6364
7090
|
wsBridge.send(ws, {
|
|
6365
7091
|
type: "session_history",
|
|
6366
7092
|
sessionId: event.sessionId,
|
|
6367
|
-
events:
|
|
7093
|
+
events: buffered
|
|
6368
7094
|
});
|
|
6369
7095
|
}
|
|
6370
|
-
} else if (bufferedEvents.length > 0) {
|
|
6371
|
-
wsBridge.send(ws, {
|
|
6372
|
-
type: "session_history",
|
|
6373
|
-
sessionId: event.sessionId,
|
|
6374
|
-
events: bufferedEvents
|
|
6375
|
-
});
|
|
6376
7096
|
}
|
|
6377
7097
|
for (const req of approvalProxy.getPendingRequestsForSession(event.sessionId)) {
|
|
6378
7098
|
wsBridge.send(ws, { type: "approval_request", request: req });
|
|
@@ -6469,7 +7189,7 @@ async function start(opts = {}) {
|
|
|
6469
7189
|
if (!isStreaming) {
|
|
6470
7190
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
6471
7191
|
try {
|
|
6472
|
-
const fileStat = await (0,
|
|
7192
|
+
const fileStat = await (0, import_promises7.stat)(filePath);
|
|
6473
7193
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
6474
7194
|
} catch {
|
|
6475
7195
|
}
|
|
@@ -6553,6 +7273,9 @@ async function start(opts = {}) {
|
|
|
6553
7273
|
wsBridge.clearViewingSession(ws);
|
|
6554
7274
|
break;
|
|
6555
7275
|
}
|
|
7276
|
+
case "approval_displayed": {
|
|
7277
|
+
break;
|
|
7278
|
+
}
|
|
6556
7279
|
case "always_allow_tool": {
|
|
6557
7280
|
approvalProxy.addToClaudeSettings(event.projectPath, event.toolName);
|
|
6558
7281
|
break;
|
|
@@ -6750,14 +7473,12 @@ async function start(opts = {}) {
|
|
|
6750
7473
|
setTimeout(() => {
|
|
6751
7474
|
if (!approvalProxy.isPending(request.id)) return;
|
|
6752
7475
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6753
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6754
7476
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
6755
7477
|
notificationService.notifyApproval(request, pendingCount);
|
|
6756
|
-
},
|
|
7478
|
+
}, 3e3);
|
|
6757
7479
|
setTimeout(() => {
|
|
6758
7480
|
if (!approvalProxy.isPending(request.id)) return;
|
|
6759
7481
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6760
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6761
7482
|
console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
|
|
6762
7483
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
6763
7484
|
notificationService.notifyApproval(request, pendingCount);
|
|
@@ -6769,13 +7490,11 @@ async function start(opts = {}) {
|
|
|
6769
7490
|
setTimeout(() => {
|
|
6770
7491
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
6771
7492
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6772
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6773
7493
|
notificationService.notifyQuestion(request);
|
|
6774
7494
|
}, 5e3);
|
|
6775
7495
|
setTimeout(() => {
|
|
6776
7496
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
6777
7497
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
6778
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
6779
7498
|
console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
|
|
6780
7499
|
notificationService.notifyQuestion(request);
|
|
6781
7500
|
}, 6e4);
|
|
@@ -6964,7 +7683,7 @@ async function autoUpdateIfNeeded() {
|
|
|
6964
7683
|
console.log(` \u{1F4E6} ${t("startup.autoUpdating", { current: PKG_VERSION, latest })}`);
|
|
6965
7684
|
console.log();
|
|
6966
7685
|
try {
|
|
6967
|
-
(0,
|
|
7686
|
+
(0, import_node_child_process13.execFileSync)("npx", [`sessix-server@${latest}`], {
|
|
6968
7687
|
stdio: "inherit",
|
|
6969
7688
|
env: { ...process.env, __SESSIX_UPDATED: "1" }
|
|
6970
7689
|
});
|