sessix-server 0.4.2 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1541 -87
- package/dist/server.js +1535 -81
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -307,12 +307,12 @@ function t(key, params) {
|
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// src/server.ts
|
|
310
|
-
var
|
|
311
|
-
var
|
|
312
|
-
var
|
|
313
|
-
var
|
|
314
|
-
var
|
|
315
|
-
var
|
|
310
|
+
var import_uuid9 = require("uuid");
|
|
311
|
+
var import_promises7 = require("fs/promises");
|
|
312
|
+
var import_node_os9 = require("os");
|
|
313
|
+
var import_node_path9 = require("path");
|
|
314
|
+
var import_node_child_process12 = require("child_process");
|
|
315
|
+
var import_node_util3 = require("util");
|
|
316
316
|
|
|
317
317
|
// src/providers/ProcessProvider.ts
|
|
318
318
|
var import_child_process = require("child_process");
|
|
@@ -543,6 +543,77 @@ var ProcessProvider = class {
|
|
|
543
543
|
getActiveSessions() {
|
|
544
544
|
return Array.from(this.activeSessions.values()).map((entry) => entry.session);
|
|
545
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* 清理空闲进程
|
|
548
|
+
*
|
|
549
|
+
* 找出所有 status='idle' 且 lastActiveAt 距今超过 maxIdleMs 的活跃进程,
|
|
550
|
+
* kill 进程释放内存。entry 保留在 activeSessions 中,用户下次 sendMessage
|
|
551
|
+
* 走 slow path 自动 --resume 重启进程。
|
|
552
|
+
*
|
|
553
|
+
* @returns 被 sweep 的 sessionId 列表
|
|
554
|
+
*/
|
|
555
|
+
async sweepIdleProcesses(maxIdleMs) {
|
|
556
|
+
const now = Date.now();
|
|
557
|
+
const swept = [];
|
|
558
|
+
for (const [sessionId, entry] of this.activeSessions) {
|
|
559
|
+
if (entry.process.exitCode !== null || entry.process.signalCode !== null) continue;
|
|
560
|
+
if (entry.session.status !== "idle") continue;
|
|
561
|
+
if (now - entry.session.lastActiveAt < maxIdleMs) continue;
|
|
562
|
+
const idleMin = Math.round((now - entry.session.lastActiveAt) / 6e4);
|
|
563
|
+
console.log(`[ProcessProvider] sweeping idle process: ${sessionId} (idle ${idleMin}m)`);
|
|
564
|
+
try {
|
|
565
|
+
entry.process.stdin?.end();
|
|
566
|
+
} catch {
|
|
567
|
+
}
|
|
568
|
+
try {
|
|
569
|
+
await killProcessCrossPlatform(entry.process);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
console.error(`[ProcessProvider] sweep kill failed for ${sessionId}:`, err);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
swept.push(sessionId);
|
|
575
|
+
}
|
|
576
|
+
return swept;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* LRU 上限清理
|
|
580
|
+
*
|
|
581
|
+
* 当活跃进程数超过 maxAlive 时,按 lastActiveAt 升序(最久未用优先)kill
|
|
582
|
+
* 状态为 idle 的进程,直到活跃数回到上限以内。
|
|
583
|
+
* running / waiting_question 状态的进程永远不会被 kill。
|
|
584
|
+
*
|
|
585
|
+
* @returns 被 sweep 的 sessionId 列表
|
|
586
|
+
*/
|
|
587
|
+
async sweepLruProcesses(maxAlive) {
|
|
588
|
+
const swept = [];
|
|
589
|
+
if (maxAlive <= 0) return swept;
|
|
590
|
+
const aliveEntries = Array.from(this.activeSessions.entries()).filter(
|
|
591
|
+
([, e]) => e.process.exitCode === null && e.process.signalCode === null
|
|
592
|
+
);
|
|
593
|
+
if (aliveEntries.length <= maxAlive) return swept;
|
|
594
|
+
const idleSorted = aliveEntries.filter(([, e]) => e.session.status === "idle").sort((a, b) => a[1].session.lastActiveAt - b[1].session.lastActiveAt);
|
|
595
|
+
let aliveCount = aliveEntries.length;
|
|
596
|
+
for (const [sessionId, entry] of idleSorted) {
|
|
597
|
+
if (aliveCount <= maxAlive) break;
|
|
598
|
+
const idleMin = Math.round((Date.now() - entry.session.lastActiveAt) / 6e4);
|
|
599
|
+
console.log(`[ProcessProvider] LRU sweep: ${sessionId} (idle ${idleMin}m, alive=${aliveCount}/${maxAlive})`);
|
|
600
|
+
try {
|
|
601
|
+
entry.process.stdin?.end();
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
try {
|
|
605
|
+
await killProcessCrossPlatform(entry.process);
|
|
606
|
+
swept.push(sessionId);
|
|
607
|
+
aliveCount--;
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error(`[ProcessProvider] LRU kill failed for ${sessionId}:`, err);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (aliveCount > maxAlive) {
|
|
613
|
+
console.warn(`[ProcessProvider] LRU sweep: ${aliveCount} alive after sweep > limit ${maxAlive}; remaining are running/waiting`);
|
|
614
|
+
}
|
|
615
|
+
return swept;
|
|
616
|
+
}
|
|
546
617
|
// ============================================
|
|
547
618
|
// 私有方法
|
|
548
619
|
// ============================================
|
|
@@ -596,7 +667,24 @@ var ProcessProvider = class {
|
|
|
596
667
|
writeUserMessage(proc, message, sessionId, images) {
|
|
597
668
|
const content = [];
|
|
598
669
|
if (images?.length) {
|
|
599
|
-
|
|
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
|
+
}
|
|
600
688
|
content.push({
|
|
601
689
|
type: "image",
|
|
602
690
|
source: { type: "base64", media_type: img.media_type, data: img.data }
|
|
@@ -623,6 +711,14 @@ var ProcessProvider = class {
|
|
|
623
711
|
this.emitWriteError(sessionId, `Failed to send message: ${err.message}`);
|
|
624
712
|
}
|
|
625
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
|
+
}
|
|
626
722
|
}
|
|
627
723
|
/**
|
|
628
724
|
* 发出写入失败的合成错误事件
|
|
@@ -1770,6 +1866,20 @@ var SessionManager = class {
|
|
|
1770
1866
|
isBufferTruncated(sessionId) {
|
|
1771
1867
|
return this.bufferTruncated.has(sessionId);
|
|
1772
1868
|
}
|
|
1869
|
+
/**
|
|
1870
|
+
* 缩减指定会话的事件缓冲区到最后 N 条,并标记 truncated
|
|
1871
|
+
*
|
|
1872
|
+
* 用于空闲进程被 sweep 后释放内存:缓冲区只为新订阅者重放服务,
|
|
1873
|
+
* 进程已死的会话可以通过 JSONL 文件补全完整历史,不需要保留全部内存事件。
|
|
1874
|
+
* 设置 bufferTruncated 后,客户端 subscribe 收到 session_history 时会从 JSONL 补齐。
|
|
1875
|
+
*/
|
|
1876
|
+
shrinkSessionBuffer(sessionId, keepLast = 100) {
|
|
1877
|
+
const buffer = this.sessionEventBuffers.get(sessionId);
|
|
1878
|
+
if (!buffer || buffer.length <= keepLast) return;
|
|
1879
|
+
buffer.splice(0, buffer.length - keepLast);
|
|
1880
|
+
this.bufferTruncated.add(sessionId);
|
|
1881
|
+
console.log(`[SessionManager] Session ${sessionId}: buffer shrunk to ${keepLast}, marked truncated`);
|
|
1882
|
+
}
|
|
1773
1883
|
/**
|
|
1774
1884
|
* 获取会话的项目路径(用于截断时从 JSONL 补全历史)
|
|
1775
1885
|
*/
|
|
@@ -3630,6 +3740,8 @@ var HookInstaller = class {
|
|
|
3630
3740
|
|
|
3631
3741
|
// src/notification/NotificationService.ts
|
|
3632
3742
|
var import_node_path5 = require("path");
|
|
3743
|
+
var RECENT_ACTIVITY_MAX = 6;
|
|
3744
|
+
var ACTIVITY_PUSH_THROTTLE_MS = 2500;
|
|
3633
3745
|
var NotificationService = class {
|
|
3634
3746
|
constructor(sessionManager, expoChannel = null) {
|
|
3635
3747
|
this.sessionManager = sessionManager;
|
|
@@ -3648,6 +3760,14 @@ var NotificationService = class {
|
|
|
3648
3760
|
latestAssistantText = /* @__PURE__ */ new Map();
|
|
3649
3761
|
/** 获取全局待审批总数的回调(跨所有会话) */
|
|
3650
3762
|
globalPendingCountProvider = null;
|
|
3763
|
+
/** sessionId → 最近活动状态(用于 LA content push) */
|
|
3764
|
+
recentActivityState = /* @__PURE__ */ new Map();
|
|
3765
|
+
/** sessionId → 节流定时器(LA content push) */
|
|
3766
|
+
activityPushTimers = /* @__PURE__ */ new Map();
|
|
3767
|
+
/** 上次推送 LA content 的时间戳(用于节流;首次立即推送) */
|
|
3768
|
+
lastActivityPushAt = /* @__PURE__ */ new Map();
|
|
3769
|
+
/** 提供器:根据 sessionId 取该会话的待审批列表(由 server.ts 注入) */
|
|
3770
|
+
pendingApprovalsProvider = null;
|
|
3651
3771
|
/** 添加通知渠道(id 唯一,可用于后续动态开关) */
|
|
3652
3772
|
addChannel(id, channel, enabled = true) {
|
|
3653
3773
|
this.channelMap.set(id, { channel, enabled });
|
|
@@ -3675,11 +3795,24 @@ var NotificationService = class {
|
|
|
3675
3795
|
}
|
|
3676
3796
|
/** 注册 ActivityKit push token(由手机端启动 Live Activity 后上报) */
|
|
3677
3797
|
addActivityPushToken(sessionId, token) {
|
|
3678
|
-
this.activityPushChannel
|
|
3798
|
+
if (!this.activityPushChannel) {
|
|
3799
|
+
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`);
|
|
3800
|
+
return;
|
|
3801
|
+
}
|
|
3802
|
+
this.activityPushChannel.addToken(sessionId, token);
|
|
3803
|
+
console.log(`[NotificationService] \u2705 LA push token \u5DF2\u6CE8\u518C (session=${sessionId.slice(0, 8)}\u2026, token=${token.slice(0, 16)}\u2026)`);
|
|
3804
|
+
this.scheduleActivityPush(sessionId, true);
|
|
3679
3805
|
}
|
|
3680
3806
|
/** 移除 ActivityKit push token */
|
|
3681
3807
|
removeActivityPushToken(sessionId) {
|
|
3682
3808
|
this.activityPushChannel?.removeToken(sessionId);
|
|
3809
|
+
this.clearActivityPushTimer(sessionId);
|
|
3810
|
+
this.recentActivityState.delete(sessionId);
|
|
3811
|
+
this.lastActivityPushAt.delete(sessionId);
|
|
3812
|
+
}
|
|
3813
|
+
/** 注入"会话 → 待审批列表"提供器(server.ts 启动时调用) */
|
|
3814
|
+
setPendingApprovalsProvider(fn) {
|
|
3815
|
+
this.pendingApprovalsProvider = fn;
|
|
3683
3816
|
}
|
|
3684
3817
|
/** 设置全局待审批总数提供者 */
|
|
3685
3818
|
setGlobalPendingCountProvider(provider) {
|
|
@@ -3702,12 +3835,15 @@ var NotificationService = class {
|
|
|
3702
3835
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3703
3836
|
const dangerLevel2 = this.getDangerLevel(request.toolName);
|
|
3704
3837
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3838
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3839
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? "";
|
|
3705
3840
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3706
3841
|
request.sessionId,
|
|
3707
3842
|
{
|
|
3708
3843
|
status: "waitingApproval",
|
|
3709
3844
|
sessionTitle,
|
|
3710
|
-
latestMessage
|
|
3845
|
+
latestMessage,
|
|
3846
|
+
recentActivity,
|
|
3711
3847
|
approvalInfo: {
|
|
3712
3848
|
requestId: request.id,
|
|
3713
3849
|
toolName: request.toolName,
|
|
@@ -3720,6 +3856,7 @@ var NotificationService = class {
|
|
|
3720
3856
|
},
|
|
3721
3857
|
{ title, body }
|
|
3722
3858
|
);
|
|
3859
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3723
3860
|
return;
|
|
3724
3861
|
}
|
|
3725
3862
|
const dangerLevel = this.getDangerLevel(request.toolName);
|
|
@@ -3753,17 +3890,20 @@ var NotificationService = class {
|
|
|
3753
3890
|
const body = `\u2753 ${request.question.slice(0, 80)}`;
|
|
3754
3891
|
if (this.activityPushChannel?.hasToken(request.sessionId)) {
|
|
3755
3892
|
const isYoloMode = this.getYoloMode(request.sessionId);
|
|
3893
|
+
const recentActivity = this.getRecentActivity(request.sessionId);
|
|
3756
3894
|
this.activityPushChannel.updateActivityWithAlert(
|
|
3757
3895
|
request.sessionId,
|
|
3758
3896
|
{
|
|
3759
3897
|
status: "waitingApproval",
|
|
3760
3898
|
sessionTitle,
|
|
3761
3899
|
latestMessage: request.question.slice(0, 80),
|
|
3900
|
+
recentActivity,
|
|
3762
3901
|
isYoloMode,
|
|
3763
3902
|
updatedAt: Date.now()
|
|
3764
3903
|
},
|
|
3765
3904
|
{ title: sessionTitle, body }
|
|
3766
3905
|
);
|
|
3906
|
+
this.lastActivityPushAt.set(request.sessionId, Date.now());
|
|
3767
3907
|
return;
|
|
3768
3908
|
}
|
|
3769
3909
|
this.notify({
|
|
@@ -3797,6 +3937,10 @@ var NotificationService = class {
|
|
|
3797
3937
|
this.unsubscribe = null;
|
|
3798
3938
|
this.yoloModeState.clear();
|
|
3799
3939
|
this.latestAssistantText.clear();
|
|
3940
|
+
for (const timer of this.activityPushTimers.values()) clearTimeout(timer);
|
|
3941
|
+
this.activityPushTimers.clear();
|
|
3942
|
+
this.recentActivityState.clear();
|
|
3943
|
+
this.lastActivityPushAt.clear();
|
|
3800
3944
|
}
|
|
3801
3945
|
// ============================================
|
|
3802
3946
|
// 内部方法
|
|
@@ -3805,15 +3949,20 @@ var NotificationService = class {
|
|
|
3805
3949
|
switch (event.type) {
|
|
3806
3950
|
case "claude_event": {
|
|
3807
3951
|
this.trackAssistantText(event.sessionId, event.event);
|
|
3952
|
+
this.updateRecentActivity(event.sessionId, event.event);
|
|
3953
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3808
3954
|
break;
|
|
3809
3955
|
}
|
|
3810
3956
|
case "claude_events": {
|
|
3811
3957
|
for (const e of event.events) {
|
|
3812
3958
|
this.trackAssistantText(event.sessionId, e);
|
|
3959
|
+
this.updateRecentActivity(event.sessionId, e);
|
|
3813
3960
|
}
|
|
3961
|
+
this.scheduleActivityPush(event.sessionId);
|
|
3814
3962
|
break;
|
|
3815
3963
|
}
|
|
3816
3964
|
case "status_change": {
|
|
3965
|
+
this.clearActivityPushTimer(event.sessionId);
|
|
3817
3966
|
if (event.status === "idle") {
|
|
3818
3967
|
const sessionTitle = this.getSessionTitle(event.sessionId);
|
|
3819
3968
|
const latestMsg = this.latestAssistantText.get(event.sessionId);
|
|
@@ -3824,9 +3973,12 @@ var NotificationService = class {
|
|
|
3824
3973
|
status: "idle",
|
|
3825
3974
|
sessionTitle,
|
|
3826
3975
|
latestMessage: body,
|
|
3976
|
+
recentActivity: this.getRecentActivity(event.sessionId),
|
|
3827
3977
|
isYoloMode,
|
|
3828
3978
|
updatedAt: Date.now()
|
|
3829
3979
|
});
|
|
3980
|
+
this.recentActivityState.delete(event.sessionId);
|
|
3981
|
+
this.lastActivityPushAt.delete(event.sessionId);
|
|
3830
3982
|
} else {
|
|
3831
3983
|
this.notify({
|
|
3832
3984
|
title: sessionTitle,
|
|
@@ -3846,9 +3998,12 @@ var NotificationService = class {
|
|
|
3846
3998
|
status: "error",
|
|
3847
3999
|
sessionTitle,
|
|
3848
4000
|
latestMessage: body,
|
|
4001
|
+
recentActivity: this.getRecentActivity(event.sessionId),
|
|
3849
4002
|
isYoloMode,
|
|
3850
4003
|
updatedAt: Date.now()
|
|
3851
4004
|
});
|
|
4005
|
+
this.recentActivityState.delete(event.sessionId);
|
|
4006
|
+
this.lastActivityPushAt.delete(event.sessionId);
|
|
3852
4007
|
} else {
|
|
3853
4008
|
this.notify({
|
|
3854
4009
|
title: sessionTitle,
|
|
@@ -3890,6 +4045,229 @@ var NotificationService = class {
|
|
|
3890
4045
|
getYoloMode(sessionId) {
|
|
3891
4046
|
return this.yoloModeState.get(sessionId) ?? false;
|
|
3892
4047
|
}
|
|
4048
|
+
// ============================================
|
|
4049
|
+
// Live Activity 内容推送(后台 LA 实时刷新)
|
|
4050
|
+
// ============================================
|
|
4051
|
+
/**
|
|
4052
|
+
* 把一个 ClaudeStreamEvent 折算到 recentActivity 列表里。
|
|
4053
|
+
* 同一 message.id 内多次 assistant 事件视为流式更新,整段重建 currentEntries;
|
|
4054
|
+
* 切换 message.id 视为新 turn,旧条目沉淀到 history。
|
|
4055
|
+
*/
|
|
4056
|
+
updateRecentActivity(sessionId, event) {
|
|
4057
|
+
if (event.type === "result") {
|
|
4058
|
+
const state2 = this.recentActivityState.get(sessionId);
|
|
4059
|
+
if (state2 && state2.currentEntries.length > 0) {
|
|
4060
|
+
state2.history.push(...state2.currentEntries);
|
|
4061
|
+
while (state2.history.length > RECENT_ACTIVITY_MAX) state2.history.shift();
|
|
4062
|
+
state2.currentEntries = [];
|
|
4063
|
+
state2.currentMessageId = null;
|
|
4064
|
+
}
|
|
4065
|
+
return;
|
|
4066
|
+
}
|
|
4067
|
+
if (event.type !== "assistant") return;
|
|
4068
|
+
const msg = event.message;
|
|
4069
|
+
if (!Array.isArray(msg.content)) return;
|
|
4070
|
+
let state = this.recentActivityState.get(sessionId);
|
|
4071
|
+
if (!state) {
|
|
4072
|
+
state = { history: [], currentMessageId: null, currentEntries: [] };
|
|
4073
|
+
this.recentActivityState.set(sessionId, state);
|
|
4074
|
+
}
|
|
4075
|
+
if (state.currentMessageId !== msg.id) {
|
|
4076
|
+
if (state.currentEntries.length > 0) {
|
|
4077
|
+
state.history.push(...state.currentEntries);
|
|
4078
|
+
while (state.history.length > RECENT_ACTIVITY_MAX) state.history.shift();
|
|
4079
|
+
}
|
|
4080
|
+
state.currentEntries = [];
|
|
4081
|
+
state.currentMessageId = msg.id;
|
|
4082
|
+
}
|
|
4083
|
+
const next = [];
|
|
4084
|
+
for (const block of msg.content) {
|
|
4085
|
+
if (block.type === "text") {
|
|
4086
|
+
const line = this.summarizeText(block.text);
|
|
4087
|
+
if (line.length >= 4) next.push(line);
|
|
4088
|
+
} else if (block.type === "tool_use") {
|
|
4089
|
+
const line = this.summarizeToolCall(block.name, block.input ?? {});
|
|
4090
|
+
if (line) next.push(line);
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
state.currentEntries = next;
|
|
4094
|
+
}
|
|
4095
|
+
/** 取该会话当前的 recentActivity(history + currentEntries),保留末尾 N 条 */
|
|
4096
|
+
getRecentActivity(sessionId) {
|
|
4097
|
+
const state = this.recentActivityState.get(sessionId);
|
|
4098
|
+
if (!state) return [];
|
|
4099
|
+
const combined = [...state.history, ...state.currentEntries];
|
|
4100
|
+
return combined.slice(-RECENT_ACTIVITY_MAX);
|
|
4101
|
+
}
|
|
4102
|
+
/** 节流调度 LA content push;首次立即推,后续合并到 throttle window 末尾 */
|
|
4103
|
+
scheduleActivityPush(sessionId, force = false) {
|
|
4104
|
+
if (!this.activityPushChannel?.hasToken(sessionId)) return;
|
|
4105
|
+
const now = Date.now();
|
|
4106
|
+
const last = this.lastActivityPushAt.get(sessionId) ?? 0;
|
|
4107
|
+
const elapsed = now - last;
|
|
4108
|
+
if (force || elapsed >= ACTIVITY_PUSH_THROTTLE_MS) {
|
|
4109
|
+
this.clearActivityPushTimer(sessionId);
|
|
4110
|
+
this.flushActivityPush(sessionId);
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
if (this.activityPushTimers.has(sessionId)) return;
|
|
4114
|
+
const wait = ACTIVITY_PUSH_THROTTLE_MS - elapsed;
|
|
4115
|
+
this.activityPushTimers.set(
|
|
4116
|
+
sessionId,
|
|
4117
|
+
setTimeout(() => {
|
|
4118
|
+
this.activityPushTimers.delete(sessionId);
|
|
4119
|
+
this.flushActivityPush(sessionId);
|
|
4120
|
+
}, wait)
|
|
4121
|
+
);
|
|
4122
|
+
}
|
|
4123
|
+
clearActivityPushTimer(sessionId) {
|
|
4124
|
+
const timer = this.activityPushTimers.get(sessionId);
|
|
4125
|
+
if (timer) {
|
|
4126
|
+
clearTimeout(timer);
|
|
4127
|
+
this.activityPushTimers.delete(sessionId);
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
/** 真正发送一次 LA content push(无 alert) */
|
|
4131
|
+
flushActivityPush(sessionId) {
|
|
4132
|
+
const channel = this.activityPushChannel;
|
|
4133
|
+
if (!channel?.hasToken(sessionId)) return;
|
|
4134
|
+
const session = this.sessionManager.getActiveSessions().find((s) => s.id === sessionId);
|
|
4135
|
+
if (!session) return;
|
|
4136
|
+
const recentActivity = this.getRecentActivity(sessionId);
|
|
4137
|
+
const latestMessage = recentActivity[recentActivity.length - 1] ?? this.latestAssistantText.get(sessionId) ?? "";
|
|
4138
|
+
const sessionTitle = this.getSessionTitle(sessionId);
|
|
4139
|
+
const isYoloMode = this.getYoloMode(sessionId);
|
|
4140
|
+
const pendingApprovals = this.pendingApprovalsProvider?.(sessionId) ?? [];
|
|
4141
|
+
const latestApproval = pendingApprovals[pendingApprovals.length - 1];
|
|
4142
|
+
const status = latestApproval ? "waitingApproval" : this.mapSessionStatus(session.status);
|
|
4143
|
+
const contentState = {
|
|
4144
|
+
status,
|
|
4145
|
+
sessionTitle,
|
|
4146
|
+
latestMessage,
|
|
4147
|
+
recentActivity,
|
|
4148
|
+
isYoloMode,
|
|
4149
|
+
updatedAt: Date.now()
|
|
4150
|
+
};
|
|
4151
|
+
if (latestApproval) {
|
|
4152
|
+
contentState.approvalInfo = {
|
|
4153
|
+
requestId: latestApproval.id,
|
|
4154
|
+
toolName: latestApproval.toolName,
|
|
4155
|
+
description: String(latestApproval.description ?? "").slice(0, 80),
|
|
4156
|
+
dangerLevel: this.getDangerLevel(latestApproval.toolName),
|
|
4157
|
+
pendingCount: pendingApprovals.length
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
if (session.stats) {
|
|
4161
|
+
contentState.stats = {
|
|
4162
|
+
totalInputTokens: session.stats.totalInputTokens,
|
|
4163
|
+
totalOutputTokens: session.stats.totalOutputTokens,
|
|
4164
|
+
totalCostUsd: session.stats.totalCostUsd
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
this.lastActivityPushAt.set(sessionId, Date.now());
|
|
4168
|
+
const lineCount = recentActivity.length;
|
|
4169
|
+
channel.updateActivity(sessionId, contentState).then(() => {
|
|
4170
|
+
console.log(`[NotificationService] \u{1F4E1} LA push \u2713 session=${sessionId.slice(0, 8)}\u2026 status=${status} lines=${lineCount}`);
|
|
4171
|
+
}).catch((err) => {
|
|
4172
|
+
console.warn(`[NotificationService] \u{1F4E1} LA push \u2717 session=${sessionId.slice(0, 8)}\u2026:`, err instanceof Error ? err.message : err);
|
|
4173
|
+
});
|
|
4174
|
+
}
|
|
4175
|
+
/** SessionStatus → LiveActivity status 字符串映射(与客户端 mapStatus 一致) */
|
|
4176
|
+
mapSessionStatus(status) {
|
|
4177
|
+
switch (status) {
|
|
4178
|
+
case "running":
|
|
4179
|
+
return "running";
|
|
4180
|
+
case "waiting_approval":
|
|
4181
|
+
return "waitingApproval";
|
|
4182
|
+
case "waiting_question":
|
|
4183
|
+
return "waitingQuestion";
|
|
4184
|
+
case "idle":
|
|
4185
|
+
return "completed";
|
|
4186
|
+
case "completed":
|
|
4187
|
+
return "completed";
|
|
4188
|
+
case "error":
|
|
4189
|
+
return "error";
|
|
4190
|
+
default:
|
|
4191
|
+
return "idle";
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
/** 文本块清洗:去多余空白 + 截断到 70 字符 */
|
|
4195
|
+
summarizeText(raw) {
|
|
4196
|
+
if (typeof raw !== "string") return "";
|
|
4197
|
+
const cleaned = raw.replace(/\s+/g, " ").trim();
|
|
4198
|
+
return cleaned.length > 70 ? cleaned.slice(0, 70) + "\u2026" : cleaned;
|
|
4199
|
+
}
|
|
4200
|
+
/** 工具调用摘要(与客户端 summarizeToolCall 行为对齐,简化版只输出中文) */
|
|
4201
|
+
summarizeToolCall(name, input) {
|
|
4202
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
4203
|
+
const baseName = (p) => {
|
|
4204
|
+
const cleaned = p.split(/[?#]/)[0];
|
|
4205
|
+
const parts = cleaned.split("/");
|
|
4206
|
+
return parts[parts.length - 1] || cleaned;
|
|
4207
|
+
};
|
|
4208
|
+
const trunc = (s, n) => s.length > n ? s.slice(0, n) + "\u2026" : s;
|
|
4209
|
+
switch (name) {
|
|
4210
|
+
case "Bash": {
|
|
4211
|
+
const cmd = str(input.command).split("\n")[0];
|
|
4212
|
+
return cmd ? `\u8FD0\u884C: ${trunc(cmd, 60)}` : "\u6267\u884C\u547D\u4EE4";
|
|
4213
|
+
}
|
|
4214
|
+
case "Edit": {
|
|
4215
|
+
const fp = baseName(str(input.file_path));
|
|
4216
|
+
return fp ? `\u7F16\u8F91 ${fp}` : "\u7F16\u8F91\u6587\u4EF6";
|
|
4217
|
+
}
|
|
4218
|
+
case "MultiEdit": {
|
|
4219
|
+
const fp = baseName(str(input.file_path));
|
|
4220
|
+
return fp ? `\u6279\u91CF\u7F16\u8F91 ${fp}` : "\u6279\u91CF\u7F16\u8F91\u6587\u4EF6";
|
|
4221
|
+
}
|
|
4222
|
+
case "Write": {
|
|
4223
|
+
const fp = baseName(str(input.file_path));
|
|
4224
|
+
return fp ? `\u5199\u5165 ${fp}` : "\u5199\u5165\u6587\u4EF6";
|
|
4225
|
+
}
|
|
4226
|
+
case "Read":
|
|
4227
|
+
case "NotebookEdit": {
|
|
4228
|
+
const fp = baseName(str(input.file_path) || str(input.notebook_path));
|
|
4229
|
+
return fp ? `\u9605\u8BFB ${fp}` : "\u9605\u8BFB\u6587\u4EF6";
|
|
4230
|
+
}
|
|
4231
|
+
case "Grep": {
|
|
4232
|
+
const p = str(input.pattern);
|
|
4233
|
+
return p ? `\u641C\u7D22: ${trunc(p, 50)}` : "\u641C\u7D22\u4EE3\u7801";
|
|
4234
|
+
}
|
|
4235
|
+
case "Glob": {
|
|
4236
|
+
const p = str(input.pattern);
|
|
4237
|
+
return p ? `\u67E5\u627E: ${trunc(p, 50)}` : "\u67E5\u627E\u6587\u4EF6";
|
|
4238
|
+
}
|
|
4239
|
+
case "WebFetch": {
|
|
4240
|
+
const url = str(input.url);
|
|
4241
|
+
let host = url;
|
|
4242
|
+
try {
|
|
4243
|
+
host = new URL(url).hostname;
|
|
4244
|
+
} catch {
|
|
4245
|
+
}
|
|
4246
|
+
return host ? `\u8BF7\u6C42 ${trunc(host, 50)}` : "\u8BF7\u6C42\u7F51\u9875";
|
|
4247
|
+
}
|
|
4248
|
+
case "WebSearch": {
|
|
4249
|
+
const q = str(input.query);
|
|
4250
|
+
return q ? `\u641C\u7D22\u7F51\u9875: ${trunc(q, 50)}` : "\u641C\u7D22\u7F51\u9875";
|
|
4251
|
+
}
|
|
4252
|
+
case "TodoWrite":
|
|
4253
|
+
return "\u66F4\u65B0\u4EFB\u52A1\u6E05\u5355";
|
|
4254
|
+
case "Task":
|
|
4255
|
+
case "Agent": {
|
|
4256
|
+
const desc = str(input.description) || str(input.subagent_type);
|
|
4257
|
+
return desc ? `\u6D3E\u53D1\u4EFB\u52A1: ${trunc(desc, 50)}` : "\u6D3E\u53D1\u5B50\u4EFB\u52A1";
|
|
4258
|
+
}
|
|
4259
|
+
case "ExitPlanMode":
|
|
4260
|
+
return "\u63D0\u4EA4\u8BA1\u5212";
|
|
4261
|
+
case "Skill": {
|
|
4262
|
+
const skill = str(input.skill);
|
|
4263
|
+
return skill ? `\u8C03\u7528\u6280\u80FD: ${trunc(skill, 40)}` : "\u8C03\u7528\u6280\u80FD";
|
|
4264
|
+
}
|
|
4265
|
+
default: {
|
|
4266
|
+
const summary = trunc(JSON.stringify(input), 50);
|
|
4267
|
+
return name ? `${name}: ${summary}` : summary;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
3893
4271
|
};
|
|
3894
4272
|
|
|
3895
4273
|
// src/notification/DesktopNotificationChannel.ts
|
|
@@ -4003,62 +4381,82 @@ var ExpoNotificationChannel = class {
|
|
|
4003
4381
|
var http2 = __toESM(require("http2"));
|
|
4004
4382
|
var fs2 = __toESM(require("fs"));
|
|
4005
4383
|
var crypto = __toESM(require("crypto"));
|
|
4384
|
+
var APNS_HOSTS = {
|
|
4385
|
+
production: "api.push.apple.com",
|
|
4386
|
+
sandbox: "api.sandbox.push.apple.com"
|
|
4387
|
+
};
|
|
4006
4388
|
var ActivityPushChannel = class {
|
|
4007
4389
|
/** sessionId -> activityPushToken */
|
|
4008
4390
|
tokens = /* @__PURE__ */ new Map();
|
|
4391
|
+
/**
|
|
4392
|
+
* 每个 token 已确认工作的 APNs 环境。
|
|
4393
|
+
* Debug build (aps-environment=development) 的 token 仅在 sandbox 端有效;
|
|
4394
|
+
* Release build (aps-environment=production) 的 token 仅在 production 端有效。
|
|
4395
|
+
* 同时维护两个连接 + 探测机制,避免环境配错时静默失败。
|
|
4396
|
+
*/
|
|
4397
|
+
tokenEnv = /* @__PURE__ */ new Map();
|
|
4398
|
+
/** 首次探测顺序(来自配置 hint),未配置则先试 sandbox(开发场景占多数) */
|
|
4399
|
+
probeOrder;
|
|
4009
4400
|
teamId;
|
|
4010
4401
|
keyId;
|
|
4011
4402
|
authKey;
|
|
4012
|
-
apnsHost;
|
|
4013
4403
|
/** 缓存的 JWT token + 过期时间 */
|
|
4014
4404
|
cachedJwt = null;
|
|
4015
|
-
/**
|
|
4016
|
-
|
|
4405
|
+
/** 每个环境一条 HTTP/2 长连接 */
|
|
4406
|
+
http2Clients = {};
|
|
4017
4407
|
constructor(config) {
|
|
4018
4408
|
this.teamId = config.teamId;
|
|
4019
4409
|
this.keyId = config.keyId;
|
|
4020
4410
|
this.authKey = fs2.readFileSync(config.authKeyPath, "utf-8");
|
|
4021
|
-
this.
|
|
4022
|
-
console.log(`[ActivityPushChannel] Initialized (${
|
|
4023
|
-
}
|
|
4024
|
-
/**
|
|
4025
|
-
getHttp2Client() {
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4411
|
+
this.probeOrder = config.sandbox === false ? ["production", "sandbox"] : ["sandbox", "production"];
|
|
4412
|
+
console.log(`[ActivityPushChannel] Initialized (probe order: ${this.probeOrder.join(" \u2192 ")})`);
|
|
4413
|
+
}
|
|
4414
|
+
/** 获取或新建指定环境的 HTTP/2 长连接 */
|
|
4415
|
+
getHttp2Client(env) {
|
|
4416
|
+
const existing = this.http2Clients[env];
|
|
4417
|
+
if (existing && !existing.destroyed && !existing.closed) {
|
|
4418
|
+
return existing;
|
|
4419
|
+
}
|
|
4420
|
+
const client = http2.connect(`https://${APNS_HOSTS[env]}`);
|
|
4421
|
+
client.on("error", (err) => {
|
|
4422
|
+
console.warn(`[ActivityPushChannel] HTTP/2 (${env}) error, will reconnect on next request:`, err.message);
|
|
4423
|
+
client.destroy();
|
|
4424
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4034
4425
|
});
|
|
4035
|
-
|
|
4036
|
-
this.
|
|
4426
|
+
client.on("close", () => {
|
|
4427
|
+
if (this.http2Clients[env] === client) delete this.http2Clients[env];
|
|
4037
4428
|
});
|
|
4038
|
-
|
|
4429
|
+
this.http2Clients[env] = client;
|
|
4430
|
+
return client;
|
|
4039
4431
|
}
|
|
4040
4432
|
/** 注册 Activity push token */
|
|
4041
4433
|
addToken(sessionId, token) {
|
|
4434
|
+
const existed = this.tokens.has(sessionId);
|
|
4042
4435
|
this.tokens.set(sessionId, token);
|
|
4043
|
-
console.log(`[ActivityPushChannel] Token registered: session=${sessionId}`);
|
|
4436
|
+
console.log(`[ActivityPushChannel] Token ${existed ? "updated" : "registered"}: session=${sessionId.slice(0, 8)}\u2026 token=${token.slice(0, 16)}\u2026`);
|
|
4044
4437
|
}
|
|
4045
4438
|
/** 移除 Activity push token */
|
|
4046
4439
|
removeToken(sessionId) {
|
|
4440
|
+
const tok = this.tokens.get(sessionId);
|
|
4047
4441
|
this.tokens.delete(sessionId);
|
|
4442
|
+
if (tok) this.tokenEnv.delete(tok);
|
|
4048
4443
|
}
|
|
4049
|
-
/** 发送 content-state 更新到指定会话的 Live Activity */
|
|
4444
|
+
/** 发送 content-state 更新到指定会话的 Live Activity(纯内容刷新,不响通知) */
|
|
4050
4445
|
async updateActivity(sessionId, contentState) {
|
|
4051
4446
|
const token = this.tokens.get(sessionId);
|
|
4052
4447
|
if (!token) return;
|
|
4448
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4053
4449
|
const payload = {
|
|
4054
4450
|
aps: {
|
|
4055
|
-
timestamp:
|
|
4451
|
+
timestamp: now,
|
|
4056
4452
|
event: "update",
|
|
4057
|
-
"content-state": contentState
|
|
4453
|
+
"content-state": contentState,
|
|
4454
|
+
// 2 分钟没新内容就让 LA 进入 stale 状态(系统自动灰化),避免显示陈旧数据
|
|
4455
|
+
"stale-date": now + 120
|
|
4058
4456
|
}
|
|
4059
4457
|
};
|
|
4060
4458
|
try {
|
|
4061
|
-
await this.sendToAPNs(token, payload);
|
|
4459
|
+
await this.sendToAPNs(token, payload, { priority: "5" });
|
|
4062
4460
|
} catch (err) {
|
|
4063
4461
|
console.warn(`[ActivityPushChannel] Update failed session=${sessionId}:`, err);
|
|
4064
4462
|
}
|
|
@@ -4067,17 +4465,19 @@ var ActivityPushChannel = class {
|
|
|
4067
4465
|
async updateActivityWithAlert(sessionId, contentState, alert) {
|
|
4068
4466
|
const token = this.tokens.get(sessionId);
|
|
4069
4467
|
if (!token) return;
|
|
4468
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4070
4469
|
const payload = {
|
|
4071
4470
|
aps: {
|
|
4072
|
-
timestamp:
|
|
4471
|
+
timestamp: now,
|
|
4073
4472
|
event: "update",
|
|
4074
4473
|
"content-state": contentState,
|
|
4474
|
+
"stale-date": now + 120,
|
|
4075
4475
|
alert,
|
|
4076
4476
|
sound: "default"
|
|
4077
4477
|
}
|
|
4078
4478
|
};
|
|
4079
4479
|
try {
|
|
4080
|
-
await this.sendToAPNs(token, payload);
|
|
4480
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4081
4481
|
} catch (err) {
|
|
4082
4482
|
console.warn(`[ActivityPushChannel] Alert update failed session=${sessionId}:`, err);
|
|
4083
4483
|
}
|
|
@@ -4086,15 +4486,16 @@ var ActivityPushChannel = class {
|
|
|
4086
4486
|
async endActivity(sessionId, contentState) {
|
|
4087
4487
|
const token = this.tokens.get(sessionId);
|
|
4088
4488
|
if (!token) return;
|
|
4489
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
4089
4490
|
const payload = {
|
|
4090
4491
|
aps: {
|
|
4091
|
-
timestamp:
|
|
4492
|
+
timestamp: now,
|
|
4092
4493
|
event: "end",
|
|
4093
4494
|
"content-state": contentState
|
|
4094
4495
|
}
|
|
4095
4496
|
};
|
|
4096
4497
|
try {
|
|
4097
|
-
await this.sendToAPNs(token, payload);
|
|
4498
|
+
await this.sendToAPNs(token, payload, { priority: "10" });
|
|
4098
4499
|
} catch (err) {
|
|
4099
4500
|
console.warn(`[ActivityPushChannel] End failed session=${sessionId}:`, err);
|
|
4100
4501
|
}
|
|
@@ -4104,15 +4505,44 @@ var ActivityPushChannel = class {
|
|
|
4104
4505
|
hasToken(sessionId) {
|
|
4105
4506
|
return this.tokens.has(sessionId);
|
|
4106
4507
|
}
|
|
4107
|
-
/**
|
|
4108
|
-
|
|
4508
|
+
/**
|
|
4509
|
+
* 发送 APNs,自动处理环境探测。
|
|
4510
|
+
* 对每个 token:先用已确认的环境(如有);否则按 probeOrder 顺序探测,
|
|
4511
|
+
* 收到 BadDeviceToken 自动切到另一个环境,并把成功的环境绑定到该 token。
|
|
4512
|
+
*/
|
|
4513
|
+
async sendToAPNs(deviceToken, payload, opts = {}) {
|
|
4514
|
+
const known = this.tokenEnv.get(deviceToken);
|
|
4515
|
+
if (known) {
|
|
4516
|
+
return this.sendToAPNsOnce(deviceToken, payload, opts, known);
|
|
4517
|
+
}
|
|
4518
|
+
let lastErr = null;
|
|
4519
|
+
for (const env of this.probeOrder) {
|
|
4520
|
+
try {
|
|
4521
|
+
await this.sendToAPNsOnce(deviceToken, payload, opts, env);
|
|
4522
|
+
this.tokenEnv.set(deviceToken, env);
|
|
4523
|
+
if (env !== this.probeOrder[0]) {
|
|
4524
|
+
console.log(`[ActivityPushChannel] Token bound to ${env} after probe (token=${deviceToken.slice(0, 16)}\u2026)`);
|
|
4525
|
+
}
|
|
4526
|
+
return;
|
|
4527
|
+
} catch (err) {
|
|
4528
|
+
lastErr = err;
|
|
4529
|
+
if (!isBadDeviceTokenError(err)) {
|
|
4530
|
+
throw err;
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
throw lastErr ?? new Error("APNs send failed: all environments rejected token");
|
|
4535
|
+
}
|
|
4536
|
+
/** 单次 APNs HTTP/2 请求(内部 helper,不做探测) */
|
|
4537
|
+
async sendToAPNsOnce(deviceToken, payload, opts, env) {
|
|
4109
4538
|
const topic = "com.kachun.sessix.push-type.liveactivity";
|
|
4110
4539
|
const jwt = this.getJWT();
|
|
4111
4540
|
const payloadStr = JSON.stringify(payload);
|
|
4541
|
+
const priority = opts.priority ?? "10";
|
|
4112
4542
|
return new Promise((resolve, reject) => {
|
|
4113
4543
|
let client;
|
|
4114
4544
|
try {
|
|
4115
|
-
client = this.getHttp2Client();
|
|
4545
|
+
client = this.getHttp2Client(env);
|
|
4116
4546
|
} catch (err) {
|
|
4117
4547
|
return reject(err);
|
|
4118
4548
|
}
|
|
@@ -4122,7 +4552,7 @@ var ActivityPushChannel = class {
|
|
|
4122
4552
|
"authorization": `bearer ${jwt}`,
|
|
4123
4553
|
"apns-topic": topic,
|
|
4124
4554
|
"apns-push-type": "liveactivity",
|
|
4125
|
-
"apns-priority":
|
|
4555
|
+
"apns-priority": priority,
|
|
4126
4556
|
"apns-expiration": String(Math.floor(Date.now() / 1e3) + 30),
|
|
4127
4557
|
"content-type": "application/json",
|
|
4128
4558
|
"content-length": Buffer.byteLength(payloadStr)
|
|
@@ -4140,10 +4570,11 @@ var ActivityPushChannel = class {
|
|
|
4140
4570
|
resolve();
|
|
4141
4571
|
} else {
|
|
4142
4572
|
if (statusCode === 0) {
|
|
4143
|
-
this.
|
|
4144
|
-
|
|
4573
|
+
const c = this.http2Clients[env];
|
|
4574
|
+
c?.destroy();
|
|
4575
|
+
delete this.http2Clients[env];
|
|
4145
4576
|
}
|
|
4146
|
-
reject(new
|
|
4577
|
+
reject(new ApnsError(statusCode, responseData));
|
|
4147
4578
|
}
|
|
4148
4579
|
});
|
|
4149
4580
|
req.on("error", (err) => {
|
|
@@ -4176,6 +4607,24 @@ var ActivityPushChannel = class {
|
|
|
4176
4607
|
return token;
|
|
4177
4608
|
}
|
|
4178
4609
|
};
|
|
4610
|
+
var ApnsError = class extends Error {
|
|
4611
|
+
constructor(statusCode, responseBody) {
|
|
4612
|
+
super(`APNs returned ${statusCode}: ${responseBody}`);
|
|
4613
|
+
this.statusCode = statusCode;
|
|
4614
|
+
this.responseBody = responseBody;
|
|
4615
|
+
this.name = "ApnsError";
|
|
4616
|
+
}
|
|
4617
|
+
};
|
|
4618
|
+
function isBadDeviceTokenError(err) {
|
|
4619
|
+
if (!(err instanceof ApnsError)) return false;
|
|
4620
|
+
if (err.statusCode !== 400 && err.statusCode !== 410) return false;
|
|
4621
|
+
try {
|
|
4622
|
+
const parsed = JSON.parse(err.responseBody);
|
|
4623
|
+
return parsed.reason === "BadDeviceToken" || parsed.reason === "Unregistered";
|
|
4624
|
+
} catch {
|
|
4625
|
+
return false;
|
|
4626
|
+
}
|
|
4627
|
+
}
|
|
4179
4628
|
|
|
4180
4629
|
// src/session/ProjectReader.ts
|
|
4181
4630
|
var import_promises3 = require("fs/promises");
|
|
@@ -4575,6 +5024,45 @@ var PairingManager = class {
|
|
|
4575
5024
|
}
|
|
4576
5025
|
};
|
|
4577
5026
|
|
|
5027
|
+
// src/utils/shellPath.ts
|
|
5028
|
+
var import_node_child_process7 = require("child_process");
|
|
5029
|
+
var fixed = false;
|
|
5030
|
+
function fixShellPath() {
|
|
5031
|
+
if (fixed || isWindows) {
|
|
5032
|
+
fixed = true;
|
|
5033
|
+
return;
|
|
5034
|
+
}
|
|
5035
|
+
fixed = true;
|
|
5036
|
+
const shell = process.env.SHELL || "/bin/zsh";
|
|
5037
|
+
const isFish = /\/fish$/.test(shell);
|
|
5038
|
+
const printPathCmd = isFish ? "string join : $PATH" : 'printf "%s" "$PATH"';
|
|
5039
|
+
let raw;
|
|
5040
|
+
try {
|
|
5041
|
+
raw = (0, import_node_child_process7.execFileSync)(shell, ["-l", "-c", printPathCmd], {
|
|
5042
|
+
encoding: "utf8",
|
|
5043
|
+
timeout: 3e3,
|
|
5044
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5045
|
+
});
|
|
5046
|
+
} catch (err) {
|
|
5047
|
+
console.warn("[fixShellPath] failed to read login shell PATH:", err);
|
|
5048
|
+
return;
|
|
5049
|
+
}
|
|
5050
|
+
const fromShell = raw.trim();
|
|
5051
|
+
if (!fromShell) return;
|
|
5052
|
+
process.env.PATH = mergePath(fromShell, process.env.PATH || "");
|
|
5053
|
+
}
|
|
5054
|
+
function mergePath(primary, secondary) {
|
|
5055
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5056
|
+
const out = [];
|
|
5057
|
+
for (const seg of primary.split(":").concat(secondary.split(":"))) {
|
|
5058
|
+
if (!seg) continue;
|
|
5059
|
+
if (seen.has(seg)) continue;
|
|
5060
|
+
seen.add(seg);
|
|
5061
|
+
out.push(seg);
|
|
5062
|
+
}
|
|
5063
|
+
return out.join(":");
|
|
5064
|
+
}
|
|
5065
|
+
|
|
4578
5066
|
// src/auth/AuthManager.ts
|
|
4579
5067
|
var import_child_process3 = require("child_process");
|
|
4580
5068
|
var import_child_process4 = require("child_process");
|
|
@@ -4694,12 +5182,12 @@ var AuthManager = class extends import_events3.EventEmitter {
|
|
|
4694
5182
|
};
|
|
4695
5183
|
|
|
4696
5184
|
// src/server.ts
|
|
4697
|
-
var
|
|
5185
|
+
var import_promises8 = require("fs/promises");
|
|
4698
5186
|
|
|
4699
5187
|
// src/terminal/TerminalExecutor.ts
|
|
4700
|
-
var
|
|
5188
|
+
var import_node_child_process8 = require("child_process");
|
|
4701
5189
|
var import_uuid5 = require("uuid");
|
|
4702
|
-
var EXEC_TIMEOUT_MS =
|
|
5190
|
+
var EXEC_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4703
5191
|
var TerminalExecutor = class {
|
|
4704
5192
|
processes = /* @__PURE__ */ new Map();
|
|
4705
5193
|
eventCallbacks = [];
|
|
@@ -4721,9 +5209,9 @@ var TerminalExecutor = class {
|
|
|
4721
5209
|
}
|
|
4722
5210
|
exec(sessionId, command, cwd) {
|
|
4723
5211
|
const execId = (0, import_uuid5.v4)();
|
|
4724
|
-
const shell = isWindows ? "powershell" : "
|
|
4725
|
-
const args = isWindows ? ["-Command", command] : ["-c", command];
|
|
4726
|
-
const proc = (0,
|
|
5212
|
+
const shell = isWindows ? "powershell" : process.env.SHELL || "/bin/zsh";
|
|
5213
|
+
const args = isWindows ? ["-Command", command] : ["-l", "-c", command];
|
|
5214
|
+
const proc = (0, import_node_child_process8.spawn)(shell, args, {
|
|
4727
5215
|
cwd,
|
|
4728
5216
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4729
5217
|
env: { ...process.env }
|
|
@@ -4760,6 +5248,14 @@ var TerminalExecutor = class {
|
|
|
4760
5248
|
});
|
|
4761
5249
|
const timer = setTimeout(() => {
|
|
4762
5250
|
if (this.processes.has(execId)) {
|
|
5251
|
+
this.emit({
|
|
5252
|
+
type: "terminal_output",
|
|
5253
|
+
sessionId,
|
|
5254
|
+
execId,
|
|
5255
|
+
stream: "stderr",
|
|
5256
|
+
data: `[killed: timeout ${Math.round(EXEC_TIMEOUT_MS / 6e4)}m]
|
|
5257
|
+
`
|
|
5258
|
+
});
|
|
4763
5259
|
killProcessCrossPlatform(proc);
|
|
4764
5260
|
}
|
|
4765
5261
|
}, EXEC_TIMEOUT_MS);
|
|
@@ -4784,13 +5280,13 @@ var TerminalExecutor = class {
|
|
|
4784
5280
|
};
|
|
4785
5281
|
|
|
4786
5282
|
// src/xcode/XcodeBuildExecutor.ts
|
|
4787
|
-
var
|
|
5283
|
+
var import_node_child_process9 = require("child_process");
|
|
4788
5284
|
var import_node_util = require("util");
|
|
4789
5285
|
var import_promises4 = require("fs/promises");
|
|
4790
5286
|
var import_node_path6 = require("path");
|
|
4791
5287
|
var import_node_os7 = require("os");
|
|
4792
5288
|
var import_uuid6 = require("uuid");
|
|
4793
|
-
var execAsync = (0, import_node_util.promisify)(
|
|
5289
|
+
var execAsync = (0, import_node_util.promisify)(import_node_child_process9.exec);
|
|
4794
5290
|
var BUILD_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
4795
5291
|
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4796
5292
|
var CONFIG_FILE = (0, import_node_path6.join)((0, import_node_os7.homedir)(), ".sessix", "xcode-config.json");
|
|
@@ -4979,7 +5475,7 @@ ${e.stderr ?? ""}`);
|
|
|
4979
5475
|
if (override) await this.saveConfig(projectPath, override);
|
|
4980
5476
|
const buildId = (0, import_uuid6.v4)();
|
|
4981
5477
|
const args = buildArgs(config);
|
|
4982
|
-
const proc = (0,
|
|
5478
|
+
const proc = (0, import_node_child_process9.spawn)("xcodebuild", args, {
|
|
4983
5479
|
cwd: projectPath,
|
|
4984
5480
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4985
5481
|
env: { ...process.env, NSUnbufferedIO: "YES" }
|
|
@@ -5075,7 +5571,7 @@ ${e.stderr ?? ""}`);
|
|
|
5075
5571
|
|
|
5076
5572
|
`
|
|
5077
5573
|
});
|
|
5078
|
-
const proc = (0,
|
|
5574
|
+
const proc = (0, import_node_child_process9.spawn)(installCmd[0], installCmd.slice(1), {
|
|
5079
5575
|
cwd: projectPath,
|
|
5080
5576
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5081
5577
|
});
|
|
@@ -5197,10 +5693,784 @@ function kindOrder(k) {
|
|
|
5197
5693
|
return k === "device" ? 0 : k === "simulator" ? 1 : k === "mac" ? 2 : 3;
|
|
5198
5694
|
}
|
|
5199
5695
|
|
|
5696
|
+
// src/commands/CommandDiscovery.ts
|
|
5697
|
+
var import_promises5 = require("fs/promises");
|
|
5698
|
+
var import_node_path7 = require("path");
|
|
5699
|
+
var import_node_crypto = require("crypto");
|
|
5700
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
5701
|
+
var MAX_README_BYTES = 256 * 1024;
|
|
5702
|
+
var SUBPACKAGE_DIRS = ["packages", "apps", "crates", "services"];
|
|
5703
|
+
var MAX_SCAN_PER_DIR = 30;
|
|
5704
|
+
var CommandDiscovery = class {
|
|
5705
|
+
cache = /* @__PURE__ */ new Map();
|
|
5706
|
+
async scan(projectPath, refresh = false) {
|
|
5707
|
+
if (!refresh) {
|
|
5708
|
+
const hit = this.cache.get(projectPath);
|
|
5709
|
+
if (hit && hit.expiresAt > Date.now()) return hit.commands;
|
|
5710
|
+
}
|
|
5711
|
+
const collector = [];
|
|
5712
|
+
await Promise.all([
|
|
5713
|
+
this.scanPackageJson(projectPath, "", collector),
|
|
5714
|
+
this.scanMakefile(projectPath, "", collector),
|
|
5715
|
+
this.scanJustfile(projectPath, "", collector),
|
|
5716
|
+
this.scanCargo(projectPath, "", collector),
|
|
5717
|
+
this.scanCompose(projectPath, "", collector),
|
|
5718
|
+
this.scanReadme(projectPath, "README.md", "readme", collector),
|
|
5719
|
+
this.scanReadme(projectPath, "CLAUDE.md", "claude.md", collector)
|
|
5720
|
+
]);
|
|
5721
|
+
for (const sub of SUBPACKAGE_DIRS) {
|
|
5722
|
+
const subRoot = (0, import_node_path7.join)(projectPath, sub);
|
|
5723
|
+
let entries;
|
|
5724
|
+
try {
|
|
5725
|
+
entries = await (0, import_promises5.readdir)(subRoot);
|
|
5726
|
+
} catch {
|
|
5727
|
+
continue;
|
|
5728
|
+
}
|
|
5729
|
+
let scanned = 0;
|
|
5730
|
+
for (const name of entries) {
|
|
5731
|
+
if (name.startsWith(".") || scanned >= MAX_SCAN_PER_DIR) continue;
|
|
5732
|
+
const childAbs = (0, import_node_path7.join)(subRoot, name);
|
|
5733
|
+
try {
|
|
5734
|
+
const s = await (0, import_promises5.stat)(childAbs);
|
|
5735
|
+
if (!s.isDirectory()) continue;
|
|
5736
|
+
} catch {
|
|
5737
|
+
continue;
|
|
5738
|
+
}
|
|
5739
|
+
scanned++;
|
|
5740
|
+
const rel = `${sub}/${name}`;
|
|
5741
|
+
await Promise.all([
|
|
5742
|
+
this.scanPackageJson(projectPath, rel, collector),
|
|
5743
|
+
this.scanCargo(projectPath, rel, collector)
|
|
5744
|
+
]);
|
|
5745
|
+
}
|
|
5746
|
+
}
|
|
5747
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5748
|
+
const deduped = [];
|
|
5749
|
+
for (const c of collector) {
|
|
5750
|
+
if (seen.has(c.id)) continue;
|
|
5751
|
+
seen.add(c.id);
|
|
5752
|
+
deduped.push(c);
|
|
5753
|
+
}
|
|
5754
|
+
deduped.sort((a, b) => {
|
|
5755
|
+
const ca = categoryWeight(a.category) - categoryWeight(b.category);
|
|
5756
|
+
if (ca !== 0) return ca;
|
|
5757
|
+
const sa = sourceWeight(a.source) - sourceWeight(b.source);
|
|
5758
|
+
if (sa !== 0) return sa;
|
|
5759
|
+
return a.title.localeCompare(b.title);
|
|
5760
|
+
});
|
|
5761
|
+
this.cache.set(projectPath, { commands: deduped, expiresAt: Date.now() + CACHE_TTL_MS });
|
|
5762
|
+
return deduped;
|
|
5763
|
+
}
|
|
5764
|
+
invalidate(projectPath) {
|
|
5765
|
+
if (projectPath) this.cache.delete(projectPath);
|
|
5766
|
+
else this.cache.clear();
|
|
5767
|
+
}
|
|
5768
|
+
// ============================================
|
|
5769
|
+
// 各来源扫描器
|
|
5770
|
+
// ============================================
|
|
5771
|
+
async scanPackageJson(rootPath, subDir, out) {
|
|
5772
|
+
const file = subDir ? `${subDir}/package.json` : "package.json";
|
|
5773
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5774
|
+
let raw;
|
|
5775
|
+
try {
|
|
5776
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5777
|
+
} catch {
|
|
5778
|
+
return;
|
|
5779
|
+
}
|
|
5780
|
+
let pkg;
|
|
5781
|
+
try {
|
|
5782
|
+
pkg = JSON.parse(raw);
|
|
5783
|
+
} catch {
|
|
5784
|
+
return;
|
|
5785
|
+
}
|
|
5786
|
+
if (!pkg.scripts) return;
|
|
5787
|
+
for (const [name, script] of Object.entries(pkg.scripts)) {
|
|
5788
|
+
if (typeof script !== "string") continue;
|
|
5789
|
+
const command = subDir ? `npm --workspace=${subDir} run ${name}` : `npm run ${name}`;
|
|
5790
|
+
const title = subDir ? `${pkg.name ?? subDir.split("/").pop()}: ${name}` : name;
|
|
5791
|
+
out.push(makeCommand({
|
|
5792
|
+
title,
|
|
5793
|
+
command,
|
|
5794
|
+
cwd: "",
|
|
5795
|
+
source: "package.json",
|
|
5796
|
+
sourceFile: file,
|
|
5797
|
+
description: script,
|
|
5798
|
+
category: classifyByName(name) ?? classifyByCommand(script)
|
|
5799
|
+
}));
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
async scanMakefile(rootPath, subDir, out) {
|
|
5803
|
+
const file = subDir ? `${subDir}/Makefile` : "Makefile";
|
|
5804
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5805
|
+
let raw;
|
|
5806
|
+
try {
|
|
5807
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5808
|
+
} catch {
|
|
5809
|
+
return;
|
|
5810
|
+
}
|
|
5811
|
+
const lines = raw.split("\n");
|
|
5812
|
+
let lastComment;
|
|
5813
|
+
const targetRegex = /^([a-zA-Z][a-zA-Z0-9_-]*)\s*:(?!=)/;
|
|
5814
|
+
for (const line of lines) {
|
|
5815
|
+
const trim = line.trim();
|
|
5816
|
+
if (trim.startsWith("#")) {
|
|
5817
|
+
lastComment = trim.replace(/^#+\s?/, "").trim() || void 0;
|
|
5818
|
+
continue;
|
|
5819
|
+
}
|
|
5820
|
+
if (trim === "") {
|
|
5821
|
+
lastComment = void 0;
|
|
5822
|
+
continue;
|
|
5823
|
+
}
|
|
5824
|
+
const match = targetRegex.exec(line);
|
|
5825
|
+
if (!match) {
|
|
5826
|
+
lastComment = void 0;
|
|
5827
|
+
continue;
|
|
5828
|
+
}
|
|
5829
|
+
const target = match[1];
|
|
5830
|
+
if (target === ".PHONY" || target === "default" && trim.startsWith("default:")) continue;
|
|
5831
|
+
out.push(makeCommand({
|
|
5832
|
+
title: target,
|
|
5833
|
+
command: `make ${target}`,
|
|
5834
|
+
cwd: subDir,
|
|
5835
|
+
source: "makefile",
|
|
5836
|
+
sourceFile: file,
|
|
5837
|
+
description: lastComment,
|
|
5838
|
+
category: classifyByName(target)
|
|
5839
|
+
}));
|
|
5840
|
+
lastComment = void 0;
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
async scanJustfile(rootPath, subDir, out) {
|
|
5844
|
+
const file = subDir ? `${subDir}/justfile` : "justfile";
|
|
5845
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5846
|
+
let raw;
|
|
5847
|
+
try {
|
|
5848
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5849
|
+
} catch {
|
|
5850
|
+
return;
|
|
5851
|
+
}
|
|
5852
|
+
const lines = raw.split("\n");
|
|
5853
|
+
let lastComment;
|
|
5854
|
+
const recipeRegex = /^([a-zA-Z][a-zA-Z0-9_-]*)\s*(?:[a-zA-Z0-9_=" ]*)?\s*:/;
|
|
5855
|
+
for (const line of lines) {
|
|
5856
|
+
const trim = line.trim();
|
|
5857
|
+
if (trim.startsWith("#")) {
|
|
5858
|
+
lastComment = trim.replace(/^#+\s?/, "").trim() || void 0;
|
|
5859
|
+
continue;
|
|
5860
|
+
}
|
|
5861
|
+
if (trim === "") {
|
|
5862
|
+
lastComment = void 0;
|
|
5863
|
+
continue;
|
|
5864
|
+
}
|
|
5865
|
+
if (line.startsWith(" ") || line.startsWith(" ")) continue;
|
|
5866
|
+
const match = recipeRegex.exec(line);
|
|
5867
|
+
if (!match) {
|
|
5868
|
+
lastComment = void 0;
|
|
5869
|
+
continue;
|
|
5870
|
+
}
|
|
5871
|
+
const recipe = match[1];
|
|
5872
|
+
out.push(makeCommand({
|
|
5873
|
+
title: recipe,
|
|
5874
|
+
command: `just ${recipe}`,
|
|
5875
|
+
cwd: subDir,
|
|
5876
|
+
source: "justfile",
|
|
5877
|
+
sourceFile: file,
|
|
5878
|
+
description: lastComment,
|
|
5879
|
+
category: classifyByName(recipe)
|
|
5880
|
+
}));
|
|
5881
|
+
lastComment = void 0;
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
async scanCargo(rootPath, subDir, out) {
|
|
5885
|
+
const file = subDir ? `${subDir}/Cargo.toml` : "Cargo.toml";
|
|
5886
|
+
const abs = (0, import_node_path7.join)(rootPath, file);
|
|
5887
|
+
try {
|
|
5888
|
+
await (0, import_promises5.stat)(abs);
|
|
5889
|
+
} catch {
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
const presets = [
|
|
5893
|
+
{ title: "cargo build", command: "cargo build", category: "build", description: "Compile in debug mode" },
|
|
5894
|
+
{ title: "cargo build --release", command: "cargo build --release", category: "build", description: "Compile in release mode" },
|
|
5895
|
+
{ title: "cargo run", command: "cargo run", category: "dev", description: "Build and run" },
|
|
5896
|
+
{ title: "cargo test", command: "cargo test", category: "test", description: "Run all tests" },
|
|
5897
|
+
{ title: "cargo check", command: "cargo check", category: "lint", description: "Type-check without producing binary" },
|
|
5898
|
+
{ title: "cargo clippy", command: "cargo clippy", category: "lint", description: "Lint with clippy" },
|
|
5899
|
+
{ title: "cargo fmt", command: "cargo fmt", category: "lint", description: "Format source" }
|
|
5900
|
+
];
|
|
5901
|
+
for (const p of presets) {
|
|
5902
|
+
out.push(makeCommand({
|
|
5903
|
+
title: subDir ? `${subDir.split("/").pop()}: ${p.title}` : p.title,
|
|
5904
|
+
command: p.command,
|
|
5905
|
+
cwd: subDir,
|
|
5906
|
+
source: "cargo",
|
|
5907
|
+
sourceFile: file,
|
|
5908
|
+
description: p.description,
|
|
5909
|
+
category: p.category
|
|
5910
|
+
}));
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
async scanCompose(rootPath, subDir, out) {
|
|
5914
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"]) {
|
|
5915
|
+
const file = subDir ? `${subDir}/${name}` : name;
|
|
5916
|
+
try {
|
|
5917
|
+
await (0, import_promises5.stat)((0, import_node_path7.join)(rootPath, file));
|
|
5918
|
+
} catch {
|
|
5919
|
+
continue;
|
|
5920
|
+
}
|
|
5921
|
+
const presets = [
|
|
5922
|
+
{ title: "docker compose up", cmd: "docker compose up", cat: "dev", desc: "Start services" },
|
|
5923
|
+
{ title: "docker compose up -d", cmd: "docker compose up -d", cat: "dev", desc: "Start services in background" },
|
|
5924
|
+
{ title: "docker compose down", cmd: "docker compose down", cat: "other", desc: "Stop and remove services" },
|
|
5925
|
+
{ title: "docker compose build", cmd: "docker compose build", cat: "build", desc: "Build service images" },
|
|
5926
|
+
{ title: "docker compose logs -f", cmd: "docker compose logs -f", cat: "other", desc: "Tail service logs" }
|
|
5927
|
+
];
|
|
5928
|
+
for (const p of presets) {
|
|
5929
|
+
out.push(makeCommand({
|
|
5930
|
+
title: p.title,
|
|
5931
|
+
command: p.cmd,
|
|
5932
|
+
cwd: subDir,
|
|
5933
|
+
source: "compose",
|
|
5934
|
+
sourceFile: file,
|
|
5935
|
+
description: p.desc,
|
|
5936
|
+
category: p.cat
|
|
5937
|
+
}));
|
|
5938
|
+
}
|
|
5939
|
+
return;
|
|
5940
|
+
}
|
|
5941
|
+
}
|
|
5942
|
+
async scanReadme(rootPath, fileName, source, out) {
|
|
5943
|
+
const abs = (0, import_node_path7.join)(rootPath, fileName);
|
|
5944
|
+
let raw;
|
|
5945
|
+
try {
|
|
5946
|
+
const s = await (0, import_promises5.stat)(abs);
|
|
5947
|
+
if (s.size > MAX_README_BYTES) return;
|
|
5948
|
+
raw = await (0, import_promises5.readFile)(abs, "utf8");
|
|
5949
|
+
} catch {
|
|
5950
|
+
return;
|
|
5951
|
+
}
|
|
5952
|
+
const fenceRegex = /```(?:bash|sh|shell|zsh)\s*\n([\s\S]*?)```/gi;
|
|
5953
|
+
let match;
|
|
5954
|
+
while ((match = fenceRegex.exec(raw)) !== null) {
|
|
5955
|
+
const block = match[1];
|
|
5956
|
+
const blockLines = block.split("\n");
|
|
5957
|
+
let blockHeading;
|
|
5958
|
+
const beforeText = raw.slice(0, match.index).split("\n").reverse();
|
|
5959
|
+
for (const prev of beforeText) {
|
|
5960
|
+
const t2 = prev.trim();
|
|
5961
|
+
if (t2 === "") continue;
|
|
5962
|
+
const head = /^#{1,6}\s+(.+)$/.exec(t2);
|
|
5963
|
+
if (head) blockHeading = head[1].replace(/[#*`]/g, "").trim();
|
|
5964
|
+
break;
|
|
5965
|
+
}
|
|
5966
|
+
const merged = [];
|
|
5967
|
+
let pending = "";
|
|
5968
|
+
for (const rawLine of blockLines) {
|
|
5969
|
+
if (rawLine.trimEnd().endsWith("\\")) {
|
|
5970
|
+
pending += rawLine.trimEnd().slice(0, -1) + " ";
|
|
5971
|
+
continue;
|
|
5972
|
+
}
|
|
5973
|
+
merged.push(pending + rawLine);
|
|
5974
|
+
pending = "";
|
|
5975
|
+
}
|
|
5976
|
+
if (pending) merged.push(pending);
|
|
5977
|
+
for (const rawLine of merged) {
|
|
5978
|
+
const cmd = sanitizeBashLine(rawLine);
|
|
5979
|
+
if (!cmd) continue;
|
|
5980
|
+
const { command: cleanCmd, inlineComment } = splitInlineComment(cmd);
|
|
5981
|
+
const { cwd: cdCwd, command: finalCmd } = splitCdPrefix(cleanCmd);
|
|
5982
|
+
const title = synthesizeTitle(finalCmd);
|
|
5983
|
+
out.push(makeCommand({
|
|
5984
|
+
title,
|
|
5985
|
+
command: finalCmd,
|
|
5986
|
+
cwd: cdCwd,
|
|
5987
|
+
source,
|
|
5988
|
+
sourceFile: fileName,
|
|
5989
|
+
description: inlineComment ?? blockHeading,
|
|
5990
|
+
category: classifyByCommand(finalCmd)
|
|
5991
|
+
}));
|
|
5992
|
+
}
|
|
5993
|
+
}
|
|
5994
|
+
}
|
|
5995
|
+
};
|
|
5996
|
+
function makeCommand(input) {
|
|
5997
|
+
const id = (0, import_node_crypto.createHash)("sha1").update(`${input.source}|${input.sourceFile}|${input.command}|${input.cwd}`).digest("hex").slice(0, 12);
|
|
5998
|
+
return {
|
|
5999
|
+
id,
|
|
6000
|
+
title: input.title,
|
|
6001
|
+
command: input.command,
|
|
6002
|
+
cwd: input.cwd,
|
|
6003
|
+
source: input.source,
|
|
6004
|
+
sourceFile: input.sourceFile,
|
|
6005
|
+
description: input.description,
|
|
6006
|
+
category: input.category ?? classifyByCommand(input.command) ?? "other"
|
|
6007
|
+
};
|
|
6008
|
+
}
|
|
6009
|
+
function sanitizeBashLine(line) {
|
|
6010
|
+
let l = line.trim();
|
|
6011
|
+
if (!l) return null;
|
|
6012
|
+
if (l.startsWith("#")) return null;
|
|
6013
|
+
l = l.replace(/^[$>]\s*/, "");
|
|
6014
|
+
if (!l) return null;
|
|
6015
|
+
if (/^[A-Z_]+=/.test(l) && !/\s/.test(l)) return null;
|
|
6016
|
+
if (l === "EOF" || l === "EOT") return null;
|
|
6017
|
+
if (l.startsWith("//") || l.startsWith("//#")) return null;
|
|
6018
|
+
if (l.length > 400) return null;
|
|
6019
|
+
if (/<.+>/.test(l) && /your[-_]/.test(l.toLowerCase())) return null;
|
|
6020
|
+
return l;
|
|
6021
|
+
}
|
|
6022
|
+
function synthesizeTitle(cmd) {
|
|
6023
|
+
let work = cmd;
|
|
6024
|
+
while (/^[A-Z_][A-Z0-9_]*=/.test(work)) {
|
|
6025
|
+
const m = /^[A-Z_][A-Z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+/.exec(work);
|
|
6026
|
+
if (!m) break;
|
|
6027
|
+
work = work.slice(m[0].length);
|
|
6028
|
+
}
|
|
6029
|
+
const cdMatch = /^cd\s+\S+\s*&&\s*(.+)$/.exec(work);
|
|
6030
|
+
if (cdMatch) work = cdMatch[1];
|
|
6031
|
+
const tokens = work.split(/\s+/).filter(Boolean);
|
|
6032
|
+
const head = tokens.slice(0, 3).join(" ");
|
|
6033
|
+
return head.length > 60 ? head.slice(0, 60) + "\u2026" : head;
|
|
6034
|
+
}
|
|
6035
|
+
function splitCdPrefix(cmd) {
|
|
6036
|
+
const m = /^cd\s+(\S+)\s*&&\s*(.+)$/.exec(cmd);
|
|
6037
|
+
if (!m) return { cwd: "", command: cmd };
|
|
6038
|
+
const path2 = m[1];
|
|
6039
|
+
if (!path2) return { cwd: "", command: cmd };
|
|
6040
|
+
if (path2.startsWith("/") || path2.startsWith("~") || path2.startsWith("-")) {
|
|
6041
|
+
return { cwd: "", command: cmd };
|
|
6042
|
+
}
|
|
6043
|
+
if (path2.split("/").some((seg) => seg === "..")) {
|
|
6044
|
+
return { cwd: "", command: cmd };
|
|
6045
|
+
}
|
|
6046
|
+
return { cwd: path2, command: m[2].trim() };
|
|
6047
|
+
}
|
|
6048
|
+
function splitInlineComment(line) {
|
|
6049
|
+
let inSingle = false;
|
|
6050
|
+
let inDouble = false;
|
|
6051
|
+
for (let i = 0; i < line.length; i++) {
|
|
6052
|
+
const ch = line[i];
|
|
6053
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle;
|
|
6054
|
+
else if (ch === '"' && !inSingle) inDouble = !inDouble;
|
|
6055
|
+
else if (ch === "#" && !inSingle && !inDouble && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
6056
|
+
const cmd = line.slice(0, i).trim();
|
|
6057
|
+
const comment = line.slice(i + 1).trim();
|
|
6058
|
+
return { command: cmd, inlineComment: comment.length > 0 ? comment : void 0 };
|
|
6059
|
+
}
|
|
6060
|
+
}
|
|
6061
|
+
return { command: line };
|
|
6062
|
+
}
|
|
6063
|
+
function classifyByName(name) {
|
|
6064
|
+
const lower = name.toLowerCase();
|
|
6065
|
+
if (/(^|[:_-])(build|compile|bundle|prebuild)([:_-]|$)/.test(lower)) return "build";
|
|
6066
|
+
if (/(^|[:_-])(test|spec|jest|vitest|e2e)([:_-]|$)/.test(lower)) return "test";
|
|
6067
|
+
if (/(^|[:_-])(dev|start|serve|watch|run)([:_-]|$)/.test(lower)) return "dev";
|
|
6068
|
+
if (/(^|[:_-])(lint|format|fmt|check|typecheck)([:_-]|$)/.test(lower)) return "lint";
|
|
6069
|
+
if (/(^|[:_-])(install|setup|init|bootstrap)([:_-]|$)/.test(lower)) return "install";
|
|
6070
|
+
if (/(^|[:_-])(deploy|publish|release|ship)([:_-]|$)/.test(lower)) return "deploy";
|
|
6071
|
+
return void 0;
|
|
6072
|
+
}
|
|
6073
|
+
function classifyByCommand(cmd) {
|
|
6074
|
+
const lower = cmd.toLowerCase();
|
|
6075
|
+
if (/\b(build|compile|bundle|prebuild|tsup|webpack|esbuild|vite build|next build)\b/.test(lower)) return "build";
|
|
6076
|
+
if (/\b(test|jest|vitest|mocha|pytest|cargo test|go test)\b/.test(lower)) return "test";
|
|
6077
|
+
if (/\b(dev|start|serve|watch|nodemon|tsx watch|next dev|expo start)\b/.test(lower)) return "dev";
|
|
6078
|
+
if (/\b(lint|eslint|tsc|tslint|fmt|format|prettier|clippy)\b/.test(lower)) return "lint";
|
|
6079
|
+
if (/\b(install|setup|bootstrap)\b/.test(lower) && !/\binstall\s+/.test(lower)) return "install";
|
|
6080
|
+
if (/^npm install\b|^pnpm install\b|^yarn install\b|^yarn\s*$|^pnpm\s*$/.test(lower)) return "install";
|
|
6081
|
+
if (/\b(deploy|publish|release)\b/.test(lower)) return "deploy";
|
|
6082
|
+
return "other";
|
|
6083
|
+
}
|
|
6084
|
+
function categoryWeight(c) {
|
|
6085
|
+
return {
|
|
6086
|
+
dev: 0,
|
|
6087
|
+
build: 1,
|
|
6088
|
+
test: 2,
|
|
6089
|
+
lint: 3,
|
|
6090
|
+
install: 4,
|
|
6091
|
+
deploy: 5,
|
|
6092
|
+
other: 6
|
|
6093
|
+
}[c];
|
|
6094
|
+
}
|
|
6095
|
+
function sourceWeight(s) {
|
|
6096
|
+
return {
|
|
6097
|
+
"package.json": 0,
|
|
6098
|
+
makefile: 1,
|
|
6099
|
+
justfile: 2,
|
|
6100
|
+
taskfile: 3,
|
|
6101
|
+
cargo: 4,
|
|
6102
|
+
compose: 5,
|
|
6103
|
+
readme: 6,
|
|
6104
|
+
"claude.md": 7
|
|
6105
|
+
}[s];
|
|
6106
|
+
}
|
|
6107
|
+
|
|
6108
|
+
// src/git/GitExecutor.ts
|
|
6109
|
+
var import_node_child_process10 = require("child_process");
|
|
6110
|
+
var import_node_util2 = require("util");
|
|
6111
|
+
var import_uuid7 = require("uuid");
|
|
6112
|
+
var execAsync2 = (0, import_node_util2.promisify)(import_node_child_process10.exec);
|
|
6113
|
+
var STATUS_TIMEOUT_MS = 15e3;
|
|
6114
|
+
var COMMIT_TIMEOUT_MS = 6e4;
|
|
6115
|
+
var PUSH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
6116
|
+
var GitExecutor = class {
|
|
6117
|
+
eventCallbacks = [];
|
|
6118
|
+
onEvent(callback) {
|
|
6119
|
+
this.eventCallbacks.push(callback);
|
|
6120
|
+
return () => {
|
|
6121
|
+
const idx = this.eventCallbacks.indexOf(callback);
|
|
6122
|
+
if (idx !== -1) this.eventCallbacks.splice(idx, 1);
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
emit(event) {
|
|
6126
|
+
for (const cb of this.eventCallbacks) {
|
|
6127
|
+
try {
|
|
6128
|
+
cb(event);
|
|
6129
|
+
} catch (err) {
|
|
6130
|
+
console.error("[GitExecutor] Event callback error:", err);
|
|
6131
|
+
}
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
async detectStatus(projectPath) {
|
|
6135
|
+
const opts = { cwd: projectPath, timeout: STATUS_TIMEOUT_MS, maxBuffer: 4 * 1024 * 1024 };
|
|
6136
|
+
try {
|
|
6137
|
+
await execAsync2("git rev-parse --is-inside-work-tree", opts);
|
|
6138
|
+
} catch {
|
|
6139
|
+
return { isRepo: false, hasRemote: false, changes: [] };
|
|
6140
|
+
}
|
|
6141
|
+
let root;
|
|
6142
|
+
try {
|
|
6143
|
+
const { stdout } = await execAsync2("git rev-parse --show-toplevel", opts);
|
|
6144
|
+
root = stdout.trim();
|
|
6145
|
+
} catch {
|
|
6146
|
+
}
|
|
6147
|
+
let branch;
|
|
6148
|
+
try {
|
|
6149
|
+
const { stdout } = await execAsync2("git symbolic-ref --short HEAD", opts);
|
|
6150
|
+
branch = stdout.trim();
|
|
6151
|
+
} catch {
|
|
6152
|
+
branch = void 0;
|
|
6153
|
+
}
|
|
6154
|
+
let upstream;
|
|
6155
|
+
try {
|
|
6156
|
+
const { stdout } = await execAsync2("git rev-parse --abbrev-ref --symbolic-full-name @{u}", opts);
|
|
6157
|
+
upstream = stdout.trim();
|
|
6158
|
+
} catch {
|
|
6159
|
+
}
|
|
6160
|
+
let hasRemote = false;
|
|
6161
|
+
try {
|
|
6162
|
+
const { stdout } = await execAsync2("git remote", opts);
|
|
6163
|
+
hasRemote = stdout.trim().length > 0;
|
|
6164
|
+
} catch {
|
|
6165
|
+
}
|
|
6166
|
+
let ahead;
|
|
6167
|
+
let behind;
|
|
6168
|
+
if (upstream) {
|
|
6169
|
+
try {
|
|
6170
|
+
const { stdout } = await execAsync2("git rev-list --left-right --count HEAD...@{u}", opts);
|
|
6171
|
+
const [a, b] = stdout.trim().split(/\s+/);
|
|
6172
|
+
ahead = Number(a);
|
|
6173
|
+
behind = Number(b);
|
|
6174
|
+
} catch {
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
const changes = await this.parsePorcelain(projectPath);
|
|
6178
|
+
return {
|
|
6179
|
+
isRepo: true,
|
|
6180
|
+
root,
|
|
6181
|
+
branch,
|
|
6182
|
+
upstream,
|
|
6183
|
+
hasRemote,
|
|
6184
|
+
changes,
|
|
6185
|
+
ahead,
|
|
6186
|
+
behind
|
|
6187
|
+
};
|
|
6188
|
+
}
|
|
6189
|
+
async parsePorcelain(projectPath) {
|
|
6190
|
+
let stdout;
|
|
6191
|
+
try {
|
|
6192
|
+
const r = await execAsync2("git status --porcelain=v1 -z", {
|
|
6193
|
+
cwd: projectPath,
|
|
6194
|
+
timeout: STATUS_TIMEOUT_MS,
|
|
6195
|
+
maxBuffer: 8 * 1024 * 1024
|
|
6196
|
+
});
|
|
6197
|
+
stdout = r.stdout;
|
|
6198
|
+
} catch {
|
|
6199
|
+
return [];
|
|
6200
|
+
}
|
|
6201
|
+
const changes = [];
|
|
6202
|
+
const records = stdout.split("\0");
|
|
6203
|
+
for (let i = 0; i < records.length; i++) {
|
|
6204
|
+
const rec = records[i];
|
|
6205
|
+
if (!rec) continue;
|
|
6206
|
+
if (rec.length < 3) continue;
|
|
6207
|
+
const x = rec.charAt(0);
|
|
6208
|
+
const y = rec.charAt(1);
|
|
6209
|
+
const path2 = rec.slice(3);
|
|
6210
|
+
const isRename = x === "R" || x === "C";
|
|
6211
|
+
if (isRename) {
|
|
6212
|
+
i += 1;
|
|
6213
|
+
}
|
|
6214
|
+
const untracked = x === "?" && y === "?";
|
|
6215
|
+
const staged = !untracked && x !== " " && x !== "?";
|
|
6216
|
+
changes.push({
|
|
6217
|
+
path: path2,
|
|
6218
|
+
staged,
|
|
6219
|
+
untracked,
|
|
6220
|
+
code: `${x}${y}`
|
|
6221
|
+
});
|
|
6222
|
+
}
|
|
6223
|
+
return changes;
|
|
6224
|
+
}
|
|
6225
|
+
/**
|
|
6226
|
+
* 执行 commit(可选连带 push)。
|
|
6227
|
+
* - 若提供 files:先 git add 这些路径
|
|
6228
|
+
* - 若未提供 files:默认 git add -A(提交所有变更)
|
|
6229
|
+
*/
|
|
6230
|
+
async commit(sessionId, projectPath, message, files, alsoPush) {
|
|
6231
|
+
const opId = (0, import_uuid7.v4)();
|
|
6232
|
+
this.runSequence(sessionId, opId, "commit", projectPath, [
|
|
6233
|
+
files && files.length > 0 ? ["git", "add", "--", ...files] : ["git", "add", "-A"],
|
|
6234
|
+
["git", "commit", "-m", message]
|
|
6235
|
+
], COMMIT_TIMEOUT_MS).then(async (ok) => {
|
|
6236
|
+
if (ok && alsoPush) {
|
|
6237
|
+
await this.runSequence(sessionId, opId, "push", projectPath, [
|
|
6238
|
+
["git", "push"]
|
|
6239
|
+
], PUSH_TIMEOUT_MS);
|
|
6240
|
+
}
|
|
6241
|
+
}).catch((err) => {
|
|
6242
|
+
console.error("[GitExecutor] commit error:", err);
|
|
6243
|
+
});
|
|
6244
|
+
return opId;
|
|
6245
|
+
}
|
|
6246
|
+
async push(sessionId, projectPath) {
|
|
6247
|
+
const opId = (0, import_uuid7.v4)();
|
|
6248
|
+
this.runSequence(sessionId, opId, "push", projectPath, [
|
|
6249
|
+
["git", "push"]
|
|
6250
|
+
], PUSH_TIMEOUT_MS).catch((err) => {
|
|
6251
|
+
console.error("[GitExecutor] push error:", err);
|
|
6252
|
+
});
|
|
6253
|
+
return opId;
|
|
6254
|
+
}
|
|
6255
|
+
/**
|
|
6256
|
+
* 顺序执行一组命令,任一失败则停止。返回是否全部成功。
|
|
6257
|
+
* 每条命令的输出和最后一条命令的退出事件统一打到同一 phase。
|
|
6258
|
+
*/
|
|
6259
|
+
async runSequence(sessionId, opId, phase, projectPath, commands, timeoutMs) {
|
|
6260
|
+
let lastCode = 0;
|
|
6261
|
+
let lastSignal = null;
|
|
6262
|
+
for (const cmd of commands) {
|
|
6263
|
+
const { code, signal } = await this.runOne(sessionId, opId, phase, projectPath, cmd, timeoutMs);
|
|
6264
|
+
lastCode = code;
|
|
6265
|
+
lastSignal = signal;
|
|
6266
|
+
if (code !== 0) break;
|
|
6267
|
+
}
|
|
6268
|
+
this.emit({ type: "git_exit", sessionId, opId, phase, code: lastCode, signal: lastSignal });
|
|
6269
|
+
return lastCode === 0;
|
|
6270
|
+
}
|
|
6271
|
+
runOne(sessionId, opId, phase, projectPath, cmd, timeoutMs) {
|
|
6272
|
+
return new Promise((resolve) => {
|
|
6273
|
+
const display = cmd.map((p) => /\s/.test(p) ? `"${p}"` : p).join(" ");
|
|
6274
|
+
this.emit({
|
|
6275
|
+
type: "git_output",
|
|
6276
|
+
sessionId,
|
|
6277
|
+
opId,
|
|
6278
|
+
phase,
|
|
6279
|
+
stream: "stdout",
|
|
6280
|
+
data: `$ ${display}
|
|
6281
|
+
`
|
|
6282
|
+
});
|
|
6283
|
+
let proc;
|
|
6284
|
+
try {
|
|
6285
|
+
proc = (0, import_node_child_process10.spawn)(cmd[0], cmd.slice(1), {
|
|
6286
|
+
cwd: projectPath,
|
|
6287
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6288
|
+
env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
|
|
6289
|
+
});
|
|
6290
|
+
} catch (err) {
|
|
6291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
6292
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: `[spawn error] ${msg}
|
|
6293
|
+
` });
|
|
6294
|
+
resolve({ code: 1, signal: null });
|
|
6295
|
+
return;
|
|
6296
|
+
}
|
|
6297
|
+
proc.stdout?.on("data", (chunk) => {
|
|
6298
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stdout", data: chunk.toString() });
|
|
6299
|
+
});
|
|
6300
|
+
proc.stderr?.on("data", (chunk) => {
|
|
6301
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: chunk.toString() });
|
|
6302
|
+
});
|
|
6303
|
+
proc.on("error", (err) => {
|
|
6304
|
+
this.emit({ type: "git_output", sessionId, opId, phase, stream: "stderr", data: `[error] ${err.message}
|
|
6305
|
+
` });
|
|
6306
|
+
});
|
|
6307
|
+
const timer = setTimeout(() => {
|
|
6308
|
+
try {
|
|
6309
|
+
proc.kill("SIGTERM");
|
|
6310
|
+
} catch {
|
|
6311
|
+
}
|
|
6312
|
+
}, timeoutMs);
|
|
6313
|
+
proc.on("exit", (code, signal) => {
|
|
6314
|
+
clearTimeout(timer);
|
|
6315
|
+
resolve({ code, signal });
|
|
6316
|
+
});
|
|
6317
|
+
});
|
|
6318
|
+
}
|
|
6319
|
+
};
|
|
6320
|
+
|
|
6321
|
+
// src/scheduling/ScheduledSessionManager.ts
|
|
6322
|
+
var import_promises6 = require("fs/promises");
|
|
6323
|
+
var import_node_os8 = require("os");
|
|
6324
|
+
var import_node_path8 = require("path");
|
|
6325
|
+
var import_uuid8 = require("uuid");
|
|
6326
|
+
var MAX_TIMEOUT_MS = 2147483647;
|
|
6327
|
+
var ScheduledSessionManager = class {
|
|
6328
|
+
tasks = /* @__PURE__ */ new Map();
|
|
6329
|
+
storeFile;
|
|
6330
|
+
onFire;
|
|
6331
|
+
onChange;
|
|
6332
|
+
onFired;
|
|
6333
|
+
persistTimer = null;
|
|
6334
|
+
constructor(opts) {
|
|
6335
|
+
this.storeFile = opts.storeFile ?? (0, import_node_path8.join)((0, import_node_os8.homedir)(), ".sessix", "scheduled-sessions.json");
|
|
6336
|
+
this.onFire = opts.onFire;
|
|
6337
|
+
this.onChange = opts.onChange;
|
|
6338
|
+
this.onFired = opts.onFired;
|
|
6339
|
+
}
|
|
6340
|
+
/** 启动时从磁盘恢复任务表,已过期的立刻触发 */
|
|
6341
|
+
async load() {
|
|
6342
|
+
let raw;
|
|
6343
|
+
try {
|
|
6344
|
+
raw = await (0, import_promises6.readFile)(this.storeFile, "utf8");
|
|
6345
|
+
} catch {
|
|
6346
|
+
return;
|
|
6347
|
+
}
|
|
6348
|
+
let parsed;
|
|
6349
|
+
try {
|
|
6350
|
+
parsed = JSON.parse(raw);
|
|
6351
|
+
} catch {
|
|
6352
|
+
return;
|
|
6353
|
+
}
|
|
6354
|
+
if (!Array.isArray(parsed)) return;
|
|
6355
|
+
for (const item of parsed) {
|
|
6356
|
+
if (!isValidTask(item)) continue;
|
|
6357
|
+
this.scheduleTimer(item);
|
|
6358
|
+
}
|
|
6359
|
+
}
|
|
6360
|
+
/** 注册一个定时任务(payload 由调用方校验) */
|
|
6361
|
+
schedule(scheduledAt, payload) {
|
|
6362
|
+
const task = {
|
|
6363
|
+
id: (0, import_uuid8.v4)(),
|
|
6364
|
+
scheduledAt,
|
|
6365
|
+
createdAt: Date.now(),
|
|
6366
|
+
payload
|
|
6367
|
+
};
|
|
6368
|
+
this.scheduleTimer(task);
|
|
6369
|
+
this.persist();
|
|
6370
|
+
this.notifyChange();
|
|
6371
|
+
return task;
|
|
6372
|
+
}
|
|
6373
|
+
/** 取消任务,返回是否成功 */
|
|
6374
|
+
cancel(id) {
|
|
6375
|
+
const entry = this.tasks.get(id);
|
|
6376
|
+
if (!entry) return false;
|
|
6377
|
+
clearTimeout(entry.timer);
|
|
6378
|
+
this.tasks.delete(id);
|
|
6379
|
+
this.persist();
|
|
6380
|
+
this.notifyChange();
|
|
6381
|
+
return true;
|
|
6382
|
+
}
|
|
6383
|
+
/** 列出所有未触发的任务(按时间升序) */
|
|
6384
|
+
list() {
|
|
6385
|
+
return [...this.tasks.values()].map((e) => e.task).sort((a, b) => a.scheduledAt - b.scheduledAt);
|
|
6386
|
+
}
|
|
6387
|
+
/** 优雅关闭(清空定时器,不删除磁盘任务) */
|
|
6388
|
+
destroy() {
|
|
6389
|
+
for (const { timer } of this.tasks.values()) clearTimeout(timer);
|
|
6390
|
+
this.tasks.clear();
|
|
6391
|
+
if (this.persistTimer) {
|
|
6392
|
+
clearTimeout(this.persistTimer);
|
|
6393
|
+
this.persistTimer = null;
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
// ============================================
|
|
6397
|
+
// 内部
|
|
6398
|
+
// ============================================
|
|
6399
|
+
scheduleTimer(task) {
|
|
6400
|
+
const delay = Math.max(0, task.scheduledAt - Date.now());
|
|
6401
|
+
const armDelay = Math.min(delay, MAX_TIMEOUT_MS);
|
|
6402
|
+
const timer = setTimeout(() => {
|
|
6403
|
+
const remaining = task.scheduledAt - Date.now();
|
|
6404
|
+
if (remaining > 1e3) {
|
|
6405
|
+
this.scheduleTimer(task);
|
|
6406
|
+
return;
|
|
6407
|
+
}
|
|
6408
|
+
this.fire(task).catch((err) => {
|
|
6409
|
+
console.error("[ScheduledSessionManager] fire error:", err);
|
|
6410
|
+
});
|
|
6411
|
+
}, armDelay);
|
|
6412
|
+
this.tasks.set(task.id, { task, timer });
|
|
6413
|
+
}
|
|
6414
|
+
async fire(task) {
|
|
6415
|
+
const entry = this.tasks.get(task.id);
|
|
6416
|
+
if (!entry) return;
|
|
6417
|
+
clearTimeout(entry.timer);
|
|
6418
|
+
this.tasks.delete(task.id);
|
|
6419
|
+
this.persist();
|
|
6420
|
+
this.notifyChange();
|
|
6421
|
+
try {
|
|
6422
|
+
const result = await this.onFire(task);
|
|
6423
|
+
this.onFired?.({ id: task.id, sessionId: result.sessionId });
|
|
6424
|
+
} catch (err) {
|
|
6425
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6426
|
+
console.error(`[ScheduledSessionManager] fire failed for task ${task.id}: ${message}`);
|
|
6427
|
+
this.onFired?.({ id: task.id, error: message });
|
|
6428
|
+
}
|
|
6429
|
+
}
|
|
6430
|
+
notifyChange() {
|
|
6431
|
+
if (!this.onChange) return;
|
|
6432
|
+
this.onChange(this.list());
|
|
6433
|
+
}
|
|
6434
|
+
/** 防抖持久化(500ms) */
|
|
6435
|
+
persist() {
|
|
6436
|
+
if (this.persistTimer) clearTimeout(this.persistTimer);
|
|
6437
|
+
this.persistTimer = setTimeout(() => {
|
|
6438
|
+
this.persistTimer = null;
|
|
6439
|
+
const tasks = [...this.tasks.values()].map((e) => e.task);
|
|
6440
|
+
(0, import_promises6.mkdir)((0, import_node_path8.join)(this.storeFile, ".."), { recursive: true }).then(() => (0, import_promises6.writeFile)(this.storeFile, JSON.stringify(tasks, null, 2), "utf8")).catch((err) => {
|
|
6441
|
+
console.error("[ScheduledSessionManager] persist error:", err);
|
|
6442
|
+
});
|
|
6443
|
+
}, 500);
|
|
6444
|
+
}
|
|
6445
|
+
};
|
|
6446
|
+
function isValidTask(value) {
|
|
6447
|
+
if (!value || typeof value !== "object") return false;
|
|
6448
|
+
const v = value;
|
|
6449
|
+
if (typeof v.id !== "string" || typeof v.scheduledAt !== "number" || typeof v.createdAt !== "number") {
|
|
6450
|
+
return false;
|
|
6451
|
+
}
|
|
6452
|
+
const payload = v.payload;
|
|
6453
|
+
if (!payload || typeof payload !== "object") return false;
|
|
6454
|
+
const p = payload;
|
|
6455
|
+
if (p.kind === "create") {
|
|
6456
|
+
return typeof p.projectPath === "string" && typeof p.message === "string";
|
|
6457
|
+
}
|
|
6458
|
+
if (p.kind === "send") {
|
|
6459
|
+
return typeof p.sessionId === "string" && typeof p.message === "string";
|
|
6460
|
+
}
|
|
6461
|
+
return false;
|
|
6462
|
+
}
|
|
6463
|
+
|
|
5200
6464
|
// src/utils/cliCapabilities.ts
|
|
5201
|
-
var
|
|
6465
|
+
var import_node_child_process11 = require("child_process");
|
|
6466
|
+
var DEFAULT_MODELS = [
|
|
6467
|
+
{ value: "opus", label: "Opus 4.7", sublabel: "Most capable for ambitious work" },
|
|
6468
|
+
{ value: "sonnet", label: "Sonnet 4.6", sublabel: "Most efficient for everyday tasks" },
|
|
6469
|
+
{ value: "haiku", label: "Haiku 4.5", sublabel: "Fastest for quick answers" }
|
|
6470
|
+
];
|
|
5202
6471
|
var DEFAULT_CAPABILITIES = {
|
|
5203
|
-
effortLevels: ["low", "medium", "high", "xhigh", "max"]
|
|
6472
|
+
effortLevels: ["low", "medium", "high", "xhigh", "max"],
|
|
6473
|
+
models: DEFAULT_MODELS
|
|
5204
6474
|
};
|
|
5205
6475
|
async function parseCliCapabilities() {
|
|
5206
6476
|
const claudePath = findClaudePath();
|
|
@@ -5226,7 +6496,7 @@ async function parseCliCapabilities() {
|
|
|
5226
6496
|
}
|
|
5227
6497
|
function runCli(path2, args) {
|
|
5228
6498
|
return new Promise((resolve) => {
|
|
5229
|
-
(0,
|
|
6499
|
+
(0, import_node_child_process11.execFile)(path2, args, { timeout: 5e3 }, (err, stdout) => {
|
|
5230
6500
|
if (err) {
|
|
5231
6501
|
console.warn(`[CliCapabilities] Failed to run ${path2} ${args.join(" ")}:`, err.message);
|
|
5232
6502
|
resolve(null);
|
|
@@ -5240,11 +6510,11 @@ function runCli(path2, args) {
|
|
|
5240
6510
|
// src/server.ts
|
|
5241
6511
|
var WS_PORT = 3745;
|
|
5242
6512
|
var HTTP_PORT = 3746;
|
|
5243
|
-
var
|
|
6513
|
+
var execAsync3 = (0, import_node_util3.promisify)(import_node_child_process12.exec);
|
|
5244
6514
|
async function killPortProcess(port) {
|
|
5245
6515
|
try {
|
|
5246
6516
|
if (isWindows) {
|
|
5247
|
-
const { stdout } = await
|
|
6517
|
+
const { stdout } = await execAsync3(
|
|
5248
6518
|
`netstat -ano | findstr :${port} | findstr LISTENING`
|
|
5249
6519
|
);
|
|
5250
6520
|
const pids = /* @__PURE__ */ new Set();
|
|
@@ -5254,20 +6524,53 @@ async function killPortProcess(port) {
|
|
|
5254
6524
|
if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
|
|
5255
6525
|
}
|
|
5256
6526
|
for (const pid of pids) {
|
|
5257
|
-
await
|
|
6527
|
+
await execAsync3(`taskkill /PID ${pid} /F`).catch(() => {
|
|
5258
6528
|
});
|
|
5259
6529
|
}
|
|
5260
6530
|
} else {
|
|
5261
|
-
const { stdout } = await
|
|
6531
|
+
const { stdout } = await execAsync3(`lsof -ti :${port}`);
|
|
5262
6532
|
const pids = stdout.trim().split("\n").filter((p) => p && /^\d+$/.test(p));
|
|
5263
6533
|
if (pids.length > 0) {
|
|
5264
|
-
await
|
|
6534
|
+
await execAsync3(`kill -9 ${pids.join(" ")}`);
|
|
5265
6535
|
}
|
|
5266
6536
|
}
|
|
5267
6537
|
await new Promise((resolve) => setTimeout(resolve, 600));
|
|
5268
6538
|
} catch {
|
|
5269
6539
|
}
|
|
5270
6540
|
}
|
|
6541
|
+
async function loadApnsConfigFromFile() {
|
|
6542
|
+
const path2 = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix", "apns.json");
|
|
6543
|
+
try {
|
|
6544
|
+
const raw = await (0, import_promises7.readFile)(path2, "utf8");
|
|
6545
|
+
const cfg = JSON.parse(raw);
|
|
6546
|
+
if (typeof cfg.teamId !== "string" || typeof cfg.keyId !== "string" || typeof cfg.authKeyPath !== "string") {
|
|
6547
|
+
console.warn(`[Server] \u26A0\uFE0F ${path2} \u7F3A\u5C11\u5FC5\u9700\u5B57\u6BB5 (teamId / keyId / authKeyPath)\uFF0CLA \u540E\u53F0\u63A8\u9001\u5DF2\u7981\u7528`);
|
|
6548
|
+
return null;
|
|
6549
|
+
}
|
|
6550
|
+
try {
|
|
6551
|
+
await (0, import_promises7.readFile)(cfg.authKeyPath, "utf8");
|
|
6552
|
+
} catch (err) {
|
|
6553
|
+
console.warn(`[Server] \u26A0\uFE0F \u65E0\u6CD5\u8BFB\u53D6 APNs Auth Key: ${cfg.authKeyPath}`, err);
|
|
6554
|
+
return null;
|
|
6555
|
+
}
|
|
6556
|
+
console.log(`[Server] \u2705 \u5DF2\u52A0\u8F7D APNs \u914D\u7F6E (${path2})`);
|
|
6557
|
+
console.log(`[Server] teamId=${cfg.teamId} keyId=${cfg.keyId} sandbox=${cfg.sandbox === true}`);
|
|
6558
|
+
return {
|
|
6559
|
+
teamId: cfg.teamId,
|
|
6560
|
+
keyId: cfg.keyId,
|
|
6561
|
+
authKeyPath: cfg.authKeyPath,
|
|
6562
|
+
sandbox: cfg.sandbox === true
|
|
6563
|
+
};
|
|
6564
|
+
} catch (err) {
|
|
6565
|
+
const code = err.code;
|
|
6566
|
+
if (code === "ENOENT") {
|
|
6567
|
+
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`);
|
|
6568
|
+
} else {
|
|
6569
|
+
console.warn(`[Server] \u26A0\uFE0F \u8BFB\u53D6 ${path2} \u5931\u8D25:`, err);
|
|
6570
|
+
}
|
|
6571
|
+
return null;
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
5271
6574
|
async function createWithRetry(label, port, factory) {
|
|
5272
6575
|
try {
|
|
5273
6576
|
return await factory();
|
|
@@ -5282,8 +6585,9 @@ async function createWithRetry(label, port, factory) {
|
|
|
5282
6585
|
}
|
|
5283
6586
|
}
|
|
5284
6587
|
async function start(opts = {}) {
|
|
5285
|
-
|
|
5286
|
-
const
|
|
6588
|
+
fixShellPath();
|
|
6589
|
+
const configDir = (0, import_node_path9.join)((0, import_node_os9.homedir)(), ".sessix");
|
|
6590
|
+
const tokenFile = (0, import_node_path9.join)(configDir, "token");
|
|
5287
6591
|
let token;
|
|
5288
6592
|
if (opts.token !== void 0) {
|
|
5289
6593
|
token = opts.token;
|
|
@@ -5293,11 +6597,11 @@ async function start(opts = {}) {
|
|
|
5293
6597
|
token = envToken;
|
|
5294
6598
|
} else {
|
|
5295
6599
|
try {
|
|
5296
|
-
token = (await (0,
|
|
6600
|
+
token = (await (0, import_promises7.readFile)(tokenFile, "utf8")).trim();
|
|
5297
6601
|
} catch {
|
|
5298
|
-
token = (0,
|
|
5299
|
-
await (0,
|
|
5300
|
-
await (0,
|
|
6602
|
+
token = (0, import_uuid9.v4)();
|
|
6603
|
+
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
6604
|
+
await (0, import_promises7.writeFile)(tokenFile, token, "utf8");
|
|
5301
6605
|
}
|
|
5302
6606
|
}
|
|
5303
6607
|
}
|
|
@@ -5305,6 +6609,9 @@ async function start(opts = {}) {
|
|
|
5305
6609
|
const sessionManager = new SessionManager(providerFactory);
|
|
5306
6610
|
const terminalExecutor = new TerminalExecutor();
|
|
5307
6611
|
const xcodeBuildExecutor = new XcodeBuildExecutor();
|
|
6612
|
+
const commandDiscovery = new CommandDiscovery();
|
|
6613
|
+
const gitExecutor = new GitExecutor();
|
|
6614
|
+
let scheduledManager = null;
|
|
5308
6615
|
const approvalProxy = await createWithRetry(
|
|
5309
6616
|
"ApprovalProxy",
|
|
5310
6617
|
HTTP_PORT,
|
|
@@ -5327,9 +6634,10 @@ async function start(opts = {}) {
|
|
|
5327
6634
|
const notificationService = new NotificationService(sessionManager, expoChannel);
|
|
5328
6635
|
notificationService.addChannel("expo", expoChannel, opts.enableExpoPush !== false);
|
|
5329
6636
|
notificationService.addChannel("mac", new DesktopNotificationChannel(), opts.enableMacNotification !== false);
|
|
5330
|
-
|
|
6637
|
+
const activityPushOpts = opts.activityPush ?? await loadApnsConfigFromFile();
|
|
6638
|
+
if (activityPushOpts) {
|
|
5331
6639
|
try {
|
|
5332
|
-
const activityChannel = new ActivityPushChannel(
|
|
6640
|
+
const activityChannel = new ActivityPushChannel(activityPushOpts);
|
|
5333
6641
|
notificationService.setActivityPushChannel(activityChannel);
|
|
5334
6642
|
console.log(`[Server] ${t("server.activityPushEnabled")}`);
|
|
5335
6643
|
} catch (err) {
|
|
@@ -5343,7 +6651,7 @@ async function start(opts = {}) {
|
|
|
5343
6651
|
let mdnsService = null;
|
|
5344
6652
|
const pairingManager = new PairingManager({
|
|
5345
6653
|
token,
|
|
5346
|
-
serverName: (0,
|
|
6654
|
+
serverName: (0, import_node_os9.hostname)(),
|
|
5347
6655
|
version: "0.2.0",
|
|
5348
6656
|
onStateChange: (state) => mdnsService?.updatePairingState(state)
|
|
5349
6657
|
});
|
|
@@ -5364,6 +6672,9 @@ async function start(opts = {}) {
|
|
|
5364
6672
|
notificationService.setGlobalPendingCountProvider(
|
|
5365
6673
|
() => approvalProxy.getPendingCount() + sessionManager.getAllPendingQuestions().length + unreadSessionIds.size
|
|
5366
6674
|
);
|
|
6675
|
+
notificationService.setPendingApprovalsProvider(
|
|
6676
|
+
(sessionId) => approvalProxy.getPendingRequestsForSession(sessionId)
|
|
6677
|
+
);
|
|
5367
6678
|
let cliCapabilities = null;
|
|
5368
6679
|
parseCliCapabilities().then((caps) => {
|
|
5369
6680
|
cliCapabilities = caps;
|
|
@@ -5372,6 +6683,49 @@ async function start(opts = {}) {
|
|
|
5372
6683
|
const broadcastUnreadSessions = () => {
|
|
5373
6684
|
wsBridge.broadcast({ type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
5374
6685
|
};
|
|
6686
|
+
scheduledManager = new ScheduledSessionManager({
|
|
6687
|
+
onFire: async (task) => {
|
|
6688
|
+
const p = task.payload;
|
|
6689
|
+
if (p.kind === "create") {
|
|
6690
|
+
await (0, import_promises7.mkdir)(p.projectPath, { recursive: true });
|
|
6691
|
+
const session = await sessionManager.createSession(
|
|
6692
|
+
p.projectPath,
|
|
6693
|
+
p.message,
|
|
6694
|
+
p.resumeSessionId,
|
|
6695
|
+
p.newSessionId,
|
|
6696
|
+
p.model,
|
|
6697
|
+
p.permissionMode,
|
|
6698
|
+
p.effort,
|
|
6699
|
+
void 0,
|
|
6700
|
+
p.agentType
|
|
6701
|
+
);
|
|
6702
|
+
wsBridge.broadcast({ type: "session_list", sessions: sessionManager.getActiveSessions() });
|
|
6703
|
+
return { sessionId: session.id };
|
|
6704
|
+
}
|
|
6705
|
+
const active = sessionManager.getActiveSessions().find((s) => s.id === p.sessionId);
|
|
6706
|
+
if (active) {
|
|
6707
|
+
await sessionManager.sendMessage(p.sessionId, p.message, p.permissionMode);
|
|
6708
|
+
} else {
|
|
6709
|
+
await sessionManager.createSession(
|
|
6710
|
+
p.projectPath,
|
|
6711
|
+
p.message,
|
|
6712
|
+
p.sessionId,
|
|
6713
|
+
void 0,
|
|
6714
|
+
void 0,
|
|
6715
|
+
p.permissionMode
|
|
6716
|
+
);
|
|
6717
|
+
}
|
|
6718
|
+
wsBridge.broadcast({ type: "session_list", sessions: sessionManager.getActiveSessions() });
|
|
6719
|
+
return { sessionId: p.sessionId };
|
|
6720
|
+
},
|
|
6721
|
+
onChange: (tasks) => {
|
|
6722
|
+
wsBridge.broadcast({ type: "scheduled_session_list", tasks });
|
|
6723
|
+
},
|
|
6724
|
+
onFired: (event) => {
|
|
6725
|
+
wsBridge.broadcast({ type: "scheduled_session_fired", ...event });
|
|
6726
|
+
}
|
|
6727
|
+
});
|
|
6728
|
+
await scheduledManager.load();
|
|
5375
6729
|
wsBridge.onConnection(async (ws) => {
|
|
5376
6730
|
const result = await getProjects();
|
|
5377
6731
|
if (result.ok) {
|
|
@@ -5391,14 +6745,17 @@ async function start(opts = {}) {
|
|
|
5391
6745
|
wsBridge.send(ws, { type: "unread_sessions", sessionIds: Array.from(unreadSessionIds) });
|
|
5392
6746
|
}
|
|
5393
6747
|
if (cliCapabilities) {
|
|
5394
|
-
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version });
|
|
6748
|
+
wsBridge.send(ws, { type: "cli_info", effortLevels: cliCapabilities.effortLevels, version: cliCapabilities.version, models: cliCapabilities.models });
|
|
6749
|
+
}
|
|
6750
|
+
if (scheduledManager) {
|
|
6751
|
+
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
5395
6752
|
}
|
|
5396
6753
|
});
|
|
5397
6754
|
wsBridge.onClientEvent(async (event, ws) => {
|
|
5398
6755
|
try {
|
|
5399
6756
|
switch (event.type) {
|
|
5400
6757
|
case "create_session": {
|
|
5401
|
-
await (0,
|
|
6758
|
+
await (0, import_promises7.mkdir)(event.projectPath, { recursive: true });
|
|
5402
6759
|
const resumeId = event.resumeSessionId ?? event.newSessionId;
|
|
5403
6760
|
if (resumeId) sessionFileWatcher.unwatch(resumeId);
|
|
5404
6761
|
await sessionManager.createSession(
|
|
@@ -5586,7 +6943,7 @@ async function start(opts = {}) {
|
|
|
5586
6943
|
if (!isStreaming) {
|
|
5587
6944
|
const filePath = getSessionFilePath(event.projectPath, event.sessionId);
|
|
5588
6945
|
try {
|
|
5589
|
-
const fileStat = await (0,
|
|
6946
|
+
const fileStat = await (0, import_promises8.stat)(filePath);
|
|
5590
6947
|
sessionFileWatcher.watch(event.sessionId, filePath, fileStat.size);
|
|
5591
6948
|
} catch {
|
|
5592
6949
|
}
|
|
@@ -5749,6 +7106,66 @@ async function start(opts = {}) {
|
|
|
5749
7106
|
xcodeBuildExecutor.killInstall(event.installId);
|
|
5750
7107
|
break;
|
|
5751
7108
|
}
|
|
7109
|
+
case "schedule_session": {
|
|
7110
|
+
if (!scheduledManager) break;
|
|
7111
|
+
const scheduledAt = Number(event.scheduledAt);
|
|
7112
|
+
if (!Number.isFinite(scheduledAt)) {
|
|
7113
|
+
wsBridge.send(ws, { type: "error", code: "INVALID_MESSAGE", message: "Invalid scheduledAt" });
|
|
7114
|
+
break;
|
|
7115
|
+
}
|
|
7116
|
+
scheduledManager.schedule(scheduledAt, event.payload);
|
|
7117
|
+
break;
|
|
7118
|
+
}
|
|
7119
|
+
case "cancel_scheduled_session": {
|
|
7120
|
+
scheduledManager?.cancel(event.id);
|
|
7121
|
+
break;
|
|
7122
|
+
}
|
|
7123
|
+
case "list_scheduled_sessions": {
|
|
7124
|
+
if (scheduledManager) {
|
|
7125
|
+
wsBridge.send(ws, { type: "scheduled_session_list", tasks: scheduledManager.list() });
|
|
7126
|
+
}
|
|
7127
|
+
break;
|
|
7128
|
+
}
|
|
7129
|
+
case "git_status": {
|
|
7130
|
+
const status = await gitExecutor.detectStatus(event.projectPath);
|
|
7131
|
+
wsBridge.send(ws, { type: "git_status_result", sessionId: event.sessionId, status });
|
|
7132
|
+
break;
|
|
7133
|
+
}
|
|
7134
|
+
case "git_commit": {
|
|
7135
|
+
await gitExecutor.commit(
|
|
7136
|
+
event.sessionId,
|
|
7137
|
+
event.projectPath,
|
|
7138
|
+
event.message,
|
|
7139
|
+
event.files,
|
|
7140
|
+
event.alsoPush
|
|
7141
|
+
);
|
|
7142
|
+
break;
|
|
7143
|
+
}
|
|
7144
|
+
case "git_push": {
|
|
7145
|
+
await gitExecutor.push(event.sessionId, event.projectPath);
|
|
7146
|
+
break;
|
|
7147
|
+
}
|
|
7148
|
+
case "list_project_commands": {
|
|
7149
|
+
try {
|
|
7150
|
+
const commands = await commandDiscovery.scan(event.projectPath, event.refresh ?? false);
|
|
7151
|
+
wsBridge.send(ws, {
|
|
7152
|
+
type: "commands_result",
|
|
7153
|
+
sessionId: event.sessionId,
|
|
7154
|
+
projectPath: event.projectPath,
|
|
7155
|
+
commands
|
|
7156
|
+
});
|
|
7157
|
+
} catch (err) {
|
|
7158
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7159
|
+
wsBridge.send(ws, {
|
|
7160
|
+
type: "commands_result",
|
|
7161
|
+
sessionId: event.sessionId,
|
|
7162
|
+
projectPath: event.projectPath,
|
|
7163
|
+
commands: [],
|
|
7164
|
+
error: message
|
|
7165
|
+
});
|
|
7166
|
+
}
|
|
7167
|
+
break;
|
|
7168
|
+
}
|
|
5752
7169
|
default: {
|
|
5753
7170
|
wsBridge.send(ws, {
|
|
5754
7171
|
type: "error",
|
|
@@ -5787,6 +7204,9 @@ async function start(opts = {}) {
|
|
|
5787
7204
|
xcodeBuildExecutor.onEvent((event) => {
|
|
5788
7205
|
wsBridge.broadcast(event);
|
|
5789
7206
|
});
|
|
7207
|
+
gitExecutor.onEvent((event) => {
|
|
7208
|
+
wsBridge.broadcast(event);
|
|
7209
|
+
});
|
|
5790
7210
|
wsBridge.onDisconnect(() => {
|
|
5791
7211
|
if (wsBridge.getConnectionCount() === 0 && approvalProxy.getPendingCount() > 0) {
|
|
5792
7212
|
approvalProxy.approveAll(t("server.phoneDisconnected"));
|
|
@@ -5804,14 +7224,12 @@ async function start(opts = {}) {
|
|
|
5804
7224
|
setTimeout(() => {
|
|
5805
7225
|
if (!approvalProxy.isPending(request.id)) return;
|
|
5806
7226
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
5807
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
5808
7227
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
5809
7228
|
notificationService.notifyApproval(request, pendingCount);
|
|
5810
7229
|
}, 5e3);
|
|
5811
7230
|
setTimeout(() => {
|
|
5812
7231
|
if (!approvalProxy.isPending(request.id)) return;
|
|
5813
7232
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
5814
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
5815
7233
|
console.log(`[Server] ${t("server.approvalRetry", { id: request.id })}`);
|
|
5816
7234
|
const pendingCount = approvalProxy.getPendingRequestsForSession(request.sessionId).length;
|
|
5817
7235
|
notificationService.notifyApproval(request, pendingCount);
|
|
@@ -5823,13 +7241,11 @@ async function start(opts = {}) {
|
|
|
5823
7241
|
setTimeout(() => {
|
|
5824
7242
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
5825
7243
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
5826
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
5827
7244
|
notificationService.notifyQuestion(request);
|
|
5828
7245
|
}, 5e3);
|
|
5829
7246
|
setTimeout(() => {
|
|
5830
7247
|
if (!sessionManager.isQuestionPending(request.id)) return;
|
|
5831
7248
|
if (wsBridge.isViewingSession(request.sessionId)) return;
|
|
5832
|
-
if (wsBridge.getConnectionCount() > 0) return;
|
|
5833
7249
|
console.log(`[Server] Question ${request.id} not answered in 60s, retrying push`);
|
|
5834
7250
|
notificationService.notifyQuestion(request);
|
|
5835
7251
|
}, 6e4);
|
|
@@ -5876,6 +7292,42 @@ async function start(opts = {}) {
|
|
|
5876
7292
|
console.error(`[Server] ${t("server.hookInstallFailed")}`, err);
|
|
5877
7293
|
console.log(`[Server] ${t("server.hookContinue")}`);
|
|
5878
7294
|
}
|
|
7295
|
+
const idleTimeoutMs = Number(process.env.SESSIX_IDLE_TIMEOUT_MS ?? 30 * 60 * 1e3);
|
|
7296
|
+
const idleSweepIntervalMs = Number(process.env.SESSIX_IDLE_SWEEP_INTERVAL_MS ?? 5 * 60 * 1e3);
|
|
7297
|
+
const maxActiveProcesses = Number(process.env.SESSIX_MAX_ACTIVE_PROCESSES ?? 15);
|
|
7298
|
+
let idleSweepTimer = null;
|
|
7299
|
+
if (idleSweepIntervalMs > 0 && (idleTimeoutMs > 0 || maxActiveProcesses > 0)) {
|
|
7300
|
+
idleSweepTimer = setInterval(async () => {
|
|
7301
|
+
try {
|
|
7302
|
+
let totalSwept = 0;
|
|
7303
|
+
const broadcastShrink = (sessionId) => {
|
|
7304
|
+
sessionManager.shrinkSessionBuffer(sessionId, 100);
|
|
7305
|
+
};
|
|
7306
|
+
for (const agentType of ["claude-code", "codex"]) {
|
|
7307
|
+
const provider = providerFactory.getProvider(agentType);
|
|
7308
|
+
if (idleTimeoutMs > 0 && typeof provider.sweepIdleProcesses === "function") {
|
|
7309
|
+
const swept = await provider.sweepIdleProcesses(idleTimeoutMs);
|
|
7310
|
+
swept.forEach(broadcastShrink);
|
|
7311
|
+
totalSwept += swept.length;
|
|
7312
|
+
}
|
|
7313
|
+
if (maxActiveProcesses > 0 && typeof provider.sweepLruProcesses === "function") {
|
|
7314
|
+
const swept = await provider.sweepLruProcesses(maxActiveProcesses);
|
|
7315
|
+
swept.forEach(broadcastShrink);
|
|
7316
|
+
totalSwept += swept.length;
|
|
7317
|
+
}
|
|
7318
|
+
}
|
|
7319
|
+
if (totalSwept > 0) {
|
|
7320
|
+
console.log(`[Server] Idle GC: swept ${totalSwept} idle session(s)`);
|
|
7321
|
+
wsBridge.broadcast({
|
|
7322
|
+
type: "session_list",
|
|
7323
|
+
sessions: sessionManager.getActiveSessions()
|
|
7324
|
+
});
|
|
7325
|
+
}
|
|
7326
|
+
} catch (err) {
|
|
7327
|
+
console.error("[Server] Idle GC failed:", err);
|
|
7328
|
+
}
|
|
7329
|
+
}, idleSweepIntervalMs);
|
|
7330
|
+
}
|
|
5879
7331
|
const stop = async () => {
|
|
5880
7332
|
console.log(`[Server] ${t("server.shuttingDown")}`);
|
|
5881
7333
|
const errors = [];
|
|
@@ -5887,6 +7339,7 @@ async function start(opts = {}) {
|
|
|
5887
7339
|
errors.push(err);
|
|
5888
7340
|
}
|
|
5889
7341
|
};
|
|
7342
|
+
if (idleSweepTimer) clearInterval(idleSweepTimer);
|
|
5890
7343
|
await attempt(() => authManager.destroy(), "AuthManager");
|
|
5891
7344
|
await attempt(() => stopMdns(), "mDNS");
|
|
5892
7345
|
await attempt(() => pairingManager.destroy(), "PairingManager");
|
|
@@ -5897,6 +7350,7 @@ async function start(opts = {}) {
|
|
|
5897
7350
|
await attempt(() => xcodeBuildExecutor.destroy(), "XcodeBuildExecutor");
|
|
5898
7351
|
await attempt(() => notificationService.destroy(), "NotificationService");
|
|
5899
7352
|
await attempt(() => sessionFileWatcher.destroy(), "SessionFileWatcher");
|
|
7353
|
+
await attempt(() => scheduledManager?.destroy(), "ScheduledSessionManager");
|
|
5900
7354
|
if (errors.length > 0) {
|
|
5901
7355
|
console.error(`[Server] ${t("server.shutdownWithErrors", { count: errors.length })}`);
|
|
5902
7356
|
throw errors[0];
|
|
@@ -5923,9 +7377,9 @@ async function start(opts = {}) {
|
|
|
5923
7377
|
openPairing: (duration) => pairingManager.open(duration),
|
|
5924
7378
|
closePairing: () => pairingManager.close(),
|
|
5925
7379
|
regenerateToken: async () => {
|
|
5926
|
-
const newToken = (0,
|
|
5927
|
-
await (0,
|
|
5928
|
-
await (0,
|
|
7380
|
+
const newToken = (0, import_uuid9.v4)();
|
|
7381
|
+
await (0, import_promises7.mkdir)(configDir, { recursive: true });
|
|
7382
|
+
await (0, import_promises7.writeFile)(tokenFile, newToken, "utf8");
|
|
5929
7383
|
instance.token = newToken;
|
|
5930
7384
|
wsBridge.updateToken(newToken);
|
|
5931
7385
|
approvalProxy.updateToken(newToken);
|