@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
@@ -0,0 +1,126 @@
1
+ /** Shared TypeScript boundaries for data crossing HTTP, provider and extension APIs. */
2
+ export type JsonRecord = Record<string, any>;
3
+
4
+ export interface AdapterConfig {
5
+ apiKey?: string;
6
+ baseUrl?: string;
7
+ model?: string;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ export interface ChatMessage {
12
+ role: string;
13
+ content?: any;
14
+ tool_call_id?: string;
15
+ tool_calls?: ToolCall[];
16
+ [key: string]: any;
17
+ }
18
+
19
+ export interface ToolCall {
20
+ id?: string;
21
+ type?: string;
22
+ index?: number;
23
+ function?: {
24
+ name?: string;
25
+ description?: string;
26
+ arguments?: string;
27
+ parameters?: JsonRecord;
28
+ };
29
+ [key: string]: any;
30
+ }
31
+
32
+ export interface ChatRequestOptions extends JsonRecord {
33
+ temperature?: number;
34
+ max_tokens?: number;
35
+ top_p?: number;
36
+ stop_sequences?: string[];
37
+ tools?: ToolCall[];
38
+ tool_choice?: any;
39
+ stream_options?: JsonRecord;
40
+ }
41
+
42
+ export interface ExtensionSource {
43
+ kind?: string;
44
+ scope?: string;
45
+ imported?: boolean;
46
+ pluginId?: string;
47
+ pluginPackage?: JsonRecord;
48
+ [key: string]: any;
49
+ }
50
+
51
+ export interface McpServerRecord extends JsonRecord {
52
+ id?: string;
53
+ name?: string;
54
+ description?: string;
55
+ transport?: string;
56
+ enabled?: boolean;
57
+ trustLevel?: string;
58
+ allowPrivate?: boolean;
59
+ targets?: string[];
60
+ source?: ExtensionSource;
61
+ command?: string;
62
+ args?: string[];
63
+ cwd?: string;
64
+ env?: Record<string, string>;
65
+ url?: string;
66
+ headers?: Record<string, string>;
67
+ bearerTokenEnvVar?: string;
68
+ updatedAt?: string;
69
+ }
70
+
71
+ export interface RequestOptions {
72
+ signal?: AbortSignal;
73
+ }
74
+
75
+ export interface JsonStore {
76
+ kind?: 'json' | 'sqlite';
77
+ read<T>(name: string, fallback: T): T;
78
+ write(name: string, value: unknown): void;
79
+ remove(name: string): void;
80
+ resolve(name: string): string;
81
+ mutate<T>(name: string, update: (current: T) => T | undefined | Promise<T | undefined>, fallback: T): Promise<T | undefined>;
82
+ searchDocuments?(query: string, options?: { projectId?: string; limit?: number }): Array<{
83
+ entityId: string;
84
+ projectId: string;
85
+ title: string;
86
+ snippet: string;
87
+ rank: number;
88
+ source: string;
89
+ }>;
90
+ close?(): void;
91
+ }
92
+
93
+ export interface WorkspaceRecord {
94
+ id: string;
95
+ name: string;
96
+ description: string;
97
+ createdAt: string;
98
+ updatedAt: string;
99
+ defaultProjectId: string;
100
+ }
101
+
102
+ export interface ProjectRecord {
103
+ id: string;
104
+ workspaceId: string;
105
+ name: string;
106
+ description: string;
107
+ createdAt: string;
108
+ updatedAt: string;
109
+ defaultAgentId?: string | null;
110
+ defaultProviderId?: string | null;
111
+ defaultModel?: string | null;
112
+ }
113
+
114
+ export interface AssetRecord {
115
+ id: string;
116
+ workspaceId: string;
117
+ projectId: string;
118
+ name: string;
119
+ mimeType: string;
120
+ size: number;
121
+ source: 'url' | 'local';
122
+ url: string | null;
123
+ content: string;
124
+ createdAt: string;
125
+ updatedAt: string;
126
+ }
@@ -0,0 +1,535 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from 'node:child_process';
4
+ import { createRequire } from 'node:module';
5
+ import net from 'node:net';
6
+ import {
7
+ accessSync,
8
+ constants,
9
+ existsSync,
10
+ readFileSync,
11
+ readdirSync,
12
+ statSync,
13
+ } from 'node:fs';
14
+ import { mkdir } from 'node:fs/promises';
15
+ import { homedir } from 'node:os';
16
+ import path from 'node:path';
17
+ import process from 'node:process';
18
+ import { fileURLToPath, pathToFileURL } from 'node:url';
19
+
20
+ const CLI_FILE = fileURLToPath(import.meta.url);
21
+ const PACKAGE_ROOT = path.resolve(path.dirname(CLI_FILE), '..');
22
+ const PACKAGE_JSON = JSON.parse(readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
23
+ const REPOSITORY_URL = 'https://github.com/Leterhong/MultiChat.git';
24
+ const COMMANDS = new Set(['web', 'doctor', 'init', 'pull', 'deploy', 'help']);
25
+ const DEFAULT_HOST = '127.0.0.1';
26
+ const DEFAULT_PORT = 3000;
27
+ const MINIMUM_NODE_VERSION = Object.freeze({ major: 22, minor: 13, patch: 0 });
28
+
29
+ class CliError extends Error {
30
+ constructor(message, exitCode = 1) {
31
+ super(message);
32
+ this.name = 'CliError';
33
+ this.exitCode = exitCode;
34
+ }
35
+ }
36
+
37
+ function optionValue(argv, index, option) {
38
+ const current = argv[index];
39
+ const equals = current.indexOf('=');
40
+ if (equals >= 0) {
41
+ const value = current.slice(equals + 1);
42
+ if (!value) throw new CliError(`${option} 缺少参数值`);
43
+ return { value, nextIndex: index + 1 };
44
+ }
45
+ const value = argv[index + 1];
46
+ if (!value || value.startsWith('--')) throw new CliError(`${option} 缺少参数值`);
47
+ return { value, nextIndex: index + 2 };
48
+ }
49
+
50
+ export function parseCliArgs(argv, cwd = process.cwd()) {
51
+ const args = [...argv];
52
+ let command = 'web';
53
+ let commandExplicit = false;
54
+ let index = 0;
55
+
56
+ if (args[0] && !args[0].startsWith('-')) {
57
+ if (!COMMANDS.has(args[0])) throw new CliError(`未知命令:${args[0]}`);
58
+ command = args[0];
59
+ commandExplicit = true;
60
+ index = 1;
61
+ }
62
+
63
+ const options = {
64
+ host: DEFAULT_HOST,
65
+ port: DEFAULT_PORT,
66
+ open: true,
67
+ workspace: path.resolve(cwd),
68
+ dataDir: path.join(homedir(), '.multichat', 'data'),
69
+ help: false,
70
+ version: false,
71
+ };
72
+ const positionals = [];
73
+
74
+ while (index < args.length) {
75
+ const arg = args[index];
76
+ if (arg === '-h' || arg === '--help') {
77
+ options.help = true;
78
+ index += 1;
79
+ } else if (arg === '-v' || arg === '--version') {
80
+ options.version = true;
81
+ index += 1;
82
+ } else if (arg === '--no-open') {
83
+ options.open = false;
84
+ index += 1;
85
+ } else if (arg === '--host' || arg.startsWith('--host=')) {
86
+ const parsed = optionValue(args, index, '--host');
87
+ options.host = parsed.value;
88
+ index = parsed.nextIndex;
89
+ } else if (arg === '--port' || arg.startsWith('--port=')) {
90
+ const parsed = optionValue(args, index, '--port');
91
+ options.port = Number(parsed.value);
92
+ index = parsed.nextIndex;
93
+ } else if (arg === '--workspace' || arg.startsWith('--workspace=')) {
94
+ const parsed = optionValue(args, index, '--workspace');
95
+ options.workspace = path.resolve(cwd, parsed.value);
96
+ index = parsed.nextIndex;
97
+ } else if (arg === '--data-dir' || arg.startsWith('--data-dir=')) {
98
+ const parsed = optionValue(args, index, '--data-dir');
99
+ options.dataDir = path.resolve(cwd, parsed.value);
100
+ index = parsed.nextIndex;
101
+ } else if (arg.startsWith('-')) {
102
+ throw new CliError(`未知参数:${arg}`);
103
+ } else {
104
+ positionals.push(arg);
105
+ index += 1;
106
+ }
107
+ }
108
+
109
+ if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) {
110
+ throw new CliError('--port 必须是 1 到 65535 之间的整数');
111
+ }
112
+ if (!options.host || !/^[a-zA-Z0-9.:[\]-]+$/.test(options.host)) {
113
+ throw new CliError('--host 不是有效的主机名或 IP 地址');
114
+ }
115
+ if (positionals.length > 1 || (positionals.length && !['init', 'pull'].includes(command))) {
116
+ throw new CliError(`${command} 命令不接受这些位置参数`);
117
+ }
118
+
119
+ return { command, commandExplicit, options, positionals };
120
+ }
121
+
122
+ function helpText(command) {
123
+ if (command === 'init') {
124
+ return `MultiChat init\n\n用法:\n multichat init [目录]\n\n克隆源码仓库,安装根目录、后端和前端依赖,并生成完整生产构建。\n默认目录:./MultiChat`;
125
+ }
126
+ if (command === 'pull') {
127
+ return `MultiChat pull\n\n用法:\n multichat pull [目录]\n\n在现有 MultiChat 源码仓库中执行 git pull --ff-only,然后重新安装依赖并生成完整生产构建。\n默认目录:当前目录`;
128
+ }
129
+ if (command === 'web' || command === 'deploy') {
130
+ return `MultiChat ${command}\n\n用法:\n multichat ${command} [参数]\n\n参数:\n --host <地址> 监听地址(默认 127.0.0.1)\n --port <端口> 监听端口(默认 3000)\n --no-open 启动后不自动打开浏览器\n --workspace <目录> Agent 工作区(默认当前目录)\n --data-dir <目录> 运行数据目录(默认 ~/.multichat/data)\n -h, --help 显示帮助\n\n${command === 'deploy' ? 'deploy 是本地部署入口,不会向远程服务器发布。' : '默认仅本机可访问;显式修改 --host 才会改变监听范围。'}`;
131
+ }
132
+ if (command === 'doctor') {
133
+ return `MultiChat doctor\n\n用法:\n multichat doctor [参数]\n\n在不启动网站的情况下检查 Node.js、TypeScript 运行时、前后端文件、工作区、数据目录和监听端口。\n\n参数:\n --host <地址> 待检查的监听地址(默认 127.0.0.1)\n --port <端口> 待检查的监听端口(默认 3000)\n --workspace <目录> 待检查的 Agent 工作区\n --data-dir <目录> 待检查的运行数据目录\n -h, --help 显示帮助`;
134
+ }
135
+ return `MultiChat ${PACKAGE_JSON.version}\n\n本地优先的多模型 Agent 工作台。\n\n用法:\n multichat [web] [参数]\n multichat doctor [参数]\n multichat init [目录]\n multichat pull [目录]\n multichat deploy [参数]\n\n命令:\n web 启动本地网站(默认命令)\n doctor 检查 Node、运行文件、目录和端口\n init 克隆源码、安装依赖并构建\n pull 快进更新现有源码、安装依赖并构建\n deploy web 的本地部署别名,不执行远程发布\n\n通用参数:\n -h, --help 显示帮助\n -v, --version 显示版本\n\n一条命令启动:\n npx --yes github:Leterhong/MultiChat web\n\n查看子命令参数:\n multichat web --help`;
136
+ }
137
+
138
+ function nodeVersionParts(version = process.versions.node) {
139
+ const [major = 0, minor = 0, patch = 0] = String(version).split('.').map((value) => Number.parseInt(value, 10) || 0);
140
+ return { major, minor, patch };
141
+ }
142
+
143
+ export function supportsRequiredNode(version = process.versions.node) {
144
+ const current = nodeVersionParts(version);
145
+ const required = MINIMUM_NODE_VERSION;
146
+ return current.major > required.major
147
+ || (current.major === required.major && current.minor > required.minor)
148
+ || (current.major === required.major && current.minor === required.minor && current.patch >= required.patch);
149
+ }
150
+
151
+ function assertNodeVersion() {
152
+ if (!supportsRequiredNode()) {
153
+ throw new CliError(`MultiChat 需要 Node.js 22.13.0 或更高版本(SQLite 默认存储要求);当前为 ${process.version}`);
154
+ }
155
+ }
156
+
157
+ export function probeSqlite() {
158
+ let database;
159
+ try {
160
+ const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
161
+ if (typeof DatabaseSync !== 'function') throw new Error('DatabaseSync 不可用');
162
+ database = new DatabaseSync(':memory:');
163
+ database.exec('CREATE TABLE multichat_doctor (id INTEGER PRIMARY KEY) STRICT');
164
+ return { ok: true, detail: 'node:sqlite / DatabaseSync 可用' };
165
+ } catch (error) {
166
+ return { ok: false, detail: error instanceof Error ? error.message : String(error) };
167
+ } finally {
168
+ try { database?.close(); } catch {}
169
+ }
170
+ }
171
+
172
+ function isDirectory(value) {
173
+ try {
174
+ return statSync(value).isDirectory();
175
+ } catch {
176
+ return false;
177
+ }
178
+ }
179
+
180
+ function comparablePath(value) {
181
+ const resolved = path.resolve(value);
182
+ return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
183
+ }
184
+
185
+ function assertDistinctDirectories(workspace, dataDir) {
186
+ const left = comparablePath(workspace);
187
+ const right = comparablePath(dataDir);
188
+ if (left === right) throw new CliError('工作区与数据目录必须使用两个不同的目录');
189
+ }
190
+
191
+ function resolveRuntime() {
192
+ const compiledEntry = path.join(PACKAGE_ROOT, 'backend', 'dist', 'server.js');
193
+ const TypeScriptEntry = path.join(PACKAGE_ROOT, 'backend', 'server.ts');
194
+ const legacyEntry = path.join(PACKAGE_ROOT, 'backend', 'server.js');
195
+ const backendEntry = existsSync(compiledEntry) ? compiledEntry : (existsSync(TypeScriptEntry) ? TypeScriptEntry : legacyEntry);
196
+ const frontendDist = path.join(PACKAGE_ROOT, 'frontend', 'dist');
197
+ const frontendIndex = path.join(frontendDist, 'index.html');
198
+ let tsxEntry = null;
199
+ if (backendEntry === TypeScriptEntry) {
200
+ const resolutionBases = [import.meta.url, pathToFileURL(path.join(PACKAGE_ROOT, 'backend', 'package.json')).href];
201
+ for (const resolutionBase of resolutionBases) {
202
+ try {
203
+ tsxEntry = createRequire(resolutionBase).resolve('tsx');
204
+ break;
205
+ } catch {
206
+ // Source-only development checkout: try the backend dependency tree.
207
+ }
208
+ }
209
+ }
210
+ return { backendEntry, compiledEntry, TypeScriptEntry, frontendDist, frontendIndex, tsxEntry };
211
+ }
212
+
213
+ function assertRuntime(runtime) {
214
+ if (!existsSync(runtime.backendEntry)) {
215
+ throw new CliError(`找不到服务入口:${runtime.backendEntry}`);
216
+ }
217
+ if (!existsSync(runtime.frontendIndex)) {
218
+ throw new CliError('找不到 frontend/dist/index.html;源码运行请先执行 npm run build');
219
+ }
220
+ if (runtime.backendEntry === runtime.TypeScriptEntry && !runtime.tsxEntry) {
221
+ throw new CliError('当前只有 TypeScript 源码;请先执行 npm run build');
222
+ }
223
+ }
224
+
225
+ // 源码比构建产物新,说明上次改完没有重新构建——这正是“改了源码却不生效”的
226
+ // 常见根因。只告警不阻断:dist 仍是可用的上一版行为。
227
+ function newestSourceMtime(dir, skipNames, state = { mtime: 0 }) {
228
+ let entries;
229
+ try { entries = readdirSync(dir, { withFileTypes: true }); }
230
+ catch { return state; }
231
+ for (const entry of entries) {
232
+ if (skipNames.has(entry.name)) continue;
233
+ const full = path.join(dir, entry.name);
234
+ if (entry.isDirectory()) newestSourceMtime(full, skipNames, state);
235
+ else if (/\.(?:ts|tsx|css)$/.test(entry.name)) {
236
+ try { state.mtime = Math.max(state.mtime, statSync(full).mtimeMs); }
237
+ catch { // 文件可能在扫描间隙被删除,忽略。
238
+ }
239
+ }
240
+ }
241
+ return state;
242
+ }
243
+
244
+ function warnIfStaleDist(runtime) {
245
+ if (runtime.backendEntry !== runtime.compiledEntry) return;
246
+ const backendRoot = path.join(PACKAGE_ROOT, 'backend');
247
+ const newestSource = newestSourceMtime(backendRoot, new Set(['node_modules', 'dist', '.test-dist', 'data'])).mtime;
248
+ let builtAt;
249
+ try { builtAt = statSync(runtime.compiledEntry).mtimeMs; }
250
+ catch { return; }
251
+ if (newestSource > builtAt + 1500) {
252
+ console.warn(`⚠ backend 源码比 backend/dist 新(上次构建后源码有改动)。当前运行的是旧构建;请执行 cd backend && npm run build 后重启。`);
253
+ try {
254
+ const newestFrontend = newestSourceMtime(path.join(PACKAGE_ROOT, 'frontend', 'src'), new Set()).mtime;
255
+ const frontendIndex = statSync(runtime.frontendIndex).mtimeMs;
256
+ if (newestFrontend > frontendIndex + 1500) {
257
+ console.warn(`⚠ frontend 源码比 frontend/dist 新。请执行 cd frontend && npm run build 后刷新页面。`);
258
+ }
259
+ } catch { // frontend/src 不存在(发布包)时跳过。
260
+ }
261
+ }
262
+ }
263
+
264
+ function checkPort(host, port) {
265
+ return new Promise((resolvePort) => {
266
+ const server = net.createServer();
267
+ server.unref();
268
+ server.once('error', (error) => resolvePort({ ok: false, error }));
269
+ server.listen({ host, port, exclusive: true }, () => {
270
+ server.close(() => resolvePort({ ok: true }));
271
+ });
272
+ });
273
+ }
274
+
275
+ function browserHost(host) {
276
+ if (host === '0.0.0.0') return '127.0.0.1';
277
+ if (host === '::' || host === '[::]') return '[::1]';
278
+ if (host.includes(':') && !host.startsWith('[')) return `[${host}]`;
279
+ return host;
280
+ }
281
+
282
+ function openBrowser(url) {
283
+ let command;
284
+ let args;
285
+ if (process.platform === 'win32') {
286
+ command = 'cmd.exe';
287
+ args = ['/d', '/s', '/c', 'start', '', url];
288
+ } else if (process.platform === 'darwin') {
289
+ command = 'open';
290
+ args = [url];
291
+ } else {
292
+ command = 'xdg-open';
293
+ args = [url];
294
+ }
295
+ try {
296
+ const opener = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true });
297
+ opener.once('error', () => {});
298
+ opener.unref();
299
+ } catch {
300
+ // The URL is printed as a fallback, so missing desktop helpers are non-fatal.
301
+ }
302
+ }
303
+
304
+ async function waitForHealth(url, child, timeoutMs = 15_000) {
305
+ const deadline = Date.now() + timeoutMs;
306
+ while (Date.now() < deadline) {
307
+ if (child.exitCode !== null) throw new CliError('服务在健康检查完成前退出');
308
+ try {
309
+ const response = await fetch(`${url}/api/health`, { signal: AbortSignal.timeout(1_000) });
310
+ if (response.ok) return;
311
+ } catch {
312
+ // The server may still be compiling or binding its socket.
313
+ }
314
+ await new Promise((resolveWait) => setTimeout(resolveWait, 250));
315
+ }
316
+ throw new CliError('服务已启动,但 15 秒内未通过健康检查');
317
+ }
318
+
319
+ function waitForChild(child) {
320
+ return new Promise((resolveChild, rejectChild) => {
321
+ child.once('error', rejectChild);
322
+ child.once('exit', (code, signal) => resolveChild({ code, signal }));
323
+ });
324
+ }
325
+
326
+ async function runWeb(options, deployAlias = false) {
327
+ assertNodeVersion();
328
+ const runtime = resolveRuntime();
329
+ assertRuntime(runtime);
330
+ warnIfStaleDist(runtime);
331
+ if (!isDirectory(options.workspace)) {
332
+ throw new CliError(`工作区不存在或不是目录:${options.workspace}`);
333
+ }
334
+ assertDistinctDirectories(options.workspace, options.dataDir);
335
+ await mkdir(options.dataDir, { recursive: true });
336
+
337
+ const port = await checkPort(options.host, options.port);
338
+ if (!port.ok) {
339
+ throw new CliError(`${options.host}:${options.port} 无法监听:${port.error?.message || '端口不可用'}`);
340
+ }
341
+
342
+ const url = `http://${browserHost(options.host)}:${options.port}`;
343
+ console.log(deployAlias ? 'MultiChat 本地部署正在启动…' : 'MultiChat 正在启动…');
344
+ console.log(` 网站:${url}`);
345
+ console.log(` 工作区:${options.workspace}`);
346
+ console.log(` 数据:${options.dataDir}`);
347
+ if (!['127.0.0.1', 'localhost', '::1', '[::1]'].includes(options.host)) {
348
+ console.warn(` 提醒:当前监听 ${options.host},请自行确认网络访问边界。`);
349
+ }
350
+
351
+ const nodeArgs = runtime.backendEntry === runtime.TypeScriptEntry
352
+ ? ['--import', pathToFileURL(runtime.tsxEntry).href, runtime.TypeScriptEntry]
353
+ : [runtime.backendEntry];
354
+
355
+ const child = spawn(process.execPath, nodeArgs, {
356
+ cwd: options.workspace,
357
+ env: {
358
+ ...process.env,
359
+ HOST: options.host,
360
+ PORT: String(options.port),
361
+ DATA_DIR: options.dataDir,
362
+ FRONTEND_DIST: runtime.frontendDist,
363
+ MULTICHAT_PROJECT_ROOT: options.workspace,
364
+ NODE_ENV: process.env.NODE_ENV || 'production',
365
+ },
366
+ stdio: 'inherit',
367
+ windowsHide: false,
368
+ });
369
+
370
+ const forwardSignal = (signal) => {
371
+ if (child.exitCode === null && !child.killed) child.kill(signal);
372
+ };
373
+ const onSigint = () => forwardSignal('SIGINT');
374
+ const onSigterm = () => forwardSignal('SIGTERM');
375
+ process.once('SIGINT', onSigint);
376
+ process.once('SIGTERM', onSigterm);
377
+
378
+ void waitForHealth(url, child)
379
+ .then(() => {
380
+ console.log(`MultiChat 已就绪:${url}`);
381
+ if (options.open && !process.env.CI) openBrowser(url);
382
+ })
383
+ .catch((error) => console.warn(`健康检查:${error.message}`));
384
+
385
+ try {
386
+ const result = await waitForChild(child);
387
+ return result.code ?? (result.signal ? 1 : 0);
388
+ } finally {
389
+ process.removeListener('SIGINT', onSigint);
390
+ process.removeListener('SIGTERM', onSigterm);
391
+ }
392
+ }
393
+
394
+ function nearestExistingDirectory(target) {
395
+ let current = path.resolve(target);
396
+ while (!existsSync(current)) {
397
+ const parent = path.dirname(current);
398
+ if (parent === current) return null;
399
+ current = parent;
400
+ }
401
+ return isDirectory(current) ? current : path.dirname(current);
402
+ }
403
+
404
+ async function runDoctor(options) {
405
+ const runtime = resolveRuntime();
406
+ const results = [];
407
+ const add = (status, label, detail) => results.push({ status, label, detail });
408
+
409
+ add(supportsRequiredNode() ? 'PASS' : 'FAIL', 'Node.js', `${process.version}(需要 >=22.13.0)`);
410
+ const sqlite = probeSqlite();
411
+ add(sqlite.ok ? 'PASS' : 'FAIL', 'SQLite', sqlite.ok ? sqlite.detail : `${sqlite.detail};可设置 MULTICHAT_STORE=json 临时降级`);
412
+ add(existsSync(runtime.backendEntry) ? 'PASS' : 'FAIL', '后端构建', runtime.backendEntry);
413
+ add(existsSync(runtime.frontendIndex) ? 'PASS' : 'FAIL', '前端构建', runtime.frontendIndex);
414
+ if (runtime.backendEntry === runtime.TypeScriptEntry) add(runtime.tsxEntry ? 'PASS' : 'FAIL', '开发运行时', runtime.tsxEntry || '未安装 tsx');
415
+
416
+ if (!isDirectory(options.workspace)) {
417
+ add('FAIL', '工作区', `${options.workspace} 不存在或不是目录`);
418
+ } else {
419
+ try {
420
+ accessSync(options.workspace, constants.R_OK | constants.W_OK);
421
+ add('PASS', '工作区', options.workspace);
422
+ } catch (error) {
423
+ add('FAIL', '工作区', `${options.workspace}:${error.message}`);
424
+ }
425
+ }
426
+
427
+ if (comparablePath(options.workspace) === comparablePath(options.dataDir)) {
428
+ add('FAIL', '数据目录', '不能与工作区相同');
429
+ } else if (existsSync(options.dataDir) && !isDirectory(options.dataDir)) {
430
+ add('FAIL', '数据目录', `${options.dataDir} 已存在,但不是目录`);
431
+ } else {
432
+ const writableRoot = nearestExistingDirectory(options.dataDir);
433
+ try {
434
+ if (!writableRoot) throw new Error('找不到可写父目录');
435
+ accessSync(writableRoot, constants.W_OK);
436
+ add('PASS', '数据目录', existsSync(options.dataDir) ? options.dataDir : `${options.dataDir}(首次启动时创建)`);
437
+ } catch (error) {
438
+ add('FAIL', '数据目录', `${options.dataDir}:${error.message}`);
439
+ }
440
+ }
441
+
442
+ const port = await checkPort(options.host, options.port);
443
+ add(port.ok ? 'PASS' : 'FAIL', '监听端口', port.ok ? `${options.host}:${options.port} 可用` : `${options.host}:${options.port}:${port.error?.message}`);
444
+
445
+ console.log(`MultiChat doctor ${PACKAGE_JSON.version}\n`);
446
+ for (const result of results) {
447
+ const marker = result.status === 'PASS' ? '✓' : '✗';
448
+ console.log(`${marker} ${result.label.padEnd(14)} ${result.detail}`);
449
+ }
450
+ const failures = results.filter((item) => item.status === 'FAIL').length;
451
+ console.log(failures ? `\n发现 ${failures} 个问题。` : '\n所有检查均已通过。');
452
+ return failures ? 1 : 0;
453
+ }
454
+
455
+ function externalCommand(name) {
456
+ return process.platform === 'win32' ? `${name}.cmd` : name;
457
+ }
458
+
459
+ function runExternal(command, args, cwd) {
460
+ return new Promise((resolveCommand, rejectCommand) => {
461
+ const child = spawn(command, args, { cwd, stdio: 'inherit', windowsHide: false });
462
+ child.once('error', rejectCommand);
463
+ child.once('exit', (code, signal) => {
464
+ if (code === 0) resolveCommand();
465
+ else rejectCommand(new CliError(`${command} ${args.join(' ')} 执行失败${signal ? `(${signal})` : `(退出码 ${code})`}`));
466
+ });
467
+ });
468
+ }
469
+
470
+ async function installAndBuild(sourceRoot) {
471
+ const backendRoot = path.join(sourceRoot, 'backend');
472
+ const frontendRoot = path.join(sourceRoot, 'frontend');
473
+ if (!existsSync(path.join(sourceRoot, 'package.json')) || !existsSync(path.join(backendRoot, 'package.json')) || !existsSync(path.join(frontendRoot, 'package.json'))) {
474
+ throw new CliError(`目录不是完整的 MultiChat 源码仓库:${sourceRoot}`);
475
+ }
476
+ const npm = externalCommand('npm');
477
+ await runExternal(npm, ['install'], sourceRoot);
478
+ await runExternal(npm, ['install'], backendRoot);
479
+ await runExternal(npm, ['install'], frontendRoot);
480
+ await runExternal(npm, ['run', 'build'], sourceRoot);
481
+ }
482
+
483
+ async function runInit(positionals) {
484
+ assertNodeVersion();
485
+ const target = path.resolve(positionals[0] || 'MultiChat');
486
+ if (existsSync(target) && (!isDirectory(target) || readdirSync(target).length > 0)) {
487
+ throw new CliError(`目标目录必须不存在或为空:${target}`);
488
+ }
489
+ console.log(`正在克隆 MultiChat 到 ${target}`);
490
+ await runExternal('git', ['clone', '--depth', '1', REPOSITORY_URL, target], process.cwd());
491
+ await installAndBuild(target);
492
+ console.log(`\n初始化完成。\n cd "${target}"\n npm start`);
493
+ return 0;
494
+ }
495
+
496
+ async function runPull(positionals) {
497
+ assertNodeVersion();
498
+ const target = path.resolve(positionals[0] || process.cwd());
499
+ if (!existsSync(path.join(target, '.git'))) {
500
+ throw new CliError(`目录不是 Git 源码仓库:${target}`);
501
+ }
502
+ console.log(`正在更新 ${target}`);
503
+ await runExternal('git', ['pull', '--ff-only'], target);
504
+ await installAndBuild(target);
505
+ console.log('\n更新与构建完成。');
506
+ return 0;
507
+ }
508
+
509
+ export async function main(argv = process.argv.slice(2)) {
510
+ const parsed = parseCliArgs(argv);
511
+ if (parsed.options.version) {
512
+ console.log(PACKAGE_JSON.version);
513
+ return 0;
514
+ }
515
+ if (parsed.command === 'help' || parsed.options.help) {
516
+ console.log(helpText(parsed.command === 'help' || !parsed.commandExplicit ? undefined : parsed.command));
517
+ return 0;
518
+ }
519
+ if (parsed.command === 'doctor') return runDoctor(parsed.options);
520
+ if (parsed.command === 'init') return runInit(parsed.positionals);
521
+ if (parsed.command === 'pull') return runPull(parsed.positionals);
522
+ return runWeb(parsed.options, parsed.command === 'deploy');
523
+ }
524
+
525
+ const invokedAsScript = process.argv[1] && path.resolve(process.argv[1]) === CLI_FILE;
526
+ if (invokedAsScript) {
527
+ main()
528
+ .then((exitCode) => {
529
+ process.exitCode = exitCode;
530
+ })
531
+ .catch((error) => {
532
+ console.error(`MultiChat:${error.message}`);
533
+ process.exitCode = error.exitCode || 1;
534
+ });
535
+ }
@@ -0,0 +1,13 @@
1
+ import{d as e,f as t,h as n,p as r,u as i}from"./index-CASVCOUO.js";r(`#workspacePicker`).onclick=()=>{let t=e.workspaces.map(e=>({id:e.id,name:e.name,description:e.description}));if(!t.length){openSettings(`workspace`);return}showModal({title:`选择工作区`,body:`<div class="picker-dialog-intro"><span>切换对话与项目所在的工作区</span><span>${t.length} 个工作区</span></div>
2
+ <div class="model-picker-list">${t.map(t=>{let r=e.selectedWorkspace?.id===t.id;return`<button type="button" class="picker-option${r?` active`:``}" data-wid="${n(t.id)}" aria-pressed="${r}">
3
+ <span class="picker-provider-mark" aria-hidden="true">${n(t.name.trim().slice(0,1).toUpperCase()||`W`)}</span>
4
+ <span class="picker-option-copy"><strong>${n(t.name)}</strong><small>${n(t.description||`本地工作区`)}</small></span>
5
+ <span class="picker-option-state" aria-hidden="true">${r?`✓`:`›`}</span>
6
+ </button>`}).join(``)}</div>`,onMount:t=>t.querySelectorAll(`[data-wid]`).forEach(t=>t.onclick=async()=>{e.selectedWorkspace=e.workspaces.find(e=>e.id===t.dataset.wid)||null,await loadProjects(),e.currentConvId=null,e.messages=[],renderTopbar(),renderConvList(),renderContent(),closeModal()})})},r(`#agentPicker`).onclick=()=>{if(!e.agents.length){openSettings(`agents`),t(`请先创建运行配置`);return}let r=[{id:``,name:`直接对话`,description:`不注入系统提示词,也不启用工具`}].concat(e.agents.map(e=>({id:e.id,name:e.name,description:e.description})));showModal({title:`选择运行配置`,body:`<div class="picker-dialog-intro"><span>选择本轮使用的提示词与能力组合</span><span>${e.agents.length} 个配置</span></div>
7
+ <div class="model-picker-list">
8
+ ${r.map(t=>{let r=(e.selectedAgent?.id||``)===t.id;return`<button type="button" class="picker-option${r?` active`:``}" data-aid="${n(t.id)}" aria-pressed="${r}">
9
+ <span class="picker-provider-mark agent" aria-hidden="true">${t.id?n(t.name.trim().slice(0,1).toUpperCase()||`A`):`—`}</span>
10
+ <span class="picker-option-copy"><strong>${n(t.name)}</strong><small>${n(t.description||`自定义运行配置`)}</small></span>
11
+ <span class="picker-option-state" aria-hidden="true">${r?`✓`:`›`}</span>
12
+ </button>`}).join(``)}
13
+ </div>`,onMount:t=>{t.querySelectorAll(`[data-aid]`).forEach(t=>{t.onclick=()=>{let n=t.dataset.aid;e.selectedAgent=n?e.agents.find(e=>e.id===n):null,i(),renderTopbar(),closeModal(),e.messages.length||renderContent()}})}})};