从一个主题开始
-
选择最接近你当下的问题。
+
选择一个你现在想解决的问题。
{starterSuggestions.map((item) => {
diff --git a/frontend/src/lib/guided-jyotish-topics.ts b/frontend/src/lib/guided-jyotish-topics.ts
index 5f32952d..a773a612 100644
--- a/frontend/src/lib/guided-jyotish-topics.ts
+++ b/frontend/src/lib/guided-jyotish-topics.ts
@@ -24,16 +24,16 @@ export const defaultGuidedJyotishTopics: GuidedJyotishTopic[] = consultationDoma
}));
const generalPromptByTheme: Partial> = {
- career: "印度占星一般会从哪些因素理解事业方向?",
- marriage: "印度占星一般如何分析关系模式?",
- wealth: "印度占星一般如何分析财富结构与风险?",
- health: "印度占星如何在非医疗诊断边界内理解身心压力?",
- education: "印度占星一般如何理解学习方式与进阶节奏?",
- migration: "印度占星一般如何分析迁居、置业与海外发展?",
- family: "印度占星一般如何理解家庭关系与责任模式?",
- annual: "印度占星中的年运分析通常包含哪些证据层?",
- timing: "印度占星中的时间推运通常会看哪些因素?",
- general: "印度占星综合咨询会如何划分不同主题与证据边界?",
+ career: "请帮我梳理目前的事业方向和下一步重点。",
+ marriage: "请帮我看看我在关系中容易重复什么模式。",
+ wealth: "请帮我分析目前的财富重点、风险和更稳妥的选择。",
+ health: "请从非医疗诊断的角度,帮我看看近期的身心压力和调整重点。",
+ education: "请帮我看看我更适合怎样学习,以及如何安排下一步。",
+ migration: "请帮我分析现阶段是否适合迁居、置业或考虑海外发展。",
+ family: "请帮我看看家庭关系中最需要处理的问题和责任边界。",
+ annual: "请帮我看看未来一年的主要趋势、机会和需要留意的阶段。",
+ timing: "请帮我看看目前适合推进什么,哪些事情需要再等等。",
+ general: "请结合我的情况,帮我找出现在最值得优先处理的三个问题。",
};
export const generalGuidedJyotishTopics: GuidedJyotishTopic[] = defaultGuidedJyotishTopics.map((topic) => ({
diff --git a/frontend/src/lib/onboarding-cache-policy.ts b/frontend/src/lib/onboarding-cache-policy.ts
index 4333abc1..5f826ac3 100644
--- a/frontend/src/lib/onboarding-cache-policy.ts
+++ b/frontend/src/lib/onboarding-cache-policy.ts
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
-const ONBOARDING_VERSION = "ayanam-onboarding-v3";
+const ONBOARDING_VERSION = "ayanam-onboarding-v4";
export const ONBOARDING_CLAIM_TTL_MS = 2 * 60 * 1000;
type OnboardingProfileInput = {
diff --git a/frontend/src/lib/onboarding-payload.ts b/frontend/src/lib/onboarding-payload.ts
index eea1b3c5..d1246afa 100644
--- a/frontend/src/lib/onboarding-payload.ts
+++ b/frontend/src/lib/onboarding-payload.ts
@@ -1,11 +1,20 @@
import { z } from "zod";
+const detachedStarterQuestionPattern = /印度占星|一般如何|通常(?:会)?(?:看|观察|分析|理解|包含)|哪些(?:因素|证据层)|如何划分/;
+
+const userCenteredStarterQuestionSchema = z.string()
+ .trim()
+ .min(4)
+ .max(80)
+ .refine((text) => text.includes("我"), "starter_question_must_be_first_person")
+ .refine((text) => !detachedStarterQuestionPattern.test(text), "starter_question_must_not_be_encyclopedic");
+
const onboardingSchema = z.object({
greeting: z.string().trim().min(8).max(180),
suggestions: z.tuple([
- z.object({ theme: z.literal("career"), text: z.string().trim().min(4).max(80) }),
- z.object({ theme: z.literal("marriage"), text: z.string().trim().min(4).max(80) }),
- z.object({ theme: z.literal("timing"), text: z.string().trim().min(4).max(80) }),
+ z.object({ theme: z.literal("career"), text: userCenteredStarterQuestionSchema }),
+ z.object({ theme: z.literal("marriage"), text: userCenteredStarterQuestionSchema }),
+ z.object({ theme: z.literal("timing"), text: userCenteredStarterQuestionSchema }),
]),
});
@@ -14,9 +23,9 @@ export type OnboardingPayload = z.infer;
export const fallbackOnboardingPayload: OnboardingPayload = {
greeting: "我们从你此刻最关心的事情开始。可以选择下面的方向,也可以直接说出你的问题。",
suggestions: [
- { theme: "career", text: "我的事业优势更适合怎样发挥?" },
- { theme: "marriage", text: "我在关系里容易重复什么模式?" },
- { theme: "timing", text: "未来一年有哪些阶段值得提前准备?" },
+ { theme: "career", text: "请帮我看看事业优势更适合怎样发挥。" },
+ { theme: "marriage", text: "请帮我看看关系里容易重复什么模式。" },
+ { theme: "timing", text: "请帮我看看未来一年哪些阶段值得提前准备。" },
],
};
diff --git a/frontend/src/mastra/index.ts b/frontend/src/mastra/index.ts
index 95053e5f..e27b5638 100644
--- a/frontend/src/mastra/index.ts
+++ b/frontend/src/mastra/index.ts
@@ -121,7 +121,9 @@ Return valid JSON only. Do not use Markdown fences, commentary, or hidden fields
The JSON shape must be:
{"greeting":"一句自然、克制的简体中文欢迎语","suggestions":[{"theme":"career","text":"问题"},{"theme":"marriage","text":"问题"},{"theme":"timing","text":"问题"}]}
The greeting should sound human and calm, and directly invite the user to begin with what matters to them. Never mention birth data, profile readiness, setup completion, or system processing. Do not overpraise, sound mystical, or use marketing slogans.
-Generate exactly three concise questions, one for each required theme in the given order. They must help a first-time user understand the product's abilities, use everyday Simplified Chinese, and be answerable through the skill. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
+Generate exactly three concise questions, one for each required theme in the given order. Write every question as the user's own first-person request and include “我”, such as “请帮我看看……”. The question must ask for useful help with the user's situation, not for a lesson about astrology.
+Never generate detached or encyclopedic wording such as “印度占星一般如何……”, “通常会看哪些因素”, “包含哪些证据层”, or “如何划分主题”.
+The questions must use everyday Simplified Chinese and be answerable through the skill. Avoid jargon, fear, deterministic promises, medical/legal/investment claims, and unsupported precision.`;
const onboardingAgents = new Map();
diff --git a/frontend/tests/onboarding-route.test.ts b/frontend/tests/onboarding-route.test.ts
index aa49a79d..75e90429 100644
--- a/frontend/tests/onboarding-route.test.ts
+++ b/frontend/tests/onboarding-route.test.ts
@@ -11,18 +11,18 @@ import {
const payloadA = {
greeting: "林遥,欢迎开始今天的咨询。",
suggestions: [
- { theme: "career", text: "林遥的事业方向是什么?" },
- { theme: "marriage", text: "林遥的关系模式是什么?" },
- { theme: "timing", text: "林遥何时适合采取行动?" },
+ { theme: "career", text: "请帮我梳理目前的事业方向。" },
+ { theme: "marriage", text: "请帮我看看关系中容易重复什么模式。" },
+ { theme: "timing", text: "请帮我看看什么时候适合采取行动。" },
],
} as const;
const payloadB = {
greeting: "周宁,欢迎开始今天的咨询。",
suggestions: [
- { theme: "career", text: "周宁的事业方向是什么?" },
- { theme: "marriage", text: "周宁的关系模式是什么?" },
- { theme: "timing", text: "周宁何时适合采取行动?" },
+ { theme: "career", text: "请帮我看看下一步的事业重点。" },
+ { theme: "marriage", text: "请帮我看看目前的关系重点。" },
+ { theme: "timing", text: "请帮我看看哪些阶段适合主动推进。" },
],
} as const;
@@ -100,6 +100,25 @@ test("slow Agent generation is aborted and a terminal fallback is cached before
});
});
+
+test("detached Agent questions are rejected in favor of user-centered fallbacks", async () => {
+ const repository = new StatefulOnboardingProfileRepository(completeProfileRow());
+ const post = createPost(repository, async () => JSON.stringify({
+ greeting: "欢迎开始今天的咨询,可以先选择一个主题。",
+ suggestions: [
+ { theme: "career", text: "印度占星一般会从哪些因素理解事业方向?" },
+ { theme: "marriage", text: "印度占星一般如何分析关系模式?" },
+ { theme: "timing", text: "印度占星中的时间推运通常会看哪些因素?" },
+ ],
+ }));
+
+ const body = await responseBody(await post());
+
+ assert.equal(body.source, "fallback");
+ assert.ok(body.suggestions.every((item) => /我/.test(item.text)));
+ assert.ok(body.suggestions.every((item) => !/印度占星|一般如何|通常|哪些因素|证据层/.test(item.text)));
+});
+
test("PostgreSQL Date birth_date is normalized before onboarding validation", async () => {
const repository = new StatefulOnboardingProfileRepository(completeProfileRow({
birth_date: new Date(1990, 5, 15),
diff --git a/frontend/tests/starter-questions.test.ts b/frontend/tests/starter-questions.test.ts
index 2a99c0af..1b4f9b40 100644
--- a/frontend/tests/starter-questions.test.ts
+++ b/frontend/tests/starter-questions.test.ts
@@ -71,19 +71,19 @@ test("default starter questions derive every canonical domain with evidence and
assert.match(domainById.get("timing")?.requiredLayers.join(" ") ?? "", /negative holdout gate/);
});
-test("profiles without a usable birth minute receive all canonical general-knowledge prompts", () => {
+test("profiles without a usable birth minute receive user-centered starter prompts", () => {
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.id), [...consultationDomainIds]);
assert.deepEqual(generalGuidedJyotishTopics.map((topic) => topic.prompt), [
- "印度占星一般会从哪些因素理解事业方向?",
- "印度占星一般如何分析关系模式?",
- "印度占星一般如何分析财富结构与风险?",
- "印度占星如何在非医疗诊断边界内理解身心压力?",
- "印度占星一般如何理解学习方式与进阶节奏?",
- "印度占星一般如何分析迁居、置业与海外发展?",
- "印度占星一般如何理解家庭关系与责任模式?",
- "印度占星中的年运分析通常包含哪些证据层?",
- "印度占星中的时间推运通常会看哪些因素?",
- "印度占星综合咨询会如何划分不同主题与证据边界?",
+ "请帮我梳理目前的事业方向和下一步重点。",
+ "请帮我看看我在关系中容易重复什么模式。",
+ "请帮我分析目前的财富重点、风险和更稳妥的选择。",
+ "请从非医疗诊断的角度,帮我看看近期的身心压力和调整重点。",
+ "请帮我看看我更适合怎样学习,以及如何安排下一步。",
+ "请帮我分析现阶段是否适合迁居、置业或考虑海外发展。",
+ "请帮我看看家庭关系中最需要处理的问题和责任边界。",
+ "请帮我看看未来一年的主要趋势、机会和需要留意的阶段。",
+ "请帮我看看目前适合推进什么,哪些事情需要再等等。",
+ "请结合我的情况,帮我找出现在最值得优先处理的三个问题。",
]);
for (const topic of generalGuidedJyotishTopics) {
const personalTopic = defaultGuidedJyotishTopics.find((candidate) => candidate.id === topic.id);
@@ -95,10 +95,11 @@ test("profiles without a usable birth minute receive all canonical general-knowl
assert.equal(topic.claimBoundary, personalTopic.claimBoundary);
assert.notEqual(topic.prompt, personalTopic.prompt);
}
- assert.ok(generalGuidedJyotishTopics.every((topic) => !/我的|我近期|未来一年,事业和收入/.test(topic.prompt)));
+ assert.ok(generalGuidedJyotishTopics.every((topic) => /我/.test(topic.prompt)));
+ assert.ok(generalGuidedJyotishTopics.every((topic) => !/印度占星|一般如何|通常|哪些因素|证据层/.test(topic.prompt)));
assert.match(pageSource, /const starterThemes = personalChartAvailable \? themes : generalGuidedJyotishTopics/);
- assert.doesNotMatch(pageSource, /回答一般占星知识/);
- assert.match(pageSource, /完成生时校正后,再讨论个人星盘结论/);
+ assert.match(pageSource, /出生时间不足以支持的部分,我会明确说明,不会补造具体分钟/);
+ assert.match(pageSource, /请帮我看一下今天的运势,重点告诉我适合推进什么、需要注意什么/);
assert.match(pageSource, /personalChartAvailable \? "daily_starlanguage" : null/);
});