@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,165 @@
|
|
|
1
|
+
export type CardUpdatePriority = "normal" | "critical";
|
|
2
|
+
|
|
3
|
+
export interface CardUpdateSchedulerOptions<T> {
|
|
4
|
+
render(state: T): Record<string, unknown>;
|
|
5
|
+
write(card: Record<string, unknown>): Promise<void>;
|
|
6
|
+
normalIntervalMs?: number;
|
|
7
|
+
criticalGapMs?: number;
|
|
8
|
+
retryBackoffMs?: number[];
|
|
9
|
+
isRateLimit?: (error: unknown) => boolean;
|
|
10
|
+
isRetryable?: (error: unknown) => boolean;
|
|
11
|
+
onError?: (error: unknown) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface FlushWaiter {
|
|
15
|
+
version: number;
|
|
16
|
+
resolve(): void;
|
|
17
|
+
reject(error: unknown): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class CardUpdateScheduler<T> {
|
|
21
|
+
private latestState?: T;
|
|
22
|
+
private version = 0;
|
|
23
|
+
private dirty = false;
|
|
24
|
+
private priority: CardUpdatePriority = "normal";
|
|
25
|
+
private lastWriteAt = Date.now();
|
|
26
|
+
private lastHash?: string;
|
|
27
|
+
private retryIndex = 0;
|
|
28
|
+
private retryAt = 0;
|
|
29
|
+
private timer?: ReturnType<typeof setTimeout>;
|
|
30
|
+
private writing = false;
|
|
31
|
+
private disposed = false;
|
|
32
|
+
private readonly waiters: FlushWaiter[] = [];
|
|
33
|
+
private readonly normalIntervalMs: number;
|
|
34
|
+
private readonly criticalGapMs: number;
|
|
35
|
+
private readonly retryBackoffMs: number[];
|
|
36
|
+
|
|
37
|
+
constructor(private readonly options: CardUpdateSchedulerOptions<T>) {
|
|
38
|
+
this.normalIntervalMs = options.normalIntervalMs ?? 2_000;
|
|
39
|
+
this.criticalGapMs = options.criticalGapMs ?? 500;
|
|
40
|
+
this.retryBackoffMs = options.retryBackoffMs ?? [2_000, 4_000, 8_000, 16_000, 30_000];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
seed(state: T): void {
|
|
44
|
+
this.latestState = state;
|
|
45
|
+
this.lastHash = hashCard(this.options.render(state));
|
|
46
|
+
this.lastWriteAt = Date.now();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
invalidateRenderedCard(): void {
|
|
50
|
+
this.lastHash = undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update(state: T, priority: CardUpdatePriority = "normal"): void {
|
|
54
|
+
if (this.disposed) return;
|
|
55
|
+
this.latestState = state;
|
|
56
|
+
this.version += 1;
|
|
57
|
+
this.dirty = true;
|
|
58
|
+
if (priority === "critical") this.priority = "critical";
|
|
59
|
+
this.schedule();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
flush(state?: T): Promise<void> {
|
|
63
|
+
if (state !== undefined) this.update(state, "critical");
|
|
64
|
+
else if (this.latestState !== undefined) this.update(this.latestState, "critical");
|
|
65
|
+
if (!this.dirty) return Promise.resolve();
|
|
66
|
+
|
|
67
|
+
const version = this.version;
|
|
68
|
+
return new Promise<void>((resolve, reject) => {
|
|
69
|
+
this.waiters.push({ version, resolve, reject });
|
|
70
|
+
this.schedule();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
dispose(): void {
|
|
75
|
+
this.disposed = true;
|
|
76
|
+
if (this.timer) clearTimeout(this.timer);
|
|
77
|
+
this.timer = undefined;
|
|
78
|
+
const error = new Error("Card update scheduler was disposed.");
|
|
79
|
+
this.rejectWaiters(Number.POSITIVE_INFINITY, error);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private schedule(): void {
|
|
83
|
+
if (this.disposed || this.writing || !this.dirty || this.timer) return;
|
|
84
|
+
const gap = this.priority === "critical" ? this.criticalGapMs : this.normalIntervalMs;
|
|
85
|
+
const dueAt = Math.max(this.lastWriteAt + gap, this.retryAt);
|
|
86
|
+
const delay = Math.max(0, dueAt - Date.now());
|
|
87
|
+
this.timer = setTimeout(() => {
|
|
88
|
+
this.timer = undefined;
|
|
89
|
+
void this.drain();
|
|
90
|
+
}, delay);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async drain(): Promise<void> {
|
|
94
|
+
if (this.disposed || this.writing || !this.dirty || this.latestState === undefined) return;
|
|
95
|
+
this.writing = true;
|
|
96
|
+
const state = this.latestState;
|
|
97
|
+
const version = this.version;
|
|
98
|
+
const card = this.options.render(state);
|
|
99
|
+
const hash = hashCard(card);
|
|
100
|
+
|
|
101
|
+
if (hash === this.lastHash) {
|
|
102
|
+
this.markHandled(version);
|
|
103
|
+
this.writing = false;
|
|
104
|
+
this.schedule();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await this.options.write(card);
|
|
110
|
+
this.lastHash = hash;
|
|
111
|
+
this.lastWriteAt = Date.now();
|
|
112
|
+
this.retryAt = 0;
|
|
113
|
+
this.retryIndex = 0;
|
|
114
|
+
this.markHandled(version);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (this.isRetryable(error)) {
|
|
117
|
+
const index = Math.min(this.retryIndex, this.retryBackoffMs.length - 1);
|
|
118
|
+
this.retryAt = Date.now() + (this.retryBackoffMs[index] ?? 30_000);
|
|
119
|
+
this.retryIndex += 1;
|
|
120
|
+
} else {
|
|
121
|
+
this.dirty = this.version > version;
|
|
122
|
+
this.rejectWaiters(version, error);
|
|
123
|
+
this.options.onError?.(error);
|
|
124
|
+
}
|
|
125
|
+
} finally {
|
|
126
|
+
this.writing = false;
|
|
127
|
+
this.schedule();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private markHandled(version: number): void {
|
|
132
|
+
this.dirty = this.version > version;
|
|
133
|
+
if (!this.dirty) this.priority = "normal";
|
|
134
|
+
const ready = this.waiters.filter((waiter) => waiter.version <= version);
|
|
135
|
+
for (const waiter of ready) waiter.resolve();
|
|
136
|
+
this.removeWaiters(ready);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private rejectWaiters(version: number, error: unknown): void {
|
|
140
|
+
const rejected = this.waiters.filter((waiter) => waiter.version <= version);
|
|
141
|
+
for (const waiter of rejected) waiter.reject(error);
|
|
142
|
+
this.removeWaiters(rejected);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private removeWaiters(waiters: FlushWaiter[]): void {
|
|
146
|
+
for (const waiter of waiters) {
|
|
147
|
+
const index = this.waiters.indexOf(waiter);
|
|
148
|
+
if (index >= 0) this.waiters.splice(index, 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private isRetryable(error: unknown): boolean {
|
|
153
|
+
if (this.options.isRetryable) return this.options.isRetryable(error);
|
|
154
|
+
if (this.options.isRateLimit?.(error)) return true;
|
|
155
|
+
return (
|
|
156
|
+
typeof error === "object" &&
|
|
157
|
+
error !== null &&
|
|
158
|
+
(("isRetryable" in error && error.isRetryable === true) || ("isRateLimit" in error && error.isRateLimit === true))
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function hashCard(card: Record<string, unknown>): string {
|
|
164
|
+
return JSON.stringify(card);
|
|
165
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FeishuOutbound } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export class ConsoleFeishuClient implements FeishuOutbound {
|
|
4
|
+
async sendText(contextKey: string, text: string): Promise<string> {
|
|
5
|
+
console.log(`\n[${contextKey}] ${text}\n`);
|
|
6
|
+
return `console-text-${Date.now()}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async sendMarkdown(contextKey: string, markdown: string): Promise<string> {
|
|
10
|
+
console.log(`\n[${contextKey}] ${markdown}\n`);
|
|
11
|
+
return `console-markdown-${Date.now()}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async sendInteractiveCard(contextKey: string, card: Record<string, unknown>): Promise<string> {
|
|
15
|
+
console.log(`\n[${contextKey}] CARD ${JSON.stringify(card, null, 2)}\n`);
|
|
16
|
+
return `console-card-${Date.now()}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async updateInteractiveCard(messageId: string, card: Record<string, unknown>): Promise<void> {
|
|
20
|
+
console.log(`\n[${messageId}] UPDATE CARD ${JSON.stringify(card, null, 2)}\n`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { AppConfig } from "../config/schema.js";
|
|
3
|
+
import { threadContextKey } from "./contextKey.js";
|
|
4
|
+
import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export class FeishuConnector {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly config: AppConfig,
|
|
9
|
+
private readonly handler: FeishuEventHandler,
|
|
10
|
+
private readonly logger: Logger,
|
|
11
|
+
) {}
|
|
12
|
+
|
|
13
|
+
async start(): Promise<void> {
|
|
14
|
+
const { appId, appSecret } = this.config.feishu;
|
|
15
|
+
if (!appId || !appSecret) {
|
|
16
|
+
throw new Error("Feishu appId/appSecret are required.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
await this.startFeishuWs(appId, appSecret);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
stop(): void {
|
|
23
|
+
// SDK connector currently relies on process lifetime.
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private async startFeishuWs(appId: string, appSecret: string): Promise<void> {
|
|
27
|
+
const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
|
|
28
|
+
const wsClient = new lark.WSClient({ appId, appSecret });
|
|
29
|
+
const eventDispatcher = new lark.EventDispatcher({}).register({
|
|
30
|
+
"im.message.receive_v1": async (data: unknown) => {
|
|
31
|
+
const message = toIncomingMessage(data);
|
|
32
|
+
if (!message) {
|
|
33
|
+
this.logger.debug({ data }, "Ignored unsupported Feishu message event.");
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
void Promise.resolve()
|
|
38
|
+
.then(() => this.handler.onMessage(message))
|
|
39
|
+
.catch((error: unknown) => {
|
|
40
|
+
this.logger.error({ error, messageId: message.messageId }, "Failed to handle Feishu message event.");
|
|
41
|
+
});
|
|
42
|
+
},
|
|
43
|
+
"im.chat.updated_v1": async (data: unknown) => {
|
|
44
|
+
const update = toChatUpdatedEvent(data);
|
|
45
|
+
if (!update) {
|
|
46
|
+
this.logger.debug({ data }, "Ignored Feishu chat update without a name change.");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (!this.handler.onChatUpdated) return;
|
|
50
|
+
|
|
51
|
+
void Promise.resolve()
|
|
52
|
+
.then(() => this.handler.onChatUpdated!(update))
|
|
53
|
+
.catch((error: unknown) => {
|
|
54
|
+
this.logger.error(
|
|
55
|
+
{ error, chatId: update.chatId, afterName: update.afterName },
|
|
56
|
+
"Failed to handle Feishu chat name update.",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
"card.action.trigger": async (data: unknown) => {
|
|
61
|
+
const action = toCardAction(data);
|
|
62
|
+
if (!action) {
|
|
63
|
+
this.logger.debug({ data }, "Ignored unsupported Feishu card action.");
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
void Promise.resolve()
|
|
68
|
+
.then(() => this.handler.onCardAction(action))
|
|
69
|
+
.catch((error: unknown) => {
|
|
70
|
+
this.logger.error({ error, actionId: action.actionId }, "Failed to handle Feishu card action.");
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
toast: {
|
|
74
|
+
type: "success",
|
|
75
|
+
content: "已处理",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
wsClient.start({ eventDispatcher });
|
|
82
|
+
this.logger.info("Feishu WebSocket connector started.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
|
|
87
|
+
const event = getFeishuEvent(data);
|
|
88
|
+
const chatId = typeof event?.chat_id === "string" ? event.chat_id : undefined;
|
|
89
|
+
const beforeName = typeof event?.before_change?.name === "string"
|
|
90
|
+
? event.before_change.name
|
|
91
|
+
: undefined;
|
|
92
|
+
const afterName = typeof event?.after_change?.name === "string"
|
|
93
|
+
? event.after_change.name
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!chatId || !afterName || beforeName === afterName) return undefined;
|
|
96
|
+
return {
|
|
97
|
+
chatId,
|
|
98
|
+
...(beforeName !== undefined ? { beforeName } : {}),
|
|
99
|
+
afterName,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function toIncomingMessage(data: unknown): IncomingMessage | undefined {
|
|
104
|
+
const event = getFeishuEvent(data);
|
|
105
|
+
const message = event?.message;
|
|
106
|
+
if (!message) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const messageType = message.message_type;
|
|
111
|
+
const content = parseJsonObject(message.content);
|
|
112
|
+
const parsed = parseMessageContent(messageType, content, message.mentions);
|
|
113
|
+
if (!parsed) return undefined;
|
|
114
|
+
const chatId = message.chat_id;
|
|
115
|
+
const chatType = message.chat_type === "group" ? "group" : "p2p";
|
|
116
|
+
const threadId = typeof message.thread_id === "string" && message.thread_id ? message.thread_id : undefined;
|
|
117
|
+
const rootMessageId = typeof message.root_id === "string" && message.root_id ? message.root_id : undefined;
|
|
118
|
+
const parentMessageId = typeof message.parent_id === "string" && message.parent_id ? message.parent_id : undefined;
|
|
119
|
+
const threadContext = Boolean(chatId && threadId);
|
|
120
|
+
const senderId =
|
|
121
|
+
event.sender?.sender_id?.open_id ??
|
|
122
|
+
event.sender?.sender_id?.user_id ??
|
|
123
|
+
event.sender?.sender_id?.union_id;
|
|
124
|
+
|
|
125
|
+
if (!chatId && !senderId) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
messageId: message.message_id ?? `${Date.now()}`,
|
|
131
|
+
contextKey: chatId
|
|
132
|
+
? threadContext
|
|
133
|
+
? threadContextKey(chatId, threadId!)
|
|
134
|
+
: `chat_id:${chatId}`
|
|
135
|
+
: `open_id:${senderId}`,
|
|
136
|
+
chatId,
|
|
137
|
+
chatType,
|
|
138
|
+
userId: senderId,
|
|
139
|
+
...(threadId ? { replyInThread: true as const } : {}),
|
|
140
|
+
...(threadContext ? { threadContext: true as const } : {}),
|
|
141
|
+
...(threadId ? { threadId } : {}),
|
|
142
|
+
...(rootMessageId ? { rootMessageId } : {}),
|
|
143
|
+
...(parentMessageId ? { parentMessageId } : {}),
|
|
144
|
+
text: parsed.text,
|
|
145
|
+
...(parsed.images.length > 0 ? { images: parsed.images.map((imageKey) => ({ imageKey })) } : {}),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseMessageContent(
|
|
150
|
+
messageType: unknown,
|
|
151
|
+
content: Record<string, unknown>,
|
|
152
|
+
mentions: unknown,
|
|
153
|
+
): { text: string; images: string[] } | undefined {
|
|
154
|
+
if (messageType === "text") {
|
|
155
|
+
const rawText = typeof content.text === "string" ? content.text : "";
|
|
156
|
+
return { text: stripLeadingMentions(rawText, mentions), images: [] };
|
|
157
|
+
}
|
|
158
|
+
if (messageType === "image") {
|
|
159
|
+
const imageKey = typeof content.image_key === "string" ? content.image_key : undefined;
|
|
160
|
+
return imageKey ? { text: "", images: [imageKey] } : undefined;
|
|
161
|
+
}
|
|
162
|
+
if (messageType !== "post") return undefined;
|
|
163
|
+
|
|
164
|
+
const locale = selectPostLocale(content);
|
|
165
|
+
if (!locale) return undefined;
|
|
166
|
+
const paragraphs: string[] = [];
|
|
167
|
+
const title = typeof locale.title === "string" ? locale.title.trim() : "";
|
|
168
|
+
if (title) paragraphs.push(title);
|
|
169
|
+
const images: string[] = [];
|
|
170
|
+
if (Array.isArray(locale.content)) {
|
|
171
|
+
for (const row of locale.content) {
|
|
172
|
+
if (!Array.isArray(row)) continue;
|
|
173
|
+
let rowText = "";
|
|
174
|
+
for (const element of row) {
|
|
175
|
+
if (!isRecord(element)) continue;
|
|
176
|
+
if ((element.tag === "text" || element.tag === "a") && typeof element.text === "string") {
|
|
177
|
+
rowText += element.text;
|
|
178
|
+
} else if (element.tag === "img" && typeof element.image_key === "string") {
|
|
179
|
+
images.push(element.image_key);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (rowText.trim()) paragraphs.push(rowText.trim());
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { text: paragraphs.join("\n"), images: [...new Set(images)] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function selectPostLocale(content: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
189
|
+
if (Array.isArray(content.content)) return content;
|
|
190
|
+
for (const locale of ["zh_cn", "en_us", "ja_jp"]) {
|
|
191
|
+
if (isRecord(content[locale])) return content[locale];
|
|
192
|
+
}
|
|
193
|
+
return Object.values(content).find(isRecord);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stripLeadingMentions(text: string, value: unknown): string {
|
|
197
|
+
if (!Array.isArray(value)) return text;
|
|
198
|
+
const mentionKeys = value.flatMap((mention): string[] => {
|
|
199
|
+
if (!isRecord(mention) || typeof mention.key !== "string" || !mention.key) return [];
|
|
200
|
+
return [mention.key];
|
|
201
|
+
});
|
|
202
|
+
let remaining = text.trimStart();
|
|
203
|
+
while (remaining) {
|
|
204
|
+
const key = mentionKeys.find((candidate) => {
|
|
205
|
+
if (!remaining.startsWith(candidate)) return false;
|
|
206
|
+
const next = remaining[candidate.length];
|
|
207
|
+
return next === undefined || /\s/u.test(next);
|
|
208
|
+
});
|
|
209
|
+
if (!key) break;
|
|
210
|
+
remaining = remaining.slice(key.length).trimStart();
|
|
211
|
+
}
|
|
212
|
+
return remaining;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function toCardAction(data: unknown) {
|
|
216
|
+
const event = getFeishuEvent(data);
|
|
217
|
+
if (!event?.action) {
|
|
218
|
+
return undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const openId = event.operator?.open_id ?? event.operator?.user_id;
|
|
222
|
+
const chatId = event.context?.open_chat_id ?? event.chat_id;
|
|
223
|
+
return {
|
|
224
|
+
actionId: getEventId(data) ?? event.action.action_id ?? event.action.name ?? `${Date.now()}`,
|
|
225
|
+
contextKey: chatId ? `chat_id:${chatId}` : `open_id:${openId}`,
|
|
226
|
+
userId: openId,
|
|
227
|
+
...(typeof event.context?.open_message_id === "string" ? { messageId: event.context.open_message_id } : {}),
|
|
228
|
+
value: isRecord(event.action.value) ? event.action.value : {},
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function getEventId(data: unknown): string | undefined {
|
|
233
|
+
const value = getNested<unknown>(data, ["header", "event_id"]) ?? getNested(data, ["data", "header", "event_id"]);
|
|
234
|
+
return typeof value === "string" ? value : undefined;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function getFeishuEvent(data: unknown): Record<string, any> | undefined {
|
|
238
|
+
return getNested<Record<string, any>>(data, ["event"]) ?? getNested(data, ["data", "event"]) ?? asRecord(data);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseJsonObject(value: unknown): Record<string, unknown> {
|
|
242
|
+
if (isRecord(value)) {
|
|
243
|
+
return value;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (typeof value !== "string") {
|
|
247
|
+
return {};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(value) as unknown;
|
|
252
|
+
return isRecord(parsed) ? parsed : {};
|
|
253
|
+
} catch {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function getNested<T>(value: unknown, path: string[]): T | undefined {
|
|
259
|
+
let current: unknown = value;
|
|
260
|
+
for (const key of path) {
|
|
261
|
+
if (!isRecord(current)) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
current = current[key];
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return current as T;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isRecord(value: unknown): value is Record<string, any> {
|
|
272
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function asRecord(value: unknown): Record<string, any> | undefined {
|
|
276
|
+
return isRecord(value) ? value : undefined;
|
|
277
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface FenceState {
|
|
2
|
+
indent: string;
|
|
3
|
+
marker: "`" | "~";
|
|
4
|
+
length: number;
|
|
5
|
+
normalizeIndent: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const FENCE_OPENER = /^([ \t]*)((?:`{3,})|(?:~{3,}))(.*)$/;
|
|
9
|
+
|
|
10
|
+
export function normalizeFeishuMarkdown(markdown: string): string {
|
|
11
|
+
const lines = markdown.match(/[^\r\n]*(?:\r\n|\n)|[^\r\n]+$/g) ?? [];
|
|
12
|
+
let fence: FenceState | undefined;
|
|
13
|
+
|
|
14
|
+
return lines.map((line) => {
|
|
15
|
+
const ending = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
|
|
16
|
+
const body = ending ? line.slice(0, -ending.length) : line;
|
|
17
|
+
|
|
18
|
+
if (!fence) {
|
|
19
|
+
const opener = FENCE_OPENER.exec(body);
|
|
20
|
+
if (!opener?.[2]) return line;
|
|
21
|
+
|
|
22
|
+
const indent = opener[1] ?? "";
|
|
23
|
+
const delimiter = opener[2];
|
|
24
|
+
fence = {
|
|
25
|
+
indent,
|
|
26
|
+
marker: delimiter[0] as "`" | "~",
|
|
27
|
+
length: delimiter.length,
|
|
28
|
+
normalizeIndent: indent.length > 0,
|
|
29
|
+
};
|
|
30
|
+
return `${fence.normalizeIndent ? body.slice(indent.length) : body}${ending}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const normalizedBody = fence.normalizeIndent && body.startsWith(fence.indent)
|
|
34
|
+
? body.slice(fence.indent.length)
|
|
35
|
+
: body;
|
|
36
|
+
if (isFenceCloser(normalizedBody, fence)) fence = undefined;
|
|
37
|
+
return `${normalizedBody}${ending}`;
|
|
38
|
+
}).join("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isFenceCloser(line: string, fence: FenceState): boolean {
|
|
42
|
+
const match = /^[ \t]*(`+|~+)[ \t]*$/.exec(line);
|
|
43
|
+
const delimiter = match?.[1];
|
|
44
|
+
return delimiter !== undefined
|
|
45
|
+
&& delimiter[0] === fence.marker
|
|
46
|
+
&& delimiter.length >= fence.length;
|
|
47
|
+
}
|