From 0a3f0585f497282d9fd119a1c18575c8806a8f0b Mon Sep 17 00:00:00 2001
From: Aaron <530816249@qq.com>
Date: Sun, 19 Apr 2026 15:04:11 +0800
Subject: [PATCH] Implement Aurask MVP backend and deployment plan
---
AGENTS.md | 451 +++++++++----------
Aurask_Technical_Operations_Plan.md | 652 +++++++++++++++-------------
README.md | 122 +++++-
deploy/k3s/README.md | 434 ++++++++++++++++++
pyproject.toml | 2 +-
src/aurask/__init__.py | 7 +-
src/aurask/__main__.py | 5 +
src/aurask/api.py | 153 +++++++
src/aurask/app.py | 66 +++
src/aurask/audit.py | 37 ++
src/aurask/auth.py | 77 ++++
src/aurask/billing.py | 99 +++++
src/aurask/cli.py | 65 +++
src/aurask/errors.py | 43 ++
src/aurask/ids.py | 14 +
src/aurask/knowledge_base.py | 113 +++++
src/aurask/orchestrator.py | 190 ++++++++
src/aurask/payments.py | 61 +++
src/aurask/plans.py | 117 +++++
src/aurask/quota.py | 170 ++++++++
src/aurask/repository.py | 94 ++++
tests/test_mvp.py | 83 ++++
22 files changed, 2531 insertions(+), 524 deletions(-)
create mode 100644 deploy/k3s/README.md
create mode 100644 src/aurask/__main__.py
create mode 100644 src/aurask/api.py
create mode 100644 src/aurask/app.py
create mode 100644 src/aurask/audit.py
create mode 100644 src/aurask/auth.py
create mode 100644 src/aurask/billing.py
create mode 100644 src/aurask/cli.py
create mode 100644 src/aurask/errors.py
create mode 100644 src/aurask/ids.py
create mode 100644 src/aurask/knowledge_base.py
create mode 100644 src/aurask/orchestrator.py
create mode 100644 src/aurask/payments.py
create mode 100644 src/aurask/plans.py
create mode 100644 src/aurask/quota.py
create mode 100644 src/aurask/repository.py
create mode 100644 tests/test_mvp.py
diff --git a/AGENTS.md b/AGENTS.md
index f03693c..4151b35 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,254 +1,261 @@
-[](https://travis-ci.org/sqshq/PiggyMetrics)
-[](https://codecov.io/github/sqshq/PiggyMetrics?branch=master)
-[](https://github.com/sqshq/PiggyMetrics/blob/master/LICENCE)
-[](https://gitter.im/sqshq/PiggyMetrics?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+# Aurask AGENTS.md
-# Piggy Metrics
+本文件是 `d:\JetBrains\git\aurask` 仓库的项目级实现约束。
+后续所有 Agent 在本仓库修改代码、文档、部署配置时,必须优先遵守本文件,并与 `Aurask_Technical_Operations_Plan.md` 保持一致。
-Piggy Metrics is a simple financial advisor app built to demonstrate the [Microservice Architecture Pattern](http://martinfowler.com/microservices/) using Spring Boot, Spring Cloud and Docker. The project is intended as a tutorial, but you are welcome to fork it and turn it into something else!
+## 1. 当前项目状态
-
+当前仓库是 Aurask 的 **Python 模块化单体 MVP**。
-
-
+已实现能力:
-## Functional services
+- 租户、用户与 API Key:`src/aurask/auth.py`
+- HTTP 网关:`src/aurask/api.py`
+- 套餐与商品目录:`src/aurask/plans.py`
+- 订单、订阅与权益发放:`src/aurask/billing.py`
+- TBU 额度、预扣、结算与账本:`src/aurask/quota.py`
+- USDT-TRC20 支付匹配:`src/aurask/payments.py`
+- 模板工作流编排:`src/aurask/orchestrator.py`
+- AnythingLLM Workspace / 文档接入适配:`src/aurask/knowledge_base.py`
+- 审计事件:`src/aurask/audit.py`
+- MVP JSON 持久化:`src/aurask/repository.py`
+- CLI:`src/aurask/cli.py`
+- 测试:`tests/test_mvp.py`
+- k3s 部署规划:`deploy/k3s/README.md`
-Piggy Metrics is decomposed into three core microservices. All of them are independently deployable applications organized around certain business domains.
+当前实现可用于本地演示、流程验证和接口联调;生产化目标是迁移到 PostgreSQL、队列、真实 Langflow/AnythingLLM 服务和 k3s 部署。
-
+## 2. 文档优先级
-#### Account service
-Contains general input logic and validation: incomes/expenses items, savings and account settings.
+实现时遵循:
-Method | Path | Description | User authenticated | Available from UI
-------------- | ------------------------- | ------------- |:-------------:|:----------------:|
-GET | /accounts/{account} | Get specified account data | |
-GET | /accounts/current | Get current account data | × | ×
-GET | /accounts/demo | Get demo account data (pre-filled incomes/expenses items, etc) | | ×
-PUT | /accounts/current | Save current account data | × | ×
-POST | /accounts/ | Register new account | | ×
+1. 用户明确要求
+2. 本 `AGENTS.md`
+3. `Aurask_Technical_Operations_Plan.md`
+4. `README.md`
+5. `deploy/k3s/README.md`
+6. 其他项目文档
+如果代码与方案冲突,优先修正为 Aurask 当前技术方案口径,不要恢复旧模板内容。
-#### Statistics service
-Performs calculations on major statistics parameters and captures time series for each account. Datapoint contains values normalized to base currency and time period. This data is used to track cash flow dynamics during the account lifetime.
+## 3. 目录与职责边界
-Method | Path | Description | User authenticated | Available from UI
-------------- | ------------------------- | ------------- |:-------------:|:----------------:|
-GET | /statistics/{account} | Get specified account statistics | |
-GET | /statistics/current | Get current account statistics | × | ×
-GET | /statistics/demo | Get demo account statistics | | ×
-PUT | /statistics/{account} | Create or update time series datapoint for specified account | |
-
-
-#### Notification service
-Stores user contact information and notification settings (reminders, backup frequency etc). Scheduled worker collects required information from other services and sends e-mail messages to subscribed customers.
-
-Method | Path | Description | User authenticated | Available from UI
-------------- | ------------------------- | ------------- |:-------------:|:----------------:|
-GET | /notifications/settings/current | Get current account notification settings | × | ×
-PUT | /notifications/settings/current | Save current account notification settings | × | ×
-
-#### Notes
-- Each microservice has its own database, so there is no way to bypass API and access persistence data directly.
-- MongoDB is used as a primary database for each of the services.
-- All services are talking to each other via the Rest API
-
-## Infrastructure
-[Spring cloud](https://spring.io/projects/spring-cloud) provides powerful tools for developers to quickly implement common distributed systems patterns -
-
-### Config service
-[Spring Cloud Config](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html) is horizontally scalable centralized configuration service for the distributed systems. It uses a pluggable repository layer that currently supports local storage, Git, and Subversion.
-
-In this project, we are going to use `native profile`, which simply loads config files from the local classpath. You can see `shared` directory in [Config service resources](https://github.com/sqshq/PiggyMetrics/tree/master/config/src/main/resources). Now, when Notification-service requests its configuration, Config service responses with `shared/notification-service.yml` and `shared/application.yml` (which is shared between all client applications).
-
-##### Client side usage
-Just build Spring Boot application with `spring-cloud-starter-config` dependency, autoconfiguration will do the rest.
-
-Now you don't need any embedded properties in your application. Just provide `bootstrap.yml` with application name and Config service url:
-```yml
-spring:
- application:
- name: notification-service
- cloud:
- config:
- uri: http://config:8888
- fail-fast: true
-```
-
-##### With Spring Cloud Config, you can change application config dynamically.
-For example, [EmailService bean](https://github.com/sqshq/PiggyMetrics/blob/master/notification-service/src/main/java/com/piggymetrics/notification/service/EmailServiceImpl.java) is annotated with `@RefreshScope`. That means you can change e-mail text and subject without rebuild and restart the Notification service.
-
-First, change required properties in Config server. Then make a refresh call to the Notification service:
-`curl -H "Authorization: Bearer #token#" -XPOST http://127.0.0.1:8000/notifications/refresh`
-
-You could also use Repository [webhooks to automate this process](http://cloud.spring.io/spring-cloud-config/spring-cloud-config.html#_push_notifications_and_spring_cloud_bus)
-
-##### Notes
-- `@RefreshScope` doesn't work with `@Configuration` classes and doesn't ignores `@Scheduled` methods
-- `fail-fast` property means that Spring Boot application will fail startup immediately, if it cannot connect to the Config Service.
-
-### Auth service
-Authorization responsibilities are extracted to a separate server, which grants [OAuth2 tokens](https://tools.ietf.org/html/rfc6749) for the backend resource services. Auth Server is used for user authorization as well as for secure machine-to-machine communication inside the perimeter.
-
-In this project, I use [`Password credentials`](https://tools.ietf.org/html/rfc6749#section-4.3) grant type for users authorization (since it's used only by the UI) and [`Client Credentials`](https://tools.ietf.org/html/rfc6749#section-4.4) grant for service-to-service communciation.
-
-Spring Cloud Security provides convenient annotations and autoconfiguration to make this really easy to implement on both server and client side. You can learn more about that in [documentation](http://cloud.spring.io/spring-cloud-security/spring-cloud-security.html).
-
-On the client side, everything works exactly the same as with traditional session-based authorization. You can retrieve `Principal` object from the request, check user roles using the expression-based access control and `@PreAuthorize` annotation.
-
-Each PiggyMetrics client has a scope: `server` for backend services and `ui` - for the browser. We can use `@PreAuthorize` annotation to protect controllers from an external access:
-
-``` java
-@PreAuthorize("#oauth2.hasScope('server')")
-@RequestMapping(value = "accounts/{name}", method = RequestMethod.GET)
-public List getStatisticsByAccountName(@PathVariable String name) {
- return statisticsService.findByAccountName(name);
-}
-```
-
-### API Gateway
-API Gateway is a single entry point into the system, used to handle requests and routing them to the appropriate backend service or by [aggregating results from a scatter-gather call](http://techblog.netflix.com/2013/01/optimizing-netflix-api.html). Also, it can be used for authentication, insights, stress and canary testing, service migration, static response handling and active traffic management.
-
-Netflix opensourced [such an edge service](http://techblog.netflix.com/2013/06/announcing-zuul-edge-service-in-cloud.html) and Spring Cloud allows to use it with a single `@EnableZuulProxy` annotation. In this project, we use Zuul to store some static content (the UI application) and to route requests to appropriate the microservices. Here's a simple prefix-based routing configuration for the Notification service:
-
-```yml
-zuul:
- routes:
- notification-service:
- path: /notifications/**
- serviceId: notification-service
- stripPrefix: false
-
-```
-
-That means all requests starting with `/notifications` will be routed to the Notification service. There is no hardcoded addresses, as you can see. Zuul uses [Service discovery](https://github.com/sqshq/PiggyMetrics/blob/master/README.md#service-discovery) mechanism to locate Notification service instances and also [Circuit Breaker and Load Balancer](https://github.com/sqshq/PiggyMetrics/blob/master/README.md#http-client-load-balancer-and-circuit-breaker), described below.
-
-### Service Discovery
-
-Service Discovery allows automatic detection of the network locations for all registered services. These locations might have dynamically assigned addresses due to auto-scaling, failures or upgrades.
-
-The key part of Service discovery is the Registry. In this project, we use Netflix Eureka. Eureka is a good example of the client-side discovery pattern, where client is responsible for looking up the locations of available service instances and load balancing between them.
-
-With Spring Boot, you can easily build Eureka Registry using the `spring-cloud-starter-eureka-server` dependency, `@EnableEurekaServer` annotation and simple configuration properties.
-
-Client support enabled with `@EnableDiscoveryClient` annotation a `bootstrap.yml` with application name:
-``` yml
-spring:
- application:
- name: notification-service
-```
-
-This service will be registered with the Eureka Server and provided with metadata such as host, port, health indicator URL, home page etc. Eureka receives heartbeat messages from each instance belonging to the service. If the heartbeat fails over a configurable timetable, the instance will be removed from the registry.
-
-Also, Eureka provides a simple interface where you can track running services and a number of available instances: `http://localhost:8761`
-
-### Load balancer, Circuit breaker and Http client
-
-#### Ribbon
-Ribbon is a client side load balancer which gives you a lot of control over the behaviour of HTTP and TCP clients. Compared to a traditional load balancer, there is no need in additional network hop - you can contact desired service directly.
-
-Out of the box, it natively integrates with Spring Cloud and Service Discovery. [Eureka Client](https://github.com/sqshq/PiggyMetrics#service-discovery) provides a dynamic list of available servers so Ribbon could balance between them.
-
-#### Hystrix
-Hystrix is the implementation of [Circuit Breaker Pattern](http://martinfowler.com/bliki/CircuitBreaker.html), which gives us a control over latency and network failures while communicating with other services. The main idea is to stop cascading failures in the distributed environment - that helps to fail fast and recover as soon as possible - important aspects of a fault-tolerant system that can self-heal.
-
-Moreover, Hystrix generates metrics on execution outcomes and latency for each command, that we can use to [monitor system's behavior](https://github.com/sqshq/PiggyMetrics#monitor-dashboard).
-
-#### Feign
-Feign is a declarative Http client which seamlessly integrates with Ribbon and Hystrix. Actually, a single `spring-cloud-starter-feign` dependency and `@EnableFeignClients` annotation gives us a full set of tools, including Load balancer, Circuit Breaker and Http client with reasonable default configuration.
-
-Here is an example from the Account Service:
-
-``` java
-@FeignClient(name = "statistics-service")
-public interface StatisticsServiceClient {
-
- @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
- void updateStatistics(@PathVariable("accountName") String accountName, Account account);
-
-}
-```
-
-- Everything you need is just an interface
-- You can share `@RequestMapping` part between Spring MVC controller and Feign methods
-- Above example specifies just a desired service id - `statistics-service`, thanks to auto-discovery through Eureka
-
-### Monitor dashboard
-
-In this project configuration, each microservice with Hystrix on board pushes metrics to Turbine via Spring Cloud Bus (with AMQP broker). The Monitoring project is just a small Spring boot application with the [Turbine](https://github.com/Netflix/Turbine) and [Hystrix Dashboard](https://github.com/Netflix-Skunkworks/hystrix-dashboard).
-
-Let's see observe the behavior of our system under load: Statistics Service imitates a delay during the request processing. The response timeout is set to 1 second:
-
-
-
-
|
|
|
---- |--- |--- |--- |
-| `0 ms delay` | `500 ms delay` | `800 ms delay` | `1100 ms delay`
-| Well behaving system. Throughput is about 22 rps. Small number of active threads in the Statistics service. Median service time is about 50 ms. | The number of active threads is growing. We can see purple number of thread-pool rejections and therefore about 40% of errors, but the circuit is still closed. | Half-open state: the ratio of failed commands is higher than 50%, so the circuit breaker kicks in. After sleep window amount of time, the next request goes through. | 100 percent of the requests fail. The circuit is now permanently open. Retry after sleep time won't close the circuit again because a single request is too slow.
-
-### Log analysis
-
-Centralized logging can be very useful while attempting to identify problems in a distributed environment. Elasticsearch, Logstash and Kibana stack lets you search and analyze your logs, utilization and network activity data with ease.
-
-### Distributed tracing
-
-Analyzing problems in distributed systems can be difficult, especially trying to trace requests that propagate from one microservice to another.
-
-[Spring Cloud Sleuth](https://cloud.spring.io/spring-cloud-sleuth/) solves this problem by providing support for the distributed tracing. It adds two types of IDs to the logging: `traceId` and `spanId`. `spanId` represents a basic unit of work, for example sending an HTTP request. The traceId contains a set of spans forming a tree-like structure. For example, with a distributed big-data store, a trace might be formed by a PUT request. Using `traceId` and `spanId` for each operation we know when and where our application is as it processes a request, making reading logs much easier.
-
-The logs are as follows, notice the `[appname,traceId,spanId,exportable]` entries from the Slf4J MDC:
+当前目录职责如下:
```text
-2018-07-26 23:13:49.381 WARN [gateway,3216d0de1384bb4f,3216d0de1384bb4f,false] 2999 --- [nio-4000-exec-1] o.s.c.n.z.f.r.s.AbstractRibbonCommand : The Hystrix timeout of 20000ms for the command account-service is set lower than the combination of the Ribbon read and connect timeout, 80000ms.
-2018-07-26 23:13:49.562 INFO [account-service,3216d0de1384bb4f,404ff09c5cf91d2e,false] 3079 --- [nio-6000-exec-1] c.p.account.service.AccountServiceImpl : new account has been created: test
+src/aurask/
+ __init__.py # 包入口,导出 CLI main
+ __main__.py # python -m aurask
+ app.py # 应用装配,不写具体业务规则
+ api.py # HTTP 网关与路由映射
+ cli.py # 命令行入口
+ repository.py # MVP 持久化层
+ errors.py # 统一错误类型
+ ids.py # ID 与时间工具
+ audit.py # 审计记录
+ auth.py # 租户、用户、API Key
+ plans.py # 套餐、商品、价格口径
+ billing.py # 订单、订阅、权益发放
+ quota.py # TBU 额度、预扣、结算、账本
+ payments.py # 支付匹配、tx_hash 幂等
+ orchestrator.py # 工作流模板与运行编排
+ knowledge_base.py # Workspace 与文档接入
+tests/
+ test_mvp.py # 核心闭环测试
+deploy/k3s/
+ README.md # 300 MAU k3s 部署方案
```
-- *`appname`*: The name of the application that logged the span from the property `spring.application.name`
-- *`traceId`*: This is an ID that is assigned to a single request, job, or action
-- *`spanId`*: The ID of a specific operation that took place
-- *`exportable`*: Whether the log should be exported to [Zipkin](https://zipkin.io/)
+新增代码时必须放入对应模块;不要把业务逻辑堆进 `api.py` 或 `cli.py`。
-## Infrastructure automation
+## 4. 架构原则
-Deploying microservices, with their interdependence, is much more complex process than deploying a monolithic application. It is really important to have a fully automated infrastructure. We can achieve following benefits with Continuous Delivery approach:
+Aurask 目标架构:
-- The ability to release software anytime
-- Any build could end up being a release
-- Build artifacts once - deploy as needed
+`Web/UI -> Auth + API Gateway -> Billing & Quota -> Workflow Orchestrator -> Langflow / AnythingLLM / LLM Proxy -> PostgreSQL / Redis / Object Storage / Vector DB -> Observability`
-Here is a simple Continuous Delivery workflow, implemented in this project:
+必须遵守:
-
+- Aurask 网关是唯一公网入口。
+- `Langflow`、`AnythingLLM`、数据库、Redis、向量库不直接暴露给终端用户。
+- 所有核心实体和操作必须携带租户上下文。
+- 基础用户只能运行审核模板,不开放任意代码执行。
+- TBU 必须先预扣再执行,执行后按实际消耗结算。
-In this [configuration](https://github.com/sqshq/PiggyMetrics/blob/master/.travis.yml), Travis CI builds tagged images for each successful git push. So, there are always the `latest` images for each microservice on [Docker Hub](https://hub.docker.com/r/sqshq/) and older images, tagged with git commit hash. It's easy to deploy any of them and quickly rollback, if needed.
+## 5. 多租户要求
-## Let's try it out
+核心字段:
-Note that starting 8 Spring Boot applications, 4 MongoDB instances and a RabbitMq requires at least 4Gb of RAM.
+- `tenant_id`
+- `user_id`
+- `workspace_id`
+- `flow_id`
+- `plan_id`
+- `order_id`
-#### Before you start
-- Install Docker and Docker Compose.
-- Change environment variable values in `.env` file for more security or leave it as it is.
-- Build the project: `mvn package [-DskipTests]`
+要求:
-#### Production mode
-In this mode, all latest images will be pulled from Docker Hub.
-Just copy `docker-compose.yml` and hit `docker-compose up`
+- 查询、写入、缓存键、对象存储路径、向量检索条件都必须带租户维度。
+- 禁止只按 ID 查询却不校验 `tenant_id`。
+- 高付费独立空间可独立 Namespace / Pod / 数据库凭证,但领域模型仍保持一致。
-#### Development mode
-If you'd like to build images yourself, you have to clone the repository and build artifacts using maven. After that, run `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up`
+## 6. 模块实现规则
-`docker-compose.dev.yml` inherits `docker-compose.yml` with additional possibility to build images locally and expose all containers ports for convenient development.
+### 6.1 `auth.py`
-If you'd like to start applications in Intellij Idea you need to either use [EnvFile plugin](https://plugins.jetbrains.com/plugin/7861-envfile) or manually export environment variables listed in `.env` file (make sure they were exported: `printenv`)
+- 负责租户、用户、API Key。
+- 不负责套餐、额度或支付。
+- API Key 返回时可以展示明文一次;持久化生产化时必须哈希。
-#### Important endpoints
-- http://localhost:80 - Gateway
-- http://localhost:8761 - Eureka Dashboard
-- http://localhost:9000/hystrix - Hystrix Dashboard (Turbine stream link: `http://turbine-stream-service:8080/turbine/turbine.stream`)
-- http://localhost:15672 - RabbitMq management (default login/password: guest/guest)
+### 6.2 `plans.py`
-## Contributions are welcome!
+- 维护套餐、商品、价格和权益口径。
+- 默认口径必须保持:
+ - 免费体验:1 工作流、1 知识库、512MB、7 天
+ - 基础套餐:20 USDT/月,3 工作流、3 知识库、1GB、900 TBU
+ - 新增栏位:20 USDT/个/月,不赠送 TBU
+ - TBU 包:1000、2000、5000 档
-PiggyMetrics is open source, and would greatly appreciate your help. Feel free to suggest and implement any improvements.
+### 6.3 `billing.py`
+
+- 负责创建订单、订阅、权益发放。
+- 不直接执行支付监听。
+- 发放权益必须调用 `QuotaService`,不得绕过额度账本。
+
+### 6.4 `quota.py`
+
+- 负责 `QuotaAccount`、`QuotaLedger`、reservation。
+- 工作流执行必须走 `reserve_tbu -> settle_reservation`。
+- 失败退款必须区分供应商是否已计费。
+- 不允许负数 TBU 或绕过账本直接改余额。
+
+### 6.5 `payments.py`
+
+- 负责 `USDT-TRC20` 支付匹配。
+- 必须校验 `tx_hash` 幂等。
+- 金额不足、确认数不足、重复转账进入人工处理或报错。
+- TRC20 手续费描述必须基于 TRON 资源模型,不要写 BTC 手续费。
+
+### 6.6 `orchestrator.py`
+
+- 只运行审核模板。
+- 默认模板不得开启任意 Python 或任意自定义组件。
+- 工作流运行必须校验 Workspace 租户归属。
+- 运行前必须预扣 TBU。
+- 运行后必须写 usage record 和 audit event。
+
+### 6.7 `knowledge_base.py`
+
+- Workspace 必须绑定 `tenant_id`。
+- 文档路径必须遵循 `tenant_id/workspace_id/document_id`。
+- 上传链路必须预留文件大小、类型、病毒扫描、敏感内容检测钩子。
+- AnythingLLM 只能作为内部适配器,不可把管理后台暴露给用户。
+
+### 6.8 `api.py`
+
+- 只做请求解析、鉴权、路由和错误映射。
+- 不要在 `api.py` 内写复杂业务规则。
+- 新增鉴权接口必须使用 `Authorization: Bearer `。
+
+### 6.9 `repository.py`
+
+- 当前是 MVP JSON store。
+- 允许为演示补功能,但不要把它描述为生产数据库。
+- 生产化时应新增 PostgreSQL repository,而不是把 JSON store 继续扩展成复杂数据库。
+
+## 7. Langflow 与 AnythingLLM 约束
+
+### Langflow
+
+- 只作为工作流编排/执行引擎。
+- 基础套餐只允许模板模式。
+- 禁止向普通用户开放 Langflow 全功能 UI。
+- 禁止任意 Python 执行、任意自定义组件、无白名单外部请求。
+- 生产环境通过内部 `ClusterIP` 暴露给 `aurask-worker`。
+
+### AnythingLLM
+
+- 只作为 Workspace、文档、RAG、聊天层。
+- Workspace 必须与 Aurask `tenant_id` 绑定。
+- 普通用户不能进入全局管理后台。
+- 文档上传必须先过 Aurask,再转发 AnythingLLM。
+
+## 8. 部署与 k3s 约束
+
+k3s 生产部署规划位于 `deploy/k3s/README.md`。
+
+300 MAU 首版目标:
+
+- `3` 台 k3s server。
+- `4` 台 worker。
+- `aurask-api` 与 `aurask-worker` 分开部署。
+- `langflow-runtime` 与 `anythingllm` 只作为内部服务。
+- PostgreSQL 使用 `CloudNativePG + PGVector`。
+- Redis 用于缓存、队列、限流。
+- Longhorn 或云块存储承载 PVC。
+- Prometheus / Grafana / Loki / Alertmanager 做观测。
+
+如果新增 Kubernetes manifests,应放入:
+
+```text
+deploy/k3s/base/
+deploy/k3s/overlays/staging/
+deploy/k3s/overlays/production/
+```
+
+## 9. 测试要求
+
+修改核心业务逻辑后至少运行:
+
+```bash
+python -m unittest discover -s tests -v
+```
+
+若修改 CLI 或启动流程,额外运行:
+
+```bash
+uv run aurask demo --reset
+```
+
+如果当前环境缺少 `uv`,可使用:
+
+```bash
+$env:PYTHONPATH='src'; py -3 -m aurask demo --reset
+```
+
+## 10. 文档同步要求
+
+以下变更必须同步文档:
+
+- 新增 API:更新 `README.md`
+- 改套餐/价格/TBU:更新 `plans.py`、`README.md`、`Aurask_Technical_Operations_Plan.md`
+- 改部署方案:更新 `deploy/k3s/README.md`
+- 改目录结构:更新本 `AGENTS.md`
+- 改生产化路线:更新 `Aurask_Technical_Operations_Plan.md`
+
+## 11. 禁止事项
+
+默认禁止:
+
+- 继续引入与 Aurask 无关的旧项目说明。
+- 绕过 Aurask 网关直连 Langflow / AnythingLLM。
+- 绕过 `QuotaService` 直接扣减或发放额度。
+- 新增工作流栏位时赠送 TBU。
+- 年付现金流重复计入 MRR。
+- 在没有隔离设计的前提下开放任意代码执行。
+- 日志记录原始密钥、完整 Prompt、完整文档正文。
+- 将 JSON store 宣称为生产持久化方案。
+
+## 12. 推荐实现顺序
+
+后续迭代默认按以下顺序推进:
+
+1. 把 `JsonStore` 替换为 PostgreSQL repository。
+2. 引入 Redis 队列,把工作流执行移到 `aurask-worker`。
+3. 接真实 Langflow Runtime Pool。
+4. 接真实 AnythingLLM API。
+5. 接 TronGrid / Tronscan 支付监听。
+6. 编写 `deploy/k3s/base` manifests。
+7. 接入监控、日志、备份和恢复演练。
+
+除非用户明确要求,不要跳过租户、额度、订单和审计边界直接堆功能页面。
diff --git a/Aurask_Technical_Operations_Plan.md b/Aurask_Technical_Operations_Plan.md
index cb06003..31acf07 100644
--- a/Aurask_Technical_Operations_Plan.md
+++ b/Aurask_Technical_Operations_Plan.md
@@ -1,408 +1,474 @@
-# Aurask(Langflow + AnythingLLM)技术与运营方案(修订版)
+# Aurask 技术与运营方案(当前实现路径版)
-> 基于原《Aurask(Langflow+AnythingLLM)完整技术+运营方案及营收净利润估算》PDF 修订。
-> 修订原则:不改变“海外用户 + Langflow 工作流 + AnythingLLM 知识库 + USDT 收款 + 数字员工工作流变现”的主旨骨架,只修正可落地性、隔离边界、Token 口径与营收净利润计算。
+> 本文档基于当前仓库实现、`AGENTS.md` 约束与 `deploy/k3s/README.md` 部署规划更新。
+> 核心方向不变:面向海外用户,用 Langflow 承载模板化工作流,用 AnythingLLM 承载知识库/RAG,用 Aurask 自身网关统一做鉴权、套餐、TBU、订单、审计和运维闭环。
-## 0. 结论摘要
+## 0. 当前结论
-- **技术方向可行,但不能把 Langflow 当作天然多租户安全边界。** Langflow 官方明确提示其 UI/运行时具备代码执行能力,单进程内不提供强租户隔离;面向第三方用户时必须用进程、磁盘、网络、数据库等基础设施层隔离。
-- **AnythingLLM 适合作为知识库与 RAG 工作区层。** 自托管 Docker 版支持多用户与 RBAC,向量库支持 LanceDB、PGVector、Chroma、Milvus 以及云向量库,但应通过 Aurask 网关统一做套餐、存储、额度与审计。
-- **原方案中的“每用户一个 Langflow + AnythingLLM 容器”对 300 人规模不经济。** 推荐按套餐分层:基础用户共享应用集群 + 工作区隔离,高风险或高付费用户再提供独立命名空间/容器。
-- **原营收利润模型方向成立,但口径不够严谨。** 主要问题是年度套餐被重复计入月收入、Token 基础额度与额外购买边界不清、额外 Token 成本容易漏算或重复计算、VPS/运营/CAC 成本偏乐观。
-- **修订后基准测算:** 在 300 名月度活跃付费海外用户、20 USDT/月基础套餐、160 个新增工作流栏位、60 人额外购买 Token 的口径下,月营收约 **70,312 元**,月总成本约 **23,864 元**,月净利润约 **46,448 元**,净利率约 **66%**。
+Aurask 当前已完成 **可运行 MVP 后端骨架**:
-## 1. 原方案可行性与计算准确性评估
+- 使用 Python 模块化单体实现首版领域闭环。
+- 已具备 `Auth + API Gateway`、`Billing + Quota + TBU Ledger`、`Workflow Orchestrator`、`Knowledge Base`、`USDT-TRC20 Payment`、`Audit` 等核心边界。
+- 已提供 CLI 演示与标准库 HTTP 网关。
+- 已新增面向 `300` 名月度活跃用户的 `k3s` 部署规划:`deploy/k3s/README.md`。
-### 1.1 技术实现可行性
+当前阶段定位:
-| 模块 | 原设想 | 可行性判断 | 修订建议 |
-| --- | --- | --- | --- |
-| Langflow 工作流编排 | 多用户模式 + user_id + RLS 隔离 | **方向可行,安全边界不足** | 不向普通用户开放任意组件/代码执行;基础用户运行审核过的模板,高权限用户放入独立容器/命名空间 |
-| AnythingLLM 知识库 | 每用户 Workspace 隔离 | **可行** | 使用 Docker 版多用户模式;默认用户只加入自己的 Workspace;后台 API 做二次权限校验 |
-| 向量库隔离 | workspace_id 前缀隔离 | **可行但需加强** | 小规模可用内置 LanceDB;生产建议 PGVector/Qdrant/Weaviate,按 tenant_id / workspace_id 分区、索引与备份 |
-| 资源管控 | 栏位、存储、Token 联动 | **可行** | 统一通过 Aurask Billing/Quota Service 做原子扣减,不依赖 Langflow 或 AnythingLLM 自身额度 |
-| 服务器部署 | 多台低价 VPS 承载 300 用户 | **需修正** | 1C1G 只能做边缘或轻量任务,不适合作为多租户主节点;300 用户需至少 4GB/8GB 节点池、监控、备份和灰度 |
-| USDT 收款 | Bitget 钱包 + Tronscan 监听 | **可行但有合规风险** | 首期可半自动确认;上线后加入链上监听、订单过期、AML 风险提示、退款与税务记录 |
-| 7 天上线 | 7 天完成全部生产能力 | **过于乐观** | 7 天可做可演示 MVP;生产可收费版本建议 4-6 周,含安全、计费、审计、备份和风控 |
+- **可以用于本地演示、业务流程验证、接口联调和领域模型迭代。**
+- **尚不是生产部署版本。**
+- 生产化需要依次补齐 PostgreSQL、队列、真实 Langflow/AnythingLLM 接入、k3s manifests、监控告警、备份恢复和支付风控。
-### 1.2 原营收利润模型的主要问题
+## 1. 项目目标
-1. **年度套餐重复计入:** 原方案已经按 300 人计算基础套餐收入,又额外加了 30 名年度套餐折算月收入。如果年度用户属于这 300 人,则不能重复加入 MRR;年度付费应区分“现金流入”与“收入确认”。
-2. **Token 单位混用:** 文档里 `U` 同时表示 USDT、Token 额度和中转站消耗,容易导致定价、扣费和利润计算混乱。修订版将 Token 记为 `TBU`(Token Billing Unit,用户侧计费单位)。
-3. **Token 成本边界不清:** 如果 30U/45U 是用户总消耗,则额外购买 Token 不应再重复计成本;如果额外购买是独立新增用量,则必须把该部分供应商成本计入。
-4. **VPS 承载量偏乐观:** AnythingLLM 自托管推荐最低约 2GB RAM / 2-core CPU;Langflow 还存在代码执行和运行时开销。1C1G 节点承载 15-20 名数字员工工作流用户风险较高。
-5. **TRC20 手续费表述错误:** TRON 交易成本由 Bandwidth/Energy/TRX 资源模型决定,不是“0.0004 BTC”。应提示用户准备 TRX 或使用交易所/钱包内置扣费。
-6. **运营成本偏低:** 以海外 300 名付费用户为目标,1300 元/月广告 + 600 元客服仅适合已有流量池;若从零获客,需按 CAC、内容产出、社群维护、客服与退款处理重新预算。
+Aurask 是面向海外个人开发者、学生、独立创业者和小团队的轻量级 AI 数字员工工作流平台。
-## 2. 技术方案
+目标能力:
-### 2.1 总体架构
+- 用户无需自行部署 Langflow / AnythingLLM。
+- 用户通过 Aurask 选择审核过的数字员工模板。
+- 用户可绑定知识库 Workspace,并通过 RAG 支撑业务问答。
+- Aurask 统一管理租户、套餐、TBU、订单、支付、审计和成本。
+- 首期支持 USDT-TRC20 支付,后续接入 Stripe / Paddle / Lemon Squeezy 等合规渠道。
+
+## 2. 当前代码实现
+
+### 2.1 当前目录结构
+
+```text
+src/aurask/
+ __init__.py # 包入口,导出 CLI main
+ __main__.py # python -m aurask 入口
+ app.py # 应用装配与 demo bootstrap
+ api.py # 标准库 HTTP 网关
+ cli.py # aurask demo / aurask serve
+ repository.py # MVP JSON 持久化
+ errors.py # 应用错误类型
+ ids.py # ID 与时间工具
+ audit.py # 审计事件
+ auth.py # 租户、用户、API Key
+ plans.py # 套餐与商品目录
+ billing.py # 订单、订阅、权益发放
+ quota.py # TBU 额度账户、预扣、结算、账本
+ payments.py # USDT-TRC20 支付匹配
+ orchestrator.py # 模板工作流编排与 Langflow Runtime 适配
+ knowledge_base.py # AnythingLLM Workspace / 文档接入适配
+tests/
+ test_mvp.py # MVP 核心流程测试
+deploy/k3s/
+ README.md # 300 MAU k3s 部署规划
+```
+
+### 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 | `LangflowRuntimeAdapter` 模拟适配层 | 内部 `ClusterIP` Langflow Runtime Pool |
+| AnythingLLM | `AnythingLLMAdapter` 模拟适配层 | 内部 `ClusterIP` AnythingLLM API |
+| 网关 | Python 标准库 HTTP Server | FastAPI / ASGI + Ingress + HPA |
+| 支付 | 人工提交 tx hash 匹配 | TronGrid / Tronscan / 自建节点监听 |
+| 观测 | 审计事件写入 store | Prometheus + Loki + Grafana + Alertmanager |
+| 部署 | 本地运行 | k3s:`aurask-api` + `aurask-worker` |
+
+## 3. 目标技术架构
```text
海外用户
↓
Aurask Web / 用户中心 / 支付页
↓
-Auth + API Gateway(鉴权、限流、套餐、Token 预扣)
+Auth + API Gateway(鉴权、限流、套餐、TBU 预扣)
↓
Billing & Quota Service(订单、余额、TBU、栏位、存储)
↓
Workflow Orchestrator(模板、任务队列、运行状态)
- ├─ Langflow Runtime Pool(工作流编排与执行)
+ ├─ Langflow Runtime Pool(模板工作流执行)
├─ AnythingLLM API(Workspace、文档、RAG、聊天)
├─ LLM Proxy(模型路由、Token 计量、成本归集)
- └─ Storage / DB / Vector DB(PostgreSQL、Redis、对象存储、向量库)
+ └─ PostgreSQL / Redis / Object Storage / Vector DB
↓
Observability(日志、指标、审计、告警、成本报表)
```
核心原则:
-- **Aurask 网关是唯一入口。** Langflow、AnythingLLM、数据库和向量库不直接暴露公网。
-- **身份与租户统一。** 所有请求携带 `tenant_id`、`user_id`、`workspace_id`、`flow_id`,网关、数据库、对象存储和向量库均按租户维度校验。
-- **模板优先,开放能力分层。** 基础用户使用已审核模板;自定义组件、Python 代码、外部 API 调用只对高付费或独立命名空间用户开放。
-- **先预扣再执行。** 工作流执行前按模型、最大上下文、预计输出和工具调用次数冻结 TBU;执行后按实际消耗结算,多退少补。
+- **Aurask 网关是唯一公网入口。**
+- `Langflow`、`AnythingLLM`、数据库、向量库、Redis 不直接暴露公网。
+- 所有核心实体默认携带 `tenant_id`。
+- 所有用户侧 Token 统一记为 `TBU`。
+- 工作流执行前预扣 TBU,执行后按实际消耗结算。
+- 基础用户只运行审核过的模板,不开放任意代码执行。
-### 2.2 多租户与安全隔离
+## 4. 多租户与安全隔离
-#### 2.2.1 Langflow 侧
+### 4.1 领域标识
-Langflow 适合作为工作流 IDE 和编排工具,但不适合作为单进程多租户安全沙箱。生产方案采用三层策略:
+核心字段:
-1. **基础套餐:模板运行模式**
- - 用户只选择和配置官方模板,不直接进入 Langflow UI。
- - Langflow 流程由 Aurask 后台维护,用户参数通过表单注入。
- - 禁用自定义组件、任意 Python 执行、任意网络请求工具。
- - 所有执行通过任务队列进入 Langflow Runtime Pool。
+- `tenant_id`
+- `user_id`
+- `workspace_id`
+- `flow_id`
+- `plan_id`
+- `order_id`
-2. **进阶套餐:受控编辑模式**
- - 开放有限组件库,如 LLM、Prompt、RAG、HTTP 白名单、数据清洗。
- - 外部 API 域名白名单、请求体大小、执行时间和并发数受控。
- - 每个租户分配独立工作目录、临时目录和日志目录。
+要求:
-3. **高付费套餐:独立命名空间模式**
- - 高风险或高负载用户使用独立容器/Pod/Namespace。
- - 独立数据库凭证、独立对象存储前缀、独立网络策略。
- - 可开放更多自定义能力,但必须保留 egress 白名单、资源限制和审计。
+- 查询、写入、缓存、对象存储路径、向量检索都必须带租户维度。
+- 不允许只按主键查询而不校验 `tenant_id`。
+- 高付费独立空间可以独立 Namespace / Pod / 数据库凭证,但领域模型保持一致。
-关键配置与防护:
+### 4.2 Langflow 安全边界
+
+Langflow 只作为工作流编排/执行引擎,不作为多租户安全边界。
+
+基础套餐:
+
+- 只允许模板化工作流。
+- 用户不进入 Langflow 全功能 UI。
+- 禁止任意 Python 执行。
+- 禁止任意自定义组件。
+- 禁止无白名单外部网络调用。
+
+生产建议配置:
- `LANGFLOW_AUTO_LOGIN=False`
-- `LANGFLOW_SECRET_KEY` 使用高强度随机值,定期轮换。
-- `LANGFLOW_DATABASE_URL` 指向 PostgreSQL,不在本地 SQLite 承载生产数据。
-- `LANGFLOW_FALLBACK_TO_ENV_VAR=False`,避免用户流程意外读取宿主环境变量。
-- 通过 Nginx/Caddy/Traefik 反向代理启用 HTTPS、认证和 IP 限流。
-- 容器开启 CPU、内存、pids、文件大小、执行超时限制。
-- 阻断容器访问云厂商元数据地址、内网管理网段和数据库管理端口。
+- `LANGFLOW_FALLBACK_TO_ENV_VAR=False`
+- `LANGFLOW_SECRET_KEY` 使用高强度随机值并定期轮换。
+- `LANGFLOW_DATABASE_URL` 指向 PostgreSQL。
+- Runtime Pod 设置 CPU、内存、pids、超时、临时目录和 egress 白名单。
-#### 2.2.2 AnythingLLM 侧
+### 4.3 AnythingLLM 边界
-AnythingLLM 负责知识库、文档、RAG、聊天历史和 Workspace 管理:
+AnythingLLM 负责 Workspace、文档、RAG 和聊天历史。
-- 使用自托管 Docker 版开启多用户模式。
-- 管理员账号仅由 Aurask 后台持有,普通用户不进入全局管理后台。
-- 每个付费用户至少一个 Workspace,Workspace 与 Aurask 的 `tenant_id` 绑定。
-- 默认用户角色只允许访问被显式加入的 Workspace。
-- 文档上传由 Aurask 先做大小、类型、病毒扫描和敏感内容检测,再调用 AnythingLLM API 入库。
-- 向量库小规模可先使用 LanceDB;当月活付费用户超过 300 或文档量超过 500GB 时迁移到 PGVector/Qdrant/Weaviate。
+要求:
-#### 2.2.3 数据与存储隔离
+- Workspace 与 Aurask `tenant_id` 绑定。
+- 普通用户只能访问被显式授权的 Workspace。
+- 文档必须先经过 Aurask 上传入口,再转发给 AnythingLLM。
+- 上传链路预留大小、类型、病毒扫描、敏感内容检测钩子。
+- 向量 metadata 必须包含 `tenant_id` 与 `workspace_id`。
-- **PostgreSQL:** 核心业务表使用 `tenant_id` 字段和行级安全策略;不同高付费租户可分配独立 schema 或独立库。
-- **对象存储:** 文件路径使用 `tenant_id/workspace_id/document_id` 前缀;下载只发放短期签名 URL。
-- **向量库:** 向量 metadata 必须包含 `tenant_id` 与 `workspace_id`;检索时强制带过滤条件。
-- **日志审计:** 不记录原始密钥、完整 Prompt、完整文档正文;只记录摘要、哈希、模型、Token 数、耗时和错误类型。
-- **备份:** 每日增量、每周全量;备份加密;每月至少做一次恢复演练。
+## 5. 套餐、资源与 TBU
-### 2.3 套餐、资源与 Token 管控
-
-#### 2.3.1 定价与权益
+### 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 TBU;2000 TBU 送 250 TBU | 赠送额度当月有效或 90 天有效需明确 |
-| 高付费独立空间 | 自定义报价 | 独立容器/命名空间、更高并发、更大存储 | 起步价建议不低于 99 USDT/月 |
+| 免费体验 | 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 白名单与审计 |
-> TBU = Token Billing Unit,用户侧计费单位。
-> 供应商实际消耗单位 = 用户侧 TBU / 1.25。
-> 若供应商采购成本为 0.06 元/单位,则每 1 TBU 的供应商成本为 `0.06 / 1.25 = 0.048 元`。
-
-#### 2.3.2 Token 计量流程
+### 5.2 TBU 计量流程
1. 用户发起工作流。
-2. Orchestrator 按流程节点、模型、输入长度、知识库检索 TopK、预计输出长度计算预计 TBU。
-3. Billing Service 先冻结预计 TBU,余额不足则提示充值或降级模型。
-4. LLM Proxy 统一调用模型供应商或中转站,记录输入、输出、缓存命中、工具调用和失败重试。
-5. 执行结束后按实际 TBU 结算,释放未用冻结额度。
-6. 若任务失败:
- - 供应商未计费:全额退回。
- - 供应商已计费:按实际已消耗 TBU 扣减,并向用户展示失败原因。
+2. Orchestrator 按模板、输入长度、RAG TopK、预计输出长度估算 TBU。
+3. Quota Service 冻结预计 TBU。
+4. 余额不足则拒绝执行或提示充值/降级模型。
+5. Runtime 执行工作流。
+6. 执行成功后按实际 TBU 结算,释放未用额度。
+7. 执行失败时:
+ - 供应商未计费:全额释放。
+ - 供应商已计费:按实际消耗扣减并记录失败原因。
-#### 2.3.3 资源限额建议
+### 5.3 当前代码对应
-| 维度 | 免费体验 | 基础套餐 | 进阶/高付费 |
-| --- | ---: | ---: | ---: |
-| 并发运行工作流 | 1 | 2 | 5-20 |
-| 单次执行超时 | 60 秒 | 180 秒 | 600 秒 |
-| 单文件上传 | 10MB | 50MB | 200MB |
-| 月上传文件数 | 20 | 200 | 1000+ |
-| 单 Workspace 文档数 | 50 | 1000 | 10000+ |
-| 每日邮件/外部 API 调用 | 禁用或低额度 | 100 次 | 按合同 |
+- 套餐定义:`src/aurask/plans.py`
+- 额度账户:`src/aurask/quota.py`
+- 订单与权益:`src/aurask/billing.py`
+- 使用记录:`src/aurask/orchestrator.py`
-### 2.4 收款与订单闭环
+## 6. 支付与订单闭环
-首期可继续使用 Bitget Wallet / USDT-TRC20,但要把“个人收款”升级为可审计订单流程:
+首期使用 `USDT-TRC20`,但必须围绕“可审计订单”设计。
+
+最低闭环:
1. 用户选择套餐或 TBU 包。
-2. 系统生成订单号、金额、链、收款地址、过期时间和备注。
-3. 前端展示二维码、地址、金额、网络提醒和到账说明。
-4. 后台监听 TronGrid/Tronscan/自建节点确认入账。
-5. 入账金额、币种、链、交易哈希与订单匹配。
-6. 达到确认数后自动开通权益。
-7. 异常金额、重复转账、超时到账进入人工处理队列。
+2. 系统生成订单号、金额、链、收款地址、过期时间。
+3. 用户转账。
+4. 系统记录 `tx_hash`、金额、确认数。
+5. 金额与订单匹配。
+6. 达到确认数后开通权益。
+7. 异常金额、重复交易、超时到账进入人工处理队列。
-修正说明:
+当前代码:
-- TRC20 手续费不是 BTC 计价,而是 TRON 的 Bandwidth/Energy/TRX 资源模型。
-- 用户承担链上手续费时,支付页必须提示“请使用 TRC20 网络且保留足够 TRX/钱包手续费”。
-- 个人收款仍需保留订单、发票/收据、退款、税务和 AML 风险记录。
-- 如果后续面向欧美用户,建议增加 Stripe / Paddle / Lemon Squeezy 等合规支付方式,USDT 作为补充渠道。
+- 订单生成:`BillingService.create_order`
+- 支付匹配:`PaymentService.match_trc20_payment`
+- 权益发放:`BillingService.fulfill_order`
-### 2.5 部署方案
+注意:
-#### 2.5.1 MVP 阶段(0-100 付费用户)
+- TRC20 手续费由 TRON Bandwidth / Energy / TRX 资源模型决定,不是 BTC 手续费。
+- 生产环境必须接入链上监听与幂等校验。
+- 需要保留退款、AML、税务与异常处理字段。
-- 1 台 4GB/2vCPU:Aurask Web、API Gateway、Billing Service。
-- 1 台 8GB/4vCPU:Langflow Runtime Pool、AnythingLLM。
-- 1 台 4GB/2vCPU:PostgreSQL、Redis、对象存储代理、备份任务。
-- 1 台备用低配节点:监控、日志、故障转移脚本。
+## 7. 数据与存储
-#### 2.5.2 300 付费用户阶段
+### 7.1 MVP
-| 节点 | 建议配置 | 数量 | 用途 |
-| --- | --- | ---: | --- |
-| Edge/API | 2-4GB RAM / 2vCPU | 2 | 前端、网关、鉴权、限流 |
-| Workflow Worker | 4-8GB RAM / 2-4vCPU | 3-4 | Langflow 执行池、任务队列消费者 |
-| RAG/AnythingLLM | 4-8GB RAM / 2-4vCPU | 2 | 知识库、RAG、文档处理 |
-| DB/Vector | 8GB RAM / 4vCPU 起 | 1-2 | PostgreSQL、PGVector 或外部向量库 |
-| Observability/Backup | 2-4GB RAM / 2vCPU | 1 | 日志、监控、备份、告警 |
+当前 MVP 使用 `JsonStore`:
-成本口径:
+- 优点:无依赖、便于演示、可快速验证领域流程。
+- 限制:不适合生产、无并发事务、无查询能力、无数据库级隔离。
-- RackNerd 年付促销节点可显著降低账面成本,但不可把促销价当作稳定长期成本。
-- 基准财务模型按 **1500 元/月基础设施预算** 估算,包含 VPS、备份、监控、少量冗余和突发扩容。
-- 如果全部使用年付促销资源,硬件成本可能低于 700 元/月;如果使用更稳定云厂商或托管数据库,可能升至 3000-6000 元/月。
+### 7.2 生产目标
-### 2.6 落地排期
+| 类型 | 推荐 |
+| --- | --- |
+| 主数据库 | PostgreSQL |
+| 向量检索 | PGVector 起步,后续 Qdrant / Weaviate |
+| 缓存与队列 | Redis 起步,后续按规模引入 RabbitMQ / NATS |
+| 对象存储 | 外部 S3 兼容存储 |
+| 备份 | PostgreSQL WAL 归档 + 对象存储备份 |
-| 阶段 | 时间 | 目标 | 交付物 |
-| --- | --- | --- | --- |
-| Demo | 第 1 周 | 跑通端到端链路 | 登录、套餐页、模板工作流、AnythingLLM Workspace、手动开通 |
-| MVP | 第 2-4 周 | 可小范围收费 | 自动订单、TBU 计量、基础隔离、监控、备份、英文 UI |
-| Beta | 第 5-8 周 | 50-100 付费用户 | 模板库、社群反馈、错误报表、支付异常处理、知识库批量上传 |
-| Scale | 第 9-12 周 | 300 付费用户 | 节点池扩容、灰度发布、成本报表、分层客服、SOP 与风控 |
+数据要求:
-## 3. 运营方案
+- 核心表必须包含 `tenant_id`。
+- 对象路径遵循 `tenant_id/workspace_id/document_id`。
+- 审计不记录完整 Prompt、完整文档正文和原始密钥。
-### 3.1 品牌定位
+## 8. k3s 生产部署规划
-Aurask 定位为面向海外个人开发者、学生、独立创业者和小团队的轻量级 AI 数字员工工作流平台:
+详细方案见:`deploy/k3s/README.md`。
-- 不需要用户自行部署 Langflow / AnythingLLM。
-- 内置客服、资料问答、线索整理、邮件草稿、表格处理、社媒内容等数字员工模板。
-- 支持知识库、工作流、Token 和支付的一体化管理。
-- 使用海外节点,默认英文界面,后续支持多语言。
-- 以低门槛月费 + 按需 Token + 栏位扩展变现。
+### 8.1 300 MAU 标准
-### 3.2 获客策略
+容量假设:
-#### 3.2.1 内容获客
+- 月度活跃付费用户:`300`
+- 日活高峰:`40-80`
+- 同时在线峰值:`15-30`
+- 同时工作流执行峰值:`10-20`
+- 外部模型 API,不在集群内自建 GPU 推理。
-- GitHub:发布模板工作流、部署对比、RAG 示例和轻量 SDK。
-- Medium / Dev.to:发布 “How to build AI employee workflows without self-hosting Langflow” 等英文教程。
-- YouTube Shorts / TikTok:录制 60-120 秒数字员工案例演示。
-- Product Hunt / BetaList:发布 Beta 版本,收集早期用户。
-- Reddit / Discord / Telegram:聚焦 `Langflow`、`RAG`、`AI agents`、`solopreneur tools` 圈层。
+推荐拓扑:
-目标:
+| 角色 | 数量 | 建议配置 |
+| --- | ---: | --- |
+| 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` |
-- 每周 2 篇长文、3 条短视频、5 个社群触达。
-- 内容 CTA 统一导向 7 天免费体验。
-- 对每个渠道打 UTM,计算注册率、激活率、付费率和退款率。
+### 8.2 工作负载
-#### 3.2.2 裂变机制
+| 工作负载 | 副本 | 说明 |
+| --- | ---: | --- |
+| `aurask-api` | 3 | 网关、鉴权、订单、额度查询 |
+| `aurask-worker` | 3 | 工作流编排、异步任务、支付匹配 |
+| `aurask-cron` | 1 | 周期任务、过期订单、报表 |
+| `langflow-runtime` | 3 | 审核模板执行 |
+| `anythingllm` | 2 | Workspace、文档、RAG |
+| PostgreSQL / PGVector | 3 | CloudNativePG |
+| Redis | 2-3 | 队列、缓存、限流 |
-- 邀请 1 名新用户首月付费:邀请人获得 15 天基础套餐延期或 200 TBU。
-- 邀请 3 名新用户首月付费:邀请人获得 1 个工作流栏位 1 个月。
-- 邀请 5 名新用户首月付费:邀请人获得 1000 TBU 或 1 次工作流搭建辅导。
-- 防作弊:同设备、同钱包、同邮箱域名异常注册进入人工复核。
+### 8.3 生产化要求
-#### 3.2.3 付费投放
+- Traefik / Ingress 只暴露 Aurask Web/API。
+- cert-manager 管理 TLS。
+- Longhorn 或云块存储承载 PVC。
+- PostgreSQL 备份到外部 S3。
+- Prometheus、Grafana、Loki、Alertmanager 提供观测。
+- NetworkPolicy 默认拒绝,按服务链路放通。
-原方案 1300 元/月投放只适合已有受众或强内容冷启动。修订建议:
+## 9. 运维与可观测性
-- **冷启动月预算:3000-8000 元。**
-- 渠道优先级:Google Search 小词、Reddit Ads、YouTube 小博主、AI 工具导航站赞助位。
-- 广告只投高意图关键词,如 `Langflow hosting`、`AI workflow builder`、`RAG chatbot for small business`。
-- 每周淘汰 CAC 高于首月毛利 50% 的广告组。
+必须观测:
-### 3.3 激活与留存
+- API QPS、延迟、错误率。
+- 工作流运行数、失败率、排队时长。
+- TBU 预扣、消耗、释放、退款。
+- 订单创建、支付匹配、异常订单。
+- PostgreSQL 连接数、复制延迟、磁盘。
+- Redis 内存、队列长度、命中率。
+- Langflow / AnythingLLM 请求耗时和失败率。
-用户首次体验路径控制在 10 分钟内:
+告警优先级:
+
+- `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. 上传 1 个样例文档或使用示例知识库。
-4. 运行 1 次工作流。
-5. 看到 TBU 消耗、节省时间和下一步建议。
+2. 选择模板。
+3. 创建 Workspace。
+4. 上传样例文档或使用示例知识库。
+5. 运行一次模板工作流。
+6. 看到 TBU 消耗、节省时间和下一步建议。
-留存动作:
+## 11. 财务测算口径
-- 每周发送英文使用报告:运行次数、节省时间、TBU 消耗、失败任务、建议优化。
-- 每月发布 2-4 个新模板。
-- 对 TBU 消耗异常高的用户推送优化建议,而不是只提示充值。
-- 对连续 7 天未运行的付费用户触发邮件召回。
-- 对连续 3 个月付费用户赠送模板搭建辅导或 TBU,而不是简单降价。
+基准假设仍沿用原方案,但与 TBU 口径对齐:
-### 3.4 变现优化
-
-1. **套餐升级:** 当用户工作流栏位、知识库或存储不足时,提示新增栏位;新增栏位不赠送 TBU,避免成本失控。
-2. **TBU 包:** 提供 1000、2000、5000 TBU 三档,价格随量折扣,但保证单 TBU 毛利为正。
-3. **模板市场:** 官方模板免费;高级模板一次性 9-29 USDT,或按月订阅。
-4. **搭建服务:** 为小团队提供 99-299 USDT 的一次性工作流搭建服务,提高现金流。
-5. **高阶套餐:** 99 USDT/月起,包含独立命名空间、更高并发、优先队列、定制模型路由。
-
-### 3.5 风险控制
-
-| 风险 | 影响 | 控制措施 |
-| --- | --- | --- |
-| Langflow 任意代码执行 | 跨租户数据泄漏、服务器被利用 | 基础用户模板化;禁用自定义组件;高付费独立容器;网络 egress 白名单 |
-| Token 成本波动 | 毛利下降 | TBU 单价动态调整;模型分层;缓存;失败重试上限;成本告警 |
-| 支付合规 | 冻结钱包、退款纠纷、税务风险 | 订单台账、AML 筛查、退款 SOP、用户协议、后续接入合规支付商 |
-| VPS 稳定性 | 服务中断 | 多节点、备份、监控、状态页、故障转移演练 |
-| 获客成本高 | 利润被广告吃掉 | 内容优先、渠道归因、CAC 上限、留存优先于盲目投放 |
-| 用户滥用 | 违规内容、垃圾请求 | 内容政策、速率限制、审核、封禁与申诉流程 |
-
-## 4. 月度营收与净利润估算
-
-### 4.1 核心假设
-
-| 项目 | 基准假设 |
+| 项目 | 基准 |
| --- | --- |
-| 付费用户 | 300 名月度活跃付费海外用户 |
-| 用户结构 | 基础 200 人;中度 70 人,各新增 1 个栏位;高付费 30 人,各新增 3 个栏位 |
-| 新增栏位 | `70 × 1 + 30 × 3 = 160` 个 |
-| 汇率 | `1 USDT ≈ 6.86 元`,实际应按收款日汇率入账 |
-| 基础套餐 | 20 USDT/月,含 900 TBU/月 |
-| 新增栏位 | 20 USDT/个/月,不额外赠送 TBU |
-| TBU 加购 | 60 人/月,平均 800 TBU/人,折后 0.15 元/TBU |
-| TBU 倍率 | 用户侧 1 TBU 对应供应商侧 `1 / 1.25 = 0.8` 单位 |
-| Token 采购成本 | 供应商侧 0.06 元/单位,需以实际中转站账单校验 |
-| 基础设施 | 1500 元/月 |
-| 运营获客 | 6000 元/月,含小额投放、内容、社群、客服 |
-| 其他成本 | 800 元/月,含域名、备份、邮件、监控、工具 |
-| 支付/合规预留 | 300 元/月,含链上异常处理、提现与台账成本 |
+| 付费用户 | 300 |
+| 基础套餐 | 20 USDT/月 |
+| 新增栏位 | 160 个/月 |
+| TBU 加购 | 60 人/月,平均 800 TBU |
+| 汇率 | 1 USDT ≈ 6.86 元 |
+| TBU 成本 | 供应商侧 0.06 元/单位,用户侧 1 TBU = 0.8 供应商单位 |
-### 4.2 月度收入
+收入:
-| 收入项 | 计算公式 | 金额 |
+| 收入项 | 公式 | 金额 |
| --- | ---: | ---: |
| 基础套餐 | `300 × 20 × 6.86` | 41,160 元 |
-| 新增工作流栏位 | `160 × 20 × 6.86` | 21,952 元 |
+| 新增栏位 | `160 × 20 × 6.86` | 21,952 元 |
| TBU 加购 | `60 × 800 × 0.15` | 7,200 元 |
-| **月度总营收** | | **70,312 元** |
+| 月营收 | | 70,312 元 |
-> 年度套餐不再额外计入 MRR。若 30 名用户选择年付,属于提前现金流入;收入确认仍应按服务期分摊,不能在 300 人基础套餐之外重复计算。
+成本:
-### 4.3 月度成本
+| 成本项 | 金额 |
+| --- | ---: |
+| 基础套餐 TBU 成本 | 12,960 元 |
+| 加购 TBU 成本 | 2,304 元 |
+| 基础设施 | 1,500-6,000 元,视 VPS/k3s/云资源而定 |
+| 运营获客 | 6,000 元起 |
+| 其他工具与支付预留 | 1,100 元起 |
-| 成本项 | 计算公式 | 金额 |
-| --- | ---: | ---: |
-| 基础套餐 TBU 成本 | `300 × 900 / 1.25 × 0.06` | 12,960 元 |
-| 加购 TBU 成本 | `60 × 800 / 1.25 × 0.06` | 2,304 元 |
-| **Token 总成本** | | **15,264 元** |
-| 基础设施 | VPS、备份、监控、冗余 | 1,500 元 |
-| 运营获客 | 内容、投放、客服、社群 | 6,000 元 |
-| 其他成本 | 域名、邮件、工具、备份维护 | 800 元 |
-| 支付/合规预留 | 链上异常、提现、台账 | 300 元 |
-| **月度总成本** | | **23,864 元** |
+说明:
-### 4.4 月度净利润
+- 年付用户不能重复计入 MRR。
+- 新增栏位不赠送 TBU。
+- k3s 高可用部署成本通常高于单机 VPS,财务模型要单独列“稳定性成本”。
-```text
-月度净利润 = 月度总营收 - 月度总成本
- = 70,312 - 23,864
- = 46,448 元
+## 12. 落地路线图
-净利率 = 46,448 / 70,312 ≈ 66%
-```
+### Phase 0:已完成
-### 4.5 情景测算
+- Python 模块化单体。
+- CLI demo。
+- HTTP API。
+- TBU 预扣与结算。
+- Workspace 与租户绑定。
+- USDT 订单与支付匹配骨架。
+- MVP 测试。
+- k3s 300 MAU 部署规划。
-| 情景 | 关键假设 | 月营收 | 月成本 | 月净利润 |
-| --- | --- | ---: | ---: | ---: |
-| 保守 | 300 用户;120 个新增栏位;40 人加购 600 TBU;Token 成本 0.08 元;运营成本 10,000 元 | 约 60,480 元 | 约 33,116 元 | 约 27,364 元 |
-| 基准 | 300 用户;160 个新增栏位;60 人加购 800 TBU;Token 成本 0.06 元;运营成本 6,000 元 | 70,312 元 | 23,864 元 | 46,448 元 |
-| 乐观 | 350 用户;220 个新增栏位;100 人加购 1000 TBU;Token 成本 0.05 元;运营成本 8,000 元 | 约 93,660 元 | 约 28,100 元 | 约 65,560 元 |
+### Phase 1:生产数据层
-### 4.6 盈亏平衡点
+- 将 `JsonStore` 替换为 PostgreSQL repository。
+- 引入数据库迁移。
+- 增加 Redis 队列与 worker。
+- 补齐幂等键与事务边界。
-只看基础套餐,不计算新增栏位和 TBU 加购:
+### Phase 2:真实服务接入
-- 单基础用户月收入:`20 × 6.86 = 137.2 元`
-- 单基础用户 TBU 成本:`900 / 1.25 × 0.06 = 43.2 元`
-- 单基础用户贡献毛利:`137.2 - 43.2 = 94 元`
-- 固定成本:`1500 + 6000 + 800 + 300 = 8600 元/月`
-- 基础用户盈亏平衡点:`8600 / 94 ≈ 92 人`
+- 接真实 Langflow Runtime Pool。
+- 接真实 AnythingLLM API。
+- 接 LLM Proxy。
+- 接 TronGrid / Tronscan 支付监听。
-结论:若 Token 采购成本保持在 0.06 元/供应商单位附近,且基础套餐不超卖,约 100 名稳定付费用户即可覆盖固定成本;300 用户阶段具备利润空间。
+### Phase 3:k3s 部署
-## 5. 关键 KPI
+- 编写 `deploy/k3s/base` manifests。
+- 拆分 `aurask-api` 与 `aurask-worker`。
+- 部署 PostgreSQL、Redis、Ingress、Secret、NetworkPolicy。
+- 接入 Prometheus / Loki / Grafana / Alertmanager。
-| 阶段 | KPI | 目标 |
-| --- | --- | --- |
-| 注册 | 访问到注册转化率 | 8%-15% |
-| 激活 | 注册后 24 小时内成功运行 1 次工作流 | 40%+ |
-| 付费 | 7 天体验到首月付费 | 8%-15% |
-| 留存 | 首月付费后次月续费 | 55%-70% |
-| 使用 | 每付费用户月均运行次数 | 30+ |
-| 成本 | Token 成本 / 总营收 | < 25% |
-| 支持 | 首响时间 | 24 小时内 |
-| 稳定性 | 月可用性 | 99%+ |
+### Phase 4:商业化 Beta
-## 6. 优先级清单
+- 英文 UI。
+- 模板库。
+- 支付异常后台。
+- 周报与留存邮件。
+- 用户协议、退款规则、AML 记录。
-### 必须优先完成
+## 13. 验收标准
-- Aurask 网关鉴权、套餐、TBU 预扣和实际结算。
-- Langflow 模板化执行,不向基础用户开放任意代码执行。
-- AnythingLLM Workspace 与 Aurask tenant 绑定。
-- USDT 订单台账、交易哈希匹配、异常订单人工处理。
-- 监控告警、备份恢复、成本报表。
+MVP 验收:
-### 第二阶段完成
+- `python -m unittest discover -s tests -v` 通过。
+- `uv run aurask demo --reset` 可创建租户、分配套餐、创建 Workspace、运行模板工作流。
+- 余额不足时阻止工作流执行。
+- TBU 加购支付后能发放额度。
+- 文档上传能按租户与 Workspace 记录路径。
-- 多节点 Runtime Pool 与任务队列。
-- 工作流模板市场。
-- 高付费用户独立容器/命名空间。
-- Stripe/Paddle 等合规支付方式。
-- 英文知识库、教程、视频和社群运营 SOP。
+300 MAU 生产验收:
-## 7. 参考来源
+- API P95 延迟 `< 500ms`,不含外部模型调用。
+- 并发工作流 `10-20` 稳定运行。
+- 工作流成功率 `95%+`。
+- 支付匹配成功率 `99%+`。
+- PostgreSQL 备份可恢复。
+- Langflow / AnythingLLM 不暴露公网。
+- NetworkPolicy 默认拒绝并按链路放通。
-- Langflow Security:
-- Langflow Environment Variables:
-- AnythingLLM Security & Access:
+## 14. 参考文档
+
+- `AGENTS.md`
+- `README.md`
+- `deploy/k3s/README.md`
+- k3s HA:
+- Longhorn:
+- cert-manager:
+- CloudNativePG:
- AnythingLLM System Requirements:
-- AnythingLLM Vector Databases:
-- TRON Resource Model:
-- RackNerd Black Friday KVM VPS offers:
-
-## 8. 最终建议
-
-Aurask 的主方向成立:用 Langflow 降低工作流搭建门槛,用 AnythingLLM 承载知识库/RAG,用 USDT 与轻量套餐服务海外小团队。但产品不能简单包装开源 UI 后直接多租户售卖,必须把 **安全隔离、计费网关、Token 成本归集、订单台账、模板化运营** 做成自己的核心能力。
-
-建议先以 50-100 名种子用户验证模板需求、TBU 消耗和付费转化,再扩到 300 名付费用户。只要 Token 成本与获客成本受控,300 用户阶段月净利润 4-5 万元人民币具备可实现性;若获客主要依赖付费广告或高成本模型,净利润会明显下降,应按周复盘 CAC、LTV、Token 毛利和退款率。
+- Langflow Security:
diff --git a/README.md b/README.md
index cd26015..ab75c61 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,126 @@
## Aurask
-This repository is now initialized as a Python project managed by `uv`.
+Aurask 首版已按 `Aurask_Technical_Operations_Plan.md` 落地为一个可运行的模块化单体后端,覆盖以下核心边界:
+
+- `Auth + API Gateway`
+- `Billing + Quota + TBU Ledger`
+- `Workflow Orchestrator`
+- `Langflow` 模板化运行适配层
+- `AnythingLLM` Workspace / 文档接入适配层
+- `USDT-TRC20` 订单与支付匹配
+- `Audit / Usage / Observability` 基础留痕
+
+当前实现是 **MVP 版本**:
+
+- 使用本地 JSON 文件持久化,便于开发和演示
+- 保留了租户、订单、额度、模板、知识库、支付等核心领域边界
+- 后续可以自然迁移到 PostgreSQL、任务队列、Runtime Pool 和真实外部服务
### Quick start
+运行演示:
+
```bash
-uv sync
-uv run aurask
+uv run aurask demo --reset
```
+启动本地网关:
+
+```bash
+uv run aurask serve --reset --host 127.0.0.1 --port 8080
+```
+
+### Demo flow
+
+`aurask demo` 会自动完成:
+
+1. 创建租户与 owner 用户
+2. 分配基础套餐
+3. 创建默认知识库 Workspace
+4. 执行一个安全模板工作流
+5. 输出 API Key、工作流结果和剩余额度
+
+### HTTP API
+
+公开接口:
+
+- `GET /health`
+- `GET /plans`
+- `POST /demo/bootstrap`
+- `POST /tenants`
+
+鉴权后接口:
+
+- `GET /quota`
+- `GET /workflow-templates`
+- `POST /workspaces`
+- `POST /documents`
+- `POST /orders`
+- `POST /payments/match`
+- `POST /workflow-runs`
+- `GET /workflow-runs/{run_id}`
+
+鉴权方式:
+
+```http
+Authorization: Bearer
+```
+
+### Example
+
+1. 创建演示租户:
+
+```bash
+curl -X POST http://127.0.0.1:8080/demo/bootstrap ^
+ -H "Content-Type: application/json" ^
+ -d "{}"
+```
+
+2. 使用返回的 `api_key` 查询模板:
+
+```bash
+curl http://127.0.0.1:8080/workflow-templates ^
+ -H "Authorization: Bearer "
+```
+
+3. 创建 Workspace:
+
+```bash
+curl -X POST http://127.0.0.1:8080/workspaces ^
+ -H "Authorization: Bearer " ^
+ -H "Content-Type: application/json" ^
+ -d "{\"name\":\"Support KB\"}"
+```
+
+4. 运行模板工作流:
+
+```bash
+curl -X POST http://127.0.0.1:8080/workflow-runs ^
+ -H "Authorization: Bearer " ^
+ -H "Content-Type: application/json" ^
+ -d "{\"template_id\":\"tpl_email_assistant\",\"inputs\":{\"topic\":\"refund policy reply\"}}"
+```
+
+### Testing
+
+```bash
+python -m unittest discover -s tests -v
+```
+
+### Deployment
+
+- `deploy/k3s/README.md`: 面向 `300` 名月度活跃用户的 `k3s` 部署方案
+
### Project layout
-- `pyproject.toml`: project metadata and packaging config
-- `.python-version`: default Python version for `uv`
-- `src/aurask`: application package
+- `AGENTS.md`: 项目级实现约束
+- `Aurask_Technical_Operations_Plan.md`: 技术与运营方案
+- `src/aurask/app.py`: 应用装配
+- `src/aurask/api.py`: HTTP 网关
+- `src/aurask/auth.py`: 租户、用户与 API Key
+- `src/aurask/billing.py`: 套餐、订单与权益发放
+- `src/aurask/quota.py`: TBU 预扣、结算与额度账本
+- `src/aurask/orchestrator.py`: 模板工作流编排
+- `src/aurask/knowledge_base.py`: Workspace 与文档接入
+- `src/aurask/payments.py`: USDT-TRC20 支付匹配
+- `tests/`: 单元测试
diff --git a/deploy/k3s/README.md b/deploy/k3s/README.md
new file mode 100644
index 0000000..92b765e
--- /dev/null
+++ b/deploy/k3s/README.md
@@ -0,0 +1,434 @@
+# Aurask `k3s` 部署方案(300 名月度活跃用户)
+
+本方案用于 Aurask 首版在 `k3s` 上的生产级部署规划,目标是支撑 **约 300 名月度活跃付费用户** 的稳定运行。
+该方案遵循 `Aurask_Technical_Operations_Plan.md` 的技术边界:**Aurask 网关为唯一公网入口、Langflow 模板化运行、AnythingLLM 与租户绑定、TBU 预扣/结算、订单台账与审计可追踪**。
+
+> 当前仓库实现仍是模块化单体 MVP。
+> 在 `k3s` 上建议先拆成 **`aurask-api` + `aurask-worker`** 两类工作负载,随后再逐步拆分为独立服务。
+
+## 1. 容量假设
+
+由于“300 月度活跃用户”不等于“300 并发用户”,本部署按以下合理首版假设规划:
+
+- 月度活跃付费用户:`300`
+- 日活高峰:`40-80`
+- 同时在线用户峰值:`15-30`
+- 同时工作流执行峰值:`10-20`
+- 基础套餐为主,绝大部分用户运行 **模板化 Langflow 工作流**
+- 外部大模型走 API / Proxy 路由,**不在集群内自建 GPU 推理**
+- AnythingLLM 文档总量控制在 `500GB` 以内
+- 向量层优先采用 `PostgreSQL + PGVector`
+
+如果后续出现以下任一情况,应升级到下一档集群:
+
+- 持续并发工作流 `> 20`
+- 文档总量 `> 500GB`
+- 高付费独立空间用户 `> 20`
+- AnythingLLM 或 Langflow 需要更强隔离时改用单租户 Namespace/Pod
+
+## 2. 目标部署拓扑
+
+建议采用 **`3` 台 control-plane + `4` 台 worker + `1` 个公网负载均衡入口** 的最小高可用拓扑。
+
+### 2.1 节点规划
+
+| 角色 | 数量 | 建议配置 | 用途 |
+| --- | ---: | --- | --- |
+| Public LB | 1 | 云负载均衡或双机 `HAProxy/Keepalived` | 统一暴露 `80/443` |
+| `k3s` server | 3 | `4 vCPU / 8GB RAM / 120GB SSD` | API Server、Scheduler、Controller、embedded etcd |
+| General worker | 2 | `8 vCPU / 16GB RAM / 200GB SSD` | `aurask-api`、`aurask-worker`、Traefik、observability |
+| AI/runtime worker | 2 | `8 vCPU / 16GB RAM / 250GB SSD` | Langflow Runtime、AnythingLLM、异步任务 |
+
+### 2.2 为什么是这个规模
+
+这个规格对应的是 **“300 MAU、10-20 并发工作流、外部模型 API”** 的首个生产档:
+
+- `3` 个 server 节点保证 `k3s` 控制面高可用
+- `4` 个 worker 节点可让应用、Runtime、数据库和监控具有基础调度弹性
+- 不再沿用文档里“低价单机 VPS 拼装”的方式,而是改为 **节点池 + 调度隔离**
+- 仍然控制在可运维范围内,适合首版商业化
+
+## 3. Namespace 设计
+
+建议至少划分以下命名空间:
+
+| Namespace | 作用 |
+| --- | --- |
+| `ingress-system` | Traefik / LoadBalancer 相关资源 |
+| `cert-manager` | TLS 证书管理 |
+| `aurask-system` | Aurask API、Worker、CronJob、Secrets |
+| `aurask-runtime` | Langflow Runtime、AnythingLLM |
+| `aurask-data` | PostgreSQL、Redis、内部状态性服务 |
+| `observability` | Prometheus、Loki、Grafana、Alertmanager |
+| `longhorn-system` | Longhorn 存储控制平面 |
+
+要求:
+
+- `Langflow`、`AnythingLLM`、`PostgreSQL`、`Redis` **不暴露公网 Ingress**
+- 默认 `NetworkPolicy` 为 `deny-all`,只放通必要南北向与东西向流量
+- 高付费独立用户后续可按租户扩展 `aurask-tenant-` 命名空间
+
+## 4. 组件部署清单
+
+### 4.1 边界入口层
+
+| 组件 | 副本数 | 部署位置 | 说明 |
+| --- | ---: | --- | --- |
+| Traefik | 2 | `ingress-system` | 统一入口,终止 TLS |
+| cert-manager | 2 | `cert-manager` | 自动签发 TLS 证书 |
+| External DNS | 1 | `ingress-system` | 可选,自动写 DNS |
+
+公网仅开放:
+
+- `80/tcp`
+- `443/tcp`
+
+Aurask 对外只暴露:
+
+- `aurask-web`
+- `aurask-api`
+
+**不要**暴露:
+
+- Langflow UI
+- AnythingLLM 管理后台
+- PostgreSQL
+- Redis
+- 内部 worker
+
+### 4.2 Aurask 应用层
+
+首版建议从同一个镜像拆出两个工作负载:
+
+| 工作负载 | 副本数 | 建议资源 `requests` | 建议资源 `limits` | 说明 |
+| --- | ---: | --- | --- | --- |
+| `aurask-api` | 3 | `500m CPU / 1Gi RAM` | `1 CPU / 2Gi RAM` | 网关、鉴权、订单、额度查询 |
+| `aurask-worker` | 3 | `1 CPU / 2Gi RAM` | `2 CPU / 4Gi RAM` | 工作流编排、异步执行、支付匹配、后台任务 |
+| `aurask-cron` | 1 | `250m CPU / 512Mi RAM` | `500m CPU / 1Gi RAM` | 账单修正、过期订单、周报任务 |
+
+建议:
+
+- `aurask-api` 开启 `HPA`,CPU `65%` 或自定义 QPS 指标触发扩容至 `5` 副本
+- `aurask-worker` 依据队列长度扩容至 `6` 副本
+- `PodDisruptionBudget` 至少保证 `aurask-api minAvailable=2`
+
+### 4.3 Runtime 层
+
+| 工作负载 | 副本数 | 建议资源 `requests` | 建议资源 `limits` | 说明 |
+| --- | ---: | --- | --- | --- |
+| `langflow-runtime` | 3 | `1500m CPU / 3Gi RAM` | `3 CPU / 6Gi RAM` | 仅运行审核模板,不开放任意代码执行 |
+| `anythingllm` | 2 | `1 CPU / 2Gi RAM` | `2 CPU / 4Gi RAM` | Workspace、文档、RAG、聊天记录 |
+
+Runtime 层要求:
+
+- 使用 `nodeSelector` 或 `taints/tolerations` 调度到 `AI/runtime worker`
+- `Langflow` 开启安全配置:
+ - `LANGFLOW_AUTO_LOGIN=False`
+ - `LANGFLOW_FALLBACK_TO_ENV_VAR=False`
+ - `LANGFLOW_DATABASE_URL` 指向集群内 PostgreSQL
+- `AnythingLLM` 只允许通过 Aurask 网关与后台服务访问
+- 两者都只通过 `ClusterIP` 暴露
+
+### 4.4 数据层
+
+#### 推荐首版方案
+
+| 组件 | 建议方案 | 副本/实例 | 说明 |
+| --- | --- | ---: | --- |
+| PostgreSQL | `CloudNativePG` + `PGVector` | 3 | 主库 + 副本 + 自动故障切换 |
+| PgBouncer | Deployment | 2 | 降低应用连接风暴 |
+| Redis | StatefulSet 或 Operator | 2-3 | 会话、缓存、任务队列 |
+| Object Storage | **优先外部 S3 兼容存储** | 外部 | 降低集群内状态复杂度 |
+
+原因:
+
+- `PGVector` 可同时承载业务数据库与向量检索,适合 Aurask 首版
+- `CloudNativePG` 比自建主从脚本更适合作为 `k3s` 首版生产标准
+- 文档对象、备份对象尽量放到外部对象存储,避免首版把 `MinIO`、数据库、向量库、应用全部堆在一个小集群里
+
+#### PostgreSQL 资源建议
+
+| 组件 | 副本数 | `requests` | `limits` |
+| --- | ---: | --- | --- |
+| `cnpg-cluster` instance | 3 | `2 CPU / 4Gi RAM` | `4 CPU / 8Gi RAM` |
+| `pgbouncer` | 2 | `500m CPU / 512Mi RAM` | `1 CPU / 1Gi RAM` |
+
+存储建议:
+
+- 数据卷:每实例 `200GB` 起
+- 存储类型:优先云块存储 / CSI Block / 本地 NVMe
+- 如果必须首版全部自建,可使用 `Longhorn`,但数据库卷要使用更快磁盘池
+
+#### Redis 资源建议
+
+| 组件 | 副本数 | `requests` | `limits` |
+| --- | ---: | --- | --- |
+| `redis` | 2 | `500m CPU / 1Gi RAM` | `1 CPU / 2Gi RAM` |
+
+Redis 在 Aurask 中主要承担:
+
+- 会话
+- 幂等键
+- 工作流队列
+- 限流与短期缓存
+
+## 5. 存储方案
+
+### 5.1 首版推荐
+
+- `Longhorn`:为集群内 PVC 提供高可用块存储
+- 外部 S3 兼容对象存储:保存文档对象、PostgreSQL 备份、审计归档、Longhorn 备份
+
+### 5.2 Longhorn 使用边界
+
+`Longhorn` 更适合:
+
+- 应用 PVC
+- AnythingLLM 一般性文档缓存
+- Redis/监控等非极端 IOPS 场景
+
+`Longhorn` 不应被当成性能无限的数据库盘。
+对 PostgreSQL:
+
+- 最优是云块存储或本地 NVMe + 复制/备份
+- 次优才是 `Longhorn` 高副本卷
+
+### 5.3 建议存储类
+
+| StorageClass | 用途 | 副本数 |
+| --- | --- | ---: |
+| `longhorn-general` | 应用与一般 PVC | 2 |
+| `longhorn-critical` | AnythingLLM、审计、重要状态 | 3 |
+| `cnpg-fast` | PostgreSQL | 3 或使用外部 CSI |
+
+## 6. 网络与安全
+
+### 6.1 网络策略
+
+必须启用:
+
+- 默认拒绝所有 Pod 间访问
+- `aurask-api -> aurask-worker`
+- `aurask-api / aurask-worker -> PostgreSQL`
+- `aurask-api / aurask-worker -> Redis`
+- `aurask-worker -> Langflow`
+- `aurask-worker -> AnythingLLM`
+- `Langflow / AnythingLLM -> 外部模型代理或白名单域名`
+
+### 6.2 入口安全
+
+- 强制 HTTPS
+- 开启 WAF/基础限流
+- 对 `/payments/*`、`/auth/*`、`/workflow-runs` 增加速率限制
+- 后台入口强制二次认证
+
+### 6.3 Secret 管理
+
+建议使用以下任一方案:
+
+- `External Secrets Operator` + 外部密钥管理
+- `SOPS` + `age`
+
+不要把以下信息明文提交到 Git:
+
+- `LANGFLOW_SECRET_KEY`
+- `DATABASE_URL`
+- `TRC20` 收款钱包配置
+- 第三方 LLM API Key
+- SMTP / Webhook 签名密钥
+
+## 7. 可观测性
+
+建议部署:
+
+| 组件 | 作用 |
+| --- | --- |
+| Prometheus | 指标采集 |
+| Grafana | 可视化 |
+| Loki | 日志检索 |
+| Alertmanager | 告警 |
+| kube-state-metrics | 集群对象指标 |
+| node-exporter | 节点指标 |
+
+至少监控以下指标:
+
+- `aurask-api` 请求量、延迟、错误率
+- `workflow_runs_total`
+- `workflow_run_failed_total`
+- `tbu_reserved_total`
+- `tbu_consumed_total`
+- `queue_depth`
+- `langflow_runtime_seconds`
+- `anythingllm_document_ingest_seconds`
+- PostgreSQL CPU、连接数、WAL、复制延迟
+- Redis 内存、延迟、命中率
+
+告警优先级:
+
+- `P1`:API 全站不可用、数据库主库不可写、支付匹配故障
+- `P2`:工作流失败率升高、AnythingLLM 入库堆积、队列堆积
+- `P3`:磁盘使用率、备份失败、证书续期异常
+
+## 8. 备份与容灾
+
+### 8.1 PostgreSQL
+
+- 使用 `CloudNativePG` 做持续归档与定时备份
+- 备份目标:外部 S3 兼容对象存储
+- 目标:
+ - `RPO <= 15 分钟`
+ - `RTO <= 2 小时`
+
+### 8.2 Longhorn
+
+- 每日快照
+- 每周备份到对象存储
+- 每月恢复演练一次
+
+### 8.3 AnythingLLM 与 Aurask 审计数据
+
+- 文档对象外部化存储
+- 审计日志每日归档
+- 支付订单与链上交易匹配记录保留至少 `180` 天
+
+## 9. 推荐调度策略
+
+### 9.1 节点标签
+
+建议给节点加标签:
+
+- `node-role.kubernetes.io/runtime=true`
+- `node-role.kubernetes.io/general=true`
+- `node-role.kubernetes.io/data=true`
+
+### 9.2 调度建议
+
+- `aurask-api`:调度到 `general`
+- `aurask-worker`:优先 `general`,可溢出到 `runtime`
+- `langflow-runtime`:只调度到 `runtime`
+- `anythingllm`:优先 `runtime`
+- PostgreSQL / Redis:优先 `data` 或资源更稳定的 worker
+
+### 9.3 高可用约束
+
+- `topologySpreadConstraints`
+- `podAntiAffinity`
+- `PodDisruptionBudget`
+
+避免:
+
+- 同一应用所有副本落在同一节点
+- PostgreSQL 三个实例共用同一宿主机
+- Langflow 与数据库争抢同一节点全部资源
+
+## 10. 推荐发布方式
+
+建议使用:
+
+- `Helm` 管理第三方组件
+- `Kustomize` 管理 Aurask 自研服务
+- `GitOps` 管理环境变更
+
+建议目录:
+
+```text
+deploy/
+ k3s/
+ README.md
+ base/
+ namespace.yaml
+ network-policies.yaml
+ aurask-api.yaml
+ aurask-worker.yaml
+ ingress.yaml
+ overlays/
+ staging/
+ production/
+```
+
+## 11. 分阶段上线顺序
+
+### Phase 1:集群底座
+
+1. 创建 `3 server + 4 worker` `k3s` 集群
+2. 配置公网 LB / MetalLB / DNS
+3. 安装 `cert-manager`
+4. 安装 `Longhorn`
+5. 安装 observability 基础栈
+
+### Phase 2:数据层
+
+1. 部署 `CloudNativePG`
+2. 初始化 `PostgreSQL + PGVector`
+3. 部署 `PgBouncer`
+4. 部署 `Redis`
+5. 配置对象存储与备份桶
+
+### Phase 3:应用层
+
+1. 部署 `aurask-api`
+2. 部署 `aurask-worker`
+3. 部署 `Langflow Runtime`
+4. 部署 `AnythingLLM`
+5. 接通内部服务发现与 NetworkPolicy
+
+### Phase 4:生产化
+
+1. 开启 `HPA`
+2. 打通告警
+3. 打通 PostgreSQL 备份
+4. 打通 Longhorn 备份
+5. 完成一次恢复演练
+6. 完成一次工作流压测与支付链路演练
+
+## 12. 首版压测目标
+
+建议上线前达到以下最低标准:
+
+| 项目 | 目标 |
+| --- | --- |
+| API 可用性 | `99.5%+` |
+| 健康工作流成功率 | `95%+` |
+| 支付匹配成功率 | `99%+` |
+| 峰值并发工作流 | `10-20` |
+| P95 API 延迟 | `< 500ms`(不含外部模型调用) |
+| P95 工作流排队时间 | `< 10s` |
+| PostgreSQL 备份恢复演练 | 每月 1 次 |
+
+## 13. 当前实现与目标部署的差异
+
+当前仓库已经具备首版领域骨架,但与 `k3s` 生产部署之间还存在以下差异:
+
+1. 当前仍是单进程模块化单体,需要拆成 `API` 与 `Worker`
+2. 当前使用本地 JSON 持久化,生产需切到 `PostgreSQL`
+3. 当前 Langflow / AnythingLLM 是适配层,生产需接真实服务
+4. 当前未交付 Helm/Kustomize manifests,本文档先定义目标方案
+
+## 14. 首版推荐结论
+
+如果 Aurask 现在就要按 **300 名月度活跃付费用户** 上 `k3s`,推荐的最稳妥首版是:
+
+- `3` 台 `k3s server`
+- `4` 台 worker
+- `Traefik + cert-manager`
+- `CloudNativePG + PGVector`
+- `Redis`
+- `Longhorn`
+- `Prometheus + Grafana + Loki + Alertmanager`
+- Aurask 自身先拆成 `aurask-api` 与 `aurask-worker`
+- Langflow / AnythingLLM 作为内部 `ClusterIP` 服务接入
+
+这个方案的重点不是“极限压低成本”,而是 **先满足 300 MAU 阶段的稳定性、隔离、审计、扩容和恢复能力**。
+
+## 15. 官方参考
+
+以下官方文档可作为落地实施时的基线依据:
+
+- `k3s` Cluster Datastore:
+- `k3s` High Availability Embedded etcd:
+- `cert-manager` Installation:
+- `Longhorn` Installation / Requirements:
+- `CloudNativePG` Documentation:
+- `CloudNativePG` Backup:
+- `AnythingLLM` Self-hosted System Requirements:
+- `Langflow` Security:
diff --git a/pyproject.toml b/pyproject.toml
index 48f1b4e..18f0c4e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[project]
name = "aurask"
version = "0.1.0"
-description = "Aurask Python project managed by uv"
+description = "Aurask MVP backend for gateway, billing, workflow orchestration, and knowledge base integration"
readme = "README.md"
authors = [
{ name = "Xu Jian", email = "xujian1@szlanyou.com" },
diff --git a/src/aurask/__init__.py b/src/aurask/__init__.py
index abf0c84..fc81782 100644
--- a/src/aurask/__init__.py
+++ b/src/aurask/__init__.py
@@ -1,2 +1,5 @@
-def main() -> None:
- print("Hello from aurask!")
+"""Aurask MVP package."""
+
+from aurask.cli import main
+
+__all__ = ["main"]
diff --git a/src/aurask/__main__.py b/src/aurask/__main__.py
new file mode 100644
index 0000000..f1907b1
--- /dev/null
+++ b/src/aurask/__main__.py
@@ -0,0 +1,5 @@
+from aurask.cli import main
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/aurask/api.py b/src/aurask/api.py
new file mode 100644
index 0000000..61f9883
--- /dev/null
+++ b/src/aurask/api.py
@@ -0,0 +1,153 @@
+"""Small standard-library HTTP gateway for the first Aurask backend."""
+
+from __future__ import annotations
+
+import json
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from typing import Any
+from urllib.parse import urlparse
+
+from aurask.app import AuraskApp
+from aurask.errors import AuraskError
+
+
+class AuraskHTTPServer(ThreadingHTTPServer):
+ def __init__(self, server_address: tuple[str, int], RequestHandlerClass, app: AuraskApp) -> None:
+ super().__init__(server_address, RequestHandlerClass)
+ self.app = app
+
+
+def make_handler(app: AuraskApp):
+ class GatewayHandler(BaseHTTPRequestHandler):
+ server_version = "AuraskMVP/0.1"
+
+ def do_GET(self) -> None:
+ self._handle("GET")
+
+ def do_POST(self) -> None:
+ self._handle("POST")
+
+ def log_message(self, format: str, *args: Any) -> None:
+ app.audit.record("gateway.access", summary=format % args)
+
+ def _handle(self, method: str) -> None:
+ path = urlparse(self.path).path.rstrip("/") or "/"
+ try:
+ if method == "GET" and path == "/health":
+ self._send(200, {"status": "ok", "service": "aurask"})
+ return
+ if method == "GET" and path == "/plans":
+ self._send(200, app.billing.list_plans())
+ return
+ if method == "POST" and path == "/demo/bootstrap":
+ body = self._read_json()
+ self._send(201, app.bootstrap_demo(tenant_name=body.get("tenant_name", "Aurask Demo"), email=body.get("email", "owner@example.com")))
+ return
+ if method == "POST" and path == "/tenants":
+ body = self._read_json()
+ tenant = app.auth.create_tenant(body.get("name", "Aurask Tenant"))
+ user = app.auth.create_user(tenant["id"], body.get("email", "owner@example.com"))
+ app.billing.grant_plan_without_payment(tenant["id"], "free_trial", source="self_signup")
+ self._send(201, {"tenant": tenant, "user": user, "api_key": user["api_key"], "quota": app.quota.get_account(tenant["id"])})
+ return
+
+ context = self._authenticate()
+ tenant_id = context["tenant"]["id"]
+ user_id = context["user"]["id"]
+
+ if method == "GET" and path == "/quota":
+ self._send(200, app.quota.get_account(tenant_id))
+ return
+ if method == "GET" and path == "/workflow-templates":
+ self._send(200, {"templates": app.orchestrator.list_templates()})
+ return
+ if method == "POST" and path == "/workspaces":
+ body = self._read_json()
+ self._send(201, app.knowledge.create_workspace(tenant_id, user_id, body.get("name", "")))
+ return
+ if method == "POST" and path == "/documents":
+ body = self._read_json()
+ document = app.knowledge.upload_document(
+ tenant_id,
+ user_id,
+ body.get("workspace_id", ""),
+ filename=body.get("filename", ""),
+ size_bytes=int(body.get("size_bytes", 0)),
+ content_type=body.get("content_type", ""),
+ content_preview=body.get("content_preview", ""),
+ )
+ self._send(201, document)
+ return
+ if method == "POST" and path == "/orders":
+ body = self._read_json()
+ order = app.billing.create_order(tenant_id, user_id, body.get("product_code", ""), quantity=int(body.get("quantity", 1)))
+ self._send(201, order)
+ return
+ if method == "POST" and path == "/payments/match":
+ body = self._read_json()
+ payment = app.payments.match_trc20_payment(
+ tenant_id,
+ body.get("order_id", ""),
+ tx_hash=body.get("tx_hash", ""),
+ amount_usdt=float(body.get("amount_usdt", 0)),
+ confirmations=int(body.get("confirmations", 20)),
+ )
+ self._send(201, payment)
+ return
+ if method == "POST" and path == "/workflow-runs":
+ body = self._read_json()
+ run = app.orchestrator.run_template(
+ tenant_id,
+ user_id,
+ body.get("template_id", ""),
+ workspace_id=body.get("workspace_id"),
+ inputs=body.get("inputs", {}),
+ )
+ self._send(201, run)
+ return
+ if method == "GET" and path.startswith("/workflow-runs/"):
+ run_id = path.split("/", 2)[2]
+ run = app.store.get("workflow_runs", run_id)
+ if not run or run["tenant_id"] != tenant_id:
+ self._send(404, {"error": {"code": "not_found", "message": "workflow run not found"}})
+ return
+ self._send(200, run)
+ return
+ self._send(404, {"error": {"code": "not_found", "message": "route not found"}})
+ except AuraskError as exc:
+ self._send(exc.status_code, {"error": {"code": exc.code, "message": exc.message, "details": exc.details}})
+ except Exception as exc:
+ app.audit.record("gateway.error", summary=exc.__class__.__name__, metadata={"message": str(exc)})
+ self._send(500, {"error": {"code": "internal_error", "message": "internal server error"}})
+
+ def _authenticate(self) -> dict:
+ return app.auth.authenticate(self.headers.get("Authorization"))
+
+ def _read_json(self) -> dict:
+ length = int(self.headers.get("Content-Length", "0"))
+ if not length:
+ return {}
+ payload = self.rfile.read(length).decode("utf-8")
+ return json.loads(payload)
+
+ def _send(self, status: int, payload: dict) -> None:
+ encoded = json.dumps(payload, ensure_ascii=False, indent=2).encode("utf-8")
+ self.send_response(status)
+ self.send_header("Content-Type", "application/json; charset=utf-8")
+ self.send_header("Content-Length", str(len(encoded)))
+ self.end_headers()
+ self.wfile.write(encoded)
+
+ return GatewayHandler
+
+
+def run_server(app: AuraskApp, *, host: str = "127.0.0.1", port: int = 8080) -> None:
+ handler = make_handler(app)
+ server = AuraskHTTPServer((host, port), handler, app)
+ try:
+ print(f"Aurask gateway listening on http://{host}:{port}")
+ server.serve_forever()
+ except KeyboardInterrupt:
+ print("\nAurask gateway stopped")
+ finally:
+ server.server_close()
diff --git a/src/aurask/app.py b/src/aurask/app.py
new file mode 100644
index 0000000..d015409
--- /dev/null
+++ b/src/aurask/app.py
@@ -0,0 +1,66 @@
+"""Application assembly for the Aurask MVP."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+
+from aurask.audit import AuditService
+from aurask.auth import AuthService
+from aurask.billing import BillingService
+from aurask.knowledge_base import KnowledgeBaseService
+from aurask.orchestrator import WorkflowOrchestrator
+from aurask.payments import PaymentService
+from aurask.plans import seed_plan_catalog
+from aurask.quota import QuotaService
+from aurask.repository import JsonStore
+
+
+@dataclass
+class AuraskApp:
+ store: JsonStore
+ audit: AuditService
+ auth: AuthService
+ quota: QuotaService
+ billing: BillingService
+ payments: PaymentService
+ knowledge: KnowledgeBaseService
+ orchestrator: WorkflowOrchestrator
+
+ def bootstrap_demo(self, *, tenant_name: str = "Aurask Demo", email: str = "owner@example.com") -> dict:
+ tenant = self.auth.create_tenant(tenant_name)
+ user = self.auth.create_user(tenant["id"], email)
+ self.billing.grant_plan_without_payment(tenant["id"], "basic_monthly", source="demo_bootstrap")
+ workspace = self.knowledge.create_workspace(tenant["id"], user["id"], "Default Knowledge Base")
+ return {
+ "tenant": tenant,
+ "user": user,
+ "api_key": user["api_key"],
+ "workspace": workspace,
+ "quota": self.quota.get_account(tenant["id"]),
+ }
+
+
+def create_app(data_path: str | Path | None = None, *, reset: bool = False) -> AuraskApp:
+ store = JsonStore(data_path)
+ if reset:
+ store.delete_all()
+ audit = AuditService(store)
+ auth = AuthService(store, audit)
+ quota = QuotaService(store, audit)
+ billing = BillingService(store, quota, audit)
+ payments = PaymentService(store, billing, audit)
+ knowledge = KnowledgeBaseService(store, quota, audit)
+ orchestrator = WorkflowOrchestrator(store, quota, audit)
+ seed_plan_catalog(store)
+ orchestrator.seed_templates()
+ return AuraskApp(
+ store=store,
+ audit=audit,
+ auth=auth,
+ quota=quota,
+ billing=billing,
+ payments=payments,
+ knowledge=knowledge,
+ orchestrator=orchestrator,
+ )
diff --git a/src/aurask/audit.py b/src/aurask/audit.py
new file mode 100644
index 0000000..451646c
--- /dev/null
+++ b/src/aurask/audit.py
@@ -0,0 +1,37 @@
+"""Tenant-aware audit event recording."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from aurask.ids import new_id, now_iso
+from aurask.repository import JsonStore
+
+
+class AuditService:
+ def __init__(self, store: JsonStore) -> None:
+ self.store = store
+
+ def record(
+ self,
+ event_type: str,
+ *,
+ tenant_id: str | None = None,
+ user_id: str | None = None,
+ resource_type: str | None = None,
+ resource_id: str | None = None,
+ summary: str = "",
+ metadata: dict[str, Any] | None = None,
+ ) -> dict[str, Any]:
+ event = {
+ "id": new_id("audit"),
+ "event_type": event_type,
+ "tenant_id": tenant_id,
+ "user_id": user_id,
+ "resource_type": resource_type,
+ "resource_id": resource_id,
+ "summary": summary,
+ "metadata": metadata or {},
+ "created_at": now_iso(),
+ }
+ return self.store.put("audit_events", event["id"], event)
diff --git a/src/aurask/auth.py b/src/aurask/auth.py
new file mode 100644
index 0000000..c7c7fa6
--- /dev/null
+++ b/src/aurask/auth.py
@@ -0,0 +1,77 @@
+"""Tenant, user, and API-key authentication."""
+
+from __future__ import annotations
+
+import secrets
+
+from aurask.audit import AuditService
+from aurask.errors import AuthError, ForbiddenError, NotFoundError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.repository import JsonStore
+
+
+class AuthService:
+ def __init__(self, store: JsonStore, audit: AuditService) -> None:
+ self.store = store
+ self.audit = audit
+
+ def create_tenant(self, name: str, *, region: str = "overseas") -> dict:
+ if not name:
+ raise ValidationError("tenant name is required")
+ tenant = {
+ "id": new_id("tenant"),
+ "name": name,
+ "region": region,
+ "status": "active",
+ "created_at": now_iso(),
+ }
+ self.store.put("tenants", tenant["id"], tenant)
+ self.audit.record("tenant.created", tenant_id=tenant["id"], resource_type="tenant", resource_id=tenant["id"])
+ return tenant
+
+ def create_user(self, tenant_id: str, email: str, *, role: str = "owner") -> dict:
+ self._require_tenant(tenant_id)
+ if "@" not in email:
+ raise ValidationError("valid email is required")
+ api_key = f"ak_{secrets.token_urlsafe(24)}"
+ user = {
+ "id": new_id("user"),
+ "tenant_id": tenant_id,
+ "email": email,
+ "role": role,
+ "status": "active",
+ "api_key_preview": api_key[:10],
+ "created_at": now_iso(),
+ }
+ self.store.put("users", user["id"], user)
+ self.store.put("api_keys", api_key, {"api_key": api_key, "user_id": user["id"], "tenant_id": tenant_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}
+
+ def authenticate(self, authorization_header: str | None) -> dict:
+ if not authorization_header or not authorization_header.startswith("Bearer "):
+ raise AuthError("missing bearer token")
+ api_key = authorization_header.removeprefix("Bearer ").strip()
+ key_record = self.store.get("api_keys", api_key)
+ if not key_record:
+ raise AuthError("invalid bearer token")
+ user = self.store.get("users", key_record["user_id"])
+ if not user or user.get("status") != "active":
+ raise AuthError("inactive user")
+ tenant = self.store.get("tenants", user["tenant_id"])
+ if not tenant or tenant.get("status") != "active":
+ raise AuthError("inactive tenant")
+ return {"tenant": tenant, "user": user, "api_key": api_key}
+
+ def require_tenant_user(self, tenant_id: str, user_id: str) -> None:
+ user = self.store.get("users", user_id)
+ if not user:
+ raise NotFoundError("user not found")
+ if user["tenant_id"] != tenant_id:
+ raise ForbiddenError("user does not belong to tenant")
+
+ def _require_tenant(self, tenant_id: str) -> dict:
+ tenant = self.store.get("tenants", tenant_id)
+ if not tenant:
+ raise NotFoundError("tenant not found")
+ return tenant
diff --git a/src/aurask/billing.py b/src/aurask/billing.py
new file mode 100644
index 0000000..fbc73d0
--- /dev/null
+++ b/src/aurask/billing.py
@@ -0,0 +1,99 @@
+"""Billing, subscriptions, products, and entitlement fulfillment."""
+
+from __future__ import annotations
+
+from aurask.audit import AuditService
+from aurask.errors import NotFoundError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.plans import PLAN_CATALOG, PRODUCT_CATALOG
+from aurask.quota import QuotaService
+from aurask.repository import JsonStore
+
+
+class BillingService:
+ def __init__(self, store: JsonStore, quota: QuotaService, audit: AuditService) -> None:
+ self.store = store
+ self.quota = quota
+ self.audit = audit
+
+ def list_plans(self) -> dict:
+ return {
+ "plans": [plan.to_dict() for plan in PLAN_CATALOG.values()],
+ "products": list(PRODUCT_CATALOG.values()),
+ }
+
+ def create_order(self, tenant_id: str, user_id: str, product_code: str, *, quantity: int = 1) -> dict:
+ if quantity < 1:
+ raise ValidationError("quantity must be positive")
+ product = PRODUCT_CATALOG.get(product_code)
+ if not product:
+ raise NotFoundError("product not found")
+ amount_usdt = round(product["price_usdt"] * quantity, 2)
+ order = {
+ "id": new_id("order"),
+ "tenant_id": tenant_id,
+ "user_id": user_id,
+ "product_code": product_code,
+ "product_kind": product["kind"],
+ "quantity": quantity,
+ "amount_usdt": amount_usdt,
+ "chain": "TRC20",
+ "currency": "USDT",
+ "receiving_address": "TXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
+ "status": "pending",
+ "expires_in_minutes": 30,
+ "created_at": now_iso(),
+ }
+ self.store.put("orders", order["id"], order)
+ self.audit.record("billing.order_created", tenant_id=tenant_id, user_id=user_id, resource_type="order", resource_id=order["id"])
+ return order
+
+ def fulfill_order(self, order_id: str) -> dict:
+ order = self.store.get("orders", order_id)
+ if not order:
+ raise NotFoundError("order not found")
+ if order["status"] == "fulfilled":
+ return order
+ if order["status"] != "paid":
+ raise ValidationError("order must be paid before fulfillment")
+ product = PRODUCT_CATALOG[order["product_code"]]
+ tenant_id = order["tenant_id"]
+ quantity = order["quantity"]
+ if product["kind"] == "subscription":
+ plan = PLAN_CATALOG[product["plan_code"]].to_dict()
+ self._activate_subscription(tenant_id, plan["code"], order["id"])
+ self.quota.apply_plan(tenant_id, plan)
+ elif product["kind"] == "workflow_slot":
+ self.quota.add_workflow_slot(tenant_id, quantity)
+ elif product["kind"] == "tbu_package":
+ total_tbu = (product["tbu"] + product.get("bonus_tbu", 0)) * quantity
+ self.quota.add_tbu(tenant_id, total_tbu, reason="tbu_package", source_id=order["id"])
+ else:
+ raise ValidationError("unsupported product kind")
+ order["status"] = "fulfilled"
+ order["fulfilled_at"] = now_iso()
+ self.store.put("orders", order_id, order)
+ self.audit.record("billing.order_fulfilled", tenant_id=tenant_id, resource_type="order", resource_id=order_id)
+ return order
+
+ def grant_plan_without_payment(self, tenant_id: str, plan_code: str, *, source: str) -> dict:
+ plan = PLAN_CATALOG.get(plan_code)
+ if not plan:
+ raise NotFoundError("plan not found")
+ subscription = self._activate_subscription(tenant_id, plan.code, source)
+ self.quota.apply_plan(tenant_id, plan.to_dict())
+ return subscription
+
+ def _activate_subscription(self, tenant_id: str, plan_code: str, source_id: str) -> dict:
+ subscription_id = f"{tenant_id}:{plan_code}"
+ subscription = {
+ "id": subscription_id,
+ "tenant_id": tenant_id,
+ "plan_code": plan_code,
+ "status": "active",
+ "source_id": source_id,
+ "updated_at": now_iso(),
+ }
+ self.store.put("subscriptions", subscription_id, subscription)
+ self.audit.record("billing.subscription_active", tenant_id=tenant_id, resource_type="subscription", resource_id=subscription_id)
+ return subscription
diff --git a/src/aurask/cli.py b/src/aurask/cli.py
new file mode 100644
index 0000000..a4c92cb
--- /dev/null
+++ b/src/aurask/cli.py
@@ -0,0 +1,65 @@
+"""Command line entrypoint."""
+
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+
+from aurask.api import run_server
+from aurask.app import create_app
+
+
+DEFAULT_DATA_PATH = Path(".aurask/state.json")
+
+
+def main() -> None:
+ parser = argparse.ArgumentParser(prog="aurask", description="Aurask MVP gateway and workflow backend")
+ parser.set_defaults(data=str(DEFAULT_DATA_PATH), reset=False)
+ subparsers = parser.add_subparsers(dest="command")
+
+ serve_parser = subparsers.add_parser("serve", help="Run the Aurask HTTP gateway")
+ serve_parser.add_argument("--data", default=str(DEFAULT_DATA_PATH), help="JSON state file for MVP persistence")
+ serve_parser.add_argument("--host", default="127.0.0.1")
+ serve_parser.add_argument("--port", type=int, default=8080)
+ serve_parser.add_argument("--reset", action="store_true", help="Reset local MVP state before serving")
+
+ demo_parser = subparsers.add_parser("demo", help="Bootstrap a tenant and run a safe template workflow")
+ demo_parser.add_argument("--data", default=str(DEFAULT_DATA_PATH), help="JSON state file for MVP persistence")
+ demo_parser.add_argument("--reset", action="store_true", help="Reset local MVP state before running demo")
+
+ args = parser.parse_args()
+ command = args.command or "demo"
+
+ if command == "serve":
+ app = create_app(args.data, reset=args.reset)
+ run_server(app, host=args.host, port=args.port)
+ return
+
+ if command == "demo":
+ app = create_app(args.data, reset=args.reset)
+ bootstrap = app.bootstrap_demo()
+ run = app.orchestrator.run_template(
+ bootstrap["tenant"]["id"],
+ bootstrap["user"]["id"],
+ "tpl_knowledge_qa",
+ workspace_id=bootstrap["workspace"]["id"],
+ inputs={"question": "How can Aurask help my support team?"},
+ )
+ print(
+ json.dumps(
+ {
+ "message": "Aurask MVP demo completed",
+ "api_key": bootstrap["api_key"],
+ "tenant_id": bootstrap["tenant"]["id"],
+ "workspace_id": bootstrap["workspace"]["id"],
+ "workflow_run": run,
+ "quota": app.quota.get_account(bootstrap["tenant"]["id"]),
+ },
+ ensure_ascii=False,
+ indent=2,
+ )
+ )
+ return
+
+ parser.error(f"unknown command: {command}")
diff --git a/src/aurask/errors.py b/src/aurask/errors.py
new file mode 100644
index 0000000..c010920
--- /dev/null
+++ b/src/aurask/errors.py
@@ -0,0 +1,43 @@
+"""Application errors mapped by the HTTP gateway."""
+
+from __future__ import annotations
+
+
+class AuraskError(Exception):
+ status_code = 400
+ code = "aurask_error"
+
+ def __init__(self, message: str, *, details: dict | None = None) -> None:
+ super().__init__(message)
+ self.message = message
+ self.details = details or {}
+
+
+class NotFoundError(AuraskError):
+ status_code = 404
+ code = "not_found"
+
+
+class AuthError(AuraskError):
+ status_code = 401
+ code = "unauthorized"
+
+
+class ForbiddenError(AuraskError):
+ status_code = 403
+ code = "forbidden"
+
+
+class QuotaError(AuraskError):
+ status_code = 402
+ code = "quota_insufficient"
+
+
+class ValidationError(AuraskError):
+ status_code = 422
+ code = "validation_error"
+
+
+class ConflictError(AuraskError):
+ status_code = 409
+ code = "conflict"
diff --git a/src/aurask/ids.py b/src/aurask/ids.py
new file mode 100644
index 0000000..0f0b379
--- /dev/null
+++ b/src/aurask/ids.py
@@ -0,0 +1,14 @@
+"""Identifier and clock helpers."""
+
+from __future__ import annotations
+
+from datetime import UTC, datetime
+from uuid import uuid4
+
+
+def new_id(prefix: str) -> str:
+ return f"{prefix}_{uuid4().hex[:16]}"
+
+
+def now_iso() -> str:
+ return datetime.now(UTC).isoformat()
diff --git a/src/aurask/knowledge_base.py b/src/aurask/knowledge_base.py
new file mode 100644
index 0000000..146cbea
--- /dev/null
+++ b/src/aurask/knowledge_base.py
@@ -0,0 +1,113 @@
+"""AnythingLLM workspace binding and document intake facade."""
+
+from __future__ import annotations
+
+import hashlib
+from math import ceil
+
+from aurask.audit import AuditService
+from aurask.errors import ForbiddenError, NotFoundError, QuotaError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.plans import PLAN_CATALOG
+from aurask.quota import QuotaService
+from aurask.repository import JsonStore
+
+
+ALLOWED_CONTENT_TYPES = {
+ "text/plain",
+ "text/markdown",
+ "text/csv",
+ "application/pdf",
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
+}
+
+
+class AnythingLLMAdapter:
+ """MVP adapter that records the external contract without exposing AnythingLLM."""
+
+ def create_workspace(self, tenant_id: str, name: str) -> str:
+ slug = name.lower().replace(" ", "-")[:40]
+ return f"anythingllm-{tenant_id}-{slug}"
+
+ def ingest_document(self, workspace_external_id: str, document: dict) -> dict:
+ return {
+ "external_document_id": f"{workspace_external_id}-{document['id']}",
+ "status": "queued",
+ }
+
+
+class KnowledgeBaseService:
+ def __init__(self, store: JsonStore, quota: QuotaService, audit: AuditService, adapter: AnythingLLMAdapter | None = None) -> None:
+ self.store = store
+ self.quota = quota
+ self.audit = audit
+ self.adapter = adapter or AnythingLLMAdapter()
+
+ def create_workspace(self, tenant_id: str, user_id: str, name: str) -> dict:
+ if not name:
+ raise ValidationError("workspace name is required")
+ account = self.quota.get_account(tenant_id)
+ existing_count = len([workspace for workspace in self.store.list("workspaces") if workspace["tenant_id"] == tenant_id])
+ if existing_count >= account["knowledge_bases"]:
+ raise QuotaError("knowledge base quota exceeded", details={"allowed": account["knowledge_bases"]})
+ workspace = {
+ "id": new_id("ws"),
+ "tenant_id": tenant_id,
+ "created_by": user_id,
+ "name": name,
+ "external_workspace_id": self.adapter.create_workspace(tenant_id, name),
+ "status": "active",
+ "created_at": now_iso(),
+ }
+ self.store.put("workspaces", workspace["id"], workspace)
+ self.audit.record("knowledge.workspace_created", tenant_id=tenant_id, user_id=user_id, resource_type="workspace", resource_id=workspace["id"])
+ return workspace
+
+ def upload_document(
+ self,
+ tenant_id: str,
+ user_id: str,
+ workspace_id: str,
+ *,
+ filename: str,
+ size_bytes: int,
+ content_type: str,
+ content_preview: str = "",
+ ) -> dict:
+ workspace = self.store.get("workspaces", workspace_id)
+ if not workspace:
+ raise NotFoundError("workspace not found")
+ if workspace["tenant_id"] != tenant_id:
+ raise ForbiddenError("workspace does not belong to tenant")
+ if content_type not in ALLOWED_CONTENT_TYPES:
+ raise ValidationError("unsupported content type")
+ if size_bytes <= 0:
+ raise ValidationError("document size must be positive")
+ account = self.quota.get_account(tenant_id)
+ plan = PLAN_CATALOG[account["plan_code"]]
+ max_bytes = plan.max_single_file_mb * 1024 * 1024
+ if size_bytes > max_bytes:
+ raise QuotaError("single file upload limit exceeded", details={"max_mb": plan.max_single_file_mb})
+ size_mb = max(1, ceil(size_bytes / 1024 / 1024))
+ document_id = new_id("doc")
+ digest = hashlib.sha256(f"{tenant_id}:{workspace_id}:{filename}:{size_bytes}:{content_preview}".encode("utf-8")).hexdigest()
+ document = {
+ "id": document_id,
+ "tenant_id": tenant_id,
+ "workspace_id": workspace_id,
+ "uploaded_by": user_id,
+ "filename": filename,
+ "size_bytes": size_bytes,
+ "size_mb": size_mb,
+ "content_type": content_type,
+ "content_hash": digest,
+ "storage_path": f"{tenant_id}/{workspace_id}/{document_id}/{filename}",
+ "status": "validated",
+ "created_at": now_iso(),
+ }
+ self.quota.consume_storage(tenant_id, size_mb, document_id=document_id)
+ ingest_result = self.adapter.ingest_document(workspace["external_workspace_id"], document)
+ document.update(ingest_result)
+ self.store.put("documents", document_id, document)
+ self.audit.record("knowledge.document_uploaded", tenant_id=tenant_id, user_id=user_id, resource_type="document", resource_id=document_id)
+ return document
diff --git a/src/aurask/orchestrator.py b/src/aurask/orchestrator.py
new file mode 100644
index 0000000..5f73cf5
--- /dev/null
+++ b/src/aurask/orchestrator.py
@@ -0,0 +1,190 @@
+"""Template-first workflow orchestration and TBU settlement."""
+
+from __future__ import annotations
+
+from aurask.audit import AuditService
+from aurask.errors import ForbiddenError, NotFoundError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.plans import PLAN_CATALOG
+from aurask.quota import QuotaService
+from aurask.repository import JsonStore
+
+
+DEFAULT_TEMPLATES: list[dict] = [
+ {
+ "id": "tpl_customer_support",
+ "name": "Customer Support Digital Employee",
+ "category": "support",
+ "description": "Answers customer questions from an approved knowledge base.",
+ "default_estimated_tbu": 80,
+ "requires_workspace": True,
+ "allow_python": False,
+ "allow_custom_components": False,
+ "allowed_http_domains": [],
+ "status": "active",
+ },
+ {
+ "id": "tpl_knowledge_qa",
+ "name": "Knowledge Base Q&A",
+ "category": "rag",
+ "description": "Runs a RAG answer against a tenant-bound AnythingLLM workspace.",
+ "default_estimated_tbu": 60,
+ "requires_workspace": True,
+ "allow_python": False,
+ "allow_custom_components": False,
+ "allowed_http_domains": [],
+ "status": "active",
+ },
+ {
+ "id": "tpl_email_assistant",
+ "name": "Email Draft Assistant",
+ "category": "productivity",
+ "description": "Drafts English email responses from structured inputs.",
+ "default_estimated_tbu": 50,
+ "requires_workspace": False,
+ "allow_python": False,
+ "allow_custom_components": False,
+ "allowed_http_domains": [],
+ "status": "active",
+ },
+ {
+ "id": "tpl_spreadsheet_processor",
+ "name": "Spreadsheet Processing Assistant",
+ "category": "data",
+ "description": "Summarizes CSV-like tabular data without arbitrary code execution.",
+ "default_estimated_tbu": 70,
+ "requires_workspace": False,
+ "allow_python": False,
+ "allow_custom_components": False,
+ "allowed_http_domains": [],
+ "status": "active",
+ },
+]
+
+
+class LangflowRuntimeAdapter:
+ """MVP Langflow facade.
+
+ It only executes approved templates and returns deterministic output. A real
+ Langflow Runtime Pool can replace this adapter without changing gateway,
+ quota, or audit boundaries.
+ """
+
+ def execute(self, template: dict, inputs: dict, *, estimated_tbu: int) -> dict:
+ if inputs.get("simulate_failure"):
+ raise RuntimeError("simulated runtime failure")
+ actual_tbu = max(1, int(estimated_tbu * 0.9))
+ return {
+ "status": "completed",
+ "actual_tbu": actual_tbu,
+ "output": {
+ "template_id": template["id"],
+ "summary": f"Executed approved template: {template['name']}",
+ "redacted_input_keys": sorted(inputs.keys()),
+ },
+ }
+
+
+class WorkflowOrchestrator:
+ def __init__(self, store: JsonStore, quota: QuotaService, audit: AuditService, runtime: LangflowRuntimeAdapter | None = None) -> None:
+ self.store = store
+ self.quota = quota
+ self.audit = audit
+ self.runtime = runtime or LangflowRuntimeAdapter()
+
+ def seed_templates(self) -> None:
+ for template in DEFAULT_TEMPLATES:
+ self.store.put("workflow_templates", template["id"], template)
+
+ def list_templates(self) -> list[dict]:
+ return [template for template in self.store.list("workflow_templates") if template["status"] == "active"]
+
+ def run_template(
+ self,
+ tenant_id: str,
+ user_id: str,
+ template_id: str,
+ *,
+ workspace_id: str | None = None,
+ inputs: dict | None = None,
+ ) -> dict:
+ inputs = inputs or {}
+ template = self.store.get("workflow_templates", template_id)
+ if not template or template["status"] != "active":
+ raise NotFoundError("workflow template not found")
+ self._validate_template_is_safe(template)
+ if template["requires_workspace"]:
+ self._require_workspace(tenant_id, workspace_id)
+ account = self.quota.get_account(tenant_id)
+ plan = PLAN_CATALOG[account["plan_code"]]
+ if account["active_workflow_runs"] >= plan.max_concurrent_runs:
+ raise ValidationError("workflow concurrency limit exceeded")
+ estimated_tbu = self._estimate_tbu(template, inputs)
+ run = {
+ "id": new_id("run"),
+ "tenant_id": tenant_id,
+ "user_id": user_id,
+ "template_id": template_id,
+ "workspace_id": workspace_id,
+ "status": "reserved",
+ "estimated_tbu": estimated_tbu,
+ "created_at": now_iso(),
+ }
+ self.store.put("workflow_runs", run["id"], run)
+ reservation = self.quota.reserve_tbu(tenant_id, estimated_tbu, workflow_run_id=run["id"])
+ account["active_workflow_runs"] += 1
+ self.store.put("quota_accounts", tenant_id, account)
+ try:
+ result = self.runtime.execute(template, inputs, estimated_tbu=estimated_tbu)
+ self.quota.settle_reservation(reservation["id"], result["actual_tbu"])
+ run.update(
+ {
+ "status": "completed",
+ "actual_tbu": result["actual_tbu"],
+ "output": result["output"],
+ "completed_at": now_iso(),
+ }
+ )
+ self._record_usage(run, result["actual_tbu"])
+ except Exception as exc:
+ provider_billed_tbu = int(inputs.get("provider_billed_tbu", 0) or 0)
+ self.quota.refund_reservation(reservation["id"], provider_billed_tbu=provider_billed_tbu)
+ run.update({"status": "failed", "error_type": exc.__class__.__name__, "error_message": str(exc), "completed_at": now_iso()})
+ finally:
+ final_account = self.quota.get_account(tenant_id)
+ final_account["active_workflow_runs"] = max(0, final_account["active_workflow_runs"] - 1)
+ self.store.put("quota_accounts", tenant_id, final_account)
+ self.store.put("workflow_runs", run["id"], run)
+ self.audit.record("workflow.run_finished", tenant_id=tenant_id, user_id=user_id, resource_type="workflow_run", resource_id=run["id"], metadata={"status": run["status"]})
+ return run
+
+ def _validate_template_is_safe(self, template: dict) -> None:
+ if template.get("allow_python") or template.get("allow_custom_components"):
+ raise ForbiddenError("unsafe template capabilities are not allowed for shared runtime")
+
+ def _require_workspace(self, tenant_id: str, workspace_id: str | None) -> dict:
+ if not workspace_id:
+ raise ValidationError("workspace_id is required for this template")
+ workspace = self.store.get("workspaces", workspace_id)
+ if not workspace:
+ raise NotFoundError("workspace not found")
+ if workspace["tenant_id"] != tenant_id:
+ raise ForbiddenError("workspace does not belong to tenant")
+ return workspace
+
+ def _estimate_tbu(self, template: dict, inputs: dict) -> int:
+ input_size = sum(len(str(value)) for value in inputs.values() if not isinstance(value, bool))
+ return max(template["default_estimated_tbu"], template["default_estimated_tbu"] + input_size // 600)
+
+ def _record_usage(self, run: dict, actual_tbu: int) -> dict:
+ usage = {
+ "id": new_id("usage"),
+ "tenant_id": run["tenant_id"],
+ "user_id": run["user_id"],
+ "workflow_run_id": run["id"],
+ "template_id": run["template_id"],
+ "tbu": actual_tbu,
+ "provider_units": round(actual_tbu / 1.25, 4),
+ "created_at": now_iso(),
+ }
+ return self.store.put("usage_records", usage["id"], usage)
diff --git a/src/aurask/payments.py b/src/aurask/payments.py
new file mode 100644
index 0000000..cc7905b
--- /dev/null
+++ b/src/aurask/payments.py
@@ -0,0 +1,61 @@
+"""USDT-TRC20 payment order matching."""
+
+from __future__ import annotations
+
+from aurask.audit import AuditService
+from aurask.billing import BillingService
+from aurask.errors import ConflictError, NotFoundError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.repository import JsonStore
+
+
+class PaymentService:
+ def __init__(self, store: JsonStore, billing: BillingService, audit: AuditService) -> None:
+ self.store = store
+ self.billing = billing
+ self.audit = audit
+
+ def match_trc20_payment(
+ self,
+ tenant_id: str,
+ order_id: str,
+ *,
+ tx_hash: str,
+ amount_usdt: float,
+ confirmations: int = 20,
+ ) -> dict:
+ order = self.store.get("orders", order_id)
+ if not order:
+ raise NotFoundError("order not found")
+ if order["tenant_id"] != tenant_id:
+ raise ValidationError("order does not belong to tenant")
+ if not tx_hash:
+ raise ValidationError("transaction hash is required")
+ for payment in self.store.list("payments"):
+ if payment["tx_hash"] == tx_hash:
+ raise ConflictError("transaction hash has already been used")
+ payment_status = "matched" if amount_usdt >= order["amount_usdt"] and confirmations >= 20 else "manual_review"
+ payment = {
+ "id": new_id("pay"),
+ "tenant_id": tenant_id,
+ "order_id": order_id,
+ "chain": "TRC20",
+ "currency": "USDT",
+ "tx_hash": tx_hash,
+ "amount_usdt": amount_usdt,
+ "confirmations": confirmations,
+ "status": payment_status,
+ "notes": "TRC20 fees are paid through the TRON Bandwidth/Energy/TRX resource model.",
+ "created_at": now_iso(),
+ }
+ self.store.put("payments", payment["id"], payment)
+ if payment_status == "matched":
+ order["status"] = "paid"
+ order["paid_at"] = now_iso()
+ self.store.put("orders", order_id, order)
+ self.billing.fulfill_order(order_id)
+ else:
+ order["status"] = "manual_review"
+ self.store.put("orders", order_id, order)
+ self.audit.record("payment.matched", tenant_id=tenant_id, resource_type="payment", resource_id=payment["id"], metadata={"status": payment_status})
+ return payment
diff --git a/src/aurask/plans.py b/src/aurask/plans.py
new file mode 100644
index 0000000..1f1c3ad
--- /dev/null
+++ b/src/aurask/plans.py
@@ -0,0 +1,117 @@
+"""Plan and product catalog matching the technical operations plan."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, asdict
+
+
+USDT_CNY_RATE = 6.86
+
+
+@dataclass(frozen=True)
+class PlanDefinition:
+ code: str
+ name: str
+ price_usdt: float
+ workflow_slots: int
+ knowledge_bases: int
+ storage_mb: int
+ monthly_tbu: int
+ max_single_file_mb: int
+ max_concurrent_runs: int
+ run_timeout_seconds: int
+ trial_days: int = 0
+ template_only: bool = True
+
+ def to_dict(self) -> dict:
+ return asdict(self)
+
+
+PLAN_CATALOG: dict[str, PlanDefinition] = {
+ "free_trial": PlanDefinition(
+ code="free_trial",
+ name="Free Trial",
+ price_usdt=0,
+ workflow_slots=1,
+ knowledge_bases=1,
+ storage_mb=512,
+ monthly_tbu=0,
+ max_single_file_mb=10,
+ max_concurrent_runs=1,
+ run_timeout_seconds=60,
+ trial_days=7,
+ template_only=True,
+ ),
+ "basic_monthly": PlanDefinition(
+ code="basic_monthly",
+ name="Basic",
+ price_usdt=20,
+ workflow_slots=3,
+ knowledge_bases=3,
+ storage_mb=1024,
+ monthly_tbu=900,
+ max_single_file_mb=50,
+ max_concurrent_runs=2,
+ run_timeout_seconds=180,
+ template_only=True,
+ ),
+ "dedicated_space": PlanDefinition(
+ code="dedicated_space",
+ name="Dedicated Space",
+ price_usdt=99,
+ workflow_slots=10,
+ knowledge_bases=10,
+ storage_mb=10_240,
+ monthly_tbu=3000,
+ max_single_file_mb=200,
+ max_concurrent_runs=5,
+ run_timeout_seconds=600,
+ template_only=False,
+ ),
+}
+
+
+PRODUCT_CATALOG: dict[str, dict] = {
+ "basic_monthly": {
+ "code": "basic_monthly",
+ "kind": "subscription",
+ "plan_code": "basic_monthly",
+ "price_usdt": 20,
+ "description": "Basic plan: 3 workflows, 3 knowledge bases, 1GB storage, 900 TBU/month",
+ },
+ "workflow_slot_monthly": {
+ "code": "workflow_slot_monthly",
+ "kind": "workflow_slot",
+ "price_usdt": 20,
+ "description": "+1 workflow slot, +1 knowledge base, +1GB storage, no extra TBU",
+ },
+ "tbu_1000": {
+ "code": "tbu_1000",
+ "kind": "tbu_package",
+ "tbu": 1000,
+ "bonus_tbu": 100,
+ "price_usdt": round(1000 * 0.15 / USDT_CNY_RATE, 2),
+ "description": "1000 TBU package with 100 bonus TBU",
+ },
+ "tbu_2000": {
+ "code": "tbu_2000",
+ "kind": "tbu_package",
+ "tbu": 2000,
+ "bonus_tbu": 250,
+ "price_usdt": round(2000 * 0.15 / USDT_CNY_RATE, 2),
+ "description": "2000 TBU package with 250 bonus TBU",
+ },
+ "tbu_5000": {
+ "code": "tbu_5000",
+ "kind": "tbu_package",
+ "tbu": 5000,
+ "bonus_tbu": 750,
+ "price_usdt": round(5000 * 0.15 / USDT_CNY_RATE, 2),
+ "description": "5000 TBU package with positive gross margin",
+ },
+}
+
+
+def seed_plan_catalog(store) -> None:
+ for plan in PLAN_CATALOG.values():
+ store.put("plans", plan.code, plan.to_dict())
diff --git a/src/aurask/quota.py b/src/aurask/quota.py
new file mode 100644
index 0000000..2bd990b
--- /dev/null
+++ b/src/aurask/quota.py
@@ -0,0 +1,170 @@
+"""Quota account and TBU ledger management."""
+
+from __future__ import annotations
+
+from aurask.audit import AuditService
+from aurask.errors import NotFoundError, QuotaError, ValidationError
+from aurask.ids import new_id, now_iso
+from aurask.repository import JsonStore
+
+
+class QuotaService:
+ def __init__(self, store: JsonStore, audit: AuditService) -> None:
+ self.store = store
+ self.audit = audit
+
+ def get_account(self, tenant_id: str) -> dict:
+ account = self.store.get("quota_accounts", tenant_id)
+ if not account:
+ raise NotFoundError("quota account not found")
+ return account
+
+ def ensure_account(self, tenant_id: str) -> dict:
+ account = self.store.get("quota_accounts", tenant_id)
+ if account:
+ return account
+ account = {
+ "tenant_id": tenant_id,
+ "plan_code": "free_trial",
+ "available_tbu": 0,
+ "reserved_tbu": 0,
+ "workflow_slots": 0,
+ "knowledge_bases": 0,
+ "storage_mb": 0,
+ "used_storage_mb": 0,
+ "active_workflow_runs": 0,
+ "updated_at": now_iso(),
+ }
+ self.store.put("quota_accounts", tenant_id, account)
+ return account
+
+ def apply_plan(self, tenant_id: str, plan: dict) -> dict:
+ account = self.ensure_account(tenant_id)
+ account["plan_code"] = plan["code"]
+ account["available_tbu"] += plan["monthly_tbu"]
+ account["workflow_slots"] = max(account["workflow_slots"], plan["workflow_slots"])
+ account["knowledge_bases"] = max(account["knowledge_bases"], plan["knowledge_bases"])
+ account["storage_mb"] = max(account["storage_mb"], plan["storage_mb"])
+ account["updated_at"] = now_iso()
+ self.store.put("quota_accounts", tenant_id, account)
+ self._ledger(tenant_id, "credit", plan["monthly_tbu"], "plan_activation", plan["code"])
+ self.audit.record("quota.plan_applied", tenant_id=tenant_id, resource_type="plan", resource_id=plan["code"])
+ return account
+
+ def add_workflow_slot(self, tenant_id: str, quantity: int = 1) -> dict:
+ if quantity < 1:
+ raise ValidationError("quantity must be positive")
+ account = self.ensure_account(tenant_id)
+ account["workflow_slots"] += quantity
+ account["knowledge_bases"] += quantity
+ account["storage_mb"] += 1024 * quantity
+ account["updated_at"] = now_iso()
+ self.store.put("quota_accounts", tenant_id, account)
+ self.audit.record("quota.workflow_slot_added", tenant_id=tenant_id, metadata={"quantity": quantity})
+ return account
+
+ def add_tbu(self, tenant_id: str, amount: int, *, reason: str, source_id: str) -> dict:
+ if amount <= 0:
+ raise ValidationError("TBU amount must be positive")
+ account = self.ensure_account(tenant_id)
+ account["available_tbu"] += amount
+ account["updated_at"] = now_iso()
+ self.store.put("quota_accounts", tenant_id, account)
+ self._ledger(tenant_id, "credit", amount, reason, source_id)
+ return account
+
+ def reserve_tbu(self, tenant_id: str, amount: int, *, workflow_run_id: str) -> dict:
+ if amount <= 0:
+ raise ValidationError("estimated TBU must be positive")
+ account = self.get_account(tenant_id)
+ if account["available_tbu"] < amount:
+ raise QuotaError(
+ "insufficient TBU balance",
+ details={"available_tbu": account["available_tbu"], "required_tbu": amount},
+ )
+ account["available_tbu"] -= amount
+ account["reserved_tbu"] += amount
+ account["updated_at"] = now_iso()
+ reservation = {
+ "id": new_id("rsv"),
+ "tenant_id": tenant_id,
+ "workflow_run_id": workflow_run_id,
+ "reserved_tbu": amount,
+ "status": "reserved",
+ "created_at": now_iso(),
+ }
+ self.store.put("quota_accounts", tenant_id, account)
+ self.store.put("reservations", reservation["id"], reservation)
+ self._ledger(tenant_id, "reserve", amount, "workflow_estimate", workflow_run_id, reservation_id=reservation["id"])
+ return reservation
+
+ def settle_reservation(self, reservation_id: str, actual_tbu: int) -> dict:
+ reservation = self.store.get("reservations", reservation_id)
+ if not reservation:
+ raise NotFoundError("reservation not found")
+ if reservation["status"] != "reserved":
+ raise ValidationError("reservation is already settled")
+ if actual_tbu < 0:
+ raise ValidationError("actual TBU cannot be negative")
+ tenant_id = reservation["tenant_id"]
+ account = self.get_account(tenant_id)
+ reserved = reservation["reserved_tbu"]
+ charge = min(actual_tbu, reserved)
+ release = reserved - charge
+ account["reserved_tbu"] -= reserved
+ account["available_tbu"] += release
+ account["updated_at"] = now_iso()
+ reservation["status"] = "settled"
+ reservation["actual_tbu"] = charge
+ reservation["released_tbu"] = release
+ reservation["settled_at"] = now_iso()
+ self.store.put("quota_accounts", tenant_id, account)
+ self.store.put("reservations", reservation_id, reservation)
+ self._ledger(tenant_id, "debit", charge, "workflow_actual", reservation["workflow_run_id"], reservation_id=reservation_id)
+ if release:
+ self._ledger(tenant_id, "release", release, "workflow_unused", reservation["workflow_run_id"], reservation_id=reservation_id)
+ return reservation
+
+ def refund_reservation(self, reservation_id: str, *, provider_billed_tbu: int = 0) -> dict:
+ reservation = self.store.get("reservations", reservation_id)
+ if not reservation:
+ raise NotFoundError("reservation not found")
+ if provider_billed_tbu:
+ return self.settle_reservation(reservation_id, provider_billed_tbu)
+ return self.settle_reservation(reservation_id, 0)
+
+ def consume_storage(self, tenant_id: str, size_mb: int, *, document_id: str) -> dict:
+ account = self.get_account(tenant_id)
+ if account["used_storage_mb"] + size_mb > account["storage_mb"]:
+ raise QuotaError(
+ "storage quota exceeded",
+ details={"storage_mb": account["storage_mb"], "used_storage_mb": account["used_storage_mb"], "required_mb": size_mb},
+ )
+ account["used_storage_mb"] += size_mb
+ account["updated_at"] = now_iso()
+ self.store.put("quota_accounts", tenant_id, account)
+ self._ledger(tenant_id, "storage", size_mb, "document_upload_mb", document_id)
+ return account
+
+ def _ledger(
+ self,
+ tenant_id: str,
+ entry_type: str,
+ amount: int,
+ reason: str,
+ source_id: str,
+ *,
+ reservation_id: str | None = None,
+ ) -> dict:
+ entry = {
+ "id": new_id("ledger"),
+ "tenant_id": tenant_id,
+ "entry_type": entry_type,
+ "amount": amount,
+ "unit": "TBU" if entry_type != "storage" else "MB",
+ "reason": reason,
+ "source_id": source_id,
+ "reservation_id": reservation_id,
+ "created_at": now_iso(),
+ }
+ return self.store.put("quota_ledger", entry["id"], entry)
diff --git a/src/aurask/repository.py b/src/aurask/repository.py
new file mode 100644
index 0000000..ef8f816
--- /dev/null
+++ b/src/aurask/repository.py
@@ -0,0 +1,94 @@
+"""JSON persistence for the first Aurask implementation.
+
+This is intentionally not presented as a production database. It gives the MVP
+an auditable local ledger while preserving tenant-aware domain boundaries that
+can be moved to PostgreSQL later.
+"""
+
+from __future__ import annotations
+
+import json
+import time
+from copy import deepcopy
+from pathlib import Path
+from threading import RLock
+from typing import Any
+from uuid import uuid4
+
+
+DEFAULT_DATA: dict[str, Any] = {
+ "tenants": {},
+ "users": {},
+ "api_keys": {},
+ "plans": {},
+ "subscriptions": {},
+ "quota_accounts": {},
+ "quota_ledger": {},
+ "reservations": {},
+ "workflow_templates": {},
+ "workflow_runs": {},
+ "workspaces": {},
+ "documents": {},
+ "orders": {},
+ "payments": {},
+ "usage_records": {},
+ "audit_events": {},
+}
+
+
+class JsonStore:
+ def __init__(self, path: str | Path | None = None) -> None:
+ self.path = Path(path) if path else None
+ self._lock = RLock()
+ self.data = deepcopy(DEFAULT_DATA)
+ if self.path:
+ self.path.parent.mkdir(parents=True, exist_ok=True)
+ if self.path.exists():
+ with self.path.open("r", encoding="utf-8") as file:
+ loaded = json.load(file)
+ self.data.update(loaded)
+ else:
+ self.save()
+
+ def save(self) -> None:
+ if not self.path:
+ return
+ with self._lock:
+ tmp_path = self.path.with_name(f"{self.path.name}.{uuid4().hex}.tmp")
+ with tmp_path.open("w", encoding="utf-8") as file:
+ json.dump(self.data, file, ensure_ascii=False, indent=2, sort_keys=True)
+ last_error: PermissionError | None = None
+ for _ in range(10):
+ try:
+ tmp_path.replace(self.path)
+ return
+ except PermissionError as exc:
+ last_error = exc
+ time.sleep(0.05)
+ if tmp_path.exists():
+ tmp_path.unlink(missing_ok=True)
+ if last_error:
+ raise last_error
+
+ def collection(self, name: str) -> dict[str, Any]:
+ if name not in self.data:
+ self.data[name] = {}
+ return self.data[name]
+
+ def put(self, collection: str, entity_id: str, entity: dict[str, Any]) -> dict[str, Any]:
+ with self._lock:
+ self.collection(collection)[entity_id] = entity
+ self.save()
+ return entity
+
+ def get(self, collection: str, entity_id: str) -> dict[str, Any] | None:
+ value = self.collection(collection).get(entity_id)
+ return value
+
+ def list(self, collection: str) -> list[dict[str, Any]]:
+ return list(self.collection(collection).values())
+
+ def delete_all(self) -> None:
+ with self._lock:
+ self.data = deepcopy(DEFAULT_DATA)
+ self.save()
diff --git a/tests/test_mvp.py b/tests/test_mvp.py
new file mode 100644
index 0000000..48cc3ea
--- /dev/null
+++ b/tests/test_mvp.py
@@ -0,0 +1,83 @@
+from __future__ import annotations
+
+import unittest
+
+from aurask.app import create_app
+from aurask.errors import QuotaError
+
+
+class AuraskMVPTests(unittest.TestCase):
+ def setUp(self) -> None:
+ self.app = create_app(None, reset=True)
+
+ def test_demo_bootstrap_creates_basic_plan_and_workspace(self) -> None:
+ demo = self.app.bootstrap_demo()
+ quota = self.app.quota.get_account(demo["tenant"]["id"])
+ self.assertEqual(quota["plan_code"], "basic_monthly")
+ self.assertEqual(quota["available_tbu"], 900)
+ self.assertEqual(quota["workflow_slots"], 3)
+ self.assertEqual(quota["knowledge_bases"], 3)
+ self.assertEqual(demo["workspace"]["tenant_id"], demo["tenant"]["id"])
+
+ def test_workflow_run_reserves_and_settles_tbu(self) -> None:
+ demo = self.app.bootstrap_demo()
+ run = self.app.orchestrator.run_template(
+ demo["tenant"]["id"],
+ demo["user"]["id"],
+ "tpl_knowledge_qa",
+ workspace_id=demo["workspace"]["id"],
+ inputs={"question": "Summarize onboarding steps"},
+ )
+ quota = self.app.quota.get_account(demo["tenant"]["id"])
+ self.assertEqual(run["status"], "completed")
+ self.assertGreater(run["actual_tbu"], 0)
+ self.assertEqual(quota["reserved_tbu"], 0)
+ self.assertLess(quota["available_tbu"], 900)
+
+ def test_tbu_package_payment_fulfills_order(self) -> None:
+ demo = self.app.bootstrap_demo()
+ order = self.app.billing.create_order(demo["tenant"]["id"], demo["user"]["id"], "tbu_1000")
+ payment = self.app.payments.match_trc20_payment(
+ demo["tenant"]["id"],
+ order["id"],
+ tx_hash="trx_hash_123",
+ amount_usdt=order["amount_usdt"],
+ confirmations=20,
+ )
+ quota = self.app.quota.get_account(demo["tenant"]["id"])
+ self.assertEqual(payment["status"], "matched")
+ self.assertEqual(self.app.store.get("orders", order["id"])["status"], "fulfilled")
+ self.assertEqual(quota["available_tbu"], 900 + 1100)
+
+ def test_document_upload_updates_storage(self) -> None:
+ demo = self.app.bootstrap_demo()
+ document = self.app.knowledge.upload_document(
+ demo["tenant"]["id"],
+ demo["user"]["id"],
+ demo["workspace"]["id"],
+ filename="manual.pdf",
+ size_bytes=1024 * 1024 * 2,
+ content_type="application/pdf",
+ content_preview="Aurask support guide",
+ )
+ quota = self.app.quota.get_account(demo["tenant"]["id"])
+ self.assertEqual(document["tenant_id"], demo["tenant"]["id"])
+ self.assertEqual(quota["used_storage_mb"], 2)
+
+ def test_insufficient_tbu_blocks_execution(self) -> None:
+ demo = self.app.bootstrap_demo()
+ quota = self.app.quota.get_account(demo["tenant"]["id"])
+ quota["available_tbu"] = 10
+ self.app.store.put("quota_accounts", demo["tenant"]["id"], quota)
+ with self.assertRaises(QuotaError):
+ self.app.orchestrator.run_template(
+ demo["tenant"]["id"],
+ demo["user"]["id"],
+ "tpl_customer_support",
+ workspace_id=demo["workspace"]["id"],
+ inputs={"question": "Very long request"},
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()