修复
1、修复流量包里的复制按钮不工作问题 2、修复新建任务关联资产不同步到任务测试范围问题
This commit is contained in:
1 parent
3675bd815c
commit
cf0989c67d
10 files changed
+297
-32
No files matched your search
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -51,6 +52,12 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
|||||||
if added != len(inputs) || skipped != 0 || invalid != 0 || len(scopeErrors) != 0 {
|
if added != len(inputs) || skipped != 0 || invalid != 0 || len(scopeErrors) != 0 {
|
||||||
t.Fatalf("add company scope: added=%d skipped=%d invalid=%d errors=%v", added, skipped, invalid, scopeErrors)
|
t.Fatalf("add company scope: added=%d skipped=%d invalid=%d errors=%v", added, skipped, invalid, scopeErrors)
|
||||||
}
|
}
|
||||||
|
assets := d.Assets()
|
||||||
|
assetID, err := assets.UpsertRootDomain(db.UpsertRootDomainReq{Domain: domain})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _, _ = assets.DeleteByIDs([]int64{assetID}) })
|
||||||
|
|
||||||
task, err := d.CreateTaskWithOptions("company context", "read configured scope", db.TaskCreateOptions{
|
task, err := d.CreateTaskWithOptions("company context", "read configured scope", db.TaskCreateOptions{
|
||||||
CompanyIDs: []int64{companyID},
|
CompanyIDs: []int64{companyID},
|
||||||
@@ -62,7 +69,14 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
|||||||
|
|
||||||
tools := NewToolSet(d.Exploration(task.ExplorationID), "planner")
|
tools := NewToolSet(d.Exploration(task.ExplorationID), "planner")
|
||||||
tools.SetTaskID(task.ID)
|
tools.SetTaskID(task.ID)
|
||||||
tools.SetAssetStore(d.Assets(), companies)
|
tools.SetAssetStore(assets, companies)
|
||||||
|
linkedAssets, err := assets.QueryByTask(task.ID, "root_domain", 10, 0)
|
||||||
|
if err != nil || len(linkedAssets) != 1 || linkedAssets[0].ID != assetID {
|
||||||
|
t.Fatalf("company asset was not linked to task: assets=%+v err=%v", linkedAssets, err)
|
||||||
|
}
|
||||||
|
if linkedAssets[0].TaskSource != "company" || !strings.Contains(linkedAssets[0].TaskSourceSummary, companiesName(t, companies, companyID)) {
|
||||||
|
t.Fatalf("company asset provenance missing: %+v", linkedAssets[0])
|
||||||
|
}
|
||||||
overview := tools.graphOverviewData()
|
overview := tools.graphOverviewData()
|
||||||
coverage, ok := overview["coverage"].(map[string]any)
|
coverage, ok := overview["coverage"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -89,6 +103,95 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
|||||||
if !ok || len(keywords) != 1 || keywords[0] != keyword {
|
if !ok || len(keywords) != 1 || keywords[0] != keyword {
|
||||||
t.Fatalf("company keywords missing: %#v", scopeRows[0]["company_keywords"])
|
t.Fatalf("company keywords missing: %#v", scopeRows[0]["company_keywords"])
|
||||||
}
|
}
|
||||||
|
hosts, ok := coverage["hosts"].([]string)
|
||||||
|
if !ok || !containsString(hosts, domain) {
|
||||||
|
t.Fatalf("company asset host missing from agent context: %#v", coverage["hosts"])
|
||||||
|
}
|
||||||
|
untested := callReadJSON(t, tools.listUntestedAssets(), `{"type":"root_domain","page":1,"page_size":10}`)
|
||||||
|
if !strings.Contains(fmt.Sprint(untested), domain) {
|
||||||
|
t.Fatalf("company asset missing from untested backlog: %#v", untested)
|
||||||
|
}
|
||||||
|
byTask := callReadJSON(t, tools.listAssets(), fmt.Sprintf(
|
||||||
|
`{"dsl":"task_id==%d","type":"root_domain","limit":10}`, task.ID,
|
||||||
|
))
|
||||||
|
if !strings.Contains(fmt.Sprint(byTask), domain) {
|
||||||
|
t.Fatalf("company asset missing from task-scoped agent query: %#v", byTask)
|
||||||
|
}
|
||||||
|
byCompany := callReadJSON(t, tools.listAssets(), fmt.Sprintf(
|
||||||
|
`{"dsl":"company_id==%d","type":"root_domain","limit":10}`, companyID,
|
||||||
|
))
|
||||||
|
if !strings.Contains(fmt.Sprint(byCompany), domain) {
|
||||||
|
t.Fatalf("company asset missing from company-scoped agent query: %#v", byCompany)
|
||||||
|
}
|
||||||
|
assetJSON, _ := json.Marshal(assetID)
|
||||||
|
intentID, err := tools.addOneIntent(intentItem{
|
||||||
|
Summary: "test associated company asset",
|
||||||
|
AssetIDs: []json.RawMessage{assetJSON},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
workerAssets, err := assets.IntentAssets(task.ID)
|
||||||
|
if err != nil || len(workerAssets) != 1 || workerAssets[0].IntentID != intentID || workerAssets[0].AssetID != assetID {
|
||||||
|
t.Fatalf("worker target did not retain company asset: assets=%+v err=%v", workerAssets, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
coverageDisabled := false
|
||||||
|
disabledTask, err := d.CreateTaskWithOptions("company context without coverage", "still expose company assets", db.TaskCreateOptions{
|
||||||
|
CompanyIDs: []int64{companyID}, CoverageEnabled: &coverageDisabled,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = d.DeleteTask(disabledTask.ID) })
|
||||||
|
disabledTools := NewToolSet(d.Exploration(disabledTask.ExplorationID), "planner")
|
||||||
|
disabledTools.SetTaskID(disabledTask.ID)
|
||||||
|
disabledTools.SetCoverageEnabled(false)
|
||||||
|
disabledTools.SetAssetStore(assets, companies)
|
||||||
|
disabledOverview := disabledTools.graphOverviewData()
|
||||||
|
disabledCoverage, ok := disabledOverview["coverage"].(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("coverage-disabled task lost asset context: %#v", disabledOverview["coverage"])
|
||||||
|
}
|
||||||
|
disabledHosts, ok := disabledCoverage["hosts"].([]string)
|
||||||
|
if !ok || !containsString(disabledHosts, domain) {
|
||||||
|
t.Fatalf("coverage-disabled task lost company asset host: %#v", disabledCoverage["hosts"])
|
||||||
|
}
|
||||||
|
if _, exists := disabledCoverage["denominator"]; exists {
|
||||||
|
t.Fatalf("coverage-disabled task unexpectedly exposed metrics: %#v", disabledCoverage)
|
||||||
|
}
|
||||||
|
if linked, err := assets.QueryByTask(disabledTask.ID, "root_domain", 10, 0); err != nil || len(linked) != 1 || linked[0].ID != assetID {
|
||||||
|
t.Fatalf("coverage-disabled task asset link=%+v err=%v", linked, err)
|
||||||
|
}
|
||||||
|
disabledIntentID, err := disabledTools.addOneIntent(intentItem{
|
||||||
|
Summary: "test associated company asset without coverage metrics",
|
||||||
|
AssetIDs: []json.RawMessage{assetJSON},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
disabledWorkerAssets, err := assets.IntentAssets(disabledTask.ID)
|
||||||
|
if err != nil || len(disabledWorkerAssets) != 1 || disabledWorkerAssets[0].IntentID != disabledIntentID || disabledWorkerAssets[0].AssetID != assetID {
|
||||||
|
t.Fatalf("coverage-disabled worker target=%+v err=%v", disabledWorkerAssets, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(items []string, target string) bool {
|
||||||
|
for _, item := range items {
|
||||||
|
if item == target {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func companiesName(t *testing.T, companies *db.CompanyStore, companyID int64) string {
|
||||||
|
t.Helper()
|
||||||
|
company, err := companies.GetCompany(companyID)
|
||||||
|
if err != nil || company == nil {
|
||||||
|
t.Fatalf("company %d: company=%+v err=%v", companyID, company, err)
|
||||||
|
}
|
||||||
|
return company.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBlackboardToolsReadDirectSources(t *testing.T) {
|
func TestBlackboardToolsReadDirectSources(t *testing.T) {
|
||||||
|
|||||||
@@ -177,9 +177,13 @@ func TestCompanyScopeInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer cleanupCompany(d, id)
|
defer cleanupCompany(d, id)
|
||||||
|
|
||||||
// TLD-only domains and overly broad CIDRs should be rejected
|
// Explicitly typed TLD-only domains and overly broad CIDRs should be
|
||||||
lines := []string{"com", "1.2.3.4/8"} // rejected
|
// rejected. Untyped plain text is intentionally classified as a keyword.
|
||||||
added, _, invalid, _ := cs.AddScope(id, lines, "test")
|
inputs := []ScopeInput{
|
||||||
|
{Kind: "domain", Value: "com"},
|
||||||
|
{Kind: "cidr", Value: "1.2.3.4/8"},
|
||||||
|
}
|
||||||
|
added, _, invalid, _ := cs.AddScopeInputs(id, inputs, "test")
|
||||||
if added != 0 {
|
if added != 0 {
|
||||||
t.Errorf("want 0 added for invalid lines, got %d", added)
|
t.Errorf("want 0 added for invalid lines, got %d", added)
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-2
@@ -89,10 +89,12 @@ const MinPlanHeartbeatSeconds = 600
|
|||||||
const MaxTaskSourceCount = 8
|
const MaxTaskSourceCount = 8
|
||||||
|
|
||||||
// MaxTaskCompanyCount bounds the number of company asset scopes attached to a
|
// MaxTaskCompanyCount bounds the number of company asset scopes attached to a
|
||||||
// task. Company scopes are prompt context, so an unbounded list would inflate
|
// task. Company scopes are prompt context, and their currently attributed
|
||||||
// every planner and worker call.
|
// assets are snapshotted into the task at creation time.
|
||||||
const MaxTaskCompanyCount = 32
|
const MaxTaskCompanyCount = 32
|
||||||
|
|
||||||
|
const taskCompanyAssetSource = "company"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrTaskCompanyIDsInvalid = errors.New("invalid task company ids")
|
ErrTaskCompanyIDsInvalid = errors.New("invalid task company ids")
|
||||||
ErrTaskCompanyNotFound = errors.New("task company not found")
|
ErrTaskCompanyNotFound = errors.New("task company not found")
|
||||||
@@ -248,6 +250,11 @@ func insertTaskCompanies(tx *sql.Tx, taskID int64, companyIDs []int64) error {
|
|||||||
if len(companyIDs) == 0 {
|
if len(companyIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Company scope edits rebuild assets.company_id. Serialize the creation-time
|
||||||
|
// snapshot with those edits so the task sees one committed attribution state.
|
||||||
|
if err := lockCompanyScopeMutation(tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var inserted int
|
var inserted int
|
||||||
err := tx.QueryRow(`
|
err := tx.QueryRow(`
|
||||||
WITH requested(company_id, position) AS (
|
WITH requested(company_id, position) AS (
|
||||||
@@ -268,6 +275,32 @@ SELECT count(*) FROM inserted`, taskID, companyIDs).Scan(&inserted)
|
|||||||
if inserted != len(companyIDs) {
|
if inserted != len(companyIDs) {
|
||||||
return fmt.Errorf("%w: one or more companies do not exist", ErrTaskCompanyNotFound)
|
return fmt.Errorf("%w: one or more companies do not exist", ErrTaskCompanyNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updating task_ids fires sync_task_asset_links, which first creates generic
|
||||||
|
// source rows. The provenance upsert must therefore run afterwards so the
|
||||||
|
// company name and creation-time reason remain visible to operators.
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
UPDATE assets
|
||||||
|
SET task_ids=CASE
|
||||||
|
WHEN $1=ANY(task_ids) THEN task_ids
|
||||||
|
ELSE array_append(task_ids, $1)
|
||||||
|
END
|
||||||
|
WHERE company_id=ANY($2::bigint[])`, taskID, companyIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(`
|
||||||
|
INSERT INTO task_asset_links(task_id, asset_id, source, source_summary)
|
||||||
|
SELECT $1, asset.id, $3, '任务创建时关联企业:' || company.name
|
||||||
|
FROM assets asset
|
||||||
|
JOIN companies company ON company.id=asset.company_id
|
||||||
|
WHERE asset.company_id=ANY($2::bigint[])
|
||||||
|
AND $1=ANY(asset.task_ids)
|
||||||
|
ON CONFLICT (task_id, asset_id) DO UPDATE
|
||||||
|
SET source=EXCLUDED.source,
|
||||||
|
source_summary=EXCLUDED.source_summary,
|
||||||
|
source_node_id=NULL`, taskID, companyIDs, taskCompanyAssetSource); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+78
-1
@@ -509,10 +509,36 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
emptyCompany, _, err := d.Companies().UpsertCompany(fmt.Sprintf("Task Empty Company %d", suffix), "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
_, _ = d.Exec(`DELETE FROM companies WHERE id IN ($1,$2)`, companyA, companyB)
|
_, _ = d.Exec(`DELETE FROM companies WHERE id IN ($1,$2,$3)`, companyA, companyB, emptyCompany)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
existingTask, err := d.CreateTask("existing company asset owner", "goal", nil, 0, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _ = d.DeleteTask(existingTask.ID) })
|
||||||
|
var companyAssetA, companyAssetB int64
|
||||||
|
domainA := fmt.Sprintf("task-company-a-%d.example.test", suffix)
|
||||||
|
domainB := fmt.Sprintf("task-company-b-%d.example.test", suffix)
|
||||||
|
if err := d.QueryRow(`
|
||||||
|
INSERT INTO assets(type, domain, root_domain, company_id, company_source, task_ids)
|
||||||
|
VALUES ('root_domain',$1,$1,$2,'explicit',ARRAY[$3]::bigint[])
|
||||||
|
RETURNING id`, domainA, companyA, existingTask.ID).Scan(&companyAssetA); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := d.QueryRow(`
|
||||||
|
INSERT INTO assets(type, domain, root_domain, company_id, company_source)
|
||||||
|
VALUES ('root_domain',$1,$1,$2,'explicit')
|
||||||
|
RETURNING id`, domainB, companyB).Scan(&companyAssetB); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { _, _ = d.Assets().DeleteByIDs([]int64{companyAssetA, companyAssetB}) })
|
||||||
|
|
||||||
task, err := d.CreateTaskWithOptions("company-scoped task", "use company scope", TaskCreateOptions{
|
task, err := d.CreateTaskWithOptions("company-scoped task", "use company scope", TaskCreateOptions{
|
||||||
CompanyIDs: []int64{companyA, companyB},
|
CompanyIDs: []int64{companyA, companyB},
|
||||||
})
|
})
|
||||||
@@ -537,6 +563,57 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
|||||||
if scopeCount != 2 {
|
if scopeCount != 2 {
|
||||||
t.Fatalf("company task scope rows=%d want 2", scopeCount)
|
t.Fatalf("company task scope rows=%d want 2", scopeCount)
|
||||||
}
|
}
|
||||||
|
assets, err := d.Assets().QueryByTask(task.ID, "root_domain", 10, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(assets) != 2 {
|
||||||
|
t.Fatalf("company task assets=%d want 2: %+v", len(assets), assets)
|
||||||
|
}
|
||||||
|
assetByID := make(map[int64]*Asset, len(assets))
|
||||||
|
for _, asset := range assets {
|
||||||
|
assetByID[asset.ID] = asset
|
||||||
|
}
|
||||||
|
for assetID, companyName := range map[int64]string{
|
||||||
|
companyAssetA: fmt.Sprintf("Task Company A %d", suffix),
|
||||||
|
companyAssetB: fmt.Sprintf("Task Company B %d", suffix),
|
||||||
|
} {
|
||||||
|
asset := assetByID[assetID]
|
||||||
|
if asset == nil {
|
||||||
|
t.Errorf("company asset %d missing from task", assetID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if asset.TaskSource != taskCompanyAssetSource || asset.TaskSourceSummary != "任务创建时关联企业:"+companyName {
|
||||||
|
t.Errorf("asset %d provenance=%q/%q", assetID, asset.TaskSource, asset.TaskSourceSummary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var existingAssociation bool
|
||||||
|
if err := d.QueryRow(`SELECT $1=ANY(task_ids) FROM assets WHERE id=$2`, existingTask.ID, companyAssetA).Scan(&existingAssociation); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !existingAssociation {
|
||||||
|
t.Fatal("company snapshot removed the asset's existing task association")
|
||||||
|
}
|
||||||
|
var existingSource string
|
||||||
|
if err := d.QueryRow(`SELECT source FROM task_asset_links WHERE task_id=$1 AND asset_id=$2`, existingTask.ID, companyAssetA).Scan(&existingSource); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if existingSource == taskCompanyAssetSource {
|
||||||
|
t.Fatalf("company snapshot overwrote another task's provenance: %q", existingSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyTask, err := d.CreateTaskWithOptions("empty company scope", "no current assets", TaskCreateOptions{
|
||||||
|
CompanyIDs: []int64{emptyCompany},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if assets, err := d.Assets().QueryByTask(emptyTask.ID, "", 10, 0); err != nil || len(assets) != 0 {
|
||||||
|
t.Fatalf("empty company task assets=%+v err=%v", assets, err)
|
||||||
|
}
|
||||||
|
if err := d.DeleteTask(emptyTask.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
duplicateTask, err := d.CreateTaskWithOptions("duplicate company scope", "deduplicate", TaskCreateOptions{
|
duplicateTask, err := d.CreateTaskWithOptions("duplicate company scope", "deduplicate", TaskCreateOptions{
|
||||||
CompanyIDs: []int64{companyA, companyA, companyB, companyA},
|
CompanyIDs: []int64{companyA, companyA, companyB, companyA},
|
||||||
|
|||||||
+1
-1
@@ -1363,7 +1363,7 @@ type createTaskReq struct {
|
|||||||
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
||||||
LLMProfileIDs []int64 `json:"llm_profile_ids,omitempty"` // 有序任务级配置链;第一项初始生效
|
LLMProfileIDs []int64 `json:"llm_profile_ids,omitempty"` // 有序任务级配置链;第一项初始生效
|
||||||
SourceTaskIDs []string `json:"source_task_ids,omitempty"` // 仅直接、只读继承的来源任务
|
SourceTaskIDs []string `json:"source_task_ids,omitempty"` // 仅直接、只读继承的来源任务
|
||||||
CompanyIDs []int64 `json:"company_ids,omitempty"` // 关联企业资产范围;不复制资产或强制生成意图
|
CompanyIDs []int64 `json:"company_ids,omitempty"` // 关联企业范围并快照关联当前企业资产;不复制资产或强制生成意图
|
||||||
TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时
|
TimeoutSeconds int `json:"timeout_seconds"` // 任务级超时(秒);0/省略=不限时
|
||||||
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒);0/省略=默认600(10min);下限=默认=600,低于自动抬到600
|
PlanHeartbeatSeconds int `json:"plan_heartbeat_seconds"` // planner 心跳触发间隔(秒);0/省略=默认600(10min);下限=默认=600,低于自动抬到600
|
||||||
SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认关闭,走标准先规划再执行。显式传 true 才开(CTF 常一 work 解决时可省掉开跑前的 planner 轮)。
|
SeedFirstIntent *bool `json:"seed_first_intent,omitempty"` // 创建时直接下发一条种子意图(内容=描述+目标),让 worker 免等首轮 planner 直接开跑;省略/null=默认关闭,走标准先规划再执行。显式传 true 才开(CTF 常一 work 解决时可省掉开跑前的 planner 轮)。
|
||||||
|
|||||||
@@ -2613,8 +2613,8 @@ function CreateTaskSheet({
|
|||||||
portalContainer={sheetContentRef}
|
portalContainer={sheetContentRef}
|
||||||
/>
|
/>
|
||||||
<FieldDescription>
|
<FieldDescription>
|
||||||
将所选企业的域名、IP、CIDR、ICP 和企业关键词作为 Agent
|
创建任务时会将所选企业当前已有资产加入“测试资产”,并将域名、IP、CIDR、ICP 和企业关键词提供给 Agent
|
||||||
可选范围上下文;不会自动生成意图或强制改变执行目标。
|
作为范围上下文;不会自动生成意图或强制改变执行目标。
|
||||||
</FieldDescription>
|
</FieldDescription>
|
||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import type { TrafficDetail, TrafficExchange, TrafficHost, TrafficResp } from "@/lib/types";
|
import type { TrafficDetail, TrafficExchange, TrafficHost, TrafficResp } from "@/lib/types";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn, copyText } from "@/lib/utils";
|
||||||
|
|
||||||
function fmtTime(ts: string) {
|
function fmtTime(ts: string) {
|
||||||
return new Date(ts).toLocaleString("zh-CN", {
|
return new Date(ts).toLocaleString("zh-CN", {
|
||||||
@@ -199,12 +199,12 @@ function HttpCodeBlock({ raw }: { raw: string }) {
|
|||||||
}, [copied]);
|
}, [copied]);
|
||||||
|
|
||||||
const copyPacket = async () => {
|
const copyPacket = async () => {
|
||||||
try {
|
const ok = await copyText(value);
|
||||||
await navigator.clipboard.writeText(value);
|
if (ok) {
|
||||||
setCopied(true);
|
setCopied(true);
|
||||||
} catch {
|
return;
|
||||||
toast.error("复制失败,请使用 Ctrl/Cmd+A 后复制");
|
|
||||||
}
|
}
|
||||||
|
toast.error("复制失败,请使用 Ctrl/Cmd+A 后复制");
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderLine = (line: string, index: number) => {
|
const renderLine = (line: string, index: number) => {
|
||||||
|
|||||||
+63
-16
@@ -42,9 +42,35 @@ const mockCompanies = structuredClone(D.companies);
|
|||||||
const mockAssets = structuredClone(D.assets);
|
const mockAssets = structuredClone(D.assets);
|
||||||
const mockTaskAssetIDs = new Map(D.tasks.map((task, index) => [task.id, index + 1]));
|
const mockTaskAssetIDs = new Map(D.tasks.map((task, index) => [task.id, index + 1]));
|
||||||
const mockTaskScopes = new Map<string, TaskScopeRow[]>();
|
const mockTaskScopes = new Map<string, TaskScopeRow[]>();
|
||||||
|
type MockTaskAssetSource = Pick<Asset, "task_source" | "task_source_summary" | "task_source_node_id">;
|
||||||
|
const mockTaskAssetSources = new Map<string, MockTaskAssetSource>();
|
||||||
let nextMockTaskAssetID = D.tasks.length + 1;
|
let nextMockTaskAssetID = D.tasks.length + 1;
|
||||||
let mockActiveTask = D.ACTIVE_TASK;
|
let mockActiveTask = D.ACTIVE_TASK;
|
||||||
|
|
||||||
|
function mockTaskAssetSourceKey(taskID: string, assetID: number): string {
|
||||||
|
return JSON.stringify([taskID, assetID]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMockTaskAssetSource(taskID: string, assetID: number, source: MockTaskAssetSource) {
|
||||||
|
mockTaskAssetSources.set(mockTaskAssetSourceKey(taskID, assetID), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockAssetForTask(taskID: string, asset: Asset): Asset {
|
||||||
|
const source = mockTaskAssetSources.get(mockTaskAssetSourceKey(taskID, asset.id));
|
||||||
|
return source ? { ...asset, ...source } : asset;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteMockTaskAssetSources(taskID: string, assetID?: number) {
|
||||||
|
if (assetID !== undefined) {
|
||||||
|
mockTaskAssetSources.delete(mockTaskAssetSourceKey(taskID, assetID));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const key of mockTaskAssetSources.keys()) {
|
||||||
|
const [linkedTaskID] = JSON.parse(key) as [string, number];
|
||||||
|
if (linkedTaskID === taskID) mockTaskAssetSources.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mockTaskAssetID(taskID: string): number | undefined {
|
function mockTaskAssetID(taskID: string): number | undefined {
|
||||||
const numeric = Number(taskID);
|
const numeric = Number(taskID);
|
||||||
if (Number.isInteger(numeric) && numeric > 0) return numeric;
|
if (Number.isInteger(numeric) && numeric > 0) return numeric;
|
||||||
@@ -289,7 +315,10 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
const profileIDs = [...((b.llm_profile_ids as number[] | undefined) ?? [])];
|
const profileIDs = [...((b.llm_profile_ids as number[] | undefined) ?? [])];
|
||||||
const sourceTaskIDs = [...((b.source_task_ids as string[] | undefined) ?? [])];
|
const sourceTaskIDs = [...((b.source_task_ids as string[] | undefined) ?? [])];
|
||||||
const companyIDs = [...((b.company_ids as number[] | undefined) ?? [])];
|
const companyIDs = [...new Set((b.company_ids as number[] | undefined) ?? [])];
|
||||||
|
if (companyIDs.some((companyID) => !mockCompanies.some((company) => company.id === companyID))) {
|
||||||
|
throw new Error("关联企业不存在或无效");
|
||||||
|
}
|
||||||
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
|
const categoryID = typeof b.category_id === "number" ? b.category_id : undefined;
|
||||||
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
|
const category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
|
||||||
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
|
if (categoryID !== undefined && !category) throw new Error("任务分类不存在");
|
||||||
@@ -318,7 +347,19 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
source_task_ids: sourceTaskIDs,
|
source_task_ids: sourceTaskIDs,
|
||||||
company_ids: companyIDs,
|
company_ids: companyIDs,
|
||||||
};
|
};
|
||||||
mockTaskAssetIDs.set(id, nextMockTaskAssetID++);
|
const numericTaskID = nextMockTaskAssetID++;
|
||||||
|
mockTaskAssetIDs.set(id, numericTaskID);
|
||||||
|
const selectedCompanies = new Set(companyIDs);
|
||||||
|
for (const asset of mockAssets) {
|
||||||
|
if (asset.company_id === undefined || !selectedCompanies.has(asset.company_id)) continue;
|
||||||
|
if (!asset.task_ids.includes(numericTaskID)) asset.task_ids.push(numericTaskID);
|
||||||
|
const company = mockCompanies.find((candidate) => candidate.id === asset.company_id);
|
||||||
|
setMockTaskAssetSource(id, asset.id, {
|
||||||
|
task_source: "company",
|
||||||
|
task_source_summary: `任务创建时关联企业:${company?.name ?? `#${asset.company_id}`}`,
|
||||||
|
task_source_node_id: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const item of mockTasks) item.active = false;
|
for (const item of mockTasks) item.active = false;
|
||||||
mockTasks.unshift(created);
|
mockTasks.unshift(created);
|
||||||
mockActiveTask = id;
|
mockActiveTask = id;
|
||||||
@@ -462,9 +503,14 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
}
|
}
|
||||||
if (seg[0] === "tasks" && seg.length === 2 && m === "DELETE") {
|
if (seg[0] === "tasks" && seg.length === 2 && m === "DELETE") {
|
||||||
const id = seg[1];
|
const id = seg[1];
|
||||||
|
const numericTaskID = mockTaskAssetID(id);
|
||||||
const index = mockTasks.findIndex((item) => item.id === id);
|
const index = mockTasks.findIndex((item) => item.id === id);
|
||||||
if (index >= 0) mockTasks.splice(index, 1);
|
if (index >= 0) mockTasks.splice(index, 1);
|
||||||
mockTaskAssetIDs.delete(id);
|
mockTaskAssetIDs.delete(id);
|
||||||
|
deleteMockTaskAssetSources(id);
|
||||||
|
if (numericTaskID !== undefined) {
|
||||||
|
for (const asset of mockAssets) asset.task_ids = asset.task_ids.filter((taskID) => taskID !== numericTaskID);
|
||||||
|
}
|
||||||
|
|
||||||
let findingsDeleted = 0;
|
let findingsDeleted = 0;
|
||||||
if (b.delete_findings) {
|
if (b.delete_findings) {
|
||||||
@@ -701,7 +747,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
});
|
});
|
||||||
const limit = Number(q.get("limit") ?? 50);
|
const limit = Number(q.get("limit") ?? 50);
|
||||||
const offset = Number(q.get("offset") ?? 0);
|
const offset = Number(q.get("offset") ?? 0);
|
||||||
const page = list.slice(offset, offset + limit);
|
const page = list.slice(offset, offset + limit).map((asset) => (taskID ? mockAssetForTask(taskID, asset) : asset));
|
||||||
return { count: page.length, total: list.length, assets: page };
|
return { count: page.length, total: list.length, assets: page };
|
||||||
}
|
}
|
||||||
if (path === "/assets" && m === "DELETE") {
|
if (path === "/assets" && m === "DELETE") {
|
||||||
@@ -792,9 +838,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
asset.task_ids.push(numericTaskID);
|
asset.task_ids.push(numericTaskID);
|
||||||
mutation.assets_linked++;
|
mutation.assets_linked++;
|
||||||
}
|
}
|
||||||
asset.task_source = "manual";
|
setMockTaskAssetSource(seg[1], asset.id, {
|
||||||
asset.task_source_summary = "用户在测试资产页手工新增";
|
task_source: "manual",
|
||||||
asset.task_source_node_id = undefined;
|
task_source_summary: "用户在测试资产页手工新增",
|
||||||
|
task_source_node_id: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
mockTaskScopes.set(seg[1], currentScopes);
|
mockTaskScopes.set(seg[1], currentScopes);
|
||||||
return mutation;
|
return mutation;
|
||||||
@@ -812,9 +860,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
asset.task_ids.push(numericTaskID);
|
asset.task_ids.push(numericTaskID);
|
||||||
mutation.attached++;
|
mutation.attached++;
|
||||||
}
|
}
|
||||||
asset.task_source = "manual";
|
setMockTaskAssetSource(seg[1], asset.id, {
|
||||||
asset.task_source_summary = sourceSummary;
|
task_source: "manual",
|
||||||
asset.task_source_node_id = undefined;
|
task_source_summary: sourceSummary,
|
||||||
|
task_source_node_id: undefined,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return mutation;
|
return mutation;
|
||||||
}
|
}
|
||||||
@@ -824,11 +874,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
if (numericTaskID === undefined || !asset) throw new Error("任务或资产不存在");
|
if (numericTaskID === undefined || !asset) throw new Error("任务或资产不存在");
|
||||||
if (!asset.task_ids.includes(numericTaskID)) throw new Error("资产未关联当前任务");
|
if (!asset.task_ids.includes(numericTaskID)) throw new Error("资产未关联当前任务");
|
||||||
asset.task_ids = asset.task_ids.filter((id) => id !== numericTaskID);
|
asset.task_ids = asset.task_ids.filter((id) => id !== numericTaskID);
|
||||||
if (asset.task_ids.length === 0) {
|
deleteMockTaskAssetSources(seg[1], asset.id);
|
||||||
asset.task_source = undefined;
|
|
||||||
asset.task_source_summary = undefined;
|
|
||||||
asset.task_source_node_id = undefined;
|
|
||||||
}
|
|
||||||
return { detached: asset.id };
|
return { detached: asset.id };
|
||||||
}
|
}
|
||||||
if (seg[0] === "tasks" && seg[2] === "intent-assets" && seg.length === 3 && m === "GET") {
|
if (seg[0] === "tasks" && seg[2] === "intent-assets" && seg.length === 3 && m === "GET") {
|
||||||
@@ -843,8 +889,9 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
|||||||
}
|
}
|
||||||
const sourceTaskID = mockTaskAssetID(seg[1]) ?? 0;
|
const sourceTaskID = mockTaskAssetID(seg[1]) ?? 0;
|
||||||
const assets: IntentAsset[] = mappings.flatMap((mapping) => {
|
const assets: IntentAsset[] = mappings.flatMap((mapping) => {
|
||||||
const asset = mockAssets.find((item) => item.id === mapping.assetID);
|
const storedAsset = mockAssets.find((item) => item.id === mapping.assetID);
|
||||||
if (!asset) return [];
|
if (!storedAsset) return [];
|
||||||
|
const asset = mockAssetForTask(seg[1], storedAsset);
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
intent_id: mapping.intentID,
|
intent_id: mapping.intentID,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const TASK_ASSET_SOURCE_LABELS: Record<string, string> = {
|
|||||||
agent: "Agent 发现",
|
agent: "Agent 发现",
|
||||||
anchor: "黑板锚点",
|
anchor: "黑板锚点",
|
||||||
api: "资产 API",
|
api: "资产 API",
|
||||||
|
company: "企业关联",
|
||||||
legacy: "历史关联",
|
legacy: "历史关联",
|
||||||
manual: "人工加入",
|
manual: "人工加入",
|
||||||
system: "系统关联",
|
system: "系统关联",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface Task {
|
|||||||
llm_failover_state?: "default" | "ready" | "chain_exhausted" | string;
|
llm_failover_state?: "default" | "ready" | "chain_exhausted" | string;
|
||||||
llm_failover_reason?: string;
|
llm_failover_reason?: string;
|
||||||
source_task_ids?: string[]; // directly related tasks inherited as read-only context
|
source_task_ids?: string[]; // directly related tasks inherited as read-only context
|
||||||
company_ids?: number[]; // associated company asset scopes available as task context
|
company_ids?: number[]; // associated company scopes; current company assets join the task at creation
|
||||||
coverage_enabled?: boolean; // 资产覆盖度功能开关(创建时定,默认开);false=不计算/不展示覆盖度
|
coverage_enabled?: boolean; // 资产覆盖度功能开关(创建时定,默认开);false=不计算/不展示覆盖度
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user