@eya46/dsh-plugin-info 0.0.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 eya46
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @eya46/dsh-plugin-info
2
+
3
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的插件信息页。包名 `@eya46/dsh-plugin-info`,挂到当前 Profile 后,设置里会出现「插件信息」分区。
4
+
5
+ 它只看 **Profile 用户自己加的插件**(`profiles/<name>/package.json` 的 `dependencies`),不把 `@deepseek-ai/dsh-base` 这类随发行版带的 bundle 算进去。默认仅展示 `@eya46/` 命名空间下的插件(见下方 `allowScopes`),可按需放开。
6
+
7
+ - 列出已安装插件、当前版本、来源(npm / 本地 / git)
8
+ - 对 registry 安装的包对照 `dist-tags.latest`,标出可更新
9
+ - 展开后默认看最近 **10** 条版本
10
+ - 点某个版本读更新说明:优先 GitHub Release,没有就回落到相邻版本的 compare / commit
11
+
12
+ 实现方式对齐 [dsh-model-meta-autofill](https://github.com/QJAG1024/dsh-model-meta-autofill):Host 注册本机 HTTP API,Client half 通过 `dsh.client` 挂到 `settings.section`。
13
+
14
+ ## 安装
15
+
16
+ 在仓库根目录构建后,把它加进当前 Profile:
17
+
18
+ ```sh
19
+ pnpm install
20
+ pnpm --filter @eya46/dsh-plugin-info build
21
+ dsh plugin --profile web add "file:./packages/dsh-plugin-info"
22
+ ```
23
+
24
+ `dsh.bundle` 会把 `@eya46/dsh-plugin-info` 写进该 Profile 的 bundle 列表。重启 `dsh web` 后,打开设置 → **插件信息**。
25
+
26
+ ## HTTP
27
+
28
+ 本机回环可用:
29
+
30
+ | 端点 | 说明 |
31
+ | --- | --- |
32
+ | `GET /plugin-info/api/plugins` | 当前 Profile 用户插件 + 是否有更新 |
33
+ | `GET /plugin-info/api/versions?name=<pkg>&limit=10` | 最近版本,默认 10 条 |
34
+ | `GET /plugin-info/api/notes?name=<pkg>&version=<ver>` | 该版本的更新说明 |
35
+
36
+ ## 配置
37
+
38
+ | 键 | 默认 | 说明 |
39
+ | --- | --- | --- |
40
+ | `registryUrl` | `https://registry.npmjs.org` | npm 源 |
41
+ | `defaultVersionLimit` | `10` | 默认版本条数 |
42
+ | `timeoutMs` | `15000` | 注册表 / GitHub 超时 |
43
+ | `cacheTtlMs` | `300000` | packument 与说明缓存 |
44
+ | `allowScopes` | `['@eya46/']` | 名称前缀白名单;为空数组时展示全部用户插件 |
45
+
46
+ 覆盖写在 Profile 的 `cordis.patch.yml`:
47
+
48
+ ```yaml
49
+ - id: dsh-plugin-info
50
+ config:
51
+ defaultVersionLimit: 10
52
+ ```
53
+
54
+ 本地 / `file:` / `link:` 安装没有 registry latest,不会标「可更新」。GitHub 说明在有 `repository` 字段时才会去拉;若遇到 API 限流,可在环境里设 `GITHUB_TOKEN`。
55
+
56
+ ## 开发
57
+
58
+ ```sh
59
+ pnpm install
60
+ pnpm --filter @eya46/dsh-plugin-info test
61
+ pnpm --filter @eya46/dsh-plugin-info build
62
+ ```
@@ -0,0 +1,4 @@
1
+ # Mount the plugin-info host + settings page. Config is optional.
2
+ - insert:
3
+ - id: dsh-plugin-info
4
+ name: '@eya46/dsh-plugin-info'
package/lib/client.js ADDED
@@ -0,0 +1,345 @@
1
+ "use strict";
2
+ /**
3
+ * @eya46/dsh-plugin-info — browser half.
4
+ *
5
+ * A settings page listing plugins the current profile added, with update
6
+ * marks and a per-plugin version drawer (default 10) that can open notes.
7
+ *
8
+ * @module @eya46/dsh-plugin-info/client
9
+ */
10
+ ;
11
+ (() => {
12
+ const loader = window.__ModuleLoader__;
13
+ if (loader === undefined)
14
+ return;
15
+ loader.load({
16
+ id: '@eya46/dsh-plugin-info',
17
+ factory: (require) => {
18
+ const API = '/plugin-info/api';
19
+ const CSS_TEXT = [
20
+ '.dpi-section { display: flex; flex-direction: column; gap: 12px; max-width: 720px; color: var(--dsw-alias-label-primary); }',
21
+ '.dpi-title { margin: 0; font-size: 16px; line-height: 24px; font-weight: 500; }',
22
+ '.dpi-intro { margin: 0; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-tertiary); }',
23
+ '.dpi-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 10px; }',
24
+ '.dpi-secondaryButton, .dpi-rowButton { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 4px; height: 36px; padding: 0 14px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 18px; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 14px; line-height: 22px; cursor: pointer; }',
25
+ '.dpi-rowButton { height: 28px; padding: 0 10px; font-size: 12px; line-height: 18px; }',
26
+ '.dpi-secondaryButton:hover:not(:disabled), .dpi-rowButton:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }',
27
+ '.dpi-secondaryButton:disabled, .dpi-rowButton:disabled { opacity: 0.4; cursor: default; }',
28
+ '.dpi-secondaryButton:focus-visible, .dpi-rowButton:focus-visible, .dpi-head:focus-visible, .dpi-version:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--dsw-alias-border-l3); }',
29
+ '.dpi-status { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); }',
30
+ '.dpi-banner { margin: 0; font-size: 12px; line-height: 18px; color: var(--dsw-alias-state-error-primary); }',
31
+ '.dpi-empty { color: var(--dsw-alias-label-tertiary); font-size: 14px; line-height: 22px; }',
32
+ '.dpi-cards { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 10px; }',
33
+ '.dpi-card { border: 1px solid var(--dsw-alias-border-l2); border-radius: 12px; overflow: hidden; }',
34
+ '.dpi-head { box-sizing: border-box; width: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 12px 14px; border: none; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }',
35
+ '.dpi-head:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }',
36
+ '.dpi-name { font-size: 14px; line-height: 22px; font-weight: 500; }',
37
+ '.dpi-tag { border: 1px solid var(--dsw-alias-border-l3); color: var(--dsw-alias-label-secondary); border-radius: 4px; padding: 1px 6px; font-size: 11px; line-height: 16px; }',
38
+ '.dpi-tag[data-tone="ok"] { color: var(--dsw-alias-state-success-primary); border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent); }',
39
+ '.dpi-tag[data-tone="warn"] { color: var(--dsw-alias-state-warn-label); border-color: color-mix(in srgb, var(--dsw-alias-state-warn-label) 40%, transparent); }',
40
+ '.dpi-body { display: flex; flex-direction: column; gap: 10px; padding: 0 14px 14px; }',
41
+ '.dpi-meta { margin: 0; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }',
42
+ '.dpi-id { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }',
43
+ '.dpi-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }',
44
+ '.dpi-versions { display: flex; flex-direction: column; gap: 6px; }',
45
+ '.dpi-version { box-sizing: border-box; width: 100%; display: flex; align-items: center; flex-wrap: wrap; gap: 8px; padding: 8px 10px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }',
46
+ '.dpi-version:hover { background: var(--dsw-alias-interactive-bg-hover-solid); }',
47
+ '.dpi-version[data-open="1"] { background: var(--dsw-alias-interactive-bg-hover-solid); }',
48
+ '.dpi-notes { display: flex; flex-direction: column; gap: 6px; padding: 8px 2px 2px; }',
49
+ '.dpi-notesBody { margin: 0; white-space: pre-wrap; font-size: 13px; line-height: 20px; color: var(--dsw-alias-label-secondary); }',
50
+ '.dpi-commits { margin: 0; padding-left: 18px; font-size: 13px; line-height: 20px; }',
51
+ '.dpi-link { color: inherit; text-underline-offset: 3px; }',
52
+ '.dpi-copyTag { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l3); color: var(--dsw-alias-label-secondary); background: transparent; border-radius: 4px; padding: 1px 6px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 16px; cursor: pointer; }',
53
+ '.dpi-copyTag:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-border-l2); }',
54
+ ].join('\n');
55
+ const React = require('react');
56
+ const h = React.createElement.bind(React);
57
+ const ensureStyles = () => {
58
+ if (document.querySelector('style[data-plugin="@eya46/dsh-plugin-info"]') !== null)
59
+ return;
60
+ const el = document.createElement('style');
61
+ el.setAttribute('data-plugin', '@eya46/dsh-plugin-info');
62
+ el.textContent = CSS_TEXT;
63
+ document.head.appendChild(el);
64
+ };
65
+ // The settings shell hardcodes nav glyphs by section id and falls back to a
66
+ // gear for unknown ids, so our section cannot get its own icon through the
67
+ // registry. We patch the nav button's SVG in place with the official Cordis
68
+ // plugin glyph (same geometry as primitives' IconCordisPluginOutline14).
69
+ // React re-renders the nav (tab switches, ledger changes) and rewrites the
70
+ // SVG back to the gear, so a one-shot patch is not enough — instead of
71
+ // polling, apply() observes the DOM and re-patches on change only.
72
+ const CORDIS_PLUGIN_GLYPH = '<path fill="currentColor" d="M3.03426 5.66661L1.70084 7.00003L3.0315 8.33069L2.14762 9.21457L-0.0669245 7.00003L2.15038 4.78273L3.03426 5.66661ZM7 14.067L4.77924 11.8462L5.66313 10.9623L7 12.2992L8.33342 10.9658L9.2173 11.8496L7 14.067ZM11.8489 9.21803L10.965 8.33414L12.2992 7.00003L10.9623 5.66316L11.8462 4.77927L14.0669 7.00003L11.8489 9.21803ZM8.33066 3.03153L7 1.70087L5.66589 3.03498L4.782 2.1511L7 -0.0668945L9.21454 2.14765L8.33066 3.03153Z"/>'
73
+ + '<rect x="5.98535" y="5.98535" width="2.02942" height="2.02942" fill="currentColor"/>';
74
+ const patchSettingsIcon = () => {
75
+ if (typeof document === 'undefined')
76
+ return;
77
+ for (const button of Array.from(document.querySelectorAll('button'))) {
78
+ const labels = Array.from(button.querySelectorAll('span'));
79
+ if (!labels.some((label) => label.textContent === '插件信息'))
80
+ continue;
81
+ const svg = button.querySelector('svg');
82
+ if (svg === null || svg.getAttribute('data-dpi-icon') === '1')
83
+ continue;
84
+ svg.setAttribute('data-dpi-icon', '1');
85
+ svg.setAttribute('viewBox', '0 0 14 14');
86
+ svg.setAttribute('width', '16');
87
+ svg.setAttribute('height', '16');
88
+ svg.innerHTML = CORDIS_PLUGIN_GLYPH;
89
+ }
90
+ };
91
+ /** Event-driven re-patch: coalesces mutation bursts into one microtask scan. */
92
+ const watchSettingsNav = () => {
93
+ let pending = false;
94
+ const schedule = () => {
95
+ if (pending)
96
+ return;
97
+ pending = true;
98
+ queueMicrotask(() => {
99
+ pending = false;
100
+ patchSettingsIcon();
101
+ });
102
+ };
103
+ const observer = new MutationObserver(schedule);
104
+ observer.observe(document.body, { childList: true, subtree: true });
105
+ patchSettingsIcon();
106
+ return () => observer.disconnect();
107
+ };
108
+ const sourceLabel = (source) => {
109
+ if (source === 'registry')
110
+ return 'npm';
111
+ if (source === 'file')
112
+ return '本地';
113
+ if (source === 'link')
114
+ return 'link';
115
+ if (source === 'git')
116
+ return 'git';
117
+ return source;
118
+ };
119
+ const formatTime = (value) => {
120
+ if (value === undefined)
121
+ return '';
122
+ const date = new Date(value);
123
+ if (Number.isNaN(date.getTime()))
124
+ return value;
125
+ return date.toISOString().slice(0, 10);
126
+ };
127
+ const PluginInfoPanel = () => {
128
+ const [rows, setRows] = React.useState(null);
129
+ const [error, setError] = React.useState('');
130
+ const [busy, setBusy] = React.useState(false);
131
+ const [open, setOpen] = React.useState({});
132
+ const [versions, setVersions] = React.useState({});
133
+ const [versionError, setVersionError] = React.useState({});
134
+ const [selected, setSelected] = React.useState({});
135
+ const [notes, setNotes] = React.useState({});
136
+ const [notesBusy, setNotesBusy] = React.useState('');
137
+ const [copied, setCopied] = React.useState({});
138
+ const copyName = React.useCallback((name) => {
139
+ const write = () => {
140
+ if (typeof navigator !== 'undefined' && navigator.clipboard !== undefined) {
141
+ return navigator.clipboard.writeText(name);
142
+ }
143
+ return Promise.reject(new Error('clipboard unavailable'));
144
+ };
145
+ write()
146
+ .then(() => {
147
+ setCopied((prev) => ({ ...prev, [name]: true }));
148
+ window.setTimeout(() => setCopied((prev) => ({ ...prev, [name]: false })), 1200);
149
+ })
150
+ .catch(() => { });
151
+ }, []);
152
+ const load = React.useCallback(() => {
153
+ setBusy(true);
154
+ fetch(API + '/plugins')
155
+ .then((response) => response.json())
156
+ .then((data) => {
157
+ if (!Array.isArray(data.plugins))
158
+ throw new Error(data.message ?? 'unexpected response');
159
+ setRows(data.plugins);
160
+ setError('');
161
+ })
162
+ .catch((caught) => setError(String(caught)))
163
+ .finally(() => setBusy(false));
164
+ }, []);
165
+ React.useEffect(() => {
166
+ ensureStyles();
167
+ load();
168
+ }, [load]);
169
+ const loadVersions = React.useCallback((name) => {
170
+ setVersionError((prev) => ({ ...prev, [name]: '' }));
171
+ fetch(API + '/versions?name=' + encodeURIComponent(name) + '&limit=10')
172
+ .then((response) => response.json())
173
+ .then((data) => {
174
+ if (!Array.isArray(data.versions))
175
+ throw new Error(data.message ?? 'unexpected response');
176
+ setVersions((prev) => ({ ...prev, [name]: data.versions ?? [] }));
177
+ })
178
+ .catch((caught) => setVersionError((prev) => ({ ...prev, [name]: String(caught) })));
179
+ }, []);
180
+ const togglePlugin = React.useCallback((name) => {
181
+ setOpen((prev) => {
182
+ const nextOpen = prev[name] !== true;
183
+ if (nextOpen && versions[name] === undefined)
184
+ loadVersions(name);
185
+ return { ...prev, [name]: nextOpen };
186
+ });
187
+ }, [loadVersions, versions]);
188
+ const openNotes = React.useCallback((name, version) => {
189
+ const key = name + '@' + version;
190
+ setSelected((prev) => ({ ...prev, [name]: prev[name] === version ? '' : version }));
191
+ if (notes[key] !== undefined)
192
+ return;
193
+ setNotesBusy(key);
194
+ fetch(API + '/notes?name=' + encodeURIComponent(name) + '&version=' + encodeURIComponent(version))
195
+ .then((response) => response.json())
196
+ .then((data) => {
197
+ if (typeof data.version !== 'string')
198
+ throw new Error(data.message ?? 'unexpected response');
199
+ setNotes((prev) => ({ ...prev, [key]: data }));
200
+ })
201
+ .catch((caught) => {
202
+ setNotes((prev) => ({
203
+ ...prev,
204
+ [key]: { name, version, source: 'none', body: String(caught) },
205
+ }));
206
+ })
207
+ .finally(() => setNotesBusy(''));
208
+ }, [notes]);
209
+ if (rows === null) {
210
+ return h('div', { className: 'dpi-section' }, h('div', { className: 'dpi-empty' }, '正在加载插件列表…'));
211
+ }
212
+ const updateCount = rows.filter((row) => row.hasUpdate).length;
213
+ const children = [
214
+ h('h2', { key: 'title', className: 'dpi-title' }, '插件信息'),
215
+ h('p', { key: 'intro', className: 'dpi-intro' }, '当前 Profile 里 @eya46 命名空间下的插件。展开后默认看最近 10 个版本,点版本可看更新说明。'),
216
+ h('div', { key: 'toolbar', className: 'dpi-toolbar' }, h('button', { className: 'dpi-secondaryButton', disabled: busy, onClick: () => { load(); } }, busy ? '刷新中…' : '刷新'), h('span', { className: 'dpi-status' }, '共 ' + rows.length + ' 个插件' + (updateCount > 0 ? ' · ' + updateCount + ' 个可更新' : ''))),
217
+ ];
218
+ if (error !== '')
219
+ children.push(h('p', { key: 'err', className: 'dpi-banner' }, '加载失败:' + error));
220
+ if (rows.length === 0)
221
+ children.push(h('div', { key: 'empty', className: 'dpi-empty' }, '这个 Profile 还没有用户添加的插件。'));
222
+ const cards = [];
223
+ for (const plugin of rows) {
224
+ const expanded = open[plugin.name] === true;
225
+ const head = [
226
+ h('span', { key: 'name', className: 'dpi-name' }, plugin.name),
227
+ h('span', { key: 'src', className: 'dpi-tag' }, sourceLabel(plugin.source)),
228
+ ];
229
+ if (plugin.installedVersion !== undefined) {
230
+ head.push(h('span', { key: 'ver', className: 'dpi-tag' }, plugin.installedVersion));
231
+ }
232
+ if (plugin.hasUpdate && plugin.latestVersion !== undefined) {
233
+ head.push(h('span', { key: 'up', className: 'dpi-tag', 'data-tone': 'warn' }, '可更新 ' + plugin.latestVersion));
234
+ }
235
+ else if (plugin.source === 'registry' && plugin.latestVersion !== undefined) {
236
+ head.push(h('span', { key: 'ok', className: 'dpi-tag', 'data-tone': 'ok' }, '已是最新'));
237
+ }
238
+ const body = [];
239
+ if (expanded) {
240
+ if (plugin.description !== undefined)
241
+ body.push(h('p', { key: 'desc', className: 'dpi-meta' }, plugin.description));
242
+ body.push(h('p', { key: 'spec', className: 'dpi-meta' }, '依赖:', h('span', { className: 'dpi-id' }, plugin.spec), plugin.latestVersion === undefined ? null : ' · 最新 ' + plugin.latestVersion));
243
+ if (plugin.error !== undefined)
244
+ body.push(h('p', { key: 'perr', className: 'dpi-banner' }, plugin.error));
245
+ const links = [
246
+ h('button', {
247
+ key: 'copy',
248
+ className: 'dpi-copyTag',
249
+ title: '复制包名',
250
+ onClick: () => { copyName(plugin.name); },
251
+ }, copied[plugin.name] === true ? '已复制' : plugin.name),
252
+ h('button', {
253
+ key: 'vers',
254
+ className: 'dpi-rowButton',
255
+ onClick: () => { loadVersions(plugin.name); },
256
+ }, versions[plugin.name] === undefined ? '加载版本' : '重新加载版本'),
257
+ ];
258
+ if (plugin.homepage !== undefined) {
259
+ links.push(h('a', { key: 'home', className: 'dpi-rowButton dpi-link', href: plugin.homepage, target: '_blank', rel: 'noreferrer' }, '主页'));
260
+ }
261
+ if (plugin.repository !== undefined) {
262
+ links.push(h('a', { key: 'repo', className: 'dpi-rowButton dpi-link', href: plugin.repository, target: '_blank', rel: 'noreferrer' }, '仓库'));
263
+ }
264
+ body.push(h('div', { key: 'actions', className: 'dpi-actions' }, ...links));
265
+ const pluginVersions = versions[plugin.name];
266
+ const loadError = versionError[plugin.name];
267
+ if (loadError !== undefined && loadError !== '') {
268
+ body.push(h('p', { key: 'verr', className: 'dpi-banner' }, loadError));
269
+ }
270
+ else if (pluginVersions === undefined) {
271
+ body.push(h('p', { key: 'vload', className: 'dpi-meta' }, '正在加载最近 10 个版本…'));
272
+ }
273
+ else if (pluginVersions.length === 0) {
274
+ body.push(h('p', { key: 'vempty', className: 'dpi-meta' }, plugin.source === 'registry' ? '注册表没有版本记录。' : '本地 / git 安装没有更多历史版本。'));
275
+ }
276
+ else {
277
+ const versionNodes = pluginVersions.map((row) => {
278
+ const key = plugin.name + '@' + row.version;
279
+ const isOpen = selected[plugin.name] === row.version;
280
+ const note = notes[key];
281
+ const tags = [h('span', { key: 'v', className: 'dpi-id' }, row.version)];
282
+ if (row.installed)
283
+ tags.push(h('span', { key: 'i', className: 'dpi-tag' }, '当前'));
284
+ if (row.latest)
285
+ tags.push(h('span', { key: 'l', className: 'dpi-tag', 'data-tone': 'ok' }, 'latest'));
286
+ if (row.publishedAt !== undefined)
287
+ tags.push(h('span', { key: 't', className: 'dpi-meta' }, formatTime(row.publishedAt)));
288
+ const noteChildren = [];
289
+ if (isOpen) {
290
+ if (notesBusy === key && note === undefined) {
291
+ noteChildren.push(h('p', { key: 'nload', className: 'dpi-meta' }, '正在加载更新说明…'));
292
+ }
293
+ else if (note === undefined) {
294
+ noteChildren.push(h('p', { key: 'nmiss', className: 'dpi-meta' }, '没有更新说明。'));
295
+ }
296
+ else if (note.source === 'github-release') {
297
+ if (note.title !== undefined)
298
+ noteChildren.push(h('p', { key: 'nt', className: 'dpi-meta' }, note.title));
299
+ if (note.body !== undefined)
300
+ noteChildren.push(h('pre', { key: 'nb', className: 'dpi-notesBody' }, note.body));
301
+ if (note.url !== undefined) {
302
+ noteChildren.push(h('a', { key: 'nu', className: 'dpi-link', href: note.url, target: '_blank', rel: 'noreferrer' }, '在 GitHub 查看 Release'));
303
+ }
304
+ }
305
+ else if (note.source === 'github-compare' && note.commits !== undefined) {
306
+ noteChildren.push(h('ul', { key: 'nc', className: 'dpi-commits' }, ...note.commits.map((commit) => h('li', { key: commit.sha }, h('span', { className: 'dpi-id' }, commit.sha), ' ', commit.message))));
307
+ if (note.url !== undefined) {
308
+ noteChildren.push(h('a', { key: 'ncu', className: 'dpi-link', href: note.url, target: '_blank', rel: 'noreferrer' }, '在 GitHub 比较'));
309
+ }
310
+ }
311
+ else {
312
+ noteChildren.push(h('p', { key: 'nnone', className: 'dpi-meta' }, note.body ?? '这个版本没有找到 GitHub Release 或提交说明。'));
313
+ }
314
+ }
315
+ return h('div', { key: row.version }, h('button', {
316
+ className: 'dpi-version',
317
+ 'data-open': isOpen ? '1' : '0',
318
+ onClick: () => { openNotes(plugin.name, row.version); },
319
+ }, ...tags), isOpen ? h('div', { className: 'dpi-notes' }, ...noteChildren) : null);
320
+ });
321
+ body.push(h('div', { key: 'versions', className: 'dpi-versions' }, ...versionNodes));
322
+ }
323
+ }
324
+ cards.push(h('li', { key: plugin.name, className: 'dpi-card' }, h('button', {
325
+ className: 'dpi-head',
326
+ 'aria-expanded': expanded,
327
+ onClick: () => { togglePlugin(plugin.name); },
328
+ }, ...head), expanded ? h('div', { className: 'dpi-body' }, ...body) : null));
329
+ }
330
+ children.push(h('ul', { key: 'cards', className: 'dpi-cards' }, ...cards));
331
+ return h('div', { className: 'dpi-section' }, ...children);
332
+ };
333
+ return {
334
+ name: 'dsh-plugin-info',
335
+ inject: ['slots'],
336
+ apply(ctx) {
337
+ ctx.effect(watchSettingsNav, '@eya46/dsh-plugin-info: settings nav icon patch');
338
+ ctx.slots.inject('settings.section', () => {
339
+ ctx.slots.register({ name: 'settings.section', id: 'plugin-info', order: 16, label: '插件信息' }, PluginInfoPanel);
340
+ });
341
+ },
342
+ };
343
+ },
344
+ });
345
+ })();