refactor(rag-api): API Key 改由平台 dapi 模块统一管理
- 删除自建 rag_api_keys 表 model + api_key_create/list/revoke 管理端点 (key 申请/管理走 dapi 的 downapp/downapikey UI,避免两套 key 体系) - verify_api_key 改调 dapi.get_apikey_user:有效性/过期/IP白名单全走平台机制, 从认证用户 orgid 注入机构边界 - 7 个对外端点去掉 rag 侧 scopes 检查(授权粒度归 dapi 账号) - 部署目标:产线平台测试环境(ragserver 弃用),rag 模块已在 pipeline-app pkgs
This commit is contained in:
parent
00262a6d2a
commit
51b0bf1b85
@ -1,98 +0,0 @@
|
|||||||
{
|
|
||||||
"summary": [
|
|
||||||
{
|
|
||||||
"name": "rag_api_keys",
|
|
||||||
"title": "对外API密钥",
|
|
||||||
"primary": [
|
|
||||||
"id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"name": "id",
|
|
||||||
"title": "ID",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "org_id",
|
|
||||||
"title": "所属机构",
|
|
||||||
"type": "str",
|
|
||||||
"length": 32,
|
|
||||||
"nullable": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "name",
|
|
||||||
"title": "密钥名称",
|
|
||||||
"type": "str",
|
|
||||||
"length": 100,
|
|
||||||
"nullable": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "key_hash",
|
|
||||||
"title": "密钥哈希(SHA256)",
|
|
||||||
"type": "str",
|
|
||||||
"length": 64,
|
|
||||||
"nullable": "no"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "prefix",
|
|
||||||
"title": "密钥前缀(展示)",
|
|
||||||
"type": "str",
|
|
||||||
"length": 16,
|
|
||||||
"nullable": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "scopes",
|
|
||||||
"title": "权限范围",
|
|
||||||
"type": "str",
|
|
||||||
"length": 500,
|
|
||||||
"nullable": "yes",
|
|
||||||
"default": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "status",
|
|
||||||
"title": "状态",
|
|
||||||
"type": "str",
|
|
||||||
"length": 16,
|
|
||||||
"nullable": "yes",
|
|
||||||
"default": "active"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "expires_at",
|
|
||||||
"title": "过期时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "last_used_at",
|
|
||||||
"title": "最近使用",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "yes"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "created_at",
|
|
||||||
"title": "创建时间",
|
|
||||||
"type": "datetime",
|
|
||||||
"nullable": "yes"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"indexes": [
|
|
||||||
{
|
|
||||||
"name": "idx_ak_hash",
|
|
||||||
"idxtype": "unique",
|
|
||||||
"idxfields": [
|
|
||||||
"key_hash"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "idx_ak_org",
|
|
||||||
"idxtype": "index",
|
|
||||||
"idxfields": [
|
|
||||||
"org_id"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,89 +1,68 @@
|
|||||||
# -*- coding:utf-8 -*-
|
# -*- coding:utf-8 -*-
|
||||||
"""RAG 对外 API 核心(B2B 机器接口)— Bearer Key 鉴权 + org 注入。
|
"""RAG 对外 API 核心(B2B 机器接口)— dapi Bearer Key 鉴权 + org 注入。
|
||||||
|
|
||||||
与 UI 通道的关系:
|
与 UI 通道的关系:
|
||||||
- UI 通道(wwwroot/knowledge_bases_list/*.dspy):RBAC 登录会话鉴权,org/user 取自会话;
|
- UI 通道(wwwroot/knowledge_bases_list/*.dspy):RBAC 登录会话鉴权,org/user 取自会话;
|
||||||
- API 通道(wwwroot/api/*.dspy → 本模块):路由/权限照常由 RBAC 控制
|
- API 通道(wwwroot/api/*.dspy → 本模块):路由/权限照常由 RBAC 控制
|
||||||
(端点授 any),业务身份由 rag_api_keys 的 Bearer Key 决定(绑定 org_id)。
|
(端点授 any),业务身份由 **dapi 模块**的 Bearer Key 决定(平台统一 key 管理,
|
||||||
|
rag 不自建 key 表)。
|
||||||
|
|
||||||
org 注入方式:构造一个轻量 env(DictObject),get_userorgid/get_user 返回 key 绑定的
|
鉴权链路:调用方在 dapi 申请 downapikey(绑定 users 账号)→ 本模块 verify_api_key()
|
||||||
|
调 dapi.get_apikey_user 校验(有效性/过期/IP白名单都在 dapi)→ 从认证用户取 orgid
|
||||||
|
→ 机构隔离以该 org 为边界。
|
||||||
|
|
||||||
|
org 注入方式:构造一个轻量 env(DictObject),get_userorgid/get_user 返回 key 用户所属
|
||||||
机构,get_module_dbname 等透传全局 ServerEnv —— 复用 init.py 的底层能力
|
机构,get_module_dbname 等透传全局 ServerEnv —— 复用 init.py 的底层能力
|
||||||
(_resolve_search_kbs/_build_search_vector/_call_uapi 等),杜绝双实现分叉。
|
(_resolve_search_kbs/_build_search_vector/_call_uapi 等),杜绝双实现分叉。
|
||||||
|
|
||||||
权限语义(v1):
|
权限语义(v1):
|
||||||
- key 即机构级凭据:org 隔离强制(只能操作本机构知识库),KB 级 maintain_roles/
|
- key 即用户级凭据:org 隔离强制(只能操作本机构知识库),能力面等于该用户在本机构
|
||||||
search_roles 是人类通道的门槛,对 API key 不生效(key 的授权面由 scopes 控制);
|
的知识库操作范围;不再做 rag 侧 scopes(授权粒度归 dapi 账号管理)。
|
||||||
- scopes 逗号分隔:kb.create / kb.delete / doc.upload / doc.delete / tag.write / search;
|
|
||||||
'*' 全通。
|
|
||||||
|
|
||||||
2026-09-03 新增(配合 wwwroot/api/ 六个对外端点)。
|
2026-09-03 新增(配合 wwwroot/api/ 七个对外端点;key 管理复用 dapi 模块)。
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import time
|
|
||||||
import uuid as _uuid
|
import uuid as _uuid
|
||||||
from hashlib import sha256
|
|
||||||
|
|
||||||
from ahserver.serverenv import ServerEnv
|
from ahserver.serverenv import ServerEnv
|
||||||
from appPublic.dictObject import DictObject
|
from appPublic.dictObject import DictObject
|
||||||
from sqlor.dbpools import get_sor_context
|
from sqlor.dbpools import get_sor_context
|
||||||
|
|
||||||
|
|
||||||
# ────────────────────────── API Key 鉴权 ──────────────────────────
|
# ────────────────────────── API Key 鉴权(dapi 统一管理) ──────────────────────────
|
||||||
|
|
||||||
def hash_key(plain):
|
|
||||||
return sha256((plain or '').encode('utf-8')).hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
async def verify_api_key(request):
|
async def verify_api_key(request):
|
||||||
"""从 Authorization: Bearer <key> 或 x-api-key: <key> 验证调用方。
|
"""用 dapi 模块验证调用方 Bearer Key(downapp/downapikey/users 三表)。
|
||||||
|
|
||||||
|
平台级机制:key 有效性、过期、IP 白名单、登录态、downappuser 角色 全由
|
||||||
|
dapi.apikey_user 负责;rag 只消费鉴权结果,从认证用户反查 org_id。
|
||||||
|
|
||||||
返回 (ctx_dict, None) 或 (None, error_message)。
|
返回 (ctx_dict, None) 或 (None, error_message)。
|
||||||
ctx = {org_id, key_id, name, scopes, user_id}
|
ctx = {org_id, user_id, username}
|
||||||
"""
|
"""
|
||||||
token = ''
|
|
||||||
auth = request.headers.get('Authorization', '') or ''
|
auth = request.headers.get('Authorization', '') or ''
|
||||||
if auth.startswith('Bearer '):
|
token = auth[7:].strip() if auth.startswith('Bearer ') else ''
|
||||||
token = auth[7:].strip()
|
|
||||||
if not token:
|
if not token:
|
||||||
token = (request.headers.get('x-api-key', '') or '').strip()
|
token = (request.headers.get('x-api-key', '') or '').strip()
|
||||||
if not token:
|
if not token:
|
||||||
return None, "missing api key(Authorization: Bearer <key> 或 x-api-key 头)"
|
return None, "missing api key(Authorization: Bearer *** 或 x-api-key 头)"
|
||||||
|
|
||||||
|
from dapi.dapi import get_apikey_user
|
||||||
|
|
||||||
env = ServerEnv()
|
env = ServerEnv()
|
||||||
async with get_sor_context(env, 'rag') as sor:
|
dbname = env.get_module_dbname('dapi')
|
||||||
recs = await sor.sqlExe(
|
client_ip = (request.get('client_ip') if hasattr(request, 'get') else '') or ''
|
||||||
"SELECT id, org_id, name, scopes, status, expires_at FROM rag_api_keys "
|
async with get_sor_context(env, dbname) as sor:
|
||||||
"WHERE key_hash=${h}$ LIMIT 1", {"h": hash_key(token)})
|
user = await get_apikey_user(sor, token, client_ip)
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
if not recs:
|
if user is None:
|
||||||
return None, "invalid api key"
|
return None, "invalid api key"
|
||||||
r = recs[0]
|
org_id = str(getattr(user, 'orgid', '') or getattr(user, 'org_id', '') or '')
|
||||||
if (getattr(r, 'status', '') or '') != 'active':
|
if not org_id:
|
||||||
return None, "api key disabled"
|
return None, "api key user has no org"
|
||||||
exp = getattr(r, 'expires_at', None)
|
return {"org_id": org_id, "user_id": str(getattr(user, 'id', '') or ''),
|
||||||
if exp is not None and str(exp).strip() and time.strftime('%Y-%m-%d %H:%M:%S') >= str(exp):
|
"username": str(getattr(user, 'username', '') or '')}, None
|
||||||
return None, "api key expired"
|
|
||||||
scopes = [s.strip() for s in str(getattr(r, 'scopes', '') or '').split(',') if s.strip()]
|
|
||||||
key_id = getattr(r, 'id', '')
|
|
||||||
# 最近使用时间(best-effort,失败不影响调用)
|
|
||||||
try:
|
|
||||||
await sor.sqlExe(
|
|
||||||
"UPDATE rag_api_keys SET last_used_at=NOW() WHERE id=${i}$", {"i": key_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return {"org_id": str(getattr(r, 'org_id', '') or ''), "key_id": key_id,
|
|
||||||
"name": str(getattr(r, 'name', '') or ''), "scopes": scopes,
|
|
||||||
"user_id": ''}, None
|
|
||||||
|
|
||||||
|
|
||||||
def require_scope(ctx, scope):
|
|
||||||
sc = ctx.get('scopes') or []
|
|
||||||
return '*' in sc or scope in sc
|
|
||||||
|
|
||||||
|
|
||||||
async def read_json_body(request, params_kw):
|
async def read_json_body(request, params_kw):
|
||||||
|
|||||||
@ -13,8 +13,8 @@ PATHS_ANY = [
|
|||||||
f"/{MOD}/knowledge_bases_list/upload.js",
|
f"/{MOD}/knowledge_bases_list/upload.js",
|
||||||
]
|
]
|
||||||
|
|
||||||
# 对外 B2B API 端点(路由权限授 any;业务鉴权靠 rag_api_keys 的 Bearer Key,
|
# 对外 B2B API 端点(路由权限授 any;业务鉴权靠 dapi 模块统一管理的 Bearer Key,
|
||||||
# org 隔离与 scopes 在 rag/api_core.py 内强制——见模块 README「对外 API」)
|
# org 隔离在 rag/api_core.py 内强制——见模块 README「对外 API」)
|
||||||
PATHS_API_ANY = [
|
PATHS_API_ANY = [
|
||||||
f"/{MOD}/api/kb_create.dspy",
|
f"/{MOD}/api/kb_create.dspy",
|
||||||
f"/{MOD}/api/kb_delete.dspy",
|
f"/{MOD}/api/kb_delete.dspy",
|
||||||
@ -63,10 +63,7 @@ PATHS_LOGINED = [
|
|||||||
# 存储用量
|
# 存储用量
|
||||||
f"/{MOD}/knowledge_bases_list/storage_card.dspy",
|
f"/{MOD}/knowledge_bases_list/storage_card.dspy",
|
||||||
f"/{MOD}/knowledge_bases_list/storage_stats.dspy",
|
f"/{MOD}/knowledge_bases_list/storage_stats.dspy",
|
||||||
# 对外 API Key 管理(管理端,登录会话鉴权)
|
# (API Key 管理走平台 dapi 模块:/dapi/ 的 key 申请/管理 UI,rag 不自建 key 端点)
|
||||||
f"/{MOD}/knowledge_bases_list/api_key_create.dspy",
|
|
||||||
f"/{MOD}/knowledge_bases_list/api_key_list.dspy",
|
|
||||||
f"/{MOD}/knowledge_bases_list/api_key_revoke.dspy",
|
|
||||||
# CRUD 管理页
|
# CRUD 管理页
|
||||||
f"/{MOD}/documents_list/index.ui",
|
f"/{MOD}/documents_list/index.ui",
|
||||||
f"/{MOD}/engine_configs_list/index.ui",
|
f"/{MOD}/engine_configs_list/index.ui",
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "doc.delete"):
|
|
||||||
return C._err("api key lacks scope: doc.delete", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.doc_delete(env, _ns)
|
return await C.doc_delete(env, _ns)
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "tag.write"):
|
|
||||||
return C._err("api key lacks scope: tag.write", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.doc_set_tags(env, _ns)
|
return await C.doc_set_tags(env, _ns)
|
||||||
|
|||||||
@ -5,8 +5,6 @@ from rag import api_core as C
|
|||||||
_ctx, _e = await C.verify_api_key(request)
|
_ctx, _e = await C.verify_api_key(request)
|
||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
if not C.require_scope(_ctx, "doc.upload"):
|
|
||||||
return C._err("api key lacks scope: doc.upload", code="forbidden")
|
|
||||||
_ns = dict(params_kw or {})
|
_ns = dict(params_kw or {})
|
||||||
_file_data = await request.read()
|
_file_data = await request.read()
|
||||||
_file_name = _ns.get('file_name', '') or 'upload.bin'
|
_file_name = _ns.get('file_name', '') or 'upload.bin'
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "kb.create"):
|
|
||||||
return C._err("api key lacks scope: kb.create", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.kb_create(env, _ns)
|
return await C.kb_create(env, _ns)
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "kb.delete"):
|
|
||||||
return C._err("api key lacks scope: kb.delete", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.kb_delete(env, _ns)
|
return await C.kb_delete(env, _ns)
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "search"):
|
|
||||||
return C._err("api key lacks scope: search", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.search(env, _ns)
|
return await C.search(env, _ns)
|
||||||
|
|||||||
@ -6,7 +6,5 @@ _ctx, _e = await C.verify_api_key(request)
|
|||||||
if _e:
|
if _e:
|
||||||
return C._err(_e, code="unauthorized")
|
return C._err(_e, code="unauthorized")
|
||||||
_ns = await C.read_json_body(request, params_kw)
|
_ns = await C.read_json_body(request, params_kw)
|
||||||
if not C.require_scope(_ctx, "tag.write"):
|
|
||||||
return C._err("api key lacks scope: tag.write", code="forbidden")
|
|
||||||
env = C.make_api_env(_ctx)
|
env = C.make_api_env(_ctx)
|
||||||
return await C.tag_create(env, _ns)
|
return await C.tag_create(env, _ns)
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
# 管理端:创建对外 API Key(登录用户;org 取自会话)
|
|
||||||
# POST /rag/knowledge_bases_list/api_key_create.dspy
|
|
||||||
# params: {name, scopes?, expires_days?}
|
|
||||||
# 返回 widget:一次性展示明文 key(库里只存 SHA256)
|
|
||||||
import secrets
|
|
||||||
from rag import api_core as C
|
|
||||||
|
|
||||||
ns = params_kw.copy()
|
|
||||||
env = request._run_ns
|
|
||||||
org_id = await env.get_userorgid()
|
|
||||||
name = (ns.get('name') or '默认密钥').strip()
|
|
||||||
scopes = ns.get('scopes') or ''
|
|
||||||
if isinstance(scopes, (list, tuple)):
|
|
||||||
scopes = ','.join(str(x) for x in scopes)
|
|
||||||
scopes = str(scopes).strip() or 'kb.create,kb.delete,doc.upload,doc.delete,tag.write,search'
|
|
||||||
try:
|
|
||||||
exp_days = int(ns.get('expires_days') or 0)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
exp_days = 0
|
|
||||||
|
|
||||||
plain = 'rag-' + secrets.token_hex(24)
|
|
||||||
kid = str(uuid()).replace('-', '')[:16]
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(get_module_dbname('rag')) as sor:
|
|
||||||
if exp_days > 0:
|
|
||||||
await sor.sqlExe(
|
|
||||||
"INSERT INTO rag_api_keys (id, org_id, name, key_hash, prefix, scopes, status, expires_at, created_at) "
|
|
||||||
"VALUES (${id}$, ${o}$, ${n}$, ${h}$, ${p}$, ${s}$, 'active', DATE_ADD(NOW(), INTERVAL ${d}$ DAY), NOW())",
|
|
||||||
{"id": kid, "o": org_id, "n": name, "h": C.hash_key(plain),
|
|
||||||
"p": plain[:12], "s": scopes, "d": exp_days})
|
|
||||||
else:
|
|
||||||
await sor.sqlExe(
|
|
||||||
"INSERT INTO rag_api_keys (id, org_id, name, key_hash, prefix, scopes, status, created_at) "
|
|
||||||
"VALUES (${id}$, ${o}$, ${n}$, ${h}$, ${p}$, ${s}$, 'active', NOW())",
|
|
||||||
{"id": kid, "o": org_id, "n": name, "h": C.hash_key(plain), "p": plain[:12], "s": scopes})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
|
|
||||||
return {"widgettype": "VBox", "options": {"padding": "12px", "spacing": "8px"}, "subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": "✅ API Key 已创建(仅此一次展示,请立即保存)", "cfontsize": 15, "color": "#10b981"}},
|
|
||||||
{"widgettype": "Text", "options": {"text": plain, "cfontsize": 13, "bgcolor": "#f5f7fa", "padding": "8px", "css": "selectable monospace"}},
|
|
||||||
{"widgettype": "Text", "options": {"text": "名称: " + name + " · 范围: " + scopes, "cfontsize": 12, "color": "#888"}},
|
|
||||||
]}
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
# 管理端:API Key 列表(本机构;只展示前缀,不回显明文)
|
|
||||||
# GET /rag/knowledge_bases_list/api_key_list.dspy
|
|
||||||
ns = params_kw.copy()
|
|
||||||
env = request._run_ns
|
|
||||||
org_id = await env.get_userorgid()
|
|
||||||
rows = []
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(get_module_dbname('rag')) as sor:
|
|
||||||
recs = await sor.sqlExe(
|
|
||||||
"SELECT id, name, prefix, scopes, status, last_used_at, expires_at, created_at "
|
|
||||||
"FROM rag_api_keys WHERE org_id=${o}$ ORDER BY created_at DESC", {"o": org_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
for r in (recs or []):
|
|
||||||
st = getattr(r, 'status', '')
|
|
||||||
badge = '🟢' if st == 'active' else '⛔'
|
|
||||||
exp = str(getattr(r, 'expires_at', '') or '')
|
|
||||||
exp_s = ('至 ' + exp[:10]) if exp and exp != 'None' else '永久'
|
|
||||||
used = str(getattr(r, 'last_used_at', '') or '')
|
|
||||||
used_s = used[:16] if used and used != 'None' else '从未使用'
|
|
||||||
rows.append({"widgettype": "HBox", "options": {"spacing": "12px", "padding": "8px 12px", "borderBottom": "1px solid #eee"},
|
|
||||||
"subwidgets": [
|
|
||||||
{"widgettype": "Text", "options": {"text": badge + " " + str(getattr(r, 'name', '') or ''), "cfontsize": 13}},
|
|
||||||
{"widgettype": "Text", "options": {"text": str(getattr(r, 'prefix', '')) + '…', "cfontsize": 12, "color": "#888"}},
|
|
||||||
{"widgettype": "Text", "options": {"text": str(getattr(r, 'scopes', '') or ''), "cfontsize": 11, "color": "#aaa"}},
|
|
||||||
{"widgettype": "Text", "options": {"text": exp_s + " · " + used_s, "cfontsize": 11, "color": "#aaa"}},
|
|
||||||
{"widgettype": "Button", "options": {"label": ("禁用" if st == 'active' else "启用"), "cfontsize": 11},
|
|
||||||
"binds": [{"wid": "self", "event": "click", "actiontype": "ajax",
|
|
||||||
"options": {"url": entire_url('./api_key_revoke.dspy'),
|
|
||||||
"params": {"key_id": str(getattr(r, 'id', '')),
|
|
||||||
"action": ("disable" if st == 'active' else "enable")},
|
|
||||||
"success": "refresh"}}]},
|
|
||||||
]})
|
|
||||||
if not rows:
|
|
||||||
rows = [{"widgettype": "Text", "options": {"text": "暂无 API Key", "color": "#aaa"}}]
|
|
||||||
return {"widgettype": "VBox", "options": {"width": "100%"}, "subwidgets": rows}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
# 管理端:启用/禁用 API Key(只能操作本机构)
|
|
||||||
# POST /rag/knowledge_bases_list/api_key_revoke.dspy params: {key_id, action=disable|enable|delete}
|
|
||||||
ns = params_kw.copy()
|
|
||||||
env = request._run_ns
|
|
||||||
org_id = await env.get_userorgid()
|
|
||||||
key_id = (ns.get('key_id') or '').strip()
|
|
||||||
action = (ns.get('action') or 'disable').strip()
|
|
||||||
if not key_id:
|
|
||||||
return json.dumps({"status": "error", "error": "key_id required"})
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(get_module_dbname('rag')) as sor:
|
|
||||||
recs = await sor.sqlExe(
|
|
||||||
"SELECT id FROM rag_api_keys WHERE id=${k}$ AND org_id=${o}$", {"k": key_id, "o": org_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
if not recs:
|
|
||||||
return json.dumps({"status": "error", "error": "key not found in your org"})
|
|
||||||
if action == 'delete':
|
|
||||||
await sor.sqlExe("DELETE FROM rag_api_keys WHERE id=${k}$", {"k": key_id})
|
|
||||||
elif action == 'enable':
|
|
||||||
await sor.sqlExe("UPDATE rag_api_keys SET status='active' WHERE id=${k}$", {"k": key_id})
|
|
||||||
else:
|
|
||||||
await sor.sqlExe("UPDATE rag_api_keys SET status='disabled' WHERE id=${k}$", {"k": key_id})
|
|
||||||
await sor.sqlExe("COMMIT", {})
|
|
||||||
return json.dumps({"status": "SUCCEEDED", "action": action, "key_id": key_id})
|
|
||||||
Loading…
x
Reference in New Issue
Block a user