dsh-m 0.9.67 → 0.9.68
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/lib/core/community.js +35 -31
- package/lib/core/market.js +97 -198
- package/lib/core/merged-market.js +356 -0
- package/lib/core/profile-ops.js +17 -6
- package/package.json +1 -1
- package/registry.json +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,16 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
8
8
|
|
|
9
9
|
## 中文
|
|
10
10
|
|
|
11
|
+
### 0.9.68 性能:合并市场物化——同身份免重付 5MB 解析 + 适配 + 合并(ADR-0016)
|
|
12
|
+
|
|
13
|
+
- **问题(性能架构审查旗舰项,三轮评审闭环)**:领域概念「合并市场(Merged Market)」在 GLOSSARY 有名无主——装配逻辑内联在 `communityOutcome`,每个市场请求(缓存全命中的快路径也在内)都重付磁盘读 5.36MB 社区目录 + JSON.parse(实测 30–55ms)→ 目录适配层重建 4,400 条(30–90ms)→ 合并排序(5–35ms);且全部是**同步 JS、阻塞宿主事件循环**——市场每翻一页/每提交一次搜索,宿主的 SSE 对话流与工具调用停摆 65–400ms。搜索评分因归一化 WeakMap 跨请求永不命中再加 ~330ms(冷)。
|
|
14
|
+
- **两级 memo,各持天然安全键(ADR-0016 决策 1)**:L1——community.ts `readCache` 按不可变 version-pin 文件名缓存已解析目录(cap=2,meta 每次实读保 checkedAt 新鲜,失败路径不入 memo);L2——新模块 `src/core/merged-market.ts` 物化适配→合并→匹配索引(cap=4),身份键 = (namespace, profile, registry 身份, 目录 version+pin+开关)。
|
|
15
|
+
- **探测驱动换代 + 同步临界区(决策 3)**:每请求照常走廉价快路径(registry ~10KB + meta 141B ≈1–2ms)以返回身份字段查代;全部 await 位于键派生前、临界区全同步——并发同键天然只建一次,`builds` 计数断言哨兵。**summary 永不入代**每调用现算(status/checkedAt/errors 永远新鲜,SWR 与⑫同版本短路零漂移);deadline/unavailable/disabled/task=null 透传不落代;communityTask reject 透传(安装侧窄 catch 吞为 miss)。
|
|
16
|
+
- **双索引保安装语义(R2-3)**:merged 域 `lookupInstalled` 服务读路径;全量域 `findById`/`lookupInstalledAll`([primary…, adapt 全量] 含 displaced 让位条目)服务安装/升级三处(resolveRegistryEntry / upgradePlugin / profile-ops desktop 升级)——id-让位条目今日可装可升,语义零变化;主清单优先短路保留(社区异常不阻断主清单命中)。
|
|
17
|
+
- **解环 + score-once**:合并层六符号自 market.ts 迁入 merged-market.ts、23 条分类标签自 community.ts 随迁(原位 re-export 保 cli/tools 零改动)——market↔community import 环三方向断开;listMarket 评分段单遍化(类别短路先行、rank 查表),tie-break 与 0.9.67 逐点一致。
|
|
18
|
+
- **行为零变化承诺**:ADR-0003/0006/0008/0012/0013 全部不动;golden 等价基线(七维度 capture,含分页/搜索/排序/探测徽标)全程逐字节绿。
|
|
19
|
+
- **验证**:1305 项测试全绿、typecheck 零错误;实测本机 4,460 条真实目录首次 319ms → 同身份第二次 65ms,搜索评分冷 330ms → 同代热 8ms(40×)。实施经三轮独立评审闭环(计划评审 16 项 + 执行评审 7 项,全部「已修复并复核通过」;含一次修复脚本静默 no-op 的虚报,被评审 `git diff` 核对抓出并以真实 diff 补正)。实施计划与 ADR-0016 v3 在案(`docs/plans/`、`docs/adr/`)。
|
|
20
|
+
|
|
11
21
|
### 0.9.67 修复:操作记录卡移动端字号失控——显式入 12px 家族 + 折行自洽
|
|
12
22
|
|
|
13
23
|
- **根因(用户手机截图反馈)**:操作记录行 `.dsvm-oprow` 从不声明 `font-size`——桌面宿主基础字号下无感,移动端浏览器基础字号(≈15-16px)下整行被放大近一倍,与同屏设置卡的 12px 家族(`dsvm-kv` 配置地址行、`dshm-hint`、`dsvm-fold`)明显脱节,观感突兀。
|
|
@@ -495,6 +505,16 @@ The full release history of dsh-m, maintained bilingually: **Chinese first, Engl
|
|
|
495
505
|
|
|
496
506
|
## English
|
|
497
507
|
|
|
508
|
+
### Performance in 0.9.68 — materialized merged market: skip the 5MB parse + adapt + merge for repeat identities (ADR-0016)
|
|
509
|
+
|
|
510
|
+
- **Problem (flagship item of the performance architecture review, closed over three review rounds)**: the domain concept "Merged Market" existed in the GLOSSARY without an owning module - assembly logic lived inline in `communityOutcome`, so every market request (including fully cache-hit fast paths) re-paid a 5.36MB community catalog disk read + JSON.parse (measured 30-55ms) → adapter rebuild of 4,400 entries (30-90ms) → merge + sort (5-35ms). All of it is **synchronous JS blocking the host event loop** - every page turn / committed search froze the host's SSE streams and tool calls for 65-400ms. Search scoring added ~330ms cold because the normalization WeakMap never survived across requests.
|
|
511
|
+
- **Two-level memo, each with a naturally safe key (ADR-0016 decision 1)**: L1 - community.ts `readCache` memoizes the parsed catalog keyed by the immutable version-pinned body filename (cap=2; meta re-read every time so `checkedAt` stays fresh; failure paths never enter the memo); L2 - new module `src/core/merged-market.ts` materializes adapt→merge→match-index (cap=4), keyed by identity = (namespace, profile, registry identity, catalog version+pin+flag).
|
|
512
|
+
- **Probe-driven generations + synchronous critical section (decision 3)**: every request still walks the cheap fast path (registry ~10KB + meta 141B ≈1-2ms) and probes the store with the returned identity fields; all awaits happen before key derivation and the critical section (derive→check→adapt→merge→index→set) is fully synchronous - concurrent same-key callers naturally build once, asserted by a `builds` counter sentinel. **Summaries never enter the generation**: they are computed per call from the current state (status/checkedAt/errors always fresh; zero drift across SWR and the ⑫ same-version short-circuit). deadline/unavailable/disabled/task=null pass through without landing; communityTask rejections propagate (install-side narrow catches swallow them into a miss).
|
|
513
|
+
- **Dual indexes preserve install semantics (R2-3)**: the merged-domain `lookupInstalled` serves read paths; the full-domain `findById`/`lookupInstalledAll` ([primary…, full adapt set] including displaced entries) serves the three install/upgrade sites (resolveRegistryEntry / upgradePlugin / profile-ops desktop upgrade) - id-displaced entries remain installable and upgradable, byte-for-byte semantics. Primary-first short-circuits preserved (community failures never block primary hits).
|
|
514
|
+
- **Cycle dissolved + score-once**: six merge-layer symbols moved from market.ts into merged-market.ts, the 23 category labels moved from community.ts (re-exported in place so cli/tools stay untouched) - the market↔community import cycle is broken in all three directions; listMarket's scoring is single-pass (category short-circuit first, rank reads the table) with tie-breaks byte-identical to 0.9.67.
|
|
515
|
+
- **Zero behavior change pledge**: ADR-0003/0006/0008/0012/0013 untouched; a golden equivalence baseline (seven capture dimensions incl. paging/search/sort/probe badges) stayed byte-identical throughout.
|
|
516
|
+
- **Verification**: 1305 tests green, typecheck clean; measured on this machine against the real 4,460-entry catalog: first call 319ms → second same-identity call 65ms; search scoring 330ms cold → 8ms warm within a generation (40×). Implementation closed through three independent review rounds (16 plan-review + 7 execution-review findings, all "fixed and re-verified"; including one silent no-op fix caught by the reviewer's `git diff` discipline and corrected with a real diff). Implementation plan and ADR-0016 v3 on record (`docs/plans/`, `docs/adr/`).
|
|
517
|
+
|
|
498
518
|
### Fixed in 0.9.67 — operations card font blows up on mobile: join the 12px family + wrap self-consistency
|
|
499
519
|
|
|
500
520
|
- **Root cause (user phone screenshot)**: the operations rows `.dsvm-oprow` never declared a `font-size` — invisible on the desktop host base size, but on mobile browsers (base ≈15-16px) the whole row rendered nearly twice the size of the settings cards around it, clashing with the 12px family (`dsvm-kv` config rows, `dshm-hint`, `dsvm-fold`) on the same screen.
|
package/lib/core/community.js
CHANGED
|
@@ -11,42 +11,15 @@ import { isAbsolute, join, relative } from 'node:path';
|
|
|
11
11
|
import { cacheRoot } from './env.js';
|
|
12
12
|
import { decodeUtf8Fatal, fetchBytesLimited, describeFetchFailure } from './httpx.js';
|
|
13
13
|
import { isExactVersion, npmLatest } from './versions.js';
|
|
14
|
-
import { communityOutcome } from './market.js';
|
|
14
|
+
import { communityOutcome } from './merged-market.js';
|
|
15
15
|
export const COMMUNITY_NPM_PACKAGE = 'dsh-plugin-catalog';
|
|
16
16
|
export const MAX_COMMUNITY_BYTES = 32 * 1024 * 1024;
|
|
17
17
|
export const MAX_COMMUNITY_ENTRIES = 30_000;
|
|
18
18
|
export const CATALOG_BODY_TIMEOUT_MS = 15_000;
|
|
19
19
|
export const COMMUNITY_CHAIN_BUDGET_MS = 30_000;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
* GUI zoneChips 消费服务端数据,客户端内嵌副本(market-state.js)随 Task 8 删除。
|
|
24
|
-
*/
|
|
25
|
-
export const COMMUNITY_CATEGORY_LABELS = {
|
|
26
|
-
agi: 'AGI 架构探索',
|
|
27
|
-
ui: 'UI 增强',
|
|
28
|
-
usage: '用量与计费',
|
|
29
|
-
theme: '主题与外观',
|
|
30
|
-
model: '模型与账号接入',
|
|
31
|
-
identity: '身份与通信',
|
|
32
|
-
session: '会话与消息',
|
|
33
|
-
memory: '记忆',
|
|
34
|
-
tools: '工具与能力',
|
|
35
|
-
wsl: 'WSL 与 Windows 互操作',
|
|
36
|
-
browser: '浏览器与网页',
|
|
37
|
-
vision: '视觉与多模态',
|
|
38
|
-
voice: '语音与音频',
|
|
39
|
-
docs: '文档与渲染',
|
|
40
|
-
skill: '技能包',
|
|
41
|
-
workflow: '工作流与自动化',
|
|
42
|
-
git: 'Git 与代码评审',
|
|
43
|
-
notify: '通知与集成',
|
|
44
|
-
dev: '开发与运行时',
|
|
45
|
-
security: '安全与权限',
|
|
46
|
-
remote: '远程与移动端',
|
|
47
|
-
market: '插件市场与管理',
|
|
48
|
-
fun: '娱乐',
|
|
49
|
-
};
|
|
20
|
+
// 0.9.68(ADR-0016 R1-2):COMMUNITY_CATEGORY_LABELS 随 communitySummary 迁入 merged-market.ts
|
|
21
|
+
// (标签单一事实源不变,原位 re-export 保 cli.ts/tools.ts 既有 import 零改动)。
|
|
22
|
+
export { COMMUNITY_CATEGORY_LABELS } from './merged-market.js';
|
|
50
23
|
const TOP_LEVEL_KEYS = new Set(['name', 'url', 'source', 'updated', 'count', 'categories', 'plugins']);
|
|
51
24
|
/**
|
|
52
25
|
* 容器层严格校验(条目层由适配层负责):
|
|
@@ -120,6 +93,31 @@ async function atomicWrite(path, text) {
|
|
|
120
93
|
await writeFile(tmp, text);
|
|
121
94
|
await rename(tmp, path);
|
|
122
95
|
}
|
|
96
|
+
// ---------- ADR-0016 决策 1 L1:已解析目录 body memo ----------
|
|
97
|
+
// body 文件名 version-pin 不可变(catalog-{version}.json,atomicWrite 新版本写新文件),
|
|
98
|
+
// 同 (dir, version) 即同字节——memo 键即身份,零失效逻辑。cap=2(身份组合常态 ≤2;
|
|
99
|
+
// 每条 ≈15-20MB 解析对象,最坏 ≈40MB)。失败路径(parse/validate 失败)不写 memo。
|
|
100
|
+
const bodyMemo = new Map();
|
|
101
|
+
const BODY_MEMO_CAP = 2;
|
|
102
|
+
let communityBodyReads = 0;
|
|
103
|
+
function rememberBody(dir, version, catalog) {
|
|
104
|
+
const key = `${dir}§${version}`;
|
|
105
|
+
if (!bodyMemo.has(key) && bodyMemo.size >= BODY_MEMO_CAP) {
|
|
106
|
+
const oldest = bodyMemo.keys().next().value;
|
|
107
|
+
if (oldest !== undefined)
|
|
108
|
+
bodyMemo.delete(oldest);
|
|
109
|
+
}
|
|
110
|
+
bodyMemo.set(key, catalog);
|
|
111
|
+
}
|
|
112
|
+
/** 测试钩子:readCache 对 body 文件的实读次数(memo 命中不计;命名先例 _waitForCommunityBackgroundForTests)。 */
|
|
113
|
+
export function _communityBodyReadsForTests() {
|
|
114
|
+
return communityBodyReads;
|
|
115
|
+
}
|
|
116
|
+
/** 测试钩子:清 body memo 与计数(测试隔离用,latest-cache resetLatestCacheForTest 同款)。 */
|
|
117
|
+
export function _resetCommunityBodyMemoForTests() {
|
|
118
|
+
bodyMemo.clear();
|
|
119
|
+
communityBodyReads = 0;
|
|
120
|
+
}
|
|
123
121
|
/** 读取顺序(v8 定稿):meta 结构校验 → isExactVersion(meta.version) → body containment → 才读正文;损坏文件清理。 */
|
|
124
122
|
async function readCache(dir, pin) {
|
|
125
123
|
const metaPath = join(dir, pin ? metaPinName(pin) : META_LATEST);
|
|
@@ -149,6 +147,11 @@ async function readCache(dir, pin) {
|
|
|
149
147
|
const bodyPath = join(dir, bodyName(meta.version));
|
|
150
148
|
if (!contained(dir, bodyPath))
|
|
151
149
|
return null;
|
|
150
|
+
// L1 快路径:meta 每次实读(checkedAt 会更新),body 命中 memo 即免 5MB 级 parse+校验
|
|
151
|
+
const memoHit = bodyMemo.get(`${dir}§${meta.version}`);
|
|
152
|
+
if (memoHit)
|
|
153
|
+
return { meta, catalog: memoHit };
|
|
154
|
+
communityBodyReads += 1;
|
|
152
155
|
let data;
|
|
153
156
|
try {
|
|
154
157
|
data = JSON.parse(await readFile(bodyPath, 'utf8'));
|
|
@@ -162,6 +165,7 @@ async function readCache(dir, pin) {
|
|
|
162
165
|
await rm(bodyPath, { force: true }).catch(() => undefined);
|
|
163
166
|
return null;
|
|
164
167
|
}
|
|
168
|
+
rememberBody(dir, meta.version, v.catalog);
|
|
165
169
|
return { meta, catalog: v.catalog };
|
|
166
170
|
}
|
|
167
171
|
async function writeMeta(dir, pin, meta) {
|
package/lib/core/market.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { existsSync } from 'node:fs';
|
|
11
11
|
import { readFile } from 'node:fs/promises';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
-
import { dshHome, webProfileDir } from './env.js';
|
|
13
|
+
import { dshHome, webProfileDir, WEB_PROFILE } from './env.js';
|
|
14
14
|
import { runProfileTransaction, makeNpmWarmPackument, TransactionError, } from './profile-transaction.js';
|
|
15
15
|
import { listInstalledPlugins as defaultListInstalledPlugins, } from './installed.js';
|
|
16
16
|
import { loaderHost } from './live-plugin.js';
|
|
@@ -18,8 +18,12 @@ import { precheckNpmCompat, IncompatibleError } from './compat-check.js';
|
|
|
18
18
|
import { PROTECTED_MODULES, composeEnablement } from './enablement.js';
|
|
19
19
|
import { readProfileOverrides } from './patch-yaml.js';
|
|
20
20
|
import { loadRegistry as defaultLoadRegistry, } from './registry.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import { fetchCommunityCatalog as defaultFetchCommunityCatalog, } from './community.js';
|
|
22
|
+
// 0.9.68(ADR-0016):合并层迁入 merged-market.ts(communityOutcome/mergeRegistries/communitySummary/
|
|
23
|
+
// deadlineRace/matchInstalledByEntry/isCommunityEntry/COMMUNITY_CATEGORY_LABELS)——market↔community
|
|
24
|
+
// import 环自此断开;re-export 保既有消费方(tools.ts 的类型、profile-ops 的 matchInstalledByEntry)零改动。
|
|
25
|
+
import { communityOutcome, communityTimeoutSummary, withFallbackLookups, deadlineRace, isCommunityEntry, matchInstalledByEntry, mergedOutcome, } from './merged-market.js';
|
|
26
|
+
export { communityOutcome, matchInstalledByEntry } from './merged-market.js';
|
|
23
27
|
import { normalizeSearchText, relevanceScore, tokenizeSearchText } from './search-relevance.js';
|
|
24
28
|
import { GithubBudgetExhaustedError, createGithubRequestBudget, isExactVersion } from './versions.js';
|
|
25
29
|
import { classifyUpgradeActivation } from './activation.js';
|
|
@@ -128,18 +132,6 @@ function timeoutRegistryState(cfg) {
|
|
|
128
132
|
count: 0,
|
|
129
133
|
};
|
|
130
134
|
}
|
|
131
|
-
function deadlineRace(task, ms) {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
const timer = setTimeout(() => resolve('deadline'), Math.max(0, ms));
|
|
134
|
-
task.then((value) => {
|
|
135
|
-
clearTimeout(timer);
|
|
136
|
-
resolve(value);
|
|
137
|
-
}, (err) => {
|
|
138
|
-
clearTimeout(timer);
|
|
139
|
-
reject(err);
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
135
|
// ---------- latest TTL cache ----------
|
|
144
136
|
// 0.9.20:纯内存 TTL 缓存(ADR-0006,推翻 0.9.14 的落盘跨重启存活——重启即失效是特性);
|
|
145
137
|
// mutation 成功后的定向失效走 invalidateLatestForEntry(只失效不回写)。
|
|
@@ -194,144 +186,6 @@ function classifyLatestError(err) {
|
|
|
194
186
|
return 'rate-limited';
|
|
195
187
|
return 'network-error';
|
|
196
188
|
}
|
|
197
|
-
/** 0.9.8 导出(原模块私有):desktopUpgrade 的「已装 ↔ 收录条目」匹配与 web upgradePlugin 同语义。 */
|
|
198
|
-
export function matchInstalledByEntry(entry, installed) {
|
|
199
|
-
return installed.find((it) => {
|
|
200
|
-
if (entry.npm && it.pkg === entry.npm)
|
|
201
|
-
return true;
|
|
202
|
-
if (entry.npm && it.name === entry.npm)
|
|
203
|
-
return true;
|
|
204
|
-
if (entry.github && it.source === 'github') {
|
|
205
|
-
const m = /^github:([^#]+)/.exec(it.spec);
|
|
206
|
-
if (m && m[1] === entry.github)
|
|
207
|
-
return true;
|
|
208
|
-
}
|
|
209
|
-
return false;
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* 合并去重(Q41):社区条目依次与主清单的 npm 名集 / github owner-repo 集 / id 集比对,
|
|
214
|
-
* 任一相撞即让位(每条只计一次);社区内部 npm 名重复同样让位(首条优先)。
|
|
215
|
-
* 主清单恒优先——让位只影响社区条目,主条目原样保留。
|
|
216
|
-
*/
|
|
217
|
-
export function mergeRegistries(primary, community) {
|
|
218
|
-
const primaryNpm = new Set(primary.filter((e) => e.npm).map((e) => e.npm));
|
|
219
|
-
const primaryGithub = new Set(primary.filter((e) => e.github).map((e) => e.github));
|
|
220
|
-
const primaryIds = new Set(primary.map((e) => e.id));
|
|
221
|
-
const displaced = [];
|
|
222
|
-
const kept = [];
|
|
223
|
-
const seenNpm = new Set();
|
|
224
|
-
for (const c of community) {
|
|
225
|
-
if (c.npm !== undefined) {
|
|
226
|
-
if (primaryNpm.has(c.npm) || seenNpm.has(c.npm)) {
|
|
227
|
-
displaced.push(c.id);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
seenNpm.add(c.npm);
|
|
231
|
-
}
|
|
232
|
-
if ((c.github !== undefined && primaryGithub.has(c.github)) || primaryIds.has(c.id)) {
|
|
233
|
-
displaced.push(c.id);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
kept.push(c);
|
|
237
|
-
}
|
|
238
|
-
// Q45:主清单置顶(组内原顺序)+ 社区按 30 天下载量降序(无数据按名称)
|
|
239
|
-
const sorted = [...kept].sort((a, b) => {
|
|
240
|
-
const da = a.downloads ?? -1;
|
|
241
|
-
const db = b.downloads ?? -1;
|
|
242
|
-
if (da !== db)
|
|
243
|
-
return db - da;
|
|
244
|
-
return a.name.localeCompare(b.name);
|
|
245
|
-
});
|
|
246
|
-
const warnings = displaced.length > 0 ? [`${displaced.length} 条社区条目与主清单重复,已让位(主清单恒优先)`] : [];
|
|
247
|
-
return { items: [...primary, ...sorted], displaced: displaced.length, warnings };
|
|
248
|
-
}
|
|
249
|
-
function communitySummary(state, counts, extraWarnings, categoryLabelsEn) {
|
|
250
|
-
const unavailableLike = state.status === 'disabled' || state.status === 'unavailable';
|
|
251
|
-
return {
|
|
252
|
-
enabled: state.enabled,
|
|
253
|
-
status: state.status,
|
|
254
|
-
version: state.version,
|
|
255
|
-
checkedAt: state.checkedAt,
|
|
256
|
-
fetchedAt: state.fetchedAt,
|
|
257
|
-
route: state.route,
|
|
258
|
-
acceptedCount: unavailableLike ? 0 : (counts.acceptedCount ?? 0),
|
|
259
|
-
upstreamCount: unavailableLike ? (state.status === 'disabled' ? null : 0) : (counts.upstreamCount ?? state.count),
|
|
260
|
-
displaced: unavailableLike ? 0 : (counts.displaced ?? 0),
|
|
261
|
-
skippedDirty: unavailableLike ? 0 : (counts.skippedDirty ?? 0),
|
|
262
|
-
skippedSubpathNoNpm: unavailableLike ? 0 : (counts.skippedSubpathNoNpm ?? 0),
|
|
263
|
-
errors: [...state.errors],
|
|
264
|
-
warnings: [...state.warnings, ...extraWarnings],
|
|
265
|
-
// 标签单一事实源(0.7.0 Task 4):disabled/skipped 下无社区数据语义,不携带;
|
|
266
|
-
// EN 标签(i18n)随行携带,仅在调用方传入非空映射时出现(缺省 = 上游无 categories 数据)
|
|
267
|
-
...(state.status !== 'disabled' && state.status !== 'skipped'
|
|
268
|
-
? {
|
|
269
|
-
categoryLabels: { ...COMMUNITY_CATEGORY_LABELS },
|
|
270
|
-
...(categoryLabelsEn && Object.keys(categoryLabelsEn).length > 0
|
|
271
|
-
? { categoryLabelsEn: { ...categoryLabelsEn } }
|
|
272
|
-
: {}),
|
|
273
|
-
}
|
|
274
|
-
: {}),
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
function disabledCommunitySummary() {
|
|
278
|
-
return communitySummary({ enabled: false, status: 'disabled', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: [], warnings: [] }, {}, []);
|
|
279
|
-
}
|
|
280
|
-
/** 查询层主动跳过(0.7.0 Task 2:source='primary'):本次未加载社区层,非配置关闭、非故障。 */
|
|
281
|
-
function skippedCommunitySummary() {
|
|
282
|
-
return communitySummary({ enabled: true, status: 'skipped', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: [], warnings: [] }, {}, []);
|
|
283
|
-
}
|
|
284
|
-
function communityTimeoutSummary() {
|
|
285
|
-
return communitySummary({ enabled: true, status: 'unavailable', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: ['社区目录状态获取超时,可稍后刷新'], warnings: [] }, {}, []);
|
|
286
|
-
}
|
|
287
|
-
/**
|
|
288
|
-
* 社区 loader waiter 收敛(v9/v10 waiter-scoped 契约):共享 flight 不接收调用者 deadline,
|
|
289
|
-
* 本函数作为 waiter 用剩余 deadline race 自己的等待;到点只结束本 waiter(summary 标超时),
|
|
290
|
-
* 共享 flight 照常继续。source='primary'/未启用 → loader 零调用(task 传 null → skipped;
|
|
291
|
-
* 配置关闭走真任务的 disabled 分支,与跳过语义分离)。
|
|
292
|
-
* 导出供 community.ts getCommunitySummary 复用(summary 组装单一产地)。
|
|
293
|
-
*/
|
|
294
|
-
export async function communityOutcome(task, deadlineAt, primary) {
|
|
295
|
-
if (!task)
|
|
296
|
-
return { summary: skippedCommunitySummary(), merged: primary };
|
|
297
|
-
let loaded;
|
|
298
|
-
try {
|
|
299
|
-
loaded = await deadlineRace(task, deadlineAt - Date.now());
|
|
300
|
-
}
|
|
301
|
-
finally {
|
|
302
|
-
// race 输出后共享 promise 若仍悬挂(deadline 先到),收尾防 unhandled rejection
|
|
303
|
-
void task.catch(() => undefined);
|
|
304
|
-
}
|
|
305
|
-
if (loaded === 'deadline')
|
|
306
|
-
return { summary: communityTimeoutSummary(), merged: primary };
|
|
307
|
-
const state = loaded.state;
|
|
308
|
-
if (state.status === 'disabled')
|
|
309
|
-
return { summary: disabledCommunitySummary(), merged: primary };
|
|
310
|
-
if (state.status === 'unavailable' || !loaded.catalog) {
|
|
311
|
-
return {
|
|
312
|
-
summary: communitySummary(state, { acceptedCount: 0, upstreamCount: 0, displaced: 0, skippedDirty: 0, skippedSubpathNoNpm: 0 }, []),
|
|
313
|
-
merged: primary,
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
const adapted = adaptCommunityCatalog(loaded.catalog);
|
|
317
|
-
const merge = mergeRegistries(primary, adapted.entries);
|
|
318
|
-
// EN 分类标签(i18n):直接取上游目录 categories.en——上游新增分类自动跟进,
|
|
319
|
-
// 缺 en 的 id 不进映射(客户端按语言取用并回退中文标签,不在此处手养第二张表)
|
|
320
|
-
const categoryLabelsEn = Object.fromEntries(Object.entries(loaded.catalog.categories)
|
|
321
|
-
.map(([id, c]) => [id, typeof c?.en === 'string' && c.en !== '' ? c.en : ''])
|
|
322
|
-
.filter(([, en]) => en !== ''));
|
|
323
|
-
const summary = communitySummary(state, {
|
|
324
|
-
acceptedCount: adapted.entries.length,
|
|
325
|
-
upstreamCount: loaded.catalog.plugins.length,
|
|
326
|
-
displaced: merge.displaced,
|
|
327
|
-
skippedDirty: adapted.skippedDirty,
|
|
328
|
-
skippedSubpathNoNpm: adapted.skippedSubpathNoNpm,
|
|
329
|
-
}, [...adapted.warnings, ...merge.warnings], categoryLabelsEn);
|
|
330
|
-
return { summary, merged: merge.items };
|
|
331
|
-
}
|
|
332
|
-
function isCommunityEntry(entry) {
|
|
333
|
-
return entry.descriptionEn !== undefined;
|
|
334
|
-
}
|
|
335
189
|
/** 旁路数值/日期读取(RegistryEntry 无这些键 → null/'';0.7.0 Task 2 排序用)。 */
|
|
336
190
|
function dlOf(e) {
|
|
337
191
|
const v = e.downloads;
|
|
@@ -469,7 +323,16 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
469
323
|
const installedComplete = installed !== null && installed.complete === true;
|
|
470
324
|
const installedItems = installed?.items ?? [];
|
|
471
325
|
// 社区 waiter 收敛:主 unavailable 时 merged = 存活社区条目(Q42 出页不返空);两层皆不可用 → 空页契约
|
|
472
|
-
|
|
326
|
+
// 0.9.68(ADR-0016):改经物化合并市场——同身份(registry 代 + 目录 version)命中即免 adapt+merge,
|
|
327
|
+
// summary 仍由当次 state 现算;失败/deadline/unavailable/disabled/task=null 形态透传不落代。
|
|
328
|
+
const community = await mergedOutcome({
|
|
329
|
+
namespace,
|
|
330
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
331
|
+
cfg,
|
|
332
|
+
registry: loaded,
|
|
333
|
+
communityTask,
|
|
334
|
+
deadlineAt,
|
|
335
|
+
});
|
|
473
336
|
// 分区过滤 + 全量统计 + query/category 过滤 + 分页(同步,极轻;0.7.0 Task 2):
|
|
474
337
|
// categoryCounts = 分区集合(不含 query/category 过滤——chips 需要全区计数);
|
|
475
338
|
// total = 分区 ∩ query ∩ category。
|
|
@@ -493,19 +356,27 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
493
356
|
// 相关性搜索(0.7.0 Task 3):归一化分词 + 字段加权评分;0 分不返回;
|
|
494
357
|
// id 整串精确匹配保证命中(收藏 stale 检测依赖)。
|
|
495
358
|
const terms = tokenizeSearchText(normalizeSearchText(opts.query ?? ''));
|
|
359
|
+
// 0.9.68(ADR-0016 决策 7,score-once):类别不匹配先短路(不付评分成本),terms 非空时
|
|
360
|
+
// 单遍评分并记入 scoreByEntry——rank 阶段查表,不再对每条目二次调用 relevanceScore。
|
|
361
|
+
// tie-break 取 afterSort 之后的位置,与 0.9.67 语义逐点一致(评审 R1-4)。
|
|
362
|
+
const scoreByEntry = new Map();
|
|
496
363
|
const filtered = zoned.filter((entry) => {
|
|
497
364
|
const also = entry.alsoCategories ?? [];
|
|
498
365
|
if (cat && entry.category !== cat && !also.includes(cat))
|
|
499
366
|
return false;
|
|
500
367
|
if (terms.length === 0)
|
|
501
368
|
return true;
|
|
502
|
-
|
|
369
|
+
const score = relevanceScore(entry, terms);
|
|
370
|
+
if (score <= 0)
|
|
371
|
+
return false;
|
|
372
|
+
scoreByEntry.set(entry, score);
|
|
373
|
+
return true;
|
|
503
374
|
});
|
|
504
375
|
// 排序:显式 sort 先行;query 命中时相关性优先(稳定 tie-break 回到既有序——merged 现序或用户排序)
|
|
505
376
|
const afterSort = opts.sort ? sortEntries(filtered, opts.sort) : filtered;
|
|
506
377
|
const ranked = terms.length > 0
|
|
507
378
|
? afterSort
|
|
508
|
-
.map((entry, idx) => ({ entry, score:
|
|
379
|
+
.map((entry, idx) => ({ entry, score: scoreByEntry.get(entry), idx }))
|
|
509
380
|
.sort((a, b) => b.score - a.score || a.idx - b.idx)
|
|
510
381
|
.map((s) => s.entry)
|
|
511
382
|
: afterSort;
|
|
@@ -590,8 +461,14 @@ export async function listMarket(cfg = {}, opts = {}, deps = {}) {
|
|
|
590
461
|
}
|
|
591
462
|
}
|
|
592
463
|
// outdated 判定统一在 probe 后进行(0.9.45:cache-only 也执行——暖缓存不丢「可升级」徽标)
|
|
464
|
+
// 0.9.68(ADR-0016 决策 5 / 评审 R1-7):pkg→installed 查表,页内 O(1) 取代逐项 find
|
|
465
|
+
// first-wins:与旧 installedItems.find 逐点一致(正常枚举无重复 pkg,理论边界防御)
|
|
466
|
+
const instByPkg = new Map();
|
|
467
|
+
for (const inst of installedItems)
|
|
468
|
+
if (!instByPkg.has(inst.pkg))
|
|
469
|
+
instByPkg.set(inst.pkg, inst);
|
|
593
470
|
for (const item of items) {
|
|
594
|
-
const inst = item.installedPkg !== undefined ?
|
|
471
|
+
const inst = item.installedPkg !== undefined ? instByPkg.get(item.installedPkg) : undefined;
|
|
595
472
|
if (!inst)
|
|
596
473
|
continue;
|
|
597
474
|
if (item.latestVersion !== undefined && inst.version) {
|
|
@@ -653,7 +530,18 @@ export async function listInstalledWithMeta(cfg = {}, opts = {}, deps = {}) {
|
|
|
653
530
|
catch {
|
|
654
531
|
loaded = 'deadline';
|
|
655
532
|
}
|
|
656
|
-
|
|
533
|
+
// 0.9.68(ADR-0016):registry 正常时经物化合并市场(同身份免 adapt+merge + 匹配查表);
|
|
534
|
+
// registry deadline 降级路径身份残缺(primary=[]、fetchedAt 未知),走无 memo 的 communityOutcome,不进 L2。
|
|
535
|
+
const community = loaded === 'deadline'
|
|
536
|
+
? withFallbackLookups(await communityOutcome(communityTask, deadlineAt, []))
|
|
537
|
+
: await mergedOutcome({
|
|
538
|
+
namespace,
|
|
539
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
540
|
+
cfg,
|
|
541
|
+
registry: loaded,
|
|
542
|
+
communityTask,
|
|
543
|
+
deadlineAt,
|
|
544
|
+
});
|
|
657
545
|
if (loaded === 'deadline' || loaded.status === 'unavailable') {
|
|
658
546
|
// registry 不可用:不做 matching/探测(原行为),但社区 summary 照常携带(CLI outdated 双源判定)
|
|
659
547
|
const registryState = loaded === 'deadline' ? timeoutRegistryState(cfg) : stateOf(loaded);
|
|
@@ -661,21 +549,22 @@ export async function listInstalledWithMeta(cfg = {}, opts = {}, deps = {}) {
|
|
|
661
549
|
return { items, others: installed.others, profileDir: installed.profileDir, registryState, community: community.summary };
|
|
662
550
|
}
|
|
663
551
|
const registryState = stateOf(loaded);
|
|
664
|
-
matchInstalled(items, community.merged);
|
|
552
|
+
matchInstalled(items, community.merged, community.lookupInstalled);
|
|
665
553
|
// 两段加载(ADR-0008):probeMode 控制探测段——缺省 'full' 行为不变;'none' 面板快列表跳过探测;
|
|
666
554
|
// 'only' 面板第二段 ttlMin=0 永远新鲜(readLatestCache(key,0) 先删共享条目再重探:host ns 下浏览页/
|
|
667
555
|
// 工具的 TTL 命中被刷新为更新值,预算消耗速率上升、上限不变,超限走 latestError 降级)。
|
|
668
556
|
if ((opts.probeMode ?? 'full') !== 'none') {
|
|
669
557
|
// 0.9.20:latest 缓存纯内存(ADR-0006)——探测段前仅一次性清扫 0.9.14 遗留磁盘信封
|
|
670
558
|
await ensureLatestCacheSwept({ namespace, profile: opts.profile });
|
|
671
|
-
await probeLatest(items, { merged: community.merged, registryAddress: loaded.configuredAddress, ttlMin: opts.probeMode === 'only' ? 0 : Math.max(0, cfg.cacheTtlMin ?? 60) }, d, { namespace, signal, githubBudget, remaining, timeoutMs: cfg.timeoutMs ?? 20_000 });
|
|
559
|
+
await probeLatest(items, { merged: community.merged, lookup: community.lookupInstalled, registryAddress: loaded.configuredAddress, ttlMin: opts.probeMode === 'only' ? 0 : Math.max(0, cfg.cacheTtlMin ?? 60) }, d, { namespace, signal, githubBudget, remaining, timeoutMs: cfg.timeoutMs ?? 20_000 });
|
|
672
560
|
}
|
|
673
561
|
return { items, others: installed.others, profileDir: installed.profileDir, registryState, community: community.summary };
|
|
674
562
|
}
|
|
675
563
|
/** matching(主+社区合并条目):命中主条目或社区条目都写 registryId;社区命中再标 community。 */
|
|
676
|
-
function matchInstalled(items, merged) {
|
|
564
|
+
function matchInstalled(items, merged, lookup) {
|
|
677
565
|
for (const item of items) {
|
|
678
|
-
|
|
566
|
+
// 0.9.68(ADR-0016 决策 5):mergedOutcome.lookupInstalled 查表 O(I);缺省回退线性扫描保旧调用面
|
|
567
|
+
const entry = lookup ? lookup(item) : merged.find((e) => matchInstalledByEntry(e, [item]));
|
|
679
568
|
if (!entry)
|
|
680
569
|
continue;
|
|
681
570
|
item.registryId = entry.id;
|
|
@@ -688,7 +577,7 @@ function matchInstalled(items, merged) {
|
|
|
688
577
|
/** 已装页 latest 探测(Q46 已装页豁免,但 GitHub 计入 request 预算 + 宿主滚动窗口)。 */
|
|
689
578
|
async function probeLatest(items, ctx, d, rt) {
|
|
690
579
|
await mapWithConcurrency(items, LATEST_WORKERS, async (item) => {
|
|
691
|
-
const entry = ctx.merged.find((e) => matchInstalledByEntry(e, [item]));
|
|
580
|
+
const entry = ctx.lookup ? ctx.lookup(item) : ctx.merged.find((e) => matchInstalledByEntry(e, [item]));
|
|
692
581
|
// latest 探测沿用 listMarket 的 TTL cache;npm-only 条目(不在收录清单的已装包)也走
|
|
693
582
|
// latestCacheKey 归一格式(0.9.14:否则首段非 host/cli 会被落盘层静默跳过)
|
|
694
583
|
const cacheKey = entry
|
|
@@ -756,11 +645,31 @@ export async function resolveRegistryEntry(id, cfg = {}, opts = {}, deps) {
|
|
|
756
645
|
if (loaded.status === 'unavailable') {
|
|
757
646
|
throw new Error(`收录清单不可用,无法安装 ${id};请检查 registry 配置或网络后重试`);
|
|
758
647
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
648
|
+
// 主清单优先短路(今日语义原样):primary 命中不触碰社区——社区异常不阻断主清单条目安装。
|
|
649
|
+
const primaryEntry = loaded.registry.plugins.find((e) => e.id === id);
|
|
650
|
+
if (primaryEntry)
|
|
651
|
+
return primaryEntry;
|
|
652
|
+
// 0.9.68(ADR-0016 / R2-3):主清单 miss 时经物化合并市场取全量域 findById([primary..., adapt 全量...]
|
|
653
|
+
// 含 displaced 让位条目——id-让位条目今日可装,语义零变化);同身份命中免每装机 4,400 条重 adapt。
|
|
654
|
+
// communityTask reject 透传,由窄 catch 吞为 miss(与旧 findCommunityInstallEntry 同构 →「没有该条目」)。
|
|
655
|
+
try {
|
|
656
|
+
const communityTask = (deps?.fetchCommunityCatalog ?? defaultFetchCommunityCatalog)(cfg, { namespace: opts.namespace ?? 'host', signal: opts.signal, profile: opts.profile });
|
|
657
|
+
const outcome = await mergedOutcome({
|
|
658
|
+
namespace: opts.namespace ?? 'host',
|
|
659
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
660
|
+
cfg,
|
|
661
|
+
registry: loaded,
|
|
662
|
+
communityTask,
|
|
663
|
+
deadlineAt: Date.now() + DEFAULT_DEADLINE_MS,
|
|
664
|
+
});
|
|
665
|
+
const entry = outcome.findById(id);
|
|
666
|
+
if (entry)
|
|
667
|
+
return entry;
|
|
668
|
+
}
|
|
669
|
+
catch {
|
|
670
|
+
/* 社区失败/异常吞为 miss(今日语义) */
|
|
671
|
+
}
|
|
672
|
+
throw new Error(`registry 中没有该条目: ${id}`);
|
|
764
673
|
}
|
|
765
674
|
export async function installFromRegistry(id, cfg = {}, opts = {}, deps) {
|
|
766
675
|
const entry = await resolveRegistryEntry(id, cfg, opts, deps);
|
|
@@ -1323,10 +1232,27 @@ async function upgradePluginLocked(pkg, cfg = {}, opts = {}, deps) {
|
|
|
1323
1232
|
const target = installed.find((it) => it.pkg === pkg);
|
|
1324
1233
|
if (!target)
|
|
1325
1234
|
throw new Error(`web profile 未安装该插件: ${pkg}`);
|
|
1235
|
+
// 0.5.1:合并市场安装的社区条目同样可升级——主清单优先,miss 时查社区目录。
|
|
1236
|
+
// 0.9.68(ADR-0016 / R2-3):社区兜底经物化合并市场全量域 lookupInstalledAll(含 displaced 让位条目,
|
|
1237
|
+
// min-ord 胜者 = 今日「先主清单 find 后社区 adapt 全量 find」合成语义);reject 透传由窄 catch 吞为 miss。
|
|
1326
1238
|
let entry = loaded.registry.plugins.find((e) => matchInstalledByEntry(e, [target]));
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1239
|
+
try {
|
|
1240
|
+
if (!entry) {
|
|
1241
|
+
const communityTask = (deps?.fetchCommunityCatalog ?? defaultFetchCommunityCatalog)(cfg, { namespace: opts.namespace ?? 'host', signal: opts.signal, profile: opts.profile });
|
|
1242
|
+
const outcome = await mergedOutcome({
|
|
1243
|
+
namespace: opts.namespace ?? 'host',
|
|
1244
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
1245
|
+
cfg,
|
|
1246
|
+
registry: loaded,
|
|
1247
|
+
communityTask,
|
|
1248
|
+
deadlineAt: Date.now() + DEFAULT_DEADLINE_MS,
|
|
1249
|
+
});
|
|
1250
|
+
entry = outcome.lookupInstalledAll(target);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
entry = undefined;
|
|
1255
|
+
}
|
|
1330
1256
|
if (!entry)
|
|
1331
1257
|
throw new Error(`「${pkg}」不是经 dsh-m 收录的插件;直接升级请用 dsh plugin update 或先在 registry 收录它`);
|
|
1332
1258
|
// 0.5.1 修复:直调 installEntryLocked——upgradePlugin 已在 mutation session 区间内,
|
|
@@ -1352,33 +1278,6 @@ async function upgradePluginLocked(pkg, cfg = {}, opts = {}, deps) {
|
|
|
1352
1278
|
* (owner--name 等),主清单 miss 时按同一 id 查社区目录。社区清单未启用/不可用/加载
|
|
1353
1279
|
* 异常/未命中 → undefined,由调用方统一报「registry 中没有该条目」。
|
|
1354
1280
|
*/
|
|
1355
|
-
async function findCommunityInstallEntry(id, cfg, opts, deps) {
|
|
1356
|
-
try {
|
|
1357
|
-
const loaded = await (deps?.fetchCommunityCatalog ?? defaultFetchCommunityCatalog)(cfg, { namespace: opts.namespace ?? 'host', signal: opts.signal, profile: opts.profile });
|
|
1358
|
-
if (!loaded.catalog)
|
|
1359
|
-
return undefined;
|
|
1360
|
-
return adaptCommunityCatalog(loaded.catalog).entries.find((e) => e.id === id);
|
|
1361
|
-
}
|
|
1362
|
-
catch {
|
|
1363
|
-
return undefined;
|
|
1364
|
-
}
|
|
1365
|
-
}
|
|
1366
|
-
/**
|
|
1367
|
-
* 社区条目升级查找(0.5.1):主清单 miss 时按同一 matchInstalledByEntry 语义查社区目录。
|
|
1368
|
-
* 社区清单未启用/不可用/加载异常/未命中 → undefined,由调用方统一报「不是经 dsh-m 收录」。
|
|
1369
|
-
*/
|
|
1370
|
-
async function findCommunityUpgradeEntry(target, cfg, opts, deps) {
|
|
1371
|
-
try {
|
|
1372
|
-
const loaded = await (deps?.fetchCommunityCatalog ?? defaultFetchCommunityCatalog)(cfg, { namespace: opts.namespace ?? 'host', signal: opts.signal, profile: opts.profile });
|
|
1373
|
-
if (!loaded.catalog)
|
|
1374
|
-
return undefined;
|
|
1375
|
-
const adapted = adaptCommunityCatalog(loaded.catalog);
|
|
1376
|
-
return adapted.entries.find((e) => matchInstalledByEntry(e, [target]));
|
|
1377
|
-
}
|
|
1378
|
-
catch {
|
|
1379
|
-
return undefined;
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
1281
|
/** 自升级(0.5.0 收编:host-api 直调事务的旁路封死):npmLatest + integrity fail-closed + installEntryLocked(session/守卫经 installEntry)。 */
|
|
1383
1282
|
export async function selfUpgrade(pkgName, currentVersion, cfg = {}, opts = {}, deps) {
|
|
1384
1283
|
return withMutationSession(async () => {
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { adaptCommunityCatalog } from './community-adapter.js';
|
|
2
|
+
// ---------- 迁入:COMMUNITY_CATEGORY_LABELS(标签单一事实源,0.7.0 Task 4) ----------
|
|
3
|
+
/**
|
|
4
|
+
* 上游 23 个分类 id 的全量中文标签(以 fixture `categories` 键为准逐条核对)。
|
|
5
|
+
* 0.7.0 Task 4 改名导出 + 作为标签单一事实源:summary 层附带(communitySummary)、
|
|
6
|
+
* GUI zoneChips 消费服务端数据,客户端内嵌副本(market-state.js)随 Task 8 删除。
|
|
7
|
+
* 0.9.68 随 communitySummary 迁入本模块(ADR-0016 R1-2:不迁则环只是从
|
|
8
|
+
* market↔community 平移为 merged-market↔community);community.ts 原位 re-export。
|
|
9
|
+
*/
|
|
10
|
+
export const COMMUNITY_CATEGORY_LABELS = {
|
|
11
|
+
agi: 'AGI 架构探索',
|
|
12
|
+
ui: 'UI 增强',
|
|
13
|
+
usage: '用量与计费',
|
|
14
|
+
theme: '主题与外观',
|
|
15
|
+
model: '模型与账号接入',
|
|
16
|
+
identity: '身份与通信',
|
|
17
|
+
session: '会话与消息',
|
|
18
|
+
memory: '记忆',
|
|
19
|
+
tools: '工具与能力',
|
|
20
|
+
wsl: 'WSL 与 Windows 互操作',
|
|
21
|
+
browser: '浏览器与网页',
|
|
22
|
+
vision: '视觉与多模态',
|
|
23
|
+
voice: '语音与音频',
|
|
24
|
+
docs: '文档与渲染',
|
|
25
|
+
skill: '技能包',
|
|
26
|
+
workflow: '工作流与自动化',
|
|
27
|
+
git: 'Git 与代码评审',
|
|
28
|
+
notify: '通知与集成',
|
|
29
|
+
dev: '开发与运行时',
|
|
30
|
+
security: '安全与权限',
|
|
31
|
+
remote: '远程与移动端',
|
|
32
|
+
market: '插件市场与管理',
|
|
33
|
+
fun: '娱乐',
|
|
34
|
+
};
|
|
35
|
+
// ---------- 迁入:deadlineRace(communityOutcome 的唯一 await 依赖;market.ts 反向值导入) ----------
|
|
36
|
+
export function deadlineRace(task, ms) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const timer = setTimeout(() => resolve('deadline'), Math.max(0, ms));
|
|
39
|
+
task.then((value) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
resolve(value);
|
|
42
|
+
}, (err) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
reject(err);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// ---------- 迁入:matchInstalledByEntry(0.9.8 导出:desktopUpgrade 与 web upgradePlugin 同语义) ----------
|
|
49
|
+
/** 「已装 ↔ 收录条目」匹配三准则:npm 名对 pkg / npm 名对 name / github spec 前缀(source 守卫)。 */
|
|
50
|
+
export function matchInstalledByEntry(entry, installed) {
|
|
51
|
+
return installed.find((it) => {
|
|
52
|
+
if (entry.npm && it.pkg === entry.npm)
|
|
53
|
+
return true;
|
|
54
|
+
if (entry.npm && it.name === entry.npm)
|
|
55
|
+
return true;
|
|
56
|
+
if (entry.github && it.source === 'github') {
|
|
57
|
+
const m = /^github:([^#]+)/.exec(it.spec);
|
|
58
|
+
if (m && m[1] === entry.github)
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 合并去重(Q41):社区条目依次与主清单的 npm 名集 / github owner-repo 集 / id 集比对,
|
|
66
|
+
* 任一相撞即让位(每条只计一次);社区内部 npm 名重复同样让位(首条优先)。
|
|
67
|
+
* 主清单恒优先——让位只影响社区条目,主条目原样保留。
|
|
68
|
+
*/
|
|
69
|
+
export function mergeRegistries(primary, community) {
|
|
70
|
+
const primaryNpm = new Set(primary.filter((e) => e.npm).map((e) => e.npm));
|
|
71
|
+
const primaryGithub = new Set(primary.filter((e) => e.github).map((e) => e.github));
|
|
72
|
+
const primaryIds = new Set(primary.map((e) => e.id));
|
|
73
|
+
const displaced = [];
|
|
74
|
+
const kept = [];
|
|
75
|
+
const seenNpm = new Set();
|
|
76
|
+
for (const c of community) {
|
|
77
|
+
if (c.npm !== undefined) {
|
|
78
|
+
if (primaryNpm.has(c.npm) || seenNpm.has(c.npm)) {
|
|
79
|
+
displaced.push(c.id);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
seenNpm.add(c.npm);
|
|
83
|
+
}
|
|
84
|
+
if ((c.github !== undefined && primaryGithub.has(c.github)) || primaryIds.has(c.id)) {
|
|
85
|
+
displaced.push(c.id);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
kept.push(c);
|
|
89
|
+
}
|
|
90
|
+
// Q45:主清单置顶(组内原顺序)+ 社区按 30 天下载量降序(无数据按名称)
|
|
91
|
+
const sorted = [...kept].sort((a, b) => {
|
|
92
|
+
const da = a.downloads ?? -1;
|
|
93
|
+
const db = b.downloads ?? -1;
|
|
94
|
+
if (da !== db)
|
|
95
|
+
return db - da;
|
|
96
|
+
return a.name.localeCompare(b.name);
|
|
97
|
+
});
|
|
98
|
+
const warnings = displaced.length > 0 ? [`${displaced.length} 条社区条目与主清单重复,已让位(主清单恒优先)`] : [];
|
|
99
|
+
return { items: [...primary, ...sorted], displaced: displaced.length, warnings };
|
|
100
|
+
}
|
|
101
|
+
function communitySummary(state, counts, extraWarnings, categoryLabelsEn) {
|
|
102
|
+
const unavailableLike = state.status === 'disabled' || state.status === 'unavailable';
|
|
103
|
+
return {
|
|
104
|
+
enabled: state.enabled,
|
|
105
|
+
status: state.status,
|
|
106
|
+
version: state.version,
|
|
107
|
+
checkedAt: state.checkedAt,
|
|
108
|
+
fetchedAt: state.fetchedAt,
|
|
109
|
+
route: state.route,
|
|
110
|
+
acceptedCount: unavailableLike ? 0 : (counts.acceptedCount ?? 0),
|
|
111
|
+
upstreamCount: unavailableLike ? (state.status === 'disabled' ? null : 0) : (counts.upstreamCount ?? state.count),
|
|
112
|
+
displaced: unavailableLike ? 0 : (counts.displaced ?? 0),
|
|
113
|
+
skippedDirty: unavailableLike ? 0 : (counts.skippedDirty ?? 0),
|
|
114
|
+
skippedSubpathNoNpm: unavailableLike ? 0 : (counts.skippedSubpathNoNpm ?? 0),
|
|
115
|
+
errors: [...state.errors],
|
|
116
|
+
warnings: [...state.warnings, ...extraWarnings],
|
|
117
|
+
// 标签单一事实源(0.7.0 Task 4):disabled/skipped 下无社区数据语义,不携带;
|
|
118
|
+
// EN 标签(i18n)随行携带,仅在调用方传入非空映射时出现(缺省 = 上游无 categories 数据)
|
|
119
|
+
...(state.status !== 'disabled' && state.status !== 'skipped'
|
|
120
|
+
? {
|
|
121
|
+
categoryLabels: { ...COMMUNITY_CATEGORY_LABELS },
|
|
122
|
+
...(categoryLabelsEn && Object.keys(categoryLabelsEn).length > 0
|
|
123
|
+
? { categoryLabelsEn: { ...categoryLabelsEn } }
|
|
124
|
+
: {}),
|
|
125
|
+
}
|
|
126
|
+
: {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function disabledCommunitySummary() {
|
|
130
|
+
return communitySummary({ enabled: false, status: 'disabled', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: [], warnings: [] }, {}, []);
|
|
131
|
+
}
|
|
132
|
+
/** 查询层主动跳过(0.7.0 Task 2:source='primary'):本次未加载社区层,非配置关闭、非故障。 */
|
|
133
|
+
function skippedCommunitySummary() {
|
|
134
|
+
return communitySummary({ enabled: true, status: 'skipped', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: [], warnings: [] }, {}, []);
|
|
135
|
+
}
|
|
136
|
+
function communityTimeoutSummary() {
|
|
137
|
+
return communitySummary({ enabled: true, status: 'unavailable', version: null, checkedAt: null, fetchedAt: null, route: null, count: 0, errors: ['社区目录状态获取超时,可稍后刷新'], warnings: [] }, {}, []);
|
|
138
|
+
}
|
|
139
|
+
/** 导出(0.9.68 迁移配套):market.ts 的 registry-deadline 分支直接取超时 summary(该分支社区 task 未启动)。 */
|
|
140
|
+
export { communityTimeoutSummary };
|
|
141
|
+
// ---------- 迁入:isCommunityEntry ----------
|
|
142
|
+
export function isCommunityEntry(entry) {
|
|
143
|
+
return entry.descriptionEn !== undefined;
|
|
144
|
+
}
|
|
145
|
+
/** 单域首现索引:每键取序最前(min-ord)条目——等价于在该域上 find(matchInstalledByEntry) 的「第一条任一准则命中」。 */
|
|
146
|
+
function buildMatchIndex(domain) {
|
|
147
|
+
const byNpm = new Map();
|
|
148
|
+
const byGithub = new Map();
|
|
149
|
+
const byId = new Map();
|
|
150
|
+
domain.forEach((entry, ord) => {
|
|
151
|
+
const slot = { entry, ord };
|
|
152
|
+
if (entry.npm !== undefined && !byNpm.has(entry.npm))
|
|
153
|
+
byNpm.set(entry.npm, slot);
|
|
154
|
+
if (entry.github !== undefined && !byGithub.has(entry.github))
|
|
155
|
+
byGithub.set(entry.github, slot);
|
|
156
|
+
if (!byId.has(entry.id))
|
|
157
|
+
byId.set(entry.id, slot);
|
|
158
|
+
});
|
|
159
|
+
return { byNpm, byGithub, byId };
|
|
160
|
+
}
|
|
161
|
+
const GITHUB_SPEC_RE = /^github:([^#]+)/;
|
|
162
|
+
/** 三候选(npm[pkg] / npm[name] / github[spec](仅 source==='github'))取域序最前者胜——与 matchInstalledByEntry 逐点等价。 */
|
|
163
|
+
function lookupInIndex(index, installed) {
|
|
164
|
+
const candidates = [];
|
|
165
|
+
const byPkg = index.byNpm.get(installed.pkg);
|
|
166
|
+
if (byPkg)
|
|
167
|
+
candidates.push(byPkg);
|
|
168
|
+
const byName = index.byNpm.get(installed.name);
|
|
169
|
+
if (byName)
|
|
170
|
+
candidates.push(byName);
|
|
171
|
+
if (installed.source === 'github') {
|
|
172
|
+
const m = GITHUB_SPEC_RE.exec(installed.spec);
|
|
173
|
+
const byRepo = m ? index.byGithub.get(m[1]) : undefined;
|
|
174
|
+
if (byRepo)
|
|
175
|
+
candidates.push(byRepo);
|
|
176
|
+
}
|
|
177
|
+
if (candidates.length === 0)
|
|
178
|
+
return undefined;
|
|
179
|
+
let best = candidates[0];
|
|
180
|
+
for (let i = 1; i < candidates.length; i++) {
|
|
181
|
+
if (candidates[i].ord < best.ord)
|
|
182
|
+
best = candidates[i];
|
|
183
|
+
}
|
|
184
|
+
return best.entry;
|
|
185
|
+
}
|
|
186
|
+
const generations = new Map();
|
|
187
|
+
const GENERATION_CAP = 4;
|
|
188
|
+
const mergedStats = { builds: 0, hits: 0, evictions: 0 };
|
|
189
|
+
/** 测试钩子:清 L2 store 与 stats(latest-cache resetLatestCacheForTest 同款先例;测试隔离用)。 */
|
|
190
|
+
export function _resetMergedMarketForTests() {
|
|
191
|
+
generations.clear();
|
|
192
|
+
Object.assign(mergedStats, { builds: 0, hits: 0, evictions: 0 });
|
|
193
|
+
}
|
|
194
|
+
/** 测试钩子:builds/hits/evictions 计数(验收断言①②③的机器可断言信号)。 */
|
|
195
|
+
export function _mergedMarketStatsForTests() {
|
|
196
|
+
return { ...mergedStats };
|
|
197
|
+
}
|
|
198
|
+
function rememberGeneration(key, gen) {
|
|
199
|
+
if (!generations.has(key) && generations.size >= GENERATION_CAP) {
|
|
200
|
+
const oldest = generations.keys().next().value;
|
|
201
|
+
if (oldest !== undefined) {
|
|
202
|
+
generations.delete(oldest);
|
|
203
|
+
mergedStats.evictions += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
generations.set(key, gen);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 身份键派生(探测驱动,R1-12 等价性:configuredAddress 即规范化地址,registry cacheKey
|
|
210
|
+
* 是它的纯函数——default→DEFAULT_CACHE_KEY、否则 stableKey(url))。调用点必须在
|
|
211
|
+
* 全部 await 完成之后(同步临界区起点)。
|
|
212
|
+
*/
|
|
213
|
+
function identityKey(namespace, profile, cfg, registry, state) {
|
|
214
|
+
const pin = typeof cfg.communityCatalogPin === 'string' ? cfg.communityCatalogPin.trim() : '';
|
|
215
|
+
return [
|
|
216
|
+
namespace,
|
|
217
|
+
profile,
|
|
218
|
+
registry.configuredAddress,
|
|
219
|
+
registry.fetchedAt ?? 'never',
|
|
220
|
+
state.version ?? 'off',
|
|
221
|
+
pin,
|
|
222
|
+
cfg.communityCatalog === false ? 'off' : 'on',
|
|
223
|
+
].join('§');
|
|
224
|
+
}
|
|
225
|
+
/** 建代(全同步——adapt → merge → 双域索引;禁止引入 await:同步临界区不变量)。 */
|
|
226
|
+
function buildGeneration(primary, catalog) {
|
|
227
|
+
const adapted = adaptCommunityCatalog(catalog);
|
|
228
|
+
const merge = mergeRegistries(primary, adapted.entries);
|
|
229
|
+
const categoryLabelsEn = Object.fromEntries(Object.entries(catalog.categories)
|
|
230
|
+
.map(([id, c]) => [id, typeof c?.en === 'string' && c.en !== '' ? c.en : ''])
|
|
231
|
+
.filter(([, en]) => en !== ''));
|
|
232
|
+
return {
|
|
233
|
+
merged: merge.items,
|
|
234
|
+
counts: {
|
|
235
|
+
acceptedCount: adapted.entries.length,
|
|
236
|
+
upstreamCount: catalog.plugins.length,
|
|
237
|
+
displaced: merge.displaced,
|
|
238
|
+
skippedDirty: adapted.skippedDirty,
|
|
239
|
+
skippedSubpathNoNpm: adapted.skippedSubpathNoNpm,
|
|
240
|
+
},
|
|
241
|
+
adaptWarnings: [...adapted.warnings, ...merge.warnings],
|
|
242
|
+
categoryLabelsEn,
|
|
243
|
+
mergedIndex: buildMatchIndex(merge.items),
|
|
244
|
+
fullIndex: buildMatchIndex([...primary, ...adapted.entries]),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// ---------- 迁入:communityOutcome(原样、无 memo;getCommunitySummary 与 registry-deadline 降级路径专用) ----------
|
|
248
|
+
/**
|
|
249
|
+
* 社区 loader waiter 收敛(v9/v10 waiter-scoped 契约):共享 flight 不接收调用者 deadline,
|
|
250
|
+
* 本函数作为 waiter 用剩余 deadline race 自己的等待;到点只结束本 waiter(summary 标超时),
|
|
251
|
+
* 共享 flight 照常继续。source='primary'/未启用 → loader 零调用(task 传 null → skipped;
|
|
252
|
+
* 配置关闭走真任务的 disabled 分支,与跳过语义分离)。
|
|
253
|
+
* 0.9.68 自 market.ts 迁入(ADR-0016):签名与行为原样;community.ts getCommunitySummary
|
|
254
|
+
* 与 listInstalledWithMeta 的 registry-deadline 降级路径继续消费本无 memo 形态。
|
|
255
|
+
*/
|
|
256
|
+
export async function communityOutcome(task, deadlineAt, primary) {
|
|
257
|
+
if (!task)
|
|
258
|
+
return { summary: skippedCommunitySummary(), merged: primary };
|
|
259
|
+
let loaded;
|
|
260
|
+
try {
|
|
261
|
+
loaded = await deadlineRace(task, deadlineAt - Date.now());
|
|
262
|
+
}
|
|
263
|
+
finally {
|
|
264
|
+
// race 输出后共享 promise 若仍悬挂(deadline 先到),收尾防 unhandled rejection
|
|
265
|
+
void task.catch(() => undefined);
|
|
266
|
+
}
|
|
267
|
+
if (loaded === 'deadline')
|
|
268
|
+
return { summary: communityTimeoutSummary(), merged: primary };
|
|
269
|
+
const state = loaded.state;
|
|
270
|
+
if (state.status === 'disabled')
|
|
271
|
+
return { summary: disabledCommunitySummary(), merged: primary };
|
|
272
|
+
if (state.status === 'unavailable' || !loaded.catalog) {
|
|
273
|
+
return {
|
|
274
|
+
summary: communitySummary(state, { acceptedCount: 0, upstreamCount: 0, displaced: 0, skippedDirty: 0, skippedSubpathNoNpm: 0 }, []),
|
|
275
|
+
merged: primary,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
const adapted = adaptCommunityCatalog(loaded.catalog);
|
|
279
|
+
const merge = mergeRegistries(primary, adapted.entries);
|
|
280
|
+
// EN 分类标签(i18n):直接取上游目录 categories.en——上游新增分类自动跟进,
|
|
281
|
+
// 缺 en 的 id 不进映射(客户端按语言取用并回退中文标签,不在此处手养第二张表)
|
|
282
|
+
const categoryLabelsEn = Object.fromEntries(Object.entries(loaded.catalog.categories)
|
|
283
|
+
.map(([id, c]) => [id, typeof c?.en === 'string' && c.en !== '' ? c.en : ''])
|
|
284
|
+
.filter(([, en]) => en !== ''));
|
|
285
|
+
const summary = communitySummary(state, {
|
|
286
|
+
acceptedCount: adapted.entries.length,
|
|
287
|
+
upstreamCount: loaded.catalog.plugins.length,
|
|
288
|
+
displaced: merge.displaced,
|
|
289
|
+
skippedDirty: adapted.skippedDirty,
|
|
290
|
+
skippedSubpathNoNpm: adapted.skippedSubpathNoNpm,
|
|
291
|
+
}, [...adapted.warnings, ...merge.warnings], categoryLabelsEn);
|
|
292
|
+
return { summary, merged: merge.items };
|
|
293
|
+
}
|
|
294
|
+
/** 导出(0.9.68 接线配套):给无 memo 的 communityOutcome 结果挂兜底 lookup(线性扫描,今日语义原样)——供降级路径(registry-deadline / settings 摘要等冷路径)取得与 mergedOutcome 同形的返回值。 */
|
|
295
|
+
export function withFallbackLookups(outcome) {
|
|
296
|
+
return outcomeWithLookups(outcome, null, null);
|
|
297
|
+
}
|
|
298
|
+
// ↑ 兜底域 = outcome.merged(透传形态下为 primary-only,不含 displaced 让位条目)——只保证读路径
|
|
299
|
+
// lookupInstalled 等价;不得用于安装/升级路径(那里需要 findById/lookupInstalledAll 的全量域语义)。
|
|
300
|
+
// 现状唯一消费点 listInstalledWithMeta 的 registry-deadline 臂在消费 lookups 前即早返回,零行为影响。
|
|
301
|
+
function outcomeWithLookups(outcome, mergedIndex, fullIndex) {
|
|
302
|
+
return {
|
|
303
|
+
...outcome,
|
|
304
|
+
lookupInstalled: (installed) => (mergedIndex ? lookupInIndex(mergedIndex, installed) : matchInstalledByEntryFallback(outcome.merged, installed)),
|
|
305
|
+
findById: (id) => (fullIndex ? fullIndex.byId.get(id)?.entry : outcome.merged.find((e) => e.id === id)),
|
|
306
|
+
lookupInstalledAll: (installed) => (fullIndex ? lookupInIndex(fullIndex, installed) : matchInstalledByEntryFallback(outcome.merged, installed)),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/** 透传形态(无代)的兜底:线性扫描(今日语义原样;仅 primary-only 域,量小)。 */
|
|
310
|
+
function matchInstalledByEntryFallback(domain, installed) {
|
|
311
|
+
return domain.find((e) => matchInstalledByEntry(e, [installed]) !== undefined);
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* memoized 主入口:内部 resolve communityTask → 派生身份键 → 查/建代。
|
|
315
|
+
* 落代/读代:仅 community 正常 resolve 且 catalog 在场(ready|stale——同 version 内容恒等);
|
|
316
|
+
* deadline 逃逸 / unavailable / disabled / task=null → 透传现算(不读不写 store)。
|
|
317
|
+
* reject 语义:透传(与 communityOutcome 同构),不做内部吞咽。
|
|
318
|
+
*/
|
|
319
|
+
export async function mergedOutcome(input) {
|
|
320
|
+
const { registry, communityTask, deadlineAt } = input;
|
|
321
|
+
const primary = registry.registry.plugins;
|
|
322
|
+
if (!communityTask) {
|
|
323
|
+
// source='primary':loader 零调用,透传(今日语义);主清单域小,索引免建
|
|
324
|
+
return outcomeWithLookups(await communityOutcome(null, deadlineAt, primary), null, null);
|
|
325
|
+
}
|
|
326
|
+
let loaded;
|
|
327
|
+
try {
|
|
328
|
+
loaded = await deadlineRace(communityTask, deadlineAt - Date.now());
|
|
329
|
+
}
|
|
330
|
+
finally {
|
|
331
|
+
void communityTask.catch(() => undefined);
|
|
332
|
+
}
|
|
333
|
+
// ---- 以下透传形态:不读不写 store ----
|
|
334
|
+
if (loaded === 'deadline') {
|
|
335
|
+
return outcomeWithLookups({ summary: communityTimeoutSummary(), merged: primary }, null, null);
|
|
336
|
+
}
|
|
337
|
+
const state = loaded.state;
|
|
338
|
+
if (state.status === 'disabled') {
|
|
339
|
+
return outcomeWithLookups({ summary: disabledCommunitySummary(), merged: primary }, null, null);
|
|
340
|
+
}
|
|
341
|
+
if (state.status === 'unavailable' || !loaded.catalog) {
|
|
342
|
+
return outcomeWithLookups({ summary: communitySummary(state, { acceptedCount: 0, upstreamCount: 0, displaced: 0, skippedDirty: 0, skippedSubpathNoNpm: 0 }, []), merged: primary }, null, null);
|
|
343
|
+
}
|
|
344
|
+
// ---- 同步临界区起点:键派生 → 查 store → 建代 → set(禁止 await) ----
|
|
345
|
+
const key = identityKey(input.namespace, input.profile, input.cfg, registry, state);
|
|
346
|
+
const hit = generations.get(key);
|
|
347
|
+
if (hit) {
|
|
348
|
+
mergedStats.hits += 1;
|
|
349
|
+
return outcomeWithLookups({ summary: communitySummary(state, hit.counts, hit.adaptWarnings, hit.categoryLabelsEn), merged: hit.merged }, hit.mergedIndex, hit.fullIndex);
|
|
350
|
+
}
|
|
351
|
+
const gen = buildGeneration(primary, loaded.catalog);
|
|
352
|
+
mergedStats.builds += 1;
|
|
353
|
+
rememberGeneration(key, gen);
|
|
354
|
+
return outcomeWithLookups({ summary: communitySummary(state, gen.counts, gen.adaptWarnings, gen.categoryLabelsEn), merged: gen.merged }, gen.mergedIndex, gen.fullIndex);
|
|
355
|
+
// ---- 同步临界区终点 ----
|
|
356
|
+
}
|
package/lib/core/profile-ops.js
CHANGED
|
@@ -22,9 +22,10 @@
|
|
|
22
22
|
* 不写 allowBuilds、不跑 dsh-m 自实现 pnpm 编排、服务缺席一律结构化拒绝。
|
|
23
23
|
*/
|
|
24
24
|
import { withMutationSession, resolveRegistryEntry, matchInstalledByEntry } from './market.js';
|
|
25
|
+
import { mergedOutcome } from './merged-market.js';
|
|
26
|
+
import { WEB_PROFILE } from './env.js';
|
|
25
27
|
import { loadRegistry as defaultLoadRegistry } from './registry.js';
|
|
26
28
|
import { fetchCommunityCatalog as defaultFetchCommunityCatalog } from './community.js';
|
|
27
|
-
import { adaptCommunityCatalog } from './community-adapter.js';
|
|
28
29
|
import { listInstalledPlugins as defaultListInstalled } from './installed.js';
|
|
29
30
|
import { precheckNpmCompat, IncompatibleError } from './compat-check.js';
|
|
30
31
|
import { npmLatest as defaultNpmLatest, githubLatestTag as defaultGithubLatestTag } from './versions.js';
|
|
@@ -32,6 +33,8 @@ import { classifyUpgradeActivation } from './activation.js';
|
|
|
32
33
|
import { togglePlugin, ToggleError } from './toggle.js';
|
|
33
34
|
import { describeReleaseAgeFailure, releaseAgePrecheck } from './release-age.js';
|
|
34
35
|
import { governExcludeBlock, registerExclusion } from './exclude-governance.js';
|
|
36
|
+
/** desktop 升级社区兜底的等待上限(对齐 market.ts DEFAULT_DEADLINE_MS 量级;仅本路径使用)。 */
|
|
37
|
+
const DEFAULT_COMMUNITY_DEADLINE_MS = 60_000;
|
|
35
38
|
// 0.9.19:等待期失败翻译移入 release-age.ts(describeReleaseAgeFailure)——全量违规解析 +
|
|
36
39
|
// 目标/旁包分述。0.9.10 版只取第一条违规并把旁包名字当成本次目标、给「改用 DSH Web」的
|
|
37
40
|
// 失配指引(本机 quota-watch 0.1.15 事件实证:被拦的是旁包 dsh-m@0.9.18,文案却让用户等
|
|
@@ -411,12 +414,20 @@ async function desktopUpgradeLocked(pkg, cfg, opts, deps) {
|
|
|
411
414
|
throw new DesktopOpsError('install-refused', `desktop profile 未安装该插件: ${pkg}(升级目标必须是已装插件)`);
|
|
412
415
|
let entry = loaded.registry.plugins.find((e) => matchInstalledByEntry(e, [target]));
|
|
413
416
|
if (!entry) {
|
|
414
|
-
// 0.5.1 同语义:主清单 miss
|
|
417
|
+
// 0.5.1 同语义:主清单 miss 查社区目录(合并市场安装的社区条目同样可升级)。
|
|
418
|
+
// 0.9.68(ADR-0016 / R2-3):经物化合并市场全量域 lookupInstalledAll(含 displaced 让位条目,
|
|
419
|
+
// min-ord = 今日「先主清单后社区 adapt 全量」合成语义);reject 透传由窄 catch 吞为 miss。
|
|
415
420
|
try {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
:
|
|
421
|
+
const communityTask = (deps.fetchCommunityCatalog ?? defaultFetchCommunityCatalog)(cfg, { namespace: opts.namespace ?? 'host', signal: opts.signal, profile: opts.profile });
|
|
422
|
+
const outcome = await mergedOutcome({
|
|
423
|
+
namespace: opts.namespace ?? 'host',
|
|
424
|
+
profile: opts.profile ?? WEB_PROFILE,
|
|
425
|
+
cfg,
|
|
426
|
+
registry: loaded,
|
|
427
|
+
communityTask,
|
|
428
|
+
deadlineAt: Date.now() + DEFAULT_COMMUNITY_DEADLINE_MS,
|
|
429
|
+
});
|
|
430
|
+
entry = outcome.lookupInstalledAll(target);
|
|
420
431
|
}
|
|
421
432
|
catch {
|
|
422
433
|
entry = undefined;
|
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -225,6 +225,17 @@
|
|
|
225
225
|
"npm": "dsh-chat-manager",
|
|
226
226
|
"github": "WSL043/dsh-chat-manager",
|
|
227
227
|
"homepage": "https://github.com/WSL043/dsh-chat-manager"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"id": "dsh-mermaid",
|
|
231
|
+
"name": "DSH Mermaid",
|
|
232
|
+
"description": "Mermaid 图表渲染:代码块转 SVG,支持源码切换、全屏缩放与导出。",
|
|
233
|
+
"category": "watchlist",
|
|
234
|
+
"tags": ["图表", "可视化", "流程图", "SVG"],
|
|
235
|
+
"source": "npm",
|
|
236
|
+
"npm": "dsh-mermaid",
|
|
237
|
+
"github": "MrmoLabs/dsh-mermaid",
|
|
238
|
+
"homepage": "https://github.com/MrmoLabs/dsh-mermaid"
|
|
228
239
|
}
|
|
229
240
|
]
|
|
230
241
|
}
|