dsh-plugin 1.1.2 → 1.2.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.
- package/client/client.js +4465 -1442
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +438 -22
- package/lib/index.js +1 -1
- package/lib/services/install/install.js +4 -0
- package/lib/services/install/npm-check.js +55 -0
- package/lib/services/{npm-resolve.js → install/npm-resolve.js} +7 -5
- package/lib/services/{preflight.js → install/preflight.js} +15 -4
- package/lib/services/{task-queue.js → install/task-queue.js} +155 -66
- package/lib/services/loader.js +56 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{profile.js → profile/profile.js} +29 -1
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +3 -0
- package/lib/types/services/{install.d.ts → install/install.d.ts} +5 -5
- package/lib/types/services/install/npm-check.d.ts +12 -0
- package/lib/types/services/{npm-resolve.d.ts → install/npm-resolve.d.ts} +2 -1
- package/lib/types/services/{task-queue.d.ts → install/task-queue.d.ts} +5 -1
- package/lib/types/services/loader.d.ts +14 -8
- package/lib/types/services/log.d.ts +49 -0
- package/lib/types/services/probe.d.ts +30 -0
- package/lib/types/services/{pending-restart.d.ts → profile/pending-restart.d.ts} +1 -1
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +2 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +341 -120
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +11 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +5 -4
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +4 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +3 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +7 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +5 -5
- package/src/client/components/modals/InstalledDetailModal.tsx +193 -0
- package/src/client/components/modals/LogsModal.tsx +301 -0
- package/src/client/components/modals/LogsPathModal.tsx +161 -0
- package/src/client/components/{NotificationsModal.tsx → modals/NotificationsModal.tsx} +47 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +2 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +117 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +14 -6
- package/src/client/components/ui/Toggle.tsx +28 -0
- package/src/client/components/ui/icons.tsx +343 -0
- package/src/client/components/views/CustomInstallView.tsx +250 -0
- package/src/client/components/views/DiagnosticsView.tsx +172 -0
- package/src/client/components/views/InstalledView.tsx +328 -0
- package/src/client/components/views/LogsView.tsx +150 -0
- package/src/client/components/views/MarketView.tsx +66 -0
- package/src/client/components/views/SettingsView.tsx +273 -0
- package/src/client/data/catalog.ts +38 -0
- package/src/client/data/host.ts +107 -0
- package/src/client/data/hub.ts +41 -0
- package/src/client/hooks/useCatalog.ts +71 -115
- package/src/client/hooks/useIncrementalList.ts +46 -0
- package/src/client/hooks/useLanguage.ts +40 -0
- package/src/client/hooks/useSettings.ts +90 -0
- package/src/client/hooks/useTaskQueue.ts +156 -39
- package/src/client/locales.ts +358 -38
- package/src/client/logic/constants.ts +86 -0
- package/src/client/{lib → logic}/failures.ts +95 -74
- package/src/client/logic/install-command.ts +56 -0
- package/src/client/logic/installed.ts +207 -0
- package/src/client/logic/normalize.ts +52 -0
- package/src/client/logic/urls.ts +125 -0
- package/src/client/styles/CustomInstallView.module.css +191 -0
- package/src/client/styles/DiagnosticsView.module.css +231 -0
- package/src/client/styles/Dropdown.module.css +42 -41
- package/src/client/styles/Header.module.css +100 -132
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +6 -0
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +352 -70
- package/src/client/styles/SectionTabs.module.css +142 -0
- package/src/client/styles/SettingsView.module.css +232 -0
- package/src/client/styles/Toggle.module.css +60 -0
- package/src/client/styles/tokens.module.css +1 -0
- package/src/client/types.ts +23 -3
- package/src/server/http/routes.ts +414 -22
- package/src/server/index.ts +1 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +3 -1
- package/src/server/services/{install.ts → install/install.ts} +5 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +7 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +16 -4
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +157 -63
- package/src/server/services/loader.ts +55 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +1 -1
- package/src/server/services/{profile.ts → profile/profile.ts} +31 -1
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -108
- package/src/client/components/icons.tsx +0 -129
- package/src/client/lib/catalog.ts +0 -293
- package/src/client/lib/env.ts +0 -20
- /package/lib/services/{install-types.js → install/install-types.js} +0 -0
- /package/lib/services/{installed-versions.js → profile/installed-versions.js} +0 -0
- /package/lib/services/{pending-restart.js → profile/pending-restart.js} +0 -0
- /package/lib/services/{progress.js → profile/progress.js} +0 -0
- /package/lib/types/services/{preflight.d.ts → install/preflight.d.ts} +0 -0
- /package/lib/types/services/{installed-versions.d.ts → profile/installed-versions.d.ts} +0 -0
- /package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +0 -0
- /package/src/client/{lib → logic}/format.ts +0 -0
- /package/src/client/{lib → logic}/renderMarkdown.ts +0 -0
- /package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +0 -0
- /package/src/server/services/{progress.ts → profile/progress.ts} +0 -0
|
@@ -5,24 +5,37 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Catalog data + view state for the Plugin Hub section.
|
|
7
7
|
*
|
|
8
|
-
* Owns the online-data pipeline (live fetch from dsh-plugin.org), the local
|
|
8
|
+
* Owns the online-data pipeline (live fetch from api.dsh-plugin.org), the local
|
|
9
9
|
* installed-plugin table, and the filter/search/sort/install-status view
|
|
10
10
|
* state, exposing the derived visible list and per-category counts.
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useMemo, useState } from 'react'
|
|
13
13
|
import type { HubPlugin, LocaleId } from '../types.ts'
|
|
14
|
-
import {
|
|
15
|
-
import type { HubAboutInfo, HubUpdateInfo } from '../
|
|
16
|
-
import type { SortKey } from '../
|
|
14
|
+
import { HUB_REPO, PLUGIN_VERSION } from '../logic/constants.ts'
|
|
15
|
+
import type { HubAboutInfo, HubUpdateInfo } from '../types.ts'
|
|
16
|
+
import type { SortKey } from '../logic/constants.ts'
|
|
17
|
+
import { fetchCatalog, fetchStats } from '../data/catalog.ts'
|
|
18
|
+
import { fetchHubAbout, fetchHubUpdate } from '../data/hub.ts'
|
|
19
|
+
import { fetchInstalled } from '../data/host.ts'
|
|
20
|
+
import {
|
|
21
|
+
hasUpdateOf, installedItemsOf, installedNameOf, installedVersionOf,
|
|
22
|
+
} from '../logic/installed.ts'
|
|
23
|
+
import type { InstalledItem, InstalledVersionSignal } from '../logic/installed.ts'
|
|
17
24
|
|
|
18
|
-
const PLUGINS_URL = (lang: LocaleId) => `https://dsh-plugin.org/api/plugins.${lang}.json`
|
|
19
|
-
const STATS_URL = 'https://dsh-plugin.org/api/stats.json'
|
|
20
25
|
/** 插件市场自身仓库:DSH-Plugin Hub 不显示在目录里(自己不进自己的插件列表) */
|
|
21
26
|
const SELF_REPO = 'dshplugin/dsh-plugin-hub'
|
|
22
27
|
|
|
28
|
+
/** 市场各排序的默认方向:全部按倒序(Star/Fork 多、更新/收录近的在前) */
|
|
29
|
+
const SORT_DEFAULT_DIR: Record<SortKey, 'asc' | 'desc'> = {
|
|
30
|
+
sortStars: 'desc', sortForks: 'desc', sortUpdated: 'desc', sortNewest: 'desc',
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export function useCatalog(lang: LocaleId) {
|
|
24
34
|
/** 目录插件(仅保留人工验证通过的条目) */
|
|
25
35
|
const [plugins, setPlugins] = useState<HubPlugin[] | null>(null)
|
|
36
|
+
/** Hub 自身目录条目(dshplugin/dsh-plugin-hub):目录过滤会排除自身(不进插件列表),
|
|
37
|
+
* 此处单独保留,供头部「可更新」徽标 → 「直接更新」覆盖重装使用。 */
|
|
38
|
+
const [hubPlugin, setHubPlugin] = useState<HubPlugin | null>(null)
|
|
26
39
|
/** 收录/精选统计(官网 /api/stats.json 实时拉取) */
|
|
27
40
|
const [stats, setStats] = useState<{ total: number; verified: number } | null>(null)
|
|
28
41
|
const [failed, setFailed] = useState(false)
|
|
@@ -30,44 +43,51 @@ export function useCatalog(lang: LocaleId) {
|
|
|
30
43
|
const [category, setCategory] = useState('all')
|
|
31
44
|
const [query, setQuery] = useState('')
|
|
32
45
|
const [sort, setSort] = useState<SortKey>('sortStars')
|
|
33
|
-
/**
|
|
34
|
-
const [
|
|
46
|
+
/** 当前排序方向:点同一个排序按钮切换 正序/倒序;点新排序用该排序的默认方向(与已安装视图一致) */
|
|
47
|
+
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
|
48
|
+
|
|
49
|
+
/** 点击排序按钮:同一按钮切换正/倒序,新按钮用默认方向 */
|
|
50
|
+
function toggleSort(key: SortKey) {
|
|
51
|
+
if (key === sort) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
|
52
|
+
else { setSort(key); setSortDir(SORT_DEFAULT_DIR[key]) }
|
|
53
|
+
}
|
|
35
54
|
/** 当前 profile 已安装插件:npm 包名 -> manifest spec(来自宿主本地路由) */
|
|
36
55
|
const [installed, setInstalled] = useState<Record<string, string>>({})
|
|
37
56
|
/** 安装时记录的目录信号:repo(小写) -> { version, updatedAt }(来自宿主本地路由);
|
|
38
57
|
* npmPackage 为 npm 优先通道反查命中的包名映射(目录数据未下发时客户端靠它把依赖 key 匹配回仓库) */
|
|
39
|
-
const [versions, setVersions] = useState<Record<string,
|
|
40
|
-
/**
|
|
58
|
+
const [versions, setVersions] = useState<Record<string, InstalledVersionSignal>>({})
|
|
59
|
+
/** 每个依赖在系统上的安装目录(profile/node_modules/<包名>),详情视图展示用 */
|
|
60
|
+
const [installPaths, setInstallPaths] = useState<Record<string, string> | null>(null)
|
|
61
|
+
/** 已加载进运行中 loader 的包名(官方 ctx.loader 对账):装完未重启的新插件不在其中 */
|
|
62
|
+
const [loadedNames, setLoadedNames] = useState<string[] | null>(null)
|
|
63
|
+
/** 真正的 dsh 插件包名(包内声明 dsh 配置 / 在 profile bundles 清单):非 dsh 插件不提示「待重启」 */
|
|
64
|
+
const [dshCapableNames, setDshCapableNames] = useState<string[] | null>(null)
|
|
65
|
+
/** Hub 自我更新信息:来自接口中心 Pages(api.dsh-plugin.org),与目录数据解耦 */
|
|
41
66
|
const [hubUpdateInfo, setHubUpdateInfo] = useState<HubUpdateInfo | null>(null)
|
|
42
|
-
/** 头部「关注我们」弹窗内容(平台介绍 +
|
|
67
|
+
/** 头部「关注我们」弹窗内容(平台介绍 + 反馈群二维码):来自接口中心 /about,Markdown 推送非写死 */
|
|
43
68
|
const [hubAboutInfo, setHubAboutInfo] = useState<HubAboutInfo | null>(null)
|
|
44
69
|
|
|
45
70
|
// 拉取目录 + 统计。在线 API 已只返回 verified;再过滤一次,保证只展示人工验证通过的插件。
|
|
46
71
|
useEffect(() => {
|
|
47
72
|
let cancelled = false
|
|
48
73
|
setPlugins(null)
|
|
74
|
+
setHubPlugin(null)
|
|
49
75
|
setStats(null)
|
|
50
76
|
setFailed(false)
|
|
51
|
-
const fetchData = (url: string): Promise<unknown> =>
|
|
52
|
-
fetch(url, { cache: 'no-store' }).then((res) => {
|
|
53
|
-
if (!res.ok) throw new Error(String(res.status))
|
|
54
|
-
return res.json()
|
|
55
|
-
})
|
|
56
77
|
// 目录数据直拉:hub 自身版本由独立 Worker 版本控制中心管理,不再依赖主站版本号接口;
|
|
57
78
|
// 数据接口 CDN 缓存 1 小时,发布新数据后最长 1 小时全网生效。
|
|
58
|
-
const load = () =>
|
|
59
|
-
Promise.all([fetchData(PLUGINS_URL(lang)), fetchData(STATS_URL)])
|
|
79
|
+
const load = () => Promise.all([fetchCatalog(lang), fetchStats()])
|
|
60
80
|
load()
|
|
61
|
-
.then(([
|
|
81
|
+
.then(([list, stats]) => {
|
|
62
82
|
if (cancelled) return
|
|
63
|
-
const list = (Array.isArray(data) ? data : []).map((item) => normalize(item as Record<string, unknown>))
|
|
64
83
|
// 插件市场不显示自己:DSH-Plugin Hub 从目录里排除自身条目,
|
|
65
84
|
// 防止「自己出现在自己的插件列表里、还能自己安装自己」;统计计数同步减 1 保持与列表一致。
|
|
66
85
|
// 主站数据已同步排除自身,此处为 CDN 缓存期的兜底。
|
|
67
86
|
const hadSelf = list.some((p) => p.source?.repo === SELF_REPO)
|
|
87
|
+
// 过滤前单独保留 hub 自身条目:它不进目录列表,但「可更新」徽标的直接更新需要它作为重装目标
|
|
88
|
+
setHubPlugin(list.find((p) => p.source?.repo === SELF_REPO) ?? null)
|
|
68
89
|
setPlugins(list.filter((p) => p.compatibility?.status === 'verified' && p.source?.repo !== SELF_REPO))
|
|
69
|
-
|
|
70
|
-
if (stats && typeof stats.total === 'number' && typeof stats.verified === 'number') {
|
|
90
|
+
if (stats) {
|
|
71
91
|
setStats(hadSelf
|
|
72
92
|
? { total: stats.total - 1, verified: stats.verified - 1 }
|
|
73
93
|
: { total: stats.total, verified: stats.verified })
|
|
@@ -83,15 +103,13 @@ export function useCatalog(lang: LocaleId) {
|
|
|
83
103
|
|
|
84
104
|
/** 刷新当前 profile 已安装插件表;宿主未挂本地路由时静默降级为空表。 */
|
|
85
105
|
const refreshInstalled = async () => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// host without the plugin's server routes — keep the empty table
|
|
94
|
-
}
|
|
106
|
+
const data = await fetchInstalled()
|
|
107
|
+
if (data === null) return
|
|
108
|
+
setInstalled(data.installed)
|
|
109
|
+
setVersions(data.versions)
|
|
110
|
+
setInstallPaths(data.paths)
|
|
111
|
+
setLoadedNames(data.loaded)
|
|
112
|
+
setDshCapableNames(data.dshCapable)
|
|
95
113
|
}
|
|
96
114
|
|
|
97
115
|
// 首次进入拉取已安装表(依赖宿主 webServer 服务)
|
|
@@ -102,16 +120,7 @@ export function useCatalog(lang: LocaleId) {
|
|
|
102
120
|
// 失败静默降级为 null(徽标不出现,不影响目录本身加载)。
|
|
103
121
|
useEffect(() => {
|
|
104
122
|
let cancelled = false
|
|
105
|
-
|
|
106
|
-
.then((res): Promise<HubUpdateInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
|
|
107
|
-
.then((data) => {
|
|
108
|
-
if (cancelled || !data) { if (!cancelled) setHubUpdateInfo(null); return }
|
|
109
|
-
const version = typeof data.version === 'string' && data.version.length > 0 ? data.version : null
|
|
110
|
-
setHubUpdateInfo(version === null
|
|
111
|
-
? null
|
|
112
|
-
: { version, publishedAt: data.publishedAt ?? null, notes: data.notes ?? null })
|
|
113
|
-
})
|
|
114
|
-
.catch(() => { if (!cancelled) setHubUpdateInfo(null) })
|
|
123
|
+
void fetchHubUpdate().then((info) => { if (!cancelled) setHubUpdateInfo(info) })
|
|
115
124
|
return () => { cancelled = true }
|
|
116
125
|
}, [reloadKey])
|
|
117
126
|
|
|
@@ -119,62 +128,28 @@ export function useCatalog(lang: LocaleId) {
|
|
|
119
128
|
// 客户端下次进入拉 /about 即可看到最新,非写死。失败静默为 null(弹窗展示兜底文案,不阻塞)。
|
|
120
129
|
useEffect(() => {
|
|
121
130
|
let cancelled = false
|
|
122
|
-
|
|
123
|
-
.then((res): Promise<HubAboutInfo | null> => (res.ok ? res.json() : Promise.resolve(null)))
|
|
124
|
-
.then((data) => {
|
|
125
|
-
if (cancelled) return
|
|
126
|
-
if (!data || data.content === null || data.content === undefined) { setHubAboutInfo(null); return }
|
|
127
|
-
const content = typeof data.content === 'string' || typeof data.content === 'object' ? data.content : null
|
|
128
|
-
setHubAboutInfo(content === null
|
|
129
|
-
? null
|
|
130
|
-
: { content, updatedAt: data.updatedAt ?? null })
|
|
131
|
-
})
|
|
132
|
-
.catch(() => { if (!cancelled) setHubAboutInfo(null) })
|
|
131
|
+
void fetchHubAbout().then((info) => { if (!cancelled) setHubAboutInfo(info) })
|
|
133
132
|
return () => { cancelled = true }
|
|
134
133
|
}, [reloadKey])
|
|
135
134
|
|
|
136
135
|
/** 插件是否已安装:匹配 Git spec 中的 owner/repo,或 npm 通道安装的依赖包名;命中返回 npm 包名。 */
|
|
137
|
-
const installedName = (p: HubPlugin): string | null =>
|
|
138
|
-
const repo = p.source?.repo
|
|
139
|
-
if (!repo) return null
|
|
140
|
-
const needle = repo.toLowerCase()
|
|
141
|
-
for (const [name, spec] of Object.entries(installed)) {
|
|
142
|
-
if (repoFromInstallTarget(spec).toLowerCase() === needle) return name
|
|
143
|
-
}
|
|
144
|
-
// npm 通道安装:profile 依赖 key 直接是 npm 包名。包名来源两处——目录数据(npmPackage),
|
|
145
|
-
// 或服务端 npm 优先反查持久化的映射(versions[repo].npmPackage,覆盖组织 scope 与 GitHub
|
|
146
|
-
// 用户名不一致、目录未下发包名的场景);命中任一并依赖 key 真实存在即视为已安装
|
|
147
|
-
const pkg = ((p.source?.npmPackage || versions[repo.toLowerCase()]?.npmPackage) ?? '').toLowerCase()
|
|
148
|
-
if (pkg) {
|
|
149
|
-
for (const name of Object.keys(installed)) {
|
|
150
|
-
if (name.toLowerCase() === pkg) return name
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return null
|
|
154
|
-
}
|
|
136
|
+
const installedName = (p: HubPlugin): string | null => installedNameOf(p, installed, versions)
|
|
155
137
|
|
|
156
138
|
/** 该插件安装时记录的目录版本(无记录/未安装 → null)。 */
|
|
157
|
-
const installedVersion = (p: HubPlugin): string | null =>
|
|
158
|
-
const repo = p.source?.repo
|
|
159
|
-
if (!repo) return null
|
|
160
|
-
return versions[repo.toLowerCase()]?.version ?? null
|
|
161
|
-
}
|
|
139
|
+
const installedVersion = (p: HubPlugin): string | null => installedVersionOf(p, versions)
|
|
162
140
|
|
|
163
141
|
/**
|
|
164
142
|
* 是否有更新:仅对已安装插件有意义。
|
|
165
143
|
* 双信号判定——有 release 版本的比版本;无版本(repo 不打 tag)的比仓库最近更新时间
|
|
166
144
|
* (repoUpdatedAt,ISO 字符串字典序 = 时间序),更新时间变新说明有新提交。
|
|
167
145
|
*/
|
|
168
|
-
const hasUpdate = (p: HubPlugin): boolean =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const current = p.dates?.repoUpdatedAt
|
|
176
|
-
return Boolean(current && rec.updatedAt && current > rec.updatedAt)
|
|
177
|
-
}
|
|
146
|
+
const hasUpdate = (p: HubPlugin): boolean => hasUpdateOf(p, installed, versions)
|
|
147
|
+
|
|
148
|
+
/** 已安装项统一列表(目录元数据 + 运行时信息合并):驱动「已安装」tab。 */
|
|
149
|
+
const installedItems = useMemo<InstalledItem[]>(
|
|
150
|
+
() => installedItemsOf(plugins, installed, versions, installPaths, loadedNames, dshCapableNames),
|
|
151
|
+
[plugins, installed, versions, installPaths, loadedNames, dshCapableNames],
|
|
152
|
+
)
|
|
178
153
|
|
|
179
154
|
/**
|
|
180
155
|
* Hub 自身是否有可用更新:Hub 就是当前运行的应用(始终已安装,无需 installedName 命中)。
|
|
@@ -195,31 +170,11 @@ export function useCatalog(lang: LocaleId) {
|
|
|
195
170
|
return plugins.filter((p) => p.category === category)
|
|
196
171
|
}, [plugins, category])
|
|
197
172
|
|
|
198
|
-
/** 当前分类下已安装插件数:已安装/未安装按钮上的计数跟随分类,不再用全局口径。 */
|
|
199
|
-
const installedCountInCategory = useMemo(() => {
|
|
200
|
-
return categoryPlugins.reduce((n, p) => n + (installedName(p) !== null ? 1 : 0), 0)
|
|
201
|
-
}, [categoryPlugins, installed])
|
|
202
|
-
|
|
203
|
-
/** 当前分类下未安装插件数。 */
|
|
204
|
-
const notInstalledCountInCategory = Math.max(0, categoryPlugins.length - installedCountInCategory)
|
|
205
|
-
|
|
206
|
-
// 自动重置:切换分类(或已安装表变化)后,若当前安装状态筛选在新分类下无结果,
|
|
207
|
-
// 自动退回「全部」,避免列表空置死胡同、也避免 0 计数按钮被误触。
|
|
208
|
-
useEffect(() => {
|
|
209
|
-
if (installedFilter === 'all') return
|
|
210
|
-
const count = installedFilter === 'installed'
|
|
211
|
-
? installedCountInCategory
|
|
212
|
-
: notInstalledCountInCategory
|
|
213
|
-
if (count === 0) setInstalledFilter('all')
|
|
214
|
-
}, [installedFilter, installedCountInCategory, notInstalledCountInCategory])
|
|
215
|
-
|
|
216
173
|
const visible = useMemo(() => {
|
|
217
174
|
if (!plugins) return []
|
|
218
175
|
const q = query.trim().toLowerCase()
|
|
219
176
|
const list = plugins.filter((p) => {
|
|
220
177
|
if (category !== 'all' && p.category !== category) return false
|
|
221
|
-
if (installedFilter === 'installed' && installedName(p) === null) return false
|
|
222
|
-
if (installedFilter === 'notInstalled' && installedName(p) !== null) return false
|
|
223
178
|
if (!q) return true
|
|
224
179
|
return (
|
|
225
180
|
(p.displayName ?? '').toLowerCase().includes(q) ||
|
|
@@ -227,13 +182,15 @@ export function useCatalog(lang: LocaleId) {
|
|
|
227
182
|
(p.topics ?? []).some((topic) => topic.toLowerCase().includes(q))
|
|
228
183
|
)
|
|
229
184
|
})
|
|
185
|
+
const dir = sortDir === 'asc' ? 1 : -1
|
|
230
186
|
return [...list].sort((a, b) => {
|
|
231
|
-
|
|
232
|
-
if (sort === '
|
|
233
|
-
if (sort === '
|
|
234
|
-
return (
|
|
187
|
+
// 比较器统一用「升序写法」再乘 dir:desc 时 dir=-1 翻成降序(Star/Fork 最多、更新/收录最近在前)
|
|
188
|
+
if (sort === 'sortStars') return ((a.stats?.stargazers_count ?? 0) - (b.stats?.stargazers_count ?? 0)) * dir
|
|
189
|
+
if (sort === 'sortForks') return ((a.stats?.forks_count ?? 0) - (b.stats?.forks_count ?? 0)) * dir
|
|
190
|
+
if (sort === 'sortNewest') return (a.dates?.addedAt ?? '').localeCompare(b.dates?.addedAt ?? '') * dir
|
|
191
|
+
return (a.dates?.repoUpdatedAt ?? '').localeCompare(b.dates?.repoUpdatedAt ?? '') * dir
|
|
235
192
|
})
|
|
236
|
-
}, [plugins, category, query, sort,
|
|
193
|
+
}, [plugins, category, query, sort, sortDir, installed])
|
|
237
194
|
|
|
238
195
|
/** Per-category plugin counts shown on the category chips. */
|
|
239
196
|
const categoryCounts = useMemo(() => {
|
|
@@ -246,10 +203,12 @@ export function useCatalog(lang: LocaleId) {
|
|
|
246
203
|
|
|
247
204
|
return {
|
|
248
205
|
plugins,
|
|
206
|
+
hubPlugin,
|
|
249
207
|
stats,
|
|
250
208
|
failed,
|
|
251
209
|
reload: () => setReloadKey((k) => k + 1),
|
|
252
210
|
installed,
|
|
211
|
+
installedItems,
|
|
253
212
|
installedName,
|
|
254
213
|
installedVersion,
|
|
255
214
|
hasUpdate,
|
|
@@ -262,13 +221,10 @@ export function useCatalog(lang: LocaleId) {
|
|
|
262
221
|
query,
|
|
263
222
|
setQuery,
|
|
264
223
|
sort,
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
setInstalledFilter,
|
|
224
|
+
sortDir,
|
|
225
|
+
toggleSort,
|
|
268
226
|
visible,
|
|
269
227
|
total: plugins?.length ?? 0,
|
|
270
|
-
installedCountInCategory,
|
|
271
|
-
notInstalledCountInCategory,
|
|
272
228
|
categoryCounts,
|
|
273
229
|
}
|
|
274
230
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* Incremental list: renders only the first batch of a (potentially huge)
|
|
7
|
+
* result set and appends the next batch whenever the end sentinel scrolls
|
|
8
|
+
* into view, so the plugin center never mounts thousands of cards at once.
|
|
9
|
+
* The items reference is the list identity — category / search / sort
|
|
10
|
+
* changes produce a new array, which resets the window to the first batch.
|
|
11
|
+
*/
|
|
12
|
+
import { useEffect, useRef, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
/** 首批渲染条数 & 每次滚到底追加的增量(单卡约 80px 高,100 条已远超一屏) */
|
|
15
|
+
const PAGE_SIZE = 100
|
|
16
|
+
/** 哨兵进入视口底部前 800px 就提前加载下一批,滚到底时无感知衔接 */
|
|
17
|
+
const PRELOAD_MARGIN = '800px 0px'
|
|
18
|
+
|
|
19
|
+
export function useIncrementalList<T>(items: T[]) {
|
|
20
|
+
const [limit, setLimit] = useState(PAGE_SIZE)
|
|
21
|
+
const sentinelRef = useRef<HTMLDivElement | null>(null)
|
|
22
|
+
|
|
23
|
+
// 筛选/分类/排序变化 → items 引用变化 → 窗口重置回首批
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setLimit(PAGE_SIZE)
|
|
26
|
+
}, [items])
|
|
27
|
+
|
|
28
|
+
// 哨兵进入视口(含预加载区)→ 追加下一批;已全部渲染则不再观察
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const el = sentinelRef.current
|
|
31
|
+
if (!el || limit >= items.length) return
|
|
32
|
+
const io = new IntersectionObserver((entries) => {
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (entry.isIntersecting) setLimit((cur) => Math.min(cur + PAGE_SIZE, items.length))
|
|
35
|
+
}
|
|
36
|
+
}, { rootMargin: PRELOAD_MARGIN })
|
|
37
|
+
io.observe(el)
|
|
38
|
+
return () => io.disconnect()
|
|
39
|
+
}, [items.length, limit])
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
shown: items.slice(0, limit),
|
|
43
|
+
hasMore: limit < items.length,
|
|
44
|
+
sentinelRef,
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* Language state for the section: follows the host (system) locale by
|
|
7
|
+
* default, with an optional manual toggle that wins until the user changes
|
|
8
|
+
* it back. Exposes the localized dictionary lookup `t()` plus the zh/en
|
|
9
|
+
* language flag and the /zh/ URL path prefix.
|
|
10
|
+
*/
|
|
11
|
+
import { useState } from 'react'
|
|
12
|
+
import type { LocaleId, SectionProps } from '../types.ts'
|
|
13
|
+
import { en, zh } from '../locales.ts'
|
|
14
|
+
import { langPathOf } from '../logic/urls.ts'
|
|
15
|
+
|
|
16
|
+
export function useLanguage(locale: SectionProps['locale']) {
|
|
17
|
+
/** 界面语言:默认跟随宿主(系统)语言;右上角按钮可手动切换,切换后以手动选择为准 */
|
|
18
|
+
const [manualLang, setManualLang] = useState<LocaleId | null>(null)
|
|
19
|
+
const lang: LocaleId = manualLang ?? locale.getSnapshot().active
|
|
20
|
+
const langKey: LocaleId = lang === 'en' ? 'en' : 'zh'
|
|
21
|
+
// dsh-plugin.org keeps zh pages under the /zh/ prefix; en is the root.
|
|
22
|
+
const langPath = langPathOf(lang)
|
|
23
|
+
|
|
24
|
+
// 界面语言跟随宿主(系统)locale 自动切换;宿主的 t() 绑定宿主 locale,
|
|
25
|
+
// 这里基于本地字典自建翻译函数,与宿主 locale 保持一致。
|
|
26
|
+
// (settings.section 的导航 label 仍在 apply 里用宿主 t(),跟随宿主语言。)
|
|
27
|
+
const dict: Record<string, string> = langKey === 'en' ? en : zh
|
|
28
|
+
const t = (key: string, params?: Record<string, string | number>): string => {
|
|
29
|
+
const raw = dict[key] ?? key
|
|
30
|
+
return params ? raw.replace(/\{(\w+)\}/g, (_, k) => String(params[k] ?? '')) : raw
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
lang,
|
|
35
|
+
langKey,
|
|
36
|
+
langPath,
|
|
37
|
+
t,
|
|
38
|
+
toggleLang: () => setManualLang(lang === 'en' ? 'zh' : 'en'),
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH-Plugin Hub — the community plugin marketplace for DeepSeek Harness.
|
|
3
|
+
* Website: https://dsh-plugin.org
|
|
4
|
+
* GitHub: https://github.com/dshplugin/dsh-plugin-hub
|
|
5
|
+
*
|
|
6
|
+
* Hub settings state: loaded once from the host's local /settings route
|
|
7
|
+
* (persisted at ~/.dsh/profiles/<profile>/hub-settings.json), updated
|
|
8
|
+
* optimistically. Every change POSTs the patch immediately so a toggled
|
|
9
|
+
* switch survives a reload; a host without the server routes falls back to
|
|
10
|
+
* the defaults and keeps working (settings simply do not persist).
|
|
11
|
+
*/
|
|
12
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
13
|
+
|
|
14
|
+
export interface HubSettings {
|
|
15
|
+
/** 启动时检查目录插件更新,发现可更新时在通知中心提示(安装不自动进行) */
|
|
16
|
+
checkUpdatesOnStart: boolean
|
|
17
|
+
/** npm 镜像源 registry 地址(空串 = 官方 https://registry.npmjs.org) */
|
|
18
|
+
npmRegistry: string
|
|
19
|
+
/** HTTP(S) 代理地址,用于 npm / git / 目录请求(空串 = 直连) */
|
|
20
|
+
proxy: string
|
|
21
|
+
/** 启用命令行安装 NPM 包(默认开) */
|
|
22
|
+
enableNpmInstall: boolean
|
|
23
|
+
/** 启用命令行安装 GitHub 源码(默认开) */
|
|
24
|
+
enableGitInstall: boolean
|
|
25
|
+
/** 启用命令行粘贴 dsh plugin 命令安装(默认开) */
|
|
26
|
+
enableDshInstall: boolean
|
|
27
|
+
/** 日志存放位置覆盖(空串 = 默认 ~/.dsh/profiles/<profile>/hub.log) */
|
|
28
|
+
logPath: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_SETTINGS: HubSettings = {
|
|
32
|
+
checkUpdatesOnStart: true,
|
|
33
|
+
npmRegistry: '',
|
|
34
|
+
proxy: '',
|
|
35
|
+
enableNpmInstall: true,
|
|
36
|
+
enableGitInstall: true,
|
|
37
|
+
enableDshInstall: true,
|
|
38
|
+
logPath: '',
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function loadRemote(): Promise<HubSettings> {
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch('/dsh-plugin-hub/settings', { cache: 'no-store' })
|
|
44
|
+
if (!res.ok) throw new Error(`settings ${res.status}`)
|
|
45
|
+
const data = await res.json() as Partial<HubSettings>
|
|
46
|
+
return { ...DEFAULT_SETTINGS, ...data }
|
|
47
|
+
} catch {
|
|
48
|
+
return DEFAULT_SETTINGS
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useSettings() {
|
|
53
|
+
const [settings, setSettings] = useState<HubSettings>(DEFAULT_SETTINGS)
|
|
54
|
+
/** 服务端配置是否已加载:更新策略等启动期逻辑须等真实值,避免用默认值误触发 */
|
|
55
|
+
const [ready, setReady] = useState(false)
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
let alive = true
|
|
59
|
+
void loadRemote().then((s) => {
|
|
60
|
+
if (!alive) return
|
|
61
|
+
setSettings(s)
|
|
62
|
+
setReady(true)
|
|
63
|
+
})
|
|
64
|
+
return () => { alive = false }
|
|
65
|
+
}, [])
|
|
66
|
+
|
|
67
|
+
/** 部分更新:本地立即生效(乐观),同时 POST 持久化;持久化失败静默(本次会话仍生效)。 */
|
|
68
|
+
const update = useCallback((patch: Partial<HubSettings>) => {
|
|
69
|
+
setSettings((prev) => ({ ...prev, ...patch }))
|
|
70
|
+
void fetch('/dsh-plugin-hub/settings', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(patch),
|
|
74
|
+
cache: 'no-store',
|
|
75
|
+
}).catch(() => { /* 宿主未挂路由:仅本次会话生效 */ })
|
|
76
|
+
}, [])
|
|
77
|
+
|
|
78
|
+
/** 重置为默认设置:本地立即回默认,同时通知服务端删除设置文件;失败静默。 */
|
|
79
|
+
const reset = useCallback(() => {
|
|
80
|
+
setSettings({ ...DEFAULT_SETTINGS })
|
|
81
|
+
void fetch('/dsh-plugin-hub/settings/reset', {
|
|
82
|
+
method: 'POST',
|
|
83
|
+
headers: { 'content-type': 'application/json' },
|
|
84
|
+
body: '{}',
|
|
85
|
+
cache: 'no-store',
|
|
86
|
+
}).catch(() => { /* 宿主未挂路由:仅本次会话回默认 */ })
|
|
87
|
+
}, [])
|
|
88
|
+
|
|
89
|
+
return { settings, ready, update, reset }
|
|
90
|
+
}
|