|
@@ -0,0 +1,258 @@
|
|
|
|
|
+"use client";
|
|
|
|
|
+
|
|
|
|
|
+import { useCallback, useEffect, useState } from "react";
|
|
|
|
|
+import {
|
|
|
|
|
+ getConfigQueryPrompts,
|
|
|
|
|
+ getConfigRulePacks,
|
|
|
|
|
+ getConfigWalkStrategy
|
|
|
|
|
+} from "@/lib/api/client";
|
|
|
|
|
+import type { ConfigFileResponse } from "@/lib/api/types";
|
|
|
|
|
+import { AppShell } from "@/components/layout/AppShell";
|
|
|
|
|
+import { FactGrid } from "@/components/cards/FactGrid";
|
|
|
|
|
+
|
|
|
|
|
+const TABS = [
|
|
|
|
|
+ { id: "rule-packs", label: "规则包" },
|
|
|
|
|
+ { id: "walk-strategy", label: "游走策略" },
|
|
|
|
|
+ { id: "query-prompts", label: "Query Prompts" }
|
|
|
|
|
+] as const;
|
|
|
|
|
+
|
|
|
|
|
+type TabId = (typeof TABS)[number]["id"];
|
|
|
|
|
+
|
|
|
|
|
+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);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function ConfigTable({
|
|
|
|
|
+ title,
|
|
|
|
|
+ note,
|
|
|
|
|
+ rows,
|
|
|
|
|
+ fields
|
|
|
|
|
+}: {
|
|
|
|
|
+ title: string;
|
|
|
|
|
+ note?: string;
|
|
|
|
|
+ rows: Array<Record<string, unknown>>;
|
|
|
|
|
+ fields: string[];
|
|
|
|
|
+}) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section className="section">
|
|
|
|
|
+ <h3 className="section-title">
|
|
|
|
|
+ {title} <span className="muted">{rows.length} 条</span>
|
|
|
|
|
+ </h3>
|
|
|
|
|
+ {note ? <p className="muted">{note}</p> : null}
|
|
|
|
|
+ {rows.length === 0 ? (
|
|
|
|
|
+ <div className="empty-state">无条目。</div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="config-table-wrap">
|
|
|
|
|
+ <table className="config-table">
|
|
|
|
|
+ <thead>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ {fields.map((field) => (
|
|
|
|
|
+ <th key={field}>{field}</th>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </thead>
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ {rows.map((row, index) => (
|
|
|
|
|
+ <tr key={index}>
|
|
|
|
|
+ {fields.map((field) => (
|
|
|
|
|
+ <td key={field}>{cell(row[field])}</td>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </section>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function asRows(value: unknown): Array<Record<string, unknown>> {
|
|
|
|
|
+ return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : [];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function asRecord(value: unknown): Record<string, unknown> {
|
|
|
|
|
+ return value && typeof value === "object" ? (value as 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
|
|
|
|
|
+ title="Dispatch(按对象选择规则包)"
|
|
|
|
|
+ note="dispatch_enabled=否 即 future 包:已归属未运行(M3 拍板,启用需打开 dispatch)。"
|
|
|
|
|
+ rows={dispatch}
|
|
|
|
|
+ fields={["dispatch_id", "target_entity", "content_format", "runtime_stage", "dispatch_enabled", "rule_pack_id", "rule_pack_version"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <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"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <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"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <ConfigTable
|
|
|
|
|
+ title="决策原因码"
|
|
|
|
|
+ rows={asRows(data.decision_reason_codes)}
|
|
|
|
|
+ fields={["decision_reason_code", "reason_label", "reason_category", "is_hard_gate"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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。"
|
|
|
|
|
+ rows={asRows(data.walk_rule_pack_binding)}
|
|
|
|
|
+ fields={["binding_id", "edge_id", "target_entity", "rule_pack_id", "dispatch_policy", "required"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <ConfigTable
|
|
|
|
|
+ title="游走边目录(walk_edge_catalog)"
|
|
|
|
|
+ rows={asRows(data.walk_edge_catalog)}
|
|
|
|
|
+ fields={["edge_id", "edge_type", "priority", "can_loop", "notes"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <ConfigTable
|
|
|
|
|
+ title="预算策略(walk_budget_policy)"
|
|
|
|
|
+ rows={asRows(data.walk_budget_policy)}
|
|
|
|
|
+ fields={["budget_id", "edge_id", "max_total_actions", "max_per_query", "max_pages", "max_depth", "max_tag_hops"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <ConfigTable
|
|
|
|
|
+ title="停止策略(walk_stop_policy)"
|
|
|
|
|
+ rows={asRows(data.walk_stop_policy)}
|
|
|
|
|
+ fields={["stop_policy_id", "edge_id", "condition_label", "field_path", "expected_value", "stop_action", "priority"]}
|
|
|
|
|
+ />
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function QueryPromptsTab({ data }: { data: Record<string, unknown> }) {
|
|
|
|
|
+ const profiles = asRecord(data.profiles);
|
|
|
|
|
+ return (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {Object.entries(profiles).map(([name, raw]) => {
|
|
|
|
|
+ const profile = asRecord(raw);
|
|
|
|
|
+ const genericFilter = asRecord(profile.generic_filter);
|
|
|
|
|
+ return (
|
|
|
|
|
+ <section className="section" key={name}>
|
|
|
|
|
+ <h3 className="section-title">Profile · {name}</h3>
|
|
|
|
|
+ <FactGrid
|
|
|
|
|
+ rows={[
|
|
|
|
|
+ ["prompt_version", profile.prompt_version],
|
|
|
|
|
+ ["temperature", profile.temperature],
|
|
|
|
|
+ ["max_tokens", profile.max_tokens],
|
|
|
|
|
+ ["variants_per_seed", `${cell(profile.variants_per_seed)}(>1 暂不支持,M2 拍板)`],
|
|
|
|
|
+ ["evidence_fields", `${asRows(profile.evidence_fields).length || (profile.evidence_fields as unknown[] | undefined)?.length || 0} 个字段`],
|
|
|
|
|
+ ["泛词过滤 queries", ((genericFilter.queries as unknown[]) || []).join("、")],
|
|
|
|
|
+ ["泛词过滤 tokens", ((genericFilter.tokens as unknown[]) || []).join("、")]
|
|
|
|
|
+ ]}
|
|
|
|
|
+ />
|
|
|
|
|
+ <details>
|
|
|
|
|
+ <summary className="muted">system / user prompt 原文(逐字复刻,运行真相源)</summary>
|
|
|
|
|
+ <pre className="event-payload">{cell(profile.system)}</pre>
|
|
|
|
|
+ <pre className="event-payload">{cell(profile.user)}</pre>
|
|
|
|
|
+ </details>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+export function ConfigPage() {
|
|
|
|
|
+ const [tab, setTab] = useState<TabId>("rule-packs");
|
|
|
|
|
+ const [responses, setResponses] = useState<Partial<Record<TabId, ConfigFileResponse>>>({});
|
|
|
|
|
+ const [error, setError] = useState<string | null>(null);
|
|
|
|
|
+ const [loading, setLoading] = useState(true);
|
|
|
|
|
+
|
|
|
|
|
+ const load = useCallback(async () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ setError(null);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const [rulePacks, walkStrategy, queryPrompts] = await Promise.all([
|
|
|
|
|
+ getConfigRulePacks(),
|
|
|
|
|
+ getConfigWalkStrategy(),
|
|
|
|
|
+ getConfigQueryPrompts()
|
|
|
|
|
+ ]);
|
|
|
|
|
+ setResponses({
|
|
|
|
|
+ "rule-packs": rulePacks,
|
|
|
|
|
+ "walk-strategy": walkStrategy,
|
|
|
|
|
+ "query-prompts": queryPrompts
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setError(err instanceof Error ? err.message : "配置加载失败");
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ void load();
|
|
|
|
|
+ }, [load]);
|
|
|
|
|
+
|
|
|
|
|
+ const current = responses[tab];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <AppShell onRefresh={load} showBack>
|
|
|
|
|
+ <section className="detail-panel">
|
|
|
|
|
+ <header className="detail-header">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h1 className="detail-title">配置即真相(只读)</h1>
|
|
|
|
|
+ <p className="muted">
|
|
|
|
|
+ 运行时读取的 JSON 配置原文。只读展示;编辑走 Excel→JSON 工具链(可视化编辑留 Web V3)。
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {current ? <span className="muted">{current.source_file}</span> : null}
|
|
|
|
|
+ </header>
|
|
|
|
|
+ <div className="filter-bar">
|
|
|
|
|
+ {TABS.map((option) => (
|
|
|
|
|
+ <button
|
|
|
|
|
+ key={option.id}
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ className={`chip${tab === option.id ? " active" : ""}`}
|
|
|
|
|
+ onClick={() => setTab(option.id)}
|
|
|
|
|
+ >
|
|
|
|
|
+ {option.label}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {loading ? <div className="loading-state">配置加载中…</div> : null}
|
|
|
|
|
+ {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 === "query-prompts" ? <QueryPromptsTab data={current.data} /> : null}
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </AppShell>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|