feat(pbl_appcodes): 初始提交 - pbls 项目模块代码入库

This commit is contained in:
pbls-bot 2026-09-15 17:39:11 +08:00 committed by agent.develop
parent 73403b155f
commit c08ab6a278
11 changed files with 348 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
__pycache__/
*.pyc
*.pyo
.pytest_cache/
.coverage
.DS_Store
*.log
node_modules/
dist/
build/
.venv/
venv/

53
init/data.json Normal file
View File

@ -0,0 +1,53 @@
{
"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"
]
}
}

19
pbl_appcodes/__init__.py Normal file
View File

@ -0,0 +1,19 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_appcodes —— PBL 枚举编码注入6+2 组,幂等,零表)
注册三处同步之 必须导出 init.py 里的全部契约函数漏一行 .dspy 调用即 NameError
"""
from pbl_appcodes.init import load_pbl_appcodes
from pbl_appcodes.api import (
pbl_appcodes_inject,
pbl_appcodes_list,
)
__all__ = [
'load_pbl_appcodes',
'pbl_appcodes_inject',
'pbl_appcodes_list',
]

70
pbl_appcodes/api.py Normal file
View File

@ -0,0 +1,70 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_appcodes.api —— 枚举 6+2 组幂等注入DDL 零 ENUM、零 INSERT编码集中于此
事实源modules/pbl_appcodes/init/data.json apps/pbls/scripts/gen_artifacts.py 同源生成
幂等 (tenant_id, appcode, codename) 已存在即跳过重复执行不产生重复行
G6 范围纪律本模块只写 appcodes 不写 pbl_kdb_item / research 任何表
"""
import json
import os
from pbl_common.api import PblError, dbname, now_str, sql_exec, sql_rows, tenant_id
SEED_RELPATH = os.path.join('init', 'data.json')
def _seed_path():
here = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # modules/pbl_appcodes
return os.path.join(here, SEED_RELPATH)
def load_seed():
p = _seed_path()
if not os.path.exists(p):
raise PblError('PBL_APPCODES_SEED_MISSING', '种子文件缺失:%s' % p)
with open(p, encoding='utf-8') as f:
return json.load(f)['appcodes']
async def pbl_appcodes_inject(**kw):
"""幂等注入 6+2 组枚举。返回 {groups, inserted, skipped}。"""
groups = load_seed()
only = kw.get('appcode')
if only:
groups = {only: groups.get(only) or []}
if not groups[only]:
raise PblError('PBL_APPCODE_UNKNOWN', '未知枚举组 %s' % only)
tid = await tenant_id(required=False)
inserted = skipped = 0
for code, names in groups.items():
for i, name in enumerate(names):
exist = await sql_rows(
'SELECT `id` FROM `appcodes` WHERE `appcode` = ${a}$ AND `codename` = ${c}$'
' AND `tenant_id` = ${t}$ LIMIT 1',
{'a': code, 'c': name, 't': tid}, 'pbl')
if exist:
skipped += 1
continue
await sql_exec(
'INSERT INTO `appcodes` (`tenant_id`,`appcode`,`codename`,`codename_cn`,'
'`order_no`,`is_enabled`,`created_at`) '
'VALUES (${t}$,${a}$,${c}$,${n}$,${o}$,1,${ts}$)',
{'t': tid, 'a': code, 'c': name, 'n': name, 'o': (i + 1) * 10,
'ts': now_str()}, 'pbl')
inserted += 1
return {'ok': True, 'groups': len(groups), 'inserted': inserted, 'skipped': skipped,
'keys': sorted(groups)}
async def pbl_appcodes_list(**kw):
"""读取枚举组(供前端下拉/校验白名单,只读)。"""
tid = await tenant_id(required=False)
sql = ('SELECT `appcode`,`codename`,`order_no` FROM `appcodes` WHERE `tenant_id` = ${t}$'
+ (' AND `appcode` = ${a}$' if kw.get('appcode') else '')
+ ' ORDER BY `appcode`,`order_no`')
rows = await sql_rows(sql, {'t': tid, 'a': kw.get('appcode')}, 'pbl')
out = {}
for r in rows:
out.setdefault(r['appcode'], []).append(r['codename'])
return {'ok': True, 'groups': out, 'counts': {k: len(v) for k, v in out.items()}}

21
pbl_appcodes/init.py Normal file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""`load_pbl_appcodes()` —— pbl_appcodes 模块唯一挂载入口。
注册三处同步之 env.<契约名> = <契约名> 定义在 api.py 导出在 __init__.py
"""
from ahserver.serverenv import ServerEnv
from pbl_appcodes.api import (
pbl_appcodes_inject,
pbl_appcodes_list,
)
def load_pbl_appcodes():
env = ServerEnv()
env.pbl_appcodes_inject = pbl_appcodes_inject
env.pbl_appcodes_list = pbl_appcodes_list
return 'pbl_appcodes'

13
pyproject.toml Normal file
View File

@ -0,0 +1,13 @@
[project]
name = "pbl_appcodes"
version = "0.1.0"
description = "PBL 枚举编码注入6+2 组,幂等,零表)"
requires-python = ">=3.9"
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
[build-system]
requires = ["setuptools>=61"]
build-backend = "setuptools.build_meta"
[tool.setuptools]
packages = ["pbl_appcodes"]

41
scripts/load_path.py Normal file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""pbl_appcodes RBAC 路径注册(硬门禁 6.6 / QC #11
约定
- 路径 = 模块自动路由 `/pbl_appcodes/api/<契约>.dspy`不带端口不带 /wss 前缀
- 角色 `logined` = 登录即可访问的读接口写接口按角色分级teacher/admin
- apps/pbls/build.sh 8 步调用 `register()`rbac CLI 不在位时打印清单不静默跳过
"""
import os
import subprocess
import sys
MODULE = 'pbl_appcodes'
# (path, role)
PATHS = [
('/pbl_appcodes/api/pbl_appcodes_inject.dspy', 'logined'),
('/pbl_appcodes/api/pbl_appcodes_list.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)

20
skill/SKILL.md Normal file
View File

@ -0,0 +1,20 @@
# pbl_appcodes 模块技能(自动生成骨架 + 人工补充)
## 定位
PBL 枚举编码注入6+2 组,幂等,零表)
## 挂载
`from pbl_appcodes.init import load_pbl_appcodes``load_pbl_appcodes()`(应用 app/pbls.py init() 中按序调用)
## 数据表0 张)
- 无(零表模块)
## 契约接口2 个,路径 `/pbl_appcodes/api/<name>.dspy`
- `pbl_appcodes_inject`
- `pbl_appcodes_list`
## 陷阱
- 库名一律 `ServerEnv().get_module_dbname('pbl_appcodes')`,禁止硬编码 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 路径。

View File

@ -0,0 +1,4 @@
# pbl_appcodes/api/pbl_appcodes_inject.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_appcodes/api/pbl_appcodes_inject.dspy: START params_kw={dict(params_kw)}')
data = await pbl_appcodes_inject(**params_kw)
return data

View File

@ -0,0 +1,4 @@
# pbl_appcodes/api/pbl_appcodes_list.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py
debug('pbl_appcodes/api/pbl_appcodes_list.dspy: START params_kw={dict(params_kw)}')
data = await pbl_appcodes_list(**params_kw)
return data

91
wwwroot/index.ui Normal file
View File

@ -0,0 +1,91 @@
{
"widgettype": "VBox",
"options": {
"width": "100%",
"height": "100%",
"padding": "20px"
},
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "PBL 枚举编码注入6+2 组,幂等,零表)",
"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_appcodes_content",
"options": {
"url": "{{entire_url('api/pbl_appcodes_inject.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_appcodes_inject"
}
}
]
},
{
"widgettype": "VBox",
"options": {
"backgroundColor": "#FFFFFF",
"padding": "20px",
"cursor": "pointer"
},
"binds": [
{
"wid": "self",
"event": "click",
"actiontype": "urlwidget",
"target": "app.pbl_appcodes_content",
"options": {
"url": "{{entire_url('api/pbl_appcodes_list.dspy')}}"
},
"mode": "replace"
}
],
"subwidgets": [
{
"widgettype": "Text",
"options": {
"label": "pbl_appcodes_list"
}
}
]
}
]
},
{
"widgettype": "VBox",
"id": "pbl_appcodes_content",
"options": {
"width": "100%",
"flex": "1",
"marginTop": "20px"
}
}
]
}