Skip to content

About

Azure · 围棋陪练:本地 KataGo 算棋 + 可自定义 LLM/TTS API 与提示词

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

Azure · 围棋陪练

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、左栏可滚、开场「开始对局」固定在底部。 详见第五节「接语音」与「九、更新」。


目录


分支说明

本仓库以 发展主版本 为主线,另保留一条 历史分支 完整承载旧版:

分支 定位 内容概要 链接
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):

  1. 用 Godot 4.6 打开 game\ 工程,或:
    Godot_v4.6.3-stable_win64.exe --path game
    
  2. 人物模型 Azure.vrm 已随仓库放在仓库根目录,源码运行不需要另放;若使用自己的形象,把 vrm_path 指过去即可:绝对路径可直接用;相对路径会依次在 exe 同目录 → game/ → 仓库根目录 → 进程工作目录 找,取第一个存在的(分享包把 Azure.vrm 与 exe 放一起)。 KataGo 的 katago.exe / 权重 / analysis 配置文件归 KataGo 云服务部署侧持有,客户端不读这些路径。
  3. 先启动 KataGo 云服务(katago_service\run_server.bat,见第六节「KataGo 服务部署」), 再启动后端(backend\run_server.bat,见第六节),最后启动游戏。

    后端在启动时读取 config.json(LLM / 提示词 / KataGo 服务地址等,见第三节「读取方」列), 所以改这些字段、或在游戏设置面板里改完 LLM 地址/Key 后,都要重启后端才生效。

  4. 玩家执黑先行:左键落子,右键拖拽旋转视角,滚轮缩放。

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。

2.1 首次运行清单(一次性准备)

# 准备项 说明
① 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 留空则整条不出声、只出文字)。

2.2 启动顺序与就绪判定

按这个顺序起,前一步就绪了再起下一步:

  1. KataGo 云服务:双击 katago_service\run_server.bat(脚本自己 cd 到仓库根目录,必须原样跑; 等价命令与参数见第六节「KataGo 服务部署」)。默认监听 127.0.0.1:8100。
  2. 等它就绪:轮询 curl http://127.0.0.1:8100/health,直到 "state": "ready"。 服务启动即后台预热(加载模型 / OpenCL 首次调优,可能几十秒到几分钟),预热期 state 是 warming (此时 /analyze 立刻回 503 可读错误,不会挂到客户端超时)。
  3. 后端:双击 backend\run_server.bat(内部 cd 到 backend\ 再 python -m app.main), 监听 127.0.0.1:8000。控制台会打印「Azure 后端启动...(KataGo 走云服务,LLM 走云端)」。
  4. 游戏: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。

2.3 界面与操作

主界面(纸幕封面,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)/ 音调(播放层变调,会略带动语速)(0.81.3)/ 音量(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 的头… 摸头反馈

2.4 一手棋的完整流程(含音画同步)

  1. 你落子(左键):客户端先乐观画一颗黑子,同时 POST /api/move。

    落子不成立(超出棋盘 / 此处已有棋子 / 打劫禁着点 / 自杀禁着点)时,后端先推一份权威快照让界面撤销那颗子,再说一句 「这手不成立:<原因>」。这条提示不走 busy、也带语音(见第五节「已知取舍」的错配窗口说明)。

  2. 后端算两个局面:你落子前(缓存复用,用于给你这手排名)与落子后(用于选她的应手); 定式识别(纯本地计算、零引擎调用)与情绪结算也在这时完成。
  3. 她的点评(事件 ai_comment):按你这手的排名给评价;若这手正好是某个定式的结构点 (进入 / 分岔 / 收尾)且门控命中,点评直接换成定式讲解(不额外增加 LLM 调用),浮层参考图卡片 同时上(见 2.3;台词与卡片都走门控——未命中只点评这一手,与「没识别到定式」完全一样)。
  4. 她的用意 + 落子宣告是同一条台词(事件 ai_move,带落点坐标):这句话说清她准备下在哪、想达成什么。 她的落点由后端算棋决定(取引擎候选里第一个合法点;不允许虚着时绝不虚着)。

    开了情绪波动(默认关,见第九节 v3.8.0)时,她可能小概率改走预测线 B / C 的首手 (与最优线的胜率差有上限、首手必须合法,否则回落 A)——这是有意让落子带「人味」,不是算错。 开了发言侧工具(tools_enabled)时,模型在工具轮里报出的落点可当候选,但要过三条边界——候选点必须过棋盘合法性、 pass 只在允许虚着时可用、候选不合法就回退后端算棋的默认落点;任何情况下都会产出可用落点 (见第四节「工具(发言侧)」)。

  5. 先落到内部棋谱:她这一手在发宣告时就已落进后端棋谱,但棋盘上的可见更新延后。
  6. 音画同步:客户端等这条「落子宣告」的语音开始播放时才把棋子揭示到棋盘(POST /api/reveal)。 语音不可用 / 没播时,回合结束会兜底揭示一次,不会一直不显示。
  7. 落子后感想(事件 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「局后只读复盘」)。

2.5 语音与情绪

  • 合成在后端、前端只播放(见第五节):后端每发一条要念的台词都紧接一条 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 会被合成占住、两栏生效范围不同、两条前端固有的错配窗口) 见第五节「已知取舍」,这里不重复。

2.6 常见问题排查

症状 原因 处置
游戏一直显示「未连接后端(…),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 + 前端每帧清锁),不会永久锁死(第五节「已知取舍」)

2.7 开发者:跑测试与调试

一键起本地全套(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 秒,而后端同步等待合成(_emit_line → tts.speak),一回合可能 12 分钟——只建议最终验收时用。
  • 本机模型路径写在 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 实时切换(第四节;客户端界面暂未提供入口)。


三、config.json 字段

这个文件前后端共用:放在可执行文件同目录即可覆盖包内默认值(源码运行时读 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()),不影响对局;引擎本体与权重归服务部署侧持有, 不入本仓库(见第一节「模型与资产」)。

四、接大模型(LLM,OpenAI 兼容)

改 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)。

玩家棋风画像 + 时间感知(v3.9.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() 的提示词、不新增调用)。


五、接语音(TTS,可选)

合成在后端、前端只播放(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 即可;本地失败会回落系统语音, 不会偷偷产生云端调用。

  1. 自备 CosyVoice 权重,放到 server\pretrained_models\ 下;
  2. 建虚拟环境并安装依赖(server\requirements_win.txt、requirements.txt);
  3. 双击 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)——一眼看出记忆库是否在用。
  • 后端配置:后端进程用环境变量 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 不再由后端进程拉起,而是独立部署的云服务(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) 都归这台机器持有,不入本仓库(见第一节「模型与资产」)。
  • 引擎配置的两个部署要点(护栏,不是前置条件):
    1. 建议显式设 analysisPVLen——引擎配置里它默认是注释掉的(本项目 default_gtp.cfg / analysis_example.cfg 都是),显式写上只是一道防引擎默认值漂移的护栏;注意它只是 pv 长度的上限, 真实决定因素是「该候选被访问的次数」(visits)——所以它不是「靠它凑够五步」、也不是前置条件。
    2. 三线那一次分析要更深——本项目不改全局 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.2(当前)

  • 每次从主界面进入都是新的一局(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 项 + ResetSilentTest 2 项);前端 18 支脚本全 PASS (新增 test_avatar_path.gd 9 项 / test_new_game.gd 8 项 / test_estimate.gd 17 项; test_title_screen.gd 19 项、test_review_mode.gd 50 项、test_view_helpers.gd 45 项)。

v3.12.0

  • 棋谱库 + 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.py 8 项 + test_events 事件形状 2 项 + EstimateWiringTest 3 项 + 棋谱库并发播种 / NaN 清洗 2 项 + 本地 TTS 档 3 项); 前端 15 支脚本全 PASS:test_config.gd 192 项、test_title_screen.gd 22 项、 test_view_helpers.gd 45 项。

v3.11.4

  • 语音自然打断:玩家落子时不再 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.3

  • 主界面运镜(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 / 淡出 / 淡入)。

v3.11.2

  • 主界面(纸幕封面):新增 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.gd 19 项(出货文件 20 句 / 无空句无重复 / 脏值与裸数组 / 200 次轮换不连复 / 首句可由 rng 钉死 / 间隔就是 1 秒)。
  • 门卫:test_review_mode.gd 补 2 项(主界面开着 ⇒ 棋盘锁 / 按钮灰 / 聊天禁用 / 别的入口也冻; 关着 ⇒ 回到原状态),test_title_screen.gd 新增 14 项(纸幕挡点击 / 三键信号 / 开始⇄继续 / 显隐)。

v3.11.1

  • 她「说多少」可调:右侧栏原来的「短回复」复选框扩成三档下拉(说话:精简 / 适中 / 详细), 对应后端 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 的四种形态)。

v3.11.0

  • 语音合成改用阿里云 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.py 23 项(本机假 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 不算慢引擎)。

v3.10.1

  • 去掉侧栏的黑框: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。
  • 版式更方便操作:
    • 按钮 / 输入框 / 下拉框的最小高度 24 → 32px(content_margin 5→7);
    • 左栏整栏可滚(ScrollContainer):窗口一矮,下面的语速/音量/试听不再被裁掉;
    • 开场对话框拆成「滚动内容 + 固定在底部的动作区」:开始对局 永远是朱砂主按钮,内容再长也不用滚到底;
    • 滚动条改 6px 墨线(默认灰条在纸上太抢眼);右栏分节处补发丝墨线。
  • 门卫:前端十支全 PASS(含 joseki_card 79 项),--headless --quit 无 SCRIPT ERROR。

v3.10.0

  • 全局 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 文案改纯文字(提示 / 局面分析 / 重来 / 结束 / 复盘 / 设置 / 语音朗读 …), 只保留 ▶ ◀ ▸ ✕ 这类排版符号。
  • 顺带修两处(都是换皮时暴露的):
    • 侧栏展开时卡片会藏在侧栏底下:_follow_joseki_card() 原来只躲棋盘,现在也躲已展开的侧栏 (与避让共用 _panel_rects());折叠按钮改透明底——它原先把面板标题的第一个字盖掉了。
  • 门卫:前端十支全 PASS(joseki_card 79 项,含新增的"底色不透明"),--headless --quit 无 SCRIPT ERROR。

v3.9.6

  • 定式参考图卡片:底色改为不透明(BG 的 alpha 0.82 → 1.0)。半透明深色压在浅色棋盘/窗口上会 泛灰,参考图看着像块脏玻璃;现在是一块实心面板,小棋盘与手顺数字的对比度也随之稳定。 避让态(faded)不变:仍然只把外壳压到 40% —— 那一态存在的意义就是「卡片被棋盘压住且无处可挪时 别挡住棋盘」,真做成不透明就退回到 v3.9.2 修掉的那个毛病。
  • 门卫:joseki_card 78 → 79(新增:底色 alpha 恒为 1.0)。

v3.9.5

  • 「她还没应手,玩家又能下一手」(根因: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。

v3.9.4

  • 角内取景 13 路 → 9 路,候选点菱形 → 白子:
    • 9 路:实测 25 条库线最宽只用 8 路(只有「星位·守角后被挂角」到 8),9 路窗口装得下全部。 格距 17px → 26px、石子直径 15px → 23px,角内那颗子终于是「一颗子」而不是一个点。
    • 越界判定扩到全部点:以前只看手顺(line),现在候选点与走错点也算——窗口收小后, 外线候选(如 12,3)或远处的走错点不会再被静默画到画外,一律退回 19 路全盘。
    • 候选点画成白子:白底 + 琥珀外环(区分「待选的一手」与「已经落下的白子」)+ 序号; 退回 19 路时石子只有 ~10px,白子加数字会糊成一团,那时只画一圈琥珀环,序号交给底部图例。
  • 门卫:前端十支全 PASS,joseki_card 76 → 78 项(新增:候选点越界退回 19 路 / 走错点越界退回)。

v3.9.3

  • 定式参考图卡片:能关、能看清。此前卡片没有任何关闭控件——只有 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,卡片再高就会一出生就压住棋盘、只能退回兜底位)。
  • 门卫:前端十支全 PASS,joseki_card 62 → 76 项(新增:关闭热区位置 / 命中判据 / dismiss 收起 / 手关过的同一结构点不再弹 / 换结构点照常弹 / 13 路取景 + 四角原点 / 越界退回 19 路 / 角名不认识退回 / 徽标文案 / 候选图例过滤脏值 / TTL 60s 两侧)。

v3.9.2

  • 定式参考图卡片压在棋盘上只剩一块灰影(用户截图复现,--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_card 51 → 62 项(新增:淡出不动整卡透明度 / dim 往返 / 落位 不压棋盘格 / 完整在窗内 / 不压右栏展开 / 就近挑位 ×3 / 哪都放不下退回兜底)。

v3.9.1

  • 兜底台词刷屏的根因修复(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_service 36 OK、前端十支全 PASS (joseki_level 25,其余与 v3.9.0 同)、--headless --quit 无 SCRIPT ERROR。

v3.9.0

  • 玩家棋风画像(新增 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_service 36 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。

v3.8.0

  • 预测线 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_service 36 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.7.0

  • 定式导学从「结构点必说」改成「开关 + 频率」(口径修订,取代 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_service 36 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。

v3.6.0

  • 定式教学从「走完才讲」变成主动跟课(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 照常归位)。
  • 接口:joseki WS 事件在旧三键(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_service 36 OK、四支前端门卫(44 / 24 / 20 / 77)全 PASS、--headless --quit 无 SCRIPT ERROR。

v3.5.2

  • 修复:她忙的时候发的聊天被后端静默挡下(POST /api/chat 回 {"ok": false, "busy": true}), 玩家的话进了聊天栏却永远等不到回复。 现在改成先本地排队:输入框下面那行(右下角)显示 「排队中 N 条 · 队头前 8 字」,她上一句说完、语音也播完的那一刻自动一条接一条发出去—— 不用玩家再发一条来「催」;最多排 5 条,满了时不吞话(原文留在输入框 + 状态栏提示)。
  • 实现:新增纯逻辑 game\scripts\net\ChatQueue.gd(FIFO / 上限 / 提示文案 / 「能不能发」判据, 门卫 tests\test_chat_queue.gd 17 项);NetClient.chat() 改成吃回包——忙 ⇒ 入队,发出后 只有明确 ok: true 才算收下(忙 / 非 2xx / 读不懂的回包一律退回队头,另压 2 秒重试闸), 新增 chat_queue_changed 信号与 flush_queued_chat(env)(三道门禁:空闲判据 / WS 未连接不发 / 失败节流),门卫 tests\test_net_chat.gd 12 项;Main.gd 只留胶水(提示标签 + 每帧续发 + 不吞话)。
  • 已知取舍:请求其实到了后端、只是回包丢了时,这条会重复发一次(她可能回两遍)—— 宁可极少数情况重复一次,也不吞玩家的话。
  • 冒烟:五支前端门卫全 PASS(17 / 12 / 87 / 24 / 50)、--quit 无 SCRIPT ERROR、后端 354 OK、 katago_service 36 OK;另跑了「她忙时连发两条」的无头端到端(假后端 + 真 Main.tscn): 排队 → 右下角提示 → 她说完自动依次发出、提示消失、顺序正确、全程不用手动再发。
  • 文档:README 使用说明「输入框」一栏补排队与提示的说明;新增本计划 docs\plans\06-chat-queue-when-busy.md。

v3.5.1

  • 修复:在她还在念上一句时发聊天,回复会被扣住——聊天栏一直挂着「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 的时机与修复前逐字一致。

v3.5.0

  • 语音链路整体搬到后端:新增 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 同步。
  • 文档:扩写「二、快速开始」为详细使用说明——首次运行清单、启动顺序与就绪判定、界面与操作逐项、 一手棋的完整流程(含音画同步)、语音与情绪、常见问题排查表、开发者测试与调试命令(章节编号未动)。

v3.4.0

  • 回合编排显式成一张状态图:新增 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 的边界)。

v3.3.0

  • 长期记忆(默认关闭):新增 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;管理端点的写 / 查另推 memory WS 事件(纯元数据,前端只记日志)。 新增配置 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 卡住也不会冻住后端事件循环。

v3.2.0

  • 后端 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 事件,只用于状态栏「她正在查资料…」提示。 见第四节「工具(发言侧)」。

v3.1.0

  • 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 每次失败刷新,两者成对读(见第六节)。

v3.0.0

  • 只保留远程后端:客户端删除 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 必归位(此前会留下棋盘永久锁死的隐患)。

v2.0.1

  • 远程模式健壮性微调:后端 /api/move、/api/undo 对畸形 / 越界参数改为优雅拒绝(不再返回 HTTP 500);客户端 REST 请求加 30 秒超时,避免后端卡住时请求节点不释放(长时间运行不泄漏);WS 连接建立时的 hello 事件附带忙闲状态,界面锁状态在连接瞬间即正确。另修正事件契约文档、补充 WS 异常日志。当时默认的「客户端内编排」行为不变(v3.0.0 已删除该模式)。

v2.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,看屏幕搭话也已移除。)

v1.5.0

  • 聊天防复读加强:抑制「非相邻的原样复读」——此前只拦紧挨着的重复句,落子宣告(如「我在D16补一手」)夹在中间时,同一句点评仍会再冒一次;现在按最近 8 条窗口去重,隔一句话的一字不差复读也会被拦下(落子宣告本身不受影响)。
  • 方位描述纠错:点评里的「左/右/上/下」必须先按坐标核对——字母是列(从左到右)、数字是行(从上到下);两子同列只能说「上下」、同行才算「左右」,核对不上就别说方位词(如「你这两颗黑子一左一右」说成实际同列一上一下的错误不再出现)。
  • 代码整理:game\scripts\ 按职责重新分类为 core / ai / services / ui 四个子目录(引用与场景同步更新);清理过期注释与只写不读的死变量,行为不变。

v1.4.0

  • 定式导学:对局中走出常见定式(点三三 / 小飞挂 / 双飞燕…,入门定式库取自 KataGo 自对弈实测)时,Azure 讲解要点、棋盘按手顺点亮高亮轨迹;你偏离已吻合的定式时,她会轻轻提一句常规形状(点到即止)。识别纯本地计算、零引擎与额外 LLM 调用(成型讲解直接替换该手点评);悔棋回滚后可重新讲解;右侧栏「定式导学」可关,关掉行为与旧版一致。
  • 发言更像真人:提示词删掉可被照抄的示例句、补「说话方式」小节;LLM 侧开启重复惩罚;完全相同的重复句直接抑制。新增导演层:重要点评前有犹豫节拍、聊天面板逐字打字(确定性本地规则,零额外开销)。
  • 语气词与情绪:预制语音(欢迎语 + 语气词)扩到 20 条——新增 加油 / 得意 / 傲娇 / 叹气 四类语气,并全部以新一轮高精度音色合成逐条择优替换,更接近 Azure 声线;新增 [[tone:…]] 情绪标记与去重(同一情绪 30 秒内只出声一次)、语气词不叠响、思考声每回合限一次。
  • 思考三档:快速 / 中等(推理限 token 预算并注入收尾提示)/ 强思考,设置面板与 config.json 均可选;旧 llm_think_strong 自动迁移。
  • 本地服务更稳:对局中看护本地 LLM 服务,意外退出(如 GPU 故障)自动重新拉起;llama-server 加载期 503 显示「正在加载」而不是报错。
  • 语音加载期不再误用系统音色:本地语音模型加载期间只出文字、绝不降级为 Windows 音色;TTS 地址指向外部 API 时不拉起 / 不等待本地服务。
  • 对局进程观:按手数识别布局 / 中盘 / 收官,让她的口吻与当前进程匹配(治刚开局就喊「反击」「缠斗」)。

v1.3.1

  • 修复强思考推理段泄漏:强思考把回复预算耗在推理上时,模型原始推理过程曾被当成正文显示给玩家(一整段全英文内心独白)。现在推理段永不显示——预算耗尽时自动改用快速模式重答一次,内容干净、语气自然(代价:强思考下每次回答会先多等 20~30 秒,日常陪玩建议用「快速」模式)。
  • 走神搭话先看屏幕:玩家切到别的窗口(微信/浏览器…)时,走神搭话一律先看一眼屏幕、结合屏幕内容开口,不再只说默认的「还在吗」;游戏窗口在前台时直接普通搭话(不白起截图进程)。看屏幕冷却从 3 分钟缩短到 1 分钟。(v3.0.0 已移除。)

v1.3.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 的旧实例)。

v1.2.0

  • 开场秒开口:第一句欢迎语改为预制语音(assets\voice\welcome_<玩家名>.wav),启动即播、零等待;缺文件自动回退普通合成。
  • 本地服务随游戏自动起停:启动时一并拉起本地 LLM / CosyVoice / 看屏幕 VLM 服务,退出时清理由游戏拉起的进程(缺环境自动跳过,不影响使用)。(v3.0.0 已移除:游戏不再拉起任何服务。)
  • 新增 llm_auto_start / llm_exe / llm_model_path / llm_port 与 vlm_* 配置项。
  • 提速:LLM 前缀缓存复用;思考型模型默认关思考(云端不认自动回退);CosyVoice 边播边合成、长句合并(可开关)。

v1.1.0

  • 长闲置看屏幕搭话:空闲时截屏 + 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、以及下载试玩并反馈的使用者。

About

Azure · 围棋陪练:本地 KataGo 算棋 + 可自定义 LLM/TTS API 与提示词

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages