feat(manager): align with product-package docs §10/§11
CORS unblock — add /api/heicode-auth/*proxyPath backend proxy to HEICODE_AUTH_BASE_URL. Frontend defaults to same-origin path so the browser never hits APIM directly. Sidebar — replace backend jargon (Git sources / Deployments / Events / Wallet / Available models / Profile) with the user-facing labels docs §10 mandates: 总览 / 准备清单 / 任务总览 / 审计 / 模型与余额 / 客户端 / 账号安全. /sk-sources rewritten as 4-card preparation wizard with progress meter; full Git form moves into a 高级补充 sheet. Drops JSON editor, permission manifest, snapshots and resource-grant pills. /deployments simplified to 任务总览: objective + status + last update. Drops risk / budget / scope / secret_ref pills and the RunDetailPanel; manifest details only in audit/advanced views. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,68 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HeicodeAuthProxy transparently forwards browser calls to the upstream Heicode
|
||||||
|
// identity service (APIM). The frontend cannot call APIM directly because that
|
||||||
|
// host does not include code.xinghanlab.com in its CORS allow-list, so we
|
||||||
|
// terminate the request same-origin and re-emit it server-side.
|
||||||
|
//
|
||||||
|
// Mounted at /api/heicode-auth/*proxyPath. The trailing path (everything after
|
||||||
|
// /api/heicode-auth/) is appended verbatim to HEICODE_AUTH_BASE_URL. Request
|
||||||
|
// method, query string, body, and the Authorization header are preserved.
|
||||||
|
func HeicodeAuthProxy(c *gin.Context) {
|
||||||
|
tail := strings.TrimPrefix(c.Param("proxyPath"), "/")
|
||||||
|
if tail == "" {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"success": false, "message": "missing upstream path"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL := defaultHeicodeAuthBaseURL()
|
||||||
|
target := baseURL + "/" + tail
|
||||||
|
if raw := c.Request.URL.RawQuery; raw != "" {
|
||||||
|
target += "?" + raw
|
||||||
|
}
|
||||||
|
|
||||||
|
var body io.Reader
|
||||||
|
if c.Request.Body != nil {
|
||||||
|
body = c.Request.Body
|
||||||
|
}
|
||||||
|
req, err := http.NewRequestWithContext(c.Request.Context(), c.Request.Method, target, body)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"success": false, "message": "upstream request build failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward only headers that matter for the upstream call. We intentionally
|
||||||
|
// drop Cookie, Host, Origin, Referer so APIM does not see browser context.
|
||||||
|
if auth := strings.TrimSpace(c.GetHeader("Authorization")); auth != "" {
|
||||||
|
req.Header.Set("Authorization", auth)
|
||||||
|
}
|
||||||
|
if ct := strings.TrimSpace(c.GetHeader("Content-Type")); ct != "" {
|
||||||
|
req.Header.Set("Content-Type", ct)
|
||||||
|
}
|
||||||
|
if accept := strings.TrimSpace(c.GetHeader("Accept")); accept != "" {
|
||||||
|
req.Header.Set("Accept", accept)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 20 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadGateway, gin.H{"success": false, "message": "upstream request failed"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||||
|
c.Writer.Header().Set("Content-Type", ct)
|
||||||
|
}
|
||||||
|
c.Writer.WriteHeader(resp.StatusCode)
|
||||||
|
_, _ = io.Copy(c.Writer, resp.Body)
|
||||||
|
}
|
||||||
@@ -54,6 +54,12 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
// Universal secure verification routes
|
// Universal secure verification routes
|
||||||
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
apiRouter.POST("/verify", middleware.UserAuth(), middleware.CriticalRateLimit(), controller.UniversalVerify)
|
||||||
|
|
||||||
|
// Same-origin proxy for Heicode external identity service (APIM).
|
||||||
|
// Frontend calls /api/heicode-auth/<path>; controller forwards to
|
||||||
|
// ${HEICODE_AUTH_BASE_URL}/<path>. Avoids browser CORS preflight failures
|
||||||
|
// when APIM has not added code.xinghanlab.com to its allow-list.
|
||||||
|
apiRouter.Any("/heicode-auth/*proxyPath", middleware.CriticalRateLimit(), controller.HeicodeAuthProxy)
|
||||||
|
|
||||||
userRoute := apiRouter.Group("/user")
|
userRoute := apiRouter.Group("/user")
|
||||||
{
|
{
|
||||||
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
userRoute.POST("/register", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.Register)
|
||||||
|
|||||||
+380
-425
@@ -390,9 +390,9 @@ export function AgnetDeploymentsPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageSurface
|
<PageSurface
|
||||||
title={t('Work / Runs')}
|
title={t('Task overview')}
|
||||||
subtitle={t(
|
subtitle={t(
|
||||||
'Track every Agnet work run by status, risk, budget, scope and secret_ref coverage.'
|
'Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.'
|
||||||
)}
|
)}
|
||||||
toolbar={
|
toolbar={
|
||||||
<>
|
<>
|
||||||
@@ -441,95 +441,60 @@ export function AgnetDeploymentsPage() {
|
|||||||
hint={t('Adjust filters or trigger a new orchestration plan.')}
|
hint={t('Adjust filters or trigger a new orchestration plan.')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className='space-y-4'>
|
<div className='grid gap-3 sm:grid-cols-2'>
|
||||||
{selectedRun && <RunDetailPanel dep={selectedRun} />}
|
{filtered.map((dep) => {
|
||||||
<div className='grid gap-3 sm:grid-cols-2'>
|
const phase = dep.phase || dep.status
|
||||||
{filtered.map((dep) => {
|
const objective =
|
||||||
const risk = describeRiskLevel(dep)
|
dep.orchestration_plan?.objective ||
|
||||||
const phase = dep.phase || dep.status
|
dep.orchestration_plan?.template_hint ||
|
||||||
const objective =
|
t('No objective')
|
||||||
dep.orchestration_plan?.objective ||
|
const selected = dep.deployment_id === selectedRun?.deployment_id
|
||||||
dep.orchestration_plan?.template_hint ||
|
return (
|
||||||
t('No objective')
|
<article
|
||||||
const selected = dep.deployment_id === selectedRun?.deployment_id
|
key={dep.deployment_id}
|
||||||
return (
|
tabIndex={0}
|
||||||
<article
|
role='button'
|
||||||
key={dep.deployment_id}
|
aria-pressed={selected}
|
||||||
tabIndex={0}
|
onClick={() => setSelectedRunId(dep.deployment_id)}
|
||||||
role='button'
|
onKeyDown={(event) => {
|
||||||
aria-pressed={selected}
|
if (event.key === 'Enter' || event.key === ' ') {
|
||||||
onClick={() => setSelectedRunId(dep.deployment_id)}
|
event.preventDefault()
|
||||||
onKeyDown={(event) => {
|
setSelectedRunId(dep.deployment_id)
|
||||||
if (event.key === 'Enter' || event.key === ' ') {
|
}
|
||||||
event.preventDefault()
|
}}
|
||||||
setSelectedRunId(dep.deployment_id)
|
className={cn(
|
||||||
}
|
'group flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:border-primary/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||||
}}
|
selected
|
||||||
className={cn(
|
? 'border-primary/55'
|
||||||
'group flex cursor-pointer flex-col gap-3 rounded-2xl border bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 transition hover:border-primary/45 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))]'
|
||||||
selected
|
)}
|
||||||
? 'border-primary/55'
|
>
|
||||||
: 'border-[color-mix(in_oklch,var(--primary)_18%,var(--border))]'
|
<header className='flex items-start justify-between gap-2'>
|
||||||
)}
|
<p className='line-clamp-2 text-sm font-medium'>
|
||||||
>
|
{objective}
|
||||||
<header className='flex items-start justify-between gap-2'>
|
</p>
|
||||||
<div className='min-w-0'>
|
<StatusBadge phase={phase} />
|
||||||
<div className='flex items-center gap-2'>
|
</header>
|
||||||
<Rocket className='h-3.5 w-3.5 text-primary' />
|
|
||||||
<span className='font-mono text-xs'>
|
|
||||||
{dep.deployment_id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className='mt-1.5 line-clamp-2 text-sm font-medium'>
|
|
||||||
{objective}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<StatusBadge phase={phase} />
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div className='flex flex-wrap gap-1.5'>
|
<footer className='mt-auto flex items-center justify-between border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-3'>
|
||||||
<MetaPill
|
<span className='text-[11px] text-muted-foreground'>
|
||||||
icon={Tag}
|
{formatRelativeTime(dep.updated_at || dep.created_at)}
|
||||||
label={t('risk')}
|
</span>
|
||||||
value={risk.label}
|
<Button
|
||||||
/>
|
asChild
|
||||||
<MetaPill
|
size='sm'
|
||||||
icon={Coins}
|
variant='ghost'
|
||||||
label={t('budget')}
|
className='gap-1 text-primary'
|
||||||
value={describeBudget(dep)}
|
>
|
||||||
/>
|
<Link to='/events'>
|
||||||
<MetaPill
|
{t('View activity')}
|
||||||
icon={Building2}
|
<ArrowUpRight className='h-3.5 w-3.5' />
|
||||||
label={t('scope')}
|
</Link>
|
||||||
value={describeScope(dep)}
|
</Button>
|
||||||
/>
|
</footer>
|
||||||
<MetaPill
|
</article>
|
||||||
icon={ShieldCheck}
|
)
|
||||||
label='secret_ref'
|
})}
|
||||||
value={describeSecretRefs(dep)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer className='mt-auto flex items-center justify-between border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-3'>
|
|
||||||
<span className='text-[11px] text-muted-foreground'>
|
|
||||||
{formatRelativeTime(dep.updated_at || dep.created_at)}
|
|
||||||
</span>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size='sm'
|
|
||||||
variant='ghost'
|
|
||||||
className='gap-1 text-primary'
|
|
||||||
>
|
|
||||||
<Link to='/events'>
|
|
||||||
{t('Inspect events')}
|
|
||||||
<ArrowUpRight className='h-3.5 w-3.5' />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
</article>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageSurface>
|
</PageSurface>
|
||||||
@@ -831,9 +796,15 @@ export function AgnetAuditPage() {
|
|||||||
// resource-binding slice for Work/Runs.
|
// resource-binding slice for Work/Runs.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
|
// AgnetSKSourcesPage — “准备清单” wizard. Frames the page as a 4-step list
|
||||||
|
// (代码 / 文档 / 云账号 / 推荐摘要) per docs/product-package/10 §"Manager 准备清单"
|
||||||
|
// + /11 §5. Does not expose repo_url / ref / paths / usage / tenant_id as the
|
||||||
|
// main flow — those move into a “手动补充”次级 sheet only opened when the user
|
||||||
|
// clicks “连接代码仓库 → 高级补充”.
|
||||||
export function AgnetSKSourcesPage() {
|
export function AgnetSKSourcesPage() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const [advancedOpen, setAdvancedOpen] = useState(false)
|
||||||
const [gitForm, setGitForm] = useState<GitSourcePayload>({
|
const [gitForm, setGitForm] = useState<GitSourcePayload>({
|
||||||
name: '',
|
name: '',
|
||||||
provider: 'github',
|
provider: 'github',
|
||||||
@@ -882,365 +853,349 @@ export function AgnetSKSourcesPage() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deploymentsQuery = useQuery({
|
const projectSources = gitSources.filter(
|
||||||
queryKey: ['agnet', 'deployments'],
|
(s) => s.usage === 'project' || s.usage === 'combined'
|
||||||
queryFn: listAgnetDeploymentsQuiet,
|
)
|
||||||
})
|
const skSources = gitSources.filter(
|
||||||
const deployments = deploymentsQuery.data ?? []
|
(s) => s.usage === 'sk' || s.usage === 'combined'
|
||||||
const [activeDeployment, setActiveDeployment] = useState<string | undefined>(
|
|
||||||
undefined
|
|
||||||
)
|
)
|
||||||
const effectiveDeployment = activeDeployment ?? deployments[0]?.deployment_id
|
|
||||||
|
|
||||||
const snapshotsQuery = useQuery({
|
const steps = [
|
||||||
queryKey: ['agnet', 'snapshots', effectiveDeployment],
|
{
|
||||||
queryFn: () => getAgnetSnapshots(effectiveDeployment as string),
|
key: 'code',
|
||||||
enabled: Boolean(effectiveDeployment),
|
title: t('Connect code repository'),
|
||||||
})
|
summary:
|
||||||
|
projectSources.length > 0
|
||||||
|
? t('{{n}} repository connected', { n: projectSources.length })
|
||||||
|
: t('Authorize GitHub / GitLab / Gitee / self-hosted Git'),
|
||||||
|
done: projectSources.length > 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'docs',
|
||||||
|
title: t('Connect SK or project docs'),
|
||||||
|
summary:
|
||||||
|
skSources.length > 0
|
||||||
|
? t('{{n}} source connected', { n: skSources.length })
|
||||||
|
: t('Pick an existing SK / docs repository or skip'),
|
||||||
|
done: skSources.length > 0,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'cloud',
|
||||||
|
title: t('Connect cloud account'),
|
||||||
|
summary: t(
|
||||||
|
'Authorize Azure / AWS / GCP. Heicode auto-discovers resources.'
|
||||||
|
),
|
||||||
|
done: false,
|
||||||
|
pendingHint: t('Cloud auto-discovery — coming soon'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'review',
|
||||||
|
title: t('Confirm recommendation summary'),
|
||||||
|
summary: t(
|
||||||
|
'Heicode generates the parameters automatically. You only confirm allowed scope and risk.'
|
||||||
|
),
|
||||||
|
done: false,
|
||||||
|
cta: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
const snapshots = snapshotsQuery.data ?? []
|
const completed = steps.filter((s) => s.done).length
|
||||||
|
const total = steps.length
|
||||||
|
const prereqsDone =
|
||||||
|
projectSources.length > 0 || skSources.length > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageSurface
|
<PageSurface
|
||||||
title={t('Resources')}
|
title={t('Preparation checklist')}
|
||||||
subtitle={t('Resources subtitle')}
|
subtitle={t(
|
||||||
|
'Connect code, docs and cloud resources for the current task, then confirm the recommendation before launching Agnet.'
|
||||||
|
)}
|
||||||
toolbar={
|
toolbar={
|
||||||
<Select
|
<span className='inline-flex items-center gap-1.5 rounded-full border border-[color-mix(in_oklch,var(--primary)_30%,var(--border))] bg-[color-mix(in_oklch,var(--primary)_10%,transparent)] px-3 py-1 text-[11px] font-semibold tracking-[0.12em] text-primary uppercase'>
|
||||||
value={effectiveDeployment ?? ''}
|
<CheckCircle2 className='h-3 w-3' />
|
||||||
onValueChange={setActiveDeployment}
|
{t('{{done}}/{{total}} completed', { done: completed, total })}
|
||||||
disabled={deployments.length === 0}
|
</span>
|
||||||
>
|
|
||||||
<SelectTrigger className='h-9 w-60 rounded-xl text-xs'>
|
|
||||||
<Rocket className='mr-1 h-3.5 w-3.5' />
|
|
||||||
<SelectValue placeholder={t('Select deployment')} />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{deployments.map((dep) => (
|
|
||||||
<SelectItem key={dep.deployment_id} value={dep.deployment_id}>
|
|
||||||
<span className='font-mono text-xs'>{dep.deployment_id}</span>
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className='grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(360px,0.8fr)]'>
|
<ol className='space-y-3'>
|
||||||
<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'>
|
{steps.map((step, idx) => (
|
||||||
<div className='flex items-center justify-between gap-3'>
|
<li
|
||||||
<div>
|
key={step.key}
|
||||||
<p className='text-sm font-medium text-foreground'>
|
className='flex items-start gap-4 rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4'
|
||||||
{t('Bind resource source')}
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold ring-1 ring-inset',
|
||||||
|
step.done
|
||||||
|
? 'bg-emerald-500/15 text-emerald-400 ring-emerald-500/30'
|
||||||
|
: 'bg-[color-mix(in_oklch,var(--primary)_12%,transparent)] text-primary ring-primary/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{step.done ? <CheckCircle2 className='h-3.5 w-3.5' /> : idx + 1}
|
||||||
|
</span>
|
||||||
|
<div className='min-w-0 flex-1'>
|
||||||
|
<p className='text-sm font-semibold text-foreground'>
|
||||||
|
{step.title}
|
||||||
|
{step.optional && (
|
||||||
|
<span className='ms-2 text-[10px] font-medium tracking-[0.12em] text-muted-foreground uppercase'>
|
||||||
|
{t('optional')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className='mt-1 text-xs text-muted-foreground'>
|
<p className='mt-1 text-xs text-muted-foreground'>
|
||||||
{t('Bind resource source description')}
|
{step.summary}
|
||||||
</p>
|
</p>
|
||||||
|
{step.pendingHint && (
|
||||||
|
<p className='mt-1 text-[11px] font-medium tracking-[0.08em] text-amber-400/90 uppercase'>
|
||||||
|
{step.pendingHint}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<GitBranch className='h-5 w-5 text-primary' />
|
{step.key === 'code' || step.key === 'docs' ? (
|
||||||
</div>
|
<Button
|
||||||
|
type='button'
|
||||||
|
variant={step.done ? 'ghost' : 'default'}
|
||||||
|
size='sm'
|
||||||
|
className='shrink-0 rounded-xl'
|
||||||
|
onClick={() => setAdvancedOpen(true)}
|
||||||
|
>
|
||||||
|
{step.done ? t('Manage') : t('Connect')}
|
||||||
|
</Button>
|
||||||
|
) : step.key === 'review' ? (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
size='sm'
|
||||||
|
className='shrink-0 rounded-xl'
|
||||||
|
disabled={!prereqsDone}
|
||||||
|
>
|
||||||
|
{t('Confirm and launch Agnet')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
type='button'
|
||||||
|
size='sm'
|
||||||
|
variant='outline'
|
||||||
|
className='shrink-0 rounded-xl'
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
{t('Coming soon')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
|
||||||
<div className='mt-4 grid gap-3'>
|
<div className='rounded-2xl border border-dashed border-[color-mix(in_oklch,var(--primary)_22%,var(--border))] bg-[color-mix(in_oklch,var(--card)_40%,transparent)] p-4 text-xs leading-relaxed text-muted-foreground'>
|
||||||
<div className='grid gap-2 sm:grid-cols-2'>
|
<p className='font-medium text-foreground'>{t('How this works')}</p>
|
||||||
<div>
|
<p className='mt-2'>
|
||||||
<label className='text-xs font-medium text-muted-foreground'>
|
{t(
|
||||||
{t('Source name')}
|
'Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.'
|
||||||
</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'>
|
|
||||||
{t('Owner scope ref')}
|
|
||||||
</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 resource 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 resource 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('Resource binding')}</p>
|
|
||||||
<p className='mt-2'>{t('Resources binding explainer')}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='mt-4 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('Resources workflow title')}
|
|
||||||
</p>
|
</p>
|
||||||
<ol className='mt-3 list-decimal space-y-2 ps-5 marker:text-muted-foreground'>
|
|
||||||
<li>{t('Resources workflow step 1')}</li>
|
|
||||||
<li>{t('Resources workflow step 2')}</li>
|
|
||||||
<li>{t('Resources workflow step 3')}</li>
|
|
||||||
<li>{t('Resources workflow step 4')}</li>
|
|
||||||
<li>{t('Resources workflow step 5')}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!effectiveDeployment ? (
|
{advancedOpen && (
|
||||||
<EmptySurface
|
<div className='fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm'>
|
||||||
title={t('No deployment for Git sources')}
|
<div className='max-h-[85vh] w-[min(640px,92vw)] overflow-auto rounded-2xl border border-[color-mix(in_oklch,var(--primary)_24%,var(--border))] bg-card p-5 shadow-2xl'>
|
||||||
hint={t('No deployment for Git sources hint')}
|
<header className='flex items-start justify-between gap-3 border-b border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pb-3'>
|
||||||
/>
|
<div>
|
||||||
) : snapshotsQuery.isLoading ? (
|
<p className='text-[11px] font-semibold tracking-[0.16em] text-muted-foreground uppercase'>
|
||||||
<LoadingGrid rows={3} height='h-24' />
|
{t('Advanced — manual entry')}
|
||||||
) : snapshots.length === 0 ? (
|
</p>
|
||||||
<EmptySurface
|
<h3 className='mt-1 text-base font-semibold'>
|
||||||
title={t('No resolved snapshots yet')}
|
{t('Connect a code or SK repository')}
|
||||||
hint={t('No resolved snapshots hint')}
|
</h3>
|
||||||
/>
|
<p className='mt-1 text-xs text-muted-foreground'>
|
||||||
) : (
|
{t(
|
||||||
<>
|
'Only needed when auto-discovery cannot find the source. Heicode will store the binding and never expose plaintext credentials.'
|
||||||
<p className='mb-3 text-[11px] font-semibold uppercase tracking-[0.16em] text-muted-foreground'>
|
|
||||||
{t('Resolved snapshot anchors')}
|
|
||||||
</p>
|
|
||||||
<ul className='space-y-3'>
|
|
||||||
{snapshots.map((entry, idx) => {
|
|
||||||
const e = entry as Record<string, unknown>
|
|
||||||
const sourceType = String(e.source_type || 'unknown')
|
|
||||||
const sourceRef = String(e.source_ref || e.ref || '—')
|
|
||||||
const hash = String(e.hash || e.snapshot_hash || '—')
|
|
||||||
const resolvedAt = String(e.resolved_at || '')
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={idx}
|
|
||||||
className='rounded-2xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4'
|
|
||||||
>
|
|
||||||
<div className='grid gap-3 sm:grid-cols-3'>
|
|
||||||
<div className='flex items-start gap-2'>
|
|
||||||
<div className='inline-flex h-8 w-8 items-center justify-center rounded-lg bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] text-primary'>
|
|
||||||
<Bot className='h-4 w-4' />
|
|
||||||
</div>
|
|
||||||
<div className='min-w-0'>
|
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
|
||||||
{t('source')}
|
|
||||||
</p>
|
|
||||||
<p className='mt-0.5 truncate font-mono text-xs'>
|
|
||||||
{sourceType}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-start gap-2'>
|
|
||||||
<div className='inline-flex h-8 w-8 items-center justify-center rounded-lg bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] text-primary'>
|
|
||||||
<GitBranch className='h-4 w-4' />
|
|
||||||
</div>
|
|
||||||
<div className='min-w-0'>
|
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
|
||||||
{t('reference')}
|
|
||||||
</p>
|
|
||||||
<p className='mt-0.5 truncate font-mono text-xs'>
|
|
||||||
{sourceRef}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex items-start gap-2'>
|
|
||||||
<div className='inline-flex h-8 w-8 items-center justify-center rounded-lg bg-[color-mix(in_oklch,var(--primary)_18%,transparent)] text-primary'>
|
|
||||||
<Hash className='h-4 w-4' />
|
|
||||||
</div>
|
|
||||||
<div className='min-w-0'>
|
|
||||||
<p className='text-[11px] font-semibold uppercase tracking-[0.14em] text-muted-foreground'>
|
|
||||||
{t('hash')}
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className='mt-0.5 truncate font-mono text-xs'
|
|
||||||
title={hash}
|
|
||||||
>
|
|
||||||
{hash.length > 18
|
|
||||||
? `${hash.slice(0, 8)}…${hash.slice(-6)}`
|
|
||||||
: hash}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{resolvedAt && (
|
|
||||||
<p className='mt-3 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-2 text-[11px] text-muted-foreground'>
|
|
||||||
<Calendar className='mr-1 inline h-3 w-3' />
|
|
||||||
{t('resolved')} {formatRelativeTime(resolvedAt)}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</li>
|
</p>
|
||||||
)
|
</div>
|
||||||
})}
|
<Button
|
||||||
</ul>
|
type='button'
|
||||||
</>
|
variant='ghost'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setAdvancedOpen(false)}
|
||||||
|
>
|
||||||
|
{t('Close')}
|
||||||
|
</Button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<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'>
|
||||||
|
{t('Repository 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-2'>
|
||||||
|
<div>
|
||||||
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
|
{t('Branch')}
|
||||||
|
</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>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className='text-xs font-medium text-muted-foreground'>
|
||||||
|
{t('Allowed paths')}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
value={pathsText}
|
||||||
|
onChange={(e) => setPathsText(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
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 className='mt-5 border-t border-dashed border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] pt-4'>
|
||||||
|
<p className='text-sm font-medium'>
|
||||||
|
{t('Connected sources')}
|
||||||
|
</p>
|
||||||
|
{gitSourcesQuery.isLoading ? (
|
||||||
|
<div className='mt-2 space-y-2'>
|
||||||
|
<Skeleton className='h-12 rounded-lg' />
|
||||||
|
<Skeleton className='h-12 rounded-lg' />
|
||||||
|
</div>
|
||||||
|
) : gitSources.length === 0 ? (
|
||||||
|
<p className='mt-2 text-xs text-muted-foreground'>
|
||||||
|
{t('No sources connected yet.')}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<ul className='mt-2 space-y-2'>
|
||||||
|
{gitSources.map((src) => (
|
||||||
|
<li
|
||||||
|
key={src.id}
|
||||||
|
className='flex items-start justify-between gap-3 rounded-lg border border-border bg-background/60 p-3'
|
||||||
|
>
|
||||||
|
<div className='min-w-0'>
|
||||||
|
<p className='truncate text-sm font-medium'>
|
||||||
|
{src.name}
|
||||||
|
</p>
|
||||||
|
<p className='mt-0.5 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>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</PageSurface>
|
</PageSurface>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Templates / Agents (kept for backward compatibility — invoked by side routes)
|
// Templates / Agents (kept for backward compatibility — invoked by side routes)
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
+3
-1
@@ -13,9 +13,11 @@ import type {
|
|||||||
ApiResponse,
|
ApiResponse,
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
|
// Default to same-origin proxy so the browser never hits APIM directly (CORS).
|
||||||
|
// Backend route: /api/heicode-auth/* → ${HEICODE_AUTH_BASE_URL}/*
|
||||||
const AUTH_BASE_URL = (
|
const AUTH_BASE_URL = (
|
||||||
(import.meta.env.VITE_HEICODE_AUTH_BASE_URL as string | undefined) ||
|
(import.meta.env.VITE_HEICODE_AUTH_BASE_URL as string | undefined) ||
|
||||||
'https://apimtaiji.azure-api.net/api/mcp'
|
'/api/heicode-auth'
|
||||||
).trim()
|
).trim()
|
||||||
|
|
||||||
const ACCESS_TOKEN_KEY = 'heicode_access_token'
|
const ACCESS_TOKEN_KEY = 'heicode_access_token'
|
||||||
|
|||||||
+13
-39
@@ -1,9 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
Activity,
|
|
||||||
Command,
|
Command,
|
||||||
Cpu,
|
|
||||||
Download,
|
Download,
|
||||||
FileBarChart,
|
|
||||||
GitBranch,
|
GitBranch,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
Rocket,
|
Rocket,
|
||||||
@@ -19,21 +16,13 @@ import { type SidebarData } from '@/components/layout/types'
|
|||||||
/**
|
/**
|
||||||
* Heicode Manager 默认 workspace 侧边栏。
|
* Heicode Manager 默认 workspace 侧边栏。
|
||||||
*
|
*
|
||||||
* 设计原则:
|
* 文案与结构以 docs/product-package/10-frontend-detail-spec.md §"全局体验结构" 和
|
||||||
* - 平台是管理后台:登录后默认仅展示 Code delivery cockpit + 个人入口;
|
* docs/product-package/11-product-prototype-wireframes.md §4 Manager 原型为准。
|
||||||
* - 不再向普通用户暴露 Playground / API Keys / Models 等开发者控制台条目;
|
|
||||||
* - Usage logs 作为交付链路的一环,归并进 Code delivery 分组;
|
|
||||||
* - Code delivery 菜单顺序:总览 → Git 来源(绑定代码/SK 与云上权限)→ 部署(子 Agent)→ 事件 → 审计 → 用量日志;
|
|
||||||
* - Git sources:绑定完成后部署,再在此对照各次部署的快照锚点;SK 正文仍以 Git 为准,不提供 Markdown 编辑;
|
|
||||||
* - 所有 admin/root 管理类入口(Channels/Models/Tenants/Redemption/...)
|
|
||||||
* 收纳进 "系统设置" workspace(点击进入 /system-settings 后才出现)。
|
|
||||||
*
|
*
|
||||||
* Visibility rules (filter happens in `app-sidebar.tsx` by `group.id`):
|
* - 不使用"Git 来源 / Deployments / Events"等后台术语,统一替换为"准备清单 / 任务总览 / 审计"等用户语义。
|
||||||
* - everyone (no id filter): cockpit / personal
|
* - 钱包 + 可用模型 合并为"模型与余额"。
|
||||||
* - id === 'admin-entry' -> ROLE.ADMIN+ (单一入口:系统设置)
|
* - 我的资料 改名"账号安全"。
|
||||||
*
|
* - admin/root 管理类入口(Channels/Models/Tenants/...)仍统一归到"系统设置" workspace。
|
||||||
* 注意:进入 /system-settings 之后侧边栏由
|
|
||||||
* `components/layout/config/system-settings.config.ts` 接管。
|
|
||||||
*/
|
*/
|
||||||
export function useSidebarData(): SidebarData {
|
export function useSidebarData(): SidebarData {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@@ -48,10 +37,10 @@ export function useSidebarData(): SidebarData {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
navGroups: [
|
navGroups: [
|
||||||
// ============ Code delivery cockpit ============
|
// ============ Heicode 主流程(按 docs §10 全局体验结构)============
|
||||||
{
|
{
|
||||||
id: 'cockpit',
|
id: 'cockpit',
|
||||||
title: t('Code delivery'),
|
title: t('Heicode'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: t('Overview'),
|
title: t('Overview'),
|
||||||
@@ -59,55 +48,40 @@ export function useSidebarData(): SidebarData {
|
|||||||
icon: LayoutDashboard,
|
icon: LayoutDashboard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Git sources'),
|
title: t('Preparation checklist'),
|
||||||
url: '/sk-sources',
|
url: '/sk-sources',
|
||||||
icon: GitBranch,
|
icon: GitBranch,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Deployments'),
|
title: t('Task overview'),
|
||||||
url: '/deployments',
|
url: '/deployments',
|
||||||
icon: Rocket,
|
icon: Rocket,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('Events'),
|
|
||||||
url: '/events',
|
|
||||||
icon: Activity,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('Audit'),
|
title: t('Audit'),
|
||||||
url: '/audit',
|
url: '/audit',
|
||||||
icon: ShieldCheck,
|
icon: ShieldCheck,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('Usage logs'),
|
|
||||||
url: '/usage-logs',
|
|
||||||
icon: FileBarChart,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// ============ Personal ============
|
// ============ 辅助入口 ============
|
||||||
{
|
{
|
||||||
id: 'personal',
|
id: 'personal',
|
||||||
title: t('Personal'),
|
title: t('Personal'),
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: t('Wallet'),
|
title: t('Models and balance'),
|
||||||
url: '/wallet',
|
url: '/wallet',
|
||||||
icon: Wallet,
|
icon: Wallet,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: t('Available models'),
|
|
||||||
url: '/available-models',
|
|
||||||
icon: Cpu,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: t('Heicode desktop client'),
|
title: t('Heicode desktop client'),
|
||||||
url: '/desktop-client',
|
url: '/desktop-client',
|
||||||
icon: Download,
|
icon: Download,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t('Profile'),
|
title: t('Account security'),
|
||||||
url: '/profile',
|
url: '/profile',
|
||||||
icon: UserCog,
|
icon: UserCog,
|
||||||
},
|
},
|
||||||
|
|||||||
+37
-3
@@ -1,4 +1,36 @@
|
|||||||
{
|
{
|
||||||
|
"Account security": "Account security",
|
||||||
|
"Advanced — manual entry": "Advanced — manual entry",
|
||||||
|
"Authorize Azure / AWS / GCP. Heicode auto-discovers resources.": "Authorize Azure / AWS / GCP. Heicode auto-discovers resources.",
|
||||||
|
"Authorize GitHub / GitLab / Gitee / self-hosted Git": "Authorize GitHub / GitLab / Gitee / self-hosted Git",
|
||||||
|
"Branch": "Branch",
|
||||||
|
"Close": "Close",
|
||||||
|
"Cloud auto-discovery — coming soon": "Cloud auto-discovery — coming soon",
|
||||||
|
"Coming soon": "Coming soon",
|
||||||
|
"Confirm and launch Agnet": "Confirm and launch Agnet",
|
||||||
|
"Confirm recommendation summary": "Confirm recommendation summary",
|
||||||
|
"Connect": "Connect",
|
||||||
|
"Connect a code or SK repository": "Connect a code or SK repository",
|
||||||
|
"Connect cloud account": "Connect cloud account",
|
||||||
|
"Connect code repository": "Connect code repository",
|
||||||
|
"Connect SK or project docs": "Connect SK or project docs",
|
||||||
|
"Connected sources": "Connected sources",
|
||||||
|
"Heicode": "Heicode",
|
||||||
|
"Heicode generates the parameters automatically. You only confirm allowed scope and risk.": "Heicode generates the parameters automatically. You only confirm allowed scope and risk.",
|
||||||
|
"How this works": "How this works",
|
||||||
|
"Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.",
|
||||||
|
"Manage": "Manage",
|
||||||
|
"Models and balance": "Models and balance",
|
||||||
|
"No sources connected yet.": "No sources connected yet.",
|
||||||
|
"No tasks yet": "No tasks yet",
|
||||||
|
"Only needed when auto-discovery cannot find the source. Heicode will store the binding and never expose plaintext credentials.": "Only needed when auto-discovery cannot find the source. Heicode will store the binding and never expose plaintext credentials.",
|
||||||
|
"optional": "optional",
|
||||||
|
"Pick an existing SK / docs repository or skip": "Pick an existing SK / docs repository or skip",
|
||||||
|
"Preparation checklist": "Preparation checklist",
|
||||||
|
"Repository URL": "Repository URL",
|
||||||
|
"Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.": "Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.",
|
||||||
|
"Task overview": "Task overview",
|
||||||
|
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.",
|
||||||
"translation": {
|
"translation": {
|
||||||
"360": "360",
|
"360": "360",
|
||||||
"1000": "1000",
|
"1000": "1000",
|
||||||
@@ -2710,7 +2742,6 @@
|
|||||||
"Provide Markdown, HTML, or an external URL for the user agreement": "Provide Markdown, HTML, or an external URL for the user agreement",
|
"Provide Markdown, HTML, or an external URL for the user agreement": "Provide Markdown, HTML, or an external URL for the user agreement",
|
||||||
"Provide per-category safety overrides as JSON. Use `default` for fallback values.": "Provide per-category safety overrides as JSON. Use `default` for fallback values.",
|
"Provide per-category safety overrides as JSON. Use `default` for fallback values.": "Provide per-category safety overrides as JSON. Use `default` for fallback values.",
|
||||||
"Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.": "Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.",
|
"Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.": "Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.",
|
||||||
"Provider": "Provider",
|
|
||||||
"Provider created successfully": "Provider created successfully",
|
"Provider created successfully": "Provider created successfully",
|
||||||
"Provider deleted successfully": "Provider deleted successfully",
|
"Provider deleted successfully": "Provider deleted successfully",
|
||||||
"Provider Name": "Provider Name",
|
"Provider Name": "Provider Name",
|
||||||
@@ -3633,7 +3664,6 @@
|
|||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"URL is required": "URL is required",
|
"URL is required": "URL is required",
|
||||||
"URL to your logo image (optional)": "URL to your logo image (optional)",
|
"URL to your logo image (optional)": "URL to your logo image (optional)",
|
||||||
"Usage": "Usage",
|
|
||||||
"Usage logs": "Usage logs",
|
"Usage logs": "Usage logs",
|
||||||
"Usage Logs": "Usage Logs",
|
"Usage Logs": "Usage Logs",
|
||||||
"Usage mode": "Usage mode",
|
"Usage mode": "Usage mode",
|
||||||
@@ -3918,5 +3948,9 @@
|
|||||||
"Resources workflow step 3": "Allocate scope, allowed paths/actions, runtime policy, and budget to the run manifest.",
|
"Resources workflow step 3": "Allocate scope, allowed paths/actions, runtime policy, and budget to the run manifest.",
|
||||||
"Resources workflow step 4": "Start a Work/Run; Agnet resolves immutable anchors and enforces the effective grants.",
|
"Resources workflow step 4": "Start a Work/Run; Agnet resolves immutable anchors and enforces the effective grants.",
|
||||||
"Resources workflow step 5": "Use snapshots, events, and audit together to replay which resource context actually ran."
|
"Resources workflow step 5": "Use snapshots, events, and audit together to replay which resource context actually ran."
|
||||||
}
|
},
|
||||||
|
"View activity": "View activity",
|
||||||
|
"{{done}}/{{total}} completed": "{{done}}/{{total}} completed",
|
||||||
|
"{{n}} repository connected": "{{n}} repository connected",
|
||||||
|
"{{n}} source connected": "{{n}} source connected"
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-5
@@ -1,4 +1,36 @@
|
|||||||
{
|
{
|
||||||
|
"Account security": "账号安全",
|
||||||
|
"Advanced — manual entry": "高级 · 手动补充",
|
||||||
|
"Authorize Azure / AWS / GCP. Heicode auto-discovers resources.": "授权 Azure / AWS / GCP,Heicode 自动发现资源",
|
||||||
|
"Authorize GitHub / GitLab / Gitee / self-hosted Git": "授权 GitHub / GitLab / Gitee / 自建 Git",
|
||||||
|
"Branch": "分支",
|
||||||
|
"Close": "关闭",
|
||||||
|
"Cloud auto-discovery — coming soon": "云资源自动发现 · 即将上线",
|
||||||
|
"Coming soon": "即将上线",
|
||||||
|
"Confirm and launch Agnet": "确认并启动 Agnet",
|
||||||
|
"Confirm recommendation summary": "确认推荐摘要",
|
||||||
|
"Connect": "连接",
|
||||||
|
"Connect a code or SK repository": "连接代码 / SK 仓库",
|
||||||
|
"Connect cloud account": "连接云账号",
|
||||||
|
"Connect code repository": "连接代码仓库",
|
||||||
|
"Connect SK or project docs": "连接 SK / 项目文档",
|
||||||
|
"Connected sources": "已连接的来源",
|
||||||
|
"Heicode": "Heicode",
|
||||||
|
"Heicode generates the parameters automatically. You only confirm allowed scope and risk.": "Heicode 自动生成参数,你只需要确认允许范围和风险",
|
||||||
|
"How this works": "这里发生了什么",
|
||||||
|
"Long-lived credentials are stored in the secret vault. Agnet only requests short-lived, scoped credentials at run time. Production deploys and other high-risk actions are approved from the desktop client.": "长期凭证保存在密钥保管器。Agnet 执行时只申请短期、最小权限凭证。生产部署等高危操作需在客户端审批。",
|
||||||
|
"Manage": "管理",
|
||||||
|
"Models and balance": "模型与余额",
|
||||||
|
"No sources connected yet.": "暂未连接任何来源",
|
||||||
|
"No tasks yet": "暂无任务",
|
||||||
|
"Only needed when auto-discovery cannot find the source. Heicode will store the binding and never expose plaintext credentials.": "仅在自动发现无法识别时使用。Heicode 只保存绑定关系,不展示明文凭证。",
|
||||||
|
"optional": "可选",
|
||||||
|
"Pick an existing SK / docs repository or skip": "选择已有 SK / 文档仓库,或跳过",
|
||||||
|
"Preparation checklist": "准备清单",
|
||||||
|
"Repository URL": "仓库地址",
|
||||||
|
"Status, latest activity and last update for every Agnet task you launched. Details live in the desktop client.": "查看每个启动的 Agnet 任务的状态、最近动态和更新时间。详细对话与产物在客户端中查看。",
|
||||||
|
"Task overview": "任务总览",
|
||||||
|
"Tasks appear here after you confirm the recommendation in the desktop client and launch Agnet.": "在客户端确认推荐摘要并启动 Agnet 后,任务会出现在这里。",
|
||||||
"translation": {
|
"translation": {
|
||||||
"360": "360",
|
"360": "360",
|
||||||
"1000": "1000",
|
"1000": "1000",
|
||||||
@@ -1664,10 +1696,10 @@
|
|||||||
"GitHub": "GitHub",
|
"GitHub": "GitHub",
|
||||||
"No Git sources bound yet": "还没有绑定 Git 来源。",
|
"No Git sources bound yet": "还没有绑定 Git 来源。",
|
||||||
"Project repository": "项目仓库",
|
"Project repository": "项目仓库",
|
||||||
"Provider": "服务商",
|
"Provider": "提供商",
|
||||||
"SK repository": "SK 技能仓库",
|
"SK repository": "SK 技能仓库",
|
||||||
"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.": "全局配置和管理工具。",
|
||||||
@@ -2710,7 +2742,6 @@
|
|||||||
"Provide Markdown, HTML, or an external URL for the user agreement": "提供 Markdown、HTML 或外部 URL 作为用户协议",
|
"Provide Markdown, HTML, or an external URL for the user agreement": "提供 Markdown、HTML 或外部 URL 作为用户协议",
|
||||||
"Provide per-category safety overrides as JSON. Use `default` for fallback values.": "以 JSON 格式提供按类别划分的安全覆盖。使用 `default` 作为回退值。",
|
"Provide per-category safety overrides as JSON. Use `default` for fallback values.": "以 JSON 格式提供按类别划分的安全覆盖。使用 `default` 作为回退值。",
|
||||||
"Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.": "以 JSON 格式提供按模型划分的标头覆盖。可用于启用测试功能,例如扩展上下文窗口。",
|
"Provide per-model header overrides as JSON. Useful for enabling beta features such as expanded context windows.": "以 JSON 格式提供按模型划分的标头覆盖。可用于启用测试功能,例如扩展上下文窗口。",
|
||||||
"Provider": "提供商",
|
|
||||||
"Provider created successfully": "提供商创建成功",
|
"Provider created successfully": "提供商创建成功",
|
||||||
"Provider deleted successfully": "提供商删除成功",
|
"Provider deleted successfully": "提供商删除成功",
|
||||||
"Provider Name": "提供商名称",
|
"Provider Name": "提供商名称",
|
||||||
@@ -3633,7 +3664,6 @@
|
|||||||
"URL": "URL",
|
"URL": "URL",
|
||||||
"URL is required": "URL 为必填项",
|
"URL is required": "URL 为必填项",
|
||||||
"URL to your logo image (optional)": "您的徽标图片 URL(可选)",
|
"URL to your logo image (optional)": "您的徽标图片 URL(可选)",
|
||||||
"Usage": "用量",
|
|
||||||
"Usage logs": "使用日志",
|
"Usage logs": "使用日志",
|
||||||
"Usage Logs": "使用日志",
|
"Usage Logs": "使用日志",
|
||||||
"Usage mode": "使用模式",
|
"Usage mode": "使用模式",
|
||||||
@@ -3918,5 +3948,9 @@
|
|||||||
"Resources workflow step 3": "为运行 manifest 分配范围、允许路径/动作、运行策略和预算。",
|
"Resources workflow step 3": "为运行 manifest 分配范围、允许路径/动作、运行策略和预算。",
|
||||||
"Resources workflow step 4": "启动 Work/Run;Agnet 解析不可变锚点并执行有效授权。",
|
"Resources workflow step 4": "启动 Work/Run;Agnet 解析不可变锚点并执行有效授权。",
|
||||||
"Resources workflow step 5": "结合快照、事件和审计回放实际运行的资源上下文。"
|
"Resources workflow step 5": "结合快照、事件和审计回放实际运行的资源上下文。"
|
||||||
}
|
},
|
||||||
|
"View activity": "查看动态",
|
||||||
|
"{{done}}/{{total}} completed": "{{done}}/{{total}} 已完成",
|
||||||
|
"{{n}} repository connected": "已连接 {{n}} 个仓库",
|
||||||
|
"{{n}} source connected": "已连接 {{n}} 个来源"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user