mirror of
https://18126008609:longquanjian123@gitee.com/feigong123/aurask.git
synced 2026-04-19 18:20:35 +00:00
Add portal sign-in flow and DevCloud deployment defaults
All checks were successful
aurask-release / build-and-deploy (push) Successful in 2m11s
All checks were successful
aurask-release / build-and-deploy (push) Successful in 2m11s
This commit is contained in:
parent
1ae23d44c1
commit
c44746a5a8
@ -1,531 +1,331 @@
|
|||||||
# Aurask 技术与运营方案(当前实现路径版)
|
# Aurask Technical Operations Plan
|
||||||
|
|
||||||
> 本文档基于当前仓库实现、`AGENTS.md` 约束与 `deploy/k3s/README.md` 部署规划更新。
|
> 更新日期:2026-04-19
|
||||||
> 核心方向不变:面向海外用户,用 Langflow 承载模板化工作流,用 AnythingLLM 承载知识库/RAG,用 Aurask 自身网关统一做鉴权、套餐、TBU、订单、审计和运维闭环。
|
> 本文档已按当前代码实现、目录结构、门户登录流与 DevCloud 默认部署方式同步更新。
|
||||||
|
|
||||||
## 0. 当前结论
|
## 1. 当前阶段定位
|
||||||
|
|
||||||
Aurask 当前已完成 **可运行 MVP 后端骨架**:
|
Aurask 当前处于 **可运行 MVP / 初版门户** 阶段,已覆盖:
|
||||||
|
|
||||||
- 使用 Python 模块化单体实现首版领域闭环。
|
- Python 模块化后端
|
||||||
- 已具备 `Auth + API Gateway`、`Billing + Quota + TBU Ledger`、`Workflow Orchestrator`、`Knowledge Base`、`USDT-TRC20 Payment`、`Audit` 等核心边界。
|
- 标准库 HTTP Gateway
|
||||||
- 已按产品职责划分根目录:`api`、`protal`、`manager`、`deploy`。
|
- 租户、用户、API Key、Session
|
||||||
- 已补充 PostgreSQL、PGVector、Redis、AnythingLLM、Langflow 的桥接配置与接口契约。
|
- `LY SSO` / Google 登录入口
|
||||||
- 已提供 CLI 演示与标准库 HTTP 网关。
|
- 首次登录自动创建独立 workspace
|
||||||
- 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。
|
- 配额 / 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
|
||||||
|
|
||||||
目标能力:
|
## 2. 当前目录结构
|
||||||
|
|
||||||
- 用户无需自行部署 Langflow / AnythingLLM。
|
|
||||||
- 用户通过 Aurask 选择审核过的数字员工模板。
|
|
||||||
- 用户可绑定知识库 Workspace,并通过 RAG 支撑业务问答。
|
|
||||||
- Aurask 统一管理租户、套餐、TBU、订单、支付、审计和成本。
|
|
||||||
- 首期支持 USDT-TRC20 支付,后续接入 Stripe / Paddle / Lemon Squeezy 等合规渠道。
|
|
||||||
|
|
||||||
## 2. 当前代码实现
|
|
||||||
|
|
||||||
### 2.1 当前目录结构
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
api/
|
api/
|
||||||
README.md # API 与外部组件桥接说明
|
README.md
|
||||||
requests/
|
|
||||||
aurask-api.http # 前端到后端请求样例
|
|
||||||
aurask/
|
aurask/
|
||||||
__init__.py # 包入口,导出 CLI main
|
api.py
|
||||||
__main__.py # python -m aurask 入口
|
app.py
|
||||||
app.py # 应用装配与 demo bootstrap
|
auth.py
|
||||||
api.py # 标准库 HTTP 网关
|
billing.py
|
||||||
bridge_status.py # 组件桥接状态
|
bridges/
|
||||||
cli.py # aurask demo / aurask serve
|
knowledge_base.py
|
||||||
repository.py # MVP JSON 持久化
|
orchestrator.py
|
||||||
errors.py # 应用错误类型
|
payments.py
|
||||||
ids.py # ID 与时间工具
|
plans.py
|
||||||
audit.py # 审计事件
|
quota.py
|
||||||
auth.py # 租户、用户、API Key
|
repository.py
|
||||||
plans.py # 套餐与商品目录
|
site_config.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
|
|
||||||
protal/
|
protal/
|
||||||
index.html # 用户前端使用面板
|
index.html
|
||||||
main.js
|
main.js
|
||||||
styles.css
|
styles.css
|
||||||
manager/
|
manager/
|
||||||
index.html # 管理员前端使用面板
|
index.html
|
||||||
main.js
|
main.js
|
||||||
styles.css
|
styles.css
|
||||||
|
deploy/
|
||||||
|
k3s/
|
||||||
|
README.md
|
||||||
|
base/
|
||||||
tests/
|
tests/
|
||||||
test_mvp.py # MVP 核心流程测试
|
test_auth_sessions.py
|
||||||
deploy/k3s/
|
test_bridges.py
|
||||||
README.md # 300 MAU k3s 部署规划
|
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 API(Workspace、文档、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 送 100;2000 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。
|
- `protal/` 目录拼写继续保持既定要求
|
||||||
- 新增栏位不赠送 TBU。
|
- `api/` 负责后端、桥接与门户配置
|
||||||
- k3s 高可用部署成本通常高于单机 VPS,财务模型要单独列“稳定性成本”。
|
- `deploy/k3s/base/` 现已开始保存基础部署清单
|
||||||
|
|
||||||
## 12. 落地路线图
|
## 3. 当前技术架构
|
||||||
|
|
||||||
### Phase 0:已完成
|
### 3.1 用户访问路径
|
||||||
|
|
||||||
- Python 模块化单体。
|
```text
|
||||||
- CLI demo。
|
Browser
|
||||||
- HTTP API。
|
-> aurask.xyz/signin
|
||||||
- TBU 预扣与结算。
|
-> Aurask Web Protal
|
||||||
- Workspace 与租户绑定。
|
-> Aurask API Gateway
|
||||||
- USDT 订单与支付匹配骨架。
|
-> Session / Quota / Workflow / Knowledge services
|
||||||
- MVP 测试。
|
-> Langflow / AnythingLLM / PostgreSQL / Redis
|
||||||
- k3s 300 MAU 部署规划。
|
```
|
||||||
|
|
||||||
### 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 3:k3s 部署
|
- `Workflows` 页签:内嵌 Langflow
|
||||||
|
- `Knowledge Base` 页签:内嵌 AnythingLLM
|
||||||
|
- 右上角个人中心:展示用户、租户、workspace、套餐与退出登录
|
||||||
|
|
||||||
- 编写 `deploy/k3s/base` manifests。
|
## 4. 登录与身份模型
|
||||||
- 拆分 `aurask-api` 与 `aurask-worker`。
|
|
||||||
- 部署 PostgreSQL、Redis、Ingress、Secret、NetworkPolicy。
|
|
||||||
- 接入 Prometheus / Loki / Grafana / Alertmanager。
|
|
||||||
|
|
||||||
### 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` 通过。
|
当前保留两类 Bearer Token:
|
||||||
- `uv run aurask demo --reset` 可创建租户、分配套餐、创建 Workspace、运行模板工作流。
|
|
||||||
- 余额不足时阻止工作流执行。
|
|
||||||
- TBU 加购支付后能发放额度。
|
|
||||||
- 文档上传能按租户与 Workspace 记录路径。
|
|
||||||
|
|
||||||
300 MAU 生产验收:
|
- `API Key`
|
||||||
|
- 面向接口调用
|
||||||
|
- `Session Token`
|
||||||
|
- 面向门户登录态
|
||||||
|
|
||||||
- API P95 延迟 `< 500ms`,不含外部模型调用。
|
### 4.3 首次登录自动开通
|
||||||
- 并发工作流 `10-20` 稳定运行。
|
|
||||||
- 工作流成功率 `95%+`。
|
|
||||||
- 支付匹配成功率 `99%+`。
|
|
||||||
- PostgreSQL 备份可恢复。
|
|
||||||
- Langflow / AnythingLLM 不暴露公网。
|
|
||||||
- NetworkPolicy 默认拒绝并按链路放通。
|
|
||||||
|
|
||||||
## 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`
|
- `README.md`
|
||||||
|
- `api/README.md`
|
||||||
|
- `protal/README.md`
|
||||||
- `deploy/k3s/README.md`
|
- `deploy/k3s/README.md`
|
||||||
- k3s HA:<https://docs.k3s.io/datastore/ha-embedded>
|
- `tests/test_auth_sessions.py`
|
||||||
- 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>
|
|
||||||
|
|||||||
180
README.md
180
README.md
@ -1,66 +1,60 @@
|
|||||||
## Aurask
|
## Aurask
|
||||||
|
|
||||||
Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,并按产品边界拆分为 `api`、`protal`、`manager`、`deploy` 四个根目录。
|
Aurask 当前是一个按产品边界拆分的首版实现,根目录保持为四个主要目录:
|
||||||
|
|
||||||
- `Auth + API Gateway`
|
- `api/`:后端网关、会话登录、配额/TBU、工作流、知识库桥接
|
||||||
- `Billing + Quota + TBU Ledger`
|
- `protal/`:用户门户,保留既定目录拼写
|
||||||
- `Workflow Orchestrator`
|
- `manager/`:管理员面板
|
||||||
- `Langflow` 模板化运行适配层
|
- `deploy/`:k3s 与 DevCloud 部署配置
|
||||||
- `AnythingLLM` Workspace / 文档接入适配层
|
|
||||||
- `USDT-TRC20` 订单与支付匹配
|
|
||||||
- `Audit / Usage / Observability` 基础留痕
|
|
||||||
|
|
||||||
当前实现是 **MVP 版本**:
|
当前版本已经补齐了用户门户登录闭环:
|
||||||
|
|
||||||
- 使用本地 JSON 文件持久化,便于开发和演示
|
- `/signin` 风格化登录页
|
||||||
- 保留了租户、订单、额度、模板、知识库、支付等核心领域边界
|
- `LY SSO` 登录入口
|
||||||
- 后续可以自然迁移到 PostgreSQL、任务队列、Runtime Pool 和真实外部服务
|
- 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
|
```powershell
|
||||||
$env:PYTHONPATH='api'
|
$env:PYTHONPATH='api'
|
||||||
py -3 -m aurask demo --reset
|
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 /health`
|
||||||
- `GET /plans`
|
- `GET /plans`
|
||||||
|
- `GET /auth/config`
|
||||||
|
- `POST /auth/ly-sso/login`
|
||||||
|
- `POST /auth/google/login`
|
||||||
- `POST /demo/bootstrap`
|
- `POST /demo/bootstrap`
|
||||||
- `POST /tenants`
|
- `POST /tenants`
|
||||||
|
|
||||||
鉴权后接口:
|
鉴权后接口:
|
||||||
|
|
||||||
|
- `GET /auth/session`
|
||||||
|
- `POST /auth/logout`
|
||||||
|
- `GET /me`
|
||||||
- `GET /quota`
|
- `GET /quota`
|
||||||
- `GET /workflow-templates`
|
- `GET /workflow-templates`
|
||||||
|
- `GET /workspaces`
|
||||||
- `POST /workspaces`
|
- `POST /workspaces`
|
||||||
- `POST /documents`
|
- `POST /documents`
|
||||||
- `POST /orders`
|
- `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 /workflow-runs/{run_id}`
|
||||||
- `GET /admin/bridge-status`
|
- `GET /admin/bridge-status`
|
||||||
|
|
||||||
鉴权方式:
|
Aurask 现在同时支持两类 Bearer Token:
|
||||||
|
|
||||||
```http
|
- API Key:用于原有 API/集成访问
|
||||||
Authorization: Bearer <api_key>
|
- 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. 创建演示租户:
|
```text
|
||||||
|
AURASK_USE_EXTERNAL_BRIDGES=true
|
||||||
```bash
|
AURASK_DATABASE_URL=postgresql://aurask:<password>@postgres.aurask-data.svc.cluster.local:5432/aurask
|
||||||
curl -X POST http://127.0.0.1:8080/demo/bootstrap ^
|
AURASK_REDIS_URL=redis://redis.aurask-data.svc.cluster.local:6379/0
|
||||||
-H "Content-Type: application/json" ^
|
AURASK_ANYTHINGLLM_BASE_URL=http://anythingllm.aurask-runtime.svc.cluster.local:3001
|
||||||
-d "{}"
|
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
|
已按 `devcloud` 仓库当前配置对齐默认镜像与路由:
|
||||||
curl http://127.0.0.1:8080/workflow-templates ^
|
|
||||||
-H "Authorization: Bearer <api_key>"
|
- 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
|
- MVP 业务闭环
|
||||||
curl -X POST http://127.0.0.1:8080/workspaces ^
|
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约
|
||||||
-H "Authorization: Bearer <api_key>" ^
|
- `LY SSO` / Google 登录与 session
|
||||||
-H "Content-Type: application/json" ^
|
|
||||||
-d "{\"name\":\"Support KB\"}"
|
|
||||||
```
|
|
||||||
|
|
||||||
4. 运行模板工作流:
|
## 相关文档
|
||||||
|
|
||||||
```bash
|
- `api/README.md`
|
||||||
curl -X POST http://127.0.0.1:8080/workflow-runs ^
|
- `protal/README.md`
|
||||||
-H "Authorization: Bearer <api_key>" ^
|
- `deploy/k3s/README.md`
|
||||||
-H "Content-Type: application/json" ^
|
- `Aurask_Technical_Operations_Plan.md`
|
||||||
-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/`: 单元测试
|
|
||||||
|
|||||||
147
api/README.md
147
api/README.md
@ -1,50 +1,129 @@
|
|||||||
# Aurask API
|
# 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
|
Aurask 现在支持两类 Bearer Token:
|
||||||
uv run aurask demo --reset
|
|
||||||
uv run aurask serve --reset --host 127.0.0.1 --port 8080
|
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
|
```text
|
||||||
$env:AURASK_USE_EXTERNAL_BRIDGES="true"
|
GET /auth/session
|
||||||
$env:AURASK_DATABASE_URL="postgresql://aurask:secret@postgres:5432/aurask"
|
POST /auth/logout
|
||||||
$env:AURASK_REDIS_URL="redis://redis:6379/0"
|
GET /me
|
||||||
$env:AURASK_ANYTHINGLLM_BASE_URL="http://anythingllm.aurask-runtime.svc.cluster.local:3001"
|
GET /quota
|
||||||
$env:AURASK_ANYTHINGLLM_API_KEY="<secret>"
|
GET /workflow-templates
|
||||||
$env:AURASK_LANGFLOW_BASE_URL="http://langflow-runtime.aurask-runtime.svc.cluster.local:7860"
|
GET /workspaces
|
||||||
$env:AURASK_LANGFLOW_API_KEY="<secret>"
|
POST /workspaces
|
||||||
uv run aurask serve --host 0.0.0.0 --port 8080
|
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
|
`GET /auth/config` 会向前端返回:
|
||||||
- `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
|
|
||||||
|
|
||||||
## 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"
|
||||||
```
|
```
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@ -28,18 +29,35 @@ def make_handler(app: AuraskApp):
|
|||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
self._handle("POST")
|
self._handle("POST")
|
||||||
|
|
||||||
|
def do_OPTIONS(self) -> None:
|
||||||
|
self._handle("OPTIONS")
|
||||||
|
|
||||||
def log_message(self, format: str, *args: Any) -> None:
|
def log_message(self, format: str, *args: Any) -> None:
|
||||||
app.audit.record("gateway.access", summary=format % args)
|
app.audit.record("gateway.access", summary=format % args)
|
||||||
|
|
||||||
def _handle(self, method: str) -> None:
|
def _handle(self, method: str) -> None:
|
||||||
path = urlparse(self.path).path.rstrip("/") or "/"
|
path = urlparse(self.path).path.rstrip("/") or "/"
|
||||||
try:
|
try:
|
||||||
|
if method == "OPTIONS":
|
||||||
|
self._send(204, {})
|
||||||
|
return
|
||||||
if method == "GET" and path == "/health":
|
if method == "GET" and path == "/health":
|
||||||
self._send(200, {"status": "ok", "service": "aurask"})
|
self._send(200, {"status": "ok", "service": "aurask"})
|
||||||
return
|
return
|
||||||
if method == "GET" and path == "/plans":
|
if method == "GET" and path == "/plans":
|
||||||
self._send(200, app.billing.list_plans())
|
self._send(200, app.billing.list_plans())
|
||||||
return
|
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":
|
if method == "POST" and path == "/demo/bootstrap":
|
||||||
body = self._read_json()
|
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")))
|
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"]
|
tenant_id = context["tenant"]["id"]
|
||||||
user_id = context["user"]["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":
|
if method == "GET" and path == "/admin/bridge-status":
|
||||||
self._send(200, bridge_status())
|
self._send(200, bridge_status())
|
||||||
return
|
return
|
||||||
@ -65,6 +89,9 @@ def make_handler(app: AuraskApp):
|
|||||||
if method == "GET" and path == "/workflow-templates":
|
if method == "GET" and path == "/workflow-templates":
|
||||||
self._send(200, {"templates": app.orchestrator.list_templates()})
|
self._send(200, {"templates": app.orchestrator.list_templates()})
|
||||||
return
|
return
|
||||||
|
if method == "GET" and path == "/workspaces":
|
||||||
|
self._send(200, {"workspaces": app.list_workspaces(tenant_id)})
|
||||||
|
return
|
||||||
if method == "POST" and path == "/workspaces":
|
if method == "POST" and path == "/workspaces":
|
||||||
body = self._read_json()
|
body = self._read_json()
|
||||||
self._send(201, app.knowledge.create_workspace(tenant_id, user_id, body.get("name", "")))
|
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:
|
if not length:
|
||||||
return {}
|
return {}
|
||||||
payload = self.rfile.read(length).decode("utf-8")
|
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")
|
encoded = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
|
||||||
self.send_response(status)
|
self.send_response(status)
|
||||||
self.send_header("Content-Type", "application/json; charset=utf-8")
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
self.send_header("Content-Length", str(len(encoded)))
|
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.end_headers()
|
||||||
self.wfile.write(encoded)
|
self.wfile.write(encoded)
|
||||||
|
|
||||||
|
|||||||
@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
from aurask.audit import AuditService
|
from aurask.audit import AuditService
|
||||||
from aurask.auth import AuthService
|
from aurask.auth import AuthService
|
||||||
@ -12,12 +16,14 @@ from aurask.billing import BillingService
|
|||||||
from aurask.bridges.anythingllm import AnythingLLMBridge
|
from aurask.bridges.anythingllm import AnythingLLMBridge
|
||||||
from aurask.bridges.config import BridgeConfig
|
from aurask.bridges.config import BridgeConfig
|
||||||
from aurask.bridges.langflow import LangflowBridge
|
from aurask.bridges.langflow import LangflowBridge
|
||||||
|
from aurask.errors import AuthError, ValidationError
|
||||||
from aurask.knowledge_base import KnowledgeBaseService
|
from aurask.knowledge_base import KnowledgeBaseService
|
||||||
from aurask.orchestrator import WorkflowOrchestrator
|
from aurask.orchestrator import WorkflowOrchestrator
|
||||||
from aurask.payments import PaymentService
|
from aurask.payments import PaymentService
|
||||||
from aurask.plans import seed_plan_catalog
|
from aurask.plans import seed_plan_catalog
|
||||||
from aurask.quota import QuotaService
|
from aurask.quota import QuotaService
|
||||||
from aurask.repository import JsonStore
|
from aurask.repository import JsonStore
|
||||||
|
from aurask.site_config import SiteConfig
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -30,6 +36,7 @@ class AuraskApp:
|
|||||||
payments: PaymentService
|
payments: PaymentService
|
||||||
knowledge: KnowledgeBaseService
|
knowledge: KnowledgeBaseService
|
||||||
orchestrator: WorkflowOrchestrator
|
orchestrator: WorkflowOrchestrator
|
||||||
|
site_config: SiteConfig
|
||||||
|
|
||||||
def bootstrap_demo(self, *, tenant_name: str = "Aurask Demo", email: str = "owner@example.com") -> dict:
|
def bootstrap_demo(self, *, tenant_name: str = "Aurask Demo", email: str = "owner@example.com") -> dict:
|
||||||
tenant = self.auth.create_tenant(tenant_name)
|
tenant = self.auth.create_tenant(tenant_name)
|
||||||
@ -44,6 +51,165 @@ class AuraskApp:
|
|||||||
"quota": self.quota.get_account(tenant["id"]),
|
"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:
|
def create_app(data_path: str | Path | None = None, *, reset: bool = False, use_external_bridges: bool | None = None) -> AuraskApp:
|
||||||
store = JsonStore(data_path)
|
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:
|
if use_external_bridges is None:
|
||||||
use_external_bridges = os.getenv("AURASK_USE_EXTERNAL_BRIDGES", "false").lower() in {"1", "true", "yes"}
|
use_external_bridges = os.getenv("AURASK_USE_EXTERNAL_BRIDGES", "false").lower() in {"1", "true", "yes"}
|
||||||
bridge_config = BridgeConfig.from_env()
|
bridge_config = BridgeConfig.from_env()
|
||||||
|
site_config = SiteConfig.from_env()
|
||||||
audit = AuditService(store)
|
audit = AuditService(store)
|
||||||
auth = AuthService(store, audit)
|
auth = AuthService(store, audit)
|
||||||
quota = QuotaService(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,
|
payments=payments,
|
||||||
knowledge=knowledge,
|
knowledge=knowledge,
|
||||||
orchestrator=orchestrator,
|
orchestrator=orchestrator,
|
||||||
|
site_config=site_config,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import secrets
|
import secrets
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
|
||||||
from aurask.audit import AuditService
|
from aurask.audit import AuditService
|
||||||
from aurask.errors import AuthError, ForbiddenError, NotFoundError, ValidationError
|
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"])
|
self.audit.record("tenant.created", tenant_id=tenant["id"], resource_type="tenant", resource_id=tenant["id"])
|
||||||
return tenant
|
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)
|
self._require_tenant(tenant_id)
|
||||||
if "@" not in email:
|
if "@" not in email:
|
||||||
raise ValidationError("valid email is required")
|
raise ValidationError("valid email is required")
|
||||||
@ -38,6 +47,8 @@ class AuthService:
|
|||||||
"id": new_id("user"),
|
"id": new_id("user"),
|
||||||
"tenant_id": tenant_id,
|
"tenant_id": tenant_id,
|
||||||
"email": email,
|
"email": email,
|
||||||
|
"display_name": display_name or email.split("@", 1)[0],
|
||||||
|
"avatar_url": avatar_url or "",
|
||||||
"role": role,
|
"role": role,
|
||||||
"status": "active",
|
"status": "active",
|
||||||
"api_key_preview": api_key[:10],
|
"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"])
|
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}
|
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:
|
def authenticate(self, authorization_header: str | None) -> dict:
|
||||||
if not authorization_header or not authorization_header.startswith("Bearer "):
|
bearer_token = self._bearer_token(authorization_header)
|
||||||
raise AuthError("missing bearer token")
|
key_record = self.store.get("api_keys", bearer_token)
|
||||||
api_key = authorization_header.removeprefix("Bearer ").strip()
|
|
||||||
key_record = self.store.get("api_keys", api_key)
|
|
||||||
if not key_record:
|
if not key_record:
|
||||||
raise AuthError("invalid bearer token")
|
return self._authenticate_session(bearer_token)
|
||||||
user = self.store.get("users", key_record["user_id"])
|
user = self.store.get("users", key_record["user_id"])
|
||||||
if not user or user.get("status") != "active":
|
if not user or user.get("status") != "active":
|
||||||
raise AuthError("inactive user")
|
raise AuthError("inactive user")
|
||||||
tenant = self.store.get("tenants", user["tenant_id"])
|
tenant = self.store.get("tenants", user["tenant_id"])
|
||||||
if not tenant or tenant.get("status") != "active":
|
if not tenant or tenant.get("status") != "active":
|
||||||
raise AuthError("inactive tenant")
|
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:
|
def require_tenant_user(self, tenant_id: str, user_id: str) -> None:
|
||||||
user = self.store.get("users", user_id)
|
user = self.store.get("users", user_id)
|
||||||
@ -75,3 +144,35 @@ class AuthService:
|
|||||||
if not tenant:
|
if not tenant:
|
||||||
raise NotFoundError("tenant not found")
|
raise NotFoundError("tenant not found")
|
||||||
return tenant
|
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}"
|
||||||
|
|||||||
@ -20,6 +20,8 @@ DEFAULT_DATA: dict[str, Any] = {
|
|||||||
"tenants": {},
|
"tenants": {},
|
||||||
"users": {},
|
"users": {},
|
||||||
"api_keys": {},
|
"api_keys": {},
|
||||||
|
"external_identities": {},
|
||||||
|
"sessions": {},
|
||||||
"plans": {},
|
"plans": {},
|
||||||
"subscriptions": {},
|
"subscriptions": {},
|
||||||
"quota_accounts": {},
|
"quota_accounts": {},
|
||||||
|
|||||||
84
api/aurask/site_config.py
Normal file
84
api/aurask/site_config.py
Normal 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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -4,6 +4,26 @@ GET http://127.0.0.1:8080/health
|
|||||||
### Plans
|
### Plans
|
||||||
GET http://127.0.0.1:8080/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
|
### Demo bootstrap
|
||||||
POST http://127.0.0.1:8080/demo/bootstrap
|
POST http://127.0.0.1:8080/demo/bootstrap
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
||||||
@ -16,6 +36,17 @@ Content-Type: application/json
|
|||||||
### Set API key after bootstrap
|
### Set API key after bootstrap
|
||||||
@api_key = ak_replace_me
|
@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
|
### Quota
|
||||||
GET http://127.0.0.1:8080/quota
|
GET http://127.0.0.1:8080/quota
|
||||||
Authorization: Bearer {{api_key}}
|
Authorization: Bearer {{api_key}}
|
||||||
@ -24,6 +55,10 @@ Authorization: Bearer {{api_key}}
|
|||||||
GET http://127.0.0.1:8080/workflow-templates
|
GET http://127.0.0.1:8080/workflow-templates
|
||||||
Authorization: Bearer {{api_key}}
|
Authorization: Bearer {{api_key}}
|
||||||
|
|
||||||
|
### List workspaces
|
||||||
|
GET http://127.0.0.1:8080/workspaces
|
||||||
|
Authorization: Bearer {{api_key}}
|
||||||
|
|
||||||
### Create workspace
|
### Create workspace
|
||||||
POST http://127.0.0.1:8080/workspaces
|
POST http://127.0.0.1:8080/workspaces
|
||||||
Authorization: Bearer {{api_key}}
|
Authorization: Bearer {{api_key}}
|
||||||
|
|||||||
@ -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
|
```text
|
||||||
deploy/k3s/
|
deploy/k3s/
|
||||||
README.md
|
README.md
|
||||||
base/
|
base/
|
||||||
namespaces.yaml
|
namespace.yaml
|
||||||
ingress.yaml
|
aurask-runtime-config.yaml
|
||||||
network-policies.yaml
|
aurask-config.yaml
|
||||||
secrets.example.yaml
|
aurask-api-pvc.yaml
|
||||||
aurask-api.yaml
|
aurask-api.yaml
|
||||||
aurask-worker.yaml
|
aurask-worker.yaml
|
||||||
aurask-web.yaml
|
aurask-web.yaml
|
||||||
aurask-manager.yaml
|
aurask-manager.yaml
|
||||||
langflow-runtime.yaml
|
postgres.yaml
|
||||||
anythingllm.yaml
|
|
||||||
postgres-cnpg.yaml
|
|
||||||
redis.yaml
|
redis.yaml
|
||||||
observability.yaml
|
kustomization.yaml
|
||||||
|
secrets.example.yaml
|
||||||
overlays/
|
overlays/
|
||||||
staging/
|
|
||||||
kustomization.yaml
|
|
||||||
production/
|
production/
|
||||||
kustomization.yaml
|
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 集群。
|
- `aurask-api`:`devcloud-trade-agent-1`
|
||||||
2. 配置公网 LB 与 DNS。
|
- `aurask-worker`:`devcloud-trade-agent-1`
|
||||||
3. 安装 `cert-manager`。
|
- `postgres`:`devcloud-trade-agent-1`
|
||||||
4. 安装 `Longhorn` 或云 CSI。
|
- `redis`:`devcloud-trade-agent-1`
|
||||||
5. 安装 observability 基础栈。
|
- `aurask-web`:`devcloud-trade-agent-2`
|
||||||
|
- `aurask-manager`:`devcloud-trade-agent-2`
|
||||||
|
|
||||||
### Phase 2:数据层
|
## Base 层职责
|
||||||
|
|
||||||
1. 安装 `CloudNativePG`。
|
`deploy/k3s/base/` 保持通用资源,不直接写死生产节点:
|
||||||
2. 初始化 PostgreSQL 与 PGVector。
|
|
||||||
3. 部署 PgBouncer。
|
|
||||||
4. 部署 Redis。
|
|
||||||
5. 配置对象存储与备份桶。
|
|
||||||
|
|
||||||
### Phase 3:Aurask 应用层
|
- `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 4:Runtime 层
|
### `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。
|
### `secrets.example.yaml`
|
||||||
2. 跑并发工作流压测。
|
|
||||||
3. 演练支付匹配。
|
|
||||||
4. 演练 PostgreSQL 备份恢复。
|
|
||||||
5. 演练节点故障与 Pod 重调度。
|
|
||||||
6. 检查 Langflow / AnythingLLM 未暴露公网。
|
|
||||||
|
|
||||||
## 16. 验收标准
|
新增门户与外部组件需要的 Secret 占位:
|
||||||
|
|
||||||
| 项目 | 目标 |
|
- `AURASK_DATABASE_URL`
|
||||||
| --- | --- |
|
- `AURASK_ANYTHINGLLM_API_KEY`
|
||||||
| API P95 | `< 500ms`,不含外部模型调用 |
|
- `AURASK_LANGFLOW_API_KEY`
|
||||||
| 峰值并发工作流 | `10-20` |
|
- `AURASK_LY_SSO_PASSWORD`
|
||||||
| 工作流成功率 | `95%+` |
|
- `AURASK_GOOGLE_CLIENT_ID`
|
||||||
| 支付匹配成功率 | `99%+` |
|
|
||||||
| 工作流排队 P95 | `< 10s` |
|
|
||||||
| PostgreSQL RPO | `<= 15 分钟` |
|
|
||||||
| PostgreSQL RTO | `<= 2 小时` |
|
|
||||||
| 月可用性 | `99%+` |
|
|
||||||
| 外部暴露面 | 仅 API、用户面板、管理面板 |
|
|
||||||
|
|
||||||
## 17. 当前代码差距
|
说明:
|
||||||
|
|
||||||
当前已经具备:
|
- `secrets.example.yaml` 仅作模板,不应直接提交真实密钥
|
||||||
|
- 生产建议继续使用 `External Secrets Operator` 或 `SOPS + age`
|
||||||
|
|
||||||
- 根目录划分:`api`、`protal`、`manager`、`deploy`。
|
## API / Web 部署说明
|
||||||
- PostgreSQL / PGVector / Redis / AnythingLLM / Langflow 桥接契约。
|
|
||||||
- `/admin/bridge-status` 桥接状态接口。
|
|
||||||
- 静态用户面板和管理面板。
|
|
||||||
- 请求样例:`api/requests/aurask-api.http`。
|
|
||||||
|
|
||||||
仍需补齐:
|
### `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/>
|
- 继续挂载 `/data/state.json`,兼容当前 MVP `JsonStore`
|
||||||
- CloudNativePG Documentation:<https://cloudnative-pg.io/documentation/current/>
|
- 同时读取:
|
||||||
- CloudNativePG Backup:<https://cloudnative-pg.io/documentation/current/backup/>
|
- `aurask-runtime-config`
|
||||||
- AnythingLLM System Requirements:<https://docs.anythingllm.com/installation-docker/system-requirements>
|
- `aurask-config`
|
||||||
- Langflow Security:<https://docs.langflow.org/security>
|
- `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 网关代理
|
||||||
|
|||||||
@ -33,9 +33,14 @@ spec:
|
|||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: aurask-runtime-config
|
name: aurask-runtime-config
|
||||||
|
- configMapRef:
|
||||||
|
name: aurask-config
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: aurask-runtime-secrets
|
name: aurask-runtime-secrets
|
||||||
optional: true
|
optional: true
|
||||||
|
- secretRef:
|
||||||
|
name: aurask-secrets
|
||||||
|
optional: true
|
||||||
ports:
|
ports:
|
||||||
- containerPort: 8080
|
- containerPort: 8080
|
||||||
name: http
|
name: http
|
||||||
|
|||||||
29
deploy/k3s/base/aurask-config.yaml
Normal file
29
deploy/k3s/base/aurask-config.yaml
Normal 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
|
||||||
@ -1,8 +1,10 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
|
namespace: aurask
|
||||||
resources:
|
resources:
|
||||||
- namespace.yaml
|
- namespace.yaml
|
||||||
- aurask-runtime-config.yaml
|
- aurask-runtime-config.yaml
|
||||||
|
- aurask-config.yaml
|
||||||
- aurask-api-pvc.yaml
|
- aurask-api-pvc.yaml
|
||||||
- aurask-api.yaml
|
- aurask-api.yaml
|
||||||
- aurask-worker.yaml
|
- aurask-worker.yaml
|
||||||
|
|||||||
12
deploy/k3s/base/secrets.example.yaml
Normal file
12
deploy/k3s/base/secrets.example.yaml
Normal 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>
|
||||||
@ -1,15 +1,45 @@
|
|||||||
# Aurask Protal
|
# Aurask Protal
|
||||||
|
|
||||||
`protal` is the user-facing panel directory. The name follows the requested root directory spelling.
|
`protal/` 是用户门户目录,目录名按既定要求保留为 `protal`。
|
||||||
|
|
||||||
Current MVP:
|
## 当前页面能力
|
||||||
|
|
||||||
- Static HTML panel
|
- 未登录时渲染 `/signin` 风格登录页
|
||||||
- Talks directly to Aurask API
|
- 支持 `LY SSO` 登录按钮与本地凭据提交
|
||||||
- Supports demo bootstrap, quota lookup, workspace creation, and template workflow run
|
- 支持 Google 登录入口
|
||||||
|
- 新用户首次登录自动创建独立 workspace
|
||||||
|
- 登录后显示:
|
||||||
|
- `Workflows`:内嵌 Langflow
|
||||||
|
- `Knowledge Base`:内嵌 AnythingLLM
|
||||||
|
- 右上角个人中心展示用户、租户、workspace 与基础额度信息
|
||||||
|
|
||||||
Open `index.html` in a browser after starting:
|
## 依赖接口
|
||||||
|
|
||||||
```bash
|
门户依赖以下 API:
|
||||||
uv run aurask serve --reset --host 127.0.0.1 --port 8080
|
|
||||||
|
- `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`
|
||||||
|
|||||||
@ -3,47 +3,11 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>Aurask User Protal</title>
|
<title>Aurask</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="stylesheet" href="./styles.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main class="shell">
|
<div id="app" class="app-shell"></div>
|
||||||
<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>
|
|
||||||
<script src="./main.js"></script>
|
<script src="./main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
487
protal/main.js
487
protal/main.js
@ -1,55 +1,458 @@
|
|||||||
const apiBaseInput = document.querySelector("#apiBase");
|
const STORAGE_KEY = "aurask.portal.session";
|
||||||
const apiKeyInput = document.querySelector("#apiKey");
|
const TAB_KEY = "aurask.portal.activeTab";
|
||||||
|
|
||||||
function apiBase() {
|
const app = document.querySelector("#app");
|
||||||
return apiBaseInput.value.replace(/\/$/, "");
|
|
||||||
|
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 = {}) {
|
async function request(path, options = {}) {
|
||||||
const headers = new Headers(options.headers || {});
|
const headers = new Headers(options.headers || {});
|
||||||
headers.set("Content-Type", "application/json");
|
headers.set("Accept", "application/json");
|
||||||
const apiKey = apiKeyInput.value.trim();
|
if (options.body !== undefined) headers.set("Content-Type", "application/json");
|
||||||
if (apiKey) headers.set("Authorization", `Bearer ${apiKey}`);
|
if (state.sessionToken) headers.set("Authorization", `Bearer ${state.sessionToken}`);
|
||||||
const response = await fetch(`${apiBase()}${path}`, { ...options, headers });
|
const response = await fetch(`${normalizeApiBase(state.config?.public_api_base_url)}${path}`, {
|
||||||
const payload = await response.json();
|
...options,
|
||||||
if (!response.ok) throw new Error(JSON.stringify(payload, null, 2));
|
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;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(target, payload) {
|
function persistSession(token) {
|
||||||
document.querySelector(target).textContent = JSON.stringify(payload, null, 2);
|
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 () => {
|
function setTab(tab) {
|
||||||
const payload = await request("/demo/bootstrap", {
|
state.activeTab = tab;
|
||||||
method: "POST",
|
window.localStorage.setItem(TAB_KEY, tab);
|
||||||
body: JSON.stringify({ tenant_name: "Aurask Protal Demo", email: "owner@example.com" }),
|
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("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
window.google.accounts.id.renderButton(googleBox, {
|
||||||
document.querySelector("#workspaceId").value = payload.workspace.id;
|
theme: "outline",
|
||||||
render("#quotaOutput", payload.quota);
|
size: "large",
|
||||||
render("#workspaceOutput", payload.workspace);
|
shape: "pill",
|
||||||
});
|
text: "signup_with",
|
||||||
|
width: 320,
|
||||||
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 }),
|
|
||||||
});
|
});
|
||||||
document.querySelector("#workspaceId").value = payload.id;
|
}
|
||||||
render("#workspaceOutput", payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelector("#runBtn").addEventListener("click", async () => {
|
function renderStatusPills() {
|
||||||
const workspaceId = document.querySelector("#workspaceId").value.trim();
|
if (!state.config) return "";
|
||||||
const body = {
|
return `
|
||||||
template_id: document.querySelector("#templateId").value,
|
<div class="pill-row">
|
||||||
inputs: { prompt: document.querySelector("#prompt").value },
|
<span class="pill">API: ${escapeHtml(state.config.public_api_base_url)}</span>
|
||||||
};
|
<span class="pill">Web: ${escapeHtml(state.config.public_base_url)}</span>
|
||||||
if (workspaceId) body.workspace_id = workspaceId;
|
<span class="pill">DevCloud API Image Ready</span>
|
||||||
render("#runOutput", await request("/workflow-runs", { method: "POST", body: JSON.stringify(body) }));
|
</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();
|
||||||
|
|||||||
@ -1,83 +1,584 @@
|
|||||||
:root {
|
:root {
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
font-family:
|
||||||
background: #f6f7fb;
|
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
color: #182033;
|
sans-serif;
|
||||||
|
background: #f6f8fb;
|
||||||
|
color: #102033;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
margin: 0;
|
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 {
|
button,
|
||||||
max-width: 1120px;
|
input {
|
||||||
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;
|
|
||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
textarea {
|
|
||||||
min-height: 100px;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 0;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #2563eb;
|
|
||||||
color: white;
|
|
||||||
padding: 12px 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
pre {
|
.app-shell,
|
||||||
overflow: auto;
|
.signin-layout,
|
||||||
background: #0f172a;
|
.dashboard-shell,
|
||||||
color: #dbeafe;
|
.loading-shell {
|
||||||
border-radius: 14px;
|
min-height: 100vh;
|
||||||
padding: 16px;
|
}
|
||||||
min-height: 72px;
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
77
tests/test_auth_sessions.py
Normal file
77
tests/test_auth_sessions.py
Normal 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()
|
||||||
Loading…
Reference in New Issue
Block a user