Compare commits

..

162 Commits

Author SHA1 Message Date
peanut 05ea918ee1 feat(script-favorite): update toggleFavorite to use backend API
- Import toggleFavoriteScript from epicScript service
- Change toggleFavorite from sync local-only to async with API call
- Implement optimistic update pattern: update local cache first, then call backend
- Rollback on API failure with error toast
- Preserves localFavorites for offline/fast-render scenarios
2026-07-19 21:51:20 +08:00
peanut 50f1bdba38 feat(script-favorite): add favorite API wrappers to epicScript.js
- toggleFavoriteScript: POST /epicScript/favorite/toggle
- getFavoriteScriptPage: GET /epicScript/favorite/page
- checkFavoriteScript: GET /epicScript/favorite/check
- Added to default export object
2026-07-19 21:48:00 +08:00
peanut 74fa304cd9 feat(script-favorite): add 3 favorite endpoints to EpicScriptController
- POST /epicScript/favorite/toggle: toggle favorite for a script
- GET /epicScript/favorite/page: paginated favorite list
- GET /epicScript/favorite/check: check if script is favorited
- Add ScriptFavoriteToggleRequest, ScriptFavoriteResponse, ScriptFavoriteService imports
- Inject ScriptFavoriteService via @Autowired
2026-07-19 21:43:46 +08:00
peanut 9d352f40a0 feat(script-favorite): add ScriptFavoriteServiceImpl
- toggleFavorite: upsert via logical-delete check + save, @Transactional
- getFavoritePage: paginate favorites then fetch EpicScript details
- getFavoritedScriptIds: batch IN query for checking multiple scripts
- isFavorited: count > 0 check for single script
- getFavoriteCount: total count for current user
- Uses UserContextHolder for userId, consistent with project pattern
2026-07-19 21:41:54 +08:00
peanut 674188f471 feat(script-favorite): add ScriptFavoriteService interface
- toggleFavorite: toggle favorite status for a script
- getFavoritePage: paginated list of user's favorite scripts
- getFavoritedScriptIds: batch check which scripts are favorited
- isFavorited: check single script favorite status
- getFavoriteCount: count user's total favorites
2026-07-19 21:41:28 +08:00
peanut 62ca1730b9 feat(script-favorite): add ScriptFavoriteResponse DTO
- Fields: scriptId, isFavorited, favoriteTime
- Used by toggle/check endpoints to return favorite status
2026-07-19 21:41:14 +08:00
peanut 0d27d1a072 feat(script-favorite): add ScriptFavoriteToggleRequest DTO
- Single field: scriptId with @NotBlank validation
- Uses javax.validation.constraints.NotBlank matching project convention
2026-07-19 21:41:02 +08:00
peanut d74d8b22c6 feat(script-favorite): add ScriptFavoriteMapper
- Extends BaseMapper<ScriptFavorite>
- Standard MyBatis-Plus mapper pattern matching project convention
2026-07-19 21:40:06 +08:00
peanut 489131a04b feat(script-favorite): add ScriptFavorite entity
- Extends BaseEntity for id, createTime, updateTime, isDeleted
- Maps to t_script_favorite table via MyBatis-Plus @TableName
- Fields: userId, scriptId
2026-07-19 21:39:47 +08:00
peanut 43ac037f5b feat(script-favorite): add t_script_favorite migration SQL
- Create t_script_favorite table with user_id, script_id
- Add unique key uk_user_script (user_id, script_id)
- Add indexes idx_user_id and idx_script_id
- Support logical delete via is_deleted flag
2026-07-19 21:39:27 +08:00
peanut b20f30f91b feat(script-favorite): add database migration and backend service
- Add t_script_favorite table migration SQL
- Add ScriptFavorite entity, mapper, service interface and impl
- Add toggle/page/check endpoints in EpicScriptController
- Add toggleFavorite/getFavoritePage/checkFavorite in epicScript.js
- Update ScriptLibraryView with backend-backed toggleFavorite
2026-07-19 21:38:53 +08:00
peanut 7e52bb9ffb fix: .chat-send-btn 设 height:60rpx 与输入框等高 2026-07-19 21:04:50 +08:00
peanut 8a2001af3d docs: Chat 视图发送按钮等高修复设计文档 2026-07-19 21:04:04 +08:00
peanut 35b4f97527 fix: .chat-scroll-content padding-bottom 改回 40rpx 避免与父级 padding 叠加 2026-07-19 20:49:16 +08:00
peanut f5b26c2fc7 refactor: .chat-page 改为 height: 100% 让父级 padding 生效 2026-07-19 20:49:05 +08:00
peanut cb965fadff docs: Chat 视图底部遮挡架构层修复实施计划 2026-07-19 20:46:10 +08:00
peanut cafa8717ab docs: Chat 视图底部遮挡架构层修复设计文档 2026-07-19 20:45:09 +08:00
peanut 283914690a fix: .chat-page 增加 safe-area-inset-bottom + .chat-scroll-content padding-bottom 增至 120rpx 2026-07-19 20:21:41 +08:00
peanut 04ffa2dceb docs: Chat 视图底部内边距修复实施计划 2026-07-19 20:17:26 +08:00
peanut 0a9ea60795 docs: Chat 视图底部内边距修复设计文档 2026-07-19 19:57:57 +08:00
peanut 118d20914d fix: 修复 openScriptChat 设置 viewState='result' 导致页面空白的问题 2026-07-19 19:30:48 +08:00
peanut b194b343ad fix: 修复 mp-weixin 模板箭头函数和 TextDecoder 不兼容
- 微信小程序 wxml 模板不支持内联箭头函数
  @submit 改为方法引用,submitClarification 自己找未提交卡片
- TextDecoder 在 mp-weixin 不可用
  decodeChunk 改用纯 JS UTF-8 解码实现
2026-07-19 19:02:52 +08:00
peanut 7a0e729ab4 fix: chat-page 用 height: 100vh 替代 min-height: 100vh 修复真机滚动
根因:min-height: 100vh 允许容器超出视口增长,破坏 flex 布局,
导致 scroll-view 内部滚动层高度=内容高度(都不滚动)。

参考项目内已工作的 .result-page(height: 100% + min-height: 0)。
H5 实际验证:内容 1779px 滚动到底部后提交按钮 visibleInViewport: true
2026-07-19 18:50:57 +08:00
peanut f07817aaff fix: chat 视图 scroll-view 添加 height: 100% + min-height: 0
参考项目内已工作的 .result-view 模式:
- flex: 1 仅在 flex 布局父容器中生效
- 但 UniApp <scroll-view> 是原生组件,必须显式 height: 100%
- 同时 min-height: 0 确保 flex 子项可收缩

之前只设 flex: 1,真机上原生 scroll-view 无法计算可滚动区域。
2026-07-19 18:32:09 +08:00
peanut 30ecf9f4a3 fix: chat 视图 scroll-view 删除固定 height,改用 flex: 1 自适应真机高度
真机上状态栏/tab 栏/安全区域高度各不相同,
硬编码 calc(100vh - 220rpx) 导致 scroll-view 超出可视区域无法滚动。
2026-07-19 18:07:00 +08:00
peanut f6ef0222b3 fix: 修复 chat 视图无法滚动 + SSE 事件重复消费
- scrollResultToLatest 守卫条件兼容 chat 视图
- shortNovel.js 添加 chunkProcessed 标志位避免 H5 双重消费
2026-07-19 16:04:54 +08:00
peanut e109e59f1a feat(mini-program): sendChat 按 generationPhase 切换接口,消息进统一对话流 2026-07-19 15:24:49 +08:00
peanut e41266e6f4 feat(mini-program): 统一 chat 视图,删除 clarifying/outlining/result 独立视图 2026-07-19 15:21:43 +08:00
peanut b4fca0abfa refactor(mini-program): runGeneration+交互函数改为操作对话消息 2026-07-19 15:15:49 +08:00
peanut 84b6a77632 refactor(mini-program): handleShortNovelEvent 改为对话流追加消息 2026-07-19 15:10:42 +08:00
peanut c0adb48949 refactor(mini-program): 扩展 addResultMessage 消息模型 + 新增 generationPhase 2026-07-19 15:05:27 +08:00
peanut 25d8a9f8f2 docs:ScriptView 统一对话页改造实现计划
6 个任务:扩展消息模型、handleShortNovelEvent 改对话流、
runGeneration+交互函数改造、统一 chat 视图模板、
sendChat 接口切换、端到端浏览器验证
2026-07-19 14:55:27 +08:00
peanut b2df7d4de0 docs:ScriptView 统一对话页改造设计
将澄清/大纲/小说生成从独立视图改为统一对话流消息。
viewState 简化为 home+chat,扩展 resultMessages 消息模型
支持 kind: text/card/outline/novel,novel_done 后切换旧接口
做后续对话修改。
2026-07-19 14:45:45 +08:00
peanut 30e2836742 fix(mini-program): 修复心愿实现页接口报错 + .env 指向生产服务器
根因:.env.development 指向 localhost:19089,但本地未启动后端,
所有 API 请求 ERR_CONNECTION_REFUSED。

切换至 https://lifescript.happylifeos.com/api(生产服务器),
符合 CLAUDE.md 服务器端验收规则。

此修复与 generationDisplayText + 继续会话 一起完成完整心愿实现流程验证。
2026-07-19 14:17:11 +08:00
peanut c1293c28ac fix(mini-program): 修复心愿实现页用户消息不显示 + 支持继续会话
1. runGeneration 补赋值 generationDisplayText,修复用户输入心愿不显示
2. 识别外部服务 DAILY_GENERATION_IN_PROGRESS 错误码,保存 session_id
3. 失败态新增'继续创作'按钮,调用 followup action=retry 恢复中断会话
4. 新增 resumeSession 方法,重置状态后恢复流式生成
2026-07-19 13:58:16 +08:00
peanut d9a061a6bc fix(deploy): 使用 shlex.quote 防止 curl URL 命令注入 2026-07-19 13:27:03 +08:00
peanut e313086d9b fix(deploy): Windows 平台 curl 验证使用 NUL 替代 /dev/null
Windows cmd.exe 不支持 /dev/null(那是 Unix 概念),
导致 curl 写入失败返回 exit code 23,验证误报为'访问异常'。
改为根据 sys.platform 动态选择 null 设备。
2026-07-19 13:25:01 +08:00
peanut 1b566d574c fix(mini-program): ScriptView 模板闭合标签和 setupRecorder H5 兼容性 2026-07-19 12:56:38 +08:00
peanut 0190941df7 fix(mini-program): 短信验证码文案从 888888 改为 123456 2026-07-19 12:39:54 +08:00
peanut 34f443939e feat(mini-program): ScriptView 多阶段状态机改造 2026-07-19 12:39:50 +08:00
peanut b3a2f3a3d3 feat(mini-program): 澄清卡片组件 2026-07-19 12:36:11 +08:00
peanut ab13ac258b feat(mini-program): 短篇小说 SSE 流式服务 2026-07-19 12:36:06 +08:00
peanut 00f17d2f47 test(server): ShortNovelController 集成测试 2026-07-19 12:22:25 +08:00
peanut c06b04869b feat(server): 短篇小说 SSE 代理 Controller 2026-07-19 12:21:09 +08:00
peanut 02c58767bd fix(server): SSE 代理错误消息不再暴露原始异常信息给客户端 2026-07-19 12:18:54 +08:00
peanut 7018f5780a feat(server): 短篇小说服务 SSE 代理实现 2026-07-19 12:15:50 +08:00
peanut 031d824dd0 refactor(server): 抽取 saveNovelResult 方法供短篇小说服务复用 2026-07-19 12:08:54 +08:00
peanut 8f96239b56 style(server): 统一 ShortNovel 配置类和 DTO 的字段注释风格 2026-07-19 12:07:26 +08:00
peanut 21ce56c6da feat(server): 短篇小说外部服务配置类和请求 DTO 2026-07-19 12:02:17 +08:00
peanut ce244f9ad5 docs:短篇小说服务接口迁移实现计划
- 后端 SSE 代理层(Controller + Service + Config + DTO)
- 数据持久化 saveNovelResult 复用方法
- 前端 shortNovel.js SSE 流式服务
- ClarificationCard 组件 + ScriptView 状态机改造
- 短信验证码文案修复 + 端到端验收
2026-07-19 11:57:53 +08:00
peanut 99a3cb883e docs:短篇小说服务接口迁移设计方案
- 后端 SSE 代理 + 前端多轮交互状态机改造
- 外部 short-novel-service 接口替代内部 AI Runtime
- 数据持久化改为小说完成后一次性保存
- 短信验证码修复(888888→123456)
2026-07-19 11:52:41 +08:00
peanut 2c778fabbb refactor: ScriptView 删除 read 模式 + 兼容旧模式,统一对话流入口 2026-07-01 08:28:47 +08:00
peanut 6aeb5319e3 refactor: ScriptView 删除 read 模式,剧本入口直接进对话流 2026-07-01 07:59:48 +08:00
peanut 3bb6716e0f docs: ScriptView 简化为纯对话流模式设计文档 2026-06-30 22:45:29 +08:00
peanut 86543df75d fix: currentScriptContent 章节字段全空时用 plotJson.fullContent 作为 fallback 2026-06-30 07:45:20 +08:00
peanut 1073123360 fix: createMessage 补 userId 字段 + 使用 senderType/contentType 2026-06-29 23:52:37 +08:00
peanut 3829a3ef92 fix: createMessage 字段名改为 senderType/contentType 匹配后端 DTO 2026-06-29 23:36:26 +08:00
peanut f45fee7cce feat: chat 模式传 messageType/versionLabel/新 emits 给 MessageCard 2026-06-29 22:53:36 +08:00
peanut a713e8823c fix: loadMessages 对历史剧本 plotJson 自动初始化首条 AI 消息 2026-06-29 22:50:15 +08:00
peanut 4a7914eefe fix: displayMessages 去掉根版本过滤(chat 模式显示首条 AI 剧本) 2026-06-29 22:47:37 +08:00
peanut 88ff280b78 feat: MessageCard 支持按 messageType 条件渲染 script/chat 专属按钮组 2026-06-29 22:45:36 +08:00
peanut 912fc42860 fix: 剧本查询时自动为历史剧本补建 conversation(兼容前端临时 ID) 2026-06-29 22:40:59 +08:00
peanut 7fb90299d1 docs: ScriptView 对话模式按钮完整修复设计文档 2026-06-29 22:34:27 +08:00
peanut 6ae721eb89 fix: 回滚 ScriptView 临时 conversationId 正则保护(误伤历史剧本) 2026-06-28 23:33:29 +08:00
peanut 808a590b8d fix: nginx 根路径 301 重定向到 /emotion-museum/(修复首页 404) 2026-06-28 23:21:03 +08:00
peanut 19337b9371 fix: ScriptView loadMessages 对临时 conversationId 正则短路保护 2026-06-28 22:42:24 +08:00
peanut e3eb1afbb0 fix: listByConversation 在 conversation 不存在时返回空列表而非抛异常 2026-06-28 22:40:13 +08:00
peanut caaeb94477 feat: 增强 download-server-log.py 支持 latest/tail/errors/grep 子命令 2026-06-28 22:23:56 +08:00
peanut bd3ccbd757 test: 新增 EpicScriptDialogueService 未登录场景测试 2026-06-28 18:49:10 +08:00
peanut bf6f2797ad feat: ScriptView 改造为对话中心查看/修改模式
- 新增 scriptChat.js 和 message.js 服务导入
- 新增 viewMode、scriptId、conversationId、currentVersionMessageId、
  selectedVersionMessageId、messages、versions、chatInput 等状态
- 新增阅读模式:版本栏 + 剧本分章节展示 + 操作按钮
- 新增聊天模式:消息列表 + 聊天输入栏 + 对话修改功能
- 集成 createScriptWithDialogue、streamScriptChat、listMessagesByConversation、
  listMessageVersions、switchVersion、deleteVersion 等后端 API
- 替换 runGeneration 使用 createScriptWithDialogue 创建剧本和对话
- 替换 openScriptChat 使用后端消息加载,不再依赖 localStorage
- 移除 localStorage 作为主要消息存储(persistResultMessages、
  hydrateResultMessages、readStoredMessages、readScriptChatHistory 改为空操作)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:41:44 +08:00
peanut 6109202a26 chore: request.js 导出 API_BASE_URL 和 getAuthHeader 2026-06-28 18:26:30 +08:00
peanut 3b9a8222de feat: 新增 ScriptChatController 统一流式接口 2026-06-28 18:16:07 +08:00
peanut 8537c27015 feat: MessageController 新增 listByConversation、versions、deleteVersion 2026-06-28 18:09:07 +08:00
peanut a66a78a0f3 feat: EpicScriptController 新增 createWithDialogue 和 switchVersion 2026-06-28 18:09:06 +08:00
peanut 57f1e311b6 feat: 实现 EpicScriptService.switchVersion
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:00:27 +08:00
peanut 8618b8b80f feat: EpicScriptService 新增 switchVersion 方法声明
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 18:00:22 +08:00
peanut b11a3463b2 feat: 实现剧本消息版本管理服务
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:42:11 +08:00
peanut e252dc5fd2 feat: 新增 ScriptMessageService 接口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:42:05 +08:00
peanut 0520f30d84 feat: 新增 ScriptChatService 接口 2026-06-28 17:36:02 +08:00
peanut 752fd30686 fix: 修复剧本对话创建服务实现问题
- 添加系统欢迎语消息(messageOrder=1, sender=system)
- 调整用户消息顺序为 messageOrder=2
- 添加 AI 助手开场白消息(messageOrder=3, sender=assistant, type=script)
- 从 AI 生成结果中解析标题并更新到剧本实体
- 修复 messageCount 为 3 以匹配实际消息数量
- 删除不再需要的 buildTitle 方法
- 新增 buildSystemWelcome 方法构建系统欢迎语

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:24:47 +08:00
peanut 56369fdeb2 feat: 新增 EpicScriptDialogueService 接口
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 17:15:47 +08:00
peanut 88d586bab1 feat: MessageResponse 新增 scriptId、parentMessageId、versionNumber、metadata、status、timestamp 2026-06-28 17:08:31 +08:00
peanut 0149bac281 feat: EpicScriptResponse 新增 conversationId 和 currentVersionMessageId 2026-06-28 17:08:26 +08:00
peanut 42d1350aff feat: 新增 SwitchVersionRequest 2026-06-28 17:08:22 +08:00
peanut 43622be5b0 feat: 新增 ScriptChatStreamRequest 2026-06-28 17:08:18 +08:00
peanut 74183ccb30 feat: 新增 EpicScriptCreateWithDialogueResponse 2026-06-28 17:08:13 +08:00
peanut 06b2ee58e2 feat: 新增 EpicScriptCreateWithDialogueRequest 2026-06-28 17:08:09 +08:00
peanut 83c379d5d5 feat: Message 实体新增 scriptId 和 versionNumber 字段 2026-06-28 17:01:52 +08:00
peanut 15fce9eadf feat: Conversation 实体新增 scriptId 和 currentMessageId 字段 2026-06-28 16:56:58 +08:00
peanut 0ba2960c1b feat: EpicScript 实体新增 conversationId 和 currentVersionMessageId 字段 2026-06-28 16:56:57 +08:00
peanut 0e2cc42a41 chore: 新增剧本对话关联字段迁移脚本 2026-06-28 16:50:31 +08:00
peanut 6bc00863fa docs: 更新本地服务管理规则
- 强制使用 dev-services.py 控制本地服务
- 明确前端/H5 固定端口 5178-5181
- 新增禁止无意义重启规则

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 10:40:28 +08:00
peanut 1b39d873ab chore: 统一前端/H5 服务固定端口
- web: 5178
- web-admin: 5179
- mini-program: 5180
- life-script: 5181
2026-06-28 10:37:36 +08:00
peanut 75de32828a feat: dev-services.py restart 命令新增热加载保护
- 无必须重启的变更时禁止 restart
- 支持 --force 参数强制重启
- 避免无意义重启前端热加载服务
2026-06-28 10:34:54 +08:00
peanut 4dc3d4cfdc feat: dev-services.py 强制前端服务使用固定端口
- 新增 FIXED_FRONTEND_PORTS 映射
- 前端/H5 端口从 5178 开始固定分配
- 端口冲突时不再自动递增前端服务端口,改为报错
2026-06-28 10:30:50 +08:00
peanut a43bb1a555 docs:新增本地服务管理规则优化实现计划 2026-06-28 10:20:54 +08:00
peanut 5b31845b33 docs:新增本地服务管理规则优化设计文档
- 强制使用 dev-services.py 控制本地服务
- 前端/H5 固定端口从 5178 开始累加
- 优化 restart 命令,避免无意义重启
2026-06-28 10:16:29 +08:00
peanut 8cd52f76ae refactor: 迁移 story-card 相关样式到 MessageCard
删除 ScriptView.vue 中已迁移到 MessageCard 的样式类

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:19:53 +08:00
peanut c2ed051de0 feat: ScriptView 复用 MessageCard 组件
- story-card 使用 MessageCard
- result-chat-list 中的 assistant 消息按内容长度自动切换短消息/完整卡片模式

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 09:16:37 +08:00
peanut cbf5157dcf feat: 创建 MessageCard 组件
封装长消息/故事卡片结构,支持短消息简洁气泡模式
2026-06-28 09:13:34 +08:00
peanut 8dc2454beb docs:新增 MessageCard 组件实现计划 2026-06-28 09:06:32 +08:00
peanut f7dba6ba60 docs:新增 MessageCard 组件统一设计文档
- 提取 MessageCard.vue 独立组件
- 按 100 字阈值自动切换短消息/完整卡片模式
- story-card 和 result-chat-list 复用同一组件
2026-06-27 23:25:26 +08:00
peanut 847e80aa0e docs:新增 AI 消息卡片功能按钮实现计划 2026-06-27 22:29:27 +08:00
peanut d724cab581 style: 添加消息按钮组样式
- .message-actions: 2 列网格布局,间距 14rpx
2026-06-27 22:22:28 +08:00
peanut 0db434c21f feat: 在 result-chat-list 中添加功能按钮组
为每条 assistant 消息添加完整功能按钮:
- 复制:复制消息内容到剪贴板
- 换个方向:进入换方向修订确认
- 不像我:进入不像我修订确认
- 继续生成:收起故事并聚焦输入框
- 播放:播放故事 TTS 音频
2026-06-27 22:20:28 +08:00
peanut 9b3006d128 feat: 添加消息卡片功能方法
- copyMessageContent: 复制消息内容到剪贴板
- playMessageTts: 播放故事 TTS 音频
- toggleMessageCollapse: 添加展开/收起埋点
2026-06-27 22:18:11 +08:00
peanut 944f6a0a13 fix: 修复 story-card 展开失效 bug
移除 resultHasScriptReplies 条件包裹,让故事正文始终由 storyCollapsed 控制展开/收起
2026-06-27 22:16:37 +08:00
peanut e014450aa3 docs:新增 AI 消息卡片功能按钮统一设计文档
- 修复 story-card 展开失效 bug
- 为每条 AI 消息添加完整功能按钮组
- 采用方案 A:在现有单文件组件内修复 + 扩展
2026-06-27 20:40:37 +08:00
peanut 2198781c68 chore: 从版本控制中移除 .idea 目录(已在 .gitignore 中) 2026-06-27 17:42:12 +08:00
peanut 8e6b69f6c7 fix: 修复遗漏的 backend-single 引用(Java源码/前端注释/配置文件/IDE配置) 2026-06-27 17:32:18 +08:00
peanut 591f70a2f6 docs: 批量更新历史文档中的 backend-single 引用为 server 2026-06-27 17:27:27 +08:00
peanut 5586dc5eac docs: 更新核心文档和前端代码中的后端目录引用为 server 2026-06-27 17:26:50 +08:00
peanut 8f51701f54 refactor: 更新根部署脚本和 .gitignore 中的后端目录引用为 server 2026-06-27 17:25:54 +08:00
peanut 6cb9496abb refactor: 更新 Maven artifactId 和部署脚本 JAR 名称为 server 2026-06-27 17:24:46 +08:00
peanut 0d77d76858 refactor: 重命名 backend-single 目录为 server 2026-06-27 17:23:53 +08:00
peanut 8daf51301a fix: 修复小程序登录页变量初始化顺序错误导致微信登录失败 2026-06-27 13:07:17 +08:00
peanut e3b21cac3e feat:管理后台系统设置页面 + 小程序登录页条件渲染 2026-06-27 10:27:14 +08:00
peanut 9131203d1c feat:新增系统配置模块(Entity/Mapper/DTO/Service/Controller)+ 登录配置公开接口 2026-06-27 10:25:17 +08:00
peanut ed741ac8bf docs:短信登录开关与通用系统配置实现计划 2026-06-27 10:18:00 +08:00
peanut 4b235fa5d3 docs:短信登录开关与通用系统配置设计文档 2026-06-27 10:09:24 +08:00
peanut 7679e973d0 feat:管理页删除确认接入自定义弹窗,标签文字不换行 2026-06-24 22:51:12 +08:00
peanut f11fb142e1 feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗 2026-06-24 22:47:15 +08:00
peanut 57a42474d7 refactor:TagDialog 移除冗余 glass-card 类引用 2026-06-24 22:43:56 +08:00
peanut bd0f4d6bea feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式 2026-06-24 22:40:43 +08:00
peanut 637334adb8 docs:添加按钮简化与自定义弹窗实现计划 2026-06-24 22:37:48 +08:00
peanut 1900ba6efc docs:添加标签按钮简化与自定义弹窗设计 2026-06-24 22:31:54 +08:00
peanut 11f1638eef docs:添加强制规则,禁止本地后端验收,必须使用 deploy.py 部署到服务器验收 2026-06-24 22:17:06 +08:00
peanut 843ddd4c69 feat:新增管理标签独立页面,支持左滑与 × 按钮删除 2026-06-24 21:56:46 +08:00
peanut 53e895797e fix:onShow 重复触发时先 off 再 on,避免事件监听累积 2026-06-24 21:55:51 +08:00
peanut 2600cb2164 feat:编辑资料页保存携带库字段,onShow 监听管理页更新事件 2026-06-24 21:53:26 +08:00
peanut c7e84f5a13 style:删除 .custom 死代码 CSS 块 2026-06-24 21:51:41 +08:00
peanut 7dc75cbfb4 feat:编辑资料页标签区 UI 重构,新增管理入口与添加标签弹窗 2026-06-24 21:46:02 +08:00
peanut 071af02482 feat:编辑资料页 form 增加库字段并对老用户兼容初始化
- form reactive 新增 personalityTagLibrary 和 hobbyLibrary 字段
- syncFromStore 增加库字段加载逻辑,后端有则用后端数据
- 老用户(后端无库字段)使用预设 + 已选合并去重初始化
- 新增 buildInitialLibrary 辅助函数处理数组合并去重
2026-06-24 21:40:34 +08:00
peanut a446848d8e feat:前端数据层与路由注册支持标签库字段 2026-06-24 21:36:31 +08:00
peanut 341b08c02e feat:UserProfile 新增性格标签库与兴趣爱好库字段
- t_user_profile 表新增 personality_tag_library 和 hobby_library 两个 TEXT 字段
- UserProfile 实体类新增对应字段,使用 @TableField 映射
- UserProfileUpdateRequest、UserProfileCreateRequest、UserProfileResponse DTO 同步新增字段
- 迁移脚本命名符合 V{YYYYMMDD}__描述.sql 规范

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 21:30:11 +08:00
peanut 433ba4c498 docs:小程序标签库扩展功能实现计划 2026-06-24 21:26:53 +08:00
peanut 1cdee47f3a docs:小程序编辑资料页性格标签与兴趣爱好库扩展设计 2026-06-24 21:20:41 +08:00
peanut 3cd7de1f55 fix:ScriptView 语音按钮改为麦克风图标 2026-06-23 22:56:15 +08:00
peanut 1c45689f03 fix:ScriptView 底部输入框区域增加左右边距 2026-06-23 22:54:52 +08:00
peanut 99a06fcc28 fix:ScriptView 结果页顶部操作栏固定不随滚动 2026-06-23 22:54:11 +08:00
peanut 4cb6d6de9b docs:ScriptView 对话结果页 UI 调整实现计划 2026-06-23 22:51:55 +08:00
peanut 6b6af757d0 docs:ScriptView 对话结果页 UI 调整设计文档 2026-06-23 22:49:15 +08:00
peanut ae1463e400 fix:剧本列表工具函数去除默认值兜底 2026-06-23 22:19:30 +08:00
peanut 70434c1325 fix:剧本列表删除 fallbackScripts 假数据 2026-06-23 22:18:32 +08:00
peanut 04b184a3c1 fix:编辑资料页无昵称时显示未设置昵称 2026-06-23 22:17:45 +08:00
peanut e7531f86cf docs:去除编辑资料页与剧本列表 mock 数据实现计划 2026-06-23 22:16:27 +08:00
peanut a00ce0bf95 docs:去除编辑资料页与剧本列表 mock 数据设计文档 2026-06-23 22:14:31 +08:00
peanut 0eaf44ed93 fix:编辑资料页昵称未填时增加 Toast 提示 2026-06-23 21:47:32 +08:00
peanut eb50358e3d docs:小程序编辑资料页昵称必填提示优化实现计划 2026-06-23 21:34:53 +08:00
peanut 41ed8257f6 docs:小程序编辑资料页昵称必填提示优化设计文档 2026-06-23 21:29:54 +08:00
peanut 2c69da46e5 fix:修复个人档案性别字段保存后在我的页面不显示的问题(中英文格式兼容) 2026-06-15 12:13:49 +08:00
peanut 42543ff8a6 feat:结果页聊天发送消息时显示与心愿生成一致的动态加载效果 2026-06-15 12:01:12 +08:00
peanut c744d92f38 feat:MineView 个人档案调整为两行四列,移除个人中心和 MBTI 2026-06-15 11:42:10 +08:00
peanut 04cdd5585e feat:RecordView 移除 profile-card 相关样式 2026-06-15 10:53:57 +08:00
peanut a62257b21a feat:RecordView 移除无用的 computed 和方法 2026-06-15 10:52:22 +08:00
peanut 816e166e08 feat:RecordView 移除 profile-card 模板 2026-06-15 10:51:30 +08:00
peanut b9bacb36e7 feat:MineView 新增个人档案区块样式 2026-06-15 10:50:48 +08:00
peanut ffa77400c9 feat:MineView 新增个人档案区块模板 2026-06-15 10:50:04 +08:00
peanut d1e223ad23 feat:MineView 新增个人档案字段 computed 属性 2026-06-15 10:49:11 +08:00
peanut 83d3f87cf6 docs:修正实现计划中 .star 样式的保留说明 2026-06-15 10:47:03 +08:00
peanut 16cee44a85 docs:个人信息组件合并实现计划 2026-06-15 10:46:21 +08:00
peanut 49f73679d0 docs:个人信息组件合并设计文档 2026-06-15 10:43:42 +08:00
632 changed files with 19768 additions and 3910 deletions
-81
View File
@@ -1,81 +0,0 @@
---
type: "always_apply"
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
-184
View File
@@ -1,184 +0,0 @@
---
description: Perform a non-destructive cross-artifact consistency and quality analysis across spec.md, plan.md, and tasks.md after task generation.
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Goal
Identify inconsistencies, duplications, ambiguities, and underspecified items across the three core artifacts (`spec.md`, `plan.md`, `tasks.md`) before implementation. This command MUST run only after `/speckit.tasks` has successfully produced a complete `tasks.md`.
## Operating Constraints
**STRICTLY READ-ONLY**: Do **not** modify any files. Output a structured analysis report. Offer an optional remediation plan (user must explicitly approve before any follow-up editing commands would be invoked manually).
**Constitution Authority**: The project constitution (`.specify/memory/constitution.md`) is **non-negotiable** within this analysis scope. Constitution conflicts are automatically CRITICAL and require adjustment of the spec, plan, or tasks—not dilution, reinterpretation, or silent ignoring of the principle. If a principle itself needs to change, that must occur in a separate, explicit constitution update outside `/speckit.analyze`.
## Execution Steps
### 1. Initialize Analysis Context
Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` once from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS. Derive absolute paths:
- SPEC = FEATURE_DIR/spec.md
- PLAN = FEATURE_DIR/plan.md
- TASKS = FEATURE_DIR/tasks.md
Abort with an error message if any required file is missing (instruct the user to run missing prerequisite command).
For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
### 2. Load Artifacts (Progressive Disclosure)
Load only the minimal necessary context from each artifact:
**From spec.md:**
- Overview/Context
- Functional Requirements
- Non-Functional Requirements
- User Stories
- Edge Cases (if present)
**From plan.md:**
- Architecture/stack choices
- Data Model references
- Phases
- Technical constraints
**From tasks.md:**
- Task IDs
- Descriptions
- Phase grouping
- Parallel markers [P]
- Referenced file paths
**From constitution:**
- Load `.specify/memory/constitution.md` for principle validation
### 3. Build Semantic Models
Create internal representations (do not include raw artifacts in output):
- **Requirements inventory**: Each functional + non-functional requirement with a stable key (derive slug based on imperative phrase; e.g., "User can upload file" → `user-can-upload-file`)
- **User story/action inventory**: Discrete user actions with acceptance criteria
- **Task coverage mapping**: Map each task to one or more requirements or stories (inference by keyword / explicit reference patterns like IDs or key phrases)
- **Constitution rule set**: Extract principle names and MUST/SHOULD normative statements
### 4. Detection Passes (Token-Efficient Analysis)
Focus on high-signal findings. Limit to 50 findings total; aggregate remainder in overflow summary.
#### A. Duplication Detection
- Identify near-duplicate requirements
- Mark lower-quality phrasing for consolidation
#### B. Ambiguity Detection
- Flag vague adjectives (fast, scalable, secure, intuitive, robust) lacking measurable criteria
- Flag unresolved placeholders (TODO, TKTK, ???, `<placeholder>`, etc.)
#### C. Underspecification
- Requirements with verbs but missing object or measurable outcome
- User stories missing acceptance criteria alignment
- Tasks referencing files or components not defined in spec/plan
#### D. Constitution Alignment
- Any requirement or plan element conflicting with a MUST principle
- Missing mandated sections or quality gates from constitution
#### E. Coverage Gaps
- Requirements with zero associated tasks
- Tasks with no mapped requirement/story
- Non-functional requirements not reflected in tasks (e.g., performance, security)
#### F. Inconsistency
- Terminology drift (same concept named differently across files)
- Data entities referenced in plan but absent in spec (or vice versa)
- Task ordering contradictions (e.g., integration tasks before foundational setup tasks without dependency note)
- Conflicting requirements (e.g., one requires Next.js while other specifies Vue)
### 5. Severity Assignment
Use this heuristic to prioritize findings:
- **CRITICAL**: Violates constitution MUST, missing core spec artifact, or requirement with zero coverage that blocks baseline functionality
- **HIGH**: Duplicate or conflicting requirement, ambiguous security/performance attribute, untestable acceptance criterion
- **MEDIUM**: Terminology drift, missing non-functional task coverage, underspecified edge case
- **LOW**: Style/wording improvements, minor redundancy not affecting execution order
### 6. Produce Compact Analysis Report
Output a Markdown report (no file writes) with the following structure:
## Specification Analysis Report
| ID | Category | Severity | Location(s) | Summary | Recommendation |
|----|----------|----------|-------------|---------|----------------|
| A1 | Duplication | HIGH | spec.md:L120-134 | Two similar requirements ... | Merge phrasing; keep clearer version |
(Add one row per finding; generate stable IDs prefixed by category initial.)
**Coverage Summary Table:**
| Requirement Key | Has Task? | Task IDs | Notes |
|-----------------|-----------|----------|-------|
**Constitution Alignment Issues:** (if any)
**Unmapped Tasks:** (if any)
**Metrics:**
- Total Requirements
- Total Tasks
- Coverage % (requirements with >=1 task)
- Ambiguity Count
- Duplication Count
- Critical Issues Count
### 7. Provide Next Actions
At end of report, output a concise Next Actions block:
- If CRITICAL issues exist: Recommend resolving before `/speckit.implement`
- If only LOW/MEDIUM: User may proceed, but provide improvement suggestions
- Provide explicit command suggestions: e.g., "Run /speckit.specify with refinement", "Run /speckit.plan to adjust architecture", "Manually edit tasks.md to add coverage for 'performance-metrics'"
### 8. Offer Remediation
Ask the user: "Would you like me to suggest concrete remediation edits for the top N issues?" (Do NOT apply them automatically.)
## Operating Principles
### Context Efficiency
- **Minimal high-signal tokens**: Focus on actionable findings, not exhaustive documentation
- **Progressive disclosure**: Load artifacts incrementally; don't dump all content into analysis
- **Token-efficient output**: Limit findings table to 50 rows; summarize overflow
- **Deterministic results**: Rerunning without changes should produce consistent IDs and counts
### Analysis Guidelines
- **NEVER modify files** (this is read-only analysis)
- **NEVER hallucinate missing sections** (if absent, report them accurately)
- **Prioritize constitution violations** (these are always CRITICAL)
- **Use examples over exhaustive rules** (cite specific instances, not generic patterns)
- **Report zero issues gracefully** (emit success report with coverage statistics)
## Context
$ARGUMENTS
-294
View File
@@ -1,294 +0,0 @@
---
description: Generate a custom checklist for the current feature based on user requirements.
---
## Checklist Purpose: "Unit Tests for English"
**CRITICAL CONCEPT**: Checklists are **UNIT TESTS FOR REQUIREMENTS WRITING** - they validate the quality, clarity, and completeness of requirements in a given domain.
**NOT for verification/testing**:
- ❌ NOT "Verify the button clicks correctly"
- ❌ NOT "Test error handling works"
- ❌ NOT "Confirm the API returns 200"
- ❌ NOT checking if code/implementation matches the spec
**FOR requirements quality validation**:
- ✅ "Are visual hierarchy requirements defined for all card types?" (completeness)
- ✅ "Is 'prominent display' quantified with specific sizing/positioning?" (clarity)
- ✅ "Are hover state requirements consistent across all interactive elements?" (consistency)
- ✅ "Are accessibility requirements defined for keyboard navigation?" (coverage)
- ✅ "Does the spec define what happens when logo image fails to load?" (edge cases)
**Metaphor**: If your spec is code written in English, the checklist is its unit test suite. You're testing whether the requirements are well-written, complete, unambiguous, and ready for implementation - NOT whether the implementation works.
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Execution Steps
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse JSON for FEATURE_DIR and AVAILABLE_DOCS list.
- All file paths must be absolute.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Clarify intent (dynamic)**: Derive up to THREE initial contextual clarifying questions (no pre-baked catalog). They MUST:
- Be generated from the user's phrasing + extracted signals from spec/plan/tasks
- Only ask about information that materially changes checklist content
- Be skipped individually if already unambiguous in `$ARGUMENTS`
- Prefer precision over breadth
Generation algorithm:
1. Extract signals: feature domain keywords (e.g., auth, latency, UX, API), risk indicators ("critical", "must", "compliance"), stakeholder hints ("QA", "review", "security team"), and explicit deliverables ("a11y", "rollback", "contracts").
2. Cluster signals into candidate focus areas (max 4) ranked by relevance.
3. Identify probable audience & timing (author, reviewer, QA, release) if not explicit.
4. Detect missing dimensions: scope breadth, depth/rigor, risk emphasis, exclusion boundaries, measurable acceptance criteria.
5. Formulate questions chosen from these archetypes:
- Scope refinement (e.g., "Should this include integration touchpoints with X and Y or stay limited to local module correctness?")
- Risk prioritization (e.g., "Which of these potential risk areas should receive mandatory gating checks?")
- Depth calibration (e.g., "Is this a lightweight pre-commit sanity list or a formal release gate?")
- Audience framing (e.g., "Will this be used by the author only or peers during PR review?")
- Boundary exclusion (e.g., "Should we explicitly exclude performance tuning items this round?")
- Scenario class gap (e.g., "No recovery flows detected—are rollback / partial failure paths in scope?")
Question formatting rules:
- If presenting options, generate a compact table with columns: Option | Candidate | Why It Matters
- Limit to AE options maximum; omit table if a free-form answer is clearer
- Never ask the user to restate what they already said
- Avoid speculative categories (no hallucination). If uncertain, ask explicitly: "Confirm whether X belongs in scope."
Defaults when interaction impossible:
- Depth: Standard
- Audience: Reviewer (PR) if code-related; Author otherwise
- Focus: Top 2 relevance clusters
Output the questions (label Q1/Q2/Q3). After answers: if ≥2 scenario classes (Alternate / Exception / Recovery / Non-Functional domain) remain unclear, you MAY ask up to TWO more targeted followups (Q4/Q5) with a one-line justification each (e.g., "Unresolved recovery path risk"). Do not exceed five total questions. Skip escalation if user explicitly declines more.
3. **Understand user request**: Combine `$ARGUMENTS` + clarifying answers:
- Derive checklist theme (e.g., security, review, deploy, ux)
- Consolidate explicit must-have items mentioned by user
- Map focus selections to category scaffolding
- Infer any missing context from spec/plan/tasks (do NOT hallucinate)
4. **Load feature context**: Read from FEATURE_DIR:
- spec.md: Feature requirements and scope
- plan.md (if exists): Technical details, dependencies
- tasks.md (if exists): Implementation tasks
**Context Loading Strategy**:
- Load only necessary portions relevant to active focus areas (avoid full-file dumping)
- Prefer summarizing long sections into concise scenario/requirement bullets
- Use progressive disclosure: add follow-on retrieval only if gaps detected
- If source docs are large, generate interim summary items instead of embedding raw text
5. **Generate checklist** - Create "Unit Tests for Requirements":
- Create `FEATURE_DIR/checklists/` directory if it doesn't exist
- Generate unique checklist filename:
- Use short, descriptive name based on domain (e.g., `ux.md`, `api.md`, `security.md`)
- Format: `[domain].md`
- If file exists, append to existing file
- Number items sequentially starting from CHK001
- Each `/speckit.checklist` run creates a NEW file (never overwrites existing checklists)
**CORE PRINCIPLE - Test the Requirements, Not the Implementation**:
Every checklist item MUST evaluate the REQUIREMENTS THEMSELVES for:
- **Completeness**: Are all necessary requirements present?
- **Clarity**: Are requirements unambiguous and specific?
- **Consistency**: Do requirements align with each other?
- **Measurability**: Can requirements be objectively verified?
- **Coverage**: Are all scenarios/edge cases addressed?
**Category Structure** - Group items by requirement quality dimensions:
- **Requirement Completeness** (Are all necessary requirements documented?)
- **Requirement Clarity** (Are requirements specific and unambiguous?)
- **Requirement Consistency** (Do requirements align without conflicts?)
- **Acceptance Criteria Quality** (Are success criteria measurable?)
- **Scenario Coverage** (Are all flows/cases addressed?)
- **Edge Case Coverage** (Are boundary conditions defined?)
- **Non-Functional Requirements** (Performance, Security, Accessibility, etc. - are they specified?)
- **Dependencies & Assumptions** (Are they documented and validated?)
- **Ambiguities & Conflicts** (What needs clarification?)
**HOW TO WRITE CHECKLIST ITEMS - "Unit Tests for English"**:
**WRONG** (Testing implementation):
- "Verify landing page displays 3 episode cards"
- "Test hover states work on desktop"
- "Confirm logo click navigates home"
**CORRECT** (Testing requirements quality):
- "Are the exact number and layout of featured episodes specified?" [Completeness]
- "Is 'prominent display' quantified with specific sizing/positioning?" [Clarity]
- "Are hover state requirements consistent across all interactive elements?" [Consistency]
- "Are keyboard navigation requirements defined for all interactive UI?" [Coverage]
- "Is the fallback behavior specified when logo image fails to load?" [Edge Cases]
- "Are loading states defined for asynchronous episode data?" [Completeness]
- "Does the spec define visual hierarchy for competing UI elements?" [Clarity]
**ITEM STRUCTURE**:
Each item should follow this pattern:
- Question format asking about requirement quality
- Focus on what's WRITTEN (or not written) in the spec/plan
- Include quality dimension in brackets [Completeness/Clarity/Consistency/etc.]
- Reference spec section `[Spec §X.Y]` when checking existing requirements
- Use `[Gap]` marker when checking for missing requirements
**EXAMPLES BY QUALITY DIMENSION**:
Completeness:
- "Are error handling requirements defined for all API failure modes? [Gap]"
- "Are accessibility requirements specified for all interactive elements? [Completeness]"
- "Are mobile breakpoint requirements defined for responsive layouts? [Gap]"
Clarity:
- "Is 'fast loading' quantified with specific timing thresholds? [Clarity, Spec §NFR-2]"
- "Are 'related episodes' selection criteria explicitly defined? [Clarity, Spec §FR-5]"
- "Is 'prominent' defined with measurable visual properties? [Ambiguity, Spec §FR-4]"
Consistency:
- "Do navigation requirements align across all pages? [Consistency, Spec §FR-10]"
- "Are card component requirements consistent between landing and detail pages? [Consistency]"
Coverage:
- "Are requirements defined for zero-state scenarios (no episodes)? [Coverage, Edge Case]"
- "Are concurrent user interaction scenarios addressed? [Coverage, Gap]"
- "Are requirements specified for partial data loading failures? [Coverage, Exception Flow]"
Measurability:
- "Are visual hierarchy requirements measurable/testable? [Acceptance Criteria, Spec §FR-1]"
- "Can 'balanced visual weight' be objectively verified? [Measurability, Spec §FR-2]"
**Scenario Classification & Coverage** (Requirements Quality Focus):
- Check if requirements exist for: Primary, Alternate, Exception/Error, Recovery, Non-Functional scenarios
- For each scenario class, ask: "Are [scenario type] requirements complete, clear, and consistent?"
- If scenario class missing: "Are [scenario type] requirements intentionally excluded or missing? [Gap]"
- Include resilience/rollback when state mutation occurs: "Are rollback requirements defined for migration failures? [Gap]"
**Traceability Requirements**:
- MINIMUM: ≥80% of items MUST include at least one traceability reference
- Each item should reference: spec section `[Spec §X.Y]`, or use markers: `[Gap]`, `[Ambiguity]`, `[Conflict]`, `[Assumption]`
- If no ID system exists: "Is a requirement & acceptance criteria ID scheme established? [Traceability]"
**Surface & Resolve Issues** (Requirements Quality Problems):
Ask questions about the requirements themselves:
- Ambiguities: "Is the term 'fast' quantified with specific metrics? [Ambiguity, Spec §NFR-1]"
- Conflicts: "Do navigation requirements conflict between §FR-10 and §FR-10a? [Conflict]"
- Assumptions: "Is the assumption of 'always available podcast API' validated? [Assumption]"
- Dependencies: "Are external podcast API requirements documented? [Dependency, Gap]"
- Missing definitions: "Is 'visual hierarchy' defined with measurable criteria? [Gap]"
**Content Consolidation**:
- Soft cap: If raw candidate items > 40, prioritize by risk/impact
- Merge near-duplicates checking the same requirement aspect
- If >5 low-impact edge cases, create one item: "Are edge cases X, Y, Z addressed in requirements? [Coverage]"
**🚫 ABSOLUTELY PROHIBITED** - These make it an implementation test, not a requirements test:
- ❌ Any item starting with "Verify", "Test", "Confirm", "Check" + implementation behavior
- ❌ References to code execution, user actions, system behavior
- ❌ "Displays correctly", "works properly", "functions as expected"
- ❌ "Click", "navigate", "render", "load", "execute"
- ❌ Test cases, test plans, QA procedures
- ❌ Implementation details (frameworks, APIs, algorithms)
**✅ REQUIRED PATTERNS** - These test requirements quality:
- ✅ "Are [requirement type] defined/specified/documented for [scenario]?"
- ✅ "Is [vague term] quantified/clarified with specific criteria?"
- ✅ "Are requirements consistent between [section A] and [section B]?"
- ✅ "Can [requirement] be objectively measured/verified?"
- ✅ "Are [edge cases/scenarios] addressed in requirements?"
- ✅ "Does the spec define [missing aspect]?"
6. **Structure Reference**: Generate the checklist following the canonical template in `.specify/templates/checklist-template.md` for title, meta section, category headings, and ID formatting. If template is unavailable, use: H1 title, purpose/created meta lines, `##` category sections containing `- [ ] CHK### <requirement item>` lines with globally incrementing IDs starting at CHK001.
7. **Report**: Output full path to created checklist, item count, and remind user that each run creates a new file. Summarize:
- Focus areas selected
- Depth level
- Actor/timing
- Any explicit user-specified must-have items incorporated
**Important**: Each `/speckit.checklist` command invocation creates a checklist file using short, descriptive names unless file already exists. This allows:
- Multiple checklists of different types (e.g., `ux.md`, `test.md`, `security.md`)
- Simple, memorable filenames that indicate checklist purpose
- Easy identification and navigation in the `checklists/` folder
To avoid clutter, use descriptive types and clean up obsolete checklists when done.
## Example Checklist Types & Sample Items
**UX Requirements Quality:** `ux.md`
Sample items (testing the requirements, NOT the implementation):
- "Are visual hierarchy requirements defined with measurable criteria? [Clarity, Spec §FR-1]"
- "Is the number and positioning of UI elements explicitly specified? [Completeness, Spec §FR-1]"
- "Are interaction state requirements (hover, focus, active) consistently defined? [Consistency]"
- "Are accessibility requirements specified for all interactive elements? [Coverage, Gap]"
- "Is fallback behavior defined when images fail to load? [Edge Case, Gap]"
- "Can 'prominent display' be objectively measured? [Measurability, Spec §FR-4]"
**API Requirements Quality:** `api.md`
Sample items:
- "Are error response formats specified for all failure scenarios? [Completeness]"
- "Are rate limiting requirements quantified with specific thresholds? [Clarity]"
- "Are authentication requirements consistent across all endpoints? [Consistency]"
- "Are retry/timeout requirements defined for external dependencies? [Coverage, Gap]"
- "Is versioning strategy documented in requirements? [Gap]"
**Performance Requirements Quality:** `performance.md`
Sample items:
- "Are performance requirements quantified with specific metrics? [Clarity]"
- "Are performance targets defined for all critical user journeys? [Coverage]"
- "Are performance requirements under different load conditions specified? [Completeness]"
- "Can performance requirements be objectively measured? [Measurability]"
- "Are degradation requirements defined for high-load scenarios? [Edge Case, Gap]"
**Security Requirements Quality:** `security.md`
Sample items:
- "Are authentication requirements specified for all protected resources? [Coverage]"
- "Are data protection requirements defined for sensitive information? [Completeness]"
- "Is the threat model documented and requirements aligned to it? [Traceability]"
- "Are security requirements consistent with compliance obligations? [Consistency]"
- "Are security failure/breach response requirements defined? [Gap, Exception Flow]"
## Anti-Examples: What NOT To Do
**❌ WRONG - These test implementation, not requirements:**
```markdown
- [ ] CHK001 - Verify landing page displays 3 episode cards [Spec §FR-001]
- [ ] CHK002 - Test hover states work correctly on desktop [Spec §FR-003]
- [ ] CHK003 - Confirm logo click navigates to home page [Spec §FR-010]
- [ ] CHK004 - Check that related episodes section shows 3-5 items [Spec §FR-005]
```
**✅ CORRECT - These test requirements quality:**
```markdown
- [ ] CHK001 - Are the number and layout of featured episodes explicitly specified? [Completeness, Spec §FR-001]
- [ ] CHK002 - Are hover state requirements consistently defined for all interactive elements? [Consistency, Spec §FR-003]
- [ ] CHK003 - Are navigation requirements clear for all clickable brand elements? [Clarity, Spec §FR-010]
- [ ] CHK004 - Is the selection criteria for related episodes documented? [Gap, Spec §FR-005]
- [ ] CHK005 - Are loading state requirements defined for asynchronous episode data? [Gap]
- [ ] CHK006 - Can "visual hierarchy" requirements be objectively measured? [Measurability, Spec §FR-001]
```
**Key Differences:**
- Wrong: Tests if the system works correctly
- Correct: Tests if the requirements are written correctly
- Wrong: Verification of behavior
- Correct: Validation of requirement quality
- Wrong: "Does it do X?"
- Correct: "Is X clearly specified?"
-181
View File
@@ -1,181 +0,0 @@
---
description: Identify underspecified areas in the current feature spec by asking up to 5 highly targeted clarification questions and encoding answers back into the spec.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
Goal: Detect and reduce ambiguity or missing decision points in the active feature specification and record the clarifications directly in the spec file.
Note: This clarification workflow is expected to run (and be completed) BEFORE invoking `/speckit.plan`. If the user explicitly states they are skipping clarification (e.g., exploratory spike), you may proceed, but must warn that downstream rework risk increases.
Execution steps:
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --paths-only` from repo root **once** (combined `--json --paths-only` mode / `-Json -PathsOnly`). Parse minimal JSON payload fields:
- `FEATURE_DIR`
- `FEATURE_SPEC`
- (Optionally capture `IMPL_PLAN`, `TASKS` for future chained flows.)
- If JSON parsing fails, abort and instruct user to re-run `/speckit.specify` or verify feature branch environment.
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. Load the current spec file. Perform a structured ambiguity & coverage scan using this taxonomy. For each category, mark status: Clear / Partial / Missing. Produce an internal coverage map used for prioritization (do not output raw map unless no questions will be asked).
Functional Scope & Behavior:
- Core user goals & success criteria
- Explicit out-of-scope declarations
- User roles / personas differentiation
Domain & Data Model:
- Entities, attributes, relationships
- Identity & uniqueness rules
- Lifecycle/state transitions
- Data volume / scale assumptions
Interaction & UX Flow:
- Critical user journeys / sequences
- Error/empty/loading states
- Accessibility or localization notes
Non-Functional Quality Attributes:
- Performance (latency, throughput targets)
- Scalability (horizontal/vertical, limits)
- Reliability & availability (uptime, recovery expectations)
- Observability (logging, metrics, tracing signals)
- Security & privacy (authN/Z, data protection, threat assumptions)
- Compliance / regulatory constraints (if any)
Integration & External Dependencies:
- External services/APIs and failure modes
- Data import/export formats
- Protocol/versioning assumptions
Edge Cases & Failure Handling:
- Negative scenarios
- Rate limiting / throttling
- Conflict resolution (e.g., concurrent edits)
Constraints & Tradeoffs:
- Technical constraints (language, storage, hosting)
- Explicit tradeoffs or rejected alternatives
Terminology & Consistency:
- Canonical glossary terms
- Avoided synonyms / deprecated terms
Completion Signals:
- Acceptance criteria testability
- Measurable Definition of Done style indicators
Misc / Placeholders:
- TODO markers / unresolved decisions
- Ambiguous adjectives ("robust", "intuitive") lacking quantification
For each category with Partial or Missing status, add a candidate question opportunity unless:
- Clarification would not materially change implementation or validation strategy
- Information is better deferred to planning phase (note internally)
3. Generate (internally) a prioritized queue of candidate clarification questions (maximum 5). Do NOT output them all at once. Apply these constraints:
- Maximum of 10 total questions across the whole session.
- Each question must be answerable with EITHER:
- A short multiplechoice selection (25 distinct, mutually exclusive options), OR
- A one-word / shortphrase answer (explicitly constrain: "Answer in <=5 words").
- Only include questions whose answers materially impact architecture, data modeling, task decomposition, test design, UX behavior, operational readiness, or compliance validation.
- Ensure category coverage balance: attempt to cover the highest impact unresolved categories first; avoid asking two low-impact questions when a single high-impact area (e.g., security posture) is unresolved.
- Exclude questions already answered, trivial stylistic preferences, or plan-level execution details (unless blocking correctness).
- Favor clarifications that reduce downstream rework risk or prevent misaligned acceptance tests.
- If more than 5 categories remain unresolved, select the top 5 by (Impact * Uncertainty) heuristic.
4. Sequential questioning loop (interactive):
- Present EXACTLY ONE question at a time.
- For multiplechoice questions:
- **Analyze all options** and determine the **most suitable option** based on:
- Best practices for the project type
- Common patterns in similar implementations
- Risk reduction (security, performance, maintainability)
- Alignment with any explicit project goals or constraints visible in the spec
- Present your **recommended option prominently** at the top with clear reasoning (1-2 sentences explaining why this is the best choice).
- Format as: `**Recommended:** Option [X] - <reasoning>`
- Then render all options as a Markdown table:
| Option | Description |
|--------|-------------|
| A | <Option A description> |
| B | <Option B description> |
| C | <Option C description> (add D/E as needed up to 5) |
| Short | Provide a different short answer (<=5 words) (Include only if free-form alternative is appropriate) |
- After the table, add: `You can reply with the option letter (e.g., "A"), accept the recommendation by saying "yes" or "recommended", or provide your own short answer.`
- For shortanswer style (no meaningful discrete options):
- Provide your **suggested answer** based on best practices and context.
- Format as: `**Suggested:** <your proposed answer> - <brief reasoning>`
- Then output: `Format: Short answer (<=5 words). You can accept the suggestion by saying "yes" or "suggested", or provide your own answer.`
- After the user answers:
- If the user replies with "yes", "recommended", or "suggested", use your previously stated recommendation/suggestion as the answer.
- Otherwise, validate the answer maps to one option or fits the <=5 word constraint.
- If ambiguous, ask for a quick disambiguation (count still belongs to same question; do not advance).
- Once satisfactory, record it in working memory (do not yet write to disk) and move to the next queued question.
- Stop asking further questions when:
- All critical ambiguities resolved early (remaining queued items become unnecessary), OR
- User signals completion ("done", "good", "no more"), OR
- You reach 5 asked questions.
- Never reveal future queued questions in advance.
- If no valid questions exist at start, immediately report no critical ambiguities.
5. Integration after EACH accepted answer (incremental update approach):
- Maintain in-memory representation of the spec (loaded once at start) plus the raw file contents.
- For the first integrated answer in this session:
- Ensure a `## Clarifications` section exists (create it just after the highest-level contextual/overview section per the spec template if missing).
- Under it, create (if not present) a `### Session YYYY-MM-DD` subheading for today.
- Append a bullet line immediately after acceptance: `- Q: <question> → A: <final answer>`.
- Then immediately apply the clarification to the most appropriate section(s):
- Functional ambiguity → Update or add a bullet in Functional Requirements.
- User interaction / actor distinction → Update User Stories or Actors subsection (if present) with clarified role, constraint, or scenario.
- Data shape / entities → Update Data Model (add fields, types, relationships) preserving ordering; note added constraints succinctly.
- Non-functional constraint → Add/modify measurable criteria in Non-Functional / Quality Attributes section (convert vague adjective to metric or explicit target).
- Edge case / negative flow → Add a new bullet under Edge Cases / Error Handling (or create such subsection if template provides placeholder for it).
- Terminology conflict → Normalize term across spec; retain original only if necessary by adding `(formerly referred to as "X")` once.
- If the clarification invalidates an earlier ambiguous statement, replace that statement instead of duplicating; leave no obsolete contradictory text.
- Save the spec file AFTER each integration to minimize risk of context loss (atomic overwrite).
- Preserve formatting: do not reorder unrelated sections; keep heading hierarchy intact.
- Keep each inserted clarification minimal and testable (avoid narrative drift).
6. Validation (performed after EACH write plus final pass):
- Clarifications session contains exactly one bullet per accepted answer (no duplicates).
- Total asked (accepted) questions ≤ 5.
- Updated sections contain no lingering vague placeholders the new answer was meant to resolve.
- No contradictory earlier statement remains (scan for now-invalid alternative choices removed).
- Markdown structure valid; only allowed new headings: `## Clarifications`, `### Session YYYY-MM-DD`.
- Terminology consistency: same canonical term used across all updated sections.
7. Write the updated spec back to `FEATURE_SPEC`.
8. Report completion (after questioning loop ends or early termination):
- Number of questions asked & answered.
- Path to updated spec.
- Sections touched (list names).
- Coverage summary table listing each taxonomy category with Status: Resolved (was Partial/Missing and addressed), Deferred (exceeds question quota or better suited for planning), Clear (already sufficient), Outstanding (still Partial/Missing but low impact).
- If any Outstanding or Deferred remain, recommend whether to proceed to `/speckit.plan` or run `/speckit.clarify` again later post-plan.
- Suggested next command.
Behavior rules:
- If no meaningful ambiguities found (or all potential questions would be low-impact), respond: "No critical ambiguities detected worth formal clarification." and suggest proceeding.
- If spec file missing, instruct user to run `/speckit.specify` first (do not create a new spec here).
- Never exceed 5 total asked questions (clarification retries for a single question do not count as new questions).
- Avoid speculative tech stack questions unless the absence blocks functional clarity.
- Respect user early termination signals ("stop", "done", "proceed").
- If no questions asked due to full coverage, output a compact coverage summary (all categories Clear) then suggest advancing.
- If quota reached with unresolved high-impact categories remaining, explicitly flag them under Deferred with rationale.
Context for prioritization: $ARGUMENTS
-84
View File
@@ -1,84 +0,0 @@
---
description: Create or update the project constitution from interactive or provided principle inputs, ensuring all dependent templates stay in sync.
handoffs:
- label: Build Specification
agent: speckit.specify
prompt: Implement the feature specification based on the updated constitution. I want to build...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
You are updating the project constitution at `.specify/memory/constitution.md`. This file is a TEMPLATE containing placeholder tokens in square brackets (e.g. `[PROJECT_NAME]`, `[PRINCIPLE_1_NAME]`). Your job is to (a) collect/derive concrete values, (b) fill the template precisely, and (c) propagate any amendments across dependent artifacts.
**Note**: If `.specify/memory/constitution.md` does not exist yet, it should have been initialized from `.specify/templates/constitution-template.md` during project setup. If it's missing, copy the template first.
Follow this execution flow:
1. Load the existing constitution at `.specify/memory/constitution.md`.
- Identify every placeholder token of the form `[ALL_CAPS_IDENTIFIER]`.
**IMPORTANT**: The user might require less or more principles than the ones used in the template. If a number is specified, respect that - follow the general template. You will update the doc accordingly.
2. Collect/derive values for placeholders:
- If user input (conversation) supplies a value, use it.
- Otherwise infer from existing repo context (README, docs, prior constitution versions if embedded).
- For governance dates: `RATIFICATION_DATE` is the original adoption date (if unknown ask or mark TODO), `LAST_AMENDED_DATE` is today if changes are made, otherwise keep previous.
- `CONSTITUTION_VERSION` must increment according to semantic versioning rules:
- MAJOR: Backward incompatible governance/principle removals or redefinitions.
- MINOR: New principle/section added or materially expanded guidance.
- PATCH: Clarifications, wording, typo fixes, non-semantic refinements.
- If version bump type ambiguous, propose reasoning before finalizing.
3. Draft the updated constitution content:
- Replace every placeholder with concrete text (no bracketed tokens left except intentionally retained template slots that the project has chosen not to define yet—explicitly justify any left).
- Preserve heading hierarchy and comments can be removed once replaced unless they still add clarifying guidance.
- Ensure each Principle section: succinct name line, paragraph (or bullet list) capturing nonnegotiable rules, explicit rationale if not obvious.
- Ensure Governance section lists amendment procedure, versioning policy, and compliance review expectations.
4. Consistency propagation checklist (convert prior checklist into active validations):
- Read `.specify/templates/plan-template.md` and ensure any "Constitution Check" or rules align with updated principles.
- Read `.specify/templates/spec-template.md` for scope/requirements alignment—update if constitution adds/removes mandatory sections or constraints.
- Read `.specify/templates/tasks-template.md` and ensure task categorization reflects new or removed principle-driven task types (e.g., observability, versioning, testing discipline).
- Read each command file in `.specify/templates/commands/*.md` (including this one) to verify no outdated references (agent-specific names like CLAUDE only) remain when generic guidance is required.
- Read any runtime guidance docs (e.g., `README.md`, `docs/quickstart.md`, or agent-specific guidance files if present). Update references to principles changed.
5. Produce a Sync Impact Report (prepend as an HTML comment at top of the constitution file after update):
- Version change: old → new
- List of modified principles (old title → new title if renamed)
- Added sections
- Removed sections
- Templates requiring updates (✅ updated / ⚠ pending) with file paths
- Follow-up TODOs if any placeholders intentionally deferred.
6. Validation before final output:
- No remaining unexplained bracket tokens.
- Version line matches report.
- Dates ISO format YYYY-MM-DD.
- Principles are declarative, testable, and free of vague language ("should" → replace with MUST/SHOULD rationale where appropriate).
7. Write the completed constitution back to `.specify/memory/constitution.md` (overwrite).
8. Output a final summary to the user with:
- New version and bump rationale.
- Any files flagged for manual follow-up.
- Suggested commit message (e.g., `docs: amend constitution to vX.Y.Z (principle additions + governance update)`).
Formatting & Style Requirements:
- Use Markdown headings exactly as in the template (do not demote/promote levels).
- Wrap long rationale lines to keep readability (<100 chars ideally) but do not hard enforce with awkward breaks.
- Keep a single blank line between sections.
- Avoid trailing whitespace.
If the user supplies partial updates (e.g., only one principle revision), still perform validation and version decision steps.
If critical info missing (e.g., ratification date truly unknown), insert `TODO(<FIELD_NAME>): explanation` and include in the Sync Impact Report under deferred items.
Do not create a new template; always operate on the existing `.specify/memory/constitution.md` file.
-135
View File
@@ -1,135 +0,0 @@
---
description: Execute the implementation plan by processing and executing all tasks defined in tasks.md
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Check checklists status** (if FEATURE_DIR/checklists/ exists):
- Scan all checklist files in the checklists/ directory
- For each checklist, count:
- Total items: All lines matching `- [ ]` or `- [X]` or `- [x]`
- Completed items: Lines matching `- [X]` or `- [x]`
- Incomplete items: Lines matching `- [ ]`
- Create a status table:
```text
| Checklist | Total | Completed | Incomplete | Status |
|-----------|-------|-----------|------------|--------|
| ux.md | 12 | 12 | 0 | ✓ PASS |
| test.md | 8 | 5 | 3 | ✗ FAIL |
| security.md | 6 | 6 | 0 | ✓ PASS |
```
- Calculate overall status:
- **PASS**: All checklists have 0 incomplete items
- **FAIL**: One or more checklists have incomplete items
- **If any checklist is incomplete**:
- Display the table with incomplete item counts
- **STOP** and ask: "Some checklists are incomplete. Do you want to proceed with implementation anyway? (yes/no)"
- Wait for user response before continuing
- If user says "no" or "wait" or "stop", halt execution
- If user says "yes" or "proceed" or "continue", proceed to step 3
- **If all checklists are complete**:
- Display the table showing all checklists passed
- Automatically proceed to step 3
3. Load and analyze the implementation context:
- **REQUIRED**: Read tasks.md for the complete task list and execution plan
- **REQUIRED**: Read plan.md for tech stack, architecture, and file structure
- **IF EXISTS**: Read data-model.md for entities and relationships
- **IF EXISTS**: Read contracts/ for API specifications and test requirements
- **IF EXISTS**: Read research.md for technical decisions and constraints
- **IF EXISTS**: Read quickstart.md for integration scenarios
4. **Project Setup Verification**:
- **REQUIRED**: Create/verify ignore files based on actual project setup:
**Detection & Creation Logic**:
- Check if the following command succeeds to determine if the repository is a git repo (create/verify .gitignore if so):
```sh
git rev-parse --git-dir 2>/dev/null
```
- Check if Dockerfile* exists or Docker in plan.md → create/verify .dockerignore
- Check if .eslintrc* exists → create/verify .eslintignore
- Check if eslint.config.* exists → ensure the config's `ignores` entries cover required patterns
- Check if .prettierrc* exists → create/verify .prettierignore
- Check if .npmrc or package.json exists → create/verify .npmignore (if publishing)
- Check if terraform files (*.tf) exist → create/verify .terraformignore
- Check if .helmignore needed (helm charts present) → create/verify .helmignore
**If ignore file already exists**: Verify it contains essential patterns, append missing critical patterns only
**If ignore file missing**: Create with full pattern set for detected technology
**Common Patterns by Technology** (from plan.md tech stack):
- **Node.js/JavaScript/TypeScript**: `node_modules/`, `dist/`, `build/`, `*.log`, `.env*`
- **Python**: `__pycache__/`, `*.pyc`, `.venv/`, `venv/`, `dist/`, `*.egg-info/`
- **Java**: `target/`, `*.class`, `*.jar`, `.gradle/`, `build/`
- **C#/.NET**: `bin/`, `obj/`, `*.user`, `*.suo`, `packages/`
- **Go**: `*.exe`, `*.test`, `vendor/`, `*.out`
- **Ruby**: `.bundle/`, `log/`, `tmp/`, `*.gem`, `vendor/bundle/`
- **PHP**: `vendor/`, `*.log`, `*.cache`, `*.env`
- **Rust**: `target/`, `debug/`, `release/`, `*.rs.bk`, `*.rlib`, `*.prof*`, `.idea/`, `*.log`, `.env*`
- **Kotlin**: `build/`, `out/`, `.gradle/`, `.idea/`, `*.class`, `*.jar`, `*.iml`, `*.log`, `.env*`
- **C++**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.so`, `*.a`, `*.exe`, `*.dll`, `.idea/`, `*.log`, `.env*`
- **C**: `build/`, `bin/`, `obj/`, `out/`, `*.o`, `*.a`, `*.so`, `*.exe`, `Makefile`, `config.log`, `.idea/`, `*.log`, `.env*`
- **Swift**: `.build/`, `DerivedData/`, `*.swiftpm/`, `Packages/`
- **R**: `.Rproj.user/`, `.Rhistory`, `.RData`, `.Ruserdata`, `*.Rproj`, `packrat/`, `renv/`
- **Universal**: `.DS_Store`, `Thumbs.db`, `*.tmp`, `*.swp`, `.vscode/`, `.idea/`
**Tool-Specific Patterns**:
- **Docker**: `node_modules/`, `.git/`, `Dockerfile*`, `.dockerignore`, `*.log*`, `.env*`, `coverage/`
- **ESLint**: `node_modules/`, `dist/`, `build/`, `coverage/`, `*.min.js`
- **Prettier**: `node_modules/`, `dist/`, `build/`, `coverage/`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`
- **Terraform**: `.terraform/`, `*.tfstate*`, `*.tfvars`, `.terraform.lock.hcl`
- **Kubernetes/k8s**: `*.secret.yaml`, `secrets/`, `.kube/`, `kubeconfig*`, `*.key`, `*.crt`
5. Parse tasks.md structure and extract:
- **Task phases**: Setup, Tests, Core, Integration, Polish
- **Task dependencies**: Sequential vs parallel execution rules
- **Task details**: ID, description, file paths, parallel markers [P]
- **Execution flow**: Order and dependency requirements
6. Execute implementation following the task plan:
- **Phase-by-phase execution**: Complete each phase before moving to the next
- **Respect dependencies**: Run sequential tasks in order, parallel tasks [P] can run together
- **Follow TDD approach**: Execute test tasks before their corresponding implementation tasks
- **File-based coordination**: Tasks affecting the same files must run sequentially
- **Validation checkpoints**: Verify each phase completion before proceeding
7. Implementation execution rules:
- **Setup first**: Initialize project structure, dependencies, configuration
- **Tests before code**: If you need to write tests for contracts, entities, and integration scenarios
- **Core development**: Implement models, services, CLI commands, endpoints
- **Integration work**: Database connections, middleware, logging, external services
- **Polish and validation**: Unit tests, performance optimization, documentation
8. Progress tracking and error handling:
- Report progress after each completed task
- Halt execution if any non-parallel task fails
- For parallel tasks [P], continue with successful tasks, report failed ones
- Provide clear error messages with context for debugging
- Suggest next steps if implementation cannot proceed
- **IMPORTANT** For completed tasks, make sure to mark the task off as [X] in the tasks file.
9. Completion validation:
- Verify all required tasks are completed
- Check that implemented features match the original specification
- Validate that tests pass and coverage meets requirements
- Confirm the implementation follows the technical plan
- Report final status with summary of completed work
Note: This command assumes a complete task breakdown exists in tasks.md. If tasks are incomplete or missing, suggest running `/speckit.tasks` first to regenerate the task list.
-89
View File
@@ -1,89 +0,0 @@
---
description: Execute the implementation planning workflow using the plan template to generate design artifacts.
handoffs:
- label: Create Tasks
agent: speckit.tasks
prompt: Break the plan into tasks
send: true
- label: Create Checklist
agent: speckit.checklist
prompt: Create a checklist for the following domain...
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/setup-plan.sh --json` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load context**: Read FEATURE_SPEC and `.specify/memory/constitution.md`. Load IMPL_PLAN template (already copied).
3. **Execute plan workflow**: Follow the structure in IMPL_PLAN template to:
- Fill Technical Context (mark unknowns as "NEEDS CLARIFICATION")
- Fill Constitution Check section from constitution
- Evaluate gates (ERROR if violations unjustified)
- Phase 0: Generate research.md (resolve all NEEDS CLARIFICATION)
- Phase 1: Generate data-model.md, contracts/, quickstart.md
- Phase 1: Update agent context by running the agent script
- Re-evaluate Constitution Check post-design
4. **Stop and report**: Command ends after Phase 2 planning. Report branch, IMPL_PLAN path, and generated artifacts.
## Phases
### Phase 0: Outline & Research
1. **Extract unknowns from Technical Context** above:
- For each NEEDS CLARIFICATION → research task
- For each dependency → best practices task
- For each integration → patterns task
2. **Generate and dispatch research agents**:
```text
For each unknown in Technical Context:
Task: "Research {unknown} for {feature context}"
For each technology choice:
Task: "Find best practices for {tech} in {domain}"
```
3. **Consolidate findings** in `research.md` using format:
- Decision: [what was chosen]
- Rationale: [why chosen]
- Alternatives considered: [what else evaluated]
**Output**: research.md with all NEEDS CLARIFICATION resolved
### Phase 1: Design & Contracts
**Prerequisites:** `research.md` complete
1. **Extract entities from feature spec** → `data-model.md`:
- Entity name, fields, relationships
- Validation rules from requirements
- State transitions if applicable
2. **Generate API contracts** from functional requirements:
- For each user action → endpoint
- Use standard REST/GraphQL patterns
- Output OpenAPI/GraphQL schema to `/contracts/`
3. **Agent context update**:
- Run `.specify/scripts/bash/update-agent-context.sh cursor-agent`
- These scripts detect which AI agent is in use
- Update the appropriate agent-specific context file
- Add only new technology from current plan
- Preserve manual additions between markers
**Output**: data-model.md, /contracts/*, quickstart.md, agent-specific file
## Key rules
- Use absolute paths
- ERROR on gate failures or unresolved clarifications
-258
View File
@@ -1,258 +0,0 @@
---
description: Create or update the feature specification from a natural language feature description.
handoffs:
- label: Build Technical Plan
agent: speckit.plan
prompt: Create a plan for the spec. I am building with...
- label: Clarify Spec Requirements
agent: speckit.clarify
prompt: Clarify specification requirements
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
The text the user typed after `/speckit.specify` in the triggering message **is** the feature description. Assume you always have it available in this conversation even if `$ARGUMENTS` appears literally below. Do not ask the user to repeat it unless they provided an empty command.
Given that feature description, do this:
1. **Generate a concise short name** (2-4 words) for the branch:
- Analyze the feature description and extract the most meaningful keywords
- Create a 2-4 word short name that captures the essence of the feature
- Use action-noun format when possible (e.g., "add-user-auth", "fix-payment-bug")
- Preserve technical terms and acronyms (OAuth2, API, JWT, etc.)
- Keep it concise but descriptive enough to understand the feature at a glance
- Examples:
- "I want to add user authentication" → "user-auth"
- "Implement OAuth2 integration for the API" → "oauth2-api-integration"
- "Create a dashboard for analytics" → "analytics-dashboard"
- "Fix payment processing timeout bug" → "fix-payment-timeout"
2. **Check for existing branches before creating new one**:
a. First, fetch all remote branches to ensure we have the latest information:
```bash
git fetch --all --prune
```
b. Find the highest feature number across all sources for the short-name:
- Remote branches: `git ls-remote --heads origin | grep -E 'refs/heads/[0-9]+-<short-name>$'`
- Local branches: `git branch | grep -E '^[* ]*[0-9]+-<short-name>$'`
- Specs directories: Check for directories matching `specs/[0-9]+-<short-name>`
c. Determine the next available number:
- Extract all numbers from all three sources
- Find the highest number N
- Use N+1 for the new branch number
d. Run the script `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS"` with the calculated number and short-name:
- Pass `--number N+1` and `--short-name "your-short-name"` along with the feature description
- Bash example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" --json --number 5 --short-name "user-auth" "Add user authentication"`
- PowerShell example: `.specify/scripts/bash/create-new-feature.sh --json "$ARGUMENTS" -Json -Number 5 -ShortName "user-auth" "Add user authentication"`
**IMPORTANT**:
- Check all three sources (remote branches, local branches, specs directories) to find the highest number
- Only match branches/directories with the exact short-name pattern
- If no existing branches/directories found with this short-name, start with number 1
- You must only ever run this script once per feature
- The JSON is provided in the terminal as output - always refer to it to get the actual content you're looking for
- The JSON output will contain BRANCH_NAME and SPEC_FILE paths
- For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot")
3. Load `.specify/templates/spec-template.md` to understand required sections.
4. Follow this execution flow:
1. Parse user description from Input
If empty: ERROR "No feature description provided"
2. Extract key concepts from description
Identify: actors, actions, data, constraints
3. For unclear aspects:
- Make informed guesses based on context and industry standards
- Only mark with [NEEDS CLARIFICATION: specific question] if:
- The choice significantly impacts feature scope or user experience
- Multiple reasonable interpretations exist with different implications
- No reasonable default exists
- **LIMIT: Maximum 3 [NEEDS CLARIFICATION] markers total**
- Prioritize clarifications by impact: scope > security/privacy > user experience > technical details
4. Fill User Scenarios & Testing section
If no clear user flow: ERROR "Cannot determine user scenarios"
5. Generate Functional Requirements
Each requirement must be testable
Use reasonable defaults for unspecified details (document assumptions in Assumptions section)
6. Define Success Criteria
Create measurable, technology-agnostic outcomes
Include both quantitative metrics (time, performance, volume) and qualitative measures (user satisfaction, task completion)
Each criterion must be verifiable without implementation details
7. Identify Key Entities (if data involved)
8. Return: SUCCESS (spec ready for planning)
5. Write the specification to SPEC_FILE using the template structure, replacing placeholders with concrete details derived from the feature description (arguments) while preserving section order and headings.
6. **Specification Quality Validation**: After writing the initial spec, validate it against quality criteria:
a. **Create Spec Quality Checklist**: Generate a checklist file at `FEATURE_DIR/checklists/requirements.md` using the checklist template structure with these validation items:
```markdown
# Specification Quality Checklist: [FEATURE NAME]
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: [DATE]
**Feature**: [Link to spec.md]
## Content Quality
- [ ] No implementation details (languages, frameworks, APIs)
- [ ] Focused on user value and business needs
- [ ] Written for non-technical stakeholders
- [ ] All mandatory sections completed
## Requirement Completeness
- [ ] No [NEEDS CLARIFICATION] markers remain
- [ ] Requirements are testable and unambiguous
- [ ] Success criteria are measurable
- [ ] Success criteria are technology-agnostic (no implementation details)
- [ ] All acceptance scenarios are defined
- [ ] Edge cases are identified
- [ ] Scope is clearly bounded
- [ ] Dependencies and assumptions identified
## Feature Readiness
- [ ] All functional requirements have clear acceptance criteria
- [ ] User scenarios cover primary flows
- [ ] Feature meets measurable outcomes defined in Success Criteria
- [ ] No implementation details leak into specification
## Notes
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`
```
b. **Run Validation Check**: Review the spec against each checklist item:
- For each item, determine if it passes or fails
- Document specific issues found (quote relevant spec sections)
c. **Handle Validation Results**:
- **If all items pass**: Mark checklist complete and proceed to step 6
- **If items fail (excluding [NEEDS CLARIFICATION])**:
1. List the failing items and specific issues
2. Update the spec to address each issue
3. Re-run validation until all items pass (max 3 iterations)
4. If still failing after 3 iterations, document remaining issues in checklist notes and warn user
- **If [NEEDS CLARIFICATION] markers remain**:
1. Extract all [NEEDS CLARIFICATION: ...] markers from the spec
2. **LIMIT CHECK**: If more than 3 markers exist, keep only the 3 most critical (by scope/security/UX impact) and make informed guesses for the rest
3. For each clarification needed (max 3), present options to user in this format:
```markdown
## Question [N]: [Topic]
**Context**: [Quote relevant spec section]
**What we need to know**: [Specific question from NEEDS CLARIFICATION marker]
**Suggested Answers**:
| Option | Answer | Implications |
|--------|--------|--------------|
| A | [First suggested answer] | [What this means for the feature] |
| B | [Second suggested answer] | [What this means for the feature] |
| C | [Third suggested answer] | [What this means for the feature] |
| Custom | Provide your own answer | [Explain how to provide custom input] |
**Your choice**: _[Wait for user response]_
```
4. **CRITICAL - Table Formatting**: Ensure markdown tables are properly formatted:
- Use consistent spacing with pipes aligned
- Each cell should have spaces around content: `| Content |` not `|Content|`
- Header separator must have at least 3 dashes: `|--------|`
- Test that the table renders correctly in markdown preview
5. Number questions sequentially (Q1, Q2, Q3 - max 3 total)
6. Present all questions together before waiting for responses
7. Wait for user to respond with their choices for all questions (e.g., "Q1: A, Q2: Custom - [details], Q3: B")
8. Update the spec by replacing each [NEEDS CLARIFICATION] marker with the user's selected or provided answer
9. Re-run validation after all clarifications are resolved
d. **Update Checklist**: After each validation iteration, update the checklist file with current pass/fail status
7. Report completion with branch name, spec file path, checklist results, and readiness for the next phase (`/speckit.clarify` or `/speckit.plan`).
**NOTE:** The script creates and checks out the new branch and initializes the spec file before writing.
## General Guidelines
## Quick Guidelines
- Focus on **WHAT** users need and **WHY**.
- Avoid HOW to implement (no tech stack, APIs, code structure).
- Written for business stakeholders, not developers.
- DO NOT create any checklists that are embedded in the spec. That will be a separate command.
### Section Requirements
- **Mandatory sections**: Must be completed for every feature
- **Optional sections**: Include only when relevant to the feature
- When a section doesn't apply, remove it entirely (don't leave as "N/A")
### For AI Generation
When creating this spec from a user prompt:
1. **Make informed guesses**: Use context, industry standards, and common patterns to fill gaps
2. **Document assumptions**: Record reasonable defaults in the Assumptions section
3. **Limit clarifications**: Maximum 3 [NEEDS CLARIFICATION] markers - use only for critical decisions that:
- Significantly impact feature scope or user experience
- Have multiple reasonable interpretations with different implications
- Lack any reasonable default
4. **Prioritize clarifications**: scope > security/privacy > user experience > technical details
5. **Think like a tester**: Every vague requirement should fail the "testable and unambiguous" checklist item
6. **Common areas needing clarification** (only if no reasonable default exists):
- Feature scope and boundaries (include/exclude specific use cases)
- User types and permissions (if multiple conflicting interpretations possible)
- Security/compliance requirements (when legally/financially significant)
**Examples of reasonable defaults** (don't ask about these):
- Data retention: Industry-standard practices for the domain
- Performance targets: Standard web/mobile app expectations unless specified
- Error handling: User-friendly messages with appropriate fallbacks
- Authentication method: Standard session-based or OAuth2 for web apps
- Integration patterns: RESTful APIs unless specified otherwise
### Success Criteria Guidelines
Success criteria must be:
1. **Measurable**: Include specific metrics (time, percentage, count, rate)
2. **Technology-agnostic**: No mention of frameworks, languages, databases, or tools
3. **User-focused**: Describe outcomes from user/business perspective, not system internals
4. **Verifiable**: Can be tested/validated without knowing implementation details
**Good examples**:
- "Users can complete checkout in under 3 minutes"
- "System supports 10,000 concurrent users"
- "95% of searches return results in under 1 second"
- "Task completion rate improves by 40%"
**Bad examples** (implementation-focused):
- "API response time is under 200ms" (too technical, use "Users see results instantly")
- "Database can handle 1000 TPS" (implementation detail, use user-facing metric)
- "React components render efficiently" (framework-specific)
- "Redis cache hit rate above 80%" (technology-specific)
-137
View File
@@ -1,137 +0,0 @@
---
description: Generate an actionable, dependency-ordered tasks.md for the feature based on available design artifacts.
handoffs:
- label: Analyze For Consistency
agent: speckit.analyze
prompt: Run a project analysis for consistency
send: true
- label: Implement Project
agent: speckit.implement
prompt: Start the implementation in phases
send: true
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. **Setup**: Run `.specify/scripts/bash/check-prerequisites.sh --json` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
2. **Load design documents**: Read from FEATURE_DIR:
- **Required**: plan.md (tech stack, libraries, structure), spec.md (user stories with priorities)
- **Optional**: data-model.md (entities), contracts/ (API endpoints), research.md (decisions), quickstart.md (test scenarios)
- Note: Not all projects have all documents. Generate tasks based on what's available.
3. **Execute task generation workflow**:
- Load plan.md and extract tech stack, libraries, project structure
- Load spec.md and extract user stories with their priorities (P1, P2, P3, etc.)
- If data-model.md exists: Extract entities and map to user stories
- If contracts/ exists: Map endpoints to user stories
- If research.md exists: Extract decisions for setup tasks
- Generate tasks organized by user story (see Task Generation Rules below)
- Generate dependency graph showing user story completion order
- Create parallel execution examples per user story
- Validate task completeness (each user story has all needed tasks, independently testable)
4. **Generate tasks.md**: Use `.specify/templates/tasks-template.md` as structure, fill with:
- Correct feature name from plan.md
- Phase 1: Setup tasks (project initialization)
- Phase 2: Foundational tasks (blocking prerequisites for all user stories)
- Phase 3+: One phase per user story (in priority order from spec.md)
- Each phase includes: story goal, independent test criteria, tests (if requested), implementation tasks
- Final Phase: Polish & cross-cutting concerns
- All tasks must follow the strict checklist format (see Task Generation Rules below)
- Clear file paths for each task
- Dependencies section showing story completion order
- Parallel execution examples per story
- Implementation strategy section (MVP first, incremental delivery)
5. **Report**: Output path to generated tasks.md and summary:
- Total task count
- Task count per user story
- Parallel opportunities identified
- Independent test criteria for each story
- Suggested MVP scope (typically just User Story 1)
- Format validation: Confirm ALL tasks follow the checklist format (checkbox, ID, labels, file paths)
Context for task generation: $ARGUMENTS
The tasks.md should be immediately executable - each task must be specific enough that an LLM can complete it without additional context.
## Task Generation Rules
**CRITICAL**: Tasks MUST be organized by user story to enable independent implementation and testing.
**Tests are OPTIONAL**: Only generate test tasks if explicitly requested in the feature specification or if user requests TDD approach.
### Checklist Format (REQUIRED)
Every task MUST strictly follow this format:
```text
- [ ] [TaskID] [P?] [Story?] Description with file path
```
**Format Components**:
1. **Checkbox**: ALWAYS start with `- [ ]` (markdown checkbox)
2. **Task ID**: Sequential number (T001, T002, T003...) in execution order
3. **[P] marker**: Include ONLY if task is parallelizable (different files, no dependencies on incomplete tasks)
4. **[Story] label**: REQUIRED for user story phase tasks only
- Format: [US1], [US2], [US3], etc. (maps to user stories from spec.md)
- Setup phase: NO story label
- Foundational phase: NO story label
- User Story phases: MUST have story label
- Polish phase: NO story label
5. **Description**: Clear action with exact file path
**Examples**:
- ✅ CORRECT: `- [ ] T001 Create project structure per implementation plan`
- ✅ CORRECT: `- [ ] T005 [P] Implement authentication middleware in src/middleware/auth.py`
- ✅ CORRECT: `- [ ] T012 [P] [US1] Create User model in src/models/user.py`
- ✅ CORRECT: `- [ ] T014 [US1] Implement UserService in src/services/user_service.py`
- ❌ WRONG: `- [ ] Create User model` (missing ID and Story label)
- ❌ WRONG: `T001 [US1] Create model` (missing checkbox)
- ❌ WRONG: `- [ ] [US1] Create User model` (missing Task ID)
- ❌ WRONG: `- [ ] T001 [US1] Create model` (missing file path)
### Task Organization
1. **From User Stories (spec.md)** - PRIMARY ORGANIZATION:
- Each user story (P1, P2, P3...) gets its own phase
- Map all related components to their story:
- Models needed for that story
- Services needed for that story
- Endpoints/UI needed for that story
- If tests requested: Tests specific to that story
- Mark story dependencies (most stories should be independent)
2. **From Contracts**:
- Map each contract/endpoint → to the user story it serves
- If tests requested: Each contract → contract test task [P] before implementation in that story's phase
3. **From Data Model**:
- Map each entity to the user story(ies) that need it
- If entity serves multiple stories: Put in earliest story or Setup phase
- Relationships → service layer tasks in appropriate story phase
4. **From Setup/Infrastructure**:
- Shared infrastructure → Setup phase (Phase 1)
- Foundational/blocking tasks → Foundational phase (Phase 2)
- Story-specific setup → within that story's phase
### Phase Structure
- **Phase 1**: Setup (project initialization)
- **Phase 2**: Foundational (blocking prerequisites - MUST complete before user stories)
- **Phase 3+**: User Stories in priority order (P1, P2, P3...)
- Within each story: Tests (if requested) → Models → Services → Endpoints → Integration
- Each phase should be a complete, independently testable increment
- **Final Phase**: Polish & Cross-Cutting Concerns
-30
View File
@@ -1,30 +0,0 @@
---
description: Convert existing tasks into actionable, dependency-ordered GitHub issues for the feature based on available design artifacts.
tools: ['github/github-mcp-server/issue_write']
---
## User Input
```text
$ARGUMENTS
```
You **MUST** consider the user input before proceeding (if not empty).
## Outline
1. Run `.specify/scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks` from repo root and parse FEATURE_DIR and AVAILABLE_DOCS list. All paths must be absolute. For single quotes in args like "I'm Groot", use escape syntax: e.g 'I'\''m Groot' (or double-quote if possible: "I'm Groot").
1. From the executed script, extract the path to **tasks**.
1. Get the Git remote by running:
```bash
git config --get remote.origin.url
```
> [!CAUTION]
> ONLY PROCEED TO NEXT STEPS IF THE REMOTE IS A GITHUB URL
1. For each task in the list, use the GitHub MCP server to create a new issue in the repository that is representative of the Git remote.
> [!CAUTION]
> UNDER NO CIRCUMSTANCES EVER CREATE ISSUES IN REPOSITORIES THAT DO NOT MATCH THE REMOTE URL
-80
View File
@@ -1,80 +0,0 @@
---
alwaysApply: true
---
# 项目开发规则
## 基础设置
1. 保持对话语言为中文
2. 不允许在未经允许的情况下删除代码和文件,不允许破坏已经正常的业务代码
3. 执行终端命令时要关注执行情况,避免无效等待
## 代码规范
4. 生成代码时必须添加类级和函数级注释
5. 使用import导包,禁止使用全限定名称引用类
6. 禁止使用枚举类型作为entity、request、response、dto对象的字段类型,禁止以枚举类作为方法的入参,确保接口的通用性和可扩展性
7. 新增数据的id使用已存在的雪花算法生成器生成
## 架构规范
8. 所有开发必须遵循当前项目规范
9. Controller层禁止添加业务逻辑
10. 使用全局异常处理,禁止使用try-catch
11. 前端接口访问尽可能走网关调用
## 接口设计规范
12. Controller层接口定义要完整:
- 入参使用request封装传递到service层
- service层的方法命名与controller层定义的接口的方法名称保持一致
- 出参使用response封装由service层传递到controller层
- 禁止在controller层做entity/domain对象与request/response的转换
- 使用项目已有的Result做接口返回
13. 接口和方法参数不允许超过两个,超过时使用request或DTO对象封装
14. Controller层路由禁止添加/api前缀
15. Controller层接口的Mapping注解value属性值不允许重复且不允许为空,必须明确指定value属性值且使用驼峰结构命名,避免使用下划线分隔符。所有接口注解必须显式指定value属性,如@GetMapping(value = "/page")、@PostMapping(value = "/create")等,禁止使用@GetMapping()或@PostMapping等空注解形式
16. 用户相关接口禁止直接传递用户id,需要后端根据token获取当前登录用户信息
17. 禁止使用/{param}格式的路径参数,避免网关路由冲突和分发错误
18. 路径参数统一使用@RequestParam而非@PathVariable,确保网关分发准确性
19. 接口路径命名应具有明确的语义,避免使用通用词汇如/get、/list等
20. 批量操作接口应使用专门的Request对象封装参数,而非直接传递List
21. 接口路径应避免层级过深,建议不超过3级路径结构
22. 更新操作应直接使用封装了ID和其他数据的Request对象,而不是单独传递ID参数。Service层方法应保持参数简洁,业务逻辑所需数据应全部包含在Request对象中
23. Controller层接口应保持简洁,避免为特定字段创建独立的更新方法(如updateStatus等),应只保留一个通用的update方法,具体的业务逻辑在Service层实现
24. Service层在执行更新操作时,应对每个字段进行空值检查,只更新非空字段,避免空字段覆盖原有值,确保数据完整性
25. Controller层应避免创建多个特定条件的查询接口,只保留一个分页查询方法,通过在请求对象中包含所有可查询字段来支持不同的查询需求
26. 分页查询接口应支持模糊查询和精确查询,通过在Service层根据字段类型和业务需求判断查询方式
## 环境配置
27. 为不同环境(local、dev、prod)创建单独配置文件,部署时通过参数选择
28. 启动服务时禁止擅自修改端口号,使用配置文件中的端口设置
## 数据库规范
29. 所有数据表必须包含创建时间(create_time)和更新时间(update_time)字段
30. 删除操作优先使用逻辑删除,添加deleted字段标识
31. 数据库字段命名使用下划线分隔,Java实体类使用驼峰命名
32. 优先使用LambdaQueryWrapper构造条件查询,避免硬编码字段名
33. 使用Lambda表达式引用实体类属性,提高代码可维护性和类型安全
34. 复杂查询条件应使用LambdaQueryWrapper的链式调用,保持代码清晰
35. 避免在查询条件中使用字符串字段名,防止字段名变更导致的运行时错误
## 安全规范
36. 所有外部输入必须进行参数校验
37. 敏感信息不得在日志中输出
38. 数据库操作必须使用参数化查询,防止SQL注入
## 性能规范
39. 避免N+1查询问题,合理使用批量查询
40. 大数据量查询必须分页处理
41. 缓存策略要考虑数据一致性问题
## 日志规范
42. 关键业务操作必须记录操作日志
43. 异常信息要包含足够的上下文信息
44. 生产环境禁止输出debug级别日志
+5 -1
View File
@@ -383,7 +383,11 @@ OpenClaw*.md
aiconfig-*.png
# 运行时错误日志
backend-single/backend.err
server/backend.err
# Windows 临时空文件
nul
# 服务器日志下载(tools/download-server-log.py 产物)
logs/server/*.log
logs/server/*.bak
-10
View File
@@ -1,10 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
File diff suppressed because one or more lines are too long
-29
View File
@@ -1,29 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/backend/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/common/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/customer/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/server/service/src/main/resources" type="java-resource" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.7.18" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.5.0" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:3.2.7" level="project" />
<orderEntry type="library" name="Maven: org.projectlombok:lombok:1.18.30" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-annotation:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-core:3.5.4" level="project" />
<orderEntry type="library" name="Maven: com.baomidou:mybatis-plus-extension:3.5.4" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-context:6.1.1" level="project" />
<orderEntry type="library" name="Maven: org.springframework:spring-core:6.1.1" level="project" />
</component>
</module>
-82
View File
@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile default="true" name="Default" enabled="true" />
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="emotion-museum-backend" />
<module name="emotion-single" />
<module name="backend-single" />
</profile>
<profile name="Annotation profile for emotion-museum-server" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$MAVEN_REPOSITORY$/org/projectlombok/lombok/1.18.30/lombok-1.18.30.jar" />
</processorPath>
<module name="emotion-museum-server" />
</profile>
</annotationProcessing>
<bytecodeTargetLevel>
<module name="admin" target="17" />
<module name="admin-api" target="17" />
<module name="admin-server" target="17" />
<module name="ai" target="17" />
<module name="ai-api" target="17" />
<module name="ai-server" target="17" />
<module name="auth" target="17" />
<module name="auth-api" target="17" />
<module name="auth-server" target="17" />
<module name="backend" target="17" />
<module name="common" target="17" />
<module name="customer" target="17" />
<module name="emotion-ai" target="17" />
<module name="emotion-auth" target="1.5" />
<module name="emotion-common" target="17" />
<module name="emotion-explore" target="17" />
<module name="emotion-gateway" target="17" />
<module name="emotion-growth" target="17" />
<module name="emotion-record" target="17" />
<module name="emotion-reward" target="17" />
<module name="emotion-stats" target="17" />
<module name="emotion-user" target="17" />
<module name="explore" target="17" />
<module name="explore-api" target="17" />
<module name="explore-server" target="17" />
<module name="gateway" target="17" />
<module name="growth" target="17" />
<module name="growth-api" target="17" />
<module name="growth-server" target="17" />
<module name="record" target="17" />
<module name="record-api" target="17" />
<module name="record-server" target="17" />
<module name="reward" target="17" />
<module name="reward-api" target="17" />
<module name="reward-server" target="17" />
<module name="server" target="1.5" />
<module name="service" target="17" />
<module name="stats" target="17" />
<module name="stats-api" target="17" />
<module name="stats-server" target="17" />
<module name="user" target="17" />
<module name="user-api" target="17" />
<module name="user-server" target="17" />
<module name="websocket" target="17" />
<module name="websocket-api" target="17" />
<module name="websocket-server" target="17" />
</bytecodeTargetLevel>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="backend" options="" />
<module name="common" options="" />
<module name="customer" options="" />
<module name="emotion-museum-backend" options="-parameters" />
<module name="emotion-museum-server" options="-parameters" />
</option>
</component>
</project>
-61
View File
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="localhost" uuid="5d3e10c1-409f-42d2-98e4-fb21f0c9102b">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum" uuid="01470282-aaf1-4f16-a5c2-d558bf1bc142">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend/emotion-websocket/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;zeroDateTimeBehavior=convertToNull&amp;useSSL=false&amp;serverTimezone=GMT</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="@localhost" uuid="c0166ff4-a42d-4e97-b828-e64e5885093a">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-local.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="emotion_museum@101.200.208.45" uuid="8f6bf0e5-3002-455c-9ab9-a1176155030c">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/backend-single/src/main/resources/application-prod.yml</remarks>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://101.200.208.45:3306/emotion_museum?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false&amp;allowPublicKeyRetrieval=true</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.resource.type" value="Deployment" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>
-105
View File
@@ -1,105 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend-single/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend-single/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/admin/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-ai/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-auth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/emotion-websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/explore/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/gateway/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/growth/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/record/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/reward/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/stats/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/user/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/api/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/server/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/backend/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/common/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/customer/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/service/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/server/src/main/resources" charset="UTF-8" />
</component>
</project>
-65
View File
@@ -1,65 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.huaweicloud.com/repository/maven/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-ai" />
<option name="name" value="Spring AI Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-milestones" />
<option name="name" value="Spring Milestones" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="spring-snapshots" />
<option name="name" value="Spring Snapshots" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyun-maven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="aliyunmaven" />
<option name="name" value="Aliyun Maven Repository" />
<option name="url" value="https://maven.aliyun.com/nexus/content/groups/public/" />
</remote-repository>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="MaterialThemeProjectNewConfig">
<option name="metadata">
<MTProjectMetadataState>
<option name="migrated" value="true" />
<option name="pristineConfig" value="false" />
<option name="userId" value="-37f43507:191123ac8a3:-7ffe" />
</MTProjectMetadataState>
</option>
</component>
</project>
-16
View File
@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/server/pom.xml" />
<option value="$PROJECT_DIR$/backend/pom.xml" />
<option value="$PROJECT_DIR$/backend-single/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/EmotionMuseum.iml" filepath="$PROJECT_DIR$/.idea/EmotionMuseum.iml" />
</modules>
</component>
</project>
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/sql/emotion_museum.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_ddl.sql" dialect="MySQL" />
<file url="file://$PROJECT_DIR$/sql/emotion_museum_init.sql" dialect="MySQL" />
</component>
</project>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+46 -17
View File
@@ -20,7 +20,7 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,11 +34,11 @@ This file provides guidance to Codex (Codex.ai/code) when working with code in t
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
@@ -47,7 +47,7 @@ mvn clean package -DskipTests
mvn spring-boot:run
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -216,24 +216,53 @@ bash deploy-all.sh admin
---
## 验证规则(强制
## 服务器端验收强制规则(最高优先级,不可违反
**修改任何前后端代码后,必须使用浏览器进行验证**
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
### 验证步骤
### 强制流程
1. **使用浏览器工具打开对应页面**
- 使用 `agent-browser` skill 或 MCP 工具
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
修改任意后端代码后,必须按以下顺序操作:
2. **验证标准**(全部满足才算通过)
- ✅ 前端修改已生效
- ✅ 页面没有报错
- ✅ 可以正常访问
- ✅ 浏览器 Console 中没有任何错误
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
3. **服务器验收**:在服务器上已部署的应用进行功能验收。
3. **只有验证通过才算任务完成**
### 各前端验收方式
| 前端 | 验收方式 |
|---|---|
| 小程序 | 本地 `npm run dev:h5` 启动 H5`.env.development` 已默认指向 `lifescript.happylifeos.com`),浏览器访问本地 H5 URL,后端请求走服务器 |
| 用户端 web | 直接访问服务器 URL(`https://lifescript.happylifeos.com/`)验收 |
| 管理后台 admin | 直接访问服务器 URL`https://lifescript.happylifeos.com/emotion-museum-admin/`)验收 |
### 禁止行为
- ❌ 禁止使用 `localhost` / `127.0.0.1` 后端服务做功能验收
- ❌ 禁止绕过 `deploy.py` 手动部署
- ❌ 禁止在后端编译有报错的情况下部署
- ❌ 禁止部署后不验收就认为任务完成
### 允许的唯一例外
- ✅ 单元测试 / 集成测试(测试代码可连本地或测试数据库)
- ✅ 纯前端样式/UI 调整(不涉及后端接口,可本地启动 H5 自测)
- ✅ 后端开发阶段的结构验证(仅 mvn 编译,不部署不验收)
### deploy.py 模块名速查
- `backend` — 部署后端到服务器
- `frontend` — 部署用户端 web
- `admin` — 部署管理后台
- `all` — 部署所有服务
- `verify` — 验证部署状态
### 验收通过标准
- 浏览器 Console 无任何错误
- 服务器接口响应正常(Network 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
+166 -39
View File
@@ -20,7 +20,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
```
.
├── backend-single/ # Spring Boot 单体后端服务
├── server/ # Spring Boot 单体后端服务
├── web/ # 用户前端 (Vue3 + TS + Vite)
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
@@ -34,20 +34,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## 常用命令
### 后端 (backend-single)
### 后端 (server)
```bash
# 进入后端目录
cd backend-single
cd server
# 编译打包
mvn clean package -DskipTests
# 本地运行
mvn spring-boot:run
# 本地运行(必须使用 dev-services.py
python dev-services.py start server
# 或直接运行 JAR
java -jar target/backend-single-1.0.0.jar
java -jar target/server-1.0.0.jar
# 运行测试
mvn test
@@ -65,8 +65,8 @@ cd web
# 安装依赖
npm install
# 启动开发服务器 (端口 5173)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5178
python dev-services.py start web
# 类型检查
npm run type-check
@@ -93,8 +93,8 @@ cd web-admin
# 安装依赖
npm install
# 启动开发服务器 (端口 5174)
npm run dev
# 启动开发服务器(必须使用 dev-services.py,固定端口 5179
python dev-services.py start web-admin
# 类型检查
npm run type-check
@@ -130,10 +130,10 @@ npm run build:h5
**H5 开发模式**(日常开发,推荐):
```bash
cd mini-program
# 或使用启动脚本
start-h5-dev.bat
# 或使用 dev-services.py 启动(固定端口 5180
python dev-services.py start mini-program
```
访问 `http://localhost:5173`,登录态保留,支持热更新。
访问 `http://localhost:5180`,登录态保留,支持热更新。
**微信小程序开发模式**(仅用于小程序特性验证):
```bash
@@ -216,48 +216,125 @@ bash deploy-all.sh admin
---
## 验证规则(强制
## 服务器端验收强制规则(最高优先级,不可违反
**修改任何前后端代码后,必须使用浏览器进行验证**
**所有前端功能验收(小程序、用户端 web、管理后台 admin)必须使用服务器上部署的后端服务,禁止使用本地后端服务验收。**
### 验证步骤
### 强制流程
1. **使用浏览器工具打开对应页面**
- 使用 `agent-browser` skill 或 MCP 工具
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
修改任意后端代码后,必须按以下顺序操作:
2. **验证标准**(全部满足才算通过)
- ✅ 前端修改已生效
- ✅ 页面没有报错
- ✅ 可以正常访问
- ✅ 浏览器 Console 中没有任何错误
1. **编译验证**:本地 `mvn clean install -DskipTests -am` 必须通过,零报错。
2. **部署到服务器**:执行 `python deploy.py backend`(或其他对应模块名),确保部署成功、无报错。
3. **服务器验收**:在服务器上已部署的应用进行功能验收。
3. **只有验证通过才算任务完成**
### 各前端验收方式
| 前端 | 验收方式 |
|---|---|
| 小程序 | 本地 `npm run dev:h5` 启动 H5`.env.development` 已默认指向 `lifescript.happylifeos.com`),浏览器访问本地 H5 URL,后端请求走服务器 |
| 用户端 web | 直接访问服务器 URL(`https://lifescript.happylifeos.com/`)验收 |
| 管理后台 admin | 直接访问服务器 URL`https://lifescript.happylifeos.com/emotion-museum-admin/`)验收 |
### 禁止行为
- ❌ 禁止使用 `localhost` / `127.0.0.1` 后端服务做功能验收
- ❌ 禁止绕过 `deploy.py` 手动部署
- ❌ 禁止在后端编译有报错的情况下部署
- ❌ 禁止部署后不验收就认为任务完成
### 允许的唯一例外
- ✅ 单元测试 / 集成测试(测试代码可连本地或测试数据库)
- ✅ 纯前端样式/UI 调整(不涉及后端接口,可本地启动 H5 自测)
- ✅ 后端开发阶段的结构验证(仅 mvn 编译,不部署不验收)
### deploy.py 模块名速查
- `backend` — 部署后端到服务器
- `frontend` — 部署用户端 web
- `admin` — 部署管理后台
- `all` — 部署所有服务
- `verify` — 验证部署状态
### 验收通过标准
- 浏览器 Console 无任何错误
- 服务器接口响应正常(Network 面板查看)
- 业务功能按预期工作
- 只有验收通过才算任务完成
---
## 热加载规则(强制
## 部署强制规则(最高优先级,不可违反
**修改前后端代码后,优先利用热加载,避免不必要的重启**
**本地开发完成、验证通过、验收没有问题后,必须执行 `deploy.py` 部署脚本,将应用部署到服务器上。**
### 判断逻辑
### 强制流程
每次功能开发或 bug 修复完成后,必须按以下顺序操作:
1. **本地验证**:在本地开发环境中完成功能开发和自测,确认无报错。
2. **验收通过**:按"服务器端验收强制规则"完成验收,确认功能正常。
3. **执行部署**:验收通过后,立即执行部署脚本将应用部署到服务器:
- 后端:`python deploy.py backend`
- 前端:`python deploy.py frontend`
- 管理后台:`python deploy.py admin`
- 全部:`python deploy.py all`
4. **部署验证**:部署完成后,在服务器上验证功能正常,无报错。
### 禁止行为
- ❌ 禁止本地开发完成后不部署到服务器就认为任务完成
- ❌ 禁止跳过验收直接部署
- ❌ 禁止部署后不验证就结束任务
- ❌ 禁止在编译或验收有报错的情况下部署
### 允许的唯一例外
- ✅ 纯文档修改(不涉及代码变更)
- ✅ 仅在本地运行的脚本工具(不涉及线上服务)
---
## 本地服务管理规则(强制)
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `dev-services.py` 脚本。**
### 强制要求
- ✅ 必须使用:`python dev-services.py [start|stop|restart|status|discover|logs] [服务名]`
- ❌ 禁止直接使用 `npm run dev` / `pnpm run dev` 启动前端
- ❌ 禁止直接使用 `npm run dev:h5` 启动小程序 H5
- ❌ 禁止直接使用 `mvn spring-boot:run` 启动后端
- ❌ 禁止无意义重启支持热加载的服务
### 前端/H5 固定端口
| 服务 | 目录 | 固定端口 |
|---|---|---|
| web | `web/` | 5178 |
| web-admin | `web-admin/` | 5179 |
| mini-program | `mini-program/` | 5180 |
| life-script | `life-script/` | 5181 |
后端服务端口保持原有配置不变。
### 禁止无意义重启
- 只有修改必须重启才能生效的文件时,才允许执行 `restart`
- 修改源码文件(`.vue``.tsx``.ts``.js``.css``.scss` 等)时,`dev-services.py restart` 会拒绝重启
- 确需强制重启时,使用 `python dev-services.py restart --force`
### 热加载规则
1. **不需要重启的场景**(热加载自动生效)
- 前端:修改 `.vue``.tsx``.ts``.js``.css``.scss` 等源码文件
- 后端:修改业务逻辑、API 接口、组件代码等支持热更新的文件
- 样式、模板、静态资源等修改
2. **需要重启的场景**(热加载无法生效)
2. **需要重启的场景**
- 前端:修改 `vite.config.ts``tsconfig.json``.env``package.json`、别名配置等
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
- 新增或删除依赖包后
### 操作原则
- 修改后先观察终端输出,确认热加载是否成功
- 只有修改了必须重启才能生效的配置时,才执行重启操作
- 重启前后端服务前,需告知用户
- 后端:按现有后端规则处理(本地不启动后端,使用服务器验收)
---
@@ -338,3 +415,53 @@ bash deploy-all.sh admin
- 关键业务操作必须记录操作日志
- 异常信息要包含足够的上下文信息
- 生产环境禁止输出 debug 级别日志
---
## 日志排查规则(强制)
**遇到"对话不存在或无权限"错误时,按以下顺序排查:**
### 1. 检查 conversationId 是否为空
- **前端检查**:在浏览器 DevTools Network 面板查看请求参数 `conversationId`
- **后端检查**:查看 `UserContextHolder.getCurrentUserId()` 是否返回有效值
- **数据库检查**:执行 `SELECT * FROM t_conversation WHERE id = '<conversationId>'` 确认对话是否存在
### 2. 检查权限匹配
- **前端问题**:当前登录用户的 userId 与对话的 userId 不一致
- **后端逻辑**`ScriptMessageServiceImpl.listByConversation` 中会校验 `conversation.getUserId().equals(currentUserId)`
- **修复方案**:确认前端传递的是正确的 conversationId,或重新创建对话
### 3. 常见原因
| 原因 | 解决方案 |
|------|----------|
| conversationId 为空字符串 | 前端增加短路保护,显示"暂无对话记录"提示 |
| 对话不存在(已删除) | 引导用户创建新对话 |
| 用户未登录 | 检查 Token 是否有效,重新登录 |
| 对话属于其他用户 | 确认前端传递的 conversationId 正确 |
### 4. 快速定位方法
使用 `tools/download-server-log.py` 脚本下载服务器日志:
```bash
# 下载完整日志
python tools/download-server-log.py latest
# 只看最近 50 条错误
python tools/download-server-log.py errors 50
# 搜索特定关键词(如 listByConversation
python tools/download-server-log.py grep "listByConversation" 20
```
日志位置:`logs/server/` 目录
### 5. 防御性编程原则
- **Service 层**:对空参数做防御性检查,返回空列表而非抛异常
- **Controller 层**:参数校验失败时返回友好错误提示
- **前端**:接口调用前做空值检查,避免发送无效请求
+1 -1
View File
@@ -70,7 +70,7 @@
│ ├── emotion-reward/ # 成就奖励服务
│ ├── emotion-stats/ # 统计分析服务
│ └── emotion-common/ # 公共模块
├── backend-single/ # SpringBoot单体后端服务
├── server/ # SpringBoot单体后端服务
├── web/ # 旧版Web前端
├── web-new/ # 新版Web前端(Vue3+TS)
└── 开心APP网页代码v1.1/ # 早期网页版本
+33 -113
View File
@@ -1,26 +1,12 @@
# Emotion Museum HTTPS Nginx 配置
# 配置路径:/www/server/panel/vhost/nginx/emotion-museum.conf
# 域名:lifescript.happylifeos.com
# HTTP 服务器 - 强制跳转 HTTPS
server {
listen 80;
server_name lifescript.happylifeos.com;
# 强制跳转 HTTPS
return 301 https://$server_name$request_uri;
}
# HTTPS 服务器
server {
listen 443 ssl;
server_name lifescript.happylifeos.com;
# SSL 证书配置
ssl_certificate /etc/letsencrypt/live/lifescript.happylifeos.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/lifescript.happylifeos.com/privkey.pem;
# SSL 优化配置
ssl_protocols TLSv1.2 TLSv1.3;
# WeChat mini program/Cronet compatibility: keep TLS 1.2 enabled with broad modern suites.
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
@@ -29,97 +15,73 @@ server {
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
add_header Strict-Transport-Security max-age=31536000 always;
# HSTS (可选,生产环境建议开启)
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# ACME 挑战目录 - 用于 SSL 证书申请和续期
location ^~ /.well-known/acme-challenge/ {
root /data/www/acme-challenge;
allow all;
try_files $uri =404;
}
# 根路径 - 用户前端应用
location = / {
return 404;
rewrite ^ /emotion-museum/ permanent;
}
location / {
location /emotion-museum/ {
alias /data/www/emotion-museum/;
autoindex off;
# 处理 Vue Router 的 history 模式
try_files $uri $uri/ /index.html;
# HTML 文件不缓存
location ~ \.html?$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 静态资源缓存 1 年
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# 静态资源直接返回文件,不经过 try_files
location ~* ^/emotion-museum/(assets|static)/ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
try_files $uri =404;
}
# SPA 路由兜底
try_files $uri $uri/ /emotion-museum/index.html;
}
location = /emotion-museum {
rewrite ^(.*)$ $1/ permanent;
}
# 管理后台应用路径
location /emotion-museum-admin/ {
alias /data/www/emotion-museum-admin/;
autoindex off;
# 处理 Vue Router 的 history 模式
try_files $uri $uri/ /emotion-museum-admin/index.html;
# HTML 文件不缓存
location ~ \.html?$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 静态资源缓存 1 年
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
}
}
location = /emotion-museum-admin {
rewrite ^(.*)$ $1/ permanent;
}
# Life-Script 应用路径
location /course-of-life/ {
alias /data/www/course-of-life/;
autoindex off;
try_files $uri $uri/ /course-of-life/index.html;
}
location = /course-of-life {
rewrite ^ /course-of-life/ last;
}
# life-script React 应用
location /life-script/ {
alias /data/www/life-script/;
autoindex off;
# 处理 React Router 的 history 模式
try_files $uri $uri/ /life-script/index.html;
# HTML 文件不缓存
location ~ \.html?$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 静态资源缓存 1 年
location ~ \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
# 静态资源直接返回文件
location ~* ^/life-script/(assets|static)/ {
add_header Cache-Control "public, max-age=31536000, immutable";
expires 1y;
try_files $uri =404;
}
# SPA 路由兜底
try_files $uri $uri/ /life-script/index.html;
}
location = /life-script {
rewrite ^ /life-script/ last;
}
# 后端 API 代理
location /api {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
@@ -131,50 +93,6 @@ server {
proxy_read_timeout 60s;
}
# Swagger UI / Knife4j API 文档代理
# 使用 ^~ 前缀匹配,防止被 / 的 try_files 拦截
location ^~ /swagger-ui {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /v3/api-docs {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /doc.html {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /webjars {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Knife4j 4.x 额外需要的路径(swagger-resources 等)
location ^~ /swagger-resources {
proxy_pass http://127.0.0.1:19089;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# WebSocket 代理
location /ws {
proxy_pass http://127.0.0.1:19089;
proxy_http_version 1.1;
@@ -189,21 +107,23 @@ server {
proxy_read_timeout 7d;
}
# 健康检查端点
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# 隐藏文件访问控制
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# 日志配置
access_log /www/wwwlogs/ssl-access.log;
error_log /www/wwwlogs/ssl-error.log;
}
server {
listen 80;
server_name lifescript.happylifeos.com;
return 301 https://$host$request_uri;
}
+7 -4
View File
@@ -23,6 +23,7 @@ Purpose: 统一部署所有服务到生产服务器,替代原有的多个 shel
import io
import os
import re
import shlex
import sys
import time
import subprocess
@@ -395,14 +396,14 @@ def deploy_nginx():
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh remote
# 后端 - 调用 server/deploy.sh remote
# ============================================================================
def deploy_backend():
"""部署后端服务"""
log_section("部署后端服务")
deploy_script = PROJECT_DIR / "backend-single" / "deploy.py"
deploy_script = PROJECT_DIR / "server" / "deploy.py"
if not deploy_script.exists():
log_error(f"后端部署脚本不存在: {deploy_script}")
return False
@@ -410,7 +411,7 @@ def deploy_backend():
log_info("执行后端部署...")
ok, _, err = run_command(
f"{PYTHON} deploy.py remote",
cwd=str(PROJECT_DIR / "backend-single"),
cwd=str(PROJECT_DIR / "server"),
timeout=600,
capture=False
)
@@ -524,8 +525,10 @@ def verify_deploy():
]
for url, label in endpoints:
null_device = 'NUL' if sys.platform == 'win32' else '/dev/null'
safe_url = shlex.quote(url)
ok, stdout, _ = run_command(
f'curl -k -s -o /dev/null -w "HTTP %{{http_code}}" {url}',
f'curl -k -s -o {null_device} -w "HTTP %{{http_code}}" {safe_url}',
timeout=30
)
if ok and stdout:
+4 -4
View File
@@ -74,16 +74,16 @@ deploy_nginx() {
}
# ============================================================================
# 后端 - 调用 backend-single/deploy.sh
# 后端 - 调用 server/deploy.sh
# ============================================================================
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
log_error "后端部署脚本不存在: backend-single/deploy.sh"
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在: server/deploy.sh"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
if bash deploy.sh remote; then
cd ..
log_info "✅ 后端部署完成"
+112 -8
View File
@@ -67,6 +67,25 @@ DEFAULT_PORTS = {
"spring-boot-gradle": 8080,
}
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
# ANSI 颜色
COLORS = {
"INFO": "\033[37m",
@@ -328,36 +347,103 @@ def _update_service_port(svc: Service, port: int):
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# 前端框架如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
def _extract_port_from_pom(pom_path: Path) -> Optional[int]:
"""从 pom.xml 提取 server.port"""
try:
@@ -1553,6 +1639,7 @@ def main():
parser.add_argument("--type", dest="svc_type", default=None, help="服务类型过滤: vite|python|spring-boot|node-backend")
parser.add_argument("--port", dest="port", type=int, default=None, help="端口号过滤")
parser.add_argument("--foreground", "-f", action="store_true", help="前台模式运行")
parser.add_argument("--force", action="store_true", help="强制重启,忽略热加载保护")
args = parser.parse_args()
@@ -1631,6 +1718,23 @@ def main():
return
if args.action == "restart":
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
blocked.append((svc.name, changed_files))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
for svc in services:
restart_service(svc)
else:
@@ -570,13 +570,13 @@ deploy_ssl() {
deploy_backend() {
log_section "部署后端服务"
if [ ! -f "backend-single/deploy.sh" ]; then
if [ ! -f "server/deploy.sh" ]; then
log_error "后端部署脚本不存在"
return 1
fi
log_info "执行后端部署..."
cd backend-single
cd server
bash deploy.sh remote
local result=$?
cd ..
@@ -828,7 +828,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
### Task 9: 检查并修改后端 CORS 配置
**Files:**
- Modify: `backend-single/src/main/resources/application.yml` (或相应配置文件)
- Modify: `server/src/main/resources/application.yml` (或相应配置文件)
- [ ] **Step 1: 读取当前后端配置文件**
@@ -841,7 +841,7 @@ git commit -m "config: 更新管理后台部署脚本访问地址提示"
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "config: 更新 CORS 配置允许新域名访问"
```
@@ -4,7 +4,7 @@
**Goal:** Add mini program behavior analytics collection, backend event storage and aggregation, and a web-admin behavior analytics dashboard.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `backend-single` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Architecture:** The mini program queues analytics events locally and flushes them to `POST /analytics/events/batch`. `server` stores raw events in `t_analytics_event` and exposes admin-only aggregation endpoints under `/admin/analytics`. `web-admin` renders overview, trend, funnel, preference, and top-event stats using Element Plus and ECharts.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, MySQL JSON, Java 17, uni-app/Vue, Vue 3, Element Plus, ECharts.
@@ -13,25 +13,25 @@
## File Structure
- Create `sql/2026-05-17-analytics-event.sql`: migration for `t_analytics_event`.
- Create `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`: MyBatis Plus entity.
- Create `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`: mapper plus aggregation SQL.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`: mini program batch request.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`: one event payload.
- Create `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`: admin query filters.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsBatchResponse.java`: accepted/rejected counts.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsOverviewResponse.java`: overview cards.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`: trend item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsFunnelItem.java`: funnel item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`: preference item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`: top event item.
- Create `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsUserItem.java`: user ranking item.
- Create `server/src/main/java/com/emotion/service/AnalyticsService.java`: write and aggregate contract.
- Create `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`: validation, persistence, aggregation.
- Create `server/src/main/java/com/emotion/controller/AnalyticsController.java`: mini program write endpoint.
- Create `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`: admin read endpoints.
- Modify `server/src/main/java/com/emotion/config/WebMvcConfig.java`: allow anonymous analytics batch endpoint while keeping admin analytics protected.
- Create `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`: service validation and aggregation tests.
- Create `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`: endpoint tests.
- Create `mini-program/src/services/analytics.js`: client SDK.
- Modify `mini-program/src/App.vue`: initialize and flush analytics.
- Modify `mini-program/src/pages/main/index.vue`: track main tab/page activity if this page owns tab switching.
@@ -121,11 +121,11 @@ git commit -m "feat: add analytics event table"
### Task 2: Add Backend DTOs and Entity
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `backend-single/src/main/java/com/emotion/dto/response/analytics/`
- Create: `server/src/main/java/com/emotion/entity/AnalyticsEvent.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java`
- Create: response DTO files under `server/src/main/java/com/emotion/dto/response/analytics/`
- [ ] **Step 1: Add the entity**
@@ -387,7 +387,7 @@ public class AnalyticsUserItem {
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -396,7 +396,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/AnalyticsEvent.java backend-single/src/main/java/com/emotion/dto/request/analytics backend-single/src/main/java/com/emotion/dto/response/analytics
git add server/src/main/java/com/emotion/entity/AnalyticsEvent.java server/src/main/java/com/emotion/dto/request/analytics server/src/main/java/com/emotion/dto/response/analytics
git commit -m "feat: add analytics DTOs and entity"
```
@@ -405,10 +405,10 @@ git commit -m "feat: add analytics DTOs and entity"
### Task 3: Add Analytics Mapper and Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `backend-single/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- Create: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Create: `server/src/main/java/com/emotion/service/AnalyticsService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AnalyticsServiceTest.java`
- [ ] **Step 1: Write service tests**
@@ -462,7 +462,7 @@ public class AnalyticsServiceTest {
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -676,7 +676,7 @@ public class AnalyticsServiceImpl extends ServiceImpl<AnalyticsEventMapper, Anal
Run:
```bash
cd backend-single
cd server
mvn -Dtest=AnalyticsServiceTest test
```
@@ -685,7 +685,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 7: Commit**
```bash
git add backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java backend-single/src/main/java/com/emotion/service/AnalyticsService.java backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java backend-single/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git add server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java server/src/main/java/com/emotion/service/AnalyticsService.java server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java server/src/test/java/com/emotion/service/AnalyticsServiceTest.java
git commit -m "feat: add analytics service"
```
@@ -694,10 +694,10 @@ git commit -m "feat: add analytics service"
### Task 4: Add Analytics Controllers and Auth Exclusion
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- Create: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Create: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Test: `server/src/test/java/com/emotion/controller/AnalyticsControllerTest.java`
- [ ] **Step 1: Add write controller**
@@ -794,7 +794,7 @@ Do not exclude `/admin/analytics/**`.
Run:
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -803,7 +803,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AnalyticsController.java backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/controller/AnalyticsController.java server/src/main/java/com/emotion/controller/AdminAnalyticsController.java server/src/main/java/com/emotion/config/WebMvcConfig.java
git commit -m "feat: expose analytics APIs"
```
@@ -1323,7 +1323,7 @@ git commit -m "feat: add analytics admin dashboard"
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -4,7 +4,7 @@
**Goal:** Add CPU-friendly open-source text-to-speech generation for user scripts, with cached backend tasks and mini program playback.
**Architecture:** `backend-single` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Architecture:** `server` creates TTS tasks, validates source ownership, cleans script text, checks audio cache, and calls a private Python FastAPI TTS service. The Python service runs on `101.200.208.45` and writes audio files under `/data/uploads/emotion-museum/tts`. The mini program requests generation from `ScriptDetailView.vue`, polls status, and plays the returned audio URL with `uni.createInnerAudioContext()`.
**Tech Stack:** Spring Boot 2.7, MyBatis Plus, Java 17, MySQL, Python FastAPI, MeloTTS or equivalent CPU-friendly Chinese TTS engine, systemd, uni-app/Vue.
@@ -13,22 +13,22 @@
## File Structure
- Create `sql/2026-05-17-tts-task.sql`: `t_tts_task` migration.
- Create `backend-single/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `backend-single/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `backend-single/src/main/resources/application.yml`: TTS config defaults.
- Modify `backend-single/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `backend-single/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `backend-single/tts-service/app.py`: FastAPI service.
- Create `backend-single/tts-service/README.md`: server setup.
- Create `backend-single/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `server/src/main/java/com/emotion/entity/TtsTask.java`: task entity.
- Create `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`: task mapper.
- Create `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`: create request.
- Create `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`: task response.
- Create `server/src/main/java/com/emotion/service/TtsTaskService.java`: contract.
- Create `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`: ownership, cache, async processing.
- Create `server/src/main/java/com/emotion/service/TtsEngineClient.java`: internal engine client contract.
- Create `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`: calls Python service.
- Create `server/src/main/java/com/emotion/controller/TtsController.java`: user-facing TTS endpoints.
- Modify `server/src/main/resources/application.yml`: TTS config defaults.
- Modify `server/src/main/resources/application-prod.yml`: production paths and internal URL.
- Create `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`: service tests.
- Create `server/tts-service/requirements.txt`: FastAPI deps; MeloTTS is installed from the official repository because the official docs use editable install plus `python -m unidic download`.
- Create `server/tts-service/app.py`: FastAPI service.
- Create `server/tts-service/README.md`: server setup.
- Create `server/tts-service/emotion-museum-tts.service`: systemd unit.
- Create `mini-program/src/services/tts.js`: TTS API client.
- Create `mini-program/src/components/ScriptAudioPlayer.vue`: player component.
- Modify `mini-program/src/pages/main/ScriptDetailView.vue`: add player.
@@ -93,10 +93,10 @@ git commit -m "feat: add tts task table"
### Task 2: Add Backend TTS Entity, DTOs, and Mapper
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- Create: `server/src/main/java/com/emotion/entity/TtsTask.java`
- Create: `server/src/main/java/com/emotion/mapper/TtsTaskMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/tts/TtsTaskResponse.java`
- [ ] **Step 1: Add entity**
@@ -205,7 +205,7 @@ public class TtsTaskResponse {
- [ ] **Step 4: Compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -214,7 +214,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/TtsTask.java backend-single/src/main/java/com/emotion/mapper/TtsTaskMapper.java backend-single/src/main/java/com/emotion/dto/request/tts backend-single/src/main/java/com/emotion/dto/response/tts
git add server/src/main/java/com/emotion/entity/TtsTask.java server/src/main/java/com/emotion/mapper/TtsTaskMapper.java server/src/main/java/com/emotion/dto/request/tts server/src/main/java/com/emotion/dto/response/tts
git commit -m "feat: add tts task model"
```
@@ -223,13 +223,13 @@ git commit -m "feat: add tts task model"
### Task 3: Add TTS Service and Engine Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `backend-single/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Test: `backend-single/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- Create: `server/src/main/java/com/emotion/service/TtsTaskService.java`
- Create: `server/src/main/java/com/emotion/service/TtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java`
- Create: `server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- Test: `server/src/test/java/com/emotion/service/TtsTaskServiceTest.java`
- [ ] **Step 1: Add service contracts**
@@ -503,7 +503,7 @@ public class TtsTaskServiceImpl extends ServiceImpl<TtsTaskMapper, TtsTask> impl
- [ ] **Step 5: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -512,7 +512,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/TtsTaskService.java backend-single/src/main/java/com/emotion/service/TtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java backend-single/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java backend-single/src/main/resources/application.yml backend-single/src/main/resources/application-prod.yml
git add server/src/main/java/com/emotion/service/TtsTaskService.java server/src/main/java/com/emotion/service/TtsEngineClient.java server/src/main/java/com/emotion/service/impl/HttpTtsEngineClient.java server/src/main/java/com/emotion/service/impl/TtsTaskServiceImpl.java server/src/main/resources/application.yml server/src/main/resources/application-prod.yml
git commit -m "feat: add tts backend service"
```
@@ -521,7 +521,7 @@ git commit -m "feat: add tts backend service"
### Task 4: Add TTS Controller
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Create: `server/src/main/java/com/emotion/controller/TtsController.java`
- [ ] **Step 1: Add controller**
@@ -569,7 +569,7 @@ public class TtsController {
- [ ] **Step 2: Run compile**
```bash
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -578,7 +578,7 @@ Expected: `BUILD SUCCESS`.
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/TtsController.java
git add server/src/main/java/com/emotion/controller/TtsController.java
git commit -m "feat: expose tts task APIs"
```
@@ -587,10 +587,10 @@ git commit -m "feat: expose tts task APIs"
### Task 5: Add Python TTS Service Skeleton
**Files:**
- Create: `backend-single/tts-service/requirements.txt`
- Create: `backend-single/tts-service/app.py`
- Create: `backend-single/tts-service/README.md`
- Create: `backend-single/tts-service/emotion-museum-tts.service`
- Create: `server/tts-service/requirements.txt`
- Create: `server/tts-service/app.py`
- Create: `server/tts-service/README.md`
- Create: `server/tts-service/emotion-museum-tts.service`
- [ ] **Step 1: Add requirements**
@@ -717,7 +717,7 @@ curl http://127.0.0.1:19110/health
- [ ] **Step 5: Run local syntax check**
```bash
python -m py_compile backend-single/tts-service/app.py
python -m py_compile server/tts-service/app.py
```
Expected: no output and exit code 0.
@@ -725,7 +725,7 @@ Expected: no output and exit code 0.
- [ ] **Step 6: Commit**
```bash
git add backend-single/tts-service
git add server/tts-service
git commit -m "feat: add private tts service scaffold"
```
@@ -960,7 +960,7 @@ git commit -m "feat: add tts control to script detail"
- [ ] **Step 1: Upload service directory**
```bash
scp -r backend-single/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
scp -r server/tts-service root@101.200.208.45:/data/programs/emotion-museum/tts-service
```
Expected: files are copied.
@@ -988,7 +988,7 @@ Expected:
- [ ] **Step 4: Install systemd unit after health passes**
```bash
scp backend-single/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
scp server/tts-service/emotion-museum-tts.service root@101.200.208.45:/etc/systemd/system/emotion-museum-tts.service
ssh root@101.200.208.45 "systemctl daemon-reload && systemctl enable emotion-museum-tts && systemctl restart emotion-museum-tts && systemctl status emotion-museum-tts --no-pager"
```
@@ -997,7 +997,7 @@ Expected: service status is active.
- [ ] **Step 5: Commit deployment doc tweaks if needed**
```bash
git add backend-single/tts-service/README.md
git add server/tts-service/README.md
git commit -m "docs: update tts deployment notes"
```
@@ -1013,7 +1013,7 @@ Only run this commit if README changed.
- [ ] **Step 1: Run backend tests**
```bash
cd backend-single
cd server
mvn test
```
@@ -44,21 +44,21 @@ This plan does not implement:
Backend:
- Create: `backend-single/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `backend-single/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `backend-single/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `backend-single/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `backend-single/src/main/java/com/emotion/service/impl/`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Create: `server/src/main/java/com/emotion/entity/SocialContentItem.java`
- Create: `server/src/main/java/com/emotion/entity/SocialProfileInsight.java`
- Create: `server/src/main/java/com/emotion/entity/UserConsentLog.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialContentItemMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/SocialProfileInsightMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/UserConsentLogMapper.java`
- Create: `server/src/main/java/com/emotion/dto/request/social/*.java`
- Create: `server/src/main/java/com/emotion/dto/response/social/*.java`
- Create: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Create: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Create: `server/src/main/java/com/emotion/service/SocialContentService.java`
- Create: `server/src/main/java/com/emotion/service/SocialInsightService.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create implementations under `server/src/main/java/com/emotion/service/impl/`
- Modify: `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- Modify SQL schema/migration file used by this repo.
Mini program:
@@ -212,7 +212,7 @@ public interface SocialContentItemMapper extends BaseMapper<SocialContentItem> {
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -221,8 +221,8 @@ Expected: compile succeeds.
## Task 3: Social Content API
**Files:**
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `backend-single/src/main/java/com/emotion/dto/response/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/request/social/`
- Create DTOs under `server/src/main/java/com/emotion/dto/response/social/`
- Create: `SocialContentController.java`
- Create: `SocialContentService.java`
- Create: `SocialContentServiceImpl.java`
@@ -317,7 +317,7 @@ Do not accept screenshots larger than the configured limit.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -405,7 +405,7 @@ Rules:
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -414,8 +414,8 @@ Expected: compile succeeds.
## Task 5: Script Context Integration
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ScriptContextService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ScriptContextServiceImpl.java`
- Modify: `EpicScriptServiceImpl.java`
- [ ] **Step 1: Implement context service**
@@ -468,7 +468,7 @@ When social insight context is non-empty, add analytics/event hook if backend an
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -658,7 +658,7 @@ Expected: build succeeds.
Run:
```powershell
cd backend-single
cd server
mvn -DskipTests compile
```
@@ -707,7 +707,7 @@ Check:
- [ ] **Step 5: Commit**
```powershell
git add backend-single mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git add server mini-program sql docs/superpowers/specs/2026-05-19-social-data-import-script-profile-design.md docs/superpowers/plans/2026-05-19-social-data-import-script-profile-plan.md
git commit -m "feat: add social data import design and profile plan"
```
@@ -13,10 +13,10 @@
### Task 1: ASR Service
**Files:**
- Create: `backend-single/asr-service/app.py`
- Create: `backend-single/asr-service/requirements.txt`
- Create: `backend-single/asr-service/emotion-museum-asr.service`
- Create: `backend-single/asr-service/README.md`
- Create: `server/asr-service/app.py`
- Create: `server/asr-service/requirements.txt`
- Create: `server/asr-service/emotion-museum-asr.service`
- Create: `server/asr-service/README.md`
- [x] Add a FastAPI service with `/health` and `/transcribe`.
- [x] Accept an uploaded audio file, save it under `/tmp/emotion-museum-asr`, run the ASR model, and return JSON with `success`, `text`, `language`, `durationMs`, `engine`, and `errorMessage`.
@@ -25,12 +25,12 @@
### Task 2: Java Backend Proxy
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `backend-single/src/main/java/com/emotion/service/AsrService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `backend-single/src/main/resources/application-prod.yml`
- Create: `server/src/main/java/com/emotion/dto/response/asr/AsrTranscribeResponse.java`
- Create: `server/src/main/java/com/emotion/service/AsrService.java`
- Create: `server/src/main/java/com/emotion/service/impl/AsrServiceImpl.java`
- Create: `server/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application-prod.yml`
- [x] Add `emotion.asr` config for `enabled`, `engine-url`, `max-file-size`, and `allowed-types`.
- [x] Validate upload presence, size, and suffix.
@@ -14,10 +14,10 @@
| 文件 | 职责 | 操作 |
|------|------|------|
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` | DTO:新增 endpointId 字段,更新 RESERVED_KEYS | 修改 |
| `server/src/main/java/com/emotion/service/AiRuntimeService.java` | Service 接口:新增 testEndpoint + invokeEndpointStream | 修改 |
| `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` | Service 实现:新增两个方法的完整实现 | 修改 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | Controller:新增 /endpoint/test 和 /endpoint/stream | 修改 |
| `web-admin/src/types/aiconfig.ts` | TypeScript 类型:新增 AiEndpointRuntimeRequest | 修改 |
| `web-admin/src/api/aiconfig.ts` | API 层:新增 testEndpointRuntime、streamEndpointRuntime、提取 fetchSseStream | 修改 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | Vue 组件:增加行内测试按钮 + endpoint 测试对话框 | 修改 |
@@ -27,7 +27,7 @@
### Task 1: DTO 增强 — AiRuntimeRequest 新增 endpointId
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 新增 endpointId 字段和 RESERVED_KEYS**
@@ -82,13 +82,13 @@ public static AiRuntimeRequest fromPayload(JSONObject payload) {
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESSendpointId 字段新增不破坏任何编译,因为这是新增字段)
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java
git commit -m "feat: AiRuntimeRequest DTO 新增 endpointId 字段
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -99,7 +99,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 2: Service 接口层 — 新增 testEndpoint + invokeEndpointStream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- [ ] **Step 1: 新增两个接口方法**
@@ -128,13 +128,13 @@ public interface AiRuntimeService {
- [ ] **Step 2: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS(接口新增方法,但实现类尚未修改,此时不会编译失败 — 因为 ServiceImpl 编译是单独的任务)
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiRuntimeService.java
git add server/src/main/java/com/emotion/service/AiRuntimeService.java
git commit -m "feat: AiRuntimeService 接口新增 endpoint 测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -145,7 +145,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 3: Service 实现层 — testEndpoint + invokeEndpointStream 实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [ ] **Step 1: 新增 testEndpoint 方法**
@@ -271,13 +271,13 @@ public void invokeEndpointStream(String endpointId, Map<String, Object> inputs,
- [ ] **Step 3: 编译验证**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 4: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git commit -m "feat: AiRuntimeServiceImpl 实现 endpoint 直接测试方法
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -288,7 +288,7 @@ Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
### Task 4: Controller 层 — 新增 /endpoint/test 和 /endpoint/stream
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 runtimeStream 方法之后新增两个 endpoint 方法**
@@ -323,13 +323,13 @@ public SseEmitter endpointStream(@RequestBody JSONObject payload) {
- [ ] **Step 2: 编译并验证完整后端**
Run: `cd backend-single && mvn clean compile -pl :backend-single -am -DskipTests`
Run: `cd server && mvn clean compile -pl :server -am -DskipTests`
Expected: BUILD SUCCESS
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: AiRoutingController 新增 /endpoint/test 和 /endpoint/stream 接口
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>"
@@ -13,10 +13,10 @@
### Task 1: Backend Test Sample Contract
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ai/AiTestTemplateResponse.java`
- Modify: `server/src/main/java/com/emotion/service/AiRuntimeService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [x] Add a response DTO with `sceneCode`, `endpointId`, `endpointCode`, `providerType`, `inputs`, and `paramFields`.
- [x] Add service methods `buildEndpointTestTemplate(String endpointId)` and `buildSceneTestTemplate(String sceneCode)`.
@@ -26,9 +26,9 @@
### Task 2: Provider Runtime Compatibility
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/ProviderHttpSupport.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add `Accept: text/event-stream` for stream requests.
- [x] Make Coze workflow body match historical calls: `workflow_id`, `user_id`, `stream`, `parameters`, and optional `bot_id`.
@@ -22,9 +22,9 @@
### Task 2: User Runtime Request Shape
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- [x] Accept user calls as `sceneCode` plus `inputs` JSON.
- [x] Keep user identity server-side by reading `UserContextHolder`.
@@ -13,8 +13,8 @@
### Task 1: Backend Chat Bridge To Runtime
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WebSocketServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/websocket/WebSocketMessage.java`
- [x] Route chat generation through `AiRuntimeService` with `sceneCode=chat`.
- [x] Push `start`, `delta`, `done`, and `error` events over the existing user WebSocket topic.
@@ -13,8 +13,8 @@
### Task 1: Backend Stream Text Normalization
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `backend-single/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/CozeProviderAdapter.java`
- Modify: `server/src/main/java/com/emotion/service/ai/DifyProviderAdapter.java`
- [x] Add a private helper that parses JSON string wrappers and returns `output`, `answer`, `content`, `text`, `result`, `data.*`, or `outputs.*`.
- [x] Call the helper before returning every extracted delta string.
@@ -54,7 +54,7 @@
**Commands:**
- `cd web-admin && npm run build`
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `git diff --check`
- [x] Build passes.
@@ -2,7 +2,7 @@
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:**backend-single 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Goal:**server 下全部 39 个 Controller 及其关联 Request DTO 补全 OpenAPI 中文注解(@Tag/@Operation/@Parameter/@Schema),使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
**Architecture:** 在不改变现有代码结构和业务逻辑的前提下,为每个 Controller 类添加 `@Tag` 注解、为每个 public 方法添加 `@Operation` 注解、为 `@RequestParam`/`@PathVariable` 参数添加 `@Parameter` 注解、为被 Controller 直接引用的 Request DTO 字段添加 `@Schema` 注解。分 3 批实施,每批完成后编译验证。
@@ -25,12 +25,12 @@
### Task 1.1: AiChatController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `backend-single/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiChatController.java`
- Modify: `server/src/main/java/com/com/emotion/dto/request/AiChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/AiSummaryRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/GuestChatRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ChatStatsRequest.java`
- [ ] **Step 1: 为 AiChatController 添加 @Tag 和 @Operation 注解**
@@ -162,8 +162,8 @@ public class ChatStatsRequest extends BaseRequest {
### Task 1.2: AiRoutingController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java`
- [ ] **Step 1: 为 AiRoutingController 添加注解**
@@ -215,10 +215,10 @@ public class AiRuntimeRequest {
### Task 1.3: CozeApiCallController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CozeApiCallController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/coze/CozeApiCallUpdateRequest.java`
- [ ] **Step 1: 为 CozeApiCallController 添加 @Tag 注解**
@@ -339,10 +339,10 @@ public class CozeApiCallUpdateRequest {
### Task 1.4: CommunityPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommunityPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/community/CommunityPostPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -387,10 +387,10 @@ CommunityPostPageRequest 各字段加 @Schema(description = "页码")、@Schema(
### Task 1.5: CommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/CommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/comment/CommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -429,10 +429,10 @@ public class CommentController {
### Task 1.6: SocialContentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialContentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentManualImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentLinkImportRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialContentApprovalRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -476,9 +476,9 @@ public class SocialContentController {
### Task 1.7: SocialInsightController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/SocialInsightController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/social/SocialInsightUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -514,8 +514,8 @@ public class SocialInsightController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS,无编译错误。
@@ -523,8 +523,8 @@ Expected: BUILD SUCCESS,无编译错误。
- [ ] **Step 2: 提交 Batch 1 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiChatController.java
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiChatController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
# ... 添加 Batch 1 所有修改的文件
git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
```
@@ -536,10 +536,10 @@ git commit -m "feat: Batch 1 — AI + 社区 Controller 中文注解补全"
### Task 2.1: EmotionAnalysisController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionAnalysisController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionAnalysisUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -578,10 +578,10 @@ public class EmotionAnalysisController {
### Task 2.2: EmotionRecordController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionRecordController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionRecordUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -625,9 +625,9 @@ public class EmotionRecordController {
### Task 2.3: EmotionSummaryController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EmotionSummaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryGenerateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EmotionSummaryStatusRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解**
@@ -646,10 +646,10 @@ public class EmotionRecordController {
### Task 2.4: DiaryPostController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryPostController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryPostUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -706,9 +706,9 @@ public class DiaryPostController {
### Task 2.5: DiaryCommentController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DiaryCommentController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/DiaryCommentPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -759,10 +759,10 @@ public class DiaryCommentController {
### Task 2.6: GrowthTopicController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GrowthTopicController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/growth/GrowthTopicUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -793,10 +793,10 @@ public class GrowthTopicController {
### Task 2.7: TopicInteractionController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TopicInteractionController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/TopicInteractionUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -829,8 +829,8 @@ public class TopicInteractionController {
### Task 2.8: ConversationController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ConversationController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/ConversationPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -872,11 +872,11 @@ public class ConversationController {
### Task 2.9: MessageController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/MessageController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessagePageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageSearchRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/MessageRecentRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -915,10 +915,10 @@ public class MessageController {
### Task 2.10: UserProfileController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfilePageRequest.java`
- [ ] **Step 1: 补充 @Operation 注解(该 Controller 已有 @Tag 但方法级注解缺失)**
@@ -951,12 +951,12 @@ public class MessageController {
### Task 2.11: AchievementController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AchievementController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementProgressUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/achievement/AchievementUnlockRequest.java`
- [ ] **Step 1: 补充 @Parameter 注解(已有 @Tag 和 @Operation,但缺少 @RequestParam 的 @Parameter**
@@ -1042,8 +1042,8 @@ public class MessageController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1051,7 +1051,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 2 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/EmotionAnalysisController.java
git add server/src/main/java/com/emotion/controller/EmotionAnalysisController.java
# ... 添加 Batch 2 所有修改的文件
git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注解补全"
```
@@ -1063,10 +1063,10 @@ git commit -m "feat: Batch 2 — 情绪 + 日记 + 互动 Controller 中文注
### Task 3.1: LifePathController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifePathController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifePathUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1113,10 +1113,10 @@ public class LifePathController {
### Task 3.2: RewardController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/RewardController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/reward/RewardUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1149,11 +1149,11 @@ public class RewardController {
### Task 3.3: UserStatsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/UserStatsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsIncrementRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/UserStatsUpdateValueRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1194,11 +1194,11 @@ public class UserStatsController {
### Task 3.4: EpicScriptController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/EpicScriptInspirationRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1247,10 +1247,10 @@ public class EpicScriptController {
### Task 3.5: DictionaryController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/DictionaryController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/dictionary/DictionaryUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1266,10 +1266,10 @@ public class DictionaryController {
### Task 3.6: GuestUserController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/GuestUserController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/guest/GuestUserUpdateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1285,8 +1285,8 @@ public class GuestUserController {
### Task 3.7: TtsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/TtsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/tts/TtsTaskCreateRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1302,7 +1302,7 @@ public class TtsController {
### Task 3.8: AsrController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AsrController.java`
- Modify: `server/src/main/java/com/emotion/controller/AsrController.java`
- [ ] **Step 1: Controller 注解**
@@ -1318,7 +1318,7 @@ public class AsrController {
### Task 3.9: HealthController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/HealthController.java`
- Modify: `server/src/main/java/com/emotion/controller/HealthController.java`
- [ ] **Step 1: Controller 注解**
@@ -1334,10 +1334,10 @@ public class HealthController {
### Task 3.10: LifeEventController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/LifeEventController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/LifeEventPageRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1353,8 +1353,8 @@ public class LifeEventController {
### Task 3.11: ChatWebSocketController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/ChatWebSocketController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/WebSocketRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1370,8 +1370,8 @@ public class ChatWebSocketController {
### Task 3.12: AnalyticsController 注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsEventBatchRequest.java`
- [ ] **Step 1: Controller 注解**
@@ -1387,13 +1387,13 @@ public class AnalyticsController {
### Task 3.13: AiConfigController 补充注解
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/controller/AiConfigController.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigPageRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigEnableRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigDefaultRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/aiconfig/AiConfigTestUpdateRequest.java`
- [ ] **Step 1: 补充缺失的 @Operation 注解(已有 @Tag 但方法级注解不全)**
@@ -1404,8 +1404,8 @@ public class AnalyticsController {
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
Expected: BUILD SUCCESS
@@ -1413,7 +1413,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 2: 提交 Batch 3 改动**
```bash
git add backend-single/src/main/java/com/emotion/controller/LifePathController.java
git add server/src/main/java/com/emotion/controller/LifePathController.java
# ... 添加 Batch 3 所有修改的文件
git commit -m "feat: Batch 3 — 其他 Controller 中文注解补全"
```
@@ -1559,8 +1559,8 @@ private String fieldName;
- [ ] **Step 1: 全量编译**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
- [ ] **Step 2: 启动服务验证 OpenAPI 同步**
@@ -13,10 +13,10 @@
### Task 1: Backend Analytics Dictionary
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- Create: `server/src/main/java/com/emotion/service/analytics/AnalyticsDictionary.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTopEventItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsTrendItem.java`
- Modify: `server/src/main/java/com/emotion/dto/response/analytics/AnalyticsPreferenceItem.java`
- [x] Add event, event type, page, dimension, value, and API label maps.
- [x] Add DTO fields for Chinese labels while keeping original keys.
@@ -25,8 +25,8 @@
### Task 2: Backend Aggregation Enrichment
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/mapper/AnalyticsEventMapper.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AnalyticsServiceImpl.java`
- [x] Query `page_path` and `api_path` for top events.
- [x] Fill `eventLabel`, `eventTypeLabel`, `pageLabel`, `apiLabel` in top events.
@@ -58,7 +58,7 @@
### Task 5: Verification
**Commands:**
- `cd backend-single && mvn test`
- `cd server && mvn test`
- `cd web-admin && npm run build`
- `cd mini-program && npm run build:mp-weixin` or available project build command
- `git diff --check`
@@ -13,10 +13,10 @@
### Task 1: 数据库实体 + Mapper 层
**Files:**
- Create: `backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `backend-single/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- Create: `server/src/main/java/com/emotion/entity/ApiEndpoint.java`
- Create: `server/src/main/java/com/emotion/entity/ApiParam.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java`
- Create: `server/src/main/java/com/emotion/mapper/ApiParamMapper.java`
- [ ] **Step 1: 创建 ApiEndpoint 实体**
@@ -153,10 +153,10 @@ public interface ApiParamMapper extends BaseMapper<ApiParam> {
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/ApiEndpoint.java
git add backend-single/src/main/java/com/emotion/entity/ApiParam.java
git add backend-single/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add backend-single/src/main/java/com/emotion/mapper/ApiParamMapper.java
git add server/src/main/java/com/emotion/entity/ApiEndpoint.java
git add server/src/main/java/com/emotion/entity/ApiParam.java
git add server/src/main/java/com/emotion/mapper/ApiEndpointMapper.java
git add server/src/main/java/com/emotion/mapper/ApiParamMapper.java
git commit -m "feat: 添加接口端点实体和Mapper"
```
@@ -165,11 +165,11 @@ git commit -m "feat: 添加接口端点实体和Mapper"
### Task 2: DTO 层 — Request/Response
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java`
- Create: `server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java`
- [ ] **Step 1: 创建 ApiEndpointListRequest(分页查询入参)**
@@ -309,11 +309,11 @@ public class ApiTestProxyRequest {
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add backend-single/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git add server/src/main/java/com/emotion/dto/request/ApiEndpointListRequest.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointItemResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiEndpointDetailResponse.java
git add server/src/main/java/com/emotion/dto/response/ApiParamItemResponse.java
git add server/src/main/java/com/emotion/dto/request/ApiTestProxyRequest.java
git commit -m "feat: 添加接口管理DTO"
```
@@ -322,8 +322,8 @@ git commit -m "feat: 添加接口管理DTO"
### Task 3: Service 层 — 核心解析与同步逻辑
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointService.java`
- Create: `server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java`
- [ ] **Step 1: 创建 Service 接口**
@@ -676,8 +676,8 @@ public class ApiEndpointServiceImpl implements ApiEndpointService {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointService.java
git add backend-single/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git add server/src/main/java/com/emotion/service/ApiEndpointService.java
git add server/src/main/java/com/emotion/service/impl/ApiEndpointServiceImpl.java
git commit -m "feat: 添加接口端点Service层"
```
@@ -686,7 +686,7 @@ git commit -m "feat: 添加接口端点Service层"
### Task 4: ApplicationRunner — 启动自动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- Create: `server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java`
- [ ] **Step 1: 创建 SyncRunner**
@@ -729,7 +729,7 @@ public class ApiEndpointSyncRunner implements ApplicationRunner {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git add server/src/main/java/com/emotion/service/ApiEndpointSyncRunner.java
git commit -m "feat: 添加启动自动同步接口数据"
```
@@ -738,7 +738,7 @@ git commit -m "feat: 添加启动自动同步接口数据"
### Task 5: Controller 层 — 分页查询 + 详情 + 手动同步
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java`
- Create: `server/src/main/java/com/emotion/controller/ApiEndpointController.java`
- [ ] **Step 1: 创建 Controller**
@@ -815,7 +815,7 @@ public class ApiEndpointController {
- [ ] **Step 2: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiEndpointController.java
git add server/src/main/java/com/emotion/controller/ApiEndpointController.java
git commit -m "feat: 添加接口管理Controller"
```
@@ -824,8 +824,8 @@ git commit -m "feat: 添加接口管理Controller"
### Task 6: Controller 层 — 代理测试接口
**Files:**
- Create: `backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- Create: `server/src/main/java/com/emotion/controller/ApiTestProxyController.java`
- Create: `server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java`
- [ ] **Step 1: 创建 ApiTestProxyResponse**
@@ -990,8 +990,8 @@ public class ApiTestProxyController {
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add backend-single/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git add server/src/main/java/com/emotion/controller/ApiTestProxyController.java
git add server/src/main/java/com/emotion/dto/response/ApiTestProxyResponse.java
git commit -m "feat: 添加接口代理测试Controller"
```
@@ -1002,8 +1002,8 @@ git commit -m "feat: 添加接口代理测试Controller"
- [ ] **Step 1: 编译验证**
```bash
cd backend-single
mvn clean install -pl :backend-single -am -DskipTests
cd server
mvn clean install -pl :server -am -DskipTests
```
预期:BUILD SUCCESS
@@ -15,28 +15,28 @@
### 后端改动文件(按批次)
**Batch 1 (P0):**
- `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `backend-single/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/controller/AdminAuthController.java` — 已有 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AuthController.java` — 补全缺少的 @Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/AdminController.java` — 已有 @Tag/@Operation,补全 @Parameter
- `server/src/main/java/com/emotion/controller/TokenController.java` — 已有 @Operation,检查完整度
- `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/controller/UserProfileController.java` — 新增 @Tag/@Operation
- `server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/RegisterRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/LoginRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserCreateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserPageRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java` — 补全 @Schema
- `server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java` — 补全 @Schema
### 前端改动文件
@@ -48,8 +48,8 @@
## Task 1: 后端 Batch 1 - AdminAuth + Auth Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAuthController.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- [ ] **Step 1: 查看 AdminAuthController 已有注解**
@@ -85,7 +85,7 @@ AuthController 已有 @Tag。需要补全以下方法的 @Operation:
- [ ] **Step 3: 为 AuthController 缺少的方法补全注解**
修改 `backend-single/src/main/java/com/emotion/controller/AuthController.java`
修改 `server/src/main/java/com/emotion/controller/AuthController.java`
为以下方法添加 @Operation(在方法上方的空白行插入):
@@ -133,7 +133,7 @@ AdminAuthController 的 6 个方法全部有 @Operation,无需修改。
- [ ] **Step 5: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git commit -m "feat: 补全 AuthController 缺失的 @Operation 和 @Parameter 注解"
```
@@ -142,16 +142,16 @@ git commit -m "feat: 补全 AuthController 缺失的 @Operation 和 @Parameter
## Task 2: 后端 Batch 1 - Admin + Token Controller 注解补全
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserController.java`
- Modify: `backend-single/src/main/java/com/emotion/controller/UserProfileController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminController.java`
- Modify: `server/src/main/java/com/emotion/controller/TokenController.java`
- Modify: `server/src/main/java/com/emotion/controller/AdminAnalyticsController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserController.java`
- Modify: `server/src/main/java/com/emotion/controller/UserProfileController.java`
- [ ] **Step 1: 读取 AdminController**
```bash
cat backend-single/src/main/java/com/emotion/controller/AdminController.java
cat server/src/main/java/com/emotion/controller/AdminController.java
```
检查每个方法的 @Operation 是否有 @Parameter,为路径参数和查询参数补全 @Parameter 注解。
@@ -159,7 +159,7 @@ cat backend-single/src/main/java/com/emotion/controller/AdminController.java
- [ ] **Step 2: 读取 TokenController**
```bash
cat backend-single/src/main/java/com/emotion/controller/TokenController.java
cat server/src/main/java/com/emotion/controller/TokenController.java
```
已有 3 个 @Operation,检查是否缺少 @Parameter,补全。
@@ -194,11 +194,11 @@ public Result<AnalyticsOverviewResponse> overview(@Validated AnalyticsQueryReque
- [ ] **Step 6: 提交**
```bash
git add backend-single/src/main/java/com/emotion/controller/AdminController.java
git add backend-single/src/main/java/com/emotion/controller/TokenController.java
git add backend-single/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add backend-single/src/main/java/com/emotion/controller/UserController.java
git add backend-single/src/main/java/com/emotion/controller/UserProfileController.java
git add server/src/main/java/com/emotion/controller/AdminController.java
git add server/src/main/java/com/emotion/controller/TokenController.java
git add server/src/main/java/com/emotion/controller/AdminAnalyticsController.java
git add server/src/main/java/com/emotion/controller/UserController.java
git add server/src/main/java/com/emotion/controller/UserProfileController.java
git commit -m "feat: 为 Admin/Token/AdminAnalytics/User/UserProfile Controller 补全 OpenAPI 注解"
```
@@ -257,21 +257,21 @@ import io.swagger.v3.oas.annotations.media.Schema;
- [ ] **Step 3: 提交**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/LoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/LoginRequest.java
git add server/src/main/java/com/emotion/dto/request/RegisterRequest.java
git add server/src/main/java/com/emotion/dto/request/ResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/RefreshTokenRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminPageRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminChangePasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/AdminResetPasswordRequest.java
git add server/src/main/java/com/emotion/dto/request/UserCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/UserPageRequest.java
git add server/src/main/java/com/emotion/dto/request/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/analytics/AnalyticsQueryRequest.java
git commit -m "feat: 为 Batch 1 请求 DTO 补全 @Schema 中文描述注解"
```
@@ -665,7 +665,7 @@ git commit -m "feat: 测试面板增加请求头展示区域"
- [ ] **Step 1: 本地构建后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -19,10 +19,10 @@
| `web-admin/src/components/JsonViewer.vue` | 创建 | 通用 JSON 格式化高亮 + 复制组件 |
| `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 创建 | 调用日志详情弹窗 |
| `web-admin/src/views/aiconfig/AiRoutingList.vue` | 修改 | 调用日志 Tab 增加筛选栏、展开行、分页 |
| `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `backend-single/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
| `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java` | 创建 | 日志查询请求 DTO |
| `server/src/main/java/com/emotion/service/AiCallLogService.java` | 修改 | 新增 `query` 方法签名 |
| `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java` | 修改 | 实现 `query` 分页查询 |
| `server/src/main/java/com/emotion/controller/AiRoutingController.java` | 修改 | 新增 POST /call-logs 接口 |
---
@@ -428,18 +428,18 @@ git commit -m "feat: 新增 AI 调用日志详情弹窗组件"
## Task 5: 后端 AiCallLogQueryRequest DTO
**Files:**
- Create: `backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
- [ ] **Step 1: 确认 dto/request/ai 目录存在**
```bash
ls backend-single/src/main/java/com/emotion/dto/request/ai/
ls server/src/main/java/com/emotion/dto/request/ai/
```
如果不存在则创建:
```bash
mkdir -p backend-single/src/main/java/com/emotion/dto/request/ai
mkdir -p server/src/main/java/com/emotion/dto/request/ai
```
- [ ] **Step 2: 创建 AiCallLogQueryRequest.java**
@@ -498,7 +498,7 @@ public class AiCallLogQueryRequest {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git add server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java
git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
```
@@ -507,8 +507,8 @@ git commit -m "feat: 新增 AI 调用日志查询请求 DTO"
## Task 6: 后端 Service 接口和实现
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AiCallLogService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java`
- [ ] **Step 1: 在 AiCallLogService 接口中新增 query 方法**
@@ -562,8 +562,8 @@ public PageResult<AiCallLog> query(AiCallLogQueryRequest request) {
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/AiCallLogService.java
git add backend-single/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git add server/src/main/java/com/emotion/service/AiCallLogService.java
git add server/src/main/java/com/emotion/service/impl/AiCallLogServiceImpl.java
git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
```
@@ -572,7 +572,7 @@ git commit -m "feat: AI 调用日志 Service 增加分页查询方法"
## Task 7: 后端 Controller 新增 POST 接口
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java`
- Modify: `server/src/main/java/com/emotion/controller/AiRoutingController.java`
- [ ] **Step 1: 在 AiRoutingController 中导入新类型**
@@ -599,7 +599,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
- [ ] **Step 3: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AiRoutingController.java
git add server/src/main/java/com/emotion/controller/AiRoutingController.java
git commit -m "feat: Controller 新增 AI 调用日志分页查询接口"
```
@@ -949,7 +949,7 @@ git commit -m "feat: 调用日志 Tab 增加筛选栏、展开行、分页和详
- [ ] **Step 1: 编译后端**
```bash
cd backend-single
cd server
mvn clean install -DskipTests
```
@@ -970,7 +970,7 @@ Expected: BUILD SUCCESS
- [ ] **Step 1: 启动后端**
```bash
cd backend-single
cd server
mvn spring-boot:run
```
@@ -16,10 +16,10 @@
| 操作 | 文件 | 说明 |
|------|------|------|
| Modify | `backend-single/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/entity/AiCallLog.java` | 实体新增 `userName` 字段 |
| Create | `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql` | 数据库迁移脚本 |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:68-73` | invokeStream 写入 userName |
| Modify | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java:219-225` | invokeEndpointStream 写入 userName |
| Modify | `web-admin/src/types/aiconfig.ts` | AiCallLog 接口新增 userName |
| Modify | `web-admin/src/views/aiconfig/AiRoutingList.vue` | 列表新增"调用用户"列 + userDisplay 函数 |
| Modify | `web-admin/src/views/aiconfig/components/AiCallLogDetailDialog.vue` | 详情弹窗"用户 ID"改为"调用用户" |
@@ -29,9 +29,9 @@
### Task 1: 后端实体 + 数据库迁移
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `backend-single/` 目录下执行编译验证
- Modify: `server/src/main/java/com/emotion/entity/AiCallLog.java:29-31`
- Create: `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`
- Test: `server/` 目录下执行编译验证
- [ ] **Step 1: 在 AiCallLog 实体中新增 userName 字段**
@@ -46,7 +46,7 @@ private String userName;
- [ ] **Step 2: 创建数据库迁移脚本**
创建 `backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
创建 `server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql`:
```sql
-- 调用日志表新增 user_name 字段
@@ -56,7 +56,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) DEFAULT NULL COMMENT
- [ ] **Step 3: 编译验证后端代码**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
@@ -65,8 +65,8 @@ mvn clean compile -DskipTests
- [ ] **Step 4: 提交**
```bash
git add backend-single/src/main/java/com/emotion/entity/AiCallLog.java
git add backend-single/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git add server/src/main/java/com/emotion/entity/AiCallLog.java
git add server/src/main/resources/db/migration/V20260525__add_user_name_to_ai_call_log.sql
git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移脚本"
```
@@ -75,8 +75,8 @@ git commit -m "feat: 调用日志实体新增 userName 字段及数据库迁移
### Task 2: 后端日志保存逻辑写入 userName
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`
- [ ] **Step 1: 在 invokeStream 方法中写入 userName**
@@ -123,7 +123,7 @@ callLog.setUserName(request.getUserName());
- [ ] **Step 3: 更新单元测试,验证 userName 被正确写入**
修改 `backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
修改 `server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java`,在 `invokeStreamRecoversWhenOutputExists` 测试中:
在已有的 `request.setRequestId("client-request-1");` 之后添加 `request.setUserName("测试用户");`,然后在断言部分新增 userName 的验证:
@@ -138,7 +138,7 @@ assertEquals("测试用户", savedLog.getUserName());
- [ ] **Step 4: 运行测试验证**
```bash
cd backend-single
cd server
mvn test -Dtest=AiRuntimeServiceImplTest
```
@@ -147,13 +147,13 @@ mvn test -Dtest=AiRuntimeServiceImplTest
- [ ] **Step 5: 编译验证 + 提交**
```bash
cd backend-single
cd server
mvn clean compile -DskipTests
```
```bash
git add backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java
git add server/src/test/java/com/emotion/service/AiRuntimeServiceImplTest.java
git commit -m "feat: 日志保存时写入 userName 字段"
```
@@ -16,28 +16,28 @@
| Action | File | Responsibility |
| --- | --- | --- |
| Create | `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `backend-single/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `backend-single/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `backend-single/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `backend-single/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Create | `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java` | Binds WeChat mini program appid, secret, and API base URL from application config. |
| Create | `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql` | Prevents duplicate non-null WeChat third-party identities. |
| Modify | `server/src/main/resources/application.yml` | Adds default WeChat config keys and public login endpoint allowlist. |
| Modify | `server/src/main/java/com/emotion/config/WebMvcConfig.java` | Excludes `/auth/wechat/login` from JWT interception. |
| Modify | `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java` | Excludes `/api/auth/wechat/login` from the interceptor's internal public endpoint list. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java` | Request body for WeChat openid login. |
| Create | `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java` | Request body for WeChat phone binding. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | Internal DTO for WeChat `code2Session` response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java` | Internal DTO for WeChat access token response. |
| Create | `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java` | Internal DTO for WeChat phone number response. |
| Create | `server/src/main/java/com/emotion/service/WechatMiniProgramService.java` | Interface for WeChat API calls. |
| Create | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | RestTemplate-backed WeChat client with Redis token caching. |
| Create | `server/src/main/java/com/emotion/service/UserIdentityMergeService.java` | Defines temporary WeChat user to phone user data transfer. |
| Create | `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java` | Transfers user-owned rows and avoids singleton row conflicts. |
| Modify | `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java` | Adds non-sensitive `wechatBound` flag for frontend display logic. |
| Modify | `server/src/main/java/com/emotion/service/UserService.java` | Adds third-party identity lookup helper. |
| Modify | `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java` | Implements third-party identity lookup. |
| Modify | `server/src/main/java/com/emotion/service/AuthService.java` | Adds WeChat login and phone binding operations. |
| Modify | `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java` | Implements openid login, phone identity merge/bind, and token issuing reuse. |
| Modify | `server/src/main/java/com/emotion/controller/AuthController.java` | Adds `/auth/wechat/login` and `/auth/wechat/phone`. |
| Test | `server/src/test/java/com/emotion/controller/AuthControllerTest.java` | Verifies controller contract. |
| Test | `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java` | Verifies identity matching and binding behavior. |
| Modify | `mini-program/src/services/auth.js` | Adds `wechatLogin` and `bindWechatPhone`. |
| Modify | `mini-program/src/stores/app.js` | Adds store methods and tracks returned auth user info. |
| Modify | `mini-program/src/pages/login/index.vue` | Adds WeChat primary login while keeping phone/SMS form. |
@@ -48,20 +48,20 @@
### Task 1: Backend WeChat Configuration And DTOs
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `backend-single/src/main/resources/application.yml`
- Create: `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`
- Create: `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`
- Create: `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`
- Create: `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`
- Create: `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Modify: `server/src/main/resources/application.yml`
- Test: backend compile
- [ ] **Step 1: Create WeChat configuration properties**
Create `backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
Create `server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java`:
```java
package com.emotion.config;
@@ -82,7 +82,7 @@ public class WechatMiniProgramProperties {
- [ ] **Step 2: Create public request DTOs**
Create `backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java`:
```java
package com.emotion.dto.request;
@@ -102,7 +102,7 @@ public class WechatLoginRequest {
}
```
Create `backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
Create `server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java`:
```java
package com.emotion.dto.request;
@@ -120,7 +120,7 @@ public class WechatPhoneBindRequest {
- [ ] **Step 3: Create internal WeChat API response DTOs**
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -147,7 +147,7 @@ public class WechatCodeSessionResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -173,7 +173,7 @@ public class WechatAccessTokenResponse {
}
```
Create `backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
Create `server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java`:
```java
package com.emotion.dto.wechat;
@@ -213,7 +213,7 @@ public class WechatPhoneNumberResponse {
- [ ] **Step 4: Add WeChat config keys**
In `backend-single/src/main/resources/application.yml`, under `emotion:`, add:
In `server/src/main/resources/application.yml`, under `emotion:`, add:
```yaml
wechat:
@@ -239,7 +239,7 @@ HAVING COUNT(*) > 1;
Expected: no rows. If rows exist, resolve duplicates before adding the index.
Create `backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
Create `server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql`:
```sql
ALTER TABLE t_user
@@ -250,7 +250,7 @@ MySQL permits multiple `NULL` values in a unique index, so users without WeChat
- [ ] **Step 6: Add non-sensitive WeChat binding flag**
In `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
In `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`, add:
```java
/**
@@ -264,7 +264,7 @@ private Boolean wechatBound;
Run:
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -273,15 +273,15 @@ Expected: command exits with status `0`.
- [ ] **Step 8: Commit**
```bash
git add backend-single/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add backend-single/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add backend-single/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add backend-single/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add backend-single/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/main/resources/application.yml
git add server/src/main/java/com/emotion/config/WechatMiniProgramProperties.java
git add server/src/main/resources/db/migration/V20260531__add_unique_wechat_identity.sql
git add server/src/main/java/com/emotion/dto/request/WechatLoginRequest.java
git add server/src/main/java/com/emotion/dto/request/WechatPhoneBindRequest.java
git add server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatAccessTokenResponse.java
git add server/src/main/java/com/emotion/dto/wechat/WechatPhoneNumberResponse.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/main/resources/application.yml
git commit -m "feat: add wechat mini program auth DTOs and config"
```
@@ -290,13 +290,13 @@ git commit -m "feat: add wechat mini program auth DTOs and config"
### Task 2: Backend WeChat API Client
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`
- Create: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 1: Create service interface**
Create `backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
Create `server/src/main/java/com/emotion/service/WechatMiniProgramService.java`:
```java
package com.emotion.service;
@@ -313,7 +313,7 @@ public interface WechatMiniProgramService {
- [ ] **Step 2: Create RestTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -434,7 +434,7 @@ public class WechatMiniProgramServiceImpl implements WechatMiniProgramService {
- [ ] **Step 3: Create focused unit tests for error behavior**
Create `backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java`:
```java
package com.emotion.service;
@@ -470,7 +470,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 4: Run test**
```bash
cd backend-single
cd server
mvn -q -Dtest=WechatMiniProgramServiceImplTest test
```
@@ -479,9 +479,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/WechatMiniProgramService.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/WechatMiniProgramServiceImplTest.java
git commit -m "feat: add wechat mini program API client"
```
@@ -490,13 +490,13 @@ git commit -m "feat: add wechat mini program API client"
### Task 3: Backend User Identity Merge Service
**Files:**
- Create: `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- Create: `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`
- Create: `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`
- Test: `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`
- [ ] **Step 1: Create merge service interface**
Create `backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
Create `server/src/main/java/com/emotion/service/UserIdentityMergeService.java`:
```java
package com.emotion.service;
@@ -508,7 +508,7 @@ public interface UserIdentityMergeService {
- [ ] **Step 2: Create JdbcTemplate implementation**
Create `backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
Create `server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java`:
```java
package com.emotion.service.impl;
@@ -592,7 +592,7 @@ public class UserIdentityMergeServiceImpl implements UserIdentityMergeService {
- [ ] **Step 3: Add merge service test**
Create `backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
Create `server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java`:
```java
package com.emotion.service;
@@ -626,7 +626,7 @@ class UserIdentityMergeServiceImplTest {
- [ ] **Step 4: Run merge service test**
```bash
cd backend-single
cd server
mvn -q -Dtest=UserIdentityMergeServiceImplTest test
```
@@ -635,9 +635,9 @@ Expected: command exits with status `0`.
- [ ] **Step 5: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git add server/src/main/java/com/emotion/service/UserIdentityMergeService.java
git add server/src/main/java/com/emotion/service/impl/UserIdentityMergeServiceImpl.java
git add server/src/test/java/com/emotion/service/UserIdentityMergeServiceImplTest.java
git commit -m "feat: add wechat user identity merge service"
```
@@ -646,22 +646,22 @@ git commit -m "feat: add wechat user identity merge service"
### Task 4: Backend User Lookup And Auth Service Methods
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/UserService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/service/AuthService.java`
- Modify: `backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- Modify: `server/src/main/java/com/emotion/service/UserService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/service/AuthService.java`
- Modify: `server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java`
- Modify: `server/src/main/java/com/emotion/dto/response/UserInfoResponse.java`
- Test: `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`
- [ ] **Step 1: Add third-party lookup to UserService**
In `backend-single/src/main/java/com/emotion/service/UserService.java`, add:
In `server/src/main/java/com/emotion/service/UserService.java`, add:
```java
User getByThirdParty(String thirdPartyType, String thirdPartyId);
```
In `backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
In `server/src/main/java/com/emotion/service/impl/UserServiceImpl.java`, add:
```java
@Override
@@ -679,7 +679,7 @@ public User getByThirdParty(String thirdPartyType, String thirdPartyId) {
- [ ] **Step 2: Add AuthService methods**
In `backend-single/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
In `server/src/main/java/com/emotion/service/AuthService.java`, import the two request DTOs and add:
```java
AuthResponse wechatLogin(WechatLoginRequest request);
@@ -858,7 +858,7 @@ public AuthResponse bindWechatPhone(String currentUserId, WechatPhoneBindRequest
- [ ] **Step 8: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -866,7 +866,7 @@ Expected: command exits with status `0`.
- [ ] **Step 9: Add service tests for identity rules**
Create `backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
Create `server/src/test/java/com/emotion/service/AuthServiceWechatTest.java`:
```java
package com.emotion.service;
@@ -1002,7 +1002,7 @@ class AuthServiceWechatTest {
- [ ] **Step 10: Run auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,UserIdentityMergeServiceImplTest test
```
@@ -1011,12 +1011,12 @@ Expected: command exits with status `0`.
- [ ] **Step 11: Commit**
```bash
git add backend-single/src/main/java/com/emotion/service/UserService.java
git add backend-single/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add backend-single/src/main/java/com/emotion/service/AuthService.java
git add backend-single/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add backend-single/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add backend-single/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git add server/src/main/java/com/emotion/service/UserService.java
git add server/src/main/java/com/emotion/service/impl/UserServiceImpl.java
git add server/src/main/java/com/emotion/service/AuthService.java
git add server/src/main/java/com/emotion/service/impl/AuthServiceImpl.java
git add server/src/main/java/com/emotion/dto/response/UserInfoResponse.java
git add server/src/test/java/com/emotion/service/AuthServiceWechatTest.java
git commit -m "feat: add wechat auth identity binding logic"
```
@@ -1025,10 +1025,10 @@ git commit -m "feat: add wechat auth identity binding logic"
### Task 5: Backend Auth Controller And Security Allowlist
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `backend-single/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java`
- Modify: `server/src/main/java/com/emotion/controller/AuthController.java`
- Modify: `server/src/main/java/com/emotion/config/WebMvcConfig.java`
- Modify: `server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java`
- Test: `server/src/test/java/com/emotion/controller/AuthControllerTest.java`
- [ ] **Step 1: Add controller endpoints**
@@ -1126,7 +1126,7 @@ public void testWechatPhoneBind() throws Exception {
- [ ] **Step 5: Run controller tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest test
```
@@ -1135,10 +1135,10 @@ Expected: command exits with status `0`.
- [ ] **Step 6: Commit**
```bash
git add backend-single/src/main/java/com/emotion/controller/AuthController.java
git add backend-single/src/main/java/com/emotion/config/WebMvcConfig.java
git add backend-single/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add backend-single/src/test/java/com/emotion/controller/AuthControllerTest.java
git add server/src/main/java/com/emotion/controller/AuthController.java
git add server/src/main/java/com/emotion/config/WebMvcConfig.java
git add server/src/main/java/com/emotion/interceptor/JwtAuthInterceptor.java
git add server/src/test/java/com/emotion/controller/AuthControllerTest.java
git commit -m "feat: expose wechat auth endpoints"
```
@@ -1596,7 +1596,7 @@ git commit -m "feat: add wechat phone binding entry"
- [ ] **Step 1: Run backend auth tests**
```bash
cd backend-single
cd server
mvn -q -Dtest=AuthControllerTest,AuthServiceWechatTest,WechatMiniProgramServiceImplTest,UserIdentityMergeServiceImplTest test
```
@@ -1605,7 +1605,7 @@ Expected: command exits with status `0`.
- [ ] **Step 2: Compile backend**
```bash
cd backend-single
cd server
mvn -q -DskipTests compile
```
@@ -1633,7 +1633,7 @@ Open `mini-program/unpackage/dist/build/mp-weixin` in WeChat DevTools and verify
- [ ] **Step 5: Add README note if config is not documented elsewhere**
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `backend-single/部署说明.md`:
If no deployment doc already lists WeChat env vars, add this section to `mini-program/README.md` or `server/部署说明.md`:
```markdown
### WeChat Mini Program Login
@@ -15,10 +15,10 @@
| 文件 | 改动 | 职责 |
|------|------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 数据库迁移:新增5列 |
| `backend-single/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `backend-single/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `backend-single/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `backend-single/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `server/.../entity/UserProfile.java` | 修改 | ORM 实体新增字段 |
| `server/.../request/UserProfileCreateRequest.java` | 修改 | 创建请求 DTO 新增字段 |
| `server/.../request/UserProfileUpdateRequest.java` | 修改 | 更新请求 DTO 新增字段 |
| `server/.../response/UserProfileResponse.java` | 修改 | 响应 DTO 新增字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 前后端数据格式双向转换 |
| `mini-program/src/stores/app.js` | 修改 | 全局状态 registrationData 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -57,7 +57,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
### Task 2: 后端实体扩展 — UserProfile.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/entity/UserProfile.java`
- Modify: `server/src/main/java/com/emotion/entity/UserProfile.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -100,7 +100,7 @@ git commit -m "db: add city, industry, company, personality_tags, birthday to t_
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/entity/UserProfile.java
git add server/src/main/java/com/emotion/entity/UserProfile.java
git commit -m "feat: extend UserProfile entity with city/industry/company/personalityTags/birthday"
```
@@ -109,7 +109,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
### Task 3: 后端创建请求 DTO 扩展 — UserProfileCreateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -145,7 +145,7 @@ git commit -m "feat: extend UserProfile entity with city/industry/company/person
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java
git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/personalityTags/birthday"
```
@@ -154,7 +154,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
### Task 4: 后端更新请求 DTO 扩展 — UserProfileUpdateRequest.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- Modify: `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -190,7 +190,7 @@ git commit -m "feat: extend UserProfileCreateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git add server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java
git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/personalityTags/birthday"
```
@@ -199,7 +199,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
### Task 5: 后端响应 DTO 扩展 — UserProfileResponse.java
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- Modify: `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- [ ] **Step 1: 在 idealLife 字段后新增5个字段**
@@ -236,7 +236,7 @@ git commit -m "feat: extend UserProfileUpdateRequest with city/industry/company/
- [ ] **Step 2: Commit**
```bash
git add backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git add server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java
git commit -m "feat: extend UserProfileResponse with city/industry/company/personalityTags/birthday"
```
@@ -631,7 +631,7 @@ git commit -m "fix: remove hardcoded defaults from profile edit page"
- [ ] **Step 1: 后端编译验证**
```bash
cd backend-single && mvn compile -q
cd server && mvn compile -q
```
预期:编译成功,无错误。
@@ -20,13 +20,13 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
| 状态 | 文件 | 责任 | 行数 |
|---|---|---|---|
| 重写 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `backend-single/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
| 重写 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | 统一 RestTemplate BeanUser-Agent + 超时 + 拦截器 + 错误处理) | 13 → 40 |
| 新增 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | 请求/响应日志(INFO+ 脱敏 bodyDEBUG | 60 |
| 新增 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | 4xx/5xx 不抛异常 | 30 |
| 新增 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | 业务异常类(HTTP 风格 code + rawBody | 30 |
| 重写 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | `code2Session` 五步防御 | 49 → 90 |
| 修改 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | 新增 `handleWechatApiException` + 脱敏日志 | +25 |
| 修改 | `server/src/main/resources/application.yml` | 移除 `app-id` / `app-secret` 硬编码默认值 | 2 行 |
**总计**:2 重写 + 3 新增 + 2 修改,约 280 行 Java + 2 行 YAML
@@ -38,9 +38,9 @@ purpose: 微信小程序登录修复 spec 的实施计划(TDD + 频繁 commit
cd "G:\IdeaProjects\emotion-museun"
git status # 确认在 master 分支
git log --oneline -1 # 确认最新 commit 是 82c308b
ls backend-single/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls backend-single/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
ls server/src/main/java/com/emotion/exception/ # 确认 GlobalExceptionHandler.java 存在
ls server/src/main/java/com/emotion/config/ # 确认 RestTemplateConfig.java 存在
ls server/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniProgramServiceImpl.java 存在
```
期望:全部存在,工作树只有 spec 文件修改(无业务代码污染)
@@ -50,7 +50,7 @@ ls backend-single/src/main/java/com/emotion/service/impl/ # 确认 WechatMiniPr
## Task 1: 创建 WechatApiException 异常类
**Files:**
- Create: `backend-single/src/main/java/com/emotion/exception/WechatApiException.java`
- Create: `server/src/main/java/com/emotion/exception/WechatApiException.java`
- [ ] **Step 1.1: 写入 WechatApiException.java**
@@ -95,7 +95,7 @@ public class WechatApiException extends RuntimeException {
- [ ] **Step 1.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -105,7 +105,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/WechatApiException.java
git add server/src/main/java/com/emotion/exception/WechatApiException.java
git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
- 继承 RuntimeException
@@ -119,12 +119,12 @@ git commit -m "feat(wechat): 新增 WechatApiException 业务异常类
## Task 2: 创建 WechatApiLoggingInterceptor(含脱敏单元测试)
**Files:**
- Create: `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- Create: `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
- Create: `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
- [ ] **Step 2.1: 写失败测试(验证脱敏逻辑)**
创建 `backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
创建 `server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java`
```java
package com.emotion.interceptor;
@@ -197,7 +197,7 @@ class WechatApiLoggingInterceptorTest {
- [ ] **Step 2.2: 运行测试验证失败**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -205,7 +205,7 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
- [ ] **Step 2.3: 写最小实现让测试通过**
创建 `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
创建 `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java`
```java
package com.emotion.interceptor;
@@ -282,7 +282,7 @@ public class WechatApiLoggingInterceptor implements ClientHttpRequestInterceptor
- [ ] **Step 2.4: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatApiLoggingInterceptorTest
```
@@ -292,8 +292,8 @@ mvn test -Dtest=WechatApiLoggingInterceptorTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add backend-single/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git add server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java
git add server/src/test/java/com/emotion/interceptor/WechatApiLoggingInterceptorTest.java
git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含脱敏单元测试)
- ClientHttpRequestInterceptor 实现
@@ -309,7 +309,7 @@ git commit -m "feat(wechat): 新增 WechatApiLoggingInterceptor 拦截器(含
## Task 3: 创建 WechatResponseErrorHandler
**Files:**
- Create: `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- Create: `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java`
- [ ] **Step 3.1: 写入 WechatResponseErrorHandler.java**
@@ -354,7 +354,7 @@ public class WechatResponseErrorHandler extends DefaultResponseErrorHandler {
- [ ] **Step 3.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -364,7 +364,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git add server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java
git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
- 继承 DefaultResponseErrorHandler
@@ -378,12 +378,12 @@ git commit -m "feat(wechat): 新增 WechatResponseErrorHandler
## Task 4: 重写 RestTemplateConfig 集成新组件
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java`
- Modify: `server/src/main/java/com/emotion/config/RestTemplateConfig.java`
- [ ] **Step 4.1: 备份并读取原文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
copy RestTemplateConfig.java RestTemplateConfig.java.bak
```
@@ -467,8 +467,8 @@ public class RestTemplateConfig {
- [ ] **Step 4.3: mvn 编译验证(确保 7 个其他使用点不受影响)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
mvn clean install -pl :backend-single -am -DskipTests
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`,无编译错误(如果 7 个其他使用点有编译错误,说明 RestTemplate API 不兼容,需要回滚)
@@ -476,7 +476,7 @@ mvn clean install -pl :backend-single -am -DskipTests
- [ ] **Step 4.4: 删除备份文件**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single\src\main\java\com\emotion\config"
cd "G:\IdeaProjects\emotion-museun\server\src\main\java\com\emotion\config"
del RestTemplateConfig.java.bak
```
@@ -484,7 +484,7 @@ del RestTemplateConfig.java.bak
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java
git add server/src/main/java/com/emotion/config/RestTemplateConfig.java
git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
解决问题:
@@ -514,8 +514,8 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
## Task 5: 重写 WechatMiniProgramServiceImpl.code2SessionTDD 10 个场景)
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- Modify: `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
- Create: `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
- [ ] **Step 5.1: 读取当前 WechatMiniProgramServiceImpl.java**
@@ -523,7 +523,7 @@ git commit -m "feat(wechat): 重写 RestTemplateConfig 统一 RestTemplate Bean
- [ ] **Step 5.2: 写失败测试(覆盖 spec 10 个场景)**
创建 `backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
创建 `server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java`
```java
package com.emotion.service.impl;
@@ -659,7 +659,7 @@ class WechatMiniProgramServiceImplTest {
- [ ] **Step 5.3: 运行测试验证失败(确认 service 现有方法未做防御)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -667,7 +667,7 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
- [ ] **Step 5.4: 重写 code2Session 方法(五步防御)**
修改 `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
修改 `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java`
在 class 顶部添加 imports
```java
@@ -748,7 +748,7 @@ public WechatCodeSessionResponse code2Session(String code) {
- [ ] **Step 5.5: 运行测试验证通过**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=WechatMiniProgramServiceImplTest
```
@@ -758,8 +758,8 @@ mvn test -Dtest=WechatMiniProgramServiceImplTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add backend-single/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git add server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java
git add server/src/test/java/com/emotion/service/impl/WechatMiniProgramServiceImplTest.java
git commit -m "feat(wechat): 重写 code2Session 五步防御
解决问题:
@@ -783,7 +783,7 @@ git commit -m "feat(wechat): 重写 code2Session 五步防御
## Task 6: 修改 application.yml 移除硬编码
**Files:**
- Modify: `backend-single/src/main/resources/application.yml`
- Modify: `server/src/main/resources/application.yml`
- [ ] **Step 6.1: 定位并修改微信配置**
@@ -820,7 +820,7 @@ $env:WECHAT_MINI_PROGRAM_APP_SECRET = "4f087bd363a4147ef543d634f9f6950d"
- [ ] **Step 6.3: mvn 编译验证(确保 YAML 可加载)**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -830,7 +830,7 @@ mvn clean compile -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/resources/application.yml
git add server/src/main/resources/application.yml
git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
安全问题:
@@ -854,7 +854,7 @@ git commit -m "fix(wechat): 移除 app-id/app-secret 硬编码默认值
## Task 7: 修改 GlobalExceptionHandler 添加 WechatApiException handler
**Files:**
- Modify: `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- Modify: `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java`
- [ ] **Step 7.1: 定位文件并添加新方法**
@@ -911,7 +911,7 @@ public Result<Void> handleWechatApiException(WechatApiException e, HttpServletRe
- [ ] **Step 7.2: mvn 编译验证**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn clean compile -DskipTests
```
@@ -919,7 +919,7 @@ mvn clean compile -DskipTests
- [ ] **Step 7.3: 写简单测试验证 handler 行为**
创建 `backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
创建 `server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java`
```java
package com.emotion.exception;
@@ -976,7 +976,7 @@ class GlobalExceptionHandlerWechatTest {
- [ ] **Step 7.4: 运行测试**
```bash
cd "G:\IdeaProjects\emotion-museun\backend-single"
cd "G:\IdeaProjects\emotion-museun\server"
mvn test -Dtest=GlobalExceptionHandlerWechatTest
```
@@ -986,8 +986,8 @@ mvn test -Dtest=GlobalExceptionHandlerWechatTest
```bash
cd "G:\IdeaProjects\emotion-museun"
git add backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add backend-single/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git add server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java
git add server/src/test/java/com/emotion/exception/GlobalExceptionHandlerWechatTest.java
git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException handler
解决问题:
@@ -1014,7 +1014,7 @@ git commit -m "feat(wechat): GlobalExceptionHandler 新增 WechatApiException ha
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn clean install -pl :backend-single -am -DskipTests
mvn clean install -pl :server -am -DskipTests
```
期望:`BUILD SUCCESS`
@@ -1023,7 +1023,7 @@ mvn clean install -pl :backend-single -am -DskipTests
```bash
cd "G:\IdeaProjects\emotion-museun"
mvn test -pl :backend-single
mvn test -pl :server
```
期望:所有测试通过,无失败
@@ -0,0 +1,445 @@
# 个人信息组件合并 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将人生轨迹页面(RecordView)的个人信息展示区域完整合并到我的页面(MineView),取并集展示所有字段,RecordView 移除个人信息区域。
**Architecture:** MineView 保留现有宇宙星空风格和居中头像布局,在 stats-grid 下方新增「个人档案」信息区块,使用三栏网格展示所有个人信息字段。RecordView 完全移除 profile-card 区域及相关代码。
**Tech Stack:** Vue 3 Composition API, Taro/UniApp 小程序, SCSS scoped styles
**设计文档:** `docs/superpowers/specs/2026-06-15-profile-merge-design.md`
---
## 文件结构
| 操作 | 文件路径 | 职责 |
|------|---------|------|
| 修改 | `mini-program/src/pages/main/MineView.vue` | 新增「个人档案」区块(computed + template + styles |
| 修改 | `mini-program/src/pages/main/RecordView.vue` | 移除 profile-card 区域及相关代码 |
---
### Task 1: MineView — 新增 computed 属性
**Files:**
- Modify: `mini-program/src/pages/main/MineView.vue:63-138`
- [ ] **Step 1: 在 `<script setup>` 中新增字段 computed 属性**
`harmonyPercent` computed 之后、`openProfileSettings` 方法之前,新增以下 computed:
```js
const genderText = computed(() => {
const map = { male: '男', female: '女', other: '其他' }
return map[profile.value.gender] || null
})
const age = computed(() => {
const birthday = profile.value.birthday
if (!birthday) return null
const birthYear = Number(String(birthday).slice(0, 4))
if (!birthYear) return null
return new Date().getFullYear() - birthYear
})
const hobbyText = computed(() => {
const hobbies = profile.value.hobbies
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4).join(' · ')
return null
})
const goEditProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/MineView.vue
git commit -m "featMineView 新增个人档案字段 computed 属性"
```
---
### Task 2: MineView — 新增「个人档案」模板
**Files:**
- Modify: `mini-program/src/pages/main/MineView.vue:32-34`
- [ ] **Step 1: 在 stats-grid 和 menu-list 之间插入个人档案区块**
在第 32 行 `</view>`stats-grid 结束标签)之后、第 34 行 `<view class="menu-list">` 之前,插入以下模板:
```html
<view class="profile-section" @click="goEditProfile">
<view class="section-header">
<text class="section-star">✦</text>
<text class="section-title">个人档案</text>
</view>
<view class="profile-grid">
<view class="grid-item">
<text class="grid-icon">♋</text>
<text class="grid-label">星座</text>
<text class="grid-value">{{ profile.zodiac || '未设置' }}</text>
</view>
<view class="grid-item">
<text class="grid-icon">◉</text>
<text class="grid-label">MBTI</text>
<text class="grid-value">{{ profile.mbti || '未设置' }}</text>
</view>
<view class="grid-item no-border">
<text class="grid-icon">◻</text>
<text class="grid-label">职业</text>
<text class="grid-value">{{ profile.profession || '未设置' }}</text>
</view>
</view>
<view class="profile-grid">
<view class="grid-item">
<text class="grid-icon">📍</text>
<text class="grid-label">城市</text>
<text class="grid-value">{{ profile.city || '未设置' }}</text>
</view>
<view class="grid-item">
<text class="grid-icon">🏢</text>
<text class="grid-label">行业</text>
<text class="grid-value">{{ profile.industry || '未设置' }}</text>
</view>
<view class="grid-item no-border">
<text class="grid-icon">🏗</text>
<text class="grid-label">公司</text>
<text class="grid-value">{{ profile.company || '未设置' }}</text>
</view>
</view>
<view class="profile-grid">
<view class="grid-item">
<text class="grid-icon">🎂</text>
<text class="grid-label">生日</text>
<text class="grid-value">{{ profile.birthday ? String(profile.birthday).slice(5).replace('-', '.') : '未设置' }}</text>
</view>
<view class="grid-item">
<text class="grid-icon">♂</text>
<text class="grid-label">性别</text>
<text class="grid-value">{{ genderText || '未设置' }}</text>
</view>
<view class="grid-item no-border">
<text class="grid-icon">⏳</text>
<text class="grid-label">年龄</text>
<text class="grid-value">{{ age ? age + '岁' : '未设置' }}</text>
</view>
</view>
<view class="hobby-line">
<text class="hobby-heart">♡</text>
<text class="hobby-label">爱好</text>
<text class="hobby-value">{{ hobbyText || '未设置' }}</text>
</view>
</view>
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/MineView.vue
git commit -m "featMineView 新增个人档案区块模板"
```
---
### Task 3: MineView — 新增「个人档案」样式
**Files:**
- Modify: `mini-program/src/pages/main/MineView.vue:141-435`
- [ ] **Step 1: 在 `<style scoped>` 中新增个人档案区块样式**
`.stat-value` 样式块之后、`.menu-list` 样式块之前,插入以下样式:
```css
.profile-section {
position: relative;
z-index: 1;
margin-top: 42rpx;
}
.section-header {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 28rpx;
}
.section-star {
color: #ffd184;
font-size: 28rpx;
}
.section-title {
color: #fff;
font-size: 40rpx;
font-weight: 800;
}
.profile-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
margin-bottom: 24rpx;
}
.grid-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6rpx;
padding-right: 16rpx;
border-right: 1rpx solid rgba(180, 139, 255, 0.22);
}
.grid-item + .grid-item {
padding-left: 16rpx;
}
.grid-item.no-border {
border-right: 0;
}
.grid-icon {
font-size: 30rpx;
line-height: 1;
margin-bottom: 4rpx;
}
.grid-label {
color: rgba(219, 204, 247, 0.54);
font-size: 20rpx;
}
.grid-value {
color: #fff;
font-size: 23rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
.hobby-line {
display: flex;
align-items: center;
gap: 16rpx;
padding-top: 20rpx;
border-top: 1rpx solid rgba(180, 139, 255, 0.22);
}
.hobby-heart {
color: #a855ff;
font-size: 30rpx;
text-shadow: 0 0 24rpx rgba(168, 85, 255, 0.7);
}
.hobby-label {
color: rgba(219, 204, 247, 0.54);
font-size: 20rpx;
flex-shrink: 0;
}
.hobby-value {
color: #fff;
font-size: 23rpx;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/MineView.vue
git commit -m "featMineView 新增个人档案区块样式"
```
---
### Task 4: RecordView — 移除 profile-card 模板
**Files:**
- Modify: `mini-program/src/pages/main/RecordView.vue:1-59`
- [ ] **Step 1: 移除 profile-card 区域的模板代码**
将模板中的第 3-59 行(从 `<view class="profile-card kos-card">` 到其闭合 `</view>`)完全删除。
删除后,模板应从 `<view class="record-view">` 直接开始,紧接 `<view class="section-head">`
修改后的模板开头应为:
```html
<template>
<view class="record-view">
<view class="section-head">
<view>
<view class="title-line">
<text class="section-title">人生轨迹</text>
<text class="star gold">✦</text>
</view>
<text class="section-subtitle">你的成长之路,正在展开</text>
</view>
<view class="social-import-btn" @click="openSocialImport">
<text>导入社交数据</text>
</view>
</view>
<scroll-view class="filters" scroll-x :show-scrollbar="false">
...(后续保持不变)
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/RecordView.vue
git commit -m "featRecordView 移除 profile-card 模板"
```
---
### Task 5: RecordView — 移除相关 script 代码
**Files:**
- Modify: `mini-program/src/pages/main/RecordView.vue:126-247`
- [ ] **Step 1: 移除不再需要的 computed 和方法**
`<script setup>` 中删除以下内容:
1. 删除 `profile` computed(约第 143 行):
```js
const profile = computed(() => store.userProfile || store.registrationData || {})
```
2. 删除 `heroTags` computed(约第 145-149 行):
```js
const heroTags = computed(() => {
const hobbies = profile.value.hobbies
if (Array.isArray(hobbies) && hobbies.length) return hobbies.slice(0, 4)
return []
})
```
3. 删除 `avatar` computed(约第 151-154 行):
```js
const avatar = computed(() => {
const nickname = profile.value.nickname || 'User'
return `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(nickname)}&backgroundColor=b982ff`
})
```
4. 删除 `editProfile` 方法(约第 223-225 行):
```js
const editProfile = () => {
uni.navigateTo({ url: '/pages/onboarding/index?edit=1' })
}
```
5. `getAgeText` 方法中仍使用 `profile.value.birthYear`,需要保留 `profile` computed 或改为直接引用。由于 `getAgeText` 仍需要 profile 数据,**保留 `profile` computed**,只删除 `heroTags``avatar``editProfile`
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/RecordView.vue
git commit -m "featRecordView 移除无用的 computed 和方法"
```
---
### Task 6: RecordView — 移除相关样式
**Files:**
- Modify: `mini-program/src/pages/main/RecordView.vue:249-784`
- [ ] **Step 1: 移除 profile-card 相关的所有样式**
删除以下样式规则:
- `.profile-card` 及其 `::after` 伪元素
- `.profile-top`, `.avatar-wrap`, `.avatar`, `.avatar-edit`, `.edit-pen`, `.tiny-pen`
- `.profile-info`, `.name-row`, `.profile-name`, `.star`(注意:`.star` 在 section-head 中也有使用,只删除 profile 相关的,保留 `.star.gold`
- `.profile-subtitle`, `.edit-profile`
- `.profile-divider`, `.profile-divider.small`
- `.meta-grid`, `.meta-item`, `.meta-icon`, `.meta-label`, `.meta-value`
- `.person-icon`, `.job-icon` 及其伪元素
- `.hobby-row`, `.hobby-text`
- `.heart`
**注意**`.star` 样式**不要删除**,因为 section-head 中的 `<text class="star gold">✦</text>` 仍在使用它(提供 `font-size: 26rpx`)。
保留的样式(不要删除):
- `.record-view`
- `.section-head`, `.title-line`, `.section-title`, `.star`, `.gold`, `.section-subtitle`
- `.social-import-btn`
- `.filters`, `.filter-row`, `.filter-chip`, `.add-filter`
- `.timeline` 及所有子样式
- `.event-card` 及所有子样式
- `.empty-card`, `.empty-title`, `.empty-text`
- `.create-fab`, `.plus-core`
- `.star.gold`section-head 中使用)
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/RecordView.vue
git commit -m "featRecordView 移除 profile-card 相关样式"
```
---
### Task 7: 浏览器验证
- [ ] **Step 1: 启动 H5 开发服务器**
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 2: 验证 MineView(我的页面)**
打开浏览器访问 `http://localhost:5173`,切换到「我的」tab
检查项:
- [ ] 头像和昵称正常显示
- [ ] 觉醒深度和星历契合正常显示
- [ ] 「个人档案」区块正常显示,包含:
- 星座、MBTI、职业三栏
- 城市、行业、公司三栏
- 生日、性别、年龄三栏
- 爱好行
- [ ] 未设置的字段显示"未设置"
- [ ] 点击「个人档案」区块可跳转到编辑页
- [ ] 菜单列表和退出按钮正常
- [ ] 整体星空风格无变化
- [ ] **Step 3: 验证 RecordView(人生轨迹页面)**
切换到「人生轨迹」tab
检查项:
- [ ] 个人信息卡片已完全移除
- [ ] "人生轨迹"标题和"导入社交数据"按钮正常
- [ ] 筛选器正常
- [ ] 事件时间线正常显示
- [ ] 创建 FAB 按钮正常
- [ ] 页面无 Console 报错
- [ ] **Step 4: 最终提交**
```bash
git add .
git commit -m "feat:完成个人信息组件合并,RecordView 移除个人信息区域,MineView 新增完整个人档案区块"
```
@@ -0,0 +1,125 @@
# 小程序编辑资料页昵称必填提示优化实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在小程序编辑资料页面,当用户点击保存按钮但昵称未填写时,弹出 Toast 提示「请填写昵称」,避免静默无反馈。
**Architecture:** 在现有 `saveProfile` 方法内部调整校验顺序:先拦截重复提交,再校验昵称非空;校验失败时调用 `uni.showToast` 后返回。不引入新文件,不改动其他字段逻辑。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面,包含 `saveProfile` 保存方法 | 修改 |
---
### Task 1: 修改 `saveProfile` 校验逻辑
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:310-328`
- [ ] **Step 1: 查看当前 `saveProfile` 方法**
确认当前代码如下:
```javascript
const saveProfile = async () => {
if (!form.nickname.trim() || saving.value) return
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 2: 修改校验顺序并添加 Toast 提示**
将第一行拆分为两步:先判断 `saving.value`,再校验昵称;昵称为空时弹出 Toast 后返回。
修改后代码:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
store.updateRegistration(JSON.parse(JSON.stringify({ ...form, birthday: birthday.value })))
const res = await store.saveUserProfile()
saving.value = false
if (!res.success) {
uni.showToast({ title: res.error || '保存失败', icon: 'none' })
return
}
syncFromStore()
uni.showToast({ title: '已保存', icon: 'success' })
setTimeout(() => {
if (isEdit.value) uni.navigateBack()
else uni.reLaunch({ url: '/pages/main/index?tab=script' })
}, 350)
}
```
- [ ] **Step 3: 在 H5 模式下验证**
1. 确保 mini-program H5 开发服务器已启动:
```bash
cd mini-program
npm run dev:h5
```
访问 `http://localhost:5173`
2. 进入编辑资料页面(从「我的」→「个人档案」进入)。
3. 清空昵称输入框,点击「保存」。
4. 期望结果:
- 页面顶部弹出 Toast「请填写昵称」。
- 页面不跳转。
- 按钮文字保持「保存」,不显示「保存中」。
- 浏览器 Console 无报错。
5. 填写昵称后再次点击「保存」。
6. 期望结果:
- 弹出 Toast「已保存」。
- 编辑模式下返回上一页。
- 浏览器 Console 无报错。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页昵称未填时增加 Toast 提示"
```
---
## Self-Review
1. **Spec coverage:** 设计文档要求「昵称未填时点击保存给出 Toast 提示」,Task 1 Step 2 直接实现该逻辑。
2. **Placeholder scan:** 无 TBD、TODO 或模糊描述,代码和命令均已给出。
3. **Type consistency:** 仅使用现有 `form.nickname``saving.value``uni.showToast`,与源码保持一致。
@@ -0,0 +1,218 @@
# 去除编辑资料页与剧本列表 mock 数据实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将编辑资料页顶部默认昵称从「Zoey」改为「未设置昵称」,并彻底删除剧本列表中的 `fallbackScripts` 数组及各工具函数的兜底默认值。
**Architecture:** 直接修改两个 Vue 文件:在 `onboarding/index.vue` 替换默认文案;在 `ScriptLibraryView.vue` 删除 `fallbackScripts`、让 `scripts` 直接返回 `store.scripts`,并清理 `getTags``getWordCount``getDateText``getProgress``getChapterCount` 中的默认假值。
**Tech Stack:** Vue 3 + UniApp + TypeScript + Pinia
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 编辑资料页面顶部预览卡片 | 修改 |
| `mini-program/src/pages/main/ScriptLibraryView.vue` | 剧本列表页面 | 修改 |
---
### Task 1: 修改编辑资料页顶部默认昵称
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue:22`
- [ ] **Step 1: 查看当前代码**
确认第 22 行代码:
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
- [ ] **Step 2: 替换默认文案**
将第 22 行改为:
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "fix:编辑资料页无昵称时显示未设置昵称"
```
---
### Task 2: 删除剧本列表 fallbackScripts 数组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:178-254`
- [ ] **Step 1: 查看当前代码**
确认第 178249 行为 `fallbackScripts` 数组:
```javascript
const fallbackScripts = [
{
id: 'demo-1',
title: '逆袭人生:从低谷到巅峰',
// ...
},
// ... 共 6 条
]
```
确认第 251254 行为 `scripts` computed
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
- [ ] **Step 2: 删除 fallbackScripts 并修改 scripts computed**
删除 `fallbackScripts` 数组及其后的空行,将 `scripts` computed 改为:
```javascript
const scripts = computed(() => store.scripts || [])
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表删除 fallbackScripts 假数据"
```
---
### Task 3: 清理剧本列表工具函数默认值
**Files:**
- Modify: `mini-program/src/pages/main/ScriptLibraryView.vue:297-317`
- [ ] **Step 1: 查看当前工具函数代码**
确认以下函数当前实现:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return [script.style || '逆袭成长', '都市', '事业', '热血']
}
const getChapterCount = (script) => script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (!count) return '3.1万字'
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || '2025.05.10'}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || '2025.05.08'}`
return `最近更新:${script.updatedAt || script.date || '今天 21:30'}`
}
const getProgress = (script) => Math.max(1, Math.min(99, Number(script.progress || 28)))
```
- [ ] **Step 2: 修改各函数去掉兜底假值**
将上述函数改为:
```javascript
const getTags = (script) => {
if (Array.isArray(script.tags) && script.tags.length) return script.tags.slice(0, 4)
return []
}
const getChapterCount = (script) => script.chapterCount || script.chapters || 0
const getWordCount = (script) => {
const count = Number(script.wordCount || 0)
if (count >= 10000) return `${(count / 10000).toFixed(1)}万字`
return `${count}字`
}
const getDateText = (script) => {
if (getStatus(script) === 'done') return `完成于:${script.completedAt || script.date || ''}`
if (getStatus(script) === 'draft') return `创建于:${script.createdAt || script.date || ''}`
return `最近更新:${script.updatedAt || script.date || ''}`
}
const getProgress = (script) => Math.max(0, Math.min(99, Number(script.progress || 0)))
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptLibraryView.vue
git commit -m "fix:剧本列表工具函数去除默认值兜底"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保 5173/5175 端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证编辑资料页**
1. 在浏览器中打开 `http://localhost:5173`(或实际分配的端口)。
2. 登录后进入编辑资料页面。
3. 清空昵称输入框,点击保存,确认顶部显示「未设置昵称」。
- [ ] **Step 4: 验证剧本列表空状态**
1. 从爽文生成页面点击左上角「历史」按钮进入剧本列表。
2. 如果当前账号没有剧本数据,确认页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 如果账号有真实剧本数据,确认真实数据正常显示。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 编辑资料页默认昵称 → Task 1
- 删除 fallbackScripts → Task 2
- 清理工具函数默认值 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有函数签名、字段名与源码保持一致。
@@ -0,0 +1,285 @@
# ScriptView 对话结果页 UI 调整实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 让 ScriptView.vue 结果页顶部的历史按钮和关闭按钮不随内容滚动,底部输入框增加左右边距,语音按钮改为麦克风图标。
**Architecture:** 将 `result-top-actions``scroll-view` 内部移到外部作为独立层,利用 flex 布局使其固定;调整 `.result-chat-bar` 内边距和语音按钮内容。
**Tech Stack:** Vue 3 + UniApp + CSS (rpx)
---
## 文件结构
| 文件 | 职责 | 变更类型 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | 剧本生成/对话结果页 | 修改 |
---
### Task 1: 将顶部操作栏移出滚动区域
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:135-158`
- [ ] **Step 1: 查看当前结构**
当前 `result-page` 结构(关键部分):
```vue
<view v-else class="result-page">
<scroll-view
class="result-view"
scroll-y
...
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
...
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 2: 移动 result-top-actions 到 scroll-view 外部**
`result-top-actions` 块从 `result-scroll-content` 中移出,放到 `scroll-view` 之前:
```vue
<view v-else class="result-page">
<view class="result-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<scroll-view
class="result-view"
scroll-y
:scroll-top="resultCommandScrollTop"
:scroll-into-view="resultScrollTarget"
:scroll-with-animation="true"
:enhanced="true"
:show-scrollbar="false"
@scroll="handleResultScroll"
>
<view class="result-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
...
</view>
</scroll-view>
...
</view>
```
- [ ] **Step 3: 调整 result-scroll-content 的 padding**
`.result-scroll-content``padding: 0 0 220rpx;`。由于顶部栏已移出,内容顶部不再需要额外留白,保持 `padding-bottom: 220rpx` 即可:
```css
.result-scroll-content {
min-height: 100%;
box-sizing: border-box;
padding: 0 0 220rpx;
}
```
- [ ] **Step 4: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 结果页顶部操作栏固定不随滚动"
```
---
### Task 2: 底部输入框增加左右边距
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3021-3034`
- [ ] **Step 1: 查看当前样式**
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 0 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 2: 修改 padding 增加左右边距**
`padding: 14rpx 0 22rpx` 改为 `padding: 14rpx 24rpx 22rpx`
```css
.result-chat-bar {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 12;
min-height: 104rpx;
display: flex;
align-items: flex-end;
gap: 12rpx;
padding: 14rpx 24rpx 22rpx;
box-sizing: border-box;
background: linear-gradient(180deg, rgba(5, 2, 13, 0), rgba(5, 2, 13, 0.9) 30%, rgba(5, 2, 13, 0.96));
}
```
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 底部输入框区域增加左右边距"
```
---
### Task 3: 语音按钮改为麦克风图标
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:266-275`
- Modify: `mini-program/src/pages/main/ScriptView.vue:3036-3056`
- [ ] **Step 1: 修改语音按钮内容**
将:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text>语音</text>
</view>
```
改为:
```vue
<view
class="chat-voice-btn"
:class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
@touchstart.prevent="startVoicePress"
@touchend.prevent="endVoicePress"
@touchcancel.prevent="cancelVoicePress"
>
<text class="voice-icon">🎤</text>
</view>
```
- [ ] **Step 2: 增加语音图标样式**
`.chat-voice-btn` 样式后增加:
```css
.voice-icon {
font-size: 32rpx;
line-height: 1;
}
```
并修改按下状态的颜色继承:
```css
.chat-voice-btn.pressing,
.chat-voice-btn.recognizing {
color: #fff;
background: linear-gradient(145deg, #934dff, #4d1ccb);
box-shadow: 0 0 30rpx rgba(168, 85, 247, 0.38);
}
```
`.chat-voice-btn` 本身已有 `color: #e8ccff;`,图标会继承该颜色;按下时父元素颜色变为 `#fff`,图标也会变白。
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fixScriptView 语音按钮改为麦克风图标"
```
---
### Task 4: 编译与 H5 验证
**Files:**
- 无新增/修改文件
- [ ] **Step 1: 运行 H5 生产构建**
```bash
cd mini-program
npm run build:h5
```
期望输出包含 `DONE Build complete.`,无编译错误。
- [ ] **Step 2: 启动 H5 开发服务器**
确保端口未被占用,然后:
```bash
cd mini-program
npm run dev:h5
```
- [ ] **Step 3: 验证顶部按钮固定**
1. 登录后进入爽文生成页。
2. 从历史列表点击一个已有剧本,进入对话结果页。
3. 上下滚动剧本内容,确认左上角「历史」按钮和右上角「×」按钮始终保持在原位,不随内容滚动。
- [ ] **Step 4: 验证底部输入框样式**
1. 确认底部输入框区域左右与屏幕边缘有 24rpx 边距。
2. 确认语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下时变为白色。
3. 确认发送按钮大小不变,可正常点击发送消息。
- [ ] **Step 5: 检查浏览器 Console**
确认没有新增的 JavaScript 错误。
---
## Self-Review
1. **Spec coverage:**
- 顶部按钮固定 → Task 1
- 底部边距 → Task 2
- 麦克风图标 → Task 3
- 编译与验证 → Task 4
2. **Placeholder scan:** 无 TBD、TODO、"implement later" 或模糊描述。
3. **Type consistency:** 所有 class 名、事件名与源码保持一致。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,679 @@
# 小程序添加标签按钮简化与自定义弹窗 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 简化编辑资料页"添加"按钮文字、收紧字数限制到 4 字、标签不换行省略号截断、用自定义深色太空主题弹窗组件替换 `uni.showModal`
**Architecture:** 新建 `TagDialog.vue` 组件支持 input / confirm 双模式,样式与小程序深色太空主题一致;编辑资料页和管理页分别引入该组件,重构 `addCustomTag``confirmDelete``uni.showModal` 切换到自定义弹窗;标签样式增加 `white-space: nowrap` 三件套防止换行。
**Tech Stack:** UniApp 3 + Vue 3 Composition API (`<script setup>`),样式用 rpx,深色太空主题(glass-card 毛玻璃 + 紫色渐变 + 金色强调)。
**Spec 文档:** `docs/superpowers/specs/2026-06-24-tag-add-button-simplify-design.md`
---
## 文件结构总览
### 新增文件
| 文件 | 职责 |
|---|---|
| `mini-program/src/components/TagDialog.vue` | 自定义弹窗组件,input / confirm 双模式,深色太空主题样式 |
### 修改文件
| 文件 | 修改内容 |
|---|---|
| `mini-program/src/pages/onboarding/index.vue` | 按钮文字简化为" 添加"、引入 TagDialog、重构 addCustomTag、字数限制 8→4、toast 文案、`.tag-choice` 不换行 |
| `mini-program/src/pages/onboarding/tag-manage.vue` | 引入 TagDialog、重构 confirmDelete、`.tag-text` 不换行 |
---
## Task 1: 新建 TagDialog.vue 组件
**目标:** 创建自定义弹窗组件,支持 input(输入)和 confirm(确认)两种模式,样式与小程序深色太空主题一致。
**Files:**
- Create: `mini-program/src/components/TagDialog.vue`
### Step 1: 创建组件文件
创建 `mini-program/src/components/TagDialog.vue`,完整内容:
```vue
<template>
<view v-if="visible" class="tag-dialog-mask" @click="onMaskClick">
<view class="tag-dialog-card glass-card" @click.stop>
<view class="tag-dialog-title-row">
<text class="tag-dialog-gold">✦</text>
<text class="tag-dialog-title">{{ title }}</text>
</view>
<input
v-if="mode === 'input'"
class="tag-dialog-input"
:value="modelValue"
:placeholder="placeholder"
placeholder-class="tag-dialog-placeholder"
:maxlength="4"
@input="onInput"
@confirm="onInputConfirm"
/>
<text v-if="mode === 'confirm'" class="tag-dialog-content">{{ content }}</text>
<view class="tag-dialog-actions">
<view class="tag-dialog-btn tag-dialog-btn-cancel" @click="onCancel">
<text>取消</text>
</view>
<view class="tag-dialog-btn tag-dialog-btn-confirm" @click="onConfirm">
<text>确定</text>
</view>
</view>
</view>
</view>
</template>
<script setup>
const props = defineProps({
visible: {
type: Boolean,
default: false
},
mode: {
type: String,
default: 'input'
},
title: {
type: String,
default: ''
},
content: {
type: String,
default: ''
},
placeholder: {
type: String,
default: ''
},
modelValue: {
type: String,
default: ''
}
})
const emit = defineEmits(['confirm', 'cancel', 'update:modelValue', 'update:visible'])
const onInput = (event) => {
const value = event.detail.value
emit('update:modelValue', value)
}
const onInputConfirm = () => {
emit('confirm', props.modelValue)
}
const onConfirm = () => {
if (props.mode === 'input') {
emit('confirm', props.modelValue)
} else {
emit('confirm')
}
}
const onCancel = () => {
emit('cancel')
emit('update:visible', false)
}
const onMaskClick = () => {
emit('cancel')
emit('update:visible', false)
}
</script>
<style scoped>
.tag-dialog-mask {
position: fixed;
inset: 0;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
background: rgba(3, 2, 13, 0.72);
}
.tag-dialog-card {
width: 560rpx;
box-sizing: border-box;
padding: 36rpx 32rpx 28rpx;
border-radius: 22rpx;
border: 1rpx solid rgba(155, 110, 255, 0.14);
background:
radial-gradient(circle at 92% 12%, rgba(104, 66, 255, 0.1), transparent 34%),
rgba(10, 13, 43, 0.54);
box-shadow: inset 0 0 30rpx rgba(123, 60, 255, 0.05), 0 10rpx 36rpx rgba(0, 0, 0, 0.16);
backdrop-filter: blur(24rpx);
-webkit-backdrop-filter: blur(24rpx);
}
.tag-dialog-title-row {
display: flex;
align-items: center;
gap: 12rpx;
margin-bottom: 24rpx;
}
.tag-dialog-gold {
color: #ffd58c;
font-size: 26rpx;
text-shadow: 0 0 20rpx rgba(255, 202, 125, 0.45);
}
.tag-dialog-title {
color: #fff;
font-size: 30rpx;
font-weight: 900;
}
.tag-dialog-input {
width: 100%;
box-sizing: border-box;
height: 80rpx;
margin-bottom: 28rpx;
padding: 0 20rpx;
border-radius: 14rpx;
border: 1rpx solid rgba(151, 111, 255, 0.22);
background: rgba(10, 12, 40, 0.66);
color: #fff;
font-size: 26rpx;
}
.tag-dialog-placeholder {
color: rgba(214, 204, 235, 0.42);
}
.tag-dialog-content {
display: block;
margin-bottom: 32rpx;
color: rgba(239, 232, 255, 0.86);
font-size: 26rpx;
line-height: 1.6;
}
.tag-dialog-actions {
display: flex;
gap: 20rpx;
}
.tag-dialog-btn {
flex: 1;
height: 72rpx;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 26rpx;
font-weight: 700;
}
.tag-dialog-btn-cancel {
color: rgba(239, 232, 255, 0.82);
border: 1rpx solid rgba(151, 111, 255, 0.42);
background: rgba(255, 255, 255, 0.02);
}
.tag-dialog-btn-confirm {
color: #fff;
border: 1rpx solid rgba(206, 82, 255, 0.95);
background: linear-gradient(180deg, rgba(169, 61, 255, 0.62), rgba(107, 41, 206, 0.5));
box-shadow: 0 0 18rpx rgba(168, 67, 255, 0.52), inset 0 1rpx 0 rgba(255, 255, 255, 0.22);
}
</style>
```
### Step 2: 提交
```bash
git add mini-program/src/components/TagDialog.vue
git commit -m "feat:新增 TagDialog 自定义弹窗组件,深色太空主题样式"
```
---
## Task 2: 编辑资料页改动(index.vue
**目标:** 简化添加按钮文字、引入 TagDialog、重构 addCustomTag 用自定义弹窗、字数限制收紧到 4、标签不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/index.vue`
### Step 1: 模板 — 简化按钮文字
修改 `mini-program/src/pages/onboarding/index.vue`
找到性格标签 panel 末尾占位(约第 146 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加标签</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('personality')"> 添加</text>
```
找到兴趣爱好 panel 末尾占位(约第 167 行):
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加兴趣</text>
```
改为:
```html
<text class="tag-choice dashed" @click="addCustomTag('hobby')"> 添加</text>
```
### Step 2: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 185-186 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 3: script — 引入组件
找到 import 区(约第 189-193 行):
```javascript
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 4: script — 新增弹窗响应式状态
找到 `const saving = ref(false)` 这一行(约第 198 行附近,在 `const isEdit = ref(false)` 之后),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input')
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null)
```
### Step 5: script — 重构 addCustomTag 并新增 handleAddTag / onDialogConfirm / onDialogCancel
找到现有的 `addCustomTag` 函数(约第 410-449 行,从 `/**` 注释到 `}` 结束):
```javascript
/**
* 新增自定义标签(性格标签 / 兴趣爱好通用)
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
const title = isPersonality ? '添加性格标签' : '添加兴趣爱好'
uni.showModal({
title,
editable: true,
placeholderText: '请输入标签(最多8个字符)',
success: (res) => {
const value = String(res.content || '').trim()
if (!res.confirm) return
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 8) {
uni.showToast({ title: '标签最多8个字符', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
})
}
```
完整替换为以下四个函数:
```javascript
/**
* 打开添加标签弹窗
* @param {string} type 'personality' 或 'hobby'
*/
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
/**
* 处理添加标签的实际逻辑(字数限制 4 字)
*/
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
// 新标签插入到库数组最前面
library.unshift(value)
// 若已选未满 5 个,自动加入已选;否则仅提示
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
}
dialogAction.value = null
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
### Step 6: 样式 — .tag-choice 不换行
找到 `.tag-choice` 样式(约第 1008-1011 行):
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
}
```
改为:
```css
.tag-choice {
height: 40rpx;
font-size: 21rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 7: 提交
```bash
git add mini-program/src/pages/onboarding/index.vue
git commit -m "feat:编辑页添加按钮简化为添加,字数限制4字,标签不换行,接入自定义弹窗"
```
---
## Task 3: 管理页改动(tag-manage.vue
**目标:** 引入 TagDialog、重构 confirmDelete 用自定义弹窗、标签文字不换行。
**Files:**
- Modify: `mini-program/src/pages/onboarding/tag-manage.vue`
### Step 1: 模板 — 挂载 TagDialog 组件
找到 `</scroll-view>``</view>` 包裹的页面结构末尾(约第 44-46 行):
```html
</scroll-view>
</view>
</template>
```
`</scroll-view>` 之后、`</view>` 之前插入 TagDialog 挂载:
```html
</scroll-view>
<TagDialog
v-model:visible="dialogVisible"
mode="confirm"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
</view>
</template>
```
### Step 2: script — 引入组件
找到 import 区(约第 48-51 行):
```javascript
import { computed, onMounted, reactive, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
```
在最后一行 import 之后追加:
```javascript
import TagDialog from '../../components/TagDialog.vue'
```
### Step 3: script — 新增弹窗响应式状态
找到 `const type = ref('personality')` 这一行(约第 56 行),在其下方追加:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
### Step 4: script — 重构 confirmDelete 并新增 onDialogConfirm / onDialogCancel
找到现有的 `confirmDelete` 函数(约第 127-139 行):
```javascript
/**
* 删除确认
*/
const confirmDelete = (tag) => {
uni.showModal({
title: '确认删除',
content: `确定删除「${tag}」吗?删除后不可恢复`,
success: (res) => {
if (!res.confirm) return
deleteTag(tag)
}
})
}
```
完整替换为以下三个函数:
```javascript
/**
* 打开删除确认弹窗
*/
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
/**
* 弹窗确认回调
*/
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
/**
* 弹窗取消回调
*/
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
### Step 5: 样式 — .tag-text 不换行
找到 `.tag-text` 样式(约第 311-314 行):
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
}
```
改为:
```css
.tag-text {
color: rgba(255, 255, 255, 0.9);
font-size: 26rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
```
### Step 6: 提交
```bash
git add mini-program/src/pages/onboarding/tag-manage.vue
git commit -m "feat:管理页删除确认接入自定义弹窗,标签文字不换行"
```
---
## Task 4: H5 浏览器验证
**目标:** 在 H5 模式下验证 spec 的 8 条验收标准。
### Step 1: 确认 H5 dev server 运行
```bash
python dev-services.py status
```
确认 `Emotion-museum-uniapp 前端` 状态为 `运行中`(端口 5175)。若未运行:
```bash
cd mini-program
npm run dev:h5
```
### Step 2: 逐条验证验收标准
在浏览器中打开 `http://localhost:5175`,登录后进入编辑资料页(`?edit=1`):
| # | 验收标准 | 验证方法 |
|---|---|---|
| 1 | 添加按钮只显示"+ 添加" | 观察性格标签、兴趣爱好 panel 末尾占位文字 |
| 2 | 点击"+ 添加"弹出深色太空主题弹窗 | 点击按钮,验证弹窗为毛玻璃卡片 + 紫色渐变按钮 + 金色 ✦,非白底蓝按钮 |
| 3 | 输入 5 字 toast 提示"内容不能超过4个字" | 输入 5 个字点确定,验证 toast 文案 |
| 4 | 输入 4 字以内新标签,确认后出现在最前 | 输入"测试"点确定,验证标签出现在列表第一位 |
| 5 | 标签过长时省略号截断不换行 | 输入 4 字标签,观察窄屏下是否单行省略号 |
| 6 | 管理页删除弹窗为深色主题 | 点"管理"进入管理页,点 × 或左滑,验证弹窗样式与文案 |
| 7 | 取消按钮和遮罩层点击都能关闭弹窗 | 分别测试取消按钮、点击遮罩层,验证弹窗关闭且不触发动作 |
| 8 | 弹窗 H5 下样式与深色主题一致 | 整体视觉检查 |
### Step 3: 验收通过后报告
所有 8 条通过后,确认工作区干净:
```bash
git status
```
---
## 执行顺序建议
1. Task 1(新建组件)→ Task 2(编辑页接入)→ Task 3(管理页接入)→ Task 4(验证)
2. Task 1 必须先完成,Task 2/3 依赖组件存在。
3. Task 2/3 完成后,H5 dev server 热加载自动生效,可直接进入 Task 4 验证。
@@ -0,0 +1,467 @@
---
author: claude
created_at: 2026-06-27
purpose: AI 消息卡片功能按钮统一实现计划
---
# AI 消息卡片功能按钮统一实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
**Architecture:** 在现有单文件组件 `ScriptView.vue` 内修复 + 扩展,不提取独立组件,保持现有文件结构
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 主页面组件(模板 + 脚本 + 样式)
---
### Task 1: 修复 story-card 展开失效 bug
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:192-210`
- [ ] **Step 1: 移除 resultHasScriptReplies 条件包裹**
打开 `mini-program/src/pages/main/ScriptView.vue`,找到第 192 行的 `<template v-if="!resultHasScriptReplies">`,删除这个条件包裹标签和对应的 `</template>` 闭合标签(第 210 行)。
修改前:
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
</template>
```
修改后:
```vue
<scroll-view
v-if="storyCollapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">
<text class="collapse-row-text">{{ storyCollapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: storyCollapsed }">
<view></view>
<view></view>
</view>
</view>
```
- [ ] **Step 2: 验证修复**
在微信开发者工具中编译运行,进入「实现心愿」页面:
1. 输入心愿并生成故事
2. 点击「继续生成」创建 script 类型消息
3. 点击 story-card 的展开/收起按钮,验证故事正文能正确展开和收起
4. 确认控制台无报错
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 story-card 展开失效 bug
移除 resultHasScriptReplies 条件包裹,让故事正文始终由 storyCollapsed 控制展开/收起"
```
---
### Task 2: 添加新方法支持消息卡片功能
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (script 部分)
- [ ] **Step 1: 添加 copyMessageContent 方法**
`toggleMessageCollapse` 方法之后(约第 450 行),添加复制消息内容的方法:
```javascript
const copyMessageContent = (message) => {
const content = message?.content || ''
if (!content.trim()) {
uni.showToast({ title: '暂无可复制内容', icon: 'none' })
return
}
uni.setClipboardData({
data: content,
success: () => {
uni.showToast({ title: '已复制', icon: 'success' })
}
})
analytics.track('script_message_copy_click', {
message_id: message?.id || '',
content_length: content.length
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 2: 添加 playMessageTts 方法**
`copyMessageContent` 方法之后,添加播放 TTS 的方法:
```javascript
const playMessageTts = (message) => {
const scriptId = currentResult.value?.id || ''
ttsPlayer.playSource(scriptId)
analytics.track('script_message_tts_click', {
message_id: message?.id || '',
script_id: scriptId
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 3: 修改 toggleMessageCollapse 方法添加埋点**
找到 `toggleMessageCollapse` 方法(约第 442-450 行),在方法开头添加埋点代码:
修改前:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
}
```
修改后:
```javascript
const toggleMessageCollapse = (message) => {
collapsedMessageIds.value[message.id] = !isMessageCollapsed(message)
analytics.track('script_message_collapse_toggle', {
message_id: message?.id || '',
collapsed: collapsedMessageIds.value[message.id]
}, { eventType: 'script', pagePath })
}
```
- [ ] **Step 4: 验证方法添加**
在微信开发者工具中编译运行,确认控制台无语法错误。新方法暂时不会被调用,后续任务会在模板中使用。
- [ ] **Step 5: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 添加消息卡片功能方法
- copyMessageContent: 复制消息内容到剪贴板
- playMessageTts: 播放故事 TTS 音频
- toggleMessageCollapse: 添加展开/收起埋点"
```
---
### Task 3: 在 result-chat-list 中添加功能按钮组
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:245-270`
- [ ] **Step 1: 在 assistant 消息中添加功能按钮组**
找到 `result-chat-list` 部分(约第 245-270 行),在消息内容和展开/收起按钮之后、时间戳之前,添加功能按钮组。
修改前:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
修改后:
```vue
<view
v-for="(message, index) in resultMessages"
:key="message.id"
class="result-chat-list"
>
<view
v-if="isAssistantMessage(message)"
class="chat-bubble assistant"
>
<text class="message-content">{{ getMessageContent(message) }}</text>
<view class="message-toggle" @click="toggleMessageCollapse(message)">
<text>{{ isMessageCollapsed(message) ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: isMessageCollapsed(message) }">
<view></view>
<view></view>
</view>
</view>
<view class="message-actions">
<view class="action-btn" @click="copyMessageContent(message)">
<text>复制</text>
</view>
<view class="action-btn" @click="changeDirection">
<text>换个方向</text>
</view>
<view class="action-btn" @click="notLikeMe">
<text>不像我</text>
</view>
<view class="action-btn" @click="continueInChat">
<text>继续生成</text>
</view>
<view class="action-btn primary" @click="playMessageTts(message)">
<text>▶ 播放</text>
</view>
</view>
<text class="message-time">{{ formatMessageTime(message.createdAt) }}</text>
</view>
<!-- user message -->
</view>
```
**说明:**
- 按钮组使用 `.message-actions` 容器,2 列网格布局
- 复制、换个方向、不像我、继续生成使用 `.action-btn` 样式
- 播放按钮使用 `.action-btn.primary` 样式(主色调突出)
- 所有按钮都调用 Task 2 中定义的方法或复用现有方法
- [ ] **Step 2: 验证按钮组显示**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证每条 assistant 消息下方显示 5 个功能按钮(复制、换个方向、不像我、继续生成、播放)
4. 验证按钮布局为 2 列网格
- [ ] **Step 3: 验证按钮功能**
逐个测试按钮功能:
1. **复制**:点击后应复制消息内容,显示「已复制」提示
2. **换个方向**:点击后应进入「换方向」修订确认流程
3. **不像我**:点击后应进入「不像我」修订确认流程
4. **继续生成**:点击后应收起故事并聚焦输入框
5. **播放**:点击后应播放故事 TTS 音频
确认控制台无报错,所有功能正常。
- [ ] **Step 4: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: 在 result-chat-list 中添加功能按钮组
为每条 assistant 消息添加完整功能按钮:
- 复制:复制消息内容到剪贴板
- 换个方向:进入换方向修订确认
- 不像我:进入不像我修订确认
- 继续生成:收起故事并聚焦输入框
- 播放:播放故事 TTS 音频"
```
---
### Task 4: 添加消息按钮组样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue` (style 部分)
- [ ] **Step 1: 添加 .message-actions 样式**
`<style scoped>` 部分(约第 1500 行之后),找到 `.message-toggle` 样式附近,添加按钮组样式:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16rpx;
margin-top: 24rpx;
}
.message-actions .action-btn {
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.08);
border: 2rpx solid rgba(255, 255, 255, 0.15);
border-radius: 16rpx;
color: #e0e0e0;
font-size: 24rpx;
text-align: center;
transition: all 0.2s;
}
.message-actions .action-btn:active {
background: rgba(255, 255, 255, 0.12);
transform: scale(0.98);
}
.message-actions .action-btn.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-color: transparent;
color: #ffffff;
font-weight: 500;
}
.message-actions .action-btn.primary:active {
opacity: 0.85;
}
```
- [ ] **Step 2: 验证样式效果**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面,生成故事
2. 点击「继续生成」创建 assistant 消息
3. 验证按钮样式:
- 按钮组为 2 列网格布局
- 普通按钮为半透明白色背景,圆角边框
- 播放按钮为主色调渐变(紫色)
- 点击时有缩放反馈效果
4. 验证按钮在不同屏幕尺寸下的响应式表现
- [ ] **Step 3: 提交代码**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: 添加消息按钮组样式
- .message-actions: 2 列网格布局,间距 16rpx
- .action-btn: 半透明白色背景,圆角边框,点击反馈
- .action-btn.primary: 主色调渐变(紫色),白色文字"
```
---
### Task 5: 最终验证和边界情况测试
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 测试 story-card 展开/收起**
在微信开发者工具中编译运行:
1. 进入「实现心愿」页面
2. 输入心愿并生成故事
3. 点击 story-card 的「展开全文」按钮,验证故事正文展开
4. 点击「收起全文」按钮,验证故事正文收起
5. 点击「继续生成」创建 script 类型消息
6. 再次点击 story-card 的展开/收起按钮,验证功能正常(之前会失效)
**预期结果:** 所有展开/收起操作正常工作
- [ ] **Step 2: 测试 assistant 消息功能按钮**
继续在上一步的基础上:
1. 验证每条 assistant 消息下方显示 5 个功能按钮
2. 点击「复制」按钮,验证复制成功提示
3. 点击「换个方向」按钮,验证进入修订确认流程
4. 点击「不像我」按钮,验证进入修订确认流程
5. 点击「继续生成」按钮,验证故事收起且输入框获得焦点
6. 点击「播放」按钮,验证 TTS 音频播放
**预期结果:** 所有按钮功能正常
- [ ] **Step 3: 测试边界情况**
测试以下边界场景:
**场景 1:空内容的 assistant 消息**
- 手动构造一条空内容的 assistant 消息(或等待 AI 返回空内容)
- 点击「复制」按钮
- **预期:** 显示「暂无可复制内容」提示
**场景 2pending 状态的 assistant 消息**
- 生成故事后,快速点击「继续生成」
- 观察正在加载的 assistant 消息(pending 状态)
- **预期:** pending 消息不显示功能按钮组
**场景 3user 消息**
- 查看用户发送的消息
- **预期:** user 消息不显示功能按钮组
**场景 4:无 currentResult 时的播放**
- 如果可能,测试在未生成故事时点击播放按钮
- **预期:** 按钮仍可点击,TTS 播放器内部处理空 id
- [ ] **Step 4: 验证埋点**
打开微信开发者工具的控制台,查看埋点日志:
1. 点击 assistant 消息的展开/收起按钮,验证 `script_message_collapse_toggle` 埋点
2. 点击「复制」按钮,验证 `script_message_copy_click` 埋点
3. 点击「播放」按钮,验证 `script_message_tts_click` 埋点
**预期结果:** 所有埋点正确触发,包含正确的参数
- [ ] **Step 5: 检查控制台错误**
在整个测试过程中,持续关注微信开发者工具的控制台:
- 确认无 Vue 警告
- 确认无 JavaScript 错误
- 确认无网络请求错误
**预期结果:** 控制台干净,无任何错误
- [ ] **Step 6: 最终提交(如有修改)**
如果在前面的测试中发现了需要修复的问题,修复后提交:
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复最终验证中发现的问题
[描述修复的具体问题]"
```
如果测试全部通过,无需额外提交。
- [ ] **Step 7: 创建 Pull Request(可选)**
如果项目使用 PR 流程,创建 PR 合并这些更改:
```bash
git push origin HEAD
```
然后在代码托管平台创建 PR,标题:
```
feat: 统一 AI 消息卡片功能按钮并修复 story-card 展开失效
```
PR 描述包含:
- 修复 story-card 展开失效 bug
- 为每条 assistant 消息添加完整功能按钮组
- 添加相应的样式和埋点
- 所有功能已通过测试
@@ -0,0 +1,462 @@
# backend-single 重命名为 server 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 `backend-single` 目录重命名为 `server`,同步更新 Maven artifactId、部署脚本、配置文件、项目文档和历史文档中的所有引用。
**Architecture:** 分 10 个任务按依赖顺序执行。先移动目录(Task 1),再更新构建配置(Task 2-3),然后更新根级脚本(Task 4-5),接着更新文档(Task 6-8),最后批量替换历史文档(Task 9)并全面验证(Task 10)。
**Tech Stack:** Git, Maven, Python (deploy scripts), Bash, sed (batch replace)
---
### Task 1: 目录重命名
**Files:**
- Move: `backend-single/``server/`
- [ ] **Step 1: 使用 git mv 重命名目录**
```bash
cd /g/IdeaProjects/emotion-museun
git mv backend-single server
```
Expected: 目录从 `backend-single/` 重命名为 `server/`Git 历史完整保留。
- [ ] **Step 2: 确认目录已移动**
```bash
ls -d server/
ls server/pom.xml
```
Expected: 两个命令都成功,无报错。
- [ ] **Step 3: 提交目录移动**
```bash
git commit -m "refactor: 重命名 backend-single 目录为 server"
```
---
### Task 2: 更新 Maven pom.xml
**Files:**
- Modify: `server/pom.xml:8,12`
- [ ] **Step 1: 修改 artifactId 和 name**
`server/pom.xml` 中,将第 8 行和第 12 行的 `backend-single` 替换为 `server`
```xml
<!-- 第 8 行 -->
<artifactId>server</artifactId>
<!-- 第 12 行 -->
<name>server</name>
```
- [ ] **Step 2: 验证 pom.xml 修改**
```bash
grep -n "backend-single" server/pom.xml
```
Expected: 无任何输出(无残留引用)。
- [ ] **Step 3: 提交 pom.xml 修改**
```bash
git add server/pom.xml
git commit -m "refactor: 更新 Maven artifactId 为 server"
```
---
### Task 3: 更新后端部署脚本 JAR 名称
**Files:**
- Modify: `server/deploy.py:22`
- Modify: `server/deploy.sh:10`
- [ ] **Step 1: 修改 server/deploy.py 中的 JAR_NAME**
`server/deploy.py` 第 22 行:
```python
JAR_NAME = "backend-single-1.0.0.jar"
```
改为:
```python
JAR_NAME = "server-1.0.0.jar"
```
- [ ] **Step 2: 修改 server/deploy.sh 中的 JAR_NAME**
`server/deploy.sh` 第 10 行:
```bash
JAR_NAME="backend-single-1.0.0.jar"
```
改为:
```bash
JAR_NAME="server-1.0.0.jar"
```
- [ ] **Step 3: 验证后端部署脚本无残留**
```bash
grep -n "backend-single" server/deploy.py server/deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交后端部署脚本修改**
```bash
git add server/deploy.py server/deploy.sh
git commit -m "refactor: 更新后端部署脚本 JAR 名称为 server-1.0.0.jar"
```
---
### Task 4: 更新根目录部署脚本
**Files:**
- Modify: `deploy.py:398,405,413`
- Modify: `deploy.sh:77,81,82,86`
- [ ] **Step 1: 修改根目录 deploy.py**
`deploy.py` 中 3 处 `backend-single` 替换为 `server`
第 398 行注释:
```python
# 后端 - 调用 server/deploy.sh remote
```
第 405 行路径拼接:
```python
deploy_script = PROJECT_DIR / "server" / "deploy.py"
```
第 413 行 cwd
```python
cwd=str(PROJECT_DIR / "server"),
```
- [ ] **Step 2: 修改根目录 deploy.sh**
`deploy.sh` 中 4 处 `backend-single` 替换为 `server`
第 77 行注释:
```bash
# 后端 - 调用 server/deploy.sh
```
第 81 行存在性检查:
```bash
if [ ! -f "server/deploy.sh" ]; then
```
第 82 行错误信息:
```bash
log_error "后端部署脚本不存在: server/deploy.sh"
```
第 86 行 cd 命令:
```bash
cd server
```
- [ ] **Step 3: 验证根部署脚本无残留**
```bash
grep -n "backend-single" deploy.py deploy.sh
```
Expected: 无任何输出。
- [ ] **Step 4: 提交根部署脚本修改**
```bash
git add deploy.py deploy.sh
git commit -m "refactor: 更新根部署脚本中的后端目录引用为 server"
```
---
### Task 5: 更新 .gitignore
**Files:**
- Modify: `.gitignore:386`
- [ ] **Step 1: 修改 .gitignore**
`.gitignore` 第 386 行:
```
backend-single/backend.err
```
改为:
```
server/backend.err
```
- [ ] **Step 2: 验证并提交**
```bash
grep -n "backend-single" .gitignore
git add .gitignore
git commit -m "refactor: 更新 .gitignore 中的后端日志路径"
```
Expected: grep 无输出,提交成功。
---
### Task 6: 更新核心项目文档
**Files:**
- Modify: `CLAUDE.md` (4 处)
- Modify: `AGENTS.md` (4 处)
- Modify: `README.md` (1 处)
- [ ] **Step 1: 修改 CLAUDE.md**
`CLAUDE.md` 中所有 `backend-single` 替换为 `server`
第 23 行项目结构树:
```
├── server/ # Spring Boot 单体后端服务
```
第 37 行标题:
```
### 后端 (server)
```
第 41 行命令:
```
cd server
```
第 50 行 JAR 名称:
```
java -jar target/server-1.0.0.jar
```
- [ ] **Step 2: 修改 AGENTS.md**
`AGENTS.md` 中所有 `backend-single` 替换为 `server`(与 CLAUDE.md 相同的 4 处位置和内容)。
- [ ] **Step 3: 修改 README.md**
`README.md` 第 73 行:
```
├── backend-single/ # SpringBoot单体后端服务
```
改为:
```
├── server/ # SpringBoot单体后端服务
```
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" CLAUDE.md AGENTS.md README.md
git add CLAUDE.md AGENTS.md README.md
git commit -m "docs: 更新核心文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 7: 更新部署相关文档
**Files:**
- Modify: `server/部署说明.md` (1 处)
- Modify: `快速部署参考.md` (4 处)
- Modify: `一键部署说明.md` (5 处)
- [ ] **Step 1: 修改 server/部署说明.md**
`server/部署说明.md` 第 14 行:
```
# 在 backend-single 目录下执行
```
改为:
```
# 在 server 目录下执行
```
- [ ] **Step 2: 修改 快速部署参考.md**
`快速部署参考.md` 中所有 `backend-single` 替换为 `server`4 处:第 71、83、134、136 行)。
- [ ] **Step 3: 修改 一键部署说明.md**
`一键部署说明.md` 中所有 `backend-single` 替换为 `server`5 处:第 78、152、156、301、304 行)。注意第 156 行 `backend-single-1.0.0.jar` 变为 `server-1.0.0.jar`
- [ ] **Step 4: 验证并提交**
```bash
grep -n "backend-single" "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git add "server/部署说明.md" "快速部署参考.md" "一键部署说明.md"
git commit -m "docs: 更新部署文档中的后端目录引用为 server"
```
Expected: grep 无输出,提交成功。
---
### Task 8: 更新前端代码和其他文档
**Files:**
- Modify: `web/src/types/auth.ts:5`
- Modify: `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md:284-287`
- [ ] **Step 1: 修改 web/src/types/auth.ts 注释**
将第 5 行:
```ts
// 登录请求(对齐 backend-single:手机号 + 短信验证码)
```
改为:
```ts
// 登录请求(对齐 server:手机号 + 短信验证码)
```
- [ ] **Step 2: 修改 web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md**
将文件中 4 处 `backend-single/src/main/java/` 替换为 `server/src/main/java/`(第 284-287 行)。
- [ ] **Step 3: 验证并提交**
```bash
grep -rn "backend-single" web/ web-admin/
git add "web/src/types/auth.ts" "web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md"
git commit -m "docs: 更新前端代码和文档中的后端目录引用为 server"
```
Expected: grep 无输出(在这两个目录中),提交成功。
---
### Task 9: 批量替换历史文档
**Files:**
- Modify: `docs/superpowers/` 下约 37 个 `.md` 文件(排除本次新建的设计文档)
- [ ] **Step 1: 使用 sed 批量替换**
在 Windows Git Bash 中执行:
```bash
cd /g/IdeaProjects/emotion-museun
find docs/superpowers -name "*.md" \
! -name "2026-06-27-backend-single-rename-to-server-design.md" \
-exec sed -i 's/backend-single/server/g' {} +
```
说明:
- `sed -i` 原地替换
- `! -name` 排除本次设计文档自身(避免修改自己的文件名引用)
- `s/backend-single/server/g` 全局替换
- [ ] **Step 2: 验证 docs/superpowers/ 中无残留**
```bash
grep -rn "backend-single" docs/superpowers/ \
--include="*.md" \
! --include="2026-06-27-backend-single-rename-to-server-design.md"
```
Expected: 无任何输出。
- [ ] **Step 3: 提交历史文档更新**
```bash
git add docs/superpowers/
git commit -m "docs: 批量更新历史文档中的 backend-single 引用为 server"
```
---
### Task 10: 全面验证
**Files:** 无新增修改
- [ ] **Step 1: 全项目 grep 验证无残留**
```bash
grep -rn "backend-single" --include="*.md" --include="*.py" --include="*.sh" \
--include="*.xml" --include="*.ts" --include="*.java" --include="*.yml" \
--include="*.yaml" --include="*.json" --include="*.properties" .
```
Expected: **无任何输出**。如果有残留,逐一修复。
**注意**:以下文件中出现 `backend-single` 是允许的(不需要修改):
- `docs/superpowers/specs/2026-06-27-backend-single-rename-to-server-design.md`(本次设计文档自身)
- `docs/superpowers/plans/2026-06-27-backend-single-rename-to-server.md`(本次计划文档自身)
- [ ] **Step 2: Maven 编译验证**
```bash
cd /g/IdeaProjects/emotion-museun/server
mvn clean install -DskipTests
```
Expected: BUILD SUCCESS,生成 `target/server-1.0.0.jar`
- [ ] **Step 3: 验证 JAR 文件名称**
```bash
ls -lh server/target/server-1.0.0.jar
```
Expected: 文件存在,名称正确。
- [ ] **Step 4: 验证 Git 历史保留**
```bash
git log --follow --oneline server/pom.xml | head -5
```
Expected: 能看到旧的重命名之前的提交历史。
- [ ] **Step 5: 验证 .gitignore 生效**
```bash
git status
```
Expected: 工作区干净(无意外未跟踪文件)。
- [ ] **Step 6: 最终提交(如有修复)**
如果 Step 1 中发现了残留并修复了,执行:
```bash
git add -A
git commit -m "fix: 修复遗漏的 backend-single 引用"
```
@@ -0,0 +1,606 @@
---
author: claude
created_at: 2026-06-27
purpose: 本地服务管理规则优化实现计划
---
# 本地服务管理规则优化实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 优化 dev-services.py,强制使用脚本管理本地服务,固定前端/H5 端口从 5178 开始累加,并在无必须重启的变更时禁止 restart
**Architecture:** 在 dev-services.py 中内置固定前端端口映射并强制使用,修改各前端项目 vite 配置和 env 文件同步端口,新增 restart 命令热加载保护,更新 CLAUDE.md 规则
**Tech Stack:** Python, Vite, UniApp, Spring Boot
---
## 文件结构
**修改:**
- `dev-services.py` - 添加固定端口映射、强制端口分配、热加载保护
- `web/vite.config.ts` - 端口改为 5178
- `web/.env.development` - 添加 `VITE_PORT=5178`
- `web-admin/vite.config.ts` - 端口改为 5179
- `web-admin/.env.development` - `VITE_APP_PORT` 改为 5179
- `mini-program/vite.config.js` - 端口改为 5180
- `mini-program/.env.development` - 添加 `VITE_PORT=5180`
- `life-script/vite.config.js` - 端口改为 5181
- `life-script/.env.development` - 添加 `VITE_PORT=5181`
- `CLAUDE.md` - 更新本地服务管理规则
---
### Task 1: 修改 dev-services.py 固定前端端口并保护端口分配
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 在常量区添加固定前端端口映射**
找到 `DEFAULT_PORTS` 定义(约第 58-68 行),在其后添加:
```python
# 前端/H5 服务固定端口映射(按项目目录名匹配)
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
- [ ] **Step 2: 修改 `_update_service_port()` 函数以支持 Vite 端口**
当前 `_update_service_port()` 已处理 `--port``--server.port` 参数,还需要处理 Vite 的 `server.port` 配置更新。函数本身不需要修改,因为端口值更新后,`assign_unique_ports()` 会通过 `_service_has_explicit_port()` 判断。
但需要注意:对于 Vite/UniApp/Taro 服务,如果 `start_cmd` 中没有 `--port` 参数(当前 UniApp 的 `dev:h5` 脚本就没有),需要在启动命令前注入 `VITE_PORT` 环境变量或在命令中追加 `--port` 参数。
修改 `_update_service_port()` 函数,使其在更新 `start_cmd` 时,如果命令中没有 `--port` 参数,则在命令末尾追加 `--port {port}`
修改前:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port" and index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
svc.start_cmd = updated
```
修改后:
```python
def _update_service_port(svc: Service, port: int):
old_port = svc.port
if old_port == port:
return
svc.port = port
svc.health_url = re.sub(r":\d+", f":{port}", svc.health_url, count=1)
updated = []
skip_next = False
has_port_arg = False
for index, part in enumerate(svc.start_cmd):
if skip_next:
skip_next = False
continue
if part == "--port":
has_port_arg = True
if index + 1 < len(svc.start_cmd):
updated.extend([part, str(port)])
skip_next = True
elif part.startswith("--port="):
has_port_arg = True
updated.append(f"--port={port}")
elif f"--server.port={old_port}" in part:
updated.append(part.replace(f"--server.port={old_port}", f"--server.port={port}"))
else:
updated.append(part)
# Vite/UniApp/Taro/Next 服务如果没有 --port 参数,追加 --port
if not has_port_arg and svc.project_type in {"vite", "uniapp", "taro", "next"}:
updated.extend(["--port", str(port)])
svc.start_cmd = updated
```
- [ ] **Step 3: 重写 `assign_unique_ports()` 强制前端固定端口**
修改前:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
reserved = {svc.port for svc in services if _service_has_explicit_port(svc)}
used = set()
for svc in services:
port = svc.port
if port in used or (port in reserved and not _service_has_explicit_port(svc)):
while port in used or port in reserved:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
used.add(svc.port)
return services
```
修改后:
```python
def assign_unique_ports(services: list[Service]) -> list[Service]:
# 按目录名匹配固定前端端口
dir_name_to_service = {}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
dir_name_to_service[dir_name] = svc
# 先为固定前端服务分配端口
for dir_name, svc in dir_name_to_service.items():
fixed_port = FIXED_FRONTEND_PORTS[dir_name]
if svc.port != fixed_port:
log(f"{svc.name} 使用固定前端端口 {fixed_port}", "INFO")
_update_service_port(svc, fixed_port)
# 检查固定端口冲突
used_ports = {}
for svc in services:
port = svc.port
if port in used_ports:
other = used_ports[port]
log(f"端口冲突: {svc.name} ({port}) 与 {other.name} ({port}) 冲突", "ERROR")
sys.exit(1)
used_ports[port] = svc
# 非固定前端服务按原有逻辑处理冲突
reserved = {svc.port for svc in services}
for svc in services:
dir_name = svc.project_dir.name
if dir_name in FIXED_FRONTEND_PORTS:
continue # 固定前端服务已处理
port = svc.port
if port in used_ports and used_ports[port] is not svc:
while port in used_ports:
port += 1
log(f"{svc.name} 端口 {svc.port} 与其他服务冲突,自动调整为 {port}", "WARN")
_update_service_port(svc, port)
return services
```
- [ ] **Step 4: 验证 dev-services.py 语法**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:成功列出所有发现的服务,且前端服务端口为 5178-5181
- [ ] **Step 5: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py 强制前端服务使用固定端口
- 新增 FIXED_FRONTEND_PORTS 映射
- 前端/H5 端口从 5178 开始固定分配
- 端口冲突时不再自动递增前端服务端口,改为报错"
```
---
### Task 2: 为 dev-services.py 添加 restart 热加载保护
**Files:**
- Modify: `dev-services.py`
- [ ] **Step 1: 添加"必须重启"文件模式常量**
`RESTART_REQUIRED_PATTERNS` 常量区(与 `FIXED_FRONTEND_PORTS` 一起)添加:
```python
# 必须重启才能生效的文件模式
RESTART_REQUIRED_PATTERNS = [
re.compile(r'vite\.config\.(ts|js|mjs|cjs)$'),
re.compile(r'tsconfig\.json$'),
re.compile(r'\.env(\.[^/]*)?$'),
re.compile(r'package\.json$'),
re.compile(r'application.*\.ya?ml$'),
re.compile(r'pom\.xml$'),
re.compile(r'build\.gradle(\.kts)?$'),
]
```
注意:需要确保文件顶部已导入 `re``sys`
- [ ] **Step 2: 添加 `_should_restart_for_changes()` 函数**
`assign_unique_ports()` 函数附近添加:
```python
def _should_restart_for_changes(service_dir: Path) -> tuple[bool, list[str]]:
"""
检查指定服务目录下是否有必须重启才能生效的变更。
返回: (是否需要重启, 已修改文件列表)
"""
try:
result = subprocess.run(
["git", "diff", "--name-only", "--", str(service_dir)],
capture_output=True,
text=True,
cwd=SCRIPT_DIR,
check=True,
)
changed_files = [line.strip() for line in result.stdout.splitlines() if line.strip()]
except subprocess.CalledProcessError:
# 非 git 仓库或 git 命令失败,默认允许重启
return True, []
except FileNotFoundError:
return True, []
if not changed_files:
return False, []
for file_path in changed_files:
for pattern in RESTART_REQUIRED_PATTERNS:
if pattern.search(file_path):
return True, changed_files
return False, changed_files
```
- [ ] **Step 3: 修改 restart 命令解析支持 --force**
找到 `main()` 函数中的 `restart` 子命令解析部分(约文件末尾),添加 `--force` 参数。
当前 restart 解析可能类似:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
```
修改后:
```python
restart_parser = subparsers.add_parser("restart", help="重启所有或指定服务")
restart_parser.add_argument("service", nargs="?", help="指定服务名")
restart_parser.add_argument(
"--force",
action="store_true",
help="强制重启,忽略热加载保护"
)
```
- [ ] **Step 4: 在 restart 执行前加入热加载保护**
找到 `cmd_restart()` 函数,在其开头添加检测逻辑。
假设 `cmd_restart()` 函数签名当前为:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
```
修改后:
```python
def cmd_restart(args):
services = discover_services()
services = _filter_services(services, args.service)
if not args.force:
blocked = []
for svc in services:
should_restart, changed_files = _should_restart_for_changes(svc.project_dir)
if not should_restart:
if changed_files:
blocked.append((svc.name, changed_files))
else:
blocked.append((svc.name, []))
if blocked:
log("检测到以下服务无需重启(当前修改支持热加载):", "WARN")
for name, files in blocked:
if files:
log(f" - {name}: 修改文件 {', '.join(files)}", "WARN")
else:
log(f" - {name}: 未检测到代码变更", "WARN")
log("如需强制重启,请使用: python dev-services.py restart --force", "WARN")
sys.exit(0)
# 原有 restart 逻辑继续...
```
注意:实际代码中 `cmd_restart()` 的函数名和参数可能不同,请根据实际代码调整。
- [ ] **Step 5: 验证热加载保护**
1. 修改一个 `.vue` 文件(例如 `mini-program/src/pages/main/ScriptView.vue`
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启,并建议使用 `--force`
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 6: 提交**
```bash
git add dev-services.py
git commit -m "feat: dev-services.py restart 命令新增热加载保护
- 无必须重启的变更时禁止 restart
- 支持 --force 参数强制重启
- 避免无意义重启前端热加载服务"
```
---
### Task 3: 同步各前端项目端口配置
**Files:**
- Modify: `web/vite.config.ts`, `web/.env.development`
- Modify: `web-admin/vite.config.ts`, `web-admin/.env.development`
- Modify: `mini-program/vite.config.js`, `mini-program/.env.development`
- Modify: `life-script/vite.config.js`, `life-script/.env.development`
- [ ] **Step 1: 修改 web/ 端口为 5178**
修改 `web/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5178,
// ... 其他配置
}
```
修改/创建 `web/.env.development`,添加:
```
VITE_PORT=5178
```
- [ ] **Step 2: 修改 web-admin/ 端口为 5179**
修改 `web-admin/vite.config.ts` 中的 `server.port`
```typescript
server: {
port: 5179,
// ... 其他配置
}
```
修改 `web-admin/.env.development`,将 `VITE_APP_PORT` 改为:
```
VITE_APP_PORT=5179
```
- [ ] **Step 3: 修改 mini-program/ 端口为 5180**
修改 `mini-program/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5180,
// ... 其他配置
}
```
修改/创建 `mini-program/.env.development`,添加:
```
VITE_PORT=5180
```
- [ ] **Step 4: 修改 life-script/ 端口为 5181**
修改 `life-script/vite.config.js` 中的 `server.port`
```javascript
server: {
port: 5181,
// ... 其他配置
}
```
修改/创建 `life-script/.env.development`,添加:
```
VITE_PORT=5181
```
- [ ] **Step 5: 验证端口配置**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 服务端口为 5178
- web-admin 服务端口为 5179
- mini-program 服务端口为 5180
- life-script 服务端口为 5181
- [ ] **Step 6: 提交**
```bash
git add web/ web-admin/ mini-program/ life-script/
git commit -m "chore: 统一前端/H5 服务固定端口
- web: 5178
- web-admin: 5179
- mini-program: 5180
- life-script: 5181"
```
---
### Task 4: 更新 CLAUDE.md 规则文档
**Files:**
- Modify: `CLAUDE.md`
- [ ] **Step 1: 添加/更新本地服务管理规则章节**
`CLAUDE.md` 中找到现有的"本地服务管理规则"或"热加载规则"章节,合并更新为统一的"本地服务管理规则(强制)":
```markdown
## 本地服务管理规则(强制)
**启动、重启、停止本地前后端服务时,必须使用项目根目录下的 `dev-services.py` 脚本。**
### 强制要求
- ✅ 必须使用:`python dev-services.py [start|stop|restart|status|discover] [服务名]`
- ❌ 禁止直接使用 `npm run dev` / `pnpm run dev` 启动前端
- ❌ 禁止直接使用 `npm run dev:h5` 启动小程序 H5
- ❌ 禁止直接使用 `mvn spring-boot:run` 启动后端
- ❌ 禁止无意义重启支持热加载的服务
### 前端/H5 固定端口
| 服务 | 目录 | 固定端口 |
|---|---|---|
| web | `web/` | 5178 |
| web-admin | `web-admin/` | 5179 |
| mini-program | `mini-program/` | 5180 |
| life-script | `life-script/` | 5181 |
### 热加载规则
1. **不需要重启的场景**(热加载自动生效)
- 前端:修改 `.vue``.tsx``.ts``.js``.css``.scss` 等源码文件
- 样式、模板、静态资源等修改
2. **需要重启的场景**
- 前端:修改 `vite.config.ts``tsconfig.json``.env``package.json`、别名配置等
- 后端:按现有后端规则处理(本地不启动后端)
3. **禁止无意义重启**
- 执行 `python dev-services.py restart` 时,脚本会自动检测是否有必须重启才能生效的变更
- 如果只有支持热加载的源码文件变更,脚本会拒绝重启并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
- 确需强制重启时,使用 `--force` 参数
```
- [ ] **Step 2: 删除冲突的旧规则**
检查 CLAUDE.md 中是否有与上述新规则冲突的旧规则(如旧的前端端口说明、热加载规则),删除或更新它们,确保文档内部一致。
- [ ] **Step 3: 提交**
```bash
git add CLAUDE.md
git commit -m "docs: 更新本地服务管理规则
- 强制使用 dev-services.py
- 明确前端/H5 固定端口
- 新增禁止无意义重启规则"
```
---
### Task 5: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 验证 dev-services.py 发现服务端口正确**
```bash
cd "G:/IdeaProjects/emotion-museun"
python dev-services.py discover
```
预期输出:
- web 端口 5178
- web-admin 端口 5179
- mini-program 端口 5180
- life-script 端口 5181
- 后端服务保持原有端口
- [ ] **Step 2: 验证热加载保护**
1. 修改 `mini-program/src/pages/main/ScriptView.vue` 中任意一处(例如加一个空行)
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:提示无需重启
3. 运行:
```bash
python dev-services.py restart mini-program --force
```
预期:强制重启
- [ ] **Step 3: 验证必须重启的场景**
1. 修改 `mini-program/vite.config.js` 中任意一处
2. 运行:
```bash
python dev-services.py restart mini-program
```
预期:正常允许重启
- [ ] **Step 4: 验证端口冲突检测**
1. 手动启动一个占用 5178 端口的进程(例如另一个 node 服务)
2. 运行:
```bash
python dev-services.py start web
```
预期:报错,提示端口 5178 被占用
3. 终止占用进程
- [ ] **Step 5: 检查 CLAUDE.md 一致性**
快速浏览 `CLAUDE.md`,确认:
- 没有重复的本地服务管理规则
- 前端端口说明与新的固定端口表一致
- 热加载规则与 dev-services.py 行为一致
- [ ] **Step 6: 提交验证修复(如有)**
如果在验证中修复了问题:
```bash
git add dev-services.py CLAUDE.md
git commit -m "fix: 修复本地服务管理规则验证中的问题
[描述具体修复]"
```
如果全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ dev-services.py 固定前端端口 → Task 1
- ✅ 热加载保护 → Task 2
- ✅ 前端项目端口配置同步 → Task 3
- ✅ CLAUDE.md 规则更新 → Task 4
- ✅ 验证 → Task 5
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** `FIXED_FRONTEND_PORTS``RESTART_REQUIRED_PATTERNS``_should_restart_for_changes` 命名和用法一致
@@ -0,0 +1,658 @@
---
author: claude
created_at: 2026-06-27
purpose: MessageCard 组件统一提取实现计划
---
# MessageCard 组件统一实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 提取 MessageCard.vue 组件,统一 story-card 和 result-chat-list 中 AI 消息卡片的样式,并按 100 字阈值自动切换短消息/完整卡片模式
**Architecture:** 新建 `MessageCard.vue` 组件封装长消息卡片结构,ScriptView.vue 中 story-card 和 result-chat-list 都复用该组件,短消息直接渲染简洁气泡
**Tech Stack:** Vue 3, UniApp, 微信小程序
---
## 文件结构
**创建:**
- `mini-program/src/components/MessageCard.vue` - 通用 AI 消息卡片组件
**修改:**
- `mini-program/src/pages/main/ScriptView.vue` - 替换 story-card 和 result-chat-list 中的 assistant 消息渲染,并迁移相关样式
---
### Task 1: 创建 MessageCard.vue 组件(短消息模式 + 基础结构)
**Files:**
- Create: `mini-program/src/components/MessageCard.vue`
- Modify: 无
- [ ] **Step 1: 创建组件文件并写入基础模板**
`mini-program/src/components/MessageCard.vue` 写入以下内容:
```vue
<template>
<view v-if="isShortMessage" class="chat-bubble system">
<text>{{ content }}</text>
</view>
<view v-else class="story-card" :class="{ collapsed }">
<view class="story-head">
<view class="story-title-wrap">
<text class="story-title">{{ title || '我的人生剧本' }}</text>
<view v-if="tags?.length" class="tag-row">
<text v-for="tag in tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="$emit('toggle-collapse')">
<text class="collapse-icon-text">{{ collapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</button>
<button class="copy-card-btn" @click="$emit('copy')">复制</button>
</view>
</view>
<scroll-view
v-if="collapsed"
class="story-body-scroll"
scroll-y
:enhanced="true"
:show-scrollbar="true"
>
<text class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
<view class="collapse-row" @click="$emit('toggle-collapse')">
<text class="collapse-row-text">{{ collapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view>
<view></view>
</view>
</view>
<view class="result-actions">
<button class="action-btn" @click="$emit('change-direction')">换个方向</button>
<button class="action-btn" @click="$emit('not-like-me')">不像我</button>
<button class="action-btn" @click="$emit('continue')">继续生成</button>
<button class="action-btn primary" @click="$emit('play-tts')">
<text class="action-icon">{{ ttsIcon || '▶' }}</text>
<text>{{ ttsText || '播放' }}</text>
</button>
</view>
</view>
</template>
<script setup>
const props = defineProps({
content: { type: String, required: true },
title: { type: String, default: '' },
tags: { type: Array, default: () => [] },
collapsed: { type: Boolean, default: false },
contentLength: { type: Number, required: true },
isShortMessage: { type: Boolean, required: true },
ttsIcon: { type: String, default: '▶' },
ttsText: { type: String, default: '播放' }
})
defineEmits([
'toggle-collapse',
'copy',
'change-direction',
'not-like-me',
'continue',
'play-tts'
])
</script>
<style scoped>
.chat-bubble {
max-width: 86%;
display: flex;
flex-direction: column;
gap: 8rpx;
padding: 24rpx 28rpx;
border-radius: 36rpx;
font-size: 34rpx;
line-height: 52rpx;
}
.chat-bubble.system {
align-self: flex-start;
background: rgba(16, 8, 34, 0.28);
border: 1rpx solid rgba(192, 132, 252, 0.3);
color: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.story-card {
border-radius: 52rpx;
padding: 34rpx;
background: rgba(12, 5, 28, 0.34);
border: 1rpx solid rgba(192, 132, 252, 0.46);
box-shadow: 0 0 48rpx rgba(125, 55, 205, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.story-card.collapsed {
box-shadow: 0 0 42rpx rgba(125, 55, 205, 0.14);
}
.story-head {
display: flex;
align-items: flex-start;
gap: 20rpx;
}
.story-head-actions {
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: stretch;
gap: 12rpx;
}
.story-title-wrap {
flex: 1;
min-width: 0;
}
.story-title {
display: block;
font-size: 52rpx;
font-weight: 700;
line-height: 1.35;
}
.collapse-icon {
width: 120rpx;
min-width: 120rpx;
height: 58rpx;
min-height: 58rpx;
line-height: 1;
padding: 0 18rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
color: rgba(255, 244, 255, 0.95);
background:
linear-gradient(135deg, rgba(134, 72, 255, 0.44), rgba(39, 15, 76, 0.42)),
rgba(255, 255, 255, 0.05);
border: 1rpx solid rgba(216, 180, 254, 0.46);
box-shadow:
0 14rpx 30rpx rgba(61, 18, 113, 0.22),
inset 0 1rpx 0 rgba(255, 255, 255, 0.26),
inset 0 -10rpx 18rpx rgba(33, 9, 73, 0.16);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
}
.collapse-icon::after {
border: 0;
}
.collapse-icon:active,
.copy-card-btn:active,
.collapse-row:active {
transform: scale(0.98);
opacity: 0.92;
}
.copy-card-btn {
width: 120rpx;
min-width: 120rpx;
height: 50rpx;
min-height: 50rpx;
line-height: 1;
padding: 0;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 244, 255, 0.9);
font-size: 23rpx;
font-weight: 800;
letter-spacing: 0;
background:
radial-gradient(circle at 50% -30%, rgba(255, 255, 255, 0.18), transparent 50%),
rgba(88, 28, 135, 0.26);
border: 1rpx solid rgba(216, 180, 254, 0.32);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 10rpx 22rpx rgba(61, 18, 113, 0.12);
backdrop-filter: blur(8rpx);
-webkit-backdrop-filter: blur(8rpx);
}
.copy-card-btn::after {
border: 0;
}
.collapse-icon-text,
.collapse-row-text {
line-height: 1;
white-space: nowrap;
}
.collapse-chevron {
position: relative;
width: 20rpx;
height: 16rpx;
flex-shrink: 0;
transition: transform 0.18s ease;
}
.collapse-chevron.down {
transform: rotate(180deg);
}
.collapse-chevron view {
position: absolute;
top: 7rpx;
width: 12rpx;
height: 4rpx;
border-radius: 999rpx;
background: linear-gradient(90deg, #fff3b0, #ffd86b);
box-shadow: 0 0 12rpx rgba(255, 216, 107, 0.5);
}
.collapse-chevron view:first-child {
left: 0;
transform: rotate(-38deg);
transform-origin: right center;
}
.collapse-chevron view:last-child {
right: 0;
transform: rotate(38deg);
transform-origin: left center;
}
.tag-row {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 18rpx;
}
.tag {
padding: 6rpx 16rpx;
border-radius: 999rpx;
color: #d18aff;
font-size: 24rpx;
background: rgba(168, 85, 247, 0.22);
}
.story-body {
display: block;
margin-top: 28rpx;
font-size: 32rpx;
font-weight: 400;
line-height: 1.78;
color: rgba(255, 255, 255, 0.92);
white-space: pre-wrap;
}
.story-body-scroll {
max-height: 420rpx;
height: auto;
margin-top: 28rpx;
padding-right: 8rpx;
box-sizing: border-box;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.story-body-scroll .story-body {
margin-top: 0;
}
.collapse-row {
position: relative;
height: 62rpx;
margin-top: 22rpx;
padding: 0 28rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
border-radius: 999rpx;
color: rgba(246, 230, 255, 0.96);
font-size: 26rpx;
font-weight: 800;
overflow: hidden;
background:
radial-gradient(circle at 50% -20%, rgba(255, 255, 255, 0.2), transparent 46%),
linear-gradient(135deg, rgba(98, 40, 174, 0.3), rgba(24, 8, 58, 0.24));
border: 1rpx solid rgba(192, 132, 252, 0.36);
box-shadow:
inset 0 1rpx 0 rgba(255, 255, 255, 0.16),
0 12rpx 28rpx rgba(61, 18, 113, 0.14);
backdrop-filter: blur(6rpx);
-webkit-backdrop-filter: blur(6rpx);
}
.collapse-row::before {
content: '';
position: absolute;
left: 34rpx;
right: 34rpx;
top: 0;
height: 1rpx;
background: linear-gradient(90deg, transparent, rgba(255, 236, 180, 0.6), transparent);
}
.result-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 26rpx;
}
.action-btn {
height: 72rpx;
min-height: 72rpx;
padding: 0 8rpx;
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
color: #e8ccff;
font-size: 26rpx;
font-weight: 700;
line-height: 1.15;
text-align: center;
white-space: normal;
box-sizing: border-box;
background: rgba(88, 28, 135, 0.18);
border: 1rpx solid rgba(192, 132, 252, 0.35);
}
.action-btn.primary {
color: #fff;
background: linear-gradient(145deg, #8c44f2, #5f1db8);
}
.action-icon {
margin-right: 8rpx;
font-size: 24rpx;
font-weight: 900;
line-height: 1;
}
.action-btn::after {
border: 0;
}
</style>
```
- [ ] **Step 2: 验证组件文件创建成功**
确认文件路径:`mini-program/src/components/MessageCard.vue` 存在。
```bash
ls "G:/IdeaProjects/emotion-museun/mini-program/src/components/MessageCard.vue"
```
- [ ] **Step 3: 提交组件创建**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/components/MessageCard.vue
git commit -m "feat: 创建 MessageCard 组件
封装长消息/故事卡片结构,支持短消息简洁气泡模式"
```
---
### Task 2: 在 ScriptView.vue 中注册并使用 MessageCard 组件
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 导入组件**
`ScriptView.vue``<script setup>` 顶部,导入 MessageCard 组件:
修改前:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
```
修改后:
```javascript
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
import MessageCard from '../../components/MessageCard.vue'
```
- [ ] **Step 2: 替换 story-card 模板**
找到第 172-219 行的 `story-card` 模板,替换为 MessageCard 组件。
修改前:
```vue
<view class="story-card" :class="{ collapsed: storyCollapsed, dialogMode: resultHasScriptReplies }">
...(原有 story-card 全部内容)...
</view>
```
修改后:
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
- [ ] **Step 3: 替换 result-chat-list 中的 assistant 消息**
找到第 221-254 行的 `result-chat-list` 模板,替换为使用 MessageCard。
修改前:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
class="chat-bubble"
:class="{ user: message.role === 'user', system: message.role === 'assistant', pending: message.pending }"
>
<text selectable user-select>{{ getMessageDisplayContent(message) }}</text>
<view v-if="message.pending" class="thinking-dots">
<view></view>
<view></view>
<view></view>
</view>
<view
v-if="isAssistantMessage(message)"
class="message-toggle"
@click.stop="toggleMessageCollapse(message)"
>
<text>{{ isMessageCollapsed(message) ? '展开' : '收起' }}</text>
...
</view>
<view v-if="isAssistantMessage(message)" class="message-actions">
...
</view>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
修改后:
```vue
<view v-if="resultMessages.length" class="result-chat-list">
<view
v-for="message in resultMessages"
:key="message.id"
>
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
</view>
```
- [ ] **Step 4: 提交使用组件**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat: ScriptView 复用 MessageCard 组件
- story-card 使用 MessageCard
- result-chat-list 中的 assistant 消息按内容长度自动切换短消息/完整卡片模式"
```
---
### Task 3: 迁移并清理样式
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`
- [ ] **Step 1: 从 ScriptView.vue 中删除已迁移到 MessageCard 的样式**
删除以下 CSS 类(从 `<style scoped>` 中移除):
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
- `.message-actions`(上一轮临时添加的,不再使用)
- `.message-toggle`(不再使用)
保留 `ScriptView.vue` 中仍然需要的样式,如:
- `.chat-bubble.user`
- `.bubble-time`
- `.result-chat-list`
- `.thinking-dots`
- 页面整体布局相关样式
- [ ] **Step 2: 验证样式清理后无遗漏**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 3: 提交样式清理**
```bash
cd "G:/IdeaProjects/emotion-museun"
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor: 迁移 story-card 相关样式到 MessageCard
删除 ScriptView.vue 中已迁移到 MessageCard 的样式类"
```
---
### Task 4: 最终验证
**Files:**
- 无需修改文件,仅做验证
- [ ] **Step 1: 构建验证**
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run build:h5 2>&1 | tail -20
```
预期输出:`DONE Build complete.`
- [ ] **Step 2: 功能验证(在 H5 浏览器中)**
启动 H5 开发服务器并验证:
```bash
cd "G:/IdeaProjects/emotion-museun/mini-program"
npm run dev:h5
```
打开浏览器访问显示的端口(默认 `http://localhost:5173`),执行以下验证:
1. **第一次生成故事**:确认 story-card 样式、标题、标签、展开/收起、复制、功能按钮组均正常
2. **重新生成(生成 script 类型长消息)**
- 内容 ≥ 100 字:显示与 story-card 完全一致的卡片样式
- 内容 < 100 字:显示简洁气泡,无任何功能按钮
3. **普通聊天 assistant 消息**
- 内容 ≥ 100 字:显示完整卡片
- 内容 < 100 字:显示简洁气泡
4. **用户消息**:保持原有 `.chat-bubble.user` 样式不变
5. **控制台**:确认无 Vue 警告和 JavaScript 错误
- [ ] **Step 3: 提交验证结果(如有修复)**
如果在验证中修复了问题,提交:
```bash
git add mini-program/src/components/MessageCard.vue mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: 修复 MessageCard 组件验证中发现的问题
[描述具体修复内容]"
```
如果验证全部通过,无需额外提交。
---
## 自审检查
- **Spec 覆盖:**
- ✅ 创建 MessageCard.vue 组件 → Task 1
- ✅ ScriptView.vue 复用组件 → Task 2
- ✅ 100 字阈值判断 → Task 2 (`:is-short-message="message.content.length < 100"`)
- ✅ 样式迁移和清理 → Task 3
- ✅ 验证 → Task 4
- **无占位符:** 计划中没有 TBD/TODO
- **类型一致:** props 名称 `contentLength` 与模板中使用一致,事件名称在组件和父组件中一致
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,225 @@
# Chat 视图底部遮挡架构层修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复 ScriptView chat 视图 outline 输入框 placeholder 和修改/确认大纲按钮被主页 bottom-nav 遮挡的问题。`.chat-page``height: 100vh` 改为 `height: 100%`,让父级 `.content``padding-bottom: 132rpx`)自然生效;同时把 `.chat-scroll-content``padding-bottom` 从 120rpx 改回 40rpx,避免双重 padding。
**Architecture:** 架构层修正而非 CSS 遮罩。让 chat-page 正确填充父级已预留 bottom-nav 空间的内容区,chat 滚动区底部自然避开 bottom-nav。iPhone 安全区用 `env(safe-area-inset-bottom)` 处理。
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译 + rpx 单位 + `env(safe-area-inset-bottom)` 安全区适配
---
### Task 1: 修改 .chat-page 从 height: 100vh 改为 height: 100%
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3589-3598`
- [ ] **Step 1: 修改 .chat-page 样式**
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3589-3598 行的 `.chat-page` 样式块:
```css
.chat-page {
height: 100vh;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
```
改为(`height: 100vh``height: 100%`,删除旧版 iOS 兼容的 `constant()`):
```css
.chat-page {
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor: .chat-page 改为 height: 100% 让父级 padding 生效"
```
---
### Task 2: 修改 .chat-scroll-content 改回 padding-bottom: 40rpx
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3617-3619`
- [ ] **Step 1: 修改 .chat-scroll-content 样式**
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3617-3619 行的 `.chat-scroll-content` 样式块:
```css
.chat-scroll-content {
padding: 0 24rpx 120rpx;
}
```
改为(`padding-bottom` 从 120rpx 改回 40rpx,避免与父级 `.content``padding-bottom: 132rpx` 双重叠加):
```css
.chat-scroll-content {
padding: 0 24rpx 40rpx;
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: .chat-scroll-content padding-bottom 改回 40rpx 避免与父级 padding 叠加"
```
---
### Task 3: H5 浏览器验证修复效果
**Files:**
- Test: `http://localhost:5180` (H5 开发服务器,端口 5180 由 `dev-services.py` 管理)
- [ ] **Step 1: 确认 H5 开发服务器运行中**
```bash
python dev-services.py status
```
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
```bash
python dev-services.py start mini-program
```
等待 `VITE` 编译完成,确认输出包含 `Local: http://localhost:5180/`
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
1. 用浏览器打开 `http://localhost:5180`
2. 确认已登录(如未登录,使用手机号验证码 123456 登录)
3. 点击底部导航「爽文生成」
4. 点击任意一条灵感推荐填充输入框,点击「发送」
- [ ] **Step 3: 检查 .chat-page 计算样式**
在浏览器 DevTools Console 中执行:
```javascript
() => {
const el = document.querySelector('.chat-page');
if (!el) return 'chat-page not found';
const styles = window.getComputedStyle(el);
const parent = el.parentElement;
const parentStyles = parent ? window.getComputedStyle(parent) : null;
return {
chatPage: {
height: styles.height,
paddingBottom: styles.paddingBottom
},
parent: parent ? {
tag: parent.tagName,
className: parent.className,
paddingBottom: parentStyles.paddingBottom
} : null
};
}
```
Expected: `chatPage.height` 应小于视口高度(不再 100vh),`parent.paddingBottom` 应为 `66px` (= 132rpx @ 750rpx 设计稿/2 = 实际 132rpx / 2 = 66px in 375px viewport)。
- [ ] **Step 4: 等待 SSE 返回澄清卡片或 outline**
在 chat 视图等待 10-15 秒:
- 若出现澄清卡片(ClarificationCard):向下滚动,确认「提交」按钮在 bottom-nav 上方完整可见
- 若出现 outline(带 beats 列表):向下滚动,确认 placeholder "如需修改请输入意见" 完整可见、「修改大纲」「确认大纲」按钮在 bottom-nav 上方完整可点击
- [ ] **Step 5: 验证 chat-scroll-content 实际高度**
在 Console 中执行:
```javascript
() => {
const scrollContent = document.querySelector('.chat-scroll-content');
const scroll = document.querySelector('.chat-scroll');
if (!scrollContent || !scroll) return 'not found';
return {
scrollContentPaddingBottom: window.getComputedStyle(scrollContent).paddingBottom,
scrollHeight: scroll.offsetHeight,
scrollClientHeight: scroll.clientHeight,
scrollScrollHeight: scroll.scrollHeight
};
}
```
Expected: `scrollContentPaddingBottom` = `20px` (= 40rpx / 2 in 375px viewport)`scrollScrollHeight > scrollClientHeight` 表示有内容可滚动。
- [ ] **Step 6: 验证 home 视图不受影响**
点击 chat 视图右上角「×」关闭按钮返回 home 视图,确认:
- ✅ 首页正常渲染
- ✅ 灵感推荐、输入框、语音球布局完整
- ✅ bottom-nav 与 home 内容无重叠
---
### Task 4: mp-weixin 编译验证
**Files:**
- Build: `mini-program/unpackage/dist/build/mp-weixin/`
- [ ] **Step 1: 执行 mp-weixin 编译**
```bash
cd mini-program
npm run build:mp-weixin
```
Expected: 编译成功,无 error 输出。编译产物在 `unpackage/dist/build/mp-weixin/`
- [ ] **Step 2: 验证编译产物样式已更新**
```bash
grep -o '.chat-page[^}]*}' mini-program/unpackage/dist/build/mp-weixin/pages/main/ScriptView.wxss
grep -o '.chat-scroll-content[^}]*}' mini-program/unpackage/dist/build/mp-weixin/pages/main/ScriptView.wxss
```
Expected:
- `.chat-page` 包含 `height:100%``padding-bottom:env(safe-area-inset-bottom)`
- `.chat-page` 不再包含 `height:100vh``constant(safe-area-inset-bottom)`
- `.chat-scroll-content` 的 padding 为 `0 24rpx 40rpx`(不再是 120rpx
- [ ] **Step 3: 提交(如有微调)**
如有需要,执行:
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: mp-weixin 编译产物验证通过"
```
---
### Task 5: 通知用户在微信开发者工具中验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> mp-weixin 编译产物已更新(`mini-program/unpackage/dist/build/mp-weixin/`)。请在**微信开发者工具**中:
> 1. 打开 `unpackage/dist/build/mp-weixin` 目录
> 2. 点击「编译」按钮重新编译
> 3. 进入心愿实现页面,触发对话流直至出现 outline 消息(带 beats 列表)
> 4. 向下滚动,确认:
> - outline 输入框的 placeholder "如需修改请输入意见" 完整可见(不被截断)
> - 「修改大纲」「确认大纲」按钮在底部导航栏上方完整可见可点击
> 5. 在 iPhone 真机/模拟器上验证底部 home indicator 区域不遮挡内容
>
> 如有问题,截图反馈。
@@ -0,0 +1,188 @@
# Chat 视图底部内边距修复实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 修复 ScriptView chat 视图 ClarificationCard 提交按钮被主页 bottom-nav(104rpx)遮挡导致用户无法滚动到可见的问题。
**Architecture:** 在 `ScriptView.vue` 样式中,给 `.chat-page` 增加 `env(safe-area-inset-bottom)` 适配 iPhone 安全区,给 `.chat-scroll-content``padding-bottom` 从 40rpx 增至 120rpx,使滚动内容底部留出 bottom-nav 高度(104rpx+ 16rpx 呼吸间距。
**Tech Stack:** UniApp Vue 3 `<script setup>` + mp-weixin 编译 + rpx 单位 + `env(safe-area-inset-bottom)` 安全区适配
---
### Task 1: 修改 .chat-page 增加 iPhone 安全区内边距
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3589-3596`
- [ ] **Step 1: 修改 .chat-page 样式**
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3589-3596 行的 `.chat-page` 样式块:
```css
.chat-page {
height: 100vh;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
}
```
改为(新增 `padding-bottom` 两行,与 `App.vue``.safe-area-bottom` 的写法保持一致,`constant()` 兼容 iOS 11.0-11.2`env()` 覆盖 iOS 11.2+):
```css
.chat-page {
height: 100vh;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: .chat-page 增加 safe-area-inset-bottom 适配 iPhone 底部"
```
---
### Task 2: 修改 .chat-scroll-content 增加底部内边距
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue:3615-3617`
- [ ] **Step 1: 修改 .chat-scroll-content 样式**
打开 `mini-program/src/pages/main/ScriptView.vue`,定位到第 3615-3617 行的 `.chat-scroll-content` 样式块:
```css
.chat-scroll-content {
padding: 0 24rpx 40rpx;
}
```
改为(`padding-bottom` 从 40rpx 增至 120rpx = bottom-nav 104rpx + 16rpx 呼吸间距):
```css
.chat-scroll-content {
padding: 0 24rpx 120rpx;
}
```
- [ ] **Step 2: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "fix: .chat-scroll-content padding-bottom 增至 120rpx 避开 bottom-nav 遮挡"
```
---
### Task 3: H5 浏览器验证修复效果
**Files:**
- Test: `http://localhost:5180` (H5 开发服务器,端口 5180 由 `dev-services.py` 管理)
- [ ] **Step 1: 确认 H5 开发服务器运行中**
```bash
python dev-services.py status
```
Expected: `mini-program` 服务在端口 5180 上运行。如未运行,执行:
```bash
python dev-services.py start mini-program
```
等待 `VITE` 编译完成,确认输出包含 `Local: http://localhost:5180/`
- [ ] **Step 2: 浏览器打开页面并进入 chat 视图**
1. 用浏览器打开 `http://localhost:5180`
2. 确认已登录(如未登录,使用手机号验证码 123456 登录)
3. 点击底部导航「爽文生成」
4. 点击任意一条灵感推荐(如「我想把最近一次低谷,改写成主角觉醒的开端。」)
5. 点击「发送」按钮
- [ ] **Step 3: 验证澄清卡片提交按钮可见且可点击**
进入 chat 视图后,等待 SSE 返回澄清卡片。在浏览器中验证:
- ✅ 澄清卡片完整渲染(标题、描述、选项、自定义输入框)
- ✅ 向下滚动时「提交」按钮在 bottom-nav(「人生轨迹/爽文生成/我的」)上方,不被遮挡
- ✅ 选中一个选项后「提交」按钮变为可点击状态(不再灰色/禁用)
- ✅ 点击「提交」后流程正常继续(追加一条 user 消息气泡)
- [ ] **Step 4: 检查浏览器 Console 无报错**
打开浏览器 DevTools → Console 面板,确认:
- ✅ 无新增红色 ERROR`uni.getRecorderManager not supported` 是 H5 环境预期内的 warning,可忽略)
- ✅ Network 面板中 `/shortNovel/followup` 请求返回正常响应
- [ ] **Step 5: 验证 home 视图不受影响**
点击 chat 视图右上角「×」关闭按钮,确认:
- ✅ 返回首页(wish-home)正常渲染
- ✅ 灵感推荐、输入框、语音球布局未受 `padding-bottom` 影响
- ✅ 主页 bottom-nav 正常显示,与 home 视图内容无重叠
- [ ] **Step 6: 验证 chat-input-bar 出现时布局正确**
`generationPhase === 'done'` 时(即 novel_done 后或从历史记录打开已有剧本时):
- ✅ 底部输入栏(`chat-input-bar`)在 bottom-nav 上方显示,无重叠
- ✅ 输入栏与滚动内容之间无多余空隙
- ✅ 输入栏内的 textarea 和「发送」按钮均可正常交互
---
### Task 4: mp-weixin 编译验证
**Files:**
- Build: `mini-program/unpackage/dist/dev/mp-weixin/`
- [ ] **Step 1: 执行 mp-weixin 编译**
```bash
cd mini-program
npm run build:mp-weixin
```
Expected: 编译成功,无 error 输出。编译产物在 `unpackage/dist/dev/mp-weixin/`
- [ ] **Step 2: 验证编译产物样式已更新**
```bash
grep "padding-bottom.*env" unpackage/dist/dev/mp-weixin/pages/main/ScriptView.wxss
grep "120rpx" unpackage/dist/dev/mp-weixin/pages/main/ScriptView.wxss
```
Expected:
- `padding-bottom: env(safe-area-inset-bottom);` 出现在 `ScriptView.wxss`
- `.chat-scroll-content``padding-bottom` 值为 `120rpx`
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "style: mp-weixin 编译产物验证通过"
```
---
### Task 5: 通知用户在微信开发者工具中验证
- [ ] **Step 1: 提示用户操作**
告知用户:
> mp-weixin 编译产物已更新(`unpackage/dist/dev/mp-weixin/`)。请在**微信开发者工具**中:
> 1. 打开 `unpackage/dist/dev/mp-weixin` 目录
> 2. 点击「编译」按钮重新编译
> 3. 进入心愿实现页面,选择灵感推荐 → 发送
> 4. 等待澄清卡片出现后,向下滚动,确认「提交」按钮在底部导航栏上方完整可见
> 5. 选中一个选项,点击「提交」,确认流程正常继续
> 6. 在 iPhone 真机/模拟器上验证底部 home indicator 区域不遮挡内容
>
> 如有问题,截图反馈。
@@ -0,0 +1,874 @@
# ScriptView 统一对话页改造实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 把 ScriptView 心愿实现页的澄清/大纲/小说生成从独立视图切换改为统一对话流,用户提交心愿后始终停留在同一对话页面
**Architecture:** viewState 简化为 home+chat;扩展 resultMessages 消息模型支持 kind: text/card/outline/novel + 交互态字段;handleShortNovelEvent 把 SSE 事件转为对话消息追加;卡片/大纲消息内嵌交互按钮,用户操作后转 user 消息并调 followupStreamnovel_done 后底部输入栏按 generationPhase 切换到旧接口 streamScriptChat 做自由对话修改
**Tech Stack:** UniApp + Vue 3 Composition API`<script setup>`),shortNovel.js(新接口 SSE),scriptChat.js(旧接口流式对话),H5 热加载验证
---
## 文件结构
### 修改文件
- `mini-program/src/pages/main/ScriptView.vue` — 核心改造文件(约 3300 行)
- 扩展 `addResultMessage` 消息模型
- 新增 `generationPhase` 状态
- 改造 `handleShortNovelEvent` 为对话流追加消息
- 改造 `runGeneration`/`submitClarification`/`confirmOutline`/`modifyOutline`/`resumeSession` 操作对话消息
- 改造 `sendChat` 按 generationPhase 切换接口
- 新增统一 chat 视图模板,删除 clarifying/outlining/novel-generating 独立视图
### 不变文件
- `mini-program/src/components/ClarificationCard.vue` — 复用,由父组件控制 submitted 态
- `mini-program/src/services/shortNovel.js` — 新接口不变
- `mini-program/src/services/scriptChat.js` — 旧接口不变
### 验证方式
本项目无前端单元测试框架,采用 **H5 热加载 + Playwright 浏览器验证**。每个任务结束后在浏览器确认页面行为。
---
## Task 1: 扩展消息模型 + 新增 generationPhase 状态
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`addResultMessage 函数约 1283 行,新增 generationPhase ref 约 350 行附近)
- [ ] **Step 1: 扩展 addResultMessage 支持 card/outline/交互态字段**
找到 `addResultMessage` 函数(约 1283 行),当前实现:
```javascript
const addResultMessage = ({ role, content, pending = false, kind = '' }) => {
const message = {
id: createMessageId(),
role,
kind,
content,
pending,
time: formatMessageTime()
}
resultMessages.value.push(message)
if (!pending) persistResultMessages()
keepResultAtBottom()
return message
}
```
替换为(新增 card/outline/submitted/confirmed/failed/answer/feedback 字段):
```javascript
const addResultMessage = ({ role, content, pending = false, kind = 'text', card = null, outline = null, answer = '', feedback = '', failed = false }) => {
const message = {
id: createMessageId(),
role,
kind,
content,
pending,
time: formatMessageTime(),
card,
outline,
answer,
feedback,
failed,
submitted: false,
confirmed: false
}
resultMessages.value.push(message)
if (!pending) persistResultMessages()
keepResultAtBottom()
return message
}
```
- [ ] **Step 2: 新增 generationPhase ref**
找到 `const resumeableSessionId = ref('')`(约 350 行),在其后新增一行:
```javascript
const resumeableSessionId = ref('')
const generationPhase = ref('idle')
```
`generationPhase` 取值:`'idle'`(未开始)/`'generating'`(生成流程中,用新接口)/`'done'`novel_done 后,用旧接口)。
- [ ] **Step 3: 验证热加载无编译错误**
H5 服务在后台运行(端口 5180),保存文件后 Vite 自动热加载。打开浏览器 http://localhost:5180 确认页面无白屏、Console 无语法错误。
- [ ] **Step 4: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): 扩展 addResultMessage 消息模型 + 新增 generationPhase"
```
---
## Task 2: 改造 handleShortNovelEvent 为对话流追加消息
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`handleShortNovelEvent 函数约 1682 行)
- [ ] **Step 1: 改造 handleShortNovelEvent**
找到 `handleShortNovelEvent` 函数(约 1682 行),当前实现是切换 viewState。替换为往 resultMessages 追加消息:
```javascript
const handleShortNovelEvent = (event) => {
const { type, session_id, payload = {} } = event
if (session_id) novelSessionId.value = session_id
switch (type) {
case 'status':
// 仅更新加载提示,不追加独立消息
generationStatus.value = payload.stage || 'processing'
break
case 'clarification_card':
addResultMessage({ role: 'assistant', kind: 'card', card: payload.card || null })
generationStatus.value = 'idle'
break
case 'outline_created':
addResultMessage({ role: 'assistant', kind: 'outline', outline: payload.outline || null })
generationStatus.value = 'idle'
break
case 'novel_start':
addResultMessage({ role: 'assistant', kind: 'novel', content: '', pending: true })
generationStatus.value = 'streaming'
break
case 'novel_delta': {
// 往最后一条 novel 消息追加 delta
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel' && m.pending)
if (lastNovel) {
lastNovel.content += payload.delta || ''
}
keepResultAtBottom()
break
}
case 'novel_done': {
// 标记最后一条 novel 消息完成
const lastNovel = [...resultMessages.value].reverse().find(m => m.kind === 'novel')
if (lastNovel) {
if (payload.full_text) lastNovel.content = payload.full_text
lastNovel.pending = false
}
scriptId.value = payload.scriptId || ''
conversationId.value = payload.conversationId || ''
currentVersionMessageId.value = payload.currentVersionMessageId || ''
generationPhase.value = 'done'
generationStatus.value = 'idle'
persistResultMessages()
break
}
case 'error':
if (payload.code === 'DAILY_GENERATION_IN_PROGRESS' && session_id) {
resumeableSessionId.value = session_id
markGenerationFailed(payload.message || '今天已有未完成的创作,可点击继续创作恢复')
} else {
markGenerationFailed(payload.message || '生成失败')
addResultMessage({ role: 'assistant', kind: 'text', content: payload.message || '生成失败', failed: true })
}
break
}
}
```
关键变化:
- 不再切换 `viewState`(始终保持 `chat`
- `clarification_card` → 追加 kind:'card' 消息
- `outline_created` → 追加 kind:'outline' 消息
- `novel_start` → 追加 pending 的 kind:'novel' 消息
- `novel_delta` → 往最后一条 pending novel 消息追加文本
- `novel_done` → 标记 novel 消息完成,设置 `generationPhase='done'`
- [ ] **Step 2: 验证热加载无错误**
保存后浏览器确认无 Console 错误(此时 UI 还未改造,但逻辑应无语法错误)。
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): handleShortNovelEvent 改为对话流追加消息"
```
---
## Task 3: 改造 runGeneration 和交互函数
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`runGeneration 约 1626 行,submitClarification/confirmOutline/modifyOutline/resumeSession 约 1727 行)
- [ ] **Step 1: 改造 runGeneration 进入 chat 视图并追加用户消息**
找到 `runGeneration` 函数(约 1626 行)。当前在函数中段设置 `viewState.value = 'generating'``generationDisplayText.value = display`
`viewState.value = 'generating'` 改为 `viewState.value = 'chat'`,并在 `generationDisplayText.value = display` 后新增追加用户消息的逻辑:
找到这一段:
```javascript
streamWriter.reset()
startGenerationFeedback()
ttsPlayer.reset()
viewState.value = 'generating'
keepGenerationAtBottom()
```
替换为:
```javascript
streamWriter.reset()
startGenerationFeedback()
ttsPlayer.reset()
viewState.value = 'chat'
generationPhase.value = 'generating'
resultMessages.value = []
addResultMessage({ role: 'user', kind: 'text', content: display })
keepResultAtBottom()
```
说明:进入 chat 视图,清空旧消息,把用户心愿作为第一条 user 消息追加。
- [ ] **Step 2: 改造 submitClarification 接收消息对象**
找到 `submitClarification` 函数(约 1727 行),当前签名 `const submitClarification = (answer) =>`。替换为接收消息对象 + answer:
```javascript
const submitClarification = (msg, answer) => {
if (answeringClarification.value) return
if (msg) {
msg.submitted = true
msg.answer = answer
}
addResultMessage({ role: 'user', kind: 'text', content: answer })
answeringClarification.value = true
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
action: 'answer_clarification',
payload: { answer },
onEvent: handleShortNovelEvent,
onError: (msg) => markGenerationFailed(msg)
})
setTimeout(() => { answeringClarification.value = false }, 200)
}
```
- [ ] **Step 3: 改造 confirmOutline 接收消息对象**
找到 `confirmOutline` 函数(约 1740 行),替换为:
```javascript
const confirmOutline = (msg) => {
if (msg) msg.confirmed = true
addResultMessage({ role: 'user', kind: 'text', content: '确认大纲' })
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
action: 'confirm_outline',
payload: null,
onEvent: handleShortNovelEvent,
onError: (msg) => markGenerationFailed(msg)
})
}
```
- [ ] **Step 4: 改造 modifyOutline 接收消息对象**
找到 `modifyOutline` 函数(约 1750 行),替换为:
```javascript
const modifyOutline = (msg) => {
const feedback = msg?.feedback || ''
if (!feedback.trim()) {
uni.showToast({ title: '请先填写修改意见', icon: 'none' })
return
}
if (msg) msg.confirmed = true
addResultMessage({ role: 'user', kind: 'text', content: feedback })
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
action: 'modify_outline',
payload: { feedback },
onEvent: handleShortNovelEvent,
onError: (msg) => markGenerationFailed(msg)
})
}
```
- [ ] **Step 5: 改造 resumeSession 在对话流内恢复**
找到 `resumeSession` 函数(约 1761 行),替换为:
```javascript
const resumeSession = () => {
if (!resumeableSessionId.value || generating.value) return
novelSessionId.value = resumeableSessionId.value
resumeableSessionId.value = ''
generating.value = true
generationPhase.value = 'generating'
generationStatus.value = 'waiting'
generationError.value = ''
addResultMessage({ role: 'user', kind: 'text', content: '继续之前的创作' })
streamWriter.reset()
startGenerationFeedback()
keepResultAtBottom()
try {
currentStreamTask.value = followupStream({
sessionId: novelSessionId.value,
action: 'retry',
payload: null,
onEvent: handleShortNovelEvent,
onError: (msg) => {
markGenerationFailed(msg)
analytics.track('script_generate_fail', {
source: 'resume',
error: msg
}, { eventType: 'script', pagePath })
}
})
} catch (error) {
markGenerationFailed(error.message || '恢复失败')
} finally {
generating.value = false
}
}
```
关键变化:不再切换 viewState(保持 chat),改为追加 user 消息"继续之前的创作"。
- [ ] **Step 6: 验证热加载无错误**
保存后浏览器确认无 Console 错误。
- [ ] **Step 7: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "refactor(mini-program): runGeneration+交互函数改为操作对话消息"
```
---
## Task 4: 新增统一 chat 视图模板 + 删除旧独立视图
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`template 部分,约 89-183 行的 clarifying/outlining 块 + 139-183 的 generating 块 + 185-302 的 result 块)
- [ ] **Step 1: 删除 clarifying 和 outlining 独立视图块**
找到 `clarifying` 视图块(约 89 行 `<view v-else-if="viewState === 'clarifying'"` 到 102 行 `</view>`)和 `outlining` 视图块(约 104 行 `<view v-else-if="viewState === 'outlining'"` 到 137 行 `</view>`),**整块删除**。
删除这两个块后,template 里不再有 viewState==='clarifying' 和 'outlining' 的分支。
- [ ] **Step 2: 改造 generating 视图为 chat 视图**
找到 generating 视图块(约 139 行 `<view v-else-if="viewState === 'generating"`,但由于 Step 1 删除了前面两个块,行号会前移)。把整个 generating 块替换为统一 chat 视图:
`<view v-else-if="viewState === 'generating'" class="generation-view">` 整块(到对应的 `</view>` 结束)替换为:
```vue
<view v-else-if="viewState === 'chat'" class="chat-page">
<view class="chat-top-actions">
<view class="history-button" @click="openScriptLibrary">
<view class="history-lines">
<view></view>
<view></view>
<view></view>
</view>
<text>历史</text>
</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
<scroll-view
class="chat-scroll"
scroll-y
:scroll-top="resultCommandScrollTop"
:scroll-into-view="resultScrollTarget"
:scroll-with-animation="true"
:enhanced="true"
:show-scrollbar="false"
@scroll="handleResultScroll"
>
<view class="chat-scroll-content">
<view id="result-scroll-top" class="result-scroll-top"></view>
<view class="conversation compact">
<view
v-for="msg in resultMessages"
:key="msg.id"
>
<!-- user text -->
<view v-if="msg.role === 'user' && msg.kind === 'text'" class="chat-bubble user">
<text>{{ msg.content }}</text>
<text class="bubble-time">{{ msg.time }}</text>
</view>
<!-- assistant card -->
<view v-else-if="msg.kind === 'card'" class="chat-bubble system">
<ClarificationCard
v-if="!msg.submitted && msg.card"
:card="msg.card"
@submit="(answer) => submitClarification(msg, answer)"
/>
<view v-else class="card-answered">
<text>已回答:{{ msg.answer }}</text>
</view>
</view>
<!-- assistant outline -->
<view v-else-if="msg.kind === 'outline'" class="chat-bubble system">
<view v-if="msg.outline">
<view v-if="msg.outline.title" class="outline-title">
<text>{{ msg.outline.title }}</text>
</view>
<view v-if="msg.outline.logline" class="outline-logline">
<text>{{ msg.outline.logline }}</text>
</view>
<view v-if="Array.isArray(msg.outline.beats)" class="outline-beats">
<view v-for="(beat, idx) in msg.outline.beats" :key="idx" class="beat-item">
<text class="beat-order">{{ beat.order || idx + 1 }}</text>
<view class="beat-content">
<text class="beat-title">{{ beat.title || '' }}</text>
<text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
</view>
</view>
</view>
<view v-if="msg.outline.ending" class="outline-ending">
<text class="ending-label">结局</text>
<text class="ending-text">{{ msg.outline.ending }}</text>
</view>
</view>
<view v-if="!msg.confirmed" class="outline-actions">
<input v-model="msg.feedback" placeholder="如需修改请输入意见" class="outline-feedback" />
<view class="outline-buttons">
<view class="btn-secondary" @click="modifyOutline(msg)">修改大纲</view>
<view class="btn-primary" @click="confirmOutline(msg)">确认大纲</view>
</view>
</view>
</view>
<!-- assistant novel -->
<view v-else-if="msg.kind === 'novel'" class="chat-bubble system">
<text class="novel-text">{{ msg.content }}<text v-if="msg.pending" class="typing-cursor">|</text></text>
</view>
<!-- assistant text (status/error) -->
<view v-else class="chat-bubble system">
<text :class="{ 'generation-error': msg.failed }">{{ msg.content }}</text>
<text class="bubble-time">{{ msg.time }}</text>
</view>
</view>
<!-- 底部 loading -->
<view v-if="generating && generationStatus !== 'failed'" class="chat-loading">
<view class="loading-orbit" :class="{ streaming: generationStatus === 'streaming' }">
<view class="orbit-ring outer"></view>
<view class="orbit-ring inner"></view>
<view class="orbit-core"></view>
</view>
<text class="loading-copy">{{ generationCopy }}</text>
</view>
<!-- 失败继续创作入口 -->
<view v-if="generationStatus === 'failed' && resumeableSessionId" class="resume-action">
<button class="generation-action primary" @click="resumeSession">继续创作</button>
</view>
<view :id="resultScrollAnchor" class="result-scroll-anchor"></view>
</view>
</view>
</scroll-view>
<!-- 底部输入栏:novel_done 后启用 -->
<view v-if="generationPhase === 'done'" class="chat-input-bar">
<view class="result-input-shell">
<textarea
class="result-chat-input"
v-model="chatInput"
:auto-height="true"
:show-confirm-bar="false"
maxlength="500"
confirm-type="send"
placeholder="输入修改建议,或让 AI 继续生成"
@confirm="sendChat"
/>
</view>
<view class="chat-send-btn" :class="{ disabled: !chatInput.trim() || generating }" @click="sendChat">发送</view>
</view>
</view>
```
- [ ] **Step 3: 删除独立的 result 视图块**
找到原 result 视图块(`<view v-else class="result-page">`,约 185 行到 302 行)。由于 chat 视图已包含所有对话功能,**整块删除**原 result-page 块。
删除后 template 的 v-if/v-else-if 链为:`home``chat`
- [ ] **Step 4: 新增 chat 视图相关样式**
`<style scoped>` 部分末尾添加 chat 视图样式(复用现有 result/generation 样式,新增缺失部分):
```scss
.chat-page {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #13091f;
}
.chat-top-actions {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
}
.chat-scroll {
flex: 1;
height: calc(100vh - 180rpx);
}
.chat-scroll-content {
padding: 0 24rpx 40rpx;
}
.card-answered {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
padding: 16rpx 0;
}
.outline-title {
color: #ffffff;
font-size: 40rpx;
font-weight: 700;
margin-bottom: 16rpx;
}
.outline-logline {
color: rgba(255, 255, 255, 0.85);
font-size: 28rpx;
line-height: 1.6;
margin-bottom: 24rpx;
}
.outline-beats {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 24rpx;
}
.beat-item {
display: flex;
gap: 20rpx;
align-items: flex-start;
}
.beat-order {
flex-shrink: 0;
width: 56rpx;
height: 56rpx;
background: #087e8b;
border-radius: 50%;
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
}
.beat-content {
flex: 1;
display: flex;
flex-direction: column;
}
.beat-title {
color: #ffffff;
font-size: 28rpx;
font-weight: 600;
}
.beat-summary {
color: rgba(255, 255, 255, 0.7);
font-size: 26rpx;
margin-top: 8rpx;
line-height: 1.5;
}
.outline-ending {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}
.ending-label {
color: rgba(255, 255, 255, 0.6);
font-size: 24rpx;
display: block;
margin-bottom: 8rpx;
}
.ending-text {
color: #ffffff;
font-size: 28rpx;
line-height: 1.6;
}
.outline-actions {
margin-top: 24rpx;
}
.outline-feedback {
width: 100%;
padding: 20rpx;
background: rgba(255, 255, 255, 0.05);
border-radius: 12rpx;
color: #ffffff;
font-size: 28rpx;
margin-bottom: 16rpx;
box-sizing: border-box;
}
.outline-buttons {
display: flex;
gap: 16rpx;
justify-content: flex-end;
}
.novel-text {
color: #ffffff;
font-size: 28rpx;
line-height: 1.8;
white-space: pre-wrap;
}
.typing-cursor {
color: #087e8b;
animation: blink 900ms steps(1) infinite;
}
.chat-loading {
display: flex;
align-items: center;
gap: 16rpx;
padding: 24rpx 0;
}
.resume-action {
padding: 24rpx 0;
display: flex;
justify-content: center;
}
.chat-input-bar {
display: flex;
align-items: flex-end;
gap: 16rpx;
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.04);
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}
.result-input-shell {
flex: 1;
background: rgba(255, 255, 255, 0.06);
border-radius: 12rpx;
padding: 8rpx 16rpx;
}
.result-chat-input {
width: 100%;
min-height: 60rpx;
padding: 12rpx 0;
color: #ffffff;
font-size: 28rpx;
}
.chat-send-btn {
padding: 16rpx 32rpx;
background: #087e8b;
border-radius: 32rpx;
color: #ffffff;
font-size: 28rpx;
}
.chat-send-btn.disabled {
opacity: 0.5;
}
@keyframes blink {
50% { opacity: 0; }
}
```
- [ ] **Step 5: 验证热加载渲染**
保存后浏览器打开 http://localhost:5180,进入心愿页输入心愿发送,确认:
- 页面进入 chat 视图(对话页)
- 用户心愿作为气泡显示
- 澄清卡片作为 AI 消息出现在对话流
- Console 无错误
- [ ] **Step 6: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat(mini-program): 统一 chat 视图,删除 clarifying/outlining/result 独立视图"
```
---
## Task 5: 改造 sendChat 按 generationPhase 切换接口
**Files:**
- Modify: `mini-program/src/pages/main/ScriptView.vue`sendChat 函数约 701 行)
- [ ] **Step 1: 改造 sendChat 根据 generationPhase 切换接口**
找到 `sendChat` 函数(约 701 行)。当前实现用旧接口 `streamScriptChat` + `createMessage`
在函数开头插入 generationPhase 判断:如果是 `'generating'`(生成流程中,理论上 novel_done 前底部输入栏不显示,但防御性处理),直接 return;如果是 `'done'`,走原有旧接口逻辑。
`sendChat` 函数替换为:
```javascript
const sendChat = async () => {
if (!chatInput.value.trim() || generating.value) return
const content = chatInput.value.trim()
chatInput.value = ''
// novel_done 前底部输入栏不显示,此处只处理 done 态的后续对话
if (generationPhase.value !== 'done') return
const messageId = currentVersionMessageId.value
const userMessageRes = await createMessage({
conversationId: conversationId.value,
userId: getCurrentUserId(),
content,
senderType: 'user',
contentType: 'chat'
})
const userMessageId = userMessageRes.data?.id
resultMessages.value.push({
id: userMessageId || createMessageId(),
role: 'user',
kind: 'text',
content,
pending: false,
time: formatMessageTime(),
submitted: false,
confirmed: false
})
keepResultAtBottom()
generating.value = true
generationStatus.value = 'streaming'
await streamScriptChat({
operationType: 'chat',
conversationId: conversationId.value,
messageId,
userMessageId,
content,
scriptId: scriptId.value,
onDelta: (delta) => {
// 往最后一条 AI 消息追加(如有)
const lastAssistant = [...resultMessages.value].reverse().find(m => m.role === 'assistant' && m.kind === 'novel')
if (lastAssistant) {
lastAssistant.content += delta
}
},
onDone: () => {
generating.value = false
generationStatus.value = 'idle'
// 重新加载持久化消息以获取最新版本
loadMessages()
},
onError: (error) => {
generating.value = false
generationStatus.value = 'idle'
uni.showToast({ title: error || '生成失败', icon: 'none' })
}
})
}
```
关键变化:
- 只在 `generationPhase === 'done'` 时处理(底部输入栏此时才显示)
- 用户消息追加到 `resultMessages`(统一对话流)
- `onDelta` 往最后一条 AI 消息追加文本
- [ ] **Step 2: 验证热加载无错误**
保存后浏览器确认无 Console 错误。
- [ ] **Step 3: 提交**
```bash
git add mini-program/src/pages/main/ScriptView.vue
git commit -m "feat(mini-program): sendChat 按 generationPhase 切换接口,消息进统一对话流"
```
---
## Task 6: 端到端浏览器验证
**Files:**
- 无代码改动,纯浏览器验证
- [ ] **Step 1: 确认 H5 服务运行**
Run: `python dev-services.py status`
Expected: mini-program 端口 5180 运行中
若未运行,启动:`cd mini-program && npm run dev:h5 -- --host 127.0.0.1 --port 5180`(后台运行)
- [ ] **Step 2: Playwright 验证完整对话流程**
使用 Playwright MCP 打开 http://localhost:5180
1. 登录(手机号 13800138000 + 验证码 123456
2. 进入心愿页,输入"今天我想当 CEO",发送
3. **验证**:进入 chat 对话页,用户心愿作为气泡显示在对话流
4. **验证**:澄清卡片作为 AI 消息出现在对话流(不在独立页面)
5. 选择选项,提交
6. **验证**:卡片变为"已回答"态,user 消息追加,下一轮澄清或大纲出现
7. 若进入大纲:**验证**大纲作为 AI 消息出现(带确认/修改按钮)
8. 确认大纲
9. **验证**:小说流式生成作为一条 AI 消息逐字出现
10. novel_done 后:**验证**底部输入栏出现,可输入修改建议
- [ ] **Step 3: 验证继续创作(DAILY_GENERATION_IN_PROGRESS**
若触发每日限制:
1. **验证**:对话流内显示失败提示 + "继续创作"按钮
2. 点击"继续创作"
3. **验证**:恢复会话,新澄清/大纲作为消息追加到对话流
- [ ] **Step 4: 检查 Console 和 Network**
- Console:无业务错误(`uni.getRecorderManager not supported` 平台限制可忽略)
- Network`/shortNovel/stream``/shortNovel/followup` 返回 SSE 事件流,无 4xx/5xx
- [ ] **Step 5: 验收报告**
输出验收报告:每个步骤通过/失败 + 截图证据 + 最终结论
---
## 验收标准
- [ ] 提交心愿后进入 chat 对话页,用户心愿作为气泡显示
- [ ] 澄清卡片作为 AI 消息出现在对话流(非独立页面)
- [ ] 卡片提交后变"已回答"态,user 消息追加
- [ ] 大纲作为 AI 消息出现,确认/修改后按钮消失
- [ ] 小说流式生成作为一条 AI 消息逐字出现
- [ ] novel_done 后底部输入栏出现,继续修改用旧接口工作
- [ ] 失败时对话流内显示"继续创作"入口
- [ ] 整个过程不切换视图,用户始终在同一对话页
File diff suppressed because it is too large Load Diff
@@ -42,7 +42,7 @@ purpose: 设计并优化情绪博物馆跨平台服务管理脚本,修复硬
services:
backend:
name: "后端服务"
dir: "backend-single"
dir: "server"
type: "java"
port: 19089
health_check_path: "/actuator/health"
@@ -33,10 +33,10 @@ The current mini program already has the core functional modules:
The backend already has script generation support under:
- `backend-single/src/main/java/com/emotion/controller/EpicScriptController.java`
- `backend-single/src/main/java/com/emotion/service/EpicScriptService.java`
- `backend-single/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `backend-single/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
- `server/src/main/java/com/emotion/controller/EpicScriptController.java`
- `server/src/main/java/com/emotion/service/EpicScriptService.java`
- `server/src/main/java/com/emotion/service/impl/EpicScriptServiceImpl.java`
- `server/src/main/java/com/emotion/dto/request/EpicScriptCreateRequest.java`
The missing product capability is a complete inspiration-mode flow. The backend currently exposes generic script creation, but it does not expose dedicated endpoints for inspiration recommendations, random inspiration, or one-sentence inspiration generation.
@@ -45,7 +45,7 @@ The missing product capability is a complete inspiration-mode flow. The backend
1. Redesign the mini-program pages to match the new prototype visual direction: dark cosmic background, purple glass panels, glowing primary actions, rounded bottom navigation, and stronger card hierarchy.
2. Keep the existing product functions largely unchanged.
3. Complete the functional flow from life events to script generation to script detail to path realization.
4. Add backend inspiration-mode APIs in `backend-single`.
4. Add backend inspiration-mode APIs in `server`.
5. Make the implementation modular enough to land in batches without leaving broken flows.
## Non-Goals
@@ -11,7 +11,7 @@ Add two complete but independently shippable capabilities:
The implementation should fit the current project shape:
- `backend-single`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `server`: Spring Boot 2.7, MyBatis Plus, MySQL, Redis.
- `mini-program`: uni-app/Vue.
- `web-admin`: Vue, Element Plus, ECharts.
- Deployment target: `101.200.208.45` with passwordless SSH.
@@ -13,7 +13,7 @@
现有实现包含:
- `web-admin/src/views/aiconfig/AiConfigList.vue`:AI 配置列表、编辑、测试、测试后保存。
- `backend-single/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `server/src/main/java/com/emotion/entity/AiConfig.java`:对应 `t_ai_config`
- `AiConfigController``AiConfigService``AiConfigServiceImpl`:配置 CRUD、启用禁用、默认配置、测试后更新。
- `AiChatServiceImpl`:包含大量 Coze 专用调用、请求组装、SSE 解析、工作流调用和日志记录逻辑。
- `docs/dify平台接口.md`:Dify 平台接口文档,当前重点使用 `/chat-messages`
@@ -382,10 +382,10 @@ async function submitEndpointStreamTest() {
| 操作 | 文件 |
|------|------|
| 修改 | `backend-single/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `backend-single/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `server/src/main/java/com/emotion/dto/request/ai/AiRuntimeRequest.java` |
| 修改 | `server/src/main/java/com/emotion/service/AiRuntimeService.java` |
| 修改 | `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java` |
| 修改 | `server/src/main/java/com/emotion/controller/AiRoutingController.java` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `web-admin/src/api/aiconfig.ts` |
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
@@ -121,7 +121,7 @@ if (value instanceof JSONObject && ((JSONObject) value).containsKey("_meta")) {
|------|------|
| 修改 | `web-admin/src/views/aiconfig/AiRoutingList.vue` |
| 修改 | `web-admin/src/types/aiconfig.ts` |
| 修改 | `backend-single/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
| 修改 | `server/src/main/java/com/emotion/service/ai/AiTemplateRenderer.java` |
## 4. 风险
@@ -8,7 +8,7 @@ purpose: 为所有 Controller 及关联 DTO 补全 OpenAPI 中文注解
## 1. 目标
`backend-single/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
`server/src/main/java/com/emotion/controller/` 下所有 39 个 Controller 及其关联的 Request/Response DTO 补全 OpenAPI 注解,使接口文档页面可通过清晰的中文描述理解每个接口的作用和参数含义。
## 2. 注解规范
@@ -31,7 +31,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**保持不变的项**
- URL 路径(如 `/emotion-museum-admin/`)— 保持兼容
- 目录名(`web/``web-admin/``backend-single/`)— 避免构建断裂
- 目录名(`web/``web-admin/``server/`)— 避免构建断裂
- 数据库表名前缀(如 `emotion_record` → 保持)— 避免 Entity/Table 映射断裂
## 第 1 步:文档层(无风险)
@@ -43,7 +43,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `README.md`
- `docs/superpowers/` 下所有 .md
- 各模块下的 `部署说明.md``技术方案.md``后端代码规范.md`
- `backend-single/部署说明.md``backend-single/后端项目结构.md`
- `server/部署说明.md``server/后端项目结构.md`
**操作**:批量 sed 替换中文 `情绪博物馆``开心星球`,英文 `Emotion Museum``Happy Planet``emotion-museum``happy-planet`
@@ -73,13 +73,13 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
**影响范围**:配置文件、部署脚本、nginx、systemd service
### 3.1 应用配置
- `backend-single/src/main/resources/application.yml``spring.application.name`
- `backend-single/src/main/resources/application-*.yml` — 同上
- `server/src/main/resources/application.yml``spring.application.name`
- `server/src/main/resources/application-*.yml` — 同上
- 日志路径、应用描述等配置中的 `emotion-museum`
### 3.2 部署脚本
- `deploy.sh``deploy.py`
- `backend-single/deploy.py``backend-single/deploy.sh`
- `server/deploy.py``server/deploy.sh`
- `web/deploy.sh``web/deploy.py`
- `web-admin/deploy.sh``web-admin/deploy.py`
- 各脚本中的路径引用、日志路径、提示文案
@@ -91,8 +91,8 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- jar 文件名、日志路径引用中的 `emotion-museum`
### 3.4 systemd Service
- `backend-single/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `backend-single/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- `server/asr-service/emotion-museum-asr.service``happy-planet-asr.service`
- `server/tts-service/emotion-museum-tts.service``happy-planet-tts.service`
- 内部路径和描述中的 `emotion-museum`
### 3.5 工具脚本
@@ -118,7 +118,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
### 4.1 Java 包名重命名
**操作**
1. 移动目录:`backend-single/src/main/java/com/emotion/``backend-single/src/main/java/com/happyplanet/`
1. 移动目录:`server/src/main/java/com/emotion/``server/src/main/java/com/happyplanet/`
2. 更新 `pom.xml` 中的包名引用
3. 用 sed 批量替换所有 `.java` 文件中 `package com.emotion``package com.happyplanet`
4. 用 sed 批量替换所有 `.java` 文件中 `import com.emotion``import com.happyplanet`
@@ -126,7 +126,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
6. 处理编译错误(可能有遗漏的引用)
**测试目录**
- `backend-single/src/test/java/com/emotion/` 同样需要迁移
- `server/src/test/java/com/emotion/` 同样需要迁移
- `.java` 文件中的 `package``import` 语句
### 4.2 SQL 文件
@@ -139,7 +139,7 @@ purpose: 全项目品牌重命名方案,从"情绪博物馆"到"开心星球"
- `USE emotion_museum``USE happy_planet`
### 4.3 其他底层引用
- `backend-single/create_api_tables.sql` 中的数据库名
- `server/create_api_tables.sql` 中的数据库名
- `.gitignore` 中的路径引用(如有)
**验证**
@@ -27,7 +27,7 @@ purpose: 设计接口管理页面的中文描述补全和测试面板表单化
### 3.1 Controller 注解补全
`backend-single/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
`server/src/main/java/com/emotion/controller/` 下所有 Controller 补全注解:
**类级别注解:**
```java
@@ -247,9 +247,9 @@ path: '/api' + testForm.value.path // 例如 /api + /auth/login = /api/auth/log
| `web-admin/src/views/endpoint/EndpointList.vue` | 操作列加"测试"按钮,新增 `showTest` 方法 |
| `web-admin/src/views/endpoint/EndpointDetailDialog.vue` | 新增 `defaultTab` prop,测试面板改造:参数表单 + JSON 预填充 + 请求头展示 |
| `web-admin/src/api/endpoint.ts` | 类型定义无需修改(`operationId` 已存在) |
| `backend-single/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `backend-single/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `backend-single/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
| `server/src/main/java/com/emotion/controller/*.java` | 补全 @Tag/@Operation/@Parameter/@Schema 注解(按 4 个批次) |
| `server/src/main/java/com/emotion/dto/request/**/*.java` | 补全 @Schema 注解(请求体参数描述来源) |
| `server/src/main/java/com/emotion/dto/response/**/*.java` | 补全 @Schema 注解(响应体参数描述来源,可选) |
## 5. 实施顺序
@@ -202,7 +202,7 @@ public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLog
### Request 对象
存放路径:`backend-single/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
存放路径:`server/src/main/java/com/emotion/dto/request/ai/AiCallLogQueryRequest.java`
```java
@Schema(description = "AI 调用日志查询请求")
@@ -14,7 +14,7 @@ purpose: 在管理后台调用日志列表中增加"调用用户"列,显示用
### 1. 后端 — 实体新增字段
**文件**: `backend-single/src/main/java/com/emotion/entity/AiCallLog.java`
**文件**: `server/src/main/java/com/emotion/entity/AiCallLog.java`
`userId` 字段下方新增:
@@ -31,7 +31,7 @@ ALTER TABLE t_ai_call_log ADD COLUMN user_name VARCHAR(100) COMMENT '用户昵
### 3. 后端 — 日志保存时写入 userName
**文件**: `backend-single/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
**文件**: `server/src/main/java/com/emotion/service/impl/AiRuntimeServiceImpl.java`
两处日志创建位置(第 68-73 行 `invokeStream` 方法、第 219-225 行 `invokeEndpointStream` 方法),在 `callLog.setUserId(...)` 之后补充:
@@ -27,7 +27,7 @@ The mini program currently supports phone number plus SMS code login only. The n
- `mini-program/src/pages/login/index.vue` contains the existing phone/SMS login UI and routes to main or onboarding based on profile presence.
- `mini-program/src/stores/app.js` already centralizes login, logout, token restore, and profile fetch behavior.
- `mini-program/src/services/auth.js` stores `access_token` and `refresh_token` from `AuthResponse`.
- `backend-single/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `server/src/main/java/com/emotion/controller/AuthController.java` exposes `/auth/login`, `/auth/refreshToken`, and token validation endpoints.
- `t_user` already has `phone`, `avatar`, `nickname`, `third_party_id`, and `third_party_type`, which can support WeChat binding without a required schema change.
- The current backend issues JWT plus Redis-backed access/refresh tokens through `AuthServiceImpl`.
- The mini program manifest already has a WeChat appid: `wxaf2eaba72d28f0e4`.
@@ -167,7 +167,7 @@ if '//' in remote_conf or '..' in remote_conf:
### 不包含(明确排除)
- 不修改其他 deploy.py 子脚本(backend-single/web/web-admin/life-script
- 不修改其他 deploy.py 子脚本(server/web/web-admin/life-script
- 不改 `ssh_command()` / `run_ssh_args()` 底层
- 不增加 `--dry-run` 模式
- 不改 nginx 配置文件本身
@@ -51,7 +51,7 @@ alter table emotion_museum.t_user_profile
### 2. 实体类扩展
文件:`backend-single/src/main/java/com/emotion/entity/UserProfile.java`
文件:`server/src/main/java/com/emotion/entity/UserProfile.java`
新增字段(与数据库列名通过 `@TableField` 映射):
@@ -67,15 +67,15 @@ alter table emotion_museum.t_user_profile
以下三个文件新增相同 5 个字段:
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `backend-single/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileCreateRequest.java`
- `server/src/main/java/com/emotion/dto/request/userprofile/UserProfileUpdateRequest.java`
- `server/src/main/java/com/emotion/dto/response/userprofile/UserProfileResponse.java`
`UserProfileResponse` 中的 `birthday` 使用 `@JsonFormat(pattern = "yyyy-MM-dd")` 注解,与现有日期字段保持一致。
### 4. Service 层
文件:`backend-single/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
文件:`server/src/main/java/com/emotion/service/impl/UserProfileServiceImpl.java`
无需改动。`BeanUtils.copyProperties``BeanUtil.copyProperties(..., CopyOptions.create().setIgnoreNullValue(true))` 会自动映射同名字段。
@@ -186,10 +186,10 @@ registrationData: {
| 文件 | 改动类型 | 说明 |
|------|----------|------|
| `sql/emotion_museum_ddl.sql` | 追加 | 5 个 ALTER TABLE 语句 |
| `backend-single/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `backend-single/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `server/.../entity/UserProfile.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileCreateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../request/.../UserProfileUpdateRequest.java` | 修改 | 新增 5 个字段 |
| `server/.../response/.../UserProfileResponse.java` | 修改 | 新增 5 个字段 |
| `mini-program/src/services/userProfile.js` | 修改 | 双向转换增加 5 个字段 |
| `mini-program/src/stores/app.js` | 修改 | `registrationData` 扩展 |
| `mini-program/src/pages/main/RecordView.vue` | 修改 | 去除写死默认值和 sampleEvents |
@@ -90,14 +90,14 @@ purpose: 修复微信小程序登录 text/plain 错误(Spring RestTemplate 默
| # | 文件 | 操作 | 行数估算 | 目的 |
|---|---|---|---|---|
| 1 | `backend-single/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `backend-single/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `backend-single/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `backend-single/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `backend-single/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `backend-single/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `backend-single/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `backend-single/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
| 1 | `server/src/main/java/com/emotion/config/RestTemplateConfig.java` | **重写** | 40 行 | 改用 `RestTemplateBuilder` + `BufferingClientHttpRequestFactory`:设置 User-Agent、超时、加 StringHttpMessageConverter、加 LoggingInterceptor、加 ErrorHandler |
| 2 | `server/src/main/java/com/emotion/service/impl/WechatMiniProgramServiceImpl.java` | **重写** | 90 行 | `code2Session` 改用 `ResponseEntity<String>` + Jackson 二次解析;捕获所有失败场景的 raw body |
| 3 | `server/src/main/resources/application.yml` | **修改** | 2 行 | 删除 `app-id``app-secret` 硬编码默认值(`${ENV_VAR:}` 空兜底) |
| 4 | `server/src/main/java/com/emotion/exception/WechatApiException.java` | **新增** | 30 行 | 业务异常类(code 用 HTTP 风格状态码 + rawBody |
| 5 | `server/src/main/java/com/emotion/interceptor/WechatApiLoggingInterceptor.java` | **新增** | 60 行 | ClientHttpRequestInterceptor:记录 method/URL/statusINFO+ 脱敏 bodyDEBUG |
| 6 | `server/src/main/java/com/emotion/config/WechatResponseErrorHandler.java` | **新增** | 30 行 | 4xx/5xx 不抛 RestClientException,重写 `hasError()` 显式声明 |
| 7 | `server/src/main/java/com/emotion/exception/GlobalExceptionHandler.java` | **修改** | 25 行 | 新增 `@ExceptionHandler(WechatApiException.class)` 返回脱敏的友好提示 |
| 8 | `server/src/main/java/com/emotion/dto/wechat/WechatCodeSessionResponse.java` | **不动** | 0 行 | **保持纯 DTO**rawBody 不放在 DTO,只在异常对象和日志中) |
**总计**:2 重写 + 3 新增 + 3 修改,约 280 行 Java + 2 行 YAML。
@@ -472,7 +472,7 @@ GlobalExceptionHandler.handleWechatApiException
| 步骤 | 命令 / 操作 | 期望 |
|---|---|---|
| 1. mvn 编译 | `mvn clean install -pl :backend-single -am -DskipTests` | 退出码 0 |
| 1. mvn 编译 | `mvn clean install -pl :server -am -DskipTests` | 退出码 0 |
| 2. 部署 | `python deploy.py backend` | 部署成功,重启 backend |
| 3. **WAF 验证**(修后 P1-7 | `tail -f /data/logs/emotion-museum/emotion-single.log \| grep "WeChatAPI"` | 看到 `[WeChatAPI] ---> GET https://api.weixin.qq.com/sns/jscode2session ...` 请求日志;`<--- 200` 响应日志 |
| 4. **UA 验证** | `grep "User-Agent" /data/logs/emotion-museum/emotion-single.log` | 看到 `Mozilla/5.0`(不再是 `Java/17.0.x` |
@@ -0,0 +1,152 @@
---
author: claude
created_at: 2026-06-15
purpose: 将人生轨迹页面的个人信息展示区域合并到我的页面,统一信息展示
---
# 个人信息组件合并设计
## 背景
当前小程序中,个人信息分散在两个页面展示:
- **RecordView(人生轨迹页面)**:顶部个人信息卡片,展示头像、昵称、星座、MBTI、职业、爱好
- **MineView(我的页面)**:展示头像、昵称、MBTI·星座·职业(一行)、觉醒深度、星历契合、菜单
两个页面存在大量重复信息,且 MineView 展示的个人字段较少。需要将 RecordView 的个人信息卡片完整合并到 MineView,取并集展示所有字段,去除重复。
## 设计目标
1. RecordView 完全移除个人信息展示区域
2. MineView 保留现有宇宙星空风格,新增「个人档案」信息区块
3. 所有可用个人信息字段取并集,不重复展示
4. 不新增后端接口,全部从现有 store 数据读取
## 变更范围
### RecordView.vue — 移除个人信息区域
**模板变更**
- 移除 `profile-card` 整个区域(包含头像、昵称、副标题、编辑按钮、meta-grid、爱好行、分隔线)
**脚本变更**
- 移除 `profile` computed`store.userProfile || store.registrationData`
- 移除 `avatar` computeddicebear 头像 URL
- 移除 `heroTags` computed(爱好数组)
- 移除 `editProfile` 方法
**样式变更**
- 移除所有 `profile-card``profile-top``avatar-wrap``avatar``avatar-edit``edit-pen``tiny-pen``profile-info``name-row``profile-name``star``profile-subtitle``edit-profile``profile-divider``meta-grid``meta-item``meta-icon``meta-label``meta-value``person-icon``job-icon``heart``hobby-row``hobby-text` 相关样式
**保留不变**
- `section-head`"人生轨迹"标题 + "导入社交数据"按钮)
- `filters`(筛选器)
- `timeline`(事件时间线)
- 空状态卡片
- `create-fab`(创建 FAB 按钮)
### MineView.vue — 新增「个人档案」区块
**位置**:在 `stats-grid`(觉醒深度 | 星历契合)下方、`menu-list` 上方。
**布局结构**
```
✦ 个人档案 ← 区块标题
┌────────┬────────┬────────┐
│ ♋ │ ◉ │ ◻ │
│ 星座 │ MBTI │ 职业 │ ← 第一行:来自 RecordView
│ 巨蟹座 │ INTJ │ 设计师 │
├────────┼────────┼────────┤
│ 📍 │ 🏢 │ 🏗 │
│ 城市 │ 行业 │ 公司 │ ← 第二行:新增字段
│ 上海 │ 互联网 │ xx公司 │
├────────┼────────┼────────┤
│ 🎂 │ ♂ │ ⏳ │
│ 生日 │ 性别 │ 年龄 │ ← 第三行:新增字段
│ 6.15 │ 男 │ 28 │
└────────┴────────┴────────┘
♡ 爱好 摄影 · 阅读 · 音乐 ← 第四行:爱好
```
**字段列表(并集)**
| 字段 | 数据来源 | 图标 | 空值显示 |
|------|---------|------|---------|
| 星座 | `profile.zodiac` | ♋ | 未设置 |
| MBTI | `profile.mbti` | ◉ | 未设置 |
| 职业 | `profile.profession` | ◻ | 未设置 |
| 城市 | `profile.city` | 📍 | 未设置 |
| 行业 | `profile.industry` | 🏢 | 未设置 |
| 公司 | `profile.company` | 🏗 | 未设置 |
| 生日 | `profile.birthday` | 🎂 | 未设置 |
| 性别 | `profile.gender` | ♂ | 未设置 |
| 年龄 | 由 birthday 计算 | ⏳ | 未设置 |
| 爱好 | `profile.hobbies` | ♡ | 未设置 |
**编辑入口**:整个区块可点击,跳转到 `/pages/onboarding/index?edit=1`
### 样式规范
**区块标题**
- 字号 40rpx,字重 800,白色
- 前缀 ✦ 金色星号(`#ffd184`),与 RecordView 的 `section-title` 风格一致
- 与上方 `stats-grid` 间距 42rpx
**字段网格**
- CSS Grid 三栏布局:`grid-template-columns: repeat(3, 1fr)`
- 每栏结构:图标(上)→ 标签(中,20rpx,`rgba(219, 204, 247, 0.54)`)→ 值(下,23rpx,白色,加粗 600)
- 栏间竖线分隔:`border-right: 1rpx solid rgba(180, 139, 255, 0.22)`,最后一栏无右边框
- 行间距 24rpx
**爱好行**
- ♡ 图标 + "爱好" 标签 + 值,水平排列
- 与网格间距 20rpx,上方有细分隔线(`1rpx solid rgba(180, 139, 255, 0.22)`
- 爱好值用 ` · ` 连接,最多显示 4 个
**整体容器**
- 不加卡片背景,透明融入 MineView 星空背景
- 与 `stats-grid``menu-list` 的间距保持一致(42rpx
### 数据逻辑
**年龄计算**
```js
const age = computed(() => {
const birthday = profile.value.birthday
if (!birthday) return null
const birthYear = Number(String(birthday).slice(0, 4))
if (!birthYear) return null
return new Date().getFullYear() - birthYear
})
```
**性别映射**
```js
const genderText = computed(() => {
const map = { male: '男', female: '女', other: '其他' }
return map[profile.value.gender] || null
})
```
**数据来源**:全部从 `store.userProfile || store.registrationData` 读取,不新增后端接口。
### MineView 现有信息处理
**保留不变**
- 头像(居中展示,保留现有宇宙光晕效果)
- 昵称(大字号居中)
- 签名行(`metaLine`:MBTI · 星座 · 职业 的组合文本)
- 统计数据(觉醒深度、星历契合)
- 菜单列表(个人档案设置、多账号切换、与开发者对话)
- 退出登录按钮
**签名行处理**`metaLine` 保留,作为昵称下方的补充签名文本,与下方「个人档案」区块的三栏网格不冲突。
## 不在范围内
- 不修改后端接口
- 不修改 `stores/app.js`
- 不修改 `profile/index.vue`(该页面只是 MineView 的包装)
- 不新增个人信息字段到 store(只展示已有字段)
@@ -0,0 +1,67 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 明确小程序编辑资料页面在昵称未填时给出 Toast 提示的优化方案
---
# 小程序编辑资料页昵称必填提示优化
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)在昵称未填写时,点击保存按钮会直接静默返回,没有任何提示。用户无法判断是按钮失效、网络问题还是字段未填,体验较差。
## 目标
- 当用户点击保存按钮时,如果昵称未填写,必须给出明确的 Toast 提示「请填写昵称」。
- 保持改动最小,不影响现有保存流程和其他字段行为。
## 当前问题定位
`saveProfile` 方法中存在以下逻辑:
```javascript
if (!form.nickname.trim() || saving.value) return
```
`form.nickname` 为空或仅包含空白字符时,条件成立,方法直接 `return`,没有触发任何提示。
## 方案
采用「保存时校验 + Toast 提示」的轻量方案。
### 具体改动
**文件**`mini-program/src/pages/onboarding/index.vue`
**修改 `saveProfile` 方法**
1. 先判断 `saving.value`,避免重复提交。
2. 再判断昵称是否为空,为空时调用 `uni.showToast({ title: '请填写昵称', icon: 'none' })` 后返回。
改动后逻辑示例:
```javascript
const saveProfile = async () => {
if (saving.value) return
if (!form.nickname.trim()) {
uni.showToast({ title: '请填写昵称', icon: 'none' })
return
}
saving.value = true
// 原有保存逻辑保持不变
}
```
### 不改动的部分
- 不修改其他字段的校验规则。
- 不调整提示方式为字段高亮、弹窗或自动聚焦。
- 不新增失焦校验或实时输入校验。
## 验证标准
- 进入编辑资料页面,清空昵称后点击保存,页面顶部弹出 Toast「请填写昵称」。
- 填写昵称后点击保存,能正常提交并显示「已保存」。
- 浏览器 Console 无报错。
@@ -0,0 +1,81 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 去除编辑资料页默认昵称和剧本列表中的 mock/兜底数据
---
# 去除编辑资料页与剧本列表中的 mock/兜底数据
## 背景
项目代码中存在两处违反「禁止 mock 与兜底规则」的问题:
1. 编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)顶部预览卡片在没有昵称时,默认显示假名「Zoey」。
2. 剧本列表页面(`mini-program/src/pages/main/ScriptLibraryView.vue`)在 `store.scripts` 为空时,使用 `fallbackScripts` 数组展示 6 条写死 demo 数据;同时多个工具函数在无数据时返回固定的默认标签、字数、日期、进度等假值。
## 目标
- 没有昵称时,编辑资料页面顶部显示「未设置昵称」。
- 剧本列表没有数据时,不显示任何假数据,直接展示空状态。
- 清理剧本列表工具函数中的兜底默认值,确保「没有就是没有」。
## 改动点
### 1. 编辑资料页昵称空状态
**文件**`mini-program/src/pages/onboarding/index.vue:22`
**修改前**
```vue
<text class="hero-name">{{ form.nickname || 'Zoey' }}</text>
```
**修改后**
```vue
<text class="hero-name">{{ form.nickname || '未设置昵称' }}</text>
```
### 2. 剧本列表删除 mock 数据与兜底值
**文件**`mini-program/src/pages/main/ScriptLibraryView.vue`
#### 2.1 删除 `fallbackScripts` 数组
删除第 178249 行的 `fallbackScripts` 常量及其 6 条 demo 数据。
#### 2.2 修改 `scripts` computed
**修改前**
```javascript
const scripts = computed(() => {
const list = store.scripts || []
return list.length ? list : fallbackScripts
})
```
**修改后**
```javascript
const scripts = computed(() => store.scripts || [])
```
#### 2.3 清理工具函数默认值
| 函数 | 修改前 | 修改后 |
|---|---|---|
| `getTags` | `return [script.style || '逆袭成长', '都市', '事业', '热血']` | `return []` |
| `getWordCount` | `if (!count) return '3.1万字'` | 删除该分支,直接走后续逻辑(返回 `'0字'` |
| `getDateText` | 各分支使用 `|| '2025.05.10'``|| '2025.05.08'``|| '今天 21:30'` | 去掉兜底日期,返回空字符串 `''` |
| `getProgress` | `Number(script.progress || 28)` | `Number(script.progress || 0)` |
| `getChapterCount` | `script.chapterCount || script.chapters || Math.max(1, Math.round((script.wordCount || 30000) / 4500))` | `script.chapterCount || script.chapters || 0` |
## 验证标准
1. 进入编辑资料页面,昵称输入框为空时,顶部预览卡片显示「未设置昵称」。
2. 剧本列表为空时,页面显示「还没有人生剧本」空状态,不展示任何 demo 剧本。
3. 剧本列表中的真实剧本数据展示不受影响(有数据时仍正常显示标题、标签、字数、日期、进度等)。
4. H5 模式下浏览器 Console 无新增报错。
5. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,133 @@
---
author: Peanut
created_at: 2026-06-23
purpose: 固定 ScriptView.vue 结果页顶部按钮,调整底部输入框边距,语音按钮改为麦克风图标
---
# ScriptView 对话结果页 UI 调整
## 背景
`ScriptView.vue`(剧本生成/对话结果页)中,结果页顶部左侧的「历史」按钮和右上角的「×」关闭按钮会随着页面内容滚动而移动,影响操作。底部输入框区域左右贴近屏幕边缘,视觉拥挤;语音按钮使用「语音」文字,不够简洁。
## 目标
1. 结果页顶部的历史按钮和关闭按钮在页面滚动时保持固定,不随内容滚动。
2. 底部输入框区域左右增加 24rpx 边距,输入框宽度适当压缩。
3. 语音按钮文字改为麦克风图标,颜色与当前主题一致。
4. 发送按钮保持当前大小和样式。
## 改动点
### 1. 顶部按钮固定
**文件**`mini-program/src/pages/main/ScriptView.vue`
将结果页顶部包裹层改为固定定位,并为下方内容增加顶部 padding 避免遮挡。
**结构示例**
```vue
<view class="result-header">
<view class="history-button" @click="openScriptLibrary">...</view>
<button class="page-close-btn" @click="closeResult">×</button>
</view>
```
**样式示例**
```css
.result-header {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
height: 100rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
background: rgba(15, 7, 26, 0.85);
backdrop-filter: blur(18rpx);
-webkit-backdrop-filter: blur(18rpx);
}
```
同时给结果页内容区域增加顶部 padding(如 `padding-top: 100rpx` 或更大,以避开固定顶部栏和安全区)。
### 2. 底部输入框边距与压缩
**文件**`mini-program/src/pages/main/ScriptView.vue`
`.result-chat-bar` 增加左右内边距:
```css
.result-chat-bar {
/* 其他样式保持不变 */
padding: 0 24rpx;
}
```
输入框 `.result-input-shell` 保持 `flex: 1`,通过整体边距自然压缩可用宽度。如需进一步压缩,可增加左右 `margin: 0 10rpx`
语音按钮(86rpx × 76rpx)和发送按钮(92rpx × 76rpx)保持当前尺寸不变。
### 3. 语音按钮改为麦克风图标
**文件**`mini-program/src/pages/main/ScriptView.vue`
**修改前**
```vue
<view class="chat-voice-btn" ...>
<text>语音</text>
</view>
```
**修改后**
```vue
<view class="chat-voice-btn" ...>
<text class="voice-icon">🎤</text>
</view>
```
**样式**
```css
.chat-voice-btn {
/* 保持原有尺寸、背景、边框 */
width: 86rpx;
height: 76rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 26rpx;
background: rgba(88, 28, 135, 0.32);
border: 1rpx solid rgba(192, 132, 252, 0.3);
}
.voice-icon {
color: #e8ccff;
font-size: 32rpx;
}
.chat-voice-btn.pressing .voice-icon,
.chat-voice-btn.recognizing .voice-icon {
color: #fff;
}
```
如项目规范不允许使用 emoji,可改用 CSS 绘制的麦克风图标。
## 验证标准
1. 进入 ScriptView.vue 的剧本对话结果页,上下滚动内容,顶部历史按钮和关闭按钮始终保持在固定位置。
2. 底部输入框区域左右与屏幕边缘有 24rpx 边距。
3. 语音按钮显示为麦克风图标,默认颜色 `#e8ccff`,按下/识别时变为白色。
4. 发送按钮大小不变。
5. 输入框可以正常输入和发送消息。
6. 浏览器 Console 无新增报错。
7. 运行 `npm run build:h5` 编译通过。
@@ -0,0 +1,240 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"性格标签"与"兴趣爱好"两个模块的扩展设计:支持用户新增自定义标签、统一管理标签库、删除标签,并保证编辑页标签容器自适应撑开展示所有标签。
---
# 小程序编辑资料页:性格标签 / 兴趣爱好 标签库扩展设计
## 背景
当前小程序的编辑资料页面(`mini-program/src/pages/onboarding/index.vue`)已具备性格标签和兴趣爱好的基础选择功能:
- 性格标签预设 11 个(理性、感性、乐观、独立、有创造力、坚韧、细腻、好奇、内敛、冒险、自由)。
- 兴趣爱好预设 10 个(阅读、旅行、音乐、写作、摄影、电影、运动、绘画、咖啡、游戏)。
- 兴趣爱好模块支持通过弹窗输入自定义标签。
- 性格标签模块 UI 上已有"+ 添加标签"占位,但未绑定事件。
- 两个模块均不支持删除标签,也没有统一的管理入口。
- 后端 `UserProfile` 实体只保存"用户已选中的标签"两个 JSON 字段(`personality_tags` / `hobbies`),不区分预设与自定义,也没有完整标签库的概念。
用户新增需求:性格标签和兴趣爱好两个模块均支持**用户自由新增自定义标签、保存、回显、删除**;编辑资料页的标签容器要**自适应撑开**,保证所有标签都能展示;**新增的标签展示在最前面**。
## 目标
1. 性格标签和兴趣爱好两个模块支持用户新增自定义标签、保存、回显、删除。
2. 新增的自定义标签展示在列表最前面。
3. 编辑资料页标签容器自适应撑开,不限行数,展示所有标签。
4. 提供独立的"管理标签"页面,支持统一删除(含预设与自定义)。
5. 老用户数据平滑迁移,不破坏现有已选标签。
6. 输入校验完整,避免重复、空值、超长。
## 非目标
- 不引入标签的分类、排序、分组等高级管理能力。
- 不做标签的全局共享、推荐、热门度等运营功能。
- 不修改后端存储架构(继续用 JSON 字段,不拆子表)。
- 不在编辑资料页做长按手势删除,统一收敛到管理页。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 新增标签输入方式 | `uni.showModal` 弹窗输入 | 与现有"自定义兴趣"交互保持一致,改动成本最低 |
| 删除入口 | 独立的"管理标签"页面 | 统一删除操作入口,避免编辑页交互过载;预设与自定义标签都能被删除 |
| 删除后的可见性 | 删除后从当前用户账号彻底移除 | 用户希望不再看到的标签就不会再出现 |
| 数据存储 | `UserProfile` 新增 `personality_tag_library` / `hobby_library` 两个 JSON 字段 | 后端需区分"用户已选中"与"用户可见的完整标签库",原有字段语义保留不变 |
| 预设 vs 自定义标签 | 前端维护一份内置预设数组;库里存的是用户专属合并后的字符串数组 | 预设标签库存在代码中便于维护,用户库字段只关心"当前账号可见的全部标签" |
| 编辑页标签顺序 | 新增的自定义标签 → 已选中的其他标签 → 其余未选中标签 | 严格保证"新增的标签展示在最前面",其他已选中标签紧随其后 |
## 后端改动
### 实体与数据库
`server` 模块:
- `UserProfile` 实体新增两个字段:
| 字段名 | Java 类型 | 数据库列 | 类型 | 说明 |
|---|---|---|---|---|
| `personalityTagLibrary` | `String` | `personality_tag_library` | `TEXT` | 用户的完整性格标签库,JSON 字符串(字符串数组) |
| `hobbyLibrary` | `String` | `hobby_library` | `TEXT` | 用户的完整兴趣爱好库,JSON 字符串(字符串数组) |
- 原 `personalityTags` / `hobbies` 字段保持不变,继续表示"用户已选中的标签"。
- 新增数据库迁移 SQL
```sql
ALTER TABLE user_profile
ADD COLUMN personality_tag_library TEXT DEFAULT NULL COMMENT '用户完整性格标签库(JSON数组)',
ADD COLUMN hobby_library TEXT DEFAULT NULL COMMENT '用户完整兴趣爱好库(JSON数组)';
```
### 接口变更
`UserProfileController` / `UserProfileService` 无需新增接口,复用现有:
- `PUT /user-profile/update`:入参 DTO 增加 `personalityTagLibrary` / `hobbyLibrary`(均为 `String`),直接持久化。
- `GET /user-profile/me`:返回体增加上述两个字段。
- `POST /user-profile/create`:同上。
### 数据兼容性
- 老用户(`personalityTagLibrary` 为空或 `null`)查询接口返回 `null`,由前端做兜底初始化。
- 后端不做兜底填充,严格遵守"没有就是没有"的禁止 mock 规则。
## 前端改动
### 数据层(`stores/app.js``services/userProfile.js`
- 请求返回的 `userProfile` 新增 `personalityTagLibrary` / `hobbyLibrary` 字段解析(JSON.parse)。
- `saveUserProfile` 提交时把两个库字段 `JSON.stringify` 后一并提交。
### 编辑资料页(`mini-program/src/pages/onboarding/index.vue`
#### 标签区结构(性格标签与兴趣爱好同构)
```
┌──────────────────────────────────────┐
│ 😊 性格标签 管理 > │ ← 标题行,右侧"管理"按钮跳转管理页
│ (最多选择5个) │
│ ┌────┐┌────┐┌────┐┌────┐ │
│ │ 理性 ││ 感性 ││ 乐观 ││ 独立 │... │ ← 标签网格,flex-wrap 自适应
│ └────┘└────┘└────┘└────┘ │
│ ┌───────┐ │
│ │ + 添加 │ │ ← 末尾占位,点击打开弹窗
│ └───────┘ │
└──────────────────────────────────────┘
```
- 容器使用 `display: flex; flex-wrap: wrap;`**高度随内容自适应撑开**。
- 标签顺序规则(严格保证"新增的标签展示在最前面"):
1. **新增的自定义标签**排在最前面(按创建时间倒序)。
2. **已选中的其他标签**(含预设与早期自定义)紧随其后。
3. **其余未选中的预设标签**排在最后。
- 样式沿用现有 `.tag-choice` / `.tag-choice.active`
- 点击标签:沿用 `toggleList(list, tag, 5)` 逻辑,超限 toast 提示。
#### 新增标签弹窗
- 点击" 添加标签"触发 `uni.showModal``editable: true`,占位符"请输入标签"。
- 校验规则:
- 非空、去除首尾空格。
- 长度 ≤ 8 个字符。
- 不能和当前库中任何标签完全相等(字符串完全一致比对)。
- 校验失败通过 `uni.showToast` 提示原因,不静默忽略。
- 校验通过后:
1. 新标签**插入到对应库数组的最前面**。
2. 若已选未满 5 个,自动加入已选数组;满则 toast 提示"已达上限,请先进入管理页删除其他标签",仍把新标签加入库,但不加入已选。
#### 保存流程
- 点击"保存"按钮时,把以下数据一起提交:
- `personalityTags`(已选中数组)
- `personalityTagLibrary`(完整库数组)
- `hobbies`(已选中数组)
- `hobbyLibrary`(完整库数组)
- 保存失败:toast 提示后端返回的真实错误信息,页面状态保留不变。
- 保存成功:toast 提示"已保存"350ms 后 `uni.navigateBack()`
### 管理页(新增独立页面)
路径:`mini-program/src/pages/onboarding/tag-manage.vue`
通过 `pages.json` 注册路由:`/pages/onboarding/tag-manage`
#### 入口与参数
- 编辑资料页的"管理"按钮跳转:`/pages/onboarding/tag-manage?type=personality``?type=hobby`
- 页面标题:根据 type 显示"管理性格标签"或"管理兴趣爱好"。
#### 页面结构
- 列表渲染当前库中全部标签(含剩余预设 + 自定义)。
- 每行结构:
```
┌────────────────────────────────────┐
│ 理性 × │
├────────────────────────────────────┤
│ 感性 × │
├────────────────────────────────────┤
│ ... │
└────────────────────────────────────┘
```
- **左滑删除**:使用 UniApp 的 `uni-swipe-action` 组件或自定义实现,向右滑动显示"删除"按钮。
- **点击 × 按钮**:同样触发删除。
- 删除前 `uni.showModal` 二次确认,提示"确定删除该标签吗?删除后不可恢复"。
- 删除后:
- 从库数组中移除该标签。
- 若该标签同时在已选数组中,一并移除。
- 页面列表立即刷新。
- 删除接口调用:删除动作本身不单独调用后端,仅在用户返回编辑资料页点击"保存"时统一提交。
#### 返回与数据同步
- 管理页的库数据存储在页面局部响应式变量中。
- 返回编辑资料页时,通过 `onUnload` 生命周期或全局 store 的 `updateRegistration` 把最新库数据回传给编辑资料页。
- 推荐方案:使用 `uni.$emit('tag-library-updated', { type, library })` 事件,编辑资料页在 `onShow` 中监听并合并。
## 数据初始化兼容方案
编辑资料页在 `onLoad` / `onShow` 时执行:
```
if (后端返回的 personalityTagLibrary 为空) {
库 = [...内置预设数组]
if (后端返回的 personalityTags 非空) {
// 把已选中的标签也合入库(老用户首次编辑时保留)
库 = 去重合并(库, 已选数组)
}
}
```
兴趣爱好同理。此逻辑保证老用户数据不丢失,新用户直接拿预设库。
## 错误处理
- 接口请求失败:按现有 `request.js` 约定,`try/catch` 返回 `{ success: false, error: 真实错误 }`,页面 toast 提示真实错误信息。**禁止本地 mock 成功响应、禁止兜底默认值**。
- 管理页删除:若保存时接口失败,本地已删除的标签状态**回滚**(恢复删除前的库内容),toast 提示失败原因。
- 网络超时:按 `request.js` 的现有超时策略处理。
## 文件清单
### 新增文件
- `mini-program/src/pages/onboarding/tag-manage.vue`:管理标签独立页面。
### 修改文件
**后端(`server/src/main/java/com/emotion/`):**
- `entity/UserProfile.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `controller/UserProfileController.java`:无需新增接口,入参 DTO 通过 `UserProfileUpdateRequest` / `UserProfileCreateRequest` 已支持新字段透传。
- `dto/request/userprofile/UserProfileUpdateRequest.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `dto/request/userprofile/UserProfileCreateRequest.java`:同上。
- `dto/response/userprofile/UserProfileResponse.java`:新增 `personalityTagLibrary``hobbyLibrary` 两个 `String` 字段。
- `service/UserProfileService.java``service/impl/UserProfileServiceImpl.java`:无业务逻辑改动,字段通过 MyBatis-Plus 自动映射持久化。
- 数据库迁移脚本:`server/src/main/resources/db/migration/2026-06-24-user-profile-tag-library.sql`,遵循现有 `YYYY-MM-DD-描述.sql` 命名约定。
**前端(`mini-program/`):**
- `src/pages/onboarding/index.vue`:重构标签区,新增管理入口、弹窗新增逻辑、自适应样式。
- `src/services/userProfile.js`:请求体与响应解析增加两个库字段。
- `src/stores/app.js``saveUserProfile` 提交时携带库字段。
- `src/pages.json`:注册 `tag-manage` 路由。
**不修改的文件:**
- `src/pages/main/MineView.vue`:展示区只读"已选中的标签"(沿用 `hobbies` 字段),不需要感知完整库,**无需修改**。
## 验收标准
1. 进入编辑资料页,性格标签、兴趣爱好两个区域的标签容器高度随内容自适应撑开,无截断。
2. 点击" 添加标签"或"+ 自定义兴趣",弹窗输入后,新标签出现在列表最前面。
3. 重复输入同名标签,toast 提示"标签已存在"。
4. 空输入、超长输入(>8 字符)分别 toast 提示对应原因。
5. 点击"管理"按钮,跳转到管理页,列表展示全部标签(预设 + 自定义)。
6. 管理页左滑或点 × 删除标签,二次确认后标签从列表消失。
7. 删除已在"已选"中的标签,返回编辑资料页后已选列表同步移除。
8. 保存后重新进入编辑资料页,新增的标签仍展示在最前面。
9. 老用户(无新字段)首次进入编辑资料页,已选标签保留,预设标签可见。
10. 接口失败时,页面保留原状态并 toast 真实错误,不静默成功。
@@ -0,0 +1,289 @@
---
author: Peanut
created_at: 2026-06-24
purpose: 明确小程序编辑资料页"添加"按钮文字简化、字数限制收紧到4字、标签不换行、弹窗替换为深色太空主题自定义组件的改动设计。
---
# 小程序添加标签按钮简化与自定义弹窗设计
## 背景
当前小程序编辑资料页(`mini-program/src/pages/onboarding/index.vue`)和管理标签页(`mini-program/src/pages/onboarding/tag-manage.vue`)的"添加"按钮和弹窗存在以下问题:
- 编辑页性格标签的添加按钮文字是"+ 添加标签",兴趣爱好是"+ 添加兴趣",文字偏长,与紧凑的标签网格不协调。
- 添加标签的字数上限是 8 个字符,偏宽松,容易生成过长的标签破坏网格布局。
- 标签 `.tag-choice` / `.tag-text` 没有设置 `white-space: nowrap`,长标签会自动换行撑高单元格。
- 添加和删除确认都使用 `uni.showModal`,在 H5 模式下是 uni-app 默认的白底蓝按钮样式,与小程序整体的深色太空主题(深紫底、毛玻璃、紫色渐变、金色强调)严重不一致。
## 目标
1. 编辑页两个添加按钮文字统一简化为"+ 添加"。
2. 添加标签字数限制收紧到 4 个字。
3. 标签文字过长时不换行,用省略号截断。
4. 添加和删除确认弹窗替换为自定义弹窗组件,样式与深色太空主题保持一致。
## 非目标
- 不改标签的选择/取消选中逻辑、最多 5 个的限制。
- 不改后端接口或数据结构。
- 不改管理页的左滑删除手势。
- 不引入第三方 UI 组件库。
## 关键设计决策
| 决策点 | 选择 | 理由 |
|---|---|---|
| 添加按钮文字 | " 添加"(去掉"标签"/"兴趣"后缀) | 用户明确要求只保留"添加"两字 |
| 弹窗 title | 保持 `添加性格标签` / `添加兴趣爱好` | 按钮简、弹窗清,分开处理 |
| 字数限制 | 4 个字 | 用户明确要求 |
| toast 文案 | "内容不能超过4个字" | 用户指定 |
| 不换行实现 | `white-space: nowrap` + `overflow: hidden` + `text-overflow: ellipsis` | 标准三件套,标签固定高度不撑高 |
| 弹窗实现 | 新建自定义组件 `TagDialog.vue`,两种模式(input / confirm | 替换 `uni.showModal`,样式可控、深色主题统一 |
## 新增组件
### `mini-program/src/components/TagDialog.vue`
**职责**:统一的弹窗组件,支持输入模式和确认模式,样式与深色太空主题一致。
**Props**
| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
| `visible` | `Boolean` | `false` | 是否显示弹窗 |
| `mode` | `String` | `'input'` | `'input'``'confirm'` |
| `title` | `String` | `''` | 弹窗标题 |
| `content` | `String` | `''` | confirm 模式的提示文案 |
| `placeholder` | `String` | `''` | input 模式的输入框占位符 |
| `modelValue` | `String` | `''` | input 模式的输入值(支持 v-model) |
**Emits**
| 事件 | 参数 | 说明 |
|---|---|---|
| `confirm` | input 模式:当前输入值;confirm 模式:无 | 点击确定按钮 |
| `cancel` | 无 | 点击取消按钮或遮罩层 |
| `update:modelValue` | 新值 | 输入框内容变化 |
| `update:visible` | `false` | 关闭弹窗 |
**模板结构**
```
┌────────────────────────────────┐
│ 遮罩层(半透明深色) │
│ ┌──────────────────────────┐ │
│ │ ✦ 添加性格标签 │ │ ← 标题 + 金色 ✦
│ │ ┌────────────────────┐ │ │
│ │ │ 输入框(深色底紫边)│ │ │ ← input 模式才有
│ │ └────────────────────┘ │ │
│ │ 确定删除「xxx」吗?... │ │ ← confirm 模式才有
│ │ │ │
│ │ [ 取消 ] [ 确定 ] │ │ ← 按钮
│ └──────────────────────────┘ │
└────────────────────────────────┘
```
**样式**(与小程序深色太空主题一致):
- 遮罩层:`rgba(3, 2, 13, 0.72)` 全屏覆盖
- 弹窗卡片:`glass-card` 毛玻璃 + `rgba(155, 110, 255, 0.14)` 紫色边框 + `22rpx` 圆角
- 标题:白色 `#fff` 加粗,配金色 `✦``#ffd58c`
- 输入框:深色底 `rgba(10, 12, 40, 0.66)` + 紫色边框 `rgba(151, 111, 255, 0.22)`
- 取消按钮:透明底 + 紫色边框
- 确定按钮:紫色渐变 `linear-gradient(180deg, #a855ff, #6b29ce)`
- 所有尺寸用 `rpx`
## 编辑资料页改动(`mini-program/src/pages/onboarding/index.vue`
### 模板
- 性格标签 panel 末尾占位:` 添加标签`` 添加`
- 兴趣爱好 panel 末尾占位:` 添加兴趣`` 添加`
- 在 `<scroll-view>` 之外、`</view>` 之前挂载 `<TagDialog>` 组件:
```html
<TagDialog
v-model="dialogInput"
v-model:visible="dialogVisible"
:mode="dialogMode"
:title="dialogTitle"
:placeholder="dialogPlaceholder"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogMode = ref('input') // 'input' 或 'confirm'
const dialogTitle = ref('')
const dialogPlaceholder = ref('')
const dialogContent = ref('')
const dialogInput = ref('')
const dialogAction = ref(null) // 记录当前弹窗对应的动作:{ kind: 'add'|'delete', type: 'personality'|'hobby', tag?: string }
```
- 重构 `addCustomTag`:改为打开弹窗(input 模式),不再调 `uni.showModal`
```javascript
const addCustomTag = (type) => {
const isPersonality = type === 'personality'
dialogMode.value = 'input'
dialogTitle.value = isPersonality ? '添加性格标签' : '添加兴趣爱好'
dialogPlaceholder.value = '请输入标签(最多4个字)'
dialogInput.value = ''
dialogAction.value = { kind: 'add', type }
dialogVisible.value = true
}
```
- 新增 `onDialogConfirm`:处理弹窗确认(add / delete 两种动作):
```javascript
const onDialogConfirm = (value) => {
const action = dialogAction.value
if (!action) return
if (action.kind === 'add') {
handleAddTag(action.type, value)
} else if (action.kind === 'delete') {
handleDeleteTag(action.type, action.tag)
}
dialogVisible.value = false
}
const onDialogCancel = () => {
dialogAction.value = null
dialogVisible.value = false
}
```
- 抽出 `handleAddTag`(原 `addCustomTag` 的 success 回调逻辑,字数限制改为 4):
```javascript
const handleAddTag = (type, rawValue) => {
const value = String(rawValue || '').trim()
const isPersonality = type === 'personality'
const library = isPersonality ? form.personalityTagLibrary : form.hobbyLibrary
const selected = isPersonality ? form.personalityTags : form.hobbies
if (!value) {
uni.showToast({ title: '请输入标签内容', icon: 'none' })
return
}
if (value.length > 4) {
uni.showToast({ title: '内容不能超过4个字', icon: 'none' })
return
}
if (library.includes(value)) {
uni.showToast({ title: '标签已存在', icon: 'none' })
return
}
library.unshift(value)
if (selected.length >= 5) {
uni.showToast({ title: '已达上限,仅加入标签库', icon: 'none' })
return
}
selected.push(value)
}
```
### 样式
`.tag-choice` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 管理页改动(`mini-program/src/pages/onboarding/tag-manage.vue`
### 模板
挂载 `<TagDialog>` 组件(confirm 模式):
```html
<TagDialog
v-model:visible="dialogVisible"
:mode="'confirm'"
:title="dialogTitle"
:content="dialogContent"
@confirm="onDialogConfirm"
@cancel="onDialogCancel"
/>
```
### script setup
- 引入组件:`import TagDialog from '../../components/TagDialog.vue'`
- 新增响应式状态:
```javascript
const dialogVisible = ref(false)
const dialogTitle = ref('确认删除')
const dialogContent = ref('')
const pendingTag = ref(null)
```
- 重构 `confirmDelete`:改为打开弹窗(confirm 模式):
```javascript
const confirmDelete = (tag) => {
pendingTag.value = tag
dialogContent.value = `确定删除「${tag}」吗?删除后不可恢复`
dialogVisible.value = true
}
const onDialogConfirm = () => {
if (pendingTag.value) {
deleteTag(pendingTag.value)
pendingTag.value = null
}
dialogVisible.value = false
}
const onDialogCancel = () => {
pendingTag.value = null
dialogVisible.value = false
}
```
- `deleteTag` 函数保持不变(已经在 Task 6 实现)。
### 样式
`.tag-text` 增加:
```css
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
```
## 文件清单
### 新增文件
- `mini-program/src/components/TagDialog.vue`:自定义弹窗组件。
### 修改文件
- `mini-program/src/pages/onboarding/index.vue`:按钮文字简化、字数限制 4、不换行样式、引入 TagDialog、重构 addCustomTag。
- `mini-program/src/pages/onboarding/tag-manage.vue`:引入 TagDialog、重构 confirmDelete、不换行样式。
## 验收标准
1. 编辑页性格标签和兴趣爱好的添加按钮只显示"+ 添加"。
2. 点击"+ 添加"弹出深色太空主题弹窗(毛玻璃卡片、紫色渐变、金色 ✦),不再是白底蓝按钮。
3. 输入 5 个字时 toast 提示"内容不能超过4个字"。
4. 输入 4 个字以内的新标签,确认后标签出现在列表最前面。
5. 标签文字过长时(如 4 字标签在窄屏),单元格内省略号截断,不换行撑高。
6. 管理页点 × 或左滑删除时,弹出深色主题确认弹窗,文案"确定删除「xxx」吗?删除后不可恢复"。
7. 弹窗的取消按钮和遮罩层点击都能关闭弹窗,不触发任何动作。
8. 弹窗在 H5 模式下样式与小程序深色太空主题一致。
@@ -0,0 +1,125 @@
---
author: claude
created_at: 2026-06-27
purpose: 统一小程序实现心愿页面中所有 AI 消息卡片的功能按钮,并修复 story-card 展开失效 bug
---
# AI 消息卡片功能按钮统一设计
## 背景
在小程序「实现心愿」页面(`mini-program/src/pages/main/ScriptView.vue`)中:
1. **第一个 AI 回复卡片**`story-card`)拥有完整的功能按钮:展开/收起、复制、换个方向、不像我、继续生成、播放。
2. **后续 AI 消息**`result-chat-list`)只有基础的展开/收起,缺少复制、换个方向、不像我、继续生成、播放。
3. **story-card 展开失效**:当用户触发过重新生成(`resultHasScriptReplies``true`)后,`<template v-if="!resultHasScriptReplies">` 会隐藏整个故事正文和底部展开按钮,导致点击展开无效果。
## 目标
- 每条 AI 回复消息卡片都拥有完整的功能按钮(展开/收起、复制、换个方向、不像我、继续生成、播放)
- 修复 story-card 展开失效的 bug
- 所有按钮功能正常可用
## 方案选择
采用 **方案 A:在现有单文件组件内修复 + 扩展**
- 不提取独立组件,保持现有文件结构
- 改动最小、风险低、不引入新文件
- 后续如有更复杂的差异化需求再升级
## 详细设计
### 1. 修复 story-card 展开失效
**根因**`ScriptView.vue` 第 192 行的条件包裹
```vue
<template v-if="!resultHasScriptReplies">
<scroll-view v-if="storyCollapsed" ...>
<text class="story-body">{{ displayedResultContent }}</text>
</scroll-view>
<text v-else class="story-body">{{ displayedResultContent }}</text>
<view class="collapse-row" @click="toggleStoryCollapse">...</view>
</template>
```
`resultHasScriptReplies``true`(有 `kind: 'script'` 的消息)时,整个故事正文和底部展开按钮被隐藏。
**修复**:去掉 `<template v-if="!resultHasScriptReplies">` 条件包裹,让故事正文始终由 `storyCollapsed` 控制展开/收起。
**副作用**story-card 始终显示完整故事正文,与 result-chat-list 中的 script 消息内容重复。这是预期行为——两个卡片独立操作各自内容。
### 2. 给每条 assistant 消息加功能按钮组
**布局**:在每条 assistant 消息的内容 + 展开/收起按钮之后,追加一行网格按钮组,2 列布局(与 story-card 的 `.result-actions` 一致)。
**按钮清单**(按顺序):
| 按钮 | 作用 | 实现方式 |
|---|---|---|
| 复制 | 复制该消息正文到剪贴板 | `uni.setClipboardData` |
| 换个方向 | 进入「换方向」修订确认 | 复用现有 `changeDirection()` |
| 不像我 | 进入「不像我」修订确认 | 复用现有 `notLikeMe()` |
| 继续生成 | 收起故事 + 聚焦输入框 | 复用现有 `continueInChat()` |
| 播放 | 播放 TTS 音频 | 复用 `ttsPlayer.playSource`,传 `currentResult.id` |
**按钮条件**
- 展开/收起、复制:所有 assistant 消息都有
- 换个方向、不像我、继续生成、播放:所有 assistant 消息都有(本质上是全局操作,作用在 `currentResult` 上)
- 正在 `pending` 的 assistant 消息不显示按钮组(保持现有逻辑,`isAssistantMessage` 已排除 `pending`
### 3. 新增/修改的事件处理器
1. **`copyMessageContent(message)`**(新增):
- 复制 `message.content` 到剪贴板
- 空内容时 `showToast` 提示「暂无可复制内容」
- 埋点 `script_message_copy_click`
2. **`playMessageTts(message)`**(新增):
- 调用 `ttsPlayer.playSource(currentResult.value?.id || '')`
- 所有消息都播放同一个故事的 TTS(TTS 基于 `currentResult` 生成)
- 埋点 `script_message_tts_click`
3. **`toggleMessageCollapse(message)`**(修改):
- 补充埋点 `script_message_collapse_toggle`(与 `toggleStoryCollapse` 对齐)
### 4. 样式
为消息卡片内的按钮组新建 `.message-actions` 样式,复用 `.result-actions``.action-btn` 的视觉风格:
```css
.message-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14rpx;
margin-top: 16rpx;
}
```
按钮直接复用 `.action-btn` 类名,确保视觉与主卡片一致。展开/收起按钮(`.message-toggle`)保留现有金色调样式,与功能按钮组做视觉区分。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 空内容的 assistant 消息 | 复制按钮 `showToast` 提示「暂无可复制内容」;其他按钮正常可用 |
| pending 中的消息 | 不显示按钮组(现有逻辑) |
| user 消息 | 不显示按钮组 |
| 无 `currentResult` 时的播放 | 按钮仍可点击,`playSource` 会走内部兜底(传空 id |
## 修改范围
仅修改一个文件:`mini-program/src/pages/main/ScriptView.vue`
### 模板修改
1. 去掉第 192 行 `<template v-if="!resultHasScriptReplies">` 条件包裹
2. 在 `result-chat-list` 的 assistant 消息中添加功能按钮组
### 脚本修改
1. 新增 `copyMessageContent(message)` 方法
2. 新增 `playMessageTts(message)` 方法
3. 修改 `toggleMessageCollapse(message)` 补充埋点
### 样式修改
1. 新增 `.message-actions` 样式
@@ -0,0 +1,74 @@
---
author: peanu
created_at: 2026-06-27
purpose: 将 backend-single 目录重命名为 server 的设计方案
---
# 设计:backend-single 重命名为 server
## 背景
项目中的后端服务目录当前名为 `backend-single`,该名称是早期"单体服务"阶段的遗留命名。随着项目演进,这个名称已不再准确反映实际情况,且在日常开发中较长不便。将其重命名为更简洁的 `server` 可提升可读性和一致性。
## 决策
- **目录重命名**`backend-single/``server/`,使用 `git mv` 保留 Git 历史
- **Maven artifactId**`backend-single``server`JAR 文件随之变为 `server-1.0.0.jar`
- **历史文档**`docs/superpowers/` 下的历史设计和计划文档也全部更新,保持一致性
## 变更范围
### 第一层:目录和构建
| 操作 | 目标 |
|------|------|
| `git mv backend-single server` | 目录重命名 |
| `server/pom.xml` artifactId | `backend-single``server` |
| `server/pom.xml` name | `backend-single``server` |
| `server/deploy.py` JAR_NAME | `backend-single-1.0.0.jar``server-1.0.0.jar` |
| `server/deploy.sh` JAR_NAME | 同上 |
### 第二层:部署脚本
| 文件 | 变更内容 |
|------|---------|
| 根目录 `deploy.py` | 3 处 `backend-single``server`(注释、路径拼接、cwd |
| 根目录 `deploy.sh` | 4 处 `backend-single``server`(注释、存在性检查、cd |
| `.gitignore` | `backend-single/backend.err``server/backend.err` |
### 第三层:项目文档
| 文件 | 说明 |
|------|------|
| `CLAUDE.md` | 项目结构树(1 处)+ 后端命令段落(3 处) |
| `AGENTS.md` | 同 CLAUDE.md 的结构和命令段落 |
| `README.md` | 项目结构树(1 处) |
| `server/部署说明.md` | 自引用路径 |
| `快速部署参考.md` | 4 处部署路径引用 |
| `一键部署说明.md` | 5 处部署路径引用 |
### 第四层:前端和其他代码
| 文件 | 说明 |
|------|------|
| `web/src/types/auth.ts` | 第 5 行注释引用 |
| `web-admin/AI_CONFIG_TEST_SAVE_FEATURE.md` | 4 处后端文件路径引用 |
### 第五层:历史文档批量替换
约 20 个 `docs/superpowers/specs/``docs/superpowers/plans/` 下的 `.md` 文件,将其中所有 `backend-single` 替换为 `server`
## 不在范围内
- **Java 包名**`com.emotion` 保持不变,不在本次重命名范围内
- **数据库名/表名**:不涉及数据库变更
- **`dev-services.py`**:该脚本自动发现 Spring Boot 服务,不直接引用 `backend-single` 字符串,无需修改
- **`deploy-server.sh`**:已存在的文件,不包含 `backend-single` 引用
## 验证标准
1. `mvn clean install -DskipTests``server/` 目录下编译成功
2. `server/deploy.py backend` 能正确找到 JAR 并部署
3. 根目录 `deploy.py` 能正确调用 `server/deploy.py`
4. 项目中不再有任何 `backend-single` 字符串残留(grep 验证)
5. Git 历史完整保留(`git log --follow server/pom.xml` 能看到旧历史)
@@ -0,0 +1,156 @@
---
author: claude
created_at: 2026-06-27
purpose: 规范本地服务管理:强制使用 dev-services.py 控制前后端服务,固定前端/H5 端口从 5178 开始累加,优化热加载避免无意义重启
---
# 本地服务管理规则优化设计
## 背景
当前项目使用 `dev-services.py` 管理本地前后端服务,但存在以下问题:
1. 前端/H5 端口分散配置在各自 `vite.config.*` 中,未统一管理
2. `dev-services.py` 端口冲突时自动递增,导致端口号不稳定
3. 用户已习惯使用 `dev-services.py`,但缺少"禁止无意义重启"的强制约束
4. 热加载场景下,开发者可能误重启支持热更新的前端服务,浪费时间
## 目标
1. 强制:启动/重启/停止本地前后端服务必须使用 `dev-services.py`
2. 固定:前端和 H5 服务端口从 5178 开始累加分配
3. 优化:`dev-services.py restart` 在无必须重启的变更时禁止重启
4. 一致:更新 CLAUDE.md 规则文档,与脚本行为对齐
## 方案选择
采用 **方案 Adev-services.py 内置固定端口表 + 修改各项目 vite 配置**
- 端口控制集中且与项目配置一致
- 与现有 `dev-services.py` 架构兼容
- 便于开发者直接查看配置文件了解端口
## 详细设计
### 1. 固定端口分配表
| 服务 | 目录 | 类型 | 固定端口 |
|---|---|---|---|
| Emotion-museum-web | `web/` | Vite | 5178 |
| Emotion-museum-admin | `web-admin/` | Vite | 5179 |
| Emotion-museum-uniapp | `mini-program/` | UniApp H5 | 5180 |
| Life-script 前端 | `life-script/` | Vite | 5181 |
后端服务端口保持现有配置不变。
### 2. dev-services.py 修改
#### 2.1 新增固定端口映射
```python
FIXED_FRONTEND_PORTS = {
"web": 5178,
"web-admin": 5179,
"mini-program": 5180,
"life-script": 5181,
}
```
按项目目录名匹配。如果目录名不在映射中,仍按原有逻辑处理。
#### 2.2 修改端口分配逻辑
`assign_unique_ports()` 中:
- 对 `FIXED_FRONTEND_PORTS` 中定义的服务,强制使用固定端口
- 如果固定端口被非前端服务占用,报错退出
- 如果固定端口被另一个前端服务占用,报错退出(说明映射配置错误)
- 不再对前端服务自动递增端口
#### 2.3 新增 restart 热加载保护
定义"必须重启才能生效"的文件模式:
```python
RESTART_REQUIRED_PATTERNS = [
r'vite\.config\.(ts|js|mjs|cjs)$',
r'tsconfig\.json$',
r'\.env(\.[^/]*)?$',
r'package\.json$',
r'application.*\.ya?ml$',
r'pom\.xml$',
r'build\.gradle(\.kts)?$',
]
```
`restart` 命令执行前:
1. 获取 git 工作区中已修改的文件列表:`git diff --name-only`
2. 如果没有修改,提示"未检测到代码变更,无需重启"
3. 如果修改文件匹配 `RESTART_REQUIRED_PATTERNS`,允许正常 restart
4. 如果修改文件不匹配,拒绝 restart 并提示:
> 当前修改只涉及支持热加载的源码文件,无需重启。如需强制重启,请使用 `python dev-services.py restart --force`
#### 2.4 支持 `--force` 参数
`restart` 子命令解析中增加 `--force` 选项,允许用户绕过热加载保护强制重启。
### 3. 前端项目配置修改
| 文件 | 修改内容 |
|---|---|
| `web/vite.config.ts` | `server.port` 改为 5178 |
| `web/.env.development` | 添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | `server.port` 改为 5179 |
| `web-admin/.env.development` | `VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | `server.port` 改为 5180 |
| `mini-program/.env.development` | 添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | `server.port` 改为 5181 |
| `life-script/.env.development` | 添加 `VITE_PORT=5181`(不存在则创建) |
### 4. CLAUDE.md 规则更新
新增"本地服务管理规则"章节:
1. **必须使用 dev-services.py**
- 启动、重启、停止本地前后端服务必须使用 `python dev-services.py [命令] [服务名]`
- 禁止直接使用 `npm run dev``npm run dev:h5``mvn spring-boot:run` 等命令
2. **前端/H5 固定端口**
- `web`: 5178
- `web-admin`: 5179
- `mini-program`: 5180
- `life-script`: 5181
3. **禁止无意义重启**
- 只有修改必须重启才能生效的文件时,才允许执行 `restart`
- 修改源码文件(`.vue``.tsx``.ts``.js``.css``.scss` 等)时禁止 restart
- 确需强制重启时,使用 `python dev-services.py restart --force`
4. **热加载规则**
- 前端修改源码文件优先利用热加载
- 后端修改代码按现有规则处理(本地不启动后端,使用服务器验收)
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| 固定端口被占用 | `dev-services.py` 报错,要求先停止占用该端口的服务 |
| 无代码变更执行 restart | 提示无需重启 |
| 只修改 `.vue` 文件执行 restart | 拒绝重启,提示使用 `--force` 或无需重启 |
| 修改 `vite.config.ts` 执行 restart | 允许正常 restart |
| 多个服务同时 restart | 每个服务独立检测,符合规则的重启,不符合的跳过 |
| 用户传 `--force` | 跳过所有检测,强制重启 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `dev-services.py` | 修改:添加固定端口映射、优化端口分配、新增热加载保护 |
| `web/vite.config.ts` | 修改:端口改为 5178 |
| `web/.env.development` | 修改/创建:添加 `VITE_PORT=5178` |
| `web-admin/vite.config.ts` | 修改:端口改为 5179 |
| `web-admin/.env.development` | 修改:`VITE_APP_PORT` 改为 5179 |
| `mini-program/vite.config.js` | 修改:端口改为 5180 |
| `mini-program/.env.development` | 修改/创建:添加 `VITE_PORT=5180` |
| `life-script/vite.config.js` | 修改:端口改为 5181 |
| `life-script/.env.development` | 修改/创建:添加 `VITE_PORT=5181` |
| `CLAUDE.md` | 修改:更新本地服务管理规则 |
@@ -0,0 +1,158 @@
---
author: claude
created_at: 2026-06-27
purpose: 提取 MessageCard 组件,统一所有 AI 消息卡片样式,按内容长度阈值(100 字)决定是否显示完整功能按钮
---
# MessageCard 组件统一设计
## 背景
在上一轮实现中,result-chat-list 中的 assistant 消息使用了 `.chat-bubble.system` 气泡样式,与 `story-card` 的卡片样式不一致。用户要求:
1. 所有 AI 回复消息卡片的样式、功能、布局、按钮等,都与第一次生成剧本的 `story-card` 完全一致
2. 内容 < 100 字的 assistant 消息:只显示消息内容,无功能按钮
3. 内容 ≥ 100 字的 assistant 消息:必须与 `story-card` 完全一致
## 目标
- 提取独立的 `MessageCard.vue` 组件
- story-card 和 result-chat-list 都复用该组件
- 按 100 字阈值自动切换显示模式
## 方案选择
采用 **方案 B:提取独立 MessageCard.vue 组件**
- 代码复用,维护性好
- 改动范围:新建 1 个组件 + 修改 ScriptView.vue
## 详细设计
### 1. 组件接口
**文件**`mini-program/src/components/MessageCard.vue`
**Props**
| Prop | 类型 | 必填 | 说明 |
|---|---|---|---|
| `content` | String | 是 | 消息/故事正文内容 |
| `title` | String | 否 | 卡片标题,默认 `'我的人生剧本'` |
| `tags` | Array | 否 | 标签列表 |
| `collapsed` | Boolean | 是 | 当前折叠状态 |
| `contentLength` | Number | 是 | 内容字数(用于阈值判断) |
| `isShortMessage` | Boolean | 是 | `contentLength < 100` 时为 true |
| `ttsIcon` | String | 否 | TTS 按钮图标(`▶``Ⅱ` |
| `ttsText` | String | 否 | TTS 按钮文字(`播放``暂停` |
**Events**
| 事件 | 说明 |
|---|---|
| `toggle-collapse` | 展开/收起 |
| `copy` | 复制内容 |
| `change-direction` | 换个方向 |
| `not-like-me` | 不像我 |
| `continue` | 继续生成 |
| `play-tts` | 播放 TTS |
### 2. 组件内部结构
**短消息(`isShortMessage = true`**
```vue
<view class="chat-bubble system">
<text>{{ content }}</text>
</view>
```
只显示纯文本气泡,无标题、无标签、无按钮。
**长消息(`isShortMessage = false`**
完整复用现有 `story-card` 的 HTML 结构:
- 标题(`.story-title`+ 标签(`.tag-row`
- 头部操作区:展开/收起按钮 + 复制按钮
- 正文:展开时用 scroll-view,收起时用 text
- 底部:展开全文/收起全文按钮
- 功能按钮组(2 列网格):换个方向、不像我、继续生成、播放
### 3. ScriptView.vue 中的使用
**替换现有 story-card**
```vue
<MessageCard
:content="displayedResultContent"
:title="currentResult?.title"
:tags="resultTags"
:collapsed="storyCollapsed"
:content-length="displayedResultContent.length"
:is-short-message="false"
:tts-icon="ttsActionIcon"
:tts-text="ttsActionText"
@toggle-collapse="toggleStoryCollapse"
@copy="copyResultContent"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="trackTtsClick"
/>
```
**替换 result-chat-list 中的 assistant 消息**
```vue
<view v-for="message in resultMessages" :key="message.id">
<MessageCard
v-if="isAssistantMessage(message)"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="message.content.length < 100"
:tts-icon="ttsPlayer.playing.value ? 'Ⅱ' : '▶'"
:tts-text="ttsPlayer.playing.value ? '暂停' : '播放'"
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
@continue="continueInChat"
@play-tts="playMessageTts(message)"
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time }}</text>
</view>
</view>
```
### 4. 样式迁移
将以下 CSS 类从 `ScriptView.vue` 迁移到 `MessageCard.vue``<style scoped>` 中:
- `.story-card``.story-card.collapsed`
- `.story-head``.story-head-actions``.story-title-wrap`
- `.story-title``.tag-row``.tag`
- `.collapse-icon``.copy-card-btn``.collapse-row`
- `.collapse-chevron``.story-body``.story-body-scroll`
- `.result-actions``.action-btn`
`ScriptView.vue` 中删除这些已迁移的样式。
### 5. 边界情况
| 场景 | 行为 |
|---|---|
| content 刚好 100 字 | `isShortMessage = false`,显示完整卡片 |
| content 为空 | 显示空卡片(与 story-card 现有行为一致) |
| pending 中的消息 | 不使用 MessageCard,保持 thinking-dots 渲染 |
| user 消息 | 不使用 MessageCard,保持 `.chat-bubble.user` 气泡 |
## 修改范围
| 文件 | 操作 |
|---|---|
| `mini-program/src/components/MessageCard.vue` | 新建 |
| `mini-program/src/pages/main/ScriptView.vue` | 修改(模板 + 脚本 + 样式) |
@@ -0,0 +1,207 @@
---
author: system
created_at: 2026-06-27
purpose: 短信登录开关与通用系统配置功能设计
---
# 短信登录开关与通用系统配置设计
## 背景
当前系统未接入真实短信服务商,短信验证码使用硬编码 `123456`。小程序登录页同时展示「微信一键登录」和「手机号+验证码登录」两种方式,SMS 相关元素始终可见。
需要在管理后台增加系统配置管理功能,通过「短信登录开关」控制小程序端是否展示手机号短信登录入口。短信未启用时,小程序登录页只显示微信授权登录。
## 方案选择
采用 **方案 AKey-Value 通用配置表**
- 新建 `t_system_config` 表,key-value 结构
- 通用性强,后续加新配置只需 INSERT 一行数据,不用改表结构
- 复杂度可控,管理后台根据 `value_type` 动态渲染对应控件
## 详细设计
### 一、数据库设计
#### 新建表 `t_system_config`
```sql
CREATE TABLE t_system_config (
id VARCHAR(64) PRIMARY KEY,
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置唯一标识',
config_value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '配置值',
value_type VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT '值类型: boolean/string/number/json',
config_group VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '配置分组: system/login/notification',
config_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '显示名称',
description VARCHAR(300) DEFAULT '' COMMENT '配置说明',
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序号',
is_visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否在管理后台可见: 0-隐藏, 1-显示',
create_by VARCHAR(64) DEFAULT NULL,
create_time DATETIME DEFAULT NULL,
update_by VARCHAR(64) DEFAULT NULL,
update_time DATETIME DEFAULT NULL,
is_deleted TINYINT NOT NULL DEFAULT 0
) COMMENT = '系统配置表';
```
#### 初始数据
```sql
INSERT INTO t_system_config (id, config_key, config_value, value_type, config_group, config_name, description, sort_order, is_visible, create_time, update_time, is_deleted)
VALUES (
REPLACE(UUID(), '-', ''),
'sms_login_enabled',
'false',
'boolean',
'login',
'短信登录',
'是否启用手机号+短信验证码登录方式',
1,
1,
NOW(),
NOW(),
0
);
```
### 二、后端设计
#### 新增文件
| 层 | 文件 | 说明 |
|---|---|---|
| Entity | `SystemConfig.java` | 继承 `BaseEntity`,映射 `t_system_config` |
| Mapper | `SystemConfigMapper.java` | MyBatis-Plus 基础 CRUD |
| Service | `SystemConfigService.java` | 接口定义 |
| Service | `SystemConfigServiceImpl.java` | 业务逻辑实现 |
| Controller | `SystemConfigController.java` | 管理端接口,需 admin 登录 |
| DTO | `SystemConfigUpdateRequest.java` | 更新请求体(key + value |
| DTO | `LoginConfigResponse.java` | 返回给小程序的登录方式配置 |
#### 接口设计
**管理端接口(需要 admin token):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/admin/systemConfig/list` | 获取所有可见配置列表(`is_visible=1` |
| PUT | `/admin/systemConfig/update` | 批量更新配置值,传入 `List<SystemConfigUpdateRequest>` |
**公开接口(不需要登录,小程序登录页调用):**
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | `/auth/loginConfig` | 返回当前启用的登录方式 |
`/auth/loginConfig` 返回格式:
```json
{
"code": 200,
"data": {
"wechatLoginEnabled": true,
"smsLoginEnabled": false
}
}
```
#### 核心逻辑
1. `SystemConfigService` 提供 `getConfigValue(String key)` 方法
2. 内部做 Redis 缓存,缓存 key 为 `system_config:{config_key}`
3. 更新配置时同步清除对应缓存
4. `/auth/loginConfig` 读取 `sms_login_enabled` 配置值,组装 `LoginConfigResponse` 返回
5. 管理端更新接口接收 `List<SystemConfigUpdateRequest>`,支持一次提交多个配置变更
6. `/auth/loginConfig``WebMvcConfig` 白名单中放行(与 `/auth/login``/auth/wechat/login` 同级)
### 三、管理后台设计
#### 路由配置
`web-admin/src/router/index.ts` 中新增:
```ts
{
path: '/system',
component: Layout,
redirect: '/system/settings',
meta: { title: '系统设置', icon: 'Setting' },
children: [
{
path: 'settings',
name: 'SystemSettings',
component: () => import('@/views/system/SystemSettings.vue'),
meta: { title: '基础设置' }
}
]
}
```
#### 页面设计
**文件**`web-admin/src/views/system/SystemSettings.vue`
**页面结构**
- 顶部:页面标题「基础设置」+ 保存按钮
- 内容区:按 `config_group` 分组展示配置项
- 每个配置项根据 `value_type` 渲染不同控件:
- `boolean``el-switch` 开关
- `string``el-input` 输入框
- `number``el-input-number` 数字输入框
- 当前只有一个配置项:「短信登录」开关,下方显示描述文字
- 点击「保存」调用批量更新接口
#### API 封装
新增 `web-admin/src/api/systemConfig.ts`
- `getSystemConfigList()` → GET `/admin/systemConfig/list`
- `updateSystemConfig(list)` → PUT `/admin/systemConfig/update`
### 四、小程序登录页设计
#### 数据获取
在登录页 `onMounted` 时调用 `/auth/loginConfig` 接口:
```js
const loginConfig = ref({ wechatLoginEnabled: true, smsLoginEnabled: false })
onMounted(async () => {
try {
const res = await getLoginConfig()
loginConfig.value = res.data
} catch (e) {
// 接口失败时默认只显示微信登录
loginConfig.value = { wechatLoginEnabled: true, smsLoginEnabled: false }
}
})
```
#### 条件渲染
`v-if="loginConfig.smsLoginEnabled"` 控制以下元素的显示:
- 分隔线(「或使用手机号登录」)
- 手机号输入框
- 验证码输入框 + 获取验证码按钮
- 「开启旅程」手机号登录按钮
始终显示的元素:
- 品牌标题区域
- 「微信一键登录」按钮
- 底部协议文字
#### 代码处理
- 新增 `services/auth.js` 中的 `getLoginConfig()` 方法
- 短信相关的 `ref``phone``code``countdown` 等)和方法保留不动,只是 `v-if` 不渲染
- 不删除任何现有短信登录代码,后续启用短信时只需将配置改为 `true` 即可
## 验收标准
1. 管理后台出现「系统设置」菜单,可看到「短信登录」开关
2. 开关默认关闭(`false`
3. 小程序登录页在短信关闭时只显示微信登录按钮,无手机号表单
4. 管理后台将开关打开后,小程序刷新登录页可看到手机号+验证码表单
5. 后端编译通过,部署到服务器后验收正常
6. 浏览器 Console 无报错
@@ -0,0 +1,106 @@
---
author: Peanut
created_at: 2026-06-28
purpose: 修复生产环境首页 404,删除 nginx 配置中显式阻断根路径的 location 块
---
# 修复 nginx 根路径 404 设计
## 背景
生产环境 `https://lifescript.happylifeos.com/` 访问首页返回 404 Not Found,但:
- 前端文件 `/data/www/emotion-museum/index.html` 实际已正确部署
- 子路径 `https://lifescript.happylifeos.com/emotion-museum-admin/``/life-script/``/emotion-museum/` 可以正常访问
- 后端 API 代理 `/api` 正常工作
## 根因(实际)
**真实生效的 nginx 配置不在 `conf/emotion-museum.conf`**,而是在 aaPanel 托管的:
`/www/server/panel/vhost/nginx/emotion-museum.conf`
原因:nginx 主配置 `/www/server/nginx/conf/nginx.conf` 的 include 顺序是:
```nginx
include /www/server/panel/vhost/nginx/*.conf;
```
它只 include aaPanel 的 vhost 目录。`/etc/nginx/sites-available/` 是部署脚本的"期望"路径,但**从未被 include**,所以 `deploy.py nginx` 实际上一直没真正生效。
真实配置中第 31-33 行有一个显式阻断根路径的 exact match
```nginx
location = / {
return 404;
}
```
并且前端在真实配置中映射的路径是 `/emotion-museum/`(不是根路径):
```nginx
location /emotion-museum/ {
alias /data/www/emotion-museum/;
...
try_files $uri $uri/ /emotion-museum/index.html;
}
```
所以访问 `/` 时,被 `location = / { return 404; }` 精确匹配拦截,根本不会走到任何 location 块。
## 修复方案
### 第一层:修改真实生效的 aaPanel 配置
直接 ssh 编辑 `/www/server/panel/vhost/nginx/emotion-museum.conf`,把:
```nginx
location = / {
return 404;
}
```
改成:
```nginx
location = / {
rewrite ^ /emotion-museum/ permanent;
}
```
这样根路径会被 301 永久重定向到 `/emotion-museum/`(前端实际部署位置)。
然后 `nginx -t && nginx -s reload` 让配置生效。
### 第二层:同步更新仓库内 `conf/emotion-museum.conf`
保持仓库内的配置文件与真实服务器一致(作为基线参考)。同步修改:
1. 删除之前的 `location / { alias /data/www/emotion-museum/; ... }` 通用块(真实配置没有)
2. 新增 `location /emotion-museum/ { ... }` 前端块(与真实配置对齐)
3. `location = /` 改为 `rewrite ^ /emotion-museum/ permanent;`
**注意**:虽然仓库内配置现在不被真实 nginx include,但保持同步是为了:
- 作为文档,记录服务器实际配置
- 未来如果切回 sites-available 部署模式,能直接生效
- 避免"仓库配置"和"服务器配置"分叉导致混乱
## 验证步骤
1. ssh 直接编辑服务器上的 `/www/server/panel/vhost/nginx/emotion-museum.conf`,改 `location = /`
2. `nginx -t` 验证语法
3. `nginx -s reload` 重载配置
4. 浏览器访问 `https://lifescript.happylifeos.com/` 确认 301 → `https://lifescript.happylifeos.com/emotion-museum/`
5. `/emotion-museum/` 正常返回前端页面(200
6. DevTools Network 无 404、Console 无报错
7. 其他子路径(`/emotion-museum-admin/``/life-script/``/api/``/ws``/health`)不受影响
8. 同步更新 `conf/emotion-museum.conf` 作为仓库基线
9. 提交 git commit
## 风险与回退
- **影响面小**:只改 1 个 location 块(3 行),其他 location 块完全不动
- **回退方案**:若出问题,把 `rewrite ^ /emotion-museum/ permanent;` 改回 `return 404;` 即可立即回滚
- **不会冲突**`/emotion-museum/``/emotion-museum-admin/``/life-script/` 都有自己的 location 块,`location = /` 精确匹配只对根路径生效
## 长期改进(不在本次范围)
`deploy.py nginx` 当前部署到 `/etc/nginx/sites-available/`,但服务器主配置 include 的是 aaPanel 路径,所以部署脚本实际上一直无效。后续应该:
- 让 `deploy.py nginx` 同时更新 `/www/server/panel/vhost/nginx/emotion-museum.conf`
- 或者在 nginx 主配置中增加 `include /etc/nginx/sites-enabled/*.conf;`
- 本次不处理,单独跟踪
@@ -0,0 +1,84 @@
---
author: Peanut
created_at: 2026-06-28
purpose: 回滚 ScriptView 的临时 conversationId 正则短路保护,修复历史剧本消息卡片不显示的回归
---
# 回滚 ScriptView 临时 ID 正则短路保护
## 背景
commit 19337b9"ScriptView loadMessages 对临时 conversationId 正则短路保护")引入了一个回归 bug:
用户从"历史剧本"列表点进详情再点"继续"进入 ScriptView**看不到完整的消息卡片**。
## 根因
DB 中历史剧本的 `conversation_id` 字段值就是 `conv-<ts>-<rand>` 格式(前端在早期版本自造后写入):
```
conv-1782552967659-615gen
conv-1782228787632-fkifia
conv-1781497026473-7l8jco
...
```
这些 conversationId 完全匹配 commit 19337b9 加的正则 `^conv-\d+-[a-z0-9]{6}$`
并且这些 conversation 在 `t_conversation` 表中大多没有对应记录(`current_version_message_id` 字段全为 NULL,佐证了当时 conversation 没有完整落库)。
用户打开历史剧本的 ScriptView 时:
1. 从 store 拿到 script`conversationId = script.conversationId` = `conv-1782552967659-615gen`
2. 进入 `loadMessages()`
3. 正则匹配 + `backedByScript=false`script 上没有 conversationId 背书?错,有背书但被误判)
4. 实际上 `currentResult.value.conversationId``openScriptChat` 里被赋值为 `script.conversationId`,但 `openScriptChat` 是在 ScriptView 页内调用的,详情页 "继续" 按钮走的是 `continueCurrent``uni.reLaunch({ url: '/pages/main/index?tab=script' })`ScriptView 重新初始化时 `currentResult.value` 还未被赋值
5. 正则保护触发 → `messages.value = []` → **所有消息卡片不显示**
## 修复方案
**删除** commit 19337b9 在 `mini-program/src/pages/main/ScriptView.vue``loadMessages` 中添加的正则短路保护代码。恢复为:
```javascript
const loadMessages = async () => {
if (!conversationId.value) return
try {
const res = await listMessagesByConversation({ conversationId: conversationId.value, includeVersions: false })
messages.value = res.data || []
const scriptMessages = messages.value.filter(m => m.type === 'script')
if (scriptMessages.length > 0) {
const rootMessage = scriptMessages[0]
const versionRes = await listMessageVersions(rootMessage.id)
versions.value = versionRes.data || []
}
} catch (error) {
uni.showToast({ title: error.message || '加载消息失败', icon: 'none' })
}
}
```
**为什么可以直接回滚**
- commit e3eb1af 已让后端 `listByConversation` 在 conversation 不存在时返回空列表(不抛 500)
- 所以前端调 API 时即使传了"不存在"的 conversationId,也只会得到空数据,不会再 500
- 5xx 噪音问题已由后端修复兜底,前端的正则保护属于过度防御
## 验证步骤
1. 修改 `mini-program/src/pages/main/ScriptView.vue`,删除正则短路保护
2. 启动 H5`python dev-services.py start mini-program`(端口 5180
3. 浏览器访问 `http://localhost:5180`,登录一个有历史剧本的账号
4. 进入任一历史剧本的 ScriptView
5. 验证:
- 不再看到正则保护导致的"消息全空"
- Network 面板有 `/message/listByConversation` 请求
- 响应状态 200body 为 `{"code":200,"data":[]}`conversation 不存在)或有数据
- Console 无报错
## 风险与回退
- **回退方案**:如果回滚后出现 500 噪音,可以再在前端加判断(但后端已兜底,不应再出现)
- **影响面**:仅影响 ScriptView.vue 的 loadMessages 函数,其他页面不受影响
## 长期改进(不在本次范围)
历史剧本的 conversation_id 是前端自造的临时 ID,但被当作"真实 conversationId"写入了 `t_epic_script.conversation_id`。这是数据模型上的不一致:
- 要么在写入 epic_script 时同步创建 t_conversation 记录
- 要么 epic_script.conversation_id 字段只存真实落库的 ID
- 本次不处理,单独跟踪
@@ -0,0 +1,286 @@
---
author: Peanut
created_at: 2026-06-29
purpose: 完整修复 ScriptView 对话模式的按钮缺失问题,补齐 spec 要求的版本/改写/续写/历史版本/删除功能
---
# ScriptView 对话模式按钮完整修复设计
## 背景
`bf6f279 feat: ScriptView 改造为对话中心查看/修改模式` 重构后,对话模式的按钮出现多处丢失:
1. **旧版按钮丢失**`MessageCard``isShortMessage=true`(短消息)分支没有按钮组,而旧版 assistant 消息后面有「复制/换个方向/不像我/继续生成/播放」按钮组(commit `0db434c` 添加)
2. **Spec 功能未实现**`2026-06-28-script-dialogue-design.md` 第 5.4 节要求 AI 剧本消息有「改写 / 续写 / 查看历史版本 / 删除版本」按钮,当前完全没实现
3. **根版本被过滤**`displayMessages``m.type !== 'script' || !m.parentMessageId` 过滤,把根版本 AI 剧本消息丢掉了
4. **版本标签缺失**AI 消息没有 `V1 / V2` 版本号标签和「(当前)」标记
## 设计
### 1. MessageCard.vue 重构
**新增 props**
- `messageType`: `'script' | 'chat' | 'system'`,默认 `'script'`
- `versionLabel`: 版本号显示(如 `V1 (当前)`),script 类型必填,其他类型不传
- `hasChildren`: 是否有子版本(布尔,用于控制「查看历史版本」按钮是否显示)
- `canDelete`: 是否可删除(布尔,当前生效版本不可删除)
- `canRewrite`: 是否可改写(布尔,默认 true
- `canContinue`: 是否可续写(布尔,默认 true
**渲染规则**
| messageType | 渲染方式 |
|---|---|
| `'script'` | **永远**走长文本 story-card 分支(不论 `isShortMessage`),因为剧本消息总是需要按钮组 |
| `'chat'``'system'` | 保持原逻辑(短消息气泡 / 长消息 story-card |
**按钮组**(按 messageType 切换):
```
script 消息按钮组:
┌─ 顶部操作(story-head-actions,不变)
│ · 收起/展开
│ · 复制
├─ 版本标签(新增,仅在 versionLabel 有值时显示)
│ · V1 (当前) / V2 / ...
├─ 功能按钮(新增,script 专属)
│ · 改写(emit 'rewrite'
│ · 续写(emit 'continue-script'
│ · 查看历史版本(emit 'view-versions'hasChildren=true 才显示)
│ · 删除版本(emit 'delete-version'canDelete=true 才显示)
└─ 通用操作(保留旧版)
· TTS 播放(emit 'play-tts'
chat 消息按钮组(isShortMessage=false 时):
┌─ 复制
├─ 收起/展开
└─ TTS 播放(emit 'play-tts'
system 消息(isShortMessage=true 默认走气泡):
└─ 不变
```
**新增 emits**
- `rewrite` — 改写按钮
- `continue-script` — 续写按钮(避免和旧 `continue` 语义冲突,但保留旧 `continue` 兼容)
- `view-versions` — 查看历史版本
- `delete-version` — 删除版本
### 2. ScriptView.vue 改动
**改动 2.1:显示根版本**
```js
// 旧
const displayMessages = computed(() => {
return messages.value.filter(m => m.type !== 'script' || !m.parentMessageId)
})
// 新:保留所有消息,根版本作为 chat 列表首条
const displayMessages = computed(() => messages.value)
```
**改动 2.2:传 messageType 给 MessageCard**
```vue
<MessageCard
v-if="message.sender === 'assistant' || message.type === 'script'"
message-type="script"
:version-label="formatVersionLabel(message)"
:has-children="messageHasChildren(message)"
:can-delete="message.id !== currentVersionMessageId"
:can-rewrite="true"
:can-continue="true"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="false"
:tts-icon="..."
:tts-text="..."
@toggle-collapse="toggleMessageCollapse(message)"
@copy="copyMessageContent(message)"
@rewrite="rewriteMessage(message)"
@continue-script="continueMessage(message)"
@view-versions="viewMessageVersions(message)"
@delete-version="removeVersion(message)"
@play-tts="playMessageTts(message)"
@change-direction="changeDirection"
@not-like-me="notLikeMe"
/>
```
**改动 2.3:版本标签生成函数**
```js
const formatVersionLabel = (message) => {
if (message.type !== 'script') return ''
const versionNum = message.versionNumber || 1
const isCurrent = message.id === currentVersionMessageId.value
return isCurrent ? `V${versionNum} (当前)` : `V${versionNum}`
}
const messageHasChildren = (message) => {
// 通过 versions 数组长度判断(已加载的)或 metadata 推断
return versions.value.some(v => v.parentMessageId === message.id)
|| (message.versionNumber && message.versionNumber > 1)
|| (message.childrenCount && message.childrenCount > 0)
}
```
**改动 2.4:查看历史版本**
新增 `viewMessageVersions(msg)` 函数:
1. 调用 `listMessageVersions(msg.id)`
2. 用 `uni.showActionSheet` 列出所有版本(V1、V2...
3. 用户选择后:
- 如果选的是当前版本:`uni.showToast` 提示「当前已生效」
- 如果选的是其他版本:调用 `switchVersion({ scriptId, messageId: selectedId })` → 更新 `currentVersionMessageId``loadMessages()` 刷新
### 4. 历史剧本兼容层(高优先级)
### 现状
DB 中绝大多数历史剧本的 `conversation_id` 是前端早期自造的临时 ID`conv-<ts>-<rand>` 格式),**`t_conversation` 表中没有对应记录**。
```
329987360219471872 我的人生剧本 → YES (最新剧本,走新流程创建)
f82818b702d5ad4a... 高考 → NO
a3dadb7e85a6c75b... 我高考了 → NO
49ae0f432253aaff... 我中了100w → NO
... 其余 6 条 → NO
```
### 影响分析
| 场景 | 是否受影响 | 原因 |
|---|---|---|
| ScriptDetailView 阅读模式 | ✅ 正常 | 读 `t_epic_script.plotJson`,不依赖 conversation |
| ScriptView `viewMode='read'` 阅读模式 | ✅ 正常 | 通过 `currentScriptContent``currentVersionMessageId` 指向的 messagefallback 到 `plotJson` |
| ScriptView `viewMode='chat'` 聊天模式 | ❌ 异常 | `listByConversation` 返回空,对话历史丢失 |
| 改写 / 续写 / 版本切换 | ❌ 异常 | 需要有效的 conversation 和 message 才能操作 |
### 兼容策略:前端 fallback + 后端自动建表
**改动 A:后端自动建 conversation**
`EpicScriptServiceImpl.getScriptById`(或 `getListByCurrentUser` 返回前)检查:
- 若 `script.conversation_id` 指向的 conversation 不存在 → 自动创建一条 `t_conversation` 记录,写入 `user_id``script_id``type='script'``status='active'`,并回填 `t_epic_script.conversation_id` 为新 ID
**改动 B:前端首次进入聊天模式时,基于 `plotJson` 初始化首条消息**
在 ScriptView.vue 的 `loadMessages` 中:
- 若 API 返回空列表 && `script.plotJson?.fullContent` 存在 → 自动调用 `/message/create` 创建一条 `type='script'` 的 AI 剧本消息(content 来自 plotJson
- 后续改写/续写/版本切换基于这条真实消息操作
**改动 CScriptView 阅读模式 fallback 强化**
当前 `currentScriptContent` 已在 message 不存在时 fallback 到 `plotJson`。需确保:
- `viewMode='read'` 下,版本标签在没有 versions 数据时隐藏(不显示"V0"之类)
- "进入对话修改"按钮在没有 conversation 时仍可点击,触发改动 B 的初始化流程
### 验收标准(历史剧本)
- [ ] 历史剧本在 ScriptDetailView 完整显示标题、summary、content
- [ ] 历史剧本在 ScriptView `viewMode='read'` 完整显示标题、章节内容(无版本标签时 UI 不异常)
- [ ] 历史剧本点击"进入对话修改" → 自动创建 conversation + 初始化首条 AI 消息 → 进入 chat 模式
- [ ] 历史剧本进入 chat 模式后,能看到完整对话历史(首条为用户心愿 + AI 剧本)
- [ ] 历史剧本的"改写/续写/查看历史版本"功能可正常使用
- [ ] 新创建的剧本所有功能不受影响(回归验证)
## 5. 实施顺序
| 顺序 | 任务 | 类型 |
|---|---|---|
| 1 | 后端 `getScriptById` 自动建 conversation | 数据兼容 |
| 2 | 前端 MessageCard 重构 + 按钮组 | 功能补齐 |
| 3 | 前端 ScriptView displayMessages 去掉根版本过滤 | Bug 修复 |
| 4 | 前端 loadMessages 空结果时自动初始化首条消息 | 数据兼容 |
| 5 | 前端 chat 模式传 messageType/versionLabel 等 props | 功能补齐 |
| 6 | 端到端验收(新剧本 + 历史剧本) | 验收 |
## 6. 影响范围
| 文件 | 改动量 |
|---|---|
| `server/.../EpicScriptServiceImpl.java` | 小(getScriptById 检查 + 自动建 conversation |
| `mini-program/src/components/MessageCard.vue` | 中(+props、+emits、+按钮组条件渲染) |
| `mini-program/src/pages/main/ScriptView.vue` | 中(displayMessages filter、MessageCard props、formatVersionLabel、viewMessageVersions、loadMessages 初始化) |
## 验收标准(代码层面)
- [ ] chat 模式下,根版本 AI 剧本消息显示为 chat 列表首条
- [ ] AI 剧本消息显示 V1 / V2 版本号 + (当前) 标记
- [ ] AI 剧本消息按钮组包含:收起/展开、复制、TTS 播放、改写、续写、查看历史版本、删除版本
- [ ] 用户消息(type=chat)保持气泡样式,有收起/展开按钮
- [ ] 点击「改写」弹出 modal 输入改写意图,提交后生成新版本
- [ ] 点击「续写」直接生成新版本
- [ ] 点击「查看历史版本」弹 ActionSheet 列出版本,切换后 UI 更新
- [ ] 当前生效版本不显示「删除版本」按钮
- [ ] 历史剧本进入 ScriptView chat 模式 → 自动创建 conversation + 初始化首条 AI 消息
- [ ] 浏览器 Console 无报错
- [ ] 不影响 read 模式(阅读模式保持现状)
## H5 端到端验收流程(强制)
**后端验证码**`AuthServiceImpl.java:80` 已硬编码 `DEFAULT_SMS_CODE = "123456"`,无需改动。
**测试账号**:手机号 `19928748688`,验证码 `123456`
### 步骤 1:启动本地 H5
```bash
python dev-services.py start mini-program
# 访问 http://localhost:5180
```
### 步骤 2:登录
1. 打开 `http://localhost:5180`
2. 输入手机号 `19928748688`
3. **必须点击"发送验证码"按钮**(触发 `/auth/sms-code` 接口,后端写入 Redis 并返回固定验证码)
4. 输入验证码 `123456`
5. 提交登录
### 步骤 3:新剧本完整流程
1. 首页输入心愿 → 生成第一个剧本
2. 查看剧本详情 → 标题、summary、content 完整显示
3. 点击"继续" → 进入 ScriptView chat 模式
4. **验证**
- 用户心愿气泡显示
- AI 剧本消息显示,带 `V1 (当前)` 版本标签
- AI 消息按钮组包含:收起/展开、复制、TTS 播放、**改写**、**续写**、**查看历史版本**、**删除版本**(当前版本不显示删除)
5. 点"改写" → 输入改写意图 → 生成新版本 V2
6. 点"查看历史版本" → ActionSheet 列出 V1/V2,点击切换
7. 点"续写" → 生成新内容
### 步骤 4:历史剧本完整流程
1. 点"历史"按钮 → 进入剧本库
2. 点任一**历史剧本**(非本次新生成的)→ 进入 ScriptDetailView
3. **验证**
- 标题、summary、content 完整显示(读 `plotJson.fullContent`
- 风格、篇幅、字数正确
4. 点"继续" → 进入 ScriptView
5. **验证**
- 自动创建 `t_conversation`DB 中新增记录)
- 自动初始化首条 AI 消息(基于 plotJson
- chat 模式看到完整对话历史(心愿 + AI 剧本)
- AI 消息带版本标签
- 改写/续写/查看历史版本 可正常操作
### 步骤 5Console 与 Network 检查
- DevTools Console 无报错
- Network 中所有 API 调用返回 2xx(无 5xx、无 401、无 403
- `listByConversation` 调用返回 200 且有数据
### 步骤 6:验收通过标准
- ✅ 新剧本 + 历史剧本所有功能正常
- ✅ 按钮组完整
- ✅ 版本标签正确
- ✅ Console 无报错
- ✅ 历史剧本 conversation 自动创建成功
**未通过的处理**:任一验收项失败 → 停止 → 定位问题 → 修复 → 重新验收 → 直到全部通过才算完成
## 风险与回退
- **MessageCard 重构风险**:新增 props 都带默认值,不会破坏非对话模式(生成页、结果页)的现有用法
- **向后兼容**:旧 `continue` emit 保留,新增 `continue-script` 作为 script 专属,避免语义冲突
- **回退方案**git revert 单个 commit 即可
@@ -0,0 +1,221 @@
---
author: Peanut
created_at: 2026-06-29
purpose: ScriptView 简化为纯对话流模式:删除 read/chat 二段切换 + 按旧版布局统一 AI 消息按钮组
---
# ScriptView 简化为纯对话流模式
## 背景
`bf6f279 feat: ScriptView 改造为对话中心查看/修改模式` 引入了 read/chat 二段切换,但偏离了原始设计。用户的原始设计意图:
- **从剧本库点卡片进入**:直接以对话形式查看完整的从创建剧本开始的对话(用户心愿 → AI 剧本 → 改写对话 → 续写对话 → ...)
- **AI 消息卡片**:按旧版布局展示按钮组
- **不要二段切换**:不要先 read 再点按钮进 chat
## 旧版布局参考(来自 commit `cbf5157``0db434c`
```
MessageCard 顶部(story-head-actions):
┌──────────────────────────────────┐
│ [剧本标题] [收起/展开] [复制] │
└──────────────────────────────────┘
MessageCard 底部(result-actions):
┌──────────────────────────────────┐
│ [换个方向] [不像我] [继续生成] [▶ 播放] │
└──────────────────────────────────┘
```
## 设计
### 1. 入口流程简化
| 入口 | 改动 |
|---|---|
| 剧本库点卡片 | `openScriptChat` → 直接设 `viewState='result'``viewMode='chat'` |
| 首页心愿输入生成新剧本 | 保持现状(home → generating → result |
| read 模式 | **整个删除** |
### 2. 删除 read 模式相关代码
**删除的位置**
| 文件 | 位置 | 内容 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | template `view v-if="viewMode === 'read'"` | 整个版本切换器 + 章节展示 + read-action-bar |
| `mini-program/src/pages/main/ScriptView.vue` | `selectedVersionMessageId` ref | 只被 read 模式用 |
| `mini-program/src/pages/main/ScriptView.vue` | `onSelectVersion` | 版本切换(read 模式用) |
| `mini-program/src/pages/main/ScriptView.vue` | `setAsCurrentVersion` | 设为当前版本(read 模式用) |
| `mini-program/src/pages/main/ScriptView.vue` | `enterChatMode` | read → chat 切换(不再需要) |
| `mini-program/src/pages/main/ScriptView.vue` | `rewriteCurrentVersion` | read 模式改写入口 |
| `mini-program/src/pages/main/ScriptView.vue` | `continueCurrentVersion` | read 模式续写入口 |
`currentVersionMessageId` 保留:chat 模式下 AI 消息的"删除版本"按钮需要它来判断 `canDelete`
### 3. 按旧版布局统一 AI 消息按钮组
**MessageCard 改造**
```vue
<template>
<!-- 短消息气泡:保持现状 -->
<view v-if="isShortMessage && messageType !== 'script'" class="chat-bubble system">
<text>{{ content }}</text>
</view>
<!-- 长文本卡片:story-card -->
<view v-else class="story-card" :class="{ collapsed }">
<!-- 顶部 story-head-actions:收起/展开 + 复制 + 版本号 -->
<view class="story-head">
<view class="story-title-wrap">
<text v-if="versionLabel" class="version-label">{{ versionLabel }}</text>
<text class="story-title">{{ title || '我的人生剧本' }}</text>
<view v-if="tags?.length" class="tag-row">
<text v-for="tag in tags" :key="tag" class="tag">{{ tag }}</text>
</view>
</view>
<view class="story-head-actions">
<button class="collapse-icon" @click="$emit('toggle-collapse')">
<text class="collapse-icon-text">{{ collapsed ? '展开' : '收起' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view><view></view>
</view>
</button>
<button class="copy-card-btn" @click="$emit('copy')">复制</button>
</view>
</view>
<!-- 正文 -->
<scroll-view v-if="collapsed" class="story-body-scroll" scroll-y>
<text class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
</scroll-view>
<text v-else class="story-body" :selectable="true" :user-select="true">{{ content }}</text>
<!-- 底部收起/展开行 -->
<view class="collapse-row" @click="$emit('toggle-collapse')">
<text class="collapse-row-text">{{ collapsed ? '展开全文' : '收起全文' }}</text>
<view class="collapse-chevron" :class="{ down: collapsed }">
<view></view><view></view>
</view>
</view>
<!-- 底部 result-actions:按旧版布局 + 加上 script 专属按钮 -->
<view class="result-actions">
<!-- 旧版基础按钮 -->
<button class="action-btn" @click="$emit('change-direction')">换个方向</button>
<button class="action-btn" @click="$emit('not-like-me')">不像我</button>
<button v-if="canContinue" class="action-btn" @click="$emit('continue-script')">续写</button>
<!-- script 专属功能按钮 -->
<button v-if="canRewrite" class="action-btn" @click="$emit('rewrite')">改写</button>
<button v-if="hasChildren" class="action-btn" @click="$emit('view-versions')">查看历史版本</button>
<button v-if="canDelete" class="action-btn danger" @click="$emit('delete-version')">删除版本</button>
<!-- TTS 播放 -->
<button class="action-btn primary" @click="$emit('play-tts')">
<text class="action-icon">{{ ttsIcon || '▶' }}</text>
<text>{{ ttsText || '播放' }}</text>
</button>
</view>
</view>
</template>
```
**关键变化**
- `messageType === 'script'` 分支去掉(长文本统一渲染)
- `result-actions` 在 script 类型下按旧版布局 + 加 script 专属按钮
- 短消息气泡(chat/system)保持不变
### 4. ScriptView template 简化
**简化前的 read 模式 section**
```vue
<view v-if="viewMode === 'read'">
<view class="version-bar">...</view>
<view class="script-read-content">...</view>
<view class="read-action-bar">...</view>
</view>
```
**删除**。
**简化后的 chat 模式 section**
```vue
<view class="result-chat-view">
<view class="conversation compact">
<!-- 用户心愿气泡 -->
<view class="chat-bubble user">
<text>{{ wishText }}</text>
<text class="bubble-time">{{ currentMessageTime }}</text>
</view>
<!-- 系统提示 -->
<view class="chat-bubble system done">
<text>心愿已实现,故事已为你展开</text>
<text class="bubble-time">{{ currentResultTime }}</text>
</view>
</view>
<!-- 对话消息列表 -->
<view v-if="displayMessages.length" class="result-chat-list">
<view v-for="message in displayMessages" :key="message.id">
<MessageCard
v-if="message.sender === 'assistant' || message.type === 'script'"
message-type="script"
:version-label="formatVersionLabel(message)"
:has-children="messageHasChildren(message)"
:can-delete="message.id !== currentVersionMessageId && message.parentMessageId !== null"
:can-rewrite="true"
:can-continue="true"
:content="message.content"
:collapsed="isMessageCollapsed(message)"
:content-length="message.content.length"
:is-short-message="false"
:tts-icon="..."
:tts-text="..."
@toggle-collapse="..."
@copy="..."
@rewrite="..."
@continue-script="..."
@view-versions="..."
@delete-version="..."
@play-tts="..."
@change-direction="..."
@not-like-me="..."
/>
<view v-else class="chat-bubble user">
<text>{{ message.content }}</text>
<text class="bubble-time">{{ message.time || formatMessageTime() }}</text>
</view>
</view>
</view>
<!-- 输入栏 -->
<view class="result-chat-bar">...</view>
</view>
```
### 5. ScriptView 的 `continue` / `change-direction` / `not-like-me` emits
旧版 emit 在 script-actions 下不存在,需要保留以兼容旧版按钮。
- `continue`(旧版 "继续生成"按钮)→ 触发 `continueMessage``continueInChat`
- `change-direction`"换个方向")→ 触发 `changeDirection`
- `not-like-me`"不像我")→ 触发 `notLikeMe`
**这些函数已经存在于 ScriptView.vue 中**(来自旧版代码)。直接绑定。
## 验收标准
- [ ] 从剧本库点卡片 → 直接进入对话流(无需点"进入对话修改")
- [ ] 对话流完整显示:用户心愿 + AI 剧本 + 改写/续写对话
- [ ] AI 消息卡片顶部:收起/展开、复制、版本号标签
- [ ] AI 消息卡片底部:换个方向、不像我、续写、改写、查看历史版本、删除版本、TTS 播放
- [ ] read 模式相关代码完全删除(不再有任何切换按钮)
- [ ] Console 无报错
## 风险与回退
- **风险低**:只删除 read 模式 + 调整 MessageCard 按钮布局,不影响后端
- **回退**`git revert` 单个 commit 即可恢复
@@ -0,0 +1,67 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: 修复 chat 视图底部发送按钮过高过大、与输入框不等高的问题
---
# Chat 视图发送按钮等高修复设计
## 概述
chat 视图底部 `.chat-input-bar` 中"发送"按钮(`.chat-send-btn`)的 `padding: 16rpx 32rpx` 撑大了按钮高度,与文本输入框 `.result-chat-input``min-height: 60rpx`)不等高,视觉上按钮比输入框大。
## 问题
- `.chat-send-btn { padding: 16rpx 32rpx; }` — 上下各 16rpx padding 撑大按钮
- `.result-chat-input { min-height: 60rpx; }` — 输入框有 min-height,但实际渲染高度随内容变化
- `.chat-input-bar { align-items: flex-end; }` — 底部对齐进一步放大不等高视觉差异
## 修复方案
### 改动文件
- `mini-program/src/pages/main/ScriptView.vue`(样式部分)
### 改动 1`.chat-send-btn` 改 padding + 设 height
```css
.chat-send-btn {
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 0 32rpx;
background: #087e8b;
border-radius: 32rpx;
color: #ffffff;
font-size: 28rpx;
}
```
**关键变化**
- 新增 `height: 60rpx`:与 `.result-chat-input``min-height: 60rpx` 保持一致
- `padding: 16rpx 32rpx``padding: 0 32rpx`:去掉上下 padding,靠 height+flex 居中文字
- 新增 `display: flex; align-items: center; justify-content: center;`:文字垂直居中
### 改动 2`.chat-input-bar``align-items`
```css
.chat-input-bar {
display: flex;
align-items: center;
gap: 16rpx;
padding: 16rpx 24rpx;
background: rgba(255, 255, 255, 0.04);
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
}
```
**关键变化**
- `align-items: flex-end``align-items: center`:按钮和输入框垂直居中对齐
## 验收标准
- [ ] 发送按钮与文本输入框等高
- [ ] 按钮文字垂直居中
- [ ] mp-weixin 编译通过
- [ ] H5 浏览器 Console 无报错
@@ -0,0 +1,94 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: 修复 ScriptView chat 视图底部 outline 输入框和按钮被主页 bottom-nav 遮挡的问题(架构层修正)
---
# Chat 视图底部遮挡架构层修复设计
## 概述
前一轮 CSS 修复(`padding-bottom: 120rpx`)未能彻底解决 bottom-nav 遮挡问题:`.outline-feedback` 输入框的 placeholder "如需修改请输入意见" 被截断,"修改大纲" 和 "确认大纲" 按钮也无法完整可见。根因是 `.chat-page` 使用 `height: 100vh`,完全无视父级 `.content` 已有的 `padding-bottom: 132rpx`(已经为 bottom-nav 预留了空间)。
## 问题根因
主页面 `pages/main/index.vue` 结构:
```css
.app-shell { height: 100vh; display: flex; flex-direction: column; }
.content { flex: 1; height: 0; padding: 0 28rpx 132rpx; } /* 父级已经预留 132rpx */
.bottom-nav { position: absolute; bottom: 0; height: 104rpx; z-index: 20; }
```
ScriptView 组件被渲染在 `.content` 内。父级 `.content` 已经预留了 132rpx 底部空间给 bottom-nav,但 `.chat-page` 设置了 `height: 100vh`,这让它撑满整个视口高度,**无视了父级的 padding-bottom**,导致 chat 滚动区底部仍然被 bottom-nav 覆盖约 104-134rpx。
## 修复方案
`.chat-page` 填充父级内容区(不设固定视口高度),让父级已有的 132rpx padding-bottom 自然生效。
### 改动文件
- `mini-program/src/pages/main/ScriptView.vue`(样式部分)
### 改动 1`.chat-page``height: 100vh` 改为 `height: 100%`
```css
.chat-page {
/* height: 100vh → height: 100% */
height: 100%;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
}
```
**关键变化**
- `height: 100%`:填充父级 `.content` 的可用高度,父级的 132rpx padding-bottom 自然生效
- 保留 `min-height: 0`:让 flex 子项可以正确收缩
- 保留 `padding-bottom: env(safe-area-inset-bottom)`:适配 iPhone home indicator
- 删除 `constant(safe-area-inset-bottom)`iOS 11.0-11.2 已极少,无需兼容
### 改动 2`.chat-scroll-content` 改回 `padding: 0 24rpx 40rpx`
```css
.chat-scroll-content {
padding: 0 24rpx 40rpx;
}
```
**为什么改回 40rpx**
- 父级 `.content` 已经预留 132rpx 给 bottom-nav
- chat-page 用 `height: 100%` 后,滚动区底部已经被父级 padding 隔开
- 不再需要额外的 120rpx padding(否则会双重叠加变成 252rpx 空白)
## 为什么这样能解决问题
1. 父级 `.content` 的 132rpx padding-bottom 创建了一个 "安全区"
2. chat-page 用 `height: 100%` 填充父级内容可用高度(不含 132rpx 安全区)
3. chat-scrollflex: 1 + height: 100%)继承这个受限高度
4. chat-scroll-content 在受限区域内滚动,底部自动避开 bottom-nav
5. outline 输入框和按钮都在 chat-scroll-content 内,自然在安全区内
6. placeholder "如需修改请输入意见" 完整显示,按钮可点击
## 不影响范围
- home 视图(`.wish-home`):独立样式,不受影响
- generation 视图(`.generation-view`):独立样式,不受影响
- 其他 message 类型(user bubble、novel、text、card-answered):样式不变
- chat-input-bar:条件渲染在 scroll-view 外,不受影响
- mp-weixin 滚动:`.chat-scroll` 仍保持 `flex: 1; height: 100%; min-height: 0`,原生 scroll-view 仍能正确计算可滚动区域
## 验收标准
- [ ] H5http://localhost:5180)心愿实现 chat 视图:outline 输入框的 placeholder "如需修改请输入意见" 完整可见(不被截断)
- [ ] "修改大纲" 和 "确认大纲" 按钮在 bottom-nav 上方完整可见可点击
- [ ] 向上滚动时大刚内容(标题、beats、结局)正常显示
- [ ] iPhone 真机(带 home indicator)底部也能完整看到所有内容
- [ ] Console 无报错
- [ ] mp-weixin 编译通过(`npm run build:mp-weixin` 无错误)
- [ ] 编译产物中 `.chat-page``height``100%``padding-bottom``env(safe-area-inset-bottom)`
- [ ] 编译产物中 `.chat-scroll-content``padding``0 24rpx 40rpx`
- [ ] home 视图、generation 视图、chat-input-bar 布局不受影响
@@ -0,0 +1,72 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: 修复 ScriptView chat 视图 ClarificationCard 提交按钮被主页 bottom-nav 遮挡的问题
---
# Chat 视图底部内边距修复设计
## 概述
小程序心愿实现页面(ScriptViewchat 视图的 ClarificationCard 提交按钮无法滚动到可见区域,根因是主页面 `pages/main/index.vue``.bottom-nav``position: absolute; bottom: 0; z-index: 20; height: 104rpx`)叠在 script-view 组件底部,遮挡了 chat 滚动内容的最后一段。
## 问题
用户在 chat 视图选择澄清卡片的选项后,需要点击卡片最下方的「提交」按钮继续流程,但:
- 滚动内容被 bottom-nav 遮住约 104rpx
- iPhone 真机底部 home indicator 进一步压缩可见区域
- 用户能看到卡片上半部分(问题描述、选项、自定义输入框)但看不到「提交」按钮
- 无法继续后续流程
## 修复方案
调整 chat 视图容器的底部内边距,让滚动内容可以滚到「提交」按钮完全可见在 bottom-nav 上方。
### 改动文件
- `mini-program/src/pages/main/ScriptView.vue`(样式部分)
### 改动 1`.chat-page` 增加 iPhone 安全区内边距
```css
.chat-page {
height: 100vh;
min-height: 0;
display: flex;
flex-direction: column;
background: #13091f;
box-sizing: border-box;
padding-bottom: env(safe-area-inset-bottom);
}
```
作用:让 chat 容器适配 iPhone 底部 home indicator 区域,避免聊天内容被 home indicator 遮挡。
### 改动 2`.chat-scroll-content` 增加底部内边距
```css
.chat-scroll-content {
padding: 0 24rpx 120rpx;
}
```
作用:在 chat 滚动区底部留出 120rpx 空间:
- 104rpx = bottom-nav 高度
- 16rpx = 呼吸间距,避免按钮贴边
保证 ClarificationCard 的「提交」按钮可滚动到 bottom-nav 上方完整可见。
## 不影响范围
- home 视图:使用独立的样式(`.wish-home`),不受影响
- chat-input-bar:条件渲染且在 scroll-view 外部,不受影响
- 其他 message 类型(user bubble、outline、novel、error):样式结构不变
## 验收标准
- [ ] 在 H5http://localhost:5180)心愿实现 chat 视图,澄清卡片可滚动到「提交」按钮完全可见
- [ ] 「提交」按钮在主页 bottom-nav 导航栏上方,无遮挡
- [ ] iPhone 真机(带 home indicator)底部也能完整看到「提交」按钮
- [ ] Console 无报错
- [ ] mp-weixin 编译通过(`npm run build:mp-weixin` 无错误)
- [ ] 点击「提交」后流程正常继续(追加 user 消息 + 调 followupStream
@@ -0,0 +1,249 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: ScriptView 心愿实现页面统一对话流改造设计,将澄清/大纲/小说生成全部纳入单一对话页面
---
# ScriptView 统一对话页改造设计
## 概述
将小程序心愿实现页面(ScriptView.vue)的生成流程从"多独立视图切换"改为"单一对话页面流"。用户从提交心愿开始,始终停留在同一个对话页面,澄清问答、大纲确认/修改、小说流式生成、后续对话修改都作为对话消息依次出现在聊天流里。
## 背景与问题
### 当前架构(AS-IS
ScriptView.vue 用 `viewState` 在 5 个独立视图间切换:
| viewState | 内容 | 对话页? |
|---|---|---|
| `home` | 心愿输入首页(灵感推荐+输入框) | 否 |
| `generating` | 生成中加载页(用户气泡+loading orbit | 否 |
| `clarifying` | 澄清卡片(只有 ClarificationCard | 否 |
| `outlining` | 大纲确认(大纲+确认/修改按钮) | 否 |
| `result` | 结果对话页(MessageCard+用户气泡+输入栏) | 是 |
问题:澄清、大纲、小说生成阶段用户"不在对话页面",体验割裂。用户要求"心愿实现时必须一直处于对话的页面,必须支持对话和选择的澄清判断"。
### 可复用资产
`result` 视图已有完整对话机制:
- `resultMessages` 数组 + `addResultMessage({id, role, kind, content, pending, time})`
- `MessageCard` 组件渲染 AI 消息
- 用户气泡渲染用户消息
- 底部输入栏 `sendChat`(当前接旧接口 `streamScriptChat`
- 滚动控制(`keepResultAtBottom``resultScrollTop` 等)
## 目标架构(TO-BE
### viewState 简化
从 5 个视图简化为 **2 个**
```
home(首页:灵感推荐+输入框,保留不变)
↓ 用户提交心愿
chat(统一对话页:澄清→大纲→小说→后续修改 全在这里)
```
删除独立的 `generating`/`clarifying`/`outlining`/`novel-generating` 视图。
### 统一对话消息模型
复用 `resultMessages` + `addResultMessage`,扩展 `kind` 字段:
```javascript
{
id, // createMessageId()
role, // 'user' | 'assistant'
kind, // 'text' | 'card' | 'outline' | 'novel'
content, // 文本内容(text/novel
pending, // 是否加载中(novel 流式时 true)
time, // formatMessageTime()
card?, // kind:'card' 时携带澄清卡片数据
outline?, // kind:'outline' 时携带大纲数据
submitted?, // kind:'card' 时标记是否已提交(禁用交互)
confirmed?, // kind:'outline' 时标记是否已确认/修改(禁用交互)
failed? // 是否失败(error 事件)
}
```
### 消息类型与渲染
`chat` 视图对话区用 `v-for` 遍历 `resultMessages`,根据 `kind` 渲染不同气泡内容:
| kind | role | 渲染方式 |
|---|---|---|
| `text` | user | 用户气泡(`chat-bubble user` |
| `text` | assistant | AI 文本气泡(`chat-bubble system`),含状态提示/错误 |
| `card` | assistant | AI 气泡内嵌 `ClarificationCard``submitted` 后禁用交互并显示已选答案 |
| `outline` | assistant | AI 气泡内嵌大纲卡片+确认/修改按钮,`confirmed` 后禁用按钮 |
| `novel` | assistant | AI 气泡内嵌小说正文,`pending` 时流式逐字追加,`pending=false` 后完成 |
新消息追加到底部,`keepResultAtBottom()` 自动滚动跟随。
### SSE 事件 → 对话消息映射
`handleShortNovelEvent` 改为往统一对话流追加/更新消息:
| SSE 事件 | 对话流行为 |
|---|---|
| `status` | 更新底部 loading 提示文案(不追加独立消息) |
| `clarification_card` | `addResultMessage({role:'assistant', kind:'card', card: payload.card, submitted:false})` |
| `outline_created` | `addResultMessage({role:'assistant', kind:'outline', outline: payload.outline, confirmed:false})` |
| `novel_start` | `addResultMessage({role:'assistant', kind:'novel', content:'', pending:true})` |
| `novel_delta` | 往最后一条 `kind:'novel'` 消息的 `content` 追加 `payload.delta` |
| `novel_done` | 标记最后一条 novel 消息 `pending=false`,保存 `scriptId`/`conversationId`/`currentVersionMessageId` |
| `error` | 若 `DAILY_GENERATION_IN_PROGRESS` 且有 `session_id`:保存 `resumeableSessionId`,更新底部 loading 为失败态+继续创作入口;其他错误:追加 `kind:'text', role:'assistant', failed:true` 消息 |
### 用户交互 → 接口调用
每张卡片/大纲消息内部渲染交互按钮,用户操作时**先追加一条 user 消息到对话流,再调接口**:
| 阶段 | 交互 | 追加 user 消息 | 接口调用 |
|---|---|---|---|
| 生成 | ClarificationCard 提交 | `{kind:'text', content: 答案}` | `followupStream(action:'answer_clarification', payload:{answer})` |
| 生成 | 大纲"确认" | `{kind:'text', content:'确认大纲'}` | `followupStream(action:'confirm_outline')` |
| 生成 | 大纲"修改" | `{kind:'text', content: 修改意见}` | `followupStream(action:'modify_outline', payload:{feedback})` |
| 生成 | 失败"继续创作" | `{kind:'text', content:'继续之前的创作'}` | `followupStream(action:'retry')` |
| 后续 | novel_done 后底部输入 | `{kind:'text', content: 修改建议}` | `streamScriptChat`(旧接口,支持改写/继续/版本) |
### 接口切换边界
- **生成流程**(澄清→大纲→小说):用新接口 `startNovelStream` / `followupStream`
- **novel_done 后**:保存记录后,底部输入栏 `sendChat` 切换用现有旧接口 `streamScriptChat` + `createMessage`(已成熟,支持改写/继续/版本切换)
切换时机:`novel_done` 事件处理时,设置 `generationPhase = 'done'`,底部输入栏根据该标志切换接口。两套接口的消息都进同一个 `resultMessages` 对话流,用户无感知。
## 交互态管理
### 卡片消息(kind:'card'
- 渲染 `ClarificationCard`,传入 `:card="message.card"`
- `submitted=false` 时:正常交互,`@submit` 触发 `submitClarification`
- `submitClarification` 内:设置 `message.submitted=true`(禁用卡片),追加 user 消息,调 followupStream
- `submitted=true` 时:卡片显示"已回答:xxx",禁用交互
### 大纲消息(kind:'outline'
- 渲染大纲内容(title/logline/beats/ending+ 确认/修改按钮
- `confirmed=false` 时:显示输入框+确认+修改按钮
- 用户确认/修改后:设置 `message.confirmed=true`(隐藏按钮),追加 user 消息,调 followupStream
- `confirmed=true` 时:只展示大纲内容,不再有按钮
### 小说消息(kind:'novel'
- `pending=true` 时:流式追加 `novel_delta` 文本,显示打字光标
- `pending=false`(novel_done)时:完整小说正文,无光标,可复制/朗读
## chat 视图模板结构
```vue
<view v-else-if="viewState === 'chat'" class="chat-page">
<!-- 顶部:历史 + 关闭 -->
<view class="chat-top-actions">...</view>
<!-- 对话流 -->
<scroll-view class="chat-scroll" scroll-y ...>
<view class="chat-scroll-content">
<view v-for="msg in resultMessages" :key="msg.id">
<!-- user text -->
<view v-if="msg.role==='user' && msg.kind==='text'" class="chat-bubble user">
<text>{{ msg.content }}</text>
<text class="bubble-time">{{ msg.time }}</text>
</view>
<!-- assistant card -->
<view v-else-if="msg.kind==='card'" class="chat-bubble system">
<ClarificationCard
v-if="!msg.submitted"
:card="msg.card"
@submit="answerClarification(msg, $event)"
/>
<view v-else class="card-answered">
<text>已回答:{{ msg.answer }}</text>
</view>
</view>
<!-- assistant outline -->
<view v-else-if="msg.kind==='outline'" class="chat-bubble system">
<!-- 大纲内联渲染(title/logline/beats/ending),复用当前 outlining 视图样式 -->
<view v-if="msg.outline?.title" class="outline-title">{{ msg.outline.title }}</view>
<view v-if="msg.outline?.logline" class="outline-logline">{{ msg.outline.logline }}</view>
<view v-if="Array.isArray(msg.outline?.beats)" class="outline-beats">
<view v-for="(beat, idx) in msg.outline.beats" :key="idx" class="beat-item">
<text class="beat-order">{{ beat.order || idx + 1 }}</text>
<view class="beat-content">
<text class="beat-title">{{ beat.title }}</text>
<text v-if="beat.summary" class="beat-summary">{{ beat.summary }}</text>
</view>
</view>
</view>
<view v-if="msg.outline?.ending" class="outline-ending">
<text class="ending-label">结局</text>
<text class="ending-text">{{ msg.outline.ending }}</text>
</view>
<view v-if="!msg.confirmed" class="outline-actions">
<input v-model="msg.feedback" placeholder="如需修改请输入意见" />
<button @click="modifyOutline(msg)">修改大纲</button>
<button @click="confirmOutline(msg)">确认大纲</button>
</view>
</view>
<!-- assistant novel -->
<view v-else-if="msg.kind==='novel'" class="chat-bubble system">
<text>{{ msg.content }}<text v-if="msg.pending" class="typing-cursor">|</text></text>
</view>
<!-- assistant text (status/error) -->
<view v-else class="chat-bubble system">
<text :class="{ failed: msg.failed }">{{ msg.content }}</text>
</view>
</view>
<!-- 底部 loading -->
<view v-if="generating" class="chat-loading">
<view class="loading-orbit streaming">...</view>
<text>正在为你重写人生</text>
</view>
<!-- 失败继续创作入口 -->
<view v-if="generationStatus==='failed' && resumeableSessionId" class="resume-action">
<button @click="resumeSession">继续创作</button>
</view>
<view :id="resultScrollAnchor" class="chat-scroll-anchor"></view>
</view>
</scroll-view>
<!-- 底部输入栏(novel_done 后启用) -->
<view class="chat-input-bar">
<textarea v-model="chatInput" @confirm="sendChat" />
<view @click="sendChat">发送</view>
</view>
</view>
```
## 文件改动清单
| 文件 | 操作 | 说明 |
|---|---|---|
| `mini-program/src/pages/main/ScriptView.vue` | 修改 | 删除 clarifying/outlining/novel-generating 独立视图;新增统一 chat 视图;扩展 addResultMessage 支持 kind/card/outline/submitted/confirmed;改造 handleShortNovelEvent 往对话流追加消息;submitClarification/confirmOutline/modifyOutline 改为操作对话消息;sendChat 按 generationPhase 切换接口 |
| `mini-program/src/components/ClarificationCard.vue` | 不变 | 复用,由父组件控制 submitted 态 |
## 不在本次范围内
- 首页 home 的灵感推荐/语音输入 — 保留不变
- MessageCard 的版本切换/改写/继续功能 — 保留不变(novel_done 后用旧接口时复用)
- 后端接口 — 不变(新接口 followupStream + 旧接口 streamScriptChat 都已就绪)
- 历史剧本库 — 保留不变
## 验收标准
- 用户提交心愿后进入对话页,用户消息作为气泡显示
- 澄清卡片作为 AI 消息出现在对话流,用户回答后卡片变为"已回答"态
- 大纲作为 AI 消息出现,确认/修改后按钮消失
- 小说流式生成作为一条 AI 消息逐字出现
- novel_done 后底部输入栏启用,继续修改用旧接口正常工作
- 失败时(DAILY_GENERATION_IN_PROGRESS)对话流内显示继续创作入口
- 整个过程不切换视图,用户始终在同一个对话页面
@@ -0,0 +1,239 @@
---
author: AI Assistant
created_at: 2026-07-19
purpose: 小程序心愿实现页面的短篇小说生成接口从内部 AI Runtime 迁移到外部 short-novel-service 的设计方案
---
# 短篇小说服务接口迁移设计
## 概述
将小程序"心愿实现"页面(ScriptView.vue)的短篇小说生成流程,从当前内部 AI Runtime 同步调用模式,迁移到外部 short-novel-service`http://49.232.138.53:8010`)的流式 SSE 多轮交互模式。同时开启小程序的手机号短信登录功能(验证码固定 123456)。
## 架构决策
- **架构模式**:后端 SSE 代理 + 前端完整多轮交互适配
- **持久化策略**:小说生成完成后一次性保存(中间交互阶段不持久化)
- **认证方式**:外部服务 API Token 保存在后端配置中,前端不接触
## 当前架构(AS-IS
### 前端流程
```
ScriptView.vue
→ submitWish() → runGeneration()
→ createScriptWithDialogue(payload) // POST /epicScript/createWithDialogue
→ 返回 { scriptId, conversationId, title, plotIntro, plotTurning, plotClimax, plotEnding }
→ loadMessages()
→ viewState = 'result'
```
### 后端流程
```
EpicScriptController.createWithDialogue()
→ EpicScriptDialogueServiceImpl.createWithDialogue()
→ 创建 EpicScript + Conversation 记录
→ invokeScriptGenerate() → aiRuntimeService.test("script_generate") // 同步 AI 调用
→ parseScriptOutput() → 解析 JSON 输出
→ 创建 3 条 Message(系统欢迎、用户输入、AI 回复)
→ 返回 EpicScriptCreateWithDialogueResponse
```
### 当前接口特征
- 同步调用,一次性返回完整结果
- AI 输出为 JSON 格式(title、plotIntro、plotTurning、plotClimax、plotEnding
- 前端无中间交互阶段
## 目标架构(TO-BE
### 前端新流程
```
ScriptView.vue 状态机:
home → clarifying ⇄ outlining → novel-generating → result
```
#### 阶段详情
| 阶段 | 触发事件 | UI 行为 | 用户操作 |
|---|---|---|---|
| `home` | 用户输入心愿 | 展示输入框+灵感推荐+语音按钮 | 输入文本/语音,点击发送 |
| `clarifying` | `clarification_card` SSE 事件 | 展示卡片式问题 | 选择选项或输入文本回答 |
| `outlining` | `outline_created` SSE 事件 | 展示大纲(标题/logline/beats/结局) | 确认大纲 或 提出修改意见 |
| `novel-generating` | `novel_start` SSE 事件 | 逐字流式展示小说正文 | 等待生成完成 |
| `result` | `novel_done` SSE 事件 | 展示完成的小说+后续对话 | 继续对话修改/朗读/分享 |
#### 澄清卡片类型
| card_type | UI 渲染 | 交互方式 |
|---|---|---|
| `single_select` | 单选按钮组 | 选择一个选项 |
| `multi_select` | 多选标签组 | 选择多个选项 |
| `text_input` | 文本输入框 | 自由输入 |
| `mixed` | 选项 + 输入框 | 选择或自由输入 |
### 后端新架构
#### 新增 Controller
```java
@RestController
@RequestMapping("/shortNovel")
public class ShortNovelController {
// 首次发起(SSE 流式代理)
@PostMapping("/stream")
public SseEmitter stream(@RequestBody ShortNovelStreamRequest request);
// 后续轮次(SSE 流式代理)
@PostMapping("/followup")
public SseEmitter followup(@RequestBody ShortNovelFollowupRequest request);
}
```
#### 请求/响应格式
**POST /shortNovel/stream**
入参:
```json
{ "query": "用户的心愿文本" }
```
后端转发到外部服务:
```json
{
"user_id": "当前登录用户ID",
"message_id": "web_{timestamp}",
"query": "用户的心愿文本"
}
```
**POST /shortNovel/followup**
入参:
```json
{
"sessionId": "外部服务返回的 session_id",
"action": "answer_clarification | confirm_outline | modify_outline | retry",
"payload": {
"answer": "用户回答(answer_clarification 时)",
"feedback": "修改意见(modify_outline 时)"
}
}
```
后端转发到外部服务:
```json
{
"session_id": "sessionId",
"user_id": "当前登录用户ID",
"message_id": "web_{timestamp}",
"action": "answer_clarification | confirm_outline | modify_outline | retry",
"payload": { ... }
}
```
#### SSE 事件格式(透传)
外部服务返回的 SSE 事件格式:
```
data: {"type":"status","session_id":"xxx","payload":{"message":"处理中","stage":"clarification"}}
data: {"type":"clarification_card","session_id":"xxx","payload":{"card":{"card_type":"single_select","question":"...","options":[...]}}}
data: {"type":"outline_created","session_id":"xxx","payload":{"outline":{"title":"...","beats":[...]}}}
data: {"type":"novel_start","session_id":"xxx","payload":{}}
data: {"type":"novel_delta","session_id":"xxx","payload":{"delta":"文本片段"}}
data: {"type":"novel_done","session_id":"xxx","payload":{"full_text":"完整小说文本"}}
data: {"type":"error","session_id":"xxx","payload":{"code":"ERROR_CODE","message":"错误描述"}}
```
后端 SseEmitter 代理逐行转发这些事件,不做任何修改。
#### SSE 代理实现
```java
// 使用 RestTemplate 读取外部服务 SSE 流
// 逐行解析 data: 行,通过 SseEmitter.send() 转发
// 超时设置 300 秒
// 错误处理:连接失败、超时、外部服务错误
```
#### 配置
```yaml
# application.yml
short-novel:
api-base-url: http://49.232.138.53:8010
api-token: c67d4a95b0bb92470a24d534302c0d40
connect-timeout: 10000
read-timeout: 300000
```
### 数据持久化
在收到 `novel_done` 事件后,后端执行一次性保存:
1. 从 SSE 流中捕获 `novel_done` 事件的 `full_text`
2. 创建 `EpicScript` 记录
3. 创建 `Conversation` 记录
4. 创建 3 条 `Message` 记录(系统欢迎、用户初始输入、AI 小说全文)
5. 在最后一个 SSE 事件(`novel_done`)中附加 `scriptId``conversationId`
实现方式:在 SSE 代理层拦截 `novel_done` 事件,在转发给前端之前执行保存逻辑,并将 ID 信息注入到事件的 payload 中。
### 短信验证码修复
#### 改动范围
| 文件 | 行号 | 当前值 | 修改为 |
|---|---|---|---|
| `mini-program/src/pages/login/index.vue` | 149 | `'验证码已发送(模拟: 888888'` | `'验证码已发送(模拟: 123456'` |
后端已使用 `DEFAULT_SMS_CODE = "123456"`AuthServiceImpl.java 第 80 行),无需修改。
## 前端新增/修改文件清单
| 文件 | 操作 | 说明 |
|---|---|---|
| `services/shortNovel.js` | 新增 | 短篇小说流式 API 服务 |
| `components/ClarificationCard.vue` | 新增 | 澄清卡片组件 |
| `pages/main/ScriptView.vue` | 修改 | 生成流程改为状态机,新增 clarifying/outlining/novel-generating 阶段 |
## 后端新增/修改文件清单
| 文件 | 操作 | 说明 |
|---|---|---|
| `controller/ShortNovelController.java` | 新增 | SSE 代理接口 |
| `service/ShortNovelService.java` | 新增 | SSE 代理转发逻辑 |
| `service/impl/ShortNovelServiceImpl.java` | 新增 | SSE 代理实现 |
| `dto/request/ShortNovelStreamRequest.java` | 新增 | 首次请求 DTO |
| `dto/request/ShortNovelFollowupRequest.java` | 新增 | 后续轮次 DTO |
| `config/ShortNovelConfig.java` | 新增 | 配置类(api-base-url、api-token |
| `application.yml` | 修改 | 新增 short-novel 配置段 |
| `EpicScriptDialogueServiceImpl.java` | 修改 | 抽取保存逻辑为可复用方法 |
## 错误处理
| 场景 | 处理方式 |
|---|---|
| 外部服务不可达 | SSE 发送 `error` 事件,前端显示"服务暂时不可用" |
| 外部服务超时(>300s | SseEmitter 超时回调,前端显示"生成超时" |
| 外部服务返回错误事件 | 透传 error 事件给前端 |
| 用户中途退出 | 前端关闭 SSE 连接,后端 SseEmitter 自动清理 |
| 小说生成失败 | 外部服务发送 `error` 事件,前端提供"重试"按钮 |
## 不在本次范围内
- 外部服务的知识库图谱(knowledge-graph)功能 — 暂不集成
- 外部服务的 prompt 自定义配置 — 使用外部服务默认配置
- 历史对话的恢复/继续 — 已完成的历史记录按现有方式展示
- 真实的短信发送服务 — 继续使用固定验证码 123456

Some files were not shown because too many files have changed in this diff Show More