ข้ามไปยังเนื้อหา

การปรับใช้และไคลเอนต์

Control Center ไม่ใช่โปรแกรมเดียว แต่เป็น สถาปัตยกรรมไคลเอนต์แบบบาง: เซิร์ฟเวอร์แบบไม่มีส่วนติดต่อที่เป็นเจ้าของข้อมูลของคุณ และชุดไคลเอนต์ที่เรนเดอร์ ส่วนติดต่อผ่านการเชื่อมต่อ RPC เพียงเส้นเดียวไปยังมัน การเข้าใจการแยกส่วนนี้ อธิบายว่าทำไมบางอย่างจึงรันที่ที่มันรัน และแต่ละไคลเอนต์เข้าถึงอะไรได้หรือไม่ได้

โปรเซสชื่อ cc_server ถือฐานข้อมูล — global.db ขนาดเล็กสำหรับสถานะทั้ง เซิร์ฟเวอร์ บวกไฟล์ SQLite หนึ่งไฟล์ต่อหนึ่งเวิร์กสเปซ ซึ่งเปิดแบบ lazy — และรันงานที่อยู่ยาว: เอนจินไปป์ไลน์ ตัวกระทบยอด พื้นผิวเครื่องมือ MCP และตัวรับฟังการประสานงาน ทุกสิ่งที่ไม่ควรตายไปพร้อมหน้าต่าง หรือที่ไคลเอนต์สองตัวควรเห็นเหมือนกันเป๊ะ อยู่ที่นั่น

ไม่มีไคลเอนต์ใดเปิดไฟล์ฐานข้อมูลเอง แต่ไคลเอนต์ทุกตัวถือ RemoteRpcClient หนึ่งตัว และออกปฏิบัติการและสมัครรับข้อมูลของรีโปผ่านมัน เซิร์ฟเวอร์ยืนยัน การเชื่อมต่อเทียบกับคีย์ที่แชร์ล่วงหน้าของอุปกรณ์ที่จับคู่ — แล้วก็เลิกสนใจ การเชื่อมต่อนั้น เซสชันไม่ได้ผูกกับเวิร์กสเปซ ดิสแพตเชอร์ไม่มีสถานะ: ทุกการเรียก ที่อยู่ในขอบเขตเวิร์กสเปซจะพา workspace_id ของตัวเองมาในอาร์กิวเมนต์ และการเรียก ที่ละเว้นมันจะถูกปฏิเสธ

นั่นเป็นเจตนาโดยตั้งใจ และเป็นสิ่งที่ทำให้ไคลเอนต์สองตัวบนเซิร์ฟเวอร์เดียวอยู่ใน เวิร์กสเปซต่างกันได้ โดยไม่มี “เวิร์กสเปซปัจจุบัน” ที่เซิร์ฟเวอร์ถือไว้ให้ทั้งคู่ ขัดแย้งกัน การแยกโดเมนถูกบังคับต่อการเรียกแทน ในสี่ขั้นตอน:

  1. ปฏิบัติการจะประกาศว่าอยู่ในขอบเขตเวิร์กสเปซหรือไม่ ถ้าใช่และไม่มี workspace_id ส่งมา การเรียกจะถูกปฏิเสธ
  2. id ที่ระบุต้องเป็นเวิร์กสเปซที่ลงทะเบียนไว้ ตรวจก่อนที่อะไรจะเปิดฐานข้อมูล — ไม่อย่างนั้น id ที่ไม่รู้จักจะสร้าง workspace.db ผีเปล่า ๆ ขึ้นมาทุกคำขอ กรณีไม่พบจะตอบว่าไม่พบ
  3. WorkspaceRole ของผู้เรียกต้องถึงระดับขั้นต่ำของปฏิบัติการ ซึ่งอนุมานจากชนิดของมัน: การอ่านต้องเป็นแขก การแก้ไขต้องเป็นสมาชิก การทำลายต้องเป็นแอดมิน
  4. ปฏิบัติการที่เปิดเผยเนื้อหารีโปต้องมีสิทธิ์ต่อรีโปในระดับที่มันประกาศเพิ่มเติม — none / read / review / write — การเป็นสมาชิกเวิร์กสเปซจึงไม่ให้สิทธิ์ เกินโฮสต์โค้ดอย่างเงียบ ๆ

ประโยคที่สำคัญ: การถือคีย์จับคู่ไม่ใช่ขอบเขตการเข้าถึง สมาชิกภาพต่างหากที่เป็น

มีแรงกดดันสามอย่างที่ผลักไปสู่เซิร์ฟเวอร์:

  1. แหล่งความจริงหนึ่งเดียว เดสก์ท็อปกับโทรศัพท์ หรือแล็ปท็อปกับเดสก์ท็อป ควรเห็นสถานะฟลีตเดียวกัน ถ้าแต่ละตัวมีฐานข้อมูลของตัวเอง ทั้งคู่จะคลาดกันไป เซิร์ฟเวอร์ตัวเดียวหมายความว่าทุกไคลเอนต์อ่านข้อมูลสดชุดเดียวกัน
  2. เว็บไม่สามารถบริการตัวเองได้ เบราว์เซอร์เปิดโปรเซสย่อยหรือไฟล์ในเครื่อง ไม่ได้ บิลด์เว็บจึงเป็นเจ้าของฐานข้อมูลหรือรันเอเจนต์ไม่ได้เลย มันต้องคุยกับ เซิร์ฟเวอร์ที่ทำได้ แทนที่จะสร้างเส้นทางข้อมูลที่เป็นของเว็บเท่านั้นเป็นเส้นที่สอง เดสก์ท็อปจึงถูกดึงมาอยู่บนเส้นทางเดียวกัน
  3. การทำงานแบบไม่มีส่วนติดต่อ เซิร์ฟเวอร์ที่คุณรันบนเครื่องเล็ก ๆ ได้ — เข้าถึงได้จากโทรศัพท์บนรถไฟหรือเบราว์เซอร์ที่ไหนก็ได้ — มีประโยชน์กว่า GUI ที่คุณต้องเปิดค้างไว้

ข้อแลกเปลี่ยนคือตอนนี้เซิร์ฟเวอร์กลายเป็นสิ่งพึ่งพา: ถ้ามันไม่รัน ก็ไม่มีอะไรทำงาน คำตอบเริ่มต้นของเดสก์ท็อปคือเปิดเซิร์ฟเวอร์ขึ้นมาเอง

มีไคลเอนต์ห้าแบบที่ต่อถึงเซิร์ฟเวอร์ แต่ละแบบมีรูปร่างและโปรไฟล์ความไว้วางใจต่างกัน

แอปเดสก์ท็อป Flutter (macOS, Windows, Linux) คือไคลเอนต์เต็มรูปแบบ เมื่อเปิด ครั้งแรก มันจะถามว่าจะให้รันอย่างไร แล้วจำตัวเลือกไว้:

  • ในเครื่อง: เปิดและดูแล cc_server บนเครื่องนี้ — เป็นค่าเริ่มต้น ตั้งอยู่ในตัว ใช้คนเดียว เดสก์ท็อปจะเปิดเซิร์ฟเวอร์ ซึ่งเป็นเจ้าของฐานข้อมูลใต้รากของ app-support และคุยกับมันผ่านลูปแบ็ก
  • ระยะไกล: เชื่อมต่อกับ cc_server ที่รันอยู่ที่อื่นผ่าน WebSocket ที่ปลอดภัย ข้อมูลอยู่บนเซิร์ฟเวอร์นั้น เดสก์ท็อปนี้เป็นเพียงตัวแสดงผล

ในทั้งสองกรณี ไคลเอนต์ RPC ที่ได้จะแทนที่ผู้ให้ข้อมูลของแอป ส่วนติดต่อทั้งหมด — ทุกหน้าจอ ทุกฟีเจอร์ — จึงอ่านและเขียนผ่านเซิร์ฟเวอร์ เดสก์ท็อปไม่แตะไฟล์ฐานข้อมูล โดยตรงเลย

บิลด์เว็บคือแอป Flutter ตัวเดียวกันที่คอมไพล์ให้รันในเบราว์เซอร์ โฮสต์อยู่ที่ app.usectrl.dev จึงไม่มีอะไรต้องติดตั้ง เบราว์เซอร์ เปิดโปรเซสย่อยไม่ได้ ไคลเอนต์เว็บจึงเป็นแบบระยะไกลเสมอ: มันเชื่อมต่อ cc_server ผ่าน wss:// และเรนเดอร์ส่วนติดต่อเดสก์ท็อปเต็มรูปแบบ เมื่อเชื่อมต่อ แล้ว มันแสดงหน้าจอเดียวกันกับฟลีตเดียวกัน เพราะเป็นโค้ดเดียวกันบน RPC เดียวกัน ช่องว่างคือไม่กี่จุดที่ต้องใช้ความสามารถแบบเนทีฟที่เบราว์เซอร์ไม่มี — ชั้นคุณภาพ ของการบันทึกการประชุมด้านล่างคือจุดหลัก และการแจ้งเตือนของระบบปฏิบัติการคืออีกจุด

นี่คือเหตุผลที่ “ไม่ต้องดาวน์โหลด” กับ “ไม่ต้องมีเซิร์ฟเวอร์” เป็นข้ออ้างคนละเรื่อง ไคลเอนต์เว็บตัดการติดตั้งออก มันไม่ได้ตัดเซิร์ฟเวอร์ออก สิ่งที่มันเชื่อมต่ออาจเป็น cc_server บนแล็ปท็อปเครื่องเดียวกัน — ws://localhost:9030 ซึ่งไม่ต้องใช้ ใบรับรอง เพราะเบราว์เซอร์ถือว่าลูปแบ็กเชื่อถือได้ — หรือเซิร์ฟเวอร์บนเครื่องอื่น ซึ่งต้องเข้าถึงได้ผ่าน wss:// ด้วยใบรับรองที่เบราว์เซอร์เชื่อถือ cc_server ปฏิเสธที่จะผูกที่อยู่ที่ไม่ใช่ลูปแบ็กหากไม่มี TLS เซิร์ฟเวอร์ระยะไกลจึงหมายถึง ใบรับรองของตัวเอง พร็อกซีที่ยุติ TLS หรือทันเนลหรือ VPN ที่ให้ใบรับรองมา

ตัวบันเดิลถูกโฮสต์แยกจากเซิร์ฟเวอร์ที่มันคุยด้วย cc_server แบบสแตนด์อโลนให้บริการ ปลายทาง RPC พื้นผิว MCP และพร็อกซีของมัน แต่ไม่ให้บริการเว็บบันเดิล — เส้นทางไฟล์สแตติกมีอยู่ในเซิร์ฟเวอร์ แต่ไม่มีการตั้งค่าเว็บรูทบนไบนารีแบบ สแตนด์อโลน มันจึงไม่มีอะไรให้บริการที่ / อิมเมจคอนเทนเนอร์ cc-webapp ที่เผยแพร่คือสิ่งที่โฮสต์บันเดิล และ scripts/build_web.sh คือสิ่งที่บิลด์มัน เซิร์ฟเวอร์ยอมรับออริจินของเบราว์เซอร์เฉพาะที่อยู่ในรายการ --allowed-origins ซึ่งค่าเริ่มต้นคือ https://app.usectrl.dev การโฮสต์บันเดิลเองจึงต้องเพิ่ม ออริจินของมัน

สื่อ (อวตาร รูปในฟีด ภาพหน้าจอ PR) วิ่งผ่านปลายทางพร็อกซีของเซิร์ฟเวอร์ แทนที่จะยิงไปยังโฮสต์ต้นทางโดยตรง เบราว์เซอร์จึงไม่ดึงออริจินใด ๆ ก็ได้ ไคลเอนต์ที่ปรับใช้แล้วติด Content-Security Policy ต่อคำขอจากคุกกี้ที่ไม่ละเอียดอ่อน ออริจินของเซิร์ฟเวอร์ที่เชื่อมต่อเท่านั้นจึงได้รับอนุญาต

แอป Remote เป็นไคลเอนต์แยกต่างหากที่เบากว่า — เป็น PWA บน Flutter เว็บที่ remote.usectrl.dev มันไม่ใช่แอปเต็มรูปแบบ โดยเจตนา มันจับคู่ผ่านรีเลย์ WebSocket ที่มีตัวกลางและปิดผนึกแบบต้นทางถึงปลายทาง และพูดกับพื้นผิวเครื่องมือเฉพาะส่วนที่เน้นอ่านเป็นหลัก: อ่านตั๋วงาน ข้อความ และ ฟีดข่าว ส่งตอบกลับ หรืออัปเดตตั๋วงาน

โทรศัพท์เป็นตัวตนที่มีสิทธิ์ต่ำกว่าเอเจนต์ในเครื่อง นโยบายเครื่องมือแบบ ปฏิเสธโดยค่าเริ่มต้น คีย์ที่แชร์ล่วงหน้าที่ปิดผนึกทุกเฟรม ความสามารถของเซสชัน ที่กันปฏิบัติการพิเศษ (เช่นการจับคู่) ออกจากไคลเอนต์คู่หู การตรวจสอบบทบาทและ สิทธิ์รีโปต่อการเรียกข้างต้น และการจำกัดอัตรา ช่วยกันไม่ให้การจับคู่ที่อนุมัติแล้ว (หรือรั่วไหล) กลายเป็นการควบคุมระยะไกลเต็มรูปแบบ ดู ควบคุมระยะไกลและมือถือ สำหรับโมเดล ความปลอดภัยฉบับเต็ม

เครื่องมือภายนอก — เอดิเตอร์ เอเจนต์อื่น ๆ และ CLI — ที่พูดภาษา Model Context Protocol จะเชื่อมต่อกับพื้นผิว MCP ของเซิร์ฟเวอร์ และใช้ทะเบียนเครื่องมือที่ระบุชนิด นี่คือเส้นทางผสานระบบเชิงโปรแกรม ไม่ใช่ GUI ดู ใช้เซิร์ฟเวอร์ MCP และ เอกสารอ้างอิงเครื่องมือ MCP

cc_worker คือไบนารี Dart ล้วนแบบไม่มีส่วนติดต่อ ที่จับคู่กับ cc_server ประกาศความสามารถของโฮสต์ และดึงงานที่เช่าไปรัน มันสตรีมเหตุการณ์ของโปรเซส กลับมาผ่าน RPC เดียวกัน และถือไม่มีสถานะถาวร: ไม่มีฐานข้อมูล ไม่มีการยืนยัน ตัวตน ไม่มีการอนุมัติ ไม่มีงบประมาณ — สิ่งเหล่านี้ไม่เคยออกจาก cc_server วอร์กเกอร์คือแขนขา ไม่ใช่สมองที่สอง เซิร์ฟเวอร์ผู้ตัดสินหนึ่งตัว แขนขาโง่ ๆ N ชิ้น ไม่มีฉันทามติ ไม่มีทราฟฟิกระหว่างวอร์กเกอร์

การรันแบบเช่ายังคงเป็นแค่ท่อของการเช่า agentRun เป็นชนิดเดียวที่รันคำสั่งจริง (CC_JOB_COMMAND หรือพรอมต์ที่สะท้อนกลับถ้าไม่ได้ระบุมา) pipelineStep, codeIndex, goldenRender, benchmark และ evalBatch ตอนนี้ตรวจสอบด้วย git --version / rev-parse HEAD เพื่อให้เส้นทางสตรีมถูกใช้งาน โดยไม่แสร้งว่า จ็อบเหล่านั้นรันไปแล้ว

สิ่งที่มันประกาศคือระบบปฏิบัติการ สถาปัตยกรรม CPU จำนวนคอร์ แรม ว่าเข้าถึง Flutter SDK ได้หรือไม่ และโฮสต์มีแบ็กเอนด์แซนด์บ็อกซ์ใดบ้าง (ไม่มีบน Windows) วอร์กเกอร์เฉพาะทางยังประกาศ alwaysOn และ acceptsParallel เสมอ ซึ่งเป็นสิ่งที่ ตัวจัดตารางแบบปักหมุด → ให้ความสำคัญ → ระบายออก ใช้ตัดสินการวางงาน ไม่มีการตรวจสอบ GPU หรือ ML

เดสก์ท็อปที่ใช้คนเดียวไม่รับรู้สิ่งเหล่านี้เลย เพราะการสั่งงานตามปกติไม่ได้ผ่าน ฟลีตเลย จริง ๆ แล้ว_มี_วอร์กเกอร์ในโปรเซสโดยนัยที่ลงทะเบียนไว้สำหรับโฮสต์ของ เซิร์ฟเวอร์ แต่เป็นแค่ตะเข็บลงทะเบียนที่ยังไม่มีตัวรันต่อพ่วงไว้ — งานที่ถูกวาง บนวอร์กเกอร์ในเครื่องจริง ๆ จะล้มเหลวแทนที่จะรัน ทุกวันนี้ มีเพียง cc_worker จริงเท่านั้นที่รันงานที่เช่า

เรื่องที่ดูแล เซิร์ฟเวอร์ (cc_server) ตัวประมวลผลในฟลีต (cc_worker) ไคลเอนต์เดสก์ท็อป ไคลเอนต์เว็บ / โทรศัพท์
ฐานข้อมูล (Drift/SQLite) เป็นเจ้าของ — — —
การยืนยันตัวตน การอนุมัติ งบประมาณ และตัวตน เป็นเจ้าของ — — —
ไปป์ไลน์ ตัวกระทบยอด ตัวรับฟังการประสานงาน รัน — — —
พื้นผิวเครื่องมือ MCP ให้บริการ — — —
การรันเอเจนต์ รัน (หรือเช่าออกไป) รันงานที่เช่า — —
การเรนเดอร์ UI — — เต็มรูปแบบ เต็มรูปแบบ (เว็บ) / เน้นอ่าน (โทรศัพท์)
การบันทึกการประชุม (ไมค์ + เสียงการประชุม) — — บันทึกแบบเนทีฟ เว็บเท่านั้น ผ่านเสียงแชร์หน้าจอ
OAuth ของปฏิทิน (Google) เก็บโทเคน — — —

สองระดับนี้ไม่แซนด์บ็อกซ์เหมือนกัน cc_server ห่อการรันที่ตัวเองประมวลผลด้วย แซนด์บ็อกซ์ของระบบปฏิบัติการของโฮสต์เมื่อมีแบ็กเอนด์ — Seatbelt บน macOS, bubblewrap บน Linux และ WSL2 — และถอยไปใช้ขอบเขตอื่นบน Windows หรือเมื่อไม่มี bwrap และ socat cc_worker ไม่แซนด์บ็อกซ์เลย มันประกาศความสามารถ sandbox เพื่อให้เซิร์ฟเวอร์วางงานตามแพลตฟอร์มได้ แต่ไม่มีส่วนใดในวอร์กเกอร์ นำไปใช้จริง: งานที่เช่าจะถูกเปิดขึ้นตรง ๆ ถูกจำกัดด้วยสภาพแวดล้อมของสัญญาเช่าและ worktree ที่ได้รับมา การเช่างานออกไปจึงย้ายการรันออกนอกแซนด์บ็อกซ์ แม้บนโฮสต์ ที่มีแซนด์บ็อกซ์ก็ตาม ดู แซนด์บ็อกซ์และความปลอดภัย เพื่อดูว่าอะไรจำกัดการรันในแต่ละกรณี

การบันทึกการประชุมเกิดขึ้นที่ไคลเอนต์ เพราะต้องใช้ไมโครโฟนและเสียงของการประชุม — เซิร์ฟเวอร์ที่ซุกอยู่ในตู้เก็บของบันทึกสายประชุมของคุณไม่ได้ จากนั้นการบันทึกจะถูก สรุปโดยไปป์ไลน์ของเซิร์ฟเวอร์ และบันทึกย่อจะถูกเก็บไว้ในที่ที่ไคลเอนต์ทุกตัวอ่านได้

ไคลเอนต์เว็บก็บันทึกได้ด้วย ซึ่งรูปทรงของตารางอาจทำให้มองไม่เห็น: มันจับเสียงจาก ไมโครโฟนพร้อมแทร็กเสียงแชร์หน้าจอในเบราว์เซอร์ แล้วสตรีม PCM 16 kHz ไปยัง เซิร์ฟเวอร์ มัน_ต้องมี_แทร็กเสียงนั้น จึงล้มเหลวอย่างชัดเจนในที่ที่เบราว์เซอร์ ไม่ให้มา สิ่งที่เป็นของเดสก์ท็อปเท่านั้นจริง ๆ คือชั้นคุณภาพรอบการบันทึก — การตัดเสียงสะท้อนระดับสัญญาณผ่านโปรเซสเซอร์เนทีฟ มาตรวัดระดับอินพุตและคำเตือน ไมค์เสีย การรันสรุปซ้ำ การยกเลิกการประมวลผล และการบันทึกพร้อมเชื่อมลิงก์ของปฏิทิน

ไคลเอนต์ทุกตัวพิสูจน์ตัวตนด้วยคีย์ที่แชร์ล่วงหน้าที่จัดเตรียมไว้ครั้งเดียว:

  • เดสก์ท็อปในโหมดในเครื่องสร้างคีย์ใหม่ทุกครั้งที่บูต และส่งให้เซิร์ฟเวอร์ที่เปิดขึ้น ผ่านตัวแปรสภาพแวดล้อม จึงไม่มีสิ่งลับใดถูกเก็บไว้บนเดสก์ท็อป
  • ไคลเอนต์ระยะไกล (เดสก์ท็อปที่เชื่อมระยะไกล เว็บ โทรศัพท์) ใช้คีย์ที่ออกโดยคำสั่ง pair ของเซิร์ฟเวอร์ และเก็บไว้ในคีย์เชนของระบบปฏิบัติการ (หรือบนโทรศัพท์ เก็บไว้ในเครื่อง)

การเพิกถอนเป็นแบบทันที ไม่ใช่แบบรอจังหวะ เซิร์ฟเวอร์เฝ้าดูตารางอุปกรณ์ที่จับคู่ ของมัน และตัดเซสชันที่เปิดอยู่ซึ่งอุปกรณ์ออกจากชุดที่ใช้งานแล้ว ภายในไม่กี่วินาที — ทั้งบนเส้นทาง WebSocket ตรงและเส้นทางรีเลย์ คุณไม่ต้องรอให้อุปกรณ์เชื่อมต่อใหม่ เพื่อตัดมันออก

คีย์ไม่เคยเดินทางผ่าน URL ที่โฮสต์สแตติกมองเห็นได้ เมื่อมันมาถึงทางลิงก์ — บน โทรศัพท์หรือในเบราว์เซอร์ — มันจะติดมาใน URL fragment ส่วนที่อยู่หลัง # ซึ่งเบราว์เซอร์ไม่เคยส่งไปยังเซิร์ฟเวอร์ที่โฮสต์หน้านั้น นั่นคือคุณสมบัติที่ ไม่เปลี่ยนแปลงที่ควรจำ และใช้กับทั้งสองแบบ ไคลเอนต์เว็บมีทางเข้าอีกสองทาง: ฟอร์มเชื่อมต่อ ที่คุณวางเซิร์ฟเวอร์และคีย์ลงไปโดยตรง และรหัสเชิญ ซึ่งมันนำไปแลก เป็นข้อมูลรับรองที่ออกให้เฉพาะเบราว์เซอร์นั้น