dsh-skill-hub 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +14 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +187 -102
- package/lib/types/client/api.d.ts +3 -3
- package/lib/types/client/locales/market.d.ts +1 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/protocol/config.d.ts +17 -11
- package/lib/types/protocol/market.d.ts +11 -0
- package/lib/types/protocol/repo.d.ts +6 -0
- package/lib/types/protocol.d.ts +1 -1
- package/lib/types/repo/github-client.d.ts +11 -4
- package/lib/types/repo.d.ts +1 -1
- package/lib/types/routes/catalog-data.d.ts +11 -2
- package/lib/types/routes/deps.d.ts +6 -1
- package/lib/types/routes/route-state.d.ts +5 -0
- package/lib/types/skillfs/scan.d.ts +6 -5
- package/lib/types/skillfs.d.ts +25 -1
- package/package.json +22 -20
- package/src/client/api.ts +4 -3
- package/src/client/locales/market.ts +2 -0
- package/src/client/panel/RepoScanCard.tsx +2 -1
- package/src/client/panel/hooks/useMarketFlow.ts +4 -2
- package/src/client/panel/panel.module.css +10 -2
- package/src/client/settings-card.tsx +2 -1
- package/src/client/settings-form.ts +6 -3
- package/src/index.ts +1 -1
- package/src/protocol/config.ts +27 -24
- package/src/protocol/market.ts +12 -0
- package/src/protocol/repo.ts +6 -0
- package/src/protocol.ts +1 -0
- package/src/repo/github-client.ts +26 -14
- package/src/repo/install.ts +5 -2
- package/src/repo.test.ts +40 -1
- package/src/repo.ts +1 -1
- package/src/routes/catalog-data.ts +20 -6
- package/src/routes/catalog.ts +4 -9
- package/src/routes/deps.ts +7 -2
- package/src/routes/market.ts +13 -2
- package/src/routes/repo-import.ts +49 -8
- package/src/routes/route-state.test.ts +1 -0
- package/src/routes/route-state.ts +5 -0
- package/src/routes.test.ts +187 -0
- package/src/skillfs/scan.ts +30 -17
- package/src/skillfs.test.ts +64 -3
- package/src/skillfs.ts +39 -8
- package/src/update.ts +6 -6
|
@@ -30,6 +30,14 @@ export declare function githubAuthHeaders(): Record<string, string>;
|
|
|
30
30
|
export declare const NO_COMPRESSION: Record<string, string>;
|
|
31
31
|
/** JSON API 默认请求头(含鉴权)。 */
|
|
32
32
|
export declare function apiHeaders(): Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* When the response shows an exhausted GitHub rate limit, the instant it
|
|
35
|
+
* resets; otherwise undefined. Shared by the repo fetch layer and the plugin's
|
|
36
|
+
* own update check so both agree on what counts as a rate-limit rejection
|
|
37
|
+
* (403/429 + `x-ratelimit-remaining: 0` + a parseable `x-ratelimit-reset`)
|
|
38
|
+
* instead of each re-deriving it from the headers.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rateLimitResetAt(response: Response): Date | undefined;
|
|
33
41
|
/**
|
|
34
42
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
35
43
|
* report the reset time instead of a bare HTTP status.
|
|
@@ -46,10 +54,9 @@ export declare function fetchJson(url: string, fetchImpl: typeof fetch, context:
|
|
|
46
54
|
response: Response;
|
|
47
55
|
}>;
|
|
48
56
|
/**
|
|
49
|
-
* Load
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* the exact same content a later download would fetch.
|
|
57
|
+
* Load one JSON document from the GitHub API, reusing an ETag when the URL was
|
|
58
|
+
* fetched before. `fetchImpl`/`context` exist so callers can stub the transport
|
|
59
|
+
* and shape the error text; the payload is whatever the endpoint returned.
|
|
53
60
|
*/
|
|
54
61
|
export declare function fetchJsonCached(url: string, fetchImpl: typeof fetch, context: string): Promise<{
|
|
55
62
|
json: unknown;
|
package/lib/types/repo.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* The implementation now lives under `./repo/` (types, discovery, api,
|
|
13
13
|
* install) and this module is a barrel that re-exports it.
|
|
14
14
|
*/
|
|
15
|
-
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, setGithubToken } from './repo/github-client.ts';
|
|
15
|
+
export { NO_COMPRESSION, RepoFetchError, apiHeaders, fetchError, fetchJson, fetchJsonCached, githubAuthHeaders, isAbortError, rateLimitResetAt, setGithubToken } from './repo/github-client.ts';
|
|
16
16
|
export type { RepoRef, RepoTreeItem, RepoFile } from './repo/types.ts';
|
|
17
17
|
export { REPO_ROOT, isRepoRoot, repoSlug, normalizeRepoInput, collectRepoSkillFiles, originForRoot, discoverRepoEntries, skillManifest, skillDirOf, skillFileAt, skillPathIn, relativeToSkillDir, diffRemoteSkills, repoSkillEntry, } from './repo/discovery.ts';
|
|
18
18
|
export { loadRepoTree, getLatestReleaseTag, getRepoStats, listRepoReleases, listRepoBranches, getLatestCommit, loadRepoTreeAt, } from './repo/api.ts';
|
|
@@ -6,8 +6,17 @@ import type { SkillDefinition } from '@deepseek-ai/dsh-skill';
|
|
|
6
6
|
import { type CatalogResponse, type SkillDetail } from '../protocol.ts';
|
|
7
7
|
import { type SkillInterface } from '../skillfs.ts';
|
|
8
8
|
import { type SkillHubRouteDeps } from './deps.ts';
|
|
9
|
-
/**
|
|
10
|
-
|
|
9
|
+
/**
|
|
10
|
+
* 目录中存在的技能名集合。默认(`includeDisabled` 未显式关闭)为「启用目录 ∪
|
|
11
|
+
* 已禁用名单」——tag 成员校验用:技能被禁用后不该从 tag 里掉出去。
|
|
12
|
+
*
|
|
13
|
+
* 导入去重必须传 `{ includeDisabled: false }`:禁用只意味着「暂不启用」,把它
|
|
14
|
+
* 算作「已存在」会让一次禁用产生「永久占名」的副作用,真技能再也导不进来
|
|
15
|
+
* (issue #16)。
|
|
16
|
+
*/
|
|
17
|
+
export declare function knownSkillNames(deps: SkillHubRouteDeps, options?: {
|
|
18
|
+
includeDisabled?: boolean;
|
|
19
|
+
}): Promise<Set<string>>;
|
|
11
20
|
/** 已知工作区条目(dsh 的 workspace.json 表)。 */
|
|
12
21
|
export interface WorkspaceEntry {
|
|
13
22
|
path: string;
|
|
@@ -37,7 +37,12 @@ export interface SkillHubRouteDeps {
|
|
|
37
37
|
}
|
|
38
38
|
/** The resolved hub config a route sees (the shared resolver fills defaults). */
|
|
39
39
|
export declare function configOf(deps: SkillHubRouteDeps): HubConfig;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* The raw saved config layer as persisted in the sidecar, reported for display
|
|
42
|
+
* only. Since dsh 0.1.7 the settings namespace is the one user layer, so this
|
|
43
|
+
* is legacy data: nothing writes it any more and the effective config above
|
|
44
|
+
* never merges it.
|
|
45
|
+
*/
|
|
41
46
|
export declare function savedOf(deps: SkillHubRouteDeps): Partial<HubConfig>;
|
|
42
47
|
/** Refuse business routes while the master switch is off (the config route stays up). */
|
|
43
48
|
export declare function disabledGate(deps: SkillHubRouteDeps, res: ServerResponse): boolean;
|
|
@@ -40,6 +40,11 @@ export interface ImportJob {
|
|
|
40
40
|
name: string;
|
|
41
41
|
error: string;
|
|
42
42
|
}>;
|
|
43
|
+
/**
|
|
44
|
+
* 导入时被移入回收站让位的「已禁用同名占位」名字(issue #16)。占位已挪走
|
|
45
|
+
* 即计入,即使随后的下载失败也在列 —— 否则用户不知道自己的占位不见了。
|
|
46
|
+
*/
|
|
47
|
+
replaced: string[];
|
|
43
48
|
status: 'running' | 'done' | 'cancelled' | 'error';
|
|
44
49
|
error?: string;
|
|
45
50
|
controller: AbortController;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type DiagnosticEntry, type WritableRoot } from '../protocol.ts';
|
|
2
2
|
/** One discoverable skill file in a scanned root. */
|
|
3
3
|
export interface SkillEntry {
|
|
4
4
|
/** Absolute path of the discovery file (SKILL.md or the flat .md). */
|
|
@@ -39,9 +39,10 @@ export declare function readSkillInterface(directory: string): Promise<SkillInte
|
|
|
39
39
|
*/
|
|
40
40
|
export declare function findProjectRoot(cwd: string): Promise<string>;
|
|
41
41
|
/**
|
|
42
|
-
* Scan one writable root for files the provider ignores, so the GUI can
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Scan one writable root for files the provider ignores, so the GUI can show
|
|
43
|
+
* why a skill never appears — a missing frontmatter, a name that disagrees
|
|
44
|
+
* with the discovery path, and the like must be visible, not silent. Files the
|
|
45
|
+
* hub itself disabled (`.disabled`) are skipped: they are a toggle state, not
|
|
46
|
+
* a defect.
|
|
46
47
|
*/
|
|
47
48
|
export declare function scanDiagnostics(root: WritableRoot, home?: string): Promise<DiagnosticEntry[]>;
|
package/lib/types/skillfs.d.ts
CHANGED
|
@@ -29,6 +29,22 @@ export declare function createSkill(root: WritableRoot, name: string, descriptio
|
|
|
29
29
|
export declare function disableSkill(path: string): Promise<string>;
|
|
30
30
|
/** Re-enable a previously disabled file (reverse of disableSkill). */
|
|
31
31
|
export declare function enableSkill(disabledPath: string): Promise<string>;
|
|
32
|
+
/**
|
|
33
|
+
* Move one already-resolved skill (a bundle directory or a flat .md file) into
|
|
34
|
+
* its root's trash as `<root>/.trash/<name>-<timestamp>`.
|
|
35
|
+
*
|
|
36
|
+
* The layout is the restore contract, not an implementation detail:
|
|
37
|
+
* `restoreSkill` renames the entry back and only proves the parent is `.trash`,
|
|
38
|
+
* and `clearTrash` deletes in place. Every producer of a trash entry must go
|
|
39
|
+
* through here — skill delete, the hub-disabled whole-group delete, and repo
|
|
40
|
+
* import's placeholder replacement all move skills for the same recovery flow.
|
|
41
|
+
*
|
|
42
|
+
* @returns the trashed path plus the original path that was moved.
|
|
43
|
+
*/
|
|
44
|
+
export declare function moveIntoTrash(source: string): Promise<{
|
|
45
|
+
path: string;
|
|
46
|
+
source: string;
|
|
47
|
+
}>;
|
|
32
48
|
/**
|
|
33
49
|
* Move a skill (directory bundle or flat .md file) into the root's trash.
|
|
34
50
|
* The skill stays recoverable: the source is renamed into
|
|
@@ -44,5 +60,13 @@ export declare function trashSkill(sourcePath: string): Promise<{
|
|
|
44
60
|
export declare function restoreSkill(entry: TrashEntry, home?: string): Promise<string>;
|
|
45
61
|
/** Permanently delete one trashed skill (directory or flat file). */
|
|
46
62
|
export declare function clearTrash(entry: TrashEntry, home?: string): Promise<string>;
|
|
47
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Repair one file on disk when its frontmatter is auto-fixable. Returns the new text.
|
|
65
|
+
*
|
|
66
|
+
* The write is atomic (sibling temp file + rename), matching the sidecar's
|
|
67
|
+
* persist path: this rewrites a real skill file in place, and a crash midway
|
|
68
|
+
* through a plain write would leave a truncated SKILL.md — exactly the failure
|
|
69
|
+
* the store layer already guards against. The `.tmp` sibling is never picked
|
|
70
|
+
* up by discovery (only `SKILL.md` and `*.md` are scanned).
|
|
71
|
+
*/
|
|
48
72
|
export declare function fixDiagnosticFile(path: string, home?: string): Promise<string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-skill-hub",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.20",
|
|
4
4
|
"description": "In-GUI skill hub for DeepSeek Harness (dsh): browse the full local skill catalog from the official ctx.skills registry (every root + third-party providers), toggle skills on/off, inspect bodies, surface frontmatter diagnostics, and scaffold new skills — plus a codex-style skill market (built-in catalog, upstream update checks, one-click update-all) with tracked source sync. The full manager beyond the read-only dsh-skill-manager browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -65,7 +65,9 @@
|
|
|
65
65
|
"0.1.7-alpha.2",
|
|
66
66
|
"0.1.7-rc.1",
|
|
67
67
|
"0.1.7-rc.2",
|
|
68
|
-
"0.2.0-rc.1"
|
|
68
|
+
"0.2.0-rc.1",
|
|
69
|
+
"0.2.0-rc.2",
|
|
70
|
+
"0.2.1-alpha.2"
|
|
69
71
|
]
|
|
70
72
|
},
|
|
71
73
|
"capability": {
|
|
@@ -85,7 +87,7 @@
|
|
|
85
87
|
"js-yaml": "^4.1.0"
|
|
86
88
|
},
|
|
87
89
|
"peerDependencies": {
|
|
88
|
-
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2",
|
|
90
|
+
"@deepseek-ai/cordis": "^4.0.1 || ^4.0.2 || ^4.0.5-alpha.1",
|
|
89
91
|
"@deepseek-ai/dsh-client-connection": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
90
92
|
"@deepseek-ai/dsh-client-locale": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
91
93
|
"@deepseek-ai/dsh-client-runtime": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
@@ -99,7 +101,7 @@
|
|
|
99
101
|
"@deepseek-ai/dsh-settings": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
100
102
|
"@deepseek-ai/dsh-skill": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
101
103
|
"@deepseek-ai/dsh-system-prompt": ">=0.1.7-alpha.1 <0.3.0-0",
|
|
102
|
-
"@deepseek-ai/schemastery": "^3.18.3",
|
|
104
|
+
"@deepseek-ai/schemastery": "^3.18.3 || ^3.18.5-alpha.1",
|
|
103
105
|
"react": "^18.2.0",
|
|
104
106
|
"react-dom": "^18.2.0"
|
|
105
107
|
},
|
|
@@ -112,22 +114,22 @@
|
|
|
112
114
|
}
|
|
113
115
|
},
|
|
114
116
|
"devDependencies": {
|
|
115
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
116
|
-
"@deepseek-ai/dsh-client-connection": "^0.2.
|
|
117
|
-
"@deepseek-ai/dsh-client-locale": "^0.2.
|
|
118
|
-
"@deepseek-ai/dsh-client-store": "^0.2.
|
|
119
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.2.
|
|
120
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.
|
|
121
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.2.
|
|
122
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.2.
|
|
123
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.2.
|
|
124
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.
|
|
125
|
-
"@deepseek-ai/dsh-session": "^0.2.
|
|
126
|
-
"@deepseek-ai/dsh-session-query": "^0.2.
|
|
127
|
-
"@deepseek-ai/dsh-settings": "^0.2.
|
|
128
|
-
"@deepseek-ai/dsh-skill": "^0.2.
|
|
129
|
-
"@deepseek-ai/dsh-system-prompt": "^0.2.
|
|
130
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
117
|
+
"@deepseek-ai/cordis": "^4.0.5-alpha.1",
|
|
118
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-alpha.2",
|
|
119
|
+
"@deepseek-ai/dsh-client-locale": "^0.2.1-alpha.2",
|
|
120
|
+
"@deepseek-ai/dsh-client-store": "^0.2.1-alpha.2",
|
|
121
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "^0.2.1-alpha.2",
|
|
122
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-alpha.2",
|
|
123
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.2.1-alpha.2",
|
|
124
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.2.1-alpha.2",
|
|
125
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.2.1-alpha.2",
|
|
126
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.1-alpha.2",
|
|
127
|
+
"@deepseek-ai/dsh-session": "^0.2.1-alpha.2",
|
|
128
|
+
"@deepseek-ai/dsh-session-query": "^0.2.1-alpha.2",
|
|
129
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-alpha.2",
|
|
130
|
+
"@deepseek-ai/dsh-skill": "^0.2.1-alpha.2",
|
|
131
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.1-alpha.2",
|
|
132
|
+
"@deepseek-ai/schemastery": "^3.18.5-alpha.1",
|
|
131
133
|
"@types/js-yaml": "^4.0.9",
|
|
132
134
|
"@types/node": "^22.20.0",
|
|
133
135
|
"@types/react": "~18.3.1",
|
package/src/client/api.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type DiagnosticFixResponse,
|
|
13
13
|
type GroupsResponse,
|
|
14
14
|
type MarketCheckResponse,
|
|
15
|
+
type MarketSourceAddResponse,
|
|
15
16
|
type MarketSourceRefRequest,
|
|
16
17
|
type MarketSourceRequest,
|
|
17
18
|
type MarketSourceResponse,
|
|
@@ -156,9 +157,9 @@ export class SkillHubApi {
|
|
|
156
157
|
return this.get<MarketSourcesResponse>(SKILL_HUB_API.market)
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
/** Add a market source repo; resolves with the fresh list. */
|
|
160
|
-
addMarketSource(repo: string): Promise<
|
|
161
|
-
return this.post<
|
|
160
|
+
/** Add a market source repo; resolves with the fresh list plus the resolved slug. */
|
|
161
|
+
addMarketSource(repo: string): Promise<MarketSourceAddResponse> {
|
|
162
|
+
return this.post<MarketSourceAddResponse>(SKILL_HUB_API.marketSource, { repo } satisfies MarketSourceRequest)
|
|
162
163
|
}
|
|
163
164
|
|
|
164
165
|
/** Remove a market source repo; resolves with the fresh list. */
|
|
@@ -70,6 +70,7 @@ export const zhMarket = {
|
|
|
70
70
|
'repo.files': '{count} 个文件 · {size}',
|
|
71
71
|
'repo.imported': '导入成功 {count} 个',
|
|
72
72
|
'repo.skippedExisting': '跳过已存在 {count} 个',
|
|
73
|
+
'repo.replaced': '已替换禁用占位 {count} 个(旧目录已移入回收站)',
|
|
73
74
|
'repo.failed': '失败 {count} 个',
|
|
74
75
|
'repo.cancel': '取消',
|
|
75
76
|
'repo.cancelled': '已取消 · 已导入 {imported}/{total} 已保留,临时文件已清理',
|
|
@@ -151,6 +152,7 @@ export const enMarket: Record<keyof typeof zhMarket, string> = {
|
|
|
151
152
|
'repo.files': '{count} files · {size}',
|
|
152
153
|
'repo.imported': 'Imported {count}',
|
|
153
154
|
'repo.skippedExisting': 'Skipped existing {count}',
|
|
155
|
+
'repo.replaced': 'Replaced {count} disabled placeholder(s) — old dir moved to trash',
|
|
154
156
|
'repo.failed': 'Failed {count}',
|
|
155
157
|
'repo.cancel': 'Cancel',
|
|
156
158
|
'repo.cancelled': 'Cancelled · {imported}/{total} kept, temp files cleaned',
|
|
@@ -55,7 +55,7 @@ export function RepoScanCard(props: { hub: SkillHubState }): JSX.Element | null
|
|
|
55
55
|
// 导入后 catalog 已包含新技能,但 scan 快照的 existing 仍为 false,需用实时 catalog + 本次导入结果兜底,避免已导入仍可选
|
|
56
56
|
const installedNames = new Set<string>([
|
|
57
57
|
...((hub.catalog?.skills ?? []).map((s) => s.name)),
|
|
58
|
-
|
|
58
|
+
// 已禁用不算「已存在」:禁用只该影响启用态,不该永久占名堵住导入(issue #16)。
|
|
59
59
|
...((repoResult?.imported ?? []).map((r) => r.name)),
|
|
60
60
|
...((repoResult?.skipped ?? []).map((r) => r.name)),
|
|
61
61
|
])
|
|
@@ -174,6 +174,7 @@ export function RepoScanCard(props: { hub: SkillHubState }): JSX.Element | null
|
|
|
174
174
|
{repoResult !== null && repoResult.status !== 'running' ? (
|
|
175
175
|
<div className={repoResult.status === 'cancelled' ? css.errorBanner + ' ' + css.actionsTop : css.formSuccess + ' ' + css.actionsTop}>
|
|
176
176
|
{repoResult.status === 'cancelled' ? tt('repo.cancelled', { imported: repoResult.imported.length, total: repoResult.total }) : `${tt('repo.imported', { count: repoResult.imported.length })} · ${tt('repo.skippedExisting', { count: repoResult.skipped.length })} · ${tt('repo.failed', { count: repoResult.failed.length })}`}
|
|
177
|
+
{(repoResult.replaced?.length ?? 0) > 0 ? <span> · {tt('repo.replaced', { count: repoResult.replaced?.length ?? 0 })}</span> : null}
|
|
177
178
|
{repoResult.failed.length > 0 ? <div style={{marginTop:6}}>{repoResult.failed.map(f => `${f.name}: ${f.error}`).join('; ')}</div> : null}
|
|
178
179
|
{repoResult.status === 'error' && repoResult.error ? <div style={{marginTop:6}}>{repoResult.error}</div> : null}
|
|
179
180
|
</div>
|
|
@@ -75,8 +75,10 @@ export function useMarketFlow(
|
|
|
75
75
|
setNewSourceName('')
|
|
76
76
|
setRepoResult(null)
|
|
77
77
|
setRepoSelected(new Set())
|
|
78
|
-
//
|
|
79
|
-
|
|
78
|
+
// 用服务端回传的归一化 slug 扫描(URL/owner@repo 输入都能对上记录)。
|
|
79
|
+
// 不能猜 `repos` 的位置:重复添加一个已存在的源只改它的 ref、不重排,
|
|
80
|
+
// 取末位会扫到另一个源。
|
|
81
|
+
const slug = result.repo
|
|
80
82
|
setScanningRepo(slug)
|
|
81
83
|
setRepoDiscoverState({ status: 'scanning' })
|
|
82
84
|
try {
|
|
@@ -4,9 +4,17 @@
|
|
|
4
4
|
on :root, with dark overrides on body[data-ds-dark-theme]). Every var()
|
|
5
5
|
keeps the colour this file used before the token pass as its fallback, so a
|
|
6
6
|
host without the theme package renders exactly as before. Sizes, spacing,
|
|
7
|
-
radii, fonts and layout are deliberately untouched.
|
|
7
|
+
radii, fonts and layout are deliberately untouched.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
color-scheme is deliberately NOT declared here. The host layout writes the
|
|
10
|
+
*resolved* scheme to document.documentElement.style.colorScheme; declaring
|
|
11
|
+
`light dark` locally overrides that and makes native controls (<select>
|
|
12
|
+
popups, scrollbars) follow the *OS* preference instead — with a manually
|
|
13
|
+
selected dark theme on a light OS the <option> list renders light while
|
|
14
|
+
.select inherits light text, i.e. invisible (issue #13). Inheriting from the
|
|
15
|
+
root keeps both dark paths (manual + system) correct. */
|
|
16
|
+
|
|
17
|
+
.panel { padding: 28px 20px 44px; display: flex; flex-direction: column; gap: 14px; max-width: 720px; margin: 0 auto; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; /* Dot colors mirror src/client/SkillHubSettingsCard.tsx (DEFAULT_DOT_MODEL_COLOR / DEFAULT_DOT_USER_COLOR) — CSS cannot import TS, so keep the two in sync. */ --hub-model: #2f81f7; --hub-user: #3fb950; /* Host theme tokens (--dsw-alias-*, dsh-client-ui-theme). Each var() carries the pre-token colour of this file as its fallback, so a missing theme package changes nothing. */ --hub-accent: var(--dsw-alias-state-business-primary, #2f81f7); --hub-text: var(--dsw-alias-label-primary, currentColor); --hub-text-2: var(--dsw-alias-label-secondary, #61666b); --hub-text-3: var(--dsw-alias-label-tertiary, #81858c); --hub-text-4: var(--dsw-alias-label-caption, #adb2b8); --hub-surface: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.06)); --hub-hover: var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.08)); --hub-active: var(--dsw-alias-interactive-bg-active, rgba(128,128,128,.16)); --hub-border: var(--dsw-alias-border-l2, rgba(128,128,128,.13)); --hub-border-2: var(--dsw-alias-border-l3, rgba(128,128,128,.2)); --hub-danger: var(--dsw-alias-state-error-primary, #d1242f); --hub-success: var(--dsw-alias-state-success-primary, #3fb950); --hub-warn: var(--dsw-alias-state-warn-primary, #d29922); --hub-on-accent: var(--dsw-alias-label-primary-foreground, #fff); --hub-skeleton: var(--dsw-alias-bg-skeleton, rgba(128,128,128,.1)); }
|
|
10
18
|
.header { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
|
|
11
19
|
.title { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
|
|
12
20
|
.hint { font-size: 12px; color: var(--hub-text-3); }
|
|
@@ -10,6 +10,7 @@ import { useState, type ReactElement } from 'react'
|
|
|
10
10
|
import { IconChevronDownOutline14 } from './icons.tsx'
|
|
11
11
|
import type { HubKey } from './locales.ts'
|
|
12
12
|
import type { CardShell } from './settings-form.ts'
|
|
13
|
+
import { HEX_COLOR_RE } from '../protocol.ts'
|
|
13
14
|
import css from './settings-card.module.css'
|
|
14
15
|
|
|
15
16
|
/** Card-level chrome props. */
|
|
@@ -178,7 +179,7 @@ export interface ColorFieldProps extends FieldBaseProps {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
export function ColorField(props: ColorFieldProps): ReactElement {
|
|
181
|
-
const value =
|
|
182
|
+
const value = HEX_COLOR_RE.test(props.text) ? props.text : props.defaultColor
|
|
182
183
|
return (
|
|
183
184
|
<div className={css.field}>
|
|
184
185
|
<FieldShell {...props} />
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { createSnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
13
|
+
// One definition of what a valid color/token looks like, shared with the host
|
|
14
|
+
// routes that re-validate the same drafts.
|
|
15
|
+
import { GITHUB_TOKEN_RE, HEX_COLOR_RE } from '../protocol.ts'
|
|
13
16
|
|
|
14
17
|
/** One staged field: its draft text plus the clear/reset marker. */
|
|
15
18
|
interface StagedEdit {
|
|
@@ -41,9 +44,9 @@ export function booleanField(field: string): FieldSpec {
|
|
|
41
44
|
export function colorField(field: string): FieldSpec {
|
|
42
45
|
return {
|
|
43
46
|
field,
|
|
44
|
-
format: (value) => (typeof value === 'string' &&
|
|
47
|
+
format: (value) => (typeof value === 'string' && HEX_COLOR_RE.test(value) ? value : ''),
|
|
45
48
|
parse: (text) => {
|
|
46
|
-
if (
|
|
49
|
+
if (HEX_COLOR_RE.test(text)) return { kind: 'set', value: text }
|
|
47
50
|
return undefined
|
|
48
51
|
},
|
|
49
52
|
}
|
|
@@ -61,7 +64,7 @@ export function secretField(field: string): FieldSpec {
|
|
|
61
64
|
parse: (text) => {
|
|
62
65
|
const trimmed = text.trim()
|
|
63
66
|
if (trimmed === '') return undefined
|
|
64
|
-
if (
|
|
67
|
+
if (!GITHUB_TOKEN_RE.test(trimmed)) return undefined
|
|
65
68
|
return { kind: 'set', value: trimmed }
|
|
66
69
|
},
|
|
67
70
|
}
|
package/src/index.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
163
163
|
}
|
|
164
164
|
return out as Partial<HubConfig>
|
|
165
165
|
}
|
|
166
|
-
const current = (): HubConfig => resolveHubConfig(
|
|
166
|
+
const current = (): HubConfig => resolveHubConfig(readConfig())
|
|
167
167
|
|
|
168
168
|
/** The Settings service, when this deployment mounts it. Absent ⇒ no user layer to read or write. */
|
|
169
169
|
const settingsOf = (): SettingsForms | undefined => ctx.get('settings') as SettingsForms | undefined
|
package/src/protocol/config.ts
CHANGED
|
@@ -89,29 +89,30 @@ export const HUB_CONFIG_DEFAULTS = {
|
|
|
89
89
|
} as const
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
92
|
+
* Fill a raw settings value into the effective hub config: schema defaults for
|
|
93
|
+
* anything missing, and the numeric stats knobs clamped to sane ranges
|
|
94
|
+
* (window ≥ 0, scan interval ≥ 1 minute).
|
|
95
|
+
*
|
|
96
|
+
* There is deliberately no "saved sidecar layer" parameter. Until dsh 0.1.7 the
|
|
97
|
+
* hub kept its own runtime overrides in the sidecar and merged them here; the
|
|
98
|
+
* settings namespace is now the only user layer (`updateConfig` writes through
|
|
99
|
+
* `ctx.settings.mutate`), so a merge against a sidecar layer could never
|
|
100
|
+
* observe anything but `{}` — see the git history of `resolveHubConfig`.
|
|
97
101
|
*/
|
|
98
|
-
export function resolveHubConfig(
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const githubToken = saved.githubToken !== undefined ? saved.githubToken : base.githubToken
|
|
102
|
-
const windowDays = clampNumber(saved.statsWindowDays ?? base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays
|
|
103
|
-
const scanMinutes = clampNumber(saved.statsScanMinutes ?? base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes
|
|
102
|
+
export function resolveHubConfig(base: Partial<HubConfig> = {}): HubConfig {
|
|
103
|
+
const windowDays = clampNumber(base.statsWindowDays, 0) ?? HUB_CONFIG_DEFAULTS.statsWindowDays
|
|
104
|
+
const scanMinutes = clampNumber(base.statsScanMinutes, 1) ?? HUB_CONFIG_DEFAULTS.statsScanMinutes
|
|
104
105
|
return {
|
|
105
|
-
enabled:
|
|
106
|
-
announceToAgent:
|
|
107
|
-
showUseCount:
|
|
108
|
-
showUseTime:
|
|
109
|
-
showGroupSummary:
|
|
106
|
+
enabled: base.enabled ?? HUB_CONFIG_DEFAULTS.enabled,
|
|
107
|
+
announceToAgent: base.announceToAgent ?? HUB_CONFIG_DEFAULTS.announceToAgent,
|
|
108
|
+
showUseCount: base.showUseCount ?? HUB_CONFIG_DEFAULTS.showUseCount,
|
|
109
|
+
showUseTime: base.showUseTime ?? HUB_CONFIG_DEFAULTS.showUseTime,
|
|
110
|
+
showGroupSummary: base.showGroupSummary ?? HUB_CONFIG_DEFAULTS.showGroupSummary,
|
|
110
111
|
statsWindowDays: windowDays,
|
|
111
112
|
statsScanMinutes: scanMinutes,
|
|
112
|
-
...(dotModelColor !== undefined ? { dotModelColor } : {}),
|
|
113
|
-
...(dotUserColor !== undefined ? { dotUserColor } : {}),
|
|
114
|
-
...(githubToken !== undefined ? { githubToken } : {}),
|
|
113
|
+
...(base.dotModelColor !== undefined ? { dotModelColor: base.dotModelColor } : {}),
|
|
114
|
+
...(base.dotUserColor !== undefined ? { dotUserColor: base.dotUserColor } : {}),
|
|
115
|
+
...(base.githubToken !== undefined ? { githubToken: base.githubToken } : {}),
|
|
115
116
|
}
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -148,7 +149,13 @@ export interface ConfigResponse {
|
|
|
148
149
|
githubTokenSet: boolean
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
/**
|
|
152
|
+
/**
|
|
153
|
+
* POST /api/skill-hub/config — a partial patch; omitted fields keep their values.
|
|
154
|
+
*
|
|
155
|
+
* Only the fields the handler actually parses belong here: the stats knobs are
|
|
156
|
+
* owned by the settings namespace (`statsWindowDays` / `statsScanMinutes` are
|
|
157
|
+
* still part of {@link HubSettingsValue}), not by this route.
|
|
158
|
+
*/
|
|
152
159
|
export interface ConfigRequest {
|
|
153
160
|
/** Set the field; null clears the saved override so it re-inherits the default. */
|
|
154
161
|
enabled?: boolean | null
|
|
@@ -164,10 +171,6 @@ export interface ConfigRequest {
|
|
|
164
171
|
dotModelColor?: string | null
|
|
165
172
|
/** Set the dot color; null clears the saved override so it re-inherits the default. */
|
|
166
173
|
dotUserColor?: string | null
|
|
167
|
-
/** 统计滚动窗口天数(0 = 全部历史);null 清除覆盖回默认。 */
|
|
168
|
-
statsWindowDays?: number | null
|
|
169
|
-
/** 自动统计扫描间隔(分钟);null 清除覆盖回默认。 */
|
|
170
|
-
statsScanMinutes?: number | null
|
|
171
174
|
/** Set the GitHub token; null/empty clears it back to anonymous. */
|
|
172
175
|
githubToken?: string | null
|
|
173
176
|
}
|
package/src/protocol/market.ts
CHANGED
|
@@ -27,6 +27,18 @@ export interface MarketSourceResponse {
|
|
|
27
27
|
repos: MarketSourceRecord[]
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* POST /api/skill-hub/market/source — same list, plus the slug the host
|
|
32
|
+
* resolved from the input. Clients must scan *this* slug rather than assume
|
|
33
|
+
* the acted-on source sits at some position in `repos`: re-adding a repo the
|
|
34
|
+
* user already tracks only updates its `ref` in place and never reorders the
|
|
35
|
+
* list.
|
|
36
|
+
*/
|
|
37
|
+
export interface MarketSourceAddResponse extends MarketSourceResponse {
|
|
38
|
+
/** Normalized `owner/repo` the host recorded for the submitted input. */
|
|
39
|
+
repo: string
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
/** POST /api/skill-hub/market/source/ref — pin a market source to an explicit ref. */
|
|
31
43
|
export interface MarketSourceRefRequest {
|
|
32
44
|
repo: string
|
package/src/protocol/repo.ts
CHANGED
|
@@ -95,6 +95,12 @@ export interface RepoImportProgressResponse {
|
|
|
95
95
|
imported: Array<{ name: string; origin: string; path: string }>
|
|
96
96
|
skipped: Array<{ name: string; reason: 'exists' }>
|
|
97
97
|
failed: Array<{ name: string; error: string }>
|
|
98
|
+
/**
|
|
99
|
+
* 本次导入中「让位的已禁用同名占位」名字(issue #16):这些条目的旧目录已
|
|
100
|
+
* 移入回收站,新技能随后落地。旧目录挪走即计入,下载失败时也在列。老客户端
|
|
101
|
+
* 不认这个字段时忽略即可,故为可选。
|
|
102
|
+
*/
|
|
103
|
+
replaced?: string[]
|
|
98
104
|
/** error 状态时的原因 */
|
|
99
105
|
error?: string
|
|
100
106
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -66,21 +66,34 @@ function etagCacheSet(url: string, etag: string, json: unknown): void {
|
|
|
66
66
|
}
|
|
67
67
|
etagCache.set(url, { etag, json })
|
|
68
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* When the response shows an exhausted GitHub rate limit, the instant it
|
|
72
|
+
* resets; otherwise undefined. Shared by the repo fetch layer and the plugin's
|
|
73
|
+
* own update check so both agree on what counts as a rate-limit rejection
|
|
74
|
+
* (403/429 + `x-ratelimit-remaining: 0` + a parseable `x-ratelimit-reset`)
|
|
75
|
+
* instead of each re-deriving it from the headers.
|
|
76
|
+
*/
|
|
77
|
+
export function rateLimitResetAt(response: Response): Date | undefined {
|
|
78
|
+
if (response.status !== 403 && response.status !== 429) return undefined
|
|
79
|
+
if (response.headers.get('x-ratelimit-remaining') !== '0') return undefined
|
|
80
|
+
const reset = response.headers.get('x-ratelimit-reset')
|
|
81
|
+
if (reset === null) return undefined
|
|
82
|
+
const at = new Date(Number(reset) * 1000)
|
|
83
|
+
return Number.isNaN(at.getTime()) ? undefined : at
|
|
84
|
+
}
|
|
85
|
+
|
|
69
86
|
/**
|
|
70
87
|
* Build a RepoFetchError; when the response shows an exhausted rate limit,
|
|
71
88
|
* report the reset time instead of a bare HTTP status.
|
|
72
89
|
*/
|
|
73
90
|
export function fetchError(context: string, response: Response, fallbackStatus = 502): RepoFetchError {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
'github rate limit reached (anonymous quota exhausted); retry after ' + at.toLocaleTimeString() + ' or set GITHUB_TOKEN',
|
|
81
|
-
403,
|
|
82
|
-
)
|
|
83
|
-
}
|
|
91
|
+
const resetAt = rateLimitResetAt(response)
|
|
92
|
+
if (resetAt !== undefined) {
|
|
93
|
+
return new RepoFetchError(
|
|
94
|
+
'github rate limit reached (anonymous quota exhausted); retry after ' + resetAt.toLocaleTimeString() + ' or set GITHUB_TOKEN',
|
|
95
|
+
403,
|
|
96
|
+
)
|
|
84
97
|
}
|
|
85
98
|
return new RepoFetchError(context + ' (HTTP ' + response.status + ')', response.status === 404 ? 404 : fallbackStatus)
|
|
86
99
|
}
|
|
@@ -111,10 +124,9 @@ export async function fetchJson(url: string, fetchImpl: typeof fetch, context: s
|
|
|
111
124
|
}
|
|
112
125
|
|
|
113
126
|
/**
|
|
114
|
-
* Load
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
* the exact same content a later download would fetch.
|
|
127
|
+
* Load one JSON document from the GitHub API, reusing an ETag when the URL was
|
|
128
|
+
* fetched before. `fetchImpl`/`context` exist so callers can stub the transport
|
|
129
|
+
* and shape the error text; the payload is whatever the endpoint returned.
|
|
118
130
|
*/
|
|
119
131
|
export async function fetchJsonCached(url: string, fetchImpl: typeof fetch, context: string): Promise<{ json: unknown; response: Response }> {
|
|
120
132
|
const cached = etagCache.get(url)
|
package/src/repo/install.ts
CHANGED
|
@@ -34,8 +34,11 @@ export async function downloadGitHubFile(repo: string, ref: string, path: string
|
|
|
34
34
|
}
|
|
35
35
|
if (response === null || !response.ok) {
|
|
36
36
|
if (signal?.aborted) throw new DOMException('aborted', 'AbortError')
|
|
37
|
-
// Fallback: api.github.com/contents with the raw media type.
|
|
38
|
-
|
|
37
|
+
// Fallback: api.github.com/contents with the raw media type. The `?ref=`
|
|
38
|
+
// is mandatory — without it /contents/ serves the default branch, so a
|
|
39
|
+
// transient raw failure would silently mix default-branch content into a
|
|
40
|
+
// tag-pinned import (issue #15).
|
|
41
|
+
const apiUrl = `https://api.github.com/repos/${repo}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`
|
|
39
42
|
try {
|
|
40
43
|
response = await fetchImpl(apiUrl, { headers: { accept: 'application/vnd.github.raw', ...NO_COMPRESSION, ...githubAuthHeaders() }, ...(signal !== undefined ? { signal } : {}) })
|
|
41
44
|
} catch (error) {
|