feat: 完成 B 群組 — 資料層與核心領域模型
- 導入 Prisma 7.9.1 + SQLite(better-sqlite3 driver adapter),prisma.config.ts 管理連線設定 - 完整資料模型:User/Work/Character/CharacterAlias/EpisodicMemory/SemanticMemory/ ProceduralRule/EmotionState/Relationship/SentimentLedgerEntry - packages/db:Prisma Client 存取層(ESM,因 Prisma 7 產出的 generated client 僅支援 ESM) - apps/api 隨之改為 ESM 以相容 packages/db - packages/shared:新增角色/記憶/情緒/關係共用型別,api 與 web 皆從此匯入 - prisma/seed.ts:建立測試使用者與元氣型測試角色,含完整關係帳本與記憶種子資料 - apps/api:新增 GET /characters - scripts/smoke/B.mjs:驗證資料表齊全、API 讀出種子角色、關係與記憶可寫入讀出 - 保留 Prisma 官方隨 CLI 附的 agent skill 文件(.agents/skills 等),供後續群組查閱 npm run restart && npm run smoke -- B 皆通過(B-V),A 群組冒煙測試無回歸。
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "users" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"name" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "works" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"title" TEXT NOT NULL,
|
||||
"volumeProgress" TEXT,
|
||||
"worldview" TEXT,
|
||||
"progressAnchor" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "characters" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"workId" TEXT,
|
||||
"source" TEXT NOT NULL,
|
||||
"buildStatus" TEXT NOT NULL DEFAULT 'CANDIDATE',
|
||||
"basicInfo" TEXT NOT NULL,
|
||||
"backgroundStory" TEXT NOT NULL,
|
||||
"personalityArchetype" TEXT NOT NULL,
|
||||
"likesDislikes" TEXT NOT NULL,
|
||||
"goalsObsessions" TEXT NOT NULL,
|
||||
"speechStyle" TEXT NOT NULL,
|
||||
"initialRelationValue" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "characters_workId_fkey" FOREIGN KEY ("workId") REFERENCES "works" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "character_aliases" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"formalName" TEXT NOT NULL,
|
||||
"nickname" TEXT,
|
||||
"calledByOthers" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "character_aliases_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "episodic_memories" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"content" TEXT NOT NULL,
|
||||
"occurredAt" DATETIME NOT NULL,
|
||||
"emotionTag" TEXT NOT NULL,
|
||||
"emotionIntensity" REAL NOT NULL,
|
||||
"retrievalCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"lastRetrievedAt" DATETIME,
|
||||
"source" TEXT NOT NULL,
|
||||
"weight" REAL NOT NULL DEFAULT 1,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "episodic_memories_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "semantic_memories" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"fact" TEXT NOT NULL,
|
||||
"about" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "semantic_memories_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "procedural_rules" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"situation" TEXT NOT NULL,
|
||||
"responsePattern" TEXT NOT NULL,
|
||||
"weight" REAL NOT NULL DEFAULT 1,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "procedural_rules_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "emotion_states" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"calm" REAL NOT NULL DEFAULT 100,
|
||||
"joy" REAL NOT NULL DEFAULT 0,
|
||||
"sad" REAL NOT NULL DEFAULT 0,
|
||||
"alert" REAL NOT NULL DEFAULT 0,
|
||||
"shy" REAL NOT NULL DEFAULT 0,
|
||||
"grumpy" REAL NOT NULL DEFAULT 0,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "emotion_states_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "relationships" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"characterId" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"intimacy" INTEGER NOT NULL DEFAULT 0,
|
||||
"trust" INTEGER NOT NULL DEFAULT 0,
|
||||
"firstMet" DATETIME,
|
||||
"lastInteraction" DATETIME,
|
||||
"interactionCount" INTEGER NOT NULL DEFAULT 0,
|
||||
"stage" TEXT NOT NULL DEFAULT 'STRANGER',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "relationships_characterId_fkey" FOREIGN KEY ("characterId") REFERENCES "characters" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||
CONSTRAINT "relationships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "sentiment_ledger_entries" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"relationshipId" TEXT NOT NULL,
|
||||
"date" DATETIME NOT NULL,
|
||||
"event" TEXT NOT NULL,
|
||||
"weight" REAL NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT "sentiment_ledger_entries_relationshipId_fkey" FOREIGN KEY ("relationshipId") REFERENCES "relationships" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "emotion_states_characterId_key" ON "emotion_states"("characterId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "relationships_characterId_userId_key" ON "relationships"("characterId", "userId");
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"private": true,
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
// Kokorone 資料模型
|
||||
// R-2 會將 provider 由 sqlite 切換為 postgresql,並為 MemoryRetriever 加上 pgvector 語意檢索。
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../packages/db/generated"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
enum CharacterSource {
|
||||
ORIGINAL // 原創
|
||||
EXISTING_WORK // 既有作品
|
||||
}
|
||||
|
||||
enum CharacterBuildStatus {
|
||||
CANDIDATE // 候補
|
||||
BUILT // 已建置
|
||||
}
|
||||
|
||||
enum MemorySource {
|
||||
INTERACTION // 互動
|
||||
OFFLINE_GENERATED // 離線生成
|
||||
SOURCE_EXTRACTION // 原作萃取
|
||||
}
|
||||
|
||||
// 情緒標籤類型:對應情緒子系統六種狀態(見 D 群組)
|
||||
enum EmotionTag {
|
||||
CALM // 平靜
|
||||
JOY // 愉悅
|
||||
SAD // 低落
|
||||
ALERT // 警戒
|
||||
SHY // 害羞
|
||||
GRUMPY // 彆扭
|
||||
}
|
||||
|
||||
enum RelationshipStage {
|
||||
STRANGER // 陌生 0-19
|
||||
ACQUAINTANCE // 認識 20-39
|
||||
FRIEND // 朋友 40-59
|
||||
CLOSE_AMBIGUOUS // 摯友-曖昧 60-79
|
||||
BONDED // 羈絆 80-100
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
relationships Relationship[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model Work {
|
||||
id String @id @default(cuid())
|
||||
title String
|
||||
volumeProgress String? // 卷數進度
|
||||
worldview String? // 世界觀
|
||||
progressAnchor String? // 作品進度錨定點(M-9/N 群組使用;描述作品實際描寫過的時期)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
characters Character[]
|
||||
|
||||
@@map("works")
|
||||
}
|
||||
|
||||
model Character {
|
||||
id String @id @default(cuid())
|
||||
workId String?
|
||||
work Work? @relation(fields: [workId], references: [id])
|
||||
|
||||
source CharacterSource
|
||||
buildStatus CharacterBuildStatus @default(CANDIDATE)
|
||||
|
||||
// 角色設定表七欄位
|
||||
basicInfo String // 基本資料:名字、年齡、外型、出身
|
||||
backgroundStory String // 背景故事:成長經歷、重大事件
|
||||
personalityArchetype String // 性格原型:傲嬌/冷淡/天然呆/元氣/大小姐/三無
|
||||
likesDislikes String // 喜好/厭惡
|
||||
goalsObsessions String // 目標/執念
|
||||
speechStyle String // 說話方式:口癖、語尾詞、第一/第二人稱
|
||||
initialRelationValue String? // 人際初始值:對誰親近、對誰有心結
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
aliases CharacterAlias[]
|
||||
episodicMemories EpisodicMemory[]
|
||||
semanticMemories SemanticMemory[]
|
||||
proceduralRules ProceduralRule[]
|
||||
emotionState EmotionState?
|
||||
relationships Relationship[]
|
||||
|
||||
@@map("characters")
|
||||
}
|
||||
|
||||
model CharacterAlias {
|
||||
id String @id @default(cuid())
|
||||
characterId String
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
formalName String // 正式名
|
||||
nickname String? // 暱稱
|
||||
calledByOthers String? // 他人稱呼(作品中其他角色如何稱呼)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("character_aliases")
|
||||
}
|
||||
|
||||
model EpisodicMemory {
|
||||
id String @id @default(cuid())
|
||||
characterId String
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
content String
|
||||
occurredAt DateTime // 發生時間
|
||||
emotionTag EmotionTag
|
||||
emotionIntensity Float // 情緒強度
|
||||
retrievalCount Int @default(0) // 提取次數
|
||||
lastRetrievedAt DateTime?
|
||||
source MemorySource
|
||||
weight Float @default(1) // 權重(供遺忘/檢索排序使用)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("episodic_memories")
|
||||
}
|
||||
|
||||
model SemanticMemory {
|
||||
id String @id @default(cuid())
|
||||
characterId String
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
fact String // 去情境化事實
|
||||
about String // 對象(關於誰/什麼)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("semantic_memories")
|
||||
}
|
||||
|
||||
model ProceduralRule {
|
||||
id String @id @default(cuid())
|
||||
characterId String
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
situation String // 情境
|
||||
responsePattern String // 回應模式
|
||||
weight Float @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("procedural_rules")
|
||||
}
|
||||
|
||||
model EmotionState {
|
||||
id String @id @default(cuid())
|
||||
characterId String @unique
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
|
||||
calm Float @default(100) // 平靜
|
||||
joy Float @default(0) // 愉悅
|
||||
sad Float @default(0) // 低落
|
||||
alert Float @default(0) // 警戒
|
||||
shy Float @default(0) // 害羞
|
||||
grumpy Float @default(0) // 彆扭
|
||||
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("emotion_states")
|
||||
}
|
||||
|
||||
model Relationship {
|
||||
id String @id @default(cuid())
|
||||
characterId String
|
||||
character Character @relation(fields: [characterId], references: [id])
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
intimacy Int @default(0) // 親密度 0~100
|
||||
trust Int @default(0) // 信任度 0~100
|
||||
firstMet DateTime?
|
||||
lastInteraction DateTime?
|
||||
interactionCount Int @default(0)
|
||||
stage RelationshipStage @default(STRANGER)
|
||||
|
||||
ledgerEntries SentimentLedgerEntry[]
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([characterId, userId])
|
||||
@@map("relationships")
|
||||
}
|
||||
|
||||
model SentimentLedgerEntry {
|
||||
id String @id @default(cuid())
|
||||
relationshipId String
|
||||
relationship Relationship @relation(fields: [relationshipId], references: [id])
|
||||
date DateTime
|
||||
event String
|
||||
weight Float // 允許正負:正向事件為正值,負向事件為負值
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@map("sentiment_ledger_entries")
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
import "dotenv/config";
|
||||
import { prisma } from "@kokorone/db";
|
||||
|
||||
async function main() {
|
||||
const userData = {
|
||||
name: "測試使用者",
|
||||
};
|
||||
const user = await prisma.user.upsert({
|
||||
where: { id: "seed-user-primary" },
|
||||
update: userData,
|
||||
create: { id: "seed-user-primary", ...userData },
|
||||
});
|
||||
|
||||
const characterData = {
|
||||
source: "ORIGINAL" as const,
|
||||
buildStatus: "BUILT" as const,
|
||||
basicInfo: "小陽,17 歲,高中二年級生,短髮、笑容燦爛,出身普通家庭",
|
||||
backgroundStory: "從小熱愛運動,國中時因為一場社團比賽的逆轉勝而確立了「全力以赴就會有轉機」的信念",
|
||||
personalityArchetype: "元氣",
|
||||
likesDislikes: "喜歡:運動、甜食、和朋友出去玩;討厭:早起、被潑冷水",
|
||||
goalsObsessions: "想在畢業前和社團一起拿到全國大賽名次",
|
||||
speechStyle: "第一人稱用「我」,語尾常帶「!」,說話直接、語速偏快",
|
||||
initialRelationValue: "對使用者:好奇又友善,容易熱絡起來",
|
||||
};
|
||||
const character = await prisma.character.upsert({
|
||||
where: { id: "seed-character-genki" },
|
||||
update: characterData,
|
||||
create: { id: "seed-character-genki", ...characterData },
|
||||
});
|
||||
|
||||
const aliasData = {
|
||||
characterId: character.id,
|
||||
formalName: "小陽",
|
||||
};
|
||||
await prisma.characterAlias.upsert({
|
||||
where: { id: "seed-alias-genki" },
|
||||
update: aliasData,
|
||||
create: { id: "seed-alias-genki", ...aliasData },
|
||||
});
|
||||
|
||||
const relationshipData = {
|
||||
characterId: character.id,
|
||||
userId: user.id,
|
||||
intimacy: 10,
|
||||
trust: 10,
|
||||
firstMet: new Date("2026-08-01T09:00:00+08:00"),
|
||||
lastInteraction: new Date("2026-08-12T21:00:00+08:00"),
|
||||
interactionCount: 3,
|
||||
stage: "ACQUAINTANCE" as const,
|
||||
};
|
||||
const relationship = await prisma.relationship.upsert({
|
||||
where: { characterId_userId: { characterId: character.id, userId: user.id } },
|
||||
update: relationshipData,
|
||||
create: { id: "seed-relationship-genki-primary", ...relationshipData },
|
||||
});
|
||||
|
||||
const ledgerData = {
|
||||
relationshipId: relationship.id,
|
||||
date: new Date("2026-08-05T18:30:00+08:00"),
|
||||
event: "使用者陪她練習到很晚,隔天比賽拿到好成績",
|
||||
weight: 8,
|
||||
};
|
||||
await prisma.sentimentLedgerEntry.upsert({
|
||||
where: { id: "seed-ledger-first-help" },
|
||||
update: ledgerData,
|
||||
create: { id: "seed-ledger-first-help", ...ledgerData },
|
||||
});
|
||||
|
||||
const emotionData = {
|
||||
calm: 70,
|
||||
joy: 20,
|
||||
sad: 0,
|
||||
alert: 0,
|
||||
shy: 0,
|
||||
grumpy: 0,
|
||||
};
|
||||
await prisma.emotionState.upsert({
|
||||
where: { characterId: character.id },
|
||||
update: emotionData,
|
||||
create: { characterId: character.id, ...emotionData },
|
||||
});
|
||||
|
||||
const episodicMemoryData = {
|
||||
characterId: character.id,
|
||||
content: "使用者在社團比賽前一晚陪她加練到很晚,隔天比賽逆轉獲勝",
|
||||
occurredAt: new Date("2026-08-05T21:00:00+08:00"),
|
||||
emotionTag: "JOY" as const,
|
||||
emotionIntensity: 0.9,
|
||||
source: "INTERACTION" as const,
|
||||
weight: 5,
|
||||
};
|
||||
await prisma.episodicMemory.upsert({
|
||||
where: { id: "seed-memory-episodic-cheer" },
|
||||
update: episodicMemoryData,
|
||||
create: { id: "seed-memory-episodic-cheer", ...episodicMemoryData },
|
||||
});
|
||||
|
||||
const semanticMemoryData = {
|
||||
characterId: character.id,
|
||||
fact: "使用者會在她練習時主動陪伴、給予鼓勵",
|
||||
about: "使用者",
|
||||
};
|
||||
await prisma.semanticMemory.upsert({
|
||||
where: { id: "seed-memory-semantic-sport" },
|
||||
update: semanticMemoryData,
|
||||
create: { id: "seed-memory-semantic-sport", ...semanticMemoryData },
|
||||
});
|
||||
|
||||
const proceduralRuleData = {
|
||||
characterId: character.id,
|
||||
situation: "使用者傳來早安問候",
|
||||
responsePattern: "以元氣滿滿的語氣回應,並附上今天的行程或心情",
|
||||
weight: 1,
|
||||
};
|
||||
await prisma.proceduralRule.upsert({
|
||||
where: { id: "seed-rule-greeting" },
|
||||
update: proceduralRuleData,
|
||||
create: { id: "seed-rule-greeting", ...proceduralRuleData },
|
||||
});
|
||||
|
||||
console.log("種子資料建立完成:", { userId: user.id, characterId: character.id });
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
Reference in New Issue
Block a user