mslxdff 0.1.131 → 0.1.132
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 +1 -1
- package/src/cli/commands/provider/add.js +2 -2
- package/src/cli/commands/provider/config.js +1 -24
- package/src/cli/commands/provider/index.js +1 -2
- package/src/cli/commands/provider/keys.js +2 -2
- package/src/cli/provider-row.js +4 -6
- package/src/cli/status.js +1 -1
- package/src/providers/share-keys.js +15 -19
- package/src/providers/workbuddy/index.js +2 -10
- package/src/routes/chat/via-route-handler.js +4 -8
- package/src/routes/peers.js +1 -1
- package/src/state/facade.js +1 -3
- package/src/state/merge.js +0 -1
- package/src/state/provider-config.js +1 -2
- package/src/state/schemas/provider.js +10 -14
package/package.json
CHANGED
|
@@ -16,7 +16,7 @@ export async function handleProviderAdd(id, sub, rest) {
|
|
|
16
16
|
console.error(`provider "${gid}" is built-in — use: mslxdff -provider ${gid} add <key> or mslxdff -provider ${gid} set-url <url>`);
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
|
-
const { saveProviderConfig, loadProviderConfig
|
|
19
|
+
const { saveProviderConfig, loadProviderConfig } = await import("../../../state.js");
|
|
20
20
|
const { normalizeProviderId } = await import("../../../providers/model-id.js");
|
|
21
21
|
const nid = normalizeProviderId(gid);
|
|
22
22
|
if (!nid) { console.error(`invalid provider id: ${gid}`); process.exit(1); }
|
|
@@ -98,7 +98,7 @@ export async function handleProviderAdd(id, sub, rest) {
|
|
|
98
98
|
console.log(` keys: ${keys.length} (${keys.map((k) => `${k.slice(0, 4)}…${k.slice(-4)}`).join(", ")})`);
|
|
99
99
|
if (allowedModels.length) console.log(` allowedModels: ${allowedModels.length} (${allowedModels.join(", ")})`);
|
|
100
100
|
else console.log(` allowedModels: (none — BLOCKED, otherwise unusable) → mslxdff -provider ${nid} allowlist set <model...> OR mslxdff -provider ${nid} allowAny on (allow all)`);
|
|
101
|
-
console.log(` share:
|
|
101
|
+
console.log(` share: 借出(默认,key 随转发自动附带,ADR-0019)`);
|
|
102
102
|
console.log(` allowAny: OFF (secure, empty allowlist = 403 block before upstream) — enable via: mslxdff -provider ${nid} allowAny on`);
|
|
103
103
|
console.log(` use as: ${nid}/<model-id> — restart daemon to activate`);
|
|
104
104
|
if (allowedModels.length) console.log(` opencode 中使用: mslxdff -setto opencode ${nid}/${allowedModels[0]} (同步进 opencode.json,会一并设为默认模型)`);
|
|
@@ -5,7 +5,7 @@ export async function handleProviderConfig(id, sub, rest) {
|
|
|
5
5
|
console.error("opencode 是内置供应商,不能删除");
|
|
6
6
|
process.exit(1);
|
|
7
7
|
}
|
|
8
|
-
const { loadProviderConfigs, saveProviderConfig
|
|
8
|
+
const { loadProviderConfigs, saveProviderConfig } = await import("../../../state.js");
|
|
9
9
|
const { isPidAlive, readPid } = await import("../../../daemon.js");
|
|
10
10
|
const configs = loadProviderConfigs();
|
|
11
11
|
if (!configs[id] && !configs[nid]) {
|
|
@@ -33,16 +33,9 @@ export async function handleProviderConfig(id, sub, rest) {
|
|
|
33
33
|
writeStateImmediate(file, { providerKeys: nk });
|
|
34
34
|
changed = true;
|
|
35
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
36
|
// 清理模型错误/延迟中该供应商前缀的条目(可选,不阻塞)
|
|
43
37
|
void changed;
|
|
44
38
|
} catch {}
|
|
45
|
-
try { saveProviderShareKeys(id, false); } catch {}
|
|
46
39
|
console.log(`已删除供应商: ${id} — 配置已清空`);
|
|
47
40
|
// 需要重启才生效,自动重启
|
|
48
41
|
const pid = readPid();
|
|
@@ -122,21 +115,5 @@ export async function handleProviderConfig(id, sub, rest) {
|
|
|
122
115
|
console.log(`set ${id} baseUrl: ${String(url).trim().replace(/\/+$/, "")} — restart daemon to activate`);
|
|
123
116
|
process.exit(0);
|
|
124
117
|
}
|
|
125
|
-
if (sub === "share") {
|
|
126
|
-
const { loadProviderShareKeys, saveProviderShareKeys } = await import("../../../state.js");
|
|
127
|
-
const on = rest[1];
|
|
128
|
-
if (!on) {
|
|
129
|
-
console.log(`share keys to peers: ${loadProviderShareKeys(id) ? "ON" : "off"}`);
|
|
130
|
-
process.exit(0);
|
|
131
|
-
}
|
|
132
|
-
if (!["on", "off", "1", "0", "true", "false"].includes(String(on).toLowerCase())) {
|
|
133
|
-
console.error("usage: mslxdff -provider openrouter share on|off");
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
const state = ["on", "1", "true"].includes(String(on).toLowerCase());
|
|
137
|
-
saveProviderShareKeys(id, state);
|
|
138
|
-
console.log(`share keys to peers: ${state ? "ON" : "off"} — restart daemon to activate`);
|
|
139
|
-
process.exit(0);
|
|
140
|
-
}
|
|
141
118
|
return false;
|
|
142
119
|
}
|
|
@@ -25,13 +25,12 @@ export async function handleProvider(args) {
|
|
|
25
25
|
const sub = args[idx + 2];
|
|
26
26
|
const rest = args.slice(idx + 2);
|
|
27
27
|
if (!id) {
|
|
28
|
-
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|models|clear|
|
|
28
|
+
console.error("usage: mslxdff -provider <id> [key...|add|remove|list|models|clear|set-url|allowlist|allowAny]");
|
|
29
29
|
console.error(" e.g. mslxdff -provider openrouter sk-1 sk-2 sk-3 set multiple keys (replaces all)");
|
|
30
30
|
console.error(" mslxdff -provider openrouter add sk-4 append one key");
|
|
31
31
|
console.error(" mslxdff -provider openrouter remove sk-1 remove a key by value");
|
|
32
32
|
console.error(" mslxdff -provider openrouter list list all keys (masked)");
|
|
33
33
|
console.error(" mslxdff -provider openrouter models [--json] list provider models (allowlist filtered)");
|
|
34
|
-
console.error(" mslxdff -provider openrouter share on|off share keys with peers on outgoing forward (ADR-0008)");
|
|
35
34
|
console.error(" mslxdff -provider openrouter set-url https://api.example.com/v1");
|
|
36
35
|
console.error(" mslxdff -provider openrouter set-models-path /v1/models");
|
|
37
36
|
console.error(" mslxdff -provider openrouter set-chat-path /v1/chat/completions");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export async function handleProviderKeys(id, sub, rest, args) {
|
|
2
2
|
if (sub === "list" || sub === "status") {
|
|
3
|
-
const { loadProviderKeys, loadProviderConfig,
|
|
3
|
+
const { loadProviderKeys, loadProviderConfig, loadProviderAllowedModels, loadProviderAllowAnyModels } = await import("../../../state.js");
|
|
4
4
|
const keys = loadProviderKeys(id);
|
|
5
5
|
const cfg = loadProviderConfig(id);
|
|
6
6
|
const allowed = loadProviderAllowedModels(id);
|
|
@@ -15,7 +15,7 @@ export async function handleProviderKeys(id, sub, rest, args) {
|
|
|
15
15
|
} else {
|
|
16
16
|
console.log(` keys: (no keys configured)`);
|
|
17
17
|
}
|
|
18
|
-
console.log(` share keys to peers:
|
|
18
|
+
console.log(` share keys to peers: 借出(默认,key 随转发自动附带,ADR-0019)`);
|
|
19
19
|
console.log(` allowAnyModels: ${allowAny ? "ON (empty allowlist = allow all)" : "OFF (empty allowlist = BLOCK ALL)"} (mslxdff -provider ${id} allowAny on|off)`);
|
|
20
20
|
if (allowed.length) console.log(` allowedModels: ${allowed.length} (${allowed.join(", ")}) — only these can be used`);
|
|
21
21
|
else console.log(` allowedModels: (none — ${allowAny ? "allow all" : "BLOCK ALL"}) (mslxdff -provider ${id} allowlist set <model...> or allowAny on)`);
|
package/src/cli/provider-row.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from "node:fs";
|
|
2
|
-
import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels,
|
|
2
|
+
import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderAllowAnyModels, loadProviderBaseUrl } from "../state.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 聚合 genericIds:来自 providerConfigs + providerKeys + env MSLXDFF_*_KEY
|
|
@@ -37,12 +37,10 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
|
|
|
37
37
|
const cfg = configs[gid];
|
|
38
38
|
let keys = [];
|
|
39
39
|
let baseUrl = "";
|
|
40
|
-
let share = false;
|
|
41
40
|
let allowed = [];
|
|
42
41
|
let allowAny = false;
|
|
43
42
|
try { keys = loadProviderKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
44
43
|
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 || ""; }
|
|
45
|
-
try { share = loadProviderShareKeys(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
46
44
|
try { allowed = loadProviderAllowedModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
47
45
|
try { allowAny = loadProviderAllowAnyModels(gid, stateFile ? { file: stateFile } : undefined); } catch {}
|
|
48
46
|
// openrouter 特殊:opencode 例外默认 allowAny true,其余默认 false
|
|
@@ -61,7 +59,7 @@ export function buildProviderRows({ stateFile, env = process.env } = {}) {
|
|
|
61
59
|
} else if (gid === "workbuddy" && auths.length && auths.length !== keys.length) {
|
|
62
60
|
note = `${auths.length} auth(s) / ${keys.length} key(s) — 数量不一致请重跑 workbuddy-token-auto.js`;
|
|
63
61
|
}
|
|
64
|
-
rows.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, allowed, allowAny,
|
|
62
|
+
rows.push({ id: gid, enabled, baseUrl: baseUrl || "(none)", keys, allowed, allowAny, note, authCount: auths.length });
|
|
65
63
|
}
|
|
66
64
|
return rows;
|
|
67
65
|
}
|
|
@@ -94,8 +92,8 @@ export function formatProviderRow(p) {
|
|
|
94
92
|
const more = p.allowed.length > 2 ? ` …+${p.allowed.length - 2}` : "";
|
|
95
93
|
allowInfo = `allow ${p.allowed.length} → ${head}${more}`;
|
|
96
94
|
}
|
|
97
|
-
// share
|
|
98
|
-
const shareInfo = p.id === "opencode" ? "无法共享" :
|
|
95
|
+
// share(ADR-0019:默认借出,无开关)
|
|
96
|
+
const shareInfo = p.id === "opencode" ? "无法共享" : "共享 借出";
|
|
99
97
|
// base
|
|
100
98
|
const base = p.baseUrl && p.baseUrl !== "(none)" ? p.baseUrl : "(none)";
|
|
101
99
|
const baseLine = ` └ ${base}${p.note ? ` · ${p.note}` : ""}`;
|
package/src/cli/status.js
CHANGED
|
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { readPid, readPidVersion, isPidAlive, pidFile } from "../daemon.js";
|
|
5
5
|
import { defaultStateFile, getPort, loadGroupsJoined, loadModelErrors, loadModelStats, loadModelPicks } from "../state.js";
|
|
6
6
|
import { resolvePort } from "../server.js";
|
|
7
|
-
import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels,
|
|
7
|
+
import { loadProviderKeys, loadProviderAuths, loadProviderConfigs, loadProviderAllowedModels, loadProviderBaseUrl } from "../state.js";
|
|
8
8
|
import { loadToken } from "../state.js";
|
|
9
9
|
import { logDir } from "../logs.js";
|
|
10
10
|
import { createGroupsService } from "../groups.js";
|
|
@@ -1,29 +1,25 @@
|
|
|
1
|
-
// ADR-
|
|
1
|
+
// ADR-0019:借道 = 用你的 key —— 转发时自动借出,无开关、无白名单(组内互信是前提)。
|
|
2
2
|
// header: `x-mslxdff-share-keys: <providerId>=<key1>,<key2>;<providerId2>=<key3>`
|
|
3
3
|
import { splitModelId, DEFAULT_PROVIDER } from "./model-id.js";
|
|
4
|
-
import {
|
|
4
|
+
import { classifyProvider } from "./classify.js";
|
|
5
|
+
import { loadProviderKeys, listProviderIdsWithKeys } from "../state.js";
|
|
5
6
|
|
|
6
7
|
export const SHARE_KEYS_HEADER = "x-mslxdff-share-keys";
|
|
7
8
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
9
|
+
// 硬排除:refresh-token 型凭据(对端刷新会轮换 token,与本机互相踢下线)——正确性问题。
|
|
10
|
+
const NEVER_SHARE_IDS = new Set(["cline", "clinebot"]);
|
|
11
|
+
|
|
12
|
+
// 本节点应 cast key 到出站转发的供应商 id 集合 = 所有「本机有 key」的供应商,
|
|
13
|
+
// 减去:opencode(无 key 且恒排除)、local-only(workbuddy,本就不走组员)、刷新型凭据。
|
|
12
14
|
export function shareableProviderIds({ file } = {}) {
|
|
13
|
-
const ids =
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ids.add(id);
|
|
20
|
-
}
|
|
21
|
-
return [...ids];
|
|
22
|
-
}
|
|
23
|
-
for (const id of ["openrouter"]) {
|
|
24
|
-
if (id !== DEFAULT_PROVIDER && loadProviderKeys(id, { file }).length && loadProviderShareKeys(id, { file })) ids.add(id);
|
|
15
|
+
const ids = [];
|
|
16
|
+
for (const id of listProviderIdsWithKeys({ file })) {
|
|
17
|
+
if (id === DEFAULT_PROVIDER) continue;
|
|
18
|
+
if (NEVER_SHARE_IDS.has(id)) continue;
|
|
19
|
+
try { if (classifyProvider(id) === "local-only") continue; } catch {}
|
|
20
|
+
ids.push(id);
|
|
25
21
|
}
|
|
26
|
-
return
|
|
22
|
+
return ids;
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
// 组装 share header 值(只含 shareable 且当前请求命中前缀的供应商)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createKeyRing } from "../keyring.js";
|
|
2
|
-
import { loadProviderKeys, loadProviderAuths, loadProviderBaseUrl,
|
|
2
|
+
import { loadProviderKeys, loadProviderAuths, loadProviderBaseUrl, WORKBUDDY_DEFAULT_BASE_URL, loadProviderModelsPath, loadProviderChatPath } from "../../state.js";
|
|
3
3
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
4
4
|
import { compatFetch } from "../../compat.js";
|
|
5
5
|
import { join } from "node:path";
|
|
@@ -176,15 +176,7 @@ export function createWorkbuddyProvider({
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
async function chatWithKeys(body, keysOverride) {
|
|
179
|
-
|
|
180
|
-
try { return loadProviderShareKeys(id, file ? { file } : {}); } catch { return false; }
|
|
181
|
-
})();
|
|
182
|
-
// allow injected balanceCache/logger to bypass share check for isolated tests:
|
|
183
|
-
// if keysOverride provided and logger is injected (test mode), permit share without state flag
|
|
184
|
-
const isTestInjection = !!balanceCacheOpt || !!loggerOpt;
|
|
185
|
-
if (!shareOn && !isTestInjection) {
|
|
186
|
-
return chatSvc.runChat(body, ring);
|
|
187
|
-
}
|
|
179
|
+
// 借入的 key(ADR-0019:转发时自动附带)——用隔离的 tmp ring/auth,不改动本机 ring。
|
|
188
180
|
// Isolated tmp ring and auth without mutating shared arrays
|
|
189
181
|
const tmpKeys = [...keysOverride].filter((k) => typeof k === "string" && k.trim().length).map((k) => k.trim());
|
|
190
182
|
const tmpAuth = authList[0] || { uid: "", domain: "www.codebuddy.cn", enterpriseId: "", refreshToken: "" };
|
|
@@ -2,8 +2,7 @@ import { createRelayPipeline } from "./relay-pipeline.js";
|
|
|
2
2
|
import { relay, SLOW_TOTAL_MS, STREAM_TIMEOUT_MS, STALL_TIMEOUT_MS, SCORE_STALL_MS } from "../stream.js";
|
|
3
3
|
import { buildFallbackInfo } from "../fallback.js";
|
|
4
4
|
import { getViaRoute } from "../../bench/via-routes.js";
|
|
5
|
-
import {
|
|
6
|
-
import { SHARE_KEYS_HEADER } from "../../providers/share-keys.js";
|
|
5
|
+
import { buildShareKeysHeader, SHARE_KEYS_HEADER } from "../../providers/share-keys.js";
|
|
7
6
|
import { errMsg } from "../helpers.js";
|
|
8
7
|
import { compatFetch } from "../../compat.js";
|
|
9
8
|
|
|
@@ -61,12 +60,9 @@ export async function handleViaRoute({
|
|
|
61
60
|
evt("via-route-hit", { reqId: handlerCtx.reqId, model, peer: peer.url, peerLabel, routeBest: route.best, at: route.at });
|
|
62
61
|
// 单路径转发,不并发:直接打 best peer
|
|
63
62
|
const hops = handlerCtx.hops || 0;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const keys = loadProviderKeys(providerId) || [];
|
|
68
|
-
if (keys.length) shareHeader = `${providerId}=${keys.join(",")}`;
|
|
69
|
-
} catch {}
|
|
63
|
+
// ADR-0019:借道 = 用 A 的 key —— 转发时自动附带(组内互信,无开关),与 peers.js 共用同一组装点。
|
|
64
|
+
// Note: 见 .agents/notes/implemented/architecture/2026-09-17-share-keys-no-switch.md
|
|
65
|
+
const shareHeader = buildShareKeysHeader(model);
|
|
70
66
|
const controller = new AbortController();
|
|
71
67
|
const timer = setTimeout(() => controller.abort(new Error("via-route timeout 30000ms")), 30000);
|
|
72
68
|
let upRes;
|
package/src/routes/peers.js
CHANGED
|
@@ -122,7 +122,7 @@ async function forwardToPeer(peer, body, model, hops, controller = new AbortCont
|
|
|
122
122
|
"x-mslxdff-model-lock": model,
|
|
123
123
|
"Accept": "text/event-stream",
|
|
124
124
|
};
|
|
125
|
-
// ADR-
|
|
125
|
+
// ADR-0019:命中供应商的 key 随转发自动借出(组内互信,无开关;opencode/workbuddy/cline 硬排除)
|
|
126
126
|
const shareHeader = buildShareKeysHeader(model);
|
|
127
127
|
if (shareHeader) headers[SHARE_KEYS_HEADER] = shareHeader;
|
|
128
128
|
const dispatcher = getPeerDispatcher();
|
package/src/state/facade.js
CHANGED
|
@@ -5,16 +5,14 @@ export {
|
|
|
5
5
|
WORKBUDDY_DEFAULT_BASE_URL,
|
|
6
6
|
providerKeyEnv,
|
|
7
7
|
providerBaseUrlEnv,
|
|
8
|
-
providerShareEnv,
|
|
9
8
|
loadProviderKeys,
|
|
10
9
|
loadProviderKey,
|
|
10
|
+
listProviderIdsWithKeys,
|
|
11
11
|
saveProviderKeys,
|
|
12
12
|
saveProviderKey,
|
|
13
13
|
addProviderKey,
|
|
14
14
|
removeProviderKey,
|
|
15
15
|
removeProviderKeys,
|
|
16
|
-
loadProviderShareKeys,
|
|
17
|
-
saveProviderShareKeys,
|
|
18
16
|
loadProviderConfigs,
|
|
19
17
|
loadProviderConfig,
|
|
20
18
|
loadProviderBaseUrl,
|
package/src/state/merge.js
CHANGED
|
@@ -56,10 +56,9 @@ export function defaultChatPath(id) {
|
|
|
56
56
|
|
|
57
57
|
export const providerKeyEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_KEY`;
|
|
58
58
|
export const providerBaseUrlEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BASE_URL`;
|
|
59
|
-
export const providerShareEnv = (id) => `MSLXDFF_${String(id || "").toUpperCase().replace(/[^A-Z0-9]/g, "_")}_SHARE_KEYS`;
|
|
60
59
|
|
|
61
60
|
/**
|
|
62
|
-
* validate 7 字段:baseUrl, keys, auths, allowedModels, modelsPath, chatPath
|
|
61
|
+
* validate 7 字段:baseUrl, keys, auths, allowedModels, modelsPath, chatPath(share 已删,key 随转发自动借出)
|
|
63
62
|
* 返回 {ok, clean, errors}
|
|
64
63
|
*/
|
|
65
64
|
export function validateProviderConfig(raw = {}, providerId = "") {
|
|
@@ -9,7 +9,6 @@ export {
|
|
|
9
9
|
defaultChatPath,
|
|
10
10
|
providerKeyEnv,
|
|
11
11
|
providerBaseUrlEnv,
|
|
12
|
-
providerShareEnv,
|
|
13
12
|
} from "../provider-config.js";
|
|
14
13
|
import {
|
|
15
14
|
normalizeBaseUrl as _normalizeBaseUrl,
|
|
@@ -20,7 +19,6 @@ import {
|
|
|
20
19
|
defaultChatPath as _defaultChatPath,
|
|
21
20
|
providerKeyEnv as _providerKeyEnv,
|
|
22
21
|
providerBaseUrlEnv as _providerBaseUrlEnv,
|
|
23
|
-
providerShareEnv as _providerShareEnv,
|
|
24
22
|
WORKBUDDY_DEFAULT_BASE_URL as _WORKBUDDY,
|
|
25
23
|
} from "../provider-config.js";
|
|
26
24
|
|
|
@@ -52,6 +50,16 @@ export function loadProviderKeys(id, { file = defaultStateFile() } = {}) {
|
|
|
52
50
|
return [];
|
|
53
51
|
}
|
|
54
52
|
export function loadProviderKey(id, opts = {}) { return loadProviderKeys(id, opts)[0] || ""; }
|
|
53
|
+
// 本节点所有「有 key」的供应商 id(providerConfigs ∪ providerKeys,含 env 覆盖)——
|
|
54
|
+
// 转发时默认借出的候选集合由它枚举;见 src/providers/share-keys.js
|
|
55
|
+
export function listProviderIdsWithKeys({ file = defaultStateFile() } = {}) {
|
|
56
|
+
const st = readState(file);
|
|
57
|
+
const ids = new Set();
|
|
58
|
+
for (const src of [st.providerConfigs, st.providerKeys]) {
|
|
59
|
+
if (src && typeof src === "object" && !Array.isArray(src)) for (const id of Object.keys(src)) ids.add(id);
|
|
60
|
+
}
|
|
61
|
+
return [...ids].filter((id) => loadProviderKeys(id, { file }).length > 0);
|
|
62
|
+
}
|
|
55
63
|
export function saveProviderKeys(id, list, { file = defaultStateFile() } = {}) {
|
|
56
64
|
const keys = { ...(readState(file).providerKeys || {}) };
|
|
57
65
|
const clean = [...new Set((Array.isArray(list) ? list : []).map((k) => String(k || "").trim()).filter(Boolean))];
|
|
@@ -72,18 +80,6 @@ export function removeProviderKeys(id, targets = [], opts = {}) {
|
|
|
72
80
|
const list = loadProviderKeys(id, opts).filter((k) => !set.has(k));
|
|
73
81
|
return saveProviderKeys(id, list, opts);
|
|
74
82
|
}
|
|
75
|
-
export function loadProviderShareKeys(id, { file = defaultStateFile() } = {}) {
|
|
76
|
-
const env = process.env[_providerShareEnv(id)] || "";
|
|
77
|
-
if (env) return ["1", "true", "on", "yes"].includes(String(env).trim().toLowerCase());
|
|
78
|
-
const map = readState(file).providerShareKeys;
|
|
79
|
-
return !!(map && typeof map === "object" && map[id]);
|
|
80
|
-
}
|
|
81
|
-
export function saveProviderShareKeys(id, on, { file = defaultStateFile() } = {}) {
|
|
82
|
-
const map = { ...(readState(file).providerShareKeys || {}) };
|
|
83
|
-
if (on) map[id] = true; else delete map[id];
|
|
84
|
-
writeStateImmediate(file, { providerShareKeys: map });
|
|
85
|
-
return !!on;
|
|
86
|
-
}
|
|
87
83
|
export function loadProviderConfigs({ file = defaultStateFile() } = {}) {
|
|
88
84
|
const v = readState(file).providerConfigs;
|
|
89
85
|
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|