Files
Qt_DesktopPet/README.dev.md
T

579 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# QtDesktopPet
QtDesktopPet 是一个基于 Qt Widgets / C++17 的 Windows 桌面宠物项目,当前已具备多状态 PNG 帧动画、托盘控制、角色包导入与切换、用户自定义大模型对话、设置面板和 Windows 发布打包能力。项目现阶段重点是完善稳定性、性能回归、角色管理和发布体验。
## 当前状态
已实现:
- 透明无边框桌宠窗口
- 鼠标拖动
- 右键菜单退出和状态测试
- 置顶切换
- `resources/characters/shiroko` 默认角色包读取
- PNG 序列帧动画播放
- `idle` / `talk` / `think` / `sleep` / `happy` / `drag` / `error` 状态
- 托盘显示、隐藏、退出
- 单实例限制,重复启动会唤醒已有实例
- 隐藏时暂停动画,显示时恢复动画
- 保存窗口位置、置顶、缩放和性能设置
- 文件日志和基础轮转
- 设置窗口按当前屏幕居中弹出
- 应用设置:缩放、性能模式、隐藏暂停、懒加载
- 状态级动画预热和 LRU 缓存卸载
- AI Provider 分组配置
- 设置页内 AI 连通性测试
- Windows DPAPI 加密保存 API Key
- 非 Windows 环境经用户确认后明文保存 API Key
- OpenAI Compatible 聊天请求
- SSE 流式输出
- 聊天输入框
- AI 回复气泡
- 对话历史面板
- 内存历史上限、可选本地历史保存、搜索和 Markdown/JSON 导出
- AI 请求取消和对话清空
- Google Gemini 原生聊天请求
- 角色文件夹导入和角色切换
- 删除用户导入角色
- 角色导出和打开用户角色目录
- 本地一次性和重复提醒:聊天创建、查询、取消,重启后 pending 提醒不丢
- 提醒到点气泡提示、稍后提醒、拖动后延迟提示和隐藏时托盘通知
- 提醒音效切换、试听、用户 wav 导入和删除
- 天气查询 v1:Open-Meteo 天气源、城市解析、默认城市、公网 IP 定位兜底和模板回复
- 本地文件操作 v1:读取文本文件、列出文件夹、复制、备份、重命名
- 联网模式:输入框开关触发 AI Provider 原生联网能力,支持 OpenAI 官方 Web Search 和 Gemini Google Search grounding
- 应用启动 v1:通过聊天打开已登记应用、开始菜单快捷方式或用户确认选择的 `.exe`
- Windows 发布打包脚本和 Inno Setup 安装器脚本
- Windows GUI 子系统,Release exe 双击不弹控制台窗口
尚未实现:
- 长期性能压测记录
- 发布包实机安装/卸载验证
- 文件操作 zip 打包、删除、覆盖、移动、脚本/命令执行
- 搜索网页全文抓取、长期缓存和浏览器自动打开网页
- 应用启动脚本、命令行参数、管理员权限和跨平台应用发现
## 技术栈
- C++17
- Qt 6 Widgets
- Qt 6 Network
- Qt 6 Multimedia
- CMake
- PNG 图片序列帧
- JSON 配置文件
- Windows 10 / Windows 11 优先
## 构建
推荐环境:
- Qt 6.5.3
- CMake 3.20+
- Ninja
- MinGW 11.2.0 或已配置好的 Qt MSVC Kit
MinGW 示例:
```powershell
cmake -S . -B build/mingw-debug -G Ninja `
-DCMAKE_BUILD_TYPE=Debug `
-DCMAKE_PREFIX_PATH=D:/Qt/6.5.3/mingw_64 `
-DCMAKE_C_COMPILER=D:/Qt/Tools/mingw1120_64/bin/gcc.exe `
-DCMAKE_CXX_COMPILER=D:/Qt/Tools/mingw1120_64/bin/g++.exe
cmake --build build/mingw-debug
```
如果使用 Qt Creator,也可以直接打开根目录 `CMakeLists.txt`,选择合适的 Qt Kit 后构建。
## 应用图标
当前应用图标位于:
```text
resources/icons/app_icon.ico
resources/icons/app_icon_1024.png
```
`app_icon.ico` 用于窗口图标、托盘图标和 Windows exe 资源图标;托盘图标加载失败时会回退到默认角色包的 `preview.png``app_icon_1024.png` 作为高分辨率源图保留。
运行时会优先读取可执行文件同级的 `resources/icons/`,找不到时回退到源码目录下的 `resources/icons/`。Windows exe 图标需要重新构建后生效。
## 角色包
当前默认角色包位于:
```text
resources/characters/shiroko/
```
内置角色包按 `resources/characters/<characterId>/` 组织。用户导入的角色包不会写入安装目录,而是复制到用户数据目录:
```text
QStandardPaths::AppDataLocation/characters/<characterId>/
```
角色包基本结构:
```text
resources/characters/shiroko/
character.json
preview.png
idle/
talk/
think/
sleep/
happy/
drag/
error/
```
当前素材版本为 `2.1.0-stable`,所有帧使用 512x512 透明画布。当前实现会读取当前角色包的各状态配置,并按 `character.json` 中的 FPS 播放。
运行时会合并内置角色和用户导入角色;内置资源优先读取可执行文件同级的 `resources/characters/`,找不到时回退到源码目录下的 `resources/characters/`
角色导入:
- 只支持导入本地文件夹,不支持 zip
- 导入前先验证源文件夹;验证失败只弹窗提示,不复制、不创建、不覆盖文件
- 验证通过后复制到用户数据目录
- 角色 id 优先读取 `character.json``id`;为空时使用文件夹名
- 角色 id 只允许 ASCII 字母、数字、点、下划线和短横线,且不能以点开头或结尾
- 用户角色同名时会询问是否覆盖
- 内置角色 id 不能被导入包覆盖
- 验证要求:`character.json` 可解析、id 安全、存在 `idle``defaultState`、状态路径安全、fps 合法、每个声明状态至少有一张可读 PNG
- 如果 `base.anchorY + bubble.offsetY` 计算出的气泡锚点明显偏低,导入时会提示用户检查配置,但不强制阻止导入
- 只允许删除用户导入角色;选择内置角色删除时只会提示“不能删除内置角色”,不做文件操作
- 设置页支持导出当前选择的角色到用户选择目录,内置角色和用户角色都可以导出副本
- 导出目标目录已存在时会二次确认;不确认时不会覆盖
- 设置页支持打开用户角色目录,便于检查导入角色文件
懒加载现状:
- `enableLazyLoad=true` 时,启动阶段只建立状态到帧路径的索引
- 某个状态首次播放时加载该状态的 PNG 帧
- 启动后会在主线程按批次预热常用状态,避免一次性加载全部帧
- 已加载状态按状态级 LRU 策略管理,超过动画缓存上限时卸载非保护状态
- 单轮预热不会反复重新加载刚被 LRU 卸载的状态,避免缓存上限较低时出现加载/卸载循环
- 隐藏到托盘时可释放非保护动画缓存
- `enableLazyLoad=false` 时仍保持启动阶段加载全部状态帧的兼容行为
## 天气查询
当前支持通过聊天输入查询基础天气,例如:
```text
西安天气怎么样
明天西安天气怎么样
后天纽约天气
未来三天北京天气
今天天气怎么样
```
天气查询使用 Open-Meteo Forecast API 和 Open-Meteo Geocoding API,不需要 API Key。回复采用稳定模板,不依赖 AI 润色。支持当前/今天、明天、后天和未来 1-3 天基础天气;空气质量、天气预警、天气提醒联动、复杂小时级降雨判断和穿衣指数暂不支持。
当前 v1 推荐使用市级城市名。用户仍可输入天气源可识别的中文或英文地名,但区县、乡镇、街道不保证精确识别,可能无法匹配,或被匹配到上级/同名城市。当前会读取天气源返回的前 5 个地理编码候选;如果存在同名城市,仍使用第一个结果查询天气,并在回复中提示当前使用的城市和其他候选。
城市来源优先级:
- 用户输入明确城市
- 设置页“天气”中的默认城市
- 默认城市为空且开启开关时,根据公网 IP 定位判断城市
当使用设置页默认城市或公网 IP 定位时,回复会明确说明城市来源。公网 IP 定位使用 ipapi.co;如不希望请求该服务,可在设置页关闭“无默认城市时根据公网 IP 判断城市”。
设置页“天气”中提供“测试默认城市”按钮,只验证输入框当前城市会匹配到哪个城市、行政区和国家,不自动保存配置;最终保存仍由设置页 Save 控制。
天气配置保存到:
```text
QStandardPaths::AppConfigLocation/weather_config.json
```
天气配置损坏时会备份为:
```text
weather_config.broken.yyyyMMdd-HHmmss.json
```
天气查询由独立 `src/weather/` 模块处理,`PetWindow` 只负责展示查询状态和结果。AI 正在请求或流式回复时不会启动天气查询,以避免覆盖 AI 气泡。
## 聊天历史管理
聊天记录默认只保存在内存中。设置页“聊天”中开启“保存聊天记录到本地”后,会保存到:
```text
QStandardPaths::AppConfigLocation/conversation_history.json
```
历史管理能力:
- 支持按关键词搜索当前聊天历史
- 支持按 Provider 和模型筛选;旧历史缺少元数据时会按未知处理
- 支持导出当前筛选结果为 Markdown
- 支持导出当前筛选结果为 JSON
- 清空聊天记录需要确认,并会清空当前内存和本地保存记录
- 导出内容不包含 API Key 或 AI 配置
- 日志不会记录完整聊天正文
## 定时提醒和音效
当前支持通过聊天输入创建一次性和重复本地提醒,例如:
```text
10分钟后提醒我喝水
半小时后提醒我休息
一个半小时后提醒我喝水
明天9点提醒我开会
后天9点提醒我开会
6月3日9点提醒我提交
下周一上午10点提醒我周会
每天9点提醒我打卡
每天提醒我9点打卡
每日晚上8点提醒我吃药
每周一上午10点提醒我周会
每周一提醒我上午10点周会
每星期五下午3点提醒我提交周报
每月3号9点提醒我交报告
每月3号提醒我9点交报告
提醒列表
取消喝水提醒
```
提醒数据保存到:
```text
QStandardPaths::AppConfigLocation/reminders.json
```
提醒数据使用原子写入,写入失败时不会触发到点 UI,也不会覆盖旧的有效提醒文件。已触发和已取消记录会写入 `finishedAt`;旧版数据没有该字段时按 `remindAt` 兼容读取。
提醒调度保留最近提醒的精确 timer,同时每 60 秒做一次兜底扫描;程序显示、外部激活或系统睡眠唤醒后,都会重新检查已到期 pending 提醒。
设置页支持编辑 pending 提醒的标题、下一次时间和重复规则;已触发/已取消历史只读。历史记录默认只保留最近 20 天,设置页“清理20天前历史”只删除超过 20 天的已触发/已取消记录,不影响 pending。
提醒文件损坏时会备份为:
```text
reminders.broken.yyyyMMdd-HHmmss.json
```
内置提醒音效位于:
```text
resources/sounds/reminders/
```
用户导入的提醒音效保存到:
```text
QStandardPaths::AppDataLocation/sounds/reminders/
```
音效规则:
- 默认音效为 `reminder_default`
- 提醒触发时使用当前设置页选择的全局音效;修改音效后对所有未触发提醒立即生效
- 内置音效可切换、可试听,但不能在设置页删除
- 用户音效只支持导入 PCM wav
- 用户导入音效可切换、可试听、可删除
- 删除当前用户音效后会回退到 `reminder_default`
触发规则:
- 桌宠可见时显示气泡,不发系统通知
- 桌宠隐藏时发 Windows 托盘通知,不在下次显示时补气泡
- AI 正在请求或流式回复时,按隐藏场景处理:播放音效并发 Windows 托盘通知,不显示气泡
- 托盘或系统通知后端不可用时只记录日志,不补气泡
- 用户拖动中不打断 `drag`,拖动结束后显示气泡
- 多条提醒同时触发时,可见状态下会按队列逐条展示
- 桌宠可见触发时显示 `知道了``5分钟后再提醒`
- `5分钟后再提醒` 会创建一条新的一次性提醒,不影响原重复规则
- 重复提醒支持 `每天 / 每周 / 每月``工作日 / 每两天 / 每月最后一天 / 自定义间隔 / 农历` 等复杂规则暂不支持
- 每月 31 号这类规则会跳过不存在该日期的月份,寻找下一个有效月份
- 用户音效删除仅允许删除用户音效目录内的安全 sound id,内置音效和非法路径不会被删除
## 本地文件操作
本地文件操作通过聊天意图触发,但不会直接使用聊天文本里的路径。所有文件和文件夹都必须由用户在系统文件选择框中主动选择。
当前 v1 支持:
- 读取用户选择的常见文本文件:txt、md、log、json、csv、ini、xml、yaml
- 列出用户选择的文件夹,最多显示前 200 项
- 复制用户选择的文件到用户选择的目标文件夹
- 为用户选择的文件创建同目录备份
- 重命名用户选择的文件
安全规则:
- 写操作会先展示操作计划并要求确认
- 不覆盖已有文件
- 不删除文件
- 不移动大量文件
- 不执行脚本或命令
- 不访问 Windows 系统目录
- 拒绝符号链接或符号链接目录内的路径
暂不支持:
- zip 打包
- 删除、覆盖、移动
- 修改源码
- 执行脚本或命令
## 联网模式
联网模式不是搜索引擎聚合器。用户在输入框打开“联网”开关后,普通聊天会进入当前 AI Provider 的原生联网能力:
- OpenAI 官方 Provider:使用 Responses API Web Search。
- Google Gemini Provider:使用 Gemini Google Search grounding。
- DeepSeek 官方 API:当前不提供托管联网搜索工具,显示“不支持原生联网”。
- Custom / 第三方 OpenAI-Compatible:默认无法确认联网能力,不会尝试旧搜索页抓取。
配置保存到:
```text
QStandardPaths::AppConfigLocation/web_config.json
```
交互规则:
- 联网开关默认关闭,避免隐私和费用意外。
- 设置页“联网模式”只显示能力状态、开关记忆、默认开关、超时、来源展示和测试联网模式。
- 当前模型不支持联网时,输入框打开联网后会明确提示,不发起伪联网请求。
- 支持联网的 Provider 返回来源时会展示来源;模型判断无需联网时允许无来源,并显示“模型未使用联网来源”。
-`search_config.json` 已废弃,新版不会读取、迁移或写入。
后续可扩展:
- 更多 AI Provider 的原生联网适配。
- 结构化搜索 API 或自建联网后端。
- 更细粒度的来源证据摘录和引用 UI。
## 应用启动
应用启动是独立能力,不归入本地文件操作。可通过聊天输入:
```text
打开 Codex
启动酷狗音乐
帮我打开 VSCode
```
配置保存到:
```text
QStandardPaths::AppConfigLocation/launcher_config.json
```
设置页“应用启动”支持:
- 启用或关闭应用启动
- 未知应用允许用户手动选择 `.exe`
- 登记应用名称和别名
- 编辑、删除、测试启动已登记应用
安全边界:
- 启动前始终二次确认,确认内容包含应用名、路径、工作目录和来源
- 只允许启动 `.exe` 或开始菜单 `.lnk` 快捷方式
- 不执行 `.bat` / `.cmd` / `.ps1` / `.vbs` / `.js` / `.msi`
- 不执行聊天文本里的命令
- 不拼接聊天文本参数
- 不以管理员权限启动
- 手选 `.exe` 后可选择记住为当前名称,后续走登记应用匹配
## 配置和日志
应用配置保存到 Qt 标准配置目录:
```text
QStandardPaths::AppConfigLocation/app_config.json
```
配置损坏时会备份为带时间戳的文件:
```text
app_config.broken.yyyyMMdd-HHmmss.json
ai_config.broken.yyyyMMdd-HHmmss.json
conversation_history.broken.yyyyMMdd-HHmmss.json
weather_config.broken.yyyyMMdd-HHmmss.json
web_config.broken.yyyyMMdd-HHmmss.json
launcher_config.broken.yyyyMMdd-HHmmss.json
```
日志输出到文件,不输出到控制台:
```text
QStandardPaths::AppConfigLocation/logs/QtDesktopPet.log
```
如果 Qt 无法取得标准配置目录,则回退到当前工作目录下的 `logs/QtDesktopPet.log`
日志轮转规则:
- 单个日志文件超过 2MB 时轮转
- 最多保留 3 个旧日志文件
- 文件名为 `QtDesktopPet.log.1``QtDesktopPet.log.2``QtDesktopPet.log.3`
## 发布打包
仓库提供 Windows Release 打包脚本。脚本不负责 CMake 构建,先手动完成 Release 构建,再把 exe 路径传给脚本:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/package_release.ps1 -ExePath build/release/QtDesktopPet.exe
```
脚本会生成目录包和 zip
```text
dist/QtDesktopPet-<version>-windows-x64/
dist/QtDesktopPet-<version>-windows-x64.zip
```
发布目录包含:
```text
QtDesktopPet.exe
Qt runtime
resources/characters/
resources/icons/
resources/sounds/
LICENSE
README.md
```
脚本会调用 `windeployqt.exe` 收集 Qt 运行库。若当前 PATH 找不到 `windeployqt.exe`,需要指定 Qt bin 目录下的工具路径:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/package_release.ps1 `
-ExePath build/release/QtDesktopPet.exe `
-WindeployQtPath D:\Qt\6.x.x\msvcXXXX_64\bin\windeployqt.exe
```
生成 Inno Setup 安装器:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/package_release.ps1 `
-ExePath build/release/QtDesktopPet.exe `
-BuildInstaller
```
安装器最终默认输出到项目根目录:
```text
QtDesktopPet-<version>-windows-x64-setup.exe
```
脚本会先让 Inno Setup 输出到当前盘符下的纯 ASCII 临时目录,例如 `D:\QtDesktopPetInstallerOutput`,再把最终安装包复制回项目根目录,避免中文项目路径下出现 `EndUpdateResource failed (5)`。如果需要改变最终安装包目录,可传入 `-InstallerOutputDir`
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/package_release.ps1 `
-ExePath build/release/QtDesktopPet.exe `
-BuildInstaller `
-InstallerOutputDir D:\ReleaseOutput
```
如果需要改变 Inno Setup 的临时编译输出目录,可传入 `-InstallerWorkOutputDir`
本地生成的安装包也可以集中放到 `release_packages/`
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/package_release.ps1 `
-ExePath build/release/QtDesktopPet.exe `
-BuildInstaller `
-InstallerOutputDir release_packages
```
`dist/``release_packages/` 都是本地发布产物目录,不进入 Git。
脚本默认优先查找:
```text
D:\Inno Setup 7\ISCC.exe
D:\Inno Setup 6\ISCC.exe
C:\Program Files (x86)\Inno Setup 7\ISCC.exe
C:\Program Files (x86)\Inno Setup 6\ISCC.exe
```
如果 Inno Setup 安装在其他位置,可传入 `-InnoCompilerPath`
安装器页面提供可选项:
- 创建桌面快捷方式,默认不勾选。
- 开机自启动,默认不勾选;勾选后写入当前用户 `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`,不需要管理员权限。
应用内设置页的“应用设置”页也提供“开机自启动”开关,保存后会立即写入或移除同一个当前用户 Run 项。卸载时会清理该 Run 项,避免卸载后残留自启动入口。
安装器卸载时会询问是否同时删除用户配置、导入角色、聊天记录和日志;用户确认后会在卸载完成阶段删除当前用户下的 QtDesktopPet 数据目录。
## 开发诊断
仓库提供开发用性能采样脚本,不进入普通用户发布包:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tools/perf_sample.ps1
```
默认采样当前 `QtDesktopPet` 进程 5 分钟,每 5 秒一条,CSV 输出到:
```text
reports/perf/
```
`reports/perf/` 已加入 `.gitignore`。稳定性检查记录模板见:
```text
docs/performance_stability_check.md
```
发布包应排除 `tools/``docs/``reports/``build/``dist/``release_packages/``.git/`,只保留运行必需文件、`resources/characters/``resources/icons/``resources/sounds/``LICENSE` 和必要说明。
## AI 配置和聊天
当前正式聊天支持 OpenAI Compatible 和 Google Gemini 两类协议。已提供以下 Provider 配置入口:
- OpenAI
- Google
- DeepSeek
- Custom
其中 OpenAI、DeepSeek、Custom 走 OpenAI Compatible 形式配置;Google 走 Gemini 原生 REST 接口。旧版保存过的已废弃 Provider 配置会在读取 AI 配置时清理,废弃 Provider 被选中时会回退为 `custom`
已支持:
- 用户自定义 Base URL
- 用户自定义 API Key
- 用户自定义 Model
- 用户自定义 Path
- 超时、Temperature、Max Tokens
- 流式输出
- Google Gemini `generateContent` / `streamGenerateContent`
- 请求中切换 `think`
- 收到首段输出后切换并保持 `talk`
- 失败时切换 `error`
- API Key 不写入日志,不在错误提示中完整显示
- 对话历史面板记录用户消息和 AI 最终回复
AI 测试入口已从角色右键菜单移除,并迁移到设置页。
## 隐私说明
程序只会把用户消息发送到用户自己配置的接口。用户需要自行判断第三方代理、中转服务或自建服务是否可信。项目不会默认承诺第三方接口的隐私安全。
日志会记录请求诊断信息,例如 Provider、Base URL 主机、Path、HTTP 状态码、响应大小、错误摘要等;日志不应记录完整 API Key、Authorization Header、完整消息正文或完整错误响应正文。错误响应只保留脱敏摘要。
当前对话历史默认保存在内存中,已支持内存历史上限、请求上下文截取和可选本地历史保存;相关上限可在设置页调整。
## 素材版权说明
源码采用 MIT License。
角色素材、图片、图标等资源需要单独确认授权。当前 `shiroko` 素材用于桌宠加载器、动画和状态切换测试;在公开发布或正式分发前,需要确认素材的版权、再分发权限和适用范围。
用户导入或替换的素材,其版权责任由用户自行承担。
## 许可证
项目源码使用 MIT License,见 [LICENSE](LICENSE)。