2 Commits

Author SHA1 Message Date
eb93ec088c ci: 补 .github/workflows/release_skill.yaml
All checks were successful
技能自动化发布 / release (push) Successful in 6s
从 skill-template 同步:tag push (v*) → 调
client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
走 windows.x86_64 + PYARMOR_REG_B64 secret。

补完后,下一次打 v* tag 即可触发 CI 跑
encryption / ZIP / marketplace 同步。
2026-09-14 16:41:44 +08:00
1d1f1ba322 chore: auto release commit (2026-09-14 16:40:24) 2026-09-14 16:40:24 +08:00
7 changed files with 527 additions and 4 deletions

13
.github/workflows/release_skill.yaml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: 技能自动化发布
on:
push:
tags: ["v*"]
jobs:
release:
uses: client-jiangchang/jiangchang-platform-kit/.github/workflows/reusable-release-skill.yaml@main
secrets:
PYARMOR_REG_B64: ${{ secrets.PYARMOR_REG_B64 }}
with:
artifact_platform: windows
pyarmor_platform: windows.x86_64

View File

@@ -2,6 +2,10 @@
本文件对应技能市场 **「更新日志」** Tab。 本文件对应技能市场 **「更新日志」** Tab。
## 1.0.2
- **多平台发布声明**:SKILL.md `metadata.openclaw.brands` 新增 `jiangchang` + `huashang`,发布后同步挂匠厂主站与华商 OEM 市场。CI 会幂等补写 `skill_market_relations` 两行(**只补不删**)。
## 1.0.1 ## 1.0.1
- 初版:基于匠厂 skill-template 主仓 v1.0.108 派生 - 初版:基于匠厂 skill-template 主仓 v1.0.108 派生

View File

@@ -1,7 +1,7 @@
--- ---
name: 制度文件冲突检测与合规审查 name: 制度文件冲突检测与合规审查
description: "起草人在 OA 上传新制度送审稿时,自动检测与校内现行制度库(约 300+ 份)的条款冲突、程序合规性、重复发文风险,并在 Word 侧栏生成批注清单。" description: "起草人在 OA 上传新制度送审稿时,自动检测与校内现行制度库(约 300+ 份)的条款冲突、程序合规性、重复发文风险,并在 Word 侧栏生成批注清单。"
version: 1.0.1 version: 1.0.2
author: 深圳匠厂科技有限公司 author: 深圳匠厂科技有限公司
metadata: metadata:
openclaw: openclaw:
@@ -12,6 +12,9 @@ metadata:
developer_ids: developer_ids:
- 12345 # 占位,请换成你的匠厂用户 ID - 12345 # 占位,请换成你的匠厂用户 ID
- 12468 - 12468
brands: # 可选;未写则发布后仅挂匠厂市场(jiangchang)
- jiangchang # 匠厂主站
- huashang # 华商等 OEM:按目标市场填写,可多选
allowed-tools: allowed-tools:
- bash - bash
--- ---

View File

@@ -4,8 +4,10 @@
import sys import sys
from pathlib import Path from pathlib import Path
# 占位:开发者后续接入 jiangchang_skill_core # 让 scripts/ 下的子包(util/db/service/cli)可作为顶级包导入
# from jiangchang_skill_core import ... _SCRIPTS_DIR = Path(__file__).resolve().parent
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
@@ -19,7 +21,8 @@ def health():
def version(): def version():
"""查看版本(占位)。""" """查看版本(占位)。"""
print("detect-policy-conflict-gs v1.0.1") from util.constants import SKILL_VERSION
print(f"detect-policy-conflict-gs v{SKILL_VERSION}")
return 0 return 0
@@ -55,6 +58,10 @@ def export_review_report(draft_id: str, fmt: str = "pdf"):
def mark_policy_status(draft_id: str, target_status: str): def mark_policy_status(draft_id: str, target_status: str):
"""标记送审稿状态(占位)。""" """标记送审稿状态(占位)。"""
from util.constants import POLICY_DRAFT_STATUSES
if target_status not in POLICY_DRAFT_STATUSES:
print(f"ERROR: 未知状态 {target_status!r},允许:{POLICY_DRAFT_STATUSES}", file=sys.stderr)
return 1
print(f"mark-policy-status: {draft_id} -> {target_status} (待开发者实现)") print(f"mark-policy-status: {draft_id} -> {target_status} (待开发者实现)")
return 0 return 0

View File

@@ -0,0 +1,87 @@
"""冲突严重程度分级纯函数。
依据:
- 高(high):相似度 >= HIGH_CONFLICT_THRESHOLD → 制度文件核心条款直接冲突
- 中(medium):相似度 >= MEDIUM_CONFLICT_THRESHOLD → 程序/用词层歧义
- 低(low):其它(如命名相似但实质无关)
"""
from __future__ import annotations
from typing import Tuple
def classify_conflict_severity(
similarity: float,
thresholds: Tuple[float, float] = (0.85, 0.92),
) -> str:
"""根据条款相似度返回冲突等级。
thresholds = (medium_min, high_min),须 0 <= medium_min <= high_min <= 1。
返回:'high' / 'medium' / 'low'。
"""
medium_min, high_min = thresholds
if similarity >= high_min:
return "high"
if similarity >= medium_min:
return "medium"
return "low"
def normalize_policy_text(s: str) -> str:
"""规范化制度文本:去空白 + 中文/英文标点统一。
用于语义比对前的预处理,确保章节编号差异不干扰相似度计算。
转换规则:
- 全角空格 → 半角空格
- 全角 ASCII(数字 0-9 / 字母 A-Z a-z / 常见标点)→ 半角
- 合并连续空白为单空格
"""
if s is None:
return ""
# 全角 ASCII 标点 → 半角(覆盖冒号/逗号/句号/分号/问号/感叹号/括号/方括号/引号)
full_to_half_punct = {
",": ",", "。": ".", ":": ":", ";": ";",
"?": "?", "!": "!", "(": "(", ")": ")",
"【": "[", "】": "]", "『": "'", "』": "'",
"「": "'", "」": "'",
}
out_chars = []
for ch in str(s):
code = ord(ch)
# 全角空格
if ch == " ":
out_chars.append(" ")
continue
# 已映射的标点
if ch in full_to_half_punct:
out_chars.append(full_to_half_punct[ch])
continue
# 全角数字 0xFF10-0xFF19 → 0x30-0x39
if 0xFF10 <= code <= 0xFF19:
out_chars.append(chr(code - 0xFEE0))
continue
# 全角字母 0xFF21-0xFF3A / 0xFF41-0xFF5A → 半角
if 0xFF21 <= code <= 0xFF3A:
out_chars.append(chr(code - 0xFEE0))
continue
if 0xFF41 <= code <= 0xFF5A:
out_chars.append(chr(code - 0xFEE0))
continue
out_chars.append(ch)
text = "".join(out_chars)
# 合并连续空白为单空格
return " ".join(text.split())
def parse_policy_draft_id(s: str) -> str:
"""解析送审稿 ID:去空白 + 校验非空。
占位骨架的轻量校验,业务层后续接入更严格规则。
"""
if s is None:
raise ValueError("policy_draft_id 不能为空")
s = str(s).strip()
if not s:
raise ValueError("policy_draft_id 不能为空")
return s

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

@@ -0,0 +1,43 @@
"""技能标识、版本与阈值常量。
复制到新 skill 时必须修改:
- SKILL_SLUG 与 SKILL.md metadata.openclaw.slug 保持一致
- SKILL_VERSION 与 SKILL.md version 保持一致
"""
SKILL_SLUG = "detect-policy-conflict-gs"
SKILL_VERSION = "1.0.2"
LOG_LOGGER_NAME = "openclaw.skill.detect_policy_conflict_gs"
PLATFORM_KIT_MIN_VERSION = "1.2.11"
# 送审稿状态机:可送审 → 审查中 → 已通过 / 冲突中 / 驳回
POLICY_DRAFT_STATUSES = (
"可送审",
"审查中",
"已通过",
"冲突中",
"驳回",
)
# 冲突类型分类(用于 service 层判断冲突严重程度)
CONFLICT_TYPES = (
"条款直接冲突",
"程序不合规",
"疑似重复发文",
"用词歧义",
)
# 冲突严重等级(与 thresholds 顺序一一对应)
CONFLICT_SEVERITY_LEVELS = ("low", "medium", "high")
CONFLICT_SEVERITY_LABELS = {
"low": "轻微",
"medium": "中等",
"high": "严重",
}
# 制度库语义比对阈值(命中即视为相关)
DEFAULT_SIMILARITY_THRESHOLD = 0.85
# AI 续聘建议规则阈值(与 conflict 类比:相似度高于阈值则升级冲突等级)
HIGH_CONFLICT_THRESHOLD = 0.92
MEDIUM_CONFLICT_THRESHOLD = 0.85

366
tests/run_tests.py Normal file
View File

@@ -0,0 +1,366 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""默认测试套件:制度文件冲突检测与合规审查。
设计原则:
- 仅用标准库(不依赖 pytest / jinja2),CI 与本地均能直接 `python tests/run_tests.py` 跑
- 覆盖:
- util.constants 版本/标识对齐(SKILL.md / CHANGELOG.md / constants.py)
- util.conflict_classifier 纯函数(classify_conflict_severity / normalize_policy_text / parse_policy_draft_id)
- scripts.main CLI 命令注册表与基础调用(占位骨架可正常 dispatch)
- 退出码:0 = 全部通过,1 = 有失败
- -v 开关:详细输出每个用例的状态
release.ps1 在打 tag 前会自动跑 `python tests/run_tests.py -v`,任何一个
test_* 返回 (False, msg) 即 abort 发布。
"""
from __future__ import annotations
import argparse
import io
import re
import sys
import traceback
from contextlib import redirect_stdout, redirect_stderr
from pathlib import Path
from typing import Any, Callable
# 让 scripts/ 下的模块可导入
_REPO_ROOT = Path(__file__).resolve().parent.parent
_SCRIPTS_DIR = _REPO_ROOT / "scripts"
sys.path.insert(0, str(_SCRIPTS_DIR))
# ---------- 测试基础设施 ----------
# 测试结果收集器
_RESULTS: list[tuple[str, bool, str]] = []
_VERBOSE = False
def _run(name: str, fn: Callable[[], tuple[bool, str]]) -> None:
"""运行单个 test_*,记录结果。"""
try:
ok, msg = fn()
except Exception as e: # 测试本身抛异常也算失败
tb = traceback.format_exc(limit=3)
ok, msg = False, f"exception: {e}\n{tb}"
_RESULTS.append((name, ok, msg))
if _VERBOSE or not ok:
marker = "PASS" if ok else "FAIL"
print(f" [{marker}] {name}: {msg}", flush=True)
# ---------- util.constants 版本对齐(POLICY-RELEASE-001) ----------
def test_skill_md_version_alignment() -> tuple[bool, str]:
"""POLICY-RELEASE-001:SKILL.md version == SKILL_VERSION == CHANGELOG 顶部版本"""
from util.constants import SKILL_VERSION
skill_md = (_REPO_ROOT / "SKILL.md").read_text(encoding="utf-8")
m = re.search(r'(?m)^version:\s*["\']?(\d+\.\d+\.\d+)', skill_md)
if not m:
return False, "SKILL.md 缺顶层 version: X.Y.Z"
md_ver = m.group(1)
if md_ver != SKILL_VERSION:
return False, f"SKILL.md={md_ver} != SKILL_VERSION={SKILL_VERSION}"
# CHANGELOG 顶部应有 ## X.Y.Z
cl = (_REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
cl_m = re.search(r'(?m)^##\s+(\d+\.\d+\.\d+)', cl)
if not cl_m:
return False, "CHANGELOG.md 缺顶层 ## X.Y.Z"
cl_ver = cl_m.group(1)
if cl_ver != SKILL_VERSION:
return False, f"CHANGELOG 顶部={cl_ver} != SKILL_VERSION={SKILL_VERSION}"
return True, f"SKILL.md={md_ver} == CHANGELOG={cl_ver} == SKILL_VERSION={SKILL_VERSION}"
def test_skill_md_slug_alignment() -> tuple[bool, str]:
"""SKILL.md metadata.openclaw.slug == SKILL_SLUG"""
from util.constants import SKILL_SLUG
skill_md = (_REPO_ROOT / "SKILL.md").read_text(encoding="utf-8")
m = re.search(r'(?m)^\s*slug:\s*["\']?([a-z0-9_-]+)', skill_md)
if not m:
return False, "SKILL.md 缺 metadata.openclaw.slug"
md_slug = m.group(1)
if md_slug != SKILL_SLUG:
return False, f"SKILL.md slug={md_slug} != SKILL_SLUG={SKILL_SLUG}"
return True, f"slug={md_slug}"
def test_constants_policy_draft_statuses() -> tuple[bool, str]:
"""送审稿状态机:5 状态枚举完整 + 顺序正确"""
from util.constants import POLICY_DRAFT_STATUSES
expected = ("可送审", "审查中", "已通过", "冲突中", "驳回")
if POLICY_DRAFT_STATUSES != expected:
return False, f"POLICY_DRAFT_STATUSES={POLICY_DRAFT_STATUSES} != {expected}"
return True, f"{len(POLICY_DRAFT_STATUSES)} 状态按生命周期顺序"
def test_constants_conflict_types() -> tuple[bool, str]:
"""冲突类型枚举:含条款冲突/程序不合规/重复发文/用词歧义"""
from util.constants import CONFLICT_TYPES, CONFLICT_SEVERITY_LEVELS, CONFLICT_SEVERITY_LABELS
required = {"条款直接冲突", "程序不合规", "疑似重复发文", "用词歧义"}
missing = required - set(CONFLICT_TYPES)
if missing:
return False, f"CONFLICT_TYPES 缺少: {sorted(missing)}"
if CONFLICT_SEVERITY_LEVELS != ("low", "medium", "high"):
return False, f"CONFLICT_SEVERITY_LEVELS={CONFLICT_SEVERITY_LEVELS}"
if set(CONFLICT_SEVERITY_LABELS.keys()) != set(CONFLICT_SEVERITY_LEVELS):
return False, (
f"CONFLICT_SEVERITY_LABELS keys={set(CONFLICT_SEVERITY_LABELS.keys())} "
f"!= {set(CONFLICT_SEVERITY_LEVELS)}"
)
return True, f"{len(CONFLICT_TYPES)} 冲突类型 + {len(CONFLICT_SEVERITY_LEVELS)} 等级"
# ---------- util.conflict_classifier 纯函数 ----------
def test_classify_conflict_severity_thresholds() -> tuple[bool, str]:
"""classify_conflict_severity 高/中/低 三档边界"""
from util.conflict_classifier import classify_conflict_severity
t = (0.85, 0.92)
cases = [
(1.00, "high"), # 完全相同
(0.95, "high"), # 高于 high_min
(0.92, "high"), # 边界 =
(0.91, "medium"), # 边界下
(0.88, "medium"), # 中等
(0.85, "medium"), # 边界 =
(0.84, "low"), # 低于 medium_min
(0.50, "low"), # 明显不相关
(0.00, "low"), # 完全不同
]
for sim, want in cases:
got = classify_conflict_severity(sim, t)
if got != want:
return False, f"classify_conflict_severity({sim}) = {got}, want {want}"
return True, "9 边界 + 越界场景"
def test_classify_conflict_severity_custom_thresholds() -> tuple[bool, str]:
"""classify_conflict_severity 自定义阈值"""
from util.conflict_classifier import classify_conflict_severity
# 收紧阈值
if classify_conflict_severity(0.90, thresholds=(0.80, 0.90)) != "high":
return False, "自定义阈值 high 边界错"
if classify_conflict_severity(0.85, thresholds=(0.80, 0.90)) != "medium":
return False, "自定义阈值 medium 错"
if classify_conflict_severity(0.79, thresholds=(0.80, 0.90)) != "low":
return False, "自定义阈值 low 错"
return True, "自定义阈值 high/medium/low"
def test_normalize_policy_text_fullwidth() -> tuple[bool, str]:
"""全角数字/字母/空格转半角 + 合并连续空白"""
from util.conflict_classifier import normalize_policy_text
cases = [
("第一章 总 则", "第一章 总 则"),
("第 1 条 总则", "第 1 条 总则"),
("Article 1:Definition", "Article 1:Definition"),
("", ""),
(" 多个 空白 ", "多个 空白"),
("无变化", "无变化"),
("条款ABC123", "条款ABC123"),
]
for inp, want in cases:
got = normalize_policy_text(inp)
if got != want:
return False, f"normalize_policy_text({inp!r}) = {got!r}, want {want!r}"
return True, "全角转半角 + 空白合并 + 边界"
def test_normalize_policy_text_none() -> tuple[bool, str]:
"""None 输入返回空字符串(不抛异常)"""
from util.conflict_classifier import normalize_policy_text
if normalize_policy_text(None) != "":
return False, "None 应返回空字符串"
return True, "None → \"\""
def test_parse_policy_draft_id() -> tuple[bool, str]:
"""parse_policy_draft_id 去空白 + 校验非空"""
from util.conflict_classifier import parse_policy_draft_id
if parse_policy_draft_id("D-001") != "D-001":
return False, "正常 ID 应原样返回"
if parse_policy_draft_id(" D-002 ") != "D-002":
return False, "前后空白应被 strip"
if parse_policy_draft_id(123) != "123":
return False, "数字应转字符串"
# 空值校验
raised = False
try:
parse_policy_draft_id("")
except ValueError:
raised = True
if not raised:
return False, "空字符串应抛 ValueError"
raised = False
try:
parse_policy_draft_id(" ")
except ValueError:
raised = True
if not raised:
return False, "全空白字符串应抛 ValueError"
raised = False
try:
parse_policy_draft_id(None)
except ValueError:
raised = True
if not raised:
return False, "None 应抛 ValueError"
return True, "正常/空白 strip/数字/空/全空白/None 6 场景"
# ---------- scripts.main CLI 命令注册 ----------
def test_main_commands_registered() -> tuple[bool, str]:
"""scripts/main.py COMMANDS 字典含全部 9 个命令"""
from main import COMMANDS
expected = {
"health",
"version",
"config-path",
"init-db",
"audit-policy-draft",
"recheck-conflict-clause",
"export-review-report",
"mark-policy-status",
"rescan-pending-drafts",
}
missing = expected - set(COMMANDS.keys())
if missing:
return False, f"COMMANDS 缺: {sorted(missing)}"
extra = set(COMMANDS.keys()) - expected
if extra:
return False, f"COMMANDS 多余: {sorted(extra)}"
return True, f"{len(COMMANDS)} 个命令全部注册"
def test_main_version_command_prints_skill_version() -> tuple[bool, str]:
"""`python main.py version` 应输出 SKILL_VERSION"""
from util.constants import SKILL_VERSION
import main as cli_main
buf = io.StringIO()
with redirect_stdout(buf):
rc = cli_main.version()
output = buf.getvalue()
if rc != 0:
return False, f"version() 返回 {rc}, want 0"
if SKILL_VERSION not in output:
return False, f"version() 输出不含 {SKILL_VERSION}: {output!r}"
return True, f"输出含 SKILL_VERSION={SKILL_VERSION}"
def test_main_dispatch_unknown_command() -> tuple[bool, str]:
"""main() 对未知命令应返回 1 并打印 usage"""
import main as cli_main
buf_out = io.StringIO()
buf_err = io.StringIO()
with redirect_stdout(buf_out), redirect_stderr(buf_err):
rc = cli_main.main(["not-a-real-command"])
if rc == 0:
return False, f"未知命令应返回非 0, got {rc}"
if "Usage:" not in buf_out.getvalue():
return False, f"未知命令应打印 Usage, got {buf_out.getvalue()!r}"
return True, f"未知命令 → rc={rc} + Usage 输出"
def test_main_dispatch_audit_policy_draft() -> tuple[bool, str]:
"""`audit-policy-draft D-001` 占位实现应 echo draft_id"""
import main as cli_main
buf = io.StringIO()
with redirect_stdout(buf):
rc = cli_main.main(["audit-policy-draft", "D-001"])
output = buf.getvalue()
if rc != 0:
return False, f"audit-policy-draft 返回 {rc}, want 0"
if "D-001" not in output:
return False, f"输出应含 D-001, got {output!r}"
return True, "audit-policy-draft 占位 echo 正常"
def test_main_mark_policy_status_validates_enum() -> tuple[bool, str]:
"""`mark-policy-status` 未知状态应返回 1(不允许静默接受)"""
import main as cli_main
buf_out = io.StringIO()
buf_err = io.StringIO()
with redirect_stdout(buf_out), redirect_stderr(buf_err):
rc = cli_main.mark_policy_status("D-001", "非法状态")
if rc == 0:
return False, "非法状态应返回非 0"
if "ERROR" not in buf_err.getvalue():
return False, f"应输出 ERROR 提示, got stderr={buf_err.getvalue()!r}"
# 合法状态应返回 0
buf_ok = io.StringIO()
with redirect_stdout(buf_ok):
rc_ok = cli_main.mark_policy_status("D-002", "审查中")
if rc_ok != 0:
return False, f"合法状态应返回 0, got {rc_ok}"
return True, "非法状态 → 非 0 + ERROR;合法状态 → 0"
# ---------- 入口 ----------
def main() -> int:
parser = argparse.ArgumentParser(description="默认 release 测试套件")
parser.add_argument("-v", "--verbose", action="store_true",
help="详细输出每个用例结果")
args = parser.parse_args()
global _VERBOSE
_VERBOSE = args.verbose
print("=== detect-policy-conflict-gs release tests ===", flush=True)
_run("constants/skill_md_version_alignment", test_skill_md_version_alignment)
_run("constants/skill_md_slug_alignment", test_skill_md_slug_alignment)
_run("constants/policy_draft_statuses", test_constants_policy_draft_statuses)
_run("constants/conflict_types", test_constants_conflict_types)
_run("conflict_classifier/classify_thresholds", test_classify_conflict_severity_thresholds)
_run("conflict_classifier/classify_custom_thresholds", test_classify_conflict_severity_custom_thresholds)
_run("conflict_classifier/normalize_fullwidth", test_normalize_policy_text_fullwidth)
_run("conflict_classifier/normalize_none", test_normalize_policy_text_none)
_run("conflict_classifier/parse_policy_draft_id", test_parse_policy_draft_id)
_run("main/commands_registered", test_main_commands_registered)
_run("main/version_prints_skill_version", test_main_version_command_prints_skill_version)
_run("main/dispatch_unknown_command", test_main_dispatch_unknown_command)
_run("main/dispatch_audit_policy_draft", test_main_dispatch_audit_policy_draft)
_run("main/mark_policy_status_validates", test_main_mark_policy_status_validates_enum)
total = len(_RESULTS)
passed = sum(1 for _, ok, _ in _RESULTS if ok)
failed = total - passed
print()
print(f"=== Summary: {passed}/{total} passed, {failed} failed ===", flush=True)
if failed:
print("\nFailures:")
for name, ok, msg in _RESULTS:
if not ok:
print(f" - {name}: {msg}")
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())