mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-30 10:03:50 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21f68a6a60 | ||
|
|
255e11aca6 | ||
|
|
6ad055d24f | ||
|
|
571b6116e0 | ||
|
|
df9d38af0d |
No files matched your search
@@ -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 {
|
||||
|
||||
+17
-16
@@ -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 {
|
||||
|
||||
@@ -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).
|
||||
|
||||
+25
-1
@@ -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`)
|
||||
|
||||
|
||||
+11
-4
@@ -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()
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import { usePathname } from "next/navigation";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
import { CpuIcon, HardDriveIcon, MemoryStickIcon } from "lucide-react";
|
||||
import { useCurrentUser } from "@/hooks/use-current-user";
|
||||
import { api } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { SystemMetrics } from "@/lib/types";
|
||||
|
||||
import { EgressIpNavbar, EgressIpProvider } from "./egress-ip-banner";
|
||||
import { LiveClock } from "./live-clock";
|
||||
@@ -78,7 +76,6 @@ function MainContentInner({ children }: { children: ReactNode }) {
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<EgressIpNavbar />
|
||||
<SystemResourceNavbar />
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<LiveClock />
|
||||
@@ -98,42 +95,3 @@ function MainContentInner({ children }: { children: ReactNode }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SystemResourceNavbar() {
|
||||
const [metrics, setMetrics] = useState<SystemMetrics | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const next = await api.systemMetrics();
|
||||
if (alive) setMetrics(next);
|
||||
} catch {
|
||||
/* Keep the status bar quiet while the API is unavailable. */
|
||||
}
|
||||
};
|
||||
void load();
|
||||
const timer = setInterval(load, 5000);
|
||||
return () => { alive = false; clearInterval(timer); };
|
||||
}, []);
|
||||
|
||||
if (!metrics) return null;
|
||||
if (!metrics.supported) {
|
||||
return <div className="flex items-center justify-center text-xs text-muted-foreground" role="status">资源状态:暂不支持</div>;
|
||||
}
|
||||
const items = [
|
||||
[CpuIcon, "CPU", metrics.cpu.percent],
|
||||
[MemoryStickIcon, "内存", metrics.memory.percent],
|
||||
[HardDriveIcon, "磁盘", metrics.data_disk.percent],
|
||||
] as const;
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground" role="status" aria-live="polite">
|
||||
<span>容器资源</span>
|
||||
{items.map(([Icon, label, value]) => (
|
||||
<span key={label} className="inline-flex items-center gap-1 tabular-nums" title={`${label} ${value.toFixed(1)}%`}>
|
||||
<Icon className="size-3" aria-hidden="true" /> {label} {value.toFixed(1)}%
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
ArrowUpRightIcon,
|
||||
BugIcon,
|
||||
ClockIcon,
|
||||
CpuIcon,
|
||||
HardDriveIcon,
|
||||
MemoryStickIcon,
|
||||
NetworkIcon,
|
||||
ShieldCheckIcon,
|
||||
TargetIcon,
|
||||
@@ -38,6 +41,7 @@ import type {
|
||||
Tool,
|
||||
TrafficExchange,
|
||||
UsageStats,
|
||||
SystemMetrics,
|
||||
} from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -101,6 +105,35 @@ function statusBg(code: number): string {
|
||||
return "bg-red-400";
|
||||
}
|
||||
|
||||
function SystemResourceStatus({ metrics }: { metrics: SystemMetrics | null }) {
|
||||
if (!metrics) return null;
|
||||
if (!metrics.supported) {
|
||||
return <div className="ml-auto text-xs text-muted-foreground" role="status">资源状态:暂不支持</div>;
|
||||
}
|
||||
const items = [
|
||||
[CpuIcon, "CPU", metrics.cpu.percent],
|
||||
[MemoryStickIcon, "内存", metrics.memory.percent],
|
||||
[HardDriveIcon, "磁盘", metrics.data_disk.percent],
|
||||
] as const;
|
||||
return (
|
||||
<div className="ml-auto flex min-w-0 items-center justify-end gap-3 text-xs text-muted-foreground" role="status" aria-live="polite">
|
||||
<span className="shrink-0 font-medium">容器资源</span>
|
||||
{items.map(([Icon, label, value]) => (
|
||||
<div key={label} className="w-24 min-w-0" title={`${label} ${value.toFixed(1)}%`}>
|
||||
<div className="mb-1 flex items-center justify-between gap-1 text-[10px]">
|
||||
<span className="inline-flex items-center gap-1"><Icon className="size-3" aria-hidden="true" />{label}</span>
|
||||
<span className="tabular-nums">{value.toFixed(1)}%</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={value}
|
||||
className="h-2 bg-muted [&_[data-slot=progress-indicator]]:bg-slate-500 dark:[&_[data-slot=progress-indicator]]:bg-slate-400"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── sub-components ────────────────────────────────────────────────────────────
|
||||
|
||||
function LiveDot({ className }: { className?: string }) {
|
||||
@@ -147,6 +180,22 @@ export default function DashboardPage() {
|
||||
const [skills, setSkills] = React.useState<SkillItem[]>([]);
|
||||
const [tools, setTools] = React.useState<Tool[]>([]);
|
||||
const [llmProfiles, setLLMProfiles] = React.useState<LLMProfile[]>([]);
|
||||
const [systemMetrics, setSystemMetrics] = React.useState<SystemMetrics | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
let alive = true;
|
||||
const load = async () => {
|
||||
try {
|
||||
const next = await api.systemMetrics();
|
||||
if (alive) setSystemMetrics(next);
|
||||
} catch {
|
||||
/* retry on the next interval */
|
||||
}
|
||||
};
|
||||
void load();
|
||||
const timer = setInterval(load, 5000);
|
||||
return () => { alive = false; clearInterval(timer); };
|
||||
}, []);
|
||||
|
||||
// The task list is the most expensive dashboard source. Poll it independently
|
||||
// so a large history cannot hold back every other dashboard panel.
|
||||
@@ -495,10 +544,13 @@ export default function DashboardPage() {
|
||||
<div className="flex flex-1 flex-col gap-4 pb-6">
|
||||
{/* ── Header ── */}
|
||||
<div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold tracking-tight">总览</h1>
|
||||
<p className="text-xs text-muted-foreground">系统全局状态 · 实时刷新</p>
|
||||
</div>
|
||||
<SystemResourceStatus metrics={systemMetrics} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Row 1: 5 stat cards ── */}
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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({
|
||||
<SideQuestionButton side={side} />
|
||||
</div>
|
||||
<CodeBlock
|
||||
label={`${current.agent_name || current.conv_agent_key || "Agent"} · 工具请求`}
|
||||
label={`${agentLabel(current)} · 工具请求`}
|
||||
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
|
||||
/>
|
||||
{command ? (
|
||||
@@ -726,8 +748,8 @@ function ApprovalTable({
|
||||
<span className="truncate" title={originLabel(row)}>
|
||||
<ApprovalOrigin row={row} />
|
||||
</span>
|
||||
<span className="truncate text-muted-foreground text-xs">
|
||||
{row.agent_name || row.conv_agent_key}
|
||||
<span className="truncate text-muted-foreground text-xs" title={agentLabel(row)}>
|
||||
Agent:{agentLabel(row)}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user