@next-open-ai/openbot 0.2.1 → 0.3.2
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/README.md +117 -17
- package/apps/desktop/renderer/dist/assets/index-DKtaRFW4.js +89 -0
- package/apps/desktop/renderer/dist/assets/index-QHuqXpWQ.css +10 -0
- package/apps/desktop/renderer/dist/index.html +3 -3
- package/dist/cli/cli.js +27 -1
- package/dist/cli/service.d.ts +13 -0
- package/dist/cli/service.js +243 -0
- package/dist/core/agent/agent-manager.d.ts +3 -0
- package/dist/core/agent/agent-manager.js +12 -6
- package/dist/core/config/desktop-config.d.ts +4 -0
- package/dist/core/config/desktop-config.js +5 -1
- package/dist/core/installer/index.d.ts +1 -1
- package/dist/core/installer/index.js +1 -1
- package/dist/core/installer/skill-installer.d.ts +9 -0
- package/dist/core/installer/skill-installer.js +94 -0
- package/dist/core/mcp/adapter.d.ts +17 -0
- package/dist/core/mcp/adapter.js +49 -0
- package/dist/core/mcp/client.d.ts +24 -0
- package/dist/core/mcp/client.js +70 -0
- package/dist/core/mcp/config.d.ts +22 -0
- package/dist/core/mcp/config.js +69 -0
- package/dist/core/mcp/index.d.ts +18 -0
- package/dist/core/mcp/index.js +20 -0
- package/dist/core/mcp/operator.d.ts +15 -0
- package/dist/core/mcp/operator.js +72 -0
- package/dist/core/mcp/transport/index.d.ts +11 -0
- package/dist/core/mcp/transport/index.js +16 -0
- package/dist/core/mcp/transport/sse.d.ts +20 -0
- package/dist/core/mcp/transport/sse.js +82 -0
- package/dist/core/mcp/transport/stdio.d.ts +32 -0
- package/dist/core/mcp/transport/stdio.js +132 -0
- package/dist/core/mcp/types.d.ts +72 -0
- package/dist/core/mcp/types.js +5 -0
- package/dist/core/tools/bookmark-tool.d.ts +9 -0
- package/dist/core/tools/bookmark-tool.js +118 -0
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/gateway/methods/agent-chat.js +1 -0
- package/dist/gateway/methods/install-skill-from-upload.d.ts +14 -0
- package/dist/gateway/methods/install-skill-from-upload.js +13 -0
- package/dist/gateway/methods/run-scheduled-task.js +1 -0
- package/dist/gateway/server.js +26 -1
- package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
- package/dist/server/agent-config/agent-config.service.d.ts +4 -1
- package/dist/server/agent-config/agent-config.service.js +2 -0
- package/dist/server/agents/agents.controller.js +3 -5
- package/dist/server/agents/agents.service.d.ts +1 -1
- package/dist/server/agents/agents.service.js +4 -2
- package/dist/server/app.module.js +2 -0
- package/dist/server/database/database.service.d.ts +7 -0
- package/dist/server/database/database.service.js +54 -5
- package/dist/server/saved-items/saved-items.controller.d.ts +26 -0
- package/dist/server/saved-items/saved-items.controller.js +78 -0
- package/dist/server/saved-items/saved-items.module.d.ts +2 -0
- package/dist/server/saved-items/saved-items.module.js +23 -0
- package/dist/server/saved-items/saved-items.service.d.ts +31 -0
- package/dist/server/saved-items/saved-items.service.js +105 -0
- package/dist/server/saved-items/tags.controller.d.ts +30 -0
- package/dist/server/saved-items/tags.controller.js +85 -0
- package/dist/server/saved-items/tags.service.d.ts +24 -0
- package/dist/server/saved-items/tags.service.js +84 -0
- package/dist/server/skills/skills.service.d.ts +2 -0
- package/dist/server/skills/skills.service.js +80 -16
- package/package.json +8 -2
- package/skills/url-bookmark/SKILL.md +36 -0
- package/apps/desktop/renderer/dist/assets/index-BOS-F8a4.js +0 -89
- package/apps/desktop/renderer/dist/assets/index-DxqxayUL.css +0 -10
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP SSE/HTTP 传输:通过 HTTP POST 向远程 URL 发送 JSON-RPC 请求,响应在 response body。
|
|
3
|
+
*/
|
|
4
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
5
|
+
export class SseTransport {
|
|
6
|
+
config;
|
|
7
|
+
initTimeoutMs;
|
|
8
|
+
requestTimeoutMs;
|
|
9
|
+
_connected = false;
|
|
10
|
+
constructor(config, options = {}) {
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.initTimeoutMs = options.initTimeoutMs ?? 15_000;
|
|
13
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
14
|
+
}
|
|
15
|
+
async start() {
|
|
16
|
+
if (this._connected)
|
|
17
|
+
return;
|
|
18
|
+
const url = (this.config.url ?? "").trim();
|
|
19
|
+
if (!url)
|
|
20
|
+
throw new Error("MCP SSE url is required");
|
|
21
|
+
const initResult = await this.request({
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id: 1,
|
|
24
|
+
method: "initialize",
|
|
25
|
+
params: {
|
|
26
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
27
|
+
capabilities: {},
|
|
28
|
+
clientInfo: { name: "openbot", version: "0.1.0" },
|
|
29
|
+
},
|
|
30
|
+
}, this.initTimeoutMs);
|
|
31
|
+
if (initResult.error) {
|
|
32
|
+
throw new Error(`MCP initialize failed: ${initResult.error.message}`);
|
|
33
|
+
}
|
|
34
|
+
await this.request({ jsonrpc: "2.0", id: 2, method: "notifications/initialized" }, this.requestTimeoutMs).catch(() => ({}));
|
|
35
|
+
this._connected = true;
|
|
36
|
+
}
|
|
37
|
+
request(req, timeoutMs) {
|
|
38
|
+
const url = (this.config.url ?? "").trim();
|
|
39
|
+
if (!url) {
|
|
40
|
+
return Promise.reject(new Error("MCP SSE url is required"));
|
|
41
|
+
}
|
|
42
|
+
const t = timeoutMs ?? this.requestTimeoutMs;
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(), t);
|
|
45
|
+
const headers = {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
Accept: "application/json",
|
|
48
|
+
...this.config.headers,
|
|
49
|
+
};
|
|
50
|
+
return fetch(url, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers,
|
|
53
|
+
body: JSON.stringify(req),
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
})
|
|
56
|
+
.then(async (res) => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
const text = await res.text();
|
|
60
|
+
throw new Error(`MCP HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
61
|
+
}
|
|
62
|
+
const json = (await res.json());
|
|
63
|
+
if (!json || json.jsonrpc !== "2.0") {
|
|
64
|
+
throw new Error("Invalid MCP response");
|
|
65
|
+
}
|
|
66
|
+
return json;
|
|
67
|
+
})
|
|
68
|
+
.catch((err) => {
|
|
69
|
+
clearTimeout(timeoutId);
|
|
70
|
+
if (err.name === "AbortError") {
|
|
71
|
+
throw new Error(`MCP request timeout (${t}ms)`);
|
|
72
|
+
}
|
|
73
|
+
throw err;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async close() {
|
|
77
|
+
this._connected = false;
|
|
78
|
+
}
|
|
79
|
+
get isConnected() {
|
|
80
|
+
return this._connected;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio 传输:子进程 + 标准输入/输出上的 Newline-delimited JSON-RPC。
|
|
3
|
+
*/
|
|
4
|
+
import type { McpServerConfigStdio } from "../types.js";
|
|
5
|
+
import type { JsonRpcRequest, JsonRpcResponse } from "../types.js";
|
|
6
|
+
export interface StdioTransportOptions {
|
|
7
|
+
/** 初始化超时(毫秒) */
|
|
8
|
+
initTimeoutMs?: number;
|
|
9
|
+
/** 单次请求超时(毫秒) */
|
|
10
|
+
requestTimeoutMs?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class StdioTransport {
|
|
13
|
+
private process;
|
|
14
|
+
private config;
|
|
15
|
+
private initTimeoutMs;
|
|
16
|
+
private requestTimeoutMs;
|
|
17
|
+
private nextId;
|
|
18
|
+
private pending;
|
|
19
|
+
private buffer;
|
|
20
|
+
constructor(config: McpServerConfigStdio, options?: StdioTransportOptions);
|
|
21
|
+
/** 启动子进程并完成 MCP initialize 握手 */
|
|
22
|
+
start(): Promise<void>;
|
|
23
|
+
private flushLines;
|
|
24
|
+
private rejectAll;
|
|
25
|
+
private initialize;
|
|
26
|
+
private sendNotification;
|
|
27
|
+
/** 发送 JSON-RPC 请求并等待响应 */
|
|
28
|
+
request(req: JsonRpcRequest, timeoutMs?: number): Promise<JsonRpcResponse>;
|
|
29
|
+
/** 关闭子进程 */
|
|
30
|
+
close(): Promise<void>;
|
|
31
|
+
get isConnected(): boolean;
|
|
32
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP stdio 传输:子进程 + 标准输入/输出上的 Newline-delimited JSON-RPC。
|
|
3
|
+
*/
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
const MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
6
|
+
export class StdioTransport {
|
|
7
|
+
process = null;
|
|
8
|
+
config;
|
|
9
|
+
initTimeoutMs;
|
|
10
|
+
requestTimeoutMs;
|
|
11
|
+
nextId = 1;
|
|
12
|
+
pending = new Map();
|
|
13
|
+
buffer = "";
|
|
14
|
+
constructor(config, options = {}) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
this.initTimeoutMs = options.initTimeoutMs ?? 10_000;
|
|
17
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
|
|
18
|
+
}
|
|
19
|
+
/** 启动子进程并完成 MCP initialize 握手 */
|
|
20
|
+
async start() {
|
|
21
|
+
if (this.process) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const env = { ...process.env, ...this.config.env };
|
|
25
|
+
this.process = spawn(this.config.command, this.config.args ?? [], {
|
|
26
|
+
env,
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
});
|
|
29
|
+
const child = this.process;
|
|
30
|
+
child.stdout?.on("data", (chunk) => {
|
|
31
|
+
this.buffer += chunk.toString("utf-8");
|
|
32
|
+
this.flushLines();
|
|
33
|
+
});
|
|
34
|
+
child.stderr?.on("data", (data) => {
|
|
35
|
+
const msg = data.toString("utf-8").trim();
|
|
36
|
+
if (msg)
|
|
37
|
+
console.warn("[mcp stdio stderr]", msg);
|
|
38
|
+
});
|
|
39
|
+
child.on("error", (err) => {
|
|
40
|
+
this.rejectAll(new Error(`MCP process error: ${err.message}`));
|
|
41
|
+
});
|
|
42
|
+
child.on("exit", (code, signal) => {
|
|
43
|
+
this.rejectAll(new Error(`MCP process exited: code=${code} signal=${signal}`));
|
|
44
|
+
this.process = null;
|
|
45
|
+
});
|
|
46
|
+
await this.initialize();
|
|
47
|
+
}
|
|
48
|
+
flushLines() {
|
|
49
|
+
const lines = this.buffer.split("\n");
|
|
50
|
+
this.buffer = lines.pop() ?? "";
|
|
51
|
+
for (const line of lines) {
|
|
52
|
+
const trimmed = line.trim();
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
continue;
|
|
55
|
+
try {
|
|
56
|
+
const msg = JSON.parse(trimmed);
|
|
57
|
+
if ("id" in msg && msg.id !== undefined) {
|
|
58
|
+
const pending = this.pending.get(msg.id);
|
|
59
|
+
if (pending) {
|
|
60
|
+
clearTimeout(pending.timer);
|
|
61
|
+
this.pending.delete(msg.id);
|
|
62
|
+
if (msg.error) {
|
|
63
|
+
pending.reject(new Error(msg.error.message));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
pending.resolve(msg);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// 忽略非 JSON 行
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
rejectAll(err) {
|
|
77
|
+
for (const [, { reject, timer }] of this.pending) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
reject(err);
|
|
80
|
+
}
|
|
81
|
+
this.pending.clear();
|
|
82
|
+
}
|
|
83
|
+
async initialize() {
|
|
84
|
+
const initResult = await this.request({
|
|
85
|
+
jsonrpc: "2.0",
|
|
86
|
+
id: this.nextId++,
|
|
87
|
+
method: "initialize",
|
|
88
|
+
params: {
|
|
89
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
90
|
+
capabilities: {},
|
|
91
|
+
clientInfo: { name: "openbot", version: "0.1.0" },
|
|
92
|
+
},
|
|
93
|
+
}, this.initTimeoutMs);
|
|
94
|
+
if (initResult.error) {
|
|
95
|
+
throw new Error(`MCP initialize failed: ${initResult.error.message}`);
|
|
96
|
+
}
|
|
97
|
+
this.sendNotification({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
98
|
+
}
|
|
99
|
+
sendNotification(msg) {
|
|
100
|
+
if (!this.process?.stdin?.writable)
|
|
101
|
+
return;
|
|
102
|
+
this.process.stdin.write(JSON.stringify(msg) + "\n", "utf-8");
|
|
103
|
+
}
|
|
104
|
+
/** 发送 JSON-RPC 请求并等待响应 */
|
|
105
|
+
request(req, timeoutMs) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
if (!this.process?.stdin?.writable) {
|
|
108
|
+
reject(new Error("MCP transport not connected"));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const t = timeoutMs ?? this.requestTimeoutMs;
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
if (this.pending.delete(req.id)) {
|
|
114
|
+
reject(new Error(`MCP request timeout (${t}ms)`));
|
|
115
|
+
}
|
|
116
|
+
}, t);
|
|
117
|
+
this.pending.set(req.id, { resolve, reject, timer });
|
|
118
|
+
this.process.stdin.write(JSON.stringify(req) + "\n", "utf-8");
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** 关闭子进程 */
|
|
122
|
+
async close() {
|
|
123
|
+
if (this.process) {
|
|
124
|
+
this.rejectAll(new Error("MCP transport closed"));
|
|
125
|
+
this.process.kill("SIGTERM");
|
|
126
|
+
this.process = null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
get isConnected() {
|
|
130
|
+
return this.process != null && this.process.stdin?.writable === true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) 相关类型定义。
|
|
3
|
+
* 仅实现 Client 端 Tools 能力;Resources/Prompts 类型预留扩展。
|
|
4
|
+
*/
|
|
5
|
+
/** stdio 传输:通过子进程 stdin/stdout 进行 JSON-RPC 通信 */
|
|
6
|
+
export interface McpServerConfigStdio {
|
|
7
|
+
transport: "stdio";
|
|
8
|
+
/** 可执行路径(禁止从用户输入拼接,需白名单或配置) */
|
|
9
|
+
command: string;
|
|
10
|
+
/** 命令行参数 */
|
|
11
|
+
args?: string[];
|
|
12
|
+
/** 环境变量(可选,合并到 process.env) */
|
|
13
|
+
env?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
/** SSE/HTTP 远程传输:通过 URL POST JSON-RPC 请求 */
|
|
16
|
+
export interface McpServerConfigSse {
|
|
17
|
+
transport: "sse";
|
|
18
|
+
/** 远程 MCP 服务地址(如 https://example.com/mcp) */
|
|
19
|
+
url: string;
|
|
20
|
+
/** 可选请求头(如 Authorization) */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
/** 单条 MCP 服务器配置(由调用方在创建 Session 时传入,与 Skill 类似) */
|
|
24
|
+
export type McpServerConfig = McpServerConfigStdio | McpServerConfigSse;
|
|
25
|
+
/** MCP 协议中 Tool 的 inputSchema 为 JSON Schema 对象 */
|
|
26
|
+
export interface McpToolInputSchema {
|
|
27
|
+
type?: string;
|
|
28
|
+
properties?: Record<string, unknown>;
|
|
29
|
+
required?: string[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
/** MCP tools/list 返回的单个工具描述 */
|
|
33
|
+
export interface McpTool {
|
|
34
|
+
name: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
inputSchema?: McpToolInputSchema;
|
|
37
|
+
}
|
|
38
|
+
/** MCP tools/call 返回的 content 项 */
|
|
39
|
+
export interface McpToolCallContentItem {
|
|
40
|
+
type: "text";
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
/** MCP tools/call 的 result */
|
|
44
|
+
export interface McpToolCallResult {
|
|
45
|
+
content: McpToolCallContentItem[];
|
|
46
|
+
isError?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** JSON-RPC 2.0 请求 */
|
|
49
|
+
export interface JsonRpcRequest {
|
|
50
|
+
jsonrpc: "2.0";
|
|
51
|
+
id: number | string;
|
|
52
|
+
method: string;
|
|
53
|
+
params?: unknown;
|
|
54
|
+
}
|
|
55
|
+
/** JSON-RPC 2.0 成功响应 */
|
|
56
|
+
export interface JsonRpcResponse<T = unknown> {
|
|
57
|
+
jsonrpc: "2.0";
|
|
58
|
+
id: number | string;
|
|
59
|
+
result?: T;
|
|
60
|
+
error?: {
|
|
61
|
+
code: number;
|
|
62
|
+
message: string;
|
|
63
|
+
data?: unknown;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** 传输层抽象:stdio / sse 等实现此接口供 McpClient 使用 */
|
|
67
|
+
export interface IMcpTransport {
|
|
68
|
+
start(): Promise<void>;
|
|
69
|
+
request(req: JsonRpcRequest, timeoutMs?: number): Promise<JsonRpcResponse>;
|
|
70
|
+
close(): Promise<void>;
|
|
71
|
+
readonly isConnected: boolean;
|
|
72
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* 获取当前系统中已维护的标签列表,供保存 URL 时选择匹配的标签。
|
|
4
|
+
*/
|
|
5
|
+
export declare function createGetBookmarkTagsTool(): ToolDefinition;
|
|
6
|
+
/**
|
|
7
|
+
* 将 URL 保存为收藏,并关联指定标签(标签名须与 get_bookmark_tags 返回的一致)。
|
|
8
|
+
*/
|
|
9
|
+
export declare function createSaveBookmarkTool(): ToolDefinition;
|
|
@@ -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
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
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 { createGetBookmarkTagsTool, createSaveBookmarkTool } from "./bookmark-tool.js";
|
package/dist/core/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
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 { createGetBookmarkTagsTool, createSaveBookmarkTool } from "./bookmark-tool.js";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface InstallFromUploadBody {
|
|
2
|
+
/** zip 文件 buffer */
|
|
3
|
+
buffer: Buffer;
|
|
4
|
+
scope?: "global" | "workspace";
|
|
5
|
+
workspace?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface InstallFromUploadResult {
|
|
8
|
+
success: true;
|
|
9
|
+
data: {
|
|
10
|
+
installDir: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export declare function handleInstallSkillFromUpload(body: InstallFromUploadBody): Promise<InstallFromUploadResult>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 在 Gateway 层处理 POST /server-api/skills/install-from-upload,
|
|
3
|
+
* 接收 multipart zip 文件,委托核心 installer 解压并安装到全局或工作区。
|
|
4
|
+
*/
|
|
5
|
+
import { installSkillFromUpload } from "../../core/installer/index.js";
|
|
6
|
+
export async function handleInstallSkillFromUpload(body) {
|
|
7
|
+
const { buffer, scope = "global", workspace = "default" } = body;
|
|
8
|
+
if (!buffer || !Buffer.isBuffer(buffer) || buffer.length === 0) {
|
|
9
|
+
throw new Error("请上传 zip 文件");
|
|
10
|
+
}
|
|
11
|
+
const result = await installSkillFromUpload(buffer, { scope, workspace });
|
|
12
|
+
return { success: true, data: { installDir: result.installDir, name: result.name } };
|
|
13
|
+
}
|
package/dist/gateway/server.js
CHANGED
|
@@ -31,13 +31,16 @@ import { handleSse } from "./sse-handler.js";
|
|
|
31
31
|
import { handleVoiceUpgrade } from "./voice-handler.js";
|
|
32
32
|
import { handleConnection } from "./connection-handler.js";
|
|
33
33
|
import { handleRunScheduledTask } from "./methods/run-scheduled-task.js";
|
|
34
|
+
import multer from "multer";
|
|
34
35
|
import { handleInstallSkillFromPath } from "./methods/install-skill-from-path.js";
|
|
36
|
+
import { handleInstallSkillFromUpload } from "./methods/install-skill-from-upload.js";
|
|
35
37
|
import { setBackendBaseUrl } from "./backend-url.js";
|
|
36
38
|
import { ensureDesktopConfigInitialized } from "../core/config/desktop-config.js";
|
|
37
39
|
import { createNestAppEmbedded } from "../server/bootstrap.js";
|
|
38
40
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
39
41
|
const PACKAGE_ROOT = join(__dirname, "..", "..");
|
|
40
|
-
|
|
42
|
+
/** 内嵌到 Electron 时由主进程设置 OPENBOT_STATIC_DIR,指向打包后的 renderer/dist */
|
|
43
|
+
const STATIC_DIR = process.env.OPENBOT_STATIC_DIR || join(PACKAGE_ROOT, "apps", "desktop", "renderer", "dist");
|
|
41
44
|
const MIME_TYPES = {
|
|
42
45
|
".html": "text/html",
|
|
43
46
|
".js": "text/javascript",
|
|
@@ -65,6 +68,10 @@ export async function startGatewayServer(port = 38080) {
|
|
|
65
68
|
gatewayExpress.post(PATHS.RUN_SCHEDULED_TASK, async (req, res) => {
|
|
66
69
|
await handleRunScheduledTask(req, res);
|
|
67
70
|
});
|
|
71
|
+
const uploadZip = multer({
|
|
72
|
+
storage: multer.memoryStorage(),
|
|
73
|
+
limits: { fileSize: 10 * 1024 * 1024 },
|
|
74
|
+
});
|
|
68
75
|
gatewayExpress.post(`${PATHS.SERVER_API}/skills/install-from-path`, async (req, res) => {
|
|
69
76
|
const body = await new Promise((resolve, reject) => {
|
|
70
77
|
const chunks = [];
|
|
@@ -87,6 +94,24 @@ export async function startGatewayServer(port = 38080) {
|
|
|
87
94
|
res.status(code).json({ success: false, message });
|
|
88
95
|
}
|
|
89
96
|
});
|
|
97
|
+
gatewayExpress.post(`${PATHS.SERVER_API}/skills/install-from-upload`, authHookServerApi, uploadZip.single("file"), async (req, res) => {
|
|
98
|
+
try {
|
|
99
|
+
const file = req.file;
|
|
100
|
+
const buffer = file?.buffer;
|
|
101
|
+
if (!buffer || !Buffer.isBuffer(buffer)) {
|
|
102
|
+
return res.status(400).json({ success: false, message: "请上传 zip 文件" });
|
|
103
|
+
}
|
|
104
|
+
const scope = req.body?.scope === "workspace" ? "workspace" : "global";
|
|
105
|
+
const workspace = req.body?.workspace ?? "default";
|
|
106
|
+
const result = await handleInstallSkillFromUpload({ buffer, scope, workspace });
|
|
107
|
+
res.status(200).json(result);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
const code = message.includes("请上传") || message.includes("SKILL.md") || message.includes("目录") || message.includes("10MB") ? 400 : 500;
|
|
112
|
+
res.status(code).json({ success: false, message });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
90
115
|
gatewayExpress.use(PATHS.SERVER_API, authHookServerApi, nestExpress);
|
|
91
116
|
gatewayExpress.use(PATHS.CHANNEL, authHookChannel, (req, res) => handleChannel(req, res));
|
|
92
117
|
gatewayExpress.use(PATHS.SSE, authHookSse, (req, res) => handleSse(req, res));
|
|
@@ -20,7 +20,7 @@ export declare class AgentConfigController {
|
|
|
20
20
|
success: boolean;
|
|
21
21
|
data: AgentConfigItem;
|
|
22
22
|
}>;
|
|
23
|
-
updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode'>>): Promise<{
|
|
23
|
+
updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers'>>): Promise<{
|
|
24
24
|
success: boolean;
|
|
25
25
|
data: AgentConfigItem;
|
|
26
26
|
}>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { McpServerConfig } from '../../core/mcp/index.js';
|
|
1
2
|
/** 缺省智能体 ID / 工作空间名,不可删除;对应目录 ~/.openbot/workspace/default */
|
|
2
3
|
export declare const DEFAULT_AGENT_ID = "default";
|
|
3
4
|
/**
|
|
@@ -14,6 +15,8 @@ export interface AgentConfigItem {
|
|
|
14
15
|
modelItemCode?: string;
|
|
15
16
|
/** 是否为系统缺省智能体(主智能体),不可删除 */
|
|
16
17
|
isDefault?: boolean;
|
|
18
|
+
/** MCP 服务器配置列表,创建 Session 时传入(与 Skill 类似) */
|
|
19
|
+
mcpServers?: McpServerConfig[];
|
|
17
20
|
}
|
|
18
21
|
export declare class AgentConfigService {
|
|
19
22
|
private configDir;
|
|
@@ -31,7 +34,7 @@ export declare class AgentConfigService {
|
|
|
31
34
|
name: string;
|
|
32
35
|
workspace: string;
|
|
33
36
|
}): Promise<AgentConfigItem>;
|
|
34
|
-
updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode'>>): Promise<AgentConfigItem>;
|
|
37
|
+
updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers'>>): Promise<AgentConfigItem>;
|
|
35
38
|
deleteAgent(id: string): Promise<void>;
|
|
36
39
|
/**
|
|
37
40
|
* 根据 config 的 defaultProvider / defaultModel / defaultModelItemCode 及 configuredModels 同步 agents.json 中缺省智能体的 provider、model、modelItemCode。
|
|
@@ -150,6 +150,8 @@ let AgentConfigService = class AgentConfigService {
|
|
|
150
150
|
agent.model = updates.model;
|
|
151
151
|
if (updates.modelItemCode !== undefined)
|
|
152
152
|
agent.modelItemCode = updates.modelItemCode;
|
|
153
|
+
if (updates.mcpServers !== undefined)
|
|
154
|
+
agent.mcpServers = updates.mcpServers;
|
|
153
155
|
await this.writeAgentsFile(file);
|
|
154
156
|
return { ...agent, isDefault: agent.id === DEFAULT_AGENT_ID };
|
|
155
157
|
}
|
|
@@ -10,7 +10,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
|
|
|
10
10
|
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
11
11
|
return function (target, key) { decorator(target, key, paramIndex); }
|
|
12
12
|
};
|
|
13
|
-
import { Controller, Get, Post, Delete, Body, Param, HttpException, HttpStatus, } from '@nestjs/common';
|
|
13
|
+
import { Controller, Get, Post, Delete, Body, Param, Header, HttpException, HttpStatus, } from '@nestjs/common';
|
|
14
14
|
import { AgentsService } from './agents.service.js';
|
|
15
15
|
let AgentsController = class AgentsController {
|
|
16
16
|
agentsService;
|
|
@@ -47,10 +47,7 @@ let AgentsController = class AgentsController {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
async deleteSession(id) {
|
|
50
|
-
|
|
51
|
-
if (!deleted) {
|
|
52
|
-
throw new HttpException('Session not found', HttpStatus.NOT_FOUND);
|
|
53
|
-
}
|
|
50
|
+
await this.agentsService.deleteSession(id);
|
|
54
51
|
return {
|
|
55
52
|
success: true,
|
|
56
53
|
message: 'Session deleted',
|
|
@@ -80,6 +77,7 @@ __decorate([
|
|
|
80
77
|
], AgentsController.prototype, "createSession", null);
|
|
81
78
|
__decorate([
|
|
82
79
|
Get('sessions'),
|
|
80
|
+
Header('Cache-Control', 'no-store'),
|
|
83
81
|
__metadata("design:type", Function),
|
|
84
82
|
__metadata("design:paramtypes", []),
|
|
85
83
|
__metadata("design:returntype", void 0)
|
|
@@ -53,7 +53,7 @@ export declare class AgentsService {
|
|
|
53
53
|
}): Promise<AgentSession>;
|
|
54
54
|
getSessions(): AgentSession[];
|
|
55
55
|
getSession(sessionId: string): AgentSession | undefined;
|
|
56
|
-
deleteSession(sessionId: string): Promise<
|
|
56
|
+
deleteSession(sessionId: string): Promise<void>;
|
|
57
57
|
getMessageHistory(sessionId: string): ChatMessage[];
|
|
58
58
|
addAssistantMessage(sessionId: string, content: string): void;
|
|
59
59
|
appendMessage(sessionId: string, role: 'user' | 'assistant', content: string, options?: {
|
|
@@ -135,9 +135,11 @@ let AgentsService = class AgentsService {
|
|
|
135
135
|
return r ? this.rowToSession(r) : undefined;
|
|
136
136
|
}
|
|
137
137
|
async deleteSession(sessionId) {
|
|
138
|
-
agentManager.deleteSession(sessionId);
|
|
139
138
|
const result = this.db.run('DELETE FROM sessions WHERE id = ?', [sessionId]);
|
|
140
|
-
|
|
139
|
+
if (result.changes > 0) {
|
|
140
|
+
this.db.persist();
|
|
141
|
+
}
|
|
142
|
+
agentManager.deleteSession(sessionId);
|
|
141
143
|
}
|
|
142
144
|
getMessageHistory(sessionId) {
|
|
143
145
|
const rows = this.db.all('SELECT * FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC', [sessionId]);
|