mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-08 00:17:30 +08:00
task queue ops: add pause/retry/complete/ignore actions and UI controls with filters
This commit is contained in:
@@ -1350,7 +1350,74 @@ func (s *RegistryServer) handleWebUITaskAudit(w http.ResponseWriter, r *http.Req
|
|||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
path := filepath.Join(strings.TrimSpace(s.workspacePath), "memory", "task-audit.jsonl")
|
if r.Method == http.MethodPost {
|
||||||
|
var body map[string]interface{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
http.Error(w, "invalid json", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := fmt.Sprintf("%v", body["action"])
|
||||||
|
taskID := fmt.Sprintf("%v", body["task_id"])
|
||||||
|
if taskID == "" {
|
||||||
|
http.Error(w, "task_id required", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tasksPath := filepath.Join(strings.TrimSpace(s.workspacePath), "memory", "tasks.json")
|
||||||
|
tb, err := os.ReadFile(tasksPath)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "tasks not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var tasks []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(tb, &tasks); err != nil {
|
||||||
|
http.Error(w, "invalid tasks file", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
now := time.Now().UTC().Format(time.RFC3339)
|
||||||
|
updated := false
|
||||||
|
for _, t := range tasks {
|
||||||
|
if fmt.Sprintf("%v", t["id"]) != taskID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch action {
|
||||||
|
case "pause":
|
||||||
|
t["status"] = "waiting"
|
||||||
|
t["block_reason"] = "manual_pause"
|
||||||
|
t["last_pause_reason"] = "manual_pause"
|
||||||
|
t["last_pause_at"] = now
|
||||||
|
case "retry":
|
||||||
|
t["status"] = "todo"
|
||||||
|
t["block_reason"] = ""
|
||||||
|
t["retry_after"] = ""
|
||||||
|
case "complete":
|
||||||
|
t["status"] = "done"
|
||||||
|
t["block_reason"] = "manual_complete"
|
||||||
|
case "ignore":
|
||||||
|
t["status"] = "blocked"
|
||||||
|
t["block_reason"] = "manual_ignore"
|
||||||
|
t["retry_after"] = "2099-01-01T00:00:00Z"
|
||||||
|
default:
|
||||||
|
http.Error(w, "unsupported action", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t["updated_at"] = now
|
||||||
|
updated = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !updated {
|
||||||
|
http.Error(w, "task not found", http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out, _ := json.MarshalIndent(tasks, "", " ")
|
||||||
|
if err := os.WriteFile(tasksPath, out, 0644); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(strings.TrimSpace(s.workspacePath), "memory", "task-audit.jsonl")
|
||||||
limit := 100
|
limit := 100
|
||||||
if v := r.URL.Query().Get("limit"); v != "" {
|
if v := r.URL.Query().Get("limit"); v != "" {
|
||||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ const resources = {
|
|||||||
lastPauseAt: 'Last Pause Time',
|
lastPauseAt: 'Last Pause Time',
|
||||||
allSources: 'All Sources',
|
allSources: 'All Sources',
|
||||||
allStatus: 'All Status',
|
allStatus: 'All Status',
|
||||||
|
pauseTask: 'Pause',
|
||||||
|
retryTask: 'Retry',
|
||||||
|
completeTask: 'Complete',
|
||||||
|
ignoreTask: 'Ignore',
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
noTaskAudit: 'No task audit records',
|
noTaskAudit: 'No task audit records',
|
||||||
selectTask: 'Select a task from the left list',
|
selectTask: 'Select a task from the left list',
|
||||||
@@ -180,6 +184,10 @@ const resources = {
|
|||||||
lastPauseAt: '最近暂停时间',
|
lastPauseAt: '最近暂停时间',
|
||||||
allSources: '全部来源',
|
allSources: '全部来源',
|
||||||
allStatus: '全部状态',
|
allStatus: '全部状态',
|
||||||
|
pauseTask: '暂停',
|
||||||
|
retryTask: '重试',
|
||||||
|
completeTask: '完成',
|
||||||
|
ignoreTask: '忽略',
|
||||||
error: '错误',
|
error: '错误',
|
||||||
noTaskAudit: '暂无任务审计记录',
|
noTaskAudit: '暂无任务审计记录',
|
||||||
selectTask: '请从左侧选择任务',
|
selectTask: '请从左侧选择任务',
|
||||||
|
|||||||
@@ -61,6 +61,18 @@ const TaskAudit: React.FC = () => {
|
|||||||
if (statusFilter !== 'all' && String(it.status || '-') !== statusFilter) return false;
|
if (statusFilter !== 'all' && String(it.status || '-') !== statusFilter) return false;
|
||||||
return true;
|
return true;
|
||||||
}), [items, sourceFilter, statusFilter]);
|
}), [items, sourceFilter, statusFilter]);
|
||||||
|
|
||||||
|
const taskAction = async (action: 'pause'|'retry'|'complete'|'ignore') => {
|
||||||
|
if (!selected?.task_id) return;
|
||||||
|
try {
|
||||||
|
const url = `/webui/api/task_queue${q}`;
|
||||||
|
const r = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, task_id: selected.task_id }) });
|
||||||
|
if (!r.ok) throw new Error(await r.text());
|
||||||
|
await fetchData();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
const selectedPretty = useMemo(() => selected ? JSON.stringify(selected, null, 2) : '', [selected]);
|
const selectedPretty = useMemo(() => selected ? JSON.stringify(selected, null, 2) : '', [selected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -114,6 +126,14 @@ const TaskAudit: React.FC = () => {
|
|||||||
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 overflow-hidden flex flex-col min-h-0">
|
<div className="border border-zinc-800 rounded-xl bg-zinc-900/40 overflow-hidden flex flex-col min-h-0">
|
||||||
<div className="px-3 py-2 border-b border-zinc-800 text-xs text-zinc-400 uppercase tracking-wider">{t('taskDetail')}</div>
|
<div className="px-3 py-2 border-b border-zinc-800 text-xs text-zinc-400 uppercase tracking-wider">{t('taskDetail')}</div>
|
||||||
<div className="p-4 overflow-y-auto min-h-0 space-y-3 text-sm">
|
<div className="p-4 overflow-y-auto min-h-0 space-y-3 text-sm">
|
||||||
|
{selected && (
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<button onClick={()=>taskAction('pause')} className="px-2 py-1 text-xs rounded bg-amber-700/70 hover:bg-amber-600">{t('pauseTask')}</button>
|
||||||
|
<button onClick={()=>taskAction('retry')} className="px-2 py-1 text-xs rounded bg-indigo-700/70 hover:bg-indigo-600">{t('retryTask')}</button>
|
||||||
|
<button onClick={()=>taskAction('complete')} className="px-2 py-1 text-xs rounded bg-emerald-700/70 hover:bg-emerald-600">{t('completeTask')}</button>
|
||||||
|
<button onClick={()=>taskAction('ignore')} className="px-2 py-1 text-xs rounded bg-zinc-700 hover:bg-zinc-600">{t('ignoreTask')}</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{!selected ? (
|
{!selected ? (
|
||||||
<div className="text-zinc-500">{t('selectTask')}</div>
|
<div className="text-zinc-500">{t('selectTask')}</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user