@keyou007/agent-bot 0.1.15-alpha.0 → 0.1.15-alpha.2
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 +14 -0
- package/README.md +23 -20
- package/README.zh.md +23 -20
- package/config.example.yaml +1 -0
- package/dist/cli/FeishuAppConfiguration.js +42 -3
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/feishu/CardRenderer.js +114 -16
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +21 -6
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +58 -6
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +33 -4
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +7 -6
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/FinalResponseBranding.js +15 -0
- package/dist/feishu/FinalResponseBranding.js.map +1 -0
- package/dist/logging/DailyLogStream.js +73 -0
- package/dist/logging/DailyLogStream.js.map +1 -0
- package/dist/logging/logger.js +2 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +270 -40
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +126 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +11 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SupervisorDiagnostics.js +37 -21
- package/dist/supervision/SupervisorDiagnostics.js.map +1 -1
- package/dist/supervisor.js +11 -4
- package/dist/supervisor.js.map +1 -1
- package/dist/utils/executeShellCommand.js +83 -16
- package/dist/utils/executeShellCommand.js.map +1 -1
- package/docs/technical-reference.md +9 -8
- package/docs/technical-reference.zh.md +9 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/cli/FeishuAppConfiguration.ts +76 -3
- package/src/cli.ts +6 -1
- package/src/feishu/CardRenderer.ts +150 -17
- package/src/feishu/FeishuConnector.ts +32 -7
- package/src/feishu/FeishuMarkdown.ts +64 -5
- package/src/feishu/FeishuMessageClient.ts +41 -12
- package/src/feishu/FeishuTurnPresenter.ts +9 -6
- package/src/feishu/FinalResponseBranding.ts +18 -0
- package/src/logging/DailyLogStream.ts +88 -0
- package/src/logging/logger.ts +2 -1
- package/src/proxy/ProxySessionController.ts +292 -39
- package/src/state/StateStore.ts +180 -0
- package/src/state/migrations.ts +11 -0
- package/src/supervision/SupervisorDiagnostics.ts +41 -25
- package/src/supervisor.ts +10 -4
- package/src/utils/executeShellCommand.ts +103 -15
package/src/logging/logger.ts
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import pino, { type Logger } from "pino";
|
|
4
4
|
import type { AppConfig } from "../config/schema.js";
|
|
5
|
+
import { DailyLogStream } from "./DailyLogStream.js";
|
|
5
6
|
|
|
6
7
|
export function createLogger(config: AppConfig): Logger {
|
|
7
8
|
const streams: pino.StreamEntry[] = [
|
|
@@ -13,7 +14,7 @@ export function createLogger(config: AppConfig): Logger {
|
|
|
13
14
|
if (config.logging.path) {
|
|
14
15
|
fs.mkdirSync(path.dirname(config.logging.path), { recursive: true });
|
|
15
16
|
streams.push({
|
|
16
|
-
stream:
|
|
17
|
+
stream: new DailyLogStream(config.logging.path),
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -24,8 +24,10 @@ import {
|
|
|
24
24
|
type ResetHistoryCardEntry,
|
|
25
25
|
type SessionTaskCardEntry,
|
|
26
26
|
type SessionTaskCardGroup,
|
|
27
|
+
type ShellCommandCardView,
|
|
27
28
|
type TaskListCardAction,
|
|
28
29
|
} from "../feishu/CardRenderer.js";
|
|
30
|
+
import { CardUpdateScheduler } from "../feishu/CardUpdateScheduler.js";
|
|
29
31
|
import { generateGroupAvatarPng, resolveGroupAvatarProjectName } from "../feishu/GroupAvatarGenerator.js";
|
|
30
32
|
import { normalizeFeishuPostText } from "../feishu/InboundText.js";
|
|
31
33
|
import { allowsFeishuUser } from "../feishu/ownerAccess.js";
|
|
@@ -54,10 +56,12 @@ import {
|
|
|
54
56
|
type TurnAnchorRecord,
|
|
55
57
|
} from "../state/StateStore.js";
|
|
56
58
|
import { createId } from "../utils/id.js";
|
|
57
|
-
import {
|
|
59
|
+
import { truncateMiddle, truncateText } from "../utils/markdown.js";
|
|
58
60
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
59
61
|
import {
|
|
60
62
|
executeShellCommand,
|
|
63
|
+
type ShellCommandOptions,
|
|
64
|
+
type ShellCommandOutputSnapshot,
|
|
61
65
|
type ShellCommandResult,
|
|
62
66
|
} from "../utils/executeShellCommand.js";
|
|
63
67
|
|
|
@@ -98,6 +102,7 @@ const RESET_HISTORY_PAGE_SIZE = 10;
|
|
|
98
102
|
const MAX_LLM_TURN_RETRIES = 3;
|
|
99
103
|
const RECOVERY_ACTIVITY_WINDOW_MS = 5 * 60 * 1_000;
|
|
100
104
|
const RECOVERY_HEARTBEAT_INTERVAL_MS = 60 * 1_000;
|
|
105
|
+
const SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS = 3_000;
|
|
101
106
|
const FORWARD_ATTACHMENT_WINDOW_MS = 800;
|
|
102
107
|
const DEFAULT_MERGED_FORWARD_INSTRUCTION = "请参考以下内容回复用户";
|
|
103
108
|
const DEFAULT_REFERENCED_MESSAGE_INSTRUCTION = "请参考引用消息回复用户";
|
|
@@ -366,7 +371,11 @@ export interface ProxyLifecycle {
|
|
|
366
371
|
rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
|
|
367
372
|
}
|
|
368
373
|
|
|
369
|
-
export type ShellCommandExecutor = (
|
|
374
|
+
export type ShellCommandExecutor = (
|
|
375
|
+
command: string,
|
|
376
|
+
cwd: string,
|
|
377
|
+
options?: ShellCommandOptions,
|
|
378
|
+
) => Promise<ShellCommandResult>;
|
|
370
379
|
export interface WindowsDriveInfo {
|
|
371
380
|
root: string;
|
|
372
381
|
label?: string;
|
|
@@ -686,7 +695,7 @@ export class ProxySessionController {
|
|
|
686
695
|
if (isQueueIndependentCommand(command)) {
|
|
687
696
|
await this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
688
697
|
try {
|
|
689
|
-
if (command.type
|
|
698
|
+
if (command.type === "prompt") await this.ensureThreadFork(message);
|
|
690
699
|
await this.execute(
|
|
691
700
|
message.contextKey,
|
|
692
701
|
command,
|
|
@@ -709,7 +718,7 @@ export class ProxySessionController {
|
|
|
709
718
|
const next = previous.catch(() => undefined).then(() =>
|
|
710
719
|
this.outbound.withReplyTarget(message.contextKey, replyTarget, async () => {
|
|
711
720
|
try {
|
|
712
|
-
if (command.type
|
|
721
|
+
if (command.type === "prompt") await this.ensureThreadFork(message);
|
|
713
722
|
await this.execute(
|
|
714
723
|
message.contextKey,
|
|
715
724
|
command,
|
|
@@ -812,6 +821,30 @@ export class ProxySessionController {
|
|
|
812
821
|
const filePath = directoryFileActionPath(scopedAction.value.filePath);
|
|
813
822
|
await this.assertSendableFile(filePath);
|
|
814
823
|
await this.outbound.sendFile(contextKey, filePath);
|
|
824
|
+
} else if (kind === "directory_new_folder_prompt") {
|
|
825
|
+
const directory = directoryActionPath(scopedAction.value.directory);
|
|
826
|
+
await this.assertBrowsableDirectory(directory);
|
|
827
|
+
await this.outbound.updateInteractiveCard(
|
|
828
|
+
contextKey,
|
|
829
|
+
requiredCardMessageId(scopedAction.messageId),
|
|
830
|
+
this.cardRenderer.renderDirectoryNewFolderCard({
|
|
831
|
+
directory,
|
|
832
|
+
displayDirectory: abbreviateHomeDirectory(directory),
|
|
833
|
+
contextKey,
|
|
834
|
+
page: directoryPageValue(scopedAction.value.page),
|
|
835
|
+
}),
|
|
836
|
+
);
|
|
837
|
+
} else if (kind === "directory_new_folder_submit") {
|
|
838
|
+
await this.createDirectoryFromCard(contextKey, scopedAction);
|
|
839
|
+
} else if (kind === "directory_new_folder_cancel") {
|
|
840
|
+
await this.openDirectoryBrowser(
|
|
841
|
+
contextKey,
|
|
842
|
+
directoryActionPath(scopedAction.value.directory),
|
|
843
|
+
{
|
|
844
|
+
updateMessageId: requiredCardMessageId(scopedAction.messageId),
|
|
845
|
+
page: directoryPageValue(scopedAction.value.page),
|
|
846
|
+
},
|
|
847
|
+
);
|
|
815
848
|
} else if (kind === "directory_new" || kind === "directory_new_group") {
|
|
816
849
|
const directory = directoryActionPath(scopedAction.value.directory);
|
|
817
850
|
await this.assertBrowsableDirectory(directory);
|
|
@@ -1488,8 +1521,7 @@ export class ProxySessionController {
|
|
|
1488
1521
|
if (!session.remoteSessionId || !this.isCodexSession(session)) {
|
|
1489
1522
|
throw new Error("The task is not an App Server task that can be reset.");
|
|
1490
1523
|
}
|
|
1491
|
-
const
|
|
1492
|
-
const rows = this.store.listCompletedTurnGraph(session.localSessionId, contextKey);
|
|
1524
|
+
const rows = this.store.listTaskTurnGraph(session.localSessionId);
|
|
1493
1525
|
const graph = buildTurnGraphRows(rows.map((turn) => ({ turnId: turn.turnId, parentTurnId: turn.parentTurnId })));
|
|
1494
1526
|
return {
|
|
1495
1527
|
session,
|
|
@@ -1641,6 +1673,9 @@ export class ProxySessionController {
|
|
|
1641
1673
|
incomingMessage?: IncomingMessage,
|
|
1642
1674
|
): Promise<void> {
|
|
1643
1675
|
const context = this.store.getOrCreateUserContext(contextKey, this.config.defaults.agent!);
|
|
1676
|
+
if (!this.currentSession(contextKey) && isThreadContextKey(contextKey) && commandRequiresCurrentSession(command)) {
|
|
1677
|
+
throw new Error(unboundThreadTaskMessage());
|
|
1678
|
+
}
|
|
1644
1679
|
switch (command.type) {
|
|
1645
1680
|
case "shell":
|
|
1646
1681
|
await this.runShellCommand(contextKey, command.command);
|
|
@@ -3308,7 +3343,13 @@ export class ProxySessionController {
|
|
|
3308
3343
|
const existing = runtime.getSession(record.localSessionId);
|
|
3309
3344
|
if (existing) return { record, runtime, session: existing };
|
|
3310
3345
|
const pending = this.sessionLoads.get(record.localSessionId);
|
|
3311
|
-
if (pending)
|
|
3346
|
+
if (pending) {
|
|
3347
|
+
const loaded = await pending;
|
|
3348
|
+
return {
|
|
3349
|
+
...loaded,
|
|
3350
|
+
record: { ...loaded.record, contextKey: record.contextKey },
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3312
3353
|
|
|
3313
3354
|
this.outbound.registerSession(
|
|
3314
3355
|
record.localSessionId,
|
|
@@ -3377,7 +3418,11 @@ export class ProxySessionController {
|
|
|
3377
3418
|
}
|
|
3378
3419
|
this.persistRuntimeSession(record, session, session.activeTurnId ? "running" : "ready");
|
|
3379
3420
|
const saved = this.store.getSession(record.localSessionId) ?? record;
|
|
3380
|
-
return {
|
|
3421
|
+
return {
|
|
3422
|
+
record: { ...saved, contextKey: record.contextKey },
|
|
3423
|
+
runtime,
|
|
3424
|
+
session,
|
|
3425
|
+
};
|
|
3381
3426
|
})();
|
|
3382
3427
|
this.sessionLoads.set(record.localSessionId, loading);
|
|
3383
3428
|
try {
|
|
@@ -3476,6 +3521,7 @@ export class ProxySessionController {
|
|
|
3476
3521
|
}
|
|
3477
3522
|
|
|
3478
3523
|
if (event.type === "turn_completed" || event.type === "turn_cancelled" || event.type === "turn_failed") {
|
|
3524
|
+
await this.refreshGoalCards(event.sessionId);
|
|
3479
3525
|
const terminalStatus = event.type === "turn_completed"
|
|
3480
3526
|
? "completed"
|
|
3481
3527
|
: event.type === "turn_cancelled"
|
|
@@ -3883,8 +3929,10 @@ export class ProxySessionController {
|
|
|
3883
3929
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
3884
3930
|
}
|
|
3885
3931
|
|
|
3886
|
-
const
|
|
3887
|
-
|
|
3932
|
+
const historyTurn = this.store.listTaskTurnGraph(current.localSessionId)
|
|
3933
|
+
.find((turn) => turn.turnId === turnId);
|
|
3934
|
+
const snapshot = turnViewSnapshot(historyTurn?.snapshot);
|
|
3935
|
+
if (!snapshot || snapshot.status !== "completed") {
|
|
3888
3936
|
throw new Error("只能将当前任务 Reset 到已成功完成的轮次。");
|
|
3889
3937
|
}
|
|
3890
3938
|
if (current.lastTurnId === turnId && current.lastTurnStatus === "completed") {
|
|
@@ -3905,7 +3953,7 @@ export class ProxySessionController {
|
|
|
3905
3953
|
}
|
|
3906
3954
|
|
|
3907
3955
|
const origin = this.store.getTurnRuntimeOrigin(turnId);
|
|
3908
|
-
if (origin &&
|
|
3956
|
+
if (origin && origin.agentName !== current.agentName) {
|
|
3909
3957
|
throw new Error("这张思考卡片与当前任务不匹配,已拒绝 Reset。");
|
|
3910
3958
|
}
|
|
3911
3959
|
const sourceRemoteSessionId = origin?.remoteSessionId ?? current.remoteSessionId;
|
|
@@ -4369,6 +4417,25 @@ export class ProxySessionController {
|
|
|
4369
4417
|
notice?: string,
|
|
4370
4418
|
record = this.currentSession(contextKey),
|
|
4371
4419
|
): Promise<void> {
|
|
4420
|
+
const card = this.renderGoalCard(goal, notice, record);
|
|
4421
|
+
if (!record) {
|
|
4422
|
+
await this.outbound.sendInteractiveCard(contextKey, card);
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4425
|
+
const existing = this.store.getGoalCardDelivery(record.localSessionId, contextKey);
|
|
4426
|
+
if (existing) {
|
|
4427
|
+
await this.outbound.updateInteractiveCard(contextKey, existing.messageId, card);
|
|
4428
|
+
return;
|
|
4429
|
+
}
|
|
4430
|
+
const messageId = await this.outbound.sendInteractiveCard(contextKey, card);
|
|
4431
|
+
if (messageId) this.store.saveGoalCardDelivery(record.localSessionId, contextKey, messageId);
|
|
4432
|
+
}
|
|
4433
|
+
|
|
4434
|
+
private renderGoalCard(
|
|
4435
|
+
goal: RuntimeGoal | undefined,
|
|
4436
|
+
notice?: string,
|
|
4437
|
+
record?: SessionRecord,
|
|
4438
|
+
): Record<string, unknown> {
|
|
4372
4439
|
const sections: CardSection[] = [];
|
|
4373
4440
|
if (notice) sections.push({ lines: [cardText(notice)] });
|
|
4374
4441
|
sections.push(goal
|
|
@@ -4390,24 +4457,138 @@ export class ProxySessionController {
|
|
|
4390
4457
|
"**/goal edit <新目标>** 修改 **/goal clear** 清除",
|
|
4391
4458
|
],
|
|
4392
4459
|
});
|
|
4393
|
-
|
|
4460
|
+
const title = goal ? `Agent Goal · ${goalStatusLabel(goal.status)}` : "Agent Goal";
|
|
4461
|
+
return this.cardRenderer.renderSectionsCard(title, sections);
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4464
|
+
private async refreshGoalCards(localSessionId: string): Promise<void> {
|
|
4465
|
+
const deliveries = this.store.listGoalCardDeliveries(localSessionId);
|
|
4466
|
+
if (deliveries.length === 0) return;
|
|
4467
|
+
const record = this.store.getSession(localSessionId);
|
|
4468
|
+
if (!record) return;
|
|
4469
|
+
const runtime = this.runtimes.forAgent(record.agentName);
|
|
4470
|
+
if (runtime.kind !== "codex" || !runtime.getGoal) return;
|
|
4471
|
+
|
|
4472
|
+
try {
|
|
4473
|
+
const loaded = runtime.getSession(localSessionId) ?? (await this.loadSession(record)).session;
|
|
4474
|
+
const goal = await runtime.getGoal(loaded.localSessionId);
|
|
4475
|
+
const card = this.renderGoalCard(goal, undefined, record);
|
|
4476
|
+
await Promise.all(deliveries.map(async (delivery) => {
|
|
4477
|
+
try {
|
|
4478
|
+
await this.outbound.updateInteractiveCard(delivery.contextKey, delivery.messageId, card);
|
|
4479
|
+
} catch (error) {
|
|
4480
|
+
this.logger.warn(
|
|
4481
|
+
{ error, localSessionId, contextKey: delivery.contextKey, messageId: delivery.messageId },
|
|
4482
|
+
"Failed to refresh an Agent Goal card.",
|
|
4483
|
+
);
|
|
4484
|
+
}
|
|
4485
|
+
}));
|
|
4486
|
+
} catch (error) {
|
|
4487
|
+
this.logger.warn({ error, localSessionId }, "Failed to read the latest Agent Goal status.");
|
|
4488
|
+
}
|
|
4394
4489
|
}
|
|
4395
4490
|
|
|
4396
4491
|
private async runShellCommand(contextKey: string, command: string): Promise<void> {
|
|
4397
4492
|
const cwd = this.currentSession(contextKey)?.cwd ?? this.config.defaults.cwd;
|
|
4398
|
-
const
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4493
|
+
const startedAt = Date.now();
|
|
4494
|
+
let latestOutput: ShellCommandOutputSnapshot = {
|
|
4495
|
+
stdout: "",
|
|
4496
|
+
stderr: "",
|
|
4497
|
+
outputTruncated: false,
|
|
4498
|
+
};
|
|
4499
|
+
let runningState: ShellCommandCardView = {
|
|
4500
|
+
command,
|
|
4501
|
+
cwd,
|
|
4502
|
+
...latestOutput,
|
|
4503
|
+
status: "running",
|
|
4504
|
+
elapsedMs: 0,
|
|
4505
|
+
};
|
|
4506
|
+
const cardMessageId = await this.outbound.sendInteractiveCard(
|
|
4507
|
+
contextKey,
|
|
4508
|
+
this.cardRenderer.renderShellCommandCard(runningState),
|
|
4509
|
+
);
|
|
4510
|
+
const scheduler = cardMessageId
|
|
4511
|
+
? new CardUpdateScheduler<ShellCommandCardView>({
|
|
4512
|
+
render: (state) => this.cardRenderer.renderShellCommandCard(state),
|
|
4513
|
+
write: (card) => this.outbound.updateInteractiveCard(contextKey, cardMessageId, card),
|
|
4514
|
+
onError: (error) => this.logger.warn(
|
|
4515
|
+
{ error, contextKey, messageId: cardMessageId },
|
|
4516
|
+
"Failed to update a shell command card.",
|
|
4517
|
+
),
|
|
4518
|
+
})
|
|
4519
|
+
: undefined;
|
|
4520
|
+
scheduler?.seed(runningState);
|
|
4521
|
+
const elapsedTimer = scheduler ? setInterval(() => {
|
|
4522
|
+
runningState = { ...runningState, elapsedMs: Date.now() - startedAt };
|
|
4523
|
+
scheduler.update(runningState);
|
|
4524
|
+
}, SHELL_COMMAND_ELAPSED_UPDATE_INTERVAL_MS) : undefined;
|
|
4525
|
+
elapsedTimer?.unref?.();
|
|
4526
|
+
|
|
4527
|
+
const updateRunningOutput = (snapshot: ShellCommandOutputSnapshot): void => {
|
|
4528
|
+
latestOutput = snapshot;
|
|
4529
|
+
runningState = {
|
|
4530
|
+
...runningState,
|
|
4531
|
+
...snapshot,
|
|
4532
|
+
elapsedMs: Date.now() - startedAt,
|
|
4533
|
+
};
|
|
4534
|
+
scheduler?.update(runningState);
|
|
4535
|
+
};
|
|
4536
|
+
|
|
4537
|
+
let result: ShellCommandResult;
|
|
4538
|
+
try {
|
|
4539
|
+
result = await this.shellCommandExecutor(command, cwd, { onOutput: updateRunningOutput });
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
if (elapsedTimer) clearInterval(elapsedTimer);
|
|
4542
|
+
const failedCard = this.cardRenderer.renderShellCommandCard({
|
|
4543
|
+
command,
|
|
4544
|
+
cwd,
|
|
4545
|
+
...latestOutput,
|
|
4546
|
+
stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
|
|
4547
|
+
status: "failed",
|
|
4548
|
+
elapsedMs: Date.now() - startedAt,
|
|
4549
|
+
});
|
|
4550
|
+
if (scheduler) {
|
|
4551
|
+
try {
|
|
4552
|
+
await scheduler.flush({
|
|
4553
|
+
...runningState,
|
|
4554
|
+
stderr: [latestOutput.stderr.trimEnd(), runtimeErrorMessage(error)].filter(Boolean).join("\n"),
|
|
4555
|
+
status: "failed",
|
|
4556
|
+
elapsedMs: Date.now() - startedAt,
|
|
4557
|
+
});
|
|
4558
|
+
} catch (cardError) {
|
|
4559
|
+
this.logger.warn(
|
|
4560
|
+
{ error: cardError, contextKey, messageId: cardMessageId },
|
|
4561
|
+
"Failed to finalize a shell command card.",
|
|
4562
|
+
);
|
|
4563
|
+
} finally {
|
|
4564
|
+
scheduler.dispose();
|
|
4565
|
+
}
|
|
4566
|
+
} else {
|
|
4567
|
+
await this.outbound.sendInteractiveCard(contextKey, failedCard);
|
|
4568
|
+
}
|
|
4569
|
+
throw error;
|
|
4570
|
+
}
|
|
4571
|
+
|
|
4572
|
+
if (elapsedTimer) clearInterval(elapsedTimer);
|
|
4573
|
+
const finalState: ShellCommandCardView = {
|
|
4574
|
+
command,
|
|
4575
|
+
cwd,
|
|
4576
|
+
stdout: result.stdout,
|
|
4577
|
+
stderr: result.stderr,
|
|
4578
|
+
status: result.timedOut ? "timed_out" : result.exitCode === 0 ? "completed" : "failed",
|
|
4579
|
+
exitCode: result.exitCode,
|
|
4580
|
+
elapsedMs: Date.now() - startedAt,
|
|
4581
|
+
outputTruncated: result.outputTruncated,
|
|
4582
|
+
};
|
|
4583
|
+
if (scheduler) {
|
|
4584
|
+
try {
|
|
4585
|
+
await scheduler.flush(finalState);
|
|
4586
|
+
} finally {
|
|
4587
|
+
scheduler.dispose();
|
|
4588
|
+
}
|
|
4589
|
+
} else {
|
|
4590
|
+
await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderShellCommandCard(finalState));
|
|
4591
|
+
}
|
|
4411
4592
|
}
|
|
4412
4593
|
|
|
4413
4594
|
private async setTitle(contextKey: string, title: string): Promise<void> {
|
|
@@ -4524,7 +4705,7 @@ export class ProxySessionController {
|
|
|
4524
4705
|
if (!current.remoteSessionId || !this.isCodexSession(current)) {
|
|
4525
4706
|
throw new Error("当前任务不是可 Reset 的 App Server 任务。");
|
|
4526
4707
|
}
|
|
4527
|
-
const allTurns = this.store.
|
|
4708
|
+
const allTurns = this.store.listTaskTurnGraph(current.localSessionId);
|
|
4528
4709
|
const graphRows = buildTurnGraphRows(allTurns.map((turn) => ({
|
|
4529
4710
|
turnId: turn.turnId,
|
|
4530
4711
|
parentTurnId: turn.parentTurnId,
|
|
@@ -4666,7 +4847,6 @@ export class ProxySessionController {
|
|
|
4666
4847
|
contextKey,
|
|
4667
4848
|
},
|
|
4668
4849
|
},
|
|
4669
|
-
actions: directoryCreationActions(entry.path, contextKey),
|
|
4670
4850
|
}
|
|
4671
4851
|
: {
|
|
4672
4852
|
name: entry.name,
|
|
@@ -4686,12 +4866,12 @@ export class ProxySessionController {
|
|
|
4686
4866
|
const card = this.cardRenderer.renderDirectoryBrowserCard({
|
|
4687
4867
|
directory: abbreviateHomeDirectory(directory),
|
|
4688
4868
|
entries: visibleEntries,
|
|
4689
|
-
currentActions: directoryCreationActions(directory, contextKey),
|
|
4869
|
+
currentActions: directoryCreationActions(directory, contextKey, page),
|
|
4690
4870
|
navigationActions,
|
|
4691
4871
|
footerLines: [
|
|
4692
4872
|
`第 ${page + 1}/${totalPages} 页 · ${directoryCount} 个目录 · ${entries.length - directoryCount} 个文件`,
|
|
4693
4873
|
"",
|
|
4694
|
-
">
|
|
4874
|
+
"> 点击目录名称进入,点击文件名称发送到当前会话;卡片顶部可新建目录、任务或群聊。",
|
|
4695
4875
|
],
|
|
4696
4876
|
});
|
|
4697
4877
|
if (options.updateMessageId) {
|
|
@@ -4723,7 +4903,6 @@ export class ProxySessionController {
|
|
|
4723
4903
|
contextKey,
|
|
4724
4904
|
},
|
|
4725
4905
|
},
|
|
4726
|
-
actions: directoryCreationActions(drive.root, contextKey),
|
|
4727
4906
|
}));
|
|
4728
4907
|
const card = this.cardRenderer.renderDirectoryBrowserCard({
|
|
4729
4908
|
directory: "此电脑",
|
|
@@ -4738,7 +4917,7 @@ export class ProxySessionController {
|
|
|
4738
4917
|
footerLines: [
|
|
4739
4918
|
`第 ${page + 1}/${totalPages} 页 · ${drives.length} 个磁盘`,
|
|
4740
4919
|
"",
|
|
4741
|
-
">
|
|
4920
|
+
"> 点击磁盘名称进入。",
|
|
4742
4921
|
],
|
|
4743
4922
|
});
|
|
4744
4923
|
if (options.updateMessageId) {
|
|
@@ -4758,6 +4937,27 @@ export class ProxySessionController {
|
|
|
4758
4937
|
if (!stats.isDirectory()) throw new Error(`这不是目录:${directory}`);
|
|
4759
4938
|
}
|
|
4760
4939
|
|
|
4940
|
+
private async createDirectoryFromCard(contextKey: string, action: CardAction): Promise<void> {
|
|
4941
|
+
const directory = directoryActionPath(action.value.directory);
|
|
4942
|
+
await this.assertBrowsableDirectory(directory);
|
|
4943
|
+
const folderName = directoryFolderName(cardFormValue(action.value, "folderName"));
|
|
4944
|
+
const newDirectory = path.join(directory, folderName);
|
|
4945
|
+
try {
|
|
4946
|
+
await fs.mkdir(newDirectory);
|
|
4947
|
+
} catch (error) {
|
|
4948
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
4949
|
+
? String(error.code)
|
|
4950
|
+
: undefined;
|
|
4951
|
+
if (code === "EEXIST") throw new Error(`目录已存在:${newDirectory}`);
|
|
4952
|
+
throw new Error(`无法创建目录 ${newDirectory}:${runtimeErrorMessage(error)}`);
|
|
4953
|
+
}
|
|
4954
|
+
await this.openDirectoryBrowser(contextKey, directory, {
|
|
4955
|
+
updateMessageId: requiredCardMessageId(action.messageId),
|
|
4956
|
+
page: directoryPageValue(action.value.page),
|
|
4957
|
+
});
|
|
4958
|
+
await this.outbound.sendText(contextKey, `已创建目录:${newDirectory}`);
|
|
4959
|
+
}
|
|
4960
|
+
|
|
4761
4961
|
private async assertSendableFile(filePath: string): Promise<void> {
|
|
4762
4962
|
let stats;
|
|
4763
4963
|
try {
|
|
@@ -5420,7 +5620,9 @@ export class ProxySessionController {
|
|
|
5420
5620
|
let activeTurnId: string | undefined;
|
|
5421
5621
|
let queued = 0;
|
|
5422
5622
|
if (!current) {
|
|
5423
|
-
taskLines.push(
|
|
5623
|
+
taskLines.push(isThreadContextKey(contextKey)
|
|
5624
|
+
? unboundThreadTaskMessage()
|
|
5625
|
+
: "无。直接发送消息即可创建一个未指定项目的 Agent 任务。");
|
|
5424
5626
|
} else {
|
|
5425
5627
|
const agent = this.ensureAgent(current.agentName);
|
|
5426
5628
|
const runtimeSession = this.runtimes.forAgent(current.agentName).getSession(current.localSessionId);
|
|
@@ -5696,7 +5898,11 @@ export class ProxySessionController {
|
|
|
5696
5898
|
|
|
5697
5899
|
private requireCurrentSession(contextKey: string): SessionRecord {
|
|
5698
5900
|
const record = this.currentSession(contextKey);
|
|
5699
|
-
if (!record)
|
|
5901
|
+
if (!record) {
|
|
5902
|
+
throw new Error(isThreadContextKey(contextKey)
|
|
5903
|
+
? unboundThreadTaskMessage()
|
|
5904
|
+
: "当前没有任务,直接发送一条消息即可自动创建。");
|
|
5905
|
+
}
|
|
5700
5906
|
return record;
|
|
5701
5907
|
}
|
|
5702
5908
|
|
|
@@ -6213,6 +6419,19 @@ function isQueueIndependentCommand(command: Command): boolean {
|
|
|
6213
6419
|
return false;
|
|
6214
6420
|
}
|
|
6215
6421
|
|
|
6422
|
+
function commandRequiresCurrentSession(command: Command): boolean {
|
|
6423
|
+
if (["dismiss", "stop", "title", "turns", "model", "provider", "thinking", "permissions", "nosteer"].includes(command.type)) {
|
|
6424
|
+
return true;
|
|
6425
|
+
}
|
|
6426
|
+
if (command.type === "archive" || command.type === "fork") return command.sessionId === undefined;
|
|
6427
|
+
if (command.type === "goal") return command.action !== "set";
|
|
6428
|
+
return false;
|
|
6429
|
+
}
|
|
6430
|
+
|
|
6431
|
+
function unboundThreadTaskMessage(): string {
|
|
6432
|
+
return "当前话题尚未绑定任务。发送普通消息后,Agent Bot 会从可识别的原始轮次创建分支;没有原始轮次时会创建全新任务。也可以使用 /new 创建全新任务,或使用 /sessions 绑定现有任务。";
|
|
6433
|
+
}
|
|
6434
|
+
|
|
6216
6435
|
function parseSessionPage(value: unknown): number {
|
|
6217
6436
|
const parsed = typeof value === "string" || typeof value === "number" ? Number(value) : Number.NaN;
|
|
6218
6437
|
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
@@ -6250,10 +6469,45 @@ function directoryFileActionPath(value: unknown): string {
|
|
|
6250
6469
|
return path.resolve(value);
|
|
6251
6470
|
}
|
|
6252
6471
|
|
|
6253
|
-
function
|
|
6472
|
+
function cardFormValue(value: Record<string, unknown>, name: string): unknown {
|
|
6473
|
+
const formValue = value.formValue;
|
|
6474
|
+
return typeof formValue === "object" && formValue !== null
|
|
6475
|
+
? (formValue as Record<string, unknown>)[name]
|
|
6476
|
+
: undefined;
|
|
6477
|
+
}
|
|
6478
|
+
|
|
6479
|
+
function directoryFolderName(value: unknown): string {
|
|
6480
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("请输入目录名。");
|
|
6481
|
+
const name = value.trim();
|
|
6482
|
+
if (name === "." || name === "..") throw new Error("目录名不能是 . 或 ..。");
|
|
6483
|
+
if (name.length > 255) throw new Error("目录名不能超过 255 个字符。");
|
|
6484
|
+
if (/[<>:"/\\|?*\u0000-\u001F]/u.test(name)) {
|
|
6485
|
+
throw new Error("目录名不能包含路径分隔符或以下字符:< > : \" / \\ | ? *");
|
|
6486
|
+
}
|
|
6487
|
+
if (/[. ]$/u.test(name)) throw new Error("目录名不能以空格或句点结尾。");
|
|
6488
|
+
if (/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/iu.test(name)) {
|
|
6489
|
+
throw new Error("该名称是系统保留名称,请使用其他目录名。");
|
|
6490
|
+
}
|
|
6491
|
+
return name;
|
|
6492
|
+
}
|
|
6493
|
+
|
|
6494
|
+
function directoryCreationActions(
|
|
6495
|
+
directory: string,
|
|
6496
|
+
contextKey: string,
|
|
6497
|
+
page: number,
|
|
6498
|
+
): TaskListCardAction[] {
|
|
6254
6499
|
return [
|
|
6255
6500
|
{
|
|
6256
|
-
text: "
|
|
6501
|
+
text: "NewFolder",
|
|
6502
|
+
value: {
|
|
6503
|
+
action: "directory_new_folder_prompt",
|
|
6504
|
+
directory,
|
|
6505
|
+
contextKey,
|
|
6506
|
+
page: String(page),
|
|
6507
|
+
},
|
|
6508
|
+
},
|
|
6509
|
+
{
|
|
6510
|
+
text: "NewTask",
|
|
6257
6511
|
value: {
|
|
6258
6512
|
action: "directory_new",
|
|
6259
6513
|
directory,
|
|
@@ -6261,7 +6515,7 @@ function directoryCreationActions(directory: string, contextKey: string): TaskLi
|
|
|
6261
6515
|
},
|
|
6262
6516
|
},
|
|
6263
6517
|
{
|
|
6264
|
-
text: "
|
|
6518
|
+
text: "NewGroupTask",
|
|
6265
6519
|
value: {
|
|
6266
6520
|
action: "directory_new_group",
|
|
6267
6521
|
directory,
|
|
@@ -6554,14 +6808,13 @@ const GROUP_PROJECT_DIRECTORY_MAX_LENGTH = 15;
|
|
|
6554
6808
|
|
|
6555
6809
|
function formatGroupProjectDirectory(projectCwd: string): string {
|
|
6556
6810
|
const value = abbreviateHomeDirectory(projectCwd);
|
|
6557
|
-
if (Array.from(value).length <= GROUP_PROJECT_DIRECTORY_MAX_LENGTH) return `[${value}]`;
|
|
6558
|
-
|
|
6559
6811
|
const levels = value
|
|
6560
6812
|
.replace(/[\\/]+$/, "")
|
|
6561
6813
|
.split(/[\\/]+/)
|
|
6562
6814
|
.filter(Boolean)
|
|
6563
6815
|
.slice(-2);
|
|
6564
|
-
|
|
6816
|
+
const separator = value.includes("\\") ? path.win32.sep : path.posix.sep;
|
|
6817
|
+
return `[${fitTrailingPathLevels(levels, GROUP_PROJECT_DIRECTORY_MAX_LENGTH, separator)}]`;
|
|
6565
6818
|
}
|
|
6566
6819
|
|
|
6567
6820
|
function abbreviateHomeDirectory(value: string): string {
|