import { z } from "zod"; import { consultationDomainDefinition, type ConsultationDomain, } from "./consultation-domain-registry.ts"; export const THINKING_STEP_STATUSES = ["pending", "active", "done"] as const; export type ThinkingStepStatus = (typeof THINKING_STEP_STATUSES)[number]; export const VISIBLE_THINKING_STEP_LIMIT = 4; export const REPORT_HEADING = { foundation: "统一参数与原始结构", audit: "技法审计表", wrap: "现代生活", } as const; const BLOCK_STEP_LABELS: Readonly> = { raw_structure: "列出岁差、上升与宫位结构", raman_six_step: "按六步宫位判断问题宫", yoga_table: "核对应 Yoga 的成立与落空", timing: "对照当前大运与行运", synthesis: "综合强弱与下一步", technique_audit_table: "贴上技法审计表", modern_wrap: "用现代生活语言收口", }; const TOOLISH_STEP_RE = /(?:rectification|run-jyotish)-[a-z0-9-]+|skill_read|proposedKind|validationErrors/i; export const publicThinkingStepSchema = z.object({ id: z.string().min(1).max(80), label: z.string().min(1).max(80), status: z.enum(THINKING_STEP_STATUSES), }).strict(); export const publicThinkingSectionSchema = z.object({ id: z.string().min(1).max(80), title: z.string().min(1).max(120), heading: z.string().min(1).max(80), steps: z.array(publicThinkingStepSchema).min(1).max(12), }).strict(); export type PublicThinkingStep = z.infer; export type PublicThinkingSection = z.infer; function step(id: string, label: string, status: ThinkingStepStatus = "pending"): PublicThinkingStep | null { const cleaned = label.replace(/\s+/g, " ").trim(); if (!cleaned || TOOLISH_STEP_RE.test(cleaned)) return null; return { id, label: cleaned.slice(0, 80), status }; } function uniqueSteps(steps: ReadonlyArray): PublicThinkingStep[] { const seen = new Set(); const kept: PublicThinkingStep[] = []; for (const item of steps) { if (!item || seen.has(item.label)) continue; seen.add(item.label); kept.push(item); if (kept.length >= 12) break; } return kept; } function layerSteps(layers: readonly string[] | undefined, prefix: string): Array { return (layers ?? []).slice(0, 6).map((layer, index) => ( step(`${prefix}-${index}`, `对照 ${layer.replace(/[_]/g, " ").trim()}`) )); } export function consultationReportHeadings(domains: readonly ConsultationDomain[]): string[] { return [ REPORT_HEADING.foundation, ...domains.map((domain) => consultationDomainDefinition(domain).label), REPORT_HEADING.audit, REPORT_HEADING.wrap, ]; } export function consultationComposeHeadingGroups( plan: readonly PublicThinkingSection[], ): Array<{ section: PublicThinkingSection; headings: readonly string[] }> { return plan.map((section) => ( section.id === "close" ? { section, headings: [REPORT_HEADING.audit, REPORT_HEADING.wrap] } : { section, headings: [section.heading] } )); } export function natalConsultationThinkingPlan(input: { domains: readonly ConsultationDomain[]; requiredBlocks?: readonly string[]; mustUseLayers?: readonly string[]; }): PublicThinkingSection[] { const domains = input.domains.slice(0, 6); const blocks = new Set(input.requiredBlocks ?? Object.keys(BLOCK_STEP_LABELS)); const foundationSteps = uniqueSteps([ step("foundation-raw", BLOCK_STEP_LABELS.raw_structure ?? "列出岁差、上升与宫位结构"), ...layerSteps(input.mustUseLayers, "foundation-layer"), ]); const domainBlocks = ["raman_six_step", "yoga_table", "timing", "synthesis"] .filter((block) => blocks.has(block)); const closeSteps = uniqueSteps([ blocks.has("technique_audit_table") ? step("close-audit", BLOCK_STEP_LABELS.technique_audit_table ?? "贴上技法审计表") : null, blocks.has("modern_wrap") ? step("close-wrap", BLOCK_STEP_LABELS.modern_wrap ?? "用现代生活语言收口") : null, ]); const sections: PublicThinkingSection[] = [ publicThinkingSectionSchema.parse({ id: "foundation", title: "先整理本盘的统一参数", heading: REPORT_HEADING.foundation, steps: foundationSteps.length > 0 ? foundationSteps : [{ id: "foundation-raw", label: "列出岁差、上升与宫位结构", status: "pending" }], }), ]; for (const domain of domains) { const definition = consultationDomainDefinition(domain); const steps = uniqueSteps([ ...domainBlocks.map((block) => step(`${domain}-${block}`, BLOCK_STEP_LABELS[block] ?? block)), ...definition.evidencePreview.slice(0, 6).map((item, index) => ( step(`${domain}-preview-${index}`, `对照 ${item}`) )), ]); sections.push(publicThinkingSectionSchema.parse({ id: `domain-${domain}`, title: `接下来分析你的${definition.label}`, heading: definition.label, steps: steps.length > 0 ? steps : [{ id: `${domain}-read`, label: `整理${definition.label}相关宫位`, status: "pending" }], })); } sections.push(publicThinkingSectionSchema.parse({ id: "close", title: "用审计表收口后再落到生活", heading: REPORT_HEADING.audit, steps: closeSteps.length > 0 ? closeSteps : [ { id: "close-audit", label: "贴上技法审计表", status: "pending" }, { id: "close-wrap", label: "用现代生活语言收口", status: "pending" }, ], })); return sections; } export function generalConsultationThinkingPlan(): PublicThinkingSection[] { return [publicThinkingSectionSchema.parse({ id: "answer", title: "接下来组织这轮回答", heading: "回答", steps: [ { id: "method", label: "读取分析方法", status: "done" }, { id: "compose", label: "组织回答", status: "pending" }, ], })]; } export function windowConsultationThinkingPlan(): PublicThinkingSection[] { return [publicThinkingSectionSchema.parse({ id: "window", title: "接下来根据声明窗口整理稳定层", heading: REPORT_HEADING.foundation, steps: [ { id: "compare", label: "比较声明窗口内的稳定层", status: "pending" }, { id: "boundary", label: "核对应答边界", status: "pending" }, { id: "compose", label: "组织回答", status: "pending" }, ], })]; } export function applyThinkingSectionProgress( sections: readonly PublicThinkingSection[], answerText: string, options?: { settled?: boolean }, ): PublicThinkingSection[] { if (sections.length === 0) return []; if (options?.settled && answerText.trim()) { return sections.map((section) => ({ ...section, steps: section.steps.map((item) => ({ ...item, status: "done" as const })), })); } const present = new Set( [...answerText.matchAll(/^##\s+(.+?)\s*$/gm)].map((match) => match[1]?.trim() ?? ""), ); let activeAssigned = false; return sections.map((section) => { const headingPresent = present.has(section.heading); let status: ThinkingStepStatus = "pending"; if (headingPresent) status = "done"; else if (!activeAssigned) { status = "active"; activeAssigned = true; } return { ...section, steps: section.steps.map((item) => ({ ...item, status: status === "done" ? "done" : status === "active" && item.status === "done" ? "done" : status, })), }; }); } export function splitAnswerByHeadings( text: string, headings: readonly string[], ): { preamble: string; slices: Record } { const slices: Record = {}; for (const heading of headings) slices[heading] = ""; if (!text.trim()) return { preamble: "", slices }; const parts = text.split(/(?=^## )/m); let preamble = ""; for (const part of parts) { const heading = /^##\s+(.+?)\s*$/m.exec(part)?.[1]?.trim(); if (!heading) { preamble += part; continue; } if (heading in slices) slices[heading] += part; else preamble += part; } return { preamble: preamble.trim(), slices }; } export function visibleThinkingSteps(steps: readonly PublicThinkingStep[]): { visible: PublicThinkingStep[]; hiddenCount: number; } { if (steps.length <= VISIBLE_THINKING_STEP_LIMIT) { return { visible: [...steps], hiddenCount: 0 }; } return { visible: steps.slice(0, VISIBLE_THINKING_STEP_LIMIT), hiddenCount: steps.length - VISIBLE_THINKING_STEP_LIMIT, }; } export function consultationSpokenHeadingRule(kind: "natal" | "general" | "window"): string { const secrets = "Never put tool names, error codes, parameters, internal IDs, scores, or secrets in the body."; const activity = "Do not invent a thinking-process checklist. Activity, progress, and receipts are server-owned."; if (kind === "natal") { return [ `Write the spoken answer with these exact Markdown H2 headings in order: ## ${REPORT_HEADING.foundation}, then ## {the Chinese label of each executed domain in tool order, such as 事业 / 财富 / 关系}, then ## ${REPORT_HEADING.audit}, then ## ${REPORT_HEADING.wrap}.`, "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.", activity, secrets, ].join(" "); } if (kind === "window") { return [ `When describing stable window structure, start with ## ${REPORT_HEADING.foundation}.`, activity, secrets, ].join(" "); } return `${activity} ${secrets}`; } export function upsertThinkingSection( sections: readonly PublicThinkingSection[], next: PublicThinkingSection, ): PublicThinkingSection[] { const parsed = publicThinkingSectionSchema.parse(next); const index = sections.findIndex((section) => section.id === parsed.id); if (index < 0) return [...sections, parsed]; return sections.map((section, current) => (current === index ? parsed : section)); } export function parsePublicThinkingSections(value: unknown): PublicThinkingSection[] { if (!Array.isArray(value)) return []; return value.flatMap((item) => { const parsed = publicThinkingSectionSchema.safeParse(item); return parsed.success ? [parsed.data] : []; }); } 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.foundation}、各已执行领域的中文名、## ${REPORT_HEADING.audit}、## ${REPORT_HEADING.wrap}。`, "已写出的末尾摘录:", output.slice(-800), ].join("\n"); } export function consultationSectionPrompt(heading: string, priorOutput: string): string { const title = heading.trim() || REPORT_HEADING.foundation; return [ "服务器计算已经完成。不要再调用排盘工具,不要重算,不要读取其他二级标题。", `只写这一个二级标题及其正文:## ${title}`, "不要写其他 ## 标题,不要复述已经写出的段落,不要写思考过程清单。", priorOutput.trim() ? `已经写出的上文(冻结,勿重复):\n${priorOutput.slice(-4000)}` : "这是正文的第一节。", ].join("\n"); }