dsh-m 0.9.67 → 0.9.69
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 +32 -0
- package/lib/client.js +332 -2
- 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
|
@@ -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
|
}
|