- 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 群組冒煙測試無回歸。
77 lines
2.8 KiB
TypeScript
77 lines
2.8 KiB
TypeScript
import { Body, Controller, Get, Param, Post, Query } from "@nestjs/common";
|
|
import type { EmotionTag } from "@kokorone/shared";
|
|
import { ScheduleService } from "./schedule.service.js";
|
|
import { ScheduleExceptionService } from "./schedule-exception.service.js";
|
|
import { ScheduleEmotionBaselineService } from "./schedule-emotion-baseline.service.js";
|
|
import { OfflineEventService } from "./offline-event.service.js";
|
|
import { PrismaService } from "../prisma/prisma.service.js";
|
|
import { availabilityForStatus } from "./constants.js";
|
|
|
|
interface SpecialDayBody {
|
|
date: string;
|
|
label: string;
|
|
}
|
|
|
|
interface ExceptionBody {
|
|
userId: string;
|
|
mode: "NORMAL" | "BRIEF" | "DELAYED" | "NO_REPLY";
|
|
intimacy: number;
|
|
dominantEmotion: EmotionTag;
|
|
userInput: string;
|
|
now?: string;
|
|
}
|
|
|
|
@Controller("schedule")
|
|
export class ScheduleController {
|
|
constructor(
|
|
private readonly schedule: ScheduleService,
|
|
private readonly exception: ScheduleExceptionService,
|
|
private readonly baseline: ScheduleEmotionBaselineService,
|
|
private readonly offlineEvent: OfflineEventService,
|
|
private readonly prisma: PrismaService,
|
|
) {}
|
|
|
|
@Post(":characterId/seed-default")
|
|
async seedDefault(@Param("characterId") characterId: string) {
|
|
await this.schedule.seedDefaultSchedule(characterId);
|
|
return { ok: true };
|
|
}
|
|
|
|
@Post(":characterId/special-days")
|
|
async addSpecialDay(@Param("characterId") characterId: string, @Body() body: SpecialDayBody) {
|
|
return this.prisma.client.specialDay.create({
|
|
data: { characterId, date: new Date(body.date), label: body.label },
|
|
});
|
|
}
|
|
|
|
@Get(":characterId/info")
|
|
async getInfo(@Param("characterId") characterId: string, @Query("now") now?: string) {
|
|
const info = await this.schedule.getInfo(characterId, now ? new Date(now) : undefined);
|
|
return { ...info, availability: availabilityForStatus(info.slot?.status ?? null) };
|
|
}
|
|
|
|
@Post(":characterId/exception")
|
|
async tryException(@Param("characterId") characterId: string, @Body() body: ExceptionBody) {
|
|
const exceptionType = await this.exception.tryApply(
|
|
characterId,
|
|
body.userId,
|
|
body.mode,
|
|
{ intimacy: body.intimacy, dominantEmotion: body.dominantEmotion, userInput: body.userInput },
|
|
body.now ? new Date(body.now) : undefined,
|
|
);
|
|
return { exceptionType };
|
|
}
|
|
|
|
@Get(":characterId/baseline")
|
|
async getBaseline(@Param("characterId") characterId: string, @Query("now") now?: string) {
|
|
const baseline = await this.baseline.computeBaseline(characterId, now ? new Date(now) : undefined);
|
|
return { baseline };
|
|
}
|
|
|
|
@Post(":characterId/offline-event")
|
|
async generateOfflineEvent(@Param("characterId") characterId: string, @Body() body: { now?: string }) {
|
|
await this.offlineEvent.maybeGenerate(characterId, body.now ? new Date(body.now) : undefined);
|
|
return { ok: true };
|
|
}
|
|
}
|