chore(agent): remove old client sub-task API (phase A of legacy teardown)

The template-agent model replaces the old sub/swarm task orchestration. Removed
the entire client-facing task surface:
- routes: registerHeicodeTaskRoutes (all /heicode/{sub-agile,swarm}/tasks/*) and
  GET /heicode/deployment-targets.
- controllers deleted: heicode_task_create, heicode_client_routes,
  heicode_project_artifacts, heicode_artifact_edits, heicode_cloud_deploy
  (+ the agent_deliverable_secret_test that covered the deleted markdown-project
  parsing).

Build + controller/router/model tests green. Shared backend (AgentDeployment,
agent_runtime_client helpers, admin /api/agent/*) intentionally kept — trimmed in
the next phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 23:29:22 +08:00
co-authored by Claude Opus 4.8
parent 19fd4f3326
commit 3863296f85
7 changed files with 0 additions and 1436 deletions
@@ -1,167 +0,0 @@
package controller
import (
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/require"
)
// P2: deliverable judgment must read structured artifact_type / file signals,
// not just the legacy title/summary/uri string heuristics.
func TestRuntimeArtifactsAreSummaryOnly_StructuredFields(t *testing.T) {
cases := []struct {
name string
artifacts []gin.H
want bool
}{
{
name: "empty list is not summary-only",
artifacts: nil,
want: false,
},
{
// The regression the review flagged: Runtime now emits
// artifact_type=document under runtime://.../artifacts/<task>,
// which the old "/artifacts/summary" uri heuristic missed.
name: "document type with new uri scheme and no files is summary-only",
artifacts: []gin.H{{
"artifact_id": "art_1",
"artifact_type": "document",
"title": "Backend delivery",
"summary": "下面是方案总结",
"uri": "runtime://swm_x/artifacts/backend_1",
}},
want: true,
},
{
name: "code_patch is a real deliverable",
artifacts: []gin.H{{
"artifact_type": "code_patch",
"uri": "runtime://swm_x/artifacts/backend_1",
}},
want: false,
},
{
name: "deployment_manifest is a real deliverable",
artifacts: []gin.H{{"artifact_type": "deployment_manifest"}},
want: false,
},
{
name: "document type but with file-change signal is a deliverable",
artifacts: []gin.H{{
"artifact_type": "document",
"metadata": map[string]any{"files_modified": float64(2)},
}},
want: false,
},
{
name: "mixed set with one real deliverable is not summary-only",
artifacts: []gin.H{
{"artifact_type": "document"},
{"artifact_type": "code_patch"},
},
want: false,
},
{
name: "legacy runtime execution summary marker still detected",
artifacts: []gin.H{{
"artifact_type": "other",
"title": "Runtime execution summary",
}},
want: true,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require.Equal(t, tc.want, runtimeArtifactsAreSummaryOnly(tc.artifacts))
})
}
}
// P6a: Resource CRUD must reject a secret_ref that is not an azkv:// reference.
func TestNormalizeResourcePayload_SecretRefMustBeAzkv(t *testing.T) {
base := func(secretRef string) resourcePayload {
return resourcePayload{Name: "repo", ResourceType: "git", SecretRef: secretRef}
}
_, err := normalizeResourcePayload(base("https://example.com/token"))
require.Error(t, err)
require.Contains(t, err.Error(), "azkv://")
_, err = normalizeResourcePayload(base("sk-live-plaintext-leak"))
require.Error(t, err)
got, err := normalizeResourcePayload(base("azkv://heicode-kv.vault.azure.net/secrets/repo"))
require.NoError(t, err)
require.Equal(t, "azkv://heicode-kv.vault.azure.net/secrets/repo", got.SecretRef)
// Empty secret_ref stays allowed (secret can be set later via UpsertResourceSecret).
_, err = normalizeResourcePayload(base(""))
require.NoError(t, err)
}
// P6b: plaintext-secret detection must scan string VALUES, not only key names.
func TestValueLooksLikeSecretAndPlaintextScan(t *testing.T) {
positives := []string{
"sk-abcdefghij1234567890",
"sk-live-abcdefghijklmnopqrst",
"ghp_abcdefghijklmnopqrstuvwxyz0123",
"AKIAIOSFODNN7EXAMPLE",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.s5h6Qk0c5Qx2hQ",
"-----BEGIN OPENSSH PRIVATE KEY-----\nabc\n-----END",
}
for _, s := range positives {
require.Truef(t, valueLooksLikeSecret(s), "expected secret-like: %q", s)
}
negatives := []string{
"",
"main",
"https://github.com/org/repo.git",
"azkv://heicode-kv.vault.azure.net/secrets/repo",
"a normal sentence with sk in it",
}
for _, s := range negatives {
require.Falsef(t, valueLooksLikeSecret(s), "expected NOT secret-like: %q", s)
}
// Value hidden under an innocuous key must now be caught.
require.True(t, containsPlaintextSecret(map[string]any{"note": "sk-abcdefghij1234567890"}))
require.False(t, containsPlaintextSecret(map[string]any{"repo_url": "https://github.com/o/r.git"}))
}
// P5: a single source of truth for the default model; no placeholder fallback.
func TestDefaultAgentModelID_SingleSourceNoPlaceholder(t *testing.T) {
def := defaultAgentModelID()
require.Equal(t, "gpt-5.4", def)
// Draft builder must use the single default, never agent-model-<role>.
plan := buildAgentDraftAgentPlan("backend", "", nil)
require.Equal(t, def, plan.DefaultModelID)
require.NotContains(t, plan.DefaultModelID, "agent-model-")
// Explicit client model is still honored.
plan = buildAgentDraftAgentPlan("backend", "gpt-5.4-mini", nil)
require.Equal(t, "gpt-5.4-mini", plan.DefaultModelID)
// Every role template resolves to the single default, no claude-* hardcoding.
for _, tpl := range agentRoleTemplates() {
require.Equal(t, def, tpl.DefaultModel, "role %s", tpl.Key)
}
}
func TestParseMarkdownProject_SplitsFiles(t *testing.T) {
md := "intro text\n\n```python\n# app.py\nprint(1)\n```\n\n## 1) package.json\n```json\n{\"name\":\"x\"}\n```\n\n```text\nno name here\n```"
files := parseMarkdownProject(md)
require.Len(t, files, 3)
require.Equal(t, "app.py", files[0].Path)
require.Contains(t, files[0].content, "print(1)")
require.Equal(t, "package.json", files[1].Path)
require.Equal(t, "file1.txt", files[2].Path)
for _, f := range files {
require.NotEmpty(t, f.ContentHash)
require.Greater(t, f.SizeBytes, 0)
require.Equal(t, "file", f.Type)
}
}
@@ -1,181 +0,0 @@
package controller
import (
"crypto/sha256"
"encoding/hex"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// Local-edit revision protocol (unified spec §12.7). Clients upload edits to a
// project artifact; Manager stores them as a new accepted revision and is the
// owner of the baseline. Runtime reads new revisions via Manager, never the
// client's local files directly.
type localEditChange struct {
Op string `json:"op"`
Path string `json:"path"`
BaseContentHash string `json:"base_content_hash"`
ContentHash string `json:"content_hash"`
Content string `json:"content"`
}
type localEditRequest struct {
EditID string `json:"edit_id"`
BaseArtifactID string `json:"base_artifact_id"`
BaseProjectRevision int `json:"base_project_revision"`
BaseRevision int `json:"base_revision"`
BaseContentHash string `json:"base_content_hash"`
ContentHash string `json:"content_hash"`
Path string `json:"path"`
Content string `json:"content"`
ChangeSummary string `json:"change_summary"`
Changes []localEditChange `json:"changes"`
}
func hashString(s string) string {
sum := sha256.Sum256([]byte(s))
return "sha256:" + hex.EncodeToString(sum[:])
}
// HeicodeArtifactLocalEdit stores a client local edit (single file or batch) as
// a new artifact revision, rejecting edits based on a stale baseline.
func HeicodeArtifactLocalEdit(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return
}
var req localEditRequest
if err := c.ShouldBindJSON(&req); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
latest, has := model.LatestAgentArtifactRevision(artifactID)
currentRev := 1
if has {
currentRev = latest.Revision
}
// Conflict: client edited on top of a baseline that is no longer current.
if req.BaseRevision > 0 && req.BaseRevision != currentRev {
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": gin.H{
"code": "ARTIFACT_REVISION_CONFLICT",
"message": "本地产物基于旧版本修改,请先同步最新版本后再提交。",
"retryable": false,
},
"data": gin.H{
"current_project_revision": currentRev,
},
})
return
}
contentHash := strings.TrimSpace(req.ContentHash)
if contentHash == "" {
if len(req.Changes) > 0 {
joined := ""
for _, ch := range req.Changes {
joined += ch.Path + "\n" + ch.Content + "\n"
}
contentHash = hashString(joined)
} else {
contentHash = hashString(req.Content)
}
}
payload, _ := common.Marshal(req)
newRev := currentRev + 1
rev := &model.AgentArtifactRevision{
ArtifactID: artifactID,
DeploymentID: record.DeploymentID,
UserID: record.Plan.UserContext.UserID,
Revision: newRev,
ProjectRevision: newRev,
Source: "client_local_edit",
BaseContentHash: strings.TrimSpace(req.BaseContentHash),
ContentHash: contentHash,
ChangeSummary: strings.TrimSpace(req.ChangeSummary),
CreatedBy: "user",
// A non-conflicting client edit becomes the current baseline immediately
// (Manager owns the baseline). Status transitions: accepted -> applied
// once the runtime consumes it on the next /messages or /execute
// (unified spec §12.7, gap P1-2).
Status: "accepted",
PayloadJSON: string(payload),
CreatedAtMs: time.Now().UnixMilli(),
}
if err := model.InsertAgentArtifactRevision(rev); err != nil {
common.SysLog("HeicodeArtifactLocalEdit: " + err.Error())
agentError(c, "REVISION_PERSIST_FAILED", "failed to store artifact revision")
return
}
notifyRuntimeArtifactEdit(record, rev)
common.ApiSuccess(c, gin.H{
"artifact_id": artifactID,
"revision": newRev,
"project_revision": newRev,
"source": "client_local_edit",
"status": "accepted",
"content_hash": contentHash,
"created_by": "user",
})
}
// HeicodeArtifactRevisions lists the revision history for a project artifact.
func HeicodeArtifactRevisions(c *gin.Context) {
if _, ok := requireAuthenticatedUserAgentDeployment(c); !ok {
return
}
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return
}
rows, err := model.ListAgentArtifactRevisions(artifactID)
if err != nil {
common.SysLog("HeicodeArtifactRevisions: " + err.Error())
agentError(c, "REVISION_QUERY_FAILED", "failed to query revisions")
return
}
current := 1
if latest, ok := model.LatestAgentArtifactRevision(artifactID); ok {
current = latest.Revision
}
common.ApiSuccess(c, gin.H{
"artifact_id": artifactID,
"current_project_revision": current,
"items": rows,
"total": len(rows),
})
}
// notifyRuntimeArtifactEdit best-effort notifies the runtime that a new local
// edit revision is available (unified spec §12.7.4). The runtime reads the
// revision via Manager; here we only emit the audit/notification side.
func notifyRuntimeArtifactEdit(record agentDeploymentRecord, rev *model.AgentArtifactRevision) {
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "artifact.local_edit_received",
SchemaVersion: 1,
UserID: record.Plan.UserContext.UserID,
ChannelID: record.Plan.UserContext.ChannelID,
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agentNow(),
}, "manager_artifact_edit", rev.ArtifactID, "", "ok")
}
-232
View File
@@ -1,232 +0,0 @@
package controller
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// HeicodeListTaskApprovals lists approval gates for a task (unified spec §15).
// Scoped to the authenticated user's deployment, optional ?status= filter
// (e.g. pending). The client polls this to render the approval inbox; decisions
// go through the existing approve/reject endpoints.
func HeicodeListTaskApprovals(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
q := model.DB.Where("deployment_id = ?", record.DeploymentID).
Where("user_id = ?", c.GetInt("id"))
statusFilter := strings.TrimSpace(c.Query("status"))
if statusFilter != "" {
q = q.Where("status = ?", statusFilter)
}
var approvals []model.AgentApprovalRequest
if err := q.Order("created_at desc, id desc").Limit(200).Find(&approvals).Error; err != nil {
common.ApiError(c, err)
return
}
items := make([]agentApprovalResponse, 0, len(approvals))
for i := range approvals {
expireAgentApprovalIfNeeded(&approvals[i])
if statusFilter != "" && approvals[i].Status != statusFilter {
continue
}
items = append(items, agentApprovalToResponse(approvals[i], nil))
}
common.ApiSuccess(c, gin.H{
"task_id": record.DeploymentID,
"items": items,
"total": len(items),
})
}
// runtimeNum extracts a numeric metric from a runtime map by trying several key
// aliases, tolerating float/int/string encodings.
func runtimeNum(m map[string]any, keys ...string) float64 {
for _, k := range keys {
v, ok := m[k]
if !ok || v == nil {
continue
}
switch n := v.(type) {
case float64:
return n
case int:
return float64(n)
case int64:
return float64(n)
case string:
if f, err := strconv.ParseFloat(strings.TrimSpace(n), 64); err == nil {
return f
}
}
}
return 0
}
// artifactSourceRole reads the originating agent role from an artifact's
// metadata, so artifacts can be grouped per agent (gap A). Empty when the
// runtime does not tag artifacts with a source role yet.
func artifactSourceRole(a model.AgentArtifact) string {
if strings.TrimSpace(a.MetadataJSON) == "" {
return ""
}
var meta map[string]any
if err := common.UnmarshalJsonStr(a.MetadataJSON, &meta); err != nil {
return ""
}
for _, k := range []string{"source_agent_role", "agent_role", "role", "source_role"} {
if v, ok := meta[k]; ok {
if s := strings.TrimSpace(fmt.Sprint(v)); s != "" {
return s
}
}
}
return ""
}
// HeicodeTaskWorkflow projects a deployment into the client-facing "workflow"
// shape consumed by the desktop right-hand task panel (unified spec §7.9 /
// §10.2). task_id is the deployment_id. Status is the Manager-judged
// display_status, so the client never has to interpret raw runtime state.
//
// Rich fields (gap A): phases[], per-agent tokens/tools/elapsed_seconds/
// artifact_ids and top-level aggregates are filled from the runtime status
// diagnostics. Values the sub-mode runtime does not yet report (per-agent
// tokens/tools, artifact source role, phase breakdown) surface as 0 / [] until
// agent_management emits them.
func HeicodeTaskWorkflow(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
record = reconcileDeploymentFromRuntime(c.Request.Context(), record)
record = withDisplayStatus(record)
diag := agentRuntimeDiagnosticsForRecord(c.Request.Context(), record)
// Index runtime-reported per-agent rows by role for metric enrichment.
diagAgentsByRole := map[string]map[string]any{}
for _, da := range diag.Agents {
role := strings.ToLower(strings.TrimSpace(firstNonEmpty(
ginString(da, "role"), ginString(da, "agent_role"), ginString(da, "name"))))
if role != "" {
diagAgentsByRole[role] = da
}
}
artifacts, _ := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
artItems := make([]gin.H, 0, len(artifacts))
allArtifactIDs := make([]string, 0, len(artifacts))
artifactIDsByRole := map[string][]string{}
for _, a := range artifacts {
artItems = append(artItems, gin.H{
"artifact_id": a.ArtifactID,
"title": a.Title,
"artifact_type": a.ArtifactType,
"summary": a.Summary,
})
allArtifactIDs = append(allArtifactIDs, a.ArtifactID)
if role := strings.ToLower(artifactSourceRole(a)); role != "" {
artifactIDsByRole[role] = append(artifactIDsByRole[role], a.ArtifactID)
}
}
var totTokens, totTools float64
agents := make([]gin.H, 0, len(record.AgentInstances))
for _, inst := range record.AgentInstances {
role := strings.ToLower(strings.TrimSpace(inst.Role))
da := diagAgentsByRole[role]
tokens := runtimeNum(da, "tokens", "tokens_used", "total_tokens")
tools := runtimeNum(da, "tools", "tool_calls", "tools_used")
elapsed := runtimeNum(da, "elapsed_seconds", "duration_seconds")
totTokens += tokens
totTools += tools
ids := artifactIDsByRole[role]
if ids == nil {
ids = []string{}
}
agents = append(agents, gin.H{
"agent_id": inst.InstanceID,
"name": inst.Role,
"role": inst.Role,
"status": firstNonEmpty(inst.RuntimeState, inst.Phase),
"tokens": tokens,
"tools": tools,
"elapsed_seconds": elapsed,
"artifact_ids": ids,
})
}
// Deployment-level metrics the sub-mode runtime reports (tokens_used,
// elapsed_seconds, total_messages). Per-agent splits override the aggregate
// when the runtime provides them.
metricsTokens := runtimeNum(diag.Metrics, "tokens_used", "total_tokens", "tokens")
if totTokens > 0 {
metricsTokens = totTokens
}
metrics := gin.H{
"tokens_used": metricsTokens,
"tools": totTools,
"elapsed_seconds": runtimeNum(diag.Metrics, "elapsed_seconds", "duration_seconds"),
"total_messages": runtimeNum(diag.Metrics, "total_messages"),
}
// phases[]: pass through a runtime-provided phase breakdown if present,
// otherwise expose the current phase as a single entry so the field is
// always an array (gap A; full breakdown pending runtime support).
phases := mapSliceFromAny(diag.Progress)
if len(phases) == 0 {
roleNames := make([]string, 0, len(record.AgentInstances))
for _, inst := range record.AgentInstances {
roleNames = append(roleNames, inst.Role)
}
curPhase := firstNonEmpty(record.Phase, diag.Phase)
if curPhase != "" {
phases = []gin.H{{
"phase_id": curPhase,
"name": curPhase,
"status": record.DisplayStatus,
"agents": roleNames,
}}
} else {
phases = []gin.H{}
}
}
common.ApiSuccess(c, gin.H{
"workflow_id": record.DeploymentID,
"task_id": record.DeploymentID,
"deployment_id": record.DeploymentID,
"conversation_id": record.Plan.Metadata.CorrelationID,
"mode": heicodeClientMode(record),
"sub_mode": firstNonEmpty(record.SubMode, "agile"),
"title": firstNonEmpty(record.Plan.Objective, record.DeploymentID),
"summary": record.Plan.Objective,
"status": record.DisplayStatus,
"display_status": record.DisplayStatus,
// Three-layer status (unified spec §10.2): Manager judges display_status;
// cloud_* is the control-plane status, runtime_* is the runtime's status.
"cloud_deployment_status": record.Status,
"runtime_execution_status": record.RuntimeState,
"last_synced_at": record.UpdatedAt,
"phase": record.Phase,
"phases": phases,
"agent_count": len(record.AgentInstances),
"agents": agents,
"artifacts": artItems,
"artifact_ids": allArtifactIDs,
"metrics": metrics,
"tokens": metricsTokens,
"tools": totTools,
"elapsed_seconds": runtimeNum(diag.Metrics, "elapsed_seconds", "duration_seconds"),
})
}
-139
View File
@@ -1,139 +0,0 @@
package controller
import (
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// Cloud deployment control plane (unified spec §18). Manager validates,
// gates approval, resolves credentials and audits; a Deploy Worker (not yet
// connected) executes provider provisioning. Until a worker is wired, records
// rest in deployment_requested / waiting_approval with executor=pending_worker.
var cloudDeployTargets = map[string]bool{"azure": true, "aliyun": false, "aws": false}
var cloudDeployEnvironments = map[string]bool{"preview": true, "production": true}
// HeicodeDeploymentTargets lists deployable cloud targets (unified spec §18.1).
func HeicodeDeploymentTargets(c *gin.Context) {
common.ApiSuccess(c, gin.H{
"targets": []gin.H{
{"id": "azure", "name": "Azure", "enabled": true, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true, "regions": []string{"eastasia", "southeastasia", "westus"}},
{"id": "aliyun", "name": "阿里云", "enabled": false, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true},
{"id": "aws", "name": "AWS", "enabled": false, "supports_preview": true, "supports_production": true, "requires_approval": true, "credential_binding_required": true},
},
})
}
type cloudDeployRequest struct {
ArtifactID string `json:"artifact_id"`
ArtifactRevision int `json:"artifact_revision"`
Target string `json:"target"`
Environment string `json:"environment"`
Region string `json:"region"`
DeploymentMode string `json:"deployment_mode"`
ResourceBindingID int `json:"resource_binding_id"`
Options map[string]any `json:"options"`
}
// HeicodeCreateDeployment validates and records a cloud deployment request
// (unified spec §18.3/§18.4). Production deployments enter waiting_approval.
func HeicodeCreateDeployment(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
var req cloudDeployRequest
if err := c.ShouldBindJSON(&req); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
req.Target = strings.ToLower(strings.TrimSpace(req.Target))
req.Environment = strings.ToLower(strings.TrimSpace(req.Environment))
if strings.TrimSpace(req.ArtifactID) == "" {
agentError(c, "POLICY_REJECTED", "artifact_id is required")
return
}
if !cloudDeployTargets[req.Target] {
agentError(c, "DEPLOY_TARGET_DISABLED", "deployment target is not available")
return
}
if !cloudDeployEnvironments[req.Environment] {
agentError(c, "POLICY_REJECTED", "environment must be preview or production")
return
}
// Client must reference a credential binding, never inline secrets (§17.6).
if req.ResourceBindingID > 0 && model.DB != nil {
uid, _ := strconv.Atoi(strings.TrimSpace(record.Plan.UserContext.UserID))
var binding model.ResourceBinding
if err := model.DB.Where("id = ? AND user_id = ?", req.ResourceBindingID, uid).First(&binding).Error; err != nil {
agentError(c, "RESOURCE_BINDING_INVALID", "resource_binding_id not found or not owned by user")
return
}
}
status := "deployment_requested"
if req.Environment == "production" {
status = "waiting_approval"
}
now := time.Now().UnixMilli()
runID := "deploy_" + common.GetUUID()[:12]
payload, _ := common.Marshal(req)
row := &model.AgentCloudDeployment{
DeployRunID: runID,
DeploymentID: record.DeploymentID,
UserID: record.Plan.UserContext.UserID,
ArtifactID: strings.TrimSpace(req.ArtifactID),
ArtifactRevision: req.ArtifactRevision,
Target: req.Target,
Environment: req.Environment,
Region: strings.TrimSpace(req.Region),
ResourceBindingID: req.ResourceBindingID,
Status: status,
PayloadJSON: string(payload),
CreatedAtMs: now,
UpdatedAtMs: now,
}
if err := model.InsertAgentCloudDeployment(row); err != nil {
common.SysLog("HeicodeCreateDeployment: " + err.Error())
agentError(c, "DEPLOY_PERSIST_FAILED", "failed to record deployment request")
return
}
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "deployment.requested",
SchemaVersion: 1,
UserID: record.Plan.UserContext.UserID,
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agentNow(),
}, "manager_cloud_deploy", runID, "", "ok")
common.ApiSuccess(c, gin.H{
"deployment_run_id": runID,
"deployment_id": record.DeploymentID,
"target": req.Target,
"environment": req.Environment,
"status": status,
"executor": "pending_worker",
})
}
// HeicodeListDeployments lists cloud deployments for a task.
func HeicodeListDeployments(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
rows, err := model.ListAgentCloudDeploymentsByDeployment(record.DeploymentID)
if err != nil {
agentError(c, "DEPLOY_QUERY_FAILED", "failed to query deployments")
return
}
common.ApiSuccess(c, gin.H{"items": rows, "total": len(rows)})
}
@@ -1,398 +0,0 @@
package controller
import (
"archive/zip"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// heicodeArtifactCollectionBase returns the request path up to and including the
// `.../artifacts` collection, so per-artifact subpaths (manifest/files/archive)
// can be built relative to however the route is mounted (sub-agile vs swarm).
func heicodeArtifactCollectionBase(c *gin.Context) string {
p := strings.TrimRight(c.Request.URL.Path, "/")
if i := strings.LastIndex(p, "/artifacts"); i >= 0 {
return p[:i] + "/artifacts"
}
return p
}
// heicodeArtifactListItem projects a stored artifact into the client list shape.
// For a real (non-summary) code deliverable it advertises artifact_type
// project_folder plus manifest/files/archive subpaths, so the client never has
// to guess whether a code_patch is a full project (unified spec §12, gap P0-3).
func heicodeArtifactListItem(c *gin.Context, base string, a model.AgentArtifact) gin.H {
item := gin.H{
"artifact_id": a.ArtifactID,
"title": a.Title,
"summary": a.Summary,
"artifact_type": a.ArtifactType,
"uri": a.URI,
"created_at_ms": a.CreatedAtMs,
}
if artifactIsSummaryOnly(persistedArtifactToGin(a)) {
item["is_project"] = false
item["display_artifact_type"] = a.ArtifactType
return item
}
sub := base + "/" + a.ArtifactID
item["is_project"] = true
item["display_artifact_type"] = "project_folder"
item["manifest_path"] = sub + "/manifest"
item["files_path"] = sub + "/files"
item["archive_path"] = sub + "/archive"
item["revisions_path"] = sub + "/revisions"
return item
}
// HeicodeListTaskArtifacts lists a task's artifacts with project_folder
// normalization for the primary code deliverable (unified spec §12.2).
func HeicodeListTaskArtifacts(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
record = reconcileDeploymentFromRuntime(c.Request.Context(), record)
rows, err := model.ListAgentArtifacts(model.ListAgentArtifactsFilter{
DeploymentID: record.DeploymentID,
Limit: 500,
})
if err != nil {
common.SysLog("HeicodeListTaskArtifacts: " + err.Error())
agentError(c, "ARTIFACT_QUERY_FAILED", "failed to query artifacts")
return
}
base := heicodeArtifactCollectionBase(c)
items := make([]gin.H, 0, len(rows))
for _, a := range rows {
items = append(items, heicodeArtifactListItem(c, base, a))
}
common.ApiSuccess(c, gin.H{
"deployment_id": record.DeploymentID,
"task_id": record.DeploymentID,
"items": items,
"total": len(items),
})
}
// Project-folder artifacts (unified spec §12). Runtime currently delivers a
// single text artifact (markdown with fenced code blocks); Manager parses it
// into a project file tree on read and serves manifest / per-file / archive,
// so the client shows a project folder instead of one long text blob.
type projectFileEntry struct {
Path string `json:"path"`
Type string `json:"type"`
MimeType string `json:"mime_type"`
SizeBytes int `json:"size_bytes"`
ContentHash string `json:"content_hash"`
Language string `json:"language,omitempty"`
ContentPath string `json:"content_path,omitempty"`
content string
}
var (
projectFileTokenRe = regexp.MustCompile(`[\w./\-]+\.[A-Za-z0-9]+`)
projectCommentRe = regexp.MustCompile(`^\s*(?:#|//|/\*|<!--|;)\s*([\w./\-]+\.[A-Za-z0-9]+)`)
)
func projectLangToExt(lang string) string {
switch strings.ToLower(strings.TrimSpace(lang)) {
case "python", "py":
return "py"
case "javascript", "js":
return "js"
case "typescript", "ts":
return "ts"
case "tsx":
return "tsx"
case "jsx":
return "jsx"
case "go", "golang":
return "go"
case "json":
return "json"
case "yaml", "yml":
return "yaml"
case "toml":
return "toml"
case "bash", "sh", "shell":
return "sh"
case "html":
return "html"
case "css":
return "css"
case "sql":
return "sql"
case "markdown", "md":
return "md"
case "dockerfile":
return "dockerfile"
case "text", "plaintext", "plain", "":
return "txt"
default:
return lang
}
}
func projectMimeForPath(path string) string {
lower := strings.ToLower(path)
switch {
case strings.HasSuffix(lower, ".json"):
return "application/json"
case strings.HasSuffix(lower, ".js"), strings.HasSuffix(lower, ".jsx"):
return "application/javascript"
case strings.HasSuffix(lower, ".ts"), strings.HasSuffix(lower, ".tsx"):
return "text/typescript"
case strings.HasSuffix(lower, ".md"):
return "text/markdown"
case strings.HasSuffix(lower, ".html"):
return "text/html"
case strings.HasSuffix(lower, ".css"):
return "text/css"
case strings.HasSuffix(lower, ".py"):
return "text/x-python"
case strings.HasSuffix(lower, ".go"):
return "text/x-go"
default:
return "text/plain"
}
}
// parseMarkdownProject extracts files from a markdown deliverable. A file path
// is taken from (1) a filename comment on the first code line, else (2) the
// nearest preceding markdown header that contains a filename token, else (3) a
// generated name. Blocks with no fenced code are ignored.
func parseMarkdownProject(content string) []projectFileEntry {
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
entries := []projectFileEntry{}
seen := map[string]bool{}
headerPath := ""
gen := 0
i := 0
for i < len(lines) {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, "#") {
if m := projectFileTokenRe.FindString(strings.Trim(trimmed, "#* `")); m != "" {
headerPath = strings.Trim(m, "`")
}
i++
continue
}
if strings.HasPrefix(trimmed, "```") {
lang := strings.TrimSpace(strings.TrimPrefix(trimmed, "```"))
i++
body := []string{}
for i < len(lines) && !strings.HasPrefix(strings.TrimSpace(lines[i]), "```") {
body = append(body, lines[i])
i++
}
i++ // closing fence
if len(body) == 0 {
continue
}
path := headerPath
if m := projectCommentRe.FindStringSubmatch(body[0]); m != nil {
path = m[1]
}
if strings.TrimSpace(path) == "" {
gen++
path = fmt.Sprintf("file%d.%s", gen, projectLangToExt(lang))
}
path = strings.TrimLeft(strings.TrimSpace(path), "/")
if seen[path] {
gen++
path = fmt.Sprintf("%s.%d", path, gen)
}
seen[path] = true
headerPath = ""
text := strings.Join(body, "\n")
sum := sha256.Sum256([]byte(text))
entries = append(entries, projectFileEntry{
Path: path,
Type: "file",
MimeType: projectMimeForPath(path),
SizeBytes: len(text),
ContentHash: "sha256:" + hex.EncodeToString(sum[:]),
Language: lang,
content: text,
})
continue
}
i++
}
return entries
}
func fetchArtifactContentString(ctx context.Context, record agentDeploymentRecord, artifactID string) (string, error) {
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if !cfg.Enabled || strings.TrimSpace(cfg.BaseURL) == "" {
return "", errors.New("runtime is not configured")
}
cctx, cancel := context.WithTimeout(ctx, cfg.Timeout)
defer cancel()
resp, err := callAgentRuntimeArtifactContent(cctx, cfg, record, artifactID)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 16<<20))
if err != nil {
return "", err
}
return string(body), nil
}
// loadProjectArtifact resolves the authenticated deployment, fetches the
// artifact content and parses it into a project file tree.
func loadProjectArtifact(c *gin.Context) (agentDeploymentRecord, string, []projectFileEntry, bool) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return record, "", nil, false
}
record = reconcileDeploymentFromRuntime(c.Request.Context(), record)
artifactID := strings.TrimSpace(c.Param("artifact_id"))
if artifactID == "" {
agentError(c, "ARTIFACT_ID_REQUIRED", "artifact_id is required")
return record, "", nil, false
}
content, err := fetchArtifactContentString(c.Request.Context(), record, artifactID)
if err != nil {
common.SysLog("loadProjectArtifact: " + err.Error())
agentError(c, "ARTIFACT_CONTENT_FETCH_FAILED", "failed to fetch artifact content")
return record, artifactID, nil, false
}
return record, artifactID, parseMarkdownProject(content), true
}
func projectArtifactBasePath(c *gin.Context, suffix string) string {
return strings.TrimSuffix(c.Request.URL.Path, suffix)
}
// HeicodeArtifactManifest serves the project file tree (unified spec §12.3).
func HeicodeArtifactManifest(c *gin.Context) {
_, artifactID, files, ok := loadProjectArtifact(c)
if !ok {
return
}
base := projectArtifactBasePath(c, "/manifest")
tree := sha256.New()
entries := make([]projectFileEntry, 0, len(files))
for _, f := range files {
// Query-param form avoids path-segment ambiguity for nested/encoded
// paths (unified spec §12.4, gap P1-1).
f.ContentPath = base + "/files?path=" + url.QueryEscape(f.Path)
tree.Write([]byte(f.ContentHash))
entries = append(entries, f)
}
common.ApiSuccess(c, gin.H{
"artifact_id": artifactID,
"artifact_type": "project_folder",
"root_dir": "project",
"revision": 1,
"content_hash": "sha256:" + hex.EncodeToString(tree.Sum(nil)),
"file_count": len(entries),
"entries": entries,
"archive": gin.H{
"format": "zip",
"download_path": base + "/archive",
},
})
}
// HeicodeArtifactFile serves a single file from the parsed project. The path is
// taken from the ?path= query (preferred, no segment ambiguity) and falls back
// to the trailing path segment for compatibility (unified spec §12.4).
func HeicodeArtifactFile(c *gin.Context) {
_, _, files, ok := loadProjectArtifact(c)
if !ok {
return
}
want := strings.TrimSpace(c.Query("path"))
if want == "" {
want = strings.TrimSpace(c.Param("path"))
}
want = strings.Trim(want, "/")
if want == "" {
agentError(c, "FILE_PATH_REQUIRED", "path is required (use ?path=<file path>)")
return
}
for _, f := range files {
if f.Path == want {
c.Data(http.StatusOK, f.MimeType+"; charset=utf-8", []byte(f.content))
return
}
}
agentError(c, "FILE_NOT_FOUND", "file not found in project artifact")
}
// HeicodeArtifactArchive zips the parsed project files for download. Returns a
// real application/zip body with Content-Disposition + Content-Length, or a
// retryable ARTIFACT_ARCHIVE_NOT_READY error when no files have been produced
// yet (unified spec §12.6, gap P0-4) — so the client never reports a phantom
// "downloaded" on an empty body.
func HeicodeArtifactArchive(c *gin.Context) {
_, artifactID, files, ok := loadProjectArtifact(c)
if !ok {
return
}
if len(files) == 0 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"error": gin.H{
"code": "ARTIFACT_ARCHIVE_NOT_READY",
"message": "项目压缩包尚未生成(产物还未就绪),请稍后重试。",
"retryable": true,
},
})
return
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
for _, f := range files {
w, err := zw.Create("project/" + f.Path)
if err != nil {
continue
}
_, _ = w.Write([]byte(f.content))
}
if err := zw.Close(); err != nil {
agentError(c, "ARTIFACT_ARCHIVE_FAILED", "failed to build project archive")
return
}
filename := projectArchiveFilename(artifactID)
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
c.Header("Content-Length", fmt.Sprintf("%d", buf.Len()))
c.Data(http.StatusOK, "application/zip", buf.Bytes())
}
// projectArchiveFilename builds a safe .zip filename from the artifact id.
func projectArchiveFilename(artifactID string) string {
name := strings.TrimSpace(artifactID)
if name == "" {
name = "project"
}
name = strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
return r
default:
return '-'
}
}, name)
return name + ".zip"
}
-274
View File
@@ -1,274 +0,0 @@
package controller
import (
"context"
"fmt"
"strings"
"github.com/gin-gonic/gin"
"github.com/heicode/manager/common"
"github.com/heicode/manager/model"
)
// consumeActiveRevision resolves the latest accepted local-edit revision for a
// deployment and marks it applied, so a follow-up /messages or /execute runs on
// the client's latest baseline (unified spec §12.7, gap P1-3). Returns the
// revision number and whether one existed.
func consumeActiveRevision(deploymentID string) (int, bool) {
rev, ok := model.LatestAcceptedRevisionForDeployment(deploymentID)
if !ok {
return 0, false
}
_ = model.DB.Model(&model.AgentArtifactRevision{}).
Where("id = ?", rev.Id).
Update("status", "applied").Error
return rev.ProjectRevision, true
}
// Client-facing task create + conversation flow (unified spec §5.1 / §8).
// The client submits a requirement package (not a raw orchestration_plan);
// Manager translates it into an orchestration_plan and creates the deployment.
// task_id == deployment_id.
type heicodeRequirement struct {
Objective string `json:"objective"`
Context []any `json:"context"`
Attachments []any `json:"attachments"`
Constraints []any `json:"constraints"`
AcceptanceCriteria []any `json:"acceptance_criteria"`
}
type heicodeModelSelection struct {
Type string `json:"type"`
DefaultModel string `json:"default_model"`
PrimaryModel string `json:"primary_model"`
Roles map[string]string `json:"roles"`
}
type heicodeTaskCreateBody struct {
// Spec-preferred requirement package.
Mode string `json:"mode"`
ClientRole string `json:"client_role"`
ConversationID string `json:"conversation_id"`
Requirement heicodeRequirement `json:"requirement"`
ModelSelection heicodeModelSelection `json:"model_selection"`
Roles []string `json:"roles"`
// Compatibility: a raw plan may still be supplied directly.
OrchestrationPlan *agentOrchestrationPlan `json:"orchestration_plan"`
}
func buildPlanFromRequirement(c *gin.Context, body heicodeTaskCreateBody, runtimeMode string) (agentOrchestrationPlan, bool) {
objective := strings.TrimSpace(body.Requirement.Objective)
if objective == "" {
agentError(c, "POLICY_REJECTED", "requirement.objective is required")
return agentOrchestrationPlan{}, false
}
roles := []string{}
for _, r := range body.Roles {
if r = strings.TrimSpace(r); r != "" {
roles = append(roles, r)
}
}
if len(roles) == 0 {
roles = []string{"backend"}
}
defaultModel := firstNonEmpty(body.ModelSelection.DefaultModel, body.ModelSelection.PrimaryModel, defaultAgentModelID())
allowedSeen := map[string]bool{}
allowed := []string{}
addAllowed := func(m string) {
if m = strings.TrimSpace(m); m != "" && !allowedSeen[m] {
allowedSeen[m] = true
allowed = append(allowed, m)
}
}
agents := make([]agentAgentPlan, 0, len(roles))
runtimeAgents := make([]agentRuntimeAgent, 0, len(roles))
for _, role := range roles {
model := defaultModel
if body.ModelSelection.Type == "per_role" {
if m, ok := body.ModelSelection.Roles[role]; ok && strings.TrimSpace(m) != "" {
model = strings.TrimSpace(m)
}
}
agents = append(agents, agentAgentPlan{
RoleTemplate: role,
Goal: fmt.Sprintf("Execute the Heicode task as %s within the approved resource scope.", role),
DefaultModelID: model,
})
runtimeAgents = append(runtimeAgents, agentRuntimeAgent{Role: role, ModelRef: model, InstanceCount: 1})
addAllowed(model)
}
correlation := firstNonEmpty(strings.TrimSpace(body.ConversationID), "conv-"+common.GetUUID()[:8])
plan := agentOrchestrationPlan{
IntentID: correlation,
TemplateHint: "heicode-task",
Objective: objective,
SubMode: "agile",
RiskLevel: agentRiskLow,
Budget: agentBudget{MaxTokens: 20000, MaxCostUSD: 1, MaxDurationSec: 900},
UserContext: agentUserContext{Role: "user"},
BillingContext: agentBillingContext{
Provider: "newapi",
DefaultModelID: defaultModel,
AllowedModelIDs: allowed,
},
AgentRuntime: agentAgentRuntime{Platform: "agent", Agents: runtimeAgents},
Agents: agents,
Constraints: agentConstraints{AllowedModelIDs: allowed},
Metadata: agentMetadata{
CorrelationID: correlation,
RuntimeMode: normalizeAgentRuntimeMode(runtimeMode),
},
}
return plan, true
}
func heicodeCreateTask(c *gin.Context, runtimeMode string, dispatchSource string) {
var body heicodeTaskCreateBody
if err := c.ShouldBindJSON(&body); err != nil {
agentError(c, "POLICY_REJECTED", err.Error())
return
}
var plan agentOrchestrationPlan
if body.OrchestrationPlan != nil {
plan = *body.OrchestrationPlan
} else {
p, ok := buildPlanFromRequirement(c, body, runtimeMode)
if !ok {
return
}
plan = p
}
record, ok := createAgentDeploymentFromPlan(c, plan, true, runtimeMode)
if !ok {
return
}
record = maybeDispatchAgentRuntimeCreate(c, record, dispatchSource)
common.ApiSuccess(c, withDisplayStatus(record))
}
// HeicodeCreateSubAgileTask: POST /api/heicode/sub-agile/tasks
func HeicodeCreateSubAgileTask(c *gin.Context) {
heicodeCreateTask(c, agentRuntimeModeAgent, "heicode_client_sub_agile")
}
// HeicodeCreateSwarmTask: POST /api/heicode/swarm/tasks
func HeicodeCreateSwarmTask(c *gin.Context) {
heicodeCreateTask(c, agentRuntimeModeSwarm, "api_swarms_adapter")
}
type heicodeTaskMessage struct {
Message string `json:"message"`
Role string `json:"role"`
}
// HeicodeTaskMessage records a follow-up conversation message on a task and
// returns the current task state (unified spec §5.1 .../messages).
func HeicodeTaskMessage(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
var msg heicodeTaskMessage
_ = common.DecodeJson(c.Request.Body, &msg)
if strings.TrimSpace(msg.Message) == "" {
agentError(c, "POLICY_REJECTED", "message is required")
return
}
recordAgentAuditEvent(agentEvent{
EventID: "evt_" + common.GetUUID()[:12],
Event: "task.message_received",
SchemaVersion: 1,
UserID: record.Plan.UserContext.UserID,
ChannelID: record.Plan.UserContext.ChannelID,
BindingScope: firstPlanBindingScope(record.Plan),
DeploymentID: record.DeploymentID,
CorrelationID: record.Plan.Metadata.CorrelationID,
OccurredAt: agentNow(),
}, "heicode_client_message", record.DeploymentID, agentRequestID(c), "ok")
record = reconcileDeploymentFromRuntime(c.Request.Context(), record)
activeRev, hasRev := consumeActiveRevision(record.DeploymentID)
resp := gin.H{
"task_id": record.DeploymentID,
"accepted": true,
"display_status": agentDeploymentDisplayStatus(record),
"note": "message recorded; continuation runs when the runtime supports mid-task input",
}
if hasRev {
resp["active_project_revision"] = activeRev
}
common.ApiSuccess(c, resp)
}
// HeicodeTaskExecute ensures the task is dispatched to its runtime and returns
// the current state (unified spec §5.1 .../execute).
func HeicodeTaskExecute(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
if strings.TrimSpace(record.RuntimeSwarmID) == "" && strings.TrimSpace(record.RuntimeDeploymentID) == "" {
record = maybeDispatchAgentRuntimeCreate(c, record, "heicode_client_execute")
}
_, _ = consumeActiveRevision(record.DeploymentID)
record = reconcileDeploymentFromRuntime(c.Request.Context(), record)
common.ApiSuccess(c, withDisplayStatus(record))
}
// heicodeListTasksByMode lists the authenticated user's tasks filtered to a
// single client mode (sub_agile | swarm), so /api/heicode/sub-agile/tasks no
// longer leaks swarm tasks and vice versa. Each item carries display_status +
// mode for the client.
func heicodeListTasksByMode(c *gin.Context, mode string) {
userID := fmt.Sprintf("%d", c.GetInt("id"))
bindingScope := strings.TrimSpace(c.Query("binding_scope"))
items := listAgentDeploymentRecords(userID, bindingScope)
filtered := make([]agentDeploymentRecord, 0, len(items))
for _, it := range items {
it = withDisplayStatus(it)
if it.Mode == mode {
filtered = append(filtered, it)
}
}
common.ApiSuccess(c, gin.H{"items": filtered, "total": len(filtered)})
}
// HeicodeListSubAgileTasks: GET /api/heicode/sub-agile/tasks (sub_agile only).
func HeicodeListSubAgileTasks(c *gin.Context) {
heicodeListTasksByMode(c, "sub_agile")
}
// HeicodeListSwarmTasks: GET /api/heicode/swarm/tasks (swarm only).
func HeicodeListSwarmTasks(c *gin.Context) {
heicodeListTasksByMode(c, "swarm")
}
// HeicodeDeleteTask hard-deletes a task: best-effort stop at the runtime, then
// remove the deployment record so it disappears from the list (unified spec
// §5.1 DELETE .../tasks/{id}).
func HeicodeDeleteTask(c *gin.Context) {
record, ok := requireAuthenticatedUserAgentDeployment(c)
if !ok {
return
}
// Best-effort runtime stop; never block the delete on a runtime error.
cfg := agentRuntimeClientConfigForMode(agentRuntimeModeForRecord(record))
if cfg.Enabled && strings.TrimSpace(record.RuntimeDeploymentID) != "" {
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
_, _ = callAgentRuntimeStop(ctx, cfg, record, "deleted by user")
cancel()
}
agentMu.Lock()
delete(agentDeployments, record.DeploymentID)
agentMu.Unlock()
if err := model.DeleteAgentDeployment(record.DeploymentID); err != nil {
common.SysLog("HeicodeDeleteTask: " + err.Error())
agentError(c, "DEPLOYMENT_DELETE_FAILED", "failed to delete task")
return
}
common.ApiSuccess(c, gin.H{"deployment_id": record.DeploymentID, "deleted": true})
}
-45
View File
@@ -63,8 +63,6 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.POST("/agent/callbacks/runtime-events", controller.AgentReceiveRuntimeEventCallback)
// Client-facing capability discovery (unified spec §6). Catalog data only.
apiRouter.GET("/heicode/capabilities", controller.HeicodeCapabilities)
// Cloud deployment target discovery (unified spec §18.1).
apiRouter.GET("/heicode/deployment-targets", middleware.UserOrV2DeviceAuth(), controller.HeicodeDeploymentTargets)
apiRouter.POST("/swarms", middleware.UserOrV2DeviceAuth(), controller.AgentCreateUserSwarm)
//apiRouter.POST("/waffo-pancake/webhook", controller.WaffoPancakeWebhook)
@@ -523,49 +521,6 @@ func SetApiRouter(router *gin.Engine) {
agentApprovalRoute.POST("/user/tasks/:task_id/deployment-draft", controller.AgentCreateTaskDeploymentDraft)
}
// Client-facing unified task routes (unified spec §5.1). task_id == the
// deployment_id, so these reuse the deployment control-plane handlers.
// Sub Agile -> agent_management; Swarm -> HeiCode-Swarm (mode is carried
// by the deployment record created at POST /tasks).
registerHeicodeTaskRoutes := func(group *gin.RouterGroup, createHandler gin.HandlerFunc, listHandler gin.HandlerFunc) {
group.GET("/tasks", listHandler)
group.POST("/tasks", createHandler)
group.GET("/tasks/:deployment_id", controller.AgentGetUserDeployment)
group.DELETE("/tasks/:deployment_id", controller.HeicodeDeleteTask)
group.POST("/tasks/:deployment_id/messages", controller.HeicodeTaskMessage)
group.POST("/tasks/:deployment_id/execute", controller.HeicodeTaskExecute)
group.POST("/tasks/:deployment_id/stop", controller.AgentStopUserDeployment)
group.GET("/tasks/:deployment_id/timeline", controller.AgentGetUserDeploymentTimeline)
group.GET("/tasks/:deployment_id/workflow", controller.HeicodeTaskWorkflow)
group.GET("/tasks/:deployment_id/logs", controller.AgentListUserDeploymentLogs)
group.GET("/tasks/:deployment_id/events", controller.AgentListUserDeploymentEvents)
group.GET("/tasks/:deployment_id/metrics", controller.AgentGetUserDeploymentMetrics)
group.GET("/tasks/:deployment_id/diagnostics", controller.AgentGetUserDeploymentRuntimeDiagnostics)
group.GET("/tasks/:deployment_id/artifacts", controller.HeicodeListTaskArtifacts)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/content", controller.AgentGetUserDeploymentArtifactContent)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/manifest", controller.HeicodeArtifactManifest)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/archive", controller.HeicodeArtifactArchive)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/files", controller.HeicodeArtifactFile)
group.GET("/tasks/:deployment_id/artifacts/:artifact_id/revisions", controller.HeicodeArtifactRevisions)
group.POST("/tasks/:deployment_id/artifacts/:artifact_id/local-edits", controller.HeicodeArtifactLocalEdit)
group.POST("/tasks/:deployment_id/artifacts/:artifact_id/local-edits/batch", controller.HeicodeArtifactLocalEdit)
group.GET("/tasks/:deployment_id/sk-snapshots", controller.AgentListUserSKSnapshots)
group.GET("/tasks/:deployment_id/approvals", controller.HeicodeListTaskApprovals)
group.POST("/tasks/:deployment_id/approvals/:approval_id/approve", controller.ApproveAgentApprovalRequest)
group.POST("/tasks/:deployment_id/approvals/:approval_id/reject", controller.RejectAgentApprovalRequest)
// Cloud deployment of the project artifact (unified spec §18).
group.POST("/tasks/:deployment_id/deployments", controller.HeicodeCreateDeployment)
group.GET("/tasks/:deployment_id/deployments", controller.HeicodeListDeployments)
}
heicodeSubAgileRoute := apiRouter.Group("/heicode/sub-agile")
heicodeSubAgileRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSubAgileRoute, controller.HeicodeCreateSubAgileTask, controller.HeicodeListSubAgileTasks)
heicodeSwarmRoute := apiRouter.Group("/heicode/swarm")
heicodeSwarmRoute.Use(middleware.UserOrV2DeviceAuth())
registerHeicodeTaskRoutes(heicodeSwarmRoute, controller.HeicodeCreateSwarmTask, controller.HeicodeListSwarmTasks)
// Template-agent model (new): deploy a template agent with bound resources
// injected as env; client reads the agent list and connects to the agent's
// subdomain directly. Same V2-device / session auth as the task routes.