feat: prepare 0.3.3 worker llm agent and ui enhancements

This commit is contained in:
pphua committed 2026-08-21 23:55:43 +08:00
1 parent d3183dbbd2
commit 1bebdce7f1
28 files changed
+1331 -978

No files matched your search

-1
View File
@@ -667,7 +667,6 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("PUT /api/tasks/{id}/llm", s.updateTaskLLMProfiles)
mux.HandleFunc("GET /api/tasks/{id}/llm/resolution", s.taskLLMResolutionHandler)
mux.HandleFunc("POST /api/tasks/{id}/intents/{iid}/control", s.controlIntent)
mux.HandleFunc("POST /api/tasks/{id}/intents/control/batch", s.controlIntentsBatch)
mux.HandleFunc("POST /api/tasks/{id}/intents/{iid}/rerun", s.rerunIntent) // 重跑单条 blocked/exhausted/stopped 意图
mux.HandleFunc("POST /api/tasks/{id}/intents/rerun-blocked", s.rerunBlocked) // 批量重跑本任务全部 blocked 意图
mux.HandleFunc("POST /api/active", s.setActive)
-37
View File
@@ -688,43 +688,6 @@ func TestBatchControlLimitCountsDeduplicatedIDs(t *testing.T) {
t.Fatalf("unique task ids status=%d body=%s", rec.Code, rec.Body.String())
}
task, err := m.CreateTask("batch intent limit", "deduplicate intent ids", nil, 0, 0)
if err != nil {
t.Fatal(err)
}
defer func() { _, _ = m.DeleteTask(task.ID, DeleteTaskOptions{}) }()
repeatedIntents := make([]string, maxBatchControlIDs+1)
for i := range repeatedIntents {
repeatedIntents[i] = "999999"
}
rec = request(s.controlIntentsBatch, "/api/tasks/1/intents/control/batch", map[string]string{"id": task.ID}, map[string]any{
"intent_ids": repeatedIntents,
"action": "pause",
})
if rec.Code != http.StatusOK {
t.Fatalf("deduplicated intent ids status=%d body=%s", rec.Code, rec.Body.String())
}
var intentPayload struct {
Items []batchControlItem `json:"items"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &intentPayload); err != nil {
t.Fatal(err)
}
if len(intentPayload.Items) != 1 || intentPayload.Items[0].ID != "999999" {
t.Fatalf("deduplicated intent response=%+v", intentPayload.Items)
}
uniqueIntents := make([]string, maxBatchControlIDs+1)
for i := range uniqueIntents {
uniqueIntents[i] = strconv.Itoa(i + 1)
}
rec = request(s.controlIntentsBatch, "/api/tasks/1/intents/control/batch", map[string]string{"id": task.ID}, map[string]any{
"intent_ids": uniqueIntents,
"action": "resume",
})
if rec.Code != http.StatusBadRequest {
t.Fatalf("unique intent ids status=%d body=%s", rec.Code, rec.Body.String())
}
}
func TestPauseTaskToolPersistsAndDequeuesTask(t *testing.T) {
-73
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"log"
"net/http"
"strconv"
"strings"
"github.com/Autumn-27/artex/agent"
@@ -32,7 +31,6 @@ type batchControlItem struct {
ID string `json:"id"`
OK bool `json:"ok"`
Status string `json:"status,omitempty"`
State string `json:"state,omitempty"`
Queued bool `json:"queued,omitempty"`
Error string `json:"error,omitempty"`
}
@@ -241,74 +239,3 @@ func (s *Server) controlTasksBatch(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, 200, map[string]any{"items": items})
}
func (s *Server) controlIntentsBatch(w http.ResponseWriter, r *http.Request) {
t, ok := s.m.Task(r.PathValue("id"))
if !ok {
writeErr(w, 404, "task not found")
return
}
r.Body = http.MaxBytesReader(w, r.Body, 256<<10)
var req struct {
IntentIDs []string `json:"intent_ids"`
Action string `json:"action"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeErr(w, 400, "bad json: "+err.Error())
return
}
if req.Action != "pause" && req.Action != "resume" {
writeErr(w, 400, "action must be pause|resume")
return
}
type parsedIntentID struct {
raw string
id int64
valid bool
}
seen := map[int64]bool{}
seenInvalid := map[string]bool{}
intentIDs := make([]parsedIntentID, 0, len(req.IntentIDs))
for _, raw := range req.IntentIDs {
iid, err := strconv.ParseInt(raw, 10, 64)
if err != nil || iid <= 0 {
if seenInvalid[raw] {
continue
}
seenInvalid[raw] = true
intentIDs = append(intentIDs, parsedIntentID{raw: raw})
continue
}
if seen[iid] {
continue
}
seen[iid] = true
intentIDs = append(intentIDs, parsedIntentID{raw: raw, id: iid, valid: true})
}
if len(intentIDs) == 0 || len(intentIDs) > maxBatchControlIDs {
writeErr(w, 400, fmt.Sprintf("intent_ids 数量必须为 1-%d", maxBatchControlIDs))
return
}
if !s.engine.beginTaskOperation(t.ID) {
writeErr(w, 409, "任务正在删除,无法控制意图")
return
}
defer s.engine.decInflight(t.ID)
items := make([]batchControlItem, 0, len(intentIDs))
for _, parsed := range intentIDs {
item := batchControlItem{ID: parsed.raw}
if !parsed.valid {
item.Error = "bad intent id"
items = append(items, item)
continue
}
result, err := s.applyIntentControl(r.Context(), t, parsed.id, req.Action)
if err != nil {
item.Error = err.Error()
} else {
item.OK, item.State = true, result.State
}
items = append(items, item)
}
writeJSON(w, 200, map[string]any{"items": items})
}
+46
View File
@@ -0,0 +1,46 @@
package server
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestWorkerControlRoutes(t *testing.T) {
m, err := NewManager(t.TempDir(), "")
if err != nil {
t.Skipf("postgres unavailable (%v) - skipping", err)
}
defer m.Close()
td := t.TempDir()
s := New(context.Background(), m, td, td, td)
h := s.Handler()
token, err := signJWT(s.jwtKey)
if err != nil {
t.Fatalf("sign jwt: %v", err)
}
request := func(path string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{"action":"pause"}`))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
return rec
}
// The single-worker route is still registered and reaches its JSON handler.
single := request("/api/tasks/missing/intents/1/control")
if single.Code != http.StatusNotFound || !strings.Contains(single.Body.String(), `"error":"task not found"`) {
t.Fatalf("single control route unavailable: status=%d body=%s", single.Code, single.Body.String())
}
// The former batch route must fall through the mux instead of reaching a task handler.
batch := request("/api/tasks/missing/intents/control/batch")
if batch.Code != http.StatusNotFound || !strings.Contains(batch.Body.String(), "404 page not found") {
t.Fatalf("batch control route still registered: status=%d body=%s", batch.Code, batch.Body.String())
}
}