dsh-plugin-manager-companion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +144 -0
- package/README.md +142 -0
- package/cordis.patch.yml +9 -0
- package/dist/about.d.ts +77 -0
- package/dist/about.js +179 -0
- package/dist/cli.d.ts +226 -0
- package/dist/cli.js +856 -0
- package/dist/client/AboutPage.d.ts +75 -0
- package/dist/client/ConsolePage.d.ts +79 -0
- package/dist/client/KindsPage.d.ts +21 -0
- package/dist/client/MarketplacePage.d.ts +36 -0
- package/dist/client/OfficialSlots.d.ts +35 -0
- package/dist/client/UpgradeRow.d.ts +108 -0
- package/dist/client/index.d.ts +26 -0
- package/dist/client/locales.d.ts +475 -0
- package/dist/client/pmSelect.d.ts +38 -0
- package/dist/client/shared.d.ts +928 -0
- package/dist/client/upgradeView.d.ts +278 -0
- package/dist/client/wire.d.ts +401 -0
- package/dist/client.js +9194 -0
- package/dist/diagnostics.d.ts +332 -0
- package/dist/diagnostics.js +2631 -0
- package/dist/envManager.d.ts +1047 -0
- package/dist/envManager.js +3214 -0
- package/dist/fix.d.ts +60 -0
- package/dist/fix.js +168 -0
- package/dist/guard.d.ts +133 -0
- package/dist/guard.js +232 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +1150 -0
- package/dist/installSession.d.ts +111 -0
- package/dist/installSession.js +150 -0
- package/dist/kinds.d.ts +464 -0
- package/dist/kinds.js +1029 -0
- package/dist/marketView.d.ts +261 -0
- package/dist/marketView.js +406 -0
- package/dist/marketplace.d.ts +248 -0
- package/dist/marketplace.js +500 -0
- package/dist/match.d.ts +67 -0
- package/dist/match.js +203 -0
- package/dist/net.d.ts +108 -0
- package/dist/net.js +163 -0
- package/dist/official.d.ts +145 -0
- package/dist/official.js +205 -0
- package/dist/paths.d.ts +108 -0
- package/dist/paths.js +236 -0
- package/dist/presets.d.ts +299 -0
- package/dist/presets.js +578 -0
- package/dist/qualityGate.d.ts +66 -0
- package/dist/qualityGate.js +247 -0
- package/dist/rank.d.ts +88 -0
- package/dist/rank.js +164 -0
- package/dist/registry.d.ts +295 -0
- package/dist/registry.js +686 -0
- package/dist/rest.d.ts +122 -0
- package/dist/rest.js +219 -0
- package/dist/scan.d.ts +134 -0
- package/dist/scan.js +396 -0
- package/dist/settings.d.ts +447 -0
- package/dist/settings.js +263 -0
- package/dist/tags.d.ts +119 -0
- package/dist/tags.js +166 -0
- package/dist/tools.d.ts +131 -0
- package/dist/tools.js +377 -0
- package/dist/types.d.ts +651 -0
- package/dist/types.js +13 -0
- package/dist/upgrade.d.ts +428 -0
- package/dist/upgrade.js +1100 -0
- package/dist/upgradeView.d.ts +313 -0
- package/dist/upgradeView.js +273 -0
- package/docs/images/readme/01-console-health.png +0 -0
- package/docs/images/readme/02-console-envs.png +0 -0
- package/docs/images/readme/03-marketplace.png +0 -0
- package/docs/images/readme/04-official-plugin-page.png +0 -0
- package/package.json +104 -0
package/dist/settings.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本插件的配置面。
|
|
3
|
+
*
|
|
4
|
+
* 归属:A 类·重写(新代码;旧仓库把配置散在 profile 文件与 market 缓存里)。
|
|
5
|
+
* 官方复用:ctx.settings(SettingsProvider)—— 注册一个命名空间,由官方负责
|
|
6
|
+
* 校验、落盘、修订号与观察。
|
|
7
|
+
* 前提检查:用户明确要求"无需手动编辑配置文件"。官方 settings 服务正是这条
|
|
8
|
+
* 要求的原生实现;自己写 YAML 既违背要求也与官方并发写同一文件。
|
|
9
|
+
*
|
|
10
|
+
* 配置只描述**本插件自己的行为**,绝不描述环境状态:环境状态是事实,读出来
|
|
11
|
+
* 即可,不该由用户配置。
|
|
12
|
+
*/
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
/** 本插件在官方 settings 里的命名空间(小写连字符,官方校验)。 */
|
|
15
|
+
export const SETTINGS_NAMESPACE = 'plugin-manager-companion';
|
|
16
|
+
/**
|
|
17
|
+
* 试装配置的默认值。
|
|
18
|
+
*
|
|
19
|
+
* 单独一份(而不是只有 DEFAULT_CONFIG.trial):CompanionConfig.trial 是可选字段,
|
|
20
|
+
* 类型上它可能是 undefined;读配置的人都该拿这一份,不该拿"可能是 undefined 的默认值"。
|
|
21
|
+
*/
|
|
22
|
+
export const DEFAULT_TRIAL_CONFIG = {
|
|
23
|
+
// 试装默认关闭:它会在用户机器上真实装包并执行对方代码(DESIGN §5.2),必须显式开启。
|
|
24
|
+
enabled: false,
|
|
25
|
+
depth: 'auto',
|
|
26
|
+
baseline: true,
|
|
27
|
+
allowNetwork: true,
|
|
28
|
+
onFailure: 'block',
|
|
29
|
+
autoCleanup: true,
|
|
30
|
+
retentionDays: 14,
|
|
31
|
+
maxKept: 0,
|
|
32
|
+
};
|
|
33
|
+
/** 升级配置的默认值(单独一份:CompanionConfig.upgrade 是可选字段)。 */
|
|
34
|
+
export const DEFAULT_UPGRADE_CONFIG = {
|
|
35
|
+
autoCheck: true,
|
|
36
|
+
interval: 'daily',
|
|
37
|
+
registryUrl: '',
|
|
38
|
+
};
|
|
39
|
+
/** 默认配置:诊断全开、质量门拦截、市场启用、试装关闭、升级检查每天一次。 */
|
|
40
|
+
export const DEFAULT_CONFIG = {
|
|
41
|
+
diagnostics: {
|
|
42
|
+
dependency: true,
|
|
43
|
+
composition: true,
|
|
44
|
+
runtime: true,
|
|
45
|
+
consistency: true,
|
|
46
|
+
// 生态层要联网,默认关闭——诊断页在用户显式开启后才访问网络。
|
|
47
|
+
ecosystem: false,
|
|
48
|
+
},
|
|
49
|
+
qualityGate: { enabled: true, mode: 'block', allowlist: [] },
|
|
50
|
+
marketplace: { enabled: true, cacheTtlMinutes: 1440, timeoutMs: 15_000, indexUrl: '' },
|
|
51
|
+
trial: { ...DEFAULT_TRIAL_CONFIG },
|
|
52
|
+
upgrade: { ...DEFAULT_UPGRADE_CONFIG },
|
|
53
|
+
};
|
|
54
|
+
/** 试装的告知事实(口径:Linux x64 / Node 24 / DSH 0.1.6-alpha.2,headless 验证启动)。 */
|
|
55
|
+
export const TRIAL_DISCLOSURE = {
|
|
56
|
+
executesCandidateCode: true,
|
|
57
|
+
peakMemoryMiB: 161,
|
|
58
|
+
measurement: '实测口径:headless 验证启动的 maxrss 峰值 161 MiB,'
|
|
59
|
+
+ '在 Linux x64 / Node 24 / DSH 0.1.6-alpha.2 上量得;候选包自带的安装脚本会真的在你机器上执行。',
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* 官方 settings 的 schema。
|
|
63
|
+
*
|
|
64
|
+
* `.default()` 逐项给出,使旧配置文件在新增字段后仍能解析;未知字段由
|
|
65
|
+
* schemastery 丢弃,不会因为未来删字段而炸掉整份配置。
|
|
66
|
+
*/
|
|
67
|
+
export const ConfigSchema = z.object({
|
|
68
|
+
diagnostics: z.object({
|
|
69
|
+
dependency: z.boolean().default(true),
|
|
70
|
+
composition: z.boolean().default(true),
|
|
71
|
+
runtime: z.boolean().default(true),
|
|
72
|
+
consistency: z.boolean().default(true),
|
|
73
|
+
ecosystem: z.boolean().default(false),
|
|
74
|
+
}).default({ ...DEFAULT_CONFIG.diagnostics }),
|
|
75
|
+
qualityGate: z.object({
|
|
76
|
+
enabled: z.boolean().default(true),
|
|
77
|
+
mode: z.union([z.const('block'), z.const('warn')]).default('block'),
|
|
78
|
+
allowlist: z.array(z.string()).default([]),
|
|
79
|
+
}).default({
|
|
80
|
+
enabled: DEFAULT_CONFIG.qualityGate.enabled,
|
|
81
|
+
mode: DEFAULT_CONFIG.qualityGate.mode,
|
|
82
|
+
allowlist: [...DEFAULT_CONFIG.qualityGate.allowlist],
|
|
83
|
+
}),
|
|
84
|
+
marketplace: z.object({
|
|
85
|
+
enabled: z.boolean().default(true),
|
|
86
|
+
cacheTtlMinutes: z.number().min(1).max(10_080).default(1440),
|
|
87
|
+
timeoutMs: z.number().min(1_000).max(120_000).default(15_000),
|
|
88
|
+
indexUrl: z.string().default(''),
|
|
89
|
+
}).default({ ...DEFAULT_CONFIG.marketplace }),
|
|
90
|
+
// 注意:maxKept 用 0 = 不限而**不用 null**。实测(schemastery 3.18.2):
|
|
91
|
+
// 字段 schema 里含 z.const(null) 时,用户配置只写了别的字段,这个键会被整个丢掉
|
|
92
|
+
// (值变成 undefined,不是 null)——即"用户在设置页看不到的一个洞"。
|
|
93
|
+
// 也正因如此,读配置一律走 effectiveTrialConfig():schema 的默认值不是运行期保证。
|
|
94
|
+
trial: z.object({
|
|
95
|
+
enabled: z.boolean().default(false),
|
|
96
|
+
depth: z.union([z.const('auto'), z.const('shallow'), z.const('full')]).default('auto'),
|
|
97
|
+
baseline: z.boolean().default(true),
|
|
98
|
+
allowNetwork: z.boolean().default(true),
|
|
99
|
+
onFailure: z.union([z.const('block'), z.const('warn')]).default('block'),
|
|
100
|
+
autoCleanup: z.boolean().default(true),
|
|
101
|
+
retentionDays: z.natural().min(1).max(3_650).default(14),
|
|
102
|
+
maxKept: z.natural().max(1_000).default(0),
|
|
103
|
+
}).default({ ...DEFAULT_TRIAL_CONFIG }),
|
|
104
|
+
upgrade: z.object({
|
|
105
|
+
autoCheck: z.boolean().default(true),
|
|
106
|
+
interval: z.union([z.const('session'), z.const('6h'), z.const('daily'), z.const('manual')]).default('daily'),
|
|
107
|
+
registryUrl: z.string().default(''),
|
|
108
|
+
}).default({ ...DEFAULT_UPGRADE_CONFIG }),
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* 读试装配置:缺字段、字段类型不对、越界一律回落到默认值。
|
|
112
|
+
*
|
|
113
|
+
* 为什么需要它(不是多此一举):schema 的默认值只在"官方 settings 解析过这份配置"时成立。
|
|
114
|
+
* 事实是配置对象还会从别处来——测试与工具自己拼的 CompanionConfig 字面量、以及未来
|
|
115
|
+
* schemastery 行为变化(本文件里 maxKept 那条注释就是一个实测例子)。试装是**会写磁盘、会起进程**
|
|
116
|
+
* 的动作,任何一个字段读到 undefined 都必须变成"用安全默认值",不能变成"意外地开启/强制浅快照"。
|
|
117
|
+
*
|
|
118
|
+
* @param config - 任意形状的配置片段。
|
|
119
|
+
* @returns 补齐后的试装配置(全部字段都有确定值)。
|
|
120
|
+
*/
|
|
121
|
+
export function effectiveTrialConfig(config) {
|
|
122
|
+
const fallback = DEFAULT_TRIAL_CONFIG;
|
|
123
|
+
const raw = (config?.trial ?? {});
|
|
124
|
+
const bool = (value, byDefault) => typeof value === 'boolean' ? value : byDefault;
|
|
125
|
+
const count = (value, byDefault, min, max) => {
|
|
126
|
+
const parsed = typeof value === 'number' && Number.isFinite(value) ? Math.trunc(value) : byDefault;
|
|
127
|
+
return Math.min(max, Math.max(min, parsed));
|
|
128
|
+
};
|
|
129
|
+
return {
|
|
130
|
+
enabled: bool(raw.enabled, fallback.enabled),
|
|
131
|
+
depth: raw.depth === 'shallow' || raw.depth === 'full' ? raw.depth : 'auto',
|
|
132
|
+
baseline: bool(raw.baseline, fallback.baseline),
|
|
133
|
+
allowNetwork: bool(raw.allowNetwork, fallback.allowNetwork),
|
|
134
|
+
onFailure: raw.onFailure === 'warn' ? 'warn' : 'block',
|
|
135
|
+
autoCleanup: bool(raw.autoCleanup, fallback.autoCleanup),
|
|
136
|
+
retentionDays: count(raw.retentionDays, fallback.retentionDays, 1, 3_650),
|
|
137
|
+
maxKept: count(raw.maxKept, fallback.maxKept, 0, 1_000),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* 检查间隔对应的毫秒数;`null` = 仅手动(永不自动检查)。
|
|
142
|
+
*
|
|
143
|
+
* 纯函数、单一事实来源:引擎与界面用它算"要不要查",避免两处各写一份间隔表而漂移。
|
|
144
|
+
*
|
|
145
|
+
* @param interval - 配置里的间隔。
|
|
146
|
+
* @returns 毫秒;仅手动时为 null。
|
|
147
|
+
*/
|
|
148
|
+
export function upgradeIntervalMs(interval) {
|
|
149
|
+
switch (interval) {
|
|
150
|
+
case 'session': return 0;
|
|
151
|
+
case '6h': return 6 * 60 * 60 * 1000;
|
|
152
|
+
case 'manual': return null;
|
|
153
|
+
default: return 24 * 60 * 60 * 1000;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 读升级配置:缺字段、类型不对一律回落默认值(与 effectiveTrialConfig 同一纪律:
|
|
158
|
+
* schema 的默认值只在官方 settings 解析过那份配置时成立;这两个字段决定要不要出网)。
|
|
159
|
+
*
|
|
160
|
+
* @param config - 任意形状的配置片段。
|
|
161
|
+
* @returns 补齐后的升级配置。
|
|
162
|
+
*/
|
|
163
|
+
export function effectiveUpgradeConfig(config) {
|
|
164
|
+
const fallback = DEFAULT_UPGRADE_CONFIG;
|
|
165
|
+
const raw = (config?.upgrade ?? {});
|
|
166
|
+
const interval = raw.interval === 'session' || raw.interval === '6h' || raw.interval === 'manual'
|
|
167
|
+
? raw.interval
|
|
168
|
+
: 'daily';
|
|
169
|
+
return {
|
|
170
|
+
autoCheck: typeof raw.autoCheck === 'boolean' ? raw.autoCheck : fallback.autoCheck,
|
|
171
|
+
interval,
|
|
172
|
+
registryUrl: typeof raw.registryUrl === 'string' ? raw.registryUrl : fallback.registryUrl,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** 最近一次配置降级的事实;null = 配置面正常。 */
|
|
176
|
+
let lastConfigDegradation = null;
|
|
177
|
+
/**
|
|
178
|
+
* 配置面的当前状态。
|
|
179
|
+
*
|
|
180
|
+
* 为什么是模块级:装配层用 ctx.inject(['settings'], …) 在 settings 就绪时才注册,
|
|
181
|
+
* 命名空间冲突发生在那个回调里;而这件事实必须能被 probeOfficialCapabilities 读到、
|
|
182
|
+
* 进而出现在用户可见的 capabilities 里(不能只在日志里)。
|
|
183
|
+
*
|
|
184
|
+
* @returns 当前配置面状态。
|
|
185
|
+
*/
|
|
186
|
+
export function configState() {
|
|
187
|
+
if (lastConfigDegradation === null)
|
|
188
|
+
return { writable: true, reason: null, detail: null };
|
|
189
|
+
return { writable: false, reason: lastConfigDegradation.reason, detail: lastConfigDegradation.detail };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* 记录一次配置降级:写日志 + 留给 capabilities 呈现。
|
|
193
|
+
*
|
|
194
|
+
* @param ctx - host 上下文(用于日志)。
|
|
195
|
+
* @param reason - 稳定原因码。
|
|
196
|
+
* @param detail - 面向用户的原因。
|
|
197
|
+
*/
|
|
198
|
+
function noteConfigDegraded(ctx, reason, detail) {
|
|
199
|
+
lastConfigDegradation = { reason, detail };
|
|
200
|
+
ctx.logger?.warn?.('plugin-manager-companion: ' + detail);
|
|
201
|
+
}
|
|
202
|
+
/** 错误消息(本地小工具)。 */
|
|
203
|
+
function messageOf(error) {
|
|
204
|
+
return error instanceof Error ? error.message : String(error);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* 官方 settings 服务尚不可用时的**只读**降级句柄。
|
|
208
|
+
*
|
|
209
|
+
* 为什么需要它:settings 服务可能**晚于**本插件装配(服务挂载顺序不保证),
|
|
210
|
+
* 而配置句柄会被各模块长期持有。装配层先拿这个,等服务出现再换成真句柄。
|
|
211
|
+
*
|
|
212
|
+
* `update` 刻意**抛错**而不是静默返回默认值——静默丢写是最糟的失败形态:
|
|
213
|
+
* 用户改了配置、界面回显成功、实际什么都没发生(实测踩过,见 CONTEXT.md)。
|
|
214
|
+
*
|
|
215
|
+
* @returns 只读句柄;任何写入尝试都抛出可读错误。
|
|
216
|
+
*/
|
|
217
|
+
export function fallbackConfigHandle() {
|
|
218
|
+
return {
|
|
219
|
+
current: () => DEFAULT_CONFIG,
|
|
220
|
+
watch: () => () => { },
|
|
221
|
+
update: () => Promise.reject(new Error('配置服务(settings)尚未就绪;写入被拒绝而不是丢弃——请稍后重试')),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 注册本插件的 settings 命名空间。
|
|
226
|
+
*
|
|
227
|
+
* 官方 `register` 在命名空间已被占用时**抛错**。这里的立场是"任何宿主都能加载、
|
|
228
|
+
* 缺能力就如实降级":冲突时退回只读的默认配置句柄,并把事实登记给 capabilities,
|
|
229
|
+
* 而不是让整个插件装配失败(那会让用户看到一个装不上、也没有可读原因的插件)。
|
|
230
|
+
*
|
|
231
|
+
* 不静默:降级句柄的 `update` 照旧**抛错**而不是丢写入,且 configState() 会让
|
|
232
|
+
* capabilities.missing 里出现一条"配置不可写"的说明。
|
|
233
|
+
*
|
|
234
|
+
* @param ctx - 本插件的 host 上下文。
|
|
235
|
+
* @returns 配置句柄;settings 不可用或命名空间冲突时返回只读的默认配置句柄。
|
|
236
|
+
*/
|
|
237
|
+
export function registerConfig(ctx) {
|
|
238
|
+
const settings = ctx.get('settings');
|
|
239
|
+
if (settings === undefined) {
|
|
240
|
+
noteConfigDegraded(ctx, 'settings-missing', 'settings 服务未就绪,配置降级为只读——装配层应改用 ctx.inject(["settings"])');
|
|
241
|
+
return fallbackConfigHandle();
|
|
242
|
+
}
|
|
243
|
+
let scope;
|
|
244
|
+
try {
|
|
245
|
+
scope = settings.register(SETTINGS_NAMESPACE, ConfigSchema);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// 命名空间已被占用(同进程内另一个本插件实例):不抛穿装配,退回只读句柄并登记事实。
|
|
249
|
+
noteConfigDegraded(ctx, 'namespace-conflict', 'settings 命名空间 "' + SETTINGS_NAMESPACE + '" 已被占用,配置降级为只读(正在用默认值):'
|
|
250
|
+
+ messageOf(error));
|
|
251
|
+
return fallbackConfigHandle();
|
|
252
|
+
}
|
|
253
|
+
// 注册成功:清掉可能残留的降级事实(例如重试装配)。
|
|
254
|
+
lastConfigDegradation = null;
|
|
255
|
+
return {
|
|
256
|
+
current: () => scope.get(),
|
|
257
|
+
watch: (listener) => scope.watch((next) => { listener(next); }),
|
|
258
|
+
update: async (patch) => {
|
|
259
|
+
await scope.update(patch);
|
|
260
|
+
return scope.get();
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}
|
package/dist/tags.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tags.ts — 市场徽标模型与详情数据(纯函数,无 fs / 无 ctx / 无网络)。
|
|
3
|
+
*
|
|
4
|
+
* 归属:B 类·参考重写(旧 src/tags.ts 仅作意图参考,未复制代码)。
|
|
5
|
+
* 旧实现参考:dsh-web-plugin-manager/src/tags.ts(理解意图用,未复制代码)——它解决的是"卡片上的标签
|
|
6
|
+
* 按载荷顺序临时拼、顺序随实现漂移";本模块继续承担"一处有序、去重的唯一来源"。
|
|
7
|
+
* 官方复用:无(官方没有市场概念)。
|
|
8
|
+
* 前提检查:**旧实现在这里被政策整体取代**(docs/private/market-tags-policy.md,task-45 全量 13,998 条实测):
|
|
9
|
+
* 1) 旧实现的 status / verification / security 三类标签**从来没有数据源**(没有上游字段可映射);
|
|
10
|
+
* 2) 政策实测出上游真正有值且值得展示的是另外几件事:风险(risk_tier,100% 覆盖)、可装性
|
|
11
|
+
* (installable,12.4%)、编辑推荐(market_tags 的 community-pick,10.2%)、归档(archived,0.6%)、
|
|
12
|
+
* 分类(100%)、功能主题(过滤泛化词后 57.5% 有值);
|
|
13
|
+
* 3) 去重必须**跨来源按归一化值**做:实测 499 条(3.6%)把自己的 category 又写进 topics
|
|
14
|
+
* (liustack/modlens=vision、MemTensor/MemOS=memory…),只做来源内部去重挡不住这种"同一个词出现两次"。
|
|
15
|
+
*
|
|
16
|
+
* 顺序契约(重要性从高到低,渲染方按数组顺序画、绝不再排序):
|
|
17
|
+
* risk → caution → manual → pick → archived → category → topic
|
|
18
|
+
* 这份顺序就是政策 §3.2 的优先级;{@link BADGE_PRIORITY} 是它的机器可读形式。
|
|
19
|
+
*/
|
|
20
|
+
import type { MarketInstallable, MarketRiskFlag, MarketRiskTier } from './types.ts';
|
|
21
|
+
/** 徽标类别(按 priority 排列的顺序见 {@link BADGE_PRIORITY})。 */
|
|
22
|
+
export type MarketTagKind =
|
|
23
|
+
/** 上游风险等级 = risk(danger)。 */
|
|
24
|
+
'risk'
|
|
25
|
+
/** 上游风险等级 = caution(warning)。 */
|
|
26
|
+
| 'caution'
|
|
27
|
+
/** 上游标记需手动安装(installable = manual)。 */
|
|
28
|
+
| 'manual'
|
|
29
|
+
/** 上游编辑推荐(market_tags 含 community-pick)。 */
|
|
30
|
+
| 'pick'
|
|
31
|
+
/** 仓库已归档(archived)。 */
|
|
32
|
+
| 'archived'
|
|
33
|
+
/** 上游分类(兜底槽)。 */
|
|
34
|
+
| 'category'
|
|
35
|
+
/** 功能主题(最低优先级,最多 2 个)。 */
|
|
36
|
+
| 'topic';
|
|
37
|
+
/** 卡片徽标的优先级契约(政策 §3.2)。 */
|
|
38
|
+
export declare const BADGE_PRIORITY: readonly MarketTagKind[];
|
|
39
|
+
/** 与语言无关的语义色调;client 映射到官方 --dsw-* token。 */
|
|
40
|
+
export type MarketTagTone = 'neutral' | 'success' | 'warning' | 'danger';
|
|
41
|
+
/** 一个展示徽标。`value` 是原始值或数据本身,由 client 侧字典决定文案。 */
|
|
42
|
+
export interface MarketTag {
|
|
43
|
+
readonly kind: MarketTagKind;
|
|
44
|
+
readonly value: string;
|
|
45
|
+
readonly tone: MarketTagTone;
|
|
46
|
+
/** 完整的、未截断的文本(title 属性用)。 */
|
|
47
|
+
readonly title?: string;
|
|
48
|
+
}
|
|
49
|
+
/** 卡片徽标的默认上限:政策 §4「只能显示 3 个」的最小集合就是取前三个。 */
|
|
50
|
+
export declare const TAG_SLOT_LIMIT = 3;
|
|
51
|
+
/** 功能主题的默认上限:政策 §3.5(泛化词占 61.3%,只能当兜底槽)。 */
|
|
52
|
+
export declare const TOPIC_SLOT_LIMIT = 2;
|
|
53
|
+
/**
|
|
54
|
+
* 徽标构建器读取的字段(刻意保持窄接口)。
|
|
55
|
+
*
|
|
56
|
+
* 全部来自上游索引,**原样使用**:本模块不把 risk_flags 折算成分数、不把 stars 折算成质量,
|
|
57
|
+
* 也不发明"可疑/推荐"这类上游没有的结论。
|
|
58
|
+
*/
|
|
59
|
+
export interface MarketTagSource {
|
|
60
|
+
readonly category?: string;
|
|
61
|
+
readonly installable?: MarketInstallable;
|
|
62
|
+
readonly riskTier?: MarketRiskTier;
|
|
63
|
+
readonly marketTags?: readonly string[];
|
|
64
|
+
readonly archived?: boolean;
|
|
65
|
+
readonly topics?: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* 构建一个条目的徽标列表(**按政策**:优先级取前 N、跨来源按归一化值去重)。
|
|
69
|
+
*
|
|
70
|
+
* @param item - 上游字段。
|
|
71
|
+
* @param options - slotLimit(卡片默认 3)与 topicLimit(默认 2)。
|
|
72
|
+
* @returns 按 {@link BADGE_PRIORITY} 排好的徽标;调用方**不得**再排序。
|
|
73
|
+
*/
|
|
74
|
+
export declare function buildMarketTags(item: MarketTagSource, options?: {
|
|
75
|
+
readonly slotLimit?: number;
|
|
76
|
+
readonly topicLimit?: number;
|
|
77
|
+
}): MarketTag[];
|
|
78
|
+
/** 徽标的稳定 key(React key / 去重标识用)。 */
|
|
79
|
+
export declare function marketTagKey(tag: MarketTag): string;
|
|
80
|
+
/** 详情区的一项(政策 §3.1 的八项)。`key` 由 client 侧字典翻译,本模块不产出文案。 */
|
|
81
|
+
export interface MarketDetailItem {
|
|
82
|
+
/** 稳定 key,client 据此取字典(category / topics / riskFlags / verified / license / npm / version / kind)。 */
|
|
83
|
+
readonly key: string;
|
|
84
|
+
/** 原始值(未本地化):分类 id、主题列表、flag 摘要、校验证据串、许可证 id、包名、版本、形态。 */
|
|
85
|
+
readonly value: string;
|
|
86
|
+
/** 可点外链(只可能是上游给的 https 报告地址)。 */
|
|
87
|
+
readonly href?: string;
|
|
88
|
+
readonly tone: MarketTagTone;
|
|
89
|
+
}
|
|
90
|
+
/** 详情构建器读取的字段。 */
|
|
91
|
+
export interface MarketDetailSource extends MarketTagSource {
|
|
92
|
+
readonly kind?: string;
|
|
93
|
+
readonly license?: string;
|
|
94
|
+
readonly packageName?: string;
|
|
95
|
+
readonly latestVersion?: string;
|
|
96
|
+
readonly riskFlags?: readonly MarketRiskFlag[];
|
|
97
|
+
readonly reportUrl?: string;
|
|
98
|
+
readonly verifiedBy?: string;
|
|
99
|
+
readonly verifiedAt?: string;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* 构建详情区的条目(政策 §3.1:分类、功能主题、风险明细、验证证据(含报告外链)、许可证、npm 包名、版本、形态)。
|
|
103
|
+
*
|
|
104
|
+
* 顺序是"身份 → 上游结论 → 证据 → 元数据":用户从上往下读能先看到"这是什么",再看到"上游怎么判的"。
|
|
105
|
+
* 空值一律不产出条目(详情区不留空行)。
|
|
106
|
+
*
|
|
107
|
+
* @param item - 上游字段 + 少量本地派生字段(kind / packageName / latestVersion)。
|
|
108
|
+
* @returns 详情项;调用方按 key 取字典文案。
|
|
109
|
+
*/
|
|
110
|
+
export declare function buildMarketDetail(item: MarketDetailSource): MarketDetailItem[];
|
|
111
|
+
/**
|
|
112
|
+
* 统计每个上游分类的条目数,返回 JSON-safe 的 Record(写进 MarketplaceResult.categories)。
|
|
113
|
+
*
|
|
114
|
+
* key 的插入顺序 = 分类首次出现的顺序,同一份 listing 永远产出同一份对象。
|
|
115
|
+
* 分类为空的条目**不计入**任何桶——把"没分类"混进某个分类会让筛选器说谎。
|
|
116
|
+
*/
|
|
117
|
+
export declare function categoryCounts(items: readonly {
|
|
118
|
+
readonly category?: string;
|
|
119
|
+
}[]): Record<string, number>;
|
package/dist/tags.js
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tags.ts — 市场徽标模型与详情数据(纯函数,无 fs / 无 ctx / 无网络)。
|
|
3
|
+
*
|
|
4
|
+
* 归属:B 类·参考重写(旧 src/tags.ts 仅作意图参考,未复制代码)。
|
|
5
|
+
* 旧实现参考:dsh-web-plugin-manager/src/tags.ts(理解意图用,未复制代码)——它解决的是"卡片上的标签
|
|
6
|
+
* 按载荷顺序临时拼、顺序随实现漂移";本模块继续承担"一处有序、去重的唯一来源"。
|
|
7
|
+
* 官方复用:无(官方没有市场概念)。
|
|
8
|
+
* 前提检查:**旧实现在这里被政策整体取代**(docs/private/market-tags-policy.md,task-45 全量 13,998 条实测):
|
|
9
|
+
* 1) 旧实现的 status / verification / security 三类标签**从来没有数据源**(没有上游字段可映射);
|
|
10
|
+
* 2) 政策实测出上游真正有值且值得展示的是另外几件事:风险(risk_tier,100% 覆盖)、可装性
|
|
11
|
+
* (installable,12.4%)、编辑推荐(market_tags 的 community-pick,10.2%)、归档(archived,0.6%)、
|
|
12
|
+
* 分类(100%)、功能主题(过滤泛化词后 57.5% 有值);
|
|
13
|
+
* 3) 去重必须**跨来源按归一化值**做:实测 499 条(3.6%)把自己的 category 又写进 topics
|
|
14
|
+
* (liustack/modlens=vision、MemTensor/MemOS=memory…),只做来源内部去重挡不住这种"同一个词出现两次"。
|
|
15
|
+
*
|
|
16
|
+
* 顺序契约(重要性从高到低,渲染方按数组顺序画、绝不再排序):
|
|
17
|
+
* risk → caution → manual → pick → archived → category → topic
|
|
18
|
+
* 这份顺序就是政策 §3.2 的优先级;{@link BADGE_PRIORITY} 是它的机器可读形式。
|
|
19
|
+
*/
|
|
20
|
+
/** 卡片徽标的优先级契约(政策 §3.2)。 */
|
|
21
|
+
export const BADGE_PRIORITY = ['risk', 'caution', 'manual', 'pick', 'archived', 'category', 'topic'];
|
|
22
|
+
/** 卡片徽标的默认上限:政策 §4「只能显示 3 个」的最小集合就是取前三个。 */
|
|
23
|
+
export const TAG_SLOT_LIMIT = 3;
|
|
24
|
+
/** 功能主题的默认上限:政策 §3.5(泛化词占 61.3%,只能当兜底槽)。 */
|
|
25
|
+
export const TOPIC_SLOT_LIMIT = 2;
|
|
26
|
+
/** 上游生态泛化主题词(与 registry.ts 的 ECO_GENERIC_TOPICS 同一判据:这个词能否区分两个插件)。 */
|
|
27
|
+
const ECO_GENERIC_TOPICS = new Set([
|
|
28
|
+
'ai', 'llm', 'agent', 'agents', 'cli', 'cordis', 'cordis-plugin', 'deepseek', 'deepseek-harness',
|
|
29
|
+
'dsh', 'dsh-plugin', 'dsh-plugins', 'gui', 'javascript', 'plugin', 'plugins', 'python', 'react',
|
|
30
|
+
'skill', 'skills', 'tool', 'tools', 'tui', 'typescript', 'ui', 'web', 'web-ui',
|
|
31
|
+
]);
|
|
32
|
+
/** 上游收录标记里的编辑推荐。 */
|
|
33
|
+
const COMMUNITY_PICK = 'community-pick';
|
|
34
|
+
/**
|
|
35
|
+
* 构建一个条目的徽标列表(**按政策**:优先级取前 N、跨来源按归一化值去重)。
|
|
36
|
+
*
|
|
37
|
+
* @param item - 上游字段。
|
|
38
|
+
* @param options - slotLimit(卡片默认 3)与 topicLimit(默认 2)。
|
|
39
|
+
* @returns 按 {@link BADGE_PRIORITY} 排好的徽标;调用方**不得**再排序。
|
|
40
|
+
*/
|
|
41
|
+
export function buildMarketTags(item, options = {}) {
|
|
42
|
+
const slotLimit = Math.max(0, options.slotLimit ?? TAG_SLOT_LIMIT);
|
|
43
|
+
const topicLimit = Math.max(0, options.topicLimit ?? TOPIC_SLOT_LIMIT);
|
|
44
|
+
const out = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
/** 归一化值去重:跨来源(分类与主题同名时只留先出现的那次,即优先级更高的来源)。 */
|
|
47
|
+
const push = (tag) => {
|
|
48
|
+
if (out.length >= slotLimit)
|
|
49
|
+
return;
|
|
50
|
+
const key = tag.value.trim().toLowerCase();
|
|
51
|
+
if (key.length === 0 || seen.has(key))
|
|
52
|
+
return;
|
|
53
|
+
seen.add(key);
|
|
54
|
+
out.push(tag);
|
|
55
|
+
};
|
|
56
|
+
if (item.riskTier === 'risk')
|
|
57
|
+
push({ kind: 'risk', value: 'risk', tone: 'danger' });
|
|
58
|
+
else if (item.riskTier === 'caution')
|
|
59
|
+
push({ kind: 'caution', value: 'caution', tone: 'warning' });
|
|
60
|
+
if (item.installable === 'manual')
|
|
61
|
+
push({ kind: 'manual', value: 'manual', tone: 'neutral' });
|
|
62
|
+
if ((item.marketTags ?? []).some((tag) => tag.trim().toLowerCase() === COMMUNITY_PICK)) {
|
|
63
|
+
push({ kind: 'pick', value: COMMUNITY_PICK, tone: 'success' });
|
|
64
|
+
}
|
|
65
|
+
if (item.archived === true)
|
|
66
|
+
push({ kind: 'archived', value: 'archived', tone: 'warning' });
|
|
67
|
+
const category = item.category?.trim();
|
|
68
|
+
if (category !== undefined && category.length > 0)
|
|
69
|
+
push({ kind: 'category', value: category, tone: 'neutral' });
|
|
70
|
+
let topics = 0;
|
|
71
|
+
for (const raw of item.topics ?? []) {
|
|
72
|
+
if (topics >= topicLimit)
|
|
73
|
+
break;
|
|
74
|
+
const value = String(raw).trim();
|
|
75
|
+
if (value.length === 0 || ECO_GENERIC_TOPICS.has(value.toLowerCase()))
|
|
76
|
+
continue;
|
|
77
|
+
const before = out.length;
|
|
78
|
+
push({ kind: 'topic', value, tone: 'neutral' });
|
|
79
|
+
if (out.length > before)
|
|
80
|
+
topics += 1;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/** 徽标的稳定 key(React key / 去重标识用)。 */
|
|
85
|
+
export function marketTagKey(tag) {
|
|
86
|
+
return tag.kind + ':' + tag.value;
|
|
87
|
+
}
|
|
88
|
+
/** 详情里风险明细的展示上限(与 registry 的透传上限一致)。 */
|
|
89
|
+
const DETAIL_FLAG_LIMIT = 12;
|
|
90
|
+
/** 详情里功能主题的展示上限(政策 §3.1:最多 8)。 */
|
|
91
|
+
const DETAIL_TOPIC_LIMIT = 8;
|
|
92
|
+
/**
|
|
93
|
+
* 构建详情区的条目(政策 §3.1:分类、功能主题、风险明细、验证证据(含报告外链)、许可证、npm 包名、版本、形态)。
|
|
94
|
+
*
|
|
95
|
+
* 顺序是"身份 → 上游结论 → 证据 → 元数据":用户从上往下读能先看到"这是什么",再看到"上游怎么判的"。
|
|
96
|
+
* 空值一律不产出条目(详情区不留空行)。
|
|
97
|
+
*
|
|
98
|
+
* @param item - 上游字段 + 少量本地派生字段(kind / packageName / latestVersion)。
|
|
99
|
+
* @returns 详情项;调用方按 key 取字典文案。
|
|
100
|
+
*/
|
|
101
|
+
export function buildMarketDetail(item) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const kind = item.kind?.trim();
|
|
104
|
+
if (kind !== undefined && kind.length > 0)
|
|
105
|
+
out.push({ key: 'kind', value: kind, tone: 'neutral' });
|
|
106
|
+
const category = item.category?.trim();
|
|
107
|
+
if (category !== undefined && category.length > 0)
|
|
108
|
+
out.push({ key: 'category', value: category, tone: 'neutral' });
|
|
109
|
+
const topics = (item.topics ?? [])
|
|
110
|
+
.map((topic) => String(topic).trim())
|
|
111
|
+
.filter((topic) => topic.length > 0 && !ECO_GENERIC_TOPICS.has(topic.toLowerCase()))
|
|
112
|
+
.slice(0, DETAIL_TOPIC_LIMIT);
|
|
113
|
+
if (topics.length > 0)
|
|
114
|
+
out.push({ key: 'topics', value: topics.join(', '), tone: 'neutral' });
|
|
115
|
+
if (item.riskTier !== undefined && item.riskTier !== 'safe') {
|
|
116
|
+
out.push({ key: 'riskTier', value: item.riskTier, tone: item.riskTier === 'risk' ? 'danger' : 'warning' });
|
|
117
|
+
const flags = (item.riskFlags ?? []).slice(0, DETAIL_FLAG_LIMIT);
|
|
118
|
+
if (flags.length > 0) {
|
|
119
|
+
out.push({
|
|
120
|
+
key: 'riskFlags',
|
|
121
|
+
value: flags.map((flag) => flag.severity.length > 0 ? flag.id + '(' + flag.severity + ')' : flag.id).join(', '),
|
|
122
|
+
tone: item.riskTier === 'risk' ? 'danger' : 'warning',
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (item.installable === 'manual')
|
|
127
|
+
out.push({ key: 'manual', value: 'manual', tone: 'neutral' });
|
|
128
|
+
if (item.archived === true)
|
|
129
|
+
out.push({ key: 'archived', value: 'archived', tone: 'warning' });
|
|
130
|
+
// 独立校验证据:谁验的 + 何时验的 + 报告外链,三者绑成一条(政策 §3.3:不写成含糊的"已验证")。
|
|
131
|
+
if (item.verifiedBy !== undefined || item.verifiedAt !== undefined) {
|
|
132
|
+
const parts = [item.verifiedBy, item.verifiedAt].filter((part) => part !== undefined && part.length > 0);
|
|
133
|
+
out.push({
|
|
134
|
+
key: 'verified',
|
|
135
|
+
value: parts.join(' · '),
|
|
136
|
+
tone: 'success',
|
|
137
|
+
...(item.reportUrl === undefined ? {} : { href: item.reportUrl }),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const license = item.license?.trim();
|
|
141
|
+
if (license !== undefined && license.length > 0)
|
|
142
|
+
out.push({ key: 'license', value: license, tone: 'neutral' });
|
|
143
|
+
const packageName = item.packageName?.trim();
|
|
144
|
+
if (packageName !== undefined && packageName.length > 0)
|
|
145
|
+
out.push({ key: 'npm', value: packageName, tone: 'neutral' });
|
|
146
|
+
const version = item.latestVersion?.trim();
|
|
147
|
+
if (version !== undefined && version.length > 0)
|
|
148
|
+
out.push({ key: 'version', value: version, tone: 'neutral' });
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 统计每个上游分类的条目数,返回 JSON-safe 的 Record(写进 MarketplaceResult.categories)。
|
|
153
|
+
*
|
|
154
|
+
* key 的插入顺序 = 分类首次出现的顺序,同一份 listing 永远产出同一份对象。
|
|
155
|
+
* 分类为空的条目**不计入**任何桶——把"没分类"混进某个分类会让筛选器说谎。
|
|
156
|
+
*/
|
|
157
|
+
export function categoryCounts(items) {
|
|
158
|
+
const counts = {};
|
|
159
|
+
for (const item of items) {
|
|
160
|
+
const id = item.category?.trim();
|
|
161
|
+
if (id === undefined || id.length === 0)
|
|
162
|
+
continue;
|
|
163
|
+
counts[id] = (counts[id] ?? 0) + 1;
|
|
164
|
+
}
|
|
165
|
+
return counts;
|
|
166
|
+
}
|