dsh-plugin 1.0.0 → 1.1.1

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.
Files changed (39) hide show
  1. package/README.en.md +12 -7
  2. package/README.md +12 -7
  3. package/client/client.js +725 -223
  4. package/client/client.js.map +1 -1
  5. package/lib/http/routes.js +43 -7
  6. package/lib/services/installed-versions.js +16 -1
  7. package/lib/services/loader.js +12 -5
  8. package/lib/services/npm-resolve.js +67 -0
  9. package/lib/services/task-queue.js +55 -10
  10. package/lib/types/services/install-types.d.ts +8 -0
  11. package/lib/types/services/installed-versions.d.ts +10 -0
  12. package/lib/types/services/loader.d.ts +6 -4
  13. package/lib/types/services/npm-resolve.d.ts +6 -0
  14. package/lib/types/services/task-queue.d.ts +11 -5
  15. package/package.json +1 -1
  16. package/src/client/components/AboutModal.tsx +72 -0
  17. package/src/client/components/CatalogHeader.tsx +30 -5
  18. package/src/client/components/HubUpdateModal.tsx +84 -0
  19. package/src/client/components/NotificationsModal.tsx +84 -58
  20. package/src/client/components/PluginCard.tsx +3 -0
  21. package/src/client/components/PluginHubSection.tsx +73 -14
  22. package/src/client/components/modals.tsx +42 -18
  23. package/src/client/hooks/useCatalog.ts +96 -18
  24. package/src/client/hooks/useTaskQueue.ts +164 -54
  25. package/src/client/lib/catalog.ts +81 -13
  26. package/src/client/lib/failures.ts +14 -2
  27. package/src/client/lib/renderMarkdown.ts +116 -0
  28. package/src/client/locales.ts +47 -4
  29. package/src/client/styles/Header.module.css +83 -3
  30. package/src/client/styles/List.module.css +4 -4
  31. package/src/client/styles/Modal.module.css +320 -5
  32. package/src/client/styles/tokens.module.css +8 -3
  33. package/src/client/types.ts +3 -1
  34. package/src/server/http/routes.ts +42 -7
  35. package/src/server/services/install-types.ts +8 -0
  36. package/src/server/services/installed-versions.ts +20 -1
  37. package/src/server/services/loader.ts +12 -5
  38. package/src/server/services/npm-resolve.ts +67 -0
  39. package/src/server/services/task-queue.ts +57 -10
@@ -11,6 +11,36 @@ export const SITE_URL = 'https://dsh-plugin.org/'
11
11
  /** 插件中心源码仓库:头部右上角 GitHub 图标的跳转地址 */
12
12
  export const GITHUB_URL = 'https://github.com/dshplugin/dsh-plugin-hub'
13
13
 
14
+ /** Hub 自身在目录数据里的仓库标识:头部版本号旁的「可更新」徽标按它定位自己。 */
15
+ export const HUB_REPO = 'dshplugin/dsh-plugin-hub'
16
+
17
+ /**
18
+ * Hub 自我更新版本控制中心(Cloudflare Worker:dsh-update)。
19
+ * 发新版后向 Worker 管理接口写入最新版本 + Markdown 变更记录(见
20
+ * dsh-update-worker 的 POST /admin/hub),所有已装用户的「可更新」徽标即时出现,
21
+ * 不再依赖主站目录数据管道(每日刷新 + 重建站点)。
22
+ * 响应:{ version: string | null, publishedAt: string | null, notes: string | {zh,en} | null }
23
+ */
24
+ export const HUB_UPDATE_URL = 'https://hub.dsh-plugin.org/hub'
25
+
26
+ /** Worker 版本控制中心返回的 Hub 自我更新信息(notes 为 Markdown 变更记录)。 */
27
+ export interface HubUpdateInfo {
28
+ version: string
29
+ publishedAt?: string | null
30
+ /** Markdown 变更记录;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
31
+ notes?: string | Record<string, string> | null
32
+ }
33
+
34
+ /** Worker「关注我们」内容接口(同款 Worker,key = hub:about,Markdown 推送非写死)。 */
35
+ export const HUB_ABOUT_URL = 'https://hub.dsh-plugin.org/about'
36
+
37
+ /** Worker 返回的「关注我们」内容(content 为 Markdown,可字符串或 {zh,en} 双语言对象)。 */
38
+ export interface HubAboutInfo {
39
+ /** Markdown 平台介绍 + 反馈群二维码;可传字符串,或 { zh, en } 双语言对象按界面语言取 */
40
+ content?: string | Record<string, string> | null
41
+ updatedAt?: string | null
42
+ }
43
+
14
44
  /** 构建时由 tsdown 从 package.json 注入的插件版本号(见 tsdown.config.ts define)。 */
15
45
  declare const __PLUGIN_VERSION__: string
16
46
 
@@ -46,19 +76,21 @@ export function pluginSiteUrl(repo: string): string {
46
76
 
47
77
  /**
48
78
  * 一键反馈 GitHub Issue 的预填链接:标题带「来自 dsh-plugin.org」标识,正文只带
49
- * 核心信息 —— 原因判定 + 关键错误代码 + 宿主机器环境快照 + 错误核心摘要(完整日志
50
- * 太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,故只收集重点)。
51
- * 错误弹窗、失败记录共用此逻辑。
79
+ * 核心信息 —— 原因判定 + 关键错误代码 + 尝试过的安装方式 + 宿主机器环境快照 +
80
+ * 错误核心摘要(完整日志太长,塞进 URL 会被 GitHub 以「request URL too long」拒绝,
81
+ * 故只收集重点)。错误弹窗、失败记录共用此逻辑。
52
82
  * env 取不到时为 null,链接照常生成、只是少环境段。
83
+ * attempts(尝试过的安装方式,npm 反查 + 实际执行命令):作者看到我们查过/试过的命令,
84
+ * 组织 scope 与 GitHub 用户名不一致时也能直接指认正确的 npm 包名。
53
85
  */
54
- export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null): string {
86
+ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | null, command?: string, attempts?: string[]): string {
55
87
  const title = `[dsh-plugin.org | dsh-plugin-hub] Install/Remove failed: ${repo}`
56
88
  // 原因判定:让作者一眼知道这是不是自己插件的问题。
57
- // [packaging](预检/装后校验拦截)单独列:点明「不支持官方默认安装方式」,并写清官方默认方式
58
- // 是什么(git 直装要求仓库提交构建产物或提供 prepare 脚本),作者照此适配即可。
89
+ // [packaging](预检/装后校验拦截)单独列:无论走 npm 还是 git 通道,都是分发物不完整
90
+ // (package.json 声明的入口文件在发布物里缺失),作者照此补齐即可。
59
91
  const kind = classifyFailure(message)
60
92
  const reason = /\[packaging\]/i.test(message)
61
- ? 'plugin does NOT support the official default install method (dsh plugin add github:owner/repo — requires build output committed to the repo or a prepare script); its git distribution lacks the entry file declared in package.json'
93
+ ? 'plugin distribution is incomplete — the entry file declared in package.json is missing from the published package (github tarball or npm package); please commit build output or publish a complete package'
62
94
  : kind === 'pluginPrepare'
63
95
  ? 'plugin prepare/build script failed during install (packaging/distribution issue)'
64
96
  : kind === 'pnpmIgnoredBuild'
@@ -72,17 +104,23 @@ export function pluginIssueUrl(repo: string, message: string, env?: EnvInfo | nu
72
104
  `- Cause: ${reason}`,
73
105
  ...(code ? [`- Key error: \`${code}\``] : []),
74
106
  '',
75
- // 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的官方安装命令(含 --profile)与执行结果,链接由常量动态拼接
107
+ // 来源说明:标题(链官网)+ 一句来源(链仓库)+ 实际执行的安装命令(含 --profile)与执行结果,链接由常量动态拼接
76
108
  `## [DSH-Plugin 插件中心](${SITE_URL}) · 安装 Plugin 失败错误信息`,
77
109
  `本错误信息由 [dsh-plugin-hub](${GITHUB_URL}) 插件中心的安装程序自动生成,随本次安装失败一并提交。`,
78
- `- 使用的安装命令(官方默认安装方式):\`dsh plugin${env?.profile ? ` --profile ${env.profile}` : ''} add github:${repo}\``,
110
+ `- 实际执行的安装命令:\`${command ?? `dsh plugin${env?.profile ? ` --profile ${env.profile}` : ''} add github:${repo}`}\``,
79
111
  `- 执行结果:安装失败,未能安装该插件。`,
80
- // [packaging] 场景:向作者说明官方默认安装方式具体是什么,请其按该方式适配(提交构建产物或提供 prepare 脚本)
112
+ // 尝试过的安装方式(npm 反查 + 实际执行命令,按先后顺序):作者据此反推正确的
113
+ // npm 包名 —— 组织 scope 与 GitHub 用户名不一致时仅凭仓库名猜不到,作者看到我们
114
+ // 查过/试过的命令就能直接指认
115
+ ...(attempts && attempts.length > 0
116
+ ? ['', '## Attempted install channels(已尝试的安装方式)', ...attempts.map((a) => `- ${a}`)]
117
+ : []),
118
+ // [packaging] 场景:说明两种官方安装通道(npm 包 / git 直装),当前插件在对应通道下分发不完整
81
119
  ...(/\[packaging\]/i.test(message)
82
120
  ? [
83
121
  '',
84
- '## 官方默认安装方式',
85
- 'DSH 生态的官方默认安装方式是 `dsh plugin add github:owner/repo`(git 直装):插件仓库需提交构建产物,或在 package.json 提供 `prepare` 脚本让 pnpm 安装时自动构建。当前插件两者皆不具备,因此无法按官方方式安装。',
122
+ '## 安装方式说明',
123
+ 'DSH 插件支持两种官方安装通道:`dsh plugin add <npm-package>`(npm 分发,需发布完整构建产物)与 `dsh plugin add github:owner/repo`(git 直装,仓库需提交构建产物或在 package.json 提供 `prepare` 脚本)。当前插件的分发物缺少 package.json 声明的入口文件,请按所用通道补齐后重新发布。',
86
124
  ]
87
125
  : []),
88
126
  '',
@@ -156,7 +194,17 @@ export function normalize(raw: Record<string, unknown>): HubPlugin {
156
194
  topics: Array.isArray(raw.t) ? (raw.t as string[]) : undefined,
157
195
  features: Array.isArray(raw.f) ? (raw.f as string[]) : undefined,
158
196
  description: typeof raw.d === 'string' ? raw.d : undefined,
159
- source: typeof raw.r === 'string' ? { repo: raw.r } : undefined,
197
+ // 投影输出两种形态:老数据 source 是 repo 字符串;新数据是 { repo, npmPackage }
198
+ source: typeof raw.r === 'string'
199
+ ? { repo: raw.r }
200
+ : raw.r !== null && typeof raw.r === 'object'
201
+ ? {
202
+ repo: typeof (raw.r as { repo?: unknown }).repo === 'string' ? (raw.r as { repo: string }).repo : undefined,
203
+ npmPackage: typeof (raw.r as { npmPackage?: unknown }).npmPackage === 'string' ? (raw.r as { npmPackage: string }).npmPackage : undefined,
204
+ }
205
+ : undefined,
206
+ // 能否网页一键安装:API 仅在 false 时下发 wi(缺省视为 true),据此禁用一键安装、只提示命令行
207
+ install: raw.wi === false ? { webInstallable: false } : undefined,
160
208
  compatibility: typeof raw.v === 'string' ? { status: raw.v } : undefined,
161
209
  dates: {
162
210
  repoUpdatedAt: typeof raw.u === 'string' ? raw.u : undefined,
@@ -171,6 +219,26 @@ export function normalize(raw: Record<string, unknown>): HubPlugin {
171
219
  return raw as unknown as HubPlugin
172
220
  }
173
221
 
222
+ /**
223
+ * 安装通道决策(用户无感知):目录探测到 npm 包名 → 用 npm 包名安装
224
+ * (走 npm registry tarball,更快、与 GitHub 网络无关);无 npm 包名 → git 直装。
225
+ * 返回值 target 即传给后端 /install 的安装目标(npm 包名 或 owner/repo)。
226
+ */
227
+ export function installTargetOf(p: HubPlugin): { target: string; via: 'npm' | 'github' } {
228
+ const pkg = (p.source?.npmPackage ?? '').trim()
229
+ const repo = (p.source?.repo ?? '').trim()
230
+ if (pkg && repo) return { target: pkg, via: 'npm' }
231
+ return { target: repo, via: 'github' }
232
+ }
233
+
234
+ /** 展示用安装命令(复制/弹窗):npm 通道显示包名,git 通道显示 github: 源。 */
235
+ export function installCommandOf(p: HubPlugin, withProfile = false): string {
236
+ const { target, via } = installTargetOf(p)
237
+ return via === 'npm'
238
+ ? `dsh plugin${withProfile ? ' --profile web' : ''} add ${target}`
239
+ : `dsh plugin${withProfile ? ' --profile web' : ''} add github:${target}`
240
+ }
241
+
174
242
  export const CATEGORY_LABELS: Record<string, { zh: string; en: string }> = {
175
243
  interface: { zh: '界面与体验', en: 'UI & Experience' },
176
244
  session: { zh: '会话与消息', en: 'Sessions & Messages' },
@@ -16,6 +16,10 @@ export interface NotificationRecord {
16
16
  repo: string
17
17
  /** 失败时的完整错误日志;成功记录为空串 */
18
18
  message: string
19
+ /** 实际执行的安装/卸载命令(issue 预填时如实展示);历史记录可能缺失 */
20
+ command?: string
21
+ /** 尝试过的安装方式(npm registry 反查 + 实际执行命令,按先后顺序):失败提 Issue 时贴给作者,便于反推正确的 npm 包名 */
22
+ attempts?: string[]
19
23
  /** 结束时间(epoch ms) */
20
24
  at: number
21
25
  }
@@ -27,7 +31,8 @@ export type FailureKind = 'pnpmIgnoredBuild' | 'pluginPrepare' | 'repo'
27
31
 
28
32
  /**
29
33
  * 失败归类,三态。无论底层机制如何(pnpm 白名单拦截 / 构建脚本被忽略 / prepare 失败),
30
- * 对用户而言结果都一样 —— 用官方默认安装方式装不上,就是插件仓库的问题,一律引导提 Issue:
34
+ * 对用户而言结果都一样 —— 当前安装通道(npm 或 git)装不上,就是插件分发/依赖的问题,
35
+ * 一律引导提 Issue:
31
36
  * - pnpmIgnoredBuild:插件依赖里的原生模块构建脚本被 pnpm 默认拦截(如 node-pty,
32
37
  * `ERR_PNPM_IGNORED_BUILDS`)。只影响带原生模块的插件,其他插件不受影响 —— 差异在
33
38
  * 插件的依赖选择,属插件依赖/打包问题 → 引导去仓库提 Issue(建议改用预编译版本)
@@ -44,7 +49,7 @@ export function classifyFailure(message: string): FailureKind {
44
49
  if (/ERR_PNPM_IGNORED_BUILDS|Ignored build scripts:/i.test(message)) return 'pnpmIgnoredBuild'
45
50
  // 再判 prepare 实际执行失败:只有构建脚本真的跑挂了才是插件问题
46
51
  if (/ERR_PNPM_PREPARE_PACKAGE|ELIFECYCLE|Command failed|prepare-guard/i.test(message)) return 'pluginPrepare'
47
- // 其余失败(含 git prepare 被 pnpm 白名单拦截):官方方式装不上 = 插件仓库的问题,一律提 Issue
52
+ // 其余失败(含 git prepare 被 pnpm 白名单拦截):当前通道装不上 = 插件分发/依赖的问题,一律提 Issue
48
53
  return 'repo'
49
54
  }
50
55
 
@@ -151,3 +156,10 @@ export function clearNotifications(): NotificationRecord[] {
151
156
  save([])
152
157
  return []
153
158
  }
159
+
160
+ /** 按 id 删除单条通知记录,返回更新后的列表(仅移除该条,不影响其余记录)。 */
161
+ export function removeNotification(id: number): NotificationRecord[] {
162
+ const next = loadNotifications().filter((r) => r.id !== id)
163
+ save(next)
164
+ return next
165
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * 轻量 Markdown 渲染器:仅用于 Hub 自我更新的变更记录(Worker 下发的 notes)。
3
+ *
4
+ * 内容由插件作者自己编写、属可信内容,但渲染端仍先整体 HTML 转义,再按
5
+ * Markdown 语法输出结构,杜绝任何未转义的原始 HTML 注入;链接仅放行 http(s)。
6
+ *
7
+ * 支持:标题(#~####)、有序/无序列表、代码块(```)、行内代码、加粗、斜体、
8
+ * 链接、图片(![alt](url),如反馈群二维码)、引用(>)、段落;其余语法一律按
9
+ * 纯文本处理,不引入外部依赖。图片仅放行 http(s),限定最大宽度不撑破弹窗。
10
+ */
11
+
12
+ /** HTML 转义:所有文本先进这里,再进入结构变换,保证输出里没有未转义的用户输入。 */
13
+ function escapeHtml(s: string): string {
14
+ return s
15
+ .replace(/&/g, '&amp;')
16
+ .replace(/</g, '&lt;')
17
+ .replace(/>/g, '&gt;')
18
+ .replace(/"/g, '&quot;')
19
+ .replace(/'/g, '&#39;')
20
+ }
21
+
22
+ /** 行内变换:链接 / 加粗 / 斜体 / 行内代码(代码先占位保护,最后还原)。 */
23
+ function inline(s: string): string {
24
+ let out = escapeHtml(s)
25
+ // 行内代码先占位:避免其中的 * [ ] 被后续链接/加粗/斜体规则破坏
26
+ const codeSpans: string[] = []
27
+ out = out.replace(/`([^`]+)`/g, (_m, code) => {
28
+ codeSpans.push(String(code))
29
+ return `\u0000${codeSpans.length - 1}\u0000`
30
+ })
31
+ // 图片 ![alt](url):仅放行 http(s),用于展示反馈群二维码等;limited by CSS
32
+ out = out.replace(/!\[([^\]]*)\]\(([^)\s]+)\)/g, (m, alt, url) =>
33
+ /^https?:\/\//i.test(String(url)) && !/[\u0000]/.test(String(url))
34
+ ? `<img src="${String(url)}" alt="${String(alt)}" loading="lazy">`
35
+ : m,
36
+ )
37
+ // 链接 [text](url):仅放行 http(s),新窗口打开
38
+ out = out.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (m, text, url) =>
39
+ /^https?:\/\//i.test(String(url)) && !/[\u0000]/.test(String(url))
40
+ ? `<a href="${String(url)}" target="_blank" rel="noopener noreferrer">${String(text)}</a>`
41
+ : m,
42
+ )
43
+ out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
44
+ out = out.replace(/(^|[^*])\*([^*]+)\*/g, '$1<em>$2</em>')
45
+ return out.replace(/\u0000(\d+)\u0000/g, (_m, idx) => `<code>${codeSpans[Number(idx)]}</code>`)
46
+ }
47
+
48
+ /** 把变更记录 Markdown 渲染为安全的 HTML 字符串(由弹窗用 dangerouslySetInnerHTML 注入)。 */
49
+ export function renderMarkdown(md: string): string {
50
+ const lines = md.replace(/\r\n?/g, '\n').split('\n')
51
+ const blocks: string[] = []
52
+ let i = 0
53
+ while (i < lines.length) {
54
+ const line = lines[i]
55
+ // 围栏代码块
56
+ if (/^\s*```/.test(line)) {
57
+ const buf: string[] = []
58
+ i++
59
+ while (i < lines.length && !/^\s*```/.test(lines[i])) {
60
+ buf.push(lines[i])
61
+ i++
62
+ }
63
+ i++ // 跳过闭合围栏
64
+ blocks.push(`<pre><code>${escapeHtml(buf.join('\n'))}</code></pre>`)
65
+ continue
66
+ }
67
+ // 空行:段落分隔符
68
+ if (line.trim() === '') {
69
+ i++
70
+ continue
71
+ }
72
+ // 标题:# 到 #### 映射为 h2~h5(弹窗标题用 modalTitle,不与其冲突)
73
+ const heading = line.match(/^(#{1,4})\s+(.*)/)
74
+ if (heading) {
75
+ const level = heading[1].length + 1
76
+ blocks.push(`<h${level}>${inline(heading[2])}</h${level}>`)
77
+ i++
78
+ continue
79
+ }
80
+ // 有序 / 无序列表(连续收集为一个列表块)
81
+ if (/^\s*[-*+]\s+/.test(line) || /^\s*\d+[.)]\s+/.test(line)) {
82
+ const ordered = /^\s*\d+[.)]\s+/.test(line)
83
+ const items: string[] = []
84
+ while (i < lines.length && (/^\s*[-*+]\s+/.test(lines[i]) || /^\s*\d+[.)]\s+/.test(lines[i]))) {
85
+ items.push(`<li>${inline(lines[i].replace(/^\s*[-*+]|\s*\d+[.)]\s*/, '').trim())}</li>`)
86
+ i++
87
+ }
88
+ blocks.push(`<${ordered ? 'ol' : 'ul'}>${items.join('')}</${ordered ? 'ol' : 'ul'}>`)
89
+ continue
90
+ }
91
+ // 引用
92
+ if (/^\s*>\s?/.test(line)) {
93
+ const buf: string[] = []
94
+ while (i < lines.length && /^\s*>\s?/.test(lines[i])) {
95
+ buf.push(inline(lines[i].replace(/^\s*>\s?/, '')))
96
+ i++
97
+ }
98
+ blocks.push(`<blockquote>${buf.join('<br>')}</blockquote>`)
99
+ continue
100
+ }
101
+ // 普通段落:收集连续非空、非特殊开头的行
102
+ const buf: string[] = []
103
+ while (
104
+ i < lines.length
105
+ && lines[i].trim() !== ''
106
+ && !/^\s*```/.test(lines[i])
107
+ && !/^#{1,4}\s+/.test(lines[i])
108
+ && !/^\s*([-*+]|\d+[.)])\s+/.test(lines[i])
109
+ ) {
110
+ buf.push(inline(lines[i]))
111
+ i++
112
+ }
113
+ blocks.push(`<p>${buf.join('<br>')}</p>`)
114
+ }
115
+ return blocks.join('')
116
+ }
@@ -21,6 +21,9 @@ export const zh = {
21
21
  failedDesc: '请稍后重试,或点击右上角按钮在浏览器中打开插件中心。',
22
22
  retry: '重试',
23
23
  install: '安装',
24
+ // AI 识别可能需要命令行辅助的插件(webInstallable=false):仍可尝试一键安装,失败时复制命令去 dsh 终端
25
+ cliOnlyHint: '此插件可能需命令行辅助,失败时复制上方命令到 dsh 终端',
26
+ failedCopyHint: '安装失败。请复制上方安装命令到 dsh 终端手动执行(原生依赖构建脚本可能需要人工放行,如 pnpm approve-builds)',
24
27
  toastCopied: '安装命令已复制,去 dsh 终端粘贴即可安装',
25
28
  installed: '已安装',
26
29
  notInstalled: '未安装',
@@ -48,8 +51,8 @@ export const zh = {
48
51
  installNow: '直接安装',
49
52
  installing: '安装中…',
50
53
  installDone: '安装成功',
51
- installResultTitle: '安装完成',
52
- installResultDesc: '插件已安装。',
54
+ installResultTitle: '插件安装成功',
55
+ installResultDesc: '',
53
56
  // 更新语义:已安装插件点「更新」走同一条 add 命令原位覆盖重装,弹窗文案区分安装/更新
54
57
  confirmUpdateTitle: '确认更新',
55
58
  updateNow: '直接更新',
@@ -65,6 +68,7 @@ export const zh = {
65
68
  restartLater: '稍后重启',
66
69
  restarting: '正在重启…',
67
70
  restartHint: '部分插件(如插件市场、顶层 bundle)需重启后才会生效。',
71
+ done: '完成',
68
72
  // 待重启:装完没重启的插件常驻提醒,直到宿主真正重启
69
73
  restartPendingHint: '安装成功,重启后生效',
70
74
  restartPendingHintUninstall: '卸载成功,重启后移除',
@@ -91,6 +95,7 @@ export const zh = {
91
95
  notificationsDesc: '安装、卸载任务的成败都会记录在这里,即使错过提示也能随时回来查看。',
92
96
  notificationsEmpty: '暂无通知记录',
93
97
  notificationsClear: '清空通知',
98
+ removeNotification: '删除这条通知',
94
99
  failCopy: '复制完整错误信息',
95
100
  failIssueHint: '带着错误日志去作者仓库一键提交 Issue(正文含官网收录链接)',
96
101
  failIssueBig: '一键提交 BUG 到 GitHub Issue 为开源作贡献',
@@ -119,6 +124,22 @@ export const zh = {
119
124
  update: '可更新',
120
125
  updateAvailable: '有更新',
121
126
  updateAvailableHint: '检测到更新:有版本号的仓库比对版本号,无版本号的仓库比对仓库最近更新时间',
127
+ hubUpdateHint: '检测到 DSH-Plugin Hub 新版本,点击查看更新内容并更新',
128
+ hubUpdateTitle: 'DSH-Plugin Hub 有新版本',
129
+ hubUpdateDesc: 'v{version} 已发布,点击下方按钮即可更新,完成后需重启宿主生效。',
130
+ hubUpdatePublished: '发布于',
131
+ hubUpdateNotes: '更新记录',
132
+ hubUpdateLater: '稍后再说',
133
+ versionHint: '查看版本信息与更新记录',
134
+ hubCurrentTitle: '当前版本',
135
+ hubCurrentDesc: '你正在使用 v{version},已是最新版本。',
136
+ hubUpToDate: '已是最新版本',
137
+ // 头部「关注我们」弹窗:平台介绍 + 用户反馈群二维码(内容由 Worker /about Markdown 推送,非写死)
138
+ followUs: '关注我们',
139
+ aboutTitle: '关注我们',
140
+ aboutDesc: '了解 DSH-Plugin Hub 是做什么的,加入用户反馈群随时交流。',
141
+ aboutEmpty: '内容暂未发布,敬请期待。',
142
+ aboutUpdated: '更新于',
122
143
  empty: '该分类暂无插件',
123
144
  dataFrom: '数据源 dsh-plugin.org · 每日人工更新',
124
145
  }
@@ -145,6 +166,10 @@ export const en = {
145
166
  failedDesc: 'Please retry, or open the hub in your browser with the button above.',
146
167
  retry: 'Retry',
147
168
  install: 'Install',
169
+ // AI-flagged plugins that may need command-line help (webInstallable=false): one-click install still allowed,
170
+ // on failure the user is guided to copy the command into the dsh terminal
171
+ cliOnlyHint: 'This plugin may need command-line help; if it fails, copy the command above and run it in your dsh terminal',
172
+ failedCopyHint: 'Install failed. Copy the command above and run it manually in your dsh terminal (native build scripts may need manual approval, e.g. pnpm approve-builds)',
148
173
  toastCopied: 'Install command copied — paste it in your dsh terminal',
149
174
  installed: 'Installed',
150
175
  notInstalled: 'Not installed',
@@ -172,8 +197,8 @@ export const en = {
172
197
  installNow: 'Install',
173
198
  installing: 'Installing…',
174
199
  installDone: 'Installed',
175
- installResultTitle: 'Installed',
176
- installResultDesc: 'The plugin has been installed.',
200
+ installResultTitle: 'Plugin installed successfully',
201
+ installResultDesc: '',
177
202
  // Update semantics: the "update" action re-runs add in place — copy differentiates install vs update
178
203
  confirmUpdateTitle: 'Confirm update',
179
204
  updateNow: 'Update',
@@ -189,6 +214,7 @@ export const en = {
189
214
  restartLater: 'Later',
190
215
  restarting: 'Restarting…',
191
216
  restartHint: 'Some plugins (such as plugin markets / top-level bundles) only take effect after a restart.',
217
+ done: 'Done',
192
218
  restartPendingHint: 'Installed — takes effect after restart',
193
219
  restartPendingHintUninstall: 'Removed — cleaned up after restart',
194
220
  sectionPendingRestart: 'Pending restart',
@@ -213,6 +239,7 @@ export const en = {
213
239
  notificationsDesc: 'Every install and remove result is logged here — even if you missed the prompt, you can always check back.',
214
240
  notificationsEmpty: 'No notifications yet',
215
241
  notificationsClear: 'Clear notifications',
242
+ removeNotification: 'Remove this notification',
216
243
  failCopy: 'Copy full log',
217
244
  failIssueHint: 'Open a pre-filled issue on the author repo with this error log (includes catalog links)',
218
245
  failIssueBig: 'Report this bug to GitHub — contribute to open source',
@@ -241,6 +268,22 @@ export const en = {
241
268
  update: 'Update',
242
269
  updateAvailable: 'Update available',
243
270
  updateAvailableHint: 'An update was detected: repos with a release are compared by tag, repos without one by last push time',
271
+ hubUpdateHint: 'A new DSH-Plugin Hub version is available — click to view the changelog and update',
272
+ hubUpdateTitle: 'A new DSH-Plugin Hub version is available',
273
+ hubUpdateDesc: 'v{version} has been released — click below to update, then restart the host to apply it.',
274
+ hubUpdatePublished: 'Published',
275
+ hubUpdateNotes: "What's new",
276
+ hubUpdateLater: 'Later',
277
+ versionHint: 'View version info and changelog',
278
+ hubCurrentTitle: 'Current version',
279
+ hubCurrentDesc: 'You are on v{version} — this is the latest version.',
280
+ hubUpToDate: 'Up to date',
281
+ // Header "Follow us" modal: platform intro + user feedback group QR (content pushed via Worker /about Markdown)
282
+ followUs: 'Follow us',
283
+ aboutTitle: 'Follow us',
284
+ aboutDesc: 'Learn what DSH-Plugin Hub does and join our user feedback group.',
285
+ aboutEmpty: 'Content not published yet — stay tuned.',
286
+ aboutUpdated: 'Updated',
244
287
  empty: 'No plugins in this category yet',
245
288
  dataFrom: 'Data from dsh-plugin.org · curated daily',
246
289
  }
@@ -20,12 +20,13 @@
20
20
  padding: 2px 2px 0;
21
21
  }
22
22
 
23
- /* 头部右上角控件组:语言切换 + GitHub 源码图标,右侧一列对齐 */
23
+ /* 头部右上角控件组:语言切换 + GitHub 源码图标,右侧一列对齐,auto 把它顶到标题行最右 */
24
24
  .headerRight {
25
25
  flex-shrink: 0;
26
26
  display: inline-flex;
27
27
  align-items: center;
28
28
  gap: 2px;
29
+ margin-left: auto;
29
30
  }
30
31
 
31
32
  /* 语言切换按钮:与 GitHub 图标同风格(次级灰低调、悬浮品牌紫),文字显示目标语言 */
@@ -75,11 +76,36 @@
75
76
  display: block;
76
77
  }
77
78
 
78
- /* 头部第一行:logo + 标题(官网链接)与 GitHub 图标同行,space-between 把 GitHub 顶到标题行末尾 */
79
+ /* 「关注我们」入口:GitHub 图标后,与语言切换按钮同风格 —— 普通文字、无边框,
80
+ 悬浮才转品牌紫 + 浅紫底,提示可点(打开平台介绍 + 用户反馈群二维码弹窗) */
81
+ .aboutBtn {
82
+ flex-shrink: 0;
83
+ display: inline-flex;
84
+ align-items: center;
85
+ justify-content: center;
86
+ height: 24px;
87
+ padding: 0 8px;
88
+ font-size: 12px;
89
+ font-weight: 600;
90
+ line-height: 1;
91
+ color: var(--hub-text-tertiary);
92
+ background: transparent;
93
+ border: none;
94
+ border-radius: 6px;
95
+ cursor: pointer;
96
+ user-select: none;
97
+ transition: color 0.15s ease, background-color 0.15s ease;
98
+ }
99
+
100
+ .aboutBtn:hover {
101
+ color: var(--hub-purple-1);
102
+ background-color: var(--hub-purple-tint);
103
+ }
104
+
105
+ /* 头部第一行:logo + 标题(官网链接)紧接着版本号入口,右侧控件组靠 auto 顶到最右 */
79
106
  .headerTitleRow {
80
107
  display: flex;
81
108
  align-items: center;
82
- justify-content: space-between;
83
109
  gap: 8px;
84
110
  min-width: 0;
85
111
  }
@@ -117,6 +143,60 @@
117
143
  color: var(--hub-text-secondary);
118
144
  }
119
145
 
146
+ /* 版本号入口按钮:常驻可点(打开版本信息/更新记录弹窗),无边框、字号与标题协调,
147
+ hover 加品牌色下划线提示可点 —— 无论有无更新都可查看更新内容;
148
+ 有更新时版本号后紧跟红色「可更新」徽标(同一按钮内的 span,不分开成两个元素) */
149
+ .versionBtn {
150
+ flex-shrink: 0;
151
+ display: inline-flex;
152
+ align-items: center;
153
+ gap: 5px;
154
+ height: 20px;
155
+ padding: 0 6px;
156
+ border: none;
157
+ border-radius: 5px;
158
+ font-size: 12px;
159
+ font-weight: 600;
160
+ line-height: 1;
161
+ color: var(--hub-text-secondary);
162
+ background: transparent;
163
+ cursor: pointer;
164
+ user-select: none;
165
+ transition: color 120ms ease;
166
+ }
167
+
168
+ /* 悬浮只提示整组可点,但版本号本身颜色保持不变;交互反馈交给「可更新」徽标单独变深红 */
169
+ .versionBtn:hover {
170
+ color: var(--hub-text-secondary);
171
+ }
172
+
173
+ /* 版本号旁的「可更新」徽标:默认红底白字(醒目),悬浮轻微加深 + 投影提升一点反馈感,
174
+ 紧跟版本号,提示可查看并直接升级。作为同一按钮内文字,点击由外层按钮统一处理,本身无需独立事件。 */
175
+ .hubUpdateBadge {
176
+ flex-shrink: 0;
177
+ display: inline-flex;
178
+ align-items: center;
179
+ height: 18px;
180
+ padding: 0 7px;
181
+ border-radius: 999px;
182
+ font-size: 11px;
183
+ font-weight: 700;
184
+ line-height: 1;
185
+ letter-spacing: 0.02em;
186
+ color: #ffffff;
187
+ background: var(--hub-danger);
188
+ box-shadow: 0 1px 4px rgba(209, 36, 47, 0.4);
189
+ user-select: none;
190
+ transition: background 120ms ease, box-shadow 120ms ease, transform 120ms ease;
191
+ }
192
+
193
+ /* 悬浮:轻微变深红 + 一点投影提升,只动徽标本身,版本号不动 */
194
+ .hubUpdateBadge:hover {
195
+ background: var(--hub-danger-hover);
196
+ box-shadow: 0 2px 8px rgba(209, 36, 47, 0.5);
197
+ transform: translateY(-1px);
198
+ }
199
+
120
200
  /* 品牌 logo 图标:与标题文字垂直居中对齐 */
121
201
  .logoIcon {
122
202
  flex-shrink: 0;
@@ -184,7 +184,7 @@
184
184
  user-select: none;
185
185
  }
186
186
 
187
- /* 可更新:琥珀警示色实底按钮(与 star / 有更新徽标同族),点击直接覆盖安装更新 */
187
+ /* 可更新:琥珀警示色实底按钮(与 star / 有更新徽标同族,与安装绿、卸载红区分),点击直接覆盖安装更新 */
188
188
  .installBtnUpdate {
189
189
  color: #fff;
190
190
  background: var(--hub-warn);
@@ -197,17 +197,17 @@
197
197
  background: var(--hub-warn-strong);
198
198
  }
199
199
 
200
- /* 卸载:危险色文字按钮,默认无底,悬浮转鲜红实底白字 */
200
+ /* 卸载:危险色按钮 —— 默认浅红底红字(危险但克制),悬浮转深红实底白字 */
201
201
  .uninstallBtn {
202
202
  color: var(--hub-danger);
203
- background: transparent;
203
+ background: var(--hub-danger-tint);
204
204
  border: none;
205
205
  user-select: none;
206
206
  }
207
207
 
208
208
  .uninstallBtn:hover {
209
209
  color: #ffffff;
210
- background: var(--hub-danger);
210
+ background: var(--hub-danger-hover);
211
211
  }
212
212
 
213
213
  .installBtn {