mslxdff 0.1.173 → 0.1.174
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/docs/cli_help_mini.md +3 -3
- package/package.json +1 -1
- package/src/chat-pipeline/policy.js +10 -2
- package/src/claude-desktop/apply.js +42 -5
- package/src/claude-desktop/context1m.js +38 -0
- package/src/claude-desktop/slots.js +66 -0
- package/src/cli/commands/claude-desktop.js +27 -7
- package/src/cli/commands/sync.js +1 -1
- package/src/cli/help.js +1 -1
- package/src/opencode-identity.js +39 -19
package/docs/cli_help_mini.md
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
| DeepSeek 登录 | `-provider deepseek login --token <userToken>` 或 `login <email\|mobile> <password>` | DeepSeek 官网免费对话接入(ADR-0014):userToken 在 chat.deepseek.com F12→Local Storage;无参数打印图文引导;落盘后 `allowAny on` + `-restart`;模型 `deepseek/{chat,reasoner,chat-search,reasoner-search}`;单账号 1 路并发,多号轮换 |
|
|
60
60
|
| DeepSeek 探活 | `-provider deepseek health [--json]` | 逐账号体检(防禁言):检测禁言(自动冷却 5min)/限频前兆/凭据坏;网络失败不误伤;禁言解封后再探自动恢复 |
|
|
61
61
|
| 供应商改址 | `-provider <id> set-url <baseUrl>` | 改通用供应商地址 |
|
|
62
|
-
| 供应商改模型路径 | `-provider <id> set-models-path <path>` | 改 `models` 路径(如 `/v1/models`、`/
|
|
62
|
+
| 供应商改模型路径 | `-provider <id> set-models-path <path>` | 改 `models` 路径(如 `/v1/models`、`/v3/config`) |
|
|
63
63
|
| 供应商改对话路径 | `-provider <id> set-chat-path <path>` | 改 `chat` 路径(如 `/v1/chat/completions`、`/v2/chat/completions`) |
|
|
64
64
|
| 供应商清空 | `-provider <id> clear` | 清空该供应商 keys |
|
|
65
65
|
| 供应商共享 | key 随转发自动借出(ADR-0019,无开关无白名单) | 借道时自动附带;opencode/workbuddy/cline/codearts/traework 硬排除 |
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
| 千问办公接入 | `-provider qwenwork login` | 千问办公设备授权(PKCE+poll,`gateway.qwenwork.cn`):打印兑换链接→浏览器确认→落盘 `auths/qwenwork-<uid>.json`+state;**默认 `allowAnyModels=false` + 只种 `flash/pro/qwen3.8-max-preview`**(额度是账号积分池,防 auto 烧分);登录尾部显示套餐名+积分剩余;`qwenwork/<modelId>` 前缀路由,恒 local-only 不借出 |
|
|
76
76
|
| zcode 接入 | `-provider zcode login [--bigmodel]` | ZCode(智谱官方编程工作台)免费额度授权:打印 chat.z.ai 授权链接→浏览器登录→CLI 轮询(`oauth/cli/init`→`poll`)拿 JWT,落盘 `auths/zcode-<uid>.json`+state 并把内置目录(`GLM-5.3`/`GLM-5.3-Flash`/`GLM-5.2`/`GLM-5-Turbo`)并入 allowlist;`zcode/<modelId>` 前缀路由(Anthropic 网关,恒上游 stream,**免验证码**);`--bigmodel` 切 bigmodel.cn;恒 local-only 不借出 |
|
|
77
77
|
| zcode 额度 | `-provider zcode quota [--json]` | 查 ZCode 套餐名/状态/有效期 + 各模型余量(`billing/balance` 分组表格);空套餐给领取指引、401 给重登指引 |
|
|
78
|
-
| raccoon 接入 | `-provider raccoon login` | Raccoon(商汤小浣熊)扫码登录:生成 `qrcode_code`→终端二维码 + `login/mp?code=` 链接→**用微信「扫一扫」扫**并确认(浏览器直接打开链接会 404
|
|
78
|
+
| raccoon 接入 | `-provider raccoon login` | Raccoon(商汤小浣熊)扫码登录:生成 `qrcode_code`→终端二维码 + `login/mp?code=` 链接→**用微信「扫一扫」扫**并确认(浏览器直接打开链接会 404,站点 SPA 无该路由)→轮询拿 token,落盘 `auths/raccoon-<uid>.json`+state(`keys` 一律以文档为准,续期后自愈)并把兜底 6 模型并入 allowlist;`raccoon/<modelId>` 前缀路由(**OpenAI 兼容网关**,**免验证码**);多号自动轮换:请求内撞积分不足/限流就当场换下一个号,**暂不支持手动指定用哪个号**;积分不足长冷 1h、限流短冷 30s;恒 local-only 不借出 |
|
|
79
79
|
| raccoon 积分 | `-provider raccoon quota [--json]` / `checkin` | 逐号查积分五分项 / 领「桌面端登录奖励」——**这家没有每日签到**:每日 300 与注册礼包 3000 由服务端自动发(无端点,每日 300 当天 23:59:59 清零),唯一要领的登录奖励 3000 是**每号一次性**且 `login` 时已自动领掉,`checkin` 只是补领兜底;判据只认账单、不认上游 `code:0`(重复领会虚回成功);未登录给 login 指引;daemon 每日 09:00 兜底扫描未领过的号(`MSLXDFF_RACCOON_CHECKIN=0` 关,`_CHECKIN_HOUR` 改点) |
|
|
80
80
|
| WorkBuddy 成长任务 | `-workbuddy growth [--json]` / `-wb growth` | 成长任务全自动(拉列表→参与→触发→领奖,串行 1.2s/1s,已领幂等跳过);可自动 `chat_5`/`automation_1`/`skill_1`/`Model_chat_GLM5.2`,需客户端任务标 MANUAL 不发包;daemon 每日 09:30 自动(`MSLXDFF_WORKBUDDY_GROWTH=0` 关,`_GROWTH_HOUR`/`_GROWTH_MODEL` 可配) |
|
|
81
81
|
| WorkBuddy 猫猫旅行 | `-workbuddy travel [--json]` / `-wb travel` | 无猫自动同意协议+领养(+300,门槛未达自动补一次对话解锁);到站领奖 / 空闲派出(location 4)/ 旅行中跳过 |
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
| 同步 opencode | `-setto opencode [modelId\|--all]` | 把本地网关注册为 opencode 供应商(`provider.mslxdff`,`http://127.0.0.1:<port>/v1`,直写裸名如 `deepseek-v4-flash-free`,`/`→`-` 如 `bai/deepseek`→`bai-deepseek` 到 8989 自动还原,`--all` 批量同步全部 picks;picks 非空时摘除失效模型;自动附模型能力:models.dev 目录 + workbuddy 走上游原生字段,opencode 原生识别;写 variants 档位供 ctrl+t 切换(effort 型按档位写,toggle 型不写,TUI 改配置后需重启)) |
|
|
96
96
|
| 同步 chatgpt | `-setto chatgpt [modelId]` | 写 Codex 三端共用 `~/.codex/config.toml`(`model_providers.mslxdff` → `127.0.0.1/v1/responses`,鉴权走 `mslxdff -showtoken` 不落盘),换模型重跑 setto 或 `codex exec -m <id>` 单次覆盖,`codex exec "hi"` 验证;排障 `MSLXDFF_RESPONSES_DEBUG=1` 看 daemon.log `[responses]` |
|
|
97
97
|
| 同步 Claude Code | `-setto claude [modelId] [--behaves-as <id>]` | 写 Claude Code 用户设置 `~/.claude/settings.json`(`CLAUDE_CONFIG_DIR` 可改址):`env.ANTHROPIC_BASE_URL=http://127.0.0.1:<port>`(**不带 /v1**)+ 明文 `ANTHROPIC_AUTH_TOKEN` + `CLAUDE_CODE_ATTRIBUTION_HEADER=0`/`CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1`/`CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` + `model` + `modelPicker.options`;**不带 modelId = 写入 `modelPicks` 勾选集全集**(顶层 `model` 也只在勾选集内取,picks 空则报错退出,不兜底到可能已失效的 `preferredModel`;`--all` 与之同义);`behavesAs` 只给客户端不认识的 id(缺省 `claude-sonnet-5`,`claude-*` 行不写;实测无映射的未知 id 会被本机 Claude Code 直接拒跑)(ADR-0047 `/v1/messages` 外壳);只动这些键、其余键与键序保留,首写前一次性备份 `settings.pre-mslxdff.json`(不覆盖),JSON 解析失败拒写、写不进即报错不谎报;排障 `MSLXDFF_ANTHROPIC_DEBUG=1` 看 daemon.log `[messages]` |
|
|
98
|
-
| 接 Claude Desktop(3P) | `-setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--no-alias] [--check] [--official]`、`-claude-desktop status\\|slots\\|check` | 桌面端**不读** `ANTHROPIC_BASE_URL`/`settings.json`,只读自家 3P profile 且只认「三档角色 ID」→ 一次写两处:① `%LOCALAPPDATA%\Claude-3p\configLibrary\<uuid>.json` + `_meta.json`(uuid 由端口派生、只 upsert 自家条目、他人 entry 不动、损坏拒写、幂等按**原文 vs 原文**、明文 token `0600`、baseUrl **不带 /v1**、`authScheme=bearer`(本仓 `authorized()` 只认 Bearer)、`inferenceModels[{name:角色槽,labelOverride:真模型名}]`)② `model-aliases.json` 写「角色槽 → 真模型」(入站 `src/chat-pipeline/policy.js` 生效、`getModelAlias` 未命中即重读 → **不重启 daemon**);槽位上限 12(取 App 签名 `model-catalog`,前三档 sonnet/opus/haiku,Haiku 供子 agent),缺 alias 时 App 探活 30–90 s 后 502;`--official` 摘登记不删配置;不写 `preferredModel`、不碰 `claude_desktop_config.json` 与注册表策略;⚠ **App 不热重载**,写完须完全退出重开并选 3P 入口(免 claude.ai 登录)(ADR-0048) |
|
|
98
|
+
| 接 Claude Desktop(3P) | `-setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official]`、`-claude-desktop status\\|slots\\|check` | 桌面端**不读** `ANTHROPIC_BASE_URL`/`settings.json`,只读自家 3P profile 且只认「三档角色 ID」→ 一次写两处:① `%LOCALAPPDATA%\Claude-3p\configLibrary\<uuid>.json` + `_meta.json`(uuid 由端口派生、只 upsert 自家条目、他人 entry 不动、损坏拒写、幂等按**原文 vs 原文**、明文 token `0600`、baseUrl **不带 /v1**、`authScheme=bearer`(本仓 `authorized()` 只认 Bearer)、`inferenceModels[{name:角色槽,labelOverride:真模型名}]`)② `model-aliases.json` 写「角色槽 → 真模型」(入站 `src/chat-pipeline/policy.js` 生效、`getModelAlias` 未命中即重读 → **不重启 daemon**);槽位上限 12(取 App 签名 `model-catalog`,前三档 sonnet/opus/haiku,Haiku 供子 agent),缺 alias 时 App 探活 30–90 s 后 502;`--1m` 打标(ADR-0056,缺省 auto:证据=本机 `/v1/models` 已裁决窗口 ≥1M → 打官方成对键 `supports1m+prefer1m`=菜单单条默认 1M,不打标被 App 按 200K 封顶;网关不可达 ℹ 降级不阻塞写盘;`off`=历史三键、`all`=担保全打、列表=只打命中行);`--official` 摘登记不删配置;不写 `preferredModel`、不碰 `claude_desktop_config.json` 与注册表策略;⚠ **App 不热重载**,写完须完全退出重开并选 3P 入口(免 claude.ai 登录)(ADR-0048) |
|
|
99
99
|
| 建组 | `-creategroup <name>` / `-group create <name>` | 建组,本机为 leader |
|
|
100
100
|
| 加组 | `-addtogroup <host> <name> [--broadband]` | 加远端组,broadband 走中继(不占端口纯出站);**零参数/单参数进手机宽带向导**:问组长地址+组名,自动起服务,回报出口 IP |
|
|
101
101
|
| 组同步 | `-group sync` | 刷新全组成员 |
|
package/package.json
CHANGED
|
@@ -18,7 +18,15 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
|
|
|
18
18
|
const rawModel = body.model || "";
|
|
19
19
|
|
|
20
20
|
let normalizedRequested = normalizeModel(lockModel || rawModel || "");
|
|
21
|
-
|
|
21
|
+
// [1m] 后缀回退(Claude Desktop 1M 条目 / Claude Code 1M 记法,ADR-0056):alias 直查未命中时
|
|
22
|
+
// 剥去尾部 [1m] 再查一次。仅用于本次查找——不回写、不注册 alias,未命中路径逐字不变(design D2)。
|
|
23
|
+
// 两个 alias 查找点(首轮 + mslxdff/ 前缀二段)共用同一回退,否则 `mslxdff/<槽位>[1m]` 不等价(评审 1 P1)。
|
|
24
|
+
const resolveAlias = (id) => {
|
|
25
|
+
const hit = getModelAlias(id);
|
|
26
|
+
if (hit) return hit;
|
|
27
|
+
return /\[1m\]$/i.test(id) ? getModelAlias(id.replace(/\[1m\]$/i, "")) : null;
|
|
28
|
+
};
|
|
29
|
+
let aliasResolved = resolveAlias(normalizedRequested);
|
|
22
30
|
if (aliasResolved) {
|
|
23
31
|
normalizedRequested = aliasResolved;
|
|
24
32
|
}
|
|
@@ -29,7 +37,7 @@ export function analyzePolicy({ headers = {}, body = {} } = {}) {
|
|
|
29
37
|
const rawPart = requested.slice("mslxdff/".length);
|
|
30
38
|
aliasInfo = `${requested} -> ${rawPart} (mslxdff provider stripped)`;
|
|
31
39
|
requested = rawPart;
|
|
32
|
-
const alias2 =
|
|
40
|
+
const alias2 = resolveAlias(requested);
|
|
33
41
|
if (alias2) {
|
|
34
42
|
aliasInfo = `${rawModel} -> ${alias2} (mslxdff + alias)`;
|
|
35
43
|
requested = alias2;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeModel } from "../reasoning.js";
|
|
2
2
|
import { registerModelAlias, persistModelAliases, loadModelAliases, modelAliasFile } from "../providers/model-id.js";
|
|
3
|
-
import { planSlots, aliasPairs, ROLE_SLOTS } from "./slots.js";
|
|
4
|
-
import {
|
|
3
|
+
import { planSlots, aliasPairs, ROLE_SLOTS, parseOneMMode, decideRows1m } from "./slots.js";
|
|
4
|
+
import { oneMContexts } from "./context1m.js";
|
|
5
|
+
import { syncToClaudeDesktop, retireClaudeDesktopProfile, claudeDesktopConfigDir, readClaudeDesktopProfile } from "../sync-claude-desktop.js";
|
|
5
6
|
import { detectManagedPolicy, currentAliases, probeMessages } from "./doctor.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -10,11 +11,37 @@ import { detectManagedPolicy, currentAliases, probeMessages } from "./doctor.js"
|
|
|
10
11
|
* ① 写 Claude Desktop 的 3P profile(`inferenceModels` 用**角色槽 id**,`labelOverride` 标真模型名);
|
|
11
12
|
* ② 写网关自己的 alias(角色槽 → 真模型),让桌面端发来的槽位请求在入站就被翻译成能跑的真模型。
|
|
12
13
|
* 少了 ②,桌面端每次请求都会被当未知模型丢组员转发 → 30~90 s 后 502(App 里就表现为 Test connection 红点)。
|
|
14
|
+
* ③ `--1m` 上下文打标(ADR-0056,Claude Desktop 专用):auto 问本机 /v1/models 拿已裁决窗口,
|
|
15
|
+
* 证据缺位时**保留盘上既有标**(无证据≠负面证据)、**绝不阻塞写盘**;all/列表为用户担保;
|
|
16
|
+
* 打标行经 toInferenceModels 写成对官方键 `supports1m+prefer1m`。evidence 参数是测试接缝。
|
|
13
17
|
*/
|
|
14
|
-
export async function applyClaudeDesktop({ picks = [], preferred = "", port = 8989, token = "", max, maxEffort, noAlias = false, dir, probe = false, probeTimeoutMs = 90000 } = {}) {
|
|
18
|
+
export async function applyClaudeDesktop({ picks = [], preferred = "", port = 8989, token = "", max, maxEffort, oneM = "auto", evidence, noAlias = false, dir, probe = false, probeTimeoutMs = 90000 } = {}) {
|
|
15
19
|
const planned = planSlots({ picks, preferred, max });
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
let rows = planned.rows.map((r) => ({ ...r, model: normalizeModel(r.model) || r.model }));
|
|
21
|
+
|
|
22
|
+
const oneMMode = oneM && typeof oneM === "object" ? oneM : parseOneMMode(oneM);
|
|
23
|
+
if (oneMMode.mode === "invalid") throw new Error("--1m 取值非法:只接受 off | auto | all | <逗号分隔槽位/模型id>");
|
|
24
|
+
const desktopDir = dir || claudeDesktopConfigDir();
|
|
25
|
+
let ev = evidence;
|
|
26
|
+
let evidenceQueried = false;
|
|
27
|
+
if (oneMMode.mode === "auto" && ev === undefined) {
|
|
28
|
+
evidenceQueried = true;
|
|
29
|
+
ev = await oneMContexts({ port, token });
|
|
30
|
+
}
|
|
31
|
+
// auto 的保留集:证据缺位(整体失败或该行不在目录)时沿用盘上既有 1M 标——网关停机的一轮
|
|
32
|
+
// 不得把上次写好的标静默抹掉(评审 1 P1-2;读盘失败自然降级为空表=与旧行为一致,不新增失败模式)
|
|
33
|
+
let preserve = null;
|
|
34
|
+
if (oneMMode.mode === "auto") {
|
|
35
|
+
preserve = new Map();
|
|
36
|
+
const existing = readClaudeDesktopProfile({ dir: desktopDir });
|
|
37
|
+
for (const m of Array.isArray(existing.profile?.inferenceModels) ? existing.profile.inferenceModels : []) {
|
|
38
|
+
if (m && typeof m === "object" && m.supports1m === true && m.name) preserve.set(String(m.name).toLowerCase(), true);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const decided = decideRows1m({ rows, oneM: oneMMode, contexts: ev && ev.ok ? ev.contexts : null, preserve });
|
|
42
|
+
rows = decided.rows;
|
|
43
|
+
|
|
44
|
+
const written = syncToClaudeDesktop({ port, token, rows, maxEffort, dir: desktopDir });
|
|
18
45
|
|
|
19
46
|
const out = {
|
|
20
47
|
dir: written.dir,
|
|
@@ -35,6 +62,16 @@ export async function applyClaudeDesktop({ picks = [], preferred = "", port = 89
|
|
|
35
62
|
aliasSkipped: noAlias,
|
|
36
63
|
managed: detectManagedPolicy(),
|
|
37
64
|
probes: [],
|
|
65
|
+
oneM: {
|
|
66
|
+
mode: oneMMode.mode,
|
|
67
|
+
list: oneMMode.list || [],
|
|
68
|
+
evidenceQueried,
|
|
69
|
+
evidenceOk: oneMMode.mode === "auto" ? Boolean(ev && ev.ok) : null,
|
|
70
|
+
evidenceReason: oneMMode.mode === "auto" && ev && !ev.ok ? String(ev.reason || "") : "",
|
|
71
|
+
markedCount: rows.filter((r) => r.supports1m).length,
|
|
72
|
+
preserved: decided.preserved,
|
|
73
|
+
unmatched: decided.unmatched,
|
|
74
|
+
},
|
|
38
75
|
};
|
|
39
76
|
|
|
40
77
|
if (!noAlias) {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// -setto claude-desktop 的 1M 证据 Adapter(design D1/D3、ADR-0056):一次 GET 本机网关 /v1/models。
|
|
2
|
+
// 真相源唯一 = ADR-0054 四级解析后的已裁决值(双层同源同值;顶层 context_length 缺/非正才回落
|
|
3
|
+
// capabilities.context 作保险,CLI 进程不复刻聚合、不另起第二证据链)。
|
|
4
|
+
// 失败一律 {ok:false,reason} 不抛:写盘职责与网关可达性解耦(spec《证据获取失败不阻塞写盘》)。
|
|
5
|
+
import { compatFetch, timeoutSignal } from "../compat.js";
|
|
6
|
+
|
|
7
|
+
function positiveInt(v) {
|
|
8
|
+
const n = Number(v);
|
|
9
|
+
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{port:number|string, token?:string, fetchImpl?:Function, timeoutMs?:number}} p
|
|
14
|
+
* @returns {Promise<{ok:true, contexts:Map<string,number>} | {ok:false, reason:string}>}
|
|
15
|
+
*/
|
|
16
|
+
export async function oneMContexts({ port, token = "", fetchImpl = compatFetch, timeoutMs = 3000 } = {}) {
|
|
17
|
+
const url = `http://127.0.0.1:${Number(port) || 8989}/v1/models`;
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetchImpl(url, {
|
|
20
|
+
headers: { Accept: "application/json", Authorization: `Bearer ${token}` },
|
|
21
|
+
signal: timeoutSignal(timeoutMs),
|
|
22
|
+
});
|
|
23
|
+
if (!res?.ok) return { ok: false, reason: `HTTP ${res?.status ?? "?"}` };
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
const list = Array.isArray(data?.data) ? data.data : null;
|
|
26
|
+
if (!list) return { ok: false, reason: "响应形状异常:缺 data[]" };
|
|
27
|
+
const contexts = new Map();
|
|
28
|
+
for (const e of list) {
|
|
29
|
+
const id = String(e?.id || "").trim().toLowerCase();
|
|
30
|
+
if (!id) continue;
|
|
31
|
+
const n = positiveInt(e.context_length) ?? positiveInt(e?.capabilities?.context);
|
|
32
|
+
if (n) contexts.set(id, n);
|
|
33
|
+
}
|
|
34
|
+
return { ok: true, contexts };
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return { ok: false, reason: String(err?.message || err) };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -76,11 +76,77 @@ export function aliasPairs(rows = []) {
|
|
|
76
76
|
* 桌面端 profile 的 `inferenceModels` 形状(官方 models 页:条目可为 id 字符串或对象,
|
|
77
77
|
* 对象里 `name` 是发给 provider 的真 id、`labelOverride` 是菜单显示名)。
|
|
78
78
|
* 显示名带上真模型名,用户在 App 下拉里能认出「这条其实是 qwenwork/pro」。
|
|
79
|
+
* `supports1m===true` 的行(--1m 裁决产物,decideRows1m 打标)追加官方成对键
|
|
80
|
+
* `supports1m+prefer1m`(= 菜单单条、新会话直接 1M 窗口,ADR-0056);
|
|
81
|
+
* 键序恒为 name→labelOverride→supports1m→prefer1m→maxEffort —— 幂等按原文比对,键序必须稳定。
|
|
79
82
|
*/
|
|
80
83
|
export function toInferenceModels(rows = [], { maxEffort } = {}) {
|
|
81
84
|
return (Array.isArray(rows) ? rows : []).map((r) => {
|
|
82
85
|
const e = { name: String(r.slot), labelOverride: String(r.label || r.model) };
|
|
86
|
+
if (r.supports1m === true) { e.supports1m = true; e.prefer1m = true; }
|
|
83
87
|
if (maxEffort) e.maxEffort = String(maxEffort);
|
|
84
88
|
return e;
|
|
85
89
|
});
|
|
86
90
|
}
|
|
91
|
+
|
|
92
|
+
/** `--1m` auto 模式阈值:/v1/models 已裁决窗口 ≥ 此值才打标(ADR-0056)。 */
|
|
93
|
+
export const ONE_M_THRESHOLD = 1_000_000;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `-setto claude-desktop --1m <值>` 的纯函数解析:
|
|
97
|
+
* `off|auto|all` 保留字(大小写不敏感)、空值缺省 auto;其余非空值一律按列表处理(单条即长度 1)——
|
|
98
|
+
* 列表值不与行集做存在性预校验,未命中经 decideRows1m 的 unmatched 以 ⚠ 呈现而非硬拒(spec《--1m 参数解析》)。
|
|
99
|
+
* 仅 trim 后为空/纯分隔符(`,,`)判 invalid,CLI 须在写盘前拒绝。
|
|
100
|
+
*/
|
|
101
|
+
export function parseOneMMode(raw) {
|
|
102
|
+
const s = String(raw ?? "").trim();
|
|
103
|
+
const low = s.toLowerCase();
|
|
104
|
+
if (s === "" || low === "auto") return { mode: "auto", list: [] };
|
|
105
|
+
if (low === "off") return { mode: "off", list: [] };
|
|
106
|
+
if (low === "all") return { mode: "all", list: [] };
|
|
107
|
+
const list = s.split(",").map((x) => x.trim().toLowerCase()).filter(Boolean);
|
|
108
|
+
if (!list.length) return { mode: "invalid" };
|
|
109
|
+
return { mode: "list", list };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* --1m 裁决(纯函数,不改入参):rows → { rows, unmatched, preserved }。
|
|
114
|
+
* `oneM` 为 parseOneMMode 产物;`contexts` 为证据 Adapter 的 Map<小写 id, 窗口数>;
|
|
115
|
+
* `preserve` 为盘上既有 1M 标 Map<小写槽位, true>。auto 语义:有证据且 ≥1M 才新增标、
|
|
116
|
+
* 有证据且 <1M 才清标;**无证据(整体失败或该行不在目录)≠ 负面证据 —— 保留既有标**,
|
|
117
|
+
* 网关停机的一轮不得静默抹掉上次写好的(评审 1 P1-2)。
|
|
118
|
+
*/
|
|
119
|
+
export function decideRows1m({ rows = [], oneM = { mode: "off", list: [] }, contexts = null, preserve = null } = {}) {
|
|
120
|
+
const mode = oneM && oneM.mode ? oneM.mode : "off";
|
|
121
|
+
const list = (Array.isArray(oneM && oneM.list) ? oneM.list : []).map((x) => String(x).toLowerCase());
|
|
122
|
+
const matched = new Set();
|
|
123
|
+
const preserved = [];
|
|
124
|
+
const out = (Array.isArray(rows) ? rows : []).map((r) => {
|
|
125
|
+
let mark = false;
|
|
126
|
+
if (mode === "all") {
|
|
127
|
+
mark = true;
|
|
128
|
+
} else if (mode === "list") {
|
|
129
|
+
const slot = String(r?.slot || "").toLowerCase();
|
|
130
|
+
const model = String(r?.model || "").toLowerCase();
|
|
131
|
+
const hitSlot = slot && list.includes(slot);
|
|
132
|
+
const hitModel = model && list.includes(model);
|
|
133
|
+
if (hitSlot || hitModel) {
|
|
134
|
+
mark = true;
|
|
135
|
+
if (hitSlot) matched.add(slot);
|
|
136
|
+
if (hitModel) matched.add(model);
|
|
137
|
+
}
|
|
138
|
+
} else if (mode === "auto") {
|
|
139
|
+
const key = String(r?.model || "").toLowerCase();
|
|
140
|
+
const n = contexts ? contexts.get(key) : undefined; // 短路写 `&&` 会把整体失败喂成 null → 误走「清标」分支(全量测试抓到)
|
|
141
|
+
if (n === undefined) {
|
|
142
|
+
mark = Boolean(preserve && preserve.get(String(r?.slot || "").toLowerCase()));
|
|
143
|
+
if (mark) preserved.push(String(r?.slot || "").toLowerCase());
|
|
144
|
+
} else {
|
|
145
|
+
mark = n >= ONE_M_THRESHOLD;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return mark ? { ...r, supports1m: true } : { ...r };
|
|
149
|
+
});
|
|
150
|
+
const unmatched = mode === "list" ? list.filter((v) => !matched.has(v)) : [];
|
|
151
|
+
return { rows: out, unmatched, preserved };
|
|
152
|
+
}
|
|
@@ -2,7 +2,7 @@ import { loadToken, loadModelPicks, loadPreferredModel } from "../../state.js";
|
|
|
2
2
|
import { getPreferredModel } from "../../auto.js";
|
|
3
3
|
import { effectivePort } from "../policy.js";
|
|
4
4
|
import { claudeDesktopConfigDir, readClaudeDesktopProfile } from "../../sync-claude-desktop.js";
|
|
5
|
-
import { ROLE_SLOTS, planSlots, slotRole } from "../../claude-desktop/slots.js";
|
|
5
|
+
import { ROLE_SLOTS, planSlots, slotRole, parseOneMMode } from "../../claude-desktop/slots.js";
|
|
6
6
|
import { detectManagedPolicy, readAppCatalogIds, appDataDirOf, currentAliases, probeMessages } from "../../claude-desktop/doctor.js";
|
|
7
7
|
import { normalizeModel } from "../../reasoning.js";
|
|
8
8
|
import { applyClaudeDesktop, retireClaudeDesktop } from "../../claude-desktop/apply.js";
|
|
@@ -57,7 +57,7 @@ export async function handleClaudeDesktop(args = [], flag = "-claude-desktop") {
|
|
|
57
57
|
const name = typeof m === "string" ? m : String(m?.name || "");
|
|
58
58
|
const label = typeof m === "string" ? "" : String(m?.labelOverride || "");
|
|
59
59
|
const target = aliases[name];
|
|
60
|
-
const mark = !isSlot(name) ? "⚠ 非角色槽(App 可能拒)" : target ? `→ ${target}` : "⚠ 无 alias(请求会被当未知模型丢组员转发→超时 502)";
|
|
60
|
+
const mark = (!isSlot(name) ? "⚠ 非角色槽(App 可能拒)" : target ? `→ ${target}` : "⚠ 无 alias(请求会被当未知模型丢组员转发→超时 502)") + (typeof m !== "string" && m?.supports1m === true ? " 1M" : "");
|
|
61
61
|
console.log(` ${name.padEnd(28)} ${slotRole(name).padEnd(7)} ${label ? `[${label}]`.padEnd(30) : ""}${mark}`);
|
|
62
62
|
}
|
|
63
63
|
const missing = models.map((m) => (typeof m === "string" ? m : String(m?.name || ""))).filter((n) => n && !aliases[n]);
|
|
@@ -151,7 +151,7 @@ function printUsage() {
|
|
|
151
151
|
mslxdff -claude-desktop status 看 profile / 托管策略 / 槽位 alias 是否齐(只读盘)
|
|
152
152
|
mslxdff -claude-desktop slots 看 App 签名模型目录认得哪些槽位 id(内置清单与实况比对)
|
|
153
153
|
mslxdff -claude-desktop check [id...] 逐槽向本机 /v1/messages 探活(等价 App 那个 Test connection,但给原因)
|
|
154
|
-
写配置: mslxdff -setto claude-desktop [modelId ...] [--max N] [--no-alias] [--official]`);
|
|
154
|
+
写配置: mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official]`);
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
/**
|
|
@@ -166,11 +166,16 @@ export async function handleClaudeDesktopSetto(args = [], idx = -1) {
|
|
|
166
166
|
if (missing) bad(`${missing} 需要取值`);
|
|
167
167
|
const maxRaw = values["--max"];
|
|
168
168
|
const effortRaw = values["--max-effort"];
|
|
169
|
-
const unknown = flags.filter((f) => !["--max", "--max-effort", "--no-alias", "--check", "--official", "--port"].includes(f));
|
|
170
|
-
if (unknown.length) bad(`未知参数: ${unknown.join(" ")}(可用:--max N / --max-effort <lvl> / --no-alias / --check / --official)`);
|
|
169
|
+
const unknown = flags.filter((f) => !["--max", "--max-effort", "--1m", "--no-alias", "--check", "--official", "--port"].includes(f));
|
|
170
|
+
if (unknown.length) bad(`未知参数: ${unknown.join(" ")}(可用:--max N / --max-effort <lvl> / --1m off|auto|all|列表 / --no-alias / --check / --official)`);
|
|
171
171
|
if (maxRaw !== undefined && !(Number(maxRaw) > 0)) bad("--max 需要一个正整数(要写几条槽位),例如 --max 3");
|
|
172
172
|
const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
173
173
|
if (effortRaw !== undefined && !EFFORTS.includes(String(effortRaw))) bad(`--max-effort 只能是 ${EFFORTS.join(" | ")}`);
|
|
174
|
+
// --1m 上下文标记(ADR-0056):不传=auto;显式传空值与「不传」区分、写盘前拒(评审 2 P1#1);invalid=纯分隔符值
|
|
175
|
+
const oneMRaw = values["--1m"];
|
|
176
|
+
if (oneMRaw !== undefined && !String(oneMRaw).trim()) bad("--1m 取值为空:不传该参数即缺省 auto;或显式传 off | auto | all | <逗号分隔槽位/模型id>");
|
|
177
|
+
const oneMParsed = parseOneMMode(oneMRaw);
|
|
178
|
+
if (oneMParsed.mode === "invalid") bad("--1m 取值非法:只接受 off | auto | all | <逗号分隔槽位/模型id>");
|
|
174
179
|
|
|
175
180
|
const port = effectivePort(args);
|
|
176
181
|
let token = "";
|
|
@@ -208,6 +213,7 @@ export async function handleClaudeDesktopSetto(args = [], idx = -1) {
|
|
|
208
213
|
token,
|
|
209
214
|
max: maxRaw !== undefined ? Number(maxRaw) : undefined,
|
|
210
215
|
maxEffort: effortRaw !== undefined ? String(effortRaw) : undefined,
|
|
216
|
+
oneM: oneMParsed,
|
|
211
217
|
noAlias: flags.includes("--no-alias"),
|
|
212
218
|
probe: flags.includes("--check"),
|
|
213
219
|
});
|
|
@@ -221,8 +227,9 @@ export async function handleClaudeDesktopSetto(args = [], idx = -1) {
|
|
|
221
227
|
console.log(` 登记: ${r.metaFile} appliedId=${r.configId}`);
|
|
222
228
|
console.log(` baseUrl: http://127.0.0.1:${port} (App 自己拼 /v1/messages) scheme=bearer`);
|
|
223
229
|
console.log(` inferenceModels: ${r.rows.length} 行(槽位 → 真模型)`);
|
|
224
|
-
for (const row of r.rows) console.log(` ${row.slot.padEnd(28)} ${slotRole(row.slot).padEnd(7)} → ${row.model}`);
|
|
230
|
+
for (const row of r.rows) console.log(` ${row.slot.padEnd(28)} ${slotRole(row.slot).padEnd(7)} → ${row.model}${row.supports1m ? " 1M" : ""}`);
|
|
225
231
|
if (r.overflow) console.log(` ⚠ 勾选集 ${r.rows.length + r.overflow} 条 > 槽位上限 ${r.slotCap},已截断 ${r.overflow} 条(${r.dropped.slice(0, 3).join(", ")}${r.dropped.length > 3 ? " …" : ""});想换哪条上槽用 -setto claude-desktop <modelId> ...`);
|
|
232
|
+
for (const line of oneMOutputLines(r.oneM, r.rows.length)) console.log(line);
|
|
226
233
|
if (!r.aliasSkipped) {
|
|
227
234
|
console.log(` alias: ${r.aliasAdded.length} 条新增/确认、${r.aliasOverwritten.length} 条覆盖 → ${r.aliasFile}(daemon 热读,不用重启)`);
|
|
228
235
|
for (const a of r.aliasOverwritten) console.log(` ⚠ 覆盖 ${a.slot}: ${a.from} → ${a.to}`);
|
|
@@ -250,7 +257,7 @@ export function splitArgs(rest = []) {
|
|
|
250
257
|
let missing = "";
|
|
251
258
|
for (let i = 0; i < rest.length; i++) {
|
|
252
259
|
const a = String(rest[i]);
|
|
253
|
-
if (a === "--max" || a === "--max-effort" || a === "--port") {
|
|
260
|
+
if (a === "--max" || a === "--max-effort" || a === "--port" || a === "--1m") {
|
|
254
261
|
flags.push(a);
|
|
255
262
|
const v = rest[++i];
|
|
256
263
|
if (v === undefined || String(v).startsWith("-")) { missing = a; break; }
|
|
@@ -262,3 +269,16 @@ export function splitArgs(rest = []) {
|
|
|
262
269
|
}
|
|
263
270
|
return { positional, flags, values, missing };
|
|
264
271
|
}
|
|
272
|
+
|
|
273
|
+
/** --1m 输出行(纯函数=可单测,spec《写入与 status 的 1M 呈现》+ 评审 1 P1-2 保留标披露)。 */
|
|
274
|
+
export function oneMOutputLines(oneM, rowsCount) {
|
|
275
|
+
if (!oneM || oneM.mode === "off") return [];
|
|
276
|
+
const kept = oneM.preserved && oneM.preserved.length ? `;已保留盘上 ${oneM.preserved.length} 条既有 1M 标(无证据≠负面证据)` : "";
|
|
277
|
+
if (oneM.mode === "auto" && oneM.evidenceOk) return [` 1M: ${oneM.markedCount}/${rowsCount} 条打标(证据:本机 /v1/models 已裁决窗口 ≥1M)${oneM.preserved && oneM.preserved.length ? `,其中 ${oneM.preserved.length} 条为保留` : ""}`];
|
|
278
|
+
if (oneM.mode === "auto") return [` ℹ 网关证据不可用${oneM.evidenceReason ? `(${oneM.evidenceReason})` : ""},本轮未新增打标 —— 先 mslxdff -d 起网关重跑,或 --1m all 用户担保打标${kept}`];
|
|
279
|
+
if (oneM.mode === "all") return [` 1M: 全 ${oneM.markedCount} 条打标(用户担保,未查证据)`];
|
|
280
|
+
const lines = [];
|
|
281
|
+
if (oneM.mode === "list" && oneM.markedCount) lines.push(` 1M: ${oneM.markedCount} 条打标(列表模式,不查窗口证据)`);
|
|
282
|
+
if (oneM.mode === "list" && oneM.unmatched && oneM.unmatched.length) lines.push(` ⚠ --1m 列表值未命中任何行: ${oneM.unmatched.join(", ")}`);
|
|
283
|
+
return lines;
|
|
284
|
+
}
|
package/src/cli/commands/sync.js
CHANGED
|
@@ -22,7 +22,7 @@ export async function handleSetto(args) {
|
|
|
22
22
|
const idx = args.findIndex((x) => x === "-setto" || x === "--setto");
|
|
23
23
|
const target = args[idx + 1];
|
|
24
24
|
if (!["workbuddy", "opencode", "chatgpt", "codex", "claude", "claude-desktop", "claudedesktop"].includes(target)) {
|
|
25
|
-
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias)");
|
|
25
|
+
console.error("usage: mslxdff -setto workbuddy [modelId] | mslxdff -setto opencode [modelId|--all] | mslxdff -setto chatgpt [modelId] | mslxdff -setto claude [modelId] [--behaves-as <id>] | mslxdff -setto claude-desktop [modelId ...] [--max N] [--max-effort <lvl>] [--1m <off|auto|all|列表>] [--no-alias] [--check] [--official] (claude: 不带 modelId = 写入 modelPicks 勾选集全集,--all 与之同义 / claude-desktop: 写 Claude Desktop 3P profile + 角色槽 alias,--1m 缺省 auto=有据才打 1M 标,详见 ADR-0056)");
|
|
26
26
|
process.exit(1);
|
|
27
27
|
}
|
|
28
28
|
if (target === "claude-desktop" || target === "claudedesktop") {
|
package/src/cli/help.js
CHANGED
|
@@ -25,7 +25,7 @@ Usage:
|
|
|
25
25
|
mslxdff -setto workbuddy [modelId] set default model and sync to WorkBuddy models.json (insert or update 127.0.0.1/v1 entry)
|
|
26
26
|
mslxdff -setto chatgpt [modelId] set default model and sync to Codex/ChatGPT ~/.codex/config.toml (model_providers.mslxdff → 127.0.0.1/v1/responses, auth via mslxdff -showtoken)
|
|
27
27
|
mslxdff -setto claude [modelId] [--behaves-as <id>] set default model and sync to Claude Code ~/.claude/settings.json (no modelId = write ALL modelPicks rows, --all is the same thing; env.ANTHROPIC_BASE_URL → 127.0.0.1 without /v1, /v1/messages shell; modelPicker rows carry behavesAs only for ids the client cannot recognize, default claude-sonnet-5; first write backs up settings.pre-mslxdff.json)
|
|
28
|
-
mslxdff -setto claude-desktop [modelId...] [--max N] [--max-effort <lvl>] [--no-alias] [--check] [--official] one-click Claude Desktop on 3P (ADR-0048): writes the app's configLibrary profile (role-slot model ids + labelOverride, baseUrl without /v1, bearer scheme) AND model-aliases.json (role slot → real pool model, hot-reloaded); --official retires only our registration; the app does NOT hot-reload → fully quit and reopen
|
|
28
|
+
mslxdff -setto claude-desktop [modelId...] [--max N] [--max-effort <lvl>] [--1m off|auto|all|ids] [--no-alias] [--check] [--official] one-click Claude Desktop on 3P (ADR-0048): writes the app's configLibrary profile (role-slot model ids + labelOverride, baseUrl without /v1, bearer scheme) AND model-aliases.json (role slot → real pool model, hot-reloaded); --1m (ADR-0056, default auto) tags entries with supports1m+prefer1m when the local /v1/models says the window is >=1M, so the app stops capping custom models at 200K; --official retires only our registration; the app does NOT hot-reload → fully quit and reopen
|
|
29
29
|
mslxdff -claude-desktop [status|slots|check] diagnose Claude Desktop 3P (read-only): managed policy → profile registration → per-slot alias → live /v1/messages probe with reasons and next command
|
|
30
30
|
mslxdff -provider add <id> <baseUrl> <key> [allowedModel...] add a generic OpenAI-compatible provider (myapi/gpt-4, baseUrl https://api.example.com/v1; extra models = allowlist)
|
|
31
31
|
mslxdff -provider add workbuddy https://copilot.tencent.com <key> [allow...] add WorkBuddy专用供应商(workbuddy/hy3 前缀路由,auths 与 keys 一一对应)
|
package/src/opencode-identity.js
CHANGED
|
@@ -1,37 +1,57 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
-
// opencode
|
|
4
|
-
//
|
|
5
|
-
// (timestamp*4096
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// opencode 客户端身份规格单一来源。真值 = 上游 packages/schema/src/identifier.ts:
|
|
4
|
+
// id = <prefix>_ + 12 位 hex 时间段 + 14 位 base62;UA 必须 "opencode/<semver>"。
|
|
5
|
+
// ⚠ 时间段是**倒序**的:value = ~(timestamp*4096 + counter) & 48bit(descending,最新排最前)。
|
|
6
|
+
// 正序时钟不合规——zen 免费层自 2026-10-13 起把低 12 位**反解回 counter** 校验,只接受 [0,63];
|
|
7
|
+
// 正序 id 反解出 4094 → 403 FreeTierError(现网实测:counter=1/40 → 200,counter=4094 → 403)。
|
|
8
|
+
// 时钟半段不校验:ms*4096 如今已需 53bit,截到 48bit 必回绕——真客户端自己反解出来是 1972 年。
|
|
9
|
+
// ⇒ 会话 id 可以继续由摘要确定性派生(跨重启稳定),只需把低 12 位强制成合法 counter。
|
|
10
|
+
// 旧判据全部仍有效:形状正则(2026-09-17)、UA ≥ 1.18.0(2026-09-18,低版本 426 UpgradeRequired)、
|
|
11
|
+
// 以及请求体那半道 agent 形状闸(stream + 核心五工具,见 free-lane.js)。
|
|
8
12
|
const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
13
|
+
const MASK48 = 0xffffffffffffn;
|
|
14
|
+
const MASK12 = 0xfffn;
|
|
15
|
+
// 同毫秒内第 N 个 id:免费层只认反解后 ∈[0,63],越界必 403 ⇒ 到 63 回头,不无限自增
|
|
16
|
+
const COUNTER_MAX = 63;
|
|
9
17
|
const DEFAULT_OPENCODE_UA = "opencode/1.18.31";
|
|
10
18
|
let idLastTs = 0;
|
|
11
19
|
let idCounter = 0;
|
|
12
20
|
|
|
21
|
+
/** 合规时间段:倒序的 timestamp*4096+counter 截 48bit,恒 12 位小写 hex */
|
|
22
|
+
export function identifierTimeHex(timestamp = Date.now(), counter = 1) {
|
|
23
|
+
return ((~(BigInt(timestamp) * 0x1000n + BigInt(counter))) & MASK48).toString(16).padStart(12, "0");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从 12 位 hex 时间段反解 per-ms 计数——与免费层闸门同一判据。
|
|
28
|
+
* 值域 0..4095,闸门只放行 ≤63;形状不符返回 NaN(绝不抛,调用方用 Number.isInteger 自检)。
|
|
29
|
+
*/
|
|
30
|
+
export function counterFromTimeHex(hex) {
|
|
31
|
+
if (typeof hex !== "string" || !/^[0-9a-f]{12}$/i.test(hex)) return Number.NaN;
|
|
32
|
+
return Number((~BigInt("0x" + hex)) & MASK12);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function base62From(bytes) {
|
|
36
|
+
let out = "";
|
|
37
|
+
for (const b of bytes) out += ID_CHARS[b % 62];
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
13
41
|
export function opencodeIdTail(timestamp = Date.now()) {
|
|
14
42
|
if (timestamp !== idLastTs) {
|
|
15
43
|
idLastTs = timestamp;
|
|
16
44
|
idCounter = 0;
|
|
17
45
|
}
|
|
18
|
-
idCounter
|
|
19
|
-
|
|
20
|
-
let tail = "";
|
|
21
|
-
for (let i = 0; i < 6; i++) {
|
|
22
|
-
tail += Number((value >> BigInt(40 - 8 * i)) & 0xffn)
|
|
23
|
-
.toString(16)
|
|
24
|
-
.padStart(2, "0");
|
|
25
|
-
}
|
|
26
|
-
for (const b of crypto.randomBytes(14)) tail += ID_CHARS[b % 62];
|
|
27
|
-
return tail;
|
|
46
|
+
idCounter = (idCounter % COUNTER_MAX) + 1;
|
|
47
|
+
return identifierTimeHex(timestamp, idCounter) + base62From(crypto.randomBytes(14));
|
|
28
48
|
}
|
|
29
49
|
|
|
30
|
-
// 摘要派生:同种子恒同 id
|
|
50
|
+
// 摘要派生:同种子恒同 id(会话亲和——上游按 session 归组免费额度与 prompt cache,改成现取
|
|
51
|
+
// 时钟会让同一会话每请求换号,绝不)。时钟半段照摘要填(不校验),低 12 位强制 counter=1。
|
|
31
52
|
export function digestIdTail(digest) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return tail;
|
|
53
|
+
const time = ((BigInt("0x" + digest.subarray(0, 6).toString("hex")) & ~MASK12) | ((~1n) & MASK12)) & MASK48;
|
|
54
|
+
return time.toString(16).padStart(12, "0") + base62From(digest.subarray(6, 20));
|
|
35
55
|
}
|
|
36
56
|
|
|
37
57
|
export function genId(prefix) {
|