@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
@@ -11,8 +11,8 @@
11
11
  <link
12
12
  href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Roboto+Mono:wght@400;500&display=swap"
13
13
  rel="stylesheet">
14
- <script type="module" crossorigin src="/assets/index-BOS-F8a4.js"></script>
15
- <link rel="stylesheet" crossorigin href="/assets/index-DxqxayUL.css">
14
+ <script type="module" crossorigin src="/assets/index-DvQjslfT.js"></script>
15
+ <link rel="stylesheet" crossorigin href="/assets/index-BNuvb6Ay.css">
16
16
  </head>
17
17
 
18
18
  <body>
@@ -1,4 +1,5 @@
1
1
  import type { AgentSession } from "@mariozechner/pi-coding-agent";
2
+ import type { McpServerConfig } from "../mcp/index.js";
2
3
  import type { Skill } from "./skills.js";
3
4
  export interface AgentManagerOptions {
4
5
  agentDir?: string;
@@ -41,21 +42,31 @@ export declare class AgentManager {
41
42
  private resolveSkillPaths;
42
43
  /**
43
44
  * Get or create an agent session.
44
- * @param options.workspace 该会话绑定的工作区名(来自 agent 配置),不传则用 default,创建时 cwd/技能路径依此
45
- * @param options.provider / options.modelId 来自 agent 配置的大模型,不传则用环境变量默认
46
- * @param options.maxSessions 若提供且当前 session >= 该值,会先淘汰最后调用时间最早的 session 再创建新的
47
- * @param options.targetAgentId 创建时绑定到 install_skill 工具,用于安装目标(具体 agentId 或 global)
45
+ * 缓存 key sessionId + "::" + agentId,同一业务 session 下可切换 agent 且各自保留上下文。
46
+ * @param sessionId 业务会话 ID(桌面 UUID channel:feishu:threadId 等)
47
+ * @param options.agentId 当前使用的 agent,与 sessionId 组成复合 key,必传或默认 "default"
48
+ * @param options.workspace 该会话绑定的工作区名(来自 agent 配置)
49
+ * @param options.maxSessions 若提供且当前 session 数 >= 该值,按 LRU 淘汰
50
+ * @param options.targetAgentId 创建时绑定到 install_skill 工具
48
51
  */
49
52
  getOrCreateSession(sessionId: string, options?: {
53
+ agentId?: string;
50
54
  workspace?: string;
51
55
  provider?: string;
52
56
  modelId?: string;
53
57
  apiKey?: string;
54
58
  maxSessions?: number;
55
59
  targetAgentId?: string;
60
+ mcpServers?: McpServerConfig[];
61
+ /** 自定义系统提示词(来自 agent 配置),会与技能等一起组成最终 systemPrompt */
62
+ systemPrompt?: string;
56
63
  }): Promise<AgentSession>;
57
- getSession(sessionId: string): AgentSession | undefined;
58
- deleteSession(sessionId: string): boolean;
64
+ /** 按复合 key 获取(key = sessionId + "::" + agentId) */
65
+ getSession(compositeKey: string): AgentSession | undefined;
66
+ /** 删除一个 Agent Session(传入复合 key) */
67
+ deleteSession(compositeKey: string): boolean;
68
+ /** 按业务 sessionId 删除该会话下所有 agent 的 Core Session(如删除会话时) */
69
+ deleteSessionsByBusinessId(sessionId: string): void;
59
70
  clearAll(): void;
60
71
  }
61
72
  export declare const agentManager: AgentManager;
@@ -3,7 +3,13 @@ import { join } from "node:path";
3
3
  import { existsSync, mkdirSync } from "node:fs";
4
4
  import { createCompactionMemoryExtensionFactory } from "../memory/compaction-extension.js";
5
5
  import { getCompactionContextForSystemPrompt } from "../memory/index.js";
6
- import { createBrowserTool, createSaveExperienceTool, createInstallSkillTool } from "../tools/index.js";
6
+ import { createBrowserTool, createSaveExperienceTool, createInstallSkillTool, createSwitchAgentTool, createListAgentsTool, createCreateAgentTool, createGetBookmarkTagsTool, createSaveBookmarkTool } from "../tools/index.js";
7
+ /** Agent Session 缓存 key:sessionId + "::" + agentId,同一业务 session 下不同 agent 各自一个 Core Session */
8
+ const COMPOSITE_KEY_SEP = "::";
9
+ function toCompositeKey(sessionId, agentId) {
10
+ return sessionId + COMPOSITE_KEY_SEP + agentId;
11
+ }
12
+ import { createMcpToolsForSession } from "../mcp/index.js";
7
13
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai/dist/providers/register-builtins.js";
8
14
  import { getOpenbotAgentDir, getOpenbotWorkspaceDir, ensureDefaultAgentDir } from "./agent-dir.js";
9
15
  import { formatSkillsForPrompt } from "./skills.js";
@@ -100,7 +106,7 @@ For downloads, provide either a direct URL or a selector to click.`;
100
106
  const systemPrompt = this.buildSystemPrompt(loadedSkills);
101
107
  return { systemPrompt, skills: loadedSkills };
102
108
  }
103
- createResourceLoader(workspaceDir, sessionId, compactionBlock) {
109
+ createResourceLoader(workspaceDir, sessionId, compactionBlock, customAgentPrompt, identity) {
104
110
  const loader = new DefaultResourceLoader({
105
111
  cwd: workspaceDir,
106
112
  agentDir: this.agentDir,
@@ -109,11 +115,18 @@ For downloads, provide either a direct URL or a selector to click.`;
109
115
  extensionFactories: sessionId ? [createCompactionMemoryExtensionFactory(sessionId)] : [],
110
116
  systemPromptOverride: (base) => {
111
117
  const loadedSkills = loader.getSkills().skills;
112
- let customPrompt = this.buildSystemPrompt(loadedSkills);
118
+ let basePrompt = this.buildSystemPrompt(loadedSkills);
119
+ const withCustom = customAgentPrompt && customAgentPrompt.trim()
120
+ ? customAgentPrompt.trim() + "\n\n" + basePrompt
121
+ : basePrompt;
122
+ const withIdentity = identity && identity.agentId
123
+ ? `[Session identity] You are the agent with ID: ${identity.agentId}, workspace: ${identity.workspace || identity.agentId}. When asked which agent you are, answer according to this identity.\n\n` +
124
+ withCustom
125
+ : withCustom;
113
126
  if (compactionBlock?.trim()) {
114
- customPrompt = customPrompt + "\n\n" + compactionBlock.trim();
127
+ return withIdentity + "\n\n" + compactionBlock.trim();
115
128
  }
116
- return customPrompt;
129
+ return withIdentity;
117
130
  },
118
131
  });
119
132
  return loader;
@@ -143,16 +156,20 @@ For downloads, provide either a direct URL or a selector to click.`;
143
156
  }
144
157
  /**
145
158
  * Get or create an agent session.
146
- * @param options.workspace 该会话绑定的工作区名(来自 agent 配置),不传则用 default,创建时 cwd/技能路径依此
147
- * @param options.provider / options.modelId 来自 agent 配置的大模型,不传则用环境变量默认
148
- * @param options.maxSessions 若提供且当前 session >= 该值,会先淘汰最后调用时间最早的 session 再创建新的
149
- * @param options.targetAgentId 创建时绑定到 install_skill 工具,用于安装目标(具体 agentId 或 global)
159
+ * 缓存 key sessionId + "::" + agentId,同一业务 session 下可切换 agent 且各自保留上下文。
160
+ * @param sessionId 业务会话 ID(桌面 UUID channel:feishu:threadId 等)
161
+ * @param options.agentId 当前使用的 agent,与 sessionId 组成复合 key,必传或默认 "default"
162
+ * @param options.workspace 该会话绑定的工作区名(来自 agent 配置)
163
+ * @param options.maxSessions 若提供且当前 session 数 >= 该值,按 LRU 淘汰
164
+ * @param options.targetAgentId 创建时绑定到 install_skill 工具
150
165
  */
151
166
  async getOrCreateSession(sessionId, options = {}) {
167
+ const agentId = options.agentId ?? "default";
168
+ const compositeKey = toCompositeKey(sessionId, agentId);
152
169
  const now = Date.now();
153
- if (this.sessions.has(sessionId)) {
154
- this.sessionLastActiveAt.set(sessionId, now);
155
- return this.sessions.get(sessionId);
170
+ if (this.sessions.has(compositeKey)) {
171
+ this.sessionLastActiveAt.set(compositeKey, now);
172
+ return this.sessions.get(compositeKey);
156
173
  }
157
174
  const { maxSessions } = options;
158
175
  if (typeof maxSessions === "number" && maxSessions > 0 && this.sessions.size >= maxSessions) {
@@ -222,7 +239,7 @@ For downloads, provide either a direct URL or a selector to click.`;
222
239
  return process.env.OPENAI_API_KEY;
223
240
  });
224
241
  const compactionBlock = await getCompactionContextForSystemPrompt(sessionId);
225
- const loader = this.createResourceLoader(sessionWorkspaceDir, sessionId, compactionBlock);
242
+ const loader = this.createResourceLoader(sessionWorkspaceDir, sessionId, compactionBlock, options.systemPrompt, { agentId, workspace: workspaceName });
226
243
  await loader.reload();
227
244
  const coreTools = {
228
245
  read: createReadTool(sessionWorkspaceDir),
@@ -233,6 +250,18 @@ For downloads, provide either a direct URL or a selector to click.`;
233
250
  grep: createGrepTool(sessionWorkspaceDir),
234
251
  ls: createLsTool(sessionWorkspaceDir),
235
252
  };
253
+ const mcpTools = await createMcpToolsForSession({ mcpServers: options.mcpServers });
254
+ const customTools = [
255
+ createBrowserTool(sessionWorkspaceDir),
256
+ createSaveExperienceTool(sessionId),
257
+ createInstallSkillTool(options.targetAgentId ?? agentId),
258
+ createSwitchAgentTool(sessionId),
259
+ createListAgentsTool(),
260
+ createCreateAgentTool(),
261
+ createGetBookmarkTagsTool(),
262
+ createSaveBookmarkTool(),
263
+ ...mcpTools,
264
+ ];
236
265
  const { session } = await createAgentSession({
237
266
  agentDir: this.agentDir,
238
267
  sessionManager: CoreSessionManager.inMemory(),
@@ -240,11 +269,7 @@ For downloads, provide either a direct URL or a selector to click.`;
240
269
  modelRegistry,
241
270
  cwd: sessionWorkspaceDir,
242
271
  resourceLoader: loader,
243
- customTools: [
244
- createBrowserTool(sessionWorkspaceDir),
245
- createSaveExperienceTool(sessionId),
246
- createInstallSkillTool(options.targetAgentId),
247
- ],
272
+ customTools,
248
273
  baseToolsOverride: coreTools,
249
274
  });
250
275
  const model = modelRegistry.find(provider, modelId);
@@ -252,16 +277,28 @@ For downloads, provide either a direct URL or a selector to click.`;
252
277
  console.log(`Setting model to ${model.provider}/${model.id} (workspace: ${workspaceName})`);
253
278
  await session.setModel(model);
254
279
  }
255
- this.sessions.set(sessionId, session);
256
- this.sessionLastActiveAt.set(sessionId, now);
280
+ this.sessions.set(compositeKey, session);
281
+ this.sessionLastActiveAt.set(compositeKey, now);
257
282
  return session;
258
283
  }
259
- getSession(sessionId) {
260
- return this.sessions.get(sessionId);
284
+ /** 按复合 key 获取(key = sessionId + "::" + agentId) */
285
+ getSession(compositeKey) {
286
+ return this.sessions.get(compositeKey);
287
+ }
288
+ /** 删除一个 Agent Session(传入复合 key) */
289
+ deleteSession(compositeKey) {
290
+ this.sessionLastActiveAt.delete(compositeKey);
291
+ return this.sessions.delete(compositeKey);
261
292
  }
262
- deleteSession(sessionId) {
263
- this.sessionLastActiveAt.delete(sessionId);
264
- return this.sessions.delete(sessionId);
293
+ /** 按业务 sessionId 删除该会话下所有 agent 的 Core Session(如删除会话时) */
294
+ deleteSessionsByBusinessId(sessionId) {
295
+ const prefix = sessionId + COMPOSITE_KEY_SEP;
296
+ for (const key of Array.from(this.sessions.keys())) {
297
+ if (key.startsWith(prefix)) {
298
+ this.sessionLastActiveAt.delete(key);
299
+ this.sessions.delete(key);
300
+ }
301
+ }
265
302
  }
266
303
  clearAll() {
267
304
  this.sessions.clear();
@@ -17,6 +17,7 @@ export async function run(options) {
17
17
  // Create a temporary session for this run
18
18
  const sessionId = `cli-${Date.now()}`;
19
19
  const session = await manager.getOrCreateSession(sessionId, {
20
+ agentId: "default",
20
21
  provider,
21
22
  modelId,
22
23
  apiKey,
@@ -58,8 +59,7 @@ export async function run(options) {
58
59
  });
59
60
  // Send prompt and wait for completion
60
61
  await session.prompt(userPrompt);
61
- // Clean up session
62
- manager.deleteSession(sessionId);
62
+ manager.deleteSession(sessionId + "::" + "default");
63
63
  result.assistantContent = assistantContent.trim();
64
64
  return result;
65
65
  }
@@ -7,6 +7,17 @@ export interface RagEmbeddingConfig {
7
7
  apiKey?: string;
8
8
  baseUrl?: string;
9
9
  }
10
+ /** 通道配置:各 IM 通道的 token、key 等,与设置页「通道配置」一致 */
11
+ export interface ChannelsConfig {
12
+ feishu?: {
13
+ enabled?: boolean;
14
+ appId?: string;
15
+ appSecret?: string;
16
+ defaultAgentId?: string;
17
+ };
18
+ }
19
+ /** MCP 服务器配置(与 core/mcp 类型一致,避免 core/config 依赖 core/mcp 实现) */
20
+ export type DesktopMcpServerConfig = import("../mcp/index.js").McpServerConfig;
10
21
  /**
11
22
  * 同步读取桌面全局配置中的 maxAgentSessions 等。
12
23
  * Gateway 进程内使用,用于会话上限等。
@@ -14,6 +25,8 @@ export interface RagEmbeddingConfig {
14
25
  export declare function getDesktopConfig(): {
15
26
  maxAgentSessions: number;
16
27
  };
28
+ /** 同步读取通道配置(Gateway 启动时用) */
29
+ export declare function getChannelsConfigSync(): ChannelsConfig;
17
30
  /** 同步读取 RAG embedding 配置;未配置或无效时返回 null,长记忆将空转 */
18
31
  export declare function getRagEmbeddingConfigSync(): RagEmbeddingConfig | null;
19
32
  export interface DesktopAgentConfig {
@@ -22,6 +35,10 @@ export interface DesktopAgentConfig {
22
35
  apiKey?: string;
23
36
  /** 工作区名,来自 agents.json 的 agent.workspace 或 agent.id */
24
37
  workspace?: string;
38
+ /** MCP 服务器配置,创建 Session 时传入 */
39
+ mcpServers?: DesktopMcpServerConfig[];
40
+ /** 自定义系统提示词,会与技能等一起组成最终 systemPrompt */
41
+ systemPrompt?: string;
25
42
  }
26
43
  /**
27
44
  * 从 config.json 读取缺省智能体 id(defaultAgentId)。
@@ -43,6 +43,20 @@ export function getDesktopConfig() {
43
43
  return { maxAgentSessions: DEFAULT_MAX_AGENT_SESSIONS };
44
44
  }
45
45
  }
46
+ /** 同步读取通道配置(Gateway 启动时用) */
47
+ export function getChannelsConfigSync() {
48
+ try {
49
+ const configPath = getConfigPath();
50
+ if (!existsSync(configPath))
51
+ return {};
52
+ const content = readFileSync(configPath, "utf-8");
53
+ const data = JSON.parse(content);
54
+ return data.channels ?? {};
55
+ }
56
+ catch {
57
+ return {};
58
+ }
59
+ }
46
60
  /** 同步读取 RAG embedding 配置;未配置或无效时返回 null,长记忆将空转 */
47
61
  export function getRagEmbeddingConfigSync() {
48
62
  try {
@@ -145,6 +159,8 @@ export async function loadDesktopAgentConfig(agentId) {
145
159
  }
146
160
  }
147
161
  let workspaceName = resolvedAgentId;
162
+ let mcpServers;
163
+ let systemPrompt;
148
164
  if (existsSync(agentsPath)) {
149
165
  try {
150
166
  const raw = await readFile(agentsPath, "utf-8");
@@ -156,6 +172,12 @@ export async function loadDesktopAgentConfig(agentId) {
156
172
  workspaceName = agent.workspace;
157
173
  else if (agent.id)
158
174
  workspaceName = agent.id;
175
+ if (agent.mcpServers && Array.isArray(agent.mcpServers)) {
176
+ mcpServers = agent.mcpServers;
177
+ }
178
+ if (agent.systemPrompt && typeof agent.systemPrompt === "string") {
179
+ systemPrompt = agent.systemPrompt.trim();
180
+ }
159
181
  if (agent.modelItemCode && Array.isArray(config.configuredModels)) {
160
182
  const configured = config.configuredModels.find((m) => m.modelItemCode === agent.modelItemCode);
161
183
  if (configured) {
@@ -185,7 +207,7 @@ export async function loadDesktopAgentConfig(agentId) {
185
207
  const apiKey = provConfig?.apiKey && typeof provConfig.apiKey === "string" && provConfig.apiKey.trim()
186
208
  ? provConfig.apiKey.trim()
187
209
  : undefined;
188
- return { provider, model, apiKey: apiKey ?? undefined, workspace: workspaceName };
210
+ return { provider, model, apiKey: apiKey ?? undefined, workspace: workspaceName, mcpServers, systemPrompt };
189
211
  }
190
212
  function ensureDesktopDir() {
191
213
  const desktopDir = getDesktopDir();
@@ -1 +1 @@
1
- export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, type InstallByUrlOptions, type InstallByUrlResult, type InstallFromPathOptions, type InstallFromPathResult, } from "./skill-installer.js";
1
+ export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, installSkillFromUpload, type InstallByUrlOptions, type InstallByUrlResult, type InstallFromPathOptions, type InstallFromPathResult, type InstallFromUploadOptions, } from "./skill-installer.js";
@@ -1 +1 @@
1
- export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, } from "./skill-installer.js";
1
+ export { resolveInstallTarget, installSkillByUrl, installSkillFromPath, installSkillFromUpload, } from "./skill-installer.js";
@@ -28,3 +28,12 @@ export interface InstallFromPathResult {
28
28
  * 从本地目录安装技能:将指定目录复制到目标 skills 目录。
29
29
  */
30
30
  export declare function installSkillFromPath(localPath: string, options?: InstallFromPathOptions): Promise<InstallFromPathResult>;
31
+ export interface InstallFromUploadOptions {
32
+ scope: "global" | "workspace";
33
+ workspace?: string;
34
+ }
35
+ /**
36
+ * 从上传的 zip 安装技能:解压到临时目录,校验为单个技能目录(含 SKILL.md),再复制到目标。
37
+ * 支持两种 zip 结构:① 单个顶层目录且内含 SKILL.md;② 根目录直接含 SKILL.md(将视为技能根目录)。
38
+ */
39
+ export declare function installSkillFromUpload(zipBuffer: Buffer, options?: InstallFromUploadOptions): Promise<InstallFromPathResult>;
@@ -7,6 +7,7 @@ import { existsSync } from "fs";
7
7
  import { join, resolve, basename } from "path";
8
8
  import { tmpdir } from "os";
9
9
  import { randomUUID } from "crypto";
10
+ import AdmZip from "adm-zip";
10
11
  import { exec } from "child_process";
11
12
  import { promisify } from "util";
12
13
  import { homedir } from "os";
@@ -119,3 +120,96 @@ export async function installSkillFromPath(localPath, options = { scope: "global
119
120
  await cp(srcResolved, destPath, { recursive: true });
120
121
  return { installDir: targetDir, name: baseName };
121
122
  }
123
+ /** 上传 zip 包最大体积(字节) */
124
+ const MAX_UPLOAD_ZIP_BYTES = 10 * 1024 * 1024;
125
+ /** 解压后忽略的条目(系统/打包产生的噪音) */
126
+ const IGNORED_ZIP_ENTRIES = new Set(["__MACOSX", ".DS_Store", ".git", "Thumbs.db"]);
127
+ function isIgnoredZipEntry(name) {
128
+ if (!name || name.includes(".."))
129
+ return true;
130
+ if (IGNORED_ZIP_ENTRIES.has(name))
131
+ return true;
132
+ if (name.startsWith("."))
133
+ return true;
134
+ return false;
135
+ }
136
+ /**
137
+ * 从上传的 zip 安装技能:解压到临时目录,校验为单个技能目录(含 SKILL.md),再复制到目标。
138
+ * 支持两种 zip 结构:① 单个顶层目录且内含 SKILL.md;② 根目录直接含 SKILL.md(将视为技能根目录)。
139
+ */
140
+ export async function installSkillFromUpload(zipBuffer, options = { scope: "global", workspace: "default" }) {
141
+ if (zipBuffer.length > MAX_UPLOAD_ZIP_BYTES) {
142
+ throw new Error("zip 包不能超过 10MB");
143
+ }
144
+ const tempDir = join(tmpdir(), `openbot-upload-${randomUUID()}`);
145
+ try {
146
+ await mkdir(tempDir, { recursive: true });
147
+ const zip = new AdmZip(zipBuffer);
148
+ zip.extractAllTo(tempDir, true);
149
+ const allEntries = await readdir(tempDir);
150
+ const entries = allEntries.filter((e) => !isIgnoredZipEntry(e));
151
+ let skillPath;
152
+ if (entries.length === 0) {
153
+ throw new Error("zip 解压后未得到有效内容,请检查 zip 是否包含技能目录或 SKILL.md");
154
+ }
155
+ if (entries.length === 1) {
156
+ const singleName = entries[0];
157
+ const candidatePath = join(tempDir, singleName);
158
+ const statEntry = await stat(candidatePath);
159
+ if (statEntry.isDirectory()) {
160
+ const skillMdInDir = join(candidatePath, "SKILL.md");
161
+ if (existsSync(skillMdInDir)) {
162
+ skillPath = candidatePath;
163
+ }
164
+ else {
165
+ throw new Error("该目录下未找到 SKILL.md,请确保 zip 内技能目录根目录含有 SKILL.md 文件");
166
+ }
167
+ }
168
+ else {
169
+ if (existsSync(join(tempDir, "SKILL.md"))) {
170
+ throw new Error("zip 根目录含有 SKILL.md,但根目录下还有其它文件,请将整个技能放在一个子目录内再打包");
171
+ }
172
+ throw new Error("zip 内未找到包含 SKILL.md 的技能目录,请检查打包方式");
173
+ }
174
+ }
175
+ else {
176
+ const skillMdAtRoot = existsSync(join(tempDir, "SKILL.md"));
177
+ const dirsWithSkillMd = [];
178
+ for (const e of entries) {
179
+ const p = join(tempDir, e);
180
+ const st = await stat(p).catch(() => null);
181
+ if (st?.isDirectory() && existsSync(join(p, "SKILL.md")))
182
+ dirsWithSkillMd.push(p);
183
+ }
184
+ if (dirsWithSkillMd.length === 1) {
185
+ skillPath = dirsWithSkillMd[0];
186
+ }
187
+ else if (dirsWithSkillMd.length > 1) {
188
+ throw new Error("zip 内包含多个技能目录,请只保留一个技能目录再打包");
189
+ }
190
+ else if (skillMdAtRoot) {
191
+ skillPath = tempDir;
192
+ }
193
+ else {
194
+ throw new Error("zip 内未找到包含 SKILL.md 的技能目录;若为多文件打包,请将 SKILL.md 放在 zip 根目录或单个子目录内");
195
+ }
196
+ }
197
+ if (skillPath === tempDir) {
198
+ const baseName = "skill";
199
+ const wrappedDir = join(tmpdir(), `openbot-skill-wrap-${randomUUID()}`);
200
+ try {
201
+ await mkdir(wrappedDir, { recursive: true });
202
+ const destPath = join(wrappedDir, baseName);
203
+ await cp(tempDir, destPath, { recursive: true });
204
+ return await installSkillFromPath(destPath, options);
205
+ }
206
+ finally {
207
+ await rm(wrappedDir, { recursive: true, force: true }).catch(() => { });
208
+ }
209
+ }
210
+ return await installSkillFromPath(skillPath, options);
211
+ }
212
+ finally {
213
+ await rm(tempDir, { recursive: true, force: true }).catch(() => { });
214
+ }
215
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * MCP Tool 转为 pi-coding-agent ToolDefinition 的适配层。
3
+ */
4
+ import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
5
+ import type { McpTool } from "./types.js";
6
+ import type { McpClient } from "./client.js";
7
+ /**
8
+ * 将单个 MCP 工具封装为 pi-coding-agent 的 ToolDefinition。
9
+ * @param tool MCP tools/list 返回的项
10
+ * @param client 已连接的 McpClient,用于 callTool
11
+ * @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
12
+ */
13
+ export declare function mcpToolToToolDefinition(tool: McpTool, client: McpClient, serverId?: string): ToolDefinition;
14
+ /**
15
+ * 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
16
+ */
17
+ export declare function mcpToolsToToolDefinitions(tools: McpTool[], client: McpClient, serverId?: string): ToolDefinition[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * MCP Tool 转为 pi-coding-agent ToolDefinition 的适配层。
3
+ */
4
+ import { Type } from "@sinclair/typebox";
5
+ /** 通用参数:MCP 工具接受任意 JSON 对象作为 arguments */
6
+ const McpToolParamsSchema = Type.Record(Type.String(), Type.Any());
7
+ /**
8
+ * 将单个 MCP 工具封装为 pi-coding-agent 的 ToolDefinition。
9
+ * @param tool MCP tools/list 返回的项
10
+ * @param client 已连接的 McpClient,用于 callTool
11
+ * @param serverId 可选前缀,用于避免多 MCP 时工具名冲突
12
+ */
13
+ export function mcpToolToToolDefinition(tool, client, serverId) {
14
+ const name = serverId ? `${serverId}_${tool.name}` : tool.name;
15
+ const description = (tool.description ?? "").trim() || `MCP tool: ${tool.name}`;
16
+ return {
17
+ name,
18
+ label: tool.name,
19
+ description,
20
+ parameters: McpToolParamsSchema,
21
+ execute: async (_toolCallId, params, _signal, _onUpdate, _ctx) => {
22
+ const args = params && typeof params === "object" ? params : {};
23
+ try {
24
+ const result = await client.callTool(tool.name, args);
25
+ const text = result.content
26
+ ?.filter((c) => c.type === "text")
27
+ .map((c) => c.text)
28
+ .join("\n") ?? (result.isError ? "MCP 调用返回错误" : "");
29
+ return {
30
+ content: [{ type: "text", text }],
31
+ details: result.isError ? { isError: true } : undefined,
32
+ };
33
+ }
34
+ catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ return {
37
+ content: [{ type: "text", text: `MCP 调用失败: ${msg}` }],
38
+ details: undefined,
39
+ };
40
+ }
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * 将某 MCP 客户端的全部工具转为 ToolDefinition 数组。
46
+ */
47
+ export function mcpToolsToToolDefinitions(tools, client, serverId) {
48
+ return tools.map((t) => mcpToolToToolDefinition(t, client, serverId));
49
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * MCP 客户端:连接、list_tools、call_tool、断开。
3
+ * 支持 stdio(本地进程)与 sse(远程 HTTP)两种传输。
4
+ */
5
+ import type { McpTool, McpToolCallResult, IMcpTransport } from "./types.js";
6
+ import type { McpServerConfig } from "./types.js";
7
+ export interface McpClientOptions {
8
+ initTimeoutMs?: number;
9
+ requestTimeoutMs?: number;
10
+ }
11
+ export declare class McpClient {
12
+ private transport;
13
+ private _tools;
14
+ private _requestId;
15
+ constructor(configOrTransport: McpServerConfig | IMcpTransport, options?: McpClientOptions);
16
+ /** 建立连接并完成握手;成功后可使用 listTools / callTool */
17
+ connect(): Promise<void>;
18
+ /** 获取工具列表(会缓存;断开重连后需重新 list) */
19
+ listTools(): Promise<McpTool[]>;
20
+ /** 调用指定工具 */
21
+ callTool(name: string, args: Record<string, unknown>): Promise<McpToolCallResult>;
22
+ close(): Promise<void>;
23
+ get isConnected(): boolean;
24
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * MCP 客户端:连接、list_tools、call_tool、断开。
3
+ * 支持 stdio(本地进程)与 sse(远程 HTTP)两种传输。
4
+ */
5
+ import { createTransport } from "./transport/index.js";
6
+ export class McpClient {
7
+ transport;
8
+ _tools = null;
9
+ _requestId = 0;
10
+ constructor(configOrTransport, options = {}) {
11
+ if (typeof configOrTransport.request === "function" &&
12
+ typeof configOrTransport.start === "function") {
13
+ this.transport = configOrTransport;
14
+ }
15
+ else {
16
+ this.transport = createTransport(configOrTransport, {
17
+ initTimeoutMs: options.initTimeoutMs,
18
+ requestTimeoutMs: options.requestTimeoutMs,
19
+ });
20
+ }
21
+ }
22
+ /** 建立连接并完成握手;成功后可使用 listTools / callTool */
23
+ async connect() {
24
+ await this.transport.start();
25
+ }
26
+ /** 获取工具列表(会缓存;断开重连后需重新 list) */
27
+ async listTools() {
28
+ if (this._tools !== null) {
29
+ return this._tools;
30
+ }
31
+ const res = await this.transport.request({
32
+ jsonrpc: "2.0",
33
+ id: ++this._requestId,
34
+ method: "tools/list",
35
+ });
36
+ if (res.error) {
37
+ throw new Error(`MCP tools/list failed: ${res.error.message}`);
38
+ }
39
+ const list = res.result?.tools;
40
+ this._tools = Array.isArray(list) ? list : [];
41
+ return this._tools;
42
+ }
43
+ /** 调用指定工具 */
44
+ async callTool(name, args) {
45
+ const res = await this.transport.request({
46
+ jsonrpc: "2.0",
47
+ id: ++this._requestId,
48
+ method: "tools/call",
49
+ params: { name, arguments: args ?? {} },
50
+ });
51
+ if (res.error) {
52
+ return {
53
+ content: [{ type: "text", text: `MCP call_tool error: ${res.error.message}` }],
54
+ isError: true,
55
+ };
56
+ }
57
+ const result = res.result;
58
+ if (!result || !Array.isArray(result.content)) {
59
+ return { content: [{ type: "text", text: "Invalid MCP tools/call result" }], isError: true };
60
+ }
61
+ return result;
62
+ }
63
+ async close() {
64
+ this._tools = null;
65
+ await this.transport.close();
66
+ }
67
+ get isConnected() {
68
+ return this.transport.isConnected;
69
+ }
70
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * MCP 配置解析与校验。
3
+ * 从 getOrCreateSession 的 options.mcpServers 中取出并规范化,支持 stdio 与 sse。
4
+ */
5
+ import type { McpServerConfig, McpServerConfigStdio, McpServerConfigSse } from "./types.js";
6
+ /**
7
+ * 从会话选项里解析出本会话启用的 MCP 服务器配置列表。
8
+ * 支持 stdio(本地进程)与 sse(远程 HTTP)。
9
+ */
10
+ export declare function resolveMcpServersForSession(mcpServers: McpServerConfig[] | undefined): McpServerConfig[];
11
+ /**
12
+ * 为 stdio 配置生成缓存键(用于 Operator 复用同一进程连接)
13
+ */
14
+ export declare function stdioConfigKey(config: McpServerConfigStdio): string;
15
+ /**
16
+ * 为 sse 配置生成缓存键
17
+ */
18
+ export declare function sseConfigKey(config: McpServerConfigSse): string;
19
+ /**
20
+ * 任意 MCP 配置的缓存键
21
+ */
22
+ export declare function mcpConfigKey(config: McpServerConfig): string;