merge: synchronize remote main
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
export type ReplyTheme = "career" | "marriage" | "timing" | "general";
|
||||
export type ReplyTheme = "career" | "marriage" | "wealth" | "timing" | "general";
|
||||
|
||||
const fallbackSuggestions: Record<ReplyTheme, readonly [string, string, string]> = {
|
||||
career: ["我更适合怎样的职业路径?", "未来一年事业上要避开什么?", "我该如何发挥自己的优势?"],
|
||||
marriage: ["我在关系里容易重复什么模式?", "怎样的伴侣更适合我?", "未来一年关系上要注意什么?"],
|
||||
wealth: ["我的财富增长方式是什么?", "接下来财务上要避开什么?", "我该如何稳定提升收入?"],
|
||||
timing: ["接下来最值得把握的阶段是什么?", "哪些时期更适合主动行动?", "我现在应该优先准备什么?"],
|
||||
general: ["未来一年,事业和收入该关注什么?", "我的关系模式是什么?", "未来哪些阶段值得把握?"],
|
||||
};
|
||||
|
||||
@@ -5,38 +5,10 @@ type CandidateCompletionRequest = {
|
||||
readonly time: string;
|
||||
};
|
||||
|
||||
function record(value: unknown): Record<string, unknown> | null {
|
||||
return value !== null && typeof value === "object"
|
||||
? value as Record<string, unknown>
|
||||
: null;
|
||||
}
|
||||
|
||||
/** Direct adoption was superseded by versioned high-confidence confirmation. */
|
||||
export function candidateWorkingTime(
|
||||
stored: unknown,
|
||||
request: CandidateCompletionRequest,
|
||||
_stored: unknown,
|
||||
_request: CandidateCompletionRequest,
|
||||
): string | null {
|
||||
const assessment = record(stored);
|
||||
const candidate = record(assessment?.candidate_result);
|
||||
const winner = record(candidate?.winningSegment);
|
||||
const turn = record(assessment?.turn_state);
|
||||
const action = record(turn?.nextAction);
|
||||
const actionKind = action?.kind;
|
||||
const terminal = actionKind === "present_low_result"
|
||||
|| actionKind === "present_medium_result"
|
||||
|| actionKind === "candidate_saved";
|
||||
const terminalStatusMatches = actionKind === "present_low_result"
|
||||
? assessment?.status === "rectifying"
|
||||
: (actionKind === "present_medium_result" || actionKind === "candidate_saved")
|
||||
&& assessment?.status === "candidate";
|
||||
|
||||
return assessment?.id === request.caseId
|
||||
&& assessment?.user_id === request.userId
|
||||
&& assessment?.journey_protocol === "legacy-guided-v1"
|
||||
&& terminalStatusMatches
|
||||
&& assessment.candidate_result_id === request.resultId
|
||||
&& action?.resultId === request.resultId
|
||||
&& terminal
|
||||
&& winner?.representativeTime === request.time
|
||||
? request.time
|
||||
: null;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ type DraftRevision = GuidedMutation & {
|
||||
};
|
||||
type CandidateSave = GuidedMutation & { readonly resultId: string };
|
||||
type CandidateConfirmation = CandidateSave & { readonly time: string };
|
||||
type CandidateCompletion = Pick<CandidateSave, "caseId" | "resultId"> & { readonly time: string };
|
||||
|
||||
const errorPayloadSchema = z.object({
|
||||
message: z.string().optional(),
|
||||
@@ -69,22 +68,3 @@ export function confirmGuidedBirthTimeCandidate(
|
||||
) {
|
||||
return send({ type: "confirm_guided_candidate", ...input });
|
||||
}
|
||||
|
||||
export async function completeGuidedBirthTimeCandidate(
|
||||
input: CandidateCompletion,
|
||||
) {
|
||||
const { response, payload } = await postJson({
|
||||
url: "/api/birth-time-candidate-completion",
|
||||
body: JSON.stringify(input),
|
||||
retryLostResponse: false,
|
||||
});
|
||||
if (!response.ok) {
|
||||
const parsed = errorPayloadSchema.safeParse(payload);
|
||||
throw new GuidedBirthTimeRequestError(
|
||||
response.status,
|
||||
parsed.success
|
||||
? parsed.data.message ?? parsed.data.error ?? "候选时间暂时无法保存"
|
||||
: "候选时间暂时无法保存",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ export function isDeclaredBirthProfileComplete(
|
||||
|
||||
export function isBirthTimeReadyForConsultation(draft: BirthTimeDraft) {
|
||||
return isBirthClockTime(draft.time)
|
||||
&& (draft.birthTimeStatus === "candidate" || draft.birthTimeStatus === "confirmed");
|
||||
&& draft.birthTimeStatus === "confirmed";
|
||||
}
|
||||
|
||||
const declaredBirthInputKeys = [
|
||||
|
||||
@@ -3,6 +3,12 @@ export type ChatMessage = {
|
||||
readonly text: string;
|
||||
readonly suggestions?: readonly string[];
|
||||
readonly techniqueTruth?: string;
|
||||
readonly workflowReceipt?: {
|
||||
readonly route: string;
|
||||
readonly status: string;
|
||||
readonly preciseTiming: string;
|
||||
readonly missingLayers: readonly string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ChatMessageView = ChatMessage & {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { ChatMessage } from "./chat-message-view";
|
||||
|
||||
export function consultationReportMarkdown(input: {
|
||||
title: string;
|
||||
messages: readonly ChatMessage[];
|
||||
}) {
|
||||
const latestAssistant = [...input.messages].reverse().find((message) => message.role === "assistant");
|
||||
const evidence = latestAssistant?.workflowReceipt;
|
||||
return [
|
||||
`# ${input.title}`,
|
||||
"",
|
||||
"## 最新回答",
|
||||
latestAssistant?.text || "暂无回答。",
|
||||
"",
|
||||
"## Claim boundary",
|
||||
`technique_truth: ${latestAssistant?.techniqueTruth || "unknown"}`,
|
||||
evidence ? `workflow_route: ${evidence.route}` : "workflow_route: unknown",
|
||||
evidence ? `workflow_status: ${evidence.status}` : "workflow_status: unknown",
|
||||
evidence ? `precise_timing: ${evidence.preciseTiming}` : "precise_timing: unknown",
|
||||
evidence ? `missing_layers: ${evidence.missingLayers.join(" / ") || "none"}` : "missing_layers: unknown",
|
||||
"",
|
||||
"## Boundary",
|
||||
"本报告保留证据边界;未闭环内容不得包装成确定预测。",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function downloadMarkdownReport(title: string, markdown: string) {
|
||||
const blob = new Blob([markdown], { type: "text/markdown;charset=utf-8" });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = `${title.replace(/[\\/:*?"<>|]+/g, "-")}.md`;
|
||||
document.body.append(anchor);
|
||||
anchor.click();
|
||||
anchor.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -2,15 +2,64 @@ export const consultationThemeValues = ["career", "marriage", "wealth", "timing"
|
||||
|
||||
export type ConsultationTheme = typeof consultationThemeValues[number];
|
||||
|
||||
export type ConsultationWorkflowRoute =
|
||||
| "career"
|
||||
| "marriage"
|
||||
| "wealth"
|
||||
| "timing"
|
||||
| "rectification"
|
||||
| "prashna"
|
||||
| "general";
|
||||
|
||||
type WorkflowProjection = {
|
||||
question: string;
|
||||
themes: readonly ("career" | "marriage" | "wealth")[];
|
||||
strictWorkflowRoute: ConsultationWorkflowRoute;
|
||||
requiredLayers: readonly string[];
|
||||
claimBoundary: string;
|
||||
};
|
||||
|
||||
const routeRequirements: Record<ConsultationTheme, Omit<WorkflowProjection, "question"> & { prefix?: string }> = {
|
||||
career: {
|
||||
themes: ["career"],
|
||||
strictWorkflowRoute: "career",
|
||||
requiredLayers: ["D1", "D10", "10th house/lord", "A10", "AmK", "Vimshottari", "Narayana", "Transit"],
|
||||
claimBoundary: "career_direction_and_broad_timing_only",
|
||||
},
|
||||
marriage: {
|
||||
themes: ["marriage"],
|
||||
strictWorkflowRoute: "marriage",
|
||||
requiredLayers: ["D1", "D9", "7th house/lord", "Venus/Jupiter", "DK", "UL", "A7", "Vimshottari", "Narayana", "Transit"],
|
||||
claimBoundary: "relationship_pattern_and_broad_window_only",
|
||||
},
|
||||
wealth: {
|
||||
themes: ["wealth"],
|
||||
strictWorkflowRoute: "wealth",
|
||||
requiredLayers: ["D1", "D2", "D11", "2nd/11th/9th/5th houses", "Wealth Yogas", "Ashtakavarga", "Dasha"],
|
||||
claimBoundary: "wealth_structure_not_financial_advice",
|
||||
},
|
||||
timing: {
|
||||
themes: ["career"],
|
||||
strictWorkflowRoute: "timing",
|
||||
requiredLayers: ["Vimshottari", "Narayana", "Transit", "Varga", "negative holdout gate"],
|
||||
claimBoundary: "candidate_day_month_window_only_until_holdout_passes",
|
||||
prefix: "应期与阶段问题:",
|
||||
},
|
||||
general: {
|
||||
themes: ["career", "marriage", "wealth"],
|
||||
strictWorkflowRoute: "general",
|
||||
requiredLayers: ["D1", "D9", "D10", "D2", "Dasha", "Narayana", "Transit", "Functional Benefic/Malefic"],
|
||||
claimBoundary: "multi_domain_summary_with_missing_layers_disclosed",
|
||||
},
|
||||
};
|
||||
|
||||
export function projectConsultationWorkflowRequest(question: string, theme: ConsultationTheme) {
|
||||
switch (theme) {
|
||||
case "career":
|
||||
case "marriage":
|
||||
case "wealth":
|
||||
return { question, themes: [theme] } as const;
|
||||
case "timing":
|
||||
return { question: `应期与阶段问题:${question}`, themes: ["career"] } as const;
|
||||
case "general":
|
||||
return { question, themes: ["career", "marriage", "wealth"] } as const;
|
||||
}
|
||||
const requirement = routeRequirements[theme];
|
||||
return {
|
||||
question: `${requirement.prefix ?? ""}${question}`,
|
||||
themes: requirement.themes,
|
||||
strictWorkflowRoute: requirement.strictWorkflowRoute,
|
||||
requiredLayers: requirement.requiredLayers,
|
||||
claimBoundary: requirement.claimBoundary,
|
||||
} satisfies WorkflowProjection;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ConsultationTheme } from "./consultation-workflow-request";
|
||||
|
||||
export type GuidedJyotishTopic = {
|
||||
id: ConsultationTheme;
|
||||
label: string;
|
||||
prompt: string;
|
||||
strictWorkflowRoute: string;
|
||||
evidencePreview: string[];
|
||||
confidenceCap: "low" | "medium";
|
||||
claimBoundary: string;
|
||||
};
|
||||
|
||||
export const defaultGuidedJyotishTopics: GuidedJyotishTopic[] = [
|
||||
{
|
||||
id: "career",
|
||||
label: "事业",
|
||||
prompt: "未来一年,事业和收入该关注什么?",
|
||||
strictWorkflowRoute: "career",
|
||||
evidencePreview: ["D1", "D10", "A10", "Vimshottari", "Narayana", "Transit"],
|
||||
confidenceCap: "medium",
|
||||
claimBoundary: "输出职业结构与阶段判断;具体日/月只作为候选窗口。",
|
||||
},
|
||||
{
|
||||
id: "marriage",
|
||||
label: "关系",
|
||||
prompt: "我的关系模式是什么?",
|
||||
strictWorkflowRoute: "marriage",
|
||||
evidencePreview: ["D1", "D9", "7宫", "DK", "UL", "Vimshottari", "Narayana"],
|
||||
confidenceCap: "medium",
|
||||
claimBoundary: "输出关系模式与宽窗口;不承诺某日必然发生事件。",
|
||||
},
|
||||
{
|
||||
id: "wealth",
|
||||
label: "财富",
|
||||
prompt: "我的财富增长方式和风险点是什么?",
|
||||
strictWorkflowRoute: "wealth",
|
||||
evidencePreview: ["D1", "D2", "D11", "2/11宫", "财富 Yoga", "Ashtakavarga"],
|
||||
confidenceCap: "medium",
|
||||
claimBoundary: "输出财富结构和风险类型;不替代投资建议。",
|
||||
},
|
||||
{
|
||||
id: "timing",
|
||||
label: "时运",
|
||||
prompt: "未来哪些阶段值得把握?",
|
||||
strictWorkflowRoute: "timing",
|
||||
evidencePreview: ["Dasha", "Narayana", "Transit", "Varga"],
|
||||
confidenceCap: "low",
|
||||
claimBoundary: "精确月/日仍是探索性候选,未通过独立 holdout 前不升级。",
|
||||
},
|
||||
];
|
||||
@@ -77,7 +77,7 @@ type OnboardingProfileFingerprintInput = {
|
||||
};
|
||||
|
||||
export type OnboardingSuggestion = {
|
||||
readonly theme: "career" | "marriage" | "timing";
|
||||
readonly theme: ConsultationTheme;
|
||||
readonly text: string;
|
||||
};
|
||||
|
||||
@@ -268,3 +268,4 @@ export async function requestOnboardingWithRecovery(
|
||||
|
||||
throw lastError ?? new OnboardingRequestError("pending");
|
||||
}
|
||||
import type { ConsultationTheme } from "./consultation-workflow-request";
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
type OracleSummary = {
|
||||
ready: string[];
|
||||
partial: string[];
|
||||
blocked: string[];
|
||||
};
|
||||
|
||||
export type TruthSourceRuntimeIdentity = {
|
||||
status: "ok" | "blocked";
|
||||
path: string;
|
||||
mountStatus: "mounted" | "not_mounted";
|
||||
commit: string;
|
||||
skillVersion: string;
|
||||
evidencePacketCount: number | null;
|
||||
oracleSummary: OracleSummary;
|
||||
claimGateStatus: "ready" | "partial_or_blocked_present" | "not_mounted";
|
||||
};
|
||||
|
||||
export const DEFAULT_RESEARCH_TRUTH_SOURCE_PATH = "/Users/wuyongnaren/Documents/印度占星";
|
||||
|
||||
function readJson(path: string): unknown {
|
||||
return JSON.parse(readFileSync(path, "utf8"));
|
||||
}
|
||||
|
||||
function readEvidencePacketCount(researchPath: string): number | null {
|
||||
const indexPath = join(researchPath, "references/oracle/evidence_packet_index_2026_07_19.json");
|
||||
if (!existsSync(indexPath)) return null;
|
||||
const data = readJson(indexPath) as { summary?: { packet_count?: unknown } };
|
||||
return typeof data.summary?.packet_count === "number" ? data.summary.packet_count : null;
|
||||
}
|
||||
|
||||
function readSkillVersion(researchPath: string): string {
|
||||
const skillPath = join(researchPath, "SKILL.md");
|
||||
if (!existsSync(skillPath)) return "unknown";
|
||||
const source = readFileSync(skillPath, "utf8");
|
||||
const version = source.match(/(?:version|Version)[::]\s*([^\n]+)/);
|
||||
return version?.[1]?.trim() || "present_unversioned";
|
||||
}
|
||||
|
||||
function readCommit(researchPath: string): string {
|
||||
const headPath = join(researchPath, ".git/HEAD");
|
||||
if (!existsSync(headPath)) return "unknown";
|
||||
const head = readFileSync(headPath, "utf8").trim();
|
||||
if (!head.startsWith("ref: ")) return head.slice(0, 40);
|
||||
const refPath = join(researchPath, ".git", head.slice(5));
|
||||
if (!existsSync(refPath)) return "unknown";
|
||||
return readFileSync(refPath, "utf8").trim().slice(0, 40);
|
||||
}
|
||||
|
||||
export function getTruthSourceRuntimeIdentity(
|
||||
researchPath = process.env.JYOTISH_RESEARCH_TRUTH_SOURCE_PATH ?? DEFAULT_RESEARCH_TRUTH_SOURCE_PATH,
|
||||
): TruthSourceRuntimeIdentity {
|
||||
if (!existsSync(researchPath)) {
|
||||
return {
|
||||
status: "blocked",
|
||||
path: researchPath,
|
||||
mountStatus: "not_mounted",
|
||||
commit: "unknown",
|
||||
skillVersion: "unknown",
|
||||
evidencePacketCount: null,
|
||||
oracleSummary: { ready: [], partial: [], blocked: ["research_truth_source_not_mounted"] },
|
||||
claimGateStatus: "not_mounted",
|
||||
};
|
||||
}
|
||||
|
||||
const evidencePacketCount = readEvidencePacketCount(researchPath);
|
||||
const blocked = evidencePacketCount === null ? ["evidence_packet_index_missing"] : [];
|
||||
const partial = ["commercial_runtime_identity_only"];
|
||||
|
||||
return {
|
||||
status: blocked.length ? "blocked" : "ok",
|
||||
path: researchPath,
|
||||
mountStatus: "mounted",
|
||||
commit: readCommit(researchPath),
|
||||
skillVersion: readSkillVersion(researchPath),
|
||||
evidencePacketCount,
|
||||
oracleSummary: { ready: [], partial, blocked },
|
||||
claimGateStatus: blocked.length ? "partial_or_blocked_present" : "partial_or_blocked_present",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user