@next-open-ai/openclawx 0.8.48 → 0.8.58
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/desktop/renderer/dist/assets/index-M5VGUUpo.js +93 -0
- package/apps/desktop/renderer/dist/assets/{index-BHY1xIZQ.css → index-y8oE2q_u.css} +1 -1
- package/apps/desktop/renderer/dist/index.html +2 -2
- package/dist/cli/cli.js +107 -0
- package/dist/core/agent/agent-manager.js +4 -0
- package/dist/core/config/desktop-config.d.ts +2 -1
- package/dist/core/config/desktop-config.js +92 -26
- 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.js +26 -5
- package/dist/core/local-llm-server/llm-context.d.ts +9 -4
- package/dist/core/local-llm-server/llm-context.js +35 -14
- package/dist/core/local-llm-server/model-resolve.d.ts +8 -1
- package/dist/core/local-llm-server/model-resolve.js +44 -12
- package/dist/core/local-llm-server/server.js +11 -12
- 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/gateway/methods/agent-chat.js +9 -0
- package/dist/gateway/server.js +8 -51
- package/dist/server/bootstrap.d.ts +1 -0
- package/dist/server/bootstrap.js +3 -0
- package/dist/server/config/config.controller.d.ts +25 -2
- package/dist/server/config/config.controller.js +62 -12
- 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 +16 -1
- package/dist/server/config/local-models.service.js +78 -46
- package/package.json +1 -1
- package/presets/preset-agents.json +6 -2
- package/presets/preset-config.json +24 -6
- package/apps/desktop/renderer/dist/assets/index-DQxlVuBe.js +0 -93
- package/presets/workspaces/finance-expert/skills/akshare-helper/SKILL.md +0 -9
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { createServer } from "node:http";
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
|
-
import { chatCompletionStream, chatCompletion, getEmbedding,
|
|
9
|
+
import { chatCompletionStream, chatCompletion, getEmbedding, isLlmReady, isEmbeddingReady, } from "./llm-context.js";
|
|
10
10
|
const LLM_MODEL_ID = process.env.LOCAL_LLM_MODEL_ID ?? "local-llm";
|
|
11
11
|
const EMB_MODEL_ID = process.env.LOCAL_EMB_MODEL_ID ?? "local-embedding";
|
|
12
12
|
function readBody(req) {
|
|
@@ -67,8 +67,8 @@ async function handleChatCompletions(req, res) {
|
|
|
67
67
|
catch {
|
|
68
68
|
return sendError(res, 400, "Invalid JSON body");
|
|
69
69
|
}
|
|
70
|
-
if (!
|
|
71
|
-
return sendError(res, 503, "
|
|
70
|
+
if (!isLlmReady())
|
|
71
|
+
return sendError(res, 503, "LLM 模型未加载,请先启动本地模型服务并选择 LLM 模型", "server_error");
|
|
72
72
|
if (!Array.isArray(body.messages)) {
|
|
73
73
|
return sendError(res, 400, "Missing or invalid 'messages' (must be an array)", "invalid_request_error");
|
|
74
74
|
}
|
|
@@ -157,8 +157,8 @@ async function handleEmbeddings(req, res) {
|
|
|
157
157
|
catch {
|
|
158
158
|
return sendError(res, 400, "Invalid JSON body", "invalid_request_error");
|
|
159
159
|
}
|
|
160
|
-
if (!
|
|
161
|
-
return sendError(res, 503, "
|
|
160
|
+
if (!isEmbeddingReady())
|
|
161
|
+
return sendError(res, 503, "Embedding 模型未加载,请先启动本地模型服务并选择 Embedding 模型", "server_error");
|
|
162
162
|
const input = body.input;
|
|
163
163
|
if (input === undefined || input === null) {
|
|
164
164
|
return sendError(res, 400, "Missing 'input' (string or array of strings)", "invalid_request_error");
|
|
@@ -186,13 +186,12 @@ async function handleEmbeddings(req, res) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
function handleModels(_req, res) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
data:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
});
|
|
189
|
+
const data = [];
|
|
190
|
+
if (isLlmReady())
|
|
191
|
+
data.push({ id: LLM_MODEL_ID, object: "model", created: 0, owned_by: "local" });
|
|
192
|
+
if (isEmbeddingReady())
|
|
193
|
+
data.push({ id: EMB_MODEL_ID, object: "model", created: 0, owned_by: "local" });
|
|
194
|
+
sendJson(res, 200, { object: "list", data });
|
|
196
195
|
}
|
|
197
196
|
export function createOpenAICompatServer(port) {
|
|
198
197
|
return new Promise((resolve, reject) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用「按当前已保存配置启动本地模型服务」逻辑。
|
|
3
|
+
* 供网关启动与 API 复用:读取默认智能体配置,尝试启动;失败只设 env 与日志,不抛错、不影响主进程。
|
|
4
|
+
* LLM/Embedding 任一存在即可启动,只提示不报错。
|
|
5
|
+
*/
|
|
6
|
+
import { loadDesktopAgentConfig } from "../config/desktop-config.js";
|
|
7
|
+
import { startLocalLlmServer } from "./index.js";
|
|
8
|
+
import { resolveModelPathInCache, LOCAL_LLM_CACHE_DIR } from "./model-resolve.js";
|
|
9
|
+
/**
|
|
10
|
+
* 按已保存的配置(默认智能体的 local 模型与上下文长度)尝试启动本地模型服务。
|
|
11
|
+
* 仅当已下载的模型文件存在时才启动;不抛错;失败时设置 process.env.LOCAL_LLM_START_FAILED 并打日志。
|
|
12
|
+
*/
|
|
13
|
+
export async function tryStartLocalModelFromSavedConfig() {
|
|
14
|
+
try {
|
|
15
|
+
const agent = await loadDesktopAgentConfig("default");
|
|
16
|
+
if (!agent || agent.provider !== "local" || !agent.model?.trim()) {
|
|
17
|
+
process.env.LOCAL_LLM_START_FAILED =
|
|
18
|
+
"未配置默认本地模型,请在「模型配置」中选择 LLM 后点击「启动本地模型服务」";
|
|
19
|
+
console.log("[local-llm] 提示:未配置默认本地模型,跳过启动。");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const llmResolved = resolveModelPathInCache(agent.model.trim(), LOCAL_LLM_CACHE_DIR);
|
|
23
|
+
if (!llmResolved) {
|
|
24
|
+
process.env.LOCAL_LLM_START_FAILED =
|
|
25
|
+
"缺省模型文件未下载,请先在「模型管理」中下载后点击「启动本地模型服务」";
|
|
26
|
+
console.log("[local-llm] 提示:缺省模型文件未下载,跳过启动。");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const contextSize = process.env.LOCAL_LLM_CONTEXT_MAX != null && String(process.env.LOCAL_LLM_CONTEXT_MAX).trim() !== ""
|
|
30
|
+
? parseInt(process.env.LOCAL_LLM_CONTEXT_MAX, 10) || 32768
|
|
31
|
+
: (agent.contextSize ?? 32768);
|
|
32
|
+
const opts = { llmModelPath: llmResolved, contextSize };
|
|
33
|
+
startLocalLlmServer(opts)
|
|
34
|
+
.then((handle) => {
|
|
35
|
+
process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
|
|
36
|
+
delete process.env.LOCAL_LLM_START_FAILED;
|
|
37
|
+
console.log("[local-llm] 已就绪:", handle.baseUrl);
|
|
38
|
+
})
|
|
39
|
+
.catch((e) => {
|
|
40
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
41
|
+
process.env.LOCAL_LLM_START_FAILED = msg;
|
|
42
|
+
console.log("[local-llm] 提示:启动未成功(如模型未下载请先在「模型管理」中下载)。", msg);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
47
|
+
process.env.LOCAL_LLM_START_FAILED = msg;
|
|
48
|
+
console.log("[local-llm] 提示:启动时发生异常,已跳过。", msg);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -23,10 +23,16 @@ export declare class StdioTransport {
|
|
|
23
23
|
private nextId;
|
|
24
24
|
private pending;
|
|
25
25
|
private buffer;
|
|
26
|
+
private stderrBuffer;
|
|
27
|
+
private static pendingKey;
|
|
26
28
|
constructor(config: McpServerConfigStdio, options?: StdioTransportOptions);
|
|
27
29
|
/** 启动子进程并完成 MCP initialize 握手 */
|
|
28
30
|
start(): Promise<void>;
|
|
31
|
+
/** 从一行中解析 JSON-RPC 响应:整行即 JSON,或从第一个 { 开始提取到匹配的 }(兼容 npx/uvx 等前缀输出) */
|
|
32
|
+
private static parseJsonRpcResponse;
|
|
29
33
|
private flushLines;
|
|
34
|
+
private flushStderrLines;
|
|
35
|
+
private flushLinesFromBuffer;
|
|
30
36
|
private rejectAll;
|
|
31
37
|
private initialize;
|
|
32
38
|
private sendNotification;
|
|
@@ -13,9 +13,15 @@ export class StdioTransport {
|
|
|
13
13
|
nextId = 1;
|
|
14
14
|
pending = new Map();
|
|
15
15
|
buffer = "";
|
|
16
|
+
stderrBuffer = "";
|
|
17
|
+
static pendingKey(id) {
|
|
18
|
+
if (id === undefined || id === null)
|
|
19
|
+
return "";
|
|
20
|
+
return String(id);
|
|
21
|
+
}
|
|
16
22
|
constructor(config, options = {}) {
|
|
17
23
|
this.config = config;
|
|
18
|
-
this.initTimeoutMs = options.initTimeoutMs ??
|
|
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 = {
|
|
@@ -305,6 +305,11 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
305
305
|
if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
|
|
306
306
|
errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
|
|
307
307
|
}
|
|
308
|
+
// 本地模型子进程退出后,SDK 会报 terminated/Connection error,用 env 中的说明替换为可操作提示
|
|
309
|
+
const localFailed = process.env.LOCAL_LLM_START_FAILED;
|
|
310
|
+
if (localFailed && (msg.errorMessage === "terminated" || /Connection error|ECONNREFUSED|fetch failed/i.test(msg.errorMessage))) {
|
|
311
|
+
errText = `请求失败:${localFailed}`;
|
|
312
|
+
}
|
|
308
313
|
sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
|
|
309
314
|
}
|
|
310
315
|
wsPayload = null;
|
|
@@ -332,6 +337,10 @@ async function handleAgentChatInner(client, targetSessionId, message, params) {
|
|
|
332
337
|
if (errText.includes("Unknown value type") && errText.includes("[object Object]")) {
|
|
333
338
|
errText = "请求失败:模型返回了不支持的数据结构(如工具调用流),请尝试关闭工具或更换模型。";
|
|
334
339
|
}
|
|
340
|
+
const localFailed = process.env.LOCAL_LLM_START_FAILED;
|
|
341
|
+
if (localFailed && (msg.errorMessage === "terminated" || /Connection error|ECONNREFUSED|fetch failed/i.test(msg.errorMessage))) {
|
|
342
|
+
errText = `请求失败:${localFailed}`;
|
|
343
|
+
}
|
|
335
344
|
sendSessionMessage(targetSessionId, { type: "chat", code: "agent.chunk", payload: { text: errText } });
|
|
336
345
|
hasReceivedAnyChunk = true;
|
|
337
346
|
}
|
package/dist/gateway/server.js
CHANGED
|
@@ -44,9 +44,8 @@ import multer from "multer";
|
|
|
44
44
|
import { handleInstallSkillFromPath } from "./methods/install-skill-from-path.js";
|
|
45
45
|
import { handleInstallSkillFromUpload } from "./methods/install-skill-from-upload.js";
|
|
46
46
|
import { setBackendBaseUrl } from "./backend-url.js";
|
|
47
|
-
import { ensureDesktopConfigInitialized, getChannelsConfigSync
|
|
48
|
-
import {
|
|
49
|
-
import { isModelFileInCache } from "../core/local-llm-server/model-resolve.js";
|
|
47
|
+
import { ensureDesktopConfigInitialized, getChannelsConfigSync } from "../core/config/desktop-config.js";
|
|
48
|
+
import { tryStartLocalModelFromSavedConfig } from "../core/local-llm-server/start-from-config.js";
|
|
50
49
|
import { createNestAppEmbedded } from "../server/bootstrap.js";
|
|
51
50
|
import { registerChannel, startAllChannels, stopAllChannels } from "./channel/registry.js";
|
|
52
51
|
import { createFeishuChannel } from "./channel/adapters/feishu.js";
|
|
@@ -81,56 +80,14 @@ export async function startGatewayServer(port = 38080) {
|
|
|
81
80
|
process.env.PORT = String(port);
|
|
82
81
|
await ensureDesktopConfigInitialized();
|
|
83
82
|
console.log(`Starting gateway server on port ${port}...`);
|
|
84
|
-
//
|
|
85
|
-
// 仅读 env 时,桌面端选「本机」默认 agent 时可能未设 OPENBOT_PROVIDER,导致本地服务未启、出现 Connection error
|
|
86
|
-
const envProvider = process.env.OPENBOT_PROVIDER ?? "";
|
|
87
|
-
let shouldStartLocal = envProvider === "local";
|
|
88
|
-
let defaultLocalModel;
|
|
89
|
-
let defaultAgentContextSize;
|
|
83
|
+
// 每次启动时按已保存配置尝试启动本地模型服务(不阻塞、不影响主进程;失败仅提示)
|
|
90
84
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
defaultAgentContextSize = defaultAgent.contextSize;
|
|
94
|
-
if (!shouldStartLocal) {
|
|
95
|
-
shouldStartLocal =
|
|
96
|
-
defaultAgent.provider === "local" &&
|
|
97
|
-
defaultAgent.runnerType !== "coze" &&
|
|
98
|
-
defaultAgent.runnerType !== "openclawx" &&
|
|
99
|
-
defaultAgent.runnerType !== "opencode" &&
|
|
100
|
-
defaultAgent.runnerType !== "claude_code";
|
|
101
|
-
}
|
|
102
|
-
if (shouldStartLocal && defaultAgent.provider === "local" && defaultAgent.model?.trim()) {
|
|
103
|
-
defaultLocalModel = defaultAgent.model.trim();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
85
|
+
console.log("[local-llm] 网关启动:按已保存配置尝试启动本地模型服务…");
|
|
86
|
+
await tryStartLocalModelFromSavedConfig();
|
|
106
87
|
}
|
|
107
|
-
catch {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
if (shouldStartLocal) {
|
|
111
|
-
// 若缺省模型已指定但文件不在缓存中,不启动本地服务,标记不可用,由用户在设置中下载后手动启动
|
|
112
|
-
const llmFileExists = !defaultLocalModel || isModelFileInCache(defaultLocalModel);
|
|
113
|
-
if (!llmFileExists) {
|
|
114
|
-
process.env.LOCAL_LLM_START_FAILED = `缺省模型文件不存在: ${defaultLocalModel},请先在「模型管理」中下载或选择已安装模型后点击「启动本地模型服务」`;
|
|
115
|
-
console.warn("[local-llm] 未启动:", process.env.LOCAL_LLM_START_FAILED);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
const opts = {
|
|
119
|
-
...(defaultLocalModel ? { llmModelPath: defaultLocalModel } : {}),
|
|
120
|
-
contextSize: defaultAgentContextSize ?? 32768,
|
|
121
|
-
};
|
|
122
|
-
startLocalLlmServer(opts)
|
|
123
|
-
.then((handle) => {
|
|
124
|
-
process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
|
|
125
|
-
delete process.env.LOCAL_LLM_START_FAILED;
|
|
126
|
-
console.log("[local-llm] 已就绪:", handle.baseUrl);
|
|
127
|
-
})
|
|
128
|
-
.catch((e) => {
|
|
129
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
130
|
-
process.env.LOCAL_LLM_START_FAILED = msg;
|
|
131
|
-
console.warn("[local-llm] 启动失败:", msg);
|
|
132
|
-
});
|
|
133
|
-
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
90
|
+
console.log("[local-llm] 提示:启动时发生异常,已跳过。", msg);
|
|
134
91
|
}
|
|
135
92
|
setBackendBaseUrl(`http://localhost:${port}`);
|
|
136
93
|
const { app: nestApp, express: nestExpress } = await createNestAppEmbedded();
|
|
@@ -11,5 +11,6 @@ export interface NestAppResult {
|
|
|
11
11
|
export declare function createNestAppEmbedded(): Promise<NestAppResult>;
|
|
12
12
|
/**
|
|
13
13
|
* 独立启动时使用:设置 globalPrefix 并监听端口。
|
|
14
|
+
* 先执行桌面配置初始化,保证首次启动即有 local provider 与缺省模型。
|
|
14
15
|
*/
|
|
15
16
|
export declare function createNestAppStandalone(port?: number): Promise<INestApplication>;
|
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
|
});
|
|
@@ -153,25 +153,48 @@ export declare class ConfigController {
|
|
|
153
153
|
baseUrl: string | undefined;
|
|
154
154
|
};
|
|
155
155
|
};
|
|
156
|
-
/**
|
|
156
|
+
/** 启动本地模型服务:LLM/Embedding 任一已下载即可启动,只启动已存在的模型并提示;失败仅返回错误信息不抛错 */
|
|
157
157
|
startLocalLlm(body: {
|
|
158
158
|
llmModelUri?: string;
|
|
159
159
|
embeddingModelUri?: string;
|
|
160
160
|
}): Promise<{
|
|
161
|
+
success: boolean;
|
|
162
|
+
data: {
|
|
163
|
+
error: string;
|
|
164
|
+
baseUrl?: undefined;
|
|
165
|
+
message?: undefined;
|
|
166
|
+
};
|
|
167
|
+
} | {
|
|
161
168
|
success: boolean;
|
|
162
169
|
data: {
|
|
163
170
|
baseUrl: string;
|
|
171
|
+
message: string;
|
|
172
|
+
error?: undefined;
|
|
164
173
|
};
|
|
165
174
|
}>;
|
|
166
|
-
/** 开始后台下载模型(立即返回,进度通过 GET local-models/progress
|
|
175
|
+
/** 开始后台下载模型(立即返回,进度通过 GET local-models/progress 轮询)。useMirror=true 使用国内镜像。 */
|
|
167
176
|
startDownload(body: {
|
|
168
177
|
modelUri: string;
|
|
178
|
+
useMirror?: boolean;
|
|
169
179
|
}): Promise<{
|
|
170
180
|
success: boolean;
|
|
171
181
|
data: {
|
|
172
182
|
filename: string;
|
|
173
183
|
};
|
|
174
184
|
}>;
|
|
185
|
+
/** 取消指定模型的下载 */
|
|
186
|
+
cancelDownload(body: {
|
|
187
|
+
modelUri: string;
|
|
188
|
+
}): {
|
|
189
|
+
success: boolean;
|
|
190
|
+
};
|
|
191
|
+
/** 推荐模型列表(含是否已安装),用于展示「已下载」或中国/全球下载按钮 */
|
|
192
|
+
getRecommendedWithStatus(): Promise<{
|
|
193
|
+
success: boolean;
|
|
194
|
+
data: (import("./local-models.service.js").RecommendedModel & {
|
|
195
|
+
isInstalled: boolean;
|
|
196
|
+
})[];
|
|
197
|
+
}>;
|
|
175
198
|
/** 查询下载进度 */
|
|
176
199
|
getDownloadProgress(uri: string): {
|
|
177
200
|
success: boolean;
|
|
@@ -12,9 +12,9 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
12
12
|
};
|
|
13
13
|
import { Controller, Get, Put, Body, Param, Query, Delete, Post } from '@nestjs/common';
|
|
14
14
|
import { OPENCODE_FREE_MODELS } from '../../core/agent/proxy/adapters/opencode-free-models.js';
|
|
15
|
-
import { loadDesktopAgentConfig } from '../../core/config/desktop-config.js';
|
|
15
|
+
import { loadDesktopAgentConfig, setDefaultModel } from '../../core/config/desktop-config.js';
|
|
16
16
|
import { startLocalLlmServer, stopLocalLlmServer } from '../../core/local-llm-server/index.js';
|
|
17
|
-
import {
|
|
17
|
+
import { resolveModelPathInCache, LOCAL_LLM_CACHE_DIR } from '../../core/local-llm-server/model-resolve.js';
|
|
18
18
|
import { ConfigService } from './config.service.js';
|
|
19
19
|
import { LocalModelsService } from './local-models.service.js';
|
|
20
20
|
let ConfigController = class ConfigController {
|
|
@@ -93,12 +93,30 @@ let ConfigController = class ConfigController {
|
|
|
93
93
|
data: { available, error: error || undefined, baseUrl: baseUrl || undefined },
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
/**
|
|
96
|
+
/** 启动本地模型服务:LLM/Embedding 任一已下载即可启动,只启动已存在的模型并提示;失败仅返回错误信息不抛错 */
|
|
97
97
|
async startLocalLlm(body) {
|
|
98
98
|
stopLocalLlmServer();
|
|
99
99
|
delete process.env.LOCAL_LLM_BASE_URL;
|
|
100
100
|
delete process.env.LOCAL_LLM_START_FAILED;
|
|
101
|
-
|
|
101
|
+
const llmPath = body.llmModelUri?.trim();
|
|
102
|
+
const embPath = body.embeddingModelUri?.trim();
|
|
103
|
+
if (!llmPath && !embPath) {
|
|
104
|
+
return { success: false, data: { error: '请至少选择 LLM 或 Embedding 模型之一' } };
|
|
105
|
+
}
|
|
106
|
+
const llmResolved = llmPath ? resolveModelPathInCache(llmPath, LOCAL_LLM_CACHE_DIR) : '';
|
|
107
|
+
const embResolved = embPath ? resolveModelPathInCache(embPath, LOCAL_LLM_CACHE_DIR) : '';
|
|
108
|
+
if (!llmResolved && !embResolved) {
|
|
109
|
+
return { success: false, data: { error: '未找到已下载的模型文件,请先在「模型管理」中下载' } };
|
|
110
|
+
}
|
|
111
|
+
if (llmPath) {
|
|
112
|
+
try {
|
|
113
|
+
await setDefaultModel('local', llmPath);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// 保存失败不阻断启动
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let contextSize;
|
|
102
120
|
try {
|
|
103
121
|
const defaultAgent = await loadDesktopAgentConfig('default');
|
|
104
122
|
if (defaultAgent?.contextSize != null && defaultAgent.contextSize > 0) {
|
|
@@ -108,29 +126,48 @@ let ConfigController = class ConfigController {
|
|
|
108
126
|
catch {
|
|
109
127
|
// ignore
|
|
110
128
|
}
|
|
111
|
-
|
|
112
|
-
|
|
129
|
+
if (contextSize == null) {
|
|
130
|
+
const envMax = process.env.LOCAL_LLM_CONTEXT_MAX;
|
|
131
|
+
contextSize = envMax != null && String(envMax).trim() !== '' ? parseInt(envMax, 10) || 32768 : 32768;
|
|
132
|
+
}
|
|
113
133
|
const opts = {
|
|
114
|
-
...(
|
|
115
|
-
...(
|
|
134
|
+
...(llmResolved ? { llmModelPath: llmResolved } : {}),
|
|
135
|
+
...(embResolved ? { embeddingModelPath: embResolved } : {}),
|
|
116
136
|
contextSize,
|
|
117
137
|
};
|
|
118
138
|
try {
|
|
119
139
|
const handle = await startLocalLlmServer(opts);
|
|
120
140
|
process.env.LOCAL_LLM_BASE_URL = handle.baseUrl;
|
|
121
|
-
|
|
141
|
+
const message = llmResolved && embResolved
|
|
142
|
+
? '已启动 LLM + Embedding'
|
|
143
|
+
: llmResolved
|
|
144
|
+
? '已启动 LLM 模型(当前未使用 Embedding)'
|
|
145
|
+
: '已启动 Embedding 模型(当前未使用 LLM)';
|
|
146
|
+
return { success: true, data: { baseUrl: handle.baseUrl, message } };
|
|
122
147
|
}
|
|
123
148
|
catch (e) {
|
|
124
149
|
const msg = e instanceof Error ? e.message : String(e);
|
|
125
150
|
process.env.LOCAL_LLM_START_FAILED = msg;
|
|
126
|
-
|
|
151
|
+
return { success: false, data: { error: msg } };
|
|
127
152
|
}
|
|
128
153
|
}
|
|
129
|
-
/** 开始后台下载模型(立即返回,进度通过 GET local-models/progress
|
|
154
|
+
/** 开始后台下载模型(立即返回,进度通过 GET local-models/progress 轮询)。useMirror=true 使用国内镜像。 */
|
|
130
155
|
async startDownload(body) {
|
|
131
|
-
const result = await this.localModelsService.startDownload(body.modelUri
|
|
156
|
+
const result = await this.localModelsService.startDownload(body.modelUri, {
|
|
157
|
+
useMirror: body.useMirror === true,
|
|
158
|
+
});
|
|
132
159
|
return { success: true, data: result };
|
|
133
160
|
}
|
|
161
|
+
/** 取消指定模型的下载 */
|
|
162
|
+
cancelDownload(body) {
|
|
163
|
+
this.localModelsService.cancelDownload(body.modelUri);
|
|
164
|
+
return { success: true };
|
|
165
|
+
}
|
|
166
|
+
/** 推荐模型列表(含是否已安装),用于展示「已下载」或中国/全球下载按钮 */
|
|
167
|
+
async getRecommendedWithStatus() {
|
|
168
|
+
const models = await this.localModelsService.getRecommendedWithStatus();
|
|
169
|
+
return { success: true, data: models };
|
|
170
|
+
}
|
|
134
171
|
/** 查询下载进度 */
|
|
135
172
|
getDownloadProgress(uri) {
|
|
136
173
|
const progress = this.localModelsService.getDownloadProgress(uri);
|
|
@@ -219,6 +256,19 @@ __decorate([
|
|
|
219
256
|
__metadata("design:paramtypes", [Object]),
|
|
220
257
|
__metadata("design:returntype", Promise)
|
|
221
258
|
], ConfigController.prototype, "startDownload", null);
|
|
259
|
+
__decorate([
|
|
260
|
+
Post('local-models/cancel-download'),
|
|
261
|
+
__param(0, Body()),
|
|
262
|
+
__metadata("design:type", Function),
|
|
263
|
+
__metadata("design:paramtypes", [Object]),
|
|
264
|
+
__metadata("design:returntype", void 0)
|
|
265
|
+
], ConfigController.prototype, "cancelDownload", null);
|
|
266
|
+
__decorate([
|
|
267
|
+
Get('local-models/recommended-with-status'),
|
|
268
|
+
__metadata("design:type", Function),
|
|
269
|
+
__metadata("design:paramtypes", []),
|
|
270
|
+
__metadata("design:returntype", Promise)
|
|
271
|
+
], ConfigController.prototype, "getRecommendedWithStatus", null);
|
|
222
272
|
__decorate([
|
|
223
273
|
Get('local-models/progress'),
|
|
224
274
|
__param(0, Query('uri')),
|
|
@@ -91,11 +91,14 @@ export declare class ConfigService {
|
|
|
91
91
|
private configPath;
|
|
92
92
|
private config;
|
|
93
93
|
constructor(agentConfigService: AgentConfigService);
|
|
94
|
+
/** 预装本地推理缺省:与 desktop-config 的 DEFAULT_LOCAL_LLM_MODEL_ID / DEFAULT_LOCAL_MODEL_ITEM_CODE 一致 */
|
|
95
|
+
private static readonly DEFAULT_LOCAL_MODEL_ID;
|
|
96
|
+
private static readonly DEFAULT_LOCAL_MODEL_ITEM_CODE;
|
|
94
97
|
private getDefaultConfig;
|
|
95
98
|
/** 当前缺省智能体 id */
|
|
96
99
|
getDefaultAgentId(config?: AppConfig): string;
|
|
97
100
|
private loadConfig;
|
|
98
|
-
/** 每次获取前从磁盘重新读取,保证打开配置界面时显示最新(含 CLI
|
|
101
|
+
/** 每次获取前从磁盘重新读取,保证打开配置界面时显示最新(含 CLI 写入的配置)。本地 LLM 可用时注入 local 与缺省模型项,供所有智能体使用。 */
|
|
99
102
|
getConfig(): Promise<AppConfig>;
|
|
100
103
|
updateConfig(updates: Partial<AppConfig>): Promise<AppConfig>;
|
|
101
104
|
private saveConfig;
|