@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.
- package/CHANGELOG.md +10 -0
- package/README.md +5 -1
- package/README.zh.md +5 -1
- package/config.example.yaml +2 -0
- package/dist/cli/FeishuAppConfiguration.js +71 -21
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/OptionalAuthorizationSkip.js +30 -0
- package/dist/cli/OptionalAuthorizationSkip.js.map +1 -0
- package/dist/cli/VerificationOutput.js +9 -0
- package/dist/cli/VerificationOutput.js.map +1 -0
- package/dist/cli.js +23 -18
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +13 -0
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +33 -4
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/config/loadConfig.js +1 -0
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +1 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/feishu/CardRenderer.js +2 -2
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuBotIdentity.js +35 -0
- package/dist/feishu/FeishuBotIdentity.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +20 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +2 -1
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +5 -1
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/utils/generatedImageMarkdown.js +11 -0
- package/dist/utils/generatedImageMarkdown.js.map +1 -0
- package/docs/technical-reference.md +8 -2
- package/docs/technical-reference.zh.md +8 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +7 -3
- package/src/cli/FeishuAppConfiguration.ts +131 -19
- package/src/cli/OptionalAuthorizationSkip.ts +38 -0
- package/src/cli/VerificationOutput.ts +22 -0
- package/src/cli.ts +30 -18
- package/src/codex/CodexEventMapper.ts +13 -0
- package/src/codex/CodexRuntime.ts +48 -5
- package/src/config/loadConfig.ts +1 -0
- package/src/config/schema.ts +1 -0
- package/src/feishu/CardRenderer.ts +2 -2
- package/src/feishu/FeishuBotIdentity.ts +55 -0
- package/src/feishu/FeishuConnector.ts +24 -4
- package/src/presentation/TurnStateReducer.ts +6 -1
- package/src/proxy/ProxySessionController.ts +4 -1
- package/src/utils/generatedImageMarkdown.ts +10 -0
package/src/config/loadConfig.ts
CHANGED
package/src/config/schema.ts
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
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
|
+
``).join("\n\n");
|
|
9
|
+
return text ? `${text}\n\n${imageMarkdown}` : imageMarkdown;
|
|
10
|
+
}
|