dsh-desktop-statusbar 2.1.2 → 2.2.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/README.en.md +11 -9
- package/README.md +11 -9
- package/docs/settings.en.png +0 -0
- package/docs/settings.png +0 -0
- package/lib/balance-api.js +49 -1
- package/lib/client.js +1288 -191
- package/lib/index.js +361 -69
- package/lib/release-age.js +127 -8
- package/package.json +3 -2
package/lib/release-age.js
CHANGED
|
@@ -109,6 +109,73 @@ export function unionSelectors(selectors) {
|
|
|
109
109
|
return versions.join(' || ')
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 本插件在豁免列表里**最多保留几个版本**。
|
|
114
|
+
*
|
|
115
|
+
* 为什么要裁:早先的实现是只追加不清理,每更新一次就往同一条规则里多塞一个版本,
|
|
116
|
+
* 于是磁盘上长成 `2.0.0 || 2.0.1 || 2.1.0 || 2.1.1 || 2.1.2 || …`。它不会让安装失败
|
|
117
|
+
* (pnpm 认 `||` 并集),但有两个实际坏处:
|
|
118
|
+
* · 规则无限变长,用户看不懂这一行在干什么;
|
|
119
|
+
* · 更要紧的是当时还只保留**最早那条**,一旦某次真的写成两条同名规则,
|
|
120
|
+
* pnpm 只认第一条 —— 新版本会被 24 小时发布年龄挡下。
|
|
121
|
+
*
|
|
122
|
+
* 保留 2 个(本次目标 + 上一次)而不是只留 1 个:用户升级后发现新版有问题,
|
|
123
|
+
* 想退回上一版时,上一版仍在豁免里,回滚不会被发布年龄策略拦。
|
|
124
|
+
* 更老的版本没有再回滚的现实意义(跨大版本回滚本就要手工处理配置/台账)。
|
|
125
|
+
*/
|
|
126
|
+
export const MAX_EXEMPT_VERSIONS = 2;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 从一堆版本号里挑出该保留的:**目标版本必留**,再按出现顺序留最近的若干个。
|
|
130
|
+
*
|
|
131
|
+
* 不假设输入是升序(`||` 里的顺序是历史写入顺序,早期实现还可能乱序),
|
|
132
|
+
* 所以这里按**语义版本**排序后取最大的几个 —— 对于「最近」这个语义,
|
|
133
|
+
* 版本号大小比写入顺序更可靠。
|
|
134
|
+
*
|
|
135
|
+
* @param {string[]} versions 已有的版本号(可能乱序、可能重复)
|
|
136
|
+
* @param {string} target 本次要放行的版本
|
|
137
|
+
* @param {number} [keep] 最多保留几个
|
|
138
|
+
* @returns {string[]} 要保留的版本;目标版本排在最后(书写顺序:旧 → 新)
|
|
139
|
+
*/
|
|
140
|
+
export function keepRecentVersions(versions, target, keep = MAX_EXEMPT_VERSIONS) {
|
|
141
|
+
const all = []
|
|
142
|
+
const push = (one) => {
|
|
143
|
+
const text = String(one).trim()
|
|
144
|
+
if (text.length > 0 && all.includes(text) !== true) all.push(text)
|
|
145
|
+
}
|
|
146
|
+
versions.forEach(push)
|
|
147
|
+
push(target)
|
|
148
|
+
const limit = keep > 0 ? keep : 1
|
|
149
|
+
if (all.length <= limit) return all
|
|
150
|
+
/* 语义版本比较:把 x.y.z(-pre) 拆成数字段;解析不出数字的(理论上不会有,
|
|
151
|
+
ensureReleaseAgeExclude 已校验过版本形态)当作 0 处理,排在最后 */
|
|
152
|
+
const keyOf = (one) => String(one).split('-')[0].split('.').map((part) => Number(part) || 0)
|
|
153
|
+
const sorted = all.slice().sort((a, b) => {
|
|
154
|
+
const ka = keyOf(a)
|
|
155
|
+
const kb = keyOf(b)
|
|
156
|
+
for (let i = 0; i < 3; i += 1) {
|
|
157
|
+
if (ka[i] !== kb[i]) return ka[i] - kb[i]
|
|
158
|
+
}
|
|
159
|
+
return 0
|
|
160
|
+
})
|
|
161
|
+
/* 从最大的往下取 limit 个,再按升序写回去(读起来自然是旧 → 新) */
|
|
162
|
+
const picked = sorted.slice(Math.max(0, sorted.length - limit))
|
|
163
|
+
/* 目标版本必须在里面 —— 极端情况下(目标版本号小于已保留的那些)也要保住它 */
|
|
164
|
+
if (picked.includes(target) !== true) {
|
|
165
|
+
picked.sort((a, b) => {
|
|
166
|
+
const ka = keyOf(a)
|
|
167
|
+
const kb = keyOf(b)
|
|
168
|
+
for (let i = 0; i < 3; i += 1) {
|
|
169
|
+
if (ka[i] !== kb[i]) return ka[i] - kb[i]
|
|
170
|
+
}
|
|
171
|
+
return 0
|
|
172
|
+
})
|
|
173
|
+
picked.shift()
|
|
174
|
+
picked.push(target)
|
|
175
|
+
}
|
|
176
|
+
return picked.filter((one) => one.length > 0)
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
/** 去掉包裹标量的引号(成对才剥,与 parseExcludeBlock 同规则)。 */
|
|
113
180
|
function unquoteScalar(value) {
|
|
114
181
|
const text = String(value).trim()
|
|
@@ -269,6 +336,22 @@ function locateExcludeBlock(source) {
|
|
|
269
336
|
*/
|
|
270
337
|
export function mergeExcludeIntoYaml(text, name, version, eol = '\n') {
|
|
271
338
|
const source = String(text)
|
|
339
|
+
/* ------------------------------------------------------------------ 安全阀
|
|
340
|
+
这个函数改的是**共享文件** `pnpm-workspace.yaml`:写坏了不只是本插件装不上,
|
|
341
|
+
整个 profile 里所有插件都起不来(YAML `duplicated mapping key` → pnpm 拒绝解析)。
|
|
342
|
+
|
|
343
|
+
最坏的一种输入:文件里**已经有两个** `minimumReleaseAgeExclude:` 键
|
|
344
|
+
(历史遗留,或两个插件各自追加过)。`locateExcludeBlock` 只定位第一个块,
|
|
345
|
+
替换后第二个原样留着 —— 输出仍是两个同名键,照样把 profile 写坏。
|
|
346
|
+
|
|
347
|
+
这种状态下唯一安全的动作是**什么都不做**:返回 ok:false,调用方
|
|
348
|
+
(ensureReleaseAgeExclude)会把它转成结构化失败,安装流程继续走 ——
|
|
349
|
+
profile 没显式设这个策略时 pnpm 自己会豁免精确版本,安装本来就能成。
|
|
350
|
+
宁可这次豁免没生效,也绝不能把一个本来还能用的 profile 写坏。 */
|
|
351
|
+
const keyCount = (source.match(/^[ \t]*minimumReleaseAgeExclude:/gm) || []).length
|
|
352
|
+
if (keyCount > 1) {
|
|
353
|
+
return { ok: false, text: source, changed: false, reason: 'duplicate-key' }
|
|
354
|
+
}
|
|
272
355
|
const found = locateExcludeBlock(source)
|
|
273
356
|
|
|
274
357
|
if (found === null) {
|
|
@@ -281,11 +364,22 @@ export function mergeExcludeIntoYaml(text, name, version, eol = '\n') {
|
|
|
281
364
|
const items = parseInlineValue(found.inline).concat(parseExcludeBlock(found.block))
|
|
282
365
|
const byName = new Map()
|
|
283
366
|
const order = []
|
|
367
|
+
/* 每个包名对应的一组「原始行」:块风格解析出来的条目带着逐字节原样的 `line`,
|
|
368
|
+
flow 风格的条目没有(它本来就要从 flow 重写成块),记 null。
|
|
369
|
+
别人的规则能用原始行时**必须原样写回** —— 早先每个条目都重新生成一遍,
|
|
370
|
+
结果 `dshmarket@1.66.22 || 1.66.32` 被加上引号变成 `'dshmarket@1.66.22 || 1.66.32'`。
|
|
371
|
+
语义虽然等价,但那是**别人的配置**,我们没有任何理由去改写它(用户的 diff 会脏)。 */
|
|
372
|
+
const rawLines = new Map()
|
|
284
373
|
for (const item of items) {
|
|
285
374
|
if (item.kind !== 'entry') continue
|
|
286
375
|
const groupName = nameOfSelector(item.selector)
|
|
287
|
-
if (byName.has(groupName) !== true) {
|
|
376
|
+
if (byName.has(groupName) !== true) {
|
|
377
|
+
byName.set(groupName, [])
|
|
378
|
+
rawLines.set(groupName, [])
|
|
379
|
+
order.push(groupName)
|
|
380
|
+
}
|
|
288
381
|
byName.get(groupName).push(item.selector)
|
|
382
|
+
rawLines.get(groupName).push(typeof item.line === 'string' ? item.line : null)
|
|
289
383
|
}
|
|
290
384
|
|
|
291
385
|
const mine = byName.get(name) ?? []
|
|
@@ -297,14 +391,22 @@ export function mergeExcludeIntoYaml(text, name, version, eol = '\n') {
|
|
|
297
391
|
if (mine.length === 1 && versionOfSelector(mine[0]) === '') {
|
|
298
392
|
return { ok: true, text: source, changed: false, reason: null }
|
|
299
393
|
}
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
394
|
+
/* 本插件的规则要**裁剪**:只留目标版本 + 最近一个(见 MAX_EXEMPT_VERSIONS)。
|
|
395
|
+
早先只追加不清理,历史版本会永久累积成一条越来越长的 `||` 列表。 */
|
|
396
|
+
const keptVersions = keepRecentVersions(versions, version)
|
|
397
|
+
const keptSelector = name + '@' + keptVersions.join(' || ')
|
|
398
|
+
if (mine.length === 1) {
|
|
399
|
+
const already = versionOfSelector(mine[0]).split('||').map((one) => one.trim()).filter((one) => one.length > 0)
|
|
400
|
+
/* 目标版本已在、且集合没变(不需要裁剪)→ 一个字节都不动,保持幂等 */
|
|
401
|
+
if (already.length === keptVersions.length && keptVersions.every((one) => already.includes(one))) {
|
|
402
|
+
return { ok: true, text: source, changed: false, reason: null }
|
|
403
|
+
}
|
|
304
404
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
405
|
+
/* 用裁剪后的版本集合替换该包名的条目(不是追加) */
|
|
406
|
+
byName.set(name, [keptSelector])
|
|
407
|
+
/* 我们这条要**重新生成**(没有可原样复用的原始行),所以原始行记 null */
|
|
408
|
+
rawLines.set(name, [null])
|
|
409
|
+
if (order.indexOf(name) === -1) order.push(name)
|
|
308
410
|
|
|
309
411
|
/* 重排这个块:每个包名一条,位置取它首次出现的地方;注释原样保留在最前。 */
|
|
310
412
|
const comments = items.filter((item) => item.kind === 'comment')
|
|
@@ -312,6 +414,23 @@ export function mergeExcludeIntoYaml(text, name, version, eol = '\n') {
|
|
|
312
414
|
for (const item of comments) lines.push(item.line)
|
|
313
415
|
for (const groupName of order) {
|
|
314
416
|
const selectors = byName.get(groupName) ?? []
|
|
417
|
+
const raws = rawLines.get(groupName) ?? []
|
|
418
|
+
/* 我们自己的那条:用裁剪后的版本集合重新生成 */
|
|
419
|
+
if (groupName === name) {
|
|
420
|
+
const merged = versionOfSelector(keptSelector)
|
|
421
|
+
const selector = merged.length === 0 ? groupName : groupName + '@' + merged
|
|
422
|
+
lines.push(' - ' + (selector.startsWith('@') || selector.includes(' || ') ? "'" + selector.replace(/'/g, "''") + "'" : selector))
|
|
423
|
+
continue
|
|
424
|
+
}
|
|
425
|
+
/* 别人的规则:只要它还是**一条且原始行可用**,就原样写回。
|
|
426
|
+
唯一例外是缩进 —— 重排后整个块统一用两空格缩进,零缩进的旧条目必须跟着对齐,
|
|
427
|
+
否则同一个序列里混着两种缩进,YAML 直接解析失败(实测过)。
|
|
428
|
+
所以这里只动**前导空白**,`-` 之后的正文(含引号、`||`、大小写)一个字节都不改。 */
|
|
429
|
+
if (raws.length === 1 && typeof raws[0] === 'string' && raws[0].length > 0) {
|
|
430
|
+
const body = raws[0].replace(/^[ \t]*/, '')
|
|
431
|
+
lines.push(' ' + body)
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
315
434
|
const merged = unionSelectors(selectors)
|
|
316
435
|
/* pnpm 把同名的多条并成一条;只有一个版本时保持 `name@version` 的朴素写法 */
|
|
317
436
|
const selector = merged.length === 0 ? groupName : groupName + '@' + merged
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-desktop-statusbar",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "把 DSH 桌面端对话区底部的统计行换成可配置状态栏:10 个字段可选可排序,按官方峰谷口径估算费用,支持自定义模型单价与账户余额。",
|
|
6
6
|
"icon": "icon.svg",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"engines": {
|
|
9
|
-
"node": "^22.19.0 || >=24.0.0"
|
|
9
|
+
"node": "^22.19.0 || >=24.0.0",
|
|
10
|
+
"dsh": ">=0.2.0-rc.2"
|
|
10
11
|
},
|
|
11
12
|
"author": "北浮森屿 (https://github.com/raphael-y7)",
|
|
12
13
|
"repository": {
|