# 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: - API: - API docs (Swagger): - 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/` | | adapter یا اتصال به سرویس بیرونی | `apps/api/app/infrastructure` | | worker پس‌زمینه بنویسم | `apps/api/app/workers` (پوستهٔ اجرا در `services/`) | | خطا ثبت/پاک‌سازی کنم | `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` | | Workspace گیت | `GIT_WORKSPACE` | `fake` (پیش‌فرض، برای تست) \| `local` (git روی host) | | دامنهٔ تست | `DOMAIN_ROUTER` | `mock` \| `caddy` | | دامنهٔ سفارشی | `ALLOW_CUSTOM_DOMAINS` | `false` (پیش‌فرض) \| `true` | | Sandbox | `SANDBOX_PROVIDER` | `mock` \| `docker` \| `daytona` \| `e2b` | | استقرار روی شبکهٔ خصوصی | `DEPLOY_ALLOW_PRIVATE_HOSTS` | `false` (پیش‌فرض)؛ `true` فقط برای مقصدی در شبکهٔ خصوصی که قابل اعتماد است | | MCP gateway | `MCP_GATEWAY_ENABLED` | `true` (پیش‌فرض) \| `false` | | پرداخت | `PAYMENT_PROVIDER` | `mock` \| `stripe` | اگر credential واقعی نداریم، **قابلیت حذف نمی‌شود**: مدل دیتابیس، interface، UI و adapter mock کامل ساخته می‌شود و adapter واقعی پشت همین flag آماده می‌ماند. با پیش‌فرض‌ها (`mock`، `fake` و `false`) تست‌ها بدون docker، git یا شبکه اجرا می‌شوند. **حالت واقعی (real mode):** وقتی سامانه باید روی یک host واقعی با docker و caddy اجرا شود، این envها را تنظیم کنید: ```bash SANDBOX_PROVIDER=docker GIT_WORKSPACE=local DOMAIN_ROUTER=caddy GIT_PROVIDER=gitea ``` نیازمندی‌های host در این حالت: - `docker` یا `podman` در `PATH` (با `SANDBOX_DOCKER_BIN` انتخاب می‌شود). کد کاربر فقط داخل کانتینرهای سخت‌شده اجرا می‌شود: `--read-only`، کاربر غیر root، `--cap-drop ALL`، و محدودیت CPU، حافظه و PID. - `git` نسخهٔ 2.20 یا جدیدتر در `PATH`. git را خود پلتفرم اجرا می‌کند؛ hookها غیرفعال‌اند و اعتبارنامه‌ها هرگز در argv یا URL نمی‌آیند. - `caddy` در `PATH` و این خط در Caddyfile: `import /etc/caddy/agy-preview/*.caddy`. بدون این خط adapter اجرا نمی‌شود، تا یک موفقیت کاذب گزارش نشود. - اجازهٔ بدون رمز برای `caddy reload --config ` به کاربر سرویس (روی systemd با sudoers یا polkit، نه با root). - پوشه‌های قابل‌نوشتن با فضای کافی زیر `SANDBOX_WORKSPACES_ROOT` و `ARTIFACTS_ROOT` (پیش‌فرض `./.data/…`). پاک‌سازی خودکار `runs/`، `artifacts/` و فایل‌های پشتیبان FTP هنوز پیاده نشده است. - هدف استقرار `ftps` (TLS صریح) است. هدف `ftp` ساده پذیرفته می‌شود، ولی بدون رمزنگاری است. - تست‌های یکپارچگی docker و استقرار با `SANDBOX_DOCKER_IT=1` و `DEPLOY_IT=1` فعال می‌شوند و بدون آن‌ها رد (skip) می‌شوند. ### ارائه‌دهندهٔ 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های پس‌زمینه پنج process پس‌زمینه وجود دارد: چهار worker و یک MCP gateway. همه در `apps/api/app/workers/` پیاده شده‌اند (MCP gateway در `apps/api/app/interfaces/mcp/`) و `services/` پوستهٔ اجرای آن‌هاست: | worker | کار | دستور | |---|---|---| | `billing-worker` | جمع‌بندی usage ledger به فاکتور draft (هر دوره یک‌بار، idempotent)، و در همان پاس حذف نرم درس‌های حافظه‌ای منقضی‌شده (به‌جز موارد pin شده) | `pnpm worker:billing` | | `ai-runtime` | برداشتن taskهای در صف و اجرای آن‌ها از مسیر run engine، و یک `Scheduler.tick()` در هر پاس (فاصلهٔ پاس‌ها با `ENGINE_TICK_SECONDS`) | `pnpm worker:ai-runtime` | | `sandbox-gateway` | متوقف‌کردن sessionهای بی‌استفاده، حذف containerهای یتیم با label `smart-org.*`، و ثبت audit | `pnpm worker:sandbox-gateway` | | `delivery` | اجرای pipeline runهای در صف (از checkout تا route) و بررسی ریموت‌ها برای pushهای بیرونی هر `DELIVERY_POLL_SECONDS` | `pnpm worker:delivery` | **MCP gateway** یک سرویس مداوم است که با `pnpm mcp:dev` اجرا می‌شود. عامل‌های بیرونی AI با توکن‌های محدود به یک سازمان به آن وصل می‌شوند. جزئیات در `services/mcp-gateway/README.md`. هر دستور worker **یک پاس** اجرا می‌کند و خارج می‌شود (شکل مناسب cron). برای اجرای مستمر `--loop` بدهید: ```bash cd apps/api && .venv/bin/python -m app.workers billing --loop --interval 3600 ``` **اصل مهم:** workerها هیچ منطق تجاری ندارند. `ai-runtime`، `sandbox-gateway` و `delivery` سرویس‌های خود را از همان builderهای `app/application/container*.py` می‌سازند که HTTP API می‌سازد، پس آن‌ها و API هرگز نمی‌توانند دربارهٔ اینکه کدام adapter پشت یک port است اختلاف داشته باشند. `billing-worker` مستقیم از repositoryها استفاده می‌کند. جزئیات هر کدام در `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) — زیرساخت فعلی (مرجع)