@boostkit-dsh/market 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/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-09-22
4
+
5
+ - 从 `@lujinxing/market-ui` 迁入本项目,切换为 `@boostkit-dsh/*` 命名和 catalog v3。
6
+ - 市场目录识别 scope 原生短包名;鉴权引导使用 `@boostkit-dsh/auth-management`。
7
+ - 机器目录包缩短为 `@boostkit-dsh/market-catalog`,避免重复 scope 中的 `dsh`。
8
+ - 基于 DSH `0.1.7-alpha.1` 官方 Plugin Manager 服务完成安装、更新、修复和卸载。
9
+ - 未鉴权时只展示鉴权插件的最小引导入口,登录后展示完整本项目 catalog。
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @boostkit-dsh/market
2
+
3
+ 鲲鹏 DSH Demo 专用计算插件市场。源码由旧 `@lujinxing/market-ui` 的交互与分发经验迁入,但当前包由本仓库独立维护,只读取 `@boostkit-dsh/market-catalog`,不依赖旧市场源码或 npm 包。
4
+
5
+ 市场作为预安装引导插件,不对鉴权管理建立硬依赖。未检测到有效 GitCode ActorRef 时,只返回并展示 `@boostkit-dsh/auth-management`;鉴权完成后才展示完整目录。除首次安装鉴权插件外,市场变更通过鉴权服务记录最小化审计事件。
6
+
7
+ Host 直接使用 DSH `0.1.7-alpha.1` 的 `ctx.pluginManager`,由 DSH 负责 profile 文件锁、pnpm 执行、失败恢复和热加载结果。市场自身不能从自己的页面卸载。
8
+
9
+ 正式安装:
10
+
11
+ ```bash
12
+ dsh plugin --profile web add --save-exact @boostkit-dsh/market@0.1.0
13
+ ```
14
+
15
+ 在 npm 包发布前,可用绝对本地路径开发安装:
16
+
17
+ ```bash
18
+ dsh plugin --profile web add file:/absolute/path/to/kunpeng-dsh-demo/plugins/market
19
+ ```
20
+
21
+ 目录包和插件包必须先按版本发布到 npm;市场不会把 GitCode raw、本地仓库目录或相对路径作为正式安装源。
package/client.js ADDED
@@ -0,0 +1,167 @@
1
+ // Generated from src/client.ts by scripts/build-clients.mjs. Do not edit.
2
+ window.__ModuleLoader__.load({ id: "@boostkit-dsh/market", factory: (require2) => {
3
+ const module = { exports: {} };
4
+ const exports = module.exports;
5
+ const { createElement: h, useCallback, useEffect, useMemo, useState } = require2("react");
6
+ const NS = "boostkit-market";
7
+ const PANEL_ID = "boostkit-market";
8
+ const AUTH_PACKAGE = "@boostkit-dsh/auth-management";
9
+ const AUTH_REQUIRED_EVENT = "kunpeng-auth-required";
10
+ const dictionaries = {
11
+ zh: {
12
+ nav: "\u9CB2\u9E4F\u63D2\u4EF6\u5E02\u573A",
13
+ title: "\u9CB2\u9E4F\u8BA1\u7B97\u63D2\u4EF6\u5E02\u573A",
14
+ subtitle: "\u53EA\u5C55\u793A\u5E76\u7BA1\u7406 kunpeng-dsh-demo catalog \u4E2D\u7684\u63D2\u4EF6\u3002",
15
+ bootstrap: "\u5C1A\u672A\u5B8C\u6210 GitCode \u9274\u6743\u3002\u4E3A\u907F\u514D\u533F\u540D\u6D4F\u89C8\uFF0C\u76EE\u524D\u53EA\u663E\u793A\u9274\u6743\u63D2\u4EF6\u3002",
16
+ login: "\u524D\u5F80 GitCode \u9274\u6743",
17
+ offline: "npm \u76EE\u5F55\u6682\u4E0D\u53EF\u7528",
18
+ offlineHelp: "\u4ECD\u53EF\u67E5\u770B\u5DF2\u5B89\u88C5\u7684\u53EF\u89C1\u63D2\u4EF6\uFF1B\u5B89\u88C5\u548C\u66F4\u65B0\u6682\u4E0D\u53EF\u7528\u3002",
19
+ search: "\u641C\u7D22\u63D2\u4EF6\u540D\u79F0\u3001\u5305\u540D\u6216\u8BF4\u660E\u2026",
20
+ installed: "\u5DF2\u5B89\u88C5",
21
+ enabled: "\u5DF2\u542F\u7528",
22
+ update: "\u53EF\u66F4\u65B0",
23
+ deprecated: "\u5DF2\u5F03\u7528",
24
+ install: "\u5B89\u88C5",
25
+ upgrade: "\u66F4\u65B0",
26
+ repair: "\u4FEE\u590D",
27
+ remove: "\u5378\u8F7D",
28
+ working: "\u5904\u7406\u4E2D\u2026",
29
+ refresh: "\u5237\u65B0",
30
+ empty: "\u6CA1\u6709\u7B26\u5408\u6761\u4EF6\u7684\u63D2\u4EF6",
31
+ failed: "\u64CD\u4F5C\u5931\u8D25",
32
+ done: "\u64CD\u4F5C\u5DF2\u63D0\u4EA4",
33
+ restart: "\u9700\u8981\u91CD\u542F DSH \u540E\u751F\u6548",
34
+ confirm: "\u786E\u8BA4\u5BF9\u63D2\u4EF6\u6267\u884C\u6B64\u64CD\u4F5C\u5417\uFF1F"
35
+ },
36
+ en: {
37
+ nav: "Kunpeng Market",
38
+ title: "Kunpeng Plugin Market",
39
+ subtitle: "Only plugins from the kunpeng-dsh-demo catalog are shown and managed.",
40
+ bootstrap: "GitCode authentication is not complete. Only the authentication plugin is visible.",
41
+ login: "Open GitCode authentication",
42
+ offline: "The npm catalog is unavailable",
43
+ offlineHelp: "Visible installed plugins remain readable; installs and updates are disabled.",
44
+ search: "Search plugin name, package, or description\u2026",
45
+ installed: "Installed",
46
+ enabled: "Enabled",
47
+ update: "Update",
48
+ deprecated: "Deprecated",
49
+ install: "Install",
50
+ upgrade: "Update",
51
+ repair: "Repair",
52
+ remove: "Remove",
53
+ working: "Working\u2026",
54
+ refresh: "Refresh",
55
+ empty: "No matching plugins",
56
+ failed: "Operation failed",
57
+ done: "Operation submitted",
58
+ restart: "Restart DSH to apply this change",
59
+ confirm: "Continue with this plugin operation?"
60
+ }
61
+ };
62
+ const styles = {
63
+ page: { flex: 1, minHeight: 0, overflow: "auto", boxSizing: "border-box", padding: "24px clamp(18px,4vw,48px)", background: "var(--dsw-alias-bg-base)" },
64
+ content: { width: "100%", maxWidth: 1180, margin: "0 auto", display: "flex", flexDirection: "column", gap: 14 },
65
+ row: { display: "flex", alignItems: "center", gap: 9, flexWrap: "wrap" },
66
+ grow: { flex: 1 },
67
+ title: { margin: 0, fontSize: 20, color: "var(--dsw-alias-label-primary)" },
68
+ muted: { color: "var(--dsw-alias-label-tertiary)", fontSize: 12, lineHeight: 1.6 },
69
+ search: { minWidth: 280, flex: 1, padding: "8px 10px", borderRadius: 7, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" },
70
+ grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill,minmax(300px,1fr))", gap: 12 },
71
+ card: { padding: 14, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l1)", background: "var(--dsw-alias-bg-layer-1)", display: "flex", flexDirection: "column", gap: 9 },
72
+ badge: { padding: "1px 7px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2)", color: "var(--dsw-alias-label-secondary)", fontSize: 11 },
73
+ button: { padding: "6px 12px", borderRadius: 7, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-2)", color: "var(--dsw-alias-label-primary)", cursor: "pointer" },
74
+ primary: { padding: "6px 12px", borderRadius: 7, border: "1px solid var(--dsw-alias-state-business-primary)", background: "var(--dsw-alias-state-business-primary)", color: "#fff", cursor: "pointer" },
75
+ notice: { padding: 12, borderRadius: 9, border: "1px solid var(--dsw-alias-state-business-primary)", color: "var(--dsw-alias-label-primary)" },
76
+ error: { padding: 12, borderRadius: 9, border: "1px solid var(--dsw-alias-state-error-primary)", color: "var(--dsw-alias-state-error-primary)" },
77
+ link: { color: "var(--dsw-alias-state-business-primary)", textDecoration: "none" }
78
+ };
79
+ async function responseJson(response) {
80
+ const value = await response.json().catch(() => ({}));
81
+ if (!response.ok || value?.ok === false) throw Object.assign(new Error(value?.error?.message ?? `HTTP ${response.status}`), { code: value?.error?.code });
82
+ return value;
83
+ }
84
+ function MarketIcon({ size = 16, active = false }) {
85
+ const color = active ? "var(--dsw-alias-state-business-primary)" : "currentColor";
86
+ return h(
87
+ "svg",
88
+ { width: size, height: size, viewBox: "0 0 24 24", fill: "none", "aria-hidden": true },
89
+ h("path", { d: "M4 9h16l-1-5H5L4 9Zm1 2v9h14v-9M9 20v-5h6v5M7 9v2m5-2v2m5-2v2", stroke: color, strokeWidth: 1.6, strokeLinecap: "round", strokeLinejoin: "round" })
90
+ );
91
+ }
92
+ function MarketPage({ t }) {
93
+ const [state, setState] = useState(null);
94
+ const [query, setQuery] = useState("");
95
+ const [busy, setBusy] = useState(null);
96
+ const [error, setError] = useState(null);
97
+ const [notice, setNotice] = useState(null);
98
+ const load = useCallback(async () => {
99
+ setError(null);
100
+ try {
101
+ setState(await fetch("/boostkit-market/catalog", { cache: "no-store" }).then(responseJson));
102
+ } catch (value) {
103
+ setError(value?.message ?? String(value));
104
+ }
105
+ }, []);
106
+ useEffect(() => {
107
+ void load();
108
+ }, [load]);
109
+ const entries = useMemo(() => (state?.plugins ?? []).filter((entry) => {
110
+ const needle = query.trim().toLowerCase();
111
+ if (!needle) return true;
112
+ return [entry.pkg, entry.dir, entry.description, entry.displayName?.zh, entry.displayName?.en].some((value) => String(value ?? "").toLowerCase().includes(needle));
113
+ }), [query, state]);
114
+ const act = useCallback(async (kind, entry) => {
115
+ if (!globalThis.confirm(`${t("confirm")}
116
+ ${entry.pkg}
117
+ ${kind}`)) return;
118
+ setBusy(entry.pkg);
119
+ setError(null);
120
+ setNotice(null);
121
+ try {
122
+ const value = await fetch(`/boostkit-market/${kind}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ pkg: entry.pkg }) }).then(responseJson);
123
+ setNotice(value.result?.application === "restart-required" ? t("restart") : t("done"));
124
+ await load();
125
+ } catch (value) {
126
+ setError(value?.message ?? String(value));
127
+ } finally {
128
+ setBusy(null);
129
+ }
130
+ }, [load, t]);
131
+ const openLogin = () => window.dispatchEvent(new CustomEvent(AUTH_REQUIRED_EVENT));
132
+ return h("main", { style: styles.page }, h(
133
+ "div",
134
+ { style: styles.content },
135
+ h("div", { style: styles.row }, h("div", null, h("h2", { style: styles.title }, t("title")), h("div", { style: styles.muted }, t("subtitle"))), h("span", { style: styles.grow }), state ? h("span", { style: styles.badge }, `v${state.marketVersion}`) : null),
136
+ state && !state.authenticated ? h("section", { style: styles.notice }, h("div", { style: styles.row }, h("strong", null, t("bootstrap")), h("span", { style: styles.grow }), state.plugins?.some((entry) => entry.pkg === AUTH_PACKAGE && entry.installed) ? h("button", { type: "button", style: styles.primary, onClick: openLogin }, t("login")) : null)) : null,
137
+ state && !state.connected ? h("section", { style: styles.error }, h("strong", null, t("offline")), h("div", { style: styles.muted }, t("offlineHelp")), h("div", { style: styles.muted }, state.error)) : null,
138
+ error ? h("section", { style: styles.error }, `${t("failed")}\uFF1A${error}`) : null,
139
+ notice ? h("section", { style: styles.notice }, notice) : null,
140
+ h("div", { style: styles.row }, h("input", { style: styles.search, value: query, placeholder: t("search"), onChange: (event) => setQuery(event.target.value) }), h("button", { type: "button", style: styles.button, disabled: busy !== null, onClick: () => void load() }, t("refresh"))),
141
+ h("div", { style: styles.grid }, entries.length ? entries.map((entry) => {
142
+ const working = busy === entry.pkg;
143
+ const action = !entry.installed ? "install" : entry.updateAvailable ? "update" : entry.error ? "repair" : null;
144
+ const disabled = busy !== null || (action === "install" || action === "update" || action === "repair") && state?.connected !== true;
145
+ return h(
146
+ "article",
147
+ { key: entry.pkg, style: styles.card },
148
+ h("div", { style: styles.row }, h("strong", null, entry.displayName?.zh ?? entry.dir), h("span", { style: styles.grow }), action ? h("button", { type: "button", style: styles.primary, disabled, onClick: () => void act(action, entry) }, working ? t("working") : t(action === "update" ? "upgrade" : action)) : null, entry.installed && entry.canRemove && state?.authenticated === true ? h("button", { type: "button", style: styles.button, disabled: busy !== null, onClick: () => void act("remove", entry) }, t("remove")) : null),
149
+ h("div", { style: styles.muted }, `${entry.pkg} \xB7 ${entry.version || entry.currentVersion || "?"}`),
150
+ h("div", null, entry.description),
151
+ h("div", { style: styles.row }, entry.installed ? h("span", { style: styles.badge }, t("installed")) : null, entry.enabled ? h("span", { style: styles.badge }, t("enabled")) : null, entry.updateAvailable ? h("span", { style: styles.badge }, t("update")) : null, entry.deprecated ? h("span", { style: styles.badge }, t("deprecated")) : null),
152
+ h("div", { style: styles.row }, h("a", { style: styles.link, href: entry.readmeUrl, target: "_blank", rel: "noreferrer" }, "README"), h("a", { style: styles.link, href: entry.changelogUrl, target: "_blank", rel: "noreferrer" }, "CHANGELOG"))
153
+ );
154
+ }) : h("div", { style: styles.muted }, state ? t("empty") : "\u2026"))
155
+ ));
156
+ }
157
+ function apply(ctx) {
158
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "boostkit-market.dictionaries");
159
+ const t = ctx.locale.bind(NS);
160
+ ctx.slots.inject("main", () => ctx.slots.register({ name: "main", key: PANEL_ID, locale: NS, inject: () => ({ t }) }, MarketPage));
161
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({ name: "sidebar.panellist", id: PANEL_ID, order: 5, label: () => t("nav"), locale: NS }, MarketIcon));
162
+ }
163
+ exports.name = "boostkit-market-client";
164
+ exports.inject = ["slots", "locale", "layout"];
165
+ exports.apply = apply;
166
+ return module.exports;
167
+ } });
@@ -0,0 +1,4 @@
1
+ # 本项目专用计算插件市场;作为安装鉴权插件的预置引导入口。
2
+ - insert:
3
+ - id: boostkit-market
4
+ name: '@boostkit-dsh/market'
package/index.js ADDED
@@ -0,0 +1,340 @@
1
+ /** Project-owned plugin market backed by DSH 0.1.7's Plugin Manager service. */
2
+ import { createHash } from 'node:crypto'
3
+ import { readFileSync } from 'node:fs'
4
+ import { gunzipSync } from 'node:zlib'
5
+
6
+ export const name = 'boostkit-market'
7
+ export const inject = ['pluginManager']
8
+
9
+ const API_PREFIX = '/boostkit-market'
10
+ const REPO_BASE = 'https://gitcode.com/lujinxing/kunpeng-dsh-demo'
11
+ const CATALOG_PACKAGE = '@boostkit-dsh/market-catalog'
12
+ const DSH_VERSION = '0.1.7-alpha.1'
13
+ const SELF_PACKAGE = '@boostkit-dsh/market'
14
+ const AUTH_PACKAGE = '@boostkit-dsh/auth-management'
15
+ const FALLBACK_DISPLAY_NAMES = {
16
+ [AUTH_PACKAGE]: { zh: '鉴权与审计管理', en: 'Authentication & Audit' },
17
+ [SELF_PACKAGE]: { zh: '鲲鹏插件市场', en: 'Kunpeng Plugin Market' },
18
+ '@boostkit-dsh/hello': { zh: 'Hello 示例插件', en: 'Hello Example Plugin' },
19
+ }
20
+ const REGISTRIES = ['https://registry.npmjs.org', 'https://registry.npmmirror.com']
21
+ const PACKAGE_RE = /^@boostkit-dsh\/[a-z0-9][a-z0-9.-]*$/
22
+ const NAME_RE = /^[a-z0-9][a-z0-9.-]*$/
23
+ const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
24
+ const REQUEST_TIMEOUT_MS = 15_000
25
+ const BOOT_ID = `${process.pid}-${Date.now()}`
26
+ let cachedCatalog = null
27
+ let activeOperation = null
28
+
29
+ function readJson(file) {
30
+ try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null }
31
+ }
32
+ const MARKET_VERSION = readJson(new URL('./package.json', import.meta.url))?.version ?? ''
33
+
34
+ function sendJson(response, status, value) {
35
+ if (response.destroyed || response.writableEnded) return
36
+ response.writeHead(status, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' })
37
+ response.end(JSON.stringify(value))
38
+ }
39
+
40
+ function sameOrigin(request) {
41
+ const origin = request.headers.origin
42
+ const host = request.headers.host
43
+ if (typeof origin !== 'string' || typeof host !== 'string') return false
44
+ try { return new URL(origin).host === host } catch { return false }
45
+ }
46
+
47
+ async function readBody(request, maxBytes = 4096) {
48
+ const chunks = []
49
+ let size = 0
50
+ for await (const chunk of request) {
51
+ const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
52
+ size += value.length
53
+ if (size > maxBytes) throw new Error('request body too large')
54
+ chunks.push(value)
55
+ }
56
+ const text = Buffer.concat(chunks).toString('utf8').trim()
57
+ return text === '' ? {} : JSON.parse(text)
58
+ }
59
+
60
+ function tarFile(tarball, wanted) {
61
+ if (tarball.length > 1024 * 1024) throw new Error('npm 目录 tarball 超过 1 MiB')
62
+ const archive = gunzipSync(tarball, { maxOutputLength: 5 * 1024 * 1024 })
63
+ for (let offset = 0; offset + 512 <= archive.length;) {
64
+ const header = archive.subarray(offset, offset + 512)
65
+ if (header.every((byte) => byte === 0)) break
66
+ const field = (start, length) => header.subarray(start, start + length).toString('utf8').replace(/\0.*$/s, '')
67
+ const name = field(0, 100)
68
+ const prefix = field(345, 155)
69
+ const path = prefix ? `${prefix}/${name}` : name
70
+ const size = Number.parseInt(field(124, 12).trim() || '0', 8)
71
+ if (!Number.isSafeInteger(size) || size < 0) throw new Error('npm tarball 文件大小非法')
72
+ const start = offset + 512
73
+ if (start + size > archive.length) throw new Error('npm tarball 文件越界')
74
+ if (path === wanted) return archive.subarray(start, start + size).toString('utf8')
75
+ offset = start + Math.ceil(size / 512) * 512
76
+ }
77
+ throw new Error(`npm tarball 缺少 ${wanted}`)
78
+ }
79
+
80
+ function validateCatalog(value) {
81
+ if (value?.version !== 3 || !SEMVER_RE.test(String(value.release ?? '')) || value.dsh !== DSH_VERSION) throw new Error('catalog.json 协议或 DSH 版本不匹配')
82
+ if (!Array.isArray(value.plugins) || !Array.isArray(value.solutions)) throw new Error('catalog.json 缺少插件或解决方案列表')
83
+ const packages = new Map()
84
+ for (const entry of value.plugins) {
85
+ if (!NAME_RE.test(String(entry?.dir ?? '')) || !PACKAGE_RE.test(String(entry?.pkg ?? ''))) throw new Error(`catalog.json 包含非法插件:${String(entry?.pkg ?? entry?.dir)}`)
86
+ if (!SEMVER_RE.test(String(entry.version ?? '')) || entry?.distribution?.type !== 'npm' || entry.distribution.spec !== `${entry.pkg}@${entry.version}`) throw new Error(`${entry.pkg} 的 npm 分发信息非法`)
87
+ if (entry.compatibility?.dsh !== DSH_VERSION || !Array.isArray(entry.allowedBuilds)) throw new Error(`${entry.pkg} 的兼容或构建信息非法`)
88
+ if (packages.has(entry.pkg)) throw new Error(`catalog.json 包名重复:${entry.pkg}`)
89
+ packages.set(entry.pkg, entry)
90
+ }
91
+ for (const entry of value.plugins) {
92
+ for (const [dependency, version] of Object.entries(entry.dependencies ?? {})) {
93
+ if (packages.get(dependency)?.version !== version) throw new Error(`${entry.pkg} 的依赖 ${dependency}@${version} 不在目录中`)
94
+ }
95
+ }
96
+ if (!packages.has(AUTH_PACKAGE) || !packages.has(SELF_PACKAGE)) throw new Error('catalog.json 缺少市场或鉴权引导插件')
97
+ return value
98
+ }
99
+
100
+ function manifestRelease(manifest) {
101
+ if (!SEMVER_RE.test(String(manifest?.version ?? '')) || !/^[a-f0-9]{64}$/.test(String(manifest?.dshCatalogSha256 ?? ''))) throw new Error('目录包 manifest 非法')
102
+ const tarball = manifest?.dist?.tarball
103
+ const integrity = manifest?.dist?.integrity
104
+ if (typeof tarball !== 'string' || !tarball.startsWith('https://') || typeof integrity !== 'string' || !integrity.startsWith('sha512-')) throw new Error('目录包 tarball 信息非法')
105
+ return { version: manifest.version, sha256: manifest.dshCatalogSha256, tarball, integrity }
106
+ }
107
+
108
+ async function remoteCatalog(fetchImpl = fetch) {
109
+ const controller = new AbortController()
110
+ const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS)
111
+ const failures = []
112
+ try {
113
+ for (const registry of REGISTRIES) {
114
+ try {
115
+ const manifestResponse = await fetchImpl(`${registry}/${encodeURIComponent(CATALOG_PACKAGE)}/latest`, { signal: controller.signal, cache: 'no-store' })
116
+ if (!manifestResponse.ok) throw new Error(`manifest HTTP ${manifestResponse.status}`)
117
+ const release = manifestRelease(await manifestResponse.json())
118
+ if (cachedCatalog?.version === release.version && cachedCatalog.sha256 === release.sha256) return cachedCatalog.value
119
+ const tarballResponse = await fetchImpl(release.tarball, { signal: controller.signal, cache: 'force-cache' })
120
+ if (!tarballResponse.ok) throw new Error(`tarball HTTP ${tarballResponse.status}`)
121
+ const tarball = Buffer.from(await tarballResponse.arrayBuffer())
122
+ const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`
123
+ if (integrity !== release.integrity) throw new Error('目录包 tarball integrity 校验失败')
124
+ const text = tarFile(tarball, 'package/catalog.json')
125
+ if (createHash('sha256').update(text).digest('hex') !== release.sha256) throw new Error('catalog SHA-256 校验失败')
126
+ const value = validateCatalog(JSON.parse(text))
127
+ if (value.release !== release.version) throw new Error('catalog release 与目录包版本不一致')
128
+ cachedCatalog = { version: release.version, sha256: release.sha256, value }
129
+ return value
130
+ } catch (error) {
131
+ failures.push(`${registry}: ${String(error?.message ?? error)}`)
132
+ }
133
+ }
134
+ throw new Error(failures.join(';') || 'npm 目录不可用')
135
+ } finally {
136
+ clearTimeout(timer)
137
+ }
138
+ }
139
+
140
+ function compareVersions(left, right) {
141
+ const parse = (value) => SEMVER_RE.exec(String(value ?? ''))?.slice(1).map(Number)
142
+ const a = parse(left)
143
+ const b = parse(right)
144
+ if (!a || !b) return 0
145
+ for (let index = 0; index < 3; index += 1) if (a[index] !== b[index]) return a[index] > b[index] ? 1 : -1
146
+ return 0
147
+ }
148
+
149
+ function entryLinks(entry) {
150
+ return {
151
+ sourceUrl: `${REPO_BASE}/tree/main/plugins/${entry.dir}`,
152
+ readmeUrl: `${REPO_BASE}/blob/main/${entry.readme ?? `plugins/${entry.dir}/README.md`}`,
153
+ changelogUrl: `${REPO_BASE}/blob/main/${entry.changelog ?? `plugins/${entry.dir}/CHANGELOG.md`}`,
154
+ }
155
+ }
156
+
157
+ function installedEntry(bundle) {
158
+ const packageName = bundle.name
159
+ const short = packageName.replace(/^@boostkit-dsh\//, '')
160
+ const fallbackName = FALLBACK_DISPLAY_NAMES[packageName] ?? { zh: short }
161
+ return {
162
+ dir: short,
163
+ pkg: packageName,
164
+ version: bundle.version ?? '',
165
+ currentVersion: bundle.version ?? '',
166
+ displayName: {
167
+ zh: bundle.meta?.displayName?.zh ?? fallbackName.zh,
168
+ en: bundle.meta?.displayName?.en ?? fallbackName.en,
169
+ },
170
+ description: bundle.description ?? '',
171
+ category: bundle.meta?.category ?? 'kunpeng-rd',
172
+ maintainers: [],
173
+ compatibility: { dsh: DSH_VERSION },
174
+ allowedBuilds: [],
175
+ dependencies: {},
176
+ deprecated: false,
177
+ listed: false,
178
+ installed: bundle.installed === true,
179
+ enabled: bundle.enabled === true,
180
+ canRemove: bundle.removable === true && packageName !== SELF_PACKAGE,
181
+ updateAvailable: false,
182
+ error: bundle.error,
183
+ ...entryLinks({ dir: short }),
184
+ }
185
+ }
186
+
187
+ function mergeCatalog(catalog, bundles) {
188
+ const installed = new Map(bundles.filter((bundle) => PACKAGE_RE.test(bundle.name)).map((bundle) => [bundle.name, installedEntry(bundle)]))
189
+ const entries = catalog.plugins.map((entry) => {
190
+ const current = installed.get(entry.pkg)
191
+ installed.delete(entry.pkg)
192
+ return {
193
+ ...entry,
194
+ ...entryLinks(entry),
195
+ listed: true,
196
+ installed: current?.installed === true,
197
+ enabled: current?.enabled === true,
198
+ currentVersion: current?.currentVersion ?? '',
199
+ canRemove: entry.pkg !== SELF_PACKAGE && current?.canRemove === true,
200
+ updateAvailable: current?.installed === true && compareVersions(entry.version, current.currentVersion) > 0,
201
+ error: current?.error,
202
+ }
203
+ })
204
+ entries.push(...installed.values())
205
+ return entries.sort((left, right) => left.dir.localeCompare(right.dir))
206
+ }
207
+
208
+ function authService(ctx) {
209
+ try { return ctx.get?.('authManagement') ?? ctx.authManagement ?? null } catch { return null }
210
+ }
211
+
212
+ async function accessState(ctx) {
213
+ const auth = authService(ctx)
214
+ if (auth === null || typeof auth.requireActor !== 'function') return { authenticated: false, auth: null }
215
+ try { return { authenticated: true, auth, actor: await auth.requireActor() } } catch (error) {
216
+ if (error?.code === 'AUTH_REQUIRED' || error?.code === 'AUTH_UNAVAILABLE') return { authenticated: false, auth, authError: error.code }
217
+ throw error
218
+ }
219
+ }
220
+
221
+ function bootstrapEntries(entries, authenticated) {
222
+ return authenticated
223
+ ? entries
224
+ : entries
225
+ .filter((entry) => entry.pkg === AUTH_PACKAGE)
226
+ .map((entry) => ({ ...entry, canRemove: false }))
227
+ }
228
+
229
+ async function marketState(ctx, catalogLoader = remoteCatalog) {
230
+ const access = await accessState(ctx)
231
+ const bundles = await ctx.pluginManager.listBundles()
232
+ try {
233
+ const catalog = await catalogLoader()
234
+ return {
235
+ ok: true,
236
+ connected: true,
237
+ authenticated: access.authenticated,
238
+ authError: access.authError,
239
+ source: `${REPO_BASE}/tree/main`,
240
+ marketVersion: MARKET_VERSION,
241
+ bootId: BOOT_ID,
242
+ plugins: bootstrapEntries(mergeCatalog(catalog, bundles), access.authenticated),
243
+ }
244
+ } catch (error) {
245
+ const installed = bundles.filter((bundle) => PACKAGE_RE.test(bundle.name)).map(installedEntry)
246
+ return {
247
+ ok: true,
248
+ connected: false,
249
+ authenticated: access.authenticated,
250
+ authError: access.authError,
251
+ error: `无法连接 npm 目录服务:${String(error?.message ?? error)}`,
252
+ source: `${REPO_BASE}/tree/main`,
253
+ marketVersion: MARKET_VERSION,
254
+ bootId: BOOT_ID,
255
+ plugins: bootstrapEntries(installed, access.authenticated),
256
+ }
257
+ }
258
+ }
259
+
260
+ function publicFailure(error) {
261
+ const code = typeof error?.code === 'string' ? error.code : 'MARKET_OPERATION_FAILED'
262
+ const message = code === 'AUTH_REQUIRED' ? '请先安装鉴权插件并完成 GitCode 登录。' : String(error?.message ?? error)
263
+ return { code, message }
264
+ }
265
+
266
+ async function mutate(ctx, kind, pkg, catalogLoader = remoteCatalog) {
267
+ if (!PACKAGE_RE.test(pkg)) return { status: 400, body: { ok: false, error: { code: 'INVALID_PACKAGE', message: '插件包名无效。' } } }
268
+ if (activeOperation !== null) return { status: 409, body: { ok: false, error: { code: 'MARKET_BUSY', message: `正在处理 ${activeOperation}` } } }
269
+ if (kind === 'remove' && pkg === SELF_PACKAGE) return { status: 400, body: { ok: false, error: { code: 'SELF_REMOVE_FORBIDDEN', message: '市场不能从自身页面卸载。' } } }
270
+
271
+ const access = await accessState(ctx)
272
+ const bootstrapAllowed = pkg === AUTH_PACKAGE && ['install', 'update', 'repair'].includes(kind)
273
+ if (!access.authenticated && !bootstrapAllowed) return { status: 401, body: { ok: false, error: { code: 'AUTH_REQUIRED', message: '请先安装鉴权插件并完成 GitCode 登录。' } } }
274
+
275
+ activeOperation = `${kind}:${pkg}`
276
+ try {
277
+ let entry = null
278
+ if (kind !== 'remove') {
279
+ const catalog = await catalogLoader()
280
+ entry = catalog.plugins.find((item) => item.pkg === pkg)
281
+ if (!entry) return { status: 404, body: { ok: false, error: { code: 'PLUGIN_NOT_LISTED', message: '目录中不存在该插件。' } } }
282
+ if (entry.deprecated && kind === 'install') return { status: 409, body: { ok: false, error: { code: 'PLUGIN_DEPRECATED', message: '该插件已弃用,禁止新安装。' } } }
283
+ }
284
+
285
+ const perform = () => kind === 'remove'
286
+ ? ctx.pluginManager.removeBundle(pkg)
287
+ : ctx.pluginManager.installBundle(entry.distribution.spec, { enabled: true })
288
+ const result = access.authenticated && access.auth?.executeAudited
289
+ ? await access.auth.executeAudited({ action: `market.${kind}`, target: { type: 'plugin', id: pkg, label: pkg } }, perform)
290
+ : await perform()
291
+ const ok = result?.error === undefined && result?.application !== 'failed' && result?.application !== 'cancelled'
292
+ return { status: ok ? 200 : 500, body: { ok, kind, pkg, result } }
293
+ } catch (error) {
294
+ const failure = publicFailure(error)
295
+ return { status: failure.code === 'AUTH_REQUIRED' ? 401 : 500, body: { ok: false, error: failure } }
296
+ } finally {
297
+ activeOperation = null
298
+ }
299
+ }
300
+
301
+ export function apply(ctx, config = {}) {
302
+ const catalogLoader = config.catalogLoader ?? remoteCatalog
303
+ ctx.inject(['webServer'], (host) => {
304
+ host.effect(() => {
305
+ const routes = [
306
+ {
307
+ kind: 'exact', path: `${API_PREFIX}/catalog`, handler: (request, response) => {
308
+ if (request.method !== 'GET') { response.writeHead(405, { allow: 'GET' }); response.end(); return }
309
+ void marketState(ctx, catalogLoader).then((value) => sendJson(response, 200, value)).catch((error) => sendJson(response, 500, { ok: false, error: publicFailure(error) }))
310
+ },
311
+ },
312
+ ...['install', 'update', 'repair', 'remove'].map((kind) => ({
313
+ kind: 'exact', path: `${API_PREFIX}/${kind}`, handler: (request, response) => {
314
+ if (request.method !== 'POST') { response.writeHead(405, { allow: 'POST' }); response.end(); return }
315
+ if (!sameOrigin(request)) { sendJson(response, 403, { ok: false, error: { code: 'ORIGIN_REQUIRED', message: '只允许 DSH 同源页面修改插件。' } }); return }
316
+ void readBody(request)
317
+ .then((body) => mutate(ctx, kind, body.pkg, catalogLoader))
318
+ .then((outcome) => sendJson(response, outcome.status, outcome.body))
319
+ .catch((error) => sendJson(response, 400, { ok: false, error: publicFailure(error) }))
320
+ },
321
+ })),
322
+ ]
323
+ const disposers = routes.map((route) => host.webServer.register(route))
324
+ return () => { for (const dispose of disposers) dispose() }
325
+ }, 'boostkit-market.http')
326
+ })
327
+ }
328
+
329
+ export const __testing = {
330
+ AUTH_PACKAGE,
331
+ SELF_PACKAGE,
332
+ accessState,
333
+ bootstrapEntries,
334
+ compareVersions,
335
+ mergeCatalog,
336
+ mutate,
337
+ remoteCatalog,
338
+ sameOrigin,
339
+ validateCatalog,
340
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@boostkit-dsh/market",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "本项目专用计算插件市场:浏览、安装、更新和卸载鲲鹏 DSH Demo 插件",
6
+ "main": "index.js",
7
+ "files": [
8
+ "CHANGELOG.md",
9
+ "README.md",
10
+ "client.js",
11
+ "cordis.patch.yml",
12
+ "index.js"
13
+ ],
14
+ "license": "UNLICENSED",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://gitcode.com/lujinxing/kunpeng-dsh-demo.git",
18
+ "directory": "plugins/market"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "registry": "https://registry.npmjs.org"
23
+ },
24
+ "exports": {
25
+ ".": "./index.js",
26
+ "./client": "./client.js",
27
+ "./package.json": "./package.json"
28
+ },
29
+ "scripts": {
30
+ "test": "node --test test/*.test.mjs"
31
+ },
32
+ "dsh": {
33
+ "bundle": {
34
+ "patch": "./cordis.patch.yml"
35
+ },
36
+ "client": {
37
+ "platform": "web",
38
+ "inject": [
39
+ "@deepseek-ai/dsh-client-locale",
40
+ "@deepseek-ai/dsh-client-ui-layout",
41
+ "@deepseek-ai/dsh-client-ui-sidebar"
42
+ ]
43
+ },
44
+ "market": {
45
+ "displayName": {
46
+ "zh": "鲲鹏计算插件市场",
47
+ "en": "Kunpeng Plugin Market"
48
+ },
49
+ "category": "kunpeng-rd",
50
+ "maintainers": [
51
+ "@lujinxing"
52
+ ],
53
+ "compatibility": {
54
+ "dsh": "0.1.7-alpha.1"
55
+ },
56
+ "allowedBuilds": []
57
+ }
58
+ }
59
+ }