mslxdff 0.1.136 → 0.1.140
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 +2 -2
- package/src/chat/gateway.js +1 -1
- package/src/chat/prompt.js +1 -1
- package/src/chat/terminal.js +1 -1
- package/src/chooser.js +31 -12
- package/src/cli/commands/model/list-live.js +22 -0
- package/src/cli/commands/model/list-providers.js +1 -1
- package/src/cli/commands/model/list-render.js +2 -2
- package/src/cli/commands/model/list.js +2 -0
- package/src/cli/commands/model/live-models.js +24 -0
- package/src/cli/commands/provider/add.js +2 -1
- package/src/cli/commands/provider/bench-via.js +1 -1
- package/src/cli/commands/provider/cline-free.js +94 -0
- package/src/cli/commands/provider/cline-login.js +20 -15
- package/src/cli/commands/provider/codearts-login.js +45 -0
- package/src/cli/commands/provider/index.js +34 -0
- package/src/cli/commands/provider/keys.js +13 -2
- package/src/cli/commands/provider/models.js +13 -1
- package/src/cli/commands/provider/qoder-checkin.js +81 -0
- package/src/cli/commands/provider/qoder-login.js +103 -0
- package/src/cli/commands/provider/traework-login.js +202 -0
- package/src/cli/commands/provider/workbuddy-login.js +1 -1
- package/src/cli/commands/workbuddy.js +14 -21
- package/src/cli/index.js +3 -0
- package/src/cli/interactive.js +41 -4
- package/src/cli/provider-row.js +1 -1
- package/src/compat.js +23 -5
- package/src/model-capabilities/merge.js +2 -2
- package/src/providers/classify.js +1 -1
- package/src/providers/cline/auth.js +1 -1
- package/src/providers/cline/chat.js +14 -11
- package/src/providers/cline/free-catalog.js +71 -0
- package/src/providers/cline/index.js +2 -16
- package/src/providers/cline/models.js +28 -51
- package/src/providers/codearts/auth-pool.js +148 -0
- package/src/providers/codearts/chat.js +176 -0
- package/src/providers/codearts/const.js +60 -0
- package/src/providers/codearts/dpop.js +95 -0
- package/src/providers/codearts/index.js +160 -0
- package/src/providers/codearts/login.js +214 -0
- package/src/providers/codearts/models.js +182 -0
- package/src/providers/codearts/sign.js +96 -0
- package/src/providers/codearts/sse.js +204 -0
- package/src/providers/codearts/stream.js +209 -0
- package/src/providers/codearts/sts.js +124 -0
- package/src/providers/codearts.js +1 -0
- package/src/providers/model-id.js +4 -4
- package/src/providers/qoder/account-store.js +107 -0
- package/src/providers/qoder/aggregate.js +51 -0
- package/src/providers/qoder/baseprompt.json +592 -0
- package/src/providers/qoder/chat.js +72 -0
- package/src/providers/qoder/checkin.js +160 -0
- package/src/providers/qoder/constants.js +45 -0
- package/src/providers/qoder/encode.js +42 -0
- package/src/providers/qoder/fingerprint.js +39 -0
- package/src/providers/qoder/index.js +106 -0
- package/src/providers/qoder/models.js +62 -0
- package/src/providers/qoder/oauth.js +85 -0
- package/src/providers/qoder/payload.js +156 -0
- package/src/providers/qoder/request.js +26 -0
- package/src/providers/qoder/session.js +135 -0
- package/src/providers/qoder/sse.js +46 -0
- package/src/providers/qoder/stream.js +81 -0
- package/src/providers/qoder.js +2 -0
- package/src/providers/registry.js +16 -1
- package/src/providers/share-keys.js +7 -2
- package/src/providers/traework/account-store.js +149 -0
- package/src/providers/traework/chat.js +211 -0
- package/src/providers/traework/checkin.js +35 -0
- package/src/providers/traework/constants.js +21 -0
- package/src/providers/traework/errors.js +52 -0
- package/src/providers/traework/headers.js +47 -0
- package/src/providers/traework/index.js +122 -0
- package/src/providers/traework/models.js +97 -0
- package/src/providers/traework/payload.js +86 -0
- package/src/providers/traework/sse.js +184 -0
- package/src/providers/traework/token.js +52 -0
- package/src/providers/traework.js +2 -0
- package/src/providers/workbuddy/account-store.js +73 -7
- package/src/providers/workbuddy/chat.js +1 -1
- package/src/providers/workbuddy/index.js +10 -20
- package/src/readline-compat.js +9 -10
- package/src/routes/models-route.js +19 -1
- package/src/runtime/bootstrap.js +10 -0
- package/src/runtime/provider-gate.js +30 -0
- package/src/runtime/providers-setup.js +18 -3
- package/src/runtime/qoder-checkin.js +93 -0
- package/src/runtime/server-lifecycle.js +2 -2
- package/src/runtime/traework-checkin.js +104 -0
- package/src/state/migrations/cline-unify.js +94 -0
- package/src/state/migrations.js +33 -0
- package/src/state/schemas/token.js +6 -2
- package/src/state/schemas/use-group.js +4 -4
- package/src/sync-workbuddy.js +1 -1
- package/src/upstream-engine/index.js +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* qoder 每日自动签到(daemon 内置),仿 src/runtime/traework-checkin.js。
|
|
3
|
+
* 开关:MSLXDFF_QODER_CHECKIN=0 关闭(默认开);时间:MSLXDFF_QODER_CHECKIN_HOUR(默认 9 点)。
|
|
4
|
+
* 账号源 = auths/qoder-<uid>.json(每号自带 region,cn 走 daily-check-in,global 走 campaigns)。
|
|
5
|
+
* 只领真积分(CLAIM_BENEFIT / daily-check-in);VIEW_DETAILS 等促销条目需手动 --any 才碰。
|
|
6
|
+
*/
|
|
7
|
+
import { todayKey, nextRunDelayMs, shouldCatchUp } from "./workbuddy-checkin.js";
|
|
8
|
+
|
|
9
|
+
export function isCheckinEnabled(env = process.env) {
|
|
10
|
+
const v = String(env.MSLXDFF_QODER_CHECKIN ?? "1").trim().toLowerCase();
|
|
11
|
+
return !["0", "false", "off", "no"].includes(v);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCheckinHour(env = process.env) {
|
|
15
|
+
const h = Number(env.MSLXDFF_QODER_CHECKIN_HOUR);
|
|
16
|
+
return Number.isInteger(h) && h >= 0 && h <= 23 ? h : 9;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function setupQoderCheckin({ bus, logs } = {}) {
|
|
20
|
+
function emit(type, data = {}) {
|
|
21
|
+
const entry = { ts: Date.now(), type, ...data };
|
|
22
|
+
try { bus?.emit(entry); } catch {}
|
|
23
|
+
try { logs?.appendEvent?.(entry); } catch {}
|
|
24
|
+
try { console.log(`[qoder-checkin] ${type} ${JSON.stringify(data)}`.slice(0, 500)); } catch {}
|
|
25
|
+
}
|
|
26
|
+
if (!isCheckinEnabled()) {
|
|
27
|
+
console.log("[qoder-checkin] disabled (set MSLXDFF_QODER_CHECKIN=1 to enable)");
|
|
28
|
+
emit("qoder-checkin-disabled");
|
|
29
|
+
return { enabled: false };
|
|
30
|
+
}
|
|
31
|
+
const hour = getCheckinHour();
|
|
32
|
+
console.log(`[qoder-checkin] enabled: daily ${String(hour).padStart(2, "0")}:00 local`);
|
|
33
|
+
emit("qoder-checkin-enabled", { hour });
|
|
34
|
+
|
|
35
|
+
const stateMod = await import("../state.js");
|
|
36
|
+
const { listAccountDocs } = await import("../providers/qoder/account-store.js");
|
|
37
|
+
|
|
38
|
+
let running = false;
|
|
39
|
+
async function runOnce(reason) {
|
|
40
|
+
if (running) return { skipped: "already-running" };
|
|
41
|
+
running = true;
|
|
42
|
+
try {
|
|
43
|
+
const accounts = listAccountDocs()
|
|
44
|
+
.map((d) => ({ uid: d.uid, deviceToken: d.doc?.auth?.deviceToken || "", region: d.doc?.auth?.region || "global" }))
|
|
45
|
+
.filter((a) => a.deviceToken);
|
|
46
|
+
if (!accounts.length) {
|
|
47
|
+
emit("qoder-checkin-no-accounts", { reason });
|
|
48
|
+
return { ok: false, reason: "no-accounts" };
|
|
49
|
+
}
|
|
50
|
+
const { runCheckin } = await import("../providers/qoder/checkin.js");
|
|
51
|
+
const results = [];
|
|
52
|
+
for (const a of accounts) {
|
|
53
|
+
try {
|
|
54
|
+
const r = await runCheckin({ deviceToken: a.deviceToken, region: a.region });
|
|
55
|
+
results.push({ uid: String(a.uid).slice(0, 8), region: r.region, ok: r.ok, status: r.status, amount: r.amount, msg: r.message });
|
|
56
|
+
} catch (e) {
|
|
57
|
+
results.push({ uid: String(a.uid).slice(0, 8), region: a.region, ok: false, status: "error", msg: String(e?.message || e).slice(0, 120) });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
61
|
+
const date = todayKey();
|
|
62
|
+
try {
|
|
63
|
+
stateMod.writeStateImmediate(stateMod.defaultStateFile(), {
|
|
64
|
+
qoderCheckin: { date, at: Date.now(), ok: okCount, total: results.length, accounts: results },
|
|
65
|
+
});
|
|
66
|
+
} catch {}
|
|
67
|
+
for (const r of results) emit("qoder-checkin-account", { ...r, reason });
|
|
68
|
+
emit("qoder-checkin-done", { date, ok: okCount, total: results.length, reason });
|
|
69
|
+
return { ok: okCount > 0, okCount, total: results.length };
|
|
70
|
+
} catch (e) {
|
|
71
|
+
emit("qoder-checkin-failed", { error: String(e?.message || e).slice(0, 200), reason });
|
|
72
|
+
return { ok: false, error: String(e?.message || e).slice(0, 200) };
|
|
73
|
+
} finally {
|
|
74
|
+
running = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const lastDate = stateMod.readState(stateMod.defaultStateFile())?.qoderCheckin?.date || "";
|
|
80
|
+
if (shouldCatchUp({ lastDate, hour })) {
|
|
81
|
+
setTimeout(() => { runOnce("catch-up").catch(() => {}); }, 90_000).unref?.();
|
|
82
|
+
console.log(`[qoder-checkin] catch-up scheduled (last=${lastDate || "never"})`);
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
function arm() {
|
|
86
|
+
const delay = nextRunDelayMs(new Date(), hour);
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
runOnce("daily").catch(() => {}).finally(arm);
|
|
89
|
+
}, delay).unref?.();
|
|
90
|
+
}
|
|
91
|
+
arm();
|
|
92
|
+
return { enabled: true, hour, runOnce };
|
|
93
|
+
}
|
|
@@ -123,10 +123,10 @@ export async function startServerLifecycle({ VERSION, token, created, upstream,
|
|
|
123
123
|
else if (r.ok) console.log(`[preheat] opencode models ok ${r.status} ${r.ms}ms`);
|
|
124
124
|
else console.log(`[preheat] opencode models failed ${r.error || r.status || ""} ${r.ms || 0}ms`);
|
|
125
125
|
}).catch(() => {});
|
|
126
|
-
//
|
|
126
|
+
// cline free 目录自检(独立于 opencode 连接预热:dispatcher.preheat 只做默认供应商)
|
|
127
127
|
// 见 .agents/notes/implemented/simplification/2026-09-16-preheat-opencode-only.md
|
|
128
128
|
try {
|
|
129
|
-
const cline = upstream?.byId?.get?.("
|
|
129
|
+
const cline = upstream?.byId?.get?.("cline");
|
|
130
130
|
if (cline?.checkFreeUpdates) void cline.checkFreeUpdates().catch(() => {});
|
|
131
131
|
} catch {}
|
|
132
132
|
}, 100).unref?.();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* traework 每日自动签到(daemon 内置),仿 src/runtime/workbuddy-checkin.js。
|
|
3
|
+
* 开关:MSLXDFF_TRAEWORK_CHECKIN=0 关闭(默认开);时间:MSLXDFF_TRAEWORK_CHECKIN_HOUR(默认 9 点)。
|
|
4
|
+
* 多账号全签 + 启动补签 + 每日定时。
|
|
5
|
+
*/
|
|
6
|
+
import { compatFetch } from "../compat.js";
|
|
7
|
+
import { todayKey, nextRunDelayMs, shouldCatchUp } from "./workbuddy-checkin.js";
|
|
8
|
+
|
|
9
|
+
export function isCheckinEnabled(env = process.env) {
|
|
10
|
+
const v = String(env.MSLXDFF_TRAEWORK_CHECKIN ?? "1").trim().toLowerCase();
|
|
11
|
+
return !["0", "false", "off", "no"].includes(v);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getCheckinHour(env = process.env) {
|
|
15
|
+
const h = Number(env.MSLXDFF_TRAEWORK_CHECKIN_HOUR);
|
|
16
|
+
return Number.isInteger(h) && h >= 0 && h <= 23 ? h : 9;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function setupTraeworkCheckin({ bus, logs } = {}) {
|
|
20
|
+
function emit(type, data = {}) {
|
|
21
|
+
const entry = { ts: Date.now(), type, ...data };
|
|
22
|
+
try { bus?.emit(entry); } catch {}
|
|
23
|
+
try { logs?.appendEvent?.(entry); } catch {}
|
|
24
|
+
try { console.log(`[traework-checkin] ${type} ${JSON.stringify(data)}`.slice(0, 500)); } catch {}
|
|
25
|
+
}
|
|
26
|
+
if (!isCheckinEnabled()) {
|
|
27
|
+
console.log("[traework-checkin] disabled (set MSLXDFF_TRAEWORK_CHECKIN=1 to enable)");
|
|
28
|
+
emit("traework-checkin-disabled");
|
|
29
|
+
return { enabled: false };
|
|
30
|
+
}
|
|
31
|
+
const hour = getCheckinHour();
|
|
32
|
+
console.log(`[traework-checkin] enabled: daily ${String(hour).padStart(2, "0")}:00 local`);
|
|
33
|
+
emit("traework-checkin-enabled", { hour });
|
|
34
|
+
|
|
35
|
+
const stateMod = await import("../state.js");
|
|
36
|
+
const fetchImpl = compatFetch;
|
|
37
|
+
|
|
38
|
+
let running = false;
|
|
39
|
+
async function runOnce(reason) {
|
|
40
|
+
if (running) return { skipped: "already-running" };
|
|
41
|
+
running = true;
|
|
42
|
+
try {
|
|
43
|
+
const cfg = stateMod.loadProviderConfigs().traework || {};
|
|
44
|
+
const keys = [...(Array.isArray(cfg.keys) ? cfg.keys : [])];
|
|
45
|
+
const authList = [...(Array.isArray(cfg.auths) ? cfg.auths : [])];
|
|
46
|
+
if (!keys.length || !authList.length) {
|
|
47
|
+
emit("traework-checkin-no-accounts", { reason });
|
|
48
|
+
return { ok: false, reason: "no-accounts" };
|
|
49
|
+
}
|
|
50
|
+
const { checkinStatus, checkinClaim } = await import("../providers/traework/checkin.js");
|
|
51
|
+
const results = await Promise.all(authList.map(async (auth, i) => {
|
|
52
|
+
const at = keys[i];
|
|
53
|
+
const uid = auth?.uid;
|
|
54
|
+
if (!uid || !at) return { uid, ok: false, msg: "missing key/uid" };
|
|
55
|
+
try {
|
|
56
|
+
const cred = { accessToken: at, deviceId: auth?.deviceId || "", uid };
|
|
57
|
+
const st = await checkinStatus({ cred, fetchImpl });
|
|
58
|
+
if (st.checkedIn) return { uid, ok: true, already: true, credits: st.credits };
|
|
59
|
+
if (!st.enable) return { uid, ok: false, msg: "checkin not enabled" };
|
|
60
|
+
await checkinClaim({ cred, fetchImpl });
|
|
61
|
+
const st2 = await checkinStatus({ cred, fetchImpl });
|
|
62
|
+
return { uid, ok: true, already: false, credits: st2.credits };
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return { uid, ok: false, msg: String(e?.message || e).slice(0, 120) };
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
const okCount = results.filter((r) => r.ok).length;
|
|
68
|
+
const date = todayKey();
|
|
69
|
+
try {
|
|
70
|
+
stateMod.writeStateImmediate(stateMod.defaultStateFile(), {
|
|
71
|
+
traeworkCheckin: { date, at: Date.now(), ok: okCount, total: results.length, accounts: results.map((r) => ({ uid: String(r.uid || "").slice(0, 8), ok: r.ok, already: !!r.already })) },
|
|
72
|
+
});
|
|
73
|
+
} catch {}
|
|
74
|
+
for (const r of results) {
|
|
75
|
+
emit("traework-checkin-account", { uid: String(r.uid || "").slice(0, 8), ok: r.ok, already: !!r.already, msg: String(r.msg || "").slice(0, 120), reason });
|
|
76
|
+
}
|
|
77
|
+
emit("traework-checkin-done", { date, ok: okCount, total: results.length, reason });
|
|
78
|
+
return { ok: okCount > 0, okCount, total: results.length };
|
|
79
|
+
} catch (e) {
|
|
80
|
+
emit("traework-checkin-failed", { error: String(e?.message || e).slice(0, 200), reason });
|
|
81
|
+
return { ok: false, error: String(e?.message || e).slice(0, 200) };
|
|
82
|
+
} finally {
|
|
83
|
+
running = false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 启动补签
|
|
88
|
+
try {
|
|
89
|
+
const lastDate = stateMod.readState(stateMod.defaultStateFile())?.traeworkCheckin?.date || "";
|
|
90
|
+
if (shouldCatchUp({ lastDate, hour })) {
|
|
91
|
+
setTimeout(() => { runOnce("catch-up").catch(() => {}); }, 60_000).unref?.();
|
|
92
|
+
console.log(`[traework-checkin] catch-up scheduled (last=${lastDate || "never"})`);
|
|
93
|
+
}
|
|
94
|
+
} catch {}
|
|
95
|
+
// 每日定时
|
|
96
|
+
function arm() {
|
|
97
|
+
const delay = nextRunDelayMs(new Date(), hour);
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
runOnce("daily").catch(() => {}).finally(arm);
|
|
100
|
+
}, delay).unref?.();
|
|
101
|
+
}
|
|
102
|
+
arm();
|
|
103
|
+
return { enabled: true, hour, runOnce };
|
|
104
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 幂等状态迁移:把遗留 id 的 cline 配置合并进 `cline` 并删除旧键(供应商 id 统一)。
|
|
3
|
+
* 见 .scratch/cline-unify/SPEC.md §4.5;语义:keys 去重 + 剔除 sk_ 旧式 key、allowlist 求并、baseUrl 收敛为根。
|
|
4
|
+
* 绝不触碰 modelPicks / modelErrors / modelLatencies(旧 clinebot/* 键交给既有 prune --orphans)。
|
|
5
|
+
*/
|
|
6
|
+
import { chmodSync, copyFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { defaultStateFile, readState, writeStateImmediate } from "../store.js";
|
|
8
|
+
import { normalizeAllowedModel, normalizeEndpointPath } from "../provider-config.js";
|
|
9
|
+
import { appendEvent } from "../../logs.js";
|
|
10
|
+
|
|
11
|
+
export const CLINE_DEFAULT_BASE_URL = "https://api.cline.bot";
|
|
12
|
+
export const LEGACY_CLINE_IDS = ["clinebot", "cline-bot"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* cline 的 chat URL = `baseUrl` + `loadProviderChatPath()`;而 loadProviderChatPath 缺省**恒**返回
|
|
16
|
+
* `/chat/completions`(src/providers/cline/index.js 里那个按 `/api/v1` 智能兜底的 defaultChat 是死代码,
|
|
17
|
+
* 永远轮不到它生效)。所以 baseUrl 必须自带 `/api/v1`,否则会拼成 `https://api.cline.bot/chat/completions`
|
|
18
|
+
* → 上游 404(2026-09-20 端到端实测)。只对 cline 官方域补全,自定义/代理 baseUrl 不做猜测。
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeClineBaseUrl(u) {
|
|
21
|
+
const s = String(u || "").trim().replace(/\/+$/, "");
|
|
22
|
+
if (!s) return `${CLINE_DEFAULT_BASE_URL}/api/v1`;
|
|
23
|
+
if (/\/api\/v1$/i.test(s)) return s;
|
|
24
|
+
if (/^https?:\/\/api\.cline\.bot$/i.test(s)) return `${s}/api/v1`;
|
|
25
|
+
return s;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function uniq(list) { return [...new Set(list)]; }
|
|
29
|
+
function strList(v) { return Array.isArray(v) ? v.map((x) => String(x || "").trim()).filter(Boolean) : []; }
|
|
30
|
+
// sk_ 旧式 API key(D3:cline 只认 refreshToken,数据层剔除)
|
|
31
|
+
function isLegacySkKey(k) { return /^sk[-_]/i.test(k); }
|
|
32
|
+
|
|
33
|
+
/** 合并后的 cline 配置:只重建 cline 自己的字段,不吸收旧 id 的 allowAnyModels(避免意外放宽安全默认) */
|
|
34
|
+
function mergeConfig(cl, bot) {
|
|
35
|
+
const next = {
|
|
36
|
+
baseUrl: normalizeClineBaseUrl(cl.baseUrl || bot.baseUrl),
|
|
37
|
+
keys: uniq([...strList(cl.keys), ...strList(bot.keys)]).filter((k) => !isLegacySkKey(k)),
|
|
38
|
+
allowedModels: uniq([...strList(cl.allowedModels), ...strList(bot.allowedModels)]
|
|
39
|
+
.map((m) => normalizeAllowedModel(m, "cline")).filter(Boolean)),
|
|
40
|
+
};
|
|
41
|
+
if (Array.isArray(cl.auths) && cl.auths.length) next.auths = cl.auths;
|
|
42
|
+
if (typeof cl.allowAnyModels === "boolean") next.allowAnyModels = cl.allowAnyModels;
|
|
43
|
+
const modelsPath = normalizeEndpointPath(cl.modelsPath || bot.modelsPath || "");
|
|
44
|
+
if (modelsPath) next.modelsPath = modelsPath;
|
|
45
|
+
const chatPath = normalizeEndpointPath(cl.chatPath || bot.chatPath || "");
|
|
46
|
+
if (chatPath) next.chatPath = chatPath;
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 备份到同目录 state.json.bak-<ISO时间戳>,权限收紧 0600(含凭据,绝不外泄) */
|
|
51
|
+
function backupState(file, now = new Date()) {
|
|
52
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
53
|
+
const target = `${file}.bak-${stamp}`;
|
|
54
|
+
copyFileSync(file, target);
|
|
55
|
+
try { chmodSync(target, 0o600); } catch {}
|
|
56
|
+
return target;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {{file?:string, dryRun?:boolean, backup?:boolean}} opts
|
|
61
|
+
* @returns {{applied:boolean, changed:boolean, reason?:string, before?:object, after?:object, backup?:string}}
|
|
62
|
+
* `applied` 仅在真正落盘时为 true;before/after 只含计数,绝不含凭据明文。
|
|
63
|
+
*/
|
|
64
|
+
export function clineUnifyMigration({ file = defaultStateFile(), dryRun = false, backup = true } = {}) {
|
|
65
|
+
const configs = readState(file).providerConfigs || {};
|
|
66
|
+
const legacyId = LEGACY_CLINE_IDS.find((pid) => configs[pid] && typeof configs[pid] === "object");
|
|
67
|
+
if (!legacyId) return { applied: false, changed: false, reason: "no legacy cline id" };
|
|
68
|
+
const bot = configs[legacyId];
|
|
69
|
+
const cl = configs.cline && typeof configs.cline === "object" ? configs.cline : {};
|
|
70
|
+
const next = mergeConfig(cl, bot);
|
|
71
|
+
const before = {
|
|
72
|
+
keys: strList(cl.keys).length + strList(bot.keys).length,
|
|
73
|
+
allowedModels: uniq([...strList(cl.allowedModels), ...strList(bot.allowedModels)]).length,
|
|
74
|
+
};
|
|
75
|
+
const after = { keys: next.keys.length, allowedModels: next.allowedModels.length };
|
|
76
|
+
if (dryRun) return { applied: false, changed: true, reason: "dry-run", before, after };
|
|
77
|
+
let backupPath = null;
|
|
78
|
+
if (backup) {
|
|
79
|
+
try { if (existsSync(file)) backupPath = backupState(file); } catch {}
|
|
80
|
+
}
|
|
81
|
+
const merged = { ...configs, cline: next };
|
|
82
|
+
for (const pid of LEGACY_CLINE_IDS) delete merged[pid];
|
|
83
|
+
writeStateImmediate(file, { providerConfigs: merged });
|
|
84
|
+
try {
|
|
85
|
+
appendEvent({
|
|
86
|
+
type: "cline-unify-migrated",
|
|
87
|
+
from: legacyId,
|
|
88
|
+
keys: after.keys,
|
|
89
|
+
allowedModels: after.allowedModels,
|
|
90
|
+
backup: backupPath ? backupPath.replace(/\\/g, "/").split("/").pop() : null,
|
|
91
|
+
});
|
|
92
|
+
} catch {}
|
|
93
|
+
return { applied: true, changed: true, reason: `merged ${legacyId}`, before, after, ...(backupPath ? { backup: backupPath } : {}) };
|
|
94
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 状态迁移运行器:按注册顺序执行(每个迁移自身幂等),供 daemon bootstrap
|
|
3
|
+
* (src/runtime/providers-setup.js,实例化任何 provider 之前)与 CLI 单点调用。
|
|
4
|
+
* 失败只收集不抛出:迁移不该拖垮 daemon 启动。
|
|
5
|
+
*/
|
|
6
|
+
import { defaultStateFile } from "./store.js";
|
|
7
|
+
import { clineUnifyMigration } from "./migrations/cline-unify.js";
|
|
8
|
+
|
|
9
|
+
export const STATE_MIGRATIONS = [
|
|
10
|
+
{ id: "cline-unify", run: (opts) => clineUnifyMigration(opts) },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {{file?:string, dryRun?:boolean, backup?:boolean}} opts
|
|
15
|
+
* @returns {Promise<{applied:string[], skipped:string[], errors:object[], details:object}>}
|
|
16
|
+
*/
|
|
17
|
+
export async function runStateMigrations({ file = defaultStateFile(), dryRun = false, backup = true } = {}) {
|
|
18
|
+
const applied = [];
|
|
19
|
+
const skipped = [];
|
|
20
|
+
const errors = [];
|
|
21
|
+
const details = {};
|
|
22
|
+
for (const m of STATE_MIGRATIONS) {
|
|
23
|
+
try {
|
|
24
|
+
const r = await m.run({ file, dryRun, backup });
|
|
25
|
+
details[m.id] = r;
|
|
26
|
+
if (r?.applied) applied.push(m.id);
|
|
27
|
+
else skipped.push(m.id);
|
|
28
|
+
} catch (err) {
|
|
29
|
+
errors.push({ id: m.id, error: String(err?.message || err) });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { applied, skipped, errors, details };
|
|
33
|
+
}
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync, existsSync, statSync, chmodSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { defaultStateFile, tokenFile, generateToken, readState, writeStateImmediate, getEntry } from "../store.js";
|
|
4
4
|
import { fmtShanghaiYMDHMS } from "../../time.js";
|
|
5
5
|
|
|
6
|
+
// 主 Bearer token 镜像落盘:0600 + 显式 chmod。
|
|
7
|
+
// 注意 writeFileSync 的 mode 只在“新建文件”时生效,已存在的文件权限不会被它改;
|
|
8
|
+
// 升级前生成的老文件会永远停在 0644(同机其他用户可读),所以必须再 chmod 一次兜住。
|
|
6
9
|
function syncTokenFile(token, file) {
|
|
7
10
|
try {
|
|
8
11
|
const tf = tokenFile(file);
|
|
9
12
|
mkdirSync(dirname(tf), { recursive: true });
|
|
10
|
-
writeFileSync(tf, String(token || "").trim() + "\n", "utf8");
|
|
13
|
+
writeFileSync(tf, String(token || "").trim() + "\n", { encoding: "utf8", mode: 0o600 });
|
|
14
|
+
try { chmodSync(tf, 0o600); } catch {}
|
|
11
15
|
} catch {}
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -45,8 +45,8 @@ export function getUseGroupKeysEnv() {
|
|
|
45
45
|
return parseBool(raw) === true;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// 取模型对应的供应商 head:兼容 canonical(workbuddy/xxx、
|
|
49
|
-
// dash 别名(
|
|
48
|
+
// 取模型对应的供应商 head:兼容 canonical(workbuddy/xxx、cline/xxx)、
|
|
49
|
+
// dash 别名(cline-xxx → 还原后取 head)、裸 id(归 opencode)。
|
|
50
50
|
// oc/ 别名归一到 opencode(与 splitModelId 一致:别名表小写 key,先小写再归一)。
|
|
51
51
|
export function providerHeadOf(model) {
|
|
52
52
|
let s = String(model || "").trim();
|
|
@@ -70,14 +70,14 @@ export function providerHeadOf(model) {
|
|
|
70
70
|
// 全局开关:off 则所有供应商都不走组员网络(via-route/hedge/peer/broadband 全禁),仅本机直连
|
|
71
71
|
// workbuddy/cline 系硬禁组员(ADR-0015 local-only):本机账号绑定(workbuddy auths/*.json + uid / cline refreshToken),
|
|
72
72
|
// 组员没有该账号转过去也用不了,且本地直连最快——无论全局开关一律仅本机直连。
|
|
73
|
-
// model 兼容 canonical(workbuddy/xxx、
|
|
73
|
+
// model 兼容 canonical(workbuddy/xxx、cline/xxx)与 dash(workbuddy-xxx、cline-xxx)两种形态。
|
|
74
74
|
export function isHardLocalOnly(model) {
|
|
75
75
|
const head = providerHeadOf(model);
|
|
76
76
|
if (!head) return false;
|
|
77
77
|
try {
|
|
78
78
|
return classifyProvider(head) === "local-only";
|
|
79
79
|
} catch {
|
|
80
|
-
return head === "workbuddy" || head === "cline"
|
|
80
|
+
return head === "workbuddy" || head === "cline";
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
package/src/sync-workbuddy.js
CHANGED
|
@@ -119,7 +119,7 @@ export async function syncToWorkbuddy({ id, token, port, file, keep } = {}) {
|
|
|
119
119
|
if (arr[idx].useCustomProtocol === undefined) arr[idx].useCustomProtocol = false;
|
|
120
120
|
action = "updated";
|
|
121
121
|
} else {
|
|
122
|
-
// 检查归一化 id 是否与已有条目冲突(如已存在
|
|
122
|
+
// 检查归一化 id 是否与已有条目冲突(如已存在 cline-z-ai-glm-5.3-flash 但不是本地 URL)
|
|
123
123
|
const conflict = arr.findIndex((m) => m && m.id === wbId);
|
|
124
124
|
if (conflict >= 0) {
|
|
125
125
|
// 已有同名条目且非我们写的,跳过不覆盖
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// 上游引擎选择(ADR-0017):sdk=AI SDK wire 层(缺省),legacy
|
|
1
|
+
// 上游引擎选择(ADR-0017):sdk=AI SDK wire 层(缺省),legacy=原实现(显式指定或 SDK 装载失败回退)。
|
|
2
2
|
// Note: 缺省即 sdk;chat 类走 /chat/completions(@ai-sdk/openai-compatible),
|
|
3
3
|
// responses 类(muse-spark*)走 /responses(@ai-sdk/openai);非流式与 anon 重试委派 legacy,
|
|
4
4
|
// SDK 不可用自动回退并告警一次 — 见 .scratch/ai-sdk-upstream/{SPEC.md,SPEC-p4-turnon.md,SPEC-p3-responses.md} 与 docs/adr/0017
|