@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.
- package/README.md +10 -0
- package/apps/desktop/renderer/dist/assets/index-M5VGUUpo.js +93 -0
- package/apps/desktop/renderer/dist/assets/index-y8oE2q_u.css +10 -0
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/cli/cli.js +107 -0
- package/dist/core/agent/agent-manager.js +13 -2
- package/dist/core/agent/proxy/adapters/local-adapter.js +1 -1
- package/dist/core/config/desktop-config.d.ts +4 -1
- package/dist/core/config/desktop-config.js +108 -21
- package/dist/core/config/provider-support-default.js +26 -0
- package/dist/core/local-llm-server/download-model.d.ts +16 -0
- package/dist/core/local-llm-server/download-model.js +37 -0
- package/dist/core/local-llm-server/index.d.ts +32 -0
- package/dist/core/local-llm-server/index.js +147 -0
- package/dist/core/local-llm-server/llm-context.d.ts +65 -0
- package/dist/core/local-llm-server/llm-context.js +242 -0
- package/dist/core/local-llm-server/model-resolve.d.ts +27 -0
- package/dist/core/local-llm-server/model-resolve.js +90 -0
- package/dist/core/local-llm-server/server.d.ts +1 -0
- package/dist/core/local-llm-server/server.js +234 -0
- package/dist/core/local-llm-server/start-from-config.d.ts +5 -0
- package/dist/core/local-llm-server/start-from-config.js +50 -0
- package/dist/core/mcp/transport/stdio.d.ts +6 -0
- package/dist/core/mcp/transport/stdio.js +107 -27
- package/dist/core/memory/local-embedding-llama.js +2 -4
- package/dist/core/memory/local-embedding.d.ts +4 -3
- package/dist/core/memory/local-embedding.js +43 -3
- package/dist/gateway/methods/agent-chat.js +80 -41
- package/dist/gateway/server.js +10 -0
- package/dist/server/agent-config/agent-config.controller.d.ts +1 -1
- package/dist/server/agent-config/agent-config.service.d.ts +2 -0
- package/dist/server/agent-config/agent-config.service.js +5 -0
- package/dist/server/bootstrap.d.ts +1 -0
- package/dist/server/bootstrap.js +3 -0
- package/dist/server/config/config.controller.d.ts +81 -4
- package/dist/server/config/config.controller.js +185 -3
- package/dist/server/config/config.module.js +3 -2
- package/dist/server/config/config.service.d.ts +4 -1
- package/dist/server/config/config.service.js +62 -9
- package/dist/server/config/local-models.service.d.ts +67 -0
- package/dist/server/config/local-models.service.js +243 -0
- package/package.json +1 -1
- package/presets/preset-agents.json +6 -2
- package/presets/preset-config.json +24 -6
- package/presets/recommended-local-models.json +42 -0
- package/apps/desktop/renderer/dist/assets/index-BSfTiTKo.css +0 -10
- package/apps/desktop/renderer/dist/assets/index-DgLpQsA-.js +0 -89
- 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 ??
|
|
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
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
*
|
|
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") {
|
|
@@ -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) {
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
346
|
-
|
|
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);
|
package/dist/gateway/server.js
CHANGED
|
@@ -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>;
|
package/dist/server/bootstrap.js
CHANGED
|
@@ -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
|
});
|