Skip to content

Commit 1aee875

Browse files
LyaQanYiclaudebreakkh811-staryiyiyiyiGKYHongzhi Wen
authored
feat(card-assist): 角色卡 AI 陪伴式捏人助手(clarify/generate/refine + chat 四端点 + 侧栏面板) (Project-N-E-K-O#1419)
* feat(frontend): 角色卡 AI 辅助生成(3 步式 clarify → generate → refine) 在「角色卡管理」里新增 AI 辅助生成入口,复用 assist API(沿用 memory/refine.py 的 create_chat_llm 路径),三步引导用户产出一张完整的猫娘设定: - POST /api/card-assist/clarify —— 根据一句话描述返回 2~4 个 chip 式澄清问题 - POST /api/card-assist/generate —— 结合澄清答案产出完整字段字典(中文 key) - POST /api/card-assist/refine —— 对单个字段重新生成(再随机/更傲娇/更温柔/自定义) 前端:在 character_card_manager.js 接入 3 步式 wizard,字段级 diff 预览、勾选应用、 inline 编辑、单字段 chip 微调;CSS 配套;en / zh-CN 双语 i18n 全量补齐。 返回字段都做了脱壳(去掉 \`\`\`json fence + 引号)、字段值统一 coerce 成非空字符串, LLM 异常路径都按 502 / 400 区分。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(ci): card-assist 改名 q→qd 绕开 ASYNC_BLOCK 误判 + 补 6 个 locale 同步 CI 两处失败: 1. ruff async-blocking 把 `q.get(...)` 启发式当成 queue.Queue.get() 报 ASYNC_BLOCK。 把 clarify() 里的循环变量 q 改名 qd(question dict),加注释提醒后续别再叫 q。 2. i18n-sync 要求 static/locales 8 个语言文件一起改。补齐 es / ja / ko / pt / ru / zh-TW 的 34 个 aiAssist* key,hunk 范围与 en.json / zh-CN.json 对齐。 本地跑 `python scripts/check_i18n_sync.py --base origin/main` 已通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): 处理 PR review 6 条 finding(field key 错位 / 保存失败误报 / 引号对 / Step2 旧答案) 回应 Codex + CodeRabbit 在 Project-N-E-K-O#1419 的 inline review: 1. [Codex P2 / prompts:142] LLM 强行返回中文 key(性别/年龄/...),但 英文 locale 模板用 Nickname / Gender / Age 等英文 key,Apply 时按 `textarea[name=...]` 精确匹配 → 平行插入新字段、旧字段不被覆盖。 - 前端用 `_cardAssistCollectFieldKeys` 把 form 上所有 textarea/input 的 name(包括空值)收集成 `target_field_keys` 一起发到 router。 - Router 加 `_resolve_target_keys` 三级回退:前端列表 → current_card keys → locale 默认(zh / en)。 - Prompt 模板的 generate zh / en 都改成「目标字段名 %s,必须 1:1 原样使用」, 不再硬编码 9 个中文 key。 2. [Codex P2 / js:10576 + CodeRabbit major / js:10241] saveCatgirlFromPanel 各个失败分支只是自己 showMessage 然后 return,不抛错,导致 Apply & Save 即便保存失败也会关向导 + 弹成功提示。 - saveCatgirlFromPanel 显式 return true/false。 - applyBtn 改成 `const saved = await save(...); if (!saved) return;`。 - `_cardAssistFetch` 的 `e.code` 改成 `body.code || body.error`,兼容 其他接口的 {code, error} 形状。 3. [CodeRabbit minor / router:321] refine 输出剥引号用 `text[0] == text[-1]` 挡不住 Unicode 配对引号(U+201C 左 vs U+201D 右)。改成显式 open→close 映射,多覆盖 `「」`/`『』`/`""`/`''`。 4. [Codex P2 / js:10418] Step3 用 Back 回到 Step2 时 state.answers 还留着 上一轮答案,但 chip / customInput 全渲染成空,导致用户以为已清空、 生成请求却仍带旧答案。从 state.answers 回填 chip selected / customInput value。 本地 ruff + check_async_blocking + check_i18n_sync + JS 语法 全部通过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(i18n): pt.json typo + 术语对齐(regerar→regenerar,API Key→chave de API) 回应 CodeRabbit 两条 minor finding: - aiAssistStep3Hint 里 "regerar" 是拼写错误,应为 "regenerar" - aiAssistApiMissing 里的 "Configurações de API Key" 改成 "Configurações de chave de API", 与同文件 line 851 (settings) / 906 (assistApiKeyQwen) 等的术语保持一致 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): 新建卡空档案名 warning 后不再叠 success toast 回应 Codex P2 Project-N-E-K-O#1419 (comment_id=3265601710): 当 isNew && 档案名为空时,apply 路径在 warning 之后会继续走 `_cardAssistClose` + 成功 toast,用户会被 warning + success 双弹窗 误导成「已落库」。其实这条路径只把字段写到了 form,没调 saveCatgirlFromPanel。 修法:warning 后立刻 `_cardAssistClose(shell.overlay) + return`, 跳过下面的 success toast(表单仍保留 AI 生成内容,用户填档案名后 正常点保存即可)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(card-assist): 抽出 _RESERVED_CARD_FIELDS 让 format / target_keys 行为一致 回应 CodeRabbit nitpick Project-N-E-K-O#1419 (review 4318164227): - `_format_card_for_prompt` 之前 filter 7 个保留字段 - `_resolve_target_keys` 之前 filter 同样 7 个 + "档案名" 两套独立写法导致逻辑漂移。把保留字段抽出 `_RESERVED_CARD_FIELDS` 常量 + `_is_reserved_card_field()` helper,两处都走同一个判定("档案名" 现在两处都过滤)。注释里写明 `档案名` 是 form 元数据 input 的固定 中文 literal name,不是按 locale 翻译的字段。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): 兜底 target_field_keys 按 locale 读模板(ja/ko/pt/ru/es/zh-TW) 回应 Codex P2 Project-N-E-K-O#1419 (comment_id=3265669416): 之前 `_DEFAULT_TARGET_KEYS_BY_LANG` 只硬编码了 zh/en。当 UI locale 是 ja/ko/pt/ru/es 时,前端发的 locale 会被 `_resolve_language` 收敛成 "en", 然后兜底字段就成了英文的;zh-TW 会被吃成 zh-CN 简体字段。空白新建卡 (`_cardAssistCollectFieldKeys()` 返回空)就会让 /generate 用错 key set, 生成出的卡和当前 locale 的模板对不上。 修法: - 新增 `_resolve_locale_code()` 把前端 locale 映射到 `config/characters/<x>.json` 的实际文件名(en / zh-CN / zh-TW / ja / ko / pt / ru / es 八种 + primary-subtag 兜底)。 - 新增 `_load_template_keys_for_locale()` (lru_cache) 直接从该 locale 的模板文件读字段名顺序,比硬编码列表自动跟住模板演进。 - `_resolve_target_keys` 兜底链:payload → current_card keys → locale 模板字段 → 硬编码 en(仅在文件读取彻底失败时兜底)。 - `_resolve_language` 仍然只走 zh/en(prompt 模板只有这两版), 注释里写明分工。 本地 smoke:8 个 locale + 各种变体(en-US / ja-JP / zh-Hant / pt-BR / es-MX / 未知 locale)都解析正确。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): 处理 CodeRabbit full review 7 条 finding 回应 Project-N-E-K-O#1419 full review: 后端 (main_routers/card_assist_router.py): - [Minor 3266209617] _invoke_assist 把 ainvoke/aclose 分开 try,aclose 抛错 不再误把成功的 resp 当 llm_call_failed 丢掉 - [Major 3266209632] /generate 输出清洗用 `_is_reserved_card_field`, 挡住 LLM 误吐 "档案名"/"voice_id"/"system_prompt" 把表单元数据污染 - [Major 3266209640] /refine 入口校验 field_key 不能在 _RESERVED_CARD_FIELDS, 防止客户端绕过来 refine 系统字段,返回 field_key_reserved (400) 前端 (static/js/character_card_manager.js): - [Major 3266209652] state 加 pendingRefines Set;_cardAssistRefineField 把任务塞进去;applyBtn 在写表单前 `await Promise.allSettled(pending)`, 避免「点完更傲娇秒点应用并保存,落库还是旧值」的 UI/状态分叉 CSS (static/css/character_card_manager.css): - [Minor 3266209645] `word-break: break-word` → `overflow-wrap: break-word` (.card-assist-row-key / .card-assist-row-original 两处) 韩语 (static/locales/ko.json): - [Minor 3266209659] aiAssistRefineRandom "재무작위" → "다시 랜덤" - [Minor 3266209684] aiAssistApiMissing "API Key 설정" → "API 키 설정" (与同文件其它位置 "API 키" 风格统一) 本地 ruff / async-blocking / JS 语法 / i18n-sync 全过。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(i18n): ko.json aiAssistRefineRandomInstr 漏改的 "재무작위화" → "무작위화" 回应 CodeRabbit (comment_id=新一轮):之前修 `aiAssistRefineRandom` 为 "다시 랜덤" 时漏改了同一组的 instruction 文案 —— "이 필드를 완전히 재무작위화" → "이 필드를 완전히 무작위화"。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor: replace AI Assist with AI Companion in character card generation flow - Updated English and Chinese localization files to reflect the new AI Companion terminology. - Removed old AI Assist steps and descriptions, replacing them with a more conversational AI Companion interface. - Enhanced user interaction prompts for a smoother character creation experience. * fix(card-assist): 处理 PR review 5 条 finding(state sync / i18n parity / 其他) 阻塞: - _companionEnsureLiveForm 换 state.form 时同步 state.isNew / state.originalName 到 liveForm._isNew / _catgirlName;isNew 翻 false 时清掉 _warnedNewCardSaveHint。 否则用户首次手动保存 blank card 之后 companion 会把已存在的卡当成新卡: auto-save 永远 bail(save-hint 反复弹),万一走到 saveCatgirlFromPanel 会 effectiveIsNew=true 触发 POST 而不是 PUT,造成同名 catgirl 409。 - i18n 缺漏:6 个 locale(es/ja/ko/pt/ru/zh-TW)之前只补了旧 wizard 时代的 aiAssistStep* 那一套(实际已是死代码),完全没补 aiCompanion* 一套。 这 6 个 locale 用户切语言后 companion UI 整套 fallback 回中文/英文。 → 翻译并补齐 aiCompanion* 29 个 key(含 2 个新增 seed key),同时把 32 个未使用的 aiAssist* 键删干净。en/zh-CN 只补了 2 个新增 seed key。 Nit: - chatHistory seed 之前硬编码中文("基于这段描述生成猫娘卡:"),英文 locale 用户走完澄清问答后 LLM 会被 seed 镜像成中文回复。改成走 i18n key aiCompanionSeedDescribe / aiCompanionSeedGenerated。 - prompts_card_assist.py 头部 docstring 还写「3-stage AI assistant」但实际 已有 chat 端点,改成 4-stage(追加 chat 一条)。 - companion avatar URL 去掉 ?t=Date.now() cache-bust:稳定静态图,让浏览器 HTTP cache 接管,多次开关 companion / 切换猫娘不用反复拉图。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): companion auto-save 失败时给系统气泡兜底(Codex P2 #2) 之前 _companionTryAutoSave 用 try{}catch{} 把 saveCatgirlFromPanel 的 return 值全吞了,但那函数 HTTP 非 2xx / success:false / 网络异常都是 toast + return false 而不是抛错。结果:companion 系统气泡仍显示「✎ 已应用:xxx」,体感是 「字段改了 + 已保存」,但其实后端拒绝了。 现在读回 ok 值,false 时再补一条 system 错误气泡。`undefined` 表示 form debounce 已经在跑了的早 return,不算失败,跳过提示避免误报。 新增 i18n key aiCompanionAutoSaveFailed,8 个 locale 全配齐。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): companion form-rebind 兜底(Codex round 2: P2 #A + P2 #B) A) _companionEnsureLiveForm 在 state.originalName 为空(空白新卡开 companion) 时也能找回 form: - 之前:blank card → state.originalName='' → `getElementById('catgirl-form-' + '')` 永远 null → 早 return false → 用户填档案名后手动 Save → 卡被建出来、form id 从 catgirl-form-new 变成 catgirl-form-<actualName>,但 companion 永远 找不到,永久陷在 "form gone"。 - 现在:originalName 空时 fallback 到 `document.querySelector('.catgirl-panel-right form[id^="catgirl-form-"]')`, panel 同时只能有一个 form,所以这条选择器命中唯一;再走原有的 isNew / originalName 同步逻辑。 B) _companionRunClarify / _companionRunGenerate / _companionRunChat 都改为检查 _companionEnsureLiveForm 的返回值,false 时直接 typing.remove() + 系统气泡 "form gone" 然后 return,不再白白发起 LLM 调用。之前忽略返回值,导致: 即使 backend 把 reply + actions 算好返回,前端 apply 阶段也只能弹「form gone」,钱白花、用户体验冲突。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): stale chip + closed-companion late-apply 双兜底(Codex round 3) A) stale question chip 防 race(_companionRenderNextQuestion) bubble 渲染时 capture 自己的 question idx + id 到 closure。chip onClick 触 发时若 state.currentQuestionIdx 已经因为用户走输入框 / 点了别的 chip 推进 到下一题,no-op 这次点击 —— 否则会: - 把 stale 答案塞进 collectedAnswers (覆盖原本的答案) - 再次 ++currentQuestionIdx (跳过下一题没被回答) 旧 bubble 在 thread 里持续可见可点,必须防一手。 B) closed companion 之后丢弃 in-flight LLM 结果 _companionTeardown 现在 set state.closed = true;3 个 runner(clarify / generate / chat)在 `await _cardAssistFetch(...)` 拿到 response 之后立即 check state.closed,如果 true 就直接 return —— 不 typing.remove、不写 form、不 autoSave。 场景:用户点开 companion → AI 在写草稿 → 慢网络 → 用户耐心耗尽关掉 panel → /generate 终于回来 → 之前会静默把 9 个字段写进 form 并 POST 保存,把 用户「关掉=取消」的意图给覆盖了。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): stale inline custom input 防 race(Codex round 4) round 3 给 chip onClick 加的 ownIdx 防 race 漏了一个对称路径:bubble 自定义 输入框的 Enter 也是一样的 race。用户走输入框 / 新 chip 推进进度后回头在老 bubble 的自定义框按 Enter,会: - 把这次的输入塞进 collectedAnswers[当前题.id](不是 bubble 对应的题) - 再次 ++ currentQuestionIdx 跳过下一题 修法: - _companionAppendAssistant 接受新可选 opts.customSubmit 回调(提供时优先 用它,否则继续走通用 _companionHandleUserText 兜底)。 - _companionRenderNextQuestion 传一个施加 ownIdx 防 race 的 customSubmit — 跟 chip onClick 内的检查同形。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): 拒绝非 object JSON payload(Codex round 5) `await request.json()` 接受任意合法 JSON 类型(list / str / int / null 都 过),4 个 endpoint 后面都假设 body 是 dict 然后 `body.get(...)`,碰到 `[]` / `"x"` / `123` / `null` 之类的合法但非 object 输入会 AttributeError 飙到 500。 四个 endpoint(clarify / generate / refine / chat)的 try/except 之后统一 加 isinstance(body, dict) 检查,不是 object 就 400 invalid_json + 明确 message "JSON body must be an object",符合 client error 语义。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): refine_field 找不到目标时 skip 而不是创建(Codex round 6) _companionApplyActions 之前对 refine_field 和 add_field 一视同仁,都丢给 _cardAssistApplyToForm 兜底创建。问题:LLM 偶尔会在 refine action 里把目 标字段名打错 / 大小写漂移("Personality archetype" vs "Personality Archetype"),ApplyToForm 找不到就新建一条 → 重复字段被 autoSave 持久化、 留下脏 schema。 修法:refine_field 进 ApplyToForm 之前先用 _findFieldTextareaByName 精确 (三层 fuzzy match)查一次,找不到直接 push 进 skippedTags 走「⤬ 未匹配/ 已保留」反馈通路。add_field 不动 —— 那才是真的「需要找不到」的场景。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): companion auto-save 区分 debounce-skip 和真失败(Codex round 7) 上一轮(commit 3bf0b17)`_companionTryAutoSave` 读回 saveCatgirlFromPanel 的 `false` 当成失败、弹"自动保存失败"系统气泡。问题是 saveCatgirlFromPanel 对**两种**完全不同的情况都返回 false: (a) form.dataset.submitting === 'true' 时的 debounce 跳过(用户手动点了 Save,companion auto-save 同时触发 → 这次跳过不算失败) (b) HTTP 非 2xx / success:false / 网络异常等真·失败 两种情况都被当成失败 → 用户手动 Save 跟 companion auto-save 撞车时会看到 误报的"自动保存失败"气泡(实际上 save 还在飞、最终也会成功)。 修法:进 save 之前自己 check 一下 `state.form.dataset.submitting === 'true'`, 是的话静默 return;其它时候 false 才视为真失败。不动 saveCatgirlFromPanel 的返回契约,避免影响它的 9 个其他 caller。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): rename rebind + i18n 占位符 + 文档对齐(Codex round 8 + CodeRabbit) 10 个 file 改动覆盖一组 Codex + CodeRabbit findings: 1. **rename rebind**(Codex 3272690171 / CR 3272709503) _companionEnsureLiveForm 之前 state.originalName 有值时只按 `catgirl-form-<originalName>` 精确查,没 fallback。用户在 companion 开 着时改了档案名 → saveCatgirlFromPanel 用新名 rebuild 表单 → 旧 id 找不到 → companion 永久陷在 "form gone"。 修法:getElementById 失败时 fallback 到 `.catgirl-panel-right form[id^="catgirl-form-"]` 选择器(panel 同时只能 有一个 form 所以命中唯一),下游 sync 逻辑会把 state.originalName 回填 成新名字。这也顺带统一了空白新卡 first-save 的兜底路径。 2. **prompts_card_assist.py docstring 矫正**(CR 3272709496) 头部之前写"四个 prompt 都要求 STRICT JSON"。实际 /refine 单独要求 plain string(无 JSON 包装),让 router 能直接拿到字段值灌进表单 textarea。改 成"三个 JSON / refine 是 plain string"。 3. **i18n 占位符 {n} → {{n}}**(CR 3272709508/536/547/562/570 + zh-CN/zh-TW 未被 CR flag 但同样有问题) repo 里 60+ 处 i18n 都是 {{var}} 风格,{n} 是 outlier。即便我 JS 那边 用 .replace('{n}', ...) 兜住了渲染,对 reviewer / 未来维护是 noise。 修法: - JS:删 .replace('{n}', ...),改成 _cardAssistT(key, fallback, { n: count }) —— vars 透传给 i18next 走标准 {{n}} 插值。fallback 字符串内联数字避免 i18next 没加载时漏出字面量。 - 8 个 locale:{n} → {{n}} 同步替换。 4. **es / pt aiCompanionSub 去 dev-jargon**(CR 3272709531) "gato dev" 读起来像内部备注,改成 "gatito/gatinho desarrollador/ desenvolvedor" —— 仍保留 dev-cat 概念(这是产品未来角色的占位),但语感 是「开发小猫」而不是程序员行话。ja/ko/zh-* 的"开发猫/開発猫/개발 고양이" 作为产品概念保留,不动。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): companion auto-save 等 in-flight save + 重放丢失字段(Codex P1) 上一轮 (722ada8) 的简化"in-flight 就 return"会让用户的修改静默丢失: T0: 用户手动 Save → saveCatgirlFromPanel 用 T0 的快照起 POST T1: companion 把 AI 应用的新字段写进同一个 form 的 textarea T2: companion tryAutoSave → 看到 dataset.submitting='true' → return T3: 后端 success → buildCatgirlDetailForm() 用 server 返回的数据 (T0 快 照) 重建 form → companion 在 T1 写进去的字段被抹掉 T4: 既没存进后端 / 不在 form 里 / companion 也不知道要重试 —— 静默丢失 修法: 1. 等 in-flight save 收尾:轮询 dataset.submitting 翻 'false'(8s 超时兜底) 2. wait 完调一次 _companionEnsureLiveForm,可能 rebuild 出来的新 form 接上 3. 如果 form 实例换了,比对 state.formWatchSnapshot(tryAutoSave 调用前刚 refresh 过,代表 companion 期望状态)和新 form 的实际值,把丢失/被抹掉 的字段 replay 一遍(仅 value diff 才 apply 避免覆盖 server 端合法数据) 4. 再调 saveCatgirlFromPanel 把 companion 的修改真正落盘 debounce-skip 的误报问题保留之前的解决(companion 不再把 false 当真失败的 唯一情况就是它自己已经 wait 完的场景;如果 wait 8s 后还 submitting,给 console.warn 然后放弃,不再弹气泡)。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(card-assist): companion auto-save 两条 race fix(Codex P1 + CR Major) 刚才的 ff6b4b1 引入 wait+replay 链路时漏掉了两个 meta-bug,两个都会让在 manual-save 与 companion-apply 撞车场景下静默丢失数据: 1. CR Major: state.formWatchSnapshot 被 _companionAttachFormWatchers 在切 到新 form 时**重写**(line 10968),把"companion 期望状态"覆盖成"刚 rebuild 完的旧值"。结果下面那条 `state.form !== formBeforeWait` 分支里 的 diff 永远比不出差异,replay 哑火、数据照样丢。 修:进 wait loop 之前把 snapshot **defensive 拷贝**到本地 expectedSnapshot 变量,不再依赖会被覆写的 state slot。 2. Codex P1: snapshot 只记得到字段值,不记得到 "companion 故意删除了这个 key"。companion 的 remove_field action 删掉的字段不在 snapshot 里 → server rebuild 后那些字段又冒出来 → 既不替换又不删除 → AI 删的字段被 静默复活并 PUT 回 server。 修:_companionApplyActions 把每次 apply 的 4 类结果(updated / created / removed / skipped)挂到 state._lastApplyResult;_companionTryAutoSave 在 wait 前也 defensive 拷贝一份 removed 名单,rebind 后单独跑一轮删除 replay。字段值 replay 那一路顺手跳过 removed 集合,避免一删一写自己跟自 己打架。 最后 replay 完顺手 _companionRefreshFormSnapshot 一次,把新 baseline 同步 给后续 form-watch listener,防止把 replay 误判成"用户手改"狂刷系统气泡。 state._lastApplyResult 用完即清,避免下一次 tryAutoSave 误用过期记录。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * Improve character card companion behavior * fix(card-assist): companion 动画补 reduced-motion + remove_field 亮 Save(CR Minor + Codex P2) - CSS(CodeRabbit #3328757121):现有 @media (prefers-reduced-motion: reduce) 没覆盖陪伴面板的新动画。补一段兜底,把窗口入场 cardCompanionWindowIn、最小化 呼吸光 cardCompanionBallGlow、最小化↔展开的收/展几何过渡(collapsing/expanding)、 气泡入场 cardCompanionBubbleIn、typing dots cardCompanionDot、字段闪烁 cardAssistRowFlash* 全部 animation/transition 静音。收/展终态切换由 JS setTimeout(260) 收尾、不依赖 transitionend,故 transition:none 后是「瞬移就位」 不会卡住;各元素基础态可见、无需复位 opacity/transform。与文件里 @1744 / @6415 两处 reduced-motion 兜底同一套语义。 - JS(Codex #3272286467):remove_field 直接删 DOM 行,不走 _cardAssistApplyToForm 末尾那段「亮 Save/Cancel」。已保存卡这两个按钮默认 display:none,一旦 autosave 失败、 系统气泡提示「请手动点 Save 重试」时按钮还藏着 → 用户无从重试、删除在 reload 后复活。 改成:纯删除场景也把 Save/Cancel 显式亮出,让 fallback 提示可操作。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): add free API watermark 为 AI 辅助捏人的免费 Lanlan 文本调用补充通用安全水印,避免免费端误判为非 Lanlan 请求。 同时收敛捏人助手输入框滚动条隐藏逻辑,并用 transform 优化 YUI 头像的下移和放大效果。 验证: - .\.venv\Scripts\python.exe -m py_compile main_routers\card_assist_router.py - git diff --check - /api/card-assist/clarify 免费端请求返回 200,未再出现 STOP ABUSE THE API * fix(card-assist): companion 防误绑别的卡 + AI 字段删除亮 Save(Codex P2 ×2) - #3328901017:_companionEnsureLiveForm 的选择器回退(path-2)会把 A 的聊天误绑到 另一张卡 B —— closeCatgirlPanel 不销毁 companion 侧栏,用户关掉 A 的详情面板、打开 B 的面板后在仍可见的 companion 里输入,回退就抓到 B 的 form,后续 action/autosave 改 错卡。改法:closeCatgirlPanel 时给 companion 打 _detailPanelClosed 标记,path-2 仅在 标记为 false 时允许;按确切档案名命中的 path-1(安全、确定同卡)清掉标记。合法的 in-place rebuild(新卡首存 popup 被拦走 rebuildSavedCatgirlPanel、改档案名字段后 saveCatgirlFromPanel 重建表单)都不经过 closeCatgirlPanel,回退照常生效;同卡关掉再开 由 path-1 兜住。 - #3328901018:_cardAssistApplyToForm 新建字段的删除按钮只 wrapper.remove(),不像普通 自定义字段删除路径(~5041)那样亮 Save/Cancel。已保存卡上删掉 AI 新建字段后既不触发 autosave、也没有可见的手动保存入口,reload 后字段复活。改成删除时一并亮出 Save/Cancel。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): autosave 不甩面板 + replay 删除亮 Save(Codex P2 ×2) - #3328942156:_companionTryAutoSave 的 _autoCreated 卡走到 saveCatgirlFromPanel 时仍 传 state.isNew=true。helper 的请求方法本就按内部 effectiveIsNew(=isNew && !_autoCreated) 走 PUT,没问题;但它**保存成功后的 UI 分支**按原始 isNew 判定,会 closeCatgirlPanel / 开卡面制作弹窗,把正在进行的 companion 聊天打断。改成按"是否已落库"传 effectiveIsNew (_autoCreated 视作已保存卡),post-save 走原地刷新、不甩面板;请求方法不变。 - #3328942158:in-flight save 的 replay 分支里,对 rebuilt 表单执行「删除」replay 后没 重新亮 Save/Cancel。若只 replay 了删除、没 replay 字段值,_cardAssistApplyToForm 不会 被调到、不顺带亮按钮;紧接着 autosave 一旦失败、提示「手动点 Save 重试」时按钮却藏着 → 删除丢失、reload 后字段复活。跟直连 remove_field / AI 字段删除两条路径一致,补上亮按钮。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 关面板即 teardown companion + 等手动保存真收尾(CR Major + Codex P1) - 防误绑改用 teardown-on-close(取代上一版 _detailPanelClosed 一刀切):CodeRabbit 指出 flag 封死 path-2 会让 companion 在重命名 / 新卡首存后「同卡也回不来」永久失联。关键事实是 openCatgirlPanel 顶部有 _catgirlPanelOpen 互斥——打开/切换到别的卡**必须先 closeCatgirlPanel**。 所以改成 closeCatgirlPanel 时直接 _companionTeardown + _companionDestroy + 置 null:跨卡误绑 从根上消除,且不再有 stranded 的 form-gone 死局。合法 in-place rebuild(改档案名字段后重建、 新卡首存 popup 被拦走 rebuildSavedCatgirlPanel)不经过 closeCatgirlPanel,companion 照常跟随, path-2 回退恢复无条件、安全(Codex #3328901017 + CodeRabbit Major @4845/11349)。 - #3328951294 P1:autosave 的 wait loop 之前盯 state.form.dataset.submitting,可手动保存 PUT 成功一触发重建,_companionEnsureLiveForm 就把 state.form 重绑到新 form(submitting 未置位)→ 循环提前 break、抢在手动保存 finally(还要 loadCharacterData + 二次重建)收尾前 replay+自存 → 被后续重建覆盖、退回静默丢失。改成盯原始 formBeforeWait.dataset.submitting 清掉再继续。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): autosave 等待超时不再静默放弃(Codex P2 #3328963563) wait loop 等手动保存收尾超过 8s 时,之前只 console.warn 后 return。那次慢保存收尾会用 较旧的请求快照重建表单、覆盖掉 companion 刚写进去的改动/删除,而用户只看到之前那条 「已应用」气泡、误以为存好了。改成:补一条 aiCompanionAutoSaveFailed 失败气泡讲清楚, 并尽量把 Save/Cancel 亮出来给手动兜底入口(那次保存最终失败、没重建表单时这俩按钮即重试 路径)。复用既有 i18n key,不新增 locale 项。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 抬高 chat action 上限避免「重写全部」被静默截断(Codex P2 #3328971304) _CHAT_MAX_ACTIONS 原为 8,但默认模板就有 9 个可见字段(昵称/性别/年龄/种族/自称/核心特点/ 行为特点/厌恶/一句话台词)。「重写全部」quick action 会一字段一个 refine_field 返回,卡在 8 会把第 9 个及之后静默丢掉、autosave 只落库半张卡。抬到 32:覆盖默认 9 字段 + 充裕自定义字段, 仍能拦住真正失控的超长 action 列表。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 给四个 LLM 端点加本地 Origin/CSRF 守卫(Codex P2 #3328998416) card-assist 的 clarify/generate/refine/chat 都会真去打用户配置的对话/辅助 LLM、花 API/免费额度,却没走仓库里其它有副作用浏览器端点的统一本地请求校验。localhost 部署下 恶意网页可用 no-cors + text/plain body 伪造合法 JSON,request.json() 照样解析、白嫖配额 (攻击者读不到响应但能烧额度)。 - 后端:复用 system_router 的 _validate_local_mutation_request(issue Project-N-E-K-O#1479 统一守卫), 四个端点在 dict 校验后、调 LLM 前先过 Origin+CSRF;不通过返回 403 csrf_validation_failed。 system_router 不反向依赖 card_assist,无循环导入。 - 前端:_cardAssistFetch 带上 X-CSRF-Token。本页(character_card_manager.html 独立页)不 加载 app-prompt-shared.js → 没有 nekoLocalMutationSecurity,故自包含地从 /api/config/page_config 取 autostart_csrf_token 缓存(与本页 universal-tutorial-manager.js 同源);主 app 上下文里若有统一安全助手则优先用它。Origin 由浏览器同源 POST 自动带。 - 测试:tests/unit/test_card_assist_csrf.py канary——四个端点无 Origin/CSRF 必返 403 csrf_validation_failed,防止守卫被误删。本地实测 4 passed,且带合法 Origin+token 能穿过守卫。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 新卡「Save 与 /generate 竞态」保住 AI 字段(Codex P2 #3329022313) _companionTryAutoSave 的新卡 guard 之前无条件 early-return,不等也不 replay。竞态: 用户在 /generate 还在飞时点了 Save → saveCatgirlFromPanel 已用「AI 写字段之前」的旧快照 序列化好 → AI 把字段写进同一张新卡表单 → 这里 guard 直接 return → 保存成功后用旧快照 rebuild/关面板,把用户已看到「已应用」的 AI 字段静默丢掉。 改法: - 新卡 guard 增加 `dataset.submitting !== 'true'` 条件——已有手动 Save 在飞行中时不 return, 落到下面的 wait/replay:等那次 Save 收尾、把 AI 字段 replay 到 rebuild 出来的已保存卡表单 再存一遍(与已保存卡路径同一套机制)。 - saveCatgirlFromPanel 前加二次 guard:等待后若卡仍未落库(save 失败/没建成),绝不 POST 建卡 + 关面板,AI 字段留在表单、提示手动 Save。 - 关面板竞态(首存 popup/有卡面 → closeCatgirlPanel)下 companion 已被 teardown,wait 循环里 ensureLiveForm 返回 false 优雅退出,无副作用。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 生成中禁掉新卡 Save,从源头堵住竞态丢字段(Codex P2 #3329817833) 承接 #3329022313 的后续:在「新卡首存成功后走 closeCatgirlPanel(开卡面制作 / 已有卡面)」 的分支里,面板关闭会 teardown companion,wait/replay 的 _companionEnsureLiveForm 返回 false 直接退出,AI 字段救不回来——这种「关面板」竞态没法在事后挽回。 按 Codex 给的「manual-save-disable during generation」方向从源头预防:_companionSetBusy 里, 未落库新卡(isNew && !_autoCreated)在 companion 打 LLM 期间禁掉详情表单 Save,用户就无法在 「/generate 还没把字段写进表单」的窗口里点 Save,竞态不再被触发。disabled = busy && 未落库新卡, busy 退场或卡一旦落库都会自动放开,不会卡死;用 disabled 属性,与 saveCatgirlFromPanel 的 dataset.submitting 去抖互不干扰。已落库卡不禁(其并发由 wait/replay 安全兜住)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 关闭 companion 时恢复被禁的新卡 Save(Codex #3331627614 / CR Major #3331629488) 上一条(8feb2805)给未落库新卡在 LLM 飞行中禁 Save 的副作用:用户在请求还没回来时点 × 关掉 companion,_companionTeardown 只置 closed / 摘监听,迟到响应的 guard 又直接 return,于是 _companionSetBusy(false) 永远不会被调到,Save 一直灰着直到请求超时(最多 60s)——表单还在 页面上却存不了。在 _companionTeardown 开头无条件把 #save-button 的 disabled 放开即可。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 复用完整保留字段列表,避免保留 key 被静默丢弃(Codex P2 #3331668038) card-assist 前后端各维护了一份不完整的保留字段拷贝,漏了 lighting / live3d_sub_type / vrm_animation / live2d_idle_animation / live2d_item_id 等。这些 key 在 chat/add_field 里 被当普通 AI 字段渲染、autosave 报成功,但保存时被 collectCharacterFields / _filter_mutable_catgirl_fields 丢掉 → 刷新后行消失、用户改动静默不持久化。 - 后端:_RESERVED_CARD_FIELDS 改成 frozenset(CHARACTER_RESERVED_FIELDS)(角色编辑器/保存 过滤同源)∪ {档案名, live3d},不再维护部分拷贝。 - 前端:去掉写死的 CARD_ASSIST_RESERVED_KEYS,新增 _cardAssistIsReservedKey(),复用角色编辑器 同一套 isCharacterReservedFieldName(后端实时配置 + ReservedFieldsUtils 兜底)+ '档案名', 6 处调用点统一切过去。 - 实测:lighting/live3d_sub_type/vrm_animation/live2d_idle_animation/档案名/live3d 均判为保留, 性别/Gender 等真设定字段仍可改;CSRF canary 4 passed。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 给 prompt 没专门版本的 locale 加输出语言指示(Codex P2 #3331696257) prompt 只写了 zh/en 两版:ja/ko/ru/pt/es 落到 en、zh-TW 落到简中,且没有「用该语言回答」的 指示——于是这些 locale 下助手用英文/简中提问、把字段值也填成英文/简中,尽管字段 key 已按 locale 模板给定。 按 Codex 给的方向二落地:新增 _LOCALE_OUTPUT_LANGUAGE 映射 + _output_language_directive(), 对 ja/ko/ru/pt/es/zh-TW 在 prompt 末尾追加一条显式输出语言指示(要求 questions / field values / 说明都用目标语言、保持 JSON 结构与 key 不变);en/zh-CN 与基础 prompt 一致返回空串。 clarify/generate/refine/chat 四端点都接上。 实测各 locale 指示正确,en/zh-CN 不加;CSRF canary 4 passed。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Improve free card assist action reliability * 头像位置修正 * Unify card assist action recovery * fix(card-assist): generate 草稿应用前重新 ensure live form(Codex P2 #3332998069) _companionRunGenerate 在 /generate 的 await **之前** ensure 过 form,但返回后直接 _cardAssistApplyToForm(state.form, ...)。若期间表单被 rebuild(用户改名/保存先于模型返回 完成、旧 form detach),draft 就写进了 detached DOM;紧接着 _companionTryAutoSave rebind 到 新 form,但 generate 这条路没有 in-flight replay 通路,于是把不带 draft 的表单存下去,助手 却报「已应用」→ 字段静默丢失。 修法:apply 前再 _companionEnsureLiveForm(state) 一次,写进当前活着的 form;接不上就报 form-gone、不写 detached DOM。chat 路径的 _companionApplyActions 开头本就重新 ensure,不受影响。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 全量重写用 locale 无关 flag + 新卡问答阶段也禁 Save(Codex P2 ×2) - #3333137718:「重写整张卡」靠后端正则匹配 latest_user 文本判定全量重写意图,只覆盖 简中/繁中/英文;es/ja/ko/pt/ru 的本地化文案匹配不到 → _complete_full_rewrite_actions 补全通路不触发,部分 action 被当部分重写存下。改成前端 quick action 透传 locale 无关的 full_rewrite flag,后端优先读 flag(保留文本启发式兼容手敲措辞)。 - #3333137733:上一版(8feb2805)只在 busy 时禁未落库新卡的 Save,但 clarify 返回后、 问答阶段 busy=false,Save 又放开;用户此时点 Save 再答完最后一题 → /generate 应用字段、 等 in-flight save,仍可能在首存成功关面板分支里丢字段。把 Save 禁用条件挪进 _companionUpdateQuickAvailability(busy + 每次 mode 切换都会同步),改成 未落库新卡 && (busy || mode !== 'chat'):整个问答/生成流程都禁,进 chat 模式(草稿已落 表单)才放开;已落库卡不禁;关 companion 时 teardown 仍无条件恢复。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): full_rewrite flag 也触发 action 恢复(Codex P2 #3333394174) 承接 #3333137718:_recover_actions_from_reply 的 gate 只看 edit_intent,而它走 _CHAT_EDIT_INTENT_RE 只覆盖中英。本地化「重写整张卡」quick chip(es/ja/ko/pt/ru/zh-TW) 文本匹配不到 → edit_intent=False;若首轮 LLM 又没吐可用 actions(纯文本 / actions:[]), recovery 被跳过、只回一句话、卡没改,辜负了显式 full_rewrite flag(_complete_full_rewrite_actions 只补全已有 actions、空 actions 救不回)。 改成 `(edit_intent or full_rewrite_intent) and not actions` 触发恢复。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): full_rewrite flag 改成真正一次性消费(CodeRabbit Major #3333410664) flag 之前在 form-gone early-return 之后才读+清,若点「重写整张卡」时撞上 form rebuild、 没接上 live form 而提前 return,标记会残留、被下一条普通聊天消息误当成整卡重写。改成在 _companionRunChat 开头(任何 early-return 之前)就读出并清掉。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 新卡首存飞行中短路 chat,避免编辑丢失(Codex P2 #3333457418) chat 模式下 Save 是放开的(草稿已生成)。用户点了首存、又在它收尾前发一条 chat 编辑: saveCatgirlFromPanel 已用「编辑前快照」序列化,这次 /chat 应用的编辑会在「首存成功关面板 / 开卡面」分支里随面板一起没掉,_companionTryAutoSave 又 rebind 不上 → 编辑静默丢失。 _companionRunChat 在打 LLM 前加 guard:state.isNew && !state.form._autoCreated && dataset.submitting === 'true' 时短路,弹一条 aiCompanionSaveInProgress 提示让用户等保存收尾 (消息还在 chatHistory 里,存好后再发即可)。问答/生成流程下 Save 本就被禁,这里只兜 chat 这条路。 新增 aiCompanionSaveInProgress i18n key,8 个 locale 全量补齐。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): use agent model without watermark * fix(card-assist): reserve free agent quota * fix(agent): free agent quota 改按实际 agent model 判定,不再绑 IS_FREE_VERSION 问题:本地每日配额按全局 IS_FREE_VERSION 判定。但 core/assist 已解耦——core=free + assist=付费、或自定义付费 agent model 时,IS_FREE_VERSION 仍为 True,导致用户用着 自费模型却被免费试用配额(每日 500)拦截。 改动: - consume_agent_daily_quota 的 gate 从 is_free_version() 改为「实际 agent model == free-agent-model」(get_model_api_config('agent'))。自费/自定义 agent model 自然放行; 自定义覆盖也被尊重。IS_FREE_VERSION 保留给免填 key、藏云端模型、默认音色等其它职责。 - 摘掉 task_executor 4 处 + deduper 1 处 quota 调用:这些判定器走的是 summary/emotion 模型而非 agent model,本就不该计入 agent 配额;删 _check_agent_quota 方法。 - 真正走 agent model 的 computer_use / browser_use / card_assist 保留,按新 gate 计数。 - 单测:test_agent_quota_notify 改 mock get_model_api_config;新增「自费 agent model 即便 IS_FREE_VERSION=True 也放行」回归;清理 stage1_filtering 里失效的 _check_agent_quota stub。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): companion teardown 后阻止迟到 finally 重新禁用 Save (Codex #3333702549) 未落库新卡的 companion 在 /clarify 飞行中被关闭时:_companionTeardown 已无条件恢复 详情表单的 Save 并置 state.closed,但迟到的 clarify finally 仍会 _companionSetBusy(false) → _companionUpdateQuickAvailability,按「未落库新卡 + 非 chat 模式」规则把 Save 又禁回去。 此时 companion 已销毁、详情面板还开着,用户再也点不动 Save。 修法:_companionUpdateQuickAvailability 开头加 state.closed 早退,teardown 后不再碰表单控件。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(card-assist): 澄清失败回退后放开未落库新卡的 Save (Codex #3333683160) 未落库新卡的 Save 原先在 mode !== 'chat' 时一律禁用,但 awaiting_description(companion 首启 + /clarify 失败回退到此)并没有任何在途生成——禁 Save 零竞态收益,只把用户困住: 澄清失败(如没配 API)后想放弃 AI、手动建卡却点不动,得先关 companion 才行。 改为只在 busy 或 asking_questions(答最后一题即触发生成)时禁 Save,放开 awaiting_description。 保留 #3329022313/#3329817833/#3333137733 的「答题/生成期间禁 Save」保护;手动 Save 撞后续 生成的竞态本就由 busy + _companionTryAutoSave 的 wait/replay 兜底。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: breakkh811-star <breakkh811@gmail.com> Co-authored-by: yiyiyiyiGKY <17858325501@163.com> Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com>
1 parent 7e8657e commit 1aee875

25 files changed

Lines changed: 4937 additions & 130 deletions

app/main_server.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,6 +1641,7 @@ async def get_response(self, path, scope):
16411641
from main_routers.workshop_router import router as workshop_router # noqa
16421642
from main_routers.cookies_login_router import router as cookies_login_router # noqa
16431643
from main_routers.game_router import router as game_router # noqa
1644+
from main_routers.card_assist_router import router as card_assist_router # noqa
16441645
from main_routers.debug_router import router as debug_router, start_watchdog as _start_debug_health_watchdog # noqa
16451646
from main_routers.shared_state import init_shared_state, set_steamworks_initializer # noqa
16461647

@@ -1772,6 +1773,7 @@ async def proxy_user_plugin_market_bridge(request: Request, path: str = ""):
17721773
app.include_router(music_router)
17731774
app.include_router(galgame_router)
17741775
app.include_router(game_router)
1776+
app.include_router(card_assist_router)
17751777
app.include_router(capture_router)
17761778
app.include_router(cookies_login_router) # Cookies登录相关路由,放在最后以避免与其他API路由冲突
17771779
app.include_router(debug_router) # 诊断观测:/api/debug/health(轻量、零侵入,详见 debug_router.py 头注释)

brain/deduper.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,6 @@ async def judge(self, new_task: str, candidates: List[Tuple[str, str]]) -> Dict[
5050

5151
for attempt in range(max_retries):
5252
try:
53-
ok, info = await get_config_manager().aconsume_agent_daily_quota(
54-
source="deduper.judge",
55-
units=1,
56-
)
57-
if not ok:
58-
logger.warning(
59-
"[Deduper] Agent quota exceeded: used=%s, limit=%s",
60-
info.get("used"),
61-
info.get("limit"),
62-
)
63-
return {"duplicate": False, "matched_id": None}
6453
set_call_type("dedup")
6554
resp = await self.llm.ainvoke([
6655
{"role": "system", "content": "You are a careful deduplication judge."},

brain/task_executor.py

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,6 @@ async def _ensure_short_descriptions(self, plugins: List[Dict[str, Any]]) -> Non
297297
try:
298298
llm = self._get_llm(temperature=0, max_completion_tokens=AGENT_PLUGIN_SHORTDESC_MAX_TOKENS)
299299
for p in to_generate:
300-
quota_error = await self._check_agent_quota("task_executor.ensure_short_desc")
301-
if quota_error:
302-
logger.debug("[Agent] Stopping short_description generation: quota exceeded")
303-
break
304300
pid = p.get("id", "unknown")
305301
try:
306302
from config import PLUGIN_INPUT_DESC_MAX_TOKENS
@@ -441,13 +437,6 @@ async def _do_close():
441437
except RuntimeError:
442438
logger.debug("[Agent] No running event loop, skipping async LLM close")
443439

444-
async def _check_agent_quota(self, source: str) -> Optional[str]:
445-
"""免费版 Agent 模型每日 300 次本地限流(async,避免事件循环阻塞)。"""
446-
ok, info = await self._config_manager.aconsume_agent_daily_quota(source=source, units=1)
447-
if ok:
448-
return None
449-
return json.dumps({"code": "AGENT_QUOTA_EXCEEDED", "details": {"used": info.get('used', 0), "limit": info.get('limit', 300)}})
450-
451440
def _format_messages(self, messages: List[Dict[str, str]]) -> str:
452441
"""格式化对话消息"""
453442
def _extract_text(m: dict) -> str:
@@ -973,10 +962,6 @@ async def _assess_unified_channels(
973962
{"role": "user", "content": user_prompt},
974963
]
975964

976-
quota_error = await self._check_agent_quota("task_executor.assess_unified")
977-
if quota_error:
978-
return UnifiedChannelDecision()
979-
980965
response = await llm.ainvoke(messages)
981966
text = (response.content or "").strip()
982967

@@ -1177,10 +1162,6 @@ async def _stage1_llm_coarse_screen(
11771162
{"role": "user", "content": user_text},
11781163
]
11791164

1180-
quota_error = await self._check_agent_quota("task_executor.coarse_screen")
1181-
if quota_error:
1182-
return []
1183-
11841165
response = await llm.ainvoke(messages)
11851166
text = (response.content or "").strip()
11861167
if text.startswith("```"):
@@ -1337,16 +1318,6 @@ async def _assess_user_plugin(self, conversation: str, plugins: Any, lang: str =
13371318
{"role": "user", "content": user_prompt},
13381319
]
13391320

1340-
quota_error = await self._check_agent_quota("task_executor.assess_user_plugin")
1341-
if quota_error:
1342-
return UserPluginDecision(
1343-
has_task=False,
1344-
can_execute=False,
1345-
task_description="",
1346-
plugin_id=None,
1347-
plugin_args=None,
1348-
reason=quota_error,
1349-
)
13501321
response = await llm.ainvoke(messages)
13511322
raw_text = response.content
13521323
# Log the prompts we sent (truncated) and the raw response (truncated) at INFO level

config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"lighting",
4242
"vrm_rotation",
4343
"live2d_item_id",
44+
"live2d_idle_animation",
4445
"item_id",
4546
"idleAnimation",
4647
"idleAnimations",
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
"""
2+
Card-Assist prompt templates.
3+
4+
Used by ``main_routers.card_assist_router`` to drive a card-design AI
5+
assistant with four entry points:
6+
7+
1) clarify — given the user's one-line description, ask 2-4 clarifying
8+
questions (each with chip options + optional free-text)
9+
2) generate — given the description + answers, output a full card field set
10+
3) refine — regenerate a single field given an instruction
11+
4) chat — persistent companion-style chat with structured actions
12+
(used by the right-side companion panel after generate)
13+
14+
Three of the four prompts (clarify / generate / chat) require the LLM to
15+
output STRICT JSON only (no markdown fences); the router strips ```json
16+
fences defensively before json.loads. The `refine` prompt is the exception:
17+
it asks for a **plain string** with no JSON wrapping (so a single field's
18+
new value can be substituted directly into the form textarea), and the
19+
router strips fences + matching quote pairs before returning.
20+
"""
21+
22+
from __future__ import annotations
23+
24+
from config.prompts.prompts_sys import _loc
25+
26+
27+
# Canonical catgirl card field keys (Chinese keys are what's stored in
28+
# characters.json and what the frontend form's textarea name attribute is).
29+
# The English labels in the screenshot ("Gender", "Age", ...) are i18n
30+
# displays of these keys.
31+
CANONICAL_FIELDS = [
32+
"性别", # Gender
33+
"年龄", # Age
34+
"性格原型", # Personality Archetype
35+
"种族", # Race
36+
"自称", # Self-Reference
37+
"核心特质", # Core Traits
38+
"行为特征", # Behavioral Traits
39+
"不喜欢", # Dislikes
40+
"招牌台词", # Signature Line
41+
]
42+
43+
44+
CARD_ASSIST_CLARIFY_PROMPT = {
45+
"zh": """你是猫娘角色卡设计助手。用户给出一句话角色描述,你需要抛出 2 到 4 个最有价值的澄清问题,帮助后续生成完整设定。
46+
47+
用户描述:
48+
%s
49+
50+
已有卡片字段(可能为空,仅供参考):
51+
%s
52+
53+
要求:
54+
- 只挑最关键的 2-4 个维度发问(如年龄段、性格基调、种族细节、说话风格、特殊背景等)。已经有用户提示出来的维度不要再问。
55+
- 每题给 3-4 个互斥的 chip 选项,覆盖常见取向。
56+
- 每题允许自由输入(allowCustom: true)。
57+
- 问题语气活泼自然,符合二次元/猫娘语境。
58+
- 严格按 JSON 返回,禁止 markdown 代码块、禁止任何前后缀文字:
59+
60+
{
61+
"questions": [
62+
{
63+
"id": "q1",
64+
"header": "短标签(≤6字)",
65+
"label": "完整问题文本",
66+
"options": ["选项A", "选项B", "选项C", "选项D"],
67+
"allowCustom": true
68+
}
69+
]
70+
}""",
71+
"en": """You are a catgirl character card design assistant. Given the user's one-line character description, raise 2 to 4 of the most valuable clarifying questions to help generate the full setting later.
72+
73+
User description:
74+
%s
75+
76+
Existing card fields (may be empty, for reference only):
77+
%s
78+
79+
Requirements:
80+
- Pick only the 2-4 most critical dimensions (age range, personality tone, species detail, speech style, special background, etc.). Don't ask about dimensions the user already specified.
81+
- For each question, give 3-4 mutually exclusive chip options covering common choices.
82+
- Each question allows free-text input (allowCustom: true).
83+
- Tone should be playful and natural, fitting the anime/catgirl context.
84+
- Return STRICT JSON only — no markdown fences, no preface or suffix text:
85+
86+
{
87+
"questions": [
88+
{
89+
"id": "q1",
90+
"header": "short tag (<=8 chars)",
91+
"label": "full question text",
92+
"options": ["Option A", "Option B", "Option C", "Option D"],
93+
"allowCustom": true
94+
}
95+
]
96+
}""",
97+
}
98+
99+
100+
CARD_ASSIST_GENERATE_PROMPT = {
101+
"zh": """你是猫娘角色卡设计助手。根据用户的一句话描述 + 多轮澄清答案,生成完整的角色卡字段。
102+
103+
用户描述:
104+
%s
105+
106+
澄清答案(id -> 回答):
107+
%s
108+
109+
现有卡片字段(如有冲突优先采用本次生成结果):
110+
%s
111+
112+
目标字段名(必须**原样**使用这些 key,**不要翻译、不要改写大小写、不要替换近义词**):
113+
%s
114+
115+
要求:
116+
- 必须输出"目标字段名"里列出的**全部**字段,键名 1:1 复制
117+
- 可以追加最多 5 个自定义字段,key 风格保持与目标字段一致(同一种语言/同一种写法)
118+
- 每个字段的值必须是字符串(不要数组、不要对象、不要 null)
119+
- 字段值具体、生动、可游戏化呈现;避免空泛的形容词堆砌
120+
- 招牌台词类字段("招牌台词"/"一句话台词"/"Signature Line" 等)要带猫娘标志(如"喵~"、"呐"、"nya"),不超过 30 字
121+
- "行为特征"/"行为特点"/"核心特质"/"Core Traits"/"Behavioral Traits" 这类字段可以用逗号分隔列出 3-5 个特点
122+
- 严格按 JSON 返回,禁止 markdown 代码块、禁止任何前后缀文字:
123+
124+
{
125+
"fields": {
126+
"<target_key_1>": "...",
127+
"<target_key_2>": "...",
128+
"...": "..."
129+
}
130+
}""",
131+
"en": """You are a catgirl character card design assistant. Generate a full character card based on the user's one-line description plus their answers to clarifying questions.
132+
133+
User description:
134+
%s
135+
136+
Clarification answers (id -> answer):
137+
%s
138+
139+
Existing card fields (this generation takes priority on conflicts):
140+
%s
141+
142+
Target field keys (use these keys **verbatim** — do NOT translate, re-case, or substitute synonyms):
143+
%s
144+
145+
Requirements:
146+
- You MUST output **every** key listed in "Target field keys" exactly as written, 1:1.
147+
- You MAY append up to 5 additional custom keys in the same language/style as the target keys.
148+
- Every field value must be a STRING (no arrays, no objects, no null).
149+
- Field values should be concrete, vivid, gameable; avoid stacks of generic adjectives.
150+
- A "signature line"-style field (e.g. "Signature Line" / "一句话台词" / "招牌台词") should include a catgirl tic (e.g. "meow~", "nya", "喵~"), <=30 chars.
151+
- Traits-style fields (e.g. "Core Traits" / "Behavioral Traits" / "核心特质" / "行为特点") may be a comma-separated list of 3-5 traits.
152+
- Return STRICT JSON only — no markdown fences, no preface or suffix text:
153+
154+
{
155+
"fields": {
156+
"<target_key_1>": "...",
157+
"<target_key_2>": "...",
158+
"...": "..."
159+
}
160+
}""",
161+
}
162+
163+
164+
CARD_ASSIST_REFINE_FIELD_PROMPT = {
165+
"zh": """你是猫娘角色卡设计助手。请对某一个字段进行局部重生。
166+
167+
完整卡片(仅供上下文,不要改其他字段):
168+
%s
169+
170+
目标字段名:%s
171+
目标字段当前值:%s
172+
调整指令:%s
173+
174+
要求:
175+
- 只输出该字段的新值(纯字符串,无引号无 JSON 包装)
176+
- 保持与其他字段的整体调性一致
177+
- 不要输出任何解释、思考过程、markdown 代码块或多余文本
178+
- 长度参考原值,不要无限扩写""",
179+
"en": """You are a catgirl character card design assistant. Regenerate a single field locally.
180+
181+
Full card (context only — do NOT modify other fields):
182+
%s
183+
184+
Target field key: %s
185+
Current value: %s
186+
Adjustment instruction: %s
187+
188+
Requirements:
189+
- Output ONLY the new value as a plain string (no quotes, no JSON wrapper)
190+
- Stay consistent with the overall tone of the other fields
191+
- Do not output any explanation, chain-of-thought, markdown fences, or extra text
192+
- Match the original length roughly — don't balloon out""",
193+
}
194+
195+
196+
CARD_ASSIST_CHAT_SYSTEM_PROMPT = {
197+
"zh": """你是 %s,一只活泼可爱的猫娘助手,正在陪用户捏一只新的猫娘角色卡。你能看到完整的当前卡片字段、可用字段 key 列表,以及最近的对话历史。你会一直陪在用户旁边,看着卡片被一点点填出来,随时给建议、随时按用户的话调整字段。
198+
199+
当前角色卡(用户已经填的内容;可能为空):
200+
%s
201+
202+
可用字段 key(必须**原样**使用这些 key,不要翻译、不要改写大小写):
203+
%s
204+
205+
工作方式:
206+
1) 用 1-3 句话自然地回复用户。语气活泼可爱,可以适度撒娇、用「喵~」「呐」等语气词,但别太腻
207+
2) 如果用户的话语**明确**暗示要修改/补充/删除某个字段,把这些操作打包成 actions 列表
208+
3) 操作合法的 type 只有这三种:
209+
- "refine_field" —— 改写某个已有字段的值(field_key 必须在「可用字段 key」里)
210+
- "add_field" —— 新增一个字段(field_key 可以是新的中文/英文名)
211+
- "remove_field" —— 删除某个字段
212+
4) 绝对不可以触及保留字段:档案名 / voice_id / system_prompt / live2d / live3d / vrm / mmd / model_type
213+
5) 如果用户只是闲聊、问问题、或者还没明确说要改什么,actions 留空数组 []
214+
6) reply 用用户的语言回复(用户用中文你就用中文,用户用英文你就用英文)
215+
7) 严格按 JSON 返回,禁止 markdown 代码块、禁止任何前后缀文字:
216+
217+
{
218+
"reply": "你给用户的话",
219+
"actions": [
220+
{"type": "refine_field", "field_key": "性格原型", "value": "新值", "reason": "为什么改"}
221+
]
222+
}""",
223+
"en": """You are %s, a playful catgirl assistant helping the user build a new catgirl character card. You can see the full current card, the list of available field keys, and the recent conversation history. You stay beside the user the whole time, watching the card take shape, giving suggestions, and adjusting fields when asked.
224+
225+
Current character card (what the user has filled so far; may be empty):
226+
%s
227+
228+
Available field keys (use these **verbatim** — do NOT translate or re-case):
229+
%s
230+
231+
How you work:
232+
1) Reply naturally in 1-3 sentences. Tone should be playful and cute, occasionally using "meow~" / "nya" tics — but don't overdo it.
233+
2) If the user's message **clearly** implies a change to a specific field, pack those edits into the actions list.
234+
3) Action types allowed (only these three):
235+
- "refine_field" — overwrite an existing field's value (field_key must be in the "Available field keys" list)
236+
- "add_field" — add a new field (field_key may be a new name)
237+
- "remove_field" — delete a field
238+
4) NEVER touch reserved fields: 档案名 / voice_id / system_prompt / live2d / live3d / vrm / mmd / model_type
239+
5) If the user is just chatting / asking questions / hasn't asked for a change yet, leave actions as an empty array [].
240+
6) Match the user's language in the reply (Chinese in, Chinese out; English in, English out).
241+
7) Return STRICT JSON only — no markdown fences, no preface or suffix text:
242+
243+
{
244+
"reply": "your message to the user",
245+
"actions": [
246+
{"type": "refine_field", "field_key": "Personality Archetype", "value": "new value", "reason": "why"}
247+
]
248+
}""",
249+
}
250+
251+
252+
def get_card_assist_clarify_prompt(lang: str = "zh") -> str:
253+
return _loc(CARD_ASSIST_CLARIFY_PROMPT, lang)
254+
255+
256+
def get_card_assist_generate_prompt(lang: str = "zh") -> str:
257+
return _loc(CARD_ASSIST_GENERATE_PROMPT, lang)
258+
259+
260+
def get_card_assist_refine_field_prompt(lang: str = "zh") -> str:
261+
return _loc(CARD_ASSIST_REFINE_FIELD_PROMPT, lang)
262+
263+
264+
def get_card_assist_chat_system_prompt(lang: str = "zh") -> str:
265+
return _loc(CARD_ASSIST_CHAT_SYSTEM_PROMPT, lang)

0 commit comments

Comments
 (0)