fix(agent): review fixes — success-check, template name, seed Once, update guard
Full review of the template-agent code surfaced and fixed: - [frontend, critical] deploy/stop/delete treated HTTP-200-with-success:false as success (backend agentError returns 200 per new-api convention) -> a failed call wrongly toasted success. Added okOrThrow() that inspects the envelope and throws the server (or a friendly Chinese) message so onError fires. - [frontend] "我的 Agent" showed the raw template key (architect) instead of the Chinese name; now resolves name via the templates list. - [backend] ensureAgentTemplatesSeeded consumed sync.Once even when model.DB was nil (would permanently skip seeding) -> DB check moved outside the Once. - [backend] AdminUpdateAgentTemplate could wipe name_zh/definition with empty values -> guard those critical fields. - [security] warn when starting an agent with secret-bearing env over a non-HTTPS AM URL (secrets must not transit the network in clear). Go + frontend build/tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -73,10 +73,12 @@ var seedAgentTemplatesOnce sync.Once
|
||||
|
||||
// ensureAgentTemplatesSeeded idempotently inserts any missing preset templates.
|
||||
func ensureAgentTemplatesSeeded() {
|
||||
// Guard the DB check OUTSIDE the Once so we don't consume it before the DB
|
||||
// is ready (which would permanently skip seeding).
|
||||
if model.DB == nil {
|
||||
return
|
||||
}
|
||||
seedAgentTemplatesOnce.Do(func() {
|
||||
if model.DB == nil {
|
||||
return
|
||||
}
|
||||
entries, err := presetAgentFS.ReadDir("agent_template_presets")
|
||||
if err != nil {
|
||||
common.SysLog("agent template preset read: " + err.Error())
|
||||
@@ -221,14 +223,20 @@ func AdminUpdateAgentTemplate(c *gin.Context) {
|
||||
agentError(c, "POLICY_REJECTED", "invalid request body")
|
||||
return
|
||||
}
|
||||
// Update editable fields only; key/source stay.
|
||||
// Update editable fields only; key/source stay. Guard the critical fields
|
||||
// (name_zh, definition) so an omitted/empty value can't wipe a working
|
||||
// template; description/model/sort_order may be cleared intentionally.
|
||||
updates := map[string]any{
|
||||
"name_zh": req.NameZh,
|
||||
"description_zh": req.DescriptionZh,
|
||||
"model": req.Model,
|
||||
"definition": req.Definition,
|
||||
"sort_order": req.SortOrder,
|
||||
}
|
||||
if strings.TrimSpace(req.NameZh) != "" {
|
||||
updates["name_zh"] = req.NameZh
|
||||
}
|
||||
if strings.TrimSpace(req.Definition) != "" {
|
||||
updates["definition"] = req.Definition
|
||||
}
|
||||
if strings.TrimSpace(req.Status) != "" {
|
||||
updates["status"] = req.Status
|
||||
}
|
||||
|
||||
@@ -127,6 +127,16 @@ func amTemplateDo(ctx context.Context, method, path string, body any, timeout ti
|
||||
// model, env, callback_url}.
|
||||
// Proposed response data: {runtime_id, subdomain, access_token, status}.
|
||||
func amStartTemplateAgent(ctx context.Context, args amStartArgs) (amStartResult, error) {
|
||||
// The env may carry plaintext secrets (vm password, db password, blob key…).
|
||||
// Warn if the AM endpoint is plain http on a non-loopback host so they don't
|
||||
// silently transit the network in the clear — AM should be HTTPS / private.
|
||||
if len(args.Env) > 0 {
|
||||
base := agentRuntimeClientConfigForMode(agentRuntimeModeAgent).BaseURL
|
||||
if strings.HasPrefix(base, "http://") &&
|
||||
!strings.Contains(base, "localhost") && !strings.Contains(base, "127.0.0.1") {
|
||||
common.SysLog("WARNING: starting template agent with secret env over a non-HTTPS AM URL; use HTTPS or a private network")
|
||||
}
|
||||
}
|
||||
payload := map[string]any{
|
||||
"manager_deployment_id": args.ManagerDeploymentID,
|
||||
"template_key": args.TemplateKey,
|
||||
|
||||
Vendored
+1
-1
@@ -20,7 +20,7 @@
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
<meta name="theme-color" content="#7B6BE3" />
|
||||
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.829c7e3fad.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/6374.bc21d8b214.js"></script><script defer src="/static/js/index.99cba94710.js"></script><link href="/static/css/index.cc291c3921.css" rel="stylesheet"></head>
|
||||
<link rel="icon" href="/favicon.ico"><script defer src="/static/js/vendor-radix.829c7e3fad.js"></script><script defer src="/static/js/vendor-tanstack.632dbe8908.js"></script><script defer src="/static/js/lib-react.5c8909c28c.js"></script><script defer src="/static/js/6374.bc21d8b214.js"></script><script defer src="/static/js/index.deee68f4fa.js"></script><link href="/static/css/index.592272f49e.css" rel="stylesheet"></head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -87,6 +87,24 @@ async function listAgents(): Promise<AgentItem[]> {
|
||||
return res.data?.data?.items ?? []
|
||||
}
|
||||
|
||||
// The backend returns HTTP 200 with {success:false, error:{code,message}} for
|
||||
// business failures (new-api convention), so axios resolves — we must check the
|
||||
// envelope ourselves and throw, otherwise a failed call looks like a success.
|
||||
type Envelope = {
|
||||
data?: { success?: boolean; message?: string; error?: { code?: string; message?: string } }
|
||||
}
|
||||
const FRIENDLY: Record<string, string> = {
|
||||
RUNTIME_UNAVAILABLE: '云端服务暂时不可用,请稍后再试',
|
||||
RESOURCE_BINDING_INVALID: '所选资源无效,请重新选择',
|
||||
}
|
||||
function okOrThrow(res: Envelope) {
|
||||
const d = res?.data
|
||||
if (d && d.success === false) {
|
||||
const code = d.error?.code ?? ''
|
||||
throw new Error(FRIENDLY[code] || d.error?.message || d.message || '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
export function DeploySubAgentPage() {
|
||||
const qc = useQueryClient()
|
||||
const [selectedBindings, setSelectedBindings] = useState<number[]>([])
|
||||
@@ -110,11 +128,13 @@ export function DeploySubAgentPage() {
|
||||
})
|
||||
|
||||
const deploy = useMutation({
|
||||
mutationFn: () =>
|
||||
api.post('/api/heicode/agents', {
|
||||
mutationFn: async () => {
|
||||
const res = await api.post('/api/heicode/agents', {
|
||||
template_id: selectedTemplate,
|
||||
binding_ids: selectedBindings,
|
||||
}),
|
||||
})
|
||||
okOrThrow(res)
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('Agent 已开始部署')
|
||||
setSelectedBindings([])
|
||||
@@ -282,7 +302,14 @@ export function DeploySubAgentPage() {
|
||||
) : (
|
||||
<div className='mt-4 space-y-3'>
|
||||
{(agentsQ.data ?? []).map((a) => (
|
||||
<AgentRow key={a.agent_id} agent={a} />
|
||||
<AgentRow
|
||||
key={a.agent_id}
|
||||
agent={a}
|
||||
templateName={
|
||||
templates.find((t) => t.template_id === a.template_id)?.name ??
|
||||
a.template_id
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -291,12 +318,20 @@ export function DeploySubAgentPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function AgentRow({ agent }: { agent: AgentItem }) {
|
||||
function AgentRow({
|
||||
agent,
|
||||
templateName,
|
||||
}: {
|
||||
agent: AgentItem
|
||||
templateName: string
|
||||
}) {
|
||||
const qc = useQueryClient()
|
||||
const st = statusLabel(agent.status)
|
||||
|
||||
const stop = useMutation({
|
||||
mutationFn: () => api.post(`/api/heicode/agents/${agent.agent_id}/stop`),
|
||||
mutationFn: async () => {
|
||||
okOrThrow(await api.post(`/api/heicode/agents/${agent.agent_id}/stop`))
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('已停止')
|
||||
void qc.invalidateQueries({ queryKey: ['heicode-agents'] })
|
||||
@@ -304,7 +339,9 @@ function AgentRow({ agent }: { agent: AgentItem }) {
|
||||
onError: (e) => toast.error(e instanceof Error ? e.message : '操作失败'),
|
||||
})
|
||||
const del = useMutation({
|
||||
mutationFn: () => api.delete(`/api/heicode/agents/${agent.agent_id}`),
|
||||
mutationFn: async () => {
|
||||
okOrThrow(await api.delete(`/api/heicode/agents/${agent.agent_id}`))
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success('已删除')
|
||||
void qc.invalidateQueries({ queryKey: ['heicode-agents'] })
|
||||
@@ -322,7 +359,7 @@ function AgentRow({ agent }: { agent: AgentItem }) {
|
||||
<article className='flex flex-col gap-2 rounded-xl border border-[color-mix(in_oklch,var(--primary)_18%,var(--border))] bg-[color-mix(in_oklch,var(--card)_55%,transparent)] p-4 sm:flex-row sm:items-center sm:justify-between'>
|
||||
<div className='min-w-0'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<span className='truncate text-sm font-medium'>{agent.template_id}</span>
|
||||
<span className='truncate text-sm font-medium'>{templateName}</span>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1 ring-inset',
|
||||
|
||||
Reference in New Issue
Block a user