M2b:导入改造产出可审阅灵胚(启镜物化世谱)与组装管线(符文展开/火候进入 IR),Kernel 打通采样参数链路

This commit is contained in:
chenxin committed 2026-08-21 23:43:29 +08:00
1 parent e6a3b7121f
commit 5f36e3785c
86 files changed
+424 -14455

No files matched your search

-26
View File
@@ -1,26 +0,0 @@
import { ChatShell } from "@/components/chat/ChatShell";
import { serverApi } from "@/lib/api/server-client";
import type { Character, ModelGatewayStatus } from "@/types/api";
export const dynamic = "force-dynamic";
/**
* 对话页:Server Component 预取当前激活人物与模型网关后注入 ChatShell。
* 任一不可达时仍渲染界面,由用户感知后端状态。
*/
export default async function ChatPage() {
let character: Character | null = null;
let gateway: ModelGatewayStatus | null = null;
try {
character = await serverApi.get<Character>("/characters/active");
} catch {
character = null;
}
try {
gateway = await serverApi.get<ModelGatewayStatus>("/settings/models");
} catch {
gateway = null;
}
return <ChatShell initialCharacter={character} gateway={gateway} />;
}
-691
View File
@@ -1,691 +0,0 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { Card } from "@/components/ui/Card";
import { TextSkeleton } from "@/components/ui/Skeleton";
import { ErrorState } from "@/components/shared/StateViews";
import { SundialDisk } from "@/components/time/SundialDisk";
import { charactersApi, timeApi } from "@/lib/api";
import { describeError } from "@/lib/utils/error";
import type {
AcquaintanceInfo,
Anniversary,
CreateAnniversaryRequest,
TimeConfig,
TimeSnapshotResponse,
} from "@/types/api";
// ============================================================
// 辰正 · 时态感知
// 实时时间展示 + 十二时辰流 + 自动刷新的后端数据
// ============================================================
// —— 十二时辰 ——
const SHICHEN_KEYS = [
"zi",
"chou",
"yin",
"mao",
"chen",
"si",
"wu",
"wei",
"shen",
"you",
"xu",
"hai",
] as const;
const SHICHEN_HOURS: Record<string, number> = {
chou: 2,
yin: 4,
mao: 6,
chen: 8,
si: 10,
wu: 12,
wei: 14,
shen: 16,
you: 18,
xu: 20,
hai: 22,
};
/** 由客户端本地时辰确定时辰 key(与后端逻辑一致) */
function shichenFromHour(hour: number): string {
if (hour >= 23 || hour < 1) return "zi";
// 取各时辰起始值,反向查找
let prev = "zi";
for (const key of SHICHEN_KEYS) {
const start = SHICHEN_HOURS[key];
if (hour >= start) prev = key;
else break;
}
return prev;
}
/** 获取目标时区的当前时间组件 */
function getLocalTimeInTimezone(timezone: string) {
const fmt = new Intl.DateTimeFormat("zh-CN", {
timeZone: timezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
const p = fmt.formatToParts(new Date());
const g = (t: string) =>
parseInt(p.find((x) => x.type === t)?.value ?? "0", 10);
return {
year: g("year"),
month: g("month"),
day: g("day"),
hour: g("hour"),
minute: g("minute"),
second: g("second"),
};
}
function getDayPeriod(h: number): string {
if (h >= 0 && h < 5) return "凌晨";
if (h >= 5 && h < 7) return "清晨";
if (h >= 7 && h < 11) return "上午";
if (h >= 11 && h < 13) return "中午";
if (h >= 13 && h < 17) return "下午";
if (h >= 17 && h < 19) return "傍晚";
if (h >= 19 && h < 22) return "夜晚";
return "深夜";
}
const KIND_OPTIONS = [
{ value: "anniversary", label: "纪念日" },
{ value: "birthday", label: "生日" },
{ value: "custom", label: "自定义" },
];
// ============================================================
// 子组件
// ============================================================
/** 调制因子(设计稿 .factor 渐变轨道) */
function FactorFactor({
label,
value,
cls,
}: {
label: string;
value: number;
cls: string;
}) {
const pct = Math.round(Math.max(0, Math.min(1, value)) * 100);
return (
<div className={`factor ${cls}`}>
<span className="fk">{label}</span>
<div className="ftrack">
<div className="ffill" style={{ width: `${pct}%` }} />
</div>
<span className="fv">{pct}%</span>
</div>
);
}
// ============================================================
// 主页面
// ============================================================
export default function TimePage() {
// —— 后端数据 ——
const [snapshot, setSnapshot] = useState<TimeSnapshotResponse | null>(null);
const [acquaintance, setAcquaintance] = useState<AcquaintanceInfo | null>(
null,
);
const [config, setConfig] = useState<TimeConfig | null>(null);
const [currentMood, setCurrentMood] = useState<{
mood: string;
intensity: number;
} | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// —— 实时时钟 ——
const [, setNow] = useState<Date>(new Date());
const [, setPrevShichen] = useState<string | null>(null);
const [shichenChanged, setShichenChanged] = useState(false);
const shichenRef = useRef("");
// 每秒 tick
useEffect(() => {
const timer = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(timer);
}, []);
// 检测时辰变化
const timezone =
snapshot?.snapshot?.timezone ?? config?.user_timezone ?? "Asia/Shanghai";
const local = getLocalTimeInTimezone(timezone);
const currentShichen = shichenFromHour(local.hour);
useEffect(() => {
if (shichenRef.current && shichenRef.current !== currentShichen) {
setPrevShichen(shichenRef.current);
setShichenChanged(true);
setTimeout(() => setShichenChanged(false), 2000);
}
shichenRef.current = currentShichen;
}, [currentShichen]);
// —— 数据获取 ——
const fetchData = useCallback(async () => {
try {
const [s, acq, cfg] = await Promise.all([
timeApi.getTimeSnapshot(),
timeApi.getAcquaintance(),
timeApi.getTimeConfig(),
]);
setSnapshot(s);
setAcquaintance(acq);
setConfig(cfg);
} catch (e) {
if (!snapshot) setError(describeError(e));
} finally {
setLoading(false);
}
}, [snapshot]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchData();
// 获取当前人物心情
charactersApi
.getActiveCharacter()
.then((c) => {
if (c.current_mood) setCurrentMood(c.current_mood);
})
.catch(() => {});
}, []); // eslint-disable-line react-hooks/exhaustive-deps
// 自动刷新:每 60 秒重新拉取后端数据(农历/节气/节日变化)
useEffect(() => {
if (loading) return;
const timer = setInterval(fetchData, 60_000);
return () => clearInterval(timer);
}, [fetchData, loading]);
// —— 渲染 ——
if (loading) {
return (
<div className="mx-auto max-w-5xl px-4 py-8">
<TextSkeleton lines={1} className="mb-8 w-48" />
<div className="grid grid-cols-1 gap-6 md:grid-cols-5">
<div className="md:col-span-3">
<div className="glass rounded-2xl p-8">
<TextSkeleton lines={3} />
</div>
</div>
<div className="md:col-span-2">
<div className="glass rounded-2xl p-5">
<TextSkeleton lines={6} />
</div>
</div>
</div>
<div className="glass mt-6 rounded-2xl p-5">
<TextSkeleton lines={2} />
</div>
</div>
);
}
if (error && !snapshot) {
return <ErrorState message={error} onRetry={fetchData} />;
}
const s = snapshot?.snapshot;
const factors = snapshot?.factors;
const context_text = snapshot?.context_text;
// 实时时辰信息(优先用本地计算的,回退后端数据)
const displayShichen = currentShichen;
const displayDayPeriod = getDayPeriod(local.hour);
return (
<div className="mx-auto max-w-5xl px-4 py-8">
{/* ===== 页头 ===== */}
<header className="mb-8 flex flex-wrap items-end justify-between gap-4">
<div>
<div className="flex items-center gap-3">
<h1 className="font-serif text-2xl font-semibold text-fg">辰正</h1>
<span className="rounded-full bg-gold/10 px-2.5 py-0.5 text-[10px] font-medium tracking-wider text-gold">
时态感知
</span>
{shichenChanged && (
<span className="animate-pulse rounded-full bg-gold/30 px-2 py-0.5 text-[10px] text-gold">
更时
</span>
)}
</div>
{config && (
<p className="mt-1 text-xs text-muted">
{config.default_city} · {config.user_timezone}
</p>
)}
</div>
<div className="flex items-center gap-2 text-[11px] text-muted">
<span className="inline-block h-1.5 w-1.5 rounded-full bg-patina" />
辰光实时
</div>
</header>
{/* ===== 日晷时辰盘 + 调制因子(设计稿 .sundial-region) ===== */}
<div className="sundial-region">
{/* 日晷 */}
<SundialDisk
currentShichen={displayShichen}
hour={local.hour}
minute={local.minute}
lunarText={s?.lunar?.text}
festivals={s?.festivals}
solarTerm={s?.solar_term?.name}
/>
{/* 调制因子(设计稿 .factor-card) */}
{factors && (
<div className="factor-card">
<div className="mb-4 flex items-center gap-2">
<h3 className="font-serif text-sm font-semibold text-fg">
调制因子
</h3>
<span className="h-px flex-1 bg-rule" />
</div>
<div className="factor-list">
<FactorFactor
label="节日喜庆"
value={factors.festival_joy}
cls="f-joy"
/>
<FactorFactor
label="深夜脆弱"
value={factors.late_night_fragility}
cls="f-frag"
/>
<FactorFactor
label="重逢强度"
value={factors.reunion_intensity}
cls="f-reun"
/>
<FactorFactor
label="周末放松"
value={factors.weekend_relax}
cls="f-wknd"
/>
<FactorFactor
label="季节共鸣"
value={factors.season_resonance}
cls="f-season"
/>
<FactorFactor
label="同期共振"
value={factors.same_period_resonance}
cls="f-period"
/>
<FactorFactor
label="关怀倾向"
value={factors.care_suggest}
cls="f-care"
/>
</div>
{currentMood && (
<div className="mt-4 rounded-xl bg-glass/40 px-4 py-3">
<div className="mb-1 flex items-center gap-2">
<h3 className="font-serif text-sm font-semibold text-fg">
当前心情
</h3>
<span className="h-px flex-1 bg-rule" />
</div>
<div className="flex items-center gap-3">
<span className="text-2xl">
{currentMood.mood === "joy"
? "😊"
: currentMood.mood === "sadness"
? "😢"
: currentMood.mood === "anger"
? "😠"
: currentMood.mood === "fear"
? "😨"
: currentMood.mood === "surprise"
? "😲"
: currentMood.mood === "disgust"
? "😣"
: "😐"}
</span>
<span className="font-serif text-base font-medium text-fg">
{currentMood.mood}
</span>
<span className="font-mono text-xs text-muted">
{Math.round(currentMood.intensity * 100)}%
</span>
</div>
</div>
)}
{factors.care_kind && (
<div className="care-note">
<b>关怀</b> · {factors.care_kind}
</div>
)}
</div>
)}
</div>
{/* ===== 相识 / 离线 / 里程碑 快照格(设计稿 .snap-grid) ===== */}
{s && (
<div className="snap-grid">
<div className="snap-card">
<div className="k">相识</div>
<div className="v">
{s.acquaintance.total_days}
<small>天</small>
</div>
<div className="sub">{s.acquaintance.natural_language}</div>
</div>
<div className="snap-card">
<div className="k">离线</div>
<div className="v">
{Math.round(s.offline.seconds / 86400)}
<small>天</small>
</div>
<div className="sub">{s.offline.natural_language}</div>
</div>
<div className="snap-card milestone">
<div className="k">下个纪念日</div>
<div className="v">
{s.next_milestone?.days_until ?? "—"}
<small>天后</small>
</div>
<div className="sub">{s.next_milestone?.name ?? "无"}</div>
</div>
<div className="snap-card">
<div className="k">辰光</div>
<div className="v">
{displayShichen}
<small>· {displayDayPeriod}</small>
</div>
<div className="sub">{s.season ?? "—"}</div>
</div>
</div>
)}
{/* ===== 时态感知文本(全宽) ===== */}
{context_text && (
<Card variant="surface" className="p-5">
<div className="mb-3 flex items-center gap-2">
<h3 className="font-serif text-sm font-semibold text-fg">
时态感知
</h3>
<span className="h-px flex-1 bg-rule" />
</div>
<p className="whitespace-pre-wrap text-sm leading-relaxed text-fg/80">
{context_text}
</p>
</Card>
)}
{/* ===== 底部双栏:相识 + 纪念日管理(设计稿 .lower-region) ===== */}
<div className="lower-region">
{acquaintance && (
<Card variant="surface" className="p-6 event-main">
<div className="mb-3 flex items-center gap-2">
<h3 className="font-serif text-sm font-semibold text-fg">相识</h3>
<span className="h-px flex-1 bg-rule" />
</div>
<p className="font-serif text-4xl font-semibold text-fg">
{acquaintance.total_days}
<span className="ml-1 text-base font-normal text-muted">天</span>
</p>
<p className="mt-1 text-sm text-muted">
{acquaintance.natural_language}
</p>
</Card>
)}
<div className="anniv-rail">
<AnniversaryManager />
</div>
</div>
</div>
);
}
// ============================================================
// 纪念日管理(保持原功能不变)
// ============================================================
function AnniversaryManager() {
const [anniversaries, setAnniversaries] = useState<Anniversary[]>([]);
const [showForm, setShowForm] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [form, setForm] = useState<CreateAnniversaryRequest & { date: string }>(
{
name: "",
date: "",
kind: "anniversary",
calendar: "solar",
subject_type: "relationship",
source: "manual",
},
);
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetch = useCallback(async () => {
try {
const data = await timeApi.listAnniversaries();
setAnniversaries(data);
} catch {
/* silent */
}
}, []);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetch();
}, [fetch]);
function resetForm() {
setForm({
name: "",
date: "",
kind: "anniversary",
calendar: "solar",
subject_type: "relationship",
source: "manual",
});
setShowForm(false);
setEditingId(null);
setError(null);
}
async function handleCreate() {
if (!form.name.trim() || !form.date) {
setError("请填写名称与日期。");
return;
}
setBusy(true);
setError(null);
try {
await timeApi.createAnniversary(form);
resetForm();
await fetch();
} catch (e) {
setError(e instanceof Error ? e.message : "创建失败。");
} finally {
setBusy(false);
}
}
async function handleUpdate(id: string) {
if (!form.name.trim() || !form.date) {
setError("请填写名称与日期。");
return;
}
setBusy(true);
setError(null);
try {
await timeApi.updateAnniversary(id, form);
resetForm();
await fetch();
} catch (e) {
setError(e instanceof Error ? e.message : "更新失败。");
} finally {
setBusy(false);
}
}
async function handleDelete(id: string) {
setBusy(true);
setError(null);
try {
await timeApi.deleteAnniversary(id);
await fetch();
} catch (e) {
setError(e instanceof Error ? e.message : "删除失败。");
} finally {
setBusy(false);
}
}
function startEdit(a: Anniversary) {
setEditingId(a.id);
setShowForm(false);
setForm({
name: a.name,
date: a.date,
kind: a.kind,
calendar: a.calendar,
subject_type: a.subject_type,
source: a.source,
});
setError(null);
}
const inputCls =
"w-full rounded-lg border border-border-glass bg-glass px-2 py-1 text-sm text-fg outline-none focus:border-patina/60 focus:shadow-[0_0_0_2px] focus:shadow-patina/20";
return (
<Card variant="surface" className="p-6">
<div className="flex items-center justify-between">
<h3 className="font-serif text-sm font-semibold text-fg">
纪念日({anniversaries.length})
</h3>
{!showForm && !editingId && (
<button
type="button"
onClick={() => {
resetForm();
setShowForm(true);
}}
className="rounded-lg border border-patina/40 bg-patina/10 px-3 py-1.5 text-xs font-medium text-patina transition-colors hover:bg-patina/20"
>
+ 新增
</button>
)}
</div>
{(showForm || editingId) && (
<div className="mt-3 space-y-2 rounded-lg border border-border-glass bg-glass/50 p-3">
<input
value={form.name}
onChange={(e) => setForm({ ...form, name: e.target.value })}
placeholder="名称"
className={inputCls}
/>
<input
value={form.date}
onChange={(e) => setForm({ ...form, date: e.target.value })}
type="date"
className={inputCls}
/>
<div className="flex gap-2">
<select
value={form.kind}
onChange={(e) => setForm({ ...form, kind: e.target.value })}
className={inputCls}
>
{KIND_OPTIONS.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
<select
value={form.calendar}
onChange={(e) => setForm({ ...form, calendar: e.target.value })}
className={inputCls}
>
<option value="solar">公历</option>
<option value="lunar">农历</option>
</select>
</div>
<div className="flex gap-2">
<button
type="button"
onClick={editingId ? () => handleUpdate(editingId) : handleCreate}
disabled={busy}
className="rounded-lg bg-patina/20 px-3 py-1 text-xs font-medium text-patina disabled:opacity-50"
>
{editingId ? "保存" : "创建"}
</button>
<button
type="button"
onClick={resetForm}
className="rounded-lg border border-border-glass bg-glass px-3 py-1 text-xs text-muted"
>
取消
</button>
</div>
{error && <p className="text-xs text-cinnabar">{error}</p>}
</div>
)}
<div className="mt-3 space-y-2">
{anniversaries.length === 0 ? (
<p className="text-sm text-muted">暂无纪念日。</p>
) : (
anniversaries.map((a) => (
<div
key={a.id}
className="flex items-center justify-between rounded-lg bg-glass px-3 py-2"
>
<div className="min-w-0 flex-1">
<span className="text-sm text-fg">{a.name}</span>
<span className="ml-2 text-[11px] text-muted">{a.date}</span>
<span className="ml-2 rounded bg-gold/10 px-1.5 py-0.5 text-[10px] text-gold">
{a.kind}
</span>
</div>
<div className="ml-2 flex shrink-0 gap-2">
<button
type="button"
onClick={() => startEdit(a)}
className="text-[11px] text-patina hover:underline"
>
编辑
</button>
<button
type="button"
onClick={() => handleDelete(a.id)}
disabled={busy}
className="text-[11px] text-cinnabar hover:underline disabled:opacity-50"
>
删除
</button>
</div>
</div>
))
)}
</div>
</Card>
);
}