mslxdff 0.1.85 → 0.1.87

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mslxdff",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "测试项目,请勿使用。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,64 @@
1
1
  export async function handleProviderConfig(id, sub, rest) {
2
+ if (sub === "del" || sub === "delete" || sub === "rm" || sub === "remove-provider" || sub === "del-provider") {
3
+ const nid = String(id || "").trim().toLowerCase();
4
+ if (nid === "opencode" || nid === "oc") {
5
+ console.error("opencode 是内置供应商,不能删除");
6
+ process.exit(1);
7
+ }
8
+ const { loadProviderConfigs, saveProviderConfig, saveProviderShareKeys } = await import("../../../state.js");
9
+ const { isPidAlive, readPid } = await import("../../../daemon.js");
10
+ const configs = loadProviderConfigs();
11
+ if (!configs[id] && !configs[nid]) {
12
+ // 也查旧的 providerKeys
13
+ const { readState } = await import("../../../state/store.js");
14
+ const { defaultStateFile } = await import("../../../state/store.js");
15
+ const raw = readState(defaultStateFile());
16
+ const hasLegacy = raw.providerKeys && raw.providerKeys[id];
17
+ if (!hasLegacy) {
18
+ console.error(`provider not found: ${id}`);
19
+ process.exit(1);
20
+ }
21
+ }
22
+ // 真删:空对象会触发 saveProviderConfig 的 delete 分支
23
+ saveProviderConfig(id, { baseUrl: "", keys: [], auths: [], allowedModels: [], modelsPath: "", chatPath: "" });
24
+ // 兼容旧路径的残留
25
+ try {
26
+ const { readState, writeStateImmediate, defaultStateFile } = await import("../../../state/store.js");
27
+ const file = defaultStateFile();
28
+ const raw = readState(file);
29
+ let changed = false;
30
+ if (raw.providerKeys && raw.providerKeys[id] !== undefined) {
31
+ const nk = { ...raw.providerKeys };
32
+ delete nk[id];
33
+ writeStateImmediate(file, { providerKeys: nk });
34
+ changed = true;
35
+ }
36
+ if (raw.providerShareKeys && raw.providerShareKeys[id] !== undefined) {
37
+ const ns = { ...raw.providerShareKeys };
38
+ delete ns[id];
39
+ writeStateImmediate(file, { providerShareKeys: ns });
40
+ changed = true;
41
+ }
42
+ // 清理模型错误/延迟中该供应商前缀的条目(可选,不阻塞)
43
+ void changed;
44
+ } catch {}
45
+ try { saveProviderShareKeys(id, false); } catch {}
46
+ console.log(`已删除供应商: ${id} — 配置已清空`);
47
+ // 需要重启才生效,自动重启
48
+ const pid = readPid();
49
+ if (pid && isPidAlive(pid)) {
50
+ console.log("检测到 daemon 运行中,自动重启以生效…");
51
+ const { spawnSync } = await import("node:child_process");
52
+ const { fileURLToPath } = await import("node:url");
53
+ const bin = fileURLToPath(new URL("../../../../bin/mslxdff.js", import.meta.url));
54
+ const r = spawnSync(process.execPath, [bin, "-restart"], { stdio: "inherit" });
55
+ if (r.status !== 0) console.log("自动重启失败,请手动执行: mslxdff -restart");
56
+ else console.log("已自动重启完成");
57
+ } else {
58
+ console.log("daemon 未运行,下次启动时生效");
59
+ }
60
+ process.exit(0);
61
+ }
2
62
  if (sub === "set-models-path" || sub === "setModelsPath" || sub === "models-path") {
3
63
  const p = rest[1];
4
64
  if (!p) {
@@ -6,73 +6,12 @@ export async function handleProviders(args) {
6
6
  const idx = args.findIndex((x) => x === "-providers" || x === "--providers");
7
7
  const sub = args[idx + 1];
8
8
  if (!sub || sub === "list" || sub === "status") {
9
- const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl, loadProviderAllowedModels } = await import("../../../state.js");
10
- const configs = loadProviderConfigs();
11
- const opencodeEnabled = true;
12
- const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
13
- const orKeys = loadProviderKeys("openrouter");
14
- const orBase = "https://openrouter.ai/api/v1";
15
- const orShare = loadProviderShareKeys("openrouter");
16
- const orAllowed = loadProviderAllowedModels("openrouter");
17
- const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode" && id !== "openrouter"));
18
- try {
19
- const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
20
- const pk = raw.providerKeys || {};
21
- for (const id of Object.keys(pk)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
22
- const cfgRaw = raw.providerConfigs || {};
23
- for (const id of Object.keys(cfgRaw)) if (id !== "opencode" && id !== "openrouter") genericIds.add(id);
24
- } catch {}
25
- for (const k of Object.keys(process.env)) {
26
- const m = k.match(/^MSLXDFF_(.+)_KEY$/);
27
- if (m) {
28
- const id = m[1].toLowerCase().replace(/__/g, "-");
29
- if (id !== "openrouter" && id !== "opencode") genericIds.add(id);
30
- }
31
- }
32
- try {
33
- const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
34
- const cfgs = raw.providerConfigs || {};
35
- for (const id of Object.keys(cfgs)) {
36
- if (id === "opencode" || id === "openrouter") continue;
37
- const am = cfgs[id]?.allowedModels;
38
- if (Array.isArray(am) && am.length) genericIds.add(id);
39
- }
40
- } catch {}
41
- const list = [];
42
- const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
43
- const opAllowAny = _la0("opencode");
44
- const orAllowAny = _la0("openrouter");
45
- const opAllowed = loadProviderAllowedModels("opencode");
46
- list.push({ id: "opencode", enabled: opencodeEnabled, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
47
- list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
48
- for (const gid of [...genericIds].sort()) {
49
- const cfg = configs[gid];
50
- const keys = loadProviderKeys(gid);
51
- const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
52
- const share = loadProviderShareKeys(gid);
53
- const allowed = loadProviderAllowedModels(gid);
54
- const allowAny = _la0(gid);
55
- const enabled = Boolean(baseUrl && keys.length);
56
- let note = "";
57
- if (!baseUrl && !keys.length && !allowed.length && allowAny === false) note = "no baseUrl, no keys, BLOCKED (allowAny OFF)";
58
- else if (!baseUrl && !keys.length && !allowed.length) note = "no baseUrl, no keys";
59
- else if (!baseUrl && !allowed.length && !allowAny) note = "missing baseUrl, BLOCKED";
60
- else if (!keys.length && !allowed.length && !allowAny) note = "no keys, BLOCKED";
61
- else if (!baseUrl) note = "missing baseUrl";
62
- else if (!keys.length) note = "no keys";
63
- list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, allowed, allowAny, note });
64
- }
65
- console.log(`providers (${list.length}):`);
66
- for (const p of list) {
67
- const state = p.enabled ? "enabled " : "disabled";
68
- const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
69
- const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
70
- const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 3).join(",")}${p.allowed.length > 3 ? "..." : ""})` : (p.allowAny ? "allow=all" : "allow=none(BLOCKED)");
71
- const note = p.note ? ` (${p.note})` : "";
72
- console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} ${allowInfo.padEnd(22)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
73
- }
74
- console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider <id> allowlist set <model...> to restrict`);
75
- console.log(` mslxdff -provider <id> allowAny on|off (empty allowlist = block or allow all)`);
9
+ const { buildProviderRows, formatProviderSection } = await import("../../provider-row.js");
10
+ const rows = buildProviderRows({});
11
+ const enabled = rows.filter((r) => r.enabled).length;
12
+ console.log(`providers ${rows.length} · ${enabled} 已启用 — mslxdff -provider <id> list 查看详情`);
13
+ console.log(formatProviderSection(rows));
14
+ console.log(`\n提示: mslxdff -provider <id> list 单看一个 · allowlist set <model...> 限制模型 · allowAny on|off 空名单放行/阻断`);
76
15
  process.exit(0);
77
16
  }
78
17
  console.error("usage: mslxdff -providers list");
@@ -106,68 +45,11 @@ export async function handleProvider(args) {
106
45
  process.exit(1);
107
46
  }
108
47
  if (id === "list" || id === "status") {
109
- const { loadProviderConfigs, loadProviderKeys, loadProviderShareKeys, loadProviderBaseUrl } = await import("../../../state.js");
110
- const configs = loadProviderConfigs();
111
- const opencodeBase = process.env.UPSTREAM_BASE_URL || "https://opencode.ai";
112
- const orKeys = loadProviderKeys("openrouter");
113
- const orBase = "https://openrouter.ai/api/v1";
114
- const orShare = loadProviderShareKeys("openrouter");
115
- const genericIds = new Set(Object.keys(configs).filter((x) => x !== "opencode" && x !== "openrouter"));
116
- try {
117
- const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
118
- const pk = raw.providerKeys || {};
119
- for (const k of Object.keys(pk)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
120
- const cfgRaw = raw.providerConfigs || {};
121
- for (const k of Object.keys(cfgRaw)) if (k !== "opencode" && k !== "openrouter") genericIds.add(k);
122
- } catch {}
123
- for (const k of Object.keys(process.env)) {
124
- const m = k.match(/^MSLXDFF_(.+)_KEY$/);
125
- if (m) {
126
- const gid = m[1].toLowerCase().replace(/__/g, "-");
127
- if (gid !== "openrouter" && gid !== "opencode") genericIds.add(gid);
128
- }
129
- }
130
- try {
131
- const raw = JSON.parse(readFileSync(defaultStateFile(), "utf8"));
132
- const cfgs = raw.providerConfigs || {};
133
- for (const gid of Object.keys(cfgs)) {
134
- if (gid === "opencode" || gid === "openrouter") continue;
135
- const am = cfgs[gid]?.allowedModels;
136
- if (Array.isArray(am) && am.length) genericIds.add(gid);
137
- }
138
- } catch {}
139
- const list = [];
140
- const { loadProviderAllowAnyModels: _la0 } = await import("../../../state.js");
141
- const { loadProviderAllowedModels } = await import("../../../state.js");
142
- const opAllowed = loadProviderAllowedModels("opencode");
143
- const opAllowAny = _la0("opencode");
144
- const orAllowed = loadProviderAllowedModels("openrouter");
145
- const orAllowAny = _la0("openrouter");
146
- list.push({ id: "opencode", enabled: true, baseUrl: opencodeBase, keys: [], share: false, allowed: opAllowed, allowAny: opAllowAny, note: "built-in, no key, cannot share" });
147
- list.push({ id: "openrouter", enabled: orKeys.length > 0, baseUrl: orBase, keys: orKeys, share: orShare, allowed: orAllowed, allowAny: orAllowAny, note: orKeys.length ? "" : "no keys" });
148
- for (const gid of [...genericIds].sort()) {
149
- const cfg = configs[gid];
150
- const keys = loadProviderKeys(gid);
151
- const baseUrl = loadProviderBaseUrl(gid) || cfg?.baseUrl || "";
152
- const share = loadProviderShareKeys(gid);
153
- const allowed = loadProviderAllowedModels(gid);
154
- const allowAny = _la0(gid);
155
- const enabled = Boolean(baseUrl && keys.length);
156
- let note = "";
157
- if (!baseUrl && !keys.length) note = "no baseUrl, no keys";
158
- else if (!baseUrl) note = "missing baseUrl";
159
- else if (!keys.length) note = "no keys";
160
- list.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, share, note });
161
- }
162
- console.log(`providers (${list.length}):`);
163
- for (const p of list) {
164
- const state = p.enabled ? "enabled " : "disabled";
165
- const keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")}` : "0 keys";
166
- const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
167
- const note = p.note ? ` (${p.note})` : "";
168
- console.log(` ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(28)} baseUrl=${p.baseUrl} ${shareInfo}${note}`);
169
- }
170
- console.log(`\nuse: mslxdff -provider <id> list to inspect one, mslxdff -provider add <id> <baseUrl> <key> to add generic`);
48
+ const { buildProviderRows, formatProviderSection } = await import("../../provider-row.js");
49
+ const rows = buildProviderRows({});
50
+ const enabled = rows.filter((r) => r.enabled).length;
51
+ console.log(`providers ${rows.length} · ${enabled} 已启用 — mslxdff -provider <id> list 单看一个`);
52
+ console.log(formatProviderSection(rows));
171
53
  process.exit(0);
172
54
  }
173
55
  if (id === "add") {
@@ -1,9 +1,9 @@
1
1
  import { join } from "node:path";
2
- import { loadToken, getPort, savePreferredModel, loadPreferredModel } from "../../state.js";
2
+ import { loadToken, getPort, savePreferredModel, loadPreferredModel, loadModelPicks } from "../../state.js";
3
3
  import { getPreferredModel as getPref } from "../../auto.js";
4
4
  import { normalizeModel } from "../../reasoning.js";
5
5
  import { syncToWorkbuddy, workbuddyModelsPath } from "../../sync-workbuddy.js";
6
- import { syncToOpencode, opencodeConfigPath, toExternalAlias, toInternalId } from "../../sync-opencode.js";
6
+ import { syncToOpencode, opencodeConfigPath } from "../../sync-opencode.js";
7
7
  import { createModelsService } from "../../models.js";
8
8
  import { createUpstreamClient } from "../../upstream.js";
9
9
  import { logDir } from "../../logs.js";
@@ -13,13 +13,46 @@ export async function handleSetto(args) {
13
13
  const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
14
14
  const target = args[idx + 1];
15
15
  if (!["workbuddy", "opencode"].includes(target)) {
16
- console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId]");
16
+ console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all]");
17
17
  process.exit(1);
18
18
  }
19
19
  if (target === "opencode") {
20
+ const wantsAll = args.includes("--all") || args.includes("-a") || args[idx + 2] === "all";
21
+ if (wantsAll) {
22
+ const picks = loadModelPicks();
23
+ const list = picks.length ? picks : [loadPreferredModel() || getPref()].filter(Boolean);
24
+ if (!list.length) {
25
+ console.error("no picks and no preferred model; use: mslxdff -setto opencode <modelId>");
26
+ process.exit(1);
27
+ }
28
+ try {
29
+ const { token } = await loadToken();
30
+ const persisted = getPort();
31
+ const envPort = Number(process.env.MSLXDFF_PORT);
32
+ const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
33
+ const file = opencodeConfigPath();
34
+ let inserted = 0, updated = 0;
35
+ for (const rawId of list) {
36
+ const norm = normalizeModel(rawId);
37
+ if (!norm || norm === "auto") continue;
38
+ // 首次循环也同步 preferred(保持 daemon 热重载语义)
39
+ if (rawId === list[0]) savePreferredModel(norm);
40
+ const r = await syncToOpencode({ id: norm, token, port, file });
41
+ if (r.action === "inserted") inserted++; else updated++;
42
+ console.log(` ${r.action} "${r.id}" -> ${r.internal} @ ${file}`);
43
+ }
44
+ console.log(`synced to opencode: ${inserted} inserted, ${updated} updated, total ${list.length} @ ${file}`);
45
+ console.log(` url: http://127.0.0.1:${port}/v1`);
46
+ console.log(` models: ${list.map((x) => normalizeModel(x)).join(", ")}`);
47
+ console.log(` opencode 选 mslxdff/<model> 直达本地,同名如 mslxdff/deepseek-v4-flash-free 或 mslxdff/bai-deepseek-v4-flash`);
48
+ } catch (err) {
49
+ console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
50
+ process.exit(1);
51
+ }
52
+ process.exit(0);
53
+ }
20
54
  const raw = args[idx + 2] && !String(args[idx + 2]).startsWith("-") ? String(args[idx + 2]).trim() : null;
21
55
  let id;
22
- let internal;
23
56
  if (raw) {
24
57
  if (raw === "auto" || !raw) {
25
58
  console.error("modelId 不能为 auto 或空");
@@ -32,8 +65,7 @@ export async function handleSetto(args) {
32
65
  }
33
66
  savePreferredModel(norm);
34
67
  console.log(`default model set to: ${norm} (daemon hot-reloads on next request)`);
35
- internal = toInternalId(norm);
36
- id = toExternalAlias(internal);
68
+ id = norm;
37
69
  } else {
38
70
  const pref = loadPreferredModel() || getPref();
39
71
  if (!pref) {
@@ -45,9 +77,9 @@ export async function handleSetto(args) {
45
77
  console.error("modelId 不能为空");
46
78
  process.exit(1);
47
79
  }
48
- internal = toInternalId(norm);
49
- id = toExternalAlias(internal);
80
+ id = norm;
50
81
  }
82
+ // 可选:校验是否在 free 列表
51
83
  try {
52
84
  const cacheFile = join(logDir(), "models.json");
53
85
  const models = createModelsService({
@@ -62,8 +94,10 @@ export async function handleSetto(args) {
62
94
  ]);
63
95
  if (fresh?.data?.length) {
64
96
  const ids = fresh.data.map((m) => m.id);
65
- if (!ids.includes(internal) && !ids.includes(id)) {
66
- console.log(`warn: "${internal}" not in current free list (${ids.length} models), still syncing to opencode (alias ${id})`);
97
+ // slash 形态也做 dash 兼容检查
98
+ const dashId = id.includes("/") ? id.replace(/\//g, "-") : id;
99
+ if (!ids.includes(id) && !ids.includes(dashId)) {
100
+ console.log(`warn: "${id}" not in current free list (${ids.length} models), still syncing to opencode`);
67
101
  }
68
102
  }
69
103
  } catch {}
@@ -74,11 +108,9 @@ export async function handleSetto(args) {
74
108
  const port = persisted !== null ? persisted : (Number.isInteger(envPort) && envPort > 0 ? envPort : 8989);
75
109
  const file = opencodeConfigPath();
76
110
  const r = await syncToOpencode({ id, token, port, file });
77
- const aliasLabel = r.alias && r.alias !== r.id ? ` (alias ${r.alias} 对应内部 ${r.internal})` : (r.id !== r.internal ? ` (alias for "${r.internal}", 原名仍兼容)` : ` (原名兼容)`);
78
- console.log(`synced to opencode: ${r.action} "${r.id}"${aliasLabel} @ ${file}`);
111
+ console.log(`synced to opencode: ${r.action} "${r.id}" @ ${file}`);
79
112
  console.log(` url: http://127.0.0.1:${port}/v1`);
80
- if (r.id !== r.internal) console.log(` alias: ${r.id} -> ${r.internal} (opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
81
- else console.log(` alias: ${r.internal} (原名直用,opencode 选 mslxdff/${r.id} 直达本地 ${r.internal})`);
113
+ console.log(` opencode 选 mslxdff/${r.id} 直达本地 ${r.internal}${r.storageKey !== r.internal ? ` (dash→${r.internal} 自动映射)` : ""}`);
82
114
  } catch (err) {
83
115
  console.error(`failed to sync to opencode: ${String(err?.message || err)}`);
84
116
  process.exit(1);
@@ -1,5 +1,5 @@
1
1
  import { readFileSync, existsSync } from "node:fs";
2
- import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderShareKeys, loadProviderBaseUrl } from "../state.js";
2
+ import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderShareKeys, loadProviderBaseUrl } from "../state.js";
3
3
 
4
4
  /**
5
5
  * 聚合 genericIds:来自 providerConfigs + providerKeys + env MSLXDFF_*_KEY
@@ -12,7 +12,8 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
12
12
  const upstreamBase = env.UPSTREAM_BASE_URL || "https://opencode.ai";
13
13
  const rows = [];
14
14
  const opAllowed = (() => { try { return loadProviderAllowedModels("opencode", stateFile ? { file: stateFile } : undefined); } catch { return []; } })();
15
- rows.push({ id: "opencode", enabled: true, baseUrl: upstreamBase, keys: [], allowed: opAllowed, share: false, note: "built-in, no key, cannot share", authCount: 0 });
15
+ const opAllowAny = (() => { try { return loadProviderAllowAnyModels("opencode", stateFile ? { file: stateFile } : undefined); } catch { return true; } })();
16
+ rows.push({ id: "opencode", enabled: true, baseUrl: upstreamBase, keys: [], allowed: opAllowed, allowAny: opAllowAny !== false, share: false, note: "built-in, no key, cannot share", authCount: 0 });
16
17
 
17
18
  const genericIds = new Set(Object.keys(configs).filter((id) => id !== "opencode"));
18
19
  try {
@@ -30,16 +31,22 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
30
31
  }
31
32
  }
32
33
 
34
+ // 保证 openrouter 始终可见(即使 0 keys,供用户发现)
35
+ genericIds.add("openrouter");
33
36
  for (const gid of [...genericIds].sort()) {
34
37
  const cfg = configs[gid];
35
38
  let keys = [];
36
39
  let baseUrl = "";
37
40
  let share = false;
38
41
  let allowed = [];
42
+ let allowAny = false;
39
43
  try { keys = loadProviderKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
40
44
  try { baseUrl = loadProviderBaseUrl(gid, stateFile ? { file: stateFile } : undefined) || cfg?.baseUrl || (gid === "openrouter" ? "https://openrouter.ai/api/v1" : gid === "workbuddy" ? "https://copilot.tencent.com" : ""); } catch { baseUrl = cfg?.baseUrl || ""; }
41
45
  try { share = loadProviderShareKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
42
46
  try { allowed = loadProviderAllowedModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
47
+ try { allowAny = loadProviderAllowAnyModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
48
+ // openrouter 特殊:opencode 例外默认 allowAny true,其余默认 false
49
+ if (gid === "opencode") allowAny = true;
43
50
  let enabled = Boolean(baseUrl && keys.length) || (gid === "openrouter" && keys.length > 0);
44
51
  const auths = gid === "workbuddy" ? (() => { try { return loadProviderAuths(gid, stateFile ? { file: stateFile } : undefined) || []; } catch { return []; } })() : [];
45
52
  let note = "";
@@ -54,20 +61,68 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
54
61
  } else if (gid === "workbuddy" && auths.length && auths.length !== keys.length) {
55
62
  note = `${auths.length} auth(s) / ${keys.length} key(s) — 数量不一致请重跑 workbuddy-token-auto.js`;
56
63
  }
57
- rows.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, allowed, share, note, authCount: auths.length });
64
+ rows.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, allowed, allowAny, share, note, authCount: auths.length });
58
65
  }
59
66
  return rows;
60
67
  }
61
68
 
69
+ function maskKey(k) {
70
+ const s = String(k || "").trim();
71
+ if (s.length <= 8) return `${s.slice(0, 3)}…${s.slice(-2)}`;
72
+ return `${s.slice(0, 3)}…${s.slice(-4)}`;
73
+ }
74
+
62
75
  export function formatProviderRow(p) {
63
- const dot = p.enabled ? "●" : "";
64
- const state = p.enabled ? "enabled " : "disabled";
76
+ const isBlocked = !p.allowed.length && !p.allowAny && p.id !== "opencode";
77
+ const dot = !p.enabled ? "○" : isBlocked ? "◐" : "";
78
+ const state = !p.enabled ? "未启用" : isBlocked ? "已启用·阻断" : "已启用";
79
+ // keys
65
80
  let keysInfo;
66
- if (p.id === "opencode") keysInfo = "无需 key (内置)";
67
- else keysInfo = p.keys.length ? `${p.keys.length} key${p.keys.length > 1 ? "s" : ""} ${p.keys.map((k) => `${k.slice(0, 3)}…${k.slice(-3)}`).join(", ")}` : "0 keys";
68
- const authInfo = p.authCount ? ` ${p.authCount} acc` : "";
69
- const allowInfo = p.allowed.length ? `allow=${p.allowed.length}(${p.allowed.slice(0, 2).join(",")}${p.allowed.length > 2 ? "…" : ""})` : "allow=all";
70
- const shareInfo = p.id === "opencode" ? "cannot share" : `share=${p.share ? "ON" : "off"}`;
71
- const note = p.note ? ` (${p.note})` : "";
72
- return ` ${dot} ${p.id.padEnd(12)} ${state} ${keysInfo}${authInfo} ${allowInfo.padEnd(18)} baseUrl=${p.baseUrl} ${shareInfo}${note}`;
81
+ if (p.id === "opencode") keysInfo = "无需 key";
82
+ else if (!p.keys.length) keysInfo = "0 keys";
83
+ else if (p.keys.length === 1) keysInfo = `1 key ${maskKey(p.keys[0])}`;
84
+ else keysInfo = `${p.keys.length} keys ${p.keys.slice(0, 2).map(maskKey).join(", ")}${p.keys.length > 2 ? ` +${p.keys.length - 2}` : ""}`;
85
+ if (p.authCount) keysInfo += ` · ${p.authCount} acc`;
86
+ // allow 空名单 + allowAny OFF = 阻断(安全默认)
87
+ let allowInfo;
88
+ if (!p.allowed.length) {
89
+ if (p.id === "opencode") allowInfo = "allow all";
90
+ else if (p.allowAny) allowInfo = "allow all";
91
+ else allowInfo = "allow none (BLOCKED)";
92
+ } else {
93
+ const head = p.allowed.slice(0, 2).join(", ");
94
+ const more = p.allowed.length > 2 ? ` …+${p.allowed.length - 2}` : "";
95
+ allowInfo = `allow ${p.allowed.length} → ${head}${more}`;
96
+ }
97
+ // share
98
+ const shareInfo = p.id === "opencode" ? "无法共享" : `共享 ${p.share ? "开" : "关"}`;
99
+ // base
100
+ const base = p.baseUrl && p.baseUrl !== "(none)" ? p.baseUrl : "(none)";
101
+ const baseLine = ` └ ${base}${p.note ? ` · ${p.note}` : ""}`;
102
+ // 主行:id 固定 12,状态 6,keys 22,allow 自适应,share 固定
103
+ const main = ` ${dot} ${p.id.padEnd(12)} ${state} ${keysInfo.padEnd(22)} ${allowInfo.padEnd(28)} ${shareInfo}`;
104
+ return `${main}\n${baseLine}`;
105
+ }
106
+
107
+ export function formatProviderSection(rows) {
108
+ if (!rows.length) {
109
+ return [
110
+ " (空) 暂无供应商 — 加一个试试:",
111
+ " mslxdff -provider add myapi https://api.example.com/v1 sk-xxx",
112
+ " 或 node workbuddy-token-auto.js (WorkBuddy 一键接入)",
113
+ ].join("\n");
114
+ }
115
+ const enabled = rows.filter((r) => r.enabled);
116
+ const disabled = rows.filter((r) => !r.enabled);
117
+ const out = [];
118
+ if (enabled.length) {
119
+ out.push(` 已启用 (${enabled.length})`);
120
+ for (const p of enabled) out.push(formatProviderRow(p));
121
+ }
122
+ if (disabled.length) {
123
+ if (enabled.length) out.push("");
124
+ out.push(` 未启用 / 需配置 (${disabled.length})`);
125
+ for (const p of disabled) out.push(formatProviderRow(p));
126
+ }
127
+ return out.join("\n");
73
128
  }
package/src/cli/status.js CHANGED
@@ -13,7 +13,7 @@ import { refreshGroupMembers } from "../groups.js";
13
13
  import { fmtShanghaiYMDHM } from "../time.js";
14
14
  import { fmtStatus, fmtUptime, fmtTs } from "./format.js";
15
15
  import { compareSemver } from "./policy.js";
16
- import { buildProviderRows, formatProviderRow } from "./provider-row.js";
16
+ import { buildProviderRows, formatProviderRow, formatProviderSection } from "./provider-row.js";
17
17
 
18
18
  export async function printStatus(VERSION) {
19
19
  const daemon = readPid();
@@ -53,12 +53,12 @@ export async function printStatus(VERSION) {
53
53
  try {
54
54
  const providerRows = buildProviderRows({});
55
55
  const enabledCount = providerRows.filter((r) => r.enabled).length;
56
- console.log(`\nupstream providers (${providerRows.length}, ${enabledCount} enabled) — mslxdff -providers list 查看详情`);
57
- for (const p of providerRows) {
58
- console.log(formatProviderRow(p));
59
- }
56
+ console.log(`\nupstream providers ${providerRows.length} 个 · ${enabledCount} 已启用 — mslxdff -providers list 查看详情`);
57
+ console.log(formatProviderSection(providerRows));
60
58
  if (enabledCount === 1 && providerRows.length === 1) {
61
- console.log(` (仅 opencode 内置免费通道;按需加:mslxdff -provider add bai https://api.b.ai/v1 <key> 或 node workbuddy-token-auto.js)`);
59
+ console.log(`\n 空状态:仅 opencode 内置免费通道`);
60
+ console.log(` → 加一个私有上游:mslxdff -provider add bai https://api.b.ai/v1 <key>`);
61
+ console.log(` → 或 WorkBuddy 一键接入:node workbuddy-token-auto.js`);
62
62
  }
63
63
  } catch (e) {
64
64
  console.log(`\nupstream providers: (unavailable — ${String(e?.message || e).slice(0, 80)})`);
@@ -1,7 +1,6 @@
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";
5
4
  import { clientIp, json, readBody, parseHops, summarizePrompt, errMsg } from "../helpers.js";
6
5
  import { hedgeDelayMs, shouldHedge } from "../hedge.js";
7
6
  import { runHook } from "../../plugins.js";
@@ -53,25 +52,20 @@ export function createChatGateway({ upstream, auto, logs, peers, maxHops, groups
53
52
  if (aliasResolved) { normalizedRequested = aliasResolved; body = { ...body, model: aliasResolved }; }
54
53
  let requested = normalizedRequested;
55
54
  let aliasInfo = null;
56
- if (requested.startsWith("mslxdff-")) {
57
- const internal = aliasToInternal(requested);
58
- if (internal) { aliasInfo = `${requested} -> ${internal}`; requested = internal; }
59
- } else if (requested.includes("/")) {
60
- const slashIdx = requested.indexOf("/");
61
- const rawPart = requested.slice(slashIdx + 1);
62
- const providerPart = requested.slice(0, slashIdx);
63
- if (rawPart.startsWith("mslxdff-")) {
64
- const internal = aliasToInternal(rawPart);
65
- if (internal) {
66
- aliasInfo = `${requested} -> ${providerPart}/${internal} (alias stripped)`;
67
- requested = `${providerPart}/${internal}`;
68
- if (providerPart === "mslxdff") { requested = internal; aliasInfo = `${rawModel} -> ${internal} (mslxdff alias stripped)`; }
69
- }
70
- } else if (providerPart === "mslxdff") {
71
- aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped, 原名兼容)`;
72
- requested = rawPart;
55
+ // opencode 侧 provider.mslxdff 模型会以 mslxdff/<id> 形式到达,剥掉前缀即得真实模型
56
+ if (requested.startsWith("mslxdff/")) {
57
+ const rawPart = requested.slice("mslxdff/".length);
58
+ aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped)`;
59
+ requested = rawPart;
60
+ // mslxdff/bai-deepseek... 这类 dash 形态二次走 alias 表还原为 bai/...
61
+ const alias2 = getModelAlias(requested);
62
+ if (alias2) {
63
+ aliasInfo = `${rawModel} -> ${alias2} (mslxdff + alias)`;
64
+ requested = alias2;
65
+ body = { ...body, model: alias2 };
73
66
  }
74
67
  }
68
+ // 非 mslxdff 前缀的 dash 形态(如 bai-deepseek)已在首轮 aliasResolved 处理
75
69
  const useAuto = isAutoModel(requested);
76
70
  mark("parsed");
77
71
  if (aliasInfo) { try { res.setHeader("x-mslxdff-alias", aliasInfo); } catch {} }
@@ -8,6 +8,7 @@ export function opencodeConfigPath() {
8
8
  return join(os.homedir(), ".config", "opencode", "opencode.json");
9
9
  }
10
10
 
11
+ // legacy mslxdff- 前缀(保留做兼容剥离,新写入不再使用)
11
12
  export function toExternalAlias(id) {
12
13
  const s = String(id || "").trim();
13
14
  if (!s) return "";
@@ -20,9 +21,28 @@ export function toInternalId(aliasOrRaw) {
20
21
  return s.startsWith("mslxdff-") ? s.slice("mslxdff-".length) : s;
21
22
  }
22
23
 
24
+ // 新存储键:/ → -(与 WorkBuddy 一致),裸 id 原样
25
+ export function toStorageKey(canonical) {
26
+ const s = String(canonical || "").trim();
27
+ if (!s) return "";
28
+ // 先剥 legacy 前缀
29
+ const internal = toInternalId(s);
30
+ return internal.includes("/") ? internal.replace(/\//g, "-") : internal;
31
+ }
32
+
33
+ // 从存储键还原为内部 canonical(需查 alias 表,调用方用 getModelAlias)
34
+ export function storageKeyToCanonical(storageKey) {
35
+ const s = String(storageKey || "").trim();
36
+ if (!s) return "";
37
+ const internal = toInternalId(s);
38
+ return internal;
39
+ }
40
+
23
41
  export function buildOpencodeProvider({ id, token, port }) {
24
42
  const p = Number(port) || 8989;
25
- const alias = toExternalAlias(id);
43
+ const internal = toInternalId(String(id || "").trim());
44
+ const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
45
+ const key = storageKey || toExternalAlias(id); // fallback 兼容
26
46
  return {
27
47
  name: "mslxdff",
28
48
  npm: "@ai-sdk/openai-compatible",
@@ -31,7 +51,7 @@ export function buildOpencodeProvider({ id, token, port }) {
31
51
  baseURL: `http://127.0.0.1:${p}/v1`,
32
52
  },
33
53
  models: {
34
- [alias]: { name: alias },
54
+ [key]: { name: key },
35
55
  },
36
56
  };
37
57
  }
@@ -43,12 +63,11 @@ export function isOpencodeLocalUrl(url) {
43
63
 
44
64
  export async function syncToOpencode({ id, token, port, file } = {}) {
45
65
  const targetFile = file || opencodeConfigPath();
46
- // id 可能是 alias 或原名,统一以 internal 去重、以 external 入库(原名兼容)
47
66
  const normalizedRaw = String(id || "").trim();
48
67
  if (!normalizedRaw) throw new Error("model id required");
49
68
  const internal = toInternalId(normalizedRaw);
50
69
  if (!internal) throw new Error("model id required");
51
- const external = toExternalAlias(internal);
70
+ const storageKey = internal.includes("/") ? internal.replace(/\//g, "-") : internal;
52
71
  const cleanToken = String(token || "");
53
72
  const p = Number(port) || 8989;
54
73
 
@@ -81,32 +100,49 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
81
100
  : null;
82
101
 
83
102
  let action;
84
- let effectiveId = external;
103
+ let effectiveId = storageKey;
85
104
  if (oldProvider) {
86
105
  const oldModels = oldProvider.models && typeof oldProvider.models === "object" && !Array.isArray(oldProvider.models)
87
106
  ? oldProvider.models
88
107
  : {};
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;
108
+ // 归一所有旧 key storageKey 维度,判断是否已存在(兼容 mslxdff- 前缀与 / 形态)
109
+ const normalizeToStorage = (k) => {
110
+ const inner = toInternalId(String(k));
111
+ return inner.includes("/") ? inner.replace(/\//g, "-") : inner;
112
+ };
113
+ let existingKey = null;
114
+ for (const k of Object.keys(oldModels)) {
115
+ if (normalizeToStorage(k) === storageKey) { existingKey = k; break; }
116
+ }
117
+ // 也兼容直接 internal(slash)形态
118
+ if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, internal)) existingKey = internal;
119
+ if (!existingKey && Object.prototype.hasOwnProperty.call(oldModels, storageKey)) existingKey = storageKey;
120
+
93
121
  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;
122
+ if (existingKey) {
123
+ // 已存在:迁移到 storageKey(新规范),清理旧的 legacy 键
124
+ const oldEntry = oldModels[existingKey];
125
+ const merged = oldEntry && typeof oldEntry === "object" && !Array.isArray(oldEntry) ? oldEntry : {};
126
+ // existingKey !== storageKey,需要把旧键删掉,统一到 storageKey
127
+ if (existingKey !== storageKey) {
128
+ // 收集所有同逻辑的旧键一起清理
129
+ for (const k of Object.keys(oldModels)) {
130
+ if (normalizeToStorage(k) === storageKey) delete nextModels[k];
131
+ }
132
+ // 也清理 legacy mslxdff- 前缀变体
133
+ const legacyAlias = toExternalAlias(storageKey);
134
+ const legacyInternal = toExternalAlias(internal);
135
+ if (nextModels[legacyAlias]) delete nextModels[legacyAlias];
136
+ if (legacyInternal !== legacyAlias && nextModels[legacyInternal]) delete nextModels[legacyInternal];
102
137
  }
138
+ nextModels[storageKey] = { ...merged, name: storageKey };
139
+ effectiveId = storageKey;
103
140
  action = "updated";
104
141
  } else {
105
- nextModels[external] = { name: external };
106
- effectiveId = external;
142
+ nextModels[storageKey] = { name: storageKey };
143
+ effectiveId = storageKey;
107
144
  action = "inserted";
108
145
  }
109
- // 合并 provider:保留 name/npm,覆盖 options.baseURL/apiKey,合并 models
110
146
  const nextProvider = {
111
147
  ...oldProvider,
112
148
  name: oldProvider.name || "mslxdff",
@@ -118,18 +154,26 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
118
154
  },
119
155
  models: nextModels,
120
156
  };
121
- // 若插入的是 alias 但原名已存在,上面已处理为不新增;否则正常
122
157
  data.provider.mslxdff = nextProvider;
123
- // 若是新插入且 external 不等于 internal,且 internal 已存在时,需避免双键,上面已处理
124
- // 若是新插入 external 且 internal 不存在,正常插入
125
- if (!exists) {
126
- data.provider.mslxdff.models = nextModels;
127
- }
128
158
  } else {
129
- data.provider.mslxdff = buildOpencodeProvider({ id: external, token: cleanToken, port: p });
159
+ data.provider.mslxdff = buildOpencodeProvider({ id: storageKey, token: cleanToken, port: p });
160
+ // buildOpencodeProvider 已用 storageKey,这里再确保
161
+ if (!data.provider.mslxdff.models[storageKey]) {
162
+ data.provider.mslxdff.models = { [storageKey]: { name: storageKey } };
163
+ }
130
164
  action = "inserted";
131
165
  }
132
166
 
167
+ // 若内部含 /,注册 dash→slash 别名,供网关 mslxdff 侧自动映射(与 WorkBuddy 同表)
168
+ if (internal.includes("/") && storageKey !== internal) {
169
+ try {
170
+ const { loadModelAliases, registerModelAlias, persistModelAliases } = await import("./providers/model-id.js");
171
+ loadModelAliases();
172
+ registerModelAlias(storageKey, internal);
173
+ persistModelAliases();
174
+ } catch {}
175
+ }
176
+
133
177
  // 原子写
134
178
  mkdirSync(dirname(targetFile), { recursive: true });
135
179
  const tmp = `${targetFile}.tmp.${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
@@ -148,5 +192,5 @@ export async function syncToOpencode({ id, token, port, file } = {}) {
148
192
  }
149
193
  } catch {}
150
194
 
151
- return { action, file: targetFile, id: effectiveId, alias: external, internal, corrupted };
195
+ return { action, file: targetFile, id: effectiveId, alias: storageKey, internal, corrupted, storageKey };
152
196
  }