@chatcode/cco-market 1.45.1
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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
3
|
+
import type { SkillMarketRuntime } from './runtime.ts'
|
|
4
|
+
import type { SkillInstallLocation, SkillMarketItem } from './types.ts'
|
|
5
|
+
|
|
6
|
+
interface SceneProps {
|
|
7
|
+
readonly React: typeof React
|
|
8
|
+
readonly ui: {
|
|
9
|
+
readonly Box: React.ComponentType<Record<string, unknown>>
|
|
10
|
+
readonly Text: React.ComponentType<Record<string, unknown>>
|
|
11
|
+
useInput(handler: (input: string, key: { upArrow?: boolean; downArrow?: boolean; leftArrow?: boolean; rightArrow?: boolean; return?: boolean; escape?: boolean; backspace?: boolean }) => void): void
|
|
12
|
+
}
|
|
13
|
+
readonly channel: { readonly cwd: string }
|
|
14
|
+
close(): void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TuiSkillMarketContext extends Context {
|
|
18
|
+
readonly commands: {
|
|
19
|
+
register(definition: {
|
|
20
|
+
readonly name: string
|
|
21
|
+
readonly description: string
|
|
22
|
+
readonly recordInput: boolean
|
|
23
|
+
readonly handler: (invocation: { readonly rawInput: string }) => Promise<{ kind: 'success' } | { kind: 'error'; text: string }>
|
|
24
|
+
}): () => void
|
|
25
|
+
}
|
|
26
|
+
readonly tuiScenes: {
|
|
27
|
+
register(descriptor: { readonly id: string; readonly title: string; readonly component: React.ComponentType<SceneProps> }, identity?: Context): () => void
|
|
28
|
+
open(id: string): boolean
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Register `/skill-market` and its full-screen terminal scene together. */
|
|
33
|
+
export function installSkillMarketTui(ctx: Context, runtime: SkillMarketRuntime): void {
|
|
34
|
+
ctx.inject(['commands', 'tuiScenes'], (scopedContext: Context) => {
|
|
35
|
+
const scoped = scopedContext as TuiSkillMarketContext
|
|
36
|
+
scoped.effect(() => {
|
|
37
|
+
const scene = createSkillMarketScene(runtime)
|
|
38
|
+
const disposeScene = scoped.tuiScenes.register({ id: 'skill-market', title: '技能市场', component: scene }, scoped)
|
|
39
|
+
const disposeCommand = scoped.commands.register({
|
|
40
|
+
name: 'skill-market',
|
|
41
|
+
description: '浏览并安装技能市场中的技能',
|
|
42
|
+
recordInput: false,
|
|
43
|
+
handler: async ({ rawInput }) => {
|
|
44
|
+
if (rawInput.trim().length > 0) return { kind: 'error', text: '用法:/skill-market' }
|
|
45
|
+
if (!scoped.tuiScenes.open('skill-market')) return { kind: 'error', text: '技能市场界面当前不可用' }
|
|
46
|
+
return { kind: 'success' }
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
return () => {
|
|
50
|
+
disposeCommand()
|
|
51
|
+
disposeScene()
|
|
52
|
+
}
|
|
53
|
+
}, 'dsh-market: skill market terminal scene')
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSkillMarketScene(runtime: SkillMarketRuntime): React.ComponentType<SceneProps> {
|
|
58
|
+
return function SkillMarketScene({ React: HostReact, ui, channel, close }: SceneProps) {
|
|
59
|
+
const { Box, Text } = ui
|
|
60
|
+
const [items, setItems] = HostReact.useState<readonly SkillMarketItem[]>([])
|
|
61
|
+
const [query, setQuery] = HostReact.useState('')
|
|
62
|
+
const [selected, setSelected] = HostReact.useState(0)
|
|
63
|
+
const [page, setPage] = HostReact.useState(1)
|
|
64
|
+
const [total, setTotal] = HostReact.useState(0)
|
|
65
|
+
const [installedNames, setInstalledNames] = HostReact.useState<ReadonlySet<string>>(() => new Set())
|
|
66
|
+
const [loading, setLoading] = HostReact.useState(true)
|
|
67
|
+
const [error, setError] = HostReact.useState<string | undefined>()
|
|
68
|
+
const [location, setLocation] = HostReact.useState<SkillInstallLocation | undefined>()
|
|
69
|
+
const [installing, setInstalling] = HostReact.useState(false)
|
|
70
|
+
const [notice, setNotice] = HostReact.useState<string | undefined>()
|
|
71
|
+
|
|
72
|
+
const loadPage = HostReact.useCallback(async (nextPage: number, search: string) => {
|
|
73
|
+
setLoading(true)
|
|
74
|
+
setError(undefined)
|
|
75
|
+
try {
|
|
76
|
+
const [result, installed] = await Promise.all([
|
|
77
|
+
runtime.api.list({ page: nextPage, pageSize: 10, search: search || undefined, sort: 'latest' }),
|
|
78
|
+
runtime.installer.installed(channel.cwd),
|
|
79
|
+
])
|
|
80
|
+
setItems(result.items)
|
|
81
|
+
setTotal(result.total)
|
|
82
|
+
setInstalledNames(new Set(installed.map(entry => entry.name)))
|
|
83
|
+
setSelected(0)
|
|
84
|
+
} catch (cause) {
|
|
85
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
86
|
+
} finally {
|
|
87
|
+
setLoading(false)
|
|
88
|
+
}
|
|
89
|
+
}, [channel.cwd])
|
|
90
|
+
|
|
91
|
+
HostReact.useEffect(() => {
|
|
92
|
+
const timer = setTimeout(() => { void loadPage(page, query) }, 250)
|
|
93
|
+
return () => clearTimeout(timer)
|
|
94
|
+
}, [loadPage, page, query])
|
|
95
|
+
|
|
96
|
+
ui.useInput((input, key) => {
|
|
97
|
+
if (installing) return
|
|
98
|
+
if (key.escape) {
|
|
99
|
+
if (location !== undefined) setLocation(undefined)
|
|
100
|
+
else close()
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const item = items[selected]
|
|
104
|
+
if (location !== undefined) {
|
|
105
|
+
if ((input === 'p' || input === 'u') && item !== undefined) {
|
|
106
|
+
const chosen: SkillInstallLocation = input === 'p' ? 'project' : 'user'
|
|
107
|
+
setInstalling(true)
|
|
108
|
+
setNotice(undefined)
|
|
109
|
+
void runtime.installer.install({ id: item.id, name: item.name, version: item.version, location: chosen, cwd: channel.cwd })
|
|
110
|
+
.then(result => {
|
|
111
|
+
setInstalledNames(current => new Set(current).add(item.name))
|
|
112
|
+
setNotice(`已安装到 ${result.installPath}`)
|
|
113
|
+
})
|
|
114
|
+
.catch(cause => setError(cause instanceof Error ? cause.message : String(cause)))
|
|
115
|
+
.finally(() => { setInstalling(false); setLocation(undefined) })
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (key.upArrow) setSelected(index => Math.max(0, index - 1))
|
|
120
|
+
else if (key.downArrow) setSelected(index => Math.min(items.length - 1, index + 1))
|
|
121
|
+
else if (key.leftArrow && page > 1) setPage(value => value - 1)
|
|
122
|
+
else if (key.rightArrow && page * 10 < total) setPage(value => value + 1)
|
|
123
|
+
else if (key.return && item !== undefined) setLocation('project')
|
|
124
|
+
else if (key.backspace) { setPage(1); setQuery(value => value.slice(0, -1)) }
|
|
125
|
+
else if (input.length === 1 && !/[\u0000-\u001f\u007f]/u.test(input)) { setPage(1); setQuery(value => value + input) }
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const h = HostReact.createElement
|
|
129
|
+
const item = items[selected]
|
|
130
|
+
const pages = Math.max(1, Math.ceil(total / 10))
|
|
131
|
+
return h(Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
132
|
+
h(Text, { bold: true, color: 'cyan' }, '技能市场'),
|
|
133
|
+
h(Text, { dimColor: true }, `搜索:${query || '(直接输入关键词)'} 第 ${page}/${pages} 页,共 ${total} 个技能`),
|
|
134
|
+
h(Text, { dimColor: true }, '↑↓ 选择 · ←→ 翻页 · Enter 安装 · Esc 返回'),
|
|
135
|
+
h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
136
|
+
loading ? h(Text, { color: 'yellow' }, '正在加载…') : null,
|
|
137
|
+
error ? h(Text, { color: 'red' }, `错误:${error}`) : null,
|
|
138
|
+
!loading && error === undefined && items.length === 0 ? h(Text, { dimColor: true }, '没有找到技能') : null,
|
|
139
|
+
...items.map((entry, index) => h(Text, {
|
|
140
|
+
key: String(entry.id),
|
|
141
|
+
bold: index === selected,
|
|
142
|
+
color: index === selected ? 'cyan' : undefined,
|
|
143
|
+
}, `${index === selected ? '▸' : ' '} ${entry.name} ${entry.version ? `v${entry.version}` : ''}${installedNames.has(entry.name) ? ' [已安装]' : ''}`)),
|
|
144
|
+
),
|
|
145
|
+
item === undefined ? null : h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
146
|
+
h(Text, { bold: true }, `${item.name}${item.author || item.creatorName ? ` · ${item.author || item.creatorName}` : ''}`),
|
|
147
|
+
h(Text, { dimColor: true }, item.description || '暂无简介'),
|
|
148
|
+
),
|
|
149
|
+
location === undefined ? null : h(Box, { marginTop: 1, flexDirection: 'column' },
|
|
150
|
+
h(Text, { color: 'yellow' }, `安装 ${item?.name ?? ''}`),
|
|
151
|
+
h(Text, null, '按 P 安装到当前项目 .chatcode-cli/skills,按 U 安装到个人 CHATCODE_CLI_HOME/skills,Esc 取消'),
|
|
152
|
+
),
|
|
153
|
+
installing ? h(Text, { color: 'yellow' }, '正在下载并验证技能包…') : null,
|
|
154
|
+
notice ? h(Text, { color: 'green' }, notice) : null,
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Sort orders accepted by the public skill catalog. */
|
|
2
|
+
export type SkillSort = 'latest' | 'downloads' | 'name'
|
|
3
|
+
|
|
4
|
+
/** Supported installation scopes. */
|
|
5
|
+
export type SkillInstallLocation = 'project' | 'user'
|
|
6
|
+
|
|
7
|
+
/** One skill returned by the public catalog. */
|
|
8
|
+
export interface SkillMarketItem {
|
|
9
|
+
readonly id: number
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly description: string
|
|
12
|
+
readonly category: string
|
|
13
|
+
readonly status: string
|
|
14
|
+
readonly downloads: number
|
|
15
|
+
readonly views: number
|
|
16
|
+
readonly readme: string
|
|
17
|
+
readonly version: string
|
|
18
|
+
readonly author: string
|
|
19
|
+
readonly creatorName: string
|
|
20
|
+
readonly tags: string
|
|
21
|
+
readonly createTime: string
|
|
22
|
+
readonly updateTime: string
|
|
23
|
+
readonly visibility: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Catalog list filters. */
|
|
27
|
+
export interface SkillListQuery {
|
|
28
|
+
readonly search?: string
|
|
29
|
+
readonly category?: string
|
|
30
|
+
readonly sort?: SkillSort
|
|
31
|
+
readonly tag?: string
|
|
32
|
+
readonly page?: number
|
|
33
|
+
readonly pageSize?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** One page returned by the public catalog. */
|
|
37
|
+
export interface SkillListResult {
|
|
38
|
+
readonly items: readonly SkillMarketItem[]
|
|
39
|
+
readonly total: number
|
|
40
|
+
readonly page: number
|
|
41
|
+
readonly pageSize: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Marketplace ownership metadata stored beside an installed skill. */
|
|
45
|
+
export interface SkillMarketMeta {
|
|
46
|
+
readonly id: number
|
|
47
|
+
readonly name: string
|
|
48
|
+
readonly version?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A skill installed in one of the roots visible to the current session. */
|
|
52
|
+
export interface InstalledSkill {
|
|
53
|
+
readonly name: string
|
|
54
|
+
readonly location: SkillInstallLocation
|
|
55
|
+
readonly path: string
|
|
56
|
+
readonly marketId?: number
|
|
57
|
+
readonly version?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Successful installation result returned to Web and terminal callers. */
|
|
61
|
+
export interface SkillInstallResult {
|
|
62
|
+
readonly skillName: string
|
|
63
|
+
readonly installPath: string
|
|
64
|
+
readonly location: SkillInstallLocation
|
|
65
|
+
readonly updated: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resolved runtime configuration for the skill marketplace. */
|
|
69
|
+
export interface ResolvedSkillMarketConfig {
|
|
70
|
+
readonly baseUrl: string
|
|
71
|
+
readonly requestTimeoutMs: number
|
|
72
|
+
readonly downloadTimeoutMs: number
|
|
73
|
+
readonly defaultPageSize: number
|
|
74
|
+
readonly maxDownloadBytes: number
|
|
75
|
+
readonly maxArchiveEntries: number
|
|
76
|
+
readonly maxUncompressedBytes: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Operator overrides accepted below the plugin's `skillMarket` key. */
|
|
80
|
+
export type SkillMarketConfig = Partial<ResolvedSkillMarketConfig>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
+
import { readJsonBody, sameOrigin, sendJson } from '../http.ts'
|
|
3
|
+
import type { WebServerService } from '../routes.ts'
|
|
4
|
+
import type { SkillMarketRuntime } from './runtime.ts'
|
|
5
|
+
import type { SkillInstallLocation, SkillListQuery, SkillSort } from './types.ts'
|
|
6
|
+
|
|
7
|
+
interface SessionLike {
|
|
8
|
+
readonly header: { readonly cwd?: string }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SkillMarketWebHost {
|
|
12
|
+
readonly webServer: WebServerService
|
|
13
|
+
readonly sessions: { get(id: string): SessionLike | undefined }
|
|
14
|
+
readonly logger?: { warn(message: string): void }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Mount local-session skill catalog and installation routes. */
|
|
18
|
+
export function mountSkillMarketRoutes(host: SkillMarketWebHost, runtime: SkillMarketRuntime): () => void {
|
|
19
|
+
const routes = [
|
|
20
|
+
host.webServer.register({ kind: 'exact', path: '/dsh-market/skills', handler: async (request, response) => {
|
|
21
|
+
if (request.method !== 'GET') return methodNotAllowed(response, 'GET')
|
|
22
|
+
try {
|
|
23
|
+
const url = requestUrl(request)
|
|
24
|
+
const query: SkillListQuery = {
|
|
25
|
+
search: optionalParam(url, 'search'),
|
|
26
|
+
category: optionalParam(url, 'category'),
|
|
27
|
+
tag: optionalParam(url, 'tag'),
|
|
28
|
+
sort: sortParam(url),
|
|
29
|
+
page: integerParam(url, 'page'),
|
|
30
|
+
pageSize: integerParam(url, 'pageSize'),
|
|
31
|
+
}
|
|
32
|
+
sendJson(response, 200, await runtime.api.list(query))
|
|
33
|
+
} catch (error) {
|
|
34
|
+
sendRouteError(host, response, error)
|
|
35
|
+
}
|
|
36
|
+
} }),
|
|
37
|
+
host.webServer.register({ kind: 'exact', path: '/dsh-market/skills/installed', handler: async (request, response) => {
|
|
38
|
+
if (request.method !== 'GET') return methodNotAllowed(response, 'GET')
|
|
39
|
+
try {
|
|
40
|
+
const cwd = sessionCwd(host, requestUrl(request).searchParams.get('sessionId'))
|
|
41
|
+
sendJson(response, 200, { items: await runtime.installer.installed(cwd) })
|
|
42
|
+
} catch (error) {
|
|
43
|
+
sendRouteError(host, response, error)
|
|
44
|
+
}
|
|
45
|
+
} }),
|
|
46
|
+
host.webServer.register({ kind: 'exact', path: '/dsh-market/skills/install', handler: async (request, response) => {
|
|
47
|
+
if (request.method !== 'POST') return methodNotAllowed(response, 'POST')
|
|
48
|
+
if (!sameOrigin(request)) {
|
|
49
|
+
sendJson(response, 403, { error: 'origin rejected' })
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const body = installBody(await readJsonBody(request))
|
|
54
|
+
const cwd = sessionCwd(host, body.sessionId)
|
|
55
|
+
const result = await runtime.installer.install({ ...body, cwd })
|
|
56
|
+
sendJson(response, 200, result)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
sendRouteError(host, response, error)
|
|
59
|
+
}
|
|
60
|
+
} }),
|
|
61
|
+
]
|
|
62
|
+
return () => {
|
|
63
|
+
for (const dispose of routes.reverse()) dispose()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function requestUrl(request: IncomingMessage): URL {
|
|
68
|
+
return new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sessionCwd(host: SkillMarketWebHost, sessionId: string | null): string {
|
|
72
|
+
if (sessionId === null || sessionId.trim().length === 0) throw new Error('请选择一个本地会话后再安装技能')
|
|
73
|
+
const session = host.sessions.get(sessionId)
|
|
74
|
+
if (session === undefined) throw new Error('会话不存在或已结束')
|
|
75
|
+
const cwd = session.header.cwd
|
|
76
|
+
if (cwd === undefined) throw new Error('该会话没有本地工作目录')
|
|
77
|
+
return cwd
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function installBody(value: unknown): {
|
|
81
|
+
id: number
|
|
82
|
+
name: string
|
|
83
|
+
version?: string
|
|
84
|
+
location: SkillInstallLocation
|
|
85
|
+
sessionId: string
|
|
86
|
+
} {
|
|
87
|
+
if (typeof value !== 'object' || value === null) throw new Error('安装请求必须是 JSON 对象')
|
|
88
|
+
const body = value as Record<string, unknown>
|
|
89
|
+
if (!Number.isSafeInteger(body.id) || (body.id as number) <= 0) throw new Error('安装请求缺少有效的技能 id')
|
|
90
|
+
if (typeof body.name !== 'string' || body.name.length === 0) throw new Error('安装请求缺少技能名称')
|
|
91
|
+
if (body.location !== 'project' && body.location !== 'user') throw new Error('安装位置必须是 project 或 user')
|
|
92
|
+
if (typeof body.sessionId !== 'string' || body.sessionId.length === 0) throw new Error('安装请求缺少 sessionId')
|
|
93
|
+
if (body.version !== undefined && typeof body.version !== 'string') throw new Error('技能版本必须是字符串')
|
|
94
|
+
return {
|
|
95
|
+
id: body.id as number,
|
|
96
|
+
name: body.name,
|
|
97
|
+
location: body.location,
|
|
98
|
+
sessionId: body.sessionId,
|
|
99
|
+
version: body.version as string | undefined,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function optionalParam(url: URL, name: string): string | undefined {
|
|
104
|
+
const value = url.searchParams.get(name)?.trim()
|
|
105
|
+
return value ? value : undefined
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function integerParam(url: URL, name: string): number | undefined {
|
|
109
|
+
const value = url.searchParams.get(name)
|
|
110
|
+
if (value === null) return undefined
|
|
111
|
+
const number = Number(value)
|
|
112
|
+
if (!Number.isSafeInteger(number) || number <= 0) throw new Error(`${name} 必须是正整数`)
|
|
113
|
+
return number
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sortParam(url: URL): SkillSort | undefined {
|
|
117
|
+
const value = optionalParam(url, 'sort')
|
|
118
|
+
if (value === undefined) return undefined
|
|
119
|
+
if (value !== 'latest' && value !== 'downloads' && value !== 'name') throw new Error('sort 参数无效')
|
|
120
|
+
return value
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function methodNotAllowed(response: ServerResponse, allowed: string): void {
|
|
124
|
+
response.setHeader('allow', allowed)
|
|
125
|
+
sendJson(response, 405, { error: 'method not allowed' })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function sendRouteError(host: SkillMarketWebHost, response: ServerResponse, error: unknown): void {
|
|
129
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
130
|
+
host.logger?.warn(`[ChatCode CLI Market] skill market request failed: ${message}`)
|
|
131
|
+
sendJson(response, 400, { error: message })
|
|
132
|
+
}
|