|
|
@@ -1,19 +1,49 @@
|
|
|
"use client";
|
|
|
|
|
|
-// 策略配置面板(run 工作台第 5 面板):静态展示人定的旋钮——搜索 Prompt / 判定规则 / 游走护栏。
|
|
|
-// 本期只读、先不互链;配置为全局当前值(非该 run 快照)。
|
|
|
+// 策略配置面板(run 工作台第 5 面板):面向运营的业务化视图,三个干净模块——
|
|
|
+// ① 搜索 Prompt(只看发给大模型的原文)② 判定规则(红线/打分/入池线)③ 游走规则(扩展动作+总量去重)。
|
|
|
+// 原始字段表留在 /config 工程页,本面板不再堆变量名/术语。只读、配置为全局当前值(非该 run 快照)。
|
|
|
import { useEffect, useState, type ReactNode } from "react";
|
|
|
-import { FileText, ShieldCheck, Compass, Pencil } from "lucide-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";
|
|
|
-import { QueryPromptsTab, RulePacksTab, WalkPolicyTab } from "@/features/config/configTables";
|
|
|
-import { RulePackBody } from "@/features/runs/WalkJourney";
|
|
|
|
|
|
type AnyRec = Record<string, unknown>;
|
|
|
|
|
|
+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<string, { cn: string; cls: string }> = {
|
|
|
+ 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<string, string> = {
|
|
|
+ missing_platform_content_id: "内容缺少 ID(抓取不完整)",
|
|
|
+ missing_source_evidence: "缺少需求证据(无法溯源)",
|
|
|
+ missing_platform_author_id: "缺少作者 ID(无法进主页)"
|
|
|
+};
|
|
|
+
|
|
|
function ConfigBlock({
|
|
|
icon,
|
|
|
title,
|
|
|
+ subtitle,
|
|
|
editHint,
|
|
|
loading,
|
|
|
error,
|
|
|
@@ -21,6 +51,7 @@ function ConfigBlock({
|
|
|
}: {
|
|
|
icon: ReactNode;
|
|
|
title: string;
|
|
|
+ subtitle: string;
|
|
|
editHint: string;
|
|
|
loading: boolean;
|
|
|
error: boolean;
|
|
|
@@ -29,8 +60,11 @@ function ConfigBlock({
|
|
|
return (
|
|
|
<section className="cfg-block">
|
|
|
<div className="cfg-block-head">
|
|
|
- <strong>{icon} {title}</strong>
|
|
|
- <span className="cfg-edit-hint"><Pencil size={12} /> 改这个:{editHint}</span>
|
|
|
+ <div className="cfg-block-titles">
|
|
|
+ <strong>{icon} {title}</strong>
|
|
|
+ <span className="cfg-block-sub">{subtitle}</span>
|
|
|
+ </div>
|
|
|
+ <span className="cfg-edit-hint"><Pencil size={12} /> 改这里:{editHint}</span>
|
|
|
</div>
|
|
|
{loading ? (
|
|
|
<div className="loading-state">加载中…</div>
|
|
|
@@ -43,12 +77,183 @@ function ConfigBlock({
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+// ===== ① 搜索 Prompt:只看原文 =====
|
|
|
+function PromptModule({ data }: { data: AnyRec }) {
|
|
|
+ const profiles = rec(data.profiles);
|
|
|
+ const entries = Object.entries(profiles);
|
|
|
+ if (!entries.length) return <div className="empty-state">暂无 Prompt 配置。</div>;
|
|
|
+ return (
|
|
|
+ <div className="cfg-prompt">
|
|
|
+ {entries.map(([name, raw]) => {
|
|
|
+ const p = rec(raw);
|
|
|
+ return (
|
|
|
+ <div className="cfg-prompt-profile" key={name}>
|
|
|
+ {entries.length > 1 ? <div className="cfg-prompt-name">{name}</div> : null}
|
|
|
+ <div className="cfg-prompt-block">
|
|
|
+ <span className="cfg-prompt-tag">系统提示词 · System</span>
|
|
|
+ <pre className="cfg-prompt-text">{String(p.system || "—")}</pre>
|
|
|
+ </div>
|
|
|
+ <div className="cfg-prompt-block">
|
|
|
+ <span className="cfg-prompt-tag">用户提示词 · User</span>
|
|
|
+ <pre className="cfg-prompt-text">{String(p.user || "—")}</pre>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ <p className="cfg-hint-line">
|
|
|
+ 发给大模型的就是上面这两段原文。<code>{"{seed_term}"}</code>、<code>{"{evidence_context}"}</code> 等占位符会在运行时被真实内容替换。
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// ===== ② 判定规则:红线 / 打分 / 入池线 =====
|
|
|
+function JudgeModule({ pack }: { pack: AnyRec | null }) {
|
|
|
+ if (!pack) return <div className="empty-state">暂无判定规则。</div>;
|
|
|
+ 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 (
|
|
|
+ <div className="cfg-judge">
|
|
|
+ {/* 红线 */}
|
|
|
+ <div className="cfg-sub">
|
|
|
+ <div className="cfg-sub-title">红线 · 命中即处理(不进入打分)</div>
|
|
|
+ <div className="cfg-gate-list">
|
|
|
+ {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 (
|
|
|
+ <div className="cfg-gate-row" key={i}>
|
|
|
+ <span className="cfg-gate-label">{label}</span>
|
|
|
+ <span className={`cfg-act ${act.cls}`}>{act.cn}</span>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 打分 */}
|
|
|
+ <div className="cfg-sub">
|
|
|
+ <div className="cfg-sub-title">怎么打分 · 满分 {totalMax}</div>
|
|
|
+ <div className="cfg-score-bars">
|
|
|
+ {dims.map((d, i) => {
|
|
|
+ const max = n(d.max_score) || 0;
|
|
|
+ return (
|
|
|
+ <div className="cfg-score-item" key={i}>
|
|
|
+ <div className="cfg-score-top">
|
|
|
+ <span>{String(d.label || d.key)}</span>
|
|
|
+ <strong>{max} 分</strong>
|
|
|
+ </div>
|
|
|
+ <div className="cfg-score-bar">
|
|
|
+ <span style={{ width: `${(max / totalMax) * 100}%` }} />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* 入池线 */}
|
|
|
+ <div className="cfg-sub">
|
|
|
+ <div className="cfg-sub-title">入池线 · 按总分落档</div>
|
|
|
+ <div className="cfg-band-row">
|
|
|
+ {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 (
|
|
|
+ <div className={`cfg-band ${act.cls}`} key={i}>
|
|
|
+ <span className="cfg-band-range">{range}</span>
|
|
|
+ <strong className="cfg-band-act">{act.cn}</strong>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+// ===== ③ 游走规则:扩展动作 + 总量去重 =====
|
|
|
+type WalkAction = { title: string; desc: string; limits: string[] };
|
|
|
+
|
|
|
+function WalkModule({ data }: { data: AnyRec }) {
|
|
|
+ const g = rec(data.global);
|
|
|
+ const dedup = rec(data.dedup);
|
|
|
+ const budgetById: Record<string, AnyRec> = {};
|
|
|
+ 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: "同一个搜索词,翻到下一页拿更多结果。",
|
|
|
+ limits: [limit("query_next_page", "max_per_query", "每个搜索最多翻"), limit("query_next_page", "max_total_actions", "整轮最多")].filter(Boolean) as string[]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "进作者主页取更多作品",
|
|
|
+ desc: "看到一条好内容,顺着作者主页再拿几条同作者作品。",
|
|
|
+ limits: [limit("author_to_works", "max_works_per_author", "每个作者最多取"), limit("author_to_works", "max_total_actions", "整轮最多")].filter(Boolean) as string[]
|
|
|
+ },
|
|
|
+ {
|
|
|
+ title: "从内容拆话题再搜",
|
|
|
+ desc: "把好内容里的话题词拆出来,作为新搜索词继续找。",
|
|
|
+ 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 geminiCap = n(g.gemini_calls_per_run_cap);
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="cfg-walk">
|
|
|
+ <div className="cfg-sub">
|
|
|
+ <div className="cfg-sub-title">扩展动作 · 找到好内容后怎么继续找</div>
|
|
|
+ <div className="cfg-walk-actions">
|
|
|
+ {actions.map((a) => (
|
|
|
+ <div className="cfg-walk-card" key={a.title}>
|
|
|
+ <div className="cfg-walk-card-head">
|
|
|
+ <strong>{a.title}</strong>
|
|
|
+ <span className="cfg-act pool">开启</span>
|
|
|
+ </div>
|
|
|
+ <p className="cfg-walk-desc">{a.desc}</p>
|
|
|
+ <div className="cfg-walk-limits">
|
|
|
+ {a.limits.length ? a.limits.map((l, i) => <span key={i}>{l}</span>) : <span className="muted">无额外上限</span>}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="cfg-sub">
|
|
|
+ <div className="cfg-sub-title">总量与去重 · 防止跑太久、捞重复</div>
|
|
|
+ <div className="cfg-walk-global">
|
|
|
+ {totalActions != null ? <span><em>整轮最多</em><b>{totalActions} 个动作</b></span> : null}
|
|
|
+ {depth != null ? <span><em>最多深入</em><b>{depth} 层</b></span> : null}
|
|
|
+ {geminiCap != null ? <span><em>大模型判定上限</em><b>{geminiCap} 次/轮</b></span> : null}
|
|
|
+ <span><em>重复内容</em><b>{Object.keys(dedup).length ? "自动去重" : "自动去重"}</b></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
export function StrategyConfigPanel() {
|
|
|
const [prompts, setPrompts] = useState<ConfigFileResponse | null>(null);
|
|
|
const [rulePacks, setRulePacks] = useState<ConfigFileResponse | null>(null);
|
|
|
const [walkPolicy, setWalkPolicy] = useState<ConfigFileResponse | null>(null);
|
|
|
const [err, setErr] = useState<{ p: boolean; r: boolean; w: boolean }>({ p: false, r: false, w: false });
|
|
|
- const [showFull, setShowFull] = useState(false);
|
|
|
|
|
|
useEffect(() => {
|
|
|
let alive = true;
|
|
|
@@ -64,21 +269,46 @@ export function StrategyConfigPanel() {
|
|
|
|
|
|
return (
|
|
|
<div className="strategy-config">
|
|
|
- <p className="muted cfg-top-note">
|
|
|
- 下面是<strong>当前生效的全局策略配置</strong>(不是本次 run 的快照)。这些都由人来定、可改;改完下次 run 就按新配置跑。
|
|
|
- </p>
|
|
|
+ <div className="cfg-top">
|
|
|
+ <p className="muted cfg-top-note">
|
|
|
+ 下面是<strong>当前生效的全局策略配置</strong>(不是本次 run 的快照),都由人来定、可改;改完下次 run 就按新配置跑。
|
|
|
+ </p>
|
|
|
+ <Link className="text-button" href="/config">
|
|
|
+ <Table2 size={14} /> 查看完整规则表
|
|
|
+ </Link>
|
|
|
+ </div>
|
|
|
|
|
|
- <ConfigBlock icon={<FileText size={15} />} title="搜索 Prompt(AI 扩写搜索词)" editHint="query_prompts.v1.json / M2 prompt brief" loading={!prompts && !err.p} error={err.p}>
|
|
|
- {prompts ? <QueryPromptsTab data={prompts.data} /> : null}
|
|
|
+ <ConfigBlock
|
|
|
+ icon={<FileText size={15} />}
|
|
|
+ title="搜索 Prompt"
|
|
|
+ subtitle="发给大模型、用来扩写搜索词的原文"
|
|
|
+ editHint="query_prompts.v1.json"
|
|
|
+ loading={!prompts && !err.p}
|
|
|
+ error={err.p}
|
|
|
+ >
|
|
|
+ {prompts ? <PromptModule data={prompts.data} /> : null}
|
|
|
</ConfigBlock>
|
|
|
|
|
|
- <ConfigBlock icon={<ShieldCheck size={15} />} title="判定规则(内容入池 / 待复看 / 淘汰)" editHint="规则包映射配置表.xlsx" loading={!rulePacks && !err.r} error={err.r}>
|
|
|
- <RulePackBody pack={pack0} showFull={showFull} onToggleFull={() => setShowFull((v) => !v)} />
|
|
|
- {rulePacks ? <RulePacksTab data={rulePacks.data} /> : null}
|
|
|
+ <ConfigBlock
|
|
|
+ icon={<ShieldCheck size={15} />}
|
|
|
+ title="判定规则"
|
|
|
+ subtitle="一条内容怎么被判成 入池 / 待复看 / 淘汰"
|
|
|
+ editHint="规则包映射配置表.xlsx"
|
|
|
+ loading={!rulePacks && !err.r}
|
|
|
+ error={err.r}
|
|
|
+ >
|
|
|
+ <JudgeModule pack={pack0} />
|
|
|
</ConfigBlock>
|
|
|
|
|
|
- <ConfigBlock icon={<Compass size={15} />} title="游走护栏(扩展放行 / 预算上限)" editHint="游走策略配置表.xlsx / walk_policy.json" loading={!walkPolicy && !err.w} error={err.w}>
|
|
|
- {walkPolicy ? <WalkPolicyTab data={walkPolicy.data} /> : null}
|
|
|
+ <ConfigBlock
|
|
|
+ icon={<Compass size={15} />}
|
|
|
+ title="游走规则"
|
|
|
+ subtitle="找到一条好内容后,机器怎么顺藤摸瓜找更多"
|
|
|
+ editHint="游走策略配置表.xlsx"
|
|
|
+ loading={!walkPolicy && !err.w}
|
|
|
+ error={err.w}
|
|
|
+ >
|
|
|
+ {walkPolicy ? <WalkModule data={walkPolicy.data} /> : null}
|
|
|
</ConfigBlock>
|
|
|
</div>
|
|
|
);
|