diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b24bc7..4253620 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 本文件对应技能市场 **「更新日志」** Tab。 +## 1.1.29 + +- **修复「老 DB 没 download_tasks 表导致入队失败」问题**: + - 用户本地 `data/alert-contract-expiring-hr.db` 是 v1.1.17 之前 init 的(只有前 6 张表),没跑过 v1.1.18 的 init-db → 没 `download_tasks` 表 + - `_enqueue_download_tasks_safe` 在 `owns_conn=True` 路径下,先 idempotent `CREATE TABLE IF NOT EXISTS download_tasks ...` + 2 索引,再 INSERT + - 解决 `OperationalError: no such table: download_tasks` + 下载队列 `failed=3` +- 新增测试 `enqueue_safe_auto_create_table_v1_29` 模拟「老 DB 没建过 download_tasks」场景(用 monkeypatch `rp.get_db_path` + `db.connection.get_db_path`),合计 67/67 + ## 1.1.28 - **彻底修复「SPA 二级登录页卡顿」**: diff --git a/SKILL.md b/SKILL.md index 24599c3..c4d0d58 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: 合同到期智能预警与续聘辅助 description: "当用户说要扫描即将到期的合同、查 90/60/30 天预警名单、给某位教师生成续聘任务书、下发单位版续聘清单、从 SmartCampus HR 平台抓合同数据、解析续聘材料 PDF/Word、查看本月合同到期人员清单时,加载本技能。支持定时扫描(每日凌晨)+ 数据管理 + Agent 三入口,覆盖合同到期智能预警与续聘辅助全流程。" -version: 1.1.28 +version: 1.1.29 author: 深圳匠厂科技有限公司 metadata: openclaw: diff --git a/scripts/service/platform_collector.py b/scripts/service/platform_collector.py index f9e59f4..585e265 100644 --- a/scripts/service/platform_collector.py +++ b/scripts/service/platform_collector.py @@ -1358,6 +1358,35 @@ def _enqueue_download_tasks_safe( owns_conn = conn is None if owns_conn: conn = get_connection() + # v1.1.29:idempotent 表创建 —— 用户没跑 init-db 时,download_tasks 表不存在, + # 首次采集直接建表(CREATE TABLE IF NOT EXISTS)而不是报 "no such table" + try: + conn.executescript( + """ + CREATE TABLE IF NOT EXISTS download_tasks ( + task_id TEXT PRIMARY KEY, + contract_id TEXT NOT NULL, + kind TEXT NOT NULL DEFAULT 'platform_button', + status TEXT NOT NULL DEFAULT 'pending', + attempts INTEGER NOT NULL DEFAULT 0, + error_msg TEXT, + enqueued_at TEXT NOT NULL, + started_at TEXT, + finished_at TEXT, + source_path TEXT, + result_path TEXT, + FOREIGN KEY (contract_id) REFERENCES contracts(contract_id) + ); + CREATE INDEX IF NOT EXISTS idx_dlq_status ON download_tasks(status); + CREATE INDEX IF NOT EXISTS idx_dlq_contract ON download_tasks(contract_id); + """ + ) + conn.commit() + except Exception as ensure_err: + logger.warning( + "platform_collector: ensure download_tasks 表创建失败(继续走入队流程): %s", + ensure_err, + ) try: for cid in contract_ids: try: diff --git a/scripts/util/constants.py b/scripts/util/constants.py index 952f363..0815697 100644 --- a/scripts/util/constants.py +++ b/scripts/util/constants.py @@ -6,7 +6,7 @@ """ SKILL_SLUG = "alert-contract-expiring-hr" -SKILL_VERSION = "1.1.28" +SKILL_VERSION = "1.1.29" LOG_LOGGER_NAME = "openclaw.skill.alert_contract_expiring_hr" PLATFORM_KIT_MIN_VERSION = "1.2.11" diff --git a/tests/run_tests.py b/tests/run_tests.py index b29f5ab..b3d6f29 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1271,6 +1271,80 @@ def test_enqueue_download_tasks_safe_empty_list() -> tuple[bool, str]: return True, "空列表 no-op OK" +def test_enqueue_download_tasks_safe_auto_create_table_v1_29() -> tuple[bool, str]: + """v1.1.29:用户没跑 init-db 时,首次调用 `_enqueue_download_tasks_safe` + 不传 conn(owns_conn=True),函数内部 idempotent 创建 download_tasks 表 + 而非抛 "no such table"。 + """ + import os + import sqlite3 + import tempfile + from pathlib import Path + + from service.platform_collector import _enqueue_download_tasks_safe + + # 1. 临时文件 DB,**只建 contracts 表**(模拟老 DB 没 download_tasks) + with tempfile.TemporaryDirectory() as tmp: + tmp_db = Path(tmp) / "old.db" + # 重定向 JIANGCHANG_DATA_ROOT 或 get_db_path 用临时路径 + # 这里用 monkeypatch get_db_path + get_connection + from util import runtime_paths as rp + from db import connection as dbconn + + orig_rp_db_path = rp.get_db_path + orig_dbconn_db_path = dbconn.get_db_path + rp.get_db_path = lambda: tmp_db # type: ignore[assignment] + dbconn.get_db_path = lambda: tmp_db # type: ignore[assignment] + try: + # 模拟老 DB:只有 contracts 表(v1.1.17 之前 init 过) + _tmp_conn = sqlite3.connect(str(tmp_db)) + try: + _tmp_conn.executescript( + """ + CREATE TABLE contracts ( + contract_id TEXT PRIMARY KEY, + teacher_name TEXT, unit_id TEXT, unit_name TEXT, + position TEXT, contract_type TEXT, contract_start TEXT, + contract_end TEXT, trial_period_end TEXT, + status TEXT DEFAULT '可续聘', + last_material_id TEXT, renewed_at TEXT + ); + INSERT INTO contracts + (contract_id, teacher_name, unit_id, unit_name, + position, contract_type, contract_start, + contract_end, trial_period_end) + VALUES ('ENQ-CREATE-1', '测试', 'U1', '物理学院', + '实验师', '专任教师聘用合同', + '2024-01-01', '2026-12-31', '2024-04-01'); + """ + ) + _tmp_conn.commit() + finally: + _tmp_conn.close() + + # 2. 不传 conn 调用(owns_conn=True,会走 get_connection → 触发 ensure 建表) + summary = _enqueue_download_tasks_safe(["ENQ-CREATE-1"]) + if summary.get("failed", 0) > 0: + return False, f"应自动建表后入队成功,但 failed={summary}" + + # 3. 验证表确实被自动创建了 + conn = sqlite3.connect(str(tmp_db)) + try: + count = conn.execute( + "SELECT COUNT(*) FROM download_tasks" + ).fetchone()[0] + finally: + conn.close() + + if count != 1: + return False, f"download_tasks 应有 1 行,实际 {count}" + + return True, f"ensure 建表 + 入队 OK(enqueued={summary.get('enqueued')})" + finally: + rp.get_db_path = orig_rp_db_path # type: ignore[assignment] + dbconn.get_db_path = orig_dbconn_db_path # type: ignore[assignment] + + # ---------- v1.1.19: _wait_for_url_stable + login flow ---------- @@ -1916,6 +1990,7 @@ def main() -> int: _run("download_tasks/enqueue_safe_idempotent", test_enqueue_download_tasks_safe_idempotent) _run("download_tasks/enqueue_safe_fk_failure", test_enqueue_download_tasks_safe_fk_failure) _run("download_tasks/enqueue_safe_empty_list", test_enqueue_download_tasks_safe_empty_list) + _run("download_tasks/enqueue_safe_auto_create_table_v1_29", test_enqueue_download_tasks_safe_auto_create_table_v1_29) _run("login_flow/wait_url_stable_immediate", test_wait_for_url_stable_immediate_match) _run("login_flow/wait_url_stable_transition", test_wait_for_url_stable_transition) _run("login_flow/wait_url_stable_timeout", test_wait_for_url_stable_timeout)