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.
Files changed (109) hide show
  1. package/README.md +40 -50
  2. package/README.zh.md +38 -48
  3. package/lib/client.js +2209 -1820
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +653 -419
  6. package/lib/types/client/SkillHubSettingsCard.d.ts +1 -0
  7. package/lib/types/client/api.d.ts +12 -6
  8. package/lib/types/client/helpers.d.ts +0 -1
  9. package/lib/types/client/locales/common.d.ts +52 -0
  10. package/lib/types/client/locales/detail.d.ts +16 -0
  11. package/lib/types/client/locales/market.d.ts +73 -0
  12. package/lib/types/client/locales/settings.d.ts +41 -0
  13. package/lib/types/client/locales/skills.d.ts +65 -0
  14. package/lib/types/client/locales/sources.d.ts +35 -0
  15. package/lib/types/client/locales.d.ts +124 -118
  16. package/lib/types/client/panel/MarketSourceRow.d.ts +12 -0
  17. package/lib/types/client/panel/MarketView.d.ts +1 -0
  18. package/lib/types/client/panel/RepoScanCard.d.ts +10 -0
  19. package/lib/types/client/panel/hooks/shared.d.ts +72 -0
  20. package/lib/types/client/panel/hooks/useCatalogFlow.d.ts +62 -0
  21. package/lib/types/client/panel/hooks/useGroupFlow.d.ts +41 -0
  22. package/lib/types/client/panel/hooks/useMarketFlow.d.ts +73 -0
  23. package/lib/types/client/panel/hooks/useMetaFlow.d.ts +18 -0
  24. package/lib/types/client/panel/hooks/useSourceFlow.d.ts +44 -0
  25. package/lib/types/client/panel/market-format.d.ts +6 -0
  26. package/lib/types/client/panel/useSkillHub.d.ts +45 -73
  27. package/lib/types/client/settings-card.d.ts +20 -0
  28. package/lib/types/client/settings-form.d.ts +6 -0
  29. package/lib/types/protocol/api.d.ts +38 -0
  30. package/lib/types/protocol/catalog.d.ts +155 -0
  31. package/lib/types/protocol/config.d.ts +113 -0
  32. package/lib/types/protocol/groups.d.ts +95 -0
  33. package/lib/types/protocol/market.d.ts +94 -0
  34. package/lib/types/protocol/repo.d.ts +107 -0
  35. package/lib/types/protocol/sources.d.ts +139 -0
  36. package/lib/types/protocol/stats.d.ts +46 -0
  37. package/lib/types/protocol.d.ts +13 -743
  38. package/lib/types/repo/github-client.d.ts +42 -0
  39. package/lib/types/repo.d.ts +6 -17
  40. package/lib/types/routes/catalog.d.ts +7 -0
  41. package/lib/types/routes/config.d.ts +7 -0
  42. package/lib/types/routes/groups.d.ts +7 -0
  43. package/lib/types/routes/helpers.d.ts +145 -0
  44. package/lib/types/routes/market.d.ts +8 -0
  45. package/lib/types/routes/repo-import.d.ts +7 -0
  46. package/lib/types/routes/route-state.d.ts +55 -0
  47. package/lib/types/routes/sources.d.ts +8 -0
  48. package/lib/types/routes.d.ts +5 -34
  49. package/lib/types/stats.d.ts +3 -2
  50. package/lib/types/store/errors.d.ts +10 -0
  51. package/lib/types/store/migrate.d.ts +44 -0
  52. package/lib/types/store/paths.d.ts +32 -0
  53. package/lib/types/store/store.d.ts +133 -0
  54. package/lib/types/store.d.ts +7 -146
  55. package/package.json +28 -27
  56. package/src/client/SkillHubSettingsCard.tsx +17 -3
  57. package/src/client/api.ts +101 -195
  58. package/src/client/helpers.ts +10 -1
  59. package/src/client/locales/common.ts +102 -0
  60. package/src/client/locales/detail.ts +30 -0
  61. package/src/client/locales/market.ts +144 -0
  62. package/src/client/locales/settings.ts +80 -0
  63. package/src/client/locales/skills.ts +128 -0
  64. package/src/client/locales/sources.ts +68 -0
  65. package/src/client/locales.ts +26 -512
  66. package/src/client/panel/MarketSourceRow.tsx +108 -0
  67. package/src/client/panel/MarketView.tsx +12 -253
  68. package/src/client/panel/RepoScanCard.tsx +171 -0
  69. package/src/client/panel/hooks/shared.ts +81 -0
  70. package/src/client/panel/hooks/useCatalogFlow.ts +211 -0
  71. package/src/client/panel/hooks/useGroupFlow.ts +240 -0
  72. package/src/client/panel/hooks/useMarketFlow.ts +421 -0
  73. package/src/client/panel/hooks/useMetaFlow.ts +58 -0
  74. package/src/client/panel/hooks/useSourceFlow.ts +193 -0
  75. package/src/client/panel/market-format.ts +22 -0
  76. package/src/client/panel/useSkillHub.ts +141 -1032
  77. package/src/client/settings-card.tsx +51 -0
  78. package/src/client/settings-form.ts +18 -0
  79. package/src/index.ts +14 -4
  80. package/src/protocol/api.ts +38 -0
  81. package/src/protocol/catalog.ts +168 -0
  82. package/src/protocol/config.ts +145 -0
  83. package/src/protocol/groups.ts +106 -0
  84. package/src/protocol/market.ts +100 -0
  85. package/src/protocol/repo.ts +104 -0
  86. package/src/protocol/sources.ts +146 -0
  87. package/src/protocol/stats.ts +42 -0
  88. package/src/protocol.ts +83 -809
  89. package/src/repo/github-client.ts +131 -0
  90. package/src/repo.test.ts +24 -1
  91. package/src/repo.ts +50 -170
  92. package/src/routes/catalog.ts +309 -0
  93. package/src/routes/config.ts +119 -0
  94. package/src/routes/groups.ts +133 -0
  95. package/src/routes/helpers.ts +489 -0
  96. package/src/routes/market.ts +255 -0
  97. package/src/routes/repo-import.ts +267 -0
  98. package/src/routes/route-state.ts +89 -0
  99. package/src/routes/sources.ts +318 -0
  100. package/src/routes.test.ts +20 -0
  101. package/src/routes.ts +21 -1699
  102. package/src/stats.test.ts +58 -3
  103. package/src/stats.ts +45 -16
  104. package/src/store/errors.ts +14 -0
  105. package/src/store/migrate.ts +272 -0
  106. package/src/store/paths.ts +43 -0
  107. package/src/store/store.ts +532 -0
  108. package/src/store.test.ts +18 -0
  109. package/src/store.ts +7 -737
package/lib/client.js CHANGED
@@ -6,13 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
- //#region src/protocol.ts
10
- /**
11
- * Shared API contract for dsh-skill-hub: the route paths and payload shapes
12
- * both the host half and the browser half import. The browser half must never
13
- * depend on host SDK packages, so registry types are re-spelled here as
14
- * plain JSON-safe interfaces.
15
- */
9
+ //#region src/protocol/api.ts
16
10
  /** Browser-facing base paths of the skill-hub API family. */
17
11
  const SKILL_HUB_API = {
18
12
  catalog: "/api/skill-hub/catalog",
@@ -48,8 +42,11 @@ window.__ModuleLoader__.load({
48
42
  sourceRestore: "/api/skill-hub/sources/restore",
49
43
  sourceTrashClear: "/api/skill-hub/sources/trash/clear",
50
44
  diagnosticFix: "/api/skill-hub/diagnostic/fix",
51
- marketSourceVersions: "/api/skill-hub/market/source/versions"
45
+ marketSourceVersions: "/api/skill-hub/market/source/versions",
46
+ marketStats: "/api/skill-hub/market/stats"
52
47
  };
48
+ //#endregion
49
+ //#region src/protocol/sources.ts
53
50
  /** 项目级技能来源(它们有 workspace 归属,不属于「个人」组)。 */
54
51
  function isProjectSource(source) {
55
52
  return source === "project-dsh" || source === "project-agents";
@@ -97,270 +94,196 @@ window.__ModuleLoader__.load({
97
94
  }
98
95
  /** The browser half's only data entry point. */
99
96
  var SkillHubApi = class {
97
+ /** One GET round trip (query already encoded by the caller). */
98
+ async get(path, query = "", ms) {
99
+ return readJson(await fetchWithTimeout(path + query, void 0, ms));
100
+ }
101
+ /** One POST round trip (body omitted for routes that take none). */
102
+ async post(path, body, ms) {
103
+ return readJson(await fetchWithTimeout(path, body !== void 0 ? {
104
+ method: "POST",
105
+ headers: { "content-type": "application/json" },
106
+ body: JSON.stringify(body)
107
+ } : { method: "POST" }, ms));
108
+ }
100
109
  /** Catalog lookup options (cwd selects a workspace's project skills). */
101
110
  catalog(options) {
102
- return fetchWithTimeout(options?.cwd !== void 0 && options.cwd !== "" ? SKILL_HUB_API.catalog + "?cwd=" + encodeURIComponent(options.cwd) : SKILL_HUB_API.catalog).then((response) => readJson(response));
111
+ const query = options?.cwd !== void 0 && options.cwd !== "" ? "?cwd=" + encodeURIComponent(options.cwd) : "";
112
+ return this.get(SKILL_HUB_API.catalog, query);
103
113
  }
104
114
  /** One skill's detail (cwd selects a workspace's project skills). */
105
115
  async skill(name, options) {
106
116
  const cwd = options?.cwd !== void 0 && options.cwd !== "" ? "&cwd=" + encodeURIComponent(options.cwd) : "";
107
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.skill + "?name=" + encodeURIComponent(name) + cwd))).skill;
117
+ return (await this.get(SKILL_HUB_API.skill, "?name=" + encodeURIComponent(name) + cwd)).skill;
108
118
  }
109
119
  /** Move one writable skill into the restorable trash. */
110
- async deleteSkill(name) {
111
- return readJson(await fetchWithTimeout(SKILL_HUB_API.skillDelete, {
112
- method: "POST",
113
- headers: { "content-type": "application/json" },
114
- body: JSON.stringify({ name })
115
- }));
120
+ deleteSkill(name) {
121
+ return this.post(SKILL_HUB_API.skillDelete, { name });
116
122
  }
117
123
  /** Toggle one skill; resolves with the fresh catalog from the route. */
118
124
  async toggle(name, enabled) {
119
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.toggle, {
120
- method: "POST",
121
- headers: { "content-type": "application/json" },
122
- body: JSON.stringify({
123
- name,
124
- enabled
125
- })
126
- }))).catalog;
125
+ return (await this.post(SKILL_HUB_API.toggle, {
126
+ name,
127
+ enabled
128
+ })).catalog;
127
129
  }
128
- async create(payload) {
129
- return readJson(await fetchWithTimeout(SKILL_HUB_API.create, {
130
- method: "POST",
131
- headers: { "content-type": "application/json" },
132
- body: JSON.stringify(payload)
133
- }));
130
+ create(payload) {
131
+ return this.post(SKILL_HUB_API.create, payload);
134
132
  }
135
- async stats() {
136
- return readJson(await fetchWithTimeout(SKILL_HUB_API.stats));
133
+ stats() {
134
+ return this.get(SKILL_HUB_API.stats);
137
135
  }
138
136
  /** Toggle a whole group in one write; resolves with the fresh catalog + failures. */
139
- async toggleBatch(names, enabled) {
137
+ toggleBatch(names, enabled) {
140
138
  const payload = {
141
139
  names,
142
140
  enabled
143
141
  };
144
- return readJson(await fetchWithTimeout(SKILL_HUB_API.toggleBatch, {
145
- method: "POST",
146
- headers: { "content-type": "application/json" },
147
- body: JSON.stringify(payload)
148
- }));
142
+ return this.post(SKILL_HUB_API.toggleBatch, payload);
149
143
  }
150
144
  /** The user's added market sources. */
151
- async market() {
152
- return readJson(await fetchWithTimeout(SKILL_HUB_API.market));
145
+ market() {
146
+ return this.get(SKILL_HUB_API.market);
153
147
  }
154
148
  /** Add a market source repo; resolves with the fresh list. */
155
- async addMarketSource(repo) {
156
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSource, {
157
- method: "POST",
158
- headers: { "content-type": "application/json" },
159
- body: JSON.stringify({ repo })
160
- }));
149
+ addMarketSource(repo) {
150
+ return this.post(SKILL_HUB_API.marketSource, { repo });
161
151
  }
162
152
  /** Remove a market source repo; resolves with the fresh list. */
163
- async removeMarketSource(repo) {
164
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceDelete, {
165
- method: "POST",
166
- headers: { "content-type": "application/json" },
167
- body: JSON.stringify({ repo })
168
- }));
153
+ removeMarketSource(repo) {
154
+ return this.post(SKILL_HUB_API.marketSourceDelete, { repo });
155
+ }
156
+ /** Stars + downloads per market source (hourly server-side cache; refresh=1 refetches stale entries). */
157
+ marketStats(refresh = false) {
158
+ const query = refresh ? "?refresh=1" : "";
159
+ return this.get(SKILL_HUB_API.marketStats, query, refresh ? 6e4 : void 0);
169
160
  }
170
161
  /** Check every market source for updates (throttled server-side). */
171
- async marketCheck() {
172
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketCheck));
162
+ marketCheck() {
163
+ return this.get(SKILL_HUB_API.marketCheck);
173
164
  }
174
165
  /** Align a market source to its pinned ref; resolves with tracked skills. */
175
- async marketSync(repo) {
176
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSync, {
177
- method: "POST",
178
- headers: { "content-type": "application/json" },
179
- body: JSON.stringify({ repo })
180
- }));
166
+ marketSync(repo) {
167
+ return this.post(SKILL_HUB_API.marketSync, { repo });
181
168
  }
182
169
  /** Releases + branches of a market source for the version picker. */
183
- async marketSourceVersions(repo) {
184
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceVersions + "?repo=" + encodeURIComponent(repo)));
170
+ marketSourceVersions(repo) {
171
+ return this.get(SKILL_HUB_API.marketSourceVersions, "?repo=" + encodeURIComponent(repo));
185
172
  }
186
173
  /** Pin a market source to an explicit ref (branch picker). */
187
- async setMarketSourceRef(repo, ref) {
188
- return readJson(await fetchWithTimeout(SKILL_HUB_API.marketSourceRef, {
189
- method: "POST",
190
- headers: { "content-type": "application/json" },
191
- body: JSON.stringify({
192
- repo,
193
- ref
194
- })
195
- }));
174
+ setMarketSourceRef(repo, ref) {
175
+ return this.post(SKILL_HUB_API.marketSourceRef, {
176
+ repo,
177
+ ref
178
+ });
196
179
  }
197
180
  /** Check the hub's own latest GitHub release. */
198
- async updateCheck() {
199
- return readJson(await fetchWithTimeout(SKILL_HUB_API.update));
181
+ updateCheck() {
182
+ return this.get(SKILL_HUB_API.update);
200
183
  }
201
184
  /** Discover importable skills in a GitHub repo. */
202
- async repoDiscover(repo) {
203
- return readJson(await fetchWithTimeout(SKILL_HUB_API.repo + "?repo=" + encodeURIComponent(repo)));
185
+ repoDiscover(repo) {
186
+ return this.get(SKILL_HUB_API.repo, "?repo=" + encodeURIComponent(repo));
204
187
  }
205
188
  /** Install selected repo skills into the user-dsh root (records the source). Returns jobId for polling. */
206
- async repoImport(repo, paths, ref) {
189
+ repoImport(repo, paths, ref) {
207
190
  const payload = {
208
191
  repo,
209
192
  paths,
210
193
  ...ref !== void 0 && ref !== "" ? { ref } : {}
211
194
  };
212
- return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImport, {
213
- method: "POST",
214
- headers: { "content-type": "application/json" },
215
- body: JSON.stringify(payload)
216
- }));
195
+ return this.post(SKILL_HUB_API.repoImport, payload);
217
196
  }
218
197
  /** Poll import job progress (B方案轮询) */
219
- async repoImportProgress(jobId) {
220
- return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImportProgress + "?jobId=" + encodeURIComponent(jobId)));
198
+ repoImportProgress(jobId) {
199
+ return this.get(SKILL_HUB_API.repoImportProgress, "?jobId=" + encodeURIComponent(jobId));
221
200
  }
222
201
  /** Cancel a running import job (选项2:唯有取消才停) */
223
- async repoImportCancel(jobId) {
224
- return readJson(await fetchWithTimeout(SKILL_HUB_API.repoImportCancel, {
225
- method: "POST",
226
- headers: { "content-type": "application/json" },
227
- body: JSON.stringify({ jobId })
228
- }));
202
+ repoImportCancel(jobId) {
203
+ return this.post(SKILL_HUB_API.repoImportCancel, { jobId });
229
204
  }
230
205
  /** Read the hub's runtime config (effective values + saved overrides). */
231
- async config() {
232
- return readJson(await fetchWithTimeout(SKILL_HUB_API.config));
206
+ config() {
207
+ return this.get(SKILL_HUB_API.config);
233
208
  }
234
209
  /** Patch the hub's runtime config (null clears a saved override). */
235
- async saveConfig(patch) {
236
- return readJson(await fetchWithTimeout(SKILL_HUB_API.config, {
237
- method: "POST",
238
- headers: { "content-type": "application/json" },
239
- body: JSON.stringify(patch)
240
- }));
210
+ saveConfig(patch) {
211
+ return this.post(SKILL_HUB_API.config, patch);
241
212
  }
242
213
  /** 用户 tag 分组 + 系统集合组 + origin 映射。 */
243
- async groups() {
244
- return readJson(await fetchWithTimeout(SKILL_HUB_API.groups));
214
+ groups() {
215
+ return this.get(SKILL_HUB_API.groups);
245
216
  }
246
217
  /** 新建(无 id)或重命名(带 id)一个 tag 分组;返回全部 tag。 */
247
218
  async saveTag(payload) {
248
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tag, {
249
- method: "POST",
250
- headers: { "content-type": "application/json" },
251
- body: JSON.stringify(payload)
252
- }))).tags;
219
+ return (await this.post(SKILL_HUB_API.tag, payload)).tags;
253
220
  }
254
221
  /** 删除一个 tag 分组;返回全部 tag。 */
255
222
  async deleteTag(id) {
256
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagDelete, {
257
- method: "POST",
258
- headers: { "content-type": "application/json" },
259
- body: JSON.stringify({ id })
260
- }))).tags;
223
+ return (await this.post(SKILL_HUB_API.tagDelete, { id })).tags;
261
224
  }
262
225
  /** 直接设置某 tag 的完整成员列表;返回全部 tag。 */
263
226
  async setTagMembers(id, skillNames) {
264
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagMembers, {
265
- method: "POST",
266
- headers: { "content-type": "application/json" },
267
- body: JSON.stringify({
268
- id,
269
- skillNames
270
- })
271
- }))).tags;
227
+ return (await this.post(SKILL_HUB_API.tagMembers, {
228
+ id,
229
+ skillNames
230
+ })).tags;
272
231
  }
273
232
  /** 拖拽重排场景分组 */
274
233
  async reorderTags(orderedIds) {
275
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.tagReorder, {
276
- method: "POST",
277
- headers: { "content-type": "application/json" },
278
- body: JSON.stringify({ orderedIds })
279
- }))).tags;
234
+ return (await this.post(SKILL_HUB_API.tagReorder, { orderedIds })).tags;
280
235
  }
281
236
  /** 拖拽重排来源集合 */
282
237
  async reorderCollections(orderedNames) {
283
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.collectionReorder, {
284
- method: "POST",
285
- headers: { "content-type": "application/json" },
286
- body: JSON.stringify({ orderedNames })
287
- }))).collections;
238
+ return (await this.post(SKILL_HUB_API.collectionReorder, { orderedNames })).collections;
288
239
  }
289
240
  /** 拖拽重排来源顶层分组(project / collections / personal) */
290
241
  async reorderSourceGroups(orderedKeys) {
291
- return (await readJson(await fetchWithTimeout(SKILL_HUB_API.sourceGroupReorder, {
292
- method: "POST",
293
- headers: { "content-type": "application/json" },
294
- body: JSON.stringify({ orderedKeys })
295
- }))).order;
242
+ return (await this.post(SKILL_HUB_API.sourceGroupReorder, { orderedKeys })).order;
296
243
  }
297
244
  /** 来源列表 + 派生 origin 映射 + 集合组 + 回收站。 */
298
- async sources() {
299
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sources));
245
+ sources() {
246
+ return this.get(SKILL_HUB_API.sources);
300
247
  }
301
248
  /** 检查指定(或全部)来源的上游更新。 */
302
- async checkSources(repo) {
249
+ checkSources(repo) {
303
250
  const payload = repo !== void 0 ? { repo } : {};
304
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceCheck, {
305
- method: "POST",
306
- headers: { "content-type": "application/json" },
307
- body: JSON.stringify(payload)
308
- }));
251
+ return this.post(SKILL_HUB_API.sourceCheck, payload);
309
252
  }
310
253
  /** 同步一个来源的所选(或全部)技能到上游最新版本。 */
311
- async syncSource(repo, skills) {
254
+ syncSource(repo, skills) {
312
255
  const payload = skills !== void 0 ? {
313
256
  repo,
314
257
  skills
315
258
  } : { repo };
316
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceSync, {
317
- method: "POST",
318
- headers: { "content-type": "application/json" },
319
- body: JSON.stringify(payload)
320
- }));
259
+ return this.post(SKILL_HUB_API.sourceSync, payload);
321
260
  }
322
261
  /** 跟进上游删除:把所选技能移入回收站。 */
323
- async confirmDeleteSource(repo, skills) {
262
+ confirmDeleteSource(repo, skills) {
324
263
  const payload = {
325
264
  repo,
326
265
  skills
327
266
  };
328
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceDelete, {
329
- method: "POST",
330
- headers: { "content-type": "application/json" },
331
- body: JSON.stringify(payload)
332
- }));
267
+ return this.post(SKILL_HUB_API.sourceDelete, payload);
333
268
  }
334
269
  /** 从回收站恢复一个技能。 */
335
- async restoreSource(name) {
270
+ restoreSource(name) {
336
271
  const payload = { name };
337
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceRestore, {
338
- method: "POST",
339
- headers: { "content-type": "application/json" },
340
- body: JSON.stringify(payload)
341
- }));
272
+ return this.post(SKILL_HUB_API.sourceRestore, payload);
342
273
  }
343
274
  /** Permanently delete every skill currently in the trash. */
344
- async clearTrash() {
345
- return readJson(await fetchWithTimeout(SKILL_HUB_API.sourceTrashClear, { method: "POST" }));
275
+ clearTrash() {
276
+ return this.post(SKILL_HUB_API.sourceTrashClear);
346
277
  }
347
278
  /** Auto-fix a fixable diagnostic (e.g. unquoted colon). */
348
- async fixDiagnostic(path) {
349
- return readJson(await fetchWithTimeout(SKILL_HUB_API.diagnosticFix, {
350
- method: "POST",
351
- headers: { "content-type": "application/json" },
352
- body: JSON.stringify({ path })
353
- }));
279
+ fixDiagnostic(path) {
280
+ return this.post(SKILL_HUB_API.diagnosticFix, { path });
354
281
  }
355
282
  };
356
283
  //#endregion
357
- //#region src/client/locales.ts
358
- /**
359
- * dsh-skill-hub surface copy (zh/en). The dictionary type is re-exported
360
- * so the LocaleNamespaceMap module augmentation in client/index.ts can
361
- * reference it without a value import.
362
- */
363
- const zh = {
284
+ //#region src/client/locales/common.ts
285
+ /** 面板框架与通用:入口/视图切换/面板/图例/报错/更新检查/新建表单/诊断修复。从 locales.ts 按前缀拆出,键集合不变。 */
286
+ const zhCommon = {
364
287
  "entry.label": "技能",
365
288
  "entry.tooltip": "技能中枢:浏览、分组与同步管理本地技能",
366
289
  "view.sources": "来源",
@@ -386,6 +309,115 @@ window.__ModuleLoader__.load({
386
309
  "panel.workspacePlaceholder": "工作区路径(回车应用)",
387
310
  "panel.workspaceHint": "填写项目路径后,面板同时显示该项目 .dsh/skills 与 .agents/skills 下的技能(只读);留空回车恢复用户级视图。",
388
311
  "panel.workspaceClear": "清除工作区",
312
+ "update.check": "检查更新",
313
+ "update.checking": "检查中…",
314
+ "update.upToDate": "已是最新版本 v{version}",
315
+ "update.available": "发现新版本 v{version}(当前 v{current})",
316
+ "update.newVersion": "新版本 v{version}",
317
+ "update.viewRelease": "查看发布",
318
+ "update.error": "检查更新失败:{error}",
319
+ "update.unavailable": "暂无可用的发布版本",
320
+ "diag.fix": "修复",
321
+ "diag.fixing": "修复中…",
322
+ "diag.fixed": "已修复",
323
+ "diag.fixConfirm": "用自动修复重写以下文件的 frontmatter?建议先备份:{path}",
324
+ "form.capabilityHint": "能力怎么选:重复方法 → 技能;外部系统 → MCP;长期资料 → 知识库。",
325
+ "form.descPlaceholder": "一句话说明用途(含触发词),Agent 靠它自动激活技能",
326
+ "form.name": "技能名(kebab-case,如 code-review)",
327
+ "form.desc": "描述(一句话路由说明)",
328
+ "form.root": "目标目录",
329
+ "form.submit": "创建",
330
+ "form.cancel": "取消",
331
+ "form.busy": "创建中…",
332
+ "form.success": "已创建:",
333
+ "form.error": "创建失败:",
334
+ "err.dismiss": "知道了"
335
+ };
336
+ const enCommon = {
337
+ "entry.label": "Skills",
338
+ "entry.tooltip": "Skill hub: browse, group, and sync local skills",
339
+ "view.sources": "Sources",
340
+ "view.scenes": "Scenes",
341
+ "view.market": "Market",
342
+ "view.flat": "Flat",
343
+ "view.grouped": "Grouped",
344
+ "panel.title": "Skill Hub",
345
+ "panel.count": "{count} skills total",
346
+ "panel.disabledCount": "{count} disabled",
347
+ "panel.new": "New Skill",
348
+ "panel.search": "Search skills…",
349
+ "legend.model": "Model-invocable — agents may call automatically",
350
+ "legend.user": "User-invocable — explicit invocation only",
351
+ "legend.hint": "At most one dot per skill; no dot = not invocable.",
352
+ "panel.enabled": "Enabled",
353
+ "panel.disabled": "Disabled",
354
+ "panel.diagnostics": "Discovery diagnostics",
355
+ "panel.empty": "No matching skills.",
356
+ "panel.emptyAll": "No skills installed yet. Create the first one.",
357
+ "panel.loading": "Loading…",
358
+ "panel.incomplete": "(catalog may be incomplete)",
359
+ "panel.workspacePlaceholder": "Workspace path (Enter to apply)",
360
+ "panel.workspaceHint": "Enter a project path to also show that workspace's .dsh/skills and .agents/skills skills (read-only); clear + Enter to return to the user-level view.",
361
+ "panel.workspaceClear": "Clear workspace",
362
+ "update.check": "Check for updates",
363
+ "update.checking": "Checking…",
364
+ "update.upToDate": "Up to date: v{version}",
365
+ "update.available": "Update available: v{version} (current v{current})",
366
+ "update.newVersion": "New version v{version}",
367
+ "update.viewRelease": "View release",
368
+ "update.error": "Update check failed: {error}",
369
+ "update.unavailable": "No release available",
370
+ "diag.fix": "Fix",
371
+ "diag.fixing": "Fixing…",
372
+ "diag.fixed": "Fixed",
373
+ "diag.fixConfirm": "Rewrite the frontmatter of this file with the auto-fix? Back it up first: {path}",
374
+ "form.capabilityHint": "Choosing a capability: repeatable method → skill; external system → MCP; long-term knowledge → knowledge base.",
375
+ "form.descPlaceholder": "One line on what it does (with trigger words) — agents auto-activate from this",
376
+ "form.name": "Skill name (kebab-case, e.g. code-review)",
377
+ "form.desc": "Description (one routing sentence)",
378
+ "form.root": "Target root",
379
+ "form.submit": "Create",
380
+ "form.cancel": "Cancel",
381
+ "form.busy": "Creating…",
382
+ "form.success": "Created: ",
383
+ "form.error": "Create failed: ",
384
+ "err.dismiss": "Dismiss"
385
+ };
386
+ //#endregion
387
+ //#region src/client/locales/detail.ts
388
+ /** 技能详情。从 locales.ts 按前缀拆出,键集合不变。 */
389
+ const zhDetail = {
390
+ "detail.copyMention": "复制 $mention",
391
+ "detail.copyPath": "复制路径",
392
+ "detail.copied": "已复制",
393
+ "detail.back": "返回列表",
394
+ "detail.provider": "提供者",
395
+ "detail.addedAt": "添加时间",
396
+ "detail.updatedAt": "更新时间",
397
+ "detail.path": "路径",
398
+ "detail.whenToUse": "适用场景",
399
+ "detail.uses": "调用次数",
400
+ "detail.groups": "所属场景",
401
+ "detail.loading": "读取技能正文…"
402
+ };
403
+ const enDetail = {
404
+ "detail.copyMention": "Copy $mention",
405
+ "detail.copyPath": "Copy path",
406
+ "detail.copied": "Copied",
407
+ "detail.back": "Back to list",
408
+ "detail.provider": "Provider",
409
+ "detail.addedAt": "Added",
410
+ "detail.updatedAt": "Updated",
411
+ "detail.path": "Path",
412
+ "detail.whenToUse": "When to use",
413
+ "detail.uses": "Uses",
414
+ "detail.groups": "Groups",
415
+ "detail.loading": "Loading skill body…"
416
+ };
417
+ //#endregion
418
+ //#region src/client/locales/market.ts
419
+ /** 市场与仓库扫描导入。从 locales.ts 按前缀拆出,键集合不变。 */
420
+ const zhMarket = {
389
421
  "market.addPlaceholder": "owner/repo 或 https://github.com/…",
390
422
  "market.addSource": "添加源",
391
423
  "market.noSources": "还没有市场源。从下方内置市场添加,或输入一个 GitHub 仓库。",
@@ -430,6 +462,8 @@ window.__ModuleLoader__.load({
430
462
  "market.versionConfirm": "切换到 {ref}",
431
463
  "market.versionHint": "点击切换跟踪版本",
432
464
  "market.unpinned": "未定版",
465
+ "market.starsHint": "GitHub 星标 {count} 个",
466
+ "market.downloadsHint": "发布附件累计下载 {count} 次",
433
467
  "market.syncTitle": "同步本地技能",
434
468
  "market.syncText": "市场源已对齐到 {ref},要同步更新以下本地技能吗?",
435
469
  "market.syncNone": "该来源还没有导入本地技能。",
@@ -452,195 +486,9 @@ window.__ModuleLoader__.load({
452
486
  "repo.importingCurrent": "正在导入: {name} ({done}/{total})",
453
487
  "repo.root.skills": "技能 skills",
454
488
  "repo.root.designTemplates": "模板 design-templates",
455
- "repo.root.generic": "{root}",
456
- "update.check": "检查更新",
457
- "update.checking": "检查中…",
458
- "update.upToDate": "已是最新版本 v{version}",
459
- "update.available": "发现新版本 v{version}(当前 v{current})",
460
- "update.newVersion": "新版本 v{version}",
461
- "update.viewRelease": "查看发布",
462
- "update.error": "检查更新失败:{error}",
463
- "update.unavailable": "暂无可用的发布版本",
464
- "group.project-dsh": "项目 .dsh/skills",
465
- "group.project-agents": "项目 .agents/skills",
466
- "group.custom": "自定义目录",
467
- "group.runtime": "运行时",
468
- "group.user-dsh": "用户 ~/.dsh/skills",
469
- "group.user-agents": "用户 ~/.agents/skills",
470
- "group.bundled": "内置",
471
- "group.other": "其他来源",
472
- "groups.new": "新建场景",
473
- "groups.namePlaceholder": "场景名",
474
- "groups.edit": "编辑",
475
- "groups.rename": "保存名称",
476
- "groups.delete": "删除场景",
477
- "groups.saveMembers": "保存成员",
478
- "groups.membersHint": "勾选技能加入此场景(含已禁用)。",
479
- "groups.empty": "还没有场景。先新建一个,再把技能归类。",
480
- "groups.noCollections": "暂无来源组。从市场导入技能后自动按来源聚合。",
481
- "groups.personal": "个人",
482
- "groups.project": "项目级",
483
- "groups.subdivide": "细分",
484
- "groups.merge": "合并",
485
- "groups.noWritable": "该组没有可写技能(只读技能无法由中枢开关)",
486
- "groups.closeAll": "全部关闭",
487
- "groups.keepOn": "保留开启",
488
- "groups.conflictTitle": "关闭此组?",
489
- "groups.conflictText": "以下技能也在其他组中开启,关闭它们会影响那些组:",
490
- "filter.allSources": "全部来源",
491
- "filter.private": "个人",
492
- "sort.name": "按名称",
493
- "sort.added": "按添加时间",
494
- "sort.uses": "按调用次数",
495
- "time.justNow": "刚刚",
496
- "time.minutesAgo": "{value} 分钟前",
497
- "time.hoursAgo": "{value} 小时前",
498
- "time.daysAgo": "{value} 天前",
499
- "time.weeksAgo": "{value} 周前",
500
- "badge.source.project-dsh": "项目 .dsh",
501
- "badge.source.project-agents": "项目 .agents",
502
- "badge.source.custom": "自定义",
503
- "badge.source.runtime": "运行时",
504
- "badge.source.user-dsh": "用户 .dsh",
505
- "badge.source.user-agents": "用户 .agents",
506
- "badge.source.bundled": "内置",
507
- "row.readonly": "只读",
508
- "row.disable": "禁用",
509
- "row.enable": "启用",
510
- "row.delete": "移入回收站",
511
- "row.open": "查看 {name} 详情(回车)",
512
- "row.duplicate": "重名",
513
- "row.duplicateHint": "同名技能存在多个来源,当前显示为优先级最高的版本",
514
- "row.copy": "复制",
515
- "row.copyMention": "复制 $mention",
516
- "row.copied": "已复制",
517
- "row.copyPath": "复制路径",
518
- "detail.copyMention": "复制 $mention",
519
- "detail.copyPath": "复制路径",
520
- "detail.copied": "已复制",
521
- "diag.fix": "修复",
522
- "diag.fixing": "修复中…",
523
- "diag.fixed": "已修复",
524
- "diag.fixConfirm": "用自动修复重写以下文件的 frontmatter?建议先备份:{path}",
525
- "filter.invocation": "调用",
526
- "filter.invocationAll": "全部调用",
527
- "filter.modelOnly": "模型可调",
528
- "filter.userOnly": "用户可调",
529
- "filter.showing": "显示 {shown}/{total}(已启用){filtered}",
530
- "filter.filteredSuffix": " · 已筛选",
531
- "filter.shortened": "短描述 {count} 条,悬浮看全文",
532
- "delete.confirmTitle": "移入回收站?",
533
- "delete.confirmText": "确定把「{name}」移入回收站吗?可随时恢复。",
534
- "delete.confirm": "移入回收站",
535
- "detail.back": "返回列表",
536
- "detail.provider": "提供者",
537
- "detail.addedAt": "添加时间",
538
- "detail.updatedAt": "更新时间",
539
- "detail.path": "路径",
540
- "detail.whenToUse": "适用场景",
541
- "detail.uses": "调用次数",
542
- "detail.groups": "所属场景",
543
- "detail.loading": "读取技能正文…",
544
- "source.private": "个人技能(未绑定上游,不检查更新)",
545
- "source.commit": "已同步版本",
546
- "source.unverified": "尚未核对上游",
547
- "source.check": "检查更新",
548
- "source.checkHint": "点击检查上游更新",
549
- "source.checking": "检查中…",
550
- "source.upToDate": "已是最新",
551
- "source.updated": "有更新 {count} 个",
552
- "source.deleted": "上游已删除 {count} 个",
553
- "source.error": "检查失败",
554
- "source.throttled": "刚刚检查过",
555
- "source.sync": "同步",
556
- "source.syncing": "同步中…",
557
- "source.syncConfirmTitle": "同步技能?",
558
- "source.syncConfirmText": "将用上游最新版本覆盖本地对以下技能的所有修改:",
559
- "source.followDelete": "跟进删除",
560
- "source.deleteConfirmTitle": "跟进上游删除?",
561
- "source.deleteConfirmText": "以下技能已从上游仓库移除,本地目录将移入回收站(可恢复):",
562
- "source.deleteGroup": "删除整组",
563
- "source.deleteGroupHint": "删除该分组下的全部 {count} 个技能",
564
- "source.deleteGroupTitle": "删除整组?",
565
- "source.deleteGroupText": "将把分组“{name}”下的全部 {count} 个技能移入回收站(可恢复),只读技能会跳过:",
566
- "source.trash": "回收站",
567
- "source.restore": "恢复",
568
- "source.clearTrash": "清空回收站",
569
- "source.clearTrashConfirmTitle": "清空回收站?",
570
- "source.clearTrashConfirmText": "回收站里的技能会被永久删除,无法恢复。此操作不可撤销。",
571
- "source.clearTrashConfirm": "永久删除",
572
- "form.capabilityHint": "能力怎么选:重复方法 → 技能;外部系统 → MCP;长期资料 → 知识库。",
573
- "form.descPlaceholder": "一句话说明用途(含触发词),Agent 靠它自动激活技能",
574
- "form.name": "技能名(kebab-case,如 code-review)",
575
- "form.desc": "描述(一句话路由说明)",
576
- "form.root": "目标目录",
577
- "form.submit": "创建",
578
- "form.cancel": "取消",
579
- "form.busy": "创建中…",
580
- "form.success": "已创建:",
581
- "form.error": "创建失败:",
582
- "err.dismiss": "知道了",
583
- "settings.title": "技能中枢",
584
- "settings.description": "管理 dsh 本地技能目录:浏览、分组、来源同步与新建。",
585
- "settings.enabled": "启用插件",
586
- "settings.enabledHint": "关闭后技能中枢的路由、入口与公告全部下线。",
587
- "settings.announceToAgent": "向 Agent 公告",
588
- "settings.announceToAgentHint": "在系统提示中加入本插件说明,用户提到技能管理时 Agent 知道如何协作。",
589
- "settings.dotModelColor": "模型可调圆点颜色",
590
- "settings.dotModelColorHint": "技能行/聊天 / 菜单中「模型可调」蓝色圆点的颜色(#rrggbb)。",
591
- "settings.dotUserColor": "用户可调圆点颜色",
592
- "settings.dotUserColorHint": "技能行/聊天 / 菜单中「仅用户可调」绿色圆点的颜色(#rrggbb)。",
593
- "settings.showUseCount": "显示调用次数",
594
- "settings.showUseCountHint": "在技能名旁显示琥珀色调用次数。",
595
- "settings.showUseTime": "显示最近调用时间",
596
- "settings.showUseTimeHint": "在技能名行右侧显示相对时间(如「3 天前」)。",
597
- "settings.showGroupSummary": "显示分组汇总",
598
- "settings.showGroupSummaryHint": "在分组标题后汇总调用次数与最近调用时间。",
599
- "settings.statsWindowDays": "统计窗口(天)",
600
- "settings.statsWindowDaysHint": "只统计最近 N 天的使用次数,默认 14 天;0 = 全部历史。改动立即生效。",
601
- "settings.statsScanMinutes": "自动统计间隔(分钟)",
602
- "settings.statsScanMinutesHint": "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。",
603
- "settings.inherit": "继承",
604
- "settings.on": "开",
605
- "settings.off": "关",
606
- "settings.overridden": "已覆盖",
607
- "settings.reset": "恢复默认",
608
- "settings.unsaved": "未保存",
609
- "settings.save": "保存",
610
- "settings.saving": "保存中…",
611
- "settings.discard": "放弃修改",
612
- "settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
613
- "settings.readOnly": "本部署的设置为只读。",
614
- "settings.notExposed": "本部署没有挂载该设置命名空间。",
615
- "settings.collapse": "收起设置",
616
- "settings.expand": "展开设置"
489
+ "repo.root.generic": "{root}"
617
490
  };
618
- const en = {
619
- "entry.label": "Skills",
620
- "entry.tooltip": "Skill hub: browse, group, and sync local skills",
621
- "view.sources": "Sources",
622
- "view.scenes": "Scenes",
623
- "view.market": "Market",
624
- "view.flat": "Flat",
625
- "view.grouped": "Grouped",
626
- "panel.title": "Skill Hub",
627
- "panel.count": "{count} skills total",
628
- "panel.disabledCount": "{count} disabled",
629
- "panel.new": "New Skill",
630
- "panel.search": "Search skills…",
631
- "legend.model": "Model-invocable — agents may call automatically",
632
- "legend.user": "User-invocable — explicit invocation only",
633
- "legend.hint": "At most one dot per skill; no dot = not invocable.",
634
- "panel.enabled": "Enabled",
635
- "panel.disabled": "Disabled",
636
- "panel.diagnostics": "Discovery diagnostics",
637
- "panel.empty": "No matching skills.",
638
- "panel.emptyAll": "No skills installed yet. Create the first one.",
639
- "panel.loading": "Loading…",
640
- "panel.incomplete": "(catalog may be incomplete)",
641
- "panel.workspacePlaceholder": "Workspace path (Enter to apply)",
642
- "panel.workspaceHint": "Enter a project path to also show that workspace's .dsh/skills and .agents/skills skills (read-only); clear + Enter to return to the user-level view.",
643
- "panel.workspaceClear": "Clear workspace",
491
+ const enMarket = {
644
492
  "market.addPlaceholder": "owner/repo or https://github.com/…",
645
493
  "market.addSource": "Add source",
646
494
  "market.noSources": "No market sources yet. Add one from the built-in catalog below, or enter a GitHub repo.",
@@ -685,6 +533,8 @@ window.__ModuleLoader__.load({
685
533
  "market.versionConfirm": "Switch to {ref}",
686
534
  "market.versionHint": "Click to switch the tracked version",
687
535
  "market.unpinned": "unpinned",
536
+ "market.starsHint": "{count} GitHub stars",
537
+ "market.downloadsHint": "{count} release asset downloads",
688
538
  "market.syncTitle": "Update local skills",
689
539
  "market.syncText": "Market source aligned to {ref}. Update these local skills?",
690
540
  "market.syncNone": "No local skills tracked from this repo yet.",
@@ -707,15 +557,156 @@ window.__ModuleLoader__.load({
707
557
  "repo.importingCurrent": "Importing: {name} ({done}/{total})",
708
558
  "repo.root.skills": "Skills",
709
559
  "repo.root.designTemplates": "Templates design-templates",
710
- "repo.root.generic": "{root}",
711
- "update.check": "Check for updates",
712
- "update.checking": "Checking…",
713
- "update.upToDate": "Up to date: v{version}",
714
- "update.available": "Update available: v{version} (current v{current})",
715
- "update.newVersion": "New version v{version}",
716
- "update.viewRelease": "View release",
717
- "update.error": "Update check failed: {error}",
718
- "update.unavailable": "No release available",
560
+ "repo.root.generic": "{root}"
561
+ };
562
+ //#endregion
563
+ //#region src/client/locales/settings.ts
564
+ /** 设置卡。从 locales.ts 按前缀拆出,键集合不变。 */
565
+ const zhSettings = {
566
+ "settings.title": "技能中枢",
567
+ "settings.description": "管理 dsh 本地技能目录:浏览、分组、来源同步与新建。",
568
+ "settings.enabled": "启用插件",
569
+ "settings.enabledHint": "关闭后技能中枢的路由、入口与公告全部下线。",
570
+ "settings.announceToAgent": "向 Agent 公告",
571
+ "settings.announceToAgentHint": "在系统提示中加入本插件说明,用户提到技能管理时 Agent 知道如何协作。",
572
+ "settings.dotModelColor": "模型可调圆点颜色",
573
+ "settings.dotModelColorHint": "技能行/聊天 / 菜单中「模型可调」蓝色圆点的颜色(#rrggbb)。",
574
+ "settings.dotUserColor": "用户可调圆点颜色",
575
+ "settings.dotUserColorHint": "技能行/聊天 / 菜单中「仅用户可调」绿色圆点的颜色(#rrggbb)。",
576
+ "settings.showUseCount": "显示调用次数",
577
+ "settings.showUseCountHint": "在技能名旁显示琥珀色调用次数。",
578
+ "settings.showUseTime": "显示最近调用时间",
579
+ "settings.showUseTimeHint": "在技能名行右侧显示相对时间(如「3 天前」)。",
580
+ "settings.showGroupSummary": "显示分组汇总",
581
+ "settings.showGroupSummaryHint": "在分组标题后汇总调用次数与最近调用时间。",
582
+ "settings.statsWindowDays": "统计窗口(天)",
583
+ "settings.statsWindowDaysHint": "只统计最近 N 天的使用次数,默认 14 天;0 = 全部历史。改动立即生效。",
584
+ "settings.statsScanMinutes": "自动统计间隔(分钟)",
585
+ "settings.statsScanMinutesHint": "后台扫描会话日志的间隔,最小 1 分钟;扫描耗时会自动拉长间隔。",
586
+ "settings.githubToken": "GitHub Token",
587
+ "settings.githubTokenHint": "市场/来源走 GitHub API,匿名每小时 60 次,填 token 后 5000 次。细粒度 token 无需任何权限。明文存本地设置,仅回环可读;留空即匿名(或跟随 GITHUB_TOKEN 环境变量)。",
588
+ "settings.githubTokenPlaceholder": "ghp_… / github_pat_…(留空为匿名)",
589
+ "settings.inherit": "继承",
590
+ "settings.on": "开",
591
+ "settings.off": "关",
592
+ "settings.overridden": "已覆盖",
593
+ "settings.reset": "恢复默认",
594
+ "settings.unsaved": "未保存",
595
+ "settings.save": "保存",
596
+ "settings.saving": "保存中…",
597
+ "settings.discard": "放弃修改",
598
+ "settings.saveFailed": "本部署没有接受这些值,已保留供你修改。",
599
+ "settings.readOnly": "本部署的设置为只读。",
600
+ "settings.notExposed": "本部署没有挂载该设置命名空间。",
601
+ "settings.collapse": "收起设置",
602
+ "settings.expand": "展开设置"
603
+ };
604
+ const enSettings = {
605
+ "settings.title": "Skill Hub",
606
+ "settings.description": "Manage the local dsh skill catalog: browse, group, sync, and scaffold skills.",
607
+ "settings.enabled": "Enable plugin",
608
+ "settings.enabledHint": "Disables the hub routes, entries, and announcement.",
609
+ "settings.announceToAgent": "Announce to agent",
610
+ "settings.announceToAgentHint": "Adds a system-prompt section so agents know how to collaborate when users mention skill management.",
611
+ "settings.dotModelColor": "Model-invocable dot color",
612
+ "settings.dotModelColorHint": "Color of the blue \"model-callable\" dot in the skill panel and the chat / menu (#rrggbb).",
613
+ "settings.dotUserColor": "User-invocable dot color",
614
+ "settings.dotUserColorHint": "Color of the green \"user-only\" dot in the skill panel and the chat / menu (#rrggbb).",
615
+ "settings.showUseCount": "Show invocation count",
616
+ "settings.showUseCountHint": "Show the amber invocation count beside each skill name.",
617
+ "settings.showUseTime": "Show last-used time",
618
+ "settings.showUseTimeHint": "Show relative last-used time on the name row (e.g. \"3 d ago\").",
619
+ "settings.showGroupSummary": "Show group summaries",
620
+ "settings.showGroupSummaryHint": "Summarize invocation count and last-used time after group titles.",
621
+ "settings.statsWindowDays": "Stats window (days)",
622
+ "settings.statsWindowDaysHint": "Count only usage within the last N days (default 14); 0 = full history. Changes apply immediately.",
623
+ "settings.statsScanMinutes": "Auto stats interval (minutes)",
624
+ "settings.statsScanMinutesHint": "How often the background session-log scan runs (min 1); heavy scans automatically stretch it.",
625
+ "settings.githubToken": "GitHub token",
626
+ "settings.githubTokenHint": "Market/sources use the GitHub API: 60 req/h anonymous, 5000/hr with a token. A fine-grained token needs no permissions. Stored plaintext in local settings, loopback-only; empty means anonymous (or GITHUB_TOKEN env).",
627
+ "settings.githubTokenPlaceholder": "ghp_… / github_pat_… (empty = anonymous)",
628
+ "settings.inherit": "Inherit",
629
+ "settings.on": "On",
630
+ "settings.off": "Off",
631
+ "settings.overridden": "Overridden",
632
+ "settings.reset": "Reset to default",
633
+ "settings.unsaved": "Unsaved",
634
+ "settings.save": "Save",
635
+ "settings.saving": "Saving…",
636
+ "settings.discard": "Discard",
637
+ "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
638
+ "settings.readOnly": "This deployment stores settings read-only.",
639
+ "settings.notExposed": "This deployment does not expose the settings namespace.",
640
+ "settings.collapse": "Hide settings",
641
+ "settings.expand": "Show settings"
642
+ };
643
+ //#endregion
644
+ //#region src/client/locales/skills.ts
645
+ /** 技能浏览:行徽章/分组/场景/过滤排序/相对时间。从 locales.ts 按前缀拆出,键集合不变。 */
646
+ const zhSkills = {
647
+ "group.project-dsh": "项目 .dsh/skills",
648
+ "group.project-agents": "项目 .agents/skills",
649
+ "group.custom": "自定义目录",
650
+ "group.runtime": "运行时",
651
+ "group.user-dsh": "用户 ~/.dsh/skills",
652
+ "group.user-agents": "用户 ~/.agents/skills",
653
+ "group.bundled": "内置",
654
+ "group.other": "其他来源",
655
+ "groups.new": "新建场景",
656
+ "groups.namePlaceholder": "场景名",
657
+ "groups.edit": "编辑",
658
+ "groups.rename": "保存名称",
659
+ "groups.delete": "删除场景",
660
+ "groups.saveMembers": "保存成员",
661
+ "groups.membersHint": "勾选技能加入此场景(含已禁用)。",
662
+ "groups.empty": "还没有场景。先新建一个,再把技能归类。",
663
+ "groups.noCollections": "暂无来源组。从市场导入技能后自动按来源聚合。",
664
+ "groups.personal": "个人",
665
+ "groups.project": "项目级",
666
+ "groups.subdivide": "细分",
667
+ "groups.merge": "合并",
668
+ "groups.noWritable": "该组没有可写技能(只读技能无法由中枢开关)",
669
+ "groups.closeAll": "全部关闭",
670
+ "groups.keepOn": "保留开启",
671
+ "groups.conflictTitle": "关闭此组?",
672
+ "groups.conflictText": "以下技能也在其他组中开启,关闭它们会影响那些组:",
673
+ "filter.allSources": "全部来源",
674
+ "filter.private": "个人",
675
+ "sort.name": "按名称",
676
+ "sort.added": "按添加时间",
677
+ "sort.uses": "按调用次数",
678
+ "time.justNow": "刚刚",
679
+ "time.minutesAgo": "{value} 分钟前",
680
+ "time.hoursAgo": "{value} 小时前",
681
+ "time.daysAgo": "{value} 天前",
682
+ "time.weeksAgo": "{value} 周前",
683
+ "badge.source.project-dsh": "项目 .dsh",
684
+ "badge.source.project-agents": "项目 .agents",
685
+ "badge.source.custom": "自定义",
686
+ "badge.source.runtime": "运行时",
687
+ "badge.source.user-dsh": "用户 .dsh",
688
+ "badge.source.user-agents": "用户 .agents",
689
+ "badge.source.bundled": "内置",
690
+ "row.readonly": "只读",
691
+ "row.disable": "禁用",
692
+ "row.enable": "启用",
693
+ "row.delete": "移入回收站",
694
+ "row.open": "查看 {name} 详情(回车)",
695
+ "row.duplicate": "重名",
696
+ "row.duplicateHint": "同名技能存在多个来源,当前显示为优先级最高的版本",
697
+ "row.copy": "复制",
698
+ "row.copyMention": "复制 $mention",
699
+ "row.copied": "已复制",
700
+ "row.copyPath": "复制路径",
701
+ "filter.invocation": "调用",
702
+ "filter.invocationAll": "全部调用",
703
+ "filter.modelOnly": "模型可调",
704
+ "filter.userOnly": "用户可调",
705
+ "filter.showing": "显示 {shown}/{total}(已启用){filtered}",
706
+ "filter.filteredSuffix": " · 已筛选",
707
+ "filter.shortened": "短描述 {count} 条,悬浮看全文"
708
+ };
709
+ const enSkills = {
719
710
  "group.project-dsh": "Project .dsh/skills",
720
711
  "group.project-agents": "Project .agents/skills",
721
712
  "group.custom": "Custom directories",
@@ -770,32 +761,54 @@ window.__ModuleLoader__.load({
770
761
  "row.copyMention": "Copy $mention",
771
762
  "row.copied": "Copied",
772
763
  "row.copyPath": "Copy path",
773
- "detail.copyMention": "Copy $mention",
774
- "detail.copyPath": "Copy path",
775
- "detail.copied": "Copied",
776
- "diag.fix": "Fix",
777
- "diag.fixing": "Fixing…",
778
- "diag.fixed": "Fixed",
779
- "diag.fixConfirm": "Rewrite the frontmatter of this file with the auto-fix? Back it up first: {path}",
780
764
  "filter.invocation": "Invocation",
781
765
  "filter.invocationAll": "All",
782
766
  "filter.modelOnly": "Model",
783
767
  "filter.userOnly": "User",
784
768
  "filter.showing": "Showing {shown}/{total} (enabled){filtered}",
785
769
  "filter.filteredSuffix": " · filtered",
786
- "filter.shortened": "{count} short descriptions, hover for full text",
787
- "delete.confirmTitle": "Move to trash?",
770
+ "filter.shortened": "{count} short descriptions, hover for full text"
771
+ };
772
+ //#endregion
773
+ //#region src/client/locales/sources.ts
774
+ /** 来源跟踪与回收站(含删除确认)。从 locales.ts 按前缀拆出,键集合不变。 */
775
+ const zhSources = {
776
+ "delete.confirmTitle": "移入回收站?",
777
+ "delete.confirmText": "确定把「{name}」移入回收站吗?可随时恢复。",
778
+ "delete.confirm": "移入回收站",
779
+ "source.private": "个人技能(未绑定上游,不检查更新)",
780
+ "source.commit": "已同步版本",
781
+ "source.unverified": "尚未核对上游",
782
+ "source.check": "检查更新",
783
+ "source.checkHint": "点击检查上游更新",
784
+ "source.checking": "检查中…",
785
+ "source.upToDate": "已是最新",
786
+ "source.updated": "有更新 {count} 个",
787
+ "source.deleted": "上游已删除 {count} 个",
788
+ "source.error": "检查失败",
789
+ "source.throttled": "刚刚检查过",
790
+ "source.sync": "同步",
791
+ "source.syncing": "同步中…",
792
+ "source.syncConfirmTitle": "同步技能?",
793
+ "source.syncConfirmText": "将用上游最新版本覆盖本地对以下技能的所有修改:",
794
+ "source.followDelete": "跟进删除",
795
+ "source.deleteConfirmTitle": "跟进上游删除?",
796
+ "source.deleteConfirmText": "以下技能已从上游仓库移除,本地目录将移入回收站(可恢复):",
797
+ "source.deleteGroup": "删除整组",
798
+ "source.deleteGroupHint": "删除该分组下的全部 {count} 个技能",
799
+ "source.deleteGroupTitle": "删除整组?",
800
+ "source.deleteGroupText": "将把分组“{name}”下的全部 {count} 个技能移入回收站(可恢复),只读技能会跳过:",
801
+ "source.trash": "回收站",
802
+ "source.restore": "恢复",
803
+ "source.clearTrash": "清空回收站",
804
+ "source.clearTrashConfirmTitle": "清空回收站?",
805
+ "source.clearTrashConfirmText": "回收站里的技能会被永久删除,无法恢复。此操作不可撤销。",
806
+ "source.clearTrashConfirm": "永久删除"
807
+ };
808
+ const enSources = {
809
+ "delete.confirmTitle": "Move to trash?",
788
810
  "delete.confirmText": "Move \"{name}\" to trash? It can be restored anytime.",
789
811
  "delete.confirm": "Move to trash",
790
- "detail.back": "Back to list",
791
- "detail.provider": "Provider",
792
- "detail.addedAt": "Added",
793
- "detail.updatedAt": "Updated",
794
- "detail.path": "Path",
795
- "detail.whenToUse": "When to use",
796
- "detail.uses": "Uses",
797
- "detail.groups": "Groups",
798
- "detail.loading": "Loading skill body…",
799
812
  "source.private": "Personal skill (not bound to an upstream; no update checks)",
800
813
  "source.commit": "Synced commit",
801
814
  "source.unverified": "upstream not verified yet",
@@ -823,52 +836,31 @@ window.__ModuleLoader__.load({
823
836
  "source.clearTrash": "Empty trash",
824
837
  "source.clearTrashConfirmTitle": "Empty the trash?",
825
838
  "source.clearTrashConfirmText": "Skills in the trash will be permanently deleted and cannot be restored.",
826
- "source.clearTrashConfirm": "Delete permanently",
827
- "form.capabilityHint": "Choosing a capability: repeatable method → skill; external system → MCP; long-term knowledge → knowledge base.",
828
- "form.descPlaceholder": "One line on what it does (with trigger words) — agents auto-activate from this",
829
- "form.name": "Skill name (kebab-case, e.g. code-review)",
830
- "form.desc": "Description (one routing sentence)",
831
- "form.root": "Target root",
832
- "form.submit": "Create",
833
- "form.cancel": "Cancel",
834
- "form.busy": "Creating…",
835
- "form.success": "Created: ",
836
- "form.error": "Create failed: ",
837
- "err.dismiss": "Dismiss",
838
- "settings.title": "Skill Hub",
839
- "settings.description": "Manage the local dsh skill catalog: browse, group, sync, and scaffold skills.",
840
- "settings.enabled": "Enable plugin",
841
- "settings.enabledHint": "Disables the hub routes, entries, and announcement.",
842
- "settings.announceToAgent": "Announce to agent",
843
- "settings.announceToAgentHint": "Adds a system-prompt section so agents know how to collaborate when users mention skill management.",
844
- "settings.dotModelColor": "Model-invocable dot color",
845
- "settings.dotModelColorHint": "Color of the blue \"model-callable\" dot in the skill panel and the chat / menu (#rrggbb).",
846
- "settings.dotUserColor": "User-invocable dot color",
847
- "settings.dotUserColorHint": "Color of the green \"user-only\" dot in the skill panel and the chat / menu (#rrggbb).",
848
- "settings.showUseCount": "Show invocation count",
849
- "settings.showUseCountHint": "Show the amber invocation count beside each skill name.",
850
- "settings.showUseTime": "Show last-used time",
851
- "settings.showUseTimeHint": "Show relative last-used time on the name row (e.g. \"3 d ago\").",
852
- "settings.showGroupSummary": "Show group summaries",
853
- "settings.showGroupSummaryHint": "Summarize invocation count and last-used time after group titles.",
854
- "settings.statsWindowDays": "Stats window (days)",
855
- "settings.statsWindowDaysHint": "Count only usage within the last N days (default 14); 0 = full history. Changes apply immediately.",
856
- "settings.statsScanMinutes": "Auto stats interval (minutes)",
857
- "settings.statsScanMinutesHint": "How often the background session-log scan runs (min 1); heavy scans automatically stretch it.",
858
- "settings.inherit": "Inherit",
859
- "settings.on": "On",
860
- "settings.off": "Off",
861
- "settings.overridden": "Overridden",
862
- "settings.reset": "Reset to default",
863
- "settings.unsaved": "Unsaved",
864
- "settings.save": "Save",
865
- "settings.saving": "Saving…",
866
- "settings.discard": "Discard",
867
- "settings.saveFailed": "The deployment did not accept these values; they were left for you to correct.",
868
- "settings.readOnly": "This deployment stores settings read-only.",
869
- "settings.notExposed": "This deployment does not expose the settings namespace.",
870
- "settings.collapse": "Hide settings",
871
- "settings.expand": "Show settings"
839
+ "source.clearTrashConfirm": "Delete permanently"
840
+ };
841
+ //#endregion
842
+ //#region src/client/locales.ts
843
+ /**
844
+ * locales 按视图拆分后的聚合入口:各域字典在 ./locales/<domain>.ts,
845
+ * 这里合并为面板消费的 zh/en。老 `from './locales.ts'` 写法保持可用,
846
+ * HubKey 的完备性仍由 `en: Record<HubKey, string>` 在类型层兜底
847
+ * (缺键即编译报错)。
848
+ */
849
+ const zh = {
850
+ ...zhCommon,
851
+ ...zhSkills,
852
+ ...zhMarket,
853
+ ...zhSources,
854
+ ...zhDetail,
855
+ ...zhSettings
856
+ };
857
+ const en = {
858
+ ...enCommon,
859
+ ...enSkills,
860
+ ...enMarket,
861
+ ...enSources,
862
+ ...enDetail,
863
+ ...enSettings
872
864
  };
873
865
  /** Interpolate a {name} template with the given values. */
874
866
  function t(dict, key, values) {
@@ -1013,8 +1005,15 @@ window.__ModuleLoader__.load({
1013
1005
  * based, family precedent) plus a small error-message extractor.
1014
1006
  */
1015
1007
  /** Active dictionary, picked by the document language at call time. */
1008
+ let cachedDictionaryLang = "";
1009
+ let cachedDictionary = null;
1016
1010
  function dictionary() {
1017
- return (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? { ...en } : { ...zh };
1011
+ const key = (typeof document !== "undefined" ? document.documentElement.lang : "zh").toLowerCase().startsWith("en") ? "en" : "zh";
1012
+ if (cachedDictionary === null || cachedDictionaryLang !== key) {
1013
+ cachedDictionaryLang = key;
1014
+ cachedDictionary = key === "en" ? { ...en } : { ...zh };
1015
+ }
1016
+ return cachedDictionary;
1018
1017
  }
1019
1018
  /** Translate a key with optional {name} template params (current language). */
1020
1019
  function tt(key, values) {
@@ -1574,6 +1573,51 @@ window.__ModuleLoader__.load({
1574
1573
  ]
1575
1574
  });
1576
1575
  }
1576
+ function SecretField(props) {
1577
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1578
+ className: settings_card_module_css_default.field,
1579
+ children: [
1580
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1581
+ className: settings_card_module_css_default.head,
1582
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
1583
+ className: settings_card_module_css_default.label,
1584
+ htmlFor: props.id,
1585
+ children: props.label
1586
+ }), props.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1587
+ className: settings_card_module_css_default.badges,
1588
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1589
+ className: settings_card_module_css_default.badge,
1590
+ children: props.overriddenLabel
1591
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1592
+ type: "button",
1593
+ className: settings_card_module_css_default.reset,
1594
+ disabled: props.disabled,
1595
+ onClick: props.onReset,
1596
+ children: props.resetLabel
1597
+ })]
1598
+ }) : null]
1599
+ }),
1600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1601
+ id: props.id,
1602
+ type: "password",
1603
+ autoComplete: "off",
1604
+ spellCheck: false,
1605
+ className: settings_card_module_css_default.input,
1606
+ value: props.text,
1607
+ disabled: props.disabled,
1608
+ placeholder: props.placeholder,
1609
+ "aria-invalid": props.invalid === true,
1610
+ onChange: (event) => {
1611
+ props.onEdit(event.target.value);
1612
+ }
1613
+ }),
1614
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1615
+ className: settings_card_module_css_default.hint,
1616
+ children: props.hint
1617
+ })
1618
+ ]
1619
+ });
1620
+ }
1577
1621
  function NumberField(props) {
1578
1622
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1579
1623
  className: settings_card_module_css_default.field,
@@ -2361,6 +2405,26 @@ window.__ModuleLoader__.load({
2361
2405
  };
2362
2406
  }
2363
2407
  /**
2408
+ * A secret field (API token): the stored value is never echoed back into the
2409
+ * draft (format always yields ''), so typing is always a fresh set and Reset
2410
+ * always unsets. An empty draft stages nothing.
2411
+ */
2412
+ function secretField(field) {
2413
+ return {
2414
+ field,
2415
+ format: () => "",
2416
+ parse: (text) => {
2417
+ const trimmed = text.trim();
2418
+ if (trimmed === "") return void 0;
2419
+ if (!/^[A-Za-z0-9_-]{8,255}$/.test(trimmed)) return void 0;
2420
+ return {
2421
+ kind: "set",
2422
+ value: trimmed
2423
+ };
2424
+ }
2425
+ };
2426
+ }
2427
+ /**
2364
2428
  * A numeric field with range clamping. Integer by default: a fractional draft
2365
2429
  * is truncated so "5.5 分钟" cannot sneak past an integer-only schema.
2366
2430
  */
@@ -2574,7 +2638,8 @@ window.__ModuleLoader__.load({
2574
2638
  numberField("statsScanMinutes", {
2575
2639
  min: 1,
2576
2640
  max: 1440
2577
- })
2641
+ }),
2642
+ secretField("githubToken")
2578
2643
  ]);
2579
2644
  this.store = this.form.bind(() => this.projection());
2580
2645
  }
@@ -2589,7 +2654,8 @@ window.__ModuleLoader__.load({
2589
2654
  showUseTime: this.form.field("showUseTime"),
2590
2655
  showGroupSummary: this.form.field("showGroupSummary"),
2591
2656
  statsWindowDays: this.form.field("statsWindowDays"),
2592
- statsScanMinutes: this.form.field("statsScanMinutes")
2657
+ statsScanMinutes: this.form.field("statsScanMinutes"),
2658
+ githubToken: this.form.field("githubToken")
2593
2659
  };
2594
2660
  }
2595
2661
  /**
@@ -2757,6 +2823,22 @@ window.__ModuleLoader__.load({
2757
2823
  onReset: () => {
2758
2824
  props.resetField("statsScanMinutes");
2759
2825
  }
2826
+ }),
2827
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SecretField, {
2828
+ id: "skill-hub-github-token",
2829
+ label: t("settings.githubToken"),
2830
+ hint: t("settings.githubTokenHint"),
2831
+ placeholder: t("settings.githubTokenPlaceholder"),
2832
+ ...fieldProps,
2833
+ text: state.githubToken.text,
2834
+ overridden: state.githubToken.overridden,
2835
+ invalid: state.githubToken.invalid,
2836
+ onEdit: (text) => {
2837
+ props.edit("githubToken", text);
2838
+ },
2839
+ onReset: () => {
2840
+ props.resetField("githubToken");
2841
+ }
2760
2842
  })
2761
2843
  ]
2762
2844
  });
@@ -4383,13 +4465,14 @@ window.__ModuleLoader__.load({
4383
4465
  });
4384
4466
  }
4385
4467
  //#endregion
4386
- //#region src/client/panel/MarketView.tsx
4387
- /**
4388
- * Market tab: one unified market list — built-in catalog entries (add
4389
- * button while not yet added) and the user's market sources (full state
4390
- * badges + actions once added, plus custom sources), with check-all and
4391
- * update-all actions and the repo scan result with the importable checklist.
4392
- */
4468
+ //#region src/client/panel/market-format.ts
4469
+ /** Market tab number formatting (stars/downloads/bytes/speed). Pure, no deps. */
4470
+ /** Compact count for stars/downloads (1234 → 1.2k). */
4471
+ function formatCount(n) {
4472
+ if (n < 1e3) return String(n);
4473
+ if (n < 1e6) return (n / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
4474
+ return (n / 1e6).toFixed(1).replace(/\.0$/, "") + "M";
4475
+ }
4393
4476
  /** Compact byte size for repo preview rows. */
4394
4477
  function formatBytes(bytes) {
4395
4478
  if (bytes < 1024) return bytes + " B";
@@ -4402,152 +4485,580 @@ window.__ModuleLoader__.load({
4402
4485
  if (bps < 1024 * 1024) return (bps / 1024).toFixed(1) + " KB/s";
4403
4486
  return (bps / 1024 / 1024).toFixed(1) + " MB/s";
4404
4487
  }
4405
- function MarketView(props) {
4406
- const { hub } = props;
4407
- const { marketState, marketCheck, sourceCheck, sourcesState, repoDiscoverState, scanningRepo, repoSelected, setRepoSelected, repoImporting, repoResult, newSourceName, setNewSourceName, syncingMarket, tagBusy, checkingSource, addSource, addMarketSource, removeMarketSource, scanRepo, checkMarket, checkSources, syncMarketSource, toggleRepoSelected, importRepo, cancelImport, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
4408
- /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
4409
- const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
4410
- const [repoSearch, setRepoSearch] = (0, react.useState)("");
4411
- const [repoFilter, setRepoFilter] = (0, react.useState)("all");
4412
- const [visibleCount, setVisibleCount] = (0, react.useState)(50);
4413
- (0, react.useEffect)(() => {
4414
- setVisibleCount(50);
4415
- }, [
4416
- repoDiscoverState,
4417
- repoSearch,
4418
- repoFilter
4419
- ]);
4420
- /** 一个已添加来源的完整行:状态徽章 + 操作按钮。B 方案:扫描主按钮 + 智能更新按钮(检查/更新二合一)+ 溢出移除 */
4421
- const sourceRow = (record) => {
4422
- const releaseCheck = marketCheck[record.repo];
4423
- const skillCheck = sourceCheck[record.repo];
4424
- const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
4425
- const scanning = scanningRepo === record.repo;
4426
- const syncing = syncingMarket === record.repo;
4427
- const checking = checkingSource === record.repo;
4428
- const hasSkillUpdate = skillCheck?.changed === true && skillCheck.updated.length > 0;
4429
- const hasReleaseUpdate = releaseCheck?.updateAvailable === true;
4430
- const hasUpdate = hasSkillUpdate || hasReleaseUpdate;
4431
- const isChecked = releaseCheck !== void 0 || skillCheck !== void 0;
4432
- let updateLabel;
4433
- let updateDisabled = false;
4434
- let updateDanger = false;
4435
- let updateAction = () => {};
4436
- if (syncing) {
4437
- updateLabel = tt("market.syncing");
4438
- updateDisabled = true;
4439
- } else if (checking) {
4440
- updateLabel = tt("market.checking");
4441
- updateDisabled = true;
4442
- } else if (!isChecked) {
4443
- updateLabel = tt("market.checkUpdate");
4444
- updateAction = () => {
4445
- checkSources(record.repo);
4446
- checkMarket();
4447
- };
4448
- } else if (hasUpdate) {
4449
- const count = hasSkillUpdate ? skillCheck.updated.length : 1;
4450
- updateLabel = count > 1 || hasSkillUpdate ? tt("market.updateCount", { count }) : tt("market.update");
4451
- updateDanger = true;
4452
- updateAction = () => {
4453
- syncMarketSource(record.repo);
4454
- };
4455
- } else {
4456
- updateLabel = tt("market.upToDate");
4457
- updateDisabled = true;
4458
- }
4459
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4460
- className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
4461
- children: [
4462
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4463
- className: panel_module_css_default.rowMain,
4464
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4465
- className: panel_module_css_default.rowName,
4488
+ //#endregion
4489
+ //#region src/client/panel/MarketSourceRow.tsx
4490
+ function MarketSourceRow(props) {
4491
+ const { hub, record } = props;
4492
+ const { marketCheck, sourceCheck, sourcesState, scanningRepo, syncingMarket, checkingSource, tagBusy, checkSources, checkMarket, syncMarketSource, scanRepo, removeMarketSource } = hub;
4493
+ const releaseCheck = marketCheck[record.repo];
4494
+ const skillCheck = sourceCheck[record.repo];
4495
+ const installedCount = sourcesState?.sources.find((source) => source.repo === record.repo)?.skills.length ?? 0;
4496
+ const scanning = scanningRepo === record.repo;
4497
+ const syncing = syncingMarket === record.repo;
4498
+ const checking = checkingSource === record.repo;
4499
+ const hasSkillUpdate = skillCheck?.changed === true && skillCheck.updated.length > 0;
4500
+ const hasReleaseUpdate = releaseCheck?.updateAvailable === true;
4501
+ const hasUpdate = hasSkillUpdate || hasReleaseUpdate;
4502
+ const isChecked = releaseCheck !== void 0 || skillCheck !== void 0;
4503
+ let updateLabel;
4504
+ let updateDisabled = false;
4505
+ let updateDanger = false;
4506
+ let updateAction = () => {};
4507
+ if (syncing) {
4508
+ updateLabel = tt("market.syncing");
4509
+ updateDisabled = true;
4510
+ } else if (checking) {
4511
+ updateLabel = tt("market.checking");
4512
+ updateDisabled = true;
4513
+ } else if (!isChecked) {
4514
+ updateLabel = tt("market.checkUpdate");
4515
+ updateAction = () => {
4516
+ checkSources(record.repo);
4517
+ checkMarket();
4518
+ };
4519
+ } else if (hasUpdate) {
4520
+ const count = hasSkillUpdate ? skillCheck.updated.length : 1;
4521
+ updateLabel = count > 1 || hasSkillUpdate ? tt("market.updateCount", { count }) : tt("market.update");
4522
+ updateDanger = true;
4523
+ updateAction = () => {
4524
+ syncMarketSource(record.repo);
4525
+ };
4526
+ } else {
4527
+ updateLabel = tt("market.upToDate");
4528
+ updateDisabled = true;
4529
+ }
4530
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4531
+ className: panel_module_css_default.row + " " + panel_module_css_default.rowStatic,
4532
+ children: [
4533
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4534
+ className: panel_module_css_default.rowMain,
4535
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4536
+ className: panel_module_css_default.rowName,
4537
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
4538
+ className: panel_module_css_default.sourceLink,
4539
+ href: "https://github.com/" + record.repo,
4540
+ target: "_blank",
4541
+ rel: "noreferrer",
4542
+ children: record.repo
4543
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4544
+ type: "button",
4545
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4546
+ style: {
4547
+ cursor: "pointer",
4548
+ fontFamily: "inherit"
4549
+ },
4550
+ title: tt("market.versionHint"),
4551
+ onClick: () => {
4552
+ hub.openVersionDialog(record.repo);
4553
+ },
4554
+ children: record.ref ?? tt("market.unpinned")
4555
+ })]
4556
+ }), (() => {
4557
+ const stats = hub.marketStats[record.repo];
4558
+ const hasDeleted = skillCheck !== void 0 && skillCheck.deleted.length > 0;
4559
+ if (stats === void 0 && installedCount === 0 && !hasSkillUpdate && !hasDeleted && !hasReleaseUpdate) return null;
4560
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4561
+ className: panel_module_css_default.hintLine,
4562
+ style: {
4563
+ display: "flex",
4564
+ gap: 4,
4565
+ flexWrap: "wrap",
4566
+ marginTop: 3
4567
+ },
4466
4568
  children: [
4467
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
4468
- className: panel_module_css_default.sourceLink,
4469
- href: "https://github.com/" + record.repo,
4470
- target: "_blank",
4471
- rel: "noreferrer",
4472
- children: record.repo
4473
- }),
4474
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4475
- type: "button",
4476
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4477
- style: {
4478
- cursor: "pointer",
4479
- fontFamily: "inherit"
4480
- },
4481
- title: tt("market.versionHint"),
4482
- onClick: () => {
4483
- hub.openVersionDialog(record.repo);
4484
- },
4485
- children: record.ref ?? tt("market.unpinned")
4486
- }),
4487
- installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4488
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
4489
- children: tt("market.installed", { count: installedCount })
4490
- }) : null,
4491
4569
  hasSkillUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4492
4570
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
4493
4571
  children: tt("market.updatable", { count: skillCheck.updated.length })
4494
4572
  }) : null,
4495
- skillCheck !== void 0 && skillCheck.deleted.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4573
+ hasDeleted ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4496
4574
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusError,
4497
4575
  children: tt("market.deletedUpstream", { count: skillCheck.deleted.length })
4498
4576
  }) : null,
4499
4577
  hasReleaseUpdate ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4500
4578
  className: panel_module_css_default.badge + " " + panel_module_css_default.statusUpdated,
4501
4579
  children: releaseCheck.latestTag !== void 0 ? tt("market.newRelease", { version: releaseCheck.latestTag }) : tt("market.updated")
4580
+ }) : null,
4581
+ installedCount > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4582
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
4583
+ children: tt("market.installed", { count: installedCount })
4584
+ }) : null,
4585
+ stats !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4586
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
4587
+ title: tt("market.starsHint", { count: stats.stars }),
4588
+ children: ["★ ", formatCount(stats.stars)]
4589
+ }) : null,
4590
+ stats !== void 0 && stats.downloads > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4591
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeCount,
4592
+ title: tt("market.downloadsHint", { count: stats.downloads }),
4593
+ children: ["⭳ ", formatCount(stats.downloads)]
4502
4594
  }) : null
4503
4595
  ]
4504
- })
4505
- }),
4506
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4507
- type: "button",
4508
- className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4509
- style: {
4510
- padding: "4px 10px",
4511
- fontSize: 12
4512
- },
4513
- disabled: scanning,
4514
- onClick: () => {
4515
- scanRepo(record.repo);
4516
- },
4517
- children: scanning ? tt("market.scanning") : tt("market.scan")
4518
- }),
4519
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4520
- type: "button",
4521
- className: panel_module_css_default.opBtn + (updateDanger ? " " + panel_module_css_default.opDanger : ""),
4522
- disabled: updateDisabled,
4523
- title: hasUpdate ? "将同步到上游最新版并提示可更新的本地技能" : void 0,
4524
- onClick: updateAction,
4525
- children: updateLabel
4526
- }),
4527
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4528
- type: "button",
4529
- className: panel_module_css_default.opBtn,
4530
- disabled: tagBusy,
4531
- title: tt("market.removeHint"),
4532
- "aria-label": tt("market.deleteSource"),
4533
- onClick: () => {
4534
- removeMarketSource(record.repo);
4535
- },
4536
- style: {
4537
- padding: "4px 8px",
4538
- minWidth: 28
4539
- },
4540
- children: "×"
4541
- })
4542
- ]
4543
- });
4544
- };
4545
- const rows = [];
4546
- for (const entry of MARKET_CATALOG) {
4547
- const record = marketState.repos.find((item) => item.repo === entry.repo);
4548
- if (record !== void 0) rows.push({
4596
+ });
4597
+ })()]
4598
+ }),
4599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4600
+ type: "button",
4601
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4602
+ style: {
4603
+ padding: "4px 10px",
4604
+ fontSize: 12
4605
+ },
4606
+ disabled: scanning,
4607
+ onClick: () => {
4608
+ scanRepo(record.repo);
4609
+ },
4610
+ children: scanning ? tt("market.scanning") : tt("market.scan")
4611
+ }),
4612
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4613
+ type: "button",
4614
+ className: panel_module_css_default.opBtn + (updateDanger ? " " + panel_module_css_default.opDanger : ""),
4615
+ disabled: updateDisabled,
4616
+ title: hasUpdate ? "将同步到上游最新版并提示可更新的本地技能" : void 0,
4617
+ onClick: updateAction,
4618
+ children: updateLabel
4619
+ }),
4620
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4621
+ type: "button",
4622
+ className: panel_module_css_default.opBtn,
4623
+ disabled: tagBusy,
4624
+ title: tt("market.removeHint"),
4625
+ "aria-label": tt("market.deleteSource"),
4626
+ onClick: () => {
4627
+ removeMarketSource(record.repo);
4628
+ },
4629
+ style: {
4630
+ padding: "4px 8px",
4631
+ minWidth: 28
4632
+ },
4633
+ children: "×"
4634
+ })
4635
+ ]
4636
+ });
4637
+ }
4638
+ //#endregion
4639
+ //#region src/client/panel/RepoScanCard.tsx
4640
+ /**
4641
+ * RepoScanCard — 扫描结果归属卡片:扫描中/错误/就绪(搜索+过滤+分页+
4642
+ * 勾选导入+进度+结果)都包在这里。搜索/过滤/分页是卡片级本地状态,
4643
+ * 随卡片挂载而生、关闭而灭。从 MarketView.tsx 原样搬出,渲染逻辑不变。
4644
+ */
4645
+ function RepoScanCard(props) {
4646
+ const { hub } = props;
4647
+ const { repoDiscoverState, scanningRepo, repoResult, repoSelected, setRepoSelected, repoImporting, toggleRepoSelected, importRepo, cancelImport } = hub;
4648
+ const [repoSearch, setRepoSearch] = (0, react.useState)("");
4649
+ const [repoFilter, setRepoFilter] = (0, react.useState)("all");
4650
+ const [visibleCount, setVisibleCount] = (0, react.useState)(50);
4651
+ (0, react.useEffect)(() => {
4652
+ setVisibleCount(50);
4653
+ }, [
4654
+ repoDiscoverState,
4655
+ repoSearch,
4656
+ repoFilter
4657
+ ]);
4658
+ if (repoDiscoverState.status === "idle" && repoResult === null) return null;
4659
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4660
+ className: panel_module_css_default.section,
4661
+ style: {
4662
+ borderLeft: "3px solid var(--hub-model, #2f81f7)",
4663
+ background: "rgba(47,129,247,0.04)"
4664
+ },
4665
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4666
+ className: panel_module_css_default.sectionTitle + " " + panel_module_css_default.sectionHeadRow,
4667
+ style: { margin: "10px 14px 6px" },
4668
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4669
+ className: panel_module_css_default.sectionTitleFill,
4670
+ style: {
4671
+ display: "flex",
4672
+ alignItems: "center",
4673
+ gap: 6
4674
+ },
4675
+ children: ["扫描结果", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4676
+ style: {
4677
+ fontWeight: 400,
4678
+ opacity: .75
4679
+ },
4680
+ children: [
4681
+ "— ",
4682
+ repoDiscoverState.status === "ready" ? repoDiscoverState.data.repo : repoDiscoverState.status === "error" ? scanningRepo ?? "" : scanningRepo ?? "",
4683
+ repoDiscoverState.status === "ready" && repoDiscoverState.data.ref !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4684
+ style: {
4685
+ opacity: .5,
4686
+ marginLeft: 6
4687
+ },
4688
+ children: ["ref ", repoDiscoverState.data.ref]
4689
+ }) : null
4690
+ ]
4691
+ })]
4692
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4693
+ type: "button",
4694
+ className: panel_module_css_default.opBtn,
4695
+ onClick: () => {
4696
+ hub.clearScan();
4697
+ },
4698
+ children: "关闭"
4699
+ })]
4700
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4701
+ style: { padding: "0 12px 12px" },
4702
+ children: [
4703
+ repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4704
+ className: panel_module_css_default.empty,
4705
+ style: { padding: "12px 0" },
4706
+ children: tt("market.scanning")
4707
+ }) : null,
4708
+ repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4709
+ className: panel_module_css_default.errorBanner,
4710
+ children: repoDiscoverState.message
4711
+ }) : null,
4712
+ repoDiscoverState.status === "ready" ? (() => {
4713
+ const entries = repoDiscoverState.data.entries;
4714
+ const installedNames = /* @__PURE__ */ new Set([
4715
+ ...(hub.catalog?.skills ?? []).map((s) => s.name),
4716
+ ...(hub.catalog?.disabled ?? []).map((d) => d.name),
4717
+ ...(repoResult?.imported ?? []).map((r) => r.name),
4718
+ ...(repoResult?.skipped ?? []).map((r) => r.name)
4719
+ ]);
4720
+ const isExisting = (entry) => entry.existing || installedNames.has(entry.name);
4721
+ const selected = entries.filter((entry) => repoSelected.has(entry.path) && !isExisting(entry));
4722
+ const selectedBytes = selected.reduce((s, e) => s + e.totalBytes, 0);
4723
+ const filtered = entries.filter((entry) => {
4724
+ if (repoFilter !== "all" && entry.root !== repoFilter) return false;
4725
+ if (repoSearch.trim() !== "" && !entry.name.toLowerCase().includes(repoSearch.trim().toLowerCase())) return false;
4726
+ return true;
4727
+ }).sort((a, b) => a.name.localeCompare(b.name));
4728
+ const paged = filtered.slice(0, visibleCount);
4729
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4730
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4731
+ className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
4732
+ children: [
4733
+ tt("repo.ready", { count: entries.length }),
4734
+ " · ",
4735
+ repoDiscoverState.data.ref !== null ? `ref ${repoDiscoverState.data.ref}` : ""
4736
+ ]
4737
+ }),
4738
+ repoDiscoverState.data.truncated === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4739
+ className: panel_module_css_default.errorBanner,
4740
+ style: {
4741
+ background: "rgba(210,153,34,0.08)",
4742
+ color: "#d29922",
4743
+ borderLeft: "3px solid #d29922"
4744
+ },
4745
+ children: tt("repo.truncated")
4746
+ }) : null,
4747
+ entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4748
+ className: panel_module_css_default.empty,
4749
+ children: tt("repo.empty")
4750
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4751
+ (() => {
4752
+ const order = /* @__PURE__ */ new Map([["skills", 0], ["design-templates", 1]]);
4753
+ const roots = [...new Set(entries.map((e) => e.root))].sort((a, b) => {
4754
+ const ao = order.get(a) ?? 99;
4755
+ const bo = order.get(b) ?? 99;
4756
+ return ao !== bo ? ao - bo : a.localeCompare(b);
4757
+ });
4758
+ const counts = /* @__PURE__ */ new Map();
4759
+ for (const e of entries) counts.set(e.root, (counts.get(e.root) ?? 0) + 1);
4760
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4761
+ style: {
4762
+ display: "flex",
4763
+ gap: 8,
4764
+ marginBottom: 8,
4765
+ flexWrap: "wrap"
4766
+ },
4767
+ children: [
4768
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4769
+ className: panel_module_css_default.input,
4770
+ style: {
4771
+ flex: 1,
4772
+ minWidth: 140
4773
+ },
4774
+ value: repoSearch,
4775
+ onChange: (e) => setRepoSearch(e.target.value),
4776
+ placeholder: "搜索技能名…"
4777
+ }),
4778
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4779
+ type: "button",
4780
+ className: panel_module_css_default.button + (repoFilter === "all" ? " " + panel_module_css_default.primary : ""),
4781
+ style: {
4782
+ padding: "6px 10px",
4783
+ fontSize: 12
4784
+ },
4785
+ onClick: () => setRepoFilter("all"),
4786
+ children: ["全部 ", entries.length]
4787
+ }),
4788
+ roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4789
+ type: "button",
4790
+ className: panel_module_css_default.button + (repoFilter === root ? " " + panel_module_css_default.primary : ""),
4791
+ style: {
4792
+ padding: "6px 10px",
4793
+ fontSize: 12
4794
+ },
4795
+ onClick: () => setRepoFilter(root),
4796
+ children: [
4797
+ root,
4798
+ " ",
4799
+ counts.get(root) ?? 0
4800
+ ]
4801
+ }, root))
4802
+ ]
4803
+ });
4804
+ })(),
4805
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4806
+ className: panel_module_css_default.hintLine,
4807
+ style: {
4808
+ display: "flex",
4809
+ justifyContent: "space-between",
4810
+ flexWrap: "wrap",
4811
+ gap: 8
4812
+ },
4813
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
4814
+ "已选 ",
4815
+ selected.length,
4816
+ "/",
4817
+ entries.length,
4818
+ " · ",
4819
+ formatBytes(selectedBytes),
4820
+ " · 显示 ",
4821
+ paged.length,
4822
+ "/",
4823
+ filtered.length,
4824
+ "(过滤后)"
4825
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4826
+ style: {
4827
+ display: "flex",
4828
+ gap: 6
4829
+ },
4830
+ children: [
4831
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4832
+ type: "button",
4833
+ className: panel_module_css_default.button,
4834
+ style: {
4835
+ padding: "4px 8px",
4836
+ fontSize: 11
4837
+ },
4838
+ onClick: () => {
4839
+ setRepoSelected(new Set(filtered.filter((e) => !isExisting(e)).slice(0, visibleCount).map((e) => e.path)));
4840
+ },
4841
+ children: "全选当前显示"
4842
+ }),
4843
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4844
+ type: "button",
4845
+ className: panel_module_css_default.button,
4846
+ style: {
4847
+ padding: "4px 8px",
4848
+ fontSize: 11
4849
+ },
4850
+ onClick: () => {
4851
+ setRepoSelected(new Set(entries.filter((e) => !isExisting(e)).map((e) => e.path)));
4852
+ },
4853
+ children: ["全选全部 ", entries.filter((e) => !isExisting(e)).length]
4854
+ }),
4855
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4856
+ type: "button",
4857
+ className: panel_module_css_default.button,
4858
+ style: {
4859
+ padding: "4px 8px",
4860
+ fontSize: 11
4861
+ },
4862
+ onClick: () => {
4863
+ setRepoSelected(/* @__PURE__ */ new Set());
4864
+ },
4865
+ children: tt("repo.clearAll")
4866
+ })
4867
+ ]
4868
+ })]
4869
+ }),
4870
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4871
+ className: panel_module_css_default.scanList,
4872
+ children: paged.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4873
+ className: panel_module_css_default.empty,
4874
+ style: { padding: 20 },
4875
+ children: "无匹配技能"
4876
+ }) : paged.map((entry) => {
4877
+ const existing = isExisting(entry);
4878
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4879
+ className: panel_module_css_default.row + (existing ? " " + panel_module_css_default.rowMuted : ""),
4880
+ children: [
4881
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4882
+ type: "checkbox",
4883
+ checked: repoSelected.has(entry.path),
4884
+ disabled: existing,
4885
+ onChange: (event) => {
4886
+ toggleRepoSelected(entry.path, event.target.checked);
4887
+ }
4888
+ }),
4889
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4890
+ className: panel_module_css_default.rowMain,
4891
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4892
+ className: panel_module_css_default.rowName,
4893
+ children: entry.name
4894
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4895
+ className: panel_module_css_default.rowDesc,
4896
+ children: [
4897
+ entry.dir,
4898
+ " · ",
4899
+ tt("repo.files", {
4900
+ count: entry.fileCount,
4901
+ size: formatBytes(entry.totalBytes)
4902
+ })
4903
+ ]
4904
+ })]
4905
+ }),
4906
+ existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4907
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
4908
+ children: tt("repo.existing")
4909
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4910
+ className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4911
+ children: entry.origin
4912
+ })
4913
+ ]
4914
+ }, entry.path);
4915
+ })
4916
+ }),
4917
+ visibleCount < filtered.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4918
+ style: {
4919
+ textAlign: "center",
4920
+ marginTop: 8
4921
+ },
4922
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4923
+ type: "button",
4924
+ className: panel_module_css_default.button,
4925
+ onClick: () => setVisibleCount((n) => n + 50),
4926
+ children: [
4927
+ "加载更多 50 (剩余 ",
4928
+ filtered.length - visibleCount,
4929
+ ")"
4930
+ ]
4931
+ })
4932
+ }) : null,
4933
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4934
+ className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
4935
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4936
+ type: "button",
4937
+ className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4938
+ disabled: selected.length === 0 || repoImporting,
4939
+ onClick: () => {
4940
+ importRepo();
4941
+ },
4942
+ children: repoImporting ? tt("repo.importing") : `${tt("repo.import", { count: selected.length })} · ${formatBytes(selectedBytes)}`
4943
+ }), repoImporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4944
+ type: "button",
4945
+ className: panel_module_css_default.button,
4946
+ onClick: () => {
4947
+ cancelImport();
4948
+ },
4949
+ children: tt("repo.cancel")
4950
+ }) : null]
4951
+ }),
4952
+ repoResult !== null && repoResult.status === "running" ? (() => {
4953
+ const pct = repoResult.totalBytes > 0 ? Math.min(100, Math.round(repoResult.downloadedBytes / repoResult.totalBytes * 100)) : repoResult.total > 0 ? Math.round(repoResult.done / repoResult.total * 100) : 0;
4954
+ const speed = formatSpeed(repoResult.bytesPerSecond);
4955
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4956
+ className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
4957
+ children: [
4958
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4959
+ style: {
4960
+ display: "flex",
4961
+ alignItems: "center",
4962
+ gap: 8,
4963
+ marginBottom: 6
4964
+ },
4965
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4966
+ className: panel_module_css_default.scanProgressTrack,
4967
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4968
+ className: panel_module_css_default.scanProgressFill,
4969
+ style: { width: `${pct}%` }
4970
+ })
4971
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4972
+ style: {
4973
+ fontSize: 11,
4974
+ opacity: .75,
4975
+ minWidth: 32,
4976
+ textAlign: "right"
4977
+ },
4978
+ children: [pct, "%"]
4979
+ })]
4980
+ }),
4981
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4982
+ className: panel_module_css_default.hintLine,
4983
+ children: [
4984
+ formatBytes(repoResult.downloadedBytes),
4985
+ " / ",
4986
+ formatBytes(repoResult.totalBytes),
4987
+ speed !== "" ? ` · ${speed}` : "",
4988
+ " · ",
4989
+ repoResult.done,
4990
+ "/",
4991
+ repoResult.total,
4992
+ " 个技能"
4993
+ ]
4994
+ }),
4995
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4996
+ className: panel_module_css_default.hintLine,
4997
+ style: { marginTop: 4 },
4998
+ children: [repoResult.current !== void 0 ? tt("repo.importingCurrent", {
4999
+ name: repoResult.current,
5000
+ done: repoResult.done + 1,
5001
+ total: repoResult.total
5002
+ }) : tt("repo.importing"), repoResult.currentFile !== void 0 ? ` · ${repoResult.currentFile}` : ""]
5003
+ }),
5004
+ repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5005
+ className: panel_module_css_default.errorBanner,
5006
+ style: { marginTop: 6 },
5007
+ children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
5008
+ }) : null
5009
+ ]
5010
+ });
5011
+ })() : null
5012
+ ] })
5013
+ ] });
5014
+ })() : null,
5015
+ repoResult !== null && repoResult.status !== "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5016
+ className: repoResult.status === "cancelled" ? panel_module_css_default.errorBanner + " " + panel_module_css_default.actionsTop : panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
5017
+ children: [
5018
+ repoResult.status === "cancelled" ? tt("repo.cancelled", {
5019
+ imported: repoResult.imported.length,
5020
+ total: repoResult.total
5021
+ }) : `${tt("repo.imported", { count: repoResult.imported.length })} · ${tt("repo.skippedExisting", { count: repoResult.skipped.length })} · ${tt("repo.failed", { count: repoResult.failed.length })}`,
5022
+ repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5023
+ style: { marginTop: 6 },
5024
+ children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
5025
+ }) : null,
5026
+ repoResult.status === "error" && repoResult.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5027
+ style: { marginTop: 6 },
5028
+ children: repoResult.error
5029
+ }) : null
5030
+ ]
5031
+ }) : null
5032
+ ]
5033
+ })]
5034
+ });
5035
+ }
5036
+ //#endregion
5037
+ //#region src/client/panel/MarketView.tsx
5038
+ /**
5039
+ * Market tab: one unified market list — built-in catalog entries (add
5040
+ * button while not yet added) and the user's market sources (full state
5041
+ * badges + actions once added, plus custom sources), with check-all and
5042
+ * update-all actions and the repo scan result with the importable checklist.
5043
+ * 行渲染在 MarketSourceRow,扫描卡片在 RepoScanCard,数字格式在 market-format。
5044
+ */
5045
+ function MarketView(props) {
5046
+ const { hub } = props;
5047
+ const { marketState, sourceCheck, scanningRepo, newSourceName, setNewSourceName, tagBusy, addSource, addMarketSource, checkMarket, checkSources, updateAllDialog, setUpdateAllDialog, updateAll } = hub;
5048
+ /** 有可更新技能的来源(全部更新按钮与确认列表共用)。 */
5049
+ const updatableRepos = Object.entries(sourceCheck).filter(([, check]) => check.changed && check.updated.length > 0);
5050
+ (0, react.useEffect)(() => {
5051
+ hub.loadMarketStats();
5052
+ }, []);
5053
+ const rows = [];
5054
+ for (const entry of MARKET_CATALOG) {
5055
+ const record = marketState.repos.find((item) => item.repo === entry.repo);
5056
+ if (record !== void 0) rows.push({
4549
5057
  key: entry.repo,
4550
- element: sourceRow(record)
5058
+ element: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarketSourceRow, {
5059
+ hub,
5060
+ record
5061
+ })
4551
5062
  });
4552
5063
  else {
4553
5064
  const busy = scanningRepo !== null || tagBusy;
@@ -4587,7 +5098,10 @@ window.__ModuleLoader__.load({
4587
5098
  if (MARKET_CATALOG.some((entry) => entry.repo === record.repo)) continue;
4588
5099
  rows.push({
4589
5100
  key: record.repo,
4590
- element: sourceRow(record)
5101
+ element: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarketSourceRow, {
5102
+ hub,
5103
+ record
5104
+ })
4591
5105
  });
4592
5106
  }
4593
5107
  const unaddedCatalog = MARKET_CATALOG.filter((entry) => !marketState.repos.some((record) => record.repo === entry.repo));
@@ -4662,393 +5176,18 @@ window.__ModuleLoader__.load({
4662
5176
  marketState.status !== "loading" ? rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react.Fragment, { children: row.element }, row.key)) : null
4663
5177
  ]
4664
5178
  }),
4665
- repoDiscoverState.status !== "idle" || repoResult !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
4666
- className: panel_module_css_default.section,
4667
- style: {
4668
- borderLeft: "3px solid var(--hub-model, #2f81f7)",
4669
- background: "rgba(47,129,247,0.04)"
4670
- },
4671
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4672
- className: panel_module_css_default.sectionTitle + " " + panel_module_css_default.sectionHeadRow,
4673
- style: { margin: "10px 14px 6px" },
4674
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4675
- className: panel_module_css_default.sectionTitleFill,
4676
- style: {
4677
- display: "flex",
4678
- alignItems: "center",
4679
- gap: 6
4680
- },
4681
- children: ["扫描结果", /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4682
- style: {
4683
- fontWeight: 400,
4684
- opacity: .75
4685
- },
4686
- children: [
4687
- "— ",
4688
- repoDiscoverState.status === "ready" ? repoDiscoverState.data.repo : repoDiscoverState.status === "error" ? scanningRepo ?? "" : scanningRepo ?? "",
4689
- repoDiscoverState.status === "ready" && repoDiscoverState.data.ref !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4690
- style: {
4691
- opacity: .5,
4692
- marginLeft: 6
4693
- },
4694
- children: ["ref ", repoDiscoverState.data.ref]
4695
- }) : null
4696
- ]
4697
- })]
4698
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4699
- type: "button",
4700
- className: panel_module_css_default.opBtn,
4701
- onClick: () => {
4702
- hub.clearScan();
4703
- },
4704
- children: "关闭"
4705
- })]
4706
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4707
- style: { padding: "0 12px 12px" },
4708
- children: [
4709
- repoDiscoverState.status === "scanning" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4710
- className: panel_module_css_default.empty,
4711
- style: { padding: "12px 0" },
4712
- children: tt("market.scanning")
4713
- }) : null,
4714
- repoDiscoverState.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4715
- className: panel_module_css_default.errorBanner,
4716
- children: repoDiscoverState.message
4717
- }) : null,
4718
- repoDiscoverState.status === "ready" ? (() => {
4719
- const entries = repoDiscoverState.data.entries;
4720
- const installedNames = /* @__PURE__ */ new Set([
4721
- ...(hub.catalog?.skills ?? []).map((s) => s.name),
4722
- ...(hub.catalog?.disabled ?? []).map((d) => d.name),
4723
- ...(repoResult?.imported ?? []).map((r) => r.name),
4724
- ...(repoResult?.skipped ?? []).map((r) => r.name)
4725
- ]);
4726
- const isExisting = (entry) => entry.existing || installedNames.has(entry.name);
4727
- const selected = entries.filter((entry) => repoSelected.has(entry.path) && !isExisting(entry));
4728
- const selectedBytes = selected.reduce((s, e) => s + e.totalBytes, 0);
4729
- const filtered = entries.filter((entry) => {
4730
- if (repoFilter !== "all" && entry.root !== repoFilter) return false;
4731
- if (repoSearch.trim() !== "" && !entry.name.toLowerCase().includes(repoSearch.trim().toLowerCase())) return false;
4732
- return true;
4733
- }).sort((a, b) => a.name.localeCompare(b.name));
4734
- const paged = filtered.slice(0, visibleCount);
4735
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4736
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4737
- className: panel_module_css_default.hintLine + " " + panel_module_css_default.hintInline,
4738
- children: [
4739
- tt("repo.ready", { count: entries.length }),
4740
- " · ",
4741
- repoDiscoverState.data.ref !== null ? `ref ${repoDiscoverState.data.ref}` : ""
4742
- ]
4743
- }),
4744
- repoDiscoverState.data.truncated === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4745
- className: panel_module_css_default.errorBanner,
4746
- style: {
4747
- background: "rgba(210,153,34,0.08)",
4748
- color: "#d29922",
4749
- borderLeft: "3px solid #d29922"
4750
- },
4751
- children: tt("repo.truncated")
4752
- }) : null,
4753
- entries.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4754
- className: panel_module_css_default.empty,
4755
- children: tt("repo.empty")
4756
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
4757
- (() => {
4758
- const order = /* @__PURE__ */ new Map([["skills", 0], ["design-templates", 1]]);
4759
- const roots = [...new Set(entries.map((e) => e.root))].sort((a, b) => {
4760
- const ao = order.get(a) ?? 99;
4761
- const bo = order.get(b) ?? 99;
4762
- return ao !== bo ? ao - bo : a.localeCompare(b);
4763
- });
4764
- const counts = /* @__PURE__ */ new Map();
4765
- for (const e of entries) counts.set(e.root, (counts.get(e.root) ?? 0) + 1);
4766
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4767
- style: {
4768
- display: "flex",
4769
- gap: 8,
4770
- marginBottom: 8,
4771
- flexWrap: "wrap"
4772
- },
4773
- children: [
4774
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4775
- className: panel_module_css_default.input,
4776
- style: {
4777
- flex: 1,
4778
- minWidth: 140
4779
- },
4780
- value: repoSearch,
4781
- onChange: (e) => setRepoSearch(e.target.value),
4782
- placeholder: "搜索技能名…"
4783
- }),
4784
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4785
- type: "button",
4786
- className: panel_module_css_default.button + (repoFilter === "all" ? " " + panel_module_css_default.primary : ""),
4787
- style: {
4788
- padding: "6px 10px",
4789
- fontSize: 12
4790
- },
4791
- onClick: () => setRepoFilter("all"),
4792
- children: ["全部 ", entries.length]
4793
- }),
4794
- roots.map((root) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4795
- type: "button",
4796
- className: panel_module_css_default.button + (repoFilter === root ? " " + panel_module_css_default.primary : ""),
4797
- style: {
4798
- padding: "6px 10px",
4799
- fontSize: 12
4800
- },
4801
- onClick: () => setRepoFilter(root),
4802
- children: [
4803
- root,
4804
- " ",
4805
- counts.get(root) ?? 0
4806
- ]
4807
- }, root))
4808
- ]
4809
- });
4810
- })(),
4811
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4812
- className: panel_module_css_default.hintLine,
4813
- style: {
4814
- display: "flex",
4815
- justifyContent: "space-between",
4816
- flexWrap: "wrap",
4817
- gap: 8
4818
- },
4819
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
4820
- "已选 ",
4821
- selected.length,
4822
- "/",
4823
- entries.length,
4824
- " · ",
4825
- formatBytes(selectedBytes),
4826
- " · 显示 ",
4827
- paged.length,
4828
- "/",
4829
- filtered.length,
4830
- "(过滤后)"
4831
- ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4832
- style: {
4833
- display: "flex",
4834
- gap: 6
4835
- },
4836
- children: [
4837
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4838
- type: "button",
4839
- className: panel_module_css_default.button,
4840
- style: {
4841
- padding: "4px 8px",
4842
- fontSize: 11
4843
- },
4844
- onClick: () => {
4845
- setRepoSelected(new Set(filtered.filter((e) => !isExisting(e)).slice(0, visibleCount).map((e) => e.path)));
4846
- },
4847
- children: "全选当前显示"
4848
- }),
4849
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4850
- type: "button",
4851
- className: panel_module_css_default.button,
4852
- style: {
4853
- padding: "4px 8px",
4854
- fontSize: 11
4855
- },
4856
- onClick: () => {
4857
- setRepoSelected(new Set(entries.filter((e) => !isExisting(e)).map((e) => e.path)));
4858
- },
4859
- children: ["全选全部 ", entries.filter((e) => !isExisting(e)).length]
4860
- }),
4861
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4862
- type: "button",
4863
- className: panel_module_css_default.button,
4864
- style: {
4865
- padding: "4px 8px",
4866
- fontSize: 11
4867
- },
4868
- onClick: () => {
4869
- setRepoSelected(/* @__PURE__ */ new Set());
4870
- },
4871
- children: tt("repo.clearAll")
4872
- })
4873
- ]
4874
- })]
4875
- }),
4876
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4877
- className: panel_module_css_default.scanList,
4878
- children: paged.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4879
- className: panel_module_css_default.empty,
4880
- style: { padding: 20 },
4881
- children: "无匹配技能"
4882
- }) : paged.map((entry) => {
4883
- const existing = isExisting(entry);
4884
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
4885
- className: panel_module_css_default.row + (existing ? " " + panel_module_css_default.rowMuted : ""),
4886
- children: [
4887
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
4888
- type: "checkbox",
4889
- checked: repoSelected.has(entry.path),
4890
- disabled: existing,
4891
- onChange: (event) => {
4892
- toggleRepoSelected(entry.path, event.target.checked);
4893
- }
4894
- }),
4895
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4896
- className: panel_module_css_default.rowMain,
4897
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4898
- className: panel_module_css_default.rowName,
4899
- children: entry.name
4900
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4901
- className: panel_module_css_default.rowDesc,
4902
- children: [
4903
- entry.dir,
4904
- " · ",
4905
- tt("repo.files", {
4906
- count: entry.fileCount,
4907
- size: formatBytes(entry.totalBytes)
4908
- })
4909
- ]
4910
- })]
4911
- }),
4912
- existing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4913
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeReadonly,
4914
- children: tt("repo.existing")
4915
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
4916
- className: panel_module_css_default.badge + " " + panel_module_css_default.badgeSource,
4917
- children: entry.origin
4918
- })
4919
- ]
4920
- }, entry.path);
4921
- })
4922
- }),
4923
- visibleCount < filtered.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4924
- style: {
4925
- textAlign: "center",
4926
- marginTop: 8
4927
- },
4928
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
4929
- type: "button",
4930
- className: panel_module_css_default.button,
4931
- onClick: () => setVisibleCount((n) => n + 50),
4932
- children: [
4933
- "加载更多 50 (剩余 ",
4934
- filtered.length - visibleCount,
4935
- ")"
4936
- ]
4937
- })
4938
- }) : null,
4939
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4940
- className: panel_module_css_default.buttons + " " + panel_module_css_default.actionsTop,
4941
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4942
- type: "button",
4943
- className: panel_module_css_default.button + " " + panel_module_css_default.primary,
4944
- disabled: selected.length === 0 || repoImporting,
4945
- onClick: () => {
4946
- importRepo();
4947
- },
4948
- children: repoImporting ? tt("repo.importing") : `${tt("repo.import", { count: selected.length })} · ${formatBytes(selectedBytes)}`
4949
- }), repoImporting ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
4950
- type: "button",
4951
- className: panel_module_css_default.button,
4952
- onClick: () => {
4953
- cancelImport();
4954
- },
4955
- children: tt("repo.cancel")
4956
- }) : null]
4957
- }),
4958
- repoResult !== null && repoResult.status === "running" ? (() => {
4959
- const pct = repoResult.totalBytes > 0 ? Math.min(100, Math.round(repoResult.downloadedBytes / repoResult.totalBytes * 100)) : repoResult.total > 0 ? Math.round(repoResult.done / repoResult.total * 100) : 0;
4960
- const speed = formatSpeed(repoResult.bytesPerSecond);
4961
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4962
- className: panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
4963
- children: [
4964
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4965
- style: {
4966
- display: "flex",
4967
- alignItems: "center",
4968
- gap: 8,
4969
- marginBottom: 6
4970
- },
4971
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4972
- className: panel_module_css_default.scanProgressTrack,
4973
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4974
- className: panel_module_css_default.scanProgressFill,
4975
- style: { width: `${pct}%` }
4976
- })
4977
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4978
- style: {
4979
- fontSize: 11,
4980
- opacity: .75,
4981
- minWidth: 32,
4982
- textAlign: "right"
4983
- },
4984
- children: [pct, "%"]
4985
- })]
4986
- }),
4987
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4988
- className: panel_module_css_default.hintLine,
4989
- children: [
4990
- formatBytes(repoResult.downloadedBytes),
4991
- " / ",
4992
- formatBytes(repoResult.totalBytes),
4993
- speed !== "" ? ` · ${speed}` : "",
4994
- " · ",
4995
- repoResult.done,
4996
- "/",
4997
- repoResult.total,
4998
- " 个技能"
4999
- ]
5000
- }),
5001
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5002
- className: panel_module_css_default.hintLine,
5003
- style: { marginTop: 4 },
5004
- children: [repoResult.current !== void 0 ? tt("repo.importingCurrent", {
5005
- name: repoResult.current,
5006
- done: repoResult.done + 1,
5007
- total: repoResult.total
5008
- }) : tt("repo.importing"), repoResult.currentFile !== void 0 ? ` · ${repoResult.currentFile}` : ""]
5009
- }),
5010
- repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5011
- className: panel_module_css_default.errorBanner,
5012
- style: { marginTop: 6 },
5013
- children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
5014
- }) : null
5015
- ]
5016
- });
5017
- })() : null
5018
- ] })
5019
- ] });
5020
- })() : null,
5021
- repoResult !== null && repoResult.status !== "running" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5022
- className: repoResult.status === "cancelled" ? panel_module_css_default.errorBanner + " " + panel_module_css_default.actionsTop : panel_module_css_default.formSuccess + " " + panel_module_css_default.actionsTop,
5023
- children: [
5024
- repoResult.status === "cancelled" ? tt("repo.cancelled", {
5025
- imported: repoResult.imported.length,
5026
- total: repoResult.total
5027
- }) : `${tt("repo.imported", { count: repoResult.imported.length })} · ${tt("repo.skippedExisting", { count: repoResult.skipped.length })} · ${tt("repo.failed", { count: repoResult.failed.length })}`,
5028
- repoResult.failed.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5029
- style: { marginTop: 6 },
5030
- children: repoResult.failed.map((f) => `${f.name}: ${f.error}`).join("; ")
5031
- }) : null,
5032
- repoResult.status === "error" && repoResult.error ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5033
- style: { marginTop: 6 },
5034
- children: repoResult.error
5035
- }) : null
5036
- ]
5037
- }) : null
5038
- ]
5039
- })]
5040
- }) : null,
5041
- updateAllDialog ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
5042
- title: tt("market.updateAllConfirmTitle"),
5043
- text: tt("market.updateAllConfirmText"),
5044
- items: updatableRepos.map(([repo, check]) => tt("market.updateAllItem", {
5045
- repo,
5046
- count: check.updated.length
5047
- })),
5048
- confirmLabel: tt("market.updateAll"),
5049
- danger: true,
5050
- onCancel: () => {
5051
- setUpdateAllDialog(false);
5179
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RepoScanCard, { hub }),
5180
+ updateAllDialog ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfirmDialog, {
5181
+ title: tt("market.updateAllConfirmTitle"),
5182
+ text: tt("market.updateAllConfirmText"),
5183
+ items: updatableRepos.map(([repo, check]) => tt("market.updateAllItem", {
5184
+ repo,
5185
+ count: check.updated.length
5186
+ })),
5187
+ confirmLabel: tt("market.updateAll"),
5188
+ danger: true,
5189
+ onCancel: () => {
5190
+ setUpdateAllDialog(false);
5052
5191
  },
5053
5192
  onConfirm: () => {
5054
5193
  updateAll();
@@ -5057,18 +5196,7 @@ window.__ModuleLoader__.load({
5057
5196
  ] });
5058
5197
  }
5059
5198
  //#endregion
5060
- //#region src/client/panel/useSkillHub.ts
5061
- /**
5062
- * useSkillHub — the panel's state owner. Everything the skill hub panel
5063
- * knows (catalog, groups, sources, market, dialogs, drafts) plus every
5064
- * async flow lives here; the panel itself only renders. All hooks run
5065
- * unconditionally at the top, so the panel may early-return for the detail
5066
- * and tag-editor views without violating the rules of hooks.
5067
- */
5068
- /** Catalog poll interval while the panel is mounted (the provider watcher feeds this). */
5069
- const POLL_MS = 5e3;
5070
- /** Relaxed poll for slower-changing data (stats, groups, sources, config). */
5071
- const SLOW_POLL_MS = 6e4;
5199
+ //#region src/client/panel/hooks/shared.ts
5072
5200
  /** localStorage key of the last daily auto-check timestamp (survives sessions). */
5073
5201
  const AUTO_CHECK_KEY = "skill-hub.last-auto-check";
5074
5202
  /** Daily auto-check interval: at most one network round per day. */
@@ -5090,108 +5218,16 @@ window.__ModuleLoader__.load({
5090
5218
  localStorage.setItem(AUTO_CHECK_KEY, String(Date.now()));
5091
5219
  } catch {}
5092
5220
  }
5093
- function useSkillHub(api) {
5094
- const [catalog, setCatalog] = (0, react.useState)(null);
5095
- const [loading, setLoading] = (0, react.useState)(true);
5096
- const [loadError, setLoadError] = (0, react.useState)(null);
5097
- /** Green success banner (create finished); shown outside the closing form. */
5098
- const [successBanner, setSuccessBanner] = (0, react.useState)(null);
5221
+ //#endregion
5222
+ //#region src/client/panel/hooks/useMetaFlow.ts
5223
+ /**
5224
+ * useMetaFlow — 自身更新检查 + 调用统计 + 配置:只依赖 api,
5225
+ * 不碰其他域。补捞 effect 自包含(uses.size + loadUses)。
5226
+ */
5227
+ function useMetaFlow(api) {
5099
5228
  const [updateState, setUpdateState] = (0, react.useState)({ status: "idle" });
5100
- const [repoDiscoverState, setRepoDiscoverState] = (0, react.useState)({ status: "idle" });
5101
- /** Market repo currently being scanned (null when idle); per-row busy state. */
5102
- const [scanningRepo, setScanningRepo] = (0, react.useState)(null);
5103
- const [repoSelected, setRepoSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
5104
- const [repoImporting, setRepoImporting] = (0, react.useState)(false);
5105
- const [repoResult, setRepoResult] = (0, react.useState)(null);
5106
- const [importJobId, setImportJobId] = (0, react.useState)(null);
5107
- const pollAbortRef = (0, react.useRef)(null);
5108
- const [search, setSearch] = (0, react.useState)("");
5109
- /** 工作区(项目)路径;空 = 只看用户级技能。 */
5110
- const [workspace, setWorkspace] = (0, react.useState)("");
5111
- const [detail, setDetail] = (0, react.useState)(null);
5112
- const [detailLoading, setDetailLoading] = (0, react.useState)(false);
5113
- const [busyNames, setBusyNames] = (0, react.useState)(/* @__PURE__ */ new Set());
5114
- const [batchBusy, setBatchBusy] = (0, react.useState)(false);
5115
- const [showForm, setShowForm] = (0, react.useState)(false);
5116
- const [formName, setFormName] = (0, react.useState)("");
5117
- const [formDesc, setFormDesc] = (0, react.useState)("");
5118
- const [formRoot, setFormRoot] = (0, react.useState)("user-dsh");
5119
- const [formBusy, setFormBusy] = (0, react.useState)(false);
5120
- const [formMessage, setFormMessage] = (0, react.useState)(null);
5121
5229
  const [uses, setUses] = (0, react.useState)(/* @__PURE__ */ new Map());
5122
5230
  const [hubConfig, setHubConfig] = (0, react.useState)(null);
5123
- const [tab, setTab] = (0, react.useState)("sources");
5124
- const [skillView, setSkillView] = (0, react.useState)("groups");
5125
- const [sourceFilter, setSourceFilter] = (0, react.useState)("all");
5126
- const [invocationFilter, setInvocationFilter] = (0, react.useState)("all");
5127
- const [sortKey, setSortKey] = (0, react.useState)("name");
5128
- const [marketState, setMarketState] = (0, react.useState)({
5129
- status: "loading",
5130
- repos: []
5131
- });
5132
- const [marketCheck, setMarketCheck] = (0, react.useState)({});
5133
- const [branchChoice, setBranchChoice] = (0, react.useState)(null);
5134
- const [branchBusy, setBranchBusy] = (0, react.useState)(false);
5135
- const [marketSyncDialog, setMarketSyncDialog] = (0, react.useState)(null);
5136
- const [syncingMarket, setSyncingMarket] = (0, react.useState)(null);
5137
- const [syncBusy, setSyncBusy] = (0, react.useState)(false);
5138
- const [newSourceName, setNewSourceName] = (0, react.useState)("");
5139
- const [groupsState, setGroupsState] = (0, react.useState)(null);
5140
- const [sourcesState, setSourcesState] = (0, react.useState)(null);
5141
- const [sourceCheck, setSourceCheck] = (0, react.useState)({});
5142
- const [checkingSource, setCheckingSource] = (0, react.useState)(null);
5143
- const [syncingSource, setSyncingSource] = (0, react.useState)(null);
5144
- const [conflictDialog, setConflictDialog] = (0, react.useState)(null);
5145
- const [confirmDialog, setConfirmDialog] = (0, react.useState)(null);
5146
- const [deleteSkillDialog, setDeleteSkillDialog] = (0, react.useState)(null);
5147
- const [deleteGroupDialog, setDeleteGroupDialog] = (0, react.useState)(null);
5148
- const [confirmClearTrash, setConfirmClearTrash] = (0, react.useState)(false);
5149
- /** 「全部更新」确认对话框(市场 tab)。 */
5150
- const [updateAllDialog, setUpdateAllDialog] = (0, react.useState)(false);
5151
- const [editingTag, setEditingTag] = (0, react.useState)(null);
5152
- const [editName, setEditName] = (0, react.useState)("");
5153
- const [membersDraft, setMembersDraft] = (0, react.useState)(/* @__PURE__ */ new Set());
5154
- const [newTagName, setNewTagName] = (0, react.useState)("");
5155
- const [tagBusy, setTagBusy] = (0, react.useState)(false);
5156
- const [editSearch, setEditSearch] = (0, react.useState)("");
5157
- /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。技能总数 >80 时首次加载自动折叠 personal。 */
5158
- const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
5159
- const autoCollapsedPersonal = (0, react.useRef)(false);
5160
- /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
5161
- const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
5162
- const [showLegend, setShowLegend] = (0, react.useState)(false);
5163
- const [editMode, setEditMode] = (0, react.useState)(false);
5164
- const toggleGroupCollapse = (0, react.useCallback)((key) => {
5165
- setCollapsedGroups((previous) => {
5166
- const next = new Set(previous);
5167
- if (next.has(key)) next.delete(key);
5168
- else next.add(key);
5169
- return next;
5170
- });
5171
- }, []);
5172
- const toggleSubdivide = (0, react.useCallback)((key) => {
5173
- setSubdividedProjects((previous) => {
5174
- const next = new Set(previous);
5175
- if (next.has(key)) next.delete(key);
5176
- else next.add(key);
5177
- return next;
5178
- });
5179
- }, []);
5180
- const load = (0, react.useCallback)(async () => {
5181
- try {
5182
- const next = await api.catalog(workspace !== "" ? { cwd: workspace } : void 0);
5183
- setCatalog(next);
5184
- if (!autoCollapsedPersonal.current && next.skills.length + next.disabled.length > 80) {
5185
- autoCollapsedPersonal.current = true;
5186
- setCollapsedGroups((previous) => new Set(previous).add("uncategorized-source"));
5187
- }
5188
- setLoadError(null);
5189
- } catch (error) {
5190
- setLoadError(errorMessage(error));
5191
- } finally {
5192
- setLoading(false);
5193
- }
5194
- }, [api, workspace]);
5195
5231
  const loadUses = (0, react.useCallback)(async () => {
5196
5232
  try {
5197
5233
  const result = await api.stats();
@@ -5206,7 +5242,6 @@ window.__ModuleLoader__.load({
5206
5242
  setHubConfig((await api.config()).config);
5207
5243
  } catch {}
5208
5244
  }, [api]);
5209
- /** Check the hub's own GitHub release once, and again on demand. */
5210
5245
  const checkUpdate = (0, react.useCallback)(async () => {
5211
5246
  setUpdateState({ status: "checking" });
5212
5247
  try {
@@ -5221,95 +5256,96 @@ window.__ModuleLoader__.load({
5221
5256
  });
5222
5257
  }
5223
5258
  }, [api]);
5224
- /** 加载用户 tag 分组 + 系统集合组 + origin 映射。 */
5225
- const loadGroups = (0, react.useCallback)(async () => {
5226
- try {
5227
- setGroupsState(await api.groups());
5228
- } catch (error) {
5229
- setLoadError(errorMessage(error));
5230
- }
5231
- }, [api]);
5232
- /** 加载来源记录 + 回收站。 */
5233
- const loadSources = (0, react.useCallback)(async () => {
5234
- try {
5235
- setSourcesState(await api.sources());
5236
- } catch (error) {
5237
- setLoadError(errorMessage(error));
5238
- }
5239
- }, [api]);
5240
- /** 加载市场源列表。 */
5241
- const loadMarket = (0, react.useCallback)(async () => {
5242
- try {
5243
- const next = await api.market();
5244
- setMarketState({
5245
- status: "ready",
5246
- repos: next.repos
5247
- });
5248
- } catch (error) {
5249
- setMarketState({
5250
- status: "error",
5251
- repos: []
5252
- });
5253
- setLoadError(errorMessage(error));
5254
- }
5255
- }, [api]);
5256
- /** 把最新的 tag 列表合并进 groups 状态(tags 以外的字段保持原样)。 */
5257
- const applyTags = (0, react.useCallback)((tags) => {
5258
- setGroupsState((previous) => previous === null ? {
5259
- ok: true,
5260
- tags,
5261
- collections: [],
5262
- origins: {}
5263
- } : {
5264
- ...previous,
5265
- tags
5266
- });
5267
- }, []);
5259
+ const usesCatchUp = (0, react.useRef)(0);
5268
5260
  (0, react.useEffect)(() => {
5269
- load();
5270
- loadUses();
5271
- loadGroups();
5272
- loadSources();
5273
- loadConfig();
5274
- const fast = window.setInterval(() => {
5275
- load();
5276
- }, POLL_MS);
5277
- const slow = window.setInterval(() => {
5261
+ if (uses.size > 0 || usesCatchUp.current >= 72) return;
5262
+ const timer = window.setInterval(() => {
5263
+ usesCatchUp.current += 1;
5278
5264
  loadUses();
5279
- loadGroups();
5280
- loadSources();
5281
- loadConfig();
5282
- }, SLOW_POLL_MS);
5265
+ if (usesCatchUp.current >= 72) window.clearInterval(timer);
5266
+ }, 5e3);
5283
5267
  return () => {
5284
- window.clearInterval(fast);
5285
- window.clearInterval(slow);
5268
+ window.clearInterval(timer);
5286
5269
  };
5287
- }, [
5288
- load,
5270
+ }, [uses.size, loadUses]);
5271
+ return {
5272
+ updateState,
5273
+ checkUpdate,
5274
+ uses,
5289
5275
  loadUses,
5290
- loadGroups,
5291
- loadSources,
5276
+ hubConfig,
5292
5277
  loadConfig
5278
+ };
5279
+ }
5280
+ //#endregion
5281
+ //#region src/client/panel/hooks/useCatalogFlow.ts
5282
+ /**
5283
+ * useCatalogFlow — 目录域:catalog 加载、详情、开关(单个/禁用态/批量)、
5284
+ * 新建、诊断修复,以及列表过滤/排序派生。跨域刷新只通过 shared 通知,
5285
+ * 不直接碰其他域的 state。
5286
+ */
5287
+ function useCatalogFlow(api, uses, shared, collapsePersonal, clearSourceFilter) {
5288
+ const [catalog, setCatalog] = (0, react.useState)(null);
5289
+ const [loading, setLoading] = (0, react.useState)(true);
5290
+ const [detail, setDetail] = (0, react.useState)(null);
5291
+ const [detailLoading, setDetailLoading] = (0, react.useState)(false);
5292
+ const [busyNames, setBusyNames] = (0, react.useState)(/* @__PURE__ */ new Set());
5293
+ const [search, setSearch] = (0, react.useState)("");
5294
+ /** 工作区(项目)路径;空 = 只看用户级技能。 */
5295
+ const [workspace, setWorkspace] = (0, react.useState)("");
5296
+ const [showForm, setShowForm] = (0, react.useState)(false);
5297
+ const [formName, setFormName] = (0, react.useState)("");
5298
+ const [formDesc, setFormDesc] = (0, react.useState)("");
5299
+ const [formRoot, setFormRoot] = (0, react.useState)("user-dsh");
5300
+ const [formBusy, setFormBusy] = (0, react.useState)(false);
5301
+ const [formMessage, setFormMessage] = (0, react.useState)(null);
5302
+ const [fixingPaths, setFixingPaths] = (0, react.useState)(/* @__PURE__ */ new Set());
5303
+ const [invocationFilter, setInvocationFilter] = (0, react.useState)("all");
5304
+ const [sortKey, setSortKey] = (0, react.useState)("name");
5305
+ const autoCollapsedPersonal = (0, react.useRef)(false);
5306
+ const load = (0, react.useCallback)(async () => {
5307
+ try {
5308
+ const next = await api.catalog(workspace !== "" ? { cwd: workspace } : void 0);
5309
+ setCatalog(next);
5310
+ if (!autoCollapsedPersonal.current && next.skills.length + next.disabled.length > 80) {
5311
+ autoCollapsedPersonal.current = true;
5312
+ collapsePersonal();
5313
+ }
5314
+ shared.clearFail();
5315
+ } catch (error) {
5316
+ shared.fail(errorMessage(error));
5317
+ } finally {
5318
+ setLoading(false);
5319
+ }
5320
+ }, [
5321
+ api,
5322
+ workspace,
5323
+ shared,
5324
+ collapsePersonal
5293
5325
  ]);
5294
5326
  const openDetail = (0, react.useCallback)(async (name) => {
5295
5327
  setDetailLoading(true);
5296
- setLoadError(null);
5328
+ shared.clearFail();
5297
5329
  try {
5298
5330
  setDetail(await api.skill(name, workspace !== "" ? { cwd: workspace } : void 0));
5299
5331
  } catch (error) {
5300
- setLoadError(errorMessage(error));
5332
+ shared.fail(errorMessage(error));
5301
5333
  } finally {
5302
5334
  setDetailLoading(false);
5303
5335
  }
5304
- }, [api, workspace]);
5336
+ }, [
5337
+ api,
5338
+ workspace,
5339
+ shared
5340
+ ]);
5305
5341
  const toggle = (0, react.useCallback)(async (skill, enabled) => {
5306
5342
  setBusyNames((previous) => new Set(previous).add(skill.name));
5307
- setLoadError(null);
5343
+ shared.clearFail();
5308
5344
  try {
5309
5345
  const next = await api.toggle(skill.name, enabled);
5310
5346
  setCatalog(next);
5311
5347
  } catch (error) {
5312
- setLoadError(errorMessage(error));
5348
+ shared.fail(errorMessage(error));
5313
5349
  } finally {
5314
5350
  setBusyNames((previous) => {
5315
5351
  const next = new Set(previous);
@@ -5317,15 +5353,15 @@ window.__ModuleLoader__.load({
5317
5353
  return next;
5318
5354
  });
5319
5355
  }
5320
- }, [api]);
5356
+ }, [api, shared]);
5321
5357
  const enableDisabled = (0, react.useCallback)(async (record) => {
5322
5358
  setBusyNames((previous) => new Set(previous).add(record.name));
5323
- setLoadError(null);
5359
+ shared.clearFail();
5324
5360
  try {
5325
5361
  const next = await api.toggle(record.name, true);
5326
5362
  setCatalog(next);
5327
5363
  } catch (error) {
5328
- setLoadError(errorMessage(error));
5364
+ shared.fail(errorMessage(error));
5329
5365
  } finally {
5330
5366
  setBusyNames((previous) => {
5331
5367
  const next = new Set(previous);
@@ -5333,412 +5369,659 @@ window.__ModuleLoader__.load({
5333
5369
  return next;
5334
5370
  });
5335
5371
  }
5336
- }, [api]);
5372
+ }, [api, shared]);
5337
5373
  /** Toggle an explicit name set in one write (enables disabled members too). */
5338
5374
  const batchToggleNames = (0, react.useCallback)(async (names, enabled) => {
5339
5375
  if (names.length === 0) return;
5340
- setBatchBusy(true);
5341
- setLoadError(null);
5376
+ shared.setBatchBusy(true);
5377
+ shared.clearFail();
5342
5378
  try {
5343
5379
  const next = await api.toggleBatch(names, enabled);
5344
5380
  setCatalog(next.catalog);
5345
- if (next.failures.length > 0) setLoadError("toggle-batch: " + next.failures.map((failure) => failure.name + ": " + failure.error).join("; "));
5381
+ if (next.failures.length > 0) shared.fail("toggle-batch: " + next.failures.map((failure) => failure.name + ": " + failure.error).join("; "));
5346
5382
  } catch (error) {
5347
- setLoadError(errorMessage(error));
5383
+ shared.fail(errorMessage(error));
5348
5384
  } finally {
5349
- setBatchBusy(false);
5385
+ shared.setBatchBusy(false);
5350
5386
  }
5351
- }, [api]);
5352
- /** 所有分组:key → 成员名(tag 与来源集合)。 */
5353
- const groupMap = (0, react.useCallback)(() => {
5354
- const map = /* @__PURE__ */ new Map();
5355
- for (const tag of groupsState?.tags ?? []) map.set("tag:" + tag.id, tag.skillNames);
5356
- for (const collection of groupsState?.collections ?? []) map.set("col:" + collection.name, collection.skillNames);
5357
- return map;
5358
- }, [groupsState]);
5359
- /** 当前启用且可写的技能名(组开关只作用于它们)。 */
5360
- const actionNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).filter((skill) => skill.writable).map((skill) => skill.name)), [catalog]);
5361
- /** 当前启用的全部技能名(含只读,用于派生开关状态)。 */
5362
- const viewNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).map((skill) => skill.name)), [catalog]);
5363
- /**
5364
- * Toggle a whole group via its tri-state switch. Closing with members that
5365
- * are enabled in other groups opens the conflict dialog instead.
5366
- */
5367
- const toggleGroup = (0, react.useCallback)((key, name, view) => {
5368
- const members = groupMap().get(key) ?? [];
5369
- if (members.length === 0) return;
5370
- if (view === "off") {
5371
- batchToggleNames(members, true);
5372
- return;
5387
+ }, [api, shared]);
5388
+ const fixDiagnostic = (0, react.useCallback)(async (path) => {
5389
+ setFixingPaths((previous) => new Set(previous).add(path));
5390
+ shared.clearFail();
5391
+ try {
5392
+ if (!window.confirm(tt("diag.fixConfirm", { path }))) return;
5393
+ await api.fixDiagnostic(path);
5394
+ await load();
5395
+ shared.succeed(tt("diag.fixed"));
5396
+ } catch (error) {
5397
+ shared.fail(errorMessage(error));
5398
+ } finally {
5399
+ setFixingPaths((previous) => {
5400
+ const next = new Set(previous);
5401
+ next.delete(path);
5402
+ return next;
5403
+ });
5373
5404
  }
5374
- const others = [...groupMap().entries()].filter(([otherKey]) => otherKey !== key).map(([, memberNames]) => ({ members: memberNames }));
5375
- const conflicts = conflictsOnClose(members, actionNames, others);
5376
- if (conflicts.length > 0) setConflictDialog({
5377
- key,
5378
- name,
5379
- conflicts
5380
- });
5381
- else batchToggleNames(members.filter((member) => actionNames.has(member)), false);
5382
5405
  }, [
5383
- groupMap,
5384
- actionNames,
5385
- batchToggleNames
5406
+ api,
5407
+ load,
5408
+ shared
5386
5409
  ]);
5387
- /** Resolve the open conflict dialog. */
5388
- const resolveConflict = (0, react.useCallback)(async (closeAll) => {
5389
- const dialog = conflictDialog;
5390
- if (dialog === null) return;
5391
- setConflictDialog(null);
5392
- const members = groupMap().get(dialog.key) ?? [];
5393
- if (closeAll) await batchToggleNames(members.filter((member) => actionNames.has(member)), false);
5394
- else await batchToggleNames(members.filter((member) => actionNames.has(member) && !dialog.conflicts.includes(member)), false);
5410
+ const create = (0, react.useCallback)(async (event) => {
5411
+ event.preventDefault();
5412
+ setFormBusy(true);
5413
+ setFormMessage(null);
5414
+ shared.succeed(null);
5415
+ try {
5416
+ const result = await api.create({
5417
+ name: formName,
5418
+ description: formDesc,
5419
+ root: formRoot
5420
+ });
5421
+ shared.succeed(tt("form.success") + result.path);
5422
+ setFormName("");
5423
+ setFormDesc("");
5424
+ setShowForm(false);
5425
+ await load();
5426
+ } catch (error) {
5427
+ setFormMessage({
5428
+ kind: "error",
5429
+ text: tt("form.error") + errorMessage(error)
5430
+ });
5431
+ } finally {
5432
+ setFormBusy(false);
5433
+ }
5395
5434
  }, [
5435
+ api,
5436
+ formName,
5437
+ formDesc,
5438
+ formRoot,
5439
+ load,
5440
+ shared
5441
+ ]);
5442
+ const normalized = search.trim().toLocaleLowerCase();
5443
+ /** 当前启用且可写的技能名(组开关只作用于它们)。 */
5444
+ const actionNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).filter((skill) => skill.writable).map((skill) => skill.name)), [catalog]);
5445
+ /** 当前启用的全部技能名(含只读,用于派生开关状态)。 */
5446
+ const viewNames = (0, react.useMemo)(() => new Set((catalog?.skills ?? []).map((skill) => skill.name)), [catalog]);
5447
+ const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => {
5448
+ if (invocationFilter === "model" && !skill.invocation.modelInvocable) return false;
5449
+ if (invocationFilter === "user" && !skill.invocation.userInvocable) return false;
5450
+ if (normalized.length === 0) return true;
5451
+ return skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized) || skill.displayName?.toLocaleLowerCase().includes(normalized) || skill.shortDescription?.toLocaleLowerCase().includes(normalized);
5452
+ }), [
5453
+ catalog,
5454
+ normalized,
5455
+ invocationFilter
5456
+ ]);
5457
+ /** Rows actually rendered with a shortened description (shortDescription in use). */
5458
+ const shortenedCount = (0, react.useMemo)(() => filtered.filter((skill) => skill.shortDescription !== void 0).length, [filtered]);
5459
+ return {
5460
+ catalog,
5461
+ loading,
5462
+ detail,
5463
+ detailLoading,
5464
+ busyNames,
5465
+ search,
5466
+ workspace,
5467
+ showForm,
5468
+ formName,
5469
+ formDesc,
5470
+ formRoot,
5471
+ formBusy,
5472
+ formMessage,
5473
+ fixingPaths,
5474
+ invocationFilter,
5475
+ sortKey,
5476
+ normalized,
5477
+ actionNames,
5478
+ viewNames,
5479
+ filtered,
5480
+ sorted: (0, react.useMemo)(() => sortSkills(filtered, sortKey, (name) => uses.get(name)?.count), [
5481
+ filtered,
5482
+ sortKey,
5483
+ uses
5484
+ ]),
5485
+ shortenedCount,
5486
+ setDetail,
5487
+ setSearch,
5488
+ setWorkspace,
5489
+ setShowForm,
5490
+ setFormName,
5491
+ setFormDesc,
5492
+ setFormRoot,
5493
+ setFormMessage,
5494
+ setInvocationFilter,
5495
+ setSortKey,
5496
+ load,
5497
+ openDetail,
5498
+ toggle,
5499
+ enableDisabled,
5500
+ batchToggleNames,
5501
+ fixDiagnostic,
5502
+ create,
5503
+ clearListFilters: (0, react.useCallback)(() => {
5504
+ setSearch("");
5505
+ clearSourceFilter();
5506
+ setInvocationFilter("all");
5507
+ }, [clearSourceFilter])
5508
+ };
5509
+ }
5510
+ //#endregion
5511
+ //#region src/client/panel/hooks/useGroupFlow.ts
5512
+ /**
5513
+ * useGroupFlow — 分组域:groups 加载、tag 新建/删除/成员/重排、集合重排、
5514
+ * 来源顶层重排、组开关与冲突解决。目录侧只消费 batchToggleNames +
5515
+ * actionNames(聚合根传入),不碰目录 state。
5516
+ */
5517
+ function useGroupFlow(api, shared, batchToggleNames, actionNames) {
5518
+ const [groupsState, setGroupsState] = (0, react.useState)(null);
5519
+ const [conflictDialog, setConflictDialog] = (0, react.useState)(null);
5520
+ const [editingTag, setEditingTag] = (0, react.useState)(null);
5521
+ const [editName, setEditName] = (0, react.useState)("");
5522
+ const [membersDraft, setMembersDraft] = (0, react.useState)(/* @__PURE__ */ new Set());
5523
+ const [newTagName, setNewTagName] = (0, react.useState)("");
5524
+ const [editSearch, setEditSearch] = (0, react.useState)("");
5525
+ /** 加载用户 tag 分组 + 系统集合组 + origin 映射。 */
5526
+ const loadGroups = (0, react.useCallback)(async () => {
5527
+ try {
5528
+ setGroupsState(await api.groups());
5529
+ } catch (error) {
5530
+ shared.fail(errorMessage(error));
5531
+ }
5532
+ }, [api, shared]);
5533
+ /** 把最新的 tag 列表合并进 groups 状态(tags 以外的字段保持原样)。 */
5534
+ const applyTags = (0, react.useCallback)((tags) => {
5535
+ setGroupsState((previous) => previous === null ? {
5536
+ ok: true,
5537
+ tags,
5538
+ collections: [],
5539
+ origins: {}
5540
+ } : {
5541
+ ...previous,
5542
+ tags
5543
+ });
5544
+ }, []);
5545
+ /** 所有分组:key → 成员名(tag 与来源集合)。 */
5546
+ const groupMap = (0, react.useCallback)(() => {
5547
+ const map = /* @__PURE__ */ new Map();
5548
+ for (const tag of groupsState?.tags ?? []) map.set("tag:" + tag.id, tag.skillNames);
5549
+ for (const collection of groupsState?.collections ?? []) map.set("col:" + collection.name, collection.skillNames);
5550
+ return map;
5551
+ }, [groupsState]);
5552
+ return {
5553
+ groupsState,
5396
5554
  conflictDialog,
5555
+ editingTag,
5556
+ editName,
5557
+ membersDraft,
5558
+ newTagName,
5559
+ editSearch,
5560
+ setConflictDialog,
5561
+ setEditingTag,
5562
+ setEditName,
5563
+ setMembersDraft,
5564
+ setNewTagName,
5565
+ setEditSearch,
5566
+ loadGroups,
5567
+ applyTags,
5397
5568
  groupMap,
5398
- actionNames,
5399
- batchToggleNames
5400
- ]);
5401
- /** Dismiss a confirm dialog and run the pending source action. */
5402
- const runConfirmed = (0, react.useCallback)(async () => {
5403
- const dialog = confirmDialog;
5404
- if (dialog === null) return;
5405
- setConfirmDialog(null);
5406
- if (dialog.kind === "sync") {
5407
- setSyncingSource(dialog.repo);
5408
- setLoadError(null);
5569
+ toggleGroup: (0, react.useCallback)((key, name, view) => {
5570
+ const members = groupMap().get(key) ?? [];
5571
+ if (members.length === 0) return;
5572
+ if (view === "off") {
5573
+ batchToggleNames(members, true);
5574
+ return;
5575
+ }
5576
+ const conflicts = conflictsOnClose(members, actionNames, [...groupMap().entries()].filter(([otherKey]) => otherKey !== key).map(([, memberNames]) => ({ members: memberNames })));
5577
+ if (conflicts.length > 0) setConflictDialog({
5578
+ key,
5579
+ name,
5580
+ conflicts
5581
+ });
5582
+ else batchToggleNames(members.filter((member) => actionNames.has(member)), false);
5583
+ }, [
5584
+ groupMap,
5585
+ actionNames,
5586
+ batchToggleNames
5587
+ ]),
5588
+ resolveConflict: (0, react.useCallback)(async (closeAll) => {
5589
+ const dialog = conflictDialog;
5590
+ if (dialog === null) return;
5591
+ setConflictDialog(null);
5592
+ const members = groupMap().get(dialog.key) ?? [];
5593
+ if (closeAll) await batchToggleNames(members.filter((member) => actionNames.has(member)), false);
5594
+ else await batchToggleNames(members.filter((member) => actionNames.has(member) && !dialog.conflicts.includes(member)), false);
5595
+ }, [
5596
+ conflictDialog,
5597
+ groupMap,
5598
+ actionNames,
5599
+ batchToggleNames
5600
+ ]),
5601
+ createTag: (0, react.useCallback)(async (event) => {
5602
+ event.preventDefault();
5603
+ const name = newTagName.trim();
5604
+ if (name === "") return;
5605
+ shared.setTagBusy(true);
5606
+ shared.clearFail();
5607
+ try {
5608
+ applyTags(await api.saveTag({ name }));
5609
+ setNewTagName("");
5610
+ } catch (error) {
5611
+ shared.fail(errorMessage(error));
5612
+ } finally {
5613
+ shared.setTagBusy(false);
5614
+ }
5615
+ }, [
5616
+ api,
5617
+ applyTags,
5618
+ newTagName,
5619
+ shared
5620
+ ]),
5621
+ deleteTag: (0, react.useCallback)(async (id) => {
5622
+ shared.setTagBusy(true);
5623
+ shared.clearFail();
5624
+ try {
5625
+ applyTags(await api.deleteTag(id));
5626
+ setEditingTag(null);
5627
+ } catch (error) {
5628
+ shared.fail(errorMessage(error));
5629
+ } finally {
5630
+ shared.setTagBusy(false);
5631
+ }
5632
+ }, [
5633
+ api,
5634
+ applyTags,
5635
+ shared
5636
+ ]),
5637
+ saveTag: (0, react.useCallback)(async (id, name, memberNames) => {
5638
+ shared.setTagBusy(true);
5639
+ shared.clearFail();
5640
+ try {
5641
+ const safeName = name.trim();
5642
+ let tags = null;
5643
+ if (safeName !== "") tags = await api.saveTag({
5644
+ id,
5645
+ name: safeName
5646
+ });
5647
+ if (memberNames !== null) tags = await api.setTagMembers(id, memberNames);
5648
+ if (tags === null) return;
5649
+ applyTags(tags);
5650
+ setEditingTag(null);
5651
+ } catch (error) {
5652
+ shared.fail(errorMessage(error));
5653
+ } finally {
5654
+ shared.setTagBusy(false);
5655
+ }
5656
+ }, [
5657
+ api,
5658
+ applyTags,
5659
+ shared
5660
+ ]),
5661
+ reorderTags: (0, react.useCallback)(async (orderedIds) => {
5662
+ shared.setTagBusy(true);
5663
+ shared.clearFail();
5664
+ try {
5665
+ const tags = await api.reorderTags(orderedIds);
5666
+ applyTags(tags);
5667
+ } catch (error) {
5668
+ const msg = errorMessage(error);
5669
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5670
+ const byId = new Map(groupsState?.tags.map((t) => [t.id, t]) ?? []);
5671
+ const reordered = orderedIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
5672
+ if (reordered.length === orderedIds.length) {
5673
+ applyTags(reordered);
5674
+ shared.succeed("已临时调整顺序(本地生效,重启宿主后持久化)");
5675
+ } else shared.fail(msg);
5676
+ } else shared.fail(msg);
5677
+ } finally {
5678
+ shared.setTagBusy(false);
5679
+ }
5680
+ }, [
5681
+ api,
5682
+ applyTags,
5683
+ groupsState,
5684
+ shared
5685
+ ]),
5686
+ reorderCollections: (0, react.useCallback)(async (orderedNames) => {
5687
+ shared.setTagBusy(true);
5688
+ shared.clearFail();
5689
+ try {
5690
+ const collections = await api.reorderCollections(orderedNames);
5691
+ setGroupsState((prev) => prev === null ? prev : {
5692
+ ...prev,
5693
+ collections
5694
+ });
5695
+ loadGroups();
5696
+ } catch (error) {
5697
+ const msg = errorMessage(error);
5698
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5699
+ setGroupsState((prev) => {
5700
+ if (prev === null) return prev;
5701
+ const map = new Map(prev.collections.map((c) => [c.name, c]));
5702
+ const reordered = orderedNames.map((n) => map.get(n)).filter((c) => c !== void 0);
5703
+ for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
5704
+ return {
5705
+ ...prev,
5706
+ collections: reordered
5707
+ };
5708
+ });
5709
+ shared.succeed("已临时调整顺序(本地生效,重启宿主后持久化)");
5710
+ } else shared.fail(msg);
5711
+ } finally {
5712
+ shared.setTagBusy(false);
5713
+ }
5714
+ }, [
5715
+ api,
5716
+ loadGroups,
5717
+ shared
5718
+ ]),
5719
+ reorderSourceGroups: (0, react.useCallback)(async (orderedKeys) => {
5720
+ shared.setTagBusy(true);
5721
+ shared.clearFail();
5409
5722
  try {
5410
- const result = await api.syncSource(dialog.repo, dialog.skills);
5723
+ await api.reorderSourceGroups(orderedKeys);
5724
+ await loadGroups();
5725
+ } catch (error) {
5726
+ const msg = errorMessage(error);
5727
+ if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5728
+ setGroupsState((prev) => {
5729
+ if (prev === null) return prev;
5730
+ const colOrder = orderedKeys.filter((k) => k.startsWith("col:")).map((k) => k.slice(4));
5731
+ const map = new Map(prev.collections.map((c) => [c.name, c]));
5732
+ const reordered = colOrder.map((n) => map.get(n)).filter((c) => c !== void 0);
5733
+ for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
5734
+ return {
5735
+ ...prev,
5736
+ collections: reordered,
5737
+ sourceGroupOrder: orderedKeys
5738
+ };
5739
+ });
5740
+ shared.succeed("已临时调整顺序(本地生效,重启宿主后持久化)");
5741
+ } else shared.fail(msg);
5742
+ } finally {
5743
+ shared.setTagBusy(false);
5744
+ }
5745
+ }, [
5746
+ api,
5747
+ loadGroups,
5748
+ shared
5749
+ ])
5750
+ };
5751
+ }
5752
+ //#endregion
5753
+ //#region src/client/panel/hooks/useSourceFlow.ts
5754
+ /**
5755
+ * useSourceFlow — 来源域:来源列表/更新检查/同步确认/跟进删除/回收站恢复/
5756
+ * 清空回收站,以及单个技能/整组的删除确认流。目录与分组的刷新经聚合根
5757
+ * 传入的 reload 回调,不直接碰其他域的 state。
5758
+ */
5759
+ function useSourceFlow(api, shared, reloadCatalog, reloadGroups) {
5760
+ const [sourcesState, setSourcesState] = (0, react.useState)(null);
5761
+ const [sourceCheck, setSourceCheck] = (0, react.useState)({});
5762
+ const [checkingSource, setCheckingSource] = (0, react.useState)(null);
5763
+ const [syncingSource, setSyncingSource] = (0, react.useState)(null);
5764
+ const [confirmDialog, setConfirmDialog] = (0, react.useState)(null);
5765
+ const [deleteSkillDialog, setDeleteSkillDialog] = (0, react.useState)(null);
5766
+ const [deleteGroupDialog, setDeleteGroupDialog] = (0, react.useState)(null);
5767
+ const [confirmClearTrash, setConfirmClearTrash] = (0, react.useState)(false);
5768
+ /** 加载来源记录 + 回收站。 */
5769
+ const loadSources = (0, react.useCallback)(async () => {
5770
+ try {
5771
+ setSourcesState(await api.sources());
5772
+ } catch (error) {
5773
+ shared.fail(errorMessage(error));
5774
+ }
5775
+ }, [api, shared]);
5776
+ return {
5777
+ sourcesState,
5778
+ sourceCheck,
5779
+ checkingSource,
5780
+ syncingSource,
5781
+ confirmDialog,
5782
+ deleteSkillDialog,
5783
+ deleteGroupDialog,
5784
+ confirmClearTrash,
5785
+ setConfirmDialog,
5786
+ setDeleteSkillDialog,
5787
+ setDeleteGroupDialog,
5788
+ setConfirmClearTrash,
5789
+ loadSources,
5790
+ checkSources: (0, react.useCallback)(async (repo) => {
5791
+ setCheckingSource(repo ?? "all");
5792
+ shared.clearFail();
5793
+ try {
5794
+ const result = await api.checkSources(repo);
5795
+ const next = { ...sourceCheck };
5796
+ for (const item of result.results) next[item.repo] = item;
5797
+ setSourceCheck(next);
5798
+ } catch (error) {
5799
+ shared.fail(errorMessage(error));
5800
+ } finally {
5801
+ setCheckingSource(null);
5802
+ }
5803
+ }, [
5804
+ api,
5805
+ sourceCheck,
5806
+ shared
5807
+ ]),
5808
+ requestSync: (0, react.useCallback)((repo, skills) => {
5809
+ setConfirmDialog({
5810
+ kind: "sync",
5811
+ repo,
5812
+ skills
5813
+ });
5814
+ }, []),
5815
+ requestDelete: (0, react.useCallback)((repo, skills) => {
5816
+ setConfirmDialog({
5817
+ kind: "delete",
5818
+ repo,
5819
+ skills
5820
+ });
5821
+ }, []),
5822
+ runConfirmed: (0, react.useCallback)(async () => {
5823
+ const dialog = confirmDialog;
5824
+ if (dialog === null) return;
5825
+ setConfirmDialog(null);
5826
+ if (dialog.kind === "sync") {
5827
+ setSyncingSource(dialog.repo);
5828
+ shared.clearFail();
5829
+ try {
5830
+ const result = await api.syncSource(dialog.repo, dialog.skills);
5831
+ await Promise.all([
5832
+ reloadCatalog(),
5833
+ reloadGroups(),
5834
+ loadSources()
5835
+ ]);
5836
+ if (result.failed.length > 0) shared.fail("sync: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
5837
+ } catch (error) {
5838
+ shared.fail(errorMessage(error));
5839
+ } finally {
5840
+ setSyncingSource(null);
5841
+ }
5842
+ } else {
5843
+ shared.clearFail();
5844
+ try {
5845
+ await api.confirmDeleteSource(dialog.repo, dialog.skills);
5846
+ await Promise.all([
5847
+ reloadCatalog(),
5848
+ reloadGroups(),
5849
+ loadSources()
5850
+ ]);
5851
+ } catch (error) {
5852
+ shared.fail(errorMessage(error));
5853
+ }
5854
+ }
5855
+ }, [
5856
+ confirmDialog,
5857
+ api,
5858
+ reloadCatalog,
5859
+ reloadGroups,
5860
+ loadSources,
5861
+ shared
5862
+ ]),
5863
+ restoreTrash: (0, react.useCallback)(async (name) => {
5864
+ shared.setTagBusy(true);
5865
+ shared.clearFail();
5866
+ try {
5867
+ await api.restoreSource(name);
5411
5868
  await Promise.all([
5412
- load(),
5413
- loadGroups(),
5869
+ reloadCatalog(),
5870
+ reloadGroups(),
5414
5871
  loadSources()
5415
5872
  ]);
5416
- if (result.failed.length > 0) setLoadError("sync: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
5417
5873
  } catch (error) {
5418
- setLoadError(errorMessage(error));
5874
+ shared.fail(errorMessage(error));
5419
5875
  } finally {
5420
- setSyncingSource(null);
5876
+ shared.setTagBusy(false);
5421
5877
  }
5422
- } else {
5423
- setLoadError(null);
5878
+ }, [
5879
+ api,
5880
+ reloadCatalog,
5881
+ reloadGroups,
5882
+ loadSources,
5883
+ shared
5884
+ ]),
5885
+ clearTrash: (0, react.useCallback)(async () => {
5886
+ setConfirmClearTrash(false);
5887
+ shared.setTagBusy(true);
5888
+ shared.clearFail();
5424
5889
  try {
5425
- await api.confirmDeleteSource(dialog.repo, dialog.skills);
5890
+ const result = await api.clearTrash();
5891
+ if (result.failed.length > 0) shared.fail("clear trash: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
5892
+ await Promise.all([reloadCatalog(), loadSources()]);
5893
+ } catch (error) {
5894
+ shared.fail(errorMessage(error));
5895
+ } finally {
5896
+ shared.setTagBusy(false);
5897
+ }
5898
+ }, [
5899
+ api,
5900
+ reloadCatalog,
5901
+ loadSources,
5902
+ shared
5903
+ ]),
5904
+ requestDeleteSkill: (0, react.useCallback)((name) => {
5905
+ setDeleteSkillDialog(name);
5906
+ }, []),
5907
+ runDeleteSkill: (0, react.useCallback)(async () => {
5908
+ const name = deleteSkillDialog;
5909
+ if (name === null) return;
5910
+ setDeleteSkillDialog(null);
5911
+ shared.setTagBusy(true);
5912
+ shared.clearFail();
5913
+ try {
5914
+ await api.deleteSkill(name);
5426
5915
  await Promise.all([
5427
- load(),
5428
- loadGroups(),
5916
+ reloadCatalog(),
5917
+ reloadGroups(),
5429
5918
  loadSources()
5430
5919
  ]);
5431
5920
  } catch (error) {
5432
- setLoadError(errorMessage(error));
5921
+ shared.fail(errorMessage(error));
5922
+ } finally {
5923
+ shared.setTagBusy(false);
5433
5924
  }
5434
- }
5435
- }, [
5436
- confirmDialog,
5437
- api,
5438
- load,
5439
- loadGroups,
5440
- loadSources
5441
- ]);
5442
- /** 检查全部来源的上游更新(服务端 5 分钟节流)。 */
5443
- const checkSources = (0, react.useCallback)(async (repo) => {
5444
- setCheckingSource(repo ?? "all");
5445
- setLoadError(null);
5446
- try {
5447
- const result = await api.checkSources(repo);
5448
- const next = { ...sourceCheck };
5449
- for (const item of result.results) next[item.repo] = item;
5450
- setSourceCheck(next);
5451
- } catch (error) {
5452
- setLoadError(errorMessage(error));
5453
- } finally {
5454
- setCheckingSource(null);
5455
- }
5456
- }, [api, sourceCheck]);
5457
- /** 请求同步某个来源的所选技能(弹确认,因为会覆盖本地修改)。 */
5458
- const requestSync = (0, react.useCallback)((repo, skills) => {
5459
- setConfirmDialog({
5460
- kind: "sync",
5461
- repo,
5462
- skills
5463
- });
5464
- }, []);
5465
- /** 请求跟进上游删除(弹确认,移入回收站)。 */
5466
- const requestDelete = (0, react.useCallback)((repo, skills) => {
5467
- setConfirmDialog({
5468
- kind: "delete",
5469
- repo,
5470
- skills
5471
- });
5472
- }, []);
5473
- /** 从回收站恢复一个技能。 */
5474
- const restoreTrash = (0, react.useCallback)(async (name) => {
5475
- setTagBusy(true);
5476
- setLoadError(null);
5477
- try {
5478
- await api.restoreSource(name);
5479
- await Promise.all([
5480
- load(),
5481
- loadGroups(),
5482
- loadSources()
5483
- ]);
5484
- } catch (error) {
5485
- setLoadError(errorMessage(error));
5486
- } finally {
5487
- setTagBusy(false);
5488
- }
5489
- }, [
5490
- api,
5491
- load,
5492
- loadGroups,
5493
- loadSources
5494
- ]);
5495
- /** 确认后永久删除回收站里的全部技能。 */
5496
- const clearTrash = (0, react.useCallback)(async () => {
5497
- setConfirmClearTrash(false);
5498
- setTagBusy(true);
5499
- setLoadError(null);
5500
- try {
5501
- const result = await api.clearTrash();
5502
- if (result.failed.length > 0) setLoadError("clear trash: " + result.failed.map((failure) => failure.name + ": " + failure.error).join("; "));
5503
- await Promise.all([load(), loadSources()]);
5504
- } catch (error) {
5505
- setLoadError(errorMessage(error));
5506
- } finally {
5507
- setTagBusy(false);
5508
- }
5509
- }, [
5510
- api,
5511
- load,
5512
- loadSources
5513
- ]);
5514
- const [fixingPaths, setFixingPaths] = (0, react.useState)(/* @__PURE__ */ new Set());
5515
- const fixDiagnostic = (0, react.useCallback)(async (path) => {
5516
- setFixingPaths((previous) => new Set(previous).add(path));
5517
- setLoadError(null);
5518
- try {
5519
- if (!window.confirm(tt("diag.fixConfirm", { path }))) return;
5520
- await api.fixDiagnostic(path);
5521
- await load();
5522
- setSuccessBanner(tt("diag.fixed"));
5523
- } catch (error) {
5524
- setLoadError(errorMessage(error));
5525
- } finally {
5526
- setFixingPaths((previous) => {
5527
- const next = new Set(previous);
5528
- next.delete(path);
5529
- return next;
5925
+ }, [
5926
+ api,
5927
+ deleteSkillDialog,
5928
+ reloadCatalog,
5929
+ reloadGroups,
5930
+ loadSources,
5931
+ shared
5932
+ ]),
5933
+ requestDeleteGroup: (0, react.useCallback)((name, skillNames) => {
5934
+ setDeleteGroupDialog({
5935
+ name,
5936
+ skillNames
5530
5937
  });
5531
- }
5532
- }, [api, load]);
5533
- /** 打开单个技能的删除确认(移入回收站,可恢复)。 */
5534
- const requestDeleteSkill = (0, react.useCallback)((name) => {
5535
- setDeleteSkillDialog(name);
5536
- }, []);
5537
- /** 执行删除确认后的回收站迁移。 */
5538
- const runDeleteSkill = (0, react.useCallback)(async () => {
5539
- const name = deleteSkillDialog;
5540
- if (name === null) return;
5541
- setDeleteSkillDialog(null);
5542
- setTagBusy(true);
5543
- setLoadError(null);
5544
- try {
5545
- await api.deleteSkill(name);
5938
+ }, []),
5939
+ runDeleteGroup: (0, react.useCallback)(async () => {
5940
+ const dialog = deleteGroupDialog;
5941
+ if (dialog === null) return;
5942
+ setDeleteGroupDialog(null);
5943
+ shared.setTagBusy(true);
5944
+ shared.clearFail();
5945
+ const failures = [];
5946
+ let done = 0;
5947
+ for (const name of dialog.skillNames) try {
5948
+ await api.deleteSkill(name);
5949
+ done += 1;
5950
+ } catch (error) {
5951
+ failures.push(name + ": " + errorMessage(error));
5952
+ }
5546
5953
  await Promise.all([
5547
- load(),
5548
- loadGroups(),
5954
+ reloadCatalog(),
5955
+ reloadGroups(),
5549
5956
  loadSources()
5550
5957
  ]);
5551
- } catch (error) {
5552
- setLoadError(errorMessage(error));
5553
- } finally {
5554
- setTagBusy(false);
5555
- }
5556
- }, [
5557
- api,
5558
- deleteSkillDialog,
5559
- load,
5560
- loadGroups,
5561
- loadSources
5562
- ]);
5563
- /** 打开整组删除确认(来源分组一键删除)。 */
5564
- const requestDeleteGroup = (0, react.useCallback)((name, skillNames) => {
5565
- setDeleteGroupDialog({
5566
- name,
5567
- skillNames
5568
- });
5569
- }, []);
5570
- /** 执行整组删除(逐个移入回收站,跳过只读)。 */
5571
- const runDeleteGroup = (0, react.useCallback)(async () => {
5572
- const dialog = deleteGroupDialog;
5573
- if (dialog === null) return;
5574
- setDeleteGroupDialog(null);
5575
- setTagBusy(true);
5576
- setLoadError(null);
5577
- const failures = [];
5578
- let done = 0;
5579
- for (const name of dialog.skillNames) try {
5580
- await api.deleteSkill(name);
5581
- done += 1;
5582
- } catch (error) {
5583
- failures.push(name + ": " + errorMessage(error));
5584
- }
5585
- await Promise.all([
5586
- load(),
5587
- loadGroups(),
5588
- loadSources()
5589
- ]);
5590
- setTagBusy(false);
5591
- if (failures.length > 0) setLoadError(`删除整组 "${dialog.name}":成功 ${done} 个,失败 ${failures.length} 个:` + failures.join("; "));
5592
- else if (done > 0) setSuccessBanner(`已删除整组 "${dialog.name}":${done} 个技能已移入回收站`);
5593
- }, [
5594
- api,
5595
- deleteGroupDialog,
5596
- load,
5597
- loadGroups,
5598
- loadSources
5599
- ]);
5600
- /** 新建一个空 tag 分组。 */
5601
- const createTag = (0, react.useCallback)(async (event) => {
5602
- event.preventDefault();
5603
- const name = newTagName.trim();
5604
- if (name === "") return;
5605
- setTagBusy(true);
5606
- setLoadError(null);
5607
- try {
5608
- applyTags(await api.saveTag({ name }));
5609
- setNewTagName("");
5610
- } catch (error) {
5611
- setLoadError(errorMessage(error));
5612
- } finally {
5613
- setTagBusy(false);
5614
- }
5615
- }, [
5616
- api,
5617
- applyTags,
5618
- newTagName
5619
- ]);
5620
- /** 删除一个 tag 分组(不影响技能文件)。 */
5621
- const deleteTag = (0, react.useCallback)(async (id) => {
5622
- setTagBusy(true);
5623
- setLoadError(null);
5624
- try {
5625
- applyTags(await api.deleteTag(id));
5626
- setEditingTag(null);
5627
- } catch (error) {
5628
- setLoadError(errorMessage(error));
5629
- } finally {
5630
- setTagBusy(false);
5631
- }
5632
- }, [api, applyTags]);
5633
- /** 重命名 tag,或保存成员勾选后回到列表。 */
5634
- const saveTag = (0, react.useCallback)(async (id, name, memberNames) => {
5635
- setTagBusy(true);
5636
- setLoadError(null);
5958
+ shared.setTagBusy(false);
5959
+ if (failures.length > 0) shared.fail(`删除整组 "${dialog.name}":成功 ${done} 个,失败 ${failures.length} 个:` + failures.join("; "));
5960
+ else if (done > 0) shared.succeed(`已删除整组 "${dialog.name}":${done} 个技能已移入回收站`);
5961
+ }, [
5962
+ api,
5963
+ deleteGroupDialog,
5964
+ reloadCatalog,
5965
+ reloadGroups,
5966
+ loadSources,
5967
+ shared
5968
+ ])
5969
+ };
5970
+ }
5971
+ //#endregion
5972
+ //#region src/client/panel/hooks/useMarketFlow.ts
5973
+ /**
5974
+ * useMarketFlow — 市场域:市场源增删、仓库扫描、分支/版本选择、导入任务
5975
+ * (轮询+取消)、更新检查、星标统计、版本同步、全部更新。目录/分组/来源
5976
+ * 的刷新与来源检查经聚合根传入,不直接碰其他域的 state。
5977
+ */
5978
+ function useMarketFlow(api, shared, reloadCatalog, reloadGroups, reloadSources, checkSources, sourceCheck) {
5979
+ const [marketState, setMarketState] = (0, react.useState)({
5980
+ status: "loading",
5981
+ repos: []
5982
+ });
5983
+ const [marketCheck, setMarketCheck] = (0, react.useState)({});
5984
+ const [branchChoice, setBranchChoice] = (0, react.useState)(null);
5985
+ const [branchBusy, setBranchBusy] = (0, react.useState)(false);
5986
+ const [marketSyncDialog, setMarketSyncDialog] = (0, react.useState)(null);
5987
+ const [syncingMarket, setSyncingMarket] = (0, react.useState)(null);
5988
+ const [syncBusy, setSyncBusy] = (0, react.useState)(false);
5989
+ const [newSourceName, setNewSourceName] = (0, react.useState)("");
5990
+ const [repoDiscoverState, setRepoDiscoverState] = (0, react.useState)({ status: "idle" });
5991
+ /** Market repo currently being scanned (null when idle); per-row busy state. */
5992
+ const [scanningRepo, setScanningRepo] = (0, react.useState)(null);
5993
+ const [repoSelected, setRepoSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
5994
+ const [repoImporting, setRepoImporting] = (0, react.useState)(false);
5995
+ const [repoResult, setRepoResult] = (0, react.useState)(null);
5996
+ const [importJobId, setImportJobId] = (0, react.useState)(null);
5997
+ const pollAbortRef = (0, react.useRef)(null);
5998
+ /** 市场源星星/下载数(SWR 缓存)。 */
5999
+ const [marketStats, setMarketStats] = (0, react.useState)({});
6000
+ /** 版本对话框:打开时拉 releases + branches,确认后定版并重扫。 */
6001
+ const [versionDialog, setVersionDialog] = (0, react.useState)(null);
6002
+ const [versionBusy, setVersionBusy] = (0, react.useState)(false);
6003
+ const [updateAllDialog, setUpdateAllDialog] = (0, react.useState)(false);
6004
+ /** 加载市场源列表。 */
6005
+ const loadMarket = (0, react.useCallback)(async () => {
5637
6006
  try {
5638
- const safeName = name.trim();
5639
- let tags = null;
5640
- if (safeName !== "") tags = await api.saveTag({
5641
- id,
5642
- name: safeName
6007
+ const next = await api.market();
6008
+ setMarketState({
6009
+ status: "ready",
6010
+ repos: next.repos
5643
6011
  });
5644
- if (memberNames !== null) tags = await api.setTagMembers(id, memberNames);
5645
- if (tags === null) return;
5646
- applyTags(tags);
5647
- setEditingTag(null);
5648
6012
  } catch (error) {
5649
- setLoadError(errorMessage(error));
5650
- } finally {
5651
- setTagBusy(false);
5652
- }
5653
- }, [api, applyTags]);
5654
- /** 拖拽重排场景分组 */
5655
- const reorderTags = (0, react.useCallback)(async (orderedIds) => {
5656
- setTagBusy(true);
5657
- setLoadError(null);
5658
- try {
5659
- const tags = await api.reorderTags(orderedIds);
5660
- applyTags(tags);
5661
- } catch (error) {
5662
- const msg = errorMessage(error);
5663
- if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5664
- const byId = new Map(groupsState?.tags.map((t) => [t.id, t]) ?? []);
5665
- const reordered = orderedIds.map((id) => byId.get(id)).filter((t) => t !== void 0);
5666
- if (reordered.length === orderedIds.length) {
5667
- applyTags(reordered);
5668
- setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
5669
- } else setLoadError(msg);
5670
- } else setLoadError(msg);
5671
- } finally {
5672
- setTagBusy(false);
5673
- }
5674
- }, [
5675
- api,
5676
- applyTags,
5677
- groupsState
5678
- ]);
5679
- /** 拖拽重排来源集合 */
5680
- const reorderCollections = (0, react.useCallback)(async (orderedNames) => {
5681
- setTagBusy(true);
5682
- setLoadError(null);
5683
- try {
5684
- const collections = await api.reorderCollections(orderedNames);
5685
- setGroupsState((prev) => prev === null ? prev : {
5686
- ...prev,
5687
- collections
6013
+ setMarketState({
6014
+ status: "error",
6015
+ repos: []
5688
6016
  });
5689
- loadGroups();
5690
- } catch (error) {
5691
- const msg = errorMessage(error);
5692
- if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5693
- setGroupsState((prev) => {
5694
- if (prev === null) return prev;
5695
- const map = new Map(prev.collections.map((c) => [c.name, c]));
5696
- const reordered = orderedNames.map((n) => map.get(n)).filter((c) => c !== void 0);
5697
- for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
5698
- return {
5699
- ...prev,
5700
- collections: reordered
5701
- };
5702
- });
5703
- setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
5704
- } else setLoadError(msg);
5705
- } finally {
5706
- setTagBusy(false);
5707
- }
5708
- }, [api, loadGroups]);
5709
- /** 拖拽重排来源顶层分组(project / col:xxx / personal 全量可拖) */
5710
- const reorderSourceGroups = (0, react.useCallback)(async (orderedKeys) => {
5711
- setTagBusy(true);
5712
- setLoadError(null);
5713
- try {
5714
- await api.reorderSourceGroups(orderedKeys);
5715
- await loadGroups();
5716
- } catch (error) {
5717
- const msg = errorMessage(error);
5718
- if (msg.includes("404") || msg.toLowerCase().includes("not found")) {
5719
- setGroupsState((prev) => {
5720
- if (prev === null) return prev;
5721
- const colOrder = orderedKeys.filter((k) => k.startsWith("col:")).map((k) => k.slice(4));
5722
- const map = new Map(prev.collections.map((c) => [c.name, c]));
5723
- const reordered = colOrder.map((n) => map.get(n)).filter((c) => c !== void 0);
5724
- for (const c of prev.collections) if (!reordered.some((r) => r.name === c.name)) reordered.push(c);
5725
- return {
5726
- ...prev,
5727
- collections: reordered,
5728
- sourceGroupOrder: orderedKeys
5729
- };
5730
- });
5731
- setSuccessBanner("已临时调整顺序(本地生效,重启宿主后持久化)");
5732
- } else setLoadError(msg);
5733
- } finally {
5734
- setTagBusy(false);
6017
+ shared.fail(errorMessage(error));
5735
6018
  }
5736
- }, [api, loadGroups]);
6019
+ }, [api, shared]);
5737
6020
  /** 添加一个市场源(内置市场目录与手动输入共用),并立即扫描它。 */
5738
6021
  const addSource = (0, react.useCallback)(async (input) => {
5739
6022
  const value = input.trim();
5740
6023
  if (value === "") return;
5741
- setLoadError(null);
6024
+ shared.clearFail();
5742
6025
  try {
5743
6026
  const result = await api.addMarketSource(value);
5744
6027
  setMarketState({
@@ -5766,17 +6049,17 @@ window.__ModuleLoader__.load({
5766
6049
  setScanningRepo(null);
5767
6050
  }
5768
6051
  } catch (error) {
5769
- setLoadError(errorMessage(error));
6052
+ shared.fail(errorMessage(error));
5770
6053
  }
5771
- }, [api]);
6054
+ }, [api, shared]);
5772
6055
  /** 手动输入框的添加动作。 */
5773
6056
  const addMarketSource = (0, react.useCallback)(async () => {
5774
6057
  await addSource(newSourceName);
5775
6058
  }, [addSource, newSourceName]);
5776
6059
  /** 删除一个市场源(不影响已装技能)。 */
5777
6060
  const removeMarketSource = (0, react.useCallback)(async (repo) => {
5778
- setTagBusy(true);
5779
- setLoadError(null);
6061
+ shared.setTagBusy(true);
6062
+ shared.clearFail();
5780
6063
  try {
5781
6064
  const result = await api.removeMarketSource(repo);
5782
6065
  setMarketState({
@@ -5784,11 +6067,11 @@ window.__ModuleLoader__.load({
5784
6067
  repos: result.repos
5785
6068
  });
5786
6069
  } catch (error) {
5787
- setLoadError(errorMessage(error));
6070
+ shared.fail(errorMessage(error));
5788
6071
  } finally {
5789
- setTagBusy(false);
6072
+ shared.setTagBusy(false);
5790
6073
  }
5791
- }, [api]);
6074
+ }, [api, shared]);
5792
6075
  /** 扫描一个市场源(或手动输入)的仓库。 */
5793
6076
  const scanRepo = (0, react.useCallback)(async (input) => {
5794
6077
  const value = input.trim();
@@ -5797,7 +6080,7 @@ window.__ModuleLoader__.load({
5797
6080
  setRepoSelected(/* @__PURE__ */ new Set());
5798
6081
  setScanningRepo(value);
5799
6082
  setRepoDiscoverState({ status: "scanning" });
5800
- setLoadError(null);
6083
+ shared.clearFail();
5801
6084
  try {
5802
6085
  const data = await api.repoDiscover(value);
5803
6086
  if (data.ref === null) {
@@ -5822,29 +6105,31 @@ window.__ModuleLoader__.load({
5822
6105
  } finally {
5823
6106
  setScanningRepo(null);
5824
6107
  }
5825
- }, [api, loadMarket]);
6108
+ }, [
6109
+ api,
6110
+ loadMarket,
6111
+ shared
6112
+ ]);
5826
6113
  /** 确认分支选择:持久化 ref 后重新扫描。 */
5827
6114
  const confirmBranchChoice = (0, react.useCallback)(async () => {
5828
6115
  if (branchChoice === null) return;
5829
6116
  setBranchBusy(true);
5830
- setLoadError(null);
6117
+ shared.clearFail();
5831
6118
  try {
5832
6119
  await api.setMarketSourceRef(branchChoice.repo, branchChoice.selected);
5833
6120
  setBranchChoice(null);
5834
6121
  await scanRepo(branchChoice.repo);
5835
6122
  } catch (error) {
5836
- setLoadError(errorMessage(error));
6123
+ shared.fail(errorMessage(error));
5837
6124
  } finally {
5838
6125
  setBranchBusy(false);
5839
6126
  }
5840
6127
  }, [
5841
6128
  api,
5842
6129
  branchChoice,
5843
- scanRepo
6130
+ scanRepo,
6131
+ shared
5844
6132
  ]);
5845
- /** 版本对话框:打开时拉 releases + branches,确认后定版并重扫。 */
5846
- const [versionDialog, setVersionDialog] = (0, react.useState)(null);
5847
- const [versionBusy, setVersionBusy] = (0, react.useState)(false);
5848
6133
  const openVersionDialog = (0, react.useCallback)(async (repo) => {
5849
6134
  const current = marketState.repos.find((item) => item.repo === repo)?.ref;
5850
6135
  setVersionDialog({
@@ -5856,7 +6141,7 @@ window.__ModuleLoader__.load({
5856
6141
  custom: "",
5857
6142
  loading: true
5858
6143
  });
5859
- setLoadError(null);
6144
+ shared.clearFail();
5860
6145
  try {
5861
6146
  const data = await api.marketSourceVersions(repo);
5862
6147
  setVersionDialog({
@@ -5869,23 +6154,27 @@ window.__ModuleLoader__.load({
5869
6154
  loading: false
5870
6155
  });
5871
6156
  } catch (error) {
5872
- setLoadError(errorMessage(error));
6157
+ shared.fail(errorMessage(error));
5873
6158
  setVersionDialog(null);
5874
6159
  }
5875
- }, [api, marketState]);
6160
+ }, [
6161
+ api,
6162
+ marketState,
6163
+ shared
6164
+ ]);
5876
6165
  const confirmVersionDialog = (0, react.useCallback)(async () => {
5877
6166
  if (versionDialog === null) return;
5878
6167
  const ref = versionDialog.custom.trim() !== "" ? versionDialog.custom.trim() : versionDialog.selected;
5879
6168
  if (ref === "") return;
5880
6169
  setVersionBusy(true);
5881
- setLoadError(null);
6170
+ shared.clearFail();
5882
6171
  try {
5883
6172
  await api.setMarketSourceRef(versionDialog.repo, ref);
5884
6173
  setVersionDialog(null);
5885
6174
  await loadMarket();
5886
6175
  await scanRepo(versionDialog.repo);
5887
6176
  } catch (error) {
5888
- setLoadError(errorMessage(error));
6177
+ shared.fail(errorMessage(error));
5889
6178
  } finally {
5890
6179
  setVersionBusy(false);
5891
6180
  }
@@ -5893,7 +6182,8 @@ window.__ModuleLoader__.load({
5893
6182
  api,
5894
6183
  versionDialog,
5895
6184
  loadMarket,
5896
- scanRepo
6185
+ scanRepo,
6186
+ shared
5897
6187
  ]);
5898
6188
  /** Toggle one repo preview row. */
5899
6189
  const toggleRepoSelected = (0, react.useCallback)((path, checked) => {
@@ -5910,7 +6200,7 @@ window.__ModuleLoader__.load({
5910
6200
  setRepoImporting(true);
5911
6201
  setRepoResult(null);
5912
6202
  setImportJobId(null);
5913
- setLoadError(null);
6203
+ shared.clearFail();
5914
6204
  let finalProgress = null;
5915
6205
  try {
5916
6206
  const created = await api.repoImport(repoDiscoverState.data.repo, [...repoSelected], repoDiscoverState.data.ref ?? void 0);
@@ -5949,13 +6239,13 @@ window.__ModuleLoader__.load({
5949
6239
  }
5950
6240
  }
5951
6241
  await Promise.all([
5952
- load(),
6242
+ reloadCatalog(),
5953
6243
  loadMarket(),
5954
- loadGroups(),
5955
- loadSources()
6244
+ reloadGroups(),
6245
+ reloadSources()
5956
6246
  ]);
5957
6247
  } catch (error) {
5958
- setLoadError(errorMessage(error));
6248
+ shared.fail(errorMessage(error));
5959
6249
  } finally {
5960
6250
  if (finalProgress !== null && (finalProgress.imported.length > 0 || finalProgress.skipped.length > 0)) {
5961
6251
  const doneNames = /* @__PURE__ */ new Set([...finalProgress.imported.map((r) => r.name), ...finalProgress.skipped.map((r) => r.name)]);
@@ -5985,10 +6275,11 @@ window.__ModuleLoader__.load({
5985
6275
  api,
5986
6276
  repoDiscoverState,
5987
6277
  repoSelected,
5988
- load,
6278
+ reloadCatalog,
5989
6279
  loadMarket,
5990
- loadGroups,
5991
- loadSources
6280
+ reloadGroups,
6281
+ reloadSources,
6282
+ shared
5992
6283
  ]);
5993
6284
  (0, react.useEffect)(() => {
5994
6285
  return () => {
@@ -6011,11 +6302,15 @@ window.__ModuleLoader__.load({
6011
6302
  } : prev);
6012
6303
  }
6013
6304
  } catch (error) {
6014
- setLoadError(errorMessage(error));
6305
+ shared.fail(errorMessage(error));
6015
6306
  } finally {
6016
6307
  setRepoImporting(false);
6017
6308
  }
6018
- }, [api, importJobId]);
6309
+ }, [
6310
+ api,
6311
+ importJobId,
6312
+ shared
6313
+ ]);
6019
6314
  /** 清空扫描结果(关闭归属卡片) */
6020
6315
  const clearScan = (0, react.useCallback)(() => {
6021
6316
  setRepoDiscoverState({ status: "idle" });
@@ -6034,317 +6329,411 @@ window.__ModuleLoader__.load({
6034
6329
  setMarketCheck(next);
6035
6330
  } catch {}
6036
6331
  }, [api]);
6037
- /** 市场源同步:版本对齐后询问是否批量更新本地技能。 */
6038
- const syncMarketSource = (0, react.useCallback)(async (repo) => {
6039
- setSyncingMarket(repo);
6040
- setLoadError(null);
6041
- try {
6042
- const result = await api.marketSync(repo);
6043
- setMarketSyncDialog({
6044
- repo: result.repo,
6045
- ref: result.ref,
6046
- skills: result.skills,
6047
- selected: new Set(result.skills)
6048
- });
6049
- await loadMarket();
6050
- } catch (error) {
6051
- setLoadError(errorMessage(error));
6052
- } finally {
6053
- setSyncingMarket(null);
6054
- }
6055
- }, [api, loadMarket]);
6056
- /** 批量更新本地技能到市场源当前版本(复用来源同步路径)。 */
6057
- const confirmMarketSync = (0, react.useCallback)(async () => {
6058
- if (marketSyncDialog === null) return;
6059
- const selected = [...marketSyncDialog.selected];
6060
- setSyncBusy(true);
6061
- setLoadError(null);
6062
- try {
6063
- if (selected.length > 0) {
6064
- const result = await api.syncSource(marketSyncDialog.repo, selected);
6065
- if (result.failed.length > 0) setLoadError(result.failed.map((item) => item.name + ": " + item.error).join("\n"));
6332
+ return {
6333
+ marketState,
6334
+ marketCheck,
6335
+ branchChoice,
6336
+ branchBusy,
6337
+ marketSyncDialog,
6338
+ syncingMarket,
6339
+ syncBusy,
6340
+ newSourceName,
6341
+ repoDiscoverState,
6342
+ scanningRepo,
6343
+ repoSelected,
6344
+ repoImporting,
6345
+ repoResult,
6346
+ importJobId,
6347
+ marketStats,
6348
+ versionDialog,
6349
+ versionBusy,
6350
+ updateAllDialog,
6351
+ setBranchChoice,
6352
+ setMarketSyncDialog,
6353
+ setNewSourceName,
6354
+ setRepoSelected,
6355
+ setUpdateAllDialog,
6356
+ setVersionDialog,
6357
+ loadMarket,
6358
+ addSource,
6359
+ addMarketSource,
6360
+ removeMarketSource,
6361
+ scanRepo,
6362
+ confirmBranchChoice,
6363
+ openVersionDialog,
6364
+ confirmVersionDialog,
6365
+ toggleRepoSelected,
6366
+ importRepo,
6367
+ cancelImport,
6368
+ clearScan,
6369
+ checkMarket,
6370
+ loadMarketStats: (0, react.useCallback)(async () => {
6371
+ const merge = (results) => {
6372
+ const partial = {};
6373
+ for (const item of results) if (item.error === void 0) partial[item.repo] = {
6374
+ stars: item.stars,
6375
+ downloads: item.downloads
6376
+ };
6377
+ if (Object.keys(partial).length > 0) setMarketStats((previous) => ({
6378
+ ...previous,
6379
+ ...partial
6380
+ }));
6381
+ };
6382
+ try {
6383
+ merge((await api.marketStats()).results);
6384
+ } catch {}
6385
+ try {
6386
+ merge((await api.marketStats(true)).results);
6387
+ } catch {}
6388
+ }, [api]),
6389
+ syncMarketSource: (0, react.useCallback)(async (repo) => {
6390
+ setSyncingMarket(repo);
6391
+ shared.clearFail();
6392
+ try {
6393
+ const result = await api.marketSync(repo);
6394
+ setMarketSyncDialog({
6395
+ repo: result.repo,
6396
+ ref: result.ref,
6397
+ skills: result.skills,
6398
+ selected: new Set(result.skills)
6399
+ });
6400
+ await loadMarket();
6401
+ } catch (error) {
6402
+ shared.fail(errorMessage(error));
6403
+ } finally {
6404
+ setSyncingMarket(null);
6066
6405
  }
6067
- setMarketSyncDialog(null);
6068
- await Promise.all([
6069
- load(),
6070
- loadGroups(),
6071
- loadSources()
6072
- ]);
6073
- checkMarket();
6074
- } catch (error) {
6075
- setLoadError(errorMessage(error));
6076
- } finally {
6077
- setSyncBusy(false);
6078
- }
6079
- }, [
6080
- api,
6081
- marketSyncDialog,
6082
- load,
6083
- loadGroups,
6084
- loadSources,
6085
- checkMarket
6086
- ]);
6087
- /**
6088
- * 一键全部更新:把每个「有可更新技能」的来源逐个同步到上游最新版本
6089
- * (复用来源同步路径,覆盖本地修改),随后刷新并重新检查。
6090
- */
6091
- const updateAll = (0, react.useCallback)(async () => {
6092
- setUpdateAllDialog(false);
6093
- setBatchBusy(true);
6094
- setLoadError(null);
6095
- const failures = [];
6096
- let done = 0;
6097
- try {
6098
- for (const [repo, check] of Object.entries(sourceCheck)) {
6099
- if (!check.changed || check.updated.length === 0) continue;
6100
- try {
6101
- const result = await api.syncSource(repo, check.updated);
6102
- done += 1;
6103
- if (result.failed.length > 0) failures.push(tt("market.updateAllItem", {
6104
- repo,
6105
- count: result.failed.length
6106
- }) + ": " + result.failed.map((item) => item.name + ": " + item.error).join("; "));
6107
- } catch (error) {
6108
- failures.push(repo + ": " + errorMessage(error));
6406
+ }, [
6407
+ api,
6408
+ loadMarket,
6409
+ shared
6410
+ ]),
6411
+ confirmMarketSync: (0, react.useCallback)(async () => {
6412
+ if (marketSyncDialog === null) return;
6413
+ const selected = [...marketSyncDialog.selected];
6414
+ setSyncBusy(true);
6415
+ shared.clearFail();
6416
+ try {
6417
+ if (selected.length > 0) {
6418
+ const result = await api.syncSource(marketSyncDialog.repo, selected);
6419
+ if (result.failed.length > 0) shared.fail(result.failed.map((item) => item.name + ": " + item.error).join("\n"));
6109
6420
  }
6421
+ setMarketSyncDialog(null);
6422
+ await Promise.all([
6423
+ reloadCatalog(),
6424
+ reloadGroups(),
6425
+ reloadSources()
6426
+ ]);
6427
+ checkMarket();
6428
+ } catch (error) {
6429
+ shared.fail(errorMessage(error));
6430
+ } finally {
6431
+ setSyncBusy(false);
6110
6432
  }
6111
- await Promise.all([
6112
- load(),
6113
- loadGroups(),
6114
- loadSources()
6115
- ]);
6116
- await checkSources();
6117
- checkMarket();
6118
- if (failures.length > 0) setLoadError(failures.join("\n"));
6119
- else if (done > 0) setSuccessBanner(tt("market.updateAllDone", { count: done }));
6120
- } finally {
6121
- setBatchBusy(false);
6122
- }
6123
- }, [
6124
- api,
6125
- sourceCheck,
6126
- checkSources,
6127
- checkMarket,
6128
- load,
6129
- loadGroups,
6130
- loadSources
6131
- ]);
6132
- (0, react.useEffect)(() => {
6133
- if (!shouldAutoCheck()) return;
6134
- markAutoChecked();
6135
- checkUpdate();
6136
- checkSources();
6137
- checkMarket();
6433
+ }, [
6434
+ api,
6435
+ marketSyncDialog,
6436
+ reloadCatalog,
6437
+ reloadGroups,
6438
+ reloadSources,
6439
+ checkMarket,
6440
+ shared
6441
+ ]),
6442
+ updateAll: (0, react.useCallback)(async () => {
6443
+ setUpdateAllDialog(false);
6444
+ shared.setBatchBusy(true);
6445
+ shared.clearFail();
6446
+ const failures = [];
6447
+ let done = 0;
6448
+ try {
6449
+ for (const [repo, check] of Object.entries(sourceCheck)) {
6450
+ if (!check.changed || check.updated.length === 0) continue;
6451
+ try {
6452
+ const result = await api.syncSource(repo, check.updated);
6453
+ done += 1;
6454
+ if (result.failed.length > 0) failures.push(tt("market.updateAllItem", {
6455
+ repo,
6456
+ count: result.failed.length
6457
+ }) + ": " + result.failed.map((item) => item.name + ": " + item.error).join("; "));
6458
+ } catch (error) {
6459
+ failures.push(repo + ": " + errorMessage(error));
6460
+ }
6461
+ }
6462
+ await Promise.all([
6463
+ reloadCatalog(),
6464
+ reloadGroups(),
6465
+ reloadSources()
6466
+ ]);
6467
+ await checkSources();
6468
+ checkMarket();
6469
+ if (failures.length > 0) shared.fail(failures.join("\n"));
6470
+ else if (done > 0) shared.succeed(tt("market.updateAllDone", { count: done }));
6471
+ } finally {
6472
+ shared.setBatchBusy(false);
6473
+ }
6474
+ }, [
6475
+ api,
6476
+ sourceCheck,
6477
+ checkSources,
6478
+ checkMarket,
6479
+ reloadCatalog,
6480
+ reloadGroups,
6481
+ reloadSources,
6482
+ shared
6483
+ ])
6484
+ };
6485
+ }
6486
+ //#endregion
6487
+ //#region src/client/panel/useSkillHub.ts
6488
+ /**
6489
+ * useSkillHub — 面板的状态聚合根:只持有跨域共享的通知状态与纯视图状态,
6490
+ * 各域状态与动作分散在 ./hooks/*,这里按依赖顺序组装(meta → 目录 →
6491
+ * 分组 → 来源 → 市场),轮询与派生组装在此。返回形状与拆分前完全一致,
6492
+ * 所有视图无需改动。All hooks run unconditionally at the top, so the panel
6493
+ * may early-return for the detail and tag-editor views without violating
6494
+ * the rules of hooks.
6495
+ */
6496
+ function useSkillHub(api) {
6497
+ const [loadError, setLoadError] = (0, react.useState)(null);
6498
+ /** Green success banner (create finished); shown outside the closing form. */
6499
+ const [successBanner, setSuccessBanner] = (0, react.useState)(null);
6500
+ const [batchBusy, setBatchBusy] = (0, react.useState)(false);
6501
+ const [tagBusy, setTagBusy] = (0, react.useState)(false);
6502
+ const shared = (0, react.useMemo)(() => ({
6503
+ fail: (message) => setLoadError(message),
6504
+ clearFail: () => setLoadError(null),
6505
+ succeed: (message) => setSuccessBanner(message),
6506
+ setTagBusy,
6507
+ setBatchBusy
6508
+ }), []);
6509
+ const [tab, setTab] = (0, react.useState)("sources");
6510
+ const [skillView, setSkillView] = (0, react.useState)("groups");
6511
+ const [sourceFilter, setSourceFilter] = (0, react.useState)("all");
6512
+ /** 分组视图里收起的分组(key 为 tag:<id>、col:<name> 或 project 树键)。技能总数 >80 时首次加载自动折叠 personal。 */
6513
+ const [collapsedGroups, setCollapsedGroups] = (0, react.useState)(/* @__PURE__ */ new Set());
6514
+ /** 项目级三级树里已细分(按 .dsh/.agents)的项目键。 */
6515
+ const [subdividedProjects, setSubdividedProjects] = (0, react.useState)(/* @__PURE__ */ new Set());
6516
+ const [showLegend, setShowLegend] = (0, react.useState)(false);
6517
+ const [editMode, setEditMode] = (0, react.useState)(false);
6518
+ const toggleGroupCollapse = (0, react.useCallback)((key) => {
6519
+ setCollapsedGroups((previous) => {
6520
+ const next = new Set(previous);
6521
+ if (next.has(key)) next.delete(key);
6522
+ else next.add(key);
6523
+ return next;
6524
+ });
6138
6525
  }, []);
6139
- const create = (0, react.useCallback)(async (event) => {
6140
- event.preventDefault();
6141
- setFormBusy(true);
6142
- setFormMessage(null);
6143
- setSuccessBanner(null);
6144
- try {
6145
- const result = await api.create({
6146
- name: formName,
6147
- description: formDesc,
6148
- root: formRoot
6149
- });
6150
- setSuccessBanner(tt("form.success") + result.path);
6151
- setFormName("");
6152
- setFormDesc("");
6153
- setShowForm(false);
6154
- await load();
6155
- } catch (error) {
6156
- setFormMessage({
6157
- kind: "error",
6158
- text: tt("form.error") + errorMessage(error)
6159
- });
6160
- } finally {
6161
- setFormBusy(false);
6162
- }
6163
- }, [
6164
- api,
6165
- formName,
6166
- formDesc,
6167
- formRoot,
6168
- load
6169
- ]);
6170
- const normalized = search.trim().toLocaleLowerCase();
6526
+ const toggleSubdivide = (0, react.useCallback)((key) => {
6527
+ setSubdividedProjects((previous) => {
6528
+ const next = new Set(previous);
6529
+ if (next.has(key)) next.delete(key);
6530
+ else next.add(key);
6531
+ return next;
6532
+ });
6533
+ }, []);
6534
+ const collapsePersonal = (0, react.useCallback)(() => {
6535
+ setCollapsedGroups((previous) => new Set(previous).add("uncategorized-source"));
6536
+ }, []);
6537
+ const clearSourceFilter = (0, react.useCallback)(() => {
6538
+ setSourceFilter("all");
6539
+ }, []);
6540
+ const meta = useMetaFlow(api);
6541
+ const catalogFlow = useCatalogFlow(api, meta.uses, shared, collapsePersonal, clearSourceFilter);
6542
+ const groupFlow = useGroupFlow(api, shared, catalogFlow.batchToggleNames, catalogFlow.actionNames);
6543
+ const sourceFlow = useSourceFlow(api, shared, catalogFlow.load, groupFlow.loadGroups);
6544
+ const marketFlow = useMarketFlow(api, shared, catalogFlow.load, groupFlow.loadGroups, sourceFlow.loadSources, sourceFlow.checkSources, sourceFlow.sourceCheck);
6171
6545
  /** skillName → origin repo(无来源记录的技能不在此映射中,筛选中视为 private)。 */
6172
- const origins = groupsState?.origins ?? {};
6546
+ const origins = groupFlow.groupsState?.origins ?? {};
6173
6547
  /** 来源筛选选项:来源记录中的仓库(排序)+ 末尾的「个人」;没有技能的仓库不列。 */
6174
6548
  const sourceOptions = (0, react.useMemo)(() => {
6175
- const skills = catalog?.skills ?? [];
6549
+ const skills = catalogFlow.catalog?.skills ?? [];
6176
6550
  const repos = [...new Set(skills.map((skill) => origins[skill.name]).filter((repo) => repo !== void 0))].sort();
6177
6551
  const hasPrivate = skills.some((skill) => origins[skill.name] === void 0 && !isProjectSource(skill.source));
6178
6552
  return [...repos, ...hasPrivate ? [PRIVATE_SOURCE] : []];
6179
- }, [catalog, origins]);
6180
- const filtered = (0, react.useMemo)(() => (catalog?.skills ?? []).filter((skill) => {
6181
- if (invocationFilter === "model" && !skill.invocation.modelInvocable) return false;
6182
- if (invocationFilter === "user" && !skill.invocation.userInvocable) return false;
6183
- if (normalized.length === 0) return true;
6184
- return skill.name.toLocaleLowerCase().includes(normalized) || skill.description.toLocaleLowerCase().includes(normalized) || skill.displayName?.toLocaleLowerCase().includes(normalized) || skill.shortDescription?.toLocaleLowerCase().includes(normalized);
6185
- }), [
6186
- catalog,
6187
- normalized,
6188
- invocationFilter
6553
+ }, [catalogFlow.catalog, origins]);
6554
+ (0, react.useEffect)(() => {
6555
+ catalogFlow.load();
6556
+ meta.loadUses();
6557
+ groupFlow.loadGroups();
6558
+ sourceFlow.loadSources();
6559
+ meta.loadConfig();
6560
+ const fast = window.setInterval(() => {
6561
+ catalogFlow.load();
6562
+ }, 5e3);
6563
+ const slow = window.setInterval(() => {
6564
+ meta.loadUses();
6565
+ groupFlow.loadGroups();
6566
+ sourceFlow.loadSources();
6567
+ meta.loadConfig();
6568
+ }, 6e4);
6569
+ return () => {
6570
+ window.clearInterval(fast);
6571
+ window.clearInterval(slow);
6572
+ };
6573
+ }, [
6574
+ catalogFlow.load,
6575
+ meta.loadUses,
6576
+ groupFlow.loadGroups,
6577
+ sourceFlow.loadSources,
6578
+ meta.loadConfig
6189
6579
  ]);
6190
- /** Rows actually rendered with a shortened description (shortDescription in use). */
6191
- const shortenedCount = (0, react.useMemo)(() => filtered.filter((skill) => skill.shortDescription !== void 0).length, [filtered]);
6580
+ (0, react.useEffect)(() => {
6581
+ if (!shouldAutoCheck()) return;
6582
+ markAutoChecked();
6583
+ meta.checkUpdate();
6584
+ sourceFlow.checkSources();
6585
+ marketFlow.checkMarket();
6586
+ }, []);
6192
6587
  return {
6193
- catalog,
6194
- loading,
6588
+ catalog: catalogFlow.catalog,
6589
+ loading: catalogFlow.loading,
6195
6590
  loadError,
6196
6591
  successBanner,
6197
- updateState,
6198
- repoDiscoverState,
6199
- scanningRepo,
6200
- repoSelected,
6201
- repoImporting,
6202
- repoResult,
6203
- importJobId,
6204
- search,
6205
- workspace,
6206
- detail,
6207
- detailLoading,
6208
- busyNames,
6592
+ updateState: meta.updateState,
6593
+ repoDiscoverState: marketFlow.repoDiscoverState,
6594
+ scanningRepo: marketFlow.scanningRepo,
6595
+ repoSelected: marketFlow.repoSelected,
6596
+ repoImporting: marketFlow.repoImporting,
6597
+ repoResult: marketFlow.repoResult,
6598
+ importJobId: marketFlow.importJobId,
6599
+ search: catalogFlow.search,
6600
+ workspace: catalogFlow.workspace,
6601
+ detail: catalogFlow.detail,
6602
+ detailLoading: catalogFlow.detailLoading,
6603
+ busyNames: catalogFlow.busyNames,
6209
6604
  batchBusy,
6210
- showForm,
6211
- formName,
6212
- formDesc,
6213
- formRoot,
6214
- formBusy,
6215
- formMessage,
6216
- uses,
6217
- hubConfig,
6605
+ showForm: catalogFlow.showForm,
6606
+ formName: catalogFlow.formName,
6607
+ formDesc: catalogFlow.formDesc,
6608
+ formRoot: catalogFlow.formRoot,
6609
+ formBusy: catalogFlow.formBusy,
6610
+ formMessage: catalogFlow.formMessage,
6611
+ uses: meta.uses,
6612
+ hubConfig: meta.hubConfig,
6218
6613
  tab,
6219
6614
  skillView,
6220
6615
  sourceFilter,
6221
- invocationFilter,
6222
- sortKey,
6223
- marketState,
6224
- marketCheck,
6225
- branchChoice,
6226
- branchBusy,
6227
- marketSyncDialog,
6228
- syncingMarket,
6229
- syncBusy,
6230
- newSourceName,
6231
- groupsState,
6232
- sourcesState,
6233
- sourceCheck,
6234
- checkingSource,
6235
- syncingSource,
6236
- conflictDialog,
6237
- confirmDialog,
6238
- deleteSkillDialog,
6239
- deleteGroupDialog,
6240
- confirmClearTrash,
6241
- updateAllDialog,
6242
- editingTag,
6243
- editName,
6244
- membersDraft,
6245
- newTagName,
6616
+ invocationFilter: catalogFlow.invocationFilter,
6617
+ sortKey: catalogFlow.sortKey,
6618
+ marketState: marketFlow.marketState,
6619
+ marketCheck: marketFlow.marketCheck,
6620
+ marketStats: marketFlow.marketStats,
6621
+ branchChoice: marketFlow.branchChoice,
6622
+ branchBusy: marketFlow.branchBusy,
6623
+ marketSyncDialog: marketFlow.marketSyncDialog,
6624
+ syncingMarket: marketFlow.syncingMarket,
6625
+ syncBusy: marketFlow.syncBusy,
6626
+ newSourceName: marketFlow.newSourceName,
6627
+ groupsState: groupFlow.groupsState,
6628
+ sourcesState: sourceFlow.sourcesState,
6629
+ sourceCheck: sourceFlow.sourceCheck,
6630
+ checkingSource: sourceFlow.checkingSource,
6631
+ syncingSource: sourceFlow.syncingSource,
6632
+ conflictDialog: groupFlow.conflictDialog,
6633
+ confirmDialog: sourceFlow.confirmDialog,
6634
+ deleteSkillDialog: sourceFlow.deleteSkillDialog,
6635
+ deleteGroupDialog: sourceFlow.deleteGroupDialog,
6636
+ confirmClearTrash: sourceFlow.confirmClearTrash,
6637
+ updateAllDialog: marketFlow.updateAllDialog,
6638
+ editingTag: groupFlow.editingTag,
6639
+ editName: groupFlow.editName,
6640
+ membersDraft: groupFlow.membersDraft,
6641
+ newTagName: groupFlow.newTagName,
6246
6642
  tagBusy,
6247
- editSearch,
6643
+ editSearch: groupFlow.editSearch,
6248
6644
  collapsedGroups,
6249
6645
  subdividedProjects,
6250
6646
  showLegend,
6251
6647
  editMode,
6252
- versionDialog,
6253
- versionBusy,
6254
- actionNames,
6255
- viewNames,
6256
- normalized,
6648
+ versionDialog: marketFlow.versionDialog,
6649
+ versionBusy: marketFlow.versionBusy,
6650
+ actionNames: catalogFlow.actionNames,
6651
+ viewNames: catalogFlow.viewNames,
6652
+ normalized: catalogFlow.normalized,
6257
6653
  origins,
6258
6654
  sourceOptions,
6259
- filtered,
6260
- sorted: (0, react.useMemo)(() => sortSkills(filtered, sortKey, (name) => uses.get(name)?.count), [
6261
- filtered,
6262
- sortKey,
6263
- uses
6264
- ]),
6265
- shortenedCount,
6655
+ filtered: catalogFlow.filtered,
6656
+ sorted: catalogFlow.sorted,
6657
+ shortenedCount: catalogFlow.shortenedCount,
6266
6658
  setLoadError,
6267
6659
  setSuccessBanner,
6268
- setSearch,
6269
- setWorkspace,
6270
- setDetail,
6271
- setShowForm,
6272
- setFormName,
6273
- setFormDesc,
6274
- setFormRoot,
6275
- setFormMessage,
6276
- setRepoSelected,
6660
+ setSearch: catalogFlow.setSearch,
6661
+ setWorkspace: catalogFlow.setWorkspace,
6662
+ setDetail: catalogFlow.setDetail,
6663
+ setShowForm: catalogFlow.setShowForm,
6664
+ setFormName: catalogFlow.setFormName,
6665
+ setFormDesc: catalogFlow.setFormDesc,
6666
+ setFormRoot: catalogFlow.setFormRoot,
6667
+ setFormMessage: catalogFlow.setFormMessage,
6668
+ setRepoSelected: marketFlow.setRepoSelected,
6277
6669
  setTab,
6278
6670
  setSkillView,
6279
6671
  setSourceFilter,
6280
- setInvocationFilter,
6281
- setSortKey,
6282
- setBranchChoice,
6283
- setMarketSyncDialog,
6284
- setNewSourceName,
6285
- setConflictDialog,
6286
- setConfirmDialog,
6287
- setDeleteSkillDialog,
6288
- setDeleteGroupDialog,
6289
- setConfirmClearTrash,
6290
- setUpdateAllDialog,
6291
- setEditingTag,
6292
- setEditName,
6293
- setMembersDraft,
6294
- setNewTagName,
6295
- setEditSearch,
6672
+ setInvocationFilter: catalogFlow.setInvocationFilter,
6673
+ setSortKey: catalogFlow.setSortKey,
6674
+ setBranchChoice: marketFlow.setBranchChoice,
6675
+ setMarketSyncDialog: marketFlow.setMarketSyncDialog,
6676
+ setNewSourceName: marketFlow.setNewSourceName,
6677
+ setConflictDialog: groupFlow.setConflictDialog,
6678
+ setConfirmDialog: sourceFlow.setConfirmDialog,
6679
+ setDeleteSkillDialog: sourceFlow.setDeleteSkillDialog,
6680
+ setDeleteGroupDialog: sourceFlow.setDeleteGroupDialog,
6681
+ setConfirmClearTrash: sourceFlow.setConfirmClearTrash,
6682
+ setUpdateAllDialog: marketFlow.setUpdateAllDialog,
6683
+ setEditingTag: groupFlow.setEditingTag,
6684
+ setEditName: groupFlow.setEditName,
6685
+ setMembersDraft: groupFlow.setMembersDraft,
6686
+ setNewTagName: groupFlow.setNewTagName,
6687
+ setEditSearch: groupFlow.setEditSearch,
6296
6688
  setShowLegend,
6297
6689
  setEditMode,
6298
- setVersionDialog,
6690
+ setVersionDialog: marketFlow.setVersionDialog,
6299
6691
  toggleGroupCollapse,
6300
6692
  toggleSubdivide,
6301
- checkUpdate,
6302
- loadMarket,
6303
- openDetail,
6304
- toggle,
6305
- enableDisabled,
6306
- batchToggleNames,
6307
- toggleGroup,
6308
- resolveConflict,
6309
- runConfirmed,
6310
- checkSources,
6311
- requestSync,
6312
- requestDelete,
6313
- restoreTrash,
6314
- clearTrash,
6315
- fixingPaths,
6316
- fixDiagnostic,
6317
- clearListFilters: (0, react.useCallback)(() => {
6318
- setSearch("");
6319
- setSourceFilter("all");
6320
- setInvocationFilter("all");
6321
- }, []),
6322
- openVersionDialog,
6323
- confirmVersionDialog,
6324
- requestDeleteSkill,
6325
- runDeleteSkill,
6326
- requestDeleteGroup,
6327
- runDeleteGroup,
6328
- createTag,
6329
- deleteTag,
6330
- saveTag,
6331
- reorderTags,
6332
- reorderCollections,
6333
- reorderSourceGroups,
6334
- addSource,
6335
- addMarketSource,
6336
- removeMarketSource,
6337
- scanRepo,
6338
- confirmBranchChoice,
6339
- toggleRepoSelected,
6340
- importRepo,
6341
- cancelImport,
6342
- clearScan,
6343
- checkMarket,
6344
- syncMarketSource,
6345
- confirmMarketSync,
6346
- updateAll,
6347
- create
6693
+ checkUpdate: meta.checkUpdate,
6694
+ loadMarket: marketFlow.loadMarket,
6695
+ openDetail: catalogFlow.openDetail,
6696
+ toggle: catalogFlow.toggle,
6697
+ enableDisabled: catalogFlow.enableDisabled,
6698
+ batchToggleNames: catalogFlow.batchToggleNames,
6699
+ toggleGroup: groupFlow.toggleGroup,
6700
+ resolveConflict: groupFlow.resolveConflict,
6701
+ runConfirmed: sourceFlow.runConfirmed,
6702
+ checkSources: sourceFlow.checkSources,
6703
+ requestSync: sourceFlow.requestSync,
6704
+ requestDelete: sourceFlow.requestDelete,
6705
+ restoreTrash: sourceFlow.restoreTrash,
6706
+ clearTrash: sourceFlow.clearTrash,
6707
+ fixingPaths: catalogFlow.fixingPaths,
6708
+ fixDiagnostic: catalogFlow.fixDiagnostic,
6709
+ clearListFilters: catalogFlow.clearListFilters,
6710
+ openVersionDialog: marketFlow.openVersionDialog,
6711
+ confirmVersionDialog: marketFlow.confirmVersionDialog,
6712
+ requestDeleteSkill: sourceFlow.requestDeleteSkill,
6713
+ runDeleteSkill: sourceFlow.runDeleteSkill,
6714
+ requestDeleteGroup: sourceFlow.requestDeleteGroup,
6715
+ runDeleteGroup: sourceFlow.runDeleteGroup,
6716
+ createTag: groupFlow.createTag,
6717
+ deleteTag: groupFlow.deleteTag,
6718
+ saveTag: groupFlow.saveTag,
6719
+ reorderTags: groupFlow.reorderTags,
6720
+ reorderCollections: groupFlow.reorderCollections,
6721
+ reorderSourceGroups: groupFlow.reorderSourceGroups,
6722
+ addSource: marketFlow.addSource,
6723
+ addMarketSource: marketFlow.addMarketSource,
6724
+ removeMarketSource: marketFlow.removeMarketSource,
6725
+ scanRepo: marketFlow.scanRepo,
6726
+ confirmBranchChoice: marketFlow.confirmBranchChoice,
6727
+ toggleRepoSelected: marketFlow.toggleRepoSelected,
6728
+ importRepo: marketFlow.importRepo,
6729
+ cancelImport: marketFlow.cancelImport,
6730
+ clearScan: marketFlow.clearScan,
6731
+ checkMarket: marketFlow.checkMarket,
6732
+ loadMarketStats: marketFlow.loadMarketStats,
6733
+ syncMarketSource: marketFlow.syncMarketSource,
6734
+ confirmMarketSync: marketFlow.confirmMarketSync,
6735
+ updateAll: marketFlow.updateAll,
6736
+ create: catalogFlow.create
6348
6737
  };
6349
6738
  }
6350
6739
  //#endregion