@zaimokuza/dsh-plugin-hub 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/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "@zaimokuza/dsh-plugin-hub",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Extensible DSH plugin marketplace and reusable enterprise marketplace runtime",
6
+ "type": "module",
7
+ "main": "./src/index.js",
8
+ "exports": {
9
+ ".": "./src/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json",
12
+ "./runtime": "./src/index.js",
13
+ "./catalog": "./src/catalog.js",
14
+ "./identity": "./src/identity.js",
15
+ "./client-entry": "./src/client/index.jsx",
16
+ "./source-plugin": "./src/source-plugin.js",
17
+ "./provider-api": {
18
+ "types": "./src/provider-api.d.ts",
19
+ "default": "./src/provider-api.js"
20
+ }
21
+ },
22
+ "engines": {
23
+ "node": "^22.19.0 || >=24.0.0"
24
+ },
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ },
29
+ "client": {
30
+ "inject": [
31
+ "@deepseek-ai/dsh-client-locale",
32
+ "@deepseek-ai/dsh-client-ui-settings"
33
+ ],
34
+ "platform": "web"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "node scripts/build.mjs",
39
+ "test": "node --test tests/*.test.js",
40
+ "validate:catalog": "node scripts/validate-catalog.mjs"
41
+ },
42
+ "dependencies": {
43
+ "registry-auth-token": "5.1.1",
44
+ "semver": "7.8.5",
45
+ "tar": "7.5.22",
46
+ "undici": "7.28.0",
47
+ "@zaimokuza/dsh-plugin-hub-catalog-demo": "0.1.0"
48
+ },
49
+ "devDependencies": {
50
+ "esbuild": "0.28.1",
51
+ "react": "18.3.1"
52
+ },
53
+ "files": [
54
+ "src",
55
+ "lib/client.js",
56
+ "cordis.patch.yml",
57
+ "README.md",
58
+ "LICENSE",
59
+ "THIRD_PARTY_NOTICES.md"
60
+ ],
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "license": "MIT",
65
+ "author": {
66
+ "name": "zaimokuza-yoshiteru"
67
+ },
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/zaimokuza-yoshiteru/dsh-plugin-hub.git",
71
+ "directory": "packages/marketplace"
72
+ },
73
+ "homepage": "https://github.com/zaimokuza-yoshiteru/dsh-plugin-hub",
74
+ "bugs": {
75
+ "url": "https://github.com/zaimokuza-yoshiteru/dsh-plugin-hub/issues"
76
+ },
77
+ "dshPluginHub": {
78
+ "hostCompatibility": "capability-based",
79
+ "testedDshVersions": [
80
+ "0.1.2-rc.1"
81
+ ]
82
+ }
83
+ }
package/src/catalog.js ADDED
@@ -0,0 +1,107 @@
1
+ import { githubRepository } from './npm-identity.js';
2
+ import semver from 'semver';
3
+ import { dshDeclaration } from './declarations.js';
4
+
5
+ export const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
6
+ export const TAG_LABELS = { agent: 'Agent 接入', 'developer-tools': '开发工具', knowledge: '知识文档', productivity: '效率工具', integration: '系统集成', ui: '界面' };
7
+
8
+ /** Validate external catalog data before replacing the last usable snapshot. */
9
+ export function validateCatalog(value) {
10
+ if (!value || value.schemaVersion !== 1 || !Array.isArray(value.plugins)) throw new Error('目录格式不受支持:需要 schemaVersion: 1 和 plugins 数组');
11
+ if (value.plugins.length > 25000) throw new Error('目录最多支持 25000 个插件');
12
+ const seen = new Set();
13
+ const plugins = value.plugins.map((item, index) => {
14
+ const fail = message => { throw new Error(`目录第 ${index + 1} 条:${message}`); };
15
+ if (!item || typeof item !== 'object') fail('插件记录必须为对象');
16
+ if (typeof item.packageName !== 'string' || !PACKAGE_NAME.test(item.packageName) || item.packageName.length > 214) fail('npm 包名无效');
17
+ if (seen.has(item.packageName)) fail('npm 包名重复');
18
+ seen.add(item.packageName);
19
+ for (const [key, limit] of [['displayName', 80], ['description', 300], ['owner', 80]]) {
20
+ if (typeof item[key] !== 'string' || !item[key].trim() || item[key].length > limit) fail(`${key} 无效`);
21
+ }
22
+ if (!['internal', 'community'].includes(item.origin)) fail('origin 无效');
23
+ if (!Array.isArray(item.tags) || item.tags.length < 1 || item.tags.length > 5 || new Set(item.tags).size !== item.tags.length || item.tags.some(tag => typeof tag !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(tag))) fail('tags 无效');
24
+ for (const key of ['documentationUrl', 'troubleshootingUrl', 'repositoryUrl']) {
25
+ if (item[key] === undefined) continue;
26
+ let url;
27
+ try { url = new URL(item[key]); } catch { fail(`${key} 不是有效 URL`); }
28
+ if (typeof item[key] !== 'string' || url.protocol !== 'https:' || url.username || url.password) fail(`${key} 必须是无凭据的 HTTPS 链接`);
29
+ }
30
+ if (item.stars !== undefined && (!Number.isSafeInteger(item.stars) || item.stars < 0)) fail('stars 无效');
31
+ if (item.verification !== undefined) {
32
+ const v = item.verification;
33
+ if (!v || v.kind !== 'bundle-manifest' || !/^[a-f0-9]{40}$/.test(v.commit ?? '') || !Number.isFinite(Date.parse(v.checkedAt)) || v.manifestPath !== 'package.json' || typeof v.patchPath !== 'string' || !item.repositoryUrl) fail('verification 无效');
34
+ }
35
+ const locales = {};
36
+ if (item.locales !== undefined) {
37
+ if (!item.locales || typeof item.locales !== 'object' || Array.isArray(item.locales) || Object.keys(item.locales).length > 10) fail('locales 无效');
38
+ for (const [language, copy] of Object.entries(item.locales)) {
39
+ if (!/^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$/.test(language) || !copy || typeof copy !== 'object' || Array.isArray(copy)) fail('locales 无效');
40
+ locales[language] = {};
41
+ for (const [key, limit] of [['displayName', 80], ['description', 300]]) {
42
+ if (copy[key] === undefined) continue;
43
+ if (typeof copy[key] !== 'string' || !copy[key].trim() || copy[key].length > limit) fail(`locales.${language}.${key} 无效`);
44
+ locales[language][key] = copy[key];
45
+ }
46
+ }
47
+ }
48
+ const result = Object.fromEntries(['packageName', 'displayName', 'description', 'owner', 'origin', 'tags', 'documentationUrl', 'troubleshootingUrl', 'repositoryUrl', 'stars', 'verification'].filter(key => item[key] !== undefined).map(key => [key, item[key]]));
49
+ return Object.keys(locales).length ? { ...result, locales } : result;
50
+ });
51
+ return { schemaVersion: 1, plugins };
52
+ }
53
+
54
+ /** Declared compatibility is not a promise that a package has been runtime-tested. */
55
+ export function evaluateVersion(version, manifest, publishedAt, host, now, minimumAgeHours = 48) {
56
+ const peerEvidence = Object.entries(manifest.peerDependencies ?? {}).some(([name, range]) => name.startsWith('@deepseek-ai/dsh-') && typeof range === 'string' && semver.validRange(range) && host.peers[name]);
57
+ const declaration = dshDeclaration(manifest, host, peerEvidence);
58
+ const dshRange = declaration.range;
59
+ const reasons = [...declaration.reasons];
60
+ let compatibility = declaration.status;
61
+ if (manifest.engines?.node && (!semver.validRange(manifest.engines.node) || !semver.satisfies(host.node, manifest.engines.node))) {
62
+ compatibility = 'incompatible'; reasons.push(`要求 Node ${manifest.engines.node}`);
63
+ }
64
+ const extraNode = manifest.dsh?.compatibility?.node;
65
+ if (extraNode !== undefined && (typeof extraNode !== 'string' || !semver.validRange(extraNode) || !semver.satisfies(host.node, extraNode))) {
66
+ compatibility = 'incompatible'; reasons.push(`要求 Node ${extraNode}`);
67
+ }
68
+ const profiles = manifest.dsh?.compatibility?.profiles;
69
+ if (Array.isArray(profiles) && !profiles.includes(host.profile ?? 'web')) { compatibility = 'incompatible'; reasons.push('声明不支持当前 DSH profile'); }
70
+ for (const [name, range] of Object.entries(manifest.peerDependencies ?? {})) {
71
+ if (!name.startsWith('@deepseek-ai/')) continue;
72
+ const actual = host.peers[name];
73
+ if (!actual) {
74
+ if (manifest.peerDependenciesMeta?.[name]?.optional) continue;
75
+ if (compatibility !== 'incompatible') compatibility = 'unknown';
76
+ reasons.push(`无法确认宿主接口 ${name}`);
77
+ } else if (typeof range !== 'string' || !semver.validRange(range) || !semver.satisfies(actual, range)) {
78
+ compatibility = 'incompatible'; reasons.push(`${name} 要求 ${range},当前为 ${actual}`);
79
+ }
80
+ }
81
+ for (const [key, actual] of [['os', host.platform], ['cpu', host.arch]]) {
82
+ const values = manifest[key];
83
+ if (Array.isArray(values) && (values.includes(`!${actual}`) || (values.some(v => !v.startsWith('!')) && !values.includes(actual) && !values.includes('any')))) {
84
+ compatibility = 'incompatible'; reasons.push(`不支持当前 ${key}: ${actual}`);
85
+ }
86
+ }
87
+ const publishedMs = typeof publishedAt === 'string' ? Date.parse(publishedAt) : NaN;
88
+ const minimumAgeMinutes = Math.round(minimumAgeHours * 60);
89
+ const eligibleAt = Number.isFinite(publishedMs) ? new Date(publishedMs + minimumAgeMinutes * 60000).toISOString() : null;
90
+ const age = eligibleAt === null ? 'unknown' : Date.parse(eligibleAt) <= now ? 'ready' : 'waiting';
91
+ if (age === 'unknown') reasons.push('仓库未返回有效发布时间');
92
+ if (age === 'waiting') reasons.push(Number.isInteger(minimumAgeHours) ? `发布未满 ${minimumAgeHours} 小时` : `发布未满 ${minimumAgeMinutes} 分钟`);
93
+ if (manifest.deprecated) reasons.push(`已弃用:${manifest.deprecated}`);
94
+ const canInstall = compatibility === 'compatible' && age === 'ready' && !manifest.deprecated;
95
+ return { version, compatibilityBasis: declaration.basis, publishedAt: Number.isFinite(publishedMs) ? new Date(publishedMs).toISOString() : null, dshRange, compatibility, age, eligibleAt, canInstall, reasons };
96
+ }
97
+
98
+ export function releaseList(metadata, host, now, minimumAgeHours, plugin) {
99
+ return Object.entries(metadata.versions ?? {})
100
+ .filter(([version, manifest]) => semver.valid(version) && manifest && typeof manifest === 'object')
101
+ .sort(([a], [b]) => semver.rcompare(a, b))
102
+ .map(([version, manifest]) => {
103
+ const release = evaluateVersion(version, manifest, metadata.time?.[version], host, now, minimumAgeHours);
104
+ if((plugin?.verification || githubRepository(plugin?.repositoryUrl)) && githubRepository(manifest.repository)!==githubRepository(plugin.repositoryUrl)) { release.canInstall=false; release.reasons.push('该版本的 npm 来源无法与目录仓库核对'); }
105
+ return release;
106
+ });
107
+ }
@@ -0,0 +1,22 @@
1
+ export class RequestError extends Error {
2
+ constructor(key, detail = '') { super(key); this.detail = detail; }
3
+ }
4
+
5
+ export async function request(path, data, { signal, fetcher = fetch, base = '/dsh-plugin-hub/hub/api/' } = {}) {
6
+ let response;
7
+ try {
8
+ response = await fetcher(base + path, {
9
+ signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(20000)]) : AbortSignal.timeout(20000),
10
+ ...(data === undefined ? {} : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(data) }),
11
+ });
12
+ } catch (error) {
13
+ if (signal?.aborted) throw error;
14
+ throw new RequestError(error.name === 'TimeoutError' ? '请求超时,正在等待 DSH 响应。' : '连接已断开,请确认本地 DSH 实例正在运行。恢复后页面会自动重连。');
15
+ }
16
+ if (response.status === 401) throw new RequestError('请先登录当前 DSH 实例');
17
+ let value;
18
+ try { value = await response.json(); }
19
+ catch { throw new RequestError('服务返回了无效响应,请刷新 DSH 页面。'); }
20
+ if (!response.ok) throw new RequestError(value.error ?? `HTTP ${response.status}`);
21
+ return value;
22
+ }
@@ -0,0 +1,201 @@
1
+ import React, { useEffect, useState, useSyncExternalStore } from 'react';
2
+ import { Input, Button, Menu, Modal, Pill, StateDot, IconChevronDownOutline14 } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import { request as apiRequest } from './api.js';
4
+ import { inScope, resolveScope } from './scope.js';
5
+ import { words, tagKeys, diagnostic, localizePlugin } from './locale.js';
6
+ import css from './market.css';
7
+ const NS = __HUB_PACKAGE__;
8
+ const MARKET_ID = __HUB_ID__;
9
+ const BRAND = __HUB_BRAND__;
10
+ const request = (path, data, options) => apiRequest(path, data, { ...options, base: `/dsh-plugin-hub/${MARKET_ID}/api/` });
11
+
12
+ function Icon({ name = 'box', size = 18, ...props }) {
13
+ const paths = {
14
+ search: <><circle cx="10.5" cy="10.5" r="6.5"/><path d="m16 16 4 4"/></>,
15
+ box: <><path d="m12 3 9 5-9 5-9-5 9-5Z"/><path d="M3 8v9l9 5 9-5V8M12 13v9M7.5 5.5l9 5"/></>,
16
+ download: <><path d="M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5"/></>,
17
+ arrow: <><path d="M7 17 17 7M7 7h10v10"/></>,
18
+ refresh: <><path d="M20 7v5h-5M4 17v-5h5"/><path d="M5.2 7A8 8 0 0 1 19 6l1 2M4 16l1 2a8 8 0 0 0 13.8-1"/></>,
19
+ clock: <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
20
+ check: <path d="m5 12 4 4L19 6"/>,
21
+ close: <path d="m6 6 12 12M18 6 6 18"/>,
22
+ code: <><path d="m8 6-6 6 6 6m8-12 6 6-6 6m-3-15-2 18"/></>,
23
+ book: <><path d="M12 5v16M3 4c4-1 6 0 9 2 3-2 5-3 9-2v15c-4-1-6 0-9 2-3-2-5-3-9-2V4Z"/></>,
24
+ plug: <><path d="M8 2v5m8-5v5M6 7h12v4a6 6 0 0 1-12 0V7ZM12 17v5"/></>,
25
+ filter: <><path d="M4 7h16M7 12h10M10 17h4"/></>,
26
+ };
27
+ return <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" {...props}>{paths[name] ?? paths.box}</svg>;
28
+ }
29
+
30
+ /** Use the host's Menu and Button, including its portal and dismissal behavior. */
31
+ function Picker({ label, value, items, onChange, disabled, icon, title }) {
32
+ const [open, setOpen] = useState(false);
33
+ return <Menu open={open} onClose={() => setOpen(false)} items={items} selectedId={value} portal align="end"
34
+ onSelect={id => { setOpen(false); onChange(id); }}
35
+ anchor={<Button variant="outline" size="sm" disabled={disabled} aria-label={label} aria-haspopup="menu" aria-expanded={open} icon={icon} onClick={() => setOpen(v => !v)}>{title ?? items.find(item => item.id === value)?.label}<IconChevronDownOutline14/></Button>}/>;
36
+ }
37
+
38
+ function Badge({ tone = 'neutral', children }) {
39
+ const state = { green: 'done', amber: 'warning', red: 'error', ongoing: 'ongoing' }[tone];
40
+ return <Pill>{state && <StateDot state={state}/>} {children}</Pill>;
41
+ }
42
+
43
+ function status(plugin, release, t, hours) {
44
+ if (plugin.metadataLoading) return { label: t('正在查询版本'), tone: 'ongoing' };
45
+ if (plugin.queryError) return { label: t(plugin.queryError.includes('(404)') ? '当前仓库未找到' : plugin.queryError.includes('仓库与目录不一致') ? '来源不匹配' : plugin.queryError.includes('未声明可核对的 GitHub 来源') ? '来源未核实' : '查询失败'), tone: 'neutral' };
46
+ if (!release || release.compatibility === 'unknown') return { label: t('兼容性未知'), tone: 'neutral' };
47
+ if (release.compatibility === 'incompatible') return { label: t('版本不兼容'), tone: 'red' };
48
+ if (release.age === 'waiting') return { label: Number.isInteger(hours) ? t('未满 {hours} 小时', { hours }) : t('未满 {minutes} 分钟', { minutes: Math.round(hours * 60) }), tone: 'amber' };
49
+ if (release.age === 'unknown') return { label: t('发布时间未知'), tone: 'neutral' };
50
+ return release.canInstall ? { label: t('符合安装条件'), tone: 'green' } : { label: t('暂不可安装'), tone: 'neutral' };
51
+ }
52
+
53
+ export function Market({ locale, t }) {
54
+ const language = useSyncExternalStore(listener => locale.subscribe(listener), () => locale.getSnapshot().active);
55
+ const date = value => value ? new Date(value).toLocaleString(language.startsWith('zh') ? 'zh-CN' : language, { month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : t('时间未知');
56
+ const explain = value => diagnostic(value, t, language);
57
+ const [data, setData] = useState(null);
58
+ const [error, setError] = useState('');
59
+ const [connectionError, setConnectionError] = useState('');
60
+ const [loading, setLoading] = useState(true);
61
+ const [query, setQuery] = useState('');
62
+ const [tab, setTab] = useState('all');
63
+ const [tag, setTag] = useState('all');
64
+ const [visibleCount, setVisibleCount] = useState(40);
65
+ useEffect(() => setVisibleCount(40), [query, tab, tag]);
66
+ const [selected, setSelected] = useState({});
67
+ const [detailName, setDetailName] = useState(null);
68
+ const [showJobs, setShowJobs] = useState(false);
69
+ const [submitting, setSubmitting] = useState({});
70
+
71
+ useEffect(() => {
72
+ const controller = new AbortController();
73
+ let timer; let failures = 0;
74
+ // Sequential polling prevents a disconnected instance from accumulating requests.
75
+ async function poll(first = false) {
76
+ try {
77
+ const value = await request('state', undefined, { signal: controller.signal });
78
+ if (!controller.signal.aborted) { setData(value); setConnectionError(''); failures = 0; }
79
+ } catch (err) {
80
+ if (!controller.signal.aborted) { setConnectionError(err.message); failures++; }
81
+ } finally {
82
+ if (!controller.signal.aborted) {
83
+ if (first) setLoading(false);
84
+ timer = setTimeout(() => poll(), Math.min(10000, 2000 * 2 ** failures));
85
+ }
86
+ }
87
+ }
88
+ void poll(true);
89
+ return () => { controller.abort(); clearTimeout(timer); };
90
+ }, []);
91
+
92
+ const pendingVisible = (data?.lazyMetadata ? data.plugins.map(p => localizePlugin(p, language))
93
+
94
+ .filter(p=>inScope(p,resolveScope(tab,data.sources??[]))&&(tag==='all'||p.tags.includes(tag))&&[p.displayName,p.packageName,p.description,p.owner,...p.tags.map(id=>tagKeys[id]?t(tagKeys[id]):id)].join(' ').toLowerCase().includes(query.trim().toLowerCase()))
95
+ .slice(0,visibleCount).filter(p=>p.metadataLoading).slice(0,40).map(p=>p.packageName) : []).join('|');
96
+ useEffect(() => {
97
+ if(!pendingVisible)return;
98
+ const controller=new AbortController();
99
+ void request('releases',{packageNames:pendingVisible.split('|')},{signal:controller.signal}).catch(error=>{if(!controller.signal.aborted)setError(error.message);});
100
+ return()=>controller.abort();
101
+ },[pendingVisible]);
102
+
103
+ function reportActionError(err) {
104
+ if (['连接已断开,请确认本地 DSH 实例正在运行。恢复后页面会自动重连。', '请求超时,正在等待 DSH 响应。', '请先登录当前 DSH 实例', '服务返回了无效响应,请刷新 DSH 页面。'].includes(err.message)) setConnectionError(err.message);
105
+ else setError(err.message);
106
+ }
107
+ async function update(path = 'refresh', body = {}) {
108
+ setLoading(true); setError('');
109
+ try { setData(await request(path === 'refresh' ? 'refresh-start' : path, body)); setConnectionError(''); }
110
+ catch (err) { reportActionError(err); }
111
+ finally { setLoading(false); }
112
+ }
113
+ async function install(plugin, release, action = 'install') {
114
+ if (action === 'install' && !release?.canInstall) return;
115
+ setSubmitting(previous => ({ ...previous, [plugin.packageName]: true })); setError('');
116
+ try { await request(action, { packageName: plugin.packageName, ...(action === 'install' ? { version: release.version } : {}) }); setData(await request('state')); setShowJobs(true); setConnectionError(''); }
117
+ catch (err) { reportActionError(err); }
118
+ finally { setSubmitting(previous => ({ ...previous, [plugin.packageName]: false })); }
119
+ }
120
+ const releaseOf = plugin => plugin.versions.find(v => v.version === selected[plugin.packageName]) ?? plugin.versions.find(v => v.version === plugin.recommendedVersion) ?? plugin.versions[0];
121
+ const linkFor = (plugin, key) => plugin[key];
122
+ function documentLinks(plugin) {
123
+ return <div className="hub-document-links">{[['documentationUrl', '介绍文档'], ['troubleshootingUrl', '排查指南']].filter(([key]) => plugin[key]).map(([key, label]) => <a className="hub-help-link" key={key} href={linkFor(plugin, key)} target="_blank" rel="noopener noreferrer" title={plugin[key]}>{t(label)} <Icon name="arrow" size={13}/></a>)}</div>;
124
+ }
125
+ function uninstallButton(plugin) {
126
+ if (!plugin.installedVersion) return null;
127
+ const job = data.jobs.find(job => job.packageName === plugin.packageName && ['queued', 'installing'].includes(job.status));
128
+ return <Button size="sm" variant="outline" disabled={Boolean(job) || submitting[plugin.packageName] || Boolean(connectionError)} onClick={() => install(plugin, null, 'uninstall')}>{t(job?.action === 'uninstall' ? '正在卸载…' : '卸载插件')}</Button>;
129
+ }
130
+ function installButton(plugin, release) {
131
+ const job = data.jobs.find(job => job.packageName === plugin.packageName && ['queued', 'installing'].includes(job.status));
132
+ const installed = plugin.installedVersion === release?.version;
133
+ const key = job ? job.status === 'queued' ? '已加入队列' : job.action === 'uninstall' ? '正在卸载…' : '正在安装…' : installed ? '已安装' : !release?.canInstall ? '暂不可安装' : '安装插件';
134
+ return <Button variant={installed ? 'outline' : 'primary'} size="sm" disabled={!release?.canInstall || installed || Boolean(job) || submitting[plugin.packageName] || Boolean(connectionError)} onClick={() => install(plugin, release)} icon={<Icon name={installed ? 'check' : job ? 'clock' : 'download'} size={15}/>}>{t(key)}</Button>;
135
+ }
136
+
137
+ if (!data) return <section className="hub-market"><style>{css}</style><div className="hub-empty"><Icon name="box" size={38}/><h2>{BRAND.navTitle}</h2><p role={error || connectionError ? 'alert' : undefined}>{explain(error || connectionError) || t('正在读取插件目录…')}</p><Button variant="outline" onClick={() => update()}>{t('重新获取')}</Button></div></section>;
138
+ const all = data.plugins.map(plugin => localizePlugin(plugin, language));
139
+ const tags = Object.fromEntries(Object.entries(tagKeys).map(([key, label]) => [key, t(label)]));
140
+ const sourceTabs = (data.sources ?? []).filter(source => !source.primary);
141
+ const activeTab = resolveScope(tab, sourceTabs);
142
+ const selectedSource = sourceTabs.find(source => activeTab === 'source:' + source.id);
143
+ const tabs = [['all', '全部插件', all.length], ['installed', '已安装', all.filter(p => p.installedVersion).length], ...sourceTabs.map(source => ['source:' + source.id, source.displayName, all.filter(p => inScope(p, 'source:' + source.id)).length])];
144
+ const filtered = all.filter(plugin => inScope(plugin, activeTab) && (tag === 'all' || plugin.tags.includes(tag)) && [plugin.displayName, plugin.packageName, plugin.description, plugin.owner, ...plugin.tags.map(item => tags[item] ?? item)].join(' ').toLowerCase().includes(query.trim().toLowerCase()));
145
+ const detail = all.find(plugin => plugin.packageName === detailName);
146
+ const pending = data.jobs.filter(job => ['queued', 'installing'].includes(job.status)).length;
147
+ const ready = all.filter(plugin => plugin.recommendedVersion).length;
148
+
149
+ const brand = data.brand ?? BRAND;
150
+ return <section className="hub-market" lang={language} style={{ '--red': brand.primaryColor }}>
151
+ <style>{css}</style>
152
+ <div className="hub-topline"><span className="hub-brand"><span className="hub-brandmark">{brand.title.slice(0, 1)}</span>{brand.title} <span className="hub-divider"/> {brand.subTitle}</span><span className="hub-runtime"><i/>DSH {data.host.dsh}</span></div>
153
+ <header className="hub-header"><div><div className="hub-eyebrow">{t('插件目录')}</div><h1>{t('插件市场')}<span>{t('。')}</span></h1><p>{t('找到适合当前 DSH 的工具。')}</p></div><div className="hub-header-art"><Icon name="box" size={44}/><span className="hub-art-plus">+</span><span className="hub-art-dot"/></div></header>
154
+ <div className="hub-stats"><div><strong>{all.length.toString().padStart(2, '0')}</strong><span>{t('收录插件')}</span></div><div><strong>{ready.toString().padStart(2, '0')}</strong><span>{t(data.lazyMetadata ? '已确认可安装' : '有可安装版本')}</span></div></div>
155
+
156
+ <p className="hub-footnote">{t('冷静期:{minutes} 分钟 · {source}', { minutes: data.minimumAgeMinutes, source: t(data.releaseAgeSource === 'pnpm' ? '本机 pnpm 配置' : '默认 48 小时') })}</p>
157
+ {(data.lazyMetadata || data.metadataProgress?.running) && <p>{t('已查询 {done} / {total} 个插件的版本', data.metadataProgress)}</p>}
158
+ {data.catalog.error && <div className="hub-notice amber"><Icon name="clock"/><div><b>{t(data.catalog.stale ? '目录更新失败,继续使用上一份有效数据' : '目录暂不可用')}</b><span>{explain(data.catalog.error)}</span></div><Button size="sm" onClick={() => update()}>{t('重试')}</Button></div>}
159
+ {connectionError && <div role="alert" className="hub-notice red"><span>{explain(connectionError)}</span><Button size="sm" onClick={() => update()}>{t('重试')}</Button></div>}
160
+ {error && error !== connectionError && <div role="alert" className="hub-notice red"><span>{explain(error)}</span><Button size="sm" aria-label={t('关闭错误')} onClick={() => setError('')} icon={<Icon name="close"/>}/></div>}
161
+
162
+ <nav className="hub-tabs" aria-label={t('插件范围')}>{tabs.map(([key, label, count]) => <Button key={key} size="sm" variant={activeTab === key ? 'toolbar' : 'ghost'} aria-pressed={activeTab === key} onClick={() => setTab(key)}>{t(label)}{count !== null && <span className="hub-tab-count">{count}</span>}</Button>)}</nav>
163
+ <div className="hub-toolbar"><Input className="hub-search-input" icon={<Icon name="search" size={16}/>} aria-label={t('搜索插件')} placeholder={t('搜索插件、用途或维护团队')} value={query} onChange={event => setQuery(event.target.value)}/>{query && <Button size="sm" aria-label={t('清空搜索')} onClick={() => setQuery('')} icon={<Icon name="close" size={14}/>}/>}<Picker label={t('按标签筛选')} value={tag} items={[{ id: 'all', label: t('所有标签') }, ...Object.entries(tags).map(([id, label]) => ({ id, label }))]} onChange={setTag} icon={<Icon name="filter" size={16}/>}/><Button variant="outline" size="sm" disabled={loading} onClick={() => update()} title={t('刷新目录与版本')} icon={<Icon name="refresh" size={16} className={loading ? 'spinning' : ''}/>}>{t('刷新')}</Button></div>
164
+ <div className="hub-results"><span>{query ? t('“{query}” 的搜索结果', { query }) : t(selectedSource?.displayName ?? (activeTab === 'installed' ? '你的工作工具' : '为团队精选'))} <b>{filtered.length}</b></span><small>{t('版本信息来自 npm · 按当前 DSH 检查声明')}</small></div>
165
+
166
+ {selectedSource?.error && <div className="hub-notice amber"><span>{t(selectedSource.displayName)} · {explain(selectedSource.error)}{selectedSource.stale && ' · ' + t('使用缓存')}</span></div>}
167
+ <div className="hub-grid">{filtered.slice(0, visibleCount).map((plugin, index) => {
168
+ const release = releaseOf(plugin); const state = status(plugin, release, t, data.minimumAgeHours);
169
+ return <article className="hub-card" key={plugin.packageName} style={{ '--card-index': index }}>
170
+ <div className="hub-card-top"><span className={`hub-plugin-icon icon-${plugin.tags[0]}`}><Icon name={plugin.tags.includes('agent') ? 'plug' : plugin.tags[0] === 'knowledge' ? 'book' : plugin.tags.includes('developer-tools') ? 'code' : 'box'} size={25}/></span><Badge tone={state.tone}>{state.label}</Badge></div>
171
+ <h2 className="hub-card-heading"><Button size="sm" onClick={() => setDetailName(plugin.packageName)}>{plugin.displayName}<Icon name="arrow" size={13}/></Button></h2>
172
+ <code className="hub-card-package">{plugin.packageName}</code>
173
+ {(data.sources?.length ?? 0) > 1 && <small className="hub-source-label">{t('来源')} · {t(plugin.catalogSource?.displayName ?? '')}</small>}
174
+ <p className="hub-description">{plugin.description}</p>
175
+ <div className="hub-tags">{plugin.tags.map(item => <Pill key={item}>{tags[item] ?? item}</Pill>)}</div>
176
+ <div className="hub-version-row"><span>{t('选择版本')}</span><Picker label={t('{name}版本', { name: plugin.displayName })} value={release?.version ?? ''} disabled={!plugin.versions.length} items={plugin.versions.length ? plugin.versions.map(v => ({ id: v.version, label: v.version + (v.version === plugin.recommendedVersion ? ' · ' + t('推荐') : '') })) : [{ id: '', label: t('暂不可用') }]} onChange={value => setSelected(previous => ({ ...previous, [plugin.packageName]: value }))}/></div>
177
+ <div className="hub-version-note">{plugin.queryError ? explain(plugin.queryError) : release?.canInstall ? t('发布于 {date}', { date: date(release.publishedAt) }) : explain(release?.reasons[0]) || t('没有可用版本')}</div>
178
+ <div className="hub-card-actions">{installButton(plugin, release)}{uninstallButton(plugin)}</div>
179
+ {documentLinks(plugin)}
180
+ </article>;
181
+ })}</div>
182
+ {filtered.length > visibleCount && <div className="hub-pagination"><Button variant="outline" size="sm" onClick={() => setVisibleCount(value => value + 40)}>{t('加载更多')}</Button></div>}
183
+ {!filtered.length && <div className="hub-empty"><Icon name="search" size={32}/><h3>{t('没有找到匹配的插件')}</h3><p>{t('试试其它关键词,或清除当前筛选条件。')}</p><Button variant="outline" onClick={() => { setQuery(''); setTag('all'); setTab('all'); }}>{t('查看全部插件')}</Button></div>}
184
+ <footer className="hub-footer"><span><i/>{t('目录 {version} · 更新于 {date}', { version: data.catalog.version ?? t('未加载'), date: date(data.catalog.updatedAt) })}</span><span>{t('市场 v{version}', { version: data.marketVersion })}</span></footer>
185
+ <p className="hub-footnote">{t('“符合安装条件”依据版本声明与发布时间,实际下载仍以 Nexus 为准。')}</p>
186
+
187
+ {!!data.jobs.length && <aside className="hub-queue"><div className="hub-queue-summary"><span className="hub-queue-icon"><Icon name={pending ? 'download' : 'check'}/></span><span><b>{pending ? t('{count} 个插件操作正在执行', { count: pending }) : t('插件操作已完成')}</b><small>{t('{count} 项待手动重启生效', { count: data.pendingRestart })}</small></span><Button size="sm" onClick={() => setShowJobs(!showJobs)} aria-expanded={showJobs}>{t(showJobs ? '收起' : '查看任务')}</Button></div>{showJobs && <div className="hub-jobs">{data.jobs.slice().reverse().map(job => <div key={job.id}><span><b>{all.find(p => p.packageName === job.packageName)?.displayName ?? job.packageName}</b><small>{t(job.action === 'uninstall' ? '卸载' : '安装')} · {job.version}</small></span><Badge tone={job.status === 'completed' ? 'green' : job.status === 'failed' ? 'red' : 'amber'}>{t({ queued: '排队中', installing: job.action === 'uninstall' ? '卸载中' : '安装中', completed: '待重启', failed: job.action === 'uninstall' ? '卸载失败' : '安装失败' }[job.status])}</Badge>{job.error && <p>{explain(job.error)}</p>}</div>)}</div>}</aside>}
188
+
189
+ <Modal open={Boolean(detail)} onClose={() => setDetailName(null)} title={detail?.displayName ?? ''} closeLabel={t('关闭详情')} className="hub-detail-dialog" contentClassName="hub-detail-scroll" description={detail?.description} footer={detail && <div className="hub-detail-actions"><div>{installButton(detail, releaseOf(detail))}{uninstallButton(detail)}</div>{documentLinks(detail)}</div>}>
190
+ {detail && <div className="hub-detail-body"><code className="hub-package-name">{detail.packageName}</code><div className="hub-detail-info"><span>{t('维护团队')}<b>{detail.owner}</b></span><span>{t('当前宿主')}<b>DSH {data.host.dsh}</b></span></div><h3>{t('发行版本')}</h3><div className="hub-releases">{detail.versions.map(release => { const state = status(detail, release, t, data.minimumAgeHours); return <div key={release.version} className="hub-release"><div><Button variant={releaseOf(detail)?.version === release.version ? 'toolbar' : 'outline'} size="sm" aria-pressed={releaseOf(detail)?.version === release.version} onClick={() => setSelected(previous => ({ ...previous, [detail.packageName]: release.version }))}>{release.version}</Button><Badge tone={state.tone}>{state.label}</Badge></div><small>{t('发布时间 {date} · DSH {range}', { date: date(release.publishedAt), range: release.dshRange ?? t(release.compatibilityBasis === 'dshPluginHub.hostCompatibility' ? '不按版本限制,运行时检查接口' : release.compatibilityBasis === 'peerDependencies' ? '宿主依赖声明' : release.compatibilityBasis?.includes('dshReleases') ? '作者逐版本声明' : '未声明') })}</small>{release.reasons.length > 0 && <p>{release.reasons.map(explain).join(' · ')}</p>}{release.age === 'waiting' && <small>{Number.isInteger(data.minimumAgeHours) ? t('预计满 {hours} 小时:{date}', { hours: data.minimumAgeHours, date: date(release.eligibleAt) }) : t('预计满 {minutes} 分钟:{date}', { minutes: data.minimumAgeMinutes, date: date(release.eligibleAt) })}</small>}</div>; })}{detail.queryError && <p>{explain(detail.queryError)}</p>}</div></div>}
191
+ </Modal>
192
+ </section>;
193
+ }
194
+
195
+ export const name = NS;
196
+ export const inject = ['slots', 'locale'];
197
+ export function apply(ctx) {
198
+ ctx.effect(() => ctx.locale.register(NS, words), 'Plugin Hub: dictionaries');
199
+ const t = ctx.locale.bind(NS);
200
+ ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: MARKET_ID, order: 39, label: () => BRAND.navTitle, locale: NS }, () => <Market locale={ctx.locale} t={t}/>));
201
+ }
@@ -0,0 +1,104 @@
1
+ // Registered with the DSH locale service. Catalog copy remains data-owned.
2
+ const pairs = [
3
+ ['冷静期:{minutes} 分钟 · {source}', 'Release age: {minutes} minutes · {source}'],
4
+ ['本机 pnpm 配置', 'Local pnpm configuration'], ['默认 48 小时', 'Default 48 hours'],
5
+ ['未满 {minutes} 分钟', 'Under {minutes} minutes'], ['预计满 {minutes} 分钟:{date}', 'Eligible after {minutes} minutes: {date}'],
6
+ ['不按版本限制,运行时检查接口','No version gate; runtime API checks'],
7
+ ['目录格式不受支持:需要 schemaVersion: 1 和 plugins 数组','Unsupported catalog: expected schemaVersion: 1 and a plugins array'],
8
+ ['目录最多支持 25000 个插件','A catalog supports at most 25000 plugins'],
9
+ ['插件记录必须为对象','Plugin entry must be an object'],
10
+ ['npm 包名无效','Invalid npm package name'], ['npm 包名重复','Duplicate npm package name'],
11
+ ['目录包 plugins.json 重复或过大','Catalog plugins.json is duplicated or too large'],
12
+ ['目录过大','Catalog is too large'], ['目录包缺少 plugins.json','Catalog package is missing plugins.json'],
13
+ ['仓库请求地址不属于配置的 Nexus','Registry request does not belong to the configured Nexus'],
14
+ ['Invalid release request','Invalid release request','无效的版本查询请求'],
15
+ ['Invalid catalog source id','Invalid catalog source ID','无效的数据源 ID'],
16
+ ['Invalid catalog package name','Invalid catalog package name','无效的目录数据包名'],
17
+ ['At most 20 catalog sources are supported','At most 20 catalog sources are supported','最多支持 20 个数据源'],
18
+ ['Catalog source kind must be npm or json','Catalog source kind must be npm or json','数据源类型必须是 npm 或 json'],
19
+ ['JSON source requires getCatalog({ signal })','JSON source requires getCatalog({ signal })','JSON 数据源需要 getCatalog({ signal })'],
20
+ ['Source priority must be an integer between -1000 and 1000','Source priority must be an integer between -1000 and 1000','数据源优先级必须为 -1000 到 1000 的整数'],
21
+ ['Invalid source displayName','Invalid source displayName','无效的数据源显示名称'],
22
+ ['Invalid source cacheVersion','Invalid source cacheVersion','无效的数据源缓存版本'],
23
+ ['兼容声明存在冲突,需要维护者确认','Compatibility declarations conflict; the maintainer must clarify'],
24
+ ['声明不支持当前 DSH profile','This DSH profile is not covered by the declaration'],
25
+ ['作者逐版本声明','Author release declaration'],
26
+ ['不支持的请求方法','Unsupported request method'], ['接口不存在','Endpoint not found'], ['请求过大','Request body is too large'],
27
+ ['目录 npm 包没有 integrity,无法校验','Catalog package has no integrity field'],
28
+ ['目录 npm 包完整性校验失败','Catalog package integrity check failed'],
29
+ ['目录来源必须是 npm 数据包','Catalog source must be an npm data package'],
30
+ ['目录 npm 包没有可下载的 latest 版本','Catalog package has no downloadable latest release'],
31
+ ['目录 tarball 地址不属于配置的仓库','Catalog tarball does not belong to the configured registry'],
32
+ ['插件命令失败','Plugin command failed'],
33
+ ['安装命令结束,但实际版本不匹配','Installation finished but the installed version does not match'],
34
+ ['包已下载,但尚未注册为 DSH bundle','Package downloaded but not registered as a DSH bundle'],
35
+ ['卸载命令结束,但插件仍在 profile 中','Uninstall finished but the plugin is still in the profile'],
36
+ ['插件操作超时,请检查 profile 后重试','Plugin operation timed out; check the profile and retry'],
37
+ ['来源未核实','Source unverified'],
38
+ ['宿主依赖声明','Host peer declarations'], ['兼容依据:{basis}','Compatibility evidence: {basis}'],
39
+
40
+ ['已确认可安装','Confirmed installable'],
41
+ ['当前 npm 仓库未找到此包(404)','Package not found in this npm registry (404)'],
42
+ ['当前仓库未找到','Not found in this registry'], ['来源不匹配','Source mismatch'],
43
+ ['npm 包未声明可核对的 GitHub 来源','The npm package does not declare a verifiable GitHub repository'],
44
+ ['npm 包对应的 GitHub 仓库与目录不一致','The npm package points to a different GitHub repository'],
45
+ ['该版本的 npm 来源无法与目录仓库核对','This npm version cannot be matched to the catalog repository'],
46
+ ['正在查询版本','Loading releases'], ['加载更多','Load more'],
47
+ ['已查询 {done} / {total} 个插件的版本','Loaded releases for {done} / {total} plugins'],
48
+
49
+ ['插件目录','Plugin directory'],
50
+ ['数据源','Data sources'], ['关闭数据源','Close data sources'], ['来源','Source'], ['Company catalog','Company catalog','公司目录'], ['Demo catalog','Demo catalog','演示目录'], ['Team handbook','Team handbook','团队手册目录'], ['npm 数据包','npm data package'], ['扩展插件 JSON','Provider plugin JSON'], ['已加载','Loaded'], ['使用缓存','Using cache'], ['目录来源读取超时','Catalog source timed out'], ['目录来源读取已取消','Catalog source read cancelled'], ['优先级 {priority} · {count} 个插件','Priority {priority} · {count} plugins'], ['已合并 {count} 条重复记录','Merged {count} duplicate entries'], ['同名插件按来源优先级保留一条;优先级相同时按来源 ID 排序。','Duplicate package names use the highest-priority source; equal priorities are ordered by source ID.'],
51
+ ['介绍文档','Documentation'], ['模拟卸载','Simulate uninstall'], ['卸载插件','Uninstall plugin'], ['正在卸载…','Uninstalling…'], ['卸载中','Uninstalling'], ['卸载失败','Uninstall failed'], ['卸载','Uninstall'], ['安装','Install'], ['模拟操作已完成','Simulated operations finished'], ['插件操作已完成','Plugin operations finished'], ['{count} 个插件操作正在执行','{count} plugin operations in progress'], ['该插件尚未安装','This plugin is not installed'], ['该插件已有其它操作,请等待完成','Wait for the other operation on this plugin to finish'], ['请通过 DSH CLI 管理市场插件自身','Manage the marketplace itself through the DSH CLI'], ['无效的插件操作','Invalid plugin operation'],
52
+
53
+ ['nav','Plugin Hub','Plugin Hub'],
54
+ ['插件市场','Marketplace'], ['。','.'], ['找到适合当前 DSH 的工具。','Find the right tools for your current DSH.'],
55
+ ['收录插件','Plugins'], ['有可安装版本','Installable'], ['使用与故障排查','Help & troubleshooting'],
56
+ ['本地演示','Local demo'], ['Nexus npm 请求已拦截,目录包、版本与安装结果均为模拟。','Nexus npm requests are mocked, including the catalog package, versions and installs.'],
57
+ ['切换场景','Demo scenarios'], ['正常目录','Normal catalog'], ['新插件登记','New plugin registered'], ['目录格式损坏','Invalid catalog JSON'], ['目录源不可用','Catalog unavailable'], ['查看拦截请求','View intercepted requests'], ['重置模拟安装','Reset simulated installs'],
58
+ ['目录更新失败,继续使用上一份有效数据','Catalog update failed. Using the last valid catalog.'], ['目录暂不可用','Catalog is currently unavailable'], ['重试','Retry'], ['关闭错误','Dismiss error'], ['关闭请求记录','Close request log'], ['请求拦截记录','Intercepted requests'], ['独立 MockAgent 返回 npm 元数据和目录压缩包,不连接真实 Nexus。','An isolated MockAgent returns npm metadata and catalog tarballs without contacting a real Nexus.'],
59
+ ['插件范围','Plugin scope'], ['全部插件','All plugins'], ['内部插件','Internal'], ['社区精选','Community'], ['已安装','Installed'], ['搜索插件','Search plugins'], ['搜索插件、用途或维护团队','Search plugins, purpose or owner'], ['清空搜索','Clear search'], ['按标签筛选','Filter by tag'], ['所有标签','All tags'], ['刷新','Refresh'], ['刷新目录与版本','Refresh catalog and versions'], ['“{query}” 的搜索结果','Results for “{query}”'], ['你的工作工具','Your tools'], ['为团队精选','Curated tools'], ['版本信息来自 npm · 按当前 DSH 检查声明','npm versions · checked against this DSH'],
60
+ ['内部','Internal'], ['精选','Curated'], ['选择版本','Version'], ['{name}版本','{name} version'], ['暂不可用','Unavailable'], ['推荐','Recommended'], ['发布于 {date}','Published {date}'], ['没有可用版本','No releases available'], ['排查指南','Troubleshooting'],
61
+ ['查询失败','Query failed'], ['兼容性未知','Compatibility unknown'], ['版本不兼容','Incompatible'], ['未满 {hours} 小时','Under {hours} hours'], ['发布时间未知','Release date unknown'], ['符合安装条件','Eligible to install'], ['暂不可安装','Not installable'],
62
+ ['已加入队列','Queued'], ['正在安装…','Installing…'], ['演示已安装','Demo installed'], ['模拟安装','Simulate install'], ['安装插件','Install plugin'], ['正在读取插件目录…','Loading plugin catalog…'], ['重新获取','Try again'], ['时间未知','Unknown time'],
63
+ ['没有找到匹配的插件','No matching plugins'], ['试试其它关键词,或清除当前筛选条件。','Try another keyword or clear your filters.'], ['查看全部插件','View all plugins'], ['未加载','Not loaded'], ['目录 {version} · 更新于 {date}','Catalog {version} · updated {date}'], ['市场 v{version}','Marketplace v{version}'],
64
+ ['“符合安装条件”依据版本声明与发布时间,实际下载仍以 Nexus 为准。','Eligibility is based on version declarations and release dates. Nexus determines actual download availability.'], ['本页包含模拟版本声明,不代表这些插件的真实兼容范围。','Version declarations on this page are simulated and do not represent actual plugin compatibility.'],
65
+ ['{count} 个插件正在排队或安装','{count} plugins queued or installing'], ['模拟安装已完成','Simulated installs finished'], ['安装任务已完成','Install tasks finished'], ['{count} 项模拟完成 · 不修改真实插件','{count} simulated operations completed · no real plugins changed'], ['{count} 项待手动重启生效','{count} installs awaiting a manual restart'], ['收起','Collapse'], ['查看任务','View tasks'], ['排队中','Queued'], ['安装中','Installing'], ['模拟完成','Simulated'], ['待重启','Restart required'], ['安装失败','Install failed'],
66
+ ['关闭详情','Close details'], ['维护团队','Maintainer'], ['当前宿主','Current host'], ['发行版本','Releases'], ['发布时间 {date} · DSH {range}','Published {date} · DSH {range}'], ['未声明','Not declared'], ['预计满 {hours} 小时:{date}','Eligible after {hours} hours: {date}'], ['打开 troubleshooting','Open troubleshooting'],
67
+ ['Agent 接入','Agent access'], ['开发工具','Developer tools'], ['知识文档','Knowledge'], ['效率工具','Productivity'], ['系统集成','Integrations'], ['界面','Interface'],
68
+ ['连接已断开,请确认本地 DSH 实例正在运行。恢复后页面会自动重连。','Disconnected. Check that the local DSH instance is running. This page will reconnect automatically.'],
69
+ ['请求超时,正在等待 DSH 响应。','Request timed out while waiting for DSH.'], ['请先登录当前 DSH 实例','Please authenticate with this DSH instance'], ['请求来源不匹配','Request origin does not match this DSH instance'], ['服务返回了无效响应,请刷新 DSH 页面。','The server returned an invalid response. Reload the DSH page.'],
70
+ ['未提供有效的 DSH 兼容声明','No valid DSH compatibility declaration'], ['仓库未返回有效发布时间','The registry did not provide a valid release date'], ['返回内容不是有效 JSON,已保留上一份可用数据','Invalid JSON received. The last valid catalog has been retained.'], ['仓库没有返回有效的发行版本','The registry returned no valid releases'], ['上次进程中断,请重新安装','The previous process was interrupted. Please install again.'], ['市场正在关闭','Marketplace is shutting down'], ['插件不在当前目录中','Plugin is not in the current catalog'], ['该版本不可安装','This version cannot be installed'], ['安装队列已满','Install queue is full'], ['无效的演示场景','Invalid demo scenario'], ['只在演示模式可用','Only available in demo mode'], ['响应超过大小限制','Response exceeds the size limit'],
71
+ ];
72
+ export const words = { zh: {}, en: {} };
73
+ for (const [key, en, zh = key] of pairs) { words.zh[key] = zh; words.en[key] = en; }
74
+ export const tagKeys = { agent: 'Agent 接入', 'developer-tools': '开发工具', knowledge: '知识文档', productivity: '效率工具', integration: '系统集成', ui: '界面' };
75
+
76
+ // Preserve third-party diagnostics verbatim; translate known marketplace diagnostics.
77
+ export function diagnostic(value, t, language) {
78
+ if (!value) return '';
79
+ if (words.en[value]) return t(value);
80
+ if (value.includes(';') || value.includes('; ')) return value.split(/;|; /).map(part => diagnostic(part, t, language)).join(language.startsWith('zh') ? ';' : '; ');
81
+ const entry = value.match(/^目录第 (\d+) 条:(.*)$/);
82
+ if (entry && !language.startsWith('zh')) return `Catalog entry ${entry[1]}: ${diagnostic(entry[2], t, language)}`;
83
+ if (language.startsWith('zh')) return value.replace(/^Duplicate catalog source id: (.+)$/, '数据源 ID 重复:$1');
84
+ return value
85
+ .replace(/(\S+) 无效/g, 'Invalid $1')
86
+ .replace(/(\S+) 不是有效 URL/g, '$1 is not a valid URL')
87
+ .replace(/(\S+) 必须是无凭据的 HTTPS 链接/g, '$1 must be an HTTPS URL without credentials')
88
+ .replace(/请求失败[( ]HTTP (\d+)[)]?/g, 'Request failed (HTTP $1)')
89
+ .replace(/作者声明不兼容 DSH (.+)/g, 'The author declares DSH $1 incompatible')
90
+ .replace(/插件命令失败(退出码 (\d+))/g, 'Plugin command failed (exit code $1)')
91
+ .replace(/要求 DSH (.+),当前为 (.+)/g, 'Requires DSH $1; current version is $2')
92
+ .replace(/要求 Node (.+)/g, 'Requires Node $1')
93
+ .replace(/无法确认宿主接口 (.+)/g, 'Cannot verify host interface $1')
94
+ .replace(/(.+) 要求 (.+),当前为 (.+)/g, '$1 requires $2; current version is $3')
95
+ .replace(/不支持当前 (os|cpu): (.+)/g, 'Unsupported $1: $2')
96
+ .replace(/发布未满 (\d+) 小时/g, 'Published less than $1 hours ago')
97
+ .replace(/发布未满 (\d+) 分钟/g, 'Published less than $1 minutes ago')
98
+ .replace(/已弃用:(.*)/g, 'Deprecated: $1');
99
+ }
100
+
101
+ export function localizePlugin(plugin, language) {
102
+ const locale = plugin.locales?.[language] ?? plugin.locales?.[language.split('-')[0]];
103
+ return locale ? { ...plugin, ...locale } : plugin;
104
+ }