@keyou007/agent-bot 0.1.0
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 +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const MARKDOWN_LINK = /(!?)\[([^\]]*)\]\(([^)\r\n]+)\)/g;
|
|
6
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".tiff", ".bmp", ".ico"]);
|
|
7
|
+
|
|
8
|
+
export async function renderMarkdownWithLocalImages(
|
|
9
|
+
markdown: string,
|
|
10
|
+
uploadImage: (filePath: string) => Promise<string>,
|
|
11
|
+
onUploadError: (error: unknown, filePath: string) => void,
|
|
12
|
+
): Promise<Array<Record<string, unknown>>> {
|
|
13
|
+
const elements: Array<Record<string, unknown>> = [];
|
|
14
|
+
const uploads = new Map<string, Promise<string>>();
|
|
15
|
+
let cursor = 0;
|
|
16
|
+
|
|
17
|
+
for (const match of markdown.matchAll(MARKDOWN_LINK)) {
|
|
18
|
+
const matchIndex = match.index;
|
|
19
|
+
const target = match[3];
|
|
20
|
+
if (matchIndex === undefined || target === undefined) continue;
|
|
21
|
+
const filePath = localImagePath(target);
|
|
22
|
+
const imageSyntax = match[1] === "!";
|
|
23
|
+
const label = match[2]?.trim() || path.basename(unwrapTarget(target.trim()));
|
|
24
|
+
if (!filePath) {
|
|
25
|
+
if (!imageSyntax) continue;
|
|
26
|
+
appendMarkdown(elements, markdown.slice(cursor, matchIndex));
|
|
27
|
+
appendMarkdown(elements, unsupportedImageFallback(label, target));
|
|
28
|
+
cursor = matchIndex + match[0].length;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
appendMarkdown(elements, markdown.slice(cursor, matchIndex));
|
|
33
|
+
try {
|
|
34
|
+
let upload = uploads.get(filePath);
|
|
35
|
+
if (!upload) {
|
|
36
|
+
upload = uploadImage(filePath);
|
|
37
|
+
uploads.set(filePath, upload);
|
|
38
|
+
}
|
|
39
|
+
const imageKey = await upload;
|
|
40
|
+
elements.push({
|
|
41
|
+
tag: "img",
|
|
42
|
+
img_key: imageKey,
|
|
43
|
+
alt: { tag: "plain_text", content: label },
|
|
44
|
+
title: { tag: "plain_text", content: label },
|
|
45
|
+
mode: "fit_horizontal",
|
|
46
|
+
preview: true,
|
|
47
|
+
});
|
|
48
|
+
} catch (error) {
|
|
49
|
+
onUploadError(error, filePath);
|
|
50
|
+
appendMarkdown(elements, `图片上传失败:${label}`);
|
|
51
|
+
}
|
|
52
|
+
cursor = matchIndex + match[0].length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
appendMarkdown(elements, markdown.slice(cursor));
|
|
56
|
+
return elements;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function localImagePath(rawTarget: string): string | undefined {
|
|
60
|
+
const target = unwrapTarget(rawTarget.trim());
|
|
61
|
+
let candidate: string;
|
|
62
|
+
try {
|
|
63
|
+
candidate = target.startsWith("file://") ? fileURLToPath(target) : decodeURIComponent(target);
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (/^\/[a-z]:[\\/]/i.test(candidate)) candidate = candidate.slice(1);
|
|
68
|
+
if (!path.isAbsolute(candidate) && !path.win32.isAbsolute(candidate)) return undefined;
|
|
69
|
+
const resolved = path.resolve(candidate);
|
|
70
|
+
if (!IMAGE_EXTENSIONS.has(path.extname(resolved).toLowerCase())) return undefined;
|
|
71
|
+
try {
|
|
72
|
+
return fs.statSync(resolved).isFile() ? resolved : undefined;
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function unwrapTarget(target: string): string {
|
|
79
|
+
return target.startsWith("<") && target.endsWith(">") ? target.slice(1, -1) : target;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function unsupportedImageFallback(label: string, rawTarget: string): string {
|
|
83
|
+
const target = unwrapTarget(rawTarget.trim());
|
|
84
|
+
return /^https?:\/\//i.test(target)
|
|
85
|
+
? `[${label || "图片"}](${target})`
|
|
86
|
+
: `图片不可用:${label || "本地图片"}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function appendMarkdown(elements: Array<Record<string, unknown>>, content: string): void {
|
|
90
|
+
if (!content) return;
|
|
91
|
+
elements.push({ tag: "markdown", content });
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const THREAD_CONTEXT_MARKER = ":thread_id:";
|
|
2
|
+
|
|
3
|
+
export function threadContextKey(chatId: string, threadId: string): string {
|
|
4
|
+
return `chat_id:${chatId}${THREAD_CONTEXT_MARKER}${threadId}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isThreadContextKey(contextKey: string): boolean {
|
|
8
|
+
return contextKey.startsWith("chat_id:") && contextKey.includes(THREAD_CONTEXT_MARKER);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function baseChatContextKey(contextKey: string): string {
|
|
12
|
+
const markerIndex = contextKey.indexOf(THREAD_CONTEXT_MARKER);
|
|
13
|
+
return markerIndex < 0 ? contextKey : contextKey.slice(0, markerIndex);
|
|
14
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AppConfig } from "../config/schema.js";
|
|
2
|
+
|
|
3
|
+
export type FeishuTransportMode = "sdk" | "console";
|
|
4
|
+
|
|
5
|
+
type FeishuTransportConfig = Pick<
|
|
6
|
+
AppConfig["feishu"],
|
|
7
|
+
"transport" | "appId" | "appSecret" | "useConsoleWhenMissingCredentials"
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
export function resolveFeishuTransport(config: FeishuTransportConfig): FeishuTransportMode {
|
|
11
|
+
if (config.transport === "console") {
|
|
12
|
+
return "console";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const hasCredentials = Boolean(config.appId && config.appSecret);
|
|
16
|
+
if (config.transport === "sdk" || !config.useConsoleWhenMissingCredentials) {
|
|
17
|
+
if (!hasCredentials) {
|
|
18
|
+
throw new Error("Feishu appId/appSecret are required.");
|
|
19
|
+
}
|
|
20
|
+
return "sdk";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return hasCredentials ? "sdk" : "console";
|
|
24
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export interface IncomingMessage {
|
|
2
|
+
messageId: string;
|
|
3
|
+
contextKey: string;
|
|
4
|
+
chatId?: string;
|
|
5
|
+
chatType?: "p2p" | "group";
|
|
6
|
+
userId?: string;
|
|
7
|
+
replyInThread?: boolean;
|
|
8
|
+
threadContext?: true;
|
|
9
|
+
threadId?: string;
|
|
10
|
+
rootMessageId?: string;
|
|
11
|
+
parentMessageId?: string;
|
|
12
|
+
text: string;
|
|
13
|
+
images?: IncomingImageReference[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IncomingImageReference {
|
|
17
|
+
imageKey: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MessageReplyTarget {
|
|
21
|
+
messageId: string;
|
|
22
|
+
replyInThread: true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CreateGroupInput {
|
|
26
|
+
name: string;
|
|
27
|
+
userOpenId: string;
|
|
28
|
+
avatarPng?: Uint8Array;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CreatedGroup {
|
|
32
|
+
chatId: string;
|
|
33
|
+
name: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CardAction {
|
|
37
|
+
actionId: string;
|
|
38
|
+
contextKey: string;
|
|
39
|
+
userId?: string;
|
|
40
|
+
messageId?: string;
|
|
41
|
+
value: Record<string, unknown>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChatUpdatedEvent {
|
|
45
|
+
chatId: string;
|
|
46
|
+
beforeName?: string;
|
|
47
|
+
afterName: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FeishuOutbound {
|
|
51
|
+
createGroup?(input: CreateGroupInput): Promise<CreatedGroup>;
|
|
52
|
+
addReaction?(messageId: string, emojiType: string): Promise<string | undefined>;
|
|
53
|
+
deleteReaction?(messageId: string, reactionId: string): Promise<void>;
|
|
54
|
+
downloadImage?(messageId: string, imageKey: string): Promise<string>;
|
|
55
|
+
sendText(contextKey: string, text: string): Promise<string | undefined>;
|
|
56
|
+
sendMarkdown(contextKey: string, markdown: string, idempotencyKey?: string): Promise<string | undefined>;
|
|
57
|
+
sendInteractiveCard(contextKey: string, card: Record<string, unknown>): Promise<string | undefined>;
|
|
58
|
+
replyText?(
|
|
59
|
+
contextKey: string,
|
|
60
|
+
target: MessageReplyTarget,
|
|
61
|
+
text: string,
|
|
62
|
+
idempotencyKey?: string,
|
|
63
|
+
): Promise<string | undefined>;
|
|
64
|
+
replyMarkdown?(
|
|
65
|
+
contextKey: string,
|
|
66
|
+
target: MessageReplyTarget,
|
|
67
|
+
markdown: string,
|
|
68
|
+
idempotencyKey?: string,
|
|
69
|
+
): Promise<string | undefined>;
|
|
70
|
+
replyInteractiveCard?(
|
|
71
|
+
contextKey: string,
|
|
72
|
+
target: MessageReplyTarget,
|
|
73
|
+
card: Record<string, unknown>,
|
|
74
|
+
idempotencyKey?: string,
|
|
75
|
+
): Promise<string | undefined>;
|
|
76
|
+
updateInteractiveCard(messageId: string, card: Record<string, unknown>): Promise<void>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface FeishuEventHandler {
|
|
80
|
+
onMessage(message: IncomingMessage): Promise<void>;
|
|
81
|
+
onCardAction(action: CardAction): Promise<void>;
|
|
82
|
+
onChatUpdated?(event: ChatUpdatedEvent): Promise<void>;
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { AcpProcessManager } from "./acp/AcpProcessManager.js";
|
|
5
|
+
import { AcpSessionManager } from "./acp/AcpSessionManager.js";
|
|
6
|
+
import { CodexProcessManager } from "./codex/CodexProcessManager.js";
|
|
7
|
+
import { CodexRuntime } from "./codex/CodexRuntime.js";
|
|
8
|
+
import { LocalControlServer } from "./cli/LocalControlServer.js";
|
|
9
|
+
import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cli/controlProtocol.js";
|
|
10
|
+
import { loadConfig } from "./config/loadConfig.js";
|
|
11
|
+
import { ConsoleConnector } from "./console/ConsoleConnector.js";
|
|
12
|
+
import { ConsoleTurnPresenter } from "./console/ConsoleTurnPresenter.js";
|
|
13
|
+
import { CardRenderer } from "./feishu/CardRenderer.js";
|
|
14
|
+
import { ConsoleFeishuClient } from "./feishu/ConsoleFeishuClient.js";
|
|
15
|
+
import { FeishuConnector } from "./feishu/FeishuConnector.js";
|
|
16
|
+
import { FeishuMessageClient } from "./feishu/FeishuMessageClient.js";
|
|
17
|
+
import { FeishuTurnPresenter } from "./feishu/FeishuTurnPresenter.js";
|
|
18
|
+
import { resolveFeishuTransport } from "./feishu/transport.js";
|
|
19
|
+
import { createLogger } from "./logging/logger.js";
|
|
20
|
+
import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
|
|
21
|
+
import { ProxySessionController } from "./proxy/ProxySessionController.js";
|
|
22
|
+
import { AcpRuntimeAdapter } from "./runtime/AcpRuntimeAdapter.js";
|
|
23
|
+
import { AgentRuntimeRegistry } from "./runtime/AgentRuntimeRegistry.js";
|
|
24
|
+
import { StateStore } from "./state/StateStore.js";
|
|
25
|
+
import { StartupNotifier } from "./startup/StartupNotifier.js";
|
|
26
|
+
import { SessionMetadataHydrator } from "./startup/SessionMetadataHydrator.js";
|
|
27
|
+
import { startFeishu } from "./startup/startFeishu.js";
|
|
28
|
+
import { STOP_EXIT_CODE } from "./supervision/restartPolicy.js";
|
|
29
|
+
import { replacementSupervisorEnvironment } from "./supervision/replacementSupervisor.js";
|
|
30
|
+
import { SafeRestartScheduler } from "./supervision/SafeRestartScheduler.js";
|
|
31
|
+
import { SafeRestartNotifier } from "./supervision/SafeRestartNotifier.js";
|
|
32
|
+
|
|
33
|
+
const processStartedAt = new Date();
|
|
34
|
+
const supervised = process.env.AGENT_BOT_SUPERVISED === "1";
|
|
35
|
+
const startupReason = process.env.AGENT_BOT_RESTART_REASON?.trim()
|
|
36
|
+
|| (supervised ? "Supervisor 启动" : "直接启动");
|
|
37
|
+
const consoleOnly = process.env.AGENT_BOT_CONSOLE_ONLY === "1";
|
|
38
|
+
const config = loadConfig();
|
|
39
|
+
const logger = createLogger(config);
|
|
40
|
+
const store = new StateStore(config.storage.sqlitePath);
|
|
41
|
+
|
|
42
|
+
const acpProcessManager = new AcpProcessManager(logger);
|
|
43
|
+
const acpSessionManager = new AcpSessionManager(config, acpProcessManager, logger);
|
|
44
|
+
const acpRuntime = new AcpRuntimeAdapter(acpSessionManager);
|
|
45
|
+
const codexAgent = Object.values(config.agents).find((agent) => agent.kind === "codex");
|
|
46
|
+
const codexProcessManager = new CodexProcessManager(
|
|
47
|
+
codexAgent?.command ?? "codex",
|
|
48
|
+
codexAgent?.args ?? ["app-server", "--listen", "stdio://"],
|
|
49
|
+
codexAgent?.env ?? {},
|
|
50
|
+
logger,
|
|
51
|
+
);
|
|
52
|
+
const codexRuntime = new CodexRuntime(codexProcessManager, logger);
|
|
53
|
+
const runtimes = new AgentRuntimeRegistry({ acp: acpRuntime, codex: codexRuntime });
|
|
54
|
+
|
|
55
|
+
const transport = consoleOnly ? "console" : resolveFeishuTransport(config.feishu);
|
|
56
|
+
const routes: OutboundRoute[] = [];
|
|
57
|
+
let feishuConnector: FeishuConnector | undefined;
|
|
58
|
+
let consoleConnector: ConsoleConnector | undefined;
|
|
59
|
+
let startupNotifier: StartupNotifier | undefined;
|
|
60
|
+
let safeRestartNotifier: SafeRestartNotifier | undefined;
|
|
61
|
+
let controlServer: LocalControlServer | undefined;
|
|
62
|
+
|
|
63
|
+
const feishuOutbound = transport === "sdk" ? new FeishuMessageClient(config, logger) : undefined;
|
|
64
|
+
if (feishuOutbound) {
|
|
65
|
+
const renderer = new CardRenderer();
|
|
66
|
+
const presenter = new FeishuTurnPresenter(feishuOutbound, store, renderer, {
|
|
67
|
+
normalIntervalMs: 2_000,
|
|
68
|
+
criticalGapMs: 500,
|
|
69
|
+
onError: (error) => logger.warn({ error }, "Failed to update Codex progress card."),
|
|
70
|
+
});
|
|
71
|
+
routes.push({ matches: (contextKey) => !contextKey.startsWith("console:"), outbound: feishuOutbound, presenter });
|
|
72
|
+
const defaultAgentName = config.defaults.agent!;
|
|
73
|
+
const defaultAgent = config.agents[defaultAgentName];
|
|
74
|
+
const metadataHydrator = new SessionMetadataHydrator(store, runtimes);
|
|
75
|
+
startupNotifier = new StartupNotifier(store, feishuOutbound, renderer, logger, {
|
|
76
|
+
defaultAgentName,
|
|
77
|
+
defaultAgentTitle: defaultAgent?.title ?? defaultAgentName,
|
|
78
|
+
cwd: path.resolve(config.defaults.cwd),
|
|
79
|
+
workspaceKind: defaultAgent?.kind === "codex" ? "projectless" : "project",
|
|
80
|
+
}, metadataHydrator);
|
|
81
|
+
safeRestartNotifier = new SafeRestartNotifier(store, feishuOutbound, renderer, logger);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const consoleEnabled = consoleOnly || config.console.enabled || transport === "console";
|
|
85
|
+
const consoleOutbound = consoleEnabled ? new ConsoleFeishuClient() : undefined;
|
|
86
|
+
if (consoleOutbound) {
|
|
87
|
+
routes.push({
|
|
88
|
+
matches: (contextKey) => contextKey.startsWith("console:"),
|
|
89
|
+
outbound: consoleOutbound,
|
|
90
|
+
presenter: new ConsoleTurnPresenter(consoleOutbound),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (routes.length === 0) throw new Error("Neither Feishu nor console input is enabled.");
|
|
95
|
+
const outbound = new OutboundRouter(routes);
|
|
96
|
+
let shuttingDown = false;
|
|
97
|
+
let restartRequested = false;
|
|
98
|
+
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
99
|
+
supervised,
|
|
100
|
+
restart: requestRestart,
|
|
101
|
+
});
|
|
102
|
+
const safeRestart = new SafeRestartScheduler({
|
|
103
|
+
readActivity: () => store.getServerActivityState(),
|
|
104
|
+
onReady: (reason) => initiateRestart(reason),
|
|
105
|
+
onStatus: (status) => safeRestartNotifier?.update(status),
|
|
106
|
+
onStatusError: (error) => logger.warn({ error }, "Failed to publish safe restart status."),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
if (feishuOutbound) feishuConnector = new FeishuConnector(config, controller, logger);
|
|
110
|
+
if (consoleEnabled) consoleConnector = new ConsoleConnector(controller, logger);
|
|
111
|
+
|
|
112
|
+
process.on("SIGINT", () => void shutdown(0));
|
|
113
|
+
process.on("SIGTERM", () => void shutdown(0));
|
|
114
|
+
|
|
115
|
+
const startControlServer = async (): Promise<void> => {
|
|
116
|
+
if (consoleOnly || controlServer) return;
|
|
117
|
+
controlServer = new LocalControlServer(
|
|
118
|
+
controlEndpoint(config.storage.sqlitePath),
|
|
119
|
+
handleControlRequest,
|
|
120
|
+
);
|
|
121
|
+
await controlServer.start();
|
|
122
|
+
logger.info({ endpoint: controlEndpoint(config.storage.sqlitePath) }, "Local Agent Bot control endpoint started.");
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (feishuConnector && startupNotifier) {
|
|
126
|
+
await startFeishu(feishuConnector, startupNotifier, processStartedAt, startupReason, startControlServer);
|
|
127
|
+
} else {
|
|
128
|
+
await startControlServer();
|
|
129
|
+
await feishuConnector?.start();
|
|
130
|
+
}
|
|
131
|
+
consoleConnector?.start();
|
|
132
|
+
|
|
133
|
+
async function requestRestart(contextKey: string): Promise<void> {
|
|
134
|
+
await initiateRestart("用户执行 /restart 命令", contextKey);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function initiateRestart(reason: string, contextKey?: string): Promise<void> {
|
|
138
|
+
if (restartRequested) {
|
|
139
|
+
if (contextKey) await outbound.sendText(contextKey, "Agent Bot 已在重启中,请稍候。").catch(() => undefined);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
restartRequested = true;
|
|
143
|
+
safeRestart.cancel();
|
|
144
|
+
await safeRestartNotifier?.flush();
|
|
145
|
+
if (contextKey) {
|
|
146
|
+
await outbound.sendText(contextKey, "Agent Bot 正在重启,恢复在线后会发送启动状态通知。").catch((error: unknown) => {
|
|
147
|
+
logger.warn({ error, contextKey }, "Failed to send restart acknowledgement.");
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
setTimeout(() => void shutdown(supervised ? STOP_EXIT_CODE : 0, reason), 100);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function handleControlRequest(request: ControlRequest): Promise<ControlResponse> {
|
|
154
|
+
switch (request.action) {
|
|
155
|
+
case "health":
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
data: {
|
|
159
|
+
pid: process.pid,
|
|
160
|
+
startedAt: processStartedAt.toISOString(),
|
|
161
|
+
supervised,
|
|
162
|
+
safeRestartScheduled: safeRestart.scheduled,
|
|
163
|
+
safeRestartReason: safeRestart.pendingReason,
|
|
164
|
+
activity: store.getServerActivityState(),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
case "server_restart":
|
|
168
|
+
if (request.mode === "safe") {
|
|
169
|
+
const newlyScheduled = safeRestart.schedule(request.reason);
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
message: newlyScheduled
|
|
173
|
+
? "已安排安全重启;将在全部任务完成并持续空闲 15 秒后执行。"
|
|
174
|
+
: "安全重启已在等待中,已更新重启原因。",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
setTimeout(() => void initiateRestart(request.reason), 25);
|
|
178
|
+
return { ok: true, message: "已请求立即重启。" };
|
|
179
|
+
case "server_stop":
|
|
180
|
+
safeRestart.cancel();
|
|
181
|
+
setTimeout(() => void shutdown(STOP_EXIT_CODE), 25);
|
|
182
|
+
return { ok: true, message: "已请求停止 Agent Bot server。" };
|
|
183
|
+
case "task_status":
|
|
184
|
+
return { ok: true, data: await controller.controlGetTaskStatus(request.localSessionId) };
|
|
185
|
+
case "task_stop":
|
|
186
|
+
return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
|
|
187
|
+
case "task_title":
|
|
188
|
+
return { ok: true, message: await controller.controlSetTaskTitle(request.localSessionId, request.title) };
|
|
189
|
+
case "task_prompt":
|
|
190
|
+
return { ok: true, message: await controller.controlSendTaskPrompt(request.localSessionId, request.text) };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function shutdown(exitCode: number, restartReason?: string): Promise<void> {
|
|
195
|
+
if (shuttingDown) return;
|
|
196
|
+
shuttingDown = true;
|
|
197
|
+
logger.info({ exitCode }, "Shutting down Agent Bot.");
|
|
198
|
+
safeRestart.cancel();
|
|
199
|
+
feishuConnector?.stop();
|
|
200
|
+
consoleConnector?.stop();
|
|
201
|
+
controller.close();
|
|
202
|
+
await Promise.race([
|
|
203
|
+
outbound.flushAll().catch((error: unknown) => logger.warn({ error }, "Failed to flush presenters.")),
|
|
204
|
+
delay(5_000),
|
|
205
|
+
]);
|
|
206
|
+
runtimes.close();
|
|
207
|
+
acpProcessManager.stopAll();
|
|
208
|
+
await controlServer?.close().catch((error: unknown) => logger.warn({ error }, "Failed to close local control endpoint."));
|
|
209
|
+
store.close();
|
|
210
|
+
if (restartReason) startReplacementSupervisor(restartReason);
|
|
211
|
+
process.exit(exitCode);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function startReplacementSupervisor(restartReason: string): void {
|
|
215
|
+
const supervisorEntry = fileURLToPath(new URL("./supervisor.js", import.meta.url));
|
|
216
|
+
const supervisor = spawn(process.execPath, [supervisorEntry], {
|
|
217
|
+
cwd: process.cwd(),
|
|
218
|
+
detached: true,
|
|
219
|
+
windowsHide: true,
|
|
220
|
+
stdio: "ignore",
|
|
221
|
+
env: replacementSupervisorEnvironment(restartReason),
|
|
222
|
+
});
|
|
223
|
+
supervisor.unref();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function delay(milliseconds: number): Promise<void> {
|
|
227
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
228
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pino, { type Logger } from "pino";
|
|
4
|
+
import type { AppConfig } from "../config/schema.js";
|
|
5
|
+
|
|
6
|
+
export function createLogger(config: AppConfig): Logger {
|
|
7
|
+
const streams: pino.StreamEntry[] = [
|
|
8
|
+
{
|
|
9
|
+
stream: process.stdout,
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
if (config.logging.path) {
|
|
14
|
+
fs.mkdirSync(path.dirname(config.logging.path), { recursive: true });
|
|
15
|
+
streams.push({
|
|
16
|
+
stream: fs.createWriteStream(config.logging.path, { flags: "a" }),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return pino(
|
|
21
|
+
{
|
|
22
|
+
level: config.logging.level,
|
|
23
|
+
base: undefined,
|
|
24
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
25
|
+
},
|
|
26
|
+
pino.multistream(streams),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type { AgentEvent } from "../runtime/types.js";
|
|
3
|
+
import type {
|
|
4
|
+
CreatedGroup,
|
|
5
|
+
CreateGroupInput,
|
|
6
|
+
FeishuOutbound,
|
|
7
|
+
MessageReplyTarget,
|
|
8
|
+
} from "../feishu/types.js";
|
|
9
|
+
|
|
10
|
+
export interface TurnPresenter {
|
|
11
|
+
registerSession(sessionId: string, contextKey: string, taskTitle?: string, projectCwd?: string): void;
|
|
12
|
+
updateSessionTitle(sessionId: string, taskTitle: string): void;
|
|
13
|
+
unregisterSession(sessionId: string): void;
|
|
14
|
+
startPendingTurn(
|
|
15
|
+
sessionId: string,
|
|
16
|
+
contextKey: string,
|
|
17
|
+
taskTitle?: string,
|
|
18
|
+
replyTarget?: MessageReplyTarget,
|
|
19
|
+
prompt?: string,
|
|
20
|
+
): Promise<void>;
|
|
21
|
+
failPendingTurn(sessionId: string, message: string): Promise<void>;
|
|
22
|
+
appendSteerMessage(sessionId: string, turnId: string, text: string, messageId?: string): Promise<void>;
|
|
23
|
+
onEvent(event: AgentEvent): Promise<void>;
|
|
24
|
+
showDetails(contextKey: string, turnId: string): Promise<void>;
|
|
25
|
+
showActivityPage(
|
|
26
|
+
contextKey: string,
|
|
27
|
+
turnId: string,
|
|
28
|
+
page: number | "latest",
|
|
29
|
+
messageId?: string,
|
|
30
|
+
): Promise<void>;
|
|
31
|
+
resumeDelivery(sessionId: string, contextKey: string, turnId: string): Promise<void>;
|
|
32
|
+
flushAll(): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface OutboundRoute {
|
|
36
|
+
matches(contextKey: string): boolean;
|
|
37
|
+
outbound: FeishuOutbound;
|
|
38
|
+
presenter: TurnPresenter;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export class OutboundRouter {
|
|
42
|
+
private readonly sessionRoutes = new Map<string, OutboundRoute>();
|
|
43
|
+
private readonly sessionContextKeys = new Map<string, string>();
|
|
44
|
+
private readonly sessionReplyTargets = new Map<string, MessageReplyTarget>();
|
|
45
|
+
private readonly replyTargets = new AsyncLocalStorage<{ contextKey: string; target: MessageReplyTarget }>();
|
|
46
|
+
|
|
47
|
+
constructor(private readonly routes: OutboundRoute[]) {
|
|
48
|
+
if (routes.length === 0) throw new Error("At least one outbound route is required.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
registerSession(sessionId: string, contextKey: string, taskTitle?: string, projectCwd?: string): void {
|
|
52
|
+
const route = this.route(contextKey);
|
|
53
|
+
this.sessionRoutes.set(sessionId, route);
|
|
54
|
+
this.sessionContextKeys.set(sessionId, contextKey);
|
|
55
|
+
route.presenter.registerSession(sessionId, contextKey, taskTitle, projectCwd);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getSessionContextKey(sessionId: string): string | undefined {
|
|
59
|
+
return this.sessionContextKeys.get(sessionId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getSessionReplyTarget(sessionId: string): MessageReplyTarget | undefined {
|
|
63
|
+
return this.sessionReplyTargets.get(sessionId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateSessionTitle(sessionId: string, taskTitle: string): void {
|
|
67
|
+
this.sessionRoutes.get(sessionId)?.presenter.updateSessionTitle(sessionId, taskTitle);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
unregisterSession(sessionId: string): void {
|
|
71
|
+
const route = this.sessionRoutes.get(sessionId);
|
|
72
|
+
route?.presenter.unregisterSession(sessionId);
|
|
73
|
+
this.sessionRoutes.delete(sessionId);
|
|
74
|
+
this.sessionContextKeys.delete(sessionId);
|
|
75
|
+
this.sessionReplyTargets.delete(sessionId);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async startPendingTurn(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
contextKey: string,
|
|
81
|
+
taskTitle?: string,
|
|
82
|
+
replyTarget?: MessageReplyTarget,
|
|
83
|
+
prompt?: string,
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
const route = this.route(contextKey);
|
|
86
|
+
this.sessionRoutes.set(sessionId, route);
|
|
87
|
+
this.sessionContextKeys.set(sessionId, contextKey);
|
|
88
|
+
if (replyTarget) this.sessionReplyTargets.set(sessionId, replyTarget);
|
|
89
|
+
else this.sessionReplyTargets.delete(sessionId);
|
|
90
|
+
await route.presenter.startPendingTurn(sessionId, contextKey, taskTitle, replyTarget, prompt);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async failPendingTurn(sessionId: string, message: string): Promise<void> {
|
|
94
|
+
await this.sessionRoutes.get(sessionId)?.presenter.failPendingTurn(sessionId, message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async appendSteerMessage(
|
|
98
|
+
sessionId: string,
|
|
99
|
+
turnId: string,
|
|
100
|
+
text: string,
|
|
101
|
+
messageId?: string,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
await this.sessionRoutes.get(sessionId)?.presenter.appendSteerMessage(sessionId, turnId, text, messageId);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async onEvent(event: AgentEvent): Promise<void> {
|
|
107
|
+
await this.sessionRoutes.get(event.sessionId)?.presenter.onEvent(event);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async showDetails(contextKey: string, turnId: string): Promise<void> {
|
|
111
|
+
await this.route(contextKey).presenter.showDetails(contextKey, turnId);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async showActivityPage(
|
|
115
|
+
contextKey: string,
|
|
116
|
+
turnId: string,
|
|
117
|
+
page: number | "latest",
|
|
118
|
+
messageId?: string,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
await this.route(contextKey).presenter.showActivityPage(contextKey, turnId, page, messageId);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async resumeDelivery(sessionId: string, contextKey: string, turnId: string): Promise<void> {
|
|
124
|
+
await this.route(contextKey).presenter.resumeDelivery(sessionId, contextKey, turnId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async addReaction(contextKey: string, messageId: string, emojiType: string): Promise<string | undefined> {
|
|
128
|
+
return this.route(contextKey).outbound.addReaction?.(messageId, emojiType);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async deleteReaction(contextKey: string, messageId: string, reactionId: string): Promise<void> {
|
|
132
|
+
await this.route(contextKey).outbound.deleteReaction?.(messageId, reactionId);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async downloadImage(contextKey: string, messageId: string, imageKey: string): Promise<string> {
|
|
136
|
+
const outbound = this.route(contextKey).outbound;
|
|
137
|
+
const download = outbound.downloadImage;
|
|
138
|
+
if (!download) throw new Error("当前消息通道不支持下载图片。");
|
|
139
|
+
return download.call(outbound, messageId, imageKey);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async createGroup(contextKey: string, input: CreateGroupInput): Promise<CreatedGroup> {
|
|
143
|
+
const outbound = this.route(contextKey).outbound;
|
|
144
|
+
const createGroup = outbound.createGroup;
|
|
145
|
+
if (!createGroup) throw new Error("当前消息通道不支持创建飞书群。");
|
|
146
|
+
return createGroup.call(outbound, input);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
withReplyTarget<T>(
|
|
150
|
+
contextKey: string,
|
|
151
|
+
target: MessageReplyTarget | undefined,
|
|
152
|
+
operation: () => Promise<T>,
|
|
153
|
+
): Promise<T> {
|
|
154
|
+
if (!target) return operation();
|
|
155
|
+
return this.replyTargets.run({ contextKey, target }, operation);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
sendText(contextKey: string, text: string): Promise<string | undefined> {
|
|
159
|
+
const outbound = this.route(contextKey).outbound;
|
|
160
|
+
const target = this.currentReplyTarget(contextKey);
|
|
161
|
+
return target && outbound.replyText
|
|
162
|
+
? outbound.replyText(contextKey, target, text)
|
|
163
|
+
: outbound.sendText(contextKey, text);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
sendMarkdown(contextKey: string, markdown: string): Promise<string | undefined> {
|
|
167
|
+
const outbound = this.route(contextKey).outbound;
|
|
168
|
+
const target = this.currentReplyTarget(contextKey);
|
|
169
|
+
return target && outbound.replyMarkdown
|
|
170
|
+
? outbound.replyMarkdown(contextKey, target, markdown)
|
|
171
|
+
: outbound.sendMarkdown(contextKey, markdown);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
sendInteractiveCard(contextKey: string, card: Record<string, unknown>): Promise<string | undefined> {
|
|
175
|
+
const outbound = this.route(contextKey).outbound;
|
|
176
|
+
const target = this.currentReplyTarget(contextKey);
|
|
177
|
+
return target && outbound.replyInteractiveCard
|
|
178
|
+
? outbound.replyInteractiveCard(contextKey, target, card)
|
|
179
|
+
: outbound.sendInteractiveCard(contextKey, card);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async updateInteractiveCard(
|
|
183
|
+
contextKey: string,
|
|
184
|
+
messageId: string,
|
|
185
|
+
card: Record<string, unknown>,
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
await this.route(contextKey).outbound.updateInteractiveCard(messageId, card);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async flushAll(): Promise<void> {
|
|
191
|
+
const presenters = [...new Set(this.routes.map((route) => route.presenter))];
|
|
192
|
+
await Promise.all(presenters.map((presenter) => presenter.flushAll()));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private route(contextKey: string): OutboundRoute {
|
|
196
|
+
const route = this.routes.find((candidate) => candidate.matches(contextKey));
|
|
197
|
+
if (!route) throw new Error(`No outbound route for context: ${contextKey}`);
|
|
198
|
+
return route;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private currentReplyTarget(contextKey: string): MessageReplyTarget | undefined {
|
|
202
|
+
const current = this.replyTargets.getStore();
|
|
203
|
+
return current?.contextKey === contextKey ? current.target : undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|