@keyou007/agent-bot 0.1.6 → 0.1.8

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 (51) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +5 -1
  3. package/README.zh.md +5 -1
  4. package/config.example.yaml +2 -0
  5. package/dist/cli/FeishuAppConfiguration.js +71 -21
  6. package/dist/cli/FeishuAppConfiguration.js.map +1 -1
  7. package/dist/cli/OptionalAuthorizationSkip.js +30 -0
  8. package/dist/cli/OptionalAuthorizationSkip.js.map +1 -0
  9. package/dist/cli/VerificationOutput.js +9 -0
  10. package/dist/cli/VerificationOutput.js.map +1 -0
  11. package/dist/cli.js +23 -18
  12. package/dist/cli.js.map +1 -1
  13. package/dist/codex/CodexEventMapper.js +13 -0
  14. package/dist/codex/CodexEventMapper.js.map +1 -1
  15. package/dist/codex/CodexRuntime.js +33 -4
  16. package/dist/codex/CodexRuntime.js.map +1 -1
  17. package/dist/config/loadConfig.js +1 -0
  18. package/dist/config/loadConfig.js.map +1 -1
  19. package/dist/config/schema.js +1 -0
  20. package/dist/config/schema.js.map +1 -1
  21. package/dist/feishu/CardRenderer.js +2 -2
  22. package/dist/feishu/CardRenderer.js.map +1 -1
  23. package/dist/feishu/FeishuBotIdentity.js +35 -0
  24. package/dist/feishu/FeishuBotIdentity.js.map +1 -0
  25. package/dist/feishu/FeishuConnector.js +20 -5
  26. package/dist/feishu/FeishuConnector.js.map +1 -1
  27. package/dist/presentation/TurnStateReducer.js +2 -1
  28. package/dist/presentation/TurnStateReducer.js.map +1 -1
  29. package/dist/proxy/ProxySessionController.js +5 -1
  30. package/dist/proxy/ProxySessionController.js.map +1 -1
  31. package/dist/utils/generatedImageMarkdown.js +11 -0
  32. package/dist/utils/generatedImageMarkdown.js.map +1 -0
  33. package/docs/technical-reference.md +8 -2
  34. package/docs/technical-reference.zh.md +8 -2
  35. package/npm-shrinkwrap.json +2 -2
  36. package/package.json +1 -1
  37. package/skills/agent-bot/SKILL.md +7 -3
  38. package/src/cli/FeishuAppConfiguration.ts +131 -19
  39. package/src/cli/OptionalAuthorizationSkip.ts +38 -0
  40. package/src/cli/VerificationOutput.ts +22 -0
  41. package/src/cli.ts +30 -18
  42. package/src/codex/CodexEventMapper.ts +13 -0
  43. package/src/codex/CodexRuntime.ts +48 -5
  44. package/src/config/loadConfig.ts +1 -0
  45. package/src/config/schema.ts +1 -0
  46. package/src/feishu/CardRenderer.ts +2 -2
  47. package/src/feishu/FeishuBotIdentity.ts +55 -0
  48. package/src/feishu/FeishuConnector.ts +24 -4
  49. package/src/presentation/TurnStateReducer.ts +6 -1
  50. package/src/proxy/ProxySessionController.ts +4 -1
  51. package/src/utils/generatedImageMarkdown.ts +10 -0
@@ -15,6 +15,7 @@ const DEFAULT_CONFIG = `feishu:
15
15
  appId: "\${FEISHU_APP_ID}"
16
16
  appSecret: "\${FEISHU_APP_SECRET}"
17
17
  userOpenId: "\${FEISHU_USER_OPEN_ID}"
18
+ respondToAllGroupMessages: true
18
19
 
19
20
  console:
20
21
  enabled: true
@@ -22,6 +22,7 @@ export const appConfigSchema = z.object({
22
22
  appId: z.string().optional(),
23
23
  appSecret: z.string().optional(),
24
24
  userOpenId: z.string().optional(),
25
+ respondToAllGroupMessages: z.boolean().default(true),
25
26
  useConsoleWhenMissingCredentials: z.boolean().default(false),
26
27
  }),
27
28
  agents: z.record(z.string().min(1), agentConfigSchema).refine(
@@ -790,8 +790,8 @@ function turnActivities(state: TurnViewState): TurnActivity[] {
790
790
 
791
791
  function toolPanel(tool: ToolState, projectCwd?: string): Record<string, unknown> {
792
792
  const elements = [markdown(renderToolDetails(tool, projectCwd))];
793
- if (tool.kind === "image_view" && tool.imagePath) {
794
- elements.push(localCardImage(tool.imagePath, "view_image 图片"));
793
+ if ((tool.kind === "image_view" || tool.kind === "image_generation") && tool.imagePath) {
794
+ elements.push(localCardImage(tool.imagePath, tool.kind === "image_generation" ? "生成图片" : "view_image 图片"));
795
795
  }
796
796
  return collapsiblePanel(toolPanelTitle(tool), elements, { elementId: toolPanelElementId(tool.id) });
797
797
  }
@@ -0,0 +1,55 @@
1
+ const TENANT_TOKEN_URL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal";
2
+ const BOT_INFO_URL = "https://open.feishu.cn/open-apis/bot/v3/info";
3
+
4
+ interface TenantTokenResponse {
5
+ code?: number;
6
+ msg?: string;
7
+ tenant_access_token?: string;
8
+ }
9
+
10
+ interface BotInfoResponse {
11
+ code?: number;
12
+ msg?: string;
13
+ bot?: {
14
+ open_id?: string;
15
+ };
16
+ }
17
+
18
+ export async function resolveFeishuBotOpenId(
19
+ appId: string,
20
+ appSecret: string,
21
+ fetchImpl: typeof globalThis.fetch = globalThis.fetch,
22
+ ): Promise<string> {
23
+ const tokenResponse = await fetchImpl(TENANT_TOKEN_URL, {
24
+ method: "POST",
25
+ headers: { "Content-Type": "application/json; charset=utf-8" },
26
+ body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
27
+ });
28
+ const tokenPayload = await readJson<TenantTokenResponse>(tokenResponse, "get the Lark tenant access token");
29
+ const token = tokenPayload.tenant_access_token;
30
+ if (!tokenResponse.ok || tokenPayload.code !== 0 || !token) {
31
+ throw new Error(`Failed to get the Lark tenant access token: ${responseMessage(tokenPayload, tokenResponse)}`);
32
+ }
33
+
34
+ const infoResponse = await fetchImpl(BOT_INFO_URL, {
35
+ headers: { Authorization: `Bearer ${token}` },
36
+ });
37
+ const infoPayload = await readJson<BotInfoResponse>(infoResponse, "get Lark bot information");
38
+ const openId = infoPayload.bot?.open_id;
39
+ if (!infoResponse.ok || infoPayload.code !== 0 || !openId) {
40
+ throw new Error(`Failed to get Lark bot information: ${responseMessage(infoPayload, infoResponse)}`);
41
+ }
42
+ return openId;
43
+ }
44
+
45
+ async function readJson<T>(response: Response, operation: string): Promise<T> {
46
+ try {
47
+ return await response.json() as T;
48
+ } catch {
49
+ throw new Error(`Failed to ${operation}: Lark returned an invalid response (HTTP ${response.status}).`);
50
+ }
51
+ }
52
+
53
+ function responseMessage(payload: { msg?: string }, response: Response): string {
54
+ return payload.msg || response.statusText || `HTTP ${response.status}`;
55
+ }
@@ -1,13 +1,17 @@
1
1
  import type { Logger } from "pino";
2
2
  import type { AppConfig } from "../config/schema.js";
3
3
  import { threadContextKey } from "./contextKey.js";
4
+ import { resolveFeishuBotOpenId } from "./FeishuBotIdentity.js";
4
5
  import type { ChatUpdatedEvent, FeishuEventHandler, IncomingMessage } from "./types.js";
5
6
 
7
+ type BotOpenIdResolver = (appId: string, appSecret: string) => Promise<string>;
8
+
6
9
  export class FeishuConnector {
7
10
  constructor(
8
11
  private readonly config: AppConfig,
9
12
  private readonly handler: FeishuEventHandler,
10
13
  private readonly logger: Logger,
14
+ private readonly botOpenIdResolver: BotOpenIdResolver = resolveFeishuBotOpenId,
11
15
  ) {}
12
16
 
13
17
  async start(): Promise<void> {
@@ -16,18 +20,22 @@ export class FeishuConnector {
16
20
  throw new Error("Feishu appId/appSecret are required.");
17
21
  }
18
22
 
19
- await this.startFeishuWs(appId, appSecret);
23
+ const respondToAllGroupMessages = this.config.feishu.respondToAllGroupMessages !== false;
24
+ const botOpenId = respondToAllGroupMessages
25
+ ? undefined
26
+ : await this.botOpenIdResolver(appId, appSecret);
27
+ await this.startFeishuWs(appId, appSecret, botOpenId);
20
28
  }
21
29
 
22
30
  stop(): void {
23
31
  // SDK connector currently relies on process lifetime.
24
32
  }
25
33
 
26
- private async startFeishuWs(appId: string, appSecret: string): Promise<void> {
34
+ private async startFeishuWs(appId: string, appSecret: string, requiredMentionOpenId?: string): Promise<void> {
27
35
  const lark = (await import("@larksuiteoapi/node-sdk")) as Record<string, any>;
28
36
  const eventDispatcher = new lark.EventDispatcher({}).register({
29
37
  "im.message.receive_v1": async (data: unknown) => {
30
- const message = toIncomingMessage(data);
38
+ const message = toIncomingMessage(data, requiredMentionOpenId);
31
39
  if (!message) {
32
40
  this.logger.debug({ data }, "Ignored unsupported Feishu message event.");
33
41
  return;
@@ -98,7 +106,7 @@ function toChatUpdatedEvent(data: unknown): ChatUpdatedEvent | undefined {
98
106
  };
99
107
  }
100
108
 
101
- function toIncomingMessage(data: unknown): IncomingMessage | undefined {
109
+ function toIncomingMessage(data: unknown, requiredMentionOpenId?: string): IncomingMessage | undefined {
102
110
  const event = getFeishuEvent(data);
103
111
  const message = event?.message;
104
112
  if (!message) {
@@ -111,6 +119,13 @@ function toIncomingMessage(data: unknown): IncomingMessage | undefined {
111
119
  if (!parsed) return undefined;
112
120
  const chatId = message.chat_id;
113
121
  const chatType = message.chat_type === "group" ? "group" : "p2p";
122
+ if (
123
+ chatType === "group" &&
124
+ requiredMentionOpenId &&
125
+ !mentionsOpenId(message.mentions, requiredMentionOpenId)
126
+ ) {
127
+ return undefined;
128
+ }
114
129
  const threadId = typeof message.thread_id === "string" && message.thread_id ? message.thread_id : undefined;
115
130
  const rootMessageId = typeof message.root_id === "string" && message.root_id ? message.root_id : undefined;
116
131
  const parentMessageId = typeof message.parent_id === "string" && message.parent_id ? message.parent_id : undefined;
@@ -142,6 +157,11 @@ function toIncomingMessage(data: unknown): IncomingMessage | undefined {
142
157
  };
143
158
  }
144
159
 
160
+ function mentionsOpenId(value: unknown, openId: string): boolean {
161
+ return Array.isArray(value) && value.some((mention) =>
162
+ isRecord(mention) && mention.id === openId);
163
+ }
164
+
145
165
  function parseMessageContent(
146
166
  messageType: unknown,
147
167
  content: Record<string, unknown>,
@@ -1,4 +1,5 @@
1
1
  import type { AgentEvent, ToolState } from "../runtime/types.js";
2
+ import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
2
3
  import type { MessageReplyTarget } from "../feishu/types.js";
3
4
  import type { FileSummary, TurnActivity, TurnViewState } from "./turnViewTypes.js";
4
5
 
@@ -107,7 +108,11 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
107
108
  approval: undefined,
108
109
  completedAt: state.startedAt + (event.durationMs ?? Date.now() - state.startedAt),
109
110
  durationMs: event.durationMs ?? Date.now() - state.startedAt,
110
- finalResponse: event.finalResponse,
111
+ finalResponse: appendGeneratedImageMarkdown(
112
+ event.finalResponse,
113
+ state.completedTools.flatMap((tool) =>
114
+ tool.kind === "image_generation" && tool.imagePath ? [tool.imagePath] : []),
115
+ ),
111
116
  };
112
117
  case "turn_cancelled":
113
118
  return { ...state, status: "cancelled", activeTool: undefined, approval: undefined, completedAt: Date.now() };
@@ -3264,7 +3264,10 @@ function truncateTail(value: string, maxLength: number): string {
3264
3264
  function parseAgentGroupName(value: string): { agentName: string; title: string } | undefined {
3265
3265
  const match = /^\[([^[\]]+)\]\s+(.+)$/.exec(value.trim());
3266
3266
  const agentName = match?.[1]?.trim();
3267
- const title = match?.[2]?.trim();
3267
+ const remainder = match?.[2]?.trim();
3268
+ if (!agentName || !remainder) return undefined;
3269
+ const projectPrefix = /^\[([^[\]]+)\](?:\s+(.+))?$/.exec(remainder);
3270
+ const title = projectPrefix ? projectPrefix[2]?.trim() : remainder;
3268
3271
  return agentName && title ? { agentName, title } : undefined;
3269
3272
  }
3270
3273
 
@@ -0,0 +1,10 @@
1
+ export function appendGeneratedImageMarkdown(finalResponse: string, imagePaths: string[]): string {
2
+ const text = finalResponse.trim();
3
+ const normalizedText = text.replaceAll("\\", "/");
4
+ const images = [...new Set(imagePaths.map((imagePath) => imagePath.trim().replaceAll("\\", "/")).filter(Boolean))]
5
+ .filter((imagePath) => !normalizedText.includes(imagePath));
6
+ if (images.length === 0) return text;
7
+ const imageMarkdown = images.map((imagePath, index) =>
8
+ `![生成图片 ${index + 1}](<${imagePath}>)`).join("\n\n");
9
+ return text ? `${text}\n\n${imageMarkdown}` : imageMarkdown;
10
+ }