@keyou007/agent-bot 0.1.13-alpha.0 → 0.1.13-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/.env.example +2 -2
- package/CHANGELOG.md +15 -0
- package/README.md +107 -98
- package/README.zh.md +81 -72
- package/dist/acp/AcpProcessManager.js +8 -3
- package/dist/acp/AcpProcessManager.js.map +1 -1
- package/dist/acp/AcpSessionManager.js +15 -0
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/cli/ServerStarter.js +17 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +16 -4
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/serverStatus.js +27 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli/taskListOutput.js +72 -0
- package/dist/cli/taskListOutput.js.map +1 -0
- package/dist/cli.js +88 -54
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +34 -4
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +3 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/FeishuUserOpenIdStore.js +76 -0
- package/dist/config/FeishuUserOpenIdStore.js.map +1 -0
- package/dist/config/loadConfig.js +3 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/feishu/CardRenderer.js +123 -22
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +16 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/index.js +125 -26
- package/dist/index.js.map +1 -1
- package/dist/presentation/turnGraph.js +36 -0
- package/dist/presentation/turnGraph.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +425 -104
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +3 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/runtime/agentEnvironment.js +24 -4
- package/dist/runtime/agentEnvironment.js.map +1 -1
- package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
- package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
- package/dist/startup/StartupNotifier.js +16 -7
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/startup/startFeishu.js +2 -2
- package/dist/startup/startFeishu.js.map +1 -1
- package/dist/state/StateStore.js +172 -0
- package/dist/state/StateStore.js.map +1 -1
- package/dist/state/migrations.js +21 -0
- package/dist/state/migrations.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +46 -3
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +65 -6
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/replacementSupervisor.js +27 -2
- package/dist/supervision/replacementSupervisor.js.map +1 -1
- package/dist/supervisor.js +27 -2
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +20 -7
- package/docs/technical-reference.zh.md +20 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +63 -26
- package/src/acp/AcpProcessManager.ts +16 -3
- package/src/acp/AcpSessionManager.ts +14 -0
- package/src/cli/ServerStarter.ts +25 -2
- package/src/cli/controlProtocol.ts +6 -1
- package/src/cli/help.ts +16 -4
- package/src/cli/serverStatus.ts +28 -0
- package/src/cli/taskListOutput.ts +91 -0
- package/src/cli.ts +123 -51
- package/src/codex/CodexProcessManager.ts +38 -3
- package/src/codex/CodexRuntime.ts +6 -0
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/config/FeishuUserOpenIdStore.ts +81 -0
- package/src/config/loadConfig.ts +3 -0
- package/src/feishu/CardRenderer.ts +169 -22
- package/src/feishu/FeishuConnector.ts +13 -4
- package/src/index.ts +195 -23
- package/src/presentation/turnGraph.ts +48 -0
- package/src/proxy/ProxySessionController.ts +507 -112
- package/src/runtime/AcpRuntimeAdapter.ts +5 -0
- package/src/runtime/agentEnvironment.ts +41 -3
- package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
- package/src/runtime/types.ts +5 -0
- package/src/startup/StartupNotifier.ts +23 -7
- package/src/startup/startFeishu.ts +3 -2
- package/src/state/StateStore.ts +224 -0
- package/src/state/migrations.ts +21 -0
- package/src/supervision/SafeRestartNotifier.ts +53 -4
- package/src/supervision/SafeRestartScheduler.ts +78 -7
- package/src/supervision/replacementSupervisor.ts +27 -1
- package/src/supervisor.ts +33 -2
|
@@ -139,9 +139,34 @@ export interface TaskListCardAction {
|
|
|
139
139
|
value: Record<string, string>;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
export interface HelpCardCommand {
|
|
143
|
+
text: string;
|
|
144
|
+
action?: TaskListCardAction;
|
|
145
|
+
usage?: string;
|
|
146
|
+
description: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface HelpCardSection {
|
|
150
|
+
title: string;
|
|
151
|
+
commands: HelpCardCommand[];
|
|
152
|
+
}
|
|
153
|
+
|
|
142
154
|
export interface TaskListCardEntry {
|
|
143
155
|
lines: string[];
|
|
144
156
|
actions?: TaskListCardAction[];
|
|
157
|
+
current?: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export interface ResetHistoryCardEntry extends TaskListCardEntry {
|
|
161
|
+
sequence: number;
|
|
162
|
+
graphNodeLine: string;
|
|
163
|
+
graphConnectorLine?: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface ResetHistoryCardView {
|
|
167
|
+
entries: ResetHistoryCardEntry[];
|
|
168
|
+
footerLines: string[];
|
|
169
|
+
pageActions: TaskListCardAction[];
|
|
145
170
|
}
|
|
146
171
|
|
|
147
172
|
export class CardRenderer {
|
|
@@ -662,6 +687,18 @@ export class CardRenderer {
|
|
|
662
687
|
type: "danger",
|
|
663
688
|
value: { action: "turn_cancel", sessionId: state.sessionId, turnId: state.turnId },
|
|
664
689
|
}]));
|
|
690
|
+
} else if (state.status === "completed") {
|
|
691
|
+
elements.push(
|
|
692
|
+
{ tag: "hr" },
|
|
693
|
+
taskActionRow([{
|
|
694
|
+
text: "Reset",
|
|
695
|
+
value: { action: "turn_reset", sessionId: state.sessionId, turnId: state.turnId },
|
|
696
|
+
}]),
|
|
697
|
+
{
|
|
698
|
+
...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到本轮完成时;不会回退本地文件。</font>"),
|
|
699
|
+
text_size: "notation",
|
|
700
|
+
},
|
|
701
|
+
);
|
|
665
702
|
}
|
|
666
703
|
return turnCard(
|
|
667
704
|
turnTitle(state.status, state.prompt ?? state.taskTitle),
|
|
@@ -769,12 +806,25 @@ export class CardRenderer {
|
|
|
769
806
|
return sectionCard(title, elements);
|
|
770
807
|
}
|
|
771
808
|
|
|
809
|
+
renderHelpCard(
|
|
810
|
+
title: string,
|
|
811
|
+
introLines: string[],
|
|
812
|
+
sections: HelpCardSection[],
|
|
813
|
+
): Record<string, unknown> {
|
|
814
|
+
const elements: Record<string, unknown>[] = [markdown(introLines.join("\n"))];
|
|
815
|
+
for (const section of sections) {
|
|
816
|
+
elements.push({ tag: "hr" }, markdown(`**${section.title}**`));
|
|
817
|
+
elements.push(...section.commands.map(helpCommandRow));
|
|
818
|
+
}
|
|
819
|
+
return sectionCard(title, elements);
|
|
820
|
+
}
|
|
821
|
+
|
|
772
822
|
renderTaskListCard(
|
|
773
823
|
title: string,
|
|
774
824
|
sectionTitle: string,
|
|
775
825
|
entries: TaskListCardEntry[],
|
|
776
826
|
footerLines: string[],
|
|
777
|
-
|
|
827
|
+
footerActions: TaskListCardAction[] = [],
|
|
778
828
|
): Record<string, unknown> {
|
|
779
829
|
const elements: Record<string, unknown>[] = [markdown(`**${sectionTitle}**`)];
|
|
780
830
|
if (entries.length === 0) {
|
|
@@ -788,30 +838,12 @@ export class CardRenderer {
|
|
|
788
838
|
if (index < entries.length - 1) elements.push({ tag: "hr" });
|
|
789
839
|
});
|
|
790
840
|
}
|
|
841
|
+
if (footerActions.length > 0) {
|
|
842
|
+
elements.push({ tag: "hr" }, taskActionRow(footerActions));
|
|
843
|
+
}
|
|
791
844
|
if (footerLines.length > 0) {
|
|
792
845
|
elements.push({ tag: "hr" }, markdown(footerLines.join("\n")));
|
|
793
846
|
}
|
|
794
|
-
if (footerAction) {
|
|
795
|
-
elements.push({
|
|
796
|
-
tag: "column_set",
|
|
797
|
-
flex_mode: "none",
|
|
798
|
-
columns: [{
|
|
799
|
-
tag: "column",
|
|
800
|
-
width: "weighted",
|
|
801
|
-
weight: 1,
|
|
802
|
-
elements: [{
|
|
803
|
-
tag: "button",
|
|
804
|
-
text: { tag: "plain_text", content: footerAction.text },
|
|
805
|
-
type: footerAction.type ?? "default",
|
|
806
|
-
width: "fill",
|
|
807
|
-
behaviors: [{
|
|
808
|
-
type: "callback",
|
|
809
|
-
value: footerAction.value,
|
|
810
|
-
}],
|
|
811
|
-
}],
|
|
812
|
-
}],
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
847
|
return {
|
|
816
848
|
schema: "2.0",
|
|
817
849
|
config: {
|
|
@@ -831,6 +863,39 @@ export class CardRenderer {
|
|
|
831
863
|
};
|
|
832
864
|
}
|
|
833
865
|
|
|
866
|
+
renderResetHistoryCard(view: ResetHistoryCardView): Record<string, unknown> {
|
|
867
|
+
const elements: Record<string, unknown>[] = [
|
|
868
|
+
{
|
|
869
|
+
...markdown("<font color='grey'>Reset 会将当前任务的对话上下文恢复到所选轮次完成时;不会回退本地文件。</font>"),
|
|
870
|
+
text_size: "notation",
|
|
871
|
+
},
|
|
872
|
+
{ tag: "hr" },
|
|
873
|
+
markdown("**历史对话轮次**"),
|
|
874
|
+
];
|
|
875
|
+
if (view.entries.length === 0) {
|
|
876
|
+
elements.push(markdown("当前任务还没有成功完成的 turn。"));
|
|
877
|
+
} else {
|
|
878
|
+
view.entries.forEach((entry) => {
|
|
879
|
+
const action = entry.actions?.[0];
|
|
880
|
+
elements.push(resetHistoryEntryRow(
|
|
881
|
+
entry.graphNodeLine,
|
|
882
|
+
entry.graphConnectorLine,
|
|
883
|
+
entry.lines,
|
|
884
|
+
action,
|
|
885
|
+
entry.current === true,
|
|
886
|
+
));
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
if (view.footerLines.length > 0) {
|
|
890
|
+
elements.push({ tag: "hr" }, {
|
|
891
|
+
...markdown(view.footerLines.join("\n")),
|
|
892
|
+
text_size: "notation",
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
if (view.pageActions.length > 0) elements.push(taskActionRow(view.pageActions));
|
|
896
|
+
return sectionCard("历史对话轮次", elements);
|
|
897
|
+
}
|
|
898
|
+
|
|
834
899
|
private baseCard(title: string, template: string, elements: unknown[]): Record<string, unknown> {
|
|
835
900
|
return {
|
|
836
901
|
config: {
|
|
@@ -1470,6 +1535,55 @@ function settingsOptionRow(input: {
|
|
|
1470
1535
|
};
|
|
1471
1536
|
}
|
|
1472
1537
|
|
|
1538
|
+
function resetHistoryEntryRow(
|
|
1539
|
+
graphNodeLine: string,
|
|
1540
|
+
graphConnectorLine: string | undefined,
|
|
1541
|
+
lines: string[],
|
|
1542
|
+
action: TaskListCardAction | undefined,
|
|
1543
|
+
current: boolean,
|
|
1544
|
+
): Record<string, unknown> {
|
|
1545
|
+
const graph = [
|
|
1546
|
+
`<font color='${current ? "green" : "blue"}'>${escapeCardHtml(graphNodeLine)}</font>`,
|
|
1547
|
+
...(graphConnectorLine
|
|
1548
|
+
? [`<font color='grey'>${escapeCardHtml(graphConnectorLine)}</font>`]
|
|
1549
|
+
: []),
|
|
1550
|
+
].join("\n");
|
|
1551
|
+
return {
|
|
1552
|
+
tag: "column_set",
|
|
1553
|
+
flex_mode: "none",
|
|
1554
|
+
horizontal_spacing: "12px",
|
|
1555
|
+
vertical_align: "top",
|
|
1556
|
+
columns: [
|
|
1557
|
+
{
|
|
1558
|
+
tag: "column",
|
|
1559
|
+
width: "auto",
|
|
1560
|
+
vertical_align: "top",
|
|
1561
|
+
elements: [{
|
|
1562
|
+
...markdown(graph),
|
|
1563
|
+
text_align: "center",
|
|
1564
|
+
}],
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
tag: "column",
|
|
1568
|
+
width: "weighted",
|
|
1569
|
+
weight: 1,
|
|
1570
|
+
vertical_align: "top",
|
|
1571
|
+
elements: [markdown(lines.join("\n"))],
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
tag: "column",
|
|
1575
|
+
width: "auto",
|
|
1576
|
+
vertical_align: "top",
|
|
1577
|
+
elements: current
|
|
1578
|
+
? [markdown("✅ 当前")]
|
|
1579
|
+
: action
|
|
1580
|
+
? [taskActionElement(action)]
|
|
1581
|
+
: [],
|
|
1582
|
+
},
|
|
1583
|
+
],
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1473
1587
|
function taskActionRow(actions: TaskListCardAction[]): Record<string, unknown> {
|
|
1474
1588
|
return {
|
|
1475
1589
|
tag: "column_set",
|
|
@@ -1501,6 +1615,39 @@ function taskActionElement(action: TaskListCardAction): Record<string, unknown>
|
|
|
1501
1615
|
};
|
|
1502
1616
|
}
|
|
1503
1617
|
|
|
1618
|
+
function helpCommandRow(command: HelpCardCommand): Record<string, unknown> {
|
|
1619
|
+
const details = [
|
|
1620
|
+
command.usage ? `**${command.usage}**` : undefined,
|
|
1621
|
+
command.description,
|
|
1622
|
+
].filter((line): line is string => Boolean(line)).join(" ");
|
|
1623
|
+
return {
|
|
1624
|
+
tag: "column_set",
|
|
1625
|
+
flex_mode: "none",
|
|
1626
|
+
horizontal_spacing: "12px",
|
|
1627
|
+
vertical_align: "top",
|
|
1628
|
+
margin: "2px 0 0 0",
|
|
1629
|
+
columns: [
|
|
1630
|
+
{
|
|
1631
|
+
tag: "column",
|
|
1632
|
+
width: "auto",
|
|
1633
|
+
vertical_align: "top",
|
|
1634
|
+
elements: [
|
|
1635
|
+
command.action
|
|
1636
|
+
? taskActionElement(command.action)
|
|
1637
|
+
: markdown(`**${escapeCardHtml(command.text)}**`),
|
|
1638
|
+
],
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
tag: "column",
|
|
1642
|
+
width: "weighted",
|
|
1643
|
+
weight: 1,
|
|
1644
|
+
vertical_align: "top",
|
|
1645
|
+
elements: [markdown(details)],
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1504
1651
|
function turnCard(
|
|
1505
1652
|
title: string,
|
|
1506
1653
|
template: string,
|
|
@@ -5,6 +5,7 @@ import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
|
|
|
5
5
|
import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
6
6
|
|
|
7
7
|
type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
|
|
8
|
+
type BotOpenIdListener = (botOpenId: string) => void;
|
|
8
9
|
|
|
9
10
|
export class FeishuConnector {
|
|
10
11
|
constructor(
|
|
@@ -12,6 +13,7 @@ export class FeishuConnector {
|
|
|
12
13
|
private readonly handler: FeishuEventHandler,
|
|
13
14
|
private readonly logger: Logger,
|
|
14
15
|
private readonly botOpenIdResolver: BotOpenIdResolver = resolveFeishuBotOpenId,
|
|
16
|
+
private readonly onBotOpenId?: BotOpenIdListener,
|
|
15
17
|
) {}
|
|
16
18
|
|
|
17
19
|
async start(): Promise<void> {
|
|
@@ -21,10 +23,17 @@ export class FeishuConnector {
|
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const respondToAllGroupMessages = this.config.feishu.respondToAllGroupMessages !== false;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
let botOpenId: string | undefined;
|
|
27
|
+
if (!respondToAllGroupMessages || this.onBotOpenId) {
|
|
28
|
+
try {
|
|
29
|
+
botOpenId = await this.botOpenIdResolver(appId, appSecret);
|
|
30
|
+
this.onBotOpenId?.(botOpenId);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (!respondToAllGroupMessages) throw error;
|
|
33
|
+
this.logger.warn({ error }, "Failed to resolve the Lark bot Open ID for the Agent environment.");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
await this.startFeishuWs(appId, appSecret, respondToAllGroupMessages ? undefined : botOpenId);
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
stop(): void {
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,13 @@ import { LocalControlServer } from "./cli/LocalControlServer.js";
|
|
|
5
5
|
import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
|
|
6
6
|
import { readPackageVersion } from "./cli/packageVersion.js";
|
|
7
7
|
import { loadConfig } from "./config/loadConfig.js";
|
|
8
|
+
import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
|
|
9
|
+
import {
|
|
10
|
+
agentBotHome,
|
|
11
|
+
defaultConfigPath,
|
|
12
|
+
defaultDotEnvPath,
|
|
13
|
+
resolveUserPath,
|
|
14
|
+
} from "./config/paths.js";
|
|
8
15
|
import { ConsoleConnector } from "./console/ConsoleConnector.js";
|
|
9
16
|
import { ConsoleTurnPresenter } from "./console/ConsoleTurnPresenter.js";
|
|
10
17
|
import { CardRenderer } from "./feishu/CardRenderer.js";
|
|
@@ -12,18 +19,26 @@ import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
|
|
|
12
19
|
import { FeishuConnector } from "./feishu/FeishuConnector.js";
|
|
13
20
|
import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
|
|
14
21
|
import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
|
|
22
|
+
import { baseChatContextKey, isThreadContextKey } from "./feishu/contextKey.js";
|
|
23
|
+
import type { MessageReplyTarget } from "./feishu/types.js";
|
|
15
24
|
import { requireServerFeishuTransport } from "./feishu/transport.js";
|
|
16
25
|
import { createLogger } from "./logging/logger.js";
|
|
17
26
|
import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
|
|
18
27
|
import { ProxySessionController } from "./proxy/ProxySessionController.js";
|
|
28
|
+
import type { AgentEnvironmentContext } from "./runtime/agentEnvironment.js";
|
|
19
29
|
import { createAgentRuntimeRegistry } from "./runtime/createAgentRuntimeRegistry.js";
|
|
20
30
|
import { StateStore } from "./state/StateStore.js";
|
|
21
31
|
import { StartupNotifier } from "./startup/StartupNotifier.js";
|
|
22
32
|
import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
|
|
23
33
|
import { startFeishu } from "./startup/startFeishu.js";
|
|
24
34
|
import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
|
|
25
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
RESTART_GROUP_CONTEXTS_ENV,
|
|
37
|
+
replacementSupervisorEnvironment,
|
|
38
|
+
restartGroupContextKeysFromEnvironment,
|
|
39
|
+
} from "./supervision/replacementSupervisor.js";
|
|
26
40
|
import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
41
|
+
import type { RestartNotificationTarget } from "./supervision/SafeRestartScheduler.js";
|
|
27
42
|
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
28
43
|
import {
|
|
29
44
|
nodeDiagnosticReportArguments,
|
|
@@ -37,13 +52,23 @@ const agentBotVersion = readPackageVersion();
|
|
|
37
52
|
const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
|
|
38
53
|
const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
|
|
39
54
|
|| (supervised ? "Supervisor 启动" : "直接启动");
|
|
55
|
+
const restartGroupContextKeys = restartGroupContextKeysFromEnvironment(
|
|
56
|
+
process.env[RESTART_GROUP_CONTEXTS_ENV],
|
|
57
|
+
);
|
|
40
58
|
const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
|
|
41
59
|
const config = loadConfig();
|
|
42
60
|
const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.feishu);
|
|
43
61
|
const logger = createLogger(config);
|
|
44
62
|
const store = new StateStore(config.storage.sqlitePath);
|
|
45
63
|
|
|
46
|
-
const
|
|
64
|
+
const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
|
|
65
|
+
const agentEnvironmentContext: AgentEnvironmentContext = {
|
|
66
|
+
profilePath: agentBotHome(),
|
|
67
|
+
configPath: configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath(),
|
|
68
|
+
larkAppId: config.feishu.appId,
|
|
69
|
+
larkUserOpenId: config.feishu.userOpenId,
|
|
70
|
+
};
|
|
71
|
+
const runtimes = createAgentRuntimeRegistry(config, logger, agentEnvironmentContext);
|
|
47
72
|
|
|
48
73
|
const routes: OutboundRoute[] = [];
|
|
49
74
|
let feishuConnector: FeishuConnector | undefined;
|
|
@@ -92,7 +117,7 @@ let shuttingDown = false;
|
|
|
92
117
|
let restartRequested = false;
|
|
93
118
|
const safeRestart = new SafeRestartScheduler({
|
|
94
119
|
readActivity: () => store.getServerActivityState(),
|
|
95
|
-
onReady: (reason) => initiateRestart(reason),
|
|
120
|
+
onReady: (reason, notificationTargets) => initiateRestart(reason, notificationTargets),
|
|
96
121
|
onStatus: (status) => safeRestartNotifier?.update(status),
|
|
97
122
|
onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
|
|
98
123
|
});
|
|
@@ -100,9 +125,28 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
|
|
|
100
125
|
supervised,
|
|
101
126
|
restart: requestRestart,
|
|
102
127
|
cancelSafeRestart: (scheduleId) => safeRestart.cancelScheduled(scheduleId),
|
|
128
|
+
rememberFeishuUserOpenId: (userOpenId) => {
|
|
129
|
+
if (config.feishu.userOpenId?.startsWith("ou_")) return;
|
|
130
|
+
const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
|
|
131
|
+
config.feishu.userOpenId = persisted.userOpenId;
|
|
132
|
+
agentEnvironmentContext.larkUserOpenId = persisted.userOpenId;
|
|
133
|
+
process.env.FEISHU_USER_OPEN_ID = persisted.userOpenId;
|
|
134
|
+
logger.info(
|
|
135
|
+
{ status: persisted.status },
|
|
136
|
+
"Stored the default Lark user Open ID from the first private chat message.",
|
|
137
|
+
);
|
|
138
|
+
},
|
|
103
139
|
});
|
|
104
140
|
|
|
105
|
-
if (feishuOutbound)
|
|
141
|
+
if (feishuOutbound) {
|
|
142
|
+
feishuConnector = new FeishuConnector(
|
|
143
|
+
config,
|
|
144
|
+
controller,
|
|
145
|
+
logger,
|
|
146
|
+
undefined,
|
|
147
|
+
(botOpenId) => { agentEnvironmentContext.larkBotOpenId = botOpenId; },
|
|
148
|
+
);
|
|
149
|
+
}
|
|
106
150
|
if (consoleEnabled) consoleConnector = new ConsoleConnector(controller, logger);
|
|
107
151
|
|
|
108
152
|
process.on("SIGINT", () => void shutdown(0));
|
|
@@ -126,6 +170,7 @@ if (feishuConnector && startupNotifier) {
|
|
|
126
170
|
startupReason,
|
|
127
171
|
startControlServer,
|
|
128
172
|
() => { serverReady = true; },
|
|
173
|
+
restartGroupContextKeys,
|
|
129
174
|
);
|
|
130
175
|
} else {
|
|
131
176
|
await startControlServer();
|
|
@@ -134,40 +179,59 @@ if (feishuConnector && startupNotifier) {
|
|
|
134
179
|
}
|
|
135
180
|
consoleConnector?.start();
|
|
136
181
|
|
|
137
|
-
async function requestRestart(
|
|
182
|
+
async function requestRestart(
|
|
183
|
+
contextKey: string,
|
|
184
|
+
force: boolean,
|
|
185
|
+
replyTarget?: MessageReplyTarget,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const notificationTarget: RestartNotificationTarget = {
|
|
188
|
+
contextKey,
|
|
189
|
+
...(replyTarget ? { replyMessageId: replyTarget.messageId } : {}),
|
|
190
|
+
};
|
|
138
191
|
if (restartRequested) {
|
|
139
192
|
await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
|
|
140
193
|
return;
|
|
141
194
|
}
|
|
142
195
|
if (force) {
|
|
143
|
-
await initiateRestart("用户执行 /restart --force 命令",
|
|
196
|
+
await initiateRestart("用户执行 /restart --force 命令", [notificationTarget]);
|
|
144
197
|
return;
|
|
145
198
|
}
|
|
146
|
-
const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令");
|
|
199
|
+
const newlyScheduled = safeRestart.schedule("用户执行 /restart 命令", notificationTarget);
|
|
147
200
|
await outbound.sendText(
|
|
148
201
|
contextKey,
|
|
149
202
|
newlyScheduled
|
|
150
203
|
? "已安排安全重启。Agent Bot 会等待所有任务完成、最终结果投递完成,并保持 15 秒无新消息后重启。"
|
|
151
|
-
: "
|
|
204
|
+
: "安全重启已在等待中;当前会话已加入通知范围,并已更新重启原因。",
|
|
152
205
|
).catch((error: unknown) => {
|
|
153
206
|
logger.warn({ error, contextKey }, "Failed to send safe restart acknowledgement.");
|
|
154
207
|
});
|
|
155
208
|
}
|
|
156
209
|
|
|
157
|
-
async function initiateRestart(
|
|
210
|
+
async function initiateRestart(
|
|
211
|
+
reason: string,
|
|
212
|
+
notificationTargets: RestartNotificationTarget[] = [],
|
|
213
|
+
): Promise<void> {
|
|
158
214
|
if (restartRequested) {
|
|
159
|
-
|
|
215
|
+
await sendRestartTexts(notificationTargets, "Agent Bot 已在重启中,请稍候。");
|
|
160
216
|
return;
|
|
161
217
|
}
|
|
162
218
|
restartRequested = true;
|
|
163
219
|
await safeRestart.cancelCurrent();
|
|
164
220
|
await safeRestartNotifier?.flush();
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
221
|
+
const allNotificationTargets = mergeRestartNotificationTargets(
|
|
222
|
+
notificationTargets,
|
|
223
|
+
safeRestartNotifier?.getNotificationTargets() ?? [],
|
|
224
|
+
);
|
|
225
|
+
const restartGroupContextKeys = groupContextKeysForRestartTargets(allNotificationTargets);
|
|
226
|
+
await sendRestartTexts(
|
|
227
|
+
allNotificationTargets,
|
|
228
|
+
"Agent Bot 正在重启,恢复在线后会发送启动状态通知。",
|
|
229
|
+
);
|
|
230
|
+
setTimeout(() => void shutdown(
|
|
231
|
+
supervised ? STOP_EXIT_CODE : 0,
|
|
232
|
+
reason,
|
|
233
|
+
restartGroupContextKeys,
|
|
234
|
+
), 100);
|
|
171
235
|
}
|
|
172
236
|
|
|
173
237
|
async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
|
|
@@ -182,14 +246,25 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
182
246
|
startedAt: processStartedAt.toISOString(),
|
|
183
247
|
supervised,
|
|
184
248
|
feishuAppId: config.feishu.appId ?? null,
|
|
249
|
+
agents: runtimes.entries().map(([name, runtime]) => {
|
|
250
|
+
const processInfo = runtime.getProcessInfo?.() ?? {};
|
|
251
|
+
return {
|
|
252
|
+
name,
|
|
253
|
+
title: config.agents[name]?.title ?? name,
|
|
254
|
+
kind: config.agents[name]?.kind ?? runtime.kind,
|
|
255
|
+
pid: processInfo.pid ?? null,
|
|
256
|
+
version: processInfo.version ?? null,
|
|
257
|
+
};
|
|
258
|
+
}),
|
|
185
259
|
safeRestartScheduled: safeRestart.scheduled,
|
|
186
260
|
safeRestartReason: safeRestart.pendingReason,
|
|
187
261
|
activity: store.getServerActivityState(),
|
|
188
262
|
},
|
|
189
263
|
};
|
|
190
|
-
case "server_restart":
|
|
264
|
+
case "server_restart": {
|
|
265
|
+
const notificationTarget = inferControlRestartTarget(request.notificationSessionId);
|
|
191
266
|
if (request.mode === "safe") {
|
|
192
|
-
const newlyScheduled = safeRestart.schedule(request.reason);
|
|
267
|
+
const newlyScheduled = safeRestart.schedule(request.reason, notificationTarget);
|
|
193
268
|
return {
|
|
194
269
|
ok: true,
|
|
195
270
|
message: newlyScheduled
|
|
@@ -197,8 +272,12 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
197
272
|
: "A safe restart is already pending. Its reason has been updated.",
|
|
198
273
|
};
|
|
199
274
|
}
|
|
200
|
-
setTimeout(() => void initiateRestart(
|
|
275
|
+
setTimeout(() => void initiateRestart(
|
|
276
|
+
request.reason,
|
|
277
|
+
notificationTarget ? [notificationTarget] : [],
|
|
278
|
+
), 25);
|
|
201
279
|
return { ok: true, message: "Immediate restart requested." };
|
|
280
|
+
}
|
|
202
281
|
case "server_stop":
|
|
203
282
|
safeRestart.cancel();
|
|
204
283
|
setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
|
|
@@ -235,7 +314,11 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
235
314
|
}
|
|
236
315
|
}
|
|
237
316
|
|
|
238
|
-
async function shutdown(
|
|
317
|
+
async function shutdown(
|
|
318
|
+
exitCode: number,
|
|
319
|
+
restartReason?: string,
|
|
320
|
+
restartGroupContextKeys: string[] = [],
|
|
321
|
+
): Promise<void> {
|
|
239
322
|
if (shuttingDown) return;
|
|
240
323
|
shuttingDown = true;
|
|
241
324
|
logger.info({ exitCode }, "Shutting down Agent Bot.");
|
|
@@ -250,11 +333,11 @@ async function shutdown(exitCode: number, restartReason?: string): Promise<void>
|
|
|
250
333
|
runtimes.close();
|
|
251
334
|
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
252
335
|
store.close();
|
|
253
|
-
if (restartReason) startReplacementSupervisor(restartReason);
|
|
336
|
+
if (restartReason) startReplacementSupervisor(restartReason, restartGroupContextKeys);
|
|
254
337
|
process.exit(exitCode);
|
|
255
338
|
}
|
|
256
339
|
|
|
257
|
-
function startReplacementSupervisor(restartReason: string): void {
|
|
340
|
+
function startReplacementSupervisor(restartReason: string, restartGroupContextKeys: string[]): void {
|
|
258
341
|
const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
259
342
|
const reportDirectory = resolveSupervisorDiagnosticsPaths(config).crashReportDirectory;
|
|
260
343
|
prepareCrashReportDirectory(reportDirectory);
|
|
@@ -275,11 +358,100 @@ function startReplacementSupervisor(restartReason: string): void {
|
|
|
275
358
|
detached: true,
|
|
276
359
|
windowsHide: true,
|
|
277
360
|
stdio: "ignore",
|
|
278
|
-
env: replacementSupervisorEnvironment(
|
|
361
|
+
env: replacementSupervisorEnvironment(
|
|
362
|
+
restartReason,
|
|
363
|
+
environmentRefresh.environment,
|
|
364
|
+
restartGroupContextKeys,
|
|
365
|
+
),
|
|
279
366
|
});
|
|
280
367
|
supervisor.unref();
|
|
281
368
|
}
|
|
282
369
|
|
|
370
|
+
function inferControlRestartTarget(notificationSessionId?: string): RestartNotificationTarget | undefined {
|
|
371
|
+
const requestedSessionId = notificationSessionId?.trim();
|
|
372
|
+
if (requestedSessionId) {
|
|
373
|
+
const requested = store.getSession(requestedSessionId)
|
|
374
|
+
?? store.findSessionByRemoteSessionId(requestedSessionId);
|
|
375
|
+
if (!requested) throw new Error(`Cannot route restart notifications: unknown task ${requestedSessionId}.`);
|
|
376
|
+
return restartNotificationTargetForSession(requested);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const runningSessions = store.listAllSessions().filter((session) => session.status === "running");
|
|
380
|
+
const runningContexts = new Set(runningSessions.map((session) => session.contextKey));
|
|
381
|
+
if (runningContexts.size > 1) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
"Cannot determine which conversation requested the restart because multiple conversations are running. Pass --task <task>.",
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
const contextKey = runningContexts.size === 1 ? runningContexts.values().next().value : undefined;
|
|
387
|
+
const session = contextKey
|
|
388
|
+
? runningSessions.find((candidate) => candidate.contextKey === contextKey)
|
|
389
|
+
: undefined;
|
|
390
|
+
return session ? restartNotificationTargetForSession(session) : undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function restartNotificationTargetForSession(
|
|
394
|
+
session: { localSessionId: string; contextKey: string },
|
|
395
|
+
): RestartNotificationTarget {
|
|
396
|
+
const replyMessageId = isThreadContextKey(session.contextKey)
|
|
397
|
+
? store.findLatestMessageIdForSession(session.localSessionId, session.contextKey)
|
|
398
|
+
?? store.findLatestMessageIdForContext(session.contextKey)
|
|
399
|
+
: undefined;
|
|
400
|
+
if (isThreadContextKey(session.contextKey) && !replyMessageId) {
|
|
401
|
+
throw new Error(
|
|
402
|
+
"Cannot route restart notifications to the task topic because it has no message anchor. Send /restart in that topic instead.",
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
contextKey: session.contextKey,
|
|
407
|
+
...(replyMessageId ? { replyMessageId } : {}),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function sendRestartText(target: RestartNotificationTarget, text: string): Promise<string | undefined> {
|
|
412
|
+
if (isThreadContextKey(target.contextKey) && !target.replyMessageId) {
|
|
413
|
+
throw new Error("Cannot send a restart notice to a topic without a reply message anchor.");
|
|
414
|
+
}
|
|
415
|
+
const replyTarget = target.replyMessageId
|
|
416
|
+
? { messageId: target.replyMessageId, replyInThread: true as const }
|
|
417
|
+
: undefined;
|
|
418
|
+
return outbound.withReplyTarget(target.contextKey, replyTarget, () =>
|
|
419
|
+
outbound.sendText(target.contextKey, text));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
async function sendRestartTexts(targets: RestartNotificationTarget[], text: string): Promise<void> {
|
|
423
|
+
await Promise.all(targets.map(async (target) => {
|
|
424
|
+
await sendRestartText(target, text).catch((error: unknown) => {
|
|
425
|
+
logger.warn(
|
|
426
|
+
{ error, contextKey: target.contextKey },
|
|
427
|
+
"Failed to send restart acknowledgement.",
|
|
428
|
+
);
|
|
429
|
+
});
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function groupContextKeysForRestartTargets(targets: RestartNotificationTarget[]): string[] {
|
|
434
|
+
const knownGroupContextKeys = new Set(
|
|
435
|
+
store.listChatContexts("group").map((chat) => chat.contextKey),
|
|
436
|
+
);
|
|
437
|
+
return [...new Set(targets
|
|
438
|
+
.map((target) => baseChatContextKey(target.contextKey))
|
|
439
|
+
.filter((contextKey) => knownGroupContextKeys.has(contextKey)))];
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function mergeRestartNotificationTargets(
|
|
443
|
+
...targetGroups: RestartNotificationTarget[][]
|
|
444
|
+
): RestartNotificationTarget[] {
|
|
445
|
+
const targets = new Map<string, RestartNotificationTarget>();
|
|
446
|
+
for (const target of targetGroups.flat()) {
|
|
447
|
+
const existing = targets.get(target.contextKey);
|
|
448
|
+
if (!existing || (!existing.replyMessageId && target.replyMessageId)) {
|
|
449
|
+
targets.set(target.contextKey, { ...target });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return [...targets.values()];
|
|
453
|
+
}
|
|
454
|
+
|
|
283
455
|
function delay(milliseconds: number): Promise<void> {
|
|
284
456
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
285
457
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface TurnGraphNode {
|
|
2
|
+
turnId: string;
|
|
3
|
+
parentTurnId?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface TurnGraphRow {
|
|
7
|
+
sequence: number;
|
|
8
|
+
nodeLine: string;
|
|
9
|
+
connectorLine?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildTurnGraphRows(nodes: readonly TurnGraphNode[]): TurnGraphRow[] {
|
|
13
|
+
const visibleTurnIds = new Set(nodes.map((node) => node.turnId));
|
|
14
|
+
const lanes: string[] = [];
|
|
15
|
+
|
|
16
|
+
return nodes.map((node, index) => {
|
|
17
|
+
let lane = lanes.indexOf(node.turnId);
|
|
18
|
+
if (lane < 0) {
|
|
19
|
+
lane = lanes.length;
|
|
20
|
+
lanes.push(node.turnId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const sequence = index + 1;
|
|
24
|
+
const nodeLine = `${lanes.map((_, laneIndex) => laneIndex === lane ? "●" : "│").join(" ")} ${sequence}`;
|
|
25
|
+
const parentTurnId = node.parentTurnId && visibleTurnIds.has(node.parentTurnId)
|
|
26
|
+
? node.parentTurnId
|
|
27
|
+
: undefined;
|
|
28
|
+
let connectorLine: string | undefined;
|
|
29
|
+
|
|
30
|
+
if (!parentTurnId) {
|
|
31
|
+
lanes.splice(lane, 1);
|
|
32
|
+
connectorLine = lanes.length > 0 ? lanes.map(() => "│").join(" ") : undefined;
|
|
33
|
+
} else {
|
|
34
|
+
const existingParentLane = lanes.indexOf(parentTurnId);
|
|
35
|
+
if (existingParentLane < 0 || existingParentLane === lane) {
|
|
36
|
+
lanes[lane] = parentTurnId;
|
|
37
|
+
connectorLine = lanes.map(() => "│").join(" ");
|
|
38
|
+
} else {
|
|
39
|
+
const connectorLanes = lanes.map(() => "│");
|
|
40
|
+
connectorLanes[lane] = lane > existingParentLane ? "╱" : "╲";
|
|
41
|
+
connectorLine = connectorLanes.join(" ");
|
|
42
|
+
lanes.splice(lane, 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { sequence, nodeLine, ...(connectorLine ? { connectorLine } : {}) };
|
|
47
|
+
});
|
|
48
|
+
}
|