mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-23 22:40:05 +02:00
411 lines
13 KiB
Go
411 lines
13 KiB
Go
package database
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ValidProjectFactEdgeTypes 项目事实图允许的边类型。
|
|
var ValidProjectFactEdgeTypes = map[string]struct{}{
|
|
"depends_on": {},
|
|
"leads_to": {},
|
|
"enables": {},
|
|
"exploits": {},
|
|
"discovered_on": {},
|
|
"contains": {},
|
|
"part_of": {},
|
|
"supports": {},
|
|
}
|
|
|
|
// ProjectFactEdge 项目事实关系边(source → target)。
|
|
type ProjectFactEdge struct {
|
|
ID string `json:"id"`
|
|
ProjectID string `json:"project_id"`
|
|
SourceFactKey string `json:"source_fact_key"`
|
|
TargetFactKey string `json:"target_fact_key"`
|
|
EdgeType string `json:"edge_type"`
|
|
Confidence string `json:"confidence"` // confirmed | tentative | deprecated
|
|
SourceConversationID string `json:"source_conversation_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ProjectFactEdgeInput 写入边时的输入(出边:source → To)。
|
|
type ProjectFactEdgeInput struct {
|
|
To string `json:"to"`
|
|
Type string `json:"type"`
|
|
Confidence string `json:"confidence,omitempty"`
|
|
}
|
|
|
|
// ProjectFactEdgeFromInput 写入入边时的输入(From → 当前事实)。
|
|
type ProjectFactEdgeFromInput struct {
|
|
From string `json:"from"`
|
|
Type string `json:"type"`
|
|
Confidence string `json:"confidence,omitempty"`
|
|
}
|
|
|
|
// ProjectFactGraphNode 图 API 节点。
|
|
type ProjectFactGraphNode struct {
|
|
ID string `json:"id"`
|
|
FactKey string `json:"fact_key"`
|
|
Category string `json:"category"`
|
|
Label string `json:"label"` // 图节点短标签(截断)
|
|
Summary string `json:"summary"` // 完整摘要(侧栏等详情用)
|
|
Confidence string `json:"confidence"`
|
|
Type string `json:"type"`
|
|
Pinned bool `json:"pinned"`
|
|
}
|
|
|
|
// ProjectFactGraphEdge 图 API 边。
|
|
type ProjectFactGraphEdge struct {
|
|
ID string `json:"id"`
|
|
Source string `json:"source"`
|
|
Target string `json:"target"`
|
|
Type string `json:"type"`
|
|
Confidence string `json:"confidence"`
|
|
}
|
|
|
|
// ProjectFactGraph 项目事实图。
|
|
type ProjectFactGraph struct {
|
|
Nodes []ProjectFactGraphNode `json:"nodes"`
|
|
Edges []ProjectFactGraphEdge `json:"edges"`
|
|
}
|
|
|
|
// ValidateProjectFactEdgeType 校验边类型。
|
|
func ValidateProjectFactEdgeType(edgeType string) error {
|
|
edgeType = strings.TrimSpace(strings.ToLower(edgeType))
|
|
if edgeType == "" {
|
|
return fmt.Errorf("edge type 不能为空")
|
|
}
|
|
if _, ok := ValidProjectFactEdgeTypes[edgeType]; !ok {
|
|
return fmt.Errorf("无效的 edge type: %s", edgeType)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeEdgeConfidence(confidence string) string {
|
|
confidence = strings.TrimSpace(strings.ToLower(confidence))
|
|
switch confidence {
|
|
case "confirmed", "deprecated":
|
|
return confidence
|
|
default:
|
|
return "tentative"
|
|
}
|
|
}
|
|
|
|
// ListProjectFactEdgesByProject 列出项目全部边。
|
|
func (db *DB) ListProjectFactEdgesByProject(projectID string) ([]*ProjectFactEdge, error) {
|
|
rows, err := db.Query(
|
|
`SELECT id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
COALESCE(source_conversation_id,''), created_at, updated_at
|
|
FROM project_fact_edges
|
|
WHERE project_id = ?
|
|
ORDER BY created_at ASC, rowid ASC`,
|
|
projectID,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanProjectFactEdges(rows)
|
|
}
|
|
|
|
// ListOutgoingProjectFactEdges 列出某事实的全部出边。
|
|
func (db *DB) ListOutgoingProjectFactEdges(projectID, sourceFactKey string) ([]*ProjectFactEdge, error) {
|
|
rows, err := db.Query(
|
|
`SELECT id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
COALESCE(source_conversation_id,''), created_at, updated_at
|
|
FROM project_fact_edges
|
|
WHERE project_id = ? AND source_fact_key = ?
|
|
ORDER BY created_at ASC, rowid ASC`,
|
|
projectID, sourceFactKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanProjectFactEdges(rows)
|
|
}
|
|
|
|
// ListIncomingProjectFactEdges 列出某事实的全部入边。
|
|
func (db *DB) ListIncomingProjectFactEdges(projectID, targetFactKey string) ([]*ProjectFactEdge, error) {
|
|
rows, err := db.Query(
|
|
`SELECT id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
COALESCE(source_conversation_id,''), created_at, updated_at
|
|
FROM project_fact_edges
|
|
WHERE project_id = ? AND target_fact_key = ?
|
|
ORDER BY created_at ASC, rowid ASC`,
|
|
projectID, targetFactKey,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
return scanProjectFactEdges(rows)
|
|
}
|
|
|
|
// ReplaceOutgoingProjectFactEdges 替换某事实的全部出边(links 省略时不调用)。
|
|
func (db *DB) ReplaceOutgoingProjectFactEdges(projectID, sourceFactKey, sourceConversationID string, inputs []ProjectFactEdgeInput) error {
|
|
sourceFactKey = strings.TrimSpace(sourceFactKey)
|
|
if sourceFactKey == "" {
|
|
return fmt.Errorf("source_fact_key 不能为空")
|
|
}
|
|
if _, err := db.Exec(
|
|
`DELETE FROM project_fact_edges WHERE project_id = ? AND source_fact_key = ?`,
|
|
projectID, sourceFactKey,
|
|
); err != nil {
|
|
return fmt.Errorf("清除旧边失败: %w", err)
|
|
}
|
|
for _, in := range inputs {
|
|
target := strings.TrimSpace(in.To)
|
|
if target == "" {
|
|
continue
|
|
}
|
|
if err := ValidateFactKey(target); err != nil {
|
|
return fmt.Errorf("target fact_key 无效 (%s): %w", target, err)
|
|
}
|
|
if target == sourceFactKey {
|
|
return fmt.Errorf("边不能指向自身: %s", sourceFactKey)
|
|
}
|
|
if err := ValidateProjectFactEdgeType(in.Type); err != nil {
|
|
return err
|
|
}
|
|
edge := &ProjectFactEdge{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
SourceFactKey: sourceFactKey,
|
|
TargetFactKey: target,
|
|
EdgeType: strings.ToLower(strings.TrimSpace(in.Type)),
|
|
Confidence: normalizeEdgeConfidence(in.Confidence),
|
|
SourceConversationID: sourceConversationID,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
if err := db.insertProjectFactEdge(edge); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ReplaceIncomingProjectFactEdges 替换某事实的全部入边(From 为来源 fact_key)。
|
|
func (db *DB) ReplaceIncomingProjectFactEdges(projectID, targetFactKey string, inputs []ProjectFactEdgeFromInput) error {
|
|
targetFactKey = strings.TrimSpace(targetFactKey)
|
|
if targetFactKey == "" {
|
|
return fmt.Errorf("target_fact_key 不能为空")
|
|
}
|
|
if _, err := db.Exec(
|
|
`DELETE FROM project_fact_edges WHERE project_id = ? AND target_fact_key = ?`,
|
|
projectID, targetFactKey,
|
|
); err != nil {
|
|
return fmt.Errorf("清除旧入边失败: %w", err)
|
|
}
|
|
for _, in := range inputs {
|
|
source := strings.TrimSpace(in.From)
|
|
if source == "" {
|
|
continue
|
|
}
|
|
if err := ValidateFactKey(source); err != nil {
|
|
return fmt.Errorf("source fact_key 无效 (%s): %w", source, err)
|
|
}
|
|
if source == targetFactKey {
|
|
return fmt.Errorf("边不能指向自身: %s", targetFactKey)
|
|
}
|
|
if err := ValidateProjectFactEdgeType(in.Type); err != nil {
|
|
return err
|
|
}
|
|
sourceConversationID := ""
|
|
if srcFact, err := db.GetProjectFactByKey(projectID, source); err == nil && srcFact != nil {
|
|
sourceConversationID = srcFact.SourceConversationID
|
|
}
|
|
edge := &ProjectFactEdge{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
SourceFactKey: source,
|
|
TargetFactKey: targetFactKey,
|
|
EdgeType: strings.ToLower(strings.TrimSpace(in.Type)),
|
|
Confidence: normalizeEdgeConfidence(in.Confidence),
|
|
SourceConversationID: sourceConversationID,
|
|
CreatedAt: time.Now(),
|
|
UpdatedAt: time.Now(),
|
|
}
|
|
if err := db.insertProjectFactEdge(edge); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetProjectFactEdge 按 ID 获取边。
|
|
func (db *DB) GetProjectFactEdge(edgeID string) (*ProjectFactEdge, error) {
|
|
var e ProjectFactEdge
|
|
var createdAt, updatedAt string
|
|
err := db.QueryRow(
|
|
`SELECT id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
COALESCE(source_conversation_id,''), created_at, updated_at
|
|
FROM project_fact_edges WHERE id = ?`, edgeID,
|
|
).Scan(&e.ID, &e.ProjectID, &e.SourceFactKey, &e.TargetFactKey, &e.EdgeType, &e.Confidence,
|
|
&e.SourceConversationID, &createdAt, &updatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("边不存在")
|
|
}
|
|
e.CreatedAt = parseDBTime(createdAt)
|
|
e.UpdatedAt = parseDBTime(updatedAt)
|
|
return &e, nil
|
|
}
|
|
|
|
// AddProjectFactEdge 新增单条边(已存在则更新 confidence)。
|
|
func (db *DB) AddProjectFactEdge(projectID string, in ProjectFactEdgeInput, sourceFactKey, sourceConversationID string) (*ProjectFactEdge, error) {
|
|
sourceFactKey = strings.TrimSpace(sourceFactKey)
|
|
target := strings.TrimSpace(in.To)
|
|
if sourceFactKey == "" || target == "" {
|
|
return nil, fmt.Errorf("source 与 target 必填")
|
|
}
|
|
if sourceFactKey == target {
|
|
return nil, fmt.Errorf("边不能指向自身")
|
|
}
|
|
if err := ValidateProjectFactEdgeType(in.Type); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := ValidateFactKey(target); err != nil {
|
|
return nil, err
|
|
}
|
|
now := time.Now()
|
|
e := &ProjectFactEdge{
|
|
ID: uuid.New().String(),
|
|
ProjectID: projectID,
|
|
SourceFactKey: sourceFactKey,
|
|
TargetFactKey: target,
|
|
EdgeType: strings.ToLower(strings.TrimSpace(in.Type)),
|
|
Confidence: normalizeEdgeConfidence(in.Confidence),
|
|
SourceConversationID: sourceConversationID,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
}
|
|
_, err := db.Exec(
|
|
`INSERT INTO project_fact_edges (
|
|
id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
source_conversation_id, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(project_id, source_fact_key, target_fact_key, edge_type)
|
|
DO UPDATE SET confidence = excluded.confidence, updated_at = excluded.updated_at`,
|
|
e.ID, e.ProjectID, e.SourceFactKey, e.TargetFactKey, e.EdgeType, e.Confidence,
|
|
nullIfEmpty(e.SourceConversationID), e.CreatedAt, e.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("添加边失败: %w", err)
|
|
}
|
|
// 返回最新
|
|
rows, err := db.Query(
|
|
`SELECT id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
COALESCE(source_conversation_id,''), created_at, updated_at
|
|
FROM project_fact_edges
|
|
WHERE project_id = ? AND source_fact_key = ? AND target_fact_key = ? AND edge_type = ?`,
|
|
projectID, sourceFactKey, target, e.EdgeType,
|
|
)
|
|
if err != nil {
|
|
return e, nil
|
|
}
|
|
defer rows.Close()
|
|
list, err := scanProjectFactEdges(rows)
|
|
if err != nil || len(list) == 0 {
|
|
return e, nil
|
|
}
|
|
return list[0], nil
|
|
}
|
|
|
|
// DeleteProjectFactEdge 删除单条边。
|
|
func (db *DB) DeleteProjectFactEdge(edgeID string) error {
|
|
res, err := db.Exec(`DELETE FROM project_fact_edges WHERE id = ?`, edgeID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n, _ := res.RowsAffected()
|
|
if n == 0 {
|
|
return fmt.Errorf("边不存在")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (db *DB) insertProjectFactEdge(e *ProjectFactEdge) error {
|
|
_, err := db.Exec(
|
|
`INSERT INTO project_fact_edges (
|
|
id, project_id, source_fact_key, target_fact_key, edge_type, confidence,
|
|
source_conversation_id, created_at, updated_at
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
e.ID, e.ProjectID, e.SourceFactKey, e.TargetFactKey, e.EdgeType, e.Confidence,
|
|
nullIfEmpty(e.SourceConversationID), e.CreatedAt, e.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("写入边失败: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RenameProjectFactKeyEdges 事实 key 变更时同步边上的引用。
|
|
func (db *DB) RenameProjectFactKeyEdges(projectID, oldKey, newKey string) error {
|
|
oldKey = strings.TrimSpace(oldKey)
|
|
newKey = strings.TrimSpace(newKey)
|
|
if oldKey == "" || newKey == "" || oldKey == newKey {
|
|
return nil
|
|
}
|
|
now := time.Now()
|
|
if _, err := db.Exec(
|
|
`UPDATE project_fact_edges SET source_fact_key = ?, updated_at = ?
|
|
WHERE project_id = ? AND source_fact_key = ?`,
|
|
newKey, now, projectID, oldKey,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
_, err := db.Exec(
|
|
`UPDATE project_fact_edges SET target_fact_key = ?, updated_at = ?
|
|
WHERE project_id = ? AND target_fact_key = ?`,
|
|
newKey, now, projectID, oldKey,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// DeleteProjectFactEdgesForKey 删除与某 fact_key 相关的全部边。
|
|
func (db *DB) DeleteProjectFactEdgesForKey(projectID, factKey string) error {
|
|
_, err := db.Exec(
|
|
`DELETE FROM project_fact_edges
|
|
WHERE project_id = ? AND (source_fact_key = ? OR target_fact_key = ?)`,
|
|
projectID, factKey, factKey,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// DeprecateProjectFactEdgesForKey 将关联边标记为 deprecated。
|
|
func (db *DB) DeprecateProjectFactEdgesForKey(projectID, factKey string) error {
|
|
now := time.Now()
|
|
_, err := db.Exec(
|
|
`UPDATE project_fact_edges SET confidence = 'deprecated', updated_at = ?
|
|
WHERE project_id = ? AND (source_fact_key = ? OR target_fact_key = ?)
|
|
AND confidence != 'deprecated'`,
|
|
now, projectID, factKey, factKey,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func scanProjectFactEdges(rows *sql.Rows) ([]*ProjectFactEdge, error) {
|
|
var out []*ProjectFactEdge
|
|
for rows.Next() {
|
|
var e ProjectFactEdge
|
|
var createdAt, updatedAt string
|
|
if err := rows.Scan(
|
|
&e.ID, &e.ProjectID, &e.SourceFactKey, &e.TargetFactKey, &e.EdgeType, &e.Confidence,
|
|
&e.SourceConversationID, &createdAt, &updatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
e.CreatedAt = parseDBTime(createdAt)
|
|
e.UpdatedAt = parseDBTime(updatedAt)
|
|
out = append(out, &e)
|
|
}
|
|
return out, rows.Err()
|
|
}
|