dsh-connect-modelscope-token-plan 0.6.5 → 0.7.0
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/CHANGELOG.md +41 -0
- package/README.md +26 -12
- package/THIRD_PARTY_NOTICES.md +32 -0
- package/client.js +225 -23
- package/cordis.patch.yml +3 -4
- package/lib/host-config.js +1 -1
- package/lib/index.js +456 -94
- package/package.json +3 -1
package/lib/index.js
CHANGED
|
@@ -1540,8 +1540,8 @@ function createCoalescedFetch(options = {}) {
|
|
|
1540
1540
|
* 最大值:个别网关把窗口抄大(如 openrouter 抄 1M)不代表魔搭侧真开那么宽。
|
|
1541
1541
|
*
|
|
1542
1542
|
* 维护纪律:
|
|
1543
|
-
* 1. 只收**当前魔搭目录里真实存在**的 id(2026-10-
|
|
1544
|
-
* 37 个中筛出
|
|
1543
|
+
* 1. 只收**当前魔搭目录里真实存在**的 id(2026-10-23 实拉 `/v1/models` 全集
|
|
1544
|
+
* 37 个中筛出 21 个);目录下架的条目应随手删除,表不是历史档案。
|
|
1545
1545
|
* 2. 每条注明来源 host 与核对日期;有多个数据源时写主流值并留离群说明。
|
|
1546
1546
|
* 3. 覆盖优先级由 `curatedWindowOf` 的调用方(inference-client 打标)保证:
|
|
1547
1547
|
* 目录声明过窗口的条目不进打标,策展表永不推翻平台声明。
|
|
@@ -1555,14 +1555,14 @@ function createCoalescedFetch(options = {}) {
|
|
|
1555
1555
|
* 而使用者无从判断一个值是三个月前还是三年前核的。快照侧据此在表过期时给一条
|
|
1556
1556
|
* shapeWarning(见 {@link curatedCatalogStale})。
|
|
1557
1557
|
*/
|
|
1558
|
-
const CURATED_AS_OF = "2026-10-
|
|
1558
|
+
const CURATED_AS_OF = "2026-10-23";
|
|
1559
1559
|
/**
|
|
1560
1560
|
* 策展表距上次核对已经过去多少天;日期解析不了返回 `null`。
|
|
1561
1561
|
* @param {Date} [now] - 时钟源(测试注入)。
|
|
1562
1562
|
* @returns {number|null} 天数(向下取整),或 null。
|
|
1563
1563
|
*/
|
|
1564
1564
|
function curatedCatalogAgeDays(now = /* @__PURE__ */ new Date()) {
|
|
1565
|
-
const asOf = Date.parse("2026-10-
|
|
1565
|
+
const asOf = Date.parse("2026-10-23T00:00:00Z");
|
|
1566
1566
|
if (Number.isNaN(asOf)) return null;
|
|
1567
1567
|
const delta = now.getTime() - asOf;
|
|
1568
1568
|
return delta < 0 ? 0 : Math.floor(delta / 864e5);
|
|
@@ -1586,6 +1586,7 @@ const CURATED_CONTEXT_WINDOWS = {
|
|
|
1586
1586
|
"deepseek-ai/DeepSeek-V4-Pro": 1048576,
|
|
1587
1587
|
"deepseek-ai/DeepSeek-V4-Pro-0813": 1048576,
|
|
1588
1588
|
"deepseek-ai/DeepSeek-V4.1-Flash": 1048576,
|
|
1589
|
+
"deepseek-ai/DeepSeek-V4.1-Flash-1017": 1048576,
|
|
1589
1590
|
"meituan-longcat/LongCat-Flash-Lite": 256e3,
|
|
1590
1591
|
"MiniMax/MiniMax-M1-80k": 1e6,
|
|
1591
1592
|
"MiniMax/MiniMax-M3": 1048576,
|
|
@@ -1599,6 +1600,7 @@ const CURATED_CONTEXT_WINDOWS = {
|
|
|
1599
1600
|
"Qwen/Qwen3.8-Flash-Next": 262144,
|
|
1600
1601
|
"stepfun-ai/Step-3.5-Flash": 262144,
|
|
1601
1602
|
"stepfun-ai/Step-3.7-Flash": 262144,
|
|
1603
|
+
"stepfun-ai/Step-5-Preview": 1e6,
|
|
1602
1604
|
"ZhipuAI/GLM-4.7-Flash": 2e5,
|
|
1603
1605
|
"ZhipuAI/GLM-5.2": 1e6
|
|
1604
1606
|
};
|
|
@@ -2026,7 +2028,7 @@ function createInferenceClient({ settings, tokenStore, deps = {}, logger }) {
|
|
|
2026
2028
|
* collapses here, so the three call sites that produce it stay in step instead
|
|
2027
2029
|
* of each spelling out the same three `null`s.
|
|
2028
2030
|
*/
|
|
2029
|
-
const EMPTY_PAYLOAD$
|
|
2031
|
+
const EMPTY_PAYLOAD$3 = {
|
|
2030
2032
|
enabled: null,
|
|
2031
2033
|
enabledIds: null,
|
|
2032
2034
|
contextWindow: null,
|
|
@@ -2255,7 +2257,7 @@ function createFileProviderStore(options = {}) {
|
|
|
2255
2257
|
* `null` branch collapses here, in one place, rather than at every caller.
|
|
2256
2258
|
* @returns {Promise<ProviderPayload>} the payload; the empty payload when nothing is stored.
|
|
2257
2259
|
*/
|
|
2258
|
-
const readPayload = async () => await read() ?? EMPTY_PAYLOAD$
|
|
2260
|
+
const readPayload = async () => await read() ?? EMPTY_PAYLOAD$3;
|
|
2259
2261
|
/**
|
|
2260
2262
|
* Read one payload, then hand it to {@link writePayload} — the read-modify-
|
|
2261
2263
|
* write that keeps the switch and the list from clobbering each other.
|
|
@@ -2479,7 +2481,7 @@ function createFileProviderStore(options = {}) {
|
|
|
2479
2481
|
const DRAW_STORE_VERSION = 1;
|
|
2480
2482
|
/** 本 build 读得懂的版本集合(升级时把新号加进来,旧号保持可读)。 */
|
|
2481
2483
|
const KNOWN_DRAW_VERSIONS = Object.freeze([1]);
|
|
2482
|
-
const EMPTY_PAYLOAD$
|
|
2484
|
+
const EMPTY_PAYLOAD$2 = {
|
|
2483
2485
|
enabled: null,
|
|
2484
2486
|
modelId: null,
|
|
2485
2487
|
version: null
|
|
@@ -2578,7 +2580,7 @@ function createFileDrawStore(options = {}) {
|
|
|
2578
2580
|
* 读当前 payload。
|
|
2579
2581
|
* @returns {Promise<DrawPayload>}
|
|
2580
2582
|
*/
|
|
2581
|
-
const readPayload = async () => await cache.read() ?? EMPTY_PAYLOAD$
|
|
2583
|
+
const readPayload = async () => await cache.read() ?? EMPTY_PAYLOAD$2;
|
|
2582
2584
|
/**
|
|
2583
2585
|
* 读-改-写一份补丁,**整段排队**。
|
|
2584
2586
|
*
|
|
@@ -2836,7 +2838,7 @@ function switchSource(panel) {
|
|
|
2836
2838
|
const USAGE_PREFS_VERSION = 1;
|
|
2837
2839
|
/** 本 build 读得懂的版本集合(升级时把新号加进来,旧号保持可读)。 */
|
|
2838
2840
|
const KNOWN_USAGE_PREFS_VERSIONS = Object.freeze([1]);
|
|
2839
|
-
const EMPTY_PAYLOAD = {
|
|
2841
|
+
const EMPTY_PAYLOAD$1 = {
|
|
2840
2842
|
windowDays: null,
|
|
2841
2843
|
version: null
|
|
2842
2844
|
};
|
|
@@ -2955,7 +2957,7 @@ function createFileUsagePrefsStore(options = {}) {
|
|
|
2955
2957
|
* 读当前 payload。
|
|
2956
2958
|
* @returns {Promise<UsagePrefsPayload>}
|
|
2957
2959
|
*/
|
|
2958
|
-
const readPayload = async () => await cache.read() ?? EMPTY_PAYLOAD;
|
|
2960
|
+
const readPayload = async () => await cache.read() ?? EMPTY_PAYLOAD$1;
|
|
2959
2961
|
/**
|
|
2960
2962
|
* 读-改-写,**整段排队**。
|
|
2961
2963
|
*
|
|
@@ -3014,6 +3016,258 @@ function createFileUsagePrefsStore(options = {}) {
|
|
|
3014
3016
|
};
|
|
3015
3017
|
}
|
|
3016
3018
|
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/host/roster-seen-store.ts
|
|
3021
|
+
/**
|
|
3022
|
+
* 「本机第一次见到这个模型是什么时候」——胶囊那句「· N 新模型」的唯一事实源。
|
|
3023
|
+
*
|
|
3024
|
+
* ## 它回答什么,不回答什么
|
|
3025
|
+
*
|
|
3026
|
+
* 只记**首见时间戳**:每个 id 第一次出现在本机拉到的目录里的那一刻。由此派生的
|
|
3027
|
+
* 那句话必须说成「本机首次见到的模型」,**不能**说成「网关新增」——上游的
|
|
3028
|
+
* `/v1/models` 不提供「上架时间」,本插件没有任何字段能证实某个模型是这周才上
|
|
3029
|
+
* 的。能证实的只有「本机从哪天开始看见它」,那就只说这一句。
|
|
3030
|
+
*
|
|
3031
|
+
* 也**不**记录活动/促销变化(限免、错峰、签到):那些是实时读数,面板的余额块与
|
|
3032
|
+
* 胶囊 popover 已经直接画上游给的现值,再造一本「促销 diff 账」就是同一事实两本
|
|
3033
|
+
* 账,两本迟早说两套话(ADR-017 的口径)。
|
|
3034
|
+
*
|
|
3035
|
+
* ## 一次调用同时 diff 与落盘
|
|
3036
|
+
*
|
|
3037
|
+
* {@link RosterSeenStore.note} 不能拆成「先 diff 缓存、稍后再写」:那样两个并发
|
|
3038
|
+
* 的收敛轮可能都基于同一份旧 map 判断,后写的那次会把先写的那批模型再宣布一遍。
|
|
3039
|
+
* 读、算、写在一个调用里完成。
|
|
3040
|
+
*
|
|
3041
|
+
* ## 首次写入是**播种**,不是宣布
|
|
3042
|
+
*
|
|
3043
|
+
* 升级到带本文件的构建时,磁盘上没有历史——此时**知道**目录里有这些模型,但
|
|
3044
|
+
* **不知道**其中哪些是新的。把整个目录宣布成「刚上架」正是本文件要避免的谎,
|
|
3045
|
+
* 所以播种那一轮把每个 id 都盖成「已过期」的戳(`now - 窗口`),`newIds` 返回
|
|
3046
|
+
* 空。这个自愈是刻意的:某个 id 要先离开目录再回来,才配得到一个真实的首见戳。
|
|
3047
|
+
*
|
|
3048
|
+
* ## 200 个 id 以上是**形状漂移**,不是「保留最新 200」
|
|
3049
|
+
*
|
|
3050
|
+
* 按「保留最新」淘汰一个仍在目录里的 id,会让它下一轮看起来像没见过 → 重新盖戳
|
|
3051
|
+
* → 再被宣布一次;而被淘汰的集合每轮轮换,于是「新模型」计数会**震荡并变大**
|
|
3052
|
+
* (实测 250 个 id 对 200 上限:50 → 100 → …)。会自己发明新模型的历史文件比
|
|
3053
|
+
* 没有历史文件更糟,所以超限一律读作形状漂移:**不写、不宣布**,旧 map 原样
|
|
3054
|
+
* 保留,标记在异常期间消失,目录回到合理规模后自愈。
|
|
3055
|
+
*
|
|
3056
|
+
* ## 分段与 legacy
|
|
3057
|
+
*
|
|
3058
|
+
* 落点与 provider / draw / usage-prefs 同一套分段规则:
|
|
3059
|
+
* `$DSH_HOME/state/<profile>/<plugin>/roster-seen.json`(无 profile 时落共享目录)。
|
|
3060
|
+
* **不搬**:本文件晚于 profile 分段引入,共享目录里不存在它的旧布局可继承——
|
|
3061
|
+
* 与 usage-prefs-store 的「legacy 继承」相反,这里没有 legacy 缝,也不该长出一条。
|
|
3062
|
+
*
|
|
3063
|
+
* 完整性三件套与其余 store 同源(全部上收在 `state-store.ts`,本文件只做调用方,
|
|
3064
|
+
* 论述见 `draw-store.ts` 头注):写串行化({@link createStateWriteQueue})、写守卫
|
|
3065
|
+
* ({@link guardStateWrite},ADR-006)、读侧说明({@link stateVersionNote})。
|
|
3066
|
+
*
|
|
3067
|
+
* doctor 覆盖:{@link describeRosterSeenPayload} 给只读速览,`doctor.ts` 据此把
|
|
3068
|
+
* roster-seen.json 的「损坏 / 未知版本」报进症状表。
|
|
3069
|
+
*
|
|
3070
|
+
* @module dsh-connect-modelscope-token-plan/roster-seen-store
|
|
3071
|
+
*/
|
|
3072
|
+
/** 本 store 落盘 payload 的版本。 */
|
|
3073
|
+
const ROSTER_SEEN_VERSION = 1;
|
|
3074
|
+
/** 本 build 读得懂的版本集合(升级时把新号加进来,旧号保持可读)。 */
|
|
3075
|
+
const KNOWN_ROSTER_SEEN_VERSIONS = Object.freeze([1]);
|
|
3076
|
+
/**
|
|
3077
|
+
* 「新」的时效窗口:首见戳落在这个窗口内就算新。
|
|
3078
|
+
*
|
|
3079
|
+
* 7 天是有意的尺度——它要盖住「用户几天没开面板、回来时发现多了两个模型」这个
|
|
3080
|
+
* 真实场景,又不能长到让一次批量上架在半个月后还挂着标记。窗口在这里是**常量**
|
|
3081
|
+
* 而不是配置:一个能被调成 365 天的「新模型」标记等于没有语义。
|
|
3082
|
+
*/
|
|
3083
|
+
const ROSTER_SEEN_NEW_WINDOW_MS = 6048e5;
|
|
3084
|
+
/**
|
|
3085
|
+
* 目录规模上限。超过即视为形状漂移(不是淘汰队列,理由见模块头注)。
|
|
3086
|
+
*
|
|
3087
|
+
* 取值与姊妹插件的同类实现一致:真实目录在数百个模型以内,200 给「这一侧目录
|
|
3088
|
+
* 突然返回了整站列表」这类事故留出足够间距。
|
|
3089
|
+
*/
|
|
3090
|
+
const ROSTER_SEEN_MAX_IDS = 200;
|
|
3091
|
+
const EMPTY_PAYLOAD = {
|
|
3092
|
+
seen: {},
|
|
3093
|
+
version: null
|
|
3094
|
+
};
|
|
3095
|
+
/**
|
|
3096
|
+
* 本 build 读不读得懂这个版本。
|
|
3097
|
+
*
|
|
3098
|
+
* 与其余 store 的 `isKnown*Version` 同形:**白名单**判据,不与
|
|
3099
|
+
* {@link ROSTER_SEEN_VERSION} 比较——升级后旧号仍要可读。读侧、报告侧问的都是
|
|
3100
|
+
* 这一个函数。
|
|
3101
|
+
* @param {unknown} version - 盘上读到的值(可能根本不是数字)。
|
|
3102
|
+
* @returns {boolean}
|
|
3103
|
+
*/
|
|
3104
|
+
function isKnownRosterSeenVersion(version) {
|
|
3105
|
+
return typeof version === "number" && KNOWN_ROSTER_SEEN_VERSIONS.includes(version);
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* 归一一张首见表:只留「id 是非空字符串、戳是有限正数」的项。
|
|
3109
|
+
*
|
|
3110
|
+
* 丢掉而不是修补坏项是有意的:一个戳为 0 或负数的项会让 `now - stamp` 变成
|
|
3111
|
+
* 几十年,那个 id 于是永远算「新」;宁可让它下次出现时重新盖一个真实戳。
|
|
3112
|
+
* @param {unknown} raw - 盘上读到的 `seen` 值。
|
|
3113
|
+
* @returns {RosterSeen} 归一后的表(可能是空对象)。
|
|
3114
|
+
*/
|
|
3115
|
+
function normalizeRosterSeen(raw) {
|
|
3116
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return {};
|
|
3117
|
+
const out = {};
|
|
3118
|
+
for (const [id, stamp] of Object.entries(raw)) {
|
|
3119
|
+
if (id === "") continue;
|
|
3120
|
+
if (typeof stamp !== "number" || !Number.isFinite(stamp) || stamp <= 0) continue;
|
|
3121
|
+
out[id] = stamp;
|
|
3122
|
+
}
|
|
3123
|
+
return out;
|
|
3124
|
+
}
|
|
3125
|
+
/** 本 store 的目录(与 provider / draw / usage-prefs 同一套分段规则)。 */
|
|
3126
|
+
function rosterSeenDir(profile, dir) {
|
|
3127
|
+
if (typeof dir === "string" && dir !== "") return dir;
|
|
3128
|
+
return profile ? profileStateDir(name, profile) : stateDir(name);
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* 从一张首见表里挑出「还算新」的 id,按首见时间升序(最老的在前)。
|
|
3132
|
+
*
|
|
3133
|
+
* 升序是有意的:调用方只展示前几个名字(胶囊 tooltip / popover 的预算),先看到
|
|
3134
|
+
* 的应该是**最早**变成新的那几个——它们离过期最近,也是读者最可能已经见过、
|
|
3135
|
+
* 需要被告知「其实才刚出现」的那批。
|
|
3136
|
+
* @param {RosterSeen} seen - 归一后的首见表。
|
|
3137
|
+
* @param {number} nowMs - 当前时刻。
|
|
3138
|
+
* @returns {string[]} 窗口内的 id 列表。
|
|
3139
|
+
*/
|
|
3140
|
+
function freshIdsOf(seen, nowMs) {
|
|
3141
|
+
return Object.entries(seen).filter(([, first]) => nowMs - first < ROSTER_SEEN_NEW_WINDOW_MS).sort((left, right) => left[1] - right[1]).map(([id]) => id);
|
|
3142
|
+
}
|
|
3143
|
+
/**
|
|
3144
|
+
* 文件支撑的首见表。
|
|
3145
|
+
* @param {StoreOptions} [options] - `{ dir, profile, ttlMs, logger }`。
|
|
3146
|
+
* @returns {object} store 句柄。
|
|
3147
|
+
*/
|
|
3148
|
+
function createFileRosterSeenStore(options = {}) {
|
|
3149
|
+
const { dir, profile = null, ttlMs = STATE_READ_TTL_MS, logger } = options;
|
|
3150
|
+
const stateDir = rosterSeenDir(profile, dir);
|
|
3151
|
+
const filePath = join(stateDir, "roster-seen.json");
|
|
3152
|
+
/**
|
|
3153
|
+
* 写一份 payload 并落盘。
|
|
3154
|
+
*
|
|
3155
|
+
* ADR-006 写守卫({@link guardStateWrite})先问盘:版本不认或文件读不出 →
|
|
3156
|
+
* 返回拒绝理由(已 degrade 记录),不落笔。
|
|
3157
|
+
* @param {object} body - 要持久化的 JSON 体。
|
|
3158
|
+
* @returns {Promise<string|null>} 拒绝原因,或 `null` = 已写入。
|
|
3159
|
+
*/
|
|
3160
|
+
const writePayload = async (body) => {
|
|
3161
|
+
const refusal = await guardStateWrite(filePath, KNOWN_ROSTER_SEEN_VERSIONS, {
|
|
3162
|
+
label: "roster-seen-state",
|
|
3163
|
+
logger
|
|
3164
|
+
});
|
|
3165
|
+
if (refusal !== null) return refusal;
|
|
3166
|
+
await ensureStateDir(stateDir);
|
|
3167
|
+
await writeStateFile(filePath, JSON.stringify(body, null, 2), { temporary: temporaryOf(stateDir, "roster-seen.json") });
|
|
3168
|
+
return null;
|
|
3169
|
+
};
|
|
3170
|
+
/** 写串行化:读-改-写必须排队(详见{@link createStateWriteQueue})。 */
|
|
3171
|
+
const writeQueue = createStateWriteQueue();
|
|
3172
|
+
/**
|
|
3173
|
+
* 解析一份路径上的 payload;认不出的版本读作「空表」但**记住**版本号——读侧
|
|
3174
|
+
* 宽容、versionNote 负责把宽容说出口(写侧守卫已挡住覆盖,两半合起来才是
|
|
3175
|
+
* ADR-006)。
|
|
3176
|
+
*/
|
|
3177
|
+
const parseAt = async (path) => {
|
|
3178
|
+
const source = obj(await readStateJson(path));
|
|
3179
|
+
const version = typeof source.version === "number" ? source.version : null;
|
|
3180
|
+
if (!isKnownRosterSeenVersion(version)) return {
|
|
3181
|
+
seen: {},
|
|
3182
|
+
version
|
|
3183
|
+
};
|
|
3184
|
+
return {
|
|
3185
|
+
seen: normalizeRosterSeen(source.seen),
|
|
3186
|
+
version
|
|
3187
|
+
};
|
|
3188
|
+
};
|
|
3189
|
+
const cache = createStateReadCache(() => parseAt(filePath), { ttlMs });
|
|
3190
|
+
/** 读当前 payload。 */
|
|
3191
|
+
const readPayload = async () => await cache.read() ?? EMPTY_PAYLOAD;
|
|
3192
|
+
return {
|
|
3193
|
+
/**
|
|
3194
|
+
* 记下这一轮的完整目录,并回答「其中哪些还在新窗口内」。
|
|
3195
|
+
*
|
|
3196
|
+
* 一次调用同时 diff 与落盘(理由见模块头注)。首轮写入是**播种**:把每个 id
|
|
3197
|
+
* 盖成已过期的戳、返回空 `newIds`。
|
|
3198
|
+
* @param {readonly string[]} ids - 上游这一轮列出的**完整**目录 id。
|
|
3199
|
+
* @param {number} [nowMs] - 时钟(测试注入)。
|
|
3200
|
+
* @returns {Promise<{newIds: string[]}>} 窗口内的 id,升序;播种那轮为空。
|
|
3201
|
+
*/
|
|
3202
|
+
async note(ids, nowMs = Date.now()) {
|
|
3203
|
+
const list = Array.isArray(ids) ? ids.filter((id) => typeof id === "string" && id !== "") : [];
|
|
3204
|
+
const unique = [...new Set(list)];
|
|
3205
|
+
if (unique.length === 0) return { newIds: [] };
|
|
3206
|
+
const now = Number.isFinite(nowMs) ? nowMs : Date.now();
|
|
3207
|
+
return writeQueue.enqueue(async () => {
|
|
3208
|
+
const current = await parseAt(filePath);
|
|
3209
|
+
if (current.version !== null && !isKnownRosterSeenVersion(current.version)) return { newIds: [] };
|
|
3210
|
+
const previous = current.seen;
|
|
3211
|
+
const seeded = current.version === null;
|
|
3212
|
+
const next = {};
|
|
3213
|
+
for (const id of unique) {
|
|
3214
|
+
const first = previous[id];
|
|
3215
|
+
next[id] = typeof first === "number" && first > 0 ? Math.min(first, now) : seeded ? now - ROSTER_SEEN_NEW_WINDOW_MS : now;
|
|
3216
|
+
}
|
|
3217
|
+
if (Object.keys(next).length > 200) return { newIds: [] };
|
|
3218
|
+
if (seeded || Object.keys(previous).length !== Object.keys(next).length || Object.keys(next).some((id) => previous[id] !== next[id])) {
|
|
3219
|
+
const refusal = await writePayload({
|
|
3220
|
+
version: 1,
|
|
3221
|
+
seen: next,
|
|
3222
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3223
|
+
});
|
|
3224
|
+
if (refusal !== null) throw new Error(refusal);
|
|
3225
|
+
cache.remember({
|
|
3226
|
+
seen: next,
|
|
3227
|
+
version: 1
|
|
3228
|
+
});
|
|
3229
|
+
}
|
|
3230
|
+
return { newIds: seeded ? [] : freshIdsOf(next, now) };
|
|
3231
|
+
});
|
|
3232
|
+
},
|
|
3233
|
+
/**
|
|
3234
|
+
* 只读:此刻还算「新」的 id(升序)。
|
|
3235
|
+
*
|
|
3236
|
+
* **不写盘**:快照与 provider 路由都是读路径,写只发生在收敛器的 `note`。
|
|
3237
|
+
* 读路径若顺手补戳,一次面板轮询就能把「谁是新模型」这件事改写掉。
|
|
3238
|
+
* @param {number} [nowMs] - 时钟(测试注入)。
|
|
3239
|
+
* @returns {Promise<string[]>}
|
|
3240
|
+
*/
|
|
3241
|
+
async fresh(nowMs = Date.now()) {
|
|
3242
|
+
return freshIdsOf((await readPayload()).seen, Number.isFinite(nowMs) ? nowMs : Date.now());
|
|
3243
|
+
},
|
|
3244
|
+
/** 这张表有没有写过(区分「播种」与「真的没有新模型」)。 */
|
|
3245
|
+
async isSet() {
|
|
3246
|
+
return (await readPayload()).version !== null;
|
|
3247
|
+
},
|
|
3248
|
+
/**
|
|
3249
|
+
* 盘上带着本构建读不懂的版本时的一句说明,否则 `null`。
|
|
3250
|
+
*
|
|
3251
|
+
* 读侧宽容(未知版本读作空表)的另一半:快照聚合把它转进 `shapeWarnings`,
|
|
3252
|
+
* doctor 报同一份文件。没有它,「首见表被忽略」在面板上就是「没有新模型」。
|
|
3253
|
+
* @returns {Promise<string|null>}
|
|
3254
|
+
*/
|
|
3255
|
+
async versionNote() {
|
|
3256
|
+
const version = (await readPayload()).version;
|
|
3257
|
+
if (version === null || isKnownRosterSeenVersion(version)) return null;
|
|
3258
|
+
return stateVersionNote({
|
|
3259
|
+
label: "roster-seen-state",
|
|
3260
|
+
file: filePath,
|
|
3261
|
+
version,
|
|
3262
|
+
known: KNOWN_ROSTER_SEEN_VERSIONS,
|
|
3263
|
+
consequence: "the first-sighting history is being answered as empty, so the pill shows no new-model marker."
|
|
3264
|
+
});
|
|
3265
|
+
},
|
|
3266
|
+
/** 落盘路径(诊断与测试用)。 */
|
|
3267
|
+
filePath
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3017
3271
|
//#endregion
|
|
3018
3272
|
//#region src/host/draw.ts
|
|
3019
3273
|
/**
|
|
@@ -3948,11 +4202,10 @@ function sleep(ms, signal) {
|
|
|
3948
4202
|
/** 当前缺席原因(`null` = 未登记 / 已清掉)。 */
|
|
3949
4203
|
let note = null;
|
|
3950
4204
|
/**
|
|
3951
|
-
*
|
|
4205
|
+
* 此刻工具是否真的在 registry 里。
|
|
3952
4206
|
*
|
|
3953
|
-
* 与 `note` 的区别见文件头:`note`
|
|
3954
|
-
*
|
|
3955
|
-
* 只有这一位能把它认出来。
|
|
4207
|
+
* 与 `note` 的区别见文件头:`note` 是「为什么没成」,这一位是「此刻在不在」。
|
|
4208
|
+
* 开关翻转会当场重算这一位,所以它是动态读数,不是「本次挂载」的存档。
|
|
3956
4209
|
*/
|
|
3957
4210
|
let mounted = false;
|
|
3958
4211
|
/**
|
|
@@ -3998,22 +4251,33 @@ function resetDrawToolNote() {
|
|
|
3998
4251
|
//#endregion
|
|
3999
4252
|
//#region src/host/draw-register.ts
|
|
4000
4253
|
/**
|
|
4001
|
-
* 出图工具的挂载副作用:读开关 → 解析目录 → 注册 agent
|
|
4254
|
+
* 出图工具的挂载副作用:读开关 → 解析目录 → 注册 agent 工具;开关翻转时**现挂现卸**。
|
|
4002
4255
|
*
|
|
4003
4256
|
* 与 `provider-publish.ts` 的分工:那个管「魔搭作为 chat provider 的注册」,本文件
|
|
4004
4257
|
* 管「给 agent 一个出图工具」。两者是**独立的开关**——回答的是不同问题,合并没有
|
|
4005
4258
|
* 好处(用户可能想用工具出图但不想把魔搭塞进模型选择器,反之亦然)。
|
|
4006
4259
|
*
|
|
4007
|
-
*
|
|
4008
|
-
*
|
|
4009
|
-
* -
|
|
4010
|
-
*
|
|
4260
|
+
* 承重契约:peer 的 tools.register 返回 disposer(`() => void` 注销句柄,见
|
|
4261
|
+
* `@deepseek-ai/dsh-tools` 的 `ToolsService.register`)。所以
|
|
4262
|
+
* - 开关翻转可以在**本次 Host 生命周期内**生效:`sync()` 读一次开关,要开就注册、
|
|
4263
|
+
* 要关就调 disposer 摘掉,不需要等下次启动;
|
|
4264
|
+
* - 句柄必须**存住**:丢掉它 = 插件卸载后注册残留(僵尸工具),而 registry 对重名
|
|
4265
|
+
* insert 会抛错,下一次挂载直接失败(错误文本形如 `tool "<name>" is already
|
|
4266
|
+
* registered`)。旧版把返回的 `unknown` 直接丢弃,正是这条缺口的来源;
|
|
4011
4267
|
* - 目录变化**不需要**重注册:工具在**调用时**才读目录(`getEntries` 是个函数),
|
|
4012
4268
|
* 所以一次目录刷新会自然被下一次出图看到。
|
|
4013
4269
|
*
|
|
4014
|
-
*
|
|
4015
|
-
*
|
|
4016
|
-
*
|
|
4270
|
+
* 并发:`sync()` 串行排队(`chain`),每次执行时**现读**开关,所以快速连点开关
|
|
4271
|
+
* 最终收敛到最后一次保存的值;重复 sync 幂等(已注册就跳过——真去重复注册会撞
|
|
4272
|
+
* registry 的重名保护,把它当正常分支会把「开关开着」误报成缺席)。
|
|
4273
|
+
*
|
|
4274
|
+
* 失败一律降级为「工具缺席」,绝不炸掉插件:tools 服务缺失、peer 加载失败、registry
|
|
4275
|
+
* 拒收——三种都由 `draw-tool-state.ts` 记下原因供面板显示。另有一种**非缺席**的
|
|
4276
|
+
* 降级:registry 收下了却**没返回 disposer**(旧 Host / 旧 peer)。工具能用但关不
|
|
4277
|
+
* 掉,记 `not-removable` 让面板说实话——否则用户关掉开关、面板显示「已关闭」而
|
|
4278
|
+
* 工具还在。
|
|
4279
|
+
*
|
|
4280
|
+
* 本文件不吞错误(那会让「工具为什么不在」无从诊断),只把它们翻译成可展示的原因码。
|
|
4017
4281
|
*
|
|
4018
4282
|
* peer-dependent:`@deepseek-ai/dsh-tools` 由 `deps.loadToolsModule` 惰性注入。
|
|
4019
4283
|
*
|
|
@@ -4031,9 +4295,8 @@ function resetDrawToolNote() {
|
|
|
4031
4295
|
/**
|
|
4032
4296
|
* 解析一个可选服务,允许它稍后才注册。
|
|
4033
4297
|
*
|
|
4034
|
-
* 承 sensenova
|
|
4035
|
-
*
|
|
4036
|
-
* 就没有)。
|
|
4298
|
+
* 承 sensenova 同名函数的纪律:**重试的是读,不是写**。这里只做有限次立即读取,
|
|
4299
|
+
* 不做长退避(服务要么已注册,要么这次就没有)。
|
|
4037
4300
|
* @param {object} ctx - Host 上下文。
|
|
4038
4301
|
* @param {string} service - 服务名。
|
|
4039
4302
|
* @param {() => boolean} isDisposed - 卸载闸。
|
|
@@ -4049,67 +4312,142 @@ async function resolveService(ctx, service, isDisposed) {
|
|
|
4049
4312
|
return null;
|
|
4050
4313
|
}
|
|
4051
4314
|
/**
|
|
4052
|
-
*
|
|
4315
|
+
* 建一个出图工具挂载控制器(不产生副作用,`sync()` 才读开关/注册)。
|
|
4053
4316
|
* @param {object} ctx - Host 上下文。
|
|
4054
|
-
* @param {object} wiring - `{ settings, configError, tokenStore, inference,
|
|
4317
|
+
* @param {object} wiring - `{ settings, configError, tokenStore, inference, drawStore, publisher, logger }`。
|
|
4055
4318
|
* @param {object} deps - 测试缝(`loadToolsModule`、`drawFetch`、`pollIntervalMs`)。
|
|
4056
|
-
* @returns {
|
|
4319
|
+
* @returns {DrawToolMount} 控制器。
|
|
4057
4320
|
*/
|
|
4058
|
-
|
|
4321
|
+
function createDrawToolMount(ctx, wiring, deps = {}) {
|
|
4059
4322
|
const { settings, configError, tokenStore, inference, drawStore, publisher, logger } = wiring;
|
|
4060
4323
|
const loggerFor = logger ?? ctx.logger;
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4324
|
+
/** 当前已注册工具的注销句柄;`null` = 没挂上(或已摘掉)。 */
|
|
4325
|
+
let disposeTool = null;
|
|
4326
|
+
/** peer 的 `defineTool` 只解析一次:成功后就缓存,失败留到下次 sync 重试。 */
|
|
4327
|
+
let defineTool = null;
|
|
4328
|
+
/** 卸载闸:`dispose()` 之后任何在途/后续 sync 都不再注册。 */
|
|
4329
|
+
let closed = false;
|
|
4330
|
+
/** 串行链:保证「读开关 → 挂/卸」不会被两次 sync 交错执行。 */
|
|
4331
|
+
let chain = Promise.resolve();
|
|
4332
|
+
const isDisposed = () => closed || publisher?.isDisposed?.() === true;
|
|
4333
|
+
/**
|
|
4334
|
+
* 摘掉已注册的工具(幂等)。
|
|
4335
|
+
*
|
|
4336
|
+
* 先清注册位再调 disposer:即使 disposer 抛了,面板也不该继续显示「已注册」。
|
|
4337
|
+
* @returns {void}
|
|
4338
|
+
*/
|
|
4339
|
+
function unmount() {
|
|
4340
|
+
const dispose = disposeTool;
|
|
4341
|
+
disposeTool = null;
|
|
4342
|
+
setDrawToolMounted(false);
|
|
4343
|
+
if (dispose !== null) try {
|
|
4344
|
+
dispose();
|
|
4345
|
+
} catch (error) {
|
|
4346
|
+
loggerFor?.warn?.(`${name}: draw tool dispose failed (${redactSecrets(errMsg(error))})`);
|
|
4072
4347
|
}
|
|
4073
|
-
|
|
4348
|
+
setDrawToolNote(null);
|
|
4349
|
+
}
|
|
4350
|
+
/**
|
|
4351
|
+
* 解析 peer 的 `defineTool`(成功后缓存)。
|
|
4352
|
+
* @returns {Promise<Function|null>} `defineTool` 或 `null`(失败已记原因)。
|
|
4353
|
+
*/
|
|
4354
|
+
async function loadDefineTool() {
|
|
4355
|
+
if (defineTool !== null) return defineTool;
|
|
4074
4356
|
try {
|
|
4075
4357
|
const mod = await Promise.resolve(deps.loadToolsModule ? deps.loadToolsModule() : import("@deepseek-ai/dsh-tools"));
|
|
4076
|
-
|
|
4358
|
+
const found = mod?.defineTool ?? mod?.default?.defineTool ?? null;
|
|
4359
|
+
if (typeof found === "function") {
|
|
4360
|
+
defineTool = found;
|
|
4361
|
+
return found;
|
|
4362
|
+
}
|
|
4077
4363
|
} catch (error) {
|
|
4078
|
-
setDrawToolNote("peer-load-failed");
|
|
4079
4364
|
loggerFor?.warn?.(`${name}: draw tool peer module failed to load (${redactSecrets(errMsg(error))}) — the tool is absent`);
|
|
4080
|
-
return;
|
|
4081
4365
|
}
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4366
|
+
return null;
|
|
4367
|
+
}
|
|
4368
|
+
/**
|
|
4369
|
+
* 一次收敛:读开关,要开就注册、要关就注销。
|
|
4370
|
+
* @returns {Promise<void>}
|
|
4371
|
+
*/
|
|
4372
|
+
async function apply() {
|
|
4373
|
+
if (closed) return;
|
|
4374
|
+
try {
|
|
4375
|
+
if (configError !== null) return;
|
|
4376
|
+
const panelValue = drawStore ? await drawStore.enabled().catch(() => null) : null;
|
|
4377
|
+
if (resolveSwitchEnabled(panelValue, settings.drawEnabled === true) !== true) {
|
|
4378
|
+
unmount();
|
|
4379
|
+
return;
|
|
4380
|
+
}
|
|
4381
|
+
if (disposeTool !== null) {
|
|
4382
|
+
setDrawToolNote(null);
|
|
4383
|
+
setDrawToolMounted(true);
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
const tools = await resolveService(ctx, "tools", isDisposed);
|
|
4387
|
+
if (tools === null || typeof tools.register !== "function") {
|
|
4388
|
+
setDrawToolNote("no-tools-service");
|
|
4389
|
+
setDrawToolMounted(false);
|
|
4390
|
+
return;
|
|
4391
|
+
}
|
|
4392
|
+
const found = await loadDefineTool();
|
|
4393
|
+
if (found === null) {
|
|
4394
|
+
setDrawToolNote("peer-load-failed");
|
|
4395
|
+
setDrawToolMounted(false);
|
|
4396
|
+
return;
|
|
4397
|
+
}
|
|
4398
|
+
const drawFetch = deps.drawFetch ?? ((url, options) => fetch(url, options));
|
|
4399
|
+
const dispose = tools.register(defineDrawTool({
|
|
4400
|
+
defineTool: found,
|
|
4401
|
+
fetchImpl: drawFetch,
|
|
4402
|
+
resolveApiKey: async () => (await tokenStore.resolve()).value,
|
|
4403
|
+
getEntries: async () => {
|
|
4404
|
+
return (await inference.fetchModels().catch(() => null))?.entries ?? [];
|
|
4405
|
+
},
|
|
4406
|
+
getPreferredModelId: async () => {
|
|
4407
|
+
const panelValue = drawStore ? await drawStore.modelId().catch(() => null) : null;
|
|
4408
|
+
return resolveSwitchValue(panelValue, settings.drawModelId === "" ? void 0 : settings.drawModelId) ?? null;
|
|
4409
|
+
},
|
|
4410
|
+
settings: {
|
|
4411
|
+
apiBase: settings.apiBase,
|
|
4412
|
+
drawModelId: settings.drawModelId,
|
|
4413
|
+
drawTimeoutMs: settings.drawTimeoutMs,
|
|
4414
|
+
drawWaitMs: settings.drawWaitMs
|
|
4415
|
+
},
|
|
4416
|
+
...wiring.drawJobs !== void 0 ? { jobs: wiring.drawJobs } : {},
|
|
4417
|
+
...deps.pollIntervalMs !== void 0 ? { pollIntervalMs: deps.pollIntervalMs } : {},
|
|
4418
|
+
isDisposed
|
|
4419
|
+
}));
|
|
4420
|
+
if (typeof dispose === "function") {
|
|
4421
|
+
disposeTool = dispose;
|
|
4422
|
+
setDrawToolNote(null);
|
|
4423
|
+
setDrawToolMounted(true);
|
|
4424
|
+
} else {
|
|
4425
|
+
disposeTool = null;
|
|
4426
|
+
setDrawToolNote("not-removable");
|
|
4427
|
+
setDrawToolMounted(true);
|
|
4428
|
+
loggerFor?.warn?.(`${name}: draw tool registered without a disposer — turning the switch off cannot unmount it on this host`);
|
|
4429
|
+
}
|
|
4430
|
+
} catch (error) {
|
|
4431
|
+
setDrawToolNote("registry-refused");
|
|
4432
|
+
setDrawToolMounted(false);
|
|
4433
|
+
loggerFor?.warn?.(`${name}: draw tool registration refused (${redactSecrets(errMsg(error))}) — the tool is absent`);
|
|
4085
4434
|
}
|
|
4086
|
-
const drawFetch = deps.drawFetch ?? ((url, options) => fetch(url, options));
|
|
4087
|
-
tools.register(defineDrawTool({
|
|
4088
|
-
defineTool,
|
|
4089
|
-
fetchImpl: drawFetch,
|
|
4090
|
-
resolveApiKey: async () => (await tokenStore.resolve()).value,
|
|
4091
|
-
getEntries: async () => {
|
|
4092
|
-
return (await inference.fetchModels().catch(() => null))?.entries ?? [];
|
|
4093
|
-
},
|
|
4094
|
-
getPreferredModelId: async () => {
|
|
4095
|
-
const panelValue = drawStore ? await drawStore.modelId().catch(() => null) : null;
|
|
4096
|
-
return resolveSwitchValue(panelValue, settings.drawModelId === "" ? void 0 : settings.drawModelId) ?? null;
|
|
4097
|
-
},
|
|
4098
|
-
settings: {
|
|
4099
|
-
apiBase: settings.apiBase,
|
|
4100
|
-
drawModelId: settings.drawModelId,
|
|
4101
|
-
drawTimeoutMs: settings.drawTimeoutMs,
|
|
4102
|
-
drawWaitMs: settings.drawWaitMs
|
|
4103
|
-
},
|
|
4104
|
-
...wiring.drawJobs !== void 0 ? { jobs: wiring.drawJobs } : {},
|
|
4105
|
-
...deps.pollIntervalMs !== void 0 ? { pollIntervalMs: deps.pollIntervalMs } : {},
|
|
4106
|
-
isDisposed
|
|
4107
|
-
}));
|
|
4108
|
-
setDrawToolMounted(true);
|
|
4109
|
-
} catch (error) {
|
|
4110
|
-
setDrawToolNote("registry-refused");
|
|
4111
|
-
loggerFor?.warn?.(`${name}: draw tool registration refused (${redactSecrets(errMsg(error))}) — the tool is absent`);
|
|
4112
4435
|
}
|
|
4436
|
+
return {
|
|
4437
|
+
sync() {
|
|
4438
|
+
chain = chain.then(apply).catch((error) => {
|
|
4439
|
+
loggerFor?.warn?.(`${name}: draw tool sync failed (${redactSecrets(errMsg(error))})`);
|
|
4440
|
+
});
|
|
4441
|
+
return chain;
|
|
4442
|
+
},
|
|
4443
|
+
dispose() {
|
|
4444
|
+
closed = true;
|
|
4445
|
+
unmount();
|
|
4446
|
+
},
|
|
4447
|
+
mounted() {
|
|
4448
|
+
return drawToolMounted();
|
|
4449
|
+
}
|
|
4450
|
+
};
|
|
4113
4451
|
}
|
|
4114
4452
|
|
|
4115
4453
|
//#endregion
|
|
@@ -4920,7 +5258,7 @@ function syncSignaturesAfterPublish(state) {
|
|
|
4920
5258
|
* @returns {{reconcile: () => Promise<void>; health: () => {{ lastError: unknown }} }}
|
|
4921
5259
|
*/
|
|
4922
5260
|
function createProviderReconciler(deps) {
|
|
4923
|
-
const { settings, providerStore, inference, publisher, usageEvents, logger } = deps;
|
|
5261
|
+
const { settings, providerStore, inference, publisher, usageEvents, logger, rosterSeenStore } = deps;
|
|
4924
5262
|
/**
|
|
4925
5263
|
* 把一次 store 读取收成「值或 null」——**同步抛与异步拒收都降级**。
|
|
4926
5264
|
*
|
|
@@ -5003,6 +5341,11 @@ function createProviderReconciler(deps) {
|
|
|
5003
5341
|
return;
|
|
5004
5342
|
}
|
|
5005
5343
|
const entries = Array.isArray(catalog?.entries) ? catalog.entries : [];
|
|
5344
|
+
if (rosterSeenStore !== void 0) try {
|
|
5345
|
+
await rosterSeenStore.note(entries.map((entry) => String(entry?.id ?? "")));
|
|
5346
|
+
} catch (error) {
|
|
5347
|
+
logger?.warn?.(`${name}: roster first-sighting write failed (${redactSecrets(errMsg(error))}) — the new-model marker may miss this pass`);
|
|
5348
|
+
}
|
|
5006
5349
|
const enabledIds = resolveEnabledIds(after.enabledIds, publisher.state.enabledIds);
|
|
5007
5350
|
const unavailable = await readUnavailable();
|
|
5008
5351
|
try {
|
|
@@ -5116,7 +5459,7 @@ async function soft(promise) {
|
|
|
5116
5459
|
}
|
|
5117
5460
|
/** 聚合快照 body。wiring 子集见 routes/snapshot.ts 的 Pick。 */
|
|
5118
5461
|
async function buildSnapshotBody(wiring) {
|
|
5119
|
-
const { settings, tokenStore, usageStore, inference, providerStore, drawStore, usagePrefsStore, publisher } = wiring;
|
|
5462
|
+
const { settings, tokenStore, usageStore, inference, providerStore, drawStore, usagePrefsStore, rosterSeenStore, publisher } = wiring;
|
|
5120
5463
|
const panelWindow = await usagePrefsStore.windowDays().catch(() => null);
|
|
5121
5464
|
const windowDays = resolveWindowDays(panelWindow, settings.trendDays, settings.retentionDays);
|
|
5122
5465
|
const [tokenState, daily, perModel, trend, events, providerNote] = await Promise.all([
|
|
@@ -5149,6 +5492,8 @@ async function buildSnapshotBody(wiring) {
|
|
|
5149
5492
|
if (drawNote.ok && drawNote.value !== null) shapeWarnings.push(drawNote.value);
|
|
5150
5493
|
const windowNote = await soft((async () => usagePrefsStore.versionNote())());
|
|
5151
5494
|
if (windowNote.ok && windowNote.value !== null) shapeWarnings.push(windowNote.value);
|
|
5495
|
+
const rosterNote = await soft((async () => rosterSeenStore.versionNote())());
|
|
5496
|
+
if (rosterNote.ok && rosterNote.value !== null) shapeWarnings.push(rosterNote.value);
|
|
5152
5497
|
if (curatedCatalogStale()) {
|
|
5153
5498
|
const age = curatedCatalogAgeDays();
|
|
5154
5499
|
shapeWarnings.push(`context-catalog: curated windows were last verified on ${CURATED_AS_OF}` + (age === null ? "" : ` (${age} days ago)`) + " — re-check them against the current catalogue");
|
|
@@ -5164,6 +5509,7 @@ async function buildSnapshotBody(wiring) {
|
|
|
5164
5509
|
const enabledCount = enabledCountOf(allowedList, roster.map((row) => row.id));
|
|
5165
5510
|
const contextWindow = await providerStore.contextWindow().catch(() => null);
|
|
5166
5511
|
const catalogNote = models.ok ? null : `模型目录读不到:${models.error}`;
|
|
5512
|
+
const newModelIds = await rosterSeenStore.fresh().catch(() => []);
|
|
5167
5513
|
return {
|
|
5168
5514
|
enabled,
|
|
5169
5515
|
source,
|
|
@@ -5175,6 +5521,7 @@ async function buildSnapshotBody(wiring) {
|
|
|
5175
5521
|
allowed: allowedList.mode,
|
|
5176
5522
|
enabledIds,
|
|
5177
5523
|
contextWindow,
|
|
5524
|
+
newModelIds,
|
|
5178
5525
|
roster
|
|
5179
5526
|
};
|
|
5180
5527
|
})());
|
|
@@ -5410,7 +5757,8 @@ function withOrigin(handler, allowedHosts) {
|
|
|
5410
5757
|
*/
|
|
5411
5758
|
/**
|
|
5412
5759
|
* 注册快照路由。wiring 子集:settings / configError / tokenStore /
|
|
5413
|
-
* usageStore / inference / providerStore /
|
|
5760
|
+
* usageStore / inference / providerStore / drawStore / usagePrefsStore /
|
|
5761
|
+
* rosterSeenStore / publisher / logger。
|
|
5414
5762
|
* @returns {Function} off() 注销回调。
|
|
5415
5763
|
*/
|
|
5416
5764
|
function registerSnapshotRoute(ctx, wiring) {
|
|
@@ -5699,7 +6047,7 @@ async function readEnabledIds(store, published) {
|
|
|
5699
6047
|
* @returns {Function} 依次调用三个 `off()` 的注销回调。
|
|
5700
6048
|
*/
|
|
5701
6049
|
function registerProviderRoute(ctx, wiring) {
|
|
5702
|
-
const { settings, providerStore, publisher, reconcile, inference, logger } = wiring;
|
|
6050
|
+
const { settings, providerStore, publisher, reconcile, inference, rosterSeenStore, logger } = wiring;
|
|
5703
6051
|
/**
|
|
5704
6052
|
* 组装一份 §9 响应形状。GET 与成功后的 POST 共用,所以两个方向不可能对「当前
|
|
5705
6053
|
* 状态是什么」说两套话。
|
|
@@ -5727,6 +6075,7 @@ function registerProviderRoute(ctx, wiring) {
|
|
|
5727
6075
|
const roster = rosterWithAvailability(entries, publisher.state.unavailableIds ?? [], settings.maxTokensOverrides);
|
|
5728
6076
|
const allowedList = parseAllowedList(enabledIds);
|
|
5729
6077
|
const modelCount = roster.length;
|
|
6078
|
+
const newModelIds = rosterSeenStore === void 0 ? [] : await optional(rosterSeenStore.fresh(), []);
|
|
5730
6079
|
return {
|
|
5731
6080
|
ok: true,
|
|
5732
6081
|
enabled: resolveSwitchEnabled(panelSwitch, settings.registerProvider),
|
|
@@ -5739,6 +6088,7 @@ function registerProviderRoute(ctx, wiring) {
|
|
|
5739
6088
|
allowed: allowedList.mode,
|
|
5740
6089
|
enabledIds,
|
|
5741
6090
|
contextWindow,
|
|
6091
|
+
newModelIds,
|
|
5742
6092
|
roster,
|
|
5743
6093
|
...catalogError !== null ? { catalogError } : {},
|
|
5744
6094
|
...extra
|
|
@@ -5882,12 +6232,11 @@ function registerProviderRoute(ctx, wiring) {
|
|
|
5882
6232
|
* 「给 agent 一个出图工具」。两者的开关、状态文件、候选集合都不同,合并会让
|
|
5883
6233
|
* 「关掉出图」波及 provider 的注册时序(详见 draw-store.ts 的头注)。
|
|
5884
6234
|
*
|
|
5885
|
-
*
|
|
5886
|
-
*
|
|
5887
|
-
*
|
|
5888
|
-
*
|
|
5889
|
-
*
|
|
5890
|
-
* 以为开关坏了。
|
|
6235
|
+
* 开关翻转**当场生效**:保存成功后本路由 await 挂载控制器的 `syncDraw()`(注册
|
|
6236
|
+
* 或注销),再把**收敛之后**的真实状态写进响应。所以面板拿到的不是「已保存、等
|
|
6237
|
+
* 重启」,而是「已挂上」或「挂载失败的原因」。`syncDraw` 由装配层注入(`index.ts`
|
|
6238
|
+
* 把同一个控制器交给路由与挂载),本路由不自己建——否则「注册」与「注销」会各持
|
|
6239
|
+
* 一份状态。
|
|
5891
6240
|
*
|
|
5892
6241
|
* peer-free:不 import 任何 Host peer。
|
|
5893
6242
|
* @module dsh-connect-modelscope-token-plan/routes/draw
|
|
@@ -5926,7 +6275,7 @@ async function readEntries(inference) {
|
|
|
5926
6275
|
* @returns {Function} 注销回调。
|
|
5927
6276
|
*/
|
|
5928
6277
|
function registerDrawRoute(ctx, wiring) {
|
|
5929
|
-
const { settings, drawStore, inference, logger } = wiring;
|
|
6278
|
+
const { settings, drawStore, inference, syncDraw, logger } = wiring;
|
|
5930
6279
|
/**
|
|
5931
6280
|
* 组装响应。GET 与写成功后的响应共用,两个方向不可能对「当前状态」说两套话。
|
|
5932
6281
|
* @param {object} [extra] - 追加/覆盖字段。
|
|
@@ -5952,15 +6301,15 @@ function registerDrawRoute(ctx, wiring) {
|
|
|
5952
6301
|
effectiveSource: choice.source,
|
|
5953
6302
|
absentReason: drawToolNote(),
|
|
5954
6303
|
/**
|
|
5955
|
-
*
|
|
6304
|
+
* 开关为开、但工具此刻还没挂上(正在收敛,或收敛失败)。
|
|
5956
6305
|
*
|
|
5957
|
-
*
|
|
5958
|
-
*
|
|
5959
|
-
*
|
|
6306
|
+
* 现在开关翻转是**当场**收敛的(见上面的 `syncDraw`),所以正常保存后这一位
|
|
6307
|
+
* 就是 false。它仍然有用:GET 与进程刚启动、还没跑到第一次 sync 时仍可能为
|
|
6308
|
+
* true,面板靠它区分「暂时没挂」与「挂上了」。
|
|
5960
6309
|
*
|
|
5961
|
-
* 注意判据不是「有缺席原因」:缺席原因非 null
|
|
5962
|
-
*
|
|
5963
|
-
*
|
|
6310
|
+
* 注意判据不是「有缺席原因」:缺席原因非 null 表示收敛已尝试且失败(面板会
|
|
6311
|
+
* 优先显示那个原因),而「还没跑完」时 absentReason 为 null。见
|
|
6312
|
+
* `draw-tool-state.ts` 文件头。
|
|
5964
6313
|
*/
|
|
5965
6314
|
mountPending: resolveSwitchEnabled(panelSwitch, settings.drawEnabled) === true && drawToolMounted() !== true,
|
|
5966
6315
|
preferredInCatalog: preferredModelId === null ? null : catalog.ok ? candidates.some((row) => row.id === preferredModelId) : null,
|
|
@@ -6010,6 +6359,7 @@ function registerDrawRoute(ctx, wiring) {
|
|
|
6010
6359
|
try {
|
|
6011
6360
|
if (hasEnabled) await drawStore.saveEnabled(body.enabled);
|
|
6012
6361
|
if (body.modelId !== void 0) await drawStore.saveModelId(body.modelId);
|
|
6362
|
+
if (hasEnabled) await syncDraw?.();
|
|
6013
6363
|
} catch (error) {
|
|
6014
6364
|
respondError(response, error);
|
|
6015
6365
|
return;
|
|
@@ -6031,6 +6381,7 @@ function registerDrawRoute(ctx, wiring) {
|
|
|
6031
6381
|
}));
|
|
6032
6382
|
try {
|
|
6033
6383
|
await drawStore.reset();
|
|
6384
|
+
await syncDraw?.();
|
|
6034
6385
|
} catch (error) {
|
|
6035
6386
|
respondError(response, error);
|
|
6036
6387
|
return;
|
|
@@ -6213,7 +6564,7 @@ function registerRoutes(ctx, wiring) {
|
|
|
6213
6564
|
*
|
|
6214
6565
|
* **出图工具**
|
|
6215
6566
|
* - `draw.ts` 协议纯逻辑半边(peer-free)
|
|
6216
|
-
* - `draw-register.ts`
|
|
6567
|
+
* - `draw-register.ts` 工具挂载控制器(开关翻转现挂现卸)
|
|
6217
6568
|
* - `draw-runtime.ts` 运行时(两段式:提交 → 轮询)
|
|
6218
6569
|
* - `draw-jobs.ts` 作业表(本次挂载内有效)
|
|
6219
6570
|
* - `draw-tool-state.ts` 缺席原因 + 本次挂载注册位(内存)
|
|
@@ -6272,6 +6623,10 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
6272
6623
|
profile,
|
|
6273
6624
|
logger: ctx.logger
|
|
6274
6625
|
});
|
|
6626
|
+
const rosterSeenStore = createFileRosterSeenStore({
|
|
6627
|
+
profile,
|
|
6628
|
+
logger: ctx.logger
|
|
6629
|
+
});
|
|
6275
6630
|
const publisher = createProviderPublisher({
|
|
6276
6631
|
settings,
|
|
6277
6632
|
panelSwitch: () => providerStore.enabled(),
|
|
@@ -6294,6 +6649,7 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
6294
6649
|
inference,
|
|
6295
6650
|
publisher,
|
|
6296
6651
|
usageEvents: () => usageStore.events(),
|
|
6652
|
+
rosterSeenStore,
|
|
6297
6653
|
logger: ctx.logger
|
|
6298
6654
|
});
|
|
6299
6655
|
const wiring = {
|
|
@@ -6306,16 +6662,21 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
6306
6662
|
drawStore,
|
|
6307
6663
|
drawJobs,
|
|
6308
6664
|
usagePrefsStore,
|
|
6665
|
+
rosterSeenStore,
|
|
6309
6666
|
publisher,
|
|
6310
6667
|
reconcile,
|
|
6311
6668
|
logger: ctx.logger
|
|
6312
6669
|
};
|
|
6313
|
-
const
|
|
6670
|
+
const drawMount = createDrawToolMount(ctx, wiring, deps);
|
|
6671
|
+
const offs = registerRoutes(ctx, {
|
|
6672
|
+
...wiring,
|
|
6673
|
+
syncDraw: () => drawMount.sync()
|
|
6674
|
+
});
|
|
6314
6675
|
ctx.effect(() => {
|
|
6315
6676
|
reconcile().catch((error) => {
|
|
6316
6677
|
ctx.logger?.warn?.(`${name}: mount reconcile failed (${redactSecrets(errMsg(error))})`);
|
|
6317
6678
|
});
|
|
6318
|
-
|
|
6679
|
+
drawMount.sync();
|
|
6319
6680
|
const timer = setInterval(() => {
|
|
6320
6681
|
reconcile().catch((error) => {
|
|
6321
6682
|
ctx.logger?.warn?.(`${name}: scheduled reconcile failed (${redactSecrets(errMsg(error))})`);
|
|
@@ -6323,6 +6684,7 @@ function apply(ctx, config = {}, deps = {}) {
|
|
|
6323
6684
|
}, settings.pollSeconds * 1e3);
|
|
6324
6685
|
return () => {
|
|
6325
6686
|
clearInterval(timer);
|
|
6687
|
+
drawMount.dispose();
|
|
6326
6688
|
resetDrawToolNote();
|
|
6327
6689
|
drawJobs.dispose();
|
|
6328
6690
|
publisher.dispose();
|