You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

9.8 KiB

角色卡(Character Card)功能 — 架构与修改速查

日期:2026-08-16 分支:feat/character-card(从 feat/agent-independence 切出) 状态:已完成,构建 + 冒烟测试通过

本文档是角色卡模块的总结 + 修改速查。读完可快速理解全貌,并知道「想改 X 该动哪个文件」。


目录


1. 一句话概述

角色卡是与 agent 完全解耦的角色扮演体系:每个角色卡(人设/世界观/开场白/对话示例)有独立隔离的对话,运行时由 persona.ts 把卡片字段自动组装成上下文,由独立的纯文本流式引擎生成回复。

  • 入口:前台 /characters(画廊)、/characters/:slug(对话页)
  • 管理:/admin/characters(列表)、/admin/characters/:slug(编辑器)

2. 设计原则:为何独立成新模块

需求约束:每个角色独立隔离对话;引擎不改旧代码、重新写一个。

层 做法 理由
角色专属逻辑(人设组装/开场白/纯文本流式/会话) 重写(server/service/character/) 角色扮演不需要工具/审批/记忆/多步推理,独立更内聚
LLM 基础设施(模型解析/AI SDK/流式) 复用(model-resolver + ai 的 streamText) 非 agent 逻辑,是接 LLM 的原语
数据表 新增 3 张,不碰 agents 相关表 数据隔离

关键边界:现有 server/service/agent/chat-engine.ts(1021 行,agentic 专用)一行未改。角色引擎 chat-engine.ts 只有约 150 行,只做纯文本流式。

3. 数据库 Schema

文件:packages/drizzle-pkg/lib/schema/character.ts(迁移 migrations/0005_open_franklin_richards.sql)

3.1 character_cards(角色卡定义)

字段 类型 说明
id integer PK autoincrement 主键
slug text(50) unique URL 标识
name text(100) 角色名
avatar text 头像 URL(可空,前端用首字母兜底)
description text 简介
personality text 性格
scenario text 世界观 / 场景
firstMessage text 开场白(进入会话自动落库为首条 assistant 消息)
exampleDialogue text 对话示例(few-shot)
systemPrompt text default '' 底层行为规范
defaultModelId integer 默认模型
tags text JSON 数组字符串
isPublic integer default 1 是否在画廊展示
enabled integer default 1 是否启用
sortOrder integer default 0 排序
createdAt / updatedAt timestamp_ms 时间戳

3.2 character_sessions(会话)

id(text pk)、cardId(fk→cards set null)、userId(fk→users cascade)、tempToken、title(default 新对话)、modelId、lastActiveAt、expiresAt、deletedAt、createdAt、updatedAt

3.3 character_messages(消息)

id(text pk)、sessionId(fk→sessions cascade)、role、content、sortOrder、createdAt

ID 前缀:会话 cs_*,消息 cm_*(区别于 agent 的 as_* / am_*,避免 abort-manager 等全局 Map 的 key 冲突)。

4. 服务层

目录:server/service/character/

文件 职责 关键导出
types.ts 行类型 + 身份类型 CharacterCardRow/SessionRow/MessageRow, CharacterIdentity
identity.ts 解析调用方身份 resolveCharacterIdentity(复用 agent 的 temp-token 原语)
persona.ts 上下文自主设计 buildCharacterSystemPrompt(card), parseTags(tags)
character-card.ts 角色卡 CRUD get/list/create/update/deleteCharacterCard
session.ts 会话/消息 CRUD createCharacterSession, listCharacterSessions, getMessagesByCharacterSession, saveCharacterMessage 等
abort.ts 流式中断(Map 管理) register/unregister/abortCharacterStream
chat-engine.ts 纯文本流式引擎 executeCharacterChat

5. API 端点

文件:server/api/characters/ + server/api/admin/characters/

方法 路径 鉴权 说明
GET /api/characters 公开 角色卡列表(仅启用 + 公开)
GET /api/characters/:slug 公开 角色卡详情
GET /api/characters/:slug/info 公开 前端初始化信息(含全部人设字段)
GET /api/characters/:slug/models 公开 可用模型列表
GET/POST /api/characters/:slug/sessions 公开(tempToken) 会话列表 / 创建(创建时插入开场白)
GET/PUT/DELETE /api/characters/:slug/sessions/:id 公开 会话详情/更新/删除
PUT /api/characters/:slug/sessions/:id/config 公开 会话配置(modelId)
GET /api/characters/:slug/sessions/:id/messages 公开 消息列表
POST /api/characters/:slug/chat 公开 发送消息(流式)
POST /api/characters/:slug/chat/stop 公开 停止生成
POST /api/characters admin 创建角色卡
PUT/DELETE /api/characters/:slug admin 更新/删除角色卡
GET /api/admin/characters admin 列表(含禁用)
GET /api/admin/characters/:slug admin 详情(含禁用)

6. 前端

文件 职责
app/composables/useCharacterSessions.ts 会话列表/创建/切换/删除/模型配置(镜像 useAgentSessions)
app/composables/useCharacterChat.ts 纯文本流式(镜像 useAgentChat,但只处理 text-delta)
app/layouts/character.vue 对话页全屏 flex 布局
app/components/character/CharacterCard.vue 通用卡片展示(头像/名称/简介/标签),画廊复用
app/pages/characters/index.vue 角色卡画廊
app/pages/characters/[slug].vue 对话页(侧边栏 + 欢迎态 + 消息流 + 输入框)
app/pages/admin/characters/index.vue 管理列表
app/pages/admin/characters/[slug].vue 创建/编辑表单
app/pages/admin.vue 导航加了「角色卡管理」入口

7. 两个核心机制

7.1 上下文自主设计(persona.ts)

buildCharacterSystemPrompt(card) 按序组装,空字段跳过:

你将扮演以下角色,请始终以该角色的身份、语气与立场进行对话,不要跳出角色…

【角色】name
【简介】description
【性格】personality
【世界观 / 场景】scenario

【对话示例】exampleDialogue

【行为规范】systemPrompt

chat-engine.ts 中:systemPrompt = buildCharacterSystemPrompt(card),直接传给 streamText({ system })。角色卡字段→上下文,全自动成型,无散落拼接。

7.2 开场白机制

会话创建路由(sessions/index.post.ts)在 createCharacterSession 后,若 card.firstMessage 存在,调用 saveCharacterMessage 插入 role: assistant, sortOrder: 1 的首条消息。前端加载消息时即看到角色「先开口」。

会话标题:首条用户消息 → 首行截断(≤50 字),不调用 LLM(与 agent 的 titleStrategy=llm 不同)。

8. 模型解析优先级

session.modelId  ??  preferredModelId(body)  ??  card.defaultModelId  ??  全局 agentDefaultModelId
  • 复用 resolveModelForUser(登录)/ resolveModelAny(访客)+ toLanguageModel
  • 全局默认模型复用现有配置 key agentDefaultModelId(未新增 config key,避免改 registry)

9. 鉴权与白名单

文件:packages/common/config/index.ts(仅追加,未改旧条目)

  • API_ALLOWLIST:新增 10 条 /api/characters/...(公开读写,靠 tempToken 隔离)
  • FRONTEND_PAGE_ALLOWLIST:新增 /characters、/characters/:slug

tempToken 复用 agent 的 agent_temp_token cookie(本质是「访客身份」,agent 与 character 共享同一访客身份)。

10. 修改速查表

想改什么 改哪里
角色卡新增字段 schema/character.ts → db:generate + db:migrate → character-card.ts(CRUD)+ info.get.ts + 前端表单
上下文的组装顺序/措辞 server/service/character/persona.ts
流式引擎行为(温度/停止/标题) server/service/character/chat-engine.ts
开场白触发条件/内容 server/api/characters/[slug]/sessions/index.post.ts
模型解析优先级 chat-engine.ts 的 targetModelId 行
鉴权白名单 packages/common/config/index.ts
前端流式解析 app/composables/useCharacterChat.ts(processStream)
对话页 UI app/pages/characters/[slug].vue
画廊/卡片样式 app/components/character/CharacterCard.vue
后台编辑器字段 app/pages/admin/characters/[slug].vue
示例角色 packages/drizzle-pkg/seed.ts 的 seedCharacters()

11. 已知边界与后续工作

  • 会话标题不实时刷新(首条消息后侧边栏需刷新页面才更新)
  • 单会话流式,切换会话中止当前流(多实例并行未做)
  • 头像仅 URL 文本输入,未接 /api/file/upload 上传
  • 访客→登录的会话迁移未做(agent 有 migrate 链路,角色未接)
  • 未写 E2E(可复用 Mock LLM 架构补 agent-chat 式用例)

12. 验证命令

bun run db:generate   # schema 变更后生成迁移
bun run db:migrate    # 应用迁移
bun run db:seed       # 重新 seed(含示例角色)
bun run build         # 完整构建

冒烟测试(dev server 起在 5789 端口):

curl http://localhost:5789/api/characters
curl http://localhost:5789/api/characters/luna/info
curl -X POST http://localhost:5789/api/characters/luna/sessions -H 'Content-Type: application/json' -d '{}'