chore: auto release commit (2026-09-15 00:32:44)
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
2026-09-15 00:32:44 +08:00
parent 693a5e4a53
commit 293b7d1591
15 changed files with 1715 additions and 16 deletions

View File

@@ -90,6 +90,25 @@ DDL_STATEMENTS: tuple[str, ...] = (
);
""",
"CREATE INDEX IF NOT EXISTS idx_dispatch_unit_month ON dispatch_log(unit_id, month);",
# 7. download_tasks (v1.1.18)
"""
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);",
)
@@ -131,6 +150,7 @@ def init_db(db_path: Path | None = None) -> dict[str, int]:
"renewal_tasks": conn.execute("SELECT COUNT(*) AS n FROM renewal_tasks").fetchone()["n"],
"dispatch_log": conn.execute("SELECT COUNT(*) AS n FROM dispatch_log").fetchone()["n"],
"materials_content": conn.execute("SELECT COUNT(*) AS n FROM materials_content").fetchone()["n"],
"download_tasks": conn.execute("SELECT COUNT(*) AS n FROM download_tasks").fetchone()["n"],
}
return counts
finally:
@@ -140,5 +160,5 @@ def init_db(db_path: Path | None = None) -> dict[str, int]:
def table_names() -> Iterable[str]:
return (
"contracts", "performance_records", "alerts", "renewal_tasks",
"dispatch_log", "materials_content",
"dispatch_log", "materials_content", "download_tasks",
)

View File

@@ -294,6 +294,59 @@ def list_dispatch_logs(conn: sqlite3.Connection) -> list[sqlite3.Row]:
# ---------- materials_content(v1.1.1 新增) ----------
def upsert_contract_from_csv_row(
conn: sqlite3.Connection, row: dict[str, str]
) -> None:
"""从 CSV 字典 upsert 一份合同。CSV 列名必须与 contracts 表列一致(或常见别名)。
缺字段兜底值:
employee_id = contract_id
teacher_name = ""
unit_id = "U-UNKNOWN"
unit_name = ""
position = ""
contract_type = ""
contract_start = "1970-01-01"
contract_end = "2099-12-31"
trial_period_end = contract_end
status = "可续聘"
"""
cid = row.get("contract_id") or row.get("合同ID") or ""
if not cid:
raise ValueError("CSV row missing contract_id / 合同ID")
conn.execute(
"""
INSERT INTO contracts
(contract_id, employee_id, teacher_name, unit_id, unit_name,
position, contract_type, contract_start, contract_end,
trial_period_end, status, last_material_id, renewed_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL)
ON CONFLICT(contract_id) DO UPDATE SET
teacher_name = excluded.teacher_name,
unit_name = excluded.unit_name,
position = excluded.position,
contract_type = excluded.contract_type,
contract_start = excluded.contract_start,
contract_end = excluded.contract_end,
trial_period_end = excluded.trial_period_end,
status = excluded.status
""",
(
cid,
row.get("employee_id") or row.get("工号") or cid,
row.get("teacher_name") or row.get("教职工姓名") or "",
row.get("unit_id") or row.get("单位ID") or "U-UNKNOWN",
row.get("unit_name") or row.get("单位") or "",
row.get("position") or row.get("岗位") or "",
row.get("contract_type") or row.get("合同类型") or "",
row.get("contract_start") or row.get("合同起始日") or "1970-01-01",
row.get("contract_end") or row.get("合同到期日") or "2099-12-31",
row.get("trial_period_end") or row.get("试用期截止") or row.get("contract_end") or "2099-12-31",
row.get("status") or "可续聘",
),
)
def list_materials_for_contract(
conn: sqlite3.Connection, contract_id: str
) -> list[sqlite3.Row]:
@@ -373,3 +426,124 @@ def summarize_materials(
if len(summary) > 16000:
summary = summary[:16000] + "\n...(材料过长已截断)"
return (summary, file_names)
# ---------- download_tasks(v1.1.18 新增) ----------
def enqueue_download_task(
conn: sqlite3.Connection,
contract_id: str,
kind: str = "platform_button",
) -> str:
"""入队一个下载任务,返回 task_id。
已存在 (contract_id, kind, status ∈ {pending, running}) 时跳过(返回已存在的 task_id)。
"""
import uuid
existing = conn.execute(
"""SELECT task_id FROM download_tasks
WHERE contract_id = ? AND kind = ? AND status IN ('pending', 'running')
LIMIT 1""",
(contract_id, kind),
).fetchone()
if existing:
return existing["task_id"]
task_id = f"DLQ-{uuid.uuid4().hex[:12]}"
conn.execute(
"""
INSERT INTO download_tasks
(task_id, contract_id, kind, status, attempts, enqueued_at)
VALUES (?, ?, ?, 'pending', 0, ?)
""",
(task_id, contract_id, kind, datetime.now().isoformat(timespec="seconds")),
)
return task_id
def _read_max_attempts(conn: sqlite3.Connection) -> int:
"""读 DOWNLOAD_QUEUE_MAX_ATTEMPTS env(默认 3)。"""
import os
return int(os.environ.get("DOWNLOAD_QUEUE_MAX_ATTEMPTS", "3"))
def claim_pending_tasks(
conn: sqlite3.Connection, limit: int, ids_filter: list[str] | None = None,
) -> list[sqlite3.Row]:
"""原子领取一批 pending 任务,标记为 running。
- limit: 最多领取条数
- ids_filter: 若给出,仅从这些 task_id 中领
"""
max_attempts = _read_max_attempts(conn)
if ids_filter:
placeholders = ",".join("?" for _ in ids_filter)
rows = conn.execute(
f"""SELECT * FROM download_tasks
WHERE status = 'pending' AND attempts < ?
AND task_id IN ({placeholders})
ORDER BY enqueued_at ASC LIMIT ?""",
[max_attempts, *ids_filter, limit],
).fetchall()
else:
rows = conn.execute(
"""SELECT * FROM download_tasks
WHERE status = 'pending' AND attempts < ?
ORDER BY enqueued_at ASC LIMIT ?""",
(max_attempts, limit),
).fetchall()
if not rows:
return []
now = datetime.now().isoformat(timespec="seconds")
for r in rows:
conn.execute(
"UPDATE download_tasks SET status = 'running', started_at = ? WHERE task_id = ?",
(now, r["task_id"]),
)
conn.commit()
return rows
def mark_task_success(
conn: sqlite3.Connection, task_id: str, result_path: str,
) -> None:
conn.execute(
"""UPDATE download_tasks
SET status = 'success', result_path = ?, finished_at = ?
WHERE task_id = ?""",
(result_path, datetime.now().isoformat(timespec="seconds"), task_id),
)
def mark_task_failed(
conn: sqlite3.Connection, task_id: str, error_msg: str,
) -> None:
"""失败一次:attempts+1,未超 max_attempts 则回退 pending,否则标 failed。"""
max_attempts = _read_max_attempts(conn)
now = datetime.now().isoformat(timespec="seconds")
conn.execute(
"""UPDATE download_tasks
SET status = CASE WHEN attempts + 1 >= ? THEN 'failed' ELSE 'pending' END,
attempts = attempts + 1,
error_msg = ?,
finished_at = ?
WHERE task_id = ?""",
(max_attempts, error_msg, now, task_id),
)
def list_recent_tasks(
conn: sqlite3.Connection, limit: int = 50
) -> list[sqlite3.Row]:
return conn.execute(
"SELECT * FROM download_tasks ORDER BY enqueued_at DESC LIMIT ?",
(limit,),
).fetchall()
def count_tasks_by_status(conn: sqlite3.Connection) -> dict[str, int]:
rows = conn.execute(
"SELECT status, COUNT(*) AS n FROM download_tasks GROUP BY status"
).fetchall()
return {r["status"]: r["n"] for r in rows}