feat: 任务模板支持预设分类与任务级拦截/允许规则
- db: task_templates 加 category_id(FK task_categories, ON DELETE SET NULL) 与 intercept_rules JSONB(规则快照);带幂等 ALTER 补旧库。 - db/task_templates.go: 用 JSONB 存取规则;PATCH 用 Set 标志区分改/清。 - server: 模板请求体加 category_id + intercept_rules,复用 buildTaskInterceptRules 校验;PATCH 靠原始 key presence 判断是否更新(可清分类为 null)。 - web: 抽共享组件 asset-intercept-rules-editor(NativeSelect 版,避免 Sheet 内 下拉 portal 误关抽屉),创建表单与模板管理器共用;模板管理器新增分类下拉与 规则编辑区,另存为模板带当前分类/规则;应用模板回填分类与规则。
This commit is contained in:
1 parent
d5359e1f04
commit
777730e255
8 files changed
+342
-143
No files matched your search
@@ -44,6 +44,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { AssetInterceptRulesEditor } from "@/components/asset-intercept-rules-editor";
|
||||
import { StatusBadge } from "@/components/status-badge";
|
||||
import { TablePagination } from "@/components/table-pagination";
|
||||
import { TaskLLMProfileChain } from "@/components/task-llm-profile-chain";
|
||||
@@ -98,7 +99,6 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from "@/components/ui/item";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
@@ -122,7 +122,6 @@ import { api } from "@/lib/api";
|
||||
import { getLocalStorageValue, setLocalStorageValue } from "@/lib/local-storage.client";
|
||||
import { type SortDirection, useStoredSortPreference } from "@/lib/sort-preference";
|
||||
import type {
|
||||
AssetInterceptKind,
|
||||
AssetInterceptRuleInput,
|
||||
ChatAttachment,
|
||||
Company,
|
||||
@@ -3035,92 +3034,6 @@ function CategoryManagementSheet({
|
||||
);
|
||||
}
|
||||
|
||||
const TASK_INTERCEPT_KIND_OPTIONS: { value: AssetInterceptKind; label: string; placeholder: string }[] = [
|
||||
{ value: "exact_domain", label: "域名(全等)", placeholder: "example.gov.cn" },
|
||||
{ value: "exact_ip", label: "IP(全等)", placeholder: "203.0.113.10" },
|
||||
{ value: "exact_url", label: "URL(全等)", placeholder: "https://example.com/login" },
|
||||
{ value: "fuzzy_domain", label: "域名(模糊)", placeholder: ".gov.cn" },
|
||||
{ value: "fuzzy_ip", label: "IP(模糊)", placeholder: "203.0.113." },
|
||||
{ value: "fuzzy_url", label: "URL(模糊)", placeholder: "/admin" },
|
||||
{ value: "cidr", label: "CIDR 网段", placeholder: "192.168.0.0/16" },
|
||||
];
|
||||
|
||||
// TaskInterceptRulesField 是创建任务时录入「任务级资产拦截规则」的多行编辑区。
|
||||
// 规则只对本任务生效、不进全局表;与全局规则合并后在 agent 执行时匹配。
|
||||
function TaskInterceptRulesField({
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
value: AssetInterceptRuleInput[];
|
||||
onChange: (v: AssetInterceptRuleInput[]) => void;
|
||||
}) {
|
||||
function update(i: number, patch: Partial<AssetInterceptRuleInput>) {
|
||||
onChange(value.map((r, idx) => (idx === i ? { ...r, ...patch } : r)));
|
||||
}
|
||||
function remove(i: number) {
|
||||
onChange(value.filter((_, idx) => idx !== i));
|
||||
}
|
||||
function add() {
|
||||
onChange([...value, { action: "block", kind: "fuzzy_domain", pattern: "", note: "", enabled: true }]);
|
||||
}
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
{value.map((r, i) => {
|
||||
const ph = TASK_INTERCEPT_KIND_OPTIONS.find((o) => o.value === r.kind)?.placeholder ?? "";
|
||||
return (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
className="w-[84px] shrink-0"
|
||||
value={r.action}
|
||||
onChange={(e) => update(i, { action: e.target.value as "block" | "allow" })}
|
||||
>
|
||||
<NativeSelectOption value="block">拦截</NativeSelectOption>
|
||||
<NativeSelectOption value="allow">允许</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<NativeSelect
|
||||
size="sm"
|
||||
className="w-[120px] shrink-0"
|
||||
value={r.kind}
|
||||
onChange={(e) => update(i, { kind: e.target.value as AssetInterceptKind })}
|
||||
>
|
||||
{TASK_INTERCEPT_KIND_OPTIONS.map((o) => (
|
||||
<NativeSelectOption key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</NativeSelectOption>
|
||||
))}
|
||||
</NativeSelect>
|
||||
<Input
|
||||
className="flex-1"
|
||||
placeholder={ph}
|
||||
value={r.pattern}
|
||||
onChange={(e) => update(i, { pattern: e.target.value })}
|
||||
/>
|
||||
<Input
|
||||
className="w-[120px] shrink-0"
|
||||
placeholder="备注(可选)"
|
||||
value={r.note}
|
||||
onChange={(e) => update(i, { note: e.target.value })}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive size-8 shrink-0"
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<Trash2Icon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<Button type="button" size="sm" variant="outline" className="w-fit" onClick={add}>
|
||||
<PlusIcon className="size-4" /> 添加一条
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CreateTaskSheet({
|
||||
tasks,
|
||||
categories,
|
||||
@@ -3268,11 +3181,15 @@ function CreateTaskSheet({
|
||||
<TaskTemplateControls
|
||||
description={description}
|
||||
goal={goal}
|
||||
categoryID={categoryID}
|
||||
interceptRules={interceptRules}
|
||||
selectedTemplateID={selectedTemplateID}
|
||||
onSelectedTemplateIDChange={setSelectedTemplateID}
|
||||
onApply={(template) => {
|
||||
setDescription(template.description);
|
||||
setGoal(template.goal);
|
||||
setCategoryID(template.category_id ?? undefined);
|
||||
setInterceptRules(template.intercept_rules ?? []);
|
||||
setUploadCount(0);
|
||||
}}
|
||||
portalContainer={sheetContentRef}
|
||||
@@ -3370,7 +3287,7 @@ function CreateTaskSheet({
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="task-intercept-rules">任务级资产拦截 / 允许规则(可选)</FieldLabel>
|
||||
<TaskInterceptRulesField value={interceptRules} onChange={setInterceptRules} />
|
||||
<AssetInterceptRulesEditor value={interceptRules} onChange={setInterceptRules} />
|
||||
<FieldDescription>
|
||||
仅对本任务生效,不写入全局规则。判定顺序:先按「拦截」规则(含全局)匹配,命中即禁止测试;未命中且本任务配置了「允许」规则时,须命中某条允许规则才放行,否则同样不允许测试;未配置任何允许规则则不启用白名单。
|
||||
</FieldDescription>
|
||||
|
||||
Reference in new issue
Block a user