mslxdff 0.1.56 → 0.1.57

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/bin/mslxdff.js CHANGED
@@ -12,6 +12,7 @@ import { loadToken, refreshToken, setPort, getPort, loadGroupsJoined, saveGroups
12
12
  import { getPreferredModel } from "../src/auto.js";
13
13
  import { normalizeModel } from "../src/reasoning.js";
14
14
  import { syncToWorkbuddy, workbuddyModelsPath } from "../src/sync-workbuddy.js";
15
+ import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../src/sync-opencode.js";
15
16
  import { startDaemon, stopDaemon, writePid, pidFile, logFile, readPid, readPidVersion, isPidAlive } from "../src/daemon.js";
16
17
  import { createAutoSelector } from "../src/auto.js";
17
18
  import { createPeersService } from "../src/peers.js";
@@ -313,14 +314,85 @@ if (args.includes("-model") || args.includes("-models")) {
313
314
  process.exit(0);
314
315
  }
315
316
 
316
- // -setto workbuddy [modelId]: set default model and sync to WorkBuddy models.json
317
+ // -setto workbuddy|opencode [modelId]: sync to WorkBuddy / opencode
317
318
  if (args.includes("-setto") || args.includes("--setto")) {
318
319
  const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
319
320
  const target = args[idx + 1];
320
- if (target !== "workbuddy") {
321
- console.error("usage: mslxdff -setto workbuddy [modelId]");
321
+ if (!["workbuddy", "opencode"].includes(target)) {
322
+ console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
322
323
  process.exit(1);
323
324
  }
325
+ if (target === "opencode") {
326
+ const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
327
+ let id;
328
+ let internal;
329
+ if (raw) {
330
+ if (raw === "auto" || !raw) {
331
+ console.error("modelId 不能为 auto 或空");
332
+ process.exit(1);
333
+ }
334
+ const norm = normalizeModel(raw);
335
+ if (!norm) {
336
+ console.error("modelId 不能为空");
337
+ process.exit(1);
338
+ }
339
+ savePreferredModel(norm);
340
+ console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
341
+ internal = toInternalId(norm);
342
+ id = toExternalAlias(internal);
343
+ } else {
344
+ const pref = loadPreferredModel() || getPreferredModel();
345
+ if (!pref) {
346
+ console.error("no preferred model set; use: mslxdff -setto opencode <modelId>");
347
+ process.exit(1);
348
+ }
349
+ const norm = normalizeModel(pref);
350
+ if (!norm) {
351
+ console.error("modelId 不能为空");
352
+ process.exit(1);
353
+ }
354
+ internal = toInternalId(norm);
355
+ id = toExternalAlias(internal);
356
+ }
357
+ try {
358
+ const cacheFile = join(logDir(), "models.json");
359
+ const models = createModelsService({
360
+ baseUrl: process.env.UPSTREAM_BASE_URL || "https://opencode.ai",
361
+ headers: createUpstreamClient({}).headers,
362
+ refreshMs: 0,
363
+ cacheFile,
364
+ });
365
+ const fresh = await Promise.race([
366
+ models.get(),
367
+ new Promise((_, rej) => setTimeout(() => rej(new Error("refresh timeout")), 4000)),
368
+ ]);
369
+ if (fresh?.data?.length) {
370
+ const ids = fresh.data.map((m) => m.id);
371
+ if (!ids.includes(internal) && !ids.includes(id)) {
372
+ console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
373
+ }
374
+ }
375
+ } catch {
376
+ // refresh failed, still proceed
377
+ }
378
+ try {
379
+ const { token } = await loadToken();
380
+ const persisted = getPort();
381
+ const envPort = Number(process.env.MSLXDFF_PORT);
382
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
383
+ const file = opencodeConfigPath();
384
+ const r = await syncToOpencode({ id, token, port, file });
385
+ const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
386
+ console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
387
+ console.log(` url: http://127.0.0.1:${port}/v1`);
388
+ if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
389
+ else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
390
+ } catch (err) {
391
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
392
+ process.exit(1);
393
+ }
394
+ process.exit(0);
395
+ }
324
396
  const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
325
397
  let id;
326
398
  if (raw) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.56",
3
+ "version": "0.1.57",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,7 @@
1
1
  import { performance } from "node:perf_hooks";
2
2
  import { injectReasoningContent, normalizeModel } from "../../reasoning.js";
3
3
  import { isAutoModel } from "../../auto.js";
4
+ import { toInternalId as aliasToInternal } from "../../sync-opencode.js";
4
5
  import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "../helpers.js";
5
6
  import { hedgeDelayMs, shouldHedge } from "../hedge.js";
6
7
  import { runHook } from "../../plugins.js";
@@ -39,9 +40,42 @@ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHo
39
40
  const workbuddyUid = (req.headers["x-mslxdff-workbuddy-uid"] || req.headers["x-workbuddy-uid"] || "").toString().trim();
40
41
  const lockModel = req.headers["x-mslxdff-model-lock"] || "";
41
42
  const rawModel = body.model || "";
42
- const requested = normalizeModel(lockModel || rawModel || "");
43
+ const normalizedRequested = normalizeModel(lockModel || rawModel || "");
44
+ // alias 还原:mslxdff-deepseek -> deepseek(原名仍兼容,双向支持 mslxdff/mslxdff-deepseek 与裸 mslxdff-deepseek/裸 deepseek)
45
+ let requested = normalizedRequested;
46
+ let aliasInfo = null;
47
+ if (requested.startsWith("mslxdff-")) {
48
+ const internal = aliasToInternal(requested);
49
+ if (internal) {
50
+ aliasInfo = `${requested} -> ${internal}`;
51
+ requested = internal;
52
+ }
53
+ } else if (requested.includes("/")) {
54
+ const slashIdx = requested.indexOf("/");
55
+ const rawPart = requested.slice(slashIdx + 1);
56
+ const providerPart = requested.slice(0, slashIdx);
57
+ if (rawPart.startsWith("mslxdff-")) {
58
+ const internal = aliasToInternal(rawPart);
59
+ if (internal) {
60
+ aliasInfo = `${requested} -> ${providerPart}/${internal} (alias stripped)`;
61
+ requested = `${providerPart}/${internal}`;
62
+ if (providerPart === "mslxdff") {
63
+ requested = internal;
64
+ aliasInfo = `${rawModel} -> ${internal} (mslxdff alias stripped)`;
65
+ }
66
+ }
67
+ } else if (providerPart === "mslxdff") {
68
+ // mslxdff/deepseek 原名直用 -> deepseek(原名兼容,provider 前缀剥离)
69
+ aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped, 原名兼容)`;
70
+ requested = rawPart;
71
+ }
72
+ }
43
73
  const useAuto = isAutoModel(requested);
44
74
  mark("parsed");
75
+ if (aliasInfo) {
76
+ // 供日志与 header 透传
77
+ try { res.setHeader("x-mslxdff-alias", aliasInfo); } catch {}
78
+ }
45
79
 
46
80
  let order;
47
81
  if (lockModel) {
@@ -70,6 +104,7 @@ export async function chatHandler({ req, res, upstream, auto, logs, peers, maxHo
70
104
  runHook(plugins, "request:completed", { reqId, requested, useAuto, hops, stream: Boolean(body.stream), durationMs: Date.now() - startedAt, ...info }).catch(() => {});
71
105
  };
72
106
  evt("request", { reqId, hops, ip: clientIp(req), stream: Boolean(body.stream), prompt: summarizePrompt(body), rawModel, requested, lockModel: lockModel || null });
107
+ if (aliasInfo) evt("alias", { reqId, alias: aliasInfo, rawModel, requested });
73
108
  if (Object.keys(shareKeys).length) evt("share-keys", { reqId, providers: Object.keys(shareKeys) });
74
109
  evt("ordered", { reqId, order, canFallback, canForwardPeers, useAuto, statuses: auto?.statuses?.() ?? null });
75
110
 
@@ -0,0 +1,152 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, renameSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import os from "node:os";
4
+
5
+ export function opencodeConfigPath() {
6
+ const env = process.env.OPENCODE_CONFIG || process.env.OPENCODE_CONFIG_PATH;
7
+ if (typeof env === "string" && env.trim()) return env.trim();
8
+ return join(os.homedir(), ".config", "opencode", "opencode.json");
9
+ }
10
+
11
+ export function toExternalAlias(id) {
12
+ const s = String(id || "").trim();
13
+ if (!s) return "";
14
+ return s.startsWith("mslxdff-") ? s : `mslxdff-${s}`;
15
+ }
16
+
17
+ export function toInternalId(aliasOrRaw) {
18
+ const s = String(aliasOrRaw || "").trim();
19
+ if (!s) return "";
20
+ return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
21
+ }
22
+
23
+ export function buildOpencodeProvider({ id, token, port }) {
24
+ const p = Number(port) || 8989;
25
+ const alias = toExternalAlias(id);
26
+ return {
27
+ name: "mslxdff",
28
+ npm: "@ai-sdk/openai-compatible",
29
+ options: {
30
+ apiKey: String(token || ""),
31
+ baseURL: `http://127.0.0.1:${p}/v1`,
32
+ },
33
+ models: {
34
+ [alias]: { name: alias },
35
+ },
36
+ };
37
+ }
38
+
39
+ export function isOpencodeLocalUrl(url) {
40
+ const u = String(url || "");
41
+ return u.includes("127.0.0.1") && u.includes("/v1");
42
+ }
43
+
44
+ export async function syncToOpencode({ id, token, port, file } = {}) {
45
+ const targetFile = file || opencodeConfigPath();
46
+ // id 可能是 alias 或原名,统一以 internal 去重、以 external 入库(原名兼容)
47
+ const normalizedRaw = String(id || "").trim();
48
+ if (!normalizedRaw) throw new Error("model id required");
49
+ const internal = toInternalId(normalizedRaw);
50
+ if (!internal) throw new Error("model id required");
51
+ const external = toExternalAlias(internal);
52
+ const cleanToken = String(token || "");
53
+ const p = Number(port) || 8989;
54
+
55
+ let data = null;
56
+ let corrupted = false;
57
+ let rawText = null;
58
+ try {
59
+ rawText = readFileSync(targetFile, "utf8");
60
+ const parsed = JSON.parse(rawText);
61
+ data = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
62
+ } catch (err) {
63
+ if (rawText !== null) {
64
+ corrupted = true;
65
+ try {
66
+ mkdirSync(dirname(targetFile), { recursive: true });
67
+ writeFileSync(targetFile + ".bak", rawText ?? "", "utf8");
68
+ } catch {}
69
+ data = {};
70
+ } else {
71
+ data = {};
72
+ }
73
+ }
74
+
75
+ if (!data.provider || typeof data.provider !== "object" || Array.isArray(data.provider)) {
76
+ data.provider = {};
77
+ }
78
+
79
+ const oldProvider = data.provider.mslxdff && typeof data.provider.mslxdff === "object" && !Array.isArray(data.provider.mslxdff)
80
+ ? data.provider.mslxdff
81
+ : null;
82
+
83
+ let action;
84
+ let effectiveId = external;
85
+ if (oldProvider) {
86
+ const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
87
+ ? oldProvider.models
88
+ : {};
89
+ // 去重:internal 或 external 任一已存在即视为已存在(原名兼容,以 internal 为基准)
90
+ const hasExternal = Object.prototype.hasOwnProperty.call(oldModels, external);
91
+ const hasInternal = Object.prototype.hasOwnProperty.call(oldModels, internal);
92
+ const exists = hasExternal || hasInternal;
93
+ const nextModels = { ...oldModels };
94
+ if (exists) {
95
+ if (hasExternal) {
96
+ nextModels[external] = { name: external, ...(oldModels[external] && typeof oldModels[external] === "object" ? oldModels[external] : {}), name: external };
97
+ effectiveId = external;
98
+ } else if (hasInternal) {
99
+ // 仅原名存在:保留原名不强制迁移为 alias,视为 updated(原名兼容)
100
+ nextModels[internal] = { name: internal, ...(oldModels[internal] && typeof oldModels[internal] === "object" ? oldModels[internal] : {}), name: internal };
101
+ effectiveId = internal;
102
+ }
103
+ action = "updated";
104
+ } else {
105
+ nextModels[external] = { name: external };
106
+ effectiveId = external;
107
+ action = "inserted";
108
+ }
109
+ // 合并 provider:保留 name/npm,覆盖 options.baseURL/apiKey,合并 models
110
+ const nextProvider = {
111
+ ...oldProvider,
112
+ name: oldProvider.name || "mslxdff",
113
+ npm: oldProvider.npm || "@ai-sdk/openai-compatible",
114
+ options: {
115
+ ...(oldProvider.options && typeof oldProvider.options === "object" ? oldProvider.options : {}),
116
+ baseURL: `http://127.0.0.1:${p}/v1`,
117
+ apiKey: cleanToken,
118
+ },
119
+ models: nextModels,
120
+ };
121
+ // 若插入的是 alias 但原名已存在,上面已处理为不新增;否则正常
122
+ data.provider.mslxdff = nextProvider;
123
+ // 若是新插入且 external 不等于 internal,且 internal 已存在时,需避免双键,上面已处理
124
+ // 若是新插入 external 且 internal 不存在,正常插入
125
+ if (!exists) {
126
+ data.provider.mslxdff.models = nextModels;
127
+ }
128
+ } else {
129
+ data.provider.mslxdff = buildOpencodeProvider({ id: external, token: cleanToken, port: p });
130
+ action = "inserted";
131
+ }
132
+
133
+ // 原子写
134
+ mkdirSync(dirname(targetFile), { recursive: true });
135
+ const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
136
+ writeFileSync(tmp, JSON.stringify(data, null, 2), "utf8");
137
+ try {
138
+ renameSync(tmp, targetFile);
139
+ } catch {
140
+ try {
141
+ writeFileSync(targetFile, readFileSync(tmp, "utf8"), "utf8");
142
+ } catch {}
143
+ }
144
+ try {
145
+ if (existsSync(tmp)) {
146
+ const { unlinkSync } = await import("node:fs");
147
+ unlinkSync(tmp);
148
+ }
149
+ } catch {}
150
+
151
+ return { action, file: targetFile, id: effectiveId, alias: external, internal, corrupted };
152
+ }