@leterhong/multichat 1.6.0 → 1.8.0

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 (84) hide show
  1. package/README.md +9 -22
  2. package/backend/dist/routes/agents.js +4 -2
  3. package/backend/dist/routes/agents.js.map +1 -1
  4. package/backend/dist/routes/assistants.js +4 -2
  5. package/backend/dist/routes/assistants.js.map +1 -1
  6. package/backend/dist/routes/chat.js +45 -13
  7. package/backend/dist/routes/chat.js.map +1 -1
  8. package/backend/dist/routes/control-plane.js +25 -18
  9. package/backend/dist/routes/control-plane.js.map +1 -1
  10. package/backend/dist/routes/conversations.js +116 -30
  11. package/backend/dist/routes/conversations.js.map +1 -1
  12. package/backend/dist/routes/import.js +5 -4
  13. package/backend/dist/routes/import.js.map +1 -1
  14. package/backend/dist/routes/mock.js +3 -0
  15. package/backend/dist/routes/mock.js.map +1 -1
  16. package/backend/dist/routes/prompt-templates.js +50 -0
  17. package/backend/dist/routes/prompt-templates.js.map +1 -0
  18. package/backend/dist/routes/providers.js +15 -4
  19. package/backend/dist/routes/providers.js.map +1 -1
  20. package/backend/dist/routes/runs.js +35 -16
  21. package/backend/dist/routes/runs.js.map +1 -1
  22. package/backend/dist/routes/tools.js +15 -13
  23. package/backend/dist/routes/tools.js.map +1 -1
  24. package/backend/dist/runtime/auto-snapshot.js +73 -0
  25. package/backend/dist/runtime/auto-snapshot.js.map +1 -0
  26. package/backend/dist/server.js +3 -0
  27. package/backend/dist/server.js.map +1 -1
  28. package/backend/package.json +5 -2
  29. package/backend/routes/agents.ts +6 -4
  30. package/backend/routes/assistants.ts +4 -2
  31. package/backend/routes/chat.ts +43 -13
  32. package/backend/routes/control-plane.ts +22 -14
  33. package/backend/routes/conversations.ts +105 -29
  34. package/backend/routes/import.ts +11 -10
  35. package/backend/routes/mock.ts +4 -0
  36. package/backend/routes/prompt-templates.ts +38 -0
  37. package/backend/routes/providers.ts +14 -4
  38. package/backend/routes/runs.ts +42 -20
  39. package/backend/routes/tools.ts +14 -12
  40. package/backend/runtime/auto-snapshot.ts +60 -0
  41. package/backend/server.ts +3 -0
  42. package/bin/multichat.mjs +18 -0
  43. package/frontend/dist/assets/agentPicker-Jan8fWb3.js +18 -0
  44. package/frontend/dist/assets/assets-ChkvQqhp.js +1 -0
  45. package/frontend/dist/assets/compare-CkyqXEbN.js +1 -0
  46. package/frontend/dist/assets/conversations-DoyXfY4T.js +1 -0
  47. package/frontend/dist/assets/data-BN7t_AoO.js +1 -0
  48. package/frontend/dist/assets/extensionImport-DPZhdnxE.js +28 -0
  49. package/frontend/dist/assets/importExport-Bb6-L-4y.js +69 -0
  50. package/frontend/dist/assets/index-CeU_ksnE.js +49 -0
  51. package/frontend/dist/assets/index-DDdMiim6.css +1 -0
  52. package/frontend/dist/assets/init-yIIG9GSH.js +1 -0
  53. package/frontend/dist/assets/modal-BYrNUOVj.js +2 -0
  54. package/frontend/dist/assets/modelPicker-B612Gojw.js +14 -0
  55. package/frontend/dist/assets/pluginsUI-KCpFagXv.js +16 -0
  56. package/frontend/dist/assets/react-sySUN5R7.js +1 -0
  57. package/frontend/dist/assets/{render-DgAg1K8A.js → render-BBfAbC8M.js} +1 -1
  58. package/frontend/dist/assets/send-BAyqvnlM.js +14 -0
  59. package/frontend/dist/assets/settings-DkUaMAOG.js +2 -0
  60. package/frontend/dist/assets/{shell-DFV2khQl.js → shell-Cny595HR.js} +1 -1
  61. package/frontend/dist/assets/workbench-cTwE7423.js +40 -0
  62. package/frontend/dist/boot-fallback.js +18 -0
  63. package/frontend/dist/icon-maskable.svg +18 -3
  64. package/frontend/dist/icon.svg +13 -3
  65. package/frontend/dist/index.html +11 -4
  66. package/frontend/dist/manifest.webmanifest +2 -2
  67. package/frontend/dist/theme-seed.js +21 -0
  68. package/package.json +4 -1
  69. package/frontend/dist/assets/agentPicker-C39VzmYa.js +0 -13
  70. package/frontend/dist/assets/compare-BpaGD32J.js +0 -1
  71. package/frontend/dist/assets/conversations-COuLmg99.js +0 -7
  72. package/frontend/dist/assets/data-ZH7Dux1K.js +0 -1
  73. package/frontend/dist/assets/extensionImport-CnTWKtWm.js +0 -28
  74. package/frontend/dist/assets/importExport-B3mB-9zs.js +0 -70
  75. package/frontend/dist/assets/index-CASVCOUO.js +0 -34
  76. package/frontend/dist/assets/index-DSvbjNLc.css +0 -1
  77. package/frontend/dist/assets/init-ZPK483vB.js +0 -1
  78. package/frontend/dist/assets/jsx-runtime-ZSsbr0oW.js +0 -9
  79. package/frontend/dist/assets/modal-CgHocv3n.js +0 -2
  80. package/frontend/dist/assets/modelPicker-EUmmVnbj.js +0 -14
  81. package/frontend/dist/assets/pluginsUI-B6jGUQkx.js +0 -16
  82. package/frontend/dist/assets/send-BJhZ_5YA.js +0 -14
  83. package/frontend/dist/assets/settings-DdnYwZl-.js +0 -2
  84. package/frontend/dist/assets/workbench-BY2J4vWC.js +0 -41
@@ -2,11 +2,66 @@
2
2
  // ── Conversations CRUD ──────────────────────────────────────────────────
3
3
  const ctx = require('../lib/context');
4
4
 
5
+ // 消息文件名由 id 拼接而成(messages_<id>.json)。id 必须先过白名单,
6
+ // 否则带路径分隔符的 id 会落到 store 的 basename 校验上变成 500 + 内部报错泄漏。
7
+ const SAFE_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
8
+ function assertSafeId(res, id) {
9
+ if (typeof id === 'string' && SAFE_ID_RE.test(id)) return true;
10
+ res.status(400).json({ error: 'invalid conversation id', code: 'INVALID_ID' });
11
+ return false;
12
+ }
13
+
14
+ function bodyObject(value) {
15
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
16
+ }
17
+
18
+ function optionalText(input, key, max) {
19
+ if (input[key] === undefined) return {};
20
+ const value = String(input[key] || '').trim().slice(0, max);
21
+ return { [key]: value };
22
+ }
23
+
24
+ function conversationChanges(raw) {
25
+ const input = bodyObject(raw);
26
+ return {
27
+ ...optionalText(input, 'title', 200),
28
+ ...optionalText(input, 'modelA', 200),
29
+ ...optionalText(input, 'modelB', 200),
30
+ ...optionalText(input, 'systemPrompt', 30_000),
31
+ ...optionalText(input, 'inputPlaceholder', 500),
32
+ ...(input.compareMode !== undefined ? { compareMode: input.compareMode === true } : {}),
33
+ ...(input.workspaceId !== undefined ? { workspaceId: input.workspaceId ? ctx.safeId(input.workspaceId, 'workspace id') : null } : {}),
34
+ ...(input.projectId !== undefined ? { projectId: input.projectId ? ctx.safeId(input.projectId, 'project id') : null } : {}),
35
+ };
36
+ }
37
+
38
+ function validateConversationLocation(workspaceId, projectId) {
39
+ const workspace = workspaceId ? ctx.workspaceStore.getWorkspace(workspaceId) : null;
40
+ const project = projectId ? ctx.workspaceStore.getProject(projectId) : null;
41
+ if (workspaceId && !workspace) throw new Error('workspace not found');
42
+ if (projectId && !project) throw new Error('project not found');
43
+ if (workspace && project && project.workspaceId !== workspace.id) throw new Error('project does not belong to workspace');
44
+ }
45
+
46
+ function validateMessages(value) {
47
+ if (!Array.isArray(value)) throw new Error('messages must be an array');
48
+ if (value.length > 2_000) throw new Error('messages exceed 2000 entries');
49
+ if (Buffer.byteLength(JSON.stringify(value), 'utf8') > 16_000_000) throw new Error('messages exceed 16 MB');
50
+ for (const message of value) {
51
+ if (!message || typeof message !== 'object' || Array.isArray(message)) throw new Error('each message must be an object');
52
+ if (!['system', 'user', 'assistant', 'tool'].includes(message.role) || typeof message.content !== 'string') {
53
+ throw new Error('each message requires a valid role and string content');
54
+ }
55
+ }
56
+ return value;
57
+ }
58
+
5
59
  module.exports = function registerConversations(app) {
6
60
  app.get('/api/conversations', (req, res) => {
7
61
  res.json(ctx.store.read('conversations.json', []));
8
62
  });
9
63
  app.get('/api/conversations/:id', (req, res) => {
64
+ if (!assertSafeId(res, req.params.id)) return;
10
65
  const convs = ctx.store.read('conversations.json', []);
11
66
  const conv = convs.find(c => c.id === req.params.id);
12
67
  if (!conv) return res.status(404).json({ error: 'not found' });
@@ -14,37 +69,53 @@ module.exports = function registerConversations(app) {
14
69
  res.json({ ...conv, messages });
15
70
  });
16
71
  app.post('/api/conversations', (req, res) => {
17
- const convs = ctx.store.read('conversations.json', []);
18
- const defaultWorkspace = ctx.workspaceStore.workspaces()[0];
19
- const defaultProject = ctx.workspaceStore.projects().find(x => x.workspaceId === defaultWorkspace?.id);
20
- const { messages: _messages, id: _id, createdAt: _createdAt, ...input } = req.body || {};
21
- const conv = {
22
- id: Date.now().toString(36),
23
- createdAt: new Date().toISOString(),
24
- compareMode: false,
25
- modelA: '',
26
- modelB: '',
27
- systemPrompt: '',
28
- inputPlaceholder: '',
29
- workspaceId: input.workspaceId || defaultWorkspace?.id || null,
30
- projectId: input.projectId || defaultProject?.id || null,
31
- ...input,
32
- };
33
- convs.unshift(conv);
34
- ctx.store.write('conversations.json', convs);
35
- ctx.store.write(`messages_${conv.id}.json`, []);
36
- res.json(conv);
72
+ try {
73
+ const convs = ctx.store.read('conversations.json', []);
74
+ const defaultWorkspace = ctx.workspaceStore.workspaces()[0];
75
+ const input = conversationChanges(req.body);
76
+ const workspaceId = input.workspaceId || defaultWorkspace?.id || null;
77
+ const defaultProject = ctx.workspaceStore.projects().find(x => x.workspaceId === workspaceId);
78
+ const projectId = input.projectId || defaultProject?.id || null;
79
+ validateConversationLocation(workspaceId, projectId);
80
+ const conv = {
81
+ compareMode: false,
82
+ modelA: '',
83
+ modelB: '',
84
+ systemPrompt: '',
85
+ inputPlaceholder: '',
86
+ title: '新对话',
87
+ ...input,
88
+ id: Date.now().toString(36),
89
+ createdAt: new Date().toISOString(),
90
+ workspaceId,
91
+ projectId,
92
+ };
93
+ convs.unshift(conv);
94
+ ctx.store.write('conversations.json', convs);
95
+ ctx.store.write(`messages_${conv.id}.json`, []);
96
+ res.json(conv);
97
+ } catch (error) {
98
+ res.status(400).json({ error: error.message, code: 'BAD_REQUEST' });
99
+ }
37
100
  });
38
101
  app.put('/api/conversations/:id', (req, res) => {
39
- const convs = ctx.store.read('conversations.json', []);
40
- const idx = convs.findIndex(c => c.id === req.params.id);
41
- if (idx < 0) return res.status(404).json({ error: 'not found' });
42
- const { messages: _messages, createdAt: _createdAt, ...changes } = req.body || {};
43
- convs[idx] = { ...convs[idx], ...changes, id: req.params.id };
44
- ctx.store.write('conversations.json', convs);
45
- res.json(convs[idx]);
102
+ if (!assertSafeId(res, req.params.id)) return;
103
+ try {
104
+ const convs = ctx.store.read('conversations.json', []);
105
+ const idx = convs.findIndex(c => c.id === req.params.id);
106
+ if (idx < 0) return res.status(404).json({ error: 'not found' });
107
+ const changes = conversationChanges(req.body);
108
+ const next = { ...convs[idx], ...changes, id: req.params.id, createdAt: convs[idx].createdAt };
109
+ validateConversationLocation(next.workspaceId, next.projectId);
110
+ convs[idx] = next;
111
+ ctx.store.write('conversations.json', convs);
112
+ res.json(next);
113
+ } catch (error) {
114
+ res.status(400).json({ error: error.message, code: 'BAD_REQUEST' });
115
+ }
46
116
  });
47
117
  app.delete('/api/conversations/:id', (req, res) => {
118
+ if (!assertSafeId(res, req.params.id)) return;
48
119
  let convs = ctx.store.read('conversations.json', []);
49
120
  convs = convs.filter(c => c.id !== req.params.id);
50
121
  ctx.store.write('conversations.json', convs);
@@ -54,9 +125,14 @@ module.exports = function registerConversations(app) {
54
125
  });
55
126
  // Save messages for a conversation
56
127
  app.post('/api/conversations/:id/messages', (req, res) => {
128
+ if (!assertSafeId(res, req.params.id)) return;
57
129
  const convs = ctx.store.read('conversations.json', []);
58
130
  if (!convs.find(c => c.id === req.params.id)) return res.status(404).json({ error: 'not found' });
59
- ctx.store.write(`messages_${req.params.id}.json`, req.body);
60
- res.json({ ok: true });
131
+ try {
132
+ ctx.store.write(`messages_${req.params.id}.json`, validateMessages(req.body));
133
+ res.json({ ok: true });
134
+ } catch (error) {
135
+ res.status(400).json({ error: error.message, code: 'BAD_REQUEST' });
136
+ }
61
137
  });
62
138
  };
@@ -44,7 +44,7 @@ function normalizeAgent(raw) {
44
44
  }
45
45
 
46
46
  module.exports = function registerImport(app) {
47
- app.post('/api/import', async (req, res) => {
47
+ app.post('/api/import', (req, res, next) => {
48
48
  const body = req.body || {};
49
49
  if (body.url || body.format === 'zip' || body.skills || body.type || body.mcp) {
50
50
  return fail(res, 400, 'INVALID_PACKAGE', 'Skill 请导入标准 SKILL.md 目录,Plugin 请使用 .codex-plugin/plugin.json,MCP 请使用独立 MCP server 配置');
@@ -59,14 +59,15 @@ module.exports = function registerImport(app) {
59
59
  let normalized;
60
60
  try { normalized = incoming.map(normalizeAgent); }
61
61
  catch (error) { return fail(res, 400, 'INVALID_PACKAGE', error.message); }
62
- await ctx.store.mutate(ctx.AGENT_FILE, agents => {
63
- for (const item of normalized) {
64
- const index = agents.findIndex(agent => agent.id === item.id);
65
- if (index >= 0) agents[index] = item;
66
- else agents.push(item);
67
- }
68
- return agents;
69
- }, []);
70
- res.json({ ok: true, agents: normalized.length, skills: 0 });
62
+ void ctx.store.mutate(ctx.AGENT_FILE, agents => {
63
+ for (const item of normalized) {
64
+ const index = agents.findIndex(agent => agent.id === item.id);
65
+ if (index >= 0) agents[index] = item;
66
+ else agents.push(item);
67
+ }
68
+ return agents;
69
+ }, [])
70
+ .then(() => res.json({ ok: true, agents: normalized.length, skills: 0 }))
71
+ .catch(next);
71
72
  });
72
73
  };
@@ -4,6 +4,10 @@
4
4
  // built-in “Mock (本地体验)” provider. It never calls the network and makes the
5
5
  // first-run/chat/usage UI testable without an API key.
6
6
  module.exports = function registerMock(app) {
7
+ app.get('/api/mock/v1/models', (_req, res) => {
8
+ res.json({ object: 'list', data: [{ id: 'echo', object: 'model', owned_by: 'multichat-local' }] });
9
+ });
10
+
7
11
  app.post('/api/mock/v1/chat/completions', (req, res) => {
8
12
  const body = req.body || {};
9
13
  const messages = Array.isArray(body.messages) ? body.messages : [];
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+ // ── 提示词模板库:运行配置系统指令的可复用命名模板(纯文本,无执行语义)──
3
+ const ctx = require('../lib/context');
4
+
5
+ function normalize(raw) {
6
+ if (!raw || typeof raw !== 'object') throw new Error('template must be an object');
7
+ const name = String(raw.name || '').trim();
8
+ const content = String(raw.content || '').replace(/\r\n/g, '\n').trim();
9
+ if (!name || name.length > 80) throw new Error('模板名称必须是 1-80 个字符');
10
+ if (!content || content.length > 30_000) throw new Error('模板内容必须是 1-30000 个字符');
11
+ return { id: ctx.safeId(raw.id || 'pt_' + Date.now().toString(36), 'template id'), name, content, createdAt: raw.createdAt || new Date().toISOString() };
12
+ }
13
+
14
+ module.exports = function registerPromptTemplates(app) {
15
+ app.get('/api/prompt-templates', (req, res) => res.json(ctx.store.read('prompt_templates.json', [])));
16
+ app.post('/api/prompt-templates', (req, res, next) => {
17
+ let tpl;
18
+ try { tpl = normalize(req.body); } catch (e) { return res.status(400).json({ error: e.message, code: 'INVALID_TEMPLATE' }); }
19
+ void ctx.store.mutate('prompt_templates.json', rows => {
20
+ const i = rows.findIndex(r => r.name === tpl.name);
21
+ if (i >= 0) rows[i] = tpl; else rows.push(tpl);
22
+ return rows;
23
+ }, [])
24
+ .then(() => res.status(201).json(tpl))
25
+ .catch(next);
26
+ });
27
+ app.delete('/api/prompt-templates/:id', (req, res, next) => {
28
+ let found = false;
29
+ void ctx.store.mutate('prompt_templates.json', rows => rows.filter(row => {
30
+ if (row.id === req.params.id) found = true;
31
+ return row.id !== req.params.id;
32
+ }), [])
33
+ .then(() => found
34
+ ? res.json({ ok: true })
35
+ : res.status(404).json({ error: 'template not found', code: 'TEMPLATE_NOT_FOUND' }))
36
+ .catch(next);
37
+ });
38
+ };
@@ -89,12 +89,17 @@ module.exports = function registerProviders(app) {
89
89
  res.json(models);
90
90
  });
91
91
 
92
- // ── 连接测试:用服务端保存的密钥探测 OpenAI 兼容的 GET /models,返回可达性与模型清单 ──
92
+ // ── 连接测试:用服务端保存的密钥探测 OpenAI 兼容的 GET /models,返回可达性与模型清单。
93
+ // 探测到的能力(vision/reasoning 启发式)会写回 provider 记录,供界面标注。──
93
94
  app.post('/api/providers/:id/probe', async (req, res) => {
94
95
  const providers = ctx.providerStore.list();
95
- const provider = providers.find(p => p.id === req.params.id);
96
- if (!provider) return res.status(404).json({ error: 'provider not found', code: 'NOT_FOUND' });
97
- const baseUrl = String(provider.baseUrl || '').replace(/\/+$/, '');
96
+ const idx = providers.findIndex(p => p.id === req.params.id);
97
+ if (idx < 0) return res.status(404).json({ error: 'provider not found', code: 'NOT_FOUND' });
98
+ const provider = providers[idx];
99
+ const configuredBaseUrl = String(provider.baseUrl || '').replace(/\/+$/, '');
100
+ const baseUrl = provider.id === 'mock' && /(?:127\.0\.0\.1|localhost):3099/i.test(configuredBaseUrl)
101
+ ? `http://127.0.0.1:${ctx.PORT}/api/mock/v1`
102
+ : configuredBaseUrl;
98
103
  let parsed: URL;
99
104
  try { parsed = new URL(baseUrl); } catch { return res.status(400).json({ error: 'provider baseUrl 无效' }); }
100
105
  const modelsUrl = `${parsed.origin}${parsed.pathname.replace(/\/+$/, '')}/models`;
@@ -114,6 +119,11 @@ module.exports = function registerProviders(app) {
114
119
  vision: models.some(m => /(?:vision|vl|omni|multimodal|4o|gpt-4-turbo|gemini|claude-[35])/i.test(m)),
115
120
  reasoning: models.some(m => /(?:reason|think|o[1345]-|r1|qwq)/i.test(m)),
116
121
  };
122
+ // 持久化探测到的能力,界面无需再次探测即可标注。
123
+ try {
124
+ providers[idx] = { ...provider, capabilities: { ...(provider.capabilities as JsonRecord | undefined), ...capabilities } };
125
+ ctx.providerStore.save(providers);
126
+ } catch { /* 能力标注失败不影响探测结果返回 */ }
117
127
  res.json({ ok: true, models, capabilities, modelCount: models.length });
118
128
  } catch (error) {
119
129
  const raw = error instanceof Error ? error.message : String(error);
@@ -10,6 +10,15 @@ module.exports = function registerRuns(app) {
10
10
  res.json(runs.slice(0, limit));
11
11
  });
12
12
 
13
+ // ── 审计导出:完整 Run/Turn/Step/审批链,供合规归档(JSON,企业审计包入口)──
14
+ app.get('/api/runs/export', (req, res) => {
15
+ const runs = ctx.store.read(ctx.RUN_FILE, []);
16
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
17
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
18
+ res.setHeader('Content-Disposition', 'attachment; filename="multichat-audit-' + stamp + '.json"');
19
+ res.json({ exportedAt: new Date().toISOString(), format: 'multichat-audit/v1', runCount: runs.length, runs });
20
+ });
21
+
13
22
  app.get('/api/runs/:id', (req, res) => {
14
23
  const run = ctx.store.read(ctx.RUN_FILE, []).find(x => x.id === req.params.id);
15
24
  if (!run) return res.status(404).json({ error: 'run not found' });
@@ -30,26 +39,39 @@ module.exports = function registerRuns(app) {
30
39
  // 审批决策:写入持久化检查点,不占用原 Agent 响应连接。
31
40
  app.post('/api/runs/:id/approval/:approvalId', (req, res, next) => {
32
41
  void (async () => {
33
- const run = ctx.store.read(ctx.RUN_FILE, []).find(x => x.id === req.params.id);
34
- if (!run) return res.status(404).json({ error: { message: 'run not found' }, code: 'RUN_NOT_FOUND' });
35
- const rec = (run.approvals || []).find(a => a.id === req.params.approvalId);
36
- if (!rec || rec.status !== 'pending') {
37
- return res.status(409).json({
38
- error: { message: '审批请求不存在或已处理' + (rec ? '(当前状态:' + rec.status + ')' : '') },
39
- code: 'APPROVAL_NOT_PENDING',
40
- });
41
- }
42
- const action = req.body && req.body.action;
43
- if (action !== 'approve' && action !== 'reject') {
44
- return res.status(400).json({ error: { message: 'action 必须是 approve 或 reject' }, code: 'BAD_REQUEST' });
45
- }
46
- rec.status = action === 'approve' ? 'approved' : 'rejected';
47
- rec.resolvedAt = new Date().toISOString();
48
- run.status = 'paused';
49
- run.pauseReason = action === 'approve' ? 'approval granted; ready to resume' : 'approval rejected; ready to resume';
50
- if (run.checkpoint) run.checkpoint.approvalDecision = action;
51
- await ctx.store.mutate(ctx.RUN_FILE, (runs) => runs.map(item => item.id === run.id ? run : item), []);
52
- res.json({ ok: true, approvalId: req.params.approvalId, action, runId: req.params.id, resumable: true });
42
+ const action = req.body && req.body.action;
43
+ if (action !== 'approve' && action !== 'reject') {
44
+ return res.status(400).json({ error: { message: 'action 必须是 approve 或 reject' }, code: 'BAD_REQUEST' });
45
+ }
46
+
47
+ let outcome = { kind: 'missing-run', status: '' };
48
+ await ctx.store.mutate(ctx.RUN_FILE, (runs) => {
49
+ const run = runs.find(item => item.id === req.params.id);
50
+ if (!run) return undefined;
51
+ const rec = (run.approvals || []).find(item => item.id === req.params.approvalId);
52
+ if (!rec || rec.status !== 'pending') {
53
+ outcome = { kind: 'not-pending', status: rec?.status || '' };
54
+ return undefined;
55
+ }
56
+ rec.status = action === 'approve' ? 'approved' : 'rejected';
57
+ rec.resolvedAt = new Date().toISOString();
58
+ run.status = 'paused';
59
+ run.pauseReason = action === 'approve' ? 'approval granted; ready to resume' : 'approval rejected; ready to resume';
60
+ if (run.checkpoint) run.checkpoint.approvalDecision = action;
61
+ outcome = { kind: 'updated', status: rec.status };
62
+ return runs;
63
+ }, []);
64
+
65
+ if (outcome.kind === 'missing-run') {
66
+ return res.status(404).json({ error: { message: 'run not found' }, code: 'RUN_NOT_FOUND' });
67
+ }
68
+ if (outcome.kind === 'not-pending') {
69
+ return res.status(409).json({
70
+ error: { message: '审批请求不存在或已处理' + (outcome.status ? '(当前状态:' + outcome.status + ')' : '') },
71
+ code: 'APPROVAL_NOT_PENDING',
72
+ });
73
+ }
74
+ res.json({ ok: true, approvalId: req.params.approvalId, action, runId: req.params.id, resumable: true });
53
75
  })().catch(next);
54
76
  });
55
77
  };
@@ -24,17 +24,19 @@ module.exports = function registerTools(app) {
24
24
 
25
25
  // ── 本地试跑:只开放无网络、无副作用的纯计算类工具,避免端点被当作代理 ──
26
26
  const TESTABLE_TYPES = new Set(['datetime', 'calculator']);
27
- app.post('/api/tools/:id/test', async (req, res) => {
28
- const tool = listTools().find(item => item.id === req.params.id);
29
- if (!tool) return res.status(404).json({ error: 'tool not found', code: 'NOT_FOUND' });
30
- if (!TESTABLE_TYPES.has(tool.type)) {
31
- return res.status(400).json({ error: `「${tool.name}」暂不支持界面试跑(涉及网络访问,请通过对话调用)`, code: 'NOT_TESTABLE' });
32
- }
33
- const args = tool.type === 'calculator'
34
- ? { expression: String(req.body?.expression ?? '').trim() }
35
- : {};
36
- const started = Date.now();
37
- const result = await agent.executeSkill(tool, args);
38
- res.json({ ...result, elapsedMs: Date.now() - started });
27
+ app.post('/api/tools/:id/test', (req, res, next) => {
28
+ void (async () => {
29
+ const tool = listTools().find(item => item.id === req.params.id);
30
+ if (!tool) return res.status(404).json({ error: 'tool not found', code: 'NOT_FOUND' });
31
+ if (!TESTABLE_TYPES.has(tool.type)) {
32
+ return res.status(400).json({ error: `「${tool.name}」暂不支持界面试跑(涉及网络访问,请通过对话调用)`, code: 'NOT_TESTABLE' });
33
+ }
34
+ const args = tool.type === 'calculator'
35
+ ? { expression: String(req.body?.expression ?? '').trim() }
36
+ : {};
37
+ const started = Date.now();
38
+ const result = await agent.executeSkill(tool, args);
39
+ res.json({ ...result, elapsedMs: Date.now() - started });
40
+ })().catch(next);
39
41
  });
40
42
  };
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ // ── 每日自动快照:为每个项目在距上次自动快照 ≥24h 时创建一份,并只保留最近 N 份自动快照 ──
3
+ // 手动创建的项目快照永不参与清理。MULTICHAT_AUTO_SNAPSHOT=0 可整体关闭。
4
+ const ctx = require('../lib/context');
5
+ const controlPlane = require('../routes/control-plane');
6
+
7
+ const SWEEP_INTERVAL_MS = 6 * 60 * 60 * 1000; // 每 6 小时巡检一次,是否到期由 24h 阈值判断
8
+ const DUE_MS = 24 * 60 * 60 * 1000;
9
+ const KEEP = Math.max(1, Number(process.env.MULTICHAT_AUTO_SNAPSHOT_KEEP || 7));
10
+ const AUTO_TITLE_PREFIX = '自动快照 ';
11
+
12
+ function isAutomatic(row) { return row.automatic === true || String(row.title || '').startsWith(AUTO_TITLE_PREFIX); }
13
+
14
+ export function runAutoSnapshotSweep(nowMs = Date.now()) {
15
+ if (process.env.MULTICHAT_AUTO_SNAPSHOT === '0') return { created: [], pruned: 0, disabled: true };
16
+ const created = [];
17
+ let pruned = 0;
18
+ const projects = ctx.workspaceStore.projects();
19
+ const assets = ctx.workspaceStore.assets();
20
+ const memories = ctx.store.read('memories.json', []);
21
+ const rows = ctx.store.read('snapshots.json', []);
22
+ for (const project of projects) {
23
+ const hasRecoverableState = assets.some(item => item.projectId === project.id)
24
+ || memories.some(item => item.projectId === project.id)
25
+ || Boolean(project.defaultAgentId || project.defaultProviderId || project.defaultModel);
26
+ // A fresh inbox/project has nothing to recover. Skipping it avoids a daily
27
+ // stream of empty snapshots that obscures meaningful restore points.
28
+ if (!hasRecoverableState) continue;
29
+ const autoRows = rows
30
+ .filter(row => row.projectId === project.id && isAutomatic(row))
31
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)));
32
+ const newest = autoRows[0]?.createdAt ? Date.parse(autoRows[0].createdAt) : 0;
33
+ if (Number.isFinite(newest) && nowMs - newest < DUE_MS) continue;
34
+ try {
35
+ const row = controlPlane.makeSnapshot(project.id, { title: AUTO_TITLE_PREFIX + new Date(nowMs).toISOString().slice(0, 10) }, true);
36
+ created.push({ projectId: project.id, id: row.id, title: row.title });
37
+ } catch { /* 单个项目失败(如超大文件)不阻断其他项目 */ }
38
+ }
39
+ if (created.length) {
40
+ const all = ctx.store.read('snapshots.json', []);
41
+ const keep = new Set();
42
+ for (const project of ctx.workspaceStore.projects()) {
43
+ all.filter(row => row.projectId === project.id && isAutomatic(row))
44
+ .sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
45
+ .slice(0, KEEP)
46
+ .forEach(row => keep.add(row.id));
47
+ }
48
+ const next = all.filter(row => !isAutomatic(row) || keep.has(row.id));
49
+ pruned = all.length - next.length;
50
+ if (pruned > 0) ctx.store.write('snapshots.json', next);
51
+ }
52
+ return { created, pruned };
53
+ }
54
+
55
+ export function startAutoSnapshots() {
56
+ if (process.env.MULTICHAT_AUTO_SNAPSHOT === '0') return;
57
+ const boot = setTimeout(() => { try { runAutoSnapshotSweep(); } catch {} }, 2 * 60 * 1000);
58
+ const timer = setInterval(() => { try { runAutoSnapshotSweep(); } catch {} }, SWEEP_INTERVAL_MS);
59
+ boot.unref?.(); timer.unref?.();
60
+ }
package/backend/server.ts CHANGED
@@ -62,6 +62,7 @@ require('./routes/conversations')(app);
62
62
  require('./routes/workspaces')(app);
63
63
  require('./routes/assistants')(app);
64
64
  require('./routes/runs')(app);
65
+ require('./routes/prompt-templates')(app);
65
66
  require('./routes/usage')(app);
66
67
  require('./routes/control-plane')(app);
67
68
  require('./routes/skills')(app);
@@ -87,6 +88,8 @@ app.use(errorHandler);
87
88
  runtime.ensureSeed();
88
89
  require('./extensions/manager').ensureDefaults();
89
90
 
91
+ // 每日自动快照调度(可用 MULTICHAT_AUTO_SNAPSHOT=0 关闭)
92
+ require("./runtime/auto-snapshot").startAutoSnapshots();
90
93
  const server = app.listen(PORT, HOST, () => {
91
94
  console.log(`Multi-model chat server running on http://${HOST}:${PORT}`);
92
95
  console.log(`OpenAI-compatible endpoint: POST /v1/chat/completions`);
package/bin/multichat.mjs CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  readFileSync,
11
11
  readdirSync,
12
12
  statSync,
13
+ unlinkSync,
14
+ writeFileSync,
13
15
  } from 'node:fs';
14
16
  import { mkdir } from 'node:fs/promises';
15
17
  import { homedir } from 'node:os';
@@ -333,6 +335,11 @@ async function runWeb(options, deployAlias = false) {
333
335
  }
334
336
  assertDistinctDirectories(options.workspace, options.dataDir);
335
337
  await mkdir(options.dataDir, { recursive: true });
338
+ try {
339
+ probeDirectoryWrite(options.dataDir);
340
+ } catch (error) {
341
+ throw new CliError(`数据目录不可写:${options.dataDir}(${error.message})`);
342
+ }
336
343
 
337
344
  const port = await checkPort(options.host, options.port);
338
345
  if (!port.ok) {
@@ -401,6 +408,15 @@ function nearestExistingDirectory(target) {
401
408
  return isDirectory(current) ? current : path.dirname(current);
402
409
  }
403
410
 
411
+ function probeDirectoryWrite(directory) {
412
+ const probe = path.join(directory, `.multichat-write-probe-${process.pid}-${Date.now()}`);
413
+ try {
414
+ writeFileSync(probe, 'ok', { encoding: 'utf8', flag: 'wx' });
415
+ } finally {
416
+ try { unlinkSync(probe); } catch {}
417
+ }
418
+ }
419
+
404
420
  async function runDoctor(options) {
405
421
  const runtime = resolveRuntime();
406
422
  const results = [];
@@ -418,6 +434,7 @@ async function runDoctor(options) {
418
434
  } else {
419
435
  try {
420
436
  accessSync(options.workspace, constants.R_OK | constants.W_OK);
437
+ probeDirectoryWrite(options.workspace);
421
438
  add('PASS', '工作区', options.workspace);
422
439
  } catch (error) {
423
440
  add('FAIL', '工作区', `${options.workspace}:${error.message}`);
@@ -433,6 +450,7 @@ async function runDoctor(options) {
433
450
  try {
434
451
  if (!writableRoot) throw new Error('找不到可写父目录');
435
452
  accessSync(writableRoot, constants.W_OK);
453
+ probeDirectoryWrite(writableRoot);
436
454
  add('PASS', '数据目录', existsSync(options.dataDir) ? options.dataDir : `${options.dataDir}(首次启动时创建)`);
437
455
  } catch (error) {
438
456
  add('FAIL', '数据目录', `${options.dataDir}:${error.message}`);
@@ -0,0 +1,18 @@
1
+ import{E as e,O as t,T as n,b as r,y as i}from"./index-CeU_ksnE.js";import{importProjectFolder as a}from"./assets-ChkvQqhp.js";e(`#workspacePicker`).onclick=()=>{let e=r.projects.map(e=>({id:e.id,name:e.name,description:e.description}));showModal({title:`选择项目`,body:`<div class="picker-dialog-intro"><span>每个项目保存自己的文件、记忆和对话上下文</span><span>${e.length} 个项目</span></div>
2
+ <button type="button" class="picker-import-project" data-import-project>
3
+ <span class="picker-provider-mark folder" aria-hidden="true">+</span>
4
+ <span class="picker-option-copy"><strong>打开本地项目文件夹</strong><small>自动创建项目并导入可读取的源码与文档</small></span>
5
+ <span class="picker-option-state" aria-hidden="true">›</span>
6
+ </button>
7
+ <div class="model-picker-list">${e.map(e=>{let n=r.selectedProject?.id===e.id,i=e.id===`pr_inbox`||e.name===`收件箱`,a=i?`临时对话`:e.name,o=i?`不载入项目文件,只进行普通对话`:e.description||`本地项目`;return`<button type="button" class="picker-option${n?` active`:``}" data-pid="${t(e.id)}" aria-pressed="${n}">
8
+ <span class="picker-provider-mark" aria-hidden="true">${i?`—`:t(a.trim().slice(0,1).toUpperCase()||`P`)}</span>
9
+ <span class="picker-option-copy"><strong>${t(a)}</strong><small>${t(o)}</small></span>
10
+ <span class="picker-option-state" aria-hidden="true">${n?`✓`:`›`}</span>
11
+ </button>`}).join(``)}</div>`,onMount:e=>{let t=e.querySelector(`[data-import-project]`);t&&(t.onclick=()=>{closeModal(),a()}),e.querySelectorAll(`[data-pid]`).forEach(e=>e.onclick=async()=>{let t=r.projects.find(t=>t.id===e.dataset.pid)||null;t&&(r.selectedProject=t,localStorage.setItem(`multichat_project`,t.id),await loadProjects(),await newConversation(),renderTopbar(),renderFileContext(),closeModal())})}})},e(`#agentPicker`).onclick=()=>{if(!r.agents.length){openSettings(`agents`),n(`请先创建运行配置`);return}let e=[{id:``,name:`直接对话`,description:`不注入系统提示词,也不启用工具`}].concat(r.agents.map(e=>({id:e.id,name:e.name,description:e.description})));showModal({title:`选择运行配置`,body:`<div class="picker-dialog-intro"><span>选择本轮使用的提示词与能力组合</span><span>${r.agents.length} 个配置</span></div>
12
+ <div class="model-picker-list">
13
+ ${e.map(e=>{let n=(r.selectedAgent?.id||``)===e.id;return`<button type="button" class="picker-option${n?` active`:``}" data-aid="${t(e.id)}" aria-pressed="${n}">
14
+ <span class="picker-provider-mark agent" aria-hidden="true">${e.id?t(e.name.trim().slice(0,1).toUpperCase()||`A`):`—`}</span>
15
+ <span class="picker-option-copy"><strong>${t(e.name)}</strong><small>${t(e.description||`自定义运行配置`)}</small></span>
16
+ <span class="picker-option-state" aria-hidden="true">${n?`✓`:`›`}</span>
17
+ </button>`}).join(``)}
18
+ </div>`,onMount:e=>{e.querySelectorAll(`[data-aid]`).forEach(e=>{e.onclick=()=>{let t=e.dataset.aid;r.selectedAgent=t?r.agents.find(e=>e.id===t):null,i(),renderTopbar(),closeModal(),r.messages.length||renderContent()}})}})};
@@ -0,0 +1 @@
1
+ import{T as e,b as t,f as n}from"./index-CeU_ksnE.js";var r=18e5,i=16e6,a=240,o=new Set([`.git`,`.hg`,`.svn`,`.idea`,`.vscode`,`node_modules`,`dist`,`build`,`coverage`,`.next`,`.nuxt`,`.cache`,`target`,`vendor`,`__pycache__`]),s=new Set(`txt.md.mdx.json.jsonc.csv.tsv.js.jsx.mjs.cjs.ts.tsx.py.go.rs.java.kt.kts.c.h.cpp.hpp.cs.php.rb.swift.vue.svelte.html.htm.css.scss.sass.less.yaml.yml.toml.ini.conf.env.sql.graphql.gql.sh.bash.zsh.fish.ps1.bat.cmd.xml.svg.lock`.split(`.`)),c=new Set([`readme`,`license`,`changelog`,`makefile`,`procfile`,`dockerfile`,`.gitignore`,`.gitattributes`,`.editorconfig`,`.npmrc`,`.nvmrc`]);function l(e){return String(e.webkitRelativePath||e.name).replaceAll(`\\`,`/`).replace(/^\/+/,``)}function u(e){let t=Array.from(e,e=>{let t=l(e);return t.includes(`/`)?t.split(`/`).filter(Boolean)[0]:``}).filter(Boolean);return t.length&&t.every(e=>e===t[0])?t[0].slice(0,120):`本地项目`}function d(e,t){let n=l(e),r=`${t}/`;return n.startsWith(r)?n.slice(r.length):n}function f(e){let t=l(e).split(`/`).filter(Boolean);if(!t.length||t.some(e=>o.has(e.toLowerCase()))||!e.size||e.size>r)return!1;let n=t.at(-1)?.toLowerCase()||``,i=n.includes(`.`)&&n.split(`.`).pop()||``;return e.type.startsWith(`text/`)||s.has(i)||c.has(n)}function p(e){let t=Array.from(e),n=u(t),r=[],o=0,s=0;for(let e of t)r.length>=a||!f(e)||o+e.size>i?s+=1:(r.push({file:e,name:d(e,n)}),o+=e.size);return{selected:r,skipped:s,bytes:o}}async function m(e){if(!t.selectedWorkspace)throw Error(`项目存储尚未初始化,请刷新页面后重试`);let r=t.projects.find(t=>t.id!==`pr_inbox`&&String(t.name).toLowerCase()===e.toLowerCase());r||(r=await n(`/api/projects`,{method:`POST`,body:JSON.stringify({workspaceId:t.selectedWorkspace.id,name:e,description:`从本地项目文件夹导入`})}),t.projects=[r,...t.projects]);let i=t.selectedProject?.id!==r.id;return t.selectedProject=r,localStorage.setItem(`multichat_project`,r.id),t.assets=await n(`/api/assets?projectId=`+encodeURIComponent(r.id)),t.selectedAssetIds=new Set(t.assets.map(e=>e.id)),await loadProjectControlData(),i&&(document.querySelector(`#settings.open`)&&closeSettings(),await newConversation()),renderTopbar(),renderFileContext(),r}async function h(r){let{selected:i,skipped:a}=p(r);if(!i.length){e(`文件夹中没有可导入的文本或源代码文件`,`error`);return}let o=u(r),s;try{s=await m(o)}catch(t){e(t.message||`无法创建项目`,`error`);return}let c=s.id,l=new Set((t.assets||[]).map(e=>String(e.name).toLowerCase())),d=i.filter(e=>!l.has(e.name.toLowerCase())),f=i.length-d.length;if(!d.length){e(`该文件夹中的可用文件已经在当前项目中`);return}e(`正在打开「${o}」并导入 ${d.length} 个文件…`);let h=[],g=[],_=0,v=async()=>{for(;_<d.length;){let e=d[_++];try{let t=await n(`/api/assets`,{method:`POST`,body:JSON.stringify({projectId:c,name:e.name,mimeType:e.file.type||`text/plain`,content:await e.file.text()})});h.push(t)}catch{g.push(e.name)}}};await Promise.all(Array.from({length:Math.min(4,d.length)},()=>v())),h.length&&(t.assets=[...h,...t.assets],t.selectedAssetIds=new Set([...t.selectedAssetIds,...h.map(e=>e.id)]));let y=[a?`跳过 ${a} 个构建产物、二进制或超限文件`:``,f?`忽略 ${f} 个重复文件`:``,g.length?`${g.length} 个文件导入失败`:``].filter(Boolean);renderTopbar(),renderFileContext(),e(`项目「${o}」已打开,新增 ${h.length} 个文件${y.length?`;${y.join(`,`)}`:``}`,g.length?`error`:void 0)}function g(){return new Promise(e=>{let t=document.createElement(`input`);t.type=`file`,t.multiple=!0,t.setAttribute(`webkitdirectory`,``),t.setAttribute(`directory`,``),t.className=`project-folder-input`,t.setAttribute(`aria-hidden`,`true`),document.body.appendChild(t);let n=()=>{t.remove(),e()};t.addEventListener(`change`,()=>{h(Array.from(t.files||[])).finally(n)},{once:!0}),t.addEventListener(`cancel`,n,{once:!0}),t.click()})}export{g as importProjectFolder,f as isProjectTextFile,u as projectFolderName,p as selectProjectFiles};
@@ -0,0 +1 @@
1
+ import{D as e,T as t,b as n,m as r}from"./index-CeU_ksnE.js";function i(){return(n.providers||[]).flatMap(e=>(e.models||[]).map(t=>({id:`${e.id}:${t}`,providerId:e.id,providerName:e.name||e.id,model:t})))}async function a(e,t,i){let a=performance.now();try{let o=await fetch(n.apiBase+`/v1/chat/completions`,{method:`POST`,headers:{"Content-Type":`application/json`,...r()},signal:i,body:JSON.stringify({providerId:e.providerId,model:e.model,messages:[{role:`user`,content:t}],stream:!1,temperature:n.params.temperature,max_tokens:n.params.max_tokens,top_p:n.params.top_p,workspaceId:n.selectedWorkspace?.id||null,projectId:n.selectedProject?.id||null,assetIds:[...n.selectedAssetIds],interactionId:`compare_${crypto.randomUUID()}`})}),s=await o.json();if(!o.ok){let e=typeof s?.error==`string`?s.error:s?.error?.message;throw Error(e||`HTTP ${o.status}`)}return{...e,status:`success`,text:s?.choices?.[0]?.message?.content||``,usage:s?.usage||{},elapsedMs:Math.round(performance.now()-a)}}catch(t){return t instanceof Error&&t.name===`AbortError`?{...e,status:`cancelled`,elapsedMs:Math.round(performance.now()-a)}:{...e,status:`error`,error:t instanceof Error?t.message:String(t),elapsedMs:Math.round(performance.now()-a)}}}async function o(e,r){if(!e.text)return;let i=(n.providers||[]).find(t=>t.id===e.providerId);i&&(n.selectedProvider=i,n.selectedModel=e.model),n.messages=[...n.messages,{role:`user`,content:r},{role:`assistant`,content:e.text,model:e.model,providerName:e.providerName,usage:e.usage,elapsedMs:e.elapsedMs,importedFromCompare:!0}];try{await ensureConversation(),await saveCurrentMessages()}catch{}renderTopbar(),renderContent(),renderConvList(),t(`已采用 ${e.model} 的结果`)}function s(){openSettings(`experiment`)}function c(){e(`[data-open-compare]`).forEach(e=>e.onclick=s)}export{o as adoptResult,i as compareTargets,a as executeTarget,s as openCompare,c as setupCompare};
@@ -0,0 +1 @@
1
+ import{C as e,E as t,T as n,b as r,f as i,i as a,w as o,y as s}from"./index-CeU_ksnE.js";function c(t=r.currentConvId,n=r.selectedProject?.id){let i=o(t,n);r.workflowScope=i,r.workflow=e(i)}async function l(){try{r.conversations=await i(`/api/conversations`),r.conversationsLoadFailed=!1}catch{r.conversations=[],r.conversationsLoadFailed=!0}}function u(){}async function d(e){let o=r.conversations.find(t=>t.id===e);if(o&&await showConfirm({title:`删除对话`,message:`删除「${o.title||`新对话`}」?`,confirmLabel:`删除`,danger:!0}))try{await i(`/api/conversations/`+encodeURIComponent(e),{method:`DELETE`}),r.conversations=r.conversations.filter(t=>t.id!==e),r.currentConvId===e&&(r.currentConvId=null,r.messages=[],c(null,r.selectedProject?.id),t(`#topbarTitle`).textContent=`新对话`,a(`chat`),location.hash!==`#/new`&&history.replaceState(null,``,`#/new`)),n(`对话已删除`)}catch(e){n(`删除对话失败:`+e.message,`error`)}}async function f(e){let a=r.conversations.find(t=>t.id===e);if(!a)return;let o=await showPrompt({title:`重命名任务`,label:`任务名称`,value:a.title||`新任务`,placeholder:`输入新的任务名称`,maxLength:120});if(o?.trim())try{let a=await i(`/api/conversations/`+encodeURIComponent(e),{method:`PUT`,body:JSON.stringify({title:o.trim()})});r.conversations=r.conversations.map(t=>t.id===e?{...t,...a}:t),r.currentConvId===e&&(t(`#topbarTitle`).textContent=a.title||o.trim()),n(`任务已重命名`)}catch(e){n(`重命名失败:`+e.message,`error`)}}async function p(){a(`home`),r.currentConvId=null,r.messages=[],c(null,r.selectedProject?.id),t(`#topbarTitle`).textContent=`新对话`,renderContent(),renderInspector(),requestAnimationFrame(()=>t(`#heroInput`)?.focus()),location.hash!==`#/home`&&history.pushState(null,``,`#/home`)}async function m(e){try{a(`chat`);let n=await i(`/api/conversations/`+encodeURIComponent(e));(n.workspaceId&&(!r.selectedWorkspace||r.selectedWorkspace.id!==n.workspaceId)||n.projectId&&(!r.selectedProject||r.selectedProject.id!==n.projectId))&&(r.selectedWorkspace=r.workspaces.find(e=>e.id===n.workspaceId)||r.selectedWorkspace,r.selectedWorkspace&&await loadProjects(),n.projectId&&(r.selectedProject=r.projects.find(e=>e.id===n.projectId)||r.selectedProject,r.selectedProject&&(localStorage.setItem(`multichat_project`,r.selectedProject.id),r.assets=await i(`/api/assets?projectId=`+encodeURIComponent(r.selectedProject.id)),r.selectedAssetIds=new Set(r.assets.map(e=>e.id))))),r.currentConvId=n.id,r.messages=(n.messages||[]).map(e=>({role:e.role,content:e.content,model:e.model})),c(n.id,n.projectId||r.selectedProject?.id),t(`#topbarTitle`).textContent=n.title||`对话`,renderTopbar(),renderContent(),g();let o=`#/chat/${encodeURIComponent(n.id)}`;location.hash!==o&&history.pushState(null,``,o)}catch(t){if(t?.status===404){r.conversations=r.conversations.filter(t=>t.id!==e),r.currentConvId===e&&(r.currentConvId=null,r.messages=[],c(null,r.selectedProject?.id)),a(`home`),history.replaceState(null,``,`#/home`),renderTopbar(),renderContent(),n(`该任务不存在或已被删除,已返回新任务页`,`error`);return}n(`打开对话失败:`+t.message,`error`)}}function h(e){try{return e<1024?e+` B`:e<1048576?(e/1024).toFixed(1)+` KB`:(e/1024/1024).toFixed(1)+` MB`}catch{return``}}function g(){renderContent(),renderInspector()}function _(){let e=r.selectedProject;if(e){if(e.defaultAgentId){let t=r.agents.find(t=>t.id===e.defaultAgentId);t&&(r.selectedAgent=t,s())}if(e.defaultProviderId&&e.defaultModel){let t=r.providers.find(t=>t.id===e.defaultProviderId);t&&(r.selectedProvider=t,r.selectedModel=e.defaultModel,localStorage.setItem(`multichat_lastModel`,t.id+`:`+e.defaultModel))}renderTopbar()}}export{c as activateWorkflow,_ as applyProjectDefaults,d as deleteConversation,h as fmtSize,l as loadConversations,p as newConversation,m as openConversation,f as renameConversation,u as renderConvList,g as renderFileContext};
@@ -0,0 +1 @@
1
+ import{b as e,f as t}from"./index-CeU_ksnE.js";async function n(){try{e.providers=await t(`/api/providers`)}catch{e.providers=[]}}async function r(){try{e.runtime=await t(`/api/runtime`)}catch{e.runtime=null}}async function i(){try{e.runs=await t(`/api/runs?limit=40`)}catch{e.runs=[]}}async function a(n=e.usageRange){e.usageRange=String(n||`30`),e.usageLoading=!0;try{let n=new Date().getTimezoneOffset();e.usage=await t(`/api/usage?range=${encodeURIComponent(e.usageRange)}&offset=${n}`)}catch{e.usage=null}finally{e.usageLoading=!1}}async function o(){try{e.capabilities=await t(`/api/capabilities`)}catch{e.capabilities=null}}async function s(){if(!e.selectedProject)e.memories=[],e.snapshots=[];else try{[e.memories,e.snapshots]=await Promise.all([t(`/api/memories?projectId=`+encodeURIComponent(e.selectedProject.id)),t(`/api/snapshots?projectId=`+encodeURIComponent(e.selectedProject.id))])}catch{e.memories=[],e.snapshots=[]}}async function c(){try{e.workspaces=await t(`/api/workspaces`);let n=localStorage.getItem(`multichat_workspace`);e.selectedWorkspace=e.workspaces.find(e=>e.id===n)||e.workspaces[0]||null,await l()}catch{e.workspaces=[],e.projects=[]}}async function l(){if(!e.selectedWorkspace)e.projects=[],e.selectedProject=null;else try{e.projects=await t(`/api/projects?workspaceId=`+encodeURIComponent(e.selectedWorkspace.id));let n=localStorage.getItem(`multichat_project`);e.selectedProject=e.projects.find(e=>e.id===n)||e.projects.find(t=>t.id===e.selectedWorkspace.defaultProjectId)||e.projects[0]||null,localStorage.setItem(`multichat_workspace`,e.selectedWorkspace.id),e.selectedProject&&localStorage.setItem(`multichat_project`,e.selectedProject.id),e.selectedProject&&(e.assets=await t(`/api/assets?projectId=`+encodeURIComponent(e.selectedProject.id)),e.selectedAssetIds=new Set(e.assets.map(e=>e.id)),applyProjectDefaults(),await s())}catch{e.projects=[],e.selectedProject=null,e.assets=[],e.selectedAssetIds=new Set}}async function u(){try{e.skills=await t(`/api/skills`)}catch{e.skills=[]}}async function d(){try{e.tools=await t(`/api/tools`)}catch{e.tools=[]}}async function f(){try{e.mcpServers=await t(`/api/mcp-servers`)}catch{e.mcpServers=[]}}async function p(){let n=e.selectedAgent?.id;try{e.agents=await t(`/api/agents`),n&&(e.selectedAgent=e.agents.find(e=>e.id===n)||null)}catch{e.agents=[],n&&(e.selectedAgent=null)}}async function m(){try{e.plugins=await t(`/api/plugins`)}catch{e.plugins=[]}}export{p as loadAgents,o as loadCapabilities,f as loadMcpServers,m as loadPlugins,s as loadProjectControlData,l as loadProjects,n as loadProviders,i as loadRuns,r as loadRuntime,u as loadSkills,d as loadTools,a as loadUsage,c as loadWorkspaces};