@next-open-ai/openbot 0.2.8 → 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.
Files changed (64) hide show
  1. package/README.md +12 -1
  2. package/apps/desktop/renderer/dist/assets/index-DKtaRFW4.js +89 -0
  3. package/apps/desktop/renderer/dist/assets/index-QHuqXpWQ.css +10 -0
  4. package/apps/desktop/renderer/dist/index.html +2 -2
  5. package/dist/core/agent/agent-manager.d.ts +3 -0
  6. package/dist/core/agent/agent-manager.js +12 -6
  7. package/dist/core/config/desktop-config.d.ts +4 -0
  8. package/dist/core/config/desktop-config.js +5 -1
  9. package/dist/core/installer/index.d.ts +1 -1
  10. package/dist/core/installer/index.js +1 -1
  11. package/dist/core/installer/skill-installer.d.ts +9 -0
  12. package/dist/core/installer/skill-installer.js +94 -0
  13. package/dist/core/mcp/adapter.d.ts +17 -0
  14. package/dist/core/mcp/adapter.js +49 -0
  15. package/dist/core/mcp/client.d.ts +24 -0
  16. package/dist/core/mcp/client.js +70 -0
  17. package/dist/core/mcp/config.d.ts +22 -0
  18. package/dist/core/mcp/config.js +69 -0
  19. package/dist/core/mcp/index.d.ts +18 -0
  20. package/dist/core/mcp/index.js +20 -0
  21. package/dist/core/mcp/operator.d.ts +15 -0
  22. package/dist/core/mcp/operator.js +72 -0
  23. package/dist/core/mcp/transport/index.d.ts +11 -0
  24. package/dist/core/mcp/transport/index.js +16 -0
  25. package/dist/core/mcp/transport/sse.d.ts +20 -0
  26. package/dist/core/mcp/transport/sse.js +82 -0
  27. package/dist/core/mcp/transport/stdio.d.ts +32 -0
  28. package/dist/core/mcp/transport/stdio.js +132 -0
  29. package/dist/core/mcp/types.d.ts +72 -0
  30. package/dist/core/mcp/types.js +5 -0
  31. package/dist/core/tools/bookmark-tool.d.ts +9 -0
  32. package/dist/core/tools/bookmark-tool.js +118 -0
  33. package/dist/core/tools/index.d.ts +1 -0
  34. package/dist/core/tools/index.js +1 -0
  35. package/dist/gateway/methods/agent-chat.js +1 -0
  36. package/dist/gateway/methods/install-skill-from-upload.d.ts +14 -0
  37. package/dist/gateway/methods/install-skill-from-upload.js +13 -0
  38. package/dist/gateway/methods/run-scheduled-task.js +1 -0
  39. package/dist/gateway/server.js +24 -0
  40. package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
  41. package/dist/server/agent-config/agent-config.service.d.ts +4 -1
  42. package/dist/server/agent-config/agent-config.service.js +2 -0
  43. package/dist/server/agents/agents.controller.js +3 -5
  44. package/dist/server/agents/agents.service.d.ts +1 -1
  45. package/dist/server/agents/agents.service.js +4 -2
  46. package/dist/server/app.module.js +2 -0
  47. package/dist/server/database/database.service.d.ts +7 -0
  48. package/dist/server/database/database.service.js +54 -5
  49. package/dist/server/saved-items/saved-items.controller.d.ts +26 -0
  50. package/dist/server/saved-items/saved-items.controller.js +78 -0
  51. package/dist/server/saved-items/saved-items.module.d.ts +2 -0
  52. package/dist/server/saved-items/saved-items.module.js +23 -0
  53. package/dist/server/saved-items/saved-items.service.d.ts +31 -0
  54. package/dist/server/saved-items/saved-items.service.js +105 -0
  55. package/dist/server/saved-items/tags.controller.d.ts +30 -0
  56. package/dist/server/saved-items/tags.controller.js +85 -0
  57. package/dist/server/saved-items/tags.service.d.ts +24 -0
  58. package/dist/server/saved-items/tags.service.js +84 -0
  59. package/dist/server/skills/skills.service.d.ts +2 -0
  60. package/dist/server/skills/skills.service.js +80 -16
  61. package/package.json +5 -1
  62. package/skills/url-bookmark/SKILL.md +36 -0
  63. package/apps/desktop/renderer/dist/assets/index-BOS-F8a4.js +0 -89
  64. package/apps/desktop/renderer/dist/assets/index-DxqxayUL.css +0 -10
@@ -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,5 @@
1
+ /**
2
+ * MCP (Model Context Protocol) 相关类型定义。
3
+ * 仅实现 Client 端 Tools 能力;Resources/Prompts 类型预留扩展。
4
+ */
5
+ export {};
@@ -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";
@@ -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";
@@ -64,6 +64,7 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
64
64
  apiKey,
65
65
  maxSessions: maxAgentSessions,
66
66
  targetAgentId: effectiveTargetAgentId,
67
+ mcpServers: agentConfig?.mcpServers,
67
68
  });
68
69
  }
69
70
  catch (err) {
@@ -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
+ }
@@ -55,6 +55,7 @@ export async function handleRunScheduledTask(req, res) {
55
55
  provider,
56
56
  modelId,
57
57
  apiKey,
58
+ mcpServers: agentConfig?.mcpServers,
58
59
  });
59
60
  let assistantContent = "";
60
61
  let turnPromptTokens = 0;
@@ -31,7 +31,9 @@ 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";
@@ -66,6 +68,10 @@ export async function startGatewayServer(port = 38080) {
66
68
  gatewayExpress.post(PATHS.RUN_SCHEDULED_TASK, async (req, res) => {
67
69
  await handleRunScheduledTask(req, res);
68
70
  });
71
+ const uploadZip = multer({
72
+ storage: multer.memoryStorage(),
73
+ limits: { fileSize: 10 * 1024 * 1024 },
74
+ });
69
75
  gatewayExpress.post(`${PATHS.SERVER_API}/skills/install-from-path`, async (req, res) => {
70
76
  const body = await new Promise((resolve, reject) => {
71
77
  const chunks = [];
@@ -88,6 +94,24 @@ export async function startGatewayServer(port = 38080) {
88
94
  res.status(code).json({ success: false, message });
89
95
  }
90
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
+ });
91
115
  gatewayExpress.use(PATHS.SERVER_API, authHookServerApi, nestExpress);
92
116
  gatewayExpress.use(PATHS.CHANNEL, authHookChannel, (req, res) => handleChannel(req, res));
93
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
- const deleted = await this.agentsService.deleteSession(id);
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<boolean>;
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
- return result.changes > 0;
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]);
@@ -15,6 +15,7 @@ import { UsersModule } from './users/users.module.js';
15
15
  import { WorkspaceModule } from './workspace/workspace.module.js';
16
16
  import { TasksModule } from './tasks/tasks.module.js';
17
17
  import { UsageModule } from './usage/usage.module.js';
18
+ import { SavedItemsModule } from './saved-items/saved-items.module.js';
18
19
  let AppModule = class AppModule {
19
20
  };
20
21
  AppModule = __decorate([
@@ -30,6 +31,7 @@ AppModule = __decorate([
30
31
  WorkspaceModule,
31
32
  TasksModule,
32
33
  UsageModule,
34
+ SavedItemsModule,
33
35
  ],
34
36
  })
35
37
  ], AppModule);
@@ -13,6 +13,13 @@ export declare class DatabaseService implements OnModuleInit, OnModuleDestroy {
13
13
  private getDb;
14
14
  private runMigrations;
15
15
  run(sql: string, params?: unknown[]): RunResult;
16
+ /**
17
+ * 使用文件 DB 时立即落盘。每次 run() 写入后都会调用,保证「每次保存马上写到磁盘」。
18
+ * 落盘失败时重新抛出,便于上层返回错误、前端不乐观更新,避免「删了但重启又出现」。
19
+ */
20
+ private persistIfFile;
21
+ /** 供删除等关键操作后显式落盘,确保删除结果持久化 */
22
+ persist(): void;
16
23
  get<T>(sql: string, params?: unknown[]): T | undefined;
17
24
  all<T>(sql: string, params?: unknown[]): T[];
18
25
  onModuleDestroy(): void;
@@ -6,7 +6,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  import { Injectable } from '@nestjs/common';
8
8
  import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
9
- import { join } from 'path';
9
+ import { join, resolve } from 'path';
10
10
  import { homedir } from 'os';
11
11
  let DatabaseService = class DatabaseService {
12
12
  sqlDb = null;
@@ -25,7 +25,8 @@ let DatabaseService = class DatabaseService {
25
25
  ? ':memory:'
26
26
  : pathEnv ?? join(process.env.OPENBOT_DB_DIR ?? defaultDir, 'openbot.db');
27
27
  if (path !== ':memory:') {
28
- const dir = path.endsWith('.db') ? join(path, '..') : path;
28
+ const resolvedPath = resolve(path);
29
+ const dir = resolvedPath.endsWith('.db') ? join(resolvedPath, '..') : resolvedPath;
29
30
  if (!existsSync(dir)) {
30
31
  mkdirSync(dir, { recursive: true });
31
32
  }
@@ -38,18 +39,23 @@ let DatabaseService = class DatabaseService {
38
39
  this.dbPath = ':memory:';
39
40
  }
40
41
  else {
41
- if (existsSync(path)) {
42
- const buf = readFileSync(path);
42
+ const absolutePath = resolve(path);
43
+ if (existsSync(absolutePath)) {
44
+ const buf = readFileSync(absolutePath);
43
45
  db = new SQL.Database(new Uint8Array(buf));
44
46
  }
45
47
  else {
46
48
  db = new SQL.Database();
47
49
  }
48
- this.dbPath = path;
50
+ this.dbPath = absolutePath;
49
51
  }
50
52
  this.sqlDb = db;
51
53
  db.run('PRAGMA foreign_keys = ON;');
52
54
  this.runMigrations();
55
+ if (path !== ':memory:') {
56
+ this.persistIfFile();
57
+ console.log('[DatabaseService] Database file:', this.dbPath);
58
+ }
53
59
  }
54
60
  getDb() {
55
61
  if (!this.sqlDb) {
@@ -127,6 +133,28 @@ let DatabaseService = class DatabaseService {
127
133
  );
128
134
  CREATE INDEX IF NOT EXISTS idx_token_usage_session_id ON token_usage(session_id);
129
135
  CREATE INDEX IF NOT EXISTS idx_token_usage_created_at ON token_usage(created_at);
136
+
137
+ CREATE TABLE IF NOT EXISTS tags (
138
+ id TEXT PRIMARY KEY,
139
+ name TEXT UNIQUE NOT NULL,
140
+ sort_order INTEGER NOT NULL DEFAULT 0,
141
+ created_at INTEGER NOT NULL
142
+ );
143
+ CREATE TABLE IF NOT EXISTS saved_items (
144
+ id TEXT PRIMARY KEY,
145
+ url TEXT NOT NULL,
146
+ title TEXT,
147
+ workspace TEXT NOT NULL DEFAULT 'default',
148
+ created_at INTEGER NOT NULL
149
+ );
150
+ CREATE TABLE IF NOT EXISTS saved_item_tags (
151
+ saved_item_id TEXT NOT NULL,
152
+ tag_id TEXT NOT NULL,
153
+ PRIMARY KEY (saved_item_id, tag_id),
154
+ FOREIGN KEY (saved_item_id) REFERENCES saved_items(id) ON DELETE CASCADE,
155
+ FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
156
+ );
157
+ CREATE INDEX IF NOT EXISTS idx_saved_item_tags_tag_id ON saved_item_tags(tag_id);
130
158
  `;
131
159
  const statements = ddl.split(';').map((s) => s.trim()).filter(Boolean);
132
160
  for (const stmt of statements) {
@@ -148,11 +176,32 @@ let DatabaseService = class DatabaseService {
148
176
  run(sql, params = []) {
149
177
  const db = this.getDb();
150
178
  db.run(sql, params);
179
+ this.persistIfFile();
151
180
  const rows = db.exec('SELECT changes() AS c, last_insert_rowid() AS id');
152
181
  const c = rows[0]?.values?.[0]?.[0] ?? 0;
153
182
  const id = rows[0]?.values?.[0]?.[1] ?? 0;
154
183
  return { changes: Number(c), lastInsertRowid: Number(id) };
155
184
  }
185
+ /**
186
+ * 使用文件 DB 时立即落盘。每次 run() 写入后都会调用,保证「每次保存马上写到磁盘」。
187
+ * 落盘失败时重新抛出,便于上层返回错误、前端不乐观更新,避免「删了但重启又出现」。
188
+ */
189
+ persistIfFile() {
190
+ if (!this.sqlDb || !this.dbPath || this.dbPath === ':memory:')
191
+ return;
192
+ try {
193
+ const data = this.sqlDb.export();
194
+ writeFileSync(this.dbPath, Buffer.from(data));
195
+ }
196
+ catch (e) {
197
+ console.error('[DatabaseService] Failed to persist database:', e);
198
+ throw e;
199
+ }
200
+ }
201
+ /** 供删除等关键操作后显式落盘,确保删除结果持久化 */
202
+ persist() {
203
+ this.persistIfFile();
204
+ }
156
205
  get(sql, params = []) {
157
206
  const db = this.getDb();
158
207
  const stmt = db.prepare(sql);
@@ -0,0 +1,26 @@
1
+ import { SavedItemsService } from './saved-items.service.js';
2
+ export declare class SavedItemsController {
3
+ private readonly savedItemsService;
4
+ constructor(savedItemsService: SavedItemsService);
5
+ list(tagId?: string, workspace?: string): Promise<{
6
+ success: boolean;
7
+ data: import("./saved-items.service.js").SavedItem[];
8
+ }>;
9
+ get(id: string): Promise<{
10
+ success: boolean;
11
+ data: import("./saved-items.service.js").SavedItem;
12
+ }>;
13
+ create(body: {
14
+ url: string;
15
+ title?: string;
16
+ workspace?: string;
17
+ tagNames?: string[];
18
+ tagIds?: string[];
19
+ }): Promise<{
20
+ success: boolean;
21
+ data: import("./saved-items.service.js").SavedItem;
22
+ }>;
23
+ delete(id: string): Promise<{
24
+ success: boolean;
25
+ }>;
26
+ }
@@ -0,0 +1,78 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { Controller, Get, Post, Delete, Body, Param, Query, HttpException, HttpStatus, } from '@nestjs/common';
14
+ import { SavedItemsService } from './saved-items.service.js';
15
+ let SavedItemsController = class SavedItemsController {
16
+ savedItemsService;
17
+ constructor(savedItemsService) {
18
+ this.savedItemsService = savedItemsService;
19
+ }
20
+ async list(tagId, workspace) {
21
+ const data = await this.savedItemsService.findAll({ tagId, workspace });
22
+ return { success: true, data };
23
+ }
24
+ async get(id) {
25
+ const data = await this.savedItemsService.findById(id);
26
+ if (!data)
27
+ throw new HttpException('Saved item not found', HttpStatus.NOT_FOUND);
28
+ return { success: true, data };
29
+ }
30
+ async create(body) {
31
+ const data = await this.savedItemsService.create({
32
+ url: body.url,
33
+ title: body.title,
34
+ workspace: body.workspace,
35
+ tagNames: body.tagNames,
36
+ tagIds: body.tagIds,
37
+ });
38
+ return { success: true, data };
39
+ }
40
+ async delete(id) {
41
+ await this.savedItemsService.delete(id);
42
+ return { success: true };
43
+ }
44
+ };
45
+ __decorate([
46
+ Get(),
47
+ __param(0, Query('tagId')),
48
+ __param(1, Query('workspace')),
49
+ __metadata("design:type", Function),
50
+ __metadata("design:paramtypes", [String, String]),
51
+ __metadata("design:returntype", Promise)
52
+ ], SavedItemsController.prototype, "list", null);
53
+ __decorate([
54
+ Get(':id'),
55
+ __param(0, Param('id')),
56
+ __metadata("design:type", Function),
57
+ __metadata("design:paramtypes", [String]),
58
+ __metadata("design:returntype", Promise)
59
+ ], SavedItemsController.prototype, "get", null);
60
+ __decorate([
61
+ Post(),
62
+ __param(0, Body()),
63
+ __metadata("design:type", Function),
64
+ __metadata("design:paramtypes", [Object]),
65
+ __metadata("design:returntype", Promise)
66
+ ], SavedItemsController.prototype, "create", null);
67
+ __decorate([
68
+ Delete(':id'),
69
+ __param(0, Param('id')),
70
+ __metadata("design:type", Function),
71
+ __metadata("design:paramtypes", [String]),
72
+ __metadata("design:returntype", Promise)
73
+ ], SavedItemsController.prototype, "delete", null);
74
+ SavedItemsController = __decorate([
75
+ Controller('saved-items'),
76
+ __metadata("design:paramtypes", [SavedItemsService])
77
+ ], SavedItemsController);
78
+ export { SavedItemsController };
@@ -0,0 +1,2 @@
1
+ export declare class SavedItemsModule {
2
+ }