@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,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
+ }
@@ -49,12 +49,16 @@ export async function handleRunScheduledTask(req, res) {
49
49
  if (agentConfig.apiKey)
50
50
  apiKey = agentConfig.apiKey;
51
51
  }
52
+ const COMPOSITE_KEY_SEP = "::";
52
53
  try {
53
54
  const session = await agentManager.getOrCreateSession(sessionId, {
55
+ agentId: sessionAgentId,
54
56
  workspace: resolvedWorkspace,
55
57
  provider,
56
58
  modelId,
57
59
  apiKey,
60
+ mcpServers: agentConfig?.mcpServers,
61
+ systemPrompt: agentConfig?.systemPrompt,
58
62
  });
59
63
  let assistantContent = "";
60
64
  let turnPromptTokens = 0;
@@ -120,7 +124,6 @@ export async function handleRunScheduledTask(req, res) {
120
124
  res.end(JSON.stringify({ success: false, error: friendlyError }));
121
125
  }
122
126
  finally {
123
- // 执行完成(成功或失败)后立即关闭并移除该 AgentSession,避免空悬占用内存/连接等资源
124
- agentManager.deleteSession(sessionId);
127
+ agentManager.deleteSession(sessionId + COMPOSITE_KEY_SEP + sessionAgentId);
125
128
  }
126
129
  }
@@ -31,10 +31,18 @@ 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
- import { ensureDesktopConfigInitialized } from "../core/config/desktop-config.js";
38
+ import { ensureDesktopConfigInitialized, getChannelsConfigSync } from "../core/config/desktop-config.js";
37
39
  import { createNestAppEmbedded } from "../server/bootstrap.js";
40
+ import { registerChannel, startAllChannels, stopAllChannels } from "./channel/registry.js";
41
+ import { createFeishuChannel } from "./channel/adapters/feishu.js";
42
+ import { setChannelSessionPersistence } from "./channel/session-persistence.js";
43
+ import { setSessionCurrentAgentResolver, setSessionCurrentAgentUpdater, setAgentListProvider, setCreateAgentProvider, } from "../core/session-current-agent.js";
44
+ import { AgentsService } from "../server/agents/agents.service.js";
45
+ import { AgentConfigService } from "../server/agent-config/agent-config.service.js";
38
46
  const __dirname = dirname(fileURLToPath(import.meta.url));
39
47
  const PACKAGE_ROOT = join(__dirname, "..", "..");
40
48
  /** 内嵌到 Electron 时由主进程设置 OPENBOT_STATIC_DIR,指向打包后的 renderer/dist */
@@ -59,6 +67,27 @@ export async function startGatewayServer(port = 38080) {
59
67
  console.log(`Starting gateway server on port ${port}...`);
60
68
  setBackendBaseUrl(`http://localhost:${port}`);
61
69
  const { app: nestApp, express: nestExpress } = await createNestAppEmbedded();
70
+ try {
71
+ const agentsService = nestApp.get(AgentsService);
72
+ setChannelSessionPersistence(agentsService);
73
+ setSessionCurrentAgentResolver((sessionId) => agentsService.getSession(sessionId)?.agentId);
74
+ setSessionCurrentAgentUpdater((sessionId, agentId) => agentsService.updateSessionAgentId(sessionId, agentId));
75
+ const agentConfigService = nestApp.get(AgentConfigService);
76
+ setAgentListProvider(() => agentConfigService.listAgents().then((agents) => agents.map((a) => ({ id: a.id, name: a.name }))));
77
+ setCreateAgentProvider(async (params) => {
78
+ try {
79
+ const agent = await agentConfigService.createAgent(params);
80
+ return { id: agent.id, name: agent.name };
81
+ }
82
+ catch (e) {
83
+ const msg = e?.message ?? e?.response?.message ?? String(e);
84
+ return { error: msg };
85
+ }
86
+ });
87
+ }
88
+ catch (e) {
89
+ console.warn("[Gateway] Channel session persistence / session-agent bridge unavailable:", e);
90
+ }
62
91
  const gatewayExpress = express();
63
92
  gatewayExpress.get(PATHS.HEALTH, (_req, res) => {
64
93
  res.status(200).json({ status: "ok", timestamp: Date.now() });
@@ -66,6 +95,10 @@ export async function startGatewayServer(port = 38080) {
66
95
  gatewayExpress.post(PATHS.RUN_SCHEDULED_TASK, async (req, res) => {
67
96
  await handleRunScheduledTask(req, res);
68
97
  });
98
+ const uploadZip = multer({
99
+ storage: multer.memoryStorage(),
100
+ limits: { fileSize: 10 * 1024 * 1024 },
101
+ });
69
102
  gatewayExpress.post(`${PATHS.SERVER_API}/skills/install-from-path`, async (req, res) => {
70
103
  const body = await new Promise((resolve, reject) => {
71
104
  const chunks = [];
@@ -88,6 +121,24 @@ export async function startGatewayServer(port = 38080) {
88
121
  res.status(code).json({ success: false, message });
89
122
  }
90
123
  });
124
+ gatewayExpress.post(`${PATHS.SERVER_API}/skills/install-from-upload`, authHookServerApi, uploadZip.single("file"), async (req, res) => {
125
+ try {
126
+ const file = req.file;
127
+ const buffer = file?.buffer;
128
+ if (!buffer || !Buffer.isBuffer(buffer)) {
129
+ return res.status(400).json({ success: false, message: "请上传 zip 文件" });
130
+ }
131
+ const scope = req.body?.scope === "workspace" ? "workspace" : "global";
132
+ const workspace = req.body?.workspace ?? "default";
133
+ const result = await handleInstallSkillFromUpload({ buffer, scope, workspace });
134
+ res.status(200).json(result);
135
+ }
136
+ catch (err) {
137
+ const message = err instanceof Error ? err.message : String(err);
138
+ const code = message.includes("请上传") || message.includes("SKILL.md") || message.includes("目录") || message.includes("10MB") ? 400 : 500;
139
+ res.status(code).json({ success: false, message });
140
+ }
141
+ });
91
142
  gatewayExpress.use(PATHS.SERVER_API, authHookServerApi, nestExpress);
92
143
  gatewayExpress.use(PATHS.CHANNEL, authHookChannel, (req, res) => handleChannel(req, res));
93
144
  gatewayExpress.use(PATHS.SSE, authHookSse, (req, res) => handleSse(req, res));
@@ -154,8 +205,30 @@ export async function startGatewayServer(port = 38080) {
154
205
  resolve(p);
155
206
  });
156
207
  });
208
+ // 通道:根据配置注册并启动(如飞书 WebSocket)
209
+ const channelsConfig = getChannelsConfigSync();
210
+ const feishuCfg = channelsConfig.feishu;
211
+ if (feishuCfg?.enabled && feishuCfg.appId?.trim() && feishuCfg.appSecret?.trim()) {
212
+ try {
213
+ console.log("[Channel] Starting Feishu (WebSocket)...");
214
+ const feishuChannel = createFeishuChannel({
215
+ appId: feishuCfg.appId.trim(),
216
+ appSecret: feishuCfg.appSecret.trim(),
217
+ defaultAgentId: feishuCfg.defaultAgentId?.trim() || "default",
218
+ });
219
+ registerChannel(feishuChannel);
220
+ await startAllChannels();
221
+ }
222
+ catch (e) {
223
+ console.warn("Feishu channel start failed:", e);
224
+ }
225
+ }
226
+ else if (feishuCfg?.enabled) {
227
+ console.warn("[Channel] Feishu is enabled but appId or appSecret is missing; skip start. Check Settings → Channels.");
228
+ }
157
229
  const close = async () => {
158
230
  console.log("Closing gateway server...");
231
+ await stopAllChannels();
159
232
  await nestApp.close();
160
233
  wss.clients.forEach((c) => c.close());
161
234
  await new Promise((r) => wss.close(() => r()));
@@ -16,11 +16,16 @@ export declare class AgentConfigController {
16
16
  createAgent(body: {
17
17
  name: string;
18
18
  workspace: string;
19
+ provider?: string;
20
+ model?: string;
21
+ modelItemCode?: string;
22
+ systemPrompt?: string;
23
+ icon?: string;
19
24
  }): Promise<{
20
25
  success: boolean;
21
26
  data: AgentConfigItem;
22
27
  }>;
23
- updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode'>>): Promise<{
28
+ updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'systemPrompt' | 'icon'>>): Promise<{
24
29
  success: boolean;
25
30
  data: AgentConfigItem;
26
31
  }>;
@@ -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,12 @@ export interface AgentConfigItem {
14
15
  modelItemCode?: string;
15
16
  /** 是否为系统缺省智能体(主智能体),不可删除 */
16
17
  isDefault?: boolean;
18
+ /** MCP 服务器配置列表,创建 Session 时传入(与 Skill 类似) */
19
+ mcpServers?: McpServerConfig[];
20
+ /** 自定义系统提示词,会与技能等一起组成最终 systemPrompt */
21
+ systemPrompt?: string;
22
+ /** 智能体图标标识(前端预设图标 id,如 default、star、code 等) */
23
+ icon?: string;
17
24
  }
18
25
  export declare class AgentConfigService {
19
26
  private configDir;
@@ -30,8 +37,15 @@ export declare class AgentConfigService {
30
37
  createAgent(params: {
31
38
  name: string;
32
39
  workspace: string;
40
+ provider?: string;
41
+ model?: string;
42
+ modelItemCode?: string;
43
+ /** 自定义系统提示词;可由用户描述生成,创建后可在详情页修改 */
44
+ systemPrompt?: string;
45
+ /** 智能体图标标识 */
46
+ icon?: string;
33
47
  }): Promise<AgentConfigItem>;
34
- updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode'>>): Promise<AgentConfigItem>;
48
+ updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'systemPrompt' | 'icon'>>): Promise<AgentConfigItem>;
35
49
  deleteAgent(id: string): Promise<void>;
36
50
  /**
37
51
  * 根据 config 的 defaultProvider / defaultModel / defaultModelItemCode 及 configuredModels 同步 agents.json 中缺省智能体的 provider、model、modelItemCode。
@@ -93,7 +93,7 @@ let AgentConfigService = class AgentConfigService {
93
93
  return null;
94
94
  }
95
95
  async createAgent(params) {
96
- const { name, workspace } = params;
96
+ const { name, workspace, provider, model, modelItemCode, systemPrompt, icon } = params;
97
97
  if (workspace === DEFAULT_AGENT_ID) {
98
98
  throw new BadRequestException('工作空间名 default 为系统保留(主智能体),请使用其他名称');
99
99
  }
@@ -118,8 +118,11 @@ let AgentConfigService = class AgentConfigService {
118
118
  id: workspace,
119
119
  name: trimmedName,
120
120
  workspace,
121
- provider: undefined,
122
- model: undefined,
121
+ provider: provider?.trim() || undefined,
122
+ model: model?.trim() || undefined,
123
+ modelItemCode: modelItemCode?.trim() || undefined,
124
+ systemPrompt: systemPrompt?.trim() || undefined,
125
+ icon: icon?.trim() || undefined,
123
126
  };
124
127
  file.agents.push(agent);
125
128
  await this.writeAgentsFile(file);
@@ -150,6 +153,12 @@ let AgentConfigService = class AgentConfigService {
150
153
  agent.model = updates.model;
151
154
  if (updates.modelItemCode !== undefined)
152
155
  agent.modelItemCode = updates.modelItemCode;
156
+ if (updates.mcpServers !== undefined)
157
+ agent.mcpServers = updates.mcpServers;
158
+ if (updates.systemPrompt !== undefined)
159
+ agent.systemPrompt = updates.systemPrompt?.trim() || undefined;
160
+ if (updates.icon !== undefined)
161
+ agent.icon = updates.icon?.trim() || undefined;
153
162
  await this.writeAgentsFile(file);
154
163
  return { ...agent, isDefault: agent.id === DEFAULT_AGENT_ID };
155
164
  }
@@ -22,6 +22,12 @@ export declare class AgentsController {
22
22
  success: boolean;
23
23
  data: import("./agents.service.js").AgentSession;
24
24
  };
25
+ updateSession(id: string, body: {
26
+ agentId?: string;
27
+ }): {
28
+ success: boolean;
29
+ data: import("./agents.service.js").AgentSession | undefined;
30
+ };
25
31
  deleteSession(id: string): Promise<{
26
32
  success: boolean;
27
33
  message: string;
@@ -30,6 +36,10 @@ export declare class AgentsController {
30
36
  success: boolean;
31
37
  data: import("./agents.service.js").ChatMessage[];
32
38
  };
39
+ clearHistory(id: string): {
40
+ success: boolean;
41
+ message: string;
42
+ };
33
43
  appendMessage(id: string, body: {
34
44
  role: 'user' | 'assistant';
35
45
  content: string;
@@ -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, Delete, Get, Patch, Post, 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;
@@ -46,11 +46,19 @@ let AgentsController = class AgentsController {
46
46
  data: session,
47
47
  };
48
48
  }
49
- async deleteSession(id) {
50
- const deleted = await this.agentsService.deleteSession(id);
51
- if (!deleted) {
49
+ updateSession(id, body) {
50
+ const session = this.agentsService.getSession(id);
51
+ if (!session) {
52
52
  throw new HttpException('Session not found', HttpStatus.NOT_FOUND);
53
53
  }
54
+ if (body.agentId != null) {
55
+ this.agentsService.updateSessionAgentId(id, String(body.agentId));
56
+ }
57
+ const updated = this.agentsService.getSession(id);
58
+ return { success: true, data: updated };
59
+ }
60
+ async deleteSession(id) {
61
+ await this.agentsService.deleteSession(id);
54
62
  return {
55
63
  success: true,
56
64
  message: 'Session deleted',
@@ -63,6 +71,14 @@ let AgentsController = class AgentsController {
63
71
  data: history,
64
72
  };
65
73
  }
74
+ clearHistory(id) {
75
+ const session = this.agentsService.getSession(id);
76
+ if (!session) {
77
+ throw new HttpException('Session not found', HttpStatus.NOT_FOUND);
78
+ }
79
+ this.agentsService.clearSessionMessages(id);
80
+ return { success: true, message: 'Messages cleared' };
81
+ }
66
82
  appendMessage(id, body) {
67
83
  this.agentsService.appendMessage(id, body.role, body.content, {
68
84
  toolCalls: body.toolCalls,
@@ -80,6 +96,7 @@ __decorate([
80
96
  ], AgentsController.prototype, "createSession", null);
81
97
  __decorate([
82
98
  Get('sessions'),
99
+ Header('Cache-Control', 'no-store'),
83
100
  __metadata("design:type", Function),
84
101
  __metadata("design:paramtypes", []),
85
102
  __metadata("design:returntype", void 0)
@@ -91,6 +108,14 @@ __decorate([
91
108
  __metadata("design:paramtypes", [String]),
92
109
  __metadata("design:returntype", void 0)
93
110
  ], AgentsController.prototype, "getSession", null);
111
+ __decorate([
112
+ Patch('sessions/:id'),
113
+ __param(0, Param('id')),
114
+ __param(1, Body()),
115
+ __metadata("design:type", Function),
116
+ __metadata("design:paramtypes", [String, Object]),
117
+ __metadata("design:returntype", void 0)
118
+ ], AgentsController.prototype, "updateSession", null);
94
119
  __decorate([
95
120
  Delete('sessions/:id'),
96
121
  __param(0, Param('id')),
@@ -105,6 +130,13 @@ __decorate([
105
130
  __metadata("design:paramtypes", [String]),
106
131
  __metadata("design:returntype", void 0)
107
132
  ], AgentsController.prototype, "getHistory", null);
133
+ __decorate([
134
+ Delete('sessions/:id/messages'),
135
+ __param(0, Param('id')),
136
+ __metadata("design:type", Function),
137
+ __metadata("design:paramtypes", [String]),
138
+ __metadata("design:returntype", void 0)
139
+ ], AgentsController.prototype, "clearHistory", null);
108
140
  __decorate([
109
141
  Post('sessions/:id/messages'),
110
142
  __param(0, Param('id')),
@@ -51,14 +51,28 @@ let AgentsGateway = class AgentsGateway {
51
51
  client.emit('agent_tool', data);
52
52
  });
53
53
  subscriptions.set('tool', unsubTool);
54
- // Subscribe to message completion
54
+ // turn_end / agent_end:各通道均可收到,按自身逻辑处理(如 desktop 用 agent_end 开放输入)
55
+ const unsubTurnEnd = this.agentsService.addEventListener('turn_end', (data) => {
56
+ if (data.sessionId === sessionId)
57
+ client.emit('turn_end', data);
58
+ });
59
+ subscriptions.set('turn_end', unsubTurnEnd);
60
+ const unsubAgentEnd = this.agentsService.addEventListener('agent_end', (data) => {
61
+ if (data.sessionId === sessionId)
62
+ client.emit('agent_end', data);
63
+ });
64
+ subscriptions.set('agent_end', unsubAgentEnd);
65
+ // 兼容:message_complete(turn_end 语义)、conversation_end(agent_end 语义)
55
66
  const unsubComplete = this.agentsService.addEventListener('message_complete', (data) => {
56
- // Ensure we only emit for the subscribed session
57
- if (data.sessionId === sessionId) {
67
+ if (data.sessionId === sessionId)
58
68
  client.emit('message_complete', data);
59
- }
60
69
  });
61
70
  subscriptions.set('complete', unsubComplete);
71
+ const unsubConversationEnd = this.agentsService.addEventListener('conversation_end', (data) => {
72
+ if (data.sessionId === sessionId)
73
+ client.emit('conversation_end', data);
74
+ });
75
+ subscriptions.set('conversation_end', unsubConversationEnd);
62
76
  this.sessionSubscriptions.set(client.id, subscriptions);
63
77
  return { success: true };
64
78
  }
@@ -53,11 +53,15 @@ 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
+ /** 更新会话的当前 agent(同一 session 内切换 agent 时调用) */
57
+ updateSessionAgentId(sessionId: string, agentId: string): void;
58
+ deleteSession(sessionId: string): Promise<void>;
57
59
  getMessageHistory(sessionId: string): ChatMessage[];
58
60
  addAssistantMessage(sessionId: string, content: string): void;
59
61
  appendMessage(sessionId: string, role: 'user' | 'assistant', content: string, options?: {
60
62
  toolCalls?: any[];
61
63
  contentParts?: any[];
62
64
  }): void;
65
+ /** 清除会话的所有对话消息(保留会话本身,message_count 置 0) */
66
+ clearSessionMessages(sessionId: string): void;
63
67
  }
@@ -134,10 +134,19 @@ let AgentsService = class AgentsService {
134
134
  const r = this.db.get('SELECT * FROM sessions WHERE id = ?', [sessionId]);
135
135
  return r ? this.rowToSession(r) : undefined;
136
136
  }
137
+ /** 更新会话的当前 agent(同一 session 内切换 agent 时调用) */
138
+ updateSessionAgentId(sessionId, agentId) {
139
+ const session = this.getSession(sessionId);
140
+ if (!session)
141
+ return;
142
+ this.db.run('UPDATE sessions SET agent_id = ? WHERE id = ?', [agentId, sessionId]);
143
+ }
137
144
  async deleteSession(sessionId) {
138
- agentManager.deleteSession(sessionId);
139
145
  const result = this.db.run('DELETE FROM sessions WHERE id = ?', [sessionId]);
140
- return result.changes > 0;
146
+ if (result.changes > 0) {
147
+ this.db.persist();
148
+ }
149
+ agentManager.deleteSessionsByBusinessId(sessionId);
141
150
  }
142
151
  getMessageHistory(sessionId) {
143
152
  const rows = this.db.all('SELECT * FROM chat_messages WHERE session_id = ? ORDER BY timestamp ASC', [sessionId]);
@@ -159,6 +168,15 @@ let AgentsService = class AgentsService {
159
168
  this.db.run(`INSERT INTO chat_messages (id, session_id, role, content, timestamp, tool_calls_json) VALUES (?, ?, ?, ?, ?, ?)`, [id, sessionId, role, content, now, toolCallsJson]);
160
169
  this.db.run('UPDATE sessions SET last_active_at = ?, status = ?, message_count = message_count + 1 WHERE id = ?', [now, role === 'assistant' ? 'idle' : session.status, sessionId]);
161
170
  }
171
+ /** 清除会话的所有对话消息(保留会话本身,message_count 置 0) */
172
+ clearSessionMessages(sessionId) {
173
+ const session = this.getSession(sessionId);
174
+ if (!session)
175
+ return;
176
+ this.db.run('DELETE FROM chat_messages WHERE session_id = ?', [sessionId]);
177
+ this.db.run('UPDATE sessions SET message_count = 0 WHERE id = ?', [sessionId]);
178
+ this.db.persist();
179
+ }
162
180
  };
163
181
  AgentsService = __decorate([
164
182
  Injectable(),
@@ -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);
@@ -26,6 +26,7 @@ export declare class ConfigController {
26
26
  embeddingProvider?: string;
27
27
  embeddingModel?: string;
28
28
  };
29
+ channels?: import("../../core/config/desktop-config.js").ChannelsConfig;
29
30
  };
30
31
  }>;
31
32
  updateConfig(updates: Partial<AppConfig>): Promise<{
@@ -52,6 +53,7 @@ export declare class ConfigController {
52
53
  embeddingProvider?: string;
53
54
  embeddingModel?: string;
54
55
  };
56
+ channels?: import("../../core/config/desktop-config.js").ChannelsConfig;
55
57
  };
56
58
  }>;
57
59
  getProviders(): Promise<{
@@ -1,3 +1,4 @@
1
+ import { type ChannelsConfig } from '../../core/config/desktop-config.js';
1
2
  import { AgentConfigService } from '../agent-config/agent-config.service.js';
2
3
  /** 模型 cost 配置,写入 models.json;缺省均为 0 */
3
4
  export interface ModelCost {
@@ -55,6 +56,8 @@ export interface AppConfig {
55
56
  embeddingProvider?: string;
56
57
  embeddingModel?: string;
57
58
  };
59
+ /** 通道配置:飞书、Telegram 等 token/key */
60
+ channels?: ChannelsConfig;
58
61
  }
59
62
  export declare class ConfigService {
60
63
  private readonly agentConfigService;
@@ -11,6 +11,7 @@ import { Injectable } from '@nestjs/common';
11
11
  import { readFile, writeFile, mkdir } from 'fs/promises';
12
12
  import { join } from 'path';
13
13
  import { existsSync } from 'fs';
14
+ import { homedir } from 'os';
14
15
  import { getProviderSupport, syncDesktopConfigToModelsJson } from '../../core/config/desktop-config.js';
15
16
  import { AgentConfigService } from '../agent-config/agent-config.service.js';
16
17
  let ConfigService = class ConfigService {
@@ -19,7 +20,7 @@ let ConfigService = class ConfigService {
19
20
  config;
20
21
  constructor(agentConfigService) {
21
22
  this.agentConfigService = agentConfigService;
22
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
23
+ const homeDir = process.env.HOME || process.env.USERPROFILE || homedir();
23
24
  const configDir = join(homeDir, '.openbot', 'desktop');
24
25
  this.configPath = join(configDir, 'config.json');
25
26
  // Ensure config directory exists
@@ -40,6 +41,7 @@ let ConfigService = class ConfigService {
40
41
  providers: {},
41
42
  configuredModels: [],
42
43
  rag: undefined,
44
+ channels: {},
43
45
  };
44
46
  }
45
47
  /** 当前缺省智能体 id */
@@ -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);