@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.
Files changed (212) hide show
  1. package/.env.example +8 -0
  2. package/CHANGELOG.md +11 -0
  3. package/LICENSE +675 -0
  4. package/README.md +187 -0
  5. package/README.zh.md +187 -0
  6. package/SECURITY.md +13 -0
  7. package/config.example.yaml +56 -0
  8. package/dist/acp/AcpJsonRpcConnection.js +155 -0
  9. package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
  10. package/dist/acp/AcpProcessManager.js +65 -0
  11. package/dist/acp/AcpProcessManager.js.map +1 -0
  12. package/dist/acp/AcpSessionManager.js +164 -0
  13. package/dist/acp/AcpSessionManager.js.map +1 -0
  14. package/dist/acp/acpTypes.js +10 -0
  15. package/dist/acp/acpTypes.js.map +1 -0
  16. package/dist/cli/FeishuAppConfiguration.js +349 -0
  17. package/dist/cli/FeishuAppConfiguration.js.map +1 -0
  18. package/dist/cli/FeishuAppRegistration.js +160 -0
  19. package/dist/cli/FeishuAppRegistration.js.map +1 -0
  20. package/dist/cli/Initializer.js +112 -0
  21. package/dist/cli/Initializer.js.map +1 -0
  22. package/dist/cli/LocalControlClient.js +47 -0
  23. package/dist/cli/LocalControlClient.js.map +1 -0
  24. package/dist/cli/LocalControlServer.js +66 -0
  25. package/dist/cli/LocalControlServer.js.map +1 -0
  26. package/dist/cli/SkillRegistry.js +126 -0
  27. package/dist/cli/SkillRegistry.js.map +1 -0
  28. package/dist/cli/controlProtocol.js +10 -0
  29. package/dist/cli/controlProtocol.js.map +1 -0
  30. package/dist/cli/taskChatRoute.js +23 -0
  31. package/dist/cli/taskChatRoute.js.map +1 -0
  32. package/dist/cli.js +616 -0
  33. package/dist/cli.js.map +1 -0
  34. package/dist/codex/AppServerConnection.js +131 -0
  35. package/dist/codex/AppServerConnection.js.map +1 -0
  36. package/dist/codex/CodexEventMapper.js +296 -0
  37. package/dist/codex/CodexEventMapper.js.map +1 -0
  38. package/dist/codex/CodexLocalActivityDetector.js +124 -0
  39. package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
  40. package/dist/codex/CodexProcessManager.js +64 -0
  41. package/dist/codex/CodexProcessManager.js.map +1 -0
  42. package/dist/codex/CodexRuntime.js +825 -0
  43. package/dist/codex/CodexRuntime.js.map +1 -0
  44. package/dist/codex/ProjectlessWorkspace.js +74 -0
  45. package/dist/codex/ProjectlessWorkspace.js.map +1 -0
  46. package/dist/codex/protocol.js +10 -0
  47. package/dist/codex/protocol.js.map +1 -0
  48. package/dist/commands/CommandRouter.js +176 -0
  49. package/dist/commands/CommandRouter.js.map +1 -0
  50. package/dist/commands/commandTypes.js +2 -0
  51. package/dist/commands/commandTypes.js.map +1 -0
  52. package/dist/config/loadConfig.js +107 -0
  53. package/dist/config/loadConfig.js.map +1 -0
  54. package/dist/config/paths.js +32 -0
  55. package/dist/config/paths.js.map +1 -0
  56. package/dist/config/schema.js +38 -0
  57. package/dist/config/schema.js.map +1 -0
  58. package/dist/console/ConsoleConnector.js +39 -0
  59. package/dist/console/ConsoleConnector.js.map +1 -0
  60. package/dist/console/ConsoleTurnPresenter.js +60 -0
  61. package/dist/console/ConsoleTurnPresenter.js.map +1 -0
  62. package/dist/feishu/CardRenderer.js +972 -0
  63. package/dist/feishu/CardRenderer.js.map +1 -0
  64. package/dist/feishu/CardUpdateScheduler.js +148 -0
  65. package/dist/feishu/CardUpdateScheduler.js.map +1 -0
  66. package/dist/feishu/ConsoleFeishuClient.js +18 -0
  67. package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
  68. package/dist/feishu/FeishuConnector.js +257 -0
  69. package/dist/feishu/FeishuConnector.js.map +1 -0
  70. package/dist/feishu/FeishuMarkdown.js +37 -0
  71. package/dist/feishu/FeishuMarkdown.js.map +1 -0
  72. package/dist/feishu/FeishuMessageClient.js +459 -0
  73. package/dist/feishu/FeishuMessageClient.js.map +1 -0
  74. package/dist/feishu/FeishuTurnPresenter.js +425 -0
  75. package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
  76. package/dist/feishu/GroupAvatarGenerator.js +86 -0
  77. package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
  78. package/dist/feishu/LocalCardImage.js +13 -0
  79. package/dist/feishu/LocalCardImage.js.map +1 -0
  80. package/dist/feishu/LocalImageMarkdown.js +89 -0
  81. package/dist/feishu/LocalImageMarkdown.js.map +1 -0
  82. package/dist/feishu/contextKey.js +12 -0
  83. package/dist/feishu/contextKey.js.map +1 -0
  84. package/dist/feishu/transport.js +14 -0
  85. package/dist/feishu/transport.js.map +1 -0
  86. package/dist/feishu/types.js +2 -0
  87. package/dist/feishu/types.js.map +1 -0
  88. package/dist/index.js +213 -0
  89. package/dist/index.js.map +1 -0
  90. package/dist/logging/logger.js +22 -0
  91. package/dist/logging/logger.js.map +1 -0
  92. package/dist/presentation/OutboundRouter.js +127 -0
  93. package/dist/presentation/OutboundRouter.js.map +1 -0
  94. package/dist/presentation/TurnStateReducer.js +307 -0
  95. package/dist/presentation/TurnStateReducer.js.map +1 -0
  96. package/dist/presentation/splitMarkdown.js +341 -0
  97. package/dist/presentation/splitMarkdown.js.map +1 -0
  98. package/dist/presentation/turnViewTypes.js +2 -0
  99. package/dist/presentation/turnViewTypes.js.map +1 -0
  100. package/dist/proxy/ProxySessionController.js +2819 -0
  101. package/dist/proxy/ProxySessionController.js.map +1 -0
  102. package/dist/runtime/AcpRuntimeAdapter.js +271 -0
  103. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
  104. package/dist/runtime/AgentRuntimeRegistry.js +18 -0
  105. package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
  106. package/dist/runtime/agentEnvironment.js +11 -0
  107. package/dist/runtime/agentEnvironment.js.map +1 -0
  108. package/dist/runtime/types.js +2 -0
  109. package/dist/runtime/types.js.map +1 -0
  110. package/dist/startup/SessionMetadataHydrator.js +46 -0
  111. package/dist/startup/SessionMetadataHydrator.js.map +1 -0
  112. package/dist/startup/StartupNotifier.js +78 -0
  113. package/dist/startup/StartupNotifier.js.map +1 -0
  114. package/dist/startup/startFeishu.js +6 -0
  115. package/dist/startup/startFeishu.js.map +1 -0
  116. package/dist/state/StateStore.js +893 -0
  117. package/dist/state/StateStore.js.map +1 -0
  118. package/dist/state/migrations.js +129 -0
  119. package/dist/state/migrations.js.map +1 -0
  120. package/dist/supervision/SafeRestartNotifier.js +116 -0
  121. package/dist/supervision/SafeRestartNotifier.js.map +1 -0
  122. package/dist/supervision/SafeRestartScheduler.js +123 -0
  123. package/dist/supervision/SafeRestartScheduler.js.map +1 -0
  124. package/dist/supervision/replacementSupervisor.js +8 -0
  125. package/dist/supervision/replacementSupervisor.js.map +1 -0
  126. package/dist/supervision/restartPolicy.js +16 -0
  127. package/dist/supervision/restartPolicy.js.map +1 -0
  128. package/dist/supervisor.js +94 -0
  129. package/dist/supervisor.js.map +1 -0
  130. package/dist/utils/executeShellCommand.js +76 -0
  131. package/dist/utils/executeShellCommand.js.map +1 -0
  132. package/dist/utils/id.js +5 -0
  133. package/dist/utils/id.js.map +1 -0
  134. package/dist/utils/markdown.js +29 -0
  135. package/dist/utils/markdown.js.map +1 -0
  136. package/dist/utils/spawnCommand.js +21 -0
  137. package/dist/utils/spawnCommand.js.map +1 -0
  138. package/dist/utils/taskTitle.js +10 -0
  139. package/dist/utils/taskTitle.js.map +1 -0
  140. package/docs/technical-reference.md +294 -0
  141. package/docs/technical-reference.zh.md +294 -0
  142. package/npm-shrinkwrap.json +4525 -0
  143. package/package.json +87 -0
  144. package/skills/agent-bot/SKILL.md +141 -0
  145. package/skills/agent-bot/agents/openai.yaml +4 -0
  146. package/src/acp/AcpJsonRpcConnection.ts +212 -0
  147. package/src/acp/AcpProcessManager.ts +87 -0
  148. package/src/acp/AcpSessionManager.ts +227 -0
  149. package/src/acp/acpTypes.ts +99 -0
  150. package/src/cli/FeishuAppConfiguration.ts +473 -0
  151. package/src/cli/FeishuAppRegistration.ts +207 -0
  152. package/src/cli/Initializer.ts +159 -0
  153. package/src/cli/LocalControlClient.ts +48 -0
  154. package/src/cli/LocalControlServer.ts +67 -0
  155. package/src/cli/SkillRegistry.ts +151 -0
  156. package/src/cli/controlProtocol.ts +34 -0
  157. package/src/cli/taskChatRoute.ts +31 -0
  158. package/src/cli.ts +630 -0
  159. package/src/codex/AppServerConnection.ts +159 -0
  160. package/src/codex/CodexEventMapper.ts +351 -0
  161. package/src/codex/CodexLocalActivityDetector.ts +148 -0
  162. package/src/codex/CodexProcessManager.ts +63 -0
  163. package/src/codex/CodexRuntime.ts +1025 -0
  164. package/src/codex/ProjectlessWorkspace.ts +90 -0
  165. package/src/codex/protocol.ts +37 -0
  166. package/src/commands/CommandRouter.ts +179 -0
  167. package/src/commands/commandTypes.ts +28 -0
  168. package/src/config/loadConfig.ts +115 -0
  169. package/src/config/paths.ts +37 -0
  170. package/src/config/schema.ts +46 -0
  171. package/src/console/ConsoleConnector.ts +40 -0
  172. package/src/console/ConsoleTurnPresenter.ts +64 -0
  173. package/src/feishu/CardRenderer.ts +1157 -0
  174. package/src/feishu/CardUpdateScheduler.ts +165 -0
  175. package/src/feishu/ConsoleFeishuClient.ts +22 -0
  176. package/src/feishu/FeishuConnector.ts +277 -0
  177. package/src/feishu/FeishuMarkdown.ts +47 -0
  178. package/src/feishu/FeishuMessageClient.ts +649 -0
  179. package/src/feishu/FeishuTurnPresenter.ts +539 -0
  180. package/src/feishu/GroupAvatarGenerator.ts +102 -0
  181. package/src/feishu/LocalCardImage.ts +13 -0
  182. package/src/feishu/LocalImageMarkdown.ts +92 -0
  183. package/src/feishu/contextKey.ts +14 -0
  184. package/src/feishu/transport.ts +24 -0
  185. package/src/feishu/types.ts +83 -0
  186. package/src/index.ts +228 -0
  187. package/src/logging/logger.ts +28 -0
  188. package/src/presentation/OutboundRouter.ts +205 -0
  189. package/src/presentation/TurnStateReducer.ts +341 -0
  190. package/src/presentation/splitMarkdown.ts +358 -0
  191. package/src/presentation/turnViewTypes.ts +54 -0
  192. package/src/proxy/ProxySessionController.ts +3311 -0
  193. package/src/runtime/AcpRuntimeAdapter.ts +324 -0
  194. package/src/runtime/AgentRuntimeRegistry.ts +20 -0
  195. package/src/runtime/agentEnvironment.ts +14 -0
  196. package/src/runtime/types.ts +215 -0
  197. package/src/startup/SessionMetadataHydrator.ts +46 -0
  198. package/src/startup/StartupNotifier.ts +92 -0
  199. package/src/startup/startFeishu.ts +19 -0
  200. package/src/state/StateStore.ts +1249 -0
  201. package/src/state/migrations.ts +128 -0
  202. package/src/supervision/SafeRestartNotifier.ts +122 -0
  203. package/src/supervision/SafeRestartScheduler.ts +149 -0
  204. package/src/supervision/replacementSupervisor.ts +10 -0
  205. package/src/supervision/restartPolicy.ts +19 -0
  206. package/src/supervisor.ts +106 -0
  207. package/src/utils/executeShellCommand.ts +97 -0
  208. package/src/utils/id.ts +5 -0
  209. package/src/utils/markdown.ts +25 -0
  210. package/src/utils/spawnCommand.ts +35 -0
  211. package/src/utils/taskTitle.ts +9 -0
  212. package/tsconfig.json +19 -0
@@ -0,0 +1,473 @@
1
+ import { gzipSync } from "node:zlib";
2
+ import type { FeishuAppCredentials } from "./FeishuAppRegistration.js";
3
+
4
+ const FEISHU_OPEN_BASE_URL = "https://open.feishu.cn";
5
+ const TENANT_TOKEN_PATH = "/open-apis/auth/v3/tenant_access_token/internal";
6
+ const APPLICATION_PATH_PREFIX = "/open-apis/application/v6/applications";
7
+ const DEFAULT_POLL_INTERVAL_MS = 5_000;
8
+ const DEFAULT_COMPLETION_TIMEOUT_MS = 10 * 60_000;
9
+ const REQUEST_TIMEOUT_MS = 30_000;
10
+
11
+ interface ScopeRequirement {
12
+ requested: string;
13
+ accepted: string[];
14
+ }
15
+
16
+ const REQUIRED_SCOPE_REQUIREMENTS: ScopeRequirement[] = [
17
+ {
18
+ requested: "application:application:self_manage",
19
+ accepted: ["application:application:self_manage"],
20
+ },
21
+ {
22
+ requested: "im:chat:create",
23
+ accepted: ["im:chat:create", "im:chat"],
24
+ },
25
+ {
26
+ requested: "im:chat:read",
27
+ accepted: ["im:chat:read", "im:chat:readonly", "im:chat"],
28
+ },
29
+ {
30
+ requested: "im:message.group_at_msg:readonly",
31
+ accepted: [
32
+ "im:message.group_at_msg:readonly",
33
+ "im:message.group_at_msg.include_bot:readonly",
34
+ "im:message.group_at_msg",
35
+ "im:message.group_msg:readonly",
36
+ "im:message.group_msg",
37
+ ],
38
+ },
39
+ {
40
+ requested: "im:message.p2p_msg:readonly",
41
+ accepted: ["im:message.p2p_msg:readonly", "im:message.p2p_msg"],
42
+ },
43
+ {
44
+ requested: "im:message.reactions:write_only",
45
+ accepted: ["im:message.reactions:write_only", "im:message"],
46
+ },
47
+ {
48
+ requested: "im:message:readonly",
49
+ accepted: ["im:message:readonly", "im:message.history:readonly", "im:message"],
50
+ },
51
+ {
52
+ requested: "im:message:send_as_bot",
53
+ accepted: ["im:message:send_as_bot", "im:message:send", "im:message"],
54
+ },
55
+ {
56
+ requested: "im:message:update",
57
+ accepted: ["im:message:update", "im:message:send_as_bot", "im:message"],
58
+ },
59
+ {
60
+ requested: "im:resource",
61
+ accepted: ["im:resource", "im:resource:upload"],
62
+ },
63
+ ];
64
+
65
+ export const REQUIRED_FEISHU_SCOPES = REQUIRED_SCOPE_REQUIREMENTS.map((requirement) => requirement.requested);
66
+ export const REQUIRED_FEISHU_EVENTS = ["im.message.receive_v1", "im.chat.updated_v1"] as const;
67
+ export const REQUIRED_FEISHU_CALLBACKS = ["card.action.trigger"] as const;
68
+
69
+ const CORE_FEISHU_SCOPES = new Set([
70
+ "application:application:self_manage",
71
+ "im:message.group_at_msg:readonly",
72
+ "im:message.p2p_msg:readonly",
73
+ "im:message:send_as_bot",
74
+ ]);
75
+ const CORE_FEISHU_EVENTS = new Set<string>(["im.message.receive_v1"]);
76
+
77
+ export interface FeishuAppConfiguration {
78
+ scopes: string[];
79
+ events: string[];
80
+ callbacks: string[];
81
+ }
82
+
83
+ export interface MissingFeishuAppConfiguration {
84
+ scopes: string[];
85
+ events: string[];
86
+ callbacks: string[];
87
+ }
88
+
89
+ export interface FeishuConfigurationChallenge {
90
+ verificationUrl: string;
91
+ missing: MissingFeishuAppConfiguration;
92
+ blocking: boolean;
93
+ }
94
+
95
+ export interface EnsureFeishuAppConfigurationOptions {
96
+ fetch?: typeof globalThis.fetch;
97
+ sleep?: (milliseconds: number) => Promise<void>;
98
+ signal?: AbortSignal;
99
+ pollIntervalMs?: number;
100
+ timeoutMs?: number;
101
+ onVerification?: (challenge: FeishuConfigurationChallenge) => void | Promise<void>;
102
+ }
103
+
104
+ export interface EnsureFeishuAppConfigurationResult {
105
+ status: "ready" | "updated" | "partial";
106
+ configuration: FeishuAppConfiguration;
107
+ added: MissingFeishuAppConfiguration;
108
+ remaining: MissingFeishuAppConfiguration;
109
+ }
110
+
111
+ type JsonObject = Record<string, unknown>;
112
+
113
+ export async function ensureFeishuAppConfiguration(
114
+ credentials: FeishuAppCredentials,
115
+ options: EnsureFeishuAppConfigurationOptions = {},
116
+ ): Promise<EnsureFeishuAppConfigurationResult> {
117
+ const fetchImpl = options.fetch ?? globalThis.fetch;
118
+ const sleep = options.sleep ?? delay;
119
+ const pollIntervalMs = positiveInteger(options.pollIntervalMs) ?? DEFAULT_POLL_INTERVAL_MS;
120
+ const timeoutMs = positiveInteger(options.timeoutMs) ?? DEFAULT_COMPLETION_TIMEOUT_MS;
121
+
122
+ let configuration: FeishuAppConfiguration;
123
+ try {
124
+ configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
125
+ } catch (error) {
126
+ if (!isPermissionError(error)) throw error;
127
+ configuration = emptyConfiguration();
128
+ }
129
+
130
+ const missing = missingFeishuAppConfiguration(configuration);
131
+ if (!hasMissingConfiguration(missing)) {
132
+ return {
133
+ status: "ready",
134
+ configuration,
135
+ added: emptyConfiguration(),
136
+ remaining: emptyConfiguration(),
137
+ };
138
+ }
139
+
140
+ const coreMissing = coreMissingFeishuAppConfiguration(missing);
141
+ if (!hasMissingConfiguration(coreMissing)) {
142
+ await requestMissingFeishuConfiguration(credentials.appId, missing, false, options.onVerification);
143
+ return {
144
+ status: "partial",
145
+ configuration,
146
+ added: emptyConfiguration(),
147
+ remaining: missing,
148
+ };
149
+ }
150
+
151
+ await requestMissingFeishuConfiguration(credentials.appId, coreMissing, true, options.onVerification);
152
+
153
+ let remainingMs = timeoutMs;
154
+ let lastError: unknown;
155
+ while (remainingMs > 0) {
156
+ const waitMs = Math.min(pollIntervalMs, remainingMs);
157
+ await sleepWithAbort(sleep, waitMs, options.signal);
158
+ remainingMs -= waitMs;
159
+
160
+ try {
161
+ configuration = await readFeishuAppConfiguration(credentials, fetchImpl, options.signal);
162
+ const remaining = missingFeishuAppConfiguration(configuration);
163
+ if (!hasMissingConfiguration(coreMissingFeishuAppConfiguration(remaining))) {
164
+ if (hasMissingConfiguration(remaining)) {
165
+ await requestMissingFeishuConfiguration(credentials.appId, remaining, false, options.onVerification);
166
+ }
167
+ return {
168
+ status: hasMissingConfiguration(remaining) ? "partial" : "updated",
169
+ configuration,
170
+ added: resolvedMissingConfiguration(missing, remaining),
171
+ remaining,
172
+ };
173
+ }
174
+ lastError = undefined;
175
+ } catch (error) {
176
+ if (options.signal?.aborted) throw abortError(options.signal);
177
+ lastError = error;
178
+ }
179
+ }
180
+
181
+ const detail = lastError instanceof Error ? ` 最后一次检查失败:${lastError.message}` : "";
182
+ throw new Error(`等待飞书核心权限和消息事件配置生效超时,请重新运行 agent-bot init。${detail}`);
183
+ }
184
+
185
+ async function requestMissingFeishuConfiguration(
186
+ appId: string,
187
+ missing: MissingFeishuAppConfiguration,
188
+ blocking: boolean,
189
+ onVerification: EnsureFeishuAppConfigurationOptions["onVerification"],
190
+ ): Promise<void> {
191
+ const verificationUrl = buildFeishuConfigurationUrl(appId, missing);
192
+ await onVerification?.({ verificationUrl, missing, blocking });
193
+ }
194
+
195
+ export function missingFeishuAppConfiguration(configuration: FeishuAppConfiguration): MissingFeishuAppConfiguration {
196
+ const scopes = new Set(configuration.scopes);
197
+ const events = new Set(configuration.events);
198
+ const callbacks = new Set(configuration.callbacks);
199
+ return {
200
+ scopes: REQUIRED_SCOPE_REQUIREMENTS.filter(
201
+ (requirement) => !requirement.accepted.some((scope) => scopes.has(scope)),
202
+ ).map((requirement) => requirement.requested),
203
+ events: REQUIRED_FEISHU_EVENTS.filter((event) => !events.has(event)),
204
+ callbacks: REQUIRED_FEISHU_CALLBACKS.filter((callback) => !callbacks.has(callback)),
205
+ };
206
+ }
207
+
208
+ export function buildFeishuConfigurationUrl(appId: string, missing: MissingFeishuAppConfiguration): string {
209
+ const manifest: JsonObject = {};
210
+ if (missing.scopes.length > 0) {
211
+ manifest.scopes = {
212
+ tenant: missing.scopes,
213
+ user: [],
214
+ };
215
+ }
216
+ if (missing.events.length > 0) {
217
+ manifest.events = {
218
+ items: {
219
+ tenant: missing.events,
220
+ user: [],
221
+ },
222
+ };
223
+ }
224
+ if (missing.callbacks.length > 0) {
225
+ manifest.callbacks = {
226
+ items: missing.callbacks,
227
+ };
228
+ }
229
+
230
+ const encoded = gzipSync(Buffer.from(JSON.stringify(manifest), "utf8")).toString("base64url");
231
+ const url = new URL("/page/launcher", FEISHU_OPEN_BASE_URL);
232
+ url.searchParams.set("clientID", appId);
233
+ url.searchParams.set("addons", encoded);
234
+ return url.toString();
235
+ }
236
+
237
+ async function readFeishuAppConfiguration(
238
+ credentials: FeishuAppCredentials,
239
+ fetchImpl: typeof globalThis.fetch,
240
+ signal?: AbortSignal,
241
+ ): Promise<FeishuAppConfiguration> {
242
+ const token = await getTenantAccessToken(credentials, fetchImpl, signal);
243
+ const appId = encodeURIComponent(credentials.appId);
244
+ const headers = { Authorization: `Bearer ${token}` };
245
+ const [application, versions] = await Promise.all([
246
+ fetchJson(
247
+ fetchImpl,
248
+ `${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}?lang=zh_cn`,
249
+ { headers },
250
+ signal,
251
+ "查询飞书应用配置",
252
+ ),
253
+ fetchJson(
254
+ fetchImpl,
255
+ `${FEISHU_OPEN_BASE_URL}${APPLICATION_PATH_PREFIX}/${appId}/app_versions?lang=zh_cn&page_size=2`,
256
+ { headers },
257
+ signal,
258
+ "查询飞书应用版本",
259
+ ),
260
+ ]);
261
+
262
+ const app = readObject(readObject(application, "data"), "app");
263
+ const callbackInfo = readObject(app, "callback_info");
264
+ const callbacks = readStringArray(callbackInfo, "subscribed_callbacks");
265
+ const publishedVersion = readArray(readObject(versions, "data"), "items")
266
+ .filter(isJsonObject)
267
+ .find((item) => readNumber(item, "status") === 1 && hasPublishTime(item.publish_time));
268
+ const scopes = publishedVersion
269
+ ? readArray(publishedVersion, "scopes")
270
+ .filter(isJsonObject)
271
+ .filter((item) => readStringArray(item, "token_types").includes("tenant"))
272
+ .map((item) => readString(item, "scope"))
273
+ .filter(Boolean)
274
+ : [];
275
+ const events = publishedVersion
276
+ ? readArray(publishedVersion, "event_infos")
277
+ .filter(isJsonObject)
278
+ .map((item) => readString(item, "event_type"))
279
+ .filter(Boolean)
280
+ : [];
281
+
282
+ return {
283
+ scopes: uniqueSorted(scopes),
284
+ events: uniqueSorted(events),
285
+ callbacks: uniqueSorted(callbacks),
286
+ };
287
+ }
288
+
289
+ async function getTenantAccessToken(
290
+ credentials: FeishuAppCredentials,
291
+ fetchImpl: typeof globalThis.fetch,
292
+ signal?: AbortSignal,
293
+ ): Promise<string> {
294
+ const payload = await fetchJson(
295
+ fetchImpl,
296
+ `${FEISHU_OPEN_BASE_URL}${TENANT_TOKEN_PATH}`,
297
+ {
298
+ method: "POST",
299
+ headers: { "Content-Type": "application/json; charset=utf-8" },
300
+ body: JSON.stringify({
301
+ app_id: credentials.appId,
302
+ app_secret: credentials.appSecret,
303
+ }),
304
+ },
305
+ signal,
306
+ "获取飞书 tenant_access_token",
307
+ );
308
+ const token = readString(payload, "tenant_access_token");
309
+ if (!token) throw new Error("飞书 tenant_access_token 响应缺少访问凭证。");
310
+ return token;
311
+ }
312
+
313
+ async function fetchJson(
314
+ fetchImpl: typeof globalThis.fetch,
315
+ url: string,
316
+ init: RequestInit,
317
+ parentSignal: AbortSignal | undefined,
318
+ operation: string,
319
+ ): Promise<JsonObject> {
320
+ if (parentSignal?.aborted) throw abortError(parentSignal);
321
+ const controller = new AbortController();
322
+ const timeout = setTimeout(() => controller.abort(new Error("request timeout")), REQUEST_TIMEOUT_MS);
323
+ const onAbort = (): void => controller.abort(parentSignal?.reason);
324
+ parentSignal?.addEventListener("abort", onAbort, { once: true });
325
+
326
+ try {
327
+ const response = await fetchImpl(url, { ...init, signal: controller.signal });
328
+ const payload = (await response.json().catch(() => undefined)) as unknown;
329
+ if (!isJsonObject(payload)) {
330
+ throw new Error(`${operation}返回了无法解析的响应(HTTP ${response.status})。`);
331
+ }
332
+ const code = readNumber(payload, "code");
333
+ if (!response.ok || code !== 0) {
334
+ throw new FeishuConfigurationApiError(
335
+ `${operation}失败:${readString(payload, "msg") || response.statusText || `HTTP ${response.status}`}`,
336
+ code,
337
+ response.status,
338
+ );
339
+ }
340
+ return payload;
341
+ } catch (error) {
342
+ if (parentSignal?.aborted) throw abortError(parentSignal);
343
+ if (controller.signal.aborted) throw new Error(`${operation}请求超时。`);
344
+ throw error;
345
+ } finally {
346
+ clearTimeout(timeout);
347
+ parentSignal?.removeEventListener("abort", onAbort);
348
+ }
349
+ }
350
+
351
+ class FeishuConfigurationApiError extends Error {
352
+ constructor(
353
+ message: string,
354
+ readonly code: number,
355
+ readonly status: number,
356
+ ) {
357
+ super(message);
358
+ }
359
+ }
360
+
361
+ function isPermissionError(error: unknown): boolean {
362
+ return (
363
+ error instanceof FeishuConfigurationApiError &&
364
+ (error.status === 403 ||
365
+ error.code === 99991672 ||
366
+ error.code === 99991676 ||
367
+ /permission|scope|权限/i.test(error.message))
368
+ );
369
+ }
370
+
371
+ function hasMissingConfiguration(missing: MissingFeishuAppConfiguration): boolean {
372
+ return missing.scopes.length > 0 || missing.events.length > 0 || missing.callbacks.length > 0;
373
+ }
374
+
375
+ function coreMissingFeishuAppConfiguration(missing: MissingFeishuAppConfiguration): MissingFeishuAppConfiguration {
376
+ return {
377
+ scopes: missing.scopes.filter((scope) => CORE_FEISHU_SCOPES.has(scope)),
378
+ events: missing.events.filter((event) => CORE_FEISHU_EVENTS.has(event)),
379
+ callbacks: [],
380
+ };
381
+ }
382
+
383
+ function resolvedMissingConfiguration(
384
+ initial: MissingFeishuAppConfiguration,
385
+ remaining: MissingFeishuAppConfiguration,
386
+ ): MissingFeishuAppConfiguration {
387
+ const remainingScopes = new Set(remaining.scopes);
388
+ const remainingEvents = new Set(remaining.events);
389
+ const remainingCallbacks = new Set(remaining.callbacks);
390
+ return {
391
+ scopes: initial.scopes.filter((scope) => !remainingScopes.has(scope)),
392
+ events: initial.events.filter((event) => !remainingEvents.has(event)),
393
+ callbacks: initial.callbacks.filter((callback) => !remainingCallbacks.has(callback)),
394
+ };
395
+ }
396
+
397
+ function emptyConfiguration(): FeishuAppConfiguration {
398
+ return { scopes: [], events: [], callbacks: [] };
399
+ }
400
+
401
+ function uniqueSorted(values: string[]): string[] {
402
+ return [...new Set(values)].sort();
403
+ }
404
+
405
+ function hasPublishTime(value: unknown): boolean {
406
+ return value !== undefined && value !== null && value !== "";
407
+ }
408
+
409
+ function readObject(value: JsonObject, key: string): JsonObject {
410
+ const field = value[key];
411
+ return isJsonObject(field) ? field : {};
412
+ }
413
+
414
+ function readArray(value: JsonObject, key: string): unknown[] {
415
+ const field = value[key];
416
+ return Array.isArray(field) ? field : [];
417
+ }
418
+
419
+ function readStringArray(value: JsonObject, key: string): string[] {
420
+ return readArray(value, key)
421
+ .filter((item): item is string => typeof item === "string")
422
+ .map((item) => item.trim())
423
+ .filter(Boolean);
424
+ }
425
+
426
+ function readString(value: JsonObject, key: string): string {
427
+ const field = value[key];
428
+ return typeof field === "string" ? field.trim() : "";
429
+ }
430
+
431
+ function readNumber(value: JsonObject, key: string): number {
432
+ const field = value[key];
433
+ return typeof field === "number" ? field : Number(field);
434
+ }
435
+
436
+ function isJsonObject(value: unknown): value is JsonObject {
437
+ return typeof value === "object" && value !== null && !Array.isArray(value);
438
+ }
439
+
440
+ function positiveInteger(value: number | undefined): number | undefined {
441
+ return Number.isInteger(value) && value !== undefined && value > 0 ? value : undefined;
442
+ }
443
+
444
+ function abortError(signal: AbortSignal): Error {
445
+ return signal.reason instanceof Error ? signal.reason : new Error("飞书应用配置已取消。");
446
+ }
447
+
448
+ function delay(milliseconds: number): Promise<void> {
449
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
450
+ }
451
+
452
+ async function sleepWithAbort(
453
+ sleep: (milliseconds: number) => Promise<void>,
454
+ milliseconds: number,
455
+ signal?: AbortSignal,
456
+ ): Promise<void> {
457
+ if (!signal) {
458
+ await sleep(milliseconds);
459
+ return;
460
+ }
461
+ if (signal.aborted) throw abortError(signal);
462
+
463
+ let onAbort: (() => void) | undefined;
464
+ const aborted = new Promise<never>((_, reject) => {
465
+ onAbort = () => reject(abortError(signal));
466
+ signal.addEventListener("abort", onAbort, { once: true });
467
+ });
468
+ try {
469
+ await Promise.race([sleep(milliseconds), aborted]);
470
+ } finally {
471
+ if (onAbort) signal.removeEventListener("abort", onAbort);
472
+ }
473
+ }
@@ -0,0 +1,207 @@
1
+ const FEISHU_ACCOUNTS_BASE_URL = "https://accounts.feishu.cn";
2
+ const FEISHU_OPEN_BASE_URL = "https://open.feishu.cn";
3
+ const APP_REGISTRATION_PATH = "/oauth/v1/app/registration";
4
+ const DEFAULT_EXPIRES_IN_SECONDS = 600;
5
+ const DEFAULT_POLL_INTERVAL_SECONDS = 5;
6
+ const MAX_POLL_INTERVAL_SECONDS = 60;
7
+ const REQUEST_TIMEOUT_MS = 30_000;
8
+
9
+ export interface FeishuAppRegistrationChallenge {
10
+ verificationUrl: string;
11
+ userCode: string;
12
+ expiresIn: number;
13
+ interval: number;
14
+ }
15
+
16
+ export interface FeishuAppCredentials {
17
+ appId: string;
18
+ appSecret: string;
19
+ }
20
+
21
+ export interface FeishuAppRegistrationOptions {
22
+ fetch?: typeof globalThis.fetch;
23
+ sleep?: (milliseconds: number) => Promise<void>;
24
+ signal?: AbortSignal;
25
+ onVerification?: (challenge: FeishuAppRegistrationChallenge) => void | Promise<void>;
26
+ }
27
+
28
+ type RegistrationPayload = Record<string, unknown>;
29
+
30
+ export async function registerFeishuApp(options: FeishuAppRegistrationOptions = {}): Promise<FeishuAppCredentials> {
31
+ const fetchImpl = options.fetch ?? globalThis.fetch;
32
+ const sleep = options.sleep ?? delay;
33
+ const begin = await postRegistration(
34
+ fetchImpl,
35
+ {
36
+ action: "begin",
37
+ archetype: "PersonalAgent",
38
+ auth_method: "client_secret",
39
+ request_user_info: "open_id tenant_brand",
40
+ },
41
+ options.signal,
42
+ );
43
+ throwRegistrationError(begin, "无法发起飞书应用创建");
44
+
45
+ const deviceCode = readString(begin, "device_code");
46
+ const userCode = readString(begin, "user_code");
47
+ if (!deviceCode || !userCode) {
48
+ throw new Error("飞书应用创建响应缺少 device_code 或 user_code。");
49
+ }
50
+
51
+ const expiresIn =
52
+ readPositiveInteger(begin, "expire_in") ?? readPositiveInteger(begin, "expires_in") ?? DEFAULT_EXPIRES_IN_SECONDS;
53
+ const initialInterval = readPositiveInteger(begin, "interval") ?? DEFAULT_POLL_INTERVAL_SECONDS;
54
+ const verificationUrl = readString(begin, "verification_uri_complete") || buildVerificationUrl(userCode);
55
+
56
+ await options.onVerification?.({
57
+ verificationUrl,
58
+ userCode,
59
+ expiresIn,
60
+ interval: initialInterval,
61
+ });
62
+
63
+ let remainingMs = expiresIn * 1_000;
64
+ let intervalSeconds = initialInterval;
65
+ let waitBeforePoll = false;
66
+
67
+ while (remainingMs > 0) {
68
+ if (options.signal?.aborted) throw abortError(options.signal);
69
+ if (waitBeforePoll) {
70
+ const waitMs = Math.min(intervalSeconds * 1_000, remainingMs);
71
+ await sleepWithAbort(sleep, waitMs, options.signal);
72
+ remainingMs -= waitMs;
73
+ if (remainingMs <= 0) break;
74
+ }
75
+ waitBeforePoll = true;
76
+
77
+ let payload: RegistrationPayload;
78
+ try {
79
+ payload = await postRegistration(
80
+ fetchImpl,
81
+ {
82
+ action: "poll",
83
+ device_code: deviceCode,
84
+ },
85
+ options.signal,
86
+ );
87
+ } catch (error) {
88
+ if (options.signal?.aborted) throw abortError(options.signal);
89
+ intervalSeconds = Math.min(intervalSeconds + 1, MAX_POLL_INTERVAL_SECONDS);
90
+ continue;
91
+ }
92
+
93
+ const errorCode = readString(payload, "error");
94
+ if (!errorCode) {
95
+ const appId = readString(payload, "client_id");
96
+ const appSecret = readString(payload, "client_secret");
97
+ if (appId && appSecret) return { appId, appSecret };
98
+ continue;
99
+ }
100
+
101
+ switch (errorCode) {
102
+ case "authorization_pending":
103
+ continue;
104
+ case "slow_down":
105
+ intervalSeconds = Math.min(intervalSeconds + 5, MAX_POLL_INTERVAL_SECONDS);
106
+ continue;
107
+ case "access_denied":
108
+ throw new Error("你已取消飞书应用创建。");
109
+ case "expired_token":
110
+ case "invalid_grant":
111
+ throw new Error("飞书应用创建链接已过期,请重新运行 agent-bot init。");
112
+ default:
113
+ throwRegistrationError(payload, "飞书应用创建失败");
114
+ }
115
+ }
116
+
117
+ throw new Error("等待飞书应用创建超时,请重新运行 agent-bot init。");
118
+ }
119
+
120
+ function buildVerificationUrl(userCode: string): string {
121
+ const url = new URL("/page/cli", FEISHU_OPEN_BASE_URL);
122
+ url.searchParams.set("user_code", userCode);
123
+ url.searchParams.set("from", "cli");
124
+ return url.toString();
125
+ }
126
+
127
+ async function postRegistration(
128
+ fetchImpl: typeof globalThis.fetch,
129
+ values: Record<string, string>,
130
+ parentSignal?: AbortSignal,
131
+ ): Promise<RegistrationPayload> {
132
+ if (parentSignal?.aborted) throw abortError(parentSignal);
133
+ const controller = new AbortController();
134
+ const timeout = setTimeout(() => controller.abort(new Error("request timeout")), REQUEST_TIMEOUT_MS);
135
+ const onAbort = (): void => controller.abort(parentSignal?.reason);
136
+ parentSignal?.addEventListener("abort", onAbort, { once: true });
137
+
138
+ try {
139
+ const response = await fetchImpl(`${FEISHU_ACCOUNTS_BASE_URL}${APP_REGISTRATION_PATH}`, {
140
+ method: "POST",
141
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
142
+ body: new URLSearchParams(values),
143
+ signal: controller.signal,
144
+ });
145
+ const payload = (await response.json().catch(() => undefined)) as unknown;
146
+ if (!isRecord(payload)) {
147
+ throw new Error(`飞书应用注册接口返回了无法解析的响应(HTTP ${response.status})。`);
148
+ }
149
+ return payload;
150
+ } finally {
151
+ clearTimeout(timeout);
152
+ parentSignal?.removeEventListener("abort", onAbort);
153
+ }
154
+ }
155
+
156
+ function throwRegistrationError(payload: RegistrationPayload, prefix: string): void {
157
+ const code = readString(payload, "error");
158
+ if (!code) return;
159
+ const description = readString(payload, "error_description") || code;
160
+ throw new Error(`${prefix}:${description}`);
161
+ }
162
+
163
+ function readString(value: RegistrationPayload, key: string): string {
164
+ const field = value[key];
165
+ return typeof field === "string" ? field.trim() : "";
166
+ }
167
+
168
+ function readPositiveInteger(value: RegistrationPayload, key: string): number | undefined {
169
+ const field = value[key];
170
+ const parsed = typeof field === "number" ? field : Number(field);
171
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
172
+ }
173
+
174
+ function isRecord(value: unknown): value is RegistrationPayload {
175
+ return typeof value === "object" && value !== null && !Array.isArray(value);
176
+ }
177
+
178
+ function abortError(signal: AbortSignal): Error {
179
+ return signal.reason instanceof Error ? signal.reason : new Error("飞书应用创建已取消。");
180
+ }
181
+
182
+ function delay(milliseconds: number): Promise<void> {
183
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
184
+ }
185
+
186
+ async function sleepWithAbort(
187
+ sleep: (milliseconds: number) => Promise<void>,
188
+ milliseconds: number,
189
+ signal?: AbortSignal,
190
+ ): Promise<void> {
191
+ if (!signal) {
192
+ await sleep(milliseconds);
193
+ return;
194
+ }
195
+ if (signal.aborted) throw abortError(signal);
196
+
197
+ let onAbort: (() => void) | undefined;
198
+ const aborted = new Promise<never>((_, reject) => {
199
+ onAbort = () => reject(abortError(signal));
200
+ signal.addEventListener("abort", onAbort, { once: true });
201
+ });
202
+ try {
203
+ await Promise.race([sleep(milliseconds), aborted]);
204
+ } finally {
205
+ if (onAbort) signal.removeEventListener("abort", onAbort);
206
+ }
207
+ }