mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-11-03 19:23:51 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a44882be6 | ||
|
|
4cf8844b6a | ||
|
|
46b5f4a5e2 | ||
|
|
b8ac6f7777 |
No files matched your search
@@ -12,7 +12,6 @@ import (
|
|||||||
// Missing host/setting means off. Rechecked on every write for running sessions.
|
// Missing host/setting means off. Rechecked on every write for running sessions.
|
||||||
var IntelligenceEnabled func() bool
|
var IntelligenceEnabled func() bool
|
||||||
var IntelligenceRecord func(context.Context, int64, int64, string, db.IntelligenceInput) (int64, error)
|
var IntelligenceRecord func(context.Context, int64, int64, string, db.IntelligenceInput) (int64, error)
|
||||||
var CredentialRecorded func()
|
|
||||||
|
|
||||||
func intelligenceEnabled() bool { return IntelligenceEnabled != nil && IntelligenceEnabled() }
|
func intelligenceEnabled() bool { return IntelligenceEnabled != nil && IntelligenceEnabled() }
|
||||||
func intelligenceSchema() map[string]any {
|
func intelligenceSchema() map[string]any {
|
||||||
|
|||||||
+16
-17
@@ -39,23 +39,22 @@ type CredentialTraffic struct {
|
|||||||
Accounts []int `json:"accounts"`
|
Accounts []int `json:"accounts"`
|
||||||
}
|
}
|
||||||
type CredentialAnalysis struct {
|
type CredentialAnalysis struct {
|
||||||
ConversationID int64 `json:"conversation_id,omitempty"`
|
ForceExtract bool `json:"force_extract,omitempty"`
|
||||||
ForceExtract bool `json:"force_extract,omitempty"`
|
TrafficNote string `json:"traffic_note,omitempty"`
|
||||||
TrafficNote string `json:"traffic_note,omitempty"`
|
State string `json:"state"`
|
||||||
State string `json:"state"`
|
Lease string `json:"lease,omitempty"`
|
||||||
Lease string `json:"lease,omitempty"`
|
LeaseUntil time.Time `json:"lease_until,omitempty"`
|
||||||
LeaseUntil time.Time `json:"lease_until,omitempty"`
|
SourceHash string `json:"source_hash,omitempty"`
|
||||||
SourceHash string `json:"source_hash,omitempty"`
|
Method string `json:"method,omitempty"`
|
||||||
Method string `json:"method,omitempty"`
|
Accounts []CredentialAccount `json:"accounts"`
|
||||||
Accounts []CredentialAccount `json:"accounts"`
|
Traffic []CredentialTraffic `json:"traffic"`
|
||||||
Traffic []CredentialTraffic `json:"traffic"`
|
TrafficState string `json:"traffic_state"`
|
||||||
TrafficState string `json:"traffic_state"`
|
Note string `json:"note,omitempty"`
|
||||||
Note string `json:"note,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
Error string `json:"error,omitempty"`
|
Model string `json:"model,omitempty"`
|
||||||
Model string `json:"model,omitempty"`
|
InputTokens int `json:"input_tokens"`
|
||||||
InputTokens int `json:"input_tokens"`
|
OutputTokens int `json:"output_tokens"`
|
||||||
OutputTokens int `json:"output_tokens"`
|
ProcessedAt *time.Time `json:"processed_at,omitempty"`
|
||||||
ProcessedAt *time.Time `json:"processed_at,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func CredentialSourceHash(content string) string {
|
func CredentialSourceHash(content string) string {
|
||||||
|
|||||||
@@ -233,13 +233,6 @@ func wireTools(pg *db.DB, domainReg map[string]actool.CoreTool) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
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
|
return item.ID, nil
|
||||||
}
|
}
|
||||||
// Seed the built-in domain tools (first-insert only; DO NOTHING preserves edits).
|
// Seed the built-in domain tools (first-insert only; DO NOTHING preserves edits).
|
||||||
|
|||||||
+1
-25
@@ -2,7 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -42,16 +41,13 @@ func (s *Server) seedCredentialProcessor() {
|
|||||||
_ = s.m.pg.SetBool(flag, true)
|
_ = s.m.pg.SetBool(flag, true)
|
||||||
}
|
}
|
||||||
func (s *Server) runCredentialProcessor() {
|
func (s *Server) runCredentialProcessor() {
|
||||||
// Events wake the worker immediately. The slow ticker is only a recovery
|
ticker := time.NewTicker(3 * time.Second)
|
||||||
// scan for rows created before a notification or after a process restart.
|
|
||||||
ticker := time.NewTicker(30 * time.Second)
|
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-s.ctx.Done():
|
case <-s.ctx.Done():
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
case <-s.credentialWake:
|
|
||||||
}
|
}
|
||||||
if !s.m.pg.GetBool(db.SettingTaskIntelligence, false) || !s.m.pg.GetBool(db.SettingCredentialAgent, false) {
|
if !s.m.pg.GetBool(db.SettingTaskIntelligence, false) || !s.m.pg.GetBool(db.SettingCredentialAgent, false) {
|
||||||
continue
|
continue
|
||||||
@@ -97,14 +93,6 @@ func (s *Server) processCredentialJob(parent context.Context, item *db.Intellige
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
result := *item.Credentials
|
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)
|
needsExtract := result.ForceExtract || result.SourceHash != db.CredentialSourceHash(item.Content) || result.Method == "" || (result.Error != "" && len(result.Accounts) == 0)
|
||||||
result.State = "ready"
|
result.State = "ready"
|
||||||
result.ForceExtract = false
|
result.ForceExtract = false
|
||||||
@@ -197,20 +185,8 @@ func (s *Server) processCredentialJob(parent context.Context, item *db.Intellige
|
|||||||
if err := s.m.pg.FinishCredentialJob(finish, item, result); err != nil {
|
if err := s.m.pg.FinishCredentialJob(finish, item, result); err != nil {
|
||||||
log.Printf("[credentials] save record #%d: %v", item.ID, err)
|
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 credentialURLs = regexp.MustCompile(`https?://[^\s<>"',;))]+`)
|
||||||
var credentialTrafficIDs = regexp.MustCompile(`\b[0-9]{8,}-[0-9]{3,}\b`)
|
var credentialTrafficIDs = regexp.MustCompile(`\b[0-9]{8,}-[0-9]{3,}\b`)
|
||||||
|
|
||||||
|
|||||||
@@ -474,11 +474,6 @@ func (s *Server) toolUpdateFindingReport() actool.CoreTool {
|
|||||||
if err := s.checkCaseReviewScope(ctx, "update_finding_report", findingCaseRequest{FindingID: json.RawMessage(i64s(fid))}); err != nil {
|
if err := s.checkCaseReviewScope(ctx, "update_finding_report", findingCaseRequest{FindingID: json.RawMessage(i64s(fid))}); err != nil {
|
||||||
return actool.Errorf(err.Error()), nil
|
return actool.Errorf(err.Error()), nil
|
||||||
}
|
}
|
||||||
if f, err := s.m.pg.GetFinding(fid); err == nil && f != nil && f.TaskID != nil {
|
|
||||||
if err := s.archiveFindingScreenshots(*f.TaskID, a.Report); err != nil {
|
|
||||||
return actool.Errorf("归档报告截图失败: " + err.Error()), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
n, err := s.m.pg.SetFindingReportVersionByNodeID(ctx, nodeID, a.Report, a.EvidenceVersion)
|
n, err := s.m.pg.SetFindingReportVersionByNodeID(ctx, nodeID, a.Report, a.EvidenceVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+4
-11
@@ -125,10 +125,9 @@ type Server struct {
|
|||||||
|
|
||||||
// Cold task archives run through one persistent FIFO worker. The buffered wake
|
// Cold task archives run through one persistent FIFO worker. The buffered wake
|
||||||
// channel coalesces enqueue bursts; the database remains the source of truth.
|
// channel coalesces enqueue bursts; the database remains the source of truth.
|
||||||
archiveWake chan struct{}
|
archiveWake chan struct{}
|
||||||
archiveWG sync.WaitGroup
|
archiveWG sync.WaitGroup
|
||||||
credentialWake chan struct{}
|
side *sideQuestionState
|
||||||
side *sideQuestionState
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// provEntry is a cached provider + its config for one LLM profile id.
|
// provEntry is a cached provider + its config for one LLM profile id.
|
||||||
@@ -162,7 +161,7 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
|
|||||||
triggerActive: map[string]int{}, triggerCfg: map[string]triggerBehavior{},
|
triggerActive: map[string]int{}, triggerCfg: map[string]triggerBehavior{},
|
||||||
profChatAgents: map[int64]*agent.ChatAgent{},
|
profChatAgents: map[int64]*agent.ChatAgent{},
|
||||||
provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg),
|
provByProfile: map[int64]*provEntry{}, llmHealth: newLLMHealthRegistry(m.pg),
|
||||||
taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1), credentialWake: make(chan struct{}, 1)}
|
taskAgents: map[string]*taskAgentBundle{}, archiveWake: make(chan struct{}, 1)}
|
||||||
s.initSideQuestions()
|
s.initSideQuestions()
|
||||||
if m.pg != nil {
|
if m.pg != nil {
|
||||||
if err := s.basicAuth.load(m.pg); err != nil {
|
if err := s.basicAuth.load(m.pg); err != nil {
|
||||||
@@ -283,12 +282,6 @@ func New(ctx context.Context, m *Manager, skillDir string, dataDir string, keyDi
|
|||||||
go s.reconcileConcurrency()
|
go s.reconcileConcurrency()
|
||||||
s.startTaskArchiveWorker()
|
s.startTaskArchiveWorker()
|
||||||
if m.pg != nil {
|
if m.pg != nil {
|
||||||
agent.CredentialRecorded = func() {
|
|
||||||
select {
|
|
||||||
case s.credentialWake <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.seedCredentialProcessor()
|
s.seedCredentialProcessor()
|
||||||
go s.runCredentialProcessor()
|
go s.runCredentialProcessor()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -310,67 +309,6 @@ func (s *Server) findSessionPreview(rawPath string) (string, bool) {
|
|||||||
return newest, newest != ""
|
return newest, newest != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// archiveFindingScreenshots materializes report-referenced screenshots in the
|
|
||||||
// task workspace. Browser runs use per-session working directories, while
|
|
||||||
// reports intentionally use the portable findings/screenshots/ path.
|
|
||||||
func (s *Server) archiveFindingScreenshots(taskID int64, markdown string) error {
|
|
||||||
if taskID <= 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, match := range screenshotMarkdownLink.FindAllStringSubmatch(markdown, -1) {
|
|
||||||
raw := strings.TrimSpace(match[2])
|
|
||||||
rel := filepath.Clean(strings.TrimPrefix(raw, "/"))
|
|
||||||
const prefix = "findings" + string(os.PathSeparator) + "screenshots" + string(os.PathSeparator)
|
|
||||||
if !strings.HasPrefix(rel, prefix) || strings.Contains(rel, ".."+string(os.PathSeparator)) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
target := filepath.Join(s.m.dir, "tasks", fmt.Sprint(taskID), rel)
|
|
||||||
if info, err := os.Stat(target); err == nil && !info.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
source, ok := s.resolveSessionScreenshot(rel)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
in, err := os.Open(source)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
|
||||||
if err == nil {
|
|
||||||
_, err = io.Copy(out, in)
|
|
||||||
}
|
|
||||||
_ = in.Close()
|
|
||||||
_ = out.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) resolveSessionScreenshot(rel string) (string, bool) {
|
|
||||||
matches, err := filepath.Glob(filepath.Join(filepath.Clean(s.m.dir), "sessions", "*", rel))
|
|
||||||
if err != nil || len(matches) == 0 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
var newest string
|
|
||||||
var newestTime int64
|
|
||||||
for _, candidate := range matches {
|
|
||||||
info, statErr := os.Stat(candidate)
|
|
||||||
if statErr != nil || info.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if newest == "" || info.ModTime().UnixNano() > newestTime {
|
|
||||||
newest, newestTime = candidate, info.ModTime().UnixNano()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newest, newest != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/workspace/upload?path=<dir> — multipart form field "file" (one or more).
|
// POST /api/workspace/upload?path=<dir> — multipart form field "file" (one or more).
|
||||||
func (s *Server) wsUpload(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) wsUpload(w http.ResponseWriter, r *http.Request) {
|
||||||
dirAbs, ok := s.wsResolve(r.URL.Query().Get("path"))
|
dirAbs, ok := s.wsResolve(r.URL.Query().Get("path"))
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ function FindingDetailInner() {
|
|||||||
const [tab, setTab] = React.useState("overview");
|
const [tab, setTab] = React.useState("overview");
|
||||||
const [reportGenerating, setReportGenerating] = React.useState(false);
|
const [reportGenerating, setReportGenerating] = React.useState(false);
|
||||||
const [autoChatOpen, setAutoChatOpen] = React.useState(false);
|
const [autoChatOpen, setAutoChatOpen] = React.useState(false);
|
||||||
const [reportChatOpen, setReportChatOpen] = React.useState(false);
|
|
||||||
const title = finding?.name || finding?.vulnclass || "未分类";
|
const title = finding?.name || finding?.vulnclass || "未分类";
|
||||||
|
|
||||||
const load = React.useCallback(() => {
|
const load = React.useCallback(() => {
|
||||||
@@ -191,12 +190,6 @@ function FindingDetailInner() {
|
|||||||
<BotIcon data-icon="inline-start" />
|
<BotIcon data-icon="inline-start" />
|
||||||
询问 Auto Agent
|
询问 Auto Agent
|
||||||
</Button>
|
</Button>
|
||||||
{!finding.inherited ? (
|
|
||||||
<Button variant="outline" size="sm" onClick={() => setReportChatOpen(true)} title="打开当前漏洞报告撰写对话">
|
|
||||||
<FileTextIcon data-icon="inline-start" />
|
|
||||||
调整报告
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
{finding.inherited && finding.source_task_id && (
|
{finding.inherited && finding.source_task_id && (
|
||||||
<Badge variant="outline">来源任务 #{finding.source_task_id} · 只读</Badge>
|
<Badge variant="outline">来源任务 #{finding.source_task_id} · 只读</Badge>
|
||||||
)}
|
)}
|
||||||
@@ -417,16 +410,6 @@ function FindingDetailInner() {
|
|||||||
open={autoChatOpen}
|
open={autoChatOpen}
|
||||||
onOpenChange={setAutoChatOpen}
|
onOpenChange={setAutoChatOpen}
|
||||||
/>
|
/>
|
||||||
<FindingAutoChat
|
|
||||||
findingId={finding.id}
|
|
||||||
findingTitle={title}
|
|
||||||
agentKey="reporter"
|
|
||||||
agentLabel="报告撰写 Agent"
|
|
||||||
promptPlaceholder="描述要如何调整漏洞报告…"
|
|
||||||
modal
|
|
||||||
open={reportChatOpen}
|
|
||||||
onOpenChange={setReportChatOpen}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -923,7 +923,7 @@ export default function FindingsPage() {
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scrollbar-none flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
|
<div className="flex min-w-0 flex-nowrap items-center gap-2 overflow-x-auto pb-1">
|
||||||
<InputGroup className="w-72 shrink-0">
|
<InputGroup className="w-72 shrink-0">
|
||||||
<InputGroupInput
|
<InputGroupInput
|
||||||
type="search"
|
type="search"
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -65,28 +65,6 @@ function originLabel(row: InterceptApprovalRow) {
|
|||||||
return "—";
|
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 }) {
|
function ApprovalOrigin({ row, detail = false }: { row: InterceptApprovalRow; detail?: boolean }) {
|
||||||
const [locating, setLocating] = React.useState(false);
|
const [locating, setLocating] = React.useState(false);
|
||||||
const label = detail && row.task_id ? `任务 ${row.task_id}` : originLabel(row);
|
const label = detail && row.task_id ? `任务 ${row.task_id}` : originLabel(row);
|
||||||
@@ -476,7 +454,7 @@ export function ApprovalDetail({
|
|||||||
<SideQuestionButton side={side} />
|
<SideQuestionButton side={side} />
|
||||||
</div>
|
</div>
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
label={`${agentLabel(current)} · 工具请求`}
|
label={`${current.agent_name || current.conv_agent_key || "Agent"} · 工具请求`}
|
||||||
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
|
text={JSON.stringify(row.tool_input ?? {}, null, 2)}
|
||||||
/>
|
/>
|
||||||
{command ? (
|
{command ? (
|
||||||
@@ -748,8 +726,8 @@ function ApprovalTable({
|
|||||||
<span className="truncate" title={originLabel(row)}>
|
<span className="truncate" title={originLabel(row)}>
|
||||||
<ApprovalOrigin row={row} />
|
<ApprovalOrigin row={row} />
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate text-muted-foreground text-xs" title={agentLabel(row)}>
|
<span className="truncate text-muted-foreground text-xs">
|
||||||
Agent:{agentLabel(row)}
|
{row.agent_name || row.conv_agent_key}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -22,19 +22,11 @@ function mergeActivities(oldItems: Activity[], incoming: Activity[]) {
|
|||||||
export function FindingAutoChat({
|
export function FindingAutoChat({
|
||||||
findingId,
|
findingId,
|
||||||
findingTitle,
|
findingTitle,
|
||||||
agentKey = "auto",
|
|
||||||
agentLabel = "Auto Agent",
|
|
||||||
promptPlaceholder = "询问这个漏洞…",
|
|
||||||
modal = false,
|
|
||||||
open,
|
open,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
}: {
|
}: {
|
||||||
findingId: string;
|
findingId: string;
|
||||||
findingTitle: string;
|
findingTitle: string;
|
||||||
agentKey?: string;
|
|
||||||
agentLabel?: string;
|
|
||||||
promptPlaceholder?: string;
|
|
||||||
modal?: boolean;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -50,7 +42,7 @@ export function FindingAutoChat({
|
|||||||
const [history, setHistory] = React.useState<Conversation[]>([]);
|
const [history, setHistory] = React.useState<Conversation[]>([]);
|
||||||
const cursor = React.useRef(0);
|
const cursor = React.useRef(0);
|
||||||
const safeTitle = findingTitle.replace(/[\[\]\r\n]/g, " ").trim().slice(0, 100);
|
const safeTitle = findingTitle.replace(/[\[\]\r\n]/g, " ").trim().slice(0, 100);
|
||||||
const storageKey = `artex.finding.${agentKey}-conversation.${findingId}`;
|
const storageKey = `artex.finding.auto-conversation.${findingId}`;
|
||||||
|
|
||||||
const createConversation = React.useCallback(async () => {
|
const createConversation = React.useCallback(async () => {
|
||||||
setManualEmpty(false);
|
setManualEmpty(false);
|
||||||
@@ -58,7 +50,7 @@ export function FindingAutoChat({
|
|||||||
setRunning(false);
|
setRunning(false);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const created = await api.createConversation(agentKey, `漏洞 #${findingId} · ${safeTitle}`);
|
const created = await api.createConversation("auto", `漏洞 #${findingId} · ${safeTitle}`);
|
||||||
window.localStorage.setItem(storageKey, String(created.id));
|
window.localStorage.setItem(storageKey, String(created.id));
|
||||||
setConversation(created);
|
setConversation(created);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -66,7 +58,7 @@ export function FindingAutoChat({
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [agentKey, findingId, safeTitle, storageKey]);
|
}, [findingId, safeTitle, storageKey]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open || conversation || manualEmpty) return;
|
if (!open || conversation || manualEmpty) return;
|
||||||
@@ -74,7 +66,7 @@ export function FindingAutoChat({
|
|||||||
if (Number.isSafeInteger(storedID) && storedID > 0) {
|
if (Number.isSafeInteger(storedID) && storedID > 0) {
|
||||||
setConversation({
|
setConversation({
|
||||||
id: storedID,
|
id: storedID,
|
||||||
agent_key: agentKey,
|
agent_key: "auto",
|
||||||
title: `漏洞 #${findingId} · ${safeTitle}`,
|
title: `漏洞 #${findingId} · ${safeTitle}`,
|
||||||
created_at: "",
|
created_at: "",
|
||||||
updated_at: "",
|
updated_at: "",
|
||||||
@@ -93,7 +85,7 @@ export function FindingAutoChat({
|
|||||||
const items = await api.conversations();
|
const items = await api.conversations();
|
||||||
if (!live) return;
|
if (!live) return;
|
||||||
const prefix = `漏洞 #${findingId} ·`;
|
const prefix = `漏洞 #${findingId} ·`;
|
||||||
setHistory(items.filter((item) => item.agent_key === agentKey && item.title.startsWith(prefix)));
|
setHistory(items.filter((item) => item.agent_key === "auto" && item.title.startsWith(prefix)));
|
||||||
} catch {
|
} catch {
|
||||||
if (live && historyOpen) setHistory([]);
|
if (live && historyOpen) setHistory([]);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -105,7 +97,7 @@ export function FindingAutoChat({
|
|||||||
live = false;
|
live = false;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [agentKey, findingId, historyOpen, running]);
|
}, [findingId, historyOpen, running]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!open || !conversation) return;
|
if (!open || !conversation) return;
|
||||||
@@ -211,17 +203,13 @@ export function FindingAutoChat({
|
|||||||
if (!open) return null;
|
if (!open) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={modal
|
<div className="pointer-events-none fixed inset-x-0 top-20 z-40 flex justify-end px-4 pb-4 sm:px-6">
|
||||||
? "pointer-events-none fixed inset-0 z-50 flex items-center justify-center bg-black/35 p-4"
|
<section className="pointer-events-auto flex min-w-0 h-[calc(100svh-6rem)] w-[min(520px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border bg-background shadow-2xl">
|
||||||
: "pointer-events-none fixed inset-x-0 top-20 z-40 flex justify-end px-4 pb-4 sm:px-6"}>
|
|
||||||
<section className={modal
|
|
||||||
? "pointer-events-auto flex min-w-0 h-[min(760px,calc(100svh-2rem))] w-[min(760px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border bg-background shadow-2xl"
|
|
||||||
: "pointer-events-auto flex min-w-0 h-[calc(100svh-6rem)] w-[min(520px,calc(100vw-2rem))] flex-col overflow-hidden rounded-xl border bg-background shadow-2xl"}>
|
|
||||||
<header className="flex items-center gap-2 border-b p-3">
|
<header className="flex items-center gap-2 border-b p-3">
|
||||||
<BotIcon className="size-4 text-primary" />
|
<BotIcon className="size-4 text-primary" />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="flex items-center gap-2 font-medium">
|
<p className="flex items-center gap-2 font-medium">
|
||||||
{agentLabel}
|
Auto Agent
|
||||||
{running && <span className="inline-flex items-center gap-1 rounded bg-blue-500/15 px-1.5 py-0.5 text-blue-600 text-xs"><span className="size-1.5 animate-pulse rounded-full bg-blue-500" />运行中</span>}
|
{running && <span className="inline-flex items-center gap-1 rounded bg-blue-500/15 px-1.5 py-0.5 text-blue-600 text-xs"><span className="size-1.5 animate-pulse rounded-full bg-blue-500" />运行中</span>}
|
||||||
</p>
|
</p>
|
||||||
<p className="truncate text-muted-foreground text-xs">漏洞 #{findingId} · {safeTitle}</p>
|
<p className="truncate text-muted-foreground text-xs">漏洞 #{findingId} · {safeTitle}</p>
|
||||||
@@ -286,7 +274,7 @@ export function FindingAutoChat({
|
|||||||
inputGroup
|
inputGroup
|
||||||
onValueChange={setInput}
|
onValueChange={setInput}
|
||||||
disabled={!conversation || loading || sending || running}
|
disabled={!conversation || loading || sending || running}
|
||||||
placeholder={promptPlaceholder}
|
placeholder="询问这个漏洞…"
|
||||||
onKeyDown={(event) => {
|
onKeyDown={(event) => {
|
||||||
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
@@ -2,8 +2,14 @@
|
|||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { EditorState } from "@codemirror/state";
|
import { EditorState } from "@codemirror/state";
|
||||||
import { drawSelection, EditorView, highlightActiveLine, keymap, lineNumbers } from "@codemirror/view";
|
import {
|
||||||
import { defaultKeymap, history, historyKeymap, indentWithTab } from "@codemirror/commands";
|
drawSelection,
|
||||||
|
EditorView,
|
||||||
|
highlightActiveLine,
|
||||||
|
keymap,
|
||||||
|
lineNumbers,
|
||||||
|
} from "@codemirror/view";
|
||||||
|
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
|
||||||
import { defaultHighlightStyle, HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language";
|
import { defaultHighlightStyle, HighlightStyle, StreamLanguage, syntaxHighlighting } from "@codemirror/language";
|
||||||
import { tags } from "@lezer/highlight";
|
import { tags } from "@lezer/highlight";
|
||||||
|
|
||||||
@@ -31,31 +37,17 @@ const httpLanguage = StreamLanguage.define({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const packetTheme = EditorView.theme(
|
const packetTheme = EditorView.theme({
|
||||||
{
|
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
|
||||||
"&": { height: "100%", backgroundColor: "transparent", color: "var(--foreground)" },
|
".cm-scroller": { overflow: "auto", fontFamily: "var(--font-mono)", fontSize: "0.75rem", fontWeight: "500", lineHeight: "1.65rem" },
|
||||||
".cm-scroller": {
|
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
|
||||||
overflow: "auto",
|
".cm-gutters": { backgroundColor: "transparent", color: "var(--muted-foreground)", border: "0", borderRight: "1px solid var(--border)" },
|
||||||
fontFamily: "var(--font-mono)",
|
".cm-lineNumbers .cm-gutterElement": { minWidth: "2rem", padding: "0 0.5rem" },
|
||||||
fontSize: "0.75rem",
|
".cm-activeLine": { backgroundColor: "transparent" },
|
||||||
fontWeight: "500",
|
".cm-activeLineGutter": { backgroundColor: "transparent" },
|
||||||
lineHeight: "1.65rem",
|
".cm-selectionBackground, ::selection": { backgroundColor: "#c9d2e3" },
|
||||||
},
|
".cm-cursor": { borderLeftColor: "#222" },
|
||||||
".cm-content": { minHeight: "100%", padding: "0.5rem 0.5rem" },
|
}, { dark: false });
|
||||||
".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({
|
export function PacketCodeEditor({
|
||||||
value,
|
value,
|
||||||
@@ -84,8 +76,7 @@ export function PacketCodeEditor({
|
|||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
history(),
|
keymap.of([...defaultKeymap, indentWithTab]),
|
||||||
keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
|
|
||||||
httpLanguage,
|
httpLanguage,
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
syntaxHighlighting(packetHighlight),
|
syntaxHighlighting(packetHighlight),
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export function RepeaterWorkspace() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-10 min-w-0 items-center rounded-md border bg-muted/20 p-1">
|
<div className="flex min-h-10 min-w-0 items-center rounded-md border bg-muted/20 p-1">
|
||||||
<div className="scrollbar-none flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
|
<div className="flex min-w-0 flex-1 items-center gap-1 overflow-x-auto">
|
||||||
{workspace.tabs.map((tab) => {
|
{workspace.tabs.map((tab) => {
|
||||||
const selected = tab.id === workspace.activeTabId;
|
const selected = tab.id === workspace.activeTabId;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|||||||
data-slot="input-group"
|
data-slot="input-group"
|
||||||
role="group"
|
role="group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-1 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
Reference in new issue
Block a user