نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
+340
View File
@@ -0,0 +1,340 @@
# نمای کلی معماری — نسخه وب مستقل AGY
> این سند تصویر فنی پروژهٔ `web/` است: چه چیزی کجاست، چرا این‌طور چیده شده، و
> مرزهای اعتماد کدام‌اند. برای چک‌لیست امنیتی به
> [`../security-checklist.md`](../security-checklist.md) مراجعه کنید.
---
## ۱. هدف و قابلیت‌های محصول
یک **SaaS چندمستأجری مستقل** به‌نام «AI Workforce Platform». مشتری وارد می‌شود،
سازمان می‌سازد، عضو دعوت می‌کند، پروژه تعریف می‌کند و به کارمندان هوش مصنوعی کار
می‌سپارد. مشتری هرگز نمی‌بیند که پشت صحنه چه مدلی، چه providerی یا چه کلیدی است.
**آنچه مشتری می‌بیند:** سازمان، اعضا، پروژه، مخزن Git، دامنهٔ preview، محیط
اجرای ایزوله، کارمند AI، گفتگو با مدیر، task و run، مصرف، صورتحساب، audit log.
**آنچه فقط مالک سامانه می‌بیند:** provider، مدل، سیاست مسیریابی، rate card،
کلیدهای رمزنگاری‌شده، همهٔ سازمان‌ها.
**مستقل از تلگرام:** نسخهٔ وب هیچ import یا وابستگی runtime به `bot.py`,
`web_panel.py`, `caddy_manager.py`, `git_manager.py` یا `gitea_api.py` ندارد. منطق
Git و domain routing فقط به‌عنوان مرجع خوانده و در `web/` به‌صورت adapter مستقل
بازنویسی شده است.
---
## ۲. استک
| لایه | فناوری | چرا |
| --- | --- | --- |
| UI | Next.js 15.1.6 (App Router) + React 19 + TypeScript | RSC، مسیریابی فایل‌محور، یک پکیج مشترک برای کل محصول |
| استایل | Tailwind CSS v3 | RTL و dark-first بدون فایل CSS پراکنده |
| State/Data | کلاینت `fetch` نازک + `localStorage` برای توکن | بدون وابستگی اضافه؛ TanStack Query لازم نشد |
| API | FastAPI + Pydantic v2 | قرارداد تایپ‌دار، OpenAPI خودکار |
| ORM | SQLAlchemy 2.x (async) | هم‌راستا با مدل async FastAPI |
| Migration | Alembic | تنها مسیر تغییر schema در production |
| DB | PostgreSQL (توسعه: SQLite حافظه‌ای برای تست) | تست‌ها بدون سرویس بیرونی اجرا می‌شوند |
| Cache/Queue | Redis | آماده برای workerهای background |
| امنیت | argon2 (رمز)، PyJWT (نشست)، cryptography/Fernet (رازها) | انتخاب‌های استاندارد و مقاوم |
| تست | pytest + pytest-asyncio (backend)، Vitest + Testing Library (frontend) | — |
---
## ۳. چیدمان مخزن
```
web/
├── apps/
│ ├── api/ ← بک‌اند FastAPI (Clean/Hexagonal)
│ └── web/ ← فرانت‌اند Next.js
├── packages/
│ ├── contracts/ ← تایپ‌های مشترک request/response
│ └── ui/ ← کامپوننت‌های پایه (RTL، dark-first)
├── services/ ← workerهای مستقل (ai-runtime, billing-worker, sandbox-gateway)
├── infra/docker/ ← docker-compose برای postgres/redis
├── docs/ ← همین اسناد
├── PLAN.md ← سند مرجع محصول و معماری
└── TODO.md ← فهرست اجرای مرحله‌به‌مرحله
```
---
## ۴. معماری بک‌اند — Clean / Hexagonal
وابستگی‌ها **همیشه به سمت داخل** است. لایهٔ domain به هیچ فریمورک، ORM، Redis یا
HTTP وابسته نیست (این با تست `test_domain_layer_has_no_framework_dependencies`
تضمین شده است).
```
┌───────────────────────────────────────────────────────────────────┐
│ interfaces/http routers + deps + error handlers │
│ فقط route، schema ورودی/خروجی و تبدیل خطا │
├───────────────────────────────────────────────────────────────────┤
│ application services (use case) + ports (driven iface) │
│ sandbox_service, workforce_service, project_service, ... │
├───────────────────────────────────────────────────────────────────┤
│ domain قواعد خالص، بدون I/O │
│ identity, projects, workforce, sandbox, billing, audit, │
│ knowledge, ai_control, platform/rules │
├───────────────────────────────────────────────────────────────────┤
│ infrastructure adapters + repositories + db/models │
│ mock, gitea, caddy, openai_compatible, sandbox_remote, stripe │
└───────────────────────────────────────────────────────────────────┘
↑ همهٔ پیکان‌ها به سمت domain
```
### ۴.۱ لایهٔ domain (بدون I/O)
هر bounded context یک پکیج است با `models.py` و قواعد مربوط به خود:
| Context | مسئولیت |
| --- | --- |
| `identity` | کاربر، سازمان، عضویت، نقش، دعوت |
| `projects` | پروژه، وضعیت مخزن، دامنهٔ preview |
| `workforce` | قالب کارمند، کارمند، thread، message، task، run، artifact، مجوز ابزار (`tools.py`) |
| `sandbox` | workspace، session، snapshot، `NetworkPolicy`، `SandboxResourceLimit` |
| `billing` | پلن، rate card، usage ledger |
| `audit` | audit event، trace، span، error، quality metric |
| `knowledge` | منبع دانش، chunk |
| `ai_control` | provider، مدل، سیاست مسیریابی |
| `platform/rules.py` | **قواعد امنیتی سکو:** اعتبارسنجی برچسب دامنه، ضدتزریق host، ضدSSRF، پورت‌های رزرو/محافظت‌شده |
`platform/rules.py` عمداً در domain است، نه در infrastructure: این قواعد باید
بدون DB و بدون شبکه قابل تست باشند، چون بیشترین بار امنیتی را دارند.
### ۴.۲ لایهٔ application
**سرویس‌ها (use case):**
- `project_service` — ساخت پروژه، اطمینان از مخزن، دامنهٔ preview
- `workforce_service` — کارمند، prompt، مجوز ابزار، thread، task، اجرای run
- `sandbox_service` — workspace، session، اجرای فرمان، clamp منابع، ثبت مصرف
- `billing_service` — خلاصهٔ مصرف، صورتحساب
- `knowledge_service` — منابع دانش و شمارش chunk
- `audit_service` — audit log، trace
**پورت‌ها (interfaceهای driven):** در `app/application/ports/`
`ModelProviderPort`, `PaymentProviderPort`, `SandboxProviderPort`,
`GitProviderPort`, `DomainRouterPort`, `StoragePort`, `VectorStorePort`,
`NotificationPort`, `UsageLedgerPort`, `AuditPort`.
هر پورت حداقل یک **adapter mock** دارد، بنابراین کل استک بدون هیچ credential
بیرونی بالا می‌آید.
### ۴.۳ لایهٔ infrastructure
**انتخاب adapter — یک نقطهٔ تصمیم:** `infrastructure/adapters/registry.py`
تنها جایی است که تصمیم می‌گیرد کدام adapter پشت هر پورت بنشیند، فقط بر اساس
سوئیچ‌های env:
| env | مقادیر مجاز | adapter واقعی |
| --- | --- | --- |
| `AI_DEFAULT_PROVIDER` | `mock` \| `openai_compatible` | `openai_compatible.py` |
| `GIT_PROVIDER` | `mock` \| `gitea` | `gitea.py` |
| `DOMAIN_ROUTER` | `mock` \| `caddy` | `caddy.py` |
| `SANDBOX_PROVIDER` | `mock` \| `daytona` \| `e2b` | `sandbox_remote.py` |
| `PAYMENT_PROVIDER` | `mock` \| `stripe` | `stripe_payments.py` |
مقدار ناشناخته به `ProviderUnavailableError` منجر می‌شود — **fail-closed**، نه
fallback بی‌صدا به mock.
**Repositoryها:** `infrastructure/repositories/` — `identity`, `projects`,
`workforce`, `sandbox`, `billing`, `knowledge`, `ai_control`, `audit`.
هر متد لیست، `limit`/`offset` می‌گیرد و سقف `min(limit, 500)` را اعمال می‌کند.
**مدل‌های DB:** `infrastructure/db/models.py` — ۴۸ جدول با indexهای ترکیبی روی
`(organization_id, created_at)` برای مسیرهای داغ داشبورد.
### ۴.۴ لایهٔ interfaces
`interfaces/http/` شامل `routers/` (۹ روتر: `health`, `auth`, `organizations`,
`projects`, `workforce`, `billing`, `knowledge`, `sandbox`, `admin`)،
`deps.py` (auth، org context، capability) و `errors.py` (تبدیل استثناها به
پاسخ‌های یکدست). `main.py` تنها composition root است: روترها را به هم می‌بندد و
CORS را تنظیم می‌کند.
---
## ۵. معماری فرانت‌اند
Next.js App Router با دو ناحیهٔ مسیر:
**ناحیهٔ عمومی (بدون احراز هویت):** `/login`, `/signup`, `/onboarding`
**ناحیهٔ اپلیکیشن (`(app)` — نیازمند نشست):**
`/dashboard`, `/workforce`, `/workforce/[id]`, `/manager`, `/tasks`,
`/tasks/[id]`, `/projects`, `/projects/[id]`, `/knowledge`, `/billing`,
`/settings`, `/traces`
**ناحیهٔ مالک سامانه:** `/admin`, `/admin/providers`, `/admin/models`,
`/admin/routing`, `/admin/rate-cards`, `/admin/organizations`, `/admin/errors`
**دو نقطهٔ اتصال مرکزی:**
- `src/lib/api.ts` — تنها جایی که base URL، bearer token و پوشش خطا را می‌شناسد.
base URL از `NEXT_PUBLIC_API_URL` می‌آید (یک URL عمومی، نه راز).
- `src/lib/session.tsx` — Context نشست: کاربر، سازمان‌ها، سازمان فعال، ورود/خروج.
**پکیج مشترک UI:** `packages/ui/src/index.tsx` کامپوننت‌های پایه را دارد —
`Button`, `Card`, `Input`, `Badge`, `Modal`, `Drawer`, `Table` (با حالت
empty/loading/error داخلی), `Tabs`, `ToastStack`, `ArtifactViewer`. RTL و
dark-first به‌صورت پیش‌فرض.
---
## ۶. مسیر یک درخواست (مثال: اجرای یک task)
```
مشتری: دکمهٔ «اجرا» در /tasks/[id]
│
├─ POST /api/organizations/{org}/tasks/{task}/runs (Bearer token)
│
├─ deps.get_current_user → توکن را decode می‌کند، کاربر را می‌خواند (۴۰۱ اگر نامعتبر)
├─ deps.get_org_context → عضویت فعال را می‌خواند (۴۰۴ اگر نباشد — نه ۴۰۳)
├─ deps.require_capability(...) → نقش کافی است؟ (۴۰۳ اگر نه)
│
├─ workforce_service.start_run(...)
│ ├─ انتخاب سیاست مسیریابی بر اساس mode (بدون افشای مدل به مشتری)
│ ├─ اجرای گام‌ها → در صورت نیاز، sandbox_service.run_command(...)
│ │ ├─ SandboxProviderPort (mock یا remote)
│ │ ├─ clamp timeout/output از policy (نه از caller)
│ │ └─ ثبت usage در UsageLedger
│ └─ ثبت گام‌های timeline (kind="output" — بدون افشای «model»)
│
└─ پاسخ: run با timeline، وضعیت، و مصرف ثبت‌شده
```
---
## ۷. مرزهای اعتماد
1. **مرز مستأجر:** هر endpoint چندسازمانی `organization_id` را enforce می‌کند.
نبود عضویت → **۴۰۴**. هیچ مسیری برای دیدن دادهٔ سازمان دیگر وجود ندارد.
2. **مرز مشتری/سکو:** provider، مدل و کلید هرگز از API به مشتری نمی‌رسند. مشتری
فقط یک *نام* سیاست مسیریابی می‌بیند.
3. **مرز اجرای کد:** هیچ مسیری برای اجرا روی host وجود ندارد. همهٔ کد کاربر فقط
از `SandboxProviderPort` عبور می‌کند، با egress پیش‌فرض بسته (`DENY_ALL`).
4. **مرز راز:** رازها در حالت سکون با Fernet رمزنگاری می‌شوند؛ در پاسخ‌ها فقط
نسخهٔ masked برمی‌گردد. frontend هیچ رازی ندارد.
---
## ۸. جریان داده و ثبت مصرف
**usage ledger منبع حقیقت صورتحساب است** — providerهای پرداخت فقط پول جابه‌جا
می‌کنند و در محاسبه دخالتی ندارند. هر مصرف (model call، زمان sandbox،
storage/artifact، tool call) یک رکورد در ledger می‌گذارد با `organization_id`,
`project_id`, `user_id`, `employee_id`, `run_id`, `unit`, `quantity`, `unit_price`.
هر عملیات حساس یک `AuditEvent` هم می‌نویسد (ساخت سازمان، دعوت عضو، ساخت repo،
ساخت دامنه، اجرای فرمان، حذف).
---
## ۹. تست
**بک‌اند (`apps/api/tests/`)** — ۲۱۶ تست:
| فایل | چه چیزی را می‌سنجد |
| --- | --- |
| `test_auth.py` | ثبت‌نام، ورود، نشست، نشت‌نداشتن hash |
| `test_tenant_isolation.py` | جداسازی مستأجر، ۴۰۴ به‌جای ۴۰۳، دعوت |
| `test_admin_control_plane.py` | دسترسی مالک سامانه، mask شدن کلید |
| `test_platform_rules.py` | برچسب دامنه، ضدتزریق host، ضدSSRF، پورت‌ها |
| `test_project_repo.py` | مخزن خصوصی، نشت‌نداشتن توکن |
| `test_workforce.py` | کارمند، مجوز ابزار (deny-by-default)، run، نشت‌نداشتن مدل |
| `test_config_guard.py` | اعتبارسنجی رازها در production |
| `test_pagination.py` | سقف صفحه‌بندی، رد offset نامعتبر |
| `test_domain_isolation.py` | نبود وابستگی فریمورک در domain |
| `test_e2e_smoke.py` | کل مسیر محصول + نامرئی‌بودن برای مستأجر دیگر |
| `test_workers.py` | اجرای task بدون کلیک، idempotency صورتحساب، reaping سندباکس |
| `test_seed.py` | قابل‌ورود بودن حساب‌های نمونه، idempotency seed |
| `test_redact.py` | پاک‌سازی secret در ۴۲ حالت (ساختاری، الگویی، مقداری) |
| `test_error_log.py` | مسیر کامل خطا: middleware → handler → recorder → دیتابیس |
| `test_error_console.py` | دسترسی مالک سامانه، گروه‌بندی، بستن/بازگشایی، audit |
**فرانت‌اند (`apps/web/src/test/`)** — ۲۵ تست: قرارداد API، کامپوننت‌های پایه،
design system، و همبستگی `request_id` در کلاینت.
---
## ۹.۱ خط لولهٔ خطا
یک استثنا از محل وقوع تا کنسول چهار مرحله را طی می‌کند:
```
route raises
│
├─ RequestContextMiddleware → request_id + method/path/ip/user_agent روی request.state
├─ deps.get_current_user → user_id (پس از تأیید توکن)
├─ deps.get_org_context → organization_id (پس از تأیید عضویت)
│
├─ exception handler → انتخاب اینکه ثبت شود یا نه + ساخت پاسخ استاندارد
│ (errors.py) (۵xx همیشه، ۴xx فقط با ERROR_LOG_CLIENT_ERRORS)
│
├─ error_log.record_error → redact → truncate → fingerprint
│ (infrastructure) سشن مستقل خودش؛ هرگز استثنا پرتاب نمی‌کند
│
└─ ObservabilityRepository → INSERT در error_events
```
سه نکته که طراحی را تعیین کرده‌اند:
1. **recorder سشن خودش را باز می‌کند.** سشن درخواست در لحظهٔ فراخوانی معمولاً
غیرقابل‌استفاده است: یک `IntegrityError` آن را در وضعیت
`PendingRollbackError` رها می‌کند. اگر از همان سشن استفاده می‌شد، سخت‌ترین
خطاها برای ثبت — خطاهای دیتابیس — هرگز ثبت نمی‌شدند.
2. **recorder هرگز پرتاب نمی‌کند.** یک خطای ثبتی نباید یک ۵۰۰ را به hang یا به
یک استثنای مدیریت‌نشده تبدیل کند.
3. **`X-Request-ID` در خود هندلر ست می‌شود، نه فقط در middleware.**
`ServerErrorMiddleware` استارلت بیرون از همهٔ middlewareهای کاربر است، پس
middleware هرگز پاسخی که برای یک استثنای مدیریت‌نشده ساخته می‌شود را
نمی‌بیند. این باگ واقعاً رخ داد و با
`test_error_responses_carry_the_header_for_every_handler` پوشش داده شد.
---
## ۱۰. اجرای پروژه
```bash
# ۱. زیرساخت
docker compose -f infra/docker/docker-compose.yml up -d postgres redis
# ۲. بک‌اند
cd apps/api
python3 -m venv .venv && .venv/bin/python -m pip install -e '.[dev]'
cp ../../.env.example ../../.env # مقادیر mock کافی است
.venv/bin/python -m alembic upgrade head # schema
.venv/bin/python -m app.seed # دادهٔ نمونه (provider/مدل/rate card/قالب)
# ۳. فرانت‌اند
cd ../.. && corepack pnpm install
corepack pnpm dev # web روی ۳۰۰۰، api روی ۸۰۰۰
```
**دستورهای کیفیت:**
```bash
pnpm lint # ESLint (web) + ruff (api)
pnpm typecheck # tsc --noEmit (web) + mypy app (api)
pnpm test # vitest (web) + pytest (api)
pnpm build # next build
```
---
## ۱۱. تصمیم‌های معماری کلیدی
| تصمیم | دلیل |
| --- | --- |
| Modular monolith، نه microservice از روز اول | مرزهای domain از ابتدا روشن‌اند؛ شکستن به سرویس بعداً ارزان است، اما پیچیدگی توزیع از روز اول گران است |
| یک adapter mock برای هر پورت | کل محصول بدون credential قابل اجرا و تست است؛ §۲۴ TODO را برآورده می‌کند |
| ۴۰۴ به‌جای ۴۰۳ برای نبود عضویت | `403` وجود منبع را تأیید می‌کند و اجازهٔ شمارش می‌دهد |
| قواعد امنیتی در domain، نه infrastructure | بدون DB/شبکه قابل تست‌اند و از لایهٔ I/O جدا می‌مانند |
| clamp منابع دولایه (service + adapter) | defense in depth؛ خطای پیکربندی policy سقف مطلق را نمی‌شکند |
| usage ledger به‌عنوان منبع حقیقت | محاسبهٔ صورتحساب از provider پرداخت جدا می‌شود |
| انتخاب adapter فقط با env، fail-closed | پیکربندی اشتباه باید بلند شکست بخورد، نه بی‌صدا به mock برگردد |
| `services/` فعلاً اسکلت است | `ai-runtime` و `billing-worker` فعلاً داخل فرآیند API اجرا می‌شوند؛ مرز پورت‌ها آمادهٔ استخراج است |