- users.is_platform_owner → users.platform_role (owner|manager|user) با مهاجرت d4e5f6a7b8c9 و backfill. ایندکس یکتای جزئی uq_single_platform_owner تضمین میکند دقیقاً یک مالک وجود داشته باشد — در سطح دیتابیس، نه فقط در کد. - ثبتنام عمومی حذف شد (POST /auth/signup). حسابها فقط با POST /admin/users (فقط مالک، با رمز موقت یکبارمصرف) یا با پذیرش لینک دعوت ساخته میشوند. در مسیر دعوت، اگر حساب وجود نداشته باشد خودِ دعوتشده رمز تعیین میکند؛ اگر وجود داشته باشد لینک هرگز جای reset رمز را نمیگیرد. - ساخت سازمان فقط برای مالک و مدیران؛ کاربر عادی با دعوت عضو میشود. - تنزل یا حذف مدیر، سازمانهایش را به مالک سامانه منتقل میکند (مالکیت + عضویت، با audit). حساب مالک از API تغییرناپذیر است؛ جابهجایی صندلی فقط با POST /admin/transfer-ownership و در یک تراکنش انجام میشود. - افزودن POST /auth/change-password برای تبدیل رمز موقت به رمز خصوصی. - فرانت: صفحهٔ /admin/users، صفحهٔ /account، بازنویسی /accept-invite، حذف /signup، گارد /onboarding برای کاربر عادی، و نشان نقش پلتفرم در پوستهٔ اپ. - PRAGMA foreign_keys=ON برای SQLite تا ON DELETE CASCADE در dev و prod یکسان رفتار کند (پیشتر حذف کاربر، عضویتهایش را یتیم میگذاشت). - تست: ۳۰۰ تست بکاند (شامل tests/test_platform_hierarchy.py با ۲۲ تست) و ۶۴ تست فرانتاند؛ ruff، mypy، tsc، eslint و build تمیز.
372 lines
18 KiB
Markdown
372 lines
18 KiB
Markdown
# Smart Organization — پلتفرم نیروی کار هوش مصنوعی
|
||
|
||
نسخهٔ وب مستقل و چندسازمانی محصول. این پروژه **کاملاً جدا از ربات تلگرام** است و
|
||
هیچ فایلی از ریشهٔ مخزن را در runtime خود import نمیکند.
|
||
|
||
- پلن محصول و معماری: [`PLAN.md`](./PLAN.md)
|
||
- چکلیست اجرایی کامل: [`TODO.md`](./TODO.md)
|
||
- یادداشت زیرساخت فعلی (فقط مرجع): [`docs/current-infra-notes.md`](./docs/current-infra-notes.md)
|
||
|
||
---
|
||
|
||
## ۱. محصول چیست
|
||
|
||
یک وب اپلیکیشن چندسازمانی برای «نیروی کار هوش مصنوعی سازمانی».
|
||
|
||
**کاربر نهایی اینها را میبیند:** سازمان، مدیر هوش مصنوعی، کارمندهای هوش مصنوعی،
|
||
پروژه، وظیفه، فایل/خروجی، مصرف و پرداخت.
|
||
|
||
**کاربر نهایی اینها را نمیبیند:** نام مدل، provider، `base_url`، API key، قیمت خام
|
||
provider. اینها فقط در **Admin Console** و فقط برای مالک سامانه قابل مدیریتاند.
|
||
|
||
پلتفرم سه لایهٔ نقش دارد:
|
||
|
||
1. **مالک سامانه (platform owner)** — **دقیقاً یک حساب** و صاحب کل سیستم. اکانتهای
|
||
agy، مدلها، routing policy، تعرفهها، sandbox templateها، مشاهدهٔ کل مصرف، و
|
||
ساخت/تنزل **مدیران**. agy تنها ارائهدهندهٔ مدل سامانه است و بخش جداگانهٔ
|
||
«ارائهدهندهها» وجود ندارد.
|
||
2. **مدیر (platform manager)** — حسابی که فقط مالک سامانه میسازد. هر مدیر
|
||
میتواند **سازمانهای خودش را بسازد** (و مالک آنها شود) یا با **دعوت** صاحب
|
||
سازمان، در سازمان مدیران دیگر عضو شود. مدیر به کنسول مالک سامانه دسترسی ندارد.
|
||
3. **کاربر عادی (platform user)** — سازمان نمیسازد؛ فقط داخل سازمانهایی که با
|
||
دعوت به آنها پیوسته کار میکند.
|
||
|
||
**ثبتنام عمومی وجود ندارد.** حسابها از دو مسیر ساخته میشوند:
|
||
|
||
- **ساخت مستقیم توسط مالک سامانه** در `/admin/users` (برای مدیر یا کاربر عادی) که
|
||
یک رمز موقت یکبارمصرف برمیگرداند؛
|
||
- **پذیرش لینک دعوت**: دعوتشده اگر حساب نداشته باشد، در همان صفحه رمز خودش را
|
||
تعیین میکند و حساب ساخته میشود. اگر حساب داشته باشد باید اول وارد شود — یک
|
||
لینک دعوت هرگز جای reset رمز را نمیگیرد.
|
||
|
||
«فقط یک مالک» یک قاعدهٔ دیتابیسی است: ایندکس یکتای جزئی روی
|
||
`users.platform_role = 'owner'`، پس حتی یک درج مستقیم هم نمیتواند مالک دوم بسازد.
|
||
جابهجایی صندلی مالکیت فقط با `POST /admin/transfer-ownership` انجام میشود که در
|
||
یک تراکنش، مالک فعلی را به مدیر تنزل میدهد و جانشین را مالک میکند.
|
||
|
||
---
|
||
|
||
## ۲. پیشنیازها
|
||
|
||
| ابزار | نسخه |
|
||
|---|---|
|
||
| Node.js | >= 20 |
|
||
| pnpm | 9.x |
|
||
| Python | 3.13 |
|
||
| Docker + Compose | برای postgres/redis |
|
||
|
||
فعالسازی pnpm در صورت نبود:
|
||
|
||
```bash
|
||
corepack enable pnpm
|
||
corepack prepare pnpm@9.12.0 --activate
|
||
```
|
||
|
||
---
|
||
|
||
## ۳. راهاندازی سریع
|
||
|
||
```bash
|
||
# ۱) نصب وابستگیهای JS
|
||
pnpm install
|
||
|
||
# ۲) ساخت venv پایتون برای backend
|
||
pnpm api:venv
|
||
|
||
# ۳) کپی فایل env و پر کردن مقادیر (پیشفرضها همه mock هستند)
|
||
cp .env.example .env
|
||
|
||
# ۴) بالا آوردن postgres و redis
|
||
docker compose -f infra/docker/docker-compose.yml up -d postgres redis
|
||
|
||
# ۵) اجرای migration و seed
|
||
pnpm db:migrate
|
||
pnpm db:seed
|
||
|
||
# ۶) اجرای همزمان frontend و backend
|
||
pnpm dev
|
||
```
|
||
|
||
آدرسها پس از اجرا:
|
||
|
||
- Frontend: <http://localhost:3000>
|
||
- API: <http://localhost:8000>
|
||
- API docs (Swagger): <http://localhost:8000/docs>
|
||
- Health: <http://localhost:8000/health>
|
||
|
||
### حسابهای نمونه (فقط محیط توسعه)
|
||
|
||
`pnpm db:seed` دو حساب میسازد:
|
||
|
||
| نقش | ایمیل | رمز | چه میبیند |
|
||
|---|---|---|---|
|
||
| مالک سامانه (admin console) | `owner@example.com` | `owner12345` | کنسول مالک سامانه، بدون هیچ سازمانی |
|
||
| مدیر نمونه | `demo@example.com` | `demo12345` | سازمان نمونه که خودش مالک آن است |
|
||
|
||
> مالک سامانه **عضو هیچ سازمانی نمیشود** — او مستأجرها را از کنسول میبیند، نه از
|
||
> داخل دادهشان. برای دیدن داشبورد سازمانی با حساب مدیر نمونه وارد شوید.
|
||
>
|
||
> اینها رمزهای **توسعه** هستند و در `app/seed.py` سختکد شدهاند. هیچوقت در
|
||
> production از اینها استفاده نکنید و `JWT_SECRET` / `ENCRYPTION_KEY` واقعی
|
||
> بگذارید (نگهبان config در حالت production مقادیر placeholder را رد میکند).
|
||
>
|
||
> نکته: دامنهٔ ایمیل عمداً `example.com` است. اگر `.local` یا `.test` بگذارید،
|
||
> `email-validator` آن را رد میکند و حساب ساخته میشود ولی **ورود به آن ناممکن
|
||
> است**. `tests/test_seed.py` همین را تضمین میکند.
|
||
|
||
### اجرای کل استک با Docker
|
||
|
||
```bash
|
||
docker compose -f infra/docker/docker-compose.yml up --build
|
||
```
|
||
|
||
---
|
||
|
||
## ۴. اسکریپتها
|
||
|
||
| دستور | کار |
|
||
|---|---|
|
||
| `pnpm dev` | اجرای همزمان Next.js و FastAPI |
|
||
| `pnpm build` | build فرانتاند |
|
||
| `pnpm lint` | ESLint (web) + ruff (api) |
|
||
| `pnpm typecheck` | tsc (web) + mypy (api) |
|
||
| `pnpm test` | vitest (web) + pytest (api) |
|
||
| `pnpm format` | Prettier (web) + ruff format (api) |
|
||
| `pnpm db:migrate` | `alembic upgrade head` |
|
||
| `pnpm db:seed` | دادههای نمونه (platform owner، کاربر نمونه، سازمان، templateها) |
|
||
| `pnpm api:venv` | ساخت venv پایتون |
|
||
| `pnpm worker:billing` | یک پاس worker صورتحساب |
|
||
| `pnpm worker:ai-runtime` | یک پاس worker اجرای taskها |
|
||
| `pnpm worker:sandbox-gateway` | یک پاس پاکسازی sessionهای بیاستفاده |
|
||
|
||
---
|
||
|
||
## ۵. معماری
|
||
|
||
### ۵.۱ سبک معماری
|
||
|
||
- **Clean Architecture** + **Hexagonal / Ports & Adapters**
|
||
- **DDD** برای مرزبندی bounded contextها
|
||
- **Event-Driven** برای billing، audit و observability (الگوی Outbox)
|
||
- **Modular Monolith** در فاز اول — microservice زودهنگام ممنوع
|
||
|
||
قاعدهٔ سخت: لایهٔ `domain` هیچ dependency به FastAPI، SQLAlchemy، Redis، HTTP یا
|
||
provider خارجی ندارد. فقط `application` و `infrastructure` به آن وابستهاند، نه برعکس.
|
||
|
||
### ۵.۲ Bounded Contextها
|
||
|
||
`identity` · `billing` · `ai_control` · `workforce` · `sandbox` · `knowledge` ·
|
||
`audit` · `projects` · `platform`
|
||
|
||
### ۵.۳ Ports الزامی
|
||
|
||
`ModelProviderPort` · `PaymentProviderPort` · `SandboxProviderPort` ·
|
||
`GitProviderPort` · `DomainRouterPort` · `StoragePort` · `VectorStorePort` ·
|
||
`NotificationPort` · `AuditPort` · `UsageLedgerPort`
|
||
|
||
هر port حداقل یک adapter **mock** دارد تا کل استک بدون هیچ credential واقعی اجرا شود.
|
||
|
||
### ۵.۴ جریان درخواست
|
||
|
||
```text
|
||
Browser → Web (Next.js/BFF) → API (FastAPI)
|
||
├── Domain Modules
|
||
├── Event Bus / Outbox → Workers
|
||
├── AI Runtime → Model Providers
|
||
├── Sandbox Gateway → E2B / Daytona
|
||
└── Billing Processor → Stripe
|
||
```
|
||
|
||
---
|
||
|
||
## ۶. ساختار پوشه
|
||
|
||
```text
|
||
web/
|
||
PLAN.md پلن محصول و معماری
|
||
TODO.md چکلیست اجرایی
|
||
README.md همین فایل
|
||
package.json اسکریپتهای ریشه
|
||
pnpm-workspace.yaml تعریف workspace
|
||
.env.example نمونهٔ env (هیچ secret واقعی ندارد)
|
||
apps/
|
||
web/ Next.js + TypeScript (UI)
|
||
api/ FastAPI + SQLAlchemy (backend)
|
||
services/
|
||
ai-runtime/ worker اجرای Agent
|
||
sandbox-gateway/ wrapper ایزولهسازی اجرا
|
||
billing-worker/ پردازش usage و invoice
|
||
packages/
|
||
contracts/ تایپها و قراردادهای مشترک
|
||
ui/ componentهای پایه (design system)
|
||
infra/
|
||
docker/ docker-compose
|
||
migrations/ محل migrationها
|
||
docs/
|
||
architecture/ مستندات معماری
|
||
product/ مستندات محصول
|
||
current-infra-notes.md
|
||
```
|
||
|
||
---
|
||
|
||
## ۷. کجا کد بنویسیم
|
||
|
||
| میخواهم... | محل |
|
||
|---|---|
|
||
| صفحه یا component UI بسازم | `apps/web/src/app` یا `apps/web/src/features` |
|
||
| component پایه/design system | `packages/ui` |
|
||
| endpoint جدید بسازم | `apps/api/app/interfaces/http` |
|
||
| use case بنویسم | `apps/api/app/application` |
|
||
| منطق دامنه بنویسم | `apps/api/app/domain/<context>` |
|
||
| adapter یا اتصال به سرویس بیرونی | `apps/api/app/infrastructure` |
|
||
| worker پسزمینه بنویسم | `apps/api/app/workers` (پوستهٔ اجرا در `services/<name>`) |
|
||
| خطا ثبت/پاکسازی کنم | `apps/api/app/core/redact.py` + `apps/api/app/infrastructure/error_log.py` |
|
||
| قرارداد مشترک front/back | `packages/contracts` |
|
||
|
||
**ممنوع:** نوشتن هر کد جدید در فایلهای ریشهٔ مخزن (`bot.py`, `web_panel.py`,
|
||
`bot_menus.py`, `bot_messages.py`) و هر وابستگی به `telegram`, `chat_id`,
|
||
`callback_data`, `InlineKeyboardMarkup`.
|
||
|
||
---
|
||
|
||
## ۸. سرویسهای خارجی و حالت mock
|
||
|
||
همهٔ سرویسهای بیرونی پیشفرض روی `mock` هستند. برای فعالسازی واقعی، این envها را
|
||
پر کنید:
|
||
|
||
| قابلیت | env | مقادیر |
|
||
|---|---|---|
|
||
| Git | `GIT_PROVIDER` | `mock` \| `gitea` |
|
||
| دامنهٔ تست | `DOMAIN_ROUTER` | `mock` \| `caddy` |
|
||
| Sandbox | `SANDBOX_PROVIDER` | `mock` \| `daytona` \| `e2b` |
|
||
| پرداخت | `PAYMENT_PROVIDER` | `mock` \| `stripe` |
|
||
|
||
اگر credential واقعی نداریم، **قابلیت حذف نمیشود**: مدل دیتابیس، interface، UI و
|
||
adapter mock کامل ساخته میشود و adapter واقعی پشت همین flag آماده میماند.
|
||
|
||
### ارائهدهندهٔ agy (Google Antigravity) — تنها ارائهدهندهٔ مدل
|
||
|
||
agy کلید API ندارد؛ **اکانت**ها هویت Google هستند و از طریق OAuth وارد
|
||
میشوند. کاتالوگ مدلها در seed ساخته میشود و همان اول فعال است. برای شروع:
|
||
|
||
1. با حساب مالک سامانه وارد کنسول مدیریت شوید و از
|
||
`/admin/agy-accounts` → «افزودن اکانت agy» شروع کنید: لینک احراز هویت گوگل
|
||
باز میشود، بعد از تأیید، URL بازگشت را از نوار آدرس مرورگر کپی و Paste
|
||
کنید (صفحهٔ مقصد عمداً بارگذاری نمیشود؛ پارامترها همانجا هستند).
|
||
2. در `/admin/models` زنجیرهٔ افزودن مدل کاملاً انتخابی است: ارائهدهنده
|
||
(agy) → اکانت → مدل (کاتالوگ لحظهای و زندهٔ همان اکانت) → افورت استدلال
|
||
(فقط برای مدلهای استدلالی). هیچ نام مدلی دستی تایپ نمیشود و مشتری نام مدل
|
||
را هرگز نمیبیند.
|
||
|
||
هر تعداد اکانت Google که بخواهید اضافه کنید. پول چرخشی، بار را بین اکانتها
|
||
پخش میکند و اکانتی که ۴۲۹ گرفته تا پایان پنجرهٔ سهمیهٔ آن کنار گذاشته
|
||
میشود (هم در حافظه و هم در دیتابیس، تا workerها همان تصویر را ببینند).
|
||
refresh tokenها فقط encrypted at rest ذخیره میشوند و هیچوقت از API
|
||
برنمیگردند.
|
||
|
||
---
|
||
|
||
## ۹. Workerهای پسزمینه
|
||
|
||
سه worker وجود دارد. همه در `apps/api/app/workers/` پیاده شدهاند و `services/<name>`
|
||
پوستهٔ اجرای آنهاست:
|
||
|
||
| worker | کار | دستور |
|
||
|---|---|---|
|
||
| `billing-worker` | جمعبندی usage ledger به فاکتور draft (هر دوره یکبار، idempotent) | `pnpm worker:billing` |
|
||
| `ai-runtime` | برداشتن taskهای در صف و اجرای آنها از مسیر run engine | `pnpm worker:ai-runtime` |
|
||
| `sandbox-gateway` | متوقفکردن sessionهای بیاستفاده و ثبت audit | `pnpm worker:sandbox-gateway` |
|
||
|
||
هر دستور **یک پاس** اجرا میکند و خارج میشود (شکل مناسب cron). برای اجرای
|
||
مستمر `--loop` بدهید:
|
||
|
||
```bash
|
||
cd apps/api && .venv/bin/python -m app.workers billing --loop --interval 3600
|
||
```
|
||
|
||
**اصل مهم:** workerها هیچ منطق تجاری ندارند. هر سه از
|
||
`app/application/container.py` همان سرویسهایی را میسازند که HTTP API میسازد،
|
||
پس worker و API هرگز نمیتوانند دربارهٔ اینکه کدام adapter پشت یک port است اختلاف
|
||
داشته باشند. جزئیات هر کدام در `services/*/README.md`.
|
||
|
||
---
|
||
|
||
## ۱۰. اصول تغییرناپذیر
|
||
|
||
1. اجرای کد کاربر یا agent روی host اصلی **ممنوع** — فقط از مسیر Sandbox Gateway.
|
||
2. هر endpoint چندسازمانی باید `organization_id` را enforce کند.
|
||
3. هیچ secret، token یا API key در frontend یا لاگ چاپ نمیشود.
|
||
4. هر عملیات destructive باید confirmation و audit log داشته باشد.
|
||
5. مدل و provider از دید کاربر عادی پنهان است.
|
||
6. هر مصرفی در usage ledger داخلی ثبت میشود (Stripe فقط adapter است).
|
||
|
||
---
|
||
|
||
## ۱۱. کنسول خطا
|
||
|
||
هر خطایی که سامانه بگیرد — در درخواست HTTP، در worker پسزمینه، یا در زمان
|
||
راهاندازی — با جزئیات کامل در جدول `error_events` ثبت میشود و در
|
||
`/admin/errors` (فقط مالک سامانه) قابل مشاهده است.
|
||
|
||
**چه چیزی ثبت میشود**
|
||
|
||
| منبع | چه زمانی | شدت |
|
||
| --- | --- | --- |
|
||
| استثنای مدیریتنشده و خطاهای ۵xx | همیشه | `critical` / `error` |
|
||
| `ValueError` بدون نوع مشخص | همیشه | `warning` |
|
||
| سایر پاسخهای ۴xx | فقط با `ERROR_LOG_CLIENT_ERRORS=1` | `warning` |
|
||
|
||
۴xx بهصورت پیشفرض ثبت نمیشود چون یک API عمومی دائماً ۴۰۴ از اسکنرها
|
||
میگیرد و ثبت همهٔ آنها خطاهای واقعی را زیر خود دفن میکند. خطاهای ۵xx
|
||
همیشه ثبت میشوند، مستقل از این تنظیم.
|
||
|
||
**هر رکورد چه چیزی دارد**
|
||
|
||
- `kind`، `error_type`، `message` و `stack` (ردِّ کامل خطا)
|
||
- `method`، `path`، `status_code` و `request_id`
|
||
- `organization_id` و `user_id` (پس از احراز هویت)
|
||
- `fingerprint` — هش پایدار از هویت خطا، برای گروهبندی
|
||
- `metadata_json` — جزئیات اضافی (مثلاً `task_id`)
|
||
|
||
**گروهبندی و رسیدگی**
|
||
|
||
خطاهای یکسان بر اساس `fingerprint` در یک ردیف جمع میشوند، پس یک باگ که ۴۰۰۰
|
||
بار رخ داده یک خط با شمارنده است، نه ۴۰۰۰ خط. `fingerprint` عمداً سازمان و
|
||
کاربر را در نظر نمیگیرد: یک باگ که دو مستأجر را میزند هنوز یک باگ است.
|
||
بستن یک گروه همهٔ رخدادهای باز آن را میبندد و در audit log ثبت میشود.
|
||
|
||
**پاکسازی خودکار secretها**
|
||
|
||
هیچ خطایی بدون عبور از `app/core/redact.py` ذخیره نمیشود. سه لایه:
|
||
کلیدهایی که معنایشان secret است (`password`، `api_key`، …)، الگوهای
|
||
شناختنی (JWT، `Bearer`، `scheme://user:pass@host`، `sk-…`، `ghp_…`)، و
|
||
خودِ مقادیر secret تنظیمشده در `settings`. یک لاگ خطا که رمز عبور ذخیره کند
|
||
از نداشتن لاگ خطا بدتر است.
|
||
|
||
**استفاده از `request_id`**
|
||
|
||
هر پاسخ `X-Request-ID` دارد و هر بدنهٔ خطا هم همان شناسه را در
|
||
`error.request_id` برمیگرداند. کاربر میتواند فقط همین یک رشته را گزارش کند
|
||
تا دقیقاً یک رکورد پیدا شود.
|
||
|
||
```bash
|
||
# ۵۰۰ بگیر، سپس با همان شناسه رکورد را پیدا کن
|
||
curl -i http://localhost:8000/api/whatever -H "X-Request-ID: my-trace"
|
||
curl "http://localhost:8000/api/admin/errors?search=my-trace" -H "Authorization: Bearer $OWNER_TOKEN"
|
||
```
|
||
|
||
**اجرای دستی workerها و ثبت خطا**
|
||
|
||
خطای یک pass کامل با `source=worker` ثبت میشود و exit code غیرصفر میدهد تا
|
||
cron متوجه شود. خطای یک task مشخص نیز با `task_id` و سازمان مربوطه ثبت
|
||
میشود.
|
||
|
||
---
|
||
|
||
## ۱۲. مستندات
|
||
|
||
- [`docs/architecture/overview.md`](./docs/architecture/overview.md) — نمای کلی معماری
|
||
- [`docs/security-checklist.md`](./docs/security-checklist.md) — چکلیست امنیتی
|
||
- [`docs/current-infra-notes.md`](./docs/current-infra-notes.md) — زیرساخت فعلی (مرجع)
|