dsh-gacha-calendar 0.9.39 → 0.10.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.md +4 -5
- package/lib/client.js +204 -32
- package/lib/index.js +44 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ DeepSeek Harness 桌面端侧边栏插件:一键查看主流二游的**当期
|
|
|
13
13
|
|
|
14
14
|
## 功能
|
|
15
15
|
|
|
16
|
-
- 侧边栏底部「📅
|
|
16
|
+
- 侧边栏底部「📅 二游日历」按钮 → 悬浮面板,按行展示每款游戏的当期卡池、卡池起止、当期活动、活动起止
|
|
17
17
|
- **倒计时**显示剩余时间;悬停看卡池/活动的完整名称与源站原文
|
|
18
18
|
- **刷新提示**(顶行):按「成功 / 卡池失败 / 活动失败 / 新卡池未公布 / 新活动未公布」归类,悬停看逐条原因;"来源报错"计入失败、"源站还没收录当期"不算失败;某列沿用上次缓存时,悬停里另起一行说明。游戏名悬停可看**上次完全成功的时间**
|
|
19
19
|
- **自动刷新**:启动时判一次——插件更新(缓存不是当前版本产出的)与首次安装会**强制刷一次,不看自动刷新开关**(悬停格式、来源地址、解析器、样式等改动一刷新即生效);其余按设置间隔(1–42 天)到点才刷,间隔从**上次成功刷新**起算
|
|
@@ -39,8 +39,7 @@ DeepSeek Harness 桌面端侧边栏插件:一键查看主流二游的**当期
|
|
|
39
39
|
|
|
40
40
|
## 数据来源
|
|
41
41
|
|
|
42
|
-
排期数据实时抓取自各游戏**官方公告 / 官方 Wiki / 第三方站**——bwiki、PRTS、wiki.gg、Game8、GameKee、Canmoe、GachaTracker、ldshop
|
|
43
|
-
无联网数据时对应列显示为空;单列抓取失败则沿用上次成功数据,并在悬停里标注。
|
|
42
|
+
排期数据实时抓取自各游戏**官方公告 / 官方 Wiki / 第三方站**——bwiki、PRTS、wiki.gg、Game8、GameKee、Canmoe、GachaTracker、ldshop、小米游戏中心等。采用**克制的抓取策略**设计,普通用户正常使用时的抓取请求不会影响网站正常运行。
|
|
44
43
|
|
|
45
44
|
## 安装
|
|
46
45
|
|
|
@@ -66,7 +65,7 @@ npm run check # 只校验:src/ 拼出来的结果是否与产物一致(不
|
|
|
66
65
|
| npm 包 | 内容 | 谁用 |
|
|
67
66
|
|---|---|---|
|
|
68
67
|
| `dsh-gacha-calendar` | DSH 桌面端插件(已内联同一份 core,运行时不依赖 npm 解析) | DSH Desktop |
|
|
69
|
-
| [`gacha-calendar-core`](packages/core) | 平台中立的抓取核心:`createEngine({ transport, storage, now })`,零依赖、零宿主依赖 |
|
|
68
|
+
| [`gacha-calendar-core`](packages/core) | 平台中立的抓取核心:`createEngine({ transport, storage, now })`,零依赖、零宿主依赖 | 其它平台 浏览器插件[`gacha-calendar-extension`](https://github.com/EastMG/gacha-calendar-extension) 等 开发中 |
|
|
70
69
|
|
|
71
70
|
```bash
|
|
72
71
|
npm run publish:plugin # 发布 DSH 插件包(dsh-gacha-calendar)
|
|
@@ -118,7 +117,7 @@ dsh-gacha-calendar/
|
|
|
118
117
|
|
|
119
118
|
## 致谢
|
|
120
119
|
|
|
121
|
-
- [MAA1999/M9A](https://github.com/MAA1999/M9A)
|
|
120
|
+
- [MAA1999/M9A](https://github.com/MAA1999/M9A)——对《重返未来:1999》逐期「征集时间」的获取手段受到**M9A**的启发。
|
|
122
121
|
|
|
123
122
|
## 免责声明
|
|
124
123
|
|
package/lib/client.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// 用途:缓存里记录"这份数据是哪版插件产出的"。更新插件后首次启动,据此**强制**刷新一次
|
|
5
5
|
// (不看自动刷新开关)——因为有些改动(悬停格式、来源地址、样式、解析器)不刷新就看不到效果。
|
|
6
6
|
// 写入时机见 engine-api.js 的 refresh();判定见 60-helpers.js 的 autoRefreshPlan()。
|
|
7
|
-
const PLUGIN_VERSION = "0.
|
|
7
|
+
const PLUGIN_VERSION = "0.10.0";
|
|
8
8
|
//#endregion
|
|
9
9
|
|
|
10
10
|
window.__ModuleLoader__.load({
|
|
@@ -18,6 +18,12 @@ window.__ModuleLoader__.load({
|
|
|
18
18
|
|
|
19
19
|
//#region config
|
|
20
20
|
const NS = "gacha-calendar";
|
|
21
|
+
// 本插件在 profile 里的条目 id 候选(用于解析设置表单,见 92-dsh-env.js 的 resolveSettingsEntryId)。
|
|
22
|
+
// DSH 0.1.7 起设置文档按**条目 id**寻址,而"插件名/条目 id/包名"三者未必同名:
|
|
23
|
+
// · cordis.patch.yml 里是 id: gacha-calendar(我们自己的 insert 声明)
|
|
24
|
+
// · 包名是 dsh-gacha-calendar
|
|
25
|
+
// 所以不写死单一字符串,按顺序试,谁能拿到表单就用谁。
|
|
26
|
+
const SETTINGS_ENTRY_IDS = [NS, "dsh-gacha-calendar"];
|
|
21
27
|
// 刷新频率选项:按天(存分钟),与 host 端 Config.refreshMinutes 对应。
|
|
22
28
|
// 档位对齐常见版本周期:14/21/28/35/42 天 —— 15≈蔚蓝档案的 14 天轮换、21=1999 半版本/3.6 整版本
|
|
23
29
|
// 与异环当期、30≈方舟月度、42=米系与 1999 的整版本
|
|
@@ -322,7 +328,7 @@ window.__ModuleLoader__.load({
|
|
|
322
328
|
},
|
|
323
329
|
{
|
|
324
330
|
id: "r1999",
|
|
325
|
-
parserVersion:
|
|
331
|
+
parserVersion: 3,
|
|
326
332
|
name: "重返未来:1999",
|
|
327
333
|
icon: "https://play-lh.googleusercontent.com/LwcueZMBbLq6aELtqJVn61ToKkJUgxEO8O4KgK_5052hfYoDAglQJIzqSu8srUJeaOZwv36Qi5YKtsXZjo-JPg=s64",
|
|
328
334
|
source: "\u5B98\u65B9\u516C\u544A",
|
|
@@ -2665,7 +2671,31 @@ window.__ModuleLoader__.load({
|
|
|
2665
2671
|
return "";
|
|
2666
2672
|
}
|
|
2667
2673
|
|
|
2668
|
-
//
|
|
2674
|
+
// 独立池公告:标题形如「池名」类型开启!,正文只有一张图(拿不到 UP 名单),
|
|
2675
|
+
// 但接口的 beginTime/endTime 就是该池的真实起止。官方**逐池各发一篇**,且**过期即从公告板下线**:
|
|
2676
|
+
// 实测公告板上 22 条无一条过期,已结束的池与其轮换期全都没有公告("有公告" ⟺ "该池在开")。
|
|
2677
|
+
// 所以轮换征集只能靠它——一览里那三行日期不带时间,过期后连日期行也会随版本一起消失。
|
|
2678
|
+
const R99_SOLO_POOL = /^\u300c([^\u300d]{1,40})\u300d([^\u300c\u300d]{1,12})\u5f00\u542f\uff01$/;
|
|
2679
|
+
function r99SoloPools(items) {
|
|
2680
|
+
const out = [];
|
|
2681
|
+
for (const it of items) {
|
|
2682
|
+
const title = String((it.contentMap && it.contentMap["zh-CN"] && it.contentMap["zh-CN"].title) || "").trim();
|
|
2683
|
+
const m = title.match(R99_SOLO_POOL);
|
|
2684
|
+
if (!m) continue;
|
|
2685
|
+
const kind = m[2].trim();
|
|
2686
|
+
if (R99_POOL_TIERS.indexOf(kind) < 0) continue; // 衣着上新/上架/上新等不是卡池
|
|
2687
|
+
const startTs = Number(it.beginTime);
|
|
2688
|
+
const endTs = Number(it.endTime) - 6e4; // 接口边界是排他的(…05:00),面板统一显示末分钟 04:59
|
|
2689
|
+
if (!startTs || !endTs) continue;
|
|
2690
|
+
out.push({
|
|
2691
|
+
name: `\u300c${m[1]}\u300d${kind}`, kind, roles: "", startTs, endTs, raw: "",
|
|
2692
|
+
display: fmtWindow(startTs, endTs)
|
|
2693
|
+
});
|
|
2694
|
+
}
|
|
2695
|
+
return out;
|
|
2696
|
+
}
|
|
2697
|
+
|
|
2698
|
+
// 解析一篇「版本活动一览」→ { pools, events, rotRows }
|
|
2669
2699
|
function parseR99Overview(item, now = nowMs()) {
|
|
2670
2700
|
const lines = r99NoticeLines(item);
|
|
2671
2701
|
if (!lines) return null;
|
|
@@ -2720,20 +2750,16 @@ window.__ModuleLoader__.load({
|
|
|
2720
2750
|
seenEvent.add(key);
|
|
2721
2751
|
events.push({ name: sec.name, cat: sec.kind, startTs, endTs, raw: fmtWindow(startTs, endTs) });
|
|
2722
2752
|
}
|
|
2723
|
-
//
|
|
2724
|
-
|
|
2753
|
+
// 轮换征集在一览里**只有日期行**(「N月N日更新:角色」,不带起止时间)→ 这里只当"角色字典"。
|
|
2754
|
+
// 时间不再用"+14 天"推算:每期轮换都有自己的独立公告(见 r99SoloPools),接口给的就是真实起止。
|
|
2755
|
+
const rotRows = [];
|
|
2725
2756
|
for (const line of lines) {
|
|
2726
2757
|
const m = line.match(/^(\d{1,2})\u6708(\d{1,2})\u65e5\u66f4\u65b0[\uff1a:]\s*(.+)$/);
|
|
2727
2758
|
if (!m) continue;
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
rotations.push({
|
|
2731
|
-
name: "\u300c\u8f6e\u6362\u5f81\u96c6\u300d", kind: "\u8f6e\u6362\u5f81\u96c6", roles: cleanRoles(m[3]),
|
|
2732
|
-
startTs, endTs, raw: `\u3010${m[1]}\u6708${m[2]}\u65e5\u66f4\u65b0\u3011${cleanRoles(m[3])}`,
|
|
2733
|
-
display: fmtWindow(startTs, endTs)
|
|
2734
|
-
});
|
|
2759
|
+
// 只用"更新日"当匹配键(与同一期独立公告的 startTs 同一天),不作为时间来源
|
|
2760
|
+
rotRows.push({ startTs: new Date(year, Number(m[1]) - 1, Number(m[2]), 5, 0).getTime(), roles: cleanRoles(m[3]) });
|
|
2735
2761
|
}
|
|
2736
|
-
return { pools, events,
|
|
2762
|
+
return { pools, events, rotRows };
|
|
2737
2763
|
}
|
|
2738
2764
|
|
|
2739
2765
|
// 官方游戏内公告 → 当期征集(含真实起止、悬停列全部并行)+ 当期活动(同其它游戏的活动列规则)。
|
|
@@ -2746,7 +2772,7 @@ window.__ModuleLoader__.load({
|
|
|
2746
2772
|
const titleOf = (it) => String((it.contentMap && it.contentMap["zh-CN"] && it.contentMap["zh-CN"].title) || "");
|
|
2747
2773
|
const overviews = items.filter((it) => /\u7248\u672c\u6d3b\u52a8\u4e00\u89c8/.test(titleOf(it)));
|
|
2748
2774
|
if (overviews.length === 0) throw new Error("r1999-notice-no-section");
|
|
2749
|
-
const pools = [], events = [],
|
|
2775
|
+
const pools = [], events = [], rotRows = [];
|
|
2750
2776
|
let parsed = 0;
|
|
2751
2777
|
for (const it of overviews) {
|
|
2752
2778
|
const o = parseR99Overview(it, now);
|
|
@@ -2754,11 +2780,27 @@ window.__ModuleLoader__.load({
|
|
|
2754
2780
|
parsed++;
|
|
2755
2781
|
pools.push.apply(pools, o.pools);
|
|
2756
2782
|
events.push.apply(events, o.events);
|
|
2757
|
-
|
|
2783
|
+
rotRows.push.apply(rotRows, o.rotRows);
|
|
2758
2784
|
}
|
|
2759
2785
|
if (parsed === 0) throw new Error("r1999-notice-no-dates");
|
|
2786
|
+
// 用独立公告补齐一览没有的池(当前就是轮换征集)。同名池**以一览为准**——它有正文【征集时间】
|
|
2787
|
+
// 与 UP 名单;独立公告正文只有图,只能补"池名 + 真实起止",角色另从日期行字典里按同一天取。
|
|
2788
|
+
const dayKey = (ts) => { const d = new Date(ts); return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`; };
|
|
2789
|
+
const rolesByDay = new Map(rotRows.map((r) => [dayKey(r.startTs), r.roles]));
|
|
2790
|
+
for (const p of r99SoloPools(items)) {
|
|
2791
|
+
if (pools.some((x) => x.name === p.name)) continue;
|
|
2792
|
+
p.roles = rolesByDay.get(dayKey(p.startTs)) || "";
|
|
2793
|
+
pools.push(p);
|
|
2794
|
+
}
|
|
2760
2795
|
const data = {};
|
|
2761
|
-
|
|
2796
|
+
// 当期 = 窗口覆盖现在的池;同名多期(一览的下一期 + 公告的当期)只留第一个 = 一览优先
|
|
2797
|
+
const seenName = new Set();
|
|
2798
|
+
const active = pools.filter((p) => {
|
|
2799
|
+
if (!(p.startTs <= now && p.endTs >= now)) return false;
|
|
2800
|
+
if (seenName.has(p.name)) return false;
|
|
2801
|
+
seenName.add(p.name);
|
|
2802
|
+
return true;
|
|
2803
|
+
});
|
|
2762
2804
|
if (active.length > 0) {
|
|
2763
2805
|
const rank = (p) => {
|
|
2764
2806
|
const i = R99_POOL_TIERS.indexOf(p.kind);
|
|
@@ -3252,6 +3294,40 @@ window.__ModuleLoader__.load({
|
|
|
3252
3294
|
gacha: { fail: "卡池失败", nomatch: "新卡池未公布", nomatchUser: "未解析出内容" },
|
|
3253
3295
|
event: { fail: "活动失败", nomatch: "新活动未公布", nomatchUser: "未解析出内容" }
|
|
3254
3296
|
};
|
|
3297
|
+
|
|
3298
|
+
// —— 配置表单写入被拒时的归因(纯函数)——
|
|
3299
|
+
// DSH 0.1.7 的 configForms 表单有四种"能读不能写"的状态,肉眼在界面上完全分不出来
|
|
3300
|
+
// (控件点一下弹回原值、没有任何提示)。这里按快照如实说清是哪一种,让下次出问题能一眼定位:
|
|
3301
|
+
// memory = 本次连接不是本机回环 → 宿主压根不接受持久化写入(只读)
|
|
3302
|
+
// loading = 表单数据还没送到(点太早了)
|
|
3303
|
+
// unavailable = 宿主没在 serve 本插件的条目(多半是条目 id 对不上 / 没进 describe)
|
|
3304
|
+
// 其它 = 宿主明确拒绝(字段没进表单,或修订号冲突)
|
|
3305
|
+
function formRejectHint(scope, key) {
|
|
3306
|
+
var snap = null;
|
|
3307
|
+
try { snap = scope && scope.getSnapshot ? scope.getSnapshot() : null; } catch (e) { snap = null; }
|
|
3308
|
+
const status = snap && snap.status ? snap.status : "unknown";
|
|
3309
|
+
const mode = snap && snap.mode ? snap.mode : "unknown";
|
|
3310
|
+
const rev = snap && typeof snap.revision === "number" ? " (rev " + snap.revision + ")" : "";
|
|
3311
|
+
const id = scope && scope.entryId ? scope.entryId : "(未解析)";
|
|
3312
|
+
if (mode === "memory") return "本次连接不是本机,宿主不接受写入(只读模式)";
|
|
3313
|
+
if (status === "loading") return "配置表单还没送到,请稍后重试";
|
|
3314
|
+
if (status === "unavailable") return "宿主未 serve 条目「" + id + "」" + servedNamespacesText(scope.configForms);
|
|
3315
|
+
return "宿主拒绝了该写入(表单状态 " + status + " / " + mode + rev + ")";
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
// 把宿主**真正 serve 的**设置 namespace 列出来。这一条是给"地址对不上"这类故障留的现场:
|
|
3319
|
+
// 只说"没提供配置条目"定位不了,列出真实 id 就能一眼看出该用哪个。
|
|
3320
|
+
function servedNamespacesText(configForms) {
|
|
3321
|
+
try {
|
|
3322
|
+
const mirror = configForms && typeof configForms.describe === "function" ? configForms.describe() : null;
|
|
3323
|
+
const view = mirror && typeof mirror.getSnapshot === "function" ? mirror.getSnapshot().view : null;
|
|
3324
|
+
const rows = view && Array.isArray(view.namespaces) ? view.namespaces : [];
|
|
3325
|
+
const names = rows.map((r) => r && r.ns).filter((ns) => typeof ns === "string");
|
|
3326
|
+
return names.length ? "(宿主当前 serve:" + names.join(", ") + ")" : "(宿主当前一个设置条目都没 serve)";
|
|
3327
|
+
} catch (e) {
|
|
3328
|
+
return "";
|
|
3329
|
+
}
|
|
3330
|
+
}
|
|
3255
3331
|
function normErr(err) {
|
|
3256
3332
|
const name = String((err && err.name) || "");
|
|
3257
3333
|
const msg = String((err && err.message) || err || "");
|
|
@@ -3818,6 +3894,7 @@ window.__ModuleLoader__.load({
|
|
|
3818
3894
|
DEFAULT_SETTINGS,
|
|
3819
3895
|
// 启动自动刷新判定(纯函数)与当前插件版本(注入自 package.json):回归脚本据此验收
|
|
3820
3896
|
autoRefreshPlan,
|
|
3897
|
+
formRejectHint,
|
|
3821
3898
|
PLUGIN_VERSION
|
|
3822
3899
|
};
|
|
3823
3900
|
|
|
@@ -3884,7 +3961,10 @@ window.__ModuleLoader__.load({
|
|
|
3884
3961
|
|
|
3885
3962
|
//#region components
|
|
3886
3963
|
// 本插件在 DSH 设置面板里的导航名;面板按钮与弹层标题也用它(唯一事实来源,90-plugin.js 注册分区时共用)。
|
|
3887
|
-
const SETTINGS_SECTION_LABEL = "\u4E8C\u6E38\
|
|
3964
|
+
const SETTINGS_SECTION_LABEL = "\u4E8C\u6E38\u65E5\u5386";
|
|
3965
|
+
// 面板上那个「设置」按钮:**临时隐藏**(用户要求)。函数与样式都保留,
|
|
3966
|
+
// 改回 true 即可恢复,不用重写逻辑。
|
|
3967
|
+
const SETTINGS_BUTTON_VISIBLE = false;
|
|
3888
3968
|
// DSH 设置面板是"点了才挂载",打开后要等导航渲染出来才能切页;预算给足但不无限等。
|
|
3889
3969
|
const SETTINGS_NAV_TIMEOUT_MS = 600;
|
|
3890
3970
|
const SETTINGS_NAV_POLL_MS = 30;
|
|
@@ -3904,11 +3984,36 @@ window.__ModuleLoader__.load({
|
|
|
3904
3984
|
if (!trigger || typeof trigger.click !== "function") return;
|
|
3905
3985
|
trigger.click();
|
|
3906
3986
|
const deadline = Date.now() + SETTINGS_NAV_TIMEOUT_MS;
|
|
3987
|
+
// 在给定根节点里找「二游排期」那一项:先按 aria-label,再按自身文本。
|
|
3988
|
+
// 不写死 `nav button` 结构 —— 设置外壳换布局时(0.1.7 把插件配置挪到了
|
|
3989
|
+
// 「插件」页)这种硬结构选择器会静默失效,表现为"点设置跳错页"。
|
|
3990
|
+
//
|
|
3991
|
+
// 必须排除本插件自己的面板:面板的开关按钮 aria-label 就是同一个标签,
|
|
3992
|
+
// 文档级兜底会先命中它 → 点下去变成"自己开自己"(典型症状:点设置没反应/跳错)。
|
|
3993
|
+
const inOwnPanel = (el) => {
|
|
3994
|
+
for (let n = el; n; n = n.parent) if (n.classList && n.classList.contains("gacha-cal-pop")) return true;
|
|
3995
|
+
return false;
|
|
3996
|
+
};
|
|
3997
|
+
const findItem = (root) => {
|
|
3998
|
+
if (!root || typeof root.querySelectorAll !== "function") return null;
|
|
3999
|
+
const items = [...root.querySelectorAll("button, a")]
|
|
4000
|
+
.filter((el) => el !== trigger && !inOwnPanel(el));
|
|
4001
|
+
const byAttr = items.find((el) => {
|
|
4002
|
+
const l = (el.getAttribute("aria-label") || el.getAttribute("title") || "").trim();
|
|
4003
|
+
return l === SETTINGS_SECTION_LABEL;
|
|
4004
|
+
});
|
|
4005
|
+
if (byAttr) return byAttr;
|
|
4006
|
+
return items.find((el) => (el.textContent || "").trim() === SETTINGS_SECTION_LABEL) || null;
|
|
4007
|
+
};
|
|
3907
4008
|
const tick = () => {
|
|
3908
|
-
//
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4009
|
+
// 只在设置面板(dialog)里找,**不做整篇文档的兜底搜索**:
|
|
4010
|
+
// 本插件面板的开关按钮 aria-label 就是同一个标签,文档级搜索会先命中它,
|
|
4011
|
+
// 一点就变成"自己把自己打开"(实测就是这样:点设置只弹本插件面板、不跳页)。
|
|
4012
|
+
const dialogs = [...document.querySelectorAll('[role="dialog"]')];
|
|
4013
|
+
for (const d of dialogs) {
|
|
4014
|
+
const hit = findItem(d);
|
|
4015
|
+
if (hit) { hit.click(); return; }
|
|
4016
|
+
}
|
|
3912
4017
|
if (Date.now() < deadline) window.setTimeout(tick, SETTINGS_NAV_POLL_MS);
|
|
3913
4018
|
};
|
|
3914
4019
|
window.setTimeout(tick, 0);
|
|
@@ -4149,14 +4254,16 @@ window.__ModuleLoader__.load({
|
|
|
4149
4254
|
})
|
|
4150
4255
|
})
|
|
4151
4256
|
}),
|
|
4152
|
-
// 设置按钮:打开 DSH
|
|
4257
|
+
// 设置按钮:打开 DSH 设置面板并切到本插件的设置页,同时关闭本面板。
|
|
4258
|
+
// **当前按用户要求临时隐藏**(SETTINGS_BUTTON_VISIBLE = false);
|
|
4259
|
+
// 下面这段逻辑与说明全部保留,改回 true 即可恢复。
|
|
4153
4260
|
// 为什么这么绕:DSH 没给插件"跳转设置页"的官方 API——面板是 modal,open/activeId 都是组件
|
|
4154
4261
|
// 私有 state(dsh-client-ui-settings-general 的 SettingsRoot),打开后按 rows[0] 兜底,也就是停在
|
|
4155
4262
|
// 导航第一项「通用」;插件拿不到 openSection。只能点侧边栏那个设置触发器(aria-haspopup="dialog"
|
|
4156
4263
|
// 是它的契约属性:全客户端带这个属性的有 6 个 dialog 类 + 12 个 menu/listbox/tree,所以按
|
|
4157
4264
|
// "可见 + 无障碍名匹配 设置/Settings" 挑,挑不到才退回第一个),再交给 openPluginSettingsSection
|
|
4158
4265
|
// 等面板挂载后按导航名切到本插件那一节。
|
|
4159
|
-
(0, react_jsx_runtime.jsx)("button", {
|
|
4266
|
+
SETTINGS_BUTTON_VISIBLE && (0, react_jsx_runtime.jsx)("button", {
|
|
4160
4267
|
type: "button",
|
|
4161
4268
|
className: "gacha-cal-refresh",
|
|
4162
4269
|
title: "\u8BBE\u7F6E",
|
|
@@ -4240,6 +4347,8 @@ window.__ModuleLoader__.load({
|
|
|
4240
4347
|
|
|
4241
4348
|
// engine:core 引擎(设置页的「解析器自检」用它跑 selfCheck())
|
|
4242
4349
|
function CalendarSettingsPage({ scope, engine }) {
|
|
4350
|
+
// 设置表单服务本体(用于在写不进去时列出宿主真正 serve 的 namespace,见 formStateText)
|
|
4351
|
+
const ctxConfigForms = scope && scope.configForms ? scope.configForms : null;
|
|
4243
4352
|
const [snapshot, setSnapshot] = (0, react.useState)(() => scope.getSnapshot());
|
|
4244
4353
|
const [adding, setAdding] = (0, react.useState)(false);
|
|
4245
4354
|
// 添加表单临时值(名称/图标手动填;卡池与活动内容由链接解析产出)
|
|
@@ -4265,6 +4374,16 @@ window.__ModuleLoader__.load({
|
|
|
4265
4374
|
};
|
|
4266
4375
|
(0, react.useEffect)(() => scope.subscribe(() => setSnapshot(scope.getSnapshot())), [scope]);
|
|
4267
4376
|
const s = { ...DEFAULT_SETTINGS, ...(snapshot.value ?? {}) };
|
|
4377
|
+
// 表单可用性(写不进去时在页面上如实说明,见下面渲染处)。
|
|
4378
|
+
// 文案与 formRejectHint 同源:只解释"为什么写不了",不猜。
|
|
4379
|
+
const formReady = snapshot.status === "ready" && snapshot.mode !== "memory";
|
|
4380
|
+
const formStateText = snapshot.mode === "memory"
|
|
4381
|
+
? "本次连接不是本机,宿主不接受写入"
|
|
4382
|
+
: snapshot.status === "loading"
|
|
4383
|
+
? "配置表单还没送到,请稍后重试"
|
|
4384
|
+
: snapshot.status === "unavailable"
|
|
4385
|
+
? "宿主未 serve 条目「" + (scope.entryId || "?") + "」" + servedNamespacesText(ctxConfigForms)
|
|
4386
|
+
: "宿主拒绝了写入(表单状态 " + String(snapshot.status) + ")";
|
|
4268
4387
|
|
|
4269
4388
|
const allEntries = getAllEntries(s);
|
|
4270
4389
|
const hidden = Array.isArray(s.hidden) ? s.hidden : [];
|
|
@@ -4280,10 +4399,16 @@ window.__ModuleLoader__.load({
|
|
|
4280
4399
|
|
|
4281
4400
|
// 保存失败必须可见:旧实现只 await 不 catch,写被宿主拒绝时控件弹回原值、
|
|
4282
4401
|
// 用户只会觉得"点了没反应"(「重置排序」写 null 被拒就是这种病的极端例子)
|
|
4402
|
+
//
|
|
4403
|
+
// DSH 0.1.7 起这里多了一个坑:`configForms` 的 set() **返回布尔值而不是抛错**
|
|
4404
|
+
// (旧 settingsScope 的 set() 失败会抛)。所以只写 try/catch 的话,
|
|
4405
|
+
// "宿主拒绝写入"会被当成成功,控件照样弹回、依旧没有任何提示。
|
|
4406
|
+
// 必须显式判返回值:false = 宿主没接受(多半是该字段没进表单 / 修订号冲突 / 页面只读)。
|
|
4283
4407
|
const [saveError, setSaveError] = (0, react.useState)("");
|
|
4284
4408
|
const commit = async (key, value) => {
|
|
4285
4409
|
try {
|
|
4286
|
-
await scope.set(key, value);
|
|
4410
|
+
const accepted = await scope.set(key, value);
|
|
4411
|
+
if (accepted === false) throw new Error(formRejectHint(scope, key));
|
|
4287
4412
|
setSaveError("");
|
|
4288
4413
|
} catch (err) {
|
|
4289
4414
|
setSaveError("\u4FDD\u5B58\u5931\u8D25\uFF1A" + String((err && err.message) || err || "未知错误"));
|
|
@@ -4398,9 +4523,14 @@ window.__ModuleLoader__.load({
|
|
|
4398
4523
|
style: { display: "flex", flexDirection: "column", gap: 14, maxWidth: 720, fontSize: 13, padding: "2px 0 8px" },
|
|
4399
4524
|
children: [
|
|
4400
4525
|
(0, react_jsx_runtime.jsx)("div", { children: [
|
|
4401
|
-
(0, react_jsx_runtime.jsx)("div", { style: { fontSize: 16, fontWeight: 600 }, children:
|
|
4526
|
+
(0, react_jsx_runtime.jsx)("div", { style: { fontSize: 16, fontWeight: 600 }, children: SETTINGS_SECTION_LABEL })
|
|
4402
4527
|
] }),
|
|
4403
4528
|
saveError ? (0, react_jsx_runtime.jsx)("div", { style: { color: "var(--dsw-alias-state-error-primary, #d4380d)", fontSize: 12 }, children: saveError }) : null,
|
|
4529
|
+
// 配置表单本身的状态:只在"写不进去"时才出现,平时不占位。
|
|
4530
|
+
// 这一行是给"控件点了弹回原值、却没有任何提示"这类问题留的现场:
|
|
4531
|
+
// memory = 非本机连接(只读);loading/unavailable = 宿主没把表单送到;
|
|
4532
|
+
// ready 但写仍被拒 = 该字段没进表单或修订号冲突。
|
|
4533
|
+
(formReady ? null : (0, react_jsx_runtime.jsx)("div", { style: { color: "var(--dsw-alias-state-error-primary, #d4380d)", fontSize: 12 }, children: "\u914D\u7F6E\u53EA\u8BFB\uFF1A" + formStateText })),
|
|
4404
4534
|
(0, react_jsx_runtime.jsxs)("div", { style: { display: "flex", flexDirection: "column", gap: 12, border: "1px solid var(--dsw-alias-border-l1)", borderRadius: 10, padding: "12px 14px", background: "var(--dsw-alias-bg-layer-1)" }, children: [
|
|
4405
4535
|
(0, react_jsx_runtime.jsxs)("label", { style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
|
|
4406
4536
|
(0, react_jsx_runtime.jsx)("input", { type: "checkbox", checked: !!s.autoRefresh, onChange: (e) => commit("autoRefresh", e.target.checked) }),
|
|
@@ -4558,7 +4688,9 @@ window.__ModuleLoader__.load({
|
|
|
4558
4688
|
//#endregion
|
|
4559
4689
|
|
|
4560
4690
|
//#region plugin
|
|
4561
|
-
|
|
4691
|
+
// configForms = DSH 0.1.7 起的设置表单服务;0.1.6 及以前叫 settingsScope。
|
|
4692
|
+
// 两者都是"按 profile 条目 id 取一个表单",本插件的条目 id 就是 NS。
|
|
4693
|
+
const inject = ["slots", "configForms", "locale"];
|
|
4562
4694
|
function apply(ctx) {
|
|
4563
4695
|
ctx.effect(() => {
|
|
4564
4696
|
const style = document.createElement("style");
|
|
@@ -4568,9 +4700,9 @@ window.__ModuleLoader__.load({
|
|
|
4568
4700
|
return () => style.remove();
|
|
4569
4701
|
}, "dsh-gacha-calendar: styles");
|
|
4570
4702
|
|
|
4571
|
-
//
|
|
4703
|
+
// 设置页左侧导航的 section 图标由 navIcon 硬编码(未知 id 一律默认齿轮),
|
|
4572
4704
|
// 插件无法通过 settings.section 配置图标;这里在设置面板打开后,把
|
|
4573
|
-
// "
|
|
4705
|
+
// "二游日历" 导航项的齿轮图标替换为日历图标(Lucide 风格 16px,幂等)。
|
|
4574
4706
|
// 性能:MutationObserver 回调只置脏标记,用 requestAnimationFrame 合并执行;
|
|
4575
4707
|
// patch 先查 dialog 是否存在,不存在立即返回(Web 端高频 DOM 变化时开销极小)。
|
|
4576
4708
|
ctx.effect(() => {
|
|
@@ -4641,7 +4773,15 @@ window.__ModuleLoader__.load({
|
|
|
4641
4773
|
};
|
|
4642
4774
|
}, "dsh-gacha-calendar: marquee");
|
|
4643
4775
|
|
|
4644
|
-
|
|
4776
|
+
// 取本插件的配置表单:读走镜像快照,写由表单自己排队并带修订号。
|
|
4777
|
+
// 条目 id 从宿主**真正 serve 的** namespace 列表里解析(候选见 10-config.js 的
|
|
4778
|
+
// SETTINGS_ENTRY_IDS)—— 不写死的原因见 92-dsh-env.js 的 resolveSettingsEntryId 注释。
|
|
4779
|
+
const settingsEntryId = resolveSettingsEntryId(ctx.configForms, SETTINGS_ENTRY_IDS);
|
|
4780
|
+
const scope = ctx.configForms.get(settingsEntryId);
|
|
4781
|
+
// 把解析结果与表单服务挂到 scope 上:设置页要靠它们说明"为什么写不进去",
|
|
4782
|
+
// 以及列出宿主真正 serve 的条目(排障用,见 60-helpers.js 的 servedNamespacesText)。
|
|
4783
|
+
scope.entryId = settingsEntryId;
|
|
4784
|
+
scope.configForms = ctx.configForms;
|
|
4645
4785
|
// core 引擎:抓取/解析/缓存合并都在 engine 里(面板只读它返回的 Result JSON)。
|
|
4646
4786
|
// 存储适配(settings scope)与传输适配(直连 + 宿主代理)见 92-dsh-env.js。
|
|
4647
4787
|
const engine = createDshEngine(scope);
|
|
@@ -4701,17 +4841,49 @@ window.__ModuleLoader__.load({
|
|
|
4701
4841
|
const DSH_TRANSPORT = { fetchRaw: dshFetchRaw, fetchViaProxy: dshFetchViaProxy };
|
|
4702
4842
|
setCoreEnv({ transport: DSH_TRANSPORT });
|
|
4703
4843
|
|
|
4704
|
-
//
|
|
4844
|
+
// 解析本插件的设置条目 id:从 configForms 的 describe 镜像里读**宿主真正 serve 的** namespace 列表,
|
|
4845
|
+
// 在候选里挑第一个匹配。为什么不能直接写死:0.1.7 按条目 id 寻址,而条目 id、包名、插件名
|
|
4846
|
+
// 三者未必同名;写死一旦对不上,表现就是"表单永远 unavailable、所有写入被拒、
|
|
4847
|
+
// 面板只显示 schema 默认值"——症状和"数据被清空"极像,其实只是地址找错了。
|
|
4848
|
+
// 拿不到镜像时回退到第一个候选,保持原行为、不更坏。
|
|
4849
|
+
function listServedNamespaces(configForms) {
|
|
4850
|
+
try {
|
|
4851
|
+
const mirror = configForms && typeof configForms.describe === "function" ? configForms.describe() : null;
|
|
4852
|
+
const view = mirror && typeof mirror.getSnapshot === "function" ? mirror.getSnapshot().view : null;
|
|
4853
|
+
const rows = view && Array.isArray(view.namespaces) ? view.namespaces : [];
|
|
4854
|
+
return rows.map((r) => r && r.ns).filter((ns) => typeof ns === "string");
|
|
4855
|
+
} catch (e) {
|
|
4856
|
+
return [];
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
function resolveSettingsEntryId(configForms, candidates) {
|
|
4860
|
+
const served = listServedNamespaces(configForms);
|
|
4861
|
+
for (const id of candidates) if (served.indexOf(id) >= 0) return id;
|
|
4862
|
+
return candidates[0];
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4865
|
+
// DSH 的存储适配:插件配置表单(profile 条目 id)→ engine 的 storage 接口。
|
|
4705
4866
|
// engine 只认 get(key)/set(key, value),键名沿用既有设置键,所以设置页与历史缓存都不用迁移。
|
|
4867
|
+
//
|
|
4868
|
+
// DSH 0.1.7 起这个表单由 `configForms` 服务给出(旧名 `settingsScope`,已改名),
|
|
4869
|
+
// 快照形态也从「恒有 value」变成带状态机的 `{ status, value, base, user, revision, writable, mode }`:
|
|
4870
|
+
// 只有 status === "ready" 时 value 才可信(其余是 loading / unavailable)。
|
|
4871
|
+
// 取不到值一律回 undefined —— engine 会把 undefined 回落成 DEFAULT_SETTINGS,
|
|
4872
|
+
// 绝不能回落成 {} 之外的东西,否则"读不到"会被当成"读到了空配置"。
|
|
4706
4873
|
function dshStorage(scope) {
|
|
4707
4874
|
return {
|
|
4708
4875
|
async get(key) {
|
|
4709
4876
|
const snap = scope.getSnapshot();
|
|
4710
|
-
|
|
4877
|
+
if (!snap || snap.status !== "ready") return undefined;
|
|
4878
|
+
const value = snap.value;
|
|
4879
|
+
if (!value || typeof value !== "object") return undefined;
|
|
4711
4880
|
return value[key];
|
|
4712
4881
|
},
|
|
4713
4882
|
async set(key, value) {
|
|
4714
|
-
|
|
4883
|
+
// set 返回"宿主是否接受本次写入";engine 不消费该布尔值,
|
|
4884
|
+
// 但失败时抛错比静默丢数据好(静默会把"没存上"伪装成"已存")。
|
|
4885
|
+
const accepted = await scope.set(key, value);
|
|
4886
|
+
if (accepted === false) throw new Error("settings-write-rejected:" + key);
|
|
4715
4887
|
}
|
|
4716
4888
|
};
|
|
4717
4889
|
}
|
package/lib/index.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
// Host-side entry for dsh-gacha-calendar.
|
|
2
|
-
// 1)
|
|
3
|
-
// section
|
|
2
|
+
// 1) Declares the persisted Config schema the browser half's Settings >
|
|
3
|
+
// 二游日历 section reads and writes. DSH 0.1.7 derives the settings document
|
|
4
|
+
// from this schema automatically, so there is nothing to register here.
|
|
4
5
|
// 2) Registers a same-origin HTTP proxy route on DSH's webServer so the browser
|
|
5
6
|
// half can fetch external sources that block cross-origin browser requests
|
|
6
7
|
// (CORS / Referer anti-scrape) — the host runs Node fetch with full control
|
|
7
8
|
// over headers, bypassing browser-only restrictions.
|
|
8
|
-
import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
9
9
|
import z from "@deepseek-ai/schemastery";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// 设置文档的标识已不再是插件自报的 namespace,而是**本插件在 profile 里的条目 id**
|
|
12
|
+
// (`~/.dsh/profiles/<profile>/cordis.patch.yml` 的 `- id: gacha-calendar`)。
|
|
13
|
+
// 旧的 settingsNamespace("gacha-calendar") 随 API 一并删除,两边现在只靠这个 id 对齐;
|
|
14
|
+
// 浏览器半边的同一字面量在 src/client/10-config.js 的 `NS`。
|
|
13
15
|
|
|
14
16
|
/** Route prefix owned by this plugin on DSH's webServer. */
|
|
15
17
|
const PROXY_PREFIX = "/api/gacha-calendar-proxy";
|
|
@@ -227,32 +229,45 @@ async function proxyHandler(req, res) {
|
|
|
227
229
|
}
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Cordis configuration schema for this plugin's persisted settings.
|
|
234
|
+
*
|
|
235
|
+
* 每个字段都标 `.volatile()`:DSH 0.1.7 起设置文档**只投影 volatile 字段**
|
|
236
|
+
* (见 @deepseek-ai/dsh-settings 的 volatileForm/projectForm),未标记的字段
|
|
237
|
+
* 既不进表单、也不接受表单写入。本插件的持久化状态(含 lastData 缓存、
|
|
238
|
+
* lastRefresh、排序、自定义条目)全都要经表单读写,所以必须整份标记。
|
|
239
|
+
* 约束:volatile 字段必须是固定对象路径、外层不能再套 volatile —— 这里全平铺,满足。
|
|
240
|
+
* 用户可见的设置页由 80-components.js 以 settings.section 自带,配合
|
|
241
|
+
* settings.configure({ auto: false }) 抑制自动生成页,故不会把 lastData 这类
|
|
242
|
+
* 运行时状态暴露成输入框。
|
|
243
|
+
*/
|
|
231
244
|
const Config = z.object({
|
|
232
|
-
autoRefresh: z.boolean().default(true),
|
|
233
|
-
refreshMinutes: z.number().min(1).max(60480).default(1440),
|
|
234
|
-
order: z.array(z.string()).default([]),
|
|
235
|
-
lastRefresh: z.number().default(0),
|
|
236
|
-
lastSource: z.string().default("builtin"),
|
|
237
|
-
lastData: z.string().default(""),
|
|
245
|
+
autoRefresh: z.boolean().default(true).volatile(),
|
|
246
|
+
refreshMinutes: z.number().min(1).max(60480).default(1440).volatile(),
|
|
247
|
+
order: z.array(z.string()).default([]).volatile(),
|
|
248
|
+
lastRefresh: z.number().default(0).volatile(),
|
|
249
|
+
lastSource: z.string().default("builtin").volatile(),
|
|
250
|
+
lastData: z.string().default("").volatile(),
|
|
238
251
|
// 产出上面那份缓存的插件版本(空 = 首次安装 / 装了本功能之前的旧缓存)。
|
|
239
252
|
// 更新插件后首次启动据此**强制**刷新一次(不看自动刷新开关),判定见 60-helpers.js 的 autoRefreshPlan
|
|
240
|
-
lastVersion: z.string().default(""),
|
|
241
|
-
hidden: z.array(z.string()).default([]),
|
|
242
|
-
removed: z.array(z.string()).default([]),
|
|
243
|
-
customUrls: z.string().default("{}"),
|
|
244
|
-
customEventUrls: z.string().default("{}"),
|
|
245
|
-
customEntries: z.string().default("[]")
|
|
253
|
+
lastVersion: z.string().default("").volatile(),
|
|
254
|
+
hidden: z.array(z.string()).default([]).volatile(),
|
|
255
|
+
removed: z.array(z.string()).default([]).volatile(),
|
|
256
|
+
customUrls: z.string().default("{}").volatile(),
|
|
257
|
+
customEventUrls: z.string().default("{}").volatile(),
|
|
258
|
+
customEntries: z.string().default("[]").volatile()
|
|
246
259
|
});
|
|
247
260
|
|
|
248
261
|
/** Required services (host side). */
|
|
249
262
|
const inject = ["webServer"];
|
|
250
263
|
|
|
251
264
|
export function apply(ctx, config = {}) {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
// 抑制「按 schema 自动生成」的设置页:本插件自带 settings.section 页面
|
|
266
|
+
// (80-components.js),自动页会把 lastData 等运行时状态渲染成可编辑输入框。
|
|
267
|
+
// 这是 DSH 官方 README 给自带页面插件的做法;配置读写不受影响。
|
|
268
|
+
// 走可选子级注入:未挂载 settings 服务的主机不会因此起不来。
|
|
269
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
270
|
+
settingsCtx.effect(() => settingsCtx.settings.configure({ auto: false }, ctx.fiber));
|
|
256
271
|
});
|
|
257
272
|
ctx.inject(["webServer"], (injected) => {
|
|
258
273
|
const webServer = injected.get("webServer");
|
|
@@ -265,4 +280,10 @@ export function apply(ctx, config = {}) {
|
|
|
265
280
|
});
|
|
266
281
|
}
|
|
267
282
|
|
|
268
|
-
|
|
283
|
+
// Config 必须**导出**:cordis loader 把插件的具名导出 `Config` 变成
|
|
284
|
+
// `fiber.runtime.Config`,而 DSH 0.1.7 的 settings.describe() 正是靠它
|
|
285
|
+
// (entry.fiber.runtime.Config 能不能 toJSON / 能不能投影出 volatile 表单)
|
|
286
|
+
// 决定这个条目进不进设置文档。不导出 = 该条目永远不在 describe 列表里,
|
|
287
|
+
// 表现是客户端 status 恒为 unavailable、**所有写入被拒**、面板只显示 schema 默认值。
|
|
288
|
+
// 这正是 v0.9.41~0.9.43 上机后"设置改不动 + 刷新失败"的根因。
|
|
289
|
+
export { Config, inject };
|
package/package.json
CHANGED