@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.
- package/README.md +60 -42
- package/apps/desktop/renderer/dist/assets/index-BHY1xIZQ.css +10 -0
- package/apps/desktop/renderer/dist/assets/index-DQxlVuBe.js +93 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/cli/cli.js +29 -0
- package/dist/cli/extension-cmd.d.ts +15 -0
- package/dist/cli/extension-cmd.js +107 -0
- package/dist/core/agent/agent-dir.d.ts +6 -0
- package/dist/core/agent/agent-dir.js +8 -0
- package/dist/core/agent/agent-manager.d.ts +13 -0
- package/dist/core/agent/agent-manager.js +77 -7
- package/dist/core/agent/proxy/adapters/claude-code-adapter.d.ts +2 -0
- package/dist/core/agent/proxy/adapters/claude-code-adapter.js +186 -0
- package/dist/core/agent/proxy/adapters/local-adapter.js +3 -1
- package/dist/core/agent/proxy/adapters/opencode-adapter.js +65 -29
- package/dist/core/agent/proxy/adapters/opencode-local-runner.js +9 -0
- package/dist/core/agent/proxy/index.js +2 -0
- package/dist/core/agent/token-usage-log-extension.d.ts +14 -0
- package/dist/core/agent/token-usage-log-extension.js +61 -0
- package/dist/core/config/desktop-config.d.ts +24 -2
- package/dist/core/config/desktop-config.js +87 -10
- package/dist/core/config/provider-support-default.js +26 -0
- package/dist/core/extensions/index.d.ts +1 -0
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/load.d.ts +11 -0
- package/dist/core/extensions/load.js +101 -0
- package/dist/core/local-llm-server/index.d.ts +32 -0
- package/dist/core/local-llm-server/index.js +126 -0
- package/dist/core/local-llm-server/llm-context.d.ts +60 -0
- package/dist/core/local-llm-server/llm-context.js +221 -0
- package/dist/core/local-llm-server/model-resolve.d.ts +20 -0
- package/dist/core/local-llm-server/model-resolve.js +58 -0
- package/dist/core/local-llm-server/server.d.ts +1 -0
- package/dist/core/local-llm-server/server.js +235 -0
- package/dist/core/mcp/adapter.d.ts +4 -2
- package/dist/core/mcp/adapter.js +10 -4
- package/dist/core/mcp/index.d.ts +2 -0
- package/dist/core/mcp/index.js +1 -0
- package/dist/core/mcp/operator.d.ts +2 -0
- package/dist/core/mcp/operator.js +1 -1
- package/dist/core/memory/local-embedding.d.ts +4 -3
- package/dist/core/memory/local-embedding.js +43 -3
- package/dist/core/tools/index.d.ts +1 -0
- package/dist/core/tools/index.js +1 -0
- package/dist/core/tools/truncate-result.d.ts +14 -0
- package/dist/core/tools/truncate-result.js +27 -0
- package/dist/core/tools/web-search/create-web-search-tool.d.ts +17 -0
- package/dist/core/tools/web-search/create-web-search-tool.js +87 -0
- package/dist/core/tools/web-search/index.d.ts +4 -0
- package/dist/core/tools/web-search/index.js +2 -0
- package/dist/core/tools/web-search/providers/brave.d.ts +2 -0
- package/dist/core/tools/web-search/providers/brave.js +87 -0
- package/dist/core/tools/web-search/providers/duck-duck-scrape.d.ts +2 -0
- package/dist/core/tools/web-search/providers/duck-duck-scrape.js +47 -0
- package/dist/core/tools/web-search/providers/index.d.ts +5 -0
- package/dist/core/tools/web-search/providers/index.js +13 -0
- package/dist/core/tools/web-search/types.d.ts +35 -0
- package/dist/core/tools/web-search/types.js +4 -0
- package/dist/gateway/methods/agent-chat.js +74 -42
- package/dist/gateway/methods/run-scheduled-task.js +2 -0
- package/dist/gateway/server.js +54 -1
- package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
- package/dist/server/agent-config/agent-config.service.d.ts +17 -3
- package/dist/server/agent-config/agent-config.service.js +23 -0
- package/dist/server/config/config.controller.d.ts +84 -4
- package/dist/server/config/config.controller.js +135 -3
- package/dist/server/config/config.module.js +3 -2
- package/dist/server/config/config.service.d.ts +14 -0
- package/dist/server/config/local-models.service.d.ts +52 -0
- package/dist/server/config/local-models.service.js +211 -0
- package/package.json +3 -1
- package/presets/preset-agents.json +121 -91
- package/presets/recommended-local-models.json +42 -0
- package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +9 -0
- package/presets/workspaces/office-automation/skills/rpa-helper/SKILL.md +9 -0
- package/presets/workspaces/self-media-bot/skills/self-media-tools/SKILL.md +9 -0
- package/apps/desktop/renderer/dist/assets/index-BGHtXhm3.js +0 -89
- 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,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
|
+
}
|
|
@@ -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
|
-
*
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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;
|
package/dist/gateway/server.js
CHANGED
|
@@ -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 };
|