chore: auto release commit (2026-09-15 00:32:44)
All checks were successful
技能自动化发布 / release (push) Successful in 7s
All checks were successful
技能自动化发布 / release (push) Successful in 7s
This commit is contained in:
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user