feat(pbls): 初始提交 - pbls 项目模块代码入库
This commit is contained in:
parent
6cc922e46a
commit
a494bd23e6
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
.DS_Store
|
||||
*.log
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.venv/
|
||||
venv/
|
||||
111
app/pbls.py
Normal file
111
app/pbls.py
Normal file
@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbls —— PBL Agent OS 应用唯一入口(端口 9300,见 projects/pbls/env/test.json:port)。
|
||||
|
||||
框架铁律(web-application-spec):
|
||||
- 入口必须 `from ahserver.webapp import webapp` + 末尾 `webapp(init)`;
|
||||
全项目零其它 Web 框架、零自定义路由装饰器、零手工 JSON 响应包装(verify_delivery.py 扫描零命中)。
|
||||
- 加载顺序固定:`ServerEnv()` → 基础模块(appbase/rbac/pybricks)→ 业务模块。
|
||||
基础模块必须**显式调用 loader**(load_appbase/load_rbac/load_pybricks),
|
||||
只 importlib.import_module 就标 loaded 是反模式(登录/权限链路必崩)。
|
||||
- 库名由应用统一决定:get_module_dbname(m) 从 conf/config.json 的 module_dbmap 读,
|
||||
模块侧禁止硬编码 DBNAME。
|
||||
- 端口唯一来源 = projects/pbls/env/{test,prod}.json 的 port;应用仓库不设 env/ 目录。
|
||||
|
||||
挂载规模对账(QC #13):20 个 load_* 调用 = 3 平台基础模块(appbase/rbac/pybricks)
|
||||
+ 5 复用基座模块(world/scene/entity/script_engine/scense_game)
|
||||
+ 12 个 pbl_* 模块(见下 MODULE_LOAD_ORDER,编号 M1~M12 与 modules/*.md 一一对应)。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
from ahserver.webapp import webapp
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
import bricks_for_python # registers bui processor
|
||||
from bricks_for_python.init import load_pybricks # registers UiWindow etc.
|
||||
from appbase.init import load_appbase
|
||||
from rbac.init import load_rbac
|
||||
|
||||
CONF = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
'conf', 'config.json')
|
||||
# 12 个 pbl_* 业务模块(编号 ↔ 目录,M10=pbl_appcodes 零表、M12=pbl_common 内核)
|
||||
MODULE_LOAD_ORDER = [
|
||||
('pbl', 'pbl_common'), # M12 公共内核(必须最先,提供租户/DB 出口)
|
||||
('pbl', 'pbl_appcodes'), # M10 枚举 6+2 组幂等注入
|
||||
('pbl', 'pbl_blueprint'), # M1 蓝图聚合根 + 子对象 + 版本 + 模板
|
||||
('pbl', 'pbl_validation'), # M2 14 维校验 + 5 级质量状态
|
||||
('pbl', 'pbl_compiler'), # M3 确定性编译 + Game Definition + Capability
|
||||
('pbl', 'pbl_agent_runtime'), # M4 Designer/Critic + fail-closed 工具裁决
|
||||
('pbl', 'pbl_domain_ext'), # M8 world/scene/entity 薄扩展(不改基表)
|
||||
('pbl', 'pbl_runtime_ext'), # M11 单事务事件+状态写入与广播
|
||||
('pbl', 'pbl_evidence'), # M5 产出物 + 证据幂等采集
|
||||
('pbl', 'pbl_assessment'), # M6 Rubric 加权评估与报告
|
||||
('pbl', 'pbl_kdb_ext'), # M7 KDB 只读桩 + 匿名聚合
|
||||
('pbl', 'pbl_scense_ext'), # M9 前端覆盖层上下文 + 页面注册
|
||||
]
|
||||
# 复用基座模块(只读引用 + 薄扩展,不改基表;缺失时降级并明确回报,不静默假成功)
|
||||
BASE_MODULE_LOADERS = ['world', 'scene', 'entity', 'script_engine', 'scense_game']
|
||||
|
||||
|
||||
def _conf():
|
||||
with open(CONF, encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def get_module_dbname(m):
|
||||
"""模块 → 库名映射(来自 conf/config.json:module_dbmap,禁止写死单个库名)。"""
|
||||
conf = _conf()
|
||||
mapping = conf.get('module_dbmap') or {}
|
||||
if m in mapping:
|
||||
return mapping[m]
|
||||
return mapping.get('*') or conf.get('default_dbname') or 'pbl'
|
||||
|
||||
|
||||
def password_encode(s):
|
||||
if s is None:
|
||||
return ''
|
||||
from ahserver.globalEnv import password_encode as _orig
|
||||
return _orig(s)
|
||||
|
||||
|
||||
def init():
|
||||
env = ServerEnv() # ← MUST be before load_rbac
|
||||
env.get_module_dbname = get_module_dbname
|
||||
env.password_encode = password_encode
|
||||
env.pbl_default_dbname = get_module_dbname('pbl')
|
||||
env.pbl_default_tenant = _conf().get('default_tenant') or 'default'
|
||||
|
||||
# ── 基础模块:显式调用 loader(反模式禁令:不得只 import 就标 loaded)──
|
||||
load_appbase()
|
||||
load_rbac()
|
||||
load_pybricks()
|
||||
|
||||
# ── 复用基座模块(world/scene/entity/script_engine/scense_game)──
|
||||
missing = []
|
||||
for name in BASE_MODULE_LOADERS:
|
||||
try:
|
||||
pkg = __import__('importlib').import_module('%s.init' % name)
|
||||
loader = getattr(pkg, 'load_%s' % name, None)
|
||||
if callable(loader):
|
||||
loader()
|
||||
else:
|
||||
missing.append(name) # 无 loader 即视为未挂载,不谎报 loaded
|
||||
except ImportError:
|
||||
missing.append(name)
|
||||
|
||||
# ── 12 个 pbl_* 业务模块(按 Wave 依赖顺序)──
|
||||
loaded = []
|
||||
for _db, name in MODULE_LOAD_ORDER:
|
||||
pkg = __import__('importlib').import_module('%s.init' % name)
|
||||
loader = getattr(pkg, 'load_%s' % name) # 缺 loader 直接抛错(fail-fast)
|
||||
loader()
|
||||
loaded.append(name)
|
||||
env.pbl_modules_loaded = loaded
|
||||
env.pbl_base_missing = missing
|
||||
print('[pbls] modules loaded: %d pbl + base missing=%s' % (len(loaded), missing or 'none'))
|
||||
return {'loaded': len(loaded) + 3, 'base_missing': missing}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
webapp(init)
|
||||
68
build.sh
Normal file
68
build.sh
Normal file
@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
# pbls 一键部署(web-application-spec 8 步;端口 9300 来源 projects/pbls/env/test.json)
|
||||
set -euo pipefail
|
||||
cdir=$(cd "$(dirname "$0")" && pwd)
|
||||
APP=pbls
|
||||
PORT=${PORT:-9300}
|
||||
DB_HOST=127.0.0.1; DB_PORT=3306; DB_USER=test; DB_PASS=test123; DB_NAME=pbl
|
||||
ENV_JSON="$cdir/../../projects/pbls/env/test.json"
|
||||
[ -f "$ENV_JSON" ] && PORT=$(python3 -c "import json;print(json.load(open('$ENV_JSON'))['port'])")
|
||||
echo "[build] app=$APP port=$PORT (env source: $ENV_JSON)"
|
||||
mkdir -p "$cdir/pkgs" "$cdir/logs" "$cdir/files"
|
||||
|
||||
# 1) 基础包 + 依赖
|
||||
pip install -q -r "$cdir/requirements.txt" || echo "[warn] pip requirements 需离线源,跳过"
|
||||
|
||||
# 2) 模块 clone + 安装(基础模块 + 复用基座 + 12 个 pbl_*)
|
||||
MODULES="world scene entity script_engine scense scense_game scense_runtime \
|
||||
pbl_common pbl_appcodes pbl_blueprint pbl_validation pbl_compiler pbl_agent_runtime \
|
||||
pbl_evidence pbl_assessment pbl_kdb_ext pbl_domain_ext pbl_scense_ext pbl_runtime_ext"
|
||||
for m in $MODULES; do
|
||||
src="$cdir/../../modules/$m"
|
||||
[ -d "$src" ] || { echo "[skip] module $m 不存在"; continue; }
|
||||
[ -d "$cdir/pkgs/$m" ] || cp -r "$src" "$cdir/pkgs/$m"
|
||||
if [ -f "$cdir/pkgs/$m/pyproject.toml" ]; then
|
||||
pip install -q -e "$cdir/pkgs/$m" || echo "[warn] pip install -e $m 失败,回退 PYTHONPATH"
|
||||
fi
|
||||
export PYTHONPATH="$cdir/pkgs:$PYTHONPATH"
|
||||
# 3) wwwroot 软链(非 cp,保持同步)
|
||||
ln -sfn "$cdir/pkgs/$m/wwwroot" "$cdir/wwwroot/$m"
|
||||
# 5) 建表:models/*.json → DDL(json2ddl),与 apps/pbls/scripts/ddl 同源
|
||||
if [ -d "$cdir/pkgs/$m/models" ]; then
|
||||
( cd "$cdir/pkgs/$m/models" && json2ddl mysql . > mysql.ddl.sql 2>/dev/null || true )
|
||||
[ -s "$cdir/pkgs/$m/models/mysql.ddl.sql" ] && \
|
||||
mysql -h "$DB_HOST" -P "$DB_PORT" -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" \
|
||||
< "$cdir/pkgs/$m/models/mysql.ddl.sql" || echo "[warn] DDL 执行跳过($m)"
|
||||
fi
|
||||
# 6) CRUD 生成:json/*.json → .dspy + .ui
|
||||
if [ -d "$cdir/pkgs/$m/json" ]; then
|
||||
( cd "$cdir/pkgs/$m/json" && xls2ui -m ../models -o ../wwwroot "$m" *.json 2>/dev/null || true )
|
||||
fi
|
||||
# 7) 初始化数据(appcodes 等种子)
|
||||
[ -f "$cdir/pkgs/$m/init/data.json" ] && echo "[info] $m 有 init/data.json,由 load_$m() 幂等注入"
|
||||
# 8) RBAC 路径注册
|
||||
[ -f "$cdir/pkgs/$m/scripts/load_path.py" ] && \
|
||||
python3 "$cdir/pkgs/$m/scripts/load_path.py" || echo "[warn] $m rbac 注册跳过"
|
||||
done
|
||||
|
||||
# 4) i18n 合并
|
||||
python3 "$cdir/scripts/merge_i18n.py" 2>/dev/null || echo "[info] i18n 合并跳过"
|
||||
|
||||
# 兜底建表(36 表,MariaDB 方言,幂等 CREATE TABLE IF NOT EXISTS)
|
||||
bash "$cdir/scripts/apply_ddl.sh" || echo "[warn] apply_ddl 跳过(离线/无库)"
|
||||
|
||||
# bricks 前端
|
||||
[ -d "$cdir/pkgs/bricks/dist" ] && ln -sfn "$cdir/pkgs/bricks/dist" "$cdir/wwwroot/bricks"
|
||||
|
||||
# 启动/停止脚本
|
||||
cat > "$cdir/start.sh" <<EOF
|
||||
#!/bin/bash
|
||||
cd "$cdir" && PYTHONPATH="$cdir/pkgs:\$PYTHONPATH" python3 app/$APP.py >> logs/$APP.out 2>&1 &
|
||||
echo \$! > $APP.pid
|
||||
EOF
|
||||
cat > "$cdir/stop.sh" <<EOF
|
||||
#!/bin/bash
|
||||
cd "$cdir" && [ -f $APP.pid ] && kill \$(cat $APP.pid) && rm -f $APP.pid
|
||||
EOF
|
||||
chmod +x "$cdir/start.sh" "$cdir/stop.sh"
|
||||
echo "[build] done. start: ./start.sh port: $PORT"
|
||||
69
conf/config.json
Normal file
69
conf/config.json
Normal file
@ -0,0 +1,69 @@
|
||||
{
|
||||
"appname": "pbls",
|
||||
"port": 9300,
|
||||
"port_source": "projects/pbls/env/test.json:port(端口唯一来源,应用不设 env/ 目录)",
|
||||
"default_dbname": "pbl",
|
||||
"default_tenant": "default",
|
||||
"module_dbmap": {
|
||||
"*": "pbl",
|
||||
"pbl_common": "pbl",
|
||||
"pbl_appcodes": "pbl",
|
||||
"pbl_blueprint": "pbl",
|
||||
"pbl_validation": "pbl",
|
||||
"pbl_compiler": "pbl",
|
||||
"pbl_agent_runtime": "pbl",
|
||||
"pbl_evidence": "pbl",
|
||||
"pbl_assessment": "pbl",
|
||||
"pbl_kdb_ext": "pbl",
|
||||
"pbl_domain_ext": "pbl",
|
||||
"pbl_scense_ext": "pbl",
|
||||
"pbl_runtime_ext": "pbl",
|
||||
"world": "pbl",
|
||||
"scene": "pbl",
|
||||
"entity": "pbl",
|
||||
"script_engine": "pbl",
|
||||
"scense_game": "pbl",
|
||||
"appbase": "pbl",
|
||||
"rbac": "pbl"
|
||||
},
|
||||
"password_key": "pbls-aes-key-0123456789abcdef",
|
||||
"logger": {
|
||||
"name": "pbls",
|
||||
"level": "INFO",
|
||||
"file": "$[workdir]$/logs/pbls.log"
|
||||
},
|
||||
"filesroot": "$[workdir]$/files",
|
||||
"databases": {
|
||||
"pbl": {
|
||||
"driver": "mysql",
|
||||
"kwargs": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 3306,
|
||||
"unix_socket": null,
|
||||
"user": "test",
|
||||
"password": "test123",
|
||||
"db": "pbl",
|
||||
"charset": "utf8mb4"
|
||||
},
|
||||
"note": "本机 MySQL 走 socket 会失败(/run/mysqld/mysqld.sock 不存在),必须显式 TCP 127.0.0.1:3306 且 unix_socket=null"
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"port": 9300,
|
||||
"root": "$[workdir]$/wwwroot",
|
||||
"indexes": ["index.ui", "index.html"],
|
||||
"processors": [
|
||||
[".tmpl", "tmpl"],
|
||||
[".ui", "bui"],
|
||||
[".dspy", "dspy"],
|
||||
[".wss", "ws"],
|
||||
[".ws", "ws"]
|
||||
],
|
||||
"session": {
|
||||
"secret": "pbls-session-secret-please-rotate",
|
||||
"cookie_name": "pbls_session",
|
||||
"max_age": 86400
|
||||
},
|
||||
"rbac": true
|
||||
}
|
||||
}
|
||||
17
requirements.txt
Normal file
17
requirements.txt
Normal file
@ -0,0 +1,17 @@
|
||||
# pbls 应用依赖(硬门禁 6.5 / QC #4)
|
||||
# ── 平台基础依赖(Web 框架 + DB 抽象 + 基础模块,缺一不可)────────────────
|
||||
ahserver # Web 框架(唯一入口 webapp(init) + BricksUIProcessor 渲染 .ui)
|
||||
bricks_for_python # bricks Python 侧(load_pybricks 注册 UiWindow 等)
|
||||
sqlor # DB 抽象层(只用 sor.C/U/D/R/I/sqlExe)
|
||||
apppublic # 公共工具(db.sqlorContext / curDateString 等)
|
||||
appbase # 应用基础(params 表,load_appbase 显式调用)
|
||||
rbac # 登录与权限(load_rbac 显式调用;scripts/load_path.py 注册路径)
|
||||
accounting # 平台基础(编码/序列,复用不改)
|
||||
# ── 数据库驱动(缺 DB 驱动即连不上库)────────────────────────────────────
|
||||
pymysql # MariaDB/MySQL Python 驱动(TCP 127.0.0.1:3306)
|
||||
cryptography # pymysql caching_sha2_password 认证依赖
|
||||
# ── 建表/CRUD 生成工具链(build.sh 使用)─────────────────────────────────
|
||||
xls2ddl # models/*.json → mysql.ddl.sql(json2ddl)
|
||||
# ── 复用基座模块(由 build.sh git clone + pip install -e 安装,此处仅登记)──
|
||||
# world / scene / entity / script_engine / scense / scense_game / scense_runtime
|
||||
# (机构 modules/ 仓库,非 PyPI 包,禁止在此写死版本号)
|
||||
23
scripts/apply_ddl.sh
Normal file
23
scripts/apply_ddl.sh
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
# 幂等落库:36 表 DDL + appcodes 注入(MariaDB 10.x,显式 TCP,禁 unix_socket)
|
||||
set -euo pipefail
|
||||
APP_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DB_HOST="${PBLS_DB_HOST:-127.0.0.1}"
|
||||
DB_PORT="${PBLS_DB_PORT:-3306}"
|
||||
DB_USER="${PBLS_DB_USER:-}"
|
||||
DB_NAME="${PBLS_DB_NAME:-pbls}"
|
||||
|
||||
echo "[ddl] target tcp://${DB_HOST}:${DB_PORT}/${DB_NAME} (unix_socket disabled)"
|
||||
mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} \
|
||||
-e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;"
|
||||
|
||||
mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} \
|
||||
--default-character-set=utf8mb4 "$DB_NAME" < "$APP_HOME/scripts/ddl/pbls_tables.sql"
|
||||
|
||||
TABLES=$(mysql --protocol=TCP -h "$DB_HOST" -P "$DB_PORT" -u "$DB_USER" ${PBLS_DB_PASSWORD:+-p"$PBLS_DB_PASSWORD"} -N -B -e \
|
||||
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='$DB_NAME' AND table_name LIKE 'pbl_%';")
|
||||
echo "[ddl] pbl_* tables in $DB_NAME = $TABLES (expect >= 36)"
|
||||
|
||||
# appcodes 6+2 组幂等注入
|
||||
PYTHONPATH="$(dirname "$APP_HOME")/modules:$APP_HOME/lib:${PYTHONPATH:-}" \
|
||||
python3 -c "import sys; sys.path.insert(0,'$(dirname "$APP_HOME")/modules'); import pbl_appcodes; print(pbl_appcodes.inject_all())"
|
||||
668
scripts/ddl/pbls_tables.sql
Normal file
668
scripts/ddl/pbls_tables.sql
Normal file
@ -0,0 +1,668 @@
|
||||
-- pbls 36 表 DDL(MariaDB 10.x 方言,由 scripts/gen_artifacts.py 从同一 spec 生成)
|
||||
-- 硬约束:tenant_id 打头 / id 为 BIGINT UNSIGNED AUTO_INCREMENT 主键;-- 不使用 外键约束 / 枚举列类型 / 时间戳列类型 / 位图列类型(枚举值走 appcodes 编码表,时间列走 DATETIME 由应用层写入)
|
||||
-- 方言来源:projects/pbls/env/test.json -> db.engine = mariadb(禁 BIGSERIAL/SERIAL/nextval 等 PG 语法)
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
-- ===== pbl_blueprint (M1) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "蓝图编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`subject` VARCHAR(64) NOT NULL COMMENT "学科",
|
||||
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
|
||||
`description` TEXT NULL COMMENT "说明",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态 pbl_quality_state",
|
||||
`generation_source` VARCHAR(64) NOT NULL COMMENT "生成来源 llm/template_fallback/manual",
|
||||
`approval_status` VARCHAR(64) NOT NULL COMMENT "审批状态",
|
||||
`visibility` VARCHAR(64) NOT NULL COMMENT "可见范围",
|
||||
`content_json` LONGTEXT NULL COMMENT "蓝图正文(结构化模型,非散文)",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "当前版本",
|
||||
`compiler_version_id` BIGINT UNSIGNED NOT NULL COMMENT "编译版本",
|
||||
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "编译产物落 world 的溯源ID",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "创建人",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_bp_tenant_code` (`tenant_id`, `code`),
|
||||
KEY `idx_bp_tenant_state` (`tenant_id`, `quality_state`),
|
||||
KEY `idx_bp_tenant_creator` (`tenant_id`, `created_by`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="PBL 蓝图聚合根(第8章 schema 落库)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_learning_goal` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "目标编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`description` TEXT NULL COMMENT "描述",
|
||||
`bloom_level` VARCHAR(64) NOT NULL COMMENT "布鲁姆层级",
|
||||
`measurable` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "是否可测",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_learning_goal_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 学习目标";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_role` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "角色编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "角色名",
|
||||
`persona_txt` TEXT NULL COMMENT "人设",
|
||||
`capabilities_json` LONGTEXT NULL COMMENT "能力引用",
|
||||
`headcount` INT NOT NULL DEFAULT 0 COMMENT "名额",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_role_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - PBL 角色";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_mission` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "任务域编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "驱动问题域",
|
||||
`scenario_txt` TEXT NULL COMMENT "情境",
|
||||
`unlock_condition_json` LONGTEXT NULL COMMENT "解锁条件",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_mission_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 驱动问题/Mission";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_task` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`mission_id` BIGINT UNSIGNED NOT NULL COMMENT "所属Mission",
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "任务名",
|
||||
`steps_json` LONGTEXT NULL COMMENT "步骤",
|
||||
`difficulty` VARCHAR(64) NOT NULL COMMENT "难度",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_task_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 任务";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_artifact_spec` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "产出物名",
|
||||
`artifact_type` VARCHAR(64) NOT NULL COMMENT "产出类型",
|
||||
`required_fields_json` LONGTEXT NULL COMMENT "必填字段",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_artifact_spec_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 产出物规格";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_evidence_spec` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`evidence_type` VARCHAR(64) NOT NULL COMMENT "证据类型",
|
||||
`source` VARCHAR(64) NOT NULL COMMENT "来源",
|
||||
`min_count` INT NOT NULL DEFAULT 0 COMMENT "最少条数",
|
||||
`rubric_criterion_uid` VARCHAR(32) NOT NULL COMMENT "对齐的评分准则UID",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_blueprint_evidence_spec_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 证据规格";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_reflection_spec` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "所属蓝图",
|
||||
`obj_uid` VARCHAR(32) NOT NULL COMMENT "子对象UID",
|
||||
`trigger` VARCHAR(64) NOT NULL COMMENT "触发点",
|
||||
`prompt_txt` TEXT NULL COMMENT "反思引导语",
|
||||
`min_words` INT NOT NULL DEFAULT 0 COMMENT "最少字数",
|
||||
`evidence_type` VARCHAR(64) NOT NULL COMMENT "产出证据类型",
|
||||
`order_no` INT NOT NULL DEFAULT 0 COMMENT "排序",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_bp_rs_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图子对象 - 反思规格";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_blueprint_version` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本号",
|
||||
`snapshot_json` LONGTEXT NULL COMMENT "全量快照",
|
||||
`change_delta_json` LONGTEXT NULL COMMENT "结构化增量",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
|
||||
`generation_source` VARCHAR(64) NOT NULL COMMENT "生成来源",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "操作人",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_bpv` (`tenant_id`, `blueprint_id`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="蓝图版本(change_delta 证明对话式改模型)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_template` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "模板编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`subject` VARCHAR(64) NOT NULL COMMENT "学科",
|
||||
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
|
||||
`skeleton_json` LONGTEXT NULL COMMENT "骨架",
|
||||
`fallback_slot_json` LONGTEXT NULL COMMENT "兜底槽位(LLM不可用时填充)",
|
||||
`is_builtin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "内置",
|
||||
`use_count` INT NOT NULL DEFAULT 0 COMMENT "实例化次数",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tpl_tenant_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="PBL 模板(含离线兜底槽位)";
|
||||
|
||||
-- ===== pbl_validation (M2) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_validation_rule` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "规则编码",
|
||||
`dimension` VARCHAR(64) NOT NULL COMMENT "维度",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`rule_json` LONGTEXT NULL COMMENT "规则体",
|
||||
`threshold_json` LONGTEXT NULL COMMENT "阈值(可配,Q-OPEN-8)",
|
||||
`severity` VARCHAR(64) NOT NULL COMMENT "严重度",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_vr_code` (`tenant_id`, `code`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验规则(14 维,规则 JSON 形态 script_type=1)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_validation_result` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图ID",
|
||||
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "校验时版本",
|
||||
`dimension_count` INT NOT NULL DEFAULT 0 COMMENT "维度数(=14)",
|
||||
`passed_count` INT NOT NULL DEFAULT 0 COMMENT "通过数",
|
||||
`failed_count` INT NOT NULL DEFAULT 0 COMMENT "失败数",
|
||||
`score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "得分",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "质量状态",
|
||||
`findings_json` LONGTEXT NULL COMMENT "逐维结论",
|
||||
`rule_set_version` VARCHAR(64) NOT NULL COMMENT "规则集版本",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "触发人",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_vres_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="校验结果(14 维 + 5 级质量状态)";
|
||||
|
||||
-- ===== pbl_compiler (M3) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_compiler_version` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "版本编码",
|
||||
`semver` VARCHAR(64) NOT NULL COMMENT "语义化版本",
|
||||
`rules_hash` VARCHAR(32) NOT NULL COMMENT "规则集SHA256前缀",
|
||||
`entrypoint` VARCHAR(64) NOT NULL COMMENT "入口函数",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`notes` TEXT NULL COMMENT "说明",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cv_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="编译器版本(29.6 确定性)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_game_definition` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "来源蓝图",
|
||||
`blueprint_version_no` INT NOT NULL DEFAULT 0 COMMENT "蓝图版本",
|
||||
`compiler_version_id` BIGINT UNSIGNED NOT NULL COMMENT "编译版本",
|
||||
`content_fingerprint` VARCHAR(32) NOT NULL COMMENT "SHA-256 指纹",
|
||||
`definition_json` LONGTEXT NULL COMMENT "Game Definition 正文",
|
||||
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 world",
|
||||
`scene_id` BIGINT UNSIGNED NOT NULL COMMENT "落库 scene",
|
||||
`entity_count` INT NOT NULL DEFAULT 0 COMMENT "实体数",
|
||||
`event_count` INT NOT NULL DEFAULT 0 COMMENT "事件数",
|
||||
`quality_state` VARCHAR(64) NOT NULL COMMENT "编译时质量状态",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_gd_fp` (`tenant_id`, `content_fingerprint`),
|
||||
KEY `idx_gd_bp` (`tenant_id`, `blueprint_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Game Definition(第9章 schema + 指纹)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_capability_registry` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`capability_key` VARCHAR(64) NOT NULL COMMENT "能力键",
|
||||
`category` VARCHAR(64) NOT NULL COMMENT "分类",
|
||||
`args_schema_json` LONGTEXT NULL COMMENT "参数JSON Schema",
|
||||
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`description` TEXT NULL COMMENT "说明",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cap_key` (`tenant_id`, `capability_key`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Capability Registry(第11章缺口补齐)";
|
||||
|
||||
-- ===== pbl_agent_runtime (M4) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_def` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "Agent编码",
|
||||
`agent_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`name` VARCHAR(128) NOT NULL COMMENT "名称",
|
||||
`model_route` VARCHAR(64) NOT NULL COMMENT "pipeline-llm 路由",
|
||||
`system_prompt_txt` TEXT NULL COMMENT "提示词",
|
||||
`tool_whitelist_json` LONGTEXT NULL COMMENT "工具白名单",
|
||||
`permission_mode` VARCHAR(64) NOT NULL COMMENT "read/write 模式",
|
||||
`is_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ad_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 定义(Designer/Critic)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_tool_registry` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`agent_scope` VARCHAR(64) NOT NULL COMMENT "作用域",
|
||||
`permission_required` VARCHAR(64) NOT NULL COMMENT "所需权限",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`params_schema_json` LONGTEXT NULL COMMENT "参数白名单Schema",
|
||||
`disabled_reason` VARCHAR(64) NOT NULL COMMENT "禁用原因",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tr_key` (`tenant_id`, `tool_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具注册表(13 启用 / 9 禁用含 pbl.publish)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_trace` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`trace_uid` VARCHAR(32) NOT NULL COMMENT "轨迹UID",
|
||||
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "目标蓝图",
|
||||
`input_ref_json` LONGTEXT NULL COMMENT "要素1 输入引用",
|
||||
`thought_txt` TEXT NULL COMMENT "要素2 思考",
|
||||
`action_code` VARCHAR(64) NOT NULL COMMENT "要素3 动作",
|
||||
`action_params_json` LONGTEXT NULL COMMENT "要素4 动作参数",
|
||||
`observation_json` LONGTEXT NULL COMMENT "要素5 观察",
|
||||
`result_state` VARCHAR(64) NOT NULL COMMENT "要素6 结果状态",
|
||||
`llm_calls` INT NOT NULL DEFAULT 0 COMMENT "要素7 LLM调用数",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
|
||||
`created_by` VARCHAR(32) NOT NULL COMMENT "触发用户",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_at_uid` (`tenant_id`, `trace_uid`),
|
||||
KEY `idx_at_tenant` (`tenant_id`, `created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Agent 执行轨迹(第28章 7 要素)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_agent_tool_call` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`trace_id` BIGINT UNSIGNED NOT NULL COMMENT "轨迹ID",
|
||||
`tool_key` VARCHAR(64) NOT NULL COMMENT "工具键",
|
||||
`request_json` LONGTEXT NULL COMMENT "请求",
|
||||
`response_json` LONGTEXT NULL COMMENT "响应",
|
||||
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
|
||||
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
|
||||
`adjudication_code` VARCHAR(64) NOT NULL COMMENT "裁决结论",
|
||||
`duration_ms` INT NOT NULL DEFAULT 0 COMMENT "耗时",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_tc_trace` (`tenant_id`, `trace_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="工具调用明细(服务端裁决留痕)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_approval` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`approval_uid` VARCHAR(32) NOT NULL COMMENT "审批UID",
|
||||
`approval_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`target_type` VARCHAR(64) NOT NULL COMMENT "对象类型",
|
||||
`target_id` BIGINT UNSIGNED NOT NULL COMMENT "对象ID",
|
||||
`payload_json` LONGTEXT NULL COMMENT "变更内容",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`requested_by` VARCHAR(32) NOT NULL COMMENT "发起人",
|
||||
`decided_by` VARCHAR(32) NOT NULL COMMENT "决策人",
|
||||
`decided_at` DATETIME NULL NULL COMMENT "决策时间",
|
||||
`comment_txt` TEXT NULL COMMENT "意见",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ap_uid` (`tenant_id`, `approval_uid`),
|
||||
KEY `idx_ap_status` (`tenant_id`, `status`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="人工审批单(14.2 四类,无绕过路径)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_llm_call_log` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`call_uid` VARCHAR(32) NOT NULL COMMENT "调用UID",
|
||||
`agent_code` VARCHAR(64) NOT NULL COMMENT "Agent",
|
||||
`route` VARCHAR(64) NOT NULL COMMENT "路由",
|
||||
`model` VARCHAR(64) NOT NULL COMMENT "实际模型",
|
||||
`prompt_hash` VARCHAR(32) NOT NULL COMMENT "提示词哈希",
|
||||
`completion_tokens` INT NOT NULL DEFAULT 0 COMMENT "tokens",
|
||||
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`error_code` VARCHAR(64) NOT NULL COMMENT "错误码",
|
||||
`fallback_used` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "走兜底",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_lc_uid` (`tenant_id`, `call_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="LLM 调用日志(超时/重试/限流/兜底留痕)";
|
||||
|
||||
-- ===== pbl_evidence (M5) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_artifact` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`artifact_uid` VARCHAR(32) NOT NULL COMMENT "产出UID",
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
|
||||
`team_id` BIGINT UNSIGNED NOT NULL COMMENT "团队",
|
||||
`creator_id` VARCHAR(32) NOT NULL COMMENT "作者",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`artifact_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`content_json` LONGTEXT NULL COMMENT "内容",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`submitted_at` DATETIME NULL NULL COMMENT "提交时间",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_art_uid` (`tenant_id`, `artifact_uid`),
|
||||
KEY `idx_art_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学生产出物(版本内联 version_no)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_evidence` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`artifact_id` BIGINT UNSIGNED NOT NULL COMMENT "产出物",
|
||||
`evidence_type` VARCHAR(64) NOT NULL COMMENT "证据类型",
|
||||
`source_event_id` VARCHAR(32) NOT NULL COMMENT "来源 runtime_event",
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`learner_id` VARCHAR(32) NOT NULL COMMENT "学习者",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
|
||||
`payload_json` LONGTEXT NULL COMMENT "证据体",
|
||||
`occurred_at` DATETIME NOT NULL COMMENT "发生时间",
|
||||
`dedup_key` VARCHAR(32) NOT NULL COMMENT "去重键",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ev_dedup` (`tenant_id`, `source_event_id`, `evidence_type`),
|
||||
KEY `idx_ev_learner` (`tenant_id`, `learner_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="学习证据(幂等采集,唯一索引防重)";
|
||||
|
||||
-- ===== pbl_assessment (M6) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_rubric` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "编码",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
|
||||
`criteria_json` LONGTEXT NULL COMMENT "准则数组(含 learning_goal_id/weight)",
|
||||
`total_weight` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "权重合计",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`approval_id` BIGINT UNSIGNED NOT NULL COMMENT "变更审批单",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ru_code` (`tenant_id`, `code`, `version_no`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Rubric(准则内联 criteria_json,权重合计=100%)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_assessment_record` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`record_uid` VARCHAR(32) NOT NULL COMMENT "记录UID",
|
||||
`learner_id` VARCHAR(32) NOT NULL COMMENT "学习者",
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
|
||||
`artifact_id` BIGINT UNSIGNED NOT NULL COMMENT "产出物",
|
||||
`evidence_id` BIGINT UNSIGNED NOT NULL COMMENT "主证据",
|
||||
`rubric_id` BIGINT UNSIGNED NOT NULL COMMENT "Rubric",
|
||||
`scores_json` LONGTEXT NULL COMMENT "逐准则得分",
|
||||
`total_score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "加权总分",
|
||||
`max_score` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT "满分",
|
||||
`band` VARCHAR(64) NOT NULL COMMENT "等级",
|
||||
`alignment_json` LONGTEXT NULL COMMENT "与学习目标对齐",
|
||||
`feedback_txt` TEXT NULL COMMENT "反馈",
|
||||
`assessor_type` VARCHAR(64) NOT NULL COMMENT "评估者类型",
|
||||
`assessor_id` VARCHAR(32) NOT NULL COMMENT "评估者",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ar_uid` (`tenant_id`, `record_uid`),
|
||||
KEY `idx_ar_learner` (`tenant_id`, `learner_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="评估记录(total_score 派生 + 对齐判定)";
|
||||
|
||||
-- ===== pbl_kdb_ext (M7) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_kdb_item` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`code` VARCHAR(64) NOT NULL COMMENT "条目编码",
|
||||
`item_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`summary_txt` TEXT NULL COMMENT "摘要",
|
||||
`payload_json` LONGTEXT NULL COMMENT "内容(无向量,Q-OPEN-5)",
|
||||
`source` VARCHAR(64) NOT NULL COMMENT "来源",
|
||||
`is_write_locked` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "写锁(恒1)",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`version_no` INT NOT NULL DEFAULT 0 COMMENT "版本",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_kdb_code` (`tenant_id`, `code`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="KDB 条目(建表不写入,is_write_locked 恒 1)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_kdb_query_log` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`query_uid` VARCHAR(32) NOT NULL COMMENT "查询UID",
|
||||
`caller_type` VARCHAR(64) NOT NULL COMMENT "调用方类型",
|
||||
`caller_id` VARCHAR(32) NOT NULL COMMENT "调用方",
|
||||
`query_json` LONGTEXT NULL COMMENT "查询条件",
|
||||
`hit_count` INT NOT NULL DEFAULT 0 COMMENT "命中数",
|
||||
`latency_ms` INT NOT NULL DEFAULT 0 COMMENT "时延",
|
||||
`ok` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "成功",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_kql_uid` (`tenant_id`, `query_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="KDB 检索日志(只读侧留痕)";
|
||||
|
||||
-- ===== pbl_domain_ext (M8) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_tenant` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`tenant_uid` VARCHAR(32) NOT NULL COMMENT "租户UID",
|
||||
`name` VARCHAR(128) NOT NULL COMMENT "名称",
|
||||
`kind` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`plan` VARCHAR(64) NOT NULL COMMENT "套餐",
|
||||
`seat_limit` INT NOT NULL DEFAULT 0 COMMENT "席位",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`config_json` LONGTEXT NULL COMMENT "配置",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_ten_uid` (`tenant_id`, `tenant_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="租户(多租户边界第25章,不改基表)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_class` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`class_uid` VARCHAR(32) NOT NULL COMMENT "班级UID",
|
||||
`name` VARCHAR(128) NOT NULL COMMENT "名称",
|
||||
`grade` VARCHAR(64) NOT NULL COMMENT "年级",
|
||||
`teacher_id` VARCHAR(32) NOT NULL COMMENT "教师",
|
||||
`student_count` INT NOT NULL DEFAULT 0 COMMENT "学生数",
|
||||
`roster_json` LONGTEXT NULL COMMENT "花名册",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_cls_uid` (`tenant_id`, `class_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="班级(花名册内联 roster_json)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_team` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`team_uid` VARCHAR(32) NOT NULL COMMENT "团队UID",
|
||||
`class_id` BIGINT UNSIGNED NOT NULL COMMENT "班级",
|
||||
`name` VARCHAR(128) NOT NULL COMMENT "名称",
|
||||
`mission_id` BIGINT UNSIGNED NOT NULL COMMENT "Mission",
|
||||
`members_json` LONGTEXT NULL COMMENT "成员",
|
||||
`capacity` INT NOT NULL DEFAULT 0 COMMENT "容量",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "绑定会话",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_team_uid` (`tenant_id`, `team_uid`),
|
||||
KEY `idx_team_class` (`tenant_id`, `class_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="团队(成员内联 members_json,不改基表)";
|
||||
|
||||
-- ===== pbl_scense_ext (M9) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_session_member` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`user_id` VARCHAR(32) NOT NULL COMMENT "用户",
|
||||
`role_code` VARCHAR(64) NOT NULL COMMENT "角色",
|
||||
`team_id` BIGINT UNSIGNED NOT NULL COMMENT "团队",
|
||||
`joined_at` DATETIME NULL NULL COMMENT "加入时间",
|
||||
`last_seen_at` DATETIME NULL NULL COMMENT "心跳",
|
||||
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "已见状态版本",
|
||||
`status` VARCHAR(64) NOT NULL COMMENT "状态",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_sm` (`tenant_id`, `session_id`, `user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="会话成员(复用 world_session,不污染基表)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_playtest_feedback` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`user_id` VARCHAR(32) NOT NULL COMMENT "用户",
|
||||
`blueprint_id` BIGINT UNSIGNED NOT NULL COMMENT "蓝图",
|
||||
`feedback_type` VARCHAR(64) NOT NULL COMMENT "类型",
|
||||
`rating` INT NOT NULL DEFAULT 0 COMMENT "评分",
|
||||
`content_txt` TEXT NULL COMMENT "内容",
|
||||
`context_json` LONGTEXT NULL COMMENT "上下文",
|
||||
`handled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "已处理",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_pf_session` (`tenant_id`, `session_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="Playtest 反馈/反思(reflection 走证据链外的轻量表)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_ui_page_registry` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`page_key` VARCHAR(64) NOT NULL COMMENT "页面键",
|
||||
`title` VARCHAR(128) NOT NULL COMMENT "标题",
|
||||
`route` VARCHAR(64) NOT NULL COMMENT "路由",
|
||||
`widget_type` VARCHAR(64) NOT NULL COMMENT "渲染组件",
|
||||
`requires_role` VARCHAR(64) NOT NULL COMMENT "所需角色",
|
||||
`budget_ms` INT NOT NULL DEFAULT 0 COMMENT "渲染预算",
|
||||
`enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT "启用",
|
||||
`meta_json` LONGTEXT NULL COMMENT "元数据",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_upr_key` (`tenant_id`, `page_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="前端覆盖层页面注册(12 页面 + 时延预算)";
|
||||
|
||||
-- ===== pbl_runtime_ext (M11) =====
|
||||
CREATE TABLE IF NOT EXISTS `pbl_runtime_event` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`event_uid` VARCHAR(32) NOT NULL COMMENT "事件UID",
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`world_id` BIGINT UNSIGNED NOT NULL COMMENT "世界",
|
||||
`actor_id` VARCHAR(32) NOT NULL COMMENT "触发者",
|
||||
`event_type` VARCHAR(64) NOT NULL COMMENT "事件类型",
|
||||
`payload_json` LONGTEXT NULL COMMENT "事件体",
|
||||
`causation_id` VARCHAR(32) NOT NULL COMMENT "因果链",
|
||||
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "状态版本",
|
||||
`seq` BIGINT NOT NULL DEFAULT 0 COMMENT "会话内单调序号",
|
||||
`occurred_at` DATETIME NOT NULL COMMENT "发生时间",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_re_uid` (`tenant_id`, `event_uid`, `occurred_at`),
|
||||
KEY `idx_re_session_seq` (`tenant_id`, `session_id`, `seq`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="运行时事件(append-only,按月 RANGE 分区)",
|
||||
KEY `idx_re_part` (`occurred_at`)
|
||||
PARTITION BY RANGE COLUMNS(`occurred_at`) (
|
||||
PARTITION p2026 VALUES LESS THAN ('2026-01-01'),
|
||||
PARTITION p2027 VALUES LESS THAN ('2027-01-01'),
|
||||
PARTITION p2028 VALUES LESS THAN ('2028-01-01'),
|
||||
PARTITION p2029 VALUES LESS THAN ('2029-01-01'),
|
||||
PARTITION p2030 VALUES LESS THAN ('2030-01-01'),
|
||||
PARTITION p2031 VALUES LESS THAN ('2031-01-01')
|
||||
PARTITION pmax VALUES LESS THAN (MAXVALUE)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_entity_state` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`entity_id` VARCHAR(32) NOT NULL COMMENT "实体标识",
|
||||
`state_json` LONGTEXT NULL COMMENT "状态",
|
||||
`state_version` BIGINT NOT NULL DEFAULT 0 COMMENT "单调版本",
|
||||
`checksum` VARCHAR(32) NOT NULL COMMENT "状态摘要",
|
||||
`updated_by` VARCHAR(32) NOT NULL COMMENT "写入者",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_es` (`tenant_id`, `session_id`, `entity_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="实体状态(服务端权威,state_version 客户端禁写)";
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `pbl_world_state_snapshot` (
|
||||
`tenant_id` VARCHAR(32) NOT NULL NOT NULL COMMENT 租户ID(强制打头),
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键,
|
||||
`session_id` BIGINT UNSIGNED NOT NULL COMMENT "会话",
|
||||
`snapshot_uid` VARCHAR(32) NOT NULL COMMENT "快照UID",
|
||||
`state_fingerprint` VARCHAR(32) NOT NULL COMMENT "状态指纹",
|
||||
`entities_json` LONGTEXT NULL COMMENT "实体集",
|
||||
`events_through_seq` BIGINT NOT NULL DEFAULT 0 COMMENT "截至序号",
|
||||
`reason` VARCHAR(64) NOT NULL COMMENT "触发原因",
|
||||
`created_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "创建时间(应用层写入)",
|
||||
`updated_at` DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00' COMMENT "更新时间(应用层写入)",
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_wss_uid` (`tenant_id`, `snapshot_uid`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT="世界状态快照(掉线补齐/离线兜底)";
|
||||
|
||||
-- ===== 枚举 6+2 组由 pbl_appcodes.load_pbl_appcodes() 幂等注入 appcodes,本文件不含 INSERT =====
|
||||
733
scripts/gen_artifacts.py
Normal file
733
scripts/gen_artifacts.py
Normal file
@ -0,0 +1,733 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pbls 交付物机械生成器(单一事实源 → DDL / models / CRUD json / dspy / 模块包骨架 / RBAC 路径)
|
||||
|
||||
为什么存在:QC 退回意见 #8/#9/#11 要求「表定义四段式 models/*.json」「CRUD json/*.json」
|
||||
「RBAC 路径注册」可机械核对。手写 36 表 × 3 份产物必然漂移,故用本脚本从一份 spec 生成,
|
||||
保证 DDL 与 models/CRUD/RBAC 完全一致(可重复执行,幂等)。
|
||||
|
||||
方言来源:projects/pbls/env/test.json -> db.engine = mariadb(见 TYPE_MAP,禁 BIGSERIAL/SERIAL/nextval)。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, '..', '..', '..')) # 机构工作空间根
|
||||
APP = os.path.join(ROOT, 'apps', 'pbls')
|
||||
MODS = os.path.join(ROOT, 'modules')
|
||||
|
||||
# ── 抽象类型 → MariaDB 10.x 物理类型映射(唯一映射表,QC #5/#8 核对用)────────────
|
||||
TYPE_MAP = {
|
||||
'pk': 'BIGINT UNSIGNED NOT NULL AUTO_INCREMENT',
|
||||
'tenant': 'VARCHAR(32) NOT NULL',
|
||||
'sid': 'VARCHAR(32) NOT NULL',
|
||||
'code': 'VARCHAR(64) NOT NULL',
|
||||
'name': 'VARCHAR(128) NOT NULL',
|
||||
'txt': 'TEXT',
|
||||
'js': 'LONGTEXT',
|
||||
'int': 'INT NOT NULL DEFAULT 0',
|
||||
'bigint': 'BIGINT NOT NULL DEFAULT 0',
|
||||
'dec': 'DECIMAL(18,2) NOT NULL DEFAULT 0',
|
||||
'bool': 'TINYINT(1) NOT NULL DEFAULT 0',
|
||||
'ts': 'DATETIME NULL',
|
||||
'tsnn': 'DATETIME NOT NULL',
|
||||
'ref': 'BIGINT UNSIGNED NOT NULL',
|
||||
'sref': 'VARCHAR(32) NOT NULL',
|
||||
}
|
||||
ABSTRACT = {
|
||||
'pk': {'type': 'int', 'length': 20, 'unsigned': True, 'auto_increment': True},
|
||||
'tenant': {'type': 'str', 'length': 32},
|
||||
'sid': {'type': 'str', 'length': 32},
|
||||
'code': {'type': 'str', 'length': 64},
|
||||
'name': {'type': 'str', 'length': 128},
|
||||
'txt': {'type': 'text'},
|
||||
'js': {'type': 'json'},
|
||||
'int': {'type': 'int'},
|
||||
'bigint': {'type': 'bigint'},
|
||||
'dec': {'type': 'double', 'precision': 18, 'scale': 2},
|
||||
'bool': {'type': 'bool'},
|
||||
'ts': {'type': 'datetime'},
|
||||
'tsnn': {'type': 'datetime'},
|
||||
'ref': {'type': 'bigint'},
|
||||
'sref': {'type': 'str', 'length': 32},
|
||||
}
|
||||
NULLABLE = {'txt', 'js', 'ts'}
|
||||
|
||||
# ── 模块清单:编号 ↔ 目录 ↔ 表 ↔ 契约(QC #13 对账表的事实源)───────────────────
|
||||
# tables=[] 且 contracts 不含 CRUD 的模块为「零表模块」(pbl_common / pbl_appcodes)
|
||||
MODULES = {
|
||||
'pbl_common': dict(no='M12', tables=[], contracts=[
|
||||
'pbl_common_ping', 'pbl_common_audit']),
|
||||
'pbl_appcodes': dict(no='M10', tables=[], contracts=[
|
||||
'pbl_appcodes_inject', 'pbl_appcodes_list']),
|
||||
'pbl_blueprint': dict(no='M1', tables=[
|
||||
'pbl_blueprint', 'pbl_blueprint_learning_goal', 'pbl_blueprint_role',
|
||||
'pbl_blueprint_mission', 'pbl_blueprint_task', 'pbl_blueprint_artifact_spec',
|
||||
'pbl_blueprint_evidence_spec', 'pbl_blueprint_reflection_spec',
|
||||
'pbl_blueprint_version', 'pbl_template'],
|
||||
contracts=[
|
||||
'pbl_blueprint_create', 'pbl_blueprint_read', 'pbl_blueprint_update',
|
||||
'pbl_blueprint_delete', 'pbl_blueprint_list', 'pbl_blueprint_tree',
|
||||
'pbl_blueprint_fork', 'pbl_blueprint_subobject_save', 'pbl_blueprint_subobject_list',
|
||||
'pbl_blueprint_version_create', 'pbl_blueprint_version_diff',
|
||||
'pbl_template_list', 'pbl_template_instantiate']),
|
||||
'pbl_validation': dict(no='M2', tables=['pbl_validation_rule', 'pbl_validation_result'],
|
||||
contracts=['pbl_validation_run', 'pbl_validation_get', 'pbl_validation_list',
|
||||
'pbl_validation_rule_list', 'pbl_validation_rule_save']),
|
||||
'pbl_compiler': dict(no='M3', tables=['pbl_compiler_version', 'pbl_game_definition',
|
||||
'pbl_capability_registry'],
|
||||
contracts=['pbl_compiler_compile', 'pbl_compiler_preview', 'pbl_compiler_compare',
|
||||
'pbl_compiler_version_list', 'pbl_compiler_version_save',
|
||||
'pbl_capability_list', 'pbl_capability_register']),
|
||||
'pbl_agent_runtime': dict(no='M4', tables=['pbl_agent_def', 'pbl_tool_registry',
|
||||
'pbl_agent_trace', 'pbl_agent_tool_call', 'pbl_approval', 'pbl_llm_call_log'],
|
||||
contracts=['pbl_agent_designer_run', 'pbl_agent_critic_run', 'pbl_agent_trace_write',
|
||||
'pbl_agent_trace_list', 'pbl_tool_registry_list', 'pbl_tool_registry_save',
|
||||
'pbl_tool_adjudicate', 'pbl_approval_create', 'pbl_approval_decide',
|
||||
'pbl_approval_list']),
|
||||
'pbl_evidence': dict(no='M5', tables=['pbl_artifact', 'pbl_evidence'],
|
||||
contracts=['pbl_artifact_create', 'pbl_artifact_read', 'pbl_artifact_update',
|
||||
'pbl_artifact_delete', 'pbl_artifact_list', 'pbl_evidence_collect',
|
||||
'pbl_evidence_list']),
|
||||
'pbl_assessment': dict(no='M6', tables=['pbl_rubric', 'pbl_assessment_record'],
|
||||
contracts=['pbl_rubric_save', 'pbl_rubric_get', 'pbl_rubric_list',
|
||||
'pbl_assessment_score', 'pbl_assessment_record_get', 'pbl_assessment_report']),
|
||||
'pbl_kdb_ext': dict(no='M7', tables=['pbl_kdb_item', 'pbl_kdb_query_log'],
|
||||
contracts=['pbl_kdb_search', 'pbl_kdb_get', 'pbl_research_aggregate']),
|
||||
'pbl_domain_ext': dict(no='M8', tables=['pbl_tenant', 'pbl_class', 'pbl_team'],
|
||||
contracts=['pbl_tenant_upsert', 'pbl_class_save', 'pbl_class_list',
|
||||
'pbl_team_save', 'pbl_team_list',
|
||||
'pbl_domain_materialize_game_definition']),
|
||||
'pbl_scense_ext': dict(no='M9', tables=['pbl_session_member', 'pbl_playtest_feedback',
|
||||
'pbl_ui_page_registry'],
|
||||
contracts=['pbl_session_context_get', 'pbl_page_registry_list',
|
||||
'pbl_playtest_feedback_save', 'pbl_overlay_bootstrap']),
|
||||
'pbl_runtime_ext': dict(no='M11', tables=['pbl_runtime_event', 'pbl_entity_state',
|
||||
'pbl_world_state_snapshot'],
|
||||
contracts=['pbl_runtime_event_append', 'pbl_runtime_event_poll',
|
||||
'pbl_entity_state_get', 'pbl_entity_state_apply',
|
||||
'pbl_world_broadcast', 'pbl_session_member_add']),
|
||||
}
|
||||
|
||||
# ── 36 表定义(字段: (name, 抽象类型, 注释[, null]))────────────────────────────
|
||||
T = {}
|
||||
|
||||
|
||||
def tbl(mod, name, comment, fields, indexes, codes=None, partition_month=None):
|
||||
T[name] = dict(module=mod, comment=comment, fields=fields, indexes=indexes,
|
||||
codes=codes or [], partition_month=partition_month)
|
||||
|
||||
|
||||
tbl('pbl_blueprint', 'pbl_blueprint', 'PBL 蓝图聚合根(第8章 schema 落库)', [
|
||||
('code', 'code', '蓝图编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
|
||||
('grade', 'code', '年级'), ('description', 'txt', '说明'),
|
||||
('quality_state', 'code', '质量状态 pbl_quality_state'),
|
||||
('generation_source', 'code', '生成来源 llm/template_fallback/manual'),
|
||||
('approval_status', 'code', '审批状态'), ('visibility', 'code', '可见范围'),
|
||||
('content_json', 'js', '蓝图正文(结构化模型,非散文)'),
|
||||
('version_no', 'int', '当前版本'), ('compiler_version_id', 'ref', '编译版本', True),
|
||||
('world_id', 'ref', '编译产物落 world 的溯源ID', True),
|
||||
('created_by', 'sref', '创建人')],
|
||||
[('uk_bp_tenant_code', ['tenant_id', 'code'], True),
|
||||
('idx_bp_tenant_state', ['tenant_id', 'quality_state'], False),
|
||||
('idx_bp_tenant_creator', ['tenant_id', 'created_by'], False)],
|
||||
[('quality_state', 'pbl_quality_state'), ('approval_status', 'pbl_approval_status'),
|
||||
('visibility', 'pbl_visibility')])
|
||||
|
||||
SUB = [('blueprint_id', 'ref', '所属蓝图'), ('obj_uid', 'sid', '子对象UID')]
|
||||
for mod_tbl, extra, cmt in [
|
||||
('pbl_blueprint_learning_goal', [('code', 'code', '目标编码'), ('title', 'name', '标题'),
|
||||
('description', 'txt', '描述'), ('bloom_level', 'code', '布鲁姆层级'),
|
||||
('measurable', 'bool', '是否可测'), ('order_no', 'int', '排序')], '学习目标'),
|
||||
('pbl_blueprint_role', [('code', 'code', '角色编码'), ('title', 'name', '角色名'),
|
||||
('persona_txt', 'txt', '人设'), ('capabilities_json', 'js', '能力引用'),
|
||||
('headcount', 'int', '名额'), ('order_no', 'int', '排序')], 'PBL 角色'),
|
||||
('pbl_blueprint_mission', [('code', 'code', '任务域编码'), ('title', 'name', '驱动问题域'),
|
||||
('scenario_txt', 'txt', '情境'), ('unlock_condition_json', 'js', '解锁条件'),
|
||||
('order_no', 'int', '排序')], '驱动问题/Mission'),
|
||||
('pbl_blueprint_task', [('mission_id', 'ref', '所属Mission'), ('code', 'code', '编码'),
|
||||
('title', 'name', '任务名'), ('steps_json', 'js', '步骤'),
|
||||
('difficulty', 'code', '难度'), ('order_no', 'int', '排序')], '任务'),
|
||||
('pbl_blueprint_artifact_spec', [('code', 'code', '编码'), ('title', 'name', '产出物名'),
|
||||
('artifact_type', 'code', '产出类型'), ('required_fields_json', 'js', '必填字段'),
|
||||
('order_no', 'int', '排序')], '产出物规格'),
|
||||
('pbl_blueprint_evidence_spec', [('evidence_type', 'code', '证据类型'),
|
||||
('source', 'code', '来源'), ('min_count', 'int', '最少条数'),
|
||||
('rubric_criterion_uid', 'sid', '对齐的评分准则UID', True),
|
||||
('order_no', 'int', '排序')], '证据规格')]:
|
||||
tbl('pbl_blueprint', mod_tbl, '蓝图子对象 - ' + cmt, SUB + extra,
|
||||
[('idx_%s_bp' % mod_tbl[4:], ['tenant_id', 'blueprint_id'], False)])
|
||||
|
||||
tbl('pbl_blueprint', 'pbl_blueprint_reflection_spec', '蓝图子对象 - 反思规格',
|
||||
SUB + [('trigger', 'code', '触发点'), ('prompt_txt', 'txt', '反思引导语'),
|
||||
('min_words', 'int', '最少字数'), ('evidence_type', 'code', '产出证据类型'),
|
||||
('order_no', 'int', '排序')],
|
||||
[('idx_bp_rs_bp', ['tenant_id', 'blueprint_id'], False)])
|
||||
|
||||
tbl('pbl_blueprint', 'pbl_blueprint_version', '蓝图版本(change_delta 证明对话式改模型)', [
|
||||
('blueprint_id', 'ref', '蓝图ID'), ('version_no', 'int', '版本号'),
|
||||
('snapshot_json', 'js', '全量快照'), ('change_delta_json', 'js', '结构化增量'),
|
||||
('quality_state', 'code', '质量状态'), ('generation_source', 'code', '生成来源'),
|
||||
('created_by', 'sref', '操作人')],
|
||||
[('uk_bpv', ['tenant_id', 'blueprint_id', 'version_no'], True)])
|
||||
|
||||
tbl('pbl_blueprint', 'pbl_template', 'PBL 模板(含离线兜底槽位)', [
|
||||
('code', 'code', '模板编码'), ('title', 'name', '标题'), ('subject', 'code', '学科'),
|
||||
('grade', 'code', '年级'), ('skeleton_json', 'js', '骨架'),
|
||||
('fallback_slot_json', 'js', '兜底槽位(LLM不可用时填充)'),
|
||||
('is_builtin', 'bool', '内置'), ('use_count', 'int', '实例化次数'),
|
||||
('quality_state', 'code', '质量状态')],
|
||||
[('uk_tpl_tenant_code', ['tenant_id', 'code'], True)])
|
||||
|
||||
tbl('pbl_validation', 'pbl_validation_rule', '校验规则(14 维,规则 JSON 形态 script_type=1)', [
|
||||
('code', 'code', '规则编码'), ('dimension', 'code', '维度'), ('title', 'name', '标题'),
|
||||
('rule_json', 'js', '规则体'), ('threshold_json', 'js', '阈值(可配,Q-OPEN-8)'),
|
||||
('severity', 'code', '严重度'), ('enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
|
||||
[('uk_vr_code', ['tenant_id', 'code', 'version_no'], True)])
|
||||
|
||||
tbl('pbl_validation', 'pbl_validation_result', '校验结果(14 维 + 5 级质量状态)', [
|
||||
('blueprint_id', 'ref', '蓝图ID'), ('blueprint_version_no', 'int', '校验时版本'),
|
||||
('dimension_count', 'int', '维度数(=14)'), ('passed_count', 'int', '通过数'),
|
||||
('failed_count', 'int', '失败数'), ('score', 'dec', '得分'),
|
||||
('quality_state', 'code', '质量状态'), ('findings_json', 'js', '逐维结论'),
|
||||
('rule_set_version', 'code', '规则集版本'), ('created_by', 'sref', '触发人')],
|
||||
[('idx_vres_bp', ['tenant_id', 'blueprint_id'], False)])
|
||||
|
||||
tbl('pbl_compiler', 'pbl_compiler_version', '编译器版本(29.6 确定性)', [
|
||||
('code', 'code', '版本编码'), ('semver', 'code', '语义化版本'),
|
||||
('rules_hash', 'sid', '规则集SHA256前缀'), ('entrypoint', 'code', '入口函数'),
|
||||
('enabled', 'bool', '启用'), ('notes', 'txt', '说明')],
|
||||
[('uk_cv_code', ['tenant_id', 'code'], True)])
|
||||
|
||||
tbl('pbl_compiler', 'pbl_game_definition', 'Game Definition(第9章 schema + 指纹)', [
|
||||
('blueprint_id', 'ref', '来源蓝图'), ('blueprint_version_no', 'int', '蓝图版本'),
|
||||
('compiler_version_id', 'ref', '编译版本'), ('content_fingerprint', 'sid', 'SHA-256 指纹'),
|
||||
('definition_json', 'js', 'Game Definition 正文'),
|
||||
('world_id', 'ref', '落库 world', True), ('scene_id', 'ref', '落库 scene', True),
|
||||
('entity_count', 'int', '实体数'), ('event_count', 'int', '事件数'),
|
||||
('quality_state', 'code', '编译时质量状态'), ('duration_ms', 'int', '耗时')],
|
||||
[('uk_gd_fp', ['tenant_id', 'content_fingerprint'], True),
|
||||
('idx_gd_bp', ['tenant_id', 'blueprint_id'], False)])
|
||||
|
||||
tbl('pbl_compiler', 'pbl_capability_registry', 'Capability Registry(第11章缺口补齐)', [
|
||||
('capability_key', 'code', '能力键'), ('category', 'code', '分类'),
|
||||
('args_schema_json', 'js', '参数JSON Schema'), ('permission_required', 'code', '所需权限'),
|
||||
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本'), ('description', 'txt', '说明')],
|
||||
[('uk_cap_key', ['tenant_id', 'capability_key', 'version_no'], True)])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_agent_def', 'Agent 定义(Designer/Critic)', [
|
||||
('code', 'code', 'Agent编码'), ('agent_type', 'code', '类型'), ('name', 'name', '名称'),
|
||||
('model_route', 'code', 'pipeline-llm 路由'), ('system_prompt_txt', 'txt', '提示词'),
|
||||
('tool_whitelist_json', 'js', '工具白名单'), ('permission_mode', 'code', 'read/write 模式'),
|
||||
('is_enabled', 'bool', '启用'), ('version_no', 'int', '版本')],
|
||||
[('uk_ad_code', ['tenant_id', 'code'], True)])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_tool_registry', '工具注册表(13 启用 / 9 禁用含 pbl.publish)', [
|
||||
('tool_key', 'code', '工具键'), ('title', 'name', '标题'), ('agent_scope', 'code', '作用域'),
|
||||
('permission_required', 'code', '所需权限'), ('enabled', 'bool', '启用'),
|
||||
('params_schema_json', 'js', '参数白名单Schema'), ('disabled_reason', 'code', '禁用原因', True),
|
||||
('version_no', 'int', '版本')],
|
||||
[('uk_tr_key', ['tenant_id', 'tool_key'], True)])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_agent_trace', 'Agent 执行轨迹(第28章 7 要素)', [
|
||||
('trace_uid', 'sid', '轨迹UID'), ('agent_code', 'code', 'Agent'),
|
||||
('blueprint_id', 'ref', '目标蓝图', True), ('input_ref_json', 'js', '要素1 输入引用'),
|
||||
('thought_txt', 'txt', '要素2 思考'), ('action_code', 'code', '要素3 动作'),
|
||||
('action_params_json', 'js', '要素4 动作参数'), ('observation_json', 'js', '要素5 观察'),
|
||||
('result_state', 'code', '要素6 结果状态'), ('llm_calls', 'int', '要素7 LLM调用数'),
|
||||
('duration_ms', 'int', '耗时'), ('created_by', 'sref', '触发用户')],
|
||||
[('uk_at_uid', ['tenant_id', 'trace_uid'], True),
|
||||
('idx_at_tenant', ['tenant_id', 'created_at'], False)])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_agent_tool_call', '工具调用明细(服务端裁决留痕)', [
|
||||
('trace_id', 'ref', '轨迹ID'), ('tool_key', 'code', '工具键'),
|
||||
('request_json', 'js', '请求'), ('response_json', 'js', '响应'),
|
||||
('ok', 'bool', '成功'), ('error_code', 'code', '错误码', True),
|
||||
('adjudication_code', 'code', '裁决结论'), ('duration_ms', 'int', '耗时')],
|
||||
[('idx_tc_trace', ['tenant_id', 'trace_id'], False)])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_approval', '人工审批单(14.2 四类,无绕过路径)', [
|
||||
('approval_uid', 'sid', '审批UID'), ('approval_type', 'code', '类型'),
|
||||
('target_type', 'code', '对象类型'), ('target_id', 'ref', '对象ID'),
|
||||
('payload_json', 'js', '变更内容'), ('status', 'code', '状态'),
|
||||
('requested_by', 'sref', '发起人'), ('decided_by', 'sref', '决策人', True),
|
||||
('decided_at', 'ts', '决策时间'), ('comment_txt', 'txt', '意见')],
|
||||
[('uk_ap_uid', ['tenant_id', 'approval_uid'], True),
|
||||
('idx_ap_status', ['tenant_id', 'status'], False)],
|
||||
[('status', 'pbl_approval_status')])
|
||||
|
||||
tbl('pbl_agent_runtime', 'pbl_llm_call_log', 'LLM 调用日志(超时/重试/限流/兜底留痕)', [
|
||||
('call_uid', 'sid', '调用UID'), ('agent_code', 'code', 'Agent'), ('route', 'code', '路由'),
|
||||
('model', 'code', '实际模型', True), ('prompt_hash', 'sid', '提示词哈希'),
|
||||
('completion_tokens', 'int', 'tokens'), ('latency_ms', 'int', '时延'),
|
||||
('status', 'code', '状态'), ('error_code', 'code', '错误码', True),
|
||||
('fallback_used', 'bool', '走兜底')],
|
||||
[('uk_lc_uid', ['tenant_id', 'call_uid'], True)])
|
||||
|
||||
tbl('pbl_evidence', 'pbl_artifact', '学生产出物(版本内联 version_no)', [
|
||||
('artifact_uid', 'sid', '产出UID'), ('session_id', 'ref', '会话'),
|
||||
('blueprint_id', 'ref', '蓝图'), ('team_id', 'ref', '团队', True),
|
||||
('creator_id', 'sref', '作者'), ('title', 'name', '标题'),
|
||||
('artifact_type', 'code', '类型'), ('content_json', 'js', '内容'),
|
||||
('version_no', 'int', '版本'), ('status', 'code', '状态'), ('submitted_at', 'ts', '提交时间')],
|
||||
[('uk_art_uid', ['tenant_id', 'artifact_uid'], True),
|
||||
('idx_art_session', ['tenant_id', 'session_id'], False)],
|
||||
[('artifact_type', 'pbl_artifact_type')])
|
||||
|
||||
tbl('pbl_evidence', 'pbl_evidence', '学习证据(幂等采集,唯一索引防重)', [
|
||||
('artifact_id', 'ref', '产出物', True), ('evidence_type', 'code', '证据类型'),
|
||||
('source_event_id', 'sid', '来源 runtime_event'), ('session_id', 'ref', '会话'),
|
||||
('learner_id', 'sref', '学习者'), ('blueprint_id', 'ref', '蓝图', True),
|
||||
('payload_json', 'js', '证据体'), ('occurred_at', 'tsnn', '发生时间'),
|
||||
('dedup_key', 'sid', '去重键')],
|
||||
[('uk_ev_dedup', ['tenant_id', 'source_event_id', 'evidence_type'], True),
|
||||
('idx_ev_learner', ['tenant_id', 'learner_id'], False)],
|
||||
[('evidence_type', 'pbl_evidence_type')])
|
||||
|
||||
tbl('pbl_assessment', 'pbl_rubric', 'Rubric(准则内联 criteria_json,权重合计=100%)', [
|
||||
('code', 'code', '编码'), ('title', 'name', '标题'), ('blueprint_id', 'ref', '蓝图', True),
|
||||
('criteria_json', 'js', '准则数组(含 learning_goal_id/weight)'),
|
||||
('total_weight', 'dec', '权重合计'), ('version_no', 'int', '版本'),
|
||||
('status', 'code', '状态'), ('approval_id', 'ref', '变更审批单', True)],
|
||||
[('uk_ru_code', ['tenant_id', 'code', 'version_no'], True)])
|
||||
|
||||
tbl('pbl_assessment', 'pbl_assessment_record', '评估记录(total_score 派生 + 对齐判定)', [
|
||||
('record_uid', 'sid', '记录UID'), ('learner_id', 'sref', '学习者'),
|
||||
('session_id', 'ref', '会话', True), ('blueprint_id', 'ref', '蓝图', True),
|
||||
('artifact_id', 'ref', '产出物', True), ('evidence_id', 'ref', '主证据', True),
|
||||
('rubric_id', 'ref', 'Rubric'), ('scores_json', 'js', '逐准则得分'),
|
||||
('total_score', 'dec', '加权总分'), ('max_score', 'dec', '满分'),
|
||||
('band', 'code', '等级'), ('alignment_json', 'js', '与学习目标对齐'),
|
||||
('feedback_txt', 'txt', '反馈'), ('assessor_type', 'code', '评估者类型'),
|
||||
('assessor_id', 'sref', '评估者')],
|
||||
[('uk_ar_uid', ['tenant_id', 'record_uid'], True),
|
||||
('idx_ar_learner', ['tenant_id', 'learner_id'], False)])
|
||||
|
||||
tbl('pbl_kdb_ext', 'pbl_kdb_item', 'KDB 条目(建表不写入,is_write_locked 恒 1)', [
|
||||
('code', 'code', '条目编码'), ('item_type', 'code', '类型'), ('title', 'name', '标题'),
|
||||
('summary_txt', 'txt', '摘要'), ('payload_json', 'js', '内容(无向量,Q-OPEN-5)'),
|
||||
('source', 'code', '来源'), ('is_write_locked', 'bool', '写锁(恒1)'),
|
||||
('status', 'code', '状态'), ('version_no', 'int', '版本')],
|
||||
[('uk_kdb_code', ['tenant_id', 'code'], True)])
|
||||
|
||||
tbl('pbl_kdb_ext', 'pbl_kdb_query_log', 'KDB 检索日志(只读侧留痕)', [
|
||||
('query_uid', 'sid', '查询UID'), ('caller_type', 'code', '调用方类型'),
|
||||
('caller_id', 'sref', '调用方'), ('query_json', 'js', '查询条件'),
|
||||
('hit_count', 'int', '命中数'), ('latency_ms', 'int', '时延'), ('ok', 'bool', '成功')],
|
||||
[('uk_kql_uid', ['tenant_id', 'query_uid'], True)])
|
||||
|
||||
tbl('pbl_domain_ext', 'pbl_tenant', '租户(多租户边界第25章,不改基表)', [
|
||||
('tenant_uid', 'sid', '租户UID'), ('name', 'name', '名称'), ('kind', 'code', '类型'),
|
||||
('plan', 'code', '套餐'), ('seat_limit', 'int', '席位'), ('status', 'code', '状态'),
|
||||
('config_json', 'js', '配置')],
|
||||
[('uk_ten_uid', ['tenant_id', 'tenant_uid'], True)])
|
||||
|
||||
tbl('pbl_domain_ext', 'pbl_class', '班级(花名册内联 roster_json)', [
|
||||
('class_uid', 'sid', '班级UID'), ('name', 'name', '名称'), ('grade', 'code', '年级'),
|
||||
('teacher_id', 'sref', '教师'), ('student_count', 'int', '学生数'),
|
||||
('roster_json', 'js', '花名册'), ('status', 'code', '状态')],
|
||||
[('uk_cls_uid', ['tenant_id', 'class_uid'], True)])
|
||||
|
||||
tbl('pbl_domain_ext', 'pbl_team', '团队(成员内联 members_json,不改基表)', [
|
||||
('team_uid', 'sid', '团队UID'), ('class_id', 'ref', '班级'), ('name', 'name', '名称'),
|
||||
('mission_id', 'ref', 'Mission', True), ('members_json', 'js', '成员'),
|
||||
('capacity', 'int', '容量'), ('status', 'code', '状态'),
|
||||
('session_id', 'ref', '绑定会话', True)],
|
||||
[('uk_team_uid', ['tenant_id', 'team_uid'], True),
|
||||
('idx_team_class', ['tenant_id', 'class_id'], False)])
|
||||
|
||||
tbl('pbl_scense_ext', 'pbl_session_member', '会话成员(复用 world_session,不污染基表)', [
|
||||
('session_id', 'ref', '会话'), ('user_id', 'sref', '用户'), ('role_code', 'code', '角色'),
|
||||
('team_id', 'ref', '团队', True), ('joined_at', 'ts', '加入时间'),
|
||||
('last_seen_at', 'ts', '心跳'), ('state_version', 'bigint', '已见状态版本'),
|
||||
('status', 'code', '状态')],
|
||||
[('uk_sm', ['tenant_id', 'session_id', 'user_id'], True)])
|
||||
|
||||
tbl('pbl_scense_ext', 'pbl_playtest_feedback', 'Playtest 反馈/反思(reflection 走证据链外的轻量表)', [
|
||||
('session_id', 'ref', '会话', True), ('user_id', 'sref', '用户'),
|
||||
('blueprint_id', 'ref', '蓝图', True), ('feedback_type', 'code', '类型'),
|
||||
('rating', 'int', '评分'), ('content_txt', 'txt', '内容'),
|
||||
('context_json', 'js', '上下文'), ('handled', 'bool', '已处理')],
|
||||
[('idx_pf_session', ['tenant_id', 'session_id'], False)])
|
||||
|
||||
tbl('pbl_scense_ext', 'pbl_ui_page_registry', '前端覆盖层页面注册(12 页面 + 时延预算)', [
|
||||
('page_key', 'code', '页面键'), ('title', 'name', '标题'), ('route', 'code', '路由'),
|
||||
('widget_type', 'code', '渲染组件'), ('requires_role', 'code', '所需角色'),
|
||||
('budget_ms', 'int', '渲染预算'), ('enabled', 'bool', '启用'), ('meta_json', 'js', '元数据')],
|
||||
[('uk_upr_key', ['tenant_id', 'page_key'], True)])
|
||||
|
||||
tbl('pbl_runtime_ext', 'pbl_runtime_event', '运行时事件(append-only,按月 RANGE 分区)', [
|
||||
('event_uid', 'sid', '事件UID'), ('session_id', 'ref', '会话'),
|
||||
('world_id', 'ref', '世界'), ('actor_id', 'sref', '触发者'),
|
||||
('event_type', 'code', '事件类型'), ('payload_json', 'js', '事件体'),
|
||||
('causation_id', 'sid', '因果链', True), ('state_version', 'bigint', '状态版本'),
|
||||
('seq', 'bigint', '会话内单调序号'), ('occurred_at', 'tsnn', '发生时间')],
|
||||
[('uk_re_uid', ['tenant_id', 'event_uid', 'occurred_at'], True),
|
||||
('idx_re_session_seq', ['tenant_id', 'session_id', 'seq'], False)],
|
||||
[('event_type', 'pbl_event_type')], partition_month=True)
|
||||
|
||||
tbl('pbl_runtime_ext', 'pbl_entity_state', '实体状态(服务端权威,state_version 客户端禁写)', [
|
||||
('session_id', 'ref', '会话'), ('entity_id', 'sid', '实体标识'),
|
||||
('state_json', 'js', '状态'), ('state_version', 'bigint', '单调版本'),
|
||||
('checksum', 'sid', '状态摘要'), ('updated_by', 'sref', '写入者')],
|
||||
[('uk_es', ['tenant_id', 'session_id', 'entity_id'], True)])
|
||||
|
||||
tbl('pbl_runtime_ext', 'pbl_world_state_snapshot', '世界状态快照(掉线补齐/离线兜底)', [
|
||||
('session_id', 'ref', '会话'), ('snapshot_uid', 'sid', '快照UID'),
|
||||
('state_fingerprint', 'sid', '状态指纹'), ('entities_json', 'js', '实体集'),
|
||||
('events_through_seq', 'bigint', '截至序号'), ('reason', 'code', '触发原因')],
|
||||
[('uk_wss_uid', ['tenant_id', 'snapshot_uid'], True)])
|
||||
|
||||
assert len(T) == 36, '表数应为 36,实际 %d' % len(T)
|
||||
|
||||
# ── 枚举 6+2 组(appcodes.md 幂等注入,零表模块 pbl_appcodes 的事实源)────────────
|
||||
APPCODES = {
|
||||
'pbl_quality_state': ['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready'],
|
||||
'pbl_approval_status': ['none', 'pending', 'approved', 'rejected', 'withdrawn'],
|
||||
'pbl_visibility': ['private', 'class', 'school', 'public_readonly'],
|
||||
'pbl_evidence_type': ['artifact', 'process_event', 'reflection', 'feedback'],
|
||||
'pbl_agent_type': ['designer', 'critic'],
|
||||
'pbl_tool_state': ['enabled', 'disabled_contract_only'],
|
||||
'pbl_artifact_type': ['document', 'presentation', 'model', 'code', 'media'],
|
||||
'pbl_event_type': ['mission.completed', 'task.completed', 'pbl.unlockMission',
|
||||
'entity.state.changed', 'session.member.joined', 'artifact.submitted'],
|
||||
}
|
||||
|
||||
W = []
|
||||
|
||||
|
||||
def w(path, content):
|
||||
full = os.path.join(os.path.dirname(path) and path.rsplit(os.sep, 1)[0], '')
|
||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||
old = None
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding='utf-8') as f:
|
||||
old = f.read()
|
||||
if old != content:
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
W.append(os.path.relpath(path, ROOT))
|
||||
|
||||
|
||||
def ddl_of(name):
|
||||
spec = T[name]
|
||||
cols = [' `tenant_id` %s NOT NULL COMMENT %s' % (TYPE_MAP['tenant'], '租户ID(强制打头)'),
|
||||
' `id` %s COMMENT %s' % (TYPE_MAP['pk'], '主键')]
|
||||
for f in spec['fields']:
|
||||
fn, ft, cmt = f[0], f[1], f[2]
|
||||
nn = f[3] if len(f) > 3 else ft not in NULLABLE
|
||||
cols.append(' `%s` %s%s COMMENT %s' % (fn, TYPE_MAP[ft], '' if nn else ' NULL',
|
||||
json.dumps(cmt, ensure_ascii=False)))
|
||||
# 时间列一律 DATETIME(不使用 时间戳类型),值由应用层 pbl_common.api.now_str() 写入,
|
||||
# 避免隐式默认带来的时区漂移;默认值为纪元占位(非 NULL,便于排序与增量拉取)。
|
||||
cols.append(' `created_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
|
||||
% json.dumps('创建时间(应用层写入)', ensure_ascii=False))
|
||||
cols.append(' `updated_at` DATETIME NOT NULL DEFAULT \'1970-01-01 00:00:00\' COMMENT %s'
|
||||
% json.dumps('更新时间(应用层写入)', ensure_ascii=False))
|
||||
cols.append(' PRIMARY KEY (`id`)')
|
||||
for iname, icols, uniq in spec['indexes']:
|
||||
cols.append(' %s `%s` (%s)' % ('UNIQUE KEY' if uniq else 'KEY', iname,
|
||||
', '.join('`%s`' % c for c in icols)))
|
||||
body = ',\n'.join(cols)
|
||||
sql = 'CREATE TABLE IF NOT EXISTS `%s` (\n%s\n) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4' % (name, body)
|
||||
sql += " COLLATE=utf8mb4_general_ci COMMENT=%s" % json.dumps(spec['comment'], ensure_ascii=False)
|
||||
if spec['partition_month']:
|
||||
parts = ',\n'.join(" PARTITION p%s VALUES LESS THAN ('%s')" % (y, '%d-01-01' % y)
|
||||
for y in range(2026, 2032))
|
||||
sql += (',\n KEY `idx_re_part` (`occurred_at`)\nPARTITION BY RANGE COLUMNS(`occurred_at`)'
|
||||
' (\n%s\n PARTITION pmax VALUES LESS THAN (MAXVALUE)\n )' % parts)
|
||||
return sql + ';\n'
|
||||
|
||||
|
||||
def model_json(name):
|
||||
spec = T[name]
|
||||
fields = [{'name': 'tenant_id', 'comment': '租户ID(强制打头)', 'null': False, **ABSTRACT['tenant']},
|
||||
{'name': 'id', 'comment': '主键', 'null': False, **ABSTRACT['pk']}]
|
||||
for f in spec['fields']:
|
||||
fn, ft, cmt = f[0], f[1], f[2]
|
||||
nn = f[3] if len(f) > 3 else ft not in NULLABLE
|
||||
fields.append({'name': fn, 'comment': cmt, 'null': not nn, **ABSTRACT[ft]})
|
||||
fields.append({'name': 'created_at', 'comment': '创建时间', 'null': False, **ABSTRACT['tsnn']})
|
||||
fields.append({'name': 'updated_at', 'comment': '更新时间', 'null': False, **ABSTRACT['tsnn']})
|
||||
doc = {
|
||||
'summary': [{'name': name, 'comment': spec['comment'], 'module': spec['module'],
|
||||
'engine': 'mariadb', 'charset': 'utf8mb4', 'tenant_scoped': True}],
|
||||
'fields': fields,
|
||||
'indexes': [{'name': i[0], 'unique': i[2], 'fields': i[1]} for i in spec['indexes']]
|
||||
+ [{'name': 'PRIMARY', 'unique': True, 'fields': ['id']}],
|
||||
'codes': [{'field': c[0], 'appcode': c[1]} for c in spec['codes']],
|
||||
}
|
||||
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
|
||||
|
||||
|
||||
def crud_json(mod, name):
|
||||
spec = T[name]
|
||||
br, ed = {}, {}
|
||||
for f in spec['fields']:
|
||||
fn, ft, cmt = f[0], f[1], f[2]
|
||||
ui = {'label': cmt or fn, 'type': 'text' if ft in ('txt', 'js', 'sid', 'sref') else
|
||||
'number' if ft in ('int', 'bigint', 'dec', 'ref') else
|
||||
'checkbox' if ft == 'bool' else 'date' if ft.startswith('ts') else 'select'}
|
||||
if ui['type'] == 'date':
|
||||
ui['type'] = 'text'
|
||||
br[fn] = dict(ui, list=True)
|
||||
if ft != 'pk':
|
||||
ed[fn] = ui
|
||||
br = dict({'id': {'label': 'ID', 'list': True, 'type': 'number'},
|
||||
'tenant_id': {'label': '租户ID', 'list': True, 'type': 'text'}}, **br)
|
||||
doc = {'tblname': name, 'alias': mod, 'browserfields': br,
|
||||
'params': {'browserfields': br, 'editable': ed,
|
||||
'order_by': 'id DESC', 'page_size': 20}}
|
||||
return json.dumps(doc, ensure_ascii=False, indent=2) + '\n'
|
||||
|
||||
|
||||
DSPY = """# {mod}/api/{fn}.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('{mod}/api/{fn}.dspy: START params_kw={{dict(params_kw)}}')
|
||||
data = await {fn}(**params_kw)
|
||||
return data
|
||||
"""
|
||||
|
||||
INIT_PY = '''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`load_{mod}()` —— {mod} 模块唯一挂载入口(自动生成)。
|
||||
|
||||
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
from {mod}.api import (
|
||||
{imports}
|
||||
)
|
||||
|
||||
|
||||
def load_{mod}():
|
||||
env = ServerEnv()
|
||||
env.get_module_dbname = env.get_module_dbname # 库名由应用决定,模块不硬编码
|
||||
{registers}
|
||||
return '{mod}'
|
||||
'''
|
||||
|
||||
DYN_INIT_PY = '''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`load_{mod}()` —— {mod} 模块唯一挂载入口。
|
||||
|
||||
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
from {mod}.api import (
|
||||
{imports}
|
||||
)
|
||||
|
||||
|
||||
def load_{mod}():
|
||||
env = ServerEnv()
|
||||
{registers}
|
||||
return '{mod}'
|
||||
'''
|
||||
|
||||
PKG_INIT_PY = '''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""{mod} —— {desc}
|
||||
|
||||
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
|
||||
"""
|
||||
from {mod}.init import load_{mod}
|
||||
from {mod}.api import (
|
||||
{imports}
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'load_{mod}',
|
||||
{exports}
|
||||
]
|
||||
'''
|
||||
|
||||
PYPROJECT = '''[project]
|
||||
name = "{mod}"
|
||||
version = "0.1.0"
|
||||
description = "{desc}"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["{mod}"]
|
||||
'''
|
||||
|
||||
LOAD_PATH = '''#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""{mod} RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
||||
|
||||
约定:
|
||||
- 路径 = 模块自动路由 `/{mod}/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||
- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin);
|
||||
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MODULE = '{mod}'
|
||||
|
||||
# (path, role)
|
||||
PATHS = [
|
||||
{entries}
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
tool = os.environ.get('RBAC_SET_PERM', 'set_role_perm.py')
|
||||
done, missing = 0, []
|
||||
for path, role in PATHS:
|
||||
if subprocess.call([sys.executable if os.environ.get('PY') else 'python3',
|
||||
tool, role, path],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) == 0:
|
||||
done += 1
|
||||
else:
|
||||
missing.append((path, role))
|
||||
print('[%s] rbac paths: total=%%d ok=%%d pending=%%d' %% (len(PATHS), done, len(missing)))
|
||||
for path, role in missing:
|
||||
print(' PENDING %%-12s %%s' %% (role, path))
|
||||
return len(missing) == 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(0 if register() else 1)
|
||||
'''
|
||||
|
||||
SKILL_MD = '''# {mod} 模块技能(自动生成骨架 + 人工补充)
|
||||
|
||||
## 定位
|
||||
{desc}
|
||||
|
||||
## 挂载
|
||||
`from {mod}.init import load_{mod}` → `load_{mod}()`(应用 app/pbls.py init() 中按序调用)
|
||||
|
||||
## 数据表({tn} 张)
|
||||
{tables}
|
||||
|
||||
## 契约接口({cn} 个,路径 `/{mod}/api/<name>.dspy`)
|
||||
{contracts}
|
||||
|
||||
## 陷阱
|
||||
- 库名一律 `ServerEnv().get_module_dbname('{mod}')`,禁止硬编码 DBNAME。
|
||||
- sqlor 只有 `C/U/D/R/I/sqlExe`;查询走 pbl_common.api 的 q_all/q_one(已适配)。
|
||||
- 所有读写强制带 `tenant_id`(pbl_common.api.tenant_id()),缺失即 fail-closed 报错。
|
||||
- 新增契约需同步三处:api.py 定义 + __init__.py 导出 + init.py env 注册 + scripts/load_path.py 路径。
|
||||
'''
|
||||
|
||||
INDEX_UI = json.dumps({
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
||||
"subwidgets": [
|
||||
{"widgettype": "Text", "options": {"label": "{title}", "fontSize": "24px"}},
|
||||
{"widgettype": "ResponsableBox", "options": {"gap": "16px", "minWidth": "250px"},
|
||||
"subwidgets": []},
|
||||
{"widgettype": "VBox", "id": "{mod}_content",
|
||||
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}}]
|
||||
}, ensure_ascii=False, indent=2) + '\n'
|
||||
|
||||
|
||||
def contracts_of(mod):
|
||||
return MODULES[mod]['contracts']
|
||||
|
||||
|
||||
def write_module(mod, desc):
|
||||
spec = MODULES[mod]
|
||||
base = os.path.join(MODS, mod)
|
||||
pkg = os.path.join(base, mod)
|
||||
cs = contracts_of(mod)
|
||||
imports = ''.join(" %s,\n" % c for c in cs)
|
||||
registers = ''.join(" env.%s = %s\n" % (c, c) for c in cs)
|
||||
exports = ''.join(" %r,\n" % c for c in cs)
|
||||
w(os.path.join(pkg, 'init.py'), DYN_INIT_PY.format(
|
||||
mod=mod, imports=imports, registers=registers))
|
||||
w(os.path.join(pkg, '__init__.py'), PKG_INIT_PY.format(
|
||||
mod=mod, desc=desc, imports=imports, exports=exports))
|
||||
w(os.path.join(base, 'pyproject.toml'), PYPROJECT.format(mod=mod, desc=desc))
|
||||
entries = ''.join(" ('/%s/api/%s.dspy', %r),\n" % (mod, c, 'logined') for c in cs)
|
||||
w(os.path.join(base, 'scripts', 'load_path.py'), LOAD_PATH.format(
|
||||
mod=mod, entries=entries))
|
||||
w(os.path.join(base, 'skill', 'SKILL.md'), SKILL_MD.format(
|
||||
mod=mod, desc=desc, tn=len(spec['tables']),
|
||||
tables='\n'.join('- `%s`:%s' % (t, T[t]['comment']) for t in spec['tables']) or '- 无(零表模块)',
|
||||
cn=len(cs), contracts='\n'.join('- `%s`' % c for c in cs)))
|
||||
cards = []
|
||||
for c in cs:
|
||||
cards.append({
|
||||
"widgettype": "VBox",
|
||||
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "cursor": "pointer"},
|
||||
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget",
|
||||
"target": "app.%s_content" % mod,
|
||||
"options": {"url": "{{entire_url('api/%s.dspy')}}" % c}, "mode": "replace"}],
|
||||
"subwidgets": [{"widgettype": "Text", "options": {"label": c}}]})
|
||||
ui = json.loads(INDEX_UI.replace('{title}', desc).replace('{mod}', mod))
|
||||
ui['subwidgets'][1]['subwidgets'] = cards
|
||||
w(os.path.join(base, 'wwwroot', 'index.ui'),
|
||||
json.dumps(ui, ensure_ascii=False, indent=2) + '\n')
|
||||
for c in cs:
|
||||
w(os.path.join(base, 'wwwroot', 'api', c + '.dspy'), DSPY.format(mod=mod, fn=c))
|
||||
for t in spec['tables']:
|
||||
w(os.path.join(base, 'models', t + '.json'), model_json(t))
|
||||
w(os.path.join(base, 'json', '%s_%s.json' % (mod[4:], t)), crud_json(mod, t))
|
||||
|
||||
|
||||
DESCS = {
|
||||
'pbl_common': 'PBL 公共内核(租户上下文/DB 适配/错误码/审计/CRUD 工厂)',
|
||||
'pbl_appcodes': 'PBL 枚举编码注入(6+2 组,幂等,零表)',
|
||||
'pbl_blueprint': 'PBL 蓝图聚合根与子对象、版本、模板(M1a/M1b)',
|
||||
'pbl_validation': 'PBL 校验引擎(14 维 + 5 级质量状态,M2)',
|
||||
'pbl_compiler': 'PBL Compiler v1(确定性编译 + Game Definition,M3a/M3b)',
|
||||
'pbl_agent_runtime': 'Designer/Critic Agent 运行时与 fail-closed 工具裁决(M4a/M4b)',
|
||||
'pbl_evidence': '产出物与证据幂等采集(M5a/M5b)',
|
||||
'pbl_assessment': 'Rubric 加权评估与报告(M6)',
|
||||
'pbl_kdb_ext': 'KDB 只读桩与匿名聚合(M7,零写入)',
|
||||
'pbl_domain_ext': 'world/scene/entity 薄扩展:租户/班级/团队关联(M8,不改基表)',
|
||||
'pbl_scense_ext': 'scense/scense_game 前端覆盖层与页面注册(M9)',
|
||||
'pbl_runtime_ext': 'scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)',
|
||||
}
|
||||
|
||||
for m in MODULES:
|
||||
write_module(m, DESCS[m])
|
||||
|
||||
# ── 应用级 DDL(36 表,按模块顺序)──────────────────────────────────────────
|
||||
order = ['pbl_blueprint', 'pbl_validation', 'pbl_compiler', 'pbl_agent_runtime',
|
||||
'pbl_evidence', 'pbl_assessment', 'pbl_kdb_ext', 'pbl_domain_ext',
|
||||
'pbl_scense_ext', 'pbl_runtime_ext']
|
||||
parts = ["-- pbls 36 表 DDL(MariaDB 10.x 方言,由 scripts/gen_artifacts.py 从同一 spec 生成)",
|
||||
"-- 硬约束:tenant_id 打头 / id 为 BIGINT UNSIGNED AUTO_INCREMENT 主键;"
|
||||
"-- 不使用 外键约束 / 枚举列类型 / 时间戳列类型 / 位图列类型(枚举值走 appcodes 编码表,"
|
||||
"时间列走 DATETIME 由应用层写入)",
|
||||
"-- 方言来源:projects/pbls/env/test.json -> db.engine = mariadb"
|
||||
"(禁 BIGSERIAL/SERIAL/nextval 等 PG 语法)",
|
||||
"SET NAMES utf8mb4;", ""]
|
||||
n = 0
|
||||
for mod in order:
|
||||
parts.append('-- ===== %s (%s) =====' % (mod, MODULES[mod]['no']))
|
||||
for t in MODULES[mod]['tables']:
|
||||
parts.append(ddl_of(t))
|
||||
n += 1
|
||||
parts.append('-- ===== 枚举 6+2 组由 pbl_appcodes.load_pbl_appcodes() 幂等注入 appcodes,本文件不含 INSERT =====')
|
||||
w(os.path.join(APP, 'scripts', 'ddl', 'pbls_tables.sql'), '\n'.join(parts) + '\n')
|
||||
assert n == 36, n
|
||||
|
||||
# 分模块 DDL 片段(模块仓库自包含,便于单模块重建表)
|
||||
for mod in order:
|
||||
frag = ['-- %s 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)' % mod]
|
||||
for t in MODULES[mod]['tables']:
|
||||
frag.append(ddl_of(t))
|
||||
w(os.path.join(MODS, mod, 'sql', mod + '.sql'), '\n'.join(frag) + '\n')
|
||||
|
||||
# ── appcodes 种子数据(pbl_appcodes 幂等注入源)─────────────────────────────
|
||||
w(os.path.join(MODS, 'pbl_appcodes', 'init', 'data.json'),
|
||||
json.dumps({'appcodes': APPCODES}, ensure_ascii=False, indent=2) + '\n')
|
||||
|
||||
print('generated/updated %d files, tables=%d, contracts=%d, appcodes=%d groups'
|
||||
% (len(W), len(T), sum(len(v['contracts']) for v in MODULES.values()), len(APPCODES)))
|
||||
for p in W:
|
||||
print(' +', p)
|
||||
654
scripts/tests/offline_contract_smoke.py
Normal file
654
scripts/tests/offline_contract_smoke.py
Normal file
@ -0,0 +1,654 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbls 离线契约冒烟测试(无 DB / 无网络,用内存 FakeDB 驱动真实 api.py 代码路径)。
|
||||
|
||||
覆盖硬门禁与关键红线:
|
||||
A 租户强制 + CRUD 白名单丢弃越权字段
|
||||
B 14 维校验 + 5 级质量状态阶梯(含 blocking 不放行)
|
||||
C 确定性编译(draft 门禁拒 → 同输入两次指纹一致 → 改版本指纹变 → 幂等命中)
|
||||
D 证据幂等(同 (source_event_id, evidence_type) 二次采集 deduped,不产生第二行)
|
||||
E fail-closed 8 步裁决:Critic 写 / 禁用工具(pbl.publish) / 未注册 / 参数白名单 / 审批缺失
|
||||
F Rubric 权重合计≠100% 拒绝;=100% 通过且 total_score 派生
|
||||
G KDB 零写入 + 匿名聚合 k=5 抑制 + 个体维度拒绝
|
||||
H 运行时事件 seq/state_version 单调 + 乐观锁 + 8 人上限 + 时延预算实测
|
||||
I appcodes 幂等注入(二次执行 inserted=0)
|
||||
J 入口挂载静态核验(ServerEnv 先于 load_rbac + 契约三处同步 + 12 loader 可导入)
|
||||
K G6 十一条禁止实现零命中
|
||||
L 前端覆盖层 12 页面 + 渲染时延预算(可执行验证方式)
|
||||
"""
|
||||
import asyncio
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import types
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(HERE, *(['..'] * 4)))
|
||||
|
||||
for _m in sorted(os.listdir(os.path.join(ROOT, 'modules'))):
|
||||
if _m.startswith('pbl_'):
|
||||
sys.path.insert(0, os.path.join(ROOT, 'modules', _m))
|
||||
sys.path.insert(0, os.path.join(ROOT, 'apps', 'pbls', 'app'))
|
||||
|
||||
|
||||
# ── FakeDB:支持 INSERT/UPDATE/DELETE/SELECT(等值+字面量+LIKE+>)/COUNT/MAX/GROUP BY ──
|
||||
class FakeSor:
|
||||
def __init__(self, db):
|
||||
self.db = db
|
||||
|
||||
async def sqlExe(self, sql, params=None):
|
||||
p = dict(params or {})
|
||||
s = ' '.join(sql.split())
|
||||
self.db.log.append(s[:160])
|
||||
up = s.upper()
|
||||
if up.startswith('INSERT'):
|
||||
return self._insert(s, p)
|
||||
if up.startswith('UPDATE'):
|
||||
return self._update(s, p)
|
||||
if up.startswith('DELETE'):
|
||||
return self._delete(s, p)
|
||||
return self._select(s, p, up)
|
||||
|
||||
# ---- INSERT ----
|
||||
def _insert(self, s, p):
|
||||
tbl = re.search(r'INSERT INTO `(\w+)`', s).group(1)
|
||||
colpart = s[s.index('(') + 1:s.index(')')]
|
||||
cols = re.findall(r'`(\w+)`', colpart)
|
||||
vals = s[s.upper().index('VALUES') + 6:]
|
||||
ph = re.findall(r'\$\{(\w+)\}\$', vals)
|
||||
self.db.next_id += 1
|
||||
row = {}
|
||||
for i, c in enumerate(cols):
|
||||
row[c] = p.get(ph[i]) if i < len(ph) else None
|
||||
row['id'] = self.db.next_id
|
||||
self.db.rows.setdefault(tbl, []).append(row)
|
||||
return [{'id': self.db.next_id}]
|
||||
|
||||
# ---- UPDATE ----
|
||||
def _update(self, s, p):
|
||||
tbl = re.search(r'UPDATE `(\w+)`', s).group(1)
|
||||
where = s[s.upper().index(' WHERE ') + 7:] if ' WHERE ' in s.upper() else ''
|
||||
setpart = s[s.upper().index(' SET ') + 5: s.upper().index(' WHERE ')] if ' WHERE ' in s.upper() \
|
||||
else s[s.upper().index(' SET ') + 5:]
|
||||
sets = re.findall(r'`(\w+)`\s*=\s*\$\{(\w+)\}\$', setpart)
|
||||
n = 0
|
||||
for row in self.db.rows.get(tbl, []):
|
||||
if not self._match(row, where, p):
|
||||
continue
|
||||
for col, key in sets:
|
||||
row[col] = p.get(key)
|
||||
if '`use_count` = `use_count` + 1' in setpart:
|
||||
row['use_count'] = int(row.get('use_count') or 0) + 1
|
||||
n += 1
|
||||
return [{'affected': n}]
|
||||
|
||||
# ---- DELETE ----
|
||||
def _delete(self, s, p):
|
||||
tbl = re.search(r'DELETE FROM `(\w+)`', s).group(1)
|
||||
where = s[s.upper().index(' WHERE ') + 7:] if ' WHERE ' in s.upper() else ''
|
||||
keep, n = [], 0
|
||||
for row in self.db.rows.get(tbl, []):
|
||||
if self._match(row, where, p):
|
||||
n += 1
|
||||
else:
|
||||
keep.append(row)
|
||||
self.db.rows[tbl] = keep
|
||||
return [{'affected': n}]
|
||||
|
||||
# ---- SELECT ----
|
||||
def _select(self, s, p, up):
|
||||
if 'LAST_INSERT_ID' in up:
|
||||
return [{'pk': self.db.next_id}]
|
||||
m = re.search(r'FROM `(\w+)`', s)
|
||||
tbl = m.group(1) if m else '?'
|
||||
rows = self.db.rows.get(tbl, [])
|
||||
where = ''
|
||||
if ' WHERE ' in up:
|
||||
w = s[s.upper().index(' WHERE ') + 7:]
|
||||
where = re.split(r' ORDER BY | GROUP BY | LIMIT ', w)[0]
|
||||
matched = [r for r in rows if self._match(r, where, p)]
|
||||
gm = re.search(r'GROUP BY `(\w+)`', s)
|
||||
if gm:
|
||||
col = gm.group(1)
|
||||
groups = {}
|
||||
for r in matched:
|
||||
groups.setdefault(r.get(col), []).append(r)
|
||||
out = []
|
||||
for g, items in sorted(groups.items(), key=lambda x: str(x[0])):
|
||||
scores = [float(i.get('total_score') or 0) for i in items]
|
||||
out.append({'g': g, 'n': len(items),
|
||||
'avg_score': sum(scores) / len(scores) if scores else 0,
|
||||
'min_score': min(scores) if scores else 0,
|
||||
'max_score': max(scores) if scores else 0})
|
||||
return out
|
||||
if 'COUNT(*)' in up:
|
||||
return [{'c': len(matched)}]
|
||||
m = re.search(r'COALESCE\((MAX|COUNT)\(`(\w+)`\),0\) AS (\w+)', s)
|
||||
if m:
|
||||
fn, col, alias = m.groups()
|
||||
vals = [r.get(col) for r in matched if r.get(col) is not None]
|
||||
if fn == 'COUNT':
|
||||
return [{alias: len(matched)}]
|
||||
try:
|
||||
return [{alias: max(vals) if vals else 0}]
|
||||
except TypeError:
|
||||
return [{alias: 0}]
|
||||
m = re.search(r'MAX\(`(\w+)`\) AS (\w+)', s)
|
||||
if m:
|
||||
col, alias = m.groups()
|
||||
vals = [r.get(col) for r in matched if r.get(col) is not None]
|
||||
try:
|
||||
return [{alias: max(vals) if vals else 0}]
|
||||
except TypeError:
|
||||
return [{alias: 0}]
|
||||
if 'DISTINCT' in up:
|
||||
col = re.search(r'DISTINCT `(\w+)`', s).group(1)
|
||||
seen = []
|
||||
for r in matched:
|
||||
if r.get(col) is not None and r[col] not in seen:
|
||||
seen.append(r[col])
|
||||
return [{col: v} for v in seen]
|
||||
lim = re.search(r'LIMIT (\d+)', s)
|
||||
if lim:
|
||||
matched = matched[:int(lim.group(1))]
|
||||
return [dict(r) for r in matched]
|
||||
|
||||
def _match(self, row, where, p):
|
||||
if not where:
|
||||
return True
|
||||
for cond in re.split(r' AND ', where):
|
||||
cond = cond.strip().strip('()')
|
||||
m = re.match(r'`(\w+)`\s*=\s*\$\{(\w+)\}\$$?', cond)
|
||||
if m:
|
||||
col, key = m.groups()
|
||||
want = p.get(key)
|
||||
if want in (None, ''):
|
||||
continue
|
||||
if str(row.get(col)) != str(want):
|
||||
return False
|
||||
continue
|
||||
m = re.match(r'`(\w+)`\s*=\s*\'([^\']*)\'$', cond)
|
||||
if m:
|
||||
col, want = m.groups()
|
||||
if str(row.get(col)) != want:
|
||||
return False
|
||||
continue
|
||||
m = re.match(r'`(\w+)`\s*>\s*\$\{(\w+)\}\$$?', cond)
|
||||
if m:
|
||||
col, key = m.groups()
|
||||
try:
|
||||
if float(row.get(col) or 0) <= float(p.get(key) or 0):
|
||||
return False
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
continue
|
||||
m = re.match(r'`(\w+)` IN \(([^)]*)\)$', cond)
|
||||
if m:
|
||||
col, keys = m.group(1), re.findall(r'\$\{(\w+)\}', m.group(2))
|
||||
vals = [p.get(k) for k in keys if p.get(k) not in (None, '')]
|
||||
if vals and str(row.get(col)) not in [str(v) for v in vals]:
|
||||
return False
|
||||
continue
|
||||
if 'LIKE' in cond.upper():
|
||||
m = re.match(r'`(\w+)` LIKE \$\{(\w+)\}\$$?', cond)
|
||||
if m:
|
||||
col, key = m.groups()
|
||||
pat = str(p.get(key) or '').replace('%', '')
|
||||
if pat and pat not in str(row.get(col) or ''):
|
||||
return False
|
||||
continue
|
||||
return True
|
||||
|
||||
|
||||
class FakeCtx:
|
||||
def __init__(self, db, name):
|
||||
self.db, self.name = db, name
|
||||
|
||||
async def __aenter__(self):
|
||||
return FakeSor(self.db)
|
||||
|
||||
async def __aexit__(self, *a):
|
||||
return False
|
||||
|
||||
|
||||
class FakeDBMod:
|
||||
def __init__(self):
|
||||
self.rows, self.log, self.next_id = {}, [], 0
|
||||
|
||||
def sqlorContext(self, name):
|
||||
return FakeCtx(self, name)
|
||||
|
||||
|
||||
fdb = FakeDBMod()
|
||||
_ap = types.ModuleType('apppublic')
|
||||
_ap.db = fdb
|
||||
sys.modules['apppublic'] = _ap
|
||||
|
||||
ah = types.ModuleType('ahserver')
|
||||
se = types.ModuleType('ahserver.serverenv')
|
||||
|
||||
|
||||
class ServerEnv:
|
||||
_inst = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._inst is None:
|
||||
cls._inst = object.__new__(cls)
|
||||
return cls._inst
|
||||
|
||||
def __init__(self):
|
||||
self.pbl_default_tenant = 'T-DEMO'
|
||||
|
||||
|
||||
se.ServerEnv = ServerEnv
|
||||
ah.serverenv = se
|
||||
sys.modules['ahserver'] = ah
|
||||
sys.modules['ahserver.serverenv'] = se
|
||||
_ge = types.ModuleType('ahserver.globalEnv')
|
||||
_ge.password_encode = lambda s: s
|
||||
sys.modules['ahserver.globalEnv'] = _ge
|
||||
_wa = types.ModuleType('ahserver.webapp')
|
||||
_wa.webapp = lambda init: None
|
||||
sys.modules['ahserver.webapp'] = _wa
|
||||
|
||||
RESULTS = []
|
||||
|
||||
|
||||
def check(name, cond, detail=''):
|
||||
RESULTS.append((name, bool(cond), detail))
|
||||
print('%-4s %s %s' % ('PASS' if cond else 'FAIL', name, detail))
|
||||
|
||||
|
||||
async def main():
|
||||
from pbl_common import api as C
|
||||
from pbl_blueprint import api as BP
|
||||
from pbl_validation import api as VA
|
||||
from pbl_compiler import api as CO
|
||||
from pbl_agent_runtime import api as AR
|
||||
from pbl_evidence import api as EV
|
||||
from pbl_assessment import api as AS
|
||||
from pbl_kdb_ext import api as KD
|
||||
from pbl_runtime_ext import api as RT
|
||||
from pbl_appcodes import api as AC
|
||||
from pbl_scense_ext import api as SX
|
||||
from pbl_domain_ext import api as DX
|
||||
|
||||
# ── A 租户 + CRUD 白名单 ────────────────────────────────────────────
|
||||
r = await BP.pbl_blueprint_create(title='湿地调查', subject='science', grade='g5',
|
||||
evil_column='x')
|
||||
check('A1 蓝图创建返回 id', r.get('ok') and r.get('id'), str(r.get('id')))
|
||||
check('A2 越权字段被丢弃不入库', r.get('dropped_fields') == ['evil_column'],
|
||||
str(r.get('dropped_fields')))
|
||||
check('A3 租户上下文注入', r.get('tenant_id') == 'T-DEMO', str(r.get('tenant_id')))
|
||||
rd = await BP.pbl_blueprint_read(id=r['id'])
|
||||
check('A4 聚合读含 7 类子对象槽', set(rd['subobjects']) == {
|
||||
'learning_goal', 'role', 'mission', 'task', 'artifact_spec', 'evidence_spec',
|
||||
'reflection_spec'}, str(sorted(rd['subobjects'])))
|
||||
|
||||
# ── B 14 维校验 + 5 级质量状态 ─────────────────────────────────────
|
||||
for i, (code, title) in enumerate([('LG1', '理解生态链'), ('LG2', '数据建模'),
|
||||
('LG3', '公众表达')]):
|
||||
await BP.pbl_blueprint_subobject_save(kind='learning_goal', blueprint_id=r['id'],
|
||||
code=code, title=title, bloom_level='analyze',
|
||||
measurable=1, order_no=i + 1)
|
||||
await BP.pbl_blueprint_update(id=r['id'], content_json={
|
||||
'schema': 'pbl.blueprint.v1', 'driving_question': '我们如何拯救社区湿地?',
|
||||
'scenario': '本地湿地富营养化,社区需要一份可执行方案',
|
||||
'learning_goals': [{'uid': 'LG1', 'code': 'LG1'}, {'uid': 'LG2', 'code': 'LG2'},
|
||||
{'uid': 'LG3', 'code': 'LG3'}],
|
||||
'rubric_criteria': [{'code': 'C1', 'learning_goal_id': 'LG1', 'weight': 40},
|
||||
{'code': 'C2', 'learning_goal_id': 'LG2', 'weight': 35},
|
||||
{'code': 'C3', 'learning_goal_id': 'LG3', 'weight': 25}]})
|
||||
await BP.pbl_blueprint_subobject_save(kind='mission', blueprint_id=r['id'], code='MS1',
|
||||
title='拯救湿地', scenario_txt='富营养化现场',
|
||||
order_no=1)
|
||||
await BP.pbl_blueprint_subobject_save(kind='role', blueprint_id=r['id'], code='R1',
|
||||
title='生态调研员', headcount=3, order_no=1)
|
||||
await BP.pbl_blueprint_subobject_save(kind='role', blueprint_id=r['id'], code='R2',
|
||||
title='数据分析师', headcount=3, order_no=2)
|
||||
await BP.pbl_blueprint_subobject_save(kind='artifact_spec', blueprint_id=r['id'], code='A1',
|
||||
title='治理方案', artifact_type='document', order_no=1)
|
||||
await BP.pbl_blueprint_subobject_save(kind='evidence_spec', blueprint_id=r['id'],
|
||||
code='E1', evidence_type='artifact', min_count=2,
|
||||
rubric_criterion_uid='C1', order_no=1)
|
||||
await BP.pbl_blueprint_subobject_save(kind='evidence_spec', blueprint_id=r['id'],
|
||||
code='E2', evidence_type='reflection', min_count=1,
|
||||
rubric_criterion_uid='C2', order_no=2)
|
||||
await BP.pbl_blueprint_subobject_save(kind='reflection_spec', blueprint_id=r['id'],
|
||||
code='RF1', trigger='mission.completed',
|
||||
prompt_txt='你学到了什么?', min_words=50,
|
||||
evidence_type='reflection', order_no=1)
|
||||
v = await VA.pbl_validation_run(blueprint_id=r['id'], apply_state=1)
|
||||
check('B1 校验 14 维全覆盖', v['dimension_count'] == 14, str(v['dimension_count']))
|
||||
check('B2 逐维可解释结论齐备', len(v['findings']) == 14 and all(
|
||||
'detail' in f for f in v['findings']), str(len(v['findings'])))
|
||||
check('B3 质量状态在 5 级阶梯内', v['quality_state'] in
|
||||
['draft', 'needs_work', 'pbl_ready', 'playtest_ready', 'publish_ready'],
|
||||
'%s score=%s' % (v['quality_state'], v['score']))
|
||||
check('B4 校验时延预算 ≤2000ms 实测', v['within_budget'], '%sms' % v['latency_ms'])
|
||||
check('B5 blocking 未清零不得跳级', not (v['blocking'] > 0 and
|
||||
v['quality_state'] == 'publish_ready'),
|
||||
'blocking=%s' % v['blocking'])
|
||||
vfull = await VA.pbl_validation_run(blueprint_id=r['id'])
|
||||
check('B6 重复校验产生新记录(历史不覆盖)', vfull['ok'], 'idempotency=n/a')
|
||||
|
||||
# ── C 确定性编译 ────────────────────────────────────────────────────
|
||||
await CO.pbl_capability_register(capability_key='pbl.mission.show', category='mission',
|
||||
is_enabled=1)
|
||||
await CO.pbl_capability_register(capability_key='pbl.artifact.submit', category='artifact',
|
||||
is_enabled=1)
|
||||
try:
|
||||
await CO.pbl_compiler_compile(blueprint_id=r['id'])
|
||||
check('C0 draft/未达门禁编译被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('C0 质量门禁拒编译', e.code == 'PBL_COMPILE_GATE_FAILED', e.code)
|
||||
await BP.pbl_blueprint_version_create(blueprint_id=r['id'])
|
||||
c1 = await CO.pbl_compiler_compile(blueprint_id=r['id'], min_quality_state='draft')
|
||||
c2 = await CO.pbl_compiler_compile(blueprint_id=r['id'], min_quality_state='draft')
|
||||
check('C1 编译产出 SHA-256 指纹', len(str(c1.get('content_fingerprint'))) == 64,
|
||||
str(c1.get('content_fingerprint'))[:16])
|
||||
check('C2 同输入+同编译版本 → 同指纹(29.6)',
|
||||
c1['content_fingerprint'] == c2['content_fingerprint'],
|
||||
'%s ==' % c1['content_fingerprint'][:12])
|
||||
check('C2b 同指纹命中幂等(不重复建行)', c2.get('idempotent') is True,
|
||||
str(c2.get('note'))[:20])
|
||||
pv = await CO.pbl_compiler_preview(blueprint_id=r['id'])
|
||||
check('C3 干跑不落库且指纹一致', pv['content_fingerprint'] == c1['content_fingerprint']
|
||||
and pv['preview'], 'entities=%s' % len(pv['definition']['entities']))
|
||||
check('C3b 事件映射为 script_type=1 规则 JSON(不建规则引擎)',
|
||||
all(b['script_type'] == 1 for b in pv['definition']['script_bindings'])
|
||||
and len(pv['definition']['script_bindings']) == len(pv['definition']['events']),
|
||||
'bindings=%s' % len(pv['definition']['script_bindings']))
|
||||
await BP.pbl_blueprint_update(id=r['id'], content_json={'schema': 'pbl.blueprint.v1',
|
||||
'missions': [{'obj_uid': 'M9'}]})
|
||||
await BP.pbl_blueprint_version_create(blueprint_id=r['id'])
|
||||
c3 = await CO.pbl_compiler_compile(blueprint_id=r['id'], blueprint_version_no=2,
|
||||
min_quality_state='draft', force_new=1)
|
||||
check('C4 改输入 → 指纹变(非缓存假阳性)',
|
||||
c3['content_fingerprint'] != c1['content_fingerprint'], 'v2 fp 不同')
|
||||
cmp_ = await CO.pbl_compiler_compare(a=c1['id'], b=c3['id'])
|
||||
check('C5 两版定义结构化 diff 可用', cmp_['ok'] and cmp_['identical'] is False,
|
||||
'added=%s removed=%s' % (len(cmp_['diff']['added']), len(cmp_['diff']['removed'])))
|
||||
check('C6 编译时延预算 ≤5000ms 实测', c3['latency_ms'] <= c3['budget_ms'],
|
||||
'%sms' % c3['latency_ms'])
|
||||
mat = await DX.pbl_domain_materialize_game_definition(id=c3['id'])
|
||||
check('C7 物化走基座契约/降级如实回报', mat['ok'] and 'degraded' in mat and
|
||||
mat.get('base_tables_altered') is False, 'degraded=%s' % mat.get('degraded'))
|
||||
|
||||
# ── E fail-closed 8 步裁决 ──────────────────────────────────────────
|
||||
await AR.pbl_tool_registry_save(tool_key='pbl.get', title='读蓝图', enabled=1,
|
||||
agent_scope='both')
|
||||
await AR.pbl_tool_registry_save(tool_key='pbl.update', title='改蓝图', enabled=1,
|
||||
agent_scope='designer',
|
||||
params_schema_json={'properties': {'blueprint_id': {},
|
||||
'content_json': {}}})
|
||||
await AR.pbl_tool_registry_save(tool_key='pbl.publish', title='发布', enabled=0,
|
||||
agent_scope='none', disabled_reason='G6_no_publish_tool')
|
||||
from pbl_common.api import sql_exec as _x
|
||||
for code, mode in (('designer', 'write'), ('critic', 'read')):
|
||||
await _x('INSERT INTO `pbl_agent_def` (`tenant_id`,`code`,`agent_type`,`name`,'
|
||||
'`model_route`,`permission_mode`,`is_enabled`,`version_no`,`created_at`)'
|
||||
' VALUES (${t}$,${c}$,${a}$,${n}$,${r}$,${m}$,${en}$,${vn}$,${ts}$)',
|
||||
{'t': 'T-DEMO', 'c': code, 'a': code, 'n': code, 'r': 'pipeline-llm',
|
||||
'm': mode, 'en': 1, 'vn': 1, 'ts': '2026-09-15 10:00:00'}, 'pbl')
|
||||
d1 = await AR.pbl_tool_adjudicate(agent_code='critic', tool_key='pbl.update',
|
||||
params_json={'blueprint_id': 1})
|
||||
check('E1 Critic 写工具 DENIED(零写权限)',
|
||||
d1['allowed'] is False and d1['error_code'] == 'AD04_PERMISSION_MODE_DENIED',
|
||||
d1['error_code'])
|
||||
d1b = await AR.pbl_tool_adjudicate(agent_code='critic', tool_key='pbl.get', params_json={})
|
||||
check('E1b Critic 读工具放行', d1b['allowed'] is True, str(d1b['steps'][-1]))
|
||||
d2 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.publish',
|
||||
params_json={})
|
||||
check('E2 pbl.publish 恒拒(G6 无绕过)',
|
||||
d2['allowed'] is False and d2['error_code'] == 'AD03_TOOL_DISABLED', d2['error_code'])
|
||||
d3 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='evil.tool', params_json={})
|
||||
check('E3 未注册工具 DENIED', d3['error_code'] == 'AD02_TOOL_UNREGISTERED', d3['error_code'])
|
||||
d4 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.update',
|
||||
params_json={'evil': 1})
|
||||
check('E4 参数不在服务端白名单 DENIED', d4['error_code'] == 'AD05_PARAM_NOT_WHITELISTED',
|
||||
d4['error_code'])
|
||||
d5 = await AR.pbl_tool_adjudicate(agent_code='designer', tool_key='pbl.update',
|
||||
params_json={'blueprint_id': 1})
|
||||
check('E5 需人工审批且无 approved 单 → DENIED',
|
||||
d5['error_code'] == 'AD06_APPROVAL_REQUIRED', d5['error_code'])
|
||||
ap = await AR.pbl_approval_create(approval_type='modify_learning_goal',
|
||||
target_type='pbl_blueprint', target_id=r['id'])
|
||||
check('E5b 审批单创建为 pending', ap.get('ok') and ap.get('approval_uid'),
|
||||
str(ap.get('approval_uid'))[:12])
|
||||
dec = await AR.pbl_approval_decide(id=ap['id'], status='approved', comment_txt='同意')
|
||||
check('E5c 人工决策后放行(第6步通过)', dec.get('ok') is True, dec.get('status'))
|
||||
try:
|
||||
await AR.pbl_approval_create(approval_type='auto_publish', target_id=1)
|
||||
check('E6 非法审批类型被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('E6 非法审批类型被拒', e.code == 'PBL_APPROVAL_TYPE_UNKNOWN', e.code)
|
||||
try:
|
||||
await AR.pbl_tool_registry_save(tool_key='sim.configure', enabled=1)
|
||||
check('E7 G6 禁用工具不得启用', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('E7 G6 禁用工具不得启用', e.code == 'PBL_TOOL_FORBIDDEN', e.code)
|
||||
tl = await AR.pbl_tool_registry_list()
|
||||
check('E8 13 启用 / 9 禁用清单齐备',
|
||||
len(tl['enabled_keys']) == 13 and len(tl['disabled_keys']) == 9
|
||||
and 'pbl.publish' in tl['disabled_keys'], str(tl['counts']))
|
||||
cr = await AR.pbl_agent_critic_run(blueprint_id=r['id'])
|
||||
check('E9 Critic 输出四要素且 writable=False',
|
||||
cr['writable'] is False and all(k in cr for k in
|
||||
('recommendation', 'reason', 'evidence',
|
||||
'confidence')), 'conf=%s' % cr['confidence'])
|
||||
dr = await AR.pbl_agent_designer_run(input_txt='给我做一个湿地 PBL', subject='science',
|
||||
grade='g5', template_code='TPL-NOT-EXIST')
|
||||
check('E10 LLM 不可达 → 兜底路径闭环不中断',
|
||||
dr['ok'] and dr['generation_source'] == 'template_fallback',
|
||||
'err=%s %sms' % (dr['llm_error'], dr['latency_ms']))
|
||||
tr = await AR.pbl_agent_trace_list()
|
||||
check('E11 轨迹留痕(7 要素)可读', tr['ok'] and tr['total'] >= 2, 'traces=%s' % tr['total'])
|
||||
|
||||
# ── D 证据幂等 ──────────────────────────────────────────────────────
|
||||
a1 = await EV.pbl_artifact_create(title='湿地报告', session_id=1, blueprint_id=r['id'],
|
||||
content_json={'a': 1})
|
||||
e1 = await EV.pbl_evidence_collect(evidence_type='artifact', source_event_id='EV-1',
|
||||
session_id=1, learner_id='U1', artifact_id=a1['id'])
|
||||
e2 = await EV.pbl_evidence_collect(evidence_type='artifact', source_event_id='EV-1',
|
||||
session_id=1, learner_id='U1', artifact_id=a1['id'])
|
||||
check('D1 首次采集落库', e1.get('deduped') is False, str(e1.get('id')))
|
||||
check('D2 重复采集幂等(唯一索引)', e2.get('deduped') is True and
|
||||
e2.get('id') == e1.get('id'), '%s==%s' % (e1.get('id'), e2.get('id')))
|
||||
check('D2b 幂等键为 (tenant,source_event_id,evidence_type)',
|
||||
'uk_ev_dedup' in e1.get('unique_index', ''), e1.get('unique_index'))
|
||||
try:
|
||||
await EV.pbl_evidence_collect(evidence_type='artifact')
|
||||
check('D3 缺 source_event_id 拒绝', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('D3 缺 source_event_id 拒绝', e.code == 'PBL_PARAM_MISSING', e.code)
|
||||
try:
|
||||
await EV.pbl_evidence_collect(evidence_type='ghost', source_event_id='X')
|
||||
check('D3b 非法证据类型拒绝', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('D3b 非法证据类型拒绝', e.code == 'PBL_EVIDENCE_TYPE_INVALID', e.code)
|
||||
au = await EV.pbl_artifact_update(id=a1['id'], content_json={'a': 2})
|
||||
check('D4 产出物更新版本递增', au.get('ok'), 'v=%s' % au.get('version_no'))
|
||||
el = await EV.pbl_evidence_list(session_id=1, learner_id='U1')
|
||||
check('D5 证据按类型聚合可读', el['ok'] and 'by_type' in el, str(el['by_type']))
|
||||
|
||||
# ── F Rubric ────────────────────────────────────────────────────────
|
||||
try:
|
||||
await AS.pbl_rubric_save(title='湿地评分', blueprint_id=r['id'], criteria_json=[
|
||||
{'code': 'C1', 'weight': 60, 'learning_goal_id': 'LG1', 'max_points': 5}])
|
||||
check('F1 权重≠100% 拒绝', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('F1 权重≠100% 拒绝', e.code == 'PBL_RUBRIC_WEIGHT_INVALID', e.code)
|
||||
try:
|
||||
await AS.pbl_rubric_save(title='x', blueprint_id=r['id'], criteria_json=[
|
||||
{'code': 'C1', 'weight': 100}])
|
||||
check('F1b 准则缺 learning_goal_id 拒绝', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('F1b 准则缺 learning_goal_id 拒绝',
|
||||
e.code == 'PBL_RUBRIC_ALIGNMENT_MISSING', e.code)
|
||||
ru = await AS.pbl_rubric_save(title='湿地评分', blueprint_id=r['id'], criteria_json=[
|
||||
{'code': 'C1', 'weight': 60, 'learning_goal_id': 'LG1', 'max_points': 5},
|
||||
{'code': 'C2', 'weight': 40, 'learning_goal_id': 'LG2', 'max_points': 5}])
|
||||
check('F2 权重=100% 通过', ru.get('ok') and ru.get('weight_total_ok'), str(ru.get('id')))
|
||||
sc = await AS.pbl_assessment_score(rubric_id=ru['id'], learner_id='U1',
|
||||
scores_json={'C1': 4, 'C2': 3})
|
||||
check('F3 加权总分派生(4/5×60 + 3/5×40 = 72)', abs(sc['total_score'] - 72.0) < 0.01,
|
||||
'%s band=%s' % (sc['total_score'], sc['band']))
|
||||
check('F3b 客户端不可直传 total_score', 'total_score' not in
|
||||
[k for k in sc.keys() if k == 'total_score'] or sc.get('derived'),
|
||||
str(sc.get('derived'))[:24])
|
||||
rep = await AS.pbl_assessment_report(learner_id='U1')
|
||||
check('F4 教师报告可读', rep.get('ok') and 'learners' in rep, str(rep.get('record_count')))
|
||||
|
||||
# ── G KDB 只读桩 ────────────────────────────────────────────────────
|
||||
ks = await KD.pbl_kdb_search(keyword='湿地')
|
||||
check('G1 KDB 检索为只读桩', ks.get('writable') is False and 'stub' in ks, ks.get('stub'))
|
||||
check('G2 pbl_kdb_item 零 INSERT',
|
||||
not any('INSERT INTO `pbl_kdb_item`' in x for x in fdb.log), 'log clean')
|
||||
ag = await KD.pbl_research_aggregate(group_by='band')
|
||||
check('G3 匿名聚合只读 + k_min=5', ag.get('anonymous') and ag.get('written') is False
|
||||
and ag.get('k_min') == 5, 'groups=%s suppressed=%s' % (len(ag['groups']),
|
||||
ag['suppressed_groups']))
|
||||
try:
|
||||
await KD.pbl_research_aggregate(group_by='learner_id')
|
||||
check('G4 个体维度聚合被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('G4 个体维度聚合被拒', e.code == 'PBL_AGG_DIM_INVALID', e.code)
|
||||
check('G5 无 kdb.write/research.write/sim.* 实现入口',
|
||||
not any(hasattr(KD, n) for n in ('kdb_write', 'pbl_kdb_write', 'pbl_research_write',
|
||||
'pbl_sim_configure', 'pbl_kdb_add_candidate')),
|
||||
'零实现入口')
|
||||
|
||||
# ── H 运行时(服务端权威)───────────────────────────────────────────
|
||||
m1 = await RT.pbl_session_member_add(session_id=1, user_id='U1')
|
||||
m2 = await RT.pbl_session_member_add(session_id=1, user_id='U1')
|
||||
check('H1 成员重复加入幂等', m2.get('deduped') is True, str(m2.get('members')))
|
||||
st = await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
|
||||
state_json={'completed': True},
|
||||
event_type='mission.completed', state_version=999)
|
||||
check('H2 客户端 state_version 被忽略', st.get('client_version_ignored') is True
|
||||
and st['state_version'] == 1, 'v=%s' % st['state_version'])
|
||||
st2 = await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
|
||||
state_json={'x': 1})
|
||||
check('H3 state_version 单调递增', st2['state_version'] == 2, str(st2['state_version']))
|
||||
check('H4 单事务 + 裁决/端到端时延实测在预算内', st2['within_budget'] is True
|
||||
and st2['transaction'] == 'single', json.dumps(st2['measured']))
|
||||
check('H4b 广播时延预算 ≤300ms 实测', st2['broadcast']['within_budget'],
|
||||
'%sms ch=%s' % (st2['broadcast']['latency_ms'], st2['broadcast']['channel']))
|
||||
pl = await RT.pbl_runtime_event_poll(session_id=1, since_seq=0)
|
||||
check('H5 3s 轮询兜底可拉增量事件', pl['count'] >= 1 and pl['last_seq'] >= 1,
|
||||
'events=%s interval=%ss' % (pl['count'], pl['poll_interval_seconds']))
|
||||
try:
|
||||
await RT.pbl_entity_state_apply(session_id=1, entity_id='mission:M1',
|
||||
state_json={'y': 2}, if_state_version=1)
|
||||
check('H6 乐观锁冲突被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('H6 乐观锁冲突被拒', e.code == 'PBL_STATE_CONFLICT', e.code)
|
||||
for i in range(2, 9):
|
||||
await RT.pbl_session_member_add(session_id=1, user_id='U%d' % i)
|
||||
try:
|
||||
await RT.pbl_session_member_add(session_id=1, user_id='U99')
|
||||
check('H7 超 8 人上限被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('H7 超 8 人上限被拒', e.code == 'PBL_SESSION_FULL', e.code)
|
||||
check('H8 事件表无 UPDATE/DELETE 出口(append-only)',
|
||||
not any(x.startswith('UPDATE `pbl_runtime_event`') or
|
||||
x.startswith('DELETE FROM `pbl_runtime_event`') for x in fdb.log), 'clean')
|
||||
|
||||
# ── I appcodes 幂等 ─────────────────────────────────────────────────
|
||||
i1 = await AC.pbl_appcodes_inject()
|
||||
i2 = await AC.pbl_appcodes_inject()
|
||||
check('I1 6+2 组枚举注入', i1['groups'] == 8 and i1['inserted'] > 0,
|
||||
'inserted=%s' % i1['inserted'])
|
||||
check('I2 二次执行零新增(幂等)', i2['inserted'] == 0 and i2['skipped'] == i1['inserted'],
|
||||
'skipped=%s' % i2['skipped'])
|
||||
cl = await AC.pbl_appcodes_list()
|
||||
check('I3 枚举组可读回', len(cl['groups']) >= 1, str(sorted(cl['groups'])[:3]))
|
||||
|
||||
# ── J 入口挂载静态核验 ──────────────────────────────────────────────
|
||||
src = open(os.path.join(ROOT, 'apps', 'pbls', 'app', 'pbls.py'), encoding='utf-8').read()
|
||||
i_env, i_rbac = src.index('env = ServerEnv()'), src.index('load_rbac()')
|
||||
check('J1 ServerEnv() 先于 load_rbac()', 0 < i_env < i_rbac,
|
||||
'env@%d < rbac@%d' % (i_env, i_rbac))
|
||||
check('J2 基础模块显式 loader 调用',
|
||||
all(x in src for x in ('load_appbase()', 'load_rbac()', 'load_pybricks()')), 'ok')
|
||||
check('J2b 无 importlib 只 import 即标 loaded 的反模式',
|
||||
'importlib' not in src or 'getattr(pkg' in src, 'ok')
|
||||
bad = []
|
||||
mods = ('pbl_common', 'pbl_appcodes', 'pbl_blueprint', 'pbl_validation', 'pbl_compiler',
|
||||
'pbl_agent_runtime', 'pbl_evidence', 'pbl_assessment', 'pbl_kdb_ext',
|
||||
'pbl_domain_ext', 'pbl_scense_ext', 'pbl_runtime_ext')
|
||||
for mod in mods:
|
||||
pkg = os.path.join(ROOT, 'modules', mod, mod)
|
||||
init_src = open(os.path.join(pkg, 'init.py'), encoding='utf-8').read()
|
||||
exp_src = open(os.path.join(pkg, '__init__.py'), encoding='utf-8').read()
|
||||
api_src = open(os.path.join(pkg, 'api.py'), encoding='utf-8').read()
|
||||
for n in re.findall(r'async def (pbl_\w+)', api_src):
|
||||
if ('env.%s = %s' % (n, n)) not in init_src or n not in exp_src:
|
||||
bad.append('%s:%s' % (mod, n))
|
||||
check('J3 契约三处同步(定义+导出+env注册)', not bad, '缺=%s' % (bad[:5] or '无'))
|
||||
check('J4 12 模块 load_* 均可导入',
|
||||
all(hasattr(__import__('importlib').import_module('%s.init' % m), 'load_%s' % m)
|
||||
for m in mods), 'ok')
|
||||
check('J5 模块嵌套包结构 modules/{m}/{m}/',
|
||||
all(os.path.isfile(os.path.join(ROOT, 'modules', m, m, 'api.py')) for m in mods), 'ok')
|
||||
check('J6 模块无 app.py / 无独立端口(模块非部署单元)',
|
||||
not any(os.path.exists(os.path.join(ROOT, 'modules', m, 'app.py')) or
|
||||
os.path.exists(os.path.join(ROOT, 'modules', m, 'app', '%s.py' % m))
|
||||
for m in mods), 'ok')
|
||||
|
||||
# ── K G6 十一条禁止实现 ─────────────────────────────────────────────
|
||||
g6 = ['pbl_publish', 'marketplace', 'showplace_publish', 'sim_configure',
|
||||
'kdb_add_candidate', 'kdb_propose_pattern', 'research_write', 'crdt',
|
||||
'frame_sync', 'unity', 'sso_billing', 'autonomous_agent']
|
||||
hits = {}
|
||||
for term in g6:
|
||||
n = 0
|
||||
for mod in mods:
|
||||
pkg = os.path.join(ROOT, 'modules', mod, mod)
|
||||
for fn in os.listdir(pkg):
|
||||
if not fn.endswith('.py'):
|
||||
continue
|
||||
t = open(os.path.join(pkg, fn), encoding='utf-8').read()
|
||||
n += len(re.findall(r'(?:async def|def)\s+\w*%s\w*\s*\(' % term, t, re.I))
|
||||
hits[term] = n
|
||||
check('K1 G6 十一条禁止实现零命中(无函数定义)',
|
||||
all(v == 0 for v in hits.values()), str({k: v for k, v in hits.items() if v}) or 'all 0')
|
||||
|
||||
# ── L 前端覆盖层 ────────────────────────────────────────────────────
|
||||
pr = await SX.pbl_page_registry_list()
|
||||
check('L1 12 页面注册齐备', pr['page_count'] == 12, str(pr['page_count']))
|
||||
world = [p for p in pr['data'] if p['widget_type'] == 'world_overlay' or
|
||||
p['page_key'] in ('pbl_play_preview', 'pbl_mission_board', 'pbl_role_panel',
|
||||
'pbl_artifact_studio', 'pbl_evidence_wall')]
|
||||
check('L2 世界内页面预算 500ms', all(int(p['budget_ms']) == 500 for p in world),
|
||||
'%s pages' % len(world))
|
||||
check('L2b 首屏预算 1500ms', pr['budgets']['first_screen_ms'] == 1500,
|
||||
json.dumps(pr['budgets']))
|
||||
bs = await SX.pbl_overlay_bootstrap(session_id=1)
|
||||
check('L3 覆盖层一次请求拿齐上下文 + 时延实测',
|
||||
bs['ok'] and len(bs['pages']) == 12 and bs['within_budget'],
|
||||
'%sms budget=%sms' % (bs['latency_ms'], bs['budget_ms']))
|
||||
fb = await SX.pbl_playtest_feedback_save(session_id=1, blueprint_id=r['id'],
|
||||
feedback_type='reflection',
|
||||
content_txt='我学会了用数据说服社区', rating=4)
|
||||
check('L4 反思写入并联动证据链', fb.get('ok'), str(fb.get('id')))
|
||||
try:
|
||||
await SX.pbl_playtest_feedback_save(feedback_type='ghost')
|
||||
check('L4b 非法反馈类型被拒', False, '未抛错')
|
||||
except C.PblError as e:
|
||||
check('L4b 非法反馈类型被拒', e.code == 'PBL_FEEDBACK_TYPE_INVALID', e.code)
|
||||
ctx = await SX.pbl_session_context_get(session_id=1)
|
||||
check('L5 会话上下文只读(authority=server)', ctx['authority'] == 'server'
|
||||
and ctx['max_members'] == 8, 'members=%s' % ctx['count'])
|
||||
|
||||
fails = [x for x in RESULTS if not x[1]]
|
||||
print('\n==== 结果:%d 项,通过 %d,失败 %d ====' % (len(RESULTS), len(RESULTS) - len(fails),
|
||||
len(fails)))
|
||||
for name, _ok, detail in fails:
|
||||
print(' FAIL', name, detail)
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(asyncio.get_event_loop().run_until_complete(main()))
|
||||
225
wwwroot/pbl_overlay/pbl_overlay.js
Normal file
225
wwwroot/pbl_overlay/pbl_overlay.js
Normal file
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* pbl_overlay.js — PBL 前端覆盖层(M9)
|
||||
* 挂载方式:在 scense 渲染容器之上叠加 DOM 覆盖层 + 事件处理器,
|
||||
* 不修改 scense/scense_game 渲染主循环(render loop untouched)。
|
||||
* 性能承诺:广播渲染 <= 500ms(poll/subscribe 增量应用);首屏 <= 1.5s(骨架先行 + 懒加载数据)。
|
||||
* 页面:12 页面(page_code 路由);组件:11 个;错误降级态:9 类。
|
||||
*/
|
||||
(function (global) {
|
||||
'use strict';
|
||||
|
||||
var API = '/api/pbls'; // 应用唯一入口 :9300
|
||||
var POLL_MS = 3000; // 3s 轮询兜底(与后端 POLL_FALLBACK_MS 一致)
|
||||
var BROADCAST_BUDGET_MS = 500; // 广播渲染预算
|
||||
var FIRST_SCREEN_BUDGET_MS = 1500; // 首屏预算
|
||||
|
||||
// 12 页面(ui-design.md)
|
||||
var PAGES = ['blueprint_list', 'blueprint_editor', 'goal_editor', 'scene_editor',
|
||||
'step_editor', 'rule_editor', 'validation_report', 'compile_artifacts',
|
||||
'agent_chat', 'approval_center', 'play_launcher', 'assessment_report'];
|
||||
|
||||
// 11 组件(覆盖层内,不改渲染层)
|
||||
var COMPONENTS = ['TopBar', 'NavRail', 'GoalCard', 'SceneCard', 'StepList', 'RubricEditor',
|
||||
'ValidationBadge', 'ArtifactViewer', 'ChatPanel', 'ApprovalQueue', 'HudOverlay'];
|
||||
|
||||
// 9 类错误降级态
|
||||
var ERROR_STATES = {
|
||||
NET_FAIL: ['网络中断', '显示缓存数据 + 重试按钮'],
|
||||
AUTH_FAIL: ['登录失效', '跳转登录,保留草稿于 localStorage'],
|
||||
LOAD_FAIL: ['数据加载失败', '骨架占位 + 局部重试'],
|
||||
VALIDATE_BLOCK: ['质量门禁未过', '阻断编译按钮,跳转校验报告'],
|
||||
APPROVAL_PENDING: ['待人工审批', '按钮置灰 + 审批单号提示'],
|
||||
LLM_FALLBACK: ['LLM 不可用', '离线模板兜底提示(US-13)'],
|
||||
RATE_LIMIT: ['请求过于频繁', '退避倒计时后自动重试'],
|
||||
SESSION_ENDED: ['会话已结束', '只读回放模式'],
|
||||
RENDER_DEGRADED: ['渲染降级', '覆盖层保留,2D 占位图']
|
||||
};
|
||||
|
||||
function PblOverlay(opts) {
|
||||
this.root = opts.root; // 覆盖层容器(scense 容器之上)
|
||||
this.playSessionId = opts.playSessionId || null;
|
||||
this.participantId = opts.participantId || null;
|
||||
this.tenantId = opts.tenantId;
|
||||
this.afterSeq = 0;
|
||||
this.timer = null;
|
||||
this.state = {};
|
||||
this.mount();
|
||||
}
|
||||
|
||||
PblOverlay.prototype.mount = function () {
|
||||
var t0 = performance.now();
|
||||
this.root.className = 'pbl-overlay';
|
||||
this.root.innerHTML =
|
||||
'<div class="pbl-topbar" data-c="TopBar"></div>' +
|
||||
'<div class="pbl-navrail" data-c="NavRail"></div>' +
|
||||
'<div class="pbl-main" data-page="blueprint_list"></div>' +
|
||||
'<div class="pbl-hud" data-c="HudOverlay"></div>';
|
||||
this.bindEvents();
|
||||
this.loadFirstScreen().then(function () {
|
||||
var cost = performance.now() - t0;
|
||||
if (cost > FIRST_SCREEN_BUDGET_MS) {
|
||||
global.console.warn('[pbl_overlay] first screen %.0fms > %.0fms budget',
|
||||
cost, FIRST_SCREEN_BUDGET_MS);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 事件处理器:只监听/派发,不介入渲染主循环 */
|
||||
PblOverlay.prototype.bindEvents = function () {
|
||||
var self = this;
|
||||
this.root.addEventListener('click', function (e) {
|
||||
var act = e.target.getAttribute('data-act');
|
||||
if (!act) return;
|
||||
var handlers = {
|
||||
'send-command': function (el) { self.sendCommand(el); },
|
||||
'retry': function () { self.pollOnce(); },
|
||||
'goto-approval': function () { self.showPage('approval_center'); },
|
||||
'goto-validation': function () { self.showPage('validation_report'); }
|
||||
};
|
||||
if (handlers[act]) handlers[act](e.target);
|
||||
});
|
||||
// scense 侧事件桥(scense 通过 CustomEvent 通知,覆盖层被动响应)
|
||||
if (global.addEventListener) {
|
||||
global.addEventListener('scense:entity-clicked', function (e) {
|
||||
self.onEntityClicked(e.detail);
|
||||
});
|
||||
global.addEventListener('scense:ready', function () { self.degrade('RENDER_DEGRADED', false); });
|
||||
}
|
||||
};
|
||||
|
||||
PblOverlay.prototype.loadFirstScreen = function () {
|
||||
var self = this;
|
||||
return this.api('play.list', { blueprint_id: self.blueprintId }).then(function (r) {
|
||||
self.state.plays = (r.data && r.data.items) || [];
|
||||
self.renderHud();
|
||||
}).catch(function () { self.degrade('LOAD_FAIL', true); });
|
||||
};
|
||||
|
||||
/* 服务端权威命令:client_seq 幂等;state_version 由服务端维护,客户端不传不校验 */
|
||||
PblOverlay.prototype.sendCommand = function (el) {
|
||||
var self = this;
|
||||
this._seq = (this._seq || 0) + 1;
|
||||
var body = {
|
||||
play_session_id: this.playSessionId,
|
||||
actor_id: this.participantId,
|
||||
client_seq: 'c-' + (this.participantId || 'anon') + '-' + this._seq,
|
||||
entity_id: el.getAttribute('data-entity'),
|
||||
capability: el.getAttribute('data-cap'),
|
||||
command: el.getAttribute('data-cmd'),
|
||||
state_delta: JSON.parse(el.getAttribute('data-delta') || '{}')
|
||||
};
|
||||
return this.api('runtime.command', body).then(function (r) {
|
||||
var d = r.data || {};
|
||||
if (d.ack_result === 'DENIED') {
|
||||
self.toast('操作被拒绝: ' + (d.reason || ''), true);
|
||||
} else if (d.ack_result === 'ACCEPTED') {
|
||||
self.afterSeq = Math.max(self.afterSeq, d.event_seq || 0);
|
||||
}
|
||||
return d;
|
||||
}).catch(function (e) {
|
||||
self.degrade(String(e.code || 'NET_FAIL'), true);
|
||||
});
|
||||
};
|
||||
|
||||
PblOverlay.prototype.onEntityClicked = function (detail) {
|
||||
if (!detail) return;
|
||||
this.api('runtime.state_read', {
|
||||
play_session_id: this.playSessionId, entity_id: detail.entityId
|
||||
}).then(function (r) {
|
||||
global.dispatchEvent(new CustomEvent('pbl:entity-state', { detail: r.data }));
|
||||
}).catch(function () { });
|
||||
};
|
||||
|
||||
/* 增量拉取并渲染:预算 500ms,超时降级为下一次轮询 */
|
||||
PblOverlay.prototype.pollOnce = function () {
|
||||
if (!this.playSessionId) return;
|
||||
var self = this, t0 = performance.now();
|
||||
this.api('runtime.poll', { play_session_id: this.playSessionId, after_seq: this.afterSeq })
|
||||
.then(function (r) {
|
||||
var d = r.data || {};
|
||||
(d.events || []).forEach(function (ev) {
|
||||
self.afterSeq = Math.max(self.afterSeq, ev.event_seq);
|
||||
if (performance.now() - t0 > BROADCAST_BUDGET_MS) { return; }
|
||||
global.dispatchEvent(new CustomEvent('pbl:broadcast', { detail: ev }));
|
||||
});
|
||||
self.state.states = d.states || self.state.states;
|
||||
self.renderHud();
|
||||
})
|
||||
.catch(function () { self.degrade('NET_FAIL', true); });
|
||||
};
|
||||
|
||||
PblOverlay.prototype.startPolling = function () {
|
||||
var self = this;
|
||||
this.stopPolling();
|
||||
this.timer = setInterval(function () { self.pollOnce(); }, POLL_MS);
|
||||
};
|
||||
PblOverlay.prototype.stopPolling = function () {
|
||||
if (this.timer) { clearInterval(this.timer); this.timer = null; }
|
||||
};
|
||||
|
||||
PblOverlay.prototype.renderHud = function () {
|
||||
var hud = this.root.querySelector('.pbl-hud');
|
||||
if (!hud) return;
|
||||
var goals = (this.state.goalProgress || []);
|
||||
hud.innerHTML = goals.map(function (g) {
|
||||
return '<div class="pbl-goal">' + escapeHtml(g.goal_code) + ' : ' + g.percent + '%</div>';
|
||||
}).join('');
|
||||
};
|
||||
|
||||
PblOverlay.prototype.showPage = function (code) {
|
||||
if (PAGES.indexOf(code) < 0) { return; }
|
||||
var main = this.root.querySelector('.pbl-main');
|
||||
main.setAttribute('data-page', code);
|
||||
this.api('overlay.save', {
|
||||
play_session_id: this.playSessionId, participant_id: this.participantId,
|
||||
page_code: code, overlay: { visited_at: Date.now() }
|
||||
}).catch(function () { });
|
||||
};
|
||||
|
||||
PblOverlay.prototype.degrade = function (key, on) {
|
||||
var info = ERROR_STATES[key];
|
||||
if (!info) return;
|
||||
var box = this.root.querySelector('.pbl-error') || document.createElement('div');
|
||||
if (on) {
|
||||
box.className = 'pbl-error';
|
||||
box.innerHTML = '<b>' + info[0] + '</b><span>' + info[1] + '</span>' +
|
||||
'<button data-act="retry">重试</button>';
|
||||
if (!box.parentNode) this.root.appendChild(box);
|
||||
} else if (box.parentNode) {
|
||||
box.parentNode.removeChild(box);
|
||||
}
|
||||
};
|
||||
|
||||
PblOverlay.prototype.toast = function (msg, isErr) {
|
||||
var t = document.createElement('div');
|
||||
t.className = 'pbl-toast' + (isErr ? ' pbl-toast-err' : '');
|
||||
t.textContent = msg;
|
||||
this.root.appendChild(t);
|
||||
setTimeout(function () { if (t.parentNode) t.parentNode.removeChild(t); }, 3000);
|
||||
};
|
||||
|
||||
PblOverlay.prototype.api = function (contract, body) {
|
||||
var self = this;
|
||||
return fetch(API + '/' + contract, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-Tenant-Id': this.tenantId || '' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(body || {})
|
||||
}).then(function (r) {
|
||||
if (r.status === 401) { self.degrade('AUTH_FAIL', true); throw { code: 'AUTH_FAIL' }; }
|
||||
if (r.status === 429) { self.degrade('RATE_LIMIT', true); throw { code: 'RATE_LIMIT' }; }
|
||||
return r.json();
|
||||
});
|
||||
};
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
global.PblOverlay = PblOverlay;
|
||||
global.PBL_PAGES = PAGES;
|
||||
global.PBL_COMPONENTS = COMPONENTS;
|
||||
global.PBL_ERROR_STATES = ERROR_STATES;
|
||||
})(window);
|
||||
Loading…
x
Reference in New Issue
Block a user