Просмотр исходного кода

refactor(web-v3): 抽配置表/prompt 公共件(合一批1,零行为变化)

- ConfigPage 私有件→configTables.tsx(ConfigTable/4 Tab/cell/unwrap/asRows 等),ConfigPage 改 import
- RunDashboardPage 的 llmVariant prompt 函数→queryPrompt.ts
- WalkJourney RulePackBody 改 export
- 为激进合一(策略配置面板 + 旅程内嵌 prompt)备料

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sam Lee 1 день назад
Родитель
Сommit
b895f37b8e

+ 1 - 322
web/features/config/ConfigPage.tsx

@@ -9,8 +9,7 @@ import {
 } from "@/lib/api/client";
 import type { ConfigFileResponse } from "@/lib/api/types";
 import { AppShell } from "@/components/layout/AppShell";
-import { FactGrid } from "@/components/cards/FactGrid";
-import { fieldAnnotation } from "@/lib/config/fieldLabels";
+import { RulePacksTab, WalkStrategyTab, WalkPolicyTab, QueryPromptsTab } from "@/features/config/configTables";
 
 const TABS = [
   { id: "rule-packs", label: "规则包" },
@@ -21,326 +20,6 @@ const TABS = [
 
 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 {
-  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({
-  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) => {
-                  const annotation = fieldAnnotation(field);
-                  return (
-                    <th key={field} title={annotation.desc}>
-                      {annotation.label}
-                      {annotation.label !== field ? <small>{field}</small> : null}
-                    </th>
-                  );
-                })}
-              </tr>
-            </thead>
-            <tbody>
-              {rows.map((row, index) => (
-                <tr key={index}>
-                  {fields.map((field) => (
-                    <td key={field}>{cell(row[field])}</td>
-                  ))}
-                </tr>
-              ))}
-            </tbody>
-          </table>
-          <details className="field-annotations">
-            <summary className="muted">字段业务含义说明</summary>
-            <dl>
-              {fields.map((field) => {
-                const annotation = fieldAnnotation(field);
-                return (
-                  <div key={field}>
-                    <dt>
-                      {annotation.label}
-                      <small>{field}</small>
-                    </dt>
-                    <dd>{annotation.desc || "—"}</dd>
-                  </div>
-                );
-              })}
-            </dl>
-          </details>
-        </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);
-  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", "notes"]}
-      />
-      {/* 遍历全部规则包(天然支持多平台);每包独立展示门槛 / 评分 / 阈值。 */}
-      {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)"
-        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"]}
-      />
-      <ConfigTable
-        title="决策原因码"
-        rows={asRows(data.decision_reason_codes)}
-        fields={["decision_reason_code", "reason_label", "reason_category", "is_hard_gate", "business_explanation"]}
-      />
-    </>
-  );
-}
-
-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="仅 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", "from_node_type", "to_node_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={[
-                [`${fieldAnnotation("prompt_version").label}(prompt_version)`, profile.prompt_version],
-                [`${fieldAnnotation("temperature").label}(temperature)`, `${cell(profile.temperature)} — ${fieldAnnotation("temperature").desc}`],
-                [`${fieldAnnotation("max_tokens").label}(max_tokens)`, profile.max_tokens],
-                [`${fieldAnnotation("variants_per_seed").label}(variants_per_seed)`, `${cell(profile.variants_per_seed)} — ${fieldAnnotation("variants_per_seed").desc}`],
-                [`${fieldAnnotation("evidence_fields").label}(evidence_fields)`, `${(profile.evidence_fields as unknown[] | undefined)?.length || 0} 个字段 — ${fieldAnnotation("evidence_fields").desc}`],
-                [`${fieldAnnotation("generic_filter").label} queries`, ((genericFilter.queries as unknown[]) || []).join("、")],
-                [`${fieldAnnotation("generic_filter").label} 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>>>({});

+ 310 - 0
web/features/config/configTables.tsx

@@ -0,0 +1,310 @@
+"use client";
+
+// 配置渲染公共件:从 ConfigPage 抽出,供 /config 页与 run 工作台「策略配置」面板共用。
+import { FactGrid } from "@/components/cards/FactGrid";
+import { fieldAnnotation } from "@/lib/config/fieldLabels";
+
+// walk_policy 的拍板值可能裸值,也可能带 {value, provenance, tbd} 留痕包裹。
+export 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;
+}
+
+export 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 "";
+}
+
+export function cell(value: unknown): string {
+  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);
+}
+
+export function asRows(value: unknown): Array<Record<string, unknown>> {
+  return Array.isArray(value) ? (value as Array<Record<string, unknown>>) : [];
+}
+
+export function asRecord(value: unknown): Record<string, unknown> {
+  return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
+}
+
+export 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) => {
+                  const annotation = fieldAnnotation(field);
+                  return (
+                    <th key={field} title={annotation.desc}>
+                      {annotation.label}
+                      {annotation.label !== field ? <small>{field}</small> : null}
+                    </th>
+                  );
+                })}
+              </tr>
+            </thead>
+            <tbody>
+              {rows.map((row, index) => (
+                <tr key={index}>
+                  {fields.map((field) => (
+                    <td key={field}>{cell(row[field])}</td>
+                  ))}
+                </tr>
+              ))}
+            </tbody>
+          </table>
+          <details className="field-annotations">
+            <summary className="muted">字段业务含义说明</summary>
+            <dl>
+              {fields.map((field) => {
+                const annotation = fieldAnnotation(field);
+                return (
+                  <div key={field}>
+                    <dt>
+                      {annotation.label}
+                      <small>{field}</small>
+                    </dt>
+                    <dd>{annotation.desc || "—"}</dd>
+                  </div>
+                );
+              })}
+            </dl>
+          </details>
+        </div>
+      )}
+    </section>
+  );
+}
+
+export function RulePacksTab({ data }: { data: Record<string, unknown> }) {
+  const dispatch = asRows(data.rule_pack_dispatch);
+  const packs = asRows(data.rule_packs);
+  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", "notes"]}
+      />
+      {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)"
+        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"]}
+      />
+      <ConfigTable
+        title="决策原因码"
+        rows={asRows(data.decision_reason_codes)}
+        fields={["decision_reason_code", "reason_label", "reason_category", "is_hard_gate", "business_explanation"]}
+      />
+    </>
+  );
+}
+
+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: "淘汰" }
+];
+
+export 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);
+
+  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>
+    </>
+  );
+}
+
+export function WalkStrategyTab({ data }: { data: Record<string, unknown> }) {
+  return (
+    <>
+      <ConfigTable
+        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", "from_node_type", "to_node_type", "priority", "can_loop", "notes"]}
+      />
+    </>
+  );
+}
+
+export 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={[
+                [`${fieldAnnotation("prompt_version").label}(prompt_version)`, profile.prompt_version],
+                [`${fieldAnnotation("temperature").label}(temperature)`, `${cell(profile.temperature)} — ${fieldAnnotation("temperature").desc}`],
+                [`${fieldAnnotation("max_tokens").label}(max_tokens)`, profile.max_tokens],
+                [`${fieldAnnotation("variants_per_seed").label}(variants_per_seed)`, `${cell(profile.variants_per_seed)} — ${fieldAnnotation("variants_per_seed").desc}`],
+                [`${fieldAnnotation("evidence_fields").label}(evidence_fields)`, `${(profile.evidence_fields as unknown[] | undefined)?.length || 0} 个字段 — ${fieldAnnotation("evidence_fields").desc}`],
+                [`${fieldAnnotation("generic_filter").label} queries`, ((genericFilter.queries as unknown[]) || []).join("、")],
+                [`${fieldAnnotation("generic_filter").label} 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>
+        );
+      })}
+    </>
+  );
+}

+ 1 - 58
web/features/runs/RunDashboardPage.tsx

@@ -37,6 +37,7 @@ import type {
 } from "@/lib/api/types";
 import {compactValue, statusLabel } from "@/lib/status/status";
 import { contentUrl, embedPlayerUrl, platformLabel } from "@/lib/platform/content";
+import { isLlmVariantQuery, llmVariantPromptPayload } from "@/features/runs/queryPrompt";
 
 type DashboardData = {
   dashboard: DashboardResponse;
@@ -312,64 +313,6 @@ function queryGenerationExplanation(query: Record<string, unknown>): Record<stri
   };
 }
 
-function isLlmVariantQuery(query: Record<string, unknown>): boolean {
-  return query.search_query_generation_method === "llm_variant";
-}
-
-function seedTermForQuery(query: Record<string, unknown>): unknown {
-  const seedRef = query.pattern_seed_ref && typeof query.pattern_seed_ref === "object"
-    ? query.pattern_seed_ref as Record<string, unknown>
-    : {};
-  const llmInput = query.llm_input_evidence && typeof query.llm_input_evidence === "object"
-    ? query.llm_input_evidence as Record<string, unknown>
-    : {};
-  return llmInput.seed_term || seedRef.seed_term || query.search_query;
-}
-
-function llmVariantPromptPayload(query: Record<string, unknown>): Record<string, unknown> {
-  const llmInput = query.llm_input_evidence && typeof query.llm_input_evidence === "object"
-    ? query.llm_input_evidence as Record<string, unknown>
-    : {};
-  const seedTerm = seedTermForQuery(query);
-  return {
-    "这个提示词在做什么": "拿一个 DemandAgent 种子词,结合 Pattern 证据,让 LLM 生成一条相邻但不重复的抖音搜索 query。",
-    "种子词": seedTerm,
-    "生成结果": query.search_query,
-    "父 query": query.llm_variant_of || "无",
-    "提示词版本": query.llm_prompt_version || "query_variant.v1",
-    "LLM 模型": query.llm_generation_model || "缺失",
-    messages: [
-      {
-        role: "system",
-        content: (
-          "You generate one concise Chinese short-video search query. "
-          + "Return exactly one plain query string. Do not return JSON, "
-          + "lists, quotes, explanations, or multiple lines."
-        )
-      },
-      {
-        role: "user",
-        content: [
-          "Seed term:",
-          compactValue(seedTerm),
-          "",
-          "Evidence context:",
-          JSON.stringify(llmInput, null, 2),
-          "",
-          "Create one adjacent search phrase that stays faithful to the evidence. Avoid any phrase listed in existing_search_queries."
-        ].join("\n")
-      }
-    ],
-    "输入证据摘要": {
-      seed_terms: llmInput.seed_terms,
-      itemset_ids: llmInput.itemset_ids,
-      category_bindings: llmInput.category_bindings,
-      element_bindings: llmInput.element_bindings,
-      existing_search_queries: llmInput.existing_search_queries
-    }
-  };
-}
-
 function primaryQuerySource(item: Record<string, unknown>): Record<string, unknown> {
   const sources = Array.isArray(item.query_sources) ? item.query_sources : [];
   const first = sources[0];

+ 1 - 1
web/features/runs/WalkJourney.tsx

@@ -384,7 +384,7 @@ export function WalkJourney({
 
 const AI_GATES = new Set(["not_fit_senior", "low_confidence", "judge_failed"]);
 
-function RulePackBody({
+export function RulePackBody({
   pack,
   showFull,
   onToggleFull

+ 62 - 0
web/features/runs/queryPrompt.ts

@@ -0,0 +1,62 @@
+// AI 扩写(LLM variant)搜索词的提示词解释:从 RunDashboardPage 抽出,供发现旅程视频详情复用。
+import { compactValue } from "@/lib/status/status";
+
+export function isLlmVariantQuery(query: Record<string, unknown>): boolean {
+  return query.search_query_generation_method === "llm_variant";
+}
+
+export function seedTermForQuery(query: Record<string, unknown>): unknown {
+  const seedRef =
+    query.pattern_seed_ref && typeof query.pattern_seed_ref === "object"
+      ? (query.pattern_seed_ref as Record<string, unknown>)
+      : {};
+  const llmInput =
+    query.llm_input_evidence && typeof query.llm_input_evidence === "object"
+      ? (query.llm_input_evidence as Record<string, unknown>)
+      : {};
+  return llmInput.seed_term || seedRef.seed_term || query.search_query;
+}
+
+export function llmVariantPromptPayload(query: Record<string, unknown>): Record<string, unknown> {
+  const llmInput =
+    query.llm_input_evidence && typeof query.llm_input_evidence === "object"
+      ? (query.llm_input_evidence as Record<string, unknown>)
+      : {};
+  const seedTerm = seedTermForQuery(query);
+  return {
+    "这个提示词在做什么": "拿一个 DemandAgent 种子词,结合 Pattern 证据,让 LLM 生成一条相邻但不重复的抖音搜索 query。",
+    "种子词": seedTerm,
+    "生成结果": query.search_query,
+    "父 query": query.llm_variant_of || "无",
+    "提示词版本": query.llm_prompt_version || "query_variant.v1",
+    "LLM 模型": query.llm_generation_model || "缺失",
+    messages: [
+      {
+        role: "system",
+        content:
+          "You generate one concise Chinese short-video search query. " +
+          "Return exactly one plain query string. Do not return JSON, " +
+          "lists, quotes, explanations, or multiple lines."
+      },
+      {
+        role: "user",
+        content: [
+          "Seed term:",
+          compactValue(seedTerm),
+          "",
+          "Evidence context:",
+          JSON.stringify(llmInput, null, 2),
+          "",
+          "Create one adjacent search phrase that stays faithful to the evidence. Avoid any phrase listed in existing_search_queries."
+        ].join("\n")
+      }
+    ],
+    "输入证据摘要": {
+      seed_terms: llmInput.seed_terms,
+      itemset_ids: llmInput.itemset_ids,
+      category_bindings: llmInput.category_bindings,
+      element_bindings: llmInput.element_bindings,
+      existing_search_queries: llmInput.existing_search_queries
+    }
+  };
+}