From 2da3cd39f8238081fafc48947db8ce7cc2699a54 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 12:08:12 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20[M11b-2c-A3]=20=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BF=AE=E5=A4=8D=EF=BC=88=E6=9D=A1=E4=BB=B6?= =?UTF-8?q?=E8=A7=A6=E5=8F=91=EF=BC=8C=E4=BB=85=E5=BD=93=20A1=20=E5=88=A4?= =?UTF-8?q?=E5=AE=9A=20PBL-RTX-0004=20=E4=B8=BA=E4=B8=9A=E5=8A=A1=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 39 +++++++++--- pbl_evidence/crud_api.py | 28 +++++++-- scripts/check_crud_endpoints.py | 105 ++++++++++++++++++++++++++++++++ skill/SKILL.md | 25 ++++++-- 4 files changed, 181 insertions(+), 16 deletions(-) create mode 100644 scripts/check_crud_endpoints.py diff --git a/README.md b/README.md index 87f5285..5ba9111 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,10 @@ cd models && json2ddl mysql . > mysql.ddl.sql mysql -h -u -p < mysql.ddl.sql # (手工建表务必带 COLLATE utf8mb4_unicode_ci,否则与 xls2ddl 产物 collation 混用报 1267) -# 3) 生成 CRUD 前端(可选;已有手写 api/ dspy 时按 build.sh 流程执行) +# 3) 生成 CRUD 前端(build.sh 步骤;json/ 下两个文件按 tblname 命名) cd json && xls2ui -m ../models -o ../wwwroot pbl_evidence *.json +# -> 生成 wwwroot/pbl_artifact/ 与 wwwroot/pbl_evidence/ 两个 CRUD 目录(构建产物, +# 已在 .gitignore 中排除,部署后由 build.sh 重新生成,禁止手改其中文件) # 4) 导入编码种子(3 组 appcodes:pbl_artifact_type / pbl_artifact_status / pbl_evidence_type) # Format B,由宿主 scripts/init_data.py(或 dbloader)读入 init/data.json 落 appcodes + appcodes_kv @@ -122,10 +124,25 @@ python3 -c "import json;d=json.load(open('init/data.json'));print('groups:',[g[' # 5) 登记 RBAC 权限(双层:模块层 + 中央宿主层,见下) python3 scripts/load_path.py --check # 清单与 wwwroot 磁盘一致性核对(不连库) RBAC_SET_PERM=/path/to/set_role_perm.py python3 scripts/load_path.py # 实际写权限 +python3 scripts/load_path.py --check --generated # 部署机上(xls2ui 已跑)连生成物一起核对 +python3 scripts/load_path.py --sql | mysql -h -u -p ``` -`scripts/load_path.py` 逐条显式登记(**禁通配符**):`/pbl_evidence/index.ui` 与 -`wwwroot/api/` 下全部 9 个 `.dspy`,角色均为 `logined`。中央宿主 `apps/pbls/scripts/load_path.py` +**permission / rolepermission 同步(新增路径必做)**:`--sql` 输出与 PATHS 一一对应的幂等 +INSERT —— `permission.permcode` = 路径本身,`rolepermission` 用 `role.rolecode='logined'` +反查 id 关联(不含任何硬编码主键,重复执行不产生脏数据)。rbac CLI(set_role_perm.py)不在位时, +这条管道就是部署说明给出的登记手段。**新增 .dspy 若忘记登记,登录后访问必 403** +(QC 硬门禁 #3 的成因)。 + +`scripts/load_path.py` 逐条显式登记(**禁通配符**),角色均为 `logined`: +- 手写资源 13 条:`/pbl_evidence/index.ui` + `wwwroot/api/` 下全部 **12** 个 `.dspy` + (产出物 CRUD 5 + 证据采集 2 + 查询/统计 2 + CRUD 适配 3: + `pbl_artifact_options` / `pbl_evidence_update` / `pbl_evidence_delete`); +- CRUD 生成物 12 条(`GENERATED_PATHS`):`/pbl_evidence/{pbl_artifact,pbl_evidence}` 目录 + + `index.ui` + `get_/add_/update_/delete_*.dspy`(每 alias 6 条)。这些是 xls2ui 产物、开发仓库 + 不落盘,故 `--check` 默认不计 stale,部署后加 `--generated` 再核。 + +中央宿主 `apps/pbls/scripts/load_path.py` 另有一份等价的硬编码兜底清单(不同代码路径,模块脚本 import 失败时仍能登记)—— 双层 RBAC 回退,两层都必须在册。 @@ -143,9 +160,16 @@ pip install . python3 -c "import json;d=json.load(open('init/data.json'));print([g['parentid'] for g in d['appcodes']])" grep -o "parentid='[a-z_]*'" models/*.json | sort -u -# ③ 三处同步:api.py 契约定义数 == init.py env 契约注册数(对外契约应为 10) -grep -c "^async def pbl_" pbl_evidence/api.py +# ③ 三处同步:api.py(10) + crud_api.py(3) 契约定义 == __init__.py 导出 == init.py env 注册(共 13) +grep -c "^async def pbl_" pbl_evidence/api.py pbl_evidence/crud_api.py grep -c "^ env.pbl_" pbl_evidence/init.py +grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py' # 定义/导出/__all__/import/env 全命中 + +# ③b CRUD 端点存在性:json/*.json 引用的每个 ../api/*.dspy 必须真实落盘(否则编辑/删除/下拉 404) +python3 scripts/check_crud_endpoints.py + +# ③c CRUD JSON 合法键 + alias 唯一性自检 +python3 scripts/check_crud_json.py # ④ RBAC 清单与 wwwroot 磁盘一致性核对(不连库) python3 scripts/load_path.py --check @@ -192,13 +216,14 @@ pbl_evidence/ ├── pbl_evidence/ # Python 包(= 模块名) │ ├── __init__.py # 导出(三处同步之 ②) │ ├── init.py # load_pbl_evidence()(三处同步之 ③) -│ ├── api.py # 10 个契约函数(三处同步之 ①) +│ ├── api.py # 10 个业务契约函数(三处同步之 ①) +│ ├── crud_api.py # 3 个 CRUD 框架适配契约(options/update/delete,三处同步之 ①) │ ├── collector.py # 事件扫描 → 映射 → 幂等落库;list/stats/watermark │ ├── evidence_map.py # 纯函数层:事件→证据类型映射、dedup_key、时间规范化 │ └── db.py # 数据访问层:get_module_dbname、列名发现、q_all/q_one/q_exec ├── models/ # pbl_artifact.json / pbl_evidence.json(四段式表定义) ├── json/ # CRUD 定义(tblname + params + editable) -├── wwwroot/ # index.ui + api/ 9 个 .dspy(自动路由 /pbl_evidence/...) +├── wwwroot/ # index.ui + api/ 12 个 .dspy(自动路由 /pbl_evidence/...) ├── init/data.json # 编码种子(Format B:3 组 appcodes + appcodes_kv 子项) ├── sql/pbl_evidence.sql # 建表 SQL 快照 ├── scripts/load_path.py # RBAC 登记(模块层,禁通配符)+ --check 一致性核对 diff --git a/pbl_evidence/crud_api.py b/pbl_evidence/crud_api.py index 4748fa3..d8b19f3 100644 --- a/pbl_evidence/crud_api.py +++ b/pbl_evidence/crud_api.py @@ -4,10 +4,30 @@ 为什么单独成文件(不改 api.py) ------------------------------ -api.py 已有 390 行契约面(产出物 CRUD + 证据幂等采集)。本文件只补 -``json/evidence_pbl_evidence.json`` 里 ``params.editable`` 三个 URL 与 -``browserfields.alters[].dataurl`` 所需的 4 个后端函数,保持 api.py 不动、 -职责清晰:api.py = 业务契约,crud_api.py = CRUD 框架适配。 +api.py 已有 390 行业务契约面(产出物 CRUD + 证据幂等采集)。本文件只补 +``json/pbl_evidence.json`` 里 ``params.editable`` 的 update/delete 两个 URL 与 +``browserfields.alters[].dataurl`` 所需的 **3 个**后端函数(数量以本文件 +``async def`` 为准:pbl_artifact_options / pbl_evidence_update / pbl_evidence_delete), +保持 api.py 不动、职责清晰:api.py = 业务契约,crud_api.py = CRUD 框架适配。 + +与既有 api.py 契约的分工(避免误解为「artifact CRUD 也走本文件」) +---------------------------------------------------------------- +- ``json/pbl_artifact.json`` 的 new/update/delete_data_url 指向 + ``wwwroot/api/pbl_artifact_{create,update,delete}.dspy``,这三个薄包装委托 + **api.py** 的 ``pbl_artifact_create/update/delete``(含 artifact_uid 自动生成、 + version_no 递增、租户归属校验),**不经过本文件**;本文件只为 artifact 提供 + 下拉数据源 ``pbl_artifact_options``(只读 id/label 两列)。 +- ``json/pbl_evidence.json`` 的 new_data_url 指向 ``pbl_evidence_collect.dspy`` + (委托 api.py 的幂等采集,服务端权威、只增不改),update/delete 才委托本文件。 + +注册位置(三处同步,缺一必 NameError) +------------------------------------ +① 定义:本文件;② 导出:``pbl_evidence/__init__.py`` 的 +``from pbl_evidence.crud_api import ...`` + ``__all__``; +③ 挂载:``pbl_evidence/init.py`` 的 ``load_pbl_evidence()`` 内 +``env.pbl_artifact_options / env.pbl_evidence_update / env.pbl_evidence_delete``。 +对外契约清单见 ``skill/SKILL.md``「契约接口」表第 11~13 行(CRUD 适配契约), +RBAC 路径见 ``scripts/load_path.py`` PATHS。 规范依据(crud-definition-spec / module-development-spec) -------------------------------------------------------- diff --git a/scripts/check_crud_endpoints.py b/scripts/check_crud_endpoints.py new file mode 100644 index 0000000..956adc8 --- /dev/null +++ b/scripts/check_crud_endpoints.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""CRUD 端点存在性自检(QC 硬门禁 #2 的 CI 化)。 + +json/*.json 里 `params.editable` / `new|update|delete_data_url` / +`browserfields.alters[].dataurl` 引用的每个 `../api/xxx.dspy`,都必须在 +`wwwroot/api/xxx.dspy` 真实落盘 —— 少一个文件,前端点了就是 404。 + +用法: + python3 scripts/check_crud_endpoints.py # 只核对手写 api/ 端点 + python3 scripts/check_crud_endpoints.py --generated # 连 xls2ui 生成物一起核(部署机用) + +退出码:0=全部命中;1=存在缺失端点。不连库、不依赖第三方包,可直接放 CI。 +""" +import argparse +import io +import json +import os +import re +import sys + +ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) +JSON_DIR = os.path.join(ROOT, 'json') +API_DIR = os.path.join(ROOT, 'wwwroot', 'api') + +# 匹配 ../api/xxx.dspy 与 /pbl_evidence/api/xxx.dspy 两种写法 +URL_RE = re.compile(r'api/([A-Za-z0-9_]+\.dspy)') + + +def referenced_endpoints(): + """返回 {dspy 文件名: [引用它的 json 文件, ...]}。""" + refs = {} + if not os.path.isdir(JSON_DIR): + return refs + for fn in sorted(os.listdir(JSON_DIR)): + if not fn.endswith('.json'): + continue + path = os.path.join(JSON_DIR, fn) + try: + json.loads(io.open(path, encoding='utf-8').read()) + except Exception as exc: + refs.setdefault('__INVALID_JSON__:%s' % fn, []).append(str(exc)) + continue + text = io.open(path, encoding='utf-8').read() + for name in URL_RE.findall(text): + refs.setdefault(name, []).append('json/' + fn) + return refs + + +def generated_endpoints(): + """xls2ui 为每个 CRUD 定义生成 wwwroot/{alias}/{get,add,update,delete}_{alias}.dspy。 + + 注意落点是 **wwwroot/{alias}/** 子目录(不是 wwwroot/api/)——那是构建产物目录, + 开发仓库被 .gitignore 排除,只有部署机跑过 build.sh 后才存在。 + """ + out = [] + for fn in sorted(os.listdir(JSON_DIR)): + if not fn.endswith('.json'): + continue + d = json.loads(io.open(os.path.join(JSON_DIR, fn), encoding='utf-8').read()) + alias = d.get('alias') or d.get('tblname') + if not alias: + continue + for op in ('get', 'add', 'update', 'delete'): + out.append(os.path.join(alias, '%s_%s.dspy' % (op, alias))) + out.append(os.path.join(alias, 'index.ui')) + return out + + +def main(): + ap = argparse.ArgumentParser(description='verify CRUD-referenced .dspy endpoints exist') + ap.add_argument('--generated', action='store_true', + help='also check xls2ui-generated get_/add_/update_/delete_{alias}.dspy ' + '(build artifacts; run on the deployment host after build.sh)') + args = ap.parse_args() + + refs = referenced_endpoints() + bad = [] + for name, srcs in sorted(refs.items()): + if name.startswith('__INVALID_JSON__:'): + bad.append('%s 不是合法 JSON:%s' % (name, srcs[0])) + continue + if not os.path.exists(os.path.join(API_DIR, name)): + bad.append('%s 缺失(被 %s 引用)' % (name, ', '.join(srcs))) + + gen_missing = [] + if args.generated: + for name in generated_endpoints(): + if not os.path.exists(os.path.join(ROOT, 'wwwroot', name)): + gen_missing.append(name) + + print('[pbl_evidence] json/ 引用的端点: %d' % len(refs)) + for line in bad: + print('MISSING ' + line) + if args.generated: + print('[pbl_evidence] xls2ui 生成物端点: %d' % len(generated_endpoints())) + for name in gen_missing: + print('MISSING(gen) wwwroot/' + name) + ok = not bad and not gen_missing + print('[pbl_evidence] crud endpoints: %s' % ('PASS' if ok else 'FAIL')) + return 0 if ok else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/skill/SKILL.md b/skill/SKILL.md index 3c375e0..e54b1e2 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -20,10 +20,12 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序 - `pbl_artifact`:学生产出物(版本内联 `version_no`,唯一索引 `uk_art_uid(tenant_id, artifact_uid)`) - `pbl_evidence`:学习证据(唯一索引 `uk_ev_dedup(tenant_id, source_event_id, evidence_type)` 防重) -表定义:`models/{table}.json`(四段式 summary/fields/indexes/codes);CRUD:`json/evidence_*.json`。 +表定义:`models/{table}.json`(四段式 summary/fields/indexes/codes);CRUD:`json/pbl_artifact.json` + +`json/pbl_evidence.json`(文件名 = tblname;**不要**给两张不同的表共用同一 alias, +xls2ui 会生成到同一目录互相覆盖)。 编码种子:`init/data.json`(Format B,3 组 appcodes,见下「编码字典」)。 -## 契约接口(10 个,与 `pbl_evidence/init.py` 契约段 env 注册一一对应) +## 契约接口(13 个,与 `pbl_evidence/init.py` 的 `load_pbl_evidence()` env 注册一一对应) | # | 契约函数 | .dspy 路径(`/pbl_evidence/api/...`) | 说明 | |---|----------|--------------------------------------|------| @@ -37,6 +39,9 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序 | 8 | `pbl_evidence_list` | `pbl_evidence_list.dspy` | 学习者/产出物/会话/蓝图/类型/时间窗过滤 + 分页 | | 9 | `pbl_evidence_stats` | `pbl_evidence_stats.dspy` | 证据类型分布统计,供 M6 评估与概览页 | | 10 | `pbl_evidence_watermark` | **无独立 .dspy**(见下) | 返回某会话/蓝图已采集到的时间水位,供 M5b/cron 增量续采 | +| 11 | `pbl_artifact_options` | `pbl_artifact_options.dspy` | **CRUD 适配**(`crud_api.py`):产出物下拉数据源,返回**裸数组** `[{value,text}]`,异常降级 `[]`;供 `json/pbl_evidence.json` 的 `alters.artifact_id.dataurl` | +| 12 | `pbl_evidence_update` | `pbl_evidence_update.dspy` | **CRUD 适配**(`crud_api.py`):人工修订证据,强制 `tenant_id` 归属校验;改唯一索引三元组时同步重算 `dedup_key`;供 `editable.update_data_url` | +| 13 | `pbl_evidence_delete` | `pbl_evidence_delete.dspy` | **CRUD 适配**(`crud_api.py`):按 id/ids 删除证据,逐条校验归属;供 `editable.delete_data_url` | **watermark 的调用方式(第 10 条契约刻意不生成 .dspy)**: - 内部调用:宿主/cron/M5b 通过 `ServerEnv().pbl_evidence_watermark(...)` 直调; @@ -46,10 +51,20 @@ load_pbl_evidence() # 宿主 apps/pbls 的 app/pbls.py init() 中按拓扑序 - 若将来需要前端直接轮询,再新增 `wwwroot/api/pbl_evidence_watermark.dspy`,并**同步登记** `scripts/load_path.py` + 中央 `apps/pbls/scripts/load_path.py`(双层 RBAC),否则 403。 -> 除上述 10 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、 +> 第 11~13 条是 **CRUD 框架适配契约**(定义在 `pbl_evidence/crud_api.py`,不改 `api.py`), +> 同样走「三处同步」:`crud_api.py` 定义 → `__init__.py` 导出 → `init.py` `env.xxx = xxx`, +> 并各自有 `wwwroot/api/*.dspy` 薄包装与 `scripts/load_path.py` 登记。漏任一处 = dspy 调用 +> NameError / 端点 404 / 登录后 403(QC 硬门禁三条,实测都踩过)。 +> `json/pbl_artifact.json` 的 new/update/delete 指向既有 `pbl_artifact_{create,update,delete}.dspy` +> (委托 `api.py` 第 1/3/4 条契约),**不经过 crud_api**。 + +> 除上述 13 个对外契约外,`init.py` 还注册了 4 个**内部能力**(供 M5b 回放 / M6 评估复用,不算对外契约、 > 无 .dspy):`pbl_collect_evidence_from_events`、`pbl_resolve_event_table`、`pbl_evidence_types`、 -> `pbl_event_to_evidence_map`。核对口径:`api.py` 契约定义 10 == `__init__.py` 契约导出 10 -> == `init.py` 契约 env 注册 10 == `wwwroot/api/*.dspy` 9(watermark 无 .dspy)。 +> `pbl_event_to_evidence_map`。核对口径: +> `api.py` 契约定义 10 + `crud_api.py` 适配定义 3 == `__init__.py` 契约导出 13 +> == `init.py` 契约 env 注册 13 == `wwwroot/api/*.dspy` 12(watermark 无 .dspy)。 +> 自查命令:`grep -rn 'pbl_evidence_update' pbl_evidence/ --include='*.py'` +> 必须命中 定义(crud_api.py) / __init__.py import / __all__ / init.py import / env 注册。 ## 编码字典(init/data.json,Format B) | parentid(≤22 字符) | 含义 | 子项 |