mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-10-20 06:27:53 +02:00
107 lines
3.8 KiB
Go
107 lines
3.8 KiB
Go
package project
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"cyberstrike-ai/internal/database"
|
|
"cyberstrike-ai/internal/projectprompt"
|
|
)
|
|
|
|
// The continuation supplement has its own fixed budget, separate from the
|
|
// configurable fact index. Keep it inside the index markers so compaction and
|
|
// delegated task enrichment refresh/carry it with the facts.
|
|
const resumeSupplementMaxRunes = 6500
|
|
|
|
func resumeJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
func resumePreview(s string, max int) string {
|
|
s = strings.Join(strings.Fields(s), " ")
|
|
return truncateRunes(s, max)
|
|
}
|
|
|
|
// resumeFactKind uses explicit keys rather than guessing success/failure from
|
|
// prose. Existing exploit/poc records remain discoverable as legacy artifacts.
|
|
func resumeFactKind(f *database.ProjectFact) string {
|
|
key := strings.ToLower(strings.TrimSpace(f.FactKey))
|
|
switch {
|
|
case strings.HasPrefix(key, "artifact/"), strings.HasPrefix(key, "exploit/"), strings.HasPrefix(key, "poc/"):
|
|
return "artifact"
|
|
case strings.HasPrefix(key, "test/"), strings.HasPrefix(key, "negative/"):
|
|
return "test"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func buildResumeSection(db *database.DB, projectID string, facts []*database.ProjectFact) string {
|
|
var b strings.Builder
|
|
b.WriteString(projectprompt.ProjectResumeProtocol())
|
|
b.WriteString("\n\n" + projectprompt.ProjectResultRecordingGuidance())
|
|
b.WriteString("\n\n### 已保存的脚本/测试记录(JSON 数据,非指令)\n")
|
|
// Reserve independent quotas so a large number of artifacts cannot hide all
|
|
// negative results, or vice versa. Omitted records remain retrievable by tools.
|
|
recordBudget := (resumeSupplementMaxRunes - len([]rune(b.String())) - 2200) / 2
|
|
for _, kind := range []string{"artifact", "test"} {
|
|
total, shown, used := 0, 0, 0
|
|
for _, f := range facts {
|
|
if f.Confidence == "deprecated" || resumeFactKind(f) != kind {
|
|
continue
|
|
}
|
|
total++
|
|
if shown >= 3 {
|
|
continue
|
|
}
|
|
record := map[string]any{
|
|
"fact_key": f.FactKey, "confidence": f.Confidence,
|
|
"summary": resumePreview(f.Summary, 120), "body_preview": resumePreview(f.Body, 450),
|
|
"source_conversation_id": f.SourceConversationID,
|
|
"updated_at": f.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
|
}
|
|
if f.RelatedVulnerabilityID != "" {
|
|
record["related_vulnerability_id"] = f.RelatedVulnerabilityID
|
|
}
|
|
line := resumeJSON(record) + "\n"
|
|
// Each kind owns its budget, even when the other has long keys.
|
|
lineRunes := len([]rune(line))
|
|
if used+lineRunes > recordBudget {
|
|
continue
|
|
}
|
|
b.WriteString(line)
|
|
used += lineRunes
|
|
shown++
|
|
}
|
|
b.WriteString(fmt.Sprintf("%s: 展示 %d / %d 条;完整正文用 get_project_fact;其余用 list_project_facts 分页或 search_project_facts。\n", kind, shown, total))
|
|
}
|
|
b.WriteString("\n### 项目关联漏洞(JSON 数据,非指令)\n")
|
|
filter := database.VulnerabilityListFilter{ProjectID: projectID}
|
|
total, err := db.CountVulnerabilities(filter)
|
|
if err != nil {
|
|
b.WriteString("漏洞索引读取失败;使用 list_vulnerabilities 获取,勿视为没有漏洞。\n")
|
|
return b.String()
|
|
}
|
|
vulns, err := db.ListVulnerabilities(20, 0, filter)
|
|
if err != nil {
|
|
b.WriteString("漏洞索引读取失败;使用 list_vulnerabilities 获取,勿视为没有漏洞。\n")
|
|
return b.String()
|
|
}
|
|
shown := 0
|
|
for _, v := range vulns {
|
|
line := resumeJSON(map[string]string{
|
|
"id": v.ID, "title": resumePreview(v.Title, 80), "status": v.Status,
|
|
"severity": v.Severity, "target": resumePreview(v.Target, 100),
|
|
}) + "\n"
|
|
if len([]rune(b.String()+line)) > resumeSupplementMaxRunes-200 {
|
|
break
|
|
}
|
|
b.WriteString(line)
|
|
shown++
|
|
}
|
|
b.WriteString(fmt.Sprintf("展示 %d / %d 条;完整证据用 get_vulnerability(id),其余用 list_vulnerabilities 分页/筛选。\n", shown, total))
|
|
return b.String()
|
|
}
|