Add portal sign-in flow and DevCloud deployment defaults
All checks were successful
aurask-release / build-and-deploy (push) Successful in 2m11s

This commit is contained in:
Aaron 2026-04-19 20:35:28 +08:00
parent 1ae23d44c1
commit c44746a5a8
19 changed files with 2273 additions and 1485 deletions

View File

@ -1,531 +1,331 @@
# Aurask 技术与运营方案(当前实现路径版)
# Aurask Technical Operations Plan
> 本文档基于当前仓库实现、`AGENTS.md` 约束与 `deploy/k3s/README.md` 部署规划更新。
> 核心方向不变:面向海外用户,用 Langflow 承载模板化工作流,用 AnythingLLM 承载知识库/RAG用 Aurask 自身网关统一做鉴权、套餐、TBU、订单、审计和运维闭环
> 更新日期2026-04-19
> 本文档已按当前代码实现、目录结构、门户登录流与 DevCloud 默认部署方式同步更新
## 0. 当前结论
## 1. 当前阶段定位
Aurask 当前已完成 **可运行 MVP 后端骨架**
Aurask 当前处于 **可运行 MVP / 初版门户** 阶段,已覆盖
- 使用 Python 模块化单体实现首版领域闭环。
- 已具备 `Auth + API Gateway`、`Billing + Quota + TBU Ledger`、`Workflow Orchestrator`、`Knowledge Base`、`USDT-TRC20 Payment`、`Audit` 等核心边界。
- 已按产品职责划分根目录:`api`、`protal`、`manager`、`deploy`。
- 已补充 PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 的桥接配置与接口契约。
- 已提供 CLI 演示与标准库 HTTP 网关。
- 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。
- Python 模块化后端
- 标准库 HTTP Gateway
- 租户、用户、API Key、Session
- `LY SSO` / Google 登录入口
- 首次登录自动创建独立 workspace
- 配额 / TBU / 订单 / 支付闭环
- 工作流模板编排
- AnythingLLM workspace / 文档接入门面
- Langflow / AnythingLLM iframe 门户嵌入
- DevCloud 镜像与 `aurask.xyz` 默认路由对齐
当前阶段定位:
当前版本适合
- **可以用于本地演示、业务流程验证、接口联调和领域模型迭代。**
- **尚不是生产部署版本。**
- 生产化需要依次补齐 PostgreSQL、队列、真实 Langflow/AnythingLLM 接入、k3s manifests、监控告警、备份恢复和支付风控。
- 本地演示
- 接口联调
- 首版门户交互验证
- DevCloud 环境持续迭代
## 1. 项目目标
当前版本尚未完成:
Aurask 是面向海外个人开发者、学生、独立创业者和小团队的轻量级 AI 数字员工工作流平台。
- PostgreSQL 正式 Repository 替换
- Redis Worker / 队列消费者
- Langflow / AnythingLLM 真正生产代理鉴权
- 完整 Ingress / NetworkPolicy / TLS / Observability
目标能力:
- 用户无需自行部署 Langflow / AnythingLLM。
- 用户通过 Aurask 选择审核过的数字员工模板。
- 用户可绑定知识库 Workspace并通过 RAG 支撑业务问答。
- Aurask 统一管理租户、套餐、TBU、订单、支付、审计和成本。
- 首期支持 USDT-TRC20 支付,后续接入 Stripe / Paddle / Lemon Squeezy 等合规渠道。
## 2. 当前代码实现
### 2.1 当前目录结构
## 2. 当前目录结构
```text
api/
README.md # API 与外部组件桥接说明
requests/
aurask-api.http # 前端到后端请求样例
README.md
aurask/
__init__.py # 包入口,导出 CLI main
__main__.py # python -m aurask 入口
app.py # 应用装配与 demo bootstrap
api.py # 标准库 HTTP 网关
bridge_status.py # 组件桥接状态
cli.py # aurask demo / aurask serve
repository.py # MVP JSON 持久化
errors.py # 应用错误类型
ids.py # ID 与时间工具
audit.py # 审计事件
auth.py # 租户、用户、API Key
plans.py # 套餐与商品目录
billing.py # 订单、订阅、权益发放
quota.py # TBU 额度账户、预扣、结算、账本
payments.py # USDT-TRC20 支付匹配
orchestrator.py # 模板工作流编排与 Langflow Runtime 适配
knowledge_base.py # AnythingLLM Workspace / 文档接入适配
bridges/
config.py # PostgreSQL/PGVector/Redis/AnythingLLM/Langflow 环境配置
postgres.py # PostgreSQL schema contract
pgvector.py # PGVector tenant-filtered collection contract
redis_bridge.py # Redis queue/cache/idempotency key contract
anythingllm.py # AnythingLLM API bridge
langflow.py # Langflow Runtime bridge
api.py
app.py
auth.py
billing.py
bridges/
knowledge_base.py
orchestrator.py
payments.py
plans.py
quota.py
repository.py
site_config.py
protal/
index.html # 用户前端使用面板
index.html
main.js
styles.css
manager/
index.html # 管理员前端使用面板
index.html
main.js
styles.css
deploy/
k3s/
README.md
base/
tests/
test_mvp.py # MVP 核心流程测试
deploy/k3s/
README.md # 300 MAU k3s 部署规划
test_auth_sessions.py
test_bridges.py
test_mvp.py
```
### 2.2 当前可运行能力
命令:
```bash
uv run aurask demo --reset
uv run aurask serve --reset --host 127.0.0.1 --port 8080
python -m unittest discover -s tests -v
```
公开 API
- `GET /health`
- `GET /plans`
- `POST /demo/bootstrap`
- `POST /tenants`
鉴权 API
- `GET /quota`
- `GET /workflow-templates`
- `POST /workspaces`
- `POST /documents`
- `POST /orders`
- `POST /payments/match`
- `POST /workflow-runs`
- `GET /workflow-runs/{run_id}`
### 2.3 当前 MVP 简化点
| 能力 | 当前实现 | 生产目标 |
| --- | --- | --- |
| 持久化 | 本地 JSON 文件 | PostgreSQL + PGVector |
| 队列 | 同进程同步执行 | Redis / RabbitMQ / NATS 队列 |
| Langflow | 默认模拟适配层,可用 `LangflowBridge` 接真实服务 | 内部 `ClusterIP` Langflow Runtime Pool |
| AnythingLLM | 默认模拟适配层,可用 `AnythingLLMBridge` 接真实服务 | 内部 `ClusterIP` AnythingLLM API |
| 网关 | Python 标准库 HTTP Server | FastAPI / ASGI + Ingress + HPA |
| 支付 | 人工提交 tx hash 匹配 | TronGrid / Tronscan / 自建节点监听 |
| 观测 | 审计事件写入 store | Prometheus + Loki + Grafana + Alertmanager |
| 部署 | 本地运行 | k3s`aurask-api` + `aurask-worker` |
### 2.4 根目录职责
| 根目录 | 职责 |
| --- | --- |
| `api` | 后端服务、外部组件桥接配置、前端请求契约 |
| `protal` | 用户前端使用面板。目录名按当前需求使用 `protal` 拼写 |
| `manager` | 管理员前端使用面板 |
| `deploy` | k3s 与后续部署配置 |
### 2.5 外部组件桥接
生产桥接通过环境变量启用:
```bash
AURASK_USE_EXTERNAL_BRIDGES=true
AURASK_DATABASE_URL=postgresql://aurask:secret@postgres:5432/aurask
AURASK_REDIS_URL=redis://redis:6379/0
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<secret>
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<secret>
```
桥接模块:
- `api/aurask/bridges/postgres.py`PostgreSQL schema contract。
- `api/aurask/bridges/pgvector.py`PGVector collection 与租户过滤契约。
- `api/aurask/bridges/redis_bridge.py`:队列、缓存、幂等键、限流键约定。
- `api/aurask/bridges/anythingllm.py`AnythingLLM Workspace / 文档入库桥接。
- `api/aurask/bridges/langflow.py`Langflow 安全模板运行桥接。
管理员可通过鉴权接口查看桥接配置状态:
- `GET /admin/bridge-status`
## 3. 目标技术架构
```text
海外用户
Aurask Web / 用户中心 / 支付页
Auth + API Gateway鉴权、限流、套餐、TBU 预扣)
Billing & Quota Service订单、余额、TBU、栏位、存储
Workflow Orchestrator模板、任务队列、运行状态
├─ Langflow Runtime Pool模板工作流执行
├─ AnythingLLM APIWorkspace、文档、RAG、聊天
├─ LLM Proxy模型路由、Token 计量、成本归集)
└─ PostgreSQL / Redis / Object Storage / Vector DB
Observability日志、指标、审计、告警、成本报表
```
核心原则:
- **Aurask 网关是唯一公网入口。**
- `Langflow`、`AnythingLLM`、数据库、向量库、Redis 不直接暴露公网。
- 所有核心实体默认携带 `tenant_id`
- 所有用户侧 Token 统一记为 `TBU`
- 工作流执行前预扣 TBU执行后按实际消耗结算。
- 基础用户只运行审核过的模板,不开放任意代码执行。
## 4. 多租户与安全隔离
### 4.1 领域标识
核心字段:
- `tenant_id`
- `user_id`
- `workspace_id`
- `flow_id`
- `plan_id`
- `order_id`
要求:
- 查询、写入、缓存、对象存储路径、向量检索都必须带租户维度。
- 不允许只按主键查询而不校验 `tenant_id`
- 高付费独立空间可以独立 Namespace / Pod / 数据库凭证,但领域模型保持一致。
### 4.2 Langflow 安全边界
Langflow 只作为工作流编排/执行引擎,不作为多租户安全边界。
基础套餐:
- 只允许模板化工作流。
- 用户不进入 Langflow 全功能 UI。
- 禁止任意 Python 执行。
- 禁止任意自定义组件。
- 禁止无白名单外部网络调用。
生产建议配置:
- `LANGFLOW_AUTO_LOGIN=False`
- `LANGFLOW_FALLBACK_TO_ENV_VAR=False`
- `LANGFLOW_SECRET_KEY` 使用高强度随机值并定期轮换。
- `LANGFLOW_DATABASE_URL` 指向 PostgreSQL。
- Runtime Pod 设置 CPU、内存、pids、超时、临时目录和 egress 白名单。
### 4.3 AnythingLLM 边界
AnythingLLM 负责 Workspace、文档、RAG 和聊天历史。
要求:
- Workspace 与 Aurask `tenant_id` 绑定。
- 普通用户只能访问被显式授权的 Workspace。
- 文档必须先经过 Aurask 上传入口,再转发给 AnythingLLM。
- 上传链路预留大小、类型、病毒扫描、敏感内容检测钩子。
- 向量 metadata 必须包含 `tenant_id``workspace_id`
## 5. 套餐、资源与 TBU
### 5.1 套餐口径
| 套餐/资源 | 价格 | 权益 | 限制 |
| --- | ---: | --- | --- |
| 免费体验 | 0 | 1 个模板工作流、1 个知识库、512MB、7 天 | 限速、低并发、不可调用高成本模型 |
| 基础套餐 | 20 USDT/月 | 3 个工作流、3 个知识库、1GB、900 TBU/月 | 仅模板/受控组件,超额需购买 TBU |
| 新增工作流栏位 | 20 USDT/个/月 | +1 工作流、+1 知识库、+1GB | 不额外赠送 TBU |
| TBU 加购 | 0.15 元/TBU 起 | 1000 TBU 送 1002000 TBU 送 250 | 赠送额度有效期需明确 |
| 高付费独立空间 | 99 USDT/月起或定制报价 | 独立容器/命名空间、更高并发、更大存储 | 保留 egress 白名单与审计 |
### 5.2 TBU 计量流程
1. 用户发起工作流。
2. Orchestrator 按模板、输入长度、RAG TopK、预计输出长度估算 TBU。
3. Quota Service 冻结预计 TBU。
4. 余额不足则拒绝执行或提示充值/降级模型。
5. Runtime 执行工作流。
6. 执行成功后按实际 TBU 结算,释放未用额度。
7. 执行失败时:
- 供应商未计费:全额释放。
- 供应商已计费:按实际消耗扣减并记录失败原因。
### 5.3 当前代码对应
- 套餐定义:`api/aurask/plans.py`
- 额度账户:`api/aurask/quota.py`
- 订单与权益:`api/aurask/billing.py`
- 使用记录:`api/aurask/orchestrator.py`
## 6. 支付与订单闭环
首期使用 `USDT-TRC20`,但必须围绕“可审计订单”设计。
最低闭环:
1. 用户选择套餐或 TBU 包。
2. 系统生成订单号、金额、链、收款地址、过期时间。
3. 用户转账。
4. 系统记录 `tx_hash`、金额、确认数。
5. 金额与订单匹配。
6. 达到确认数后开通权益。
7. 异常金额、重复交易、超时到账进入人工处理队列。
当前代码:
- 订单生成:`BillingService.create_order`
- 支付匹配:`PaymentService.match_trc20_payment`
- 权益发放:`BillingService.fulfill_order`
注意:
- TRC20 手续费由 TRON Bandwidth / Energy / TRX 资源模型决定,不是 BTC 手续费。
- 生产环境必须接入链上监听与幂等校验。
- 需要保留退款、AML、税务与异常处理字段。
## 7. 数据与存储
### 7.1 MVP
当前 MVP 使用 `JsonStore`
- 优点:无依赖、便于演示、可快速验证领域流程。
- 限制:不适合生产、无并发事务、无查询能力、无数据库级隔离。
### 7.2 生产目标
| 类型 | 推荐 |
| --- | --- |
| 主数据库 | PostgreSQL |
| 向量检索 | PGVector 起步,后续 Qdrant / Weaviate |
| 缓存与队列 | Redis 起步,后续按规模引入 RabbitMQ / NATS |
| 对象存储 | 外部 S3 兼容存储 |
| 备份 | PostgreSQL WAL 归档 + 对象存储备份 |
数据要求:
- 核心表必须包含 `tenant_id`
- 对象路径遵循 `tenant_id/workspace_id/document_id`
- 审计不记录完整 Prompt、完整文档正文和原始密钥。
## 8. k3s 生产部署规划
详细方案见:`deploy/k3s/README.md`。
### 8.1 300 MAU 标准
容量假设:
- 月度活跃付费用户:`300`
- 日活高峰:`40-80`
- 同时在线峰值:`15-30`
- 同时工作流执行峰值:`10-20`
- 外部模型 API不在集群内自建 GPU 推理。
推荐拓扑:
| 角色 | 数量 | 建议配置 |
| --- | ---: | --- |
| Public LB | 1 | 云 LB 或 `HAProxy/Keepalived` |
| k3s server | 3 | `4 vCPU / 8GB RAM / 120GB SSD` |
| General worker | 2 | `8 vCPU / 16GB RAM / 200GB SSD` |
| AI/runtime worker | 2 | `8 vCPU / 16GB RAM / 250GB SSD` |
### 8.2 工作负载
| 工作负载 | 副本 | 说明 |
| --- | ---: | --- |
| `aurask-api` | 3 | 网关、鉴权、订单、额度查询 |
| `aurask-worker` | 3 | 工作流编排、异步任务、支付匹配 |
| `aurask-cron` | 1 | 周期任务、过期订单、报表 |
| `langflow-runtime` | 3 | 审核模板执行 |
| `anythingllm` | 2 | Workspace、文档、RAG |
| PostgreSQL / PGVector | 3 | CloudNativePG |
| Redis | 2-3 | 队列、缓存、限流 |
### 8.3 生产化要求
- Traefik / Ingress 只暴露 Aurask Web/API。
- cert-manager 管理 TLS。
- Longhorn 或云块存储承载 PVC。
- PostgreSQL 备份到外部 S3。
- Prometheus、Grafana、Loki、Alertmanager 提供观测。
- NetworkPolicy 默认拒绝,按服务链路放通。
## 9. 运维与可观测性
必须观测:
- API QPS、延迟、错误率。
- 工作流运行数、失败率、排队时长。
- TBU 预扣、消耗、释放、退款。
- 订单创建、支付匹配、异常订单。
- PostgreSQL 连接数、复制延迟、磁盘。
- Redis 内存、队列长度、命中率。
- Langflow / AnythingLLM 请求耗时和失败率。
告警优先级:
- `P1`API 不可用、数据库不可写、支付匹配失败。
- `P2`:工作流失败率升高、队列堆积、文档入库堆积。
- `P3`:磁盘使用率高、备份失败、证书续期异常。
## 10. 运营方案
### 10.1 定位
面向海外小团队的低门槛 AI 数字员工平台:
- 不用自建 Langflow / AnythingLLM。
- 内置客服、知识库问答、邮件助理、表格处理、社媒内容等模板。
- 统一管理知识库、工作流、TBU 和支付。
- 默认英文 UI后续多语言。
### 10.2 获客
优先渠道:
- GitHub 模板工作流与 SDK 示例。
- Medium / Dev.to 英文教程。
- Product Hunt / BetaList。
- Reddit / Discord / Telegram 圈层运营。
- Google Search 小词与 AI 工具导航站赞助。
冷启动预算建议:
- 内容与社群优先。
- 付费投放 `3000-8000 元/月` 起步。
- 每周复盘 CAC、激活率、付费率、退款率。
### 10.3 激活
首个 10 分钟体验路径:
1. 注册。
2. 选择模板。
3. 创建 Workspace。
4. 上传样例文档或使用示例知识库。
5. 运行一次模板工作流。
6. 看到 TBU 消耗、节省时间和下一步建议。
## 11. 财务测算口径
基准假设仍沿用原方案,但与 TBU 口径对齐:
| 项目 | 基准 |
| --- | --- |
| 付费用户 | 300 |
| 基础套餐 | 20 USDT/月 |
| 新增栏位 | 160 个/月 |
| TBU 加购 | 60 人/月,平均 800 TBU |
| 汇率 | 1 USDT ≈ 6.86 元 |
| TBU 成本 | 供应商侧 0.06 元/单位,用户侧 1 TBU = 0.8 供应商单位 |
收入:
| 收入项 | 公式 | 金额 |
| --- | ---: | ---: |
| 基础套餐 | `300 × 20 × 6.86` | 41,160 元 |
| 新增栏位 | `160 × 20 × 6.86` | 21,952 元 |
| TBU 加购 | `60 × 800 × 0.15` | 7,200 元 |
| 月营收 | | 70,312 元 |
成本:
| 成本项 | 金额 |
| --- | ---: |
| 基础套餐 TBU 成本 | 12,960 元 |
| 加购 TBU 成本 | 2,304 元 |
| 基础设施 | 1,500-6,000 元,视 VPS/k3s/云资源而定 |
| 运营获客 | 6,000 元起 |
| 其他工具与支付预留 | 1,100 元起 |
说明:
- 年付用户不能重复计入 MRR。
- 新增栏位不赠送 TBU。
- k3s 高可用部署成本通常高于单机 VPS财务模型要单独列“稳定性成本”。
- `protal/` 目录拼写继续保持既定要求
- `api/` 负责后端、桥接与门户配置
- `deploy/k3s/base/` 现已开始保存基础部署清单
## 12. 落地路线图
## 3. 当前技术架构
### Phase 0已完成
### 3.1 用户访问路径
- Python 模块化单体。
- CLI demo。
- HTTP API。
- TBU 预扣与结算。
- Workspace 与租户绑定。
- USDT 订单与支付匹配骨架。
- MVP 测试。
- k3s 300 MAU 部署规划。
```text
Browser
-> aurask.xyz/signin
-> Aurask Web Protal
-> Aurask API Gateway
-> Session / Quota / Workflow / Knowledge services
-> Langflow / AnythingLLM / PostgreSQL / Redis
```
### Phase 1生产数据层
### 3.2 门户结构
- 将 `JsonStore` 替换为 PostgreSQL repository。
- 引入数据库迁移。
- 增加 Redis 队列与 worker。
- 补齐幂等键与事务边界。
未登录:
### Phase 2真实服务接入
- 渲染 `/signin`
- 提供 `Use LY SSO`
- 提供 `Continue with Google`
- 接真实 Langflow Runtime Pool。
- 接真实 AnythingLLM API。
- 接 LLM Proxy。
- 接 TronGrid / Tronscan 支付监听。
已登录:
### Phase 3k3s 部署
- `Workflows` 页签:内嵌 Langflow
- `Knowledge Base` 页签:内嵌 AnythingLLM
- 右上角个人中心展示用户、租户、workspace、套餐与退出登录
- 编写 `deploy/k3s/base` manifests。
- 拆分 `aurask-api``aurask-worker`
- 部署 PostgreSQL、Redis、Ingress、Secret、NetworkPolicy。
- 接入 Prometheus / Loki / Grafana / Alertmanager。
## 4. 登录与身份模型
### Phase 4商业化 Beta
### 4.1 登录方式
- 英文 UI。
- 模板库。
- 支付异常后台。
- 周报与留存邮件。
- 用户协议、退款规则、AML 记录。
当前支持:
## 13. 验收标准
1. `LY SSO`
- 以按钮形式进入
- 当前实现为配置驱动的本地接入层
- 通过 `AURASK_LY_SSO_USERNAME` / `AURASK_LY_SSO_PASSWORD` 控制
2. Google
- 通过 `AURASK_GOOGLE_CLIENT_ID` 开启
- 首次注册后自动开通独立 tenant + user + workspace
MVP 验收:
### 4.2 Token 模型
- `python -m unittest discover -s tests -v` 通过。
- `uv run aurask demo --reset` 可创建租户、分配套餐、创建 Workspace、运行模板工作流。
- 余额不足时阻止工作流执行。
- TBU 加购支付后能发放额度。
- 文档上传能按租户与 Workspace 记录路径。
当前保留两类 Bearer Token
300 MAU 生产验收:
- `API Key`
- 面向接口调用
- `Session Token`
- 面向门户登录态
- API P95 延迟 `< 500ms`,不含外部模型调用。
- 并发工作流 `10-20` 稳定运行。
- 工作流成功率 `95%+`
- 支付匹配成功率 `99%+`
- PostgreSQL 备份可恢复。
- Langflow / AnythingLLM 不暴露公网。
- NetworkPolicy 默认拒绝并按链路放通。
### 4.3 首次登录自动开通
## 14. 参考文档
对新用户执行:
1. 创建 tenant
2. 创建 owner user
3. 关联外部身份
4. 开通 `free_trial`
5. 创建默认 `Personal Workspace`
6. 签发 session token
## 5. 核心模块现状
### `auth.py`
当前负责:
- tenant / user 创建
- API Key 认证
- external identity 绑定
- session 创建、校验、吊销
### `site_config.py`
当前负责:
- 公网域名
- API 地址
- Langflow / AnythingLLM 嵌入地址
- `LY SSO` / Google 开关
- DevCloud 镜像 / NodePort 默认值
### `knowledge_base.py`
当前负责:
- workspace 创建
- 文档元数据接入
- tenant 维度隔离
### `orchestrator.py`
当前负责:
- 审核模板执行
- TBU 预扣 / 结算
- workspace 绑定校验
## 6. 当前接口规划
### 6.1 公开接口
```text
GET /health
GET /plans
GET /auth/config
POST /auth/ly-sso/login
POST /auth/google/login
POST /demo/bootstrap
POST /tenants
```
### 6.2 登录后接口
```text
GET /auth/session
POST /auth/logout
GET /me
GET /quota
GET /workflow-templates
GET /workspaces
POST /workspaces
POST /documents
POST /orders
POST /payments/match
POST /workflow-runs
GET /workflow-runs/{run_id}
GET /admin/bridge-status
```
## 7. DevCloud 对齐结果
根据 `devcloud` 当前仓库Aurask 已知线上默认值如下:
- API 镜像:`registry.mydevcloud.love/devcloud/aurask-api:latest`
- Web 镜像:`registry.mydevcloud.love/devcloud/aurask-web:latest`
- API NodePort`30091`
- Web NodePort`30090`
- 线上 API 入口:`https://aurask.xyz/api`
- 线上 Web 入口:`https://aurask.xyz`
这些值已经体现在:
- `api/aurask/site_config.py`
- `deploy/k3s/base/aurask-config.yaml`
- `deploy/k3s/base/aurask-api.yaml`
- `deploy/k3s/base/aurask-web.yaml`
## 8. k3s 首版部署路径
### 8.1 当前已落地清单
```text
deploy/k3s/base/namespace.yaml
deploy/k3s/base/aurask-config.yaml
deploy/k3s/base/secrets.example.yaml
deploy/k3s/base/aurask-api.yaml
deploy/k3s/base/aurask-web.yaml
deploy/k3s/base/kustomization.yaml
```
### 8.2 首版目标
`300 MAU` 规划:
- `3` 台 k3s server
- `2` 台 general worker
- `2` 台 runtime worker
基础服务:
- `aurask-api`
- `aurask-web`
- PostgreSQL + PGVector
- Redis
- Langflow
- AnythingLLM
## 9. 运行时与嵌入要求
### 9.1 Langflow
当前门户要求:
- 在登录后以 iframe 嵌入
- 地址由 `AURASK_PUBLIC_LANGFLOW_URL` 控制
后续生产建议:
- 使用 Aurask 网关代理转发
- 加入 session 校验
- 只暴露审核过的模板能力
### 9.2 AnythingLLM
当前门户要求:
- 在登录后以 iframe 嵌入
- 地址由 `AURASK_PUBLIC_ANYTHINGLLM_URL` 控制
后续生产建议:
- 由 Aurask 代理 workspace 身份
- 不对终端用户暴露全局管理后台
## 10. 安全与隔离原则
当前仍坚持以下边界:
- 所有核心实体保留 `tenant_id`
- workspace 必须绑定 tenant
- workflow 执行前必须预扣 TBU
- 文档接入路径保留 `tenant_id/workspace_id/document_id`
- 密码、API Key、OAuth client 不写入 Git
针对当前 iframe 方案,后续必须补强:
- Langflow / AnythingLLM 代理鉴权
- session 到 runtime 的信任链
- CSP / X-Frame-Options / SameSite 策略
## 11. 验证状态
当前已通过:
```text
python -m unittest discover -s tests -v
```
已覆盖:
- MVP 核心业务闭环
- 桥接配置契约
- `LY SSO` 登录
- Google 首次登录建 workspace
- Session 认证
## 12. 下一阶段实施顺序
建议按以下顺序继续推进:
1. 用 PostgreSQL Repository 替换 `JsonStore`
2. 引入 Redis 队列与 `aurask-worker`
3. 为 Langflow / AnythingLLM 增加网关代理层
4. 补齐 `deploy/k3s/base` 的 runtime / data / ingress / secrets 清单
5. 增加观测与告警
6. 增加支付风控与异常订单后台
## 13. 参考文件
- `AGENTS.md`
- `README.md`
- `api/README.md`
- `protal/README.md`
- `deploy/k3s/README.md`
- k3s HA<https://docs.k3s.io/datastore/ha-embedded>
- Longhorn<https://longhorn.io/docs/latest/deploy/install/>
- cert-manager<https://cert-manager.io/docs/installation/>
- CloudNativePG<https://cloudnative-pg.io/documentation/current/>
- AnythingLLM System Requirements<https://docs.anythingllm.com/installation-docker/system-requirements>
- Langflow Security<https://docs.langflow.org/security>
- `tests/test_auth_sessions.py`

180
README.md
View File

@ -1,66 +1,60 @@
## Aurask
Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,并按产品边界拆分为 `api`、`protal`、`manager`、`deploy` 四个根目录。
Aurask 当前是一个按产品边界拆分的首版实现,根目录保持为四个主要目录:
- `Auth + API Gateway`
- `Billing + Quota + TBU Ledger`
- `Workflow Orchestrator`
- `Langflow` 模板化运行适配层
- `AnythingLLM` Workspace / 文档接入适配层
- `USDT-TRC20` 订单与支付匹配
- `Audit / Usage / Observability` 基础留痕
- `api/`:后端网关、会话登录、配额/TBU、工作流、知识库桥接
- `protal/`:用户门户,保留既定目录拼写
- `manager/`:管理员面板
- `deploy/`k3s 与 DevCloud 部署配置
当前实现是 **MVP 版本**
当前版本已经补齐了用户门户登录闭环:
- 使用本地 JSON 文件持久化,便于开发和演示
- 保留了租户、订单、额度、模板、知识库、支付等核心领域边界
- 后续可以自然迁移到 PostgreSQL、任务队列、Runtime Pool 和真实外部服务
- `/signin` 风格化登录页
- `LY SSO` 登录入口
- Google 首次注册/登录后自动创建用户独立 workspace
- 登录后双标签页工作台:
- `Workflows` 内嵌 `Langflow`
- `Knowledge Base` 内嵌 `AnythingLLM`
- 右上角个人中心与登出
### Quick start
## 本地运行
启动 API
```powershell
$env:PYTHONPATH='api'
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
```
运行演示:
```bash
uv run aurask demo --reset
```
启动本地网关:
```bash
uv run aurask serve --reset --host 127.0.0.1 --port 8080
```
如果本机未安装 `uv`,可用:
```powershell
$env:PYTHONPATH='api'
py -3 -m aurask demo --reset
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
```
### Demo flow
门户静态文件位于 `protal/`,部署到静态 Web 服务后即可使用。开发时可直接由本地静态服务器打开 `protal/index.html`
`aurask demo` 会自动完成:
1. 创建租户与 owner 用户
2. 分配基础套餐
3. 创建默认知识库 Workspace
4. 执行一个安全模板工作流
5. 输出 API Key、工作流结果和剩余额度
### HTTP API
## 当前接口
公开接口:
- `GET /health`
- `GET /plans`
- `GET /auth/config`
- `POST /auth/ly-sso/login`
- `POST /auth/google/login`
- `POST /demo/bootstrap`
- `POST /tenants`
鉴权后接口:
- `GET /auth/session`
- `POST /auth/logout`
- `GET /me`
- `GET /quota`
- `GET /workflow-templates`
- `GET /workspaces`
- `POST /workspaces`
- `POST /documents`
- `POST /orders`
@ -69,72 +63,72 @@ py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
- `GET /workflow-runs/{run_id}`
- `GET /admin/bridge-status`
鉴权方式
Aurask 现在同时支持两类 Bearer Token
```http
Authorization: Bearer <api_key>
- API Key用于原有 API/集成访问
- Session Token用于门户登录态
## 关键环境变量
门户与登录:
```text
AURASK_PUBLIC_BASE_URL=https://aurask.xyz
AURASK_PUBLIC_API_BASE_URL=https://aurask.xyz/api
AURASK_PUBLIC_LANGFLOW_URL=https://aurask.xyz/runtime/langflow/
AURASK_PUBLIC_ANYTHINGLLM_URL=https://aurask.xyz/runtime/anythingllm/
AURASK_LY_SSO_ENABLED=true
AURASK_LY_SSO_USERNAME=ly-xujian1
AURASK_LY_SSO_PASSWORD=<inject-secret>
AURASK_GOOGLE_ENABLED=true
AURASK_GOOGLE_CLIENT_ID=<google-client-id>
AURASK_SESSION_TTL_DAYS=7
```
### Example
运行时桥接:
1. 创建演示租户:
```bash
curl -X POST http://127.0.0.1:8080/demo/bootstrap ^
-H "Content-Type: application/json" ^
-d "{}"
```text
AURASK_USE_EXTERNAL_BRIDGES=true
AURASK_DATABASE_URL=postgresql://aurask:<password>@postgres.aurask-data.svc.cluster.local:5432/aurask
AURASK_REDIS_URL=redis://redis.aurask-data.svc.cluster.local:6379/0
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<anythingllm-api-key>
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<langflow-api-key>
```
2. 使用返回的 `api_key` 查询模板:
## DevCloud 默认值
```bash
curl http://127.0.0.1:8080/workflow-templates ^
-H "Authorization: Bearer <api_key>"
已按 `devcloud` 仓库当前配置对齐默认镜像与路由:
- API 镜像:`registry.mydevcloud.love/devcloud/aurask-api:latest`
- Web 镜像:`registry.mydevcloud.love/devcloud/aurask-web:latest`
- `https://aurask.xyz/api/*``45.113.2.55:30091`
- `https://aurask.xyz/*``45.113.2.55:30090`
对应的基础 k3s 清单位于:
- `deploy/k3s/base/aurask-api.yaml`
- `deploy/k3s/base/aurask-web.yaml`
- `deploy/k3s/base/aurask-config.yaml`
- `deploy/k3s/base/secrets.example.yaml`
## 测试
```powershell
$env:PYTHONPATH='api'
cmd /c "py -3 -m unittest discover -s tests -v"
```
3. 创建 Workspace
当前已覆盖
```bash
curl -X POST http://127.0.0.1:8080/workspaces ^
-H "Authorization: Bearer <api_key>" ^
-H "Content-Type: application/json" ^
-d "{\"name\":\"Support KB\"}"
```
- MVP 业务闭环
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约
- `LY SSO` / Google 登录与 session
4. 运行模板工作流:
## 相关文档
```bash
curl -X POST http://127.0.0.1:8080/workflow-runs ^
-H "Authorization: Bearer <api_key>" ^
-H "Content-Type: application/json" ^
-d "{\"template_id\":\"tpl_email_assistant\",\"inputs\":{\"topic\":\"refund policy reply\"}}"
```
### Testing
```bash
python -m unittest discover -s tests -v
```
### Deployment
- `deploy/k3s/README.md`: 面向 `300` 名月度活跃用户的 `k3s` 部署方案
- `api/README.md`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置
- `api/requests/aurask-api.http`: 前端到后端请求样例
### Project layout
- `AGENTS.md`: 项目级实现约束
- `Aurask_Technical_Operations_Plan.md`: 技术与运营方案
- `api/aurask/app.py`: 应用装配
- `api/aurask/api.py`: HTTP 网关
- `api/aurask/bridges/`: PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 桥接配置
- `api/aurask/auth.py`: 租户、用户与 API Key
- `api/aurask/billing.py`: 套餐、订单与权益发放
- `api/aurask/quota.py`: TBU 预扣、结算与额度账本
- `api/aurask/orchestrator.py`: 模板工作流编排
- `api/aurask/knowledge_base.py`: Workspace 与文档接入
- `api/aurask/payments.py`: USDT-TRC20 支付匹配
- `protal/`: 用户前端使用面板
- `manager/`: 管理员前端使用面板
- `tests/`: 单元测试
- `api/README.md`
- `protal/README.md`
- `deploy/k3s/README.md`
- `Aurask_Technical_Operations_Plan.md`

View File

@ -1,50 +1,129 @@
# Aurask API
This directory contains the Python backend package and bridge configuration for:
`api/` 包含 Aurask 后端服务与外部组件桥接配置。
- PostgreSQL
- PGVector
- Redis
- AnythingLLM
- Langflow
- Frontend-to-backend request contracts
## 当前实现范围
## Runtime modes
- HTTP 网关:`api/aurask/api.py`
- 应用装配:`api/aurask/app.py`
- 租户、用户、API Key、Session`api/aurask/auth.py`
- 套餐、订单、支付、TBU`api/aurask/plans.py`、`api/aurask/billing.py`、`api/aurask/quota.py`
- 工作流编排:`api/aurask/orchestrator.py`
- AnythingLLM workspace / 文档接入:`api/aurask/knowledge_base.py`
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接:`api/aurask/bridges/`
- 门户站点配置:`api/aurask/site_config.py`
Default local mode uses `JsonStore` and simulated bridges:
## 认证模式
```bash
uv run aurask demo --reset
uv run aurask serve --reset --host 127.0.0.1 --port 8080
Aurask 现在支持两类 Bearer Token
1. API Key
- 适用于接口集成、自动化脚本
- 由创建租户/用户或 demo bootstrap 返回
2. Session Token
- 适用于 `protal/` 登录态
- 由 `LY SSO` 或 Google 登录接口签发
## 公开接口
```text
GET /health
GET /plans
GET /auth/config
POST /auth/ly-sso/login
POST /auth/google/login
POST /demo/bootstrap
POST /tenants
```
Production bridge mode can be enabled with:
## 鉴权接口
```bash
$env:AURASK_USE_EXTERNAL_BRIDGES="true"
$env:AURASK_DATABASE_URL="postgresql://aurask:secret@postgres:5432/aurask"
$env:AURASK_REDIS_URL="redis://redis:6379/0"
$env:AURASK_ANYTHINGLLM_BASE_URL="http://anythingllm.aurask-runtime.svc.cluster.local:3001"
$env:AURASK_ANYTHINGLLM_API_KEY="<secret>"
$env:AURASK_LANGFLOW_BASE_URL="http://langflow-runtime.aurask-runtime.svc.cluster.local:7860"
$env:AURASK_LANGFLOW_API_KEY="<secret>"
uv run aurask serve --host 0.0.0.0 --port 8080
```text
GET /auth/session
POST /auth/logout
GET /me
GET /quota
GET /workflow-templates
GET /workspaces
POST /workspaces
POST /documents
POST /orders
POST /payments/match
POST /workflow-runs
GET /workflow-runs/{run_id}
GET /admin/bridge-status
```
## Bridge modules
## 门户与嵌入配置
- `aurask.bridges.config`: environment-driven configuration
- `aurask.bridges.postgres`: PostgreSQL schema contract
- `aurask.bridges.pgvector`: PGVector tenant-filtered collection contract
- `aurask.bridges.redis_bridge`: Redis queue/cache/idempotency key contract
- `aurask.bridges.anythingllm`: AnythingLLM API bridge
- `aurask.bridges.langflow`: Langflow runtime bridge
`GET /auth/config` 会向前端返回:
## Admin bridge status
- 公网站点地址
- API 地址
- `LY SSO` / Google 开关
- Langflow / AnythingLLM iframe 地址
- DevCloud 对齐后的镜像与 NodePort 默认值
Authenticated admin status endpoint:
## 环境变量
```http
GET /admin/bridge-status
Authorization: Bearer <api_key>
### 门户 / 登录
```text
AURASK_PUBLIC_BASE_URL=https://aurask.xyz
AURASK_PUBLIC_API_BASE_URL=https://aurask.xyz/api
AURASK_PUBLIC_LANGFLOW_URL=https://aurask.xyz/runtime/langflow/
AURASK_PUBLIC_ANYTHINGLLM_URL=https://aurask.xyz/runtime/anythingllm/
AURASK_LY_SSO_ENABLED=true
AURASK_LY_SSO_LOGIN_URL=
AURASK_LY_SSO_USERNAME=ly-xujian1
AURASK_LY_SSO_PASSWORD=<ly-sso-password>
AURASK_LY_SSO_EMAIL_DOMAIN=ly.szlanyou.local
AURASK_GOOGLE_ENABLED=true
AURASK_GOOGLE_CLIENT_ID=<google-client-id>
AURASK_GOOGLE_TOKENINFO_URL=https://oauth2.googleapis.com/tokeninfo
AURASK_GOOGLE_ALLOW_UNVERIFIED_ID_TOKEN=false
AURASK_SESSION_TTL_DAYS=7
AURASK_CORS_ALLOW_ORIGIN=https://aurask.xyz
```
### 运行时桥接
```text
AURASK_USE_EXTERNAL_BRIDGES=true
AURASK_DATABASE_URL=postgresql://aurask:<password>@postgres.aurask-data.svc.cluster.local:5432/aurask
AURASK_POSTGRES_MIN_CONNECTIONS=1
AURASK_POSTGRES_MAX_CONNECTIONS=10
AURASK_PGVECTOR_TABLE=aurask_vectors
AURASK_PGVECTOR_DIMENSION=1536
AURASK_REDIS_URL=redis://redis.aurask-data.svc.cluster.local:6379/0
AURASK_REDIS_WORKFLOW_QUEUE=aurask:workflow-runs
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<anythingllm-api-key>
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<langflow-api-key>
```
## DevCloud 默认值
已内置以下默认值以对齐 `devcloud` 当前部署:
```text
AURASK_DEVCLOUD_API_IMAGE=registry.mydevcloud.love/devcloud/aurask-api:latest
AURASK_DEVCLOUD_WEB_IMAGE=registry.mydevcloud.love/devcloud/aurask-web:latest
AURASK_DEVCLOUD_API_NODE_URL=http://45.113.2.55:30091
AURASK_DEVCLOUD_WEB_NODE_URL=http://45.113.2.55:30090
```
## 示例请求
接口示例文件:
- `api/requests/aurask-api.http`
## 本地验证
```powershell
$env:PYTHONPATH='api'
py -3 -m aurask serve --reset --host 127.0.0.1 --port 8080
cmd /c "py -3 -m unittest discover -s tests -v"
```

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import json
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from typing import Any
from urllib.parse import urlparse
@ -28,18 +29,35 @@ def make_handler(app: AuraskApp):
def do_POST(self) -> None:
self._handle("POST")
def do_OPTIONS(self) -> None:
self._handle("OPTIONS")
def log_message(self, format: str, *args: Any) -> None:
app.audit.record("gateway.access", summary=format % args)
def _handle(self, method: str) -> None:
path = urlparse(self.path).path.rstrip("/") or "/"
try:
if method == "OPTIONS":
self._send(204, {})
return
if method == "GET" and path == "/health":
self._send(200, {"status": "ok", "service": "aurask"})
return
if method == "GET" and path == "/plans":
self._send(200, app.billing.list_plans())
return
if method == "GET" and path == "/auth/config":
self._send(200, app.public_auth_config())
return
if method == "POST" and path == "/auth/ly-sso/login":
body = self._read_json()
self._send(200, app.login_with_ly_sso(username=body.get("username", ""), password=body.get("password", "")))
return
if method == "POST" and path == "/auth/google/login":
body = self._read_json()
self._send(200, app.login_with_google(id_token=body.get("id_token", "")))
return
if method == "POST" and path == "/demo/bootstrap":
body = self._read_json()
self._send(201, app.bootstrap_demo(tenant_name=body.get("tenant_name", "Aurask Demo"), email=body.get("email", "owner@example.com")))
@ -56,6 +74,12 @@ def make_handler(app: AuraskApp):
tenant_id = context["tenant"]["id"]
user_id = context["user"]["id"]
if method == "GET" and path in {"/auth/session", "/me"}:
self._send(200, app.session_profile(context))
return
if method == "POST" and path == "/auth/logout":
self._send(200, app.auth.revoke_session(self.headers.get("Authorization")))
return
if method == "GET" and path == "/admin/bridge-status":
self._send(200, bridge_status())
return
@ -65,6 +89,9 @@ def make_handler(app: AuraskApp):
if method == "GET" and path == "/workflow-templates":
self._send(200, {"templates": app.orchestrator.list_templates()})
return
if method == "GET" and path == "/workspaces":
self._send(200, {"workspaces": app.list_workspaces(tenant_id)})
return
if method == "POST" and path == "/workspaces":
body = self._read_json()
self._send(201, app.knowledge.create_workspace(tenant_id, user_id, body.get("name", "")))
@ -132,13 +159,19 @@ def make_handler(app: AuraskApp):
if not length:
return {}
payload = self.rfile.read(length).decode("utf-8")
return json.loads(payload)
decoded = json.loads(payload)
if not isinstance(decoded, dict):
return {}
return decoded
def _send(self, status: int, payload: dict) -> None:
def _send(self, status: int, payload: Any) -> None:
encoded = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(encoded)))
self.send_header("Access-Control-Allow-Origin", os.getenv("AURASK_CORS_ALLOW_ORIGIN", "*"))
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type, Accept")
self.end_headers()
self.wfile.write(encoded)

View File

@ -2,9 +2,13 @@
from __future__ import annotations
import base64
import json
import os
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import urlencode
from urllib.request import urlopen
from aurask.audit import AuditService
from aurask.auth import AuthService
@ -12,12 +16,14 @@ from aurask.billing import BillingService
from aurask.bridges.anythingllm import AnythingLLMBridge
from aurask.bridges.config import BridgeConfig
from aurask.bridges.langflow import LangflowBridge
from aurask.errors import AuthError, ValidationError
from aurask.knowledge_base import KnowledgeBaseService
from aurask.orchestrator import WorkflowOrchestrator
from aurask.payments import PaymentService
from aurask.plans import seed_plan_catalog
from aurask.quota import QuotaService
from aurask.repository import JsonStore
from aurask.site_config import SiteConfig
@dataclass
@ -30,6 +36,7 @@ class AuraskApp:
payments: PaymentService
knowledge: KnowledgeBaseService
orchestrator: WorkflowOrchestrator
site_config: SiteConfig
def bootstrap_demo(self, *, tenant_name: str = "Aurask Demo", email: str = "owner@example.com") -> dict:
tenant = self.auth.create_tenant(tenant_name)
@ -44,6 +51,165 @@ class AuraskApp:
"quota": self.quota.get_account(tenant["id"]),
}
def public_auth_config(self) -> dict:
return self.site_config.public_dict()
def login_with_ly_sso(self, *, username: str, password: str) -> dict:
if not self.site_config.ly_sso_enabled:
raise AuthError("LY SSO is not enabled")
if not self.site_config.ly_sso_password:
raise AuthError("LY SSO password secret is not configured")
if username != self.site_config.ly_sso_username or password != self.site_config.ly_sso_password:
raise AuthError("invalid LY SSO credentials")
email = os.getenv("AURASK_LY_SSO_EMAIL", f"{username}@{self.site_config.ly_sso_email_domain}")
return self._sign_in_external_identity(
provider="ly_sso",
subject=username,
email=email,
display_name=username,
avatar_url="",
)
def login_with_google(self, *, id_token: str) -> dict:
claims = self._verify_google_id_token(id_token)
email = claims.get("email", "").strip().lower()
if "@" not in email:
raise AuthError("Google account email is required")
subject = claims.get("sub") or email
return self._sign_in_external_identity(
provider="google",
subject=subject,
email=email,
display_name=claims.get("name") or email.split("@", 1)[0],
avatar_url=claims.get("picture") or "",
)
def session_profile(self, context: dict) -> dict:
tenant = context["tenant"]
user = context["user"]
workspace = self._ensure_default_workspace(tenant["id"], user["id"])
payload = self._profile_payload(tenant, user, workspace)
if context.get("session"):
payload["session"] = {
"expires_at": context["session"]["expires_at"],
"provider": context["session"]["provider"],
}
return payload
def list_workspaces(self, tenant_id: str) -> list[dict]:
return [workspace for workspace in self.store.list("workspaces") if workspace["tenant_id"] == tenant_id and workspace["status"] == "active"]
def _sign_in_external_identity(self, *, provider: str, subject: str, email: str, display_name: str, avatar_url: str) -> dict:
identity = self.auth.get_external_identity(provider, subject)
is_new_user = False
if identity:
user = self.store.get("users", identity["user_id"])
tenant = self.store.get("tenants", identity["tenant_id"])
if not user or not tenant:
raise AuthError("linked identity is not available")
else:
user = self.auth.find_user_by_email(email)
if user:
tenant = self.store.get("tenants", user["tenant_id"])
if not tenant:
raise AuthError("user tenant is not available")
else:
tenant = self.auth.create_tenant(f"{display_name}'s Workspace")
user = self.auth.create_user(tenant["id"], email, display_name=display_name, avatar_url=avatar_url)
self.billing.grant_plan_without_payment(tenant["id"], "free_trial", source=f"{provider}_signup")
is_new_user = True
self.auth.link_external_identity(provider, subject, tenant["id"], user["id"], email=email)
self._ensure_quota_account(tenant["id"], provider)
self._refresh_user_profile(user, display_name, avatar_url)
workspace = self._ensure_default_workspace(tenant["id"], user["id"])
session = self.auth.create_session(tenant["id"], user["id"], provider=provider, ttl_days=self.site_config.session_ttl_days)
self.audit.record(
"auth.external_login",
tenant_id=tenant["id"],
user_id=user["id"],
metadata={"provider": provider, "is_new_user": is_new_user},
)
payload = self._profile_payload(tenant, self.store.get("users", user["id"]) or user, workspace)
payload.update(
{
"token": session["token"],
"token_type": "Bearer",
"expires_at": session["expires_at"],
"is_new_user": is_new_user,
}
)
return payload
def _ensure_quota_account(self, tenant_id: str, source: str) -> None:
if not self.store.get("quota_accounts", tenant_id):
self.billing.grant_plan_without_payment(tenant_id, "free_trial", source=f"{source}_default")
def _refresh_user_profile(self, user: dict, display_name: str, avatar_url: str) -> None:
changed = False
if display_name and user.get("display_name") != display_name:
user["display_name"] = display_name
changed = True
if avatar_url and user.get("avatar_url") != avatar_url:
user["avatar_url"] = avatar_url
changed = True
if changed:
self.store.put("users", user["id"], user)
def _ensure_default_workspace(self, tenant_id: str, user_id: str) -> dict:
workspaces = self.list_workspaces(tenant_id)
if workspaces:
return workspaces[0]
return self.knowledge.create_workspace(tenant_id, user_id, "Personal Workspace")
def _profile_payload(self, tenant: dict, user: dict, workspace: dict) -> dict:
return {
"user": {
"id": user["id"],
"tenant_id": user["tenant_id"],
"email": user["email"],
"display_name": user.get("display_name") or user["email"].split("@", 1)[0],
"avatar_url": user.get("avatar_url", ""),
"role": user["role"],
"status": user["status"],
"api_key_preview": user.get("api_key_preview", ""),
"created_at": user["created_at"],
},
"tenant": tenant,
"workspace": workspace,
"quota": self.quota.get_account(tenant["id"]),
"config": self.public_auth_config(),
}
def _verify_google_id_token(self, id_token: str) -> dict:
if not id_token:
raise ValidationError("Google id_token is required")
if self.site_config.google_allow_unverified_id_token:
return self._decode_jwt_payload(id_token)
if not self.site_config.google_enabled or not self.site_config.google_client_id:
raise AuthError("Google OAuth client is not configured")
query = urlencode({"id_token": id_token})
try:
with urlopen(f"{self.site_config.google_tokeninfo_url}?{query}", timeout=8) as response:
claims = json.loads(response.read().decode("utf-8"))
except Exception as exc:
raise AuthError("unable to verify Google id_token") from exc
if claims.get("aud") != self.site_config.google_client_id:
raise AuthError("Google token audience mismatch")
if str(claims.get("email_verified", "")).lower() not in {"true", "1"}:
raise AuthError("Google account email is not verified")
return claims
def _decode_jwt_payload(self, id_token: str) -> dict:
parts = id_token.split(".")
if len(parts) < 2:
raise AuthError("invalid Google id_token format")
payload = parts[1] + "=" * (-len(parts[1]) % 4)
try:
return json.loads(base64.urlsafe_b64decode(payload.encode("utf-8")).decode("utf-8"))
except Exception as exc:
raise AuthError("invalid Google id_token payload") from exc
def create_app(data_path: str | Path | None = None, *, reset: bool = False, use_external_bridges: bool | None = None) -> AuraskApp:
store = JsonStore(data_path)
@ -52,6 +218,7 @@ def create_app(data_path: str | Path | None = None, *, reset: bool = False, use_
if use_external_bridges is None:
use_external_bridges = os.getenv("AURASK_USE_EXTERNAL_BRIDGES", "false").lower() in {"1", "true", "yes"}
bridge_config = BridgeConfig.from_env()
site_config = SiteConfig.from_env()
audit = AuditService(store)
auth = AuthService(store, audit)
quota = QuotaService(store, audit)
@ -72,4 +239,5 @@ def create_app(data_path: str | Path | None = None, *, reset: bool = False, use_
payments=payments,
knowledge=knowledge,
orchestrator=orchestrator,
site_config=site_config,
)

View File

@ -3,6 +3,7 @@
from __future__ import annotations
import secrets
from datetime import UTC, datetime, timedelta
from aurask.audit import AuditService
from aurask.errors import AuthError, ForbiddenError, NotFoundError, ValidationError
@ -29,7 +30,15 @@ class AuthService:
self.audit.record("tenant.created", tenant_id=tenant["id"], resource_type="tenant", resource_id=tenant["id"])
return tenant
def create_user(self, tenant_id: str, email: str, *, role: str = "owner") -> dict:
def create_user(
self,
tenant_id: str,
email: str,
*,
role: str = "owner",
display_name: str | None = None,
avatar_url: str | None = None,
) -> dict:
self._require_tenant(tenant_id)
if "@" not in email:
raise ValidationError("valid email is required")
@ -38,6 +47,8 @@ class AuthService:
"id": new_id("user"),
"tenant_id": tenant_id,
"email": email,
"display_name": display_name or email.split("@", 1)[0],
"avatar_url": avatar_url or "",
"role": role,
"status": "active",
"api_key_preview": api_key[:10],
@ -48,20 +59,78 @@ class AuthService:
self.audit.record("user.created", tenant_id=tenant_id, user_id=user["id"], resource_type="user", resource_id=user["id"])
return {**user, "api_key": api_key}
def find_user_by_email(self, email: str) -> dict | None:
normalized_email = email.strip().lower()
for user in self.store.list("users"):
if user.get("email", "").lower() == normalized_email:
return user
return None
def get_external_identity(self, provider: str, subject: str) -> dict | None:
return self.store.get("external_identities", self._identity_key(provider, subject))
def link_external_identity(self, provider: str, subject: str, tenant_id: str, user_id: str, *, email: str) -> dict:
self.require_tenant_user(tenant_id, user_id)
identity = {
"id": self._identity_key(provider, subject),
"provider": provider,
"subject": subject,
"tenant_id": tenant_id,
"user_id": user_id,
"email": email,
"linked_at": now_iso(),
}
self.store.put("external_identities", identity["id"], identity)
self.audit.record(
"auth.identity_linked",
tenant_id=tenant_id,
user_id=user_id,
resource_type="external_identity",
resource_id=identity["id"],
metadata={"provider": provider},
)
return identity
def create_session(self, tenant_id: str, user_id: str, *, provider: str, ttl_days: int = 7) -> dict:
self.require_tenant_user(tenant_id, user_id)
token = f"sess_{secrets.token_urlsafe(32)}"
expires_at = (datetime.now(UTC) + timedelta(days=ttl_days)).isoformat()
session = {
"token": token,
"tenant_id": tenant_id,
"user_id": user_id,
"provider": provider,
"status": "active",
"created_at": now_iso(),
"expires_at": expires_at,
}
self.store.put("sessions", token, session)
self.audit.record("auth.session_created", tenant_id=tenant_id, user_id=user_id, metadata={"provider": provider})
return session
def revoke_session(self, authorization_header: str | None) -> dict:
token = self._bearer_token(authorization_header)
session = self.store.get("sessions", token)
if not session:
return {"revoked": False}
session["status"] = "revoked"
session["revoked_at"] = now_iso()
self.store.put("sessions", token, session)
self.audit.record("auth.session_revoked", tenant_id=session["tenant_id"], user_id=session["user_id"])
return {"revoked": True}
def authenticate(self, authorization_header: str | None) -> dict:
if not authorization_header or not authorization_header.startswith("Bearer "):
raise AuthError("missing bearer token")
api_key = authorization_header.removeprefix("Bearer ").strip()
key_record = self.store.get("api_keys", api_key)
bearer_token = self._bearer_token(authorization_header)
key_record = self.store.get("api_keys", bearer_token)
if not key_record:
raise AuthError("invalid bearer token")
return self._authenticate_session(bearer_token)
user = self.store.get("users", key_record["user_id"])
if not user or user.get("status") != "active":
raise AuthError("inactive user")
tenant = self.store.get("tenants", user["tenant_id"])
if not tenant or tenant.get("status") != "active":
raise AuthError("inactive tenant")
return {"tenant": tenant, "user": user, "api_key": api_key}
return {"tenant": tenant, "user": user, "api_key": bearer_token, "token_type": "api_key"}
def require_tenant_user(self, tenant_id: str, user_id: str) -> None:
user = self.store.get("users", user_id)
@ -75,3 +144,35 @@ class AuthService:
if not tenant:
raise NotFoundError("tenant not found")
return tenant
def _authenticate_session(self, token: str) -> dict:
session = self.store.get("sessions", token)
if not session:
raise AuthError("invalid bearer token")
if session.get("status") != "active":
raise AuthError("inactive session")
expires_at = datetime.fromisoformat(session["expires_at"])
if expires_at < datetime.now(UTC):
session["status"] = "expired"
self.store.put("sessions", token, session)
raise AuthError("expired session")
user = self.store.get("users", session["user_id"])
if not user or user.get("status") != "active":
raise AuthError("inactive user")
tenant = self.store.get("tenants", session["tenant_id"])
if not tenant or tenant.get("status") != "active":
raise AuthError("inactive tenant")
return {"tenant": tenant, "user": user, "session": session, "token_type": "session"}
def _bearer_token(self, authorization_header: str | None) -> str:
if not authorization_header or not authorization_header.startswith("Bearer "):
raise AuthError("missing bearer token")
token = authorization_header.removeprefix("Bearer ").strip()
if not token:
raise AuthError("missing bearer token")
return token
def _identity_key(self, provider: str, subject: str) -> str:
if not provider or not subject:
raise ValidationError("external identity provider and subject are required")
return f"{provider}:{subject}"

View File

@ -20,6 +20,8 @@ DEFAULT_DATA: dict[str, Any] = {
"tenants": {},
"users": {},
"api_keys": {},
"external_identities": {},
"sessions": {},
"plans": {},
"subscriptions": {},
"quota_accounts": {},

84
api/aurask/site_config.py Normal file
View File

@ -0,0 +1,84 @@
"""Public site, authentication, and embed configuration."""
from __future__ import annotations
import os
from dataclasses import dataclass
def _enabled(name: str, default: bool = False) -> bool:
fallback = "true" if default else "false"
return os.getenv(name, fallback).lower() in {"1", "true", "yes", "on"}
@dataclass(frozen=True)
class SiteConfig:
public_base_url: str
public_api_base_url: str
langflow_embed_url: str
anythingllm_embed_url: str
ly_sso_enabled: bool
ly_sso_login_url: str
ly_sso_username: str
ly_sso_password: str
ly_sso_email_domain: str
google_enabled: bool
google_client_id: str
google_tokeninfo_url: str
google_allow_unverified_id_token: bool
session_ttl_days: int
devcloud_api_image: str
devcloud_web_image: str
devcloud_api_node_url: str
devcloud_web_node_url: str
@classmethod
def from_env(cls) -> "SiteConfig":
public_base_url = os.getenv("AURASK_PUBLIC_BASE_URL", "https://aurask.xyz").rstrip("/")
return cls(
public_base_url=public_base_url,
public_api_base_url=os.getenv("AURASK_PUBLIC_API_BASE_URL", f"{public_base_url}/api").rstrip("/"),
langflow_embed_url=os.getenv("AURASK_PUBLIC_LANGFLOW_URL", f"{public_base_url}/runtime/langflow/"),
anythingllm_embed_url=os.getenv("AURASK_PUBLIC_ANYTHINGLLM_URL", f"{public_base_url}/runtime/anythingllm/"),
ly_sso_enabled=_enabled("AURASK_LY_SSO_ENABLED", True),
ly_sso_login_url=os.getenv("AURASK_LY_SSO_LOGIN_URL", ""),
ly_sso_username=os.getenv("AURASK_LY_SSO_USERNAME", "ly-xujian1"),
ly_sso_password=os.getenv("AURASK_LY_SSO_PASSWORD", ""),
ly_sso_email_domain=os.getenv("AURASK_LY_SSO_EMAIL_DOMAIN", "ly.szlanyou.local"),
google_enabled=_enabled("AURASK_GOOGLE_ENABLED", bool(os.getenv("AURASK_GOOGLE_CLIENT_ID", ""))),
google_client_id=os.getenv("AURASK_GOOGLE_CLIENT_ID", ""),
google_tokeninfo_url=os.getenv("AURASK_GOOGLE_TOKENINFO_URL", "https://oauth2.googleapis.com/tokeninfo"),
google_allow_unverified_id_token=_enabled("AURASK_GOOGLE_ALLOW_UNVERIFIED_ID_TOKEN", False),
session_ttl_days=int(os.getenv("AURASK_SESSION_TTL_DAYS", "7")),
devcloud_api_image=os.getenv("AURASK_DEVCLOUD_API_IMAGE", "registry.mydevcloud.love/devcloud/aurask-api:latest"),
devcloud_web_image=os.getenv("AURASK_DEVCLOUD_WEB_IMAGE", "registry.mydevcloud.love/devcloud/aurask-web:latest"),
devcloud_api_node_url=os.getenv("AURASK_DEVCLOUD_API_NODE_URL", "http://45.113.2.55:30091"),
devcloud_web_node_url=os.getenv("AURASK_DEVCLOUD_WEB_NODE_URL", "http://45.113.2.55:30090"),
)
def public_dict(self) -> dict:
return {
"public_base_url": self.public_base_url,
"public_api_base_url": self.public_api_base_url,
"auth": {
"ly_sso": {
"enabled": self.ly_sso_enabled,
"login_url": self.ly_sso_login_url,
"username_hint": self.ly_sso_username,
},
"google": {
"enabled": self.google_enabled,
"client_id": self.google_client_id,
},
},
"embeds": {
"langflow_url": self.langflow_embed_url,
"anythingllm_url": self.anythingllm_embed_url,
},
"devcloud": {
"api_image": self.devcloud_api_image,
"web_image": self.devcloud_web_image,
"api_node_url": self.devcloud_api_node_url,
"web_node_url": self.devcloud_web_node_url,
},
}

View File

@ -4,6 +4,26 @@ GET http://127.0.0.1:8080/health
### Plans
GET http://127.0.0.1:8080/plans
### Portal auth config
GET http://127.0.0.1:8080/auth/config
### LY SSO login
POST http://127.0.0.1:8080/auth/ly-sso/login
Content-Type: application/json
{
"username": "ly-xujian1",
"password": "replace-with-env-password"
}
### Google login
POST http://127.0.0.1:8080/auth/google/login
Content-Type: application/json
{
"id_token": "replace-with-google-id-token"
}
### Demo bootstrap
POST http://127.0.0.1:8080/demo/bootstrap
Content-Type: application/json
@ -16,6 +36,17 @@ Content-Type: application/json
### Set API key after bootstrap
@api_key = ak_replace_me
### Set session token after portal login
@session_token = sess_replace_me
### Session profile
GET http://127.0.0.1:8080/auth/session
Authorization: Bearer {{session_token}}
### Logout
POST http://127.0.0.1:8080/auth/logout
Authorization: Bearer {{session_token}}
### Quota
GET http://127.0.0.1:8080/quota
Authorization: Bearer {{api_key}}
@ -24,6 +55,10 @@ Authorization: Bearer {{api_key}}
GET http://127.0.0.1:8080/workflow-templates
Authorization: Bearer {{api_key}}
### List workspaces
GET http://127.0.0.1:8080/workspaces
Authorization: Bearer {{api_key}}
### Create workspace
POST http://127.0.0.1:8080/workspaces
Authorization: Bearer {{api_key}}

View File

@ -1,748 +1,217 @@
# Aurask `k3s` 部署方案(当前 DevCloud 部署与后续扩展)
# Aurask k3s / DevCloud 部署说明
## 当前 DevCloud 落地方案
本文档基于当前仓库与 DevCloud 现网结构,兼顾两部分目标:
当前 `master` 分支已经对接 DevCloud 的实际部署形态。仓库采用 **base + production overlay** 的混合结构:`deploy/k3s/base` 保持可复用默认清单,`deploy/k3s/overlays/production` 绑定当前 3 节点 DevCloud 集群、NodePort 和节点调度策略。
- 保留已落地的 DevCloud `base + production overlay` 部署方式
- 纳入本次新增的门户登录流、`LY SSO` / Google、Langflow / AnythingLLM 嵌入配置
### 当前已落地组件
- `aurask-api`
- 镜像:`registry.mydevcloud.love/devcloud/aurask-api`
- 节点:`45.113.2.55`
- Kubernetes 节点名:`devcloud-trade-agent-1`
- Service`NodePort 30091`
- `aurask-web`
- 镜像:`registry.mydevcloud.love/devcloud/aurask-web`
- 来源:`protal/`
- 节点:`154.193.250.23`
- Kubernetes 节点名:`devcloud-trade-agent-2`
- Service`NodePort 30090`
- `aurask-worker`
- 镜像:复用 `registry.mydevcloud.love/devcloud/aurask-api`
- 节点:`45.113.2.55`
- Kubernetes 节点名:`devcloud-trade-agent-1`
- 启动命令:`python -m aurask worker --data /data/state.json`
- `aurask-manager`
- 镜像:`registry.mydevcloud.love/devcloud/aurask-manager`
- 来源:`manager/`
- 节点:`154.193.250.23`
- Kubernetes 节点名:`devcloud-trade-agent-2`
- Service`NodePort 30092`
- 当前不接入公网 Caddy仅作为管理员入口预留
- `postgres`
- 镜像:`pgvector/pgvector:pg16`
- 节点:`45.113.2.55`
- Kubernetes 节点名:`devcloud-trade-agent-1`
- Service仅集群内访问
- 用途:预置 PostgreSQL + PGVector 数据面,当前业务主存储仍为 MVP JSON
- `redis`
- 镜像:`redis:7-alpine`
- 节点:`45.113.2.55`
- Kubernetes 节点名:`devcloud-trade-agent-1`
- Service仅集群内访问
- 用途:预置队列、幂等、缓存、限流数据面
- `aurask` 命名空间
- `aurask-api-state` PVC
- `StorageClass`: `local-path`
- 用于保存 MVP JSON 状态文件
### 当前公网入口
- 域名:`https://aurask.xyz`
- 路由方式:
- `/` -> `aurask-web`
- `/api/*` -> `aurask-api`
- 边界入口通过前端节点宿主机 `Caddy` 转发:
- `154.193.250.23:443` -> `NodePort 30090 / 30091`
### 当前仍未纳入自动部署的组件
以下内容仍属于后续扩展计划,**当前 `master` 分支自动发布不会部署**
- Langflow Runtime
- AnythingLLM
- Observability / Longhorn / CNPG
当前 production overlay 中 `AURASK_USE_EXTERNAL_BRIDGES=false`。也就是说PostgreSQL / PGVector / Redis 已作为生产数据面清单落地,但 Aurask 业务代码仍运行在 MVP JSON 主存储模式;待真实 PostgreSQL repository、Redis 队列消费者、Langflow 与 AnythingLLM 工作负载完成后,再切换到外部桥接模式。
### 自动发布流水线
仓库内置 Gitea Actions 工作流:
- 文件:`.gitea/workflows/aurask-release.yml`
- 触发条件:`master` 分支 push
- 动作:
- 运行单元测试
- 构建 `aurask-api` 镜像
- 构建 `aurask-web` 镜像
- 构建 `aurask-manager` 镜像
- 推送镜像到私有仓库
- 通过 SSH 连接 `64.90.15.15`
- `kubectl apply -k deploy/k3s/overlays/production`
- 自动更新 `aurask-api`、`aurask-worker`、`aurask-web` 与 `aurask-manager` 镜像到当前 commit SHA
### 仓库所需 Gitea Actions Secrets
- `SSH_PRIVATE_KEY`
- `REGISTRY_USER`
- `REGISTRY_PASSWORD`
- `POSTGRES_DB`
- `POSTGRES_USER`
- `POSTGRES_PASSWORD`
### 当前仓库内的部署资产位置
```text
deploy/
images/
aurask-api/
Dockerfile
aurask-web/
Dockerfile
aurask-manager/
Dockerfile
k3s/
base/
aurask-runtime-config.yaml
namespace.yaml
aurask-api-pvc.yaml
aurask-api.yaml
aurask-worker.yaml
aurask-web.yaml
aurask-manager.yaml
postgres.yaml
redis.yaml
kustomization.yaml
overlays/
production/
kustomization.yaml
examples/
aurask-runtime-secrets.example.yaml
aurask-postgres-secret.example.yaml
aurask-redis-secret.example.yaml
```
## 目标扩展方案300 名月度活跃用户)
本部署计划基于当前仓库结构更新:
```text
api/ # 后端服务、桥接配置、前端到后端请求契约
protal/ # 用户前端使用面板
manager/ # 管理员前端使用面板
deploy/ # k3s 与后续部署配置
```
目标是支撑 **约 300 名月度活跃付费用户**,并满足 `Aurask_Technical_Operations_Plan.md` 中的核心边界:
- Aurask 网关是唯一公网业务入口。
- `Langflow`、`AnythingLLM`、`PostgreSQL`、`PGVector`、`Redis` 不直接暴露公网。
- 基础用户只运行审核模板,不开放任意代码执行。
- AnythingLLM Workspace 与 Aurask `tenant_id` 绑定。
- 工作流执行前预扣 TBU执行后按实际消耗结算。
- 支付、额度、审计、成本链路可追踪。
## 1. 当前代码到集群工作负载映射
当前仓库虽然仍是 Python 模块化单体,但已经按生产职责拆出目录和桥接层。部署时按以下方式映射:
| 仓库目录/模块 | k3s 工作负载 | 说明 |
| --- | --- | --- |
| `api/aurask/api.py` | `aurask-api` | HTTP 网关、鉴权、订单、额度、前端请求入口 |
| `api/aurask/cli.py` | `aurask-api` / `aurask-worker` 启动入口 | 当前用同一镜像不同命令启动 |
| `api/aurask/orchestrator.py` | `aurask-worker` | 工作流编排、TBU 预扣/结算、调用 Langflow |
| `api/aurask/payments.py` | `aurask-worker` / `aurask-cron` | 支付匹配、后续链上监听 |
| `api/aurask/bridges/postgres.py` | `aurask-api` / `aurask-worker` | PostgreSQL schema contract |
| `api/aurask/bridges/pgvector.py` | `aurask-worker` | 向量检索契约,强制租户过滤 |
| `api/aurask/bridges/redis_bridge.py` | `aurask-api` / `aurask-worker` | 队列、缓存、幂等、限流 key 规则 |
| `api/aurask/bridges/anythingllm.py` | `aurask-worker` | AnythingLLM Workspace / 文档入库桥接 |
| `api/aurask/bridges/langflow.py` | `aurask-worker` | Langflow 模板运行桥接 |
| `protal/` | `aurask-protal` | 用户前端静态站点 |
| `manager/` | `aurask-manager` | 管理员前端静态站点 |
| `deploy/k3s/` | GitOps / Kustomize / Helm | 部署配置根目录 |
## 2. 容量假设
300 MAU 首版不是 300 并发。按以下容量规划:
- 月度活跃付费用户:`300`
- 日活高峰:`40-80`
- 同时在线用户峰值:`15-30`
- 同时工作流执行峰值:`10-20`
- 文档总量:`<= 500GB`
- 向量层:首版 `PostgreSQL + PGVector`
- 模型推理:外部模型 API / LLM Proxy不在集群内自建 GPU 推理
- Runtime基础用户共享 Runtime Pool高付费用户后续独立 Namespace
触发扩容条件:
- 持续并发工作流 `> 20`
- 文档总量 `> 500GB`
- 高付费独立空间用户 `> 20`
- 工作流排队 P95 `> 10s`
- PostgreSQL 连接数、WAL、磁盘或 CPU 持续接近上限
## 3. 集群拓扑
推荐首版生产拓扑:
| 角色 | 数量 | 建议配置 | 用途 |
| --- | ---: | --- | --- |
| Public LB | 1 | 云负载均衡或 `HAProxy/Keepalived` | 统一暴露 `80/443` |
| `k3s` server | 3 | `4 vCPU / 8GB RAM / 120GB SSD` | 控制面 + embedded etcd |
| General worker | 2 | `8 vCPU / 16GB RAM / 200GB SSD` | `aurask-api`、前端、Ingress、观测 |
| Runtime worker | 2 | `8 vCPU / 16GB RAM / 250GB SSD` | `aurask-worker`、Langflow、AnythingLLM |
| Data worker | 可选 1-2 | `8 vCPU / 16GB RAM / 300GB+ SSD/NVMe` | PostgreSQL、Redis、存储密集型组件 |
如果预算有限,可先用 `3 server + 4 worker`,将 Data worker 合并到 worker 池;但 PostgreSQL 节点必须通过反亲和规则打散。
## 4. Namespace 规划
| Namespace | 组件 |
| --- | --- |
| `ingress-system` | Traefik / Nginx Ingress、External DNS |
| `cert-manager` | TLS 证书 |
| `aurask-api` | `aurask-api`、API Secret、API ServiceAccount |
| `aurask-web` | `aurask-protal`、`aurask-manager` 静态前端 |
| `aurask-runtime` | `aurask-worker`、`langflow-runtime`、`anythingllm` |
| `aurask-data` | PostgreSQL、PGVector、PgBouncer、Redis |
| `observability` | Prometheus、Grafana、Loki、Alertmanager |
| `longhorn-system` | Longhorn |
要求:
- 默认 `NetworkPolicy``deny-all`
- 只允许 `aurask-api` 接收公网 Ingress 流量。
- `aurask-protal`、`aurask-manager` 可以暴露公网,但它们只访问 `aurask-api`
- `Langflow`、`AnythingLLM`、`PostgreSQL`、`Redis` 仅 `ClusterIP`
## 5. 应用工作负载
### 5.1 `aurask-api`
来源:
- `api/aurask/api.py`
- `api/aurask/app.py`
- `api/aurask/auth.py`
- `api/aurask/billing.py`
- `api/aurask/quota.py`
职责:
- HTTP 网关。
- 鉴权。
- 租户上下文注入。
- 套餐、订单、额度查询。
- 前端请求入口。
- 管理端桥接状态接口:`GET /admin/bridge-status`。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `3` |
| requests | `500m CPU / 1Gi RAM` |
| limits | `1 CPU / 2Gi RAM` |
| HPA | CPU 65% 或 QPS 指标,扩到 `5` |
| PDB | `minAvailable=2` |
### 5.2 `aurask-worker`
来源:
- `api/aurask/orchestrator.py`
- `api/aurask/knowledge_base.py`
- `api/aurask/payments.py`
- `api/aurask/bridges/*`
职责:
- 工作流编排。
- TBU 预扣与结算。
- 调用 Langflow Runtime。
- 调用 AnythingLLM。
- 处理文档入库。
- 后续消费 Redis 队列。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `3` |
| requests | `1 CPU / 2Gi RAM` |
| limits | `2 CPU / 4Gi RAM` |
| HPA/KEDA | 按 Redis 队列长度扩到 `6` |
| 调度 | 优先 Runtime worker |
### 5.3 `aurask-cron`
职责:
- 过期订单处理。
- 支付异常扫描。
- 周报/月报。
- 备份检查。
- 成本报表归集。
建议:
| 项目 | 值 |
| --- | --- |
| 类型 | `CronJob` |
| requests | `250m CPU / 512Mi RAM` |
| limits | `500m CPU / 1Gi RAM` |
### 5.4 `aurask-protal`
来源:`protal/`
职责:
- 用户登录后的使用面板。
- 调用 Aurask API。
- 创建 Workspace、运行模板、查看额度。
建议:
| 项目 | 值 |
| --- | --- |
| 类型 | 静态站点 Deployment |
| 副本 | `2` |
| requests | `50m CPU / 64Mi RAM` |
| limits | `200m CPU / 256Mi RAM` |
| Ingress host | `app.aurask.example.com` |
### 5.5 `aurask-manager`
来源:`manager/`
职责:
- 管理员使用面板。
- 查看桥接状态。
- 后续扩展租户、订单、异常支付、成本、审计。
建议:
| 项目 | 值 |
| --- | --- |
| 类型 | 静态站点 Deployment |
| 副本 | `2` |
| requests | `50m CPU / 64Mi RAM` |
| limits | `200m CPU / 256Mi RAM` |
| Ingress host | `manager.aurask.example.com` |
| 额外保护 | IP allowlist / SSO / BasicAuth / 二次认证 |
## 6. Runtime 组件
### 6.1 Langflow Runtime
部署名:`langflow-runtime`
用途:
- 仅执行 Aurask 审核过的模板工作流。
- 不向普通用户暴露 Langflow UI。
- 被 `aurask-worker` 通过内部 Service 调用。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `3` |
| requests | `1500m CPU / 3Gi RAM` |
| limits | `3 CPU / 6Gi RAM` |
| Service | `ClusterIP` |
| 调度 | Runtime worker |
关键环境变量:
```text
LANGFLOW_AUTO_LOGIN=False
LANGFLOW_FALLBACK_TO_ENV_VAR=False
LANGFLOW_DATABASE_URL=postgresql://...
LANGFLOW_SECRET_KEY=<secret>
```
Aurask 侧桥接:
```text
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<secret>
```
### 6.2 AnythingLLM
部署名:`anythingllm`
用途:
- Workspace。
- 文档入库。
- RAG。
- 聊天历史。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `2` |
| requests | `1 CPU / 2Gi RAM` |
| limits | `2 CPU / 4Gi RAM` |
| Service | `ClusterIP` |
| 调度 | Runtime worker |
Aurask 侧桥接:
```text
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<secret>
```
要求:
- 管理员账号仅由 Aurask 后台持有。
- 普通用户不进入 AnythingLLM 管理后台。
- Workspace 必须通过 Aurask 创建并绑定 `tenant_id`
## 7. 数据组件
### 7.1 PostgreSQL + PGVector
推荐:`CloudNativePG + PGVector`
用途:
- 租户、用户、订单、额度、审计。
- 向量检索起步方案。
建议:
| 项目 | 值 |
| --- | --- |
| 实例数 | `3` |
| requests | `2 CPU / 4Gi RAM` |
| limits | `4 CPU / 8Gi RAM` |
| PVC | 每实例 `200GB` 起 |
| 备份 | WAL 归档到 S3 |
Aurask schema 契约来源:
- `api/aurask/bridges/postgres.py`
- `api/aurask/bridges/pgvector.py`
必须启用:
```sql
CREATE EXTENSION IF NOT EXISTS vector;
```
### 7.2 PgBouncer
用途:
- 降低 API / Worker 连接风暴。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `2` |
| requests | `500m CPU / 512Mi RAM` |
| limits | `1 CPU / 1Gi RAM` |
### 7.3 Redis
用途:
- 工作流队列。
- 幂等键。
- 限流。
- 短期缓存。
建议:
| 项目 | 值 |
| --- | --- |
| 副本 | `2-3` |
| requests | `500m CPU / 1Gi RAM` |
| limits | `1 CPU / 2Gi RAM` |
Aurask key 契约来源:
- `api/aurask/bridges/redis_bridge.py`
## 8. 镜像构建计划
当前仓库建议构建三个镜像:
| 镜像 | 来源目录 | 用途 |
| --- | --- | --- |
| `aurask-api` | `api/` | 后端 API 与 worker 运行时 |
| `aurask-web` | `protal/` | 用户前端静态站点 |
| `aurask-manager` | `manager/` | 管理员前端静态站点 |
首版可先使用同一个 Python 镜像启动 `aurask-api``aurask-worker`
```text
aurask-api: python -m aurask serve --host 0.0.0.0 --port 8080
aurask-worker: python -m aurask worker # 当前提供最小常驻 worker 与 --once 自检
```
当前 `aurask-worker` 提供最小常驻进程、心跳日志与 `--once` 自检;真实 Redis 队列消费者完成前,它作为生产拓扑预留工作负载。
## 9. 环境变量
`aurask-api``aurask-worker` 共享:
```text
AURASK_USE_EXTERNAL_BRIDGES=true
AURASK_DATABASE_URL=postgresql://aurask:<password>@pgbouncer.aurask-data.svc.cluster.local:5432/aurask
AURASK_POSTGRES_MIN_CONNECTIONS=1
AURASK_POSTGRES_MAX_CONNECTIONS=10
AURASK_PGVECTOR_TABLE=aurask_vectors
AURASK_PGVECTOR_DIMENSION=1536
AURASK_REDIS_URL=redis://redis.aurask-data.svc.cluster.local:6379/0
AURASK_REDIS_WORKFLOW_QUEUE=aurask:workflow-runs
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_ANYTHINGLLM_API_KEY=<secret>
AURASK_LANGFLOW_BASE_URL=http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LANGFLOW_API_KEY=<secret>
```
Secret 管理:
- 推荐 `External Secrets Operator`
- 或使用 `SOPS + age` 管理 GitOps secret。
- 禁止将真实 API Key、数据库密码、钱包配置提交到 Git。
## 10. Ingress 规划
建议域名:
| Host | Service | 说明 |
| --- | --- | --- |
| `api.aurask.example.com` | `aurask-api` | API 网关 |
| `app.aurask.example.com` | `aurask-protal` | 用户面板 |
| `manager.aurask.example.com` | `aurask-manager` | 管理员面板 |
不创建公网 Ingress
- `langflow-runtime`
- `anythingllm`
- `postgres`
- `pgbouncer`
- `redis`
管理员面板额外要求:
- IP allowlist。
- SSO / BasicAuth / 二次认证。
- 操作审计。
## 11. NetworkPolicy 规划
默认拒绝所有东西向访问,然后按链路放通:
| From | To | Purpose |
| --- | --- | --- |
| Ingress | `aurask-api` | API 流量 |
| Ingress | `aurask-protal` | 用户面板 |
| Ingress | `aurask-manager` | 管理面板 |
| `aurask-api` | PgBouncer | 业务读写 |
| `aurask-worker` | PgBouncer | 业务读写 |
| `aurask-api` | Redis | 限流、缓存 |
| `aurask-worker` | Redis | 队列、幂等 |
| `aurask-worker` | Langflow | 模板执行 |
| `aurask-worker` | AnythingLLM | Workspace / 文档 / RAG |
| Langflow | 外部 LLM Proxy | 模型调用 |
| AnythingLLM | 外部 LLM Proxy / Object Storage | RAG 与文档处理 |
禁止:
- 用户请求直接访问 Langflow / AnythingLLM。
- 前端直接访问 PostgreSQL / Redis。
- Runtime 访问内网管理网段、云元数据地址。
## 12. 存储与备份
### 12.1 PVC
| 组件 | 存储 |
| --- | --- |
| PostgreSQL | `cnpg-fast`,优先云块存储 / NVMe |
| Redis | `longhorn-general` 或云块存储 |
| AnythingLLM | `longhorn-critical` 或外部对象存储缓存 |
| Observability | `longhorn-general` |
### 12.2 对象存储
优先外部 S3 兼容存储,用于:
- 用户文档对象。
- PostgreSQL 备份。
- Longhorn 备份。
- 审计归档。
### 12.3 备份目标
- PostgreSQL`RPO <= 15 分钟``RTO <= 2 小时`。
- Longhorn每日快照每周备份。
- 支付订单与链上交易匹配记录:至少保留 `180` 天。
- 每月至少做一次恢复演练。
## 13. 可观测性
组件:
- Prometheus
- Grafana
- Loki
- Alertmanager
- kube-state-metrics
- node-exporter
必须监控:
- `aurask-api` QPS、P95、错误率。
- `aurask-worker` 队列深度、执行时长、失败率。
- `tbu_reserved_total`、`tbu_consumed_total`、`tbu_released_total`。
- 订单创建、支付匹配、异常订单数量。
- Langflow 执行耗时与错误率。
- AnythingLLM 文档入库耗时与失败率。
- PostgreSQL 连接数、WAL、复制延迟、磁盘。
- Redis 内存、队列长度、延迟。
## 14. 推荐部署配置目录
后续 manifests 建议落入:
## 目录结构
```text
deploy/k3s/
README.md
base/
namespaces.yaml
ingress.yaml
network-policies.yaml
secrets.example.yaml
namespace.yaml
aurask-runtime-config.yaml
aurask-config.yaml
aurask-api-pvc.yaml
aurask-api.yaml
aurask-worker.yaml
aurask-web.yaml
aurask-manager.yaml
langflow-runtime.yaml
anythingllm.yaml
postgres-cnpg.yaml
postgres.yaml
redis.yaml
observability.yaml
kustomization.yaml
secrets.example.yaml
overlays/
staging/
kustomization.yaml
production/
kustomization.yaml
aurask-api-production.yaml
aurask-worker-production.yaml
aurask-web-production.yaml
aurask-manager-production.yaml
postgres-production.yaml
redis-production.yaml
runtime-config-production.yaml
examples/
aurask-runtime-secrets.example.yaml
aurask-postgres-secret.example.yaml
aurask-redis-secret.example.yaml
```
Helm 管理:
## 当前 DevCloud 现网映射
- `cert-manager`
- `longhorn`
- `cloudnative-pg`
- `prometheus-stack`
- `loki`
- `redis` operator 或 chart
### 域名与入口
Kustomize 管理:
- 公网域名:`https://aurask.xyz`
- `https://aurask.xyz/api/*``aurask-api`
- `https://aurask.xyz/*``aurask-web`
- 当前由前端宿主机 Caddy 转发到 DevCloud NodePort
- Aurask 自研服务。
- Ingress。
- NetworkPolicy。
- 环境差异。
### 已知镜像与端口
## 15. 上线步骤
- API 镜像:`registry.mydevcloud.love/devcloud/aurask-api:latest`
- Web 镜像:`registry.mydevcloud.love/devcloud/aurask-web:latest`
- Manager 镜像:`registry.mydevcloud.love/devcloud/aurask-manager:latest`
- `aurask-api` NodePort`30091`
- `aurask-web` NodePort`30090`
- `aurask-manager` NodePort`30092`
### Phase 1集群底座
### 生产 overlay 节点绑定
1. 创建 `3 server + 4 worker` k3s 集群。
2. 配置公网 LB 与 DNS。
3. 安装 `cert-manager`
4. 安装 `Longhorn` 或云 CSI。
5. 安装 observability 基础栈。
- `aurask-api``devcloud-trade-agent-1`
- `aurask-worker``devcloud-trade-agent-1`
- `postgres``devcloud-trade-agent-1`
- `redis``devcloud-trade-agent-1`
- `aurask-web``devcloud-trade-agent-2`
- `aurask-manager``devcloud-trade-agent-2`
### Phase 2数据层
## Base 层职责
1. 安装 `CloudNativePG`
2. 初始化 PostgreSQL 与 PGVector。
3. 部署 PgBouncer。
4. 部署 Redis。
5. 配置对象存储与备份桶。
`deploy/k3s/base/` 保持通用资源,不直接写死生产节点:
### Phase 3Aurask 应用层
- `namespace.yaml`Aurask namespace
- `aurask-runtime-config.yaml`:桥接/运行时基础配置
- `aurask-config.yaml`:站点、门户、登录、嵌入默认配置
- `aurask-api-pvc.yaml`MVP 状态文件持久化
- `aurask-api.yaml`API Deployment + Service
- `aurask-worker.yaml`Worker Deployment
- `aurask-web.yaml`:用户门户 Deployment + Service
- `aurask-manager.yaml`:管理员门户 Deployment + Service
- `postgres.yaml`PostgreSQL / PGVector 基础资源
- `redis.yaml`Redis 基础资源
1. 构建并推送 `aurask-api` 镜像。
2. 构建并推送 `aurask-web` 镜像。
3. 构建并推送 `aurask-manager` 镜像。
4. 部署 `aurask-api`
5. 部署 `aurask-worker`
6. 部署 `aurask-web``aurask-manager`
7. 配置 Ingress、TLS、NetworkPolicy。
## 本次新增的门户配置
### Phase 4Runtime 层
### `aurask-config.yaml`
1. 部署 Langflow Runtime。
2. 部署 AnythingLLM。
3. 配置 `AURASK_LANGFLOW_*``AURASK_ANYTHINGLLM_*` secret。
4. 使用 `GET /admin/bridge-status` 验证桥接配置。
5. 跑通模板工作流与文档入库。
新增以下站点默认值:
### Phase 5生产化验收
- `AURASK_PUBLIC_BASE_URL=https://aurask.xyz`
- `AURASK_PUBLIC_API_BASE_URL=https://aurask.xyz/api`
- `AURASK_PUBLIC_LANGFLOW_URL=https://aurask.xyz/runtime/langflow/`
- `AURASK_PUBLIC_ANYTHINGLLM_URL=https://aurask.xyz/runtime/anythingllm/`
- `AURASK_DEVCLOUD_API_IMAGE=registry.mydevcloud.love/devcloud/aurask-api:latest`
- `AURASK_DEVCLOUD_WEB_IMAGE=registry.mydevcloud.love/devcloud/aurask-web:latest`
- `AURASK_DEVCLOUD_API_NODE_URL=http://45.113.2.55:30091`
- `AURASK_DEVCLOUD_WEB_NODE_URL=http://45.113.2.55:30090`
- `AURASK_LY_SSO_ENABLED=true`
- `AURASK_LY_SSO_USERNAME=ly-xujian1`
- `AURASK_GOOGLE_ENABLED=true`
- `AURASK_SESSION_TTL_DAYS=7`
1. 开启 HPA / KEDA。
2. 跑并发工作流压测。
3. 演练支付匹配。
4. 演练 PostgreSQL 备份恢复。
5. 演练节点故障与 Pod 重调度。
6. 检查 Langflow / AnythingLLM 未暴露公网。
### `secrets.example.yaml`
## 16. 验收标准
新增门户与外部组件需要的 Secret 占位:
| 项目 | 目标 |
| --- | --- |
| API P95 | `< 500ms`,不含外部模型调用 |
| 峰值并发工作流 | `10-20` |
| 工作流成功率 | `95%+` |
| 支付匹配成功率 | `99%+` |
| 工作流排队 P95 | `< 10s` |
| PostgreSQL RPO | `<= 15 分钟` |
| PostgreSQL RTO | `<= 2 小时` |
| 月可用性 | `99%+` |
| 外部暴露面 | 仅 API、用户面板、管理面板 |
- `AURASK_DATABASE_URL`
- `AURASK_ANYTHINGLLM_API_KEY`
- `AURASK_LANGFLOW_API_KEY`
- `AURASK_LY_SSO_PASSWORD`
- `AURASK_GOOGLE_CLIENT_ID`
## 17. 当前代码差距
说明:
当前已经具备:
- `secrets.example.yaml` 仅作模板,不应直接提交真实密钥
- 生产建议继续使用 `External Secrets Operator``SOPS + age`
- 根目录划分:`api`、`protal`、`manager`、`deploy`。
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约。
- `/admin/bridge-status` 桥接状态接口。
- 静态用户面板和管理面板。
- 请求样例:`api/requests/aurask-api.http`。
## API / Web 部署说明
仍需补齐:
### `aurask-api`
- `deploy/k3s/base` manifests。
- 真实 PostgreSQL repository。
- Redis 队列消费者。
- `aurask-worker` 独立启动命令。
- 真实 Langflow / AnythingLLM API 适配细节校验。
- 前端构建流程与容器镜像。
- Secret / NetworkPolicy / HPA / KEDA manifests。
职责:
## 18. 官方参考
- API Gateway
- `LY SSO` / Google 登录
- Session 签发与校验
- 配额、订单、支付、工作流入口
- 返回门户配置与嵌入 URL
- k3s HA embedded etcd<https://docs.k3s.io/datastore/ha-embedded>
- cert-manager Installation<https://cert-manager.io/docs/installation/>
- Longhorn Installation<https://longhorn.io/docs/latest/deploy/install/>
- CloudNativePG Documentation<https://cloudnative-pg.io/documentation/current/>
- CloudNativePG Backup<https://cloudnative-pg.io/documentation/current/backup/>
- AnythingLLM System Requirements<https://docs.anythingllm.com/installation-docker/system-requirements>
- Langflow Security<https://docs.langflow.org/security>
配置说明:
- 继续挂载 `/data/state.json`,兼容当前 MVP `JsonStore`
- 同时读取:
- `aurask-runtime-config`
- `aurask-config`
- `aurask-runtime-secrets`
- `aurask-secrets`
### `aurask-web`
职责:
- 承载 `protal/`
- 对外提供 `/signin`
- 登录后提供 `Workflows` / `Knowledge Base` 双标签工作台
## 生产 overlay 说明
`deploy/k3s/overlays/production/` 负责绑定现网特定配置:
- NodeSelector
- NodePort
- 生产 runtime 开关
当前生产 overlay 已保留:
- `aurask-api-production.yaml`
- `aurask-worker-production.yaml`
- `aurask-web-production.yaml`
- `aurask-manager-production.yaml`
- `postgres-production.yaml`
- `redis-production.yaml`
- `runtime-config-production.yaml`
## 运行时嵌入建议
当前门户已经引用:
- `https://aurask.xyz/runtime/langflow/`
- `https://aurask.xyz/runtime/anythingllm/`
建议后续分两步完成:
1. 先由公网反代到内部运行时入口
2. 再收敛为 `aurask-api` 网关代理鉴权,避免直接暴露运行时
## 300 MAU 首版建议
### 集群规模
- `3` 台 k3s server
- `2` 台 general worker
- `2` 台 runtime worker
### 组件建议
- `aurask-api`
- `aurask-web`
- `aurask-worker`
- `aurask-manager`
- PostgreSQL + PGVector
- Redis
- 后续补充 Langflow / AnythingLLM 专用清单
## 部署方式
### 应用基础资源
```powershell
kubectl apply -k deploy/k3s/base
```
### 应用生产 overlay
```powershell
kubectl apply -k deploy/k3s/overlays/production
```
## 后续建议
建议继续推进:
1. 用 PostgreSQL Repository 替换 `JsonStore`
2. 让 `aurask-worker` 接入真实 Redis 队列消费
3. 为 Langflow / AnythingLLM 增加独立清单
4. 增加 Ingress / TLS / NetworkPolicy
5. 让运行时访问统一收敛到 Aurask 网关代理

View File

@ -33,9 +33,14 @@ spec:
envFrom:
- configMapRef:
name: aurask-runtime-config
- configMapRef:
name: aurask-config
- secretRef:
name: aurask-runtime-secrets
optional: true
- secretRef:
name: aurask-secrets
optional: true
ports:
- containerPort: 8080
name: http

View File

@ -0,0 +1,29 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: aurask-config
namespace: aurask
data:
AURASK_PUBLIC_BASE_URL: https://aurask.xyz
AURASK_PUBLIC_API_BASE_URL: https://aurask.xyz/api
AURASK_PUBLIC_LANGFLOW_URL: https://aurask.xyz/runtime/langflow/
AURASK_PUBLIC_ANYTHINGLLM_URL: https://aurask.xyz/runtime/anythingllm/
AURASK_DEVCLOUD_API_IMAGE: registry.mydevcloud.love/devcloud/aurask-api:latest
AURASK_DEVCLOUD_WEB_IMAGE: registry.mydevcloud.love/devcloud/aurask-web:latest
AURASK_DEVCLOUD_API_NODE_URL: http://45.113.2.55:30091
AURASK_DEVCLOUD_WEB_NODE_URL: http://45.113.2.55:30090
AURASK_USE_EXTERNAL_BRIDGES: "true"
AURASK_POSTGRES_MIN_CONNECTIONS: "1"
AURASK_POSTGRES_MAX_CONNECTIONS: "10"
AURASK_PGVECTOR_TABLE: aurask_vectors
AURASK_PGVECTOR_DIMENSION: "1536"
AURASK_REDIS_URL: redis://redis.aurask-data.svc.cluster.local:6379/0
AURASK_REDIS_WORKFLOW_QUEUE: aurask:workflow-runs
AURASK_ANYTHINGLLM_BASE_URL: http://anythingllm.aurask-runtime.svc.cluster.local:3001
AURASK_LANGFLOW_BASE_URL: http://langflow-runtime.aurask-runtime.svc.cluster.local:7860
AURASK_LY_SSO_ENABLED: "true"
AURASK_LY_SSO_USERNAME: ly-xujian1
AURASK_LY_SSO_EMAIL_DOMAIN: ly.szlanyou.local
AURASK_GOOGLE_ENABLED: "true"
AURASK_SESSION_TTL_DAYS: "7"
AURASK_CORS_ALLOW_ORIGIN: https://aurask.xyz

View File

@ -1,8 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: aurask
resources:
- namespace.yaml
- aurask-runtime-config.yaml
- aurask-config.yaml
- aurask-api-pvc.yaml
- aurask-api.yaml
- aurask-worker.yaml

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Secret
metadata:
name: aurask-secrets
namespace: aurask
type: Opaque
stringData:
AURASK_DATABASE_URL: postgresql://aurask:<postgres-password>@postgres.aurask-data.svc.cluster.local:5432/aurask
AURASK_ANYTHINGLLM_API_KEY: <anythingllm-api-key>
AURASK_LANGFLOW_API_KEY: <langflow-api-key>
AURASK_LY_SSO_PASSWORD: <ly-sso-password>
AURASK_GOOGLE_CLIENT_ID: <google-oauth-client-id>

View File

@ -1,15 +1,45 @@
# Aurask Protal
`protal` is the user-facing panel directory. The name follows the requested root directory spelling.
`protal/` 是用户门户目录,目录名按既定要求保留为 `protal`
Current MVP:
## 当前页面能力
- Static HTML panel
- Talks directly to Aurask API
- Supports demo bootstrap, quota lookup, workspace creation, and template workflow run
- 未登录时渲染 `/signin` 风格登录页
- 支持 `LY SSO` 登录按钮与本地凭据提交
- 支持 Google 登录入口
- 新用户首次登录自动创建独立 workspace
- 登录后显示:
- `Workflows`:内嵌 Langflow
- `Knowledge Base`:内嵌 AnythingLLM
- 右上角个人中心展示用户、租户、workspace 与基础额度信息
Open `index.html` in a browser after starting:
## 依赖接口
```bash
uv run aurask serve --reset --host 127.0.0.1 --port 8080
门户依赖以下 API
- `GET /auth/config`
- `POST /auth/ly-sso/login`
- `POST /auth/google/login`
- `GET /auth/session`
- `POST /auth/logout`
## 部署说明
门户默认假设:
- 生产环境通过 `https://aurask.xyz`
- API 通过 `https://aurask.xyz/api`
- Langflow iframe 通过 `https://aurask.xyz/runtime/langflow/`
- AnythingLLM iframe 通过 `https://aurask.xyz/runtime/anythingllm/`
本地调试时会自动回退到:
```text
http://127.0.0.1:8080
```
## 静态资源
- `protal/index.html`
- `protal/main.js`
- `protal/styles.css`

View File

@ -3,47 +3,11 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurask User Protal</title>
<title>Aurask</title>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<main class="shell">
<section class="hero">
<p class="eyebrow">Aurask User Protal</p>
<h1>Launch safe AI employee workflows</h1>
<p>Bootstrap a tenant, create a knowledge workspace, and run an approved template through the Aurask gateway.</p>
</section>
<section class="card">
<h2>Connection</h2>
<label>API Base URL <input id="apiBase" value="http://127.0.0.1:8080" /></label>
<label>API Key <input id="apiKey" placeholder="Bootstrap to fill automatically" /></label>
<button id="bootstrapBtn">Bootstrap Demo</button>
</section>
<section class="grid">
<div class="card">
<h2>Quota</h2>
<button id="quotaBtn">Load Quota</button>
<pre id="quotaOutput"></pre>
</div>
<div class="card">
<h2>Workspace</h2>
<label>Name <input id="workspaceName" value="Support KB" /></label>
<button id="workspaceBtn">Create Workspace</button>
<pre id="workspaceOutput"></pre>
</div>
</section>
<section class="card">
<h2>Run Template</h2>
<label>Template ID <input id="templateId" value="tpl_email_assistant" /></label>
<label>Workspace ID <input id="workspaceId" placeholder="Required for RAG templates" /></label>
<label>Prompt <textarea id="prompt">Draft a friendly refund policy reply.</textarea></label>
<button id="runBtn">Run Workflow</button>
<pre id="runOutput"></pre>
</section>
</main>
<div id="app" class="app-shell"></div>
<script src="./main.js"></script>
</body>
</html>

View File

@ -1,55 +1,458 @@
const apiBaseInput = document.querySelector("#apiBase");
const apiKeyInput = document.querySelector("#apiKey");
const STORAGE_KEY = "aurask.portal.session";
const TAB_KEY = "aurask.portal.activeTab";
function apiBase() {
return apiBaseInput.value.replace(/\/$/, "");
const app = document.querySelector("#app");
const state = {
config: null,
sessionToken: window.localStorage.getItem(STORAGE_KEY) || "",
profile: null,
activeTab: window.localStorage.getItem(TAB_KEY) || "workflows",
lyFormOpen: false,
loading: true,
error: "",
};
function detectApiBase() {
if (window.location.protocol === "file:") return "http://127.0.0.1:8080";
if (["127.0.0.1", "localhost"].includes(window.location.hostname)) return "http://127.0.0.1:8080";
return `${window.location.origin}/api`;
}
function normalizeApiBase(url) {
return (url || detectApiBase()).replace(/\/$/, "");
}
async function request(path, options = {}) {
const headers = new Headers(options.headers || {});
headers.set("Content-Type", "application/json");
const apiKey = apiKeyInput.value.trim();
if (apiKey) headers.set("Authorization", `Bearer ${apiKey}`);
const response = await fetch(`${apiBase()}${path}`, { ...options, headers });
const payload = await response.json();
if (!response.ok) throw new Error(JSON.stringify(payload, null, 2));
headers.set("Accept", "application/json");
if (options.body !== undefined) headers.set("Content-Type", "application/json");
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
...options,
headers,
body: options.body !== undefined ? JSON.stringify(options.body) : undefined,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
const message = payload?.error?.message || payload?.message || "Request failed";
const error = new Error(message);
error.status = response.status;
error.payload = payload;
throw error;
}
return payload;
}
function render(target, payload) {
document.querySelector(target).textContent = JSON.stringify(payload, null, 2);
function persistSession(token) {
state.sessionToken = token || "";
if (state.sessionToken) {
window.localStorage.setItem(STORAGE_KEY, state.sessionToken);
} else {
window.localStorage.removeItem(STORAGE_KEY);
}
}
document.querySelector("#bootstrapBtn").addEventListener("click", async () => {
const payload = await request("/demo/bootstrap", {
method: "POST",
body: JSON.stringify({ tenant_name: "Aurask Protal Demo", email: "owner@example.com" }),
function setTab(tab) {
state.activeTab = tab;
window.localStorage.setItem(TAB_KEY, tab);
render();
}
function initials(label) {
return (label || "AU")
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase())
.join("");
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
function withEmbedContext(baseUrl) {
if (!baseUrl) return "";
const url = new URL(baseUrl, window.location.origin);
if (state.profile?.tenant?.id) url.searchParams.set("tenant_id", state.profile.tenant.id);
if (state.profile?.workspace?.id) url.searchParams.set("workspace_id", state.profile.workspace.id);
url.searchParams.set("source", "aurask-protal");
return url.toString();
}
function ensureSigninRoute() {
if (state.profile) return;
if (window.location.pathname !== "/signin") {
window.history.replaceState({}, "", "/signin");
}
}
function ensureDashboardRoute() {
if (!state.profile) return;
if (!window.location.pathname.startsWith("/app")) {
window.history.replaceState({}, "", "/app");
}
}
async function bootstrap() {
state.loading = true;
render();
try {
const activeApiBase = detectApiBase();
const localHost = window.location.protocol === "file:" || ["127.0.0.1", "localhost"].includes(window.location.hostname);
state.config = await fetch(`${activeApiBase}/auth/config`).then((response) => response.json());
state.config.public_api_base_url = normalizeApiBase(localHost ? activeApiBase : state.config.public_api_base_url || activeApiBase);
if (state.sessionToken) {
try {
state.profile = await request("/auth/session");
ensureDashboardRoute();
} catch (error) {
persistSession("");
state.profile = null;
}
}
if (!state.profile) ensureSigninRoute();
} catch (error) {
state.error = error.message || "Failed to load Aurask configuration";
} finally {
state.loading = false;
render();
mountGoogleButton();
}
}
async function submitLySso(event) {
event.preventDefault();
state.error = "";
render();
const form = new FormData(event.currentTarget);
try {
const payload = await request("/auth/ly-sso/login", {
method: "POST",
body: {
username: String(form.get("username") || "").trim(),
password: String(form.get("password") || ""),
},
});
persistSession(payload.token);
state.profile = payload;
state.lyFormOpen = false;
ensureDashboardRoute();
} catch (error) {
state.error = error.message || "LY SSO sign-in failed";
} finally {
render();
mountGoogleButton();
}
}
async function signInWithGoogle(idToken) {
state.error = "";
render();
try {
const payload = await request("/auth/google/login", {
method: "POST",
body: { id_token: idToken },
});
persistSession(payload.token);
state.profile = payload;
ensureDashboardRoute();
} catch (error) {
state.error = error.message || "Google sign-in failed";
render();
}
}
async function logout() {
try {
await request("/auth/logout", { method: "POST" });
} catch (error) {
console.warn("Logout request failed", error);
}
persistSession("");
state.profile = null;
state.lyFormOpen = false;
ensureSigninRoute();
render();
mountGoogleButton();
}
function mountGoogleButton() {
const googleBox = document.querySelector("#googleButton");
if (!googleBox || !state.config?.auth?.google?.enabled || !state.config?.auth?.google?.client_id) return;
if (!window.google?.accounts?.id) return;
googleBox.innerHTML = "";
window.google.accounts.id.initialize({
client_id: state.config.auth.google.client_id,
callback: ({ credential }) => signInWithGoogle(credential),
});
apiKeyInput.value = payload.api_key;
document.querySelector("#workspaceId").value = payload.workspace.id;
render("#quotaOutput", payload.quota);
render("#workspaceOutput", payload.workspace);
});
document.querySelector("#quotaBtn").addEventListener("click", async () => {
render("#quotaOutput", await request("/quota"));
});
document.querySelector("#workspaceBtn").addEventListener("click", async () => {
const payload = await request("/workspaces", {
method: "POST",
body: JSON.stringify({ name: document.querySelector("#workspaceName").value }),
window.google.accounts.id.renderButton(googleBox, {
theme: "outline",
size: "large",
shape: "pill",
text: "signup_with",
width: 320,
});
document.querySelector("#workspaceId").value = payload.id;
render("#workspaceOutput", payload);
});
}
document.querySelector("#runBtn").addEventListener("click", async () => {
const workspaceId = document.querySelector("#workspaceId").value.trim();
const body = {
template_id: document.querySelector("#templateId").value,
inputs: { prompt: document.querySelector("#prompt").value },
};
if (workspaceId) body.workspace_id = workspaceId;
render("#runOutput", await request("/workflow-runs", { method: "POST", body: JSON.stringify(body) }));
});
function renderStatusPills() {
if (!state.config) return "";
return `
<div class="pill-row">
<span class="pill">API: ${escapeHtml(state.config.public_api_base_url)}</span>
<span class="pill">Web: ${escapeHtml(state.config.public_base_url)}</span>
<span class="pill">DevCloud API Image Ready</span>
</div>
`;
}
function renderSignin() {
const lyConfig = state.config?.auth?.ly_sso || {};
const googleConfig = state.config?.auth?.google || {};
const googleEnabled = Boolean(googleConfig.enabled && googleConfig.client_id);
app.innerHTML = `
<main class="signin-layout">
<section class="signin-brand">
<div class="brand-mark">A</div>
<p class="brand-eyebrow">Aurask</p>
<h1>Ship AI workflows with private knowledge, safer by default.</h1>
<p class="brand-copy">
Sign in to open your personal workspace, manage Langflow workflows, and use AnythingLLM knowledge bases from one portal.
</p>
${renderStatusPills()}
</section>
<section class="signin-card">
<div class="signin-header">
<p class="section-kicker">Welcome back</p>
<h2>Sign in to Aurask</h2>
<p>Use your organization account first, or enable Google one-tap registration for self-serve users.</p>
</div>
<div class="signin-actions">
<button id="lyButton" class="auth-btn auth-btn-primary" type="button">Use LY SSO</button>
<button id="googleFallbackButton" class="auth-btn auth-btn-secondary" type="button" ${googleEnabled ? "" : "disabled"}>
Continue with Google
</button>
</div>
<div id="googleButton" class="google-box ${googleEnabled ? "" : "is-hidden"}"></div>
${
googleEnabled
? '<p class="signin-tip">Google new-user login provisions a dedicated Aurask workspace on first sign-in.</p>'
: '<p class="signin-tip">Set `AURASK_GOOGLE_CLIENT_ID` to enable Google one-tap registration.</p>'
}
${
state.lyFormOpen
? `
<form id="lyForm" class="ly-form">
<label>
Username
<input name="username" value="${escapeHtml(lyConfig.username_hint || "ly-xujian1")}" autocomplete="username" />
</label>
<label>
Password
<input name="password" type="password" autocomplete="current-password" placeholder="Enter LY SSO password" />
</label>
<button class="auth-btn auth-btn-primary" type="submit">Sign in with LY SSO</button>
</form>
`
: ""
}
${state.error ? `<p class="message error">${escapeHtml(state.error)}</p>` : ""}
<div class="signin-footer">
<span>Need a first workspace?</span>
<span>Aurask creates one automatically after your first successful login.</span>
</div>
</section>
</main>
`;
document.querySelector("#lyButton")?.addEventListener("click", () => {
if (lyConfig.login_url) {
window.location.href = lyConfig.login_url;
return;
}
state.lyFormOpen = !state.lyFormOpen;
render();
mountGoogleButton();
});
document.querySelector("#lyForm")?.addEventListener("submit", submitLySso);
document.querySelector("#googleFallbackButton")?.addEventListener("click", () => {
document.querySelector("#googleButton")?.scrollIntoView({ behavior: "smooth", block: "center" });
});
}
function renderDashboard() {
const user = state.profile?.user || {};
const tenant = state.profile?.tenant || {};
const workspace = state.profile?.workspace || {};
const quota = state.profile?.quota || {};
const config = state.profile?.config || state.config || {};
const langflowUrl = withEmbedContext(config.embeds?.langflow_url);
const anythingllmUrl = withEmbedContext(config.embeds?.anythingllm_url);
app.innerHTML = `
<main class="dashboard-shell">
<header class="topbar">
<div class="topbar-left">
<div class="brand-mark brand-mark-small">A</div>
<div>
<p class="section-kicker">Aurask Workspace</p>
<h1>${escapeHtml(tenant.name || "Aurask")}</h1>
</div>
</div>
<div class="topbar-right">
<nav class="tabbar" aria-label="Workspace sections">
<button class="tab ${state.activeTab === "workflows" ? "is-active" : ""}" data-tab="workflows" type="button">Workflows</button>
<button class="tab ${state.activeTab === "knowledge" ? "is-active" : ""}" data-tab="knowledge" type="button">Knowledge Base</button>
</nav>
<details class="profile-menu">
<summary>
${user.avatar_url ? `<img class="avatar-image" src="${escapeHtml(user.avatar_url)}" alt="${escapeHtml(user.display_name || user.email)}" />` : `<span class="avatar">${escapeHtml(initials(user.display_name || user.email))}</span>`}
<span class="profile-name">${escapeHtml(user.display_name || user.email || "Profile")}</span>
</summary>
<div class="profile-panel">
<div class="profile-block">
<strong>${escapeHtml(user.display_name || user.email || "")}</strong>
<span>${escapeHtml(user.email || "")}</span>
</div>
<div class="profile-block">
<span>Tenant</span>
<strong>${escapeHtml(tenant.name || "")}</strong>
</div>
<div class="profile-block">
<span>Workspace</span>
<strong>${escapeHtml(workspace.name || "")}</strong>
</div>
<div class="profile-block profile-grid">
<span>Plan</span>
<strong>${escapeHtml(quota.plan_code || "")}</strong>
<span>TBU</span>
<strong>${escapeHtml(String(quota.available_tbu ?? 0))}</strong>
<span>Knowledge Bases</span>
<strong>${escapeHtml(String(quota.knowledge_bases ?? 0))}</strong>
</div>
<button id="logoutButton" class="auth-btn auth-btn-secondary" type="button">Sign out</button>
</div>
</details>
</div>
</header>
<section class="dashboard-grid">
<aside class="side-panel">
<section class="info-card">
<p class="section-kicker">Personal Center</p>
<h2>${escapeHtml(user.display_name || user.email || "Aurask User")}</h2>
<p>${escapeHtml(user.email || "")}</p>
<div class="stat-list">
<div class="stat-item">
<span>Tenant ID</span>
<strong>${escapeHtml(tenant.id || "")}</strong>
</div>
<div class="stat-item">
<span>Workspace ID</span>
<strong>${escapeHtml(workspace.id || "")}</strong>
</div>
<div class="stat-item">
<span>API Image</span>
<strong>${escapeHtml(config.devcloud?.api_image || "")}</strong>
</div>
<div class="stat-item">
<span>Web Image</span>
<strong>${escapeHtml(config.devcloud?.web_image || "")}</strong>
</div>
</div>
</section>
<section class="info-card">
<p class="section-kicker">Deploy Defaults</p>
<ul class="plain-list">
<li>API Gateway: ${escapeHtml(config.public_api_base_url || "")}</li>
<li>Langflow Embed: ${escapeHtml(config.embeds?.langflow_url || "")}</li>
<li>AnythingLLM Embed: ${escapeHtml(config.embeds?.anythingllm_url || "")}</li>
</ul>
</section>
</aside>
<section class="workspace-panel">
<div class="embed-card ${state.activeTab === "workflows" ? "" : "is-hidden"}">
<div class="embed-header">
<div>
<p class="section-kicker">Workflow Studio</p>
<h2>Langflow</h2>
</div>
<a href="${escapeHtml(langflowUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
</div>
<iframe title="Aurask Langflow" src="${escapeHtml(langflowUrl)}"></iframe>
</div>
<div class="embed-card ${state.activeTab === "knowledge" ? "" : "is-hidden"}">
<div class="embed-header">
<div>
<p class="section-kicker">Knowledge Base</p>
<h2>AnythingLLM</h2>
</div>
<a href="${escapeHtml(anythingllmUrl)}" target="_blank" rel="noreferrer">Open in new tab</a>
</div>
<iframe title="Aurask AnythingLLM" src="${escapeHtml(anythingllmUrl)}"></iframe>
</div>
</section>
</section>
${state.error ? `<div class="toast error">${escapeHtml(state.error)}</div>` : ""}
</main>
`;
document.querySelectorAll("[data-tab]").forEach((button) => {
button.addEventListener("click", () => setTab(button.dataset.tab));
});
document.querySelector("#logoutButton")?.addEventListener("click", logout);
}
function renderLoading() {
app.innerHTML = `
<main class="loading-shell">
<div class="loader"></div>
<p>Loading Aurask...</p>
</main>
`;
}
function render() {
if (state.loading) {
renderLoading();
return;
}
if (state.profile) {
renderDashboard();
return;
}
renderSignin();
}
window.addEventListener("popstate", render);
if (state.sessionToken) {
document.addEventListener("visibilitychange", async () => {
if (document.visibilityState !== "visible" || !state.profile) return;
try {
state.profile = await request("/auth/session");
render();
} catch (error) {
persistSession("");
state.profile = null;
state.error = "Your Aurask session has expired. Please sign in again.";
ensureSigninRoute();
render();
mountGoogleButton();
}
});
}
const googleScript = document.createElement("script");
googleScript.src = "https://accounts.google.com/gsi/client";
googleScript.async = true;
googleScript.defer = true;
googleScript.onload = mountGoogleButton;
document.head.appendChild(googleScript);
bootstrap();

View File

@ -1,83 +1,584 @@
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #f6f7fb;
color: #182033;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
background: #f6f8fb;
color: #102033;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, rgba(76, 111, 255, 0.2), transparent 34rem),
radial-gradient(circle at bottom right, rgba(14, 165, 233, 0.16), transparent 30rem),
#f7f9fc;
}
.shell {
max-width: 1120px;
margin: 0 auto;
padding: 40px 20px;
}
.hero {
padding: 32px;
border-radius: 24px;
background: linear-gradient(135deg, #172554, #2563eb);
color: white;
margin-bottom: 24px;
}
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.14em;
opacity: 0.8;
}
.grid {
display: grid;
gap: 20px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
}
.card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 20px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 16px 40px rgba(15, 23, 42, 0.06);
}
label {
display: grid;
gap: 8px;
margin: 12px 0;
font-weight: 600;
}
input,
textarea {
border: 1px solid #cbd5e1;
border-radius: 12px;
padding: 12px;
button,
input {
font: inherit;
}
textarea {
min-height: 100px;
}
button {
border: 0;
border-radius: 999px;
background: #2563eb;
color: white;
padding: 12px 18px;
font-weight: 700;
cursor: pointer;
}
pre {
overflow: auto;
background: #0f172a;
color: #dbeafe;
border-radius: 14px;
padding: 16px;
min-height: 72px;
.app-shell,
.signin-layout,
.dashboard-shell,
.loading-shell {
min-height: 100vh;
}
.signin-layout {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(360px, 470px);
gap: 48px;
align-items: center;
width: min(1180px, calc(100% - 40px));
margin: 0 auto;
padding: 54px 0;
}
.signin-brand {
padding: 48px;
color: #f8fbff;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 34px;
background:
linear-gradient(145deg, rgba(13, 33, 90, 0.96), rgba(37, 99, 235, 0.9)),
#132965;
box-shadow: 0 28px 80px rgba(37, 99, 235, 0.24);
}
.brand-mark {
display: grid;
width: 58px;
height: 58px;
place-items: center;
border-radius: 18px;
color: #1d4ed8;
background: #ffffff;
font-size: 28px;
font-weight: 900;
box-shadow: 0 18px 46px rgba(15, 23, 42, 0.18);
}
.brand-mark-small {
width: 44px;
height: 44px;
border-radius: 14px;
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #06b6d4);
font-size: 22px;
}
.brand-eyebrow,
.section-kicker {
margin: 0;
color: #2563eb;
font-size: 12px;
font-weight: 800;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.signin-brand .brand-eyebrow,
.signin-brand .section-kicker {
color: #bfdbfe;
}
.signin-brand h1 {
max-width: 760px;
margin: 28px 0 18px;
font-size: clamp(38px, 6vw, 76px);
line-height: 0.95;
letter-spacing: -0.06em;
}
.brand-copy {
max-width: 640px;
margin: 0;
color: rgba(248, 251, 255, 0.78);
font-size: 18px;
line-height: 1.65;
}
.pill-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 34px;
}
.pill {
display: inline-flex;
align-items: center;
min-height: 34px;
padding: 6px 12px;
color: #dbeafe;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
font-size: 12px;
font-weight: 700;
}
.signin-card {
padding: 34px;
border: 1px solid #e4e9f2;
border-radius: 30px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 30px 90px rgba(15, 23, 42, 0.12);
backdrop-filter: blur(16px);
}
.signin-header h2 {
margin: 8px 0 8px;
color: #0f172a;
font-size: 30px;
letter-spacing: -0.03em;
}
.signin-header p {
margin: 0;
color: #667085;
line-height: 1.6;
}
.signin-actions,
.ly-form {
display: grid;
gap: 14px;
margin-top: 26px;
}
.auth-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: 48px;
padding: 12px 18px;
border: 0;
border-radius: 999px;
font-weight: 800;
transition:
transform 160ms ease,
box-shadow 160ms ease,
opacity 160ms ease;
}
.auth-btn:hover:not(:disabled) {
transform: translateY(-1px);
}
.auth-btn:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.auth-btn-primary {
color: #ffffff;
background: linear-gradient(135deg, #2563eb, #0ea5e9);
box-shadow: 0 16px 38px rgba(37, 99, 235, 0.24);
}
.auth-btn-secondary {
color: #0f172a;
background: #eef4ff;
border: 1px solid #dbe7ff;
}
.google-box {
display: flex;
justify-content: center;
min-height: 48px;
margin-top: 14px;
}
.ly-form label {
display: grid;
gap: 8px;
color: #344054;
font-weight: 700;
}
.ly-form input {
width: 100%;
min-height: 48px;
padding: 12px 14px;
color: #101828;
border: 1px solid #d0d5dd;
border-radius: 14px;
outline: none;
}
.ly-form input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.1);
}
.signin-tip,
.signin-footer,
.message {
color: #667085;
font-size: 13px;
line-height: 1.55;
}
.signin-footer {
display: grid;
gap: 2px;
margin-top: 28px;
padding-top: 18px;
border-top: 1px solid #edf2f7;
}
.error {
color: #b42318;
}
.dashboard-shell {
width: min(1440px, calc(100% - 32px));
margin: 0 auto;
padding: 18px 0 28px;
}
.topbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
min-height: 76px;
padding: 14px 18px;
border: 1px solid rgba(226, 232, 240, 0.92);
border-radius: 24px;
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.08);
backdrop-filter: blur(14px);
}
.topbar-left,
.topbar-right {
display: flex;
align-items: center;
gap: 14px;
}
.topbar h1 {
margin: 2px 0 0;
font-size: 20px;
letter-spacing: -0.02em;
}
.tabbar {
display: inline-flex;
gap: 6px;
padding: 5px;
border: 1px solid #e2e8f0;
border-radius: 999px;
background: #f8fafc;
}
.tab {
min-height: 38px;
padding: 0 16px;
color: #475569;
border: 0;
border-radius: 999px;
background: transparent;
font-weight: 800;
}
.tab.is-active {
color: #ffffff;
background: #2563eb;
box-shadow: 0 10px 24px rgba(37, 99, 235, 0.18);
}
.profile-menu {
position: relative;
}
.profile-menu summary {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 44px;
padding: 4px 12px 4px 4px;
border: 1px solid #e2e8f0;
border-radius: 999px;
background: #ffffff;
list-style: none;
cursor: pointer;
}
.profile-menu summary::-webkit-details-marker {
display: none;
}
.avatar,
.avatar-image {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border-radius: 999px;
color: #ffffff;
background: linear-gradient(135deg, #0f172a, #2563eb);
font-weight: 900;
}
.avatar-image {
object-fit: cover;
}
.profile-name {
max-width: 140px;
overflow: hidden;
color: #0f172a;
font-size: 14px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-panel {
position: absolute;
top: calc(100% + 10px);
right: 0;
display: grid;
gap: 14px;
width: 310px;
padding: 18px;
border: 1px solid #e2e8f0;
border-radius: 22px;
background: #ffffff;
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.16);
}
.profile-block {
display: grid;
gap: 4px;
color: #667085;
font-size: 13px;
}
.profile-block strong {
overflow-wrap: anywhere;
color: #0f172a;
}
.profile-grid {
grid-template-columns: 1fr auto;
}
.dashboard-grid {
display: grid;
grid-template-columns: 340px minmax(0, 1fr);
gap: 18px;
margin-top: 18px;
}
.side-panel {
display: grid;
align-content: start;
gap: 18px;
}
.info-card,
.embed-card {
border: 1px solid #e2e8f0;
border-radius: 24px;
background: rgba(255, 255, 255, 0.94);
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.07);
}
.info-card {
padding: 22px;
}
.info-card h2 {
margin: 8px 0 4px;
font-size: 22px;
letter-spacing: -0.03em;
}
.info-card p {
margin: 0;
color: #667085;
}
.stat-list {
display: grid;
gap: 12px;
margin-top: 20px;
}
.stat-item {
display: grid;
gap: 4px;
padding: 12px;
border-radius: 16px;
background: #f8fafc;
}
.stat-item span,
.plain-list {
color: #667085;
font-size: 13px;
}
.stat-item strong {
overflow-wrap: anywhere;
color: #0f172a;
font-size: 13px;
}
.plain-list {
display: grid;
gap: 10px;
padding-left: 18px;
}
.workspace-panel {
min-width: 0;
}
.embed-card {
overflow: hidden;
}
.embed-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
min-height: 76px;
padding: 18px 22px;
border-bottom: 1px solid #e2e8f0;
}
.embed-header h2 {
margin: 4px 0 0;
font-size: 24px;
}
.embed-header a {
color: #2563eb;
font-size: 14px;
font-weight: 800;
text-decoration: none;
}
iframe {
display: block;
width: 100%;
height: calc(100vh - 150px);
min-height: 680px;
border: 0;
background: #f8fafc;
}
.loading-shell {
display: grid;
place-items: center;
align-content: center;
gap: 16px;
color: #475569;
}
.loader {
width: 44px;
height: 44px;
border: 4px solid #dbeafe;
border-top-color: #2563eb;
border-radius: 999px;
animation: spin 900ms linear infinite;
}
.toast {
position: fixed;
right: 24px;
bottom: 24px;
max-width: 420px;
padding: 14px 18px;
border: 1px solid #fecaca;
border-radius: 18px;
background: #fff1f2;
box-shadow: 0 18px 50px rgba(15, 23, 42, 0.1);
}
.is-hidden {
display: none !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 1020px) {
.signin-layout,
.dashboard-grid {
grid-template-columns: 1fr;
}
.signin-brand {
padding: 34px;
}
.topbar {
align-items: stretch;
flex-direction: column;
}
.topbar-right {
justify-content: space-between;
}
}
@media (max-width: 680px) {
.signin-layout,
.dashboard-shell {
width: min(100% - 20px, 100%);
}
.signin-card,
.signin-brand,
.info-card {
border-radius: 22px;
padding: 22px;
}
.topbar-right {
align-items: stretch;
flex-direction: column-reverse;
}
.tabbar,
.profile-menu summary,
.profile-panel {
width: 100%;
}
.profile-panel {
position: static;
margin-top: 10px;
}
iframe {
min-height: 560px;
}
}

View File

@ -0,0 +1,77 @@
from __future__ import annotations
import base64
import json
import os
import sys
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "api"))
from aurask.app import create_app
class AuthSessionTests(unittest.TestCase):
def setUp(self) -> None:
self.env_keys = [
"AURASK_LY_SSO_PASSWORD",
"AURASK_GOOGLE_ENABLED",
"AURASK_GOOGLE_ALLOW_UNVERIFIED_ID_TOKEN",
"AURASK_GOOGLE_CLIENT_ID",
]
self.old_env = {key: os.environ.get(key) for key in self.env_keys}
def tearDown(self) -> None:
for key, value in self.old_env.items():
if value is None:
os.environ.pop(key, None)
else:
os.environ[key] = value
def test_ly_sso_login_creates_session_and_workspace(self) -> None:
os.environ["AURASK_LY_SSO_PASSWORD"] = "local-secret"
app = create_app(None, reset=True)
payload = app.login_with_ly_sso(username="ly-xujian1", password="local-secret")
context = app.auth.authenticate(f"Bearer {payload['token']}")
self.assertEqual(context["token_type"], "session")
self.assertEqual(payload["workspace"]["tenant_id"], payload["tenant"]["id"])
self.assertEqual(payload["quota"]["plan_code"], "free_trial")
self.assertEqual(len(app.list_workspaces(payload["tenant"]["id"])), 1)
def test_google_first_login_reuses_workspace_after_registration(self) -> None:
os.environ["AURASK_GOOGLE_ENABLED"] = "true"
os.environ["AURASK_GOOGLE_ALLOW_UNVERIFIED_ID_TOKEN"] = "true"
os.environ["AURASK_GOOGLE_CLIENT_ID"] = "local-google-client"
app = create_app(None, reset=True)
token = self._fake_google_token(
{
"sub": "google-user-1",
"email": "founder@example.com",
"email_verified": True,
"name": "Aurask Founder",
}
)
first_login = app.login_with_google(id_token=token)
second_login = app.login_with_google(id_token=token)
self.assertTrue(first_login["is_new_user"])
self.assertFalse(second_login["is_new_user"])
self.assertEqual(first_login["workspace"]["id"], second_login["workspace"]["id"])
self.assertEqual(len(app.list_workspaces(first_login["tenant"]["id"])), 1)
def _fake_google_token(self, claims: dict) -> str:
header = self._base64url({"alg": "none", "typ": "JWT"})
payload = self._base64url(claims)
return f"{header}.{payload}.signature"
def _base64url(self, payload: dict) -> str:
encoded = base64.urlsafe_b64encode(json.dumps(payload).encode("utf-8")).decode("utf-8")
return encoded.rstrip("=")
if __name__ == "__main__":
unittest.main()