From 315c2f40a55ef562affd2b50ac80ffdb919537a8 Mon Sep 17 00:00:00 2001 From: Wang Zhiyu Date: Tue, 11 Nov 2025 00:21:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E8=87=B3?= =?UTF-8?q?=20/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 +++- dynanote.py | 408 +++++++++++++++++++++++++++++++++++++++++++++++ hasher.py | 8 + requirements.txt | 3 + sm2.py | 80 ++++++++++ 5 files changed, 529 insertions(+), 2 deletions(-) create mode 100644 dynanote.py create mode 100644 hasher.py create mode 100644 requirements.txt create mode 100644 sm2.py diff --git a/README.md b/README.md index 648b4cd..a2d0ca4 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,31 @@ -# DynaNote +# DynaNote - 简单系统 -简易复习规划CLI程序, HeurAMS 衍生项目 \ No newline at end of file +## 命令列表 + +- `list [all|整数]` - 列出记忆单元 + - `list all` - 显示所有单元 + - `list 5` - 显示今天需要复习≤5次的单元 + - `list` - 等同于 `list 5` + +- `select ` - 选择记忆单元 +- `attach <字符串>` - 为选中的单元附加字符串 +- `new <名称>` - 创建新记忆单元 +- `del ` - 删除记忆单元 +- `mark <评分>` - 使用评分(0-5)更新SM-2算法 +- `show` - 显示选中单元的详细信息 +- `edit` - 使用nano编辑器编辑选中单元 +- `clear` - 清屏 +- `help` - 显示帮助和SM-2评分标准 +- `exit` - 退出shell + +## 数据结构 + +记忆单元以以下结构存储在`data.toml`中: + +```toml +[unit_id] +name = "单元名称" +attachments = [["附件文本", 时间戳]] +created_time = 日期戳 +algodata = {"SM-2" = {"efactor" = 2.5, "real_rept" = 0, ...}} +``` \ No newline at end of file diff --git a/dynanote.py b/dynanote.py new file mode 100644 index 0000000..3265f45 --- /dev/null +++ b/dynanote.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 +""" +DynaNote Shell - 使用 SM-2 算法的间隔重复闪卡系统 +""" + +import os +import sys +import cmd +import toml +import tempfile +import subprocess +from typing import Dict, List, Optional, Tuple +from tabulate import tabulate + +# 导入现有模块 +import hasher +import timer +import sm2 + + +class MemoryUnit: + """表示单个记忆单元(闪卡)""" + + def __init__(self, name: str, unit_id: Optional[str] = None): + self.name = name + self.unit_id = unit_id or hasher.get_md5(name) + self.attachments = set() # 元组集合 (字符串, 创建时间戳) + self.created_time = timer.get_daystamp() + self.algodata = { + sm2.SM2Algorithm.algo_name: { + 'efactor': 2.5, + 'real_rept': 0, + 'rept': 0, + 'interval': 0, + 'last_date': 0, + 'next_date': 0, + 'is_activated': 0, + 'last_modify': timer.get_timestamp() + } + } + + def to_dict(self) -> Dict: + """将记忆单元转换为字典以便 TOML 序列化""" + return { + 'name': self.name, + 'attachments': list(self.attachments), + 'created_time': self.created_time, + 'algodata': self.algodata + } + + @classmethod + def from_dict(cls, unit_id: str, data: Dict) -> 'MemoryUnit': + """从字典创建记忆单元""" + unit = cls(data['name'], unit_id) + unit.attachments = set(tuple(att) for att in data['attachments']) + unit.created_time = data['created_time'] + unit.algodata = data['algodata'] + return unit + + +class DynaNoteShell(cmd.Cmd): + """DynaNote 间隔重复系统的交互式 shell""" + + intro = '欢迎使用 DynaNote Shell. 输入 help 或 ? 查看命令列表. \n' + prompt = '(dynanote) > ' + + def __init__(self): + super().__init__() + self.data_file = './data.toml' + self.memory_units: Dict[str, MemoryUnit] = {} + self.selected_unit: Optional[MemoryUnit] = None + self.load_data() + + def load_data(self) -> None: + """从 data.toml 文件加载记忆单元""" + if os.path.exists(self.data_file): + try: + with open(self.data_file, 'r', encoding='utf-8') as f: + data = toml.load(f) + + for unit_id, unit_data in data.items(): + self.memory_units[unit_id] = MemoryUnit.from_dict(unit_id, unit_data) + + print(f"已加载 {len(self.memory_units)} 个记忆单元") + except Exception as e: + print(f"加载数据时出错: {e}") + else: + print("未找到数据文件. 从空数据库开始. ") + + def save_data(self) -> None: + """将记忆单元保存到 data.toml 文件""" + try: + data = {} + for unit_id, unit in self.memory_units.items(): + data[unit_id] = unit.to_dict() + + with open(self.data_file, 'w', encoding='utf-8') as f: + toml.dump(data, f) + + print("数据保存成功") + except Exception as e: + print(f"保存数据时出错: {e}") + + def find_unit_by_prefix(self, prefix: str) -> Optional[str]: + """通过前缀查找单元 ID(类似 docker rm)""" + matches = [unit_id for unit_id in self.memory_units.keys() + if unit_id.startswith(prefix)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + print(f"多个单元匹配前缀 '{prefix}': {', '.join(matches)}") + return None + else: + # 尝试通过名称查找 + name_matches = [unit_id for unit_id, unit in self.memory_units.items() + if unit.name == prefix] + if len(name_matches) == 1: + return name_matches[0] + elif len(name_matches) > 1: + print(f"多个单元具有名称 '{prefix}'") + return None + + return None + + def do_list(self, arg: str) -> None: + """列出记忆单元 + 用法: list [all|整数] + - list all: 显示所有记忆单元 + - list 5: 显示复习次数<=5且今天到期的单元 + - list: 同 'list 5' + """ + if not arg: + arg = "5" + + if arg.lower() == "all": + units_to_show = list(self.memory_units.values()) + else: + try: + max_reviews = int(arg) + today = timer.get_daystamp() + units_to_show = [ + unit for unit in self.memory_units.values() + if unit.algodata[sm2.SM2Algorithm.algo_name]['real_rept'] <= max_reviews + and unit.algodata[sm2.SM2Algorithm.algo_name]['next_date'] <= today + ] + except ValueError: + print("无效参数. 使用 'all' 或整数. ") + return + + if not units_to_show: + print("没有符合条件的记忆单元") + return + + # 准备 tabulate 数据 + table_data = [] + for unit in units_to_show: + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + table_data.append([ + unit.unit_id[:8] + "...", + unit.name, + algodata['real_rept'], + algodata['efactor'], + algodata['next_date'], + len(unit.attachments) + ]) + + headers = ["ID", "名称", "复习次数", "EFactor", "下次复习", "附件数"] + print(tabulate(table_data, headers=headers, tablefmt="grid")) + + def do_select(self, arg: str) -> None: + """通过 ID、前缀或名称选择记忆单元 + 用法: select <单元ID|前缀|名称> + """ + if not arg: + print("请提供单元 ID、前缀或名称") + return + + unit_id = self.find_unit_by_prefix(arg) + if unit_id: + self.selected_unit = self.memory_units[unit_id] + print(f"已选择单元: {self.selected_unit.name} ({unit_id[:8]}...)") + else: + print(f"未找到唯一匹配 '{arg}' 的单元") + + def do_attach(self, arg: str) -> None: + """将字符串附加到选定的记忆单元 + 用法: attach <字符串> + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + if not arg: + print("请提供要附加的字符串") + return + + attachment = (arg, int(timer.get_timestamp())) + self.selected_unit.attachments.add(attachment) + print(f"附件已添加到 {self.selected_unit.name}") + + def do_new(self, arg: str) -> None: + """创建新的记忆单元 + 用法: new <名称> + """ + if not arg: + print("请提供新单元的名称") + return + + unit = MemoryUnit(arg) + self.memory_units[unit.unit_id] = unit + self.selected_unit = unit + print(f"已创建新单元: {unit.name} ({unit.unit_id[:8]}...)") + + def do_del(self, arg: str) -> None: + """删除记忆单元 + 用法: del <单元ID|前缀|名称> + """ + if not arg: + print("请提供单元 ID、前缀或名称") + return + + unit_id = self.find_unit_by_prefix(arg) + if unit_id: + unit_name = self.memory_units[unit_id].name + del self.memory_units[unit_id] + + # 如果删除的是当前选中的单元,则清除选择 + if self.selected_unit and self.selected_unit.unit_id == unit_id: + self.selected_unit = None + + print(f"已删除单元: {unit_name}") + else: + print(f"未找到唯一匹配 '{arg}' 的单元") + + def do_mark(self, arg: str) -> None: + """使用评分(0-5)更新 SM-2 算法 + 用法: mark <评分> + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + try: + rating = int(arg) + if rating < 0 or rating > 5: + print("评分必须在 0 到 5 之间") + return + except ValueError: + print("请提供有效的整数评分(0-5)") + return + + # 显示评分标准以确认 + print("\nSM-2 评分标准:") + print(" 5 - 完美回答") + print(" 4 - 犹豫后正确回答") + print(" 3 - 经过严重困难后回忆起正确答案") + print(" 2 - 错误回答; 但记得正确答案") + print(" 1 - 错误回答; 但正确答案似乎熟悉") + print(" 0 - 完全遗忘") + + # 请求确认 + response = input(f"\n确认对 '{self.selected_unit.name}' 评分 {rating}?(y/N): ") + if response.lower() not in ['y', 'yes']: + print("评分已取消") + return + + sm2.SM2Algorithm.revisor(self.selected_unit.algodata, rating) + print(f"已使用评分 {rating} 更新 {self.selected_unit.name} 的 SM-2 参数") + + def do_show(self, arg: str) -> None: + """显示选定记忆单元的详细信息 + 用法: show + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + unit = self.selected_unit + algodata = unit.algodata[sm2.SM2Algorithm.algo_name] + + print(f"\n记忆单元: {unit.name}") + print(f"ID: {unit.unit_id}") + print(f"创建时间: {unit.created_time}") + print(f"\nSM-2 算法数据:") + print(f" EFactor: {algodata['efactor']}") + print(f" 实际复习次数: {algodata['real_rept']}") + print(f" 当前重复次数: {algodata['rept']}") + print(f" 间隔: {algodata['interval']} 天") + print(f" 上次复习: {algodata['last_date']}") + print(f" 下次复习: {algodata['next_date']}") + print(f" 是否激活: {algodata['is_activated']}") + + if unit.attachments: + print(f"\n附件 ({len(unit.attachments)}):") + for i, (text, timestamp) in enumerate(unit.attachments, 1): + print(f" {i}. {text[:50]}{'...' if len(text) > 50 else ''} (于 {timestamp})") + else: + print("\n无附件") + + def do_edit(self, arg: str) -> None: + """使用 nano 编辑器编辑选定的记忆单元 + 用法: edit + """ + if not self.selected_unit: + print("未选择单元. 请先使用 'select'. ") + return + + # 创建包含单元数据的临时文件 + with tempfile.NamedTemporaryFile(mode='w', suffix='.toml', delete=False) as f: + temp_file = f.name + toml.dump({self.selected_unit.unit_id: self.selected_unit.to_dict()}, f) + + try: + # 启动 nano 编辑器 + subprocess.run(['nano', temp_file], check=True) + + # 读取编辑后的文件 + with open(temp_file, 'r', encoding='utf-8') as f: + edited_data = toml.load(f) + + # 更新记忆单元 + if self.selected_unit.unit_id in edited_data: + updated_data = edited_data[self.selected_unit.unit_id] + self.memory_units[self.selected_unit.unit_id] = MemoryUnit.from_dict( + self.selected_unit.unit_id, updated_data + ) + self.selected_unit = self.memory_units[self.selected_unit.unit_id] + print("单元更新成功") + else: + print("错误: 在编辑后的文件中未找到单元 ID") + + except subprocess.CalledProcessError: + print("编辑器已关闭但未保存") + except Exception as e: + print(f"编辑单元时出错: {e}") + finally: + # 清理临时文件 + if os.path.exists(temp_file): + os.unlink(temp_file) + + def do_clear(self, arg: str) -> None: + """清屏 + 用法: clear + """ + os.system('clear') + + def do_help(self, arg: str) -> None: + """显示帮助和 SM-2 评分标准 + 用法: help + """ + print("\nDynaNote Shell 命令:") + print(" list [all|整数] - 列出记忆单元") + print(" select - 选择记忆单元") + print(" attach <字符串> - 将字符串附加到选定单元") + print(" new <名称> - 创建新记忆单元") + print(" del - 删除记忆单元") + print(" mark <评分> - 使用评分(0-5)更新 SM-2") + print(" show - 显示选定单元详情") + print(" edit - 使用 nano 编辑选定单元") + print(" clear - 清屏") + print(" help - 显示此帮助") + print(" exit - 退出 shell") + + print("\nSM-2 评分标准:") + print(" 5 - 完美回答") + print(" 4 - 犹豫后正确回答") + print(" 3 - 经过严重困难后回忆起正确答案") + print(" 2 - 错误回答; 但记得正确答案") + print(" 1 - 错误回答; 但正确答案似乎熟悉") + print(" 0 - 完全遗忘") + + def do_exit(self, arg: str) -> bool: + """退出 shell + 用法: exit + """ + self.save_data() + print("再见!") + return True + + def do_quit(self, arg: str) -> bool: + """退出 shell(exit 的别名) + 用法: quit + """ + return self.do_exit(arg) + + # 别名 + def do_sel(self, arg: str) -> None: + """select 命令的别名""" + self.do_select(arg) + + def do_ls(self, arg: str) -> None: + """list 命令的别名""" + self.do_list(arg) + + +def main(): + """主入口点""" + try: + DynaNoteShell().cmdloop() + except KeyboardInterrupt: + print("\n用户中断") + except Exception as e: + print(f"错误: {e}") + + +if __name__ == '__main__': + main() diff --git a/hasher.py b/hasher.py new file mode 100644 index 0000000..82229a0 --- /dev/null +++ b/hasher.py @@ -0,0 +1,8 @@ +# 哈希服务 +import hashlib + +def get_md5(text): + return hashlib.md5(text.encode('utf-8')).hexdigest() + +def hash(text): + return hashlib.md5(text.encode('utf-8')).hexdigest() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5764523 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +toml +cmd2 +tabulate \ No newline at end of file diff --git a/sm2.py b/sm2.py new file mode 100644 index 0000000..aac280b --- /dev/null +++ b/sm2.py @@ -0,0 +1,80 @@ +import timer +from typing import TypedDict + +class SM2Algorithm(): + algo_name = "SM-2" + + class AlgodataDict(TypedDict): + efactor: float + real_rept: int + rept: int + interval: int + last_date: int + next_date: int + is_activated: int + last_modify: float + + defaults = { + 'efactor': 2.5, + 'real_rept': 0, + 'rept': 0, + 'interval': 0, + 'last_date': 0, + 'next_date': 0, + 'is_activated': 0, + 'last_modify': timer.get_timestamp() + } + + @classmethod + def revisor(cls, algodata: dict, feedback: int = 5, is_new_activation: bool = False): + """SM-2 算法迭代决策机制实现 + 根据 quality(0 ~ 5) 进行参数迭代最佳间隔 + quality 由主程序评估 + + Args: + quality (int): 记忆保留率量化参数 + """ + if feedback == -1: + return + + algodata[cls.algo_name]['efactor'] = algodata[cls.algo_name]['efactor'] + ( + 0.1 - (5 - feedback) * (0.08 + (5 - feedback) * 0.02) + ) + algodata[cls.algo_name]['efactor'] = max(1.3, algodata[cls.algo_name]['efactor']) + + if feedback < 3: + algodata[cls.algo_name]['rept'] = 0 + algodata[cls.algo_name]['interval'] = 0 + else: + algodata[cls.algo_name]['rept'] += 1 + + algodata[cls.algo_name]['real_rept'] += 1 + + if is_new_activation: + algodata[cls.algo_name]['rept'] = 0 + algodata[cls.algo_name]['efactor'] = 2.5 + + if algodata[cls.algo_name]['rept'] == 0: + algodata[cls.algo_name]['interval'] = 1 + elif algodata[cls.algo_name]['rept'] == 1: + algodata[cls.algo_name]['interval'] = 6 + else: + algodata[cls.algo_name]['interval'] = round( + algodata[cls.algo_name]['interval'] * algodata[cls.algo_name]['efactor'] + ) + + algodata[cls.algo_name]['last_date'] = timer.get_daystamp() + algodata[cls.algo_name]['next_date'] = timer.get_daystamp() + algodata[cls.algo_name]['interval'] + algodata[cls.algo_name]['last_modify'] = timer.get_timestamp() + + @classmethod + def is_due(cls, algodata): + return (algodata[cls.algo_name]['next_date'] <= timer.get_daystamp()) + + @classmethod + def rate(cls, algodata): + return str(algodata[cls.algo_name]['efactor']) + + @classmethod + def nextdate(cls, algodata): + return algodata[cls.algo_name]['next_date']