@next-open-ai/openclawx 0.8.40 → 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 (48) hide show
  1. package/README.md +10 -0
  2. package/apps/desktop/renderer/dist/assets/index-M5VGUUpo.js +93 -0
  3. package/apps/desktop/renderer/dist/assets/index-y8oE2q_u.css +10 -0
  4. package/apps/desktop/renderer/dist/index.html +2 -2
  5. package/dist/cli/cli.js +107 -0
  6. package/dist/core/agent/agent-manager.js +13 -2
  7. package/dist/core/agent/proxy/adapters/local-adapter.js +1 -1
  8. package/dist/core/config/desktop-config.d.ts +4 -1
  9. package/dist/core/config/desktop-config.js +108 -21
  10. package/dist/core/config/provider-support-default.js +26 -0
  11. package/dist/core/local-llm-server/download-model.d.ts +16 -0
  12. package/dist/core/local-llm-server/download-model.js +37 -0
  13. package/dist/core/local-llm-server/index.d.ts +32 -0
  14. package/dist/core/local-llm-server/index.js +147 -0
  15. package/dist/core/local-llm-server/llm-context.d.ts +65 -0
  16. package/dist/core/local-llm-server/llm-context.js +242 -0
  17. package/dist/core/local-llm-server/model-resolve.d.ts +27 -0
  18. package/dist/core/local-llm-server/model-resolve.js +90 -0
  19. package/dist/core/local-llm-server/server.d.ts +1 -0
  20. package/dist/core/local-llm-server/server.js +234 -0
  21. package/dist/core/local-llm-server/start-from-config.d.ts +5 -0
  22. package/dist/core/local-llm-server/start-from-config.js +50 -0
  23. package/dist/core/mcp/transport/stdio.d.ts +6 -0
  24. package/dist/core/mcp/transport/stdio.js +107 -27
  25. package/dist/core/memory/local-embedding-llama.js +2 -4
  26. package/dist/core/memory/local-embedding.d.ts +4 -3
  27. package/dist/core/memory/local-embedding.js +43 -3
  28. package/dist/gateway/methods/agent-chat.js +80 -41
  29. package/dist/gateway/server.js +10 -0
  30. package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
  31. package/dist/server/agent-config/agent-config.service.d.ts +2 -0
  32. package/dist/server/agent-config/agent-config.service.js +5 -0
  33. package/dist/server/bootstrap.d.ts +1 -0
  34. package/dist/server/bootstrap.js +3 -0
  35. package/dist/server/config/config.controller.d.ts +81 -4
  36. package/dist/server/config/config.controller.js +185 -3
  37. package/dist/server/config/config.module.js +3 -2
  38. package/dist/server/config/config.service.d.ts +4 -1
  39. package/dist/server/config/config.service.js +62 -9
  40. package/dist/server/config/local-models.service.d.ts +67 -0
  41. package/dist/server/config/local-models.service.js +243 -0
  42. package/package.json +1 -1
  43. package/presets/preset-agents.json +6 -2
  44. package/presets/preset-config.json +24 -6
  45. package/presets/recommended-local-models.json +42 -0
  46. package/apps/desktop/renderer/dist/assets/index-BSfTiTKo.css +0 -10
  47. package/apps/desktop/renderer/dist/assets/index-DgLpQsA-.js +0 -89
  48. package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +0 -9
@@ -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 = {
@@ -1,10 +1,11 @@
1
1
  /**
2
- * 本地 embedding:仅 node-llama-cpp (GGUF)。不可用时返回 null,由上层决定是否使用在线 RAG。
2
+ * 本地 embedding:优先走本地 LLM 子进程服务(/v1/embeddings),
3
+ * 不可用时回退到 node-llama-cpp 直接加载(GGUF)。
3
4
  */
4
5
  import type { IEmbeddingProvider } from "./embedding-types.js";
5
6
  export declare function getLocalEmbeddingUnavailableReason(): string | null;
6
7
  /**
7
- * 获取本地 embedding 提供方(懒加载,失败后不再重试)。
8
- * 仅使用 node-llama-cpp (GGUF)。不可用时返回 null。
8
+ * 获取本地 embedding 提供方(懒加载)。
9
+ * 优先使用本地 LLM 子进程服务;不可用时回退到 node-llama-cpp 直接加载。
9
10
  */
10
11
  export declare function getLocalEmbeddingProvider(): Promise<IEmbeddingProvider | null>;
@@ -5,19 +5,59 @@ let envLogged = false;
5
5
  export function getLocalEmbeddingUnavailableReason() {
6
6
  return getLocalEmbeddingLlamaUnavailableReason();
7
7
  }
8
+ /** 通过本地 LLM 子进程服务的 /v1/embeddings 接口获取向量 */
9
+ function createLocalServerEmbeddingProvider(baseUrl) {
10
+ return {
11
+ name: "local-llm-server",
12
+ async embed(text) {
13
+ try {
14
+ const res = await fetch(`${baseUrl}/embeddings`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json", Authorization: "Bearer local" },
17
+ body: JSON.stringify({ input: text }),
18
+ signal: AbortSignal.timeout(30_000),
19
+ });
20
+ if (!res.ok)
21
+ return null;
22
+ const data = await res.json();
23
+ const vec = data?.data?.[0]?.embedding;
24
+ return Array.isArray(vec) && vec.length > 0 ? vec : null;
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ },
30
+ };
31
+ }
8
32
  /**
9
- * 获取本地 embedding 提供方(懒加载,失败后不再重试)。
10
- * 仅使用 node-llama-cpp (GGUF)。不可用时返回 null。
33
+ * 获取本地 embedding 提供方(懒加载)。
34
+ * 优先使用本地 LLM 子进程服务;不可用时回退到 node-llama-cpp 直接加载。
11
35
  */
12
36
  export async function getLocalEmbeddingProvider() {
13
37
  if (cached)
14
38
  return cached;
39
+ // 优先:本地 LLM 子进程服务
40
+ const localBaseUrl = process.env.LOCAL_LLM_BASE_URL;
41
+ if (localBaseUrl) {
42
+ const serverProvider = createLocalServerEmbeddingProvider(localBaseUrl);
43
+ // 快速探测服务是否可用
44
+ const testVec = await serverProvider.embed("test");
45
+ if (testVec !== null) {
46
+ cached = serverProvider;
47
+ if (!envLogged) {
48
+ envLogged = true;
49
+ console.log("[RAG embedding] 使用本地 LLM 子进程服务");
50
+ }
51
+ return cached;
52
+ }
53
+ }
54
+ // 回退:node-llama-cpp 直接加载
15
55
  const provider = await getLocalEmbeddingLlamaProvider(getRagLocalModelPathSync());
16
56
  if (provider) {
17
57
  cached = provider;
18
58
  if (!envLogged) {
19
59
  envLogged = true;
20
- console.warn("[RAG embedding] 本地模型使用 node-llama-cpp (GGUF)");
60
+ console.log("[RAG embedding] 使用 node-llama-cpp (GGUF) 直接加载");
21
61
  }
22
62
  return cached;
23
63
  }
@@ -9,6 +9,23 @@ import { consumePendingAgentReload } from "../../core/config/agent-reload-pendin
9
9
  import { registerProxyRunAbort } from "../proxy-run-abort.js";
10
10
  import { getSessionOutlet, sendSessionMessage } from "../../core/session-outlet/index.js";
11
11
  const COMPOSITE_KEY_SEP = "::";
12
+ /** 将 delta/text 规范为字符串,避免 SDK 或上游返回对象时前端显示 [object Object] 或触发 Unknown value type */
13
+ function normalizeChunkText(v) {
14
+ if (v == null)
15
+ return "";
16
+ if (typeof v === "string")
17
+ return v;
18
+ if (typeof v.content === "string")
19
+ return v.content;
20
+ if (typeof v.text === "string")
21
+ return v.text;
22
+ try {
23
+ return String(JSON.stringify(v));
24
+ }
25
+ catch {
26
+ return String(v);
27
+ }
28
+ }
12
29
  /** 当前每个 session 的流式订阅(用于在 cancel 或新 run 前移除旧订阅,避免重复广播) */
13
30
  const sessionSubscriptionBySessionId = new Map();
14
31
  /**
@@ -40,7 +57,7 @@ const SYSTEM_MSG_PREFIX = "[System Message] ";
40
57
  const SYSTEM_MSG_SUFFIX = "\n";
41
58
  /**
42
59
  * 创建 Web 端会话消息消费者:将统一出口的 SessionMessage 转为 Gateway 事件并 broadcast。
43
- * 系统消息以 agent.chunk 形式发送,正文带 [System Message] 前缀且结尾换行,与当轮回复分行。
60
+ * 系统消息以独立事件 system_message 下发,前端做中间展示、不进入 session 聊天记录;各通道通过统一出口收到原始 system 消息后自行处理。
44
61
  */
45
62
  function createWebSessionConsumer(_sessionId) {
46
63
  return {
@@ -48,9 +65,8 @@ function createWebSessionConsumer(_sessionId) {
48
65
  const sid = msg.sessionId;
49
66
  if (msg.type === "system" && msg.code === "command.result") {
50
67
  const raw = msg.payload?.text ?? "";
51
- const text = raw ? SYSTEM_MSG_PREFIX + raw + SYSTEM_MSG_SUFFIX : "";
52
- if (text)
53
- broadcastToSession(sid, createEvent("agent.chunk", { text, sessionId: sid }));
68
+ if (raw)
69
+ broadcastToSession(sid, createEvent("system_message", { text: raw, code: "command.result", sessionId: sid }));
54
70
  broadcastToSession(sid, createEvent("turn_end", { sessionId: sid, content: "" }));
55
71
  broadcastToSession(sid, createEvent("message_complete", { sessionId: sid, content: "" }));
56
72
  broadcastToSession(sid, createEvent("agent_end", { sessionId: sid }));
@@ -59,10 +75,8 @@ function createWebSessionConsumer(_sessionId) {
59
75
  }
60
76
  if (msg.type === "system" && msg.code === "mcp.progress") {
61
77
  const raw = msg.payload?.message ?? msg.payload?.phase ?? "";
62
- if (raw) {
63
- const text = SYSTEM_MSG_PREFIX + raw + SYSTEM_MSG_SUFFIX;
64
- broadcastToSession(sid, createEvent("agent.chunk", { text, sessionId: sid }));
65
- }
78
+ if (raw)
79
+ broadcastToSession(sid, createEvent("system_message", { text: raw, code: "mcp.progress", sessionId: sid }));
66
80
  return;
67
81
  }
68
82
  if (msg.type === "chat") {
@@ -159,37 +173,40 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
159
173
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent_end", payload: {} });
160
174
  sendSessionMessage(targetSessionId, { type: "chat", code: "conversation_end", payload: {} });
161
175
  };
162
- try {
163
- await runForChannelStream({
164
- sessionId: targetSessionId,
165
- message,
166
- agentId: currentAgentId,
167
- signal,
168
- }, {
169
- onChunk(delta) {
170
- sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: delta } });
171
- },
172
- onTurnEnd() {
173
- sendSessionMessage(targetSessionId, { type: "chat", code: "turn_end", payload: {} });
174
- sendSessionMessage(targetSessionId, { type: "chat", code: "message_complete", payload: {} });
175
- },
176
- onDone() {
177
- finishAndUnregister();
178
- },
179
- });
180
- return { status: "completed", sessionId: targetSessionId };
181
- }
182
- catch (error) {
176
+ runForChannelStream({
177
+ sessionId: targetSessionId,
178
+ message,
179
+ agentId: currentAgentId,
180
+ signal,
181
+ }, {
182
+ onChunk(delta) {
183
+ sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: delta } });
184
+ },
185
+ onTurnEnd() {
186
+ sendSessionMessage(targetSessionId, { type: "chat", code: "turn_end", payload: {} });
187
+ sendSessionMessage(targetSessionId, { type: "chat", code: "message_complete", payload: {} });
188
+ },
189
+ onDone() {
190
+ finishAndUnregister();
191
+ },
192
+ }).catch((error) => {
183
193
  const isAbort = error?.name === "AbortError" || (typeof error?.message === "string" && error.message.includes("abort"));
184
194
  if (!isAbort)
185
195
  console.error(`Error in agent chat (proxy ${runnerType}):`, error);
186
196
  finishAndUnregister();
187
197
  if (!isAbort) {
188
- const errMsg = error?.message || String(error);
198
+ let errMsg = error?.message || String(error);
199
+ const needNormalize = typeof errMsg === "object" || (typeof errMsg === "string" && errMsg.includes("[object Object]"));
200
+ if (needNormalize) {
201
+ errMsg = normalizeChunkText(errMsg);
202
+ if (typeof errMsg === "string" && errMsg.includes("Unknown value type") && errMsg.includes("[object Object]")) {
203
+ errMsg = "模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
204
+ }
205
+ }
189
206
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: `请求失败:${errMsg}` } });
190
207
  }
191
- return { status: "completed", sessionId: targetSessionId };
192
- }
208
+ });
209
+ return { status: "streaming", sessionId: targetSessionId };
193
210
  }
194
211
  const isEphemeralSession = sessionType === "system" || sessionType === "scheduled";
195
212
  if (isEphemeralSession) {
@@ -250,10 +267,10 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
250
267
  const update = event;
251
268
  if (update.assistantMessageEvent && update.assistantMessageEvent.type === "text_delta") {
252
269
  hasReceivedAnyChunk = true;
253
- wsPayload = { type: "chat", code: "agent.chunk", payload: { text: update.assistantMessageEvent.delta } };
270
+ wsPayload = { type: "chat", code: "agent.chunk", payload: { text: normalizeChunkText(update.assistantMessageEvent.delta) } };
254
271
  }
255
272
  else if (update.assistantMessageEvent && update.assistantMessageEvent.type === "thinking_delta") {
256
- wsPayload = { type: "chat", code: "agent.chunk", payload: { text: update.assistantMessageEvent.delta, isThinking: true } };
273
+ wsPayload = { type: "chat", code: "agent.chunk", payload: { text: normalizeChunkText(update.assistantMessageEvent.delta), isThinking: true } };
257
274
  }
258
275
  else if (update.assistantMessageEvent?.type === "error" && update.assistantMessageEvent?.error?.errorMessage) {
259
276
  console.warn("[agent.chat] model error:", update.assistantMessageEvent.error.errorMessage);
@@ -278,9 +295,21 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
278
295
  hasReceivedAnyChunk = true;
279
296
  }
280
297
  if (msg?.errorMessage) {
281
- const errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
298
+ // 调试:定位本地 LLM 流式报错来源(pi-ai SDK 抛出的原始 errorMessage
299
+ console.error("[agent.chat] message_end errorMessage:", msg.errorMessage);
300
+ if (typeof msg.errorStack === "string")
301
+ console.error("[agent.chat] message_end errorStack:", msg.errorStack);
302
+ let errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
282
303
  ? "API 余额不足,请到「设置」检查并充值后重试。"
283
- : `请求失败:${msg.errorMessage}`;
304
+ : `请求失败:${normalizeChunkText(msg.errorMessage)}`;
305
+ if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
306
+ errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
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
+ }
284
313
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
285
314
  }
286
315
  wsPayload = null;
@@ -298,9 +327,20 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
298
327
  }
299
328
  }
300
329
  if (msg?.errorMessage) {
301
- const errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
330
+ // 调试:定位 turn_end SDK 传入的原始错误
331
+ console.error("[agent.chat] turn_end errorMessage:", msg.errorMessage);
332
+ if (typeof msg.errorStack === "string")
333
+ console.error("[agent.chat] turn_end errorStack:", msg.errorStack);
334
+ let errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
302
335
  ? "API 余额不足,请到「设置」检查并充值后重试。"
303
- : `请求失败:${msg.errorMessage}`;
336
+ : `请求失败:${normalizeChunkText(msg.errorMessage)}`;
337
+ if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
338
+ errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
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
+ }
304
344
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
305
345
  hasReceivedAnyChunk = true;
306
346
  }
@@ -342,9 +382,8 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
342
382
  sessionSubscriptionBySessionId.set(targetSessionId, unsubscribe);
343
383
  try {
344
384
  await session.sendUserMessage(message, { deliverAs: "followUp" });
345
- await agentDonePromise;
346
- console.log(`Agent chat completed for session ${targetSessionId}`);
347
- return { status: "completed", sessionId: targetSessionId };
385
+ // 流已启动,立即返回;前端以 agent_end 判断整轮结束,超时以「首包」计算更优
386
+ return { status: "streaming", sessionId: targetSessionId };
348
387
  }
349
388
  catch (error) {
350
389
  console.error(`Error in agent chat:`, error);
@@ -45,6 +45,7 @@ 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
47
  import { ensureDesktopConfigInitialized, getChannelsConfigSync } from "../core/config/desktop-config.js";
48
+ import { tryStartLocalModelFromSavedConfig } from "../core/local-llm-server/start-from-config.js";
48
49
  import { createNestAppEmbedded } from "../server/bootstrap.js";
49
50
  import { registerChannel, startAllChannels, stopAllChannels } from "./channel/registry.js";
50
51
  import { createFeishuChannel } from "./channel/adapters/feishu.js";
@@ -79,6 +80,15 @@ export async function startGatewayServer(port = 38080) {
79
80
  process.env.PORT = String(port);
80
81
  await ensureDesktopConfigInitialized();
81
82
  console.log(`Starting gateway server on port ${port}...`);
83
+ // 每次启动时按已保存配置尝试启动本地模型服务(不阻塞、不影响主进程;失败仅提示)
84
+ try {
85
+ console.log("[local-llm] 网关启动:按已保存配置尝试启动本地模型服务…");
86
+ await tryStartLocalModelFromSavedConfig();
87
+ }
88
+ catch (e) {
89
+ const msg = e instanceof Error ? e.message : String(e);
90
+ console.log("[local-llm] 提示:启动时发生异常,已跳过。", msg);
91
+ }
82
92
  setBackendBaseUrl(`http://localhost:${port}`);
83
93
  const { app: nestApp, express: nestExpress } = await createNestAppEmbedded();
84
94
  try {
@@ -25,7 +25,7 @@ export declare class AgentConfigController {
25
25
  success: boolean;
26
26
  data: AgentConfigItem;
27
27
  }>;
28
- updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'mcpMaxResultTokens' | 'systemPrompt' | 'icon' | 'runnerType' | 'coze' | 'openclawx' | 'opencode' | 'claudeCode' | 'useLongMemory' | 'webSearch'>>): Promise<{
28
+ updateAgent(id: string, body: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'mcpMaxResultTokens' | 'systemPrompt' | 'icon' | 'runnerType' | 'coze' | 'openclawx' | 'opencode' | 'claudeCode' | 'useLongMemory' | 'webSearch' | 'contextSize'>>): Promise<{
29
29
  success: boolean;
30
30
  data: AgentConfigItem;
31
31
  }>;
@@ -86,6 +86,8 @@ export interface AgentConfigItem {
86
86
  provider?: 'brave' | 'duck-duck-scrape';
87
87
  maxResultTokens?: number;
88
88
  };
89
+ /** 本地模型上下文长度(token 数),仅 runnerType 为 local 时生效;默认 32768(32K) */
90
+ contextSize?: number;
89
91
  }
90
92
  export interface DeleteAgentOptions {
91
93
  /** 是否同时删除该工作区在磁盘上的目录及文件;默认 false(仅删数据库中的工作区相关数据,保留目录) */
@@ -217,6 +217,11 @@ let AgentConfigService = class AgentConfigService {
217
217
  agent.claudeCode = updates.claudeCode;
218
218
  if (updates.useLongMemory !== undefined)
219
219
  agent.useLongMemory = updates.useLongMemory;
220
+ if ('contextSize' in updates) {
221
+ const v = updates.contextSize;
222
+ agent.contextSize =
223
+ typeof v === 'number' && Number.isInteger(v) && v > 0 ? v : undefined;
224
+ }
220
225
  if (updates.webSearch !== undefined) {
221
226
  agent.webSearch =
222
227
  updates.webSearch && (updates.webSearch.enabled || updates.webSearch.provider)
@@ -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
  });