"use client"; // 策略配置面板(run 工作台第 5 面板):面向运营的业务化视图,三个干净模块—— // ① 搜索 Prompt(只看发给大模型的原文)② 判定规则(红线/打分/入池线)③ 游走规则(扩展动作+总量去重)。 // 原始字段表留在 /config 工程页,本面板不再堆变量名/术语。只读、配置为全局当前值(非该 run 快照)。 import { useEffect, useState, type ReactNode } from "react"; import Link from "next/link"; import { Compass, FileText, Pencil, ShieldCheck, Table2 } from "lucide-react"; import { getConfigQueryPrompts, getConfigRulePacks, getConfigWalkPolicy } from "@/lib/api/client"; import type { ConfigFileResponse } from "@/lib/api/types"; type AnyRec = Record; function rec(v: unknown): AnyRec { return v && typeof v === "object" ? (v as AnyRec) : {}; } function rows(v: unknown): AnyRec[] { return Array.isArray(v) ? (v as AnyRec[]) : []; } // walk_policy 值可能是裸值,也可能 {value,...} 包裹 function uw(v: unknown): unknown { return v && typeof v === "object" && "value" in (v as AnyRec) ? (v as AnyRec).value : v; } function n(v: unknown): number | null { const x = Number(uw(v)); return Number.isFinite(x) ? x : null; } // 判定动作 → 业务文案 + 色板 const ACTION: Record = { ADD_TO_CONTENT_POOL: { cn: "入池", cls: "pool" }, KEEP_CONTENT_FOR_REVIEW: { cn: "转待复看", cls: "review" }, REJECT_CONTENT: { cn: "淘汰", cls: "reject" }, DO_NOT_EXPAND_AUTHOR: { cn: "不进作者主页", cls: "muted" } }; // 数据完整性类红线的技术 label 软化成人话 const GATE_RELABEL: Record = { missing_platform_content_id: "内容缺少 ID(抓取不完整)", missing_source_evidence: "缺少需求证据(无法溯源)", missing_platform_author_id: "缺少作者 ID(无法进主页)" }; function ConfigBlock({ icon, title, subtitle, editHint, loading, error, children }: { icon: ReactNode; title: string; subtitle: string; editHint: string; loading: boolean; error: boolean; children: ReactNode; }) { return (
{icon} {title} {subtitle}
改这里:{editHint}
{loading ? (
加载中…
) : error ? (
这块配置加载失败(其余不受影响)。
) : ( children )}
); } // ===== ① 搜索 Prompt:只看原文 ===== function PromptModule({ data }: { data: AnyRec }) { const profiles = rec(data.profiles); const entries = Object.entries(profiles); if (!entries.length) return
暂无 Prompt 配置。
; return (
{entries.map(([name, raw]) => { const p = rec(raw); return (
{entries.length > 1 ?
{name}
: null}
系统提示词 · System
{String(p.system || "—")}
用户提示词 · User
{String(p.user || "—")}
); })}

发给大模型的就是上面这两段原文。{"{seed_term}"}{"{evidence_context}"} 等占位符会在运行时被真实内容替换。

); } // ===== ② 判定规则:红线 / 打分 / 入池线 ===== function JudgeModule({ pack }: { pack: AnyRec | null }) { if (!pack) return
暂无判定规则。
; const gates = rows(pack.hard_gates); const dims = rows(rec(pack.scorecard).dimensions).filter((d) => String(d.runtime_status || "active") === "active"); const thresholds = rows(pack.thresholds); const totalMax = dims.reduce((s, d) => s + (n(d.max_score) || 0), 0) || 100; return (
{/* 红线 */}
红线 · 命中即处理(不进入打分)
{gates.map((g, i) => { const act = ACTION[String(g.decision_action || "")] || { cn: String(g.decision_action || ""), cls: "muted" }; const label = GATE_RELABEL[String(g.gate_id || "")] || String(g.label || g.gate_id || ""); return (
{label} {act.cn}
); })}
{/* 打分 */}
怎么打分 · 满分 {totalMax}
{dims.map((d, i) => { const max = n(d.max_score) || 0; return (
{String(d.label || d.key)} {max} 分
); })}
{/* 入池线 */}
入池线 · 按总分落档
{thresholds.map((t, i) => { const act = ACTION[String(t.decision_action || "")] || { cn: String(t.decision_action || ""), cls: "muted" }; const min = n(t.min_score); const max = n(t.max_score); const range = min != null && max != null ? `${min}–${max} 分` : "—"; return (
{range} {act.cn}
); })}
); } // ===== ③ 游走规则:起点判断 → 三种动作 → 拆标签后接着走 + 刹车 ===== type WalkAction = { title: string; desc: string; trigger: string; limits: string[] }; // edge_permissions 的放行值 → 业务文案 const PERM: Record = { allow: { cn: "放行", cls: "pool" }, allow_low_budget: { cn: "低预算放行", cls: "review" }, deny: { cn: "不放行", cls: "reject" } }; // 起点判断只看两种"发现型"游走(收入内容池属判定结果,不在此列) const WALK_EDGES: Array<{ edge: string; label: string }> = [ { edge: "author_to_works", label: "进作者主页" }, { edge: "video_to_hashtag", label: "拆标签再搜" } ]; const VERDICTS: Array<{ key: string; cn: string; cls: string }> = [ { key: "ADD_TO_CONTENT_POOL", cn: "入池", cls: "pool" }, { key: "KEEP_CONTENT_FOR_REVIEW", cn: "待复看", cls: "review" }, { key: "REJECT_CONTENT", cn: "淘汰", cls: "reject" } ]; function WalkModule({ data }: { data: AnyRec }) { const g = rec(data.global); const perm = rec(data.edge_permissions); const budgetById: Record = {}; rows(data.edge_budgets).forEach((b) => { budgetById[String(b.edge_id || "")] = b; }); const limit = (edge: string, key: string, unit: string): string | null => { const v = n(budgetById[edge]?.[key]); return v == null ? null : `${unit} ${v}`; }; const actions: WalkAction[] = [ { title: "翻页继续搜", desc: "同一个搜索词,翻到下一页拿更多结果。", trigger: "任何搜索都可翻页(与判定结果无关)", limits: [limit("query_next_page", "max_per_query", "每个搜索最多翻"), limit("query_next_page", "max_total_actions", "整轮最多")].filter(Boolean) as string[] }, { title: "进作者主页取作品", desc: "顺着这条内容的作者主页,再拿几条同作者作品。", trigger: "入池→正常进 · 待复看→只低预算探 · 淘汰→不进", limits: [limit("author_to_works", "max_works_per_author", "每个作者最多取"), limit("author_to_works", "max_total_actions", "整轮最多")].filter(Boolean) as string[] }, { title: "从内容拆标签再搜", desc: "把内容里的标签拆出来,作为新搜索词继续找。", trigger: "仅「入池」内容、且标签强相关才会拆", limits: [limit("hashtag_to_query", "max_total_actions", "整轮最多")].filter(Boolean) as string[] } ]; const totalActions = n(g.max_total_actions_per_run); const depth = n(g.max_depth); const reseedRounds = n(g.max_reseed_rounds); const geminiCap = n(g.gemini_calls_per_run_cap); return (
{/* A. 起点:用判定结果决定能不能继续游走 */}
从哪种内容开始游走 · 由判定结果决定

「好不好」不在这里另判,而是直接用上面②的判定结果:入池最优、放开游走;待复看只小范围探一探;淘汰到此为止。

判定结果 {WALK_EDGES.map((e) => ( {e.label} ))}
{VERDICTS.map((v) => { const row = rec(perm[v.key]); return (
{v.cn} {WALK_EDGES.map((e) => { const p = PERM[String(uw(row[e.edge]) || "deny")] || PERM.deny; return ( {p.cn} ); })}
); })}
{/* B. 三种游走动作 */}
能往哪三个方向走
{actions.map((a) => (
{a.title}

{a.desc}

触发:{a.trigger}
{a.limits.length ? a.limits.map((l, i) => {l}) : 无额外上限}
))}
{/* C. 拆标签之后接着往哪走 + 刹车 */}
拆标签之后,怎么接着走
入池内容拆出强相关标签 标签变成新的搜索词 这条新搜索重新走全流程:翻页 → 判定 → 对又判成入池的,再进作者主页 / 再拆标签 如此层层深入,直到撞上下面的刹车
{depth != null ? 最多深入 {depth} : null} {reseedRounds != null ? 拆标签再搜最多 {reseedRounds} : null} {totalActions != null ? 整轮最多 {totalActions} 个动作 : null} {geminiCap != null ? 大模型判定上限 {geminiCap} 次/轮 : null} 只有「入池」内容能当种子 只有强相关标签才扩散 重复内容自动去重
); } export function StrategyConfigPanel() { const [prompts, setPrompts] = useState(null); const [rulePacks, setRulePacks] = useState(null); const [walkPolicy, setWalkPolicy] = useState(null); const [err, setErr] = useState<{ p: boolean; r: boolean; w: boolean }>({ p: false, r: false, w: false }); useEffect(() => { let alive = true; getConfigQueryPrompts().then((r) => alive && setPrompts(r)).catch(() => alive && setErr((e) => ({ ...e, p: true }))); getConfigRulePacks().then((r) => alive && setRulePacks(r)).catch(() => alive && setErr((e) => ({ ...e, r: true }))); getConfigWalkPolicy().then((r) => alive && setWalkPolicy(r)).catch(() => alive && setErr((e) => ({ ...e, w: true }))); return () => { alive = false; }; }, []); const pack0 = ((rulePacks?.data?.rule_packs as AnyRec[] | undefined) || [])[0] || null; return (

下面是当前生效的全局策略配置(不是本次 run 的快照),都由人来定、可改;改完下次 run 就按新配置跑。

查看完整规则表
} title="搜索 Prompt" subtitle="发给大模型、用来扩写搜索词的原文" editHint="query_prompts.v1.json" loading={!prompts && !err.p} error={err.p} > {prompts ? : null} } title="判定规则" subtitle="一条内容怎么被判成 入池 / 待复看 / 淘汰" editHint="规则包映射配置表.xlsx" loading={!rulePacks && !err.r} error={err.r} > } title="游走规则" subtitle="找到一条好内容后,机器怎么顺藤摸瓜找更多" editHint="游走策略配置表.xlsx" loading={!walkPolicy && !err.w} error={err.w} > {walkPolicy ? : null}
); }