diff --git a/package.json b/package.json
index 97f8050..33c39b1 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@kevisual/cnb-center",
"private": true,
- "version": "0.0.5",
+ "version": "0.0.6",
"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.5 -y y -u"
+ "pub": "envision deploy ./dist -k cnb-center -v 0.0.6 -y y -u"
},
"files": [
"dist"
@@ -28,6 +28,7 @@
"@kevisual/kv-login": "^0.1.15",
"@kevisual/router": "0.0.80",
"@tanstack/react-router": "^1.161.1",
+ "@uiw/react-codemirror": "^4.25.5",
"ai": "^6.0.91",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -49,6 +50,7 @@
"access": "public"
},
"devDependencies": {
+ "@codemirror/lang-yaml": "^6.1.2",
"@kevisual/gitea": "^0.0.6",
"@kevisual/query": "0.0.49",
"@kevisual/types": "^0.0.12",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8fc7f32..ebd3ecf 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -41,6 +41,9 @@ importers:
'@tanstack/react-router':
specifier: ^1.161.1
version: 1.162.9(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
+ '@uiw/react-codemirror':
+ specifier: ^4.25.5
+ version: 4.25.5(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
ai:
specifier: ^6.0.91
version: 6.0.97(zod@4.3.6)
@@ -90,6 +93,9 @@ importers:
specifier: ^5.0.11
version: 5.0.11(@types/react@19.2.14)(react@19.2.4)(use-sync-external-store@1.6.0(react@19.2.4))
devDependencies:
+ '@codemirror/lang-yaml':
+ specifier: ^6.1.2
+ version: 6.1.2
'@kevisual/gitea':
specifier: ^0.0.6
version: 0.0.6
@@ -295,6 +301,33 @@ packages:
'@types/react':
optional: true
+ '@codemirror/autocomplete@6.20.0':
+ resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
+
+ '@codemirror/commands@6.10.2':
+ resolution: {integrity: sha512-vvX1fsih9HledO1c9zdotZYUZnE4xV0m6i3m25s5DIfXofuprk6cRcLUZvSk3CASUbwjQX21tOGbkY2BH8TpnQ==}
+
+ '@codemirror/lang-yaml@6.1.2':
+ resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
+
+ '@codemirror/language@6.12.2':
+ resolution: {integrity: sha512-jEPmz2nGGDxhRTg3lTpzmIyGKxz3Gp3SJES4b0nAuE5SWQoKdT5GoQ69cwMmFd+wvFUhYirtDTr0/DRHpQAyWg==}
+
+ '@codemirror/lint@6.9.4':
+ resolution: {integrity: sha512-ABc9vJ8DEmvOWuH26P3i8FpMWPQkduD9Rvba5iwb6O3hxASgclm3T3krGo8NASXkHCidz6b++LWlzWIUfEPSWw==}
+
+ '@codemirror/search@6.6.0':
+ resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
+
+ '@codemirror/state@6.5.4':
+ resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
+
+ '@codemirror/theme-one-dark@6.1.3':
+ resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
+
+ '@codemirror/view@6.39.15':
+ resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
+
'@emnapi/core@1.8.1':
resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==}
@@ -545,6 +578,21 @@ packages:
resolution: {integrity: sha512-jLsL80wBBKkrJZrfk3SQpJ9JA/zREdlUROj7eCkmzqduAWKSI0wVcXuCKf+mLFCHB0Q0Tkh2rgzjSlurt3JQgw==}
engines: {node: '>=10.0.0'}
+ '@lezer/common@1.5.1':
+ resolution: {integrity: sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw==}
+
+ '@lezer/highlight@1.2.3':
+ resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
+
+ '@lezer/lr@1.4.8':
+ resolution: {integrity: sha512-bPWa0Pgx69ylNlMlPvBPryqeLYQjyJjqPx+Aupm5zydLIF3NE+6MMLT8Yi23Bd9cif9VS00aUebn+6fDIGBcDA==}
+
+ '@lezer/yaml@1.0.4':
+ resolution: {integrity: sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw==}
+
+ '@marijn/find-cluster-break@1.0.2':
+ resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+
'@napi-rs/wasm-runtime@1.1.1':
resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==}
@@ -856,6 +904,28 @@ packages:
'@types/react@19.2.14':
resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==}
+ '@uiw/codemirror-extensions-basic-setup@4.25.5':
+ resolution: {integrity: sha512-2KWS4NqrS9SQzlPs/3sxFhuArvjB3JF6WpsrZqBtGHM5/smCNTULX3lUGeRH+f3mkfMt0k6DR+q0xCW9k+Up5w==}
+ peerDependencies:
+ '@codemirror/autocomplete': '>=6.0.0'
+ '@codemirror/commands': '>=6.0.0'
+ '@codemirror/language': '>=6.0.0'
+ '@codemirror/lint': '>=6.0.0'
+ '@codemirror/search': '>=6.0.0'
+ '@codemirror/state': '>=6.0.0'
+ '@codemirror/view': '>=6.0.0'
+
+ '@uiw/react-codemirror@4.25.5':
+ resolution: {integrity: sha512-WUMBGwfstufdbnaiMzQzmOf+6Mzf0IbiOoleexC9ItWcDTJybidLtEi20aP2N58Wn/AQxsd5Otebydaimh7Opw==}
+ peerDependencies:
+ '@babel/runtime': '>=7.11.0'
+ '@codemirror/state': '>=6.0.0'
+ '@codemirror/theme-one-dark': '>=6.0.0'
+ '@codemirror/view': '>=6.0.0'
+ codemirror: '>=6.0.0'
+ react: '>=17.0.0'
+ react-dom: '>=17.0.0'
+
'@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
engines: {node: '>= 20'}
@@ -931,6 +1001,9 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
+ codemirror@6.0.2:
+ resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
+
convert-source-map@2.0.0:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
@@ -940,6 +1013,9 @@ packages:
cookie-es@2.0.0:
resolution: {integrity: sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==}
+ crelt@1.0.6:
+ resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
crossws@0.3.5:
resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==}
@@ -1333,6 +1409,9 @@ packages:
spark-md5@3.0.2:
resolution: {integrity: sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw==}
+ style-mod@4.1.3:
+ resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
+
tabbable@6.4.0:
resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==}
@@ -1505,6 +1584,9 @@ packages:
yaml:
optional: true
+ w3c-keyname@2.2.8:
+ resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
webpack-virtual-modules@0.6.2:
resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
@@ -1721,6 +1803,69 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.14
+ '@codemirror/autocomplete@6.20.0':
+ dependencies:
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ '@lezer/common': 1.5.1
+
+ '@codemirror/commands@6.10.2':
+ dependencies:
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ '@lezer/common': 1.5.1
+
+ '@codemirror/lang-yaml@6.1.2':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.0
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+ '@lezer/yaml': 1.0.4
+
+ '@codemirror/language@6.12.2':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+ style-mod: 4.1.3
+
+ '@codemirror/lint@6.9.4':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ crelt: 1.0.6
+
+ '@codemirror/search@6.6.0':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ crelt: 1.0.6
+
+ '@codemirror/state@6.5.4':
+ dependencies:
+ '@marijn/find-cluster-break': 1.0.2
+
+ '@codemirror/theme-one-dark@6.1.3':
+ dependencies:
+ '@codemirror/language': 6.12.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+ '@lezer/highlight': 1.2.3
+
+ '@codemirror/view@6.39.15':
+ dependencies:
+ '@codemirror/state': 6.5.4
+ crelt: 1.0.6
+ style-mod: 4.1.3
+ w3c-keyname: 2.2.8
+
'@emnapi/core@1.8.1':
dependencies:
'@emnapi/wasi-threads': 1.1.0
@@ -2002,6 +2147,24 @@ snapshots:
'@kevisual/ws@8.19.0': {}
+ '@lezer/common@1.5.1': {}
+
+ '@lezer/highlight@1.2.3':
+ dependencies:
+ '@lezer/common': 1.5.1
+
+ '@lezer/lr@1.4.8':
+ dependencies:
+ '@lezer/common': 1.5.1
+
+ '@lezer/yaml@1.0.4':
+ dependencies:
+ '@lezer/common': 1.5.1
+ '@lezer/highlight': 1.2.3
+ '@lezer/lr': 1.4.8
+
+ '@marijn/find-cluster-break@1.0.2': {}
+
'@napi-rs/wasm-runtime@1.1.1':
dependencies:
'@emnapi/core': 1.8.1
@@ -2278,6 +2441,33 @@ snapshots:
dependencies:
csstype: 3.2.3
+ '@uiw/codemirror-extensions-basic-setup@4.25.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)':
+ dependencies:
+ '@codemirror/autocomplete': 6.20.0
+ '@codemirror/commands': 6.10.2
+ '@codemirror/language': 6.12.2
+ '@codemirror/lint': 6.9.4
+ '@codemirror/search': 6.6.0
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+
+ '@uiw/react-codemirror@4.25.5(@babel/runtime@7.28.6)(@codemirror/autocomplete@6.20.0)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/theme-one-dark@6.1.3)(@codemirror/view@6.39.15)(codemirror@6.0.2)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
+ dependencies:
+ '@babel/runtime': 7.28.6
+ '@codemirror/commands': 6.10.2
+ '@codemirror/state': 6.5.4
+ '@codemirror/theme-one-dark': 6.1.3
+ '@codemirror/view': 6.39.15
+ '@uiw/codemirror-extensions-basic-setup': 4.25.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.2)(@codemirror/language@6.12.2)(@codemirror/lint@6.9.4)(@codemirror/search@6.6.0)(@codemirror/state@6.5.4)(@codemirror/view@6.39.15)
+ codemirror: 6.0.2
+ react: 19.2.4
+ react-dom: 19.2.4(react@19.2.4)
+ transitivePeerDependencies:
+ - '@codemirror/autocomplete'
+ - '@codemirror/language'
+ - '@codemirror/lint'
+ - '@codemirror/search'
+
'@vercel/oidc@3.1.0': {}
'@vitejs/plugin-react@5.1.4(vite@8.0.0-beta.15(@types/node@25.3.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0))':
@@ -2364,12 +2554,24 @@ snapshots:
clsx@2.1.1: {}
+ codemirror@6.0.2:
+ dependencies:
+ '@codemirror/autocomplete': 6.20.0
+ '@codemirror/commands': 6.10.2
+ '@codemirror/language': 6.12.2
+ '@codemirror/lint': 6.9.4
+ '@codemirror/search': 6.6.0
+ '@codemirror/state': 6.5.4
+ '@codemirror/view': 6.39.15
+
convert-source-map@2.0.0: {}
cookie-es@1.2.2: {}
cookie-es@2.0.0: {}
+ crelt@1.0.6: {}
+
crossws@0.3.5:
dependencies:
uncrypto: 0.1.3
@@ -2690,6 +2892,8 @@ snapshots:
spark-md5@3.0.2: {}
+ style-mod@4.1.3: {}
+
tabbable@6.4.0: {}
tailwind-merge@3.5.0: {}
@@ -2775,6 +2979,8 @@ snapshots:
jiti: 2.6.1
tsx: 4.21.0
+ w3c-keyname@2.2.8: {}
+
webpack-virtual-modules@0.6.2: {}
yallist@3.1.1: {}
diff --git a/src/pages/repos/components/BuildConfig.tsx b/src/pages/repos/components/BuildConfig.tsx
new file mode 100644
index 0000000..3ecb64b
--- /dev/null
+++ b/src/pages/repos/components/BuildConfig.tsx
@@ -0,0 +1,130 @@
+import { useEffect, useState } from "react";
+import { useRepoStore } from "../store";
+import { useShallow } from "zustand/shallow";
+import { toast } from "sonner";
+import CodeMirror from "@uiw/react-codemirror";
+import { yaml } from "@codemirror/lang-yaml";
+import { useLayoutStore } from "@/pages/auth/store";
+import { Input } from "@/components/ui/input";
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
+import { Workflow } from "lucide-react";
+
+export const BuildConfig = () => {
+ const repoStore = useRepoStore(useShallow((state) => ({
+ getItem: state.getItem,
+ editRepo: state.editRepo,
+ buildConfig: state.buildConfig,
+ setBuildConfig: state.setBuildConfig,
+ initBuildConfig: state.initBuildConfig,
+ deleteBuildConfig: state.deleteBuildConfig,
+ loading: state.loading,
+ buildWorkspace: state.buildWorkspace,
+ })));
+ const repo = repoStore.editRepo!;
+ const me = useLayoutStore((state) => state.me);
+ const [localConfig, setLocalConfig] = useState(repoStore.buildConfig?.config || "");
+
+ // 同步 buildConfig 变化时的状态
+ useEffect(() => {
+ setLocalConfig(repoStore.buildConfig?.config || "");
+ }, [repoStore.buildConfig]);
+ useEffect(() => {
+ if (repo) {
+ repoStore.initBuildConfig({ repo: repo, user: me });
+ }
+ }, [repo, me])
+
+ const handleSave = () => {
+ if (repoStore.buildConfig) {
+ repoStore.setBuildConfig({
+ ...repoStore.buildConfig,
+ config: localConfig,
+ }, true);
+ }
+ };
+
+ const handleFieldChange = (field: string, value: string | null) => {
+ if (repoStore.buildConfig && value !== null) {
+ repoStore.setBuildConfig({
+ ...repoStore.buildConfig,
+ [field]: value,
+ }, false);
+ }
+ };
+ if (repoStore.loading) {
+ return
Loading...
+ }
+ return (
+
+ {/* 左侧边栏 - 配置信息 */}
+
+
+ 构建配置
+
+
+
+
+ handleFieldChange("repo", e.target.value)}
+ placeholder="仓库名称"
+ />
+
+
+
+ handleFieldChange("branch", e.target.value)}
+ placeholder="分支名称"
+ />
+
+
+
+ handleFieldChange("event", e.target.value)}
+ placeholder="事件名称"
+ />
+
+
+
+ {/* 右侧 - 编辑器 */}
+
+
+
配置文件
+
+
+
+
+
+
+ setLocalConfig(value)}
+ theme="light"
+ />
+
+
+
+ )
+}
+
+export default BuildConfig;
diff --git a/src/pages/repos/components/RepoCard.tsx b/src/pages/repos/components/RepoCard.tsx
index a0221b0..7535e17 100644
--- a/src/pages/repos/components/RepoCard.tsx
+++ b/src/pages/repos/components/RepoCard.tsx
@@ -13,13 +13,14 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
-import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square } from 'lucide-react'
+import { Star, GitFork, FileText, Edit, FolderGit2, MoreVertical, FileText as IssueIcon, Settings, Play, Trash2, RefreshCw, BookOpen, Copy, Clock, Info, Eye, Square, LinkIcon, ExternalLink } from 'lucide-react'
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 { toast } from 'sonner'
+import { useNavigate } from '@tanstack/react-router'
interface RepoCardProps {
repo: any
@@ -46,13 +47,13 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
const isWorkspaceActive = !!workspace
const owner = repo.path.split('/')[0]
const isMine = myOrgs.includes(owner)
-
+ const navigate = useNavigate();
const isKnowledge = repo?.flags === "KnowledgeBase"
const createKnow = async () => {
const res = await app.run({ path: 'cnb', key: 'build-knowledge-base', payload: { repo: repo.path } })
if (res.code === 200) {
toast.success("知识库创建中")
- getList(true)
+ getList({}, true)
}
}
const onClone = async () => {
@@ -72,6 +73,14 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
+
{
+ navigate({ to: `/repo?repo=${repo.path}` })
+ }}
+ >
+ {repo.path}
+
{isKnowledge && (
@@ -88,14 +97,6 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
)}
-
- {repo.path}
-
{isWorkspaceActive && (
@@ -195,13 +196,19 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
Clone URL
+ {
+ window.open(repo.web_url, '_blank')
+ }} className="cursor-pointer">
+
+ 访问仓库
+
onIssue(repo)} className="cursor-pointer">
- Issue
+ 访问问题
onSettings(repo)} className="cursor-pointer">
- 设置
+ 访问设置
{
@@ -268,22 +275,25 @@ export function RepoCard({ repo, onStartWorkspace, onEdit, onIssue, onSettings,
{repo.site && (
-
{
+ window.open(repo.site, '_blank')
+ }}
>
- 🔗 {repo.site}
-
+
+
+ {repo.site}
+
+
)}
-
{repo.description && (
-
+
{repo.description}
)}
+
diff --git a/src/pages/repos/components/RepoInfoCard.tsx b/src/pages/repos/components/RepoInfoCard.tsx
index 95f415b..dee83a6 100644
--- a/src/pages/repos/components/RepoInfoCard.tsx
+++ b/src/pages/repos/components/RepoInfoCard.tsx
@@ -1,16 +1,35 @@
+import { useNavigate } from "@tanstack/react-router";
+import { useMemo } from "react";
import { useRepoStore } from "../store";
import { useShallow } from "zustand/shallow";
import { toast } from "sonner";
import { Card } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
-import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
+import { Star, GitFork, FileText, ExternalLink, Calendar, User, Copy, ArrowLeft, Play, Square, Eye, BookOpen, RefreshCw } from "lucide-react";
+import { myOrgs } from "../store/build";
export const RepoInfoCard = () => {
- const repoStore = useRepoStore(useShallow((state) => ({
- getItem: state.getItem,
+ const navigate = useNavigate();
+ const { workspaceList, getWorkspaceDetail, stopWorkspace, editRepo, setSelectedSyncRepo, setSyncDialogOpen } = useRepoStore(useShallow((state) => ({
+ workspaceList: state.workspaceList,
+ getWorkspaceDetail: state.getWorkspaceDetail,
+ stopWorkspace: state.stopWorkspace,
editRepo: state.editRepo,
+ setSelectedSyncRepo: state.setSelectedSyncRepo,
+ setSyncDialogOpen: state.setSyncDialogOpen,
})));
- const repo = repoStore.editRepo!;
+ const repo = editRepo!;
+
+ const workspace = useMemo(() => {
+ return workspaceList.find(ws => ws.slug === repo.path)
+ }, [workspaceList, repo.path])
+ const isWorkspaceActive = !!workspace
+ const owner = repo.path.split('/')[0]
+ const isMine = myOrgs.includes(owner)
+ const isKnowledge = repo?.flags === "KnowledgeBase"
+
const onClone = () => {
const url = `git clone https://cnb.cool/${repo.path}`
navigator.clipboard.writeText(url).then(() => {
@@ -30,6 +49,13 @@ export const RepoInfoCard = () => {
{/* 标题行 */}
+
+
{repo.path}
@@ -43,37 +69,147 @@ export const RepoInfoCard = () => {
{repo.visibility_level === 'Public' ? '公开' : repo.visibility_level === 'Private' ? '私有' : repo.visibility_level}
+ {isKnowledge && (
+
+
+
+
+
+ }
+ />
+
+ 知识库
+
+
+
+ )}
+ {isWorkspaceActive && (
+
+
+
+
+ )}
+
+
+ {isWorkspaceActive && (
+
+
+ {
+ stopWorkspace(workspace)
+ }}
+ className="h-8 w-8 p-0 border-neutral-200 hover:border-red-600 hover:bg-red-600 hover:text-white transition-all cursor-pointer"
+ >
+
+
+ }
+ />
+
+ 停止工作区
+
+
+
+ )}
+
+
+ {
+ if (!isWorkspaceActive) {
+ // TODO: 启动工作区
+ } else {
+ getWorkspaceDetail(workspace)
+ }
+ }}
+ className="h-8 w-8 p-0 border-neutral-200 hover:border-neutral-900 hover:bg-neutral-900 hover:text-white transition-all cursor-pointer"
+ >
+ {isWorkspaceActive ? : }
+
+ }
+ />
+
+ {isWorkspaceActive ? '查看工作区' : '启动工作区'}
+
+
+
+
+
+ CNB
+
-
-
- 在 CNB 上查看
-
{/* 描述 */}
{repo.description && (
-
+
{repo.description}
)}
- {/* 主题标签 */}
- {repo.topics && (
-
- {repo.topics.split(',').map((topic: string, idx: number) => (
-
- {topic.trim()}
-
- ))}
-
- )}
+ {/* 主题标签和知识库 */}
+
+ {/* 主题标签 */}
+ {repo.topics && (
+
+ {repo.topics.split(',').map((topic: string, idx: number) => (
+
+ {topic.trim()}
+
+ ))}
+
+ )}
- {/* 语言和更新时间 */}
+
+
+ {/* 统计信息 */}
+
+
+
+ {repo.star_count}
+
+
+
+ {repo.fork_count}
+
+
+
+ {repo.open_issue_count}
+
+ {isWorkspaceActive && (
+
+
+ 运行中
+
+ )}
+ {isMine && (
+
{
+ setSelectedSyncRepo(repo)
+ setSyncDialogOpen(true)
+ }}
+ >
+
+ 同步
+
+ )}
+
+
+ {/* 更新信息 */}
{repo.last_update_nickname && (
diff --git a/src/pages/repos/modules/EditRepoDialog.tsx b/src/pages/repos/modules/EditRepoDialog.tsx
index a8110bf..9cbd2c4 100644
--- a/src/pages/repos/modules/EditRepoDialog.tsx
+++ b/src/pages/repos/modules/EditRepoDialog.tsx
@@ -59,7 +59,7 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps
const onSubmit = async (data: FormData) => {
if (!repo) return
-
+
await updateRepoInfo({
path: repo.path,
description: data.description?.trim() || '',
@@ -67,8 +67,8 @@ export function EditRepoDialog({ open, onOpenChange, repo }: EditRepoDialogProps
topics: tags.join(','),
license: data.license?.trim() || '',
})
-
- await getList(true)
+
+ await getList({}, true)
onOpenChange(false)
}
diff --git a/src/pages/repos/page.tsx b/src/pages/repos/page.tsx
index 2f7eb44..727170d 100644
--- a/src/pages/repos/page.tsx
+++ b/src/pages/repos/page.tsx
@@ -165,6 +165,21 @@ export const App = () => {
+
+
+ )
+}
+
+export const CommonRepoDialog = () => {
+ const { editRepo, showEditDialog, setShowEditDialog, showCreateDialog, setShowCreateDialog, } = useRepoStore(useShallow((state) => ({
+ editRepo: state.editRepo,
+ showEditDialog: state.showEditDialog,
+ setShowEditDialog: state.setShowEditDialog,
+ showCreateDialog: state.showCreateDialog,
+ setShowCreateDialog: state.setShowCreateDialog,
+ })))
+ return (
+ <>
{
/>
-
+ >
)
}
-
export default App;
\ No newline at end of file
diff --git a/src/pages/repos/repo/page.tsx b/src/pages/repos/repo/page.tsx
index 0c77deb..1c1c9e5 100644
--- a/src/pages/repos/repo/page.tsx
+++ b/src/pages/repos/repo/page.tsx
@@ -1,18 +1,27 @@
import { useSearch } from "@tanstack/react-router";
import { useRepoStore } from "../store";
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useShallow } from "zustand/shallow";
import { RepoInfoCard } from "../components/RepoInfoCard";
+import BuildConfig from "../components/BuildConfig";
+import { CommonRepoDialog } from "../page";
export const App = () => {
const params = useSearch({ strict: false }) as { repo?: string };
const repoStore = useRepoStore(useShallow((state) => ({
getItem: state.getItem,
editRepo: state.editRepo,
+ refresH: state.refresh,
})));
+ const [activeTab, setActiveTab] = useState("build");
+ const tabs = [
+ { key: "build", label: "构建配置" },
+ { key: "info", label: "基本信息" },
+ ]
useEffect(() => {
if (params.repo) {
repoStore.getItem(params.repo);
+ repoStore.refresH({ search: params.repo, showTips: false });
} else {
console.log('no repo param')
}
@@ -21,10 +30,30 @@ export const App = () => {
return Loading...
}
return (
-
+
+
+
+ {tabs.map(tab => (
+
setActiveTab(tab.key)}
+ >
+ {tab.label}
+
+ ))}
+
+ {activeTab === 'build' &&
}
+ {activeTab === 'info' && (
+
+
{JSON.stringify(repoStore.editRepo, null, 2)}
+
+ )}
+
+
)
}
diff --git a/src/pages/repos/store/build.ts b/src/pages/repos/store/build.ts
index b3745ee..c5cd4c9 100644
--- a/src/pages/repos/store/build.ts
+++ b/src/pages/repos/store/build.ts
@@ -49,4 +49,58 @@ export const createCommitBlankConfig = (params: { repo?: string, event: 'api_tri
git commit --allow-empty -m "up: ${now}"
git push
`
+}
+
+export const createDevConfig = (params: { repo?: string, event?: string }) => {
+ const event = params?.event || 'api_trigger_event';
+ return `##### 配置开始,保留注释 #####
+.common_env: &common_env
+ env:
+ # 使用环境变量管理密钥,推荐使用密钥仓库管理密钥, 详情见 readme.md
+ # 使用仓库密钥时,注释
+ ## 可选 API-Key 配置(按需取消注释)
+ # MINIMAX_API_KEY: '' # Minimax 模型
+ # ZHIPU_API_KEY: '' # 智谱 AI
+ # BAILIAN_CODE_API_KEY: '' # 阿里云百炼
+ # VOLCENGINE_API_KEY: '' # 火山引擎
+ # CNB_API_KEY: '' # CNB API
+
+ # 可选应用配置
+ # FEISHU_APP_ID: '' # 飞书应用 ID
+ # FEISHU_APP_SECRET: '' # 飞书应用密钥
+
+ # CNB_COOKIE: '' # 可选配置,用cnb.cool的cookie
+ USERNAME: root
+ ASSISTANT_CONFIG_DIR: /workspace/kevisual # ASSISTANT_CONFIG_DIR 环境变量指定了配置文件所在的目录
+ # CNB_KEVISUAL_ORG: kevisual # 私密仓库使用环境配置(默认即可,默认为当前用户组CNB_GROUP_SLUG)
+ # CNB_KEVISUAL_APP: assistant-app # 可选配置(默认即可)
+ # CNB_OPENCLAW: openclaw # 仓库名(默认即可)
+ # CNB_OPENWEBUI: open-webui # 仓库名(默认即可)
+
+##### 配置结束 #####
+
+main:
+ ${event}:
+ - docker:
+ image: docker.cnb.cool/kevisual/dev-env:latest
+ services:
+ - vscode
+ - docker
+ runner:
+ cpus: 16
+ imports:
+ - https://cnb.cool/kevisual/env/-/blob/main/.env.development
+ env: !reference [.common_env, env]
+ stages:
+ - name: 环境变量
+ script: printenv > ~/.env.development
+ - name: 启动nginx
+ script: nginx
+ - name: 初始化开发机
+ script: zsh /workspace/scripts/init.sh
+ # endStages:
+ # - name: 结束阶段
+ # script: bun /workspace/scripts/end.ts
+
+`
}
\ No newline at end of file
diff --git a/src/pages/repos/store/index.ts b/src/pages/repos/store/index.ts
index 7ca41d6..9f16786 100644
--- a/src/pages/repos/store/index.ts
+++ b/src/pages/repos/store/index.ts
@@ -3,7 +3,8 @@ import { query } from '@/modules/query';
import { toast } from 'sonner';
import { cnb } from '@/agents/app'
import { WorkspaceInfo } from '@kevisual/cnb'
-import { createBuildConfig, createCommitBlankConfig } from './build';
+import { createBuildConfig, createCommitBlankConfig, createDevConfig } from './build';
+import { useLayoutStore } from '@/pages/auth/store';
interface DisplayModule {
activity: boolean;
contributors: boolean;
@@ -52,6 +53,12 @@ interface Data {
type WorkspaceTabType = 'dev' | 'work'
+type BuildConfig = {
+ repo: string;
+ branch: string;
+ event: string;
+ config: string;
+}
type State = {
formData: Record
;
setFormData: (data: Record) => void;
@@ -68,13 +75,13 @@ type State = {
setShowEditDialog: (show: boolean) => void;
showCreateDialog: boolean;
setShowCreateDialog: (show: boolean) => void;
- getList: (silent?: boolean) => Promise;
+ getList: (params?: { search?: string }, silent?: boolean) => Promise;
updateRepoInfo: (data: Partial) => Promise;
createRepo: (data: { visibility: any, path: string, description: string, license: string }) => Promise;
deleteItem: (repo: string) => Promise;
workspaceList: WorkspaceInfo[];
getWorkspaceList: () => Promise;
- refresh: (opts?: { message?: string, showTips?: boolean }) => Promise;
+ refresh: (opts?: { message?: string, showTips?: boolean, search?: string }) => Promise;
startWorkspace: (data: Partial, params?: { open?: boolean, branch?: string }) => Promise;
stopWorkspace: (workspace?: WorkspaceInfo) => Promise;
getWorkspaceDetail: (data: WorkspaceInfo) => Promise;
@@ -89,6 +96,11 @@ type State = {
buildSync: (data: Partial, params: { toRepo?: string, fromRepo?: string }) => Promise;
buildUpdate: (data: Partial, params?: any) => Promise;
getItem: (repo: string) => Promise;
+ buildConfig: BuildConfig | null;
+ setBuildConfig: (config: BuildConfig | null, save?: boolean) => Promise;
+ deleteBuildConfig: (params: { repo: Data, user?: any }) => Promise;
+ initBuildConfig: (params: { repo: Data, user?: any }) => Promise;
+ buildWorkspace: () => Promise;
}
export const useRepoStore = create((set, get) => {
@@ -114,6 +126,127 @@ export const useRepoStore = create((set, get) => {
setSyncDialogOpen: (open) => set({ syncDialogOpen: open }),
selectedSyncRepo: null,
setSelectedSyncRepo: (repo) => set({ selectedSyncRepo: repo }),
+ buildConfig: null,
+ setBuildConfig: async (config, save = true) => {
+ const me = useLayoutStore.getState().me;
+ if (config && config!.repo && save) {
+ let path: string = config.repo || '';
+ path = path.replace(/\//g, '__');
+ const key = `buildConfig_${path}.json`;
+ if (config && me) {
+ const res = await query.post({
+ path: 'config',
+ key: 'update',
+ data: {
+ key: key,
+ data: config,
+ }
+ })
+ if (res.code === 200) {
+ toast.success('配置已保存')
+ } else {
+ toast.error(res.message || '配置保存失败')
+ }
+ } else if (config) {
+ localStorage.setItem(key, JSON.stringify(config));
+ }
+ }
+
+ set({ buildConfig: config })
+ },
+ deleteBuildConfig: async (params: { repo: Data, user?: any }) => {
+ const repo = params.repo;
+ let path: string = repo.path || '';
+ path = path.replace(/\//g, '__');
+ const key = `buildConfig_${path}.json`;
+ if (params?.user) {
+ const res = await query.post({
+ path: 'config',
+ key: 'delete',
+ data: {
+ key: key,
+ }
+ })
+ if (res.code === 200) {
+ toast.success('配置已删除')
+ } else {
+ toast.error(res.message || '配置删除失败')
+ }
+ } else {
+ localStorage.removeItem(key);
+ toast.success('配置已删除')
+ }
+ },
+ initBuildConfig: async (params) => {
+ const repo = params.repo;
+ if (!repo) {
+ toast.error('仓库数据异常');
+ return;
+ }
+ set({ loading: true })
+ try {
+ console.log('初始化构建配置', params)
+ let path: string = repo.path || '';
+ path = path.replace(/\//g, '__');
+ const key = `buildConfig_${path}.json`;
+ if (params?.user) {
+ const res = await query.post({
+ path: 'config',
+ key: 'get',
+ data: {
+ key: key,
+ }
+ })
+ if (res.code === 200 && res.data?.data) {
+ set({ buildConfig: res.data.data })
+ return;
+ }
+ } else {
+ const localConfig = localStorage.getItem(key);
+ if (localConfig) {
+ try {
+ const config = JSON.parse(localConfig);
+ set({ buildConfig: config })
+ return;
+ } catch (e) {
+ console.error('本地配置解析失败', e);
+ }
+ }
+ }
+ const config: BuildConfig = {
+ repo: repo.path,
+ branch: 'main',
+ event: 'api_trigger_event',
+ config: createDevConfig({ repo: repo.path, event: 'api_trigger_event' }),
+ }
+ set({ buildConfig: config })
+ } catch (e) {
+ toast.error('配置加载失败');
+ console.error('配置加载失败', e);
+ }
+ finally {
+ set({ loading: false })
+ }
+
+ },
+ buildWorkspace: async () => {
+ const config = get().buildConfig;
+ if (!config) {
+ toast.error('请先保存构建配置');
+ return;
+ }
+ const res = await cnb.build.startBuild(config.repo, {
+ branch: config.branch,
+ env: {},
+ event: config.event,
+ config: config.config,
+ })
+ if (res.code === 200) {
+ toast.success('构建已触发')
+ } else {
+ toast.error(res.message || '构建触发失败')
+ }
+ },
getItem: async (repo: string) => {
const { setLoading } = get();
setLoading(true);
@@ -129,13 +262,19 @@ export const useRepoStore = create((set, get) => {
setLoading(false);
}
},
- getList: async (silent = false) => {
+ getList: async (params?: { search?: string }, silent = false) => {
const { setLoading } = get();
if (!silent) {
setLoading(true);
}
try {
- const res = await cnb.repo.getRepoList({})
+ let opts = {}
+ if (params?.search) {
+ opts = {
+ search: params.search
+ }
+ }
+ const res = await cnb.repo.getRepoList(opts)
if (res.code === 200) {
const list = res.data! || []
set({ list });
@@ -171,8 +310,8 @@ export const useRepoStore = create((set, get) => {
toast.error(res.message || '更新失败');
}
},
- refresh: async (opts?: { message?: string, showTips?: boolean }) => {
- const getList = get().getList();
+ refresh: async (opts?: { message?: string, showTips?: boolean, search?: string }) => {
+ const getList = get().getList({ search: opts?.search }, true);
const getWorkspaceList = get().getWorkspaceList();
await Promise.all([getList, getWorkspaceList]);
if (opts?.showTips !== false) {
@@ -207,7 +346,7 @@ export const useRepoStore = create((set, get) => {
if (res.code === 200) {
toast.success('删除成功');
// 刷新列表
- await get().getList(true);
+ await get().getList({}, true);
} else {
toast.error(res.message || '删除失败');
}
@@ -216,7 +355,7 @@ export const useRepoStore = create((set, get) => {
if (e.message?.includes('JSON') || e.message?.includes('json')) {
toast.success('删除成功');
// 刷新列表
- await get().getList(true);
+ await get().getList({}, true);
} else {
toast.error('删除失败');
console.error('删除错误:', e);