dsh-config-manager 0.1.13 → 0.1.15
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.md +1 -0
- package/README.zh-CN.md +1 -0
- package/lib/client.d.ts +76 -73
- package/lib/client.js +700 -697
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/ConfigManagerSection.tsx +33 -8
- package/src/client/client-types.ts +4 -0
- package/src/client/index.ts +5 -11
- package/src/client/locales.ts +2 -0
- package/src/client/run-store.test.ts +16 -0
- package/src/client/run-store.ts +15 -6
- package/src/client/sync/SyncSettingsView.tsx +9 -23
- package/src/client/sync/sync-api.ts +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-config-manager",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH Config Manager
|
|
3
|
+
"version": "0.1.15",
|
|
4
|
+
"description": "DSH Config Manager — backup / export / import / migrate DSH configuration (dual-face Cordis plugin: host engine + web UI). Manifest shape mirrors @linxin666/dsh-ssh@0.1.12.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"deepseek-harness",
|
|
@@ -16,6 +16,7 @@ import { runStore, type MainView } from './run-store.ts'
|
|
|
16
16
|
import { ExportView } from './export/ExportView.tsx'
|
|
17
17
|
import { ImportWizardView } from './import/ImportWizardView.tsx'
|
|
18
18
|
import { SnapshotsPanel } from './snapshots/SnapshotsPanel.tsx'
|
|
19
|
+
import { SyncSettingsView } from './sync/SyncSettingsView.tsx'
|
|
19
20
|
import css from './config-manager.module.css'
|
|
20
21
|
|
|
21
22
|
export type ConfigManagerSectionProps =
|
|
@@ -24,14 +25,15 @@ export type ConfigManagerSectionProps =
|
|
|
24
25
|
& { t: TranslateNS<'config-manager'> }
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
* 设置页容器:Export / Import / Snapshots
|
|
28
|
-
* Export/Import 状态在模块级 store(切 tab
|
|
28
|
+
* 设置页容器:Export / Import / Snapshots / Sync 四视图切换。
|
|
29
|
+
* Export/Import 状态在模块级 store(切 tab 不丢失);快照恢复与远程同步为低频显式
|
|
29
30
|
* 操作,其状态组件内自持(local state,不进 sessionStorage)。
|
|
30
31
|
*/
|
|
31
|
-
export function ConfigManagerSection({ api, t }: ConfigManagerSectionProps) {
|
|
32
|
+
export function ConfigManagerSection({ api, syncApi, syncT, t }: ConfigManagerSectionProps) {
|
|
32
33
|
const state = useSyncExternalStore(runStore.subscribe, runStore.getSnapshot)
|
|
33
34
|
const view = state.view
|
|
34
35
|
const [snapshotsOpen, setSnapshotsOpen] = useState(false)
|
|
36
|
+
const [syncOpen, setSyncOpen] = useState(false)
|
|
35
37
|
|
|
36
38
|
// m2-resume:挂载时重新订阅进行中的 run(刷新 / 重开面板后服务端继续执行,
|
|
37
39
|
// 这里经 /runs 找回活跃 runId 再轮询 /progress);卸载时停止轮询,重开再订阅。
|
|
@@ -44,10 +46,21 @@ export function ConfigManagerSection({ api, t }: ConfigManagerSectionProps) {
|
|
|
44
46
|
|
|
45
47
|
const setView = (next: MainView): void => {
|
|
46
48
|
setSnapshotsOpen(false)
|
|
49
|
+
setSyncOpen(false)
|
|
47
50
|
runStore.patch({ view: next })
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
const
|
|
53
|
+
const openSnapshots = (): void => {
|
|
54
|
+
setSnapshotsOpen(true)
|
|
55
|
+
setSyncOpen(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const openSync = (): void => {
|
|
59
|
+
setSyncOpen(true)
|
|
60
|
+
setSnapshotsOpen(false)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const activeTab: MainView | 'snapshots' | 'sync' = syncOpen ? 'sync' : snapshotsOpen ? 'snapshots' : view
|
|
51
64
|
|
|
52
65
|
return (
|
|
53
66
|
<div className={css.section}>
|
|
@@ -79,16 +92,28 @@ export function ConfigManagerSection({ api, t }: ConfigManagerSectionProps) {
|
|
|
79
92
|
aria-selected={activeTab === 'snapshots'}
|
|
80
93
|
data-active={snapshotsOpen ? '' : undefined}
|
|
81
94
|
className={css.viewTab}
|
|
82
|
-
onClick={
|
|
95
|
+
onClick={openSnapshots}
|
|
83
96
|
>
|
|
84
97
|
{t('view.snapshots')}
|
|
85
98
|
</button>
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
role="tab"
|
|
102
|
+
aria-selected={activeTab === 'sync'}
|
|
103
|
+
data-active={syncOpen ? '' : undefined}
|
|
104
|
+
className={css.viewTab}
|
|
105
|
+
onClick={openSync}
|
|
106
|
+
>
|
|
107
|
+
{t('view.sync')}
|
|
108
|
+
</button>
|
|
86
109
|
</div>
|
|
87
110
|
</div>
|
|
88
111
|
<div className={css.sectionBody}>
|
|
89
|
-
{
|
|
90
|
-
? <
|
|
91
|
-
:
|
|
112
|
+
{syncOpen
|
|
113
|
+
? <SyncSettingsView api={syncApi} t={syncT} />
|
|
114
|
+
: snapshotsOpen
|
|
115
|
+
? <SnapshotsPanel api={api} t={t} />
|
|
116
|
+
: view === 'export' ? <ExportView api={api} t={t} /> : <ImportWizardView api={api} t={t} />}
|
|
92
117
|
</div>
|
|
93
118
|
</div>
|
|
94
119
|
)
|
|
@@ -18,4 +18,8 @@ export type { TranslateNS }
|
|
|
18
18
|
/** 本插件 Client 半的注入业务面:每个 settings.section 注册项都拿到同一个 api 实例 */
|
|
19
19
|
export interface ConfigManagerSectionInjected {
|
|
20
20
|
api: import('./api.ts').ConfigManagerApi
|
|
21
|
+
/** 远程同步 API(备份与迁移页第 4 个 tab 使用,主 section 注册时注入) */
|
|
22
|
+
syncApi: import('./sync/sync-api.ts').SyncApi
|
|
23
|
+
/** 远程同步 locale(config-manager-sync 命名空间,主 section 注册时注入) */
|
|
24
|
+
syncT: import('@deepseek-ai/dsh-client-ui-slots').TranslateNS<'config-manager-sync'>
|
|
21
25
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -66,22 +66,16 @@ export function apply(ctx: ClientContext): void {
|
|
|
66
66
|
const syncT = ctx.locale.bind(SYNC_NS)
|
|
67
67
|
const syncApi = new SyncApi()
|
|
68
68
|
|
|
69
|
+
// 单一 settings.section:备份与迁移页(内部 Export/Import/Snapshots/Sync 四 tab)。
|
|
70
|
+
// 远程同步不再注册独立设置页 —— 并入主 section 的 inject 面(syncApi/syncT),
|
|
71
|
+
// 由 ConfigManagerSection 渲染第 4 个 tab。避免第二个 settings.section 注册在
|
|
72
|
+
// 目标 DSH 渲染 section 列表时抛错导致整页空白。
|
|
69
73
|
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
70
74
|
name: 'settings.section',
|
|
71
75
|
id: 'config-manager',
|
|
72
76
|
order: 60,
|
|
73
77
|
label: () => t('section.label'),
|
|
74
78
|
locale: NS,
|
|
75
|
-
inject: () => ({ api }),
|
|
79
|
+
inject: () => ({ api, syncApi, syncT }),
|
|
76
80
|
}, ConfigManagerSection))
|
|
77
|
-
|
|
78
|
-
// m-sync-ui:远程同步独立设置页(不触碰 ConfigManagerSection 的 tab 容器)。
|
|
79
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register({
|
|
80
|
-
name: 'settings.section',
|
|
81
|
-
id: 'config-manager-sync',
|
|
82
|
-
order: 70,
|
|
83
|
-
label: () => syncT('section.label'),
|
|
84
|
-
locale: SYNC_NS,
|
|
85
|
-
inject: () => ({ api: syncApi }),
|
|
86
|
-
}, SyncSettingsView))
|
|
87
81
|
}
|
package/src/client/locales.ts
CHANGED
|
@@ -12,6 +12,7 @@ export const zh = {
|
|
|
12
12
|
'view.export': '导出备份',
|
|
13
13
|
'view.import': '导入恢复',
|
|
14
14
|
'view.snapshots': '快照恢复',
|
|
15
|
+
'view.sync': '远程同步',
|
|
15
16
|
// 公共
|
|
16
17
|
'common.close': '关闭',
|
|
17
18
|
'common.cancel': '取消',
|
|
@@ -128,6 +129,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
128
129
|
'view.export': 'Export',
|
|
129
130
|
'view.import': 'Import',
|
|
130
131
|
'view.snapshots': 'Snapshots & Restore',
|
|
132
|
+
'view.sync': 'Remote Sync',
|
|
131
133
|
'common.close': 'Close',
|
|
132
134
|
'common.cancel': 'Cancel',
|
|
133
135
|
'common.back': 'Back',
|
|
@@ -498,3 +498,19 @@ test('subscribe/notify: patch 触发监听器,退订后不再通知', () => {
|
|
|
498
498
|
store.patch({ view: 'import' })
|
|
499
499
|
assert.notEqual(store.getSnapshot(), before)
|
|
500
500
|
})
|
|
501
|
+
|
|
502
|
+
test('回归: subscribe/getSnapshot 以裸引用调用时 this 绑定实例(useSyncExternalStore 的调用方式)', () => {
|
|
503
|
+
const store = new RunStore({ storage: null })
|
|
504
|
+
// React 内部以无接收者方式调用 getSnapshot()/subscribe(listener) ——
|
|
505
|
+
// 若为原型方法则 this 为 undefined 直接崩溃(备份与迁移页空白根因)。
|
|
506
|
+
const bareGet = store.getSnapshot
|
|
507
|
+
const bareSub = store.subscribe
|
|
508
|
+
assert.equal(bareGet().view, 'export', '裸引用 getSnapshot 不崩(this 绑定实例)')
|
|
509
|
+
let notified = 0
|
|
510
|
+
const unsub = bareSub(() => { notified += 1 })
|
|
511
|
+
store.patch({ view: 'import' })
|
|
512
|
+
assert.equal(notified, 1, '裸引用 subscribe 不崩且能退订')
|
|
513
|
+
unsub()
|
|
514
|
+
store.patch({ view: 'export' })
|
|
515
|
+
assert.equal(notified, 1, '退订后不再通知')
|
|
516
|
+
})
|
package/src/client/run-store.ts
CHANGED
|
@@ -344,18 +344,27 @@ export class RunStore {
|
|
|
344
344
|
|
|
345
345
|
/* ------------------------------------------------------------- 订阅 */
|
|
346
346
|
|
|
347
|
-
/**
|
|
348
|
-
|
|
347
|
+
/**
|
|
348
|
+
* 订阅 store 变化(useSyncExternalStore 的 subscribe)。
|
|
349
|
+
*
|
|
350
|
+
* 必须为箭头函数类字段:React 以裸引用(`runStore.subscribe`)调用它,
|
|
351
|
+
* 无接收者,若为原型方法则 `this` 为 undefined,`this.listeners` 直接崩溃。
|
|
352
|
+
*/
|
|
353
|
+
subscribe = (listener: () => void): (() => void) => {
|
|
349
354
|
this.listeners.add(listener)
|
|
350
355
|
return () => {
|
|
351
356
|
this.listeners.delete(listener)
|
|
352
357
|
}
|
|
353
358
|
}
|
|
354
359
|
|
|
355
|
-
/**
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
360
|
+
/**
|
|
361
|
+
* 当前完整快照(引用稳定:仅在 patch 后替换,适合 useSyncExternalStore)。
|
|
362
|
+
*
|
|
363
|
+
* 必须为箭头函数类字段:React 以裸引用(`runStore.getSnapshot`)调用它,
|
|
364
|
+
* 无接收者,若为原型方法则 `this` 为 undefined,`this.state` 抛 TypeError,
|
|
365
|
+
* 导致整个 settings.section 槽位渲染崩溃(备份与迁移页空白)。
|
|
366
|
+
*/
|
|
367
|
+
getSnapshot = (): StoreState => this.state
|
|
359
368
|
|
|
360
369
|
private notify(): void {
|
|
361
370
|
for (const listener of this.listeners) listener()
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 远程同步面板(备份与迁移页的第 4 个 tab 内容)。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 独立设置页壳(sectionHeader/close/自身 tab)已移除 —— tab 容器由
|
|
5
|
+
* ConfigManagerSection 统一渲染,本组件只输出内容体:
|
|
5
6
|
* - 仓库配置表单:repoUrl(必填)+ 认证 token(可选,写入 DSH credentials 的提示)
|
|
6
7
|
* + gitBin(可选);
|
|
7
8
|
* - 私有仓库强制提示横幅(常驻);
|
|
@@ -16,7 +17,6 @@
|
|
|
16
17
|
*/
|
|
17
18
|
import { useEffect, useState } from 'react'
|
|
18
19
|
import type { ChangeEvent } from 'react'
|
|
19
|
-
import type { PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
20
20
|
import type { TranslateNS } from '../client-types.ts'
|
|
21
21
|
import type { SyncPullReport, SyncPushReport } from '../../sync/sync-engine.ts'
|
|
22
22
|
import { Badge, Banner, Button, Card, SectionTitle, Spinner } from '../common/ui.tsx'
|
|
@@ -29,15 +29,11 @@ import {
|
|
|
29
29
|
} from './sync-view.ts'
|
|
30
30
|
import css from '../config-manager.module.css'
|
|
31
31
|
|
|
32
|
-
export interface
|
|
32
|
+
export interface SyncSettingsViewProps {
|
|
33
33
|
api: SyncApi
|
|
34
|
+
t: TranslateNS<'config-manager-sync'>
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export type SyncSettingsViewProps =
|
|
37
|
-
& PropsRuntime<'settings.section'>
|
|
38
|
-
& SyncSettingsSectionInjected
|
|
39
|
-
& { t: TranslateNS<'config-manager-sync'> }
|
|
40
|
-
|
|
41
37
|
interface SyncUiState {
|
|
42
38
|
loading: boolean
|
|
43
39
|
loadError: string | null
|
|
@@ -65,7 +61,7 @@ const initial: SyncUiState = {
|
|
|
65
61
|
error: null,
|
|
66
62
|
}
|
|
67
63
|
|
|
68
|
-
export function SyncSettingsView({
|
|
64
|
+
export function SyncSettingsView({ api, t }: SyncSettingsViewProps) {
|
|
69
65
|
const [state, setState] = useState<SyncUiState>(initial)
|
|
70
66
|
const patch = (p: Partial<SyncUiState>): void => setState((s) => ({ ...s, ...p }))
|
|
71
67
|
|
|
@@ -120,16 +116,8 @@ export function SyncSettingsView({ close, api, t }: SyncSettingsViewProps) {
|
|
|
120
116
|
const pullView = pullReportView(state.pullReport)
|
|
121
117
|
|
|
122
118
|
return (
|
|
123
|
-
<div className={css.
|
|
124
|
-
<
|
|
125
|
-
<div className={css.viewTabs}>
|
|
126
|
-
<span role="tab" aria-selected="true" data-active className={css.viewTab}>{t('section.label')}</span>
|
|
127
|
-
</div>
|
|
128
|
-
<button type="button" className={css.iconButton} title={t('common.close')} aria-label={t('common.close')} onClick={close}>×</button>
|
|
129
|
-
</div>
|
|
130
|
-
<div className={css.sectionBody}>
|
|
131
|
-
<div className={css.viewBody}>
|
|
132
|
-
<SectionTitle title={t('section.label')} subtitle={t('section.description')} />
|
|
119
|
+
<div className={css.viewBody}>
|
|
120
|
+
<SectionTitle title={t('section.label')} subtitle={t('section.description')} />
|
|
133
121
|
|
|
134
122
|
{/* 私有仓库强制提示 */}
|
|
135
123
|
<Banner kind="warn">{PRIVATE_REPO_HINT}</Banner>
|
|
@@ -254,8 +242,6 @@ export function SyncSettingsView({ close, api, t }: SyncSettingsViewProps) {
|
|
|
254
242
|
{pullView.previewHint !== '' && <Banner kind="info">{pullView.previewHint}</Banner>}
|
|
255
243
|
</Card>
|
|
256
244
|
)}
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
245
|
</div>
|
|
260
246
|
)
|
|
261
|
-
}
|
|
247
|
+
}
|
|
@@ -112,7 +112,7 @@ async function postJson<T>(path: string, body: unknown): Promise<T> {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
/**
|
|
115
|
+
/** 远程同步浏览器半数据入口(备份与迁移页第 4 个 tab 的注入业务面) */
|
|
116
116
|
export class SyncApi {
|
|
117
117
|
/** 读取同步状态(配置 / 凭据 / 上次同步时间 / 分区数) */
|
|
118
118
|
async status(): Promise<SyncStatusResponse> {
|