AI问数二期

This commit is contained in:
BBIT-Kai
2026-07-16 13:55:21 +08:00
parent 7be72a50ad
commit a84173066e
10 changed files with 1731 additions and 302 deletions
+137 -26
View File
@@ -11,6 +11,7 @@ from fastapi import HTTPException, UploadFile
from config.cubeReport import get_cube_report_settings
from db.sqlserver import execute_parameterized_scalar, execute_parameterized_sql
from service.cube_metadata import cube_member_candidates, get_cube_member_titles
def _require_dify_key() -> str:
@@ -143,15 +144,11 @@ async def get_dify_parameters() -> dict[str, Any]:
def normalize_conversation(item: dict[str, Any]) -> dict[str, Any]:
inputs = item.get("inputs") if isinstance(item.get("inputs"), dict) else {}
allow_global = _as_bool(inputs.get("allow_global"))
return {
"id": str(item.get("id") or ""),
"title": str(item.get("name") or "新数据对话"),
"tenantId": None if allow_global else str(inputs.get("tenant_id") or ""),
"tenantName": "不限租户"
if allow_global
else str(inputs.get("tenant_name") or "未指定租户"),
"allowGlobal": allow_global,
"tenantId": str(inputs.get("tenant_id") or ""),
"tenantName": str(inputs.get("tenant_name") or "未指定租户"),
"createdAt": _iso_timestamp(item.get("created_at")),
"updatedAt": _iso_timestamp(item.get("updated_at")),
"inputs": inputs,
@@ -188,19 +185,118 @@ async def list_dify_conversations(
conversations.extend(item for item in batch if isinstance(item, dict))
if not payload.get("has_more"):
break
last_id = str(batch[-1].get("id") or "")
last_item = batch[-1]
last_id = (
str(last_item.get("id") or "")
if isinstance(last_item, dict)
else ""
)
if not last_id:
break
return [normalize_conversation(item) for item in conversations]
async def list_dify_conversations_page(
user_id: str,
*,
tenant_id: str,
cursor: str | None,
page_size: int,
keyword: str = "",
max_scanned: int = 500,
) -> dict[str, Any]:
"""按 Dify 游标向后扫描,收集当前租户的一页会话。"""
settings = get_cube_report_settings()
key = _require_dify_key()
items: list[dict[str, Any]] = []
last_id = cursor
has_more = True
scanned = 0
normalized_keyword = keyword.strip().lower()
async with httpx.AsyncClient(timeout=_timeout()) as client:
while has_more and len(items) < page_size and scanned < max_scanned:
response = await client.get(
f"{settings.dify_api_base}/conversations",
headers=_headers(key),
params={
"user": user_id,
"limit": min(100, max_scanned - scanned),
"sort_by": "-updated_at",
**({"last_id": last_id} if last_id else {}),
},
)
_raise_dify_error(response)
payload = response.json()
batch = payload.get("data", [])
if not isinstance(batch, list) or not batch:
has_more = False
break
stopped_inside_batch = False
for index, raw_item in enumerate(batch):
if not isinstance(raw_item, dict):
continue
scanned += 1
last_id = str(raw_item.get("id") or last_id or "")
item = normalize_conversation(raw_item)
if item["tenantId"] != tenant_id:
continue
if normalized_keyword and normalized_keyword not in item[
"title"
].lower():
continue
items.append(item)
if len(items) >= page_size:
stopped_inside_batch = index < len(batch) - 1
break
has_more = stopped_inside_batch or bool(payload.get("has_more"))
if not last_id:
has_more = False
return {
"items": items,
"nextCursor": last_id if has_more else None,
"hasMore": has_more,
}
async def get_dify_conversation(
conversation_id: str, user_id: str
) -> dict[str, Any]:
conversations = await list_dify_conversations(user_id)
for item in conversations:
if item["id"] == conversation_id:
return item
settings = get_cube_report_settings()
key = _require_dify_key()
last_id: str | None = None
scanned = 0
async with httpx.AsyncClient(timeout=_timeout()) as client:
while scanned < 5000:
response = await client.get(
f"{settings.dify_api_base}/conversations",
headers=_headers(key),
params={
"user": user_id,
"limit": 100,
"sort_by": "-updated_at",
**({"last_id": last_id} if last_id else {}),
},
)
_raise_dify_error(response)
payload = response.json()
batch = payload.get("data", [])
if not isinstance(batch, list) or not batch:
break
for raw_item in batch:
if not isinstance(raw_item, dict):
continue
scanned += 1
if str(raw_item.get("id") or "") == conversation_id:
return normalize_conversation(raw_item)
if not payload.get("has_more"):
break
last_id = str(batch[-1].get("id") or "")
if not last_id:
break
raise HTTPException(status_code=404, detail="Dify 会话不存在或无权访问")
@@ -684,8 +780,16 @@ def _display_sql(raw_sql: str, values: list[Any]) -> str:
return re.sub(r"@_(\d+)", replace, raw_sql)
def _column_title(column: str, aliases: dict[str, Any]) -> str:
def _column_title(
column: str,
aliases: dict[str, Any],
member_titles: dict[str, str],
) -> str:
member = str(aliases.get(column) or column)
for candidate in cube_member_candidates(member, column):
title = member_titles.get(candidate)
if title:
return title
return member
@@ -693,7 +797,6 @@ async def query_cube_page(
*,
base_load: dict[str, Any],
tenant_id: str | None,
allow_global: bool,
page: int,
page_size: int,
limit_source: str | None,
@@ -702,10 +805,9 @@ async def query_cube_page(
page_size = min(max(page_size, 1), settings.max_page_size)
page_offset = (page - 1) * page_size
canonical_query = deepcopy(base_load)
if not allow_global:
if not tenant_id:
raise HTTPException(status_code=422, detail="当前会话缺少租户信息")
ensure_tenant_filter(canonical_query, tenant_id)
if not tenant_id:
raise HTTPException(status_code=422, detail="当前会话缺少租户信息")
ensure_tenant_filter(canonical_query, tenant_id)
is_user_limit = str(limit_source).lower() == "user"
semantic_limit = (
@@ -739,9 +841,10 @@ async def query_cube_page(
for key in ("limit", "offset", "order", "total"):
count_query.pop(key, None)
page_sql, count_sql = await asyncio.gather(
page_sql, count_sql, member_titles = await asyncio.gather(
_generate_cube_sql(page_query),
_generate_cube_sql(count_query),
get_cube_member_titles(),
)
executable_sql, bindings = _prepare_cube_sql(page_sql["sql"], page_sql["params"])
count_executable, count_bindings = _prepare_cube_sql(
@@ -761,7 +864,11 @@ async def query_cube_page(
columns = [
{
"key": key,
"title": _column_title(key, page_sql["aliases"]),
"title": _column_title(
key,
page_sql["aliases"],
member_titles,
),
"type": "text",
}
for key in keys
@@ -795,25 +902,29 @@ async def query_cube_export(
*,
base_load: dict[str, Any],
tenant_id: str | None,
allow_global: bool,
limit_source: str | None,
) -> dict[str, Any]:
query = deepcopy(base_load)
if not allow_global:
if not tenant_id:
raise HTTPException(status_code=422, detail="当前会话缺少租户信息")
ensure_tenant_filter(query, tenant_id)
if not tenant_id:
raise HTTPException(status_code=422, detail="当前会话缺少租户信息")
ensure_tenant_filter(query, tenant_id)
if str(limit_source).lower() != "user":
query.pop("limit", None)
query.pop("offset", None)
query.pop("total", None)
sql_info = await _generate_cube_sql(query)
sql_info, member_titles = await asyncio.gather(
_generate_cube_sql(query),
get_cube_member_titles(),
)
executable_sql, bindings = _prepare_cube_sql(sql_info["sql"], sql_info["params"])
keys, rows = await asyncio.to_thread(
execute_parameterized_sql, executable_sql, bindings
)
headers = {key: _column_title(key, sql_info["aliases"]) for key in keys}
headers = {
key: _column_title(key, sql_info["aliases"], member_titles)
for key in keys
}
return {
"headers": headers,
"rows": rows,