feat: prepare 0.3.3 worker llm agent and ui enhancements
This commit is contained in:
1 parent
d3183dbbd2
commit
1bebdce7f1
28 files changed
+1331
-978
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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})
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user