رفتن به محتوا

معماری

Control Center از معماری تمیزِ ویژگی‌محور با الگوی پورت‌ها و آداپتورها پیروی می‌کند. کدبیس حول دامنه‌های کسب‌وکار (ویژگی‌ها) سازماندهی شده و قواعد سخت‌گیرانهٔ وابستگی با یک تست خودکار معماری اعمال می‌شود.

کلاینتی نازک روی سروری بدون رابط

بخش با عنوان “کلاینتی نازک روی سروری بدون رابط”

Control Center یک معماری کلاینت نازک است. هیچ کلاینتی پایگاه‌داده را باز نمی‌کند — یک فرایند cc_server بدون رابط مالک داده است (یک global.db کوچک به‌همراه یک فایل SQLite برای هر فضای کاری که به‌صورت تنبل باز می‌شود) و آن را روی RPC مبتنی بر WebSocket ارائه می‌دهد. برنامهٔ رایانه، نسخهٔ وب و همراه تلفن همه رندرکننده‌هایی روی همان یک اتصال RPC هستند.

برنامهٔ رایانه در نخستین اجرا انتخاب می‌کند که چگونه به سرورش برسد: محلی (روی همین ماشین یک cc_server اجرا می‌کند و از طریق لوپ‌بک با آن گفتگو می‌کند — پیش‌فرض همین است) یا راه دور (به یک cc_server در جای دیگر روی یک WebSocket امن وصل می‌شود). کلاینت وب همیشه راه دور است. حل‌کنندهٔ بوت (bootstrap/server_backend.dart) مسیر را پیش از آنکه Riverpod وجود داشته باشد انتخاب می‌کند، سپس rpcClientProvider را با کلاینت متصل بازنویسی می‌کند — بنابراین هر ویژگی از طریق سرور می‌خواند و می‌نویسد، نه از یک پایگاه‌دادهٔ محلی.

خودِ لایهٔ RPC بی‌حالت است: یک نشست احراز هویت می‌شود، اما به فضای کاری گره نمی‌خورد. هر فراخوانیِ محدود به فضای کاری، workspace_id خود را در آرگومان‌هایش حمل می‌کند و دیسپچر چنین فراخوانی‌ای را بدون آن رد می‌کند. همین است که به دو کلاینت روی یک سرور اجازه می‌دهد فضاهای کاری فعال متفاوتی داشته باشند، بدون آنکه «فضای کاری جاری»ی ذخیره‌شده در سرور باعث اختلاف شود — و یعنی ایزوله‌سازی به‌ازای هر فراخوانی اعمال می‌شود، نه به‌ازای هر اتصال.

این یک workspace دارت با تک‌فایل lockfile حل‌شده و بیست‌ویک عضو است — پانزده پکیج به‌علاوهٔ شش اپ — و اپ رایانه+وب (control_center) ریشهٔ workspace است. آن شش اپ عبارت‌اند از باینری سرور بدون رابط (cc_server)، اجراکنندهٔ ناوگان بدون رابط (cc_worker)، PWA تلفن (cc_remote)، کارگزار رلهٔ WebSocket (cc_signaling_server)، گالری Widgetbook (cc_gallery) و میزبان دموی عمومی (cc_demo_server). آن پانزده پکیج عبارت‌اند از cc_ui (سیستم طراحی)، cc_domain (کرنل مشترک Dart خالص: موجودیت‌ها، اشیای ارزشی، پورت‌ها، رویدادها و لایهٔ دامنهٔ هر ویژگی)، cc_harness (کرنل درونی عامل: یک حلقهٔ عامل Dart خالص — پیام‌ها، پورت ارائه‌دهنده، ابزارها، فشرده‌سازی، هدایت، قلاب‌ها، زیرعامل‌ها — بدون dart:io و بدون هیچ وابستگی cc_* دیگر)، cc_harness_runtime (آداپتور زمان‌اجرای هارنس درونی: ارائه‌دهنده‌های جریانیِ فقط-VM، کارگزاری اعتبارنامهٔ OAuth/PKCE، مخزن‌های اعتبارنامه و مجموعهٔ ابزار عمومی — عامل‌ها را بدون نصب هیچ CLI بیرونی اجرا می‌کند)، cc_rpc (حمل‌ونقل‌های کلاینت)، cc_host (کرنل سرور)، cc_data (مخزن‌های راه دور)، cc_persistence (پایگاه‌دادهٔ سمت سرور)، cc_server_core (ترکیب اپ-سرور + زمان‌اجرای هویت/حضور/ناوگان/ارزیابی‌ها)، cc_infra (آداپتورهای فقط-VM)، cc_mcp (ابزارهای MCP)، cc_mcp_client (کلاینت سرورهای MCP بیرونی که ابزارهایشان را به رجیستری محلی پل می‌زند)، cc_markdown (موتور markdown درون‌مخزن)، cc_natives (برگ FFI) و system_audio_capture. برای ماتریس کامل کلاینت و اینکه چه چیزی کجا اجرا می‌شود، استقرار و کلاینت‌ها را ببینید.

Presentation → Application/Providers → Domain ← Infrastructure
  • لایهٔ دامنه: موجودیت‌ها، اشیای ارزشی، واسط‌های مخزن، پورت‌ها و سرویس‌های دامنهٔ خالص Dart. صفر واردات زیرساختی (بدون dio، drift یا مدل‌های شبکه).
  • لایهٔ نمایش: صفحه‌ها، ویجت‌ها، نوتیفایرها. بدون دسترسی مستقیم به drift/DAO/لایهٔ داده؛ همه‌چیز از مسیر ارائه‌دهنده‌های Riverpod → مخزن‌ها می‌گذرد.
  • لایهٔ زیرساخت: پیاده‌سازی‌های مشخصِ پورت‌های دامنه و واسط‌های مخزن. به دامنه وابسته است، هرگز برعکس.

شکل کامل یک ویژگی چنین است:

feature_name/
├── data/ # Repository implementations, data sources, services, DTOs, mappers
├── domain/ # Entities, repository interfaces, ports, use cases
├── presentation/ # Screens, widgets, notifiers
└── providers/ # Riverpod providers

ویژگی‌های بسیار کمی زیر lib/features/ واقعاً هر چهار بخش را دارند و این شکاف‌ها آگاهانه‌اند، نه تصادفی. کرنل مشترک، domain/ هر ویژگی را در cc_domain جذب کرده و کلاینت نازک هیچ پیاده‌سازی مخزنی ندارد؛ بنابراین بیشتر ویژگی‌های زیر lib/ فقط presentation/ به‌علاوهٔ providers/ هستند و بس. پوشهٔ data/ یا domain/ که در کلاینت باقی مانده باشد، نشانهٔ چیزی است که واقعاً سمت کلاینت است.

دانستن دو استثنای نام‌دار ارزش دارد:

  • mcp فقط ارائه‌دهنده دارد. رابط تنظیمات و وضعیتش زیر settings/ است و خودِ سطح ابزار یک پکیج است (cc_mcp)، نه یک پوشهٔ ویژگی.
  • orchestration و plan_studio فقط presentation/ و providers/ دارند.

ویژگی guardrails لایهٔ دامنهٔ خود را در cc_domain/features/guardrails/ می‌گذارد، نه در اپ ریشه، چون سرور آن را اعمال می‌کند: یک قاعده، یک ActionClass (یا یک commandPrefix پوسته) را در یک دامنهٔ اعمال به یک تصمیم نگاشت می‌کند — allow، prompt یا deny — قواعد به‌ازای هر فضای کاری ذخیره می‌شوند و هر اقدام تحت‌کنترل عامل پیش از اجرا از زنجیرهٔ سیاست عبور می‌کند. حل سیاست مسیر space > agent > workspace > mode preset > built-in default را طی می‌کند و اولین دامنه‌ای که قاعده‌ای منطبق داشته باشد تصمیم می‌گیرد؛ «سخت‌گیرانه‌ترین» فقط یک قاعدهٔ فصل درون یک دامنه است و برای ترکیب چند کلاسی که یک اقدام اعلام می‌کند به کار می‌آید. سطح روبه‌روی اپراتور، تنظیمات → فضای کاری → مجوزهای عامل است، در /workspaces/<id>/settings/workspace/permissions. برای این مدل، گاردریل‌ها را ببینید.

packages/cc_domain/lib/core/domain/ موجودیت‌ها و مخزن‌هایی را نگه می‌دارد که میان ۳ ویژگی یا بیشتر مشترک‌اند. این کد Dart خالص با صفر وابستگی زیرساختی است، و همین است که به کلاینت Flutter و باینری سرور بدون Flutter اجازه می‌دهد همان تعریف‌ها را وارد کنند:

  • موجودیت‌های هسته: Agent، AgentRunLog، Workspace، Repo، ReviewSpaceAssociation
  • هویت و چندنفره: User، Principal (مُهرشدهٔ UserPrincipal | AgentPrincipal)، WorkspaceMember، WorkspaceRole
  • موجودیت‌های حافظه: MemoryFact، MemoryPolicy، AgentWorkingMemory، MemoryAccessGrant
  • اشیای ارزشی مشترک: AgentSkills، AgentRole، Mode، SandboxBackend، SandboxSpec، RunCost
  • پورت‌های مشترک: SandboxPort، CredentialBrokerPort، WorkspaceFilesystemPort، GitRepoInspectorPort، EmbeddingPort
  • سرویس‌های دامنه: MemoryAccessPolicy، ActivityLogger، MentionResolver، RunLivenessClassifier
  • DomainEventBus به‌علاوهٔ انواع رویداد (فضای کاری/عامل، PR/بازبینی، پیام‌رسانی، تیکتینگ، پایپ‌لاین، ارکستراسیون، حافظه، تقویم/جلسه‌ها، هویت و عضویت، رصدپذیری)

Control Center چندکاربره است: انسان‌ها و عامل‌ها کنشگرهایی هم‌ارزند که یک Principal آن‌ها را یکپارچه می‌کند. عضو فضای کاری، یک User است که با یک WorkspaceRole (owner/admin/member/viewer/guest) به یک Workspace گره می‌خورد. عضویت، مرز دسترسی است — نه یک کلید جفت‌سازی — و مجوزهای به‌ازای هر مخزن مانع از آن می‌شوند که عضویت فضای کاری از میزبان کد پیشی بگیرد. نخستین کاربری که روی یک سرور تازه ساخته می‌شود، مالک نصب است؛ دیگران با دعوت یا OIDC می‌پیوندند. ساخت یک فضای کاری، آن کاربر را به‌عنوان owner ثبت می‌کند.

همکاری زمان‌واقعی سرور مرجع + برندهٔ آخرین نوشتن (LWW) در سطح هر فیلد است، نه یک CRDT. حضور (چه کسی اینجاست، کجاست، نشانگرها، در حال تایپ بودن، وضعیت زندهٔ یک عامل و هزینهٔ در حال اجرا) یک خط گذرای جداگانه است که هرگز ذخیره نمی‌شود؛ حالت پایدار روی ستون فقرات تغییر خوش‌بینانه + بازپایه‌گذاری سرور + LWW سوار می‌شود. انسان‌ها و عامل‌ها یک فهرست مشترک دارند. با یک اپراتور انسانیِ تنها، خط حضور بی‌استفاده می‌ماند و هیچ عنصر واسطی برای فهرست اعضا پدیدار نمی‌شود — چندنفره بودن برای کاربر تنها هیچ هزینه‌ای ندارد.

پورت‌ها واسط‌های انتزاعی در لایهٔ دامنه‌اند. آداپتورها پیاده‌سازی‌های مشخص در زیرساخت‌اند. چون سرور بدون رابط مالک داده است و باید بدون Flutter ساخته شود، تقریباً هر آداپتور فقط برای VM است و در packages/cc_infra/lib/src/، بر حسب موضوع دسته‌بندی‌شده، زندگی می‌کند:

پورت محل آداپتور
SandboxPort cc_infra/lib/src/sandboxing/ (native_sandbox_adapter.dart)
CredentialBrokerPort cc_infra/lib/src/sandboxing/ (env_credential_broker.dart, github_fine_grained_broker.dart)
GitRepoInspectorPort cc_infra/lib/src/git/ (git_repo_inspector.dart)
EmbeddingPort cc_infra/lib/src/embedding/ (embedding_service.dart)
NotificationPort lib/core/notifications/ (سمت کلاینت: رویدادهای سرور را به‌صورت محلی رندر می‌کند)
AgentBackend cc_infra/lib/src/dispatch/backends/ (acp_backend.dart, harness_backend.dart, cli_backends.dart)
TicketProviderPort cc_infra/lib/src/tickets/linear/ (LinearTicketAdapter). Jira، ClickUp و GitHub Issues آداپتورهای همگام‌سازی تیکت در cc_infra/lib/src/tickets/sync/ هستند؛ تیکت‌های محلی در پایگاه‌دادهٔ فضای کاری زندگی می‌کنند و TicketProviderPort ندارند.
PipelineEnginePort cc_domain features/pipelines/domain/services/ (یک سرویس دامنهٔ Dart خالص — به هیچ زیرساختی نیاز ندارد)

ریشهٔ ترکیب اپ (di/providers.dart) پورت‌های سمت کلاینت را از طریق ارائه‌دهنده‌های Riverpod به پیاده‌سازی‌ها می‌بندد؛ سرور آداپتورهای خود را در cc_server_core ترکیب می‌کند.

Riverpod برای همهٔ حالت‌های کلاینت:

  • Notifier<T> و AsyncNotifier<T> برای حالت تغییرپذیر
  • FutureProvider<T> برای دادهٔ ناهمگام
  • فهرست‌ها و ردیف‌های زنده به‌صورت AsyncValue<List<T>> از طریق اشتراک‌های RPC می‌رسند — سرور در داخل از .watch() در Drift استفاده می‌کند؛ هیچ کلاینتی Drift را باز نمی‌کند
  • ابزارهای MCP وابستگی‌ها را به‌شکل پارامترهای تایپ‌شدهٔ سازنده دریافت می‌کنند، هرگز Ref

Drift (SQLite)، تقسیم‌شده به دو پایگاه‌داده، هر دو در cc_persistence:

  • GlobalDatabase (<dataDir>/global.db) — فهرست کوتاهی از جدول‌های واقعاً سراسری: رجیستری فضای کاری، کاربران/ترجیحات/دستگاه‌های جفت‌شده، اتصال‌های SSO، تنظیمات سرور در سطح نصب، سیاست‌های اقدام مدیریت‌شده (یک محدودکنندهٔ سطح نصب که فقط می‌تواند گاردریل‌های فضای کاری را سخت‌تر کند)، خوراک خبری هر کاربر، صف ناوگان (کارگرها/کارها/گزارش جای‌گذاری)، ایندکس پیش از احراز هویتِ workspace_routes و فرادادهٔ نصب. بوت فقط این فایل را باز می‌کند، پس هزینهٔ راه‌اندازی ثابت می‌ماند، هر چقدر هم که فضاهای کاری تاریخ انباشته باشند. افزودن جدول در اینجا یک تصمیم ایزوله‌سازی است که یک تست بازدارنده شما را وادار می‌کند برایش دلیل بیاورید.
  • WorkspaceDatabase (<dataDir>/<workspaceId>/workspace.db) — یک پوشه برای هر فضای کاری، که یک فایل SQLite به‌همراه هر چیز دیگری که فقط به آن فضای کاری تعلق دارد را نگه می‌دارد، پس worktreeهای یک گفتگو و فایل‌های عامل کنار پایگاه‌داده می‌نشینند و با آن حذف می‌شوند. تقریباً همه‌چیز اینجا زندگی می‌کند: عامل‌ها، فضاها، تیکت‌ها، حافظه، پایپ‌لاین‌ها، جلسه‌ها، گراف کد، بازبینی‌ها، مخزن‌ها. فایل‌ها در نخستین برخورد از طریق WorkspaceDatabaseManager.of(workspaceId) به‌صورت تنبل باز می‌شوند و آن‌گاه quick_check، نصب FTS/تریگر و vector_init خود را می‌پردازند.

هر دو نیمه از یک خط پایهٔ v1 فشرده‌شده شروع کردند — onCreate اسکیمای کنونی را می‌سازد — و گام‌های مهاجرت فقط زمانی افزوده می‌شوند که یک پایگاه‌دادهٔ مستقر باید به جلو حمل شود.

چون یک فضای کاری یک فایل است، برون‌بری یک فضای کاری یک دستور VACUUM INTO است (workspace.export)، نه یک دامپ جدول‌به‌جدول؛ workspace.import چنین فایلی را می‌پذیرد و از workspace_meta جاسازی‌شده استفاده می‌کند تا تشخیص دهد فایل خودش دوباره پذیرفته شده یا فایلی که از نصب دیگری آمده است.

هر دو پایگاه‌داده متعلق به cc_persistence هستند و فقط cc_server آن‌ها را باز می‌کند؛ هیچ کلاینتی DB را باز نمی‌کند — هر کلاینت از طریق RPC می‌خواند/می‌نویسد. جدول‌ها در packages/cc_persistence/lib/database/tables/ تعریف می‌شوند و DAOها در .../daos/؛ موجودیت‌های دامنه Dart خالص‌اند و از کلاس‌های جدول Drift جدا هستند و نگاشت در لایه‌های داده انجام می‌شود. FTS5 جستجوی متن کامل و sqlite_vector جای‌گذاری‌های برداری را بر عهده دارد، هر دو فقط سروری؛ هر دو پایگاه‌داده PRAGMA foreign_keys=ON را در حالت ژورنال WAL اجرا می‌کنند.

go_router، با یک واقعیت که بقیه را جهت می‌دهد: هر مقصد درون‌برنامه‌ای پیشوند فضای کاری دارد، به‌شکل /workspaces/:workspaceId/…. شناسهٔ فضای کاری در URL تنها منبع حقیقت برای فضای کاری فعال است — activeWorkspaceIdProvider از مسیر رانده می‌شود، نه برعکس — و همین است که خواندن دادهٔ محدود به فضای کاری بیرون از یک مسیر پیشونددار را بدون جایی برای گرفتن شناسه‌اش می‌گذارد.

  • توابع سازندهٔ مسیر در router/routes.dart، هر یک با شناسهٔ فضای کاری به‌عنوان آرگومان نخست، نه ثابت‌های رشته‌ای
  • چهار مسیر پیش از بافت بدون پیشوندند: /splash، /onboarding، /signed-out و /workspaces (انتخابگر)
  • ShellRoute پوستهٔ اپ را می‌پیچد؛ آن چهار سطح بیرون از آن تمام‌صفحه رندر می‌شوند
  • نگهبان احراز هویت: راه‌اندازی ناقص به /onboarding می‌رود؛ راه‌اندازی کامل که اعتبارنامهٔ میزبان کدش رفته باشد به /signed-out می‌رود. کامل یعنی حداقل یک میزبان کد برای کاربر واردشده متصل است و حداقل یک فضای کاری وجود دارد — نه مشخصاً GitHub — و CLIی gh روش احراز هویت نیست
  • دروازهٔ راه‌اندازی نخست در features/auth/providers/

همهٔ HTTP بیرونی در سرور زندگی می‌کند. کلاینت‌ها هرگز مستقیماً به GitHub، Linear یا Google وصل نمی‌شوند — آن‌ها عملیات RPC سرور را صدا می‌زنند و حتی رسانهٔ راه دور از طریق نقطهٔ پایانی /proxy/media سرور واکشی می‌شود، نه از مبدأ بالادستی.

  • کلاینت‌های dio در packages/cc_infra/lib/src/: GitHubApiClient، GitHubPrClient، GitHubContentClient، GitHubGraphQLClient، GitLabApiClient، BitbucketApiClient، LinearGraphQlClient و GoogleCalendarApiClient
  • تزریق توکن احراز هویت از طریق رهگیرها، از جمله یک رهگیر Google OAuth به‌ازای هر حساب در cc_server_core که توکن‌ها را روی 401 تازه می‌کند
  • همهٔ خطاها از packages/cc_infra/lib/src/network/error_mapper.dart به زیرکلاس‌های تایپ‌شدهٔ AppException که در cc_domain تعریف شده‌اند نگاشت می‌شوند

سرور اعتبارنامه‌هایی را نگه می‌دارد که اهمیت دارند: توکن‌های GitHub و تیکتینگ برای اعزام عامل‌ها، کلیدهای ارائه‌دهندهٔ LLM و توکن‌های OAuth همه زیر پوشهٔ دادهٔ سرور یا محیطش زندگی می‌کنند. آنچه کیف‌کلید کلاینت هنوز نگه می‌دارد، اعتبارنامهٔ دستگاه خودش و کلید جفت‌سازی برای یک سرور راه دور است.

  • رازها در flutter_secure_storage (کیف‌کلید macOS، مدیر اعتبارنامهٔ Windows، libsecret در Linux) — اعتبارنامهٔ دستگاه کلاینت و کلیدهای جفت‌سازی راه دور، نه توکن‌های ارائه‌دهنده
  • shared_preferences فقط برای ترجیحات غیرحساس (پوسته، قلم، چیدمان)
  • رازهای باقی‌ماندهٔ ارائه‌دهنده از نصب‌های قدیمی یک‌بار توسط features/auth/providers/credential_migration.dart به سرور سپرده می‌شوند و سپس از کیف‌کلید کلاینت حذف می‌شوند

قیدهای معماری با test/core/architecture_constraints_test.dart اعتبارسنجی می‌شوند، که اگر قواعد وابستگی نقض شوند شکست می‌خورد.