修复
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"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -51,6 +52,12 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
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{
|
||||
CompanyIDs: []int64{companyID},
|
||||
@@ -62,7 +69,14 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
||||
|
||||
tools := NewToolSet(d.Exploration(task.ExplorationID), "planner")
|
||||
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()
|
||||
coverage, ok := overview["coverage"].(map[string]any)
|
||||
if !ok {
|
||||
@@ -89,6 +103,95 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) {
|
||||
if !ok || len(keywords) != 1 || keywords[0] != keyword {
|
||||
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) {
|
||||
|
||||
@@ -177,9 +177,13 @@ func TestCompanyScopeInvalid(t *testing.T) {
|
||||
}
|
||||
defer cleanupCompany(d, id)
|
||||
|
||||
// TLD-only domains and overly broad CIDRs should be rejected
|
||||
lines := []string{"com", "1.2.3.4/8"} // rejected
|
||||
added, _, invalid, _ := cs.AddScope(id, lines, "test")
|
||||
// Explicitly typed TLD-only domains and overly broad CIDRs should be
|
||||
// rejected. Untyped plain text is intentionally classified as a keyword.
|
||||
inputs := []ScopeInput{
|
||||
{Kind: "domain", Value: "com"},
|
||||
{Kind: "cidr", Value: "1.2.3.4/8"},
|
||||
}
|
||||
added, _, invalid, _ := cs.AddScopeInputs(id, inputs, "test")
|
||||
if added != 0 {
|
||||
t.Errorf("want 0 added for invalid lines, got %d", added)
|
||||
}
|
||||
|
||||
+35
-2
@@ -89,10 +89,12 @@ const MinPlanHeartbeatSeconds = 600
|
||||
const MaxTaskSourceCount = 8
|
||||
|
||||
// MaxTaskCompanyCount bounds the number of company asset scopes attached to a
|
||||
// task. Company scopes are prompt context, so an unbounded list would inflate
|
||||
// every planner and worker call.
|
||||
// task. Company scopes are prompt context, and their currently attributed
|
||||
// assets are snapshotted into the task at creation time.
|
||||
const MaxTaskCompanyCount = 32
|
||||
|
||||
const taskCompanyAssetSource = "company"
|
||||
|
||||
var (
|
||||
ErrTaskCompanyIDsInvalid = errors.New("invalid task company ids")
|
||||
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 {
|
||||
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
|
||||
err := tx.QueryRow(`
|
||||
WITH requested(company_id, position) AS (
|
||||
@@ -268,6 +275,32 @@ SELECT count(*) FROM inserted`, taskID, companyIDs).Scan(&inserted)
|
||||
if inserted != len(companyIDs) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+78
-1
@@ -509,10 +509,36 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
emptyCompany, _, err := d.Companies().UpsertCompany(fmt.Sprintf("Task Empty Company %d", suffix), "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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{
|
||||
CompanyIDs: []int64{companyA, companyB},
|
||||
})
|
||||
@@ -537,6 +563,57 @@ func TestCreateTaskWithCompanyScopes(t *testing.T) {
|
||||
if scopeCount != 2 {
|
||||
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{
|
||||
CompanyIDs: []int64{companyA, companyA, companyB, companyA},
|
||||
|
||||
+1
-1
@@ -1363,7 +1363,7 @@ type createTaskReq struct {
|
||||
LLMProfileID *int64 `json:"llm_profile_id,omitempty"` // 指定运行本任务的 LLM 配置;省略/null=用激活配置
|
||||
LLMProfileIDs []int64 `json:"llm_profile_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/省略=不限时
|
||||
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 轮)。
|
||||
|
||||
@@ -2613,8 +2613,8 @@ function CreateTaskSheet({
|
||||
portalContainer={sheetContentRef}
|
||||
/>
|
||||
<FieldDescription>
|
||||
将所选企业的域名、IP、CIDR、ICP 和企业关键词作为 Agent
|
||||
可选范围上下文;不会自动生成意图或强制改变执行目标。
|
||||
创建任务时会将所选企业当前已有资产加入“测试资产”,并将域名、IP、CIDR、ICP 和企业关键词提供给 Agent
|
||||
作为范围上下文;不会自动生成意图或强制改变执行目标。
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Field>
|
||||
|
||||
@@ -39,7 +39,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { api } from "@/lib/api";
|
||||
import type { TrafficDetail, TrafficExchange, TrafficHost, TrafficResp } from "@/lib/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, copyText } from "@/lib/utils";
|
||||
|
||||
function fmtTime(ts: string) {
|
||||
return new Date(ts).toLocaleString("zh-CN", {
|
||||
@@ -199,12 +199,12 @@ function HttpCodeBlock({ raw }: { raw: string }) {
|
||||
}, [copied]);
|
||||
|
||||
const copyPacket = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
const ok = await copyText(value);
|
||||
if (ok) {
|
||||
setCopied(true);
|
||||
} catch {
|
||||
toast.error("复制失败,请使用 Ctrl/Cmd+A 后复制");
|
||||
return;
|
||||
}
|
||||
toast.error("复制失败,请使用 Ctrl/Cmd+A 后复制");
|
||||
};
|
||||
|
||||
const renderLine = (line: string, index: number) => {
|
||||
|
||||
+63
-16
@@ -42,9 +42,35 @@ const mockCompanies = structuredClone(D.companies);
|
||||
const mockAssets = structuredClone(D.assets);
|
||||
const mockTaskAssetIDs = new Map(D.tasks.map((task, index) => [task.id, index + 1]));
|
||||
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 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 {
|
||||
const numeric = Number(taskID);
|
||||
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 profileIDs = [...((b.llm_profile_ids as number[] | 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 category = categoryID === undefined ? undefined : mockTaskCategories.find((item) => item.id === categoryID);
|
||||
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,
|
||||
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;
|
||||
mockTasks.unshift(created);
|
||||
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") {
|
||||
const id = seg[1];
|
||||
const numericTaskID = mockTaskAssetID(id);
|
||||
const index = mockTasks.findIndex((item) => item.id === id);
|
||||
if (index >= 0) mockTasks.splice(index, 1);
|
||||
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;
|
||||
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 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 };
|
||||
}
|
||||
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);
|
||||
mutation.assets_linked++;
|
||||
}
|
||||
asset.task_source = "manual";
|
||||
asset.task_source_summary = "用户在测试资产页手工新增";
|
||||
asset.task_source_node_id = undefined;
|
||||
setMockTaskAssetSource(seg[1], asset.id, {
|
||||
task_source: "manual",
|
||||
task_source_summary: "用户在测试资产页手工新增",
|
||||
task_source_node_id: undefined,
|
||||
});
|
||||
}
|
||||
mockTaskScopes.set(seg[1], currentScopes);
|
||||
return mutation;
|
||||
@@ -812,9 +860,11 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re
|
||||
asset.task_ids.push(numericTaskID);
|
||||
mutation.attached++;
|
||||
}
|
||||
asset.task_source = "manual";
|
||||
asset.task_source_summary = sourceSummary;
|
||||
asset.task_source_node_id = undefined;
|
||||
setMockTaskAssetSource(seg[1], asset.id, {
|
||||
task_source: "manual",
|
||||
task_source_summary: sourceSummary,
|
||||
task_source_node_id: undefined,
|
||||
});
|
||||
}
|
||||
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 (!asset.task_ids.includes(numericTaskID)) throw new Error("资产未关联当前任务");
|
||||
asset.task_ids = asset.task_ids.filter((id) => id !== numericTaskID);
|
||||
if (asset.task_ids.length === 0) {
|
||||
asset.task_source = undefined;
|
||||
asset.task_source_summary = undefined;
|
||||
asset.task_source_node_id = undefined;
|
||||
}
|
||||
deleteMockTaskAssetSources(seg[1], asset.id);
|
||||
return { detached: asset.id };
|
||||
}
|
||||
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 assets: IntentAsset[] = mappings.flatMap((mapping) => {
|
||||
const asset = mockAssets.find((item) => item.id === mapping.assetID);
|
||||
if (!asset) return [];
|
||||
const storedAsset = mockAssets.find((item) => item.id === mapping.assetID);
|
||||
if (!storedAsset) return [];
|
||||
const asset = mockAssetForTask(seg[1], storedAsset);
|
||||
return [
|
||||
{
|
||||
intent_id: mapping.intentID,
|
||||
|
||||
@@ -13,6 +13,7 @@ const TASK_ASSET_SOURCE_LABELS: Record<string, string> = {
|
||||
agent: "Agent 发现",
|
||||
anchor: "黑板锚点",
|
||||
api: "资产 API",
|
||||
company: "企业关联",
|
||||
legacy: "历史关联",
|
||||
manual: "人工加入",
|
||||
system: "系统关联",
|
||||
|
||||
@@ -35,7 +35,7 @@ export interface Task {
|
||||
llm_failover_state?: "default" | "ready" | "chain_exhausted" | string;
|
||||
llm_failover_reason?: string;
|
||||
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=不计算/不展示覆盖度
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user