docs: 修改文档

This commit is contained in:
2026-05-07 13:26:22 +08:00
parent cd6bac3d00
commit 7a1c1c6093
16 changed files with 464 additions and 57 deletions

View File

@@ -53,17 +53,12 @@
## 设置开发环境 ## 设置开发环境
```bash ```bash
# 克隆仓库
git clone https://git.pluv27.top/pluv/HeurAMS git clone https://git.pluv27.top/pluv/HeurAMS
cd HeurAMS cd HeurAMS
# 可能需要切换到 dev 分支
git checkout dev git checkout dev
# 如果决定使用 uv (推荐) # 如果决定使用 uv (推荐)
## 首先要安装uv, 例如通过 pip 或者其他包管理器
python3 -m pip install uv python3 -m pip install uv
uv sync --all-extras # 同步开发运行环境 uv sync --all-extras # 同步开发运行环境
@@ -72,13 +67,12 @@ uv run heurams # 验证包安装
uv run heurams-tui # 启动 TUI uv run heurams-tui # 启动 TUI
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)运行 HeurAMS) # 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)运行)
## 安装依赖并将 HeurAMS 安装为本地包 python3 -m pip install -e .[all] # 安装依赖并将 HeurAMS 安装为本地包
python3 -m pip install -r requirements.txt
python3 -m pip install -e .
python3 -m heurams # 验证安装 python3 -m heurams # 验证安装
python3 -m heurams.interface # 启动 TUI python3 -m heurams.interface # 启动 TUI
``` ```
@@ -109,10 +103,9 @@ HeurAMS 被设计为一个可独立于前端的程序库, 这意味着:
您可以: 您可以:
- 协助创建各种语言的翻译来翻译软件的界面 (但我们目前还没有 i18n 平台, 所以如果您想贡献翻译, 可能需要手动联系我们) - 协助创建或核对各种语言的翻译来翻译软件的界面和文档
- 制作图像、主题、音效乃至制作开放的记忆单元集给其他用户使用 - 制作开放的记忆单元集(包括但不限于文字、图像、音效)给其他用户使用
- 改进软件配套的文档 - 改进软件配套的文档
- 维护软件的开发/交流群组
- 给其他用户答疑解惑或分享自己的经验 - 给其他用户答疑解惑或分享自己的经验
- 在讨论区提出新想法或反馈问题 - 在讨论区提出新想法或反馈问题

407
FAQ.md Normal file
View File

@@ -0,0 +1,407 @@
# 常见问题
## 什么是终端模拟器?
终端模拟器是在图形桌面环境中模拟并使用终端的应用程序, 例如 KDE Konsole, GNOME Terminal, Windows Terminal, iTerm2 等.
较旧 Windows 的那个很寒酸的小黑窗口也是终端模拟器(conhost.exe), 但它对此软件基本用户界面(以及一切现代终端应用)支持不佳, 建议在 Windows 平台使用 WezTerm (支持 sixel) 或 Windows Terminal (不支持 sixel).
## 软件支持移动设备吗?
基本用户界面 (Textual TUI) 可在 Android Termux 中良好运行.
此外, 正在开发的 KiriMemo 前端基于 KDE Kirigami 框架, 将原生支持 Android 和 iOS.
## HeurAMS 和 Anki 有什么区别?
大体地说:
| 方面 | HeurAMS | Anki |
|------|---------|------|
| 数据格式 | 文本文件 (TOML/JSON), 人类可读 | SQLite 和资源文件组成的专有压缩格式 (.apkg) |
| 复习模式 | 多阶段流程 + 多种谜题类型 | 单面/双面闪卡 |
| 算法系统 | 模块化, 可插拔, 多种算法可选 | 内置 SM-2 / FSRS |
| 插件生态 | 较少, 体现于类似微内核架构的"能力扩展", 例如新算法或新服务 | 多, 但为不受限的"猴子补丁" |
| 用户基数 | 少 | 多 |
| 现有资源丰富度 | 少 | 多 |
| AI 辅助产生单元集/牌组 | 原生支持 | 困难 |
| 协议 | AGPL-3.0, 有一个附加豁免条款 | AGPL-3.0 |
## 软件是免费的吗?
是的, 完全免费, 且开源. 您无需支付任何费用即可使用全部功能.
## 黑乎乎的这个界面我怎么用?
得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe``cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的.
但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些.
### 可以用鼠标
现代终端模拟器 (如 Windows Terminal、Konsole、iTerm2、WezTerm 等) 支持一个叫做 "鼠标跟踪" (Mouse Tracking) 的功能, 而 Textual 启动时会发送特殊指令给终端让它报告鼠标事件.
所以可能和您的想象不同, 您事实上可以直接用鼠标点击按钮, 就像使用普通软件一样.
### 也可以用键盘
- `Tab` 键在不同区域之间切换焦点
- `方向键` 在列表中上下移动
- `Enter` 确认选择
- `q` 返回
- 屏幕上会有按键提示, 例如 `[n] 导航器` 表示按 `n` 键打开导航器
### 触屏也可以
在平板或手机 Termux 中, 您可以触摸或者滑动屏幕操作.
## 我怎么启动这个软件?
首先需要确保系统中安装了 Python (推荐 3.12.13 版本) 并安装了 HeurAMS 的所需组件.
### Windows
打开"命令提示符"或"PowerShell", 输入以下命令后按回车, 或者把这玩意另存为快捷方式:
```
python -m heurams.interface
```
### macOS
打开"终端"应用程序, 输入以上命令.
### Linux
打开您的终端模拟器 (一般是按 Ctrl + Alt + T), 输入以上命令.
如果您觉得每次输入命令太麻烦, 可以创建一个桌面快捷方式或脚本文件, 详见网上的相关教程.
## 我怎么退出软件?
按键盘上的 `q` 键返回主界面后退出.
您的学习进度会自动保存, 不会丢失.
## 我看不到图片怎么办?
终端模拟器需要支持 sixel 图像协议才能显示图片.
- WezTerm (适用于几乎所有操作系统): 支持
- KDE Konsole: 支持
- GNOME Terminal: 不支持
- iTerm2 (macOS): 支持
- Windows Terminal: 不支持
- mintty (Windows): 支持
如果您的终端不支持图片, 软件的其他功能不受影响, 只是记忆内容中的图片无法显示.
## 中文显示成乱码或方框怎么办?
这说明您没有正确设置终端中文字体. 请检查:
1. 您没有使用 getty 和 xterm 这种明确不支持非 ASCII 字体的终端
2. 终端设置中的字体选项, 选择一款支持中文的字体, 例如 "Noto Sans SC", "微软雅黑", "Source Han Sans"
3. 确保终端的字符编码设置为 UTF-8 (通常是默认值)
## 我的数据存哪里了? 会不会丢?
数据存储在软件安装目录下的 `data/` 文件夹中:
- 您可以用记事本直接打开查看和修改
- 复制粘贴 `data/` 文件夹到 U 盘或网盘即可完成备份 (推荐定期备份)
- 即使软件卸载了, 只要保留 `data/` 文件夹, 重新安装后复制回去, 所有学习记录都在
## 怎么跟朋友分享我的单元集?
找到 `data/repo/` 下的对应文件夹, 复制整个文件夹发给朋友即可. 对方把它放到自己的 `data/repo/` 目录下就能用.
您也可以导出为单一文本文件或压缩包, 通过微信、QQ、邮件等方式分享.
## 我复制粘贴不了内容?
一般来说, 在终端中:
- 复制: Ctrl+Shift+C
- 粘贴: Ctrl+Shift+V
这和普通软件的操作习惯不太一样(因为 Ctrl + C 在终端中的语义是 "中断进程"), 但稍加适应即可.
## 字体太小/太大怎么办?
在您的终端模拟器设置中找到"字体大小"选项进行调整.
软件会跟随终端的字体设置.
## 为什么我的界面和截图不一样?
截图使用的是 KDE Plasma 桌面上的 Konsole, 80x25 字符尺寸, Cascadia Code 和 Noto Sans SC 字体.
如果您的终端尺寸更大, 界面会更宽裕; 如果使用不同字体或者不同操作系统, 视觉效果会略有差异.
功能上完全一致.
## 评分 (1-5) 是什么意思? 我该怎么打分?
需要说明的是, 我们非常不鼓励这种类似 Anki, 让用户自己直接给自己评分的单元集设计(在我们的程序中, 这种方式的实现被称为 `basic_puzzle`, 基本只用于算法测试).
因为我们认为这种方式非常主观, 而且还需要您思考"我是多少分""我是不是乐观了""我是不是分低了""我要是把分评错了怎么办"这一系列打断记忆进程且令人焦虑的问题, 这本质上是把责任推给用户, 并且违背了认知科学原理.
并且这种方式于学术研究与实验不利, 用户自评分产生的数据是不可靠的.
因此 HeurAMS 的前端内建了基于用户行为分析的自动评分系统, 也就是"谜题".
它会根据题目本身难度和您的答题行为(包括但不限于正确性, 操作回退次数, 有效答题时间)并自动为您评分.
但如果您或者某个单元集选择使用 `basic_puzzle`, 或者打算自己实现自动评分系统, 分数含义如下:
| 分数 | 含义 | 说明 |
|------|------|------|
| 1 | 完全忘了 | 一点都没想起来, 跟没学过一样 |
| 2 | 模糊 | 好像见过, 但答不上来 |
| 3 | 有点印象 | 想了一会儿才答对, 不太确定 |
| 4 | 比较顺利 | 能答对, 但稍微犹豫了一下 |
| 5 | 非常轻松 | 立刻反应过来, 毫不费力 |
-**1-2 分**: 软件会认为您还没掌握, 短期内会再次安排复习
-**3 分**: 正常掌握, 按计划间隔复习
-**4-5 分**: 掌握得很好, 下次复习间隔会拉长
我们建议您不要纠结, 凭第一感觉打分就好, 软件会根据您的评分自动调整复习节奏.
当然我们还是建议尽量避免这种方式并尽量使用其他谜题评测.
您可能认为那种方式可以让用户直接"干预"算法, 类似百词斩的"斩"功能用于跳过已经熟悉的内容, 实际上您并不需要那样做: 我们内建了"快速通过/正确应答"功能, 等同于直接选择"5".
## 我每天都要打开软件吗? 不学会怎样?
理论上不需要每天打开. 软件会自动记录每个知识点下次该复习的时间.
但建议您每天打开软件看下状态.
即使隔了几天甚至几周没学:
- 已经记住的知识点不会消失, 只是下次复习时会多复习几次
- 学习记录完好保存在 `data/` 文件夹里, 不会丢失
建议尽量按软件的提醒复习, 效果最好; 但忙的时候跳过几天也没关系.
## 能同时学多个科目吗?
可以.
每个科目或课程可以做成独立的"单元集".
## 我换电脑了, 怎么迁移数据?
1. 在旧电脑上复制整个 `data/` 文件夹到 U 盘或网盘
2. 在新电脑上安装好 HeurAMS
3. 用 U 盘里的 `data/` 文件夹覆盖新电脑上的 `data/` 文件夹
所有学习记录、配置、单元集全部迁移完毕.
## 一些术语听不懂
| 术语 | 意思 |
|------|--------|
| **单元集** | 相当于"一本书"或"一门课", 包含一系列知识点 |
| **谜题** | 测试您的题目类型 (选择题、填空题等) |
| **算法** | 决定什么时候该复习哪个知识点的"智能排课表" |
| **复习队列** | 今天需要复习的知识点列表 |
| **激活** | 第一次开始学习某个知识点 |
| **到期** | 到了该复习这个知识点的时间了 |
## 软件卡住了/没反应怎么办?
1. 建议先等片刻
2. 如果不行, 直接关闭终端窗口
3. 重新打开软件
4. 如果您有时间的话, 欢迎报告问题, 我们为此深表歉意
## 同时用 Anki 和 HeurAMS 会冲突吗?
不会.
两者是独立的软件, 数据互不影响. 您可以逐步将内容迁移到 HeurAMS, 也可以两个一起用.
## 我需要安装 Python 吗?
需要的, HeurAMS 是基于 Python 的软件.
- Windows/macOS: 从 python.org 下载安装即可
- Linux: 系统通常已自带 Python
- Android: 安装 Termux 应用, 然后在 Termux 中安装 Python (运行 `pkg in python`)
如果看到"python 不是内部或外部命令"的提示, 说明 Python 没有正确安装或添加到系统路径, 搜索"Python 安装教程"按步骤操作即可.
HeurAMS 建议的 Python 版本是 3.12.13.
## 软件安全吗? 会不会有病毒?
HeurAMS 是开源软件, 所有代码公开可查阅, 不会有病毒或后门.
它只读写自己的 `data/` 文件夹, 不会动您电脑上的其他文件.
## 软件报错, 出现一堆我看不懂的英文怎么办?
1. 把错误信息复制下来
2. 先找一下这个页面有没有收录您遇到的问题
3. 如果没有, 可以和软件日志一起上传到 issues, 我们会尽快处理
## 复习到一半可以暂停吗? 下次会从头开始吗?
暂时没有保存中间状态的功能, 但我们很快会添加.
## 怎么看我学了多少? 有统计吗?
仪表盘界面会显示统计信息.
您可以通过导航器随时回到仪表盘查看.
## 我觉得复习太快/太慢了, 能调吗?
可以. 您可以通过切换算法或调整算法参数或改变记忆单元数来改变复习节奏.
在设置界面可以找到相关设置.
## 能换界面颜色/主题吗?
能的, Textual 框架提供了多种主题.
## 我不小心删了东西, 能恢复吗?
这取决于您系统是否启用回收站, 软件本身没有自动备份功能.
## 能把软件放 U 盘随身带吗?
可以. 将整个 HeurAMS 文件夹复制到 U 盘, 在任意电脑上安装 Python 后运行即可. `data/` 目录下的学习数据也会一同携带.
## 怎么关掉语音朗读?
在设置界面中找到 TTS 相关选项, 将其关闭即可.
## 哪里可以下载别人做好的单元集?
目前项目还没有官方的单元集市场.
但随着社区发展, 未来可能会有用户分享的单元集, 您也可以和朋友互相分享.
## 我能把学习内容导出打印吗?
可以.
软件本身支持将单元集导出为单一文本文件, 您可以用任何文本编辑器打开并打印. 也可以直接复制内容到 Word 等软件.
## 我想从头重新学, 怎么重置?
删除 `data/repo/` 下对应单元集文件夹中的 `algodata.json` 文件即可重置所有学习进度.
## 如何创建自己的单元集?
`data/repo/` 目录下创建一个新文件夹, 包含以下文件即可:
```
data/repo/my_pack/
├── manifest.toml # 元信息: title, author 等
├── typedef.toml # 通用字段定义和谜题配置
├── payload.toml # 记忆条目内容
├── algodata.json # 算法状态数据 (可留空)
└── schedule.toml # 复习策略配置
```
您也可以使用我们提供的工具从 CSV 等格式转换, 或利用 AI 工具生成.
## 如何切换算法?
设置界面中有详细的说明.
## 如何从 Anki 导入?
暂时没有迁移工具, 因为两个软件的设计思路不同.
但欢迎关注 HeurStudio 项目, 它能从根本上解决内容创建与迁移问题 :)
## 为什么不用 Flutter?
Flutter 是构建跨平台图形界面的优秀框架, HeurAMS 的设计目标之一是保持核心程序库独立于特定前端.
但 Flutter 在 "集成 python" 方面不如 PyOtherSide, 只能通过 RPC 标准件和程序库通讯, 并且 Flutter 的桌面多窗口一直以来没有被官方稳定支持, 所以我们暂时放弃了 Flutter 而选择了 Kirigami.
当前我们优先开发了基于 Textual 的 TUI 前端和基于 Kirigami 的原生前端, 但这不排除未来出现 Flutter 或其他框架前端的可能性.
如果您有兴趣开发 Flutter 前端, 欢迎参考[贡献指南](CONTRIBUTING.md#新的用户界面前端).
## 软件需要联网吗?
核心复习功能完全离线可用. 以下功能需要联网:
- 文本转语音 (TTS) 的 Edge TTS 提供者
- LLM 提供者 (如 OpenAI 兼容 API)
- 从远程仓库下载单元集
## 许可证中的"本机 API 调用豁免"是什么?
简言之, 如果您在自己的程序中通过本地进程间 API 方式的通信 (如同一主机上的 RPC 调用) 使用 HeurAMS, 而无需通过网络转发, 则您的程序不受 AGPL-3.0 许可证的约束.
这项附加条款旨在鼓励第三方前端和工具的开发.
所以 HeurAMS 的许可证实质上是比原始的 AGPL-3.0 松一点的.
## HeurAMS 和百词斩有什么区别?
大体地说:
| 方面 | HeurAMS | 百词斩 |
|------|---------|--------|
| 使用场景 | 电脑/终端 | 手机 App |
| 学习内容 | 任何知识, 不限语言和科目 | 英语单词 |
| 记忆策略 | 多算法可选, 可自定义复习流程 | 固定算法, 不可调 |
| 测验方式 | 选择题/填空题/识别题等多种谜题 | 看图选词/听音选义等多种谜题 |
| 内容创建 | 自己创建或导入, 完全自由 | 仅官方提供词书 |
| 费用 | 完全免费, 无内购 | 免费记忆功能 + 付费课程 |
| 数据所有权 | 数据在您自己手上, 文本文件 | 数据不可被提取 |
| 离线使用 | 核心功能完全离线 | 部分功能需联网 |
| 学习统计 | 基础统计 | 打卡/排行榜/社交 |
## 百词斩有图片联想记忆, HeurAMS 也有吗?
支持.
如果您的终端支持图片显示 (如 Konsole 或者 WezTerm), 单元集中可以包含图片, 复习时会直接展示.
但图片需要您自己放入单元集.
## 百词斩有打卡和排行榜, HeurAMS 有吗?
目前没有.
HeurAMS 不设打卡、排行榜或社交功能, 也不向任何人收集您的学习数据.
## 百词斩有现成的词书, HeurAMS 去哪找内容?
百词斩的课程是官方制作好的, HeurAMS 的内容需要您自己创建或从社区获取.
详见"如何创建自己的单元集?".
## 百词斩在手机上用很方便, HeurAMS 能在手机用吗?
可以, 但现阶段需要您"折腾"一下.
Android 手机安装 Termux 后可运行 HeurAMS 的基本用户界面.
此外, 正在开发的 KiriMemo 前端将原生支持 Android 和 iOS, 这就不需要用户去折腾了.
## 百词斩能背单词, HeurAMS 还能学什么?
任何需要记忆的知识都可以: 外语单词、医学名词、法律条文、历史年代、化学方程式、编程语法、乐谱符号...
单元集的内容完全由您自己定义.
## 百词斩学英语效果很好, 换成 HeurAMS 会不会效果差?
考虑到百词斩的算法和词库是事实上闭源的, 我们无从得知算法来源.
但 HeurAMS 的架构设计可保证单元集制成后效果不比百词斩差, 甚至优于百词斩.
HeurAMS 的间隔重复算法基于相同的认知科学原理, 且算法透明可调, 您可以自由选择最适合自己的调度策略.
## 如何参与项目?
详见[贡献指南](CONTRIBUTING.md).
即使不是开发者, 您也可以通过编写文档、制作记忆单元集、翻译界面、答疑等方式参与.

View File

@@ -26,7 +26,7 @@
- 此外, 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法 - 此外, 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试 - 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
- 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器 - 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
- 内置 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程) - 内置 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性 - 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
- 得益于项目的模块化架构与单元集结构设计, 一个项目甚至可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好 - 得益于项目的模块化架构与单元集结构设计, 一个项目甚至可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
@@ -54,9 +54,11 @@
- 跨平台, 并支持触屏/鼠标/键盘多操作模式 - 跨平台, 并支持触屏/鼠标/键盘多操作模式
- 与几乎所有现代终端模拟器相容 - 与几乎所有现代终端模拟器相容
- 对于[支持 sixel 协议的终端模拟器](https://www.arewesixelyet.com/), 可显示图像内容 - 对于<a href="https://www.arewesixelyet.com/" target="_blank" rel="noopener noreferrer">支持 sixel 协议的终端模拟器</a>, 可显示图像内容
- 可通过 textual-web 作为服务部署, 并在任意浏览器使用 - 可通过 textual-web 作为服务部署, 并在任意浏览器使用
- 简洁直观, 键盘友好, 且高效率的用户界面设计 - 简洁直观, 键盘友好, 全功能且高效率的用户界面设计
- 易于嵌入: 可在 getty/kmscon 中运行而无需任何桌面图形服务
- 资源占用小, 运行流畅, 不拖泥带水
- 便于测试与调试程序库 - 便于测试与调试程序库
[查看屏幕截图](SCREENSHOTS.md) [查看屏幕截图](SCREENSHOTS.md)
@@ -98,7 +100,7 @@ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
| 依赖组 | 包含模块 | 说明 | | 依赖组 | 包含模块 | 说明 |
|--------|----------|------| |--------|----------|------|
| 最小化安装 | tabulate, toml, transitions | 核心驱动程序库始终必需 | | 最小化安装 | tabulate, toml, transitions | 核心驱动程序库, 始终必需 |
| interface | textual, psutil | 基本用户界面依赖 | | interface | textual, psutil | 基本用户界面依赖 |
| algo-fsrs | py-fsrs | FSRS 算法模块 | | algo-fsrs | py-fsrs | FSRS 算法模块 |
| tts-edgetts | edge-tts | 微软文本转语音 | | tts-edgetts | edge-tts | 微软文本转语音 |
@@ -113,6 +115,10 @@ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
我们提供原生 python 和 uv 两种安装方式. 我们提供原生 python 和 uv 两种安装方式.
详见[贡献指南](CONTRIBUTING.md). 详见[贡献指南](CONTRIBUTING.md).
## 常见问题 (FAQ)
详见[常见问题](FAQ.md).
## 项目架构 ## 项目架构
详见[架构说明](ARCHITECTURE.md). 详见[架构说明](ARCHITECTURE.md).
@@ -120,13 +126,14 @@ python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
## 参与项目 ## 参与项目
欢迎参与到项目协作中! 欢迎参与到项目协作中!
请参阅[贡献指南](CONTRIBUTING.md). 详见[贡献指南](CONTRIBUTING.md).
## 许可证 ## 许可证
### 项目本身 ### 项目本身
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款. 本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松.
详见根目录下 [LICENSE](LICENSE) 文件. 详见根目录下 [LICENSE](LICENSE) 文件.
### 第三方代码 ### 第三方代码

View File

@@ -16,7 +16,7 @@ X射线衍射 -> 复杂分子, 分析键长键角
可以在光照下和卤素单质发生取代(卤素单质光照下生成自由基 发生自由基取代反应) 可以在光照下和卤素单质发生取代(卤素单质光照下生成自由基 发生自由基取代反应)
可以高温分解为小的烷烃和烯烃 可以高温分解为小的烷烃和烯烃
不和酸性高锰酸钾, 酸碱反应, 不和Br2(CCl4)反应 不和酸性高锰酸钾, 酸碱反应, 不和Br2(CCl4)反应
除 CH3Br 为气体外多溴代物为液体("油状液滴"), CBr4是固体 除 CH3Br 为气体外, 多溴代物为液体("油状液滴"), CBr4是固体
光照用紫外灯或强日光(太阳光直射),不能用普通白炽灯 uv含量低 光照用紫外灯或强日光(太阳光直射),不能用普通白炽灯 uv含量低

View File

@@ -24,7 +24,7 @@ logger.debug(f"工作目录: {workdir}")
(workdir / "data" / "config").mkdir(parents=True, exist_ok=True) (workdir / "data" / "config").mkdir(parents=True, exist_ok=True)
config_var: ContextVar[ConfigDict].get = ContextVar( config_var: ContextVar[ConfigDict] = ContextVar(
"config_var", "config_var",
default=ConfigDict(workdir / "data" / "config"), default=ConfigDict(workdir / "data" / "config"),
) )

View File

@@ -85,7 +85,7 @@ class FavoriteManagerScreen(Screen):
def _encode_favorite_key(self, repo_path: str, ident: str) -> str: def _encode_favorite_key(self, repo_path: str, ident: str) -> str:
"""编码仓库路径和标识符为安全的按钮 ID 部分""" """编码仓库路径和标识符为安全的按钮 ID 部分"""
# 使用 \x00 分隔两部分然后进行 base64 编码 # 使用 \x00 分隔两部分, 然后进行 base64 编码
combined = f"{repo_path}\x00{ident}" combined = f"{repo_path}\x00{ident}"
encoded = base64.urlsafe_b64encode(combined.encode()).decode() encoded = base64.urlsafe_b64encode(combined.encode()).decode()
# 去掉填充的等号 # 去掉填充的等号
@@ -114,7 +114,7 @@ class FavoriteManagerScreen(Screen):
# 创建安全的按钮 ID # 创建安全的按钮 ID
button_key = self._encode_favorite_key(fav.repo_path, fav.ident) button_key = self._encode_favorite_key(fav.repo_path, fav.ident)
# 创建列表项包含移除按钮 # 创建列表项, 包含移除按钮
container = Horizontal( container = Horizontal(
Label(display_text, classes="favorite-content"), Label(display_text, classes="favorite-content"),
Button("移除", id=f"remove-{button_key}", variant="error", flat=True, classes="favorite-item-btn"), Button("移除", id=f"remove-{button_key}", variant="error", flat=True, classes="favorite-item-btn"),

View File

@@ -237,14 +237,14 @@ class MemScreen(Screen):
"""获取仓库相对路径(相对于 data/repo""" """获取仓库相对路径(相对于 data/repo"""
if self.repo is None: if self.repo is None:
return "" return ""
# self.repo.source 是 Path 对象指向仓库目录 # self.repo.source 是 Path 对象, 指向仓库目录
repo_full_path = self.repo.source repo_full_path = self.repo.source
data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo" data_repo_path = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
try: try:
rel_path = repo_full_path.relative_to(data_repo_path) rel_path = repo_full_path.relative_to(data_repo_path)
return str(rel_path) return str(rel_path)
except ValueError: except ValueError:
# 如果不在 data/repo 下则返回完整路径(字符串形式) # 如果不在 data/repo 下, 则返回完整路径(字符串形式)
return str(repo_full_path) return str(repo_full_path)
def _is_current_atom_favorited(self) -> bool: def _is_current_atom_favorited(self) -> bool:

View File

@@ -14,7 +14,7 @@ import heurams.kernel.particles as pt
import heurams.services.hasher as hasher import heurams.services.hasher as hasher
from heurams.context import * from heurams.context import *
# 兼容性缓存路径:优先使用 paths.cache否则使用 data/cache # 兼容性缓存路径:优先使用 paths.cache, 否则使用 data/cache
paths = config_var.get()["global"]["paths"] paths = config_var.get()["global"]["paths"]
cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice" cache_dir = pathlib.Path(paths.get("cache", paths["data"] + "/cache")) / "voice"
@@ -55,7 +55,7 @@ class PrecachingScreen(Screen):
self.precache_worker = None self.precache_worker = None
self.cancel_flag = 0 self.cancel_flag = 0
self.desc = desc self.desc = desc
# 不再需要缓存配置保留配置读取以兼容 # 不再需要缓存配置, 保留配置读取以兼容
self.cache_stats = { self.cache_stats = {
"total_size": 0, "total_size": 0,
"file_count": 0, "file_count": 0,

View File

@@ -130,7 +130,7 @@ class SyncScreen(Screen):
log_widget = self.query_one("#log_output") log_widget = self.query_one("#log_output")
log_widget.update("\n".join(self.log_messages)) # type: ignore log_widget.update("\n".join(self.log_messages)) # type: ignore
except Exception: except Exception:
pass # 如果组件未就绪忽略错误 pass # 如果组件未就绪, 忽略错误
def on_button_pressed(self, event: Button.Pressed) -> None: def on_button_pressed(self, event: Button.Pressed) -> None:
"""处理按钮点击事件""" """处理按钮点击事件"""
@@ -150,7 +150,7 @@ class SyncScreen(Screen):
def test_connection(self): def test_connection(self):
"""测试 WebDAV 服务器连接""" """测试 WebDAV 服务器连接"""
if not self.sync_service: if not self.sync_service:
self.log_message("同步服务未初始化请检查配置", is_error=True) self.log_message("同步服务未初始化, 请检查配置", is_error=True)
self.update_status("❌ 同步服务未初始化") self.update_status("❌ 同步服务未初始化")
return return
@@ -172,7 +172,7 @@ class SyncScreen(Screen):
def start_sync(self): def start_sync(self):
"""开始同步""" """开始同步"""
if not self.sync_service: if not self.sync_service:
self.log_message("同步服务未初始化无法开始同步", is_error=True) self.log_message("同步服务未初始化, 无法开始同步", is_error=True)
return return
if self.is_syncing: if self.is_syncing:

View File

@@ -26,13 +26,13 @@ _SCHEDULER_STATE_FILE = pathlib.Path(
def _get_global_scheduler(): def _get_global_scheduler():
"""获取全局 FSRS Scheduler 实例从文件加载或创建新的""" """获取全局 FSRS Scheduler 实例, 从文件加载或创建新的"""
if os.path.exists(_SCHEDULER_STATE_FILE): if os.path.exists(_SCHEDULER_STATE_FILE):
try: try:
with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f: with open(_SCHEDULER_STATE_FILE, "r", encoding="utf-8") as f:
return Scheduler.from_json(f.read()) return Scheduler.from_json(f.read())
except Exception: except Exception:
logger.warning("FSRS Scheduler 状态文件加载失败创建新实例") logger.warning("FSRS Scheduler 状态文件加载失败, 创建新实例")
return Scheduler() return Scheduler()
@@ -80,7 +80,7 @@ class FSRSAlgorithm(BaseAlgorithm):
# FSRS 特有字段 # FSRS 特有字段
fsrs_state: int # State 枚举值: 1=Learning, 2=Review, 3=Relearning fsrs_state: int # State 枚举值: 1=Learning, 2=Review, 3=Relearning
fsrs_step: int # 当前学习步进索引, -1 表示 None (Review 状态) fsrs_step: int # 当前学习步进索引, -1 表示 None (Review 状态)
fsrs_stability: float # 稳定性(秒)0.0 表示尚未计算 fsrs_stability: float # 稳定性(秒), 0.0 表示尚未计算
fsrs_difficulty: float # 难度 [1.0, 10.0], 0.0 表示尚未计算 fsrs_difficulty: float # 难度 [1.0, 10.0], 0.0 表示尚未计算
# 标准 BaseAlgorithm 兼容字段 # 标准 BaseAlgorithm 兼容字段
real_rept: int real_rept: int
@@ -194,7 +194,7 @@ class FSRSAlgorithm(BaseAlgorithm):
if is_new_activation: if is_new_activation:
card = Card() card = Card()
logger.debug("新激活创建新 Card") logger.debug("新激活, 创建新 Card")
else: else:
card = cls._algodata_to_card(algodata) card = cls._algodata_to_card(algodata)

View File

@@ -611,7 +611,7 @@ def _get_global_sm():
with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f: with open(_GLOBAL_STATE_FILE, "r", encoding="utf-8") as f:
return SM.load(json.load(f)) return SM.load(json.load(f))
except Exception: except Exception:
logger.warning("SM-15M 全局状态文件加载失败创建新实例") logger.warning("SM-15M 全局状态文件加载失败, 创建新实例")
sm = SM() sm = SM()
_save_global_sm(sm) _save_global_sm(sm)
return sm return sm
@@ -646,7 +646,7 @@ class SM15MAlgorithm(BaseAlgorithm):
# 毫秒精度(子日排程) # 毫秒精度(子日排程)
last_date_ms: int last_date_ms: int
next_date_ms: int next_date_ms: int
# BaseAlgorithm 兼容(天精度向后兼容) # BaseAlgorithm 兼容(天精度, 向后兼容)
real_rept: int real_rept: int
rept: int rept: int
interval: int interval: int
@@ -694,7 +694,7 @@ class SM15MAlgorithm(BaseAlgorithm):
opt_days * 24 * 60 * 60 * 1000 if opt_days > 0 else sm.interval_base opt_days * 24 * 60 * 60 * 1000 if opt_days > 0 else sm.interval_base
) )
# 毫秒精度优先退化至天精度 # 毫秒精度优先, 退化至天精度
last_date_ms = data.get("last_date_ms", 0) last_date_ms = data.get("last_date_ms", 0)
if last_date_ms: if last_date_ms:
item.previous_date = datetime.datetime(1970, 1, 1) + datetime.timedelta( item.previous_date = datetime.datetime(1970, 1, 1) + datetime.timedelta(

View File

@@ -26,14 +26,14 @@ class BaseLLM:
"""发送聊天消息并获取响应 """发送聊天消息并获取响应
Args: Args:
messages: 消息列表每个消息为 {"role": "user"|"assistant"|"system", "content": "消息内容"} messages: 消息列表, 每个消息为 {"role": "user"|"assistant"|"system", "content": "消息内容"}
**kwargs: 其他参数如 temperature, max_tokens 等 **kwargs: 其他参数, 如 temperature, max_tokens 等
Returns: Returns:
模型返回的文本响应 模型返回的文本响应
""" """
logger.debug("BaseLLM.chat: messages=%d, kwargs=%s", len(messages), kwargs) logger.debug("BaseLLM.chat: messages=%d, kwargs=%s", len(messages), kwargs)
logger.warning("BaseLLM.chat 是基类方法未实现具体功能") logger.warning("BaseLLM.chat 是基类方法, 未实现具体功能")
await asyncio.sleep(0) # 避免未使用异步的警告 await asyncio.sleep(0) # 避免未使用异步的警告
return "BaseLLM 未实现具体功能" return "BaseLLM 未实现具体功能"
@@ -50,6 +50,6 @@ class BaseLLM:
logger.debug( logger.debug(
"BaseLLM.chat_stream: messages=%d, kwargs=%s", len(messages), kwargs "BaseLLM.chat_stream: messages=%d, kwargs=%s", len(messages), kwargs
) )
logger.warning("BaseLLM.chat_stream 是基类方法未实现具体功能") logger.warning("BaseLLM.chat_stream 是基类方法, 未实现具体功能")
await asyncio.sleep(0) await asyncio.sleep(0)
yield "BaseLLM 未实现流式功能" yield "BaseLLM 未实现流式功能"

View File

@@ -27,8 +27,8 @@ class OpenAILLM(BaseLLM):
try: try:
from openai import AsyncOpenAI from openai import AsyncOpenAI
except ImportError: except ImportError:
logger.error("未安装 openai 库请运行: pip install openai") logger.error("未安装 openai 库, 请运行: pip install openai")
raise ImportError("未安装 openai 库请运行: pip install openai") raise ImportError("未安装 openai 库, 请运行: pip install openai")
self._client = AsyncOpenAI( self._client = AsyncOpenAI(
api_key=self.api_key if self.api_key else None, api_key=self.api_key if self.api_key else None,
@@ -49,7 +49,7 @@ class OpenAILLM(BaseLLM):
"max_tokens": kwargs.get("max_tokens", 1000), "max_tokens": kwargs.get("max_tokens", 1000),
} }
# 合并参数优先使用传入的 kwargs # 合并参数, 优先使用传入的 kwargs
request_kwargs = {**default_kwargs, **kwargs} request_kwargs = {**default_kwargs, **kwargs}
request_kwargs["messages"] = messages request_kwargs["messages"] = messages

View File

@@ -10,7 +10,7 @@ from heurams.services.exceptions import WTFException
logger = get_logger(__name__) logger = get_logger(__name__)
class ConfigDict(UserDict): # 舒服了 class ConfigDict(UserDict):
_instances = {} # 必须使用单例模式, 不然有严重的多实例导致的配置无法持久化问题 _instances = {} # 必须使用单例模式, 不然有严重的多实例导致的配置无法持久化问题
def __new__(cls, config_path: pathlib.Path, dict=None): def __new__(cls, config_path: pathlib.Path, dict=None):

View File

@@ -73,7 +73,7 @@ class FavoriteManager:
with open(self._file_path, "r", encoding="utf-8") as f: with open(self._file_path, "r", encoding="utf-8") as f:
data = json.load(f) data = json.load(f)
self._favorites = [FavoriteItem.from_dict(item) for item in data] self._favorites = [FavoriteItem.from_dict(item) for item in data]
logger.debug("收藏列表加载成功%d", len(self._favorites)) logger.debug("收藏列表加载成功, %d", len(self._favorites))
except Exception as e: except Exception as e:
logger.error("加载收藏列表失败: %s", e) logger.error("加载收藏列表失败: %s", e)
self._favorites = [] self._favorites = []
@@ -86,7 +86,7 @@ class FavoriteManager:
data = [item.to_dict() for item in self._favorites] data = [item.to_dict() for item in self._favorites]
with open(self._file_path, "w", encoding="utf-8") as f: with open(self._file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
logger.debug("收藏列表保存成功%d", len(self._favorites)) logger.debug("收藏列表保存成功, %d", len(self._favorites))
except Exception as e: except Exception as e:
logger.error("保存收藏列表失败: %s", e) logger.error("保存收藏列表失败: %s", e)

View File

@@ -6,9 +6,9 @@
转换规则: 转换规则:
1. `ident` 列用作 TOML 的 section 标题(`[ident]`) 1. `ident` 列用作 TOML 的 section 标题(`[ident]`)
2. 若某行的 `ident` 为空则自动按顺序生成标识符例如 `idx_1`、`idx_2` 等 2. 若某行的 `ident` 为空, 则自动按顺序生成标识符, 例如 `idx_1`、`idx_2` 等
3. 所有其他列(除 `ident` 外)都作为该 section 下的键值对 3. 所有其他列(除 `ident` 外)都作为该 section 下的键值对
4. 所有列都是可选的但 `ident` 为空时会自动生成 4. 所有列都是可选的, 但 `ident` 为空时会自动生成
示例 CSV: 示例 CSV:
```csv ```csv
@@ -61,8 +61,8 @@ meaning = "狗发出的声音"
补充说明: 补充说明:
- 自动生成的标识符使用 `idx_` 前缀加数字序列 - 自动生成的标识符使用 `idx_` 前缀加数字序列
- 生成序列基于原始 CSV 中 `ident` 为空的行出现的顺序 - 生成序列基于原始 CSV 中 `ident` 为空的行出现的顺序
- 所有值都保留为字符串类型符合 TOML 字符串格式要求 - 所有值都保留为字符串类型, 符合 TOML 字符串格式要求
- 如果 CSV 包含更多列它们也会以相同方式转换为键值对 - 如果 CSV 包含更多列, 它们也会以相同方式转换为键值对
- 支持 `-r` 参数指定随机种子来打乱 section 顺序 - 支持 `-r` 参数指定随机种子来打乱 section 顺序
""" """
@@ -79,8 +79,8 @@ def csv_to_toml(csv_path, toml_path=None, random_seed=None):
Args: Args:
csv_path (str): 输入CSV文件路径 csv_path (str): 输入CSV文件路径
toml_path (str): 输出TOML文件路径默认为相同目录下同名文件 toml_path (str): 输出TOML文件路径, 默认为相同目录下同名文件
random_seed (int): 随机种子用于打乱section顺序None表示不打乱 random_seed (int): 随机种子, 用于打乱section顺序, None表示不打乱
""" """
# 检查CSV文件是否存在 # 检查CSV文件是否存在
csv_file = Path(csv_path) csv_file = Path(csv_path)
@@ -108,7 +108,7 @@ def csv_to_toml(csv_path, toml_path=None, random_seed=None):
print("错误: CSV文件为空或格式不正确") print("错误: CSV文件为空或格式不正确")
sys.exit(1) sys.exit(1)
# 如果指定了随机种子设置随机种子并打乱行顺序 # 如果指定了随机种子, 设置随机种子并打乱行顺序
if random_seed is not None: if random_seed is not None:
random.seed(random_seed) random.seed(random_seed)
random.shuffle(rows) random.shuffle(rows)
@@ -119,7 +119,7 @@ def csv_to_toml(csv_path, toml_path=None, random_seed=None):
idx_counter = 1 idx_counter = 1
for row in rows: for row in rows:
# 处理ident列为空时生成自动标识符 # 处理ident列, 为空时生成自动标识符
ident = row.get("ident", "").strip() ident = row.get("ident", "").strip()
if not ident: if not ident:
ident = f"idx_{idx_counter}" ident = f"idx_{idx_counter}"
@@ -155,7 +155,7 @@ def csv_to_toml(csv_path, toml_path=None, random_seed=None):
def main(): def main():
"""主函数""" """主函数"""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="将CSV文件转换为TOML格式支持随机打乱section顺序", description="将CSV文件转换为TOML格式, 支持随机打乱section顺序",
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
示例: 示例:
@@ -168,10 +168,10 @@ def main():
parser.add_argument("csv_path", help="输入的CSV文件路径") parser.add_argument("csv_path", help="输入的CSV文件路径")
parser.add_argument( parser.add_argument(
"toml_path", nargs="?", help="输出的TOML文件路径默认为CSV同名文件" "toml_path", nargs="?", help="输出的TOML文件路径, 默认为CSV同名文件"
) )
parser.add_argument( parser.add_argument(
"-r", "--random-seed", type=int, help="随机种子用于打乱TOML section的顺序" "-r", "--random-seed", type=int, help="随机种子, 用于打乱TOML section的顺序"
) )
args = parser.parse_args() args = parser.parse_args()