diff --git a/SKILL.md b/SKILL.md index 5b424af5..ca53a703 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,7 +15,7 @@ description: "印度占星(Jyotish)商业解盘与推运系统。核心能 3. `references/oracle/commercial_skill_truth_overlay.v1.json`:商业声明和受限技法的最终覆盖层。 4. 服务端 `consumer_context.answer_policy`:当前请求可回答范围的最终合同。 -若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话在口语回答末尾给出 Technique Audit Table(已执行 / 阻塞 / 不适用);正式个人报告仍按 `executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。 +若研究快照、商业覆盖层和服务端回执冲突,以商业覆盖层和服务端回执为准。真实计算只能来自服务端工具,模型不得重算或发明行星位置。候选出生时间不得写成 confirmed;`blocked`、参数敏感、外部验证未闭环和多体系冲突必须原样保留。网页对话的技法审计表由界面折叠展示(已执行 / 阻塞 / 不适用),口语回答不要再把该表写进正文;正式个人报告仍按 `executive_summary -> thematic_narrative -> evidence_appendix` 排列,审计表位于附录。医疗、法律、投资、安全关键结论及确定性死亡/诊断/妊娠预测均禁止。 > **版本**:v6.9.14 | **详细变更**:`CHANGELOG.md` > **对标状态**:中文用户端与技法覆盖领先;D1/D9/AV/Chara 等有守门,Dasha/Shadbala 外部 oracle 扩充仍在进行。 @@ -46,7 +46,7 @@ KP/Muhurta/Gochara/Sahams/Sphuta/Tajika等高阶分支必须按 skill truth over - 领域关联:事业至少 `D10 + A10`,财富至少 `D2 / D11`,婚恋至少 `D9 + UL`。 - 西洋层:有本命出生资料时,自动运行已实现的热带本命及 timing(行运、回归、次限、日弧、converse、中点、月回归、duration scan、parans)。未实现或失败标 `blocked`,不适用标 `not_applicable`。 - 特殊体系:KP、Tajika、Saham、Gulika/Mandi、Panchanga、Muhurta、Prashna、Sphuta、合婚等,按问题关联调用或解释 `blocked / not_applicable`。商业上 KP/Muhurta/Tajika 不得作为确定性结论。 -- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(可见回答译为 已执行 / 阻塞 / 不适用)。未闭环技法仍展示状态与边界,不得写成确定事件。 +- Technique Audit Table 对每项使用 `executed / blocked / not_applicable`(界面折叠展示,译为 已执行 / 阻塞 / 不适用)。口语正文不要再贴该表。未闭环技法仍展示状态与边界,不得写成确定事件。 ### P0/P1 观察层(不得升格为已验证真值) @@ -163,7 +163,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r 6. **阶段五**:应期输出(五层验证→时间窗口→Actionable Output+案例检索) 7. **阶段六**:补救措施(可选) 8. **阶段七**:现代措辞包装 -9. **阶段八**:输出 Technique Audit Table,逐项声明已调用/未调用/部分可用/缺失模块及其对置信度的影响。 +9. **阶段八**:产出 Technique Audit Table,逐项声明已调用/未调用/部分可用/缺失模块及其对置信度的影响。网页由界面折叠展示,不要写入口语正文。 ### ⚙️ 事件判定骨架(总入口) @@ -249,7 +249,7 @@ adapter available 解释为已完成 VedAstro、PyJHora/JHora 或 jyotishganit r 核心要求: 1. 先判断问题类型,再自动选择 `career-timing-strict` / `relationship-timing-strict` / `wealth-timing-strict` / `event-timing-strict` / `event-verification-strict`。 2. 用户不需要知道 Chara Dasha、A10、Argala、Shadbala、Ashtakavarga 等技法名称;AI 必须按问题类型自动调用。 -3. 输出末尾必须给出 Technique Audit Table,说明每项高级技法是否调用、结果是什么、缺失会如何降低置信度。 +3. 必须产出 Technique Audit Table,说明每项高级技法是否调用、结果是什么、缺失会如何降低置信度。网页由界面折叠展示,口语正文不要再贴该表。 4. 不得把未实现或未调用的技法静默省略;A10/Karma Pada、Pushkara、Vargottama、Dasha Sandhi 已进入 full-reading 输出;Bhava Chalit 与 Sudarshana Chakra 已进入 complete,可正常纳入 Technique Audit Table。 ### MEVG 强制外部验证门控(v4.2.0+) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 8f92ba30..632f8b07 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -4458,3 +4458,19 @@ - 相关记录:BUG-120、BUG-292、BUG-294、BUG-296 - 复发自:无 - 修复版本:待提交 + +## BUG-298 | 咨询回答把技法审计表铺进正文,列表和表格也缺少对话样式 + +- 状态:resolved +- 首次发现:2026-08-19 +- 最近更新:2026-08-19 +- 影响面:普通咨询消息渲染、`VISIBLE VOICE`、live `jyotish-vedic-astrology` SKILL.md、Agent 回执 +- 用户现象:个人盘回答后,比较表和技法审计表像未排版的管道符墙;末尾约四十行技法表压过口语正文。 +- 触发条件:有出生分钟的个人盘咨询,模型按旧合同把 `| 技法 | 状态 | 说明 |` 写进 `answer.delta`。 +- 根因:可见语音要求口语末尾贴完整 Markdown 技法审计表;`.markdown-table` 没有样式;列表项里的段落仍吃正文边距。BUG-011 只禁止内部 `EvidenceAuditPanel`,没有另做用户可读的折叠表。 +- 修复:口语不再粘贴审计表。服务端把已交付行放进回执,界面用默认收起的 `本轮技法` 折叠块展示。历史回答若仍带该 Markdown 表,则从正文拆出再折叠。比较表与列表改用对话字号、边距和横向滚动。不恢复内部证据面板、英文状态码或 raw receipt。 +- 验证:`frontend/tests/consultation-technique-audit.test.ts`、`consultation-voice-contract`、`skill-binding`、`evidence-audit-panel`、`chat-bundle-splitting-contract`。 +- 防复发:聊天消息不得再挂载 `EvidenceAuditPanel`;口语合同不得要求把技法审计表写进正文;折叠块默认不得带 `open`。 +- 相关记录:BUG-011、BUG-287 +- 复发自:BUG-011 +- 修复版本:待提交 diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index 95981139..72f3d9e5 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -641,7 +641,7 @@ export async function POST(request: Request) { ? generalDailyContext ? "当前是无出生分钟的公共今日趋势咨询。可依据服务器提供的公共 Panchanga 摘要回答,但不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" : "当前是用户明确选择的无出生分钟一般咨询。不得计算或推断个人星盘;不得补 00:00、时段中点或任何候选分钟。" - : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;回答末尾附上技法审计表,不要复述内部字段。", + : "先加载 Jyotish Skill;如需新的个人星盘结论,必须调用服务器绑定的排盘工具。请用口语直接回答;不要把技法审计表写进正文,不要复述内部字段。", generalDailyContextPrompt(generalDailyContext), resolvedQuestion.modelQuestion, ].filter(Boolean).join("\n"), @@ -691,6 +691,9 @@ export async function POST(request: Request) { stepBudget: consultationStepBudgetReceipt(state), workflow: workflowReceipt, techniqueTruth: generalDailyContext ? "public-panchanga-only" : "not-applicable", + ...(state.techniqueAuditTable?.length + ? { techniqueAuditTable: state.techniqueAuditTable } + : {}), }); return streamAgentResponse({ runId: requestId, @@ -774,6 +777,9 @@ export async function POST(request: Request) { stepBudget: consultationStepBudgetReceipt(state), workflow: state.workflowReceipt ?? workflowReceipt, techniqueTruth: state.techniqueTruth ?? "unknown", + ...(state.techniqueAuditTable?.length + ? { techniqueAuditTable: state.techniqueAuditTable } + : {}), }); return streamAgentResponse({ runId: requestId, diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index a048385c..afe88b96 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -195,8 +195,46 @@ button:disabled { cursor: default; opacity: .45; } .raw-evidence-receipt summary { padding: var(--space-1) 0; color: var(--color-ink-tertiary); font-size: 12px; } .raw-evidence-receipt pre { max-height: 180px; overflow: auto; margin: var(--space-2) 0 0; padding: var(--space-3); border-radius: var(--radius-md); background: var(--color-canvas); color: var(--color-ink-secondary); font-size: 11px; line-height: 1.5; } .message-markdown p { margin: 0 0 14px; } -.message-markdown ul, .message-markdown ol { margin: 10px 0 16px; padding-left: 24px; } -.message-markdown li { margin: 6px 0; } +.message-markdown ul, .message-markdown ol { margin: 8px 0 18px; padding-inline-start: 1.15em; } +.message-markdown li { margin: 0; padding-block: 4px; padding-inline-start: 4px; } +.message-markdown li::marker { color: var(--color-ink-tertiary); } +.message-markdown li > p, +.message-markdown th > p, +.message-markdown td > p { margin: 0; } +.message-markdown li > p + p { margin-top: 6px; } +.message-answer { min-width: 0; } +.markdown-table { + max-width: 100%; + margin: 14px 0 18px; + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +.markdown-table table { + width: 100%; + min-width: 0; + border-collapse: collapse; + font-size: var(--type-body-sm); + line-height: 1.55; +} +.markdown-table th, +.markdown-table td { + padding: 8px 10px 8px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent); + vertical-align: top; + text-align: start; + overflow-wrap: anywhere; +} +.markdown-table thead th { + color: var(--color-ink-secondary); + font-size: var(--type-overline); + font-weight: 650; + letter-spacing: .02em; +} +.markdown-table tbody th { + font-weight: 550; +} +.markdown-table tr:last-child th, +.markdown-table tr:last-child td { border-bottom: 0; } .agent-activity-status { min-height: 24px; display: flex; align-items: center; gap: var(--space-2); color: var(--color-ink-tertiary); font-size: var(--type-body-sm); line-height: 1.5; } .agent-activity-status canvas { flex: 0 0 auto; } .agent-activity-status__text { @@ -223,7 +261,7 @@ button:disabled { cursor: default; opacity: .45; } from { background-position: 120% 0; } to { background-position: -120% 0; } } -.agent-activity-status + .message-markdown { margin-top: var(--space-2); } +.agent-activity-status + .message-answer { margin-top: var(--space-2); } .composer textarea::placeholder { color: var(--color-ink-tertiary); } .composer button svg { width: 19px; height: 19px; } .dialog-close svg { width: 19px; height: 19px; } @@ -720,6 +758,86 @@ button:disabled { cursor: default; opacity: .45; } .message-markdown strong { color: var(--color-ink); font-weight: 500; } .message-markdown a { text-underline-offset: 3px; color: var(--color-action); } .message-markdown code { padding: 2px 5px; font-size: .9em; border-radius: var(--radius-xs); background: var(--color-canvas-muted); font-family: var(--font-mono); } +.technique-audit { + margin-top: var(--space-5); + padding-top: var(--space-3); + border-top: 1px solid color-mix(in srgb, var(--color-border) 72%, transparent); +} +.technique-audit > summary { + width: fit-content; + display: flex; + align-items: center; + gap: var(--space-2); + min-height: 32px; + margin-inline: calc(var(--space-1) * -1); + padding: 0 var(--space-2); + border-radius: var(--radius-sm); + color: var(--color-ink-tertiary); + cursor: pointer; + list-style: none; + font-size: var(--type-caption); + font-weight: 500; +} +.technique-audit > summary::-webkit-details-marker { display: none; } +.technique-audit > summary::before { + width: 6px; + height: 6px; + border-inline-end: 1.5px solid currentColor; + border-bottom: 1.5px solid currentColor; + content: ""; + transform: rotate(-45deg); + transition: transform 120ms ease-out; +} +.technique-audit[open] > summary::before { transform: rotate(45deg) translate(-1px, -1px); } +.technique-audit > summary:hover { background: var(--color-canvas-muted); color: var(--color-ink-secondary); } +.technique-audit > summary:focus-visible { + outline: 2px solid color-mix(in srgb, var(--color-focus) 52%, transparent); + outline-offset: 1px; +} +.technique-audit > summary small { + color: var(--color-ink-tertiary); + font-size: inherit; + font-variant-numeric: tabular-nums; +} +.technique-audit-panel { + max-width: 100%; + margin-top: var(--space-2); + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} +.technique-audit-panel table { + width: 100%; + border-collapse: collapse; + font-size: var(--type-caption); + line-height: 1.5; +} +.technique-audit-panel th, +.technique-audit-panel td { + padding: 7px 10px 7px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border) 64%, transparent); + vertical-align: top; + text-align: start; + overflow-wrap: anywhere; +} +.technique-audit-panel thead th { + color: var(--color-ink-tertiary); + font-size: 11px; + font-weight: 650; + letter-spacing: .04em; +} +.technique-audit-panel tbody th { + width: 28%; + color: var(--color-ink); + font-weight: 550; +} +.technique-audit-panel td[data-status] { + width: 4.5em; + color: var(--color-ink-secondary); + white-space: nowrap; +} +.technique-audit-panel td:last-child { color: var(--color-ink-secondary); } +.technique-audit-panel tr:last-child th, +.technique-audit-panel tr:last-child td { border-bottom: 0; } .error-message { margin: 12px 0 0; padding: 12px 14px; border-left: 3px solid var(--color-danger); background: var(--color-danger-muted); color: var(--color-danger); font-size: 13px; line-height: 1.6; border-color: var(--color-danger); border-radius: 0 var(--radius-md) var(--radius-md) 0; } .composer-wrap { position: sticky; bottom: 0; z-index: 2; min-width: 0; border-top: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent); padding: var(--space-3) var(--space-6) var(--space-4); background: var(--color-frosted); backdrop-filter: saturate(130%) blur(20px); } @@ -1490,6 +1608,15 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class margin-bottom: var(--space-4); } +.conversation:not(.is-empty):not(.is-rectification) .message-markdown ul, +.conversation:not(.is-empty):not(.is-rectification) .message-markdown ol { + margin: 4px 0 var(--space-5); +} + +.conversation:not(.is-empty):not(.is-rectification) .markdown-table { + margin: var(--space-5) 0; +} + .conversation:not(.is-empty):not(.is-rectification) .message-markdown h2, .conversation:not(.is-empty):not(.is-rectification) .message-markdown h3 { margin: var(--space-8) 0 var(--space-3); @@ -2168,4 +2295,5 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class -webkit-text-fill-color: currentColor; } .rectification-activity-receipt__toggle svg { transition: none; } + .technique-audit > summary::before { transition: none; } } diff --git a/frontend/src/components/chat-markdown-view.tsx b/frontend/src/components/chat-markdown-view.tsx index cf1b3f74..de24c227 100644 --- a/frontend/src/components/chat-markdown-view.tsx +++ b/frontend/src/components/chat-markdown-view.tsx @@ -14,6 +14,8 @@ const markdownComponents: Components = {
{paragraph}
- ))} +{paragraph}
+ ))} +{message.text}
}| 技法 | +状态 | +说明 | +
|---|---|---|
| {displayTechniqueAuditLabel(row.technique)} | +{techniqueAuditStatusLabel(row.status)} | +{displayTechniqueAuditNote(row.note)} | +
\{paragraph\}<\/p>/);
- assert.match(contentSource, /plainParagraphs\(text\) \?\? \[\]/);
+ assert.match(contentSource, /plainParagraphs\(spoken\) \?\? \[\]/);
});
test("gsap loads on demand while keeping the reduced-motion gate", () => {
diff --git a/frontend/tests/consultation-technique-audit.test.ts b/frontend/tests/consultation-technique-audit.test.ts
new file mode 100644
index 00000000..8c6a8f07
--- /dev/null
+++ b/frontend/tests/consultation-technique-audit.test.ts
@@ -0,0 +1,112 @@
+import assert from "node:assert/strict";
+import { readFileSync } from "node:fs";
+import test from "node:test";
+
+import { agentExecutionReceiptSchema } from "../src/lib/consultation-agent-events.ts";
+import {
+ displayTechniqueAuditLabel,
+ displayTechniqueAuditNote,
+ normalizeTechniqueAuditRows,
+ resolveTechniqueAuditRows,
+ splitSpokenAnswerAndTechniqueAudit,
+ techniqueAuditStatusLabel,
+} from "../src/lib/consultation-technique-audit.ts";
+
+const disclosureSource = readFileSync(new URL("../src/components/technique-audit-disclosure.tsx", import.meta.url), "utf8");
+const contentSource = readFileSync(new URL("../src/components/chat-message-content.tsx", import.meta.url), "utf8");
+const toolsSource = readFileSync(new URL("../src/mastra/consultation-tools.ts", import.meta.url), "utf8");
+const routeSource = readFileSync(new URL("../src/app/api/consult/route.ts", import.meta.url), "utf8");
+const globalStyles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
+
+const spoken = [
+ "今天适合推进沟通,不适合拍板承诺。",
+ "",
+ "| 适合推进 | 需要避开 |",
+ "|---|---|",
+ "| 汇报、提案、写作 | 冲动承诺、夸大其词 |",
+ "",
+ "给一个现在就能做的行动。",
+].join("\n");
+
+const auditTable = [
+ "| 技法 | 状态 | 说明 |",
+ "|---|---|---|",
+ "| VedAstro 云状态 | 阻塞 | 未经核验的官方云证据,置信度封顶 |",
+ "| 功能吉凶星 | 已执行 | 功能属性与自然属性冲突时降置信 |",
+ "| 问卦盘(Prashna) | 不适用 | 本轮为本命咨询 |",
+].join("\n");
+
+test("a trailing technique audit table is split out of the spoken answer", () => {
+ const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n${auditTable}`);
+ assert.equal(split.complete, true);
+ assert.equal(split.spoken, spoken);
+ assert.equal(split.spoken.includes("适合推进"), true);
+ assert.equal(split.spoken.includes("技法"), false);
+ assert.deepEqual(split.rows, [
+ { technique: "VedAstro 云状态", status: "blocked", note: "未经核验的官方云证据,置信度封顶" },
+ { technique: "功能吉凶星", status: "executed", note: "功能属性与自然属性冲突时降置信" },
+ { technique: "问卦盘(Prashna)", status: "not_applicable", note: "本轮为本命咨询" },
+ ]);
+});
+
+test("an incomplete streaming audit table is hidden rather than shown as pipes", () => {
+ const split = splitSpokenAnswerAndTechniqueAudit(`${spoken}\n\n| 技法 | 状态 | 说明 |\n|---|---|---|\n| VedAstro 云状态 | 阻塞 |`);
+ assert.equal(split.complete, false);
+ assert.equal(split.spoken, spoken);
+ assert.deepEqual(split.rows, []);
+});
+
+test("delivered receipt rows fill the folded table when the spoken answer has no dump", () => {
+ const delivered = normalizeTechniqueAuditRows([
+ { technique: "VedAstro Cloud State", status: "blocked", boundary: "confidence_capped_without_verified_official_cloud" },
+ { technique: "Formal Vargas D1–D60", status: "executed", boundary: "20/20 named traditional charts; remaining D-N are separate" },
+ { technique: "Western transits", status: "used", note: "layer_not_materialized" },
+ ]);
+ assert.deepEqual(delivered, [
+ { technique: "VedAstro Cloud State", status: "blocked", note: "confidence_capped_without_verified_official_cloud" },
+ { technique: "Formal Vargas D1–D60", status: "executed", note: "20/20 named traditional charts; remaining D-N are separate" },
+ { technique: "Western transits", status: "executed", note: "layer_not_materialized" },
+ ]);
+ assert.equal(displayTechniqueAuditLabel("VedAstro Cloud State"), "VedAstro 云状态");
+ assert.equal(displayTechniqueAuditLabel("Western transits"), "西洋行运");
+ assert.equal(displayTechniqueAuditNote("confidence_capped_without_verified_official_cloud"), "未经核验的官方云证据,置信度封顶");
+ assert.equal(displayTechniqueAuditNote("20/20 named traditional charts; remaining D-N are separate"), "20/20 传统命名分盘");
+ assert.equal(displayTechniqueAuditNote("layer_not_materialized"), "本层未物化");
+ assert.equal(displayTechniqueAuditNote("unmapped_machine_key"), "");
+ assert.equal(techniqueAuditStatusLabel("blocked"), "阻塞");
+
+ const split = splitSpokenAnswerAndTechniqueAudit(spoken);
+ assert.deepEqual(resolveTechniqueAuditRows(split, delivered), delivered);
+});
+
+test("the public receipt may carry the audit rows and still rejects internal diagnostics", () => {
+ const receipt = agentExecutionReceiptSchema.parse({
+ runId: "run",
+ runtime: "mastra-agentic",
+ skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 },
+ steps: [{ sequence: 1, kind: "skill", name: "jyotish-vedic-astrology", status: "completed" }],
+ workflow: { route: "timing", status: "ready", preciseTiming: "allowed", missingLayers: [] },
+ techniqueAuditTable: [
+ { technique: "Narayana Dasha", status: "executed", note: "本轮无独立当前读数" },
+ ],
+ });
+ assert.equal(receipt.techniqueAuditTable?.[0]?.technique, "Narayana Dasha");
+ assert.throws(() => agentExecutionReceiptSchema.parse({
+ ...receipt,
+ techniqueAuditTable: [{ technique: "Narayana Dasha", status: "used" }],
+ }));
+});
+
+test("the folded audit control stays closed and never remounts the internal panel", () => {
+ assert.match(disclosureSource, /