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,172 @@
|
|
|
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
|
+
* System diagnostics list: one row per connectivity channel (npm registry /
|
|
7
|
+
* GitHub API / catalog site), each with a live status badge — 200 OK in
|
|
8
|
+
* green, Unreachable in red, Checking… while probing. The list runs
|
|
9
|
+
* automatically when the page opens; every row is a button that re-probes
|
|
10
|
+
* just that channel, so a click always yields visible feedback
|
|
11
|
+
* (row pulses → badge flips → latency refreshes).
|
|
12
|
+
*/
|
|
13
|
+
import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
|
|
14
|
+
import styles from '../../styles/DiagnosticsView.module.css'
|
|
15
|
+
import type { EnvInfo, Translate } from '../../types.ts'
|
|
16
|
+
import { PLUGIN_VERSION } from '../../logic/constants.ts'
|
|
17
|
+
|
|
18
|
+
type ProbeKey = 'npm' | 'github' | 'catalog'
|
|
19
|
+
|
|
20
|
+
/** 系统版本文本:提交 Issue 时粘贴到正文,方便作者复现。 */
|
|
21
|
+
function formatEnv(env: EnvInfo): string {
|
|
22
|
+
return [
|
|
23
|
+
`DSH-Plugin Hub ${PLUGIN_VERSION}`,
|
|
24
|
+
`DSH ${env.dshVersion ?? 'unknown'}`,
|
|
25
|
+
`Node ${env.nodeVersion}`,
|
|
26
|
+
`${env.platform} ${env.arch} (${env.release})`,
|
|
27
|
+
`Profile ${env.profile}`,
|
|
28
|
+
env.dshHome ? `Home ${env.dshHome}` : '',
|
|
29
|
+
].filter(Boolean).join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RowState {
|
|
33
|
+
key: ProbeKey
|
|
34
|
+
/** 目标地址展示(registry 地址 / github.com / dsh-plugin.org) */
|
|
35
|
+
display: string
|
|
36
|
+
nameKey: string
|
|
37
|
+
status: 'idle' | 'running' | 'ok' | 'fail'
|
|
38
|
+
ms: number | null
|
|
39
|
+
statusCode: number | null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const INITIAL_ROWS: RowState[] = [
|
|
43
|
+
{ key: 'npm', display: 'registry.npmjs.org', nameKey: 'diagNpm', status: 'idle', ms: null, statusCode: null },
|
|
44
|
+
{ key: 'github', display: 'github.com', nameKey: 'diagGithub', status: 'idle', ms: null, statusCode: null },
|
|
45
|
+
{ key: 'catalog', display: 'api.dsh-plugin.org', nameKey: 'diagCatalog', status: 'idle', ms: null, statusCode: null },
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
export function DiagnosticsView({ t, env, onCopy }: {
|
|
49
|
+
t: Translate
|
|
50
|
+
env: EnvInfo | null
|
|
51
|
+
onCopy: (text: string) => void
|
|
52
|
+
}) {
|
|
53
|
+
const [rows, setRows] = useState<RowState[]>(INITIAL_ROWS)
|
|
54
|
+
/** 当前探测请求:重测/卸载时 abort 旧请求,避免乱序结果覆盖 */
|
|
55
|
+
const runningRef = useRef<AbortController | null>(null)
|
|
56
|
+
|
|
57
|
+
const patchRows = (key: ProbeKey | undefined, patch: Partial<RowState>) => {
|
|
58
|
+
setRows((prev) => prev.map((r) => (key === undefined || r.key === key ? { ...r, ...patch } : r)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 探测:key 缺省 = 全量串行;传 key = 只重测该通道 */
|
|
62
|
+
const run = useCallback((key?: ProbeKey) => {
|
|
63
|
+
runningRef.current?.abort()
|
|
64
|
+
const controller = new AbortController()
|
|
65
|
+
runningRef.current = controller
|
|
66
|
+
patchRows(key, { status: 'running', ms: null, statusCode: null })
|
|
67
|
+
|
|
68
|
+
void (async () => {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch('/dsh-plugin-hub/diagnostics', {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { 'content-type': 'application/json' },
|
|
73
|
+
body: key === undefined ? '{}' : JSON.stringify({ key }),
|
|
74
|
+
cache: 'no-store',
|
|
75
|
+
signal: controller.signal,
|
|
76
|
+
})
|
|
77
|
+
if (!res.ok || res.body === null) throw new Error(`http ${res.status}`)
|
|
78
|
+
const reader = res.body.getReader()
|
|
79
|
+
const decoder = new TextDecoder()
|
|
80
|
+
let buf = ''
|
|
81
|
+
for (;;) {
|
|
82
|
+
const { done, value } = await reader.read()
|
|
83
|
+
if (done) break
|
|
84
|
+
buf += decoder.decode(value, { stream: true })
|
|
85
|
+
let nl = buf.indexOf('\n')
|
|
86
|
+
while (nl !== -1) {
|
|
87
|
+
const raw = buf.slice(0, nl).trim()
|
|
88
|
+
buf = buf.slice(nl + 1)
|
|
89
|
+
if (raw !== '') {
|
|
90
|
+
const ev = JSON.parse(raw) as { type?: string; key?: string; ms?: number | null; status?: number | null }
|
|
91
|
+
if (ev.type === 'ok' || ev.type === 'fail') {
|
|
92
|
+
patchRows(ev.key as ProbeKey, {
|
|
93
|
+
status: ev.type === 'ok' ? 'ok' : 'fail',
|
|
94
|
+
ms: typeof ev.ms === 'number' ? ev.ms : null,
|
|
95
|
+
statusCode: typeof ev.status === 'number' ? ev.status : null,
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
nl = buf.indexOf('\n')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// 连接本地服务失败:该项标记失败(被新请求 abort 的旧请求直接忽略)
|
|
104
|
+
if (controller.signal.aborted) return
|
|
105
|
+
patchRows(key, { status: 'fail', ms: null, statusCode: null })
|
|
106
|
+
}
|
|
107
|
+
})()
|
|
108
|
+
}, [])
|
|
109
|
+
|
|
110
|
+
// 进入页面自动全量探测一轮;卸载时中断
|
|
111
|
+
useEffect(() => {
|
|
112
|
+
run()
|
|
113
|
+
return () => runningRef.current?.abort()
|
|
114
|
+
}, [run])
|
|
115
|
+
|
|
116
|
+
const anyRunning = rows.some((r) => r.status === 'running')
|
|
117
|
+
const allOk = !anyRunning && rows.every((r) => r.status === 'ok')
|
|
118
|
+
const anyFail = !anyRunning && rows.some((r) => r.status === 'fail')
|
|
119
|
+
|
|
120
|
+
return h('div', { className: styles.panel },
|
|
121
|
+
// 最顶部:系统版本 —— 复制当前宿主版本与系统信息,提 Issue 时粘贴到正文便于复现
|
|
122
|
+
h('div', { className: styles.envRow },
|
|
123
|
+
h('div', { className: styles.envLabel },
|
|
124
|
+
h('div', { className: styles.envTitle }, t('settingsEnvSnapshot')),
|
|
125
|
+
h('div', { className: styles.envDesc }, t('settingsEnvSnapshotDesc')),
|
|
126
|
+
),
|
|
127
|
+
h('button', {
|
|
128
|
+
type: 'button',
|
|
129
|
+
className: styles.envCopyBtn,
|
|
130
|
+
disabled: env === null,
|
|
131
|
+
onClick: () => { if (env) onCopy(formatEnv(env)) },
|
|
132
|
+
}, t('settingsEnvCopy')),
|
|
133
|
+
),
|
|
134
|
+
// 头部:右侧「立即诊断」按钮,点击全量重测所有通道
|
|
135
|
+
h('div', { className: styles.head },
|
|
136
|
+
h('span', { className: styles.headHint }, t('diagHeadHint', { n: rows.length })),
|
|
137
|
+
h('button', {
|
|
138
|
+
type: 'button',
|
|
139
|
+
className: styles.runBtn,
|
|
140
|
+
disabled: anyRunning,
|
|
141
|
+
onClick: () => run(),
|
|
142
|
+
}, t('diagRunAll')),
|
|
143
|
+
),
|
|
144
|
+
rows.map((r) => {
|
|
145
|
+
const badge = r.status === 'running'
|
|
146
|
+
? h('span', { className: `${styles.badge} ${styles.badgeRunning}` }, t('diagChecking'))
|
|
147
|
+
: r.status === 'ok'
|
|
148
|
+
? h('span', { className: `${styles.badge} ${styles.badgeOk}` },
|
|
149
|
+
r.statusCode !== null ? `HTTP ${r.statusCode} ${t('diagOk')}` : t('diagOk'))
|
|
150
|
+
: r.status === 'fail'
|
|
151
|
+
? h('span', { className: `${styles.badge} ${styles.badgeFail}` }, t('settingsDiagFail'))
|
|
152
|
+
: h('span', { className: `${styles.badge} ${styles.badgeIdle}` }, t('diagIdle'))
|
|
153
|
+
return h('button', {
|
|
154
|
+
key: r.key,
|
|
155
|
+
type: 'button',
|
|
156
|
+
className: styles.row,
|
|
157
|
+
disabled: r.status === 'running',
|
|
158
|
+
onClick: () => run(r.key),
|
|
159
|
+
title: t('diagRecheck'),
|
|
160
|
+
},
|
|
161
|
+
h('span', { className: styles.name }, t(r.nameKey)),
|
|
162
|
+
h('span', { className: styles.display }, r.display),
|
|
163
|
+
h('span', { className: styles.meta }, r.status === 'ok' && r.ms !== null ? `${r.ms} ms` : ''),
|
|
164
|
+
badge,
|
|
165
|
+
)
|
|
166
|
+
}),
|
|
167
|
+
// 底部总览:全部通过 / 存在异常 / 检测中,汇总整体状态
|
|
168
|
+
h('div', {
|
|
169
|
+
className: `${styles.summary} ${anyRunning ? styles.summaryRunning : allOk ? styles.summaryOk : styles.summaryFail}`,
|
|
170
|
+
}, anyRunning ? t('diagSummaryRunning') : allOk ? t('diagSummaryOk') : t('diagSummaryFail')),
|
|
171
|
+
)
|
|
172
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
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
|
+
* Installed view: manages the plugins installed into the current profile.
|
|
7
|
+
* Follows the mainstream extension-manager pattern (VSCode Installed tab) —
|
|
8
|
+
* a searchable, sortable list of installed entries that mixes catalog
|
|
9
|
+
* plugins (full metadata) and custom command installs (outside the catalog,
|
|
10
|
+
* only runtime info, tagged with a custom badge). The toolbar carries a
|
|
11
|
+
* search box plus two segmented radio-button groups: source filter
|
|
12
|
+
* (all / catalog / custom) and sort (name / recently installed / stars / forks).
|
|
13
|
+
* Each row carries restart (when pending), update and uninstall actions
|
|
14
|
+
* and opens a detail modal with the full install metadata.
|
|
15
|
+
*
|
|
16
|
+
* Pure presentational: owns only local search/sort state; the InstalledItem
|
|
17
|
+
* model arrives from the catalog hook and actions bubble up via callbacks.
|
|
18
|
+
*/
|
|
19
|
+
import { createElement as h, useMemo, useState } from 'react'
|
|
20
|
+
import type { FormEvent, MouseEvent, ReactNode } from 'react'
|
|
21
|
+
import styles from '../../styles/InstalledView.module.css'
|
|
22
|
+
import type { LocaleId, Translate } from '../../types.ts'
|
|
23
|
+
import type { InstalledItem } from '../../logic/installed.ts'
|
|
24
|
+
import { CATEGORY_LABELS, categoryLabel } from '../../logic/constants.ts'
|
|
25
|
+
import { SortArrowIcon } from '../ui/icons.tsx'
|
|
26
|
+
|
|
27
|
+
const INSTALLED_SORTS = ['sortName', 'sortInstalledAt', 'sortStars', 'sortForks'] as const
|
|
28
|
+
type InstalledSortKey = (typeof INSTALLED_SORTS)[number]
|
|
29
|
+
|
|
30
|
+
/** 各排序的默认方向:名称按字母正序,最近安装 / Star / Fork 按数值倒序 */
|
|
31
|
+
const SORT_DEFAULT_DIR: Record<InstalledSortKey, 'asc' | 'desc'> = {
|
|
32
|
+
sortName: 'asc', sortInstalledAt: 'desc', sortStars: 'desc', sortForks: 'desc',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 已安装列表的来源筛选:全部 / 目录收录 / 自定义安装 */
|
|
36
|
+
type SourceFilter = 'all' | 'catalog' | 'custom'
|
|
37
|
+
|
|
38
|
+
/** 单选按钮组中的一个按钮(segmented control,与排序/来源筛选共用);
|
|
39
|
+
可带尾随图标(排序按钮激活时显示当前方向箭头) */
|
|
40
|
+
function SegBtn({ active, onClick, label, icon }: {
|
|
41
|
+
active: boolean
|
|
42
|
+
onClick: () => void
|
|
43
|
+
label: string
|
|
44
|
+
icon?: ReactNode
|
|
45
|
+
}) {
|
|
46
|
+
return h('button', {
|
|
47
|
+
type: 'button',
|
|
48
|
+
className: active ? `${styles.segBtn} ${styles.segBtnActive}` : styles.segBtn,
|
|
49
|
+
'aria-pressed': active,
|
|
50
|
+
onClick,
|
|
51
|
+
}, label, icon)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function InstalledRow({ item, t, langKey, canReveal, revealLabel, onOpenDetail, onReveal, onUpdate, onUninstall, onRestart }: {
|
|
55
|
+
item: InstalledItem
|
|
56
|
+
t: Translate
|
|
57
|
+
langKey: LocaleId
|
|
58
|
+
/** 系统文件管理器可定位安装目录的平台(macOS / Linux):行内显示打开目录按钮 */
|
|
59
|
+
canReveal: boolean
|
|
60
|
+
/** 打开目录按钮文案(macOS = 在 Finder 中显示,Linux = 在文件夹中显示) */
|
|
61
|
+
revealLabel: string
|
|
62
|
+
onOpenDetail: (item: InstalledItem) => void
|
|
63
|
+
/** 在系统文件管理器里定位安装目录(macOS Finder / Linux 文件管理器) */
|
|
64
|
+
onReveal: (item: InstalledItem) => void
|
|
65
|
+
onUpdate: (item: InstalledItem) => void
|
|
66
|
+
onUninstall: (item: InstalledItem) => void
|
|
67
|
+
/** 重启宿主:仅待重启(装完未挂载)条目展示,重启后该插件才生效 */
|
|
68
|
+
onRestart: () => void
|
|
69
|
+
}) {
|
|
70
|
+
const name = item.plugin?.displayName ?? item.name
|
|
71
|
+
const repo = item.repo
|
|
72
|
+
const category = item.plugin?.category
|
|
73
|
+
const desc = item.plugin?.description
|
|
74
|
+
// 命令行安装进来的插件没有目录元数据(plugin === null):列表里打「自定义安装」标签区分
|
|
75
|
+
const isCustom = item.plugin === null
|
|
76
|
+
return h('li', {
|
|
77
|
+
className: styles.row,
|
|
78
|
+
onClick: () => onOpenDetail(item),
|
|
79
|
+
role: 'button',
|
|
80
|
+
tabIndex: 0,
|
|
81
|
+
title: t('detail'),
|
|
82
|
+
onKeyDown: (e: KeyboardEvent) => {
|
|
83
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
84
|
+
e.preventDefault()
|
|
85
|
+
onOpenDetail(item)
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
// 第一行:状态点 + 名称/版本/更新徽标 + 行尾来源标签列 + 右侧固定操作按钮组
|
|
90
|
+
h('div', { className: styles.rowTitleLine },
|
|
91
|
+
// 运行状态点:绿 = 已加载进 loader;橙 = 待重启(装完未挂载的 dsh 插件);
|
|
92
|
+
// 灰 = 非 dsh 插件(如官方示例项目,宿主不加载,无需重启)
|
|
93
|
+
h('span', {
|
|
94
|
+
className: item.loaded ? styles.statusDot
|
|
95
|
+
: item.dshCapable ? `${styles.statusDot} ${styles.statusPending}`
|
|
96
|
+
: `${styles.statusDot} ${styles.statusInactive}`,
|
|
97
|
+
title: item.loaded ? t('statusRunning')
|
|
98
|
+
: item.dshCapable ? t('statusPending')
|
|
99
|
+
: t('exampleHint'),
|
|
100
|
+
'aria-label': item.loaded ? t('statusRunning')
|
|
101
|
+
: item.dshCapable ? t('statusPending')
|
|
102
|
+
: t('exampleHint'),
|
|
103
|
+
}),
|
|
104
|
+
// 主信息:名称 + 已装版本 + 有更新徽标(flex 撑满,超长省略)
|
|
105
|
+
h('div', { className: styles.rowMain },
|
|
106
|
+
h('span', { className: styles.rowTitle }, name),
|
|
107
|
+
item.installedVersion
|
|
108
|
+
? h('span', { className: styles.versionBadge, title: t('installedVersionLabel') }, item.installedVersion)
|
|
109
|
+
: null,
|
|
110
|
+
item.hasUpdate
|
|
111
|
+
? h('span', { className: styles.updateBadge, title: t('updateAvailableHint') }, t('updateAvailable'))
|
|
112
|
+
: null,
|
|
113
|
+
),
|
|
114
|
+
// 来源标签列(行尾、操作按钮前,右对齐固定):目录插件 →「插件中心」(品牌色),
|
|
115
|
+
// 命令行安装 →「手动安装」(琥珀色);两词 4 字等宽,所有条目标签同列对齐
|
|
116
|
+
h('span', {
|
|
117
|
+
className: isCustom
|
|
118
|
+
? `${styles.rowSourceTag} ${styles.rowSourceTagManual}`
|
|
119
|
+
: `${styles.rowSourceTag} ${styles.rowSourceTagHub}`,
|
|
120
|
+
title: isCustom ? t('manualInstallHint') : t('hubInstallHint'),
|
|
121
|
+
}, isCustom ? t('manualInstall') : t('hubInstall')),
|
|
122
|
+
// 行操作:重启(dsh 插件待重启时)/ 详情 / 更新(有更新时)/ 卸载 —— 按钮点击不触发行打开详情
|
|
123
|
+
h('div', { className: styles.rowActions },
|
|
124
|
+
// 装完未挂载的 dsh 插件:给「重启」入口,点了宿主重启后插件即生效;非 dsh 插件不提示(重启无用)
|
|
125
|
+
!item.loaded && item.dshCapable
|
|
126
|
+
? h('button', {
|
|
127
|
+
className: styles.rowRestart,
|
|
128
|
+
type: 'button',
|
|
129
|
+
title: t('restartPendingHint'),
|
|
130
|
+
onClick: (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onRestart() },
|
|
131
|
+
}, t('restart'))
|
|
132
|
+
: null,
|
|
133
|
+
// 支持文件管理器定位的平台(macOS / Linux):行内该位改为打开安装目录按钮;
|
|
134
|
+
// 行点击本身即打开详情弹窗,故不支持的平台(如 Windows)不显示此按钮
|
|
135
|
+
canReveal
|
|
136
|
+
? h('button', {
|
|
137
|
+
className: styles.rowDetail,
|
|
138
|
+
type: 'button',
|
|
139
|
+
onClick: (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onReveal(item) },
|
|
140
|
+
}, revealLabel)
|
|
141
|
+
: null,
|
|
142
|
+
item.hasUpdate && item.plugin
|
|
143
|
+
? h('button', {
|
|
144
|
+
className: styles.rowUpdate,
|
|
145
|
+
type: 'button',
|
|
146
|
+
title: t('updateAvailableHint'),
|
|
147
|
+
onClick: (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onUpdate(item) },
|
|
148
|
+
}, t('update'))
|
|
149
|
+
: null,
|
|
150
|
+
h('button', {
|
|
151
|
+
className: styles.rowUninstall,
|
|
152
|
+
type: 'button',
|
|
153
|
+
onClick: (e: MouseEvent<HTMLButtonElement>) => { e.stopPropagation(); onUninstall(item) },
|
|
154
|
+
}, t('uninstall')),
|
|
155
|
+
),
|
|
156
|
+
),
|
|
157
|
+
// 示例项目徽标单独一行(非 dsh 插件):标签较长也能完整显示,不挤压名称行
|
|
158
|
+
!item.dshCapable
|
|
159
|
+
? h('div', { className: styles.exampleRow },
|
|
160
|
+
h('span', { className: styles.exampleBadge, title: t('exampleHint') }, t('exampleLabel')),
|
|
161
|
+
)
|
|
162
|
+
: null,
|
|
163
|
+
// 第二行:仓库 · 分类(与名称左对齐缩进,独立成行不再挤压名称)
|
|
164
|
+
repo || category
|
|
165
|
+
? h('div', { className: styles.rowMeta },
|
|
166
|
+
item.repo ? h('span', { className: styles.rowRepo }, item.repo) : null,
|
|
167
|
+
category ? h('span', { className: styles.rowCategory },
|
|
168
|
+
categoryLabel(CATEGORY_LABELS, category, langKey)) : null,
|
|
169
|
+
)
|
|
170
|
+
: null,
|
|
171
|
+
// 第三行(仅目录插件):简介单独一行展示,超长以省略号收尾
|
|
172
|
+
desc ? h('div', { className: styles.rowDesc, title: desc }, desc) : null,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** 行列表:单个已安装列表(自定义安装与目录插件混排,靠徽标区分)。 */
|
|
177
|
+
function RowList({ items, t, langKey, canReveal, revealLabel, onOpenDetail, onReveal, onUpdate, onUninstall, onRestart }: {
|
|
178
|
+
items: InstalledItem[]
|
|
179
|
+
t: Translate
|
|
180
|
+
langKey: LocaleId
|
|
181
|
+
canReveal: boolean
|
|
182
|
+
revealLabel: string
|
|
183
|
+
onOpenDetail: (item: InstalledItem) => void
|
|
184
|
+
onReveal: (item: InstalledItem) => void
|
|
185
|
+
onUpdate: (item: InstalledItem) => void
|
|
186
|
+
onUninstall: (item: InstalledItem) => void
|
|
187
|
+
onRestart: () => void
|
|
188
|
+
}) {
|
|
189
|
+
return h('ul', { className: styles.list },
|
|
190
|
+
items.map((item) => h(InstalledRow, {
|
|
191
|
+
key: item.name,
|
|
192
|
+
item,
|
|
193
|
+
t,
|
|
194
|
+
langKey,
|
|
195
|
+
canReveal,
|
|
196
|
+
revealLabel,
|
|
197
|
+
onOpenDetail,
|
|
198
|
+
onReveal,
|
|
199
|
+
onUpdate,
|
|
200
|
+
onUninstall,
|
|
201
|
+
onRestart,
|
|
202
|
+
})),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function InstalledView({ items, t, langKey, platform, onOpenDetail, onReveal, onUpdate, onUninstall, onRestart }: {
|
|
207
|
+
items: InstalledItem[]
|
|
208
|
+
t: Translate
|
|
209
|
+
langKey: LocaleId
|
|
210
|
+
/** 宿主系统平台(node process.platform):支持文件管理器定位(macOS / Linux)时行内显示打开目录按钮 */
|
|
211
|
+
platform: string
|
|
212
|
+
onOpenDetail: (item: InstalledItem) => void
|
|
213
|
+
onReveal: (item: InstalledItem) => void
|
|
214
|
+
/** 更新:仅目录插件且有更新时触发(自定义安装无目录信号可比) */
|
|
215
|
+
onUpdate: (item: InstalledItem) => void
|
|
216
|
+
onUninstall: (item: InstalledItem) => void
|
|
217
|
+
/** 重启宿主:待重启条目行内按钮触发(装完未挂载的插件重启后生效) */
|
|
218
|
+
onRestart: () => void
|
|
219
|
+
}) {
|
|
220
|
+
const [query, setQuery] = useState('')
|
|
221
|
+
/** 默认按「最近安装」排序(最新在前);点其他排序按钮用该排序的默认方向(Star/Fork 最多在前、名称正序) */
|
|
222
|
+
const [sort, setSort] = useState<InstalledSortKey>('sortInstalledAt')
|
|
223
|
+
/** 当前排序方向:点同一个排序按钮切换 正/倒序;点新排序用该排序的默认方向 */
|
|
224
|
+
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
|
|
225
|
+
/** 已安装列表的来源筛选:全部 / 目录收录 / 自定义安装(单选按钮组) */
|
|
226
|
+
const [sourceFilter, setSourceFilter] = useState<SourceFilter>('all')
|
|
227
|
+
|
|
228
|
+
/** 点击排序按钮:同一按钮切换正/倒序,新按钮用默认方向 */
|
|
229
|
+
function pickSort(key: InstalledSortKey) {
|
|
230
|
+
if (key === sort) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'))
|
|
231
|
+
else { setSort(key); setSortDir(SORT_DEFAULT_DIR[key]) }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 搜索 + 排序后的展示列表:搜索命中名称/包名/仓库/描述;排序支持名称/最近安装/Star/Fork
|
|
235
|
+
const list = useMemo(() => {
|
|
236
|
+
const q = query.trim().toLowerCase()
|
|
237
|
+
const filtered = items.filter((item) => {
|
|
238
|
+
if (!q) return true
|
|
239
|
+
return (item.plugin?.displayName ?? item.name).toLowerCase().includes(q)
|
|
240
|
+
|| item.name.toLowerCase().includes(q)
|
|
241
|
+
|| (item.repo ?? '').toLowerCase().includes(q)
|
|
242
|
+
|| (item.plugin?.description ?? '').toLowerCase().includes(q)
|
|
243
|
+
})
|
|
244
|
+
const dir = sortDir === 'asc' ? 1 : -1
|
|
245
|
+
return [...filtered].sort((a, b) => {
|
|
246
|
+
if (sort === 'sortName') return (a.plugin?.displayName ?? a.name).localeCompare(b.plugin?.displayName ?? b.name) * dir
|
|
247
|
+
// Star / Fork:目录插件按数量比较(正/倒序随 dir);自定义安装(无目录数据,stats 缺失)永远排最后
|
|
248
|
+
if (sort === 'sortStars' || sort === 'sortForks') {
|
|
249
|
+
const metric = sort === 'sortStars' ? 'stargazers_count' : 'forks_count'
|
|
250
|
+
const sa = a.plugin?.stats?.[metric]
|
|
251
|
+
const sb = b.plugin?.stats?.[metric]
|
|
252
|
+
if (sa == null && sb == null) return 0
|
|
253
|
+
if (sa == null) return 1
|
|
254
|
+
if (sb == null) return -1
|
|
255
|
+
return (sa - sb) * dir
|
|
256
|
+
}
|
|
257
|
+
// 最近安装:按安装时间比较;无安装时间记录的永远排最后
|
|
258
|
+
const av = a.installedAt ?? ''
|
|
259
|
+
const bv = b.installedAt ?? ''
|
|
260
|
+
if (av === bv) return 0
|
|
261
|
+
if (av === '') return 1
|
|
262
|
+
if (bv === '') return -1
|
|
263
|
+
return av.localeCompare(bv) * dir
|
|
264
|
+
})
|
|
265
|
+
}, [items, query, sort, sortDir])
|
|
266
|
+
|
|
267
|
+
const catalogItems = list.filter((item) => item.plugin !== null)
|
|
268
|
+
const customItems = list.filter((item) => item.plugin === null)
|
|
269
|
+
// 已安装列表:搜索 + 排序后,再按来源筛选(全部 / 目录收录 / 自定义安装)
|
|
270
|
+
const filtered = sourceFilter === 'all' ? list
|
|
271
|
+
: sourceFilter === 'catalog' ? catalogItems : customItems
|
|
272
|
+
|
|
273
|
+
// 行列表公共参数:macOS 文案特指 Finder,Windows / Linux 用通用「在文件夹中显示」
|
|
274
|
+
const rowListProps = {
|
|
275
|
+
t, langKey,
|
|
276
|
+
canReveal: platform === 'darwin' || platform === 'win32' || platform === 'linux',
|
|
277
|
+
revealLabel: platform === 'darwin' ? t('revealFolder') : t('openFolder'),
|
|
278
|
+
onOpenDetail, onReveal, onUpdate, onUninstall, onRestart,
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 工具栏:第一行搜索框撑满,第二行来源筛选按钮组 + 排序按钮组(单选按钮组形态)
|
|
282
|
+
const listToolbar = h('div', { className: styles.toolbar },
|
|
283
|
+
h('div', { className: styles.searchWrap },
|
|
284
|
+
h('input', {
|
|
285
|
+
className: styles.searchInput,
|
|
286
|
+
type: 'search',
|
|
287
|
+
placeholder: t('installedSearch'),
|
|
288
|
+
value: query,
|
|
289
|
+
spellCheck: false,
|
|
290
|
+
onInput: (e: FormEvent<HTMLInputElement>) =>
|
|
291
|
+
setQuery((e.target as HTMLInputElement).value),
|
|
292
|
+
}),
|
|
293
|
+
),
|
|
294
|
+
h('div', { className: styles.segRow },
|
|
295
|
+
h('span', { className: styles.segLabel }, t('filterByLabel')),
|
|
296
|
+
h('div', { className: styles.segGroup, role: 'radiogroup', 'aria-label': t('filterByLabel') },
|
|
297
|
+
SegBtn({ active: sourceFilter === 'all', onClick: () => setSourceFilter('all'), label: t('all') }),
|
|
298
|
+
SegBtn({ active: sourceFilter === 'catalog', onClick: () => setSourceFilter('catalog'), label: t('installedFilterCatalog') }),
|
|
299
|
+
SegBtn({ active: sourceFilter === 'custom', onClick: () => setSourceFilter('custom'), label: t('customLabel') }),
|
|
300
|
+
),
|
|
301
|
+
h('span', { className: styles.segLabel }, t('sortByLabel')),
|
|
302
|
+
h('div', { className: styles.segGroup, role: 'radiogroup', 'aria-label': t('sortAria') },
|
|
303
|
+
INSTALLED_SORTS.map((key) => SegBtn({
|
|
304
|
+
active: sort === key,
|
|
305
|
+
onClick: () => pickSort(key),
|
|
306
|
+
label: t(key),
|
|
307
|
+
icon: sort === key ? h(SortArrowIcon, { up: sortDir === 'asc' }) : null,
|
|
308
|
+
})),
|
|
309
|
+
),
|
|
310
|
+
),
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
return h('div', { className: styles.root },
|
|
314
|
+
listToolbar,
|
|
315
|
+
// 筛选后的列表;一个都没装 → 引导空态,搜索/筛选无结果 → 无结果空态
|
|
316
|
+
filtered.length === 0
|
|
317
|
+
? items.length === 0
|
|
318
|
+
? h('div', { className: styles.empty },
|
|
319
|
+
h('div', { className: styles.emptyTitle }, t('installedEmpty')),
|
|
320
|
+
h('div', { className: styles.emptyDesc }, t('installedEmptyDesc')),
|
|
321
|
+
)
|
|
322
|
+
: h('div', { className: styles.empty },
|
|
323
|
+
h('div', { className: styles.emptyTitle }, t('noResult')),
|
|
324
|
+
h('div', { className: styles.emptyDesc }, t('noResultDesc')),
|
|
325
|
+
)
|
|
326
|
+
: h(RowList, { items: filtered, ...rowListProps }),
|
|
327
|
+
)
|
|
328
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
* 日志文件真实路径(可一键在系统文件管理器中定位)+ 最近若干条预览。
|
|
8
|
+
* 完整阅读/筛选/分页在 LogsModal 内置查看器弹窗里完成 —— 无需借助任何
|
|
9
|
+
* 外部软件,点击即弹出自己的窗口加载本地 JSONL 日志。
|
|
10
|
+
*/
|
|
11
|
+
import { createElement as h, useCallback, useEffect, useRef, useState } from 'react'
|
|
12
|
+
import styles from '../../styles/LogsView.module.css'
|
|
13
|
+
import type { Translate } from '../../types.ts'
|
|
14
|
+
import { openLogFile } from '../../data/host.ts'
|
|
15
|
+
import { LogsModal, LogEntryRow, fmtLogTime, type LogEntryView } from '../modals/LogsModal.tsx'
|
|
16
|
+
import { LogsPathModal } from '../modals/LogsPathModal.tsx'
|
|
17
|
+
|
|
18
|
+
/** 设置页预览条数:只放最近一小段,给用户快速感观,完整内容进查看器。 */
|
|
19
|
+
const PREVIEW = 12
|
|
20
|
+
|
|
21
|
+
export function LogsView({ t, logPath, onLogPathSaved }: {
|
|
22
|
+
t: Translate
|
|
23
|
+
/** 设置里已保存的日志位置覆盖(空串 = 默认位置) */
|
|
24
|
+
logPath: string
|
|
25
|
+
/** 保存自定义日志位置(写入设置,服务端同时预建目录验证可写) */
|
|
26
|
+
onLogPathSaved: (value: string) => void
|
|
27
|
+
}) {
|
|
28
|
+
const [entries, setEntries] = useState<LogEntryView[]>([])
|
|
29
|
+
const [path, setPath] = useState('')
|
|
30
|
+
// 真正默认位置(~/.dsh/profiles/<profile>/hub.log,跨机器/平台动态):弹窗「恢复默认」用
|
|
31
|
+
const [defaultPath, setDefaultPath] = useState('')
|
|
32
|
+
const [loading, setLoading] = useState(false)
|
|
33
|
+
const [error, setError] = useState(false)
|
|
34
|
+
const [open, setOpen] = useState(false)
|
|
35
|
+
const [opening, setOpening] = useState(false)
|
|
36
|
+
const [openFailed, setOpenFailed] = useState(false)
|
|
37
|
+
// 日志存放位置弹窗:点「修改」打开,手输或选目录
|
|
38
|
+
const [showPathModal, setShowPathModal] = useState(false)
|
|
39
|
+
const mountedRef = useRef(true)
|
|
40
|
+
|
|
41
|
+
const load = useCallback(async () => {
|
|
42
|
+
setLoading(true)
|
|
43
|
+
try {
|
|
44
|
+
const res = await fetch('/dsh-plugin-hub/logs', { cache: 'no-store' })
|
|
45
|
+
if (!res.ok) throw new Error(`http ${res.status}`)
|
|
46
|
+
const data = await res.json() as { entries: LogEntryView[]; path: string; defaultPath?: string }
|
|
47
|
+
if (mountedRef.current) {
|
|
48
|
+
setEntries(data.entries)
|
|
49
|
+
if (data.path !== '') setPath(data.path)
|
|
50
|
+
if (data.defaultPath !== undefined) setDefaultPath(data.defaultPath)
|
|
51
|
+
setError(false)
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
if (mountedRef.current) setError(true)
|
|
55
|
+
} finally {
|
|
56
|
+
if (mountedRef.current) setLoading(false)
|
|
57
|
+
}
|
|
58
|
+
}, [])
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
mountedRef.current = true
|
|
62
|
+
void load()
|
|
63
|
+
return () => { mountedRef.current = false }
|
|
64
|
+
}, [load])
|
|
65
|
+
|
|
66
|
+
/** 导出:拼纯文本 → Blob → 触发浏览器下载 .log 文件 */
|
|
67
|
+
const exportFile = () => {
|
|
68
|
+
const text = entries.map((e) => `[${fmtLogTime(e.at)}] [${e.level}] [${e.category}] [${e.event}] ${e.message}`).join('\n')
|
|
69
|
+
if (text === '') return
|
|
70
|
+
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' })
|
|
71
|
+
const url = URL.createObjectURL(blob)
|
|
72
|
+
const a = document.createElement('a')
|
|
73
|
+
a.href = url
|
|
74
|
+
a.download = 'dsh-plugin-hub-log.log'
|
|
75
|
+
document.body.appendChild(a)
|
|
76
|
+
a.click()
|
|
77
|
+
a.remove()
|
|
78
|
+
URL.revokeObjectURL(url)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 在系统文件管理器中定位日志文件。 */
|
|
82
|
+
const openFile = async () => {
|
|
83
|
+
if (opening) return
|
|
84
|
+
setOpening(true)
|
|
85
|
+
setOpenFailed(false)
|
|
86
|
+
const ok = await openLogFile()
|
|
87
|
+
setOpening(false)
|
|
88
|
+
if (!ok) setOpenFailed(true)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const preview = entries.slice(0, PREVIEW)
|
|
92
|
+
return h('div', { className: styles.panel },
|
|
93
|
+
h('div', { className: styles.head },
|
|
94
|
+
h('span', { className: styles.headHint }, t('logsHeadHint')),
|
|
95
|
+
h('div', { className: styles.actions },
|
|
96
|
+
h('button', {
|
|
97
|
+
type: 'button',
|
|
98
|
+
className: `${styles.btn} ${styles.btnPrimary}`,
|
|
99
|
+
onClick: () => setOpen(true),
|
|
100
|
+
}, t('logViewerOpen')),
|
|
101
|
+
h('button', { type: 'button', className: styles.btn, disabled: entries.length === 0, onClick: exportFile }, t('logsExport')),
|
|
102
|
+
h('button', { type: 'button', className: styles.btn, disabled: loading, onClick: () => void load() }, t('logsRefresh')),
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
// 日志存放位置:一行展示真实路径 + 「修改」弹窗编辑(手输 / 选目录)+ 一键定位文件
|
|
106
|
+
h('div', { className: styles.pathRow },
|
|
107
|
+
h('span', { className: styles.pathLabel }, t('logPathSettingTitle')),
|
|
108
|
+
h('span', { className: styles.pathText, title: path }, path === '' ? '…' : path),
|
|
109
|
+
openFailed && h('span', { className: styles.footFail }, t('logOpenFileFail')),
|
|
110
|
+
h('button', {
|
|
111
|
+
type: 'button',
|
|
112
|
+
className: styles.btn,
|
|
113
|
+
onClick: () => setShowPathModal(true),
|
|
114
|
+
}, t('logPathChange')),
|
|
115
|
+
h('button', {
|
|
116
|
+
type: 'button',
|
|
117
|
+
className: styles.btn,
|
|
118
|
+
disabled: opening || path === '',
|
|
119
|
+
title: t('logOpenFileTip'),
|
|
120
|
+
onClick: () => void openFile(),
|
|
121
|
+
}, t('logOpenFile')),
|
|
122
|
+
),
|
|
123
|
+
error
|
|
124
|
+
? h('div', { className: styles.empty }, t('logsLoadFail'))
|
|
125
|
+
: loading && entries.length === 0
|
|
126
|
+
? h('div', { className: styles.empty }, t('logsLoading'))
|
|
127
|
+
: entries.length === 0
|
|
128
|
+
? h('div', { className: styles.empty }, t('logsEmpty'))
|
|
129
|
+
: h('div', { className: styles.list },
|
|
130
|
+
preview.map((e, i) => h('button', {
|
|
131
|
+
key: `${e.at}-${i}`,
|
|
132
|
+
type: 'button',
|
|
133
|
+
className: styles.previewRow,
|
|
134
|
+
onClick: () => setOpen(true),
|
|
135
|
+
title: t('logViewerOpen'),
|
|
136
|
+
}, h(LogEntryRow, { e, t }))),
|
|
137
|
+
),
|
|
138
|
+
open && h(LogsModal, { t, onClose: () => setOpen(false) }),
|
|
139
|
+
showPathModal && h(LogsPathModal, {
|
|
140
|
+
t,
|
|
141
|
+
defaultPath,
|
|
142
|
+
currentPath: path,
|
|
143
|
+
onSaved: (v: string) => {
|
|
144
|
+
onLogPathSaved(v)
|
|
145
|
+
void load() // 刷新生效路径与最近日志(新位置若已写入则立即可见)
|
|
146
|
+
},
|
|
147
|
+
onClose: () => setShowPathModal(false),
|
|
148
|
+
}),
|
|
149
|
+
)
|
|
150
|
+
}
|