dsh-skill-browser 1.9.9 → 1.9.10
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 +24 -0
- package/README-es.md +1 -1
- package/README-hi.md +1 -1
- package/README-pt.md +1 -1
- package/README-zh.md +4 -4
- package/README.md +18 -3
- package/lib/client.js +16 -10
- package/lib/index.js +1 -1
- package/locale/en.json +1 -1
- package/locale/zh.json +1 -1
- package/package.json +4 -3
- package/scripts/audit-contrast.mjs +215 -0
- package/scripts/check-bom.mjs +51 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.9.10 — 2026-10-25
|
|
4
|
+
|
|
5
|
+
### 修复
|
|
6
|
+
- **品牌渐变底文字对比度按「最差端点」判色**:原来用渐变中点算白字对比度,但 `linear-gradient` 实际铺满 g1..g2 整条 ramp,浅色皮肤(04 全息幻彩 / 051 / 113–127 等)的中点达标而浅端只有 3.7–4.4:1。现改为 `worst(ink, g1, g2) = min(cr(ink,g1), cr(ink,g2))`,两个端点都 ≥4.6:1 才用白字,否则翻成近黑或按 0.05 步长压深整条渐变。**128 皮肤 × 两端点 = 256 项全部 ≥4.5:1。**
|
|
7
|
+
|
|
8
|
+
### 文档(与实装功能对齐)
|
|
9
|
+
- **`locale/zh.json` / `locale/en.json` 的 `meta.description` 重写**:原描述只写到「一级/二级分类 + 启停 + 失效台账」,完全没提 v1.9.0 之后新增的自定义分组、树状/平铺、版本号徽章、调用台账看板、⚡ 热重载、全皮肤自适应配色 —— 与实装功能严重不符。现按真实功能重写。
|
|
10
|
+
- **`README.md`(主 README,英文为主)补全 v1.9.0–v1.9.9 全部功能**:新增「自定义分组」「树状 / 平铺双模式」「版本号徽章」「调用台账看板」「热重载」「性能」「全皮肤自适应配色」七节中文说明 + 对应 4 条英文 Highlights;API 表补 `/rev`、`/reload`、`/groups`(含 11 个 action)、`/ledger/stats`(含 `gran`/`sort` 参数),`/skills` 注明返回 `version` 字段。原来 API 表完全没有分组、统计、热重载这三组接口。
|
|
11
|
+
- **`README-zh.md` 数字口径修正**:AI 分组的大类数公式写的是「技能数 ÷ 8,6~18 组、单组上限 12 个」,而 `lib/groups.js` 实际是 `Math.max(3, Math.min(16, Math.ceil(list.length / 12)))`(技能数 ÷ 12、3~16 个)且 `addSub()` 按 10 个切片、`CLASSIFY_SYSTEM` 要求「每个子类最多 10 个技能」。三处已全部改成 10 / ÷12 / 3~16,第三遍兜底拆组的「超过 12 个」也改为「超过 10 个」。
|
|
12
|
+
- **`package.json` `description` 从「逐版本罗列」改为一份统一功能描述**:原来是把 v1.9.0→v1.9.10 每个版本的新增点串起来,越写越长、且和市场卡片/README 口径各不相同。现在 `package.json` description、`locale/zh.json` `meta.description`、`locale/en.json` `meta.description`、五语言 README 首段**全部指向同一份口径**(含「分组只改视图不动技能文件」「无版本显示暂未编版本号」两条容易被误解的点)。npm 市场的卡片描述取自 `package.json` description,插件管理器/设置页的描述取自 `locale/*.json` 的 `meta`,两边现在一字不差。
|
|
13
|
+
- **`README-es.md` / `README-pt.md` / `README-hi.md` 首段补齐**:原来这三份只有一个 8 行的空壳(只有标题 + 一句旧描述 + 一个跳转链接),连分组、版本号、台账看板、热重载、皮肤自适应都没提。现在各自有一段完整的本地语言功能描述,标题统一为 `Skill Browser`。
|
|
14
|
+
|
|
15
|
+
### 校验
|
|
16
|
+
- **`scripts/audit-contrast.mjs`(新写,随包发布,唯一权威口径)**:128 皮肤 × 18 组前景/背景组合 = **2304 项,0 项不达标**。与旧临时脚本的差别:
|
|
17
|
+
- 语义变量直接从真实调用链取 —— 切皮肤后读 `<style id="dsb-skin-style">` 的 `textContent`,不另存一份「期望值」,杜绝审计与实现各算一套的假绿。
|
|
18
|
+
- 背景一律取**实际叠层色**:`card = chipBg over panel`、`surface = --dsb-surface`(吸顶表头不透明底实算值)。
|
|
19
|
+
- 渐变底按**两个端点分别校验**:品牌渐变 `--dsb-onbrand-bg1/2`、分组大类行 `--dsb-tile1/2`(叠 `--dsb-shade` 后)。只测中点会漏掉浅端。
|
|
20
|
+
- 踩过的坑写进脚本注释:DOM stub 的 `localStorage` 必须可读写,否则 `SKIN.setSkin(id)` 记不住,128 款皮肤全按 01 号深色皮肤算 → 20 款浅色皮肤集体假报不达标(首轮就误报 200 项)。
|
|
21
|
+
- `audit_onbrand.mjs`:128 皮肤逐款取 `--dsb-onbrand-bg1/2` 与 `--dsb-onbrand`,两端点对比度 **0 项 < 4.5:1**。
|
|
22
|
+
- `doc_audit2.mjs` / `doc_audit3.mjs` / `route_audit.mjs`:README-zh 16 项功能词全覆盖;README.md 10 项新功能词 + 4 个新路由全在位;实装 17 条路由与文档逐一对应。
|
|
23
|
+
- `scripts/check-bom.mjs`:37 个文本文件 **0 BOM**。
|
|
24
|
+
|
|
25
|
+
> **口径统一**:此前各版本 CHANGELOG 出现过 `14 组 1792 项`、`17 组 2176 项` 两种数字,那是各阶段临时脚本的覆盖范围(未含吸顶表头底、未拆渐变端点)。自 v1.9.10 起以 `scripts/audit-contrast.mjs` 的 **18 组 2304 项**为准,README-zh / README.md / package.json 描述均已对齐该数字。
|
|
26
|
+
|
|
3
27
|
## 1.9.9 — 2026-10-25
|
|
4
28
|
|
|
5
29
|
### 新增
|
package/README-es.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill Browser
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Panel de bola flotante que convierte tu biblioteca de skills local en un activo navegable, buscable y gobernable: categorías de uno/dos niveles más grupos personalizados (vistas de árbol y plana, grupos manuales, agrupación instantánea por palabras clave, agrupación automática con IA y portadas generadas por IA — agrupar solo cambia la vista y nunca mueve archivos de skills), insignias de versión por skill con reserva "aún sin versión", habilitar/deshabilitar por tarjeta / por categoría / toda la biblioteca con copias de seguridad automáticas, y visor completo de SKILL.md. Un panel de libro de llamadas añade rangos de fechas, granularidad día/semana/mes/año, clasificaciones, gráfico de tendencia y detalles de fallos, alimentado automáticamente por el evento nativo tools/result. Recarga en caliente con un clic, 128 skins adaptables al contraste, raíz de skills configurable y bola arrastrable y restaurable.
|
|
4
4
|
|
|
5
5
|
> Full documentation: [English README](./README.md)
|
|
6
6
|
|
package/README-hi.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill Browser
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> फ्लोटिंग-बॉल पैनल जो आपकी लोकल स्किल लाइब्रेरी को ब्राउज़ करने, खोजने और प्रबंधित करने योग्य संपत्ति बनाता है: एक/दो-स्तरीय श्रेणियाँ और कस्टम समूह (ट्री/फ्लैट व्यू, मैन्युअल समूह, तुरंत कीवर्ड ग्रुपिंग, AI ऑटो-ग्रुपिंग और AI कवर — ग्रुपिंग केवल व्यू बदलती है, स्किल फ़ाइलें कभी नहीं हिलतीं), प्रति-स्किल वर्ज़न बैज जिसमें "अभी वर्ज़न नहीं" फ़ॉलबैक है, प्रति-कार्ड / प्रति-श्रेणी / पूरी लाइब्रेरी के लिए स्वचालित बैकअप के साथ चालू-बंद, और पूरा SKILL.md व्यूअर। कॉल-लेजर डैशबोर्ड में तिथि सीमा, दिन/सप्ताह/माह/वर्ष ग्रैन्युलैरिटी, रैंकिंग, ट्रेंड चार्ट और विफलता विवरण जुड़ते हैं, जो होस्ट के tools/result इवेंट से स्वचालित रूप से भरते हैं। एक-क्लिक हॉट रीलोड, सभी 128 स्किन कंट्रास्ट-अनुकूल, कॉन्फ़िगर करने योग्य स्किल रूट, खींचने योग्य और रीसेट करने योग्य बॉल।
|
|
4
4
|
|
|
5
5
|
> Full documentation: [English README](./README.md)
|
|
6
6
|
|
package/README-pt.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Skill Browser
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Painel de bola flutuante que transforma sua biblioteca local de skills em um ativo navegável, pesquisável e gerenciável: categorias de um/dois níveis mais grupos personalizados (visões em árvore e plana, grupos manuais, agrupamento instantâneo por palavras-chave, agrupamento automático com IA e capas geradas por IA — agrupar só altera a visão e nunca move arquivos de skills), selos de versão por skill com fallback "ainda sem versão", habilitar/desabilitar por cartão / por categoria / toda a biblioteca com backups automáticos, e visualizador completo de SKILL.md. Um painel de registro de chamadas adiciona intervalos de datas, granularidade dia/semana/mês/ano, rankings, gráfico de tendência e detalhes de falhas, alimentado automaticamente pelo evento nativo tools/result. Recarga a quente com um clique, 128 skins adaptáveis ao contraste, raiz de skills configurável e bola arrastável e restaurável.
|
|
4
4
|
|
|
5
5
|
> Full documentation: [English README](./README.md)
|
|
6
6
|
|
package/README-zh.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# dsh-skill-browser
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> 悬浮球打开面板,把本机技能库变成可浏览、可检索、可治理的资产:两级分类 + 自定义分组(树状/平铺双模式、手动建组、⚡ 关键词分组、🤖 AI 自动分组与 AI 封面,**分组只改视图,不动技能文件**)、版本号徽章(无版本显示「暂未编版本号」)、单卡/分类/全库启停(自动备份)、SKILL.md 全文查看;内置调用台账看板(起止日期 + 日/周/月/年 + 排行榜 + 趋势图 + 失效明细),监听 `tools/result` 自动登记每次技能调用结果。⚡ 一键热重载,128 款皮肤全量自适应配色,技能库位置可配置,悬浮球可拖拽可复位。
|
|
4
4
|
|
|
5
5
|
> 完整文档以 [English README](./README.md) 为准,本页提供中文导读与关键信息速查。
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
- `--dsb-soft` / `--dsb-line` / `--dsb-hover`:分隔线、悬停底色
|
|
21
21
|
- `--dsb-scheme`:`light` / `dark`,驱动原生 `<input type="date">` 日历弹窗配色
|
|
22
22
|
|
|
23
|
-
切换皮肤后无需刷新,配色随皮肤即时重算。128 款皮肤 ×
|
|
23
|
+
切换皮肤后无需刷新,配色随皮肤即时重算。128 款皮肤 × 18 组前景/背景组合共 2304 项已全量校验,0 项不达标(`scripts/audit-contrast.mjs` 可随时复跑)。
|
|
24
24
|
|
|
25
25
|
> **v1.9.5**:排行榜表头是吸顶的,底色改用语义变量 `--dsb-surface`(行/卡片真实不透明底色),滚动时不会再和行文字重叠。
|
|
26
26
|
>
|
|
@@ -108,9 +108,9 @@ AI 分组与关键词分组是并列的两条路:想快、想稳定就用 ⚡
|
|
|
108
108
|
|
|
109
109
|
v1.9.2 起改为**三遍覆盖式分类**,解决「只分了 Fluent、其他技能全丢进一个杂项筐」的问题:
|
|
110
110
|
|
|
111
|
-
1.
|
|
111
|
+
1. **第一遍**:全量技能交给模型,大类数按规模自动定(技能数 ÷ 12 向上取整,3~16 个,允许 ±2),每个大类下分 1~6 个子类,单个子类上限 10 个技能,提示词明确禁止「通用工具 / 其他 / 杂项 / 综合」这类筐,并要求各领域拆分粒度一致。
|
|
112
112
|
2. **第二遍**:把模型漏掉的技能单独再问一次,并要求避开第一遍已建的分组名,让它们得到真正的语义分组,而不是一个「其他」大筐。
|
|
113
|
-
3.
|
|
113
|
+
3. **第三遍(不调模型)**:仍未覆盖的技能按自身分类标签兜底成组,单个子类超过 10 个技能时自动拆成 `名称 1` / `名称 2`。
|
|
114
114
|
|
|
115
115
|
因此 AI 分组后**不会再出现「未分组」堆积**,全部技能都会落到某个组里。
|
|
116
116
|
|
package/README.md
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
**Turn your skill library into something you can see, search, and govern.**
|
|
6
6
|
|
|
7
|
-
在 DeepSeek Harness(DSH)桌面端 /
|
|
7
|
+
在 DeepSeek Harness(DSH)桌面端 / 网页端里用一个悬浮球打开面板,把本机技能库变成可浏览、可检索、可治理的资产:两级分类 + **自定义分组**(树状/平铺双模式、手动建大类与子类、⚡ 关键词分组、🤖 AI 自动分组与 AI 封面,**分组只是视图归档,绝不移动任何技能文件**)、**版本号徽章**(无版本显示「暂未编版本号」)、单卡 / 分类 / 全库启停(自动备份 `.bak-toggle`)、SKILL.md 全文查看;内置 **调用台账看板**(起止日期 + 日/周/月/年 + 排行榜 + 趋势图 + 失效明细),监听 DSH 官方 `tools/result` 事件自动登记每次技能调用结果。面板 ⚡ 一键热重载,**128 款皮肤全量自适应配色**(对比度实算),技能库位置可配置,悬浮球可拖拽可复位。
|
|
8
|
+
|
|
9
|
+
Open a floating-ball panel inside DeepSeek Harness (DSH) and turn your local skill library into something you can browse, search and govern: one/two-level categories plus **custom groups** (tree/flat views, manual parent/child groups, ⚡ instant keyword grouping, 🤖 AI auto-grouping with AI-generated covers — **grouping is view-only and never moves a skill file**), **version badges** (falling back to "no version yet"), per-card / per-category / library-wide enable-disable with automatic `.bak-toggle` backups, and a full SKILL.md viewer. A **call-ledger dashboard** adds date ranges, day/week/month/year granularity, rankings, a trend chart and failure details, fed automatically by DSH's native `tools/result` event. One-click ⚡ hot reload, **all 128 ball skins contrast-adaptive**, configurable skill root, draggable and resettable ball.
|
|
8
10
|
|
|
9
|
-
Browse your local DSH skill library straight from the GUI, with an **automatic skill-failure ledger**. One floating-ball entry point opens a wall of skill cards — categories, live search, full SKILL.md viewer — plus a ledger page that logs the outcome of every `skill` tool call.
|
|
10
11
|
|
|
11
12
|

|
|
12
13
|
|
|
@@ -26,10 +27,17 @@ Did that skill actually work? The plugin listens to DSH's native `tools/result`
|
|
|
26
27
|
- **悬浮球 🧩**:固定在屏幕角落,点击开关面板;可拖拽换位置(位置记忆),Alt+点击收起成小圆点
|
|
27
28
|
- **技能库位置自己定**:首次使用在面板 ⚙ 设置里选择技能库文件夹(弹系统目录选择框),或手动粘贴路径;未配置时自动探测官方默认根 `~/.dsh/skills`。**插件不含任何预设路径**
|
|
28
29
|
- **一级/二级分类**:按技能目录名模式自动归类(Fluent/CFD 仿真、DSH 工具链、多端协同、Excel/表格、文档处理、图片/视觉、语音/媒体、数据/可视化、开发/技能治理等 15 个一级分类与 Fluent/DSH 域二级分类)
|
|
30
|
+
- **自定义分组(v1.9.0 起,v1.9.8 支持树状/平铺双模式)**:分组栏可手动新建「大类 → 子类」两级分组,也可用「⚡ 关键词分组」(本地正则,零 token、毫秒级、结果确定)或「🤖 AI 自动分组」(读技能清单让模型出方案)自动建组。AI / 关键词都**只出方案不落盘**,方案弹层里确认「替换」或「追加」才写入 `groups.json`。**分组只是视图归档,绝不移动、不重命名任何技能文件。**
|
|
31
|
+
- **树状 / 平铺双模式**:树状模式下每个大类独占一整行,子类常驻缩进挂在下方并用 `├─` / `└─` 树杈连接,一眼看清「Fluent 下面有画网格 / 后处理 / SpaceClaim 几何操作」;平铺模式保留旧版卡片墙。选择持久化到 `localStorage.dsh-skill-browser-gmode`。
|
|
32
|
+
- **版本号徽章(v1.9.0 起)**:技能名右侧显示版本号(优先 SKILL.md frontmatter 的 `version`,其次同目录 `package.json` 的 `version`),没有版本号则显示「暂未编版本号」。
|
|
29
33
|
- **技能启停**(v1.7.0 起):单个技能卡片开关 / 分类级聚合滑块 / 全库批量启停(带确认菜单)。停用即改写该技能 `SKILL.md` frontmatter 的 `disable-model-invocation`,改写前自动备份为 `.bak-toggle`,停用卡灰显并带徽章;随时可一键恢复
|
|
30
34
|
- **中文简介**:技能简介直接取自各技能 SKILL.md frontmatter 的 description(v1.5.0 起插件不内置任何技能名清单/字典,避免泄漏技能库构成,也保证所有人看到的是自己技能库的准确描述)
|
|
31
35
|
- **搜索**:按技能名 / 中文简介 / 原始描述实时过滤
|
|
32
36
|
- **详情**:点击卡片查看 SKILL.md 全文,可一键复制
|
|
37
|
+
- **调用台账看板(v1.9.1 起)**:台账页从「原始流水」升级为看板——起止日期筛选 + 今天/近 7 天/近 30 天/本月/上月/今年/全部七个快捷区间 + 日/周/月/年四种粒度 + 6 张指标卡(调用次数 / 活跃技能 / 活跃天数 / 成功率 / 失败 / 日均)+ 纯 CSS 趋势柱 + 排行榜(可按调用、失败、成功率、最后调用、技能名排序,带版本号与迷你占比条)。原始流水(人工台账 + 自动登记)折叠在下方。
|
|
38
|
+
- **热重载(v1.9.4 起)**:面板头部 ⚡ 按钮调用 `POST /reload`,插件重新加载自己的 host 半代码(`?v=<ts>` 绕开 ESM 缓存)并递增代号,前端轮询 `GET /rev` 等到代号变化后自动刷新界面——**改完代码不用重启桌面端**。
|
|
39
|
+
- **性能(v1.9.6 起)**:技能卡片分片渲染(一次 60 张,滚到底自动追加),原始流水分片渲染(一次 120 行),jsonl 走尾部增量读取,`/ledger/stats` 缓存键并入 jsonl 的 size+mtime,详情页默认只回最近 200 条。
|
|
40
|
+
- **全皮肤自适应配色(v1.9.2 / v1.9.5 / v1.9.9 三轮加固)**:面板所有新增控件(分组栏、版本徽章、台账看板、吸顶表头)颜色全部走语义变量(`--dsb-brand` / `--dsb-dim` / `--dsb-ok` / `--dsb-fail` / `--dsb-warn` / `--dsb-surface` / `--dsb-onbrand` 等),按当前皮肤实际叠层背景实算对比度,128 款皮肤 × 18 组前景/背景组合共 2304 项全部达标。
|
|
33
41
|
- **失效台账**(人工 / 自动分开显示):
|
|
34
42
|
- **👤 人工台账**:解析台账 md 的统计表与失败明细(人工维护区,插件永不改动)
|
|
35
43
|
- **🤖 自动登记**:插件监听 DSH 官方 `tools/result` 事件,自动记录每次 `skill` 工具调用结果——成功记「生效✅」、加载失败记「未生效❌」(含错误信息)
|
|
@@ -46,6 +54,9 @@ Did that skill actually work? The plugin listens to DSH's native `tools/result`
|
|
|
46
54
|
- 🧩 **Floating-ball entry** — pinned to a screen corner, click to toggle the panel; draggable with position memory, Alt+click to collapse to a dot.
|
|
47
55
|
- 📂 **Configurable skill root** — pick the folder via the native directory chooser in ⚙ settings, or paste a path; auto-detects the official `~/.dsh/skills` root when unset. **No hard-coded paths shipped.**
|
|
48
56
|
- 📚 **Two-level categories** — auto-classified from skill directory names (15 top-level categories: Fluent/CFD simulation, DSH toolchain, multi-client collaboration, Excel/spreadsheets, document processing, image/vision, voice/media, data/visualisation, dev/skill governance…).
|
|
57
|
+
- 🗂 **Custom groups** (since v1.9.0; tree/flat dual view since v1.9.8) — create your own two-level `domain → sub` groups by hand, or let **⚡ keyword grouping** (local regex, zero tokens, millisecond, deterministic) / **🤖 AI auto-grouping** build them for you. Both only produce a *plan*; nothing is written to `groups.json` until you confirm **Replace** or **Merge**. **Groups are a view-only archive — no skill file is ever moved or renamed.**
|
|
58
|
+
- 🌳 **Tree / flat views** — tree mode gives every domain its own full-width row with its sub-groups permanently indented underneath, joined by `├─` / `└─` branches, so "Fluent → meshing / post-processing / SpaceClaim geometry" is visible at a glance. Choice persisted in `localStorage.dsh-skill-browser-gmode`.
|
|
59
|
+
- 🏷 **Version badges** (since v1.9.0) — the skill's version sits right next to its name (frontmatter `version`, else the sibling `package.json` `version`), showing "not versioned yet" when absent.
|
|
49
60
|
- 🔘 **Skill enable/disable** (since v1.7.0) — per-card switch, category-level aggregate slider, and whole-library batch toggle with a confirmation menu. Disabling rewrites the skill's `SKILL.md` frontmatter `disable-model-invocation` after backing it up to `.bak-toggle`; disabled cards grey out with a badge and can be restored at any time.
|
|
50
61
|
- 🇨🇳 **Chinese summaries** — taken straight from each skill's `SKILL.md` frontmatter `description`. Since v1.5.0 the plugin ships **no built-in skill dictionary**, so you always see an accurate description of *your own* library.
|
|
51
62
|
- 🔍 **Live search** — filter by skill name, Chinese summary, or raw description.
|
|
@@ -88,10 +99,14 @@ DSH 桌面端内置**供应链安全策略**(minimumReleaseAge):刚发布
|
|
|
88
99
|
| 路由 | 说明 |
|
|
89
100
|
|---|---|
|
|
90
101
|
| `GET /dsh-skill-browser/health` | 健康检查(含 root 与识别来源) |
|
|
102
|
+
| `GET /dsh-skill-browser/rev` | 当前加载代号与版本(热重载探针) |
|
|
103
|
+
| `POST /dsh-skill-browser/reload` | 热重载 host 半代码,返回新代号(v1.9.4) |
|
|
91
104
|
| `GET/POST /dsh-skill-browser/config` | 读取/保存运行时配置(skillsDir、ledgerEnabled) |
|
|
92
|
-
| `GET /dsh-skill-browser/skills[?refresh=1]` |
|
|
105
|
+
| `GET /dsh-skill-browser/skills[?refresh=1]` | 全量技能清单(含 `version` 字段)+ 分类元数据 + 总数 |
|
|
93
106
|
| `GET /dsh-skill-browser/skill?dir=<name>` | 读取某技能 SKILL.md 全文 |
|
|
107
|
+
| `GET/POST /dsh-skill-browser/groups` | 读取/写入两级分组(`groups.json`)。POST 的 `action`:`create` / `rename` / `delete` / `move` / `assign` / `assign-batch` / `ai-classify` / `kw-classify` / `ai-apply` / `ai-cover` / `model-info` |
|
|
94
108
|
| `GET /dsh-skill-browser/ledger[?refresh=1]` | 人工台账表 + 自动登记统计与流水 |
|
|
109
|
+
| `GET /dsh-skill-browser/ledger/stats?from=&to=&gran=&sort=&top=` | 调用台账看板聚合(`gran`=day\|week\|month\|year,`sort`=calls\|fails\|rate\|last\|name) |
|
|
95
110
|
| `POST /dsh-skill-browser/ledger/report` | 手动上报失败 `{ skill, note, kind }` |
|
|
96
111
|
| `POST /dsh-skill-browser/ledger/flush` | 立即落盘队列中的登记 |
|
|
97
112
|
| `GET /dsh-skill-browser/ledger-detail?skill=<name>` | 该技能的人工明细 + 全部自动记录 |
|
package/lib/client.js
CHANGED
|
@@ -3157,23 +3157,29 @@ window.__ModuleLoader__.load({
|
|
|
3157
3157
|
// #12151c) until white text clears 4.6:1, then emit --dsb-onbrand-bg1/2.
|
|
3158
3158
|
// Every active-state background uses those instead of the raw stA/stD.
|
|
3159
3159
|
var g1 = skinRgb(v.stA) || [91, 108, 255, 1], g2 = skinRgb(v.stP || v.stD) || g1;
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3160
|
+
// v1.9.10: judge the gradient by its WORST ENDPOINT, not its midpoint.
|
|
3161
|
+
// A linear-gradient paints g1..g2, so the label sits on the whole ramp;
|
|
3162
|
+
// checking only the midpoint let 04/051/113-127 pass at the middle while
|
|
3163
|
+
// their light end was still 3.7-4.4:1 against white text.
|
|
3164
|
+
var worst = function (ink, a1, a2) {
|
|
3165
|
+
return Math.min(skinCr(ink, a1), skinCr(ink, a2));
|
|
3166
|
+
};
|
|
3167
|
+
var W = [255, 255, 255], K = [18, 21, 28];
|
|
3168
|
+
if (worst(W, g1, g2) >= 4.6) {
|
|
3164
3169
|
d["--dsb-onbrand"] = "#ffffff";
|
|
3165
3170
|
d["--dsb-onbrand-bg1"] = skinHex(g1);
|
|
3166
3171
|
d["--dsb-onbrand-bg2"] = skinHex(g2);
|
|
3167
|
-
} else if (
|
|
3172
|
+
} else if (worst(K, g1, g2) >= 4.6) {
|
|
3168
3173
|
d["--dsb-onbrand"] = "#12151c";
|
|
3169
3174
|
d["--dsb-onbrand-bg1"] = skinHex(g1);
|
|
3170
3175
|
d["--dsb-onbrand-bg2"] = skinHex(g2);
|
|
3171
3176
|
} else {
|
|
3172
|
-
// Neither extreme works → darken the
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
+
// Neither extreme works → darken the ramp until white clears 4.6 at
|
|
3178
|
+
// BOTH ends.
|
|
3179
|
+
var ink = [18, 21, 28, 1], a1 = g1, a2 = g2, tt = 0;
|
|
3180
|
+
for (tt = 0.1; tt <= 0.95; tt += 0.05) {
|
|
3181
|
+
a1 = skinMix(g1, ink, tt); a2 = skinMix(g2, ink, tt);
|
|
3182
|
+
if (worst(W, a1, a2) >= 4.6) break;
|
|
3177
3183
|
}
|
|
3178
3184
|
d["--dsb-onbrand"] = "#ffffff";
|
|
3179
3185
|
d["--dsb-onbrand-bg1"] = skinHex(a1);
|
package/lib/index.js
CHANGED
|
@@ -46,7 +46,7 @@ var listGroups = grMod.listGroups, createGroup = grMod.createGroup,
|
|
|
46
46
|
var keywordClassify = kwMod.keywordClassify;
|
|
47
47
|
|
|
48
48
|
var PREFIX = "/dsh-skill-browser";
|
|
49
|
-
export var VERSION = "1.9.
|
|
49
|
+
export var VERSION = "1.9.10";
|
|
50
50
|
var CACHE_TTL_MS = 30 * 1000;
|
|
51
51
|
var DIAG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "diag.log");
|
|
52
52
|
var CONFIG_FILE = join(homedir(), ".dsh", "data", "dsh-skill-browser", "config.json");
|
package/locale/en.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "Skill Browser",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "One floating ball opens a panel that turns your local skill library into something you can browse, search and govern: one/two-level categories plus custom groups (tree/flat views, manual groups, instant keyword grouping, AI auto-grouping with AI covers — grouping is view-only and never moves skill files), per-skill version badges with a \"no version yet\" fallback, per-card / per-category / library-wide enable-disable with automatic backups, and a full SKILL.md viewer. A call-ledger dashboard adds date ranges, day/week/month/year granularity, rankings, trend chart and failure details, fed automatically by the host's tools/result event. One-click hot reload, all 128 ball skins contrast-adaptive, configurable skill root, draggable and resettable ball."
|
|
5
5
|
}
|
|
6
6
|
}
|
package/locale/zh.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"meta": {
|
|
3
3
|
"title": "技能浏览器",
|
|
4
|
-
"description": "
|
|
4
|
+
"description": "技能浏览器:悬浮球打开面板,把本机技能库变成可浏览、可检索、可治理的资产——两级分类 + 自定义分组(树状/平铺双模式、手动建组、⚡关键词分组、🤖AI 自动分组与 AI 封面,分组只改视图不动技能文件)、版本号徽章(无版本显示「暂未编版本号」)、单卡/分类/全库启停(自动备份)、SKILL.md 全文查看;调用台账看板(起止日期 + 日/周/月/年 + 排行榜 + 趋势图 + 失效明细),监听 tools/result 自动登记每次技能调用结果。⚡ 一键热重载,128 款皮肤全量自适应配色,技能库位置可配置,悬浮球可拖拽可复位。"
|
|
5
5
|
}
|
|
6
6
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-browser",
|
|
3
|
-
"version": "1.9.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.9.10",
|
|
4
|
+
"description": "技能浏览器:悬浮球打开面板,把本机技能库变成可浏览、可检索、可治理的资产——两级分类 + 自定义分组(树状/平铺双模式、手动建组、⚡关键词分组、🤖AI 自动分组与 AI 封面,分组只改视图不动技能文件)、版本号徽章(无版本显示「暂未编版本号」)、单卡/分类/全库启停(自动备份)、SKILL.md 全文查看;调用台账看板(起止日期 + 日/周/月/年 + 排行榜 + 趋势图 + 失效明细),监听 tools/result 自动登记每次技能调用结果。⚡ 一键热重载,128 款皮肤全量自适应配色,技能库位置可配置,悬浮球可拖拽可复位。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"sideEffects": false,
|
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
"README-zh.md",
|
|
32
32
|
"README-es.md",
|
|
33
33
|
"README-pt.md",
|
|
34
|
-
"README-hi.md"
|
|
34
|
+
"README-hi.md",
|
|
35
|
+
"scripts"
|
|
35
36
|
],
|
|
36
37
|
"license": "MIT",
|
|
37
38
|
"keywords": [
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// 全皮肤对比度总审计(唯一权威口径)
|
|
2
|
+
//
|
|
3
|
+
// 用法:node scripts/audit-contrast.mjs [--verbose]
|
|
4
|
+
// 退出码:0 = 全部达标;1 = 有不达标项(明细打印到 stdout)
|
|
5
|
+
//
|
|
6
|
+
// 口径说明:
|
|
7
|
+
// - 皮肤目录(128 款)来自 lib/client.js 内联的 window.__DSH_BALL_SKINS。
|
|
8
|
+
// - 语义变量来自真实调用链:applySkinNow() 把 skinBallCss() + skinPanelCss()
|
|
9
|
+
// 写进 <style id="dsb-skin-style">,本脚本切皮肤后直接从该 style 的
|
|
10
|
+
// textContent 里解析 --dsb-* 实际值 —— 不复制一份「期望值」,
|
|
11
|
+
// 避免审计与生产实现各算一套、出现假绿。
|
|
12
|
+
// - 背景一律取「实际叠层后的颜色」:
|
|
13
|
+
// panel = pbgA || bgA
|
|
14
|
+
// card = chipBg over panel (卡片/排行行真实底色)
|
|
15
|
+
// surface = --dsb-surface (吸顶表头不透明底色,实算值)
|
|
16
|
+
// gtile1/2= shade over tile1/2 (分组大类行是线性渐变,两个端点分别校验)
|
|
17
|
+
// obg1/2 = --dsb-onbrand-bg1/2 (品牌渐变两个端点,只测中点会漏浅端)
|
|
18
|
+
// - 渐变底必须按最差端点判定,这是 v1.9.10 修的 bug:
|
|
19
|
+
// 中点达标但浅端只有 3.7–4.4:1,实际渲染出来白字就是看不清。
|
|
20
|
+
import fs from "node:fs";
|
|
21
|
+
import vm from "node:vm";
|
|
22
|
+
import { createRequire } from "node:module";
|
|
23
|
+
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const CLIENT = join(HERE, "..", "lib", "client.js");
|
|
29
|
+
const VERBOSE = process.argv.includes("--verbose");
|
|
30
|
+
|
|
31
|
+
// ---------- 最小 DOM stub:只为让 client.js 的 factory 跑完并产出 style ----------
|
|
32
|
+
const registry = {};
|
|
33
|
+
function mkEl(tag) {
|
|
34
|
+
const el = {
|
|
35
|
+
tagName: tag, __ev: {}, style: {}, dataset: {}, children: [], options: { length: 0 },
|
|
36
|
+
classList: {
|
|
37
|
+
_s: new Set(),
|
|
38
|
+
add(c) { this._s.add(c); }, remove(c) { this._s.delete(c); },
|
|
39
|
+
toggle(c, v) { v ? this._s.add(c) : this._s.delete(c); }, contains(c) { return this._s.has(c); }
|
|
40
|
+
},
|
|
41
|
+
_html: "", textContent: "", id: "", className: "", title: "",
|
|
42
|
+
getAttribute(k) { return this[k] == null ? null : this[k]; },
|
|
43
|
+
setAttribute(k, v) { if (k === "id") { this.id = v; registry[v] = this; } this[k] = v; },
|
|
44
|
+
removeAttribute(k) { delete this[k]; }, hasAttribute(k) { return this[k] != null; },
|
|
45
|
+
appendChild(c) { this.children.push(c); if (c && c.id) registry[c.id] = c; return c; },
|
|
46
|
+
insertBefore(c) { this.children.push(c); if (c && c.id) registry[c.id] = c; return c; },
|
|
47
|
+
removeChild(c) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); return c; },
|
|
48
|
+
remove() { }, addEventListener(t, f) { (this.__ev[t] = this.__ev[t] || []).push(f); }, removeEventListener() { },
|
|
49
|
+
querySelector() { return null; }, querySelectorAll() { return []; }, closest() { return null; },
|
|
50
|
+
focus() { }, blur() { }, click() { },
|
|
51
|
+
getBoundingClientRect() { return { left: 10, top: 10, right: 110, bottom: 40, width: 100, height: 30 }; },
|
|
52
|
+
contains() { return false; }
|
|
53
|
+
};
|
|
54
|
+
Object.defineProperty(el, "innerHTML", {
|
|
55
|
+
get() { return this._html; },
|
|
56
|
+
set(v) {
|
|
57
|
+
this._html = String(v);
|
|
58
|
+
const re = /id="([A-Za-z0-9_-]+)"/g; let m;
|
|
59
|
+
while ((m = re.exec(this._html))) if (!registry[m[1]]) registry[m[1]] = mkEl("div");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return el;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sandbox = {
|
|
66
|
+
console: { log() { }, warn() { }, error() { } }, // 屏蔽 client.js 的 mounted 日志
|
|
67
|
+
JSON, Math, Date, String, Number, Object, Array, RegExp, parseInt, parseFloat, isNaN, Boolean,
|
|
68
|
+
setTimeout: (f) => { try { f(); } catch (e) { } }, clearTimeout() { },
|
|
69
|
+
setInterval: () => 0, clearInterval() { },
|
|
70
|
+
fetch: () => Promise.reject(new Error("audit: no network")),
|
|
71
|
+
// 必须可读写:SKIN.setSkin(id) 靠 localStorage 记住选择,写不进去就会一直用 01 号皮肤,
|
|
72
|
+
// 导致 128 款皮肤全按深色算 → 20 款浅色皮肤集体假报不达标(踩过)。
|
|
73
|
+
localStorage: {
|
|
74
|
+
_d: {},
|
|
75
|
+
getItem(k) { return this._d[k] ?? null; },
|
|
76
|
+
setItem(k, v) { this._d[k] = String(v); },
|
|
77
|
+
removeItem(k) { delete this._d[k]; }
|
|
78
|
+
},
|
|
79
|
+
CustomEvent: function (n, o) { this.type = n; this.detail = o && o.detail; },
|
|
80
|
+
document: {
|
|
81
|
+
head: mkEl("head"), body: mkEl("body"), documentElement: mkEl("html"),
|
|
82
|
+
createElement: mkEl,
|
|
83
|
+
getElementById: (i) => registry[i] || null,
|
|
84
|
+
querySelector: (s) => (s && s.startsWith("#") ? registry[s.slice(1)] || null : null),
|
|
85
|
+
querySelectorAll: () => [], addEventListener() { }, removeEventListener() { }
|
|
86
|
+
},
|
|
87
|
+
location: { reload() { }, href: "http://127.0.0.1/" },
|
|
88
|
+
navigator: { userAgent: "node" }
|
|
89
|
+
};
|
|
90
|
+
sandbox.window = sandbox; sandbox.self = sandbox; sandbox.globalThis = sandbox;
|
|
91
|
+
sandbox.dispatchEvent = function () { };
|
|
92
|
+
vm.createContext(sandbox);
|
|
93
|
+
|
|
94
|
+
const mod = { id: "dsh-skill-browser", exports: {} };
|
|
95
|
+
sandbox.__ModuleLoader__ = { load(o) { mod.exports = o.factory((n) => require(n)); } };
|
|
96
|
+
vm.runInContext(fs.readFileSync(CLIENT, "utf8"), sandbox);
|
|
97
|
+
mod.exports.apply({
|
|
98
|
+
slots: { register() { }, inject() { }, slot() { } },
|
|
99
|
+
locale: { register() { }, t: (k) => k },
|
|
100
|
+
settings: { register() { } },
|
|
101
|
+
effect(f) { return f && f(); },
|
|
102
|
+
logger: { info() { }, warn() { }, error() { } },
|
|
103
|
+
get: () => null, on() { }
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const SKIN = sandbox.window.__DSH_BALL_SKINS;
|
|
107
|
+
if (!SKIN) { console.error("FATAL: window.__DSH_BALL_SKINS 未挂载,皮肤目录没加载成功"); process.exit(2); }
|
|
108
|
+
const ids = Object.keys(SKIN.BY_ID);
|
|
109
|
+
|
|
110
|
+
// ---------- 颜色工具(与 lib/client.js 的 skinRgb/skinCr 同口径,但独立实现以交叉校验) ----------
|
|
111
|
+
function rgb(c) {
|
|
112
|
+
if (c == null) return null;
|
|
113
|
+
const s = String(c).trim(); let m;
|
|
114
|
+
if ((m = /^#([0-9a-f]{3})$/i.exec(s))) return [parseInt(m[1][0] + m[1][0], 16), parseInt(m[1][1] + m[1][1], 16), parseInt(m[1][2] + m[1][2], 16), 1];
|
|
115
|
+
if ((m = /^#([0-9a-f]{6})$/i.exec(s))) return [parseInt(s.slice(1, 3), 16), parseInt(s.slice(3, 5), 16), parseInt(s.slice(5, 7), 16), 1];
|
|
116
|
+
// 100 款派生皮肤的颜色是 hsl()/hsla() 字符串,不解析 hsl 会全部落空 → 假绿/假红。
|
|
117
|
+
if ((m = /^hsla?\(\s*(-?[\d.]+)(?:deg)?[,\s]+([\d.]+)%[,\s]+([\d.]+)%(?:[,\s/]+([\d.]+))?/i.exec(s))) {
|
|
118
|
+
const h = ((parseFloat(m[1]) % 360) + 360) % 360, sat = parseFloat(m[2]) / 100, l = parseFloat(m[3]) / 100;
|
|
119
|
+
const a = m[4] == null ? 1 : parseFloat(m[4]);
|
|
120
|
+
const cc = (1 - Math.abs(2 * l - 1)) * sat, x = cc * (1 - Math.abs(((h / 60) % 2) - 1)), mm = l - cc / 2;
|
|
121
|
+
let r = 0, g = 0, b = 0;
|
|
122
|
+
if (h < 60) { r = cc; g = x; } else if (h < 120) { r = x; g = cc; } else if (h < 180) { g = cc; b = x; }
|
|
123
|
+
else if (h < 240) { g = x; b = cc; } else if (h < 300) { r = x; b = cc; } else { r = cc; b = x; }
|
|
124
|
+
return [Math.round((r + mm) * 255), Math.round((g + mm) * 255), Math.round((b + mm) * 255), a];
|
|
125
|
+
}
|
|
126
|
+
if ((m = /^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)(?:[,\s/]+([\d.]+))?/i.exec(s))) return [+m[1], +m[2], +m[3], m[4] == null ? 1 : +m[4]];
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const lum = (r) => {
|
|
130
|
+
const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
131
|
+
return 0.2126 * f(r[0]) + 0.7152 * f(r[1]) + 0.0722 * f(r[2]);
|
|
132
|
+
};
|
|
133
|
+
const cr = (a, b) => { const l1 = lum(a), l2 = lum(b); return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); };
|
|
134
|
+
const over = (fg, bg) => { const a = fg[3]; return [fg[0] * a + bg[0] * (1 - a), fg[1] * a + bg[1] * (1 - a), fg[2] * a + bg[2] * (1 - a), 1]; };
|
|
135
|
+
|
|
136
|
+
function varsOf(id) {
|
|
137
|
+
SKIN.setSkin(id); // 触发 __dshBallSkinHooks → applySkinNow() 重算 style
|
|
138
|
+
const css = (registry["dsb-skin-style"] || {}).textContent || "";
|
|
139
|
+
const out = {}; let m;
|
|
140
|
+
const re = /--dsb-([a-z0-9-]+):([^;}]+)/g;
|
|
141
|
+
while ((m = re.exec(css))) out["--dsb-" + m[1]] = m[2].trim();
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 检查表:[说明, 前景来源(变量名或皮肤原生 key), 背景 key, 最低对比度]
|
|
146
|
+
// 4.5 = WCAG AA 正常文字;3.0 = 大号文字 / 非文字图形(图标、趋势柱、迷你条)
|
|
147
|
+
const CHECKS = [
|
|
148
|
+
["正文 ptx / 面板", "ptx", "panel", 4.5],
|
|
149
|
+
["卡片名 dsb-brand / 卡片底", "--dsb-brand", "card", 4.5],
|
|
150
|
+
["排行名 dsb-brand / 行底", "--dsb-brand", "row", 4.5],
|
|
151
|
+
["指标大数字 brand-strong / 卡片底", "--dsb-brand-strong", "card", 3.0],
|
|
152
|
+
["次要小字 dsb-dim / 面板", "--dsb-dim", "panel", 4.5],
|
|
153
|
+
["次要小字 dsb-dim / 卡片底", "--dsb-dim", "card", 4.5],
|
|
154
|
+
["吸顶表头 dsb-dim / 不透明表头底", "--dsb-dim", "surface", 4.5],
|
|
155
|
+
["成功绿 dsb-ok / 卡片底", "--dsb-ok", "card", 3.0],
|
|
156
|
+
["失败红 dsb-fail / 卡片底", "--dsb-fail", "card", 3.0],
|
|
157
|
+
["警告黄 dsb-warn / 卡片底", "--dsb-warn", "card", 3.0],
|
|
158
|
+
["激活态文字 onbrand / 品牌渐变起点", "--dsb-onbrand", "obg1", 4.5],
|
|
159
|
+
["激活态文字 onbrand / 品牌渐变终点", "--dsb-onbrand", "obg2", 4.5],
|
|
160
|
+
["分组卡名 oncard / 分组卡底(渐变起点)", "--dsb-oncard", "gtile1", 4.5],
|
|
161
|
+
["分组卡名 oncard / 分组卡底(渐变终点)", "--dsb-oncard", "gtile2", 4.5],
|
|
162
|
+
["分组计数 oncard-dim / 分组卡底(起点)", "--dsb-oncard-dim", "gtile1", 3.0],
|
|
163
|
+
["分组计数 oncard-dim / 分组卡底(终点)", "--dsb-oncard-dim", "gtile2", 3.0],
|
|
164
|
+
["趋势柱 bar1 / 面板", "--dsb-bar1", "panel", 3.0],
|
|
165
|
+
["趋势柱 bar2 / 面板", "--dsb-bar2", "panel", 3.0]
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const fails = [];
|
|
169
|
+
for (const id of ids) {
|
|
170
|
+
const V = varsOf(id);
|
|
171
|
+
const base = SKIN.BY_ID[id].vars;
|
|
172
|
+
const panel = rgb(base.pbgA || base.bgA);
|
|
173
|
+
const chip = rgb(base.chipBg);
|
|
174
|
+
const card = chip ? over(chip, panel) : panel;
|
|
175
|
+
const surface = rgb(V["--dsb-surface"]) || card;
|
|
176
|
+
const gt1 = rgb(V["--dsb-tile1"]) || [58, 67, 112, 1];
|
|
177
|
+
const gt2 = rgb(V["--dsb-tile2"]) || [35, 42, 69, 1];
|
|
178
|
+
const gshade = rgb(V["--dsb-shade"]) || [8, 10, 18, 0.42];
|
|
179
|
+
const BG = {
|
|
180
|
+
panel, card, row: card, surface,
|
|
181
|
+
gtile1: over(gshade, gt1), gtile2: over(gshade, gt2),
|
|
182
|
+
obg1: rgb(V["--dsb-onbrand-bg1"]), obg2: rgb(V["--dsb-onbrand-bg2"])
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
for (const [name, key, bgKey, min] of CHECKS) {
|
|
186
|
+
const fgStr = key.startsWith("--") ? V[key] : base[key];
|
|
187
|
+
if (!fgStr) { fails.push({ id, name, why: "变量缺失 " + key }); continue; }
|
|
188
|
+
const fg = rgb(fgStr), bg = BG[bgKey];
|
|
189
|
+
if (!fg || !bg) { fails.push({ id, name, why: "无法解析 " + fgStr + " / " + bgKey }); continue; }
|
|
190
|
+
const eff = fg[3] < 1 ? over(fg, bg) : fg;
|
|
191
|
+
const v = cr(eff, bg);
|
|
192
|
+
if (v < min) fails.push({ id, name, v: +v.toFixed(2), min, fg: fgStr, bgKey });
|
|
193
|
+
else if (VERBOSE) console.log(`ok ${id} ${SKIN.BY_ID[id].name} ${name} ${v.toFixed(2)}:1`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const light = ids.filter((i) => {
|
|
198
|
+
const b = SKIN.BY_ID[i].vars;
|
|
199
|
+
const p = rgb(b.pbgA || b.bgA);
|
|
200
|
+
return p && lum(p) > 0.45;
|
|
201
|
+
}).length;
|
|
202
|
+
|
|
203
|
+
console.log(`皮肤: ${ids.length}(浅色 ${light}) 检查组: ${CHECKS.length} 总项: ${ids.length * CHECKS.length}`);
|
|
204
|
+
console.log(`不达标: ${fails.length}`);
|
|
205
|
+
if (fails.length) {
|
|
206
|
+
const byName = {};
|
|
207
|
+
for (const f of fails) (byName[f.name] = byName[f.name] || []).push(f);
|
|
208
|
+
for (const n of Object.keys(byName)) {
|
|
209
|
+
console.log(`\n[${n}] ${byName[n].length} 款`);
|
|
210
|
+
for (const f of byName[n].slice(0, 8)) {
|
|
211
|
+
console.log(` ${f.id} ${SKIN.BY_ID[f.id].name} ${f.v ?? f.why}${f.min ? " < " + f.min : ""} (fg=${f.fg || "-"} bg=${f.bgKey || "-"})`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// 防 BOM 回归:扫描仓库内所有文本文件,发现 UTF-8 BOM 立即报错退出。
|
|
3
|
+
// 背景:v1.9.2 改造时 package.json / lib/index.js 被写入 BOM,导致宿主 JSON.parse 失败、
|
|
4
|
+
// 市场判定「该包不可用」、整包不激活、悬浮球消失。此脚本作为发布前硬门禁。
|
|
5
|
+
// 用法:node scripts/check-bom.mjs [--fix]
|
|
6
|
+
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")), "..");
|
|
11
|
+
const FIX = process.argv.includes("--fix");
|
|
12
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "coverage"]);
|
|
13
|
+
|
|
14
|
+
function walk(dir, out) {
|
|
15
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
17
|
+
const p = path.join(dir, e.name);
|
|
18
|
+
if (e.isDirectory()) walk(p, out);
|
|
19
|
+
else out.push(p);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const files = walk(ROOT, []).filter((p) =>
|
|
25
|
+
/\.(json|js|mjs|cjs|ts|yml|yaml|md|txt|ps1|html|css|svg)$/i.test(p)
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const bad = [];
|
|
29
|
+
for (const p of files) {
|
|
30
|
+
const b = fs.readFileSync(p);
|
|
31
|
+
if (b.length >= 3 && b[0] === 0xef && b[1] === 0xbb && b[2] === 0xbf) {
|
|
32
|
+
bad.push(p);
|
|
33
|
+
if (FIX) {
|
|
34
|
+
fs.writeFileSync(p, b.subarray(3));
|
|
35
|
+
console.log("FIXED " + path.relative(ROOT, p));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (bad.length === 0) {
|
|
41
|
+
console.log("OK: " + files.length + " 个文本文件,0 个带 UTF-8 BOM");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!FIX) {
|
|
46
|
+
console.error("FAIL: 发现 " + bad.length + " 个带 UTF-8 BOM 的文件(会导致宿主 JSON.parse 失败、插件被判不可用):");
|
|
47
|
+
for (const p of bad) console.error(" " + path.relative(ROOT, p));
|
|
48
|
+
console.error("修复:node scripts/check-bom.mjs --fix");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
console.log("已修复 " + bad.length + " 个文件");
|