chore: auto release commit (2026-09-14 16:38:51)
This commit is contained in:
@@ -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
51
scripts/util/constants.py
Normal 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
154
scripts/util/score_util.py
Normal 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
|
||||
Reference in New Issue
Block a user