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