@keyou007/agent-bot 0.1.24-alpha.4 → 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 +59 -0
- package/README.md +42 -6
- package/README.zh.md +42 -6
- 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/AppServerPermissions.js +34 -0
- package/dist/codex/AppServerPermissions.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +37 -5
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexRuntime.js +238 -127
- 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/codex/ThreadWriterProcess.js +19 -5
- package/dist/codex/ThreadWriterProcess.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 +146 -72
- 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/CsvPreview.js +148 -0
- package/dist/local-files/CsvPreview.js.map +1 -0
- package/dist/local-files/LocalFileViewerServer.js +104 -14
- package/dist/local-files/LocalFileViewerServer.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +128 -12
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +80 -6
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +767 -260
- 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 +53 -3
- package/docs/technical-reference.zh.md +52 -2
- package/npm-shrinkwrap.json +55 -5
- package/package.json +11 -4
- package/skills/agent-bot/SKILL.md +15 -2
- package/src/cli/controlProtocol.ts +2 -1
- package/src/cli/serverStatus.ts +1 -0
- package/src/cli.ts +6 -1
- package/src/codex/AppServerPermissions.ts +39 -0
- package/src/codex/CodexLocalActivityDetector.ts +34 -4
- package/src/codex/CodexRuntime.ts +243 -139
- package/src/codex/ProviderSwitch.ts +11 -12
- package/src/codex/ThreadWriterProcess.ts +17 -9
- 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 +159 -72
- 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/CsvPreview.ts +132 -0
- package/src/local-files/LocalFileViewerServer.ts +102 -15
- package/src/local-files/TurnPreviewPage.ts +131 -13
- package/src/presentation/TurnStateReducer.ts +87 -7
- package/src/presentation/turnViewTypes.ts +9 -0
- package/src/proxy/ProxySessionController.ts +762 -259
- package/src/proxy/SessionOperationQueue.ts +13 -0
- package/src/runtime/AcpRuntimeAdapter.ts +5 -3
- package/src/runtime/types.ts +10 -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
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
2
1
|
import type { RuntimeExecutionSettings } from "../runtime/types.js";
|
|
3
2
|
|
|
4
3
|
export const PROVIDER_SWITCH_BUSY = "当前任务正在执行,暂时不能切换 Provider。请等待任务完成,或停止任务后重试。";
|
|
4
|
+
export const PROVIDER_SWITCH_EMPTY = "App Server 尚未保存此空会话,无法在保留任务 ID 的前提下切换 Provider。未改变会话或设置,也不会自动创建替代任务。";
|
|
5
5
|
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
return /no rollout found|rollout[^\n]*(?:not found|missing)/iu.test(message);
|
|
6
|
+
export class ProviderSwitchError extends Error {
|
|
7
|
+
constructor(message: string, readonly previousSettingsRestored: boolean, options?: ErrorOptions) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = "ProviderSwitchError";
|
|
10
|
+
}
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export function assertProviderSettingsApplied(
|
|
15
14
|
actual: { modelProvider?: string; model?: string },
|
|
16
|
-
requested: Pick<RuntimeExecutionSettings, "modelProvider" | "model"
|
|
15
|
+
requested: Partial<Pick<RuntimeExecutionSettings, "modelProvider" | "model">>,
|
|
17
16
|
): void {
|
|
18
|
-
if (actual.modelProvider !== requested.modelProvider) {
|
|
19
|
-
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}
|
|
17
|
+
if (requested.modelProvider !== undefined && actual.modelProvider !== requested.modelProvider) {
|
|
18
|
+
throw new Error(`Provider 未生效:请求 ${requested.modelProvider},实际返回 ${actual.modelProvider ?? "未知"}。App Server 未应用设置;可在任务空闲后安全重启再重试,不会自动 Fork 或更改历史。`);
|
|
20
19
|
}
|
|
21
|
-
if (actual.model !== requested.model) {
|
|
20
|
+
if (requested.model !== undefined && actual.model !== requested.model) {
|
|
22
21
|
throw new Error(`模型未生效:请求 ${requested.model},实际返回 ${actual.model ?? "未知"}。请确认目标 Provider 支持该模型。`);
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -29,7 +28,7 @@ export function providerSwitchFailure(error: unknown): string {
|
|
|
29
28
|
if (/model[^\n]*(?:unsupported|not supported|not found|unavailable|does not exist)|unknown model/iu.test(message)) {
|
|
30
29
|
return `目标 Provider 不支持当前模型,请选择兼容的模型后重试。${message}`;
|
|
31
30
|
}
|
|
32
|
-
if (/unauthorized|forbidden|auth(?:
|
|
31
|
+
if (/unauthorized|forbidden|auth(?:enticat(?:e|ion)|orization)|api.?key|provider[^\n]*(?:not found|unknown|not configured)/iu.test(message)) {
|
|
33
32
|
return `请检查目标 Provider 的配置和认证信息。${message}`;
|
|
34
33
|
}
|
|
35
34
|
return message;
|
|
@@ -29,8 +29,9 @@ export class SystemThreadWriterProcessController implements ThreadWriterProcessC
|
|
|
29
29
|
async inspect(lockPath: string): Promise<ThreadWriterProcess[]> {
|
|
30
30
|
try {
|
|
31
31
|
await access(lockPath);
|
|
32
|
-
} catch {
|
|
33
|
-
return [];
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return [];
|
|
34
|
+
throw error;
|
|
34
35
|
}
|
|
35
36
|
return process.platform === "win32"
|
|
36
37
|
? inspectWindowsLockOwners(lockPath)
|
|
@@ -134,12 +135,15 @@ async function inspectWindowsLockOwners(lockPath: string): Promise<ThreadWriterP
|
|
|
134
135
|
async function inspectPosixLockOwners(lockPath: string): Promise<ThreadWriterProcess[]> {
|
|
135
136
|
const pids = await posixLockOwnerPids(lockPath);
|
|
136
137
|
const owners = await Promise.all(pids.map((pid) => inspectPosixProcess(pid)));
|
|
138
|
+
if (owners.some((owner) => !owner)) throw new Error("Cannot identify a reported thread writer; retry the inspection.");
|
|
137
139
|
return owners.filter((owner): owner is ThreadWriterProcess => owner !== undefined);
|
|
138
140
|
}
|
|
139
141
|
|
|
140
142
|
async function posixLockOwnerPids(lockPath: string): Promise<number[]> {
|
|
141
143
|
const lsof = await tryExecuteFile("lsof", ["-t", "--", lockPath]);
|
|
142
|
-
const
|
|
144
|
+
const result = lsof.ok || lsof.noMatches ? lsof : await tryExecuteFile("fuser", [lockPath]);
|
|
145
|
+
if (!result.ok && !result.noMatches) throw new Error("Cannot inspect thread writers: lsof and fuser failed.");
|
|
146
|
+
const raw = result.stdout;
|
|
143
147
|
return [...new Set(raw.match(/\d+/gu)?.map(Number).filter((pid) => Number.isSafeInteger(pid) && pid > 0) ?? [])];
|
|
144
148
|
}
|
|
145
149
|
|
|
@@ -233,11 +237,15 @@ function executeFile(command: string, args: string[], extraEnv: Record<string, s
|
|
|
233
237
|
});
|
|
234
238
|
}
|
|
235
239
|
|
|
236
|
-
function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string }> {
|
|
237
|
-
return
|
|
238
|
-
(
|
|
239
|
-
|
|
240
|
-
|
|
240
|
+
function tryExecuteFile(command: string, args: string[]): Promise<{ ok: boolean; stdout: string; noMatches: boolean }> {
|
|
241
|
+
return new Promise((resolve) => {
|
|
242
|
+
execFile(command, args, {
|
|
243
|
+
timeout: PROCESS_INSPECTION_TIMEOUT_MS, maxBuffer: MAX_PROCESS_OUTPUT_BYTES, windowsHide: true, encoding: "utf8",
|
|
244
|
+
}, (error, stdout, stderr) => resolve({
|
|
245
|
+
ok: !error, stdout,
|
|
246
|
+
noMatches: error?.code === 1 && !error.killed && !stdout.trim() && !stderr.trim(),
|
|
247
|
+
}));
|
|
248
|
+
});
|
|
241
249
|
}
|
|
242
250
|
|
|
243
251
|
function positiveInteger(value: unknown): number | undefined {
|
|
@@ -352,7 +360,7 @@ try {
|
|
|
352
360
|
for ($index = 0; $index -lt $count; $index++) {
|
|
353
361
|
$writerPid = $items[$index].Process.dwProcessId
|
|
354
362
|
$writer = Get-AgentBotProcess $writerPid
|
|
355
|
-
if ($null -eq $writer) {
|
|
363
|
+
if ($null -eq $writer) { throw "Cannot identify reported thread writer PID $writerPid; retry the inspection." }
|
|
356
364
|
$application = $writer
|
|
357
365
|
$cursor = $writer
|
|
358
366
|
for ($depth = 0; $depth -lt 8 -and $cursor.ParentProcessId -gt 1; $depth++) {
|
|
@@ -33,7 +33,9 @@ export function writeAgentExecutionDefaults(
|
|
|
33
33
|
for (const key of DEFAULT_KEYS) {
|
|
34
34
|
const value = defaults[key];
|
|
35
35
|
if (value !== undefined) target.set(key, value);
|
|
36
|
+
else target.delete(key);
|
|
36
37
|
}
|
|
38
|
+
if (target.items.length === 0) agent.delete("defaults");
|
|
37
39
|
|
|
38
40
|
const updated = document.toString();
|
|
39
41
|
if (updated === original) return false;
|
package/src/config/loadConfig.ts
CHANGED
|
@@ -30,6 +30,10 @@ updates:
|
|
|
30
30
|
# Daily stable update check, private 60-second cancellation window, then safe update.
|
|
31
31
|
enabled: true
|
|
32
32
|
|
|
33
|
+
terminal:
|
|
34
|
+
enabled: true
|
|
35
|
+
# Shares fileViewer host, port and publicBaseUrl; keeps independent owner authorization.
|
|
36
|
+
|
|
33
37
|
fileViewer:
|
|
34
38
|
enabled: true
|
|
35
39
|
# Use 0.0.0.0 to accept LAN connections and automatically prefer
|
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,9 +1,11 @@
|
|
|
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";
|
|
5
6
|
import type {
|
|
6
7
|
ApprovalDecision,
|
|
8
|
+
ApprovalRequest,
|
|
7
9
|
ModelOption,
|
|
8
10
|
ModelProviderOption,
|
|
9
11
|
PermissionMode,
|
|
@@ -13,6 +15,7 @@ import type {
|
|
|
13
15
|
import type { TurnActivity, TurnViewState, TurnViewStatus } from "../presentation/turnViewTypes.js";
|
|
14
16
|
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
15
17
|
import type { UpdateNotice } from "../updates/types.js";
|
|
18
|
+
import type { TerminalCardView } from "../terminal/TerminalServer.js";
|
|
16
19
|
import { localCardImage } from "./LocalCardImage.js";
|
|
17
20
|
|
|
18
21
|
export interface StartupStatusView {
|
|
@@ -57,6 +60,7 @@ export interface InitializationWelcomeView {
|
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export interface SafeRestartStatusView {
|
|
63
|
+
activeTerminals?: number;
|
|
60
64
|
scheduleId: number;
|
|
61
65
|
reason: string;
|
|
62
66
|
phase: "waiting_tasks" | "waiting_delivery" | "countdown" | "restarting" | "cancelled" | "superseded";
|
|
@@ -84,6 +88,7 @@ export interface ModelSelectorCardView {
|
|
|
84
88
|
sessionId: string;
|
|
85
89
|
contextKey: string;
|
|
86
90
|
currentModel?: string;
|
|
91
|
+
modelPage?: number;
|
|
87
92
|
reasoningEffort?: string;
|
|
88
93
|
models: ModelOption[];
|
|
89
94
|
modelProvider?: string;
|
|
@@ -130,7 +135,7 @@ export type ExecutionSettingsTab = "agent" | "provider" | "model" | "thinking" |
|
|
|
130
135
|
|
|
131
136
|
const EXECUTION_SETTING_EFFECTS: Record<ExecutionSettingsTab, string> = {
|
|
132
137
|
agent: "只影响新建任务,不切换当前任务。",
|
|
133
|
-
provider: "
|
|
138
|
+
provider: "只影响当前任务;运行中选择将在本轮结束后生效。",
|
|
134
139
|
model: "只影响当前任务,下一轮生效。",
|
|
135
140
|
thinking: "只影响当前任务,下一轮生效。",
|
|
136
141
|
permission: "只影响当前任务,下一轮生效。",
|
|
@@ -142,6 +147,8 @@ export interface ExecutionSettingsAgentOption {
|
|
|
142
147
|
}
|
|
143
148
|
|
|
144
149
|
export interface ExecutionSettingsCardView {
|
|
150
|
+
pendingNotice?: string;
|
|
151
|
+
pendingFailed?: boolean;
|
|
145
152
|
sessionId?: string;
|
|
146
153
|
contextKey: string;
|
|
147
154
|
activeTab: ExecutionSettingsTab;
|
|
@@ -151,6 +158,7 @@ export interface ExecutionSettingsCardView {
|
|
|
151
158
|
runtimeSettingsAvailable: boolean;
|
|
152
159
|
currentProvider?: string;
|
|
153
160
|
currentModel?: string;
|
|
161
|
+
modelPage?: number;
|
|
154
162
|
currentEffort?: string;
|
|
155
163
|
currentPermissionMode: PermissionMode;
|
|
156
164
|
providers: ModelProviderOption[];
|
|
@@ -197,6 +205,7 @@ export interface SessionTaskCardEntry {
|
|
|
197
205
|
reference: string;
|
|
198
206
|
summary: string;
|
|
199
207
|
detailLines: string[];
|
|
208
|
+
stopAction?: TaskListCardAction;
|
|
200
209
|
actions?: TaskListCardAction[];
|
|
201
210
|
current?: boolean;
|
|
202
211
|
}
|
|
@@ -261,6 +270,7 @@ export interface DismissGroupCardView {
|
|
|
261
270
|
}
|
|
262
271
|
|
|
263
272
|
export interface ShellCommandCardView {
|
|
273
|
+
terminalUrl?: string;
|
|
264
274
|
jobId?: string;
|
|
265
275
|
contextKey?: string;
|
|
266
276
|
command: string;
|
|
@@ -506,6 +516,9 @@ export class CardRenderer {
|
|
|
506
516
|
...(outputWasTruncated ? ["输出过长,已保留开头和结尾并截断中间内容。"] : []),
|
|
507
517
|
].join("\n")),
|
|
508
518
|
];
|
|
519
|
+
if (view.terminalUrl) {
|
|
520
|
+
elements.push(markdown(`[Open Terminal](${view.terminalUrl}) · 独立终端,需在本会话由所有者授权`));
|
|
521
|
+
}
|
|
509
522
|
if (view.status === "running" && view.jobId && view.contextKey) {
|
|
510
523
|
elements.push({
|
|
511
524
|
tag: "button",
|
|
@@ -551,6 +564,7 @@ export class CardRenderer {
|
|
|
551
564
|
`**思考强度 / 权限**:${inlineCode(view.currentEffort ?? "自动")} / ${inlineCode(permissionModeLabel(view.currentPermissionMode))}`,
|
|
552
565
|
]
|
|
553
566
|
: []),
|
|
567
|
+
...(view.pendingNotice ? [view.pendingNotice] : []),
|
|
554
568
|
...(view.notice ? [view.notice] : []),
|
|
555
569
|
...(view.warning ? [`**提示**:${escapeCardHtml(view.warning)}`] : []),
|
|
556
570
|
].join("\n")),
|
|
@@ -582,7 +596,8 @@ export class CardRenderer {
|
|
|
582
596
|
inlineCode(provider.id),
|
|
583
597
|
provider.isDefault ? " · 默认" : "",
|
|
584
598
|
].join(""),
|
|
585
|
-
current: provider.id === view.currentProvider,
|
|
599
|
+
current: provider.id === view.currentProvider && !view.pendingFailed,
|
|
600
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
586
601
|
action: {
|
|
587
602
|
text: "Switch",
|
|
588
603
|
value: {
|
|
@@ -597,18 +612,24 @@ export class CardRenderer {
|
|
|
597
612
|
if (view.models.length === 0) {
|
|
598
613
|
elements.push(markdown("当前运行时未返回可用模型。"));
|
|
599
614
|
} else {
|
|
600
|
-
|
|
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({
|
|
601
619
|
label: `${inlineCode(model.id)}${model.isDefault ? " · 默认" : ""}`,
|
|
602
|
-
current: model.id === view.currentModel,
|
|
620
|
+
current: model.id === view.currentModel && !view.pendingFailed,
|
|
621
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
603
622
|
action: {
|
|
604
623
|
text: "Switch",
|
|
605
624
|
value: {
|
|
606
625
|
action: "settings_model_select",
|
|
607
626
|
...baseAction,
|
|
627
|
+
provider: view.currentProvider ?? "",
|
|
608
628
|
model: model.id,
|
|
609
629
|
},
|
|
610
630
|
},
|
|
611
631
|
})));
|
|
632
|
+
elements.push(...page.navigation);
|
|
612
633
|
}
|
|
613
634
|
} else if (view.activeTab === "thinking") {
|
|
614
635
|
if (!view.currentModel) {
|
|
@@ -620,11 +641,13 @@ export class CardRenderer {
|
|
|
620
641
|
elements.push(...view.reasoningOptions.map((option) => settingsOptionRow({
|
|
621
642
|
label: inlineCode(option.value),
|
|
622
643
|
current: option.value === view.currentEffort,
|
|
644
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
623
645
|
action: {
|
|
624
646
|
text: "Switch",
|
|
625
647
|
value: {
|
|
626
648
|
action: "settings_thinking_select",
|
|
627
649
|
...baseAction,
|
|
650
|
+
provider: view.currentProvider ?? "",
|
|
628
651
|
model: currentModel,
|
|
629
652
|
effort: option.value,
|
|
630
653
|
},
|
|
@@ -635,6 +658,7 @@ export class CardRenderer {
|
|
|
635
658
|
elements.push(...(["auto", "confirm"] as PermissionMode[]).map((mode) => settingsOptionRow({
|
|
636
659
|
label: `${inlineCode(mode)} · ${permissionModeLabel(mode)}`,
|
|
637
660
|
current: mode === view.currentPermissionMode,
|
|
661
|
+
currentLabel: view.pendingNotice ? "待生效" : undefined,
|
|
638
662
|
action: {
|
|
639
663
|
text: "Switch",
|
|
640
664
|
value: {
|
|
@@ -647,7 +671,7 @@ export class CardRenderer {
|
|
|
647
671
|
}
|
|
648
672
|
|
|
649
673
|
elements.push(markdown(`> ${EXECUTION_SETTING_EFFECTS[view.activeTab]}`));
|
|
650
|
-
return sectionCard("运行设置", elements, view.warning ? "orange" : view.notice ? "green" : "blue");
|
|
674
|
+
return sectionCard("运行设置", elements, view.warning || view.pendingFailed ? "orange" : view.notice ? "green" : "blue");
|
|
651
675
|
}
|
|
652
676
|
|
|
653
677
|
renderProviderSelector(view: ProviderSelectorCardView): Record<string, unknown> {
|
|
@@ -783,7 +807,14 @@ export class CardRenderer {
|
|
|
783
807
|
if (view.models.length === 0) {
|
|
784
808
|
elements.push(markdown("当前运行时未返回可用模型。"));
|
|
785
809
|
} else {
|
|
786
|
-
|
|
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) => {
|
|
787
818
|
const isCurrent = model.id === view.currentModel;
|
|
788
819
|
return {
|
|
789
820
|
tag: "column_set",
|
|
@@ -821,6 +852,7 @@ export class CardRenderer {
|
|
|
821
852
|
],
|
|
822
853
|
};
|
|
823
854
|
}));
|
|
855
|
+
elements.push(...page.navigation);
|
|
824
856
|
}
|
|
825
857
|
if (view.unifiedSettings) {
|
|
826
858
|
elements.push(
|
|
@@ -992,6 +1024,24 @@ export class CardRenderer {
|
|
|
992
1024
|
view.status === "completed" ? "green" : view.status === "cancelled" ? "grey" : "blue");
|
|
993
1025
|
}
|
|
994
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
|
+
|
|
995
1045
|
renderSafeRestartStatus(view: SafeRestartStatusView): Record<string, unknown> {
|
|
996
1046
|
const actionable = view.phase !== "restarting" && view.phase !== "cancelled" && view.phase !== "superseded";
|
|
997
1047
|
const countdown = view.phase === "countdown"
|
|
@@ -1019,6 +1069,7 @@ export class CardRenderer {
|
|
|
1019
1069
|
`**重启原因**:${inlineCode(view.reason)}`,
|
|
1020
1070
|
`**重启倒计时**:${countdown}`,
|
|
1021
1071
|
`**待投递结果**:${view.pendingFinalDeliveries} 条`,
|
|
1072
|
+
...(view.activeTerminals ? [`**活动终端**:${view.activeTerminals} 个,请在网页或原会话的终端管理卡中关闭。ForceRestart 会中断终端。`] : []),
|
|
1022
1073
|
];
|
|
1023
1074
|
const elements: Record<string, unknown>[] = [markdown(lines.join("\n"))];
|
|
1024
1075
|
if (view.waitingTasks.length > 0) {
|
|
@@ -1698,38 +1749,7 @@ function renderTurnElements(
|
|
|
1698
1749
|
elements.push(...renderActivities(visibleActivities, state.projectCwd));
|
|
1699
1750
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1700
1751
|
|
|
1701
|
-
if (state.approval)
|
|
1702
|
-
const request = state.approval;
|
|
1703
|
-
elements.push(markdown([
|
|
1704
|
-
`**${request.title}**`,
|
|
1705
|
-
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1706
|
-
request.reason,
|
|
1707
|
-
].filter(Boolean).join("\n")));
|
|
1708
|
-
elements.push({
|
|
1709
|
-
tag: "column_set",
|
|
1710
|
-
flex_mode: "flow",
|
|
1711
|
-
horizontal_spacing: "8px",
|
|
1712
|
-
columns: request.options.map((option) => ({
|
|
1713
|
-
tag: "column",
|
|
1714
|
-
width: "auto",
|
|
1715
|
-
elements: [{
|
|
1716
|
-
tag: "button",
|
|
1717
|
-
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1718
|
-
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1719
|
-
behaviors: [{
|
|
1720
|
-
type: "callback",
|
|
1721
|
-
value: {
|
|
1722
|
-
action: "approval",
|
|
1723
|
-
sessionId: state.sessionId,
|
|
1724
|
-
turnId: state.turnId,
|
|
1725
|
-
requestId: request.id,
|
|
1726
|
-
decision: option.id,
|
|
1727
|
-
},
|
|
1728
|
-
}],
|
|
1729
|
-
}],
|
|
1730
|
-
})),
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1752
|
+
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1733
1753
|
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1734
1754
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1735
1755
|
if (showAssistantText) {
|
|
@@ -1769,38 +1789,7 @@ function renderGroupedTurnElements(
|
|
|
1769
1789
|
elements.push(...renderGroupedActivityGroups(visibleGroups, state.projectCwd));
|
|
1770
1790
|
if (state.fileSummary.length > 0) elements.push(fileSummaryPanel(state));
|
|
1771
1791
|
|
|
1772
|
-
if (state.approval)
|
|
1773
|
-
const request = state.approval;
|
|
1774
|
-
elements.push(markdown([
|
|
1775
|
-
`**${request.title}**`,
|
|
1776
|
-
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1777
|
-
request.reason,
|
|
1778
|
-
].filter(Boolean).join("\n")));
|
|
1779
|
-
elements.push({
|
|
1780
|
-
tag: "column_set",
|
|
1781
|
-
flex_mode: "flow",
|
|
1782
|
-
horizontal_spacing: "8px",
|
|
1783
|
-
columns: request.options.map((option) => ({
|
|
1784
|
-
tag: "column",
|
|
1785
|
-
width: "auto",
|
|
1786
|
-
elements: [{
|
|
1787
|
-
tag: "button",
|
|
1788
|
-
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1789
|
-
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1790
|
-
behaviors: [{
|
|
1791
|
-
type: "callback",
|
|
1792
|
-
value: {
|
|
1793
|
-
action: "approval",
|
|
1794
|
-
sessionId: state.sessionId,
|
|
1795
|
-
turnId: state.turnId,
|
|
1796
|
-
requestId: request.id,
|
|
1797
|
-
decision: option.id,
|
|
1798
|
-
},
|
|
1799
|
-
}],
|
|
1800
|
-
}],
|
|
1801
|
-
})),
|
|
1802
|
-
});
|
|
1803
|
-
}
|
|
1792
|
+
if (state.approval) elements.push(...renderApprovalElements(state, state.approval));
|
|
1804
1793
|
if (state.error) elements.push(markdown(codeBlock(state.error, 2_000)));
|
|
1805
1794
|
const showAssistantText = state.assistantText && assistantTextMode === "always";
|
|
1806
1795
|
if (showAssistantText) {
|
|
@@ -1812,6 +1801,83 @@ function renderGroupedTurnElements(
|
|
|
1812
1801
|
return elements;
|
|
1813
1802
|
}
|
|
1814
1803
|
|
|
1804
|
+
function renderApprovalElements(state: TurnViewState, request: ApprovalRequest): Record<string, unknown>[] {
|
|
1805
|
+
const elements: Record<string, unknown>[] = [];
|
|
1806
|
+
const command = request.command?.trim();
|
|
1807
|
+
if (command && request.kind !== "mode_change") {
|
|
1808
|
+
elements.push(markdown("**命令执行确认**"));
|
|
1809
|
+
const reason = request.reason?.trim()
|
|
1810
|
+
|| (request.title.trim() !== command ? request.title.trim() : "");
|
|
1811
|
+
if (reason && reason !== command) {
|
|
1812
|
+
const preview = approvalTextPreview(reason, 1_000, escapeApprovalText);
|
|
1813
|
+
elements.push(markdown(escapeApprovalText(preview)));
|
|
1814
|
+
if (preview !== reason) elements.push(markdown("<font color='grey'>说明较长,请在 Preview 中查看完整内容。</font>"));
|
|
1815
|
+
}
|
|
1816
|
+
const display = displayToolCommand(command) ?? command;
|
|
1817
|
+
const summary = truncateText(display.replace(/\s+/gu, " "), 64);
|
|
1818
|
+
const preview = approvalTextPreview(command, 2_200);
|
|
1819
|
+
elements.push(collapsiblePanel(`查看命令 · ${summary}`, [
|
|
1820
|
+
markdown(codeBlock(preview, preview.length)),
|
|
1821
|
+
...(preview !== command ? [markdown("<font color='grey'>命令过长,卡片仅显示开头。请在 Preview 中查看完整命令后再授权。</font>")] : []),
|
|
1822
|
+
], {
|
|
1823
|
+
elementId: `approval_${createHash("sha256").update(request.id).digest("hex").slice(0, 11)}`,
|
|
1824
|
+
compact: true,
|
|
1825
|
+
borderColor: "grey",
|
|
1826
|
+
}));
|
|
1827
|
+
} else {
|
|
1828
|
+
elements.push(markdown([
|
|
1829
|
+
`**${request.title}**`,
|
|
1830
|
+
request.command ? codeBlock(request.command, 800) : undefined,
|
|
1831
|
+
request.reason,
|
|
1832
|
+
].filter(Boolean).join("\n")));
|
|
1833
|
+
}
|
|
1834
|
+
if (request.automatic) {
|
|
1835
|
+
elements.push(markdown("自动执行模式:正在自动同意。"));
|
|
1836
|
+
return elements;
|
|
1837
|
+
}
|
|
1838
|
+
elements.push({
|
|
1839
|
+
tag: "column_set",
|
|
1840
|
+
flex_mode: "flow",
|
|
1841
|
+
horizontal_spacing: "8px",
|
|
1842
|
+
columns: request.options.map((option) => ({
|
|
1843
|
+
tag: "column",
|
|
1844
|
+
width: "auto",
|
|
1845
|
+
elements: [{
|
|
1846
|
+
tag: "button",
|
|
1847
|
+
text: { tag: "plain_text", content: request.kind === "mode_change" ? option.label : approvalDecisionLabel(option.id) },
|
|
1848
|
+
type: option.id === "accept" || option.id === "acceptForSession" ? "primary" : option.id === "cancel" ? "danger" : "default",
|
|
1849
|
+
behaviors: [{
|
|
1850
|
+
type: "callback",
|
|
1851
|
+
value: {
|
|
1852
|
+
action: "approval",
|
|
1853
|
+
sessionId: state.sessionId,
|
|
1854
|
+
turnId: state.turnId,
|
|
1855
|
+
requestId: request.id,
|
|
1856
|
+
decision: option.id,
|
|
1857
|
+
},
|
|
1858
|
+
}],
|
|
1859
|
+
}],
|
|
1860
|
+
})),
|
|
1861
|
+
});
|
|
1862
|
+
return elements;
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function approvalTextPreview(value: string, maxBytes: number, encode = (text: string): string => text): string {
|
|
1866
|
+
if (Buffer.byteLength(JSON.stringify(encode(value)), "utf8") <= maxBytes) return value;
|
|
1867
|
+
let bytes = 5;
|
|
1868
|
+
let result = "";
|
|
1869
|
+
for (const character of value) {
|
|
1870
|
+
bytes += Buffer.byteLength(JSON.stringify(encode(character)), "utf8") - 2;
|
|
1871
|
+
if (bytes > maxBytes) break;
|
|
1872
|
+
result += character;
|
|
1873
|
+
}
|
|
1874
|
+
return `${result}...`;
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
function escapeApprovalText(value: string): string {
|
|
1878
|
+
return escapeCardHtml(value).replace(/[\\`*_[\]#]/gu, (character) => `&#${character.codePointAt(0)};`);
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1815
1881
|
function emptyTurnText(status: TurnViewStatus, agentLabel?: string): string {
|
|
1816
1882
|
const label = agentLabel?.trim().replace(/[\r\n]+/g, " ") || "Agent";
|
|
1817
1883
|
if (status === "starting") return `正在连接 ${label}…`;
|
|
@@ -2233,7 +2299,8 @@ function removeMarkdownBold(value: string): string {
|
|
|
2233
2299
|
}
|
|
2234
2300
|
|
|
2235
2301
|
function turnActivities(state: TurnViewState): TurnActivity[] {
|
|
2236
|
-
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;
|
|
2237
2304
|
|
|
2238
2305
|
const activities: TurnActivity[] = [];
|
|
2239
2306
|
if (state.progressText) {
|
|
@@ -2298,6 +2365,7 @@ function collapsiblePanel(
|
|
|
2298
2365
|
|
|
2299
2366
|
function sessionTaskPanel(entry: SessionTaskCardEntry): Record<string, unknown> {
|
|
2300
2367
|
const elements: Record<string, unknown>[] = [markdown(entry.detailLines.join("\n"))];
|
|
2368
|
+
if (entry.stopAction) elements.push(sessionActionButton(entry.stopAction));
|
|
2301
2369
|
if (entry.actions?.length) elements.push(sessionActionOverflow(entry.actions));
|
|
2302
2370
|
return collapsiblePanel(entry.summary, elements, {
|
|
2303
2371
|
elementId: sessionTaskPanelElementId(entry.reference),
|
|
@@ -2831,6 +2899,24 @@ function acpPermissionOptionLabel(option: { name: string; kind: string }): strin
|
|
|
2831
2899
|
return labels[option.kind] ?? (/^[\x20-\x7E]+$/.test(option.name) ? option.name : "Select");
|
|
2832
2900
|
}
|
|
2833
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
|
+
|
|
2834
2920
|
function settingsTabRow(
|
|
2835
2921
|
activeTab: ExecutionSettingsTab,
|
|
2836
2922
|
baseAction: Record<string, string>,
|
|
@@ -2904,6 +2990,7 @@ function settingsTabElement(
|
|
|
2904
2990
|
function settingsOptionRow(input: {
|
|
2905
2991
|
label: string;
|
|
2906
2992
|
current: boolean;
|
|
2993
|
+
currentLabel?: string;
|
|
2907
2994
|
action: TaskListCardAction;
|
|
2908
2995
|
}): Record<string, unknown> {
|
|
2909
2996
|
return {
|
|
@@ -2923,7 +3010,7 @@ function settingsOptionRow(input: {
|
|
|
2923
3010
|
tag: "column",
|
|
2924
3011
|
width: "auto",
|
|
2925
3012
|
vertical_align: "center",
|
|
2926
|
-
elements: input.current ? [markdown("
|
|
3013
|
+
elements: input.current ? [markdown(`✅ ${input.currentLabel ?? "当前"}`)] : [taskActionElement(input.action)],
|
|
2927
3014
|
},
|
|
2928
3015
|
],
|
|
2929
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
|