#!/usr/bin/env python3
import os, random, json, hashlib, re, urllib.request, urllib.parse, urllib.error
from datetime import datetime, timedelta

TASKS_DIR = "/home/ubuntu/tasks"
LOG_FILE = "/tmp/study_cron.log"
TAVILY_KEY = "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"

DS_TOPICS = [
    {"id": "ds_01", "title": "时间复杂度与空间复杂度分析", "content": """### 时间复杂度与空间复杂度分析\n\n**核心概念**\n- 时间复杂度：算法执行次数与问题规模的渐近关系\n- 空间复杂度：算法所需存储空间与问题规模的渐近关系\n- 常用记号：O(1)、O(log n)、O(n)、O(n log n)、O(n2)、O(n3)、O(2n)、O(n!)\n\n**易错点**\n1. 混淆O(n)和O(2n)：系数不重要，渐近上界只看最高次项\n2. 递归算法空间复杂度：递归深度×每次递归的局部变量大小\n3. 最好/最坏/平均复杂度：不要把三者混淆\n\n**典型题目**\nfor (i = 0; i < n; i++)\n    for (j = i; j < n; j++):\n        pass  # O(1)操作\n\n分析：外层n次，内层分别为n, n-1, n-2...1，总和 = n(n+1)/2 = O(n2)\n"""},
    {"id": "ds_02", "title": "栈和队列的性质与扩展", "content": """### 栈和队列的性质与扩展\n\n**核心概念**\n- 栈：LIFO，先进后出；操作：push/pop/top\n- 队列：FIFO，先进先出；操作：enqueue/dequeue/front\n- 循环队列：避免假溢出，用(n+1)个空间区分满/空\n\n**易错点**\n1. 循环队列队空条件：front == rear\n2. 循环队列队满条件：(rear + 1) % maxsize == front\n3. 中缀表达式转后缀表达式时，栈中存放操作符，优先级低的运算符在前\n\n**出栈序列判定定理**\n对于入栈序列1,2,3...,n的输出序列：每个数字后面比它小的数字必须按**递减顺序**排列。例如：入栈1,2,3,4，若出栈序列为4,3,2,1，对于数字4后面比4小的{1,2,3}必须递减排列（3>2>1 ✓）。\n\n**典型题目**\n使用两个栈实现队列：stack_in用于入队，stack_out用于出队\n- 入队：元素压入stack_in\n- 出队：如果stack_out不为空，弹出stack_out栈顶；否则把stack_in全部倒入stack_out\n均摊时间复杂度：O(1)\n"""},
    {"id": "ds_03", "title": "二叉树遍历与线索二叉树", "content": """### 二叉树遍历与线索二叉树\n\n**核心概念**\n- 先序遍历（DLR）：根-左-右\n- 中序遍历（LDR）：左-根-右\n- 后序遍历（LRD）：左-右-根\n- 层序遍历：利用队列，层内从左到右\n\n**易错点**\n1. 已知两种遍历序列（包含中序），可唯一确定二叉树\n2. 仅有先序+后序无法唯一确定（可确定祖先关系但不能确定结构）\n3. 递归遍历时间复杂度O(n)，空间复杂度O(h)，h为树高\n\n**线索二叉树**\n- 目的：加快遍历速度，避免递归/栈\n- 规定：若无左孩子，指向前驱；若无右孩子，指向后继\n- 需要ltag/rtag标志位区分孩子指针和线索指针\n\n**典型题目**\n已知先序序列：ABDECF，中序序列：DBEAFC，构建二叉树\n1. 先序根A → 中序分 DBE | A | CF\n2. 左子树先序：BDE，中序：DBE → B为左子树根\n3. 右子树先序：CF，中序：CF → C为右子树根\n4. 结果：A左子B(B左D右E)，A右子C(C左F)\n"""},
    {"id": "ds_04", "title": "图遍历：DFS与BFS及其应用", "content": """### 图遍历：DFS与BFS及其应用\n\n**核心概念**\n- BFS（广度优先）：用队列，按层次遍历\n- DFS（深度优先）：用栈（或递归），先深后广\n- 时间复杂度：邻接表O(V+E)，邻接矩阵O(V2)\n\n**易错点**\n1. BFS计算最短路径（无权图）：首次到达即为最短\n2. DFS生成树/森林：同一强连通分量的顶点可通过DFS访问\n3. 邻接矩阵空间复杂度O(V2)，适合稠密图\n\n**应用**\n- BFS：最短路径（无权）、拓扑排序（Kahn算法）\n- DFS：拓扑排序（基于DFS的逆序）、强连通分量（Kosaraju）\n\n**典型题目**\n拓扑排序算法（Kahn）：\n1. 计算所有顶点入度\n2. 将入度为0的顶点入队\n3. 不断出队，删除边，更新入度\n4. 若输出顶点数为V，则无环；否则有环\n时间复杂度：O(V+E)\n"""},
    {"id": "ds_05", "title": "查找算法：二分、平衡二叉树、散列表", "content": """### 查找算法：二分、平衡二叉树、散列表\n\n**二分查找**\n- 条件：有序顺序表\n- 时间复杂度：O(log n)\n- 易错：边界条件，mid = left + (right - left) // 2\n\n**平衡二叉树（AVL）**\n- 定义：左右子树高度差绝对值小于等于1\n- 旋转操作：LL（右单旋）、RR（左单旋）、LR（先左后右双旋）、RL（先右后左双旋）\n- 插入：先 BST 插入，再调整平衡因子\n\n**B树与B+树**\n- B树：多路平衡查找树，节点包含数据\n- B+树：非叶子节点只含索引，所有数据在叶子\n- MySQL索引用B+树（叶子链表便于范围查询）\n\n**散列表**\n- 冲突处理：开放地址法（线性探测、二次探测）、链地址法\n- 装填因子 = n/m，影响查找效率\n- 开放地址法探测序列：h(key) = (h0 + f(i)) % m\n"""},
]

OS_TOPICS = [
    {"id": "os_01", "title": "进程与线程的区别及调度算法", "content": """### 进程与线程的区别及调度算法\n\n**进程与线程**\n- 进程：资源分配的基本单位，有独立地址空间\n- 线程：CPU调度的基本单位，同一进程内共享资源\n- 线程开销小：创建/切换/通信成本低\n\n**易错点**\n1. 进程切换vs线程切换：进程切换需要切换页表（TLB刷新），开销远大于线程切换\n2. 线程不能独立拥有资源，仍需依赖进程的资源空间\n3. 进程阻塞时同进程所有线程都阻塞（除主线程）\n\n**调度算法**\n- FCFS：简单，但 convoy effect（护航效应）\n- SJF：最短优先，可能产生饥饿\n- RR（时间片轮转）：适合分时系统，时间片大小关键\n- 优先级调度：静态/动态优先级，可能饥饿（老化解决）\n"""},
    {"id": "os_02", "title": "死锁：必要条件、预防、避免、检测", "content": """### 死锁：必要条件、预防、避免、检测\n\n**死锁必要条件（Coffman条件）**\n1. 互斥条件：资源一次只能被一个进程使用\n2. 占有并等待：进程持有资源同时请求其他资源\n3. 不可抢占：资源在释放前不可被抢占\n4. 循环等待：形成进程-资源的循环链\n\n**易错点**\n1. 死锁预防：破坏任一条件即可，但可能导致资源利用率低\n2. 死锁避免：Banker算法需要预先知道最大资源需求\n3. 死锁检测：允许发生，定期检测并恢复\n\n**银行家算法**\n- 核心思想：分配前检查安全性\n- Available：可用资源向量\n- Max：最大需求矩阵\n- Allocation：已分配矩阵\n- Need = Max - Allocation\n"""},
    {"id": "os_03", "title": "内存管理：分页、分段、虚拟内存", "content": """### 内存管理：分页、分段、虚拟内存\n\n**分页存储管理**\n- 逻辑地址 = 页号 + 页内偏移\n- 页表：页号到页框号映射\n- 快表（TLB）：高速缓存，减少访存次数\n- 二级页表：解决大逻辑地址空间问题\n\n**分段存储管理**\n- 逻辑地址 = 段号 + 段内偏移\n- 支持代码/数据/堆/栈等不同类型\n- 段表：段号到基址+限长\n\n**虚拟内存**\n- 页面置换算法：FIFO、LRU、LFU、Clock(NRU)\n- LRU近似：Clock算法（访问位+修改位）\n- 颠簸（Thrashing）：频繁页面调度\n\n**易错点**\n1. 分页无外部碎片，分段有外部碎片\n2. 快表命中只需一次访存，无TLB需两次\n3. 页面淘汰时，修改位决定是否写回磁盘\n"""},
    {"id": "os_04", "title": "文件系统和磁盘调度算法", "content": """### 文件系统和磁盘调度算法\n\n**文件系统**\n- 连续分配：顺序访问快，随机访问快，但有外碎片\n- 链接分配：消除碎片，但不支持随机访问\n- 索引分配：支持随机访问，索引块问题\n\n**Unix文件系统（inode）**\n- inode包含：文件元数据（大小、时间戳、权限）+ 数据块指针\n- 索引节点：直接指针12个，一级/二级/三级间接指针\n- 最大文件大小：取决于间接索引层级\n\n**磁盘调度算法**\n- FCFS：简单，效率低\n- SSTF（最短寻道）：可能饥饿\n- SCAN（电梯算法）：来回扫描\n- C-SCAN：单向扫描，返回快速\n- LOOK：SCAN的改进，不走到端点\n\n**易错点**\n1. 磁臂黏性（armstickiness）：SSTF易产生同一磁道反复调度\n2. 调度算法考虑：寻道时间、旋转延迟、传输时间\n"""},
]

NET_TOPICS = [
    {"id": "net_01", "title": "OSI七层模型与TCP/IP四层模型", "content": """### OSI七层模型与TCP/IP四层模型\n\n**OSI七层**\n1. 物理层：比特传输，接口标准（RJ45、光纤）\n2. 数据链路层：帧传输，MAC地址，交换机（CSMA/CD）\n3. 网络层：IP路由，路由器，三层交换\n4. 传输层：TCP/UDP，端到端可靠传输\n5. 会话层：会话管理，同步\n6. 表示层：数据格式转换，加密解密\n7. 应用层：HTTP、FTP、SMTP、DNS\n\n**TCP/IP四层**\n- 应用层（OSI 5-7）\n- 传输层（OSI 4）\n- 网际层（OSI 3）\n- 网络接口层（OSI 1-2）\n\n**易错点**\n1. 路由器工作在网络层（三层），交换机工作在数据链路层（二层）\n2. ARP将IP地址解析为MAC地址\n3. NAT工作在网络层，转换IP头部地址\n"""},
    {"id": "net_02", "title": "TCP三次握手四次挥手与可靠传输", "content": """### TCP三次握手四次挥手与可靠传输\n\n**三次握手**\n- 第一步：客户端SYN=1, seq=x → 服务端\n- 第二步：服务端SYN=1, ACK=1, seq=y, ack=x+1 → 客户端\n- 第三步：客户端ACK=1, seq=x+1, ack=y+1 → 服务端\n- 双方同步初始序列号(ISN)\n\n**四次挥手**\n- 客户端FIN → 服务端\n- 服务端ACK → 客户端\n- 服务端FIN → 客户端\n- 客户端ACK → 服务端\n- TIME_WAIT：2MSL等待，确保最后ACK到达\n\n**可靠传输机制**\n- 序号机制：每个字节编号\n- 确认机制（ACK）：累计确认\n- 超时重传：RTT估计\n- 流量控制：滑动窗口，接收方通告窗口\n- 拥塞控制：慢启动、拥塞避免、快速恢复\n\n**易错点**\n1. TIME_WAIT存在原因：防止延迟数据被后续连接接收；确保对方收到最终ACK\n2. 2MSL = 4分钟（通常），取决于实现\n3. 连接建立是三次握手，连接释放是四次挥手（因为半关闭）\n"""},
    {"id": "net_03", "title": "IP协议与路由算法", "content": """### IP协议与路由算法\n\n**IPv4首部**\n- 首部20字节（不含可选字段）\n- 字段：版本、首部长度、总长度、标识、标志、片偏移、TTL、协议、首部校验和、源IP、目的IP\n- TTL限制：最大255跳，每经过一个路由器减1\n\n**IP地址分类**\n- A类：1.0.0.0-126.255.255.255（/8）\n- B类：128.0.0.0-191.255.255.255（/16）\n- C类：192.0.0.0-223.255.255.255（/24）\n- D类：多播地址，E类：保留\n\n**特殊IP**\n- 127.0.0.1：本地回环\n- 0.0.0.0：默认路由\n- 255.255.255.255：受限广播\n- 私有IP：A类10.0.0.0/8，B类172.16.0.0/12，C类192.168.0.0/16\n\n**路由算法**\n- 距离向量（RIP）：跳数，周期性更新，易产生环路\n- 链路状态（OSPF）：Dijkatra算法，快速收敛，无环路\n- BGP：路径向量，用于AS间路由\n"""},
    {"id": "net_04", "title": "HTTP/HTTPS/DNS应用层协议", "content": """### HTTP/HTTPS/DNS应用层协议\n\n**HTTP**\n- 无状态协议，Web基础\n- 请求方法：GET（获取）、POST（提交）、PUT（更新）、DELETE（删除）\n- 状态码：1xx信息，2xx成功，3xx重定向，4xx客户端错误，5xx服务端错误\n- HTTP/1.0：短连接，每次请求建立新TCP\n- HTTP/1.1：持久连接（keep-alive），管道化\n- HTTP/2.0：多路复用，头部压缩，服务端推送\n\n**HTTPS**\n- HTTP + TLS/SSL，端口443\n- 加密：对称加密（内容）+ 非对称加密（密钥交换）+ 数字证书\n\n**DNS**\n- 层次结构：根域名服务器→顶级域名服务器→权威域名服务器\n- 记录类型：A（域名到IPv4）、AAAA（域名到IPv6）、CNAME（别名）、MX（邮件）、NS（nameserver）\n- 迭代查询vs递归查询\n- 缓存：浏览器/OS/递归服务器\n\n**易错点**\n1. HTTP是无状态的，Cookie/Session解决状态问题\n2. DNS使用UDP端口53（小型查询），大型查询用TCP\n3. HTTP GET POST 区别：GET参数URL长度有限制，POST在body中更安全\n"""},
]

ARCH_TOPICS = [
    {"id": "arch_01", "title": "数据的表示：原码、反码、补码、浮点数", "content": """### 数据的表示：原码、反码、补码、浮点数\n\n**定点数表示**\n- 原码：符号位+绝对值，0正1负\n- 反码：正数同原码，负数符号位不变，数值位取反\n- 补码：正数同原码，负数反码+1；补码比原码/反码多表示一个最小负数\n- 移码：补码符号位取反，用于浮点数阶码\n\n**易错点**\n1. 8位补码表示范围：-128 ~ +127（-2^7 ~ 2^7-1）\n2. 补码优点：统一加减法，0的表示唯一\n3. 符号扩展：正数高位补0，负数高位补1（保持数值不变）\n\n**浮点数表示**\n- IEEE 754：阶码（移码）+ 尾数（原码）\n- 单精度(32bit)：1位符号 + 8位阶码 + 23位尾数\n- 双精度(64bit)：1位符号 + 11位阶码 + 52位尾数\n- 规格化：尾数最高位必须为1（隐含存储）\n\n**IEEE 754特殊值**\n- 阶码全0+尾数非0：非规格化数（逐步溢出）\n- 阶码全1+尾数全0：无穷大\n- 阶码全1+尾数非0：NaN\n"""},
    {"id": "arch_02", "title": "指令系统：CISC与RISC、寻址方式", "content": """### 指令系统：CISC与RISC、寻址方式\n\n**CISC（复杂指令集）**\n- x86架构代表，指令长度不等，微程序控制\n- 寄存器少，内存访问指令多\n- 特点：指令丰富，但复杂指令执行效率低\n\n**RISC（精简指令集）**\n- ARM、MIPS代表，指令长度固定（32位），硬连线控制\n- 寄存器多（32个），Load/Store架构\n- 特点：指令简单，流水线效率高\n\n**寻址方式**\n1. 立即寻址：操作数在指令中\n2. 寄存器寻址：操作数在寄存器\n3. 直接寻址：操作数地址在指令中\n4. 间接寻址：操作数地址在寄存器/内存中\n5. 寄存器间接寻址：地址在寄存器\n6. 相对寻址：PC相对偏移（用于分支/跳转）\n7. 基址变址寻址：基址+变址+偏移\n\n**易错点**\n1. RISC不能执行复杂寻址（如x86的复杂地址模式）\n2. 相对寻址用于控制转移指令，跳转范围有限\n3. 堆栈寻址：隐含使用SP寄存器\n"""},
    {"id": "arch_03", "title": "存储系统：Cache、虚拟存储器、RAID", "content": """### 存储系统：Cache、虚拟存储器、RAID\n\n**Cache**\n- 映射方式：直接映射、组相联（N路组相联）\n- 写策略：写直达（write-through）+ 写回（write-back）\n- 写分配：先加载到Cache再写；写不分配：直接写内存\n- 替换算法：FIFO、LRU、随机\n- 命中率：h = Nc / (Nc + Nm)\n\n**Cache/主存层次**\n- 平均访问时间：Ta = h×Tc + (1-h)×(Tc + Tm)\n- Tc：Cache访问时间，Tm：主存访问时间\n\n**虚拟存储器**\n- 页式：固定大小页，管理灵活，但有内部碎片\n- 段式：按程序逻辑分段，便于共享保护\n- 段页式：结合两者优点\n\n**TLB（快表）**\n- 命中率接近100%，减少页表访问\n- 全相联/组相联\n\n**易错点**\n1. Cache对程序员透明，虚拟存储器对应用程序员透明\n2. TLB是页表的Cache，属于MMU\n3. 虚拟存储器以页为单位，交换单位是进程（整体换出）\n"""},
    {"id": "arch_04", "title": "CPU结构与指令流水线", "content": """### CPU结构与指令流水线\n\n**CPU基本组成**\n- 运算器：ALU、累加器、暂存寄存器、程序状态字（PSW）\n- 控制器：PC、IR、指令译码器、时序发生器\n- 寄存器组：通用寄存器、专用寄存器（PC、IR、PSW、SP）\n\n**指令周期**\n- 取指周期→间址周期（若有）→执行周期→中断周期（若有）\n- 机器周期：基本操作周期（通常=存取周期）\n- 时钟周期：最基本定时单位\n\n**指令流水线**\n- 五级流水线：取指(IF)、译码(ID)、执行(EX)、访存(MEM)、写回(WB)\n- 流水线吞吐率：T = n / (k+n-1)×Δt\n- 加速比：S = k×n / (k+n-1)\n\n**流水线冲突/冒险**\n1. 结构冒险：硬件资源冲突，解决：增加资源\n2. 数据冒险：数据依赖，解决：转发/暂停/编译器优化\n3. 控制冒险：分支指令，解决：分支预测\n\n**易错点**\n1. 数据冒险包括：RAW（读后写）、WAR（写后读）、WAW（写后写）\n2. 转发技术解决数据冒险，减少流水线暂停\n3. 流水线深度的增加受限于物理条件和冲突\n"""},
    {"id": "arch_05", "title": "总线系统与I/O控制方式", "content": """### 总线系统与I/O控制方式\n\n**总线分类**\n- 内部总线：CPU内部连接\n- 系统总线：CPU与内存/外设（数据总线、地址总线、控制总线）\n- 通信总线：I/O设备间通信（USB、PCIe）\n\n**总线仲裁**\n- 集中式仲裁：链式查询（优先级固定）、计数器定时查询、独立请求\n- 分布式仲裁：自举分布式、冲突检测分布式\n\n**总线性能**\n- 总线带宽 = 总线宽度(bit) × 时钟频率(Hz) / 传输次数\n- 总线复用：地址线与数据线分时复用\n- 猝发传输（Burst）：一次传输多个数据\n\n**I/O控制方式**\n1. 程序查询方式：CPU主动轮询，占用CPU\n2. 中断方式：设备主动通知CPU，CPU响应中断\n3. DMA方式：DMA控制器管理数据传送，CPU不参与\n4. 通道方式：独立处理器，执行通道程序\n\n**易错点**\n1. DMA与Cache一致性：DMA直接与内存交换数据，可能绕过Cache\n2. 中断向量号→中断向量表→中断服务程序入口地址\n3. 通道是更独立的I/O处理器，功能介于DMA和CPU之间\n"""},
]

ALL_TOPICS = {
    "knowledge_datastruct.md": DS_TOPICS,
    "knowledge_os.md": OS_TOPICS,
    "knowledge_network.md": NET_TOPICS,
    "knowledge_arch.md": ARCH_TOPICS,
}

# 搜索关键词映射：知识点ID -> 搜索关键词
SEARCH_KEYWORDS = {
    "ds_01": "408考研真题 数据结构 时间复杂度 空间复杂度 王道",
    "ds_02": "408考研真题 数据结构 栈 出栈序列 王道",
    "ds_03": "408考研真题 数据结构 二叉树 遍历 王道",
    "ds_04": "408考研真题 数据结构 DFS BFS 拓扑排序 王道",
    "ds_05": "408考研真题 数据结构 二分查找 AVL B树 散列表 王道",
    "os_01": "408考研真题 操作系统 进程线程 调度算法 王道",
    "os_02": "408考研真题 操作系统 死锁 银行家算法 王道",
    "os_03": "408考研真题 操作系统 内存管理 分页分段 虚拟内存 王道",
    "os_04": "408考研真题 操作系统 文件系统 磁盘调度 inode 王道",
    "net_01": "408考研真题 计算机网络 OSI七层 TCPIP 四层模型 王道",
    "net_02": "408考研真题 计算机网络 TCP三次握手 四次挥手 王道",
    "net_03": "408考研真题 计算机网络 IP协议 路由算法 王道",
    "net_04": "408考研真题 计算机网络 HTTP HTTPS DNS 王道",
    "arch_01": "408考研真题 计算机组成原理 原码补码 浮点数 IEEE754 王道",
    "arch_02": "408考研真题 计算机组成原理 CISC RISC 寻址方式 王道",
    "arch_03": "408考研真题 计算机组成原理 Cache 虚拟存储器 TLB 王道",
    "arch_04": "408考研真题 计算机组成原理 CPU流水线 指令周期 王道",
    "arch_05": "408考研真题 计算机组成原理 总线 I/O DMA 中断 王道",
}

def log(msg):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {msg}\n")

def load_tracker():
    p = os.path.join(TASKS_DIR, "study_tracker.json")
    if os.path.exists(p):
        with open(p, "r", encoding="utf-8") as f:
            return json.load(f)
    return {"learned_ids": [], "study_count": 0, "last_study": None, "last_topic_id": None, "stats": {}}

def save_tracker(t):
    p = os.path.join(TASKS_DIR, "study_tracker.json")
    with open(p, "w", encoding="utf-8") as f:
        json.dump(t, f, ensure_ascii=False, indent=2)

def mid(s):
    return hashlib.md5(s.encode()).hexdigest()[:8]

def search_exam_questions(topic_id):
    """使用Tavily搜索相关考研408真题"""
    keyword = SEARCH_KEYWORDS.get(topic_id, "")
    if not keyword:
        return None
    
    log(f"开始搜索真题：{keyword}")
    
    try:
        import urllib.request, urllib.parse, json as json_mod
        
        query = {
            "api_key": TAVILY_KEY,
            "query": keyword,
            "max_results": 10,
            "search_depth": "advanced",
            "include_answer": False,
        }
        
        data = json_mod.dumps(query).encode("utf-8")
        req = urllib.request.Request(
            "https://api.tavily.com/search",
            data=data,
            headers={"Content-Type": "application/json"},
            method="POST"
        )
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json_mod.loads(resp.read().decode("utf-8"))
        
        log(f"搜索完成，获得 {len(result.get('results', []))} 条结果")
        return result
    except Exception as e:
        log(f"搜索失败：{e}")
        return None

def parse_search_results(result):
    """解析搜索结果，提取定理/技巧和典型题目"""
    if not result:
        return None
    
    theorems = []
    examples = []
    
    # 提取AI答案
    ai_answer = result.get("answer", "")
    if ai_answer and len(ai_answer) > 20:
        theorems.append(ai_answer[:500])
    
    # 从搜索结果中提取相关片段
    for r in result.get("results", []):
        snippet = r.get("content", "")
        title = r.get("title", "")
        if len(snippet) > 30:
            eng_chars = len(re.findall(r"[a-zA-Z]", snippet))
            total_chars = len(snippet)
            if eng_chars / total_chars > 0.6 and len(snippet) < 100:
                continue
            # 优先提取包含中文
            if any(k in snippet for k in ["定理", "技巧", "规律", "结论", "要点", "公式", "性质"]):
                theorems.append(f"[{title}] {snippet[:300]}")
            elif len(examples) < 3 and any(k in snippet for k in ["题目", "例题", "题", "选项", "答案"]):
                examples.append(f"[{title}] {snippet[:400]}")
    
    return {
        "theorems": theorems[:5],
        "examples": examples[:3],
    }

def append_search_results(fname, topic_id):
    """搜索并追加真题相关内容"""
    result = search_exam_questions(topic_id)
    if not result:
        log("未获得搜索结果，跳过")
        return
    
    parsed = parse_search_results(result)
    if not parsed or (not parsed["theorems"] and not parsed["examples"]):
        log("解析结果为空，跳过")
        return
    
    fp = os.path.join(TASKS_DIR, fname)
    with open(fp, "a", encoding="utf-8") as f:
        f.write("\n\n---\n### 🔍 考研真题相关定理与技巧\n\n")
        
        if parsed["theorems"]:
            f.write("**定理/技巧：**\n")
            for i, t in enumerate(parsed["theorems"], 1):
                f.write(f"{i}. {t}\n")
            f.write("\n")
        
        if parsed["examples"]:
            f.write("**典型真题摘录：**\n")
            for i, ex in enumerate(parsed["examples"], 1):
                f.write(f"{i}. {ex}\n\n")
        
        f.write("---\n")
    
    log(f"真题内容已追加到：{fname}")

def select_topic(tracker):
    learned = set(tracker.get("learned_ids", []))
    last_topic_id = tracker.get("last_topic_id")

    order = list(ALL_TOPICS.keys())
    last_file = None
    if last_topic_id:
        for f, tops in ALL_TOPICS.items():
            if any(t["id"] == last_topic_id for t in tops):
                last_file = f
                break

    if last_file and last_file in order:
        last_idx = order.index(last_file)
        search_order = order[last_idx+1:] + order[:last_idx+1]
    else:
        search_order = order

    for fname in search_order:
        tops = ALL_TOPICS[fname]
        candidates = [t for t in tops if t["id"] not in learned]
        if candidates:
            return fname, random.choice(candidates)

    log("四科全部学完一轮！重置学习进度，进入下一轮...")
    tracker["learned_ids"] = []
    tracker["stats"] = {}
    save_tracker(tracker)
    fname, tops = order[0], ALL_TOPICS[order[0]]
    return fname, random.choice(tops)

def append_note(fname, topic):
    fp = os.path.join(TASKS_DIR, fname)
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    hdr = f"\n\n{'='*60}\n📅 学习时间：{ts}\n🏷️  知识点ID：{topic['id']} (MD5: {mid(topic['id'])})\n{'='*60}\n\n"
    with open(fp, "a", encoding="utf-8") as f:
        f.write(hdr)
        f.write(topic["content"])

def init_files():
    for fname, content in {
        "knowledge_datastruct.md": "# 📚 数据结构知识点笔记\n\n> 考研408 - 数据结构\n",
        "knowledge_os.md": "# 📚 操作系统知识点笔记\n\n> 考研408 - 操作系统\n",
        "knowledge_network.md": "# 📚 计算机网络知识点笔记\n\n> 考研408 - 计算机网络\n",
        "knowledge_arch.md": "# 📚 计算机组成原理知识点笔记\n\n> 考研408 - 计算机组成原理\n",
    }.items():
        p = os.path.join(TASKS_DIR, fname)
        if not os.path.exists(p):
            with open(p, "w", encoding="utf-8") as f:
                f.write(content)
    tp = os.path.join(TASKS_DIR, "study_tracker.json")
    if not os.path.exists(tp):
        save_tracker({"learned_ids": [], "study_count": 0, "last_study": None, "last_topic_id": None, "stats": {}})

def run_study():
    log("=== 开始学习任务 ===")
    init_files()
    tracker = load_tracker()

    last_study = tracker.get("last_study")
    if last_study:
        try:
            last_dt = datetime.fromisoformat(last_study)
            if datetime.now() - last_dt < timedelta(hours=1):
                log(f"1小时内已学习过（{last_dt.strftime('%H:%M')}），跳过本次")
                log("=== 学习任务跳过 ===")
                return
        except Exception:
            pass

    fname, topic = select_topic(tracker)
    log(f"选择科目文件：{fname}")
    log(f"学习知识点：{topic['title']} (ID: {topic['id']})")
    append_note(fname, topic)
    log(f"笔记已追加到：{TASKS_DIR}/{fname}")
    
    # 追加真题搜索结果
    append_search_results(fname, topic["id"])
    
    tracker["learned_ids"].append(topic["id"])
    tracker["last_topic_id"] = topic["id"]
    tracker["study_count"] += 1
    tracker["last_study"] = datetime.now().isoformat()
    course = fname.replace("knowledge_", "").replace(".md", "")
    tracker["stats"][course] = tracker["stats"].get(course, 0) + 1
    save_tracker(tracker)
    log(f"追踪器已更新 - 已学习 {tracker['study_count']} 个知识点")
    log("=== 学习任务完成 ===")

if __name__ == "__main__":
    run_study()
