نام نمایشی و هویت اپلیکیشن در همهٔ سطوح کاربر-پسند از «AGY Web» به «Smart Organization» تغییر کرد: - UI: عنوان تب مرورگر، صفحهٔ ورود، برند و لوگوی پوستهٔ اپ (AG → SO) - بکاند: مقدار پیشفرض app_name، نام و توضیح توزیع پایتون - مانیفستها: package.json ریشه و چهار پکیج، pyproject.toml - مستندات: README، PLAN، TODO و دو سند docs شناسههای داخلی عمداً دستنخورده ماندهاند: scope پکیجهای @agy/*، کلیدهای localStorage، نامهای logger، نام فایل پایگاهٔ داده، و اطلاعات اتصال پایگاه داده در .env.example و infra/docker/docker-compose.yml.
20 KiB
نمای کلی معماری — نسخه وب مستقل Smart Organization
این سند تصویر فنی پروژهٔ
web/است: چه چیزی کجاست، چرا اینطور چیده شده، و مرزهای اعتماد کداماند. برای چکلیست امنیتی به../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— ساخت پروژه، اطمینان از مخزن، دامنهٔ previewworkforce_service— کارمند، prompt، مجوز ابزار، thread، task، اجرای runsandbox_service— workspace، session، اجرای فرمان، clamp منابع، ثبت مصرفbilling_service— خلاصهٔ مصرف، صورتحسابknowledge_service— منابع دانش و شمارش chunkaudit_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، وضعیت، و مصرف ثبتشده
۷. مرزهای اعتماد
- مرز مستأجر: هر endpoint چندسازمانی
organization_idرا enforce میکند. نبود عضویت → ۴۰۴. هیچ مسیری برای دیدن دادهٔ سازمان دیگر وجود ندارد. - مرز مشتری/سکو: provider، مدل و کلید هرگز از API به مشتری نمیرسند. مشتری فقط یک نام سیاست مسیریابی میبیند.
- مرز اجرای کد: هیچ مسیری برای اجرا روی host وجود ندارد. همهٔ کد کاربر فقط
از
SandboxProviderPortعبور میکند، با egress پیشفرض بسته (DENY_ALL). - مرز راز: رازها در حالت سکون با 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
سه نکته که طراحی را تعیین کردهاند:
- recorder سشن خودش را باز میکند. سشن درخواست در لحظهٔ فراخوانی معمولاً
غیرقابلاستفاده است: یک
IntegrityErrorآن را در وضعیتPendingRollbackErrorرها میکند. اگر از همان سشن استفاده میشد، سختترین خطاها برای ثبت — خطاهای دیتابیس — هرگز ثبت نمیشدند. - recorder هرگز پرتاب نمیکند. یک خطای ثبتی نباید یک ۵۰۰ را به hang یا به یک استثنای مدیریتنشده تبدیل کند.
X-Request-IDدر خود هندلر ست میشود، نه فقط در middleware.ServerErrorMiddlewareاستارلت بیرون از همهٔ middlewareهای کاربر است، پس middleware هرگز پاسخی که برای یک استثنای مدیریتنشده ساخته میشود را نمیبیند. این باگ واقعاً رخ داد و باtest_error_responses_carry_the_header_for_every_handlerپوشش داده شد.
۱۰. اجرای پروژه
# ۱. زیرساخت
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 روی ۸۰۰۰
دستورهای کیفیت:
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 اجرا میشوند؛ مرز پورتها آمادهٔ استخراج است |