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",
"alias": "pbl_evidence",
"title": "学习产出物artifact",
"params": {
"browserfields": {
@ -45,13 +44,14 @@
"updated_at"
],
"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": {
"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')}}"
},
"sortby": ["created_at desc"],
"order_by": "id DESC",
"page_size": 20
"sortby": ["created_at desc"]
}
}

View File

@ -43,13 +43,14 @@
"updated_at"
],
"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": {
"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')}}"
},
"sortby": ["occurred_at desc"],
"order_by": "id DESC",
"page_size": 20
"sortby": ["occurred_at desc"]
}
}

View File

@ -23,6 +23,11 @@ from pbl_evidence.collector import (
collect_evidence_from_events,
resolve_event_table,
)
from pbl_evidence.crud_api import (
pbl_artifact_options,
pbl_evidence_update,
pbl_evidence_delete,
)
from pbl_evidence.evidence_map import (
EVIDENCE_TYPES,
EVENT_TO_EVIDENCE,
@ -36,6 +41,8 @@ __all__ = [
'pbl_evidence_list', 'pbl_evidence_stats', 'pbl_evidence_watermark',
'collect_evidence_from_events', 'resolve_event_table', 'CollectError',
'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,
)
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
@ -42,6 +47,11 @@ def load_pbl_evidence():
env.pbl_evidence_list = pbl_evidence_list
env.pbl_evidence_stats = pbl_evidence_stats
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 评估直接复用)──────────────────
env.pbl_collect_evidence_from_events = collect_evidence_from_events
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'))
# ── (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 在此登记,
# 漏登记 = 部署后点击一律 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 = [
# 模块入口页wwwroot/index.ui
('/pbl_evidence/index.ui', 'logined'),
@ -56,8 +59,59 @@ PATHS = [
# 证据查询 / 统计
('/pbl_evidence/api/pbl_evidence_list.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_PATHSxls2ui 产物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/namerole: rolecoderolepermission: 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')
@ -90,9 +144,15 @@ def on_disk_paths():
return got
def check():
"""清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。"""
registered = set(p for p, _role in PATHS)
def check(include_generated=False):
"""清单 vs 磁盘一致性核对,返回 (missing, stale, wildcard) 三个列表。
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()
missing = sorted(on_disk - registered)
stale = sorted(registered - on_disk)
@ -129,16 +189,25 @@ def register(verbose=True):
def main():
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('--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('--sql', action='store_true',
help='print idempotent permission/rolepermission INSERTs and exit')
args = ap.parse_args()
if args.sql:
print_permission_sql()
return 0
if args.list:
for path, role in PATHS:
print('%s %s' % (role, path))
return 0
if args.check:
missing, stale, wildcard = check()
missing, stale, wildcard = check(include_generated=args.generated)
print('[%s] PATHS entries: %d' % (MODULE, len(PATHS)))
for p in missing:
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/datacrud-definition-spec Pitfall包了会让下拉静默不渲染
# 后端函数 pbl_artifact_options 由 load_pbl_evidence() 注册到 ServerEnvinit.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)