|
|
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
import {
|
|
|
getConfigQueryPrompts,
|
|
|
getConfigRulePacks,
|
|
|
+ getConfigWalkPolicy,
|
|
|
getConfigWalkStrategy
|
|
|
} from "@/lib/api/client";
|
|
|
import type { ConfigFileResponse } from "@/lib/api/types";
|
|
|
@@ -14,16 +15,33 @@ import { fieldAnnotation } from "@/lib/config/fieldLabels";
|
|
|
const TABS = [
|
|
|
{ id: "rule-packs", label: "规则包" },
|
|
|
{ id: "walk-strategy", label: "游走策略" },
|
|
|
+ { id: "walk-policy", label: "游走护栏(walk-policy)" },
|
|
|
{ id: "query-prompts", label: "Query Prompts" }
|
|
|
] as const;
|
|
|
|
|
|
type TabId = (typeof TABS)[number]["id"];
|
|
|
|
|
|
+// walk_policy 的拍板值可能裸值,也可能带 {value, provenance, tbd} 留痕包裹。
|
|
|
+function unwrap(value: unknown): unknown {
|
|
|
+ if (value && typeof value === "object" && "value" in (value as Record<string, unknown>)) {
|
|
|
+ return (value as Record<string, unknown>).value;
|
|
|
+ }
|
|
|
+ return value;
|
|
|
+}
|
|
|
+
|
|
|
+function provenanceOf(value: unknown): string {
|
|
|
+ if (value && typeof value === "object" && "provenance" in (value as Record<string, unknown>)) {
|
|
|
+ return String((value as Record<string, unknown>).provenance || "");
|
|
|
+ }
|
|
|
+ return "";
|
|
|
+}
|
|
|
+
|
|
|
function cell(value: unknown): string {
|
|
|
- if (value == null || value === "") return "—";
|
|
|
- if (typeof value === "boolean") return value ? "是" : "否";
|
|
|
- if (typeof value === "object") return JSON.stringify(value, null, 0);
|
|
|
- return String(value);
|
|
|
+ const v = unwrap(value);
|
|
|
+ if (v == null || v === "") return "—";
|
|
|
+ if (typeof v === "boolean") return v ? "是" : "否";
|
|
|
+ if (typeof v === "object") return JSON.stringify(v, null, 0);
|
|
|
+ return String(v);
|
|
|
}
|
|
|
|
|
|
function ConfigTable({
|
|
|
@@ -105,10 +123,6 @@ function asRecord(value: unknown): Record<string, unknown> {
|
|
|
function RulePacksTab({ data }: { data: Record<string, unknown> }) {
|
|
|
const dispatch = asRows(data.rule_pack_dispatch);
|
|
|
const packs = asRows(data.rule_packs);
|
|
|
- const contentPack = asRecord(
|
|
|
- packs.find((pack) => pack.rule_pack_id === "douyin_content_discovery_rule_pack_v1") || packs[0]
|
|
|
- );
|
|
|
- const scorecard = asRecord(contentPack.scorecard);
|
|
|
return (
|
|
|
<>
|
|
|
<ConfigTable
|
|
|
@@ -117,30 +131,40 @@ function RulePacksTab({ data }: { data: Record<string, unknown> }) {
|
|
|
rows={dispatch}
|
|
|
fields={["dispatch_id", "target_entity", "content_format", "runtime_stage", "dispatch_enabled", "rule_pack_id", "rule_pack_version", "notes"]}
|
|
|
/>
|
|
|
- <ConfigTable
|
|
|
- title={`Hard Gates(${String(contentPack.rule_pack_id || "Content 包")})`}
|
|
|
- note="missing_content_portrait 的 KEEP_CONTENT_FOR_REVIEW/review 即 M3 画像止血;动作/严重度全部配置驱动,代码无业务硬编码。"
|
|
|
- rows={asRows(contentPack.hard_gates)}
|
|
|
- fields={["gate_id", "label", "when", "decision_action", "severity", "stop_scoring", "priority"]}
|
|
|
- />
|
|
|
- <ConfigTable
|
|
|
- title="Scorecard 维度"
|
|
|
- rows={asRows(scorecard.dimensions)}
|
|
|
- fields={["key", "label", "max_score", "weight_percent", "runtime_status"]}
|
|
|
- />
|
|
|
- <ConfigTable
|
|
|
- title="Scorecard 评分规则"
|
|
|
- rows={asRows(scorecard.scoring_rules)}
|
|
|
- fields={["scoring_rule_id", "dimension_key", "field_path", "operator", "expected_value", "score_value", "enabled"]}
|
|
|
- />
|
|
|
- <ConfigTable
|
|
|
- title="阈值(Thresholds)"
|
|
|
- rows={asRows(contentPack.thresholds)}
|
|
|
- fields={["min_score", "max_score", "decision_action", "decision_reason_code", "effect_status", "enabled"]}
|
|
|
- />
|
|
|
+ {/* 遍历全部规则包(天然支持多平台);每包独立展示门槛 / 评分 / 阈值。 */}
|
|
|
+ {packs.map((pack, i) => {
|
|
|
+ const p = asRecord(pack);
|
|
|
+ const scorecard = asRecord(p.scorecard);
|
|
|
+ const packId = String(p.rule_pack_id || `包 ${i + 1}`);
|
|
|
+ return (
|
|
|
+ <div key={packId}>
|
|
|
+ <ConfigTable
|
|
|
+ title={`Hard Gates(${packId})`}
|
|
|
+ note="动作 / 严重度全部配置驱动;V3 新增 not_fit_senior / low_confidence / judge_failed(Gemini 判定门槛)。"
|
|
|
+ rows={asRows(p.hard_gates)}
|
|
|
+ fields={["gate_id", "label", "when", "decision_action", "severity", "stop_scoring", "priority"]}
|
|
|
+ />
|
|
|
+ <ConfigTable
|
|
|
+ title="Scorecard 维度"
|
|
|
+ note="V3 实际打分 = relevance(60)+ platform_heat(40);runtime_status=deprecated 的旧 5 维已弃用,仅留档。"
|
|
|
+ rows={asRows(scorecard.dimensions)}
|
|
|
+ fields={["key", "label", "max_score", "weight_percent", "runtime_status"]}
|
|
|
+ />
|
|
|
+ <ConfigTable
|
|
|
+ title="Scorecard 评分规则"
|
|
|
+ rows={asRows(scorecard.scoring_rules)}
|
|
|
+ fields={["scoring_rule_id", "dimension_key", "field_path", "operator", "expected_value", "score_value", "enabled"]}
|
|
|
+ />
|
|
|
+ <ConfigTable
|
|
|
+ title="阈值(Thresholds)"
|
|
|
+ rows={asRows(p.thresholds)}
|
|
|
+ fields={["min_score", "max_score", "decision_action", "decision_reason_code", "effect_status", "enabled"]}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ })}
|
|
|
<ConfigTable
|
|
|
title="效果状态映射(effect_status_mapping)"
|
|
|
- note="含 M3 新增的 hard gate KEEP→pending 映射(map_keep_for_review_pending_hard_gate)。"
|
|
|
rows={asRows(data.effect_status_mapping)}
|
|
|
fields={["mapping_id", "decision_action", "reason_category", "is_hard_gate", "content_effect_status", "query_effect_status", "priority", "enabled", "notes"]}
|
|
|
/>
|
|
|
@@ -153,19 +177,122 @@ function RulePacksTab({ data }: { data: Record<string, unknown> }) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+const GATE_ACTION_ROWS: Array<{ key: string; cn: string }> = [
|
|
|
+ { key: "ADD_TO_CONTENT_POOL", cn: "入池" },
|
|
|
+ { key: "KEEP_CONTENT_FOR_REVIEW", cn: "复看" },
|
|
|
+ { key: "REJECT_CONTENT", cn: "淘汰" }
|
|
|
+];
|
|
|
+
|
|
|
+function WalkPolicyTab({ data }: { data: Record<string, unknown> }) {
|
|
|
+ const global = asRecord(data.global);
|
|
|
+ const perm = asRecord(data.edge_permissions);
|
|
|
+ const budgets = asRows(data.edge_budgets);
|
|
|
+ const tiers = asRecord(data.budget_tiers);
|
|
|
+ const dedup = asRecord(data.dedup);
|
|
|
+
|
|
|
+ // 受控边集合(edge_permissions 三态行里出现的边,排除 _meta)
|
|
|
+ const controlledEdges = new Set<string>();
|
|
|
+ GATE_ACTION_ROWS.forEach(({ key }) => {
|
|
|
+ const row = asRecord(perm[key]);
|
|
|
+ Object.keys(row).forEach((edge) => {
|
|
|
+ if (!edge.startsWith("_") && edge !== "note") controlledEdges.add(edge);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ const edgeCols = [...controlledEdges];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <>
|
|
|
+ <section className="section">
|
|
|
+ <h3 className="section-title">全局护栏(global)</h3>
|
|
|
+ <FactGrid
|
|
|
+ rows={Object.entries(global).map(([k, v]) => {
|
|
|
+ const prov = provenanceOf(v);
|
|
|
+ return [
|
|
|
+ `${fieldAnnotation(k).label}(${k})`,
|
|
|
+ prov ? `${cell(v)} — ${prov}` : cell(v)
|
|
|
+ ];
|
|
|
+ })}
|
|
|
+ />
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <section className="section">
|
|
|
+ <h3 className="section-title">判定门控矩阵(edge_permissions)</h3>
|
|
|
+ <p className="muted">
|
|
|
+ 判定动作 → 各受控出边的放行结果。hashtag_to_query 随 video_to_hashtag 链放行;其余 6 条边为导航 / 系统边,无判定门控。
|
|
|
+ </p>
|
|
|
+ <div className="config-table-wrap">
|
|
|
+ <table className="config-table">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>判定动作</th>
|
|
|
+ {edgeCols.map((edge) => (
|
|
|
+ <th key={edge}>{edge}</th>
|
|
|
+ ))}
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ {GATE_ACTION_ROWS.map(({ key, cn }) => {
|
|
|
+ const row = asRecord(perm[key]);
|
|
|
+ return (
|
|
|
+ <tr key={key}>
|
|
|
+ <td>
|
|
|
+ {cn}
|
|
|
+ <small>{key}</small>
|
|
|
+ </td>
|
|
|
+ {edgeCols.map((edge) => {
|
|
|
+ const v = String(unwrap(row[edge]) || "—");
|
|
|
+ const cls =
|
|
|
+ v === "allow" ? "gate-allow" : v === "allow_low_budget" ? "gate-low" : v === "deny" ? "gate-deny" : "";
|
|
|
+ const vCn =
|
|
|
+ v === "allow" ? "放行" : v === "allow_low_budget" ? "低预算" : v === "deny" ? "拦截" : v;
|
|
|
+ return (
|
|
|
+ <td key={edge} className={cls}>
|
|
|
+ {vCn}
|
|
|
+ </td>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </tr>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <ConfigTable
|
|
|
+ title="边预算(edge_budgets)"
|
|
|
+ note="对齐 v1 实际硬限;provenance 留拍板来源,理想值待 M7 标定。"
|
|
|
+ rows={budgets}
|
|
|
+ fields={["edge_id", "max_total_actions", "max_per_query", "max_works_per_author", "max_per_content", "max_pages", "max_tag_hops", "provenance"]}
|
|
|
+ />
|
|
|
+
|
|
|
+ <section className="section">
|
|
|
+ <h3 className="section-title">预算档位(budget_tiers)与去重(dedup)</h3>
|
|
|
+ <FactGrid
|
|
|
+ rows={[
|
|
|
+ ...Object.entries(tiers).map(([k, v]) => [`tier · ${k}`, cell(v)] as [string, string]),
|
|
|
+ ...Object.entries(dedup).map(([k, v]) => [`dedup · ${k}`, cell(v)] as [string, string])
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ </section>
|
|
|
+ </>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
function WalkStrategyTab({ data }: { data: Record<string, unknown> }) {
|
|
|
return (
|
|
|
<>
|
|
|
<ConfigTable
|
|
|
- title="边→规则包归属(walk_rule_pack_binding)"
|
|
|
- note="M4 增补 path_stop / decision_to_asset 后共 8 条;归属包写在 walk action 顶层 rule_pack_id,实际执行包写在 raw_payload.rule_pack_execution。"
|
|
|
+ title="边→规则包显式绑定(walk_rule_pack_binding)"
|
|
|
+ note="仅 decision_to_asset → 内容包(advisory);其余边的判定门控见「游走护栏」tab 的 edge_permissions。"
|
|
|
rows={asRows(data.walk_rule_pack_binding)}
|
|
|
fields={["binding_id", "edge_id", "target_entity", "rule_pack_id", "dispatch_policy", "required", "notes"]}
|
|
|
/>
|
|
|
<ConfigTable
|
|
|
title="游走边目录(walk_edge_catalog)"
|
|
|
+ note="共 10 条边;edge_type=navigate/reseed/stop/budget;门控与预算见「游走护栏」tab。"
|
|
|
rows={asRows(data.walk_edge_catalog)}
|
|
|
- fields={["edge_id", "edge_type", "priority", "can_loop", "notes"]}
|
|
|
+ fields={["edge_id", "edge_type", "from_node_type", "to_node_type", "priority", "can_loop", "notes"]}
|
|
|
/>
|
|
|
<ConfigTable
|
|
|
title="预算策略(walk_budget_policy)"
|
|
|
@@ -224,14 +351,16 @@ export function ConfigPage() {
|
|
|
setLoading(true);
|
|
|
setError(null);
|
|
|
try {
|
|
|
- const [rulePacks, walkStrategy, queryPrompts] = await Promise.all([
|
|
|
+ const [rulePacks, walkStrategy, walkPolicy, queryPrompts] = await Promise.all([
|
|
|
getConfigRulePacks(),
|
|
|
getConfigWalkStrategy(),
|
|
|
+ getConfigWalkPolicy(),
|
|
|
getConfigQueryPrompts()
|
|
|
]);
|
|
|
setResponses({
|
|
|
"rule-packs": rulePacks,
|
|
|
"walk-strategy": walkStrategy,
|
|
|
+ "walk-policy": walkPolicy,
|
|
|
"query-prompts": queryPrompts
|
|
|
});
|
|
|
} catch (err) {
|
|
|
@@ -275,6 +404,7 @@ export function ConfigPage() {
|
|
|
{error ? <div className="error-state">{error}</div> : null}
|
|
|
{current && tab === "rule-packs" ? <RulePacksTab data={current.data} /> : null}
|
|
|
{current && tab === "walk-strategy" ? <WalkStrategyTab data={current.data} /> : null}
|
|
|
+ {current && tab === "walk-policy" ? <WalkPolicyTab data={current.data} /> : null}
|
|
|
{current && tab === "query-prompts" ? <QueryPromptsTab data={current.data} /> : null}
|
|
|
</section>
|
|
|
</AppShell>
|