From 6ad055d24f3ac890f19df91fbc7564b7032d1023 Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:12:36 +0800 Subject: [PATCH 1/3] fix(web): enable repeater request undo --- web/src/components/packet-code-editor.tsx | 49 ++++++++++++++--------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/web/src/components/packet-code-editor.tsx b/web/src/components/packet-code-editor.tsx index b201d97..f3bf230 100644 --- a/web/src/components/packet-code-editor.tsx +++ b/web/src/components/packet-code-editor.tsx @@ -2,14 +2,8 @@ import * as React from "react"; import { EditorState } from "@codemirror/state"; -import { - drawSelection, - EditorView, - highlightActiveLine, - keymap, - lineNumbers, -} from "@codemirror/view"; -import { defaultKeymap, indentWithTab } from "@codemirror/commands"; +import { drawSelection, EditorView, highlightActiveLine, keymap, lineNumbers } from "@codemirror/view"; +import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands"; import { defaultHighlightStyle, HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language"; import { tags } from "@lezer/highlight"; @@ -37,17 +31,31 @@ const httpLanguage = StreamLanguage.define({ }, }); -const packetTheme = EditorView.theme({ - "&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" }, - ".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono)", fontSize: "0.75rem", fontWeight: "500", lineHeight: "1.65rem" }, - ".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" }, - ".cm-gutters": { backgroundColor: "transparent", color: "var(--muted-foreground)", border: "0", borderRight: "1px solid var(--border)" }, - ".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" }, - ".cm-activeLine": { backgroundColor: "transparent" }, - ".cm-activeLineGutter": { backgroundColor: "transparent" }, - ".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" }, - ".cm-cursor": { borderLeftColor: "#222" }, -}, { dark: false }); +const packetTheme = EditorView.theme( + { + "&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" }, + ".cm-scroller": { + overflow: "auto", + fontFamily: "var(--font-mono)", + fontSize: "0.75rem", + fontWeight: "500", + lineHeight: "1.65rem", + }, + ".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" }, + ".cm-gutters": { + backgroundColor: "transparent", + color: "var(--muted-foreground)", + border: "0", + borderRight: "1px solid var(--border)", + }, + ".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" }, + ".cm-activeLine": { backgroundColor: "transparent" }, + ".cm-activeLineGutter": { backgroundColor: "transparent" }, + ".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" }, + ".cm-cursor": { borderLeftColor: "#222" }, + }, + { dark: false }, +); export function PacketCodeEditor({ value, @@ -76,7 +84,8 @@ export function PacketCodeEditor({ lineNumbers(), drawSelection(), highlightActiveLine(), - keymap.of([...defaultKeymap, indentWithTab]), + history(), + keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]), httpLanguage, syntaxHighlighting(defaultHighlightStyle), syntaxHighlighting(packetHighlight), From 255e11aca6858d1d759a7844baca341389f1365f Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:01:57 +0800 Subject: [PATCH 2/3] feat(web): show agent source in approval records --- web/src/components/approval-records.tsx | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/web/src/components/approval-records.tsx b/web/src/components/approval-records.tsx index c947fc1..646a7b9 100644 --- a/web/src/components/approval-records.tsx +++ b/web/src/components/approval-records.tsx @@ -65,6 +65,28 @@ function originLabel(row: InterceptApprovalRow) { return "—"; } +// Background task runs carry a pool label such as "work#2 · #2770", while +// conversation-backed runs carry the agent key (for example "reporter"). +// Present a stable, human-readable agent label in both cases. +function agentLabel(row: InterceptApprovalRow) { + const key = (row.conv_agent_key || "").trim().toLowerCase(); + const labels: Record = { + auto: "自动 Agent", + mainagent: "主 Agent", + planner: "规划 Agent", + worker: "执行 Worker", + reporter: "报告编写", + retester: "复测 Agent", + pentest: "渗透测试 Agent", + }; + if (key && labels[key]) return labels[key]; + const raw = (row.agent_name || "").trim(); + if (/^work#\d+(?:\s|$)/i.test(raw)) return "执行 Worker"; + const rawKey = raw.toLowerCase(); + if (labels[rawKey]) return labels[rawKey]; + return raw || "未知 Agent"; +} + function ApprovalOrigin({ row, detail = false }: { row: InterceptApprovalRow; detail?: boolean }) { const [locating, setLocating] = React.useState(false); const label = detail && row.task_id ? `任务 ${row.task_id}` : originLabel(row); @@ -454,7 +476,7 @@ export function ApprovalDetail({ {command ? ( @@ -726,8 +748,8 @@ function ApprovalTable({ - - {row.agent_name || row.conv_agent_key} + + Agent:{agentLabel(row)} From 21f68a6a606683adc38aad3d93fa1ffa711ad8a9 Mon Sep 17 00:00:00 2001 From: ji ruo <79234113+RuoJi6@users.noreply.github.com> Date: Fri, 30 Oct 2026 09:18:31 +0800 Subject: [PATCH 3/3] feat: event-driven credential processing conversations --- agent/intelligence.go | 1 + db/credentials.go | 33 +++++++++++++++++---------------- server/assembly.go | 7 +++++++ server/credentials.go | 26 +++++++++++++++++++++++++- server/server.go | 15 +++++++++++---- 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/agent/intelligence.go b/agent/intelligence.go index a407475..cabd599 100644 --- a/agent/intelligence.go +++ b/agent/intelligence.go @@ -12,6 +12,7 @@ import ( // Missing host/setting means off. Rechecked on every write for running sessions. var IntelligenceEnabled func() bool var IntelligenceRecord func(context.Context, int64, int64, string, db.IntelligenceInput) (int64, error) +var CredentialRecorded func() func intelligenceEnabled() bool { return IntelligenceEnabled != nil && IntelligenceEnabled() } func intelligenceSchema() map[string]any { diff --git a/db/credentials.go b/db/credentials.go index b6abd80..a63cfa0 100644 --- a/db/credentials.go +++ b/db/credentials.go @@ -39,22 +39,23 @@ type CredentialTraffic struct { Accounts []int `json:"accounts"` } type CredentialAnalysis struct { - ForceExtract bool `json:"force_extract,omitempty"` - TrafficNote string `json:"traffic_note,omitempty"` - State string `json:"state"` - Lease string `json:"lease,omitempty"` - LeaseUntil time.Time `json:"lease_until,omitempty"` - SourceHash string `json:"source_hash,omitempty"` - Method string `json:"method,omitempty"` - Accounts []CredentialAccount `json:"accounts"` - Traffic []CredentialTraffic `json:"traffic"` - TrafficState string `json:"traffic_state"` - Note string `json:"note,omitempty"` - Error string `json:"error,omitempty"` - Model string `json:"model,omitempty"` - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` - ProcessedAt *time.Time `json:"processed_at,omitempty"` + ConversationID int64 `json:"conversation_id,omitempty"` + ForceExtract bool `json:"force_extract,omitempty"` + TrafficNote string `json:"traffic_note,omitempty"` + State string `json:"state"` + Lease string `json:"lease,omitempty"` + LeaseUntil time.Time `json:"lease_until,omitempty"` + SourceHash string `json:"source_hash,omitempty"` + Method string `json:"method,omitempty"` + Accounts []CredentialAccount `json:"accounts"` + Traffic []CredentialTraffic `json:"traffic"` + TrafficState string `json:"traffic_state"` + Note string `json:"note,omitempty"` + Error string `json:"error,omitempty"` + Model string `json:"model,omitempty"` + InputTokens int `json:"input_tokens"` + OutputTokens int `json:"output_tokens"` + ProcessedAt *time.Time `json:"processed_at,omitempty"` } func CredentialSourceHash(content string) string { diff --git a/server/assembly.go b/server/assembly.go index 32bf3d5..5e048ef 100644 --- a/server/assembly.go +++ b/server/assembly.go @@ -233,6 +233,13 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) { if err != nil { return 0, err } + if item.Kind == "credential" { + // The durable database row is the source of truth; this wake-up only + // reduces latency. The recovery ticker handles missed notifications. + if agent.CredentialRecorded != nil { + agent.CredentialRecorded() + } + } return item.ID, nil } // Seed the built-in domain tools (first-insert only; DO NOTHING preserves edits). diff --git a/server/credentials.go b/server/credentials.go index fbea3bb..bbac994 100644 --- a/server/credentials.go +++ b/server/credentials.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/json" "io" "log" "net/http" @@ -41,13 +42,16 @@ func (s *Server) seedCredentialProcessor() { _ = s.m.pg.SetBool(flag, true) } func (s *Server) runCredentialProcessor() { - ticker := time.NewTicker(3 * time.Second) + // Events wake the worker immediately. The slow ticker is only a recovery + // scan for rows created before a notification or after a process restart. + ticker := time.NewTicker(30 * time.Second) defer ticker.Stop() for { select { case <-s.ctx.Done(): return case <-ticker.C: + case <-s.credentialWake: } if !s.m.pg.GetBool(db.SettingTaskIntelligence, false) || !s.m.pg.GetBool(db.SettingCredentialAgent, false) { continue @@ -93,6 +97,14 @@ func (s *Server) processCredentialJob(parent context.Context, item *db.Intellige } }() result := *item.Credentials + conversationID := result.ConversationID + if conversationID == 0 { + if c, err := s.m.pg.CreateConversation(db.CredentialAgentKey, "凭据整理 · "+firstLine(item.Title, 42), nil); err == nil { + conversationID = c.ID + result.ConversationID = c.ID + _, _ = s.m.pg.AppendConvActivity(c.ID, db.Activity{Worker: db.CredentialAgentKey, Kind: "user", Summary: "收到凭据上报,开始整理", Detail: item.Content}) + } + } needsExtract := result.ForceExtract || result.SourceHash != db.CredentialSourceHash(item.Content) || result.Method == "" || (result.Error != "" && len(result.Accounts) == 0) result.State = "ready" result.ForceExtract = false @@ -185,8 +197,20 @@ func (s *Server) processCredentialJob(parent context.Context, item *db.Intellige if err := s.m.pg.FinishCredentialJob(finish, item, result); err != nil { log.Printf("[credentials] save record #%d: %v", item.ID, err) } + if conversationID > 0 { + detail, _ := json.Marshal(result) + kind, summary := "result", "凭据整理完成" + isError := result.State == "error" + if isError { + kind, summary = "text", "凭据整理失败:"+result.Error + } + _, _ = s.m.pg.AppendConvActivity(conversationID, db.Activity{Worker: db.CredentialAgentKey, Kind: kind, IsError: isError, Summary: summary, Detail: string(detail), InputTokens: intPtr(result.InputTokens), OutputTokens: intPtr(result.OutputTokens)}) + _ = s.m.pg.TouchConversation(conversationID) + } } +func intPtr(v int) *int { return &v } + var credentialURLs = regexp.MustCompile(`https?://[^\s<>"',;))]+`) var credentialTrafficIDs = regexp.MustCompile(`\b[0-9]{8,}-[0-9]{3,}\b`) diff --git a/server/server.go b/server/server.go index 2cd3610..8a22522 100644 --- a/server/server.go +++ b/server/server.go @@ -125,9 +125,10 @@ type Server struct { // Cold task archives run through one persistent FIFO worker. The buffered wake // channel coalesces enqueue bursts; the database remains the source of truth. - archiveWake chan struct{} - archiveWG sync.WaitGroup - side *sideQuestionState + archiveWake chan struct{} + archiveWG sync.WaitGroup + credentialWake chan struct{} + side *sideQuestionState } // provEntry is a cached provider + its config for one LLM profile id. @@ -161,7 +162,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi triggerActive: map[string]int{}, triggerCfg: map[string]triggerBehavior{}, profChatAgents: map[int64]*agent.ChatAgent{}, provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg), - taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1)} + taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1), credentialWake: make(chan struct{}, 1)} s.initSideQuestions() if m.pg != nil { if err := s.basicAuth.load(m.pg); err != nil { @@ -282,6 +283,12 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi go s.reconcileConcurrency() s.startTaskArchiveWorker() if m.pg != nil { + agent.CredentialRecorded = func() { + select { + case s.credentialWake <- struct{}{}: + default: + } + } s.seedCredentialProcessor() go s.runCredentialProcessor() }