@keyou007/agent-bot 0.1.24-alpha.5 → 0.1.24-alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +27 -3
- package/README.zh.md +27 -3
- package/SECURITY.md +12 -0
- package/config.example.yaml +5 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/serverStatus.js +1 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexRuntime.js +202 -115
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/codex/ProviderSwitch.js +12 -13
- package/dist/codex/ProviderSwitch.js.map +1 -1
- package/dist/config/AgentExecutionDefaultsStore.js +4 -0
- package/dist/config/AgentExecutionDefaultsStore.js.map +1 -1
- package/dist/config/loadConfig.js +4 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +3 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +66 -8
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +1 -0
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +121 -17
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +43 -6
- package/dist/index.js.map +1 -1
- package/dist/local-files/LocalFileViewerServer.js +51 -4
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +7 -2
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +41 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +615 -214
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/proxy/SessionOperationQueue.js +14 -0
- package/dist/proxy/SessionOperationQueue.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +4 -2
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/state/StateStore.js +22 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/TurnPreviewJournal.js +19 -7
- package/dist/state/TurnPreviewJournal.js.map +1 -1
- package/dist/state/migrations.js +6 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +19 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/terminal/TerminalCards.js +13 -0
- package/dist/terminal/TerminalCards.js.map +1 -0
- package/dist/terminal/TerminalLinks.js +76 -0
- package/dist/terminal/TerminalLinks.js.map +1 -0
- package/dist/terminal/TerminalPage.js +212 -0
- package/dist/terminal/TerminalPage.js.map +1 -0
- package/dist/terminal/TerminalProcess.js +71 -0
- package/dist/terminal/TerminalProcess.js.map +1 -0
- package/dist/terminal/TerminalServer.js +639 -0
- package/dist/terminal/TerminalServer.js.map +1 -0
- package/docs/technical-reference.md +48 -0
- package/docs/technical-reference.zh.md +48 -0
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +9 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/CodexRuntime.ts +200 -123
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/config/AgentExecutionDefaultsStore.ts +2 -0
- package/src/config/loadConfig.ts +4 -0
- package/src/config/schema.ts +5 -1
- package/src/feishu/CardRenderer.ts +81 -8
- package/src/feishu/FeishuConnector.ts +1 -0
- package/src/feishu/FeishuTurnPresenter.ts +108 -16
- package/src/feishu/types.ts +1 -0
- package/src/index.ts +44 -7
- package/src/local-files/LocalFileViewerServer.ts +48 -4
- package/src/local-files/TurnPreviewPage.ts +7 -2
- package/src/presentation/TurnStateReducer.ts +46 -7
- package/src/presentation/turnViewTypes.ts +5 -0
- package/src/proxy/ProxySessionController.ts +625 -211
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +8 -3
- package/src/state/StateStore.ts +38 -1
- package/src/state/TurnPreviewJournal.ts +19 -7
- package/src/state/migrations.ts +6 -0
- package/src/supervision/SafeRestartNotifier.ts +1 -0
- package/src/supervision/SafeRestartScheduler.ts +18 -6
- package/src/terminal/TerminalCards.ts +18 -0
- package/src/terminal/TerminalLinks.ts +61 -0
- package/src/terminal/TerminalPage.ts +213 -0
- package/src/terminal/TerminalProcess.ts +71 -0
- package/src/terminal/TerminalServer.ts +513 -0
package/src/config/schema.ts
CHANGED
|
@@ -57,6 +57,9 @@ export const agentConfigSchema = z.object({
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
export const appConfigSchema = z.object({
|
|
60
|
+
terminal: z.object({
|
|
61
|
+
enabled: z.boolean().default(true),
|
|
62
|
+
}).default({ enabled: true }),
|
|
60
63
|
updates: z.object({ enabled: z.boolean().default(true) }).default({ enabled: true }),
|
|
61
64
|
console: z
|
|
62
65
|
.object({
|
|
@@ -108,6 +111,7 @@ export type AgentConfig = Omit<ParsedAgentConfig, "defaults"> & {
|
|
|
108
111
|
defaults?: AgentExecutionDefaults;
|
|
109
112
|
};
|
|
110
113
|
export type GroupNameFormatConfig = z.infer<typeof groupNameFormatSchema>;
|
|
111
|
-
export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates"> & {
|
|
114
|
+
export type AppConfig = Omit<z.infer<typeof appConfigSchema>, "updates" | "terminal"> & {
|
|
112
115
|
updates?: { enabled: boolean };
|
|
116
|
+
terminal?: z.infer<typeof appConfigSchema>["terminal"];
|
|
113
117
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
+
import { approvalActivityId } from "../presentation/TurnStateReducer.js";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import type { JsonValue } from "../acp/acpTypes.js";
|
|
4
5
|
import type { RuntimeSession } from "../acp/AcpSessionManager.js";
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
15
16
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
16
17
|
import type { UpdateNotice } from "../updates/types.js";
|
|
18
|
+
import type { TerminalCardView } from "../terminal/TerminalServer.js";
|
|
17
19
|
import { localCardImage } from "./LocalCardImage.js";
|
|
18
20
|
|
|
19
21
|
export interface StartupStatusView {
|
|
@@ -58,6 +60,7 @@ export interface InitializationWelcomeView {
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
export interface SafeRestartStatusView {
|
|
63
|
+
activeTerminals?: number;
|
|
61
64
|
scheduleId: number;
|
|
62
65
|
reason: string;
|
|
63
66
|
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
|
|
@@ -85,6 +88,7 @@ export interface ModelSelectorCardView {
|
|
|
85
88
|
sessionId: string;
|
|
86
89
|
contextKey: string;
|
|
87
90
|
currentModel?: string;
|
|
91
|
+
modelPage?: number;
|
|
88
92
|
reasoningEffort?: string;
|
|
89
93
|
models: ModelOption[];
|
|
90
94
|
modelProvider?: string;
|
|
@@ -131,7 +135,7 @@ export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" |
|
|
|
131
135
|
|
|
132
136
|
const EXECUTION_SETTING_EFFECTS: Record<ExecutionSettingsTab, string> = {
|
|
133
137
|
agent: "只影响新建任务,不切换当前任务。",
|
|
134
|
-
provider: "
|
|
138
|
+
provider: "只影响当前任务;运行中选择将在本轮结束后生效。",
|
|
135
139
|
model: "只影响当前任务,下一轮生效。",
|
|
136
140
|
thinking: "只影响当前任务,下一轮生效。",
|
|
137
141
|
permission: "只影响当前任务,下一轮生效。",
|
|
@@ -143,6 +147,8 @@ export interface ExecutionSettingsAgentOption {
|
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
export interface ExecutionSettingsCardView {
|
|
150
|
+
pendingNotice?: string;
|
|
151
|
+
pendingFailed?: boolean;
|
|
146
152
|
sessionId?: string;
|
|
147
153
|
contextKey: string;
|
|
148
154
|
activeTab: ExecutionSettingsTab;
|
|
@@ -152,6 +158,7 @@ export interface ExecutionSettingsCardView {
|
|
|
152
158
|
runtimeSettingsAvailable: boolean;
|
|
153
159
|
currentProvider?: string;
|
|
154
160
|
currentModel?: string;
|
|
161
|
+
modelPage?: number;
|
|
155
162
|
currentEffort?: string;
|
|
156
163
|
currentPermissionMode: PermissionMode;
|
|
157
164
|
providers: ModelProviderOption[];
|
|
@@ -263,6 +270,7 @@ export interface DismissGroupCardView {
|
|
|
263
270
|
}
|
|
264
271
|
|
|
265
272
|
export interface ShellCommandCardView {
|
|
273
|
+
terminalUrl?: string;
|
|
266
274
|
jobId?: string;
|
|
267
275
|
contextKey?: string;
|
|
268
276
|
command: string;
|
|
@@ -508,6 +516,9 @@ export class CardRenderer {
|
|
|
508
516
|
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
509
517
|
].join("\n")),
|
|
510
518
|
];
|
|
519
|
+
if (view.terminalUrl) {
|
|
520
|
+
elements.push(markdown(`[Open Terminal](${view.terminalUrl}) · 独立终端,需在本会话由所有者授权`));
|
|
521
|
+
}
|
|
511
522
|
if (view.status === "running" && view.jobId && view.contextKey) {
|
|
512
523
|
elements.push({
|
|
513
524
|
tag: "button",
|
|
@@ -553,6 +564,7 @@ export class CardRenderer {
|
|
|
553
564
|
`**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
|
|
554
565
|
]
|
|
555
566
|
: []),
|
|
567
|
+
...(view.pendingNotice ? [view.pendingNotice] : []),
|
|
556
568
|
...(view.notice ? [view.notice] : []),
|
|
557
569
|
...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
|
|
558
570
|
].join("\n")),
|
|
@@ -584,7 +596,8 @@ export class CardRenderer {
|
|
|
584
596
|
inlineCode(provider.id),
|
|
585
597
|
provider.isDefault ? " · 默认" : "",
|
|
586
598
|
].join(""),
|
|
587
|
-
current: provider.id === view.currentProvider,
|
|
599
|
+
current: provider.id === view.currentProvider && !view.pendingFailed,
|
|
600
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
588
601
|
action: {
|
|
589
602
|
text: "Switch",
|
|
590
603
|
value: {
|
|
@@ -599,18 +612,24 @@ export class CardRenderer {
|
|
|
599
612
|
if (view.models.length === 0) {
|
|
600
613
|
elements.push(markdown("当前运行时未返回可用模型。"));
|
|
601
614
|
} else {
|
|
602
|
-
|
|
615
|
+
const page = paginateModels(view.models, view.currentModel, view.modelPage, {
|
|
616
|
+
action: "settings_tab_open", ...baseAction, tab: "model", provider: view.currentProvider ?? "",
|
|
617
|
+
});
|
|
618
|
+
elements.push(...page.models.map((model) => settingsOptionRow({
|
|
603
619
|
label: `${inlineCode(model.id)}${model.isDefault ? " · 默认" : ""}`,
|
|
604
|
-
current: model.id === view.currentModel,
|
|
620
|
+
current: model.id === view.currentModel && !view.pendingFailed,
|
|
621
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
605
622
|
action: {
|
|
606
623
|
text: "Switch",
|
|
607
624
|
value: {
|
|
608
625
|
action: "settings_model_select",
|
|
609
626
|
...baseAction,
|
|
627
|
+
provider: view.currentProvider ?? "",
|
|
610
628
|
model: model.id,
|
|
611
629
|
},
|
|
612
630
|
},
|
|
613
631
|
})));
|
|
632
|
+
elements.push(...page.navigation);
|
|
614
633
|
}
|
|
615
634
|
} else if (view.activeTab === "thinking") {
|
|
616
635
|
if (!view.currentModel) {
|
|
@@ -622,11 +641,13 @@ export class CardRenderer {
|
|
|
622
641
|
elements.push(...view.reasoningOptions.map((option) => settingsOptionRow({
|
|
623
642
|
label: inlineCode(option.value),
|
|
624
643
|
current: option.value === view.currentEffort,
|
|
644
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
625
645
|
action: {
|
|
626
646
|
text: "Switch",
|
|
627
647
|
value: {
|
|
628
648
|
action: "settings_thinking_select",
|
|
629
649
|
...baseAction,
|
|
650
|
+
provider: view.currentProvider ?? "",
|
|
630
651
|
model: currentModel,
|
|
631
652
|
effort: option.value,
|
|
632
653
|
},
|
|
@@ -637,6 +658,7 @@ export class CardRenderer {
|
|
|
637
658
|
elements.push(...(["auto", "confirm"] as PermissionMode[]).map((mode) => settingsOptionRow({
|
|
638
659
|
label: `${inlineCode(mode)} · ${permissionModeLabel(mode)}`,
|
|
639
660
|
current: mode === view.currentPermissionMode,
|
|
661
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
640
662
|
action: {
|
|
641
663
|
text: "Switch",
|
|
642
664
|
value: {
|
|
@@ -649,7 +671,7 @@ export class CardRenderer {
|
|
|
649
671
|
}
|
|
650
672
|
|
|
651
673
|
elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
|
|
652
|
-
return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
|
|
674
|
+
return sectionCard("运行设置", elements, view.warning || view.pendingFailed ? "orange" : view.notice ? "green" : "blue");
|
|
653
675
|
}
|
|
654
676
|
|
|
655
677
|
renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
|
|
@@ -785,7 +807,14 @@ export class CardRenderer {
|
|
|
785
807
|
if (view.models.length === 0) {
|
|
786
808
|
elements.push(markdown("当前运行时未返回可用模型。"));
|
|
787
809
|
} else {
|
|
788
|
-
|
|
810
|
+
const page = paginateModels(view.models, view.currentModel, view.modelPage, {
|
|
811
|
+
action: view.unifiedSettings ? "provider_model_open" : "model_open",
|
|
812
|
+
sessionId: view.sessionId,
|
|
813
|
+
contextKey: view.contextKey,
|
|
814
|
+
...(view.modelProvider ? { provider: view.modelProvider } : {}),
|
|
815
|
+
...(view.permissionMode ? { permissionMode: view.permissionMode } : {}),
|
|
816
|
+
});
|
|
817
|
+
elements.push(...page.models.map((model) => {
|
|
789
818
|
const isCurrent = model.id === view.currentModel;
|
|
790
819
|
return {
|
|
791
820
|
tag: "column_set",
|
|
@@ -823,6 +852,7 @@ export class CardRenderer {
|
|
|
823
852
|
],
|
|
824
853
|
};
|
|
825
854
|
}));
|
|
855
|
+
elements.push(...page.navigation);
|
|
826
856
|
}
|
|
827
857
|
if (view.unifiedSettings) {
|
|
828
858
|
elements.push(
|
|
@@ -994,6 +1024,24 @@ export class CardRenderer {
|
|
|
994
1024
|
view.status === "completed" ? "green" : view.status === "cancelled" ? "grey" : "blue");
|
|
995
1025
|
}
|
|
996
1026
|
|
|
1027
|
+
renderTerminalAuthorization(view: TerminalCardView): Record<string, unknown> {
|
|
1028
|
+
const action = (decision: string) => ({ action: "terminal_authorize", contextKey: view.contextKey, requestId: view.requestId, token: view.approvalToken, decision });
|
|
1029
|
+
const elements: Record<string, unknown>[] = [markdown(`**目录**:${inlineCode(view.cwd)}\n**终端**:${inlineCode(view.reopenToken)}`)];
|
|
1030
|
+
if (view.state === "pending") {
|
|
1031
|
+
elements.push(markdown(`**验证码**:${inlineCode(view.code)}\n仅 AgentBot 所有者可授权。授权后,该浏览器可在本机执行任意命令,拥有 AgentBot 的系统用户权限。请核对网页验证码,仅批准本人发起的请求。5 分钟内有效。${view.recovery ? (view.shellStarted ? "恢复原 Shell,不创建新终端;授权后旧网页失去访问权限。拒绝不影响原终端。" : "继续打开同一个终端;授权后旧网页失去访问权限,Shell 在连接成功后启动。拒绝不影响原授权。") : "不执行原卡片命令。"}`),
|
|
1032
|
+
taskActionRow([{ text: "Allow", value: action("allow") }, { text: "Deny", type: "danger", value: action("deny") }, { text: "刷新", value: action("refresh") }]));
|
|
1033
|
+
} else if (view.state === "closed") {
|
|
1034
|
+
elements.push(markdown(`**状态**:${view.message ? escapeCardHtml(view.message) : "已关闭"}\n原终端已结束时,请从命令卡片创建新终端。`));
|
|
1035
|
+
} else {
|
|
1036
|
+
const waiting = view.state === "approved";
|
|
1037
|
+
const label = waiting ? "打开终端" : "重新打开终端";
|
|
1038
|
+
elements.push(markdown(`**状态**:${waiting ? "已授权,等待网页连接" : view.connected ? "已连接" : "网页已断开,Shell 仍在运行"}\n${waiting ? "请返回原网页连接;原网页已关闭时,点击下方“打开终端”。Shell 尚未启动。" : "点击下方“重新打开终端”恢复原 Shell。前台连接时不因空闲关闭;后台空闲 30 分钟或网页断开 3 分钟后会自动关闭 Shell,成功重连取消断开计时并重置空闲计时;“关闭终端”会中断其中的命令。"}`));
|
|
1039
|
+
elements.push(markdown(`[${label}](${view.reopenUrl}) · 新网页需核对验证码重新授权${waiting ? ",连接成功后启动 Shell" : ",恢复原 Shell"}`));
|
|
1040
|
+
elements.push(taskActionRow([{ text: "关闭终端", type: "danger", value: action("close") }, { text: "刷新", value: action("refresh") }]));
|
|
1041
|
+
}
|
|
1042
|
+
return sectionCard(view.state === "pending" ? "网页终端授权" : "网页终端管理", elements, view.state === "closed" ? "grey" : view.state === "pending" ? "orange" : "blue");
|
|
1043
|
+
}
|
|
1044
|
+
|
|
997
1045
|
renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
|
|
998
1046
|
const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
|
|
999
1047
|
const countdown = view.phase === "countdown"
|
|
@@ -1021,6 +1069,7 @@ export class CardRenderer {
|
|
|
1021
1069
|
`**重启原因**:${inlineCode(view.reason)}`,
|
|
1022
1070
|
`**重启倒计时**:${countdown}`,
|
|
1023
1071
|
`**待投递结果**:${view.pendingFinalDeliveries} 条`,
|
|
1072
|
+
...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals} 个,请在网页或原会话的终端管理卡中关闭。ForceRestart 会中断终端。`] : []),
|
|
1024
1073
|
];
|
|
1025
1074
|
const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
|
|
1026
1075
|
if (view.waitingTasks.length > 0) {
|
|
@@ -1782,6 +1831,10 @@ function renderApprovalElements(state: TurnViewState, request: ApprovalRequest):
|
|
|
1782
1831
|
request.reason,
|
|
1783
1832
|
].filter(Boolean).join("\n")));
|
|
1784
1833
|
}
|
|
1834
|
+
if (request.automatic) {
|
|
1835
|
+
elements.push(markdown("自动执行模式:正在自动同意。"));
|
|
1836
|
+
return elements;
|
|
1837
|
+
}
|
|
1785
1838
|
elements.push({
|
|
1786
1839
|
tag: "column_set",
|
|
1787
1840
|
flex_mode: "flow",
|
|
@@ -2246,7 +2299,8 @@ function removeMarkdownBold(value: string): string {
|
|
|
2246
2299
|
}
|
|
2247
2300
|
|
|
2248
2301
|
function turnActivities(state: TurnViewState): TurnActivity[] {
|
|
2249
|
-
if (state.activities?.length) return state.
|
|
2302
|
+
if (state.activities?.length) return state.approval
|
|
2303
|
+
? state.activities.filter((a) => a.id !== approvalActivityId(state.turnId, state.approval!.id)) : state.activities;
|
|
2250
2304
|
|
|
2251
2305
|
const activities: TurnActivity[] = [];
|
|
2252
2306
|
if (state.progressText) {
|
|
@@ -2845,6 +2899,24 @@ function acpPermissionOptionLabel(option: { name: string; kind: string }): strin
|
|
|
2845
2899
|
return labels[option.kind] ?? (/^[\x20-\x7E]+$/.test(option.name) ? option.name : "Select");
|
|
2846
2900
|
}
|
|
2847
2901
|
|
|
2902
|
+
const MODEL_PAGE_SIZE = 20;
|
|
2903
|
+
|
|
2904
|
+
function paginateModels(
|
|
2905
|
+
models: ModelOption[],
|
|
2906
|
+
currentModel: string | undefined,
|
|
2907
|
+
requestedPage: number | undefined,
|
|
2908
|
+
action: Record<string, string>,
|
|
2909
|
+
): { models: ModelOption[]; navigation: Record<string, unknown>[] } {
|
|
2910
|
+
const totalPages = Math.max(1, Math.ceil(models.length / MODEL_PAGE_SIZE));
|
|
2911
|
+
const currentPage = Math.floor(models.findIndex((model) => model.id === currentModel) / MODEL_PAGE_SIZE);
|
|
2912
|
+
const page = Math.max(0, Math.min(requestedPage ?? currentPage, totalPages - 1));
|
|
2913
|
+
const navigation = totalPages > 1 ? [taskActionRow([
|
|
2914
|
+
...(page > 0 ? [{ text: "上一页", value: { ...action, page: String(page - 1) } }] : []),
|
|
2915
|
+
...(page < totalPages - 1 ? [{ text: "下一页", value: { ...action, page: String(page + 1) } }] : []),
|
|
2916
|
+
], `第 ${page + 1}/${totalPages} 页 · 共 ${models.length} 个模型`)] : [];
|
|
2917
|
+
return { models: models.slice(page * MODEL_PAGE_SIZE, (page + 1) * MODEL_PAGE_SIZE), navigation };
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2848
2920
|
function settingsTabRow(
|
|
2849
2921
|
activeTab: ExecutionSettingsTab,
|
|
2850
2922
|
baseAction: Record<string, string>,
|
|
@@ -2918,6 +2990,7 @@ function settingsTabElement(
|
|
|
2918
2990
|
function settingsOptionRow(input: {
|
|
2919
2991
|
label: string;
|
|
2920
2992
|
current: boolean;
|
|
2993
|
+
currentLabel?: string;
|
|
2921
2994
|
action: TaskListCardAction;
|
|
2922
2995
|
}): Record<string, unknown> {
|
|
2923
2996
|
return {
|
|
@@ -2937,7 +3010,7 @@ function settingsOptionRow(input: {
|
|
|
2937
3010
|
tag: "column",
|
|
2938
3011
|
width: "auto",
|
|
2939
3012
|
vertical_align: "center",
|
|
2940
|
-
elements: input.current ? [markdown("
|
|
3013
|
+
elements: input.current ? [markdown(`✅ ${input.currentLabel ?? "当前"}`)] : [taskActionElement(input.action)],
|
|
2941
3014
|
},
|
|
2942
3015
|
],
|
|
2943
3016
|
};
|
|
@@ -262,6 +262,7 @@ function toIncomingMessage(
|
|
|
262
262
|
const messageId = message.message_id ?? `${Date.now()}`;
|
|
263
263
|
return {
|
|
264
264
|
messageId,
|
|
265
|
+
...(typeof messageType === "string" ? { messageType } : {}),
|
|
265
266
|
...(Number.isFinite(Number(message.create_time)) && Number(message.create_time) > 0
|
|
266
267
|
? { createdAt: Number(message.create_time) } : {}),
|
|
267
268
|
contextKey: chatId
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
appendSteerMessage as appendSteerMessageToState,
|
|
5
5
|
createTurnViewState,
|
|
6
6
|
compactTurnView,
|
|
7
|
+
continueTurnView,
|
|
7
8
|
reduceTurnEvent,
|
|
8
9
|
} from "../presentation/TurnStateReducer.js";
|
|
9
10
|
import { splitMarkdown } from "../presentation/splitMarkdown.js";
|
|
@@ -74,6 +75,7 @@ interface TurnEntry {
|
|
|
74
75
|
elapsedTimer?: ReturnType<typeof setInterval>;
|
|
75
76
|
finalizing?: Promise<void>;
|
|
76
77
|
lastFinalizedStatus?: TurnViewState["status"];
|
|
78
|
+
continuing?: boolean;
|
|
77
79
|
}
|
|
78
80
|
|
|
79
81
|
export class FeishuTurnPresenter {
|
|
@@ -158,6 +160,13 @@ export class FeishuTurnPresenter {
|
|
|
158
160
|
const existing = this.pendingEntries.get(sessionId);
|
|
159
161
|
if (existing) {
|
|
160
162
|
await existing.initializing;
|
|
163
|
+
if (existing.continuing) {
|
|
164
|
+
existing.state = { ...existing.state, status: "running", error: undefined, completedAt: undefined, durationMs: undefined };
|
|
165
|
+
this.persist(existing.state.turnId, sessionId, existing.state, existing.contextKey);
|
|
166
|
+
existing.scheduler?.update(existing.state, "critical");
|
|
167
|
+
this.startElapsedUpdates(existing);
|
|
168
|
+
return existing.state.turnId;
|
|
169
|
+
}
|
|
161
170
|
const images = localImagePaths === undefined ? existing.state.promptImagePaths : [...new Set(localImagePaths)];
|
|
162
171
|
if ((prompt && existing.state.prompt !== prompt) || JSON.stringify(images) !== JSON.stringify(existing.state.promptImagePaths)) {
|
|
163
172
|
existing.state = { ...existing.state, ...(prompt ? { prompt } : {}), ...(images ? { promptImagePaths: images } : {}) };
|
|
@@ -204,15 +213,15 @@ export class FeishuTurnPresenter {
|
|
|
204
213
|
): Promise<boolean> {
|
|
205
214
|
const entry = this.pendingEntries.get(sessionId);
|
|
206
215
|
if (!entry) return false;
|
|
207
|
-
this.pendingEntries.delete(sessionId);
|
|
208
|
-
this.stopElapsedUpdates(entry);
|
|
216
|
+
if (!entry.continuing) this.pendingEntries.delete(sessionId);
|
|
217
|
+
if (!entry.continuing) this.stopElapsedUpdates(entry);
|
|
209
218
|
const completedAt = Date.now();
|
|
210
219
|
entry.state = {
|
|
211
220
|
...entry.state,
|
|
212
|
-
status: "failed",
|
|
221
|
+
status: entry.continuing ? "running" : "failed",
|
|
213
222
|
error: message,
|
|
214
|
-
completedAt,
|
|
215
|
-
durationMs: Math.max(0, completedAt - entry.state.startedAt),
|
|
223
|
+
completedAt: entry.continuing ? undefined : completedAt,
|
|
224
|
+
durationMs: entry.continuing ? undefined : Math.max(0, completedAt - entry.state.startedAt),
|
|
216
225
|
};
|
|
217
226
|
this.persist(entry.state.turnId, sessionId, entry.state, entry.contextKey);
|
|
218
227
|
await entry.initializing;
|
|
@@ -298,7 +307,7 @@ export class FeishuTurnPresenter {
|
|
|
298
307
|
status: "running" as const,
|
|
299
308
|
};
|
|
300
309
|
this.store.previews?.seed(initial);
|
|
301
|
-
this.store.previews?.user(turnId, activityId, text, localImagePaths);
|
|
310
|
+
this.store.previews?.user(initial.previewTurnId ?? turnId, activityId, text, localImagePaths);
|
|
302
311
|
const state = this.cardState(appendSteerMessageToState(initial, activityId, text, localImagePaths));
|
|
303
312
|
entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
|
|
304
313
|
this.entries.set(turnId, entry);
|
|
@@ -309,7 +318,7 @@ export class FeishuTurnPresenter {
|
|
|
309
318
|
}
|
|
310
319
|
|
|
311
320
|
await entry.initializing;
|
|
312
|
-
this.store.previews?.user(turnId, activityId, text, localImagePaths);
|
|
321
|
+
this.store.previews?.user(entry.state.previewTurnId ?? turnId, activityId, text, localImagePaths);
|
|
313
322
|
entry.state = this.cardState(appendSteerMessageToState(entry.state, activityId, text, localImagePaths));
|
|
314
323
|
this.persist(turnId, sessionId, entry.state, entry.contextKey);
|
|
315
324
|
if (!entry.historySnapshot) entry.scheduler?.update(entry.state, "critical");
|
|
@@ -317,10 +326,50 @@ export class FeishuTurnPresenter {
|
|
|
317
326
|
|
|
318
327
|
async onEvent(event: AgentEvent): Promise<void> {
|
|
319
328
|
let existing = this.entries.get(event.turnId);
|
|
329
|
+
if (!existing) {
|
|
330
|
+
const saved = this.store.getTurnSnapshot(event.turnId);
|
|
331
|
+
if (isTurnViewState(saved) && saved.continuedToTurnId) return;
|
|
332
|
+
}
|
|
320
333
|
let eventApplied = false;
|
|
334
|
+
if (event.type === "turn_completed" && event.approvedPlan) {
|
|
335
|
+
const entry = existing ?? this.createEntry({ type: "progress", sessionId: event.sessionId,
|
|
336
|
+
turnId: event.turnId, activityId: "plan-handoff", text: "方案已确认,继续执行" });
|
|
337
|
+
if (!entry || entry.continuing) return;
|
|
338
|
+
entry.continuing = true;
|
|
339
|
+
entry.historySnapshot = undefined;
|
|
340
|
+
this.store.previews?.seed(entry.state);
|
|
341
|
+
entry.state = { ...entry.state, status: "running", approval: undefined };
|
|
342
|
+
this.pendingEntries.set(event.sessionId, entry);
|
|
343
|
+
this.persist(event.turnId, event.sessionId, entry.state, entry.contextKey);
|
|
344
|
+
await entry.initializing;
|
|
345
|
+
await entry.scheduler?.flush(entry.state);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
321
348
|
if (!existing && event.type === "turn_started") {
|
|
322
349
|
const pending = this.pendingEntries.get(event.sessionId);
|
|
323
|
-
if (pending) {
|
|
350
|
+
if (pending?.continuing) {
|
|
351
|
+
const previous = pending.state;
|
|
352
|
+
const previewTurnId = previous.previewTurnId ?? previous.turnId;
|
|
353
|
+
this.flushPersistence();
|
|
354
|
+
const state = continueTurnView(previous, event.turnId, event.startedAt);
|
|
355
|
+
this.store.previews?.handoff(previewTurnId, event.turnId, event.startedAt);
|
|
356
|
+
this.store.saveTurnSnapshot(previous.turnId, event.sessionId, {
|
|
357
|
+
...previous, status: "completed", completedAt: event.startedAt,
|
|
358
|
+
durationMs: Math.max(0, event.startedAt - previous.startedAt),
|
|
359
|
+
finalResponse: "", continuedToTurnId: event.turnId, previewTurnId,
|
|
360
|
+
}, pending.contextKey);
|
|
361
|
+
if (pending.messageId) this.store.saveTurnDelivery(event.turnId, { progressMessageId: pending.messageId });
|
|
362
|
+
this.entries.delete(previous.turnId);
|
|
363
|
+
this.pendingEntries.delete(event.sessionId);
|
|
364
|
+
pending.state = state;
|
|
365
|
+
pending.continuing = false;
|
|
366
|
+
pending.lastFinalizedStatus = undefined;
|
|
367
|
+
pending.historySnapshot = undefined;
|
|
368
|
+
this.entries.set(event.turnId, pending);
|
|
369
|
+
this.persist(event.turnId, event.sessionId, state, pending.contextKey);
|
|
370
|
+
existing = pending;
|
|
371
|
+
eventApplied = true;
|
|
372
|
+
} else if (pending) {
|
|
324
373
|
const pendingTurnId = pending.state.turnId;
|
|
325
374
|
const state = reduceTurnEvent(
|
|
326
375
|
createTurnViewState(
|
|
@@ -362,8 +411,9 @@ export class FeishuTurnPresenter {
|
|
|
362
411
|
if (!entry) return;
|
|
363
412
|
|
|
364
413
|
if (existing && !eventApplied) {
|
|
414
|
+
event = presentationEvent(event, entry.state);
|
|
365
415
|
this.store.previews?.seed(entry.state);
|
|
366
|
-
this.store.previews?.event(event);
|
|
416
|
+
this.store.previews?.event(event, entry.state.previewTurnId);
|
|
367
417
|
const previousStatus = entry.state.status;
|
|
368
418
|
entry.state = this.cardState(reduceTurnEvent(entry.state, event));
|
|
369
419
|
if (isTerminalViewStatus(previousStatus) && !event.type.startsWith("turn_")) entry.state.status = previousStatus;
|
|
@@ -374,6 +424,8 @@ export class FeishuTurnPresenter {
|
|
|
374
424
|
if (!entry.scheduler) return;
|
|
375
425
|
|
|
376
426
|
if (isTerminalEvent(event)) {
|
|
427
|
+
if (this.pendingEntries.get(event.sessionId) === entry) this.pendingEntries.delete(event.sessionId);
|
|
428
|
+
entry.continuing = false;
|
|
377
429
|
this.stopElapsedUpdates(entry);
|
|
378
430
|
if (entry.finalizing) await entry.finalizing;
|
|
379
431
|
const terminalState = entry.state;
|
|
@@ -394,7 +446,8 @@ export class FeishuTurnPresenter {
|
|
|
394
446
|
}
|
|
395
447
|
|
|
396
448
|
async showDetails(contextKey: string, turnId: string): Promise<void> {
|
|
397
|
-
const
|
|
449
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
450
|
+
const snapshot = this.store.previews?.load(isTurnViewState(saved) ? saved.previewTurnId ?? turnId : turnId) ?? saved;
|
|
398
451
|
if (!isTurnViewState(snapshot)) {
|
|
399
452
|
await this.outbound.sendText(contextKey, "未找到这次执行的详情。可能已被清理。");
|
|
400
453
|
return;
|
|
@@ -408,6 +461,7 @@ export class FeishuTurnPresenter {
|
|
|
408
461
|
page: number | "latest",
|
|
409
462
|
messageId?: string,
|
|
410
463
|
): Promise<void> {
|
|
464
|
+
turnId = this.latestPresentationTurn(turnId, contextKey, messageId);
|
|
411
465
|
const entry = this.entries.get(turnId);
|
|
412
466
|
if (entry) await entry.initializing;
|
|
413
467
|
const ownsProgressCard = entry
|
|
@@ -430,7 +484,7 @@ export class FeishuTurnPresenter {
|
|
|
430
484
|
}
|
|
431
485
|
|
|
432
486
|
if (!entry.historySnapshot) {
|
|
433
|
-
entry.historySnapshot = this.store.previews?.load(turnId) ?? entry.state;
|
|
487
|
+
entry.historySnapshot = this.store.previews?.load(entry.state.previewTurnId ?? turnId) ?? entry.state;
|
|
434
488
|
await entry.scheduler?.flush();
|
|
435
489
|
}
|
|
436
490
|
const card = this.renderActivityHistory(entry.historySnapshot, page);
|
|
@@ -439,7 +493,8 @@ export class FeishuTurnPresenter {
|
|
|
439
493
|
return;
|
|
440
494
|
}
|
|
441
495
|
|
|
442
|
-
const
|
|
496
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
497
|
+
const snapshot = this.store.previews?.load(isTurnViewState(saved) ? saved.previewTurnId ?? turnId : turnId) ?? saved;
|
|
443
498
|
if (!isTurnViewState(snapshot)) throw new Error("未找到这次执行的活动历史。");
|
|
444
499
|
const card = page === "latest"
|
|
445
500
|
? this.renderTurn(snapshot)
|
|
@@ -477,9 +532,9 @@ export class FeishuTurnPresenter {
|
|
|
477
532
|
const compact = compactTurnView(state);
|
|
478
533
|
this.dirty.set(turnId, { state: compact, contextKey });
|
|
479
534
|
if (!event || isTerminalViewStatus(state.status) || state.status === "waiting_for_approval") {
|
|
480
|
-
this.store.previews.flush(turnId, true);
|
|
535
|
+
this.store.previews.flush(state.previewTurnId ?? turnId, true);
|
|
481
536
|
this.flushPersistence();
|
|
482
|
-
if (isTerminalViewStatus(state.status)) this.store.previews.release(turnId);
|
|
537
|
+
if (isTerminalViewStatus(state.status)) this.store.previews.release(state.previewTurnId ?? turnId);
|
|
483
538
|
} else if (!this.persistTimer) {
|
|
484
539
|
this.persistTimer = setTimeout(() => {
|
|
485
540
|
try { this.flushPersistence(); } catch (error) { this.options.onError?.(error); }
|
|
@@ -519,7 +574,8 @@ export class FeishuTurnPresenter {
|
|
|
519
574
|
this.sessionModelProviders.get(event.sessionId),
|
|
520
575
|
);
|
|
521
576
|
this.store.previews?.seed(initial);
|
|
522
|
-
|
|
577
|
+
event = presentationEvent(event, initial);
|
|
578
|
+
this.store.previews?.event(event, initial.previewTurnId);
|
|
523
579
|
const state = this.cardState(reduceTurnEvent(initial, event));
|
|
524
580
|
const entry = { contextKey, state, initializing: Promise.resolve() } as TurnEntry;
|
|
525
581
|
this.entries.set(event.turnId, entry);
|
|
@@ -596,7 +652,21 @@ export class FeishuTurnPresenter {
|
|
|
596
652
|
private turnPreviewUrl(state: TurnViewState): string | undefined {
|
|
597
653
|
return state.turnId.startsWith("pending_")
|
|
598
654
|
? undefined
|
|
599
|
-
: this.options.turnPreviewUrl?.(state.turnId);
|
|
655
|
+
: this.options.turnPreviewUrl?.(state.previewTurnId ?? state.turnId);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
private latestPresentationTurn(turnId: string, contextKey: string, messageId?: string): string {
|
|
659
|
+
const seen = new Set<string>();
|
|
660
|
+
while (!seen.has(turnId)) {
|
|
661
|
+
seen.add(turnId);
|
|
662
|
+
const saved = this.store.getTurnSnapshot(turnId);
|
|
663
|
+
if (!isTurnViewState(saved) || !saved.continuedToTurnId) break;
|
|
664
|
+
const next = saved.continuedToTurnId;
|
|
665
|
+
if (this.store.getTurnContextKey?.(next) !== contextKey
|
|
666
|
+
|| (messageId && this.store.getTurnDelivery(next)?.progressMessageId !== messageId)) break;
|
|
667
|
+
turnId = next;
|
|
668
|
+
}
|
|
669
|
+
return turnId;
|
|
600
670
|
}
|
|
601
671
|
|
|
602
672
|
private startElapsedUpdates(entry: TurnEntry): void {
|
|
@@ -717,6 +787,28 @@ export class FeishuTurnPresenter {
|
|
|
717
787
|
}
|
|
718
788
|
}
|
|
719
789
|
|
|
790
|
+
function presentationEvent(event: AgentEvent, state: TurnViewState): AgentEvent {
|
|
791
|
+
if (event.type === "turn_started" && state.segmentStartedAt !== undefined) return { ...event, startedAt: state.startedAt };
|
|
792
|
+
if (event.type === "turn_completed" && event.durationMs !== undefined && state.segmentStartedAt !== undefined) {
|
|
793
|
+
return { ...event, durationMs: Math.max(0, state.segmentStartedAt - state.startedAt) + event.durationMs };
|
|
794
|
+
}
|
|
795
|
+
if (state.continuedFromTurnId) {
|
|
796
|
+
// Tool/item IDs are only required to be unique within one runtime Turn.
|
|
797
|
+
const scoped = (id: string) => event.turnId + ":" + id;
|
|
798
|
+
if (event.type === "tool_started" || event.type === "tool_updated") return { ...event, tool: { ...event.tool, id: scoped(event.tool.id) } };
|
|
799
|
+
if (event.type === "tool_output_delta") return { ...event, toolId: scoped(event.toolId) };
|
|
800
|
+
if (event.type === "reasoning_delta" || event.type === "reasoning_completed") return { ...event, itemId: scoped(event.itemId) };
|
|
801
|
+
if (event.type === "progress") return { ...event,
|
|
802
|
+
activityId: (event.activityId?.startsWith("commentary:") ? "commentary:" : "") + scoped(event.activityId ?? "progress"),
|
|
803
|
+
...(event.reasoning ? { reasoning: { ...event.reasoning, itemId: scoped(event.reasoning.itemId) } } : {}) };
|
|
804
|
+
if (event.type === "agent_text_delta" && event.replacesActivityId) return {
|
|
805
|
+
...event, replacesActivityId: "commentary:" + scoped(event.replacesActivityId),
|
|
806
|
+
};
|
|
807
|
+
if (event.type === "context_compaction" && event.compactionId) return { ...event, compactionId: scoped(event.compactionId) };
|
|
808
|
+
}
|
|
809
|
+
return event;
|
|
810
|
+
}
|
|
811
|
+
|
|
720
812
|
function normalizeTurnCardMarkdown(
|
|
721
813
|
state: TurnViewState,
|
|
722
814
|
localFileUrl: LocalFileUrlResolver | undefined,
|
package/src/feishu/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { TerminalServer } from "./terminal/TerminalServer.js";
|
|
5
|
+
import { sendTerminalAuthorization } from "./terminal/TerminalCards.js";
|
|
6
|
+
import { ShellCommandJobManager } from "./shell/ShellCommandJobManager.js";
|
|
4
7
|
import { LocalControlServer } from "./cli/LocalControlServer.js";
|
|
5
8
|
import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
|
|
6
9
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
@@ -79,6 +82,8 @@ const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.
|
|
|
79
82
|
const logger = createLogger(config);
|
|
80
83
|
const store = new StateStore(config.storage.sqlitePath, (error) => logger.error({ error }, "Failed to persist Turn preview journal."));
|
|
81
84
|
let localFileViewer: LocalFileViewerServer | undefined;
|
|
85
|
+
let terminalServer: TerminalServer | undefined;
|
|
86
|
+
const shellCommandJobs = new ShellCommandJobManager(path.join(path.dirname(config.storage.sqlitePath), "command-jobs"));
|
|
82
87
|
let loadTurnDetails: ((turnId: string) => Promise<unknown>) | undefined;
|
|
83
88
|
if (config.fileViewer.enabled) {
|
|
84
89
|
const viewer = new LocalFileViewerServer({
|
|
@@ -160,7 +165,7 @@ let restartRequested = false;
|
|
|
160
165
|
let pendingSelfUpdatePlanPath: string | undefined;
|
|
161
166
|
let preparingSelfUpdate = false;
|
|
162
167
|
const safeRestart = new SafeRestartScheduler({
|
|
163
|
-
readActivity: () =>
|
|
168
|
+
readActivity: () => serverActivity(),
|
|
164
169
|
onReady: (reason, notificationTargets) => pendingSelfUpdatePlanPath
|
|
165
170
|
? initiateSelfUpdate(reason, notificationTargets)
|
|
166
171
|
: initiateRestart(reason, notificationTargets),
|
|
@@ -226,8 +231,39 @@ async function prepareAndScheduleUpdate(version: string, target: RestartNotifica
|
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
233
|
|
|
234
|
+
if (feishuOutbound && localFileViewer && config.terminal?.enabled !== false) {
|
|
235
|
+
const server = new TerminalServer({
|
|
236
|
+
stateDirectory: path.join(path.dirname(config.storage.sqlitePath), "terminal"),
|
|
237
|
+
ownerId: () => config.feishu.userOpenId,
|
|
238
|
+
canStart: () => !shuttingDown && !restartRequested && !safeRestart.scheduled && !safeRestart.restarting,
|
|
239
|
+
readJob: (jobId) => shellCommandJobs.readJob(jobId),
|
|
240
|
+
requestApproval: (authorization) => sendTerminalAuthorization(feishuOutbound, authorization),
|
|
241
|
+
updateCard: (messageId, view) => feishuOutbound.updateInteractiveCard(messageId, new CardRenderer().renderTerminalAuthorization(view)),
|
|
242
|
+
logger,
|
|
243
|
+
});
|
|
244
|
+
try {
|
|
245
|
+
const publicBaseUrl = await server.start(localFileViewer);
|
|
246
|
+
terminalServer = server;
|
|
247
|
+
logger.info({ publicBaseUrl }, "Interactive terminal service started (owner authorization required).");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
await server.close();
|
|
250
|
+
logger.warn({ error }, "Terminal service unavailable; command cards remain usable.");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function serverActivity() {
|
|
255
|
+
const activity = store.getServerActivityState();
|
|
256
|
+
const activeTerminals = terminalServer?.activeCount ?? 0;
|
|
257
|
+
return { ...activity, activeTerminals };
|
|
258
|
+
}
|
|
259
|
+
|
|
229
260
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
230
261
|
supervised,
|
|
262
|
+
terminalUrl: (jobId) => terminalServer?.createUrl(jobId),
|
|
263
|
+
authorizeTerminal: (action) => {
|
|
264
|
+
if (!terminalServer) throw new Error("网页终端未启用。");
|
|
265
|
+
return terminalServer.resolveApproval(action);
|
|
266
|
+
},
|
|
231
267
|
showUpdates: (contextKey, userId) => manualUpdates.show(contextKey, userId),
|
|
232
268
|
selectUpdate: (action, replyTarget) => manualUpdates.select(action, replyTarget),
|
|
233
269
|
restart: requestRestart,
|
|
@@ -264,7 +300,7 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
264
300
|
const changed = writeAgentExecutionDefaults(activeConfigPath, agentName, defaults);
|
|
265
301
|
logger.info({ agentName, changed }, "Stored Agent execution defaults.");
|
|
266
302
|
},
|
|
267
|
-
});
|
|
303
|
+
}, undefined, undefined, shellCommandJobs);
|
|
268
304
|
loadTurnDetails = (turnId) => controller.loadTurnDetails(turnId);
|
|
269
305
|
|
|
270
306
|
if (feishuOutbound) {
|
|
@@ -327,7 +363,7 @@ function recoverPendingSelfUpdate(): void {
|
|
|
327
363
|
pending.plan.reason ?? `Resume Agent Bot update to ${pending.plan.toVersion}`,
|
|
328
364
|
notificationTarget,
|
|
329
365
|
{
|
|
330
|
-
restartImmediatelyIfIdle: isServerIdle(
|
|
366
|
+
restartImmediatelyIfIdle: isServerIdle(serverActivity()),
|
|
331
367
|
},
|
|
332
368
|
);
|
|
333
369
|
logger.warn(
|
|
@@ -468,7 +504,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
468
504
|
}),
|
|
469
505
|
safeRestartScheduled: safeRestart.scheduled,
|
|
470
506
|
safeRestartReason: safeRestart.pendingReason,
|
|
471
|
-
activity:
|
|
507
|
+
activity: serverActivity(),
|
|
472
508
|
},
|
|
473
509
|
};
|
|
474
510
|
case "server_restart": {
|
|
@@ -497,7 +533,7 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
497
533
|
}
|
|
498
534
|
assertSelfUpdatePlanPath(request.planPath, agentBotHome());
|
|
499
535
|
const notificationTarget = inferServerRestartTarget(request.notificationSessionId);
|
|
500
|
-
const restartImmediatelyIfIdle = isServerIdle(
|
|
536
|
+
const restartImmediatelyIfIdle = isServerIdle(serverActivity());
|
|
501
537
|
pendingSelfUpdatePlanPath = request.planPath;
|
|
502
538
|
safeRestart.schedule(request.reason, notificationTarget, { restartImmediatelyIfIdle });
|
|
503
539
|
return {
|
|
@@ -636,6 +672,7 @@ async function shutdown(
|
|
|
636
672
|
outbound.flushAll().catch((error: unknown) => logger.warn({ error }, "Failed to flush presenters.")),
|
|
637
673
|
delay(5_000),
|
|
638
674
|
]);
|
|
675
|
+
await terminalServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close terminals."));
|
|
639
676
|
runtimes.close();
|
|
640
677
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
641
678
|
await localFileViewer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local file viewer."));
|
|
@@ -708,8 +745,8 @@ function inferServerRestartTarget(notificationSessionId?: string): RestartNotifi
|
|
|
708
745
|
return privateRestartNotificationTarget(config.feishu.userOpenId);
|
|
709
746
|
}
|
|
710
747
|
|
|
711
|
-
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number }): boolean {
|
|
712
|
-
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0;
|
|
748
|
+
function isServerIdle(activity: { runningSessions: number; pendingFinalDeliveries: number; activeTerminals?: number }): boolean {
|
|
749
|
+
return activity.runningSessions === 0 && activity.pendingFinalDeliveries === 0 && !activity.activeTerminals;
|
|
713
750
|
}
|
|
714
751
|
|
|
715
752
|
function restartNotificationTargetForSession(
|