@next-open-ai/openclawx 0.8.48 → 0.8.58

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 (35) hide show
  1. package/apps/desktop/renderer/dist/assets/index-M5VGUUpo.js +93 -0
  2. package/apps/desktop/renderer/dist/assets/{index-BHY1xIZQ.css → index-y8oE2q_u.css} +1 -1
  3. package/apps/desktop/renderer/dist/index.html +2 -2
  4. package/dist/cli/cli.js +107 -0
  5. package/dist/core/agent/agent-manager.js +4 -0
  6. package/dist/core/config/desktop-config.d.ts +2 -1
  7. package/dist/core/config/desktop-config.js +92 -26
  8. package/dist/core/local-llm-server/download-model.d.ts +16 -0
  9. package/dist/core/local-llm-server/download-model.js +37 -0
  10. package/dist/core/local-llm-server/index.js +26 -5
  11. package/dist/core/local-llm-server/llm-context.d.ts +9 -4
  12. package/dist/core/local-llm-server/llm-context.js +35 -14
  13. package/dist/core/local-llm-server/model-resolve.d.ts +8 -1
  14. package/dist/core/local-llm-server/model-resolve.js +44 -12
  15. package/dist/core/local-llm-server/server.js +11 -12
  16. package/dist/core/local-llm-server/start-from-config.d.ts +5 -0
  17. package/dist/core/local-llm-server/start-from-config.js +50 -0
  18. package/dist/core/mcp/transport/stdio.d.ts +6 -0
  19. package/dist/core/mcp/transport/stdio.js +107 -27
  20. package/dist/core/memory/local-embedding-llama.js +2 -4
  21. package/dist/gateway/methods/agent-chat.js +9 -0
  22. package/dist/gateway/server.js +8 -51
  23. package/dist/server/bootstrap.d.ts +1 -0
  24. package/dist/server/bootstrap.js +3 -0
  25. package/dist/server/config/config.controller.d.ts +25 -2
  26. package/dist/server/config/config.controller.js +62 -12
  27. package/dist/server/config/config.service.d.ts +4 -1
  28. package/dist/server/config/config.service.js +62 -9
  29. package/dist/server/config/local-models.service.d.ts +16 -1
  30. package/dist/server/config/local-models.service.js +78 -46
  31. package/package.json +1 -1
  32. package/presets/preset-agents.json +6 -2
  33. package/presets/preset-config.json +24 -6
  34. package/apps/desktop/renderer/dist/assets/index-DQxlVuBe.js +0 -93
  35. package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +0 -9
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import { createServer } from "node:http";
8
8
  import { randomUUID } from "node:crypto";
9
- import { chatCompletionStream, chatCompletion, getEmbedding, isReady, } from "./llm-context.js";
9
+ import { chatCompletionStream, chatCompletion, getEmbedding, isLlmReady, isEmbeddingReady, } from "./llm-context.js";
10
10
  const LLM_MODEL_ID = process.env.LOCAL_LLM_MODEL_ID ?? "local-llm";
11
11
  const EMB_MODEL_ID = process.env.LOCAL_EMB_MODEL_ID ?? "local-embedding";
12
12
  function readBody(req) {
@@ -67,8 +67,8 @@ async function handleChatCompletions(req, res) {
67
67
  catch {
68
68
  return sendError(res, 400, "Invalid JSON body");
69
69
  }
70
- if (!isReady())
71
- return sendError(res, 503, "模型尚未加载完成,请稍后重试", "server_error");
70
+ if (!isLlmReady())
71
+ return sendError(res, 503, "LLM 模型未加载,请先启动本地模型服务并选择 LLM 模型", "server_error");
72
72
  if (!Array.isArray(body.messages)) {
73
73
  return sendError(res, 400, "Missing or invalid 'messages' (must be an array)", "invalid_request_error");
74
74
  }
@@ -157,8 +157,8 @@ async function handleEmbeddings(req, res) {
157
157
  catch {
158
158
  return sendError(res, 400, "Invalid JSON body", "invalid_request_error");
159
159
  }
160
- if (!isReady())
161
- return sendError(res, 503, "模型尚未加载完成,请稍后重试", "server_error");
160
+ if (!isEmbeddingReady())
161
+ return sendError(res, 503, "Embedding 模型未加载,请先启动本地模型服务并选择 Embedding 模型", "server_error");
162
162
  const input = body.input;
163
163
  if (input === undefined || input === null) {
164
164
  return sendError(res, 400, "Missing 'input' (string or array of strings)", "invalid_request_error");
@@ -186,13 +186,12 @@ async function handleEmbeddings(req, res) {
186
186
  }
187
187
  }
188
188
  function handleModels(_req, res) {
189
- sendJson(res, 200, {
190
- object: "list",
191
- data: [
192
- { id: LLM_MODEL_ID, object: "model", created: 0, owned_by: "local" },
193
- { id: EMB_MODEL_ID, object: "model", created: 0, owned_by: "local" },
194
- ],
195
- });
189
+ const data = [];
190
+ if (isLlmReady())
191
+ data.push({ id: LLM_MODEL_ID, object: "model", created: 0, owned_by: "local" });
192
+ if (isEmbeddingReady())
193
+ data.push({ id: EMB_MODEL_ID, object: "model", created: 0, owned_by: "local" });
194
+ sendJson(res, 200, { object: "list", data });
196
195
  }
197
196
  export function createOpenAICompatServer(port) {
198
197
  return new Promise((resolve, reject) => {
@@ -0,0 +1,5 @@
1
+ /**
2
+ * 按已保存的配置(默认智能体的 local 模型与上下文长度)尝试启动本地模型服务。
3
+ * 仅当已下载的模型文件存在时才启动;不抛错;失败时设置 process.env.LOCAL_LLM_START_FAILED 并打日志。
4
+ */
5
+ export declare function tryStartLocalModelFromSavedConfig(): Promise<void>;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 通用「按当前已保存配置启动本地模型服务」逻辑。
3
+ * 供网关启动与 API 复用:读取默认智能体配置,尝试启动;失败只设 env 与日志,不抛错、不影响主进程。
4
+ * LLM/Embedding 任一存在即可启动,只提示不报错。
5
+ */
6
+ import { loadDesktopAgentConfig } from "../config/desktop-config.js";
7
+ import { startLocalLlmServer } from "./index.js";
8
+ import { resolveModelPathInCache, LOCAL_LLM_CACHE_DIR } from "./model-resolve.js";
9
+ /**
10
+ * 按已保存的配置(默认智能体的 local 模型与上下文长度)尝试启动本地模型服务。
11
+ * 仅当已下载的模型文件存在时才启动;不抛错;失败时设置 process.env.LOCAL_LLM_START_FAILED 并打日志。
12
+ */
13
+ export async function tryStartLocalModelFromSavedConfig() {
14
+ try {
15
+ const agent = await loadDesktopAgentConfig("default");
16
+ if (!agent || agent.provider !== "local" || !agent.model?.trim()) {
17
+ process.env.LOCAL_LLM_START_FAILED =
18
+ "未配置默认本地模型,请在「模型配置」中选择 LLM 后点击「启动本地模型服务」";
19
+ console.log("[local-llm] 提示:未配置默认本地模型,跳过启动。");
20
+ return;
21
+ }
22
+ const llmResolved = resolveModelPathInCache(agent.model.trim(), LOCAL_LLM_CACHE_DIR);
23
+ if (!llmResolved) {
24
+ process.env.LOCAL_LLM_START_FAILED =
25
+ "缺省模型文件未下载,请先在「模型管理」中下载后点击「启动本地模型服务」";
26
+ console.log("[local-llm] 提示:缺省模型文件未下载,跳过启动。");
27
+ return;
28
+ }
29
+ const contextSize = process.env.LOCAL_LLM_CONTEXT_MAX != null && String(process.env.LOCAL_LLM_CONTEXT_MAX).trim() !== ""
30
+ ? parseInt(process.env.LOCAL_LLM_CONTEXT_MAX, 10) || 32768
31
+ : (agent.contextSize ?? 32768);
32
+ const opts = { llmModelPath: llmResolved, contextSize };
33
+ startLocalLlmServer(opts)
34
+ .then((handle) => {
35
+ process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
36
+ delete process.env.LOCAL_LLM_START_FAILED;
37
+ console.log("[local-llm] 已就绪:", handle.baseUrl);
38
+ })
39
+ .catch((e) => {
40
+ const msg = e instanceof Error ? e.message : String(e);
41
+ process.env.LOCAL_LLM_START_FAILED = msg;
42
+ console.log("[local-llm] 提示:启动未成功(如模型未下载请先在「模型管理」中下载)。", msg);
43
+ });
44
+ }
45
+ catch (e) {
46
+ const msg = e instanceof Error ? e.message : String(e);
47
+ process.env.LOCAL_LLM_START_FAILED = msg;
48
+ console.log("[local-llm] 提示:启动时发生异常,已跳过。", msg);
49
+ }
50
+ }
@@ -23,10 +23,16 @@ export declare class StdioTransport {
23
23
  private nextId;
24
24
  private pending;
25
25
  private buffer;
26
+ private stderrBuffer;
27
+ private static pendingKey;
26
28
  constructor(config: McpServerConfigStdio, options?: StdioTransportOptions);
27
29
  /** 启动子进程并完成 MCP initialize 握手 */
28
30
  start(): Promise<void>;
31
+ /** 从一行中解析 JSON-RPC 响应:整行即 JSON,或从第一个 { 开始提取到匹配的 }(兼容 npx/uvx 等前缀输出) */
32
+ private static parseJsonRpcResponse;
29
33
  private flushLines;
34
+ private flushStderrLines;
35
+ private flushLinesFromBuffer;
30
36
  private rejectAll;
31
37
  private initialize;
32
38
  private sendNotification;
@@ -13,9 +13,15 @@ export class StdioTransport {
13
13
  nextId = 1;
14
14
  pending = new Map();
15
15
  buffer = "";
16
+ stderrBuffer = "";
17
+ static pendingKey(id) {
18
+ if (id === undefined || id === null)
19
+ return "";
20
+ return String(id);
21
+ }
16
22
  constructor(config, options = {}) {
17
23
  this.config = config;
18
- this.initTimeoutMs = options.initTimeoutMs ?? 10_000;
24
+ this.initTimeoutMs = options.initTimeoutMs ?? 20_000;
19
25
  this.requestTimeoutMs = options.requestTimeoutMs ?? 30_000;
20
26
  this.initRetries = options.initRetries ?? 1;
21
27
  this.initRetryDelayMs = options.initRetryDelayMs ?? 3_000;
@@ -26,6 +32,26 @@ export class StdioTransport {
26
32
  return;
27
33
  }
28
34
  const env = { ...process.env, ...this.config.env };
35
+ // 避免 Python 类 MCP 在 pipe 下全缓冲 stdout,导致 initialize 响应迟迟不到而超时
36
+ if (env.PYTHONUNBUFFERED === undefined)
37
+ env.PYTHONUNBUFFERED = "1";
38
+ // npx/uvx 可能向 stdout 输出安装/进度等,污染 Newline-delimited JSON,导致无法解析;设为静默
39
+ const cmd = (this.config.command || "").trim().toLowerCase();
40
+ const cmdBase = cmd.includes("/") ? cmd.split("/").pop() : cmd;
41
+ if (cmdBase === "npx" || cmdBase === "npm") {
42
+ if (env.CI === undefined)
43
+ env.CI = "1";
44
+ if (env.NO_UPDATE_NOTIFIER === undefined)
45
+ env.NO_UPDATE_NOTIFIER = "1";
46
+ if (env.npm_config_loglevel === undefined)
47
+ env.npm_config_loglevel = "silent";
48
+ }
49
+ else if (cmdBase === "uvx" || cmdBase === "uv") {
50
+ if (env.CI === undefined)
51
+ env.CI = "1";
52
+ if (env.UV_SILENT === undefined)
53
+ env.UV_SILENT = "1";
54
+ }
29
55
  this.process = spawn(this.config.command, this.config.args ?? [], {
30
56
  env,
31
57
  stdio: ["pipe", "pipe", "pipe"],
@@ -35,10 +61,14 @@ export class StdioTransport {
35
61
  this.buffer += chunk.toString("utf-8");
36
62
  this.flushLines();
37
63
  });
64
+ // 部分 MCP 实现或包装可能把 JSON-RPC 写到 stderr,单独按行解析以尝试匹配响应(不混入 stdout 避免交叉破坏 JSON)
38
65
  child.stderr?.on("data", (data) => {
39
- const msg = data.toString("utf-8").trim();
40
- if (msg)
41
- console.warn("[mcp stdio stderr]", msg);
66
+ const raw = data.toString("utf-8");
67
+ const trimmed = raw.trim();
68
+ if (trimmed && !raw.includes("jsonrpc"))
69
+ console.warn("[mcp stdio stderr]", trimmed);
70
+ this.stderrBuffer += raw;
71
+ this.flushStderrLines();
42
72
  });
43
73
  child.on("error", (err) => {
44
74
  this.rejectAll(new Error(`MCP process error: ${err.message}`));
@@ -66,31 +96,76 @@ export class StdioTransport {
66
96
  }
67
97
  }
68
98
  }
69
- flushLines() {
70
- const lines = this.buffer.split("\n");
71
- this.buffer = lines.pop() ?? "";
72
- for (const line of lines) {
73
- const trimmed = line.trim();
74
- if (!trimmed)
75
- continue;
76
- try {
77
- const msg = JSON.parse(trimmed);
78
- if ("id" in msg && msg.id !== undefined) {
79
- const pending = this.pending.get(msg.id);
80
- if (pending) {
81
- clearTimeout(pending.timer);
82
- this.pending.delete(msg.id);
83
- if (msg.error) {
84
- pending.reject(new Error(msg.error.message));
85
- }
86
- else {
87
- pending.resolve(msg);
88
- }
99
+ /** 从一行中解析 JSON-RPC 响应:整行即 JSON,或从第一个 { 开始提取到匹配的 }(兼容 npx/uvx 等前缀输出) */
100
+ static parseJsonRpcResponse(line) {
101
+ const trimmed = line.trim();
102
+ if (!trimmed)
103
+ return null;
104
+ try {
105
+ const msg = JSON.parse(trimmed);
106
+ if ("id" in msg && msg.id !== undefined)
107
+ return msg;
108
+ return null;
109
+ }
110
+ catch {
111
+ const start = trimmed.indexOf("{");
112
+ if (start === -1)
113
+ return null;
114
+ let depth = 0;
115
+ let end = -1;
116
+ for (let i = start; i < trimmed.length; i++) {
117
+ const c = trimmed[i];
118
+ if (c === "{")
119
+ depth++;
120
+ else if (c === "}") {
121
+ depth--;
122
+ if (depth === 0) {
123
+ end = i;
124
+ break;
89
125
  }
90
126
  }
91
127
  }
128
+ if (end === -1)
129
+ return null;
130
+ try {
131
+ const msg = JSON.parse(trimmed.slice(start, end + 1));
132
+ if ("id" in msg && msg.id !== undefined)
133
+ return msg;
134
+ return null;
135
+ }
92
136
  catch {
93
- // 忽略非 JSON 行
137
+ return null;
138
+ }
139
+ }
140
+ }
141
+ flushLines() {
142
+ this.flushLinesFromBuffer(this.buffer, (rest) => {
143
+ this.buffer = rest;
144
+ });
145
+ }
146
+ flushStderrLines() {
147
+ this.flushLinesFromBuffer(this.stderrBuffer, (rest) => {
148
+ this.stderrBuffer = rest;
149
+ });
150
+ }
151
+ flushLinesFromBuffer(buf, setRest) {
152
+ const lines = buf.split("\n");
153
+ setRest(lines.pop() ?? "");
154
+ for (const line of lines) {
155
+ const msg = StdioTransport.parseJsonRpcResponse(line);
156
+ if (!msg)
157
+ continue;
158
+ const key = StdioTransport.pendingKey(msg.id);
159
+ const pending = key ? this.pending.get(key) : undefined;
160
+ if (pending) {
161
+ clearTimeout(pending.timer);
162
+ this.pending.delete(key);
163
+ if (msg.error) {
164
+ pending.reject(new Error(msg.error.message));
165
+ }
166
+ else {
167
+ pending.resolve(msg);
168
+ }
94
169
  }
95
170
  }
96
171
  }
@@ -129,13 +204,18 @@ export class StdioTransport {
129
204
  reject(new Error("MCP transport not connected"));
130
205
  return;
131
206
  }
207
+ const key = StdioTransport.pendingKey(req.id);
208
+ if (!key) {
209
+ reject(new Error("MCP request id is required"));
210
+ return;
211
+ }
132
212
  const t = timeoutMs ?? this.requestTimeoutMs;
133
213
  const timer = setTimeout(() => {
134
- if (this.pending.delete(req.id)) {
214
+ if (this.pending.delete(key)) {
135
215
  reject(new Error(`MCP request timeout (${t}ms)`));
136
216
  }
137
217
  }, t);
138
- this.pending.set(req.id, { resolve, reject, timer });
218
+ this.pending.set(key, { resolve, reject, timer });
139
219
  this.process.stdin.write(JSON.stringify(req) + "\n", "utf-8");
140
220
  });
141
221
  }
@@ -1,5 +1,3 @@
1
- import { join } from "path";
2
- import { homedir } from "os";
3
1
  let cached = null;
4
2
  let initError = null;
5
3
  let lastQueryError = null;
@@ -34,9 +32,9 @@ export async function getLocalEmbeddingLlamaProvider(modelPath) {
34
32
  await Promise.resolve(nodeModule.register(loaderUrl, import.meta.url));
35
33
  }
36
34
  const { getLlama, resolveModelFile, LlamaLogLevel } = await import("node-llama-cpp");
35
+ const { LOCAL_LLM_CACHE_DIR } = await import("../local-llm-server/model-resolve.js");
37
36
  const llama = await getLlama({ logLevel: LlamaLogLevel.error });
38
- const cacheDir = join(homedir(), ".cache", "llama");
39
- const resolved = await resolveModelFile(effectivePath, cacheDir);
37
+ const resolved = await resolveModelFile(effectivePath, LOCAL_LLM_CACHE_DIR);
40
38
  const model = await llama.loadModel({ modelPath: resolved });
41
39
  const embeddingCtx = await model.createEmbeddingContext();
42
40
  const provider = {
@@ -305,6 +305,11 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
305
305
  if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
306
306
  errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
307
307
  }
308
+ // 本地模型子进程退出后,SDK 会报 terminated/Connection error,用 env 中的说明替换为可操作提示
309
+ const localFailed = process.env.LOCAL_LLM_START_FAILED;
310
+ if (localFailed && (msg.errorMessage === "terminated" || /Connection error|ECONNREFUSED|fetch failed/i.test(msg.errorMessage))) {
311
+ errText = `请求失败:${localFailed}`;
312
+ }
308
313
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
309
314
  }
310
315
  wsPayload = null;
@@ -332,6 +337,10 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
332
337
  if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
333
338
  errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
334
339
  }
340
+ const localFailed = process.env.LOCAL_LLM_START_FAILED;
341
+ if (localFailed && (msg.errorMessage === "terminated" || /Connection error|ECONNREFUSED|fetch failed/i.test(msg.errorMessage))) {
342
+ errText = `请求失败:${localFailed}`;
343
+ }
335
344
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
336
345
  hasReceivedAnyChunk = true;
337
346
  }
@@ -44,9 +44,8 @@ import multer from "multer";
44
44
  import { handleInstallSkillFromPath } from "./methods/install-skill-from-path.js";
45
45
  import { handleInstallSkillFromUpload } from "./methods/install-skill-from-upload.js";
46
46
  import { setBackendBaseUrl } from "./backend-url.js";
47
- import { ensureDesktopConfigInitialized, getChannelsConfigSync, loadDesktopAgentConfig } from "../core/config/desktop-config.js";
48
- import { startLocalLlmServer } from "../core/local-llm-server/index.js";
49
- import { isModelFileInCache } from "../core/local-llm-server/model-resolve.js";
47
+ import { ensureDesktopConfigInitialized, getChannelsConfigSync } from "../core/config/desktop-config.js";
48
+ import { tryStartLocalModelFromSavedConfig } from "../core/local-llm-server/start-from-config.js";
50
49
  import { createNestAppEmbedded } from "../server/bootstrap.js";
51
50
  import { registerChannel, startAllChannels, stopAllChannels } from "./channel/registry.js";
52
51
  import { createFeishuChannel } from "./channel/adapters/feishu.js";
@@ -81,56 +80,14 @@ export async function startGatewayServer(port = 38080) {
81
80
  process.env.PORT = String(port);
82
81
  await ensureDesktopConfigInitialized();
83
82
  console.log(`Starting gateway server on port ${port}...`);
84
- // 若默认智能体或环境变量指定为 local provider,后台启动本地 LLM 子进程(不阻塞主服务启动)
85
- // 仅读 env 时,桌面端选「本机」默认 agent 时可能未设 OPENBOT_PROVIDER,导致本地服务未启、出现 Connection error
86
- const envProvider = process.env.OPENBOT_PROVIDER ?? "";
87
- let shouldStartLocal = envProvider === "local";
88
- let defaultLocalModel;
89
- let defaultAgentContextSize;
83
+ // 每次启动时按已保存配置尝试启动本地模型服务(不阻塞、不影响主进程;失败仅提示)
90
84
  try {
91
- const defaultAgent = await loadDesktopAgentConfig("default");
92
- if (defaultAgent) {
93
- defaultAgentContextSize = defaultAgent.contextSize;
94
- if (!shouldStartLocal) {
95
- shouldStartLocal =
96
- defaultAgent.provider === "local" &&
97
- defaultAgent.runnerType !== "coze" &&
98
- defaultAgent.runnerType !== "openclawx" &&
99
- defaultAgent.runnerType !== "opencode" &&
100
- defaultAgent.runnerType !== "claude_code";
101
- }
102
- if (shouldStartLocal && defaultAgent.provider === "local" && defaultAgent.model?.trim()) {
103
- defaultLocalModel = defaultAgent.model.trim();
104
- }
105
- }
85
+ console.log("[local-llm] 网关启动:按已保存配置尝试启动本地模型服务…");
86
+ await tryStartLocalModelFromSavedConfig();
106
87
  }
107
- catch {
108
- // ignore
109
- }
110
- if (shouldStartLocal) {
111
- // 若缺省模型已指定但文件不在缓存中,不启动本地服务,标记不可用,由用户在设置中下载后手动启动
112
- const llmFileExists = !defaultLocalModel || isModelFileInCache(defaultLocalModel);
113
- if (!llmFileExists) {
114
- process.env.LOCAL_LLM_START_FAILED = `缺省模型文件不存在: ${defaultLocalModel},请先在「模型管理」中下载或选择已安装模型后点击「启动本地模型服务」`;
115
- console.warn("[local-llm] 未启动:", process.env.LOCAL_LLM_START_FAILED);
116
- }
117
- else {
118
- const opts = {
119
- ...(defaultLocalModel ? { llmModelPath: defaultLocalModel } : {}),
120
- contextSize: defaultAgentContextSize ?? 32768,
121
- };
122
- startLocalLlmServer(opts)
123
- .then((handle) => {
124
- process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
125
- delete process.env.LOCAL_LLM_START_FAILED;
126
- console.log("[local-llm] 已就绪:", handle.baseUrl);
127
- })
128
- .catch((e) => {
129
- const msg = e instanceof Error ? e.message : String(e);
130
- process.env.LOCAL_LLM_START_FAILED = msg;
131
- console.warn("[local-llm] 启动失败:", msg);
132
- });
133
- }
88
+ catch (e) {
89
+ const msg = e instanceof Error ? e.message : String(e);
90
+ console.log("[local-llm] 提示:启动时发生异常,已跳过。", msg);
134
91
  }
135
92
  setBackendBaseUrl(`http://localhost:${port}`);
136
93
  const { app: nestApp, express: nestExpress } = await createNestAppEmbedded();
@@ -11,5 +11,6 @@ export interface NestAppResult {
11
11
  export declare function createNestAppEmbedded(): Promise<NestAppResult>;
12
12
  /**
13
13
  * 独立启动时使用:设置 globalPrefix 并监听端口。
14
+ * 先执行桌面配置初始化,保证首次启动即有 local provider 与缺省模型。
14
15
  */
15
16
  export declare function createNestAppStandalone(port?: number): Promise<INestApplication>;
@@ -6,6 +6,7 @@
6
6
  import { NestFactory } from '@nestjs/core';
7
7
  import express from 'express';
8
8
  import { AppModule } from './app.module.js';
9
+ import { ensureDesktopConfigInitialized } from '../core/config/desktop-config.js';
9
10
  const BODY_LIMIT = '10mb';
10
11
  /**
11
12
  * 创建 Nest 应用(内嵌模式):不 listen,不设置 globalPrefix。
@@ -27,8 +28,10 @@ export async function createNestAppEmbedded() {
27
28
  }
28
29
  /**
29
30
  * 独立启动时使用:设置 globalPrefix 并监听端口。
31
+ * 先执行桌面配置初始化,保证首次启动即有 local provider 与缺省模型。
30
32
  */
31
33
  export async function createNestAppStandalone(port = 38081) {
34
+ await ensureDesktopConfigInitialized();
32
35
  const app = await NestFactory.create(AppModule, {
33
36
  cors: true,
34
37
  });
@@ -153,25 +153,48 @@ export declare class ConfigController {
153
153
  baseUrl: string | undefined;
154
154
  };
155
155
  };
156
- /** 启动本地模型服务:可选指定 LLMEmbedding 模型(文件名或 hf: URI),先停后启 */
156
+ /** 启动本地模型服务:LLM/Embedding 任一已下载即可启动,只启动已存在的模型并提示;失败仅返回错误信息不抛错 */
157
157
  startLocalLlm(body: {
158
158
  llmModelUri?: string;
159
159
  embeddingModelUri?: string;
160
160
  }): Promise<{
161
+ success: boolean;
162
+ data: {
163
+ error: string;
164
+ baseUrl?: undefined;
165
+ message?: undefined;
166
+ };
167
+ } | {
161
168
  success: boolean;
162
169
  data: {
163
170
  baseUrl: string;
171
+ message: string;
172
+ error?: undefined;
164
173
  };
165
174
  }>;
166
- /** 开始后台下载模型(立即返回,进度通过 GET local-models/progress/:uri 轮询) */
175
+ /** 开始后台下载模型(立即返回,进度通过 GET local-models/progress 轮询)。useMirror=true 使用国内镜像。 */
167
176
  startDownload(body: {
168
177
  modelUri: string;
178
+ useMirror?: boolean;
169
179
  }): Promise<{
170
180
  success: boolean;
171
181
  data: {
172
182
  filename: string;
173
183
  };
174
184
  }>;
185
+ /** 取消指定模型的下载 */
186
+ cancelDownload(body: {
187
+ modelUri: string;
188
+ }): {
189
+ success: boolean;
190
+ };
191
+ /** 推荐模型列表(含是否已安装),用于展示「已下载」或中国/全球下载按钮 */
192
+ getRecommendedWithStatus(): Promise<{
193
+ success: boolean;
194
+ data: (import("./local-models.service.js").RecommendedModel & {
195
+ isInstalled: boolean;
196
+ })[];
197
+ }>;
175
198
  /** 查询下载进度 */
176
199
  getDownloadProgress(uri: string): {
177
200
  success: boolean;
@@ -12,9 +12,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
12
  };
13
13
  import { Controller, Get, Put, Body, Param, Query, Delete, Post } from '@nestjs/common';
14
14
  import { OPENCODE_FREE_MODELS } from '../../core/agent/proxy/adapters/opencode-free-models.js';
15
- import { loadDesktopAgentConfig } from '../../core/config/desktop-config.js';
15
+ import { loadDesktopAgentConfig, setDefaultModel } from '../../core/config/desktop-config.js';
16
16
  import { startLocalLlmServer, stopLocalLlmServer } from '../../core/local-llm-server/index.js';
17
- import { toModelPathForStart, LOCAL_LLM_CACHE_DIR } from '../../core/local-llm-server/model-resolve.js';
17
+ import { resolveModelPathInCache, LOCAL_LLM_CACHE_DIR } from '../../core/local-llm-server/model-resolve.js';
18
18
  import { ConfigService } from './config.service.js';
19
19
  import { LocalModelsService } from './local-models.service.js';
20
20
  let ConfigController = class ConfigController {
@@ -93,12 +93,30 @@ let ConfigController = class ConfigController {
93
93
  data: { available, error: error || undefined, baseUrl: baseUrl || undefined },
94
94
  };
95
95
  }
96
- /** 启动本地模型服务:可选指定 LLMEmbedding 模型(文件名或 hf: URI),先停后启 */
96
+ /** 启动本地模型服务:LLM/Embedding 任一已下载即可启动,只启动已存在的模型并提示;失败仅返回错误信息不抛错 */
97
97
  async startLocalLlm(body) {
98
98
  stopLocalLlmServer();
99
99
  delete process.env.LOCAL_LLM_BASE_URL;
100
100
  delete process.env.LOCAL_LLM_START_FAILED;
101
- let contextSize = 32768;
101
+ const llmPath = body.llmModelUri?.trim();
102
+ const embPath = body.embeddingModelUri?.trim();
103
+ if (!llmPath && !embPath) {
104
+ return { success: false, data: { error: '请至少选择 LLM 或 Embedding 模型之一' } };
105
+ }
106
+ const llmResolved = llmPath ? resolveModelPathInCache(llmPath, LOCAL_LLM_CACHE_DIR) : '';
107
+ const embResolved = embPath ? resolveModelPathInCache(embPath, LOCAL_LLM_CACHE_DIR) : '';
108
+ if (!llmResolved && !embResolved) {
109
+ return { success: false, data: { error: '未找到已下载的模型文件,请先在「模型管理」中下载' } };
110
+ }
111
+ if (llmPath) {
112
+ try {
113
+ await setDefaultModel('local', llmPath);
114
+ }
115
+ catch {
116
+ // 保存失败不阻断启动
117
+ }
118
+ }
119
+ let contextSize;
102
120
  try {
103
121
  const defaultAgent = await loadDesktopAgentConfig('default');
104
122
  if (defaultAgent?.contextSize != null && defaultAgent.contextSize > 0) {
@@ -108,29 +126,48 @@ let ConfigController = class ConfigController {
108
126
  catch {
109
127
  // ignore
110
128
  }
111
- const llmPath = body.llmModelUri?.trim();
112
- const embPath = body.embeddingModelUri?.trim();
129
+ if (contextSize == null) {
130
+ const envMax = process.env.LOCAL_LLM_CONTEXT_MAX;
131
+ contextSize = envMax != null && String(envMax).trim() !== '' ? parseInt(envMax, 10) || 32768 : 32768;
132
+ }
113
133
  const opts = {
114
- ...(llmPath ? { llmModelPath: toModelPathForStart(llmPath, LOCAL_LLM_CACHE_DIR) } : {}),
115
- ...(embPath ? { embeddingModelPath: toModelPathForStart(embPath, LOCAL_LLM_CACHE_DIR) } : {}),
134
+ ...(llmResolved ? { llmModelPath: llmResolved } : {}),
135
+ ...(embResolved ? { embeddingModelPath: embResolved } : {}),
116
136
  contextSize,
117
137
  };
118
138
  try {
119
139
  const handle = await startLocalLlmServer(opts);
120
140
  process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
121
- return { success: true, data: { baseUrl: handle.baseUrl } };
141
+ const message = llmResolved && embResolved
142
+ ? '已启动 LLM + Embedding'
143
+ : llmResolved
144
+ ? '已启动 LLM 模型(当前未使用 Embedding)'
145
+ : '已启动 Embedding 模型(当前未使用 LLM)';
146
+ return { success: true, data: { baseUrl: handle.baseUrl, message } };
122
147
  }
123
148
  catch (e) {
124
149
  const msg = e instanceof Error ? e.message : String(e);
125
150
  process.env.LOCAL_LLM_START_FAILED = msg;
126
- throw new Error(msg);
151
+ return { success: false, data: { error: msg } };
127
152
  }
128
153
  }
129
- /** 开始后台下载模型(立即返回,进度通过 GET local-models/progress/:uri 轮询) */
154
+ /** 开始后台下载模型(立即返回,进度通过 GET local-models/progress 轮询)。useMirror=true 使用国内镜像。 */
130
155
  async startDownload(body) {
131
- const result = await this.localModelsService.startDownload(body.modelUri);
156
+ const result = await this.localModelsService.startDownload(body.modelUri, {
157
+ useMirror: body.useMirror === true,
158
+ });
132
159
  return { success: true, data: result };
133
160
  }
161
+ /** 取消指定模型的下载 */
162
+ cancelDownload(body) {
163
+ this.localModelsService.cancelDownload(body.modelUri);
164
+ return { success: true };
165
+ }
166
+ /** 推荐模型列表(含是否已安装),用于展示「已下载」或中国/全球下载按钮 */
167
+ async getRecommendedWithStatus() {
168
+ const models = await this.localModelsService.getRecommendedWithStatus();
169
+ return { success: true, data: models };
170
+ }
134
171
  /** 查询下载进度 */
135
172
  getDownloadProgress(uri) {
136
173
  const progress = this.localModelsService.getDownloadProgress(uri);
@@ -219,6 +256,19 @@ __decorate([
219
256
  __metadata("design:paramtypes", [Object]),
220
257
  __metadata("design:returntype", Promise)
221
258
  ], ConfigController.prototype, "startDownload", null);
259
+ __decorate([
260
+ Post('local-models/cancel-download'),
261
+ __param(0, Body()),
262
+ __metadata("design:type", Function),
263
+ __metadata("design:paramtypes", [Object]),
264
+ __metadata("design:returntype", void 0)
265
+ ], ConfigController.prototype, "cancelDownload", null);
266
+ __decorate([
267
+ Get('local-models/recommended-with-status'),
268
+ __metadata("design:type", Function),
269
+ __metadata("design:paramtypes", []),
270
+ __metadata("design:returntype", Promise)
271
+ ], ConfigController.prototype, "getRecommendedWithStatus", null);
222
272
  __decorate([
223
273
  Get('local-models/progress'),
224
274
  __param(0, Query('uri')),
@@ -91,11 +91,14 @@ export declare class ConfigService {
91
91
  private configPath;
92
92
  private config;
93
93
  constructor(agentConfigService: AgentConfigService);
94
+ /** 预装本地推理缺省:与 desktop-config 的 DEFAULT_LOCAL_LLM_MODEL_ID / DEFAULT_LOCAL_MODEL_ITEM_CODE 一致 */
95
+ private static readonly DEFAULT_LOCAL_MODEL_ID;
96
+ private static readonly DEFAULT_LOCAL_MODEL_ITEM_CODE;
94
97
  private getDefaultConfig;
95
98
  /** 当前缺省智能体 id */
96
99
  getDefaultAgentId(config?: AppConfig): string;
97
100
  private loadConfig;
98
- /** 每次获取前从磁盘重新读取,保证打开配置界面时显示最新(含 CLI 写入的配置) */
101
+ /** 每次获取前从磁盘重新读取,保证打开配置界面时显示最新(含 CLI 写入的配置)。本地 LLM 可用时注入 local 与缺省模型项,供所有智能体使用。 */
99
102
  getConfig(): Promise<AppConfig>;
100
103
  updateConfig(updates: Partial<AppConfig>): Promise<AppConfig>;
101
104
  private saveConfig;