From 5a589a90581eb1152536cd9917795ce92ad742e1 Mon Sep 17 00:00:00 2001 From: jesse-ux Date: Tue, 6 Oct 2026 13:24:45 +0800 Subject: [PATCH] fix(consult): write the first turn as chat, not a sectioned report Ordinary consult first turns drop Markdown headings and the required action list. People are named in the paragraph, and pinned examples no longer contain copyable advice. BUG-1244, BUG-1245. --- CHANGELOG.md | 7 + docs/BUG_HISTORY.md | 30 ++++ ...-consult-conversational-answer-20261006.md | 74 ++++++++++ docs/tasks/README.md | 2 +- .../consult-conversational-answer-20261006.md | 14 ++ frontend/DESIGN.md | 6 +- frontend/docs/VOICE.md | 22 +-- frontend/src/app/api/consult/route.ts | 2 +- .../src/lib/consultation-thinking-plan.ts | 85 ++++++----- frontend/src/mastra/index.ts | 4 +- frontend/src/mastra/product-voice.ts | 54 +++---- frontend/src/mastra/skill-binding.ts | 4 +- ...nsult-answer-the-question-20260927.test.ts | 11 +- ...onsult-single-pass-answer-20260927.test.ts | 10 +- .../tests/consultation-run-timeline.test.ts | 35 +++++ frontend/tests/consultation-smalltalk.test.ts | 8 +- .../tests/consultation-thinking-plan.test.ts | 97 +++++++++---- .../tests/consultation-voice-contract.test.ts | 137 +++++++++++++----- 18 files changed, 434 insertions(+), 168 deletions(-) create mode 100644 docs/tasks/PROGRESS-consult-conversational-answer-20261006.md create mode 100644 docs/testing/consult-conversational-answer-20261006.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ba07a0f..2dec4af4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 印度占星 Skill 更新日志 +## 2026-10-06 — 普通对话首轮改成几段聊天,不再交汇报骨架(未上线) + +- 问事业、财运、父母这类首轮,以前会按固定小标题往下写,最后再交一份「这周可以做的一件事」。现在首轮是几段人话:先给结论,再写最关键的依据,相关时带上时间。问到几个人就分段说,段首点名,不写小标题(BUG-1244)。 +- 行动不再每轮都要。盘上确实指向一件相关的具体事情时,顺口说一句;用户问该怎么办时,那一轮可以多说几条。提示词里的示范删掉了会被逐字照抄的生活建议(BUG-1245)。 +- 同一件事仍只说一遍。追问仍是直接回答,讲清楚为准。思考栏不再出现「这周可以做什么」;没有小标题的回答,写作行是「组织回答」。 +- Skill 版本不变;不改数据库;不改模型、答题时钟和计费。 + ## 2026-10-05 — 普通对话不再白等校正闸的外部快照,用量页能看分段时间(未上线) - 问父母、问年运这类普通对话,每个领域以前会在校正闸里再等一次外部星历快照,大约多 4 秒,失败了同一天还会再等。现在咨询路径复用当天已经拿到的官方结果;没有的话不再干等,记成「咨询里先不做」,同一天不重复等。生时校正自己的核对不变。顶层那一次外部核对仍会做(BUG-1231)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b07a3453..dc610bb7 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16673,3 +16673,33 @@ - 相关记录:BUG-560、BUG-1090、BUG-1091、BUG-1105、BUG-1141。 - 复发自:无 - 修复版本:研究分支 `codex/rectification-nadi-seconds-research-20261005`,2026-10-05 Claude 验收后合入 staging(只有研究脚本与文档,没有运行时改动) + +## BUG-1244 | 普通对话首轮按固定小标题汇报,读起来不像聊天 + +- 状态:fixed-pending-verify(分支 `codex/consult-conversational-answer-20261006`;部署和真机清单完成前不标 resolved) +- 首次发现 / 最近更新:2026-10-06 / 2026-10-06 +- 来源:产品看普通对话首轮,反馈像机器在汇报。任务书 `docs/tasks/TASK-consult-conversational-answer-20261006.md`。 +- 影响面:普通咨询本命 / 无出生分钟 / 申报时段的首轮写作指令、续写提示、思考计划里的「这周可以做什么」、没有标题时的写作行。不改证据卡、工具、模型、输出长度上限、答题时钟、计费。每日星语和「深入看今日」的骨架不动。生时校正的口气不动。 +- 现象:首轮按「开场 → 按对象的小标题 → 依据节 → 时间节 → 行动节」往下写。行动清单放在固定的最后一节。问到几个人时,段首是二级标题。申报时段还会要求先写「先回答你的问题」这个标题。 +- 根因:2026-10-01 的形状(BUG-1132)把「按对象分段」做成了二级标题,并保留三个固定节。2026-09-27 的「行动只在最后一节出现一次」(BUG-1073 的标题部分)仍要求交那一节。模型按标题填空,读起来是汇报。 +- 修复:形状只在 `consultation-thinking-plan.ts` 定义一处。首轮不写二级标题;问到几个人就分段落,段首点名。行动不强制,盘上有具体指向才顺口一句,用户问怎么办时那一轮可以给几条。首轮通常三到六段,不设硬截断。追问仍讲清楚为准;换领域或要求完整看时回到同样的段落形状。思考计划去掉行动节。正文没有二级标题时,思考节在流式中不保持进行中,结算后一起完成。写作行走「组织回答」。历史消息里已经写出的标题仍能拆开。`REPORT_HEADING` 只留在拆旧消息和思考节的 heading 字段里,不再拼进提示词。 +- 验证:合同测试见进度记录。本机没有模型凭据,四道离线题没有真跑。真机八步见 `docs/testing/consult-conversational-answer-20261006.md`。staging 尚未部署这次改动。 +- 防复发:`consultation-thinking-plan.test.ts`、`consultation-voice-contract.test.ts`、`consultation-run-timeline.test.ts` 锁住「不写 ## 节名」「段首点名」「行动不强制」「三到六段」,以及无标题首轮的思考节与写作行。改断言写了原值 / 新值 / 原因。 +- 相关记录:BUG-1073(同一件事只说一遍,保留)、BUG-1132(10-01 的按对象标题与三个固定节,本记录推翻其标题部分)。 +- 复发自:无。这是产品改口径,不是 1073 / 1132 的同一缺陷复发。 +- 修复版本:待发布 + +## BUG-1245 | 提示词里的行动示范被回答逐字照抄 + +- 状态:fixed-pending-verify(分支 `codex/consult-conversational-answer-20261006`;部署和真机清单完成前不标 resolved) +- 首次发现 / 最近更新:2026-10-06 / 2026-10-06 +- 来源:同上任务书。产品指出示范里的具体做法会出现在真实回答里。 +- 影响面:`product-voice.ts` 的 Good 示范。不改计算,不改证据卡。 +- 现象:不同人的回答里出现同一套可以照抄的生活建议,例如书面确认、这周去看小毛病、给妈妈打电话先问近况,以及「这周」行动节的标题。 +- 根因:Good 示范把具体行动句和固定标题写进了提示词。模型把示范当答案抄。 +- 修复:父母题和事业题的 Good 示范改成段落,段首点名,括号里只留依据占位。删掉全部具体行动句。另加一条只示意结构、不给可抄句子的汇报体 Bad。提示词里不再出现那三句旧行动,也不再出现「## 这周」。 +- 验证:`consultation-voice-contract.test.ts` 的「pinned examples carry no copyable action and no weekly heading (BUG-1245)」。真机第 3 步换人再问,看建议是否还是同一模子。本机没有模型凭据,没有对照跑新旧提示词。 +- 防复发:上述测试锁住三句旧行动和「## 这周」不得出现在提示词里。 +- 相关记录:BUG-1165、BUG-1168、BUG-1182(示范被照抄的前几轮)、BUG-1244。 +- 复发自:BUG-1165 的「示范被当成规则照抄」。旧测试锁的是读法和职业形态,没有锁行动句,所以这次换了一类句子仍能抄出去。 +- 修复版本:待发布 diff --git a/docs/tasks/PROGRESS-consult-conversational-answer-20261006.md b/docs/tasks/PROGRESS-consult-conversational-answer-20261006.md new file mode 100644 index 00000000..348d7c11 --- /dev/null +++ b/docs/tasks/PROGRESS-consult-conversational-answer-20261006.md @@ -0,0 +1,74 @@ +# PROGRESS:普通对话首轮改成聊天 — 2026-10-06 + +基线:`origin/staging` `86cafe68`(任务书,只有文档)。形状文件上一次产品提交仍是 `7806c72f` / `6a02e5e9`,没有更新的提交,没有变基。分支 `codex/consult-conversational-answer-20261006`,工作树 `.worktrees/consult-conversational-answer-20261006`。 + +未推 staging,未部署。BUG-1244、BUG-1245 状态是 `fixed-pending-verify`。Skill 版本不变。 + +## 做了什么 + +1. 首轮形状只留在 `frontend/src/lib/consultation-thinking-plan.ts`。`product-voice.ts`、`skill-binding.ts`、`index.ts`、`route.ts` 引用它,不再各写一套标题。首轮不写二级标题;问到几个人就分段落、段首点名;行动不强制;通常三到六段,不设硬截断。追问仍讲清楚为准。换领域或要求完整看时,回到同样的段落形状(BUG-1244)。 +2. Good 示范改成段落。删掉三句会被照抄的行动(书面确认、这周去看小毛病、给妈妈打电话先问近况),也删掉「## 这周」。另加一条只示意结构的汇报体 Bad(BUG-1245)。 +3. 思考计划去掉「这周可以做什么」。正文没有二级标题时,流式中没有任何一节停在进行中,结算后一起完成。写作行是「组织回答」/「正在组织回答…」。历史消息里的旧标题仍能拆开。 +4. 文案:`frontend/docs/VOICE.md` 第七条、首轮只说一遍、对照表;`frontend/DESIGN.md` 写作行。真机清单 `docs/testing/consult-conversational-answer-20261006.md`。 + +## 提示词里还留下的旧标题(允许的兼容命中) + +`git grep -n "这周可以做的一件事" frontend/src` 只剩: + +- `frontend/src/lib/consultation-thinking-plan.ts:20`:`REPORT_HEADING.action`。给旧回答拆段、缩短标题用,不拼进新提示词。 + +`git grep -n "骨架不可省略" frontend/src`:无。 + +`git grep -n "REPORT_HEADING" frontend/src` 都在 `consultation-thinking-plan.ts`,没有出现在模板字符串里: + +| 行 | 用途 | +| --- | --- | +| 17–21 | 常量。注释写明只给 2026-10-06 之前的回答用 | +| 95–97 | `consultationReportHeadings()` 返回这三个字符串,给旧回答对标题 | +| 140、152 | 思考节 `heading` 字段(schema 必填),值是「盘上依据」。无标题的新回答不靠它推进进度 | +| 160 | 时间节的 `heading` 字段,值是「时间怎么看」 | + +`frontend/src` 的 `*.ts` / `*.tsx` 里没有 `## 盘上依据`、`## 时间怎么看`、`## 这周可以做的一件事`。仓库根 `SKILL.md` 里也没有这三句。历史文档、旧任务书、旧真机记录里的标题没有改。 + +## 断言三栏(本单改过的) + +| 位置 | 原值 | 新值 | 原因 | +| --- | --- | --- | --- | +| 思考计划节 | 四节,含「这周可以做什么」 | 三节:question / support / timing | T3,BUG-1244 | +| 无标题时的进度 | 口语开场还没有标题时首节 active | 整篇没有二级标题时没有 active;结算后全部 done | 首轮不再写标题,避免一直转圈 | +| 时间节出现后的状态 | done, done, done, active | done, done, done | 行动节已删,时间节是最后一节 | +| 续写 | 要求补三个固定二级标题 | 认出最后一个已有标题,接着用段落写,不再点名三个节名 | D1 | +| 本命列表 | 并列点必须是 Markdown 列表 | 默认段落;用户要求列举,或三个以上时间窗,才用列表。每日星语仍用列表 | D5 | +| 首轮指令 | 要求 `## 盘上依据` 等三个标题 | 不要求任何 `##` 节名;含段首点名、行动不强制、三到六段 | D1–D3 | +| 口吻合同 | 「骨架不可省略」、不设字数上限 | 通常三到六段、不设硬截断;合同里没有「骨架不可省略」 | D1/D3 | +| 技能骨架句 | Level 2 模板就是回答形状;行动出现一次 | Level 2 只是依据来源;六步宫位和 Yoga 表不贴在标题下 | D1/D2/D6 | +| 长度句 | There is no length cap | usually three to six paragraphs;There is no hard cutoff | D3,只约束首轮 | +| 行动句 | 一到三条破折号,只在最后一节 | 行动不强制 | D2 | +| 父母 / 事业示范 | 带 `##` 和行动清单 | 段落、段首点名、括号只放依据占位 | D1/D4,BUG-1245 | +| 寒暄合同 | opener-plus-skeleton | paragraph shape | D1。测试名保留 | +| 写作提示(两份路由测试) | 提示里含三个固定 `##` 标题 | 含段首点名、行动不强制、三到六段,且不含 `##` + 旧节名 | D1–D3。夹具正文仍带旧标题,只测续写和放出 | + +测试名没有删。新增两条:无标题首轮的思考节与写作行(BUG-1244);示范不含可抄行动和「## 这周」(BUG-1245)。 + +## 验证 + +| 项 | 结果 | +| --- | --- | +| `tsc --noEmit` | 0 错 | +| `npm run lint` | 0 error,126 warning。本单改过的文件里,只有 `consultation-thinking-plan.ts` 的 `_domains` 未使用,这是原来的参数。没有为清 warning 改业务代码 | +| 定向测试 | `consultation-thinking-plan`、`consultation-voice-contract`、`consultation-smalltalk`、`consultation-run-timeline`、`consult-career-yoga-functional-20261002`、`consult-no-presupposition-20261001`:79 项,79 通过,0 失败,0 cancelled | +| `npm test` | 4851 项,4706 通过,145 失败,0 cancelled,0 skipped。退出码 1 | +| 隐私标记 | `tests/test_repo_privacy_markers.py` 通过。进度和 Bug 历史没有写入具体出生资料 | +| `next build --webpack` | 编译成功(2.9 分钟),TypeScript 检查通过。收集页面数据时在 `/api/birth-time-guide` 失败:现场技能符号链接 EPERM。没有打出路由表,因此没有 `/` 是否 Static、也没有首屏 gzip。这是本机工作树的已知环境缺口,不是这轮文案改动的编译错误 | + +145 条失败里,和本单相关的文件是整文件加载失败,不是新断言没对上:`consult-answer-the-question-20260927.test.ts`、`consult-single-pass-answer-20260927.test.ts` 在导入 `skill-binding` 时抛 `SkillPackageRegistryError`,Windows 不允许为现场技能创建符号链接(EPERM)。同一原因还打红了 `consultation-workflow-request`、`consult-condensed-checklist`、`consult-evidence-lookup`、`consult-gender-card`、`skill-binding.test.ts` 等。本机工作树里的 `skills/jyotish-vedic-astrology/SKILL.md` 是 14 字节的链接文本(`../../SKILL.md`),不是真正的符号链接,所以「方法来自现场技能树」那一组也会空。数据库、备份、部署脚本失败是这台机器上原有的 Docker / 环境缺口。本轮没有在干净的 `86cafe68` 上再跑一遍全量做逐条对照。 + +写作提示那两条断言没有在测试进程里跑到。静态核对:`frontend/src` 和仓库根 `SKILL.md` 都不含 `## 盘上依据`、`## 时间怎么看`、`## 这周可以做的一件事`。 + +## T5 长度与离线 + +没有模型凭据,四道题(父母、事业、财运、伴侣)没有真跑,不把估计写成实测。新形状要求首轮通常三到六段,不设硬截断。相对旧的约 1,500 字、同一判断说三遍的父母样本,新指令去掉了固定节和行动清单。真机八步在 `docs/testing/consult-conversational-answer-20261006.md`。浏览器登录验收没有做。 + +## 构建 + +`npx next build --webpack`(Next.js 16.3.1):webpack 编译成功,接着的 TypeScript 检查也通过。收集页面数据时失败,原因与测试里的技能链接相同:`SkillPackageRegistryError`,无法为 `skills/jyotish-vedic-astrology/SKILL.md` 创建符号链接(EPERM)。路由表没有打出来。`/` 是否仍为 Static、首屏 gzip 相对基线的变化,本机没有数。没有改用 Turbopack:工作树的 `node_modules` 是联接,Turbopack 会直接拒绝。 diff --git a/docs/tasks/README.md b/docs/tasks/README.md index a907b83b..36f708db 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -424,4 +424,4 @@ | `TASK-consult-latency-quickwins-20261005.md` | `PROGRESS-consult-latency-quickwins-20261005.md` | 普通对话耗时两项快修:咨询链校正闸不再同步白等 VedAstro 官方快照子进程(每域约 4 s,复用顶层缓存 + 负缓存,不推翻 BUG-301);补分段计时(第 0/1 步耗时、推理 token、分类耗时进日志与 usage)。推理强度/精简说明待模型 key 另单(BUG-1231、BUG-1232) | 已验收(Claude 10-05 Linux:Python 62 / 前端 24 失败与基线逐名相同,Static、gzip 0%;装 SDK 实测每领域 4.47→1.66 s,剩余 1.33 s 是保留的顶层前台等待;/admin/usage 真机欠) | 分支 `codex/consult-latency-quickwins-20261005` | | `TASK-rectification-nadi-seconds-research-20261005.md` | `PROGRESS-rectification-nadi-seconds-research-20261005.md` | **「纳迪秒级校准」可证伪检验(离线)**:竞品宣传「问前事到天 → 秒级」。本仓主链只用三层小运,Sookshma / Prana 与 D150 从未进评价集;v5 真值 52/77 是整 5 分钟(秒级无真值可对)。N0 五层小运 + D150 底座(前三层与主链对账 0 差)、N1 拟合率 vs 安慰剂日期(核心)、N2 留一件预测、N3 六题后区间内再细分能否提头名、N4 岁差 / 坐标 / 时间扰动的噪声地板、N5 D150 结构层(原文比对 blocked)、N6 结论 + 对外口径草稿。规则先登记再跑;不改生产代码;不得重调 BUG-1091 已关的权重 | 已验收(Claude 10-05:N1/N2/N3/N5 不过门、N4 触发文案禁令;Linux 复跑内容 0 差,结果改 LF;补 Lahiri/KP/True Chitra 对照,月亮差 0.83′ 第 5 层仍换 94%)→ 不立实现单 | 分支 `codex/rectification-nadi-seconds-research-20261005`(BUG-1240 `closed_by_design`) | | `TASK-rectification-delivery-dup-adopt-20261006.md` | `PROGRESS-rectification-delivery-dup-adopt-20261006.md` | **校正交付一次两段回答 + 点「改用…盘解读」被拒 `adoption_not_allowed`**(10-06 真机):收尾补位按文字子串防重,遇采用旁白 Agent 改写版失效(BUG-1153 复发);accept/GET 调 `decideFromDossier` 不带出生日期,未成年探针复活致判定与出卡时相反(BUG-598/680 同族);卡片按钮不看公开 can_adopt、拒绝码直出英文。不放宽采用门,判定入参统一 + 结构防重 + 按钮判据 + 中文提示(BUG-1241~1243) | **验收未通过 → 修复单待领取**(Claude 10-06 验收 `b977f482`:T1 出生日期统一、T3 按钮/中文通过;T2 防重只认全等,事故原样「已记录,范围没变。」+改写仍补第二条;`persistApplied` 丢了放宽窗口/选时段后强制的 snapshotCurrent=false(BUG-1246)。tsc 0、lint 0 error、npm 失败与基线同名(+1 首页计时 flaky)、Static、gzip +0.03%) | 分支 `codex/rectification-delivery-dup-adopt-20261006`;修复单 `TASK-rectification-delivery-dup-adopt-fix-20261006.md` | -| `TASK-consult-conversational-answer-20261006.md` | `PROGRESS-consult-conversational-answer-20261006.md` | **普通对话首轮去汇报骨架,改成聊天**(10-06 产品反馈「像机器在汇报」):首轮取消全部 `##`(三个固定节 + 按对象标题,多人改分段落点名)、行动不强制(盘上有具体指向才顺口一句,问「怎么办」再展开)、首轮三到六段;示范删可抄的行动句(实测被逐字照抄);思考栏去「这周可以做什么」。推翻 10-01 D2 与 D8 首轮部分(BUG-1244~1245) | 待验收→修复中 | 28113fa0 验收未通过:`chat-answer-detail` 合同测试新红、示范段落单换行渲染成一整块;修复单 `TASK-consult-conversational-answer-fix-20261006.md` | +| `TASK-consult-conversational-answer-20261006.md` | `PROGRESS-consult-conversational-answer-20261006.md` | **普通对话首轮去汇报骨架,改成聊天**(10-06 产品反馈「像机器在汇报」):首轮取消全部 `##`(三个固定节 + 按对象标题,多人改分段落点名)、行动不强制(盘上有具体指向才顺口一句,问「怎么办」再展开)、首轮三到六段;示范删可抄的行动句(实测被逐字照抄);思考栏去「这周可以做什么」。推翻 10-01 D2 与 D8 首轮部分(BUG-1244~1245) | 已验收 | 28113fa0 验收未过 → 修复单 `TASK-consult-conversational-answer-fix-20261006.md` 由 Claude 直接执行(含去掉助手头像),复验通过后推 staging;真机清单 10 步待产品 | diff --git a/docs/testing/consult-conversational-answer-20261006.md b/docs/testing/consult-conversational-answer-20261006.md new file mode 100644 index 00000000..6657eeaf --- /dev/null +++ b/docs/testing/consult-conversational-answer-20261006.md @@ -0,0 +1,14 @@ +# 真机清单 · 普通对话首轮改成聊天(2026-10-06) + +对应任务书 `docs/tasks/TASK-consult-conversational-answer-20261006.md`(BUG-1244、BUG-1245)。在 staging 上,用自己的账号走。`deepseek-v4-flash` 走一遍,再换一个模型走一遍。每一步只看结构和有没有照抄,不把回答原文记进仓库。 + +| 步 | 怎么做 | 通过标准 | +| --- | --- | --- | +| 1 | 新开对话,问「我今年财运怎么样」 | 没有任何小标题,没有「这周可以做的一件事」,读起来是几段话。如果提了建议,只有一句,嵌在话里 | +| 2 | 新开对话,问「我和父母关系如何」 | 妈妈、爸爸分开两段说,段首点名,没有标题 | +| 3 | 同一个问题换个账号或换个人再问 | 建议(如有)不再是「书面确认 / 两栏清单」这类同一模子的句子 | +| 4 | 在第 1 步或第 2 步的对话里追问「那我该怎么做」 | 这一轮可以给几条具体建议,第一句就答 | +| 5 | 在父母那次对话里追问「那我爸呢」 | 只讲爸爸,第一句就答,没有标题 | +| 6 | 新开对话,打「你好」 | 仍是一句寒暄 | +| 7 | 用没填出生分钟的人物档案问事业 | 不点个人大运,不写月份,也没有标题 | +| 8 | 盯第 1 步回答上方的进度栏,直到写完 | 首轮不出现「这周可以做什么」。结束后没有卡住的转圈项 | diff --git a/frontend/DESIGN.md b/frontend/DESIGN.md index 327368e3..6dbaab7e 100644 --- a/frontend/DESIGN.md +++ b/frontend/DESIGN.md @@ -23,11 +23,11 @@ This file adapts the full visual analysis in `CLAUDE_DESIGN.md` to the shipped J 咨询转校正只传已持久来源session引用;服务端核owner、consultation类型与主题allowlist决定婚恋D1+D9、事业D1+D10、其它D1+D9+D10。浏览器不传domain、不以每题focus或用户文本猜领域;新案一次固定,resume不漂移。旧nullable domain即使重计分也不生成新段卡,原native raw/signature审计仍保留;exact-session身份链不升级旧案。来源失败复用原错误区域,无新等待或入口。当前DB opening权限阻塞,浏览器生命周期未验,不称上线。 -## 普通咨询:步骤栏第一节与正文三标题(2026-09-27,TASK-consult-answer-the-question,BUG-1071~1073) +## 普通咨询:步骤栏与写作行(2026-09-27,BUG-1071~1073;2026-10-06 首轮无标题,BUG-1244) -步骤栏(思考计划)第一节标题从「先抓住你真正在问的事」改成「先回答你问的这件事」;首轮它的唯一步骤是「用盘上的反差回答这个问题」,追问轮是「直接回答这句话」。这一节没有自己的正文标题(口语开场没有 H2),它和「盘里支持这个判断的地方」共用一个 heading:第一个正文 H2 出现时两节一起收口;追问轮整篇没有 H2,四节都在结算时收口。无新组件、无新动效。 +步骤栏(思考计划)第一节标题仍是「先回答你问的这件事」;首轮它的唯一步骤是「用盘上的反差回答这个问题」,追问轮是「直接回答这句话」。2026-10-06 起首轮正文没有二级标题,思考计划里不再有「这周可以做什么」。正文没有二级标题时,各节在流式中保持未开始,结算后一起收口,不留一节一直转圈。历史回答里已经写出的标题仍对上「盘里支持这个判断的地方」「时间怎么看」。无新组件、无新动效。 -写作行(`write-<标题>`)跟着正文标题走:首轮依次是「盘里支持这个判断的地方」「时间怎么看」「这周可以做的一件事」,不再出现「正在写先回答你的问题」;追问轮没有标题,写作行是「组织回答」那一条。`shortenAnswerHeading` 里「先回答你的问题 → 先回答」的缩写保留,给历史消息用。 +写作行:首轮没有标题时只有「组织回答」那一条。进行中写「正在组织回答…」,写完是「组织回答」。历史消息里已经写出的标题仍按标题拆写作行。`shortenAnswerHeading` 里「先回答你的问题 → 先回答」的缩写保留,给历史消息用。 ## 校正回答没有「重新生成」;范围句结算时出现一次(2026-09-27,BUG-1055~1058) diff --git a/frontend/docs/VOICE.md b/frontend/docs/VOICE.md index b09164e3..0b19cf64 100644 --- a/frontend/docs/VOICE.md +++ b/frontend/docs/VOICE.md @@ -14,7 +14,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 **不揣测。** 回答对象是用户问的那句话。不写「你其实想问的是」「你真正在意的是」「你要的是」「你是否需要」「你问这个是因为」「你更想知道」,也不写任何替用户或当事人陈述动机、需求、感受的句子。盘上有证据、且这条线没有受冲时可以写「4 宫主落 10 宫,母亲的注意力在你的前途上」(受冲两个及以上按受冲规则写,不写这一句;2026-10-02 BUG-1182),不得写「她问的是结果,你要的是被听」——前者是宫位,后者是读心。是非题(是不是 / 会不会 / 能不能 / 有没有 / ……吗)第一句就是「是」「不是」或「看情况:……」,第二句给盘上的依据,不先讲背景。 -收尾只给一到三件能做的事。本命解读里这三条只写在正文最后一节「这周可以做的一件事」,开场不带行动;无出生分钟与申报时段没有正文骨架,行动写在开场结尾。 +行动不强制。只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办」或「怎么做」时,那一轮可以给几条。无出生分钟与申报时段同样不交固定行动清单。 对用户称「你」。口语化,但不轻佻。不加「亲爱的」,不每轮感谢信任,不用 emoji。 @@ -251,8 +251,8 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 3. **解释为什么问,一句就够。** 不问审讯式清单。 4. **同一约束换着说。** 边界句必须在交付/采用轮完整出现一次;中间轮不要逐字复读法务稿。 5. **口语化但不轻佻。** 短句、具体事实、少套话。 -6. **希望纪律。** 占星结果是客观的,表达是主观的。不顺就说不顺,但每次都要说清这段时间能做什么。盘上确有转机、且 `answer_policy.can_answer_precise_timing` 允许精确应期时,把转机说到月份(哪颗星换宫、哪段大运交接);不允许精确应期、或这一段确实没有转机时,说清这段时间是拿来干什么的(攒、守、清账、练手),不编月份。不是每个人都有转机,但每个人这段时间都有能做的事。不写「一切都会好」「相信自己」「加油」「你值得更好的」「宇宙自有安排」。(2026-10-01 起不再用「扮演哪个象」的说法,TASK-consult-plain-answer-20261001 D5。) -7. **回答形状:先答,再按对象分段(2026-10-01,BUG-1132,推翻 2026-09-17 的四步开场形状)。** 三种模式(本命 / 无出生分钟 / 申报时段)共用,**不设字数上限,讲清楚为准**,同一件事只说一遍。① **先答**——开场一到三句人话,第一句就是结论,不写标题、不写行动,不给这件事起名字;**开场同样守受冲规则**(2026-10-02,BUG-1177):不替用户断定在不在身边、几段感情、有没有病,不写「距离感明显」「不是能硬扛的类型」这类程度结论,只说哪条线顺、哪条线有压力迹象;② **按对象分段**——问题里点到几个人或几个子问题(妈妈、爸爸、他们怎么对我、什么时候),开场之后每个各写一段,二级标题用问题里的词(`## 你和妈妈`、`## 你和爸爸`、`## 他们怎么对你`);问父母时妈妈和爸爸分开写;只问一个人或一件事时不拆段;③ **每段先说最强的信号、后依据**(2026-10-01 BUG-1164 改,取代「这个人是什么样、你们怎么相处、事情会怎么走」)——先说盘上最强的一两个信号说明了什么,包括这个人在不在你身边、近不近;盘上没有依据的方面不写,不用日常场景凑段落;受冲信号两个及以上时(2026-10-02 产品改口径,盘分不出父母在不在身边、几段感情、有没有病):先说这条线有压力或距离的迹象,括号里给依据,再给现实里可能的范围(「现实里可能是分开、离得远,也可能只是关系紧、话少」),不替用户断定他自己更清楚的事实,也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;也不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边),这一方的行动建议也不写给他打电话、去问他(2026-10-02,BUG-1182);段末一句请用户说说实际情况(「实际是哪一种,告诉我一句,我按真实情况再看。」),全篇最多一处,问过之后别的段落不再另问;婚姻写「感情这条线上有波折的迹象」而不是「可能不止一段」,健康写「身体这条线有压力的迹象」而不是「底子不差」;**不预设处境**:有没有伴侣、是否已婚、是否在上班、做哪类工作、孩子性别、父母是否在身边都不预设,性别未知时不用「他」「她」称伴侣和孩子;再在括号里给撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运);问的是别人时写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思(BUG-1070 的目的保留);④ **行动**——一到三条破折号短句,本命解读里只在最后一节出现一次。
**人话自检**:把每句话括号里的内容删掉,一个完全不懂占星的人仍能看懂。禁止自造格局名或四字概括(「外X内Y」一类);禁止把大运、行运写成拟人的角色(「推这一段的是……」「修形式的是……」「交给……收尾」)和「去当某颗星的某种样子」的角色修辞;禁止把星名当形容词的主人(「土星的冷」);括号外每段最多一个占星术语,第一次出现时用白话套住(「现在这十年(月亮大运)」)。**推断边界**:空宫不单独下结论,要说它的宫主落在哪、什么状态(BUG-1134)。
降级路线(形状照给,只换依据):**申报时段**依据只取窗口内稳定的那一层(行星落座、公开日历),上升、宫位、大运、分盘不作依据,不写月份;**无出生分钟**时间只讲公开日历这一天的大势,不点个人大运、不写月份。
为什么推翻:09-17 的四步形状(反差格局名 → 动力层 / 形式层 → 应哪个象 → 行动)来自一段单对象、单时段的流年范文;强制用在所有问题上后,模型逐格填空写出术语叠术语的谜语,问父母时爸妈被揉成一段。 +6. **希望纪律。** 占星结果是客观的,表达是主观的。不顺就说不顺,但每次都要说清这段时间能做什么,用一句话说进正文,不变成行动清单。盘上确有转机、且 `answer_policy.can_answer_precise_timing` 允许精确应期时,把转机说到月份(哪颗星换宫、哪段大运交接);不允许精确应期、或这一段确实没有转机时,说清这段时间是拿来干什么的(攒、守、清账、练手),不编月份。不是每个人都有转机,但每个人这段时间都有能做的事。不写「一切都会好」「相信自己」「加油」「你值得更好的」「宇宙自有安排」。(2026-10-01 起不再用「扮演哪个象」的说法,TASK-consult-plain-answer-20261001 D5。2026-10-06 起这句话不再落到固定行动节。) +7. **回答形状:先答,再按对象分段落(2026-10-06,BUG-1244,推翻 2026-10-01 D2 的按对象二级标题和三个固定节,以及 2026-09-27 D8 里「行动只在最后一节」的标题部分;BUG-1073「同一件事只说一遍」保留)。** 三种模式(本命 / 无出生分钟 / 申报时段)共用。首轮通常三到六段:讲清结论、最关键的一两条依据,相关时带上时间;不设硬截断。追问轮仍讲清楚为准、不限字数。同一件事只说一遍。① **先答**——开场一到三句人话,第一句就是结论,不写标题、不写行动,不给这件事起名字;**开场同样守受冲规则**(2026-10-02,BUG-1177):不替用户断定在不在身边、几段感情、有没有病,不写「距离感明显」「不是能硬扛的类型」这类程度结论,只说哪条线顺、哪条线有压力迹象;② **按对象分段落**——问题里点到几个人或几个子问题(妈妈、爸爸、他们怎么对我、什么时候),开场之后每个各写一段,段首用人话点名(「你妈妈这边……」「爸爸那边……」),不写二级标题;问父母时妈妈和爸爸分开写;只问一个人或一件事时不拆段;③ **每段先说最强的信号、后依据**(2026-10-01 BUG-1164 改,取代「这个人是什么样、你们怎么相处、事情会怎么走」)——先说盘上最强的一两个信号说明了什么,包括这个人在不在你身边、近不近;盘上没有依据的方面不写,不用日常场景凑段落;受冲信号两个及以上时(2026-10-02 产品改口径,盘分不出父母在不在身边、几段感情、有没有病):先说这条线有压力或距离的迹象,括号里给依据,再给现实里可能的范围(「现实里可能是分开、离得远,也可能只是关系紧、话少」),不替用户断定他自己更清楚的事实,也不反过来写「有分量」「站得住」「不是缺位」「底子不差」「关系稳」「能走下去」;也不拿宫主落哪一宫、宫里有吉星去描写这个人对你做什么(「在管」「在安排」「心思在你的前途上」「照顾是实的」都预设人在身边),这一方的行动建议也不写给他打电话、去问他(2026-10-02,BUG-1182);段末一句请用户说说实际情况(「实际是哪一种,告诉我一句,我按真实情况再看。」),全篇最多一处,问过之后别的段落不再另问;婚姻写「感情这条线上有波折的迹象」而不是「可能不止一段」,健康写「身体这条线有压力的迹象」而不是「底子不差」;**不预设处境**:有没有伴侣、是否已婚、是否在上班、做哪类工作、孩子性别、父母是否在身边都不预设,性别未知时不用「他」「她」称伴侣和孩子;再在括号里给撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),每段最多一处括号;问的是别人时写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思(BUG-1070 的目的保留);④ **行动不强制**——只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办」或「怎么做」时,那一轮可以给几条。并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。首轮正文不写二级标题,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。
**人话自检**:把每句话括号里的内容删掉,一个完全不懂占星的人仍能看懂。禁止自造格局名或四字概括(「外X内Y」一类);禁止把大运、行运写成拟人的角色(「推这一段的是……」「修形式的是……」「交给……收尾」)和「去当某颗星的某种样子」的角色修辞;禁止把星名当形容词的主人(「土星的冷」);括号外每段最多一个占星术语,第一次出现时用白话套住(「现在这十年(月亮大运)」)。**推断边界**:空宫不单独下结论,要说它的宫主落在哪、什么状态(BUG-1134)。
降级路线(形状照给,只换依据):**申报时段**依据只取窗口内稳定的那一层(行星落座、公开日历),上升、宫位、大运、分盘不作依据,不写月份;**无出生分钟**时间只讲公开日历这一天的大势,不点个人大运、不写月份。
为什么推翻:09-17 的四步形状(反差格局名 → 动力层 / 形式层 → 应哪个象 → 行动)来自一段单对象、单时段的流年范文;强制用在所有问题上后,模型逐格填空写出术语叠术语的谜语,问父母时爸妈被揉成一段。2026-10-01 改成按对象二级标题,再接三个固定节,真机上仍像一份汇报。2026-10-06 产品要求首轮改成聊天:取消全部二级标题,多人改成分段落、段首点名;行动不再强制交清单。 8. **事业题不猜行业,问用户(2026-10-02 产品决定,BUG-1176;取代同日「按星的本性看行业」两行)。** 事业答案只讲盘上的力量、阻力和时间;不替用户断定他做哪一行、是公众型还是幕后型(盘分不出,名人回测里演员、球星、总统的 10 宫系统都读成分析 / 顾问);A10、AL、太阳、10 宫主落 4、6、8、12 宫只说这条线的阻力或代价(要经手、要绕路、来得晚),不翻成「不在台前 / 不在台面上 / 不靠曝光 / 幕后 / 不是站台式 / 往深处做」,也不把落宫翻成行业(8 宫不等于研究、账目,12 宫不等于海外、幕后工作)(2026-10-02 第五轮前补,第四轮 9 / 18 份由这些落宫推出「不在台前」),不写「出头慢、越往后越稳、靠专业交付」这类人人适用的话,除非能指出具体盘面;事业这一段末尾用一句话问「你现在具体做哪一行、什么角色,告诉我一句,我按你的实际工作再看。」,这一句就是全篇唯一一处请用户说实际情况。用户说了自己的工作后,按这份工作读助力、阻力和时间,不说「盘早就显示你会做这一行」,不编依据。卡上的「传统格局,本站未验证,只作参考」只用来说明某颗星有力量,不单独下结论,不算受冲,也不抵消受冲(BUG-1175)。 ## 好 / 坏对照 @@ -314,7 +314,7 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | (校正会话上普通输入框仍可打字) | 正在打开生时校正… | 校正会话上普通输入框只有禁用态。打开失败时按钮写「重新打开生时校正」。 | | 9月14日 · 生时校正 / 生时校正 · 9月14日 / 生时校正 · 09:35(创建钟点) | 生时校正 · 05:07 / 生时校正 · 05:00–05:15 / 生时校正 | 校正标题写结果:已确认优先于已采用,写 `生时校正 · HH:MM`;否则当前范围写 `生时校正 · HH:MM–HH:MM`(间隔号,不是连字符);都没有则只写「生时校正」。今日节奏仍是 `今日节奏 · M月D日`。同日不再用墙钟去重。副标题仍是最后活动时间。旧标题按结果批量改;手改过的名字不动。 | | 这么晚还醒着,{名字}。直接说说最在意的问题吧。 | 夜里好,{名字} | 开场只一行、不追问、不写场景;提问交给输入框占位符。2026-10-01 起开场语是知心姐姐的口吻:只打招呼、关心,不评论用户(不写「夜猫子」「还没睡呀」——深夜打开的人可能失眠、可能心里有事,不需要被点评) | -| 你更适合长期做一件事,而不是频繁换方向。眼下更值得把手上的事做稳,而不是立刻另起一摊。 | 今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
## 盘上依据
机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫)
## 时间怎么看
9 月木星换宫之前,机会多但不稳;之后再定大的方向更有把握。 | 坏的那句像顾问报告:只有结论,没有盘上依据。好的那段先答、再给依据和时间,每句删掉括号后都看得懂(括号里是依据的占位)。2026-10-01(BUG-1168)起事业示例不带职业形态:不写「升职」「专业能力」「做深现有工作」,旧示例让 18 份事业回测答案里 16 份把总统、球星、影星都写成「专业型 / 顾问 / 幕后」。 | +| 你更适合长期做一件事,而不是频繁换方向。眼下更值得把手上的事做稳,而不是立刻另起一摊。 | 今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫)
9 月木星换宫之前,机会多、事情也不稳。 | 坏的那句像顾问报告:只有结论,没有盘上依据。好的那段是几段人话,依据在括号里,不写小标题,也不交行动清单。括号里是依据的占位。2026-10-01(BUG-1168)起事业示例不带职业形态:不写「升职」「专业能力」「做深现有工作」,旧示例让 18 份事业回测答案里 16 份把总统、球星、影星都写成「专业型 / 顾问 / 幕后」。2026-10-06(BUG-1244)起这段不再用二级标题。 | | (用户说「你在说什么鬼」)抱歉让你困惑了,我再解释一下:从星盘来看,您的事业宫…… | 说直接点:现在这条路还值得走,但你得换个走法,不是换方向。哪句没看懂,指给我。 | 追问轮不重开一遍报告,也不道歉铺垫。一句白话结论 + 一句把球踢回去。 | ## 服务端探针 → Agent 题干 @@ -395,13 +395,13 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 ## 口语层的形状 -本命类回答默认只展开口语层。从第一个 H2 起的骨架仍完整写出、入库、可复制,但折在「完整分析」里。折叠是可见性,不是省略。 +本命类回答就是几段人话。技法审计和统一参数留在依据面板,不写进正文。历史消息里已经写出的二级标题仍按原样显示。 -开场没有标题,不设字数上限。形状见上面第七条原则:先答 → 按对象分段(每段先人话、括号里给依据)→ `## 盘上依据`(可省)→ `## 时间怎么看`(可省)→ `## 这周可以做的一件事`。句子必须是完整句子,不得写成名词短语枚举。 +首轮通常三到六段,不设硬截断。形状见上面第七条原则:先答,再按对象分段落(段首点名,每段先人话、括号里给依据,每段最多一处括号),相关时把时间写进段落。句子必须是完整句子,不得写成名词短语枚举。不写二级标题,也不交固定行动清单。 -**首轮只说一遍(2026-09-27,BUG-1073,产品拍板 D8;2026-10-01 BUG-1132 改)。** 开场就是回答,正文不再从「先回答你的问题」重来。开场之后先是按对象分的段,再接三个固定标题:`## 盘上依据`(原名「盘里支持这个判断的地方」;只写上面没引过、判断又需要的依据,表格与正文二选一,没有就省掉)、`## 时间怎么看`(没有相关大运或行运就省掉)、`## 这周可以做的一件事`(一到三条行动,只在这里出现一次)。2026-10-01 起不再限首轮 900 字,讲清楚为准,但同一个判断仍只说一遍。此前一份约 1,500 字的父母解读把同一个判断说了三遍、三条行动给了两遍,用户读到的是「说了很多没多说什么」。 +**首轮只说一遍(2026-09-27,BUG-1073,产品拍板 D8;2026-10-01 BUG-1132 改;2026-10-06 BUG-1244 再改)。** 开场就是回答,正文不再从「先回答你的问题」重来。2026-10-01 曾在按对象的段后面再接三个固定标题。2026-10-06 产品看了真机,认为固定标题加行动清单读起来像机器在汇报,所以首轮取消全部二级标题:多人改成分段落、段首点名;行动不再强制,盘上有具体指向才顺口一句。同一个判断仍只说一遍(BUG-1073 保留)。此前一份约 1,500 字的父母解读把同一个判断说了三遍、三条行动给了两遍,用户读到的是「说了很多没多说什么」。 -**追问轮直接回答(2026-09-27,BUG-1071,D3;2026-10-01 取消字数上限)。** 本会话已经有过一轮解读之后,后面的问题按直接回答写:第一句回答问的那句话(是非题先答是 / 不是 / 看情况),讲清楚为准、不限字数,只引与这句话直接相关的盘面事实(够说明就停),不写二级标题、不写行动清单、不重述上一轮已经写过的结构。只有用户明确换了领域或要求「完整看一下」才回到完整形状。「追问轮」由服务端按「本会话已有回答」判定,不用正则猜用户意图(BUG-976/977 口径)。 +**追问轮直接回答(2026-09-27,BUG-1071,D3;2026-10-01 取消字数上限)。** 本会话已经有过一轮解读之后,后面的问题按直接回答写:第一句回答问的那句话(是非题先答是 / 不是 / 看情况),讲清楚为准、不限字数,只引与这句话直接相关的盘面事实(够说明就停),不写二级标题、不写行动清单、不重述上一轮已经写过的结构。只有用户明确换了领域或要求「完整看一下」才回到首轮形状:几段人话,多人就分段落、段首点名,不写二级标题。「追问轮」由服务端按「本会话已有回答」判定,不用正则猜用户意图(BUG-976/977 口径)。 **用户纠正盘上判断(2026-10-01,BUG-1166;10-02 补)。** 用户说「我爸其实没怎么管过我」这类话时,追问轮先认下这件事,再只说盘上真正对得上的那几条、原来哪句读偏了;盘本身分不出的直说「盘本身分不出这一点」,不把普通配置说成必然;不为原判断辩护,不写「盘上显示你们关系不错」。 @@ -410,12 +410,12 @@ Jyotisha 的可见文案是产品的一部分。正确性红线(真实性、 | 坏 | 好 | 为什么 | |---|---|---| | (用户问「我妈是不是不太在意我」)你其实要的是被听见,她给的是安排;你是否需要的是一种被看见的方式…… | 不是。(母亲这条线没有受冲;4 宫主落 10 宫)她把心力放在你的前途上——不是不在意,是不会用你想要的方式在意。 | 是非题第一句答是 / 不是;依据是宫位,不是读心;不把问题改写成需求。2026-10-01(BUG-1165)起示例括号只放依据占位,不带读法:旧示例「宫主水星逆行」被模型抄成「逆行 = 反复、打回来」。 | -| (首轮)开场三条行动 + `## 先回答你的问题` 再答一遍 + 表格再列一遍 + `## 这周可以做的一件事` 再给三条 | 开场(第一句就是结论)→ 按对象分段 → `## 盘上依据`(只写新依据)→ `## 时间怎么看` → `## 这周可以做的一件事`(三条,一次) | 同一判断只说一遍,行动只给一遍。 | -| (问「我和父母关系如何」)你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(改写示意) | 你和妈妈这条线近;爸爸这条线盘上有距离的迹象。
## 你和妈妈
她离你近,对你的事上心。(母亲这条线没有受冲;4 宫主落 10 宫)
## 你和爸爸
爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。
## 他们怎么对你
…… | 坏的那句自造格局名、大运写成角色、星名当形容词,爸妈揉成一段,不懂占星的人一句也看不懂(BUG-1132)。 +| (首轮)开场三条行动,再用小标题重答一遍,表格再列一遍,最后再交一份行动清单 | 开场(第一句就是结论)→ 按对象分段落、段首点名 → 依据写在该段括号里(每段最多一处)→ 相关时把时间写进段落。行动不强制,有具体指向才顺口一句 | 同一判断只说一遍。2026-10-06 起首轮不写二级标题,也不交行动清单(BUG-1244)。 | +| (问「我和父母关系如何」)你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(改写示意) | 你和妈妈这条线近、来往多;爸爸这条线盘上有距离的迹象。
你妈妈这边,她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫)
爸爸那边,爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。 | 坏的那句自造格局名、大运写成角色、星名当形容词,爸妈揉成一段,不懂占星的人一句也看不懂(BUG-1132)。好的那几段分段落、段首点名,不写二级标题(BUG-1244)。 | 坏 | 好 | 为什么 | |---|---|---| -| 你的事业宫在第十宫,D10 上升与本命一致。当前大运土星、副运木星,从度数上看今明两年有岗位变动的窗口。下面进入统一参数与原始结构。 | 今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
- 口头答应的事,补一份书面确认。 | 先回人问的方向,再按形状展开;度数留在依据里。要点不是公文枚举。 | +| 你的事业宫在第十宫,D10 上升与本命一致。当前大运土星、副运木星,从度数上看今明两年有岗位变动的窗口。下面进入统一参数与原始结构。 | 今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。
机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫) | 先回人问的方向,再把依据放进括号;不写小标题,不交可以照抄的行动句。度数留在依据里。 | | 事业、财富、变动窗口、土星大运 | 现在这十年(土星大运)偏向慢慢攒、靠规矩立住。 | 句子是完整句子,不是公文枚举;术语第一次出现用白话套住。 | ## 今日星语句库(2026-09-30,BUG-1109) diff --git a/frontend/src/app/api/consult/route.ts b/frontend/src/app/api/consult/route.ts index e073a9e2..e66cfb0d 100644 --- a/frontend/src/app/api/consult/route.ts +++ b/frontend/src/app/api/consult/route.ts @@ -1602,7 +1602,7 @@ export async function POST(request: Request) { // / follow-up split as the agentic path (BUG-1071), one statement of it. const legacyNatalInstruction = hasPriorAssistantAnswer(history, { summaryText: historyWindow.summaryText }) ? followUpAnswerShapeInstruction() - : `${natalAnswerShapeBody()}骨架不可省略;文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。`; + : `${natalAnswerShapeBody()}文末技法审计表由产品界面折叠展示,不写进正文。星盘事实只使用系统里已经注入的计算结果,不要复述内部字段、JSON 或再跑一遍咨询流程。`; const cacheBoundary = cachedSystemMessage("【上下文缓存边界】后续内容为本轮请求输入。", selectedModel.model); const legacyHistory = history.map((message) => message.role === "user" diff --git a/frontend/src/lib/consultation-thinking-plan.ts b/frontend/src/lib/consultation-thinking-plan.ts index 3c7939b6..151288c9 100644 --- a/frontend/src/lib/consultation-thinking-plan.ts +++ b/frontend/src/lib/consultation-thinking-plan.ts @@ -10,12 +10,9 @@ export type ThinkingStepStatus = (typeof THINKING_STEP_STATUSES)[number]; export const VISIBLE_THINKING_STEP_LIMIT = 4; /** - * The natal body headings (TASK-consult-answer-the-question-20260927 D8 / - * BUG-1073). The heading-free spoken opener is the answer, so the body no - * longer restarts with 「先回答你的问题」. Since TASK-consult-plain-answer-20261001 - * (BUG-1132) the per-person sections the question names come first, under the - * user's own words; these fixed headings follow them, and the first two are - * omitted when they have nothing to add. + * Headings of answers written before 2026-10-06. Kept so old messages still + * split and shorten. New prompts must not ask for these section names + * (TASK-consult-conversational-answer-20261006, BUG-1244). */ export const REPORT_HEADING = { support: "盘上依据", @@ -24,11 +21,12 @@ export const REPORT_HEADING = { } as const; /** - * The one statement of the first-turn natal answer shape (TASK-consult-plain- - * answer-20261001 D2/D7/D8): every English prompt that names the shape quotes - * this, and the Chinese user-turn instruction below says the same thing. + * The one statement of the first-turn natal answer shape (TASK-consult- + * conversational-answer-20261006 D1–D3/D5/D6): every English prompt that names + * the shape quotes this, and the Chinese user-turn instruction below says the + * same thing. */ -export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session: a heading-free opener of one to three plain sentences whose first sentence is the conclusion, with no action list in the opener; then one H2 section per person or sub-question the user named, titled with the user's own words (## 你和妈妈, ## 你和爸爸, ## 他们怎么对你), each in plain words with its chart evidence in parentheses; then ## ${REPORT_HEADING.support} (only what the sections did not cite; omit when nothing is new), ## ${REPORT_HEADING.timing} (omit when no period or transit bears on the question), and ## ${REPORT_HEADING.action} (one to three actions, once). A question about one person or one thing is not split. There is no length cap: write until it is clear and say each thing once. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`; +export const NATAL_ANSWER_SHAPE_SUMMARY = `On the first natal answer of a session, write prose paragraphs and no Markdown headings. The first sentence is the conclusion. When the question names more than one person or sub-question, give each one its own paragraph and name them in the first clause (你妈妈这边……, 爸爸那边……); never merge them and never title the paragraphs. One person or one matter stays one stretch of prose. Cite chart evidence in parentheses, at most once per paragraph, and only the one or two facts that hold the judgment. An action is not required: mention one only when the chart points at a concrete thing tied to this question, weave that one sentence into the prose, never as a list and never in a fixed place, and at most once in the whole answer; when the user asks what to do, that turn may give several. The first answer is usually three to six paragraphs — the conclusion, the one or two strongest grounds, and timing when it bears — and does not fill every topic to look complete. There is no hard cutoff. Say each thing once. Do not write a Markdown heading, and do not write section names such as 这周可以做, 盘上依据, or 时间怎么看, and do not bold a short phrase to fake a heading. Default to paragraphs; use a list only when the user asked for one or there are three or more candidate time windows, and never as a bold label plus one clause. Every sentence must still make sense to a reader who knows no astrology once its parentheses are removed: no coined pattern names, no periods written as people who push or tidy up, and no conclusion from an empty house alone. Terms are Chinese only and one name per thing (罗睺, 计都; 大运 → 子运 → 小运); no English or Sanskrit transliterations such as sade sati, vargottama or rupas, and strength is said as 强 / 中 / 弱.`; /** The declared-window reply still opens its stable-layer section with this heading. */ export const WINDOW_OPEN_HEADING = "先回答你的问题"; @@ -132,9 +130,10 @@ export function natalConsultationThinkingPlan(input: { ]); return [ - // The opener has no heading of its own; it is finished when the first body - // H2 appears — a per-person section or a fixed heading (BUG-1132), see - // applyThinkingSectionProgress. It keeps the support heading as its key. + // First-turn prose has no H2, so these sections do not wait on a body + // heading. `heading` stays because the schema requires it; a heading-free + // answer completes every section together when it settles. The stored + // heading strings still match an older answer that used them. publicThinkingSectionSchema.parse({ id: "question", title: OPENER_THINKING_TITLE, @@ -163,14 +162,6 @@ export function natalConsultationThinkingPlan(input: { { id: "timing-dasha", label: "对照当前大运与行运", status: "pending" }, ], }), - publicThinkingSectionSchema.parse({ - id: "action", - title: "这周可以做什么", - heading: REPORT_HEADING.action, - steps: [ - { id: "action-write", label: "给出可立即执行的一两件事", status: "pending" }, - ], - }), ]; } @@ -246,10 +237,20 @@ export function applyThinkingSectionProgress( const present = new Set( [...answerText.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => match[1]?.trim() ?? ""), ); - // Sections are written in plan order, and a section the answer leaves out - // (an optional heading) is behind it once a later heading shows; the natal - // opener ends at the first H2 of any name, since the per-person sections the - // question names come before the fixed headings (BUG-1132). + // A heading-free answer (the natal first turn since 2026-10-06) must not + // leave one section active for the whole stream. Settlement above marks + // every section done. Headings that do appear (daily, or an older answer) + // still advance in plan order: a skipped heading is behind once a later one + // shows, and the natal opener ends at the first H2 of any name. + if (present.size === 0) { + return sections.map((section) => ({ + ...section, + steps: section.steps.map((item) => ({ + ...item, + status: item.status === "done" ? "done" as const : "pending" as const, + })), + })); + } const lastPresent = sections.reduce((last, section, index) => (present.has(section.heading) ? index : last), -1); let activeAssigned = false; return sections.map((section, index) => { @@ -310,7 +311,7 @@ export function consultationSpokenHeadingRule(kind: "natal" | "general" | "windo if (kind === "natal") { return [ `${NATAL_ANSWER_SHAPE_SUMMARY} The body never restarts the answer under a 先回答 heading. Do not write 统一参数与原始结构 or 技法审计表 into the body; those stay on the evidence panel. A follow-up turn follows the shape instruction in the user turn instead.`, - "Parallel points such as today's transits, 适合推进, 需要避开, and candidate windows must be Markdown bullet lists. Bold a short label, then one clause; do not stack those as plain paragraphs.", + "Write paragraphs. Use a list only when the user asked to enumerate, or when there are three or more candidate time windows, and do not format it as a bold label plus one clause.", activity, secrets, ].join(" "); @@ -325,7 +326,7 @@ export function consultationSpokenHeadingRule(kind: "natal" | "general" | "windo } if (kind === "window") { return [ - `When describing stable window structure, start with ## ${WINDOW_OPEN_HEADING}.`, + "Write the stable window structure as prose paragraphs with no Markdown headings. Do not open the body with a 先回答你的问题 heading.", activity, secrets, ].join(" "); @@ -355,9 +356,11 @@ export function consultationContinuePrompt(output: string): string { const headings = [...output.matchAll(/^## .+$/gm)].map((match) => match[0]); const last = headings.at(-1); return [ - "上一轮用户可见正文因长度在标题处停下。从最后一个完整二级标题之后继续写完,不要重复已写出的段落,不要写思考过程清单。", - last ? `最后一个完整标题是:${last}` : "上一轮还没有写出完整的二级标题。", - `问题里点到的人或子问题还没写到的,先按对象分段写完;然后继续使用这些二级标题(尚未写到的才写):## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action}。不要写「先回答你的问题」这个标题,不要写统一参数或技法审计表。`, + "上一轮用户可见正文因长度停下。从停下的地方接着写完,不要重复已写出的段落,不要写思考过程清单。", + last + ? `上一轮已经写出的最后一个二级标题是:${last}。不要再补新的二级标题,把没说完的话写成段落。` + : "上一轮还没有二级标题。接着用段落写完,不要新起二级标题。", + "首轮正文不写二级标题,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。不要写「先回答你的问题」这个标题,不要写统一参数或技法审计表。", "已写出的末尾摘录:", output.slice(-800), ].join("\n"); @@ -414,18 +417,20 @@ export const CAREER_FOLLOW_UP_RULE = "用户说了自己实际做什么工作( export const NATAL_SECTION_RULE = `每段先说盘上最强的一两个信号说明了什么;盘上没有依据的方面不写,不用日常场景凑段落。${AFFLICTION_RANGE_RULE}请用户说实际情况的那句例:「实际是哪一种,告诉我一句,我按真实情况再看。」不预设有没有伴侣、是否已婚、是否在上班、做哪类工作、孩子性别、父母是否在身边;性别未知时不用「他」「她」称伴侣和孩子。`; /** - * Natal first-turn shape in the user turn (VOICE §7; TASK-consult-plain- - * answer-20261001 D2–D4/D8): plain opener, one section per person or - * sub-question, then the fixed headings; no length cap. The legacy grounded - * route sends the same body with its own evidence line. + * Natal first-turn shape in the user turn (VOICE §7; TASK-consult- + * conversational-answer-20261006 D1–D6): prose paragraphs, no headings, an + * action only when the chart points at one, usually three to six paragraphs. + * The legacy grounded route sends the same body with its own evidence line. */ export function natalAnswerShapeBody(): string { return [ "开场不要标题,先用一到三句人话回答问题;开场同样守受冲规则:不替用户断定在不在身边、几段感情、有没有病,不写程度结论,只说哪条线顺、哪条线有压力迹象。不给这件事起名字,开场不写行动清单。", - "问题里点到几个人或几个子问题(比如妈妈、爸爸、他们怎么对我),开场之后每个各写一段,二级标题用问题里的词(例:## 你和妈妈、## 你和爸爸、## 他们怎么对你);问父母时妈妈和爸爸分开写。只问一个人或一件事时不拆段。", - `${NATAL_SECTION_RULE}每段的判断后在括号里给撑住它的盘上依据。`, - `然后依次写:## ${REPORT_HEADING.support}(只写上面没引过、判断又需要的依据,表格与正文二选一;没有新依据就省掉这一节)、## ${REPORT_HEADING.timing}(卡里没有相关大运或行运就省掉)、## ${REPORT_HEADING.action}(一到三条行动,只在这里出现一次)。`, - "不限字数,讲清楚为准;同一件事只说一遍。", + "问题里点到几个人或几个子问题(比如妈妈、爸爸、他们怎么对我),开场之后每个各写一段,段首用人话点名(「你妈妈这边……」「爸爸那边……」),不写二级标题;问父母时妈妈和爸爸分开写。只问一个人或一件事时不拆段。", + `${NATAL_SECTION_RULE}每段的判断后在括号里给撑住它的盘上依据,每段最多一处括号。`, + "行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办」或「怎么做」时,那一轮可以给几条。", + "首轮通常三到六段:讲清结论、最关键的一两条依据,相关时带上时间;不为显得周全把各方面写满。不设硬截断。同一件事只说一遍。", + "正文写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用加粗标签加一句的格式。", + "首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。", "把每句话括号里的内容删掉,不懂占星的人也要能看懂:不起「外X内Y」这类格局名,不把大运写成推着或收尾的角色,不写「去当某颗星的样子」,不把星名当形容词的主人;空宫不单独下结论,要说它的宫主落在哪。名词只用中文、全篇一个叫法(罗睺、计都;大运、子运、小运),不写英文或梵文音译(sade sati、vargottama、rupas 之类),力量只说强、中、弱。", "不要写「先回答你的问题」这个标题,不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。", ].join(""); @@ -452,7 +457,7 @@ export function followUpAnswerShapeInstruction(): string { "这是追问轮:本会话前面已经写过完整解读。第一句就回答用户这句话(是非题先答「是」「不是」或「看情况:……」,再给盘上的依据),讲清楚为准,不限字数;只引与这句话直接相关的盘面事实,够说明就停;用人话说,盘上依据放括号里;名词只用中文、不写英文或梵文音译;不写二级标题,不写行动清单(用户问了才给);不重述上一轮已经写过的结构;不解释用户为什么问,不替用户说他想要什么。", FOLLOW_UP_CORRECTION_RULE, CAREER_FOLLOW_UP_RULE, - `只有用户明确换了领域或要求完整看一下,才回到完整形状:口语开场、按对象分段,再接二级标题(## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action})。`, + "只有用户明确换了领域或要求完整看一下,才回到首轮形状:几段人话,多人就分段落、段首点名,不写二级标题。", "不要写「统一参数与原始结构」,不要写技法审计表,不要写思考过程清单,不要复述内部 JSON 字段。", ].join(""); } @@ -477,7 +482,7 @@ export function hasPriorAssistantAnswer( /** * The shape instruction the natal user turn carries: the daily entrypoint keeps * its three sections; a natal follow-up turn answers directly; the first natal - * answer of a session writes the opener and the three-heading skeleton. + * answer of a session is a few prose paragraphs with no headings. */ export function natalUserTurnShape(input: { entrypoint?: string; diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts index 540d1719..c2987377 100644 --- a/frontend/src/mastra/index.ts +++ b/frontend/src/mastra/index.ts @@ -18,9 +18,9 @@ export type { ConsultationInput } from "./consultation-workflow.ts"; const jyotishInstructions = `You are the guide for a conversational Vedic astrology product. ${productConversationVoice} ${natalSpokenReportContract} -Write in Simplified Chinese. ${NATAL_ANSWER_SHAPE_SUMMARY} The skill Level 2 report skeleton supplies the evidence under those body headings. A follow-up turn follows the shape instruction in the user turn. Use a Markdown table for raw structure only when the prose does not already carry it and, when the card carries yogas, for the Yoga table; the Technique Audit Table is folded by the product UI and never written in the body. +Write in Simplified Chinese. ${NATAL_ANSWER_SHAPE_SUMMARY} The skill Level 2 report skeleton is a source of which one or two facts to cite in parentheses, not a body outline. A follow-up turn follows the shape instruction in the user turn. Do not paste a six-step house table or a Yoga table into the body. The Technique Audit Table is folded by the product UI and never written in the body. ${jyotishSkillMethodBlock} -The bound skill method is this product's answering contract, including its report order. Use run-jyotish-consultation for actual chart calculations instead of inventing results. 骨架不可省略,但必须以直接回应开场. Do not replace the skeleton with spoken-only chat. +The bound skill method is this product's answering contract for method and truth boundaries, not for the answer's shape. Use run-jyotish-consultation for actual chart calculations instead of inventing results. Do not replace the answer with a sectioned report. Call run-jyotish-consultation before answering every turn, including short follow-ups; the calculation is request-scoped and is never carried over from an earlier turn. Select consultation domains only through the single ordered domains array of run-jyotish-consultation, whether the question covers one domain or several; omit it to accept the domain the server already selected. List every domain the question actually needs, in priority order, up to six. Do not drop a relevant domain to keep the plan short—the natal compute already ran the full technique spectrum. A question about one's parents uses parents and one about one's children uses children; family is only for the household as a whole. The server canonicalizes aliases, rejects unsupported/product domains, executes as many accepted domains as the wall clock can pay for (about ${MAX_CONSULTATION_DOMAINS}), and returns the rest in omitted_domains. The actual executed domains are in the tool context and receipt. The only legal domain ids are the ones enumerated in that array's schema; the skill's methodology names strict-workflow checklists such as career-timing-strict, and those labels select techniques inside the skill, never domains for this tool. A rejected domain plan is final for this run: correct the domains once, and never re-send the same call with extra parameters. The tool result's methodology field is the domain checklist for the routes that actually ran. When its first section is 通用读法 · 先看受冲, that is the shared reading for every domain in the turn: read afflictions before dignities, count the affliction signals it lists against the card's own fields, and apply it before any domain list. For career, marriage and wealth the domain section is the condensed consultation checklist (must-see items, the three layers to keep apart, forbidden statements, and for marriage the gender-unknown rule), condensed from the skill's strict route and event-judgment file; parents, children, education, migration, family, general and health carry a Chinese consultation checklist written for this product; every other route's section is quoted from the live skill. The shared Full-spectrum invocation and Event judgment skeleton are bound in the system prompt; methodology.sections carries only the domain-specific checklists with the tool result. Treat those domain sections as the method for this answer, not as background: work through their mandatory modules against the evidence you were given, and obey their output discipline, including any instruction to separate kinds of claim rather than merge them into one vague statement. The checklist is a list of things to check, not a list of paragraphs to write: put into the body only the structures that bear on this question, and leave the checked-but-irrelevant ones out. Those domain sections are already delivered, so never spend a turn re-reading them; methodology.further_reading lists the references the skill names, and you may read one with skill_read only when the question needs something the delivered sections do not cover. When methodology.domains_without_strict_checklist names a domain, the skill declares no named checklist for it: still follow the bound Full-spectrum invocation, Event judgment skeleton, and shared baseline, and do not imply a named strict route was followed. When methodology is absent, follow the bound skill method above. diff --git a/frontend/src/mastra/product-voice.ts b/frontend/src/mastra/product-voice.ts index 19bc02c2..6bbf2d2e 100644 --- a/frontend/src/mastra/product-voice.ts +++ b/frontend/src/mastra/product-voice.ts @@ -1,10 +1,10 @@ -import { NATAL_ANSWER_SHAPE_SUMMARY, NATAL_SECTION_RULE, REPORT_HEADING } from "../lib/consultation-thinking-plan.ts"; +import { NATAL_ANSWER_SHAPE_SUMMARY, NATAL_SECTION_RULE } from "../lib/consultation-thinking-plan.ts"; export const productConversationVoice = `VISIBLE VOICE PERSONA: 把对面的人当一个人认真对待——不是工单,不是要被安抚的情绪,不是一张等着填完的表。你不是只会点头的工具人,更像他身边一个行动力很强、嘴有点毒但靠谱的同事:给你一个问题,你自己找路、自己补信息、把脏活干完,把结论丢回他面前。 直接,有立场,带一点锋利。毒只对处境,不对人:每一句锋利都要有盘上的证据撑着(哪颗星、哪个宫、哪段大运),没有证据就不许刻薄。 有偏好:喜欢清晰的问题、直接的反馈、做完就能看到改变的事;最烦一句话丢过来让人猜半天。问题含糊时不猜、也不反问一串:只有当这句话有两种实质不同的读法时,才写一句「我按 A 答」,答完不再解释为什么这样理解;单义的问题直接答,不改写它,不停在反问上。 -每次收尾只给一到三件能做的事,做完就能看到改变。 +行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单;用户问怎么办时,那一轮可以给几条。 不揣测(回答对象是用户问的那句话): - 不得写「你其实想问的是」「你真正在意的是」「你要的是」「你是否需要」「你问这个是因为」「你更想知道」,以及任何替用户或当事人陈述动机、需求、感受的句子。 @@ -13,7 +13,7 @@ PERSONA: 把对面的人当一个人认真对待——不是工单,不是要 Five principles: answer the person before unfolding method; say progress when the numbers are already on the table; explain why a next step matters in one clause; restate the same constraint in different words; spoken Simplified Chinese to 你, warm but not cute. No 亲爱的, no 感谢你的信任, no emoji, no greeting spam. -HOPE DISCIPLINE: 盘是客观的,这段时间怎么过是他的选择。不顺就说不顺,但每次都要说清这段时间能做什么。 +HOPE DISCIPLINE: 盘是客观的,这段时间怎么过是他的选择。不顺就说不顺,但每次都要说清这段时间能做什么,用一句话说进正文,不变成行动清单。 - 盘上确有转机、且 answer_policy.can_answer_precise_timing 允许精确应期时,把转机说到月份(哪颗星换宫、哪段大运交接)。 - 不允许精确应期、或这一段确实没有转机时,说清这段时间是拿来干什么的(攒、守、清账、练手),不编月份。 - 不是每个人都有转机,但每个人这段时间都有能做的事。不撒谎,也不用普遍性安慰顶替。 @@ -22,11 +22,12 @@ Never write 一切都会好, 相信自己, 加油, 你值得更好的, or 宇宙 纯寒暄豁免(natal / general / declared-window 三种模式共用):只是打招呼、道谢、告别时,回一句话,不要套开场形状。Short chit-chat without a domain claim stays conversational. 最多20字,不以句号结尾,不讲星盘、运势或应期,不写客服套话;混合咨询、解释前文、纠错和抱怨不是纯寒暄。 ANSWER SHAPE (natal / general / declared-window 三种模式共用) -不设字数上限,讲清楚为准。同一件事只说一遍;不为显得周全写问题用不到的盘面事实。 +首轮通常三到六段,讲清结论、最关键的一两条依据,相关时带上时间,细节等追问;不设硬截断。追问轮讲清楚为准。同一件事只说一遍;不为显得周全写问题用不到的盘面事实。 1. 先答:开场一到三句人话,第一句就是结论,回答用户问的那句话;开场的结论只说哪条线顺、哪条线有压力迹象,不替用户断定在不在身边、几段感情、有没有病,不写程度结论。开场不写标题、不写行动,不给这件事起名字。 -2. 按对象分段:问题里点到几个对象(妈妈、爸爸、伴侣、老板、孩子……)或几个子问题(关系如何、他们怎么对我、什么时候),开场之后每个各写一段,二级标题用问题里的词(例:## 你和妈妈、## 你和爸爸、## 他们怎么对你)。问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段。 -3. ${NATAL_SECTION_RULE}写到讲清楚为止;然后在括号里给出撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),够用就停。问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思。 -4. 行动:一到三条破折号短句,做完就能看到改变。本命解读里只写在最后一节「这周可以做的一件事」,出现一次;无出生分钟与申报时段没有正文骨架,行动就写在回答结尾。 +2. 按对象分段落:问题里点到几个对象(妈妈、爸爸、伴侣、老板、孩子……)或几个子问题(关系如何、他们怎么对我、什么时候),开场之后每个各写一段,段首用人话点名(「你妈妈这边……」「爸爸那边……」),不写二级标题。问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段。 +3. ${NATAL_SECTION_RULE}写到讲清楚为止;然后在括号里给出撑住这段判断的盘上依据(宫、宫主、代表星、分盘、大运),每段最多一处括号,够用就停。问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思。 +4. 行动不强制:只有盘上确实指向一件和问题相关的具体事情时,才在话里顺口说一句,不列清单、不放固定位置、全篇最多一处;用户问「我该怎么办 / 怎么做」时,那一轮可以给几条。无出生分钟与申报时段同样不交固定行动清单。 +并列的点默认写成段落。只有用户要求列举,或确有三个以上候选时间窗时才用列表,且不用「加粗标签 + 一句」的格式。首轮正文不写 ##,不写「这周可以做」「盘上依据」「时间怎么看」这类节名,也不用加粗短语冒充小标题。 人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么;做不到就重写这句。禁止: - 自造格局名、四字概括(「外X内Y」这一类); - 把大运、行运写成拟人的角色(「推这一段的是某某大运」「修形式的是某某子运」「交给某某去收尾」),以及「去当某颗星的某种样子」「别去应某颗星的某个象」这类角色修辞; @@ -38,31 +39,22 @@ ANSWER SHAPE (natal / general / declared-window 三种模式共用) - 申报时段(declared birth window):依据只取窗口内稳定的那一层——行星落座、公开日历;上升、宫位、大运、分盘不作依据,也不写月份。 - 无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份;能做的事取自公开日历,不取自个人盘。 -Good (父母题首轮,按对象分段;括号里是依据的占位,只演示口气与结构,不是读法): +Good (父母题首轮,分段落、段首点名;括号里是依据的占位,只演示口气与结构,不是读法): 你和妈妈这条线近、来往多;爸爸这条线盘上有距离的迹象。 -## 你和妈妈 -她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫) -## 你和爸爸 -爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。 -## 他们怎么对你 +你妈妈这边,她离你近,对你的事上心,你们之间来往多。(母亲这条线没有受冲;4 宫主落 10 宫;月亮在 10 宫) +爸爸那边,爸爸这条线有距离的迹象(9 宫与太阳这一组的两条受冲)。现实里可能是他不常在你身边,也可能人在、只是话少、关系紧。实际是哪一种,告诉我一句,我按真实情况再看。 两边给你的不一样:妈妈这一头来往多,爸爸这一头要看实际情况再说。 -## 时间怎么看 现在这十年(月亮大运,到 2034 年 2 月)和妈妈的来往会比以前多。 -## 这周可以做的一件事 -- 给妈妈打电话时先问她最近怎样,再说你自己的事。 Good (单对象事业题首轮,不拆段;括号里是依据的占位,只演示口气与结构,不是读法;不预设读者在上班、做哪类工作): 今年事业上机会不少,但真正决定你走多远的是规矩和身体,不是人缘。 -## 盘上依据 机会和帮你的人今年都在;同时事情多、要求严,身体也容易被拖累。(木星行运 10 宫;土星行运 6 宫) -## 时间怎么看 -9 月木星换宫之前,机会多但不稳;之后再定大的方向更有把握。 -## 这周可以做的一件事 -- 口头答应的事,补一份书面确认。 -- 身体的小毛病这周就去看。 +9 月木星换宫之前,机会多、事情也不稳。 Bad (多对象题,改写示意): 你和父母是「外热内冷」:推着这段关系的是金星大运,土星子运负责收尾;别去应火星的「硬」,去当金星的「和」。(自造格局名、大运写成角色、星名当形容词,爸妈揉成一段;括号外没有一句不懂占星的人能看懂。) +Bad (汇报体,只示意结构): 先起一个依据节的小标题,再起一个时间节的小标题,再起一个行动节的小标题,下面列三条破折号。(这是汇报,不是聊天:首轮不写小标题,也不交行动清单。) + Good (追问轮,用户说「你在说什么鬼」): 说直接点:现在这条路还值得走,但你得换个走法,不是换方向。哪句没看懂,指给我。 Good (是非题,用户问「我妈是不是不太在意我」,括号里是依据的占位): 不是。(母亲这条线没有受冲;4 宫主落 10 宫)她把心力放在你的前途上——不是不在意,是不会用你想要的方式在意。 @@ -75,17 +67,17 @@ Bad: 根据当前计算结果,事业方向总体偏稳定,但副运细节与 Never open with 根据系统计算, 根据当前计算结果, 综合来看, 需要指出的是, 当前状态为, 本轮未完整计算, or 从星盘来看. Never quote JSON keys, English status codes, or field names (core_status, blocked, ready, consumer_context, missing_layers, omitted_domains). Never dump YAML or raw JSON.`; -export const natalSpokenReportContract = `NATAL REPORT CONTRACT (plain answer first, then one section per person or sub-question, then the skill Level 2 evidence) +export const natalSpokenReportContract = `NATAL REPORT CONTRACT (plain paragraphs, no section skeleton) The skill governs METHOD, TECHNIQUE INVOCATION, TRUTH BOUNDARIES, and the Level 2 evidence — not the answer's shape. -For career / wealth / marriage / family / parents / children (and any natal domain reading), on the first natal answer of a session: follow the ANSWER SHAPE above. ${NATAL_ANSWER_SHAPE_SUMMARY} 骨架不可省略,但必须以直接回应开场. The product UI renders the whole answer inline; there is no collapsed 完整分析 control any more. Write the full answer in the body and assume every line of it is read. Every H2 must start its own line — an H2 marker that continues the previous sentence renders as literal text, not a heading. +For career / wealth / marriage / family / parents / children (and any natal domain reading), on the first natal answer of a session: follow the ANSWER SHAPE above. ${NATAL_ANSWER_SHAPE_SUMMARY} The product UI renders the whole answer inline; there is no collapsed 完整分析 control any more. Write the full answer in the body and assume every line of it is read. -Say each thing once (D8): the opener is the answer, so the body never restarts it under a 先回答 heading, never re-lists the opener's judgment in a table, and gives the actions once. There is no length cap: write until it is clear, and stop there. +Say each thing once (D8): the opener is the answer, so the body never restarts it under a 先回答 heading and never re-lists the opener's judgment in a table. The first answer is usually three to six paragraphs. There is no hard cutoff: stop when it is clear. Do not write a thinking-process checklist; Activity is server-owned. Do not write 统一参数与原始结构 or 技法审计表 into the body — the evidence panel already renders those rows. -1. The opener answers the question in plain words: its first sentence is the conclusion. Not the person's motives. -2. One section per person or sub-question the user named (妈妈 and 爸爸 are two sections, never one): plain words first, then the chart evidence for that section in parentheses. -3. ## ${REPORT_HEADING.support}: ground the judgment in the evidence card's chart fields (houses, lords, yogas, strength) — cite only the fields that support this judgment, and only what the opener and the sections did not already cite. A table or prose, not both. Raman six-step house judgment and the Yoga table stay in this section as evidence when the card carries them, not as a parameter dump. Cite, do not dump degrees tables. Omit the section when there is nothing new to cite. -4. ## ${REPORT_HEADING.timing}: current Dasha and transits when delivered. Phrase as 能量活跃度/概率, never an absolute date guarantee. Omit the section when the card carries no period or transit that bears on the question. -5. ## ${REPORT_HEADING.action}: One to three actions for this week, here and nowhere else. Direct, not footnote-soft. +1. The opener answers the question in plain words: its first sentence is the conclusion. Not the person's motives. No heading. +2. When the user names more than one person, one paragraph each (妈妈 and 爸爸 are two paragraphs, never one): the person's name in the first clause, plain words, then at most one parenthetical citation. +3. Chart evidence stays inside those parentheses. Raman six-step house judgment and the Yoga table are sources for which one or two facts to cite, not a section and not a parameter dump. Cite, do not dump degrees tables. +4. Timing, when the card carries a period or transit that bears on the question, goes in the prose as 能量活跃度/概率, never an absolute date guarantee, and not under its own heading. +5. An action is not required. Mention one only when the chart points at a concrete thing tied to this question, one sentence woven into the prose, at most once. When the user asks what to do, that turn may give several. Never a standing list. Address the reader as 你. Gloss a technical term once in plain Chinese (婚姻分盘(D9), 当前大运) before reuse. Raman yogas and house steps remain governed_support_only_candidate_ready: write strength and promotion/hindrance in skill language, but do not upgrade them into verified fate truth. @@ -97,4 +89,4 @@ Never: - Draw a conclusion from an empty house alone. Say where its lord sits and in what state instead. - Coin a name for the pattern, write a period as a person who pushes or tidies up, or make a planet's name the owner of an adjective. -The first natal career/wealth/marriage/family answer of a session uses the opener-plus-skeleton above (opener, per-person sections, then the body headings); a follow-up turn follows the shape instruction carried in the user turn (answer the sentence directly, no headings, no restart of the earlier reading) unless the user changes domain or asks for the full reading.`; +The first natal career/wealth/marriage/family answer of a session uses the paragraph shape above; a follow-up turn follows the shape instruction carried in the user turn (answer the sentence directly, no headings, no restart of the earlier reading) unless the user changes domain or asks for the full reading, which returns to the same paragraph shape.`; diff --git a/frontend/src/mastra/skill-binding.ts b/frontend/src/mastra/skill-binding.ts index 9a2e32db..7ce61f04 100644 --- a/frontend/src/mastra/skill-binding.ts +++ b/frontend/src/mastra/skill-binding.ts @@ -6,7 +6,7 @@ import { resolveLiveJyotishSkillRuntimePath, } from "../lib/skill-package-registry.ts"; import { sharedConsultationMethodMarkdown } from "../lib/consultation-methodology.ts"; -import { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING } from "../lib/consultation-thinking-plan.ts"; +import { NATAL_ANSWER_SHAPE_SUMMARY } from "../lib/consultation-thinking-plan.ts"; const skill = resolveLiveJyotishSkill(); @@ -112,7 +112,7 @@ function boundMethod(): string { export const BOUND_METHOD_MARKER = ``; -const NATAL_REPORT_SKELETON = `For career, wealth, marriage, family, parents and children answers, present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself. ${NATAL_ANSWER_SHAPE_SUMMARY} Raw structure, six-step houses and the Yoga table go under ## ${REPORT_HEADING.support}; the actions appear once; the Technique Audit Table stays on the evidence panel, and a follow-up turn follows the shape instruction in the user turn instead. `; +const NATAL_REPORT_SKELETON = `For career, wealth, marriage, family, parents and children answers, present its Level 2 report template as a source of evidence, not as the answer's shape. ${NATAL_ANSWER_SHAPE_SUMMARY} Six-step houses and the Yoga table are sources for the one or two facts cited in parentheses; they are not pasted under a heading. The Technique Audit Table stays on the evidence panel, and a follow-up turn follows the shape instruction in the user turn instead. `; function methodBlock(reportSkeleton: string) { return `The jyotish-vedic-astrology skill is already loaded. Its runtime method is quoted below from the live skill the operator maintains; there is no activation step, no hashed package, and no tool that loads it. Follow this method and its truth boundaries. ${reportSkeleton}Construction notes, CLI indexes, and case catalogs stay in the skill tree and are not part of this block. diff --git a/frontend/tests/consult-answer-the-question-20260927.test.ts b/frontend/tests/consult-answer-the-question-20260927.test.ts index 1e783df1..fea27101 100644 --- a/frontend/tests/consult-answer-the-question-20260927.test.ts +++ b/frontend/tests/consult-answer-the-question-20260927.test.ts @@ -106,8 +106,15 @@ test("the first turn of a session keeps the opener-plus-skeleton shape (control) assert.deepEqual(run.terminal.map((event) => event.type), ["run.completed"]); assert.equal(run.workflowRuns, 1); const writerPrompt = JSON.stringify(run.prompts[1]); - // ① The first-turn shape: three body headings, no 先回答 heading, actions once. - for (const heading of Object.values(REPORT_HEADING)) assert.ok(writerPrompt.includes(`## ${heading}`)); + // 原值: 写作提示含三个固定 ## 标题 + // 新值: 含「段首用人话点名」「行动不强制」「三到六段」,且不要求这些 ## 节名 + // 原因: TASK-consult-conversational-answer-20261006 D1–D3(BUG-1244) + assert.match(writerPrompt, /段首用人话点名/); + assert.match(writerPrompt, /行动不强制/); + assert.match(writerPrompt, /三到六段/); + for (const heading of Object.values(REPORT_HEADING)) { + assert.equal(writerPrompt.includes(`## ${heading}`), false, heading); + } assert.doesNotMatch(writerPrompt, /## 先回答你的问题/); assert.match(writerPrompt, /开场不写行动清单/); assert.doesNotMatch(writerPrompt, /这是追问轮/); diff --git a/frontend/tests/consult-single-pass-answer-20260927.test.ts b/frontend/tests/consult-single-pass-answer-20260927.test.ts index 68ade33e..25dd68e8 100644 --- a/frontend/tests/consult-single-pass-answer-20260927.test.ts +++ b/frontend/tests/consult-single-pass-answer-20260927.test.ts @@ -280,7 +280,15 @@ test("the model call that writes the answer has the calculation result in its pr test("the writing instructions travel with the user turn the loop sees", async () => { const run = await runNatal([calcCall(), { parts: pieces(ANSWER), finish: "stop" }]); const writerPrompt = JSON.stringify(run.prompts[1]); - for (const heading of Object.values(REPORT_HEADING)) assert.ok(writerPrompt.includes(`## ${heading}`)); + // 原值: 写作提示含三个固定 ## 标题 + // 新值: 含「段首用人话点名」「行动不强制」「三到六段」,且不要求这些 ## 节名 + // 原因: TASK-consult-conversational-answer-20261006 D1–D3(BUG-1244)。夹具正文仍带旧标题,只用来测续写与放出,不是新形状。 + assert.match(writerPrompt, /段首用人话点名/); + assert.match(writerPrompt, /行动不强制/); + assert.match(writerPrompt, /三到六段/); + for (const heading of Object.values(REPORT_HEADING)) { + assert.equal(writerPrompt.includes(`## ${heading}`), false, heading); + } assert.match(writerPrompt, /开场不要标题/); // The removed compose prompt claimed a finished calculation it never saw. assert.doesNotMatch(writerPrompt, /服务器计算已经完成。不要再调用排盘工具/); diff --git a/frontend/tests/consultation-run-timeline.test.ts b/frontend/tests/consultation-run-timeline.test.ts index 115295d4..45e71b4a 100644 --- a/frontend/tests/consultation-run-timeline.test.ts +++ b/frontend/tests/consultation-run-timeline.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { chartCalculationProgressLabel } from "../src/lib/consultation-activity-labels.ts"; +import type { ConsultationAgentPublicEvent } from "../src/lib/consultation-agent-events.ts"; import { generalConsultationThinkingPlan, natalConsultationThinkingPlan } from "../src/lib/consultation-thinking-plan.ts"; import { consultationTimelineFromSettled, @@ -157,6 +158,40 @@ test("a finished run leaves no outline row and nothing live", () => { assert.equal(state.rows.some((row) => row.kind === "think"), false); }); +test("a heading-free first turn uses the open writing row and settles as 组织回答 (BUG-1244)", () => { + const text = "你和妈妈这条线近,来往多。\n爸爸那边有距离的迹象。\n"; + const prelude: ConsultationAgentPublicEvent[] = [ + { type: "skill.started", name: "jyotish-vedic-astrology" }, + { type: "skill.completed", name: "jyotish-vedic-astrology" }, + { type: "tool.started", callId: "tool-1", tool: "run-jyotish-consultation", label: "正在计算本命盘…" }, + { type: "answer.delta", text }, + ]; + const streaming = reduceConsultationTimelineEvents(prelude); + const liveWrite = streaming.rows.filter((row) => row.kind === "write"); + assert.deepEqual(liveWrite.map((row) => row.id), ["write-open"]); + assert.equal(liveWrite[0]?.status, "live"); + assert.match(liveWrite[0]?.label ?? "", /正在组织回答/); + const settled = reduceConsultationTimelineEvents([ + ...prelude, + { + type: "run.completed", + receipt: { + runId: "run-1", + runtime: "mastra-agentic", + skill: { name: "jyotish-vedic-astrology", loaded: true, referenceReads: 0, methodologySections: 0 }, + steps: [], + workflow: { route: "parents", status: "ready", preciseTiming: "blocked", missingLayers: [] }, + }, + }, + ]); + const doneWrite = settled.rows.filter((row) => row.kind === "write"); + assert.deepEqual(doneWrite.map((row) => row.id), ["write-open"]); + assert.equal(doneWrite[0]?.status, "done"); + assert.equal(doneWrite[0]?.label, "组织回答"); + assert.equal(settled.rows.some((row) => (row.label ?? "").includes("这周可以做什么")), false); + assert.equal(settled.rows.every((row) => row.status === "done"), true); +}); + test("a stored natal answer rebuilds the same rows as live: no outline rows", () => { const plan = natalConsultationThinkingPlan({ domains: ["parents"] }); const rows = consultationTimelineFromSettled({ diff --git a/frontend/tests/consultation-smalltalk.test.ts b/frontend/tests/consultation-smalltalk.test.ts index 6049d493..bd7eeff6 100644 --- a/frontend/tests/consultation-smalltalk.test.ts +++ b/frontend/tests/consultation-smalltalk.test.ts @@ -232,10 +232,10 @@ test("route gates all special entries before a single selected-model classifier test("all three agent voices share the exemption, natal domain skeleton stays mandatory", () => { assert.match(productConversationVoice, /只是打招呼、道谢、告别时,回一句话,不要套开场形状/); assert.doesNotMatch(natalSpokenReportContract, /Short chit-chat/); - // 原值: /One natal career\/wealth\/marriage\/family question still uses the opener-plus-skeleton/ - // 新值: 首轮仍用开场 + 骨架;追问轮按用户轮里的指令直接回答(骨架对首轮仍是强制的) - // 原因: TASK-consult-answer-the-question-20260927 D3 / BUG-1071:追问轮不再重开整套骨架 - assert.match(natalSpokenReportContract, /The first natal career\/wealth\/marriage\/family answer of a session uses the opener-plus-skeleton above/); + // 原值: /The first natal career\/wealth\/marriage\/family answer of a session uses the opener-plus-skeleton above/ + // 新值: 首轮用段落形状;追问轮仍按用户轮里的指令直接回答 + // 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1244):首轮骨架取消,追问轮形状保留 + assert.match(natalSpokenReportContract, /The first natal career\/wealth\/marriage\/family answer of a session uses the paragraph shape above/); assert.match(natalSpokenReportContract, /a follow-up turn follows the shape instruction carried in the user turn/); const index = source("../src/mastra/index.ts"); assert.ok((index.match(/\$\{productConversationVoice\}/g) ?? []).length >= 3); diff --git a/frontend/tests/consultation-thinking-plan.test.ts b/frontend/tests/consultation-thinking-plan.test.ts index 4b54c855..cdcb56be 100644 --- a/frontend/tests/consultation-thinking-plan.test.ts +++ b/frontend/tests/consultation-thinking-plan.test.ts @@ -39,15 +39,15 @@ test("multi-domain thinking plan uses Chinese product labels and hides tool ids" mustUseLayers: ["D1", "run-jyotish-consultation", "D10", "skill_read"], }); const encoded = JSON.stringify(sections); - // 原值: 四节 heading = [question, support, timing, action] - // 新值: 开场节的 heading 与「盘里支持」共用(开场没有自己的标题,第一个正文 H2 出现即算写完);正文三节 - // 原因: TASK-consult-answer-the-question-20260927 D5/D8(BUG-1072/1073):删「先回答你的问题」节,开场节标题改「先回答你问的这件事」 + // 原值: 四节 heading = [support, support, timing, action],含「这周可以做什么」 + // 新值: 三节 heading = [support, support, timing],不再有 action 节 + // 原因: TASK-consult-conversational-answer-20261006 T3(BUG-1244):思考栏去掉「这周可以做什么」;heading 字段仍是 schema 必填,留给旧答案对上标题 assert.deepEqual(sections.map((section) => section.heading), [ REPORT_HEADING.support, REPORT_HEADING.support, REPORT_HEADING.timing, - REPORT_HEADING.action, ]); + assert.equal(JSON.stringify(sections).includes("这周可以做什么"), false); assert.equal(sections[0]?.title, OPENER_THINKING_TITLE); assert.equal(sections[0]?.title, "先回答你问的这件事"); assert.equal(JSON.stringify(sections).includes("先抓住你真正在问的事"), false); @@ -115,18 +115,29 @@ test("settled answers mark leftover thinking sections done", () => { test("progress marks the current heading active and completed ones done", () => { const sections = natalConsultationThinkingPlan({ domains: ["career"] }); - // 原值: 写出 `## 先回答你的问题` → 首节 done、第二节 active - // 新值: 口语开场还没有标题时首节 active;写出 `## 盘里支持` 后开场节与依据节都 done、时间节 active - // 原因: 开场没有自己的 H2,它与依据节共用标题(D8) + // 原值: 口语开场还没有标题时首节 active + // 新值: 整篇没有二级标题时没有任何一节是 active(避免首轮一直卡在转圈) + // 原因: TASK-consult-conversational-answer-20261006 T3(BUG-1244):首轮正文不写标题,进度改走结算时全部完成 const opening = applyThinkingSectionProgress(sections, "外松内紧。\n"); - assert.equal(opening[0]?.steps[0]?.status, "active"); - assert.equal(opening[1]?.steps[0]?.status, "pending"); + assert.equal(opening.some((section) => section.steps.some((step) => step.status === "active")), false); + assert.equal(opening[0]?.steps[0]?.status, "pending"); const progressed = applyThinkingSectionProgress(sections, `外松内紧。\n## ${REPORT_HEADING.support}\n事业宫被土星压着。\n`); assert.equal(progressed[0]?.steps[0]?.status, "done"); assert.equal(progressed[1]?.steps[0]?.status, "done"); assert.equal(progressed[2]?.steps[0]?.status, "active"); }); +test("a heading-free first turn stays unstuck while streaming and finishes every section when settled (BUG-1244)", () => { + const sections = natalConsultationThinkingPlan({ domains: ["career"] }); + const text = "你今年事业上机会不少,同时事情多、要求严。\n现在这十年偏向慢慢攒。\n"; + const streaming = applyThinkingSectionProgress(sections, text); + assert.equal(streaming.some((section) => section.steps.some((step) => step.status === "active")), false); + assert.equal(streaming.every((section) => section.steps.every((step) => step.status === "pending")), true); + const settled = applyThinkingSectionProgress(sections, text, { settled: true }); + assert.equal(settled.every((section) => section.steps.every((step) => step.status === "done")), true); + assert.equal(JSON.stringify(sections).includes("这周可以做什么"), false); +}); + test("the opener section closes at the first per-person heading and skipped optional headings close behind later ones (BUG-1132)", () => { const sections = natalConsultationThinkingPlan({ domains: ["parents"] }); const opener = "你和父母不疏远,但亲近主要落在为你的前途操心上。\n"; @@ -135,29 +146,46 @@ test("the opener section closes at the first per-person heading and skipped opti assert.equal(onPerson[1]?.steps[0]?.status, "active"); // 盘上依据 omitted: once 时间怎么看 shows, the support section is behind it. const onTiming = applyThinkingSectionProgress(sections, `${opener}## 你和妈妈\n上心。\n## ${REPORT_HEADING.timing}\n这十年。\n`); - assert.deepEqual(onTiming.map((section) => section.steps[0]?.status), ["done", "done", "done", "active"]); + // 原值: ["done", "done", "done", "active"](时间节之后还有行动节) + // 新值: ["done", "done", "done"](行动节已删,时间节是最后一节) + // 原因: TASK-consult-conversational-answer-20261006 T3(BUG-1244) + assert.deepEqual(onTiming.map((section) => section.steps[0]?.status), ["done", "done", "done"]); }); test("continue prompt asks to resume after the last complete heading", () => { const prompt = consultationContinuePrompt("## 事业\n方向稳定。\n"); - assert.match(prompt, /最后一个完整标题是:## 事业/); - // 续写的标题清单跟着正文三标题走,不再让续写补出「先回答你的问题」(D8)。 + // 原值: /最后一个完整标题是:## 事业/,并要求续写补三个固定二级标题 + // 新值: 认出已写出的最后一个二级标题,但要求改用段落续写,不再点名三个固定标题 + // 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1244):首轮不再有固定节名 + assert.match(prompt, /最后一个二级标题是:## 事业/); assert.doesNotMatch(prompt, /## 先回答你的问题、/); assert.match(prompt, /不要写「先回答你的问题」这个标题/); - assert.match(prompt, new RegExp(`## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}、## ${REPORT_HEADING.action}`)); + assert.doesNotMatch(prompt, new RegExp(`## ${REPORT_HEADING.support}、## ${REPORT_HEADING.timing}`)); + assert.match(prompt, /不要再补新的二级标题/); assert.match(prompt, /不要重复已写出的段落/); assert.match(prompt, /不要写思考过程清单/); }); -test("natal spoken answers must use markdown lists for parallel points", () => { - assert.match(consultationSpokenHeadingRule("natal"), /Markdown bullet lists/); +test("natal spoken answers default to paragraphs, not bold-label lists (BUG-1244)", () => { + // 原值: /Markdown bullet lists/(并列点必须是加粗标签列表) + // 新值: 默认写段落;只有用户要求列举或有三个以上时间窗才用列表 + // 原因: TASK-consult-conversational-answer-20261006 D5 + const natal = consultationSpokenHeadingRule("natal"); + assert.match(natal, /Write paragraphs/); + assert.match(natal, /three or more candidate time windows/); + assert.doesNotMatch(natal, /Markdown bullet lists/); + assert.match(consultationSpokenHeadingRule("daily"), /Markdown bullet lists/); + assert.doesNotMatch(consultationSpokenHeadingRule("window"), /start with ## /); }); test("think plan flattens natal sections into v2 steps", () => { const steps = thinkPlanSteps(natalConsultationThinkingPlan({ domains: ["career", "wealth"], })); - assert.deepEqual(steps.map((step) => step.id), ["question", "support", "timing", "action"]); + // 原值: ["question", "support", "timing", "action"] + // 新值: ["question", "support", "timing"] + // 原因: TASK-consult-conversational-answer-20261006 T3(BUG-1244):删掉「这周可以做什么」 + assert.deepEqual(steps.map((step) => step.id), ["question", "support", "timing"]); }); test("section prompt asks for one heading and forbids another calculation", () => { @@ -169,24 +197,28 @@ test("section prompt asks for one heading and forbids another calculation", () = const natal = natalAnswerShapeInstruction(); assert.match(natal, /只用结果里的盘面事实/); assert.match(natal, /开场不要标题/); - // 原值: 一次写完四个标题(含 ## 先回答你的问题) - // 新值: 三个标题;开场不写行动、行动只在最后一节出现一次、全文不超过 900 字、不写「先回答你的问题」 - // 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073 - for (const heading of Object.values(REPORT_HEADING)) assert.ok(natal.includes(`## ${heading}`)); + // 原值: 三个固定标题都写进首轮指令(## 盘上依据 / 时间怎么看 / 这周可以做的一件事) + // 新值: 首轮指令不要求任何 ## 节名;多人分段落、行动不强制、通常三到六段 + // 原因: TASK-consult-conversational-answer-20261006 D1–D3/D5(BUG-1244) + assert.doesNotMatch(natal, /## (盘上依据|时间怎么看|这周|你和妈妈)/); + assert.match(natal, /不写 ##/); + assert.match(natal, /段首用人话点名/); + assert.match(natal, /行动不强制/); + assert.match(natal, /三到六段/); assert.equal(Object.values(REPORT_HEADING).length, 3); - // 原值: assert.match(natal, /这三个二级标题/) - // 新值: 先按对象分段,再「依次写」三个固定标题(前两个可省) - // 原因: TASK-consult-plain-answer-20261001 D2(BUG-1132):问到几个人就分几段,固定标题跟在后面 + // 原值: assert.match(natal, /## 你和妈妈、## 你和爸爸、## 他们怎么对你/) 与 /然后依次写:/ + // 新值: 段首点名,不写二级标题;仍要求父母分开写 + // 原因: 同上 D1:按对象的二级标题一并取消 assert.match(natal, /问题里点到几个人或几个子问题/); - assert.match(natal, /## 你和妈妈、## 你和爸爸、## 他们怎么对你/); + assert.match(natal, /「你妈妈这边……」「爸爸那边……」/); assert.match(natal, /问父母时妈妈和爸爸分开写/); - assert.match(natal, /然后依次写:/); + assert.doesNotMatch(natal, /然后依次写:/); assert.match(natal, /开场不写行动清单/); - assert.match(natal, /只在这里出现一次/); - // 原值: assert.match(natal, /全文不超过 900 字/) - // 新值: 不限字数、讲清楚为准,且不得出现任何「不超过 N 字」 - // 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1073 的 900 字) - assert.match(natal, /不限字数,讲清楚为准;同一件事只说一遍/); + assert.match(natal, /全篇最多一处/); + // 原值: assert.match(natal, /不限字数,讲清楚为准;同一件事只说一遍/) + // 新值: 首轮通常三到六段、不设硬截断;仍不得出现「不超过 N 字」 + // 原因: TASK-consult-conversational-answer-20261006 D3:推翻 10-01「首轮不限字数」 + assert.match(natal, /不设硬截断。同一件事只说一遍/); assert.doesNotMatch(natal, /不超过 \d+ 字/); assert.match(natal, /不要写「先回答你的问题」这个标题/); // 原值: assert.match(natal, /反差只写盘上的结构/) @@ -294,7 +326,10 @@ test("a follow-up turn answers directly and the thinking bar says so (TASK-consu const plan = natalConsultationThinkingPlan({ domains: ["parents"], followUp: true }); assert.equal(plan[0]?.title, OPENER_THINKING_TITLE); assert.equal(plan[0]?.steps[0]?.label, "直接回答这句话"); - assert.deepEqual(thinkPlanSteps(plan).map((step) => step.id), ["question", "support", "timing", "action"]); + // 原值: ["question", "support", "timing", "action"] + // 新值: ["question", "support", "timing"] + // 原因: TASK-consult-conversational-answer-20261006 T3(BUG-1244) + assert.deepEqual(thinkPlanSteps(plan).map((step) => step.id), ["question", "support", "timing"]); const settled = applyThinkingSectionProgress(plan, "不是。4 宫主落 10 宫,她把心力投在你的前途上。", { settled: true }); assert.equal(settled.every((section) => section.steps.every((step) => step.status === "done")), true); }); diff --git a/frontend/tests/consultation-voice-contract.test.ts b/frontend/tests/consultation-voice-contract.test.ts index a1b2240a..2970f1f8 100644 --- a/frontend/tests/consultation-voice-contract.test.ts +++ b/frontend/tests/consultation-voice-contract.test.ts @@ -38,18 +38,23 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton" // 新值(2): 两句都不得再出现;改锁「不设字数上限,讲清楚为准」 // 原因(2): TASK-consult-plain-answer-20261001 D1/D8:扮演象修辞下线,产品取消字数上限 assert.doesNotMatch(voice, /扮演|≤ 400|Spoken-layer total/); - assert.match(voice, /不设字数上限,讲清楚为准/); - assert.match(voice, /骨架不可省略,但必须以直接回应开场/); + // 原值: assert.match(voice, /不设字数上限,讲清楚为准/) 与 /骨架不可省略,但必须以直接回应开场/ + // 新值: 首轮通常三到六段、不设硬截断;合同里不再有「骨架不可省略」 + // 原因: TASK-consult-conversational-answer-20261006 D1/D3(BUG-1244) + assert.match(voice, /首轮通常三到六段/); + assert.match(voice, /不设硬截断/); + assert.doesNotMatch(voice, /骨架不可省略/); assert.match(mastra, /productConversationVoice/); assert.match(mastra, /natalSpokenReportContract/); assert.match(mastra, /skill Level 2 report skeleton/); assert.match(mastra, /Do not paste the Technique Audit Table into the answer body/); assert.match(binding, /Follow this method and its truth boundaries/); - // 原值: assert.match(binding, /present its Level 2 report template in the chat body after a 3-6 sentence spoken reply/) - // 新值: Level 2 模板是回答形状的依据层,形状引用 NATAL_ANSWER_SHAPE_SUMMARY - // 原因: TASK-consult-plain-answer-20261001 D2/D7:先答、按对象分段,Level 2 内容落在「盘上依据」 - assert.match(binding, /present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/); + // 原值: assert.match(binding, /present its Level 2 report template as the evidence of the product's answer shape, not as the shape itself/) + // 新值: Level 2 模板只是依据来源,六步宫位和 Yoga 表不贴进正文标题下 + // 原因: TASK-consult-conversational-answer-20261006 D1/D6(BUG-1244) + assert.match(binding, /present its Level 2 report template as a source of evidence, not as the answer's shape\. \$\{NATAL_ANSWER_SHAPE_SUMMARY\}/); + assert.match(binding, /they are not pasted under a heading/); assert.match(mastra, /The bound skill method is this product's answering contract/); assert.match(mastra, /Do not use a restricted technique/); // 原值: assert.match(mastra, /must_use_layers is the executed shortlist/) @@ -64,11 +69,11 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton" // 新值: 正文只有三个标题,「先回答你的问题」不再出现在合同里;开场不带行动、行动只在最后一节、首轮 ≤ 900 字 // 原因: TASK-consult-answer-the-question-20260927 D8 / BUG-1073:开场就是回答,同一判断不再说三遍 assert.doesNotMatch(voice, /## 先回答你的问题/); - // 原值: assert.match(voice, /## 盘里支持这个判断的地方, then ## 时间怎么看, then ## 这周可以做的一件事/) - // 新值: 按对象分段之后才是三个固定标题,标题取自 REPORT_HEADING(「盘里支持这个判断的地方」改名「盘上依据」),前两个可省 - // 原因: TASK-consult-plain-answer-20261001 D2/D7(BUG-1132) - assert.match(voice, /One section per person or sub-question the user named \(妈妈 and 爸爸 are two sections, never one\)/); - assert.match(voice, /## \$\{REPORT_HEADING\.support\}[\s\S]*Omit the section when there is nothing new to cite[\s\S]*## \$\{REPORT_HEADING\.timing\}[\s\S]*## \$\{REPORT_HEADING\.action\}/); + // 原值: assert.match(voice, /## ${REPORT_HEADING.support}…## ${REPORT_HEADING.timing}…## ${REPORT_HEADING.action}/) + // 新值: 多人各写一段、段首点名;合同不再要求三个固定 ## 标题 + // 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1244) + assert.match(voice, /When the user names more than one person, one paragraph each \(妈妈 and 爸爸 are two paragraphs, never one\)/); + assert.doesNotMatch(voice, /## \$\{REPORT_HEADING/); // 原值: assert.match(voice, /The opener carries no action list/) // 新值: 「开场不写行动」写在共享 ANSWER SHAPE 第 1 步与形状摘要里 // 原因: TASK-consult-plain-answer-20261001 D2:合同不再复述开场细节,引用唯一定义 @@ -78,12 +83,19 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton" // 新值: 没有字数上限,写到讲清楚为止 // 原因: TASK-consult-plain-answer-20261001 D8:产品取消字数上限(推翻 BUG-1073 的 900 字,保留「只说一遍」) assert.doesNotMatch(voice, /900/); - assert.match(voice, /There is no length cap: write until it is clear, and stop there/); - // 原值: /cite only the fields that support this judgment, and only what the opener did not already say/ - // 新值: 「只写开场与对象段都没引过的」 - // 原因: TASK-consult-plain-answer-20261001 D2:对象段已经在括号里给过依据 - assert.match(voice, /cite only the fields that support this judgment, and only what the opener and the sections did not already cite/); - assert.match(voice, /One to three actions for this week, here and nowhere else/); + // 原值: assert.match(voice, /There is no length cap: write until it is clear, and stop there/) + // 新值: 首轮通常三到六段,不设硬截断 + // 原因: TASK-consult-conversational-answer-20261006 D3(BUG-1244) + assert.match(voice, /The first answer is usually three to six paragraphs/); + assert.match(voice, /There is no hard cutoff/); + // 原值: /cite only the fields that support this judgment, and only what the opener and the sections did not already cite/ + // 新值: 依据留在括号里,六步宫位和 Yoga 表是来源,不是一节 + // 原因: TASK-consult-conversational-answer-20261006 D6(BUG-1244) + assert.match(voice, /Raman six-step house judgment and the Yoga table are sources/); + // 原值: assert.match(voice, /One to three actions for this week, here and nowhere else/) + // 新值: 行动不强制,盘上指向具体事情时才顺口一句 + // 原因: TASK-consult-conversational-answer-20261006 D2(BUG-1244) + assert.match(voice, /An action is not required/); assert.doesNotMatch(voice, /One natal career\/wealth\/marriage\/family question still uses the opener-plus-skeleton above/); assert.match(voice, /a follow-up turn follows the shape instruction carried in the user turn/); // 原值: assert.match(mastra, /each thing said once, first-turn total ≤ 900 characters/) @@ -96,10 +108,10 @@ test("product voice uses a spoken opener then the skill Level 2 report skeleton" // 新值: 同一句改由引用的 NATAL_ANSWER_SHAPE_SUMMARY 携带(见下方 shape-summary 测试) // 原因: TASK-consult-plain-answer-20261001 D7:形状只定义一处 assert.doesNotMatch(binding, /反差|扮演|≤ 400/); - // 原值: assert.match(binding, /the actions, once/) - // 新值: assert.match(binding, /the actions appear once/) - // 原因: TASK-consult-plain-answer-20261001 D7:骨架句改写,语义不变 - assert.match(binding, /the actions appear once/); + // 原值: assert.match(binding, /the actions appear once/) + // 新值: 骨架句不再要求行动出现一次;行动是否写由形状摘要决定 + // 原因: TASK-consult-conversational-answer-20261006 D2(BUG-1244) + assert.doesNotMatch(binding, /the actions appear once/); assert.match(mastra, /consultationSpokenHeadingRule\("natal"\)/); assert.match(workflow, /template: "skill_level_2"/); assert.match(workflow, /raman_six_step/); @@ -156,17 +168,29 @@ test("consult user turn no longer dumps tool JSON that the model would parrot", test("the answer is a shape — conclusion first, one section per person, plain words checked, no length cap (BUG-1132)", () => { assert.match(voice, /ANSWER SHAPE \(natal \/ general \/ declared-window 三种模式共用\)/); assert.doesNotMatch(voice, /OPENER SHAPE|表面 A,底下 B|谁在推|命名成一个格局|外松内紧/); - assert.match(voice, /不设字数上限,讲清楚为准。同一件事只说一遍/); + // 原值: assert.match(voice, /不设字数上限,讲清楚为准。同一件事只说一遍/) + // 新值: 首轮三到六段、不设硬截断;追问轮仍讲清楚为准;同一件事只说一遍 + // 原因: TASK-consult-conversational-answer-20261006 D3(BUG-1244) + assert.match(voice, /首轮通常三到六段,讲清结论、最关键的一两条依据,相关时带上时间,细节等追问;不设硬截断。追问轮讲清楚为准。同一件事只说一遍/); assert.match(voice, /开场一到三句人话,第一句就是结论/); - assert.match(voice, /按对象分段:问题里点到几个对象/); + // 原值: /按对象分段:问题里点到几个对象/ + // 新值: /按对象分段落:问题里点到几个对象/,段首点名、不写二级标题 + // 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1244) + assert.match(voice, /按对象分段落:问题里点到几个对象/); + assert.match(voice, /段首用人话点名/); assert.match(voice, /问「父母」时妈妈和爸爸分开写,不揉成一段。只有一个对象、一个子问题时不拆段/); assert.match(voice, /然后在括号里给出撑住这段判断的盘上依据/); + assert.match(voice, /每段最多一处括号/); assert.match(voice, /人话自检:把每句话括号里的内容删掉,一个完全不懂占星的人仍然能看懂它在说什么/); for (const banned of ["自造格局名、四字概括", "把大运、行运写成拟人的角色", "把星名当形容词的主人", "括号外每段最多一个占星术语"]) { assert.ok(voice.includes(banned), banned); } assert.match(voice, /推断边界:空宫不单独下结论——要说这个宫,就说它的宫主落在哪、什么状态/); - assert.match(voice, /一到三条破折号短句/); + // 原值: assert.match(voice, /一到三条破折号短句/) + // 新值: 行动不强制 + // 原因: TASK-consult-conversational-answer-20261006 D2(BUG-1244) + assert.match(voice, /行动不强制:只有盘上确实指向一件和问题相关的具体事情时/); + assert.doesNotMatch(voice, /一到三条破折号短句/); // 降级路线:申报时段与无出生分钟都不编月份;形状照给。 assert.match(voice, /申报时段(declared birth window):依据只取窗口内稳定的那一层[\s\S]*?也不写月份。/); assert.match(voice, /无出生分钟(no birth minute):时间只讲公开日历这一天的大势,不点个人大运,不写月份/); @@ -177,14 +201,18 @@ test("one shape definition: contract, thinking plan, user turn and agent prompts const { NATAL_ANSWER_SHAPE_SUMMARY, REPORT_HEADING, consultationSpokenHeadingRule, natalAnswerShapeBody } = await import("../src/lib/consultation-thinking-plan.ts"); const { natalSpokenReportContract } = await import("../src/mastra/product-voice.ts"); assert.equal(REPORT_HEADING.support, "盘上依据"); + // 原值: 形状摘要、合同、首轮正文都含 `## ${heading}` + // 新值: 三处都引用同一份无标题摘要,且不要求这些 ## 节名 + // 原因: TASK-consult-conversational-answer-20261006 D1/D7(BUG-1244) for (const heading of Object.values(REPORT_HEADING)) { - assert.ok(NATAL_ANSWER_SHAPE_SUMMARY.includes(`## ${heading}`), heading); - assert.ok(natalSpokenReportContract.includes(`## ${heading}`), heading); - assert.ok(natalAnswerShapeBody().includes(`## ${heading}`), heading); + assert.equal(NATAL_ANSWER_SHAPE_SUMMARY.includes(`## ${heading}`), false, heading); + assert.equal(natalSpokenReportContract.includes(`## ${heading}`), false, heading); + assert.equal(natalAnswerShapeBody().includes(`## ${heading}`), false, heading); } assert.ok(natalSpokenReportContract.includes(NATAL_ANSWER_SHAPE_SUMMARY)); assert.ok(consultationSpokenHeadingRule("natal").includes(NATAL_ANSWER_SHAPE_SUMMARY)); - assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /There is no length cap/); + assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /usually three to six paragraphs/); + assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /no hard cutoff/); assert.match(NATAL_ANSWER_SHAPE_SUMMARY, /no conclusion from an empty house alone/); for (const text of [NATAL_ANSWER_SHAPE_SUMMARY, natalSpokenReportContract, natalAnswerShapeBody(), consultationSpokenHeadingRule("natal")]) { assert.doesNotMatch(text, /扮演|谁在推|命名成一个格局|外松内紧|≤ 400|≤ 900|不超过 \d+ 字/); @@ -220,14 +248,20 @@ test("the pinned Good examples split by person and stay plain; the coined-patter // 原值: indexOf("Good (父母题首轮,按对象分段;盘上数据是示意)") / indexOf("Good (单对象事业题首轮,不拆段;盘上数据是示意)") // 新值: 两个标签改为「括号里是依据的占位,只演示口气与结构,不是读法」,事业例再加「不预设读者在上班、做哪类工作」 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D3/D7(BUG-1165/1168):示例不得携带读法规则与职业形态 - const parentsStart = voice.indexOf("Good (父母题首轮,按对象分段;括号里是依据的占位,只演示口气与结构,不是读法)"); + // 原值: indexOf("Good (父母题首轮,按对象分段;…)") + // 新值: 标签改为「分段落、段首点名」 + // 原因: TASK-consult-conversational-answer-20261006 D1/D4(BUG-1245) + const parentsStart = voice.indexOf("Good (父母题首轮,分段落、段首点名;括号里是依据的占位,只演示口气与结构,不是读法)"); const careerStart = voice.indexOf("Good (单对象事业题首轮,不拆段;括号里是依据的占位,只演示口气与结构,不是读法;不预设读者在上班、做哪类工作)"); const badStart = voice.indexOf("Bad (多对象题,改写示意)"); assert.ok(parentsStart > -1 && careerStart > parentsStart && badStart > careerStart); const parents = voice.slice(parentsStart, careerStart); - for (const heading of ["## 你和妈妈", "## 你和爸爸", "## 他们怎么对你", "## 时间怎么看", "## 这周可以做的一件事"]) { - assert.ok(parents.includes(heading), heading); - } + // 原值: 父母示范含 ## 你和妈妈 / 你和爸爸 / 他们怎么对你 / 时间怎么看 / 这周可以做的一件事 + // 新值: 段首点名,示范里没有任何 ## + // 原因: TASK-consult-conversational-answer-20261006 D1/D4(BUG-1245) + assert.match(parents, /你妈妈这边/); + assert.match(parents, /爸爸那边/); + assert.doesNotMatch(parents, /^## /m); // 原值: assert.match(parents, /(4 宫母亲宫的宫主水星落在 10 宫事业宫;代表母亲的月亮也在 10 宫)/) // 新值: 括号只放占位(4 宫主落 10 宫;月亮在 10 宫);爸爸段演示「受冲先说、用可能、段末请确认」;不再有「分量重」「问爸爸的意见」 // 原因: TASK-consult-no-presupposition-and-backtest-20261001 D1/D3(BUG-1164/1165):旧例预设父亲在场,回测里缺席的父亲被照抄成「话少、分量重」 @@ -242,7 +276,11 @@ test("the pinned Good examples split by person and stay plain; the coined-patter assert.doesNotMatch(parents, /分量重|标杆|爸爸的意见|靠做事/); const career = voice.slice(careerStart, badStart); assert.doesNotMatch(career, /## 你和/); - assert.match(career, /## 盘上依据/); + // 原值: assert.match(career, /## 盘上依据/) + // 新值: 单对象示范也是段落,不写 ## 盘上依据 + // 原因: TASK-consult-conversational-answer-20261006 D1(BUG-1245) + assert.doesNotMatch(career, /^## /m); + assert.match(career, /(木星行运 10 宫;土星行运 6 宫)/); for (const good of [parents, career]) { assert.doesNotMatch(good, /一切都会好|相信自己|加油|宇宙自有安排|扮演|格局/); // No coined 外X内Y pattern names and no empty-house verdicts in the pinned examples. @@ -277,12 +315,12 @@ test("the voice answers the sentence asked: no motive guessing, yes/no first, co // 新值(2): 反差形状下线;「问别人时写宫位与代表星,不写心思」移到对象段规则里 // 原因(2): TASK-consult-plain-answer-20261001 D1(BUG-1070 的目的保留) assert.match(voice, /问的是别人时,写那个人在你盘上对应的宫位与代表星说明了什么,不写那个人的心思/); - // Actions stay in the opener only where there is no body skeleton. - // 原值(2): /本命解读里这三条不写在开场,只写在正文最后一节「这周可以做的一件事」/、/…行动就写在开场结尾/ - // 新值(2): /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/、/…行动就写在回答结尾/ - // 原因(2): TASK-consult-plain-answer-20261001 D2:开场之后还有对象段,「开场结尾」改「回答结尾」 - assert.match(voice, /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/); - assert.match(voice, /无出生分钟与申报时段没有正文骨架,行动就写在回答结尾/); + // 原值: /本命解读里只写在最后一节「这周可以做的一件事」,出现一次/、/行动就写在回答结尾/ + // 新值: 行动不强制,无出生分钟与申报时段也不交固定清单 + // 原因: TASK-consult-conversational-answer-20261006 D2(BUG-1244) + assert.match(voice, /行动不强制:只有盘上确实指向一件和问题相关的具体事情时/); + assert.match(voice, /无出生分钟与申报时段同样不交固定行动清单/); + assert.doesNotMatch(voice, /这周可以做的一件事/); // Yes/no Good and Bad pair, fictional chart. assert.match(voice, /Good \(是非题,用户问「我妈是不是不太在意我」/); // 原值: assert.match(voice, /不是。4 宫主水星逆行落 10 宫/) @@ -304,6 +342,27 @@ test("the voice answers the sentence asked: no motive guessing, yes/no first, co assert.match(voice, /Draw a conclusion from an empty house alone\. Say where its lord sits and in what state instead/); }); +test("pinned examples carry no copyable action and no weekly heading (BUG-1245)", () => { + const banned = [ + "口头答应的事,补一份书面确认", + "身体的小毛病这周就去看", + "给妈妈打电话时先问她最近怎样", + ]; + for (const phrase of banned) assert.equal(voice.includes(phrase), false, phrase); + assert.equal(voice.includes("## 这周"), false); + assert.match(voice, /Bad \(汇报体,只示意结构\)/); + const goodParents = voice.slice( + voice.indexOf("Good (父母题首轮"), + voice.indexOf("Good (单对象事业题首轮"), + ); + const goodCareer = voice.slice( + voice.indexOf("Good (单对象事业题首轮"), + voice.indexOf("Bad (多对象题"), + ); + assert.doesNotMatch(`${goodParents}\n${goodCareer}`, /^## /m); + assert.doesNotMatch(`${goodParents}\n${goodCareer}`, /^- /m); +}); + test("the voice pins Chinese-only terms and one spelling per thing (BUG-1148)", () => { assert.match(voice, /名词只用中文,同一个东西全篇一个叫法/); for (const term of ["克图", "凯图", "中运", "sade sati", "vargottama", "rupas"]) {