approve: 修复当前迭代 2 个功能 Bug(pbls-初始迭代)
This commit is contained in:
parent
d2cf64dd7f
commit
bf00609c15
@ -85,7 +85,7 @@ venv 创建与 pip 升级 + 完整 7b 段」后 `bash` 执行、`tee` 捕获得
|
|||||||
两处如实说明(均属本任务范围外的既有问题,已作为发现项上报 PM,未在本任务内改动):
|
两处如实说明(均属本任务范围外的既有问题,已作为发现项上报 PM,未在本任务内改动):
|
||||||
|
|
||||||
1. 截取时**跳过**了第 7 步的 `pip install -r requirements.txt`:该步指向机构内网 PyPI 源
|
1. 截取时**跳过**了第 7 步的 `pip install -r requirements.txt`:该步指向机构内网 PyPI 源
|
||||||
`http://10.20.30.41:8081`,本工作空间不可达(`curl` rc=124),会先于 7b 失败退出。
|
`http://DEAD-INTERNAL-PYPI-REMOVED:8081`,本工作空间不可达(`curl` rc=124),会先于 7b 失败退出。
|
||||||
因此场景 1 日志证明的是「7b 段自身可执行并让 pytest 在应用 venv 内可用」,
|
因此场景 1 日志证明的是「7b 段自身可执行并让 pytest 在应用 venv 内可用」,
|
||||||
**不代表**运行时依赖(ahserver/sqlor/…)在本机已装齐,也不代表 `build.sh` 全流程在本机跑通。
|
**不代表**运行时依赖(ahserver/sqlor/…)在本机已装齐,也不代表 `build.sh` 全流程在本机跑通。
|
||||||
2. 首轮曾按「截取第 1 行起的全部前缀」执行,`build.sh` 第 4 步 DDL 方言门禁 die:
|
2. 首轮曾按「截取第 1 行起的全部前缀」执行,`build.sh` 第 4 步 DDL 方言门禁 die:
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
* webapp(init) 置于 __main__ 守卫内,被 import 不触发启动
|
* webapp(init) 置于 __main__ 守卫内,被 import 不触发启动
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
|
||||||
@ -108,7 +109,11 @@ def init():
|
|||||||
# ---- 引用模块(只读挂载,不改基表)
|
# ---- 引用模块(只读挂载,不改基表)
|
||||||
load_world()
|
load_world()
|
||||||
load_scene()
|
load_scene()
|
||||||
load_entity()
|
# [r13-start3 修复] entity.init.load_entity 是 async def:同步 init() 裸调只产生
|
||||||
|
# RuntimeWarning: coroutine never awaited,契约一个都不会挂上。init() 本身是同步
|
||||||
|
# 签名(ahserver webapp 以无参同步调用),无法 await;协程体是纯 ServerEnv 属性
|
||||||
|
# 赋值(无 DB/网络),asyncio.run 在临时 loop 里真跑一遍即完成注册,安全幂等。
|
||||||
|
asyncio.run(load_entity())
|
||||||
load_script_engine()
|
load_script_engine()
|
||||||
load_scense_runtime()
|
load_scense_runtime()
|
||||||
load_scense()
|
load_scense()
|
||||||
|
|||||||
194
build.sh
Normal file → Executable file
194
build.sh
Normal file → Executable file
@ -2,7 +2,8 @@
|
|||||||
# =====================================================================
|
# =====================================================================
|
||||||
# PBL Agent OS 验证原型(Phase 0/1)—— 一键部署脚本(test 环境)
|
# PBL Agent OS 验证原型(Phase 0/1)—— 一键部署脚本(test 环境)
|
||||||
# 端口唯一事实源:projects/pbls/env/test.json(9380);本脚本不硬编码端口,从 env 读取
|
# 端口唯一事实源:projects/pbls/env/test.json(9380);本脚本不硬编码端口,从 env 读取
|
||||||
# 门禁:建表 37 张 + 9 项种子全绿 + self_check PASS 74/74 + py_compile 全通过
|
# 门禁:手写 DDL 32 张 + 模块 models 链 6 张(10a 步)+ 9 项种子全绿
|
||||||
|
# + self_check PASS 74/74 + py_compile 全通过
|
||||||
# =====================================================================
|
# =====================================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@ -108,7 +109,8 @@ log "硬门禁 6.2 通过: ServerEnv()(L${ENV_LINE}) < load_rbac()(L${RBAC_LINE}
|
|||||||
# 表数不可信(且旧断言 36 与 DDL 实际 37 张不符,新增 pbl_compile_task 后未同步)。
|
# 表数不可信(且旧断言 36 与 DDL 实际 37 张不符,新增 pbl_compile_task 后未同步)。
|
||||||
# 修复:改为「先剥 SQL 注释 + 字符串字面量(含 COMMENT 串),再在纯 DDL 正文上扫描」,
|
# 修复:改为「先剥 SQL 注释 + 字符串字面量(含 COMMENT 串),再在纯 DDL 正文上扫描」,
|
||||||
# 由 scripts/ddl_gate_check.py 单遍字符扫描实现(非逐行正则,串内 -- / # 不会误判为注释),
|
# 由 scripts/ddl_gate_check.py 单遍字符扫描实现(非逐行正则,串内 -- / # 不会误判为注释),
|
||||||
# 期望值与 DDL 实际表数对齐为 37(表数 / BIGINT AUTO_INCREMENT 主键数同为 37)。
|
# 期望值与 DDL 实际表数对齐(表数 / BIGINT AUTO_INCREMENT 主键数同值);
|
||||||
|
# [2026-09-28 方案 A] governance/template 6 表迁出后为 32。
|
||||||
[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}"
|
[ -f "${DDL_FILE}" ] || die "缺少 DDL ${DDL_FILE}"
|
||||||
DDL_GATE="${APP_DIR}/scripts/ddl_gate_check.py"
|
DDL_GATE="${APP_DIR}/scripts/ddl_gate_check.py"
|
||||||
[ -f "${DDL_GATE}" ] || die "缺少 DDL 门禁扫描器 ${DDL_GATE}(6.4 依赖,禁止回退到原文 grep)"
|
[ -f "${DDL_GATE}" ] || die "缺少 DDL 门禁扫描器 ${DDL_GATE}(6.4 依赖,禁止回退到原文 grep)"
|
||||||
@ -118,11 +120,14 @@ SELFTEST_RC=0
|
|||||||
python3 "${DDL_GATE}" --selftest || SELFTEST_RC=$?
|
python3 "${DDL_GATE}" --selftest || SELFTEST_RC=$?
|
||||||
[ "${SELFTEST_RC}" = "0" ] || die "DDL 门禁扫描器自检未通过(--selftest rc=${SELFTEST_RC}),扫描结果不可信,终止部署"
|
[ "${SELFTEST_RC}" = "0" ] || die "DDL 门禁扫描器自检未通过(--selftest rc=${SELFTEST_RC}),扫描结果不可信,终止部署"
|
||||||
|
|
||||||
# 4b. 在降噪后的纯 DDL 正文上核验:方言零命中 + 表数 37 + BIGINT AUTO_INCREMENT 37
|
# 4b. 在降噪后的纯 DDL 正文上核验:方言零命中 + 表数 32 + BIGINT AUTO_INCREMENT 32
|
||||||
|
# [2026-09-28 方案 A] pbl_governance 4 表 + pbl_template 2 表迁出至模块 models/*.json
|
||||||
|
# 单一真源(id=str(32) 应用层 gen_pk,M5a-DISC.1 裁决(a)),手写 DDL 37→32。
|
||||||
|
# 模块 6 表由第 10a 步 gen/apply 链建,不进本门禁的 autoinc 口径。
|
||||||
GATE_RC=0
|
GATE_RC=0
|
||||||
python3 "${DDL_GATE}" --ddl "${DDL_FILE}" --expect-tables 37 --expect-autoinc 37 || GATE_RC=$?
|
python3 "${DDL_GATE}" --ddl "${DDL_FILE}" --expect-tables 32 --expect-autoinc 32 || GATE_RC=$?
|
||||||
[ "${GATE_RC}" = "0" ] || die "硬门禁 6.4 未通过(ddl_gate_check.py rc=${GATE_RC}):非 mariadb 方言关键字命中 / 表数≠37 / BIGINT AUTO_INCREMENT≠37,详见上方回显"
|
[ "${GATE_RC}" = "0" ] || die "硬门禁 6.4 未通过(ddl_gate_check.py rc=${GATE_RC}):非 mariadb 方言关键字命中 / 表数≠32 / BIGINT AUTO_INCREMENT≠32,详见上方回显"
|
||||||
log "硬门禁 6.4 通过: mariadb 方言(剥注释+COMMENT 串后零命中),37 表 / 37 处 BIGINT AUTO_INCREMENT"
|
log "硬门禁 6.4 通过: mariadb 方言(剥注释+COMMENT 串后零命中),32 表 / 32 处 BIGINT AUTO_INCREMENT"
|
||||||
|
|
||||||
# ---- 5. 依赖声明核验(6.5)----
|
# ---- 5. 依赖声明核验(6.5)----
|
||||||
for dep in ahserver sqlor apppublic appbase rbac PyMySQL; do
|
for dep in ahserver sqlor apppublic appbase rbac PyMySQL; do
|
||||||
@ -168,7 +173,7 @@ fi
|
|||||||
"${PY}" -m pip install --upgrade pip wheel setuptools
|
"${PY}" -m pip install --upgrade pip wheel setuptools
|
||||||
|
|
||||||
# [deploy_test R5 修复 #1] 平台基础包(ahserver/sqlor/apppublic/appbase/rbac/bricks_for_python/accounting)
|
# [deploy_test R5 修复 #1] 平台基础包(ahserver/sqlor/apppublic/appbase/rbac/bricks_for_python/accounting)
|
||||||
# 原走内网私有源 http://10.20.30.41:8081 —— 目标机 192.168.16.3 实测 connect timeout(5 次重试全失败),
|
# 原走内网私有源 http://DEAD-INTERNAL-PYPI-REMOVED:8081 —— 目标机 192.168.16.3 实测 connect timeout(5 次重试全失败),
|
||||||
# build.sh 在此处 BUILD_RC=1 终止(R4 部署失败根因,见 evidence/r4_build.log)。
|
# build.sh 在此处 BUILD_RC=1 终止(R4 部署失败根因,见 evidence/r4_build.log)。
|
||||||
# 改为:机构 wheelhouse 离线供给(--find-links,绝对路径,不依赖 cwd)+ 公网 pypi.org 取第三方包
|
# 改为:机构 wheelhouse 离线供给(--find-links,绝对路径,不依赖 cwd)+ 公网 pypi.org 取第三方包
|
||||||
# (目标机实测 https://pypi.org/simple/pymysql/ HTTP=200)。
|
# (目标机实测 https://pypi.org/simple/pymysql/ HTTP=200)。
|
||||||
@ -336,11 +341,35 @@ log "py_compile 全部通过"
|
|||||||
log "执行 DDL: ${DDL_FILE}"
|
log "执行 DDL: ${DDL_FILE}"
|
||||||
mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
--default-character-set=utf8mb4 "${DB_NAME}" < "${DDL_FILE}"
|
--default-character-set=utf8mb4 "${DB_NAME}" < "${DDL_FILE}"
|
||||||
|
# [2026-09-28 方案 A] 计数口径从 LIKE 'pbl_%' 改为 IN 点名 32 张手写表——
|
||||||
|
# LIKE 会被其它模块自建表(rtx pbl_entity_state 等)/迁移备份表干扰而虚高判死。
|
||||||
|
HAND_TABLES="'pbl_blueprint','pbl_blueprint_version','pbl_blueprint_lock','pbl_subobject','pbl_subobject_attr','pbl_subobject_rel','pbl_subobject_asset','pbl_subobject_ref','pbl_subobject_review','pbl_blueprint_tag','pbl_blueprint_publish','pbl_validation_ruleset','pbl_validation_run','pbl_validation_finding','pbl_compile_job','pbl_compile_artifact','pbl_game_definition','pbl_compile_log','pbl_agent_session','pbl_agent_message','pbl_agent_tool_call','pbl_agent_arbitration','pbl_artifact','pbl_evidence','pbl_evidence_idem','pbl_evidence_binding','pbl_rubric','pbl_rubric_criterion','pbl_assessment_report','pbl_domain_binding','pbl_analytics_snapshot','pbl_compile_task'"
|
||||||
ACTUAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
ACTUAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")"
|
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name IN (${HAND_TABLES})" "${DB_NAME}")"
|
||||||
# Q4 修复:与 DDL 实际表数对齐(36 → 37,新增 pbl_compile_task)
|
# Q4 修复:与 DDL 实际表数对齐;[2026-09-28 方案 A] governance/template 6 表迁出
|
||||||
[ "${ACTUAL_TABLES}" = "37" ] || die "建表数应为 37,实际 ${ACTUAL_TABLES}"
|
# 至模块 models 链(第 10a 步建),本文件手写 DDL 37→32。
|
||||||
log "建表完成: pbl_* 共 ${ACTUAL_TABLES} 张"
|
[ "${ACTUAL_TABLES}" = "32" ] || die "建表数应为 32(手写 DDL 段),实际 ${ACTUAL_TABLES}"
|
||||||
|
log "手写 DDL 建表完成: pbl_* 共 ${ACTUAL_TABLES} 张(模块 models 链 6 张见 10a 步)"
|
||||||
|
|
||||||
|
# ---- 10a. 模块 models 链建表(owner 裁决 2026-09-28 方案 A)----
|
||||||
|
# pbl_governance 4 表 + pbl_template 2 表:models/*.json 四段式 → json2ddl 生成
|
||||||
|
# → apply_ddl.py 执行(存在性门禁,绝不 DROP)。先 --check 校验 sql/ 产物与
|
||||||
|
# models 指纹一致(改表忘重跑 gen 即 die),再逐模块 apply + verify。
|
||||||
|
for MOD in pbl_governance pbl_template; do
|
||||||
|
MOD_DIR="${WS_DIR}/modules/${MOD}"
|
||||||
|
[ -f "${MOD_DIR}/scripts/gen_ddl_from_models.py" ] || die "${MOD} 缺 gen_ddl_from_models.py(models 链不完整)"
|
||||||
|
log "[10a] ${MOD}: gen --check(models↔sql 指纹幂等门禁)"
|
||||||
|
"${PY}" "${MOD_DIR}/scripts/gen_ddl_from_models.py" --check \
|
||||||
|
|| die "${MOD}: sql/ 产物与 models/*.json 指纹不一致——改表结构后必须重跑 gen_ddl_from_models.py"
|
||||||
|
log "[10a] ${MOD}: apply_ddl(存在性门禁建表 + information_schema 列核验)"
|
||||||
|
"${PY}" "${MOD_DIR}/scripts/apply_ddl.py" --verify \
|
||||||
|
|| die "${MOD}: apply_ddl 失败(详见上方真实错误链)"
|
||||||
|
done
|
||||||
|
# 模块链 6 表终态断言(含补建 pbl_privacy_consent)
|
||||||
|
MOD_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
|
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name IN ('pbl_tenant','pbl_role_binding','pbl_audit_log','pbl_privacy_consent','pbl_template','pbl_template_item')" "${DB_NAME}")"
|
||||||
|
[ "${MOD_TABLES}" = "6" ] || die "模块 models 链建表应为 6 张,实际 ${MOD_TABLES}"
|
||||||
|
log "[10a] 模块 models 链建表完成: 6/6(governance 4 含 privacy_consent + template 2)"
|
||||||
|
|
||||||
# ---- 10b. 运行目录前置创建(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)----
|
# ---- 10b. 运行目录前置创建(Bug WIu0Rkn0KwB0XTjYZz0qk 修复)----
|
||||||
# 原脚本到步骤 13 才 mkdir logs/run,而步骤 11 已经 `tee ${APP_DIR}/logs/self_check.log`。
|
# 原脚本到步骤 13 才 mkdir logs/run,而步骤 11 已经 `tee ${APP_DIR}/logs/self_check.log`。
|
||||||
@ -371,17 +400,71 @@ log "枚举数量契约自检通过: 74 项 / 自检全绿(无 FAIL)"
|
|||||||
|
|
||||||
# ---- 12. 9 项种子数据幂等注入(重复执行零差异)----
|
# ---- 12. 9 项种子数据幂等注入(重复执行零差异)----
|
||||||
log "种子注入(第 1 轮)"
|
log "种子注入(第 1 轮)"
|
||||||
|
# [2026-09-29 修复] 种子段是**独立 python 进程**(heredoc),必须自己做 DB bootstrap:
|
||||||
|
# ServerEnv.get_module_dbname(库名唯一事实源=conf/config.json 的 module_dbname 段)
|
||||||
|
# + DBPools(cfg.databases)。原脚本从不做这一步,于是 get_db() 解析不到库名 →
|
||||||
|
# get_module_dbname fail-closed 抛 'NoneType' object is not callable,
|
||||||
|
# _table_exists 退化成误报「共享编码表 appcodes 不存在(appbase 未初始化)」,
|
||||||
|
# 最终 DbError 中断部署。此前该缺陷被 seed.py 的 `SHOW TABLES LIKE %s` 旧式传参
|
||||||
|
# (sqlor v2 只认 ${name}$ 具名 dict)以更早的 PBL_E_PARAM/AttributeError 形式
|
||||||
|
# 掩盖,两处必须一起修才能真正走通。
|
||||||
|
# bootstrap 序列与 modules/pbl_*/scripts/apply_ddl.py 及 app/pbls.py init() 同口径
|
||||||
|
# (本修复已在测试机实测:种子注入两轮幂等零差异通过)。
|
||||||
|
export SEED_APP_DIR="${APP_DIR}"
|
||||||
|
export SEED_WS_DIR="${WS_DIR}"
|
||||||
"${PY}" - <<'PYEOF'
|
"${PY}" - <<'PYEOF'
|
||||||
import json, asyncio
|
import json, asyncio, os, sys
|
||||||
|
|
||||||
|
# ---- DB bootstrap(独立进程必做,见上方注释)----
|
||||||
|
_APP_DIR = os.environ.get('SEED_APP_DIR') or os.getcwd()
|
||||||
|
_WS_DIR = os.environ.get('SEED_WS_DIR') or os.path.normpath(
|
||||||
|
os.path.join(_APP_DIR, '..', '..'))
|
||||||
|
os.chdir(_APP_DIR) # conf/config.json 相对路径口径
|
||||||
|
for _p in (os.path.join(_WS_DIR, 'modules', 'pbl_common'),
|
||||||
|
os.path.join(_WS_DIR, 'modules', 'pbl_appcodes'),
|
||||||
|
os.path.join(_WS_DIR, 'pkgs'),
|
||||||
|
'/d/pipeline/binpkg_overlay'):
|
||||||
|
if os.path.isdir(_p) and _p not in sys.path:
|
||||||
|
sys.path.insert(0, _p)
|
||||||
|
|
||||||
|
from ahserver.globalEnv import getConfig, DBPools
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
with open(os.path.join(_APP_DIR, 'conf', 'config.json'), encoding='utf-8') as _fh:
|
||||||
|
_MODULE_DBNAME = json.load(_fh).get('module_dbname') or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _gmd(_m):
|
||||||
|
_n = _MODULE_DBNAME.get(_m) or _MODULE_DBNAME.get('default')
|
||||||
|
if not _n:
|
||||||
|
raise RuntimeError('conf/config.json 缺 module_dbname 映射: %r' % _m)
|
||||||
|
return _n
|
||||||
|
|
||||||
|
|
||||||
|
_cfg = getConfig()
|
||||||
|
_env = ServerEnv() # MUST be before DBPools(同 pbls.py init)
|
||||||
|
_env.get_module_dbname = _gmd
|
||||||
|
DBPools(_cfg.databases)
|
||||||
|
print('[seed] DB bootstrap 完成: 目标库=%s(module_dbname 映射,非硬编码)'
|
||||||
|
% _gmd('pbl_appcodes'))
|
||||||
|
|
||||||
from pbl_appcodes.seed import seed_all
|
from pbl_appcodes.seed import seed_all
|
||||||
# [deploy_test R12 定点补丁] seed_all 是 async def(modules/pbl_appcodes/pbl_appcodes/seed.py:460),
|
# [deploy_test R12 定点补丁] seed_all 是 async def,原脚本同步调用 → TypeError:
|
||||||
# 原脚本同步调用 → TypeError: Object of type coroutine is not JSON serializable
|
# Object of type coroutine is not JSON serializable + RuntimeWarning: never awaited。
|
||||||
# + RuntimeWarning: coroutine 'seed_all' was never awaited(r12d 实测,建表 37/37 后种子段中断)。
|
# [2026-09-29 修复] 两轮必须跑在**同一个事件循环**里,不能各自 asyncio.run():
|
||||||
# 修法:asyncio.run 驱动协程,两轮各自独立事件循环(幂等对比口径不变)。
|
# sqlor 连接池的 Future 绑定首次建池的 loop,第二次 asyncio.run() 新建 loop 后
|
||||||
s1 = asyncio.run(seed_all())
|
# 复用同一连接池 → "got Future attached to a different loop"(build_run7 实测,
|
||||||
print('[seed round1]', json.dumps(s1, ensure_ascii=False, sort_keys=True))
|
# 第 1 轮 inserted=82 成功、第 2 轮崩)。幂等对比口径不变:同 loop 内顺序两轮,
|
||||||
s2 = asyncio.run(seed_all())
|
# 第 2 轮仍须零插入零更新。
|
||||||
print('[seed round2]', json.dumps(s2, ensure_ascii=False, sort_keys=True))
|
async def _seed_two_rounds():
|
||||||
|
r1 = await seed_all()
|
||||||
|
print('[seed round1]', json.dumps(r1, ensure_ascii=False, sort_keys=True))
|
||||||
|
r2 = await seed_all()
|
||||||
|
print('[seed round2]', json.dumps(r2, ensure_ascii=False, sort_keys=True))
|
||||||
|
return r1, r2
|
||||||
|
|
||||||
|
|
||||||
|
s1, s2 = asyncio.run(_seed_two_rounds())
|
||||||
# [deploy_test R12 定点补丁 #2] 原断言用 groups_inserted/items_inserted/idempotent 三个键,
|
# [deploy_test R12 定点补丁 #2] 原断言用 groups_inserted/items_inserted/idempotent 三个键,
|
||||||
# 但 seed_all 真实返回契约(seed.py:494-499)是 ok/groups/items/inserted/updated/skipped/per_group,
|
# 但 seed_all 真实返回契约(seed.py:494-499)是 ok/groups/items/inserted/updated/skipped/per_group,
|
||||||
# 无上述键 → 同步调用修好后必然 KeyError。改按真实返回键断言,幂等口径不变(第二轮零插入零更新)。
|
# 无上述键 → 同步调用修好后必然 KeyError。改按真实返回键断言,幂等口径不变(第二轮零插入零更新)。
|
||||||
@ -398,6 +481,36 @@ PYEOF
|
|||||||
# 先跑 --check 做通配符 + 聚合完整性门禁(rc!=0 即终止,禁止带缺口部署)。
|
# 先跑 --check 做通配符 + 聚合完整性门禁(rc!=0 即终止,禁止带缺口部署)。
|
||||||
LOAD_PATH="${APP_DIR}/scripts/load_path.py"
|
LOAD_PATH="${APP_DIR}/scripts/load_path.py"
|
||||||
[ -f "${LOAD_PATH}" ] || die "缺少 RBAC 注册入口 ${LOAD_PATH}"
|
[ -f "${LOAD_PATH}" ] || die "缺少 RBAC 注册入口 ${LOAD_PATH}"
|
||||||
|
|
||||||
|
# ---- 12.45 RBAC 写入工具定位(set_role_perm.py)----
|
||||||
|
# [2026-09-29 修复] load_path.py 的 find_set_perm() 候选根是 ~/repos/sage、
|
||||||
|
# ~/test/sage、/d/ymq/repos/sage、/opt/sage 四个**外部 sage 根**加 PATH;本机
|
||||||
|
# (pipeline-app 部署形态)一个都不命中(/d/ymq/repos/sage 存在但对 pipeline
|
||||||
|
# 账号 Permission denied,os.path.exists 返回 False 故不会误选),于是
|
||||||
|
# register() 把 308 条路径全部记为 PENDING 并返回 rc=1 → 12.5 die。
|
||||||
|
# 本机真实副本是 /d/pipeline/pipeline-app/set_role_perm.py(全机唯一 find 命中,
|
||||||
|
# conf/config.json 完备)。该工具实测:
|
||||||
|
# · 可写库:`Registered: logined(logined) -> /pipeline-pbls/healthz_probe.ui`,rc=0
|
||||||
|
# (退出时 aiomysql 的 "Event loop is closed" 是 GC 噪音,不影响写入)
|
||||||
|
# · 先查后写天然幂等:连跑 3 次 permission / rolepermission 各仍只 1 行
|
||||||
|
# · 单次约 0.65s → 308 条约 200s,在本脚本 timeout 预算内
|
||||||
|
# 修法用 load_path.py **原生支持的 RBAC_SET_PERM 覆盖**,不改 load_path.py
|
||||||
|
# (该文件属上游 agent.develop,其候选口径面向 sage 根,改它会波及其他宿主)。
|
||||||
|
if [ -n "${RBAC_SET_PERM:-}" ] && [ -f "${RBAC_SET_PERM}" ]; then
|
||||||
|
log "RBAC 写入工具: ${RBAC_SET_PERM}(外部 RBAC_SET_PERM 指定)"
|
||||||
|
else
|
||||||
|
RBAC_SET_PERM=""
|
||||||
|
for _cand in /d/pipeline/pipeline-app/set_role_perm.py \
|
||||||
|
"${PIPE_APP_DIR:-/nonexistent}/set_role_perm.py" \
|
||||||
|
"$(command -v set_role_perm.py || true)"; do
|
||||||
|
if [ -n "${_cand}" ] && [ -f "${_cand}" ]; then RBAC_SET_PERM="${_cand}"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "${RBAC_SET_PERM}" ]; then
|
||||||
|
die "未找到 RBAC 写入工具 set_role_perm.py:load_path.py 候选根(~/repos/sage、~/test/sage、/d/ymq/repos/sage、/opt/sage)本机均不存在或不可读。处置:导出 RBAC_SET_PERM=<绝对路径> 后重跑(本机参考位置 /d/pipeline/pipeline-app/set_role_perm.py)"
|
||||||
|
fi
|
||||||
|
log "RBAC 写入工具: ${RBAC_SET_PERM}(本机候选探测)"
|
||||||
|
fi
|
||||||
|
export RBAC_SET_PERM
|
||||||
log "RBAC 一致性门禁: load_path.py --check"
|
log "RBAC 一致性门禁: load_path.py --check"
|
||||||
CHK_RBAC=0
|
CHK_RBAC=0
|
||||||
WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" --check || CHK_RBAC=$?
|
WS_DIR="${WS_DIR}" "${PY}" "${LOAD_PATH}" --check || CHK_RBAC=$?
|
||||||
@ -433,10 +546,13 @@ else
|
|||||||
# 可能未装 rsync,原实现直接调用 → command not found → set -e 下 die,
|
# 可能未装 rsync,原实现直接调用 → command not found → set -e 下 die,
|
||||||
# 部署目录 /d/pbl/pbls 永远建不出来(复测门禁 ④ 恒红)。
|
# 部署目录 /d/pbl/pbls 永远建不出来(复测门禁 ④ 恒红)。
|
||||||
# 无 rsync 时退化为 tar 管道复制(POSIX 必备,保留权限与软链本身),并显式清理运行态目录。
|
# 无 rsync 时退化为 tar 管道复制(POSIX 必备,保留权限与软链本身),并显式清理运行态目录。
|
||||||
|
# [Bug r13-start2 修复·续] exclude 模式必须无尾斜杠:'venv/' 只匹配目录,venv 是软链时
|
||||||
|
# 不命中 → --delete 会把部署侧 venv 软链删掉(实测发生)。并排除 venv.stale-* 保留目录。
|
||||||
if command -v rsync >/dev/null 2>&1; then
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
rsync -a --delete \
|
rsync -a --delete \
|
||||||
--exclude 'venv/' --exclude '.venv/' --exclude 'logs/' --exclude 'run/' \
|
--exclude 'venv' --exclude '.venv' --exclude 'venv.stale-*' --exclude '.venv.stale-*' \
|
||||||
--exclude '.git/' --exclude '__pycache__/' --exclude '*.pyc' \
|
--exclude 'logs' --exclude 'run' \
|
||||||
|
--exclude '.git' --exclude '__pycache__' --exclude '*.pyc' \
|
||||||
"${APP_DIR}/" "${DEPLOY_PATH}/"
|
"${APP_DIR}/" "${DEPLOY_PATH}/"
|
||||||
else
|
else
|
||||||
log "rsync 不可用,退化 tar 管道同步(保留权限/软链)"
|
log "rsync 不可用,退化 tar 管道同步(保留权限/软链)"
|
||||||
@ -449,8 +565,21 @@ else
|
|||||||
fi
|
fi
|
||||||
mkdir -p "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run"
|
mkdir -p "${DEPLOY_PATH}/logs" "${DEPLOY_PATH}/run"
|
||||||
# venv 软链复用(目标侧 start_cmd 要求 ${DEPLOY_PATH}/venv/bin/python 可用)
|
# venv 软链复用(目标侧 start_cmd 要求 ${DEPLOY_PATH}/venv/bin/python 可用)
|
||||||
if [ ! -e "${DEPLOY_PATH}/venv" ]; then
|
# [Bug r13-start2 修复] 原条件 [ ! -e venv ] 对部署目录里**真实存在的陈旧 venv 目录**
|
||||||
|
# 恒为假 → 软链永远建不出来 → 启动实际用旧 venv 的旧 rbac(无 event_dispatcher 判空守卫),
|
||||||
|
# load_rbac 即 AttributeError: NoneType.bind,进程数=0/healthz=000。
|
||||||
|
# 处置:venv 是目录(非软链)或软链目标不符时,同目录 rename 保留为 venv.stale-<ts>
|
||||||
|
# 再建软链——不跨文件系统 mv(会退化 copy+delete 失败),不 rm(保留证据可回退)。
|
||||||
|
if [ -L "${DEPLOY_PATH}/venv" ] && [ "$(readlink -f "${DEPLOY_PATH}/venv")" = "$(readlink -f "${VENV_DIR}")" ]; then
|
||||||
|
log "部署目录 venv 软链已指向 ${VENV_DIR},跳过"
|
||||||
|
else
|
||||||
|
if [ -e "${DEPLOY_PATH}/venv" ] || [ -L "${DEPLOY_PATH}/venv" ]; then
|
||||||
|
STALE="${DEPLOY_PATH}/venv.stale-$(date +%Y%m%d%H%M%S)"
|
||||||
|
mv "${DEPLOY_PATH}/venv" "${STALE}" || die "陈旧 venv 无法移开: ${DEPLOY_PATH}/venv -> ${STALE}"
|
||||||
|
log "陈旧 venv 已保留为 ${STALE}(属主 $(stat -c %U:%G "${STALE}" 2>/dev/null || echo unknown))"
|
||||||
|
fi
|
||||||
ln -sfn "${VENV_DIR}" "${DEPLOY_PATH}/venv"
|
ln -sfn "${VENV_DIR}" "${DEPLOY_PATH}/venv"
|
||||||
|
log "venv 软链建立: ${DEPLOY_PATH}/venv -> ${VENV_DIR}"
|
||||||
fi
|
fi
|
||||||
[ -x "${DEPLOY_PATH}/venv/bin/python" ] || die "部署目录 venv 不可用: ${DEPLOY_PATH}/venv/bin/python"
|
[ -x "${DEPLOY_PATH}/venv/bin/python" ] || die "部署目录 venv 不可用: ${DEPLOY_PATH}/venv/bin/python"
|
||||||
[ -f "${DEPLOY_PATH}/app/${APP_NAME}.py" ] || die "部署目录缺入口: ${DEPLOY_PATH}/app/${APP_NAME}.py"
|
[ -f "${DEPLOY_PATH}/app/${APP_NAME}.py" ] || die "部署目录缺入口: ${DEPLOY_PATH}/app/${APP_NAME}.py"
|
||||||
@ -507,10 +636,21 @@ if [ "${HZ_CODE}" != "200" ] || [ "${AH_CODE}" != "200" ]; then
|
|||||||
die "健康检查失败(/healthz=${HZ_CODE}, /api/public/health=${AH_CODE}),门禁 ① 不成立"
|
die "健康检查失败(/healthz=${HZ_CODE}, /api/public/health=${AH_CODE}),门禁 ① 不成立"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 表数终态复述(对应复测门禁 ③):部署完成后 pbl 库必须有 37 张 pbl_* 表。
|
# 表数终态复述(对应复测门禁 ③):部署完成后 pbl 库必须**逐张在场** 38 张关键表
|
||||||
|
# (手写 DDL 32 + 模块 models 链 6:governance 4 表含补建 pbl_privacy_consent + template 2 表)。
|
||||||
|
# [2026-09-28 方案 A] 断言从「LIKE 计数」升级为「关键表逐张点名」——裸计数会被
|
||||||
|
# 其它模块启动期自建表/迁移备份表干扰(计数虚高同样判死),点名在场才是确定性门禁。
|
||||||
|
MOD6_TABLES="'pbl_tenant','pbl_role_binding','pbl_audit_log','pbl_privacy_consent','pbl_template','pbl_template_item'"
|
||||||
|
REQUIRED_TABLES="${MOD6_TABLES},${HAND_TABLES}"
|
||||||
FINAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
FINAL_TABLES="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
|
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name IN (${REQUIRED_TABLES})" "${DB_NAME}")"
|
||||||
|
TOTAL_PBL="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")"
|
-N -B -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name LIKE 'pbl\\_%'" "${DB_NAME}")"
|
||||||
log "终态表数: ${DB_NAME}.pbl_* = ${FINAL_TABLES}/37"
|
log "终态表数: 关键表 ${FINAL_TABLES}/38 在场(pbl_* 总计 ${TOTAL_PBL} 张,含模块自建/扩展表)"
|
||||||
[ "${FINAL_TABLES}" = "37" ] || die "终态表数 ${FINAL_TABLES} != 37,门禁 ③ 不成立"
|
[ "${FINAL_TABLES}" = "38" ] || {
|
||||||
|
MISSING="$(mysql --protocol=TCP -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASS}" \
|
||||||
|
-N -B -e "SELECT GROUP_CONCAT(t.n ORDER BY t.n) FROM (SELECT 'pbl_tenant' AS n UNION ALL SELECT 'pbl_role_binding' AS n UNION ALL SELECT 'pbl_audit_log' AS n UNION ALL SELECT 'pbl_privacy_consent' AS n UNION ALL SELECT 'pbl_template' AS n UNION ALL SELECT 'pbl_template_item' AS n UNION ALL SELECT 'pbl_blueprint' AS n UNION ALL SELECT 'pbl_blueprint_version' AS n UNION ALL SELECT 'pbl_blueprint_lock' AS n UNION ALL SELECT 'pbl_subobject' AS n UNION ALL SELECT 'pbl_subobject_attr' AS n UNION ALL SELECT 'pbl_subobject_rel' AS n UNION ALL SELECT 'pbl_subobject_asset' AS n UNION ALL SELECT 'pbl_subobject_ref' AS n UNION ALL SELECT 'pbl_subobject_review' AS n UNION ALL SELECT 'pbl_blueprint_tag' AS n UNION ALL SELECT 'pbl_blueprint_publish' AS n UNION ALL SELECT 'pbl_validation_ruleset' AS n UNION ALL SELECT 'pbl_validation_run' AS n UNION ALL SELECT 'pbl_validation_finding' AS n UNION ALL SELECT 'pbl_compile_job' AS n UNION ALL SELECT 'pbl_compile_artifact' AS n UNION ALL SELECT 'pbl_game_definition' AS n UNION ALL SELECT 'pbl_compile_log' AS n UNION ALL SELECT 'pbl_agent_session' AS n UNION ALL SELECT 'pbl_agent_message' AS n UNION ALL SELECT 'pbl_agent_tool_call' AS n UNION ALL SELECT 'pbl_agent_arbitration' AS n UNION ALL SELECT 'pbl_artifact' AS n UNION ALL SELECT 'pbl_evidence' AS n UNION ALL SELECT 'pbl_evidence_idem' AS n UNION ALL SELECT 'pbl_evidence_binding' AS n UNION ALL SELECT 'pbl_rubric' AS n UNION ALL SELECT 'pbl_rubric_criterion' AS n UNION ALL SELECT 'pbl_assessment_report' AS n UNION ALL SELECT 'pbl_domain_binding' AS n UNION ALL SELECT 'pbl_analytics_snapshot' AS n UNION ALL SELECT 'pbl_compile_task' AS n) t WHERE NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema='${DB_NAME}' AND table_name=t.n)" "${DB_NAME}" 2>/dev/null)"
|
||||||
|
die "关键表在场 ${FINAL_TABLES} != 38(32 手写 + 6 模块 models 链),门禁 ③ 不成立——缺表: ${MISSING:-未知(mysql查询失败)}"
|
||||||
|
}
|
||||||
|
|
||||||
log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH},/healthz=200,表数 ${FINAL_TABLES}/37)"
|
log "BUILD SUCCESS: ${APP_NAME} 已运行于 http://127.0.0.1:${PORT}/ (部署路径 ${DEPLOY_PATH},/healthz=200,关键表 ${FINAL_TABLES}/38)"
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
"port": 3306,
|
"port": 3306,
|
||||||
"user": "test",
|
"user": "test",
|
||||||
"password": "hImoFMkBVbdGiQE030YGvg==",
|
"password": "hImoFMkBVbdGiQE030YGvg==",
|
||||||
"database": "pbl",
|
"db": "pbl",
|
||||||
"charset": "utf8mb4"
|
"charset": "utf8mb4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"website": {
|
"website": {
|
||||||
|
"port": 9380,
|
||||||
"root": "wwwroot",
|
"root": "wwwroot",
|
||||||
|
"paths": [
|
||||||
|
[
|
||||||
|
"$[workdir]$/wwwroot",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
],
|
||||||
"indexes": [
|
"indexes": [
|
||||||
"index.ui",
|
"index.ui",
|
||||||
"index.tmpl",
|
"index.tmpl",
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# PBL Agent OS 验证原型(Phase 0/1)—— 应用依赖声明
|
# PBL Agent OS 验证原型(Phase 0/1)—— 应用依赖声明
|
||||||
# 平台基础依赖(ahserver / sqlor / apppublic / appbase / rbac)由机构私有源提供,
|
# 平台基础依赖(ahserver / sqlor / apppublic / appbase / rbac)由机构私有源提供,
|
||||||
# 通过 --index-url 指向内网 pypi;缺失时 build.sh 会 fail-fast 报错而非静默跳过。
|
# 通过 --index-url 指向内网 pypi;缺失时 build.sh 会 fail-fast 报错而非静默跳过。
|
||||||
# [deploy_test R5 修复] 原 --extra-index-url http://10.20.30.41:8081/... 在目标机 192.168.16.3
|
# [deploy_test R5 修复] 原 --extra-index-url http://DEAD-INTERNAL-PYPI-REMOVED:8081/... 在目标机 192.168.16.3
|
||||||
# 实测 connect timeout(5 次重试全失败 → build.sh BUILD_RC=1,部署卡在依赖安装)。
|
# 实测 connect timeout(5 次重试全失败 → build.sh BUILD_RC=1,部署卡在依赖安装)。
|
||||||
# 平台基础包改由机构 wheelhouse 离线供给(--find-links 相对本文件定位 WS/wheelhouse),
|
# 平台基础包改由机构 wheelhouse 离线供给(--find-links 相对本文件定位 WS/wheelhouse),
|
||||||
# 第三方包走公网 pypi.org(目标机实测 HTTP=200 可达)。
|
# 第三方包走公网 pypi.org(目标机实测 HTTP=200 可达)。
|
||||||
@ -13,7 +13,7 @@
|
|||||||
# 已由 build.sh 硬门禁 6.6(scripts/check_requirements_satisfiable.py)静态把关:
|
# 已由 build.sh 硬门禁 6.6(scripts/check_requirements_satisfiable.py)静态把关:
|
||||||
# 幻影包黑名单(pybricks / mysqlclient / hashlib-additional / 本地 pbl_* 模块)、
|
# 幻影包黑名单(pybricks / mysqlclient / hashlib-additional / 本地 pbl_* 模块)、
|
||||||
# 标准库名误当第三方声明、基座包 == 版本与 wheelhouse 实物逐字一致、
|
# 标准库名误当第三方声明、基座包 == 版本与 wheelhouse 实物逐字一致、
|
||||||
# wheelhouse 覆盖完整性、门禁 6.5 六项声明在位、失效内网源 10.20.30.41 残留、
|
# wheelhouse 覆盖完整性、门禁 6.5 六项声明在位、失效内网源 DEAD-INTERNAL-PYPI-REMOVED 残留、
|
||||||
# 相对路径 --find-links(pip 不解析)——并同步扫描全部 modules/*/pyproject.toml 同类缺陷。
|
# 相对路径 --find-links(pip 不解析)——并同步扫描全部 modules/*/pyproject.toml 同类缺陷。
|
||||||
# 基座版本口径 = env/test.json deps.corrected_requirements_hint(业主 2026-09-25 修正)。
|
# 基座版本口径 = env/test.json deps.corrected_requirements_hint(业主 2026-09-25 修正)。
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- PBL Agent OS 验证原型(Phase 0/1)—— 36 表可执行 DDL
|
-- PBL Agent OS 验证原型(Phase 0/1)—— 32 表可执行 DDL
|
||||||
|
-- 【2026-09-28 方案 A】pbl_governance 4 表 + pbl_template 2 表已迁出至各模块
|
||||||
|
-- models/*.json 单一真源(json2ddl 生成 + apply_ddl 执行),本文件余 32 表。
|
||||||
-- 方言:mariadb(InnoDB + utf8mb4/utf8mb4_general_ci)
|
-- 方言:mariadb(InnoDB + utf8mb4/utf8mb4_general_ci)
|
||||||
-- 规则:id BIGINT NOT NULL AUTO_INCREMENT 物理主键 + xxx_code VARCHAR(32) 业务主键(唯一索引)
|
-- 规则:id BIGINT NOT NULL AUTO_INCREMENT 物理主键 + xxx_code VARCHAR(32) 业务主键(唯一索引)
|
||||||
|
-- (⚠ 仅限本文件遗留 32 表;已迁出模块表一律 id=str(32) 应用层 gen_pk,
|
||||||
|
-- 见各模块 models/*.json —— M5a-DISC.1 裁决(a),本文件为存量过渡真源)
|
||||||
-- tenant_id VARCHAR(32) NOT NULL 为第一业务列,所有索引以 tenant_id 打头
|
-- tenant_id VARCHAR(32) NOT NULL 为第一业务列,所有索引以 tenant_id 打头
|
||||||
-- 禁止 FOREIGN KEY / 禁止 ENUM / 禁止原生 TIMESTAMP(用 DATETIME)
|
-- 禁止 FOREIGN KEY / 禁止 ENUM / 禁止原生 TIMESTAMP(用 DATETIME)
|
||||||
-- append-only 表无 update_time、无物理删除(11 张,见文末清单)
|
-- append-only 表无 update_time、无物理删除(11 张,见文末清单)
|
||||||
@ -13,75 +17,14 @@ SET NAMES utf8mb4;
|
|||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 模块 pbl_governance(3 表)
|
-- 模块 pbl_governance(4 表)——【已迁出本文件】owner 裁决 2026-09-28 方案 A:
|
||||||
|
-- 表定义单一真源 = modules/pbl_governance/models/*.json(四段式,id=str(32)
|
||||||
|
-- 应用层 gen_pk,M5a-DISC.1 裁决(a)),DDL 由模块 scripts/gen_ddl_from_models.py
|
||||||
|
-- 真调平台 json2ddl 生成(sql/pbl_governance.sql)、scripts/apply_ddl.py 执行。
|
||||||
|
-- 本文件不得再维护 pbl_tenant / pbl_role_binding / pbl_audit_log /
|
||||||
|
-- pbl_privacy_consent(design §A3 补建,手写 DDL 时代缺失)任何列定义。
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
-- 1. pbl_tenant 租户主表
|
|
||||||
CREATE TABLE IF NOT EXISTS pbl_tenant (
|
|
||||||
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID(第一业务列)',
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
tenant_code VARCHAR(32) NOT NULL COMMENT '租户业务主键',
|
|
||||||
tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称',
|
|
||||||
org_code VARCHAR(32) DEFAULT NULL COMMENT '所属机构编码(rbac 侧只读引用)',
|
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态:active/suspended/closed(appcodes_kv: pbl_tenant_status)',
|
|
||||||
quota_bp INT NOT NULL DEFAULT 100 COMMENT '蓝图配额',
|
|
||||||
quota_stu INT NOT NULL DEFAULT 500 COMMENT '学生席位配额',
|
|
||||||
remark VARCHAR(512) DEFAULT NULL COMMENT '备注',
|
|
||||||
create_by VARCHAR(32) DEFAULT NULL COMMENT '创建人',
|
|
||||||
create_time DATETIME DEFAULT NULL COMMENT '创建时间',
|
|
||||||
update_by VARCHAR(32) DEFAULT NULL COMMENT '更新人',
|
|
||||||
update_time DATETIME DEFAULT NULL COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_pbl_tenant_code (tenant_id, tenant_code),
|
|
||||||
KEY idx_pbl_tenant_tid_status (tenant_id, status),
|
|
||||||
KEY idx_pbl_tenant_tid_org (tenant_id, org_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 租户主表';
|
|
||||||
|
|
||||||
-- 2. pbl_role_binding 租户内角色绑定(复用 rbac,不改 rbac 基表)
|
|
||||||
CREATE TABLE IF NOT EXISTS pbl_role_binding (
|
|
||||||
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
binding_code VARCHAR(32) NOT NULL COMMENT '绑定业务主键',
|
|
||||||
user_code VARCHAR(32) NOT NULL COMMENT '用户编码(rbac 只读引用)',
|
|
||||||
role_code VARCHAR(32) NOT NULL COMMENT '角色编码:teacher/assistant/student/observer/admin(appcodes_kv: pbl_role)',
|
|
||||||
scope_type VARCHAR(16) NOT NULL DEFAULT 'tenant' COMMENT '作用域类型:tenant/blueprint/class/team',
|
|
||||||
scope_code VARCHAR(32) DEFAULT NULL COMMENT '作用域对象编码',
|
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态:active/revoked',
|
|
||||||
effective_from DATETIME DEFAULT NULL COMMENT '生效起',
|
|
||||||
effective_to DATETIME DEFAULT NULL COMMENT '生效止',
|
|
||||||
create_by VARCHAR(32) DEFAULT NULL,
|
|
||||||
create_time DATETIME DEFAULT NULL,
|
|
||||||
update_by VARCHAR(32) DEFAULT NULL,
|
|
||||||
update_time DATETIME DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_pbl_role_binding_code (tenant_id, binding_code),
|
|
||||||
KEY idx_pbl_role_binding_tid_user (tenant_id, user_code, status),
|
|
||||||
KEY idx_pbl_role_binding_tid_scope (tenant_id, scope_type, scope_code),
|
|
||||||
KEY idx_pbl_role_binding_tid_role (tenant_id, role_code)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 租户角色绑定';
|
|
||||||
|
|
||||||
-- 3. pbl_audit_log 审计日志(append-only)
|
|
||||||
CREATE TABLE IF NOT EXISTS pbl_audit_log (
|
|
||||||
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
audit_code VARCHAR(32) NOT NULL COMMENT '审计业务主键',
|
|
||||||
actor_type VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '主体类型:user/agent/system',
|
|
||||||
actor_code VARCHAR(32) DEFAULT NULL COMMENT '主体编码',
|
|
||||||
action_code VARCHAR(64) NOT NULL COMMENT '动作编码(appcodes_kv: pbl_audit_action)',
|
|
||||||
object_type VARCHAR(32) DEFAULT NULL COMMENT '对象类型',
|
|
||||||
object_code VARCHAR(32) DEFAULT NULL COMMENT '对象编码',
|
|
||||||
result VARCHAR(16) NOT NULL DEFAULT 'success' COMMENT '结果:success/denied/error',
|
|
||||||
detail TEXT DEFAULT NULL COMMENT '详情 JSON',
|
|
||||||
trace_id VARCHAR(64) DEFAULT NULL COMMENT '链路追踪ID',
|
|
||||||
client_ip VARCHAR(64) DEFAULT NULL COMMENT '来源IP',
|
|
||||||
create_time DATETIME DEFAULT NULL COMMENT '发生时间(append-only,无 update_time)',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_pbl_audit_code (tenant_id, audit_code),
|
|
||||||
KEY idx_pbl_audit_tid_time (tenant_id, create_time),
|
|
||||||
KEY idx_pbl_audit_tid_obj (tenant_id, object_type, object_code),
|
|
||||||
KEY idx_pbl_audit_tid_actor (tenant_id, actor_type, actor_code),
|
|
||||||
KEY idx_pbl_audit_tid_trace (tenant_id, trace_id)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 审计日志(append-only)';
|
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 模块 pbl_blueprint(11 表,M1a)
|
-- 模块 pbl_blueprint(11 表,M1a)
|
||||||
@ -329,52 +272,13 @@ CREATE TABLE IF NOT EXISTS pbl_blueprint_publish (
|
|||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 蓝图发布记录(append-only)';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 蓝图发布记录(append-only)';
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 模块 pbl_template(2 表,M1b)
|
-- 模块 pbl_template(2 表)——【已迁出本文件】owner 裁决 2026-09-28 方案 A:
|
||||||
|
-- 表定义单一真源 = modules/pbl_template/models/*.json(四段式,id=str(32)
|
||||||
|
-- 应用层 gen_pk,M5a-DISC.1 裁决(a)),DDL 由模块 scripts/gen_ddl_from_models.py
|
||||||
|
-- 真调平台 json2ddl 生成(sql/pbl_template.sql)、scripts/apply_ddl.py 执行。
|
||||||
|
-- 本文件不得再维护 pbl_template / pbl_template_item 任何列定义。
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
|
|
||||||
-- 15. pbl_template 模板主表(含离线兜底模板)
|
|
||||||
CREATE TABLE IF NOT EXISTS pbl_template (
|
|
||||||
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID(__system__ 表示平台内置)',
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
tpl_code VARCHAR(32) NOT NULL COMMENT '模板业务主键',
|
|
||||||
tpl_name VARCHAR(128) NOT NULL COMMENT '模板名称',
|
|
||||||
tpl_kind VARCHAR(32) NOT NULL DEFAULT 'blueprint' COMMENT '模板种类:blueprint/subobject/rubric',
|
|
||||||
subject_code VARCHAR(32) DEFAULT NULL COMMENT '适用学科',
|
|
||||||
grade_code VARCHAR(32) DEFAULT NULL COMMENT '适用学段',
|
|
||||||
offline_flag TINYINT NOT NULL DEFAULT 0 COMMENT '是否离线兜底模板 0否1是',
|
|
||||||
body LONGTEXT DEFAULT NULL COMMENT '模板正文 JSON(含子对象树)',
|
|
||||||
id_prefix_rule VARCHAR(64) DEFAULT NULL COMMENT '实例化 ID 重映射前缀规则',
|
|
||||||
version_no INT NOT NULL DEFAULT 1 COMMENT '模板版本',
|
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT '状态:active/disabled',
|
|
||||||
create_by VARCHAR(32) DEFAULT NULL,
|
|
||||||
create_time DATETIME DEFAULT NULL,
|
|
||||||
update_by VARCHAR(32) DEFAULT NULL,
|
|
||||||
update_time DATETIME DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_pbl_template_code (tenant_id, tpl_code),
|
|
||||||
KEY idx_pbl_template_tid_kind (tenant_id, tpl_kind, status),
|
|
||||||
KEY idx_pbl_template_tid_subj (tenant_id, subject_code, grade_code),
|
|
||||||
KEY idx_pbl_template_tid_offline (tenant_id, offline_flag)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 模板主表';
|
|
||||||
|
|
||||||
-- 16. pbl_template_item 模板条目(实例化 ID 重映射映射表)
|
|
||||||
CREATE TABLE IF NOT EXISTS pbl_template_item (
|
|
||||||
tenant_id VARCHAR(32) NOT NULL COMMENT '租户ID',
|
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
||||||
item_code VARCHAR(32) NOT NULL COMMENT '条目业务主键',
|
|
||||||
tpl_code VARCHAR(32) NOT NULL COMMENT '模板编码',
|
|
||||||
tpl_obj_key VARCHAR(64) NOT NULL COMMENT '模板内占位键(如 __OBJ_1__)',
|
|
||||||
obj_type VARCHAR(32) NOT NULL COMMENT '子对象类型',
|
|
||||||
parent_key VARCHAR(64) DEFAULT NULL COMMENT '父占位键',
|
|
||||||
payload LONGTEXT DEFAULT NULL COMMENT '条目载荷 JSON',
|
|
||||||
sort_no INT NOT NULL DEFAULT 0 COMMENT '排序',
|
|
||||||
create_time DATETIME DEFAULT NULL,
|
|
||||||
update_time DATETIME DEFAULT NULL,
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_pbl_template_item_code (tenant_id, item_code),
|
|
||||||
UNIQUE KEY uk_pbl_template_item_key (tenant_id, tpl_code, tpl_obj_key),
|
|
||||||
KEY idx_pbl_template_item_tid_tpl (tenant_id, tpl_code, sort_no)
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PBL 模板条目';
|
|
||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 模块 pbl_validation(3 表,M2:14 维 + 5 级质量状态)
|
-- 模块 pbl_validation(3 表,M2:14 维 + 5 级质量状态)
|
||||||
@ -880,10 +784,12 @@ SET FOREIGN_KEY_CHECKS = 1;
|
|||||||
|
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 契约自检(供 build.sh / QC grep 核验)
|
-- 契约自检(供 build.sh / QC grep 核验)
|
||||||
-- 表总数:37(pbl_governance 3 + pbl_blueprint 11 + pbl_template 2 + pbl_validation 3
|
-- 表总数:32(pbl_blueprint 11 + pbl_validation 3 + pbl_compiler 5
|
||||||
-- + pbl_compiler 5 + pbl_agent_runtime 4 + pbl_evidence 4 + pbl_assessment 3
|
-- + pbl_agent_runtime 4 + pbl_evidence 4 + pbl_assessment 3
|
||||||
-- + pbl_domain_ext 1 + pbl_analytics 1 = 37)
|
-- + pbl_domain_ext 1 + pbl_analytics 1 = 32)
|
||||||
-- 注:pbl_compiler 由 4 增至 5(新增 pbl_compile_task),build.sh 门禁 6.4 表数断言同步 36→37
|
-- 注:pbl_governance 3 表 + pbl_template 2 表已迁出至模块 models/*.json 单一真源
|
||||||
|
-- (owner 裁决 2026-09-28 方案 A),build.sh 门禁 6.4 断言同步 37→32;
|
||||||
|
-- 模块侧另建 6 表(含补建 pbl_privacy_consent),库内 pbl_* 终态 = 32+6 = 38
|
||||||
-- append-only 表(11,无 update_time):
|
-- append-only 表(11,无 update_time):
|
||||||
-- pbl_audit_log / pbl_blueprint_version / pbl_subobject_review / pbl_blueprint_publish
|
-- pbl_audit_log / pbl_blueprint_version / pbl_subobject_review / pbl_blueprint_publish
|
||||||
-- pbl_validation_finding / pbl_compile_log / pbl_agent_message / pbl_agent_tool_call
|
-- pbl_validation_finding / pbl_compile_log / pbl_agent_message / pbl_agent_tool_call
|
||||||
|
|||||||
@ -136,7 +136,49 @@ PBL_EVIDENCE_FALLBACK = [
|
|||||||
|
|
||||||
|
|
||||||
# 授权严格度排序:数值越大越严格。冲突时保留更严格者,避免合并顺序放宽权限。
|
# 授权严格度排序:数值越大越严格。冲突时保留更严格者,避免合并顺序放宽权限。
|
||||||
ROLE_STRICTNESS = {'any': 0, 'logined': 1, 'owner.operator': 2, 'owner.superuser': 3}
|
ROLE_STRICTNESS = {'any': 0, 'logined': 1, 'owner.operator': 2, 'owner.superuser': 3,
|
||||||
|
'admin': 2, 'anonymous': 0}
|
||||||
|
|
||||||
|
# ── 角色可解析性词汇表(复刻 set_role_perm.py 的判定,离线即可审计)────────────
|
||||||
|
# 写入工具的角色解析规则:any/anonymous/logined 是特殊角色,直接用字面 id;
|
||||||
|
# 其余按 ``[orgtypeid.]name`` 查 role 表(缺省 orgtypeid='*'),查不到即 exit(1)。
|
||||||
|
# 因此**自造角色名必然注册失败**,而这类失败原先只在 register 阶段以不透明的
|
||||||
|
# rc=1 形式出现(stderr/stdout 被 DEVNULL 吞掉),排障成本极高。
|
||||||
|
SPECIAL_ROLES = ('any', 'anonymous', 'logined')
|
||||||
|
# 已知具名角色:需在 role 表存在对应行(orgtypeid='*' 或显式带前缀)。
|
||||||
|
# 平台词汇里的既有具名角色(role 表实测存在)。模块若要用**此处未列**的具名角色,
|
||||||
|
# 必须先确认该行由种子保证存在,并把它加进本表——否则 --check 门禁会拦下。
|
||||||
|
KNOWN_NAMED_ROLES = ('admin', 'owner.superuser', 'owner.operator',
|
||||||
|
'owner.audit', 'owner.maintainer')
|
||||||
|
RESOLVABLE_ROLES = SPECIAL_ROLES + KNOWN_NAMED_ROLES
|
||||||
|
|
||||||
|
|
||||||
|
def audit_roles(paths):
|
||||||
|
"""离线审计角色名是否可被 set_role_perm.py 解析。
|
||||||
|
|
||||||
|
返回 [(path, role, reason), ...];空列表即全部可解析。
|
||||||
|
|
||||||
|
为什么必须做这道门禁:角色是运行时可增删的(role/orgtypes 有 CRUD),
|
||||||
|
模块自造角色名(如 pbl_agent_runtime 曾用 ``user``)在 role 表里没有
|
||||||
|
``orgtypeid='*'`` 行时,set_role_perm.py 直接 exit(1) → 该路径 PENDING →
|
||||||
|
端点上线即 403。2026-09-29 实测:total=308 ok=300 pending=8,8 条全是
|
||||||
|
自造 ``user``。这道审计把它提前到 --check 阶段并给出可行动的修法。
|
||||||
|
"""
|
||||||
|
bad = []
|
||||||
|
for path, role in paths:
|
||||||
|
r = str(role or '').strip()
|
||||||
|
if not r:
|
||||||
|
bad.append((path, role, '角色名为空'))
|
||||||
|
elif r not in RESOLVABLE_ROLES:
|
||||||
|
hint = ('特殊角色(字面 id,恒可解析):%s;'
|
||||||
|
'已知具名角色(需 role 表有对应行):%s。'
|
||||||
|
'若这是「普通登录用户」语义,平台既有词是 logined;'
|
||||||
|
'若确需新具名角色,须先由种子保证 role 表存在该行'
|
||||||
|
'(orgtypeid=* 或声明时带 orgtype 前缀),再加入 '
|
||||||
|
'KNOWN_NAMED_ROLES。'
|
||||||
|
% ('/'.join(SPECIAL_ROLES), '/'.join(KNOWN_NAMED_ROLES)))
|
||||||
|
bad.append((path, r, '角色名不在平台可解析词汇表内 → %s' % hint))
|
||||||
|
return bad
|
||||||
|
|
||||||
|
|
||||||
def _stricter(a, b):
|
def _stricter(a, b):
|
||||||
@ -349,17 +391,38 @@ def register(paths, verbose=True):
|
|||||||
'(rerun inside the app venv on the deploy host)' % len(paths))
|
'(rerun inside the app venv on the deploy host)' % len(paths))
|
||||||
return [p for p, _r in paths]
|
return [p for p, _r in paths]
|
||||||
py = sys.executable or 'python3'
|
py = sys.executable or 'python3'
|
||||||
|
# [2026-09-29 修复] 原先 stdout/stderr 全 DEVNULL,工具那句
|
||||||
|
# `ERROR: role not found: *.user` 被丢掉,只剩 PENDING 清单 —— 报错不可行动
|
||||||
|
# (用户纪律:报错必须真实可行动)。改为捕获并回显首行原因。
|
||||||
pending = []
|
pending = []
|
||||||
|
reasons = {}
|
||||||
for path, role in paths:
|
for path, role in paths:
|
||||||
rc = subprocess.call([py, tool, role, path],
|
proc = subprocess.run([py, tool, role, path],
|
||||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||||
if rc != 0:
|
text=True, timeout=120)
|
||||||
|
if proc.returncode != 0:
|
||||||
pending.append((path, role))
|
pending.append((path, role))
|
||||||
|
first = ''
|
||||||
|
for ln in (proc.stdout or '').splitlines():
|
||||||
|
ln = ln.strip()
|
||||||
|
# 过滤 aiomysql 退出噪音("Event loop is closed" 的 GC traceback)
|
||||||
|
if ln and 'Event loop is closed' not in ln and not ln.startswith(('Traceback', 'File "', 'RuntimeError')):
|
||||||
|
first = ln
|
||||||
|
break
|
||||||
|
reasons[(path, role)] = first or 'rc=%d(工具无输出)' % proc.returncode
|
||||||
if verbose:
|
if verbose:
|
||||||
print('[apps/pbls] rbac paths: total=%d ok=%d pending=%d'
|
print('[apps/pbls] rbac paths: total=%d ok=%d pending=%d'
|
||||||
% (len(paths), len(paths) - len(pending), len(pending)))
|
% (len(paths), len(paths) - len(pending), len(pending)))
|
||||||
for path, role in pending:
|
for path, role in pending:
|
||||||
print(' PENDING %-8s %s' % (role, path))
|
print(' PENDING %-8s %s' % (role, path))
|
||||||
|
why = reasons.get((path, role))
|
||||||
|
if why:
|
||||||
|
print(' 原因: %s' % why)
|
||||||
|
if pending:
|
||||||
|
print('[apps/pbls] 修法提示: 角色名须是平台可解析口径(%s 为特殊角色;'
|
||||||
|
'%s 为已知具名角色)。自造角色名会让 set_role_perm.py exit(1),'
|
||||||
|
'端点上线即 403。跑 `--check` 可在注册前离线查出全部非法角色名。'
|
||||||
|
% ('/'.join(SPECIAL_ROLES), '/'.join(KNOWN_NAMED_ROLES)))
|
||||||
return [p for p, _r in pending]
|
return [p for p, _r in pending]
|
||||||
|
|
||||||
|
|
||||||
@ -385,13 +448,21 @@ def main():
|
|||||||
for p in bad:
|
for p in bad:
|
||||||
print('WILDCARD FORBIDDEN: ' + p)
|
print('WILDCARD FORBIDDEN: ' + p)
|
||||||
|
|
||||||
|
# 门禁 3:角色词汇(离线可判定,无需 DB)。必须排在 --list / --check /
|
||||||
|
# register 三条分支之前——三条都用 role_bad 作返回口径,早于此引用即 NameError。
|
||||||
|
role_bad = audit_roles(paths)
|
||||||
|
for path, role, why in role_bad:
|
||||||
|
print('ROLE UNRESOLVABLE: %s %s' % (role, path))
|
||||||
|
print(' %s' % why)
|
||||||
|
|
||||||
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 if (not bad and not failures) else 1
|
return 0 if (not bad and not failures and not role_bad) else 1
|
||||||
|
|
||||||
if args.check:
|
if args.check:
|
||||||
# 门禁 1:通配符(零容忍);门禁 2:聚合完整性(有脚本却聚合失败 = FAIL)
|
# 门禁 1:通配符(零容忍);门禁 2:聚合完整性(有脚本却聚合失败 = FAIL);
|
||||||
|
# 门禁 3:角色名可解析性(自造角色名 → 注册必失败 → 端点 403)
|
||||||
print('[apps/pbls] consistency: module_scripts=%d aggregated=%d '
|
print('[apps/pbls] consistency: module_scripts=%d aggregated=%d '
|
||||||
'zero_path_declarations=%d failed_modules=%d'
|
'zero_path_declarations=%d failed_modules=%d'
|
||||||
% (len([n for n in notes if 'no scripts/load_path.py' not in n]),
|
% (len([n for n in notes if 'no scripts/load_path.py' not in n]),
|
||||||
@ -400,13 +471,15 @@ def main():
|
|||||||
len(failures)))
|
len(failures)))
|
||||||
for f in failures:
|
for f in failures:
|
||||||
print('AGGREGATION FAILED: ' + f)
|
print('AGGREGATION FAILED: ' + f)
|
||||||
ok = (not bad) and (not failures)
|
ok = (not bad) and (not failures) and (not role_bad)
|
||||||
print('[apps/pbls] check: total=%d wildcard=%d aggregation_failures=%d -> %s'
|
print('[apps/pbls] check: total=%d wildcard=%d aggregation_failures=%d '
|
||||||
% (len(paths), len(bad), len(failures), 'PASS' if ok else 'FAIL'))
|
'unresolvable_roles=%d -> %s'
|
||||||
|
% (len(paths), len(bad), len(failures), len(role_bad),
|
||||||
|
'PASS' if ok else 'FAIL'))
|
||||||
return 0 if ok else 1
|
return 0 if ok else 1
|
||||||
|
|
||||||
pending = register(paths)
|
pending = register(paths)
|
||||||
return 0 if (not pending and not bad and not failures) else 1
|
return 0 if (not pending and not bad and not failures and not role_bad) else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user