From dbd7169f5e3723dc3dcca0918f101005d03a36e3 Mon Sep 17 00:00:00 2001 From: "agent.develop" Date: Tue, 22 Sep 2026 17:21:22 +0800 Subject: [PATCH] =?UTF-8?q?approve:=20[M9-=E6=94=B6=E5=8F=A3]=20Bug=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=8A=B6=E6=80=81=E6=B5=81=E8=BD=AC=20+=20?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=97=A5=E5=BF=97=E5=BD=92=E6=A1=A3=20+=20?= =?UTF-8?q?=E4=BA=A4=E4=BB=98=E4=B8=80=E8=87=B4=E6=80=A7=E6=A0=B8=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- init/rbac_seed.json | 100 ++++++++++ scripts/m11b34_rbac_seed.py | 379 ++++++++++++++++++++++++++++++++++++ sql/m11b34_rbac_seed.sql | 23 +++ 3 files changed, 502 insertions(+) create mode 100644 init/rbac_seed.json create mode 100644 scripts/m11b34_rbac_seed.py create mode 100644 sql/m11b34_rbac_seed.sql diff --git a/init/rbac_seed.json b/init/rbac_seed.json new file mode 100644 index 0000000..af618e8 --- /dev/null +++ b/init/rbac_seed.json @@ -0,0 +1,100 @@ +{ + "module": "pbl_runtime_ext", + "milestone": "M11b-3 / M11b-4", + "note": "PBL 运行时广播/轮询端点 RBAC 权限种子清单(逐条显式登记,禁止 % / * 通配符)。由 scripts/m11b34_rbac_seed.py 导入 permission/rolepermission,并把 scripts/load_path.py --check 收敛到 rc=0。", + "default_role": "logined", + "permissions": [ + { + "name": "pbl_runtime_event_append", + "path": "/pbl_runtime_ext/api/pbl_runtime_event_append.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_runtime_event_append.dspy", + "milestone": "M11b-2", + "title": "运行时事件追加(单事务事件+状态写入)", + "traceability": "契约 pbl_runtime_event_append → pbl_runtime_ext/api.py::api_pbl_runtime_event_append → scripts/load_path.py PATHS[0]" + }, + { + "name": "pbl_runtime_event_poll", + "path": "/pbl_runtime_ext/api/pbl_runtime_event_poll.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_runtime_event_poll.dspy", + "milestone": "M11b-4", + "title": "运行时事件 3s 轮询兜底拉取", + "traceability": "契约 pbl_runtime_event_poll → pbl_runtime_ext/api.py::api_pbl_runtime_event_poll → scripts/load_path.py PATHS[1]" + }, + { + "name": "pbl_entity_state_get", + "path": "/pbl_runtime_ext/api/pbl_entity_state_get.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_entity_state_get.dspy", + "milestone": "M11b-4", + "title": "实体状态读取(服务端权威 state_version)", + "traceability": "契约 pbl_entity_state_get → pbl_runtime_ext/api.py::api_pbl_entity_state_get → scripts/load_path.py PATHS[2]" + }, + { + "name": "pbl_entity_state_apply", + "path": "/pbl_runtime_ext/api/pbl_entity_state_apply.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_entity_state_apply.dspy", + "milestone": "M11b-2", + "title": "实体状态变更申请(客户端禁写 state_version)", + "traceability": "契约 pbl_entity_state_apply → pbl_runtime_ext/api.py::api_pbl_entity_state_apply → scripts/load_path.py PATHS[3]" + }, + { + "name": "pbl_world_broadcast", + "path": "/pbl_runtime_ext/api/pbl_world_broadcast.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_world_broadcast.dspy", + "milestone": "M11b-3", + "title": "服务端世界广播(提交后广播 ≤300ms)", + "traceability": "契约 pbl_world_broadcast → pbl_runtime_ext/api.py::api_pbl_world_broadcast → scripts/load_path.py PATHS[4]" + }, + { + "name": "pbl_session_member_add", + "path": "/pbl_runtime_ext/api/pbl_session_member_add.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_session_member_add.dspy", + "milestone": "M11b-3", + "title": "会话成员加入(广播订阅名单维护)", + "traceability": "契约 pbl_session_member_add → pbl_runtime_ext/api.py::api_pbl_session_member_add → scripts/load_path.py PATHS[5]" + }, + { + "name": "pbl_runtime_broadcast_pull", + "path": "/pbl_runtime_ext/api/pbl_runtime_broadcast_pull.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_runtime_broadcast_pull.dspy", + "milestone": "M11b-4", + "title": "广播增量拉取(3s 轮询兜底)", + "traceability": "契约 pbl_runtime_broadcast_pull → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_pull → scripts/load_path.py PATHS[6]" + }, + { + "name": "pbl_runtime_broadcast_stats", + "path": "/pbl_runtime_ext/api/pbl_runtime_broadcast_stats.dspy", + "method": "POST", + "role": "logined", + "endpoint": "wwwroot/api/pbl_runtime_broadcast_stats.dspy", + "milestone": "M11b-4", + "title": "广播时延统计(裁决/广播/端到端指标)", + "traceability": "契约 pbl_runtime_broadcast_stats → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_stats → scripts/load_path.py PATHS[7]" + } + ], + "role_grants": { + "logined": [ + "pbl_runtime_event_append", + "pbl_runtime_event_poll", + "pbl_entity_state_get", + "pbl_entity_state_apply", + "pbl_world_broadcast", + "pbl_session_member_add", + "pbl_runtime_broadcast_pull", + "pbl_runtime_broadcast_stats" + ] + } +} diff --git a/scripts/m11b34_rbac_seed.py b/scripts/m11b34_rbac_seed.py new file mode 100644 index 0000000..7afcd4d --- /dev/null +++ b/scripts/m11b34_rbac_seed.py @@ -0,0 +1,379 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""M11b-3 / M11b-4 权限种子导入器(pbl_runtime_ext 广播/轮询端点,8 条)。 + +背景:anchor3 自检 `scripts/load_path.py` rc=1,8 条 PENDING 全部属 M11b-3 +服务端广播与 M11b-4 3s 轮询端点。本脚本把这 8 条 permission / rolepermission +种子**幂等**导入应用库,使 load_path.py 的自检收敛到 rc=0。 + +硬规定(module-development-spec 6.6 / crud-definition-spec +references/endpoint-registration.md / sqlor-database-module): +1. **逐条显式登记,禁 `%` / `*` / `?` 通配符**——导入前静态校验,命中即 FAIL 不写半截。 +2. **load_path.py 单独不够**:新 api/*.dspy 必须同时落 DB `permission` + + `rolepermission`,否则线上 403。角色用 `logined`(登录即可访问)。 +3. **不猜列名**:permission/rolepermission 的实际列集合运行时从 + information_schema 读取,只写交集列;缺关键列直接 FAIL 而非静默跳过。 +4. **幂等**:permission 按 path 查重(存在则 UPDATE 非键列),rolepermission 按 + (roleid, permid) 查重;id 用 `md5` 派生的稳定 32 位串,重复执行零差异。 +5. **库名不硬编码**:dbname 依次 --dbname > $PBL_RUNTIME_EXT_DBNAME > + $PBL_DBNAME > 兜底 'pbl'(真实值由应用 get_module_dbname 决定)。 + +真实表结构(rbac,见 modules/scense/scripts/ddl.sql 实测 DDL): + permission(id, name, description, ptype, parentid, path, title, icon) + rolepermission(id, roleid, permid) + +用法: + python3 scripts/m11b34_rbac_seed.py --check # 离线校验,不连库 + python3 scripts/m11b34_rbac_seed.py --emit-sql x.sql # 出 SQL 交 build.sh 第 9 步 + python3 scripts/m11b34_rbac_seed.py --apply # 连库幂等导入 +""" + +import argparse +import hashlib +import json +import os +import re +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +MODULE_DIR = os.path.dirname(HERE) +MODULE = 'pbl_runtime_ext' +SEED_JSON = os.path.join(MODULE_DIR, 'init', 'rbac_seed.json') +WWWROOT_API = os.path.join(MODULE_DIR, 'wwwroot', 'api') +LOAD_PATH_PY = os.path.join(HERE, 'load_path.py') + +WILDCARD_RE = re.compile(r'[%*?]') +# rbac 真实列(information_schema 取交集后才使用,缺列 = FAIL,不静默降级) +PERM_REQUIRED = ['id', 'path'] +PERM_OPTIONAL = ['name', 'description', 'ptype', 'parentid', 'title', 'icon'] +RP_REQUIRED = ['id', 'roleid', 'permid'] +# permission.ptype:api 类权限(与 rbac 菜单类权限区分,见 appbase 菜单 ptype='menu') +PTYPE_API = 'api' + + +def load_seed(): + with open(SEED_JSON, 'r', encoding='utf-8') as f: + return json.load(f) + + +def stable_id(prefix, *parts): + """32 位稳定 id(重复执行幂等,不用 UUID——UUID 每次不同会插重复行)。""" + h = hashlib.md5(('|'.join([MODULE] + [str(p) for p in parts])).encode('utf-8')) + return ('%s%s' % (prefix, h.hexdigest()))[:32] + + +def load_path_registered(): + """从 scripts/load_path.py 以 AST 解析 PATHS 的 (path, role) 集合(不 exec)。""" + import ast + with open(LOAD_PATH_PY, 'r', encoding='utf-8') as f: + tree = ast.parse(f.read(), filename=LOAD_PATH_PY) + pairs = set() + for node in ast.walk(tree): + if isinstance(node, ast.Assign): + for tgt in node.targets: + if isinstance(tgt, ast.Name) and tgt.id == 'PATHS': + for item in ast.literal_eval(node.value): + if isinstance(item, (list, tuple)) and len(item) == 2: + pairs.add((item[0], item[1])) + return pairs + + +def static_validate(seed): + """离线三方一致性(种子 ↔ 磁盘 .dspy ↔ load_path.PATHS)+ 通配符门禁。""" + errors = [] + perms = seed.get('permissions') or [] + if not perms: + return False, ['init/rbac_seed.json permissions 为空'] + + disk = set(f for f in os.listdir(WWWROOT_API) if f.endswith('.dspy')) \ + if os.path.isdir(WWWROOT_API) else set() + registered = load_path_registered() + names, paths = set(), set() + + for p in perms: + name, path, role = p.get('name') or '', p.get('path') or '', p.get('role') or '' + if name in names: + errors.append('重复权限名: %s' % name) + names.add(name) + paths.add(path) + if WILDCARD_RE.search(path): + errors.append('禁止通配符(硬政策): %s' % path) + if not path.startswith('/%s/api/' % MODULE): + errors.append('路径前缀不符合模块自动路由 /%s/api/: %s' % (MODULE, path)) + if os.path.basename(path) not in disk: + errors.append('磁盘缺 .dspy 端点文件: %s' % os.path.basename(path)) + if (path, role) not in registered: + errors.append('scripts/load_path.py PATHS 未登记: %s (%s)' % (path, role)) + if not p.get('traceability'): + errors.append('缺 traceability(无法追溯到端点与角色): %s' % name) + if not p.get('milestone'): + errors.append('缺 milestone 归属: %s' % name) + + for path, role in sorted(registered): + if path not in paths: + errors.append('load_path.py 有登记但种子清单缺失: %s (%s)' % (path, role)) + + for role, lst in (seed.get('role_grants') or {}).items(): + for gname in lst: + if gname not in names: + errors.append('role_grants[%s] 引用不存在的权限名: %s' % (role, gname)) + return (not errors), errors + + +def app_conf_candidates(): + """应用 conf/config.json 候选路径(库名/连接从应用配置读,禁止在本模块硬编码 DBNAME)。""" + ws = os.path.dirname(os.path.dirname(MODULE_DIR)) # .../sdlc_general + return [os.path.join(ws, 'apps', 'pbls', 'conf', 'config.json'), + os.environ.get('PBL_APP_CONF', '')] + + +def read_app_conf(): + for path in app_conf_candidates(): + if path and os.path.isfile(path): + try: + with open(path, 'r', encoding='utf-8') as f: + return json.load(f), path + except Exception: # noqa: BLE001 + continue + return None, None + + +def resolve_dbname(args): + """dbname 优先级:--dbname > $PBL_RUNTIME_EXT_DBNAME > 应用 module_dbname 映射。""" + if args.dbname: + return args.dbname, '--dbname' + env_name = os.environ.get('PBL_RUNTIME_EXT_DBNAME') + if env_name: + return env_name, '$PBL_RUNTIME_EXT_DBNAME' + conf, path = read_app_conf() + if conf: + m = (conf.get('module_dbname') or {}) + name = m.get(MODULE) or m.get('default') + if name: + return name, 'conf:%s' % path + return None, 'unresolved' + + +def db_kwargs(dbname): + """从应用 conf 的 databases[dbname].kwargs 取连接参数(不硬编码凭据)。""" + conf, _path = read_app_conf() + kw = {} + if conf: + entry = (conf.get('databases') or {}).get(dbname) or {} + kw = dict(entry.get('kwargs') or {}) + kw.setdefault('host', os.environ.get('PBL_DB_HOST', '127.0.0.1')) + kw.setdefault('port', int(os.environ.get('PBL_DB_PORT', '3306'))) + return kw + + +def table_columns(cur, dbname, table): + cur.execute("SELECT COLUMN_NAME FROM information_schema.COLUMNS " + "WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s", (dbname, table)) + return [r[0] for r in cur.fetchall()] + + +def perm_values(p, cols, role_hint='logined'): + """按真实列集合构造 permission 行(只写交集列)。""" + pid = stable_id('rp_', p['path']) + row = {} + vals = { + 'id': pid, + 'path': p['path'], + 'name': p['name'], + 'title': p.get('title') or p['name'], + 'ptype': PTYPE_API, + 'parentid': 'pbl_runtime_ext', + 'description': '%s | %s | role=%s | %s' % ( + p.get('title') or p['name'], p.get('milestone') or '', + p.get('role') or role_hint, p.get('traceability') or ''), + 'icon': 'api', + } + for c in cols: + row[c] = vals.get(c) + return pid, row + + +def build_rows(seed, perm_cols, rp_cols): + """生成 (permission 行, rolepermission 行) 列表,全部 id 稳定 → 幂等。""" + perm_rows, rp_rows, name_to_pid = [], [], {} + for p in seed['permissions']: + pid, row = perm_values(p, perm_cols) + name_to_pid[p['name']] = pid + perm_rows.append((pid, row)) + for role, names in (seed.get('role_grants') or {}).items(): + for n in names: + pid = name_to_pid.get(n) + if not pid: + continue + rrow = {'id': stable_id('rr_', role, pid)} + if 'roleid' in rp_cols: + rrow['roleid'] = role + if 'permid' in rp_cols: + rrow['permid'] = pid + rp_rows.append(rrow) + return perm_rows, rp_rows + + +def upsert(cur, table, cols, rows, key_col, stats): + """按 key_col 查重:存在 UPDATE,不存在 INSERT。rows 为 dict 列表。""" + for r in rows: + cur.execute('SELECT COUNT(1) FROM `%s` WHERE `%s` = %%s' % (table, key_col), + (r[key_col],)) + exists = bool(cur.fetchone()[0]) + present = [c for c in cols if c in r] + if exists: + sets = ', '.join('`%s` = %%s' % c for c in present if c != key_col) + if sets: + cur.execute('UPDATE `%s` SET %s WHERE `%s` = %%s' % (table, sets, key_col), + [r[c] for c in present if c != key_col] + [r[key_col]]) + stats['%s_updated' % table] = stats.get('%s_updated' % table, 0) + 1 + else: + ph = ', '.join(['%s'] * len(present)) + cur.execute('INSERT INTO `%s` (%s) VALUES (%s)' + % (table, ', '.join('`%s`' % c for c in present), ph), + [r[c] for c in present]) + stats['%s_inserted' % table] = stats.get('%s_inserted' % table, 0) + 1 + return stats + + +def emit_sql(seed, perm_cols, rp_cols, path): + perm_rows, rp_rows = build_rows(seed, perm_cols, rp_cols) + lines = ['-- M11b-3/M11b-4 pbl_runtime_ext 广播/轮询端点权限种子(8 条,逐条显式,无通配符)', + '-- 由 scripts/m11b34_rbac_seed.py 生成,幂等(id 为 path/role 的 md5 派生)', + ''] + for pid, r in perm_rows: + present = [c for c in perm_cols if c in r] + vals = ', '.join("'%s'" % (str(r[c]).replace("'", "''")) for c in present) + lines.append('INSERT INTO `permission` (%s) VALUES (%s)' + % (', '.join('`%s`' % c for c in present), vals) + + ' ON DUPLICATE KEY UPDATE ' + + ', '.join('`%s`=VALUES(`%s`)' % (c, c) + for c in present if c != 'id') + ';') + lines.append('') + for r in rp_rows: + present = [c for c in rp_cols if c in r] + vals = ', '.join("'%s'" % r[c] for c in present) + lines.append('INSERT INTO `rolepermission` (%s) VALUES (%s)' + % (', '.join('`%s`' % c for c in present), vals) + + ' ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`);') + lines.append('') + lines.append('-- 校验:应为 8 条 permission + 8 条 rolepermission') + lines.append("SELECT COUNT(1) AS perm_cnt FROM `permission` " + "WHERE `path` LIKE '/pbl\\_runtime\\_ext/api/%';") + with open(path, 'w', encoding='utf-8') as f: + f.write('\n'.join(lines) + '\n') + return len(perm_rows), len(rp_rows) + + +def get_conn(dbname): + try: + import pymysql + except ImportError: + return None + kw = db_kwargs(dbname) + kw['charset'] = 'utf8mb4' + kw['autocommit'] = False + kw['connect_timeout'] = 6 + return pymysql.connect(**kw) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument('--check', action='store_true', help='只做离线校验,不连库') + ap.add_argument('--apply', action='store_true', help='连库幂等导入') + ap.add_argument('--emit-sql', default=None, help='输出幂等 SQL 文件(不连库)') + ap.add_argument('--dbname', default=None) + args = ap.parse_args() + + seed = load_seed() + ok, errors = static_validate(seed) + print('[%s] rbac_seed 静态校验: permissions=%d errors=%d' + % (MODULE, len(seed['permissions']), len(errors))) + for e in errors: + print(' FAIL ' + e) + if not ok: + return 1 + print(' OK 全部路径逐条显式登记(无 %% / * 通配符);' + '种子 ↔ 磁盘 .dspy ↔ load_path.PATHS 三方一致') + + if args.check: + print('[%s] --check 通过(离线,未连库)' % MODULE) + return 0 + + dbname, src = resolve_dbname(args) + if not dbname: + print(' FAIL 无法解析 %s 的库名(--dbname / $PBL_RUNTIME_EXT_DBNAME / ' + '应用 conf module_dbname 均缺)' % MODULE) + return 1 + print(' dbname=%s (来源 %s)' % (dbname, src)) + if args.emit_sql: + np_, nr = emit_sql(seed, PERM_REQUIRED + PERM_OPTIONAL, + RP_REQUIRED, args.emit_sql) + print(' SQL 已落盘: %s (permission=%d rolepermission=%d,列名取 rbac 标准列)' + % (args.emit_sql, np_, nr)) + return 0 + try: + import pymysql # noqa: F401 + except ImportError: + print(' SKIP 无 pymysql:改用 --emit-sql 出 SQL 交 build.sh 第 9 步导入') + return 2 + try: + conn = get_conn(dbname) + except Exception as e: # noqa: BLE001 + print(' SKIP 连库失败(%s):DB 不可达时用 --emit-sql 出 SQL,' + '交 build.sh 第 9 步(import_rp)导入' % str(e)[:100]) + return 2 + + cur = conn.cursor() + try: + perm_cols = table_columns(cur, dbname, 'permission') + rp_cols = table_columns(cur, dbname, 'rolepermission') + except Exception as e: # noqa: BLE001 + print(' SKIP 无法读取 %s 库的 rbac 表结构(%s):' + '先执行 rbac 建表或用 --emit-sql 出 SQL' % (dbname, str(e)[:80])) + conn.close() + return 2 + if not perm_cols or not rp_cols: + print(' SKIP 库 %s 缺 permission/rolepermission 表(rbac 未建表),' + '本任务不冒充成功' % dbname) + conn.close() + return 2 + missing = [c for c in PERM_REQUIRED if c not in perm_cols] \ + + [c for c in RP_REQUIRED if c not in rp_cols] + if missing: + print(' FAIL rbac 表缺关键列: %s(实际列 %s / %s)' + % (missing, perm_cols, rp_cols)) + conn.close() + return 1 + + use_perm = [c for c in PERM_REQUIRED + PERM_OPTIONAL if c in perm_cols] + use_rp = [c for c in RP_REQUIRED if c in rp_cols] + perm_rows, rp_rows = build_rows(seed, use_perm, use_rp) + + if not args.apply: + print(' DRY:加 --apply 才写库,或 --emit-sql 出 SQL') + conn.close() + return 0 + + stats = {} + stats = upsert(cur, 'permission', use_perm, [r for _, r in perm_rows], 'path', stats) + stats = upsert(cur, 'rolepermission', use_rp, rp_rows, 'id', stats) + conn.commit() + cur.execute("SELECT COUNT(1) FROM `permission` WHERE `path` LIKE ${p}$".replace( + '${p}$', '%s'), ('/%s/api/%%' % MODULE,)) + stats['verify_permission_in_db'] = cur.fetchone()[0] + cur.execute("SELECT COUNT(1) FROM `rolepermission` rp JOIN `permission` p " + "ON rp.permid = p.id WHERE p.path LIKE %s", ('/%s/api/%%' % MODULE,)) + stats['verify_rolepermission_in_db'] = cur.fetchone()[0] + conn.close() + print('[%s] 导入结果: %s' % (MODULE, json.dumps(stats, ensure_ascii=False))) + good = (stats.get('verify_permission_in_db') == len(seed['permissions']) + and stats.get('verify_rolepermission_in_db') == len(seed['permissions'])) + print(' %s 库内回查:permission=%s rolepermission=%s 期望各 %d' + % ('OK' if good else 'FAIL', stats.get('verify_permission_in_db'), + stats.get('verify_rolepermission_in_db'), len(seed['permissions']))) + return 0 if good else 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/sql/m11b34_rbac_seed.sql b/sql/m11b34_rbac_seed.sql new file mode 100644 index 0000000..8e772af --- /dev/null +++ b/sql/m11b34_rbac_seed.sql @@ -0,0 +1,23 @@ +-- M11b-3/M11b-4 pbl_runtime_ext 广播/轮询端点权限种子(8 条,逐条显式,无通配符) +-- 由 scripts/m11b34_rbac_seed.py 生成,幂等(id 为 path/role 的 md5 派生) + +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_450b1b77dc9b1beb7a06a9b0f0fca', '/pbl_runtime_ext/api/pbl_runtime_event_append.dspy', 'pbl_runtime_event_append', '运行时事件追加(单事务事件+状态写入) | M11b-2 | role=logined | 契约 pbl_runtime_event_append → pbl_runtime_ext/api.py::api_pbl_runtime_event_append → scripts/load_path.py PATHS[0]', 'api', 'pbl_runtime_ext', '运行时事件追加(单事务事件+状态写入)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_58d7b9596b19d722759a7973ad50d', '/pbl_runtime_ext/api/pbl_runtime_event_poll.dspy', 'pbl_runtime_event_poll', '运行时事件 3s 轮询兜底拉取 | M11b-4 | role=logined | 契约 pbl_runtime_event_poll → pbl_runtime_ext/api.py::api_pbl_runtime_event_poll → scripts/load_path.py PATHS[1]', 'api', 'pbl_runtime_ext', '运行时事件 3s 轮询兜底拉取', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_0d3922b6801b0ed20b946590f1ba9', '/pbl_runtime_ext/api/pbl_entity_state_get.dspy', 'pbl_entity_state_get', '实体状态读取(服务端权威 state_version) | M11b-4 | role=logined | 契约 pbl_entity_state_get → pbl_runtime_ext/api.py::api_pbl_entity_state_get → scripts/load_path.py PATHS[2]', 'api', 'pbl_runtime_ext', '实体状态读取(服务端权威 state_version)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_56a266cb31f203815239c7b25c5f1', '/pbl_runtime_ext/api/pbl_entity_state_apply.dspy', 'pbl_entity_state_apply', '实体状态变更申请(客户端禁写 state_version) | M11b-2 | role=logined | 契约 pbl_entity_state_apply → pbl_runtime_ext/api.py::api_pbl_entity_state_apply → scripts/load_path.py PATHS[3]', 'api', 'pbl_runtime_ext', '实体状态变更申请(客户端禁写 state_version)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_fbe9adbe519a2c38a7a23c48f9cc3', '/pbl_runtime_ext/api/pbl_world_broadcast.dspy', 'pbl_world_broadcast', '服务端世界广播(提交后广播 ≤300ms) | M11b-3 | role=logined | 契约 pbl_world_broadcast → pbl_runtime_ext/api.py::api_pbl_world_broadcast → scripts/load_path.py PATHS[4]', 'api', 'pbl_runtime_ext', '服务端世界广播(提交后广播 ≤300ms)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_5ca8286318bbbbd7cd270080a7753', '/pbl_runtime_ext/api/pbl_session_member_add.dspy', 'pbl_session_member_add', '会话成员加入(广播订阅名单维护) | M11b-3 | role=logined | 契约 pbl_session_member_add → pbl_runtime_ext/api.py::api_pbl_session_member_add → scripts/load_path.py PATHS[5]', 'api', 'pbl_runtime_ext', '会话成员加入(广播订阅名单维护)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_ad04e35c34433e171ac69800fe736', '/pbl_runtime_ext/api/pbl_runtime_broadcast_pull.dspy', 'pbl_runtime_broadcast_pull', '广播增量拉取(3s 轮询兜底) | M11b-4 | role=logined | 契约 pbl_runtime_broadcast_pull → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_pull → scripts/load_path.py PATHS[6]', 'api', 'pbl_runtime_ext', '广播增量拉取(3s 轮询兜底)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); +INSERT INTO `permission` (`id`, `path`, `name`, `description`, `ptype`, `parentid`, `title`, `icon`) VALUES ('rp_abddb912040e109aef26528c6bb61', '/pbl_runtime_ext/api/pbl_runtime_broadcast_stats.dspy', 'pbl_runtime_broadcast_stats', '广播时延统计(裁决/广播/端到端指标) | M11b-4 | role=logined | 契约 pbl_runtime_broadcast_stats → pbl_runtime_ext/api.py::api_pbl_runtime_broadcast_stats → scripts/load_path.py PATHS[7]', 'api', 'pbl_runtime_ext', '广播时延统计(裁决/广播/端到端指标)', 'api') ON DUPLICATE KEY UPDATE `path`=VALUES(`path`), `name`=VALUES(`name`), `description`=VALUES(`description`), `ptype`=VALUES(`ptype`), `parentid`=VALUES(`parentid`), `title`=VALUES(`title`), `icon`=VALUES(`icon`); + +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0365a6a1679328328a337aaae4ce4', 'logined', 'rp_450b1b77dc9b1beb7a06a9b0f0fca') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_e601324f76b0703166ae853124b62', 'logined', 'rp_58d7b9596b19d722759a7973ad50d') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_54e96ce003e48250e153e0cea47f5', 'logined', 'rp_0d3922b6801b0ed20b946590f1ba9') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_119eac70d6411625f72a05b94a1f0', 'logined', 'rp_56a266cb31f203815239c7b25c5f1') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_9cd4cf0eec4378e78a751d0b900c6', 'logined', 'rp_fbe9adbe519a2c38a7a23c48f9cc3') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_824b636853f523ee9c5c3af35d8a9', 'logined', 'rp_5ca8286318bbbbd7cd270080a7753') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_0047385267d96c7d6b519b68c68be', 'logined', 'rp_ad04e35c34433e171ac69800fe736') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); +INSERT INTO `rolepermission` (`id`, `roleid`, `permid`) VALUES ('rr_85058b3d449dc86c47380293a2f45', 'logined', 'rp_abddb912040e109aef26528c6bb61') ON DUPLICATE KEY UPDATE `permid`=VALUES(`permid`); + +-- 校验:应为 8 条 permission + 8 条 rolepermission +SELECT COUNT(1) AS perm_cnt FROM `permission` WHERE `path` LIKE '/pbl\_runtime\_ext/api/%';