deliver: 交付收口(引擎代为提交)

This commit is contained in:
agent.develop 2026-09-19 09:00:57 +08:00
parent 4051e9d17f
commit c276af5d9d
3 changed files with 781 additions and 52 deletions

View File

@ -1,11 +1,12 @@
{
"tblname": "pbl_game_definition",
"alias": "pbl_compiler",
"_comment": "M3b 整改(QC #13):主键/外键类字段在 models/pbl_game_definition.json 中为 str(32)CRUD 渲染类型必须为 text原 number 会按数字输入框渲染 32 位字符串主键,排序/校验错位);同时补齐 M3b 规则导出四字段 rules_json/rules_hash/rule_count/rules_manifest 与 compile_status/error_message移除模型中不存在的幽灵字段 entity_count/event_count。",
"browserfields": {
"id": {
"label": "ID",
"list": true,
"type": "number"
"type": "text"
},
"tenant_id": {
"label": "租户ID",
@ -14,7 +15,7 @@
},
"blueprint_id": {
"label": "来源蓝图",
"type": "number",
"type": "text",
"list": true
},
"blueprint_version_no": {
@ -24,7 +25,7 @@
},
"compiler_version_id": {
"label": "编译版本",
"type": "number",
"type": "text",
"list": true
},
"content_fingerprint": {
@ -32,29 +33,19 @@
"type": "text",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"rules_hash": {
"label": "规则集合指纹",
"type": "text",
"list": true
},
"world_id": {
"label": "落库 world",
"rule_count": {
"label": "规则条数",
"type": "number",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "number",
"list": true
},
"entity_count": {
"label": "实体数",
"type": "number",
"list": true
},
"event_count": {
"label": "事件数",
"type": "number",
"compile_status": {
"label": "编译结果状态",
"type": "select",
"list": true
},
"quality_state": {
@ -62,10 +53,45 @@
"type": "select",
"list": true
},
"world_id": {
"label": "落库 world",
"type": "text",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "text",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text",
"list": false
},
"rules_json": {
"label": "规则映射产物(script_type=1)",
"type": "text",
"list": false
},
"rules_manifest": {
"label": "规则溯源清单",
"type": "text",
"list": false
},
"error_message": {
"label": "失败原因",
"type": "text",
"list": false
},
"created_at": {
"label": "创建时间",
"type": "text",
"list": true
}
},
"params": {
@ -73,7 +99,7 @@
"id": {
"label": "ID",
"list": true,
"type": "number"
"type": "text"
},
"tenant_id": {
"label": "租户ID",
@ -82,7 +108,7 @@
},
"blueprint_id": {
"label": "来源蓝图",
"type": "number",
"type": "text",
"list": true
},
"blueprint_version_no": {
@ -92,7 +118,7 @@
},
"compiler_version_id": {
"label": "编译版本",
"type": "number",
"type": "text",
"list": true
},
"content_fingerprint": {
@ -100,29 +126,19 @@
"type": "text",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"rules_hash": {
"label": "规则集合指纹",
"type": "text",
"list": true
},
"world_id": {
"label": "落库 world",
"rule_count": {
"label": "规则条数",
"type": "number",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "number",
"list": true
},
"entity_count": {
"label": "实体数",
"type": "number",
"list": true
},
"event_count": {
"label": "事件数",
"type": "number",
"compile_status": {
"label": "编译结果状态",
"type": "select",
"list": true
},
"quality_state": {
@ -130,16 +146,51 @@
"type": "select",
"list": true
},
"world_id": {
"label": "落库 world",
"type": "text",
"list": true
},
"scene_id": {
"label": "落库 scene",
"type": "text",
"list": true
},
"duration_ms": {
"label": "耗时",
"type": "number",
"list": true
},
"definition_json": {
"label": "Game Definition 正文",
"type": "text",
"list": false
},
"rules_json": {
"label": "规则映射产物(script_type=1)",
"type": "text",
"list": false
},
"rules_manifest": {
"label": "规则溯源清单",
"type": "text",
"list": false
},
"error_message": {
"label": "失败原因",
"type": "text",
"list": false
},
"created_at": {
"label": "创建时间",
"type": "text",
"list": true
}
},
"editable": {
"blueprint_id": {
"label": "来源蓝图",
"type": "number"
"type": "text"
},
"blueprint_version_no": {
"label": "蓝图版本",
@ -147,7 +198,7 @@
},
"compiler_version_id": {
"label": "编译版本",
"type": "number"
"type": "text"
},
"content_fingerprint": {
"label": "SHA-256 指纹",
@ -157,21 +208,33 @@
"label": "Game Definition 正文",
"type": "text"
},
"rules_json": {
"label": "规则映射产物(script_type=1)",
"type": "text"
},
"rules_hash": {
"label": "规则集合指纹",
"type": "text"
},
"rule_count": {
"label": "规则条数",
"type": "number"
},
"rules_manifest": {
"label": "规则溯源清单",
"type": "text"
},
"world_id": {
"label": "落库 world",
"type": "number"
"type": "text"
},
"scene_id": {
"label": "落库 scene",
"type": "number"
"type": "text"
},
"entity_count": {
"label": "实体数",
"type": "number"
},
"event_count": {
"label": "事件数",
"type": "number"
"compile_status": {
"label": "编译结果状态",
"type": "select"
},
"quality_state": {
"label": "编译时质量状态",
@ -180,9 +243,14 @@
"duration_ms": {
"label": "耗时",
"type": "number"
},
"error_message": {
"label": "失败原因",
"type": "text"
}
},
"_crud_note": "pbl_game_definition 是编译产物表(只读,写入由 pbl_compiler_compile 主流程完成),故本定义刻意不提供 new_data_url/update_data_url/delete_data_url —— 不开放手工增删改,避免绕过 29.6 确定性指纹与 rules_hash 收口逻辑。",
"order_by": "id DESC",
"page_size": 20
}
}
}

View File

@ -0,0 +1,269 @@
# -*- coding: utf-8 -*-
"""pbl_compiler.gd_rules_writer —— M3b 第12章收口Game Definition 规则产物落库。
背景本轮 M3b 整改的真实缺口
exporter.write_game_definition() 候选列名探测 GD候选集是
('game_definition','gd_json','definition','content') models/pbl_game_definition.json
的真实列名是 `definition_json` / `content_fingerprint` / `rules_json` / `rules_hash` /
`rule_count` / `rules_manifest` / `compile_status` / `quality_state` / `world_id` /
`scene_id` / `duration_ms` / `error_message`探测不命中 M3b 的规则映射产物
rules_json / rules_hash / rule_count / rules_manifest根本没写进 game_definition
规则导出与 game_definition 写入这条需求等于没落地
本文件是 M3b **权威 GD 写入器**列名直接对齐 models/pbl_game_definition.json
显式列名 INSERT/UPDATE不靠位置不靠猜并把规则四件套一并落库
铁律
- 所有 SELECT/INSERT/UPDATE 一律 tenant_id 打头缺失即抛 CompileErrorfail-closed
- 幂等 (tenant_id, blueprint_id, blueprint_version_no) 重复写只更新不新增
同输入必同 content_fingerprint / rules_hash29.6 确定性无时间戳无 uuid 进正文
- created_at/updated_at 属审计字段只写审计列绝不参与指纹计算
"""
import json
from .script_mapping import (
CompileError,
Issue,
canonical_rules_json,
build_rules_manifest,
canonical_dumps,
sha256_text,
strip_volatile,
to_int,
)
__all__ = [
'GD_TABLE',
'GD_COLUMNS',
'gd_primary_id',
'build_gd_row',
'write_gd_rules',
'read_gd_rules',
'read_gd_rules_by_id',
]
#: 表名与列集合,唯一事实源 = models/pbl_game_definition.json勿在此表外造列
GD_TABLE = 'pbl_game_definition'
GD_COLUMNS = (
'id', 'tenant_id', 'blueprint_id', 'blueprint_version_no', 'compiler_version_id',
'content_fingerprint', 'definition_json',
'rules_json', 'rules_hash', 'rule_count', 'rules_manifest',
'world_id', 'scene_id', 'compile_status', 'quality_state',
'duration_ms', 'error_message',
'created_by', 'created_at', 'updated_by', 'updated_at', 'is_deleted',
)
#: 参与内容指纹的列29.6:排除审计/耗时/状态等易变列)
_FINGERPRINT_COLUMNS = ('blueprint_id', 'blueprint_version_no', 'definition_json',
'rules_json')
def _text(value):
"""任意对象 → 稳定文本None → 空串dict/list → canonical JSON"""
if value is None:
return ''
if isinstance(value, str):
return value
if isinstance(value, (dict, list)):
return canonical_dumps(strip_volatile(value))
return str(value)
def gd_primary_id(tenant_id, blueprint_id, blueprint_version_no, content_fingerprint):
"""确定性主键sha256(tenant|bp|ver|fp) 前 32 位十六进制。
不用 uuid/不自增 保证同输入重编译 同一行幂等 upsert 才成立
"""
key = '|'.join([str(tenant_id or ''), str(blueprint_id or ''),
str(blueprint_version_no or ''), str(content_fingerprint or '')])
return sha256_text(key)[:32]
def content_fingerprint_of(row):
"""按参与指纹的列计算 GD 内容指纹(列序固定,与 sort_keys 无关)。"""
parts = []
for col in _FINGERPRINT_COLUMNS:
parts.append('%s=%s' % (col, _text(row.get(col))))
return sha256_text('\n'.join(parts))
def build_gd_row(gd, ctx, rules=None, manifest=None, base_definition_json=None,
issues=None):
"""组装一行 pbl_game_definition 记录(列名对齐 models不猜列
gd: M3a gd_builder + M3b rules 顶层键合成后的完整 GD dict10 顶层键 + rules
ctx: tenant_id* / blueprint_id* / blueprint_version* / compiler_version_id /
world_id / scene_id / quality_state / compile_status / duration_ms /
created_by / error_message
rules: build_rules() 产出的 script_type=1 规则列表
返回 (row_dict, issues)
"""
issues = list(issues or [])
ctx = dict(ctx or {})
tenant_id = str(ctx.get('tenant_id') or '').strip()
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失,拒绝写入 game_definition')
blueprint_id = str(ctx.get('blueprint_id') or ctx.get('bp_id') or '').strip()
blueprint_version_no = to_int(ctx.get('blueprint_version')
or ctx.get('blueprint_version_no'), 0)
if not blueprint_id or blueprint_version_no <= 0:
raise CompileError('E_GD_KEY_MISSING',
'blueprint_id / blueprint_version_no 缺失或非法,无法定位 game_definition')
rules = list(rules or [])
manifest = manifest or build_rules_manifest(rules)
gd = dict(gd or {})
gd_text = canonical_dumps(strip_volatile(gd))
rules_text = canonical_rules_json(rules)
row = {
'tenant_id': tenant_id,
'blueprint_id': blueprint_id,
'blueprint_version_no': blueprint_version_no,
'compiler_version_id': _text(ctx.get('compiler_version_id')
or ctx.get('compiler_version') or '')[:32],
'definition_json': base_definition_json if base_definition_json is not None else gd_text,
'rules_json': rules_text,
'rules_hash': _text(manifest.get('rules_hash'))[:64],
'rule_count': to_int(manifest.get('rule_count'), len(rules)),
'rules_manifest': canonical_dumps(strip_volatile(manifest)),
'world_id': _text(ctx.get('world_id'))[:32],
'scene_id': _text(ctx.get('scene_id'))[:32],
'compile_status': _text(ctx.get('compile_status') or ctx.get('status')
or 'compiled')[:32],
'quality_state': _text(ctx.get('quality_state'))[:64],
'duration_ms': to_int(ctx.get('duration_ms'), 0),
'error_message': _text(ctx.get('error_message'))[:60000],
'created_by': _text(ctx.get('created_by') or ctx.get('actor_id'))[:32],
'is_deleted': '0',
}
row['content_fingerprint'] = content_fingerprint_of(row)
row['id'] = gd_primary_id(row['tenant_id'], row['blueprint_id'],
row['blueprint_version_no'], row['content_fingerprint'])
if not row['rules_hash']:
issues.append(Issue('error', 'E_RULES_HASH_EMPTY',
'rules_manifest 未给出 rules_hash规则产物不可信',
'', 'rules_hash'))
if row['rule_count'] != len(rules):
issues.append(Issue('warning', 'W_RULE_COUNT_MISMATCH',
'rule_count=%s 与实际规则数=%s 不一致'
% (row['rule_count'], len(rules)), '', 'rule_count'))
return row, issues
async def _available_columns(sor, table):
"""探测实际库中该表已有列(跨宿主/未跑 DDL 时降级,不猜列名写 SQL"""
try:
recs = await sor.sqlExe(
'SELECT COLUMN_NAME AS c FROM INFORMATION_SCHEMA.COLUMNS '
'WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=${tbl}$', {'tbl': table})
cols = set()
for r in recs or []:
v = getattr(r, 'c', None) or getattr(r, 'COLUMN_NAME', None) \
or getattr(r, 'column_name', None)
if v:
cols.add(str(v))
if cols:
return cols
except Exception:
pass
return set(GD_COLUMNS)
async def write_gd_rules(sor, row, ctx=None):
"""幂等 upsert 一行 game_definition含规则四件套
唯一键tenant_id + blueprint_id + blueprint_version_noidx_gd_bp
返回 {'written','mode','gd_id','content_fingerprint','rules_hash','rule_count'}
"""
ctx = dict(ctx or {})
tenant_id = str(row.get('tenant_id') or '').strip()
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失,拒绝写入 game_definition')
cols = await _available_columns(sor, GD_TABLE)
payload = {k: v for k, v in row.items() if k in cols and k != 'id'}
missing = [c for c in ('definition_json', 'rules_json', 'rules_hash', 'rule_count')
if c not in cols]
if missing:
raise CompileError('E_GD_COLUMN_MISSING',
'pbl_game_definition 缺少 M3b 规则产物列 %s,请先执行 models/'
'pbl_game_definition.json 生成的 DDLjson2ddl' % missing)
where = {'tenant_id': tenant_id,
'blueprint_id': str(row.get('blueprint_id') or ''),
'blueprint_version_no': int(row.get('blueprint_version_no') or 0)}
try:
exist = await sor.R(GD_TABLE, dict(where, is_deleted='0'))
except Exception as e:
raise CompileError('E_GD_QUERY', '查询 game_definition 失败: %s' % e)
now = ctx.get('now_str') or ''
try:
if exist:
old_id = str(getattr(exist[0], 'id', '') or '')
upd = dict(payload)
if 'updated_at' in cols and now:
upd['updated_at'] = now
await sor.U(GD_TABLE, dict(upd, **{'id': old_id}))
mode, gd_id = 'update', old_id
else:
ins = dict(payload)
if 'id' in cols:
ins['id'] = row['id']
if 'created_at' in cols and now:
ins['created_at'] = now
await sor.C(GD_TABLE, ins)
mode, gd_id = 'insert', str(row.get('id') or '')
except Exception as e:
raise CompileError('E_GD_WRITE', '写入 game_definition 失败: %s' % e)
return {
'written': True,
'mode': mode,
'gd_id': gd_id,
'content_fingerprint': row.get('content_fingerprint'),
'rules_hash': row.get('rules_hash'),
'rule_count': row.get('rule_count'),
}
async def read_gd_rules(sor, tenant_id, blueprint_id, blueprint_version_no=None):
"""读回 GD含规则四件套rules_json 解析成 list。"""
if not tenant_id:
raise CompileError('E_TENANT_REQUIRED', 'tenant_id 缺失')
where = {'tenant_id': str(tenant_id), 'blueprint_id': str(blueprint_id or '')}
if blueprint_version_no not in (None, ''):
where['blueprint_version_no'] = int(blueprint_version_no)
recs = await sor.R(GD_TABLE, where)
out = []
for r in recs or []:
d = {c: getattr(r, c, None) for c in GD_COLUMNS}
if d.get('rules_json'):
try:
d['rules'] = json.loads(d['rules_json'])
except Exception:
d['rules'] = []
out.append(d)
return out
async def read_gd_rules_by_id(sor, tenant_id, gd_id):
"""按主键读单行(租户打头,跨租户查不到即 None"""
if not tenant_id or not gd_id:
raise CompileError('E_PARAM_MISSING', 'tenant_id / id 均为必填')
recs = await sor.R(GD_TABLE, {'tenant_id': str(tenant_id), 'id': str(gd_id)})
if not recs:
return None
r = recs[0]
d = {c: getattr(r, c, None) for c in GD_COLUMNS}
for key in ('rules_json', 'rules_manifest', 'definition_json'):
if d.get(key):
try:
d[key + '_parsed'] = json.loads(d[key])
except Exception:
d[key + '_parsed'] = None
return d

View File

@ -0,0 +1,392 @@
# -*- coding: utf-8 -*-
"""pbl_compiler.rules_export_api —— M3b Web 契约层:规则映射 / 导出 / GD 写入。
本文件是 M3b第12章 eventconditionresponse script_type=1 规则 JSON 的映射
产物导出与 game_definition 写入这条需求的**契约入口**三层分工
pbl_compiler/script_mapping.py 纯函数映射层ECR 归一规则 IO确定性
pbl_compiler/exporter.py 导出层规则 script_engine 脚本表 + 版本回写
pbl_compiler/gd_rules_writer.py GD 落库层规则四件套 pbl_game_definition 真实列
本文件 契约层参数校验 / 门禁 / 组装返回.dspy 直接调用
导出的 5 个契约 init.py CONTRACTS__init__.pyscripts/load_path.py 三处同步
pbl_compiler_export_rules 编译并导出规则写脚本表 + GD支持 dry_run
pbl_compiler_rules_preview 只映射不落库预览 rules_json / rules_hash / issues
pbl_compiler_rules_diff 两个蓝图版本之间的规则集差异diff_rules
pbl_game_definition_rules_get 读回 GD 的规则产物列rules_json/rules_hash/rule_count/manifest
pbl_script_rule_list 分页查询已导出的 pbl_script_rule
铁律 api.py 一致
- tenant_id 打头缺失即 fail-closed不猜租户不落跨租户数据
- error issue 存在时绝不落库拒绝产出半成品规则
- 返回值统一 {'status','data',...}异常统一转 PblError 语义 dict不抛裸异常给前端
"""
import time
from pbl_common.api import PblError, tenant_id as _ctx_tenant_id
from pbl_compiler.script_mapping import (
CompileError,
Issue,
build_rules,
build_rules_manifest,
canonical_rules_json,
diff_rules,
has_errors,
validate_rules,
)
from pbl_compiler import exporter
from pbl_compiler.gd_rules_writer import (
GD_TABLE,
build_gd_row,
content_fingerprint_of,
read_gd_rules,
read_gd_rules_by_id,
write_gd_rules,
)
__all__ = [
'SCRIPT_TYPE_RULE',
'export_rules_contract',
'preview_rules_contract',
'rules_diff_contract',
'gd_rules_get_contract',
'script_rule_list_contract',
'pbl_compiler_export_rules',
'pbl_compiler_rules_preview',
'pbl_compiler_rules_diff',
'pbl_game_definition_rules_get',
'pbl_script_rule_list',
]
SCRIPT_TYPE_RULE = 1
#: 脚本表候选(与 exporter._SCRIPT_TABLE_CANDIDATES 同源,只用于列表查询降级)
_RULE_TABLE_CANDIDATES = ('pbl_script_rule',)
# ---------------------------------------------------------------------------
# 内部工具
# ---------------------------------------------------------------------------
def _ok(data, **extra):
out = {'status': 'ok', 'data': data}
out.update(extra)
return out
def _fail(code, message, detail=None):
out = {'status': 'error', 'code': code, 'message': message, 'data': None}
if detail is not None:
out['detail'] = detail
return out
def _require_tenant():
tid = str(_ctx_tenant_id() or '').strip()
if not tid:
raise CompileError('E_TENANT_REQUIRED', '缺少租户上下文tenant_id拒绝执行规则导出')
return tid
def _issue_dicts(issues, limit=200):
out = []
for it in list(issues or [])[:limit]:
out.append({k: it.get(k) for k in ('severity', 'code', 'message', 'rule_id', 'path')})
return out
async def _load_snapshot(blueprint_id, version_no):
"""取蓝图版本快照(复用 api._snapshot失败降级为直接查库"""
tid = _require_tenant()
try:
from pbl_compiler.api import _snapshot
snap, vrow = await _snapshot(tid, blueprint_id, version_no)
if snap:
return snap, vrow
except Exception:
pass
from pbl_compiler.api import _one
vrow = await _one(
'SELECT * FROM `pbl_blueprint_version` WHERE `tenant_id` = ${t}$'
' AND `blueprint_id` = ${b}$ AND `version_no` = ${v}$ LIMIT 1',
{'t': tid, 'b': blueprint_id, 'v': version_no})
if not vrow:
raise CompileError('E_SNAPSHOT_NOT_FOUND',
'未找到蓝图版本快照: blueprint_id=%s version_no=%s'
% (blueprint_id, version_no))
for key in ('snapshot_json', 'content_json', 'snapshot'):
if vrow.get(key):
import json as _json
raw = vrow[key]
return (_json.loads(raw) if isinstance(raw, str) else raw), vrow
raise CompileError('E_SNAPSHOT_EMPTY', '蓝图版本快照字段为空,无法映射规则')
async def _map_rules(blueprint_id, version_no, kw):
"""快照 → 规则(含校验),返回 (rules, issues, manifest, snapshot, vrow)。"""
snapshot, vrow = await _load_snapshot(blueprint_id, version_no)
ctx = {
'tenant_id': _require_tenant(),
'blueprint_id': str(blueprint_id),
'blueprint_version': str(version_no or (vrow or {}).get('version_no') or ''),
'strict': bool(kw.get('strict', True)),
'known_actions': kw.get('known_actions') or None,
'event_types': kw.get('event_types') or None,
'compiler_version': str(kw.get('compiler_version') or ''),
}
issues = []
rules, map_issues = build_rules(snapshot, ctx)
issues.extend(map_issues)
issues.extend(validate_rules(rules))
manifest = build_rules_manifest(rules)
return rules, issues, manifest, snapshot, ctx
# ---------------------------------------------------------------------------
# 契约 1导出规则写 script_engine 脚本表 + 写 game_definition
# ---------------------------------------------------------------------------
async def export_rules_contract(**kw):
"""M3b 主导出契约ECR → 规则 → 脚本表 → pbl_game_definition 规则列。
入参blueprint_id*, version_no*, compiler_version_id, dry_run, skip_gd,
skip_scripts, base_gd(可选 M3a GD), status, duration_ms
fail-closed任一 error issue 不落库返回 status=error + issues
"""
t0 = time.time()
kw = dict(kw or {})
blueprint_id = str(kw.get('blueprint_id') or '').strip()
if not blueprint_id:
return _fail('PBL_E_PARAM', 'blueprint_id 必填')
version_no = kw.get('version_no') or kw.get('blueprint_version_no') or 1
dry_run = bool(kw.get('dry_run'))
try:
tid = _require_tenant()
rules, issues, manifest, snapshot, ctx = await _map_rules(blueprint_id, version_no, kw)
if has_errors(issues):
errs = [i for i in issues if i['severity'] == 'error']
return _fail('E_RULE_INVALID',
'规则映射校验未通过(%d 个错误),拒绝导出' % len(errs),
{'issues': _issue_dicts(errs), 'rule_count': len(rules)})
if dry_run:
return _ok({
'dry_run': True,
'rule_count': len(rules),
'rules_hash': manifest.get('rules_hash'),
'rules_json': canonical_rules_json(rules),
'rules_manifest': manifest,
'issues': _issue_dicts(issues),
})
result = {
'dry_run': False,
'rule_count': len(rules),
'rules_hash': manifest.get('rules_hash'),
'rules_manifest': manifest,
'issues': _issue_dicts(issues),
}
async with exporter.open_sor(None, 'pbl_compiler') as sor:
# ② 导出到 script_engine 脚本表(幂等 UPSERT
if not kw.get('skip_scripts'):
export_result = await exporter.export_rules(sor, rules, ctx, issues)
result['export'] = {k: v for k, v in (export_result or {}).items()
if k != 'issues'}
# ③ 写 game_definition规则四件套 + GD 正文,列名对齐 models
if not kw.get('skip_gd'):
base_gd = kw.get('base_gd')
if isinstance(base_gd, str):
import json as _json
base_gd = _json.loads(base_gd)
gd, gd_text, gd_hash = exporter.build_game_definition(
rules, ctx, base_gd, result.get('export'))
ctx['compiler_version_id'] = str(kw.get('compiler_version_id') or '')
ctx['compile_status'] = str(kw.get('status') or 'compiled')
ctx['quality_state'] = str(kw.get('quality_state') or '')
ctx['world_id'] = str(kw.get('world_id') or '')
ctx['scene_id'] = str(kw.get('scene_id') or '')
ctx['duration_ms'] = int((time.time() - t0) * 1000)
row, row_issues = build_gd_row(gd, ctx, rules, manifest,
base_definition_json=gd_text, issues=issues)
result['content_fingerprint'] = row['content_fingerprint']
result['gd_row'] = {k: row[k] for k in
('id', 'blueprint_id', 'blueprint_version_no',
'rule_count', 'rules_hash', 'compile_status')}
if has_errors(row_issues):
return _fail('E_GD_ROW_INVALID', 'GD 行组装校验未通过,拒绝写入',
{'issues': _issue_dicts(
[i for i in row_issues if i['severity'] == 'error'])})
result['game_definition'] = await write_gd_rules(sor, row, ctx)
result['duration_ms'] = int((time.time() - t0) * 1000)
result['tenant_id'] = tid
result['blueprint_id'] = blueprint_id
result['blueprint_version'] = str(version_no)
return _ok(result)
except CompileError as e:
return _fail(getattr(e, 'code', 'E_RULE_EXPORT'), str(e),
getattr(e, 'detail', None))
except PblError as e:
return _fail(getattr(e, 'code', 'PBL_E_COMPILE'), str(e))
except Exception as e: # pragma: no cover - 兜底,绝不裸异常给前端
return _fail('E_RULE_EXPORT_UNEXPECTED', '%s: %s' % (type(e).__name__, e))
# ---------------------------------------------------------------------------
# 契约 2规则映射预览零写入
# ---------------------------------------------------------------------------
async def preview_rules_contract(**kw):
"""只跑映射 + 校验不碰任何表第12章映射逻辑的单步验收入口"""
kw = dict(kw or {})
kw['dry_run'] = True
return await export_rules_contract(**kw)
# ---------------------------------------------------------------------------
# 契约 3两个蓝图版本之间的规则差异
# ---------------------------------------------------------------------------
async def rules_diff_contract(**kw):
"""from_blueprint_id/from_version_no → to_*diff_rules 产出新增/删除/变更。"""
kw = dict(kw or {})
bp_a = str(kw.get('blueprint_id') or kw.get('from_blueprint_id') or '').strip()
bp_b = str(kw.get('to_blueprint_id') or bp_a).strip()
ver_a = kw.get('from_version_no') or kw.get('version_no') or 1
ver_b = kw.get('to_version_no') or 1
if not bp_a or not bp_b:
return _fail('PBL_E_PARAM', 'blueprint_id / to_blueprint_id 必填')
try:
_require_tenant()
rules_a, issues_a, man_a, _, _ = await _map_rules(bp_a, ver_a, kw)
rules_b, issues_b, man_b, _, _ = await _map_rules(bp_b, ver_b, kw)
diff = diff_rules(rules_a, rules_b)
return _ok({
'from': {'blueprint_id': bp_a, 'version_no': str(ver_a),
'rule_count': man_a.get('rule_count'), 'rules_hash': man_a.get('rules_hash')},
'to': {'blueprint_id': bp_b, 'version_no': str(ver_b),
'rule_count': man_b.get('rule_count'), 'rules_hash': man_b.get('rules_hash')},
'changed': bool(man_a.get('rules_hash') != man_b.get('rules_hash')),
'diff': diff,
'issues': {'from': _issue_dicts(issues_a), 'to': _issue_dicts(issues_b)},
})
except CompileError as e:
return _fail(getattr(e, 'code', 'E_RULES_DIFF'), str(e))
except Exception as e: # pragma: no cover
return _fail('E_RULES_DIFF_UNEXPECTED', '%s: %s' % (type(e).__name__, e))
# ---------------------------------------------------------------------------
# 契约 4读回 GD 的规则产物
# ---------------------------------------------------------------------------
async def gd_rules_get_contract(**kw):
"""按 id 或 (blueprint_id[, version_no]) 读 pbl_game_definition 的规则列。"""
kw = dict(kw or {})
try:
tid = _require_tenant()
gd_id = str(kw.get('id') or kw.get('game_def_id') or '').strip()
async with exporter.open_sor(None, 'pbl_compiler') as sor:
if gd_id:
row = await read_gd_rules_by_id(sor, tid, gd_id)
rows = [row] if row else []
else:
bp = str(kw.get('blueprint_id') or '').strip()
if not bp:
return _fail('PBL_E_PARAM', 'id 或 blueprint_id 至少一个必填')
rows = await read_gd_rules(sor, tid, bp, kw.get('version_no')
or kw.get('blueprint_version_no'))
slim = []
for r in rows:
if not r:
continue
slim.append({
'id': r.get('id'),
'blueprint_id': r.get('blueprint_id'),
'blueprint_version_no': r.get('blueprint_version_no'),
'content_fingerprint': r.get('content_fingerprint'),
'rules_hash': r.get('rules_hash'),
'rule_count': r.get('rule_count'),
'compile_status': r.get('compile_status'),
'quality_state': r.get('quality_state'),
'rules_manifest': r.get('rules_manifest_parsed'),
'rules': r.get('rules') or [],
})
return _ok({'table': GD_TABLE, 'total': len(slim), 'rows': slim})
except CompileError as e:
return _fail(getattr(e, 'code', 'E_GD_READ'), str(e))
except Exception as e: # pragma: no cover
return _fail('E_GD_READ_UNEXPECTED', '%s: %s' % (type(e).__name__, e))
# ---------------------------------------------------------------------------
# 契约 5已导出规则行分页查询
# ---------------------------------------------------------------------------
async def script_rule_list_contract(**kw):
"""分页查 pbl_script_ruleM3b 导出产物行tenant_id 强制打头。"""
kw = dict(kw or {})
try:
tid = _require_tenant()
page = max(1, int(kw.get('page') or 1))
rows_n = min(200, max(1, int(kw.get('rows') or kw.get('page_size') or 20)))
offset = (page - 1) * rows_n
where = ['`tenant_id` = ${t}$']
params = {'t': tid, 'lim': rows_n, 'off': offset}
if kw.get('blueprint_id'):
where.append('`blueprint_id` = ${b}$')
params['b'] = str(kw.get('blueprint_id'))
if kw.get('game_def_id'):
where.append('`game_def_id` = ${g}$')
params['g'] = str(kw.get('game_def_id'))
if kw.get('rules_hash'):
where.append('`rules_hash` = ${h}$')
params['h'] = str(kw.get('rules_hash'))
if kw.get('event_type'):
where.append('`event_type` = ${e}$')
params['e'] = str(kw.get('event_type'))
wsql = ' AND '.join(where)
from pbl_compiler.api import _rows
total_rows = await _rows('SELECT COUNT(*) AS c FROM `%s` WHERE %s'
% (_RULE_TABLE_CANDIDATES[0], wsql), params)
total = int((total_rows[0].get('c') if total_rows else 0) or 0)
data = await _rows(
'SELECT `id`,`rule_id`,`script_code`,`name`,`script_type`,`event_type`,'
'`priority`,`enabled`,`condition_count`,`response_count`,`content_hash`,'
'`rules_hash`,`sync_status`,`blueprint_id`,`blueprint_version`,'
'`game_def_id`,`compile_task_no`,`created_at` '
'FROM `%s` WHERE %s ORDER BY `priority` DESC, `rule_id` ASC'
' LIMIT ${lim}$ OFFSET ${off}$' % (_RULE_TABLE_CANDIDATES[0], wsql), params)
return _ok({'table': _RULE_TABLE_CANDIDATES[0], 'page': page, 'rows': rows_n,
'total': total, 'rows_data': list(data or [])})
except CompileError as e:
return _fail(getattr(e, 'code', 'E_RULE_LIST'), str(e))
except Exception as e: # pragma: no cover
return _fail('E_RULE_LIST_UNEXPECTED', '%s: %s' % (type(e).__name__, e))
# ---------------------------------------------------------------------------
# 契约名(.dspy 直接调用的全局名)
# ---------------------------------------------------------------------------
async def pbl_compiler_export_rules(**kw):
return await export_rules_contract(**kw)
async def pbl_compiler_rules_preview(**kw):
return await preview_rules_contract(**kw)
async def pbl_compiler_rules_diff(**kw):
return await rules_diff_contract(**kw)
async def pbl_game_definition_rules_get(**kw):
return await gd_rules_get_contract(**kw)
async def pbl_script_rule_list(**kw):
return await script_rule_list_contract(**kw)