@next-open-ai/openbot 0.2.8 → 0.6.6

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 (94) hide show
  1. package/README.md +13 -3
  2. package/apps/desktop/renderer/dist/assets/index-BNuvb6Ay.css +10 -0
  3. package/apps/desktop/renderer/dist/assets/index-DvQjslfT.js +89 -0
  4. package/apps/desktop/renderer/dist/index.html +2 -2
  5. package/dist/core/agent/agent-manager.d.ts +17 -6
  6. package/dist/core/agent/agent-manager.js +62 -25
  7. package/dist/core/agent/run.js +2 -2
  8. package/dist/core/config/desktop-config.d.ts +17 -0
  9. package/dist/core/config/desktop-config.js +23 -1
  10. package/dist/core/installer/index.d.ts +1 -1
  11. package/dist/core/installer/index.js +1 -1
  12. package/dist/core/installer/skill-installer.d.ts +9 -0
  13. package/dist/core/installer/skill-installer.js +94 -0
  14. package/dist/core/mcp/adapter.d.ts +17 -0
  15. package/dist/core/mcp/adapter.js +49 -0
  16. package/dist/core/mcp/client.d.ts +24 -0
  17. package/dist/core/mcp/client.js +70 -0
  18. package/dist/core/mcp/config.d.ts +22 -0
  19. package/dist/core/mcp/config.js +69 -0
  20. package/dist/core/mcp/index.d.ts +18 -0
  21. package/dist/core/mcp/index.js +20 -0
  22. package/dist/core/mcp/operator.d.ts +15 -0
  23. package/dist/core/mcp/operator.js +72 -0
  24. package/dist/core/mcp/transport/index.d.ts +11 -0
  25. package/dist/core/mcp/transport/index.js +16 -0
  26. package/dist/core/mcp/transport/sse.d.ts +20 -0
  27. package/dist/core/mcp/transport/sse.js +82 -0
  28. package/dist/core/mcp/transport/stdio.d.ts +32 -0
  29. package/dist/core/mcp/transport/stdio.js +132 -0
  30. package/dist/core/mcp/types.d.ts +72 -0
  31. package/dist/core/mcp/types.js +5 -0
  32. package/dist/core/session-current-agent.d.ts +34 -0
  33. package/dist/core/session-current-agent.js +32 -0
  34. package/dist/core/tools/bookmark-tool.d.ts +9 -0
  35. package/dist/core/tools/bookmark-tool.js +118 -0
  36. package/dist/core/tools/create-agent-tool.d.ts +6 -0
  37. package/dist/core/tools/create-agent-tool.js +97 -0
  38. package/dist/core/tools/index.d.ts +4 -0
  39. package/dist/core/tools/index.js +4 -0
  40. package/dist/core/tools/list-agents-tool.d.ts +5 -0
  41. package/dist/core/tools/list-agents-tool.js +45 -0
  42. package/dist/core/tools/switch-agent-tool.d.ts +6 -0
  43. package/dist/core/tools/switch-agent-tool.js +54 -0
  44. package/dist/gateway/channel/adapters/feishu.d.ts +11 -0
  45. package/dist/gateway/channel/adapters/feishu.js +218 -0
  46. package/dist/gateway/channel/channel-core.d.ts +9 -0
  47. package/dist/gateway/channel/channel-core.js +127 -0
  48. package/dist/gateway/channel/registry.d.ts +16 -0
  49. package/dist/gateway/channel/registry.js +54 -0
  50. package/dist/gateway/channel/run-agent.d.ts +26 -0
  51. package/dist/gateway/channel/run-agent.js +137 -0
  52. package/dist/gateway/channel/session-persistence.d.ts +36 -0
  53. package/dist/gateway/channel/session-persistence.js +46 -0
  54. package/dist/gateway/channel/types.d.ts +70 -0
  55. package/dist/gateway/channel/types.js +4 -0
  56. package/dist/gateway/channel-handler.d.ts +3 -4
  57. package/dist/gateway/channel-handler.js +8 -2
  58. package/dist/gateway/methods/agent-chat.js +31 -12
  59. package/dist/gateway/methods/install-skill-from-upload.d.ts +14 -0
  60. package/dist/gateway/methods/install-skill-from-upload.js +13 -0
  61. package/dist/gateway/methods/run-scheduled-task.js +5 -2
  62. package/dist/gateway/server.js +74 -1
  63. package/dist/server/agent-config/agent-config.controller.d.ts +6 -1
  64. package/dist/server/agent-config/agent-config.service.d.ts +15 -1
  65. package/dist/server/agent-config/agent-config.service.js +12 -3
  66. package/dist/server/agents/agents.controller.d.ts +10 -0
  67. package/dist/server/agents/agents.controller.js +36 -4
  68. package/dist/server/agents/agents.gateway.js +18 -4
  69. package/dist/server/agents/agents.service.d.ts +5 -1
  70. package/dist/server/agents/agents.service.js +20 -2
  71. package/dist/server/app.module.js +2 -0
  72. package/dist/server/config/config.controller.d.ts +2 -0
  73. package/dist/server/config/config.service.d.ts +3 -0
  74. package/dist/server/config/config.service.js +3 -1
  75. package/dist/server/database/database.service.d.ts +7 -0
  76. package/dist/server/database/database.service.js +54 -5
  77. package/dist/server/saved-items/saved-items.controller.d.ts +57 -0
  78. package/dist/server/saved-items/saved-items.controller.js +229 -0
  79. package/dist/server/saved-items/saved-items.module.d.ts +2 -0
  80. package/dist/server/saved-items/saved-items.module.js +25 -0
  81. package/dist/server/saved-items/saved-items.service.d.ts +31 -0
  82. package/dist/server/saved-items/saved-items.service.js +105 -0
  83. package/dist/server/saved-items/tags.controller.d.ts +30 -0
  84. package/dist/server/saved-items/tags.controller.js +85 -0
  85. package/dist/server/saved-items/tags.service.d.ts +24 -0
  86. package/dist/server/saved-items/tags.service.js +84 -0
  87. package/dist/server/skills/skills.service.d.ts +2 -0
  88. package/dist/server/skills/skills.service.js +80 -16
  89. package/dist/server/workspace/workspace.service.d.ts +11 -0
  90. package/dist/server/workspace/workspace.service.js +40 -1
  91. package/package.json +6 -1
  92. package/skills/url-bookmark/SKILL.md +36 -0
  93. package/apps/desktop/renderer/dist/assets/index-BOS-F8a4.js +0 -89
  94. package/apps/desktop/renderer/dist/assets/index-DxqxayUL.css +0 -10
@@ -0,0 +1,118 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getBackendBaseUrl } from "../../gateway/backend-url.js";
3
+ const GetBookmarkTagsSchema = Type.Object({});
4
+ const SaveBookmarkSchema = Type.Object({
5
+ url: Type.String({ description: "要收藏的 URL" }),
6
+ title: Type.Optional(Type.String({ description: "可选标题" })),
7
+ tagNames: Type.Optional(Type.Array(Type.String(), {
8
+ description: "标签名称列表,须与系统中已维护的标签一致。保存前应先用 get_bookmark_tags 获取可用标签。",
9
+ })),
10
+ });
11
+ async function apiGet(path) {
12
+ const base = getBackendBaseUrl();
13
+ if (!base) {
14
+ throw new Error("收藏功能需要在前端/桌面环境中使用,当前无法访问后端。");
15
+ }
16
+ const res = await fetch(`${base.replace(/\/$/, "")}/server-api${path}`, {
17
+ method: "GET",
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ if (!res.ok) {
21
+ const text = await res.text();
22
+ throw new Error(`请求失败: ${res.status} ${text}`);
23
+ }
24
+ return res.json();
25
+ }
26
+ async function apiPost(path, body) {
27
+ const base = getBackendBaseUrl();
28
+ if (!base) {
29
+ throw new Error("收藏功能需要在前端/桌面环境中使用,当前无法访问后端。");
30
+ }
31
+ const res = await fetch(`${base.replace(/\/$/, "")}/server-api${path}`, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify(body),
35
+ });
36
+ if (!res.ok) {
37
+ const text = await res.text();
38
+ throw new Error(`请求失败: ${res.status} ${text}`);
39
+ }
40
+ return res.json();
41
+ }
42
+ /**
43
+ * 获取当前系统中已维护的标签列表,供保存 URL 时选择匹配的标签。
44
+ */
45
+ export function createGetBookmarkTagsTool() {
46
+ return {
47
+ name: "get_bookmark_tags",
48
+ label: "Get Bookmark Tags",
49
+ description: "获取系统中已维护的收藏标签列表。在用户要求保存链接时,应先调用本工具获取可用标签,再根据用户意图匹配最合适的标签名,并用 save_bookmark 保存。",
50
+ parameters: GetBookmarkTagsSchema,
51
+ execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) => {
52
+ try {
53
+ const json = await apiGet("/tags");
54
+ const data = json.data ?? [];
55
+ const names = data.map((t) => t.name);
56
+ const text = names.length > 0
57
+ ? `当前可用标签:${names.join("、")}。请根据用户意图选择匹配的标签名用于 save_bookmark。`
58
+ : "当前暂无标签。请在设置中先添加标签,或使用 save_bookmark 时不传 tagNames。";
59
+ return {
60
+ content: [{ type: "text", text }],
61
+ details: { tags: data },
62
+ };
63
+ }
64
+ catch (err) {
65
+ const msg = err instanceof Error ? err.message : String(err);
66
+ return {
67
+ content: [{ type: "text", text: `获取标签失败: ${msg}` }],
68
+ details: undefined,
69
+ };
70
+ }
71
+ },
72
+ };
73
+ }
74
+ /**
75
+ * 将 URL 保存为收藏,并关联指定标签(标签名须与 get_bookmark_tags 返回的一致)。
76
+ */
77
+ export function createSaveBookmarkTool() {
78
+ return {
79
+ name: "save_bookmark",
80
+ label: "Save Bookmark",
81
+ description: "将用户提供的 URL 保存到收藏库,并可关联一个或多个标签。标签名必须使用 get_bookmark_tags 返回的已有标签;若用户未指定标签,可根据上下文推断或留空。",
82
+ parameters: SaveBookmarkSchema,
83
+ execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
84
+ const url = (params.url ?? "").trim();
85
+ if (!url) {
86
+ return {
87
+ content: [{ type: "text", text: "请提供要收藏的 URL。" }],
88
+ details: undefined,
89
+ };
90
+ }
91
+ try {
92
+ const json = await apiPost("/saved-items", {
93
+ url,
94
+ title: params.title?.trim() || undefined,
95
+ tagNames: params.tagNames?.length ? params.tagNames.map((n) => n.trim()).filter(Boolean) : undefined,
96
+ });
97
+ const data = json.data;
98
+ const tagStr = data?.tagNames?.length ? `,标签:${data.tagNames.join("、")}` : "";
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `已收藏:${data?.url ?? url}${tagStr}`,
104
+ },
105
+ ],
106
+ details: data,
107
+ };
108
+ }
109
+ catch (err) {
110
+ const msg = err instanceof Error ? err.message : String(err);
111
+ return {
112
+ content: [{ type: "text", text: `保存失败: ${msg}` }],
113
+ details: undefined,
114
+ };
115
+ }
116
+ },
117
+ };
118
+ }
@@ -0,0 +1,6 @@
1
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ /**
3
+ * 创建 create_agent 工具:根据用户意图创建新智能体配置,供用户在对话中选择使用。
4
+ * 缺省:未提供 workspace 时根据 name 生成英文标识;未提供 system_prompt 时根据 language 生成简短描述;模型使用全局缺省。
5
+ */
6
+ export declare function createCreateAgentTool(): ToolDefinition;
@@ -0,0 +1,97 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getCreateAgentProvider } from "../session-current-agent.js";
3
+ const CreateAgentSchema = Type.Object({
4
+ name: Type.String({
5
+ description: "智能体显示名称,如用户说的「美妆助手」「代码审查助手」",
6
+ }),
7
+ workspace: Type.Optional(Type.String({
8
+ description: "工作空间英文标识,仅允许字母、数字、下划线、连字符。不传则根据 name 自动生成(如 My Assistant → my-assistant)",
9
+ })),
10
+ system_prompt: Type.Optional(Type.String({
11
+ description: "系统提示词/角色描述。不传则根据 language 生成一句最简短的角色描述。",
12
+ })),
13
+ language: Type.Optional(Type.Union([Type.Literal("zh"), Type.Literal("en")], {
14
+ description: "用户语言,用于生成缺省 system_prompt。不传默认 en。",
15
+ })),
16
+ });
17
+ const WORKSPACE_NAME_REGEX = /^[a-zA-Z0-9_-]+$/;
18
+ const RESERVED_WORKSPACE = "default";
19
+ function slugifyFromName(name) {
20
+ const trimmed = (name || "").trim();
21
+ if (!trimmed)
22
+ return "agent-" + Date.now().toString(36);
23
+ const asciiOnly = trimmed
24
+ .replace(/\s+/g, "-")
25
+ .replace(/[^a-zA-Z0-9_-]/g, "")
26
+ .replace(/-+/g, "-")
27
+ .replace(/^-|-$/g, "")
28
+ .toLowerCase();
29
+ if (asciiOnly && WORKSPACE_NAME_REGEX.test(asciiOnly) && asciiOnly !== RESERVED_WORKSPACE)
30
+ return asciiOnly;
31
+ return "agent-" + Date.now().toString(36);
32
+ }
33
+ function getDefaultSystemPrompt(lang) {
34
+ if (lang === "zh")
35
+ return "你是根据用户需求定制的助手,请按用户描述的角色与能力提供帮助。";
36
+ return "You are a helpful assistant customized as requested by the user.";
37
+ }
38
+ /**
39
+ * 创建 create_agent 工具:根据用户意图创建新智能体配置,供用户在对话中选择使用。
40
+ * 缺省:未提供 workspace 时根据 name 生成英文标识;未提供 system_prompt 时根据 language 生成简短描述;模型使用全局缺省。
41
+ */
42
+ export function createCreateAgentTool() {
43
+ return {
44
+ name: "create_agent",
45
+ label: "Create Agent",
46
+ description: "根据用户意图创建一个新的智能体配置。用户可在对话中通过 list_agents 与 switch_agent 选择使用。需提供智能体名称;工作空间名与系统提示词可选,未提供时自动生成。",
47
+ parameters: CreateAgentSchema,
48
+ execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
49
+ const provider = getCreateAgentProvider();
50
+ if (!provider) {
51
+ return {
52
+ content: [{ type: "text", text: "当前环境不支持创建智能体。" }],
53
+ details: undefined,
54
+ };
55
+ }
56
+ const name = (params.name ?? "").trim();
57
+ if (!name) {
58
+ return {
59
+ content: [{ type: "text", text: "请提供智能体名称(name)。" }],
60
+ details: undefined,
61
+ };
62
+ }
63
+ let workspace = (params.workspace ?? "").trim() && WORKSPACE_NAME_REGEX.test((params.workspace ?? "").trim())
64
+ ? (params.workspace ?? "").trim()
65
+ : slugifyFromName(name);
66
+ if (workspace === RESERVED_WORKSPACE)
67
+ workspace = "agent-" + Date.now().toString(36);
68
+ const systemPrompt = (params.system_prompt ?? "").trim() ||
69
+ getDefaultSystemPrompt(params.language === "zh" ? "zh" : "en");
70
+ try {
71
+ const result = await provider({
72
+ name,
73
+ workspace,
74
+ systemPrompt,
75
+ });
76
+ if ("error" in result) {
77
+ return {
78
+ content: [{ type: "text", text: `创建失败: ${result.error}` }],
79
+ details: undefined,
80
+ };
81
+ }
82
+ const text = `已创建智能体「${result.name}」(ID: ${result.id})。用户可通过 list_agents 查看、并调用 switch_agent 切换到该智能体使用。`;
83
+ return {
84
+ content: [{ type: "text", text }],
85
+ details: result,
86
+ };
87
+ }
88
+ catch (err) {
89
+ const msg = err instanceof Error ? err.message : String(err);
90
+ return {
91
+ content: [{ type: "text", text: `创建失败: ${msg}` }],
92
+ details: undefined,
93
+ };
94
+ }
95
+ },
96
+ };
97
+ }
@@ -1,3 +1,7 @@
1
1
  export { createBrowserTool, closeBrowser } from "./browser-tool.js";
2
2
  export { createSaveExperienceTool } from "./save-experience-tool.js";
3
3
  export { createInstallSkillTool } from "./install-skill-tool.js";
4
+ export { createSwitchAgentTool } from "./switch-agent-tool.js";
5
+ export { createListAgentsTool } from "./list-agents-tool.js";
6
+ export { createCreateAgentTool } from "./create-agent-tool.js";
7
+ export { createGetBookmarkTagsTool, createSaveBookmarkTool } from "./bookmark-tool.js";
@@ -1,3 +1,7 @@
1
1
  export { createBrowserTool, closeBrowser } from "./browser-tool.js";
2
2
  export { createSaveExperienceTool } from "./save-experience-tool.js";
3
3
  export { createInstallSkillTool } from "./install-skill-tool.js";
4
+ export { createSwitchAgentTool } from "./switch-agent-tool.js";
5
+ export { createListAgentsTool } from "./list-agents-tool.js";
6
+ export { createCreateAgentTool } from "./create-agent-tool.js";
7
+ export { createGetBookmarkTagsTool, createSaveBookmarkTool } from "./bookmark-tool.js";
@@ -0,0 +1,5 @@
1
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ /**
3
+ * 创建 list_agents 工具:返回当前节点上配置的所有智能体列表,供用户在对话中选择切换。
4
+ */
5
+ export declare function createListAgentsTool(): ToolDefinition;
@@ -0,0 +1,45 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getAgentListProvider } from "../session-current-agent.js";
3
+ const ListAgentsSchema = Type.Object({});
4
+ /**
5
+ * 创建 list_agents 工具:返回当前节点上配置的所有智能体列表,供用户在对话中选择切换。
6
+ */
7
+ export function createListAgentsTool() {
8
+ return {
9
+ name: "list_agents",
10
+ label: "List Agents",
11
+ description: "列出当前可用的所有智能体(id 与名称)。用户可根据列表选择并让助手调用 switch_agent 切换到对应智能体。",
12
+ parameters: ListAgentsSchema,
13
+ execute: async (_toolCallId, _params, _signal, _onUpdate, _ctx) => {
14
+ const provider = getAgentListProvider();
15
+ if (!provider) {
16
+ return {
17
+ content: [{ type: "text", text: "当前无法获取智能体列表。" }],
18
+ details: undefined,
19
+ };
20
+ }
21
+ try {
22
+ const list = await provider();
23
+ if (!list?.length) {
24
+ return {
25
+ content: [{ type: "text", text: "暂无配置的智能体。" }],
26
+ details: { agents: [] },
27
+ };
28
+ }
29
+ const lines = list.map((a) => `- **${a.id}**${a.name ? `:${a.name}` : ""}`);
30
+ const text = "当前可用智能体:\n\n" + lines.join("\n") + "\n\n使用 switch_agent 工具并传入上述 id 可切换。";
31
+ return {
32
+ content: [{ type: "text", text }],
33
+ details: { agents: list },
34
+ };
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ return {
39
+ content: [{ type: "text", text: `获取列表失败: ${msg}` }],
40
+ details: undefined,
41
+ };
42
+ }
43
+ },
44
+ };
45
+ }
@@ -0,0 +1,6 @@
1
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
2
+ /**
3
+ * 创建 switch_agent 工具:将当前会话的下一次及后续对话切换到指定 agent。
4
+ * 需在 Gateway 启动时通过 setSessionCurrentAgentUpdater 注入更新函数。
5
+ */
6
+ export declare function createSwitchAgentTool(businessSessionId: string): ToolDefinition;
@@ -0,0 +1,54 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import { getSessionCurrentAgentUpdater } from "../session-current-agent.js";
3
+ const SwitchAgentSchema = Type.Object({
4
+ agent_id: Type.String({
5
+ description: "要切换到的智能体 ID,与设置中智能体列表一致(如 default 或工作空间名)",
6
+ }),
7
+ });
8
+ /**
9
+ * 创建 switch_agent 工具:将当前会话的下一次及后续对话切换到指定 agent。
10
+ * 需在 Gateway 启动时通过 setSessionCurrentAgentUpdater 注入更新函数。
11
+ */
12
+ export function createSwitchAgentTool(businessSessionId) {
13
+ return {
14
+ name: "switch_agent",
15
+ label: "Switch Agent",
16
+ description: "将当前对话切换为使用指定智能体。切换后,用户下一次及后续消息将由该智能体处理。可用于在对话中更换助手(如从主智能体切换到专项智能体)。",
17
+ parameters: SwitchAgentSchema,
18
+ execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
19
+ const agentId = (params.agent_id ?? "").trim();
20
+ if (!agentId) {
21
+ return {
22
+ content: [{ type: "text", text: "请提供要切换到的智能体 ID(如 default 或工作空间名)。" }],
23
+ details: undefined,
24
+ };
25
+ }
26
+ const updateFn = getSessionCurrentAgentUpdater();
27
+ if (!updateFn) {
28
+ return {
29
+ content: [{ type: "text", text: "当前环境不支持切换智能体。" }],
30
+ details: undefined,
31
+ };
32
+ }
33
+ try {
34
+ updateFn(businessSessionId, agentId);
35
+ return {
36
+ content: [
37
+ {
38
+ type: "text",
39
+ text: `已切换到智能体「${agentId}」。后续回复将由该智能体处理。`,
40
+ },
41
+ ],
42
+ details: { agentId },
43
+ };
44
+ }
45
+ catch (err) {
46
+ const msg = err instanceof Error ? err.message : String(err);
47
+ return {
48
+ content: [{ type: "text", text: `切换失败: ${msg}` }],
49
+ details: undefined,
50
+ };
51
+ }
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,11 @@
1
+ import type { IChannel } from "../types.js";
2
+ export interface FeishuChannelConfig {
3
+ appId: string;
4
+ appSecret: string;
5
+ /** 默认绑定的 agentId */
6
+ defaultAgentId?: string;
7
+ }
8
+ /**
9
+ * 飞书通道:WebSocket 入站 + API 出站,使用官方 Node SDK。
10
+ */
11
+ export declare function createFeishuChannel(config: FeishuChannelConfig): IChannel;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * 飞书通道适配器:使用 @larksuiteoapi/node-sdk WebSocket 长连接模式接收消息,API 发送回复。
3
+ */
4
+ import * as Lark from "@larksuiteoapi/node-sdk";
5
+ import { dispatchMessage } from "../registry.js";
6
+ function parseFeishuTextContent(content) {
7
+ try {
8
+ const obj = JSON.parse(content);
9
+ if (obj && typeof obj.text === "string")
10
+ return obj.text;
11
+ return String(content);
12
+ }
13
+ catch {
14
+ return String(content);
15
+ }
16
+ }
17
+ /** 已处理的飞书 message_id 缓存,避免同一条消息被重复触发导致多条回复与顺序错乱。TTL 5 分钟。 */
18
+ const PROCESSED_MESSAGE_IDS = new Map();
19
+ const PROCESSED_TTL_MS = 5 * 60 * 1000;
20
+ function isMessageAlreadyProcessed(messageId) {
21
+ const now = Date.now();
22
+ if (PROCESSED_MESSAGE_IDS.has(messageId)) {
23
+ const ts = PROCESSED_MESSAGE_IDS.get(messageId);
24
+ if (now - ts < PROCESSED_TTL_MS)
25
+ return true;
26
+ PROCESSED_MESSAGE_IDS.delete(messageId);
27
+ }
28
+ return false;
29
+ }
30
+ function markMessageProcessed(messageId) {
31
+ const now = Date.now();
32
+ PROCESSED_MESSAGE_IDS.set(messageId, now);
33
+ if (PROCESSED_MESSAGE_IDS.size > 5000) {
34
+ for (const [id, ts] of PROCESSED_MESSAGE_IDS.entries()) {
35
+ if (now - ts > PROCESSED_TTL_MS)
36
+ PROCESSED_MESSAGE_IDS.delete(id);
37
+ }
38
+ }
39
+ }
40
+ /**
41
+ * 飞书 WebSocket 入站:SDK WSClient + EventDispatcher,收到 im.message.receive_v1 后转 UnifiedMessage。
42
+ * 按 message_id 去重,同一条用户消息只处理一次,避免重复回复与顺序错乱。
43
+ */
44
+ class FeishuWSInbound {
45
+ config;
46
+ wsClient = null;
47
+ messageHandler = null;
48
+ constructor(config) {
49
+ this.config = config;
50
+ }
51
+ setMessageHandler(handler) {
52
+ this.messageHandler = handler;
53
+ }
54
+ async start() {
55
+ if (this.wsClient)
56
+ return;
57
+ const { appId, appSecret } = this.config;
58
+ if (!appId?.trim() || !appSecret?.trim()) {
59
+ console.warn("[Feishu] appId/appSecret missing, skip WS start");
60
+ return;
61
+ }
62
+ const eventDispatcher = new Lark.EventDispatcher({}).register({
63
+ // 已读回执事件,无需处理,注册空 handler 避免 SDK 打 no handle 的 warn
64
+ "im.message.message_read_v1": async () => { },
65
+ "im.message.receive_v1": async (data) => {
66
+ const msg = data?.message;
67
+ if (!msg?.chat_id)
68
+ return;
69
+ const messageId = msg?.message_id;
70
+ if (messageId && isMessageAlreadyProcessed(messageId)) {
71
+ return;
72
+ }
73
+ if (messageId)
74
+ markMessageProcessed(messageId);
75
+ const content = msg.content ? parseFeishuTextContent(msg.content) : "";
76
+ if (!content.trim())
77
+ return;
78
+ const sender = data?.sender?.sender_id?.open_id ?? data?.sender?.sender_id?.user_id ?? "";
79
+ const unified = {
80
+ channelId: "feishu",
81
+ threadId: msg.chat_id,
82
+ userId: sender || "unknown",
83
+ userName: data?.sender?.sender_id?.name,
84
+ messageText: content,
85
+ replyTarget: "default",
86
+ messageId,
87
+ raw: data,
88
+ };
89
+ if (this.messageHandler) {
90
+ await this.messageHandler(unified);
91
+ }
92
+ else {
93
+ await dispatchMessage(unified);
94
+ }
95
+ },
96
+ });
97
+ this.wsClient = new Lark.WSClient({
98
+ appId,
99
+ appSecret,
100
+ loggerLevel: Lark.LoggerLevel.warn,
101
+ });
102
+ await this.wsClient.start({ eventDispatcher });
103
+ console.log("[Feishu] WS client started");
104
+ }
105
+ async stop() {
106
+ if (this.wsClient) {
107
+ try {
108
+ await this.wsClient.stop?.();
109
+ }
110
+ catch (e) {
111
+ console.warn("[Feishu] WS stop error", e);
112
+ }
113
+ this.wsClient = null;
114
+ }
115
+ }
116
+ }
117
+ /**
118
+ * 飞书 API 出站:用 Client 调发送消息接口。
119
+ */
120
+ class FeishuApiOutbound {
121
+ client;
122
+ constructor(config) {
123
+ this.client = new Lark.Client({
124
+ appId: config.appId,
125
+ appSecret: config.appSecret,
126
+ appType: Lark.AppType.SelfBuild,
127
+ domain: Lark.Domain.Feishu,
128
+ });
129
+ }
130
+ async send(targetId, reply) {
131
+ if (!targetId || targetId === "default") {
132
+ console.error("[Feishu] send skipped: invalid receive_id (missing or 'default'), check threadId from event");
133
+ return;
134
+ }
135
+ const text = reply.text?.trim() || "(无内容)";
136
+ try {
137
+ await this.client.im.v1.message.create({
138
+ params: { receive_id_type: "chat_id" },
139
+ data: {
140
+ receive_id: targetId,
141
+ content: JSON.stringify({ text }),
142
+ msg_type: "text",
143
+ },
144
+ });
145
+ }
146
+ catch (e) {
147
+ console.error("[Feishu] send message failed:", e);
148
+ throw e;
149
+ }
150
+ }
151
+ async sendStream(targetId) {
152
+ if (!targetId || targetId === "default") {
153
+ throw new Error("[Feishu] sendStream: invalid receive_id");
154
+ }
155
+ const initialCard = {
156
+ config: { wide_screen_mode: true },
157
+ header: { title: { tag: "plain_text", content: "🤔 思考中..." } },
158
+ elements: [{ tag: "div", text: { tag: "plain_text", content: "正在生成回答,请稍候..." } }],
159
+ };
160
+ const createRes = await this.client.im.v1.message.create({
161
+ params: { receive_id_type: "chat_id" },
162
+ data: {
163
+ receive_id: targetId,
164
+ msg_type: "interactive",
165
+ content: JSON.stringify(initialCard),
166
+ },
167
+ });
168
+ const messageId = createRes.data?.message_id;
169
+ if (!messageId) {
170
+ throw new Error("[Feishu] sendStream: create card did not return message_id");
171
+ }
172
+ const patch = async (content, title, showCursor) => {
173
+ const card = {
174
+ config: { wide_screen_mode: true },
175
+ header: { title: { tag: "plain_text", content: title } },
176
+ elements: [{ tag: "markdown", content: content + (showCursor ? " ▌" : "") }],
177
+ };
178
+ await this.client.im.v1.message.patch({
179
+ path: { message_id: messageId },
180
+ data: { content: JSON.stringify(card) },
181
+ });
182
+ };
183
+ return {
184
+ onChunk: async (accumulated) => {
185
+ try {
186
+ await patch(accumulated || " ", "🤖 回答中...", true);
187
+ }
188
+ catch (e) {
189
+ console.error("[Feishu] stream patch failed:", e);
190
+ }
191
+ },
192
+ onDone: async (accumulated) => {
193
+ try {
194
+ await patch(accumulated || "(无内容)", "✅ 回答完成", false);
195
+ }
196
+ catch (e) {
197
+ console.error("[Feishu] stream final patch failed:", e);
198
+ }
199
+ },
200
+ };
201
+ }
202
+ }
203
+ /**
204
+ * 飞书通道:WebSocket 入站 + API 出站,使用官方 Node SDK。
205
+ */
206
+ export function createFeishuChannel(config) {
207
+ const inbound = new FeishuWSInbound(config);
208
+ const outbound = new FeishuApiOutbound(config);
209
+ // 入站回调统一走 registry 分发(会带上 channelId,找到本 channel 再处理)
210
+ inbound.setMessageHandler((msg) => dispatchMessage(msg));
211
+ return {
212
+ id: "feishu",
213
+ name: "飞书",
214
+ defaultAgentId: config.defaultAgentId ?? "default",
215
+ getInbounds: () => [inbound],
216
+ getOutbounds: () => [outbound],
217
+ };
218
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 通道核心:收 UnifiedMessage → 会话映射 → 调 Agent → 选 Outbound → 发 UnifiedReply。
3
+ */
4
+ import type { UnifiedMessage, IChannel } from "./types.js";
5
+ /**
6
+ * 处理一条入站消息:映射 session、跑 Agent、选出站并发送回复。
7
+ * 若 outbound 支持 sendStream,则用流式(先发卡片再逐次更新);否则一次性 send。
8
+ */
9
+ export declare function handleChannelMessage(channel: IChannel, msg: UnifiedMessage): Promise<void>;