feat(pbl_kdb_ext): 初始提交 - pbls 项目模块代码入库
This commit is contained in:
parent
324a741625
commit
495a84642f
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/
|
||||
160
json/kdb_ext_pbl_kdb_item.json
Normal file
160
json/kdb_ext_pbl_kdb_item.json
Normal file
@ -0,0 +1,160 @@
|
||||
{
|
||||
"tblname": "pbl_kdb_item",
|
||||
"alias": "pbl_kdb_ext",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "条目编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"item_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"summary_txt": {
|
||||
"label": "摘要",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "内容(无向量,Q-OPEN-5)",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"source": {
|
||||
"label": "来源",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"is_write_locked": {
|
||||
"label": "写锁(恒1)",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"code": {
|
||||
"label": "条目编码",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"item_type": {
|
||||
"label": "类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"summary_txt": {
|
||||
"label": "摘要",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "内容(无向量,Q-OPEN-5)",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"source": {
|
||||
"label": "来源",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"is_write_locked": {
|
||||
"label": "写锁(恒1)",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"code": {
|
||||
"label": "条目编码",
|
||||
"type": "select"
|
||||
},
|
||||
"item_type": {
|
||||
"label": "类型",
|
||||
"type": "select"
|
||||
},
|
||||
"title": {
|
||||
"label": "标题",
|
||||
"type": "select"
|
||||
},
|
||||
"summary_txt": {
|
||||
"label": "摘要",
|
||||
"type": "text"
|
||||
},
|
||||
"payload_json": {
|
||||
"label": "内容(无向量,Q-OPEN-5)",
|
||||
"type": "text"
|
||||
},
|
||||
"source": {
|
||||
"label": "来源",
|
||||
"type": "select"
|
||||
},
|
||||
"is_write_locked": {
|
||||
"label": "写锁(恒1)",
|
||||
"type": "checkbox"
|
||||
},
|
||||
"status": {
|
||||
"label": "状态",
|
||||
"type": "select"
|
||||
},
|
||||
"version_no": {
|
||||
"label": "版本",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
132
json/kdb_ext_pbl_kdb_query_log.json
Normal file
132
json/kdb_ext_pbl_kdb_query_log.json
Normal file
@ -0,0 +1,132 @@
|
||||
{
|
||||
"tblname": "pbl_kdb_query_log",
|
||||
"alias": "pbl_kdb_ext",
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"query_uid": {
|
||||
"label": "查询UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"caller_type": {
|
||||
"label": "调用方类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"caller_id": {
|
||||
"label": "调用方",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"query_json": {
|
||||
"label": "查询条件",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"hit_count": {
|
||||
"label": "命中数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"browserfields": {
|
||||
"id": {
|
||||
"label": "ID",
|
||||
"list": true,
|
||||
"type": "number"
|
||||
},
|
||||
"tenant_id": {
|
||||
"label": "租户ID",
|
||||
"list": true,
|
||||
"type": "text"
|
||||
},
|
||||
"query_uid": {
|
||||
"label": "查询UID",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"caller_type": {
|
||||
"label": "调用方类型",
|
||||
"type": "select",
|
||||
"list": true
|
||||
},
|
||||
"caller_id": {
|
||||
"label": "调用方",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"query_json": {
|
||||
"label": "查询条件",
|
||||
"type": "text",
|
||||
"list": true
|
||||
},
|
||||
"hit_count": {
|
||||
"label": "命中数",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number",
|
||||
"list": true
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox",
|
||||
"list": true
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"query_uid": {
|
||||
"label": "查询UID",
|
||||
"type": "text"
|
||||
},
|
||||
"caller_type": {
|
||||
"label": "调用方类型",
|
||||
"type": "select"
|
||||
},
|
||||
"caller_id": {
|
||||
"label": "调用方",
|
||||
"type": "text"
|
||||
},
|
||||
"query_json": {
|
||||
"label": "查询条件",
|
||||
"type": "text"
|
||||
},
|
||||
"hit_count": {
|
||||
"label": "命中数",
|
||||
"type": "number"
|
||||
},
|
||||
"latency_ms": {
|
||||
"label": "时延",
|
||||
"type": "number"
|
||||
},
|
||||
"ok": {
|
||||
"label": "成功",
|
||||
"type": "checkbox"
|
||||
}
|
||||
},
|
||||
"order_by": "id DESC",
|
||||
"page_size": 20
|
||||
}
|
||||
}
|
||||
119
models/pbl_kdb_item.json
Normal file
119
models/pbl_kdb_item.json
Normal file
@ -0,0 +1,119 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_kdb_item",
|
||||
"comment": "KDB 条目(建表不写入,is_write_locked 恒 1)",
|
||||
"module": "pbl_kdb_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": "code",
|
||||
"comment": "条目编码",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "item_type",
|
||||
"comment": "类型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"comment": "标题",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 128
|
||||
},
|
||||
{
|
||||
"name": "summary_txt",
|
||||
"comment": "摘要",
|
||||
"null": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "payload_json",
|
||||
"comment": "内容(无向量,Q-OPEN-5)",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"comment": "来源",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "is_write_locked",
|
||||
"comment": "写锁(恒1)",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"comment": "状态",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "version_no",
|
||||
"comment": "版本",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_kdb_code",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
105
models/pbl_kdb_query_log.json
Normal file
105
models/pbl_kdb_query_log.json
Normal file
@ -0,0 +1,105 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "pbl_kdb_query_log",
|
||||
"comment": "KDB 检索日志(只读侧留痕)",
|
||||
"module": "pbl_kdb_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": "query_uid",
|
||||
"comment": "查询UID",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "caller_type",
|
||||
"comment": "调用方类型",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 64
|
||||
},
|
||||
{
|
||||
"name": "caller_id",
|
||||
"comment": "调用方",
|
||||
"null": false,
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "query_json",
|
||||
"comment": "查询条件",
|
||||
"null": true,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"name": "hit_count",
|
||||
"comment": "命中数",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "latency_ms",
|
||||
"comment": "时延",
|
||||
"null": false,
|
||||
"type": "int"
|
||||
},
|
||||
{
|
||||
"name": "ok",
|
||||
"comment": "成功",
|
||||
"null": false,
|
||||
"type": "bool"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"comment": "创建时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"comment": "更新时间",
|
||||
"null": false,
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "uk_kql_uid",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"tenant_id",
|
||||
"query_uid"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "PRIMARY",
|
||||
"unique": true,
|
||||
"fields": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
21
pbl_kdb_ext/__init__.py
Normal file
21
pbl_kdb_ext/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_kdb_ext —— KDB 只读桩与匿名聚合(M7,零写入)
|
||||
|
||||
注册三处同步之 ②:必须导出 init.py 里的全部契约函数,漏一行 .dspy 调用即 NameError。
|
||||
"""
|
||||
from pbl_kdb_ext.init import load_pbl_kdb_ext
|
||||
from pbl_kdb_ext.api import (
|
||||
pbl_kdb_search,
|
||||
pbl_kdb_get,
|
||||
pbl_research_aggregate,
|
||||
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'load_pbl_kdb_ext',
|
||||
'pbl_kdb_search',
|
||||
'pbl_kdb_get',
|
||||
'pbl_research_aggregate',
|
||||
|
||||
]
|
||||
98
pbl_kdb_ext/api.py
Normal file
98
pbl_kdb_ext/api.py
Normal file
@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_kdb_ext.api —— KDB 只读桩 + Research 匿名聚合(Q-OPEN-5/6,M7)。
|
||||
|
||||
G6 范围纪律(本模块零实现入口,可 grep 核对):
|
||||
- 无 kdb.write / kdb.add_candidate / kdb.propose_pattern / research.write / sim.* 任何函数;
|
||||
- `pbl_kdb_item.is_write_locked` 恒 1,本模块不提供任何 UPDATE/INSERT 该表的路径;
|
||||
- 不引入向量库/Embedding(桩:结构化字段等值 + LIKE 匹配);
|
||||
- research.aggregate 只读、匿名(≥k=5 最小分组,低于阈值返回 suppressed,不出个体)。
|
||||
"""
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
|
||||
from pbl_common.api import (PblError, actor_id, json_dump, now_str, sql_exec, sql_rows,
|
||||
tenant_id)
|
||||
|
||||
K_MIN_GROUP = 5 # 匿名聚合最小分组(隐私线)
|
||||
STUB_NOTE = 'kdb_stub: no vector store (Q-OPEN-5)'
|
||||
|
||||
|
||||
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 pbl_kdb_search(**kw):
|
||||
"""只读检索桩:按 item_type/subject/grade/关键词过滤,命中数与耗时留痕。"""
|
||||
t0 = time.time()
|
||||
tid = await _tid()
|
||||
where, params = ['`tenant_id` = ${t}$', '`is_write_locked` = 1', "`status` = 'published'"], \
|
||||
{'t': tid}
|
||||
for col in ('item_type', 'source'):
|
||||
if kw.get(col):
|
||||
where.append('`%s` = ${%s}$' % (col, col))
|
||||
params[col] = kw[col]
|
||||
if kw.get('keyword'):
|
||||
where.append('(`title` LIKE ${kw}$ OR `summary_txt` LIKE ${kw}$)')
|
||||
params['kw'] = '%%%s%%' % kw['keyword']
|
||||
limit = min(int(kw.get('limit') or 10), 50)
|
||||
rows = await sql_rows('SELECT `id`,`code`,`item_type`,`title`,`summary_txt`,`version_no`'
|
||||
' FROM `pbl_kdb_item` WHERE %s ORDER BY `id` LIMIT ${n}$'
|
||||
% ' AND '.join(where), dict(params, n=limit), 'pbl')
|
||||
await sql_exec('INSERT INTO `pbl_kdb_query_log` (`tenant_id`,`query_uid`,`caller_type`,'
|
||||
'`caller_id`,`query_json`,`hit_count`,`latency_ms`,`ok`,`created_at`)'
|
||||
' VALUES (${t}$,${q}$,${c}$,${i}$,${j}$,${h}$,${l}$,1,${ts}$)',
|
||||
{'t': tid, 'q': 'Q%s' % hashlib.sha256(
|
||||
(str(kw) + now_str()).encode()).hexdigest()[:20],
|
||||
'c': kw.get('caller_type') or 'agent', 'i': await actor_id(),
|
||||
'j': json_dump({k: v for k, v in kw.items() if k != 'caller_id'}),
|
||||
'h': len(rows), 'l': int((time.time() - t0) * 1000), 'ts': now_str()}, 'pbl')
|
||||
return {'ok': True, 'data': rows, 'hit_count': len(rows), 'stub': STUB_NOTE,
|
||||
'writable': False, 'latency_ms': int((time.time() - t0) * 1000)}
|
||||
|
||||
|
||||
async def pbl_kdb_get(**kw):
|
||||
tid = await _tid()
|
||||
rows = await sql_rows('SELECT * FROM `pbl_kdb_item` WHERE `tenant_id` = ${t}$'
|
||||
' AND (`id` = ${id}$ OR `code` = ${c}$) LIMIT 1',
|
||||
{'t': tid, 'id': kw.get('id') or 0, 'c': kw.get('code') or ''}, 'pbl')
|
||||
if not rows:
|
||||
raise PblError('PBL_KDB_NOT_FOUND', 'KDB 条目不存在(只读桩,无写入路径)')
|
||||
rows[0]['payload_json'] = _loads(rows[0].get('payload_json'), {})
|
||||
return {'ok': True, 'data': rows[0], 'is_write_locked': True, 'stub': STUB_NOTE}
|
||||
|
||||
|
||||
async def pbl_research_aggregate(**kw):
|
||||
"""匿名聚合:按维度分组统计评估分数/证据数,分组样本 < K_MIN_GROUP 直接抑制。"""
|
||||
tid = await _tid()
|
||||
dim = kw.get('group_by') or 'band'
|
||||
if dim not in ('band', 'assessor_type', 'blueprint_id'):
|
||||
raise PblError('PBL_AGG_DIM_INVALID', 'group_by 仅支持 band/assessor_type/blueprint_id')
|
||||
rows = await sql_rows(
|
||||
'SELECT `%s` AS g, COUNT(*) AS n, AVG(`total_score`) AS avg_score,'
|
||||
' MIN(`total_score`) AS min_score, MAX(`total_score`) AS max_score'
|
||||
' FROM `pbl_assessment_record` WHERE `tenant_id` = ${t}$ GROUP BY `%s`' % (dim, dim),
|
||||
{'t': tid}, 'pbl')
|
||||
groups, suppressed = [], 0
|
||||
for r in rows:
|
||||
if int(r['n'] or 0) < K_MIN_GROUP:
|
||||
suppressed += 1
|
||||
continue
|
||||
groups.append({'group': r['g'], 'n': int(r['n']),
|
||||
'avg_score': round(float(r['avg_score'] or 0), 2),
|
||||
'min_score': float(r['min_score'] or 0),
|
||||
'max_score': float(r['max_score'] or 0)})
|
||||
return {'ok': True, 'dimension': dim, 'groups': groups,
|
||||
'suppressed_groups': suppressed, 'k_min': K_MIN_GROUP,
|
||||
'anonymous': True, 'written': False,
|
||||
'note': '只读聚合,无 research 写入路径(Q-OPEN-6);样本不足分组已抑制'}
|
||||
23
pbl_kdb_ext/init.py
Normal file
23
pbl_kdb_ext/init.py
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""`load_pbl_kdb_ext()` —— pbl_kdb_ext 模块唯一挂载入口。
|
||||
|
||||
注册三处同步之 ③:env.<契约名> = <契约名>(① 定义在 api.py,② 导出在 __init__.py)。
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
|
||||
from pbl_kdb_ext.api import (
|
||||
pbl_kdb_search,
|
||||
pbl_kdb_get,
|
||||
pbl_research_aggregate,
|
||||
|
||||
)
|
||||
|
||||
|
||||
def load_pbl_kdb_ext():
|
||||
env = ServerEnv()
|
||||
env.pbl_kdb_search = pbl_kdb_search
|
||||
env.pbl_kdb_get = pbl_kdb_get
|
||||
env.pbl_research_aggregate = pbl_research_aggregate
|
||||
|
||||
return 'pbl_kdb_ext'
|
||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
||||
[project]
|
||||
name = "pbl_kdb_ext"
|
||||
version = "0.1.0"
|
||||
description = "KDB 只读桩与匿名聚合(M7,零写入)"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = ["apppublic", "sqlor", "ahserver", "appbase", "rbac"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=61"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["pbl_kdb_ext"]
|
||||
42
scripts/load_path.py
Normal file
42
scripts/load_path.py
Normal file
@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""pbl_kdb_ext RBAC 路径注册(硬门禁 6.6 / QC #11)。
|
||||
|
||||
约定:
|
||||
- 路径 = 模块自动路由 `/pbl_kdb_ext/api/<契约>.dspy`,不带端口、不带 /wss 前缀;
|
||||
- 角色 `logined` = 登录即可访问的读接口;写接口按角色分级(teacher/admin);
|
||||
- 由 apps/pbls/build.sh 第 8 步调用 `register()`;rbac CLI 不在位时打印清单(不静默跳过)。
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MODULE = 'pbl_kdb_ext'
|
||||
|
||||
# (path, role)
|
||||
PATHS = [
|
||||
('/pbl_kdb_ext/api/pbl_kdb_search.dspy', 'logined'),
|
||||
('/pbl_kdb_ext/api/pbl_kdb_get.dspy', 'logined'),
|
||||
('/pbl_kdb_ext/api/pbl_research_aggregate.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)
|
||||
22
skill/SKILL.md
Normal file
22
skill/SKILL.md
Normal file
@ -0,0 +1,22 @@
|
||||
# pbl_kdb_ext 模块技能(自动生成骨架 + 人工补充)
|
||||
|
||||
## 定位
|
||||
KDB 只读桩与匿名聚合(M7,零写入)
|
||||
|
||||
## 挂载
|
||||
`from pbl_kdb_ext.init import load_pbl_kdb_ext` → `load_pbl_kdb_ext()`(应用 app/pbls.py init() 中按序调用)
|
||||
|
||||
## 数据表(2 张)
|
||||
- `pbl_kdb_item`:KDB 条目(建表不写入,is_write_locked 恒 1)
|
||||
- `pbl_kdb_query_log`:KDB 检索日志(只读侧留痕)
|
||||
|
||||
## 契约接口(3 个,路径 `/pbl_kdb_ext/api/<name>.dspy`)
|
||||
- `pbl_kdb_search`
|
||||
- `pbl_kdb_get`
|
||||
- `pbl_research_aggregate`
|
||||
|
||||
## 陷阱
|
||||
- 库名一律 `ServerEnv().get_module_dbname('pbl_kdb_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 路径。
|
||||
35
sql/pbl_kdb_ext.sql
Normal file
35
sql/pbl_kdb_ext.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- pbl_kdb_ext 表 DDL(自动生成,与 apps/pbls/scripts/ddl/pbls_tables.sql 同源)
|
||||
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 检索日志(只读侧留痕)";
|
||||
|
||||
4
wwwroot/api/pbl_kdb_get.dspy
Normal file
4
wwwroot/api/pbl_kdb_get.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_kdb_ext/api/pbl_kdb_get.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_kdb_ext/api/pbl_kdb_get.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_kdb_get(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_kdb_search.dspy
Normal file
4
wwwroot/api/pbl_kdb_search.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_kdb_ext/api/pbl_kdb_search.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_kdb_ext/api/pbl_kdb_search.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_kdb_search(**params_kw)
|
||||
return data
|
||||
4
wwwroot/api/pbl_research_aggregate.dspy
Normal file
4
wwwroot/api/pbl_research_aggregate.dspy
Normal file
@ -0,0 +1,4 @@
|
||||
# pbl_kdb_ext/api/pbl_research_aggregate.dspy —— 契约端点(自动生成,勿手改:改 spec 后跑 gen_artifacts.py)
|
||||
debug('pbl_kdb_ext/api/pbl_research_aggregate.dspy: START params_kw={dict(params_kw)}')
|
||||
data = await pbl_research_aggregate(**params_kw)
|
||||
return data
|
||||
119
wwwroot/index.ui
Normal file
119
wwwroot/index.ui
Normal file
@ -0,0 +1,119 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "KDB 只读桩与匿名聚合(M7,零写入)",
|
||||
"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_kdb_ext_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_kdb_search.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_kdb_search"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_kdb_ext_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_kdb_get.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_kdb_get"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"padding": "20px",
|
||||
"cursor": "pointer"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.pbl_kdb_ext_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('api/pbl_research_aggregate.dspy')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"label": "pbl_research_aggregate"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "pbl_kdb_ext_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user