Compare commits
18 Commits
5b52e4b3ee
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a3fa75e13 | |||
| b31c045aa5 | |||
| 3d113f2eaa | |||
| b0625ef636 | |||
| dc8fa36a28 | |||
| 2918662222 | |||
| 60caee0f67 | |||
| 4ba164e2ab | |||
| 2735465629 | |||
| be9e79b576 | |||
| 470a7383bf | |||
| ceaebf2c54 | |||
| 54daa4128d | |||
| d22966b34d | |||
| 0889bfa1c3 | |||
| 92796451d1 | |||
| 66870c4987 | |||
| 477fa972eb |
+10
-8
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
欢迎支持此项目!
|
欢迎支持此项目!
|
||||||
|
|
||||||
目前, 项目仓库主服务器为<a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">作者的 Gitea 实例</a>, 以保证可用性并同时接受来自多个社区的协作, 并在 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a> 和 <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 设置了镜像同步.
|
目前, 项目仓库主服务器为<a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">作者的 Gitea 实例</a>, 它负责管理同步, 保证可用性并同时接受来自多个社区的协作, 并在 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 和 <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a> 设置了镜像同步.
|
||||||
|
|
||||||
这丝毫不影响项目接受来自 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a> 和 <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 的 PR, 在 GitHub 与 KDE Invent 所接受的 PR 会保留贡献者标识并按原样同步回所有平台, 欢迎在任意平台为项目做出贡献.
|
这丝毫不影响项目接受来自 <a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">KDE Invent</a> 和 <a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer">Gitee</a> 的 PR, 在 GitHub, KDE Invent 和 Gitee 所接受的 PR 会保留贡献者标识并按原样同步回所有平台, 欢迎在任意平台为项目做出贡献.
|
||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
|
> 我们已经开始着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
分支划分:
|
分支划分:
|
||||||
|
|
||||||
- `dev` 分支(也是默认分支): 主线开发分支, 自身仅用于非重构的问题修复和整合功能分支, 拉取请求在该分支合并
|
- `dev` 分支(仓库默认分支): 主线开发分支, 自身仅用于非重构的问题修复和整合功能分支, 拉取请求在该分支合并
|
||||||
- `master` 分支: 稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `master` 上
|
- `master` 分支: 主线稳定版本, 仅当稳定版本释出或修补版本时将 `dev` 合并到 `master` 上
|
||||||
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` 或 `fix/描述` 或 `refactor/v版本号`
|
- 功能与重构分支: 从 `dev` 分支创建, 命名格式为 `feature/描述` 或 `fix/描述` 或 `refactor/描述` 或 `next/版本号`
|
||||||
- 功能与重构分支应先合并至 `dev`, 再合并至 `master`
|
- 功能与重构分支应先合并至 `dev`, 再合并至 `master`
|
||||||
|
|
||||||
代码格式化:
|
代码格式化:
|
||||||
@@ -54,6 +54,10 @@
|
|||||||
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
|
- 为了一致性和可追溯性, 项目自 v0.4.0 重构后重新初始化仓库起就禁止使用 Fast-forward 合并
|
||||||
- 可以设置 `git config merge.ff false`
|
- 可以设置 `git config merge.ff false`
|
||||||
|
|
||||||
|
提交署名方式:
|
||||||
|
由于 KDE Invent 设施的奇怪 git hook, commit 的 Author 字段需要看起来像个真名(例如 Wang Zhiyu 不能写为 wangzhiyu, 否则 KDE Invent 的 hook 会拒绝 push),
|
||||||
|
所以请确保您的 git 配置使用了类似正式姓名的格式 (例如 git config user.name "Li Hua", 也即中间需要空格), 不一定要真实姓名, 邮箱无要求, 也可以将单名重复使用两次 (例如 Thura Thura) 以通过检测.
|
||||||
|
|
||||||
## 设置开发环境
|
## 设置开发环境
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -67,9 +71,7 @@ python3 -m pip install uv
|
|||||||
|
|
||||||
uv sync --all-extras # 同步开发运行环境
|
uv sync --all-extras # 同步开发运行环境
|
||||||
|
|
||||||
uv run heurams # 验证包安装
|
uv run heurams
|
||||||
|
|
||||||
uv run heurams-tui # 启动 TUI
|
|
||||||
|
|
||||||
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)上运行)
|
# 如果决定使用原生 python 环境 (不推荐, 但我们保留了这种方式以便在不便支持 uv 与硬链接的环境和文件系统(例如 termux)上运行)
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
|
|
||||||
## 黑乎乎的这个界面我怎么用?
|
## 黑乎乎的这个界面我怎么用?
|
||||||
|
|
||||||
|
首先, 如果您只是想要一个亮色模式, 可以直接按下 `d` 键或点击 "d 主题" 按钮, 这会让您的界面变得白乎乎的(
|
||||||
|
|
||||||
得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe` 和 `cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的.
|
得益于微软几十年对用户进行的"命令行即落后"教育, 以及 `conhost.exe` 和 `cmd.exe` 的糟糕体验, 您对终端用户界面感到不适应是完全正常的.
|
||||||
|
|
||||||
但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些.
|
但实际上, 虽然看起来像老式电脑屏幕, Textual 和终端标准其实比您想象得要现代一些.
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
## 特性
|
||||||
|
|
||||||
|
### 间隔重复调度器
|
||||||
|
|
||||||
|
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的, 但不会导致遗忘的间隔**.
|
||||||
|
|
||||||
|
- 软件开箱即用, 无需多加配置即可使用默认的 `SM-2` 算法进行学习
|
||||||
|
- 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
|
||||||
|
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
|
||||||
|
- 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
|
||||||
|
- 还内置了 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
|
||||||
|
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
|
||||||
|
- 得益于项目的模块化架构与单元集结构设计, 同一个单元集可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
|
||||||
|
|
||||||
|
### 多模态学习进程
|
||||||
|
|
||||||
|
与 Anki 的 SQLite `.apkg` 包不同, 我们坚持使用人类可读的文件夹组织单元集, 这带来了若干好处, 包括:
|
||||||
|
|
||||||
|
- 人类可读: 您可以用任意工具, 乃至一个记事本自由修改记忆载荷数据而无需打开软件
|
||||||
|
- 元数据配置: 配置自由度极高, 可以任意组合, 重造, 乃至创造新内容
|
||||||
|
- 测验, 算法与知识互相隔离: 一条知识不再是单一的闪卡, 不仅可以用若干不同的算法规划, 还可以用多种并行的谜题类型测验, 极大地提升学习效果和丰富度. 作为学习者, 您无需担忧概念复杂--仅需从云端下载单元集即可开箱即用上述特性!
|
||||||
|
- 多模态学习
|
||||||
|
- 软件自身集成了文本转语音 (TTS) , 音频与语言模型 (LLM) 模块, 这些功能乃至功能本身都是可插拔, 可扩展, 可切换驱动的, 这为内容创建了极大的丰富度
|
||||||
|
- 软件内置多种谜题类型, 包括选择题 (MCQ), 填空题 (Cloze) 与识别题 (Recognition), 您可在同一单元应用多种, 或是选择性启用
|
||||||
|
- 软件天然支持动态内容生成, 支持宏驱动的模板系统, 根据上下文乃至语言模型动态生成知识点的解析
|
||||||
|
- 在间隔重复研究尚被 SuperMemo 系列独占的时代, Wozniak 就早已表示 "如果不能理解知识, 就无需记忆它". 今天, 我们依然相信理解是记忆的基石
|
||||||
|
- 云同步与分享优化:
|
||||||
|
- 由于记忆数据和单元集文件都是文本文件, 故可进行快速的增量同步而无需完整地上传所有文件, 并且设计天然支持版本控制
|
||||||
|
- 如果您想分享单文件, 软件也支持导出为压缩包或合并成单文本文件以通过纯文本文件形式在 pastebin 等平台分享
|
||||||
|
- 性能提升: 得益于现代且支持分块的文件组织结构, 潜进能在保持高自由度的同时仅使用 python 就能达到敏捷且低占用的用户体验
|
||||||
|
- AI 辅助友好: 想象您有一些 `.apkg` 牌组或一大段教材内容, 您可以方便且高效率地使用 AI 工具以创建可在 HeurAMS 使用的单元集
|
||||||
|
|
||||||
|
### 内置实用用户界面
|
||||||
|
|
||||||
|
尽管不是唯一前端, 但响应式 Textual 框架构建的内置终端用户界面在多种场景下仍具有独特优势:
|
||||||
|
|
||||||
|
- 跨平台, 并支持触屏/鼠标/键盘多操作模式
|
||||||
|
- 与几乎所有现代终端模拟器相容
|
||||||
|
- 对于<a href="https://www.arewesixelyet.com/" target="_blank" rel="noopener noreferrer">支持 sixel 协议的终端模拟器</a>, 可高清显示图像内容
|
||||||
|
- 对于不支持 sixel 协议的终端模拟器, 也支持图片低清的兼容显示模式
|
||||||
|
- 可通过 textual-web 作为服务部署, 并在任意浏览器使用
|
||||||
|
- 简洁直观, 键盘友好, 全功能且高效率的用户界面设计
|
||||||
|
- 易于嵌入: 可在 getty/kmscon 中运行而无需任何桌面图形服务
|
||||||
|
- 资源占用小, 运行流畅, 不拖泥带水
|
||||||
|
- 便于测试与调试程序库
|
||||||
|
|
||||||
|
查看[屏幕截图](SCREENSHOTS.md).
|
||||||
|
|
||||||
|
|
||||||
|
## 包依赖组说明
|
||||||
|
|
||||||
|
由于部分依赖只被少数功能需要, 所以我们把可选依赖分得比较细, 前面提供的命令会安装部分可选依赖, 以下是依赖组列表:
|
||||||
|
|
||||||
|
| 依赖组 | 包含模块 | 说明 |
|
||||||
|
|--------|----------|------|
|
||||||
|
| 构建系统 | hatchling | 构建时安装 |
|
||||||
|
| 最小化安装 | tabulate, toml, transitions, click | 核心驱动程序库, 始终必需 |
|
||||||
|
| interface | textual | 基本用户界面依赖 |
|
||||||
|
| algo-fsrs | fsrs | FSRS 算法模块 |
|
||||||
|
| tts-edgetts | edge-tts | 微软文本转语音 |
|
||||||
|
| llm | llms-py | API 调用 |
|
||||||
|
| audio-playsound | playsound3 | 通用音频模块 |
|
||||||
|
| dev | zmq, pytest, pytest-cov | 开发调试与测试工具 |
|
||||||
|
| basic | [tts-edgetts], [llm], [algo-fsrs] | 适用于用户体验的较轻依赖组(推荐) |
|
||||||
|
| all | 以上所有依赖 | 完整安装组 |
|
||||||
|
|
||||||
|
## 关于此仓库
|
||||||
|
|
||||||
|
此仓库为 "潜进" 的核心程序库在 python 语言下的实现\
|
||||||
|
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
|
||||||
|
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC` 与 `heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
|
||||||
|
|
||||||
|
潜进项目的所有仓库如下:
|
||||||
|
|
||||||
|
| 项目名称 | 状态 | 说明 | 包名 | 技术栈 | 目标平台 |
|
||||||
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
|
| HeurAMS | 开发中<br/>原型可用 | 提供通用核心程序库与基本用户界面 | `heurams` | Python | 标准 Python 环境 |
|
||||||
|
| KiriMemo | 开发中<br/>原型可用 | 基于 KDE 技术的现代跨平台前端 | `org.kde.kirimemo` | C++, Qt6, Kirigami, PyOtherSide | 桌面与移动设备 |
|
||||||
|
| ArkMemo | 开发中 | 基于 ArkUI 的现代跨平台前端 | `top.pluv27.arkmemo` | ArkTS, ArkUI | 移动设备 |
|
||||||
|
| HeurStudio | 计划中 | AI 辅助的单体单元集高级创建与编辑工具 | `top.pluv27.heurstudio` | C++, Qt6, Kirigami, PyOtherSide | 桌面 |
|
||||||
|
| HeurSync | 开发中 | 用户数据同步服务器<br/>集成 Web 前端与排行榜 | `heursync` | Go, SQL | 网页与服务器 |
|
||||||
|
| HeurRepo | 开发中 | 单元集文档源服务器<br/>与单元集分享平台 | `heurrepo` | Go, SQL | 网页与服务器 |
|
||||||
|
|
||||||
|
尽管现在后三样有点画大饼的意思, 但是我们的路线是明了的
|
||||||
@@ -2,123 +2,93 @@
|
|||||||
|
|
||||||
## 概述
|
## 概述
|
||||||
|
|
||||||
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,\
|
"潜进" (HeurAMS: Heuristic Auxiliary Memorizing Scheduler, 启发式记忆辅助调度器) 是一种基于启发式算法与认知科学理论的辅助记忆调度器, 旨在帮助用户更高效地进行记忆工作与学习规划,
|
||||||
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的研究.
|
也是一种开放, 优雅, 易于扩展的间隔重复调度器实验平台, 旨在帮助研究者更高效地进行前沿记忆算法的调查实验与研究.
|
||||||
|
|
||||||
## 关于此仓库
|
[详细介绍](INTRODUCTION.md) [屏幕截图](SCREENSHOTS.md)
|
||||||
|
|
||||||
此仓库为 "潜进" 的核心程序库在 python 语言下的实现\
|
<p align="left">
|
||||||
包含数据模型与框架, 并内置了基于 textual 框架的前端实现 (interface 子模块)\
|
<a href="https://github.com/pluvium27/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/GitHub-fafafa?style=for-the-badge&logo=github&logoColor=181717" alt="GitHub" /></a><a href="https://invent.kde.org/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/KDE_Invent-1D99F3?style=for-the-badge&logo=kde&logoColor=white" alt="KDE Invent" /></a><a href="https://gitee.com/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/Gitee-C71D23?style=for-the-badge&logo=gitee&logoColor=white" alt="Gitee" /></a><a href="https://git.pluv27.top/pluv/HeurAMS" target="_blank" rel="noopener noreferrer"><img src="https://img.shields.io/badge/git.pluv27.top-609926?style=for-the-badge&logo=gitea&logoColor=white" alt="git.pluv27.top" /></a>
|
||||||
除了通过内置前端进行学习外, 开发者也能在 python 环境中导入 `heurams` 库或使用 `RPC` 与 `heurams` 程序库实例通讯, 使用框架构建其他辅助记忆功能前端或其他应用程序
|
</p>
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 我们已经着手于基于 KDE 用户界面框架 `Kirigami` 的现代跨平台前端开发, 称作 "KiriMemo", 包名是 "org.kde.kirimemo", 但其并非 KDE 项目\
|
|
||||||
> 它通过 `PyOtherSide` 直接复用 python 内核, 为 Windows, Linux, macOS, Android, iOS 和 Plasma Mobile 提供现代用户界面\
|
|
||||||
> 如果您善于开发 C++, QML, Qt 与 KDE 框架, 欢迎加入到 KiriMemo 项目的开发
|
|
||||||
|
|
||||||
## 特性
|
|
||||||
|
|
||||||
### 间隔重复调度器
|
|
||||||
|
|
||||||
> 许多出版物都广泛讨论了不同重复间隔对学习效果的影响. 特别是, 间隔效应被认为是一种普遍现象. 间隔效应是指, 如果重复的间隔是分散/稀疏的, 而不是集中重复, 那么学习任务的表现会更好. 因此, 有观点提出, 学习中使用的最佳重复间隔是**最长的, 但不会导致遗忘的间隔**.
|
|
||||||
|
|
||||||
- 软件开箱即用, 无需多加配置即可使用默认的 `SM-2` 算法进行学习
|
|
||||||
- 此外, 算法模块是 "潜进" 内核 (heurams.kernel) 中的一等公民, 内核天然支持插拔各型算法
|
|
||||||
- 无需安装繁杂的插件即可分单元集完成算法快速切换与调优, 研究者可以方便地修改算法模块以便捷地进行研究与测试
|
|
||||||
- 默认使用 `SM-2` 简单间隔重复算法, 此算法亦用作 `Anki` 闪卡记忆软件的默认闪卡调度器
|
|
||||||
- 内置 `NSP-0` 筛选用非间隔重复算法以便快速筛选记忆内容, `FSRS` 先进间隔重复算法作为效率更高的调度器, 与 `SM-15M (移植自 sm.js 项目)` 复杂间隔重复算法(逆向工程)
|
|
||||||
- 算法模块可以标记记忆项目, 也可以动态规划每个记忆单元的记忆间隔时间表, 动态跟踪记忆反馈数据, 以优化长期记忆保留率与稳定性
|
|
||||||
- 得益于项目的模块化架构与单元集结构设计, 一个项目甚至可以与任意种算法共存并互通, 这对研究者及想探索/实验高效率方法的用户极其友好
|
|
||||||
|
|
||||||
### 多模态学习进程
|
|
||||||
|
|
||||||
与 Anki 的 SQLite `.apkg` 包不同, 我们坚持使用人类可读的文件夹组织单元集, 这带来了若干好处, 包括:
|
|
||||||
|
|
||||||
- 人类可读: 您可以用任意工具, 乃至一个记事本自由修改记忆载荷数据而无需打开软件
|
|
||||||
- 元数据配置: 配置自由度极高, 可以任意组合, 重造, 乃至创造新内容
|
|
||||||
- 测验, 算法与知识互相隔离: 一条知识不再是单一的闪卡, 不仅可以用若干不同的算法规划, 还可以用多种并行的谜题类型测验, 极大地提升学习效果和丰富度. 作为学习者, 您无需担忧概念复杂--仅需从云端下载单元集即可开箱即用上述特性!
|
|
||||||
- 多模态学习
|
|
||||||
- 软件自身集成了文本转语音 (TTS) , 音频与语言模型 (LLM) 模块, 这些功能乃至功能本身都是可插拔, 可扩展, 可切换驱动的, 这为内容创建了极大的丰富度
|
|
||||||
- 软件内置多种谜题类型, 包括选择题 (MCQ), 填空题 (Cloze) 与识别题 (Recognition), 您可在同一单元应用多种, 或是选择性启用
|
|
||||||
- 软件天然支持动态内容生成, 支持宏驱动的模板系统, 根据上下文乃至语言模型动态生成知识点的解析
|
|
||||||
- 在间隔重复研究尚被 SuperMemo 系列独占的时代, Wozniak 就早已表示 "如果不能理解知识, 就无需记忆它". 今天, 我们依然相信理解是记忆的基石
|
|
||||||
- 云同步与分享优化:
|
|
||||||
- 由于记忆数据和单元集文件都是文本文件, 故可进行快速的增量同步而无需完整地上传所有文件, 并且设计天然支持版本控制
|
|
||||||
- 如果您想分享单文件, 软件也支持导出为压缩包或合并成单文本文件以通过纯文本文件形式在 pastebin 等平台分享
|
|
||||||
- 性能提升: 得益于现代且支持分块的文件组织结构, 潜进能在保持高自由度的同时仅使用 python 就能达到敏捷且低占用的用户体验
|
|
||||||
- AI 辅助友好: 想象您有一些 .apkg 牌组或一大段教材内容, 您可以方便且高效率地使用 AI 工具创建可在 HeurAMS 使用的单元集
|
|
||||||
|
|
||||||
### 内置实用用户界面
|
|
||||||
|
|
||||||
尽管不是唯一前端, 但响应式 Textual 框架构建的内置终端用户界面在多种场景下仍具有独特优势:
|
|
||||||
|
|
||||||
- 跨平台, 并支持触屏/鼠标/键盘多操作模式
|
|
||||||
- 与几乎所有现代终端模拟器相容
|
|
||||||
- 对于<a href="https://www.arewesixelyet.com/" target="_blank" rel="noopener noreferrer">支持 sixel 协议的终端模拟器</a>, 可高清显示图像内容
|
|
||||||
- 对于不支持 sixel 协议的终端模拟器, 也支持图片低清的兼容显示模式
|
|
||||||
- 可通过 textual-web 作为服务部署, 并在任意浏览器使用
|
|
||||||
- 简洁直观, 键盘友好, 全功能且高效率的用户界面设计
|
|
||||||
- 易于嵌入: 可在 getty/kmscon 中运行而无需任何桌面图形服务
|
|
||||||
- 资源占用小, 运行流畅, 不拖泥带水
|
|
||||||
- 便于测试与调试程序库
|
|
||||||
|
|
||||||
查看[屏幕截图](SCREENSHOTS.md).
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
### 从包管理器安装
|
### 安装软件
|
||||||
|
|
||||||
潜进 (包名是 `heurams`) 处于早期开发考虑, 尚未上架 PyPI, 但您可以用我们的基础设施安装稳定版和开发版本.
|
#### 从包管理器安装
|
||||||
|
|
||||||
#### 稳定版本
|
潜进 (包名是 `heurams`) 处于早期开发考虑, 尚未上架 PyPI.
|
||||||
|
但可以用 pip 从仓库安装稳定版和开发版本, 这要求设备上安装了 python 环境 (建议 3.12.13 及之后版本).
|
||||||
|
|
||||||
安装适用于用户体验的可选依赖(推荐):
|
从稳定的 `master` 分支安装, 并安装适用于用户体验的可选依赖(推荐):
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m pip install heurams[basic] -i https://pypi.pluv27.top/root/stable/+simple/
|
pip install --upgrade 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
|
||||||
```
|
```
|
||||||
|
|
||||||
安装适用于一般计算机的通用音频模块(基于 playsound3):\
|
从较前沿, 大致稳定的 `dev` 分支安装, 并安装适用于用户体验的可选依赖(如果您追求较前沿的改进):
|
||||||
|
|
||||||
|
```
|
||||||
|
pip install --force-reinstall --no-deps 'heurams[basic] @ https://git.pluv27.top/pluv/HeurAMS/archive/dev.zip'
|
||||||
|
```
|
||||||
|
|
||||||
|
安装适用于一般计算机的通用音频模块 (基于 playsound3):\
|
||||||
(此项不适用于 termux 环境, termux 的音频支持是内建的)
|
(此项不适用于 termux 环境, termux 的音频支持是内建的)
|
||||||
|
|
||||||
```
|
```
|
||||||
python -m pip install heurams[audio-playsound] -i https://pypi.pluv27.top/root/stable/+simple/
|
pip install --upgrade 'heurams[audio-playsound] @ https://git.pluv27.top/pluv/HeurAMS/archive/master.zip'
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 开发版本
|
> 您也可以从 `refactor/...` 等特定分支安装以测试某项更改
|
||||||
|
|
||||||
> [!CAUTION]
|
[依赖分组说明](INTRODUCTION.md#包依赖组说明)
|
||||||
> 对于部分 Linux 发行版和 Android Termux 用户:\
|
|
||||||
> 您需要先行安装 `cmake` 和 `libzmq` 才能正确安装项目的 `zmq` 依赖.\
|
|
||||||
> 例如在 termux 上先运行 `pkg install cmake clang libzmq`.\
|
|
||||||
> 项目功能本身不依赖它, 但需要该依赖用于启动可选的调试服务器.
|
|
||||||
|
|
||||||
安装全部可选依赖(推荐):
|
#### 从源码安装
|
||||||
|
|
||||||
```
|
我们提供原生 python 和 uv 两种源码安装方式.\
|
||||||
python -m pip install heurams[all] -i https://pypi.pluv27.top/root/dev/+simple/
|
详见[贡献指南 - 设置开发环境](CONTRIBUTING.md#设置开发环境).
|
||||||
|
|
||||||
|
### 使用软件
|
||||||
|
|
||||||
|
在终端中运行 `heurams`, 您会看到一系列帮助信息, 例如:
|
||||||
|
|
||||||
|
```plain
|
||||||
|
~ $ heurams
|
||||||
|
Usage: heurams [OPTIONS] COMMAND [ARGS]...
|
||||||
|
|
||||||
|
HeurAMS 0.5.1 - 启发式辅助记忆调度器
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-v, --version Show the version and exit.
|
||||||
|
-h, --help Show this message and exit.
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
help 显示此帮助信息
|
||||||
|
tui 启动内置基本用户界面 (TUI)
|
||||||
|
version 输出版本信息
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 依赖组说明
|
可以通过键入 `heurams tui` 启动基本用户界面, 例如:
|
||||||
|
|
||||||
由于部分依赖只被少数功能需要, 所以我们把可选依赖分得比较细, 前面提供的命令会安装部分可选依赖, 以下是依赖组列表:
|
```plain
|
||||||
|
~ $ heurams tui
|
||||||
|
欢迎使用基本用户界面!
|
||||||
|
加载配置与上下文... 已完成! (耗时: 33ms)
|
||||||
|
加载用户界面框架... 已完成! (耗时: 169ms)
|
||||||
|
加载用户界面布局... 已完成! (耗时: 165ms)
|
||||||
|
组件目录: <软件包所在目录>
|
||||||
|
工作目录: <运行目录, 将在此目录下建立 ./data 文件夹>
|
||||||
|
前置工作共计耗时: 142ms
|
||||||
|
|
||||||
| 依赖组 | 包含模块 | 说明 |
|
(此时您的终端将转为呈现美观的 TUI 基本用户界面)
|
||||||
|--------|----------|------|
|
```
|
||||||
| 最小化安装 | tabulate, toml, transitions | 核心驱动程序库, 始终必需 |
|
|
||||||
| interface | textual, psutil | 基本用户界面依赖 |
|
|
||||||
| algo-fsrs | fsrs | FSRS 算法模块 |
|
|
||||||
| tts-edgetts | edge-tts | 微软文本转语音 |
|
|
||||||
| llm | llms-py | API 调用 |
|
|
||||||
| audio-playsound | playsound3 | 通用音频模块 |
|
|
||||||
| dev | zmq, pytest, pytest-cov | 开发调试与测试工具 |
|
|
||||||
| basic | [tts-edgetts], [llm], [algo-fsrs] | 适用于用户体验的较轻依赖组(推荐) |
|
|
||||||
| all | 以上所有依赖 | 完整安装组 |
|
|
||||||
|
|
||||||
### 从源码安装
|
通过键入 `heurams -v` 查看版本:
|
||||||
|
|
||||||
我们提供原生 python 和 uv 两种安装方式.\
|
```
|
||||||
详见[贡献指南](CONTRIBUTING.md).
|
~ $ heurams -v
|
||||||
|
HeurAMS 0.5.1 stable (fulcrum/支点), Linux
|
||||||
|
```
|
||||||
|
|
||||||
## 常见问题 (FAQ)
|
## 常见问题 (FAQ)
|
||||||
|
|
||||||
@@ -152,19 +122,19 @@ HeurAMS 项目标识如下, 文件(位图和矢量图)位于 `./src/heurams/asse
|
|||||||
|
|
||||||
### 项目本身
|
### 项目本身
|
||||||
|
|
||||||
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更松.
|
本项目基于 AGPL-3.0 许可证开放源代码, 并有一个豁免本机 API 调用的附加条款, 较标准 AGPL-3.0 更宽松.
|
||||||
|
|
||||||
详见根目录下 [LICENSE](LICENSE) 文件.
|
详见根目录下 [LICENSE](LICENSE) 文件.
|
||||||
|
|
||||||
### 第三方代码
|
### 第三方代码
|
||||||
|
|
||||||
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置间接使用了以下第三方代码(可能有修改):
|
项目在 `src/heurams/vendor/` 目录下嵌入或在其他位置直接使用了以下第三方代码或其衍生作品 (可能有修改):
|
||||||
|
|
||||||
#### SM.js (slaypni)
|
#### SM.js
|
||||||
|
|
||||||
- 上游版本: commit `6e3bb4afaf484426deb4a9fa3bcffe42ac066b45` (2015年2月4日上游已停止维护)
|
- 上游版本: commit `6e3bb4a` (2015年2月4日上游已停止维护)
|
||||||
- 引用方式: 将 coffeescript 重写为 python 并间接引用, 数学原理一致; 并对重写后代码进行逻辑, 性能与标准化 API 改进
|
- 引用方式: 将 coffeescript 重写为 python 并间接引用, 数学原理一致; 并对重写后代码进行逻辑, 性能与标准化 API 改进
|
||||||
- 位置: `src/heurams/kernel/algorithms/sm15m*.py`
|
- 位置: `src/heurams/kernel/algorithms/sm15m.py`
|
||||||
- 原项目: [SM.js](https://github.com/slaypni/SM-15)
|
- 原项目: [SM.js](https://github.com/slaypni/SM-15)
|
||||||
- 原版权: Copyright (c) 2014 Kazuaki Tanida
|
- 原版权: Copyright (c) 2014 Kazuaki Tanida
|
||||||
- 原许可证: MIT License
|
- 原许可证: MIT License
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
- Textual 基本用户界面 (heurams.interface): 基于 Python Textual 框架构建的程序库内置跨平台 TUI 界面, 支持触屏、鼠标、键盘多操作模式, 是当前开箱即用的默认前端.
|
- Textual 基本用户界面 (heurams.interface): 基于 Python Textual 框架构建的程序库内置跨平台 TUI 界面, 支持触屏、鼠标、键盘多操作模式, 是当前开箱即用的默认前端.
|
||||||
- KiriMemo (org.kde.kirimemo): 基于 KDE Kirigami 框架的现代跨平台前端, 使用 C++ 和 QML 构建, 通过 `PyOtherSide` 直接复用 Python 内核, 为多种平台提供原生体验 (尚未稳定).
|
- KiriMemo (org.kde.kirimemo): 基于 KDE Kirigami 框架的现代跨平台前端, 使用 C++ 和 QML 构建, 通过 `PyOtherSide` 直接复用 Python 内核, 为多种平台提供原生体验 (尚未稳定).
|
||||||
|
<!--- ArkMemo (top.pluv27.arkmemo): 基于 ArkUI 的现代移动设备前端, 使用 ArkTS 构建, 通过 API 调用 Python 内核, 为 Android, HarmonyOS, iOS 平台提供原生体验 (尚未稳定)-->
|
||||||
|
|
||||||
欢迎为现有前端贡献代码, 或开发您自己的前端.\
|
欢迎为现有前端贡献代码, 或开发您自己的前端.\
|
||||||
详见[贡献指南](CONTRIBUTING.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
|
详见[贡献指南](CONTRIBUTING.md#%E6%96%B0%E7%9A%84%E7%94%A8%E6%88%B7%E7%95%8C%E9%9D%A2%E5%89%8D%E7%AB%AF).
|
||||||
|
|||||||
+5
-5
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "heurams"
|
name = "heurams"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
authors = [{ name = "pluvium27", email = "pluvium27@outlook.com" }]
|
authors = [{ name = "Wang Zhiyu", email = "pluvium27@outlook.com" }]
|
||||||
description = "Heuristic Auxiliary Memory Scheduler"
|
description = "Heuristic Auxiliary Memory Scheduler"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
@@ -13,6 +13,7 @@ license = "AGPL-3.0-or-later"
|
|||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
|
|
||||||
dependencies = [ # 这些依赖只能驱动 kernel 程序库
|
dependencies = [ # 这些依赖只能驱动 kernel 程序库
|
||||||
|
"click>=8.3.3",
|
||||||
"tabulate>=0.10.0",
|
"tabulate>=0.10.0",
|
||||||
"toml>=0.10.2",
|
"toml>=0.10.2",
|
||||||
"transitions>=0.9.3",
|
"transitions>=0.9.3",
|
||||||
@@ -60,7 +61,6 @@ default = true
|
|||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
heurams = "heurams.__main__:main"
|
heurams = "heurams.__main__:main"
|
||||||
heurams-tui = "heurams.interface.__main__:main"
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
@@ -72,5 +72,5 @@ markers = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["uv_build>=0.7.19"]
|
requires = ["hatchling"]
|
||||||
build-backend = "uv_build"
|
build-backend = "hatchling.build"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
print("欢迎使用 HeurAMS 及其组件!")
|
#print("欢迎使用 HeurAMS 及其组件!")
|
||||||
|
|
||||||
# 补充日志记录
|
# 补充日志记录
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|||||||
+57
-16
@@ -1,22 +1,63 @@
|
|||||||
import heurams.services.version as ver
|
import platform
|
||||||
|
|
||||||
|
import click
|
||||||
|
from heurams.services.version import ver, stage, codename, codename_cn
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(
|
||||||
|
invoke_without_command=True,
|
||||||
|
help=(
|
||||||
|
f"HeurAMS {ver} - 启发式辅助记忆调度器"
|
||||||
|
),
|
||||||
|
context_settings={"help_option_names": ["-h", "--help"]},
|
||||||
|
)
|
||||||
|
@click.version_option(
|
||||||
|
ver, "-v", "--version",
|
||||||
|
prog_name="HeurAMS",
|
||||||
|
message=f"%(prog)s %(version)s {stage} ({codename}/{codename_cn}), {platform.system()}",
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def cli(ctx):
|
||||||
|
if ctx.invoked_subcommand is None:
|
||||||
|
click.echo(cli.get_help(ctx))
|
||||||
|
ctx.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def tui():
|
||||||
|
"""启动内置基本用户界面 (TUI)"""
|
||||||
|
import heurams.interface.__main__ as tui_module
|
||||||
|
|
||||||
|
tui_module.main()
|
||||||
|
|
||||||
|
|
||||||
|
def _print_version():
|
||||||
|
click.echo(
|
||||||
|
f"HeurAMS {ver} ({codename}/{codename_cn}), 阶段: {stage}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command()
|
||||||
|
def version():
|
||||||
|
"""输出版本信息"""
|
||||||
|
_print_version()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name="ver", hidden=True)
|
||||||
|
def ver_cmd():
|
||||||
|
"""输出版本信息"""
|
||||||
|
_print_version()
|
||||||
|
|
||||||
|
|
||||||
|
@cli.command(name="help")
|
||||||
|
@click.pass_context
|
||||||
|
def help_cmd(ctx):
|
||||||
|
"""显示此帮助信息"""
|
||||||
|
click.echo(cli.get_help(ctx.parent))
|
||||||
|
|
||||||
|
|
||||||
# __main__.py
|
|
||||||
def main():
|
def main():
|
||||||
prompt = f"""HeurAMS {ver.ver} 已经被成功地安装在系统中.
|
cli()
|
||||||
HeurAMS 被设计为一个带有辅助记忆调度器功能的软件包, 无法直接被执行, 但可被其他 Python 程序调用.
|
|
||||||
若您想启动内置的基本用户界面:
|
|
||||||
请运行 python -m heurams.interface,
|
|
||||||
或者 python -m heurams.interface.__main__
|
|
||||||
python 代指您使用的解释器, 在某些发行版中可能是 python3, 而 python 命令被指向了 python2.
|
|
||||||
尽管项目保留了 requirements.txt, 我们仍不推荐使用系统 python 和原始 venv 进行开发.
|
|
||||||
项目的推荐开发环境工具是 uv.
|
|
||||||
如果你的环境已经安装了 uv:
|
|
||||||
先运行 uv sync --all-extras 同步环境, 此命令只需要执行一遍, uv 会自动处理依赖.
|
|
||||||
然后通过运行 uv run heurams-tui 启动内置基本用户界面.
|
|
||||||
此时您的解释器在项目目录里的 .venv/bin 中, 使用 IDE 开发前, 务必切换解释器!
|
|
||||||
注意: 一个常见的误区是, 执行 interface 下的 __main__.py 运行基本用户界面, 这会导致 Python 上下文环境异常, 请不要这样做."""
|
|
||||||
print(prompt)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -72,9 +72,8 @@ class HeurAMSApp(App):
|
|||||||
) -> None: # 用来给没使用/禁用的快捷键占位, 因为 Binding 删除不了
|
) -> None: # 用来给没使用/禁用的快捷键占位, 因为 Binding 删除不了
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# 移除烦人的 "rich traceback"
|
'''
|
||||||
# Textual 官方不会管这破事, 写 Rich 写入脑了导致的
|
# 移除烦人的 "rich traceback", 但可能导致未定义行为出现, 所以注释掉
|
||||||
# 不知道哪来的自信改标准库的 traceback
|
|
||||||
# https://github.com/Textualize/textual/discussions/6255
|
# https://github.com/Textualize/textual/discussions/6255
|
||||||
# NOTE: 进行 textual 版本升级时, 确保查看过上游代码, 尤其是 App 的 _exception
|
# NOTE: 进行 textual 版本升级时, 确保查看过上游代码, 尤其是 App 的 _exception
|
||||||
# 如果行为变了就把下面的删了 (虽然有 fallback)
|
# 如果行为变了就把下面的删了 (虽然有 fallback)
|
||||||
@@ -89,3 +88,4 @@ class HeurAMSApp(App):
|
|||||||
self._close_messages_no_wait()
|
self._close_messages_no_wait()
|
||||||
raise self._exception
|
raise self._exception
|
||||||
super().panic(*args) # ditto
|
super().panic(*args) # ditto
|
||||||
|
'''
|
||||||
@@ -2,13 +2,13 @@ from heurams.interface import *
|
|||||||
from heurams.context import config_var
|
from heurams.context import config_var
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
import threading
|
import threading
|
||||||
import zmq
|
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def start_debug_server(app):
|
def start_debug_server(app):
|
||||||
|
import zmq
|
||||||
logger = get_logger("zmq_debug")
|
logger = get_logger("zmq_debug")
|
||||||
context = zmq.Context()
|
context = zmq.Context()
|
||||||
socket = context.socket(zmq.REP)
|
socket = context.socket(zmq.REP)
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ API 版本代号: `{version.codename.capitalize()}`
|
|||||||
感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石:
|
感谢以下人士与团体, 他们的算法与理论构成了此软件现有算法的基石:
|
||||||
|
|
||||||
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论
|
- [Piotr A. Woźniak](https://supermemo.guru/wiki/Piotr_Wozniak): SM-2 算法与 SM-15 算法理论
|
||||||
|
- [Jarrett Ye](https://github.com/L-M-Sherlock): FSRS 算法与间隔重复理论文献参考
|
||||||
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现
|
- [Kazuaki Tanida](https://github.com/slaypni): SM-15 算法的 CoffeeScript 逆向实现
|
||||||
- [Thoughts Memo](https://www.zhihu.com/people/L.M.Sherlock): 间隔重复文献参考
|
|
||||||
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现
|
- [Open Spaced Repetition](https://github.com/open-spaced-repetition): FSRS 算法底层实现
|
||||||
|
|
||||||
# 运行环境信息
|
# 运行环境信息
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class FavoriteManagerScreen(Screen):
|
|||||||
return ListItem(container)
|
return ListItem(container)
|
||||||
|
|
||||||
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
|
def _get_repo_info(self, repo_path: str, fav: FavoriteItem) -> Optional[dict]:
|
||||||
"""获取仓库信息(标题、原子内容预览)"""
|
"""获取仓库信息 (标题、原子内容预览) """
|
||||||
try:
|
try:
|
||||||
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
data_repo = Path(config_var.get()["global"]["paths"]["data"]) / "repo"
|
||||||
repo_dir = data_repo / repo_path
|
repo_dir = data_repo / repo_path
|
||||||
@@ -200,7 +200,7 @@ class FavoriteManagerScreen(Screen):
|
|||||||
# 重新组合
|
# 重新组合
|
||||||
if not self.favorites:
|
if not self.favorites:
|
||||||
container.mount(Label("暂无收藏", classes="empty-label"))
|
container.mount(Label("暂无收藏", classes="empty-label"))
|
||||||
container.mount(Static("使用 * 键在记忆界面中添加收藏。"))
|
container.mount(Static("使用 * 键在记忆界面中添加收藏. "))
|
||||||
else:
|
else:
|
||||||
container.mount(
|
container.mount(
|
||||||
Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
Label(f"共 {len(self.favorites)} 个收藏项", classes="count-label")
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class MemScreen(Screen):
|
|||||||
self.rating = 3
|
self.rating = 3
|
||||||
|
|
||||||
def _get_repo_rel_path(self) -> str:
|
def _get_repo_rel_path(self) -> str:
|
||||||
"""获取仓库相对路径(相对于 data/repo)"""
|
"""获取仓库相对路径 (相对于 data/repo) """
|
||||||
if self.repo is None:
|
if self.repo is None:
|
||||||
return ""
|
return ""
|
||||||
# self.repo.source 是 Path 对象, 指向仓库目录
|
# self.repo.source 是 Path 对象, 指向仓库目录
|
||||||
@@ -250,7 +250,7 @@ class MemScreen(Screen):
|
|||||||
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:
|
||||||
@@ -273,7 +273,7 @@ class MemScreen(Screen):
|
|||||||
else:
|
else:
|
||||||
favorite_manager.add(repo_path, ident)
|
favorite_manager.add(repo_path, ident)
|
||||||
self.app.notify(f"已收藏:{ident}", severity="information")
|
self.app.notify(f"已收藏:{ident}", severity="information")
|
||||||
# 更新显示(如果需要)
|
# 更新显示 (如果需要)
|
||||||
self.update_display()
|
self.update_display()
|
||||||
|
|
||||||
def action_block_prompt(self):
|
def action_block_prompt(self):
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class PrecachingScreen(Screen):
|
|||||||
for repo in repos:
|
for repo in repos:
|
||||||
try:
|
try:
|
||||||
total += len(repo.ident_index)
|
total += len(repo.ident_index)
|
||||||
except:
|
except (AttributeError, TypeError):
|
||||||
continue
|
continue
|
||||||
return total
|
return total
|
||||||
|
|
||||||
@@ -278,7 +278,7 @@ class PrecachingScreen(Screen):
|
|||||||
repo.nucleonic_data_lict.get_itemic_unit(i)
|
repo.nucleonic_data_lict.get_itemic_unit(i)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except:
|
except (KeyError, TypeError, AttributeError):
|
||||||
continue
|
continue
|
||||||
self.total = len(nucleon_list)
|
self.total = len(nucleon_list)
|
||||||
return self.precache_by_list(nucleon_list)
|
return self.precache_by_list(nucleon_list)
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class SyncScreen(Screen):
|
|||||||
self.run_worker(self.perform_sync, thread=True)
|
self.run_worker(self.perform_sync, thread=True)
|
||||||
|
|
||||||
def perform_sync(self):
|
def perform_sync(self):
|
||||||
"""执行同步任务(在后台线程中运行)"""
|
"""执行同步任务 (在后台线程中运行) """
|
||||||
worker = get_current_worker()
|
worker = get_current_worker()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -234,7 +234,7 @@ class SyncScreen(Screen):
|
|||||||
is_error=True,
|
is_error=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 同步 orbital 目录(如果存在)
|
# 同步 orbital 目录 (如果存在)
|
||||||
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
orbital_dir = pathlib.Path(paths.get("orbital_dir", "./data/orbital"))
|
||||||
if orbital_dir.exists():
|
if orbital_dir.exists():
|
||||||
self.log_message(f"同步 orbital 目录: {orbital_dir}")
|
self.log_message(f"同步 orbital 目录: {orbital_dir}")
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""算法模块
|
||||||
|
|
||||||
|
自动发现并加载所有算法实现, 提供统一的算法注册表.
|
||||||
|
"""
|
||||||
|
|
||||||
import importlib
|
import importlib
|
||||||
import pkgutil
|
import pkgutil
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ _registry: dict[str, type["BaseAlgorithm"]] = {}
|
|||||||
|
|
||||||
|
|
||||||
class BaseAlgorithm:
|
class BaseAlgorithm:
|
||||||
|
"""间隔重复算法基类
|
||||||
|
|
||||||
|
定义所有调度算法必须实现的接口. 子类通过继承此类并设置 algo_name
|
||||||
|
自动注册到全局算法注册表.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
algo_name: 算法的唯一标识名称, 用于注册和查找
|
||||||
|
desc: 算法的简短描述
|
||||||
|
defaults: 算法数据字典的默认值模板
|
||||||
|
"""
|
||||||
|
|
||||||
algo_name = "BaseAlgorithm"
|
algo_name = "BaseAlgorithm"
|
||||||
desc = "算法基类"
|
desc = "算法基类"
|
||||||
|
|
||||||
@@ -18,6 +29,11 @@ class BaseAlgorithm:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_registry(cls) -> dict[str, type["BaseAlgorithm"]]:
|
def get_registry(cls) -> dict[str, type["BaseAlgorithm"]]:
|
||||||
|
"""获取所有已注册算法的字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
键为 algo_name, 值为算法类的字典
|
||||||
|
"""
|
||||||
return dict(_registry)
|
return dict(_registry)
|
||||||
|
|
||||||
class AlgodataDict(TypedDict):
|
class AlgodataDict(TypedDict):
|
||||||
@@ -43,7 +59,15 @@ class BaseAlgorithm:
|
|||||||
def revisor(
|
def revisor(
|
||||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
"""迭代记忆数据"""
|
"""迭代记忆数据
|
||||||
|
|
||||||
|
根据用户反馈更新算法状态, 计算下一次复习时间.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典, 包含该算法的所有状态参数
|
||||||
|
feedback: 用户反馈评分 (0-5), -1 表示跳过更新
|
||||||
|
is_new_activation: 是否为首次激活, 首次激活时重置部分参数
|
||||||
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"BaseAlgorithm.revisor 被调用, algodata keys: %s, feedback: %d, is_new_activation: %s",
|
"BaseAlgorithm.revisor 被调用, algodata keys: %s, feedback: %d, is_new_activation: %s",
|
||||||
list(algodata.keys()) if algodata else [],
|
list(algodata.keys()) if algodata else [],
|
||||||
@@ -53,7 +77,14 @@ class BaseAlgorithm:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_due(cls, algodata) -> int:
|
def is_due(cls, algodata) -> int:
|
||||||
"""是否应该复习"""
|
"""判断是否应该复习
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
1 表示应该复习, 0 表示不需要
|
||||||
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"BaseAlgorithm.is_due 被调用, algodata keys: %s",
|
"BaseAlgorithm.is_due 被调用, algodata keys: %s",
|
||||||
list(algodata.keys()) if algodata else [],
|
list(algodata.keys()) if algodata else [],
|
||||||
@@ -62,7 +93,14 @@ class BaseAlgorithm:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rating(cls, algodata) -> str:
|
def get_rating(cls, algodata) -> str:
|
||||||
"""获取评分信息"""
|
"""获取当前记忆状态的评分信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
评分的字符串表示, 如 efactor 值
|
||||||
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"BaseAlgorithm.rate 被调用, algodata keys: %s",
|
"BaseAlgorithm.rate 被调用, algodata keys: %s",
|
||||||
list(algodata.keys()) if algodata else [],
|
list(algodata.keys()) if algodata else [],
|
||||||
@@ -71,7 +109,14 @@ class BaseAlgorithm:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def nextdate(cls, algodata) -> int:
|
def nextdate(cls, algodata) -> int:
|
||||||
"""获取下一次记忆时间戳"""
|
"""获取下一次复习的时间戳
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
下次复习的日期戳 (天数), -1 表示无计划
|
||||||
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"BaseAlgorithm.nextdate 被调用, algodata keys: %s",
|
"BaseAlgorithm.nextdate 被调用, algodata keys: %s",
|
||||||
list(algodata.keys()) if algodata else [],
|
list(algodata.keys()) if algodata else [],
|
||||||
@@ -80,8 +125,16 @@ class BaseAlgorithm:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_integrity(cls, algodata):
|
def check_integrity(cls, algodata):
|
||||||
|
"""校验算法数据完整性
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
1 表示数据完整, 0 表示数据缺失或格式错误
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
cls.AlgodataDict(**algodata[cls.algo_name])
|
cls.AlgodataDict(**algodata[cls.algo_name])
|
||||||
return 1
|
return 1
|
||||||
except:
|
except (KeyError, TypeError, ValueError):
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def _feedback_to_rating(feedback: int) -> Rating:
|
|||||||
|
|
||||||
|
|
||||||
def _datetime_to_daystamp(dt: datetime) -> int:
|
def _datetime_to_daystamp(dt: datetime) -> int:
|
||||||
"""将 datetime 转换为天数戳(从 1970-01-01)"""
|
"""将 datetime 转换为天数戳 (从 1970-01-01) """
|
||||||
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
epoch = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
||||||
delta = dt - epoch
|
delta = dt - epoch
|
||||||
return delta.days
|
return delta.days
|
||||||
@@ -81,7 +81,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
|
||||||
@@ -116,11 +116,11 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
# State: int → IntEnum
|
# State: int → IntEnum
|
||||||
card.state = data.get("fsrs_state", 1)
|
card.state = data.get("fsrs_state", 1)
|
||||||
|
|
||||||
# Step: -1 表示 None(Review 状态下的 card.step 为 None)
|
# Step: -1 表示 None (Review 状态下的 card.step 为 None)
|
||||||
step = data.get("fsrs_step", -1)
|
step = data.get("fsrs_step", -1)
|
||||||
card.step = None if step == -1 else step
|
card.step = None if step == -1 else step
|
||||||
|
|
||||||
# Stability: 0.0 表示尚未计算(新卡片)
|
# Stability: 0.0 表示尚未计算 (新卡片)
|
||||||
stability = data.get("fsrs_stability", 0.0)
|
stability = data.get("fsrs_stability", 0.0)
|
||||||
card.stability = None if stability == 0.0 else stability
|
card.stability = None if stability == 0.0 else stability
|
||||||
|
|
||||||
@@ -170,11 +170,11 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
):
|
):
|
||||||
"""FSRS 算法迭代决策机制实现
|
"""FSRS 算法迭代决策机制实现
|
||||||
|
|
||||||
将 feedback (0-5) 映射为 FSRS Rating 后交由 py-fsrs 调度器处理。
|
将 feedback (0-5) 映射为 FSRS Rating 后交由 py-fsrs 调度器处理.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
feedback (int): 0-5 的记忆保留率量化参数
|
feedback (int): 0-5 的记忆保留率量化参数
|
||||||
is_new_activation: 是否为全新激活(重置为初始状态)
|
is_new_activation: 是否为全新激活 (重置为初始状态)
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"FSRS.revisor 开始, feedback: %d, is_new_activation: %s",
|
"FSRS.revisor 开始, feedback: %d, is_new_activation: %s",
|
||||||
@@ -201,7 +201,7 @@ class FSRSAlgorithm(BaseAlgorithm):
|
|||||||
cls._card_to_algodata(card, algodata)
|
cls._card_to_algodata(card, algodata)
|
||||||
# real_rept: 总复习次数
|
# real_rept: 总复习次数
|
||||||
algodata[cls.algo_name]["real_rept"] += 1
|
algodata[cls.algo_name]["real_rept"] += 1
|
||||||
# rept: 成功回忆次数(feedback ≥ 3 视为成功)
|
# rept: 成功回忆次数 (feedback ≥ 3 视为成功)
|
||||||
if feedback >= 3:
|
if feedback >= 3:
|
||||||
algodata[cls.algo_name]["rept"] += 1
|
algodata[cls.algo_name]["rept"] += 1
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class NSP0Algorithm(BaseAlgorithm):
|
class NSP0Algorithm(BaseAlgorithm):
|
||||||
|
"""NSP-0 非间隔重复调度器
|
||||||
|
|
||||||
|
快速筛选用算法, 对低分项目保持每日复习, 高分项目标记为已掌握.
|
||||||
|
适用于需要快速过滤大量材料的场景.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
algo_name: "NSP-0"
|
||||||
|
desc: 快速筛选用非间隔重复调度器
|
||||||
|
"""
|
||||||
|
|
||||||
algo_name = "NSP-0"
|
algo_name = "NSP-0"
|
||||||
desc = "快速筛选用非间隔重复调度器"
|
desc = "快速筛选用非间隔重复调度器"
|
||||||
|
|
||||||
@@ -38,11 +48,13 @@ class NSP0Algorithm(BaseAlgorithm):
|
|||||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||||
):
|
):
|
||||||
"""NSP-0 算法迭代决策机制实现
|
"""NSP-0 算法迭代决策机制实现
|
||||||
根据 quality(0 ~ 5) 进行参数迭代最佳间隔
|
|
||||||
quality 由主程序评估
|
低分 (feedback<=3) 设置间隔为 1 天, 高分标记为已掌握 (间隔无限).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
quality (int): 记忆保留率量化参数
|
algodata: 算法数据字典
|
||||||
|
feedback: 记忆保留率量化参数 (0-5), -1 表示跳过
|
||||||
|
is_new_activation: 是否为首次激活
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"NSP0.revisor 开始, feedback: %d, is_new_activation: %s",
|
"NSP0.revisor 开始, feedback: %d, is_new_activation: %s",
|
||||||
@@ -73,6 +85,14 @@ class NSP0Algorithm(BaseAlgorithm):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_due(cls, algodata):
|
def is_due(cls, algodata):
|
||||||
|
"""判断是否应该复习
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示到期, False 表示未到期
|
||||||
|
"""
|
||||||
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"NSP0.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
"NSP0.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
||||||
@@ -84,12 +104,28 @@ class NSP0Algorithm(BaseAlgorithm):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rating(cls, algodata):
|
def get_rating(cls, algodata):
|
||||||
efactor = algodata[cls.algo_name]["efactor"]
|
"""获取当前 important 标记作为评分信息
|
||||||
logger.debug("NSP0.rate: efactor=%f", efactor)
|
|
||||||
return str(efactor)
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
important 值的字符串表示
|
||||||
|
"""
|
||||||
|
important = algodata[cls.algo_name]["important"]
|
||||||
|
logger.debug("NSP0.rate: important=%d", important)
|
||||||
|
return str(important)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def nextdate(cls, algodata) -> int:
|
def nextdate(cls, algodata) -> int:
|
||||||
|
"""获取下一次复习日期
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
下次复习的天数戳
|
||||||
|
"""
|
||||||
next_date = algodata[cls.algo_name]["next_date"]
|
next_date = algodata[cls.algo_name]["next_date"]
|
||||||
logger.debug("NSP0.nextdate: %d", next_date)
|
logger.debug("NSP0.nextdate: %d", next_date)
|
||||||
return next_date
|
return next_date
|
||||||
|
|||||||
@@ -643,13 +643,13 @@ class SM15MAlgorithm(BaseAlgorithm):
|
|||||||
lapse: int
|
lapse: int
|
||||||
repetition: int
|
repetition: int
|
||||||
of_val: float # O-Factor
|
of_val: float # O-Factor
|
||||||
optimum_interval_days: int # 最优间隔(天)
|
optimum_interval_days: int # 最优间隔 (天)
|
||||||
afs: list # A-Factor 历史
|
afs: list # A-Factor 历史
|
||||||
af: float # 当前 A-Factor
|
af: float # 当前 A-Factor
|
||||||
# 毫秒精度(子日排程)
|
# 毫秒精度 (子日排程)
|
||||||
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
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class SM2Algorithm(BaseAlgorithm):
|
class SM2Algorithm(BaseAlgorithm):
|
||||||
|
"""SuperMemo-2 算法实现
|
||||||
|
|
||||||
|
经典间隔重复算法, 基于 1987 年 Piotr Wozniak 设计的 SM-2.
|
||||||
|
通过维护 efactor (难度因子) 来调整复习间隔.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
algo_name: "SM-2"
|
||||||
|
desc: SuperMemo2 (1987) 简单间隔重复调度器
|
||||||
|
"""
|
||||||
|
|
||||||
algo_name = "SM-2"
|
algo_name = "SM-2"
|
||||||
desc = "SuperMemo2 (1987) 简单间隔重复调度器"
|
desc = "SuperMemo2 (1987) 简单间隔重复调度器"
|
||||||
|
|
||||||
@@ -38,11 +48,13 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False
|
||||||
):
|
):
|
||||||
"""SM-2 算法迭代决策机制实现
|
"""SM-2 算法迭代决策机制实现
|
||||||
根据 quality(0 ~ 5) 进行参数迭代最佳间隔
|
|
||||||
quality 由主程序评估
|
根据 feedback (0-5) 更新 efactor 并计算下次复习间隔.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
quality (int): 记忆保留率量化参数
|
algodata: 算法数据字典
|
||||||
|
feedback: 记忆保留率量化参数 (0-5), -1 表示跳过
|
||||||
|
is_new_activation: 是否为首次激活
|
||||||
"""
|
"""
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"SM2.revisor 开始, feedback: %d, is_new_activation: %s",
|
"SM2.revisor 开始, feedback: %d, is_new_activation: %s",
|
||||||
@@ -107,6 +119,14 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_due(cls, algodata):
|
def is_due(cls, algodata):
|
||||||
|
"""判断是否应该复习
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示到期, False 表示未到期
|
||||||
|
"""
|
||||||
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
result = algodata[cls.algo_name]["next_date"] <= timer.get_daystamp()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"SM2.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
"SM2.is_due: next_date=%d, current_daystamp=%d, result=%s",
|
||||||
@@ -118,12 +138,28 @@ class SM2Algorithm(BaseAlgorithm):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_rating(cls, algodata):
|
def get_rating(cls, algodata):
|
||||||
|
"""获取当前 efactor 作为评分信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
efactor 值的字符串表示
|
||||||
|
"""
|
||||||
efactor = algodata[cls.algo_name]["efactor"]
|
efactor = algodata[cls.algo_name]["efactor"]
|
||||||
logger.debug("SM2.rate: efactor=%f", efactor)
|
logger.debug("SM2.rate: efactor=%f", efactor)
|
||||||
return str(efactor)
|
return str(efactor)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def nextdate(cls, algodata) -> int:
|
def nextdate(cls, algodata) -> int:
|
||||||
|
"""获取下一次复习日期
|
||||||
|
|
||||||
|
Args:
|
||||||
|
algodata: 算法数据字典
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
下次复习的天数戳
|
||||||
|
"""
|
||||||
next_date = algodata[cls.algo_name]["next_date"]
|
next_date = algodata[cls.algo_name]["next_date"]
|
||||||
logger.debug("SM2.nextdate: %d", next_date)
|
logger.debug("SM2.nextdate: %d", next_date)
|
||||||
return next_date
|
return next_date
|
||||||
|
|||||||
@@ -1,19 +1,41 @@
|
|||||||
class Evalizer:
|
class Evalizer:
|
||||||
"""几乎无副作用的模板系统
|
"""几乎无副作用的模板系统
|
||||||
|
|
||||||
接受环境信息并创建一个模板解析工具, 工具传入参数支持list, dict及其嵌套
|
接受环境信息并创建一个模板解析工具, 递归遍历数据结构,
|
||||||
副作用问题: 仅存在于 eval 函数
|
对 "eval:" 前缀的字符串执行表达式求值.
|
||||||
|
|
||||||
|
副作用问题: 仅存在于 eval 函数调用.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
env: 模板求值时的环境变量字典
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: 弃用风险极高的 eval
|
# TODO: 弃用风险极高的 eval
|
||||||
# TODO: 异步/多线程执行避免堵塞
|
# TODO: 异步/多线程执行避免堵塞
|
||||||
def __init__(self, environment: dict) -> None:
|
def __init__(self, environment: dict) -> None:
|
||||||
|
"""初始化模板解析器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
environment: 模板求值时的环境变量字典
|
||||||
|
"""
|
||||||
self.env = environment
|
self.env = environment
|
||||||
|
|
||||||
def __call__(self, anyobj):
|
def __call__(self, anyobj):
|
||||||
|
"""调用入口, 等同于 travel"""
|
||||||
return self.travel(anyobj)
|
return self.travel(anyobj)
|
||||||
|
|
||||||
def travel(self, anyobj):
|
def travel(self, anyobj):
|
||||||
|
"""递归遍历数据结构并展开模板表达式
|
||||||
|
|
||||||
|
支持 list, dict, tuple 的嵌套结构.
|
||||||
|
字符串以 "eval:" 开头时执行表达式求值.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
anyobj: 任意 Python 对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
展开后的对象
|
||||||
|
"""
|
||||||
if isinstance(anyobj, list):
|
if isinstance(anyobj, list):
|
||||||
return list(map(self.travel, anyobj))
|
return list(map(self.travel, anyobj))
|
||||||
elif isinstance(anyobj, dict):
|
elif isinstance(anyobj, dict):
|
||||||
@@ -29,5 +51,13 @@ class Evalizer:
|
|||||||
return anyobj
|
return anyobj
|
||||||
|
|
||||||
def eval_with_env(self, s: str):
|
def eval_with_env(self, s: str):
|
||||||
|
"""在环境变量上下文中执行表达式
|
||||||
|
|
||||||
|
Args:
|
||||||
|
s: Python 表达式字符串
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
表达式求值结果
|
||||||
|
"""
|
||||||
ret = eval(s, globals(), self.env)
|
ret = eval(s, globals(), self.env)
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ class Lict(MutableSequence):
|
|||||||
self._list_dirty = False
|
self._list_dirty = False
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
|
"""按键或索引获取值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 字符串键 (字典访问) 或整数索引 (列表访问)
|
||||||
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
return self._dict[key]
|
return self._dict[key]
|
||||||
else:
|
else:
|
||||||
@@ -57,7 +62,12 @@ class Lict(MutableSequence):
|
|||||||
return self._list[key]
|
return self._list[key]
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
"""传入键值对时等同于操作字典, 传入索引+元组时等用于替换某索引的列表值为新元组"""
|
"""按键或索引设置值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 字符串键 (字典设置) 或整数索引 (替换列表元组)
|
||||||
|
value: 新值, 索引访问时必须为 (key, value) 元组
|
||||||
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
self._dict[key] = value
|
self._dict[key] = value
|
||||||
self._list_dirty = True
|
self._list_dirty = True
|
||||||
@@ -81,14 +91,17 @@ class Lict(MutableSequence):
|
|||||||
del self._dict[del_key]
|
del self._dict[del_key]
|
||||||
|
|
||||||
def keys(self):
|
def keys(self):
|
||||||
|
"""返回所有键"""
|
||||||
self._sync_if_needed()
|
self._sync_if_needed()
|
||||||
return self._dict.keys()
|
return self._dict.keys()
|
||||||
|
|
||||||
def values(self):
|
def values(self):
|
||||||
|
"""返回所有值"""
|
||||||
self._sync_if_needed()
|
self._sync_if_needed()
|
||||||
return self._dict.values()
|
return self._dict.values()
|
||||||
|
|
||||||
def items(self):
|
def items(self):
|
||||||
|
"""返回所有键值对元组列表"""
|
||||||
self._sync_if_needed()
|
self._sync_if_needed()
|
||||||
return self._list
|
return self._list
|
||||||
|
|
||||||
@@ -105,6 +118,14 @@ class Lict(MutableSequence):
|
|||||||
return item in self._list or item in self.keys() or item in self.values()
|
return item in self._list or item in self.keys() or item in self.values()
|
||||||
|
|
||||||
def append(self, item):
|
def append(self, item):
|
||||||
|
"""追加键值对元组
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item: (key, value) 格式的元组
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: item 不是二元组
|
||||||
|
"""
|
||||||
if item != (item[0], item[1]):
|
if item != (item[0], item[1]):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
self._sync_if_needed() # 以防 forced_order
|
self._sync_if_needed() # 以防 forced_order
|
||||||
@@ -114,6 +135,14 @@ class Lict(MutableSequence):
|
|||||||
self._sync_if_needed() # 以防 forced_order
|
self._sync_if_needed() # 以防 forced_order
|
||||||
|
|
||||||
def append_if_it_doesnt_exist_before(self, item: Any):
|
def append_if_it_doesnt_exist_before(self, item: Any):
|
||||||
|
"""若键不存在则追加键值对
|
||||||
|
|
||||||
|
Args:
|
||||||
|
item: (key, value) 格式的元组
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
NotImplementedError: item 不是二元组
|
||||||
|
"""
|
||||||
if item != (item[0], item[1]):
|
if item != (item[0], item[1]):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
self._sync_if_needed()
|
self._sync_if_needed()
|
||||||
@@ -162,9 +191,25 @@ class Lict(MutableSequence):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def get_itemic_unit(self, ident):
|
def get_itemic_unit(self, ident):
|
||||||
|
"""获取指定键的 (key, value) 元组
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ident: 键名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(key, value) 格式的元组
|
||||||
|
"""
|
||||||
return (ident, self._dict[ident])
|
return (ident, self._dict[ident])
|
||||||
|
|
||||||
def keys_equal_with(self, other):
|
def keys_equal_with(self, other):
|
||||||
|
"""比较两个 Lict 的键集合是否相同
|
||||||
|
|
||||||
|
Args:
|
||||||
|
other: 另一个 Lict 实例
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示键集合相同
|
||||||
|
"""
|
||||||
self._sync_if_needed()
|
self._sync_if_needed()
|
||||||
return self.key_equality(self, other)
|
return self.key_equality(self, other)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""粒子数据模型模块
|
||||||
|
|
||||||
|
定义记忆单元的核心数据结构: Nucleon (内容)、Electron (状态)、Atom (组装).
|
||||||
|
"""
|
||||||
|
|
||||||
from .atom import Atom
|
from .atom import Atom
|
||||||
from .electron import Electron
|
from .electron import Electron
|
||||||
from .nucleon import Nucleon
|
from .nucleon import Nucleon
|
||||||
|
|||||||
@@ -70,7 +70,8 @@ class Electron:
|
|||||||
try:
|
try:
|
||||||
result = self.algo.get_rating(self.algodata)
|
result = self.algo.get_rating(self.algodata)
|
||||||
return result
|
return result
|
||||||
except:
|
except (KeyError, TypeError, AttributeError) as e:
|
||||||
|
logger.warning("获取评分失败 (ident=%s): %s", self.ident, e)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def nextdate(self) -> int:
|
def nextdate(self) -> int:
|
||||||
|
|||||||
@@ -8,9 +8,27 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Nucleon:
|
class Nucleon:
|
||||||
"""原子核: 带有运行时隔离的模板化只读材料元数据容器"""
|
"""原子核: 带有运行时隔离的模板化只读材料元数据容器
|
||||||
|
|
||||||
|
封装记忆单元的内容数据, 通过 Evalizer 模板系统展开 payload 和 common
|
||||||
|
中的动态表达式. 创建后数据不可修改.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
ident: 记忆单元的唯一标识
|
||||||
|
data: 展开后的内容字典 (只读)
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, ident, payload, common):
|
def __init__(self, ident, payload, common):
|
||||||
|
"""初始化核子
|
||||||
|
|
||||||
|
合并 payload 和 common, 通过 Evalizer 展开模板表达式.
|
||||||
|
展开失败时静默降级为原始数据.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ident: 记忆单元标识
|
||||||
|
payload: 记忆内容字典
|
||||||
|
common: 通用元数据字典
|
||||||
|
"""
|
||||||
self.ident = ident
|
self.ident = ident
|
||||||
try:
|
try:
|
||||||
data_safe = deepcopy((payload | common))
|
data_safe = deepcopy((payload | common))
|
||||||
@@ -36,31 +54,64 @@ class Nucleon:
|
|||||||
self.data = payload | common
|
self.data = payload | common
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
|
"""按字符串键获取数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 字符串键名, "ident" 返回标识
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
对应键的值
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: 键类型不是字符串
|
||||||
|
"""
|
||||||
if isinstance(key, str):
|
if isinstance(key, str):
|
||||||
if key == "ident":
|
if key == "ident":
|
||||||
return self.ident
|
return self.ident
|
||||||
return self.data[key]
|
return self.data[key]
|
||||||
else:
|
else:
|
||||||
raise AttributeError
|
raise AttributeError(f"Nucleon 仅支持字符串键访问, 收到: {type(key).__name__}")
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
|
"""禁止修改 (只读容器)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: 始终抛出
|
||||||
|
"""
|
||||||
raise AttributeError("应为只读")
|
raise AttributeError("应为只读")
|
||||||
|
|
||||||
def __delitem__(self, key):
|
def __delitem__(self, key):
|
||||||
|
"""禁止删除 (只读容器)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
AttributeError: 始终抛出
|
||||||
|
"""
|
||||||
raise AttributeError("应为只读")
|
raise AttributeError("应为只读")
|
||||||
|
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
|
"""迭代数据字典的键"""
|
||||||
return iter(self.data)
|
return iter(self.data)
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
|
"""检查键是否存在于数据中"""
|
||||||
return key in (self.data)
|
return key in (self.data)
|
||||||
|
|
||||||
def get(self, key, default=None):
|
def get(self, key, default=None):
|
||||||
|
"""安全获取数据值
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 键名
|
||||||
|
default: 键不存在时的默认值
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
键对应的值或默认值
|
||||||
|
"""
|
||||||
if key in self:
|
if key in self:
|
||||||
return self[key]
|
return self[key]
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
|
"""返回数据字典的长度"""
|
||||||
return len(self.data)
|
return len(self.data)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -71,6 +122,14 @@ class Nucleon:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_data(nucleonic_data: tuple):
|
def from_data(nucleonic_data: tuple):
|
||||||
|
"""从元组数据创建核子
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nucleonic_data: 格式为 (ident, (payload, common)) 的元组
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Nucleon 实例
|
||||||
|
"""
|
||||||
_data = nucleonic_data
|
_data = nucleonic_data
|
||||||
payload = _data[1][0]
|
payload = _data[1][0]
|
||||||
common = _data[1][1]
|
common = _data[1][1]
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""占位符模块
|
||||||
|
|
||||||
|
提供用于 UI 预览和测试的占位粒子对象, 避免空值错误.
|
||||||
|
"""
|
||||||
|
|
||||||
from .atom import Atom
|
from .atom import Atom
|
||||||
from .electron import Electron
|
from .electron import Electron
|
||||||
from .nucleon import Nucleon
|
from .nucleon import Nucleon
|
||||||
@@ -21,6 +26,8 @@ orbital_placeholder = {
|
|||||||
|
|
||||||
|
|
||||||
class NucleonPlaceholder(Nucleon):
|
class NucleonPlaceholder(Nucleon):
|
||||||
|
"""核子占位符, 用于 UI 预览"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("__placeholder__", {}, {})
|
super().__init__("__placeholder__", {}, {})
|
||||||
|
|
||||||
@@ -29,11 +36,15 @@ class NucleonPlaceholder(Nucleon):
|
|||||||
|
|
||||||
|
|
||||||
class ElectronPlaceholder(Electron):
|
class ElectronPlaceholder(Electron):
|
||||||
|
"""电子占位符, 用于 UI 预览"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("__placeholder__", {"": {"": ""}}, "")
|
super().__init__("__placeholder__", {"": {"": ""}}, "")
|
||||||
|
|
||||||
|
|
||||||
class AtomPlaceholder(Atom):
|
class AtomPlaceholder(Atom):
|
||||||
|
"""原子占位符, 用于 UI 预览"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
NucleonPlaceholder(), ElectronPlaceholder(), orbital_placeholder
|
NucleonPlaceholder(), ElectronPlaceholder(), orbital_placeholder
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"""猜测谜题模块 (预留)"""
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
from .base import BasePuzzle
|
from .base import BasePuzzle
|
||||||
@@ -6,5 +8,10 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class GuessPuzzle(BasePuzzle):
|
class GuessPuzzle(BasePuzzle):
|
||||||
|
"""猜测型谜题 (预留实现)
|
||||||
|
|
||||||
|
要求用户猜测词义, 尚未完成实现.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# mcq.py
|
"""识别谜题模块"""
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
@@ -8,11 +8,14 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RecognitionPuzzle(BasePuzzle):
|
class RecognitionPuzzle(BasePuzzle):
|
||||||
"""识别占位符"""
|
"""识别型谜题
|
||||||
|
|
||||||
|
展示内容供用户识别确认, 无需主动回忆.
|
||||||
|
常用于复习流程的最后阶段 (retronly 回溯模式).
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
logger.debug("RecognitionPuzzle.__init__")
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
logger.debug("RecognitionPuzzle.refresh(空实现)")
|
"""刷新谜题 (识别型无需生成内容)"""
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
from heurams.services.logger import get_logger
|
"""反应器模块
|
||||||
|
|
||||||
|
基于三层嵌套状态机 (Router -> Procession -> Expander) 实现复习流程调度与排程.
|
||||||
|
"""
|
||||||
|
|
||||||
from .expander import Expander
|
from .expander import Expander
|
||||||
from .router import Router
|
from .router import Router
|
||||||
|
|||||||
@@ -14,7 +14,17 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Expander(Machine):
|
class Expander(Machine):
|
||||||
"""单原子调度展开器"""
|
"""单原子调度展开器
|
||||||
|
|
||||||
|
根据轨道策略 (orbital) 将单个原子展开为谜题序列.
|
||||||
|
包含 exammode (考试模式) 和 retronly (回溯模式) 两个阶段.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
atom: 关联的 Atom 实例
|
||||||
|
route: 当前路由阶段
|
||||||
|
puzzles_inf: 展开后的谜题信息列表
|
||||||
|
min_ratings: 每个谜题的最低评分记录
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, atom: pt.Atom, route=RouterState.RECOGNITION):
|
def __init__(self, atom: pt.Atom, route=RouterState.RECOGNITION):
|
||||||
self.route = route
|
self.route = route
|
||||||
@@ -85,20 +95,47 @@ class Expander(Machine):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_puzzles_inf(self):
|
def get_puzzles_inf(self):
|
||||||
|
"""获取谜题信息列表
|
||||||
|
|
||||||
|
回溯模式下返回识别谜题, 否则返回展开的谜题列表.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
谜题信息字典列表
|
||||||
|
"""
|
||||||
if self.state == "retronly":
|
if self.state == "retronly":
|
||||||
return [{"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}]
|
return [{"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}]
|
||||||
return self.puzzles_inf
|
return self.puzzles_inf
|
||||||
|
|
||||||
def get_current_puzzle_inf(self):
|
def get_current_puzzle_inf(self):
|
||||||
|
"""获取当前谜题信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
当前谜题的信息字典
|
||||||
|
"""
|
||||||
if self.state == "retronly":
|
if self.state == "retronly":
|
||||||
return {"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}
|
return {"puzzle": puz.puzzles["recognition"], "alia": "Recognition"}
|
||||||
return self.current_puzzle_inf
|
return self.current_puzzle_inf
|
||||||
|
|
||||||
def report(self, rating):
|
def report(self, rating):
|
||||||
|
"""报告当前谜题的评分
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rating: 用户评分 (0-5)
|
||||||
|
"""
|
||||||
if self.puzzles_inf:
|
if self.puzzles_inf:
|
||||||
self.min_ratings[self.cursor] = min(rating, self.min_ratings[self.cursor])
|
self.min_ratings[self.cursor] = min(rating, self.min_ratings[self.cursor])
|
||||||
|
|
||||||
def get_quality(self):
|
def get_quality(self):
|
||||||
|
"""获取所有谜题的最低评分
|
||||||
|
|
||||||
|
仅在回溯模式 (retronly) 下可用.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
所有谜题评分的最小值
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IndexError: 非回溯模式下调用
|
||||||
|
"""
|
||||||
if self.puzzles_inf:
|
if self.puzzles_inf:
|
||||||
if self.is_state("retronly", self):
|
if self.is_state("retronly", self):
|
||||||
return reduce(lambda x, y: min(x, y), self.min_ratings)
|
return reduce(lambda x, y: min(x, y), self.min_ratings)
|
||||||
|
|||||||
@@ -100,20 +100,40 @@ class Procession(Machine):
|
|||||||
return length
|
return length
|
||||||
|
|
||||||
def process(self):
|
def process(self):
|
||||||
|
"""获取当前游标位置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
当前游标索引
|
||||||
|
"""
|
||||||
logger.debug("Procession.process: cursor=%d", self.cursor)
|
logger.debug("Procession.process: cursor=%d", self.cursor)
|
||||||
return self.cursor
|
return self.cursor
|
||||||
|
|
||||||
def total_length(self):
|
def total_length(self):
|
||||||
|
"""获取队列总长度 (含已处理的原子)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
原子总数
|
||||||
|
"""
|
||||||
total = len(self.atoms)
|
total = len(self.atoms)
|
||||||
logger.debug("Procession.total_length: %d", total)
|
logger.debug("Procession.total_length: %d", total)
|
||||||
return total
|
return total
|
||||||
|
|
||||||
def is_empty(self):
|
def is_empty(self):
|
||||||
|
"""判断队列是否为空
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示队列为空
|
||||||
|
"""
|
||||||
empty = len(self.atoms) == 0
|
empty = len(self.atoms) == 0
|
||||||
logger.debug("Procession.is_empty: %s", empty)
|
logger.debug("Procession.is_empty: %s", empty)
|
||||||
return empty
|
return empty
|
||||||
|
|
||||||
def get_expander(self):
|
def get_expander(self):
|
||||||
|
"""获取当前原子的展开器
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Expander 实例
|
||||||
|
"""
|
||||||
return Expander(atom=self.current_atom, route=self.route) # type: ignore
|
return Expander(atom=self.current_atom, route=self.route) # type: ignore
|
||||||
|
|
||||||
def __repr__(self, style="pipe", ends="\n"):
|
def __repr__(self, style="pipe", ends="\n"):
|
||||||
|
|||||||
@@ -113,6 +113,14 @@ class Router(Machine):
|
|||||||
logger.debug("Router 进入 FINISHED 状态")
|
logger.debug("Router 进入 FINISHED 状态")
|
||||||
|
|
||||||
def current_procession(self):
|
def current_procession(self):
|
||||||
|
"""获取当前未完成的队列
|
||||||
|
|
||||||
|
遍历所有队列, 返回第一个未完成的 Procession.
|
||||||
|
若全部完成则切换到 FINISHED 状态并返回占位队列.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
当前活跃的 Procession, 或占位 Procession (全部完成时)
|
||||||
|
"""
|
||||||
logger.debug("Router.current_procession 被调用")
|
logger.debug("Router.current_procession 被调用")
|
||||||
for i in self.processions:
|
for i in self.processions:
|
||||||
i: Procession
|
i: Procession
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""状态枚举定义
|
||||||
|
|
||||||
|
定义反应器三层状态机的所有状态值.
|
||||||
|
"""
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
@@ -6,6 +11,8 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RouterState(Enum):
|
class RouterState(Enum):
|
||||||
|
"""路由器状态: 全局复习阶段"""
|
||||||
|
|
||||||
UNSURE = "unsure"
|
UNSURE = "unsure"
|
||||||
QUICK_REVIEW = "quick_review"
|
QUICK_REVIEW = "quick_review"
|
||||||
RECOGNITION = "recognition"
|
RECOGNITION = "recognition"
|
||||||
@@ -14,11 +21,15 @@ class RouterState(Enum):
|
|||||||
|
|
||||||
|
|
||||||
class ProcessionState(Enum):
|
class ProcessionState(Enum):
|
||||||
|
"""队列状态: 单阶段进度"""
|
||||||
|
|
||||||
ACTIVE = "active"
|
ACTIVE = "active"
|
||||||
FINISHED = "finished"
|
FINISHED = "finished"
|
||||||
|
|
||||||
|
|
||||||
class ExpanderState(Enum):
|
class ExpanderState(Enum):
|
||||||
|
"""展开器状态: 单原子调度模式"""
|
||||||
|
|
||||||
EXAMMODE = "exammode"
|
EXAMMODE = "exammode"
|
||||||
RETRONLY = "retronly"
|
RETRONLY = "retronly"
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
"""仓库系统模块
|
||||||
|
|
||||||
|
提供记忆单元集的加载、保存和管理功能.
|
||||||
|
"""
|
||||||
|
|
||||||
from .repo import Repo, RepoManifest
|
from .repo import Repo, RepoManifest
|
||||||
|
|
||||||
__all__ = ["Repo", "RepoManifest"]
|
__all__ = ["Repo", "RepoManifest"]
|
||||||
|
|||||||
@@ -10,6 +10,15 @@ from heurams.kernel.auxiliary.lict import Lict
|
|||||||
|
|
||||||
|
|
||||||
class RepoManifest(TypedDict):
|
class RepoManifest(TypedDict):
|
||||||
|
"""仓库清单数据结构
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
title: 仓库标题
|
||||||
|
author: 作者名称
|
||||||
|
package: 包名标识
|
||||||
|
desc: 仓库描述
|
||||||
|
"""
|
||||||
|
|
||||||
title: str
|
title: str
|
||||||
author: str
|
author: str
|
||||||
package: str
|
package: str
|
||||||
@@ -17,8 +26,21 @@ class RepoManifest(TypedDict):
|
|||||||
|
|
||||||
|
|
||||||
class Repo:
|
class Repo:
|
||||||
"""只维护仓库本身
|
"""记忆单元仓库
|
||||||
上层 API 请访问此对象下粒子对象列表"""
|
|
||||||
|
管理单个记忆单元集的所有数据, 包括内容 (payload)、算法状态 (algodata)、
|
||||||
|
复习策略 (schedule) 和元信息 (manifest/typedef).
|
||||||
|
|
||||||
|
上层 API 请访问此对象下的粒子对象列表 (nucleonic_data_lict 等).
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
schedule: 复习策略字典 (轨道定义)
|
||||||
|
payload: 记忆内容 (Lict)
|
||||||
|
algodata: 算法状态数据 (Lict)
|
||||||
|
manifest: 仓库清单信息
|
||||||
|
typedef: 类型定义和谜题配置
|
||||||
|
source: 仓库来源目录路径
|
||||||
|
"""
|
||||||
|
|
||||||
file_mapping = {
|
file_mapping = {
|
||||||
"schedule": "schedule.toml",
|
"schedule": "schedule.toml",
|
||||||
@@ -67,12 +89,16 @@ class Repo:
|
|||||||
}
|
}
|
||||||
try:
|
try:
|
||||||
self.config.update(dict(config_var.get()["repo"][self.manifest["package"]]))
|
self.config.update(dict(config_var.get()["repo"][self.manifest["package"]]))
|
||||||
except:
|
except (KeyError, TypeError, ValueError):
|
||||||
pass
|
pass
|
||||||
self._generate_particles_data()
|
self._generate_particles_data()
|
||||||
|
|
||||||
def _generate_particles_data(self):
|
def _generate_particles_data(self):
|
||||||
"""生成上层的粒子对象组和 API 交互, 会在 init 后自动调用"""
|
"""生成上层的粒子数据
|
||||||
|
|
||||||
|
将 payload 转换为 Nucleon 所需格式, 并为每个 ident 初始化
|
||||||
|
algodata 条目. 会在 __init__ 后自动调用.
|
||||||
|
"""
|
||||||
self.nucleonic_data_lict = Lict(
|
self.nucleonic_data_lict = Lict(
|
||||||
initlist=list(map(self._nucleonic_proc, self.payload))
|
initlist=list(map(self._nucleonic_proc, self.payload))
|
||||||
)
|
)
|
||||||
@@ -113,7 +139,7 @@ class Repo:
|
|||||||
with open(source / filename, "w") as f:
|
with open(source / filename, "w") as f:
|
||||||
try:
|
try:
|
||||||
dict_data = self.database[keyname].dicted_data
|
dict_data = self.database[keyname].dicted_data
|
||||||
except:
|
except AttributeError:
|
||||||
dict_data = dict(self.database[keyname])
|
dict_data = dict(self.database[keyname])
|
||||||
if filename.endswith("toml"):
|
if filename.endswith("toml"):
|
||||||
toml.dump(dict_data, f)
|
toml.dump(dict_data, f)
|
||||||
@@ -128,7 +154,14 @@ class Repo:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_new_repo(cls, source=None):
|
def create_new_repo(cls, source=None):
|
||||||
"""创建新的空单元集"""
|
"""创建新的空单元集
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: 可选的仓库目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含空数据的 Repo 实例
|
||||||
|
"""
|
||||||
default_database = {
|
default_database = {
|
||||||
"schedule": {},
|
"schedule": {},
|
||||||
"payload": Lict([]),
|
"payload": Lict([]),
|
||||||
@@ -141,7 +174,20 @@ class Repo:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_repodir(cls, source: Path):
|
def from_repodir(cls, source: Path):
|
||||||
"""从目录创建单元集"""
|
"""从目录创建单元集
|
||||||
|
|
||||||
|
读取目录中的 TOML/JSON 文件并构建 Repo 实例.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: 仓库目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repo 实例
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
FileNotFoundError: 目录缺少必要的文件
|
||||||
|
ValueError: 文件格式不支持
|
||||||
|
"""
|
||||||
database = {}
|
database = {}
|
||||||
for keyname, filename in cls.file_mapping.items():
|
for keyname, filename in cls.file_mapping.items():
|
||||||
with open(source / filename, "r") as f:
|
with open(source / filename, "r") as f:
|
||||||
@@ -163,23 +209,47 @@ class Repo:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, dictdata, source: Path | None = None):
|
def from_dict(cls, dictdata, source: Path | None = None):
|
||||||
"""从单一字典创建单元集"""
|
"""从单一字典创建单元集
|
||||||
|
|
||||||
|
Args:
|
||||||
|
dictdata: 包含 schedule/payload/algodata/manifest/typedef 的字典
|
||||||
|
source: 可选的仓库目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Repo 实例
|
||||||
|
"""
|
||||||
database = dictdata
|
database = dictdata
|
||||||
database["source"] = source
|
database["source"] = source
|
||||||
return Repo(**database)
|
return Repo(**database)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def check_repodir(cls, source: Path):
|
def check_repodir(cls, source: Path):
|
||||||
"""检测单元集目录合法性"""
|
"""检测单元集目录合法性
|
||||||
|
|
||||||
|
尝试从目录加载 Repo, 成功则视为合法.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
source: 待检测的目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True 表示合法, False 表示不合法
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
cls.from_repodir(source)
|
cls.from_repodir(source)
|
||||||
return True
|
return True
|
||||||
except:
|
except (FileNotFoundError, KeyError, ValueError, toml.TomlDecodeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def probe_valid_repos_in_dir(cls, folder: Path):
|
def probe_valid_repos_in_dir(cls, folder: Path):
|
||||||
"""返回一个合法的子目录 Path() 列表"""
|
"""扫描目录中的所有合法仓库子目录
|
||||||
|
|
||||||
|
Args:
|
||||||
|
folder: 待扫描的父目录路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
合法仓库子目录的 Path 列表
|
||||||
|
"""
|
||||||
lst = list()
|
lst = list()
|
||||||
for i in folder.iterdir():
|
for i in folder.iterdir():
|
||||||
if i.is_dir():
|
if i.is_dir():
|
||||||
|
|||||||
@@ -5,8 +5,6 @@
|
|||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
|
|
||||||
import playsound3
|
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -15,6 +13,7 @@ logger = get_logger(__name__)
|
|||||||
def play_by_path(path: pathlib.Path):
|
def play_by_path(path: pathlib.Path):
|
||||||
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
|
logger.debug("playsound_audio.play_by_path: 开始播放 %s", path)
|
||||||
try:
|
try:
|
||||||
|
import playsound3
|
||||||
playsound3.playsound(str(path))
|
playsound3.playsound(str(path))
|
||||||
logger.debug("播放完成: %s", path)
|
logger.debug("播放完成: %s", path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -12,12 +12,10 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
# from .protocol import PlayFunctionProtocol
|
# from .protocol import PlayFunctionProtocol
|
||||||
|
|
||||||
|
|
||||||
def play_by_path(path: pathlib.Path):
|
def play_by_path(path: pathlib.Path):
|
||||||
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
|
logger.debug("termux_audio.play_by_path: 开始播放 %s", path)
|
||||||
try:
|
try:
|
||||||
os.system(f"play-audio {path}")
|
os.system(f"play-audio {path.resolve()}")
|
||||||
logger.debug("播放命令已执行: %s", path)
|
logger.debug("播放命令已执行: %s", path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("播放失败: %s, 错误: %s", path, e)
|
logger.error("播放失败: %s, 错误: %s", path, e)
|
||||||
raise
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class BaseLLM:
|
|||||||
return "BaseLLM 未实现具体功能"
|
return "BaseLLM 未实现具体功能"
|
||||||
|
|
||||||
async def chat_stream(self, messages: List[Dict[str, str]], **kwargs):
|
async def chat_stream(self, messages: List[Dict[str, str]], **kwargs):
|
||||||
"""流式聊天(可选实现)
|
"""流式聊天 (可选实现)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
messages: 消息列表
|
messages: 消息列表
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class OpenAILLM(BaseLLM):
|
|||||||
logger.debug("OpenAILLM 初始化完成: base_url=%s", self.base_url)
|
logger.debug("OpenAILLM 初始化完成: base_url=%s", self.base_url)
|
||||||
|
|
||||||
def _get_client(self):
|
def _get_client(self):
|
||||||
"""获取 OpenAI 客户端(延迟导入)"""
|
"""获取 OpenAI 客户端 (延迟导入) """
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
try:
|
try:
|
||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from heurams.context import config_var
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import atexit
|
import atexit
|
||||||
from heurams.services import timer
|
from heurams.services import timer
|
||||||
from heurams.services.exceptions import WTFException
|
from heurams.services.exceptions import AtticError
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class Attic:
|
|||||||
self.ident = self.ident.replace("<DAYSTAMP>", str(timer.get_daystamp()))
|
self.ident = self.ident.replace("<DAYSTAMP>", str(timer.get_daystamp()))
|
||||||
self.ident = self.ident.replace("<TIMESTAMP>", str(timer.get_timestamp()))
|
self.ident = self.ident.replace("<TIMESTAMP>", str(timer.get_timestamp()))
|
||||||
if "<" in ident or ">" in ident:
|
if "<" in ident or ">" in ident:
|
||||||
raise WTFException
|
raise AtticError(f"Attic 标识 '{ident}' 中仍含有未替换的占位符")
|
||||||
# self.ident = get_md5(self.ident)
|
# self.ident = get_md5(self.ident)
|
||||||
self.pklpath = atticdir / f"{self.ident}.pkl"
|
self.pklpath = atticdir / f"{self.ident}.pkl"
|
||||||
atexit.register(self.save)
|
atexit.register(self.save)
|
||||||
@@ -43,7 +43,8 @@ class Attic:
|
|||||||
try:
|
try:
|
||||||
self.load()
|
self.load()
|
||||||
return
|
return
|
||||||
except:
|
except (pkl.UnpicklingError, EOFError, ModuleNotFoundError, ImportError) as e:
|
||||||
|
logger.warning("Attic '%s' 加载失败, 将重建: %s", self.ident, e)
|
||||||
self.pklpath.unlink(missing_ok=True)
|
self.pklpath.unlink(missing_ok=True)
|
||||||
self.pklpath.touch(exist_ok=True)
|
self.pklpath.touch(exist_ok=True)
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import toml
|
|||||||
from collections import UserDict
|
from collections import UserDict
|
||||||
|
|
||||||
from heurams.services.logger import get_logger
|
from heurams.services.logger import get_logger
|
||||||
from heurams.services.exceptions import WTFException
|
from heurams.services.exceptions import ConfigError
|
||||||
|
|
||||||
# 我们的流程是: 找到文件名: 返回文件名里头的数据; 找不到: 继续查索引; 所以 self.data 除了存本级各种索引球用没得
|
# 流程: 找到文件名: 返回文件名里头的数据; 找不到: 继续查索引; 所以 self.data 除了存本级各种索引无他用
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class ConfigDict(UserDict):
|
|||||||
|
|
||||||
def __new__(cls, config_path: pathlib.Path, dict=None):
|
def __new__(cls, config_path: pathlib.Path, dict=None):
|
||||||
if dict:
|
if dict:
|
||||||
raise WTFException("不要放默认值...")
|
raise ConfigError("ConfigDict 不接受默认字典参数")
|
||||||
|
|
||||||
# 规范化路径, 免得单例存在"别名"
|
# 规范化路径, 免得单例存在"别名"
|
||||||
path_key = config_path.resolve()
|
path_key = config_path.resolve()
|
||||||
@@ -33,7 +33,7 @@ class ConfigDict(UserDict):
|
|||||||
return
|
return
|
||||||
self._initialized = True
|
self._initialized = True
|
||||||
if dict:
|
if dict:
|
||||||
raise WTFException("不要放默认值...")
|
raise ConfigError("ConfigDict 不接受默认字典参数")
|
||||||
super().__init__(dict)
|
super().__init__(dict)
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
self.path = config_path
|
self.path = config_path
|
||||||
@@ -44,12 +44,14 @@ class ConfigDict(UserDict):
|
|||||||
with open(self.path, "r+") as f: # TODO: 给这个做缓存
|
with open(self.path, "r+") as f: # TODO: 给这个做缓存
|
||||||
try:
|
try:
|
||||||
self.data = toml.load(f)
|
self.data = toml.load(f)
|
||||||
except:
|
self._writable = True
|
||||||
|
except toml.TomlDecodeError as e:
|
||||||
|
logger.warning("配置文件解析失败: %s, 将以空配置运行", e)
|
||||||
self.data = {}
|
self.data = {}
|
||||||
self.persist = lambda: False # 不修改错误的配置文件
|
self._writable = False
|
||||||
|
|
||||||
def __getitem__(self, key):
|
def __getitem__(self, key):
|
||||||
# 我们实现了先进的懒狗加载
|
# 实现懒加载
|
||||||
value = super().__getitem__(key)
|
value = super().__getitem__(key)
|
||||||
if isinstance(value, pathlib.Path):
|
if isinstance(value, pathlib.Path):
|
||||||
return ConfigDict(value)
|
return ConfigDict(value)
|
||||||
@@ -59,10 +61,10 @@ class ConfigDict(UserDict):
|
|||||||
return super().__contains__(key)
|
return super().__contains__(key)
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
origvalue = super().__getitem__(key) # 所以你不该访问不存在的对象
|
origvalue = super().__getitem__(key) # 所以不该访问不存在的对象
|
||||||
if isinstance(origvalue, ConfigDict):
|
if isinstance(origvalue, ConfigDict):
|
||||||
if origvalue.path.is_dir():
|
if origvalue.path.is_dir():
|
||||||
raise WTFException("你怎么能变更目录配置的内容呢?!")
|
raise ConfigError("不允许变更目录配置的内容")
|
||||||
else:
|
else:
|
||||||
# 对文件, 我们允许这种覆写存在
|
# 对文件, 我们允许这种覆写存在
|
||||||
# 但是不准变类型
|
# 但是不准变类型
|
||||||
@@ -71,8 +73,12 @@ class ConfigDict(UserDict):
|
|||||||
|
|
||||||
def update_index(
|
def update_index(
|
||||||
self,
|
self,
|
||||||
): # 如果有人没事干在config里面创建指向config的符号链接 这玩意会崩溃 但是不要修复: 需要这个符号链接特性
|
): # 如果有人没事干在config里面创建指向config的符号链接 此函数会崩溃 但是不要修复: 需要这个符号链接特性并且真崩溃了绝对是用户故意的
|
||||||
for i in self.path.iterdir():
|
for i in self.path.iterdir():
|
||||||
|
if i.name.startswith("."):
|
||||||
|
continue # 以防 OSX 与 dolphin 在目录产生隐藏废料
|
||||||
|
if i.name == "desktop.ini":
|
||||||
|
continue # 以防 Windows 产生废料
|
||||||
if i.name.startswith("_"):
|
if i.name.startswith("_"):
|
||||||
if i.name == "_.toml" and not i.is_dir():
|
if i.name == "_.toml" and not i.is_dir():
|
||||||
with open(self.path / "_.toml", "r+") as f:
|
with open(self.path / "_.toml", "r+") as f:
|
||||||
@@ -87,6 +93,9 @@ class ConfigDict(UserDict):
|
|||||||
logger.debug(f"配置目录中有无效的文件 {i.stem}") # what's up bro
|
logger.debug(f"配置目录中有无效的文件 {i.stem}") # what's up bro
|
||||||
|
|
||||||
def persist(self):
|
def persist(self):
|
||||||
|
if not getattr(self, "_writable", True):
|
||||||
|
logger.warning("跳过写入: 配置文件解析失败, 避免覆盖损坏的文件")
|
||||||
|
return
|
||||||
if self.is_dir:
|
if self.is_dir:
|
||||||
for i in self.data.keys():
|
for i in self.data.keys():
|
||||||
j = self[i]
|
j = self[i]
|
||||||
|
|||||||
@@ -1,2 +1,10 @@
|
|||||||
class WTFException(Exception):
|
class HeurAMSError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(HeurAMSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AtticError(HeurAMSError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ class FavoriteManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def get_all(self) -> List[FavoriteItem]:
|
def get_all(self) -> List[FavoriteItem]:
|
||||||
"""获取所有收藏项(按添加时间倒序)"""
|
"""获取所有收藏项 (按添加时间倒序) """
|
||||||
return sorted(self._favorites, key=lambda x: x.added, reverse=True)
|
return sorted(self._favorites, key=lambda x: x.added, reverse=True)
|
||||||
|
|
||||||
def get_by_repo(self, repo_path: str) -> List[FavoriteItem]:
|
def get_by_repo(self, repo_path: str) -> List[FavoriteItem]:
|
||||||
|
|||||||
@@ -49,9 +49,9 @@ def daystamp_to_datetime(daystamp: int) -> datetime.datetime:
|
|||||||
|
|
||||||
|
|
||||||
def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
||||||
"""将 datetime 转换为日戳(从 1970-01-01 起的天数)
|
"""将 datetime 转换为日戳 (从 1970-01-01 起的天数)
|
||||||
|
|
||||||
接受带时区或 naive 的 datetime(naive 视为 UTC)。
|
接受带时区或 naive 的 datetime (naive 视为 UTC).
|
||||||
"""
|
"""
|
||||||
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
epoch = datetime.datetime(1970, 1, 1, tzinfo=datetime.timezone.utc)
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
@@ -61,5 +61,5 @@ def datetime_to_daystamp(dt: datetime.datetime) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def get_now_datetime() -> datetime.datetime:
|
def get_now_datetime() -> datetime.datetime:
|
||||||
"""获取当前时间的 UTC datetime(遵守时间覆盖)"""
|
"""获取当前时间的 UTC datetime (遵守时间覆盖) """
|
||||||
return datetime.datetime.fromtimestamp(get_timestamp(), tz=datetime.timezone.utc)
|
return datetime.datetime.fromtimestamp(get_timestamp(), tz=datetime.timezone.utc)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from heurams.services.logger import get_logger
|
|||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
ver = "0.5.0"
|
ver = "0.5.1"
|
||||||
stage = "stable"
|
stage = "stable"
|
||||||
codename = "fulcrum"
|
codename = "fulcrum"
|
||||||
codename_cn = "支点"
|
codename_cn = "支点"
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ def csv_to_toml(csv_path, toml_path=None, random_seed=None):
|
|||||||
# 添加section标题
|
# 添加section标题
|
||||||
toml_content.append(f"[{ident}]")
|
toml_content.append(f"[{ident}]")
|
||||||
|
|
||||||
# 添加所有其他列作为键值对(排除ident列)
|
# 添加所有其他列作为键值对 (排除ident列)
|
||||||
for key, value in row.items():
|
for key, value in row.items():
|
||||||
if key == "ident":
|
if key == "ident":
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -184,6 +184,18 @@ wheels = [
|
|||||||
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.3.3"
|
||||||
|
source = { registry = "https://mirrors.ustc.edu.cn/pypi/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://mirrors.ustc.edu.cn/pypi/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://mirrors.ustc.edu.cn/pypi/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
@@ -395,9 +407,10 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heurams"
|
name = "heurams"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
{ name = "tabulate" },
|
{ name = "tabulate" },
|
||||||
{ name = "toml" },
|
{ name = "toml" },
|
||||||
{ name = "transitions" },
|
{ name = "transitions" },
|
||||||
@@ -442,6 +455,7 @@ tts-edgetts = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
|
{ name = "click", specifier = ">=8.3.3" },
|
||||||
{ name = "edge-tts", marker = "extra == 'tts-edgetts'", specifier = ">=7.2.8" },
|
{ name = "edge-tts", marker = "extra == 'tts-edgetts'", specifier = ">=7.2.8" },
|
||||||
{ name = "fsrs", marker = "extra == 'algo-fsrs'", specifier = ">=6.3.1" },
|
{ name = "fsrs", marker = "extra == 'algo-fsrs'", specifier = ">=6.3.1" },
|
||||||
{ name = "heurams", extras = ["algo-fsrs"], marker = "extra == 'all'" },
|
{ name = "heurams", extras = ["algo-fsrs"], marker = "extra == 'all'" },
|
||||||
|
|||||||
Reference in New Issue
Block a user