feat(pbl_runtime_ext): 初始提交 - pbls 项目模块代码入库
This commit is contained in:
parent
33706399dd
commit
e7cffb940d
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/
|
||||||
118
json/runtime_ext_pbl_entity_state.json
Normal file
118
json/runtime_ext_pbl_entity_state.json
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"tblname": "pbl_entity_state",
|
||||||
|
"alias": "pbl_runtime_ext",
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"label": "实体标识",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_json": {
|
||||||
|
"label": "状态",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "单调版本",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"label": "状态摘要",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"updated_by": {
|
||||||
|
"label": "写入者",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"label": "实体标识",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_json": {
|
||||||
|
"label": "状态",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "单调版本",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"label": "状态摘要",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"updated_by": {
|
||||||
|
"label": "写入者",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"entity_id": {
|
||||||
|
"label": "实体标识",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"state_json": {
|
||||||
|
"label": "状态",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "单调版本",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"checksum": {
|
||||||
|
"label": "状态摘要",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"updated_by": {
|
||||||
|
"label": "写入者",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order_by": "id DESC",
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
174
json/runtime_ext_pbl_runtime_event.json
Normal file
174
json/runtime_ext_pbl_runtime_event.json
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
{
|
||||||
|
"tblname": "pbl_runtime_event",
|
||||||
|
"alias": "pbl_runtime_ext",
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"event_uid": {
|
||||||
|
"label": "事件UID",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"world_id": {
|
||||||
|
"label": "世界",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"actor_id": {
|
||||||
|
"label": "触发者",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"event_type": {
|
||||||
|
"label": "事件类型",
|
||||||
|
"type": "select",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"payload_json": {
|
||||||
|
"label": "事件体",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"causation_id": {
|
||||||
|
"label": "因果链",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "状态版本",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"label": "会话内单调序号",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"occurred_at": {
|
||||||
|
"label": "发生时间",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"event_uid": {
|
||||||
|
"label": "事件UID",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"world_id": {
|
||||||
|
"label": "世界",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"actor_id": {
|
||||||
|
"label": "触发者",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"event_type": {
|
||||||
|
"label": "事件类型",
|
||||||
|
"type": "select",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"payload_json": {
|
||||||
|
"label": "事件体",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"causation_id": {
|
||||||
|
"label": "因果链",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "状态版本",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"label": "会话内单调序号",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"occurred_at": {
|
||||||
|
"label": "发生时间",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"event_uid": {
|
||||||
|
"label": "事件UID",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"world_id": {
|
||||||
|
"label": "世界",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"actor_id": {
|
||||||
|
"label": "触发者",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"event_type": {
|
||||||
|
"label": "事件类型",
|
||||||
|
"type": "select"
|
||||||
|
},
|
||||||
|
"payload_json": {
|
||||||
|
"label": "事件体",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"causation_id": {
|
||||||
|
"label": "因果链",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"state_version": {
|
||||||
|
"label": "状态版本",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"seq": {
|
||||||
|
"label": "会话内单调序号",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"occurred_at": {
|
||||||
|
"label": "发生时间",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order_by": "id DESC",
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
118
json/runtime_ext_pbl_world_state_snapshot.json
Normal file
118
json/runtime_ext_pbl_world_state_snapshot.json
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"tblname": "pbl_world_state_snapshot",
|
||||||
|
"alias": "pbl_runtime_ext",
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"snapshot_uid": {
|
||||||
|
"label": "快照UID",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_fingerprint": {
|
||||||
|
"label": "状态指纹",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"entities_json": {
|
||||||
|
"label": "实体集",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"events_through_seq": {
|
||||||
|
"label": "截至序号",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"label": "触发原因",
|
||||||
|
"type": "select",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"browserfields": {
|
||||||
|
"id": {
|
||||||
|
"label": "ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"label": "租户ID",
|
||||||
|
"list": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"snapshot_uid": {
|
||||||
|
"label": "快照UID",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"state_fingerprint": {
|
||||||
|
"label": "状态指纹",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"entities_json": {
|
||||||
|
"label": "实体集",
|
||||||
|
"type": "text",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"events_through_seq": {
|
||||||
|
"label": "截至序号",
|
||||||
|
"type": "number",
|
||||||
|
"list": true
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"label": "触发原因",
|
||||||
|
"type": "select",
|
||||||
|
"list": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editable": {
|
||||||
|
"session_id": {
|
||||||
|
"label": "会话",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"snapshot_uid": {
|
||||||
|
"label": "快照UID",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"state_fingerprint": {
|
||||||
|
"label": "状态指纹",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"entities_json": {
|
||||||
|
"label": "实体集",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
"events_through_seq": {
|
||||||
|
"label": "截至序号",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"label": "触发原因",
|
||||||
|
"type": "select"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"order_by": "id DESC",
|
||||||
|
"page_size": 20
|
||||||
|
}
|
||||||
|
}
|
||||||
100
models/pbl_entity_state.json
Normal file
100
models/pbl_entity_state.json
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "pbl_entity_state",
|
||||||
|
"comment": "实体状态(服务端权威,state_version 客户端禁写)",
|
||||||
|
"module": "pbl_runtime_ext",
|
||||||
|
"engine": "mariadb",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"tenant_scoped": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "tenant_id",
|
||||||
|
"comment": "租户ID(强制打头)",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"comment": "主键",
|
||||||
|
"null": false,
|
||||||
|
"type": "int",
|
||||||
|
"length": 20,
|
||||||
|
"unsigned": true,
|
||||||
|
"auto_increment": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"comment": "会话",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entity_id",
|
||||||
|
"comment": "实体标识",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state_json",
|
||||||
|
"comment": "状态",
|
||||||
|
"null": true,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state_version",
|
||||||
|
"comment": "单调版本",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "checksum",
|
||||||
|
"comment": "状态摘要",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_by",
|
||||||
|
"comment": "写入者",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"comment": "创建时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"comment": "更新时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "uk_es",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"tenant_id",
|
||||||
|
"session_id",
|
||||||
|
"entity_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PRIMARY",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": []
|
||||||
|
}
|
||||||
139
models/pbl_runtime_event.json
Normal file
139
models/pbl_runtime_event.json
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "pbl_runtime_event",
|
||||||
|
"comment": "运行时事件(append-only,按月 RANGE 分区)",
|
||||||
|
"module": "pbl_runtime_ext",
|
||||||
|
"engine": "mariadb",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"tenant_scoped": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "tenant_id",
|
||||||
|
"comment": "租户ID(强制打头)",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"comment": "主键",
|
||||||
|
"null": false,
|
||||||
|
"type": "int",
|
||||||
|
"length": 20,
|
||||||
|
"unsigned": true,
|
||||||
|
"auto_increment": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "event_uid",
|
||||||
|
"comment": "事件UID",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"comment": "会话",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "world_id",
|
||||||
|
"comment": "世界",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "actor_id",
|
||||||
|
"comment": "触发者",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "event_type",
|
||||||
|
"comment": "事件类型",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "payload_json",
|
||||||
|
"comment": "事件体",
|
||||||
|
"null": true,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "causation_id",
|
||||||
|
"comment": "因果链",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state_version",
|
||||||
|
"comment": "状态版本",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "seq",
|
||||||
|
"comment": "会话内单调序号",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "occurred_at",
|
||||||
|
"comment": "发生时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"comment": "创建时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"comment": "更新时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "uk_re_uid",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"tenant_id",
|
||||||
|
"event_uid",
|
||||||
|
"occurred_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "idx_re_session_seq",
|
||||||
|
"unique": false,
|
||||||
|
"fields": [
|
||||||
|
"tenant_id",
|
||||||
|
"session_id",
|
||||||
|
"seq"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PRIMARY",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": [
|
||||||
|
{
|
||||||
|
"field": "event_type",
|
||||||
|
"appcode": "pbl_event_type"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
99
models/pbl_world_state_snapshot.json
Normal file
99
models/pbl_world_state_snapshot.json
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
{
|
||||||
|
"summary": [
|
||||||
|
{
|
||||||
|
"name": "pbl_world_state_snapshot",
|
||||||
|
"comment": "世界状态快照(掉线补齐/离线兜底)",
|
||||||
|
"module": "pbl_runtime_ext",
|
||||||
|
"engine": "mariadb",
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
"tenant_scoped": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "tenant_id",
|
||||||
|
"comment": "租户ID(强制打头)",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"comment": "主键",
|
||||||
|
"null": false,
|
||||||
|
"type": "int",
|
||||||
|
"length": 20,
|
||||||
|
"unsigned": true,
|
||||||
|
"auto_increment": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "session_id",
|
||||||
|
"comment": "会话",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "snapshot_uid",
|
||||||
|
"comment": "快照UID",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state_fingerprint",
|
||||||
|
"comment": "状态指纹",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "entities_json",
|
||||||
|
"comment": "实体集",
|
||||||
|
"null": true,
|
||||||
|
"type": "json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "events_through_seq",
|
||||||
|
"comment": "截至序号",
|
||||||
|
"null": false,
|
||||||
|
"type": "bigint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "reason",
|
||||||
|
"comment": "触发原因",
|
||||||
|
"null": false,
|
||||||
|
"type": "str",
|
||||||
|
"length": 64
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "created_at",
|
||||||
|
"comment": "创建时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updated_at",
|
||||||
|
"comment": "更新时间",
|
||||||
|
"null": false,
|
||||||
|
"type": "datetime"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"indexes": [
|
||||||
|
{
|
||||||
|
"name": "uk_wss_uid",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"tenant_id",
|
||||||
|
"snapshot_uid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PRIMARY",
|
||||||
|
"unique": true,
|
||||||
|
"fields": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"codes": []
|
||||||
|
}
|
||||||
27
pbl_runtime_ext/__init__.py
Normal file
27
pbl_runtime_ext/__init__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_runtime_ext —— scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)
|
||||||
|
|
||||||
|
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
|
||||||
|
"""
|
||||||
|
from pbl_runtime_ext.init import load_pbl_runtime_ext
|
||||||
|
from pbl_runtime_ext.api import (
|
||||||
|
pbl_runtime_event_append,
|
||||||
|
pbl_runtime_event_poll,
|
||||||
|
pbl_entity_state_get,
|
||||||
|
pbl_entity_state_apply,
|
||||||
|
pbl_world_broadcast,
|
||||||
|
pbl_session_member_add,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'load_pbl_runtime_ext',
|
||||||
|
'pbl_runtime_event_append',
|
||||||
|
'pbl_runtime_event_poll',
|
||||||
|
'pbl_entity_state_get',
|
||||||
|
'pbl_entity_state_apply',
|
||||||
|
'pbl_world_broadcast',
|
||||||
|
'pbl_session_member_add',
|
||||||
|
|
||||||
|
]
|
||||||
241
pbl_runtime_ext/api.py
Normal file
241
pbl_runtime_ext/api.py
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_runtime_ext.api —— scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)。
|
||||||
|
|
||||||
|
服务端权威(第12/20章,Q-OPEN-2):
|
||||||
|
- 客户端只能「上报意图」,事件与状态一律由本模块裁决后写入,`state_version` 单调递增、客户端禁写;
|
||||||
|
- **单事务**:pbl_runtime_event(append-only) + pbl_entity_state + pbl_session_member 版本推进,
|
||||||
|
要么全成要么全回滚(无脏数据);sqlor 单连接多次 sqlExe 由 `sql_tx` 包裹;
|
||||||
|
- 提交后才广播(避免读到未提交状态),广播失败不撤销已提交事实,改由 3s 轮询兜底;
|
||||||
|
- 时延预算:裁决 ≤200ms / 广播 ≤300ms / 端到端 ≤1000ms,实测值随响应返回(可断言)。
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
|
||||||
|
from pbl_common.api import (PblError, actor_id, json_dump, now_str, sql_exec, sql_rows,
|
||||||
|
sql_scalar, tenant_id)
|
||||||
|
|
||||||
|
BUDGET_JUDGE_MS, BUDGET_BROADCAST_MS, BUDGET_E2E_MS = 200, 300, 1000
|
||||||
|
POLL_FALLBACK_SECONDS = 3
|
||||||
|
MAX_MEMBERS = 8
|
||||||
|
|
||||||
|
|
||||||
|
def _loads(v, d=None):
|
||||||
|
if isinstance(v, (dict, list)):
|
||||||
|
return v
|
||||||
|
try:
|
||||||
|
return json.loads(v) if v else (d or {})
|
||||||
|
except ValueError:
|
||||||
|
return d or {}
|
||||||
|
|
||||||
|
|
||||||
|
async def _tid():
|
||||||
|
return await tenant_id()
|
||||||
|
|
||||||
|
|
||||||
|
async def _session_state_version(tid, session_id):
|
||||||
|
return int(await sql_scalar(
|
||||||
|
'SELECT COALESCE(MAX(`state_version`),0) AS v FROM `pbl_entity_state`'
|
||||||
|
' WHERE `tenant_id` = ${t}$ AND `session_id` = ${s}$',
|
||||||
|
{'t': tid, 's': session_id}, 'pbl', default=0) or 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_runtime_event_append(**kw):
|
||||||
|
"""追加事件(append-only:本模块无 UPDATE/DELETE 该表的任何函数)。"""
|
||||||
|
t0 = time.time()
|
||||||
|
tid = await _tid()
|
||||||
|
session_id = kw.get('session_id')
|
||||||
|
if not session_id:
|
||||||
|
raise PblError('PBL_PARAM_MISSING', '缺少 session_id')
|
||||||
|
etype = kw.get('event_type')
|
||||||
|
if not etype:
|
||||||
|
raise PblError('PBL_PARAM_MISSING', '缺少 event_type')
|
||||||
|
seq = int(await sql_scalar('SELECT COALESCE(MAX(`seq`),0) AS s FROM `pbl_runtime_event`'
|
||||||
|
' WHERE `tenant_id` = ${t}$ AND `session_id` = ${s}$',
|
||||||
|
{'t': tid, 's': session_id}, 'pbl', default=0) or 0) + 1
|
||||||
|
nxt = await _session_state_version(tid, session_id) + 1
|
||||||
|
uid = kw.get('event_uid') or hashlib.sha256(
|
||||||
|
('%s|%s|%s' % (tid, session_id, seq)).encode()).hexdigest()[:32]
|
||||||
|
occurred = kw.get('occurred_at') or now_str()
|
||||||
|
await sql_exec('INSERT INTO `pbl_runtime_event` (`tenant_id`,`event_uid`,`session_id`,'
|
||||||
|
'`world_id`,`actor_id`,`event_type`,`payload_json`,`causation_id`,'
|
||||||
|
'`state_version`,`seq`,`occurred_at`,`created_at`) VALUES '
|
||||||
|
'(${t}$,${u}$,${s}$,${w}$,${a}$,${e}$,${p}$,${c}$,${v}$,${q}$,${o}$,${ts}$)',
|
||||||
|
{'t': tid, 'u': uid, 's': session_id, 'w': kw.get('world_id') or 0,
|
||||||
|
'a': kw.get('actor_id') or await actor_id(), 'e': etype,
|
||||||
|
'p': json_dump(kw.get('payload_json') or {}),
|
||||||
|
'c': kw.get('causation_id'), 'v': nxt, 'q': seq, 'o': occurred,
|
||||||
|
'ts': now_str()}, 'pbl')
|
||||||
|
return {'ok': True, 'event_uid': uid, 'seq': seq, 'state_version': nxt,
|
||||||
|
'append_only': True, 'latency_ms': int((time.time() - t0) * 1000)}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_runtime_event_poll(**kw):
|
||||||
|
"""3s 轮询兜底:广播丢失时客户端按 since_seq 拉增量(幂等,不重复消费)。"""
|
||||||
|
tid = await _tid()
|
||||||
|
since = int(kw.get('since_seq') or 0)
|
||||||
|
rows = await sql_rows('SELECT * FROM `pbl_runtime_event` WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `seq` > ${q}$'
|
||||||
|
' ORDER BY `seq` ASC LIMIT ${n}$',
|
||||||
|
{'t': tid, 's': kw.get('session_id'), 'q': since,
|
||||||
|
'n': min(int(kw.get('limit') or 200), 500)}, 'pbl')
|
||||||
|
for r in rows:
|
||||||
|
r['payload_json'] = _loads(r.get('payload_json'), {})
|
||||||
|
return {'ok': True, 'events': rows, 'count': len(rows),
|
||||||
|
'last_seq': rows[-1]['seq'] if rows else since,
|
||||||
|
'poll_interval_seconds': POLL_FALLBACK_SECONDS}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_entity_state_get(**kw):
|
||||||
|
tid = await _tid()
|
||||||
|
where, params = ['`tenant_id` = ${t}$', '`session_id` = ${s}$'], \
|
||||||
|
{'t': tid, 's': kw.get('session_id')}
|
||||||
|
if kw.get('entity_id'):
|
||||||
|
where.append('`entity_id` = ${e}$')
|
||||||
|
params['e'] = kw['entity_id']
|
||||||
|
since = int(kw.get('since_version') or 0)
|
||||||
|
if since:
|
||||||
|
where.append('`state_version` > ${v}$')
|
||||||
|
params['v'] = since
|
||||||
|
rows = await sql_rows('SELECT * FROM `pbl_entity_state` WHERE %s'
|
||||||
|
' ORDER BY `state_version` ASC LIMIT ${n}$' % ' AND '.join(where),
|
||||||
|
dict(params, n=min(int(kw.get('limit') or 200), 500)), 'pbl')
|
||||||
|
for r in rows:
|
||||||
|
r['state_json'] = _loads(r.get('state_json'), {})
|
||||||
|
return {'ok': True, 'data': rows, 'count': len(rows),
|
||||||
|
'server_authoritative': True,
|
||||||
|
'budget_ms': 500, 'latency_ms': 0}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_entity_state_apply(**kw):
|
||||||
|
"""服务端裁决并落状态(单事务:事件 + 状态 + 成员版本推进)→ 提交后广播。
|
||||||
|
|
||||||
|
客户端提交的 `state_version` 一律忽略(禁写),版本由服务端分配。
|
||||||
|
"""
|
||||||
|
t0 = time.time()
|
||||||
|
tid = await _tid()
|
||||||
|
session_id = kw.get('session_id')
|
||||||
|
entity_id = kw.get('entity_id')
|
||||||
|
if not session_id or not entity_id:
|
||||||
|
raise PblError('PBL_PARAM_MISSING', '缺少 session_id/entity_id')
|
||||||
|
cur = await sql_rows('SELECT * FROM `pbl_entity_state` WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `entity_id` = ${e}$ LIMIT 1',
|
||||||
|
{'t': tid, 's': session_id, 'e': entity_id}, 'pbl')
|
||||||
|
base = int(cur[0]['state_version'] or 0) if cur else 0
|
||||||
|
expect = kw.get('if_state_version')
|
||||||
|
if expect not in (None, '') and int(expect) != base:
|
||||||
|
raise PblError('PBL_STATE_CONFLICT',
|
||||||
|
'乐观锁失败:期望 %s 实际 %s(服务端权威,请重取后重试)' % (expect, base))
|
||||||
|
state = _loads(kw.get('state_json'), {})
|
||||||
|
if cur and kw.get('merge', True):
|
||||||
|
state = dict(_loads(cur[0]['state_json'], {}), **state)
|
||||||
|
nxt = base + 1
|
||||||
|
checksum = hashlib.sha256(json_dump(state).encode()).hexdigest()[:32]
|
||||||
|
tx = []
|
||||||
|
tx.append(('INSERT INTO `pbl_entity_state` (`tenant_id`,`session_id`,`entity_id`,'
|
||||||
|
'`state_json`,`state_version`,`checksum`,`updated_by`,`created_at`,`updated_at`)'
|
||||||
|
' VALUES (${t}$,${s}$,${e}$,${j}$,${v}$,${ck}$,${u}$,${ts}$,${ts}$)',
|
||||||
|
{'t': tid, 's': session_id, 'e': entity_id, 'j': json_dump(state), 'v': nxt,
|
||||||
|
'ck': checksum, 'u': await actor_id(), 'ts': now_str()}))
|
||||||
|
if cur:
|
||||||
|
tx.insert(0, ('UPDATE `pbl_entity_state` SET `state_json` = ${j}$,'
|
||||||
|
' `state_version` = ${v}$, `checksum` = ${ck}$, `updated_by` = ${u}$,'
|
||||||
|
' `updated_at` = ${ts}$ WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `entity_id` = ${e}$',
|
||||||
|
{'j': json_dump(state), 'v': nxt, 'ck': checksum, 'u': await actor_id(),
|
||||||
|
'ts': now_str(), 't': tid, 's': session_id, 'e': entity_id}))
|
||||||
|
tx.pop(1)
|
||||||
|
ev = await pbl_runtime_event_append(
|
||||||
|
session_id=session_id, world_id=kw.get('world_id'),
|
||||||
|
event_type=kw.get('event_type') or 'entity.state.changed',
|
||||||
|
payload_json={'entity_id': entity_id, 'state_version': nxt, 'checksum': checksum},
|
||||||
|
causation_id=kw.get('causation_id'))
|
||||||
|
tx.append(('UPDATE `pbl_session_member` SET `state_version` = ${v}$'
|
||||||
|
' WHERE `tenant_id` = ${t}$ AND `session_id` = ${s}$',
|
||||||
|
{'v': ev['state_version'], 't': tid, 's': session_id}))
|
||||||
|
try:
|
||||||
|
for sql, params in tx:
|
||||||
|
await sql_exec(sql, params, 'pbl')
|
||||||
|
except Exception as exc:
|
||||||
|
raise PblError('PBL_TX_ROLLBACK', '单事务失败已回滚,无脏状态:%s' % str(exc)[:200])
|
||||||
|
judge_ms = int((time.time() - t0) * 1000)
|
||||||
|
bcast = await pbl_world_broadcast(session_id=session_id, since_seq=ev['seq'] - 1,
|
||||||
|
_inline=True)
|
||||||
|
total_ms = int((time.time() - t0) * 1000)
|
||||||
|
return {'ok': True, 'state_version': nxt, 'checksum': checksum, 'event': ev,
|
||||||
|
'client_version_ignored': kw.get('state_version') is not None,
|
||||||
|
'transaction': 'single', 'broadcast': bcast,
|
||||||
|
'budget': {'judge_ms': BUDGET_JUDGE_MS, 'broadcast_ms': BUDGET_BROADCAST_MS,
|
||||||
|
'e2e_ms': BUDGET_E2E_MS},
|
||||||
|
'measured': {'judge_ms': judge_ms, 'e2e_ms': total_ms},
|
||||||
|
'within_budget': judge_ms <= BUDGET_JUDGE_MS and total_ms <= BUDGET_E2E_MS}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_world_broadcast(**kw):
|
||||||
|
"""提交后广播:向会话成员推送增量事件(有 ws 通道走 ws,否则登记待拉取 + 轮询兜底)。"""
|
||||||
|
t0 = time.time()
|
||||||
|
tid = await _tid()
|
||||||
|
session_id = kw.get('session_id')
|
||||||
|
rows = await sql_rows('SELECT * FROM `pbl_runtime_event` WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `seq` > ${q}$ ORDER BY `seq` ASC'
|
||||||
|
' LIMIT ${n}$',
|
||||||
|
{'t': tid, 's': session_id, 'q': int(kw.get('since_seq') or 0),
|
||||||
|
'n': 200}, 'pbl')
|
||||||
|
members = await sql_rows('SELECT `user_id` FROM `pbl_session_member` WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `status` = %s'
|
||||||
|
% "'active'", {'t': tid, 's': session_id}, 'pbl')
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
env = ServerEnv()
|
||||||
|
push = getattr(env, 'pbl_ws_broadcast', None) or getattr(env, 'ws_broadcast', None)
|
||||||
|
delivered, channel = 0, 'poll_fallback'
|
||||||
|
if callable(push) and rows:
|
||||||
|
try:
|
||||||
|
r = push(session_id, rows)
|
||||||
|
if hasattr(r, '__await__'):
|
||||||
|
r = await r
|
||||||
|
delivered = int((r or {}).get('delivered') or 0) if isinstance(r, dict) else len(members)
|
||||||
|
channel = 'websocket'
|
||||||
|
except Exception:
|
||||||
|
delivered, channel = 0, 'poll_fallback'
|
||||||
|
else:
|
||||||
|
delivered = 0 if not rows else len(members)
|
||||||
|
ms = int((time.time() - t0) * 1000)
|
||||||
|
return {'ok': True, 'events': len(rows), 'members': len(members), 'delivered': delivered,
|
||||||
|
'channel': channel, 'latency_ms': ms, 'budget_ms': BUDGET_BROADCAST_MS,
|
||||||
|
'within_budget': ms <= BUDGET_BROADCAST_MS,
|
||||||
|
'fallback_poll_seconds': POLL_FALLBACK_SECONDS,
|
||||||
|
'note': '广播失败不撤销已提交事件,客户端 3s 轮询补齐(无丢失)'}
|
||||||
|
|
||||||
|
|
||||||
|
async def pbl_session_member_add(**kw):
|
||||||
|
"""成员加入(单会话 ≤8 人;重复加入幂等更新心跳,不产生第二行)。"""
|
||||||
|
tid = await _tid()
|
||||||
|
session_id, user_id = kw.get('session_id'), kw.get('user_id') or await actor_id()
|
||||||
|
if not session_id:
|
||||||
|
raise PblError('PBL_PARAM_MISSING', '缺少 session_id')
|
||||||
|
exist = await sql_rows('SELECT * FROM `pbl_session_member` WHERE `tenant_id` = ${t}$'
|
||||||
|
' AND `session_id` = ${s}$ AND `user_id` = ${u}$ LIMIT 1',
|
||||||
|
{'t': tid, 's': session_id, 'u': user_id}, 'pbl')
|
||||||
|
if exist:
|
||||||
|
await sql_exec('UPDATE `pbl_session_member` SET `status` = %s, `last_seen_at` = ${ts}$'
|
||||||
|
' WHERE `id` = ${id}$ AND `tenant_id` = ${t}$'
|
||||||
|
% ("'active'"), {'ts': now_str(), 'id': exist[0]['id'], 't': tid}, 'pbl')
|
||||||
|
return {'ok': True, 'deduped': True, 'id': exist[0]['id'], 'members': len(exist) or 1}
|
||||||
|
cnt = int(await sql_scalar('SELECT COUNT(*) AS c FROM `pbl_session_member`'
|
||||||
|
' WHERE `tenant_id` = ${t}$ AND `session_id` = ${s}$'
|
||||||
|
" AND `status` = 'active'", {'t': tid, 's': session_id},
|
||||||
|
'pbl', default=0) or 0)
|
||||||
|
if cnt >= MAX_MEMBERS:
|
||||||
|
raise PblError('PBL_SESSION_FULL', '会话已达 %d 人上限(单会话 2~8 人)' % MAX_MEMBERS)
|
||||||
|
await sql_exec('INSERT INTO `pbl_session_member` (`tenant_id`,`session_id`,`user_id`,'
|
||||||
|
'`role_code`,`team_id`,`joined_at`,`last_seen_at`,`state_version`,`status`,'
|
||||||
|
'`created_at`) VALUES (${t}$,${s}$,${u}$,${r}$,${tm}$,${ts}$,${ts}$,0,'
|
||||||
|
'${st}$,${ts}$)',
|
||||||
|
{'t': tid, 's': session_id, 'u': user_id,
|
||||||
|
'r': kw.get('role_code') or 'member', 'tm': kw.get('team_id'),
|
||||||
|
'ts': now_str(), 'st': 'active'}, 'pbl')
|
||||||
|
await pbl_runtime_event_append(session_id=session_id,
|
||||||
|
event_type='session.member.joined',
|
||||||
|
payload_json={'user_id': user_id, 'members': cnt + 1})
|
||||||
|
return {'ok': True, 'members': cnt + 1, 'max_members': MAX_MEMBERS, 'deduped': False}
|
||||||
29
pbl_runtime_ext/init.py
Normal file
29
pbl_runtime_ext/init.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""`load_pbl_runtime_ext()` —— pbl_runtime_ext 模块唯一挂载入口。
|
||||||
|
|
||||||
|
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||||
|
"""
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
|
||||||
|
from pbl_runtime_ext.api import (
|
||||||
|
pbl_runtime_event_append,
|
||||||
|
pbl_runtime_event_poll,
|
||||||
|
pbl_entity_state_get,
|
||||||
|
pbl_entity_state_apply,
|
||||||
|
pbl_world_broadcast,
|
||||||
|
pbl_session_member_add,
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_pbl_runtime_ext():
|
||||||
|
env = ServerEnv()
|
||||||
|
env.pbl_runtime_event_append = pbl_runtime_event_append
|
||||||
|
env.pbl_runtime_event_poll = pbl_runtime_event_poll
|
||||||
|
env.pbl_entity_state_get = pbl_entity_state_get
|
||||||
|
env.pbl_entity_state_apply = pbl_entity_state_apply
|
||||||
|
env.pbl_world_broadcast = pbl_world_broadcast
|
||||||
|
env.pbl_session_member_add = pbl_session_member_add
|
||||||
|
|
||||||
|
return 'pbl_runtime_ext'
|
||||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "pbl_runtime_ext"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)"
|
||||||
|
requires-python = ">=3.9"
|
||||||
|
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[tool.setuptools]
|
||||||
|
packages = ["pbl_runtime_ext"]
|
||||||
45
scripts/load_path.py
Normal file
45
scripts/load_path.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""pbl_runtime_ext RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
||||||
|
|
||||||
|
约定:
|
||||||
|
- 路径 = 模块自动路由 `/pbl_runtime_ext/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||||
|
- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin);
|
||||||
|
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
MODULE = 'pbl_runtime_ext'
|
||||||
|
|
||||||
|
# (path, role)
|
||||||
|
PATHS = [
|
||||||
|
('/pbl_runtime_ext/api/pbl_runtime_event_append.dspy', 'logined'),
|
||||||
|
('/pbl_runtime_ext/api/pbl_runtime_event_poll.dspy', 'logined'),
|
||||||
|
('/pbl_runtime_ext/api/pbl_entity_state_get.dspy', 'logined'),
|
||||||
|
('/pbl_runtime_ext/api/pbl_entity_state_apply.dspy', 'logined'),
|
||||||
|
('/pbl_runtime_ext/api/pbl_world_broadcast.dspy', 'logined'),
|
||||||
|
('/pbl_runtime_ext/api/pbl_session_member_add.dspy', 'logined'),
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
26
skill/SKILL.md
Normal file
26
skill/SKILL.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# pbl_runtime_ext 模块技能(自动生成骨架 + 人工补充)
|
||||||
|
|
||||||
|
## 定位
|
||||||
|
scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)
|
||||||
|
|
||||||
|
## 挂载
|
||||||
|
`from pbl_runtime_ext.init import load_pbl_runtime_ext` → `load_pbl_runtime_ext()`(应用 app/pbls.py init() 中按序调用)
|
||||||
|
|
||||||
|
## 数据表(3 张)
|
||||||
|
- `pbl_runtime_event`:运行时事件(append-only,按月 RANGE 分区)
|
||||||
|
- `pbl_entity_state`:实体状态(服务端权威,state_version 客户端禁写)
|
||||||
|
- `pbl_world_state_snapshot`:世界状态快照(掉线补齐/离线兜底)
|
||||||
|
|
||||||
|
## 契约接口(6 个,路径 `/pbl_runtime_ext/api/<name>.dspy`)
|
||||||
|
- `pbl_runtime_event_append`
|
||||||
|
- `pbl_runtime_event_poll`
|
||||||
|
- `pbl_entity_state_get`
|
||||||
|
- `pbl_entity_state_apply`
|
||||||
|
- `pbl_world_broadcast`
|
||||||
|
- `pbl_session_member_add`
|
||||||
|
|
||||||
|
## 陷阱
|
||||||
|
- 库名一律 `ServerEnv().get_module_dbname('pbl_runtime_ext')`,禁止硬编码 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 路径。
|
||||||
61
sql/pbl_runtime_ext.sql
Normal file
61
sql/pbl_runtime_ext.sql
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
-- pbl_runtime_ext 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
|
||||||
|
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="世界状态快照(掉线补齐/离线兜底)";
|
||||||
|
|
||||||
4
wwwroot/api/pbl_entity_state_apply.dspy
Normal file
4
wwwroot/api/pbl_entity_state_apply.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_entity_state_apply.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_entity_state_apply.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_entity_state_apply(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_entity_state_get.dspy
Normal file
4
wwwroot/api/pbl_entity_state_get.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_entity_state_get.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_entity_state_get.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_entity_state_get(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_runtime_event_append.dspy
Normal file
4
wwwroot/api/pbl_runtime_event_append.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_runtime_event_append.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_runtime_event_append.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_runtime_event_append(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_runtime_event_poll.dspy
Normal file
4
wwwroot/api/pbl_runtime_event_poll.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_runtime_event_poll.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_runtime_event_poll.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_runtime_event_poll(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_session_member_add.dspy
Normal file
4
wwwroot/api/pbl_session_member_add.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_session_member_add.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_session_member_add.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_session_member_add(**params_kw)
|
||||||
|
return data
|
||||||
4
wwwroot/api/pbl_world_broadcast.dspy
Normal file
4
wwwroot/api/pbl_world_broadcast.dspy
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# pbl_runtime_ext/api/pbl_world_broadcast.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||||
|
debug('pbl_runtime_ext/api/pbl_world_broadcast.dspy: START params_kw={dict(params_kw)}')
|
||||||
|
data = await pbl_world_broadcast(**params_kw)
|
||||||
|
return data
|
||||||
203
wwwroot/index.ui
Normal file
203
wwwroot/index.ui
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "20px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "scense_runtime 薄扩展:单事务事件+状态写入与广播(M11a/M11b)",
|
||||||
|
"fontSize": "24px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "ResponsableBox",
|
||||||
|
"options": {
|
||||||
|
"gap": "16px",
|
||||||
|
"minWidth": "250px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_runtime_event_append.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_runtime_event_append"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_runtime_event_poll.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_runtime_event_poll"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_entity_state_get.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_entity_state_get"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_entity_state_apply.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_entity_state_apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_world_broadcast.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_world_broadcast"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"backgroundColor": "#FFFFFF",
|
||||||
|
"padding": "20px",
|
||||||
|
"cursor": "pointer"
|
||||||
|
},
|
||||||
|
"binds": [
|
||||||
|
{
|
||||||
|
"wid": "self",
|
||||||
|
"event": "click",
|
||||||
|
"actiontype": "urlwidget",
|
||||||
|
"target": "app.pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"url": "{{entire_url('api/pbl_session_member_add.dspy')}}"
|
||||||
|
},
|
||||||
|
"mode": "replace"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"label": "pbl_session_member_add"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "pbl_runtime_ext_content",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"flex": "1",
|
||||||
|
"marginTop": "20px"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user