feat(agnet): allow users to bind git sources
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/heicode/manager/common"
|
||||
"github.com/heicode/manager/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type gitSourcePayload struct {
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Ref string `json:"ref"`
|
||||
Paths []string `json:"paths"`
|
||||
Usage string `json:"usage"`
|
||||
TenantId string `json:"tenant_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type gitSourceResponse struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id"`
|
||||
TenantId string `json:"tenant_id"`
|
||||
Name string `json:"name"`
|
||||
Provider string `json:"provider"`
|
||||
RepoURL string `json:"repo_url"`
|
||||
Ref string `json:"ref"`
|
||||
Paths []string `json:"paths"`
|
||||
Usage string `json:"usage"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
func normalizeGitSourcePayload(p gitSourcePayload) (gitSourcePayload, error) {
|
||||
p.Name = strings.TrimSpace(p.Name)
|
||||
p.Provider = strings.TrimSpace(p.Provider)
|
||||
p.RepoURL = strings.TrimSpace(p.RepoURL)
|
||||
p.Ref = strings.TrimSpace(p.Ref)
|
||||
p.Usage = strings.TrimSpace(p.Usage)
|
||||
p.TenantId = strings.TrimSpace(p.TenantId)
|
||||
p.Status = strings.TrimSpace(p.Status)
|
||||
|
||||
if p.Name == "" {
|
||||
return p, errors.New("name required")
|
||||
}
|
||||
if p.RepoURL == "" {
|
||||
return p, errors.New("repo_url required")
|
||||
}
|
||||
u, err := url.Parse(p.RepoURL)
|
||||
if err != nil || u.Scheme == "" || u.Host == "" {
|
||||
return p, errors.New("repo_url must be a valid http(s) URL")
|
||||
}
|
||||
if p.Ref == "" {
|
||||
p.Ref = "main"
|
||||
}
|
||||
if p.Provider == "" {
|
||||
p.Provider = "custom"
|
||||
}
|
||||
if p.Usage == "" {
|
||||
p.Usage = "project"
|
||||
}
|
||||
if p.Status == "" {
|
||||
p.Status = "active"
|
||||
}
|
||||
|
||||
paths := make([]string, 0, len(p.Paths))
|
||||
for _, path := range p.Paths {
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(path, "/") || strings.Contains(path, "..") {
|
||||
return p, errors.New("paths must be relative and must not contain ..")
|
||||
}
|
||||
paths = append(paths, path)
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
paths = []string{"."}
|
||||
}
|
||||
p.Paths = paths
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func gitSourceToResponse(src model.GitSource) gitSourceResponse {
|
||||
var paths []string
|
||||
if src.Paths != "" {
|
||||
_ = common.UnmarshalJsonStr(src.Paths, &paths)
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
paths = []string{"."}
|
||||
}
|
||||
return gitSourceResponse{
|
||||
Id: src.Id,
|
||||
UserId: src.UserId,
|
||||
TenantId: src.TenantId,
|
||||
Name: src.Name,
|
||||
Provider: src.Provider,
|
||||
RepoURL: src.RepoURL,
|
||||
Ref: src.Ref,
|
||||
Paths: paths,
|
||||
Usage: src.Usage,
|
||||
Status: src.Status,
|
||||
CreatedAt: src.CreatedAt,
|
||||
UpdatedAt: src.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func ListGitSources(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
var sources []model.GitSource
|
||||
if err := model.DB.Where("user_id = ?", userId).Order("id desc").Find(&sources).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
items := make([]gitSourceResponse, 0, len(sources))
|
||||
for _, src := range sources {
|
||||
items = append(items, gitSourceToResponse(src))
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func CreateGitSource(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
var payload gitSourcePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
|
||||
return
|
||||
}
|
||||
payload, err := normalizeGitSourcePayload(payload)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
paths, err := common.Marshal(payload.Paths)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
src := model.GitSource{
|
||||
UserId: userId,
|
||||
TenantId: payload.TenantId,
|
||||
Name: payload.Name,
|
||||
Provider: payload.Provider,
|
||||
RepoURL: payload.RepoURL,
|
||||
Ref: payload.Ref,
|
||||
Paths: string(paths),
|
||||
Usage: payload.Usage,
|
||||
Status: payload.Status,
|
||||
}
|
||||
if err := model.DB.Create(&src).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gitSourceToResponse(src))
|
||||
}
|
||||
|
||||
func UpdateGitSource(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
var src model.GitSource
|
||||
if err := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).First(&src).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
common.ApiErrorMsg(c, "git source not found")
|
||||
return
|
||||
}
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
var payload gitSourcePayload
|
||||
if err := c.ShouldBindJSON(&payload); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": "invalid params"})
|
||||
return
|
||||
}
|
||||
payload, err := normalizeGitSourcePayload(payload)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
paths, err := common.Marshal(payload.Paths)
|
||||
if err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
src.TenantId = payload.TenantId
|
||||
src.Name = payload.Name
|
||||
src.Provider = payload.Provider
|
||||
src.RepoURL = payload.RepoURL
|
||||
src.Ref = payload.Ref
|
||||
src.Paths = string(paths)
|
||||
src.Usage = payload.Usage
|
||||
src.Status = payload.Status
|
||||
if err := model.DB.Save(&src).Error; err != nil {
|
||||
common.ApiError(c, err)
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gitSourceToResponse(src))
|
||||
}
|
||||
|
||||
func DeleteGitSource(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
res := model.DB.Where("id = ? AND user_id = ?", c.Param("id"), userId).Delete(&model.GitSource{})
|
||||
if res.Error != nil {
|
||||
common.ApiError(c, res.Error)
|
||||
return
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
common.ApiErrorMsg(c, "git source not found")
|
||||
return
|
||||
}
|
||||
common.ApiSuccess(c, gin.H{"deleted": true})
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
type GitSource struct {
|
||||
Id int `json:"id"`
|
||||
UserId int `json:"user_id" gorm:"index;not null"`
|
||||
TenantId string `json:"tenant_id" gorm:"type:varchar(64);index"`
|
||||
Name string `json:"name" gorm:"type:varchar(128);not null"`
|
||||
Provider string `json:"provider" gorm:"type:varchar(32);default:'custom'"`
|
||||
RepoURL string `json:"repo_url" gorm:"type:varchar(512);not null"`
|
||||
Ref string `json:"ref" gorm:"type:varchar(128);default:'main'"`
|
||||
Paths string `json:"paths" gorm:"type:text"`
|
||||
Usage string `json:"usage" gorm:"type:varchar(32);default:'project'"`
|
||||
Status string `json:"status" gorm:"type:varchar(32);default:'active'"`
|
||||
CreatedAt int64 `json:"created_at" gorm:"autoCreateTime;column:created_at"`
|
||||
UpdatedAt int64 `json:"updated_at" gorm:"autoUpdateTime;column:updated_at"`
|
||||
}
|
||||
@@ -280,6 +280,7 @@ func migrateDB() error {
|
||||
&SubscriptionPreConsumeRecord{},
|
||||
&CustomOAuthProvider{},
|
||||
&UserOAuthBinding{},
|
||||
&GitSource{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -328,6 +329,7 @@ func migrateDBFast() error {
|
||||
{&SubscriptionPreConsumeRecord{}, "SubscriptionPreConsumeRecord"},
|
||||
{&CustomOAuthProvider{}, "CustomOAuthProvider"},
|
||||
{&UserOAuthBinding{}, "UserOAuthBinding"},
|
||||
{&GitSource{}, "GitSource"},
|
||||
}
|
||||
// 动态计算migration数量,确保errChan缓冲区足够大
|
||||
errChan := make(chan error, len(migrations))
|
||||
|
||||
@@ -174,6 +174,16 @@ func SetApiRouter(router *gin.Engine) {
|
||||
apiRouter.GET("/subscription/epay/notify", controller.SubscriptionEpayNotify)
|
||||
apiRouter.GET("/subscription/epay/return", controller.SubscriptionEpayReturn)
|
||||
apiRouter.POST("/subscription/epay/return", controller.SubscriptionEpayReturn)
|
||||
|
||||
gitSourceRoute := apiRouter.Group("/git-sources")
|
||||
gitSourceRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
gitSourceRoute.GET("/", controller.ListGitSources)
|
||||
gitSourceRoute.POST("/", controller.CreateGitSource)
|
||||
gitSourceRoute.PUT("/:id", controller.UpdateGitSource)
|
||||
gitSourceRoute.DELETE("/:id", controller.DeleteGitSource)
|
||||
}
|
||||
|
||||
optionRoute := apiRouter.Group("/option")
|
||||
optionRoute.Use(middleware.RootAuth())
|
||||
{
|
||||
|
||||
Vendored
+1
@@ -94,6 +94,7 @@
|
||||
"@tanstack/react-router-devtools": "^1.166.13",
|
||||
"@tanstack/router-plugin": "^1.167.23",
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
|
||||
+71
-1
@@ -96,6 +96,33 @@ export type AgnetDeployment = {
|
||||
|
||||
type ApiEnvelope<T> = { success: boolean; data?: T; message?: string }
|
||||
|
||||
export type GitSourceUsage = 'project' | 'sk' | 'combined'
|
||||
|
||||
export type GitSource = {
|
||||
id: number
|
||||
user_id: number
|
||||
tenant_id?: string
|
||||
name: string
|
||||
provider: string
|
||||
repo_url: string
|
||||
ref: string
|
||||
paths: string[]
|
||||
usage: GitSourceUsage | string
|
||||
status: string
|
||||
created_at?: number
|
||||
updated_at?: number
|
||||
}
|
||||
|
||||
export type GitSourcePayload = {
|
||||
tenant_id?: string
|
||||
name: string
|
||||
provider: string
|
||||
repo_url: string
|
||||
ref: string
|
||||
paths: string[]
|
||||
usage: GitSourceUsage
|
||||
}
|
||||
|
||||
export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
|
||||
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
||||
'/api/agnet/deployments'
|
||||
@@ -103,6 +130,18 @@ export async function listAgnetDeployments(): Promise<AgnetDeployment[]> {
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function listAgnetDeploymentsQuiet(): Promise<AgnetDeployment[]> {
|
||||
const res = await api.get<ApiEnvelope<{ items?: AgnetDeployment[] }>>(
|
||||
'/api/agnet/deployments',
|
||||
{
|
||||
skipBusinessError: true,
|
||||
skipErrorHandler: true,
|
||||
} as Record<string, unknown>
|
||||
)
|
||||
if (!res.data?.success) return []
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function createAgnetDeployment(
|
||||
body: AgnetCreateDeploymentBody
|
||||
): Promise<AgnetCreateDeploymentResult> {
|
||||
@@ -137,7 +176,38 @@ export async function getAgnetAuditLogs() {
|
||||
|
||||
export async function getAgnetSnapshots(deploymentId: string) {
|
||||
const res = await api.get<ApiEnvelope<{ items?: Array<Record<string, unknown>> }>>(
|
||||
`/api/agnet/deployments/${deploymentId}/sk-snapshots`
|
||||
`/api/agnet/deployments/${deploymentId}/sk-snapshots`,
|
||||
{
|
||||
skipBusinessError: true,
|
||||
skipErrorHandler: true,
|
||||
} as Record<string, unknown>
|
||||
)
|
||||
if (!res.data?.success) return []
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function listGitSources(): Promise<GitSource[]> {
|
||||
const res = await api.get<ApiEnvelope<{ items?: GitSource[] }>>(
|
||||
'/api/git-sources/'
|
||||
)
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
export async function createGitSource(
|
||||
body: GitSourcePayload
|
||||
): Promise<GitSource> {
|
||||
const res = await api.post<ApiEnvelope<GitSource>>('/api/git-sources/', body)
|
||||
if (!res.data?.success || !res.data.data) {
|
||||
throw new Error(res.data?.message || 'Create Git source failed')
|
||||
}
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function deleteGitSource(id: number): Promise<void> {
|
||||
const res = await api.delete<ApiEnvelope<{ deleted?: boolean }>>(
|
||||
`/api/git-sources/${id}`
|
||||
)
|
||||
if (!res.data?.success) {
|
||||
throw new Error(res.data?.message || 'Delete Git source failed')
|
||||
}
|
||||
}
|
||||
|
||||
+261
-2
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import {
|
||||
Activity,
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
Search,
|
||||
ShieldCheck,
|
||||
Tag,
|
||||
Trash2,
|
||||
User2,
|
||||
XCircle,
|
||||
} from 'lucide-react'
|
||||
@@ -41,10 +42,16 @@ import {
|
||||
getAgnetAuditLogs,
|
||||
getAgnetDeploymentEvents,
|
||||
getAgnetSnapshots,
|
||||
createGitSource,
|
||||
deleteGitSource,
|
||||
listGitSources,
|
||||
listAgnetDeployments,
|
||||
listAgnetDeploymentsQuiet,
|
||||
type AgnetDeployment,
|
||||
type AgnetRuntimeExecution,
|
||||
type AgnetSKAccessPolicy,
|
||||
type GitSourcePayload,
|
||||
type GitSourceUsage,
|
||||
} from './api'
|
||||
import { CreateAgnetDeploymentSheet } from './create-agnet-deployment-sheet'
|
||||
|
||||
@@ -642,9 +649,58 @@ export function AgnetAuditPage() {
|
||||
|
||||
export function AgnetSKSourcesPage() {
|
||||
const { t } = useTranslation()
|
||||
const queryClient = useQueryClient()
|
||||
const [gitForm, setGitForm] = useState<GitSourcePayload>({
|
||||
name: '',
|
||||
provider: 'github',
|
||||
repo_url: '',
|
||||
ref: 'main',
|
||||
paths: ['.'],
|
||||
usage: 'project',
|
||||
tenant_id: '',
|
||||
})
|
||||
const [pathsText, setPathsText] = useState('.')
|
||||
|
||||
const gitSourcesQuery = useQuery({
|
||||
queryKey: ['git-sources'],
|
||||
queryFn: listGitSources,
|
||||
})
|
||||
const gitSources = gitSourcesQuery.data ?? []
|
||||
|
||||
const createGitMutation = useMutation({
|
||||
mutationFn: () =>
|
||||
createGitSource({
|
||||
...gitForm,
|
||||
paths: pathsText
|
||||
.split('\n')
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean),
|
||||
}),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['git-sources'] })
|
||||
setGitForm({
|
||||
name: '',
|
||||
provider: 'github',
|
||||
repo_url: '',
|
||||
ref: 'main',
|
||||
paths: ['.'],
|
||||
usage: 'project',
|
||||
tenant_id: '',
|
||||
})
|
||||
setPathsText('.')
|
||||
},
|
||||
})
|
||||
|
||||
const deleteGitMutation = useMutation({
|
||||
mutationFn: deleteGitSource,
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ['git-sources'] })
|
||||
},
|
||||
})
|
||||
|
||||
const deploymentsQuery = useQuery({
|
||||
queryKey: ['agnet', 'deployments'],
|
||||
queryFn: listAgnetDeployments,
|
||||
queryFn: listAgnetDeploymentsQuiet,
|
||||
})
|
||||
const deployments = deploymentsQuery.data ?? []
|
||||
const [activeDeployment, setActiveDeployment] = useState<string | undefined>(
|
||||
@@ -684,6 +740,209 @@ export function AgnetSKSourcesPage() {
|
||||
</Select>
|
||||
}
|
||||
>
|
||||
<div className='grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(360px,0.8fr)]'>
|
||||
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4'>
|
||||
<div className='flex items-center justify-between gap-3'>
|
||||
<div>
|
||||
<p className='text-sm font-medium text-foreground'>
|
||||
{t('Bind Git source')}
|
||||
</p>
|
||||
<p className='mt-1 text-xs text-muted-foreground'>
|
||||
{t('Bind Git source description')}
|
||||
</p>
|
||||
</div>
|
||||
<GitBranch className='h-5 w-5 text-primary' />
|
||||
</div>
|
||||
|
||||
<div className='mt-4 grid gap-3'>
|
||||
<div className='grid gap-2 sm:grid-cols-2'>
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
{t('Source name')}
|
||||
</label>
|
||||
<Input
|
||||
value={gitForm.name}
|
||||
onChange={(e) =>
|
||||
setGitForm((v) => ({ ...v, name: e.target.value }))
|
||||
}
|
||||
placeholder='project-main'
|
||||
className='mt-1 h-9 text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
{t('Provider')}
|
||||
</label>
|
||||
<Select
|
||||
value={gitForm.provider}
|
||||
onValueChange={(provider) =>
|
||||
setGitForm((v) => ({ ...v, provider }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='mt-1 h-9 text-xs'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='github'>GitHub</SelectItem>
|
||||
<SelectItem value='gitlab'>GitLab</SelectItem>
|
||||
<SelectItem value='gitea'>Gitea</SelectItem>
|
||||
<SelectItem value='gitee'>Gitee</SelectItem>
|
||||
<SelectItem value='custom'>Custom Git</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
repo_url
|
||||
</label>
|
||||
<Input
|
||||
value={gitForm.repo_url}
|
||||
onChange={(e) =>
|
||||
setGitForm((v) => ({ ...v, repo_url: e.target.value }))
|
||||
}
|
||||
placeholder='https://github.com/org/repo.git'
|
||||
className='mt-1 h-9 font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='grid gap-2 sm:grid-cols-3'>
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
ref
|
||||
</label>
|
||||
<Input
|
||||
value={gitForm.ref}
|
||||
onChange={(e) =>
|
||||
setGitForm((v) => ({ ...v, ref: e.target.value }))
|
||||
}
|
||||
className='mt-1 h-9 font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
{t('Usage')}
|
||||
</label>
|
||||
<Select
|
||||
value={gitForm.usage}
|
||||
onValueChange={(usage) =>
|
||||
setGitForm((v) => ({
|
||||
...v,
|
||||
usage: usage as GitSourceUsage,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className='mt-1 h-9 text-xs'>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value='project'>{t('Project repository')}</SelectItem>
|
||||
<SelectItem value='sk'>{t('SK repository')}</SelectItem>
|
||||
<SelectItem value='combined'>{t('Combined repository')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
tenant_id
|
||||
</label>
|
||||
<Input
|
||||
value={gitForm.tenant_id}
|
||||
onChange={(e) =>
|
||||
setGitForm((v) => ({ ...v, tenant_id: e.target.value }))
|
||||
}
|
||||
placeholder='optional'
|
||||
className='mt-1 h-9 font-mono text-xs'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className='text-xs font-medium text-muted-foreground'>
|
||||
{t('Allowed paths')}
|
||||
</label>
|
||||
<textarea
|
||||
value={pathsText}
|
||||
onChange={(e) => setPathsText(e.target.value)}
|
||||
rows={3}
|
||||
spellCheck={false}
|
||||
className='mt-1 w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-xs shadow-sm outline-none focus-visible:ring-1 focus-visible:ring-ring'
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type='button'
|
||||
className='w-fit gap-1.5'
|
||||
disabled={
|
||||
createGitMutation.isPending ||
|
||||
!gitForm.name.trim() ||
|
||||
!gitForm.repo_url.trim()
|
||||
}
|
||||
onClick={() => createGitMutation.mutate()}
|
||||
>
|
||||
<Plus className='h-3.5 w-3.5' />
|
||||
{t('Bind source')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4'>
|
||||
<p className='text-sm font-medium text-foreground'>
|
||||
{t('Bound Git sources')}
|
||||
</p>
|
||||
{gitSourcesQuery.isLoading ? (
|
||||
<div className='mt-3 space-y-2'>
|
||||
<Skeleton className='h-16 rounded-xl' />
|
||||
<Skeleton className='h-16 rounded-xl' />
|
||||
</div>
|
||||
) : gitSources.length === 0 ? (
|
||||
<p className='mt-3 text-xs text-muted-foreground'>
|
||||
{t('No Git sources bound yet')}
|
||||
</p>
|
||||
) : (
|
||||
<ul className='mt-3 space-y-2'>
|
||||
{gitSources.map((src) => (
|
||||
<li
|
||||
key={src.id}
|
||||
className='rounded-xl border border-border bg-background/60 p-3'
|
||||
>
|
||||
<div className='flex items-start justify-between gap-3'>
|
||||
<div className='min-w-0'>
|
||||
<p className='truncate text-sm font-medium'>
|
||||
{src.name}
|
||||
</p>
|
||||
<p className='mt-1 truncate font-mono text-[11px] text-muted-foreground'>
|
||||
{src.repo_url}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type='button'
|
||||
variant='ghost'
|
||||
size='icon'
|
||||
className='h-8 w-8 shrink-0'
|
||||
disabled={deleteGitMutation.isPending}
|
||||
onClick={() => deleteGitMutation.mutate(src.id)}
|
||||
>
|
||||
<Trash2 className='h-3.5 w-3.5 text-destructive' />
|
||||
</Button>
|
||||
</div>
|
||||
<div className='mt-2 flex flex-wrap gap-1.5'>
|
||||
<MetaPill icon={GitBranch} label='ref' value={src.ref} />
|
||||
<MetaPill icon={Tag} label='usage' value={src.usage} />
|
||||
<MetaPill
|
||||
icon={FileSearch}
|
||||
label='paths'
|
||||
value={src.paths.join(', ')}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_50%,transparent)] p-4 text-sm leading-relaxed text-muted-foreground'>
|
||||
<p className='font-medium text-foreground'>{t('Git binding')}</p>
|
||||
<p className='mt-2'>{t('Git sources binding explainer')}</p>
|
||||
|
||||
+12
@@ -1638,6 +1638,12 @@
|
||||
"Generic cache": "Generic cache",
|
||||
"Get notified when balance falls below this value": "Get notified when balance falls below this value",
|
||||
"Get Started": "Get Started",
|
||||
"Allowed paths": "Allowed paths",
|
||||
"Bind Git source": "Bind Git source",
|
||||
"Bind Git source description": "Manually register a project repo, SK repo, or combined repo. Deployments will choose refs, paths, and AGENT.md from these sources.",
|
||||
"Bind source": "Bind source",
|
||||
"Bound Git sources": "Bound Git sources",
|
||||
"Combined repository": "Project + SK combined repository",
|
||||
"Git binding": "Git binding",
|
||||
"Git sources": "Git sources",
|
||||
"Git sources binding explainer": "Skill (SK) definitions live in Git. Manager does not edit Markdown here—register code repos, SK tool repos, and refs via the Heicode client or your deployment plan sk_sources. Pass runtime and SK-policy parameters into Agnet when it starts each deployment; effective permissions are stored and enforced on Agnet. Below lists immutable snapshot anchors (Git commit / upload artifact) Agnet resolved for auditing.",
|
||||
@@ -1651,6 +1657,12 @@
|
||||
"Git-backed SK sources": "Git-backed SK sources",
|
||||
"Git-backed SK sources description": "Bind repos and capacity first; immutable snapshots after each deployment show which SK lineage actually ran under tenant policy.",
|
||||
"GitHub": "GitHub",
|
||||
"No Git sources bound yet": "No Git sources bound yet.",
|
||||
"Project repository": "Project repository",
|
||||
"Provider": "Provider",
|
||||
"SK repository": "SK repository",
|
||||
"Source name": "Source name",
|
||||
"Usage": "Usage",
|
||||
"Give the group a recognizable name and optional description.": "Give the group a recognizable name and optional description.",
|
||||
"Give this group a recognizable name.": "Give this group a recognizable name.",
|
||||
"Global configuration and administrative tools.": "Global configuration and administrative tools.",
|
||||
|
||||
+12
@@ -1638,6 +1638,12 @@
|
||||
"Generic cache": "通用缓存",
|
||||
"Get notified when balance falls below this value": "当余额低于此值时接收通知",
|
||||
"Get Started": "开始使用",
|
||||
"Allowed paths": "允许路径",
|
||||
"Bind Git source": "绑定 Git 来源",
|
||||
"Bind Git source description": "手工登记项目仓库、SK 仓库或二合一仓库。部署子 Agent 时会从这些来源选择 ref、路径与 AGENT.md。",
|
||||
"Bind source": "绑定来源",
|
||||
"Bound Git sources": "已绑定 Git 来源",
|
||||
"Combined repository": "项目 + SK 二合一仓库",
|
||||
"Git binding": "Git 绑定",
|
||||
"Git sources": "Git 来源",
|
||||
"Git sources binding explainer": "Skill(SK)定义以 Git 为唯一事实源。Manager 不在此编辑 Markdown:请在 Heicode 客户端或部署计划的 sk_sources 中登记仓库与引用;运行时与 SK 策略等参数在 Agnet 拉起编队/子 Agent 时传入。有效权限与策略落账在 Agnet 侧并由其执行;Manager 仅展示 Agnet 回传的不可变快照锚点(Git commit / 上传制品)供审计。",
|
||||
@@ -1651,6 +1657,12 @@
|
||||
"Git-backed SK sources": "基于 Git 的 SK 来源",
|
||||
"Git-backed SK sources description": "先完成仓库与云上能力绑定;每次部署后的不可变快照反映在该租户策略下实际运行的 SK 血缘。",
|
||||
"GitHub": "GitHub",
|
||||
"No Git sources bound yet": "还没有绑定 Git 来源。",
|
||||
"Project repository": "项目仓库",
|
||||
"Provider": "服务商",
|
||||
"SK repository": "SK 技能仓库",
|
||||
"Source name": "来源名称",
|
||||
"Usage": "用途",
|
||||
"Give the group a recognizable name and optional description.": "为该分组提供一个可识别的名称和可选的描述。",
|
||||
"Give this group a recognizable name.": "为此分组提供一个可识别的名称。",
|
||||
"Global configuration and administrative tools.": "全局配置和管理工具。",
|
||||
|
||||
Reference in New Issue
Block a user