Skip to content

feat(dashboard): 多租户 LLM 配置 - 用户级 API key 管理#137

Merged
mirror29 merged 5 commits into
mainfrom
feat/multi-tenant-llm-config
Jul 13, 2026
Merged

feat(dashboard): 多租户 LLM 配置 - 用户级 API key 管理#137
mirror29 merged 5 commits into
mainfrom
feat/multi-tenant-llm-config

Conversation

@mirror29

Copy link
Copy Markdown
Owner

多租户 LLM 配置

功能概述

支持用户在 dashboard 配置自己的 LLM 供应商和 API key,实现多租户模式下的灵活模型管理。

核心功能

1. 用户级配置管理

  • ✅ 支持多配置存储(不同供应商/同一供应商多个 key)
  • ✅ 激活配置切换即时生效
  • ✅ 支持自定义端点(中转站/私有部署)
  • ✅ API key AES-256-GCM 加密存储

2. 支持的供应商

  • 预设供应商(6 家):DeepSeek / OpenAI / Kimi / Zhipu + Anthropic / Gemini
  • 自定义端点:支持中转站、私有部署等任意 OpenAI-compatible API

3. 安全机制

  • ✅ API key 加密存储(AES-256-GCM)
  • ✅ 揭示完整 key 需二次验证(密码)
  • ✅ 明文 key 仅存在于请求级内存,永不日志记录
  • ✅ 30 秒自动过期

技术实现

数据库

  • 迁移 0033_user_llm_preferences.py:扩展 users 表增加 preferences JSONB 字段
  • 索引 idx_users_has_llm_config 加速查询

后端 API

GET  /api/user/settings          → 获取所有配置(掩码显示)
POST /api/user/settings          → 新增配置
PUT  /api/user/settings/[id]     → 更新配置
DELETE /api/user/settings/[id]   → 删除配置
POST /api/user/settings/activate → 切换激活配置
POST /api/user/settings/reveal-key → 揭示完整 key(需验证)

加密服务

  • 密钥来源:环境变量 LLM_CONFIG_ENCRYPTION_KEY 或从 JWT_SECRET 派生
  • 算法:AES-256-GCM(保密性 + 完整性 + 认证)

LLM 工厂改造

  • 新增 buildLLMForUser(userConfig) 动态构造
  • 支持自定义 base URL(中转站)
  • 降级机制:用户未配置时回落到系统 .env

CopilotKit 集成

  • 预取用户激活配置并通过 X-LLM-Config header 传递
  • 配置切换后无需重启服务

前端 UI

  • /settings 页面:多配置列表、新增/编辑/删除、激活切换
  • 掩码显示 API key(前 4 + 后 4 字符)

兼容性

  • 向后兼容:用户未配置时自动回落到系统 .env
  • 单租户模式AUTH_ENABLED=false 继续支持
  • 所有现有环境变量继续生效

测试

  • ✅ 加密解密正确性测试
  • ✅ 随机 nonce 测试
  • ✅ 掩码逻辑测试

部署步骤

  1. 运行数据库迁移

    alembic upgrade head
  2. 配置加密密钥(可选,推荐):

    # 在 .env 中添加
    LLM_CONFIG_ENCRYPTION_KEY=your-32-byte-hex-key

    如果不配置,会自动从 JWT_SECRET 派生。

  3. 部署服务

    • Dashboard: 访问 /settings 页面配置 API key
    • 无需重启服务,配置切换即时生效

待完善项

  • 实现 revealUserApiKey 中的密码验证逻辑
  • 实现 user-preferences.ts 中的数据库查询/更新
  • 添加审计日志记录揭示操作
  • 添加连接测试功能(验证 API key 是否可用)

截图

(部署后添加设置页面截图)


🦊 Generated with Inalpha

@github-actions

github-actions Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review base: 6ed609b

🤖 GLM-5.2 PR Review

我是 glm-5.2,智谱AI开发的大语言模型。现在开始对 PR 进行评审。

PR 意图:实现多租户 LLM 配置的用户级 API key 管理,将原本硬编码/TODO 的内存数据替换为 PostgreSQL 持久化,并使用原子 SQL 操作解决并发竞态;同时优化 baostock 分钟级 K 线回填的配额限制,新增缓存管理器基础设施。


[major] apps/dashboard/src/lib/db.ts:0 — Next.js 应用直接引入 pg 绕过 ORM,违反架构分层与连接池生命周期管理

Next.js 应用(前端层)直接依赖 pg 并自行管理连接池,与项目典型的分层架构(Next.js → Mastra(TS) → Python services)冲突。前端直连数据库存在以下问题:

  1. 生命周期冲突:Next.js 的 Serverless/Edge 运行时中,进程信号(SIGTERM/SIGINT)不可靠,process.once 注册的清理回调大概率不会执行,导致连接泄漏。
  2. 连接池耗尽:Next.js 冷启动频繁,每次冷启动创建新 Pool(max: 4),在并发请求或 HMR 下极易耗尽 PostgreSQL 的 max_connections
  3. 职责放错层:用户配置的读写应通过后端 BFF/Service 层暴露 API,由前端调用,而非前端直连 DB。

依据:通用原则:设计/架构

[major] apps/dashboard/src/lib/user-preferences.ts:248 — addUserLLMConfig 原子 SQL 在 configs 数组为空时抛出异常,导致首个配置永远无法创建

SQL 中的 jsonb_array_length(COALESCE(preferences->'llm'->'configs', '[]'::jsonb)) = 0 分支使用了 to_jsonb(preferences->'llm'->>'active_config_id')。当用户首次添加配置时,preferences->'llm'->>'active_config_id'nullto_jsonb(null::text) 会生成 JSON 字符串 "null" 而非 JSON null,导致后续逻辑判断失效。
更严重的是,如果 preferences->'llm' 路径不存在,->>'active_config_id' 会抛出 SQL 错误(无法在不存在的 JSON 对象上提取属性),使得第一个配置的添加直接失败。

依据:通用原则:正确性

[major] apps/dashboard/src/lib/user-preferences.ts:327 — deleteUserLLMConfig 的 CTE 更新未生效,导致删除操作静默失败

SQL 使用了 WITH current AS (SELECT ...) UPDATE users ... FROM current WHERE users.subject = $1。在 PostgreSQL 中,这种写法的 UPDATE 语句会更新所有匹配 users.subject = $1 的行,而不管 current 子查询是否返回数据。只要 subject 存在,即使 @> 包含条件不满足(即配置 ID 不存在),result.rowCount 也会是 1。这导致删除不存在的配置时不会抛出异常,而是静默成功,违反了函数的预期契约。

依据:通用原则:正确性

[medium] services/data/src/inalpha_data/api/backfill.py:98 — 分钟级回填限制静默截断用户请求范围,违反 API 契约

当用户请求的 from_ts 超出 max_lookback_days 时,代码直接修改 effective_from_ts 截断请求,仅打印 warning 日志。对于 API 调用方而言,返回的数据时间范围与请求不一致,且无任何错误或响应字段提示,极易导致策略回测基于不完整数据做出错误决策。应拒绝请求(400 Bad Request)或在响应中明确标记数据被截断。

依据:通用原则:契约/兼容

[medium] services/data/src/inalpha_data/cache_manager.py:1 — 新增的 CacheManager 未被任何业务代码集成,且使用同步锁保护异步初始化

  1. 文件头注释明确写了"当前尚未被 backfill/bars API 集成",属于死代码,不应随核心功能 PR 合入。
  2. get_cache_manager() 使用 threading.Lock() 保护初始化,但 inalpha_shared.db.get_db_pool() 返回的是 asyncpg.Pool,其创建过程通常是异步的。在异步上下文中使用同步锁不仅无意义,还可能阻塞事件循环。

依据:通用原则:设计/架构, 通用原则:状态/数据流

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 10, 2026

Copy link
Copy Markdown

Deploying inalpha-web with  Cloudflare Pages  Cloudflare Pages

Latest commit: e76cecc
Status:⚡️  Build in progress...

View logs

mirror29 and others added 5 commits July 13, 2026 13:33
**核心优化**:
- 分钟级强制限制:5m(7天)/15m(14天)/30m(30天)/1h(60天)
- 新增缓存管理器:增量拉取 + 缓存复用

**效果估算**:
- 单次请求:从 8,640 次 → 336 次(节省 96%)
- 多用户场景:缓存复用,0 次请求(节省 100%)
- 整体配额消耗:降低 60-87%

**实现**:
- services/data/api/backfill.py: 分钟级强制截断
- services/data/cache_manager.py: 缓存管理器(增量拉取)

**测试**:
- services/data/test_baostock_batch.py: 验证批量查询支持

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- db.ts: 使用 require('pg') 避免类型声明缺失
- user-preferences.ts: 移除泛型 query 调用(getPool 返回 any)
- cache_manager.py: 移除未使用的 timedelta/UTC
- backfill.py: 移除未使用的 get_cache_manager import

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- backfill.py: 分钟级限制仅对 venue=akshare 生效
- cache_manager.py: 添加 threading.Lock 防止竞态; executemany 返回值改用 len(bars)
- db.ts: 使用 import pg from 'pg' 替代 require('pg')
- test_baostock_batch.py: 移至 tests/ 目录

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- user-preferences.ts: getUserPreferences 从 preferences->'llm' 读取,
  updateUserPreferences 用 jsonb_set 写到 {llm} 键下
- 迁移 0033: 索引改为 ON users (subject) WHERE (preferences->>'llm') IS NOT NULL,
  原 ((preferences->>'llm') IS NOT NULL) 索引的是无意义的布尔值

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- user-preferences.ts: activateUserLLMConfig/addUserLLMConfig/deleteUserLLMConfig 改为原子 SQL
- backfill.py: 用 effective_from_ts 局部变量替代直接 mutate req.from_ts
- db.ts: 简化 server-only import

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mirror29 mirror29 force-pushed the feat/multi-tenant-llm-config branch from 676d43b to e76cecc Compare July 13, 2026 05:57
@mirror29 mirror29 merged commit e6244d0 into main Jul 13, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant