@leterhong/multichat 1.6.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 (214) hide show
  1. package/.agents/plugins/marketplace.json +32 -0
  2. package/.agents/plugins/plugins/developer-toolkit/.codex-plugin/plugin.json +24 -0
  3. package/.agents/plugins/plugins/developer-toolkit/skills/code-review/SKILL.md +9 -0
  4. package/.agents/plugins/plugins/developer-toolkit/skills/code-review/agents/openai.yaml +3 -0
  5. package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/SKILL.md +9 -0
  6. package/.agents/plugins/plugins/developer-toolkit/skills/release-notes/agents/openai.yaml +3 -0
  7. package/.agents/plugins/plugins/developer-toolkit/skills/security-review/SKILL.md +9 -0
  8. package/.agents/plugins/plugins/developer-toolkit/skills/security-review/agents/openai.yaml +3 -0
  9. package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/SKILL.md +9 -0
  10. package/.agents/plugins/plugins/developer-toolkit/skills/test-plan/agents/openai.yaml +3 -0
  11. package/.agents/plugins/plugins/writing-toolkit/.codex-plugin/plugin.json +24 -0
  12. package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/SKILL.md +9 -0
  13. package/.agents/plugins/plugins/writing-toolkit/skills/meeting-notes/agents/openai.yaml +3 -0
  14. package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/SKILL.md +9 -0
  15. package/.agents/plugins/plugins/writing-toolkit/skills/resume-tune/agents/openai.yaml +3 -0
  16. package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/SKILL.md +9 -0
  17. package/.agents/plugins/plugins/writing-toolkit/skills/weekly-report/agents/openai.yaml +3 -0
  18. package/.agents/skills/.gitkeep +1 -0
  19. package/.agents/skills/wanghong-handwritten-ppt/SKILL.md +172 -0
  20. package/.agents/skills/wanghong-handwritten-ppt/assets/HTML-PPT-LICENSE +21 -0
  21. package/.agents/skills/wanghong-handwritten-ppt/assets/NEAT-ANNOTATIONS-LICENSE +21 -0
  22. package/.agents/skills/wanghong-handwritten-ppt/assets/animations.css +138 -0
  23. package/.agents/skills/wanghong-handwritten-ppt/assets/base.css +150 -0
  24. package/.agents/skills/wanghong-handwritten-ppt/assets/deepseek-example.css +893 -0
  25. package/.agents/skills/wanghong-handwritten-ppt/assets/neat-annotations.css +152 -0
  26. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-cover.png +0 -0
  27. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-diagram.png +0 -0
  28. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-summary.png +0 -0
  29. package/.agents/skills/wanghong-handwritten-ppt/assets/preview-table.png +0 -0
  30. package/.agents/skills/wanghong-handwritten-ppt/assets/runtime.js +960 -0
  31. package/.agents/skills/wanghong-handwritten-ppt/assets/template.css +198 -0
  32. package/.agents/skills/wanghong-handwritten-ppt/evals/evals.json +20 -0
  33. package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/index.html +414 -0
  34. package/.agents/skills/wanghong-handwritten-ppt/examples/deepseek-v4-flash/style.css +897 -0
  35. package/.agents/skills/wanghong-handwritten-ppt/references/prompts.md +170 -0
  36. package/.agents/skills/wanghong-handwritten-ppt/references/style-guide.md +133 -0
  37. package/.agents/skills/wanghong-handwritten-ppt/scripts/check_deck.py +41 -0
  38. package/.agents/skills/wanghong-handwritten-ppt/scripts/prepare_render_html.py +137 -0
  39. package/.agents/skills/wanghong-handwritten-ppt/scripts/render.sh +110 -0
  40. package/.agents/skills/wanghong-handwritten-ppt/templates/deck.html +94 -0
  41. package/.agents/skills/wanghong-handwritten-ppt/tests/test_font_contract.py +73 -0
  42. package/LICENSE +21 -0
  43. package/README.md +186 -0
  44. package/backend/adapters/README.md +77 -0
  45. package/backend/adapters/anthropic.ts +115 -0
  46. package/backend/adapters/base.ts +47 -0
  47. package/backend/adapters/index.ts +41 -0
  48. package/backend/adapters/lmstudio.ts +7 -0
  49. package/backend/adapters/moonshot.ts +7 -0
  50. package/backend/adapters/ollama.ts +7 -0
  51. package/backend/adapters/openai-compatible.ts +51 -0
  52. package/backend/adapters/openai.ts +7 -0
  53. package/backend/adapters/qwen.ts +112 -0
  54. package/backend/adapters/wenxin.ts +136 -0
  55. package/backend/adapters/zhipu.ts +93 -0
  56. package/backend/dist/adapters/anthropic.js +130 -0
  57. package/backend/dist/adapters/anthropic.js.map +1 -0
  58. package/backend/dist/adapters/base.js +38 -0
  59. package/backend/dist/adapters/base.js.map +1 -0
  60. package/backend/dist/adapters/index.js +39 -0
  61. package/backend/dist/adapters/index.js.map +1 -0
  62. package/backend/dist/adapters/lmstudio.js +8 -0
  63. package/backend/dist/adapters/lmstudio.js.map +1 -0
  64. package/backend/dist/adapters/moonshot.js +8 -0
  65. package/backend/dist/adapters/moonshot.js.map +1 -0
  66. package/backend/dist/adapters/ollama.js +8 -0
  67. package/backend/dist/adapters/ollama.js.map +1 -0
  68. package/backend/dist/adapters/openai-compatible.js +47 -0
  69. package/backend/dist/adapters/openai-compatible.js.map +1 -0
  70. package/backend/dist/adapters/openai.js +8 -0
  71. package/backend/dist/adapters/openai.js.map +1 -0
  72. package/backend/dist/adapters/qwen.js +103 -0
  73. package/backend/dist/adapters/qwen.js.map +1 -0
  74. package/backend/dist/adapters/wenxin.js +114 -0
  75. package/backend/dist/adapters/wenxin.js.map +1 -0
  76. package/backend/dist/adapters/zhipu.js +86 -0
  77. package/backend/dist/adapters/zhipu.js.map +1 -0
  78. package/backend/dist/extensions/demo-mcp-server.js +75 -0
  79. package/backend/dist/extensions/demo-mcp-server.js.map +1 -0
  80. package/backend/dist/extensions/importer.js +828 -0
  81. package/backend/dist/extensions/importer.js.map +1 -0
  82. package/backend/dist/extensions/manager.js +1154 -0
  83. package/backend/dist/extensions/manager.js.map +1 -0
  84. package/backend/dist/lib/catalog.js +30 -0
  85. package/backend/dist/lib/catalog.js.map +1 -0
  86. package/backend/dist/lib/context.js +59 -0
  87. package/backend/dist/lib/context.js.map +1 -0
  88. package/backend/dist/lib/errors.js +75 -0
  89. package/backend/dist/lib/errors.js.map +1 -0
  90. package/backend/dist/lib/provider-store.js +116 -0
  91. package/backend/dist/lib/provider-store.js.map +1 -0
  92. package/backend/dist/lib/redact.js +11 -0
  93. package/backend/dist/lib/redact.js.map +1 -0
  94. package/backend/dist/lib/runtime-store.js +28 -0
  95. package/backend/dist/lib/runtime-store.js.map +1 -0
  96. package/backend/dist/lib/sqlite-store.js +218 -0
  97. package/backend/dist/lib/sqlite-store.js.map +1 -0
  98. package/backend/dist/lib/sse.js +46 -0
  99. package/backend/dist/lib/sse.js.map +1 -0
  100. package/backend/dist/lib/ssrf.js +131 -0
  101. package/backend/dist/lib/ssrf.js.map +1 -0
  102. package/backend/dist/lib/store.js +84 -0
  103. package/backend/dist/lib/store.js.map +1 -0
  104. package/backend/dist/lib/usage.js +219 -0
  105. package/backend/dist/lib/usage.js.map +1 -0
  106. package/backend/dist/lib/util.js +38 -0
  107. package/backend/dist/lib/util.js.map +1 -0
  108. package/backend/dist/lib/workspace-store.js +152 -0
  109. package/backend/dist/lib/workspace-store.js.map +1 -0
  110. package/backend/dist/mcp.js +429 -0
  111. package/backend/dist/mcp.js.map +1 -0
  112. package/backend/dist/routes/agents.js +51 -0
  113. package/backend/dist/routes/agents.js.map +1 -0
  114. package/backend/dist/routes/assistants.js +32 -0
  115. package/backend/dist/routes/assistants.js.map +1 -0
  116. package/backend/dist/routes/chat.js +697 -0
  117. package/backend/dist/routes/chat.js.map +1 -0
  118. package/backend/dist/routes/control-plane.js +200 -0
  119. package/backend/dist/routes/control-plane.js.map +1 -0
  120. package/backend/dist/routes/conversations.js +66 -0
  121. package/backend/dist/routes/conversations.js.map +1 -0
  122. package/backend/dist/routes/extensions-import.js +22 -0
  123. package/backend/dist/routes/extensions-import.js.map +1 -0
  124. package/backend/dist/routes/import.js +85 -0
  125. package/backend/dist/routes/import.js.map +1 -0
  126. package/backend/dist/routes/mcp-servers.js +56 -0
  127. package/backend/dist/routes/mcp-servers.js.map +1 -0
  128. package/backend/dist/routes/meta.js +37 -0
  129. package/backend/dist/routes/meta.js.map +1 -0
  130. package/backend/dist/routes/mock.js +28 -0
  131. package/backend/dist/routes/mock.js.map +1 -0
  132. package/backend/dist/routes/plugins.js +41 -0
  133. package/backend/dist/routes/plugins.js.map +1 -0
  134. package/backend/dist/routes/providers.js +197 -0
  135. package/backend/dist/routes/providers.js.map +1 -0
  136. package/backend/dist/routes/runs.js +57 -0
  137. package/backend/dist/routes/runs.js.map +1 -0
  138. package/backend/dist/routes/skills.js +73 -0
  139. package/backend/dist/routes/skills.js.map +1 -0
  140. package/backend/dist/routes/tools.js +40 -0
  141. package/backend/dist/routes/tools.js.map +1 -0
  142. package/backend/dist/routes/usage.js +14 -0
  143. package/backend/dist/routes/usage.js.map +1 -0
  144. package/backend/dist/routes/workspaces.js +158 -0
  145. package/backend/dist/routes/workspaces.js.map +1 -0
  146. package/backend/dist/runtime/agent.js +518 -0
  147. package/backend/dist/runtime/agent.js.map +1 -0
  148. package/backend/dist/server.js +103 -0
  149. package/backend/dist/server.js.map +1 -0
  150. package/backend/dist/types.js +3 -0
  151. package/backend/dist/types.js.map +1 -0
  152. package/backend/extensions/demo-mcp-server.js +70 -0
  153. package/backend/extensions/importer.ts +709 -0
  154. package/backend/extensions/manager.ts +1055 -0
  155. package/backend/lib/catalog.ts +28 -0
  156. package/backend/lib/context.ts +61 -0
  157. package/backend/lib/errors.ts +78 -0
  158. package/backend/lib/provider-store.ts +116 -0
  159. package/backend/lib/redact.ts +9 -0
  160. package/backend/lib/runtime-store.ts +38 -0
  161. package/backend/lib/sqlite-store.ts +220 -0
  162. package/backend/lib/sse.ts +42 -0
  163. package/backend/lib/ssrf.ts +120 -0
  164. package/backend/lib/store.ts +79 -0
  165. package/backend/lib/usage.ts +212 -0
  166. package/backend/lib/util.ts +31 -0
  167. package/backend/lib/workspace-store.ts +150 -0
  168. package/backend/mcp.ts +417 -0
  169. package/backend/package.json +34 -0
  170. package/backend/prompts.json +82 -0
  171. package/backend/routes/agents.ts +55 -0
  172. package/backend/routes/assistants.ts +30 -0
  173. package/backend/routes/chat.ts +660 -0
  174. package/backend/routes/control-plane.ts +165 -0
  175. package/backend/routes/conversations.ts +62 -0
  176. package/backend/routes/extensions-import.ts +15 -0
  177. package/backend/routes/import.ts +72 -0
  178. package/backend/routes/mcp-servers.ts +36 -0
  179. package/backend/routes/meta.ts +38 -0
  180. package/backend/routes/mock.ts +25 -0
  181. package/backend/routes/plugins.ts +27 -0
  182. package/backend/routes/providers.ts +166 -0
  183. package/backend/routes/runs.ts +55 -0
  184. package/backend/routes/skills.ts +52 -0
  185. package/backend/routes/tools.ts +40 -0
  186. package/backend/routes/usage.ts +13 -0
  187. package/backend/routes/workspaces.ts +124 -0
  188. package/backend/runtime/agent.ts +474 -0
  189. package/backend/server.ts +106 -0
  190. package/backend/types.ts +126 -0
  191. package/bin/multichat.mjs +535 -0
  192. package/frontend/dist/assets/agentPicker-C39VzmYa.js +13 -0
  193. package/frontend/dist/assets/compare-BpaGD32J.js +1 -0
  194. package/frontend/dist/assets/conversations-COuLmg99.js +7 -0
  195. package/frontend/dist/assets/data-ZH7Dux1K.js +1 -0
  196. package/frontend/dist/assets/extensionImport-CnTWKtWm.js +28 -0
  197. package/frontend/dist/assets/importExport-B3mB-9zs.js +70 -0
  198. package/frontend/dist/assets/index-CASVCOUO.js +34 -0
  199. package/frontend/dist/assets/index-DSvbjNLc.css +1 -0
  200. package/frontend/dist/assets/init-ZPK483vB.js +1 -0
  201. package/frontend/dist/assets/jsx-runtime-ZSsbr0oW.js +9 -0
  202. package/frontend/dist/assets/modal-CgHocv3n.js +2 -0
  203. package/frontend/dist/assets/modelPicker-EUmmVnbj.js +14 -0
  204. package/frontend/dist/assets/pluginsUI-B6jGUQkx.js +16 -0
  205. package/frontend/dist/assets/render-DgAg1K8A.js +1 -0
  206. package/frontend/dist/assets/send-BJhZ_5YA.js +14 -0
  207. package/frontend/dist/assets/settings-DdnYwZl-.js +2 -0
  208. package/frontend/dist/assets/shell-DFV2khQl.js +1 -0
  209. package/frontend/dist/assets/workbench-BY2J4vWC.js +41 -0
  210. package/frontend/dist/icon-maskable.svg +4 -0
  211. package/frontend/dist/icon.svg +4 -0
  212. package/frontend/dist/index.html +21 -0
  213. package/frontend/dist/manifest.webmanifest +26 -0
  214. package/package.json +58 -0
package/backend/mcp.ts ADDED
@@ -0,0 +1,417 @@
1
+ 'use strict';
2
+
3
+ // MCP client used by MultiChat's runtime and extension manager.
4
+ // STDIO follows the MCP transport contract: one JSON-RPC message per line.
5
+ // Streamable HTTP is supported for remote servers. Connections are cached by
6
+ // server configuration so tools/list and tools/call share one session.
7
+ const { spawn } = require('child_process');
8
+ const { redactSecrets } = require('./lib/redact');
9
+ const { readPackageVersion } = require('./lib/catalog');
10
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
11
+ import type { JsonRecord, RequestOptions } from './types';
12
+
13
+ interface PendingRequest {
14
+ resolve: (value: any) => void;
15
+ reject: (reason?: any) => void;
16
+ timer: NodeJS.Timeout;
17
+ signal?: AbortSignal;
18
+ onAbort?: () => void;
19
+ }
20
+
21
+ interface McpClientConfig {
22
+ command?: string;
23
+ args?: string[];
24
+ cwd?: string;
25
+ env?: Record<string, string>;
26
+ url?: string;
27
+ headers?: Record<string, string>;
28
+ fetchImpl?: typeof fetch;
29
+ }
30
+
31
+ const PROTOCOL_VERSION = '2025-06-18';
32
+ const REQUEST_TIMEOUT_MS = 60_000;
33
+ const MAX_STDOUT_BUFFER = 4 * 1024 * 1024;
34
+ const MAX_HTTP_BODY = 8 * 1024 * 1024;
35
+
36
+ function timeoutError(label: string) { return new Error(`${label} timed out`); }
37
+
38
+ function childEnvironment(extra?: Record<string, string>): NodeJS.ProcessEnv {
39
+ // Arbitrary MCP packages do not inherit user profiles, credential helpers or
40
+ // NODE_PATH. A server that truly needs a value must declare it explicitly in
41
+ // its own MCP configuration.
42
+ const keep = ['PATH', 'Path', 'PATHEXT', 'SystemRoot', 'WINDIR', 'ComSpec', 'TEMP', 'TMP'];
43
+ const base: NodeJS.ProcessEnv = {};
44
+ for (const key of keep) if (process.env[key] !== undefined) base[key] = process.env[key];
45
+ return { ...base, ...(extra || {}) };
46
+ }
47
+
48
+ async function readLimitedText(response: Response, limit = MAX_HTTP_BODY) {
49
+ const declared = Number(response.headers.get('content-length') || 0);
50
+ if (declared > limit) throw new Error(`MCP HTTP response exceeds ${limit} bytes`);
51
+ if (!response.body) return '';
52
+ const reader = response.body.getReader();
53
+ const decoder = new TextDecoder();
54
+ let total = 0;
55
+ let text = '';
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ total += value.byteLength;
60
+ if (total > limit) { await reader.cancel(); throw new Error(`MCP HTTP response exceeds ${limit} bytes`); }
61
+ text += decoder.decode(value, { stream: true });
62
+ }
63
+ return text + decoder.decode();
64
+ }
65
+
66
+ class McpStdioClient {
67
+ command: string;
68
+ args: string[];
69
+ cwd?: string;
70
+ env: Record<string, string>;
71
+ proc: ChildProcessWithoutNullStreams | null;
72
+ buffer: string;
73
+ nextId: number;
74
+ pending: Map<number, PendingRequest>;
75
+ starting: Promise<void> | null;
76
+ ready: boolean;
77
+ protocolVersion: string;
78
+ serverInfo: JsonRecord | null;
79
+ instructions: string;
80
+
81
+ constructor(command: string, args?: string[], cwd?: string, env?: Record<string, string>) {
82
+ this.command = command;
83
+ this.args = Array.isArray(args) ? args : [];
84
+ this.cwd = cwd;
85
+ this.env = env || {};
86
+ this.proc = null;
87
+ this.buffer = '';
88
+ this.nextId = 1;
89
+ this.pending = new Map();
90
+ this.starting = null;
91
+ this.ready = false;
92
+ this.protocolVersion = PROTOCOL_VERSION;
93
+ this.serverInfo = null;
94
+ this.instructions = '';
95
+ }
96
+
97
+ async ensureStarted(options: RequestOptions = {}) {
98
+ if (this.ready && this.proc) return;
99
+ if (this.starting) return this.starting;
100
+ this.starting = this._start(options);
101
+ try { await this.starting; }
102
+ finally { this.starting = null; }
103
+ }
104
+
105
+ async _start(options: RequestOptions = {}) {
106
+ if (!this.command) throw new Error('MCP stdio command is required');
107
+ this.proc = spawn(this.command, this.args, {
108
+ cwd: this.cwd || undefined,
109
+ env: childEnvironment(this.env),
110
+ windowsHide: true,
111
+ stdio: ['pipe', 'pipe', 'pipe'],
112
+ });
113
+ this.proc.stdout.setEncoding('utf8');
114
+ this.proc.stdout.on('data', (chunk: string) => this._onData(chunk));
115
+ this.proc.stderr.setEncoding('utf8');
116
+ this.proc.stderr.on('data', (chunk: string) => {
117
+ const text = String(chunk).trim();
118
+ if (text) console.error('[mcp stderr]', redactSecrets(text).slice(0, 500));
119
+ });
120
+ this.proc.on('error', (error: Error) => this._disconnect(error));
121
+ this.proc.on('exit', (code: number | null) => this._disconnect(new Error(`MCP server exited with code ${code}`)));
122
+
123
+ try {
124
+ const initialized = await this._request('initialize', {
125
+ protocolVersion: PROTOCOL_VERSION,
126
+ capabilities: {},
127
+ clientInfo: { name: 'MultiChat', version: readPackageVersion() },
128
+ }, options);
129
+ this.protocolVersion = initialized?.protocolVersion || PROTOCOL_VERSION;
130
+ this.serverInfo = initialized?.serverInfo || null;
131
+ this.instructions = initialized?.instructions || '';
132
+ this._notify('notifications/initialized', {});
133
+ this.ready = true;
134
+ } catch (error) {
135
+ if (this.proc) this.proc.kill();
136
+ this._disconnect(error);
137
+ throw error;
138
+ }
139
+ }
140
+
141
+ _disconnect(error: Error) {
142
+ this.ready = false;
143
+ this.proc = null;
144
+ this.buffer = '';
145
+ for (const pending of this.pending.values()) {
146
+ clearTimeout(pending.timer);
147
+ if (pending.signal && pending.onAbort) pending.signal.removeEventListener('abort', pending.onAbort);
148
+ pending.reject(error);
149
+ }
150
+ this.pending.clear();
151
+ }
152
+
153
+ _write(message: JsonRecord) {
154
+ if (!this.proc?.stdin?.writable) throw new Error('MCP server is not running');
155
+ this.proc.stdin.write(JSON.stringify(message) + '\n');
156
+ }
157
+
158
+ _notify(method: string, params: JsonRecord) { this._write({ jsonrpc: '2.0', method, params }); }
159
+
160
+ _request(method: string, params: JsonRecord, options: RequestOptions = {}): Promise<any> {
161
+ const id = this.nextId++;
162
+ return new Promise((resolve, reject) => {
163
+ const signal = options.signal;
164
+ const finish = (callback: (value: any) => void, value: any) => {
165
+ const pending = this.pending.get(id);
166
+ if (!pending) return;
167
+ this.pending.delete(id);
168
+ clearTimeout(pending.timer);
169
+ if (pending.signal && pending.onAbort) pending.signal.removeEventListener('abort', pending.onAbort);
170
+ callback(value);
171
+ };
172
+ const timer = setTimeout(() => {
173
+ finish(reject, timeoutError(`MCP ${method}`));
174
+ }, REQUEST_TIMEOUT_MS);
175
+ const onAbort = () => {
176
+ try { this._notify('notifications/cancelled', { requestId: id, reason: 'cancelled by caller' }); } catch {}
177
+ const error = new Error('MCP request cancelled'); error.name = 'AbortError';
178
+ finish(reject, error);
179
+ };
180
+ this.pending.set(id, { resolve, reject, timer, signal, onAbort });
181
+ if (signal) {
182
+ if (signal.aborted) { onAbort(); return; }
183
+ signal.addEventListener('abort', onAbort, { once: true });
184
+ }
185
+ try { this._write({ jsonrpc: '2.0', id, method, params }); }
186
+ catch (error) {
187
+ finish(reject, error);
188
+ }
189
+ });
190
+ }
191
+
192
+ _onData(chunk: string) {
193
+ this.buffer += chunk;
194
+ if (this.buffer.length > MAX_STDOUT_BUFFER) {
195
+ const error = new Error('MCP stdout buffer limit exceeded');
196
+ if (this.proc) this.proc.kill();
197
+ this._disconnect(error);
198
+ return;
199
+ }
200
+ while (true) {
201
+ const newline = this.buffer.indexOf('\n');
202
+ if (newline < 0) break;
203
+ const line = this.buffer.slice(0, newline).trim();
204
+ this.buffer = this.buffer.slice(newline + 1);
205
+ if (!line) continue;
206
+ let message;
207
+ try { message = JSON.parse(line); }
208
+ catch {
209
+ console.error('[mcp] ignored non-JSON stdout line:', redactSecrets(line).slice(0, 200));
210
+ continue;
211
+ }
212
+ if (message.id === undefined || message.id === null) continue;
213
+ const pending = this.pending.get(message.id);
214
+ if (!pending) continue;
215
+ this.pending.delete(message.id);
216
+ clearTimeout(pending.timer);
217
+ if (pending.signal && pending.onAbort) pending.signal.removeEventListener('abort', pending.onAbort);
218
+ if (message.error) pending.reject(new Error(message.error.message || 'MCP error'));
219
+ else pending.resolve(message.result);
220
+ }
221
+ }
222
+
223
+ async listTools(options: RequestOptions = {}) {
224
+ await this.ensureStarted(options);
225
+ const tools: JsonRecord[] = [];
226
+ let cursor: string | null | undefined;
227
+ do {
228
+ const result = await this._request('tools/list', cursor ? { cursor } : {}, options);
229
+ if (Array.isArray(result?.tools)) tools.push(...result.tools);
230
+ cursor = result?.nextCursor || null;
231
+ } while (cursor);
232
+ return tools;
233
+ }
234
+
235
+ async callTool(name: string, args: JsonRecord, options: RequestOptions = {}) {
236
+ await this.ensureStarted(options);
237
+ return this._request('tools/call', { name, arguments: args || {} }, options);
238
+ }
239
+
240
+ close() {
241
+ if (this.proc) this.proc.kill();
242
+ this._disconnect(new Error('MCP client closed'));
243
+ }
244
+ }
245
+
246
+ class McpHttpClient {
247
+ url: string;
248
+ headers: Record<string, string>;
249
+ fetchImpl: typeof fetch;
250
+ sessionId: string | null;
251
+ nextId: number;
252
+ ready: boolean;
253
+ starting: Promise<void> | null;
254
+ protocolVersion: string;
255
+ serverInfo: JsonRecord | null;
256
+ instructions: string;
257
+
258
+ constructor(url: string, headers?: Record<string, string>, fetchImpl?: typeof fetch) {
259
+ this.url = url;
260
+ this.headers = headers || {};
261
+ this.fetchImpl = fetchImpl || fetch;
262
+ this.sessionId = null;
263
+ this.nextId = 1;
264
+ this.ready = false;
265
+ this.starting = null;
266
+ this.protocolVersion = PROTOCOL_VERSION;
267
+ this.serverInfo = null;
268
+ this.instructions = '';
269
+ }
270
+
271
+ async ensureStarted(options: RequestOptions = {}) {
272
+ if (this.ready) return;
273
+ if (this.starting) return this.starting;
274
+ this.starting = (async () => {
275
+ const initialized = await this._request('initialize', {
276
+ protocolVersion: PROTOCOL_VERSION,
277
+ capabilities: {},
278
+ clientInfo: { name: 'MultiChat', version: readPackageVersion() },
279
+ }, options);
280
+ const negotiated = initialized?.protocolVersion;
281
+ if (negotiated !== undefined && (typeof negotiated !== 'string' || !negotiated.trim())) {
282
+ throw new Error('MCP server returned an invalid protocolVersion');
283
+ }
284
+ this.protocolVersion = negotiated || PROTOCOL_VERSION;
285
+ this.serverInfo = initialized?.serverInfo || null;
286
+ this.instructions = initialized?.instructions || '';
287
+ await this._notify('notifications/initialized', {}, options);
288
+ this.ready = true;
289
+ })();
290
+ try { await this.starting; }
291
+ finally { this.starting = null; }
292
+ }
293
+
294
+ async _post(message: JsonRecord, signal?: AbortSignal): Promise<any> {
295
+ const headers: Record<string, string> = {
296
+ ...this.headers,
297
+ Accept: 'application/json, text/event-stream',
298
+ 'Content-Type': 'application/json',
299
+ 'MCP-Protocol-Version': this.protocolVersion,
300
+ };
301
+ if (this.sessionId) headers['MCP-Session-Id'] = this.sessionId;
302
+ const timeoutSignal = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
303
+ const requestSignal = signal && AbortSignal.any ? AbortSignal.any([timeoutSignal, signal]) : (signal || timeoutSignal);
304
+ const response = await this.fetchImpl(this.url, {
305
+ method: 'POST',
306
+ headers,
307
+ body: JSON.stringify(message),
308
+ signal: requestSignal,
309
+ });
310
+ if (!response.ok) throw new Error(`MCP HTTP ${response.status}: ${(await readLimitedText(response, 64 * 1024)).slice(0, 300)}`);
311
+ const sessionId = response.headers.get('mcp-session-id');
312
+ if (sessionId) this.sessionId = sessionId;
313
+ if (response.status === 202 || !response.body) return null;
314
+ const text = await readLimitedText(response);
315
+ if (!text.trim()) return null;
316
+ const contentType = response.headers.get('content-type') || '';
317
+ if (contentType.includes('text/event-stream')) {
318
+ const messages: JsonRecord[] = [];
319
+ for (const block of text.split(/\r?\n\r?\n/)) {
320
+ const data = block.split(/\r?\n/).filter(line => line.startsWith('data:')).map(line => line.slice(5).trim()).join('\n');
321
+ if (!data) continue;
322
+ try { messages.push(JSON.parse(data)); } catch {}
323
+ }
324
+ return messages.find(item => item.id === message.id) || messages.at(-1) || null;
325
+ }
326
+ return JSON.parse(text);
327
+ }
328
+
329
+ async _notify(method: string, params: JsonRecord, options: RequestOptions = {}) { await this._post({ jsonrpc: '2.0', method, params }, options.signal); }
330
+
331
+ async _request(method: string, params: JsonRecord, options: RequestOptions = {}): Promise<any> {
332
+ const id = this.nextId++;
333
+ const message = await this._post({ jsonrpc: '2.0', id, method, params }, options.signal);
334
+ if (!message) throw new Error(`MCP ${method} returned no response`);
335
+ if (message.error) throw new Error(message.error.message || 'MCP error');
336
+ return message.result;
337
+ }
338
+
339
+ async listTools(options: RequestOptions = {}) {
340
+ await this.ensureStarted(options);
341
+ const tools: JsonRecord[] = [];
342
+ let cursor: string | null | undefined;
343
+ do {
344
+ const result = await this._request('tools/list', cursor ? { cursor } : {}, options);
345
+ if (Array.isArray(result?.tools)) tools.push(...result.tools);
346
+ cursor = result?.nextCursor || null;
347
+ } while (cursor);
348
+ return tools;
349
+ }
350
+
351
+ async callTool(name: string, args: JsonRecord, options: RequestOptions = {}) {
352
+ await this.ensureStarted(options);
353
+ return this._request('tools/call', { name, arguments: args || {} }, options);
354
+ }
355
+
356
+ close() { this.ready = false; this.sessionId = null; }
357
+ }
358
+
359
+ const MCP_IDLE_TIMEOUT_MS = Math.max(30_000, Number(process.env.MCP_IDLE_TIMEOUT_MS || 5 * 60_000));
360
+ type CachedClient = { client: McpStdioClient | McpHttpClient; timer: NodeJS.Timeout };
361
+ const clientCache = new Map<string, CachedClient>();
362
+
363
+ function touchClient(key: string, cached: CachedClient) {
364
+ clearTimeout(cached.timer);
365
+ cached.timer = setTimeout(() => {
366
+ const current = clientCache.get(key);
367
+ if (current !== cached) return;
368
+ current.client.close();
369
+ clientCache.delete(key);
370
+ }, MCP_IDLE_TIMEOUT_MS);
371
+ cached.timer.unref?.();
372
+ }
373
+
374
+ function getMcpClient(config: McpClientConfig) {
375
+ if (!config || (!config.command && !config.url)) return null;
376
+ const key = JSON.stringify({ ...config, fetchImpl: undefined });
377
+ if (!clientCache.has(key)) {
378
+ const client = config.url
379
+ ? new McpHttpClient(config.url, config.headers, config.fetchImpl)
380
+ : new McpStdioClient(config.command, config.args, config.cwd, config.env);
381
+ const cached = { client, timer: setTimeout(() => {}, MCP_IDLE_TIMEOUT_MS) };
382
+ clientCache.set(key, cached);
383
+ touchClient(key, cached);
384
+ }
385
+ const cached = clientCache.get(key);
386
+ touchClient(key, cached);
387
+ return cached.client;
388
+ }
389
+
390
+ function closeMcpClient(config: McpClientConfig) {
391
+ if (!config) return;
392
+ const key = JSON.stringify({ ...config, fetchImpl: undefined });
393
+ const cached = clientCache.get(key);
394
+ if (cached) {
395
+ clearTimeout(cached.timer);
396
+ cached.client.close();
397
+ }
398
+ clientCache.delete(key);
399
+ }
400
+
401
+ function closeAllMcpClients() {
402
+ for (const cached of clientCache.values()) {
403
+ clearTimeout(cached.timer);
404
+ cached.client.close();
405
+ }
406
+ clientCache.clear();
407
+ }
408
+
409
+ module.exports = {
410
+ McpClient: McpStdioClient,
411
+ McpStdioClient,
412
+ McpHttpClient,
413
+ getMcpClient,
414
+ closeMcpClient,
415
+ closeAllMcpClients,
416
+ childEnvironment,
417
+ };
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "multichat-backend",
3
+ "version": "1.6.0",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "description": "MultiChat backend — Express-based multi-model Agent platform",
7
+ "main": "dist/server.js",
8
+ "scripts": {
9
+ "build": "tsc -p tsconfig.build.json",
10
+ "start": "node dist/server.js",
11
+ "dev": "tsx watch server.ts",
12
+ "test": "tsc -p tsconfig.test.json && node --test .test-dist/tests/*.test.js",
13
+ "lint": "eslint . --ext .ts,.cjs --max-warnings 99999",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "engines": {
17
+ "node": ">=22.13.0"
18
+ },
19
+ "dependencies": {
20
+ "@leterhong/multichat": "file:..",
21
+ "adm-zip": "^0.6.0",
22
+ "cors": "^2.8.5",
23
+ "express": "^4.19.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/express": "^4.17.21",
27
+ "@types/node": "^22.10.0",
28
+ "@typescript-eslint/eslint-plugin": "^8.18.0",
29
+ "@typescript-eslint/parser": "^8.18.0",
30
+ "eslint": "^8.57.1",
31
+ "tsx": "^4.19.2",
32
+ "typescript": "^5.6.3"
33
+ }
34
+ }
@@ -0,0 +1,82 @@
1
+ [
2
+ {
3
+ "id": "code-assistant",
4
+ "title": "代码助手",
5
+ "category": "编程",
6
+ "icon": "💻",
7
+ "systemPrompt": "你是一位资深的全栈开发工程师,精通多种编程语言和框架。请用简洁、专业的方式回答编程问题,优先给出可运行的代码示例,必要时解释关键逻辑和设计思路。代码注释使用中文。",
8
+ "inputPlaceholder": "描述你想实现的功能或遇到的 bug…"
9
+ },
10
+ {
11
+ "id": "code-reviewer",
12
+ "title": "代码审查助手",
13
+ "category": "编程",
14
+ "icon": "🔍",
15
+ "systemPrompt": "你是一位严格的代码审查专家。用户会粘贴代码,请你从以下维度逐一审查:1) 潜在 Bug 和逻辑错误 2) 安全漏洞(注入、XSS、敏感信息泄露等)3) 性能问题 4) 代码风格和可读性 5) 最佳实践偏离。每个问题标注严重等级(🔴 严重 / 🟡 建议 / 🟢 优化),并给出修复建议和修改后的代码片段。如果代码质量优秀,也请指出亮点。",
16
+ "inputPlaceholder": "粘贴需要审查的代码…"
17
+ },
18
+ {
19
+ "id": "translator",
20
+ "title": "翻译专家",
21
+ "category": "翻译",
22
+ "icon": "🌐",
23
+ "systemPrompt": "你是一位专业翻译,精通中英双语互译。翻译时做到信达雅:准确传达原文含义,表达自然流畅,兼顾语言美感。如遇专业术语,请在译文中用括号标注原文。用户发送任何文本,你直接给出翻译结果,无需多余解释。",
24
+ "inputPlaceholder": "输入需要翻译的文本…"
25
+ },
26
+ {
27
+ "id": "email-polisher",
28
+ "title": "英文邮件润色",
29
+ "category": "翻译",
30
+ "icon": "✉️",
31
+ "systemPrompt": "你是一位英文商务邮件润色专家。用户会提供中文邮件草稿或英文初稿,请你:1) 润色为地道、专业的商务英文 2) 根据邮件场景(正式/半正式/内部沟通)调整语气 3) 优化措辞使表达更简洁有力 4) 修正语法和用词错误。输出格式:先给出润色后的完整邮件,再用简短注释说明主要改动。如用户未指定收件人关系,默认为正式商务场景。",
32
+ "inputPlaceholder": "粘贴需要润色的邮件内容(中英文均可)…"
33
+ },
34
+ {
35
+ "id": "copywriter",
36
+ "title": "文案策划",
37
+ "category": "写作",
38
+ "icon": "✍️",
39
+ "systemPrompt": "你是一位顶尖文案策划,擅长品牌文案、社交媒体内容、营销话术和活动策划。请根据用户的需求,提供有创意、有感染力的文案方案,并给出适用于不同平台的版本建议。",
40
+ "inputPlaceholder": "描述你的文案需求,如产品、场景、目标受众…"
41
+ },
42
+ {
43
+ "id": "xiaohongshu-copy",
44
+ "title": "小红书爆款文案",
45
+ "category": "写作",
46
+ "icon": "📕",
47
+ "systemPrompt": "你是一位小红书爆款文案专家,深谙平台推荐算法和用户心理。请根据用户提供的主题生成小红书笔记文案,要求:1) 标题:包含数字、对比、悬念或情绪词,15字以内抓眼球 2) 正文:口语化、场景化,多用短句和换行,穿插emoji增加阅读节奏 3) 结尾设置互动钩子(提问/投票/征集)4) 生成5-8个相关话题标签 5) 提供3个备选标题供选择。语气要真诚不做作,像朋友分享而非广告。",
48
+ "inputPlaceholder": "输入你想写的小红书笔记主题,如:平价护肤推荐、职场穿搭…"
49
+ },
50
+ {
51
+ "id": "data-analyst",
52
+ "title": "数据分析",
53
+ "category": "分析",
54
+ "icon": "📊",
55
+ "systemPrompt": "你是一位数据分析专家,擅长数据清洗、统计分析、可视化建议和业务洞察。请用结构化的方式回答:先明确问题和数据现状,再给出分析思路和具体方法,最后提供可操作的结论和建议。涉及代码时优先使用 Python(pandas / matplotlib / plotly)。",
56
+ "inputPlaceholder": "描述你的数据分析问题或粘贴数据…"
57
+ },
58
+ {
59
+ "id": "weekly-report",
60
+ "title": "周报生成器",
61
+ "category": "工作",
62
+ "icon": "📋",
63
+ "systemPrompt": "你是一位职场周报撰写助手。用户会提供本周工作的零散记录(要点、完成的任务、遇到的问题等),请你整理为结构清晰的周报,格式如下:\n\n## 本周完成\n- 按优先级排列,每项用一句话概括成果(用动词开头)\n\n## 进行中\n- 正在推进的事项及当前进度\n\n## 下周计划\n- 基于本周进展推导下一步\n\n## 需要协助\n- 阻塞项和资源需求\n\n要求:语言精练专业,突出成果而非过程,用数据说话。如果用户信息模糊,合理推断补充但标注[需确认]。",
64
+ "inputPlaceholder": "输入本周做了什么(随意写,越碎越好,我来整理)…"
65
+ },
66
+ {
67
+ "id": "interview-coach",
68
+ "title": "面试教练",
69
+ "category": "职场",
70
+ "icon": "🎯",
71
+ "systemPrompt": "你是一位资深面试教练,熟悉互联网、金融、咨询等行业的面试流程和常见问题。请根据用户提供的职位信息和背景,模拟真实面试场景提问,对用户的回答给出专业点评和改进建议,帮助用户提升面试表现。",
72
+ "inputPlaceholder": "输入你准备面试的职位和公司…"
73
+ },
74
+ {
75
+ "id": "study-notes",
76
+ "title": "学习笔记整理",
77
+ "category": "学习",
78
+ "icon": "📝",
79
+ "systemPrompt": "你是一位学习笔记整理专家。用户会提供课堂笔记、读书摘录、会议记录等原始内容,请你:1) 提取核心概念和关键信息 2) 用结构化格式重新组织(标题、列表、表格、思维导图文字版)3) 标注知识点之间的逻辑关系(因果、对比、递进)4) 对重要概念给出简明解释 5) 生成3-5个复习自测问题。输出要求层次分明、一目了然,方便后续复习。",
80
+ "inputPlaceholder": "粘贴你的原始笔记、课堂记录或读书摘录…"
81
+ }
82
+ ]
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ // ── Agents CRUD ────────────────────────────────────────────────────────
3
+ // 注:/api/agents/:id/chat(流式工具循环)位于 routes/chat.js,
4
+ // 与 /v1/chat/completions 共用 adapters 与 agent runtime,便于统一维护。
5
+ const ctx = require('../lib/context');
6
+
7
+ function normalizeCapabilityRefs(agent, migrateLegacyTools = true) {
8
+ const builtinToolIds = new Set(ctx.store.read(ctx.SKILL_FILE, [])
9
+ .filter(item => !['prompt', 'mcp'].includes(item.type))
10
+ .map(item => item.id));
11
+ const legacyIds = Array.isArray(agent.skillIds) ? agent.skillIds.map(String) : [];
12
+ const explicitTools = Array.isArray(agent.toolIds) ? agent.toolIds.map(String) : [];
13
+ const legacyTools = migrateLegacyTools ? legacyIds.filter(id => builtinToolIds.has(id)) : [];
14
+ agent.toolIds = [...new Set([...explicitTools, ...legacyTools])];
15
+ agent.skillIds = legacyIds.filter(id => !builtinToolIds.has(id));
16
+ agent.skillRefs = Array.isArray(agent.skillRefs) ? agent.skillRefs.map(String) : [];
17
+ agent.mcpServerIds = Array.isArray(agent.mcpServerIds) ? agent.mcpServerIds.map(String) : [];
18
+ return agent;
19
+ }
20
+
21
+ module.exports = function registerAgents(app) {
22
+ app.get('/api/agents', (req, res) => res.json(ctx.store.read(ctx.AGENT_FILE, [])));
23
+ app.get('/api/agents/:id', (req, res) => {
24
+ const agent = ctx.store.read(ctx.AGENT_FILE, []).find(x => x.id === req.params.id);
25
+ if (!agent) return res.status(404).json({ error: 'not found' });
26
+ res.json(agent);
27
+ });
28
+ app.post('/api/agents', (req, res) => {
29
+ const agents = ctx.store.read(ctx.AGENT_FILE, []);
30
+ const agent = normalizeCapabilityRefs(
31
+ { id: 'ag_' + Date.now().toString(36), skillIds: [], skillRefs: [], toolIds: [], mcpServerIds: [], systemPrompt: '', createdAt: new Date().toISOString(), ...req.body },
32
+ !Array.isArray(req.body?.toolIds),
33
+ );
34
+ agents.push(agent);
35
+ ctx.store.write(ctx.AGENT_FILE, agents);
36
+ res.json(agent);
37
+ });
38
+ app.put('/api/agents/:id', (req, res) => {
39
+ const agents = ctx.store.read(ctx.AGENT_FILE, []);
40
+ const idx = agents.findIndex(a => a.id === req.params.id);
41
+ if (idx < 0) return res.status(404).json({ error: 'not found' });
42
+ agents[idx] = normalizeCapabilityRefs(
43
+ { ...agents[idx], ...req.body, id: req.params.id },
44
+ !Array.isArray(req.body?.toolIds),
45
+ );
46
+ ctx.store.write(ctx.AGENT_FILE, agents);
47
+ res.json(agents[idx]);
48
+ });
49
+ app.delete('/api/agents/:id', (req, res) => {
50
+ let agents = ctx.store.read(ctx.AGENT_FILE, []);
51
+ agents = agents.filter(a => a.id !== req.params.id);
52
+ ctx.store.write(ctx.AGENT_FILE, agents);
53
+ res.json({ ok: true });
54
+ });
55
+ };
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ // ── Assistants CRUD(遗留,保留兼容) ──────────────────────────────────
3
+ const ctx = require('../lib/context');
4
+
5
+ module.exports = function registerAssistants(app) {
6
+ app.get('/api/assistants', (req, res) => {
7
+ res.json(ctx.store.read('assistants.json', []));
8
+ });
9
+ app.post('/api/assistants', (req, res) => {
10
+ const assistants = ctx.store.read('assistants.json', []);
11
+ const assistant = { id: Date.now().toString(36), ...req.body };
12
+ assistants.push(assistant);
13
+ ctx.store.write('assistants.json', assistants);
14
+ res.json(assistant);
15
+ });
16
+ app.put('/api/assistants/:id', (req, res) => {
17
+ const assistants = ctx.store.read('assistants.json', []);
18
+ const idx = assistants.findIndex(a => a.id === req.params.id);
19
+ if (idx < 0) return res.status(404).json({ error: 'not found' });
20
+ assistants[idx] = { ...assistants[idx], ...req.body, id: req.params.id };
21
+ ctx.store.write('assistants.json', assistants);
22
+ res.json(assistants[idx]);
23
+ });
24
+ app.delete('/api/assistants/:id', (req, res) => {
25
+ let assistants = ctx.store.read('assistants.json', []);
26
+ assistants = assistants.filter(a => a.id !== req.params.id);
27
+ ctx.store.write('assistants.json', assistants);
28
+ res.json({ ok: true });
29
+ });
30
+ };