dsh-skill-hub 0.3.7 → 0.3.8
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 +40 -50
- package/README.zh.md +38 -48
- package/lib/client.js +2209 -1820
- package/lib/client.js.map +1 -1
- package/lib/index.js +653 -419
- package/lib/types/client/SkillHubSettingsCard.d.ts +1 -0
- package/lib/types/client/api.d.ts +12 -6
- package/lib/types/client/helpers.d.ts +0 -1
- package/lib/types/client/locales/common.d.ts +52 -0
- package/lib/types/client/locales/detail.d.ts +16 -0
- package/lib/types/client/locales/market.d.ts +73 -0
- package/lib/types/client/locales/settings.d.ts +41 -0
- package/lib/types/client/locales/skills.d.ts +65 -0
- package/lib/types/client/locales/sources.d.ts +35 -0
- package/lib/types/client/locales.d.ts +124 -118
- package/lib/types/client/panel/MarketSourceRow.d.ts +12 -0
- package/lib/types/client/panel/MarketView.d.ts +1 -0
- package/lib/types/client/panel/RepoScanCard.d.ts +10 -0
- package/lib/types/client/panel/hooks/shared.d.ts +72 -0
- package/lib/types/client/panel/hooks/useCatalogFlow.d.ts +62 -0
- package/lib/types/client/panel/hooks/useGroupFlow.d.ts +41 -0
- package/lib/types/client/panel/hooks/useMarketFlow.d.ts +73 -0
- package/lib/types/client/panel/hooks/useMetaFlow.d.ts +18 -0
- package/lib/types/client/panel/hooks/useSourceFlow.d.ts +44 -0
- package/lib/types/client/panel/market-format.d.ts +6 -0
- package/lib/types/client/panel/useSkillHub.d.ts +45 -73
- package/lib/types/client/settings-card.d.ts +20 -0
- package/lib/types/client/settings-form.d.ts +6 -0
- package/lib/types/protocol/api.d.ts +38 -0
- package/lib/types/protocol/catalog.d.ts +155 -0
- package/lib/types/protocol/config.d.ts +113 -0
- package/lib/types/protocol/groups.d.ts +95 -0
- package/lib/types/protocol/market.d.ts +94 -0
- package/lib/types/protocol/repo.d.ts +107 -0
- package/lib/types/protocol/sources.d.ts +139 -0
- package/lib/types/protocol/stats.d.ts +46 -0
- package/lib/types/protocol.d.ts +13 -743
- package/lib/types/repo/github-client.d.ts +42 -0
- package/lib/types/repo.d.ts +6 -17
- package/lib/types/routes/catalog.d.ts +7 -0
- package/lib/types/routes/config.d.ts +7 -0
- package/lib/types/routes/groups.d.ts +7 -0
- package/lib/types/routes/helpers.d.ts +145 -0
- package/lib/types/routes/market.d.ts +8 -0
- package/lib/types/routes/repo-import.d.ts +7 -0
- package/lib/types/routes/route-state.d.ts +55 -0
- package/lib/types/routes/sources.d.ts +8 -0
- package/lib/types/routes.d.ts +5 -34
- package/lib/types/stats.d.ts +3 -2
- package/lib/types/store/errors.d.ts +10 -0
- package/lib/types/store/migrate.d.ts +44 -0
- package/lib/types/store/paths.d.ts +32 -0
- package/lib/types/store/store.d.ts +133 -0
- package/lib/types/store.d.ts +7 -146
- package/package.json +28 -27
- package/src/client/SkillHubSettingsCard.tsx +17 -3
- package/src/client/api.ts +101 -195
- package/src/client/helpers.ts +10 -1
- package/src/client/locales/common.ts +102 -0
- package/src/client/locales/detail.ts +30 -0
- package/src/client/locales/market.ts +144 -0
- package/src/client/locales/settings.ts +80 -0
- package/src/client/locales/skills.ts +128 -0
- package/src/client/locales/sources.ts +68 -0
- package/src/client/locales.ts +26 -512
- package/src/client/panel/MarketSourceRow.tsx +108 -0
- package/src/client/panel/MarketView.tsx +12 -253
- package/src/client/panel/RepoScanCard.tsx +171 -0
- package/src/client/panel/hooks/shared.ts +81 -0
- package/src/client/panel/hooks/useCatalogFlow.ts +211 -0
- package/src/client/panel/hooks/useGroupFlow.ts +240 -0
- package/src/client/panel/hooks/useMarketFlow.ts +421 -0
- package/src/client/panel/hooks/useMetaFlow.ts +58 -0
- package/src/client/panel/hooks/useSourceFlow.ts +193 -0
- package/src/client/panel/market-format.ts +22 -0
- package/src/client/panel/useSkillHub.ts +141 -1032
- package/src/client/settings-card.tsx +51 -0
- package/src/client/settings-form.ts +18 -0
- package/src/index.ts +14 -4
- package/src/protocol/api.ts +38 -0
- package/src/protocol/catalog.ts +168 -0
- package/src/protocol/config.ts +145 -0
- package/src/protocol/groups.ts +106 -0
- package/src/protocol/market.ts +100 -0
- package/src/protocol/repo.ts +104 -0
- package/src/protocol/sources.ts +146 -0
- package/src/protocol/stats.ts +42 -0
- package/src/protocol.ts +83 -809
- package/src/repo/github-client.ts +131 -0
- package/src/repo.test.ts +24 -1
- package/src/repo.ts +50 -170
- package/src/routes/catalog.ts +309 -0
- package/src/routes/config.ts +119 -0
- package/src/routes/groups.ts +133 -0
- package/src/routes/helpers.ts +489 -0
- package/src/routes/market.ts +255 -0
- package/src/routes/repo-import.ts +267 -0
- package/src/routes/route-state.ts +89 -0
- package/src/routes/sources.ts +318 -0
- package/src/routes.test.ts +20 -0
- package/src/routes.ts +21 -1699
- package/src/stats.test.ts +58 -3
- package/src/stats.ts +45 -16
- package/src/store/errors.ts +14 -0
- package/src/store/migrate.ts +272 -0
- package/src/store/paths.ts +43 -0
- package/src/store/store.ts +532 -0
- package/src/store.test.ts +18 -0
- package/src/store.ts +7 -737
|
@@ -1,154 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useSkillHub —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* unconditionally at the top, so the panel
|
|
6
|
-
* and tag-editor views without violating
|
|
2
|
+
* useSkillHub — 面板的状态聚合根:只持有跨域共享的通知状态与纯视图状态,
|
|
3
|
+
* 各域状态与动作分散在 ./hooks/*,这里按依赖顺序组装(meta → 目录 →
|
|
4
|
+
* 分组 → 来源 → 市场),轮询与派生组装在此。返回形状与拆分前完全一致,
|
|
5
|
+
* 所有视图无需改动。All hooks run unconditionally at the top, so the panel
|
|
6
|
+
* may early-return for the detail and tag-editor views without violating
|
|
7
|
+
* the rules of hooks.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { useCallback, useEffect, useMemo,
|
|
10
|
-
import type {
|
|
11
|
-
CatalogResponse,
|
|
12
|
-
CatalogSkill,
|
|
13
|
-
DisabledSkill,
|
|
14
|
-
GroupsResponse,
|
|
15
|
-
HubConfig,
|
|
16
|
-
MarketCheckResponse,
|
|
17
|
-
MarketSourceRecord,
|
|
18
|
-
RepoDiscoverResponse,
|
|
19
|
-
RepoImportProgressResponse,
|
|
20
|
-
RepoImportResponse,
|
|
21
|
-
SkillDetail,
|
|
22
|
-
SkillTag,
|
|
23
|
-
SourceCheckResult,
|
|
24
|
-
SourcesResponse,
|
|
25
|
-
UpdateCheckResponse,
|
|
26
|
-
WritableRoot,
|
|
27
|
-
} from '../../protocol.ts'
|
|
10
|
+
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
28
11
|
import type { SkillHubApi } from '../api.ts'
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const SLOW_POLL_MS = 60_000
|
|
37
|
-
|
|
38
|
-
/** localStorage key of the last daily auto-check timestamp (survives sessions). */
|
|
39
|
-
const AUTO_CHECK_KEY = 'skill-hub.last-auto-check'
|
|
40
|
-
/** Daily auto-check interval: at most one network round per day. */
|
|
41
|
-
const AUTO_CHECK_INTERVAL_MS = 24 * 60 * 60_000
|
|
42
|
-
|
|
43
|
-
/** Whether the daily auto-check should run now (first visit or ≥24h since the last). */
|
|
44
|
-
function shouldAutoCheck(): boolean {
|
|
45
|
-
try {
|
|
46
|
-
const raw = localStorage.getItem(AUTO_CHECK_KEY)
|
|
47
|
-
const last = raw === null ? NaN : Number(raw)
|
|
48
|
-
if (!Number.isFinite(last)) return true
|
|
49
|
-
return Date.now() - last >= AUTO_CHECK_INTERVAL_MS
|
|
50
|
-
} catch {
|
|
51
|
-
// 无 localStorage(隐私模式等):每次打开都尝试,服务端 5 分钟节流兜底。
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/** Record that the daily auto-check ran. */
|
|
57
|
-
function markAutoChecked(): void {
|
|
58
|
-
try {
|
|
59
|
-
localStorage.setItem(AUTO_CHECK_KEY, String(Date.now()))
|
|
60
|
-
} catch {
|
|
61
|
-
// 无 localStorage:不记录,下次打开仍会尝试(服务端节流兜底)。
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** One-shot self-update check state. */
|
|
66
|
-
type UpdateState =
|
|
67
|
-
| { status: 'idle' }
|
|
68
|
-
| { status: 'checking' }
|
|
69
|
-
| { status: 'ready'; data: UpdateCheckResponse }
|
|
70
|
-
| { status: 'error'; message: string }
|
|
71
|
-
|
|
72
|
-
/** Repo scanner/import state (drives the market source preview). */
|
|
73
|
-
type RepoDiscoverState =
|
|
74
|
-
| { status: 'idle' }
|
|
75
|
-
| { status: 'scanning' }
|
|
76
|
-
| { status: 'ready'; data: RepoDiscoverResponse }
|
|
77
|
-
| { status: 'error'; message: string }
|
|
78
|
-
|
|
79
|
-
/** Market sources list state. */
|
|
80
|
-
type MarketState = { status: 'loading' | 'ready' | 'error'; repos: MarketSourceRecord[] }
|
|
81
|
-
|
|
82
|
-
/** One market source's update-check outcome. */
|
|
83
|
-
type MarketCheckResult = MarketCheckResponse['results'][number]
|
|
12
|
+
import { isProjectSource, PRIVATE_SOURCE } from '../grouping.ts'
|
|
13
|
+
import { useMetaFlow } from './hooks/useMetaFlow.ts'
|
|
14
|
+
import { useCatalogFlow } from './hooks/useCatalogFlow.ts'
|
|
15
|
+
import { useGroupFlow } from './hooks/useGroupFlow.ts'
|
|
16
|
+
import { useSourceFlow } from './hooks/useSourceFlow.ts'
|
|
17
|
+
import { useMarketFlow } from './hooks/useMarketFlow.ts'
|
|
18
|
+
import { markAutoChecked, shouldAutoCheck, type FlowNotices } from './hooks/shared.ts'
|
|
84
19
|
|
|
85
20
|
/** The hook's result: the panel's complete state + action surface. */
|
|
86
21
|
export type SkillHubState = ReturnType<typeof useSkillHub>
|
|
87
22
|
|
|
88
23
|
export function useSkillHub(api: SkillHubApi) {
|
|
89
|
-
|
|
90
|
-
|
|
24
|
+
// ------------------------------------------------------- shared notices
|
|
25
|
+
// 多域共用的报错/成功条幅与忙碌开关:setter 本身引用稳定,memo 常驻后
|
|
26
|
+
// 各域回调依赖它不会失稳。
|
|
91
27
|
const [loadError, setLoadError] = useState<string | null>(null)
|
|
92
28
|
/** Green success banner (create finished); shown outside the closing form. */
|
|
93
29
|
const [successBanner, setSuccessBanner] = useState<string | null>(null)
|
|
94
|
-
const [updateState, setUpdateState] = useState<UpdateState>({ status: 'idle' })
|
|
95
|
-
const [repoDiscoverState, setRepoDiscoverState] = useState<RepoDiscoverState>({ status: 'idle' })
|
|
96
|
-
/** Market repo currently being scanned (null when idle); per-row busy state. */
|
|
97
|
-
const [scanningRepo, setScanningRepo] = useState<string | null>(null)
|
|
98
|
-
const [repoSelected, setRepoSelected] = useState<ReadonlySet<string>>(new Set())
|
|
99
|
-
const [repoImporting, setRepoImporting] = useState(false)
|
|
100
|
-
const [repoResult, setRepoResult] = useState<RepoImportProgressResponse | null>(null)
|
|
101
|
-
const [importJobId, setImportJobId] = useState<string | null>(null)
|
|
102
|
-
const pollAbortRef = useRef<AbortController | null>(null)
|
|
103
|
-
const [search, setSearch] = useState('')
|
|
104
|
-
/** 工作区(项目)路径;空 = 只看用户级技能。 */
|
|
105
|
-
const [workspace, setWorkspace] = useState('')
|
|
106
|
-
const [detail, setDetail] = useState<SkillDetail | null>(null)
|
|
107
|
-
const [detailLoading, setDetailLoading] = useState(false)
|
|
108
|
-
const [busyNames, setBusyNames] = useState<ReadonlySet<string>>(new Set())
|
|
109
30
|
const [batchBusy, setBatchBusy] = useState(false)
|
|
110
|
-
const [
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
31
|
+
const [tagBusy, setTagBusy] = useState(false)
|
|
32
|
+
const shared = useMemo<FlowNotices>(() => ({
|
|
33
|
+
fail: (message: string) => setLoadError(message),
|
|
34
|
+
clearFail: () => setLoadError(null),
|
|
35
|
+
succeed: (message: string | null) => setSuccessBanner(message),
|
|
36
|
+
setTagBusy,
|
|
37
|
+
setBatchBusy,
|
|
38
|
+
}), [])
|
|
39
|
+
|
|
40
|
+
// ------------------------------------------------------------ view state
|
|
41
|
+
// 纯视图状态:不触发数据加载,只影响渲染。
|
|
118
42
|
const [tab, setTab] = useState<'sources' | 'scenes' | 'market'>('sources')
|
|
119
43
|
const [skillView, setSkillView] = useState<'flat' | 'groups'>('groups')
|
|
120
44
|
const [sourceFilter, setSourceFilter] = useState('all')
|
|
121
|
-
const [invocationFilter, setInvocationFilter] = useState<'all' | 'model' | 'user'>('all')
|
|
122
|
-
const [sortKey, setSortKey] = useState<SortKey>('name')
|
|
123
|
-
const [marketState, setMarketState] = useState<MarketState>({ status: 'loading', repos: [] })
|
|
124
|
-
const [marketCheck, setMarketCheck] = useState<Readonly<Record<string, MarketCheckResult>>>({})
|
|
125
|
-
const [branchChoice, setBranchChoice] = useState<BranchChoiceState | null>(null)
|
|
126
|
-
const [branchBusy, setBranchBusy] = useState(false)
|
|
127
|
-
const [marketSyncDialog, setMarketSyncDialog] = useState<MarketSyncDialogState | null>(null)
|
|
128
|
-
const [syncingMarket, setSyncingMarket] = useState<string | null>(null)
|
|
129
|
-
const [syncBusy, setSyncBusy] = useState(false)
|
|
130
|
-
const [newSourceName, setNewSourceName] = useState('')
|
|
131
|
-
const [groupsState, setGroupsState] = useState<GroupsResponse | null>(null)
|
|
132
|
-
const [sourcesState, setSourcesState] = useState<SourcesResponse | null>(null)
|
|
133
|
-
const [sourceCheck, setSourceCheck] = useState<Readonly<Record<string, SourceCheckResult>>>({})
|
|
134
|
-
const [checkingSource, setCheckingSource] = useState<string | null>(null)
|
|
135
|
-
const [syncingSource, setSyncingSource] = useState<string | null>(null)
|
|
136
|
-
const [conflictDialog, setConflictDialog] = useState<ConflictDialogState | null>(null)
|
|
137
|
-
const [confirmDialog, setConfirmDialog] = useState<ConfirmDialogState | null>(null)
|
|
138
|
-
const [deleteSkillDialog, setDeleteSkillDialog] = useState<string | null>(null)
|
|
139
|
-
const [deleteGroupDialog, setDeleteGroupDialog] = useState<{ name: string; skillNames: string[] } | null>(null)
|
|
140
|
-
const [confirmClearTrash, setConfirmClearTrash] = useState(false)
|
|
141
|
-
/** 「全部更新」确认对话框(市场 tab)。 */
|
|
142
|
-
const [updateAllDialog, setUpdateAllDialog] = useState(false)
|
|
143
|
-
const [editingTag, setEditingTag] = useState<SkillTag | null>(null)
|
|
144
|
-
const [editName, setEditName] = useState('')
|
|
145
|
-
const [membersDraft, setMembersDraft] = useState<ReadonlySet<string>>(new Set())
|
|
146
|
-
const [newTagName, setNewTagName] = useState('')
|
|
147
|
-
const [tagBusy, setTagBusy] = useState(false)
|
|
148
|
-
const [editSearch, setEditSearch] = useState('')
|
|
149
45
|
/** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。技能总数 >80 时首次加载自动折叠 personal。 */
|
|
150
46
|
const [collapsedGroups, setCollapsedGroups] = useState<ReadonlySet<string>>(new Set())
|
|
151
|
-
const autoCollapsedPersonal = useRef(false)
|
|
152
47
|
/** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
|
|
153
48
|
const [subdividedProjects, setSubdividedProjects] = useState<ReadonlySet<string>>(new Set())
|
|
154
49
|
const [showLegend, setShowLegend] = useState(false)
|
|
@@ -172,843 +67,64 @@ export function useSkillHub(api: SkillHubApi) {
|
|
|
172
67
|
})
|
|
173
68
|
}, [])
|
|
174
69
|
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
setCatalog(next)
|
|
179
|
-
if (!autoCollapsedPersonal.current && next.skills.length + next.disabled.length > 80) {
|
|
180
|
-
autoCollapsedPersonal.current = true
|
|
181
|
-
setCollapsedGroups((previous) => new Set(previous).add('uncategorized-source'))
|
|
182
|
-
}
|
|
183
|
-
setLoadError(null)
|
|
184
|
-
} catch (error) {
|
|
185
|
-
setLoadError(errorMessage(error))
|
|
186
|
-
} finally {
|
|
187
|
-
setLoading(false)
|
|
188
|
-
}
|
|
189
|
-
}, [api, workspace])
|
|
70
|
+
const collapsePersonal = useCallback((): void => {
|
|
71
|
+
setCollapsedGroups((previous) => new Set(previous).add('uncategorized-source'))
|
|
72
|
+
}, [])
|
|
190
73
|
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (result.available) setUses(new Map(result.stats.map((stat) => [stat.name, { count: stat.count, ...(stat.lastUsed !== undefined ? { lastUsed: stat.lastUsed } : {}) }])))
|
|
195
|
-
} catch {
|
|
196
|
-
// Invocation counts are best-effort; a stats failure must not disturb the catalog.
|
|
197
|
-
}
|
|
198
|
-
}, [api])
|
|
74
|
+
const clearSourceFilter = useCallback((): void => {
|
|
75
|
+
setSourceFilter('all')
|
|
76
|
+
}, [])
|
|
199
77
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
} catch {
|
|
204
|
-
// Dot colors are cosmetic; a config failure falls back to the defaults.
|
|
205
|
-
}
|
|
206
|
-
}, [api])
|
|
78
|
+
// ------------------------------------------------------------ meta first
|
|
79
|
+
// 更新检查/统计/配置只依赖 api,最先组装。
|
|
80
|
+
const meta = useMetaFlow(api)
|
|
207
81
|
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
setUpdateState({ status: 'checking' })
|
|
211
|
-
try {
|
|
212
|
-
setUpdateState({ status: 'ready', data: await api.updateCheck() })
|
|
213
|
-
} catch (error) {
|
|
214
|
-
setUpdateState({ status: 'error', message: errorMessage(error) })
|
|
215
|
-
}
|
|
216
|
-
}, [api])
|
|
82
|
+
// ---------------------------------------------------------------- catalog
|
|
83
|
+
const catalogFlow = useCatalogFlow(api, meta.uses, shared, collapsePersonal, clearSourceFilter)
|
|
217
84
|
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
try {
|
|
221
|
-
setGroupsState(await api.groups())
|
|
222
|
-
} catch (error) {
|
|
223
|
-
setLoadError(errorMessage(error))
|
|
224
|
-
}
|
|
225
|
-
}, [api])
|
|
85
|
+
// ----------------------------------------------------------------- groups
|
|
86
|
+
const groupFlow = useGroupFlow(api, shared, catalogFlow.batchToggleNames, catalogFlow.actionNames)
|
|
226
87
|
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
try {
|
|
230
|
-
setSourcesState(await api.sources())
|
|
231
|
-
} catch (error) {
|
|
232
|
-
setLoadError(errorMessage(error))
|
|
233
|
-
}
|
|
234
|
-
}, [api])
|
|
88
|
+
// ----------------------------------------------------------------- sources
|
|
89
|
+
const sourceFlow = useSourceFlow(api, shared, catalogFlow.load, groupFlow.loadGroups)
|
|
235
90
|
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
91
|
+
// ------------------------------------------------------------------ market
|
|
92
|
+
const marketFlow = useMarketFlow(
|
|
93
|
+
api,
|
|
94
|
+
shared,
|
|
95
|
+
catalogFlow.load,
|
|
96
|
+
groupFlow.loadGroups,
|
|
97
|
+
sourceFlow.loadSources,
|
|
98
|
+
sourceFlow.checkSources,
|
|
99
|
+
sourceFlow.sourceCheck,
|
|
100
|
+
)
|
|
246
101
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
102
|
+
// ------------------------------------------------------------- derived
|
|
103
|
+
/** skillName → origin repo(无来源记录的技能不在此映射中,筛选中视为 private)。 */
|
|
104
|
+
const origins = groupFlow.groupsState?.origins ?? {}
|
|
105
|
+
/** 来源筛选选项:来源记录中的仓库(排序)+ 末尾的「个人」;没有技能的仓库不列。 */
|
|
106
|
+
const sourceOptions = useMemo(() => {
|
|
107
|
+
const skills = catalogFlow.catalog?.skills ?? []
|
|
108
|
+
const repos = [...new Set(skills.map((skill) => origins[skill.name]).filter((repo): repo is string => repo !== undefined))].sort()
|
|
109
|
+
// 项目级技能(有 workspace 归属)不算「个人」。
|
|
110
|
+
const hasPrivate = skills.some((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
111
|
+
return [...repos, ...(hasPrivate ? [PRIVATE_SOURCE] : [])]
|
|
112
|
+
}, [catalogFlow.catalog, origins])
|
|
253
113
|
|
|
254
114
|
// 初始全量加载 + 双速轮询:目录是「活视图」(文件变化要 5 秒内可见),
|
|
255
115
|
// 统计/分组/来源/配置变化慢,60 秒一次足够;写操作本身会触发即时刷新。
|
|
256
116
|
// 更新检查、来源检查、市场检查全部改为手动按钮触发,打开面板不再自动
|
|
257
117
|
// 打三路 GitHub 请求。
|
|
258
118
|
useEffect(() => {
|
|
259
|
-
void load()
|
|
260
|
-
void loadUses()
|
|
261
|
-
void loadGroups()
|
|
262
|
-
void loadSources()
|
|
263
|
-
void loadConfig()
|
|
264
|
-
const fast = window.setInterval(() => { void load() },
|
|
265
|
-
const slow = window.setInterval(() => { void loadUses(); void loadGroups(); void loadSources(); void loadConfig() },
|
|
119
|
+
void catalogFlow.load()
|
|
120
|
+
void meta.loadUses()
|
|
121
|
+
void groupFlow.loadGroups()
|
|
122
|
+
void sourceFlow.loadSources()
|
|
123
|
+
void meta.loadConfig()
|
|
124
|
+
const fast = window.setInterval(() => { void catalogFlow.load() }, 5000)
|
|
125
|
+
const slow = window.setInterval(() => { void meta.loadUses(); void groupFlow.loadGroups(); void sourceFlow.loadSources(); void meta.loadConfig() }, 60_000)
|
|
266
126
|
return () => { window.clearInterval(fast); window.clearInterval(slow) }
|
|
267
|
-
}, [load, loadUses, loadGroups, loadSources, loadConfig])
|
|
268
|
-
|
|
269
|
-
const openDetail = useCallback(async (name: string): Promise<void> => {
|
|
270
|
-
setDetailLoading(true)
|
|
271
|
-
setLoadError(null)
|
|
272
|
-
try {
|
|
273
|
-
setDetail(await api.skill(name, workspace !== '' ? { cwd: workspace } : undefined))
|
|
274
|
-
} catch (error) {
|
|
275
|
-
setLoadError(errorMessage(error))
|
|
276
|
-
} finally {
|
|
277
|
-
setDetailLoading(false)
|
|
278
|
-
}
|
|
279
|
-
}, [api, workspace])
|
|
280
|
-
|
|
281
|
-
const toggle = useCallback(async (skill: CatalogSkill, enabled: boolean): Promise<void> => {
|
|
282
|
-
setBusyNames((previous) => new Set(previous).add(skill.name))
|
|
283
|
-
setLoadError(null)
|
|
284
|
-
try {
|
|
285
|
-
const next = await api.toggle(skill.name, enabled)
|
|
286
|
-
setCatalog(next)
|
|
287
|
-
} catch (error) {
|
|
288
|
-
setLoadError(errorMessage(error))
|
|
289
|
-
} finally {
|
|
290
|
-
setBusyNames((previous) => {
|
|
291
|
-
const next = new Set(previous)
|
|
292
|
-
next.delete(skill.name)
|
|
293
|
-
return next
|
|
294
|
-
})
|
|
295
|
-
}
|
|
296
|
-
}, [api])
|
|
297
|
-
|
|
298
|
-
const enableDisabled = useCallback(async (record: DisabledSkill): Promise<void> => {
|
|
299
|
-
setBusyNames((previous) => new Set(previous).add(record.name))
|
|
300
|
-
setLoadError(null)
|
|
301
|
-
try {
|
|
302
|
-
const next = await api.toggle(record.name, true)
|
|
303
|
-
setCatalog(next)
|
|
304
|
-
} catch (error) {
|
|
305
|
-
setLoadError(errorMessage(error))
|
|
306
|
-
} finally {
|
|
307
|
-
setBusyNames((previous) => {
|
|
308
|
-
const next = new Set(previous)
|
|
309
|
-
next.delete(record.name)
|
|
310
|
-
return next
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
}, [api])
|
|
314
|
-
|
|
315
|
-
/** Toggle an explicit name set in one write (enables disabled members too). */
|
|
316
|
-
const batchToggleNames = useCallback(async (names: string[], enabled: boolean): Promise<void> => {
|
|
317
|
-
if (names.length === 0) return
|
|
318
|
-
setBatchBusy(true)
|
|
319
|
-
setLoadError(null)
|
|
320
|
-
try {
|
|
321
|
-
const next = await api.toggleBatch(names, enabled)
|
|
322
|
-
setCatalog(next.catalog)
|
|
323
|
-
if (next.failures.length > 0) {
|
|
324
|
-
setLoadError('toggle-batch: ' + next.failures.map((failure) => failure.name + ': ' + failure.error).join('; '))
|
|
325
|
-
}
|
|
326
|
-
} catch (error) {
|
|
327
|
-
setLoadError(errorMessage(error))
|
|
328
|
-
} finally {
|
|
329
|
-
setBatchBusy(false)
|
|
330
|
-
}
|
|
331
|
-
}, [api])
|
|
332
|
-
|
|
333
|
-
// -------------------------------------------------------- group switches
|
|
334
|
-
|
|
335
|
-
/** 所有分组:key → 成员名(tag 与来源集合)。 */
|
|
336
|
-
const groupMap = useCallback((): Map<string, string[]> => {
|
|
337
|
-
const map = new Map<string, string[]>()
|
|
338
|
-
for (const tag of groupsState?.tags ?? []) map.set('tag:' + tag.id, tag.skillNames)
|
|
339
|
-
for (const collection of groupsState?.collections ?? []) map.set('col:' + collection.name, collection.skillNames)
|
|
340
|
-
return map
|
|
341
|
-
}, [groupsState])
|
|
342
|
-
|
|
343
|
-
/** 当前启用且可写的技能名(组开关只作用于它们)。 */
|
|
344
|
-
const actionNames = useMemo(() => new Set((catalog?.skills ?? []).filter((skill) => skill.writable).map((skill) => skill.name)), [catalog])
|
|
345
|
-
|
|
346
|
-
/** 当前启用的全部技能名(含只读,用于派生开关状态)。 */
|
|
347
|
-
const viewNames = useMemo(() => new Set((catalog?.skills ?? []).map((skill) => skill.name)), [catalog])
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Toggle a whole group via its tri-state switch. Closing with members that
|
|
351
|
-
* are enabled in other groups opens the conflict dialog instead.
|
|
352
|
-
*/
|
|
353
|
-
const toggleGroup = useCallback((key: string, name: string, view: GroupSwitchState): void => {
|
|
354
|
-
const members = groupMap().get(key) ?? []
|
|
355
|
-
if (members.length === 0) return
|
|
356
|
-
if (view === 'off') {
|
|
357
|
-
void batchToggleNames(members, true)
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
const others = [...groupMap().entries()].filter(([otherKey]) => otherKey !== key).map(([, memberNames]) => ({ members: memberNames }))
|
|
361
|
-
const conflicts = conflictsOnClose(members, actionNames, others)
|
|
362
|
-
if (conflicts.length > 0) {
|
|
363
|
-
setConflictDialog({ key, name, conflicts })
|
|
364
|
-
} else {
|
|
365
|
-
void batchToggleNames(members.filter((member) => actionNames.has(member)), false)
|
|
366
|
-
}
|
|
367
|
-
}, [groupMap, actionNames, batchToggleNames])
|
|
368
|
-
|
|
369
|
-
/** Resolve the open conflict dialog. */
|
|
370
|
-
const resolveConflict = useCallback(async (closeAll: boolean): Promise<void> => {
|
|
371
|
-
const dialog = conflictDialog
|
|
372
|
-
if (dialog === null) return
|
|
373
|
-
setConflictDialog(null)
|
|
374
|
-
const members = groupMap().get(dialog.key) ?? []
|
|
375
|
-
if (closeAll) {
|
|
376
|
-
await batchToggleNames(members.filter((member) => actionNames.has(member)), false)
|
|
377
|
-
} else {
|
|
378
|
-
await batchToggleNames(
|
|
379
|
-
members.filter((member) => actionNames.has(member) && !dialog.conflicts.includes(member)),
|
|
380
|
-
false,
|
|
381
|
-
)
|
|
382
|
-
}
|
|
383
|
-
}, [conflictDialog, groupMap, actionNames, batchToggleNames])
|
|
384
|
-
|
|
385
|
-
/** Dismiss a confirm dialog and run the pending source action. */
|
|
386
|
-
const runConfirmed = useCallback(async (): Promise<void> => {
|
|
387
|
-
const dialog = confirmDialog
|
|
388
|
-
if (dialog === null) return
|
|
389
|
-
setConfirmDialog(null)
|
|
390
|
-
if (dialog.kind === 'sync') {
|
|
391
|
-
setSyncingSource(dialog.repo)
|
|
392
|
-
setLoadError(null)
|
|
393
|
-
try {
|
|
394
|
-
const result = await api.syncSource(dialog.repo, dialog.skills)
|
|
395
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
396
|
-
if (result.failed.length > 0) {
|
|
397
|
-
setLoadError('sync: ' + result.failed.map((failure) => failure.name + ': ' + failure.error).join('; '))
|
|
398
|
-
}
|
|
399
|
-
} catch (error) {
|
|
400
|
-
setLoadError(errorMessage(error))
|
|
401
|
-
} finally {
|
|
402
|
-
setSyncingSource(null)
|
|
403
|
-
}
|
|
404
|
-
} else {
|
|
405
|
-
setLoadError(null)
|
|
406
|
-
try {
|
|
407
|
-
await api.confirmDeleteSource(dialog.repo, dialog.skills)
|
|
408
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
409
|
-
} catch (error) {
|
|
410
|
-
setLoadError(errorMessage(error))
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
}, [confirmDialog, api, load, loadGroups, loadSources])
|
|
414
|
-
|
|
415
|
-
// ---------------------------------------------------------- source ops
|
|
416
|
-
|
|
417
|
-
/** 检查全部来源的上游更新(服务端 5 分钟节流)。 */
|
|
418
|
-
const checkSources = useCallback(async (repo?: string): Promise<void> => {
|
|
419
|
-
setCheckingSource(repo ?? 'all')
|
|
420
|
-
setLoadError(null)
|
|
421
|
-
try {
|
|
422
|
-
const result = await api.checkSources(repo)
|
|
423
|
-
const next: Record<string, SourceCheckResult> = { ...sourceCheck }
|
|
424
|
-
for (const item of result.results) next[item.repo] = item
|
|
425
|
-
setSourceCheck(next)
|
|
426
|
-
} catch (error) {
|
|
427
|
-
setLoadError(errorMessage(error))
|
|
428
|
-
} finally {
|
|
429
|
-
setCheckingSource(null)
|
|
430
|
-
}
|
|
431
|
-
}, [api, sourceCheck])
|
|
432
|
-
|
|
433
|
-
/** 请求同步某个来源的所选技能(弹确认,因为会覆盖本地修改)。 */
|
|
434
|
-
const requestSync = useCallback((repo: string, skills: string[]): void => {
|
|
435
|
-
setConfirmDialog({ kind: 'sync', repo, skills })
|
|
436
|
-
}, [])
|
|
437
|
-
|
|
438
|
-
/** 请求跟进上游删除(弹确认,移入回收站)。 */
|
|
439
|
-
const requestDelete = useCallback((repo: string, skills: string[]): void => {
|
|
440
|
-
setConfirmDialog({ kind: 'delete', repo, skills })
|
|
441
|
-
}, [])
|
|
442
|
-
|
|
443
|
-
/** 从回收站恢复一个技能。 */
|
|
444
|
-
const restoreTrash = useCallback(async (name: string): Promise<void> => {
|
|
445
|
-
setTagBusy(true)
|
|
446
|
-
setLoadError(null)
|
|
447
|
-
try {
|
|
448
|
-
await api.restoreSource(name)
|
|
449
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
450
|
-
} catch (error) {
|
|
451
|
-
setLoadError(errorMessage(error))
|
|
452
|
-
} finally {
|
|
453
|
-
setTagBusy(false)
|
|
454
|
-
}
|
|
455
|
-
}, [api, load, loadGroups, loadSources])
|
|
456
|
-
|
|
457
|
-
/** 确认后永久删除回收站里的全部技能。 */
|
|
458
|
-
const clearTrash = useCallback(async (): Promise<void> => {
|
|
459
|
-
setConfirmClearTrash(false)
|
|
460
|
-
setTagBusy(true)
|
|
461
|
-
setLoadError(null)
|
|
462
|
-
try {
|
|
463
|
-
const result = await api.clearTrash()
|
|
464
|
-
if (result.failed.length > 0) {
|
|
465
|
-
setLoadError('clear trash: ' + result.failed.map((failure) => failure.name + ': ' + failure.error).join('; '))
|
|
466
|
-
}
|
|
467
|
-
await Promise.all([load(), loadSources()])
|
|
468
|
-
} catch (error) {
|
|
469
|
-
setLoadError(errorMessage(error))
|
|
470
|
-
} finally {
|
|
471
|
-
setTagBusy(false)
|
|
472
|
-
}
|
|
473
|
-
}, [api, load, loadSources])
|
|
474
|
-
|
|
475
|
-
const [fixingPaths, setFixingPaths] = useState<ReadonlySet<string>>(new Set())
|
|
476
|
-
const fixDiagnostic = useCallback(async (path: string): Promise<void> => {
|
|
477
|
-
setFixingPaths((previous) => new Set(previous).add(path))
|
|
478
|
-
setLoadError(null)
|
|
479
|
-
try {
|
|
480
|
-
const confirmed = window.confirm(tt('diag.fixConfirm', { path }))
|
|
481
|
-
if (!confirmed) return
|
|
482
|
-
await api.fixDiagnostic(path)
|
|
483
|
-
await load()
|
|
484
|
-
setSuccessBanner(tt('diag.fixed'))
|
|
485
|
-
} catch (error) {
|
|
486
|
-
setLoadError(errorMessage(error))
|
|
487
|
-
} finally {
|
|
488
|
-
setFixingPaths((previous) => {
|
|
489
|
-
const next = new Set(previous)
|
|
490
|
-
next.delete(path)
|
|
491
|
-
return next
|
|
492
|
-
})
|
|
493
|
-
}
|
|
494
|
-
}, [api, load])
|
|
495
|
-
|
|
496
|
-
/** 打开单个技能的删除确认(移入回收站,可恢复)。 */
|
|
497
|
-
const requestDeleteSkill = useCallback((name: string): void => {
|
|
498
|
-
setDeleteSkillDialog(name)
|
|
499
|
-
}, [])
|
|
500
|
-
|
|
501
|
-
/** 执行删除确认后的回收站迁移。 */
|
|
502
|
-
const runDeleteSkill = useCallback(async (): Promise<void> => {
|
|
503
|
-
const name = deleteSkillDialog
|
|
504
|
-
if (name === null) return
|
|
505
|
-
setDeleteSkillDialog(null)
|
|
506
|
-
setTagBusy(true)
|
|
507
|
-
setLoadError(null)
|
|
508
|
-
try {
|
|
509
|
-
await api.deleteSkill(name)
|
|
510
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
511
|
-
} catch (error) {
|
|
512
|
-
setLoadError(errorMessage(error))
|
|
513
|
-
} finally {
|
|
514
|
-
setTagBusy(false)
|
|
515
|
-
}
|
|
516
|
-
}, [api, deleteSkillDialog, load, loadGroups, loadSources])
|
|
517
|
-
|
|
518
|
-
/** 打开整组删除确认(来源分组一键删除)。 */
|
|
519
|
-
const requestDeleteGroup = useCallback((name: string, skillNames: string[]): void => {
|
|
520
|
-
setDeleteGroupDialog({ name, skillNames })
|
|
521
|
-
}, [])
|
|
522
|
-
|
|
523
|
-
/** 执行整组删除(逐个移入回收站,跳过只读)。 */
|
|
524
|
-
const runDeleteGroup = useCallback(async (): Promise<void> => {
|
|
525
|
-
const dialog = deleteGroupDialog
|
|
526
|
-
if (dialog === null) return
|
|
527
|
-
setDeleteGroupDialog(null)
|
|
528
|
-
setTagBusy(true)
|
|
529
|
-
setLoadError(null)
|
|
530
|
-
const failures: string[] = []
|
|
531
|
-
let done = 0
|
|
532
|
-
for (const name of dialog.skillNames) {
|
|
533
|
-
try {
|
|
534
|
-
await api.deleteSkill(name)
|
|
535
|
-
done += 1
|
|
536
|
-
} catch (error) {
|
|
537
|
-
// 只读/不存在的跳过并记录
|
|
538
|
-
failures.push(name + ': ' + errorMessage(error))
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
542
|
-
setTagBusy(false)
|
|
543
|
-
if (failures.length > 0) {
|
|
544
|
-
setLoadError(`删除整组 "${dialog.name}":成功 ${done} 个,失败 ${failures.length} 个:` + failures.join('; '))
|
|
545
|
-
} else if (done > 0) {
|
|
546
|
-
setSuccessBanner(`已删除整组 "${dialog.name}":${done} 个技能已移入回收站`)
|
|
547
|
-
}
|
|
548
|
-
}, [api, deleteGroupDialog, load, loadGroups, loadSources])
|
|
549
|
-
|
|
550
|
-
/** 新建一个空 tag 分组。 */
|
|
551
|
-
const createTag = useCallback(async (event: FormEvent): Promise<void> => {
|
|
552
|
-
event.preventDefault()
|
|
553
|
-
const name = newTagName.trim()
|
|
554
|
-
if (name === '') return
|
|
555
|
-
setTagBusy(true)
|
|
556
|
-
setLoadError(null)
|
|
557
|
-
try {
|
|
558
|
-
applyTags(await api.saveTag({ name }))
|
|
559
|
-
setNewTagName('')
|
|
560
|
-
} catch (error) {
|
|
561
|
-
setLoadError(errorMessage(error))
|
|
562
|
-
} finally {
|
|
563
|
-
setTagBusy(false)
|
|
564
|
-
}
|
|
565
|
-
}, [api, applyTags, newTagName])
|
|
566
|
-
|
|
567
|
-
/** 删除一个 tag 分组(不影响技能文件)。 */
|
|
568
|
-
const deleteTag = useCallback(async (id: string): Promise<void> => {
|
|
569
|
-
setTagBusy(true)
|
|
570
|
-
setLoadError(null)
|
|
571
|
-
try {
|
|
572
|
-
applyTags(await api.deleteTag(id))
|
|
573
|
-
setEditingTag(null)
|
|
574
|
-
} catch (error) {
|
|
575
|
-
setLoadError(errorMessage(error))
|
|
576
|
-
} finally {
|
|
577
|
-
setTagBusy(false)
|
|
578
|
-
}
|
|
579
|
-
}, [api, applyTags])
|
|
580
|
-
|
|
581
|
-
/** 重命名 tag,或保存成员勾选后回到列表。 */
|
|
582
|
-
const saveTag = useCallback(async (id: string, name: string, memberNames: string[] | null): Promise<void> => {
|
|
583
|
-
setTagBusy(true)
|
|
584
|
-
setLoadError(null)
|
|
585
|
-
try {
|
|
586
|
-
const safeName = name.trim()
|
|
587
|
-
let tags: SkillTag[] | null = null
|
|
588
|
-
if (safeName !== '') tags = await api.saveTag({ id, name: safeName })
|
|
589
|
-
if (memberNames !== null) tags = await api.setTagMembers(id, memberNames)
|
|
590
|
-
if (tags === null) return
|
|
591
|
-
applyTags(tags)
|
|
592
|
-
setEditingTag(null)
|
|
593
|
-
} catch (error) {
|
|
594
|
-
setLoadError(errorMessage(error))
|
|
595
|
-
} finally {
|
|
596
|
-
setTagBusy(false)
|
|
597
|
-
}
|
|
598
|
-
}, [api, applyTags])
|
|
599
|
-
|
|
600
|
-
/** 拖拽重排场景分组 */
|
|
601
|
-
const reorderTags = useCallback(async (orderedIds: string[]): Promise<void> => {
|
|
602
|
-
setTagBusy(true)
|
|
603
|
-
setLoadError(null)
|
|
604
|
-
try {
|
|
605
|
-
const tags = await api.reorderTags(orderedIds)
|
|
606
|
-
applyTags(tags)
|
|
607
|
-
} catch (error) {
|
|
608
|
-
const msg = errorMessage(error)
|
|
609
|
-
if (msg.includes('404') || msg.toLowerCase().includes('not found')) {
|
|
610
|
-
// 旧宿主无此路由:本地重排
|
|
611
|
-
const byId = new Map(groupsState?.tags.map((t) => [t.id, t] as const) ?? [])
|
|
612
|
-
const reordered = orderedIds.map((id) => byId.get(id)).filter((t): t is NonNullable<typeof t> => t !== undefined)
|
|
613
|
-
if (reordered.length === orderedIds.length) {
|
|
614
|
-
applyTags(reordered as typeof groupsState extends { tags: infer T } ? T : never)
|
|
615
|
-
setSuccessBanner('已临时调整顺序(本地生效,重启宿主后持久化)')
|
|
616
|
-
} else {
|
|
617
|
-
setLoadError(msg)
|
|
618
|
-
}
|
|
619
|
-
} else {
|
|
620
|
-
setLoadError(msg)
|
|
621
|
-
}
|
|
622
|
-
} finally {
|
|
623
|
-
setTagBusy(false)
|
|
624
|
-
}
|
|
625
|
-
}, [api, applyTags, groupsState])
|
|
626
|
-
|
|
627
|
-
/** 拖拽重排来源集合 */
|
|
628
|
-
const reorderCollections = useCallback(async (orderedNames: string[]): Promise<void> => {
|
|
629
|
-
setTagBusy(true)
|
|
630
|
-
setLoadError(null)
|
|
631
|
-
try {
|
|
632
|
-
const collections = await api.reorderCollections(orderedNames)
|
|
633
|
-
setGroupsState((prev) => prev === null ? prev : { ...prev, collections })
|
|
634
|
-
void loadGroups()
|
|
635
|
-
} catch (error) {
|
|
636
|
-
const msg = errorMessage(error)
|
|
637
|
-
// 404 说明宿主仍在跑旧版(需重启后才有新路由),降级为本地即时生效
|
|
638
|
-
if (msg.includes('404') || msg.toLowerCase().includes('not found')) {
|
|
639
|
-
setGroupsState((prev) => {
|
|
640
|
-
if (prev === null) return prev
|
|
641
|
-
const map = new Map(prev.collections.map((c) => [c.name, c] as const))
|
|
642
|
-
const reordered = orderedNames.map((n) => map.get(n)).filter((c): c is NonNullable<typeof c> => c !== undefined)
|
|
643
|
-
// 补上未在 orderedNames 中的集合(新出现的)
|
|
644
|
-
for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c)
|
|
645
|
-
return { ...prev, collections: reordered }
|
|
646
|
-
})
|
|
647
|
-
setSuccessBanner('已临时调整顺序(本地生效,重启宿主后持久化)')
|
|
648
|
-
} else {
|
|
649
|
-
setLoadError(msg)
|
|
650
|
-
}
|
|
651
|
-
} finally {
|
|
652
|
-
setTagBusy(false)
|
|
653
|
-
}
|
|
654
|
-
}, [api, loadGroups])
|
|
655
|
-
|
|
656
|
-
/** 拖拽重排来源顶层分组(project / col:xxx / personal 全量可拖) */
|
|
657
|
-
const reorderSourceGroups = useCallback(async (orderedKeys: string[]): Promise<void> => {
|
|
658
|
-
setTagBusy(true)
|
|
659
|
-
setLoadError(null)
|
|
660
|
-
try {
|
|
661
|
-
await api.reorderSourceGroups(orderedKeys)
|
|
662
|
-
await loadGroups()
|
|
663
|
-
} catch (error) {
|
|
664
|
-
const msg = errorMessage(error)
|
|
665
|
-
if (msg.includes('404') || msg.toLowerCase().includes('not found')) {
|
|
666
|
-
// 旧宿主无此路由:本地重排顶层顺序,提示重启后持久化
|
|
667
|
-
setGroupsState((prev) => {
|
|
668
|
-
if (prev === null) return prev
|
|
669
|
-
// 本地重排 collections 以匹配 topOrderedKeys 中的 col:xxx 顺序
|
|
670
|
-
const colOrder = orderedKeys.filter((k) => k.startsWith('col:')).map((k) => k.slice(4))
|
|
671
|
-
const map = new Map(prev.collections.map((c) => [c.name, c] as const))
|
|
672
|
-
const reordered = colOrder.map((n) => map.get(n)).filter((c): c is NonNullable<typeof c> => c !== undefined)
|
|
673
|
-
for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c)
|
|
674
|
-
return { ...prev, collections: reordered, sourceGroupOrder: orderedKeys }
|
|
675
|
-
})
|
|
676
|
-
setSuccessBanner('已临时调整顺序(本地生效,重启宿主后持久化)')
|
|
677
|
-
} else {
|
|
678
|
-
setLoadError(msg)
|
|
679
|
-
}
|
|
680
|
-
} finally {
|
|
681
|
-
setTagBusy(false)
|
|
682
|
-
}
|
|
683
|
-
}, [api, loadGroups])
|
|
684
|
-
|
|
685
|
-
// -------------------------------------------------------------- market
|
|
686
|
-
|
|
687
|
-
/** 添加一个市场源(内置市场目录与手动输入共用),并立即扫描它。 */
|
|
688
|
-
const addSource = useCallback(async (input: string): Promise<void> => {
|
|
689
|
-
const value = input.trim()
|
|
690
|
-
if (value === '') return
|
|
691
|
-
setLoadError(null)
|
|
692
|
-
try {
|
|
693
|
-
const result = await api.addMarketSource(value)
|
|
694
|
-
setMarketState({ status: 'ready', repos: result.repos })
|
|
695
|
-
setNewSourceName('')
|
|
696
|
-
setRepoResult(null)
|
|
697
|
-
setRepoSelected(new Set())
|
|
698
|
-
// 用服务端归一化后的 slug 扫描(URL/owner@repo 输入都能对上记录)。
|
|
699
|
-
const slug = result.repos[result.repos.length - 1].repo
|
|
700
|
-
setScanningRepo(slug)
|
|
701
|
-
setRepoDiscoverState({ status: 'scanning' })
|
|
702
|
-
try {
|
|
703
|
-
const data = await api.repoDiscover(slug)
|
|
704
|
-
setRepoDiscoverState({ status: 'ready', data })
|
|
705
|
-
} catch (error) {
|
|
706
|
-
setRepoDiscoverState({ status: 'error', message: errorMessage(error) })
|
|
707
|
-
} finally {
|
|
708
|
-
setScanningRepo(null)
|
|
709
|
-
}
|
|
710
|
-
} catch (error) {
|
|
711
|
-
setLoadError(errorMessage(error))
|
|
712
|
-
}
|
|
713
|
-
}, [api])
|
|
714
|
-
|
|
715
|
-
/** 手动输入框的添加动作。 */
|
|
716
|
-
const addMarketSource = useCallback(async (): Promise<void> => {
|
|
717
|
-
await addSource(newSourceName)
|
|
718
|
-
}, [addSource, newSourceName])
|
|
719
|
-
|
|
720
|
-
/** 删除一个市场源(不影响已装技能)。 */
|
|
721
|
-
const removeMarketSource = useCallback(async (repo: string): Promise<void> => {
|
|
722
|
-
setTagBusy(true)
|
|
723
|
-
setLoadError(null)
|
|
724
|
-
try {
|
|
725
|
-
const result = await api.removeMarketSource(repo)
|
|
726
|
-
setMarketState({ status: 'ready', repos: result.repos })
|
|
727
|
-
} catch (error) {
|
|
728
|
-
setLoadError(errorMessage(error))
|
|
729
|
-
} finally {
|
|
730
|
-
setTagBusy(false)
|
|
731
|
-
}
|
|
732
|
-
}, [api])
|
|
733
|
-
|
|
734
|
-
/** 扫描一个市场源(或手动输入)的仓库。 */
|
|
735
|
-
const scanRepo = useCallback(async (input: string): Promise<void> => {
|
|
736
|
-
const value = input.trim()
|
|
737
|
-
if (value === '') return
|
|
738
|
-
setRepoResult(null)
|
|
739
|
-
setRepoSelected(new Set())
|
|
740
|
-
setScanningRepo(value)
|
|
741
|
-
setRepoDiscoverState({ status: 'scanning' })
|
|
742
|
-
setLoadError(null)
|
|
743
|
-
try {
|
|
744
|
-
const data = await api.repoDiscover(value)
|
|
745
|
-
if (data.ref === null) {
|
|
746
|
-
// 无 release 且未定版:让用户选分支(默认第一项,通常是 main)。
|
|
747
|
-
setBranchChoice({ repo: data.repo, branches: data.branches ?? [], selected: (data.branches ?? [])[0] ?? 'main' })
|
|
748
|
-
setRepoDiscoverState({ status: 'idle' })
|
|
749
|
-
return
|
|
750
|
-
}
|
|
751
|
-
setRepoDiscoverState({ status: 'ready', data })
|
|
752
|
-
// The discover route may have auto-pinned the release; refresh the list so the ref badge shows.
|
|
753
|
-
void loadMarket()
|
|
754
|
-
} catch (error) {
|
|
755
|
-
setRepoDiscoverState({ status: 'error', message: errorMessage(error) })
|
|
756
|
-
} finally {
|
|
757
|
-
setScanningRepo(null)
|
|
758
|
-
}
|
|
759
|
-
}, [api, loadMarket])
|
|
760
|
-
|
|
761
|
-
/** 确认分支选择:持久化 ref 后重新扫描。 */
|
|
762
|
-
const confirmBranchChoice = useCallback(async (): Promise<void> => {
|
|
763
|
-
if (branchChoice === null) return
|
|
764
|
-
setBranchBusy(true)
|
|
765
|
-
setLoadError(null)
|
|
766
|
-
try {
|
|
767
|
-
await api.setMarketSourceRef(branchChoice.repo, branchChoice.selected)
|
|
768
|
-
setBranchChoice(null)
|
|
769
|
-
await scanRepo(branchChoice.repo)
|
|
770
|
-
} catch (error) {
|
|
771
|
-
setLoadError(errorMessage(error))
|
|
772
|
-
} finally {
|
|
773
|
-
setBranchBusy(false)
|
|
774
|
-
}
|
|
775
|
-
}, [api, branchChoice, scanRepo])
|
|
776
|
-
|
|
777
|
-
/** 版本对话框:打开时拉 releases + branches,确认后定版并重扫。 */
|
|
778
|
-
const [versionDialog, setVersionDialog] = useState<VersionChoiceState | null>(null)
|
|
779
|
-
const [versionBusy, setVersionBusy] = useState(false)
|
|
780
|
-
const openVersionDialog = useCallback(async (repo: string): Promise<void> => {
|
|
781
|
-
const current = marketState.repos.find((item) => item.repo === repo)?.ref
|
|
782
|
-
setVersionDialog({ repo, ...(current !== undefined ? { current } : {}), releases: [], branches: [], selected: current ?? '', custom: '', loading: true })
|
|
783
|
-
setLoadError(null)
|
|
784
|
-
try {
|
|
785
|
-
const data = await api.marketSourceVersions(repo)
|
|
786
|
-
setVersionDialog({
|
|
787
|
-
repo,
|
|
788
|
-
...(data.current !== undefined ? { current: data.current } : {}),
|
|
789
|
-
releases: data.releases,
|
|
790
|
-
branches: data.branches,
|
|
791
|
-
selected: data.current ?? data.releases[0] ?? data.branches[0] ?? '',
|
|
792
|
-
custom: '',
|
|
793
|
-
loading: false,
|
|
794
|
-
})
|
|
795
|
-
} catch (error) {
|
|
796
|
-
setLoadError(errorMessage(error))
|
|
797
|
-
setVersionDialog(null)
|
|
798
|
-
}
|
|
799
|
-
}, [api, marketState])
|
|
800
|
-
const confirmVersionDialog = useCallback(async (): Promise<void> => {
|
|
801
|
-
if (versionDialog === null) return
|
|
802
|
-
const ref = versionDialog.custom.trim() !== '' ? versionDialog.custom.trim() : versionDialog.selected
|
|
803
|
-
if (ref === '') return
|
|
804
|
-
setVersionBusy(true)
|
|
805
|
-
setLoadError(null)
|
|
806
|
-
try {
|
|
807
|
-
await api.setMarketSourceRef(versionDialog.repo, ref)
|
|
808
|
-
setVersionDialog(null)
|
|
809
|
-
await loadMarket()
|
|
810
|
-
await scanRepo(versionDialog.repo)
|
|
811
|
-
} catch (error) {
|
|
812
|
-
setLoadError(errorMessage(error))
|
|
813
|
-
} finally {
|
|
814
|
-
setVersionBusy(false)
|
|
815
|
-
}
|
|
816
|
-
}, [api, versionDialog, loadMarket, scanRepo])
|
|
817
|
-
|
|
818
|
-
/** Toggle one repo preview row. */
|
|
819
|
-
const toggleRepoSelected = useCallback((path: string, checked: boolean): void => {
|
|
820
|
-
setRepoSelected((previous) => {
|
|
821
|
-
const next = new Set(previous)
|
|
822
|
-
if (checked) next.add(path)
|
|
823
|
-
else next.delete(path)
|
|
824
|
-
return next
|
|
825
|
-
})
|
|
826
|
-
}, [])
|
|
827
|
-
|
|
828
|
-
/** Import every checked, non-existing repo skill (B方案:job+轮询+进度). */
|
|
829
|
-
const importRepo = useCallback(async (): Promise<void> => {
|
|
830
|
-
if (repoDiscoverState.status !== 'ready') return
|
|
831
|
-
setRepoImporting(true)
|
|
832
|
-
setRepoResult(null)
|
|
833
|
-
setImportJobId(null)
|
|
834
|
-
setLoadError(null)
|
|
835
|
-
let finalProgress: RepoImportProgressResponse | null = null
|
|
836
|
-
try {
|
|
837
|
-
const created = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? undefined)
|
|
838
|
-
setImportJobId(created.jobId)
|
|
839
|
-
// 初始占位,让进度卡片立刻出现(带真实 totalBytes)
|
|
840
|
-
setRepoResult({ ok: true, jobId: created.jobId, status: 'running', total: created.total, done: 0, totalBytes: created.totalBytes, downloadedBytes: 0, imported: [], skipped: [], failed: [] })
|
|
841
|
-
// 轮询直到 done/cancelled/error(800ms 起步 + 退避,避免 276 技能 900 次请求)
|
|
842
|
-
pollAbortRef.current?.abort()
|
|
843
|
-
pollAbortRef.current = new AbortController()
|
|
844
|
-
const signal = pollAbortRef.current.signal
|
|
845
|
-
let attempt = 0
|
|
846
|
-
for (;;) {
|
|
847
|
-
if (signal.aborted) break
|
|
848
|
-
const delay = Math.min(2000, 800 + attempt * 200)
|
|
849
|
-
await new Promise((r) => setTimeout(r, delay))
|
|
850
|
-
if (signal.aborted) break
|
|
851
|
-
try {
|
|
852
|
-
const progress = await api.repoImportProgress(created.jobId)
|
|
853
|
-
setRepoResult(progress)
|
|
854
|
-
finalProgress = progress
|
|
855
|
-
if (progress.status !== 'running') {
|
|
856
|
-
break
|
|
857
|
-
}
|
|
858
|
-
attempt = 0
|
|
859
|
-
} catch (pollError) {
|
|
860
|
-
// 轮询失败退避重试
|
|
861
|
-
const msg = errorMessage(pollError)
|
|
862
|
-
if (msg.includes('not found')) break
|
|
863
|
-
attempt += 1
|
|
864
|
-
if (attempt > 8) break
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
await Promise.all([load(), loadMarket(), loadGroups(), loadSources()])
|
|
868
|
-
} catch (error) {
|
|
869
|
-
setLoadError(errorMessage(error))
|
|
870
|
-
} finally {
|
|
871
|
-
// 导入完成后把已导入/已跳过的项从勾选中移除,并把扫描快照里的 existing 置为 true,避免“仍可勾选已导入”的错觉
|
|
872
|
-
if (finalProgress !== null && (finalProgress.imported.length > 0 || finalProgress.skipped.length > 0)) {
|
|
873
|
-
const doneNames = new Set([...finalProgress.imported.map((r) => r.name), ...finalProgress.skipped.map((r) => r.name)])
|
|
874
|
-
const donePaths = new Set(
|
|
875
|
-
repoDiscoverState.data.entries.filter((e) => doneNames.has(e.name)).map((e) => e.path),
|
|
876
|
-
)
|
|
877
|
-
setRepoSelected((prev) => {
|
|
878
|
-
const next = new Set(prev)
|
|
879
|
-
for (const p of donePaths) next.delete(p)
|
|
880
|
-
return next
|
|
881
|
-
})
|
|
882
|
-
setRepoDiscoverState((prev) => {
|
|
883
|
-
if (prev.status !== 'ready') return prev
|
|
884
|
-
return {
|
|
885
|
-
...prev,
|
|
886
|
-
data: {
|
|
887
|
-
...prev.data,
|
|
888
|
-
entries: prev.data.entries.map((e) => doneNames.has(e.name) ? { ...e, existing: true } : e),
|
|
889
|
-
},
|
|
890
|
-
}
|
|
891
|
-
})
|
|
892
|
-
}
|
|
893
|
-
setRepoImporting(false)
|
|
894
|
-
}
|
|
895
|
-
}, [api, repoDiscoverState, repoSelected, load, loadMarket, loadGroups, loadSources])
|
|
896
|
-
|
|
897
|
-
// 卸载时中断轮询,避免泄露
|
|
898
|
-
useEffect(() => {
|
|
899
|
-
return () => { pollAbortRef.current?.abort() }
|
|
900
|
-
}, [])
|
|
901
|
-
|
|
902
|
-
/** 取消正在进行的导入(选项2:唯有取消才停) */
|
|
903
|
-
const cancelImport = useCallback(async (): Promise<void> => {
|
|
904
|
-
if (importJobId === null) return
|
|
905
|
-
pollAbortRef.current?.abort()
|
|
906
|
-
try {
|
|
907
|
-
const res = await api.repoImportCancel(importJobId)
|
|
908
|
-
// 立刻刷新一次进度
|
|
909
|
-
try {
|
|
910
|
-
const progress = await api.repoImportProgress(importJobId)
|
|
911
|
-
setRepoResult(progress)
|
|
912
|
-
} catch {
|
|
913
|
-
setRepoResult((prev) => prev !== null ? { ...prev, status: res.status as 'cancelled' } : prev)
|
|
914
|
-
}
|
|
915
|
-
} catch (error) {
|
|
916
|
-
setLoadError(errorMessage(error))
|
|
917
|
-
} finally {
|
|
918
|
-
setRepoImporting(false)
|
|
919
|
-
}
|
|
920
|
-
}, [api, importJobId])
|
|
921
|
-
|
|
922
|
-
/** 清空扫描结果(关闭归属卡片) */
|
|
923
|
-
const clearScan = useCallback((): void => {
|
|
924
|
-
setRepoDiscoverState({ status: 'idle' })
|
|
925
|
-
setScanningRepo(null)
|
|
926
|
-
setRepoResult(null)
|
|
927
|
-
setRepoSelected(new Set())
|
|
928
|
-
setImportJobId(null)
|
|
929
|
-
pollAbortRef.current?.abort()
|
|
930
|
-
}, [])
|
|
931
|
-
|
|
932
|
-
/** 检查所有市场源的上游更新(服务端节流)。 */
|
|
933
|
-
const checkMarket = useCallback(async (): Promise<void> => {
|
|
934
|
-
try {
|
|
935
|
-
const result = await api.marketCheck()
|
|
936
|
-
const next: Record<string, MarketCheckResult> = {}
|
|
937
|
-
for (const item of result.results) next[item.repo] = item
|
|
938
|
-
setMarketCheck(next)
|
|
939
|
-
} catch {
|
|
940
|
-
// 检查失败不打扰市场列表本身。
|
|
941
|
-
}
|
|
942
|
-
}, [api])
|
|
943
|
-
|
|
944
|
-
/** 市场源同步:版本对齐后询问是否批量更新本地技能。 */
|
|
945
|
-
const syncMarketSource = useCallback(async (repo: string): Promise<void> => {
|
|
946
|
-
setSyncingMarket(repo)
|
|
947
|
-
setLoadError(null)
|
|
948
|
-
try {
|
|
949
|
-
const result = await api.marketSync(repo)
|
|
950
|
-
setMarketSyncDialog({ repo: result.repo, ref: result.ref, skills: result.skills, selected: new Set(result.skills) })
|
|
951
|
-
await loadMarket()
|
|
952
|
-
} catch (error) {
|
|
953
|
-
setLoadError(errorMessage(error))
|
|
954
|
-
} finally {
|
|
955
|
-
setSyncingMarket(null)
|
|
956
|
-
}
|
|
957
|
-
}, [api, loadMarket])
|
|
958
|
-
|
|
959
|
-
/** 批量更新本地技能到市场源当前版本(复用来源同步路径)。 */
|
|
960
|
-
const confirmMarketSync = useCallback(async (): Promise<void> => {
|
|
961
|
-
if (marketSyncDialog === null) return
|
|
962
|
-
const selected = [...marketSyncDialog.selected]
|
|
963
|
-
setSyncBusy(true)
|
|
964
|
-
setLoadError(null)
|
|
965
|
-
try {
|
|
966
|
-
if (selected.length > 0) {
|
|
967
|
-
const result = await api.syncSource(marketSyncDialog.repo, selected)
|
|
968
|
-
if (result.failed.length > 0) setLoadError(result.failed.map((item) => item.name + ': ' + item.error).join('\n'))
|
|
969
|
-
}
|
|
970
|
-
setMarketSyncDialog(null)
|
|
971
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
972
|
-
void checkMarket()
|
|
973
|
-
} catch (error) {
|
|
974
|
-
setLoadError(errorMessage(error))
|
|
975
|
-
} finally {
|
|
976
|
-
setSyncBusy(false)
|
|
977
|
-
}
|
|
978
|
-
}, [api, marketSyncDialog, load, loadGroups, loadSources, checkMarket])
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* 一键全部更新:把每个「有可更新技能」的来源逐个同步到上游最新版本
|
|
982
|
-
* (复用来源同步路径,覆盖本地修改),随后刷新并重新检查。
|
|
983
|
-
*/
|
|
984
|
-
const updateAll = useCallback(async (): Promise<void> => {
|
|
985
|
-
setUpdateAllDialog(false)
|
|
986
|
-
setBatchBusy(true)
|
|
987
|
-
setLoadError(null)
|
|
988
|
-
const failures: string[] = []
|
|
989
|
-
let done = 0
|
|
990
|
-
try {
|
|
991
|
-
for (const [repo, check] of Object.entries(sourceCheck)) {
|
|
992
|
-
if (!check.changed || check.updated.length === 0) continue
|
|
993
|
-
try {
|
|
994
|
-
const result = await api.syncSource(repo, check.updated)
|
|
995
|
-
done += 1
|
|
996
|
-
if (result.failed.length > 0) {
|
|
997
|
-
failures.push(tt('market.updateAllItem', { repo, count: result.failed.length }) + ': ' + result.failed.map((item) => item.name + ': ' + item.error).join('; '))
|
|
998
|
-
}
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
failures.push(repo + ': ' + errorMessage(error))
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
await Promise.all([load(), loadGroups(), loadSources()])
|
|
1004
|
-
await checkSources()
|
|
1005
|
-
void checkMarket()
|
|
1006
|
-
if (failures.length > 0) setLoadError(failures.join('\n'))
|
|
1007
|
-
else if (done > 0) setSuccessBanner(tt('market.updateAllDone', { count: done }))
|
|
1008
|
-
} finally {
|
|
1009
|
-
setBatchBusy(false)
|
|
1010
|
-
}
|
|
1011
|
-
}, [api, sourceCheck, checkSources, checkMarket, load, loadGroups, loadSources])
|
|
127
|
+
}, [catalogFlow.load, meta.loadUses, groupFlow.loadGroups, sourceFlow.loadSources, meta.loadConfig])
|
|
1012
128
|
|
|
1013
129
|
// ------------------------------------------------- daily auto-check
|
|
1014
130
|
// 打开面板时每天最多自动检查一次(自身更新 + 全部来源 + 全部市场源),
|
|
@@ -1016,88 +132,81 @@ export function useSkillHub(api: SkillHubApi) {
|
|
|
1016
132
|
useEffect(() => {
|
|
1017
133
|
if (!shouldAutoCheck()) return
|
|
1018
134
|
markAutoChecked()
|
|
1019
|
-
void checkUpdate()
|
|
1020
|
-
void checkSources()
|
|
1021
|
-
void checkMarket()
|
|
135
|
+
void meta.checkUpdate()
|
|
136
|
+
void sourceFlow.checkSources()
|
|
137
|
+
void marketFlow.checkMarket()
|
|
1022
138
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1023
139
|
}, [])
|
|
1024
140
|
|
|
1025
|
-
// ---------------------------------------------------------------- create
|
|
1026
|
-
|
|
1027
|
-
const create = useCallback(async (event: FormEvent): Promise<void> => {
|
|
1028
|
-
event.preventDefault()
|
|
1029
|
-
setFormBusy(true)
|
|
1030
|
-
setFormMessage(null)
|
|
1031
|
-
setSuccessBanner(null)
|
|
1032
|
-
try {
|
|
1033
|
-
const result = await api.create({ name: formName, description: formDesc, root: formRoot })
|
|
1034
|
-
// The form closes on success, so the confirmation lives in the green
|
|
1035
|
-
// banner outside it (a success message inside the closing form is
|
|
1036
|
-
// never visible).
|
|
1037
|
-
setSuccessBanner(tt('form.success') + result.path)
|
|
1038
|
-
setFormName('')
|
|
1039
|
-
setFormDesc('')
|
|
1040
|
-
setShowForm(false)
|
|
1041
|
-
await load()
|
|
1042
|
-
} catch (error) {
|
|
1043
|
-
setFormMessage({ kind: 'error', text: tt('form.error') + errorMessage(error) })
|
|
1044
|
-
} finally {
|
|
1045
|
-
setFormBusy(false)
|
|
1046
|
-
}
|
|
1047
|
-
}, [api, formName, formDesc, formRoot, load])
|
|
1048
|
-
|
|
1049
|
-
// ------------------------------------------------------------- derived
|
|
1050
|
-
|
|
1051
|
-
const normalized = search.trim().toLocaleLowerCase()
|
|
1052
|
-
/** skillName → origin repo(无来源记录的技能不在此映射中,筛选中视为 private)。 */
|
|
1053
|
-
const origins = groupsState?.origins ?? {}
|
|
1054
|
-
/** 来源筛选选项:来源记录中的仓库(排序)+ 末尾的「个人」;没有技能的仓库不列。 */
|
|
1055
|
-
const sourceOptions = useMemo(() => {
|
|
1056
|
-
const skills = catalog?.skills ?? []
|
|
1057
|
-
const repos = [...new Set(skills.map((skill) => origins[skill.name]).filter((repo): repo is string => repo !== undefined))].sort()
|
|
1058
|
-
// 项目级技能(有 workspace 归属)不算「个人」。
|
|
1059
|
-
const hasPrivate = skills.some((skill) => origins[skill.name] === undefined && !isProjectSource(skill.source))
|
|
1060
|
-
return [...repos, ...(hasPrivate ? [PRIVATE_SOURCE] : [])]
|
|
1061
|
-
}, [catalog, origins])
|
|
1062
|
-
const filtered = useMemo(() => (catalog?.skills ?? []).filter((skill) => {
|
|
1063
|
-
if (invocationFilter === 'model' && !skill.invocation.modelInvocable) return false
|
|
1064
|
-
if (invocationFilter === 'user' && !skill.invocation.userInvocable) return false
|
|
1065
|
-
if (normalized.length === 0) return true
|
|
1066
|
-
return skill.name.toLocaleLowerCase().includes(normalized)
|
|
1067
|
-
|| skill.description.toLocaleLowerCase().includes(normalized)
|
|
1068
|
-
|| skill.displayName?.toLocaleLowerCase().includes(normalized)
|
|
1069
|
-
|| skill.shortDescription?.toLocaleLowerCase().includes(normalized)
|
|
1070
|
-
}), [catalog, normalized, invocationFilter])
|
|
1071
|
-
/** Rows actually rendered with a shortened description (shortDescription in use). */
|
|
1072
|
-
const shortenedCount = useMemo(() => filtered.filter((skill) => skill.shortDescription !== undefined).length, [filtered])
|
|
1073
|
-
/** 排序后的技能列表(所有视图共用;调用次数未知按 0 处理)。 */
|
|
1074
|
-
const sorted = useMemo(() => sortSkills(filtered, sortKey, (name) => uses.get(name)?.count), [filtered, sortKey, uses])
|
|
1075
|
-
|
|
1076
|
-
/** Clear the list filters (search + source + invocation) back to the full view. */
|
|
1077
|
-
const clearListFilters = useCallback((): void => {
|
|
1078
|
-
setSearch('')
|
|
1079
|
-
setSourceFilter('all')
|
|
1080
|
-
setInvocationFilter('all')
|
|
1081
|
-
}, [])
|
|
1082
|
-
|
|
1083
141
|
return {
|
|
1084
142
|
// state
|
|
1085
|
-
catalog, loading, loadError, successBanner,
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
143
|
+
catalog: catalogFlow.catalog, loading: catalogFlow.loading, loadError, successBanner,
|
|
144
|
+
updateState: meta.updateState,
|
|
145
|
+
repoDiscoverState: marketFlow.repoDiscoverState, scanningRepo: marketFlow.scanningRepo,
|
|
146
|
+
repoSelected: marketFlow.repoSelected, repoImporting: marketFlow.repoImporting,
|
|
147
|
+
repoResult: marketFlow.repoResult, importJobId: marketFlow.importJobId,
|
|
148
|
+
search: catalogFlow.search, workspace: catalogFlow.workspace,
|
|
149
|
+
detail: catalogFlow.detail, detailLoading: catalogFlow.detailLoading,
|
|
150
|
+
busyNames: catalogFlow.busyNames, batchBusy,
|
|
151
|
+
showForm: catalogFlow.showForm, formName: catalogFlow.formName, formDesc: catalogFlow.formDesc,
|
|
152
|
+
formRoot: catalogFlow.formRoot, formBusy: catalogFlow.formBusy, formMessage: catalogFlow.formMessage,
|
|
153
|
+
uses: meta.uses, hubConfig: meta.hubConfig, tab, skillView, sourceFilter,
|
|
154
|
+
invocationFilter: catalogFlow.invocationFilter, sortKey: catalogFlow.sortKey,
|
|
155
|
+
marketState: marketFlow.marketState, marketCheck: marketFlow.marketCheck,
|
|
156
|
+
marketStats: marketFlow.marketStats, branchChoice: marketFlow.branchChoice, branchBusy: marketFlow.branchBusy,
|
|
157
|
+
marketSyncDialog: marketFlow.marketSyncDialog, syncingMarket: marketFlow.syncingMarket,
|
|
158
|
+
syncBusy: marketFlow.syncBusy, newSourceName: marketFlow.newSourceName,
|
|
159
|
+
groupsState: groupFlow.groupsState, sourcesState: sourceFlow.sourcesState,
|
|
160
|
+
sourceCheck: sourceFlow.sourceCheck, checkingSource: sourceFlow.checkingSource,
|
|
161
|
+
syncingSource: sourceFlow.syncingSource,
|
|
162
|
+
conflictDialog: groupFlow.conflictDialog, confirmDialog: sourceFlow.confirmDialog,
|
|
163
|
+
deleteSkillDialog: sourceFlow.deleteSkillDialog, deleteGroupDialog: sourceFlow.deleteGroupDialog,
|
|
164
|
+
confirmClearTrash: sourceFlow.confirmClearTrash, updateAllDialog: marketFlow.updateAllDialog,
|
|
165
|
+
editingTag: groupFlow.editingTag, editName: groupFlow.editName, membersDraft: groupFlow.membersDraft,
|
|
166
|
+
newTagName: groupFlow.newTagName, tagBusy,
|
|
167
|
+
editSearch: groupFlow.editSearch, collapsedGroups, subdividedProjects, showLegend, editMode,
|
|
168
|
+
versionDialog: marketFlow.versionDialog, versionBusy: marketFlow.versionBusy,
|
|
1091
169
|
// derived
|
|
1092
|
-
actionNames, viewNames
|
|
170
|
+
actionNames: catalogFlow.actionNames, viewNames: catalogFlow.viewNames,
|
|
171
|
+
normalized: catalogFlow.normalized, origins, sourceOptions,
|
|
172
|
+
filtered: catalogFlow.filtered, sorted: catalogFlow.sorted, shortenedCount: catalogFlow.shortenedCount,
|
|
1093
173
|
// actions + setters
|
|
1094
|
-
setLoadError, setSuccessBanner, setSearch, setWorkspace
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
174
|
+
setLoadError, setSuccessBanner, setSearch: catalogFlow.setSearch, setWorkspace: catalogFlow.setWorkspace,
|
|
175
|
+
setDetail: catalogFlow.setDetail, setShowForm: catalogFlow.setShowForm, setFormName: catalogFlow.setFormName,
|
|
176
|
+
setFormDesc: catalogFlow.setFormDesc, setFormRoot: catalogFlow.setFormRoot,
|
|
177
|
+
setFormMessage: catalogFlow.setFormMessage,
|
|
178
|
+
setRepoSelected: marketFlow.setRepoSelected, setTab, setSkillView,
|
|
179
|
+
setSourceFilter, setInvocationFilter: catalogFlow.setInvocationFilter, setSortKey: catalogFlow.setSortKey,
|
|
180
|
+
setBranchChoice: marketFlow.setBranchChoice, setMarketSyncDialog: marketFlow.setMarketSyncDialog,
|
|
181
|
+
setNewSourceName: marketFlow.setNewSourceName, setConflictDialog: groupFlow.setConflictDialog,
|
|
182
|
+
setConfirmDialog: sourceFlow.setConfirmDialog,
|
|
183
|
+
setDeleteSkillDialog: sourceFlow.setDeleteSkillDialog, setDeleteGroupDialog: sourceFlow.setDeleteGroupDialog,
|
|
184
|
+
setConfirmClearTrash: sourceFlow.setConfirmClearTrash, setUpdateAllDialog: marketFlow.setUpdateAllDialog,
|
|
185
|
+
setEditingTag: groupFlow.setEditingTag, setEditName: groupFlow.setEditName,
|
|
186
|
+
setMembersDraft: groupFlow.setMembersDraft, setNewTagName: groupFlow.setNewTagName,
|
|
187
|
+
setEditSearch: groupFlow.setEditSearch, setShowLegend, setEditMode,
|
|
188
|
+
setVersionDialog: marketFlow.setVersionDialog,
|
|
189
|
+
toggleGroupCollapse, toggleSubdivide, checkUpdate: meta.checkUpdate, loadMarket: marketFlow.loadMarket,
|
|
190
|
+
openDetail: catalogFlow.openDetail, toggle: catalogFlow.toggle, enableDisabled: catalogFlow.enableDisabled,
|
|
191
|
+
batchToggleNames: catalogFlow.batchToggleNames, toggleGroup: groupFlow.toggleGroup,
|
|
192
|
+
resolveConflict: groupFlow.resolveConflict,
|
|
193
|
+
runConfirmed: sourceFlow.runConfirmed, checkSources: sourceFlow.checkSources,
|
|
194
|
+
requestSync: sourceFlow.requestSync, requestDelete: sourceFlow.requestDelete,
|
|
195
|
+
restoreTrash: sourceFlow.restoreTrash, clearTrash: sourceFlow.clearTrash,
|
|
196
|
+
fixingPaths: catalogFlow.fixingPaths, fixDiagnostic: catalogFlow.fixDiagnostic,
|
|
197
|
+
clearListFilters: catalogFlow.clearListFilters, openVersionDialog: marketFlow.openVersionDialog,
|
|
198
|
+
confirmVersionDialog: marketFlow.confirmVersionDialog, requestDeleteSkill: sourceFlow.requestDeleteSkill,
|
|
199
|
+
runDeleteSkill: sourceFlow.runDeleteSkill, requestDeleteGroup: sourceFlow.requestDeleteGroup,
|
|
200
|
+
runDeleteGroup: sourceFlow.runDeleteGroup, createTag: groupFlow.createTag,
|
|
201
|
+
deleteTag: groupFlow.deleteTag, saveTag: groupFlow.saveTag, reorderTags: groupFlow.reorderTags,
|
|
202
|
+
reorderCollections: groupFlow.reorderCollections, reorderSourceGroups: groupFlow.reorderSourceGroups,
|
|
203
|
+
addSource: marketFlow.addSource, addMarketSource: marketFlow.addMarketSource,
|
|
204
|
+
removeMarketSource: marketFlow.removeMarketSource, scanRepo: marketFlow.scanRepo,
|
|
205
|
+
confirmBranchChoice: marketFlow.confirmBranchChoice, toggleRepoSelected: marketFlow.toggleRepoSelected,
|
|
206
|
+
importRepo: marketFlow.importRepo, cancelImport: marketFlow.cancelImport,
|
|
207
|
+
clearScan: marketFlow.clearScan,
|
|
208
|
+
checkMarket: marketFlow.checkMarket, loadMarketStats: marketFlow.loadMarketStats,
|
|
209
|
+
syncMarketSource: marketFlow.syncMarketSource, confirmMarketSync: marketFlow.confirmMarketSync,
|
|
210
|
+
updateAll: marketFlow.updateAll, create: catalogFlow.create,
|
|
1102
211
|
}
|
|
1103
212
|
}
|