feat: add cnb API module with various endpoints for workspace management, repository operations, and issue tracking

This commit is contained in:
2026-03-10 01:23:18 +08:00
parent 253ef2ac7d
commit 4ed81a1c68
10 changed files with 1264 additions and 288 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@kevisual/cnb-center",
"private": true,
"version": "0.0.7",
"version": "0.0.8",
"type": "module",
"basename": "/root/cnb-center",
"scripts": {
@@ -9,7 +9,7 @@
"build": "vite build",
"preview": "vite preview",
"ui": "pnpm dlx shadcn@latest add ",
"pub": "envision deploy ./dist -k cnb-center -v 0.0.7 -y y -u"
"pub": "envision deploy ./dist -k cnb-center -v 0.0.8 -y y -u"
},
"files": [
"dist"

View File

@@ -1,28 +1,9 @@
import { QueryRouterServer } from '@kevisual/router/browser'
import { useContextKey } from '@kevisual/context'
import { useConfigStore } from '@/pages/config/store'
import { useGiteaConfigStore } from '@/pages/config/gitea/store'
import { CNB } from '@kevisual/cnb'
import { Gitea } from '@kevisual/gitea';
export const app = useContextKey('app', new QueryRouterServer())
export const cnb: CNB = useContextKey('cnb', () => {
const state = useConfigStore.getState()
const config = state.config || {}
const cors: any = {}
if (config.ENABLE_CORS) {
cors.baseUrl = config.CNB_CORS_URL || 'https://cors.kevisual.cn'
}
console.log('state', state)
// if(state.config.)
return new CNB({
token: config.CNB_API_KEY,
cookie: config.CNB_COOKIE,
cors
})
})
// import '@kevisual/cnb-ai'
const url = 'https://kevisual.cn/root/cnb-ai/dist/app.js'

1227
src/modules/cnb-api.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,28 +3,14 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { Info } from 'lucide-react';
import { configSchema } from './store/schema';
import { toast } from 'sonner';
import { useLayoutStore } from '../auth/store';
import { useShallow } from 'zustand/shallow';
export const ConfigPage = () => {
const { config, setConfig, resetConfig, saveToRemote, loadFromRemote } = useConfigStore();
const layoutStore = useLayoutStore(useShallow(state => ({ me: state.me })))
const { config, setConfig, saveToRemote, loadFromRemote } = useConfigStore();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const result = configSchema.safeParse(config);
if (result.success) {
toast.success('配置已保存')
setTimeout(() => {
location.reload()
}, 400)
} else {
console.error('验证错误:', result.error.format());
}
saveToRemote();
};
const handleChange = (field: keyof typeof config, value: string | boolean) => {
@@ -105,87 +91,13 @@ export const ConfigPage = () => {
/>
</div>
<div className="space-y-2">
<Label htmlFor="cors-url"></Label>
<Input
id="cors-url"
type="url"
value={config.CNB_CORS_URL}
onChange={(e) => handleChange('CNB_CORS_URL', e.target.value)}
placeholder="https://cors.example.com"
/>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="enable-cors"
checked={config.ENABLE_CORS}
onCheckedChange={(checked) => handleChange('ENABLE_CORS', checked === true)}
/>
<Label htmlFor="enable-cors" className="cursor-pointer">
</Label>
</div>
<div className="space-y-2">
<Label htmlFor="ai-base-url">AI </Label>
<Input
id="ai-base-url"
type="url"
value={config.AI_BASE_URL}
onChange={(e) => handleChange('AI_BASE_URL', e.target.value)}
placeholder="请输入 AI 基础地址"
/>
</div>
<div className="space-y-2">
<Label htmlFor="ai-model">AI </Label>
<Input
id="ai-model"
type="text"
value={config.AI_MODEL}
onChange={(e) => handleChange('AI_MODEL', e.target.value)}
placeholder="请输入 AI 模型名称"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Label htmlFor="ai-api-key">AI </Label>
<Tooltip>
<TooltipTrigger>
<Info className="h-4 w-4 text-muted-foreground cursor-help" />
</TooltipTrigger>
<TooltipContent>
<p>
使 CNB AI API
</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="ai-api-key"
type="password"
value={config.AI_API_KEY}
onChange={(e) => handleChange('AI_API_KEY', e.target.value)}
placeholder="请输入您的 AI API 密钥"
/>
</div>
<div className="flex gap-4">
<Button type="submit"></Button>
<Button type="button" variant="outline" onClick={resetConfig}>
</Button>
{layoutStore.me && <>
<Button type="button" variant="outline" onClick={loadFromRemote}>
</Button>
<Button type="button" variant="outline" onClick={saveToRemote}>
</Button>
</>
}
</div>
</form>
</CardContent>

View File

@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import type { Config, defaultConfig } from './schema';
import type { Config, } from './schema';
import { queryLogin } from '@/modules/query';
import { toast } from 'sonner';
@@ -18,11 +18,6 @@ const STORAGE_KEY = 'cnb-config';
const DEFAULT_CONFIG = {
CNB_API_KEY: '',
CNB_COOKIE: '',
CNB_CORS_URL: 'https://cors.kevisual.cn',
ENABLE_CORS: true,
AI_BASE_URL: 'https://api.cnb.cool/kevisual/cnb-ai/-/ai/',
AI_MODEL: 'CNB-Models',
AI_API_KEY: ''
}
const loadInitialConfig = (): Config => {
try {

View File

@@ -3,11 +3,6 @@ import { z } from 'zod';
export const configSchema = z.object({
CNB_API_KEY: z.string().min(1, 'API Key is required'),
CNB_COOKIE: z.string().min(1, 'Cookie is required'),
CNB_CORS_URL: z.url('Must be a valid URL'),
ENABLE_CORS: z.boolean(),
AI_BASE_URL: z.url('Must be a valid URL'),
AI_MODEL: z.string().min(1, 'AI Model is required'),
AI_API_KEY: z.string().min(1, 'AI API Key is required'),
});
export type Config = z.infer<typeof configSchema>;
@@ -15,9 +10,4 @@ export type Config = z.infer<typeof configSchema>;
export const defaultConfig: Config = {
CNB_API_KEY: '',
CNB_COOKIE: '',
CNB_CORS_URL: 'https://cors.kevisual.cn',
ENABLE_CORS: true,
AI_BASE_URL: '',
AI_MODEL: '',
AI_API_KEY: ''
};

View File

@@ -18,7 +18,7 @@ import { useRepoStore } from '../store'
import { useMemo, useState } from 'react'
import { useShallow } from 'zustand/shallow'
import { myOrgs } from '../store/build'
import { app, cnb } from '@/agents/app'
import { app } from '@/agents/app'
import { toast } from 'sonner'
import { useNavigate } from '@tanstack/react-router'
import clsx from 'clsx'

View File

@@ -64,7 +64,7 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps
path: repo.path,
description: data.description?.trim() || '',
site: data.site?.trim() || '',
topics: tags.join(','),
topics: tags as any,
license: data.license?.trim() || '',
})

View File

@@ -1,11 +1,10 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { toast } from 'sonner';
import { cnb } from '@/agents/app'
import { queryApi as cnbApi } from '@/modules/cnb-api'
import { WorkspaceInfo } from '@kevisual/cnb'
import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
import { useLayoutStore } from '@/pages/auth/store';
import { useConfigStore } from '@/pages/config/store';
interface DisplayModule {
activity: boolean;
contributors: boolean;
@@ -77,7 +76,7 @@ type State = {
showCreateDialog: boolean;
setShowCreateDialog: (show: boolean) => void;
getList: (params?: { search?: string }, silent?: boolean) => Promise<any>;
updateRepoInfo: (data: Partial<Data>) => Promise<any>;
updateRepoInfo: (data: Partial<Data & { topics: string[] }>) => Promise<any>;
createRepo: (data: { visibility: any, path: string, description: string, license: string }) => Promise<any>;
deleteItem: (repo: string) => Promise<any>;
workspaceList: WorkspaceInfo[];
@@ -236,9 +235,10 @@ export const useRepoStore = create<State>((set, get) => {
toast.error('请先保存构建配置');
return;
}
const res = await cnb.build.startBuild(config.repo, {
const res = await cnbApi.cnb['cloud-build']({
repo: config.repo,
branch: config.branch,
env: {},
env: {} as any,
event: config.event,
config: config.config,
})
@@ -252,7 +252,7 @@ export const useRepoStore = create<State>((set, get) => {
const { setLoading } = get();
setLoading(true);
try {
const res = await cnb.repo.getRepo(repo)
const res = await cnbApi.cnb['get-repo']({ name: repo })
if (res.code === 200) {
const data = res.data!;
set({ editRepo: data })
@@ -275,9 +275,9 @@ export const useRepoStore = create<State>((set, get) => {
search: params.search
}
}
const res = await cnb.repo.getRepoList(opts)
const res = await cnbApi.cnb['list-repos'](opts)
if (res.code === 200) {
const list = res.data! || []
const list = res.data?.list || []
set({ list });
} else {
toast.error(res.message || '请求失败');
@@ -291,7 +291,7 @@ export const useRepoStore = create<State>((set, get) => {
},
updateRepoInfo: async (data) => {
const repo = data.path!;
let topics = data.topics?.split?.(',');
let topics = data.topics as string[];
if (Array.isArray(topics)) {
topics = topics.map(t => t.trim()).filter(Boolean);
}
@@ -299,12 +299,12 @@ export const useRepoStore = create<State>((set, get) => {
topics.push('cnb-center')
}
const updateData = {
description: data.description,
description: data.description!,
license: data?.license as any,
site: data.site,
topics: topics
}
const res = await cnb.repo.updateRepoInfo(repo, updateData)
const res = await cnbApi.cnb['update-repo-info']({ name: repo, ...updateData })
if (res.code === 200) {
toast.success('更新成功');
} else {
@@ -329,7 +329,7 @@ export const useRepoStore = create<State>((set, get) => {
description: data.description || '',
license: data?.license as any,
};
const res = await cnb.repo.createRepo(createData);
const res = await cnbApi.cnb['create-repo'](createData);
console.log('res', res)
// if (res.code === 200) {
// toast.success('仓库创建成功');
@@ -345,7 +345,7 @@ export const useRepoStore = create<State>((set, get) => {
},
deleteItem: async (repo: string) => {
try {
const res = await cnb.repo.deleteRepoCookie(repo)
const res = await cnbApi.cnb['delete-repo']({ name: repo });
if (res.code === 200) {
toast.success('删除成功');
// 刷新列表
@@ -367,7 +367,8 @@ export const useRepoStore = create<State>((set, get) => {
},
workspaceList: [],
getWorkspaceList: async () => {
const res = await cnb.workspace.list({
// const res = await cnb.workspace.list({
const res = await cnbApi.cnb['list-workspace']({
status: 'running',
pageSize: 100
})
@@ -381,9 +382,7 @@ export const useRepoStore = create<State>((set, get) => {
startWorkspace: async (data, params = { open: true, branch: 'main' }) => {
const repo = data.path;
const checkOpen = async () => {
const res = await cnb.workspace.startWorkspace(repo!, {
branch: params.branch || 'main'
})
const res = await cnbApi.cnb['start-workspace']({ repo: repo!, branch: params.branch || 'main' });
if (res.code === 200) {
if (!res?.data?.sn) {
const url = res.data?.url! || '';
@@ -456,7 +455,7 @@ export const useRepoStore = create<State>((set, get) => {
toast.error('未选择工作区');
return;
}
const res = await cnb.workspace.stopWorkspace({ sn });
const res = await cnbApi.cnb['stop-workspace']({ sn })
// @ts-ignore
if (res?.code === 200) {
toast.success('工作区已停止');
@@ -469,7 +468,7 @@ export const useRepoStore = create<State>((set, get) => {
},
selectWorkspace: undefined,
getWorkspaceDetail: async (workspaceInfo) => {
const res = await cnb.workspace.getDetail(workspaceInfo.slug, workspaceInfo.sn) as any;
const res = await cnbApi.cnb['get-workspace']({ repo: workspaceInfo.slug, sn: workspaceInfo.sn }) as any;
if (res.code === 200) {
set({
workspaceLink: res.data,
@@ -488,9 +487,10 @@ export const useRepoStore = create<State>((set, get) => {
return;
}
let event = toRepo ? 'api_trigger_sync_to_gitea' : 'api_trigger_sync_from_gitea';
const res = await cnb.build.startBuild(repo, {
const res = await cnbApi.cnb['cloud-build']({
repo: toRepo! || fromRepo!,
branch: 'main',
env: {},
env: {} as any,
event: event,
config: createBuildConfig({ repo: toRepo! || fromRepo! }),
})
@@ -501,9 +501,10 @@ export const useRepoStore = create<State>((set, get) => {
}
},
buildUpdate: async (data) => {
const res = await cnb.build.startBuild(data.path!, {
const res = await cnbApi.cnb['cloud-build']({
repo: data.path!,
branch: 'main',
env: {},
env: {} as any,
event: 'api_trigger_event',
config: createCommitBlankConfig({ repo: data.path!, event: 'api_trigger_event' }),
})

View File

@@ -1,130 +0,0 @@
import { create } from 'zustand';
import { query } from '@/modules/query';
import { toast } from 'sonner';
import { cnb } from '@/agents/app'
interface DisplayModule {
activity: boolean;
contributors: boolean;
release: boolean;
}
interface Languages {
language: string;
color: string;
}
interface Data {
id: string;
name: string;
freeze: boolean;
status: number;
visibility_level: string;
flags: string;
created_at: string;
updated_at: string;
description: string;
site: string;
topics: string;
license: string;
display_module: DisplayModule;
star_count: number;
fork_count: number;
mark_count: number;
last_updated_at?: string | null;
web_url: string;
path: string;
tags: any;
open_issue_count: number;
open_pull_request_count: number;
languages: Languages;
second_languages: Languages;
last_update_username: string;
last_update_nickname: string;
access: string;
stared: boolean;
star_time: string;
pinned: boolean;
pinned_time: string;
}
type State = {
formData: Record<string, any>;
setFormData: (data: Record<string, any>) => void;
showEdit: boolean;
setShowEdit: (showEdit: boolean) => void;
loading: boolean;
setLoading: (loading: boolean) => void;
list: Data[];
editRepo: Data | null;
setEditRepo: (repo: Data | null) => void;
showEditDialog: boolean;
setShowEditDialog: (show: boolean) => void;
getList: () => Promise<any>;
updateRepoInfo: (data: Partial<Data>) => Promise<any>;
}
export const useRepoStore = create<State>((set, get) => {
return {
formData: {},
setFormData: (data) => set({ formData: data }),
showEdit: false,
setShowEdit: (showEdit) => set({ showEdit }),
loading: false,
setLoading: (loading) => set({ loading }),
list: [],
editRepo: null,
setEditRepo: (repo) => set({ editRepo: repo }),
showEditDialog: false,
setShowEditDialog: (show) => set({ showEditDialog: show }),
getItem: async (id) => {
const { setLoading } = get();
setLoading(true);
try {
const res = await query.post({
path: 'demo',
key: 'item',
data: { id }
})
if (res.code === 200) {
return res;
} else {
toast.error(res.message || '请求失败');
}
} finally {
setLoading(false);
}
},
getList: async () => {
const { setLoading } = get();
setLoading(true);
try {
const res = await cnb.repo.getRepoList({})
if (res.code === 200) {
const list = res.data! || []
set({ list });
} else {
toast.error(res.message || '请求失败');
}
return res;
} finally {
setLoading(false);
}
},
updateRepoInfo: async (data) => {
const repo = data.path!;
const updateData = {
description: data.description,
license: data?.license as any,
site: data.site,
topics: data.topics?.split?.(','),
}
const res = await cnb.repo.updateRepoInfo(repo, updateData)
if (res.code === 200) {
toast.success('更新成功');
} else {
toast.error(res.message || '更新失败');
}
},
}
})