mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-16 14:57:15 +00:00
Merge pull request #2520 from langbot-app/feat/api-key-system-context
为 lbctl 增加 API Key 发现与管理能力 当前为 lbctl 提供的能力通过 system.py -> SYSTEM_CAPABILITY_OPERATIONS 维护,后续可通过统一的接口暴露能力,避免频繁维护常量。
This commit is contained in:
@@ -3,6 +3,7 @@ import quart
|
||||
from ....authz import Permission, has_permission
|
||||
from ....context import RequestContext
|
||||
from ... import group
|
||||
from .query import resolve_include_secret
|
||||
|
||||
|
||||
@group.group_class('models/llm', '/api/v1/provider/models/llm')
|
||||
@@ -16,7 +17,12 @@ class LLMModelsRouterGroup(group.RouterGroup):
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
provider_uuid = quart.request.args.get('provider_uuid')
|
||||
include_secret = has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE)
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
if provider_uuid:
|
||||
models = await self.ap.llm_model_service.get_llm_models_by_provider(
|
||||
request_context,
|
||||
@@ -53,10 +59,16 @@ class LLMModelsRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
model = await self.ap.llm_model_service.get_llm_model(
|
||||
request_context,
|
||||
model_uuid,
|
||||
include_secret=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
include_secret=include_secret,
|
||||
)
|
||||
if model is None:
|
||||
return self.http_status(404, -1, 'model not found')
|
||||
@@ -111,7 +123,12 @@ class EmbeddingModelsRouterGroup(group.RouterGroup):
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
provider_uuid = quart.request.args.get('provider_uuid')
|
||||
include_secret = has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE)
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
if provider_uuid:
|
||||
models = await self.ap.embedding_models_service.get_embedding_models_by_provider(
|
||||
request_context,
|
||||
@@ -148,10 +165,16 @@ class EmbeddingModelsRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
model = await self.ap.embedding_models_service.get_embedding_model(
|
||||
request_context,
|
||||
model_uuid,
|
||||
include_secret=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
include_secret=include_secret,
|
||||
)
|
||||
if model is None:
|
||||
return self.http_status(404, -1, 'model not found')
|
||||
@@ -208,7 +231,12 @@ class RerankModelsRouterGroup(group.RouterGroup):
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
provider_uuid = quart.request.args.get('provider_uuid')
|
||||
include_secret = has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE)
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
if provider_uuid:
|
||||
models = await self.ap.rerank_models_service.get_rerank_models_by_provider(
|
||||
request_context,
|
||||
@@ -245,10 +273,16 @@ class RerankModelsRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(model_uuid: str, request_context: RequestContext) -> str:
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
model = await self.ap.rerank_models_service.get_rerank_model(
|
||||
request_context,
|
||||
model_uuid,
|
||||
include_secret=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
include_secret=include_secret,
|
||||
)
|
||||
if model is None:
|
||||
return self.http_status(404, -1, 'model not found')
|
||||
|
||||
@@ -3,6 +3,7 @@ import quart
|
||||
from ....authz import Permission, has_permission
|
||||
from ....context import RequestContext
|
||||
from ... import group
|
||||
from .query import resolve_include_secret
|
||||
|
||||
|
||||
@group.group_class('models/providers', '/api/v1/provider/providers')
|
||||
@@ -89,9 +90,15 @@ class ModelProvidersRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
providers = await self.ap.provider_service.get_providers(
|
||||
request_context,
|
||||
include_secret=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
include_secret=include_secret,
|
||||
)
|
||||
for provider in providers:
|
||||
counts = await self.ap.provider_service.get_provider_model_counts(request_context, provider['uuid'])
|
||||
@@ -121,10 +128,16 @@ class ModelProvidersRouterGroup(group.RouterGroup):
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(provider_uuid: str, request_context: RequestContext) -> str:
|
||||
include_secret, error = resolve_include_secret(
|
||||
quart.request.args.get('include_secret'),
|
||||
permitted=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
)
|
||||
if error:
|
||||
return self.http_status(400, -1, error)
|
||||
provider = await self.ap.provider_service.get_provider(
|
||||
request_context,
|
||||
provider_uuid,
|
||||
include_secret=has_permission(request_context, Permission.PROVIDER_SECRET_MANAGE),
|
||||
include_secret=include_secret,
|
||||
)
|
||||
if provider is None:
|
||||
return self.http_status(404, -1, 'provider not found')
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def resolve_include_secret(raw_value: str | None, *, permitted: bool) -> tuple[bool, str | None]:
|
||||
"""Resolve the optional secret projection query parameter."""
|
||||
|
||||
if raw_value is None:
|
||||
return permitted, None
|
||||
|
||||
value = raw_value.strip().lower()
|
||||
if value == 'false':
|
||||
return False, None
|
||||
if value == 'true':
|
||||
return permitted, None
|
||||
return False, 'include_secret must be either true or false'
|
||||
@@ -7,11 +7,93 @@ from .. import group
|
||||
from .....utils import constants
|
||||
from .....entity.persistence.metadata import WorkspaceMetadata
|
||||
from ...authz import Permission
|
||||
from ...context import RequestContext
|
||||
from ...context import PrincipalType, RequestContext
|
||||
from .....provider.tools.loaders.mcp_policy import stdio_mcp_enabled
|
||||
from .....workspace.invitation_delivery import InvitationDeliveryService
|
||||
|
||||
|
||||
SYSTEM_CAPABILITY_OPERATIONS = (
|
||||
'bot.list',
|
||||
'bot.get',
|
||||
'bot.create',
|
||||
'bot.update',
|
||||
'bot.delete',
|
||||
'pipeline.list',
|
||||
'pipeline.get',
|
||||
'pipeline.create',
|
||||
'pipeline.update',
|
||||
'pipeline.delete',
|
||||
'pipeline.copy',
|
||||
'task.list',
|
||||
'task.get',
|
||||
'knowledge_base.list',
|
||||
'knowledge_base.get',
|
||||
'knowledge_base.create',
|
||||
'knowledge_base.update',
|
||||
'knowledge_base.delete',
|
||||
'knowledge_base.file.list',
|
||||
'knowledge_base.file.store',
|
||||
'knowledge_base.file.delete',
|
||||
'knowledge_base.retrieve',
|
||||
'file.document.upload',
|
||||
'plugin.install.github',
|
||||
'plugin.install.marketplace',
|
||||
'plugin.install.local',
|
||||
'plugin.upgrade',
|
||||
'plugin.get',
|
||||
'plugin.list',
|
||||
'plugin.config.get',
|
||||
'plugin.config.update',
|
||||
'plugin.logs',
|
||||
'plugin.delete',
|
||||
'provider.list',
|
||||
'provider.get',
|
||||
'provider.create',
|
||||
'provider.update',
|
||||
'provider.delete',
|
||||
'provider.scan_models',
|
||||
'model.llm.list',
|
||||
'model.llm.get',
|
||||
'model.llm.create',
|
||||
'model.llm.update',
|
||||
'model.llm.delete',
|
||||
'model.llm.test',
|
||||
'model.embedding.list',
|
||||
'model.embedding.get',
|
||||
'model.embedding.create',
|
||||
'model.embedding.update',
|
||||
'model.embedding.delete',
|
||||
'model.embedding.test',
|
||||
'model.rerank.list',
|
||||
'model.rerank.get',
|
||||
'model.rerank.create',
|
||||
'model.rerank.update',
|
||||
'model.rerank.delete',
|
||||
'model.rerank.test',
|
||||
'skill.list',
|
||||
'skill.get',
|
||||
'skill.create',
|
||||
'skill.update',
|
||||
'skill.delete',
|
||||
'skill.files.list',
|
||||
'skill.files.read',
|
||||
'skill.files.write',
|
||||
'skill.preview',
|
||||
'skill.install.github',
|
||||
'skill.install.upload',
|
||||
'mcp_server.list',
|
||||
'mcp_server.get',
|
||||
'mcp_server.create',
|
||||
'mcp_server.update',
|
||||
'mcp_server.delete',
|
||||
'mcp_server.resources',
|
||||
'mcp_server.resource_templates',
|
||||
'mcp_server.resource_read',
|
||||
'mcp_server.logs',
|
||||
'mcp_server.test',
|
||||
)
|
||||
|
||||
|
||||
@group.group_class('system', '/api/v1/system')
|
||||
class SystemRouterGroup(group.RouterGroup):
|
||||
async def initialize(self) -> None:
|
||||
@@ -26,6 +108,15 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/capabilities', methods=['GET'], auth_type=group.AuthType.API_KEY)
|
||||
async def _() -> str:
|
||||
return self.success(
|
||||
data={
|
||||
'schema_version': 1,
|
||||
'operations': {operation: {'supported': True} for operation in SYSTEM_CAPABILITY_OPERATIONS},
|
||||
}
|
||||
)
|
||||
|
||||
@self.route('/info', methods=['GET'], auth_type=group.AuthType.NONE)
|
||||
async def _() -> str:
|
||||
# Read wizard_status and wizard_progress from metadata table
|
||||
@@ -234,7 +325,7 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
@self.route(
|
||||
'/tasks',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(request_context: RequestContext) -> str:
|
||||
@@ -253,18 +344,23 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
instance_uuid=request_context.instance_uuid,
|
||||
workspace_uuid=request_context.workspace_uuid,
|
||||
placement_generation=request_context.placement_generation,
|
||||
public=request_context.principal.principal_type == PrincipalType.API_KEY,
|
||||
)
|
||||
)
|
||||
|
||||
@self.route(
|
||||
'/tasks/<task_id>',
|
||||
methods=['GET'],
|
||||
auth_type=group.AuthType.USER_TOKEN,
|
||||
auth_type=group.AuthType.USER_TOKEN_OR_API_KEY,
|
||||
permission=Permission.RESOURCE_VIEW,
|
||||
)
|
||||
async def _(task_id: str, request_context: RequestContext) -> str:
|
||||
try:
|
||||
task_index = int(task_id)
|
||||
except (TypeError, ValueError):
|
||||
return self.http_status(404, 404, 'Task not found')
|
||||
task = self.ap.task_mgr.get_task_by_id(
|
||||
int(task_id),
|
||||
task_index,
|
||||
instance_uuid=request_context.instance_uuid,
|
||||
workspace_uuid=request_context.workspace_uuid,
|
||||
placement_generation=request_context.placement_generation,
|
||||
@@ -273,6 +369,8 @@ class SystemRouterGroup(group.RouterGroup):
|
||||
if task is None:
|
||||
return self.http_status(404, 404, 'Task not found')
|
||||
|
||||
if request_context.principal.principal_type == PrincipalType.API_KEY:
|
||||
return self.success(data=task.to_public_dict())
|
||||
return self.success(data=task.to_dict())
|
||||
|
||||
@self.route(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import typing
|
||||
import datetime
|
||||
import time
|
||||
@@ -197,6 +198,41 @@ class TaskWrapper:
|
||||
},
|
||||
}
|
||||
|
||||
def to_public_dict(self) -> dict:
|
||||
"""Return the stable task projection exposed to API-key callers."""
|
||||
if self.task.cancelled():
|
||||
status = 'cancelled'
|
||||
error = {'type': 'task_cancelled', 'message': 'Task was cancelled'}
|
||||
result = None
|
||||
elif not self.task.done():
|
||||
status = 'running'
|
||||
error = None
|
||||
result = None
|
||||
else:
|
||||
exception = self.assume_exception()
|
||||
if exception is not None:
|
||||
status = 'failed'
|
||||
error = {'type': 'task_failed', 'message': 'Task execution failed'}
|
||||
result = None
|
||||
else:
|
||||
status = 'succeeded'
|
||||
error = None
|
||||
result = self.assume_result()
|
||||
try:
|
||||
json.dumps(result)
|
||||
except (TypeError, ValueError):
|
||||
result = None
|
||||
|
||||
return {
|
||||
'id': self.id,
|
||||
'task_type': self.task_type,
|
||||
'kind': self.kind,
|
||||
'status': status,
|
||||
'error': error,
|
||||
'result': result,
|
||||
'created_at': self.created_at,
|
||||
}
|
||||
|
||||
def cancel(self):
|
||||
self.task.cancel()
|
||||
|
||||
@@ -325,19 +361,20 @@ class AsyncTaskManager:
|
||||
instance_uuid: str | None = None,
|
||||
workspace_uuid: str | None = None,
|
||||
placement_generation: int | None = None,
|
||||
public: bool = False,
|
||||
) -> dict:
|
||||
return {
|
||||
'tasks': [
|
||||
t.to_dict()
|
||||
for t in self.tasks
|
||||
if (type is None or t.task_type == type)
|
||||
and (kind is None or t.kind == kind)
|
||||
and (instance_uuid is None or t.instance_uuid == instance_uuid)
|
||||
and (workspace_uuid is None or t.workspace_uuid == workspace_uuid)
|
||||
and (placement_generation is None or t.placement_generation == placement_generation)
|
||||
],
|
||||
'id_index': TaskWrapper._id_index,
|
||||
}
|
||||
tasks = [
|
||||
t.to_public_dict() if public else t.to_dict()
|
||||
for t in self.tasks
|
||||
if (type is None or t.task_type == type)
|
||||
and (kind is None or t.kind == kind)
|
||||
and (instance_uuid is None or t.instance_uuid == instance_uuid)
|
||||
and (workspace_uuid is None or t.workspace_uuid == workspace_uuid)
|
||||
and (placement_generation is None or t.placement_generation == placement_generation)
|
||||
]
|
||||
if public:
|
||||
return {'tasks': tasks}
|
||||
return {'tasks': tasks, 'id_index': TaskWrapper._id_index}
|
||||
|
||||
def get_stats(self) -> dict:
|
||||
completed = sum(1 for t in self.tasks if t.task.done())
|
||||
|
||||
Reference in New Issue
Block a user