mslxdff 0.1.173 → 0.1.175

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 (122) hide show
  1. package/bin/mslxdff.js +3 -3
  2. package/package.json +1 -1
  3. package/src/auto.js +254 -254
  4. package/src/bench/cline-bench.js +42 -42
  5. package/src/bench/probe.js +70 -70
  6. package/src/bench/report.js +162 -162
  7. package/src/bench/runner.js +77 -77
  8. package/src/bench/via-probe.js +124 -124
  9. package/src/bench/via-routes.js +87 -87
  10. package/src/bench/workbuddy-bench.js +54 -54
  11. package/src/chat/engine.js +160 -160
  12. package/src/chat/gateway.js +163 -163
  13. package/src/chat/orchestrator.js +234 -234
  14. package/src/chat/prompt.js +70 -70
  15. package/src/chat/repl.js +88 -88
  16. package/src/chat/terminal.js +135 -135
  17. package/src/chat/tools.js +306 -306
  18. package/src/chat-pipeline/auto-race.js +26 -1
  19. package/src/chat-pipeline/index.js +131 -131
  20. package/src/chat-pipeline/policy.js +84 -76
  21. package/src/chat-pipeline/serial-trial.js +35 -8
  22. package/src/chat-pipeline/terminal-verdict.js +109 -0
  23. package/src/claude-desktop/apply.js +42 -5
  24. package/src/claude-desktop/context1m.js +38 -0
  25. package/src/claude-desktop/slots.js +66 -0
  26. package/src/cli/commands/claude-desktop.js +27 -7
  27. package/src/cli/commands/group.js +249 -249
  28. package/src/cli/commands/model/picks.js +50 -50
  29. package/src/cli/commands/provider/bench-via.js +247 -247
  30. package/src/cli/commands/provider/bench.js +141 -141
  31. package/src/cli/commands/provider/index.js +136 -133
  32. package/src/cli/commands/provider/keys-guard.js +50 -0
  33. package/src/cli/commands/provider/keys.js +7 -0
  34. package/src/cli/commands/provider/models.js +145 -145
  35. package/src/cli/commands/provider/qwenwork-login.js +119 -119
  36. package/src/cli/commands/provider/traework-checkin.js +144 -0
  37. package/src/cli/commands/provider/traework-login.js +7 -45
  38. package/src/cli/commands/sync.js +310 -310
  39. package/src/cli/help.js +4 -1
  40. package/src/cli/status.js +279 -279
  41. package/src/daemon.js +112 -112
  42. package/src/model-capabilities/enrich.js +86 -86
  43. package/src/model-capabilities/index.js +183 -183
  44. package/src/model-capabilities/parse.js +70 -70
  45. package/src/model-trace.js +2 -0
  46. package/src/models.js +225 -225
  47. package/src/opencode-identity.js +39 -19
  48. package/src/providers/cline/auth.js +228 -228
  49. package/src/providers/cline/chat.js +307 -307
  50. package/src/providers/cline.js +2 -2
  51. package/src/providers/codearts/chat.js +7 -1
  52. package/src/providers/codearts/const.js +6 -1
  53. package/src/providers/codearts/index.js +18 -11
  54. package/src/providers/globalqwenwork/cosy.js +123 -123
  55. package/src/providers/globalqwenwork/crypto.js +218 -218
  56. package/src/providers/globalqwenwork/http.js +20 -20
  57. package/src/providers/globalqwenwork/rsa.js +54 -54
  58. package/src/providers/globalqwenwork/sse.js +268 -268
  59. package/src/providers/globalqwenwork/stream.js +130 -130
  60. package/src/providers/globalqwenwork/upstream.js +122 -122
  61. package/src/providers/keyring.js +60 -60
  62. package/src/providers/qoder/chat.js +203 -203
  63. package/src/providers/qoder/index.js +249 -249
  64. package/src/providers/qoder/sse.js +145 -145
  65. package/src/providers/qwenwork/account-store.js +133 -133
  66. package/src/providers/qwenwork/constants.js +67 -67
  67. package/src/providers/qwenwork/cosy.js +120 -120
  68. package/src/providers/qwenwork/crypto.js +218 -218
  69. package/src/providers/qwenwork/http.js +20 -20
  70. package/src/providers/qwenwork/index.js +327 -327
  71. package/src/providers/qwenwork/payload.js +144 -144
  72. package/src/providers/qwenwork/rsa.js +54 -54
  73. package/src/providers/qwenwork/sse.js +268 -268
  74. package/src/providers/qwenwork/stream.js +130 -130
  75. package/src/providers/qwenwork/upstream.js +120 -120
  76. package/src/providers/qwenwork.js +1 -1
  77. package/src/providers/registry.js +79 -79
  78. package/src/providers/traework/account-store.js +87 -2
  79. package/src/providers/traework/checkin.js +51 -9
  80. package/src/providers/traework/constants.js +20 -0
  81. package/src/providers/traework/headers.js +42 -10
  82. package/src/providers/traework/index.js +14 -4
  83. package/src/providers/traework/market-identity.js +116 -0
  84. package/src/providers/workbuddy/chat.js +248 -248
  85. package/src/providers/workbuddy/reshape.js +152 -152
  86. package/src/providers/workbuddy.js +2 -2
  87. package/src/reasoning.js +32 -32
  88. package/src/routes/chat/gateway.js +46 -46
  89. package/src/routes/chat/relay-pipeline.js +294 -294
  90. package/src/routes/chat/via-route-handler.js +146 -146
  91. package/src/routes/hedge.js +255 -255
  92. package/src/routes/models-route.js +167 -167
  93. package/src/routes/peers.js +273 -273
  94. package/src/routes/stream.js +385 -385
  95. package/src/runtime/bootstrap.js +50 -50
  96. package/src/runtime/provider-gate.js +36 -36
  97. package/src/runtime/providers-setup.js +168 -165
  98. package/src/runtime/traework-checkin.js +26 -11
  99. package/src/server.js +69 -69
  100. package/src/state/schemas/allowlist.js +92 -92
  101. package/src/sync-opencode.js +280 -280
  102. package/src/transport/index.js +244 -244
  103. package/src/transport/pool.js +56 -56
  104. package/src/transport/retry.js +24 -24
  105. package/src/transport/sse.js +93 -93
  106. package/src/upstream-probe/display.js +52 -52
  107. package/src/upstream-probe/probe.js +49 -49
  108. package/src/upstream-probe/rotate.js +110 -110
  109. package/src/upstream-probe/start.js +45 -45
  110. package/src/upstream.js +291 -289
  111. package/src/warp-pool/config.js +71 -0
  112. package/src/warp-pool/health.js +65 -0
  113. package/src/warp-pool/index.js +252 -0
  114. package/src/warp-pool/keys.js +29 -0
  115. package/src/warp-pool/nodes.js +94 -0
  116. package/src/warp-pool/register.js +113 -0
  117. package/src/warp-pool/rotate.js +150 -0
  118. package/src/warp-pool/strategy.js +43 -0
  119. package/src/warp-pool/wireproxy.js +153 -0
  120. package/docs/cli_help_mini.md +0 -143
  121. package/src/providers/AGENTS.md +0 -49
  122. package/src/routes/AGENTS.md +0 -37
package/src/chat/tools.js CHANGED
@@ -1,307 +1,307 @@
1
- import { execFile } from "node:child_process";
2
- import { readFileSync, statSync, existsSync } from "node:fs";
3
- import { resolve, join, dirname } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { performance } from "node:perf_hooks";
6
- import { FORBIDDEN } from "./config.js";
7
- import { logDir } from "../logs.js";
8
- import { defaultStateFile, loadProviderKeys, loadProviderConfigs } from "../state.js";
9
- import { compatFetch } from "../compat.js";
1
+ import { execFile } from "node:child_process";
2
+ import { readFileSync, statSync, existsSync } from "node:fs";
3
+ import { resolve, join, dirname } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { performance } from "node:perf_hooks";
6
+ import { FORBIDDEN } from "./config.js";
7
+ import { logDir } from "../logs.js";
8
+ import { defaultStateFile, loadProviderKeys, loadProviderConfigs } from "../state.js";
9
+ import { compatFetch } from "../compat.js";
10
10
  import { opencodeClientIdentity } from "../opencode-identity.js";
11
-
12
- const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
13
- const allowedRoots = [
14
- resolve(pkgRoot),
15
- resolve(logDir()),
16
- resolve(dirname(defaultStateFile())),
17
- ];
18
-
19
- function isAllowed(p) {
20
- const abs = resolve(p);
21
- // 展开 ~/ 形式
22
- const expanded = abs.startsWith("~/") ? join(process.env.HOME || process.env.USERPROFILE || "", abs.slice(2)) : abs;
23
- const chk = resolve(expanded);
24
- return allowedRoots.some((r) => chk === r || chk.startsWith(r + "/") || chk.startsWith(r + "\\"));
25
- }
26
-
27
- function parseCommand(str) {
28
- // 支持引号包裹
29
- const out = [];
30
- let cur = "";
31
- let q = null;
32
- for (let i = 0; i < str.length; i++) {
33
- const c = str[i];
34
- if (q) {
35
- if (c === q) q = null;
36
- else cur += c;
37
- } else if (c === '"' || c === "'") {
38
- q = c;
39
- } else if (c === " " || c === "\t") {
40
- if (cur) { out.push(cur); cur = ""; }
41
- } else {
42
- cur += c;
43
- }
44
- }
45
- if (cur) out.push(cur);
46
- return out;
47
- }
48
-
49
- export function getToolDefs() {
50
- return [
51
- {
52
- type: "function",
53
- function: {
54
- name: "run_command",
55
- description: "执行一条 mslxdff CLI 命令(不含 mslxdff 前缀)。仅限 cli_help_mini 所列命令,禁止 -uninstall;禁止 mslxdff \"hi\" --model X / --model X \"hi\" / -chat --model X 等幻觉命令,探活模型必须用 curl 工具 POST 本机 /v1/chat/completions。",
56
- parameters: {
57
- type: "object",
58
- properties: {
59
- command: { type: "string", description: "例如: -model set hy3-free 或 -group list 或 -log 20;模型探活禁止用此工具,必须用 curl POST http://localhost:8989/v1/chat/completions" },
60
- },
61
- required: ["command"],
62
- },
63
- },
64
- },
65
- {
66
- type: "function",
67
- function: {
68
- name: "read_file",
69
- description: "读取项目内的文件内容(src/ docs/ logs/ 等),用于查看日志或配置。禁止读取项目外文件。",
70
- parameters: {
71
- type: "object",
72
- properties: {
73
- path: { type: "string", description: "相对项目根或绝对路径,如 src/logs.js 或 ~/.config/mslxdff/events.log" },
74
- limit: { type: "number", description: "最多读取字符数,默认 8000" },
75
- },
76
- required: ["path"],
77
- },
78
- },
79
- },
80
- {
81
- type: "function",
82
- function: {
83
- name: "curl",
84
- description: "网络/HTTP 探活,检测上游或本机服务可用性。支持任意 http(s) URL,返回状态码、耗时、响应头与前几千字符。常用: upstream(上游模型列表)、local/health(本机健康)、local/models(本机模型列表)。探活指定模型必须用 POST http://localhost:8989/v1/chat/completions body {\"model\":\"<provider/模型>\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}。简写会自动补全为完整 URL。",
85
- parameters: {
86
- type: "object",
87
- properties: {
88
- url: { type: "string", description: "要请求的 URL 或简写:upstream、upstream/models、upstream/chat、local、local/health、local/models、health、models,或完整 http(s) URL" },
89
- method: { type: "string", description: "HTTP 方法,默认 GET", enum: ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"] },
90
- headers: { type: "object", description: "附加请求头,键值均为字符串", additionalProperties: { type: "string" } },
91
- body: { type: "string", description: "请求体(POST/PUT 时),字符串,如 JSON" },
92
- timeoutMs: { type: "number", description: "超时毫秒,默认 8000,最大 15000" },
93
- },
94
- required: ["url"],
95
- },
96
- },
97
- },
98
- ];
99
- }
100
-
101
- export function validateCommand(cmd) {
102
- const low = String(cmd || "").toLowerCase();
103
- for (const f of FORBIDDEN) if (low.includes(f)) return `forbidden: ${f} 已拦截,禁止执行`;
104
- return null;
105
- }
106
-
107
- export async function execCommand(command) {
108
- const err = validateCommand(command);
109
- if (err) return { ok: false, output: err };
110
- const args = parseCommand(String(command).trim());
111
- if (!args.length) return { ok: false, output: "empty command" };
112
- // 关键路径打点:exec 本身耗时(fork+加载)是 -chat 最大的本地开销,后续可改直调以省 300-500ms
113
- const bin = join(pkgRoot, "bin/mslxdff.js");
114
- const t0 = performance.now();
115
- const res = await new Promise((resolve) => {
116
- execFile(process.execPath, [bin, ...args], { timeout: 15000, maxBuffer: 1024 * 500 }, (e, stdout, stderr) => {
117
- const out = String(stdout || "") + (stderr ? `\n${stderr}` : "");
118
- if (e) {
119
- resolve({ ok: false, output: out.slice(0, 8000) || String(e.message).slice(0, 2000) });
120
- } else {
121
- resolve({ ok: true, output: out.slice(0, 8000) || "(no output)" });
122
- }
123
- });
124
- });
125
- res._ms = Math.round(performance.now() - t0);
126
- return res;
127
- }
128
-
129
- export async function readFileTool({ path, limit }) {
130
- const raw = String(path || "").trim();
131
- if (!raw) return { ok: false, output: "empty path" };
132
- let target = raw;
133
- if (target.startsWith("~/")) {
134
- const home = process.env.HOME || process.env.USERPROFILE || "";
135
- target = join(home, target.slice(2));
136
- } else if (!target.startsWith("/") && !/^[A-Za-z]:/.test(target)) {
137
- // 相对路径视作项目根相对
138
- target = join(pkgRoot, target);
139
- }
140
- if (!isAllowed(target)) return { ok: false, output: `not allowed: ${raw}(仅限项目目录或日志目录)` };
141
- try {
142
- if (!existsSync(target)) return { ok: false, output: `not found: ${raw}` };
143
- const st = statSync(target);
144
- if (st.isDirectory()) {
145
- // 列目录
146
- const { readdirSync } = await import("node:fs");
147
- const files = readdirSync(target).slice(0, 80);
148
- return { ok: true, output: `dir ${raw}:\n` + files.join("\n") };
149
- }
150
- const cap = Number(limit) > 0 ? Math.min(Number(limit), 20000) : 8000;
151
- const data = readFileSync(target, "utf8");
152
- const txt = data.length > cap ? data.slice(0, cap) + `\n... (truncated ${data.length - cap} chars)` : data;
153
- return { ok: true, output: txt || "(empty file)" };
154
- } catch (e) {
155
- return { ok: false, output: String(e.message).slice(0, 2000) };
156
- }
157
- }
158
-
159
- function resolveLocalPort() {
160
- // 复用 state 的端口优先级:state.port > MSLXDFF_PORT env > 8989
161
- try {
162
- const f = defaultStateFile();
163
- const j = JSON.parse(readFileSync(f, "utf8"));
164
- const p = j?.port;
165
- if (Number.isInteger(p) && p > 0) return p;
166
- } catch {}
167
- const env = Number(process.env.MSLXDFF_PORT);
168
- if (Number.isInteger(env) && env > 0) return env;
169
- return 8989;
170
- }
171
-
172
- function resolveLocalToken() {
173
- try {
174
- const f = defaultStateFile();
175
- const j = JSON.parse(readFileSync(f, "utf8"));
176
- const t = j?.token;
177
- if (typeof t === "string" && t.trim()) return t.trim();
178
- } catch {}
179
- return "";
180
- }
181
-
182
- function expandCurlUrl(raw) {
183
- const s = String(raw || "").trim();
184
- const low = s.toLowerCase();
185
- const port = resolveLocalPort();
186
- const localBase = `http://127.0.0.1:${port}`;
187
- // 完整 http(s) 直接用
188
- if (/^https?:\/\//i.test(s)) return s;
189
- // 简写映射
190
- if (low === "upstream" || low === "upstream/models" || low === "upstream/models/") return "https://opencode.ai/zen/v1/models";
191
- if (low === "upstream/chat" || low === "upstream/chat/completions") return "https://opencode.ai/zen/v1/chat/completions";
192
- if (low === "local" || low === "local/health" || low === "health") return `${localBase}/health`;
193
- if (low === "local/models" || low === "models" || low === "v1/models") return `${localBase}/v1/models`;
194
- if (low === "local/chat" || low === "v1/chat/completions") return `${localBase}/v1/chat/completions`;
195
- // 供应商简写:bai/models、openrouter/models 等 → 该供应商 baseUrl + /models
196
- const provMatch = low.match(/^([a-z0-9_-]+)\/models\/?$/);
197
- if (provMatch) {
198
- try {
199
- const pid = provMatch[1];
200
- const cfgs = loadProviderConfigs();
201
- const cfg = cfgs[pid];
202
- if (cfg?.baseUrl) return `${cfg.baseUrl.replace(/\/+$/, "")}/models`;
203
- } catch {}
204
- }
205
- // 裸路径视作本机相对
206
- if (s.startsWith("/")) return `${localBase}${s}`;
207
- return s;
208
- }
209
-
210
- export async function curlTool({ url, method, headers, body, timeoutMs }) {
211
- const raw = String(url || "").trim();
212
- if (!raw) return { ok: false, output: "empty url" };
213
- let target = expandCurlUrl(raw);
214
- // 仅允许 http(s)
215
- if (!/^https?:\/\//i.test(target)) return { ok: false, output: `only http(s) allowed: ${raw} -> ${target}` };
216
- let m = String(method || "GET").toUpperCase();
217
- if (!["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"].includes(m)) return { ok: false, output: `unsupported method: ${m}` };
218
- const timeout = Math.min(Math.max(Number(timeoutMs) || 8000, 500), 15000);
219
- // 组装头
220
- const h = {};
221
- if (headers && typeof headers === "object") {
222
- for (const [k, v] of Object.entries(headers)) if (typeof v === "string") h[String(k)] = v;
223
- }
224
- // 上游自动补最小可用头(zen 免费层门禁:UA 带版本 + opencode 形状 session/request)
225
- if (/opencode\.ai/i.test(target)) {
226
- const ident = opencodeClientIdentity();
227
- if (!h["x-opencode-client"]) h["x-opencode-client"] = "desktop";
228
- if (!h["Authorization"] && !h["authorization"]) h["Authorization"] = "Bearer public";
229
- if (!h["Accept"]) h["Accept"] = m === "GET" ? "*/*" : "application/json";
230
- if (!h["Content-Type"] && m !== "GET" && m !== "HEAD") h["Content-Type"] = "application/json";
231
- if (!h["User-Agent"] && !h["user-agent"]) h["User-Agent"] = ident["User-Agent"];
232
- if (!h["x-opencode-session"]) h["x-opencode-session"] = ident["x-opencode-session"];
233
- if (!h["x-opencode-request"]) h["x-opencode-request"] = ident["x-opencode-request"];
234
- if (!h["x-opencode-project"]) h["x-opencode-project"] = "global";
235
- }
236
- // 本机自动带 token(/v1/* 需要鉴权);/v1/models 必须是 GET,模型常误用 POST 直接纠正
237
- if (/127\.0\.0\.1|localhost/i.test(target) && /\/v1\//i.test(target)) {
238
- const hasAuth = !!(h["Authorization"] || h["authorization"]);
239
- if (!hasAuth) {
240
- const tok = resolveLocalToken();
241
- if (tok) h["Authorization"] = `Bearer ${tok}`;
242
- }
243
- if (/\/v1\/models/i.test(target) && m === "POST") m = "GET";
244
- }
245
- // 已配置供应商自动带 key(curl 直连供应商时免手动传头)
246
- if (!h["Authorization"] && !h["authorization"]) {
247
- try {
248
- const cfgs = loadProviderConfigs();
249
- for (const [pid, cfg] of Object.entries(cfgs)) {
250
- const base = String(cfg?.baseUrl || "").replace(/\/+$/, "");
251
- if (!base) continue;
252
- if (target.toLowerCase().startsWith(base.toLowerCase())) {
253
- const keys = loadProviderKeys(pid);
254
- if (keys.length) { h["Authorization"] = `Bearer ${keys[0]}`; break; }
255
- }
256
- }
257
- // 兜底:api.b.ai 即 bai 供应商(env 或 state)
258
- if (!h["Authorization"] && /api\.b\.ai/i.test(target)) {
259
- const k = loadProviderKeys("bai")[0];
260
- if (k) h["Authorization"] = `Bearer ${k}`;
261
- }
262
- } catch {}
263
- }
264
- // fetch 走兼容层(undici 优先,老 Node 兜底)
265
- const fetchImpl = compatFetch;
266
- const controller = new AbortController();
267
- const timer = setTimeout(() => controller.abort(new Error(`curl timed out after ${timeout}ms`)), timeout);
268
- const t0 = performance.now();
269
- try {
270
- const opts = { method: m, headers: h, signal: controller.signal };
271
- if (body != null && m !== "GET" && m !== "HEAD") opts.body = String(body);
272
- const res = await fetchImpl(target, opts);
273
- const ms = Math.round(performance.now() - t0);
274
- // 抓响应头(过滤成可读)
275
- const rh = {};
276
- try {
277
- for (const [k, v] of res.headers.entries()) rh[k] = v;
278
- } catch {}
279
- let txt = "";
280
- try {
281
- // 限制读取大小,避免超大 body
282
- const ab = await res.arrayBuffer();
283
- const buf = Buffer.from(ab);
284
- const lim = 6000;
285
- txt = buf.toString("utf8").slice(0, lim);
286
- if (buf.length > lim) txt += `\n... (truncated ${buf.length - lim} bytes)`;
287
- } catch (e) {
288
- txt = String(e.message || e).slice(0, 1000);
289
- }
290
- const headerLines = Object.entries(rh).slice(0, 20).map(([k, v]) => `${k}: ${v}`).join("\n");
291
- const out = [
292
- `${m} ${target}`,
293
- `status: ${res.status} ${res.statusText || ""}`.trim(),
294
- `latency: ${ms}ms`,
295
- headerLines ? `headers:\n${headerLines}` : "headers: (none)",
296
- `body (${txt.length} chars):`,
297
- txt || "(empty body)",
298
- ].join("\n");
299
- // 2xx 视为 ok,其余仍返回 ok:true 但带状态,便于模型判断(网络层面成功)
300
- return { ok: true, output: out.slice(0, 8000) };
301
- } catch (e) {
302
- const ms = Math.round(performance.now() - t0);
303
- return { ok: false, output: `curl failed: ${String(e.message || e).slice(0, 1200)}\nurl: ${target}\nlatency: ${ms}ms` };
304
- } finally {
305
- clearTimeout(timer);
306
- }
307
- }
11
+
12
+ const pkgRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
13
+ const allowedRoots = [
14
+ resolve(pkgRoot),
15
+ resolve(logDir()),
16
+ resolve(dirname(defaultStateFile())),
17
+ ];
18
+
19
+ function isAllowed(p) {
20
+ const abs = resolve(p);
21
+ // 展开 ~/ 形式
22
+ const expanded = abs.startsWith("~/") ? join(process.env.HOME || process.env.USERPROFILE || "", abs.slice(2)) : abs;
23
+ const chk = resolve(expanded);
24
+ return allowedRoots.some((r) => chk === r || chk.startsWith(r + "/") || chk.startsWith(r + "\\"));
25
+ }
26
+
27
+ function parseCommand(str) {
28
+ // 支持引号包裹
29
+ const out = [];
30
+ let cur = "";
31
+ let q = null;
32
+ for (let i = 0; i < str.length; i++) {
33
+ const c = str[i];
34
+ if (q) {
35
+ if (c === q) q = null;
36
+ else cur += c;
37
+ } else if (c === '"' || c === "'") {
38
+ q = c;
39
+ } else if (c === " " || c === "\t") {
40
+ if (cur) { out.push(cur); cur = ""; }
41
+ } else {
42
+ cur += c;
43
+ }
44
+ }
45
+ if (cur) out.push(cur);
46
+ return out;
47
+ }
48
+
49
+ export function getToolDefs() {
50
+ return [
51
+ {
52
+ type: "function",
53
+ function: {
54
+ name: "run_command",
55
+ description: "执行一条 mslxdff CLI 命令(不含 mslxdff 前缀)。仅限 cli_help_mini 所列命令,禁止 -uninstall;禁止 mslxdff \"hi\" --model X / --model X \"hi\" / -chat --model X 等幻觉命令,探活模型必须用 curl 工具 POST 本机 /v1/chat/completions。",
56
+ parameters: {
57
+ type: "object",
58
+ properties: {
59
+ command: { type: "string", description: "例如: -model set hy3-free 或 -group list 或 -log 20;模型探活禁止用此工具,必须用 curl POST http://localhost:8989/v1/chat/completions" },
60
+ },
61
+ required: ["command"],
62
+ },
63
+ },
64
+ },
65
+ {
66
+ type: "function",
67
+ function: {
68
+ name: "read_file",
69
+ description: "读取项目内的文件内容(src/ docs/ logs/ 等),用于查看日志或配置。禁止读取项目外文件。",
70
+ parameters: {
71
+ type: "object",
72
+ properties: {
73
+ path: { type: "string", description: "相对项目根或绝对路径,如 src/logs.js 或 ~/.config/mslxdff/events.log" },
74
+ limit: { type: "number", description: "最多读取字符数,默认 8000" },
75
+ },
76
+ required: ["path"],
77
+ },
78
+ },
79
+ },
80
+ {
81
+ type: "function",
82
+ function: {
83
+ name: "curl",
84
+ description: "网络/HTTP 探活,检测上游或本机服务可用性。支持任意 http(s) URL,返回状态码、耗时、响应头与前几千字符。常用: upstream(上游模型列表)、local/health(本机健康)、local/models(本机模型列表)。探活指定模型必须用 POST http://localhost:8989/v1/chat/completions body {\"model\":\"<provider/模型>\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}。简写会自动补全为完整 URL。",
85
+ parameters: {
86
+ type: "object",
87
+ properties: {
88
+ url: { type: "string", description: "要请求的 URL 或简写:upstream、upstream/models、upstream/chat、local、local/health、local/models、health、models,或完整 http(s) URL" },
89
+ method: { type: "string", description: "HTTP 方法,默认 GET", enum: ["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"] },
90
+ headers: { type: "object", description: "附加请求头,键值均为字符串", additionalProperties: { type: "string" } },
91
+ body: { type: "string", description: "请求体(POST/PUT 时),字符串,如 JSON" },
92
+ timeoutMs: { type: "number", description: "超时毫秒,默认 8000,最大 15000" },
93
+ },
94
+ required: ["url"],
95
+ },
96
+ },
97
+ },
98
+ ];
99
+ }
100
+
101
+ export function validateCommand(cmd) {
102
+ const low = String(cmd || "").toLowerCase();
103
+ for (const f of FORBIDDEN) if (low.includes(f)) return `forbidden: ${f} 已拦截,禁止执行`;
104
+ return null;
105
+ }
106
+
107
+ export async function execCommand(command) {
108
+ const err = validateCommand(command);
109
+ if (err) return { ok: false, output: err };
110
+ const args = parseCommand(String(command).trim());
111
+ if (!args.length) return { ok: false, output: "empty command" };
112
+ // 关键路径打点:exec 本身耗时(fork+加载)是 -chat 最大的本地开销,后续可改直调以省 300-500ms
113
+ const bin = join(pkgRoot, "bin/mslxdff.js");
114
+ const t0 = performance.now();
115
+ const res = await new Promise((resolve) => {
116
+ execFile(process.execPath, [bin, ...args], { timeout: 15000, maxBuffer: 1024 * 500 }, (e, stdout, stderr) => {
117
+ const out = String(stdout || "") + (stderr ? `\n${stderr}` : "");
118
+ if (e) {
119
+ resolve({ ok: false, output: out.slice(0, 8000) || String(e.message).slice(0, 2000) });
120
+ } else {
121
+ resolve({ ok: true, output: out.slice(0, 8000) || "(no output)" });
122
+ }
123
+ });
124
+ });
125
+ res._ms = Math.round(performance.now() - t0);
126
+ return res;
127
+ }
128
+
129
+ export async function readFileTool({ path, limit }) {
130
+ const raw = String(path || "").trim();
131
+ if (!raw) return { ok: false, output: "empty path" };
132
+ let target = raw;
133
+ if (target.startsWith("~/")) {
134
+ const home = process.env.HOME || process.env.USERPROFILE || "";
135
+ target = join(home, target.slice(2));
136
+ } else if (!target.startsWith("/") && !/^[A-Za-z]:/.test(target)) {
137
+ // 相对路径视作项目根相对
138
+ target = join(pkgRoot, target);
139
+ }
140
+ if (!isAllowed(target)) return { ok: false, output: `not allowed: ${raw}(仅限项目目录或日志目录)` };
141
+ try {
142
+ if (!existsSync(target)) return { ok: false, output: `not found: ${raw}` };
143
+ const st = statSync(target);
144
+ if (st.isDirectory()) {
145
+ // 列目录
146
+ const { readdirSync } = await import("node:fs");
147
+ const files = readdirSync(target).slice(0, 80);
148
+ return { ok: true, output: `dir ${raw}:\n` + files.join("\n") };
149
+ }
150
+ const cap = Number(limit) > 0 ? Math.min(Number(limit), 20000) : 8000;
151
+ const data = readFileSync(target, "utf8");
152
+ const txt = data.length > cap ? data.slice(0, cap) + `\n... (truncated ${data.length - cap} chars)` : data;
153
+ return { ok: true, output: txt || "(empty file)" };
154
+ } catch (e) {
155
+ return { ok: false, output: String(e.message).slice(0, 2000) };
156
+ }
157
+ }
158
+
159
+ function resolveLocalPort() {
160
+ // 复用 state 的端口优先级:state.port > MSLXDFF_PORT env > 8989
161
+ try {
162
+ const f = defaultStateFile();
163
+ const j = JSON.parse(readFileSync(f, "utf8"));
164
+ const p = j?.port;
165
+ if (Number.isInteger(p) && p > 0) return p;
166
+ } catch {}
167
+ const env = Number(process.env.MSLXDFF_PORT);
168
+ if (Number.isInteger(env) && env > 0) return env;
169
+ return 8989;
170
+ }
171
+
172
+ function resolveLocalToken() {
173
+ try {
174
+ const f = defaultStateFile();
175
+ const j = JSON.parse(readFileSync(f, "utf8"));
176
+ const t = j?.token;
177
+ if (typeof t === "string" && t.trim()) return t.trim();
178
+ } catch {}
179
+ return "";
180
+ }
181
+
182
+ function expandCurlUrl(raw) {
183
+ const s = String(raw || "").trim();
184
+ const low = s.toLowerCase();
185
+ const port = resolveLocalPort();
186
+ const localBase = `http://127.0.0.1:${port}`;
187
+ // 完整 http(s) 直接用
188
+ if (/^https?:\/\//i.test(s)) return s;
189
+ // 简写映射
190
+ if (low === "upstream" || low === "upstream/models" || low === "upstream/models/") return "https://opencode.ai/zen/v1/models";
191
+ if (low === "upstream/chat" || low === "upstream/chat/completions") return "https://opencode.ai/zen/v1/chat/completions";
192
+ if (low === "local" || low === "local/health" || low === "health") return `${localBase}/health`;
193
+ if (low === "local/models" || low === "models" || low === "v1/models") return `${localBase}/v1/models`;
194
+ if (low === "local/chat" || low === "v1/chat/completions") return `${localBase}/v1/chat/completions`;
195
+ // 供应商简写:bai/models、openrouter/models 等 → 该供应商 baseUrl + /models
196
+ const provMatch = low.match(/^([a-z0-9_-]+)\/models\/?$/);
197
+ if (provMatch) {
198
+ try {
199
+ const pid = provMatch[1];
200
+ const cfgs = loadProviderConfigs();
201
+ const cfg = cfgs[pid];
202
+ if (cfg?.baseUrl) return `${cfg.baseUrl.replace(/\/+$/, "")}/models`;
203
+ } catch {}
204
+ }
205
+ // 裸路径视作本机相对
206
+ if (s.startsWith("/")) return `${localBase}${s}`;
207
+ return s;
208
+ }
209
+
210
+ export async function curlTool({ url, method, headers, body, timeoutMs }) {
211
+ const raw = String(url || "").trim();
212
+ if (!raw) return { ok: false, output: "empty url" };
213
+ let target = expandCurlUrl(raw);
214
+ // 仅允许 http(s)
215
+ if (!/^https?:\/\//i.test(target)) return { ok: false, output: `only http(s) allowed: ${raw} -> ${target}` };
216
+ let m = String(method || "GET").toUpperCase();
217
+ if (!["GET", "POST", "PUT", "DELETE", "HEAD", "PATCH", "OPTIONS"].includes(m)) return { ok: false, output: `unsupported method: ${m}` };
218
+ const timeout = Math.min(Math.max(Number(timeoutMs) || 8000, 500), 15000);
219
+ // 组装头
220
+ const h = {};
221
+ if (headers && typeof headers === "object") {
222
+ for (const [k, v] of Object.entries(headers)) if (typeof v === "string") h[String(k)] = v;
223
+ }
224
+ // 上游自动补最小可用头(zen 免费层门禁:UA 带版本 + opencode 形状 session/request)
225
+ if (/opencode\.ai/i.test(target)) {
226
+ const ident = opencodeClientIdentity();
227
+ if (!h["x-opencode-client"]) h["x-opencode-client"] = "desktop";
228
+ if (!h["Authorization"] && !h["authorization"]) h["Authorization"] = "Bearer public";
229
+ if (!h["Accept"]) h["Accept"] = m === "GET" ? "*/*" : "application/json";
230
+ if (!h["Content-Type"] && m !== "GET" && m !== "HEAD") h["Content-Type"] = "application/json";
231
+ if (!h["User-Agent"] && !h["user-agent"]) h["User-Agent"] = ident["User-Agent"];
232
+ if (!h["x-opencode-session"]) h["x-opencode-session"] = ident["x-opencode-session"];
233
+ if (!h["x-opencode-request"]) h["x-opencode-request"] = ident["x-opencode-request"];
234
+ if (!h["x-opencode-project"]) h["x-opencode-project"] = "global";
235
+ }
236
+ // 本机自动带 token(/v1/* 需要鉴权);/v1/models 必须是 GET,模型常误用 POST 直接纠正
237
+ if (/127\.0\.0\.1|localhost/i.test(target) && /\/v1\//i.test(target)) {
238
+ const hasAuth = !!(h["Authorization"] || h["authorization"]);
239
+ if (!hasAuth) {
240
+ const tok = resolveLocalToken();
241
+ if (tok) h["Authorization"] = `Bearer ${tok}`;
242
+ }
243
+ if (/\/v1\/models/i.test(target) && m === "POST") m = "GET";
244
+ }
245
+ // 已配置供应商自动带 key(curl 直连供应商时免手动传头)
246
+ if (!h["Authorization"] && !h["authorization"]) {
247
+ try {
248
+ const cfgs = loadProviderConfigs();
249
+ for (const [pid, cfg] of Object.entries(cfgs)) {
250
+ const base = String(cfg?.baseUrl || "").replace(/\/+$/, "");
251
+ if (!base) continue;
252
+ if (target.toLowerCase().startsWith(base.toLowerCase())) {
253
+ const keys = loadProviderKeys(pid);
254
+ if (keys.length) { h["Authorization"] = `Bearer ${keys[0]}`; break; }
255
+ }
256
+ }
257
+ // 兜底:api.b.ai 即 bai 供应商(env 或 state)
258
+ if (!h["Authorization"] && /api\.b\.ai/i.test(target)) {
259
+ const k = loadProviderKeys("bai")[0];
260
+ if (k) h["Authorization"] = `Bearer ${k}`;
261
+ }
262
+ } catch {}
263
+ }
264
+ // fetch 走兼容层(undici 优先,老 Node 兜底)
265
+ const fetchImpl = compatFetch;
266
+ const controller = new AbortController();
267
+ const timer = setTimeout(() => controller.abort(new Error(`curl timed out after ${timeout}ms`)), timeout);
268
+ const t0 = performance.now();
269
+ try {
270
+ const opts = { method: m, headers: h, signal: controller.signal };
271
+ if (body != null && m !== "GET" && m !== "HEAD") opts.body = String(body);
272
+ const res = await fetchImpl(target, opts);
273
+ const ms = Math.round(performance.now() - t0);
274
+ // 抓响应头(过滤成可读)
275
+ const rh = {};
276
+ try {
277
+ for (const [k, v] of res.headers.entries()) rh[k] = v;
278
+ } catch {}
279
+ let txt = "";
280
+ try {
281
+ // 限制读取大小,避免超大 body
282
+ const ab = await res.arrayBuffer();
283
+ const buf = Buffer.from(ab);
284
+ const lim = 6000;
285
+ txt = buf.toString("utf8").slice(0, lim);
286
+ if (buf.length > lim) txt += `\n... (truncated ${buf.length - lim} bytes)`;
287
+ } catch (e) {
288
+ txt = String(e.message || e).slice(0, 1000);
289
+ }
290
+ const headerLines = Object.entries(rh).slice(0, 20).map(([k, v]) => `${k}: ${v}`).join("\n");
291
+ const out = [
292
+ `${m} ${target}`,
293
+ `status: ${res.status} ${res.statusText || ""}`.trim(),
294
+ `latency: ${ms}ms`,
295
+ headerLines ? `headers:\n${headerLines}` : "headers: (none)",
296
+ `body (${txt.length} chars):`,
297
+ txt || "(empty body)",
298
+ ].join("\n");
299
+ // 2xx 视为 ok,其余仍返回 ok:true 但带状态,便于模型判断(网络层面成功)
300
+ return { ok: true, output: out.slice(0, 8000) };
301
+ } catch (e) {
302
+ const ms = Math.round(performance.now() - t0);
303
+ return { ok: false, output: `curl failed: ${String(e.message || e).slice(0, 1200)}\nurl: ${target}\nlatency: ${ms}ms` };
304
+ } finally {
305
+ clearTimeout(timer);
306
+ }
307
+ }