Files
artex/server/constraints_api.go
T
dela 97668dab7e
ci / go (push) Waiting to run
ci / go-db (agent) (push) Waiting to run
ci / go-db (config) (push) Waiting to run
ci / go-db (db) (push) Waiting to run
ci / go-db (evidence) (push) Waiting to run
ci / go-db (llmrec) (push) Waiting to run
ci / go-db (server) (push) Waiting to run
detections / detections (push) Waiting to run
docs / links (push) Waiting to run
web / web (push) Waiting to run
Localize user-facing UI, agent output, and backend messages to Simplified Chinese
2026-10-09 14:08:31 +08:00

190 lines
5.8 KiB
Go

package server
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"github.com/Autumn-27/artex/db"
)
// 总览「约束管理」的人工 CRUD 接口 + 注入范围开关的解析。操作约束(allow/deny)与 agent 侧的
// set_constraints 工具写同一张 task_constraints 表;这里是人类在 UI 上直接增删改。约束仅是
// 提示上下文——增删改后【不】通知 planner,下一轮规划自然读库生效(按产品决策)。每个变更 handler
// 都走 beginTaskOperation/decInflight,避免与任务删除竞态(与目标/意图 CRUD 一致)。
// 注入范围开关的 settings key,默认都开(GetBool 第二参数 = true)。
const (
settingConstraintsInjectPlanner = "constraints_inject_planner"
settingConstraintsInjectWorker = "constraints_inject_worker"
)
// 约束 CRUD 핸들러의 사용자 노출 에러 응답(한국어). writeErr 로 그대로 UI 토스트에 노출된다.
// 用語: 约束→제약(ko.json 의 약속/제약 표기와 정합), 任务→작업. 추가/수정/삭제 문구는
// goals_api.go 의 errGoalTaskDeleting* 와 같은 문형이고, kind 검증은 intercept.go 의
// "값은 … 중 하나여야 합니다" 패턴을 따른다.
const (
errConstraintTaskDeletingAdd = "正在删除任务,无法添加约束"
errConstraintTaskDeletingEdit = "正在删除任务,无法修改约束"
errConstraintTaskDeletingDelete = "正在删除任务,无法删除约束"
errConstraintTextEmpty = "约束内容不能为空"
errConstraintKindInvalid = "kind 的值必须是 allow 或 deny 之一"
)
// constraintInjectPlanner / constraintInjectWorker 报告是否把操作约束注入对应 agent 的
// 系统提示(默认开)。作为 resolver 传给 planner/worker,每轮读 → 改开关即时生效。
func (s *Server) constraintInjectPlanner() bool {
return s.m.pg.GetBool(settingConstraintsInjectPlanner, true)
}
func (s *Server) constraintInjectWorker() bool {
return s.m.pg.GetBool(settingConstraintsInjectWorker, true)
}
// listConstraints 返回本任务的全部操作约束(allow 在前、deny 在后)。
func (s *Server) listConstraints(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
rows, err := t.Store.ListConstraints()
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]any{"constraints": constraintDTOs(rows)})
}
// addConstraint 人工新增一条操作约束(kind=allow|deny)。不通知 planner。
func (s *Server) addConstraint(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, errConstraintTaskDeletingAdd)
return
}
defer s.engine.decInflight(t.ID)
var body struct {
Text string `json:"text"`
Kind string `json:"kind"`
}
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, errConstraintTextEmpty)
return
}
kind := normalizeConstraintKind(body.Kind)
if kind == "" {
writeErr(w, 400, errConstraintKindInvalid)
return
}
id, err := t.Store.AddConstraint(kind, text, "human")
if err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, ConstraintDTO{ID: strconv.FormatInt(id, 10), Kind: kind, Text: text, Origin: "human"})
}
// editConstraint 人工修改一条约束(kind + text)。不通知 planner。
func (s *Server) editConstraint(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, errConstraintTaskDeletingEdit)
return
}
defer s.engine.decInflight(t.ID)
cid, err := strconv.ParseInt(r.PathValue("cid"), 10, 64)
if err != nil || cid <= 0 {
writeErr(w, 400, "bad constraint id")
return
}
var body struct {
Text string `json:"text"`
Kind string `json:"kind"`
}
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, errConstraintTextEmpty)
return
}
kind := normalizeConstraintKind(body.Kind)
if kind == "" {
writeErr(w, 400, errConstraintKindInvalid)
return
}
if err := t.Store.UpdateConstraint(cid, kind, text); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, ConstraintDTO{ID: strconv.FormatInt(cid, 10), Kind: kind, Text: text})
}
// deleteConstraint 人工删除一条约束。不通知 planner。
func (s *Server) deleteConstraint(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, errConstraintTaskDeletingDelete)
return
}
defer s.engine.decInflight(t.ID)
cid, err := strconv.ParseInt(r.PathValue("cid"), 10, 64)
if err != nil || cid <= 0 {
writeErr(w, 400, "bad constraint id")
return
}
if err := t.Store.DeleteConstraint(cid); err != nil {
writeErr(w, 500, err.Error())
return
}
writeJSON(w, 200, map[string]bool{"ok": true})
}
// normalizeConstraintKind lowercases + validates the kind; "" on invalid.
func normalizeConstraintKind(k string) string {
k = strings.TrimSpace(strings.ToLower(k))
if k == "allow" || k == "deny" {
return k
}
return ""
}
// constraintDTOs converts db rows to the frontend shape.
func constraintDTOs(in []db.Constraint) []ConstraintDTO {
out := make([]ConstraintDTO, 0, len(in))
for _, c := range in {
out = append(out, ConstraintDTO{
ID: strconv.FormatInt(c.ID, 10),
Kind: c.Kind,
Text: c.Text,
Origin: c.Origin,
TS: rfc3339(c.CreatedAt),
})
}
return out
}