@next-open-ai/openclawx 0.8.36 → 0.8.48

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 (78) hide show
  1. package/README.md +60 -42
  2. package/apps/desktop/renderer/dist/assets/index-BHY1xIZQ.css +10 -0
  3. package/apps/desktop/renderer/dist/assets/index-DQxlVuBe.js +93 -0
  4. package/apps/desktop/renderer/dist/index.html +2 -2
  5. package/dist/cli/cli.js +29 -0
  6. package/dist/cli/extension-cmd.d.ts +15 -0
  7. package/dist/cli/extension-cmd.js +107 -0
  8. package/dist/core/agent/agent-dir.d.ts +6 -0
  9. package/dist/core/agent/agent-dir.js +8 -0
  10. package/dist/core/agent/agent-manager.d.ts +13 -0
  11. package/dist/core/agent/agent-manager.js +77 -7
  12. package/dist/core/agent/proxy/adapters/claude-code-adapter.d.ts +2 -0
  13. package/dist/core/agent/proxy/adapters/claude-code-adapter.js +186 -0
  14. package/dist/core/agent/proxy/adapters/local-adapter.js +3 -1
  15. package/dist/core/agent/proxy/adapters/opencode-adapter.js +65 -29
  16. package/dist/core/agent/proxy/adapters/opencode-local-runner.js +9 -0
  17. package/dist/core/agent/proxy/index.js +2 -0
  18. package/dist/core/agent/token-usage-log-extension.d.ts +14 -0
  19. package/dist/core/agent/token-usage-log-extension.js +61 -0
  20. package/dist/core/config/desktop-config.d.ts +24 -2
  21. package/dist/core/config/desktop-config.js +87 -10
  22. package/dist/core/config/provider-support-default.js +26 -0
  23. package/dist/core/extensions/index.d.ts +1 -0
  24. package/dist/core/extensions/index.js +1 -0
  25. package/dist/core/extensions/load.d.ts +11 -0
  26. package/dist/core/extensions/load.js +101 -0
  27. package/dist/core/local-llm-server/index.d.ts +32 -0
  28. package/dist/core/local-llm-server/index.js +126 -0
  29. package/dist/core/local-llm-server/llm-context.d.ts +60 -0
  30. package/dist/core/local-llm-server/llm-context.js +221 -0
  31. package/dist/core/local-llm-server/model-resolve.d.ts +20 -0
  32. package/dist/core/local-llm-server/model-resolve.js +58 -0
  33. package/dist/core/local-llm-server/server.d.ts +1 -0
  34. package/dist/core/local-llm-server/server.js +235 -0
  35. package/dist/core/mcp/adapter.d.ts +4 -2
  36. package/dist/core/mcp/adapter.js +10 -4
  37. package/dist/core/mcp/index.d.ts +2 -0
  38. package/dist/core/mcp/index.js +1 -0
  39. package/dist/core/mcp/operator.d.ts +2 -0
  40. package/dist/core/mcp/operator.js +1 -1
  41. package/dist/core/memory/local-embedding.d.ts +4 -3
  42. package/dist/core/memory/local-embedding.js +43 -3
  43. package/dist/core/tools/index.d.ts +1 -0
  44. package/dist/core/tools/index.js +1 -0
  45. package/dist/core/tools/truncate-result.d.ts +14 -0
  46. package/dist/core/tools/truncate-result.js +27 -0
  47. package/dist/core/tools/web-search/create-web-search-tool.d.ts +17 -0
  48. package/dist/core/tools/web-search/create-web-search-tool.js +87 -0
  49. package/dist/core/tools/web-search/index.d.ts +4 -0
  50. package/dist/core/tools/web-search/index.js +2 -0
  51. package/dist/core/tools/web-search/providers/brave.d.ts +2 -0
  52. package/dist/core/tools/web-search/providers/brave.js +87 -0
  53. package/dist/core/tools/web-search/providers/duck-duck-scrape.d.ts +2 -0
  54. package/dist/core/tools/web-search/providers/duck-duck-scrape.js +47 -0
  55. package/dist/core/tools/web-search/providers/index.d.ts +5 -0
  56. package/dist/core/tools/web-search/providers/index.js +13 -0
  57. package/dist/core/tools/web-search/types.d.ts +35 -0
  58. package/dist/core/tools/web-search/types.js +4 -0
  59. package/dist/gateway/methods/agent-chat.js +74 -42
  60. package/dist/gateway/methods/run-scheduled-task.js +2 -0
  61. package/dist/gateway/server.js +54 -1
  62. package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
  63. package/dist/server/agent-config/agent-config.service.d.ts +17 -3
  64. package/dist/server/agent-config/agent-config.service.js +23 -0
  65. package/dist/server/config/config.controller.d.ts +84 -4
  66. package/dist/server/config/config.controller.js +135 -3
  67. package/dist/server/config/config.module.js +3 -2
  68. package/dist/server/config/config.service.d.ts +14 -0
  69. package/dist/server/config/local-models.service.d.ts +52 -0
  70. package/dist/server/config/local-models.service.js +211 -0
  71. package/package.json +3 -1
  72. package/presets/preset-agents.json +121 -91
  73. package/presets/recommended-local-models.json +42 -0
  74. package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +9 -0
  75. package/presets/workspaces/office-automation/skills/rpa-helper/SKILL.md +9 -0
  76. package/presets/workspaces/self-media-bot/skills/self-media-tools/SKILL.md +9 -0
  77. package/apps/desktop/renderer/dist/assets/index-BGHtXhm3.js +0 -89
  78. package/apps/desktop/renderer/dist/assets/index-CB2-m4ae.css +0 -10
@@ -0,0 +1,87 @@
1
+ const PROVIDER_ID = "brave";
2
+ const BRAVE_WEB_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
3
+ const cache = new Map();
4
+ function cacheKey(query, count) {
5
+ return `${PROVIDER_ID}:${query}:${count}`;
6
+ }
7
+ export const braveProvider = {
8
+ id: PROVIDER_ID,
9
+ isAvailable(options) {
10
+ return !!(options.apiKey && String(options.apiKey).trim());
11
+ },
12
+ async search(params) {
13
+ const query = (params.query ?? "").trim();
14
+ const count = Math.min(10, Math.max(1, params.count ?? 5));
15
+ const apiKey = params.apiKey?.trim();
16
+ if (!apiKey) {
17
+ return {
18
+ query,
19
+ provider: PROVIDER_ID,
20
+ results: [],
21
+ count: 0,
22
+ tookMs: 0,
23
+ cached: false,
24
+ };
25
+ }
26
+ const cacheTtlMinutes = typeof params.cacheTtlMinutes === "number" && params.cacheTtlMinutes >= 0 ? params.cacheTtlMinutes : 0;
27
+ const key = cacheKey(query, count);
28
+ if (cacheTtlMinutes > 0) {
29
+ const hit = cache.get(key);
30
+ if (hit && hit.expiresAt > Date.now()) {
31
+ return { ...hit.result, cached: true };
32
+ }
33
+ }
34
+ const timeoutMs = (params.timeoutSeconds ?? 15) * 1000;
35
+ const controller = new AbortController();
36
+ const t = setTimeout(() => controller.abort(), timeoutMs);
37
+ const start = Date.now();
38
+ try {
39
+ const url = new URL(BRAVE_WEB_SEARCH_URL);
40
+ url.searchParams.set("q", query);
41
+ url.searchParams.set("count", String(count));
42
+ if (params.country?.trim())
43
+ url.searchParams.set("country", params.country.trim());
44
+ if (params.freshness?.trim())
45
+ url.searchParams.set("freshness", params.freshness.trim());
46
+ const res = await fetch(url.toString(), {
47
+ method: "GET",
48
+ headers: {
49
+ Accept: "application/json",
50
+ "X-Subscription-Token": apiKey,
51
+ },
52
+ signal: controller.signal,
53
+ });
54
+ const tookMs = Date.now() - start;
55
+ if (!res.ok) {
56
+ const text = await res.text();
57
+ throw new Error(`Brave Search API error ${res.status}: ${text || res.statusText}`);
58
+ }
59
+ const data = (await res.json());
60
+ const rawResults = data.web?.results ?? [];
61
+ const results = rawResults.slice(0, count).map((r) => ({
62
+ title: r.title ?? "",
63
+ url: r.url ?? "",
64
+ description: r.description,
65
+ published: r.age,
66
+ }));
67
+ const result = {
68
+ query,
69
+ provider: PROVIDER_ID,
70
+ results,
71
+ count: results.length,
72
+ tookMs,
73
+ cached: false,
74
+ };
75
+ if (cacheTtlMinutes > 0) {
76
+ cache.set(key, {
77
+ result,
78
+ expiresAt: Date.now() + cacheTtlMinutes * 60 * 1000,
79
+ });
80
+ }
81
+ return result;
82
+ }
83
+ finally {
84
+ clearTimeout(t);
85
+ }
86
+ },
87
+ };
@@ -0,0 +1,2 @@
1
+ import type { IWebSearchProvider } from "../types.js";
2
+ export declare const duckDuckScrapeProvider: IWebSearchProvider;
@@ -0,0 +1,47 @@
1
+ import { search as ddgSearch } from "duck-duck-scrape";
2
+ const PROVIDER_ID = "duck-duck-scrape";
3
+ const cache = new Map();
4
+ function cacheKey(query, count) {
5
+ return `${PROVIDER_ID}:${query}:${count}`;
6
+ }
7
+ export const duckDuckScrapeProvider = {
8
+ id: PROVIDER_ID,
9
+ isAvailable() {
10
+ return true;
11
+ },
12
+ async search(params) {
13
+ const query = (params.query ?? "").trim();
14
+ const count = Math.min(10, Math.max(1, params.count ?? 5));
15
+ const cacheTtlMinutes = typeof params.cacheTtlMinutes === "number" && params.cacheTtlMinutes >= 0 ? params.cacheTtlMinutes : 0;
16
+ const key = cacheKey(query, count);
17
+ if (cacheTtlMinutes > 0) {
18
+ const hit = cache.get(key);
19
+ if (hit && hit.expiresAt > Date.now()) {
20
+ return { ...hit.result, cached: true };
21
+ }
22
+ }
23
+ const start = Date.now();
24
+ const res = await ddgSearch(query, {});
25
+ const tookMs = Date.now() - start;
26
+ const results = (res.results ?? []).slice(0, count).map((r) => ({
27
+ title: r.title ?? "",
28
+ url: r.url ?? "",
29
+ description: r.description,
30
+ }));
31
+ const result = {
32
+ query,
33
+ provider: PROVIDER_ID,
34
+ results,
35
+ count: results.length,
36
+ tookMs,
37
+ cached: false,
38
+ };
39
+ if (cacheTtlMinutes > 0) {
40
+ cache.set(key, {
41
+ result,
42
+ expiresAt: Date.now() + cacheTtlMinutes * 60 * 1000,
43
+ });
44
+ }
45
+ return result;
46
+ },
47
+ };
@@ -0,0 +1,5 @@
1
+ import type { WebSearchProviderId, IWebSearchProvider } from "../types.js";
2
+ import { duckDuckScrapeProvider } from "./duck-duck-scrape.js";
3
+ import { braveProvider } from "./brave.js";
4
+ export declare function getWebSearchProvider(id: WebSearchProviderId): IWebSearchProvider;
5
+ export { duckDuckScrapeProvider, braveProvider };
@@ -0,0 +1,13 @@
1
+ import { duckDuckScrapeProvider } from "./duck-duck-scrape.js";
2
+ import { braveProvider } from "./brave.js";
3
+ const providers = {
4
+ "duck-duck-scrape": duckDuckScrapeProvider,
5
+ brave: braveProvider,
6
+ };
7
+ export function getWebSearchProvider(id) {
8
+ const p = providers[id];
9
+ if (!p)
10
+ throw new Error(`Unknown web search provider: ${id}`);
11
+ return p;
12
+ }
13
+ export { duckDuckScrapeProvider, braveProvider };
@@ -0,0 +1,35 @@
1
+ /**
2
+ * 在线搜索 Provider 统一类型,供 web_search 工具与多 Provider 实现使用。
3
+ */
4
+ export type WebSearchProviderId = "duck-duck-scrape" | "brave";
5
+ export interface WebSearchResultItem {
6
+ title: string;
7
+ url: string;
8
+ description?: string;
9
+ published?: string;
10
+ siteName?: string;
11
+ }
12
+ export interface WebSearchProviderResult {
13
+ query: string;
14
+ provider: WebSearchProviderId;
15
+ results: WebSearchResultItem[];
16
+ count: number;
17
+ tookMs?: number;
18
+ cached?: boolean;
19
+ }
20
+ export interface WebSearchSearchParams {
21
+ query: string;
22
+ count?: number;
23
+ apiKey?: string;
24
+ timeoutSeconds?: number;
25
+ cacheTtlMinutes?: number;
26
+ country?: string;
27
+ freshness?: string;
28
+ }
29
+ export interface IWebSearchProvider {
30
+ id: WebSearchProviderId;
31
+ isAvailable(options: {
32
+ apiKey?: string;
33
+ }): boolean;
34
+ search(params: WebSearchSearchParams): Promise<WebSearchProviderResult>;
35
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 在线搜索 Provider 统一类型,供 web_search 工具与多 Provider 实现使用。
3
+ */
4
+ export {};
@@ -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") {
@@ -145,7 +159,7 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
145
159
  apiKey = agentConfig.apiKey;
146
160
  }
147
161
  const runnerType = agentConfig?.runnerType ?? "local";
148
- const isProxyAgent = runnerType === "coze" || runnerType === "openclawx" || runnerType === "opencode";
162
+ const isProxyAgent = runnerType === "coze" || runnerType === "openclawx" || runnerType === "opencode" || runnerType === "claude_code";
149
163
  if (isProxyAgent) {
150
164
  console.log(`[agent.chat] Using proxy agent (${runnerType}) for session=${targetSessionId}, agentId=${currentAgentId}`);
151
165
  }
@@ -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) {
@@ -211,8 +228,10 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
211
228
  maxSessions: maxAgentSessions,
212
229
  targetAgentId: effectiveTargetAgentId,
213
230
  mcpServers: agentConfig?.mcpServers,
231
+ mcpMaxResultTokens: agentConfig?.mcpMaxResultTokens,
214
232
  systemPrompt: agentConfig?.systemPrompt,
215
233
  useLongMemory: agentConfig?.useLongMemory,
234
+ webSearch: agentConfig?.webSearch,
216
235
  });
217
236
  }
218
237
  catch (err) {
@@ -248,10 +267,10 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
248
267
  const update = event;
249
268
  if (update.assistantMessageEvent && update.assistantMessageEvent.type === "text_delta") {
250
269
  hasReceivedAnyChunk = true;
251
- wsPayload = { type: "chat", code: "agent.chunk", payload: { text: update.assistantMessageEvent.delta } };
270
+ wsPayload = { type: "chat", code: "agent.chunk", payload: { text: normalizeChunkText(update.assistantMessageEvent.delta) } };
252
271
  }
253
272
  else if (update.assistantMessageEvent && update.assistantMessageEvent.type === "thinking_delta") {
254
- 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 } };
255
274
  }
256
275
  else if (update.assistantMessageEvent?.type === "error" && update.assistantMessageEvent?.error?.errorMessage) {
257
276
  console.warn("[agent.chat] model error:", update.assistantMessageEvent.error.errorMessage);
@@ -276,9 +295,16 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
276
295
  hasReceivedAnyChunk = true;
277
296
  }
278
297
  if (msg?.errorMessage) {
279
- 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")
280
303
  ? "API 余额不足,请到「设置」检查并充值后重试。"
281
- : `请求失败:${msg.errorMessage}`;
304
+ : `请求失败:${normalizeChunkText(msg.errorMessage)}`;
305
+ if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
306
+ errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
307
+ }
282
308
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
283
309
  }
284
310
  wsPayload = null;
@@ -296,9 +322,16 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
296
322
  }
297
323
  }
298
324
  if (msg?.errorMessage) {
299
- const errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
325
+ // 调试:定位 turn_end SDK 传入的原始错误
326
+ console.error("[agent.chat] turn_end errorMessage:", msg.errorMessage);
327
+ if (typeof msg.errorStack === "string")
328
+ console.error("[agent.chat] turn_end errorStack:", msg.errorStack);
329
+ let errText = msg.errorMessage.includes("402") || msg.errorMessage.includes("Insufficient Balance")
300
330
  ? "API 余额不足,请到「设置」检查并充值后重试。"
301
- : `请求失败:${msg.errorMessage}`;
331
+ : `请求失败:${normalizeChunkText(msg.errorMessage)}`;
332
+ if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
333
+ errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
334
+ }
302
335
  sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
303
336
  hasReceivedAnyChunk = true;
304
337
  }
@@ -340,9 +373,8 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
340
373
  sessionSubscriptionBySessionId.set(targetSessionId, unsubscribe);
341
374
  try {
342
375
  await session.sendUserMessage(message, { deliverAs: "followUp" });
343
- await agentDonePromise;
344
- console.log(`Agent chat completed for session ${targetSessionId}`);
345
- return { status: "completed", sessionId: targetSessionId };
376
+ // 流已启动,立即返回;前端以 agent_end 判断整轮结束,超时以「首包」计算更优
377
+ return { status: "streaming", sessionId: targetSessionId };
346
378
  }
347
379
  catch (error) {
348
380
  console.error(`Error in agent chat:`, error);
@@ -61,8 +61,10 @@ export async function handleRunScheduledTask(req, res) {
61
61
  modelId,
62
62
  apiKey,
63
63
  mcpServers: agentConfig?.mcpServers,
64
+ mcpMaxResultTokens: agentConfig?.mcpMaxResultTokens,
64
65
  systemPrompt: agentConfig?.systemPrompt,
65
66
  useLongMemory: agentConfig?.useLongMemory,
67
+ webSearch: agentConfig?.webSearch,
66
68
  });
67
69
  let assistantContent = "";
68
70
  let turnPromptTokens = 0;
@@ -44,7 +44,9 @@ 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 } from "../core/config/desktop-config.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";
48
50
  import { createNestAppEmbedded } from "../server/bootstrap.js";
49
51
  import { registerChannel, startAllChannels, stopAllChannels } from "./channel/registry.js";
50
52
  import { createFeishuChannel } from "./channel/adapters/feishu.js";
@@ -79,6 +81,57 @@ export async function startGatewayServer(port = 38080) {
79
81
  process.env.PORT = String(port);
80
82
  await ensureDesktopConfigInitialized();
81
83
  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;
90
+ 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
+ }
106
+ }
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
+ }
134
+ }
82
135
  setBackendBaseUrl(`http://localhost:${port}`);
83
136
  const { app: nestApp, express: nestExpress } = await createNestAppEmbedded();
84
137
  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' | 'systemPrompt' | 'icon' | 'runnerType' | 'coze' | 'openclawx' | 'opencode'>>): 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
  }>;
@@ -3,8 +3,8 @@ import { DatabaseService } from '../database/database.service.js';
3
3
  import { WorkspaceService } from '../workspace/workspace.service.js';
4
4
  /** 缺省智能体 ID / 工作空间名,不可删除;对应目录 ~/.openbot/workspace/default */
5
5
  export declare const DEFAULT_AGENT_ID = "default";
6
- /** 执行器类型:local=本机,coze/openclawx/opencode=远程代理 */
7
- export type AgentRunnerType = 'local' | 'coze' | 'openclawx' | 'opencode';
6
+ /** 执行器类型:local=本机,coze/openclawx/opencode=远程代理,claude_code=本机 Claude Code CLI */
7
+ export type AgentRunnerType = 'local' | 'coze' | 'openclawx' | 'opencode' | 'claude_code';
8
8
  /** Coze 站点:cn=国内 api.coze.cn,com=国际 api.coze.com,凭证不通用 */
9
9
  export type CozeRegion = 'cn' | 'com';
10
10
  /** 某站点的 Bot 凭证 */
@@ -60,6 +60,8 @@ export interface AgentConfigItem {
60
60
  isDefault?: boolean;
61
61
  /** MCP 配置:数组(含 transport)或标准 JSON 对象(key 为服务器名称),创建 Session 时归一化使用 */
62
62
  mcpServers?: McpServerConfig[] | McpServersStandardFormat;
63
+ /** MCP 单次返回最大 token;不配置则不限制 */
64
+ mcpMaxResultTokens?: number;
63
65
  /** 自定义系统提示词,会与技能等一起组成最终 systemPrompt */
64
66
  systemPrompt?: string;
65
67
  /** 智能体图标标识(前端预设图标 id,如 default、star、code 等) */
@@ -72,8 +74,20 @@ export interface AgentConfigItem {
72
74
  openclawx?: AgentOpenClawXConfig;
73
75
  /** OpenCode 代理配置(runnerType 为 opencode 时使用) */
74
76
  opencode?: AgentOpenCodeConfig;
77
+ /** Claude Code 代理配置(runnerType 为 claude_code 时使用):工作目录等 */
78
+ claudeCode?: {
79
+ workingDirectory?: string;
80
+ };
75
81
  /** 是否使用经验(长记忆):memory_recall / save_experience;默认 true */
76
82
  useLongMemory?: boolean;
83
+ /** 在线搜索:启用后该智能体拥有 web_search 工具;可选默认 provider、maxResultTokens(前端默认 64K) */
84
+ webSearch?: {
85
+ enabled?: boolean;
86
+ provider?: 'brave' | 'duck-duck-scrape';
87
+ maxResultTokens?: number;
88
+ };
89
+ /** 本地模型上下文长度(token 数),仅 runnerType 为 local 时生效;默认 32768(32K) */
90
+ contextSize?: number;
77
91
  }
78
92
  export interface DeleteAgentOptions {
79
93
  /** 是否同时删除该工作区在磁盘上的目录及文件;默认 false(仅删数据库中的工作区相关数据,保留目录) */
@@ -104,7 +118,7 @@ export declare class AgentConfigService {
104
118
  /** 智能体图标标识 */
105
119
  icon?: string;
106
120
  }): Promise<AgentConfigItem>;
107
- updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'systemPrompt' | 'icon' | 'runnerType' | 'coze' | 'openclawx' | 'opencode' | 'useLongMemory'>>): Promise<AgentConfigItem>;
121
+ updateAgent(id: string, updates: Partial<Pick<AgentConfigItem, 'name' | 'provider' | 'model' | 'modelItemCode' | 'mcpServers' | 'mcpMaxResultTokens' | 'systemPrompt' | 'icon' | 'runnerType' | 'coze' | 'openclawx' | 'opencode' | 'claudeCode' | 'useLongMemory' | 'webSearch'>>): Promise<AgentConfigItem>;
108
122
  deleteAgent(id: string, options?: DeleteAgentOptions): Promise<void>;
109
123
  /** 仅删除数据库中与该工作区相关的数据(会话、定时任务、收藏等),不删磁盘目录 */
110
124
  private deleteWorkspaceDataFromDb;
@@ -162,6 +162,8 @@ let AgentConfigService = class AgentConfigService {
162
162
  agent.modelItemCode = updates.modelItemCode;
163
163
  if (updates.mcpServers !== undefined)
164
164
  agent.mcpServers = updates.mcpServers;
165
+ if (updates.mcpMaxResultTokens !== undefined)
166
+ agent.mcpMaxResultTokens = updates.mcpMaxResultTokens;
165
167
  if (updates.systemPrompt !== undefined)
166
168
  agent.systemPrompt = updates.systemPrompt?.trim() || undefined;
167
169
  if (updates.icon !== undefined)
@@ -211,8 +213,29 @@ let AgentConfigService = class AgentConfigService {
211
213
  agent.openclawx = updates.openclawx;
212
214
  if (updates.opencode !== undefined)
213
215
  agent.opencode = updates.opencode;
216
+ if (updates.claudeCode !== undefined)
217
+ agent.claudeCode = updates.claudeCode;
214
218
  if (updates.useLongMemory !== undefined)
215
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
+ }
225
+ if (updates.webSearch !== undefined) {
226
+ agent.webSearch =
227
+ updates.webSearch && (updates.webSearch.enabled || updates.webSearch.provider)
228
+ ? {
229
+ enabled: !!updates.webSearch.enabled,
230
+ provider: updates.webSearch.provider === 'brave' || updates.webSearch.provider === 'duck-duck-scrape'
231
+ ? updates.webSearch.provider
232
+ : 'duck-duck-scrape',
233
+ maxResultTokens: updates.webSearch.maxResultTokens != null && typeof updates.webSearch.maxResultTokens === 'number' && updates.webSearch.maxResultTokens > 0
234
+ ? updates.webSearch.maxResultTokens
235
+ : undefined,
236
+ }
237
+ : undefined;
238
+ }
216
239
  await this.writeAgentsFile(file);
217
240
  await addPendingAgentReload(id).catch(() => { });
218
241
  return { ...agent, isDefault: agent.id === DEFAULT_AGENT_ID };