From 16cca5df1c9404db90f7dff61a544032282064b6 Mon Sep 17 00:00:00 2001 From: chenchen Date: Thu, 4 Jun 2026 00:59:03 +0800 Subject: [PATCH] =?UTF-8?q?fix(agent):=20review=20fixes=20=E2=80=94=20succ?= =?UTF-8?q?ess-check,=20template=20name,=20seed=20Once,=20update=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- heicode/controller/agent_template_library.go | 20 ++++--- heicode/controller/agent_template_runtime.go | 10 ++++ heicode/web/default/dist/index.html | 2 +- .../deploy-agent/deploy-agent-page.tsx | 53 ++++++++++++++++--- 4 files changed, 70 insertions(+), 15 deletions(-) diff --git a/heicode/controller/agent_template_library.go b/heicode/controller/agent_template_library.go index 169bbdd..402ddd0 100644 --- a/heicode/controller/agent_template_library.go +++ b/heicode/controller/agent_template_library.go @@ -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 } diff --git a/heicode/controller/agent_template_runtime.go b/heicode/controller/agent_template_runtime.go index fdcf361..3269cb6 100644 --- a/heicode/controller/agent_template_runtime.go +++ b/heicode/controller/agent_template_runtime.go @@ -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, diff --git a/heicode/web/default/dist/index.html b/heicode/web/default/dist/index.html index 4acc00f..d51fe90 100644 --- a/heicode/web/default/dist/index.html +++ b/heicode/web/default/dist/index.html @@ -20,7 +20,7 @@ - +
diff --git a/heicode/web/default/src/features/deploy-agent/deploy-agent-page.tsx b/heicode/web/default/src/features/deploy-agent/deploy-agent-page.tsx index 99095df..d9a6f4f 100644 --- a/heicode/web/default/src/features/deploy-agent/deploy-agent-page.tsx +++ b/heicode/web/default/src/features/deploy-agent/deploy-agent-page.tsx @@ -87,6 +87,24 @@ async function listAgents(): Promise { 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 = { + 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([]) @@ -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() { ) : (
{(agentsQ.data ?? []).map((a) => ( - + t.template_id === a.template_id)?.name ?? + a.template_id + } + /> ))}
)} @@ -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 }) {
- {agent.template_id} + {templateName}