From 7c5755027760fb45df14b82a6d03c49eb9e054ba Mon Sep 17 00:00:00 2001 From: yumoqing Date: Tue, 25 Aug 2026 14:11:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(llm-proxy):=20OpenAI=20=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E7=AB=AF=E7=82=B9=20+=20token=20=E7=AD=BE?= =?UTF-8?q?=E5=8F=91/=E5=90=8A=E9=94=80/=E5=88=97=E8=A1=A8=20dspy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/api/llm_token_create.dspy | 38 +++++++++++++++++ wwwroot/api/llm_token_revoke.dspy | 21 +++++++++ wwwroot/api/llm_tokens.dspy | 16 +++++++ wwwroot/api/llm_v1/chat/completions.dspy | 54 ++++++++++++++++++++++++ 4 files changed, 129 insertions(+) create mode 100644 wwwroot/api/llm_token_create.dspy create mode 100644 wwwroot/api/llm_token_revoke.dspy create mode 100644 wwwroot/api/llm_tokens.dspy create mode 100644 wwwroot/api/llm_v1/chat/completions.dspy diff --git a/wwwroot/api/llm_token_create.dspy b/wwwroot/api/llm_token_create.dspy new file mode 100644 index 0000000..8015056 --- /dev/null +++ b/wwwroot/api/llm_token_create.dspy @@ -0,0 +1,38 @@ +# llm_token_create.dspy — 签发短期 LLM 代理 token(登录用户,机构隔离) +# +# 用途:给运行环境(browser-use 子进程 / 沙箱脚本 / 被开发的 AI 应用)发一把 +# 短期钥匙,代替下发真实模型 api_key。真 key 永不出服务进程。 + +user_id = await get_user() +if not user_id: + return json.dumps({"success": False, "error": "未登录"}, ensure_ascii=False) + +p = params_kw or {} +project_id = (p.get('project_id', '') or '').strip() +task_id = (p.get('task_id', '') or '').strip() +model_name = (p.get('model_name', '') or '').strip() +purpose = (p.get('purpose', '') or '').strip() +ttl_hours = p.get('ttl_hours', 8) +max_calls = p.get('max_calls', 500) + +# 机构隔离:token 的 org_id 一律取当前登录用户所属机构,不接受前端传入 +org_id = get_userorgid() +if not org_id: + org_id = '0' + +# 指定了 project_id 时校验 owner(防越权给别人的项目签发 token) +if project_id: + ok_owner, msg_owner = await check_project_owner(project_id, user_id) + if not ok_owner: + return json.dumps({"success": False, "error": msg_owner or "仅项目 owner 可签发"}, + ensure_ascii=False) + +ok, token = await create_llm_token(org_id, project_id=project_id, task_id=task_id, + model_name=model_name, purpose=purpose, + ttl_hours=ttl_hours, max_calls=max_calls, + created_by=user_id) +if ok: + return json.dumps({"success": True, "token": token, "org_id": org_id, + "base_url": entire_url('/pipeline_core/api/llm_v1')}, + ensure_ascii=False) +return json.dumps({"success": False, "error": token}, ensure_ascii=False) diff --git a/wwwroot/api/llm_token_revoke.dspy b/wwwroot/api/llm_token_revoke.dspy new file mode 100644 index 0000000..b3a408d --- /dev/null +++ b/wwwroot/api/llm_token_revoke.dspy @@ -0,0 +1,21 @@ +# llm_token_revoke.dspy — 吊销短期 LLM 代理 token + +user_id = await get_user() +if not user_id: + return json.dumps({"success": False, "error": "未登录"}, ensure_ascii=False) + +p = params_kw or {} +token_id = (p.get('token_id', '') or '').strip() +task_id = (p.get('task_id', '') or '').strip() + +if task_id: + ok, msg = await revoke_task_tokens(task_id) +elif token_id: + ok, msg = await revoke_llm_token(token_id=token_id) +else: + return json.dumps({"success": False, "error": "须指定 token_id 或 task_id"}, + ensure_ascii=False) + +if ok: + return json.dumps({"success": True, "message": msg}, ensure_ascii=False) +return json.dumps({"success": False, "error": msg}, ensure_ascii=False) diff --git a/wwwroot/api/llm_tokens.dspy b/wwwroot/api/llm_tokens.dspy new file mode 100644 index 0000000..4222e10 --- /dev/null +++ b/wwwroot/api/llm_tokens.dspy @@ -0,0 +1,16 @@ +# llm_tokens.dspy — 列出已签发 token(元信息 + 用量,不返回 token 明文,供审计) + +user_id = await get_user() +if not user_id: + return json.dumps({"success": False, "error": "未登录"}, ensure_ascii=False) + +p = params_kw or {} +project_id = (p.get('project_id', '') or '').strip() +status = (p.get('status', '') or '').strip() + +# 机构隔离:只看本机构签发的 token +org_id = get_userorgid() or '0' + +rows = await list_llm_tokens(org_id=org_id, project_id=project_id or None, + status=status or None) +return json.dumps({"success": True, "tokens": rows}, ensure_ascii=False, default=str) diff --git a/wwwroot/api/llm_v1/chat/completions.dspy b/wwwroot/api/llm_v1/chat/completions.dspy new file mode 100644 index 0000000..30da72b --- /dev/null +++ b/wwwroot/api/llm_v1/chat/completions.dspy @@ -0,0 +1,54 @@ +# completions.dspy — OpenAI 兼容 LLM 代理端点(内部运行环境专用) +# +# URL: /pipeline_core/api/llm_v1/chat/completions +# OpenAI 兼容客户端(browser-use / LangChain / openai SDK)配置: +# base_url = http:///pipeline_core/api/llm_v1 +# api_key = <短期 token>(pipeline_llm_tokens 签发,非真实模型 key) +# +# 鉴权:Authorization: Bearer <短期 token>,不依赖登录会话(运行环境无 session)。 +# 机构隔离:按 token 绑定的 org_id 解析真实模型 key,运行环境拿不到真 key。 + +auth = '' +try: + auth = request.headers.get('Authorization', '') or '' +except Exception: + auth = '' +if not auth: + auth = (params_kw or {}).get('api_key', '') or '' + +if not auth: + return json.dumps({"error": {"message": "缺少 Authorization Bearer token", + "type": "invalid_request_error", "code": "missing_token"}}, + ensure_ascii=False) + +payload = None +try: + payload = await request.json() +except Exception: + payload = None +if not isinstance(payload, dict): + try: + _raw = await request.text() + payload = json.loads(_raw) if _raw else None + except Exception: + payload = None +if not isinstance(payload, dict): + _pk = dict(params_kw or {}) + _msgs = _pk.get('messages') + if isinstance(_msgs, str): + try: + _msgs = json.loads(_msgs) + except Exception: + _msgs = None + payload = {"model": _pk.get('model', ''), "messages": _msgs} if _msgs else None + +if not isinstance(payload, dict) or not payload.get('messages'): + return json.dumps({"error": {"message": "请求体缺 messages", + "type": "invalid_request_error", "code": "missing_messages"}}, + ensure_ascii=False) + +ok, result = await proxy_chat_completion(auth, payload) +if ok: + return json.dumps(result, ensure_ascii=False, default=str) +return json.dumps({"error": {"message": result, "type": "invalid_request_error", + "code": "proxy_error"}}, ensure_ascii=False)