WARNING:这是一个由ai完善的项目!
ORIGINAL ASPIRATION:Azure是一个我设定的ai少女,最开始只有概念,后来根据自己学过的几年python知识以及一台比较好的设备在自己机器上跑了一下qwen2.5,后来又通过ai工具的使用逐渐做成了一个通过Napcat接入qq的机器人,后来得知了maibot这个工具并自己部署了qwen3.5但聊天效果太慢了,又恰好最近对围棋比较感兴趣(小时候也学过一点),遂有了此项目。
FUN WHERE:市场上有各种ai围棋陪练,但都是基于katago的直接应用,并没有指导的感觉,很枯燥,所以我希望AazureGo不仅仅可以让大家感到开心,并且也能激发大家在现代无法沉心环境背景下对于围棋学习的乐趣。
这是一个 3D 围棋陪练程序:KataGo 负责算棋(玩家执黑先行,Azure 执白应手), 大模型 负责讲解与闲聊,语音合成 让 Azure 开口说话, Godot 4 场景里是一个会眨眼、歪头、注视你的青蓝色 AI 少女 Azure。
运行形态:后端是本机进程(
backend\,127.0.0.1:8000),对局编排与语音合成都由它做, 只有 LLM 走可自定义的 OpenAI 兼容 API(填地址 + API Key;语音合成的合成动作在后端进程里, 但tts_base_url指向哪儿就在哪儿,不必然是远端)。KataGo 由独立部署的云服务 (katago_service\)提供,人设与提示词也能在config.json里直接改。 客户端(game\)只做渲染、输入与语音播放,连不上后端时只在状态栏提示。当前版本 v3.12.2:合并 PR #5(每次从主界面进入都是新的一局 / 化身 VRM 路径恢复 / 「重来」清账 / 局后按钮冻结),并保留本地 v3.12.0 的棋谱库 / 全盘复盘 / 自适应难度 / 领地热力图 / 音效与战报 / 局面点目,以及 TTS 四档(系统 / 本地 / 云端 / 阿里云)、 圆形胡桃木露台 + 水墨山水背景。 主界面第三键打开棋谱库(含 24 手示例局),选中一盘即可让 Azure 按分段强思考从头讲到尾; 设置里可开自适应难度、领地热力图与对局音效;局后一键生成 1200×1600 PNG 战报。 点「局面分析」时,棋盘会标出已定型的归属点、左上角弹出形势纸签(黑 / 白目数与黑方视角目差)—— 复用同一次分析,零额外引擎调用,下一手自动收起。 仍保留 v3.11.4 的主界面「远看棋盘」开场运镜、语音自然打断与思考短句—— 启动时相机停在院子另一头,整张棋盘在画面下半部、上方压着一枚半透明纸签(开始对局 / 设置 / 退出); 点开始后封面淡出、相机缓动飞近并正对棋盘,右侧工具栏再从画面外滑入;对局中按
Esc回主界面时整段反向播放。 等待时气泡里是她思考的短句,每秒换一句(20 句预存在game/data/thinking_lines.json)。 v3.11.4 起,你一落子,她上一轮没说完的话会先 0.25 秒淡出,再随机接一句预制短句 (嗯? / 这么着急? / 这么快就有思路啦? / 唔…… / 好快! / 可以呀! / 哦?), 等新合成语音接上——像人被打断时收住话头,而不是硬切。 v3.11.2 起有了主界面与思考短句;v3.11.1 起她「说多少 / 想多久」当场可调、说话过程中也能继续落子—— 右侧栏「说话:精简 / 适中 / 详细」与 ⚙ 设置里的「思考强度:快速 / 中等 / 强思考」都即时生效 (思考强度不再需要重启后端),改动写回config.json;棋盘在她这一轮结束、那颗子已揭示后就解锁, 语音继续播,你一落子就把她上一轮没说完的话打断(台词照常显示,不丢字)。 v3.11.0 的语音合成改用阿里云 Voice Studio 实时语音合成(WebSocket 流式)——tts_mode新增aliyun(模板默认),适配器backend/app/tts_aliyun.py继承原有TTSClient, 只替换「文本 → 一条音频」这一步,清洗 / 分块 / LRU 缓存 / 失败整条降级全部沿用;协议为run-task → task-started → continue-task → finish-task → task-finished,连接复用、空闲/超龄主动换新。 原有openai/system两条路原样保留(改tts_mode即可切回)。详见第五节「接语音」与「九、更新」。 v3.10.1 起,她心里多了一份「你这个人」和「现在几点」—— 玩家棋风画像(⚙ 设置抽屉一格,默认关)在每次点「🏁 结束」时把这一局记进独立的profile.db,再由对局记录经指数衰减 EMA 融合成四组维度(开局偏好 / 行棋风格 / 失误与薄弱方位 / 性格与话题);时间感知(同一抽屉一格,默认关)让她知道现在几点、这局下了多久、你多久没来、 连续第几天。两者都只在相关时注入提示词(时间块 + 画像块,插在长期记忆段 → 事件区块 → 时间块 → 画像块 → 铁律段),两块为空则context_block逐字符不变;写入只在summarize()(手动结束)里、落盘一律asyncio.to_thread,回合内零新增 LLM / 引擎调用。三个延伸: 主动约战提醒(连接时一次、模板句、零新增调用;它只与「有没有历史记录」耦合——从未开启 / 库里没有历史时关掉画像就不会提醒,库里已有历史时提醒照旧会发)、🪞 她眼中的你 (只读面板)、局末进步报告(并进现有summarize()提示词,不新增调用)。 v3.8.0 的预测线 A/B/C + 情绪波动 + 局内事件表(A 常态 → B 小概率 → C 更小概率;事件区块插在 长期记忆段之后、铁律段之前,空则逐字符不变)、v3.7.0 的定式导学「开关 + 频率」(右侧栏一格下拉: 关闭 / 低 25% / 中 50% / 高 75% / 总是 100%,默认中 50%;p = clamp(rate + 棋力画像 + 情绪, 0, 1), 走错绕过频率层、必说)、局后只读复盘(📖 复盘)、视角快捷键(Home回初始取景、C依次看四角)、悬停显示坐标 + 手数、目差量级人话,以及 v3.6.0 的定式主动跟课 (进入 / 分岔 / 收尾三结构点 + 浮层参考图卡片)与 v3.5.x 的语音链路整体搬到后端都保持不变。v3.9.1 是一次缺陷修复:输出上限被推理段吃光会让每句台词都退化成兜底句(推理型模型的推理 token 也计入
max_tokens),现在给预算加推理余量、并在「空内容 +finish_reason=length」时去上限 重试一次;同时给定式导学补了「总是 100%」档,复测不必再手改配置。v3.9.2 修的是定式参考图卡片的呈现:压在棋盘上时只淡外壳、不淡字(原先把整卡压到 28%, 参考图变成一块灰影),落位改为跟随取景的纯函数(旋转 / 缩放 / 开合侧栏后自动挪出棋盘, 玩家拖过则不接管),并用 4×4 棋盘格子判重叠(菱形棋盘的外接矩形会把两腰的空白也判成棋盘)。
v3.9.3 把定式参考图卡片补成能「看」也能「关」的样子:右上角 × 随时关闭(同一结构点不再 自动弹回)、头部加结构点徽标(进入 / 分岔 / 走错 / 收尾)、小棋盘按角放大成 9 路(有子越界 自动退回 19 路全图)、石子上叠手顺数字、底下列出候选定式名、候选点画成白子。
v3.9.4 把角内取景从 13 路收到 9 路(实测 25 条库线最宽只用 8 路,9 路装得下全部,格距再大 一号),候选点由菱形改成白子 + 琥珀外环(避免和「已落下的白子」混淆;退回 19 路时只画环)。
v3.9.5 修「她还没应手,玩家又能下一手」:
busy置真时发放所有者编号,旧回合的finally不再能解开新回合的锁;前端同时补两道防线(点下就锁、看/api/move的回包,被拒就按权威 快照撤掉乐观子)。v3.9.6 定式参考图卡片改成不透明底色(半透明压在浅色棋盘上会发灰);避让时仍按原规则变半透明 ——那一态存在的意义就是「别挡住棋盘」。
v3.10.0 全局换成古风(书卷气):宣纸纹理 + 手绘墨框 + 朱砂印,字体改楷体,圆角全部取消, 界面里不再有 emoji。所有样式收进
GufengTheme.gd一处。v3.10.1 按实机反馈收口配色与版式:去掉侧栏的黑框(改纸页边发丝线)、标题条从墨黑换成黛青、 清掉所有荧光色(目差绿 / 棋盘上的青红点 / 纯白桌面)、全站文字对比度补齐(含「玩家气泡白字压在 纸底上」这个真 bug),控件抬高到 32px、左栏可滚、开场「开始对局」固定在底部。 详见第五节「接语音」与「九、更新」。
- 分支说明
- 一、仓库结构
- 二、快速开始
- 三、
config.json字段 - 四、接大模型(LLM,OpenAI 兼容)
- 五、接语音(TTS,可选)
- 六、远程后端(唯一的运行方式)
- 七、导出与分发(可选)
- 八、许可
- 九、更新
- 十、致谢
本仓库以 发展主版本 为主线,另保留一条 历史分支 完整承载旧版:
| 分支 | 定位 | 内容概要 | 链接 |
|---|---|---|---|
main |
发展主版本(当前主线,v3.x) | 前后端分离:game\ 只负责渲染、输入与语音播放,backend\ 统一编排对局与模型调用,katago_service\ 独立部署提供 KataGo;支持 LangChain 提示链、发言侧工具与长期记忆。 |
查看 main |
legacy |
历史分支(发展副版本,v1.0–v2.0.1) | 旧版完整存档:本机运行 KataGo 与本地模型、三维情感模型(valence / curiosity / arousal)、前后端分离 v2.0.1,以及 peek\ 窥屏脚本等。 |
查看 legacy |
legacy为旧版的历史存档,后续功能开发集中在main。
.
├─ Azure.vrm 人物模型(约 14MB;v3.12.2 起随仓库提供,分享包与 exe 并列)
├─ game\ 游戏本体(Godot 4.6 工程)
│ ├─ scenes\Main.tscn 主场景
│ ├─ scripts\ GDScript 源码(按职责分类)
│ │ ├─ ui\ Main.gd 主控:3D 棋盘 + 左右面板 + 接线
│ │ │ Board3D.gd 3D 棋盘绘制与交互(含定式高亮轨迹)
│ │ │ AzureAvatar.gd VRM 形象、表情、视线、风场
│ │ │ Ambience.gd 圆形木露台 / 环形护栏 / 环境风
│ │ ├─ services\ TTSClient.gd 语音播放器(按后端 speech 指令出声)
│ │ │ SpeechPairing.gd 台词与语音的配对状态机(兜底不丢字)
│ │ ├─ core\ AzureConfig.gd config.json 读写
│ │ │ Coords.gd 坐标换算(棋盘坐标标签)
│ │ └─ net\ NetClient.gd 后端客户端(REST + WebSocket)
│ ├─ assets\voice\ 预制语音:开场欢迎语 + 20 条语气词(即播零等待,与情绪/表情绑定)
│ ├─ assets\textures\floor\ 胡桃木地板(diffuse + normal,用户提供的 walnut_veneer_02)
│ ├─ assets\ui\ 纸幕 / 印框 / 青绿山水背景
│ ├─ tests\ 纯逻辑门卫脚本(十八支,清单见 2.7:语音 / 配对 / 定式卡片 / 配置 / 复盘 / 视角 / 目差 / 定式档位 / 聊天 / 音效 / 战报 / 棋谱回放 / 新局 / 点目 / 化身路径)
│ ├─ config.json 客户端参数模板(见第三节)
│ └─ export_presets.cfg Godot 导出预设(个人导出用)
├─ backend\ 本机后端(唯一权威:对局编排 / LLM / 情绪 / 定式;FastAPI + REST + WebSocket)
│ ├─ app\ 对局编排(状态图)/ LangChain 链(LLM + 提示词) / 情感模型 / 定式识别与族计数与提示门控 / 局后复盘 / 全盘分段复盘 / 发言侧工具 / 长期记忆 / 玩家棋风画像与时间感知 / 棋谱库与 SGF / 语音(清洗 + 合成)
│ ├─ tests\ 后端单元测试(含情感模型黄金样本)
│ ├─ requirements.txt 后端依赖
│ ├─ run_server.bat 一键启动后端(监听 127.0.0.1:8000)
│ └─ data\ 运行产物(已 gitignore):长期记忆库 memory.db(见第四节)、定式族计数库 joseki_progress.db、玩家棋风画像库 profile.db、棋谱库 games.db(见第四节)
├─ katago_service\ KataGo 云服务(独立部署:常驻 `katago analysis` 进程,对外 `/health` + `/analyze`)
│ ├─ server.py FastAPI 应用:Bearer 鉴权、串行化、warming/ready/dead 状态与 /health
│ ├─ proc.py 常驻引擎子进程封装(stderr 持续排空 + 64MB 读行缓冲 + 启动预热)
│ ├─ tests\ 服务单元测试(假引擎,不依赖真实 KataGo)
│ ├─ requirements.txt 服务依赖(fastapi / uvicorn)
│ └─ run_server.bat 一键启动服务(监听 127.0.0.1:8100,见第六节)
├─ server\ 自建语音服务(CosyVoice 3,可选;**需自己启动**,游戏不再拉起)
│ ├─ azure_tts_server.py FastAPI 服务:OpenAI 兼容 `/v1/audio/speech`、`/health`、`/voices`(另有 `/v1/models`)
│ ├─ run_hidden.vbs 隐藏窗口启动(手动启动自建服务时用)
│ ├─ start_tts_server*.bat 手动启动脚本
│ ├─ voices.json 克隆音色的参考音频与文本
│ ├─ third_party\OpenVoice\ 快速克隆音色转换(源码;权重 .pth 另行获取)
│ └─ sherpa_voices.json 轻量音色清单(小雅 / 少女 / 女声)
├─ docs\ 后端化决策与实现计划
└─ peek\ 窥屏 VLM 脚本(azure_peek.py / start_vlm_server.py);v3.x 未接线,保留供后续功能
模型与资产(大体积模型不随源码仓库分发,见 GitHub Releases):
katago.exe + *.dll、model-b18c384nbt.bin.gz(KataGo 权重)——这两个归 KataGo 云服务的部署侧持有
(见第六节「KataGo 服务部署」),本仓库只含连它的客户端;
CosyVoice 权重(pretrained_models / sherpa_models)、
OpenVoice 权重(third_party/OpenVoice/checkpoints_v2 的 .pth)、云端 LLM / VLM 的 gguf 均不入库。
人物模型 Azure.vrm(约 14MB)自 v3.12.2 起随仓库根目录提供,源码运行不需要另外下载;
分享包把它放在 exe 同目录即可(客户端路径解析顺序见 2.1)。
最短路径(细节见下方 2.1~2.7):
- 用 Godot 4.6 打开
game\工程,或:Godot_v4.6.3-stable_win64.exe --path game - 人物模型
Azure.vrm已随仓库放在仓库根目录,源码运行不需要另放;若使用自己的形象,把vrm_path指过去即可:绝对路径可直接用;相对路径会依次在 exe 同目录 →game/→ 仓库根目录 → 进程工作目录 找,取第一个存在的(分享包把Azure.vrm与 exe 放一起)。 KataGo 的katago.exe/ 权重 / analysis 配置文件归 KataGo 云服务部署侧持有,客户端不读这些路径。 - 先启动 KataGo 云服务(
katago_service\run_server.bat,见第六节「KataGo 服务部署」), 再启动后端(backend\run_server.bat,见第六节),最后启动游戏。后端在启动时读取
config.json(LLM / 提示词 / KataGo 服务地址等,见第三节「读取方」列), 所以改这些字段、或在游戏设置面板里改完 LLM 地址/Key 后,都要重启后端才生效。 - 玩家执黑先行:左键落子,右键拖拽旋转视角,滚轮缩放。
config.json 放在可执行文件同目录即可覆盖包内默认值(源码运行时读 game\config.json)。
朗读方式(左侧栏下拉,tts_mode)现在有四档同级选项:
系统语音(Windows)/ 本地(只走本地 CosyVoice/MeloTTS,失败回落系统语音)/
云端 OpenAI 兼容(有本地地址时仍本地优先,兼容旧配置)/ 阿里云 Voice Studio。
本地服务地址写在 tts_local_base_url(如 http://127.0.0.1:9880/v1);dev_local.ps1
会启动本地语音、等 /health 就绪,并把 game/config.json 的朗读方式切成 local。
阿里云要填 tts_aliyun_api_key 与 tts_aliyun_voice(平台音色 ID 为 UUID)。
合成在后端,文字始终照常显示;左侧栏「实际朗读」一行显示当前生效路线。
聊天/讲解需要按第四节接上一个大模型 API。
| # | 准备项 | 说明 |
|---|---|---|
| ① | Python 环境 | pip install -r backend\requirements.txt(fastapi / uvicorn / websockets / httpx / langchain-core / langchain-openai / langgraph)与 pip install -r katago_service\requirements.txt(fastapi / uvicorn,httpx 只给测试用) |
| ② | KataGo 云服务部署侧 | 备好 katago.exe + 配套 *.dll、权重 model-b18c384nbt.bin.gz、analysis 配置文件(如 analysis_example.cfg)——都不入库(见第一节「模型与资产」与第六节「KataGo 服务部署」) |
| ③ | 客户端 | Godot 4.6 打开 game\ 工程;人物模型 Azure.vrm 已在仓库根(也可自备,见 vrm_path) |
| ④ | 可选(缺了也能跑) | 云端 LLM 的地址 / 模型名 / Key(第四节)、云端 TTS 三件套 + Key(第五节)、embedding 三件套(长期记忆,第四节)、自建 CosyVoice(第五节) |
websockets不能省:裸装uvicorn不带 WebSocket 实现,缺了它/ws会拒绝升级(404), 客户端只会一直显示「未连接后端」(见第六节)。
最小可跑组合:只保留 backend_url 的默认值 http://127.0.0.1:8000,按 2.2 把 KataGo 云服务与后端
起起来(服务侧要备好引擎与权重),就能对局——她照常算棋、应手、给提示。此时:
讲解 / 闲聊退化为兜底台词(没接 LLM,见第四节);语音走 Windows 系统语音(云端 TTS 没配 Key 时后端
整条降级,见第五节;把 tts_base_url 留空则整条不出声、只出文字)。
按这个顺序起,前一步就绪了再起下一步:
- KataGo 云服务:双击
katago_service\run_server.bat(脚本自己cd到仓库根目录,必须原样跑; 等价命令与参数见第六节「KataGo 服务部署」)。默认监听127.0.0.1:8100。 - 等它就绪:轮询
curl http://127.0.0.1:8100/health,直到"state": "ready"。 服务启动即后台预热(加载模型 / OpenCL 首次调优,可能几十秒到几分钟),预热期state是warming(此时/analyze立刻回 503 可读错误,不会挂到客户端超时)。 - 后端:双击
backend\run_server.bat(内部cd到backend\再python -m app.main), 监听127.0.0.1:8000。控制台会打印「Azure 后端启动...(KataGo 走云服务,LLM 走云端)」。 - 游戏:
Godot_v4.6.3-stable_win64.exe --path game。
两个「看起来像坏了」的现象:
- 预热期起后端 ⇒ 后端最多 180 秒不监听任何端口:后端在 lifespan 里等 KataGo 就绪
(代码常量
READY_TIMEOUT_MS=180 秒,不可配置)才绑端口。这段时间/api/state、/ws都不可达,游戏只显示 「未连接后端(…),N 秒后重试…」——看起来像后端根本没起来,其实它在等预热;预热完成、后端绑上端口后 客户端会自动重连。 - 服务侧没配引擎路径 ⇒ 服务以可读中文提示 + 退出码 2 结束:控制台打印
「
[KataGo服务] 启动失败:create_app 的 cfg 缺少引擎路径:…」与「请用--katago-exe/--model/--config或环境变量KATAGO_EXE/KATAGO_MODEL/KATAGO_CONFIG指定引擎」,然后进程退出。若路径给了但引擎起不来 (路径错 / 缺 dll / 显存不足…),服务仍在跑,但/health的state是dead(katago_error给原因),/analyze回 503。
主界面(纸幕封面,v3.11.3):启动后先是远看棋盘的取景——相机停在院子另一头(略偏一个角度), 整张棋盘落在画面下半部,上方压着一枚半透明宣纸纸签(朱砂方印 + 「Azure · 围棋陪练」+ 三键), 棋盘透过纸面看得见。三键:
- 开始对局:封面淡出 → 相机缓动飞近、正对棋盘 → 右侧工具栏从画面外滑入 → 她才开口问候;
- 设置:只开设置面板(复用「出发前的准备」),保存后回封面,下次开局不再弹第二遍;
- 退出:退出游戏。
首次「开始对局」前仍会弹一次「出发前的准备」设置面板(盖在封面上),确认后才播入场运镜。
对局中 Esc 回主界面,动效反向播放(工具栏先滑出 → 相机飞回远处 → 封面淡入);主按钮变「开始新对局」
(每次从封面进来都是新的一局,上一局不保留)。再按 Esc 或点它,入场运镜再播一遍。
封面 / 运镜期间棋盘、四个功能按钮、聊天与回档 / 开关一起冻住,闲置搭话也停。
视角与鼠标(左侧栏默认收起,右侧栏默认展开;两侧各有一个贴边按钮收起 / 展开对应面板,
按钮文案在 ▶ / ◀ 间随状态切换):
- 左键:落在交叉点上=落子(她思考中或还有话没说完时棋盘锁着,点不动);落在她头部附近=摸摸头 (开心 + 微微脸红 + 一声语气词,状态栏「你摸了摸 Azure 的头…」)。
- 右键拖拽:旋转视角(松开后她先看你约 1 秒)。
- 滚轮:缩放(上推拉近 / 下拉拉远)。
Home/C(键盘):Home一键回初始取景;C依次看四个角(西北 A19 → 西南 A1 → 东南 T1 → 东北 T19,循环)。两键都只在聊天框没有焦点时生效(正在输入时按键归输入框),且带修饰键不算 (Ctrl+C/Shift+C/Alt+C不转视角)——都不易误触。看角只转方位角,你自己缩放过的视距与俯仰不动。- 悬停提示:鼠标停在交叉点上会给坐标——空点提示「D4 · 点击落子」;已有棋子提示「D4 · 第 3 手」
(坐标 + 手数,手数以后端
move_seq为准、与右栏「手数: N」同源)。她思考中 / 复盘只读时,空点不再提示 「点击落子」(此时点了也没用),但已有棋子的坐标与手数照常显示(复盘回看时正好用来核对第几手)。
右侧栏(标题「♟️ Azure · 围棋陪练」,副标题「青蓝色 · 永恒17岁 · 天才少女 · 右键拖拽旋转视角 · 滚轮缩放」):
| 控件 | 行为 |
|---|---|
手数: N / 黑胜率: xx.x% / 目差: ±x.x |
顶部信息栏;未算出时为 --。手数与俗手列表随棋盘快照刷新,胜率 / 目差在她落子后与「局面分析」时刷新。目差下面还有一行 11px 小字给量级人话(差不多 / 半个角 / 一个角的实空 / 已经很难追了)——胜率与目差打架时取更保守的那一档,原始数字照旧保留(正数=黑好,负数=白好) |
允许 Azure 虚着(复选框) |
勾上=这局她可以虚着;即时生效(POST /api/config) |
定式导学(下拉:关闭 / 低 25% / 中 50% / 高 75% / 总是 100%) |
开关 + 频率合一格(v3.7.0 口径修订,见第九节):开启后识别进入 / 分岔 / 收尾三个结构点,按频率 p = clamp(rate + 棋力画像 + 情绪, 0, 1)(画像与情绪各浮动 ±10%)决定说不说;未命中只点评这一手(与「没识别到定式」逐字段一样);走错(真亏且偏离库线)绕过频率层、必说;同族 8 手冷却。写回 config.json(joseki_teach + joseki_rate)并对当前对局即时生效。「总是 100%」是给复测用的:有资格就一定讲 |
说话:精简 / 适中 / 详细(下拉) |
她每次说多少(v3.11.1):精简=点评 1 句 / 聊天最多 2 句(默认,与旧「短回复」开一致);适中=不加额外约束;详细=允许 2~3 句(约 80 字内)。只改提示词末尾的长度约束,经 /api/config 转发后端(reply_style),立即生效并写回 config.json(旧键 tts_limit_len 同步写回,旧版本也读得懂) |
⚙ 设置(抽屉,默认收起) |
收着思考强度(下拉:快速 / 中等 / 强思考,v3.11.1,立即生效、不用重启后端)、情绪波动(下拉:关闭 / 低 3% / 中 5% / 高 6%,默认关)、聊天摘要用 LLM(复选框,默认关)、玩家棋风画像(复选框,默认关)、时间感知(复选框,默认关)、🪞 她眼中的你(只读按钮 + 一块文本)与「同步说话 / 整段合成 / 长句合并」三个开关(下面几行)。除长句合并外都立即生效(长句合并要重启后端才生效);改动都写回 config.json,并在连上 WS 时把抽屉里的开关一并推给后端(POST /api/config) |
情绪波动(下拉:关闭 / 低 3% / 中 5% / 高 6%,在 ⚙ 设置里) |
开关 + 基础概率合一格(v3.8.0,见第九节):开启后她小概率不走最优解,在预测线 A/B/C 里按「越偏越少」挑一条落子(P(A) > P(B) > P(C));关闭=行为零变化(恒走最优解、不掷骰)。B/C 缺失 / 胜率差超限 / 首手非法一律回落 A。写回 config.json(emotion_drift + drift_base)并对当前对局即时生效 |
聊天摘要用 LLM(在 ⚙ 设置里) |
默认关:聊天按主题入事件表走规则式(纯函数分段 + 模板摘要)、零额外 LLM 调用;开启后只在局末 / 空闲用 LLM 分段总结(落子回合内绝不加调用)→ 立即生效 |
玩家棋风画像(在 ⚙ 设置里) |
默认关(v3.9.0,见第九节):开启后每次「🏁 结束」把这一局记进 profile.db、跨局融合成「她眼中的你」,只在相关时注入画像块;关掉=行为零变化(不注入、不写记录)。写回 config.json(profile_enabled)并立即生效。从未开启 / 库里没有历史时关掉它 ⇒ 不再写对局记录 ⇒ 没有历史可判 ⇒ 主动约战提醒不会发;库里已有历史时提醒照旧会发——它只与「有没有记录」耦合,不与开关本身耦合 |
时间感知(在 ⚙ 设置里) |
默认关(v3.9.0):开启后她知道现在几点 / 这局多久 / 你多久没来 / 连续第几天,只在相关时提、不会把数字当口头禅。写回 config.json(time_aware_enabled)并立即生效(与画像同一格、一次推一次 /api/config)。主动约战提醒不受它门控 |
🪞 她眼中的你(在 ⚙ 设置里) |
只读画像面板(v3.9.0):点一下拉一次 GET /api/profile,把各维(值 + 把握 + 局数)铺到下面那块文本上,末尾标「她眼中的你(最近 N 局记录)」。无副作用、复盘期间也可看;功能关着 / 还没攒下时给出可读提示 |
同步说话(在 ⚙ 设置里) |
开启后她这一轮还在跑的朗读台词,文字等这条语音开始播放后再显示(落子宣告的揭示也跟着这一刻走);回合已经结束的台词(聊天 / 闲置搭话 / 非法手提示)当场显示——她上一句可能还在念,傻等它就会让回复一直挂在「Azure 思考中……」里,要再发一条才冒出来(v3.5.1 修);取音频失败 / 合成超时 / 超过 45 秒配对窗口时同样兜底立即显示(不卡字、不丢字)→ 立即生效 |
整段合成(在 ⚙ 设置里) |
她连发的多条台词合并成一段连续朗读 → 立即生效 |
长句合并(在 ⚙ 设置里) |
后端单次合成字数上限 100 → 160(关=首句更快)→ 重启后端后生效(分块在后端做) |
音效 / 音效音量(在 ⚙ 设置里) |
v3.11.5:落子、提子、虚着、终局的轻音效;纯前端、事件驱动,关闭后完全静音(不影响语音朗读),音量只控制音效 → 立即生效 |
领地热力图(在 ⚙ 设置里) |
v3.11.5:按 KataGo 逐点归属显示双方势力(墨色=黑 / 纸白=白),复用每次落子后的既有分析,零额外引擎调用;默认关,打开后立即显示 → 立即生效 |
自适应难度 / 难度基线(在 ⚙ 设置里) |
v3.11.5:默认关。开启后按你的棋风画像(blunder.rate,置信度够才用)、聊天关键词(「太难 / 让着我 / 放水」或「认真点 / 太简单 / 别放水」)与最近 6 手表现(rank / 亏损 / 黑胜率)在她算好的候选点里挑一个更弱但仍在容差内的落点。当前档位永远夹在基线 ±1 档(1 入门 … 5 严苛),每 4 手最多调整一次;关闭=落点与原来完全一致 |
战报(复盘框内) |
v3.11.5:一键生成 1200×1600 PNG 战报(棋盘 + 领地图、手顺、胜率/目差、关键点评、亏损手),独立 2D SubViewport 渲染、不截主界面也不受 3D 相机影响;保存到 user://reports/ 并把路径复制到剪贴板(走 GET /api/review/notes,无额外引擎调用) |
棋谱库(主界面第三键) |
v3.11.5:全屏模态纸签——左侧对局列表(日期 / 手数 / 黑胜率,示例局带标记),右侧小棋盘可←/→逐手翻看,另有局后总结、全盘复盘、导出 SGF、删除;导入 SGF 走本机文件选择,入库前后端会逐手校验。列表 / 详情来自 GET /api/games*,不碰当前对局、零引擎调用;全新库首次打开自动塞入 24 手内置示例局 |
全盘复盘(棋谱库内) |
v3.11.5:选中一盘棋 → 主棋盘只读本地回放,Azure 按分段(最多 8 段 / 每段 ≤48 手)用强思考从头讲到尾;每段一段台词 + 语音,上一段播完才请求下一段(TTS 不堆积)。棋盘可手动 ←/→,自动解说可暂停 / 继续;台词可带 {行为} 标签,在文字/语音出现的那一刻触发表情。退出时拉回当前对局,全程不改后端棋局 |
输入框 和Azure聊天... + 发送 |
自由聊天(最多 200 字,回车与按钮同效);她的消息逐字显示,等待时先出现占位气泡——v3.11.2 起占位里是她思考时的短句,每秒换一句(20 句预存在 game/data/thinking_lines.json,随机且不连复;文件缺失 / 写坏时回退旧的「Azure 思考中…」省略号)。她忙时(正在想 / 还在说话)你发的这句先排队:输入框下面那行显示「排队中 N 条 · 队头前几个字」,等她上一句说完、语音也播完就自动发出去(不用你再发一条);最多排 5 条,满了会保留你输入框里的原文并提示,绝不吞话 |
💡 提示 |
方向性提示,不直接说坐标(POST /api/hint) |
📊 局面分析 |
以她的视角解读当前局势(POST /api/analyze)。v3.12.0 起同时给出局面点目:棋盘上标出归属明确(|归属| ≥ 50%)的定型点,左上角弹出形势纸签(黑 / 白目数、黑方视角目差 + 贴目)。只复用这一次分析,零额外引擎调用;玩家下一手 / 局面变化 / 重来 / 进复盘时自动收起 |
🔄 重来 |
弹确认框(可填备注)→ 清空棋局与俗手记录(POST /api/reset) |
🏁 结束 |
弹总结框(可填备注、可留空直接确定)→ 对局总结(POST /api/summarize) |
📖 复盘(对局结束后才出现) |
进入只读复盘(POST /api/review {"on":true}):棋盘只读、四个功能按钮置灰、聊天禁用;期间她静音(落子 / 聊天 / 闲置 / 提示 / 分析被拒,且不碰 busy)。按钮文案变 ✅ 退出复盘,退出后回到进入前的局面(「结束之后还能继续下」的既有语义不变) |
🏠 返回主界面(对局结束后才出现,紧挨「复盘」) |
v3.11.3:不用先想起 Esc,直接走反向运镜回封面(工具栏滑出 → 相机飞远 → 封面淡入),可接着开新局 / 改设置 / 退出。复盘期间这一格隐藏(那一行已有四个按钮),先「退出复盘」即可回封面 |
◀ 上一手 / 下一手 ▶(只在复盘模式内可见) |
回看第 n 手(POST /api/review/goto:只读重放一份盘面、不改棋局),也可用键盘 ← / → 翻页;目标手数夹到 0..总手数 |
⏪ 俗手回档(点一下回到那手之前) |
列出最近最多 3 手俗手(按钮文案 第N手 <坐标>,悬停显示原因);点一下回档到那一手之前(连带撤掉她的应手);没有记录时显示「暂无俗手记录」 |
| 底部状态栏 | 见下方状态栏文案表 |
她忙时(思考 / 说话)
💡 提示/📊 局面分析/🔄 重来/🏁 结束四个按钮置灰,棋盘也锁着; 俗手回档按钮不禁用,但点击时若她正忙则不生效。复盘期间冻结的范围更大:棋盘只读 + 四个功能按钮 + 聊天输入 + 俗手回档 + 「允许虚着 / 定式导学 / 情绪波动」三个开关(她忙时不冻这几项——忙时回档 / 改配置是既有玩法); ⚙ 设置抽屉里的「聊天摘要用 LLM」等开关复盘期间仍可改——它只影响聊天摘要怎么做、不改棋局, 而且复盘期间聊天 / 闲置 / 总结本来就被拒,不会因此产生新事件。
局后只读复盘(v3.7.0,📖 复盘):只在对局结束后出现(🏁 结束 流程走完整组按钮才显示,未结束不挤界面)。
进入后棋盘只读、四个功能按钮置灰、聊天禁用,用 ◀ 上一手 / 下一手 ▶ 或键盘 ← / → 翻页
(POST /api/review/goto 只重放一份只读盘面,一个字都不改棋局)。复盘可进可出:点 ✅ 退出复盘
即回到进入前的局面(后端退出时不推 state,前端自己 GET /api/state 拉回终局对齐)。
- 期间她静音:后端的
review是独立模式——落子 / 聊天 / 闲置搭话 / 提示 / 分析一律被拒 (可读拒绝{"ok":false,"error":"复盘模式中"},与busy的{"ok":false,"busy":true,…}形状刻意不同), 两种拒绝都不碰busy;情绪心跳与语气词也一并冻结。 - 不改变「结束还能继续下」:
goto只重放临时盘面、不碰st.finished;退出复盘后照旧可以继续落子。 - 进复盘前若她还在跑回合(如「🏁 结束」的总结还没收尾)或语音还没播完,前端会先提示「等她收尾再复盘」 (免得总结的台词 + 语音落到复盘期间)。
- 回看时逐手存档照常可读(
GET /api/review/notes:本局每手的用意 / 点评 / 感想与真亏手账)。 - 棋谱库(v3.11.5):每次「🏁 结束」自动把本局归档到
backend\data\games.db(与画像开关无关); 主界面「棋谱库」可看列表、逐手翻盘、导出 / 导入 SGF、删除。全新库首次打开时会塞入一盘 24 手内置示例局(demo标记;只塞一次,删掉不会复活),供体验与后续全盘复盘测试。棋谱库不碰当前对局, 列表 / 导入 / 导出 / 删除都不受busy门禁;导入的 SGF 会先逐手重放校验(非法手 / 手序断裂 / 让子摆子 / 非 19 路一律可读拒绝),不会把坏棋谱塞进库。 - 全盘 AI 复盘(v3.11.5):在棋谱库选中一盘 →「全盘复盘」。主棋盘用
GameReplay本地只读回放, Azure 把整盘按 最多 8 段 / 每段 ≤48 手分段,每段固定think_mode=2写 2~4 句点评并发message+speech;前端等上一段的语音与打字播完才请求下一段(POST /api/review/full/segment), 所以 TTS 不会堆积。每段会对「段首…段末」的每个前缀各做一次 KataGo 分析(复用相邻结果,深度压到 120), 再结合棋谱库里的逐手点评 / 亏损手账写提示词;引擎或 LLM 失败都有基于棋谱的兜底点评,绝不卡死。 棋盘可手动 ←/→、自动解说可暂停 / 继续;退出时拉一次/api/state回到当前对局——后端棋局全程没被改动。
棋盘上的浮层参考图卡片(定式导学开启、且门控命中时才出现:族计数 < 3 时有资格,
≥ 3 只在真走错时有资格;卡片与台词同走门控——未命中就只点评这一手、卡片也不上。次数按族(一课)
跨局累计,同族同一局最多计 1 次(按教过几次计),悔棋把这一局计的那次退回去):
| 项目 | 行为 |
|---|---|
| 位置 | 浮在棋盘相邻的空白处(默认落位按序取第一个不压住棋盘投影、也不压住已展开侧栏的位置,全不可行就退回左下角),不进右栏、不加右栏控件;窗口缩放时只在你还没拖动过它的情况下重新摆位 |
| 内容 | 一张 19 路小棋盘:全部手顺(黑白子)+ 第 k 手画高亮环 + 分岔候选点画菱形带序号 + 走错那一手标红(只有后端判定的真走错才标) |
| 拖动 | 左键按住可拖;至少留一角在窗口内(拖出去会找不到) |
| 避让 | 与棋盘落点热区 / 鼠标附近的小方块 / 已展开的侧栏重叠时半透明淡出(还看得见),移开即恢复。淡出只是变淡、不做点击穿透:卡片仍会吃掉落在它矩形内的左键,压在棋盘上时可能压住你要落子的地方,得先把它拖开;拖动时鼠标必然在卡片内,所以拖动期间它保持淡出(有意的——否则就按不住它了) |
| 收起 | 定式走完(formed)时立即收起;2 手条目(守角 / 大飞挂 / 一间挂这类「先认出这个字形」的课)第 2 手判进入、永不判收尾,卡片不会被一句话收掉;偏离库线(deviated)不是收尾——这一手若门控命中(走错必命中)就把走错那一手标红继续显示;脱先(下一手落在角外)时识别器不出声、不发事件,靠距最后一次事件超过 120 秒的兜底 TTL 收起 |
这张卡片只新增「浮在棋盘上的参考图卡片」这一样:不进右栏、不新增右栏控件、不产生语音或台词 (台词与卡片都走门控:结构点先过频率层,未命中只点评这一手、卡片也不上;走错必说必上)、 不给大棋盘加手顺编号连线。 「📖 复盘」已在 v3.7.0 落地(见上面「局后只读复盘」):它是整局逐手只读回看, 不是「只回顾这一局走过的定式」。
定式库覆盖哪些开局(v3.6.0 扩库后:11 族 / 25 条,逐条取自 KataGo 自对弈实测): 星位·点三三(5 条)、星位·小飞挂·靠压 / 小飞应(4 条)、星位·双飞燕(2 条)、星位·小飞守角(2 条)、 星位·大飞挂(2 条)、星位·一间挂(2 条)、小目·小飞挂·尖应 / 飞应(2 条)、小目·小飞挂·尖顶(2 条)、 小目·一间跳守角(1 条)、小目·一间低挂(1 条)、三三·被挂角(2 条)。 未做的部分如实说明:星位·飞压 / 夹击没做(挂角局面下引擎实测给不出飞压与夹击的手顺,不编造), **目外(5-3)/ 高目(5-4)**也没做(设计阶段即标为可砍)。识别是宽松前缀匹配,库里没有的族就不出声、不会硬套。 逐族清单与未做理由见第九节「更新 · v3.6.0」。
「认字形」的短条目(U53 已修):像星位·小飞守角 / 星位·大飞挂 / 星位·一间挂 / 小目·一间跳守角
这类基准线只有一手交换(双方各一手就成「字形」)的条目,识别器在这两手落定时判为进入
(entered)——说的就是「这一角开始走 X」那句,候选照旧带进事件与卡片;它们永不判收尾
(formed 只对 ≥ 3 手的条目成立),所以参考图卡片不会被一句话收掉。这四条是「认出这个字形」的前缀
条目,不是一门走完的课;一旦再往下走,识别器照旧按结构点沉默或开口。
左侧栏「🔊 语音朗读」(默认收起):
朗读 Azure 的话:语音总开关(写回config.json;关掉即刻停止播放与取音频,后端重启后才真正零请求、零speech事件;文字照常)。朗读方式下拉:系统语音/本地 CosyVoice/云端 OpenAI 兼容/阿里云 Voice Studio(实时流式)(对应system/local/openai/aliyun;重启后端后生效)。云端服务地址(上面第 3 项才需要)/模型/音色(可下拉选);重填音色候选按钮 + 可手填音色 id 的输入框。 本地地址与本地音色仍在config.json的tts_local_base_url/tts_local_voice(dev_local.ps1自动写入)。 下拉候选是本地静态列表:alloy/echo/fable/nova/onyx/shimmer/中文女/粤语女/英文女, 也可直接手填服务商的音色 id。语速(0.52.0)/1.3)/音调(播放层变调,会略带动语速)(0.8音量(0.0~1.5)三个滑块。▶ 试听:POST /api/tts/speak合成一句话再播(不看朗读开关是否打开;系统语音模式下会回「当前语音方式不产生音频」)/■ 停止。- 左侧栏底部另有一行语音状态(如「朗读方式:…(合成在后端,重启后端后生效)」);只有朗读开始 / 完成、试听与出错这类状态会同时镜像到底部状态栏。
开场「⚙️ 出发前的准备」面板(无窗口模式与截图 / --demo 模式会跳过;用 -- --setup 可只把面板画出来):
思考模式(只影响 Qwen3 这类思考型模型)下拉:快速:不思考,首字 1~2 秒(推荐)/中等:先想几句再答,约 3~6 秒/强思考:先推理再回答,更慢、质量更好(对应llm_think_mode的0/1/2)。- 可折叠分节
LLM 对话 API(点评 / 聊天):服务地址/模型名/API Key(只有云端需要)。 - 可折叠分节
语音 TTS API:服务地址/音色。 ▶ 开始对局:确认后写回config.json(地址 / 模型名 / 音色留空时保留原值,Key 可以清空), 随后才按新配置起各服务。面板里没有的字段(如tts_model/tts_api_key)保持原样——要改它们得手动编辑config.json或用左侧栏。
状态栏文案(底部那一行,左侧栏收起时也能看到):
| 文案 | 含义 |
|---|---|
后端模式:编排与引擎在 <地址>(本地不启动 KataGo/LLM) |
启动后的默认文案 |
未连接后端(<地址>),N 秒后重试… |
WS 连不上(后端没起 / 端口不对 / 缺 websockets / 后端在等 KataGo 预热),退避重试 |
后端已连接:<地址> |
WS 已连上 |
她正在查资料… |
发言侧工具在跑(仅 tools_enabled 开启时),工具结束恢复原文案 |
语音:<…> |
语音状态与错误(如「这段语音的音频取回失败(…),改为只显示文字」) |
定式导学已开启 / 关闭、说话详略:精简 / 适中 / 详细、思考强度:快速 / 中等 / 强思考 |
开关反馈(底栏文案);带生效范围的那条在左栏:说话详略:精简(立即生效) / 长句合并:开(重启后端后生效) |
视角:已回到初始取景 / 视角:看西北角(A19) |
按 Home / C 的视角反馈(只转方位角) |
复盘:只读回看中 · 第 N / M 手(← → 或按钮翻页) / 已退出复盘,回到当前局面 |
进 / 出复盘的进度反馈;她还在收尾时会先提示「等她收尾再复盘」 |
你摸了摸 Azure 的头… |
摸头反馈 |
- 你落子(左键):客户端先乐观画一颗黑子,同时
POST /api/move。落子不成立(超出棋盘 / 此处已有棋子 / 打劫禁着点 / 自杀禁着点)时,后端先推一份权威快照让界面撤销那颗子,再说一句 「这手不成立:<原因>」。这条提示不走
busy、也带语音(见第五节「已知取舍」的错配窗口说明)。 - 后端算两个局面:你落子前(缓存复用,用于给你这手排名)与落子后(用于选她的应手); 定式识别(纯本地计算、零引擎调用)与情绪结算也在这时完成。
- 她的点评(事件
ai_comment):按你这手的排名给评价;若这手正好是某个定式的结构点 (进入 / 分岔 / 收尾)且门控命中,点评直接换成定式讲解(不额外增加 LLM 调用),浮层参考图卡片 同时上(见 2.3;台词与卡片都走门控——未命中只点评这一手,与「没识别到定式」完全一样)。 - 她的用意 + 落子宣告是同一条台词(事件
ai_move,带落点坐标):这句话说清她准备下在哪、想达成什么。 她的落点由后端算棋决定(取引擎候选里第一个合法点;不允许虚着时绝不虚着)。开了情绪波动(默认关,见第九节 v3.8.0)时,她可能小概率改走预测线 B / C 的首手 (与最优线的胜率差有上限、首手必须合法,否则回落 A)——这是有意让落子带「人味」,不是算错。 开了发言侧工具(
tools_enabled)时,模型在工具轮里报出的落点可当候选,但要过三条边界——候选点必须过棋盘合法性、pass只在允许虚着时可用、候选不合法就回退后端算棋的默认落点;任何情况下都会产出可用落点 (见第四节「工具(发言侧)」)。 - 先落到内部棋谱:她这一手在发宣告时就已落进后端棋谱,但棋盘上的可见更新延后。
- 音画同步:客户端等这条「落子宣告」的语音开始播放时才把棋子揭示到棋盘(
POST /api/reveal)。 语音不可用 / 没播时,回合结束会兜底揭示一次,不会一直不显示。 - 落子后感想(事件
ai_comment)+ 右侧栏形势栏刷新。
几条配套行为:
- 俗手记录:你这手不在她算的前 5,或胜率下滑超过 10%,会被记为俗手(右侧栏「俗手回档」按钮, 最多保留最近 3 手)。点按钮回档到那一手之前——连同她的应手、俗手记录、定式去重记录、以及 **这一局已计的那次定式族计数(库里的计数退回一次)**一起回滚,所以悔棋重走那手可以重新讲解。
- 定式识别:纯本地识别(零引擎调用);进入(角内刚走出库里某条的起手,含 2 手条目)/
分岔(第 3 手起出现两种走法,或你真走错时)/收尾(库里那条线走完,只对 ≥ 3 手的条目成立)
三个结构点先过门控(
p = clamp(rate + 画像 + 情绪, 0, 1),见 2.3):命中才说,正跟着书走的 中间推进不打扰;走错(真亏且偏离库线)绕过频率层、必说。棋盘上的浮层参考图卡片画出全部手顺、 进度、分岔候选与走错的那一手(规则见 2.3),与台词同走门控;族计数< 3有资格、≥ 3只在真走错时才有资格。 - 总结写长期记忆:
🏁 结束出的真总结(不是兜底台词)会把棋谱 + 结果 + 总结写进长期记忆 (memory_enabled=true时;见第四节「长期记忆(跨局记得你)」)。 - 锁:她思考 / 说话期间棋盘锁着,
busy必归位是设计保证(见 2.6 最后一条); 复盘是另一套冻结(只读 + 静音),与busy无关、也不碰busy(见 2.3「局后只读复盘」)。
- 合成在后端、前端只播放(见第五节):后端每发一条要念的台词都紧接一条
speech事件 (turn_id+seq配对)。mode=openai时前端按id去GET /api/tts/audio/{id}取音频播放;mode=system时把后端已清洗好的text交给 Windows 系统语音。 - 三种模式:
aliyun(阿里云 Voice Studio 实时语音合成,WebSocket 流式,默认)/openai(云端 OpenAI 兼容/v1/audio/speech)/system(Windows 系统语音,零配置即可发声)。aliyun模式的 Key / 音色 ID 没填全时 同样走system(不会静默没声音)。 - 失败整条降级:连不上 / 超时 / 服务端拒绝 / 响应解析不了 / 单条音频超过缓存预算,后端把这一条整条改发
system指令(speech.mode变system),文字永不消失、回合不卡;下一句仍会重试云端。 后端控制台留一行[TTS] …便于排查(合成失败 / 单条超预算)。 - 缓存:条数 32 / 8 MiB 双向 LRU。被淘汰或后端重启后取不到音频 ⇒ 这一条只显示文字(不阻塞)。
- 语气词:
assets\voice\下的预制语气词与情绪 / 表情绑定(如sigh/cheer/proud/think/pat);think在每次玩家互动后的额度是 1 次,同一情绪 30 秒内延续不重复出声;只在对局互动中插, 开场问候与闲置搭话不插。 - 开场欢迎语:
assets\voice\welcome_<名字>.wav(名字取prompt_user_name的小写;模板里是welcome_estarth.wav),有就零等待直播、没有就退回普通流程(纯文字)。 [[tone:…]]由后端剥净:语气标记(sigh/cheer/proud)只在后端解析,并整段从朗读文本里删掉, 不会被念出来;未知标记照删但不出声。{行为}标签(v3.11.5):台词里可带{笑}/{得意}/{惊讶}/{担心}/{难过}/{脸红}(后端白名单,最多 2 个);后端在 TTS 前把标签整段剥掉(不会被念出来),前端在文字/语音真正 出现的那一刻触发表情(脸红 2.6 秒后褪)。全盘复盘用它让 Azure 的点评带上动作。- 情绪区域怎么影响她:情绪区域(愉悦度 × 好奇度 × 唤醒度 → 27 区)决定表情形变(持续层 + 一次性强调)、
区域对应的语气词与本句音高微调(
speech.pitch,叠加在播放音高上,不改你的配置)。
已知取舍(4 条短提示只有文字没有语音、busy 会被合成占住、两栏生效范围不同、两条前端固有的错配窗口)
见第五节「已知取舍」,这里不重复。
| 症状 | 原因 | 处置 |
|---|---|---|
| 游戏一直显示「未连接后端(…),N 秒后重试…」 | ① 后端没起;② backend_url 地址 / 端口不对;③ 后端缺 websockets(/ws 升级被拒 404);④ 后端还在等 KataGo 预热(最长 180 秒不监听端口) |
起后端(backend\run_server.bat);核对 backend_url(默认 http://127.0.0.1:8000);pip install -r backend\requirements.txt 补 websockets;若后端控制台停在等 KataGo,先 curl http://127.0.0.1:8100/health 确认 state == "ready"(见 2.2) |
| 她有话,但没声音 | ① tts_enabled=false;② tts_mode 与三件套不配套(如用 中文女 调 OpenAI 会被 HTTP 400 拒);③ tts_api_key 空;④ 合成超时(tts_timeout_sec);⑤ 音频被 LRU 淘汰或后端重启过(GET /api/tts/audio/{id} 404);⑥ 走了系统语音(speech.mode=system) |
看底部状态栏「语音:…」与后端控制台 [TTS] … 行;按第三节把三件套配成同一家服务商的取值;system 模式本来就是系统语音出声,检查 Windows 音量与音色 |
| 完全没有声音,系统语音也没声 | PowerShell / SAPI 被策略拦掉(前端用 OS.create_process("powershell") 调系统语音,起不来就念不出) |
状态栏会提示「系统语音不可用(无法启动 PowerShell),这条改为只显示文字」;放行 PowerShell,或改用云端 openai 模式 |
| 形象不显示 | Azure.vrm 缺失,或 vrm_path 指向的文件在候选位置里都找不到 |
按 2.1 ③ 放好 .vrm;相对路径会依次找 exe 同目录 / game/ / 仓库根 / 工作目录(见第三节),日志里会列出试过的路径 |
| 她算不清,提示说「嗯...我这边一时算不清,稍等再试吧~」 | KataGo 云服务不可用(没起 / 地址或令牌不对 / 引擎 dead) |
读 GET http://127.0.0.1:8000/api/health 的 katago_dead + katago_error,成对读(见第六节):dead=true=引擎(或到它的链路)真没了;dead=false + 新的非空 error=服务在线但这次没答好。再去看服务侧 GET http://127.0.0.1:8100/health 的 state |
| 讲解 / 闲聊变成「唔…我脑子有点转不过来…」这类兜底台词 | LLM 没接 / 401 / 429 / 超时;或请求体扩展字段被服务商拒(HTTP 400/422) | 核对 llm_base_url / llm_model / llm_api_key(第四节)。扩展字段被拒时后端会自动去掉重试一次、且本次会话不再发送(设计好的降级);要彻底避免就把 llm_extra_body / llm_think_extra_body 留空 |
| 长期记忆没生效 | memory_enabled=false;或改了字段没重启后端 |
置 memory_enabled=true 后重启后端;GET /api/health 的 memory_rows 一眼看出库里有没有东西(见第四节) |
开了 stream_tokens 却看不到逐字效果 |
它默认 false,且后端只在启动时读 |
置 stream_tokens=true 后重启后端(第四节「流式输出」) |
| 棋盘像被锁住、点不动 | 她正在思考 / 说话(busy,或被语音占住) |
等一会儿;busy 必归位是设计保证(后端 try/finally + 前端每帧清锁),不会永久锁死(第五节「已知取舍」) |
一键起本地全套(Godot 编辑器工具栏,或命令行):
- 编辑器工具栏 「本地一键」:依次启动本地 LLM(8080) / 语音(9880) / KataGo 服务(8100) / 后端(8000), 就绪后运行游戏;「全量测试」:新开控制台跑后端 + KataGo 服务 + 前端 headless。
- 命令行等价:
powershell -NoProfile -ExecutionPolicy Bypass -File dev_local.ps1-NoGame只起服务(确认各模型接上、不开游戏)-QualityTts用 CosyVoice 质量音色(azure,约 20 秒/句)-Tests跑全量测试;-Stop停止本脚本启动的进程
- 默认走快引擎(MeloTTS,约 0.4 秒/句,音色
xiaoya)。质量引擎(CosyVoice,azure)每句 1525 秒,而后端同步等待合成(2 分钟——只建议最终验收时用。_emit_line→tts.speak),一回合可能 1 - 本机模型路径写在
dev_local.ps1顶部(KataGoExe/KataGoModel/KataGoCfg/GodotExe), 换机器只改这几行;它是开发工具,不进入游戏运行时(导出的游戏仍然只连后端)。 - 本地若用 Qwen3 这类推理模型:在
game/config.json把llm_extra_body设为{"chat_template_kwargs": {"enable_thinking": false}}。否则每次发言会先把 输出预算耗在思考段(content为空),后端按设计去掉上限重试一次,单回合可能拖到数分钟。
测试(后端与 KataGo 服务都用标准库 unittest,不用 pytest):
cd backend && python -m unittest discover -s tests # 后端全量(861)
cd katago_service && python -m unittest discover -s tests # KataGo 云服务全量(36;假引擎,不依赖真实 KataGo 与公网)
godot --path game --headless --script res://tests/test_tts.gd # 语音播放器(22)
godot --path game --headless --script res://tests/test_speech_pairing.gd # 语音配对状态机(82;纯逻辑)
godot --path game --headless --script res://tests/test_joseki_card.gd # 定式参考图卡片(79;纯逻辑)
godot --path game --headless --script res://tests/test_config.gd # 客户端配置读写(192)
godot --path game --headless --script res://tests/test_review_mode.gd # 复盘冻结 / 翻页夹取 / 存档渲染 / 全盘分段 / 行为标签(50;纯逻辑)
godot --path game --headless --script res://tests/test_view_helpers.gd # 视角 / 运镜 / 悬停文案 / 摸头 / 领地图 / 点目文案(45;纯逻辑)
godot --path game --headless --script res://tests/test_score_phrase.gd # 目差量级人话(17;纯逻辑)
godot --path game --headless --script res://tests/test_joseki_level.gd # 定式档位纯函数(15;纯逻辑)
godot --path game --headless --script res://tests/test_net_chat.gd # 聊天回包与排队(14;纯逻辑)
godot --path game --headless --script res://tests/test_chat_queue.gd # 聊天队列 FIFO / 上限(17;纯逻辑)
godot --path game --headless --script res://tests/test_thinking_lines.gd # 思考短句解析 / 不连复(19;纯逻辑)
godot --path game --headless --script res://tests/test_title_screen.gd # 主界面纸幕 / 四键 / 开始·开始新对局 / 淡入淡出(19;纯逻辑)
godot --path game --headless --script res://tests/test_new_game.gd # 每次进对局都重开 / 空局文案(8;纯逻辑)
godot --path game --headless --script res://tests/test_estimate.gd # 局面点目文案 / 脏值 / 棋盘标记(17)
godot --path game --headless --script res://tests/test_avatar_path.gd # VRM 相对路径候选与解析(9;纯逻辑)
godot --path game --headless --script res://tests/test_sfx.gd # 对局音效资源 / 音量换算 / 静默(8;纯逻辑)
godot --path game --headless --script res://tests/test_report.gd # 战报布局纯函数:坐标 / 点评 / 亏损手 / 手顺(9;纯逻辑)
godot --path game --headless --script res://tests/test_replay.gd # 棋谱本地回放:落子 / 提子 / 虚着 / 脏值(10;纯逻辑)后端跑前先
pip install -r backend\requirements.txt。test_tts.gd会真的拉起一次系统语音进程(音量已压到最低); 其余前端门卫都纯逻辑、无外部依赖(语音配对 / 定式卡片 / 客户端配置 / 复盘 / 视角运镜 / 目差 / 定式档位 / 聊天队列 / 思考短句 / 主界面 / 音效 / 战报 / 棋谱回放)。 括号里是断言项数(本版实测,只增不减)。 v3.8.0 新增的后端门卫模块:tests\test_pv_lines.py(预测线 A/B/C 提取,零引擎调用)、tests\test_event_log.py(局内事件表:只增不减 / 去重 / 不随悔棋回退)、tests\test_decay.py(遗忘权重 4 档)。 v3.9.0 新增的后端门卫模块:tests\test_profile_store.py(profile.db存储层:只增不减 / 库不可用降级)、tests\test_profile.py(四维特征提取 + 指数衰减 EMA 融合)、tests\test_time_ctx.py(时间事实:跨日 / 跨时区边界)。 v3.11.5 新增的后端门卫模块:tests\test_sgf.py(SGF 坐标 / 虚着 / 变体 / 规则校验 / 注释往返)、tests\test_game_store.py(games.db增删查 / 示例播种 / 降级)、tests\test_games_api.py(棋谱库端点 + 局末自动归档)、tests\test_full_review.py(分段规划 / 分段分析 / 强思考 / 行为标签剥离 / 兜底)、tests\test_full_review_api.py(全盘复盘端点 + WS 推送)、tests\test_adaptive.py(自适应难度:画像 / 聊天 / 表现信号、基线夹取、冷却、落点弱化)。 v3.12.0 新增的后端门卫模块:tests\test_estimate.py(点目纯函数:归属方向 / 50% 阈值 / 活子与死子 / 脏值降级),tests\test_agent_graph.py追加EstimateWiringTest(只在「局面分析」发一次、先于台词、普通落子不发)。 v3.12.2 追加:test_agent.py的「重来清账」全账目断言、test_main.py的ResetSilentTest; 前端新增test_avatar_path.gd/test_new_game.gd/test_estimate.gd。
调试参数(走 OS.get_cmdline_user_args(),必须放在 -- 之后):
Godot_v4.6.3-stable_win64.exe --path game -- --shot=out.png # 渲染数帧后截图退出
Godot_v4.6.3-stable_win64.exe --path game -- --demo # 摆几颗示例棋子(纯视觉核对,不走引擎)
Godot_v4.6.3-stable_win64.exe --path game -- --setup # 只画设置面板供截图
Godot_v4.6.3-stable_win64.exe --path game -- --title --shot=menu.png # 只画主界面(纸幕封面)供截图另有
--yaw=/--pitch=/--dist=/--tilt=调相机与形象,--panels=both|left|right调面板开合;--shot/--demo/--setup默认跳过主界面与启动设置面板(配--title强制画主界面、配--setup强制画设置面板)。
日志位置:
| 位置 | 内容 |
|---|---|
| 后端控制台 | [KataGo] / [TTS] / [Agent] / [Memory] / [Profile] / [WS] 前缀的运行日志与降级原因(标准输出的编码错误策略在启动时改成 replace:日志打不出的字符降级成 ?,绝不把回合打挂) |
| Godot 控制台(客户端) | [Setup] 启动设置已保存 / [TTS] 取音频失败与配对清出 / [chat] 防复读 |
katago_service\logs\katago\ |
引擎自己的日志(服务用 -override-config logDir=… 指到这里) |
server\server_run.log |
自建 CosyVoice 语音服务的日志(VBS 隐藏窗口启动,只能看文件) |
改 config.json 后要不要重启后端:见第三节的「读取方」列——标「后端(启动时)」的字段
(LLM / 提示词 / katago_* / tts_timeout_sec / tools_enabled / stream_tokens / memory_* /
embed_* / profile_db_path / game_db_path / adaptive_difficulty_spread / tts_long_chunk 等)改完都要重启后端;标「客户端 + 后端(启动时)」的
tts_enabled / tts_mode / tts_voice / tts_speed 也要重启后端(前端那半边立刻生效,后端那半边不会);
标「实时」的(joseki_teach、joseki_rate、emotion_drift / drift_base、chat_digest_llm、
profile_enabled / time_aware_enabled、adaptive_difficulty / adaptive_difficulty_base、tts_limit_len)不用。纯客户端字段(backend_url / vrm_path /
tts_pitch / tts_volume / tts_sync_speak / tts_batch_speak)与后端无关。tools_enabled 另可经
POST /api/config 实时切换(第四节;客户端界面暂未提供入口)。
这个文件前后端共用:放在可执行文件同目录即可覆盖包内默认值(源码运行时读 game\config.json)。
下表「读取方」一列标出谁读它、什么时候读:
- 后端(启动时):
backend\run_server.bat启动时一次性读入,改完必须重启后端才生效 (游戏内设置面板写回的 LLM 地址 / 模型名 / Key / 思考档位也属于这一类)。 - 后端(实时):经
/api/config转发,改完立即生效。 - 客户端:游戏进程自己读,改完(或下次启动)即时生效。
- 后端专用(客户端不读):
katago_url/katago_token/katago_timeout_ms、llm_extra_body/llm_think_extra_body/stream_tokens/tools_enabled/max_tool_calls与memory_enabled/memory_db_path/embed_base_url/embed_model/embed_api_key/embed_timeout/memory_top_k/memory_max_inject_chars/profile_db_path只由后端消费; 语音侧的tts_base_url/tts_model/tts_api_key/tts_timeout_sec也只由后端消费 (tts_long_chunk客户端只留一个勾选框、分块本身在后端);这些键客户端不参与计算, 但写回配置时会原样保留(游戏内保存设置不会把它们弄丢)。
| 字段 | 读取方 | 含义 |
|---|---|---|
backend_url |
客户端 | 后端地址(本机进程,默认 http://127.0.0.1:8000):编排与 LLM 调用都在后端进程里,游戏只做渲染与语音(WebSocket 地址由其推导) |
max_visits |
后端(启动时) | KataGo 每手搜索次数,越大越强越慢(由后端逐次分析请求传给云服务,客户端不跑 KataGo)。预测线 A/B/C 那一次会另取 max(pv_max_visits, max_visits)(见 pv_max_visits) |
katago_url |
后端(启动时) | KataGo 云服务地址(默认 http://127.0.0.1:8100);引擎在独立服务里,后端只发 HTTP 请求(见第六节「KataGo 服务部署」) |
katago_token |
后端(启动时) | KataGo 云服务的 Bearer 令牌;与服务侧 KATAGO_SERVICE_TOKEN / --token 一致,服务未配令牌时留空(仅限本机回环调试) |
katago_timeout_ms |
后端(启动时) | 单次分析超时(毫秒,代码默认 45000,随包 game\config.json 模板仍写 30000);超时/连接失败会重试 1 次,仍失败才报「云服务不可用」 |
pv_max_visits |
后端(启动时) | 预测线 A/B/C 那一次分析的深度下限(默认 150):实际取 max(pv_max_visits, max_visits)——只增不减(引擎默认更深时用引擎默认,绝不把这一次砍浅);只加深「玩家落子后局面」这一次分析,不增加调用次数(见第六节「KataGo 服务部署」) |
pv_max_drop |
后端(实时) | 预测线 B / C 与最优线 A 的胜率差上限(0~1,默认 0.05):某条候选与 A 的胜率差超过它就整条线不用(宁可只有 A,也不下偏得太远的棋)。改它不用重启后端(/api/config 实时改;客户端界面暂未提供入口) |
joseki_teach |
后端(实时) | 定式导学:主动识别你与 Azure 走出的定式,在进入 / 分岔 / 收尾三个结构点提示(讲解 + 浮层参考图卡片 + 棋盘高亮手顺);右侧栏的「定式导学」下拉也能切(开关 + 频率合一格:关闭 / 低 25% / 中 50% / 高 75% / 总是 100%);默认 true,关掉=不识别不打扰。提示还要过频率层(见 joseki_rate) |
joseki_rate |
后端(实时) | 定式导学的频率(0~1,默认 0.5):结构点的基础概率,最终 p = clamp(rate + 棋力画像 + 情绪, 0, 1)(画像与情绪各浮动 ±10%);未命中只点评这一手(与「没识别到定式」逐字段同构);走错绕过频率层、必说;同族 8 手冷却。与 joseki_teach 同属右侧栏那一格,写回 config.json 并对当前对局即时生效 |
emotion_drift |
客户端 + 后端(实时) | 情绪波动开关(默认 false):开启后她小概率不走最优解,在预测线 A/B/C 里按「越偏越少」挑一条落子;关闭=行为与现状逐项一致(恒走最优解、不掷骰、不记事件)。⚙ 设置抽屉那一格读写,写回 config.json 并经 /api/config 对当前对局即时生效 |
drift_base |
客户端 + 后端(实时) | 情绪波动的基础概率(0~1,默认 0.03,与开关同一格):最终 p_drift = clamp(drift_base + 情绪偏移, 0, drift_max);关闭档不动它,重新开启时概率能回来 |
drift_max |
后端(启动时)+ 实时 + 客户端(只读镜像) | p_drift 的上限(默认 0.08,配置层另夹到 0.2):越大偏航越多,但不许开到 1.0(p ≥ 0.6 时阶梯不变量会反转)。设置抽屉不写它;可手动编辑 config.json(改完重启后端生效),也可经 /api/config 实时改(客户端界面暂未提供入口)。客户端把它当只读镜像(AzureConfig.drift_max,从 config.json 读、默认 0.08、脏值→默认)拿它当 drift_base 的夹取上限——两端读到的上限因此始终一致(v3.9.0) |
chat_digest_llm |
客户端 + 后端(实时) | 聊天摘要用 LLM(默认 false):关=聊天按主题入事件表走规则式(纯函数分段 + 模板摘要)、零额外 LLM 调用;开=只在局末 summarize() / 空闲 idle_remark() 用 LLM 分段总结(落子回合内绝不加调用)。⚙ 设置抽屉那一格读写,写回 config.json 并经 /api/config 即时生效 |
profile_enabled |
客户端 + 后端(实时) | 玩家棋风画像开关(默认 false):开启后每次「🏁 结束」把这一局记进 profile.db、跨局融合成画像,出话前只在相关时注入画像块。⚙ 设置 抽屉那一格读写,写回 config.json 并经 /api/config 即时生效;关掉=行为零变化(不注入、不写记录)。从未开启 / 库里没有历史时关掉画像 ⇒ 不再写对局记录 ⇒ 没有历史可判 ⇒ 主动约战提醒不会发;库里已有历史时提醒照旧会发——它只与「有没有记录」耦合,不与开关本身耦合 |
time_aware_enabled |
客户端 + 后端(实时) | 时间感知开关(默认 false):开启后把「当前时间 / 本局时长 / 单手用时 / 距上次对局间隔 / 连续下棋天数」注入时间块(只在相关时提、禁止把数字当口头禅)。与 profile_enabled 同属 ⚙ 设置 抽屉那一格、一次推一次 /api/config,即时生效;关掉=行为零变化。主动约战提醒不受它门控(独立延伸,只与「有没有历史记录」耦合——见 profile_enabled 一行) |
profile_db_path |
后端(启动时) | 画像库路径;留空=backend\data\profile.db(运行产物、已 gitignore,只在 profile_enabled=true 时才写)。只从配置文件读、不经 /api/config(与 memory_db_path 同形);库被删 / 只读 / 目录不存在 ⇒ 当「无画像」,绝不影响对局 |
game_db_path |
后端(启动时) | 棋谱库路径(v3.11.5);留空=backend\data\games.db(运行产物、已 gitignore,首次打开棋谱库 / 首次结束对局时按需建库)。同样只从配置文件读、不经 /api/config;库不可用 ⇒ 列表为空 / 归档失败只打一行日志,绝不影响对局 |
llm_base_url / llm_model |
后端(启动时) | 云端 LLM 的 OpenAI 兼容地址与模型名 |
llm_api_key |
后端(启动时) | 云端 LLM 的 API Key;非空则发送 Authorization: Bearer <key> |
llm_think_mode |
后端(启动时)+ 客户端(实时) | 思考档位:0=快速(直答,首字快)/ 1=中等(推理再多给 160 token 并催收尾,约 4~7 秒)/ 2=强思考(再多给 800 token、自由推理,更慢更好);默认 0;开局设置面板与 ⚙ 设置抽屉里都能改(旧键 llm_think_strong 自动迁移)。v3.11.1 起可以运行期改:抽屉里选完即时经 /api/config 推给后端,推理预算与 llm_think_extra_body 一起进 / 出,不用重启。三档都先给 REASON_HEADROOM_DEFAULT = 4096 的推理余量(推理型模型的推理 token 也计入 max_tokens),档位只决定「再多想多少」 |
llm_extra_body |
后端(启动时) | 透传给云端 LLM 的请求体扩展字段(JSON 对象,如 {"top_k": 40}):每次请求都并入。服务商不认这些字段(HTTP 400/422)时后端自动去掉扩展字段重试一次,且本次会话不再发送它们(设计好的降级,不是故障)。输出上限按线上键 max_tokens 发送(预算 = 台词预算 + 推理余量);在这里显式写 max_tokens / max_completion_tokens 可覆盖后端注入的预算(服务商只认新键、或模型输出上限不到 4K 时的退路) |
llm_think_extra_body |
后端(启动时) | 同上,但只在思考档位开启(llm_think_mode > 0)时并入(如 {"reasoning_effort": "high"});不开思考档位时完全不发 |
stream_tokens |
后端(启动时) | 输出流式开关,默认 false:开启后她的台词逐段推送(WS token 事件),首字更快;气泡最终内容仍以完整 message 为准(见第四节「流式输出」) |
tools_enabled |
后端(启动时) | 发言侧工具总开关,默认 false:开启后她在「提示」与「落子宣告」两个回合可自主复算局面 / 查定式 / 核对盘面;工具轮里模型报出的落点也能参与落点候选(见第四节「工具(发言侧)」) |
max_tool_calls |
后端(启动时) | 一次发言里工具执行次数上限(默认 2;代码侧硬上限也是 2,配置只能收紧、不能放宽) |
memory_enabled |
后端(启动时) | 长期记忆总开关,默认 false:开启后她跨局记得你(对局总结与显式事实落 SQLite,出话前召回注入);见第四节「长期记忆(跨局记得你)」 |
memory_db_path |
后端(启动时) | 长期记忆库路径;留空=backend\data\memory.db(只在开启时创建,属运行产物、已 gitignore) |
embed_base_url / embed_model / embed_api_key |
后端(启动时) | 云端 embedding 的 OpenAI 兼容地址 / 模型名 / Key(如 https://api.openai.com/v1 + text-embedding-3-small);三者任一为空=不配向量,召回自动降级为关键词匹配(不报错、不影响出话) |
embed_timeout |
后端(启动时) | 单次 embedding 请求超时(秒,默认 10);网络差或想让降级更快发生时可调小(≤0 / 脏值退回默认)。召回 / 写入已移出事件循环,这里只决定单次请求最长等多久 |
memory_top_k |
后端(启动时) | 每次召回注入条数上限(默认 3;0=记忆开着但不召回,管理面查询同样返回空列表) |
memory_max_inject_chars |
后端(启动时) | 注入提示词的总字数上限(默认 240;单条另截到 120 字) |
prompt_system |
后端(启动时) | 覆盖系统提示词(人设全文);留空用内置 Azure 人设 |
prompt_user_name |
后端(启动时) | 玩家称呼,替换提示词中的「Estarth」(默认 Estarth) |
prompt_phase_notes |
后端(启动时) | 覆盖各阶段提示,键:observe / after_player / own_turn / just_moved / analyze / chat / summary |
vrm_path |
客户端 | 虚拟形象 .vrm 路径。绝对路径直接用;相对路径依次在 exe 同目录 → game/ → 仓库根 → 工作目录里找,取第一个存在的 |
tts_enabled |
客户端 + 后端(启动时) | 是否启用语音;后端据此决定要不要合成(关闭时零请求、不发 speech 事件,文字照常) |
tts_mode |
后端(启动时)+ 客户端 | system(Windows 系统语音)/ local(只走本地 tts_local_base_url,失败回落系统语音,不产生云端调用)/ openai(云端 OpenAI 兼容;本地地址非空时仍本地优先,兼容旧配置)/ aliyun(阿里云 Voice Studio 实时合成);后端按它决定合成路由,客户端只作本地镜像(两边都把未知值回落 openai) |
tts_base_url |
后端(启动时) | 云端语音服务地址(OpenAI 兼容,含 /v1,如 https://api.openai.com/v1);留空=不试云端(本地也没有时只出文字) |
tts_model |
后端(启动时) | 云端语音模型名 |
tts_voice |
后端(启动时)+ 客户端 | 音色 id(随服务商而定,也可手填);后端用它合成,客户端系统语音兜底时也拿它选音色 |
tts_api_key |
后端(启动时) | 云端 TTS 的 API Key;非空则发送 Authorization: Bearer <key> |
tts_speed |
后端(启动时)+ 客户端 | 语速:后端合成时传给服务商,客户端系统语音兜底也用它 |
tts_pitch / tts_volume |
客户端 | 音调(播放层变调,会略带动语速)/ 音量 |
sfx_enabled / sfx_volume |
客户端 | 对局音效开关 / 音量(v3.11.5):落子、提子、虚着、终局;纯前端,默认开、0.8;音量夹到 0~1,脏值退回默认 |
ownership_enabled |
客户端 | 领地热力图(v3.11.5):默认 false;打开后按后端 ownership 事件渲染,只做视觉辅助、不改棋局 |
adaptive_difficulty |
客户端 + 后端(实时) | 自适应难度开关(v3.11.5,默认 false):开启后按画像 / 聊天 / 最近表现,在基线上下最多 ±1 档内弱化落点;关闭=逐手与原来一致(不读信号、不改落点)。写回 config.json 并经 /api/config 即时生效 |
adaptive_difficulty_base |
客户端 + 后端(实时) | 自适应难度基线(1 入门 … 5 严苛,默认 3 均衡):当前档位永远夹在 基线 ± adaptive_difficulty_spread 内;改基线会回到新基线并推一条 difficulty 事件。脏值退回默认、越界夹到 1..5 |
adaptive_difficulty_spread |
后端(启动时) | 基线上下允许的浮动档数(默认 1,夹到 0..2):0=锁死在基线(只保留画像对基线的修正) |
tts_timeout_sec |
后端(启动时) | 云端单次合成超时(秒,默认 30);≤0 钳到 1 秒、脏值退回默认;超时一律改用下一路(本地超时另见 tts_local_timeout_sec) |
tts_limit_len |
客户端 + 后端(实时) | 旧键(v3.11.1 起由 reply_style 取代):true ⇔ reply_style=0(精简)。仍随 save() 一起写回,旧版本客户端 / 后端读到的仍是「短回复 开 / 关」 |
reply_style |
客户端 + 后端(实时) | 说话详略三档(v3.11.1):0 精简(点评 1 句 / 聊天最多 2 句,默认)/ 1 适中(不加额外约束)/ 2 详细(2~3 句、约 80 字内);经 /api/config 转发后端,立即生效。脏值退回默认,越界夹到 0~2;文件里只有旧键 tts_limit_len 时按旧语义迁移 |
tts_local_base_url |
后端(启动时) | 本地语音服务地址(OpenAI 兼容,含 /v1,如 http://127.0.0.1:9880/v1)。tts_mode=local 时只走它、失败回落系统语音;tts_mode=openai 时作为本地优先的第一路,失败再走云端,云端也不通才系统语音 |
tts_local_model / tts_local_voice / tts_local_api_key |
后端(启动时) | 本地端点专用;留空沿用云端同名字段(旧配置零迁移) |
tts_local_timeout_sec |
后端(启动时) | 本地单次合成超时(默认 120;本地模型冷启动较慢);0 / 脏值沿用 tts_timeout_sec |
tts_sync_speak |
客户端 | 同步说话:开启后她的朗读台词(含聊天与闲置搭话)文字等这条语音开始播放后再显示;取音频失败 / 合成超时 / 超过 45 秒配对窗口时会兜底立即显示文字(不卡字、不丢字)。聊天与闲置搭话看起来接近即时,是因为它们的回合在发话前就已解锁、前端会立刻放字——不是因为音频已就绪;立即生效 |
tts_batch_speak |
客户端 | 整段排队播报:连发的多条台词合并成一段连续朗读(立即生效);语义变化见下方说明 |
tts_long_chunk |
后端(启动时) | 长句合并:后端单块字数上限 100 → 160(关 = 100,首句更快);改完要重启后端 |
tts_aliyun_ws_url |
后端(启动时) | 阿里云实时合成的 WebSocket 地址;留空=官方固定地址 wss://convai-platform.aliyuncs.com/api-ws/v1/inference(只有联调 / 私有网关才改) |
tts_aliyun_api_key |
后端(启动时) | Voice Studio 签发的 API Key(sk- + 32 位十六进制);百炼原生 Key 会被 401 拒。留空=这条不合成,直接回落系统语音 |
tts_aliyun_voice |
后端(启动时) | 平台音色 ID(UUID 形式,如 9f2a1c4e-7b30-4d51-8a6c-2e5f1b8d0a37),在 Voice Studio「音色管理」里复制;不接受 {model}:{voice} 或裸音色名。留空则退回 tts_voice |
tts_aliyun_format |
后端(启动时) | 音频格式:pcm(默认,后端补 44 字节 WAV 头,最稳——引擎对 mp3 的支持因音色而异)/ wav / mp3 / opus;不认识的值回落 pcm |
tts_aliyun_sample_rate |
后端(启动时) | 采样率(默认 24000);钳到 8000~48000。CosyVoice3 系引擎只原生支持 24000,22050 会被 400 拒;opus 只认 8000/16000/24000/48000 |
tts_aliyun_volume |
后端(启动时) | 合成侧音量 0~100(默认 50);播放音量是客户端的 tts_volume,两者独立 |
tts_aliyun_tn |
后端(启动时) | 文本归一化:把「12345」「2026-10-21」读成口语(默认开)。开启后计费字符按归一化后文本算 |
tts_base_url/tts_model/tts_voice要配套:模板的三件套是https://api.openai.com/v1+tts-1+中文女, 其中中文女不是 OpenAI 的音色 id——用 OpenAI 的话要把音色一并改成alloy/nova/onyx之类的 OpenAI 音色(音色 id 也可手填);换其他服务商时地址、模型名、音色 id 都按服务商的取值改。- 合成在后端,失败一律降级、不出错刷屏:服务连不上(断网 / 地址写错 / 服务没启动)、超时、 服务端拒绝了这次请求(音色 / 模型名不被接受,例如用
中文女调 OpenAI 返回 HTTP 400)或响应解析不了, 后端都把这一条整条降级为「用 Windows 系统语音念」的system指令(speech事件的mode变成system), 文字照常显示、回合不卡;下一句仍会重新尝试云端合成,服务恢复后自动回到云端(不报错、不改配置)。 后端控制台会留一行[TTS] …的降级日志(合成失败 / 单条音频超过缓存预算)便于排查。tts_long_chunk现在是后端的分块开关:它决定后端单次合成的字数上限(关 = 100 / 开 = 160), 改完要重启后端;tts_batch_speak的语义也变了——合成改成「一行一条」之后,它从「一次请求合成一整段」 变成整段排队连续播报(仍影响观感与时延,但不再减少合成请求数),仍在前端即时生效。- 定式导学的两个配置键:
joseki_teach(总开关)与joseki_rate(频率,默认0.5),都由右侧栏 「定式导学」那一格下拉读写、实时生效(v3.7.0 起;v3.6.0 只有joseki_teach、没有频率)。 族计数本身没有开关;运行产物落在backend\data\joseki_progress.db(已 gitignore), 存储出任何问题只打一行日志、绝不影响对局。- 预测线与事件区块的几个阈值 / 上限(代码常量,不是
config.json键):预测线 B/C 的判据pv_diverge_min = 2(与已选各线不同手数的下限,pv_lines.extract_lines的参数默认);事件淡化的DECAY_SPAN = [1, 3, 6](每档累计手数)、DECAY_DROP_AT = 12(超过就不进提示词,事件仍留在表里)、 事件区块字符预算EVENT_BLOCK_MAX_CHARS = 240(超限丢【最旧 → 最低档】)。这几个都在后端代码里, 改它们要动代码并重启后端。(胜率差上限pv_max_drop是 config 键、不在这一条里,见上表。)- KataGo 引擎路径类的旧键(可执行文件 / 权重 / analysis 配置三件套)不在上表,也不再有任何读取方: 它们是「后端自行拉起 KataGo 进程」时代的旧键(v3.0.0 及更早),v3.1.0 起 KataGo 由独立云服务提供, 后端只连
katago_url。游戏内保存设置时会把这几个旧键一并清掉(自我清理,见game\scripts\core\AzureConfig.gd的save()),不影响对局;引擎本体与权重归服务部署侧持有, 不入本仓库(见第一节「模型与资产」)。
改 config.json 三行即可,不内置任何模型:
"llm_base_url": "https://api.deepseek.com/v1",
"llm_model": "deepseek-chat",
"llm_api_key": "sk-你的密钥"- 也可在游戏里改:开局前的「出发前的准备」面板能直接填地址 / 模型名 / API Key(确认后写回
config.json),不用手动编辑。⚠️ 后端只在启动时读取这几个字段(游戏面板不会实时通知后端),所以改完要重启后端才生效。 - 云端:DeepSeek / OpenAI / 通义 / 硅基流动 等 OpenAI 兼容服务,填地址、模型名与 Key。
- 自建服务:vLLM / LM Studio / llama.cpp server 等,
llm_api_key留空即可——游戏只按地址请求,不负责启动它。 - 命令行临时指定(不写回配置):
AzureGoGodot.exe --llm=https://api.deepseek.com/v1 --model=deepseek-chat --key=你的密钥(指向自建服务时把地址换成它的,如http://127.0.0.1:8080/v1) - 没接 LLM 时:对局功能照常可用,只是讲解/闲聊退化为兜底台词。
- 后端内部走 LangChain(
ChatOpenAI):上面三个字段照旧填,服务商专有参数用llm_extra_body/llm_think_extra_body透传(见第三节);调用失败一律降级为可读提示 + 兜底台词,不会卡住棋盘。
stream_tokens: true 后,她说话时逐段推送(WS token 事件,带 turn_id 与 delta),首字更快;
默认 false 时一个 token 都不发,事件序列与没有这个功能时逐字一致。
- 气泡最终内容以完整
message为准:token只是过程;收口的那条message要过clean_reply()与后端的 8 条窗口去重闸门(见「九、更新」v3.4.0)之后才发,整句原样复读时连token也不发, 前端不会留下没收口的残余气泡。 - 推理段永不进
token:增量只取模型回答的 content 通道,content 里的 think 段由后端在累加文本上 跨块剥离。成对<think>…</think>与<|begin…|>…<|end…|>形态(含标记被切块)保证增量与台词都干净; 「网关吞掉开始标记、只剩收尾标记」与「未闭合think」这类畸形形态下,增量流可能短暂带出原文, 但最终message与语音恒为干净台词。 - 流式本身失败(连接断开 / 服务端报错)绝不影响这一回合:自动回落为整段出话,台词必定完整。
- 改完这个字段要重启后端才生效。
默认关闭(tools_enabled: false,改成 true 即开启):关闭时行为与没有工具时完全一致。开启后她只在两个回合可以自己查资料——
「提示」与「落子宣告」——工具请求由状态图的条件边工具轮驱动(模型要工具就先执行、把结果回灌给模型
再来一轮,直到没有请求或预算用尽)。
- 工具可参与落点候选(本版有意放宽,v3.2.0 时是「绝不影响她下哪里」):模型在这一轮里报出的落点
(她这句话里出现的 GTP 坐标或虚着)会被后端认出来当候选,但有三条边界——① 候选点必须过棋盘合法性;
②
pass只在开了允许虚着时可用;③ 任何情况下都产出可用落点(候选不合法就回退后端算棋的默认落点), 绝不会因为她报了个坏点而这一手不下。未开启工具时落点仍完全由后端算棋决定。 - 三个工具:
katago_analyze(复算局面:黑方胜率 / 目差 / 候选点)、joseki_lookup(查入门定式库)、board_query(把局面画成紧凑盘面图,核对坐标与归属)。 - 代价:这两个回合会多出几次模型往返(工具执行次数受
max_tool_calls与代码硬上限约束、单工具 10 秒超时), 回答会变慢;闲聊 / 闲置搭话 / 点评等路径不启用工具,时延不受影响。 - 工具输出永不直接给玩家看:返回的盘面 / JSON 只回到模型手里,玩家看到的仍是她据此重写的自然语言;
客户端只多一类
tool事件(开始 / 结束),仅用于状态栏提示(「她正在查资料…」)。 - 改完
tools_enabled/max_tool_calls要重启后端;POST /api/config也能实时切换tools_enabled(客户端界面暂未提供入口)。
默认关闭(memory_enabled: false):关闭时零开销——不创建数据库文件、不调任何 embedding 接口,
提示词与没有这个功能时逐字符一致。开启后她跨局记得你这个人;与「这盘棋」的局内记忆
(落子评定存档,随对局重置)互不替代。
存什么
- 对局总结:每局
POST /api/summarize成功出总结后,后端把棋谱 + 结果 + 总结落一条归档, 并把总结另存为一条可召回的记忆(kind=game)。LLM 失败退化为兜底台词时不写,防止污染。 - 显式事实 / 偏好:经
POST /api/memory/write手动写入(kind=fact,如「他喜欢简明,不喜欢复杂战斗」)。 - 不存:她的台词、当前盘面描述、模型推理段——长期记忆只收总结与显式事实,越用越准、不会越用越失真。
- 写入硬上限:单条 200 字(超长截断并标「…」)、同
tag+ 同text去重、库内最多 2000 条 (超出按时间淘汰最旧)。
怎么用
- 两个召回点:闲聊(用你这句原话做查询)与她自己落子前(用最近一手 + 阶段做查询); 召回文本插在提示词的「【最近的对话】」之后、铁律段之前,措辞明确「不是当前盘面、仅供参考、别照抄」。
- 注入硬上限:每次最多
memory_top_k条、单条 120 字、总计memory_max_inject_chars字。 - 配了
embed_*就按语义相似度(云端 embedding + 余弦)排序,向量命中不足memory_top_k条时用 关键词从其余记忆里补齐(没向量的老行 / 写入时 embedding 失败的行不会被有向量的行压掉); 没配 / 401 / 超时一律降级为关键词匹配(不伪造相似度分数)。记忆出任何问题都只打日志、自动降级, 绝不影响对局(busy照常归位);召回 / 写入都在工作线程里执行(asyncio.to_thread), 云端 embedding 卡住也不会冻住后端事件循环(单次最长等embed_timeout秒,默认 10)。 - 库是 SQLite:
memory_db_path留空时落在backend\data\memory.db(运行产物,已 gitignore); 这几个键都在后端启动时读取,改完要重启后端。
怎么忘掉 / 查看
四个管理端点(memory_enabled=false 时查询面返回空结果、写入被拒,都不会 500):
# 1) 看她记住了什么(query 必填;k 可省,缺省用 memory_top_k;memory_top_k=0 或显式 k≤0 =不召回,返回空列表)
curl -X POST http://127.0.0.1:8000/api/memory/query -H "Content-Type: application/json" \
-d '{"query":"Estarth 的偏好","k":3}'
# → {"ok":true,"items":[{"id":1,"ts":1750000000.0,"kind":"fact","tag":"estarth","text":"…","score":0.83}]}
# 2) 手动记一条(tag 可省;关闭 / 空文本会被拒:{"ok":false,"id":null})
curl -X POST http://127.0.0.1:8000/api/memory/write -H "Content-Type: application/json" \
-d '{"tag":"estarth","text":"他喜欢简明,不喜欢复杂战斗"}'
# → {"ok":true,"id":1}
# 3) 看体量:rows=记忆条数、games=对局归档数、has_vectors=有向量的条数、db=库文件路径
curl http://127.0.0.1:8000/api/memory/stats
# → {"ok":true,"rows":1,"games":0,"has_vectors":1,"db":"…\\backend\\data\\memory.db"}
# 4) 忘掉指定几条,或一次清空
curl -X POST http://127.0.0.1:8000/api/memory/forget -H "Content-Type: application/json" -d '{"ids":[1]}'
curl -X POST http://127.0.0.1:8000/api/memory/forget -H "Content-Type: application/json" -d '{"all":true}'
# → {"ok":true,"removed":1}
forget只清记忆(会被召回的部分),不动对局归档;停掉后端后直接删掉库文件(连同-wal/-shm) 也能全部忘记。- 示例里
-d '…'的单引号写法在 PowerShell 7 / Git Bash 可用;cmd.exe 要改写成-d "{\"query\":\"…\"}"。- 管理端点的写 / 查会往
/ws推memory事件(形状{type, op, tag, text, score};写一条、 查每条命中一条)——纯元数据:客户端只记一行日志,不进台词、不产生界面变化; 对局中她自己的召回 / 写入不发事件。GET /api/health的memory_rows是当前记忆条数(关闭 / 库不可用时为0)。
两个开关都在 ⚙ 设置 抽屉里、默认都关:关着时行为与没有这两个功能逐字符一致(不注入
时间块 / 画像块、不写对局记录、不建库)。画像与时间感知互不牵连,也与长期记忆解耦
(memory_enabled 关着画像照常)。
玩家棋风画像(profile_enabled,独立库 backend\data\profile.db,运行产物、已 gitignore):
- 记什么:每次点「🏁 结束」(
POST /api/summarize)成功出总结后,把这一局落一条结构化记录 (时刻 / 手数 / 结果 / 胜率·目差 / 双方失误 / 进入的定式族 / 聊天主题 / 每手用时)——games表 一局一行、只增不减;games是全局的最近 N 局记录,不是每维各自的来源(profile表没有局 id 列)。 - 四组维度(键名见设计 §6.2):开局偏好(星位 / 小目 / 三三 / 目外占比 + 常用挂角守角)、 行棋风格(激进 ↔ 稳健:战斗 / 打入 / 断 / 脱先 / 围空)、失误与薄弱方位(常亏手型 + 四个角·边的 薄弱方位)、性格与话题(聊天主题偏好、称呼与语气偏好)。
- 怎么攒:由对局记录经指数衰减 EMA 融合(
v ← v*(1-α) + x*α)——首样本=初始化(α=1), 其后单局 α 上限 0.25,且 α 随样本增多收敛到半衰期稳态(half_life=20局),置信度随局数上升。 无信号的维度不产出(不把「不知道」写成 0)。 - 怎么用:只在相关时注入画像块(插在时间块之后、铁律段之前);块内是模板拼出的人话, 末尾明确「别逐条念、别把说法当口头禅复述、也别提起这份印象本身」。
- 怎么看:
🪞 她眼中的你按钮只读拉一次GET /api/profile(各维值 + 置信度 + 局数 + 最近 N 局记录), 无副作用。管理面只有这一个只读端点;没有「手动修正 / 清空画像」入口(本轮不做)。 - 降级:库被删 / 只读 / 目录不存在 ⇒ 一律当「无画像」、只打一行日志,绝不影响对局;
写入 / 融合只在
summarize()里、落盘一律asyncio.to_thread(...),回合内零新增 LLM / 引擎调用。
时间感知(time_aware_enabled):四项事实——当前时间 / 星期、本局时长与单手用时、
距上次对局的间隔、连续下棋天数(按本地日历日切)。单手用时由后端在 POST /api/move
合法性通过后打点、下一手到达时结算,不要求前端配合。注入时间块(插在事件区块之后、
画像块之前);块内明确「只在相关时提、别把数字当口头禅复述、也别一次全念出来」——
守住「她变话多」这个反目标。缺什么就不说哪个(不补 0、不瞎猜)。
两个延伸:主动约战提醒(玩家连上 / 首次交互时一次、模板句、零新增 LLM 调用;它走
ai_notice 声明「这条没有语音」——不受 time_aware_enabled 门控,也只与**「有没有历史记录」**
耦合:greet_with_context 只读 profile_store.last_game_ts()、不检查 profile_enabled(store 无条件
装配)⇒ 从未开启 / 库里没有历史时关掉画像就随之消失,库里已有历史时提醒照旧会发)与
局末进步报告(并进现有 summarize() 的提示词、不新增调用)。
合成在后端、前端只播放(v3.5.0 起):她说什么、怎么切句、什么语气都由后端决定
(清洗 / 分句 / [[tone:…]] 剥离见 backend\app\speech_text.py,云端合成 + 缓存 + 降级见
backend\app\tts.py)。后端每发一条要念的台词,都会紧接补一条 WS speech 事件,用
turn_id + seq 与那条 message 一一配对;客户端 TTSClient 退化为播放器,只按指令出声:
speech.mode == "openai":按事件里的id去GET /api/tts/audio/{id}取回音频播放(后端把合成结果按 条数 32 / 8 MiB 双向 LRU 缓存;被淘汰或后端重启后取不到,这种情况只显示文字、不阻塞);speech.mode == "system":用text(后端已清洗好,不再含语气标记)交给 Windows 系统语音念;- 语音关闭(
tts_enabled=false)时后端一条speech都不发、也不发起任何合成请求,文字照常显示。
三种模式
- 阿里云
aliyun(默认):Voice Studio 实时语音合成,走 WebSocket 长连接(backend/app/tts_aliyun.py)。 只需两样:tts_aliyun_api_key(Voice Studio 签发的sk-…)与tts_aliyun_voice(平台音色 ID,UUID)。 一次合成 = 一条连接上的一个任务:run-task → task-started → continue-task → finish-task → task-finished, 音频按 BINARY 帧收齐后整条进 LRU 缓存——对上层与openai模式完全同形(speech.mode仍是openai, 前端不需要知道用的是哪家)。默认走format=pcm+sample_rate=24000:帧是裸 PCM,后端自己补 44 字节 WAV 头(引擎对 mp3 的支持因音色而异,PCM 是最稳那一档;24000 是 CosyVoice3 系引擎的原生率,22050 会被拒)。 连接会复用;空闲 45 秒 / 超 50 分钟主动换新,断线后下一次合成自动重连一次。 其余键(tts_aliyun_ws_url/_format/_sample_rate/_volume/_tn)见表,一般不用动。 - 云端
openai:任何 OpenAI 兼容/v1/audio/speech服务(如 OpenAI、自建 CosyVoice), 填tts_base_url/tts_model/tts_voice/tts_api_key——这几个字段现在由后端启动时读取 (改完要重启后端),加tts_timeout_sec(默认 30 秒)控制单次合成超时。 system兜底:Windows 系统语音,零配置即可发声;后端合成失败(连不上 / 超时 / 服务端拒绝 / 响应解析不了)时后端把这一条整条改发system指令(不报错、不改配置),下一句仍会重试云端。 左侧栏第 1 项「系统语音」也可以主动把整条链路切到它;aliyun模式下 Key / 音色 ID 没填全时 也是走这条路(不会静默没声音)。
已知取舍
- 有 4 条短提示只有文字、没有语音:
undo的两条(回档点不存在 / 已回档)与reset_game、set_allow_pass——它们是同步方法,无法await合成;文字照常显示。 busy会被合成占住:speech事件要带上音频 id,就必须先合成、后发事件,所以后端在一行 合成完成前一直busy(/api/undo、/api/reset等会据此被挡下,返回busy而不错乱棋局)—— 最坏每行数秒,player_move这种一次要念数行的会叠加。这是「合成归后端」的固有设计(计划 05 · Task 3)。 v3.11.1 起:busy一结束、她的落子一揭示,棋盘就立刻放开——不再等她把这整段话念完; 你一落子还会把她上一轮没播完的语音打断(台词照常显示)。- 两栏的生效范围不同:朗读方式 / 服务地址 / 模型(左栏「语音设置」)与长句合并(右栏「语音开关」)
现在只对后端重启生效(合成归后端,前端不再自行合成);「同步说话」「整段合成」「说话详略」「思考强度」
都是前端 / 请求级即时生效(后两个经
/api/config转发后端;tts_limit_len作为reply_style的旧键继续兼容)。 界面在这些项上会提示「重启后端后生效」。 speech与message的配对:后端保证speech紧随对应message之后发出;seq是 同一个turn_id内从 1 开始的序号,跨回合会重置——客户端必须按turn_id + seq配对, 不能只看seq。- 两条前端固有的错配窗口(音频序列本身仍然正确,只是归属 / 顺序可能错一位):
① 「这手不成立」是唯一一条
busy=false且带语音的台词,玩家可以马上再落一手与它交错; ② 本地被去重的台词会留下一个占位(tomb)却永远收不到speech,它可能「用掉」下一条的语音。 两者都有自愈:被顶掉的那句仍会在回合结束时放出文字,不锁死棋盘、不静音全局。 后续选项(暂未实现):让这条提示留在busy窗口内,或给message也带上turn_id。 - 流式的保证边界见第四节「流式输出」(成对 think 区间保证
token与台词都干净;「只剩收尾标记」与 未闭合think的畸形形态只保证最终message与 TTS 干净)。
两个语音端点(对局中由 speech 事件驱动,这两个是试听 / 手动用的;失败只回可读错误,不 500):
# 试听:直接返回 audio/wav;合成不出音频(语音关闭 / 系统语音模式 / 空文本 / 合成失败)→ 409
curl -X POST http://127.0.0.1:8000/api/tts/speak -H "Content-Type: application/json" \
-d '{"text":"你好呀"}' -o preview.wav
# 取一条已合成的音频(speech 事件 mode=openai 时带的 id);未命中 / 已被淘汰 / 后端重启过 → 404
curl -o line.wav http://127.0.0.1:8000/api/tts/audio/音频id
-d '…'的单引号写法在 PowerShell 7 / Git Bash 可用;cmd.exe 要改写成-d "{\"text\":\"…\"}"。
自建 CosyVoice 用 local 档:server\ 的 azure_tts_server.py 实现了 OpenAI 兼容的
/v1/audio/speech。把左侧栏「朗读方式」选 本地 CosyVoice(tts_mode=local),
并确认 tts_local_base_url 指向 http://127.0.0.1:9880/v1 即可;本地失败会回落系统语音,
不会偷偷产生云端调用。
- 自备 CosyVoice 权重,放到
server\pretrained_models\下; - 建虚拟环境并安装依赖(
server\requirements_win.txt、requirements.txt); - 双击
server\start_tts_server.bat手动启动服务(游戏不会自动拉起),服务监听http://127.0.0.1:9880。 开发时用dev_local.ps1会自动启动本地服务并等它就绪。
对局编排、LLM 与定式识别都在后端进程(backend\,FastAPI + REST + WebSocket)里,它是本机进程
(默认监听 http://127.0.0.1:8000)——只有 LLM 走可自定义的 OpenAI 兼容 API(语音的合成动作也在后端
进程里做,具体发往哪儿由 tts_base_url 决定,不必然是远端);KataGo 由独立部署的
云服务(katago_service\)提供,后端只连它(见下方「KataGo 服务部署」)。游戏客户端只做渲染、输入与语音播放,
不启动任何模型服务(没有 local 模式)。后端连不上时只在状态栏提示,不影响界面操作。
- 一键启动 / 自检:双击仓库根目录的
run_test_env.bat—— 默认启动全套(KataGo 云服务 → 等它就绪 → 后端 → 等它就绪 → 游戏);run_test_env.bat backend只起两个服务、run_test_env.bat tests跑全部门卫(后端全量 + KataGo 服务全量 +game\tests\下全部 headless 脚本 +--headless --quit自检,跑完自动还原game\project.godot)、run_test_env.bat stop停掉它们、run_test_env.bat restore单独还原game\project.godot。 引擎 / 权重 / analysis 配置与 Godot 可执行文件都可经KATAGO_EXE/KATAGO_MODEL/KATAGO_CONFIG/GODOT_EXE覆盖。 启动模式别把它的输出接到管道或重定向里(服务窗口会一直持有那个句柄,看起来像卡住)。 - 启动后端:双击
backend\run_server.bat(等价的手动启动:cd backend后python -m app.main), 监听http://127.0.0.1:8000。依赖fastapi/uvicorn/websockets/httpx/langchain-core/langchain-openai(见backend\requirements.txt;LLM 调用走 LangChain)。websockets不能省:裸装uvicorn不带 WebSocket 实现,缺了它/ws会拒绝升级(404), 客户端只会一直显示「未连接后端」。 - 配置:
config.json里也可命令行临时指定:"backend_url": "http://127.0.0.1:8000"
AzureGoGodot.exe --backend-url=http://127.0.0.1:8000(只影响本次运行,不写回配置)。 - 接口契约:
- REST:
/api/move|hint|analyze|chat|summarize|undo|reset|config|reveal|interaction|idle|mood/tick, 以及GET /api/state、GET /api/health,局后复盘/api/review(进 / 出)、/api/review/goto(只读重放)、GET /api/review/notes(逐手存档 + 真亏手账,见 2.3「局后只读复盘」),全盘复盘POST /api/review/full/segment(按段分析棋谱库对局 + Azure 强思考点评,见 2.3「全盘 AI 复盘」),棋谱库GET /api/games/GET /api/games/{id}/GET /api/games/{id}/sgf/POST /api/games/import/DELETE /api/games/{id}(见 2.3「棋谱库」),长期记忆管理面/api/memory/query|write|stats|forget(见第四节「长期记忆(跨局记得你)」),语音的试听 / 取音频 两个端点POST /api/tts/speak、GET /api/tts/audio/{audio_id}(见第五节「接语音」)。 - WebSocket:
/ws推送hello / message / state / info / busy / mood / joseki / tool / token / speech / memory / difficulty(tool是发言侧工具的开始 / 结束,只喂状态栏提示,不进入对话;token是可选流式增量,仅stream_tokens开启时才有、默认关闭,气泡最终内容仍以随后的message为准;speech是每条台词的 语音指令——mode=openai时按id取上面的音频端点、mode=system时用text交系统语音,与对应message同turn_id、seq递增配对,语音关着时根本不发;memory是记忆管理动作的元数据, 只记一行日志,不进台词、不产生界面变化;difficulty是自适应难度的档位 / 基线,只喂状态栏)。 GET /api/health返回{ok, katago_dead, katago_error, katago_url, llm_url, memory_rows}(字段都改为描述所连云服务: 地址 / 引擎是否还在 / 最近错误)。其中katago_dead与katago_error必须成对读:katago_dead表示引擎已经没了:start()探活失败、或分析时发生传输层失败(连接被拒 / 连接或 读取错误,相当于旧子进程时代的「管道断了」)都会置位;一次成功的分析会把它清掉(云服务重启后可自愈);katago_error则在每次分析失败时刷新(成功时清空);- 分析时的超时与 HTTP 状态响应(503 预热 / 401 / 500)只刷新
katago_error、不置katago_dead——服务还在、只是这一次没答好。所以「katago_dead=false+ 新的非空katago_error」=服务在线但这次 没答好,「katago_dead=true」=引擎(或到它的链路)真的没了。运维自检以katago_error为准; katago_url是后端当前连接的 KataGo 服务地址,便于确认连的是哪台(留空时客户端回退默认回环地址http://127.0.0.1:8100);memory_rows是长期记忆库里的记忆条数(未启用 / 库不可用时为0)——一眼看出记忆库是否在用。
- REST:
- 后端配置:后端进程用环境变量
AZURE_CONFIG指定config.json;未设置时依次找<当前工作目录>\config.json→backend\config.json(backend\app\config.py的load)。backend\run_server.bat会先把..\AzureGoGodot\config.json写进AZURE_CONFIG,该文件不存在时改用..\game\config.json——两个都在时用前者。 - 注意:后端依赖本机 Python 环境,需按上面的命令自行启动;后端不在时游戏只提示、不崩溃。
KataGo 服务没起或
katago_url写错时,后端启动横幅会打印「[KataGo] 云服务不可用:…」并继续运行—— 对局时该手退化为可读提示、busy必归位、棋盘不会锁死;把地址改对后重启后端即可。
KataGo 不再由后端进程拉起,而是独立部署的云服务(katago_service\):FastAPI 里常驻一个
katago analysis 子进程,启动即后台预热(OpenCL 首次调优挪到部署阶段),对外只暴露
POST /analyze(请求/响应都是引擎原文)与 GET /health。后端通过 katago_url 连它,因此服务可以
先起、也可以单独重启,对局侧只会拿到「云服务不可用」的可读提示。
- 机器要求:能跑 KataGo 的机器(Windows + 显卡驱动;纯 CPU 也能跑,只是慢)。引擎本体
katago.exe/*.dll、权重model-b18c384nbt.bin.gz、analysis 配置文件(如analysis_example.cfg) 都归这台机器持有,不入本仓库(见第一节「模型与资产」)。 - 引擎配置的两个部署要点(护栏,不是前置条件):
- 建议显式设
analysisPVLen——引擎配置里它默认是注释掉的(本项目default_gtp.cfg/analysis_example.cfg都是),显式写上只是一道防引擎默认值漂移的护栏;注意它只是pv长度的上限, 真实决定因素是「该候选被访问的次数」(visits)——所以它不是「靠它凑够五步」、也不是前置条件。 - 三线那一次分析要更深——本项目不改全局
max_visits,而是只给「玩家落子后局面」那一次分析传max(pv_max_visits, max_visits)(只增不减):随包game\config.json是max_visits=100⇒ 那一次取 150;katago_timeout_ms的代码默认已提到45000(随包模板仍30000)。 代价:那一次已有分析会变慢(单线程实测 200 visits ≈ 9.8s),但不增加调用次数——post复算 / 提示 / 局面分析 / 总结仍用默认深度(全局加深会让每回合都变慢)。
- 建议显式设
- 装依赖:
pip install -r katago_service\requirements.txt(fastapi/uvicorn;httpx只给测试用)。 - 启动:双击
katago_service\run_server.bat。脚本必须原样跑——它自己cd到仓库根目录, 因为katago_service是包,python -m katago_service.server要求父目录在sys.path上。 等价的手动命令是在仓库根目录执行:参数留空则由环境变量兜底:python -m katago_service.server --port 8100 --token <令牌> --katago-exe <katago.exe> --model <权重> --config <analysis cfg> [--analysis-threads N]KATAGO_HOST/KATAGO_SERVICE_TOKEN/KATAGO_EXE/KATAGO_MODEL/KATAGO_CONFIG(对直接执行与经run_server.bat两种方式都生效)。默认监听127.0.0.1:8100。 - 启动顺序(重要):先起 KataGo 服务,轮询
/health直到state == "ready",再起后端。 服务启动即后台预热(加载模型 / OpenCL 首次调优可能要几十秒到几分钟),预热期间不要启动后端: 后端在 lifespan 里会等它就绪(代码常量READY_TIMEOUT_MS=180 秒,不可配置)才绑端口,这段时间里/api/chat、/api/state与/ws都不可达,游戏只会显示「未连接后端」——看起来像后端根本没起来, 其实它是在等 KataGo 预热(预热完成、后端绑上端口后会自动恢复)。 - 自检:
curl http://127.0.0.1:8100/health→{"ok": true, "state": "ready", "katago_error": "", ...}。 模型加载期间state是warming(此时/analyze立刻回 503 可读错误,不会挂到客户端超时,也不会 让游戏卡住);引擎起不来则是dead,katago_error给出原因。日志在katago_service\logs\katago\。 - 鉴权与暴露面:服务配了 token(
--token/KATAGO_SERVICE_TOKEN)就必须带Authorization: Bearer <token>,后端读katago_token对应它;不配 token 只允许本机回环调试。 跨机部署要显式--host 0.0.0.0或KATAGO_HOST=0.0.0.0——此时服务启动会打印一条「对外监听且未配置 token」的告警(只是告警、不阻止启动,回环调试仍可无 token)。 务必只在内网 / TLS 反代之后暴露,并配上 token。 - 端口/地址要对上:服务默认 8100、后端
katago_url默认http://127.0.0.1:8100;换机器或换端口时 两边一起改(服务侧--port,后端侧katago_url)。katago_url/katago_token/katago_timeout_ms都在后端启动时读取,改完重启后端。
game\export_presets.cfg 保留着导出预设,可以用 Godot 自带的「项目 → 导出」自行导出(PCK / 可执行文件)。
原先的 PCK 打包脚本(tools\ 下那个 PowerShell 脚本)已随 v3.0.0 删除,那套「引擎本体 exe + 替换 PCK」
的分发流程不再维护。
本项目源码采用 Apache License 2.0(见 LICENSE)。
server\ 下的 CosyVoice 及其 third_party\Matcha-TTS 为各自上游项目,遵循其原始许可。
KataGo 引擎与权重遵循其上游许可。
- 每次从主界面进入都是新的一局(v3.12.1 / PR #5):封面主按钮改为「开始新对局」,进入前先清本地
棋盘 / 手数 / 胜率 / 目差 / 俗手 / 复盘入口,再静默调用
POST /api/reset {"silent": true}; 后端忙时挂起,等busy=false补发,避免「她思考时回主界面再进来」保留上一局。首次启动也走同一条 路径清掉后端可能残留的棋局;旧「继续对局」语义退场。 - 化身 VRM 路径恢复(v3.12.2 / PR #5):
vrm_path写相对路径时依次尝试 exe 同目录 →game/→ 仓库根目录 → 工作目录,取第一个存在的文件并在失败日志里列出全部候选;源码运行可直接 使用仓库根的Azure.vrm,发布包把Azure.vrm与 exe 放一起即可。 - 「重来」清账补全(v3.12.2 / PR #5):后端同时清空上一局未揭示的落子(
_reveal_pending)与待记账的 情绪偏航(_drift_pending);前端清空复盘计数 /_reviewing与复盘按钮状态,重来后不再等一条不会来的语音。 - 局后按钮冻结(v3.12.2 / PR #5):「结束」流程走完后,提示 / 局面分析 / 结束 立即置灰, 重来保持可用作为开新局出口;进入复盘后重来也一起冻结。
- 点目文案守卫(PR #5):
estimate_count_line/estimate_lead_line显式接收字段,JSON 的 float 正常 显示,缺字段 / 脏值给「形势: --」,不再把脏值硬报成 0。 - 视觉(本地):地板改为圆形胡桃木露台(半径 22,棋盘落在圆心),64 段环形矮护栏用
MultiMeshInstance3D合成两个 draw call;台面手写三角扇 + 平面投影 UV(木纹保持直纹), 侧壁给出台面厚度;地板albedo_color压到 0.80 融入宣纸 / 墨色,背景仍是相机上的青绿山水横幅。 - 门卫:后端全量 861 OK(新增「重来清账」1 项 +
ResetSilentTest2 项);前端 18 支脚本全 PASS (新增test_avatar_path.gd9 项 /test_new_game.gd8 项 /test_estimate.gd17 项;test_title_screen.gd19 项、test_review_mode.gd50 项、test_view_helpers.gd45 项)。
- 棋谱库 + SGF(v3.11.5 批):主界面第三键打开全屏模态纸签——对局列表(日期 / 手数 / 黑胜率)、
右侧小棋盘逐手回看、局后总结、导入 / 导出 SGF、删除;库在
backend\data\games.db(SQLite), 导入前后端逐手校验;全新库首次打开自动塞入 24 手内置示例局。列表 / 详情不碰当前对局、零引擎调用。 - 全盘 AI 复盘(v3.11.5 批):棋谱库选中一盘 → 主棋盘只读本地回放,Azure 按分段(最多 8 段 /
每段 ≤48 手)用强思考从头讲到尾;上一段文字与语音播完才请求下一段,TTS 不堆积。
台词可带
{行为}标签,在文字 / 语音出现的那一刻触发表情;退出时拉回当前对局,全程不改后端棋局。 - 自适应难度(v3.11.5 批):默认关。开启后按玩家棋风画像、聊天关键词与最近 6 手表现, 在她算好的候选里挑一个更弱但仍在容差内的落点;档位永远夹在基线 ±1 档,每 4 手最多调整一次; 关闭=落点与原来完全一致。
- 领地热力图 / 音效 / 战报(v3.11.5 批):热力图复用既有
ownership分析(零额外引擎调用); 落子 / 提子 / 虚着 / 终局音效纯前端事件驱动;局后一键生成 1200×1600 PNG 战报(独立 2D 渲染)。 - 局面点目(采纳 PR #4 的
estimate):点「局面分析」时,后端复用同一次引擎分析,把归属明确 (|归属| ≥ 50%)的定型点收敛成estimate事件——棋盘上画归属小方块,左上角弹形势纸签 (黑 / 白目数与黑方视角目差 + 贴目)。活子不叠标记、被点死的对手子画在棋子上;玩家下一手 / 局面变化 / 重来 / 进复盘时自动收起。不新增引擎调用、不产生台词或语音、不进任何 LLM 上下文。 PR #4 的「每次进对局开新局」在 v3.12.1 单独落地(见上),旧「继续对局」语义同时退场。 - 最终微调(发布后复盘):棋谱库首访播种加线程锁(连点刷新不会出现两盘示例局)、
全盘复盘的分段请求在读库回来后再核一次
busy(两次并发请求不会并行跑两段)、 棋谱库的列表 / 详情回包带请求序号(慢回包不会覆盖后来点选的那盘;纯刷新不重置回看位置); 领地热力图复用同一块 Image / ImageTexture(每手只更新像素,不再新建纹理);定式卡片收起时 跳过每帧的棋盘投影(常态零开销);形势纸签与定式卡片互相让位(不再叠在左上角); 战报的字体只建一次(一张战报一百多个 Label 不再各建一个 SystemFont); 全盘复盘段间先等 WS 台词落地(HTTP 回包先到时不会「上一段还没开口就排下一段」); WS 连接加 8 秒超时(远端地址黑洞时不会一直停在「连接中」),重连时重推一次当前设置。 - 棋谱库数据健壮性:
games.db的 NaN / inf 时间戳与形势值一律落成「未知」, 不会再让/api/games的 JSON 序列化整页 500。 - 最终体验调整(v3.12.0 收尾):朗读方式改为 系统 / 本地 / 云端 / 阿里云 四档同级;
local档只走本地 CosyVoice/MeloTTS(失败回落系统语音、不产生云端调用),dev_local.ps1会启动本地服务、等/health就绪并把配置切到local。 v3.12.1 起每次从主界面进入都是新的一局(见 v3.12.2 一节)。开始封面整体下移约 8% 屏高。 3D 背景改为相机上的一张青绿山水画(参考 Art-Of-Ink 的构图与配色,AI 生成的独立素材), 地板改用用户提供的 walnut_veneer_02 4K(漫反射 + 法线降采样到 2K),铺成圆形木露台并略微压暗; 不再叠 3D 纸幕 / 木框 / 棋笥 / 暗角 / 雾 / 暖灯。 - 门卫:后端全量 858 OK(新增
test_estimate.py8 项 +test_events事件形状 2 项 +EstimateWiringTest3 项 + 棋谱库并发播种 / NaN 清洗 2 项 + 本地 TTS 档 3 项); 前端 15 支脚本全 PASS:test_config.gd192 项、test_title_screen.gd22 项、test_view_helpers.gd45 项。
- 语音自然打断:玩家落子时不再
stop()硬切——当前句先 0.25 秒音量淡出(文字立即照常显示), 然后随机播一句预制短句(filler_interrupt_1..7.wav:嗯? / 这么着急? / 这么快就有思路啦? / 唔…… / 好快! / 可以呀! / 哦?),新合成语音等短句 + 0.12 秒气口后再开播。 预制句走独立的_filler_player,不会和正式语音叠在一起;短句缺失 / 语音关闭时自动退回「淡出后直接接新语音」。 - 定式讲解角色修正:定式库的
key是按「角上先落子一方」写的;识别结果新增角主映射(line[0].c), 进入 / 分岔 / 收尾提示词显式声明「你 / 我 / 自己 / 他 / 对方 / 别人」的对应关系—— 再也不会出现「他占角、我守外围」这种主客颠倒;同时去掉「开始学 / 上课 / 这一课」的课堂口吻。 - 摸头判定修正:放大视角后头部出画时,旧判定永远点不到;现在按屏幕空间「头 → 髋 → 脚」折线命中, 点她可见的上身或腿也算摸头。
- 界面字号:默认字号 14→15、按钮 15/13→16/14、印章字 17→18。
- 本地 TTS 可见性:左侧栏与启动面板新增「实际朗读」行;
openai模式显示本地优先地址,aliyun模式显示 Voice Studio 状态,不再被空的云端地址误导。
- 主界面运镜(v3.11.2 封面的再设计):主界面不再是「满屏不透明纸幕」,而是远看棋盘:
- 远看取景
Main.CAM_MENU_YAW / PITCH / DIST = 10° / 36° / 64——棋盘完整落在画面下半部; 纸签偏上(TitleScreen的 CenterContainer 取 0.02..0.55 高度带),纸面 alpha 0.66、满屏Veil只留 0.40, 棋盘透过纸面看得见。 - 入场(
Main._play_enter_game):封面fade_out(0.35)→board.fly_home(1.05)(Board3D.fly_to的 smoothstep 缓动)→_show_game_chrome(0.35)(右侧面板从画面外滑入、两侧贴边开关亮起)→ 才起服务 / 问候。 - 回主界面(
_play_exit_to_menu):工具栏滑出(0.22)→fly_to(远看, 0.95)→ 封面fade_in(0.45);Esc与「开始新对局」都走这一对函数,完全对称。 - 不能回退的规则:运镜期间
_transition=true(Esc 不叠加、按钮不重入);_title_open一直为真到入场结束 (棋盘全程锁着);Board3D的飞行中_unhandled_input整体让路;首帧用board.snap_panels(false,false)(主界面没有侧栏)——不要在这种状态留出右侧面板的位置,否则棋盘偏在一边(--title截图能看出来)。 - 顺带修好的小毛病:
Board3D.set_panels现在平滑改变侧栏让位(_step_panel_bias,指数逼近), 手动开合侧栏 / 入场滑入时棋盘不再瞬移一下。 - 局后出口:
🏁 结束走完后,「复盘」旁边多一个🏠 返回主界面(_btn_home), 点它走与Esc相同的反向运镜回封面;进复盘时这一格先隐藏(那一行已有四个按钮,避免挤爆侧栏),✅ 退出复盘后恢复。 - 门卫:
test_view_helpers.gd补 7 项(远看取景 /fly_home(0)立刻到位 / 飞行状态 /fly_ease两端与单调 / 越界夹取);test_title_screen.gd补 4 项(纸幕半透明 /set_alpha/ 淡出 / 淡入)。
- 远看取景
- 主界面(纸幕封面):新增
game/scripts/ui/TitleScreen.gd(class_name TitleScreen),启动后先显示一块 宣纸纸幕——3D 棋盘 / 化身 / 侧栏都在纸幕后面(半透明 0.90 宣纸 + 低透明度纸纤维),中间是朱砂方印 「亚」+ 标题 + 副标题 + 三键(开始对局 / 设置 / 退出)。视觉全部取自GufengTheme(panel/seal/apply_seal_button/apply_button/ 楷体),不新增任何配色或几何常量。- 首次「开始对局」→ 释放
_ready的等待,随后照旧弹「出发前的准备」设置面板;主界面里的「设置」 复用同一块面板(主按钮变「保存设置」、说明改为保存后回封面),保存过就不再弹第二遍(_setup_done)。 Esc在对局与主界面之间切换(_menu_keys走最早的_input;设置面板 / 备注框开着时不抢)。 封面开着时review_lock_state(..., menu_open=true)冻住棋盘、四个按钮、聊天与回档 / 开关, 闲置计时与主动搭话一起停摆(_process的hold与搭话条件都算上)。- 首次启动 / 对局中回来只换主按钮措辞(开始 ⇄ 继续),其余一字不动。
- 调试:
-- --title --shot=menu.png只画封面;--shot/--demo/--setup默认跳过封面。 - 真实坑(已修):
TitleScreen._ready里必须用set_anchors_and_offsets_preset——set_anchors_preset会保留「加入时」的零尺寸、把 offsets 算成 −父宽 / −父高,纸幕实际是0×0(实机截图里只有中间那张纸签)。门卫tests/test_title_screen.gd钉住纸幕可点与三键信号。
- 首次「开始对局」→ 释放
- 思考中的短句(20 句 JSON,每秒一句):新增
game/data/thinking_lines.json({"lines": [...]},20 句, 古风口吻、去重、每句 ≤ 30 字)。Main._update_thinking在占位气泡里每THINKING_SWITCH_MS = 1000毫秒 换一句(randf()给next_thinking_index,随机且绝不与上一句相同);_show_thinking立即给第一句。- 解析是纯函数
parse_thinking_lines:接受{"lines": [...]}或裸数组,非字符串 / 空串 / 重复句丢掉, 上限 64 句;JSON.new().parse()静默失败(不用JSON.parse_string,否则坏 JSON 会往控制台喷错误)。 - 回退:文件缺失 / 写坏 ⇒ 短句表为空 ⇒ 气泡退回旧的「Azure 思考中 + 省略号每 450ms 动一下」, 绝不因为一句文案把等待气泡搞崩。
- 导出:
game/export_presets.cfg的include_filter补了data/*.json——Godot 默认只导出资源, 非资源文件不进 PCK(漏了它,导出版会静默回退成旧动效;不要写成*.json,那会把带 Key 的config.json一起打进包)。 - 门卫:
tests/test_thinking_lines.gd19 项(出货文件 20 句 / 无空句无重复 / 脏值与裸数组 / 200 次轮换不连复 / 首句可由 rng 钉死 / 间隔就是 1 秒)。
- 解析是纯函数
- 门卫:
test_review_mode.gd补 2 项(主界面开着 ⇒ 棋盘锁 / 按钮灰 / 聊天禁用 / 别的入口也冻; 关着 ⇒ 回到原状态),test_title_screen.gd新增 14 项(纸幕挡点击 / 三键信号 / 开始⇄继续 / 显隐)。
- 她「说多少」可调:右侧栏原来的「短回复」复选框扩成三档下拉(
说话:精简 / 适中 / 详细), 对应后端reply_style = 0 / 1 / 2——0沿用原LEN_NOTE(点评 1 句、聊天最多 2 句)、1不加额外约束、2换成新的LEN_VERBOSE_NOTE(2~3 句、约 80 字内)。只改提示词末尾的长度约束, 人设 / 阶段提示/ 定式讲解(LEN_TEACH_NOTE)一字未动;经/api/config即时生效并写回config.json。 旧键tts_limit_len保留为兼容别名(true⇔reply_style=0):文件里只有旧键时按旧语义迁移,save()两个键一起写,旧版本客户端 / 后端都读得懂;agent.limit_len也做成属性(读写映射到 0 / 1), 既有调用点与门卫不动。 - 她「想多久」也能当场调:⚙ 设置抽屉新增
思考强度:快速 / 中等 / 强思考(与启动面板同一字段llm_think_mode)。以前它只在后端启动时读一次,改完必须重启后端;现在/api/config接受think_mode,LLMClient.set_think_mode()同时改推理预算与扩展字段(llm_think_extra_body跟着进 / 出, 撤档时用基础字段重算、不留上一档的键),服务端拒过扩展字段时保持静默降级、不再加料。 - 说话过程中也能下棋:棋盘锁原来把「她还有话没播完」(
_speech_hold)也算进去,于是后端早就空闲、 玩家却必须把整段语音听完。现在locked只看busy/ 点下就锁 / 还有没揭示的落子 (SpeechPairing.pending_reveal():她的落子宣告一显示就POST /api/reveal,此前绝不放行, 免得玩家先落子、她的子过一会儿才冒出来)。_speech_hold仍留着,但只挡四个功能按钮与复盘入口。 - 一落子就打断她未播完的话:
TTSClient.stop()给正在播 / 排队中的每条补speech_skipped(配对表照常 finish + 显示文字);还在取音频(HTTP 在途 / 排队)的那几条由Main自己收账 (_speech_fetch_current+ 清空 FIFO),在途回调见到played直接判done,不会补播、也不会留下got && !played的悬空记录(那种记录会锁死棋盘)。 - 门卫:后端
test_config/test_prompt_chains/test_llm/test_main补 15 项 (三档提示词、limit_len别名、运行期改档与扩展字段进出、脏值 / 越界 / 旧键迁移); 前端test_config.gd补 19 项(读写往返、旧键迁移、setter 推送)、test_review_mode.gd补 2 项(说话中不锁棋盘 / 未揭示必须锁)、test_speech_pairing.gd补 5 项(pending_reveal的四种形态)。
- 语音合成改用阿里云 Voice Studio 实时语音合成(WebSocket):
tts_mode新增aliyun并作为模板默认值。 协议按官方《实时语音合成 WebSocket API 参考》实现:建连(Authorization: Bearer <key>、可选X-TTS-TN: enable)→run-task(音色 UUID / format / sample_rate / rate / volume)→task-started→continue-task→finish-task(不可省略)→ 收 BINARY 音频帧直到task-finished。 连接复用(一条连接串行跑任务),空闲 45 秒 / 超过 50 分钟主动换新,断线后下一次合成自动重连一次。 - 原有 TTS 一行逻辑都没改:新增
backend/app/tts_aliyun.py的AliyunTTSClient继承TTSClient, 只重写_synthesize(整段文本走一个 WS 任务)/enabled/close;文本清洗、分块、LRU 缓存 (32 条 / 8 MiB 双向淘汰)、「speak()绝不抛异常」、「失败整条降级system」全部继承。tts.py只把"aliyun"加进了TTS_MODES。上层契约不变:speech.mode仍是openai, 前端Main.gd/TTSClient.gd/SpeechPairing.gd的播放链路一行未改(只有设置面板新增两个输入框)。 - 配置:新增
tts_aliyun_ws_url/tts_aliyun_api_key/tts_aliyun_voice(平台音色 ID,UUID)/tts_aliyun_format/tts_aliyun_sample_rate/tts_aliyun_volume/tts_aliyun_tn;tts_aliyun_api_key与tts_aliyun_voice在左侧栏「语音设置」里有输入框(Key 框按密码样式显示)。 Key / 音色 ID 没填全时直接回落系统语音(控制台一行[TTS-阿里云] …),不会静默没声音。 - MIME 修正:
/api/tts/audio/{id}与/api/tts/speak之前一律标audio/wav,现在按字节嗅探 (WAV / MP3 / OGG),不会再标错。 - 启动设置面板:原来那节「语音 TTS API / 服务地址 / 音色」换成 「阿里云语音合成(Voice Studio)」——
只填
API Key(密码框)与音色 ID(平台音色 UUID),写回tts_aliyun_*;不改tts_mode(朗读方式是左侧栏的下拉,启动面板不该每次开局把它改回去),面板底部一行提示当前朗读方式。 - 实测口径(真凭证跑通):该引擎
format=mp3/sample_rate=22050都会被 400 拒 (only supports sample_rate 24000 natively/Unsupported format: 'mp3'),所以默认改成pcm+24000:后端把收齐的裸 PCM 补 44 字节 WAV 头再入缓存。真实合成已验证 (94124 字节 WAV,speak -> openai)。 - 门卫:新增
backend/tests/test_tts_aliyun.py23 项(本机假 WS 服务:帧形状 / 同一 task_id / 鉴权头 / 连接复用 / 断线重连一次 / task-failed / 无音频 / 连接被拒 / 缺 Key 缺音色 / 脏配置钳位 / 未知事件前向兼容 / 应用装配与 MIME 嗅探 / 默认 pcm / WAV 头自洽 / PCM 帧补头成可播 WAV),test_config.py补 4 项(新键读入、脏值回落、tts_mode路由),game/tests/test_config.gd补 4 项(aliyun不被归一化冲掉、阿里云三键读入),game/tests/test_tts.gd补 2 项(aliyun/system不算慢引擎)。
- 去掉侧栏的黑框:
panel()不再用 22px 墨框,换成frame_line.png(宣纸 + 1px 发丝纸边 + 内侧极淡压暗)。 墨框贴图frame_ink.png只留给定式卡片——它是一张"纸片",有装裱边才像纸片。 - 标题条不再是黑底:
#2C2A26墨黑 → 黛青#2B423D(DEEP);按钮按下态同步改黛青,弹窗遮罩改暖墨 (0.09/0.08/0.06,不用纯黑)。 - 清掉高饱和荧光色:
- 目差数字
#27AE60→ 黛#3E5C58; - 棋盘「最后一手」圆点:青
#4ECDC4/ 亮红#E74C3C→ 象牙 / 朱砂; - 定式结构点:荧光青 / 亮金 → 黛 / 赭石;
- 桌面(
Ambience地面)纯白#F1F4F2→ 暖纸#BDB4A0(受光后 ≈#D6CCB5);棋盘木色压深一档、主光 1.2→0.95。
- 目差数字
- 对比度补齐(WCAG 实测,
tools里无依赖的小脚本可复算): 墨/纸 13.3:1、淡墨/纸 6.7:1、纸白/黛青 10.0:1、朱砂/纸 5.9:1、黛/纸 6.8:1、 赭石/纸 4.75:1、印章纸白/朱砂 5.9:1;禁用按钮 2.5:1(规范对禁用态豁免)。- 修掉一个真 bug:玩家气泡的文字是
Color.WHITE,而气泡在 v3.10.0 变成了纸底 ⇒ 白字压白纸=看不见。 现在三种气泡一律墨字。 - 所有灰字(
#999/#737373/#8C8C8C…)统一收到淡墨#5A5249或墨;标题条副标题 0.72→0.88 alpha; 说话人名字改朱砂小字;10–11px 的说明文字抬到 11–12px。
- 修掉一个真 bug:玩家气泡的文字是
- 版式更方便操作:
- 按钮 / 输入框 / 下拉框的最小高度 24 → 32px(
content_margin5→7); - 左栏整栏可滚(
ScrollContainer):窗口一矮,下面的语速/音量/试听不再被裁掉; - 开场对话框拆成「滚动内容 + 固定在底部的动作区」:
开始对局永远是朱砂主按钮,内容再长也不用滚到底; - 滚动条改 6px 墨线(默认灰条在纸上太抢眼);右栏分节处补发丝墨线。
- 按钮 / 输入框 / 下拉框的最小高度 24 → 32px(
- 门卫:前端十支全 PASS(含
joseki_card79 项),--headless --quit无SCRIPT ERROR。
- 全局 UI 换成古风(书卷气):宣纸 + 墨线 + 朱砂印。样式只有一个出处
game/scripts/ui/GufengTheme.gd,Main.gd/JosekiCard.gd不再各自配色。- 材质(程序化生成,共 ~80KB,
game/assets/ui/):paper.png(256×256 可平铺宣纸:低频云斑 + 细颗粒- 纤维)、
frame_ink.png(128×128 九宫格手绘墨框:抖动双线 + 四角积墨)、grabber.png/check_off.png/check_on.png(墨色滑杆圆点、墨框复选框、朱砂勾)。
- 纤维)、
- 字体:
SystemFont优先 KaiTi / 华文楷体,退回宋体 / 雅黑(不引入字体文件); 复核过 Windows 上KaiTi → simkai.ttf能解析。 - 形状:圆角全部取消;按钮=纸签 + 底部墨线(悬停朱砂字、按下反白);主操作(结束)是整块朱砂印; 面板=宣纸 + 墨框;标题条=墨底 + 朱砂横线 + 单字印章(右栏「弈」、左栏「音」); 气泡=纸签 + 左侧墨竖线(玩家那句是右侧朱砂竖线);分节用发丝墨线。
- 定式卡片:宣纸 + 墨框 + 投影,棋盘线改墨色、第 k 手环与候选外环改朱砂、候选白子改成象牙白, 结构点徽标从"色块胶囊"改成方印(黛 / 赭石 / 朱砂 / 淡墨四种印色)。
- 去 emoji:所有 UI 文案改纯文字(
提示/局面分析/重来/结束/复盘/设置/语音朗读…), 只保留▶ ◀ ▸ ✕这类排版符号。
- 材质(程序化生成,共 ~80KB,
- 顺带修两处(都是换皮时暴露的):
- 侧栏展开时卡片会藏在侧栏底下:
_follow_joseki_card()原来只躲棋盘,现在也躲已展开的侧栏 (与避让共用_panel_rects());折叠按钮改透明底——它原先把面板标题的第一个字盖掉了。
- 侧栏展开时卡片会藏在侧栏底下:
- 门卫:前端十支全 PASS(
joseki_card79 项,含新增的"底色不透明"),--headless --quit无SCRIPT ERROR。
- 定式参考图卡片:底色改为不透明(
BG的 alpha0.82 → 1.0)。半透明深色压在浅色棋盘/窗口上会 泛灰,参考图看着像块脏玻璃;现在是一块实心面板,小棋盘与手顺数字的对比度也随之稳定。 避让态(faded)不变:仍然只把外壳压到 40% —— 那一态存在的意义就是「卡片被棋盘压住且无处可挪时 别挡住棋盘」,真做成不透明就退回到 v3.9.2 修掉的那个毛病。 - 门卫:
joseki_card78 → 79(新增:底色 alpha 恒为 1.0)。
- 「她还没应手,玩家又能下一手」(根因:
busy没有所有权)。- 路径:
_finalize_just_moved在她这一手讲完就_set_busy(False)(设计如此:不让玩家等后台的 情绪小调用),紧接着_mood_llm_refine()还要跑 1~20 秒——它限频 15 秒,所以每两三手才出现一次 (这正是「某些时候」)。玩家在这段窗口里落子 ⇒ 新回合把busy置真;旧回合的finally回来 无条件清位,就把新回合的锁解了 ⇒ 她还没应手,棋盘已经放行,玩家可以再下一手。 - 修法:
busy置真时发放所有者编号(_set_busy(True)返回它),五个公开方法的finally改走_clear_busy(owner)——只有当前拥有者能清位,旧回合对新回合无效。不传 owner(看门狗 / 旧调用点)仍是旧语义:无条件清位。 - 前端防线 ①「点下就锁」:
busy事件回来之前还有一瞬,期间再点会发出第二手(后端会挡下,但 棋盘上已经乐观画上了那颗子)。现在点击当场置本地闩并并入review_lock_state,由busy/state/ 拒绝回包解开,另有 12 秒兜底解锁(回包与事件全丢也不会把棋盘锁死)。 - 前端防线 ②「看回包」:
POST /api/move忙时回{"ok": false, "busy": true, "state": …}(HTTP 仍是 200),旧代码发完不看 ⇒ 那颗乐观子再也撤不掉。现在解析回包:被拒就按权威快照重画 棋盘,并在状态栏说明「这一手没走成:她还在应手」。
- 路径:
- 门卫:后端 738 OK(新增 3 条:旧回合不许解新回合的锁 / 拥有者自己清位照常生效 / 不传 owner
保持无条件清位);前端十支全 PASS(
review_mode 33:新增「点下就锁」与「闩释放后与三参调用逐项一致」);--headless --quit无SCRIPT ERROR。
- 角内取景 13 路 → 9 路,候选点菱形 → 白子:
- 9 路:实测 25 条库线最宽只用 8 路(只有「星位·守角后被挂角」到 8),9 路窗口装得下全部。 格距 17px → 26px、石子直径 15px → 23px,角内那颗子终于是「一颗子」而不是一个点。
- 越界判定扩到全部点:以前只看手顺(
line),现在候选点与走错点也算——窗口收小后, 外线候选(如 12,3)或远处的走错点不会再被静默画到画外,一律退回 19 路全盘。 - 候选点画成白子:白底 + 琥珀外环(区分「待选的一手」与「已经落下的白子」)+ 序号; 退回 19 路时石子只有 ~10px,白子加数字会糊成一团,那时只画一圈琥珀环,序号交给底部图例。
- 门卫:前端十支全 PASS,
joseki_card76 → 78 项(新增:候选点越界退回 19 路 / 走错点越界退回)。
- 定式参考图卡片:能关、能看清。此前卡片没有任何关闭控件——只有
formed(库线走完)、show_card=false或 120 秒兜底 TTL 能收,玩家想撤掉它只能等。- 关闭入口:右上角自绘 ×(22×22 热区,悬停变手形),命中判据是纯函数
hit_close(); 关掉后记住(name, kind),同一结构点不再自动弹回(换结构点 / 换定式照常弹),TTL 120s → 60s。 - 头部:课名 15px + 结构点徽标(进入=青 / 分岔=橙 / 走错=红 / 收尾=灰),一眼分清这张卡在讲什么。
- 角内 13 路放大:定式只看一个角,19 路全盘会把 3~8 颗子摊成一片。按事件的
corner取 13 路 (格距 11px → 17px,石子直径 9px → 15px);任何一颗子落在窗口外就自动退回 19 路全盘 (参考图绝不裁子),角名不认识同样退回全盘。 - 手顺数字叠在石子上(黑子白字 / 白子黑字,19 路回退时字号不足就不写,避免噪点);星位小点 只画落在窗口里的。
- 候选定式名:
candidates[].name后端一直在发,以前白扔;现在底部按序列出(最多三条,超出用 …), 与卡面上的菱形序号一一对应。 - 外观:圆角 10px + 投影 + 头部细分隔线;卡片 236×288(这个高度是算过的:1280×800、侧栏展开时 棋盘顶在 y≈323,卡片再高就会一出生就压住棋盘、只能退回兜底位)。
- 关闭入口:右上角自绘 ×(22×22 热区,悬停变手形),命中判据是纯函数
- 门卫:前端十支全 PASS,
joseki_card62 → 76 项(新增:关闭热区位置 / 命中判据 / dismiss 收起 / 手关过的同一结构点不再弹 / 换结构点照常弹 / 13 路取景 + 四角原点 / 越界退回 19 路 / 角名不认识退回 / 徽标文案 / 候选图例过滤脏值 / TTL 60s 两侧)。
- 定式参考图卡片压在棋盘上只剩一块灰影(用户截图复现,
--shot探针一比一同形)。三处成因一起修:- 淡出只淡外壳:原先避让时把整卡
modulate.a压到0.28,12px 标题 + 0.55 网格 + 黑白子一起褪成 灰雾。现在改成外壳系数FADED_SHELL = 0.40,只作用于底色 / 网格 / 边框;标题、棋子、高亮环、 候选点恒不透明(JosekiCard.dim()是纯逻辑,树外可断言)。 - 落位跟随取景:
_place_joseki_card()原先只在启动与窗口尺寸变化时算一次,玩家右键旋转 / 滚轮缩放 /C·Home换视角 / 开合侧栏之后棋盘会长到卡片下面。现在每0.25s检查一次:位置仍是我们摆的 且被棋盘盖住才重摆;玩家拖过就永不接管;一个可行位都没有时留在原地(不做「从一个被压住的 位置跳到另一个」的无用移动)。 - 重叠判据去掉「虚胖」:旋转后的棋盘是菱形,原先用它的外接矩形判重叠,两腰的空白三角也被算成
棋盘。现在按 4×4 棋盘格子矩形判(
_board_cell_rects());卡片仍然只认矩形(U39 契约不变)。 - 顺带可读性:卡片 200×222 → 240×260,标题 12px → 14px,网格
0.55 → 0.70,底色0.72 → 0.82。
- 淡出只淡外壳:原先避让时把整卡
- 落位抽成纯函数
Main.joseki_anchor(vp, board_cells, panels, prefer, card, left_open, right_open): 五个候选位里挑可行且离当前位置最近的那个(挑「最近」而不是「第一个」,画面小幅移动时卡片不会横跳)。 - 门卫:前端十支全 PASS,
joseki_card51 → 62 项(新增:淡出不动整卡透明度 /dim往返 / 落位 不压棋盘格 / 完整在窗内 / 不压右栏展开 / 就近挑位 ×3 / 哪都放不下退回兜底)。
- 兜底台词刷屏的根因修复(
backend\app\llm.py):推理型模型(如deepseek-v4.1-flash)把推理 token 也计入max_tokens,而旧实现按「思考档位」只注入 80~600 的输出上限(档位 0 一个推理预算都不加)⇒ 服务端在推理段就把上限烧光,返回finish_reason=length+ 空content⇒ 被当成调用失败 ⇒ 台词退化成 「唔…我脑子有点转不过来了」「嗯…思路有点断了…」。实测(同一份真实提示词、只改一个变量):不注入上限 ⇒ 正常台词(completion_tokens=2119、finish_reason=stop);注入300⇒ 空内容、length、2.1 秒(全部 花在推理段);真实链路 9/9 全空,且每次 1.2~2.6 秒返回——不是超时、不是限流、不是凭据。- 常态:
_budget()一律先加REASON_HEADROOM_DEFAULT = 4096(推理余量);思考档位 1/2 的+160 / +800叠在余量之上(阶梯必须单调——照旧只加档位的话,档位 1 的上限会比档位 0 更低);no_think与「配置里显式写死max_tokens⇒ 注入值让位」两条既有契约不变。 - 长尾:命中「空
content+finish_reason=length」时去掉全部扩展字段重试一次 (bind(extra_body={}),真·裸请求;实测模型级extra_body也会被去掉);重试仍空 ⇒ 落兜底台词, 绝不循环;有内容只是被截断的回复照用、不重试。 - 覆盖面:工具轮(
_bind_tools)与流式通道(_bind_stream)原来各自把上限写死(工具轮只有 80), 现在一律走LLMClient.budget_for()——开了tools_enabled/stream_tokens也吃到同一份推理余量。 另外「400 去掉扩展字段」之后的那次重试本身就是裸请求,再命中「空 + length」不再重发同形请求。 - 日志:两条不带凭据的
[LLM]行(重试 / 兜底),事后能分清「预算问题」还是「真的失败」。
- 常态:
- 定式导学加「总是 100%」档(
game\scripts\ui\Main.gd):下拉第五档joseki_rate = 1.0——有资格就 一定讲,复测定式不必再手改config.json、也不会被掷骰吞掉。后端本就夹在0~1,无后端改动; 并列仍优先中间档(0.875夹在「高」与「总是」之间 ⇒ 仍显示「高」)。 - 门卫:后端全量 735 OK(
test_llm新增 6 条:推理余量阶梯 / 空内容 +length去上限重试 / 重试仍空落兜底 / 400 之后不再重复裸请求 / 重试失败可读 / 截断但非空不重试;test_tools新增 2 条: 工具轮与流式通道走同一个budget_for入口)、katago_service36 OK、前端十支全 PASS (joseki_level 25,其余与 v3.9.0 同)、--headless --quit无SCRIPT ERROR。
- 玩家棋风画像(新增
backend\app\profile.py四维特征 + 指数衰减融合、backend\app\profile_store.py存储层,独立库backend\data\profile.db——运行产物、已 gitignore):每次点「🏁 结束」 (POST /api/summarize成功出总结后)把这一局落一条结构化记录(时刻 / 手数 / 结果 / 胜率·目差 / 双方失误 / 进入的定式族 / 聊天主题 / 每手用时),games表一局一行、只增不减。画像由记录经 指数衰减 EMA 融合(v ← v*(1-α) + x*α):首样本=初始化(α=1)、其后单局 α 上限 0.25、 α 随样本增多收敛到半衰期稳态(half_life=20局),置信度随局数上升。四组维度:开局偏好 (星位 / 小目 / 三三 / 目外占比 + 常用挂角守角)、行棋风格(激进 ↔ 稳健:战斗 / 打入 / 断 / 脱先 / 围空)、失误与薄弱方位(常亏手型 + 四个角·边的薄弱方位)、性格与话题(聊天主题偏好、称呼与语气偏好)。 无信号的维度不产出(不把「不知道」写成 0);缺键一律原样带过(不衰减、不涨样本数)。 - 时间感知(新增
backend\app\time_ctx.py,纯函数、时钟可注入):四项事实——当前时间 / 星期、 本局时长与单手用时、距上次对局的间隔、连续下棋天数(按本地日历日切、含今天)。 单手用时由后端在POST /api/move合法性通过后打点、下一手到达时结算(不要求前端配合); 缺什么就不说哪个(不补 0、不瞎猜),脏值不抛。 - 提示词两块(时间块 + 画像块):
prompts.time_note()/prompts.profile_note()都是纯函数, 插在长期记忆段 → 事件区块 → 时间块 → 画像块 → 铁律段的顺序里(铁律段一行不动)。 两块为空 / 传空 dict ⇒ 返回'',context_block逐字符不变(tests\golden\prompts.json锁定)。 两块各有字符预算 240:时间块超限从最旧的事实开始丢(当前时间最后走)、画像块超限丢 最低置信 / 最旧维度。块内明确「只在相关时提、别把数字当口头禅复述」——守住「她变话多」反目标。 - 三个延伸:① 主动约战提醒(玩家连上 / 首次交互时一次、模板句、零新增 LLM 调用,
走
ai_notice声明「这条没有语音」);② 🪞 她眼中的你(⚙ 设置抽屉里的只读一格,GET /api/profile返回各维值 + 置信度 + 局数 + 最近 N 局记录);③ 局末进步报告 (并进现有summarize()的提示词、不新增调用,MAX_LLM_CALLS_PER_TURN不动)。 - 写入纪律:对局记录与画像融合只在手动结束(
summarize())里写,落盘一律asyncio.to_thread; 回合内零新增 LLM / 引擎调用;库被删 / 只读 / 目录不存在 ⇒ 当「无画像」、只打一行日志、busy照常归位,绝不影响对局。两个开关(profile_enabled/time_aware_enabled,默认都关) 在⚙ 设置抽屉里、连上 WS 时推给后端(与joseki_teach/emotion_drift/chat_digest_llm同形); 都关 ⇒ 与改动前逐字符一致。 - 两条口径:① 主动约战不受
time_aware_enabled门控(独立延伸),但与profile_enabled隐式耦合——关掉画像 ⇒ 不再写对局记录 ⇒ 提醒也随之消失(没有历史可判);② 面板的 「来源局」=「最近 N 局记录」的全局列表,不是每维各自的来源(profile表没有局 id 列)。 - 界面:
⚙ 设置抽屉新增玩家棋风画像(复选框,默认关)、时间感知(复选框,默认关)与 🪞 她眼中的你(只读按钮 + 文本块);两者与既有开关同形(写回config.json+ 连上推/api/config)。 - 顺手加固:后端启动路径把标准输出的编码错误策略改成
replace(带守卫、只改errors不改编码)—— 日志打不出某个字符(emoji / 生僻字)时降级成?,不再抛UnicodeEncodeError把整回合(连同写记录 / 融合画像)打挂。 - 门卫:后端全量 721 OK(新增
tests\test_profile_store.py/tests\test_profile.py/tests\test_time_ctx.py)、katago_service36 OK;前端十支全 PASS(config 151/joseki_card 51/tts 20/speech_pairing 77/review_mode 31/view_helpers 22/score_phrase 17/joseki_level 13/net_chat 14/chat_queue 17);--headless --quit无SCRIPT ERROR。 联动冒烟:起 KataGo 服务 → 起后端 →GET /api/health→ 两手POST /api/move→GET /api/profile→POST /api/reset→ 前端 headless 连 WS。
- 预测线 A/B/C(零新增引擎调用):新增纯函数模块
backend\app\pv_lines.py(零 import,只读既有那一次 分析的moveInfos——每条候选自带pv/winrate/scoreLead,复杂度 O(候选数))。线 A =moveInfos[0]的首手 + 其pv前 4 手(引擎惯例pv[0] == move,先剥掉这个重复首手);线 B / C = 在 前 10 个候选里依次取「与已选的每一条线不同手数 ≥pv_diverge_min(默认 2)」且「与 A 的胜率差 ≤pv_max_drop(默认 5%)」的候选——凑不齐就降级(只有 A/B,或只剩 A),绝不抛;pv不足五步时 用可用长度。三条线全部来自同一次分析,不新开任何引擎分析。 - 那一次分析更深(唯一被允许的代价):新增配置
pv_max_visits(默认 150、只增不减)——只给 「玩家落子后局面」那一次分析传max(pv_max_visits, max_visits)(随包max_visits=100⇒ 那一次取 150),post复算 / 提示 / 局面分析 / 总结仍用默认深度。那一次已有分析会变慢、但不增加调用次数;katago_timeout_ms的代码默认提到45000(随包模板仍30000)。部署侧建议显式设analysisPVLen(防默认值漂移的护栏,不是前置条件;pv长度真实由 visits 决定)——见第六节「KataGo 服务部署」。 - 情绪波动(默认关):新增配置
emotion_drift/drift_base(默认 0.03)/drift_max(默认 0.08, 配置层另夹到 0.2)与纯函数emotion_bias。开启后按 A 常态 → B 小概率 → C 更小概率 的阶梯选线 (p_drift = clamp(drift_base + 情绪偏移, 0, drift_max),P(A) > P(B) > P(C)、P(A) ≥ 0.8); B/C 缺失 / 胜率差超pv_max_drop/ 首手不是当前棋盘的合法点一律回落 A(c → b → a逐级回落), 关闭时行为与现状逐项一致(不解析线、不掷骰、不记事件)。走 B/C 时写一条ai_bad事件——在 落点定案并提交之后才记账,工具轮重定案 / 回落 A / 本回合被看门狗作废都不留事件。 - 局内事件表 + 事件区块:新增
backend\app\event_log.py(EventLog:带时刻move_no+ 墙钟ts、 只增不减、去重键 =kind+ 归一化text、不进GameState快照、不随悔棋回退,reset()清空) 与backend\app\decay.py(纯函数:DECAY_SPAN = [1, 3, 6]四档、DECAY_DROP_AT = 12、方位词按坐标核对)。 注入侧新增prompts.event_block():插在长期记忆段之后、铁律段之前,受「衰减窗口 + 字符预算EVENT_BLOCK_MAX_CHARS = 240」双重限制,超限丢【最旧 → 最低档】;事件为空 / 全部淡出 ⇒ 区块为空串、context_block逐字符不变(tests\golden\prompts.json锁定)。事件原文是给模型看的素材:永不进台词、 不产生speech。 - 聊天按主题入表(默认零额外 LLM):
chat()收尾后按规则式(纯函数分段 + 模板摘要)把聊天写成chat事件;新增配置chat_digest_llm(默认 false)——开启后只在局末summarize()/ 空闲idle_remark()用 LLM 补做分段总结(自带单次超时,落子 / 聊天回合内零额外调用);LLM 失败文案 (「调用出错:…」)整段与单行两道闸门都挡住、绝不落表。 - 界面:
⚙ 设置抽屉新增两格——情绪波动(下拉:关闭 / 低 3% / 中 5% / 高 6%,默认关;开关与基础概率 同占一格、原子推送一次/api/config)与聊天摘要用 LLM(复选框,默认关)。都写回config.json、 即时生效。 - 门卫:后端全量 578 OK(新增
tests\test_pv_lines.py/tests\test_event_log.py/tests\test_decay.py)、katago_service36 OK;前端十支全 PASS(config 103/joseki_card 51/tts 20/speech_pairing 77/review_mode 31/view_helpers 22/score_phrase 17/joseki_level 13/net_chat 14/chat_queue 17);--headless --quit无SCRIPT ERROR。联动冒烟:起 KataGo 服务 → 起后端 →GET /api/health(ok:true/katago_dead:false)→ 两手POST /api/move→POST /api/reset→ 前端 headless 连 WS。
- 定式导学从「结构点必说」改成「开关 + 频率」(口径修订,取代 v3.6.0 的次数规则):新增纯函数模块
backend\app\joseki_gate.py(两层决策;rng 由调用方注入、无副作用)。资格层——joseki_teach=false一律不教;族计数< 3有资格、≥ 3只在走错时有资格(v3.6.0 的「前 3 次 / 第 4 次起」并进这一层)。 频率层——结构点(进入 / 分岔 / 收尾)掷一次p = clamp(rate + 棋力画像 + 情绪, 0, 1):rate是新增配置joseki_rate(默认0.5),画像与情绪各浮动 ±10%,且只吃局内信号(本局玩家每手的 rank + 该族计数 / 现有情绪区域,不读任何持久化画像)。未命中与「没识别到定式」走同一条出口—— 不发joseki、不换讲解模板、不上卡片,只点评这一手(逐字段同构)。走错(真亏且偏离库线)绕过 频率层与冷却、必说;同族 8 手冷却(COOLDOWN_MOVES = 8)。族计数改为按「教过几次」计 (只在真的讲了时 bump;走错必讲照旧计数),同族同一局最多计 1 次 / 悔棋退回 / 存储降级三条既有语义不变。 - 右侧栏「定式导学」一格下拉(关闭 / 低 25% / 中 50% / 高 75% / 总是 100%,默认中 50%):
joseki_teach+joseki_rate一起写回config.json、经/api/config对当前对局即时生效;初始化只读、不改写配置。 - 局后只读复盘(📖 复盘):后端新增
POST /api/review(进 / 出)、POST /api/review/goto(只读重放第 n 手)、GET /api/review/notes(逐手存档 + 真亏手账)。复盘期间落子 / 聊天 / 闲置 / 提示 / 分析一律可读拒绝 ({"ok":false,"error":"复盘模式中"},与busy的{"ok":false,"busy":true,…}刻意不同), 两种都不碰busy;情绪心跳与语气词一并冻结。goto只重放临时盘面、不改st、也不碰st.finished, 所以复盘可进可出、退出回到进入前的局面——「🏁 结束之后还能继续下」的既有语义不变。前端只在对局结束后 显示入口,←/→或按钮翻页;冻结范围在纯函数Main.review_lock_state一处定死、退出逐项恢复。 busy看门狗 + 代际守卫(backend\app\agent.py/agent_graph.py):最外层_turn_guard按WATCHDOG_SEC = 1380(≈真实上限 1130s × 1.22,算式写在注释里;katago_timeout_ms代码默认 提到 45s 之后真实上限是840 + 3×45×2 + 20 = 1130)兜底;超时只解锁 + 前推一代 + 一条可读提示, 不取消在途任务——旧那一代的一切写入(棋谱 / 快照 / 逐手存档 / 事件 /message+speech/ 定式计数) 由代数核对全部作废。既有_set_busy(False)的位置一处未挪。- 本局逐手存档:
GameState.move_notes(她的用意 / 点评 / 感想,纯追加、不滚动、不截断)与losses(真亏手,drop > 0.10才入账)两份新账,供复盘回看;与既有滚动 18 条的memory_log互不影响。 - 界面:视角快捷键
Home(回初始取景)/C(四角轮转,只转方位角,你自己的缩放与俯仰不动); 悬停显示坐标 + 手数(已有棋子「D4 · 第 3 手」);目差下面一行 11px 量级人话 (差不多 / 半个角 / 一个角的实空 / 已经很难追了,两个信号打架时取更保守的档,原始数字保留); 「同步说话 / 整段合成 / 长句合并」收进右栏 ⚙ 设置抽屉(前两个立即生效、长句合并重启后端后生效)。 - 修复(既有缺陷,随本版一起修):右栏「手数: N」原为白字白底(真机看不见)→ 补深色覆盖;
键盘
Ctrl/Shift/Alt + C不再转视角(这两个快捷键只认裸键);悬停手数从「每次鼠标移动O(n)扫描 整条move_seq」改为在set_move_seq预建查表、查询O(1)(行为逐字不变);test_config.gd的 大数用例从"1e999"(会触发引擎WARNING: Exponent too high)换成"1e308",门禁输出干净。 - 门卫:后端全量 509 OK、
katago_service36 OK;前端十支门卫全 PASS(config 49/joseki_card 51/tts 20/speech_pairing 77/review_mode 31/view_helpers 22/score_phrase 17/joseki_level 13/net_chat 14/chat_queue 17);--headless --quit无SCRIPT ERROR。 联动冒烟:起 KataGo 服务 → 起后端 →GET /api/health(ok:true/katago_dead:false)→ 两手POST /api/move→POST /api/reset→ 前端 headless 连 WS。
- 定式教学从「走完才讲」变成主动跟课(
backend\app\joseki.py):识别时机提前到进入定式那一手 (角内恰好 2 手吻合库中某条的起手——含「先认出这个字形」的 2 手条目;这一刻即使库里第 3 手有 两种走法也仍判进入,两种走法照旧带进事件与卡片),此后按结构点开口——进入 / 分岔 / 收尾必说, 正跟着书走的中间推进不说(分岔=第 3 手起出现两种走法,或你真走错时;收尾=库里那条线 走完,只对 ≥ 3 手的条目成立;脱先——下一手落在角外——识别器不出声、不发事件)。 识别是纯本地的(每手几十次比较,零引擎调用、也不额外增加 LLM 调用),进入 / 分岔 / 收尾的讲解 顶替那一手本来那条点评,不新增气泡;进入那一手可能是她自己下的,所以那句话的主语是中性的。 - 棋盘上的浮层参考图卡片(
game\scripts\ui\JosekiCard.gd,Main.gd只接线):一张 19 路小棋盘画出 全部手顺、第 k 手高亮环、分岔候选点(菱形带序号)与走错的那一手;浮在棋盘相邻的空白处、可拖动、 与落点热区 / 鼠标附近 / 已展开侧栏重叠时半透明淡出;走完(formed)才立即收起——偏离库线 (deviated)不是收尾,该上卡片时就把走错那一手标红继续显示;脱先则由兜底 TTL 收起 (CARD_TTL_SEC=120 秒)。它不进右栏、不新增右栏控件、不产生语音或台词(说不说只由结构点决定), 也不给大棋盘加手顺编号连线;「📖 复盘」(局后回顾)不在本版。 - 定式库覆盖(扩库后的实际清单,11 族 / 25 条):星位·点三三(
star_33,5 条:基本型 / 定形 ×2 / 挡另一边 ×2)、星位·小飞挂(star_kakari_small,4 条:靠压 ×2 / 小飞应 ×2)、星位·双飞燕 (star_double_kakari,2 条)、星位·小飞守角(star_shimari,2 条:守角 / 守角后被挂角)、 星位·大飞挂(star_kakari_big,2 条)、星位·一间挂(star_kakari_one,2 条)、小目·小飞挂·尖应与飞应 (komoku_small_kakari,2 条)、小目·小飞挂·尖顶(komoku_tsuke,2 条)、小目·一间跳守角 (komoku_shimari,1 条)、小目·一间低挂(komoku_low_kakari,1 条)、三三·被挂角 (sansan_kakari,2 条:顶这边 / 顶那边)。未做(如实记下,不写成已覆盖):① 星位·飞压 / 夹击—— 挂角局面下引擎实测只给出靠压 / 小飞应 / 一间跳 / 小飞点,提取不到飞压与夹击的手顺,故不编条目; ② 目外(5-3)/ 高目(5-4)——设计阶段即标为「可砍」,本版没有。 - 次数规则与持久化:同一族前 3 次一律上卡片,第 4 次起只在真走错时才上;次数按族(一课的粒度,
不是定式名、也不是单条线)跨局累计,落
backend\app\joseki_progress.py的 SQLite (backend\data\joseki_progress.db,运行产物、已 gitignore、没有开关)。同族同一局最多计 1 次; 悔棋回档把这一局已计的那一次退回去(-1),重走那一手可以重新讲解。※ 本条已在 v3.7.0 修订为提示门控:次数并进「资格层」(计数按教过几次计), 说不说改由频率层决定、走错必说——见下面 v3.7.0 段。
- 「走错」=真亏且偏离库线(
agent_graph.is_joseki_mistake):不在她算的前 5,或胜率掉 >10%, 且这手不在库线上;库线本身那一手永不判走错(治「照书下还被骂」)。任何计数故障只打一行日志、 绝不影响对局(busy照常归位)。 - 接口:
josekiWS 事件在旧三键(kind/corner/points)之外带出family / name / k / line / candidates / show_card / mistake(旧字段取值路径不变,缺字段 / 脏值一律安全缺省);game\config.json不新增任何字段。 - 门卫:后端
tests\test_joseki.py/tests\test_joseki_progress.py(编排用例在test_agent.py/test_agent_graph.py),前端新增纯逻辑game\tests\test_joseki_card.gd;后端全量 455 OK、katago_service36 OK、四支前端门卫(44 / 24 / 20 / 77)全 PASS、--headless --quit无SCRIPT ERROR。
- 修复:她忙的时候发的聊天被后端静默挡下(
POST /api/chat回{"ok": false, "busy": true}), 玩家的话进了聊天栏却永远等不到回复。 现在改成先本地排队:输入框下面那行(右下角)显示 「排队中 N 条 · 队头前 8 字」,她上一句说完、语音也播完的那一刻自动一条接一条发出去—— 不用玩家再发一条来「催」;最多排5条,满了时不吞话(原文留在输入框 + 状态栏提示)。 - 实现:新增纯逻辑
game\scripts\net\ChatQueue.gd(FIFO / 上限 / 提示文案 / 「能不能发」判据, 门卫tests\test_chat_queue.gd17 项);NetClient.chat()改成吃回包——忙 ⇒ 入队,发出后 只有明确ok: true才算收下(忙 / 非 2xx / 读不懂的回包一律退回队头,另压2秒重试闸), 新增chat_queue_changed信号与flush_queued_chat(env)(三道门禁:空闲判据 / WS 未连接不发 / 失败节流),门卫tests\test_net_chat.gd12 项;Main.gd只留胶水(提示标签 + 每帧续发 + 不吞话)。 - 已知取舍:请求其实到了后端、只是回包丢了时,这条会重复发一次(她可能回两遍)—— 宁可极少数情况重复一次,也不吞玩家的话。
- 冒烟:五支前端门卫全 PASS(
17 / 12 / 87 / 24 / 50)、--quit无 SCRIPT ERROR、后端 354 OK、katago_service36 OK;另跑了「她忙时连发两条」的无头端到端(假后端 + 真Main.tscn): 排队 → 右下角提示 → 她说完自动依次发出、提示消失、顺序正确、全程不用手动再发。 - 文档:
README使用说明「输入框」一栏补排队与提示的说明;新增本计划docs\plans\06-chat-queue-when-busy.md。
- 修复:在她还在念上一句时发聊天,回复会被扣住——聊天栏一直挂着「Azure 思考中……」,要等再发一条
消息才把上一条回复放出来。 根因:逐句模式下的「同步说话」原来一律让文字等这条台词自己的语音开播
(
arm(rec, cfg.tts_sync_speak)),而唯一的提前放行发生在Main._deliver_msg末尾的release_stale()——它只要看到「还有音频在途」(她上一句还在播 / 音频还在取)就不放。于是玩家发的 聊天回复一直压在配对表里:占位气泡不消失、回复不出现;玩家再发一条消息时_deliver_msg对玩家消息 的「先放积压」把上一条回复放了出来,看起来就是「上一条要等下一次发送」。 - 修法(判断收进状态机,可测):新增
SpeechPairing.should_wait_voice(sync, env, reveal),三种情况 才等语音——她这一轮还没结束(对局中的同步说话与落子同步照旧)、这条要揭示棋子(ai_move, 音画同步硬规则:子仍在这手宣告的语音开始播放时揭示)、前面还有台词在等它的语音(不许插队, 聊天栏顺序不乱);其余(回合已结束、不用揭示、前面没欠账:聊天 / 闲置搭话 / 非法手提示)当场显示。 整段合成(tts_batch_speak)那条路本来就只在「已经在响了」时才等,不受此问题影响。 - 门卫:
game\tests\test_speech_pairing.gd新增 ⑪ / ⑪c / ⑪d 三组断言(回合结束即显示、不许插队、reveal与回合中的台词照旧等语音);README「同步说话」一栏改写为上述规则。 - 端到端复核:无头跑真
Main.tscn+ 假后端(本地 12 秒长音频)——修复前:玩家在她朗读时发聊天,pending=true、占位不变、10 秒后回复仍未出现,发下一条消息才显示;修复后:回复当场显示、占位随即被 回复替换。落子场景逐帧对照:ai_move文字与POST /api/reveal的时机与修复前逐字一致。
- 语音链路整体搬到后端:新增
backend\app\speech_text.py(清洗 / 分句 / 分块 /[[tone:…]]剥离, 从TTSClient.gd与Main.gd:_strip_tone逐条移植)与backend\app\tts.py(云端合成客户端:条数 32 / 8 MiB 双向 LRU、单条超预算直接不插、多块按格式拼接、任一步失败整条降级);后端每发一条要念的 台词都紧接一条speech事件(turn_id+seq配对),音频经GET /api/tts/audio/{id}取回; 新增端点POST /api/tts/speak(试听)。见第五节「接语音」。 - 前端退化为播放器:
TTSClient.gd不再清洗 / 分句 / 合成,只有play_audio(bytes, seq)与speak_system(text, seq)两个入口;新增SpeechPairing.gd(可测的配对状态机)守住不变量 「任何已登记、且不属于「有意不显示」两类的台词最终一定会被显示一次」——音频取不回 / HTTP 200 但 body 为空 / 字节解析不了 /speech没到 / 连接断开,都会先显示文字再收账,绝不静默丢字、 绝不锁死棋盘。 - 降级与超时:后端合成失败(连不上 / 超时 / 非 200 / 解析失败)一律改发
mode:"system"指令, 文字照常、回合不卡(fail-closed);新增配置tts_timeout_sec(单次合成超时,默认30秒,≤0钳到 1 秒、脏值退回默认)。清洗规则不再在前端出现(语气标记已由后端剥净,不会被系统语音念出来)。 tts_batch_speak的语义变化:合成改成「一行一条」之后,它从「一次请求合成一整段」变成 整段排队连续播报(仍影响观感 / 时延,但不再减少合成请求数);tts_long_chunk现在是后端的 分块上限开关(单块100→160字),改完要重启后端。- 已知取舍:4 条同步方法的短提示只有文字没有语音;朗读方式 / 服务地址 / 模型 / 长句合并只对
后端重启生效;
speech与message按turn_id+seq配对(seq同回合递增、跨回合重置); 两条前端固有的错配窗口(非法手提示与第二次落子交错、tomb 占位吃掉下一条语音)各自有兜底。 详见第五节「接语音」。 - 文档与配置:WS 事件清单由 9 类增至 10 类(新增
speech,连接建立时的hello另算);game\config.json模板新增tts_timeout_sec: 30(不删任何tts_*键——tts_limit_len/tts_batch_speak/tts_long_chunk都还有活的读取方);README 字段表与AGENTS.md同步。 - 文档:扩写「二、快速开始」为详细使用说明——首次运行清单、启动顺序与就绪判定、界面与操作逐项、 一手棋的完整流程(含音画同步)、语音与情绪、常见问题排查表、开发者测试与调试命令(章节编号未动)。
- 回合编排显式成一张状态图:新增
backend\app\agent_graph.py(LangGraph),拓扑prepare → decide →(条件边 tools → decide,可反复)→ speak → finalize;六个需要 LLM 编排的公开方法 (player_move/hint/analyze_position/chat/idle_remark/summarize)在agent.py里只做 「构造状态 → 跑图」,引擎调用 / 提示词 / 台词 / 事件 / 记忆标签的顺序全在四个节点里。player_move一次公开调用=三次ainvoke(after_player → own_turn → just_moved,中间数据经TurnState["data"]透传、turn_id三阶段共用);其余undo/reset_game/set_allow_pass/reveal_move/tick_mood/player_interaction仍是同步方法、直接发事件。公开契约不变: 端点、事件形状与AzureAgent公开方法签名一行未动,客户端NetClient.gd仅新增token_event一条。 - 条件边工具轮与落点候选:工具请求改由
decide后的条件边驱动(tools → decide,回到模型再来一轮, 直到没有请求或预算用尽);decide内总 LLM 调用次数上限MAX_LLM_CALLS_PER_TURN = 4(含工具轮), 工具预算仍是MAX_TOOL_CALLS = 2/ 单工具 10 秒超时。工具/模型现在可以参与落点候选 (模型原话里的 GTP 坐标或虚着经_candidate_from_text+_resolve_ai_move认下),但有三条边界: ① 候选点必须过board.is_legal(...);②pass只在允许虚着时可用;③ 任何情况下都产出可用落点 (候选不合法就回退后端算棋的默认落点)。tools_enabled默认false,关闭时行为与没有工具完全一致。 - AI 原话去重下沉到后端:
AI_DEDUP_WINDOW = 8(backend\app\agent.py)与判据移进finalize阶段 (agent_graph._emit_deduped/_is_repeat/_is_repeat_prefix)。只有台词类message过闸——ai_move落子宣告永不判重,state/info/busy/mood/joseki/tool不受影响; 「这手不成立 / 空盘提示 / 回档 / 重开 / 虚着开关 / 引擎失败恒定句」等错误与事件型提示有意不过闸 (连按两次必须照旧还给玩家);被抑制的台词照常写memory_log/chat_history。前端Main.gd的同名 常量只作 WS 重连乱序的兜底(计划明示的双保险)。 - 可选流式输出(默认关闭):新增配置
stream_tokens(默认false);开启后speak阶段逐块推{"type":"token","turn_id","delta"},气泡最终内容仍以经clean_reply()+ 去重闸门的完整message为准。 增量只取模型回答的 content 通道(推理段永不进delta);成对 think 区间保证增量与台词都干净, 「只剩收尾标记」与未闭合think的畸形形态只保证最终message/ 语音干净;流式失败自动回落非流式整段出话。 关闭时一个token都不发,事件序列与没有这个功能时逐字一致。见第四节「流式输出」。 - 文档与配置:
game\config.json模板新增stream_tokens: false;WS 事件清单由 8 类增至 9 类 (新增token);README 字段表与AGENTS.md同步(编排在状态图里、工具可参与落点候选、去重与busy的边界)。
- 长期记忆(默认关闭):新增
backend\app\memory.py(SQLite 存储 + 检索:有向量按余弦相似度、 无向量按关键词)与backend\app\memory_service.py(云端 embedding 装配,embed_*任一为空就不装配 embedding 客户端、自动降级为关键词检索)。对局总结成功后写入长期记忆,闲聊与她自己落子前 两处召回注入(插在提示词「【最近的对话】」之后、铁律段之前,措辞强调「不是当前盘面、仅供参考、别照抄」)。 默认关闭时零开销:不建库、不调 embedding,且提示词与没有这个功能时逐字符一致(黄金样本锁定)。 - 写入与注入都有硬上限:单条 200 字、同 tag + 同 text 去重、库内最多 2000 条(超出按时间淘汰最旧);
每次最多注入
memory_top_k(默认3)条、单条 120 字、总计memory_max_inject_chars(默认240)字。 任何记忆故障(库不可写、embedding 401 / 超时)都只打日志、自动降级,绝不影响对局。 - 管理面与配置:新增
POST /api/memory/query|write|forget与GET /api/memory/stats(查看 / 手动写入 / 忘记),GET /api/health增加memory_rows;管理端点的写 / 查另推memoryWS 事件(纯元数据,前端只记日志)。 新增配置memory_enabled/memory_db_path/embed_base_url/embed_model/embed_api_key/embed_timeout(默认 10 秒) /memory_top_k/memory_max_inject_chars,库文件默认在backend\data\(运行产物,已 gitignore)。见第四节「长期记忆(跨局记得你)」。 - 召回与阻塞:向量命中不足
memory_top_k条时用关键词从其余记忆里补齐(没向量的老行 / 写入时 embedding 失败的行不会被压掉);召回 / 写入(她的两处 + 管理端点)都走asyncio.to_thread, 云端 embedding 卡住也不会冻住后端事件循环。
- 后端 LLM 调用改走 LangChain:新增
backend\app\llm_chain.py(ChatOpenAI的唯一装配点)与backend\app\prompt_chains.py(七阶段提示的ChatPromptTemplate链);llm.py退化为薄适配器 (对外chat()契约不变,台词仍必经clean_reply()与去 think 段的content_of())。依赖锁定langchain-core>=1.0,<2.0/langchain-openai>=1.0,<2.0(要求 1.0 及以上、限定在 1.x——测过的 就是装到的;backend\requirements.txt);旧的本地地址预检与probe_models探测随之删除。 - 可配置的请求体扩展字段:新增
llm_extra_body(始终并入)与llm_think_extra_body(仅思考档位开启时并入), 透传服务商专有参数(top_k/reasoning_effort等);输出上限按线上键max_tokens发送,服务商拒绝 (HTTP 400/422)时自动去掉扩展字段重试一次。推理段永不显示给玩家:只取AIMessage.content,reasoning_content/response_metadata一律不进台词。 - 情绪增量改为结构化输出:情绪微调改用
with_structured_output(function_calling方式——DeepSeek 等 服务商不认response_format.json_schema,工具调用则是公共能力),拿不到时静默回落到原有的正则解析, 行为与旧版一致。 - 发言侧工具(默认关闭):新增
backend\app\tools.py(katago_analyze/joseki_lookup/board_query) 与受限 tool loop——只在「提示」与「落子宣告」两个回合启用,工具次数有硬上限(默认2次、单工具 10 秒超时)、 工具输出永不直接当台词;只影响她怎么说,不影响她下哪里(落点仍由后端算棋决定)。新增配置tools_enabled/max_tool_calls;客户端新增tool事件,只用于状态栏「她正在查资料…」提示。 见第四节「工具(发言侧)」。
- KataGo 独立成云服务:新增
katago_service\(FastAPI + 常驻katago analysis子进程):POST /analyze(请求/响应即引擎原文)、GET /health(warming/ready/dead)、可选 Bearer 鉴权、启动即后台预热(OpenCL 首次调优挪到部署阶段,端口不会被预热堵住),另有run_server.bat与--host/--port/--token/--katago-exe/--model/--config/--analysis-threads入口 +KATAGO_*环境变量兜底。部署见第六节「KataGo 服务部署」。 - 后端改为 HTTP 客户端:
backend\app\engine.py重写为连/health+/analyze的客户端(就绪判定只看state,不认ok;超时/连接失败重试 1 次后返回可读错误),不再持有引擎子进程——agent.py/prompts.py一行未改。子进程时代的两条坑(stderr 必须持续排空、读行缓冲放大到 64MB)随引擎搬进katago_service\proc.py。 - 配置换血:后端与模板里的 KataGo 引擎路径三件套(可执行文件 / 权重 / analysis 配置)换成
katago_url(默认http://127.0.0.1:8100)/katago_token/katago_timeout_ms;game\config.json模板与 README 字段表同步,AzureConfig.save()改为把这三个旧键自我清理(此前为兼容后端共用同一份配置而刻意保留)。后端不再解析引擎路径(_resolve删除),GET /api/health增加katago_url。 - 服务不可用的表现不变:KataGo 服务没起 / 地址写错 / 预热中,后端都给出可读中文提示且
busy必归位,棋盘不会永久锁死;katago_dead在启动探活失败或分析时传输层失败(连接被拒 / 连接或读取错误)时置位、一次成功分析会清除它,katago_error每次失败刷新,两者成对读(见第六节)。
- 只保留远程后端:客户端删除
AzureAgent / AzurePrompts / KataGoEngine / LLMClient / GoBoard / GameState / Joseki* / EmotionModel与端到端测试脚本,装配路径收敛为NetClient(REST 命令 + WebSocket 事件);config.json删掉运行方式键(local/remote)、katago_*(前端侧)、llm_auto_start/exe/model_path/port、tts_auto_start/bf16/short_prompt、vlm_*等只服务本机部署的键,旧配置读入后会在下次写回时自我清理,不会崩溃、也不会把旧键写回文件。 - 补齐后端的 WebSocket 依赖:
backend\requirements.txt增列websockets。此前只有裸uvicorn,不含任何 WS 实现,/ws的升级请求会被拒成 404 —— 远程模式还是可选项时被掩盖,v3.0.0 让它成为唯一运行方式后就成了致命问题(客户端只会一直显示「未连接后端」)。 - 模型全云端:LLM 与语音 TTS 一律走 OpenAI 兼容 API(
llm_*/tts_base_url/tts_model/tts_voice/tts_api_key),tts_mode默认改为openai;system只作连不上时的临时兜底(不写回配置)。后端不再拉起本地 LLM 服务(services.py与看护逻辑删除),backend/app/config.py的内置默认值改为云端示例。 - 看屏幕搭话(peek)整体移除:前后端的
peer_remark/POST /api/peer/_peek_*/ VLM 配置与情绪条目全部删除;闲置主动搭话仍走POST /api/idle+ai_idle(随 peek 一起删掉的只有发言前那声「哼」,情绪区域爆发时仍会用同一段语气词)。 - 客户端不再拉起任何服务:语音服务改为手动启动(
server\start_tts_server*.bat),游戏只请求与播放;PCK 打包脚本删除,export_presets.cfg保留供个人导出。 - 健壮性:云端 LLM 失败(断网 / 401 / 429 / 超时)时所有流程都回兜底台词且
busy必归位(此前会留下棋盘永久锁死的隐患)。
- 远程模式健壮性微调:后端
/api/move、/api/undo对畸形 / 越界参数改为优雅拒绝(不再返回 HTTP 500);客户端 REST 请求加 30 秒超时,避免后端卡住时请求节点不释放(长时间运行不泄漏);WS 连接建立时的hello事件附带忙闲状态,界面锁状态在连接瞬间即正确。另修正事件契约文档、补充 WS 异常日志。当时默认的「客户端内编排」行为不变(v3.0.0 已删除该模式)。
- 三维情感模型:Azure 的情绪由「愉悦度 × 好奇度 × 唤醒度」三维状态驱动(各 0~1,分低/中/高三带 → 27 个情绪区域);区域带滞回防抖、随时间向基线平缓回落。情绪经「落子 / 聊天 / 互动 / 闲置」等事件即时结算,表情(形变持续层 + 一次性强调)、语气词与语音音高随之变化,另有一次异步的 LLM「情绪变化」小调用做细腻微调(失败静默、绝不阻塞交互)。
- 前后端分离(可选):新增 Python 后端
backend\(FastAPI,REST + WebSocket),可把对局编排与 KataGo / LLM 移到独立进程,客户端只做渲染、语音与看屏幕搭话;config.json用运行方式键(local/remote)与backend_url切换。当时默认仍是local,全在客户端内,行为与旧版一致;分发包不随后端,玩家无需改动。(v3.0.0 起只剩 remote,看屏幕搭话也已移除。)
- 聊天防复读加强:抑制「非相邻的原样复读」——此前只拦紧挨着的重复句,落子宣告(如「我在D16补一手」)夹在中间时,同一句点评仍会再冒一次;现在按最近 8 条窗口去重,隔一句话的一字不差复读也会被拦下(落子宣告本身不受影响)。
- 方位描述纠错:点评里的「左/右/上/下」必须先按坐标核对——字母是列(从左到右)、数字是行(从上到下);两子同列只能说「上下」、同行才算「左右」,核对不上就别说方位词(如「你这两颗黑子一左一右」说成实际同列一上一下的错误不再出现)。
- 代码整理:
game\scripts\按职责重新分类为core/ai/services/ui四个子目录(引用与场景同步更新);清理过期注释与只写不读的死变量,行为不变。
- 定式导学:对局中走出常见定式(点三三 / 小飞挂 / 双飞燕…,入门定式库取自 KataGo 自对弈实测)时,Azure 讲解要点、棋盘按手顺点亮高亮轨迹;你偏离已吻合的定式时,她会轻轻提一句常规形状(点到即止)。识别纯本地计算、零引擎与额外 LLM 调用(成型讲解直接替换该手点评);悔棋回滚后可重新讲解;右侧栏「定式导学」可关,关掉行为与旧版一致。
- 发言更像真人:提示词删掉可被照抄的示例句、补「说话方式」小节;LLM 侧开启重复惩罚;完全相同的重复句直接抑制。新增导演层:重要点评前有犹豫节拍、聊天面板逐字打字(确定性本地规则,零额外开销)。
- 语气词与情绪:预制语音(欢迎语 + 语气词)扩到 20 条——新增 加油 / 得意 / 傲娇 / 叹气 四类语气,并全部以新一轮高精度音色合成逐条择优替换,更接近 Azure 声线;新增
[[tone:…]]情绪标记与去重(同一情绪 30 秒内只出声一次)、语气词不叠响、思考声每回合限一次。 - 思考三档:快速 / 中等(推理限 token 预算并注入收尾提示)/ 强思考,设置面板与
config.json均可选;旧llm_think_strong自动迁移。 - 本地服务更稳:对局中看护本地 LLM 服务,意外退出(如 GPU 故障)自动重新拉起;llama-server 加载期 503 显示「正在加载」而不是报错。
- 语音加载期不再误用系统音色:本地语音模型加载期间只出文字、绝不降级为 Windows 音色;TTS 地址指向外部 API 时不拉起 / 不等待本地服务。
- 对局进程观:按手数识别布局 / 中盘 / 收官,让她的口吻与当前进程匹配(治刚开局就喊「反击」「缠斗」)。
- 修复强思考推理段泄漏:强思考把回复预算耗在推理上时,模型原始推理过程曾被当成正文显示给玩家(一整段全英文内心独白)。现在推理段永不显示——预算耗尽时自动改用快速模式重答一次,内容干净、语气自然(代价:强思考下每次回答会先多等 20~30 秒,日常陪玩建议用「快速」模式)。
- 走神搭话先看屏幕:玩家切到别的窗口(微信/浏览器…)时,走神搭话一律先看一眼屏幕、结合屏幕内容开口,不再只说默认的「还在吗」;游戏窗口在前台时直接普通搭话(不白起截图进程)。看屏幕冷却从 3 分钟缩短到 1 分钟。(v3.0.0 已移除。)
- 开局设置面板:启动时先弹出「出发前的准备」——选择快速 / 强思考,并可直接修改 LLM / 语音 TTS / 看屏幕 VLM 的 API 配置(默认沿用上次的值,只有改过的才写回
config.json;命令行--llm/--model/--key的临时覆盖不会写回)。(v3.0.0 起面板只剩 LLM 与语音 TTS。) - 强思考模式:思考型模型(Qwen3 等)可选「先推理再回答」(更慢、质量更好);本地服务自动放行推理段,云端 API 不支持时自动回退,不影响使用。
- 语音修正:取消「首块短切」分句合成——短消息整句一次合成、连续读完,不再开口碎句。
- 看屏幕修正:游戏窗口在前台(玩家就在游戏里)时不再白起 python 进程;超时不再残留孤儿进程、失败不再占用冷却;结果文件按次命名;VLM 服务未就绪时等待重试(不再白白回退 CPU)。
- 修复
start_qwen35b.bat误杀看屏幕 VLM 的 llama-server(现在只结束占用 8080 的旧实例)。
- 开场秒开口:第一句欢迎语改为预制语音(
assets\voice\welcome_<玩家名>.wav),启动即播、零等待;缺文件自动回退普通合成。 - 本地服务随游戏自动起停:启动时一并拉起本地 LLM / CosyVoice / 看屏幕 VLM 服务,退出时清理由游戏拉起的进程(缺环境自动跳过,不影响使用)。(v3.0.0 已移除:游戏不再拉起任何服务。)
- 新增
llm_auto_start/llm_exe/llm_model_path/llm_port与vlm_*配置项。 - 提速:LLM 前缀缓存复用;思考型模型默认关思考(云端不认自动回退);CosyVoice 边播边合成、长句合并(可开关)。
- 长闲置看屏幕搭话:空闲时截屏 + OCR + 本地 VLM 看图,Azure 主动找你说话(缺环境自动降级为普通搭话)。
- 点评跨回合去重;落子宣告不复读;走神搭话语音可整句作废。
本人技术有限,但想象力绝对够,后续版本更新取决于ai以及我的计算机&人工智能学习深度(:-])
自建部署采用 CosyVoice 3,Qwen3.6 35B(Qwen3.6-35B-A3B-UD-Q4_K_XL)。
- @CarrotMissileVehicle —— 本版本的前后端分离重构由其贡献:
通过 PR #1 让
game\只负责渲染与输入、backend\统一编排、katago_service\独立部署,并引入 LangChain 提示链、发言侧工具与长期记忆。特此感谢。 - 感谢 KataGo、CosyVoice、Godot、LangChain 等开源项目,本项目建立在它们之上。
- 感谢每一位提交 issue / PR、以及下载试玩并反馈的使用者。