feat: 完成 I 群組 — 生活作息與離線生活
- ScheduleService:I-1 平日/週末/假期時段表(可為角色寫入高中生預設作息)、 I-3 時間感知(Asia/Taipei 當地時間,不受伺服器時區影響)、剛醒/睡前偵測 - constants.ts:I-2 時段狀態 → 回應可用度對照(FREE/SEMI_BUSY/BUSY/SLEEP → NORMAL/BRIEF/DELAYED/NO_REPLY),沒有作息表的角色一律視為 NORMAL(相容既有角色) - ScheduleExceptionService:I-4 三種破例(深夜還陪你聊/上課偷回訊息/為你調整行程), 各自獨立冷卻期,觸發時記入關係帳本 - ScheduleEmotionBaselineService:I-5 作息驅動情緒基線(社團剛結束的疲憊、假日偏高心情), 只在本輪沒有明確情緒訊號時介入,串接進 EmotionService.processInput 的新增選項 - OfflineEventService:I-6 離線事件生成,依「今天已發生的固定行程」挑選對應模板, 一天只生成一次,寫入低權重情節記憶 - DialogueService 整合以上機制:離線事件生成 → 情緒基線標記 → 累積親密度 → 作息可用度判定 → 不符合則嘗試破例 → 依可用度決定回覆內容 - 修正真實 bug:G-5 角色建立流程誤用 source=OFFLINE_GENERATED 標記背景故事, 與 I-6「今天是否已生成離線事件」的判斷共用同一標記,已改為 SOURCE_EXTRACTION - ChatView.tsx 補上 NO_REPLY/context 為 null 的防呆(作息可用度整合後的必要調整) - scripts/smoke/I.mjs:以獨立測試角色(避免污染種子角色的排程狀態)驗證全部六項 npm run restart && npm run smoke -- I 皆通過(I-V),A~H 群組冒煙測試無回歸。
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
// I-3 時間感知:所有作息判斷一律以 Asia/Taipei 當地時間為準,不受伺服器所在時區影響。
|
||||
const TAIPEI_PARTS_FORMATTER = new Intl.DateTimeFormat("en-US", {
|
||||
timeZone: "Asia/Taipei",
|
||||
weekday: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
|
||||
const WEEKDAY_INDEX: Record<string, number> = {
|
||||
Sun: 0,
|
||||
Mon: 1,
|
||||
Tue: 2,
|
||||
Wed: 3,
|
||||
Thu: 4,
|
||||
Fri: 5,
|
||||
Sat: 6,
|
||||
};
|
||||
|
||||
export interface TaipeiTimeParts {
|
||||
weekday: number; // 0=週日 ... 6=週六
|
||||
hour: number;
|
||||
minute: number;
|
||||
year: number;
|
||||
month: number;
|
||||
day: number;
|
||||
}
|
||||
|
||||
export function taipeiParts(date: Date): TaipeiTimeParts {
|
||||
const parts = TAIPEI_PARTS_FORMATTER.formatToParts(date);
|
||||
const get = (type: string) => parts.find((p) => p.type === type)?.value ?? "";
|
||||
return {
|
||||
weekday: WEEKDAY_INDEX[get("weekday")] ?? 0,
|
||||
hour: Number(get("hour")),
|
||||
minute: Number(get("minute")),
|
||||
year: Number(get("year")),
|
||||
month: Number(get("month")),
|
||||
day: Number(get("day")),
|
||||
};
|
||||
}
|
||||
|
||||
export function minutesOfDay(date: Date): number {
|
||||
const { hour, minute } = taipeiParts(date);
|
||||
return hour * 60 + minute;
|
||||
}
|
||||
|
||||
export function isSameTaipeiDate(a: Date, b: Date): boolean {
|
||||
const pa = taipeiParts(a);
|
||||
const pb = taipeiParts(b);
|
||||
return pa.year === pb.year && pa.month === pb.month && pa.day === pb.day;
|
||||
}
|
||||
|
||||
// 回傳「當地日期」的午夜時間點(以 UTC Date 物件表示,僅用於資料庫時間比較的下界)。
|
||||
export function startOfTaipeiDay(date: Date): Date {
|
||||
const { year, month, day } = taipeiParts(date);
|
||||
// Asia/Taipei 為 UTC+8,當地 00:00 對應 UTC 前一天 16:00。
|
||||
return new Date(Date.UTC(year, month - 1, day, -8, 0, 0, 0));
|
||||
}
|
||||
Reference in New Issue
Block a user