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

รันเซิร์ฟเวอร์แบบไม่มีส่วนติดต่อ

รัน cc_server บนเครื่องที่เปิดค้างไว้ แล้วเชื่อมต่อกับมันจากเดสก์ท็อป เบราว์เซอร์ หรือโทรศัพท์ของคุณ เซิร์ฟเวอร์เป็นไบนารีเนทีฟที่เขียนด้วย Dart ล้วน — ไม่มีเอนจิน Flutter — จึงรันแบบไม่มีส่วนติดต่อได้บน macOS, Linux หรือ Windows มันเป็นเจ้าของฐานข้อมูล รันเอเจนต์ และเรียก API ภายนอกทุกครั้ง ส่วนไคลเอนต์ เพียงเรนเดอร์เท่านั้น

ทุกการเผยแพร่จะส่งสแตกแบบโฮสต์เองซึ่งมีอิมเมจ GHCR สี่ตัว — แบ็กเอนด์ ไคลเอนต์แบบบางแบบสแตติกสองตัว และรีเลย์จับคู่ที่เป็นตัวเลือก — พร้อมอิมเมจ cc-server-demo แยกต่างหากสำหรับโฮสต์เดโมสาธารณะ การติดตั้งแบบโฮสต์เอง ไม่ต้องเช็กเอาต์ซอร์ส ตรึงแท็กเวอร์ชัน (ghcr.io/samuelalev/<image>:v0.0.1-rc.1) หรือใช้ latest

อิมเมจ สิ่งที่มันเป็น เผยแพร่พอร์ต
ghcr.io/samuelalev/cc-server แบ็กเอนด์: ฐานข้อมูล เอเจนต์ MCP เว็บฮุก -p 9030:9030
ghcr.io/samuelalev/cc-webapp ไคลเอนต์เว็บ (nginx แบบสแตติก) -p 8080:8080
ghcr.io/samuelalev/cc-remote PWA คู่หูบนโทรศัพท์ (nginx แบบสแตติก) -p 8081:8080
ghcr.io/samuelalev/cc-signaling-server รีเลย์จับคู่ WebRTC (ตัวเลือก) -p 8788:8788

สแตกทั้งหมดในรูปไฟล์ compose:

services:
server:
image: ghcr.io/samuelalev/cc-server:latest
ports: ["9030:9030"]
volumes: ["cc_data:/data"]
restart: unless-stopped
web:
image: ghcr.io/samuelalev/cc-webapp:latest
ports: ["8080:8080"]
restart: unless-stopped
remote:
image: ghcr.io/samuelalev/cc-remote:latest
ports: ["8081:8080"]
restart: unless-stopped
signaling:
image: ghcr.io/samuelalev/cc-signaling-server:latest
ports: ["8788:8788"]
restart: unless-stopped
volumes:
cc_data:

เรื่องที่ควรรู้ก่อนเปิดเผยสิ่งเหล่านี้พ้นลูปแบ็ก:

  • อิมเมจ cc-server มาพร้อม CC_SERVER_INSECURE=1 (มันต้องผูกกับ 0.0.0.0 เพื่อรับทราฟฟิกจากพอร์ตที่เผยแพร่) ซึ่งปลอดภัยเฉพาะเมื่ออยู่หลัง รีเวิร์สพร็อกซีที่ยุติ TLS — Caddy, Traefik, nginx หรือ Cloudflare Tunnel — บนเครือข่ายที่เชื่อถือได้ หากต้องการยุติ TLS ในโปรเซสเอง ให้เมานต์ใบรับรอง และตั้ง CC_SERVER_TLS_CERT / CC_SERVER_TLS_KEY (แล้วยกเลิก CC_SERVER_INSECURE) ทุกอย่างแก้ทับได้ด้วย docker run -e … หรือบล็อก environment: ใน compose

  • /data คือสถานะเดียว ฐานข้อมูล SQLite, ข้อมูลลับของอุปกรณ์ที่จับคู่, โมเดลที่ดาวน์โหลด และเสียงการประชุม ล้วนอยู่ในวอลุ่ม — สำรองมันไว้ แล้ว คอนเทนเนอร์จะกลายเป็นสิ่งที่ทิ้งได้

  • จับคู่ไคลเอนต์จากภายในอิมเมจ ก่อนการเชื่อมต่อครั้งแรก ซึ่งเทียบเท่ากับ ขั้นตอนที่ 3 ในคอนเทนเนอร์:

    หน้าต่างเทอร์มินัล
    docker run --rm -it -v cc_data:/data \
    --entrypoint /app/bin/cc_server ghcr.io/samuelalev/cc-server:latest pair
  • อิมเมจไคลเอนต์ทั้งสองเป็นโฮสต์ nginx แบบสแตติก — ไม่มีอะไรต้องตั้งค่า ในขณะรัน ฟอร์มเชื่อมต่อของไคลเอนต์แต่ละตัวรับ URL ของเซิร์ฟเวอร์, ID อุปกรณ์ และคีย์จับคู่ของคุณ รีเลย์สัญญาณก็เช่นกัน ต้องการแค่พอร์ตของมัน (แก้ทับได้ด้วย SIGNALING_HOST / SIGNALING_PORT) มันไม่มีสถานะ ไม่เก็บ ข้อมูลลับ และเป็นทางเลือกสำรอง — โทรศัพท์จะใช้ลูปแบ็ก, LAN, tailnet หรือ เส้นทาง wss:// ตรง ๆ เมื่อมีเส้นทางใดเข้าถึงได้ และจะถอยไปใช้รีเลย์เฉพาะ เมื่อไม่มีเลย ข้ามมันได้หากไคลเอนต์ของคุณเข้าถึงเซิร์ฟเวอร์โดยตรงเสมอ

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

  • Dart SDK รีโปนี้ตรึง SDK ไว้ด้วย fvm จึงต้องนำหน้าทุกคำสั่งด้วย fvm
  • รีโปนี้ที่เช็กเอาต์ไว้แล้ว
  • ชุดเครื่องมือเนทีฟที่สคริปต์จัดเตรียมต้องการ — Rust/cargo รวมถึง meson, ninja, pkg-config, git และคอมไพเลอร์ C++ สคริปต์บิลด์แต่ละตัวจะระบุสิ่งที่มันหาไม่พบและวิธีติดตั้ง
  • โฮสต์ที่เข้าถึงได้: ลูปแบ็กสำหรับใช้ในเครื่อง หรือที่อยู่ LAN/สาธารณะที่มี TLS สำหรับไคลเอนต์ระยะไกล

ไลบรารีเนทีฟเป็นสิ่งจำเป็น — ไม่มีโหมดลดความสามารถ บิลด์มันจากรากรีโป ก่อนสิ่งอื่นใด:

หน้าต่างเทอร์มินัล
scripts/natives/build_natives.sh

คำสั่งนี้บิลด์ rift, fff, tree-sitter พร้อมไวยากรณ์ของมัน, aec, lame, ccpty, cc_watcher, cc_inference และเนทีฟ SAML ลงใน build/natives/ ฮุกบิลด์ของ cc_server จะส่งสิ่งที่จัดเตรียมไว้ตรงนั้นซ้ำเข้าไปในบันเดิล การข้ามขั้นตอนนี้จึงทำให้บิลด์ล้มเหลวไปเลย หรือ — ด้วยช่องทางเฉพาะด้านล่าง — สร้างไบนารีที่การตรวจสอบก่อนบูตปฏิเสธที่จะเริ่มและระบุชื่อไลบรารีที่ขาดหาย

หน้าต่างเทอร์มินัล
cd apps/cc_server
fvm dart build cli

บันเดิลจะพา libsqlite3 และเนทีฟที่จัดเตรียมไว้มาพร้อมกับไบนารี — ไม่ต้องใช้ SQLite ของระบบและไม่ต้องใช้เอนจิน Flutter ไฟล์ปฏิบัติการจะไปอยู่ที่ ./build/cli/<os_arch>/bundle/bin/cc_server

ขั้นตอนที่ 3: จับคู่ไคลเอนต์ก่อนเริ่มครั้งแรก

หัวข้อที่มีชื่อว่า “ขั้นตอนที่ 3: จับคู่ไคลเอนต์ก่อนเริ่มครั้งแรก”

ไดเรกทอรีข้อมูลใหม่ยังไม่มีอุปกรณ์ที่จับคู่และไม่มีคีย์ที่แชร์ล่วงหน้า จึงยัง ตอบพรอมป์ต์คีย์จับคู่ของไคลเอนต์ไม่ได้ สร้างขึ้นมาด้วยคำสั่งย่อย pair มันเขียน global.db และ secrets.json โดยตรง เซิร์ฟเวอร์ที่ให้บริการ ไดเรกทอรีข้อมูลนั้นอยู่แล้วจะรับอุปกรณ์ใหม่โดยไม่ต้องรีสตาร์ต:

หน้าต่างเทอร์มินัล
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030

มันพิมพ์ค่าสามค่าสำหรับวางลงในฟอร์มเชื่อมต่อของไคลเอนต์:

Connect a thin client (paste into its connect form):
Server ws://localhost:9030/rpc
Device id web-client
Pairing key <a freshly generated key>

แฟล็กที่มีประโยชน์: --device (ค่าเริ่มต้น web-client; ID ยังเลือกได้ว่า ระดับของอุปกรณ์เป็นอะไร — desktop, ios, android นอกนั้นเป็นไคลเอนต์เว็บ), --label (ค่าเริ่มต้นคือชื่อแพลตฟอร์มบวกชื่อโฮสต์ของเครื่องนี้), --host (โฮสต์ที่จะพิมพ์ใน URL เนื่องจากเครื่องอื่นเข้าถึงลูปแบ็กไม่ได้) และ --client-url การรัน pair ซ้ำจะหมุนคีย์ของอุปกรณ์นั้นและตัดเซสชันที่ใช้งานอยู่ ทิ้ง การจับคู่ไม่เคยสร้างเวิร์กสเปซ ไคลเอนต์ที่เชื่อมต่อจะตั้งชื่อและสร้าง เวิร์กสเปซแรกผ่านขั้นตอนเริ่มต้นใช้งาน

เมื่อใช้ --client-url มันยังพิมพ์ QR ที่สแกนได้จากเทอร์มินัล ซึ่งเปิดไคลเอนต์เว็บ ที่กรอกข้อมูลไว้แล้ว:

หน้าต่างเทอร์มินัล
./build/cli/<os_arch>/bundle/bin/cc_server pair --data-dir ./data --port 9030 \
--bind any --host 192.168.1.42 --client-url https://app.usectrl.dev

ข้อมูลรับรองที่สร้างโดย cc_server pair ไม่มีเวลาหมดอายุ ส่วนที่สร้างในแอป จะหมดอายุหลัง 30 วันเสมอ เพิกถอนอุปกรณ์ที่สร้างจาก CLI ได้จาก การตั้งค่า → คุณ → อุปกรณ์ของคุณ เมื่อใช้เสร็จแล้ว

บนลูปแบ็ก — ค่าเริ่มต้นและเพียงพอสำหรับไคลเอนต์บนเครื่องเดียวกัน:

หน้าต่างเทอร์มินัล
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030

หากต้องการให้เครื่องอื่นเข้าถึง ให้ผูกกับทุกอินเทอร์เฟซ การผูกกับที่อยู่ที่ไม่ใช่ ลูปแบ็กโดยไม่มี TLS จะถูกปฏิเสธตอนเริ่มระบบ (Refusing to bind non-loopback address … without TLS) จึงต้องมีใบรับรอง:

หน้าต่างเทอร์มินัล
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --tls-cert /path/fullchain.pem --tls-key /path/privkey.pem \
--public-url wss://server.example:9030/rpc

หรือยุติ TLS ที่รีเวิร์สพร็อกซีบนเครือข่ายส่วนตัวที่เชื่อถือได้ แล้วเลือกผูกแบบ ข้อความล้วนไว้ข้างหลัง:

หน้าต่างเทอร์มินัล
./build/cli/<os_arch>/bundle/bin/cc_server --data-dir ./data --port 9030 \
--bind any --insecure --public-url wss://server.example/rpc

--insecure จะถูกเพิกเฉยเมื่อตั้งทั้ง --tls-cert และ --tls-key — TLS ชนะเสมอ เซิร์ฟเวอร์ประกาศสถานะข้อความล้วนเป็น insecure: true ใน GET /healthz และไคลเอนต์จะติดป้ายการเชื่อมต่อแบบนั้นว่าไม่ปลอดภัย

เซิร์ฟเวอร์จะรันจนถึง SIGINT/SIGTERM แล้วปิดตัวลงอย่างสะอาด

ชี้ไคลเอนต์ไปยัง URL ของเซิร์ฟเวอร์และคีย์จับคู่จากขั้นตอนที่ 3:

  • แอปเดสก์ท็อป: บนหน้าจอเปิดครั้งแรก (Control Center ควรรันอย่างไร?) ให้กรอกช่องเชื่อมต่อแทน รันในแอปนี้ หรือเพิ่มเซิร์ฟเวอร์ภายหลังที่ การตั้งค่า → เซิร์ฟเวอร์ → การเชื่อมต่อและสถานะ ดูเชื่อมต่อเซิร์ฟเวอร์ระยะไกล
  • ไคลเอนต์เว็บ: เปิด app.usectrl.dev (หรือสำเนาบิลด์เว็บของคุณเอง) แล้วป้อน URL ของเซิร์ฟเวอร์, ID อุปกรณ์ และคีย์จับคู่
  • โทรศัพท์: จับคู่แอป Remote จากไคลเอนต์ที่กำลังรัน — ดูจับคู่อุปกรณ์

หากต้องการซิงค์ปฏิทินของเวิร์กสเปซโดยไม่ต้องมี GUI ให้ใช้ขั้นตอน device code:

หน้าต่างเทอร์มินัล
GOOGLE_OAUTH_CLIENT_ID=<id> GOOGLE_OAUTH_CLIENT_SECRET=<secret> \
./build/cli/<os_arch>/bundle/bin/cc_server calendar connect \
--data-dir ./data --workspace <workspace-id>

มันพิมพ์โค้ดและ URL สำหรับอนุมัติบนอุปกรณ์อื่น เก็บรีเฟรชโทเค็นไว้ฝั่งเซิร์ฟเวอร์ แล้วออก บิลด์จากซอร์สไม่ได้พาไคลเอนต์ Google มาให้ จึงต้องใช้ตัวแปรสภาพแวดล้อม id และ secret — ไม่มีแฟล็ก --google-client-id ดูเชื่อมต่อ Google Calendar

แฟล็กที่ใช้ข้างต้นคือชุดที่การตั้งค่าครั้งแรกต้องใช้ พื้นผิวฉบับเต็ม — แฟล็กและ ตัวแปรสภาพแวดล้อม 23 คู่ คำสั่งย่อยสี่คำสั่ง และเลย์เอาต์ของไดเรกทอรีข้อมูล — อยู่ในเอกสารอ้างอิง CLI ของ cc_server

สี่ตัวที่คุณน่าจะต้องใช้มากที่สุด:

แฟล็ก ค่าเริ่มต้น ความหมาย
--data-dir ไดเรกทอรีข้อมูลแอปพลิเคชันต่อผู้ใช้ของระบบปฏิบัติการ ฐานข้อมูล ข้อมูลลับ โมเดล และสื่อที่แคชไว้
--port 9030 พอร์ต TCP (0 = ชั่วคราว)
--bind loopback loopback หรือ any สำหรับทุกอินเทอร์เฟซ (ต้องมี TLS หรือ --insecure)
--public-url อนุมานจากค่าที่ผูก URL ของ RPC ที่ประกาศให้ไคลเอนต์ที่จับคู่ไว้ ตั้งค่านี้อย่างชัดเจนเมื่ออยู่หลังพร็อกซี, NAT หรือทันเนล

ไดเรกทอรีข้อมูลเริ่มต้นคือ ~/Library/Application Support/control-center บน macOS, %APPDATA%\control-center บน Windows และ $XDG_DATA_HOME/control-center (หรือ ~/.local/share/control-center) ที่อื่น ๆ จะใช้ .cc_server สัมพัทธ์กับ cwd ก็เฉพาะเมื่อหาไดเรกทอรีโฮมหรือไดเรกทอรีข้อมูลแอปไม่เจอเท่านั้น

cc_server คือพื้นผิวผลิตภัณฑ์ทั้งหมด ไม่ใช่ส่วนย่อย: เส้นทางข้อมูล repo-RPC และการสมัครรับข้อมูล, ปลายทางเครื่องมือ MCP ที่ POST /mcp และ GET /sse บนพอร์ตเดียวกัน, เว็บฮุกขาเข้า, พร็อกซีสื่อและฟอนต์, คอลแบ็กการลงชื่อเข้าใช้ ครั้งเดียว, โปรโตคอลลีสของฟลีต, การดึง RSS ฝั่งเซิร์ฟเวอร์ตามกำหนดทุก 30 นาที และการค้นหาแบบเวกเตอร์ (ส่วนขยาย sqlite_vector ลงทะเบียนเป็นส่วนขยายอัตโนมัติ ระดับโปรเซส โดยลดลงเป็นการค้นหาแบบข้อความเต็มเฉพาะที่ส่วนขยายใช้ไม่ได้)

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

สองเรื่องที่ควรรู้ก่อนเปิดพอร์ตพ้นลูปแบ็ก:

  • MCP ปิดกั้นแบบล้มเหลวปลอดภัยนอกโฮสต์ พื้นผิว MCP ที่ไม่มีโทเค็นจะตอบเฉพาะลูปแบ็ก การผูกกับ any ไม่ได้ขยายขอบเขตนั้น — คำขอจากนอกโฮสต์จะถูกปฏิเสธด้วย “MCP requires a bearer token to answer non-loopback clients” จนกว่าจะตั้งค่าไว้ (mcp.setToken หรือ การตั้งค่า → เซิร์ฟเวอร์ → เซิร์ฟเวอร์ MCP)
  • GET /healthz ไม่ต้องยืนยันตัวตนโดยการออกแบบ เปิด CORS แบบไวลด์การ์ด และเปิดเผย id, ชื่อ, ลายนิ้วมือตัวตน และเวอร์ชันบิลด์ของเซิร์ฟเวอร์ ไคลเอนต์ต้องใช้ลายนิ้วมือเพื่อปักหมุดเซิร์ฟเวอร์ จึงเป็นเจตนา — แต่มันหมายความว่า cc_server ใดก็ตามที่เข้าถึงได้จะเปิดเผยข้อมูลเท่านั้นให้ใครก็ตามที่ถาม