لینک دعوت هم کلیکپذیر نبود و هم کار نمیکرد؛ چهار نقص همزمان:
- `invite_url` نسبی بود (`/accept-invite?token=…`) و host نداشت → حالا از
`settings.app_url` آدرس مطلق ساخته میشود.
- صفحهٔ `/accept-invite` در اپ Next وجود نداشت → ۴۰۴.
- هیچ اندپوینت پذیرش دعوت در بکاند نبود؛ `InviteRepository.mark_accepted`
نوشته شده بود ولی هرگز صدا زده نمیشد، پس توکن مصرف نمیشد.
- در تنظیمات، لینک بهصورت متن ساده در `<p>` رندر میشد.
بکاند:
- روتر `app/interfaces/http/routers/invites.py`:
`GET /api/invites/{token}` (پیشنمایش) و `POST /api/invites/accept`.
عمداً زیر `/organizations/{org_id}` نیست: دعوتشونده هنوز عضو نیست و
`get_org_context` ردش میکند، پس خود توکن مجوز است. accept ایدمپوتنت است.
- `AuditAction.MEMBER_JOIN = "member.join"`.
- رفع باگ نهفته: `Invite.is_expired()` بین `expires_at` خواندهشده از SQLite
(naive) و `utcnow()` (aware) خطای مقایسه میداد. طبق قرارداد موجود پروژه
(`_aware` در `adapters/agy.py`)، در `repositories/identity.py` تابع `_aware`
اضافه و روی `expires_at`/`accepted_at` اعمال شد.
فرانت:
- صفحهٔ `app/accept-invite/page.tsx` با پیشنمایش سازمان/نقش و پذیرش؛ پس از
پذیرش `setActiveOrganization` میکند تا در همان سازمان لند کند.
- قراردادهای `InvitePreview`/`AcceptInviteResult` و کلاینت
`previewInvite`/`acceptInvite`.
- `login`/`signup` پارامتر `?next=` را میفهمند (فقط same-origin) تا توکن دعوت
در مسیر احراز هویت گم نشود.
- `settings`: لینک به `<a>` واقعی + دکمهٔ «کپی لینک».
تست: `tests/test_invites.py` (۶) و `src/test/accept-invite.test.tsx` (۵).
بکاند ۲۵۲ سبز + ruff/mypy تمیز؛ فرانت ۶۱ سبز + tsc/eslint تمیز.
Smart Organization — پلتفرم نیروی کار هوش مصنوعی
نسخهٔ وب مستقل و چندسازمانی محصول. این پروژه کاملاً جدا از ربات تلگرام است و هیچ فایلی از ریشهٔ مخزن را در runtime خود import نمیکند.
- پلن محصول و معماری:
PLAN.md - چکلیست اجرایی کامل:
TODO.md - یادداشت زیرساخت فعلی (فقط مرجع):
docs/current-infra-notes.md
۱. محصول چیست
یک وب اپلیکیشن چندسازمانی برای «نیروی کار هوش مصنوعی سازمانی».
کاربر نهایی اینها را میبیند: سازمان، مدیر هوش مصنوعی، کارمندهای هوش مصنوعی، پروژه، وظیفه، فایل/خروجی، مصرف و پرداخت.
کاربر نهایی اینها را نمیبیند: نام مدل، provider، base_url، API key، قیمت خام
provider. اینها فقط در Admin Console و فقط برای مالک سامانه قابل مدیریتاند.
پلتفرم سه لایهٔ نقش دارد:
- مالک سامانه (platform owner) — اکانتهای agy، مدلها، routing policy، تعرفهها، sandbox templateها، مشاهدهٔ کل مصرف. agy تنها ارائهدهندهٔ مدل سامانه است و بخش جداگانهٔ «ارائهدهندهها» وجود ندارد.
- مدیر سازمان مشتری — ساخت سازمان، دعوت عضو، پروژه، فعالسازی کارمند، مصرف.
- عضو سازمان — گفتگو با مدیر/کارمند، ساخت task، آپلود فایل، دیدن خروجی.
۲. پیشنیازها
| ابزار | نسخه |
|---|---|
| Node.js | >= 20 |
| pnpm | 9.x |
| Python | 3.13 |
| Docker + Compose | برای postgres/redis |
فعالسازی pnpm در صورت نبود:
corepack enable pnpm
corepack prepare pnpm@9.12.0 --activate
۳. راهاندازی سریع
# ۱) نصب وابستگیهای 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 دو حساب میسازد و هر دو عضو سازمان نمونه هستند، پس بعد از ورود
بلافاصله یک داشبورد پر میبینید:
| نقش | ایمیل | رمز |
|---|---|---|
| کاربر نمونه | demo@example.com |
demo12345 |
| مالک سامانه (admin console) | owner@example.com |
owner12345 |
اینها رمزهای توسعه هستند و در
app/seed.pyسختکد شدهاند. هیچوقت در production از اینها استفاده نکنید وJWT_SECRET/ENCRYPTION_KEYواقعی بگذارید (نگهبان config در حالت production مقادیر placeholder را رد میکند).نکته: دامنهٔ ایمیل عمداً
example.comاست. اگر.localیا.testبگذارید،email-validatorآن را رد میکند و حساب ساخته میشود ولی ورود به آن ناممکن است.tests/test_seed.pyهمین را تضمین میکند.
اجرای کل استک با Docker
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 واقعی اجرا شود.
۵.۴ جریان درخواست
Browser → Web (Next.js/BFF) → API (FastAPI)
├── Domain Modules
├── Event Bus / Outbox → Workers
├── AI Runtime → Model Providers
├── Sandbox Gateway → E2B / Daytona
└── Billing Processor → Stripe
۶. ساختار پوشه
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 ساخته میشود و همان اول فعال است. برای شروع:
- با حساب مالک سامانه وارد کنسول مدیریت شوید و از
/admin/agy-accounts→ «افزودن اکانت agy» شروع کنید: لینک احراز هویت گوگل باز میشود، بعد از تأیید، URL بازگشت را از نوار آدرس مرورگر کپی و Paste کنید (صفحهٔ مقصد عمداً بارگذاری نمیشود؛ پارامترها همانجا هستند). - در
/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 بدهید:
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.
۱۰. اصول تغییرناپذیر
- اجرای کد کاربر یا agent روی host اصلی ممنوع — فقط از مسیر Sandbox Gateway.
- هر endpoint چندسازمانی باید
organization_idرا enforce کند. - هیچ secret، token یا API key در frontend یا لاگ چاپ نمیشود.
- هر عملیات destructive باید confirmation و audit log داشته باشد.
- مدل و provider از دید کاربر عادی پنهان است.
- هر مصرفی در 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_idorganization_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 برمیگرداند. کاربر میتواند فقط همین یک رشته را گزارش کند
تا دقیقاً یک رکورد پیدا شود.
# ۵۰۰ بگیر، سپس با همان شناسه رکورد را پیدا کن
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/security-checklist.md— چکلیست امنیتیdocs/current-infra-notes.md— زیرساخت فعلی (مرجع)