dsh-plugin 1.1.1 → 1.2.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/README.en.md +5 -5
- package/README.md +5 -5
- package/client/client.js +4772 -1659
- package/client/client.js.map +1 -1
- package/lib/http/routes.js +451 -28
- package/lib/index.js +5 -1
- package/lib/services/{install-types.js → install/install-types.js} +8 -2
- 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} +11 -5
- package/lib/services/{preflight.js → install/preflight.js} +23 -7
- package/lib/services/{task-queue.js → install/task-queue.js} +166 -71
- package/lib/services/loader.js +60 -1
- package/lib/services/log.js +138 -0
- package/lib/services/probe.js +157 -0
- package/lib/services/{installed-versions.js → profile/installed-versions.js} +5 -1
- package/lib/services/{pending-restart.js → profile/pending-restart.js} +1 -1
- package/lib/services/{profile.js → profile/profile.js} +59 -4
- package/lib/services/{progress.js → profile/progress.js} +4 -0
- package/lib/services/settings.js +60 -0
- package/lib/types/http/routes.d.ts +1 -1
- package/lib/types/index.d.ts +5 -1
- package/lib/types/services/{install-types.d.ts → install/install-types.d.ts} +13 -4
- package/lib/types/services/{install.d.ts → install/install.d.ts} +9 -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 -4
- 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} +9 -3
- package/lib/types/services/{profile.d.ts → profile/profile.d.ts} +6 -2
- package/lib/types/services/{progress.d.ts → profile/progress.d.ts} +4 -0
- package/lib/types/services/settings.d.ts +24 -0
- package/package.json +1 -1
- package/src/client/components/PluginHubSection.tsx +346 -121
- package/src/client/components/{CatalogList.tsx → catalog/CatalogList.tsx} +15 -7
- package/src/client/components/{PluginCard.tsx → catalog/PluginCard.tsx} +10 -5
- package/src/client/components/layout/CatalogControls.tsx +80 -0
- package/src/client/components/{CatalogHeader.tsx → layout/CatalogHeader.tsx} +8 -4
- package/src/client/components/{CategoryTabs.tsx → layout/CategoryTabs.tsx} +7 -3
- package/src/client/components/layout/SectionTabs.tsx +60 -0
- package/src/client/components/{AboutModal.tsx → modals/AboutModal.tsx} +11 -7
- package/src/client/components/{HubUpdateModal.tsx → modals/HubUpdateModal.tsx} +9 -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} +51 -20
- package/src/client/components/{ProgressView.tsx → modals/ProgressView.tsx} +6 -2
- package/src/client/components/{modals.tsx → modals/modals.tsx} +121 -40
- package/src/client/components/{Dropdown.tsx → ui/Dropdown.tsx} +18 -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 +77 -117
- 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 +168 -47
- package/src/client/index.tsx +4 -0
- package/src/client/locales.ts +365 -39
- package/src/client/logic/constants.ts +87 -0
- package/src/client/{lib → logic}/failures.ts +99 -74
- package/src/client/{lib → logic}/format.ts +4 -0
- 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/{lib → logic}/renderMarkdown.ts +4 -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 +46 -41
- package/src/client/styles/Header.module.css +105 -133
- package/src/client/styles/InstalledView.module.css +447 -0
- package/src/client/styles/List.module.css +11 -1
- package/src/client/styles/LogsView.module.css +503 -0
- package/src/client/styles/Modal.module.css +357 -71
- 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 +6 -1
- package/src/client/types.ts +28 -4
- package/src/server/http/routes.ts +426 -28
- package/src/server/index.ts +5 -1
- package/src/server/services/{install-types.ts → install/install-types.ts} +13 -5
- package/src/server/services/{install.ts → install/install.ts} +9 -5
- package/src/server/services/install/npm-check.ts +53 -0
- package/src/server/services/{npm-resolve.ts → install/npm-resolve.ts} +11 -5
- package/src/server/services/{preflight.ts → install/preflight.ts} +24 -7
- package/src/server/services/{task-queue.ts → install/task-queue.ts} +168 -68
- package/src/server/services/loader.ts +59 -1
- package/src/server/services/log.ts +167 -0
- package/src/server/services/probe.ts +157 -0
- package/src/server/services/{installed-versions.ts → profile/installed-versions.ts} +5 -1
- package/src/server/services/{pending-restart.ts → profile/pending-restart.ts} +9 -3
- package/src/server/services/{profile.ts → profile/profile.ts} +58 -4
- package/src/server/services/{progress.ts → profile/progress.ts} +4 -0
- package/src/server/services/settings.ts +79 -0
- package/lib/services/install.js +0 -4
- package/src/client/components/CatalogControls.tsx +0 -104
- package/src/client/components/icons.tsx +0 -125
- package/src/client/lib/catalog.ts +0 -272
- package/src/client/lib/env.ts +0 -16
- /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
|
@@ -0,0 +1,301 @@
|
|
|
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
|
+
* Built-in log viewer dialog: no external software needed — the app opens
|
|
7
|
+
* its own window and loads the local system log (JSONL at
|
|
8
|
+
* ~/.dsh/profiles/<profile>/hub.log) straight from the server, one page at
|
|
9
|
+
* a time.
|
|
10
|
+
*
|
|
11
|
+
* Every line is categorized twice: a function area (install / uninstall /
|
|
12
|
+
* update / diagnostics / settings / system) and a severity level (debug /
|
|
13
|
+
* info / success / warn / error). Filtering on both axes lets you target,
|
|
14
|
+
* say, only install errors — so the exact incompatible step of an install
|
|
15
|
+
* (or uninstall) is easy to spot. Search narrows by event code or message.
|
|
16
|
+
*
|
|
17
|
+
* Server pagination: GET /dsh-plugin-hub/logs?offset=&limit=&category=
|
|
18
|
+
* &level=&query= → { entries, total, hasMore, path }. The footer shows the
|
|
19
|
+
* real file location and reveals it in the system file manager.
|
|
20
|
+
*/
|
|
21
|
+
import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
|
|
22
|
+
import type { MouseEvent } from 'react'
|
|
23
|
+
import styles from '../../styles/Modal.module.css'
|
|
24
|
+
import view from '../../styles/LogsView.module.css'
|
|
25
|
+
import type { Translate } from '../../types.ts'
|
|
26
|
+
import { openLogFile } from '../../data/host.ts'
|
|
27
|
+
import { CloseIcon } from '../ui/icons.tsx'
|
|
28
|
+
|
|
29
|
+
/** 与服务端 /logs 返回的 LogEntry 对齐的客户端投影。 */
|
|
30
|
+
export interface LogEntryView {
|
|
31
|
+
/** 毫秒时间戳 */
|
|
32
|
+
at: number
|
|
33
|
+
level: 'debug' | 'info' | 'success' | 'warn' | 'error'
|
|
34
|
+
category: 'install' | 'uninstall' | 'update' | 'diagnostics' | 'settings' | 'system'
|
|
35
|
+
/** 机器可读事件码,如 install.start */
|
|
36
|
+
event: string
|
|
37
|
+
message: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type LogCategoryFilter = 'all' | LogEntryView['category']
|
|
41
|
+
export type LogLevelFilter = 'all' | LogEntryView['level']
|
|
42
|
+
|
|
43
|
+
const PAGE = 100
|
|
44
|
+
|
|
45
|
+
/** 类别筛选标签:值 → 语言包 key(弹窗内 chip 与行内类别徽章共用)。 */
|
|
46
|
+
const CAT_LABEL: Record<LogEntryView['category'], string> = {
|
|
47
|
+
install: 'logCatInstall',
|
|
48
|
+
uninstall: 'logCatUninstall',
|
|
49
|
+
update: 'logCatUpdate',
|
|
50
|
+
diagnostics: 'logCatDiagnostics',
|
|
51
|
+
settings: 'logCatSettings',
|
|
52
|
+
system: 'logCatSystem',
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CAT_FILTERS: Array<{ value: LogCategoryFilter; key: string }> = [
|
|
56
|
+
{ value: 'all', key: 'logCatAll' },
|
|
57
|
+
{ value: 'install', key: 'logCatInstall' },
|
|
58
|
+
{ value: 'uninstall', key: 'logCatUninstall' },
|
|
59
|
+
{ value: 'update', key: 'logCatUpdate' },
|
|
60
|
+
{ value: 'diagnostics', key: 'logCatDiagnostics' },
|
|
61
|
+
{ value: 'settings', key: 'logCatSettings' },
|
|
62
|
+
{ value: 'system', key: 'logCatSystem' },
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
const LEVEL_FILTERS: Array<{ value: LogLevelFilter; key: string }> = [
|
|
66
|
+
{ value: 'all', key: 'logLvAll' },
|
|
67
|
+
{ value: 'error', key: 'logLvError' },
|
|
68
|
+
{ value: 'warn', key: 'logLvWarn' },
|
|
69
|
+
{ value: 'success', key: 'logLvSuccess' },
|
|
70
|
+
{ value: 'info', key: 'logLvInfo' },
|
|
71
|
+
{ value: 'debug', key: 'logLvDebug' },
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
/** 级别徽章文案:弹窗 chip 与行内级别徽章共用同一语言包 key。 */
|
|
75
|
+
const LEVEL_LABEL: Record<LogEntryView['level'], string> = {
|
|
76
|
+
debug: 'logLvDebug',
|
|
77
|
+
info: 'logLvInfo',
|
|
78
|
+
success: 'logLvSuccess',
|
|
79
|
+
warn: 'logLvWarn',
|
|
80
|
+
error: 'logLvError',
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** 时间:YYYY-MM-DD HH:mm:ss(每条都精确到秒)。 */
|
|
84
|
+
export function fmtLogTime(at: number): string {
|
|
85
|
+
const d = new Date(at)
|
|
86
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
87
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 导出文件名时间戳:2026-08-24_21-35-00。 */
|
|
91
|
+
function fmtFileStamp(at: number): string {
|
|
92
|
+
const d = new Date(at)
|
|
93
|
+
const pad = (n: number) => String(n).padStart(2, '0')
|
|
94
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 单条日志行:时间 + 级别徽章 + 类别徽章 + 事件码 + 描述。查看器弹窗与设置页预览共用。 */
|
|
98
|
+
export function LogEntryRow({ e, t }: { e: LogEntryView; t: Translate }) {
|
|
99
|
+
return h('div', { className: view.entry, title: e.message },
|
|
100
|
+
h('span', { className: view.time }, fmtLogTime(e.at)),
|
|
101
|
+
h('span', { className: `${view.badge} ${view[`badge${e.level}`]}` }, t(LEVEL_LABEL[e.level])),
|
|
102
|
+
h('span', { className: `${view.catBadge} ${view[`cat${e.category}`]}` }, t(CAT_LABEL[e.category])),
|
|
103
|
+
h('span', { className: view.event }, e.event),
|
|
104
|
+
h('span', { className: view.message }, e.message),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function LogsModal({ t, onClose }: {
|
|
109
|
+
t: Translate
|
|
110
|
+
onClose: () => void
|
|
111
|
+
}) {
|
|
112
|
+
const [category, setCategory] = useState<LogCategoryFilter>('all')
|
|
113
|
+
const [level, setLevel] = useState<LogLevelFilter>('all')
|
|
114
|
+
const [query, setQuery] = useState('')
|
|
115
|
+
const [entries, setEntries] = useState<LogEntryView[]>([])
|
|
116
|
+
const [total, setTotal] = useState(0)
|
|
117
|
+
const [hasMore, setHasMore] = useState(false)
|
|
118
|
+
const [path, setPath] = useState('')
|
|
119
|
+
const [loading, setLoading] = useState(false)
|
|
120
|
+
const [error, setError] = useState(false)
|
|
121
|
+
const [opening, setOpening] = useState(false)
|
|
122
|
+
const [openFailed, setOpenFailed] = useState(false)
|
|
123
|
+
const offsetRef = useRef(0)
|
|
124
|
+
|
|
125
|
+
/** 拉一页:过滤参数取当前 state,偏移量由调用方给出。 */
|
|
126
|
+
const fetchPage = useCallback(async (offset: number): Promise<{ entries: LogEntryView[]; total: number; hasMore: boolean; path: string } | null> => {
|
|
127
|
+
try {
|
|
128
|
+
const qs = new URLSearchParams({ offset: String(offset), limit: String(PAGE) })
|
|
129
|
+
if (category !== 'all') qs.set('category', category)
|
|
130
|
+
if (level !== 'all') qs.set('level', level)
|
|
131
|
+
const q = query.trim()
|
|
132
|
+
if (q !== '') qs.set('query', q)
|
|
133
|
+
const res = await fetch(`/dsh-plugin-hub/logs?${qs.toString()}`, { cache: 'no-store' })
|
|
134
|
+
if (!res.ok) return null
|
|
135
|
+
return await res.json() as { entries: LogEntryView[]; total: number; hasMore: boolean; path: string }
|
|
136
|
+
} catch {
|
|
137
|
+
return null
|
|
138
|
+
}
|
|
139
|
+
}, [category, level, query])
|
|
140
|
+
|
|
141
|
+
/** 从第一页重新加载(换筛选/搜索时)。 */
|
|
142
|
+
const reload = useCallback(async () => {
|
|
143
|
+
setLoading(true)
|
|
144
|
+
const data = await fetchPage(0)
|
|
145
|
+
if (data === null) {
|
|
146
|
+
setError(true)
|
|
147
|
+
setLoading(false)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
offsetRef.current = data.entries.length
|
|
151
|
+
setEntries(data.entries)
|
|
152
|
+
setTotal(data.total)
|
|
153
|
+
setHasMore(data.hasMore)
|
|
154
|
+
if (data.path !== '') setPath(data.path)
|
|
155
|
+
setError(false)
|
|
156
|
+
setLoading(false)
|
|
157
|
+
}, [fetchPage])
|
|
158
|
+
|
|
159
|
+
/** 追加下一页(「加载更多」)。 */
|
|
160
|
+
const loadMore = useCallback(async () => {
|
|
161
|
+
if (loading) return
|
|
162
|
+
setLoading(true)
|
|
163
|
+
const data = await fetchPage(offsetRef.current)
|
|
164
|
+
if (data === null) {
|
|
165
|
+
setError(true)
|
|
166
|
+
setLoading(false)
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
offsetRef.current += data.entries.length
|
|
170
|
+
setEntries((prev) => [...prev, ...data.entries])
|
|
171
|
+
setTotal(data.total)
|
|
172
|
+
setHasMore(data.hasMore)
|
|
173
|
+
setLoading(false)
|
|
174
|
+
}, [fetchPage, loading])
|
|
175
|
+
|
|
176
|
+
// 首次进入 + 类别/级别/搜索变化(搜索 280ms 防抖)都重查第一页
|
|
177
|
+
const first = useRef(true)
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
if (first.current) {
|
|
180
|
+
first.current = false
|
|
181
|
+
void reload()
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
const id = window.setTimeout(() => { void reload() }, 280)
|
|
185
|
+
return () => window.clearTimeout(id)
|
|
186
|
+
}, [reload])
|
|
187
|
+
|
|
188
|
+
/** 在系统文件管理器中定位日志文件(服务端 spawn open)。 */
|
|
189
|
+
const openFile = async () => {
|
|
190
|
+
if (opening) return
|
|
191
|
+
setOpening(true)
|
|
192
|
+
setOpenFailed(false)
|
|
193
|
+
const ok = await openLogFile()
|
|
194
|
+
setOpening(false)
|
|
195
|
+
if (!ok) setOpenFailed(true)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** 导出:拼纯文本 → Blob → 触发浏览器下载 .log 文件 */
|
|
199
|
+
const exportFile = () => {
|
|
200
|
+
const text = entries.map((e) => `[${fmtLogTime(e.at)}] [${e.level}] [${e.category}] [${e.event}] ${e.message}`).join('\n')
|
|
201
|
+
if (text === '') return
|
|
202
|
+
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
|
203
|
+
const url = URL.createObjectURL(blob)
|
|
204
|
+
const a = document.createElement('a')
|
|
205
|
+
a.href = url
|
|
206
|
+
a.download = `dsh-plugin-hub-log-${fmtFileStamp(Date.now())}.log`
|
|
207
|
+
document.body.appendChild(a)
|
|
208
|
+
a.click()
|
|
209
|
+
a.remove()
|
|
210
|
+
URL.revokeObjectURL(url)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return h('div', {
|
|
214
|
+
className: styles.overlay,
|
|
215
|
+
onClick: (e: MouseEvent<HTMLDivElement>) => {
|
|
216
|
+
if (e.target === e.currentTarget) onClose()
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
h('div', { className: `${styles.errorModal} ${styles.logModal}`, role: 'dialog', 'aria-modal': 'true' },
|
|
220
|
+
h('div', { className: styles.modalHead },
|
|
221
|
+
h('div', { className: styles.modalTitle }, t('logModalTitle')),
|
|
222
|
+
h('button', {
|
|
223
|
+
className: styles.modalClose,
|
|
224
|
+
'aria-label': t('errorClose'),
|
|
225
|
+
onClick: onClose,
|
|
226
|
+
}, h(CloseIcon)),
|
|
227
|
+
),
|
|
228
|
+
h('div', { className: styles.modalBody },
|
|
229
|
+
// 类别筛选行:安装/卸载/更新… 功能域区分
|
|
230
|
+
h('div', { className: view.filterBar },
|
|
231
|
+
CAT_FILTERS.map((f) => h('button', {
|
|
232
|
+
key: f.value,
|
|
233
|
+
type: 'button',
|
|
234
|
+
className: category === f.value ? `${view.filterChip} ${view.filterChipActive}` : view.filterChip,
|
|
235
|
+
onClick: () => setCategory(f.value),
|
|
236
|
+
}, t(f.key))),
|
|
237
|
+
),
|
|
238
|
+
// 级别筛选行 + 搜索框:按级别区分报错与正常,便于针对性排查
|
|
239
|
+
h('div', { className: view.filterBar },
|
|
240
|
+
LEVEL_FILTERS.map((f) => h('button', {
|
|
241
|
+
key: f.value,
|
|
242
|
+
type: 'button',
|
|
243
|
+
className: level === f.value ? `${view.filterChip} ${view.filterChipActive}` : view.filterChip,
|
|
244
|
+
onClick: () => setLevel(f.value),
|
|
245
|
+
}, t(f.key))),
|
|
246
|
+
h('div', { className: view.search },
|
|
247
|
+
h('input', {
|
|
248
|
+
className: view.searchInput,
|
|
249
|
+
type: 'search',
|
|
250
|
+
value: query,
|
|
251
|
+
placeholder: t('logSearchPlaceholder'),
|
|
252
|
+
'aria-label': t('logSearchPlaceholder'),
|
|
253
|
+
onChange: (e) => setQuery((e.target as HTMLInputElement).value),
|
|
254
|
+
}),
|
|
255
|
+
),
|
|
256
|
+
),
|
|
257
|
+
h('div', { className: view.logList },
|
|
258
|
+
error
|
|
259
|
+
? h('div', { className: view.empty }, t('logsLoadFail'))
|
|
260
|
+
: loading && entries.length === 0
|
|
261
|
+
? h('div', { className: view.empty }, t('logsLoading'))
|
|
262
|
+
: entries.length === 0
|
|
263
|
+
? h('div', { className: view.empty }, t('logEmptyFilter'))
|
|
264
|
+
: entries.map((e, i) => h(LogEntryRow, { key: `${e.at}-${i}`, e, t })),
|
|
265
|
+
// 分页:还有就「加载更多」,拉完了给个总数收尾
|
|
266
|
+
entries.length > 0 && (hasMore
|
|
267
|
+
? h('button', {
|
|
268
|
+
type: 'button',
|
|
269
|
+
className: view.more,
|
|
270
|
+
disabled: loading,
|
|
271
|
+
onClick: () => void loadMore(),
|
|
272
|
+
}, loading ? t('logsLoading') : t('logLoadMore'))
|
|
273
|
+
: h('div', { className: view.moreEnd }, t('logNoMore', { n: total }))),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
// 底部:日志真实路径 + 打开文件 / 导出日志
|
|
277
|
+
h('div', { className: view.foot },
|
|
278
|
+
h('span', { className: view.footPath, title: path },
|
|
279
|
+
`${t('logPathLabel')}: ${path === '' ? '…' : path}`),
|
|
280
|
+
openFailed
|
|
281
|
+
? h('span', { className: view.footFail }, t('logOpenFileFail'))
|
|
282
|
+
: h('span', { className: view.footCount }, t('logCount', { n: total })),
|
|
283
|
+
h('div', { className: view.footActions },
|
|
284
|
+
h('button', {
|
|
285
|
+
type: 'button',
|
|
286
|
+
className: view.btn,
|
|
287
|
+
disabled: opening || path === '',
|
|
288
|
+
title: t('logOpenFileTip'),
|
|
289
|
+
onClick: () => void openFile(),
|
|
290
|
+
}, t('logOpenFile')),
|
|
291
|
+
h('button', {
|
|
292
|
+
type: 'button',
|
|
293
|
+
className: `${view.btn} ${view.btnPrimary}`,
|
|
294
|
+
disabled: entries.length === 0,
|
|
295
|
+
onClick: exportFile,
|
|
296
|
+
}, t('logsExport')),
|
|
297
|
+
),
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
)
|
|
301
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
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
|
+
* 日志存放位置弹窗:设置页不内联长表单(挡视线),点「修改」弹窗编辑。
|
|
7
|
+
* 两种输入方式并存 —— 手输目录 / .log 文件路径,或点「选择目录」调系统
|
|
8
|
+
* 原生文件夹对话框(服务端 spawn osascript / FolderBrowserDialog / zenity)。
|
|
9
|
+
* 文本框预填当前生效位置,目录选择器也从该位置打开 —— 默认地址是动态的
|
|
10
|
+
* (随用户主目录/平台变化,如 ~/.dsh/profiles/<profile>/hub.log),必须实时展示。
|
|
11
|
+
* 留空或点「恢复默认」= 回到默认;保存前服务端预验证可写。
|
|
12
|
+
*/
|
|
13
|
+
import { createElement as h, useEffect, useState } from 'react'
|
|
14
|
+
import styles from '../../styles/Modal.module.css'
|
|
15
|
+
import view from '../../styles/LogsView.module.css'
|
|
16
|
+
import type { Translate } from '../../types.ts'
|
|
17
|
+
import { chooseLogDir } from '../../data/host.ts'
|
|
18
|
+
import { CloseIcon } from '../ui/icons.tsx'
|
|
19
|
+
|
|
20
|
+
export function LogsPathModal({ t, defaultPath, currentPath, onSaved, onClose }: {
|
|
21
|
+
t: Translate
|
|
22
|
+
/** 真正默认位置(服务端动态算出的 ~/.dsh/profiles/<profile>/hub.log),跨机器/平台不同 */
|
|
23
|
+
defaultPath: string
|
|
24
|
+
/** 当前生效的真实路径(可能已被自定义覆盖) */
|
|
25
|
+
currentPath: string
|
|
26
|
+
/** 保存成功回调(写入设置;父级据此刷新生效路径与日志列表) */
|
|
27
|
+
onSaved: (value: string) => void
|
|
28
|
+
onClose: () => void
|
|
29
|
+
}) {
|
|
30
|
+
// 打开即显示当前生效位置 —— 用户一眼看到日志现在存在哪,也方便在此基础上微调
|
|
31
|
+
const [draft, setDraft] = useState(currentPath)
|
|
32
|
+
const [saving, setSaving] = useState(false)
|
|
33
|
+
const [fail, setFail] = useState(false)
|
|
34
|
+
const [browsing, setBrowsing] = useState(false)
|
|
35
|
+
const [browseFail, setBrowseFail] = useState(false)
|
|
36
|
+
|
|
37
|
+
// Esc 关闭:与其它弹窗交互一致
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
|
40
|
+
window.addEventListener('keydown', onKey)
|
|
41
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
42
|
+
}, [onClose])
|
|
43
|
+
|
|
44
|
+
/** 保存:空串 = 回默认;填回默认位置同样按「未自定义」处理(设置里保持干净)。
|
|
45
|
+
* 服务端预建目录验证可写,失败给出反馈。 */
|
|
46
|
+
const save = async () => {
|
|
47
|
+
if (saving) return
|
|
48
|
+
setSaving(true)
|
|
49
|
+
setFail(false)
|
|
50
|
+
try {
|
|
51
|
+
const value = draft.trim()
|
|
52
|
+
const normalized = value === '' || value === defaultPath ? '' : value
|
|
53
|
+
const res = await fetch('/dsh-plugin-hub/settings', {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ logPath: normalized }),
|
|
57
|
+
cache: 'no-store',
|
|
58
|
+
})
|
|
59
|
+
if (!res.ok) {
|
|
60
|
+
setFail(true)
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
// 保存成功即关:主界面路径行随即刷新为新位置,本身就是反馈
|
|
64
|
+
onSaved(normalized)
|
|
65
|
+
onClose()
|
|
66
|
+
} catch {
|
|
67
|
+
setFail(true)
|
|
68
|
+
} finally {
|
|
69
|
+
setSaving(false)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 选择目录:系统对话框从默认位置打开,选中路径回填输入框(仍可手改)。 */
|
|
74
|
+
const browse = async () => {
|
|
75
|
+
if (browsing) return
|
|
76
|
+
setBrowsing(true)
|
|
77
|
+
setBrowseFail(false)
|
|
78
|
+
const picked = await chooseLogDir()
|
|
79
|
+
setBrowsing(false)
|
|
80
|
+
if (picked !== null) {
|
|
81
|
+
setDraft(picked)
|
|
82
|
+
setFail(false)
|
|
83
|
+
} else {
|
|
84
|
+
setBrowseFail(true)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return h('div', {
|
|
89
|
+
className: styles.overlay,
|
|
90
|
+
onClick: (e: { target: unknown; currentTarget: unknown }) => {
|
|
91
|
+
if (e.target === e.currentTarget) onClose()
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
h('div', { className: `${styles.modal} ${styles.modalWide}`, role: 'dialog', 'aria-modal': 'true' },
|
|
95
|
+
h('div', { className: styles.modalHead },
|
|
96
|
+
h('div', { className: styles.modalTitle }, t('logPathSettingTitle')),
|
|
97
|
+
h('button', {
|
|
98
|
+
className: styles.modalClose,
|
|
99
|
+
'aria-label': t('errorClose'),
|
|
100
|
+
onClick: onClose,
|
|
101
|
+
}, h(CloseIcon)),
|
|
102
|
+
),
|
|
103
|
+
h('div', { className: styles.modalBody },
|
|
104
|
+
h('div', { className: view.pathDialog },
|
|
105
|
+
h('div', { className: view.pathDialogDesc }, t('logPathSettingDesc')),
|
|
106
|
+
// 手输路径 + 系统目录选择器两条路并行
|
|
107
|
+
h('div', { className: view.pathDialogRow },
|
|
108
|
+
h('input', {
|
|
109
|
+
type: 'text',
|
|
110
|
+
className: view.pathDraft,
|
|
111
|
+
value: draft,
|
|
112
|
+
placeholder: t('logPathPlaceholder'),
|
|
113
|
+
spellCheck: false,
|
|
114
|
+
onChange: (e: { currentTarget: HTMLInputElement }) => {
|
|
115
|
+
setDraft(e.currentTarget.value)
|
|
116
|
+
setFail(false)
|
|
117
|
+
setBrowseFail(false)
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
120
|
+
h('button', {
|
|
121
|
+
type: 'button',
|
|
122
|
+
className: view.btn,
|
|
123
|
+
disabled: browsing,
|
|
124
|
+
title: t('logPathBrowse'),
|
|
125
|
+
onClick: () => void browse(),
|
|
126
|
+
}, browsing ? t('logPathSaving') : t('logPathBrowse')),
|
|
127
|
+
),
|
|
128
|
+
// 提示 + 「恢复默认」:提示左、动作右,一行内不挤
|
|
129
|
+
h('div', { className: view.pathDialogReset },
|
|
130
|
+
h('span', { className: view.pathDialogHint }, t('logPathResetHint')),
|
|
131
|
+
h('button', {
|
|
132
|
+
type: 'button',
|
|
133
|
+
className: view.linkBtn,
|
|
134
|
+
onClick: () => {
|
|
135
|
+
setDraft(defaultPath)
|
|
136
|
+
setFail(false)
|
|
137
|
+
setBrowseFail(false)
|
|
138
|
+
},
|
|
139
|
+
}, t('logPathReset')),
|
|
140
|
+
),
|
|
141
|
+
browseFail && h('div', { className: view.footFail }, t('logPathBrowseFail')),
|
|
142
|
+
fail && h('div', { className: view.footFail }, t('logPathSaveFail')),
|
|
143
|
+
h('div', { className: view.pathDialogFoot },
|
|
144
|
+
h('button', {
|
|
145
|
+
type: 'button',
|
|
146
|
+
className: view.btn,
|
|
147
|
+
disabled: saving,
|
|
148
|
+
onClick: onClose,
|
|
149
|
+
}, t('confirmCancel')),
|
|
150
|
+
h('button', {
|
|
151
|
+
type: 'button',
|
|
152
|
+
className: `${view.btn} ${view.btnPrimary}`,
|
|
153
|
+
disabled: saving,
|
|
154
|
+
onClick: () => void save(),
|
|
155
|
+
}, saving ? t('logPathSaving') : t('logPathSave')),
|
|
156
|
+
),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
}
|
|
@@ -1,23 +1,27 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* Notification-center dialog: a persistent log of every settled install /
|
|
3
7
|
* remove task — successes and failures alike.
|
|
4
8
|
*
|
|
5
|
-
* Records are written to localStorage at settle time (see
|
|
9
|
+
* Records are written to localStorage at settle time (see logic/failures.ts),
|
|
6
10
|
* so a result is never lost even when the dialog was dismissed or the user
|
|
7
11
|
* was away. Each entry carries a circular status badge (green check for
|
|
8
12
|
* success, red cross for failure) with white glyph and message text;
|
|
9
13
|
* failures keep their copy / fix / file-an-issue actions. Opened from the
|
|
10
|
-
* header entry button.
|
|
14
|
+
* header entry button (after the Settings tab).
|
|
11
15
|
*/
|
|
12
16
|
import { createElement as h } from 'react'
|
|
13
17
|
import type { MouseEvent } from 'react'
|
|
14
|
-
import styles from '
|
|
15
|
-
import type { EnvInfo, Translate } from '
|
|
16
|
-
import type { NotificationRecord } from '
|
|
17
|
-
import { classifyFailure } from '
|
|
18
|
-
import type { PendingRestart, QueueTask } from '
|
|
19
|
-
import { pluginIssueUrl, pluginSiteUrl } from '
|
|
20
|
-
import { CloseIcon } from '
|
|
18
|
+
import styles from '../../styles/Modal.module.css'
|
|
19
|
+
import type { EnvInfo, Translate } from '../../types.ts'
|
|
20
|
+
import type { NotificationRecord } from '../../logic/failures.ts'
|
|
21
|
+
import { classifyFailure, npmTooLowVersion } from '../../logic/failures.ts'
|
|
22
|
+
import type { PendingRestart, QueueTask } from '../../hooks/useTaskQueue.ts'
|
|
23
|
+
import { pluginIssueUrl, pluginSiteUrl } from '../../logic/urls.ts'
|
|
24
|
+
import { CloseIcon } from '../ui/icons.tsx'
|
|
21
25
|
|
|
22
26
|
/** 记录时间完整展示:YYYY-MM-DD HH:mm:ss(每条通知都带精确到秒的时间戳)。 */
|
|
23
27
|
function fmtTime(at: number): string {
|
|
@@ -51,7 +55,7 @@ function BadgeGlyph({ ok }: { ok: boolean }) {
|
|
|
51
55
|
}))
|
|
52
56
|
}
|
|
53
57
|
|
|
54
|
-
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, cancelTask, restarting, onRestart }: {
|
|
58
|
+
export function NotificationsModal({ records, tasks, pendingRestarts, t, env, onClose, onCopy, onClear, onRemove, onUpdate, cancelTask, restarting, onRestart }: {
|
|
55
59
|
records: NotificationRecord[]
|
|
56
60
|
/** 进行中的安装/卸载任务(实时进度,与队列弹窗同一数据源) */
|
|
57
61
|
tasks: QueueTask[]
|
|
@@ -64,6 +68,8 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
64
68
|
onClear: () => void
|
|
65
69
|
/** 删除单条通知记录 */
|
|
66
70
|
onRemove: (id: number) => void
|
|
71
|
+
/** 更新提醒通知点击:跳去该插件的更新确认弹窗 */
|
|
72
|
+
onUpdate: (repo: string) => void
|
|
67
73
|
cancelTask: (id: number) => void
|
|
68
74
|
restarting: boolean
|
|
69
75
|
onRestart: () => void
|
|
@@ -156,9 +162,14 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
156
162
|
? h('div', { className: styles.failEmpty }, t('notificationsEmpty'))
|
|
157
163
|
: h('div', { className: styles.noticeList },
|
|
158
164
|
records.map((r) => {
|
|
165
|
+
const isUpdate = r.kind === 'update'
|
|
159
166
|
return h('div', {
|
|
160
167
|
key: r.id,
|
|
161
|
-
|
|
168
|
+
// 更新提醒:整行可点击直达更新确认弹窗;install/uninstall 记录维持原交互
|
|
169
|
+
className: r.ok
|
|
170
|
+
? `${styles.noticeRow} ${styles.noticeRowOk}${isUpdate ? ` ${styles.noticeRowUpdate}` : ''}`
|
|
171
|
+
: styles.noticeRow,
|
|
172
|
+
...(isUpdate ? { onClick: () => onUpdate(r.repo), title: t('updateNoticeGo') } : {}),
|
|
162
173
|
},
|
|
163
174
|
h('div', { className: styles.noticeRowMain },
|
|
164
175
|
h('div', {
|
|
@@ -169,17 +180,23 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
169
180
|
h('span', {
|
|
170
181
|
className: r.ok ? styles.noticeTextOk : styles.noticeTextFail,
|
|
171
182
|
}, r.ok
|
|
172
|
-
? (r.kind === 'install' ? t('installDone') : t('uninstallDone'))
|
|
183
|
+
? (isUpdate ? t('updateNoticeTitle') : (r.kind === 'install' ? t('installDone') : t('uninstallDone')))
|
|
173
184
|
: (r.kind === 'install' ? t('errorTitleInstall') : t('errorTitleUninstall'))),
|
|
185
|
+
// 更新提醒:新版本号紧随标题展示;repo 为纯文本(整行点击即去更新,不跳官网外链)
|
|
186
|
+
isUpdate && r.version
|
|
187
|
+
? h('span', { className: styles.noticeVersion, title: r.version }, `v${r.version}`)
|
|
188
|
+
: null,
|
|
174
189
|
r.repo
|
|
175
|
-
?
|
|
176
|
-
className: styles.failRepo,
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
? (isUpdate
|
|
191
|
+
? h('span', { className: styles.failRepo, title: r.repo }, r.repo)
|
|
192
|
+
: h('a', {
|
|
193
|
+
className: styles.failRepo,
|
|
194
|
+
// 跳转到官网详情页(含插件收录信息),不在通知弹窗直接跳 GitHub
|
|
195
|
+
href: pluginSiteUrl(r.repo),
|
|
196
|
+
target: '_blank',
|
|
197
|
+
rel: 'noopener noreferrer',
|
|
198
|
+
title: r.repo,
|
|
199
|
+
}, r.repo))
|
|
183
200
|
: null,
|
|
184
201
|
!r.ok && h('button', {
|
|
185
202
|
className: styles.failCopy,
|
|
@@ -193,6 +210,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
193
210
|
title: new Date(r.at).toLocaleString(),
|
|
194
211
|
}, fmtTime(r.at))
|
|
195
212
|
: null,
|
|
213
|
+
// 更新提醒:行尾「去更新」按钮直达更新确认弹窗(整行也可点击)
|
|
214
|
+
isUpdate
|
|
215
|
+
? h('button', {
|
|
216
|
+
className: styles.noticeUpdateGo,
|
|
217
|
+
onClick: (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onUpdate(r.repo) },
|
|
218
|
+
}, t('updateNoticeGo'))
|
|
219
|
+
: null,
|
|
196
220
|
// 每条通知右侧的删除按钮:单独移除这一条
|
|
197
221
|
h('button', {
|
|
198
222
|
className: styles.noticeRemove,
|
|
@@ -205,6 +229,13 @@ export function NotificationsModal({ records, tasks, pendingRestarts, t, env, on
|
|
|
205
229
|
// 卡片不展示错误日志预览,完整日志靠「复制完整日志」按钮带走,避免每条卡片被日志撑高
|
|
206
230
|
!r.ok && (() => {
|
|
207
231
|
const kind = classifyFailure(r.message)
|
|
232
|
+
if (kind === 'npmTooOld') {
|
|
233
|
+
// npm 内部崩溃(edgesOut)+ 本机 npm 版本过低:npm 自身已知缺陷,不是插件问题 ——
|
|
234
|
+
// 直接给升级指引,不引导提 Issue
|
|
235
|
+
const v = npmTooLowVersion(r.message)
|
|
236
|
+
return h('div', { className: styles.failPrepareHint },
|
|
237
|
+
t(v ? 'failNpmTooLowV' : 'failNpmTooLow', v ? { v } : undefined))
|
|
238
|
+
}
|
|
208
239
|
if (kind === 'pluginPrepare' || kind === 'pnpmIgnoredBuild') {
|
|
209
240
|
// [packaging](预检/装后校验拦截:git 分发缺产物)与
|
|
210
241
|
// prepare 构建脚本实际执行失败 / 原生依赖构建被 pnpm 拦截:都是插件打包分发问题 —— 先说明原因,按钮在下方提交
|
|
@@ -1,12 +1,16 @@
|
|
|
1
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
|
+
*
|
|
2
6
|
* Inline progress block shared by the install and uninstall dialogs:
|
|
3
7
|
* a real percentage bar (server-estimated, client-polled). No live terminal
|
|
4
8
|
* window — pnpm emits nothing during git clone/build, so a log pane would be
|
|
5
9
|
* empty for most of the run; failures surface their full output in ErrorModal.
|
|
6
10
|
*/
|
|
7
11
|
import { createElement as h } from 'react'
|
|
8
|
-
import styles from '
|
|
9
|
-
import type { TaskState } from '
|
|
12
|
+
import styles from '../../styles/Modal.module.css'
|
|
13
|
+
import type { TaskState } from '../../types.ts'
|
|
10
14
|
|
|
11
15
|
export function ProgressView({ task }: { task: TaskState }) {
|
|
12
16
|
return h('div', { className: styles.progress },
|