poi-plugin-kai-planner 1.0.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.
Files changed (54) hide show
  1. package/README.md +48 -0
  2. package/index.js +35 -0
  3. package/package.json +25 -0
  4. package/src/app/KaiPlannerApp.js +211 -0
  5. package/src/app/tabs/daily/DailyTab.js +439 -0
  6. package/src/app/tabs/debug/DebugTab.js +554 -0
  7. package/src/app/tabs/wishlist/CreatePlanForm.js +185 -0
  8. package/src/app/tabs/wishlist/WishlistTab.js +704 -0
  9. package/src/app/tabs/wishlist/components/MouseComboBox.js +185 -0
  10. package/src/app/tabs/wishlist/components/WishlistExpandedDetail.js +170 -0
  11. package/src/app/tabs/wishlist/components/WishlistTable.js +253 -0
  12. package/src/core/poi/secretaryName.js +64 -0
  13. package/src/data/indexes/buildArrangementIndex.js +103 -0
  14. package/src/data/loaders/loadStaticData.js +182 -0
  15. package/src/data/static/data_manifest.json +15 -0
  16. package/src/data/static/equip_base_cost.json +5000 -0
  17. package/src/data/static/equipment_upgrade_path.json +3555 -0
  18. package/src/data/static/improvement_arrangement.json +15677 -0
  19. package/src/data/static/improvement_consume_item.json +8933 -0
  20. package/src/data/static/improvement_consume_step.json +9284 -0
  21. package/src/data/static/improvement_upgrade_cost.json +4766 -0
  22. package/src/data/static/improvement_upgrade_target.json +2641 -0
  23. package/src/data/static/material.json +90 -0
  24. package/src/services/common/secretaryDisplay.js +42 -0
  25. package/src/services/daily/buildDailyViewModel.js +402 -0
  26. package/src/services/planner/buildUpgradePath.js +79 -0
  27. package/src/services/planner/calcImproveSteps.js +104 -0
  28. package/src/services/planner/calcRemainingPlan.js +169 -0
  29. package/src/services/planner/calcRoutePlan.js +85 -0
  30. package/src/services/planner/calcUpgradeStep.js +85 -0
  31. package/src/services/planner/detectCurrentPosition.js +57 -0
  32. package/src/services/planner/summarizeShortage.js +76 -0
  33. package/src/services/player/countPlayerEquipByMasterId.js +27 -0
  34. package/src/services/player/getEquipOwnerShip.js +66 -0
  35. package/src/services/player/getPlayerData.js +14 -0
  36. package/src/services/player/getPlayerItemCountByUseitemId.js +45 -0
  37. package/src/services/player/getReduxStateFromEnvWindow.js +12 -0
  38. package/src/services/player/resolveMaterialKeyToUseitemId.js +54 -0
  39. package/src/services/static/indexes/buildConsumeIndexes.js +28 -0
  40. package/src/services/static/indexes/buildPathIndexes.js +29 -0
  41. package/src/services/static/indexes/buildUpgradeIndexes.js +57 -0
  42. package/src/services/static/version/dataSourceConfig.js +25 -0
  43. package/src/services/static/version/dataUpdateManager.js +176 -0
  44. package/src/services/static/version/versionStore.js +205 -0
  45. package/src/services/utils/toInt.js +11 -0
  46. package/src/services/utils/tokyoTime.js +58 -0
  47. package/src/services/wishlist/buildWishlistViewModel.js +56 -0
  48. package/src/services/wishlist/dropdownInteraction.js +30 -0
  49. package/src/services/wishlist/wishlistActions.js +485 -0
  50. package/src/storage/userPlans/fileStore.js +106 -0
  51. package/src/storage/userPlans/localStorageStore.js +50 -0
  52. package/src/storage/userPlans/migrate.js +60 -0
  53. package/src/storage/userPlans/planStore.js +107 -0
  54. package/src/storage/userPlans/storeAdapter.js +77 -0
@@ -0,0 +1,45 @@
1
+ // src/services/player/getPlayerItemCountByUseitemId.js
2
+ // 读取玩家道具(useitem)数量。POI 的 state.info.items 在不同版本/场景下可能有不同形态,
3
+ // 这里做一个兼容层,避免算出来全是 0。
4
+
5
+ function getPlayerItemCountByUseitemId(poiState, useitemId) {
6
+ if (!poiState || !poiState.info) return 0;
7
+ const items = poiState.info.items || poiState.info.useitems;
8
+ if (!items) return 0;
9
+
10
+ const idStr = String(useitemId);
11
+
12
+ // 形态1:object map: { "77": { api_id: 77, api_count: 64 }, ... }
13
+ if (typeof items === "object" && !Array.isArray(items)) {
14
+ // shape A: keyed by api_id, e.g. { "77": { api_id:77, api_count:64 } }
15
+ const direct = items[idStr] || items[useitemId];
16
+ if (direct) {
17
+ const c = direct.api_count ?? direct.count;
18
+ const n = Number(c);
19
+ return Number.isFinite(n) ? n : 0;
20
+ }
21
+
22
+ // shape B: object map but keyed by something else; find by row.api_id
23
+ for (const k of Object.keys(items)) {
24
+ const row = items[k];
25
+ if (!row) continue;
26
+ if (String(row.api_id) !== idStr) continue;
27
+ const c = row.api_count ?? row.count;
28
+ const n = Number(c);
29
+ return Number.isFinite(n) ? n : 0;
30
+ }
31
+ return 0;
32
+ }
33
+
34
+ // 形态2:array: [{ api_id, api_count }, ...]
35
+ if (Array.isArray(items)) {
36
+ const row = items.find((x) => x && String(x.api_id) === idStr);
37
+ if (!row) return 0;
38
+ const n = Number(row.api_count ?? row.count);
39
+ return Number.isFinite(n) ? n : 0;
40
+ }
41
+
42
+ return 0;
43
+ }
44
+
45
+ module.exports = { getPlayerItemCountByUseitemId };
@@ -0,0 +1,12 @@
1
+ /* src/services/player/getReduxStateFromEnvWindow.js */
2
+
3
+ function getReduxStateFromEnvWindow(envWindow) {
4
+ try {
5
+ if (envWindow && typeof envWindow.getStore === "function") return envWindow.getStore();
6
+ if (envWindow && envWindow.store && typeof envWindow.store.getState === "function") return envWindow.store.getState();
7
+ if (envWindow && envWindow.app && envWindow.app.store && typeof envWindow.app.store.getState === "function") return envWindow.app.store.getState();
8
+ } catch {}
9
+ return null;
10
+ }
11
+
12
+ module.exports = { getReduxStateFromEnvWindow };
@@ -0,0 +1,54 @@
1
+ /* src/services/player/resolveMaterialKeyToUseitemId.js */
2
+
3
+ /**
4
+ * Resolve item_material_key -> master useitem api_id
5
+ *
6
+ * ✅ Supports TWO call styles:
7
+ * 1) resolveMaterialKeyToUseitemId({ materialByKey, masterUseitemsById }, materialKey)
8
+ * 2) resolveMaterialKeyToUseitemId(materialKey, staticData, masterUseitemsById)
9
+ */
10
+ function resolveMaterialKeyToUseitemId(arg1, arg2, arg3) {
11
+ // Style (1)
12
+ if (arg1 && typeof arg1 === "object" && arg1.materialByKey) {
13
+ const { materialByKey, masterUseitemsById } = arg1;
14
+ const materialKey = arg2;
15
+ return _resolve(materialKey, materialByKey, masterUseitemsById);
16
+ }
17
+
18
+ // Style (2)
19
+ const materialKey = arg1;
20
+ const staticData = arg2 || {};
21
+ const masterUseitemsById = arg3 || {};
22
+ const materialByKey = staticData.materialByKey || buildMaterialByKey(staticData.material || []);
23
+ return _resolve(materialKey, materialByKey, masterUseitemsById);
24
+ }
25
+
26
+ function buildMaterialByKey(materialArr) {
27
+ const m = {};
28
+ for (const r of materialArr || []) {
29
+ if (!r || r.key == null) continue;
30
+ m[String(r.key)] = r;
31
+ }
32
+ return m;
33
+ }
34
+
35
+ function _resolve(materialKey, materialByKey, masterUseitemsById) {
36
+ if (!materialKey) return null;
37
+ const row = materialByKey && materialByKey[String(materialKey)];
38
+ if (!row) return null;
39
+
40
+ const materialName = row.name;
41
+ if (!materialName) return null;
42
+
43
+ // match by api_name
44
+ for (const idStr of Object.keys(masterUseitemsById || {})) {
45
+ const u = masterUseitemsById[idStr];
46
+ if (!u || !u.api_name) continue;
47
+ if (String(u.api_name) === String(materialName)) {
48
+ return Number(idStr);
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ module.exports = { resolveMaterialKeyToUseitemId };
@@ -0,0 +1,28 @@
1
+ /* src/services/static/indexes/buildConsumeIndexes.js */
2
+
3
+ function buildConsumeIndexes(staticData) {
4
+ const stepById = {};
5
+ for (const r of staticData.improvementConsumeStep || []) {
6
+ if (!r || !r.id || r.is_deleted === true) continue;
7
+ // 统一字段命名:planner 层用 dev_*/screw_*(避免记忆歧义)
8
+ stepById[String(r.id)] = {
9
+ ...r,
10
+ dev_min: Number(r.consume_development_min || 0),
11
+ dev_max: Number(r.consume_development_max || 0),
12
+ screw_min: Number(r.consume_improvement_min || 0),
13
+ screw_max: Number(r.consume_improvement_max || 0),
14
+ };
15
+ }
16
+
17
+ const itemsByStepId = {};
18
+ for (const r of staticData.improvementConsumeItem || []) {
19
+ if (!r || !r.step_id || r.is_deleted === true) continue;
20
+ const k = String(r.step_id);
21
+ if (!itemsByStepId[k]) itemsByStepId[k] = [];
22
+ itemsByStepId[k].push(r);
23
+ }
24
+
25
+ return { stepById, itemsByStepId };
26
+ }
27
+
28
+ module.exports = { buildConsumeIndexes };
@@ -0,0 +1,29 @@
1
+ /* src/services/static/indexes/buildPathIndexes.js */
2
+
3
+ function toInt(x) {
4
+ const n = Number(x);
5
+ return Number.isFinite(n) ? n : null;
6
+ }
7
+
8
+ function buildPathIndexes(staticData) {
9
+ // key: "from|to" -> path array (int[])
10
+ const pathByPair = {};
11
+
12
+ for (const r of staticData.equipmentUpgradePath || []) {
13
+ if (!r || !r.equipment_path || r.is_deleted === true) continue;
14
+
15
+ const arr = Array.isArray(r.equipment_path) ? r.equipment_path : [];
16
+ const path = arr.map((x) => toInt(x)).filter((x) => x != null);
17
+ if (path.length < 1) continue;
18
+
19
+ const from = toInt(r.from_equipment_id);
20
+ const to = toInt(r.to_equipment_id);
21
+ if (from == null || to == null) continue;
22
+
23
+ pathByPair[`${from}|${to}`] = path;
24
+ }
25
+
26
+ return { pathByPair };
27
+ }
28
+
29
+ module.exports = { buildPathIndexes };
@@ -0,0 +1,57 @@
1
+ /* src/services/static/indexes/buildUpgradeIndexes.js */
2
+
3
+ function buildUpgradeIndexes(staticData) {
4
+ const normalizeRouteKind = (rk) => {
5
+ if (rk === null || rk === undefined) return "__NULL__";
6
+ const s = String(rk).trim();
7
+ return s.length ? s : "__NULL__";
8
+ };
9
+
10
+ const pairKey = (equipmentId, upgradeId) => `${Number(equipmentId)}|${Number(upgradeId)}`;
11
+ const targetKey = (equipmentId, upgradeId) => pairKey(equipmentId, upgradeId);
12
+
13
+ const targetByKey = {};
14
+ const targetsByFromEquipId = {};
15
+ for (const r of staticData.improvementUpgradeTarget || []) {
16
+ if (!r || r.is_deleted === true) continue;
17
+ if (r.equipment_id == null || r.upgrade_id == null) continue;
18
+
19
+ const normalized = {
20
+ ...r,
21
+ __routeKindKey: normalizeRouteKind(r.route_kind),
22
+ // compatibility alias for older planner code
23
+ upgrade_to_equipment_id: r.upgrade_id,
24
+ };
25
+
26
+ const k = pairKey(r.equipment_id, r.upgrade_id);
27
+ if (!targetByKey[k]) targetByKey[k] = [];
28
+ targetByKey[k].push(normalized);
29
+
30
+ const fromKey = String(r.equipment_id);
31
+ if (!targetsByFromEquipId[fromKey]) targetsByFromEquipId[fromKey] = [];
32
+ targetsByFromEquipId[fromKey].push(normalized);
33
+ }
34
+
35
+ const costByKey = {};
36
+ for (const r of staticData.improvementUpgradeCost || []) {
37
+ if (!r || r.is_deleted === true) continue;
38
+ if (r.equipment_id == null || r.upgrade_id == null) continue;
39
+
40
+ const k = pairKey(r.equipment_id, r.upgrade_id);
41
+ if (!costByKey[k]) costByKey[k] = [];
42
+ costByKey[k].push(r);
43
+ }
44
+
45
+ return {
46
+ normalizeRouteKind,
47
+ pairKey,
48
+ targetsByPair: targetByKey,
49
+ costsByPair: costByKey,
50
+ targetsByFromEquipId,
51
+ targetKey,
52
+ targetByKey,
53
+ costByKey,
54
+ };
55
+ }
56
+
57
+ module.exports = { buildUpgradeIndexes };
@@ -0,0 +1,25 @@
1
+ /* src/services/static/version/dataSourceConfig.js */
2
+
3
+ function readPackageJson() {
4
+ try {
5
+ // eslint-disable-next-line global-require
6
+ return require("../../../../package.json");
7
+ } catch {
8
+ return {};
9
+ }
10
+ }
11
+
12
+ function getDataSourceConfig() {
13
+ const pkg = readPackageJson();
14
+ const poiPlugin = pkg && pkg.poiPlugin ? pkg.poiPlugin : {};
15
+ const dataSource = poiPlugin && poiPlugin.dataSource ? poiPlugin.dataSource : {};
16
+
17
+ return {
18
+ manifestRawUrl: dataSource.manifestRawUrl || "",
19
+ rawBaseUrl: dataSource.rawBaseUrl || "",
20
+ };
21
+ }
22
+
23
+ module.exports = {
24
+ getDataSourceConfig,
25
+ };
@@ -0,0 +1,176 @@
1
+ /* src/services/static/version/dataUpdateManager.js */
2
+
3
+ const { getDataSourceConfig } = require("./dataSourceConfig");
4
+ const { readMeta, writeMeta, writeCachedFile, readCachedJson } = require("./versionStore");
5
+
6
+ const CHECK_INTERVAL_DAYS = 7;
7
+
8
+ function nowMs() {
9
+ return Date.now();
10
+ }
11
+
12
+ function toMs(iso) {
13
+ if (!iso) return 0;
14
+ const t = new Date(iso).getTime();
15
+ return Number.isFinite(t) ? t : 0;
16
+ }
17
+
18
+ function parseVersion(v) {
19
+ return String(v || "")
20
+ .split(/[^0-9]+/)
21
+ .filter((x) => x !== "")
22
+ .map((x) => Number(x));
23
+ }
24
+
25
+ function compareVersion(a, b) {
26
+ const av = parseVersion(a);
27
+ const bv = parseVersion(b);
28
+ const len = Math.max(av.length, bv.length);
29
+ for (let i = 0; i < len; i += 1) {
30
+ const x = av[i] || 0;
31
+ const y = bv[i] || 0;
32
+ if (x !== y) return x - y;
33
+ }
34
+ return 0;
35
+ }
36
+
37
+ async function fetchJson(url) {
38
+ const res = await fetch(url, { method: "GET", cache: "no-store" });
39
+ if (!res.ok) throw new Error(`http ${res.status} for ${url}`);
40
+ return res.json();
41
+ }
42
+
43
+ async function fetchText(url) {
44
+ const res = await fetch(url, { method: "GET", cache: "no-store" });
45
+ if (!res.ok) throw new Error(`http ${res.status} for ${url}`);
46
+ return res.text();
47
+ }
48
+
49
+ function getDaySerial(d) {
50
+ if (!(d instanceof Date) || !Number.isFinite(d.getTime())) return 0;
51
+ // "Natural day" comparison: convert to UTC date number and compare day diff.
52
+ return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) / (24 * 60 * 60 * 1000);
53
+ }
54
+
55
+ function shouldAutoCheck(meta) {
56
+ const lastIso = meta && meta.lastCheckAt ? String(meta.lastCheckAt) : "";
57
+ if (!lastIso) return true;
58
+ const last = new Date(lastIso);
59
+ if (!Number.isFinite(last.getTime())) return true;
60
+ const now = new Date();
61
+ const diffDays = getDaySerial(now) - getDaySerial(last);
62
+ return diffDays >= CHECK_INTERVAL_DAYS;
63
+ }
64
+
65
+ async function applyRemoteData(remoteManifest, rawBaseUrl) {
66
+ const files = Array.isArray(remoteManifest && remoteManifest.files) ? remoteManifest.files : [];
67
+ if (!files.length) {
68
+ throw new Error("remote manifest has no files");
69
+ }
70
+
71
+ // Always cache manifest itself.
72
+ writeCachedFile("data_manifest.json", JSON.stringify(remoteManifest, null, 2));
73
+
74
+ for (const f of files) {
75
+ const fileName = String(f);
76
+ const url = `${rawBaseUrl.replace(/\/+$/, "")}/${fileName}`;
77
+ const text = await fetchText(url);
78
+ writeCachedFile(fileName, text);
79
+ }
80
+ }
81
+
82
+ function getLocalDataVersion() {
83
+ const cachedManifest = readCachedJson("data_manifest.json");
84
+ if (cachedManifest && cachedManifest.data_version) return String(cachedManifest.data_version);
85
+
86
+ try {
87
+ // eslint-disable-next-line global-require
88
+ const bundled = require("../../../data/static/data_manifest.json");
89
+ return bundled && bundled.data_version ? String(bundled.data_version) : "0";
90
+ } catch {
91
+ return "0";
92
+ }
93
+ }
94
+
95
+ function getLocalManifestUpdatedAt() {
96
+ const cachedManifest = readCachedJson("data_manifest.json");
97
+ if (cachedManifest && cachedManifest.updated_at) return String(cachedManifest.updated_at);
98
+
99
+ try {
100
+ // eslint-disable-next-line global-require
101
+ const bundled = require("../../../data/static/data_manifest.json");
102
+ if (bundled && bundled.updated_at) return String(bundled.updated_at);
103
+ } catch {}
104
+ return null;
105
+ }
106
+
107
+ async function checkAndUpdateData({ force = false } = {}) {
108
+ const cfg = getDataSourceConfig();
109
+ const meta = readMeta();
110
+ const startCheckAt = new Date().toISOString();
111
+
112
+ if (!cfg.manifestRawUrl || !cfg.rawBaseUrl) {
113
+ const nextMeta = writeMeta({
114
+ lastCheckAt: startCheckAt,
115
+ });
116
+ return {
117
+ ok: false,
118
+ skipped: true,
119
+ reason: "data_source_not_configured",
120
+ meta: nextMeta,
121
+ };
122
+ }
123
+
124
+ if (!force && !shouldAutoCheck(meta)) {
125
+ return {
126
+ ok: true,
127
+ skipped: true,
128
+ reason: "within_weekly_window",
129
+ meta,
130
+ };
131
+ }
132
+
133
+ try {
134
+ const remoteManifest = await fetchJson(cfg.manifestRawUrl);
135
+ const remoteVersion = String(remoteManifest && remoteManifest.data_version ? remoteManifest.data_version : "0");
136
+ const localVersion = String(meta.lastVersion || getLocalDataVersion());
137
+ const hasNewer = compareVersion(remoteVersion, localVersion) > 0;
138
+
139
+ if (hasNewer) {
140
+ await applyRemoteData(remoteManifest, cfg.rawBaseUrl);
141
+ }
142
+
143
+ const nextMeta = writeMeta({
144
+ lastCheckAt: startCheckAt,
145
+ lastVersion: hasNewer ? remoteVersion : localVersion,
146
+ lastUpdateAt: hasNewer
147
+ ? (remoteManifest.updated_at || startCheckAt)
148
+ : (meta.lastUpdateAt || getLocalManifestUpdatedAt()),
149
+ });
150
+
151
+ return {
152
+ ok: true,
153
+ skipped: false,
154
+ updated: hasNewer,
155
+ remoteVersion,
156
+ localVersion,
157
+ meta: nextMeta,
158
+ };
159
+ } catch (e) {
160
+ const nextMeta = writeMeta({
161
+ lastCheckAt: startCheckAt,
162
+ });
163
+ return {
164
+ ok: false,
165
+ skipped: false,
166
+ error: String(e && (e.stack || e)),
167
+ meta: nextMeta,
168
+ };
169
+ }
170
+ }
171
+
172
+ module.exports = {
173
+ CHECK_INTERVAL_DAYS,
174
+ shouldAutoCheck,
175
+ checkAndUpdateData,
176
+ };
@@ -0,0 +1,205 @@
1
+ /* src/services/static/version/versionStore.js */
2
+
3
+ function getNodeModule(name) {
4
+ try {
5
+ if (typeof window !== "undefined" && typeof window.require === "function") {
6
+ return window.require(name);
7
+ }
8
+ } catch {}
9
+
10
+ try {
11
+ // eslint-disable-next-line global-require
12
+ return require(name);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ const fs = getNodeModule("fs");
19
+ const path = getNodeModule("path");
20
+
21
+ const LAST_CHECK_KEY = "kai_planner_data_last_check_at";
22
+ const LAST_UPDATE_KEY = "kai_planner_data_last_update_at";
23
+ const LAST_VERSION_KEY = "kai_planner_data_last_version";
24
+
25
+ function nowIso() {
26
+ return new Date().toISOString();
27
+ }
28
+
29
+ function canUseLocalStorage() {
30
+ try {
31
+ return typeof window !== "undefined" && window.localStorage != null;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ function getPluginRoot() {
38
+ if (!fs || !path) return null;
39
+ return path.resolve(__dirname, "../../../../");
40
+ }
41
+
42
+ function getDataDir() {
43
+ const root = getPluginRoot();
44
+ if (!root || !path) return null;
45
+ return path.join(root, "runtime_data", "static_data");
46
+ }
47
+
48
+ function getMetaPath() {
49
+ const dir = getDataDir();
50
+ if (!dir || !path) return null;
51
+ return path.join(dir, "meta.json");
52
+ }
53
+
54
+ function getCachePath() {
55
+ const dir = getDataDir();
56
+ if (!dir || !path) return null;
57
+ return path.join(dir, "cache");
58
+ }
59
+
60
+ function ensureDirs() {
61
+ if (!fs || !path) return false;
62
+ try {
63
+ const dataDir = getDataDir();
64
+ const cacheDir = getCachePath();
65
+ if (!dataDir || !cacheDir) return false;
66
+ fs.mkdirSync(dataDir, { recursive: true });
67
+ fs.mkdirSync(cacheDir, { recursive: true });
68
+ return true;
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+
74
+ function readMetaFromFile() {
75
+ if (!fs) return null;
76
+ const fp = getMetaPath();
77
+ if (!fp) return null;
78
+ try {
79
+ if (!fs.existsSync(fp)) return null;
80
+ const s = fs.readFileSync(fp, "utf8");
81
+ if (!s) return null;
82
+ return JSON.parse(s);
83
+ } catch {
84
+ return null;
85
+ }
86
+ }
87
+
88
+ function writeMetaToFile(meta) {
89
+ if (!fs) return false;
90
+ if (!ensureDirs()) return false;
91
+ const fp = getMetaPath();
92
+ if (!fp) return false;
93
+ const tmp = `${fp}.tmp`;
94
+ try {
95
+ fs.writeFileSync(tmp, JSON.stringify(meta, null, 2), "utf8");
96
+ fs.renameSync(tmp, fp);
97
+ return true;
98
+ } catch {
99
+ try {
100
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
101
+ } catch {}
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function readMetaFromLocalStorage() {
107
+ if (!canUseLocalStorage()) return null;
108
+ try {
109
+ return {
110
+ lastCheckAt: window.localStorage.getItem(LAST_CHECK_KEY) || null,
111
+ lastUpdateAt: window.localStorage.getItem(LAST_UPDATE_KEY) || null,
112
+ lastVersion: window.localStorage.getItem(LAST_VERSION_KEY) || null,
113
+ };
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ function writeMetaToLocalStorage(meta) {
120
+ if (!canUseLocalStorage()) return false;
121
+ try {
122
+ window.localStorage.setItem(LAST_CHECK_KEY, meta.lastCheckAt || "");
123
+ window.localStorage.setItem(LAST_UPDATE_KEY, meta.lastUpdateAt || "");
124
+ window.localStorage.setItem(LAST_VERSION_KEY, meta.lastVersion || "");
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+
131
+ function getDefaultMeta() {
132
+ return {
133
+ lastCheckAt: null,
134
+ lastUpdateAt: null,
135
+ lastVersion: null,
136
+ updatedAt: nowIso(),
137
+ };
138
+ }
139
+
140
+ function readMeta() {
141
+ return readMetaFromFile() || readMetaFromLocalStorage() || getDefaultMeta();
142
+ }
143
+
144
+ function writeMeta(patch) {
145
+ const prev = readMeta();
146
+ const next = {
147
+ ...prev,
148
+ ...(patch || {}),
149
+ updatedAt: nowIso(),
150
+ };
151
+ if (!writeMetaToFile(next)) {
152
+ writeMetaToLocalStorage(next);
153
+ }
154
+ return next;
155
+ }
156
+
157
+ function getCachedFilePath(fileName) {
158
+ if (!path) return null;
159
+ const base = getCachePath();
160
+ if (!base) return null;
161
+ return path.join(base, String(fileName));
162
+ }
163
+
164
+ function writeCachedFile(fileName, content) {
165
+ if (!fs) return false;
166
+ if (!ensureDirs()) return false;
167
+ const fp = getCachedFilePath(fileName);
168
+ if (!fp) return false;
169
+ const tmp = `${fp}.tmp`;
170
+ try {
171
+ fs.writeFileSync(tmp, content, "utf8");
172
+ fs.renameSync(tmp, fp);
173
+ return true;
174
+ } catch {
175
+ try {
176
+ if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
177
+ } catch {}
178
+ return false;
179
+ }
180
+ }
181
+
182
+ function readCachedJson(fileName) {
183
+ if (!fs) return null;
184
+ const fp = getCachedFilePath(fileName);
185
+ if (!fp) return null;
186
+ try {
187
+ if (!fs.existsSync(fp)) return null;
188
+ const s = fs.readFileSync(fp, "utf8");
189
+ if (!s) return null;
190
+ return JSON.parse(s);
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ module.exports = {
197
+ getDefaultMeta,
198
+ readMeta,
199
+ writeMeta,
200
+ getCachedFilePath,
201
+ writeCachedFile,
202
+ readCachedJson,
203
+ getDataDir,
204
+ getCachePath,
205
+ };
@@ -0,0 +1,11 @@
1
+ // src/services/utils/toInt.js
2
+ // 小工具:把输入安全转换成整数(NaN -> fallback)
3
+
4
+ function toInt(v, fallback = 0) {
5
+ if (v === null || v === undefined) return fallback;
6
+ const n = Number(v);
7
+ if (!Number.isFinite(n)) return fallback;
8
+ return Math.trunc(n);
9
+ }
10
+
11
+ module.exports = { toInt };