chore: auto release commit (2026-09-14 16:38:51)

This commit is contained in:
2026-09-14 16:38:51 +08:00
parent 1d78e542ca
commit daab294af6
6 changed files with 659 additions and 3 deletions

View File

@@ -4,6 +4,8 @@
import sys
from pathlib import Path
from util.constants import SKILL_SLUG, SKILL_VERSION
# 占位:开发者后续接入 jiangchang_skill_core
# from jiangchang_skill_core import ...
@@ -12,14 +14,14 @@ BASE_DIR = Path(__file__).resolve().parent.parent
def health():
"""运行检查(占位)。"""
print(f"check-course-syllabus-aa health check (placeholder)")
print(f"{SKILL_SLUG} health check (placeholder)")
print(f"base_dir: {BASE_DIR}")
return 0
def version():
"""查看版本(占位)。"""
print("check-course-syllabus-aa v1.0.1")
print(f"{SKILL_SLUG} v{SKILL_VERSION}")
return 0

51
scripts/util/constants.py Normal file
View File

@@ -0,0 +1,51 @@
"""技能标识、版本与阈值常量。
复制到新 skill 时必须修改:
- SKILL_SLUG 与 SKILL.md metadata.openclaw.slug 保持一致
- SKILL_VERSION 与 SKILL.md version 保持一致
"""
SKILL_SLUG = "check-course-syllabus-aa"
SKILL_VERSION = "1.0.2"
LOG_LOGGER_NAME = "openclaw.skill.check_course_syllabus_aa"
PLATFORM_KIT_MIN_VERSION = "1.2.11"
# 体检评分阈值(百分制)
# SCORE_PASS_THRESHOLD:>= 该值建议「直接送审」
# SCORE_MODIFY_THRESHOLD:>= 该值建议「修改后送审」;< 该值建议「重写」
# 与 .env.example 中的 SCORE_PASS_THRESHOLD / SCORE_MODIFY_THRESHOLD 保持一致
DEFAULT_SCORE_PASS_THRESHOLD = 85
DEFAULT_SCORE_MODIFY_THRESHOLD = 60
# 三个子项加权后的总评分(百分制,0~100)
TOTAL_SCORE_MIN = 0
TOTAL_SCORE_MAX = 100
# 体检最终状态标签(与 .env / CLI 输出保持稳定字符串)
AUDIT_STATUS_PASS = "pass" # 直接送审
AUDIT_STATUS_MODIFY = "modify" # 修改后送审
AUDIT_STATUS_REWRITE = "rewrite" # 需重写
AUDIT_STATUSES = (AUDIT_STATUS_PASS, AUDIT_STATUS_MODIFY, AUDIT_STATUS_REWRITE)
# 子项维度
DIMENSION_FORMAT = "format" # 格式结构
DIMENSION_MATRIX = "matrix" # 课程目标与毕业要求矩阵
DIMENSION_IDEOLOGY = "ideology" # 课程思政融合度
DIMENSIONS = (DIMENSION_FORMAT, DIMENSION_MATRIX, DIMENSION_IDEOLOGY)
# 默认子项权重(必须和为 1.0)
DEFAULT_WEIGHT_FORMAT = 0.3
DEFAULT_WEIGHT_MATRIX = 0.4
DEFAULT_WEIGHT_IDEOLOGY = 0.3
# 语义相似度阈值:课程目标与毕业要求指标点匹配判定
DEFAULT_MATRIX_MATCH_THRESHOLD = 0.7
# 单份大纲体检默认超时(秒)
DEFAULT_AUDIT_TIMEOUT_SEC = 60
# 错误前缀(统一为 ERROR: 起头)
ERROR_PREFIX = "ERROR: "
# 业务行一次性体检(无生命周期 / 不重复处理)
NO_LIFECYCLE = True

154
scripts/util/score_util.py Normal file
View File

@@ -0,0 +1,154 @@
"""课程教学大纲体检 - 纯函数工具层。
设计原则:
- 仅依赖标准库;不依赖 jiangchang_skill_core / jinja2 / python-docx
- 这些函数是 service 层 / CLI 入口都要复用的纯计算,便于 unit test
- 输入允许 str / list[str] / dict;返回稳定标签 / 数值
模块内函数:
- clamp_score(value): 把 0~100 之外的分数裁剪到合法区间
- weighted_total(format_score, matrix_score, ideology_score, weights): 加权总分
- classify_audit_status(score, pass_thr, modify_thr): 根据总分给最终建议
- parse_score_threshold(env_value, default): 从 env 字符串读取阈值
- parse_ideology_keywords(env_value, default_keywords): 解析思政关键词 JSON 数组
- count_keyword_hits(text, keywords): 统计文本中思政关键词命中数
"""
from __future__ import annotations
import json
from typing import Iterable, Mapping
from util.constants import (
AUDIT_STATUS_MODIFY,
AUDIT_STATUS_PASS,
AUDIT_STATUS_REWRITE,
DEFAULT_WEIGHT_FORMAT,
DEFAULT_WEIGHT_IDEOLOGY,
DEFAULT_WEIGHT_MATRIX,
TOTAL_SCORE_MAX,
TOTAL_SCORE_MIN,
)
def clamp_score(value, min_val: float = TOTAL_SCORE_MIN, max_val: float = TOTAL_SCORE_MAX) -> float:
"""把分数裁剪到 [min_val, max_val] 区间。None / 非数字输入 → min_val。"""
try:
v = float(value)
except (TypeError, ValueError):
return float(min_val)
if v < min_val:
return float(min_val)
if v > max_val:
return float(max_val)
return v
def weighted_total(
format_score,
matrix_score,
ideology_score,
weights: Mapping[str, float] | None = None,
) -> float:
"""三个子项加权求和,权重默认 (0.3 / 0.4 / 0.3)。
weights 可选 dict,覆盖默认值;缺 key 时回退默认权重。
返回值经 clamp_score 裁剪到 [0, 100]。
"""
w = weights or {}
wf = float(w.get("format", DEFAULT_WEIGHT_FORMAT))
wm = float(w.get("matrix", DEFAULT_WEIGHT_MATRIX))
wi = float(w.get("ideology", DEFAULT_WEIGHT_IDEOLOGY))
total = (
clamp_score(format_score) * wf
+ clamp_score(matrix_score) * wm
+ clamp_score(ideology_score) * wi
)
return round(clamp_score(total), 2)
def classify_audit_status(
score,
pass_thr,
modify_thr,
) -> str:
"""根据总分给最终建议。
- score >= pass_thr → pass(直接送审)
- modify_thr <= score < pass_thr → modify(修改后送审)
- score < modify_thr → rewrite(重写)
"""
s = clamp_score(score)
try:
p = float(pass_thr)
except (TypeError, ValueError):
p = 0.0
try:
m = float(modify_thr)
except (TypeError, ValueError):
m = 0.0
if p < m:
p, m = m, p # 防御性:保证 pass >= modify
if s >= p:
return AUDIT_STATUS_PASS
if s >= m:
return AUDIT_STATUS_MODIFY
return AUDIT_STATUS_REWRITE
def parse_score_threshold(env_value, default: float) -> float:
"""从 env 字符串读取分数阈值;非法值回退 default。"""
if env_value is None:
return float(default)
s = str(env_value).strip()
if not s:
return float(default)
try:
v = float(s)
except ValueError:
return float(default)
if v < TOTAL_SCORE_MIN or v > TOTAL_SCORE_MAX:
return float(default)
return v
def parse_ideology_keywords(env_value, default_keywords: Iterable[str]) -> list[str]:
"""从 env(JSON 数组字符串)解析思政关键词列表。
- 空 / 非法 JSON → 默认关键词列表
- 元素需为 str;非 str 一律跳过
"""
if env_value is None:
return list(default_keywords)
s = str(env_value).strip()
if not s:
return list(default_keywords)
try:
data = json.loads(s)
except (ValueError, TypeError):
return list(default_keywords)
if not isinstance(data, list):
return list(default_keywords)
out: list[str] = []
for item in data:
if isinstance(item, str) and item.strip():
out.append(item.strip())
return out or list(default_keywords)
def count_keyword_hits(text: str, keywords: Iterable[str]) -> dict[str, int]:
"""统计文本中每个关键词出现的次数(不区分大小写)。
返回 {keyword: count};不命中的关键词不出现在结果中(count=0 省略)。
"""
if not isinstance(text, str):
return {}
lower = text.lower()
out: dict[str, int] = {}
for kw in keywords:
if not isinstance(kw, str) or not kw:
continue
c = lower.count(kw.lower())
if c > 0:
out[kw] = c
return out