dsh-m 0.6.0 → 0.8.4

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/lib/client.js CHANGED
@@ -26,14 +26,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
26
26
  // src/client/market-state.js
27
27
  var market_state_exports = {};
28
28
  __export(market_state_exports, {
29
- MARKET_PAGE_SIZE: () => MARKET_PAGE_SIZE,
29
+ DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
30
+ MARKET_PAGE_SIZES: () => MARKET_PAGE_SIZES,
31
+ createZoneState: () => createZoneState,
30
32
  marketNotice: () => marketNotice,
31
33
  normalizeMarketQuery: () => normalizeMarketQuery,
32
34
  normalizeMarketResponse: () => normalizeMarketResponse,
35
+ pageItems: () => pageItems,
33
36
  registryNotice: () => registryNotice,
34
37
  resetPageOnFilterChange: () => resetPageOnFilterChange,
35
- sortMergedItems: () => sortMergedItems,
36
- splitCategories: () => splitCategories
38
+ zoneChips: () => zoneChips
37
39
  });
38
40
  function toSafeInt(value, fallback, min, max) {
39
41
  const n = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : fallback;
@@ -41,29 +43,60 @@ function toSafeInt(value, fallback, min, max) {
41
43
  if (max !== void 0 && n > max) return max;
42
44
  return n;
43
45
  }
44
- function normalizeMarketQuery(input) {
46
+ function createZoneState(zone) {
47
+ const z = zone === "primary" ? "primary" : "community";
48
+ return {
49
+ zone: z,
50
+ query: "",
51
+ category: null,
52
+ sort: z === "community" ? { field: "downloads", dir: "desc" } : null,
53
+ offset: 0,
54
+ limit: z === "primary" ? 96 : DEFAULT_PAGE_SIZE
55
+ };
56
+ }
57
+ function normalizeMarketQuery(input, zone = "community") {
58
+ const z = zone === "primary" ? "primary" : "community";
45
59
  const raw = input && typeof input === "object" ? input : {};
46
60
  const query = typeof raw.query === "string" ? raw.query.trim() : "";
47
- const category = typeof raw.category === "string" && (CATEGORIES.includes(raw.category) || COMMUNITY_SLUG_RE.test(raw.category)) ? raw.category : null;
61
+ const catOk = typeof raw.category === "string" && raw.category !== "" && (z === "primary" ? CURATED_IDS.has(raw.category) || SLUG_RE.test(raw.category) : SLUG_RE.test(raw.category));
62
+ const category = catOk ? raw.category : null;
48
63
  const offset = toSafeInt(raw.offset, 0, 0);
49
- const limit = toSafeInt(raw.limit, MARKET_PAGE_SIZE, 1, MARKET_PAGE_SIZE);
50
- const primaryOnly = raw.primaryOnly === true;
51
- return { query, category, offset, limit, primaryOnly };
64
+ const limit = toSafeInt(raw.limit, DEFAULT_PAGE_SIZE, 1, 96);
65
+ let sort = null;
66
+ if (z === "community" && raw.sort && typeof raw.sort === "object" && !Array.isArray(raw.sort)) {
67
+ const field = raw.sort.field;
68
+ const dir = raw.sort.dir;
69
+ if (SORT_FIELDS.has(field) && (dir === "asc" || dir === "desc")) sort = { field, dir };
70
+ }
71
+ return { zone: z, source: z, query, category, sort, offset, limit };
52
72
  }
53
73
  function resetPageOnFilterChange(previous, next) {
54
74
  const prev = previous && typeof previous === "object" ? previous : {};
55
75
  const merged = { ...next };
56
- if (prev.query !== next.query || prev.category !== next.category) {
76
+ if (prev.query !== next.query || prev.category !== next.category || !sameSort(prev.sort, next.sort)) {
57
77
  merged.offset = 0;
58
78
  }
59
79
  return merged;
60
80
  }
81
+ function pageItems(current, totalPages) {
82
+ const last = Math.max(1, Math.floor(totalPages));
83
+ const cur = Math.min(Math.max(1, Math.floor(current)), last);
84
+ if (last <= 7) return Array.from({ length: last }, (_, i) => i + 1);
85
+ const out = [1];
86
+ const from = Math.max(2, cur - 1);
87
+ const to = Math.min(last - 1, cur + 1);
88
+ if (from > 2) out.push("...");
89
+ for (let p = from; p <= to; p++) out.push(p);
90
+ if (to < last - 1) out.push("...");
91
+ out.push(last);
92
+ return out;
93
+ }
61
94
  function normalizeMarketResponse(raw) {
62
95
  const body = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : {};
63
96
  const items = Array.isArray(body.items) ? body.items.filter((it) => it && typeof it === "object") : [];
64
97
  const total = toSafeInt(body.total, items.length, 0);
65
98
  const offset = toSafeInt(body.offset, 0, 0);
66
- const limit = toSafeInt(body.limit, MARKET_PAGE_SIZE, 1);
99
+ const limit = toSafeInt(body.limit, DEFAULT_PAGE_SIZE, 1);
67
100
  let categoryCounts = {};
68
101
  if (body.categoryCounts && typeof body.categoryCounts === "object" && !Array.isArray(body.categoryCounts)) {
69
102
  for (const [key, value] of Object.entries(body.categoryCounts)) {
@@ -88,7 +121,9 @@ function normalizeMarketResponse(raw) {
88
121
  skippedDirty: typeof c.skippedDirty === "number" && Number.isFinite(c.skippedDirty) ? c.skippedDirty : 0,
89
122
  skippedSubpathNoNpm: typeof c.skippedSubpathNoNpm === "number" && Number.isFinite(c.skippedSubpathNoNpm) ? c.skippedSubpathNoNpm : 0,
90
123
  errors: Array.isArray(c.errors) ? c.errors.map(String) : [],
91
- warnings: Array.isArray(c.warnings) ? c.warnings.map(String) : []
124
+ warnings: Array.isArray(c.warnings) ? c.warnings.map(String) : [],
125
+ categoryLabels: narrowCategoryLabels(c.categoryLabels),
126
+ categoryLabelsEn: narrowCategoryLabels(c.categoryLabelsEn)
92
127
  };
93
128
  return {
94
129
  items,
@@ -103,53 +138,76 @@ function normalizeMarketResponse(raw) {
103
138
  latestTimedOut: body.latestTimedOut === true
104
139
  };
105
140
  }
141
+ function narrowCategoryLabels(raw) {
142
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
143
+ const out = {};
144
+ let n = 0;
145
+ for (const [key, value] of Object.entries(raw)) {
146
+ if (typeof value === "string" && value !== "") {
147
+ out[key] = value;
148
+ n += 1;
149
+ }
150
+ }
151
+ return n > 0 ? out : void 0;
152
+ }
106
153
  function registryNotice(summary, total) {
107
154
  const s = summary && typeof summary === "object" ? summary : {};
108
- let key = "notice.default";
109
- if (s.status === "unavailable") key = "notice.unavailable";
110
- else if (s.stale || s.status === "stale") key = "notice.stale";
111
- else if (!s.isDefault) key = "notice.custom";
112
- return { key, count: typeof total === "number" && Number.isFinite(total) ? total : 0 };
155
+ if (s.status === "unavailable") return { key: "notice.unavailable" };
156
+ return null;
113
157
  }
114
- function splitCategories(categoryCounts) {
158
+ function zoneChips(categoryCounts, categoryLabels, zone) {
115
159
  const counts = categoryCounts && typeof categoryCounts === "object" && !Array.isArray(categoryCounts) ? categoryCounts : {};
116
- const curated = CURATED_CATEGORIES.map(({ id, label }) => ({ id, label, count: typeof counts[id] === "number" ? counts[id] : 0 }));
117
- const community = [];
160
+ if (zone === "primary") {
161
+ return CURATED_ORDER.map((id) => ({
162
+ id,
163
+ label: CURATED_LABELS[id],
164
+ labelKey: `cat.${id}`,
165
+ count: typeof counts[id] === "number" && Number.isFinite(counts[id]) ? counts[id] : 0
166
+ }));
167
+ }
168
+ const labels = categoryLabels && typeof categoryLabels === "object" && !Array.isArray(categoryLabels) ? categoryLabels : {};
169
+ const countOf = (id) => typeof counts[id] === "number" && Number.isFinite(counts[id]) ? counts[id] : 0;
170
+ const known = [];
171
+ const seen = /* @__PURE__ */ new Set();
172
+ for (const [id, label] of Object.entries(labels)) {
173
+ if (typeof label !== "string" || label === "") continue;
174
+ const count = countOf(id);
175
+ if (count === 0) continue;
176
+ known.push({ id, label, count });
177
+ seen.add(id);
178
+ }
118
179
  const unknown = [];
119
180
  for (const [id, raw] of Object.entries(counts)) {
181
+ if (seen.has(id) || CURATED_IDS.has(id)) continue;
120
182
  const count = typeof raw === "number" && Number.isFinite(raw) ? raw : 0;
121
- if (CURATED_IDS.has(id)) continue;
122
- if (COMMUNITY_KNOWN_LABELS[id]) community.push({ id, label: COMMUNITY_KNOWN_LABELS[id], count });
123
- else if (!SHARED_BUCKETS.has(id)) unknown.push({ id, label: id, count });
183
+ if (count === 0) continue;
184
+ unknown.push({ id, label: id, count });
124
185
  }
125
- return { curated, community, unknown };
186
+ return [...known, ...unknown];
126
187
  }
127
188
  function marketNotice(registryState, community) {
128
- const base = registryNotice(registryState, void 0);
189
+ const out = {};
129
190
  const c = community && typeof community === "object" ? community : {};
130
- if (c.status === "stale") base.communityStale = true;
191
+ if (c.status === "stale") out.communityStale = true;
131
192
  if (registryState && registryState.status === "unavailable" && (c.status === "ready" || c.status === "stale")) {
132
- base.communityFallback = true;
193
+ out.communityFallback = true;
133
194
  }
134
- return base;
135
- }
136
- function sortMergedItems(items) {
137
- const list = Array.isArray(items) ? items.filter((it) => it && typeof it === "object") : [];
138
- const primary = list.filter((it) => it.community !== true);
139
- const community = list.filter((it) => it.community === true).slice().sort((a, b) => {
140
- const da = typeof a.downloads === "number" && Number.isFinite(a.downloads) ? a.downloads : -1;
141
- const db = typeof b.downloads === "number" && Number.isFinite(b.downloads) ? b.downloads : -1;
142
- if (da !== db) return db - da;
143
- return String(a.name).localeCompare(String(b.name));
144
- });
145
- return [...primary, ...community];
195
+ const notice = registryNotice(registryState);
196
+ if (notice) out.notice = notice;
197
+ return out;
146
198
  }
147
- var MARKET_PAGE_SIZE, CATEGORIES, FALLBACK_REGISTRY_STATE, COMMUNITY_SLUG_RE, CURATED_CATEGORIES, CURATED_IDS, SHARED_BUCKETS, COMMUNITY_KNOWN_LABELS, FALLBACK_COMMUNITY;
199
+ var MARKET_PAGE_SIZES, DEFAULT_PAGE_SIZE, CURATED_ORDER, CURATED_LABELS, CURATED_IDS, SLUG_RE, SORT_FIELDS, sameSort, FALLBACK_REGISTRY_STATE, FALLBACK_COMMUNITY;
148
200
  var init_market_state = __esm({
149
201
  "src/client/market-state.js"() {
150
202
  "use strict";
151
- MARKET_PAGE_SIZE = 50;
152
- CATEGORIES = ["market", "tools", "ui", "search", "other"];
203
+ MARKET_PAGE_SIZES = [24, 48, 96];
204
+ DEFAULT_PAGE_SIZE = 24;
205
+ CURATED_ORDER = ["market", "tools", "ui", "search", "other"];
206
+ CURATED_LABELS = { market: "\u5E02\u573A", tools: "\u5DE5\u5177", ui: "\u754C\u9762", search: "\u641C\u7D22", other: "\u5176\u4ED6" };
207
+ CURATED_IDS = new Set(CURATED_ORDER);
208
+ SLUG_RE = /^[a-z0-9-]{1,32}$/;
209
+ SORT_FIELDS = /* @__PURE__ */ new Set(["downloads", "stars", "added"]);
210
+ sameSort = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
153
211
  FALLBACK_REGISTRY_STATE = {
154
212
  configuredAddress: "",
155
213
  activeAddress: null,
@@ -161,38 +219,6 @@ var init_market_state = __esm({
161
219
  errors: [],
162
220
  count: 0
163
221
  };
164
- COMMUNITY_SLUG_RE = /^[a-z0-9-]{1,32}$/;
165
- CURATED_CATEGORIES = [
166
- { id: "market", label: "\u5E02\u573A" },
167
- { id: "tools", label: "\u5DE5\u5177" },
168
- { id: "ui", label: "\u754C\u9762" },
169
- { id: "search", label: "\u641C\u7D22" },
170
- { id: "other", label: "\u5176\u4ED6" }
171
- ];
172
- CURATED_IDS = new Set(CURATED_CATEGORIES.map((c) => c.id));
173
- SHARED_BUCKETS = /* @__PURE__ */ new Set(["ui", "tools", "market"]);
174
- COMMUNITY_KNOWN_LABELS = {
175
- agi: "AGI \u67B6\u6784\u63A2\u7D22",
176
- usage: "\u7528\u91CF\u4E0E\u8BA1\u8D39",
177
- theme: "\u4E3B\u9898\u4E0E\u5916\u89C2",
178
- model: "\u6A21\u578B\u4E0E\u8D26\u53F7\u63A5\u5165",
179
- identity: "\u8EAB\u4EFD\u4E0E\u901A\u4FE1",
180
- session: "\u4F1A\u8BDD\u4E0E\u6D88\u606F",
181
- memory: "\u8BB0\u5FC6",
182
- wsl: "WSL \u4E0E Windows \u4E92\u64CD\u4F5C",
183
- browser: "\u6D4F\u89C8\u5668\u4E0E\u7F51\u9875",
184
- vision: "\u89C6\u89C9\u4E0E\u591A\u6A21\u6001",
185
- voice: "\u8BED\u97F3\u4E0E\u97F3\u9891",
186
- docs: "\u6587\u6863\u4E0E\u6E32\u67D3",
187
- skill: "\u6280\u80FD\u5305",
188
- workflow: "\u5DE5\u4F5C\u6D41\u4E0E\u81EA\u52A8\u5316",
189
- git: "Git \u4E0E\u4EE3\u7801\u8BC4\u5BA1",
190
- notify: "\u901A\u77E5\u4E0E\u96C6\u6210",
191
- dev: "\u5F00\u53D1\u4E0E\u8FD0\u884C\u65F6",
192
- security: "\u5B89\u5168\u4E0E\u6743\u9650",
193
- remote: "\u8FDC\u7A0B\u4E0E\u79FB\u52A8\u7AEF",
194
- fun: "\u5A31\u4E50"
195
- };
196
222
  FALLBACK_COMMUNITY = {
197
223
  enabled: false,
198
224
  status: "disabled",
@@ -578,14 +604,375 @@ var init_view_refresh = __esm({
578
604
  }
579
605
  });
580
606
 
607
+ // src/client/operations.js
608
+ var operations_exports = {};
609
+ __export(operations_exports, {
610
+ OP_STORAGE_KEY: () => OP_STORAGE_KEY,
611
+ createOperationsStore: () => createOperationsStore,
612
+ createOpsPump: () => createOpsPump,
613
+ drainRestored: () => drainRestored,
614
+ opAppliesTo: () => opAppliesTo,
615
+ restoreRecords: () => restoreRecords
616
+ });
617
+ function safeParse(raw) {
618
+ try {
619
+ const v = JSON.parse(raw);
620
+ return Array.isArray(v) ? v.filter((r) => r && typeof r === "object" && typeof r.id === "string") : [];
621
+ } catch {
622
+ return [];
623
+ }
624
+ }
625
+ function storageOf(storage) {
626
+ if (!storage || typeof storage.getItem !== "function" || typeof storage.setItem !== "function") {
627
+ return { degraded: true, read: () => null, write: () => {
628
+ throw new Error("degraded");
629
+ } };
630
+ }
631
+ return {
632
+ degraded: false,
633
+ read: () => {
634
+ try {
635
+ return storage.getItem(OP_STORAGE_KEY);
636
+ } catch {
637
+ return null;
638
+ }
639
+ },
640
+ write: (text) => {
641
+ storage.setItem(OP_STORAGE_KEY, text);
642
+ }
643
+ };
644
+ }
645
+ function createOperationsStore(storage) {
646
+ const s = storageOf(storage);
647
+ let records = safeParse(s.read());
648
+ let persistDegraded = false;
649
+ const persist = () => {
650
+ if (persistDegraded) return;
651
+ try {
652
+ s.write(JSON.stringify(records.map((r) => r.meta && r.meta.session === true ? { ...r, meta: { ...r.meta, session: void 0 } } : r)));
653
+ } catch {
654
+ persistDegraded = true;
655
+ }
656
+ };
657
+ return {
658
+ /** @returns {OperationRecord[]} 当前记录(浅拷贝) */
659
+ list() {
660
+ return records.map((r) => ({ ...r }));
661
+ },
662
+ get persistDegraded() {
663
+ return persistDegraded;
664
+ },
665
+ /** upsert 按 id 覆盖;不带 id 视为新记录生成 id。@returns 存储后的记录(含 id/时间戳)。 */
666
+ upsert(rec) {
667
+ const now = Date.now();
668
+ const id = typeof rec.id === "string" && rec.id !== "" ? rec.id : `op-${now}-${Math.random().toString(36).slice(2, 8)}`;
669
+ const merged = { createdAt: now, ...rec, id, updatedAt: now };
670
+ const idx = records.findIndex((r) => r.id === id);
671
+ let stored;
672
+ if (idx >= 0) {
673
+ records[idx] = stored = { ...records[idx], ...merged, createdAt: records[idx].createdAt };
674
+ } else {
675
+ records = [...records, merged];
676
+ stored = merged;
677
+ }
678
+ persist();
679
+ return { ...stored };
680
+ },
681
+ remove(id) {
682
+ records = records.filter((r) => r.id !== id);
683
+ persist();
684
+ return this.list();
685
+ },
686
+ /** 只清 done/warned;failed/superseded 保留供回看(评审共识:不把失败静默抹掉)。 */
687
+ clearFinished() {
688
+ records = records.filter((r) => !TERMINAL_CLEARABLE.has(r.status));
689
+ persist();
690
+ return this.list();
691
+ },
692
+ /** 供 restore 之后的批量覆写(恢复校验结果落库)。
693
+ * 合并语义(0.7.0 评审 P1/R2·N1/R3 修正):以**当前**记录为权威按 id 覆写——
694
+ * - 快照外新增(恢复校验窗口内用户发起的操作)保留,不被旧快照抹掉;
695
+ * - 快照内但窗口期被用户删除的记录不复活;
696
+ * - 活会话记录(meta.session)不覆写——泵在拾取(queued→running)时打标,覆盖其任意
697
+ * 后续状态(含 done),防恢复快照旧态回卷;**无标记的 crashed running 照常被改标为
698
+ * failed「进程重启中断」**(恢复语义核心,R3:不得用 status 短路——泵拥有与崩溃残留
699
+ * 在此处不可区分,session 标记才是唯一可信的所有权凭证)。 */
700
+ replaceAll(next) {
701
+ const byId = new Map((Array.isArray(next) ? next : []).map((r) => [r.id, r]));
702
+ records = records.map((r) => {
703
+ const incoming = byId.get(r.id);
704
+ if (!incoming) return { ...r };
705
+ if (r.meta && r.meta.session === true) return { ...r };
706
+ return { ...incoming };
707
+ });
708
+ persist();
709
+ return this.list();
710
+ }
711
+ };
712
+ }
713
+ async function restoreRecords(records, stillApplies) {
714
+ const list = Array.isArray(records) ? records : [];
715
+ const out = [];
716
+ for (const rec of list) {
717
+ if (!rec || typeof rec !== "object") continue;
718
+ if (rec.meta && rec.meta.session === true) {
719
+ out.push({ ...rec });
720
+ continue;
721
+ }
722
+ if (rec.status === "queued") {
723
+ let applies = false;
724
+ try {
725
+ applies = await stillApplies(rec);
726
+ } catch {
727
+ applies = false;
728
+ }
729
+ out.push(
730
+ applies ? { ...rec, status: "queued" } : { ...rec, status: "superseded", error: "\u6062\u590D\u65F6\u524D\u63D0\u6D88\u5931\uFF08\u6821\u9A8C\u4E0D\u6210\u7ACB\uFF09", updatedAt: Date.now() }
731
+ );
732
+ } else if (rec.status === "input") {
733
+ out.push({ ...rec, status: "failed", error: "\u5F85\u51B3\u786E\u8BA4\u4E0D\u53EF\u6062\u590D\uFF0C\u8BF7\u91CD\u65B0\u53D1\u8D77", updatedAt: Date.now() });
734
+ } else if (rec.status === "running") {
735
+ out.push({ ...rec, status: "failed", error: "\u8FDB\u7A0B\u91CD\u542F\u4E2D\u65AD", updatedAt: Date.now() });
736
+ } else {
737
+ out.push({ ...rec });
738
+ }
739
+ }
740
+ return out;
741
+ }
742
+ function opAppliesTo(rec, installedItems) {
743
+ const items = Array.isArray(installedItems) ? installedItems : [];
744
+ const has = items.some(
745
+ (x) => x && (x.pkg === rec.target || rec.meta && typeof rec.meta.npm === "string" && x.pkg === rec.meta.npm || rec.meta && typeof rec.meta.github === "string" && x.registryGithub === rec.meta.github)
746
+ );
747
+ return rec.kind === "install" ? !has : has;
748
+ }
749
+ function createOpsPump(store, getCtx) {
750
+ const executors = /* @__PURE__ */ new Map();
751
+ const waiters = /* @__PURE__ */ new Map();
752
+ let pumping = false;
753
+ async function pump() {
754
+ if (pumping) return;
755
+ pumping = true;
756
+ try {
757
+ for (; ; ) {
758
+ const queued = store.list().find((r) => r.status === "queued");
759
+ if (!queued) break;
760
+ const waitersEntry = waiters.get(queued.id);
761
+ const executor = executors.get(queued.id);
762
+ waiters.delete(queued.id);
763
+ executors.delete(queued.id);
764
+ const ctx = getCtx();
765
+ const finalize = (patch) => {
766
+ store.upsert({ id: queued.id, ...patch });
767
+ const c = getCtx();
768
+ if (c && c.syncOps) c.syncOps();
769
+ };
770
+ if (ctx && ctx.stillApplies) {
771
+ let applies = true;
772
+ try {
773
+ applies = await ctx.stillApplies(queued);
774
+ } catch {
775
+ applies = false;
776
+ }
777
+ if (!applies) {
778
+ finalize({ status: "superseded", error: "\u6267\u884C\u65F6\u524D\u63D0\u6D88\u5931\uFF08\u5DF2\u624B\u52A8\u5904\u7406\uFF1F\uFF09" });
779
+ if (waitersEntry) {
780
+ waitersEntry.reject(Object.assign(new Error("op superseded"), { opSuperseded: true, opId: queued.id }));
781
+ }
782
+ continue;
783
+ }
784
+ }
785
+ store.upsert({ id: queued.id, status: "running", meta: { ...queued.meta || {}, session: true } });
786
+ if (ctx && ctx.syncOps) ctx.syncOps();
787
+ let value;
788
+ let err = null;
789
+ try {
790
+ if (executor) {
791
+ value = await executor.exec();
792
+ } else if (ctx && ctx.dispatchRestored) {
793
+ const r = await ctx.dispatchRestored(queued);
794
+ if (!r || !r.ok) err = Object.assign(new Error(r && r.error || "unknown"), r && r.issue ? { issue: r.issue } : {});
795
+ } else {
796
+ err = new Error("\u65E0\u6267\u884C\u4E0A\u4E0B\u6587\uFF08\u9762\u677F\u672A\u6302\u8F7D\uFF09");
797
+ }
798
+ } catch (e) {
799
+ err = e;
800
+ }
801
+ if (!err) {
802
+ finalize({ status: value && value.opWarning ? "warned" : "done", warning: value && value.opWarning });
803
+ if (waitersEntry) waitersEntry.resolve(value);
804
+ } else {
805
+ const isInput = Boolean(err.issue);
806
+ finalize(isInput ? { status: "input", inputKind: "peer-incompatible" } : { status: "failed", error: String(err.message || err) });
807
+ err.opId = queued.id;
808
+ if (waitersEntry) waitersEntry.reject(err);
809
+ }
810
+ }
811
+ } finally {
812
+ pumping = false;
813
+ }
814
+ }
815
+ return {
816
+ /** 入队并返回该记录终态的 Promise(waiters 持原始错误)。 */
817
+ enqueue(record, exec) {
818
+ const stored = store.upsert(record);
819
+ const ctx = getCtx();
820
+ if (ctx && ctx.syncOps) ctx.syncOps();
821
+ return new Promise((resolve, reject) => {
822
+ waiters.set(stored.id, { resolve, reject });
823
+ executors.set(stored.id, { exec });
824
+ void pump();
825
+ });
826
+ },
827
+ /** 启动泵(恢复流程改标后调用,处理 restored queued)。 */
828
+ kick() {
829
+ void pump();
830
+ }
831
+ };
832
+ }
833
+ async function drainRestored(store, dispatch, stillApplies) {
834
+ const queue = store.list().filter((r) => r.status === "queued");
835
+ for (const rec of queue) {
836
+ let applies = false;
837
+ try {
838
+ applies = await stillApplies(rec);
839
+ } catch {
840
+ applies = false;
841
+ }
842
+ if (!applies) {
843
+ store.upsert({ ...rec, status: "superseded", error: "\u6267\u884C\u65F6\u524D\u63D0\u6D88\u5931\uFF08\u5DF2\u624B\u52A8\u5904\u7406\uFF1F\uFF09" });
844
+ continue;
845
+ }
846
+ store.upsert({ ...rec, status: "running" });
847
+ let result;
848
+ try {
849
+ result = await dispatch(rec);
850
+ } catch (e) {
851
+ result = { ok: false, error: String(e && e.message || e) };
852
+ }
853
+ if (result && result.ok) {
854
+ store.upsert({ ...rec, status: result.warning ? "warned" : "done", warning: result.warning });
855
+ } else {
856
+ store.upsert({ ...rec, status: "failed", error: result && result.error || "unknown" });
857
+ }
858
+ }
859
+ }
860
+ var OP_STORAGE_KEY, TERMINAL_CLEARABLE;
861
+ var init_operations = __esm({
862
+ "src/client/operations.js"() {
863
+ "use strict";
864
+ OP_STORAGE_KEY = "dshm-operations";
865
+ TERMINAL_CLEARABLE = /* @__PURE__ */ new Set(["done", "warned"]);
866
+ }
867
+ });
868
+
869
+ // src/client/favorites.js
870
+ var favorites_exports = {};
871
+ __export(favorites_exports, {
872
+ FAV_STORAGE_KEY: () => FAV_STORAGE_KEY,
873
+ createFavoritesStore: () => createFavoritesStore,
874
+ partitionStale: () => partitionStale
875
+ });
876
+ function safeParse2(raw) {
877
+ try {
878
+ const v = JSON.parse(raw);
879
+ return Array.isArray(v) ? v.filter((f) => f && typeof f === "object" && typeof f.id === "string") : [];
880
+ } catch {
881
+ return [];
882
+ }
883
+ }
884
+ function createFavoritesStore(storage) {
885
+ const memory = { list: [] };
886
+ const canPersist = () => storage && typeof storage.getItem === "function" && typeof storage.setItem === "function";
887
+ let degraded = false;
888
+ const persist = (list) => {
889
+ if (!canPersist() || degraded) return;
890
+ try {
891
+ storage.setItem(FAV_STORAGE_KEY, JSON.stringify(list));
892
+ } catch {
893
+ degraded = true;
894
+ }
895
+ };
896
+ return {
897
+ get persistDegraded() {
898
+ return degraded || !canPersist();
899
+ },
900
+ /** @returns {FavoriteEntry[]} */
901
+ list() {
902
+ let raw = null;
903
+ if (canPersist() && !degraded) {
904
+ try {
905
+ raw = storage.getItem(FAV_STORAGE_KEY);
906
+ } catch {
907
+ degraded = true;
908
+ raw = null;
909
+ }
910
+ }
911
+ if (!canPersist() || degraded) return memory.list.map((f) => ({ ...f, snapshot: { ...f.snapshot } }));
912
+ return safeParse2(raw).map((f) => ({ ...f, snapshot: { ...f.snapshot } }));
913
+ },
914
+ /** toggle 收藏:已存在则移除,否则存快照。@returns 操作后的全量列表。 */
915
+ toggle(snapshot) {
916
+ const list = this.list();
917
+ const next = list.some((f) => f.id === snapshot.id) ? list.filter((f) => f.id !== snapshot.id) : [...list, { id: snapshot.id, savedAt: Date.now(), snapshot: { ...snapshot } }];
918
+ if (!canPersist() || degraded) memory.list = next;
919
+ persist(next);
920
+ return this.list();
921
+ },
922
+ /** 批量移除(清理失效收藏)。 */
923
+ removeIds(ids) {
924
+ const set = new Set(ids);
925
+ const next = this.list().filter((f) => !set.has(f.id));
926
+ if (!canPersist() || degraded) memory.list = next;
927
+ persist(next);
928
+ return this.list();
929
+ }
930
+ };
931
+ }
932
+ async function partitionStale(favorites, lookup2) {
933
+ const list = Array.isArray(favorites) ? favorites : [];
934
+ const live = [];
935
+ const stale = [];
936
+ const CONCURRENCY = 8;
937
+ let cursor = 0;
938
+ const workers = Array.from({ length: Math.min(CONCURRENCY, list.length) }, async () => {
939
+ for (; ; ) {
940
+ const i = cursor;
941
+ cursor += 1;
942
+ if (i >= list.length) return;
943
+ const fav = list[i];
944
+ let exists = false;
945
+ try {
946
+ exists = await lookup2(fav);
947
+ } catch {
948
+ exists = false;
949
+ }
950
+ if (exists) live.push(fav);
951
+ else stale.push(fav);
952
+ }
953
+ });
954
+ await Promise.all(workers);
955
+ const order = new Map(list.map((f, i) => [f.id, i]));
956
+ live.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
957
+ stale.sort((a, b) => (order.get(a.id) ?? 0) - (order.get(b.id) ?? 0));
958
+ return { live, stale };
959
+ }
960
+ var FAV_STORAGE_KEY;
961
+ var init_favorites = __esm({
962
+ "src/client/favorites.js"() {
963
+ "use strict";
964
+ FAV_STORAGE_KEY = "dshm-favorites";
965
+ }
966
+ });
967
+
581
968
  // src/client/main.jsx
582
969
  var React = require("react");
583
970
  var rd = require("react-dom");
584
971
  var h = React.createElement;
585
- var { useState, useEffect, useCallback, useMemo, useRef } = React;
972
+ var { useState, useEffect, useLayoutEffect, useCallback, useMemo, useRef } = React;
586
973
  var PLUGIN_ID = "dsh-m";
587
974
  var API = "/dshm";
588
- var { MARKET_PAGE_SIZE: MARKET_PAGE_SIZE2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, splitCategories: splitCategories2, marketNotice: marketNotice2, sortMergedItems: sortMergedItems2 } = (init_market_state(), __toCommonJS(market_state_exports));
975
+ var { DEFAULT_PAGE_SIZE: DEFAULT_PAGE_SIZE2, MARKET_PAGE_SIZES: MARKET_PAGE_SIZES2, pageItems: pageItems2, createZoneState: createZoneState2, normalizeMarketQuery: normalizeMarketQuery2, resetPageOnFilterChange: resetPageOnFilterChange2, normalizeMarketResponse: normalizeMarketResponse2, registryNotice: registryNotice2, zoneChips: zoneChips2, marketNotice: marketNotice2 } = (init_market_state(), __toCommonJS(market_state_exports));
589
976
  var { createMarkdown: createMarkdown2 } = (init_markdown(), __toCommonJS(markdown_exports));
590
977
  var { ExtLink, MdImg, renderMarkdown } = createMarkdown2(h);
591
978
  var { installedViewModel: installedViewModel2, registrySourceKey: registrySourceKey2 } = (init_installed_view(), __toCommonJS(installed_view_exports));
@@ -593,6 +980,8 @@ var { toggleViewModel: toggleViewModel2, toggleNoticeKeys: toggleNoticeKeys2 } =
593
980
  var { pickPayload: pickPayload2, parseToolArgs: parseToolArgs2 } = (init_tool_view(), __toCommonJS(tool_view_exports));
594
981
  var { RESTART_POLL_MS: RESTART_POLL_MS2, RESTART_DEADLINE_MS: RESTART_DEADLINE_MS2, nextRestartWait: nextRestartWait2, isAmbiguousRestartRequestError: isAmbiguousRestartRequestError2 } = (init_restart_wait(), __toCommonJS(restart_wait_exports));
595
982
  var { refreshAfterMutation: refreshAfterMutation2 } = (init_view_refresh(), __toCommonJS(view_refresh_exports));
983
+ var { createOperationsStore: createOperationsStore2, restoreRecords: restoreRecords2, createOpsPump: createOpsPump2, opAppliesTo: opAppliesTo2 } = (init_operations(), __toCommonJS(operations_exports));
984
+ var { createFavoritesStore: createFavoritesStore2, partitionStale: partitionStale2 } = (init_favorites(), __toCommonJS(favorites_exports));
596
985
  var ZH = {
597
986
  "market.title": "\u63D2\u4EF6\u5E02\u573A",
598
987
  "tab.market": "\u5E02\u573A",
@@ -604,6 +993,47 @@ var ZH = {
604
993
  "cat.ui": "\u754C\u9762",
605
994
  "cat.search": "\u641C\u7D22",
606
995
  "cat.other": "\u5176\u4ED6",
996
+ "zone.community": "\u793E\u533A",
997
+ "zone.primary": "\u7CBE\u9009",
998
+ "zone.favorites": "\u6536\u85CF",
999
+ "badge.deprecated": "\u5DF2\u5F03\u7528",
1000
+ "sub.snapshot": "v{v}\uFF08\u76EE\u5F55\u5FEB\u7167\uFF09",
1001
+ "badge.verified": "\u5DF2\u5B9E\u6D4B",
1002
+ "modal.category": "\u5206\u7C7B",
1003
+ "modal.added": "\u6536\u5F55\u65E5\u671F",
1004
+ "modal.dlwindow": "\u4E0B\u8F7D\u91CF\uFF0830 \u5929\u7A97\u53E3\uFF09",
1005
+ "modal.checkedat": "\u6838\u5BF9\u4E8E",
1006
+ "modal.dlnone": "\u65E0\u7A97\u53E3\u6570\u636E",
1007
+ "modal.verified": "\u5B9E\u6D4B\u7248\u672C",
1008
+ "modal.tags": "\u6807\u7B7E",
1009
+ "modal.replacement": "\u5DF2\u5F03\u7528 \xB7 \u66FF\u4EE3",
1010
+ "modal.installcmd": "\u5B89\u88C5\u547D\u4EE4",
1011
+ "modal.copy": "\u590D\u5236",
1012
+ "modal.copied": "\u5DF2\u590D\u5236",
1013
+ "op.clear": "\u6E05\u9664\u5DF2\u5B8C\u6210",
1014
+ "op.superseded.note": "{target} \u5DF2\u8DF3\u8FC7\uFF08\u524D\u63D0\u5DF2\u4E0D\u6210\u7ACB\u6216\u5DF2\u624B\u52A8\u5904\u7406\uFF09",
1015
+ "op.cancelled": "\u7528\u6237\u653E\u5F03\u786E\u8BA4",
1016
+ "op.remove": "\u79FB\u9664\u8BB0\u5F55",
1017
+ "op.panel.jump": "\u67E5\u770B\u64CD\u4F5C\u9762\u677F",
1018
+ "op.kind.install": "\u5B89\u88C5",
1019
+ "op.kind.upgrade": "\u5347\u7EA7",
1020
+ "op.kind.uninstall": "\u5378\u8F7D",
1021
+ "op.kind.toggle": "\u5F00\u5173",
1022
+ "op.status.queued": "\u6392\u961F\u4E2D",
1023
+ "op.status.running": "\u8FDB\u884C\u4E2D",
1024
+ "op.status.input": "\u5F85\u51B3",
1025
+ "op.status.done": "\u5B8C\u6210",
1026
+ "op.status.warned": "\u5E26\u8B66\u544A",
1027
+ "op.status.failed": "\u5931\u8D25",
1028
+ "op.status.superseded": "\u5DF2\u8DF3\u8FC7",
1029
+ "favorites.hint": "\u8FD8\u6CA1\u6709\u6536\u85CF\u2014\u2014\u53BB\u793E\u533A/\u7CBE\u9009\u9875\u70B9\u63D2\u4EF6\u5361\u7247\u53F3\u4E0A\u89D2\u7684 \u2606 \u6536\u85CF",
1030
+ "favorites.stale": "{n} \u6761\u6536\u85CF\u5DF2\u4ECE\u76EE\u5F55\u4E0B\u67B6",
1031
+ "favorites.clean": "\u6E05\u7406\u5931\u6548\u6536\u85CF",
1032
+ "favorites.checking": "\u6821\u9A8C\u6536\u85CF\u6709\u6548\u6027\u4E2D\u2026",
1033
+ "favorites.stalebadge": "\u5DF2\u4E0B\u67B6",
1034
+ "fav.add": "\u6536\u85CF",
1035
+ "fav.remove": "\u53D6\u6D88\u6536\u85CF",
1036
+ "common.clear": "\u6E05\u7A7A",
607
1037
  "search.ph": "\u641C\u7D22\u540D\u79F0 / \u63CF\u8FF0 / \u6807\u7B7E\u2026",
608
1038
  "common.refresh": "\u5237\u65B0",
609
1039
  "common.close": "\u5173\u95ED",
@@ -611,11 +1041,12 @@ var ZH = {
611
1041
  "common.ok": "\u77E5\u9053\u4E86",
612
1042
  "common.none": "\u2014",
613
1043
  "market.loading": "\u52A0\u8F7D\u6536\u5F55\u6E05\u5355\u4E2D\u2026 ",
614
- "market.empty": "\u6CA1\u6709\u5339\u914D\u7684\u6536\u5F55\u6761\u76EE",
1044
+ "market.empty": "\u65E0\u5339\u914D\u63D2\u4EF6\uFF0C\u8BD5\u8BD5\u5176\u4ED6\u5173\u952E\u8BCD\u6216\u5206\u7C7B",
615
1045
  "installed.loading": "\u8BFB\u53D6 web profile \u4E2D\u2026 ",
616
1046
  "installed.empty": "web profile \u5C1A\u672A\u5B89\u88C5\u4EFB\u4F55 dsh \u63D2\u4EF6",
617
1047
  "installed.none": "\u672A\u5B89\u88C5",
618
1048
  "installed.others": "\u53E6\u6709 {n} \u4E2A\u975E dsh \u4F9D\u8D56\uFF08\u672A\u8BC6\u522B\u4E3A\u63D2\u4EF6\uFF09\uFF0C\u5DF2\u9ED8\u8BA4\u6298\u53E0\u3002",
1049
+ "installed.upgradeAll": "\u5168\u90E8\u66F4\u65B0 ({n})",
619
1050
  "badge.installed": "\u5DF2\u5B89\u88C5",
620
1051
  "badge.update": "\u53EF\u5347\u7EA7",
621
1052
  "badge.market": "\u5E02\u573A\u5B89\u88C5",
@@ -650,23 +1081,17 @@ var ZH = {
650
1081
  "sub.latest": "\u6700\u65B0 v{v}",
651
1082
  "sub.head": "HEAD {sha}",
652
1083
  "sub.installed": "\u5DF2\u88C5 v{v}",
653
- "settings.registry": "\u6536\u5F55\u6E05\u5355\uFF08registry\uFF09",
1084
+ "settings.registry": "\u7CBE\u9009\u6E05\u5355\uFF08registry.json\uFF09",
654
1085
  "settings.source": "\u5F53\u524D\u6765\u6E90",
655
1086
  "settings.updated": "\u66F4\u65B0\u65F6\u95F4",
656
1087
  "settings.count": "\u6761\u76EE\u6570",
657
- "settings.count.v": "{n} \u6761",
658
- "settings.policy": "\u7F13\u5B58\u7B56\u7565",
659
- "settings.policy.v": "TTL 60 \u5206\u949F\uFF1B\u8BBE\u7F6E registryUrl \u53EF\u8986\u76D6\u6E90",
1088
+ "settings.count.v": "{n}",
660
1089
  "settings.remotehint": "\u8FDC\u7AEF\u63D0\u793A",
661
1090
  "settings.force": "\u5F3A\u5236\u5237\u65B0",
662
- "settings.self": "dsh-m \u81EA\u8EAB",
663
- "settings.current": "\u5F53\u524D\u7248\u672C",
664
- "settings.npmlatest": "npm \u6700\u65B0",
665
- "settings.lookupfailed": "\u67E5\u8BE2\u5931\u8D25\uFF1A{err}",
666
- "settings.upgradeself": "\u5347\u7EA7 dsh-m",
667
- "settings.upgradehint": "\u5347\u7EA7\u540E\u540C\u6837\u9700\u8981\u91CD\u542F\u751F\u6548",
668
1091
  "settings.about": "\u5173\u4E8E",
669
- "settings.about.text": "\u4E2A\u4EBA DSH \u63D2\u4EF6\u5E02\u573A\uFF1A\u6536\u5F55\u3001\u5B89\u88C5\u3001\u5378\u8F7D\u3001\u5347\u7EA7\u5168\u90E8\u672C\u673A\u5B8C\u6210\uFF1B\u6536\u5F55\u6E05\u5355\u652F\u6301\u81EA\u5B9A\u4E49\u8986\u76D6\uFF0C\u5E94\u7528\u5373\u65F6\u751F\u6548\u3002",
1092
+ "settings.about.text": "dsh-m \u63D2\u4EF6\u5E02\u573A\uFF1A\u300CAwesome DSH Plugin\u300D\u793E\u533A + \u300C\u7CBE\u9009\u7B56\u5C55\u300D\u7684\u53CC\u6E05\u5355\u5206\u533A\u5E02\u573A\uFF0C\u6536\u5F55\u3001\u5B89\u88C5\u3001\u5378\u8F7D\u3001\u5347\u7EA7\u4E00\u7AD9\u5B8C\u6210\u3002",
1093
+ "settings.about.issues": "\u95EE\u9898\u53CD\u9988",
1094
+ "settings.about.npm": "npm",
670
1095
  "src.override": "\u81EA\u5B9A\u4E49\u6E90",
671
1096
  "src.jsdelivr": "GitHub \u955C\u50CF\uFF08\u5907\u7528\uFF09",
672
1097
  "src.raw": "raw.githubusercontent\uFF08@main\uFF09",
@@ -680,14 +1105,14 @@ var ZH = {
680
1105
  "src.custom.cache": "\u81EA\u5B9A\u4E49\u6E90\uFF08\u7F13\u5B58\uFF09",
681
1106
  "src.custom.unavailable": "\u81EA\u5B9A\u4E49\u6E90\uFF08\u4E0D\u53EF\u7528\uFF09",
682
1107
  "settings.address": "Registry \u5730\u5740",
683
- "settings.address.hint": "\u7A7A = \u5B98\u65B9\u9ED8\u8BA4\u6E05\u5355\uFF1B\u652F\u6301 HTTPS URL \u6216\u672C\u673A\u7EDD\u5BF9\u8DEF\u5F84 / file://\u3002\u6574\u4F53\u8986\u76D6\u9ED8\u8BA4\u6E05\u5355\uFF0C\u4E0D\u505A\u5408\u5E76\u3002",
1108
+ "settings.address.default": "\u5185\u7F6E\u9ED8\u8BA4\u6E05\u5355",
1109
+ "settings.note.custom": "\u81EA\u5B9A\u4E49\u6E90\u5C06\u6574\u4F53\u66FF\u6362\u300C\u7CBE\u9009\u6E05\u5355\u300D\uFF1B\u6761\u76EE\u683C\u5F0F\u53EF\u53C2\u7167\u4E0B\u8F7D\u7684\u9ED8\u8BA4\u6E05\u5355\u3002\u7559\u7A7A = \u4F7F\u7528\u5185\u7F6E\u9ED8\u8BA4\u6E05\u5355\u3002",
684
1110
  "settings.address.ph": "https://example.com/registry.json \u6216 /path/to/registry.json",
685
1111
  "settings.configured": "\u914D\u7F6E\u5730\u5740",
686
- "settings.activecfg": "\u5F53\u524D\u751F\u6548\u914D\u7F6E",
687
1112
  "settings.effective": "\u751F\u6548\u6765\u6E90",
688
1113
  "settings.status.label": "\u914D\u7F6E\u72B6\u6001",
689
1114
  "settings.status.loading": "\u52A0\u8F7D\u4E2D",
690
- "settings.status.ready": "\u5DF2\u751F\u6548",
1115
+ "settings.status.ready": "\u6B63\u5E38",
691
1116
  "settings.status.pending": "\u5F85\u5199\u5165\uFF08\u6821\u9A8C\u5DF2\u901A\u8FC7\uFF09",
692
1117
  "settings.status.rejected": "\u5DF2\u62D2\u7EDD\uFF08\u4FDD\u6301\u65E7\u914D\u7F6E\uFF09",
693
1118
  "settings.status.unavailable": "\u4E0D\u53EF\u7528",
@@ -697,31 +1122,31 @@ var ZH = {
697
1122
  "settings.apply.failed": "\u5E94\u7528\u5931\u8D25\uFF1A{err}",
698
1123
  "settings.reset": "\u6062\u590D\u9ED8\u8BA4",
699
1124
  "settings.reset.ok": "\u5DF2\u6062\u590D\u9ED8\u8BA4\u6536\u5F55\u6E05\u5355",
700
- "settings.download": "\u4E0B\u8F7D\u9ED8\u8BA4 registry.json",
1125
+ "settings.download": "\u4E0B\u8F7D\u9ED8\u8BA4\u6E05\u5355",
1126
+ "settings.download.title": "\u4E0B\u8F7D\u9ED8\u8BA4 registry.json\uFF08\u53EF\u4F5C\u81EA\u5B9A\u4E49\u6A21\u677F\u4E0E\u683C\u5F0F\u53C2\u7167\uFF09",
701
1127
  "settings.download.downloading": "\u4E0B\u8F7D\u4E2D\u2026",
702
1128
  "settings.download.ok": "\u9ED8\u8BA4\u6E05\u5355\u5DF2\u4E0B\u8F7D\uFF08\u5F53\u524D\u914D\u7F6E\u4E0D\u53D8\uFF09",
703
1129
  "settings.download.failed": "\u4E0B\u8F7D\u5931\u8D25\uFF1A{err}",
704
- "settings.diagnose": "\u68C0\u67E5\u6761\u76EE\u53EF\u8FBE\u6027",
705
- "settings.diagnose.running": "\u8BCA\u65AD\u4E2D\u2026",
706
- "settings.diagnose.failed": "\u8BCA\u65AD\u5931\u8D25\uFF1A{err}",
707
- "settings.diagnose.result": "\u63A2\u6D4B {checked} \u9879 \xB7 \u901A\u8FC7 {passed} \xB7 \u5931\u8D25 {failed}{trunc}",
708
- "settings.diagnose.truncated": "\uFF08\u4EC5\u663E\u793A\u524D 100 \u6761\u95EE\u9898\uFF09",
709
- "settings.diagnose.none": "\u672A\u53D1\u73B0\u95EE\u9898",
710
1130
  "settings.trust.hint": "\u26A0\uFE0F \u81EA\u5B9A\u4E49\u6536\u5F55\u6E05\u5355\u672A\u7ECF\u5B98\u65B9 CI \u6821\u9A8C\uFF0C\u6761\u76EE\u6765\u6E90\u8BF7\u786E\u8BA4\u53EF\u4FE1\u540E\u518D\u5B89\u88C5\u3002",
711
- "settings.cache.hint": "\u5207\u6362\u540E\u65E7\u81EA\u5B9A\u4E49\u6E90\u7F13\u5B58\u5C06\u88AB\u6E05\u7406\uFF08\u9ED8\u8BA4\u7F13\u5B58\u4FDD\u7559\uFF09\uFF1B\u81EA\u5B9A\u4E49\u6E90\u5931\u8D25\u65F6\u4FDD\u7559\u5176\u6700\u8FD1\u4E00\u6B21\u6210\u529F\u7F13\u5B58\u3002",
712
1131
  "settings.warnings": "\u7EF4\u62A4\u63D0\u793A",
713
- "notice.default": "\u5B98\u65B9\u9ED8\u8BA4\u6536\u5F55\u6E05\u5355 \xB7 \u5171 {count} \u6761",
714
- "notice.custom": "\u81EA\u5B9A\u4E49\u6536\u5F55\u6E05\u5355 \xB7 \u5171 {count} \u6761",
715
- "notice.stale": "\u6765\u6E90\u4E3A\u672C\u5730\u7F13\u5B58\uFF08\u5171 {count} \u6761\uFF09\uFF0C\u53EF\u7528\u300C\u5F3A\u5236\u5237\u65B0\u300D\u66F4\u65B0",
716
1132
  "notice.unavailable": "\u6536\u5F55\u6E05\u5355\u4E0D\u53EF\u7528 \xB7 \u8BF7\u5230\u8BBE\u7F6E\u9875\u68C0\u67E5\u5730\u5740",
1133
+ "pager.jump": "\u8DF3\u8F6C",
1134
+ "pager.jump.ph": "\u9875\u53F7",
1135
+ "panel.fullscreen": "\u5168\u5C4F",
1136
+ "panel.restore": "\u8FD8\u539F",
1137
+ "filter.title": "\u7B5B\u9009",
1138
+ "filter.sortfield": "\u6392\u5E8F\u5B57\u6BB5",
1139
+ "filter.sortdir": "\u6392\u5217\u65B9\u5411",
1140
+ "filter.pagesize": "\u6BCF\u9875\u6761\u6570",
1141
+ "filter.field.downloads": "npm \u4E0B\u8F7D\u91CF\uFF08\u8FD1 30 \u5929\uFF09",
1142
+ "filter.field.stars": "Star \u6570",
1143
+ "filter.field.added": "\u6536\u5F55\u65E5\u671F",
1144
+ "filter.dir.desc": "\u964D\u5E8F",
1145
+ "filter.dir.asc": "\u5347\u5E8F",
717
1146
  "market.page.prev": "\u4E0A\u4E00\u9875",
718
1147
  "market.page.next": "\u4E0B\u4E00\u9875",
719
1148
  "market.page.info": "\u7B2C {page} / {pages} \u9875 \xB7 \u5171 {total} \u6761",
720
- "market.perf": "\u6536\u5F55\u8D85\u8FC7 200 \u6761\uFF1A\u4EC5\u67E5\u8BE2\u5F53\u524D\u9875\u7684\u6700\u65B0\u7248\u672C\uFF08\u6BCF\u9875 50 \u6761\uFF09\uFF0C\u5982\u9700\u5168\u90E8\u8BF7\u7528\u641C\u7D22/\u5206\u7C7B\u8FC7\u6EE4",
721
1149
  "notice.toolview.err": "\u6536\u5F55\u6E05\u5355\u6682\u4E0D\u53EF\u7528",
722
- "cat.primaryonly": "\u53EA\u770B\u4E3B\u6E05\u5355",
723
- "community.group": "\u793E\u533A",
724
- "community.newgroup": "\u793E\u533A \xB7 \u65B0\u5206\u7C7B",
725
1150
  "badge.community": "\u793E\u533A\u6536\u5F55",
726
1151
  "community.stale": "\u793E\u533A\u76EE\u5F55\u4E3A\u7F13\u5B58\u5FEB\u7167\uFF08\u663E\u793A\u7684\u4E0D\u662F\u6700\u65B0\u6570\u636E\uFF09",
727
1152
  "community.fallback": "\u6536\u5F55\u6E05\u5355\u4E0D\u53EF\u7528\uFF0C\u5F53\u524D\u5C55\u793A\u793E\u533A\u6E05\u5355\u6761\u76EE",
@@ -736,15 +1161,20 @@ var ZH = {
736
1161
  "guard.noforce": "\u5B88\u536B\u62E6\u622A\u65E0\u300C\u4ECD\u8981\u5B89\u88C5\u300D\u901A\u9053\uFF0C\u8BF7\u6309\u4FEE\u590D\u4F9D\u636E\u4EBA\u5DE5\u5904\u7406",
737
1162
  "detail.screenshots": "\u622A\u56FE",
738
1163
  "installed.check.incomplete": "\u68C0\u67E5\u672A\u5B8C\u6210",
739
- "settings.community": "\u793E\u533A\u6E05\u5355\uFF08awesome-dsh-plugin \u76EE\u5F55\uFF09",
740
- "settings.community.none": "\u793E\u533A\u6E05\u5355\u672A\u542F\u7528\u6216\u4E0D\u53EF\u7528",
1164
+ "settings.community": "\u793E\u533A\u6E05\u5355\uFF08from awesome-dsh-plugin\uFF09",
1165
+ "settings.community.toggle": "\u793E\u533A\u76EE\u5F55\u5F00\u5173",
1166
+ "settings.community.off": "\u793E\u533A\u76EE\u5F55\u5DF2\u5173\u95ED\uFF1A\u5E02\u573A\u4EC5\u663E\u793A\u7CBE\u9009\u6E05\u5355\uFF0C\u91CD\u65B0\u5F00\u542F\u5373\u65F6\u751F\u6548\u3002",
1167
+ "settings.community.on.ok": "\u793E\u533A\u76EE\u5F55\u5DF2\u5F00\u542F\uFF08\u5373\u65F6\u751F\u6548\uFF09",
1168
+ "settings.community.off.ok": "\u793E\u533A\u76EE\u5F55\u5DF2\u5173\u95ED\uFF08\u5373\u65F6\u751F\u6548\uFF09",
1169
+ "settings.community.toggle.failed": "\u5207\u6362\u5931\u8D25\uFF1A{err}",
741
1170
  "settings.community.status": "\u72B6\u6001",
1171
+ "settings.community.status.stale": "\u7F13\u5B58\u5FEB\u7167",
742
1172
  "settings.community.version": "\u76EE\u5F55\u7248\u672C",
743
1173
  "settings.community.route": "\u83B7\u53D6\u7EBF\u8DEF",
744
- "settings.community.accepted": "\u6536\u5F55 / \u4E0A\u6E38",
745
- "settings.community.displaced": "\u4E0E\u4E3B\u6E05\u5355\u91CD\u590D\u8BA9\u4F4D",
746
- "self.upgraded": "dsh-m \u5DF2\u66F4\u65B0\u5230 v{v}\uFF0C\u91CD\u542F\u540E\u751F\u6548",
747
- "self.failed": "\u81EA\u66F4\u65B0\u5931\u8D25\uFF1A{err}",
1174
+ "settings.community.count": "\u6536\u5F55\u6761\u76EE",
1175
+ "settings.community.count.v": "{n}\uFF08\u4E0A\u6E38 {up}\uFF09",
1176
+ "settings.community.displaced": "\u4E0E\u7CBE\u9009\u91CD\u590D",
1177
+ "settings.community.displaced.v": "{n} \u6761",
748
1178
  "registry.refreshed": "\u6536\u5F55\u6E05\u5355\u5DF2\u5F3A\u5236\u5237\u65B0",
749
1179
  "notify.installed": "\u5DF2\u5B89\u88C5 {pkg}{version}",
750
1180
  "notify.allowbuilds": "\uFF08\u6CE8\u610F\uFF1A\u8BE5\u63D2\u4EF6\u6267\u884C\u4E86\u6784\u5EFA\u811A\u672C\uFF0C\u5DF2\u6309\u7B56\u7565\u653E\u884C\uFF09",
@@ -779,7 +1209,6 @@ var ZH = {
779
1209
  "failed.install": "\u5B89\u88C5\u5931\u8D25\uFF1A{err}",
780
1210
  "failed.uninstall": "\u5378\u8F7D\u5931\u8D25\uFF1A{err}",
781
1211
  "failed.upgrade": "\u5347\u7EA7\u5931\u8D25\uFF1A{err}",
782
- "failed.selfupdate": "\u81EA\u66F4\u65B0\u5931\u8D25\uFF1A{err}",
783
1212
  "failed.load": "\u52A0\u8F7D\u5931\u8D25\uFF1A{err}",
784
1213
  "failed.read": "\u8BFB\u53D6\u5931\u8D25\uFF1A{err}",
785
1214
  "failed.open": "\u6253\u5F00\u5E02\u573A\u9762\u677F\u5931\u8D25:",
@@ -803,8 +1232,6 @@ var ZH = {
803
1232
  "warn.unlink": "\u5378\u8F7D\u53EA\u79FB\u9664 profile \u5BF9\u672C\u5730\u76EE\u5F55\u7684\u5F15\u7528\uFF08{path}\uFF09\uFF0C\u4E0D\u4F1A\u5220\u9664\u76EE\u5F55\u672C\u8EAB\u3002",
804
1233
  "warn.core": "\u8FD9\u662F file: \u5B89\u88C5\u7684\u6838\u5FC3/\u5F52\u6863\u5305\uFF0C\u5378\u8F7D\u53EF\u80FD\u5F71\u54CD DSH \u529F\u80FD\uFF0C\u4E14\u9700\u8981\u624B\u52A8\u6062\u590D\u3002",
805
1234
  "profile.hint": "web profile\uFF1A{path}",
806
- "dsh.chip.copyhint": "\u70B9\u51FB\u590D\u5236\u7248\u672C\u53F7",
807
- "dsh.chip.copied": "\u5DF2\u590D\u5236 \u2713",
808
1235
  "title.panel": "\u63D2\u4EF6\u5E02\u573A",
809
1236
  "title.full": "DeepSeek Harness \u63D2\u4EF6\u5E02\u573A"
810
1237
  };
@@ -819,6 +1246,47 @@ var EN = {
819
1246
  "cat.ui": "UI",
820
1247
  "cat.search": "Search",
821
1248
  "cat.other": "Other",
1249
+ "zone.community": "Community",
1250
+ "zone.primary": "Curated",
1251
+ "zone.favorites": "Favorites",
1252
+ "badge.deprecated": "Deprecated",
1253
+ "sub.snapshot": "v{v} (catalog snapshot)",
1254
+ "badge.verified": "Verified",
1255
+ "modal.category": "Category",
1256
+ "modal.added": "Added",
1257
+ "modal.dlwindow": "Downloads (30-day window)",
1258
+ "modal.checkedat": "checked at",
1259
+ "modal.dlnone": "No window data",
1260
+ "modal.verified": "Verified runtimes",
1261
+ "modal.tags": "Tags",
1262
+ "modal.replacement": "Deprecated \xB7 replacement",
1263
+ "modal.installcmd": "Install command",
1264
+ "modal.copy": "Copy",
1265
+ "modal.copied": "Copied",
1266
+ "op.clear": "Clear finished",
1267
+ "op.superseded.note": "{target} skipped (precondition gone or already handled)",
1268
+ "op.cancelled": "user cancelled",
1269
+ "op.remove": "Dismiss",
1270
+ "op.panel.jump": "Open operations panel",
1271
+ "op.kind.install": "Install",
1272
+ "op.kind.upgrade": "Upgrade",
1273
+ "op.kind.uninstall": "Uninstall",
1274
+ "op.kind.toggle": "Toggle",
1275
+ "op.status.queued": "Queued",
1276
+ "op.status.running": "Running",
1277
+ "op.status.input": "Pending",
1278
+ "op.status.done": "Done",
1279
+ "op.status.warned": "Warned",
1280
+ "op.status.failed": "Failed",
1281
+ "op.status.superseded": "Skipped",
1282
+ "favorites.hint": "No favorites yet \u2014 tap \u2606 on a plugin card in Community/Curated to bookmark it",
1283
+ "favorites.stale": "{n} favorites no longer in the catalog",
1284
+ "favorites.clean": "Clean up stale favorites",
1285
+ "favorites.checking": "Checking favorites\u2026",
1286
+ "favorites.stalebadge": "Delisted",
1287
+ "fav.add": "Bookmark",
1288
+ "fav.remove": "Remove bookmark",
1289
+ "common.clear": "Clear",
822
1290
  "search.ph": "Search name, description, tags\u2026",
823
1291
  "common.refresh": "Refresh",
824
1292
  "common.close": "Close",
@@ -826,11 +1294,12 @@ var EN = {
826
1294
  "common.ok": "OK",
827
1295
  "common.none": "\u2014",
828
1296
  "market.loading": "Loading listings\u2026 ",
829
- "market.empty": "No matching listings",
1297
+ "market.empty": "No matching plugins \u2014 try another keyword or category",
830
1298
  "installed.loading": "Reading web profile\u2026 ",
831
1299
  "installed.empty": "No DSH plugins installed in this web profile",
832
1300
  "installed.none": "Not installed",
833
1301
  "installed.others": "{n} non-DSH dependencies (not recognized as plugins) are collapsed.",
1302
+ "installed.upgradeAll": "Update all ({n})",
834
1303
  "badge.installed": "Installed",
835
1304
  "badge.update": "Update",
836
1305
  "badge.market": "Via market",
@@ -865,23 +1334,17 @@ var EN = {
865
1334
  "sub.latest": "Latest v{v}",
866
1335
  "sub.head": "HEAD {sha}",
867
1336
  "sub.installed": "Installed v{v}",
868
- "settings.registry": "Registry",
1337
+ "settings.registry": "Curated registry (registry.json)",
869
1338
  "settings.source": "Source",
870
1339
  "settings.updated": "Updated",
871
1340
  "settings.count": "Listings",
872
- "settings.count.v": "{n} listings",
873
- "settings.policy": "Caching",
874
- "settings.policy.v": "60 min TTL; override via registryUrl",
1341
+ "settings.count.v": "{n}",
875
1342
  "settings.remotehint": "Remote notice",
876
1343
  "settings.force": "Force refresh",
877
- "settings.self": "dsh-m itself",
878
- "settings.current": "Current version",
879
- "settings.npmlatest": "npm latest",
880
- "settings.lookupfailed": "lookup failed: {err}",
881
- "settings.upgradeself": "Upgrade dsh-m",
882
- "settings.upgradehint": "A restart is required after upgrading",
883
1344
  "settings.about": "About",
884
- "settings.about.text": "A personal DSH plugin marketplace \u2014 browse, install, uninstall and upgrade, all local; registry overrides apply live.",
1345
+ "settings.about.text": 'The DeepSeek Harness plugin marketplace \u2014 a zoned dual-catalog market of the "Awesome DSH Plugin" community plus a curated registry; install, uninstall and upgrade in one place.',
1346
+ "settings.about.issues": "Issues",
1347
+ "settings.about.npm": "npm",
885
1348
  "src.override": "Custom source",
886
1349
  "src.jsdelivr": "GitHub mirror (backup)",
887
1350
  "src.raw": "raw.githubusercontent (@main)",
@@ -895,14 +1358,14 @@ var EN = {
895
1358
  "src.custom.cache": "Custom source (cache)",
896
1359
  "src.custom.unavailable": "Custom source (unavailable)",
897
1360
  "settings.address": "Registry address",
898
- "settings.address.hint": "Empty = official default registry; accepts an HTTPS URL or a local absolute path / file://. Replaces (not merges) the default registry. Live effect.",
1361
+ "settings.address.default": "Built-in default registry",
1362
+ "settings.note.custom": "A custom source replaces the whole Curated registry; use the downloaded default registry as the format reference. Leave empty for the built-in default.",
899
1363
  "settings.address.ph": "https://example.com/registry.json or /path/to/registry.json",
900
1364
  "settings.configured": "Configured address",
901
- "settings.activecfg": "Active config",
902
1365
  "settings.effective": "Effective source",
903
1366
  "settings.status.label": "Config status",
904
1367
  "settings.status.loading": "Loading",
905
- "settings.status.ready": "Applied",
1368
+ "settings.status.ready": "Online",
906
1369
  "settings.status.pending": "Pending write (validated)",
907
1370
  "settings.status.rejected": "Rejected (previous config kept)",
908
1371
  "settings.status.unavailable": "Unavailable",
@@ -912,31 +1375,31 @@ var EN = {
912
1375
  "settings.apply.failed": "Apply failed: {err}",
913
1376
  "settings.reset": "Restore default",
914
1377
  "settings.reset.ok": "Restored to the default registry",
915
- "settings.download": "Download default registry.json",
1378
+ "settings.download": "Download default registry",
1379
+ "settings.download.title": "Download the default registry.json (a template and format reference for custom registries)",
916
1380
  "settings.download.downloading": "Downloading\u2026",
917
1381
  "settings.download.ok": "Default registry downloaded (current config unchanged)",
918
1382
  "settings.download.failed": "Download failed: {err}",
919
- "settings.diagnose": "Check entries reachability",
920
- "settings.diagnose.running": "Checking\u2026",
921
- "settings.diagnose.failed": "Diagnose failed: {err}",
922
- "settings.diagnose.result": "Probes {checked} \xB7 passed {passed} \xB7 failed {failed}{trunc}",
923
- "settings.diagnose.truncated": " (showing first 100 issues)",
924
- "settings.diagnose.none": "No issues found",
925
1383
  "settings.trust.hint": "\u26A0\uFE0F Custom registries are not validated by official CI. Only install entries from sources you trust.",
926
- "settings.cache.hint": "Old custom-source caches are cleaned after switching (the default cache is kept); a failed custom source keeps its last good cache.",
927
1384
  "settings.warnings": "Maintenance notice",
928
- "notice.default": "Official default registry \xB7 {count} listings",
929
- "notice.custom": "Custom registry \xB7 {count} listings",
930
- "notice.stale": "Served from local cache ({count} listings) \u2014 force refresh to update",
931
1385
  "notice.unavailable": "Registry unavailable \xB7 check the address in Settings",
1386
+ "pager.jump": "Go",
1387
+ "pager.jump.ph": "Page",
1388
+ "panel.fullscreen": "Fullscreen",
1389
+ "panel.restore": "Restore",
1390
+ "filter.title": "Filter",
1391
+ "filter.sortfield": "Sort by",
1392
+ "filter.sortdir": "Direction",
1393
+ "filter.pagesize": "Per page",
1394
+ "filter.field.downloads": "npm downloads (30-day)",
1395
+ "filter.field.stars": "Stars",
1396
+ "filter.field.added": "Date added",
1397
+ "filter.dir.desc": "Descending",
1398
+ "filter.dir.asc": "Ascending",
932
1399
  "market.page.prev": "Previous",
933
1400
  "market.page.next": "Next",
934
1401
  "market.page.info": "Page {page} / {pages} \xB7 {total} listings",
935
- "market.perf": "200+ listings: latest versions are queried for the current page only (50 per page); use search/category filters",
936
1402
  "notice.toolview.err": "Registry temporarily unavailable",
937
- "cat.primaryonly": "Primary only",
938
- "community.group": "Community",
939
- "community.newgroup": "Community \xB7 New",
940
1403
  "badge.community": "Community",
941
1404
  "community.stale": "Community catalog served from cache (not the latest data)",
942
1405
  "community.fallback": "Registry unavailable \u2014 showing community listings",
@@ -951,15 +1414,20 @@ var EN = {
951
1414
  "guard.noforce": "Guard blocks have no force channel \u2014 repair manually per the basis above",
952
1415
  "detail.screenshots": "Screenshots",
953
1416
  "installed.check.incomplete": "Check incomplete",
954
- "settings.community": "Community catalog (awesome-dsh-plugin)",
955
- "settings.community.none": "Community catalog disabled or unavailable",
1417
+ "settings.community": "Community catalog (from awesome-dsh-plugin)",
1418
+ "settings.community.toggle": "Community catalog toggle",
1419
+ "settings.community.off": "Community catalog is off: the market shows only the Curated registry. Toggle back on anytime \u2014 it applies live.",
1420
+ "settings.community.on.ok": "Community catalog enabled (live)",
1421
+ "settings.community.off.ok": "Community catalog disabled (live)",
1422
+ "settings.community.toggle.failed": "Toggle failed: {err}",
956
1423
  "settings.community.status": "Status",
1424
+ "settings.community.status.stale": "Cached snapshot",
957
1425
  "settings.community.version": "Catalog version",
958
1426
  "settings.community.route": "Route",
959
- "settings.community.accepted": "Accepted / upstream",
960
- "settings.community.displaced": "Displaced (duplicate of primary)",
961
- "self.upgraded": "dsh-m updated to v{v} \u2014 restart to take effect",
962
- "self.failed": "Self-update failed: {err}",
1427
+ "settings.community.count": "Entries",
1428
+ "settings.community.count.v": "{n} (upstream {up})",
1429
+ "settings.community.displaced": "Displaced (duplicates)",
1430
+ "settings.community.displaced.v": "{n}",
963
1431
  "registry.refreshed": "Registry force-refreshed",
964
1432
  "notify.installed": "Installed {pkg}{version}",
965
1433
  "notify.allowbuilds": " (note: this plugin ran build scripts, allowed by policy)",
@@ -994,7 +1462,6 @@ var EN = {
994
1462
  "failed.install": "Install failed: {err}",
995
1463
  "failed.uninstall": "Uninstall failed: {err}",
996
1464
  "failed.upgrade": "Upgrade failed: {err}",
997
- "failed.selfupdate": "Self-update failed: {err}",
998
1465
  "failed.load": "Load failed: {err}",
999
1466
  "failed.read": "Read failed: {err}",
1000
1467
  "failed.open": "Failed to open the marketplace panel:",
@@ -1018,8 +1485,6 @@ var EN = {
1018
1485
  "warn.unlink": "Uninstalling only removes the profile's reference to the local directory ({path}); the directory itself is kept.",
1019
1486
  "warn.core": "This is a core/archive package installed via file:. Uninstalling may affect DSH features and requires manual restore.",
1020
1487
  "profile.hint": "web profile: {path}",
1021
- "dsh.chip.copyhint": "Click to copy version",
1022
- "dsh.chip.copied": "Copied \u2713",
1023
1488
  "title.panel": "Plugin Marketplace",
1024
1489
  "title.full": "DeepSeek Harness Plugin Marketplace"
1025
1490
  };
@@ -1035,16 +1500,37 @@ function lookup(key, params) {
1035
1500
  const dict = browserLang() === "en" ? EN : ZH;
1036
1501
  return interpolate(dict[key] ?? ZH[key] ?? key, params);
1037
1502
  }
1038
- var CATEGORIES2 = ["market", "tools", "ui", "search", "other"];
1503
+ function communityLabels(data) {
1504
+ const c = data && data.community && typeof data.community === "object" ? data.community : {};
1505
+ const zh = c.categoryLabels && typeof c.categoryLabels === "object" ? c.categoryLabels : {};
1506
+ if (browserLang() !== "en") return zh;
1507
+ const en = c.categoryLabelsEn && typeof c.categoryLabelsEn === "object" ? c.categoryLabelsEn : {};
1508
+ return { ...zh, ...en };
1509
+ }
1039
1510
  var CSS = `
1040
1511
  .dshm-overlay{position:fixed;inset:0;z-index:2147483000;background:var(--dsw-alias-bg-mask-3,rgba(15,23,42,.48));display:flex;align-items:center;justify-content:center;padding:24px 16px;box-sizing:border-box}
1041
1512
  .dshm-panel{width:min(920px,100%);height:min(680px,86vh);display:flex;flex-direction:column;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 86%,transparent);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);overflow:hidden;font-family:inherit;color:var(--dsw-alias-label-primary,inherit)}
1513
+ .dshm-overlay.full{padding:0}
1514
+ .dshm-panel.full{width:100%;height:100%;border-radius:0}
1515
+ /* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u7B49\u5BBD\u4E24\u683C + \u53D1\u4E1D\u5206\u9694\u7EBF\uFF0C\u7EDF\u4E00\u7EBF\u6761\u56FE\u6807 */
1516
+ /* \u7A97\u53E3\u63A7\u5236\u7EC4\uFF080.7.7/0.7.8\uFF09\uFF1A\u6700\u5927\u5316/\u5173\u95ED\u8FDE\u4F53\u6309\u94AE\u7EC4\u2014\u2014\u900F\u660E\u5E95 + \u53D1\u4E1D\u5916\u6846\uFF0C\u4E0E\u641C\u7D22\u6E05\u9664\u94AE\u540C\u4E00\u914D\u8272\u8BED\u8A00\uFF1B\u5BBD\u683C\u9632\u8BEF\u89E6 */
1517
+ .dshm-winctl{display:inline-flex;align-items:stretch;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:9px;background:transparent;overflow:hidden;flex:none}
1518
+ .dshm-winctl button{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);width:44px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:background .15s,color .15s}
1519
+ .dshm-winctl button + button{border-left:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.18))}
1520
+ .dshm-winctl button:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12));color:var(--dsw-alias-label-primary,inherit)}
1521
+ .dshm-winctl button:active{background:var(--dsw-alias-fill-secondary,rgba(127,127,127,.2))}
1522
+ .dshm-winctl button.danger:hover{background:#e5484d;color:#fff}
1042
1523
  .dshm-head{display:flex;align-items:center;gap:8px;padding:10px 14px;border-bottom:1px solid var(--dsw-alias-border-l2,#e5e7eb)}
1043
- .dshm-title{font-weight:700;font-size:15px;margin-right:6px}
1524
+ .dshm-title{font-weight:600;font-size:15px;margin-right:2px}
1525
+ .dshm-head-divider{width:1px;height:18px;background:var(--dsw-alias-border-l2,#e5e7eb);flex:none}
1044
1526
  .dshm-seg{display:inline-flex;align-items:center;gap:2px;padding:2px;border:1px solid var(--dsw-alias-border-l2,#e2e4e8);border-radius:9px;background:var(--dsw-alias-bg-layer-1,#f5f6f8)}
1045
1527
  .dshm-seg button{appearance:none;border:0;background:transparent;height:28px;padding:0 14px;border-radius:7px;font:inherit;font-size:12px;color:var(--dsw-alias-label-tertiary,#7b8088);cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:background .15s,color .15s,box-shadow .15s}
1046
1528
  .dshm-seg button:hover{color:var(--dsw-alias-label-secondary,#4b5058)}
1047
1529
  .dshm-seg button.on{background:var(--dsw-alias-bg-layer-3,#fff);color:var(--dsw-alias-label-primary,#17191c);font-weight:600;box-shadow:var(--dsw-shadow-lv1,0 2px 8px rgb(20 24 32 / 8%))}
1530
+ .dsvm-filterbtn{appearance:none;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-elevated,transparent);color:var(--dsw-alias-label-secondary,#4b5563);border-radius:8px;padding:4px 12px;font:inherit;font-size:12px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;white-space:nowrap;transition:color .15s,border-color .15s,background .15s}
1531
+ .dsvm-filterbtn:hover{color:var(--dsw-alias-label-primary,inherit);border-color:var(--dsw-alias-label-caption,#9ca3af)}
1532
+ .dsvm-filterbtn.on{color:var(--dsw-alias-state-business-primary,#4d6bfe);border-color:var(--dsw-alias-state-business-primary,#4d6bfe);background:color-mix(in srgb,var(--dsw-alias-state-business-primary,#4d6bfe) 8%,transparent)}
1533
+ .dsvm-filterchev{font-size:10px;line-height:1}
1048
1534
  .dshm-seg .dshm-count{font-size:11px;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-caption,#9ca3af);margin:0}
1049
1535
  .dshm-seg button.on .dshm-count{color:var(--dsw-alias-state-business-primary,#4d6bfe)}
1050
1536
  .dshm-spacer{flex:1}
@@ -1058,12 +1544,82 @@ var CSS = `
1058
1544
  .dshm-btn.primary:hover{filter:brightness(1.08)}
1059
1545
  .dshm-btn.danger{color:var(--dsw-alias-state-error-primary,#b91c1c);border-color:var(--dsw-alias-state-error-primary,#b91c1c)}
1060
1546
  .dshm-btn.sm{padding:3px 9px;font-size:11px}
1061
- .dshm-input{flex:1;min-width:120px;border:1px solid var(--dsw-alias-border-l2,#c7d2fe);background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);border-radius:8px;padding:5px 10px;font:inherit;font-size:12px;outline:none}
1547
+ .dshm-input{flex:1;min-width:120px;border:1px solid var(--dsw-alias-border-l2,#c7d2fe);background:var(--dsw-alias-bg-layer-2,transparent);color:var(--dsw-alias-label-primary,inherit);border-radius:999px;padding:7px 14px;font:inherit;font-size:12px;outline:none}
1062
1548
  .dshm-input:focus{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5)}
1063
1549
  .dshm-chips{display:flex;flex-wrap:wrap;gap:6px}
1550
+ .dsvm-searchrow{display:flex;align-items:center;gap:8px}
1551
+ .dsvm-searchrow .dshm-search{flex:1;display:flex}
1552
+ .dshm-search{position:relative}
1553
+ .dshm-search .dshm-xbtn-infield{position:absolute;right:4px;top:50%;transform:translateY(-50%)}
1554
+ .dshm-xbtn{appearance:none;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);width:26px;height:26px;padding:0;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;flex:none;transition:border-color .15s,color .15s,box-shadow .15s}
1555
+ .dshm-xbtn:hover{border-color:var(--dsw-alias-label-caption,#9ca3af);color:var(--dsw-alias-label-primary,inherit);box-shadow:inset 0 1px 0 rgba(255,255,255,.06),0 1px 2px rgba(0,0,0,.18)}
1556
+ .dshm-xbtn:active{box-shadow:inset 0 2px 3px rgba(0,0,0,.15)}
1557
+ .dsvm-filterwrap{position:relative;margin-left:auto;display:inline-flex}
1558
+ .dsvm-filterback{position:fixed;inset:0;z-index:25}
1559
+ .dsvm-filterpop{position:absolute;top:calc(100% + 6px);right:0;z-index:30;width:250px;box-sizing:border-box;display:flex;flex-direction:column;gap:10px;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 92%,transparent);backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;box-shadow:0 18px 48px rgba(2,6,23,.25);padding:12px}
1560
+ .dsvm-filtergroup{display:flex;flex-direction:column}
1561
+ .dsvm-filtergt{font-size:11px;color:var(--dsw-alias-label-caption,#9ca3af);margin-bottom:4px}
1562
+ .dsvm-filteropt{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;text-align:left;padding:5px 6px;border-radius:7px;cursor:pointer;display:flex;justify-content:space-between;align-items:center;gap:8px}
1563
+ .dsvm-filteropt:hover{background:var(--dsw-alias-fill-secondary,rgba(127,127,127,.12))}
1564
+ .dsvm-filteropt.on{color:var(--dsw-alias-state-business-primary,#4d6bfe);font-weight:600}
1565
+ .dsvm-filtercheck{font-size:11px}
1064
1566
  .dshm-chip{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:2px 10px;font:inherit;font-size:11px;cursor:pointer}
1065
1567
  .dshm-chip:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06))}
1066
1568
  .dshm-chip.on{background:var(--dsw-specific-sidebar-nav-item-active,rgba(38,49,72,.08));border-color:transparent;color:var(--dsw-alias-label-primary,inherit);font-weight:500}
1569
+ .dsvm-chipswrap{position:sticky;top:-14px;z-index:5;background:var(--dsw-alias-bg-base,#fff);padding:8px 0;margin:-8px 0;border-bottom:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.14))}
1570
+ .dsvm-pagejump{display:inline-flex;align-items:center;gap:4px;margin-left:8px}
1571
+ .dsvm-pagejump-input{width:56px;height:24px;box-sizing:border-box;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.28));background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:0 10px;font:inherit;font-size:12px;outline:none;text-align:center;transition:border-color .15s,color .15s}
1572
+ .dsvm-pagejump-input:hover{color:var(--dsw-alias-label-primary,inherit)}
1573
+ .dsvm-pagejump-input:focus{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5);color:var(--dsw-alias-label-primary,inherit)}
1574
+ .dsvm-pagejump-btn{color:var(--dsw-alias-state-business-primary,#4d6bfe)}
1575
+ .dsvm-pagejump-btn:disabled{opacity:.4;cursor:default}
1576
+ .dsvm-sortrow{display:flex;flex-wrap:wrap;gap:6px;align-items:center}
1577
+ .dsvm-byline{display:flex;flex-wrap:wrap;gap:8px;color:var(--dsw-alias-label-caption,#6b7280);font-size:11px;line-height:16px;margin:2px 0 0;font-variant-numeric:tabular-nums}
1578
+ .dsvm-pager{display:flex;flex-wrap:wrap;gap:4px;align-items:center;justify-content:center;margin-top:4px}
1579
+ .dsvm-pagebtn{min-width:26px;height:24px;border:1px solid transparent;border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#4b5563);font:inherit;font-size:12px;cursor:pointer}
1580
+ .dsvm-pagebtn.on{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5);color:var(--dsw-alias-interactive-bg-selected,#4f46e5);font-weight:600}
1581
+ .dsvm-pagebtn:disabled{opacity:.4;cursor:default}
1582
+ .dsvm-modal{position:fixed;inset:0;z-index:2147483100;background:var(--dsw-alias-bg-mask-3,rgba(15,23,42,.55));display:flex;align-items:center;justify-content:center;padding:24px 16px}
1583
+ .dsvm-modalbox{width:min(720px,100%);max-height:86vh;overflow:auto;background:var(--dsw-alias-bg-elevated,#fff);background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 94%,transparent);backdrop-filter:blur(14px);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:14px;box-shadow:0 18px 48px rgba(2,6,23,.25);padding:16px;display:flex;flex-direction:column;gap:10px}
1584
+ .dsvm-modalhead{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
1585
+ .dsvm-kv{display:grid;grid-template-columns:auto 1fr;gap:4px 14px;font-size:12px;margin:0}
1586
+ .dsvm-kv dt{color:var(--dsw-alias-label-caption,#6b7280);white-space:nowrap}
1587
+ .dsvm-kv dd{margin:0;color:var(--dsw-alias-label-secondary,#4b5563);overflow-wrap:anywhere}
1588
+ .dsvm-fold{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;padding:6px 10px;font-size:12px}
1589
+ .dsvm-fold summary{cursor:pointer;color:var(--dsw-alias-label-secondary,#4b5563)}
1590
+ .dsvm-cmdrow{display:flex;align-items:center;gap:8px;margin-top:6px;flex-wrap:wrap}
1591
+ .dsvm-code{background:rgba(127,127,127,.12);padding:3px 8px;border-radius:6px;font-size:11px;overflow-wrap:anywhere}
1592
+ .dsvm-modalactions{display:flex;justify-content:flex-end;gap:8px}
1593
+ .dsvm-shotrow{display:flex;gap:6px;overflow-x:auto;padding:2px 0}
1594
+ .dsvm-shotbox{min-width:120px;min-height:84px;display:flex;align-items:center;justify-content:center;background:rgba(127,127,127,.08);border-radius:6px;cursor:zoom-in;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.25))}
1595
+ .dsvm-shot{max-width:220px;max-height:130px;border-radius:6px;display:block}
1596
+ .dsvm-lightbox{position:fixed;inset:0;z-index:2147483200;background:rgba(0,0,0,.88);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px}
1597
+ .dsvm-lightbox img{max-width:94vw;max-height:80vh;border-radius:8px}
1598
+ .dsvm-lbnav{display:flex;align-items:center;gap:10px}
1599
+ .dsvm-lbcount{color:rgba(255,255,255,.75);font-size:12px}
1600
+ .dsvm-lbdots{display:flex;gap:6px}
1601
+ .dsvm-lbdot{width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.3);cursor:pointer}
1602
+ .dsvm-lbdot.on{background:#fff}
1603
+ .dsvm-card-clickable, .dshm-card{cursor:pointer}
1604
+ .dshm-card:hover{border-color:var(--dsw-alias-interactive-bg-selected,#4f46e5)}
1605
+ .dsvm-btn{border:1px solid rgba(255,255,255,.35);background:rgba(255,255,255,.14);color:#fff;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;cursor:pointer}
1606
+ .dsvm-btn:hover{background:rgba(255,255,255,.24)}
1607
+ .dsvm-ops{border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;padding:8px 10px;display:flex;flex-direction:column;gap:4px;font-size:12px}
1608
+ .dsvm-opgroup{display:flex;flex-direction:column;gap:3px}
1609
+ .dsvm-opgroup.done{opacity:.75}
1610
+ .dsvm-oprow{display:flex;align-items:center;gap:8px;min-height:22px}
1611
+ .dsvm-opstatus{min-width:44px;font-size:11px;color:var(--dsw-alias-label-caption,#6b7280)}
1612
+ .dsvm-oprow.ok .dsvm-opstatus{color:#15803d}
1613
+ .dsvm-oprow.warn .dsvm-opstatus, .dsvm-oprow.run .dsvm-opstatus{color:#b45309}
1614
+ .dsvm-oprow.err .dsvm-opstatus{color:var(--dsw-alias-state-error-primary,#b91c1c)}
1615
+ .dsvm-oprow.sup .dsvm-opstatus{color:var(--dsw-alias-label-caption,#6b7280)}
1616
+ .dsvm-opkind{color:var(--dsw-alias-label-secondary,#4b5563)}
1617
+ .dsvm-optarget{font-weight:500;overflow-wrap:anywhere}
1618
+ .dsvm-opnote{color:var(--dsw-alias-label-caption,#6b7280);font-size:11px;overflow-wrap:anywhere}
1619
+ .dsvm-favbtn{appearance:none;border:0;background:transparent;color:var(--dsw-alias-label-caption,#9ca3af);font-size:15px;line-height:1;cursor:pointer;padding:0 2px;margin-left:auto}
1620
+ .dsvm-favbtn:hover{color:var(--dsw-alias-state-business-primary,#4d6bfe)}
1621
+ .dsvm-favbtn.on{color:#e0a33c}
1622
+ .dsvm-reddot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-error-primary,#ef4444);margin-left:5px;vertical-align:middle}
1067
1623
  .dshm-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}
1068
1624
  @media (max-width:680px){.dshm-cards{grid-template-columns:1fr}}
1069
1625
  .dshm-card{display:flex;gap:12px;align-items:flex-start;background:var(--dsw-alias-bg-layer-2,rgba(38,49,72,.04));border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:12px;padding:12px;cursor:pointer;text-align:left;width:100%;box-sizing:border-box;min-width:0;font:inherit;color:var(--dsw-alias-label-primary,inherit);transition:border-color .16s,background .16s}
@@ -1072,7 +1628,9 @@ var CSS = `
1072
1628
  .dshm-meta{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}
1073
1629
  .dshm-top{display:flex;align-items:center;gap:8px;min-width:0}
1074
1630
  .dshm-name{flex:1;min-width:0;font-weight:600;font-size:14px;line-height:20px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1075
- .dshm-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-state-success-tertiary,#ecfdf5);color:var(--dsw-alias-state-success-primary,#047857)}
1631
+ .dshm-badge{appearance:none;border:0;font:inherit;flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:999px;background:var(--dsw-alias-state-success-tertiary,#ecfdf5);color:var(--dsw-alias-state-success-primary,#047857)}
1632
+ button.dshm-badge{cursor:pointer}
1633
+ button.dshm-badge:hover{filter:brightness(.95)}
1076
1634
  .dshm-badge.warn{background:var(--dsw-alias-state-warn-tertiary,#fffbeb);color:var(--dsw-alias-state-warn-primary,#b45309)}
1077
1635
  .dshm-badge.info{background:var(--dsw-alias-state-business-tertiary,#eef2ff);color:var(--dsw-alias-state-business-primary,#4338ca)}
1078
1636
  .dshm-badge.err{background:var(--dsw-alias-state-error-secondary,#fee2e2);color:var(--dsw-alias-state-error-primary,#b91c1c)}
@@ -1135,14 +1693,9 @@ var CSS = `
1135
1693
  [data-slot="sidebar.footer.action"]{display:flex!important;flex-direction:column;width:100%;min-width:0}
1136
1694
  [data-slot="sidebar.footer.action"]>*{flex:none;min-width:0}
1137
1695
  .dshm-empty{text-align:center;color:var(--dsw-alias-label-caption,#6b7280);font-size:13px;padding:32px 0}
1138
- /* \u5934\u90E8 DSH \u7248\u672C chip\uFF1A\u7B49\u5BBD\u5C0F\u5B57\u5706\u89D2\uFF0Chover \u5C55\u5F00\u8BE6\u60C5\uFF0C\u70B9\u51FB\u590D\u5236\uFF082026-09-14 \u5B9A\u7A3F\uFF09 */
1139
- .dshm-dshchip{position:relative;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#f5f6f8);color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:4px 10px;font:11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;cursor:pointer;white-space:nowrap;flex:none}
1140
- .dshm-dshchip:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(38,49,72,.06));color:var(--dsw-alias-label-primary,inherit)}
1141
- .dshm-dshchip-v{font-weight:600;color:var(--dsw-alias-label-primary,inherit)}
1142
- .dshm-dshchip-tip{position:absolute;top:calc(100% + 7px);right:0;visibility:hidden;opacity:0;transition:opacity .12s;z-index:60;background:var(--dsw-alias-bg-elevated,var(--dsw-alias-bg-base,#fff));background:color-mix(in srgb,var(--dsw-alias-bg-base,#fff) 86%,transparent);backdrop-filter:blur(14px) saturate(1.3);-webkit-backdrop-filter:blur(14px) saturate(1.3);border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:9px;box-shadow:var(--dsw-shadow-lv1,0 2px 8px rgb(20 24 32 / 8%));padding:7px 10px;text-align:left;pointer-events:none}
1143
- .dshm-dshchip:hover .dshm-dshchip-tip,.dshm-dshchip:focus-visible .dshm-dshchip-tip{visibility:visible;opacity:1}
1144
- .dshm-dshchip-tiprow{font:12px/16px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:600;color:var(--dsw-alias-label-primary,inherit)}
1145
- .dshm-dshchip-tipsub{display:block;margin-top:2px;font-size:10px;line-height:14px;color:var(--dsw-alias-label-caption,#9ca3af)}
1696
+ /* \u5934\u90E8 dsh-m \u7248\u672C\u89D2\u6807\uFF1A\u7B49\u5BBD\u5C0F\u5B57\u5706\u89D2\uFF0C\u9759\u6001\u5C55\u793A\u4E0D\u52A0\u7C97\u4E0D\u53EF\u70B9\uFF080.7.5 \u8D77\u6539\u663E dsh-m \u7248\u672C\uFF09 */
1697
+ .dshm-dshchip{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);background:var(--dsw-alias-bg-layer-1,#f5f6f8);color:var(--dsw-alias-label-secondary,#4b5563);border-radius:999px;padding:5px 10px;font:11px/1 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;flex:none}
1698
+ .dshm-dshchip-v{font-weight:400;color:inherit}
1146
1699
 
1147
1700
  /* 0.4.0\uFF1A\u5F00\u5173 / \u76F8\u4F4D\u70B9 / \u517C\u5BB9\u786E\u8BA4\u5F39\u7A97 */
1148
1701
  .dshm-dot{font-size:9px;line-height:1;vertical-align:middle;margin-right:2px}
@@ -1164,12 +1717,24 @@ var CSS = `
1164
1717
  .dshm-compat-peers li{font-family:ui-monospace,monospace}
1165
1718
  .dshm-compat-risk{font-size:12px;color:#f87171;margin-bottom:12px}
1166
1719
  `;
1720
+ function cssVersionOf(s) {
1721
+ let h2 = 5381;
1722
+ for (let i = 0; i < s.length; i += 1) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0;
1723
+ return (h2 >>> 0).toString(36);
1724
+ }
1167
1725
  function ensureCss() {
1168
- if (typeof document === "undefined" || document.getElementById("dshm-css")) return;
1169
- const el = document.createElement("style");
1170
- el.id = "dshm-css";
1171
- el.textContent = CSS;
1172
- document.head.appendChild(el);
1726
+ if (typeof document === "undefined") return;
1727
+ const ver = cssVersionOf(CSS);
1728
+ const el = document.getElementById("dshm-css");
1729
+ if (el) {
1730
+ if (el.getAttribute("data-ver") === ver) return;
1731
+ el.remove();
1732
+ }
1733
+ const n = document.createElement("style");
1734
+ n.id = "dshm-css";
1735
+ n.setAttribute("data-ver", ver);
1736
+ n.textContent = CSS;
1737
+ document.head.appendChild(n);
1173
1738
  }
1174
1739
  async function api(method, params, signal) {
1175
1740
  const res = await fetch(API, {
@@ -1205,8 +1770,8 @@ function useAsync(fn, deps) {
1205
1770
  }, [run]);
1206
1771
  return { ...state, reload: run };
1207
1772
  }
1208
- function useMarketData() {
1209
- const [query, setQuery] = useState(() => normalizeMarketQuery2({}));
1773
+ function useMarketData(zone = "community") {
1774
+ const [query, setQuery] = useState(() => normalizeMarketQuery2(createZoneState2(zone), zone));
1210
1775
  const [data, setData] = useState(null);
1211
1776
  const [loading, setLoading] = useState(true);
1212
1777
  const [error, setError] = useState(null);
@@ -1223,7 +1788,8 @@ function useMarketData() {
1223
1788
  const params = {
1224
1789
  query: nextQuery.query || void 0,
1225
1790
  category: nextQuery.category || void 0,
1226
- primaryOnly: nextQuery.primaryOnly === true ? true : void 0,
1791
+ source: nextQuery.source || "community",
1792
+ ...nextQuery.sort ? { sort: nextQuery.sort } : {},
1227
1793
  offset: nextQuery.offset,
1228
1794
  limit: nextQuery.limit,
1229
1795
  ...force ? { force: true } : {}
@@ -1239,7 +1805,7 @@ function useMarketData() {
1239
1805
  });
1240
1806
  }, []);
1241
1807
  const updateQuery = useCallback((patch, opts = {}) => {
1242
- const next = resetPageOnFilterChange2(queryRef.current, normalizeMarketQuery2({ ...queryRef.current, ...patch }));
1808
+ const next = resetPageOnFilterChange2(queryRef.current, normalizeMarketQuery2({ ...queryRef.current, ...patch }, zone));
1243
1809
  queryRef.current = next;
1244
1810
  setQuery(next);
1245
1811
  if (opts.fetch !== false) fetchPage(next, opts.force);
@@ -1396,26 +1962,385 @@ function RestartBanner({ note, onDone }) {
1396
1962
  phase === "idle" && !err ? h("button", { className: "dshm-btn sm", onClick: () => onDone(false) }, lookup("common.later")) : null
1397
1963
  );
1398
1964
  }
1399
- function MarketTab({ notify, market, onMutation }) {
1400
- const { data, loading, error, reload, query, updateQuery } = market;
1401
- const [openId, setOpenId] = useState(null);
1402
- const [busyId, setBusyId] = useState(null);
1403
- const [qInput, setQInput] = useState(query.query);
1404
- const debounceRef = useRef(null);
1405
- const items = sortMergedItems2(data && data.items || []);
1965
+ function SearchBox({ placeholder, initial, onCommit }) {
1966
+ const [draft, setDraft] = useState(initial || "");
1967
+ const [composing, setComposing] = useState(false);
1968
+ const timerRef = useRef(null);
1969
+ const inputRef = useRef(null);
1970
+ useEffect(() => () => {
1971
+ if (timerRef.current) clearTimeout(timerRef.current);
1972
+ }, []);
1973
+ const schedule = (v) => {
1974
+ if (timerRef.current) clearTimeout(timerRef.current);
1975
+ timerRef.current = setTimeout(() => onCommit(v), 250);
1976
+ };
1977
+ const onChange = (v) => {
1978
+ setDraft(v);
1979
+ if (composing) return;
1980
+ schedule(v);
1981
+ };
1982
+ const submitNow = () => {
1983
+ if (timerRef.current) clearTimeout(timerRef.current);
1984
+ onCommit(draft);
1985
+ };
1986
+ return h(
1987
+ "div",
1988
+ { className: "dshm-search" },
1989
+ h("input", {
1990
+ ref: inputRef,
1991
+ className: "dshm-input",
1992
+ placeholder,
1993
+ value: draft,
1994
+ style: draft ? { paddingRight: "32px" } : null,
1995
+ onChange: (e) => onChange(e.target.value),
1996
+ onCompositionStart: () => setComposing(true),
1997
+ onCompositionEnd: (e) => {
1998
+ setComposing(false);
1999
+ schedule(e && e.target ? e.target.value : draft);
2000
+ },
2001
+ onKeyDown: (e) => {
2002
+ if (e.key === "Enter" && !e.isComposing && e.keyCode !== 229) {
2003
+ e.preventDefault();
2004
+ submitNow();
2005
+ }
2006
+ },
2007
+ onBlur: submitNow
2008
+ }),
2009
+ draft ? h("button", {
2010
+ className: "dshm-xbtn dshm-xbtn-infield",
2011
+ title: lookup("common.clear"),
2012
+ onClick: () => {
2013
+ setDraft("");
2014
+ if (timerRef.current) clearTimeout(timerRef.current);
2015
+ onCommit("");
2016
+ if (inputRef.current) inputRef.current.focus();
2017
+ }
2018
+ }, h(XIcon)) : null
2019
+ );
2020
+ }
2021
+ function ZoneChips({ zone, counts, labels, active, onPick, trailing }) {
2022
+ const chips = useMemo(() => zoneChips2(counts, labels, zone), [counts, labels, zone]);
2023
+ const [expanded, setExpanded] = useState(false);
2024
+ const [stuck, setStuck] = useState(false);
2025
+ const [fit, setFit] = useState({ rows2: 99, rows1: 99 });
2026
+ const wrapRef = useRef(null);
2027
+ const sentinelRef = useRef(null);
2028
+ const autoRef = useRef(null);
2029
+ const collapsedBudget = Math.min(stuck ? fit.rows1 : fit.rows2, chips.length);
2030
+ const activeIdx = active ? chips.findIndex((c) => c.id === active) : -1;
2031
+ useEffect(() => {
2032
+ if (!active) {
2033
+ autoRef.current = null;
2034
+ setExpanded(false);
2035
+ return;
2036
+ }
2037
+ if (activeIdx >= collapsedBudget && autoRef.current !== active) {
2038
+ autoRef.current = active;
2039
+ setExpanded(true);
2040
+ }
2041
+ }, [active, activeIdx, collapsedBudget]);
2042
+ useLayoutEffect(() => {
2043
+ const el = wrapRef.current;
2044
+ if (!el || expanded) return;
2045
+ let rows2 = 0;
2046
+ let rows1 = 0;
2047
+ let rowCount = 0;
2048
+ let lastTop = null;
2049
+ for (const k of el.children) {
2050
+ if (!(k instanceof HTMLElement) || k.getAttribute("data-chip") !== "1") continue;
2051
+ const t = k.offsetTop;
2052
+ if (lastTop === null || t !== lastTop) {
2053
+ rowCount += 1;
2054
+ lastTop = t;
2055
+ }
2056
+ if (rowCount <= 2) rows2 += 1;
2057
+ if (rowCount <= 1) rows1 += 1;
2058
+ }
2059
+ setFit({ rows2, rows1 });
2060
+ }, [chips.length, expanded, zone]);
2061
+ useEffect(() => {
2062
+ const s = sentinelRef.current;
2063
+ if (!s || typeof IntersectionObserver === "undefined") return;
2064
+ const io = new IntersectionObserver((entries) => setStuck(!entries[0].isIntersecting), { threshold: 0 });
2065
+ io.observe(s);
2066
+ return () => io.disconnect();
2067
+ }, []);
2068
+ const budget = expanded ? chips.length : collapsedBudget;
2069
+ const shown = chips.slice(0, budget);
2070
+ const hidden = chips.length - shown.length;
2071
+ const btn = (c) => h(
2072
+ "button",
2073
+ { key: c.id, "data-chip": "1", className: `dshm-chip${active === c.id ? " on" : ""}`, onClick: () => onPick(active === c.id ? null : c.id) },
2074
+ `${c.labelKey ? lookup(c.labelKey) : c.label}${c.count ? ` ${c.count}` : ""}`
2075
+ );
2076
+ return h(
2077
+ React.Fragment,
2078
+ null,
2079
+ h("div", { ref: sentinelRef, style: { height: "1px" } }),
2080
+ h(
2081
+ "div",
2082
+ { className: "dsvm-chipswrap" },
2083
+ h(
2084
+ "div",
2085
+ { ref: wrapRef, className: "dshm-chips" },
2086
+ h("button", { "data-chip": "1", className: `dshm-chip${active == null ? " on" : ""}`, onClick: () => onPick(null) }, lookup("cat.all")),
2087
+ ...shown.map(btn),
2088
+ hidden > 0 ? h(
2089
+ "button",
2090
+ { "data-chip": "1", className: "dshm-chip", onClick: () => setExpanded(!expanded) },
2091
+ expanded ? "\u2303" : `+${hidden}`
2092
+ ) : null,
2093
+ trailing || null
2094
+ )
2095
+ )
2096
+ );
2097
+ }
2098
+ function compactCount(n) {
2099
+ if (typeof n !== "number" || !Number.isFinite(n)) return "\u2014";
2100
+ if (Math.abs(n) >= 1e3) {
2101
+ const k = n / 1e3;
2102
+ return `${k >= 100 ? Math.round(k) : Number(k.toFixed(1))}k`;
2103
+ }
2104
+ return String(n);
2105
+ }
2106
+ function Shot({ src, onClick }) {
2107
+ const [show, setShow] = useState(false);
2108
+ const ref = useRef(null);
2109
+ useEffect(() => {
2110
+ const el = ref.current;
2111
+ if (!el || typeof IntersectionObserver === "undefined") {
2112
+ setShow(true);
2113
+ return;
2114
+ }
2115
+ const io = new IntersectionObserver(
2116
+ (entries) => {
2117
+ if (entries[0].isIntersecting) {
2118
+ setShow(true);
2119
+ io.disconnect();
2120
+ }
2121
+ },
2122
+ { rootMargin: "200px" }
2123
+ );
2124
+ io.observe(el);
2125
+ return () => io.disconnect();
2126
+ }, []);
2127
+ return h(
2128
+ "div",
2129
+ { ref, className: "dsvm-shotbox", onClick, role: "button", tabIndex: 0 },
2130
+ show ? h("img", { className: "dsvm-shot", src, alt: "", loading: "lazy", referrerPolicy: "no-referrer", fetchPriority: "low" }) : null
2131
+ );
2132
+ }
2133
+ function Lightbox({ shots, index, onNav, onClose }) {
2134
+ return h(
2135
+ "div",
2136
+ { className: "dsvm-lightbox", onClick: onClose },
2137
+ h("img", { src: shots[index], alt: "", onClick: (e) => e.stopPropagation(), referrerPolicy: "no-referrer" }),
2138
+ h(
2139
+ "div",
2140
+ { className: "dsvm-lbnav", onClick: (e) => e.stopPropagation() },
2141
+ h("button", { className: "dsvm-btn", onClick: () => onNav((index - 1 + shots.length) % shots.length) }, "\u2039"),
2142
+ h("span", { className: "dsvm-lbcount" }, `${index + 1} / ${shots.length}`),
2143
+ h("button", { className: "dsvm-btn", onClick: () => onNav((index + 1) % shots.length) }, "\u203A")
2144
+ ),
2145
+ h(
2146
+ "div",
2147
+ { className: "dsvm-lbdots", onClick: (e) => e.stopPropagation() },
2148
+ ...shots.map((s, i) => h("span", { key: s, className: `dsvm-lbdot${i === index ? " on" : ""}`, onClick: () => onNav(i) }))
2149
+ )
2150
+ );
2151
+ }
2152
+ var dsvmModalDepth = 0;
2153
+ function useModalDepth(active) {
2154
+ useEffect(() => {
2155
+ if (!active) return;
2156
+ dsvmModalDepth += 1;
2157
+ return () => {
2158
+ dsvmModalDepth -= 1;
2159
+ };
2160
+ }, [active]);
2161
+ }
2162
+ function DetailModal({ it, labels, busy, onClose, onInstall }) {
2163
+ useModalDepth(true);
2164
+ const shots = it.community === true ? safeScreenshots(it) : [];
2165
+ const [lb, setLb] = useState(null);
2166
+ const [copied, setCopied] = useState(false);
2167
+ useEffect(() => {
2168
+ const onKey = (e) => {
2169
+ if (e.key === "Escape") {
2170
+ if (lb !== null) setLb(null);
2171
+ else onClose();
2172
+ }
2173
+ if (lb !== null && shots.length > 1) {
2174
+ if (e.key === "ArrowLeft") setLb((i) => (i - 1 + shots.length) % shots.length);
2175
+ if (e.key === "ArrowRight") setLb((i) => (i + 1) % shots.length);
2176
+ }
2177
+ };
2178
+ document.addEventListener("keydown", onKey);
2179
+ return () => document.removeEventListener("keydown", onKey);
2180
+ }, [lb, shots.length, onClose]);
2181
+ const catLabel = labels && labels[it.category] || (["market", "tools", "ui", "search", "other"].includes(it.category) ? lookup("cat." + it.category) : it.category);
2182
+ const installCmd = typeof it.install === "string" && it.install !== "" ? it.install : it.source === "npm" ? `dsh plugin --profile web add ${it.npm}` : `dsh plugin --profile web add github:${it.github}`;
2183
+ const copyCmd = async () => {
2184
+ try {
2185
+ await navigator.clipboard.writeText(installCmd);
2186
+ setCopied(true);
2187
+ setTimeout(() => setCopied(false), 1600);
2188
+ } catch {
2189
+ }
2190
+ };
2191
+ const descFull = browserLang() === "en" && typeof it.descriptionEn === "string" && it.descriptionEn !== "" ? it.descriptionEn : it.description;
2192
+ const kv = (k, v) => h(React.Fragment, { key: k }, h("dt", null, k), h("dd", null, v));
2193
+ return h(
2194
+ "div",
2195
+ { className: "dsvm-modal", onClick: onClose },
2196
+ h(
2197
+ "div",
2198
+ { className: "dsvm-modalbox", onClick: (e) => e.stopPropagation() },
2199
+ h(
2200
+ "div",
2201
+ { className: "dsvm-modalhead" },
2202
+ h(Icon, { entry: it }),
2203
+ h("span", { className: "dshm-name" }, it.name),
2204
+ it.deprecated === true ? h("span", { className: "dshm-badge warn" }, lookup("badge.deprecated")) : null,
2205
+ it.community !== true && Array.isArray(it.verified) && it.verified.length ? h("span", { className: "dshm-badge", title: it.verified.join("\u3001") }, lookup("badge.verified")) : null,
2206
+ it.installed ? h("span", { className: "dshm-badge" }, lookup("badge.installed")) : null,
2207
+ it.community === true ? h("span", { className: "dshm-badge info" }, lookup("badge.community")) : null,
2208
+ h("span", { className: "dshm-badge info" }, it.source === "npm" ? "npm" : "github"),
2209
+ h("span", { className: "dshm-spacer" }),
2210
+ h("button", { className: "dshm-xbtn", "aria-label": lookup("common.close"), title: lookup("common.close"), onClick: onClose }, h(XIcon))
2211
+ ),
2212
+ it.community === true ? h(
2213
+ "div",
2214
+ { className: "dsvm-byline" },
2215
+ it.owner ? h("span", null, `by ${it.owner}`) : null,
2216
+ typeof it.stars === "number" ? h("span", { title: String(it.stars) }, `${compactCount(it.stars)} \u2605`) : null,
2217
+ typeof it.downloads === "number" ? h("span", { title: String(it.downloads) }, `${compactCount(it.downloads)} \u2193`) : null
2218
+ ) : null,
2219
+ h(LinksRow, { npm: it.npm, github: it.github, homepage: it.homepage }),
2220
+ h(
2221
+ "dl",
2222
+ { className: "dsvm-kv" },
2223
+ kv(lookup("modal.category"), catLabel),
2224
+ it.added ? kv(lookup("modal.added"), it.added) : null,
2225
+ it.community === true ? kv(
2226
+ lookup("modal.dlwindow"),
2227
+ typeof it.downloads === "number" ? h("span", { title: String(it.downloads) }, `${compactCount(it.downloads)}\uFF08${it.downloadsStart || "?"} ~ ${it.downloadsEnd || "?"}${it.downloadsCheckedAt ? `\uFF0C${lookup("modal.checkedat")} ${it.downloadsCheckedAt}` : ""}\uFF09`) : lookup("modal.dlnone")
2228
+ ) : null,
2229
+ kv(lookup("detail.latest"), it.latestVersion ? `v${it.latestVersion}` : it.latestTag ? it.latestTag : it.latestSha ? it.latestSha : it.latestError ? it.version ? lookup("sub.snapshot", { v: it.version }) : it.latestError : "\u2014"),
2230
+ kv(lookup("detail.installed"), it.installedPkg ? `${it.installedPkg} v${it.installedVersion || "?"}` : lookup("installed.none")),
2231
+ it.community !== true && Array.isArray(it.verified) && it.verified.length ? kv(lookup("modal.verified"), it.verified.join("\u3001")) : null,
2232
+ (it.tags || []).length ? kv(lookup("modal.tags"), it.tags.join(", ")) : null,
2233
+ it.deprecated === true && it.replacement ? kv(lookup("modal.replacement"), it.replacement) : null
2234
+ ),
2235
+ h("div", { className: "dshm-desc", style: { WebkitLineClamp: "unset" } }, descFull),
2236
+ shots.length ? h(
2237
+ "div",
2238
+ { className: "dsvm-shotrow" },
2239
+ ...shots.map((src, i) => h(Shot, { key: src, src, onClick: () => setLb(i) }))
2240
+ ) : null,
2241
+ it.community === true ? h(
2242
+ "details",
2243
+ { className: "dsvm-fold" },
2244
+ h("summary", null, lookup("detail.capabilities")),
2245
+ Array.isArray(it.capabilities) && it.capabilities.length ? h(
2246
+ "div",
2247
+ null,
2248
+ h("div", null, it.capabilities.join(", ")),
2249
+ Array.isArray(it.capabilityRedLines) && it.capabilityRedLines.length ? h("div", { className: "dshm-err" }, `${lookup("detail.redlines")}: ${it.capabilityRedLines.join("; ")}`) : null
2250
+ ) : h("div", { className: "dshm-hint" }, lookup("detail.capabilities.unscanned"))
2251
+ ) : null,
2252
+ h(
2253
+ "details",
2254
+ { className: "dsvm-fold" },
2255
+ h("summary", null, lookup("modal.installcmd")),
2256
+ h(
2257
+ "div",
2258
+ { className: "dsvm-cmdrow" },
2259
+ h("code", { className: "dsvm-code" }, installCmd),
2260
+ h("button", { className: "dshm-btn sm", onClick: copyCmd }, copied ? lookup("modal.copied") : lookup("modal.copy"))
2261
+ )
2262
+ ),
2263
+ h(
2264
+ "div",
2265
+ { className: "dsvm-modalactions" },
2266
+ it.installed ? h("span", { className: "dshm-hint" }, lookup("manage.hint")) : h("button", { className: "dshm-btn primary", disabled: busy, onClick: () => onInstall(it) }, busy ? h(Spin) : lookup("action.install"))
2267
+ )
2268
+ ),
2269
+ lb !== null && shots.length ? h(Lightbox, { shots, index: lb, onNav: setLb, onClose: () => setLb(null) }) : null
2270
+ );
2271
+ }
2272
+ var ZONE_TABS = [
2273
+ { id: "community", labelKey: "zone.community" },
2274
+ { id: "primary", labelKey: "zone.primary" },
2275
+ { id: "favorites", labelKey: "zone.favorites" }
2276
+ ];
2277
+ function MarketTab({ notify, markets, onMutation, ops, favorites }) {
2278
+ const [zone, setZone] = useState("community");
2279
+ const market = zone === "favorites" ? null : markets[zone];
2280
+ const { data, loading, error, reload, query, updateQuery } = market || {};
2281
+ const [detailId, setDetailId] = useState(null);
2282
+ const [compatConfirm, setCompatConfirm] = useState(null);
2283
+ useModalDepth(compatConfirm != null);
2284
+ const [filterOpen, setFilterOpen] = useState(false);
2285
+ useModalDepth(filterOpen);
2286
+ useEffect(() => {
2287
+ if (!filterOpen) return;
2288
+ const onKey = (e) => {
2289
+ if (e.key === "Escape") setFilterOpen(false);
2290
+ };
2291
+ document.addEventListener("keydown", onKey);
2292
+ return () => document.removeEventListener("keydown", onKey);
2293
+ }, [filterOpen]);
2294
+ const [favDetailItem, setFavDetailItem] = useState(null);
2295
+ const [pageJump, setPageJump] = useState("");
2296
+ const activeInstallTarget = (ops.records.find((r) => r.kind === "install" && (r.status === "running" || r.status === "queued" || r.status === "input")) || {}).target || null;
2297
+ const items = data && data.items || [];
1406
2298
  const total = data && data.total || 0;
1407
- const limit = data && data.limit || MARKET_PAGE_SIZE2;
2299
+ const limit = data && data.limit || DEFAULT_PAGE_SIZE2;
1408
2300
  const offset = data && data.offset || 0;
1409
2301
  const page = total > 0 ? Math.floor(offset / limit) + 1 : 1;
1410
2302
  const pages = total > 0 ? Math.max(1, Math.ceil(total / limit)) : 1;
1411
2303
  const counts = data && data.categoryCounts || {};
1412
2304
  const notice = data ? marketNotice2(data.registryState, data.community) : null;
1413
- const onSearchInput = (value) => {
1414
- setQInput(value);
1415
- if (debounceRef.current) clearTimeout(debounceRef.current);
1416
- debounceRef.current = setTimeout(() => updateQuery({ query: value }), 300);
2305
+ const gotoPage = (p) => {
2306
+ updateQuery({ offset: Math.max(0, (p - 1) * limit) });
2307
+ if (typeof document !== "undefined") {
2308
+ const el = document.querySelector(".dsvm-chipswrap");
2309
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "start" });
2310
+ }
1417
2311
  };
1418
- const [compatConfirm, setCompatConfirm] = useState(null);
2312
+ const pageJumpNum = pageJump ? parseInt(pageJump, 10) : 0;
2313
+ const pageJumpValid = pageJumpNum >= 1 && pageJumpNum <= pages && pageJumpNum !== page;
2314
+ const jumpToPage = () => {
2315
+ if (!pageJumpValid) return;
2316
+ gotoPage(pageJumpNum);
2317
+ setPageJump("");
2318
+ };
2319
+ const detailItem = detailId ? items.find((x) => x.id === detailId) : null;
2320
+ const detailLabels = communityLabels(data);
2321
+ const zoneChips3 = h(
2322
+ "div",
2323
+ { className: "dshm-chips" },
2324
+ ...ZONE_TABS.map((z) => {
2325
+ const zCount = z.id === "community" ? markets.community.data && markets.community.data.total || 0 : z.id === "primary" ? markets.primary.data && markets.primary.data.total || 0 : favorites && favorites.list.length;
2326
+ return h(
2327
+ "button",
2328
+ {
2329
+ key: z.id,
2330
+ className: `dshm-chip${zone === z.id ? " on" : ""}`,
2331
+ onClick: () => setZone(z.id)
2332
+ },
2333
+ lookup(z.labelKey),
2334
+ zCount ? ` ${zCount}` : ""
2335
+ );
2336
+ })
2337
+ );
2338
+ const searchRow = market ? h(
2339
+ "div",
2340
+ { className: "dsvm-searchrow" },
2341
+ h(SearchBox, { key: zone, placeholder: lookup("search.ph"), initial: query.query, onCommit: (v) => updateQuery({ query: v }) })
2342
+ ) : null;
2343
+ const zoneBar = zoneChips3;
1419
2344
  const installDone = (res) => {
1420
2345
  notify({
1421
2346
  kind: "ok",
@@ -1423,14 +2348,23 @@ function MarketTab({ notify, market, onMutation }) {
1423
2348
  text: lookup("notify.installed", { pkg: res.pkg, version: res.version ? ` v${res.version}` : "" }) + (res.buildApprovals && res.buildApprovals.length ? lookup("notify.builds", { names: res.buildApprovals.join(", ") }) : res.fallbackAllBuilds ? lookup("notify.builds.fallback") : "") + (res.bundleWarning === "no-patch-layer" ? lookup("notify.bundlewarning") : "")
1424
2349
  });
1425
2350
  };
1426
- const doInstall = async (it, version, forceIncompatible) => {
1427
- setBusyId(it.id);
2351
+ const doInstall = async (it, version, forceIncompatible, reuseOpId) => {
1428
2352
  try {
1429
- const res = await api("install", { id: it.id, ...version ? { version } : {}, ...forceIncompatible ? { forceIncompatible: true } : {} });
2353
+ const res = await ops.runOp(
2354
+ "install",
2355
+ it.id,
2356
+ () => api("install", { id: it.id, ...version ? { version } : {}, ...forceIncompatible ? { forceIncompatible: true } : {} }),
2357
+ { version, npm: it.npm, github: it.github },
2358
+ // 审计 #6:meta 携带 npm 供 stillApplies 比对
2359
+ reuseOpId
2360
+ // 审计 #12:确认/重试复用同一记录,不再新开
2361
+ );
1430
2362
  installDone(res);
1431
2363
  await (onMutation ? onMutation() : reload(false));
1432
2364
  } catch (e) {
1433
- if (e && e.guard) {
2365
+ if (e && e.opSuperseded) {
2366
+ notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.name }) });
2367
+ } else if (e && e.guard) {
1434
2368
  notify({
1435
2369
  kind: "err",
1436
2370
  text: [
@@ -1442,12 +2376,10 @@ function MarketTab({ notify, market, onMutation }) {
1442
2376
  ].filter(Boolean).join(" | ")
1443
2377
  });
1444
2378
  } else if (e && e.issue) {
1445
- setCompatConfirm({ it, version, issue: e.issue });
2379
+ setCompatConfirm({ it, version, issue: e.issue, opId: e.opId });
1446
2380
  } else {
1447
2381
  notify({ kind: "err", text: lookup("failed.install", { err: e && e.message || e }) });
1448
2382
  }
1449
- } finally {
1450
- setBusyId(null);
1451
2383
  }
1452
2384
  };
1453
2385
  const CompatDialog = compatConfirm ? h(
@@ -1473,82 +2405,118 @@ function MarketTab({ notify, market, onMutation }) {
1473
2405
  { className: "dshm-actions" },
1474
2406
  h("button", {
1475
2407
  className: "dshm-btn sm",
1476
- onClick: () => setCompatConfirm(null)
2408
+ onClick: () => {
2409
+ if (compatConfirm.opId && ops.dispose) ops.dispose(compatConfirm.opId, lookup("op.cancelled"));
2410
+ setCompatConfirm(null);
2411
+ }
1477
2412
  }, lookup("common.cancel")),
1478
2413
  h("button", {
1479
2414
  className: "dshm-btn primary sm",
1480
2415
  onClick: () => {
1481
2416
  const pending = compatConfirm;
1482
2417
  setCompatConfirm(null);
1483
- doInstall(pending.it, pending.version, true);
2418
+ doInstall(pending.it, pending.version, true, pending.opId);
1484
2419
  }
1485
2420
  }, lookup("compat.force"))
1486
2421
  )
1487
2422
  )
1488
2423
  ) : null;
1489
- return h(
1490
- React.Fragment,
1491
- null,
1492
- CompatDialog,
1493
- notice ? h(
2424
+ const openFavDetail = async (fav) => {
2425
+ const id = fav && fav.id;
2426
+ const scan = (d) => d && Array.isArray(d.items) ? d.items.find((x) => x && x.id === id) : null;
2427
+ const hit = scan(markets.community.data) || scan(markets.primary.data);
2428
+ if (hit) {
2429
+ setFavDetailItem(hit);
2430
+ return;
2431
+ }
2432
+ try {
2433
+ const res = await api("market", { query: id, source: "all", limit: 8 });
2434
+ const exact = res && Array.isArray(res.items) ? res.items.find((x) => x && x.id === id) : null;
2435
+ if (exact) {
2436
+ setFavDetailItem(exact);
2437
+ return;
2438
+ }
2439
+ } catch {
2440
+ }
2441
+ setFavDetailItem(fav && fav.snapshot || null);
2442
+ };
2443
+ if (zone === "favorites") {
2444
+ return h(
2445
+ React.Fragment,
2446
+ null,
2447
+ CompatDialog,
2448
+ zoneBar,
2449
+ h(FavoriteZone, { favorites, onOpen: openFavDetail }),
2450
+ favDetailItem ? h(DetailModal, {
2451
+ it: favDetailItem,
2452
+ labels: communityLabels(markets.community.data),
2453
+ busy: activeInstallTarget === favDetailItem.id,
2454
+ onClose: () => setFavDetailItem(null),
2455
+ onInstall: (it2) => doInstall(it2)
2456
+ }) : null
2457
+ );
2458
+ }
2459
+ const optRow = (label, activeOpt, onPick) => h(
2460
+ "button",
2461
+ { key: label, className: `dsvm-filteropt${activeOpt ? " on" : ""}`, onClick: onPick },
2462
+ h("span", null, label),
2463
+ activeOpt ? h("span", { className: "dsvm-filtercheck" }, "\u2713") : null
2464
+ );
2465
+ const curSortField = query && query.sort && query.sort.field || "downloads";
2466
+ const curSortDir = query && query.sort && query.sort.dir || "desc";
2467
+ const filterPop = zone === "community" && filterOpen ? h(
2468
+ "div",
2469
+ { className: "dsvm-filterpop", onClick: (e) => e.stopPropagation() },
2470
+ h(
1494
2471
  "div",
1495
- { className: notice.key === "notice.unavailable" ? "dshm-err" : "dshm-hint" },
1496
- lookup(notice.key, { count: notice.count })
1497
- ) : null,
1498
- notice && notice.communityFallback ? h("div", { className: "dshm-hint" }, lookup("community.fallback")) : null,
1499
- notice && notice.communityStale ? h("div", { className: "dshm-hint" }, lookup("community.stale")) : null,
1500
- total > 200 ? h("div", { className: "dshm-hint" }, lookup("market.perf")) : null,
2472
+ { className: "dsvm-filtergroup" },
2473
+ h("div", { className: "dsvm-filtergt" }, lookup("filter.sortfield")),
2474
+ ...[["downloads", "filter.field.downloads"], ["stars", "filter.field.stars"], ["added", "filter.field.added"]].map(([f, key]) => optRow(lookup(key), curSortField === f, () => updateQuery({ sort: { field: f, dir: curSortDir } })))
2475
+ ),
1501
2476
  h(
1502
2477
  "div",
1503
- { className: "dshm-row" },
1504
- h("input", {
1505
- className: "dshm-input",
1506
- placeholder: lookup("search.ph"),
1507
- value: qInput,
1508
- onChange: (e) => onSearchInput(e.target.value)
1509
- }),
1510
- h("button", { className: "dshm-btn", onClick: () => reload(true), title: lookup("settings.policy.v") }, loading ? Spin() : `\u21BB ${lookup("common.refresh")}`)
2478
+ { className: "dsvm-filtergroup" },
2479
+ h("div", { className: "dsvm-filtergt" }, lookup("filter.sortdir")),
2480
+ ...[["desc", "filter.dir.desc"], ["asc", "filter.dir.asc"]].map(([d, key]) => optRow(lookup(key), curSortDir === d, () => updateQuery({ sort: { field: curSortField, dir: d } })))
1511
2481
  ),
1512
2482
  h(
1513
2483
  "div",
1514
- { className: "dshm-chips" },
1515
- h("button", { className: `dshm-chip${query.category === null ? " on" : ""}`, onClick: () => updateQuery({ category: null, primaryOnly: false, offset: 0 }) }, lookup("cat.all")),
1516
- h("button", { className: `dshm-chip${query.primaryOnly === true ? " on" : ""}`, onClick: () => updateQuery({ primaryOnly: query.primaryOnly !== true, offset: 0 }) }, lookup("cat.primaryonly")),
1517
- CATEGORIES2.map((key) => {
1518
- const n = typeof counts[key] === "number" ? counts[key] : 0;
1519
- return h(
1520
- "button",
1521
- { key, className: `dshm-chip${query.category === key ? " on" : ""}`, onClick: () => updateQuery({ category: query.category === key ? null : key, offset: 0 }) },
1522
- `${lookup("cat." + key)}${n ? ` ${n}` : ""}`
1523
- );
1524
- })
2484
+ { className: "dsvm-filtergroup" },
2485
+ h("div", { className: "dsvm-filtergt" }, lookup("filter.pagesize")),
2486
+ ...MARKET_PAGE_SIZES2.map((n) => optRow(String(n), limit === n, () => updateQuery({ limit: n, offset: 0 })))
2487
+ )
2488
+ ) : null;
2489
+ const filterTrigger = zone === "community" ? h(
2490
+ "div",
2491
+ { className: "dsvm-filterwrap", key: "__filter" },
2492
+ filterOpen ? h("div", { className: "dsvm-filterback", onClick: () => setFilterOpen(false) }) : null,
2493
+ h(
2494
+ "button",
2495
+ // 0.7.3:独立于分类 chips 的筛选按钮样式(方角矩形 + 前置 chevron,参考 dsh-market)
2496
+ { className: `dsvm-filterbtn${filterOpen ? " on" : ""}`, onClick: () => setFilterOpen(!filterOpen) },
2497
+ h("span", { className: "dsvm-filterchev", "aria-hidden": "true" }, filterOpen ? "\u2303" : "\u2304"),
2498
+ lookup("filter.title")
1525
2499
  ),
1526
- (() => {
1527
- const groups = splitCategories2(counts);
1528
- if (!groups.community.length && !groups.unknown.length) return null;
1529
- const chip = (c) => h(
1530
- "button",
1531
- { key: c.id, className: `dshm-chip${query.category === c.id ? " on" : ""}`, onClick: () => updateQuery({ category: query.category === c.id ? null : c.id, offset: 0 }) },
1532
- `${c.label}${c.count ? ` ${c.count}` : ""}`
1533
- );
1534
- return h(
1535
- React.Fragment,
1536
- null,
1537
- groups.community.length ? h(
1538
- "div",
1539
- { className: "dshm-chips" },
1540
- h("span", { className: "dshm-hint" }, lookup("community.group")),
1541
- ...groups.community.map(chip)
1542
- ) : null,
1543
- groups.unknown.length ? h(
1544
- "div",
1545
- { className: "dshm-chips" },
1546
- h("span", { className: "dshm-hint" }, lookup("community.newgroup")),
1547
- ...groups.unknown.map(chip)
1548
- ) : null
1549
- );
1550
- })(),
1551
- busyId ? h(ProgressLine, { key: "prog" }) : null,
2500
+ filterPop
2501
+ ) : null;
2502
+ return h(
2503
+ React.Fragment,
2504
+ null,
2505
+ CompatDialog,
2506
+ zoneBar,
2507
+ searchRow,
2508
+ notice && notice.notice ? h("div", { className: "dshm-err" }, lookup(notice.notice.key)) : null,
2509
+ notice && notice.communityFallback ? h("div", { className: "dshm-hint" }, lookup("community.fallback")) : null,
2510
+ notice && notice.communityStale ? h("div", { className: "dshm-hint" }, lookup("community.stale")) : null,
2511
+ h(ZoneChips, {
2512
+ zone,
2513
+ counts,
2514
+ labels: communityLabels(data),
2515
+ active: query.category,
2516
+ onPick: (id) => updateQuery({ category: id, offset: 0 }),
2517
+ trailing: filterTrigger
2518
+ }),
2519
+ activeInstallTarget ? h(ProgressLine, { key: "prog" }) : null,
1552
2520
  loading && !data ? h("div", { className: "dshm-empty" }, lookup("market.loading"), Spin()) : error ? h("div", { className: "dshm-err" }, lookup("failed.load", { err: error })) : items.length === 0 ? h("div", { className: "dshm-empty" }, lookup("market.empty")) : h(
1553
2521
  React.Fragment,
1554
2522
  null,
@@ -1560,67 +2528,89 @@ function MarketTab({ notify, market, onMutation }) {
1560
2528
  icon: h(Icon, { entry: it }),
1561
2529
  name: it.name,
1562
2530
  badges: [
2531
+ activeInstallTarget === it.id ? h(
2532
+ "button",
2533
+ {
2534
+ className: "dshm-badge warn",
2535
+ key: "busy",
2536
+ title: lookup("op.panel.jump"),
2537
+ onClick: (e) => {
2538
+ e.stopPropagation();
2539
+ if (typeof document !== "undefined") {
2540
+ const el = document.querySelector(".dsvm-ops");
2541
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "center", behavior: "smooth" });
2542
+ }
2543
+ }
2544
+ },
2545
+ lookup("op.status.running")
2546
+ ) : null,
2547
+ it.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
2548
+ it.community !== true && Array.isArray(it.verified) && it.verified.length ? h("span", { className: "dshm-badge", key: "v", title: it.verified.join("\u3001") }, lookup("badge.verified")) : null,
1563
2549
  it.outdated ? h("span", { className: "dshm-badge warn", key: "u" }, lookup("badge.update")) : null,
1564
2550
  it.installed ? h("span", { className: "dshm-badge", key: "i" }, lookup("badge.installed")) : null,
1565
2551
  it.community === true ? h("span", { className: "dshm-badge info", key: "c" }, lookup("badge.community")) : null,
1566
2552
  h("span", { className: "dshm-badge info", key: "s" }, it.source === "npm" ? "npm" : "github")
1567
2553
  ],
1568
- desc: it.description,
2554
+ byline: it.community === true ? [
2555
+ it.owner ? { text: `by ${it.owner}` } : null,
2556
+ typeof it.downloads === "number" ? { text: `${compactCount(it.downloads)} \u2193`, title: String(it.downloads) } : null,
2557
+ typeof it.stars === "number" ? { text: `${compactCount(it.stars)} \u2605`, title: String(it.stars) } : null
2558
+ ].filter(Boolean) : (it.tags || []).map((t) => ({ text: `#${t}` })),
2559
+ desc: browserLang() === "en" && typeof it.descriptionEn === "string" && it.descriptionEn !== "" ? it.descriptionEn : it.description,
2560
+ clampLines: it.community === true ? 5 : 2,
1569
2561
  sub: [
1570
2562
  it.latestVersion ? lookup("sub.latest", { v: it.latestVersion }) : it.latestTag ? it.latestTag : it.latestSha ? lookup("sub.head", { sha: it.latestSha.slice(0, 7) }) : null,
1571
2563
  it.installedVersion ? lookup("sub.installed", { v: it.installedVersion }) : null,
1572
- it.latestError ? lookup("version.failed") : null
2564
+ it.latestError ? it.version ? lookup("sub.snapshot", { v: it.version }) : lookup("version.failed") : null
1573
2565
  ].filter(Boolean).join(" \xB7 "),
1574
2566
  links: h(LinksRow, { npm: it.npm, github: it.github, homepage: it.homepage }),
1575
- open: openId === it.id,
1576
- onToggle: () => setOpenId(openId === it.id ? null : it.id),
1577
- detail: DetailRows([
1578
- [lookup("detail.id"), it.id],
1579
- [lookup("detail.source"), it.source === "npm" ? h(ExtLink, { href: `https://www.npmjs.com/package/${it.npm}` }, `npm \xB7 ${it.npm}`) : h(ExtLink, { href: `https://github.com/${it.github}` }, `GitHub \xB7 ${it.github}`)],
1580
- [lookup("detail.latest"), it.latestVersion ? `v${it.latestVersion}` : it.latestTag ? it.latestTag : it.latestSha ? it.latestSha : it.latestError || "\u2014"],
1581
- [lookup("detail.installed"), it.installedPkg ? `${it.installedPkg} v${it.installedVersion || "?"}` : lookup("installed.none")],
1582
- [lookup("detail.tags"), (it.tags || []).join(", ") || "\u2014"],
1583
- it.latestError ? [lookup("version.failed"), it.latestError] : null,
1584
- it.community === true ? [lookup("detail.capabilities"), Array.isArray(it.capabilities) && it.capabilities.length ? `${it.capabilities.join(", ")}${Array.isArray(it.capabilityRedLines) && it.capabilityRedLines.length ? `\uFF1B${lookup("detail.redlines")}: ${it.capabilityRedLines.join("; ")}` : ""}` : lookup("detail.capabilities.unscanned")] : null,
1585
- it.community === true && safeScreenshots(it).length ? [lookup("detail.screenshots"), h(
1586
- "div",
1587
- { className: "dshm-chips" },
1588
- ...safeScreenshots(it).map((src) => h("img", { key: src, src, alt: "", loading: "lazy", referrerPolicy: "no-referrer", style: { maxWidth: "220px", maxHeight: "140px", borderRadius: "6px", border: "1px solid rgba(127,127,127,.25)" } }))
1589
- )] : null
1590
- ]),
1591
- actions: [
1592
- it.installed ? h("span", { className: "dshm-hint", key: "hint" }, lookup("manage.hint")) : h(
1593
- "button",
1594
- {
1595
- key: "install",
1596
- className: "dshm-btn primary sm",
1597
- disabled: busyId === it.id,
1598
- onClick: (e) => {
1599
- e.stopPropagation();
1600
- doInstall(it);
1601
- }
1602
- },
1603
- busyId === it.id ? h(Spin) : lookup("action.install")
1604
- )
1605
- ]
2567
+ onToggle: () => setDetailId(it.id),
2568
+ topRight: favorites ? h(
2569
+ "button",
2570
+ {
2571
+ className: `dsvm-favbtn${favorites.list.some((f) => f.id === it.id) ? " on" : ""}`,
2572
+ title: favorites.list.some((f) => f.id === it.id) ? lookup("fav.remove") : lookup("fav.add"),
2573
+ onClick: (e) => {
2574
+ e.stopPropagation();
2575
+ favorites.toggle(snapshotOf(it));
2576
+ }
2577
+ },
2578
+ favorites.list.some((f) => f.id === it.id) ? "\u2605" : "\u2606"
2579
+ ) : null
1606
2580
  }))
1607
2581
  ),
1608
2582
  pages > 1 ? h(
1609
2583
  "div",
1610
- { className: "dshm-row", style: { justifyContent: "center", marginTop: "4px" } },
1611
- h("button", {
1612
- className: "dshm-btn sm",
1613
- disabled: page <= 1 || loading,
1614
- onClick: () => updateQuery({ offset: Math.max(0, offset - limit) })
1615
- }, lookup("market.page.prev")),
1616
- h("span", { className: "dshm-hint" }, lookup("market.page.info", { page, pages, total })),
1617
- h("button", {
1618
- className: "dshm-btn sm",
1619
- disabled: page >= pages || loading,
1620
- onClick: () => updateQuery({ offset: Math.min(total - 1, offset + limit) })
1621
- }, lookup("market.page.next"))
2584
+ { className: "dsvm-pager" },
2585
+ h("button", { className: "dsvm-pagebtn", disabled: page <= 1 || loading, onClick: () => gotoPage(page - 1) }, "\u2039"),
2586
+ ...pageItems2(page, pages).map((p, i) => p === "..." ? h("span", { key: `e${i}`, className: "dshm-hint" }, "\u2026") : h("button", { key: p, className: `dsvm-pagebtn${p === page ? " on" : ""}`, onClick: () => gotoPage(p) }, String(p))),
2587
+ h("button", { className: "dsvm-pagebtn", disabled: page >= pages || loading, onClick: () => gotoPage(page + 1) }, "\u203A"),
2588
+ // 页号跳转(0.7.3):输入有效页号回车或点「跳转」直达
2589
+ h(
2590
+ "span",
2591
+ { className: "dsvm-pagejump" },
2592
+ h("input", {
2593
+ className: "dsvm-pagejump-input",
2594
+ value: pageJump,
2595
+ placeholder: lookup("pager.jump.ph"),
2596
+ inputMode: "numeric",
2597
+ "aria-label": lookup("pager.jump.ph"),
2598
+ onChange: (e) => setPageJump(e.target.value.replace(/\D/g, "").slice(0, 4)),
2599
+ onKeyDown: (e) => {
2600
+ if (e.key === "Enter" && !e.isComposing) jumpToPage();
2601
+ }
2602
+ }),
2603
+ h("button", { className: "dsvm-pagebtn dsvm-pagejump-btn", disabled: !pageJumpValid || loading, onClick: jumpToPage }, lookup("pager.jump"))
2604
+ )
1622
2605
  ) : null
1623
- )
2606
+ ),
2607
+ detailItem ? h(DetailModal, {
2608
+ it: detailItem,
2609
+ labels: detailLabels,
2610
+ busy: activeInstallTarget === detailItem.id,
2611
+ onClose: () => setDetailId(null),
2612
+ onInstall: (it2) => doInstall(it2)
2613
+ }) : null
1624
2614
  );
1625
2615
  }
1626
2616
  var PHASE_LABEL = { resolving: "phase.resolving", downloading: "phase.downloading", linking: "phase.linking", building: "phase.building" };
@@ -1673,36 +2663,37 @@ function ReadmeBlock({ pkg }) {
1673
2663
  state.truncated ? h("div", { className: "dshm-md-note" }, lookup("readme.truncated")) : null
1674
2664
  );
1675
2665
  }
1676
- function InstalledTab({ notify, installed, onMutation }) {
2666
+ function InstalledTab({ notify, installed, onMutation, ops }) {
1677
2667
  const { loading, data, error, reload } = installed;
1678
2668
  const [openPkg, setOpenPkg] = useState(null);
1679
2669
  const [readmePkg, setReadmePkg] = useState(null);
1680
- const [busyPkg, setBusyPkg] = useState(null);
2670
+ const activeMutateTarget = (ops.records.find((r) => r.kind !== "install" && (r.status === "running" || r.status === "queued" || r.status === "input")) || {}).target || null;
1681
2671
  const doToggle = async (it, enabled) => {
1682
- setBusyPkg(it.pkg);
1683
2672
  const call = () => api("set-enabled", { pkg: it.pkg, enabled });
1684
2673
  try {
1685
2674
  let res;
1686
2675
  try {
1687
- res = await call();
2676
+ res = await ops.runOp("toggle", it.pkg, call, { on: enabled });
1688
2677
  } catch (first) {
2678
+ if (first && first.opSuperseded) throw first;
1689
2679
  await new Promise((r) => setTimeout(r, 1500));
1690
- res = await call();
2680
+ res = await ops.runOp("toggle", it.pkg, call, { on: enabled }, first && first.opId);
1691
2681
  }
1692
2682
  const note = toggleNoticeKeys2(res);
1693
2683
  const extra = res.warnings && res.warnings.length ? `\uFF08${res.warnings.join("\uFF1B")}\uFF09` : "";
1694
2684
  notify({ kind: "ok", needsRestart: note.needsRestart, text: lookup(note.textKey, note.params) + extra });
1695
2685
  await (onMutation ? onMutation() : reload());
1696
2686
  } catch (e) {
1697
- notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
1698
- } finally {
1699
- setBusyPkg(null);
2687
+ if (e && e.opSuperseded) {
2688
+ notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
2689
+ } else {
2690
+ notify({ kind: "err", text: lookup("toggle.failed", { err: e && e.message || e }) });
2691
+ }
1700
2692
  }
1701
2693
  };
1702
2694
  const doUninstall = async (it) => {
1703
- setBusyPkg(it.pkg);
1704
2695
  try {
1705
- const res = await api("uninstall", { pkg: it.pkg });
2696
+ const res = await ops.runOp("uninstall", it.pkg, () => api("uninstall", { pkg: it.pkg }));
1706
2697
  notify({
1707
2698
  kind: "ok",
1708
2699
  needsRestart: true,
@@ -1710,15 +2701,16 @@ function InstalledTab({ notify, installed, onMutation }) {
1710
2701
  });
1711
2702
  await (onMutation ? onMutation() : reload());
1712
2703
  } catch (e) {
1713
- notify({ kind: "err", text: lookup("failed.uninstall", { err: e && e.message || e }) });
1714
- } finally {
1715
- setBusyPkg(null);
2704
+ if (e && e.opSuperseded) {
2705
+ notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
2706
+ } else {
2707
+ notify({ kind: "err", text: lookup("failed.uninstall", { err: e && e.message || e }) });
2708
+ }
1716
2709
  }
1717
2710
  };
1718
2711
  const doUpgrade = async (it) => {
1719
- setBusyPkg(it.pkg);
1720
2712
  try {
1721
- const res = await api("upgrade", { pkg: it.pkg });
2713
+ const res = await ops.runOp("upgrade", it.pkg, () => api("upgrade", { pkg: it.pkg }));
1722
2714
  notify({
1723
2715
  kind: "ok",
1724
2716
  needsRestart: true,
@@ -1741,11 +2733,11 @@ function InstalledTab({ notify, installed, onMutation }) {
1741
2733
  e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
1742
2734
  ].filter(Boolean).join(" | ")
1743
2735
  });
2736
+ } else if (e && e.opSuperseded) {
2737
+ notify({ kind: "ok", text: lookup("op.superseded.note", { target: it.pkg }) });
1744
2738
  } else {
1745
2739
  notify({ kind: "err", text: lookup("failed.upgrade", { err: e && e.message || e }) });
1746
2740
  }
1747
- } finally {
1748
- setBusyPkg(null);
1749
2741
  }
1750
2742
  };
1751
2743
  if (loading && !data) return h("div", { className: "dshm-empty" }, lookup("installed.loading"), Spin());
@@ -1757,7 +2749,22 @@ function InstalledTab({ notify, installed, onMutation }) {
1757
2749
  null,
1758
2750
  h("div", { className: "dshm-hint" }, lookup("profile.hint", { path: data.profileDir })),
1759
2751
  data.others > 0 ? h("div", { className: "dshm-others" }, lookup("installed.others", { n: data.others })) : null,
1760
- busyPkg ? h(ProgressLine, { key: "prog" }) : null,
2752
+ items.some((x) => x && x.outdated) ? h(
2753
+ "div",
2754
+ { className: "dsvm-sortrow" },
2755
+ h(
2756
+ "button",
2757
+ {
2758
+ className: "dshm-btn primary sm",
2759
+ disabled: activeMutateTarget != null,
2760
+ onClick: () => {
2761
+ for (const it of items.filter((x) => x && x.outdated)) void doUpgrade(it);
2762
+ }
2763
+ },
2764
+ lookup("installed.upgradeAll", { n: items.filter((x) => x && x.outdated).length })
2765
+ )
2766
+ ) : null,
2767
+ activeMutateTarget ? h(ProgressLine, { key: "prog" }) : null,
1761
2768
  h(
1762
2769
  "div",
1763
2770
  { className: "dshm-cards" },
@@ -1770,7 +2777,7 @@ function InstalledTab({ notify, installed, onMutation }) {
1770
2777
  name: it.name,
1771
2778
  topRight: h(ToggleSwitch, {
1772
2779
  vm: tvm,
1773
- disabled: busyPkg === it.pkg,
2780
+ disabled: activeMutateTarget === it.pkg,
1774
2781
  onChange: (enabled) => doToggle(it, enabled)
1775
2782
  }),
1776
2783
  badges: [
@@ -1814,20 +2821,20 @@ function InstalledTab({ notify, installed, onMutation }) {
1814
2821
  {
1815
2822
  key: "up",
1816
2823
  className: "dshm-btn primary sm",
1817
- disabled: busyPkg === it.pkg,
2824
+ disabled: activeMutateTarget === it.pkg,
1818
2825
  onClick: (e) => {
1819
2826
  e.stopPropagation();
1820
2827
  doUpgrade(it);
1821
2828
  }
1822
2829
  },
1823
- busyPkg === it.pkg ? h(Spin) : lookup("action.upgrade")
2830
+ activeMutateTarget === it.pkg ? h(Spin) : lookup("action.upgrade")
1824
2831
  ) : null,
1825
2832
  h(TwoStepButton, {
1826
2833
  key: "un",
1827
2834
  label: lookup("action.uninstall"),
1828
2835
  confirmLabel: lookup(vm.guard.confirmKey),
1829
2836
  className: "dshm-btn sm",
1830
- disabled: busyPkg === it.pkg,
2837
+ disabled: activeMutateTarget === it.pkg,
1831
2838
  onConfirm: () => doUninstall(it)
1832
2839
  })
1833
2840
  ]
@@ -1843,25 +2850,23 @@ function regSourceLabel(data) {
1843
2850
  function configStatusLabel(status) {
1844
2851
  return lookup(`settings.status.${status || "loading"}` || "settings.status.loading");
1845
2852
  }
1846
- var STATUS_BADGE = { ready: "", pending: "info", rejected: "warn", unavailable: "err", loading: "info" };
1847
2853
  function SettingsTab({ notify, onRegistryChanged }) {
1848
2854
  const reg = useAsync((force) => api("registry", force ? { force: true } : {}), []);
1849
2855
  const cfgState = useAsync(() => api("registry-config"), []);
1850
- const self = useAsync(() => api("self-check"), []);
1851
2856
  const [busy, setBusy] = useState(false);
1852
- const [upgrading, setUpgrading] = useState(false);
1853
2857
  const [draftAddress, setDraftAddress] = useState(null);
1854
2858
  const [applying, setApplying] = useState(false);
1855
2859
  const [downloading, setDownloading] = useState(false);
1856
- const [diagnosing, setDiagnosing] = useState(false);
1857
2860
  const [applyError, setApplyError] = useState(null);
1858
- const [diagnosticResult, setDiagnosticResult] = useState(null);
1859
- const diagnoseAbort = useRef(null);
2861
+ const [communityOn, setCommunityOn] = useState(null);
2862
+ const [communityBusy, setCommunityBusy] = useState(false);
1860
2863
  const cfgData = cfgState.data;
1861
2864
  useEffect(() => {
1862
2865
  if (draftAddress === null && cfgData) setDraftAddress(cfgData.registryUrl ?? "");
1863
2866
  }, [cfgData, draftAddress]);
1864
- useEffect(() => () => diagnoseAbort.current?.abort(), []);
2867
+ useEffect(() => {
2868
+ if (communityOn === null && cfgData) setCommunityOn(cfgData.communityCatalog === true);
2869
+ }, [cfgData, communityOn]);
1865
2870
  const refresh = async () => {
1866
2871
  setBusy(true);
1867
2872
  try {
@@ -1912,44 +2917,71 @@ function SettingsTab({ notify, onRegistryChanged }) {
1912
2917
  setDownloading(false);
1913
2918
  }
1914
2919
  };
1915
- const runDiagnose = async () => {
1916
- diagnoseAbort.current?.abort();
1917
- const ac = new AbortController();
1918
- diagnoseAbort.current = ac;
1919
- setDiagnosing(true);
1920
- setDiagnosticResult(null);
2920
+ const snap = cfgData || {};
2921
+ const state = snap.registryState || (reg.data ? reg.data.registryState : null) || null;
2922
+ const communitySummary = reg.data && reg.data.community && typeof reg.data.community === "object" ? reg.data.community : null;
2923
+ const applyCommunity = async (enabled) => {
2924
+ const prev = communityOn;
2925
+ setCommunityOn(enabled);
2926
+ setCommunityBusy(true);
1921
2927
  try {
1922
- const res = await api("registry-diagnose", {}, ac.signal);
1923
- if (!ac.signal.aborted) setDiagnosticResult(res.check);
2928
+ await api("set-community", { enabled });
2929
+ await reloadRegistryState();
2930
+ await (onRegistryChanged ? onRegistryChanged() : reg.reload(false));
2931
+ notify({ kind: "ok", text: lookup(enabled ? "settings.community.on.ok" : "settings.community.off.ok"), needsRestart: false });
1924
2932
  } catch (e) {
1925
- if (!ac.signal.aborted) notify({ kind: "err", text: lookup("settings.diagnose.failed", { err: e && e.message || e }) });
2933
+ setCommunityOn(prev);
2934
+ notify({ kind: "err", text: lookup("settings.community.toggle.failed", { err: e && e.message || e }) });
1926
2935
  } finally {
1927
- if (!ac.signal.aborted) setDiagnosing(false);
2936
+ setCommunityBusy(false);
1928
2937
  }
1929
2938
  };
1930
- const snap = cfgData || {};
1931
- const state = snap.registryState || (reg.data ? reg.data.registryState : null) || null;
1932
- const communitySummary = reg.data && reg.data.community && typeof reg.data.community === "object" ? reg.data.community : null;
1933
- const communityRows = communitySummary && (communitySummary.status === "ready" || communitySummary.status === "stale") ? [
1934
- [lookup("settings.community.status"), communitySummary.status === "stale" ? lookup("community.stale") : lookup("settings.status.ready")],
1935
- [lookup("settings.community.version"), communitySummary.version || "\u2014"],
1936
- [lookup("settings.community.route"), communitySummary.route || "\u2014"],
1937
- [lookup("settings.community.accepted"), `${communitySummary.acceptedCount} / ${communitySummary.upstreamCount ?? "\u2014"}`],
1938
- [lookup("settings.community.displaced"), String(communitySummary.displaced ?? 0)]
1939
- ] : [[lookup("settings.community.status"), lookup("settings.community.none")]];
2939
+ const communityLive = communitySummary && (communitySummary.status === "ready" || communitySummary.status === "stale");
2940
+ const communityStatusBadge = !communityLive ? { cls: "err", label: lookup("settings.status.unavailable") } : communitySummary.status === "stale" ? { cls: "warn", label: lookup("settings.community.status.stale") } : { cls: "ok", label: lookup("settings.status.ready") };
1940
2941
  return h(
1941
2942
  React.Fragment,
1942
2943
  null,
1943
- Section(
1944
- lookup("settings.community"),
1945
- ...communityRows.map(([k, v]) => h(
2944
+ // ① 社区清单(from awesome-dsh-plugin):标题行右侧开关;开态紧凑 kv,关态单行说明
2945
+ h(
2946
+ "div",
2947
+ { className: "dshm-section" },
2948
+ h(
1946
2949
  "div",
1947
- { key: k, className: "dshm-row", style: { justifyContent: "space-between" } },
1948
- h("span", { className: "dshm-hint" }, k),
1949
- h("span", null, v)
1950
- )),
1951
- communitySummary && Array.isArray(communitySummary.errors) && communitySummary.errors.length ? h("div", { className: "dshm-err" }, communitySummary.errors.join("\uFF1B")) : null
2950
+ { className: "dshm-section-title" },
2951
+ lookup("settings.community"),
2952
+ h("span", { className: "dshm-spacer" }),
2953
+ h(
2954
+ "label",
2955
+ {
2956
+ className: `dshm-switch${communityOn ? " on" : ""}${communityBusy || communityOn === null ? " locked" : ""}`,
2957
+ title: lookup("settings.community.toggle")
2958
+ },
2959
+ h("input", {
2960
+ type: "checkbox",
2961
+ checked: communityOn === true,
2962
+ disabled: communityBusy || communityOn === null,
2963
+ onChange: (e) => applyCommunity(e.target.checked)
2964
+ }),
2965
+ h("span", { className: "dshm-switch-slider" })
2966
+ )
2967
+ ),
2968
+ communityOn === false ? h("div", { className: "dshm-hint" }, lookup("settings.community.off")) : h(
2969
+ "div",
2970
+ { className: "dshm-kv" },
2971
+ h("span", { className: "k" }, lookup("settings.community.status")),
2972
+ h("span", null, h("span", { className: `dshm-badge ${communityStatusBadge.cls}` }, communityStatusBadge.label)),
2973
+ h("span", { className: "k" }, lookup("settings.community.version")),
2974
+ h("span", null, communityLive ? communitySummary.version || "\u2014" : "\u2014"),
2975
+ h("span", { className: "k" }, lookup("settings.community.route")),
2976
+ h("span", null, communityLive ? communitySummary.route || "\u2014" : "\u2014"),
2977
+ h("span", { className: "k" }, lookup("settings.community.count")),
2978
+ h("span", null, communityLive ? lookup("settings.community.count.v", { n: communitySummary.acceptedCount ?? 0, up: communitySummary.upstreamCount ?? "\u2014" }) : "\u2014"),
2979
+ h("span", { className: "k" }, lookup("settings.community.displaced")),
2980
+ h("span", null, communityLive ? lookup("settings.community.displaced.v", { n: communitySummary.displaced ?? 0 }) : "\u2014")
2981
+ ),
2982
+ communitySummary && Array.isArray(communitySummary.errors) && communitySummary.errors.length && communityOn !== false ? h("div", { className: "dshm-err" }, communitySummary.errors.join("\uFF1B")) : null
1952
2983
  ),
2984
+ // ② 精选清单(registry.json):自定义源整体替换主清单,仅影响市场「精选」区
1953
2985
  Section(
1954
2986
  lookup("settings.registry"),
1955
2987
  h(
@@ -1962,14 +2994,16 @@ function SettingsTab({ notify, onRegistryChanged }) {
1962
2994
  onChange: (e) => setDraftAddress(e.target.value),
1963
2995
  spellcheck: "false"
1964
2996
  }),
1965
- h("div", { className: "dshm-note" }, lookup("settings.address.hint"))
2997
+ h("div", { className: "dshm-note" }, lookup("settings.note.custom"))
1966
2998
  ),
1967
2999
  h(
1968
3000
  "div",
1969
3001
  { className: "dshm-actions" },
3002
+ h("button", { className: "dshm-btn sm", disabled: busy || reg.loading, onClick: refresh }, busy || reg.loading ? h(Spin) : lookup("settings.force")),
1970
3003
  h("button", {
1971
3004
  className: "dshm-btn primary sm",
1972
- disabled: applying || diagnosing || draftAddress === null,
3005
+ // 空草稿时禁用:回到默认只走「恢复默认」一条路,避免与右侧按钮双通道等价
3006
+ disabled: applying || draftAddress === null || (draftAddress ?? "").trim() === "",
1973
3007
  onClick: () => applyAddress(draftAddress ?? "")
1974
3008
  }, applying ? h("span", null, lookup("settings.apply.applying"), " ", h(Spin)) : lookup("settings.apply")),
1975
3009
  h("button", {
@@ -1980,101 +3014,48 @@ function SettingsTab({ notify, onRegistryChanged }) {
1980
3014
  h("button", {
1981
3015
  className: "dshm-btn sm",
1982
3016
  disabled: downloading,
3017
+ title: lookup("settings.download.title"),
1983
3018
  onClick: downloadDefault
1984
- }, downloading ? h("span", null, lookup("settings.download.downloading"), " ", h(Spin)) : lookup("settings.download")),
1985
- h("button", {
1986
- className: "dshm-btn sm",
1987
- disabled: diagnosing || applying,
1988
- onClick: runDiagnose
1989
- }, diagnosing ? h("span", null, lookup("settings.diagnose.running"), " ", h(Spin)) : lookup("settings.diagnose")),
1990
- h("button", { className: "dshm-btn sm", disabled: busy || reg.loading, onClick: refresh }, busy || reg.loading ? h(Spin) : lookup("settings.force"))
3019
+ }, downloading ? h("span", null, lookup("settings.download.downloading"), " ", h(Spin)) : lookup("settings.download"))
1991
3020
  ),
1992
3021
  applyError ? h("div", { className: "dshm-err" }, lookup("settings.apply.failed", { err: applyError })) : null,
3022
+ // 配置状态仅在异常态出现(ready 是无信息量的常态,不再常驻)
3023
+ snap.configStatus && snap.configStatus !== "ready" ? h(
3024
+ "div",
3025
+ { className: snap.configStatus === "pending" ? "dshm-note warn" : "dshm-err" },
3026
+ `${lookup("settings.status.label")}\uFF1A${configStatusLabel(snap.configStatus)}${snap.configErrors && snap.configErrors.length ? `\uFF08${snap.configErrors.slice(0, 2).join("\uFF1B")}\uFF09` : ""}`
3027
+ ) : null,
1993
3028
  h(
1994
3029
  "div",
1995
3030
  { className: "dshm-kv", style: { marginTop: "4px" } },
1996
3031
  h("span", { className: "k" }, lookup("settings.configured")),
1997
- h("span", null, snap.registryUrl ? h("span", { className: "dshm-code" }, snap.registryUrl) : h("span", { className: "dshm-hint" }, "\uFF08\u9ED8\u8BA4\uFF09")),
1998
- h("span", { className: "k" }, lookup("settings.activecfg")),
1999
- h("span", null, snap.activeConfigAddress ? h("span", { className: "dshm-code" }, snap.activeConfigAddress) : h("span", { className: "dshm-hint" }, "\uFF08\u9ED8\u8BA4\uFF09")),
2000
- h("span", { className: "k" }, lookup("settings.status.label")),
2001
- h("span", null, h("span", { className: `dshm-badge ${STATUS_BADGE[snap.configStatus] || ""}` }, configStatusLabel(snap.configStatus))),
3032
+ h("span", null, snap.registryUrl ? h("span", { className: "dshm-code" }, snap.registryUrl) : h("span", { className: "dshm-hint" }, lookup("settings.address.default"))),
2002
3033
  h("span", { className: "k" }, lookup("settings.effective")),
2003
3034
  h("span", null, regSourceLabel(state), state && !state.isDefault ? h("span", { className: "dshm-badge info", style: { marginLeft: "6px" } }, lookup("badge.custom")) : null),
2004
3035
  h("span", { className: "k" }, lookup("settings.updated")),
2005
3036
  h("span", null, fmtDate(state && state.fetchedAt)),
2006
3037
  h("span", { className: "k" }, lookup("settings.count")),
2007
- h("span", null, state ? lookup("settings.count.v", { n: state.count ?? 0 }) : "\u2014"),
2008
- h("span", { className: "k" }, lookup("settings.policy")),
2009
- h("span", null, lookup("settings.policy.v"))
3038
+ h("span", null, state ? lookup("settings.count.v", { n: state.count ?? 0 }) : "\u2014")
2010
3039
  ),
2011
- state && state.stale && state.status !== "unavailable" ? h("div", { className: "dshm-note" }, lookup("settings.cache.hint")) : null,
2012
3040
  state && !state.isDefault ? h("div", { className: "dshm-note warn" }, lookup("settings.trust.hint")) : null,
2013
3041
  snap.warnings && snap.warnings.length ? h("div", { className: "dshm-note warn" }, `${lookup("settings.warnings")}\uFF1A${snap.warnings.join("\uFF1B")}`) : null,
2014
- state && state.errors && state.errors.length ? h("div", { className: "dshm-err" }, `${lookup("settings.remotehint")}\uFF1A${state.errors.slice(0, 5).join("\uFF1B")}`) : null,
2015
- diagnosticResult ? h(
2016
- "div",
2017
- { className: "dshm-hint", style: { wordBreak: "break-all" } },
2018
- lookup("settings.diagnose.result", {
2019
- checked: diagnosticResult.checked ?? 0,
2020
- passed: diagnosticResult.passed ?? 0,
2021
- failed: diagnosticResult.failed ?? 0,
2022
- trunc: diagnosticResult.truncated ? lookup("settings.diagnose.truncated") : ""
2023
- }),
2024
- diagnosticResult.issues && diagnosticResult.issues.length ? h(
2025
- "div",
2026
- { style: { marginTop: "4px" } },
2027
- diagnosticResult.issues.slice(0, 100).map((iss, i) => h("div", { key: i, className: "dshm-err" }, `\xB7 [${iss.id}] ${iss.field}: ${iss.message}`))
2028
- ) : h("div", { className: "dshm-note" }, lookup("settings.diagnose.none"))
2029
- ) : null
3042
+ state && state.errors && state.errors.length ? h("div", { className: "dshm-err" }, `${lookup("settings.remotehint")}\uFF1A${state.errors.slice(0, 5).join("\uFF1B")}`) : null
2030
3043
  ),
3044
+ // ③ 关于:当前定位 + 仓库/反馈链接(dsh-m 自身卡已按主人要求移除——升级走市场卡片/CLI,self-check/self-upgrade API 保留)
2031
3045
  Section(
2032
- lookup("settings.self"),
3046
+ lookup("settings.about"),
3047
+ h("div", { className: "dshm-hint" }, lookup("settings.about.text")),
2033
3048
  h(
2034
3049
  "div",
2035
- { className: "dshm-kv" },
2036
- h("span", { className: "k" }, lookup("settings.current")),
2037
- h("span", null, self.data ? `v${self.data.current}` : "\u2014"),
2038
- h("span", { className: "k" }, lookup("settings.npmlatest")),
2039
- h("span", null, self.data ? self.data.latest ? `v${self.data.latest}` : lookup("settings.lookupfailed", { err: self.data.error || "" }) : "\u2026")
2040
- ),
2041
- self.data && self.data.outdated ? h(
2042
- "div",
2043
- { className: "dshm-actions" },
2044
- h("button", { className: "dshm-btn primary sm", disabled: upgrading, onClick: upgradeSelf }, upgrading ? h(Spin) : lookup("settings.upgradeself")),
2045
- h("span", { className: "dshm-hint" }, lookup("settings.upgradehint"))
2046
- ) : null
2047
- ),
2048
- Section(
2049
- lookup("settings.about"),
2050
- h("div", { className: "dshm-hint" }, lookup("settings.about.text"))
3050
+ { className: "dshm-links" },
3051
+ h(ExtLink, { href: "https://github.com/iasiv5/dsh-m" }, "GitHub"),
3052
+ h("span", { className: "dshm-links-sep" }, "\xB7"),
3053
+ h(ExtLink, { href: "https://github.com/iasiv5/dsh-m/issues" }, lookup("settings.about.issues")),
3054
+ h("span", { className: "dshm-links-sep" }, "\xB7"),
3055
+ h(ExtLink, { href: "https://www.npmjs.com/package/dsh-m" }, lookup("settings.about.npm"))
3056
+ )
2051
3057
  )
2052
3058
  );
2053
- async function upgradeSelf() {
2054
- setUpgrading(true);
2055
- try {
2056
- const res = await api("self-upgrade");
2057
- notify({ kind: "ok", text: lookup("self.upgraded", { v: res.version }), needsRestart: true });
2058
- await self.reload();
2059
- } catch (e) {
2060
- if (e && e.guard) {
2061
- notify({
2062
- kind: "err",
2063
- text: [
2064
- lookup("guard.blocked"),
2065
- `${lookup("guard.compstatus")}: ${e.guard.compensation?.status || "\u2014"}\uFF08${e.guard.compensation?.note || ""}\uFF09`,
2066
- e.guard.repairBasis ? `${lookup("guard.repairbasis")}: ${e.guard.repairBasis}` : null,
2067
- lookup("guard.noforce"),
2068
- e.guard.restartSafe ? lookup("guard.restartsafenow") : lookup("guard.restartunsafe")
2069
- ].filter(Boolean).join(" | ")
2070
- });
2071
- } else {
2072
- notify({ kind: "err", text: lookup("failed.selfupdate", { err: e && e.message || e }) });
2073
- }
2074
- } finally {
2075
- setUpgrading(false);
2076
- }
2077
- }
2078
3059
  }
2079
3060
  function Section(title, sub, ...children) {
2080
3061
  if (sub != null && typeof sub !== "string") {
@@ -2104,7 +3085,7 @@ function DetailRows(rows) {
2104
3085
  ])
2105
3086
  );
2106
3087
  }
2107
- function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions, topRight }) {
3088
+ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, actions, topRight, byline, clampLines }) {
2108
3089
  return h(
2109
3090
  "div",
2110
3091
  {
@@ -2121,7 +3102,8 @@ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, ac
2121
3102
  "div",
2122
3103
  { className: "dshm-meta" },
2123
3104
  h("div", { className: "dshm-top" }, h("span", { className: "dshm-name" }, name), ...badges.filter(Boolean), topRight || null),
2124
- h("div", { className: "dshm-desc", style: open ? { WebkitLineClamp: "unset" } : null }, desc),
3105
+ byline && byline.length ? h("div", { className: "dsvm-byline" }, ...byline.map((b, i) => h("span", { key: i, title: b.title || void 0 }, b.text))) : null,
3106
+ h("div", { className: "dshm-desc", style: open ? { WebkitLineClamp: "unset" } : clampLines ? { WebkitLineClamp: String(clampLines) } : null }, desc),
2125
3107
  sub ? h("div", { className: "dshm-sub" }, sub) : null,
2126
3108
  links || null,
2127
3109
  open ? h("div", { className: "dshm-detail" }, detail) : null,
@@ -2129,63 +3111,305 @@ function Card({ icon, name, badges, desc, sub, links, open, onToggle, detail, ac
2129
3111
  )
2130
3112
  );
2131
3113
  }
3114
+ function snapshotOf(it) {
3115
+ const s = {
3116
+ id: it.id,
3117
+ name: it.name,
3118
+ description: it.description,
3119
+ category: it.category,
3120
+ categoryLabel: it.categoryLabel,
3121
+ source: it.source
3122
+ };
3123
+ for (const k of ["descriptionEn", "npm", "github", "homepage", "owner", "downloads", "stars", "added", "deprecated"]) {
3124
+ if (it[k] !== void 0 && it[k] !== null) s[k] = it[k];
3125
+ }
3126
+ return s;
3127
+ }
3128
+ function FavoriteZone({ favorites, onOpen }) {
3129
+ const list = favorites.list;
3130
+ const [check, setCheck] = useState(null);
3131
+ useEffect(() => {
3132
+ let live = true;
3133
+ setCheck(null);
3134
+ if (!list.length) return;
3135
+ const timer = setTimeout(() => {
3136
+ void (async () => {
3137
+ const lookupLive = async (fav) => {
3138
+ try {
3139
+ const res = await api("market", { query: fav.id, source: "all", limit: 8 });
3140
+ const items = res && Array.isArray(res.items) ? res.items : [];
3141
+ return items.some((x) => x && x.id === fav.id);
3142
+ } catch {
3143
+ return false;
3144
+ }
3145
+ };
3146
+ const { stale } = await partitionStale2(list, lookupLive);
3147
+ if (live) setCheck({ staleIds: stale.map((f) => f.id) });
3148
+ })();
3149
+ }, 800);
3150
+ return () => {
3151
+ live = false;
3152
+ clearTimeout(timer);
3153
+ };
3154
+ }, [list]);
3155
+ if (!list.length) {
3156
+ return h("div", { className: "dshm-empty" }, lookup("favorites.hint"));
3157
+ }
3158
+ const staleSet = new Set(check && check.staleIds || []);
3159
+ return h(
3160
+ React.Fragment,
3161
+ null,
3162
+ check && staleSet.size ? h(
3163
+ "div",
3164
+ { className: "dshm-hint" },
3165
+ lookup("favorites.stale", { n: staleSet.size }),
3166
+ " ",
3167
+ h("button", { className: "dshm-btn sm", onClick: () => favorites.removeIds([...staleSet]) }, lookup("favorites.clean"))
3168
+ ) : null,
3169
+ !check ? h("div", { className: "dshm-hint" }, lookup("favorites.checking")) : null,
3170
+ h(
3171
+ "div",
3172
+ { className: "dshm-cards" },
3173
+ ...list.map((fav) => {
3174
+ const s = fav.snapshot;
3175
+ const isStale = staleSet.has(fav.id);
3176
+ return Card({
3177
+ key: fav.id,
3178
+ icon: h(Icon, { entry: s }),
3179
+ name: s.name,
3180
+ badges: [
3181
+ s.deprecated === true ? h("span", { className: "dshm-badge warn", key: "dep" }, lookup("badge.deprecated")) : null,
3182
+ s.owner ? h("span", { className: "dshm-badge info", key: "c" }, lookup("badge.community")) : null,
3183
+ h("span", { className: "dshm-badge info", key: "s" }, s.source === "npm" ? "npm" : "github"),
3184
+ isStale ? h("span", { className: "dshm-badge warn", key: "st" }, lookup("favorites.stalebadge")) : null
3185
+ ],
3186
+ byline: s.owner ? [
3187
+ s.owner ? { text: `by ${s.owner}` } : null,
3188
+ typeof s.downloads === "number" ? { text: `${compactCount(s.downloads)} \u2193`, title: String(s.downloads) } : null,
3189
+ typeof s.stars === "number" ? { text: `${compactCount(s.stars)} \u2605`, title: String(s.stars) } : null
3190
+ ].filter(Boolean) : null,
3191
+ desc: s.description,
3192
+ clampLines: 5,
3193
+ links: h(LinksRow, { npm: s.npm, github: s.github, homepage: s.homepage }),
3194
+ // 0.7.2 修复:收藏卡点击开详情(此前无 onToggle,点击无响应);
3195
+ // 解析在 onOpen 内做(内存/API/快照三级),★ 移除按钮 stopPropagation 不受影响
3196
+ onToggle: () => {
3197
+ if (onOpen) onOpen(fav);
3198
+ },
3199
+ topRight: h("button", {
3200
+ className: "dsvm-favbtn on",
3201
+ title: lookup("fav.remove"),
3202
+ onClick: (e) => {
3203
+ e.stopPropagation();
3204
+ favorites.toggle(s);
3205
+ }
3206
+ }, "\u2605")
3207
+ });
3208
+ })
3209
+ )
3210
+ );
3211
+ }
3212
+ var opsStore = createOperationsStore2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
3213
+ var opsPumpCtx = null;
3214
+ var opsPump = createOpsPump2(opsStore, () => opsPumpCtx);
3215
+ var OP_STATUS_CLS = {
3216
+ queued: "",
3217
+ running: "run",
3218
+ input: "warn",
3219
+ done: "ok",
3220
+ warned: "warn",
3221
+ failed: "err",
3222
+ superseded: "sup"
3223
+ };
3224
+ function OperationsPanel({ records, onClearFinished, onRemove }) {
3225
+ const active = records.filter((r) => r.status === "queued" || r.status === "running" || r.status === "input");
3226
+ const finished = records.filter((r) => active.indexOf(r) < 0).slice(-6);
3227
+ if (!active.length && !finished.length) return null;
3228
+ const row = (r) => h(
3229
+ "div",
3230
+ { key: r.id, className: `dsvm-oprow ${OP_STATUS_CLS[r.status] || ""}` },
3231
+ h("span", { className: "dsvm-opstatus" }, lookup("op.status." + r.status)),
3232
+ h("span", { className: "dsvm-opkind" }, lookup("op.kind." + r.kind)),
3233
+ h("span", { className: "dsvm-optarget", title: r.error || r.warning || void 0 }, r.target),
3234
+ r.status === "running" ? Spin() : null,
3235
+ r.error ? h("span", { className: "dsvm-opnote" }, r.error) : null,
3236
+ r.warning ? h("span", { className: "dsvm-opnote" }, r.warning) : null,
3237
+ // 单条移除(终审·新伤2):input/failed/superseded 行给出口(对话区来源的 input 不再永挂)
3238
+ (r.status === "input" || r.status === "failed" || r.status === "superseded") && onRemove ? h("button", { className: "dshm-xbtn", title: lookup("op.remove"), onClick: () => onRemove(r.id) }, h(XIcon)) : null
3239
+ );
3240
+ return h(
3241
+ "div",
3242
+ { className: "dsvm-ops" },
3243
+ active.length ? h("div", { className: "dsvm-opgroup" }, ...active.map(row)) : null,
3244
+ finished.length ? h(
3245
+ "div",
3246
+ { className: "dsvm-opgroup done" },
3247
+ ...finished.map(row),
3248
+ h("button", { className: "dshm-btn sm", onClick: onClearFinished }, lookup("op.clear"))
3249
+ ) : null
3250
+ );
3251
+ }
2132
3252
  var TABS = [
2133
3253
  ["market", "tab.market", "market"],
2134
3254
  ["installed", "tab.installed", "installed"],
2135
3255
  ["settings", "tab.settings", null]
2136
3256
  ];
2137
- function DshVersionChip({ version }) {
2138
- const [copied, setCopied] = useState(false);
2139
- const copy = async () => {
2140
- try {
2141
- await navigator.clipboard.writeText(version);
2142
- setCopied(true);
2143
- setTimeout(() => setCopied(false), 1600);
2144
- } catch {
2145
- }
2146
- };
3257
+ function DshmVersionChip({ version }) {
2147
3258
  return h(
2148
- "button",
2149
- { type: "button", className: "dshm-dshchip", onClick: copy, "aria-label": `DSH ${version}` },
2150
- "DSH ",
2151
- h("span", { className: "dshm-dshchip-v" }, version),
2152
- h(
2153
- "span",
2154
- { className: "dshm-dshchip-tip", role: "tooltip" },
2155
- h("span", { className: "dshm-dshchip-tiprow" }, `DSH ${version}`),
2156
- h("span", { className: "dshm-dshchip-tipsub" }, lookup(copied ? "dsh.chip.copied" : "dsh.chip.copyhint"))
2157
- )
3259
+ "span",
3260
+ { className: "dshm-dshchip", title: `dsh-m v${version}` },
3261
+ "dsh-m ",
3262
+ h("span", { className: "dshm-dshchip-v" }, `v${version}`)
3263
+ );
3264
+ }
3265
+ function XIcon() {
3266
+ return h(
3267
+ "svg",
3268
+ { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", "aria-hidden": "true" },
3269
+ h("path", { d: "M6 6l12 12M18 6L6 18", stroke: "currentColor", strokeWidth: "2.2", strokeLinecap: "round" })
3270
+ );
3271
+ }
3272
+ function FsIcon({ full }) {
3273
+ return full ? h(
3274
+ "svg",
3275
+ { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", "aria-hidden": "true" },
3276
+ h("path", { d: "M16 8V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v10a1 1 0 0 0 1 1h3", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }),
3277
+ h("rect", { x: "8", y: "8", width: "12", height: "12", rx: "1", stroke: "currentColor", strokeWidth: "2" })
3278
+ ) : h(
3279
+ "svg",
3280
+ { viewBox: "0 0 24 24", width: "12", height: "12", fill: "none", "aria-hidden": "true" },
3281
+ h("path", { d: "M9 4H4v5M15 4h5v5M9 20H4v-5M15 20h5v-5", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" })
2158
3282
  );
2159
3283
  }
2160
3284
  function MarketPanel({ onClose }) {
2161
3285
  const [tab, setTab] = useState("market");
2162
- const market = useMarketData();
3286
+ const FS_KEY = "dshm-panel-fullscreen";
3287
+ const [full, setFull] = useState(() => {
3288
+ try {
3289
+ return typeof window !== "undefined" && window.localStorage ? window.localStorage.getItem(FS_KEY) === "1" : false;
3290
+ } catch {
3291
+ return false;
3292
+ }
3293
+ });
3294
+ const toggleFull = useCallback(() => {
3295
+ setFull((v) => {
3296
+ const next = !v;
3297
+ try {
3298
+ if (typeof window !== "undefined" && window.localStorage) window.localStorage.setItem(FS_KEY, next ? "1" : "0");
3299
+ } catch {
3300
+ }
3301
+ return next;
3302
+ });
3303
+ }, []);
3304
+ const marketCommunity = useMarketData("community");
3305
+ const marketPrimary = useMarketData("primary");
3306
+ const markets = { community: marketCommunity, primary: marketPrimary };
2163
3307
  const installed = useAsync(() => api("installed"), []);
2164
- const [dshVersion, setDshVersion] = useState(null);
3308
+ const [opRecords, setOpRecords] = useState(() => opsStore.list().map((r) => ({ ...r })));
3309
+ const syncOps = useCallback(() => setOpRecords(opsStore.list().map((r) => ({ ...r }))), []);
3310
+ const runOp = useCallback(
3311
+ (kind, target, exec, meta = {}, reuseId) => (
3312
+ // session:true = 活会话标记(终审·新伤1):重挂载的 restore 跳过(泵仍拥有);persist 时剥离。
3313
+ // 入队与 waiters/原始错误保真都在 createOpsPump(评审 P4:实现与测试同源)。
3314
+ opsPump.enqueue({ ...reuseId ? { id: reuseId } : {}, kind, target, status: "queued", meta: { ...meta, session: true } }, exec)
3315
+ ),
3316
+ []
3317
+ );
3318
+ const disposeOp = useCallback((id, note) => {
3319
+ opsStore.upsert({ id, status: "superseded", error: note });
3320
+ syncOps();
3321
+ }, [syncOps]);
3322
+ const ops = { records: opRecords, runOp, dispose: disposeOp };
3323
+ const favStoreRef = useRef(null);
3324
+ if (!favStoreRef.current) {
3325
+ favStoreRef.current = createFavoritesStore2(typeof window !== "undefined" && window.localStorage ? window.localStorage : null);
3326
+ }
3327
+ const favStore = favStoreRef.current;
3328
+ const [favList, setFavList] = useState(() => favStore.list());
3329
+ const favorites = {
3330
+ list: favList,
3331
+ toggle: useCallback((snapshot) => setFavList(favStore.toggle(snapshot)), [favStore]),
3332
+ removeIds: useCallback((ids) => setFavList(favStore.removeIds(ids)), [favStore])
3333
+ };
3334
+ const [pluginVersion, setPluginVersion] = useState(null);
2165
3335
  useEffect(() => {
2166
3336
  let live = true;
2167
3337
  api("ping").then((r) => {
2168
- if (live) setDshVersion(typeof r?.dshVersion === "string" && r.dshVersion ? r.dshVersion : null);
3338
+ if (live) setPluginVersion(typeof r?.version === "string" && r.version ? r.version : null);
2169
3339
  }).catch(() => {
2170
3340
  });
2171
3341
  return () => {
2172
3342
  live = false;
2173
3343
  };
2174
3344
  }, []);
3345
+ const marketReloadAll = useCallback(
3346
+ (force) => {
3347
+ marketCommunity.reload(force);
3348
+ marketPrimary.reload(force);
3349
+ },
3350
+ [marketCommunity.reload, marketPrimary.reload]
3351
+ );
2175
3352
  const refreshViews = useCallback(
2176
- () => refreshAfterMutation2({ marketReload: market.reload, installedReload: installed.reload }),
2177
- [market.reload, installed.reload]
3353
+ () => refreshAfterMutation2({ marketReload: marketReloadAll, installedReload: installed.reload }),
3354
+ [marketReloadAll, installed.reload]
2178
3355
  );
2179
3356
  const onRegistryChanged = refreshViews;
3357
+ const stillApplies = useCallback(async (r) => {
3358
+ let fresh = null;
3359
+ try {
3360
+ fresh = await api("installed");
3361
+ } catch {
3362
+ fresh = null;
3363
+ }
3364
+ const items = fresh && Array.isArray(fresh.items) ? fresh.items : null;
3365
+ if (!items) return true;
3366
+ return opAppliesTo2(r, items);
3367
+ }, []);
3368
+ const dispatchRestored = useCallback(async (r) => {
3369
+ try {
3370
+ if (r.kind === "install") {
3371
+ await api("install", { id: r.target, ...r.meta && r.meta.version ? { version: r.meta.version } : {} });
3372
+ } else if (r.kind === "upgrade") {
3373
+ await api("upgrade", { pkg: r.target, ...r.meta && r.meta.force ? { force: true } : {} });
3374
+ } else if (r.kind === "uninstall") {
3375
+ await api("uninstall", { pkg: r.target });
3376
+ } else {
3377
+ await api("set-enabled", { pkg: r.target, enabled: r.meta && r.meta.on === true });
3378
+ }
3379
+ return { ok: true };
3380
+ } catch (e) {
3381
+ return { ok: false, error: String(e && e.message || e), ...e && e.issue ? { issue: e.issue } : {} };
3382
+ }
3383
+ }, []);
3384
+ useEffect(() => {
3385
+ opsPumpCtx = { stillApplies, dispatchRestored, syncOps, refreshViews };
3386
+ return () => {
3387
+ if (opsPumpCtx && opsPumpCtx.syncOps === syncOps) opsPumpCtx = null;
3388
+ };
3389
+ }, [stillApplies, dispatchRestored, syncOps, refreshViews]);
3390
+ const restoredRef = useRef(false);
3391
+ useEffect(() => {
3392
+ if (restoredRef.current || !installed.data) return;
3393
+ restoredRef.current = true;
3394
+ void (async () => {
3395
+ const restored = await restoreRecords2(opsStore.list(), stillApplies);
3396
+ opsStore.replaceAll(restored);
3397
+ syncOps();
3398
+ if (restored.some((r) => r.status === "queued")) {
3399
+ void opsPump.kick();
3400
+ }
3401
+ })();
3402
+ }, [installed.data, stillApplies, syncOps]);
2180
3403
  const counts = {
2181
- market: market.data ? market.data.total : null,
3404
+ market: marketCommunity.data ? marketCommunity.data.total : null,
2182
3405
  installed: installed.data ? installed.data.items.length : null
2183
3406
  };
3407
+ const outdatedCount = installed.data && Array.isArray(installed.data.items) ? installed.data.items.filter((x) => x && x.outdated).length : 0;
2184
3408
  const [banner, setBanner] = useState(null);
2185
3409
  const [toast, setToast] = useState(null);
2186
3410
  useEffect(() => {
2187
3411
  const onKey = (e) => {
2188
- if (e.key === "Escape" && onClose) onClose();
3412
+ if (e.key === "Escape" && dsvmModalDepth === 0 && onClose) onClose();
2189
3413
  };
2190
3414
  document.addEventListener("keydown", onKey);
2191
3415
  return () => document.removeEventListener("keydown", onKey);
@@ -2201,14 +3425,15 @@ function MarketPanel({ onClose }) {
2201
3425
  }, []);
2202
3426
  return h(
2203
3427
  "div",
2204
- { className: "dshm-overlay", onClick: onClose },
3428
+ { className: full ? "dshm-overlay full" : "dshm-overlay", onClick: onClose },
2205
3429
  h(
2206
3430
  "div",
2207
- { className: "dshm-panel", onClick: (e) => e.stopPropagation() },
3431
+ { className: full ? "dshm-panel full" : "dshm-panel", onClick: (e) => e.stopPropagation() },
2208
3432
  h(
2209
3433
  "div",
2210
3434
  { className: "dshm-head" },
2211
3435
  h("span", { className: "dshm-title" }, lookup("title.full")),
3436
+ h("span", { className: "dshm-head-divider", "aria-hidden": "true" }),
2212
3437
  h(
2213
3438
  "div",
2214
3439
  { className: "dshm-seg", role: "tablist" },
@@ -2224,20 +3449,49 @@ function MarketPanel({ onClose }) {
2224
3449
  onClick: () => setTab(key)
2225
3450
  },
2226
3451
  lookup(labelKey),
2227
- countKey && counts[countKey] != null ? h("span", { className: "dshm-count" }, String(counts[countKey])) : null
3452
+ countKey && counts[countKey] != null ? h("span", { className: "dshm-count" }, String(counts[countKey])) : null,
3453
+ key === "installed" && outdatedCount > 0 ? h("span", { className: "dsvm-reddot", title: lookup("installed.upgradeAll", { n: outdatedCount }) }) : null
2228
3454
  )
2229
3455
  )
2230
3456
  ),
2231
3457
  h("span", { className: "dshm-spacer" }),
2232
- dshVersion ? h(DshVersionChip, { version: dshVersion }) : null,
2233
- h("button", { className: "dshm-btn", onClick: onClose }, lookup("common.close"))
3458
+ pluginVersion ? h(DshmVersionChip, { version: pluginVersion }) : null,
3459
+ // 窗口控制组(0.7.7):最大化/还原 + 关闭,连体设计(系统化窗口按钮)
3460
+ h(
3461
+ "div",
3462
+ { className: "dshm-winctl", role: "group", "aria-label": lookup("panel.fullscreen") },
3463
+ h("button", {
3464
+ type: "button",
3465
+ title: lookup(full ? "panel.restore" : "panel.fullscreen"),
3466
+ "aria-label": lookup(full ? "panel.restore" : "panel.fullscreen"),
3467
+ onClick: toggleFull
3468
+ }, h(FsIcon, { full })),
3469
+ h("button", {
3470
+ type: "button",
3471
+ className: "danger",
3472
+ title: lookup("common.close"),
3473
+ "aria-label": lookup("common.close"),
3474
+ onClick: onClose
3475
+ }, h(XIcon))
3476
+ )
2234
3477
  ),
2235
3478
  h(
2236
3479
  "div",
2237
3480
  { className: "dshm-body" },
2238
- tab === "market" ? h(MarketTab, { notify, market, onMutation: refreshViews }) : null,
2239
- tab === "installed" ? h(InstalledTab, { notify, installed, onMutation: refreshViews }) : null,
2240
- tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged }) : null
3481
+ tab === "market" ? h(MarketTab, { notify, markets, onMutation: refreshViews, ops, favorites }) : null,
3482
+ tab === "installed" ? h(InstalledTab, { notify, installed, onMutation: refreshViews, ops }) : null,
3483
+ tab === "settings" ? h(SettingsTab, { notify, onRegistryChanged }) : null,
3484
+ h(OperationsPanel, {
3485
+ records: opRecords,
3486
+ onClearFinished: () => {
3487
+ opsStore.clearFinished();
3488
+ syncOps();
3489
+ },
3490
+ onRemove: (id) => {
3491
+ opsStore.remove(id);
3492
+ syncOps();
3493
+ }
3494
+ })
2241
3495
  ),
2242
3496
  toast ? h(
2243
3497
  "div",
@@ -2277,13 +3531,11 @@ function mountPanel() {
2277
3531
  function MarketIcon() {
2278
3532
  return h(
2279
3533
  "svg",
2280
- { viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true" },
2281
- h("rect", { x: "1.75", y: "1.75", width: "5.5", height: "5.5", rx: "1.2", stroke: "currentColor", strokeWidth: "1.4" }),
2282
- h("rect", { x: "8.75", y: "1.75", width: "5.5", height: "5.5", rx: "1.2", stroke: "currentColor", strokeWidth: "1.4" }),
2283
- h("rect", { x: "1.75", y: "8.75", width: "5.5", height: "5.5", rx: "1.2", stroke: "currentColor", strokeWidth: "1.4" }),
2284
- // 右下:放大镜(收一点,光学尺寸与其他宫格图标一致)
2285
- h("circle", { cx: "10.5", cy: "10.5", r: "2.9", stroke: "currentColor", strokeWidth: "1.4" }),
2286
- h("path", { d: "M12.6 12.6 14.3 14.3", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round" })
3534
+ { viewBox: "0 0 24 24", width: "16", height: "16", fill: "none", "aria-hidden": "true" },
3535
+ h("rect", { x: "3", y: "3", width: "7", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "2" }),
3536
+ h("rect", { x: "14", y: "3", width: "7", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "2" }),
3537
+ h("rect", { x: "3", y: "14", width: "7", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "2" }),
3538
+ h("rect", { x: "14", y: "14", width: "7", height: "7", rx: "1", stroke: "currentColor", strokeWidth: "2" })
2287
3539
  );
2288
3540
  }
2289
3541
  function MarketEntry(props) {
@@ -2318,12 +3570,15 @@ function ToolCardRow({ it, onInstalled }) {
2318
3570
  const install = async (e) => {
2319
3571
  e.stopPropagation();
2320
3572
  setBusy(true);
3573
+ const rec = opsStore.upsert({ kind: "install", target: it.id, status: "running", meta: { npm: it.npm, github: it.github, session: true } });
2321
3574
  try {
2322
3575
  const res = await api("install", { id: it.id });
3576
+ opsStore.upsert({ id: rec.id, status: "done" });
2323
3577
  onInstalled({ ...it, installed: true, installedPkg: res.pkg, installedVersion: res.version });
2324
- } catch (e2) {
2325
- if (e2 && e2.guard) {
2326
- setGuardNote(`${lookup("guard.blocked")} \xB7 ${e2.guard.compensation?.status || "\u2014"} \xB7 ${lookup("guard.noforce")}`);
3578
+ } catch (err) {
3579
+ opsStore.upsert({ id: rec.id, status: err && err.issue ? "input" : "failed", error: String(err && err.message || err) });
3580
+ if (err && err.guard) {
3581
+ setGuardNote(`${lookup("guard.blocked")} \xB7 ${err.guard.compensation?.status || "\u2014"} \xB7 ${lookup("guard.noforce")}`);
2327
3582
  }
2328
3583
  } finally {
2329
3584
  setBusy(false);