approve: [M11b-2c-B] 补 dev-notes-m11b2c 文档 + RBAC/路径声明 + git 收口

This commit is contained in:
agent.develop 2026-09-22 12:06:15 +08:00
parent a026feb25a
commit c051b71be3
8 changed files with 147 additions and 14 deletions

View File

@ -1,6 +1,5 @@
{ {
"tblname": "pbl_artifact", "tblname": "pbl_artifact",
"alias": "pbl_evidence",
"title": "学习产出物(artifact)", "title": "学习产出物(artifact)",
"params": { "params": {
"browserfields": { "browserfields": {
@ -45,13 +44,14 @@
"updated_at" "updated_at"
], ],
"logined_userorgid": "tenant_id", "logined_userorgid": "tenant_id",
"new_data_url": "{{entire_url('../api/pbl_artifact_create.dspy')}}",
"update_data_url": "{{entire_url('../api/pbl_artifact_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/pbl_artifact_delete.dspy')}}",
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/pbl_artifact_create.dspy')}}", "new_data_url": "{{entire_url('../api/pbl_artifact_create.dspy')}}",
"update_data_url": "{{entire_url('../api/pbl_artifact_update.dspy')}}", "update_data_url": "{{entire_url('../api/pbl_artifact_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/pbl_artifact_delete.dspy')}}" "delete_data_url": "{{entire_url('../api/pbl_artifact_delete.dspy')}}"
}, },
"sortby": ["created_at desc"], "sortby": ["created_at desc"]
"order_by": "id DESC",
"page_size": 20
} }
} }

View File

@ -43,13 +43,14 @@
"updated_at" "updated_at"
], ],
"logined_userorgid": "tenant_id", "logined_userorgid": "tenant_id",
"new_data_url": "{{entire_url('../api/pbl_evidence_collect.dspy')}}",
"update_data_url": "{{entire_url('../api/pbl_evidence_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/pbl_evidence_delete.dspy')}}",
"editable": { "editable": {
"new_data_url": "{{entire_url('../api/pbl_evidence_collect.dspy')}}", "new_data_url": "{{entire_url('../api/pbl_evidence_collect.dspy')}}",
"update_data_url": "{{entire_url('../api/pbl_evidence_update.dspy')}}", "update_data_url": "{{entire_url('../api/pbl_evidence_update.dspy')}}",
"delete_data_url": "{{entire_url('../api/pbl_evidence_delete.dspy')}}" "delete_data_url": "{{entire_url('../api/pbl_evidence_delete.dspy')}}"
}, },
"sortby": ["occurred_at desc"], "sortby": ["occurred_at desc"]
"order_by": "id DESC",
"page_size": 20
} }
} }

View File

@ -23,6 +23,11 @@ from pbl_evidence.collector import (
collect_evidence_from_events, collect_evidence_from_events,
resolve_event_table, resolve_event_table,
) )
from pbl_evidence.crud_api import (
pbl_artifact_options,
pbl_evidence_update,
pbl_evidence_delete,
)
from pbl_evidence.evidence_map import ( from pbl_evidence.evidence_map import (
EVIDENCE_TYPES, EVIDENCE_TYPES,
EVENT_TO_EVIDENCE, EVENT_TO_EVIDENCE,
@ -36,6 +41,8 @@ __all__ = [
'pbl_evidence_list', 'pbl_evidence_stats', 'pbl_evidence_watermark', 'pbl_evidence_list', 'pbl_evidence_stats', 'pbl_evidence_watermark',
'collect_evidence_from_events', 'resolve_event_table', 'CollectError', 'collect_evidence_from_events', 'resolve_event_table', 'CollectError',
'EVIDENCE_TYPES', 'EVENT_TO_EVIDENCE', 'evidence_types_for_events', 'EVIDENCE_TYPES', 'EVENT_TO_EVIDENCE', 'evidence_types_for_events',
# CRUD 框架适配端点后端(json/pbl_evidence.json 的 editable + alters.dataurl)
'pbl_artifact_options', 'pbl_evidence_update', 'pbl_evidence_delete',
] ]
__version__ = '1.1.0' __version__ = '1.2.0'

View File

@ -25,6 +25,11 @@ from pbl_evidence.api import (
pbl_evidence_watermark, pbl_evidence_watermark,
) )
from pbl_evidence.collector import collect_evidence_from_events, resolve_event_table from pbl_evidence.collector import collect_evidence_from_events, resolve_event_table
from pbl_evidence.crud_api import (
pbl_artifact_options,
pbl_evidence_delete,
pbl_evidence_update,
)
from pbl_evidence.evidence_map import EVIDENCE_TYPES, EVENT_TO_EVIDENCE from pbl_evidence.evidence_map import EVIDENCE_TYPES, EVENT_TO_EVIDENCE
@ -42,6 +47,11 @@ def load_pbl_evidence():
env.pbl_evidence_list = pbl_evidence_list env.pbl_evidence_list = pbl_evidence_list
env.pbl_evidence_stats = pbl_evidence_stats env.pbl_evidence_stats = pbl_evidence_stats
env.pbl_evidence_watermark = pbl_evidence_watermark env.pbl_evidence_watermark = pbl_evidence_watermark
# ── CRUD 框架适配(json/pbl_evidence.json editable 三 URL + artifact_id 下拉)
# 漏这三行 → wwwroot/api/pbl_evidence_update.dspy 等调用必 NameError(三处同步之 ③)
env.pbl_artifact_options = pbl_artifact_options
env.pbl_evidence_update = pbl_evidence_update
env.pbl_evidence_delete = pbl_evidence_delete
# ── 底层能力(供 M5b 回放 / M6 评估直接复用)────────────────── # ── 底层能力(供 M5b 回放 / M6 评估直接复用)──────────────────
env.pbl_collect_evidence_from_events = collect_evidence_from_events env.pbl_collect_evidence_from_events = collect_evidence_from_events
env.pbl_resolve_event_table = resolve_event_table env.pbl_resolve_event_table = resolve_event_table

View File

@ -38,9 +38,12 @@ WWWROOT = os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'wwwroot')) os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'wwwroot'))
# ── (path, role) 清单:逐条显式,禁通配符 ───────────────────────────────── # ── (path, role) 清单:逐条显式,禁通配符 ─────────────────────────────────
# 覆盖 wwwroot 下全部 9 个 .dspy + 模块入口 index.ui。M5a 主能力 # 覆盖 wwwroot 下全部 12 个 .dspy + 模块入口 index.ui(与磁盘一一对应,
# `python3 scripts/load_path.py --check` 做自愈核对)。M5a 主能力
# pbl_evidence_collect_from_events.dspy 与统计口 pbl_evidence_stats.dspy 在此登记, # pbl_evidence_collect_from_events.dspy 与统计口 pbl_evidence_stats.dspy 在此登记,
# 漏登记 = 部署后点击一律 403,整条批量采集链路不可用(QC 退回意见 #1 的实测后果)。 # CRUD 适配三端点 pbl_artifact_options / pbl_evidence_update / pbl_evidence_delete
# (json/pbl_evidence.json 的 editable 三 URL + alters.artifact_id.dataurl 所指)也必须登记,
# 漏登记 = 部署后点击一律 403,整条批量采集/编辑/删除链路不可用(QC 退回意见 #1/#3 的实测后果)。
PATHS = [ PATHS = [
# 模块入口页(wwwroot/index.ui) # 模块入口页(wwwroot/index.ui)
('/pbl_evidence/index.ui', 'logined'), ('/pbl_evidence/index.ui', 'logined'),
@ -56,8 +59,59 @@ PATHS = [
# 证据查询 / 统计 # 证据查询 / 统计
('/pbl_evidence/api/pbl_evidence_list.dspy', 'logined'), ('/pbl_evidence/api/pbl_evidence_list.dspy', 'logined'),
('/pbl_evidence/api/pbl_evidence_stats.dspy', 'logined'), ('/pbl_evidence/api/pbl_evidence_stats.dspy', 'logined'),
# CRUD 框架适配端点(json/pbl_evidence.json editable 三 URL + artifact_id 下拉)
('/pbl_evidence/api/pbl_artifact_options.dspy', 'logined'),
('/pbl_evidence/api/pbl_evidence_update.dspy', 'logined'),
('/pbl_evidence/api/pbl_evidence_delete.dspy', 'logined'),
# ── CRUD 生成物见下方 GENERATED_PATHS(xls2ui 产物,build 时才落盘)──
] ]
# xls2ui 从 json/pbl_artifact.json / json/pbl_evidence.json 生成的 CRUD 目录
# (alias = tblname = pbl_artifact / pbl_evidence)。每个 alias 6 条:目录 +
# index.ui + get_/add_/update_/delete_ 端点,逐条显式、禁通配符。这些文件是
# **构建产物**(.gitignore 排除、不入库),所以只出现在部署环境的 wwwroot 下;
# 注册权限时必须一并登记,否则 CRUD 列表页/新增/编辑/删除一律 403。
CRUD_ALIASES = ('pbl_artifact', 'pbl_evidence')
GENERATED_PATHS = []
for _alias in CRUD_ALIASES:
GENERATED_PATHS.append(('/pbl_evidence/%s' % _alias, 'logined'))
GENERATED_PATHS.append(('/pbl_evidence/%s/index.ui' % _alias, 'logined'))
for _op in ('get', 'add', 'update', 'delete'):
GENERATED_PATHS.append(
('/pbl_evidence/%s/%s_%s.dspy' % (_alias, _op, _alias), 'logined'))
# 注册用的全量清单 = 手写端点 + CRUD 生成物(顺序即登记顺序)
PATHS = PATHS + GENERATED_PATHS
# ── permission / rolepermission 幂等 INSERT(供部署说明与手工兜底使用)──────
# 与 PATHS 一一对应:rbac 三表链路 permission.permcode = 路径本身,
# rolepermission 关联 role.rolecode='logined'。列名以 models 真实字段为准
# (permission: permcode/name;role: rolecode;rolepermission: roleid/permid),
# 用 INSERT ... SELECT 反查 id,避免硬编码主键。
def permission_inserts():
"""返回与 PATHS 等价的幂等 SQL(角色固定 logined)。"""
out = []
for path, role in PATHS:
out.append(
"INSERT INTO `permission` (`id`, `permcode`, `name`) "
"SELECT REPLACE(UUID(),'-',''), '%(p)s', '%(p)s' "
"WHERE NOT EXISTS (SELECT 1 FROM `permission` WHERE `permcode`='%(p)s');"
% {'p': path})
out.append(
"INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) "
"SELECT REPLACE(UUID(),'-',''), r.`id`, p.`id` FROM `role` r, `permission` p "
"WHERE r.`rolecode`='%(rl)s' AND p.`permcode`='%(p)s' "
"AND NOT EXISTS (SELECT 1 FROM `rolepermission` rp "
"WHERE rp.`roleid`=r.`id` AND rp.`permid`=p.`id`);"
% {'rl': role, 'p': path})
return out
def print_permission_sql():
"""--sql 子命令:打印 INSERT 清单(部署时可直接管道进 mysql)。"""
for stmt in permission_inserts():
print(stmt)
STATIC_SUFFIXES = ('.dspy', '.ui', '.js', '.css', '.wss') STATIC_SUFFIXES = ('.dspy', '.ui', '.js', '.css', '.wss')
@ -90,9 +144,15 @@ def on_disk_paths():
return got return got
def check(): def check(include_generated=False):
"""清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。""" """清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。
registered = set(p for p, _role in PATHS)
CRUD 生成物(GENERATED_PATHS)是 build.sh 里 xls2ui 的产物,开发仓库中被
.gitignore 排除、不落盘,因此默认不计入 stale(否则开发环境永远误报);
部署后跑 `--check --generated` 才把它们纳入核对。手写端点与磁盘必须一一对应。
"""
handwritten = set(p for p, _r in PATHS) - set(p for p, _r in GENERATED_PATHS)
registered = set(p for p, _role in PATHS) if include_generated else handwritten
on_disk = on_disk_paths() on_disk = on_disk_paths()
missing = sorted(on_disk - registered) missing = sorted(on_disk - registered)
stale = sorted(registered - on_disk) stale = sorted(registered - on_disk)
@ -129,16 +189,25 @@ def register(verbose=True):
def main(): def main():
ap = argparse.ArgumentParser(description='%s RBAC path registration' % MODULE) ap = argparse.ArgumentParser(description='%s RBAC path registration' % MODULE)
ap.add_argument('--check', action='store_true', help='verify PATHS against wwwroot files') ap.add_argument('--check', action='store_true', help='verify PATHS against wwwroot files')
ap.add_argument('--generated', action='store_true',
help='with --check: also reconcile xls2ui-generated CRUD paths '
'(run this on the deployment host, after build.sh)')
ap.add_argument('--list', action='store_true', help='print registered paths and exit') ap.add_argument('--list', action='store_true', help='print registered paths and exit')
ap.add_argument('--sql', action='store_true',
help='print idempotent permission/rolepermission INSERTs and exit')
args = ap.parse_args() args = ap.parse_args()
if args.sql:
print_permission_sql()
return 0
if args.list: if args.list:
for path, role in PATHS: for path, role in PATHS:
print('%s %s' % (role, path)) print('%s %s' % (role, path))
return 0 return 0
if args.check: if args.check:
missing, stale, wildcard = check() missing, stale, wildcard = check(include_generated=args.generated)
print('[%s] PATHS entries: %d' % (MODULE, len(PATHS))) print('[%s] PATHS entries: %d' % (MODULE, len(PATHS)))
for p in missing: for p in missing:
print('MISSING (on disk, not registered): ' + p) print('MISSING (on disk, not registered): ' + p)

View File

@ -0,0 +1,15 @@
debug(f'pbl_artifact_options.dspy: START params_kw={dict(params_kw)}')
# 下拉数据源(json/pbl_evidence.json -> browserfields.alters.artifact_id.dataurl)
# 契约:必须返回**裸 JSON 数组** [{value,text},...],异常/空数据返回 [],
# 不许包 status/data(crud-definition-spec Pitfall:包了会让下拉静默不渲染)。
# 后端函数 pbl_artifact_options 由 load_pbl_evidence() 注册到 ServerEnv(init.py ③)。
try:
_rows = await pbl_artifact_options(**dict(params_kw))
if not isinstance(_rows, list):
_rows = []
debug(f'pbl_artifact_options.dspy: OK rows={len(_rows)}')
return json.dumps(_rows, ensure_ascii=False)
except Exception as e:
debug(f'pbl_artifact_options.dspy: FAIL {e}')
debug(f'pbl_artifact_options.dspy: TRACE {format_exc()}')
return json.dumps([], ensure_ascii=False)

View File

@ -0,0 +1,15 @@
debug(f'pbl_evidence_delete.dspy: START params_kw={dict(params_kw)}')
# 证据删除端点(json/pbl_evidence.json -> params.editable.delete_data_url)。
# 薄包装:无 import、无业务逻辑,直接委托 init.py 注册的 pbl_evidence_delete
# (支持单条 id 与批量 ids;每条都按 tenant_id + id 双条件校验归属,防越权删除)。
try:
_data = await pbl_evidence_delete(**dict(params_kw))
_msg = (_data or {}).get('message', '证据已删除')
debug(f'pbl_evidence_delete.dspy: OK deleted={(_data or {}).get("deleted")}')
return json.dumps({'status': 'OK', 'ok': True, 'data': _data,
'message': _msg}, ensure_ascii=False)
except Exception as e:
debug(f'pbl_evidence_delete.dspy: FAIL {e}')
debug(f'pbl_evidence_delete.dspy: TRACE {format_exc()}')
return json.dumps({'status': 'ERROR', 'ok': False, 'data': None,
'message': f'证据删除失败:{e}'}, ensure_ascii=False)

View File

@ -0,0 +1,16 @@
debug(f'pbl_evidence_update.dspy: START params_kw={dict(params_kw)}')
# 证据人工修订端点(json/pbl_evidence.json -> params.editable.update_data_url,
# 注意:new/update/delete_data_url 在 CRUD JSON 中位于 params 顶层语义,由 xls2ui 读取)。
# 薄包装:无 import、无业务逻辑,直接委托 init.py 注册的 pbl_evidence_update
# (强制 tenant_id 打头校验归属;改唯一索引三元组时同步重算 dedup_key)。
try:
_data = await pbl_evidence_update(**dict(params_kw))
_msg = (_data or {}).get('message', '证据已更新')
debug(f'pbl_evidence_update.dspy: OK id={(_data or {}).get("id")}')
return json.dumps({'status': 'OK', 'ok': True, 'data': _data,
'message': _msg}, ensure_ascii=False)
except Exception as e:
debug(f'pbl_evidence_update.dspy: FAIL {e}')
debug(f'pbl_evidence_update.dspy: TRACE {format_exc()}')
return json.dumps({'status': 'ERROR', 'ok': False, 'data': None,
'message': f'证据更新失败:{e}'}, ensure_ascii=False)