Files
ARTEX/server/goals_api.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

173 lines
5.4 KiB
Go

package server
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/Autumn-27/artex/db"
)
// Human CRUD endpoints for the overview "Goal Management" card. They write the same goal nodes
// as the agent-side set_goals tool, but the entry point here is a human editing in the UI;
// add/edit reuse the "revive task" logic (admitTask resume: terminal→running, lift the pause,
// queue when needed), delete does not revive (product decision). Each handler uses beginTaskOperation/decInflight to avoid racing task deletion (matching intent CRUD).
// listGoals returns all goals of this task (text/vulnclass/state split out) for the goal-management card to render.
func (s *Server) listGoals(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
goals, err := t.Store.ListByKind(db.KindGoal, 10000)
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"goals": goalDTOs(goals)})
}
// addGoal manually adds one goal: persist it (under the task root's spawns edge) → record
// a "goal added" trigger to wake the planner → revive the task so it re-judges completion against the new goal.
func (s *Server) addGoal(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
if !s.engine.beginTaskOperation(t.ID) {
writeErr(w, 409, "Task is being deleted; cannot add goal")
return
}
defer s.engine.decInflight(t.ID)
var body struct {
Text string `json:"text"`
VulnClass string `json:"vulnclass"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, 400, "invalid JSON")
return
}
text := strings.TrimSpace(body.Text)
if text == "" {
writeErr(w, 400, "Goal text cannot be empty")
return
}
payload := map[string]any{"text": text}
if vc := strings.TrimSpace(body.VulnClass); vc != "" {
payload["vulnclass"] = vc
}
id, err := t.Store.AddGoal(payload, "human")
if err != nil {
writeErr(w, 500, err.Error())
return
}
if of, _ := t.Store.OriginFactID(); of > 0 && id > 0 {
_ = t.Store.Link(of, db.RelSpawns, id) // goal descends from the task root (origin fact)
}
t.NotifyGoal([]string{text}) // record a "human added a goal: …" trigger and wake the planner
s.reviveTask(t) // pull a completed/paused task back to running
node, _ := t.Store.GetNode(id)
if node == nil {
writeErr(w, 500, "failed to read the goal back after writing")
return
}
writeJSON(w, 200, goalDTO(node))
}
// editGoal manually edits a goal's text (and vulnclass): update the store → record a "user
// changed the goal from old to new" trigger to wake the planner → revive the task so it adjusts direction.
func (s *Server) editGoal(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
if !s.engine.beginTaskOperation(t.ID) {
writeErr(w, 409, "Task is being deleted; cannot edit goal")
return
}
defer s.engine.decInflight(t.ID)
gid, err := strconv.ParseInt(r.PathValue("gid"), 10, 64)
if err != nil || gid <= 0 {
writeErr(w, 400, "bad goal id")
return
}
var body struct {
Text string `json:"text"`
VulnClass string `json:"vulnclass"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, 400, "invalid JSON")
return
}
text := strings.TrimSpace(body.Text)
if text == "" {
writeErr(w, 400, "Goal text cannot be empty")
return
}
node, err := t.Store.GetNode(gid)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if node == nil || node.Kind != db.KindGoal {
writeErr(w, 404, "goal not found")
return
}
oldText := goalDTO(node).Text
if err := t.Store.UpdateGoalPayload(gid, text, strings.TrimSpace(body.VulnClass)); err != nil {
writeErr(w, 500, err.Error())
return
}
t.NotifyGoalEdited(oldText, text) // record a "human changed the goal from old to new" trigger and wake the planner
s.reviveTask(t) // same as add: revive the task to re-judge the new goal
updated, _ := t.Store.GetNode(gid)
if updated == nil {
writeErr(w, 500, "failed to read the goal back after update")
return
}
writeJSON(w, 200, goalDTO(updated))
}
// deleteGoal manually deletes one goal (hard delete, cascades edges/anchors): delete from
// the store → record a "user deleted goal X" trigger to wake the planner to re-judge the remaining goals. Per product decision, deleting does [not] revive the task.
func (s *Server) deleteGoal(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
if !s.engine.beginTaskOperation(t.ID) {
writeErr(w, 409, "Task is being deleted; cannot delete goal")
return
}
defer s.engine.decInflight(t.ID)
gid, err := strconv.ParseInt(r.PathValue("gid"), 10, 64)
if err != nil || gid <= 0 {
writeErr(w, 400, "bad goal id")
return
}
node, err := t.Store.GetNode(gid)
if err != nil {
writeErr(w, 500, err.Error())
return
}
if node == nil || node.Kind != db.KindGoal {
writeErr(w, 404, "goal not found")
return
}
text := goalDTO(node).Text
if err := t.Store.DeleteGoal(gid); err != nil {
writeErr(w, 500, err.Error())
return
}
t.NotifyGoalDeleted(text) // record a "human deleted the goal: …" trigger and wake the planner (task not revived)
writeJSON(w, 200, map[string]bool{"ok": true})
}