From acd1ac29d732c8345876a51a901326b9b36afab1 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Mon, 31 Aug 2026 11:22:34 -0400 Subject: [PATCH] =?UTF-8?q?feat(assets):=20=E5=8E=BB=E6=8E=89=E4=BC=81?= =?UTF-8?q?=E4=B8=9A=E8=B5=84=E4=BA=A7=E8=8C=83=E5=9B=B4=20256=20=E6=9D=A1?= =?UTF-8?q?=E8=A7=84=E5=88=99=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 逐个 IP / 域名录范围的企业很容易撞上这个上限,撞上之后只能拆成多个企业, 而范围本身并不因为条数多就更慢。 - db:删掉 MaxCompanyScopeRules 常量、请求条数校验与 ensureCompanyScopeCapacityTx(每企业持久化总数的事务内校验) - 前端:去掉 MAX_COMPANY_SCOPE_RULES 与「剩余额度」计算,追加范围弹窗不再 按剩余条数报错;demo mock 的两处上限一并移除 - 单条规则仍限 1024 字符;请求正文 2 MiB(maxCompanyMutationBodyBytes)保留 作兜底,约合四五万条规则 测试:原先断言「超容量被拒」的两个用例翻转为「超过旧上限也能写入」—— db 侧批量写 300 条 + 追加 1 条,server 侧一次创建带 300 条范围的企业。 新用例先注册清理再断言,避免断言失败把企业留在共享开发库里。 --- CHANGELOG.md | 6 ++++ db/companies.go | 31 +++------------------ db/company_scope_consistency_test.go | 24 ++++++++-------- server/assets_scope_test.go | 19 +++++++++---- web/src/app/(main)/function/assets/page.tsx | 8 ++---- web/src/lib/company-scope.ts | 10 +------ web/src/lib/mock/handler.ts | 7 +---- 7 files changed, 41 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd9c84..38b705c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ - 任务 LLM 配置链改为任何状态都可修改,不再限制「运行中 / 暂停 / 配置链耗尽」:任务结束(done / failed / timeout)后主 Agent 对话仍走这条链,链上模型出问题时原先既不能改也就没法继续交互。后端 HTTP 与 DB 事务两处终态拦截一并去掉,前端弹窗对终态任务也开放编辑与保存。终态任务保存时不再重开额度阻塞意图(那些意图会被挪到 open,既没有 worker 执行、也不再满足「重跑意图」的条件),想继续跑仍走重跑意图 / 新增目标,由它们把任务重新拉回运行态。 +### 资产 + +#### 修改的功能 + +- 去掉企业资产范围「最多 256 条规则」的限制:逐个 IP / 域名录入范围的企业很容易撞上这个上限,撞上之后只能拆成多个企业,而范围本身并不因为条数多就更慢。前后端与 demo mock 三处上限一并移除,单条规则仍限 1024 字符,请求正文 2 MiB 的上限保留作兜底(约合四五万条规则)。 + ### UI #### 新增的功能 diff --git a/db/companies.go b/db/companies.go index d2d5745..f7b122b 100644 --- a/db/companies.go +++ b/db/companies.go @@ -52,9 +52,9 @@ var ( ) const ( - // MaxCompanyScopeRules bounds both request processing and the total number - // of persisted rules for one company. - MaxCompanyScopeRules = 256 + // 企业范围不限制规则条数:逐个 IP / 域名录入的范围动辄上千条,封顶只会逼用户 + // 拆成多个企业。请求体大小(server 侧 maxCompanyMutationBodyBytes)仍然兜底。 + // // Raw and normalized textual scope payloads are bounded by Unicode rune // count so multi-byte input is treated consistently by the API and DB layer. MaxCompanyScopeRawRunes = 1024 @@ -69,12 +69,8 @@ func (e *CompanyScopeValidationError) Error() string { return e.Message } // ValidateCompanyScopeInputBounds applies request-wide limits before parsing. // Store methods call it again so non-HTTP callers cannot bypass the limits. +// 只约束单条规则的长度,不限制条数。 func ValidateCompanyScopeInputBounds(inputs []ScopeInput) error { - if len(inputs) > MaxCompanyScopeRules { - return &CompanyScopeValidationError{Message: fmt.Sprintf( - "企业范围规则过多: 最多 %d 条", MaxCompanyScopeRules, - )} - } for i, input := range inputs { if utf8.RuneCountInString(input.Value) > MaxCompanyScopeRawRunes { return &CompanyScopeValidationError{Message: fmt.Sprintf( @@ -373,9 +369,6 @@ func (s *CompanyStore) AddScopeInputsChecked(companyID int64, inputs []ScopeInpu if err != nil { return 0, 0, invalid, errors, err } - if err := ensureCompanyScopeCapacityTx(tx, companyID); err != nil { - return 0, 0, invalid, errors, err - } if needsAttribution { warning, err := recomputeAttributionTx(tx) if err != nil { @@ -430,19 +423,6 @@ func ensureCompanyExistsTx(tx *sql.Tx, companyID int64) error { return nil } -func ensureCompanyScopeCapacityTx(tx *sql.Tx, companyID int64) error { - var count int - if err := tx.QueryRow(`SELECT count(*) FROM company_scope WHERE company_id = $1`, companyID).Scan(&count); err != nil { - return err - } - if count > MaxCompanyScopeRules { - return &CompanyScopeValidationError{Message: fmt.Sprintf( - "企业范围规则过多: 每个企业最多 %d 条", MaxCompanyScopeRules, - )} - } - return nil -} - func lockCompanyScopeMutation(tx *sql.Tx) error { _, err := tx.Exec(`SELECT pg_advisory_xact_lock($1)`, companyScopeMutationLock) return err @@ -714,9 +694,6 @@ func (s *CompanyStore) UpdateScopeInputsChecked(companyID int64, inputs []ScopeI if err != nil { return 0, invalid, errs, err } - if err := ensureCompanyScopeCapacityTx(tx, companyID); err != nil { - return 0, invalid, errs, err - } // Rebuild even for an empty replacement because removing the old rules may // detach scope-derived assets or expose a lower-precedence company match. warning, err := recomputeAttributionTx(tx) diff --git a/db/company_scope_consistency_test.go b/db/company_scope_consistency_test.go index 19e83fd..211edf1 100644 --- a/db/company_scope_consistency_test.go +++ b/db/company_scope_consistency_test.go @@ -180,9 +180,9 @@ func TestCompanyScopeLimitsAndCheckedErrors(t *testing.T) { if err := ValidateCompanyScopeInputBounds([]ScopeInput{{Kind: "keyword", Value: boundary + "界"}}); !errors.As(err, &validationErr) { t.Fatalf("oversized raw value error=%v want CompanyScopeValidationError", err) } - tooMany := make([]ScopeInput, MaxCompanyScopeRules+1) - if err := ValidateCompanyScopeInputBounds(tooMany); !errors.As(err, &validationErr) { - t.Fatalf("too many rules error=%v want CompanyScopeValidationError", err) + // 条数不再设上限,只校验单条长度。 + if err := ValidateCompanyScopeInputBounds(make([]ScopeInput, 1000)); err != nil { + t.Fatalf("rule count should be unbounded, got %v", err) } d, _, companies := testSetup(t) @@ -193,24 +193,26 @@ func TestCompanyScopeLimitsAndCheckedErrors(t *testing.T) { t.Fatal(err) } defer cleanupCompany(d, companyID) - rules := make([]ScopeInput, MaxCompanyScopeRules) + // 曾经封顶 256 条,逐个 IP / 域名录范围的企业很容易撞上;现在不限条数。 + const bulk = 300 + rules := make([]ScopeInput, bulk) for i := range rules { rules[i] = ScopeInput{Kind: "keyword", Value: fmt.Sprintf("limit-%d-%d", stamp, i)} } - if added, _, _, _, err := companies.AddScopeInputsChecked(companyID, rules, "test"); err != nil || added != MaxCompanyScopeRules { - t.Fatalf("fill scope capacity: added=%d err=%v", added, err) + if added, _, _, _, err := companies.AddScopeInputsChecked(companyID, rules, "test"); err != nil || added != bulk { + t.Fatalf("bulk scope insert: added=%d want=%d err=%v", added, bulk, err) } if added, _, _, _, err := companies.AddScopeInputsChecked(companyID, []ScopeInput{ - {Kind: "keyword", Value: fmt.Sprintf("limit-%d-overflow", stamp)}, - }, "test"); added != 0 || !errors.As(err, &validationErr) { - t.Fatalf("scope overflow: added=%d err=%v", added, err) + {Kind: "keyword", Value: fmt.Sprintf("limit-%d-extra", stamp)}, + }, "test"); added != 1 || err != nil { + t.Fatalf("append past the old cap: added=%d err=%v", added, err) } var count int if err := d.QueryRow(`SELECT count(*) FROM company_scope WHERE company_id=$1`, companyID).Scan(&count); err != nil { t.Fatal(err) } - if count != MaxCompanyScopeRules { - t.Fatalf("failed capacity mutation changed scope count=%d want=%d", count, MaxCompanyScopeRules) + if count != bulk+1 { + t.Fatalf("scope count=%d want=%d", count, bulk+1) } missingID := companyID + 1_000_000_000 diff --git a/server/assets_scope_test.go b/server/assets_scope_test.go index c3e6b7c..a9b09d5 100644 --- a/server/assets_scope_test.go +++ b/server/assets_scope_test.go @@ -151,21 +151,30 @@ func TestCompanyScopeHTTPErrorClassificationAndBounds(t *testing.T) { } }) - t.Run("too many scope rules is 400", func(t *testing.T) { - scope := make([]map[string]string, db.MaxCompanyScopeRules+1) + t.Run("scope rule count is unbounded", func(t *testing.T) { + scope := make([]map[string]string, 300) for i := range scope { - scope[i] = map[string]string{"kind": "keyword", "value": fmt.Sprintf("keyword-%d", i)} + scope[i] = map[string]string{"kind": "keyword", "value": fmt.Sprintf("unbounded-%d-%d", time.Now().UnixNano(), i)} } - body, err := json.Marshal(map[string]any{"name": "Too Many Scope Rules", "scope": scope}) + body, err := json.Marshal(map[string]any{"name": fmt.Sprintf("Unbounded Scope %d", time.Now().UnixNano()), "scope": scope}) if err != nil { t.Fatal(err) } req := httptest.NewRequest(http.MethodPost, "/api/companies", bytes.NewReader(body)) rec := httptest.NewRecorder() s.createCompany(rec, req) - if rec.Code != http.StatusBadRequest { + var created struct { + ID int64 `json:"id"` + Added int `json:"scope_added"` + } + _ = json.Unmarshal(rec.Body.Bytes(), &created) + defer func() { _ = m.pg.Companies().DeleteCompany(created.ID) }() + if rec.Code != http.StatusCreated { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } + if created.Added != len(scope) { + t.Fatalf("scope_added=%d want=%d", created.Added, len(scope)) + } }) t.Run("oversized request body is 413", func(t *testing.T) { diff --git a/web/src/app/(main)/function/assets/page.tsx b/web/src/app/(main)/function/assets/page.tsx index bcc4438..697543d 100644 --- a/web/src/app/(main)/function/assets/page.tsx +++ b/web/src/app/(main)/function/assets/page.tsx @@ -59,7 +59,7 @@ import { import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { api } from "@/lib/api"; -import { MAX_COMPANY_SCOPE_RULES, parseCompanyScopeText } from "@/lib/company-scope"; +import { parseCompanyScopeText } from "@/lib/company-scope"; import type { Asset, Company, CompanyScopeRule } from "@/lib/types"; import { cn } from "@/lib/utils"; @@ -1510,11 +1510,7 @@ function AppendScopeDialog({ company, onSaved }: { company: Company; onSaved: () const [scopeText, setScopeText] = React.useState(""); const [reason, setReason] = React.useState(""); const [busy, setBusy] = React.useState(false); - const remainingScopeRules = Math.max(0, MAX_COMPANY_SCOPE_RULES - (company.scope?.length || 0)); - const parsedScope = React.useMemo( - () => parseCompanyScopeText(scopeText, { maxRules: remainingScopeRules }), - [remainingScopeRules, scopeText], - ); + const parsedScope = React.useMemo(() => parseCompanyScopeText(scopeText), [scopeText]); React.useEffect(() => { if (!open) return; diff --git a/web/src/lib/company-scope.ts b/web/src/lib/company-scope.ts index c818b65..dba5f7d 100644 --- a/web/src/lib/company-scope.ts +++ b/web/src/lib/company-scope.ts @@ -1,6 +1,5 @@ import type { CompanyScopeKind, CompanyScopeRule } from "@/lib/types"; -export const MAX_COMPANY_SCOPE_RULES = 256; export const MAX_COMPANY_SCOPE_VALUE_LENGTH = 1024; export type CompanyScopeTextIssue = { @@ -173,7 +172,7 @@ export function classifyCompanyScopeLine( export function parseCompanyScopeText( value: string, - options: { preservedRules?: CompanyScopeRule[]; maxRules?: number } = {}, + options: { preservedRules?: CompanyScopeRule[] } = {}, ): ParsedCompanyScopeText { const nonEmpty = value .split(/\r?\n/) @@ -190,13 +189,6 @@ export function parseCompanyScopeText( const preservedRule = queue?.shift(); return classifyCompanyScopeLine(raw, line, preservedRule); }); - const maxRules = options.maxRules ?? MAX_COMPANY_SCOPE_RULES; - if (nonEmpty.length > maxRules) { - issues.push({ - line: nonEmpty[Math.max(0, maxRules)]?.line ?? 1, - error: `最多允许 ${maxRules} 条范围,当前 ${nonEmpty.length} 条`, - }); - } return { rules: issues.flatMap((item) => (item.rule ? [item.rule] : [])), errors: issues.flatMap((item) => (item.error ? [{ line: item.line, error: item.error }] : [])), diff --git a/web/src/lib/mock/handler.ts b/web/src/lib/mock/handler.ts index d60cd63..3eaab34 100644 --- a/web/src/lib/mock/handler.ts +++ b/web/src/lib/mock/handler.ts @@ -6,7 +6,6 @@ import { classifyCompanyScopeLine, companyScopeRuleError, isCompanyScopeKind, - MAX_COMPANY_SCOPE_RULES, normalizeCompanyScopeValue, } from "../company-scope"; import type { @@ -599,7 +598,6 @@ function mockScopeRows( existing: ScopeRow[] = [], ): { rows: ScopeRow[]; invalid: number; skipped: number } { if (!Array.isArray(input)) return { rows: [], invalid: 0, skipped: 0 }; - if (input.length > MAX_COMPANY_SCOPE_RULES) throw new Error(`企业范围最多支持 ${MAX_COMPANY_SCOPE_RULES} 条`); let nextID = mockCompanies.flatMap((company) => company.scope ?? []).reduce((max, row) => Math.max(max, row.id), 0) + 1; const rows: ScopeRow[] = []; @@ -636,9 +634,6 @@ function mockScopeRows( keys.add(key); rows.push(row); } - if (existing.length + rows.length > MAX_COMPANY_SCOPE_RULES) { - throw new Error(`企业范围规则过多:每个企业最多 ${MAX_COMPANY_SCOPE_RULES} 条`); - } return { rows, invalid, skipped }; } @@ -1438,7 +1433,7 @@ function route(m: string, path: string, seg: string[], q: URLSearchParams, b: Re const numericTaskID = mockTaskAssetID(seg[1]); if (!task || numericTaskID === undefined) throw new Error("任务不存在"); if (Array.isArray(b.scope)) { - if (b.scope.length === 0 || b.scope.length > MAX_COMPANY_SCOPE_RULES) throw new Error("请填写有效测试范围"); + if (b.scope.length === 0) throw new Error("请填写有效测试范围"); const rules: CompanyScopeRule[] = b.scope.map((candidate, index) => { if (typeof candidate === "string") { const issue = classifyCompanyScopeLine(candidate, index + 1);