新增(scripts/ 分层完整实现):
- scripts/util/{constants,date_util,runtime_paths,logging_setup}.py
- scripts/db/{connection,seed_data,repository}.py(SQLite + 5 张表 + 50 份样例合同)
- scripts/service/{scanner,task_generator,dispatcher,html_renderer}.py
- scripts/cli/app.py(argparse 子命令)
- scripts/templates/task.html.j2(Jinja2 渲染,无 jinja2 时降级到 str.replace)
- examples/{contracts_sample,performance_sample}.csv(本地样例数据)
修改:
- scripts/main.py:argparse + jiangchang_skill_core 降级
- SKILL.md:version 1.0.1 → 1.1.0
- CHANGELOG.md:加 v1.1.0 条目
- .env.example:加 DB_PATH / HTML_OUTPUT_DIR / JIANGCHANG_DATA_DIR
本地验证(9 个 CLI 命令全部 exit code = 0):
- health / version / config-path / init-db:基础 4 命令
- scan-expiring-contracts --thresholds 90,60,30:42 份可续聘 → 红 10 / 橙 10 / 黄 10,生成 30 任务
- render-html:写 output/<task_id>.html(4.9 KB)
- generate-renewal-task:单份合同任务书生成
- mark-contract-renewed:状态机更新 + 关闭 open alerts
- dispatch-task-notice:按单位/月份派发 payload(dry-run 模式)
业务规则(AI 续聘建议本地版,规则顺序判断):
1. 师德 < 60 → non_renew
2. 近 3 年考核有不合格(< 60)→ observe
3. 全部 >= 80 且逐年上升 → renew
4. 其他 → observe
Co-Authored-By: Claude Code <noreply@anthropic.com>
124 lines
3.6 KiB
Python
124 lines
3.6 KiB
Python
"""派发器:按单位/月份汇总任务 → 生成派发 payload JSON → 写派发日志。
|
||
|
||
v1.1.0 dry-run 模式:payload 写 data/dispatch/{unit}_{month}.json,
|
||
但**不真正调用企业微信 API**。开发者后续接入:
|
||
- 真实 API:调用 WECOM_API_BASE_URL + WECOM_API_TOKEN
|
||
- 在 dispatch_log 写 status='sent' 而非 'dry_run'
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import sqlite3
|
||
import uuid
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
from db import repository as repo
|
||
from util.date_util import parse_iso_date
|
||
from util.logging_setup import get_skill_logger
|
||
from util.runtime_paths import get_dispatch_dir
|
||
|
||
logger = get_skill_logger()
|
||
|
||
|
||
def build_payload(
|
||
conn: sqlite3.Connection,
|
||
unit_id: str,
|
||
month: str = "",
|
||
) -> dict:
|
||
"""构建派发 payload:含单位、月份、任务清单、收件人、消息正文。"""
|
||
rows = repo.list_open_alerts_by_unit_month(conn, unit_id, month)
|
||
|
||
# 单位名(取第一条)
|
||
unit_name = rows[0]["unit_name"] if rows else "(无)"
|
||
|
||
items = []
|
||
for r in rows:
|
||
items.append({
|
||
"contract_id": r["contract_id"],
|
||
"teacher_name": r["teacher_name"],
|
||
"alert_level": r["alert_level"],
|
||
"days_to_expire": r["days_to_expire"],
|
||
"contract_end": r["contract_end"],
|
||
})
|
||
|
||
if month:
|
||
title = f"{unit_name} {month} 合同到期人员清单及续聘建议"
|
||
else:
|
||
title = f"{unit_name} 待续聘合同清单"
|
||
|
||
body_lines = [
|
||
title,
|
||
"",
|
||
f"共 {len(items)} 份合同需在到期前办理续聘手续。",
|
||
"",
|
||
]
|
||
for it in items:
|
||
level_zh = {"yellow": "黄色", "orange": "橙色", "red": "红色"}.get(
|
||
it["alert_level"], it["alert_level"]
|
||
)
|
||
body_lines.append(
|
||
f"- {it['teacher_name']}(合同至 {it['contract_end']},{level_zh}预警,距到期 {it['days_to_expire']} 天)"
|
||
)
|
||
body_lines.append("")
|
||
body_lines.append("请人事秘书尽快通知相关教师备齐续聘材料。")
|
||
|
||
payload = {
|
||
"schema_version": 1,
|
||
"channel": "wecom",
|
||
"dry_run": True,
|
||
"to": {
|
||
"unit_id": unit_id,
|
||
"unit_name": unit_name,
|
||
"role": "人事秘书",
|
||
},
|
||
"title": title,
|
||
"body": "\n".join(body_lines),
|
||
"items": items,
|
||
"generated_at": datetime.now().isoformat(timespec="seconds"),
|
||
}
|
||
return payload
|
||
|
||
|
||
def dispatch(
|
||
conn: sqlite3.Connection,
|
||
unit_id: str,
|
||
month: str = "",
|
||
) -> dict:
|
||
"""派发 dry-run:写 payload 文件 + 写 dispatch_log。"""
|
||
payload = build_payload(conn, unit_id, month)
|
||
items = payload["items"]
|
||
task_ids = [f"T-{it['contract_id']}" for it in items]
|
||
|
||
dispatch_dir = get_dispatch_dir()
|
||
dispatch_dir.mkdir(parents=True, exist_ok=True)
|
||
|
||
month_part = month or "all"
|
||
payload_path = dispatch_dir / f"{unit_id}_{month_part}.json"
|
||
payload_path.write_text(
|
||
json.dumps(payload, ensure_ascii=False, indent=2),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
log_id = f"L-{uuid.uuid4().hex[:8]}"
|
||
repo.insert_dispatch_log(
|
||
conn, log_id, unit_id, month or "all", task_ids, str(payload_path),
|
||
status="dry_run",
|
||
)
|
||
conn.commit()
|
||
|
||
logger.info(
|
||
"dispatcher: unit=%s month=%s items=%d payload=%s log=%s",
|
||
unit_id, month or "(all)", len(items), payload_path, log_id,
|
||
)
|
||
return {
|
||
"log_id": log_id,
|
||
"unit_id": unit_id,
|
||
"unit_name": payload["to"]["unit_name"],
|
||
"month": month or "(all)",
|
||
"item_count": len(items),
|
||
"payload_path": str(payload_path),
|
||
"status": "dry_run",
|
||
}
|