From 495a84642f84f0fc1f81c0a9711dada6c0fca521 Mon Sep 17 00:00:00 2001 From: pbls-bot Date: Tue, 15 Sep 2026 17:39:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(pbl=5Fkdb=5Fext):=20=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E6=8F=90=E4=BA=A4=20-=20pbls=20=E9=A1=B9=E7=9B=AE=E6=A8=A1?= =?UTF-8?q?=E5=9D=97=E4=BB=A3=E7=A0=81=E5=85=A5=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 12 ++ json/kdb_ext_pbl_kdb_item.json | 160 ++++++++++++++++++++++++ json/kdb_ext_pbl_kdb_query_log.json | 132 +++++++++++++++++++ models/pbl_kdb_item.json | 119 ++++++++++++++++++ models/pbl_kdb_query_log.json | 105 ++++++++++++++++ pbl_kdb_ext/__init__.py | 21 ++++ pbl_kdb_ext/api.py | 98 +++++++++++++++ pbl_kdb_ext/init.py | 23 ++++ pyproject.toml | 13 ++ scripts/load_path.py | 42 +++++++ skill/SKILL.md | 22 ++++ sql/pbl_kdb_ext.sql | 35 ++++++ wwwroot/api/pbl_kdb_get.dspy | 4 + wwwroot/api/pbl_kdb_search.dspy | 4 + wwwroot/api/pbl_research_aggregate.dspy | 4 + wwwroot/index.ui | 119 ++++++++++++++++++ 16 files changed, 913 insertions(+) create mode 100644 .gitignore create mode 100644 json/kdb_ext_pbl_kdb_item.json create mode 100644 json/kdb_ext_pbl_kdb_query_log.json create mode 100644 models/pbl_kdb_item.json create mode 100644 models/pbl_kdb_query_log.json create mode 100644 pbl_kdb_ext/__init__.py create mode 100644 pbl_kdb_ext/api.py create mode 100644 pbl_kdb_ext/init.py create mode 100644 pyproject.toml create mode 100644 scripts/load_path.py create mode 100644 skill/SKILL.md create mode 100644 sql/pbl_kdb_ext.sql create mode 100644 wwwroot/api/pbl_kdb_get.dspy create mode 100644 wwwroot/api/pbl_kdb_search.dspy create mode 100644 wwwroot/api/pbl_research_aggregate.dspy create mode 100644 wwwroot/index.ui diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6579d96 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +*.pyc +*.pyo +.pytest_cache/ +.coverage +.DS_Store +*.log +node_modules/ +dist/ +build/ +.venv/ +venv/ diff --git a/json/kdb_ext_pbl_kdb_item.json b/json/kdb_ext_pbl_kdb_item.json new file mode 100644 index 0000000..8c49d62 --- /dev/null +++ b/json/kdb_ext_pbl_kdb_item.json @@ -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 + } +} diff --git a/json/kdb_ext_pbl_kdb_query_log.json b/json/kdb_ext_pbl_kdb_query_log.json new file mode 100644 index 0000000..dc96700 --- /dev/null +++ b/json/kdb_ext_pbl_kdb_query_log.json @@ -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 + } +} diff --git a/models/pbl_kdb_item.json b/models/pbl_kdb_item.json new file mode 100644 index 0000000..396af8b --- /dev/null +++ b/models/pbl_kdb_item.json @@ -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": [] +} diff --git a/models/pbl_kdb_query_log.json b/models/pbl_kdb_query_log.json new file mode 100644 index 0000000..1cf3adc --- /dev/null +++ b/models/pbl_kdb_query_log.json @@ -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": [] +} diff --git a/pbl_kdb_ext/__init__.py b/pbl_kdb_ext/__init__.py new file mode 100644 index 0000000..278d61e --- /dev/null +++ b/pbl_kdb_ext/__init__.py @@ -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', + +] diff --git a/pbl_kdb_ext/api.py b/pbl_kdb_ext/api.py new file mode 100644 index 0000000..1b1d127 --- /dev/null +++ b/pbl_kdb_ext/api.py @@ -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);样本不足分组已抑制'} diff --git a/pbl_kdb_ext/init.py b/pbl_kdb_ext/init.py new file mode 100644 index 0000000..f1616b8 --- /dev/null +++ b/pbl_kdb_ext/init.py @@ -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' diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..43f16ab --- /dev/null +++ b/pyproject.toml @@ -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"] diff --git a/scripts/load_path.py b/scripts/load_path.py new file mode 100644 index 0000000..24c5cf0 --- /dev/null +++ b/scripts/load_path.py @@ -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) diff --git a/skill/SKILL.md b/skill/SKILL.md new file mode 100644 index 0000000..f6566de --- /dev/null +++ b/skill/SKILL.md @@ -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/.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 路径。 diff --git a/sql/pbl_kdb_ext.sql b/sql/pbl_kdb_ext.sql new file mode 100644 index 0000000..069097f --- /dev/null +++ b/sql/pbl_kdb_ext.sql @@ -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 检索日志(只读侧留痕)"; + diff --git a/wwwroot/api/pbl_kdb_get.dspy b/wwwroot/api/pbl_kdb_get.dspy new file mode 100644 index 0000000..81bb854 --- /dev/null +++ b/wwwroot/api/pbl_kdb_get.dspy @@ -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 diff --git a/wwwroot/api/pbl_kdb_search.dspy b/wwwroot/api/pbl_kdb_search.dspy new file mode 100644 index 0000000..bdaf897 --- /dev/null +++ b/wwwroot/api/pbl_kdb_search.dspy @@ -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 diff --git a/wwwroot/api/pbl_research_aggregate.dspy b/wwwroot/api/pbl_research_aggregate.dspy new file mode 100644 index 0000000..11126ba --- /dev/null +++ b/wwwroot/api/pbl_research_aggregate.dspy @@ -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 diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..a558060 --- /dev/null +++ b/wwwroot/index.ui @@ -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" + } + } + ] +}