# โปรโตคอลการสื่อสาร

> HTML: https://remote.ivjn.us/docs/th/protocol/

เอกสารนี้ระบุรูปแบบการโยงที่สมบูรณ์ระหว่าง agent, exchange และ cli คำจำกัดความอยู่ใน `crates/proto` และทั้งสามใช้รหัสเดียวกัน: ฟิลด์ถูกกำหนดไว้ในที่เดียว ดังนั้นความไม่สอดคล้องกันจึงปรากฏขึ้นในเวลาคอมไพล์ แทนที่จะเป็นในระหว่างเซสชันการใช้งานจริง

ให้บริการแก่ผู้ให้บริการตรวจสอบการรับส่งข้อมูลเครือข่ายเพื่อวินิจฉัยปัญหา และนักพัฒนาเปลี่ยนโปรโตคอลหรือเขียนไคลเอนต์บุคคลที่สาม

---

## สารบัญ

- [ภาพรวม](#ภาพรวม)
- [การขนส่งและจุดสิ้นสุด](#การขนสงและจดสนสด)
- [เวอร์ชันโปรโตคอล](#เวอรชนโปรโตคอล)
- [ข้อความควบคุม](#ควบคมขอความ)
  - [DeviceUp (agent → exchange)](#อปกรณขน-agent--exchange)
  - [อุปกรณ์ลง (exchange → agent)](#อปกรณลง-exchange--agent)
  - [OperatorUp (cli → exchange)](#operatorup-cli--exchange)
  - [ตัวดำเนินการลง (exchange → cli)](#ตวดำเนนการลง-exchange--cli)
  - [โครงสร้างข้อมูลที่ใช้ร่วมกัน](#โครงสรางขอมลทใชรวมกน)
- [เฟรมข้อมูลไบนารี](#เฟรมขอมลไบนาร)
- [Session ID การแปลระหว่างสองเนมสเปซ](#การแปลรหสเซสชนระหวางสองเนมสเปซ)
- [ลำดับการจับมือ](#ลำดบการจบมอ)
- [Eการจัดการข้อผิดพลาดและความล้มเหลวหมวดหมู่](#หมวดหมการจดการขอผดพลาดและความลมเหลว)
- [Keepalive และหมดเวลา](#keepalive-และหมดเวลา)
- [แรงดันย้อนกลับ](#แรงดนยอนกลบ)
- [Android การควบคุมหน้าจอ](#การควบคมหนาจอ-android)
- [Eเซสชันที่ปิดผนึกตั้งแต่ต้นทางถึงปลายทาง](#เซสชนทปดผนกตงแตตนทางถงปลายทาง)

---

## ภาพรวม

โปรโตคอลมีสองชั้นที่ใช้การเชื่อมต่อ WebSocket ร่วมกัน:

| เลเยอร์ | ผู้ให้บริการ | การเข้ารหัส | สารบัญ |
|---|---|---|---|
| ควบคุมข้อความ | WebSocket **ข้อความ** เฟรม | เจสัน | การลงทะเบียน ฮาร์ทบีท ช่วงเปิด/ปิด การแจ้งเตือนผล |
| กรอบข้อมูล | WebSocket **binary** เฟรม | ส่วนหัวคงที่ 9 ไบต์ + ไบต์ดิบ | สตรีมไบต์ PTY คำสั่ง stdout/stderr |

การฝังข้อมูลสตรีมมิ่งใน JSON เป็น base64 จะเพิ่มขนาดขึ้นประมาณ 33% การรับส่งข้อมูล PTY ประกอบด้วยแพ็กเก็ตขนาดเล็กบ่อยครั้งซึ่งสะท้อนการกดแป้นพิมพ์แต่ละครั้ง ดังนั้นโอเวอร์เฮดนี้จึงส่งผลโดยตรงต่อเวลาแฝงของการโต้ตอบ ข้อความควบคุมไม่บ่อยนัก ทำให้การอ่าน JSON มีคุณค่ามากขึ้น: การจับแพ็กเก็ตสามารถเข้าใจได้ทันที

ตามแบบแผน แต่ละเฟรมไบนารี WebSocket ** จะมีหนึ่งเฟรมข้อมูล** เท่านั้น ไบต์ที่เกินหรือหายไปถือเป็นข้อผิดพลาด

---

## การขนส่งและจุดสิ้นสุด

| จุดสิ้นสุด | ลูกค้า | การรับรองความถูกต้อง |
|---|---|---|
| `GET /ws/device` | agent | **เฟรมแรก** หลังจากสร้างการเชื่อมต่อต้องเป็นข้อความควบคุม `register` ที่มี `device_id` และ `token` |
| `GET /ws/operator` | remote-cli | ส่วนหัวคำขอ HTTP `Authorization: Bearer <运维令牌>` ในการจับมือ WebSocket |

ทั้งสองเส้นทางใช้ประเภทข้อมูลรับรองแยกกัน โทเค็นอุปกรณ์ไม่สามารถเข้าถึงจุดสิ้นสุดของผู้ให้บริการ และในทางกลับกัน

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

การใช้งานจริงต้องใช้ `wss://`: ข้อมูลประจำตัวทั้งสองประเภทจะปรากฏเป็นข้อความธรรมดาระหว่างการจับมือกัน

---

## เวอร์ชันโปรโตคอล

```rust
pub const PROTOCOL_VERSION: u16 = 1;
```

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

- ฝั่งอุปกรณ์: agent มีเวอร์ชันอยู่ใน `register` ในกรณีที่ไม่ตรงกัน เซิร์ฟเวอร์ส่งคืน `register_ack` ด้วย `ok: false` อธิบายทั้งสองเวอร์ชันใน `error` และปิดการเชื่อมต่อ
- ฝั่งผู้ดำเนินการ: เซิร์ฟเวอร์จะส่ง `welcome` ในเชิงรุกหลังจากสร้างการเชื่อมต่อ หาก CLI ตรวจพบความไม่ตรงกัน ระบบจะรายงานข้อผิดพลาดและแนะนำให้ผู้ใช้อัปเกรดเป็นเวอร์ชันที่ตรงกับ `server_version`

**ข้อความที่ไม่รู้จัก ค่า `type` จะถูกปฏิเสธเสมอ** ไม่เคยตีความว่าเป็นประเภทที่รู้จักโดยไม่โต้ตอบ

---

## ควบคุมข้อความ

ข้อความควบคุมทั้งหมดเป็นอ็อบเจ็กต์ JSON ที่แท็กโดยฟิลด์ `type` (serde enum ที่แท็กภายใน) ค่าแท็กและชื่อฟิลด์ใช้ `snake_case` `session_id` ได้รับการซีเรียลไลซ์อย่างโปร่งใสเป็นตัวเลขธรรมดา

### อุปกรณ์ขึ้น (agent → exchange)

| `type` | เขตข้อมูล | วัตถุประสงค์ |
|---|---|---|
| `register` | `protocol_version: u16`<br>`device_id: string`<br>`token: string`<br>`info: DeviceInfo` | ต้องเป็นข้อความ **first** หลังการเชื่อมต่อ เซิร์ฟเวอร์ไม่ยอมรับข้อความอื่นก่อนหน้านั้น โดยมีกำหนดเวลา 15 วินาที |
| `heartbeat` | `metrics: Metrics` | ตัวชี้วัดการดำเนินงานเป็นระยะ ความสดของการเชื่อมต่อได้รับการจัดการแยกกันโดยปิงปอง/ปิงปอง WebSocket ดั้งเดิม: ฮาร์ตบีทที่หายไปไม่ได้หมายความถึงการเชื่อมต่อที่ขาด หรือในทางกลับกัน |
| `exec_started` | `session_id`<br>`job_id`<br>`hold`<br>`timeout_secs` | คำสั่งเริ่มต้น; `job_id` ใช้ในรายการพักและสำหรับการติดตั้งใหม่หลังจากเชื่อมต่อใหม่ |
| `exec_exit` | `session_id`<br>`code: i32 \| null`<br>`timed_out: bool` | คำสั่งเสร็จสิ้น `code` คือ `null` เมื่อสัญญาณยุติกระบวนการ `timed_out` บ่งชี้ถึงการบังคับยุติเนื่องจากการหมดเวลา |
| `exec_list_resp` | `session_id`<br>`jobs: ExecHoldInfo[]` | รายการงานค้าง |
| `ubus_resp` | `session_id`<br>`code: i32`<br>`data: any` | ผลการโทร ubus `code` คือโค้ดส่งคืน ubus 0 หมายถึงความสำเร็จ |
| `android_resp` | `session_id`<br>`result: AndroidResult` | การทำงานของหน้าจอ Android เสร็จสมบูรณ์ ข้อความนี้จะสิ้นสุดเซสชัน |
| `pty_closed` | `session_id`<br>`code: i32 \| null` | เซสชัน PTY สิ้นสุดแล้ว `code` คือโค้ดทางออกของเชลล์ |
| `session_error` | `session_id`<br>`message: string` | ข้อผิดพลาดระดับเซสชัน อุปกรณ์จะส่งสิ่งนี้เมื่อไม่สามารถดำเนินการตามคำขอได้ แทนที่จะส่งไปโดยไม่ตั้งใจ |

`register` ตัวที่สองบนการเชื่อมต่อที่ลงทะเบียนไว้แล้วจะถือเป็นการใช้โปรโตคอลในทางที่ผิดและบันทึกเป็นคำเตือน

ตัวอย่าง:

```json
{"type":"register","protocol_version":1,"device_id":"lab-router","token":"a1b2….c2Vj…","info":{"hostname":"OpenWrt","model":"ath79/generic","openwrt_version":"OpenWrt 23.05.5","kernel":"5.15.137","arch":"mips","agent_version":"0.1.0"}}
{"type":"exec_exit","session_id":31,"code":0,"timed_out":false}
```

### อุปกรณ์ลง (exchange → agent)

| `type` | เขตข้อมูล | วัตถุประสงค์ |
|---|---|---|
| `register_ack` | `ok: bool`<br>`error: string \| null`<br>`heartbeat_interval_secs: u64` | ผลการลงทะเบียน ด้วย `ok: false` นั้น `error` จะจัดเตรียมเหตุผล จากนั้นเซิร์ฟเวอร์จะปิดการเชื่อมต่อ เซิร์ฟเวอร์ระบุช่วงเวลาการเต้นของหัวใจเพื่อให้สามารถลดความถี่จากส่วนกลางได้เมื่อฟลีตเติบโตขึ้นโดยไม่ต้องกำหนดค่าอุปกรณ์แต่ละเครื่องใหม่ agent หนีบ 0 ถึงอย่างน้อย 1 วินาทีเพื่อป้องกันไม่ให้ตัวจับเวลาไม่ว่างทำให้ CPU อิ่มตัว |
| `device_access` | `managers: DeviceManager[]`<br>`updated_at: u64` (มิลลิวินาที) | ใครได้รับอนุญาตในอุปกรณ์นี้ `DeviceManager` = `operator_id`, `username`, `email?`, `role`, `level` (`manage`/`view`), `source` (`role_default`/`grant`) ส่งหลังจากการลงทะเบียนและการอนุญาตเปลี่ยนแปลงเฉพาะกับตัวแทนที่โฆษณาความสามารถ `device_access` เท่านั้น ดูที่เก็บ `docs/permissions.md` |
| `exec_req` | `session_id`<br>`command: string`<br>`timeout_secs: u64`<br>`hold: bool` (ค่าเริ่มต้น `true`)<br>`hold_timeout_secs?: u64` | ดำเนินการคำสั่ง ด้วย `hold=true` (ค่าเริ่มต้น) กระบวนการสามารถดำเนินต่อไปได้หลังจากที่ตัวดำเนินการยกเลิกการเชื่อมต่อ การหมดเวลาใช้ `hold_timeout_secs` (ขาด/0 → 3600, ฮาร์ดสูงสุด 86400) `hold=false` ยกเลิกคำสั่งเมื่อตัวดำเนินการยกเลิกการเชื่อมต่อ |
| `exec_cancel` | `session_id` | ยุติการดำเนินการคำสั่ง (ฆ่า) ก่อนกำหนด |
| `exec_detach` | `session_id` | หยุดการสตรีมเอาต์พุตไปยังตัวดำเนินการปัจจุบันในขณะที่ยังคงกระบวนการพักไว้ |
| `exec_list_req` | `session_id` | สอบถามงานระงับปัจจุบัน |
| `exec_attach_req` | `session_id`<br>`job_id` | แนบงานการระงับเข้ากับเซสชันนี้อีกครั้ง และเล่นซ้ำเอาต์พุตที่บัฟเฟอร์ |
| `exec_hold_kill` | `session_id`<br>`job_id` | ยุติงานการระงับโดย job_id |
| `exec_hold_tail` | `session_id`<br>`job_id` | ติดตามเอาต์พุตใหม่เท่านั้น โดยไม่ต้องเล่นบัฟเฟอร์ซ้ำ |
| `exec_hold_cat` | `session_id`<br>`job_id` | ดัมพ์บัฟเฟอร์ปัจจุบันและจบด้วย `exec_hold_cat_done` |
| `ubus_req` | `session_id`<br>`object: string`<br>`method: string`<br>`params: any` | โทรออกรถอูบัส เมื่อ `params` คือ `null` จะไม่มีการส่งผ่านอาร์กิวเมนต์ตำแหน่งที่สามไปยัง `ubus` |
| `pty_open` | `session_id`<br>`rows: u16`<br>`cols: u16` | เปิดเซสชัน PTY ขนาดศูนย์ถอยกลับไปที่ 24×80 |
| `pty_resize` | `session_id`<br>`rows: u16`<br>`cols: u16` | ขนาดของหน้าต่างเทอร์มินัลมีการเปลี่ยนแปลง |
| `pty_close` | `session_id` | ปิดเซสชัน PTY |
| `android_req` | `session_id`<br>`request: AndroidAction` | การทำงานของหน้าจอ Android หนึ่งหน้าจอ |
| `android_cancel` | `session_id` | ยกเลิกคำร้องขอ Android ที่ยังไม่เสร็จสิ้น |

### OperatorUp (cli → exchange)

การตรวจสอบความถูกต้องเกิดขึ้นระหว่างการจับมือ WebSocket ดังนั้นจึงไม่มีข้อความเข้าสู่ระบบ

| `type` | เขตข้อมูล | วัตถุประสงค์ |
|---|---|---|
| `open_exec` | `session_id`<br>`device_id`<br>`command`<br>`timeout_secs`<br>`hold` (ค่าเริ่มต้นเป็นจริง) <br>`hold_timeout_secs?` | ดำเนินการคำสั่งบนอุปกรณ์เป้าหมาย การระงับถูกเปิดใช้งานตามค่าเริ่มต้น `hold=true` พร้อมระยะหมดเวลา 0 → 3600 วินาที `hold=false` พร้อมการหมดเวลา 0 → Exchange ค่าเริ่มต้น 60 วินาที |
| `list_exec_holds` | `session_id`<br>`device_id` | ค้นหางานผู้บริหารและการดาวน์โหลด การอัพโหลด และการสืบค้นไดเร็กทอรีที่ใช้งานอยู่ |
| `list_device_sessions` | `session_id`<br>`device_id` | ค้นหาตารางเส้นทาง Exchange โดยตรงโดยไม่ต้องใช้สล็อตเซสชันของอุปกรณ์ |
| `kill_device_session` | `session_id`<br>`device_id`<br>`target_session_id` | ยกเลิกโดย `target_session_id` ฝั่งอุปกรณ์ และปล่อยเส้นทางในเครื่อง รวมการถือครอง exec และไม่ใช้ช่องพิเศษ |
| `attach_exec` | `session_id`<br>`device_id`<br>`job_id` | แนบไปกับงานพักและรับเอาต์พุตต่อ |
| `exec_hold_kill` / `exec_hold_tail` / `exec_hold_cat` | `session_id`<br>`device_id`<br>`job_id` | ฆ่า / หาง / แมว สำหรับงานค้าง |
| `detach_session` | `session_id` | หยุดการรอในเบื้องหน้าและแยกออกโดยไม่ทำให้กระบวนการระงับหยุดลง |
| `open_ubus` | `session_id`<br>`device_id: string`<br>`object: string`<br>`method: string`<br>`params: any` | โทรออกรถอูบัส |
| `open_pty` | `session_id`<br>`device_id: string`<br>`rows: u16`<br>`cols: u16` | เปิดเซสชัน PTY แบบโต้ตอบ |
| `open_android` | `session_id`<br>`device_id: string`<br>`request: AndroidAction` | เริ่มต้นการทำงานของหน้าจอ Android ห้ามใช้บัญชีแบบอ่านอย่างเดียว |
| `pty_resize` | `session_id`<br>`rows: u16`<br>`cols: u16` | ขนาดของหน้าต่างเทอร์มินัลมีการเปลี่ยนแปลง ไม่มี `device_id`: มีเซสชันอยู่แล้ว ดังนั้นเซิร์ฟเวอร์จึงสามารถค้นหาอุปกรณ์เป้าหมายได้ |
| `close_session` | `session_id` | สิ้นสุดเซสชันในเชิงรุก: การออก PTY ปกติหรือการขัดจังหวะการดำเนินการคำสั่งของผู้ใช้ |

`session_id` ในข้อความ `open_*` ทั้งสี่ข้อความคือ ** จัดสรรโดย CLI เอง** (ดู [Session ID Translation ระหว่างสอง Namespaces](#การแปลรหสเซสชนระหวางสองเนมสเปซ)) อนุญาตให้ใช้เฟรมที่ตามมาทันทีหลังจากการร้องขอโดยไม่ต้องรอการตอบรับ ID

### ตัวดำเนินการลง (exchange → cli)

| `type` | เขตข้อมูล | วัตถุประสงค์ |
|---|---|---|
| `welcome` | `protocol_version: u16`<br>`server_version: string` | ส่งเชิงรุกโดยเซิร์ฟเวอร์หลังจากสร้างการเชื่อมต่อเพื่อตรวจสอบเวอร์ชันโปรโตคอลที่ตรงกัน |
| `device_sessions` | `session_id`<br>`sessions: DeviceSessionInfo[]`<br>`limit`<br>`count` | เซสชันฝั่งอุปกรณ์ปัจจุบันใน Exchange |
| `device_session_killed` | `session_id`<br>`target_session_id`<br>`released: bool` | เผยแพร่ในประเทศ เท็จถ้ามันไม่มีอยู่ |
| `exec_exit` | `session_id`<br>`code: i32 \| null`<br>`timed_out: bool` | คำสั่งสิ้นสุดลง แปลจากข้อความฝั่งอุปกรณ์ที่มีชื่อเดียวกัน |
| `ubus_resp` | `session_id`<br>`code: i32`<br>`data: any` | ผลการโทร ubus |
| `android_resp` | `session_id`<br>`result: AndroidResult` | ผลลัพธ์ Android พร้อม ID เซสชันที่แปลเป็นเนมสเปซของตัวดำเนินการ |
| `pty_closed` | `session_id`<br>`code: i32 \| null` | เซสชัน PTY สิ้นสุดแล้ว |
| `session_failed` | `session_id`<br>`reason: SessionFailure`<br>`message: string` | เซสชันล้มเหลว `reason` แยกสาเหตุเพื่อให้ CLI สามารถตัดสินใจได้ว่าการลองใหม่นั้นคุ้มค่าหรือไม่ |

### โครงสร้างข้อมูลที่ใช้ร่วมกัน
`ExecHoldInfo`: `job_id`, `kind` (`exec` / `download` / `upload` / `file`), `age_secs`, `session_id?`, `command_preview`, `status`, `timeout_secs`, `started_at_unix_ms`, `exit_code?`, `timed_out`. เพื่อความเข้ากันได้ `kind` ที่หายไปจะมีค่าเริ่มต้นเป็น `exec` และไม่มี `age_secs` ที่มีค่าเริ่มต้นเป็น 0 รายการคำตอบจะคำนวณอายุจากนาฬิกาปัจจุบัน สรุปพาธคำสั่ง/ไฟล์จำกัดอยู่ที่ประมาณ 120 อักขระ งานที่ไม่ใช่ exec รองรับการ kill เท่านั้น ไม่ใช่ tail/cat/attach

`DeviceSessionInfo`: `session_id: u32` (ID ฝั่งอุปกรณ์), `operator_session_id: u32`, `kind: string`, `hold: bool`, `age_secs: u64`, `deadline_unix_ms: u64` Exchange ตอบกลับรายการเซสชันและคำขอการยกเลิกที่บังคับโดยตรงโดยไม่ต้องเปิดเซสชันอุปกรณ์อื่น บัญชีแบบอ่านอย่างเดียวจะได้รับ `forbidden`

#### `DeviceInfo`

ข้อมูลอุปกรณ์แบบคงที่ รายงานหนึ่งครั้งใน `register` และจัดเก็บไว้ใน `devices.info` ทุกอย่างยกเว้น `agent_version` เป็นทางเลือก: แหล่งที่มาส่วนใหญ่จะไม่พร้อมใช้งานเมื่อ agent ทำงานภายนอก OpenWrt เช่น บนเครื่องพัฒนาหรือในคอนเทนเนอร์ การลงทะเบียนควรจะสำเร็จมากกว่าการเริ่มต้นล้มเหลว ช่องที่ขาดหายไปจะถูกละเว้นระหว่างการทำให้เป็นอนุกรม

| สนาม | ประเภท | แหล่งที่มา |
|---|---|---|
| `hostname` | `string?` | `/proc/sys/kernel/hostname` |
| `model` | `string?` | `DISTRIB_TARGET` ใน `/etc/openwrt_release` ถอยกลับไปเป็น `model` จาก `ubus call system board` ถ้าไม่พร้อมใช้งาน |
| `openwrt_version` | `string?` | `DISTRIB_DESCRIPTION` หรือ ubus `release.description` |
| `kernel` | `string?` | `/proc/sys/kernel/osrelease` |
| `arch` | `string?` | ค่าคงที่เวลาคอมไพล์ เชื่อถือได้มากกว่าการตรวจจับรันไทม์ |
| `agent_version` | `string` | เวอร์ชัน agent ใช้เพื่อวินิจฉัยเวอร์ชันที่ไม่ตรงกัน |

#### `Metrics`

รวบรวมอีกครั้งสำหรับแต่ละฮาร์ตบีต โดยใช้เฉพาะค่าที่มีอยู่โดยตรงจาก `/proc` โดยไม่ต้องฟอร์กกระบวนการย่อย ความล้มเหลวในการอ่านรายการเดียวไม่ได้ทำให้คอลเลกชันทั้งหมดล้มเหลว: การสูญเสียการวัดหนึ่งรายการจะดีกว่าความล้มเหลวของฮาร์ทบีททั้งหมด และการรายงานอุปกรณ์ออฟไลน์อย่างไม่ถูกต้อง

| สนาม | ประเภท | คำอธิบาย |
|---|---|---|
| `uptime_secs` | `u64` | `/proc/uptime` |
| `load` | `[f32; 3]` | โหลดเฉลี่ย 1/5/15 นาทีจาก `/proc/loadavg` |
| `mem_total_kb` | `u64` | `/proc/meminfo` |
| `mem_available_kb` | `u64` | ใช้ `MemAvailable` แทนที่จะเป็น `MemFree`: อย่างหลังไม่รวมแคชที่เรียกคืนได้และประเมินหน่วยความจำที่มีอยู่บนเราเตอร์ต่ำเกินไปอย่างมาก ทำให้เกิดการแจ้งเตือนที่ผิดพลาด |
| `cpu_usage_pct` | `f32?` | คำนวณจากความแตกต่างระหว่างสองตัวอย่าง `/proc/stat` ละเว้นในการเต้นของหัวใจครั้งแรกเนื่องจากไม่มีตัวอย่างอ้างอิง |
| `interfaces` | `InterfaceStats[]` | `/proc/net/dev`. รายงาน **cumulative** รับ/ส่งไบต์ แทนที่จะเป็นอัตรา ปล่อยให้เซิร์ฟเวอร์มีอิสระในการคำนวณอัตราในช่วงเวลาใดก็ได้ ละเว้นเมื่อว่างเปล่า |

`InterfaceStats`: `{ name: string, rx_bytes: u64, tx_bytes: u64 }`

---

## เฟรมข้อมูลไบนารี

```text
 0        1                    5                    9
 +--------+--------------------+--------------------+
 |  kind  |     session_id     |    payload_len     |  payload (payload_len 字节)
 |   u8   |    u32 (大端)      |    u32 (大端)      |
 +--------+--------------------+--------------------+
 └──────────── 9 字节定长头 ────────────┘
```

| ออฟเซ็ต | ความยาว | สนาม | คำอธิบาย |
|---|---|---|---|
| 0 | 1 | `kind` | ประเภทสตรีม; ดูด้านล่าง |
| 1 | 4 | `session_id` | บิ๊กเอนด์ `u32`. 0 ถูกสงวนไว้เป็น Sentinel ซึ่งหมายความว่าไม่มีเซสชัน |
| 5 | 4 | `payload_len` | Big-endian `u32` จำนวนไบต์ของเพย์โหลด สูงสุด 1 MiB |
| 9 | `payload_len` | `payload` | ไบต์ดิบโดยไม่มีการเข้ารหัสหรือการหลบหนี |

### ประเภทสตรีม

| ความคุ้มค่า | ชื่อ | ทิศทาง | คำอธิบาย |
|---|---|---|---|
| 1 | `ExecStdout` | อุปกรณ์ → ตัวดำเนินการ | เอาต์พุตมาตรฐานคำสั่ง Remote |
| 2 | `ExecStderr` | อุปกรณ์ → ตัวดำเนินการ | ข้อผิดพลาดมาตรฐาน ส่งแยกจาก stdout เพื่อให้ CLI สามารถแสดงผลแตกต่างออกไป เช่น เป็นสีแดง |
| 3 | `PtyData` | **สองทิศทาง** | สตรีมไบต์ PTY ต้นน้ำนำอินพุตคีย์บอร์ด ปลายน้ำนำเอาต์พุตหน้าจอ ประเภทเดียวกันให้บริการทั้งสองทิศทาง |

หาก agent ได้รับ `ExecStdout` / `ExecStderr` ดาวน์สตรีม ระบบจะบันทึกคำเตือนและละทิ้งเฟรม ซึ่งบ่งชี้ถึงข้อผิดพลาดในการใช้งานที่เพียร์

### หมายเหตุการออกแบบ

**`payload_len` นั้นซ้ำซ้อนกับฟิลด์ความยาว WebSocket ** มันยังคงอยู่ด้วยเหตุผลสองประการ: การถอดรหัสสามารถตรวจจับการตัดทอน (การประกาศ 11 ไบต์โดยมีเพียง 5 ที่ให้มาจะต้องถูกปฏิเสธ แทนที่จะยอมรับโดยไม่แจ้งให้ทราบว่าเป็นแพ็กเก็ตที่สั้นกว่า) และรูปแบบเฟรมจะยังคงไม่เปลี่ยนแปลงหากจำเป็นต้องใช้ TCP แบบดิบในภายหลัง

ขีดจำกัดเพย์โหลด **1 MiB** ไม่ได้มีไว้สำหรับการรับส่งข้อมูลปกติ: PTY และเอาต์พุตคำสั่งเป็นสตรีมส่วนเพิ่มที่เล็กกว่านั้นมาก ป้องกันไม่ให้เพียร์ใช้หน่วยความจำเซิร์ฟเวอร์จนหมดด้วยส่วนหัวที่อ้างว่ามีเพย์โหลด 2 GiB

เพย์โหลด **Empty ถูกต้อง**: การอ่านแบบศูนย์ไบต์บางครั้งเกิดขึ้นในเซสชัน PTY เฟรมดังกล่าวประกอบด้วยส่วนหัวขนาด 9 ไบต์เพียงอย่างเดียว

**เพย์โหลดมีไบต์ที่กำหนดเอง** รวมถึงลำดับที่ไม่ใช่ UTF-8 และต้องได้รับการเก็บรักษาไว้อย่างถูกต้อง

### ข้อผิดพลาดในการถอดรหัส

| เกิดข้อผิดพลาด | ทริกเกอร์ |
|---|---|
| `FrameTooShort { len }` | ความยาวรวมต่ำกว่า 9 ไบต์ |
| `UnknownStreamKind(v)` | ไบต์แรกไม่ใช่ 1/2/3 |
| `PayloadTooLarge { len }` | ประกาศ `payload_len` เกิน 1 MiB |
| `LengthMismatch { declared, actual }` | ความยาวที่ประกาศแตกต่างจากจำนวนไบต์ที่เหลืออยู่จริง |

ความล้มเหลวในการถอดรหัส data-frame ส่งผลต่อเฟรมนั้นเท่านั้น: exchange บันทึกคำเตือนและละทิ้งโดยไม่ยกเลิกการเชื่อมต่อ

---

## การแปลรหัสเซสชันระหว่างสองเนมสเปซ

**นี่คือส่วนหนึ่งของโปรโตคอลที่เข้าใจผิดได้ง่ายที่สุด**

### ในประโยคเดียว

**initiator** จัดสรร `session_id` ซึ่งเป็น **unique ภายในการเชื่อมต่อของตัวเองเท่านั้น** การเชื่อมต่อระหว่างโอเปอเรเตอร์และอุปกรณ์จะรักษาเนมสเปซ ID อิสระ และ exchange จะแปลระหว่างทั้งสอง เซสชันโลจิคัลหนึ่งเซสชันมี ID ที่แตกต่างกันสองรหัส

```text
  remote-cli                    exchange                      remote-agent
                                                        （设备 lab-router）

  ┌────────────┐        ┌────────────────────┐            ┌────────────┐
  │ 运维编号   │        │      路由表        │            │ 设备编号   │
  │  空间      │        │                    │            │  空间      │
  │            │        │  lab-router:       │            │            │
  │  session 7 │───────▶│    31 → (op-A, 7)  │───────────▶│ session 31 │
  │            │◀───────│                    │◀───────────│            │
  └────────────┘        │  op-A:             │            └────────────┘
                        │    7 → (lab-router,│
   由 CLI 自己的        │         31)        │             由 exchange
   SessionIdGenerator   └────────────────────┘             的 DeviceHandle
   分配（1,2,3…）                                          分配（1,2,3…）
```

### ทำไมต้องออกแบบนี้

**Aหลีกเลี่ยงการเดินทางไปกลับ ** หากเซิร์ฟเวอร์จัดสรร ID ทั้งหมด CLI จะต้องรอการตอบรับโดยแจ้งว่า ID เซสชันคือ 31 ก่อนที่จะส่งอินพุตคีย์บอร์ดหลังจากเปิด PTY การจัดสรร CLI อนุญาตเฟรมข้อมูลทันทีหลังจาก `open_pty`

**Aหลีกเลี่ยงการชนกันของ ID ระหว่างตัวดำเนินการ ** ตัวดำเนินการสองคนอาจใช้ ID ท้องถิ่น 1 บนอุปกรณ์เดียวกันพร้อมกัน อุปกรณ์จะต้องแยกแยะความแตกต่างเหล่านี้เพื่อหลีกเลี่ยงการส่งเอาต์พุตของบุคคลหนึ่งไปยังอีกบุคคลหนึ่ง exchange จัดสรร ID ฝั่งอุปกรณ์แยกกันสำหรับการเชื่อมต่ออุปกรณ์แต่ละรายการ โดยจับคู่อินสแตนซ์ `session 1` สองตัวกับ `31` ฝั่งอุปกรณ์และ `32`

**ป้องกันพฤติกรรมของผู้ปฏิบัติงานไม่ให้ส่งผลต่อ ID ฝั่งอุปกรณ์ ** exchange ควบคุม ID ฝั่งอุปกรณ์ทั้งหมด ไม่มีผู้ปฏิบัติงานสามารถระบุหรือคาดเดาได้

### สองโต๊ะ

exchange รักษาหนึ่งตารางต่อการเชื่อมต่อ โดยทั้งสองตารางจะอัปเดตร่วมกันเสมอ:

| ที่ตั้ง | ตาราง | คีย์ → ค่า |
|---|---|---|
| `DeviceHandle.sessions` | โต๊ะข้างหน้าฝั่งอุปกรณ์ | ID เซสชั่นอุปกรณ์ → (ตัวดำเนินการส่ง, ID เซสชั่นของโอเปอเรเตอร์, ID บัญชีของโอเปอเรเตอร์, ประเภทเซสชั่น, ID แถวการตรวจสอบ) |
| `OperatorHandle.sessions` | ดัชนีย้อนกลับด้านตัวดำเนินการ | รหัสเซสชันของผู้ปฏิบัติงาน → (รหัสอุปกรณ์ รหัสเซสชันอุปกรณ์) |

ดัชนีย้อนกลับช่วยให้สามารถล้างข้อมูลได้: เมื่อผู้ปฏิบัติงานตัดการเชื่อมต่อ เซิร์ฟเวอร์จะต้องค้นหาและยุติเซสชันบนอุปกรณ์ **e ทุกเครื่อง** ไม่เช่นนั้น กระบวนการและ PTY ที่ไม่มีผู้รับเอาต์พุตจะยังคงอยู่

### การแปลเกิดขึ้นที่ไหน

| ทิศทาง | ข้อความ | การแปล |
|---|---|---|
| ผู้ดำเนินการ → อุปกรณ์ (`open_*`) | ข้อความควบคุม | **Allocate** ID ใหม่ในเนมสเปซอุปกรณ์ ลงทะเบียนในทั้งสองตาราง และใช้ ID ใหม่ในข้อความที่ส่งไปยังอุปกรณ์ |
| ผู้ดำเนินการ → อุปกรณ์ (`pty_resize` / `close_session`) | ข้อความควบคุม | ค้นหารหัสเซสชันของอุปกรณ์ผ่านดัชนีย้อนกลับ แทนที่และส่งต่อ |
| ผู้ประกอบการ → อุปกรณ์ | กรอบข้อมูล | ค้นหารหัสเซสชันของอุปกรณ์ผ่านดัชนีแบบย้อนกลับและเขียนทับไบต์ของส่วนหัวทั้ง 4 ไบต์ โดยไม่เปลี่ยนแปลงเพย์โหลด |
| อุปกรณ์ → ตัวดำเนินการ | ข้อความควบคุม | ค้นหารหัสเซสชันของผู้ปฏิบัติงานและการเชื่อมต่อเป้าหมายในตารางส่งต่อ แทนที่รหัส และส่งมอบ |
| อุปกรณ์ → ตัวดำเนินการ | กรอบข้อมูล | ค้นหารหัสเซสชันของโอเปอเรเตอร์ในตารางไปข้างหน้าและเขียนทับส่วนหัวแทน โดยไม่เปลี่ยนแปลงเพย์โหลด |

การแปลเฟรมข้อมูลคือการเขียนใหม่ **only**: exchange จะไม่แยกวิเคราะห์ บัฟเฟอร์ หรือเปลี่ยนเพย์โหลดไบต์ใดๆ `DataFrame::with_session_id` แทนที่ ID ที่มีอยู่โดยไม่ต้องจัดสรรเพย์โหลดใหม่

### กฎการจัดสรร

ตัวจัดสรรทั้งสองเพิ่มขึ้นจาก 1 และ **skip 0** ซึ่งเป็น no-session sentinel ที่ใช้ใน ตัวอย่างเช่น ในข้อความแสดงข้อผิดพลาดระดับการเชื่อมต่อ

- CLI (`SessionIdGenerator`): wraps บนโอเวอร์โฟลว์และข้าม 0 การใช้สี่พันล้านเซสชันในการเชื่อมต่อเดียวนั้นไม่สมจริง แต่การ wraps เหมาะกับกระบวนการที่ใช้เวลานานมากกว่าการตื่นตระหนก
- อุปกรณ์ (`DeviceHandle::open_session`): หลังจากสรุปแล้ว หาก ID อยู่ในเซสชันสด ให้ดำเนินการต่อไปยังช่องว่างถัดไป

exchange ปฏิเสธ ID ตัวดำเนินการ 0 และการนำ ID เซสชันที่ใช้งานอยู่กลับมาใช้ใหม่ ความถูกต้องของการเชื่อมต่ออุปกรณ์ ขีดจำกัดการทำงานพร้อมกัน และการลงทะเบียนในดัชนีทั้งสองจะได้รับการตรวจสอบภายในส่วนสำคัญเดียวกัน ป้องกันไม่ให้เซสชันที่เปิดพร้อมกันจากผู้ให้บริการหลายรายเกินขีดจำกัด

### เมื่อไม่มีเส้นทาง

** ทิ้งอย่างเงียบๆ โดยไม่มีข้อผิดพลาด ** เพียร์ที่ยังคงส่งข้อมูลต่อท้ายหลังจากปิดเซสชันถือเป็นการแข่งขันปกติ สิ่งนี้ใช้ได้กับทั้งสองทิศทาง:

- อุปกรณ์ตอบกลับสำหรับเซสชันที่ไม่มีอยู่อีกต่อไป → บันทึกการแก้ไขข้อบกพร่อง ผู้ดำเนินการอาจตัดการเชื่อมต่อไปแล้ว
- ผู้ดำเนินการส่งข้อมูลสำหรับเซสชันที่ไม่ได้รับอนุญาต → ติดตามบันทึกและละทิ้ง

### เส้นทางการล้างข้อมูลเซสชัน

| ทริกเกอร์ | การล้างข้อมูล |
|---|---|
| อุปกรณ์สิ้นสุดเซสชันตามปกติ (`exec_exit` / `ubus_resp` / `android_resp` / `pty_closed` / `session_error`) | ลบเส้นทางไปข้างหน้าและดัชนีย้อนกลับของการเชื่อมต่อที่เป็นเจ้าของพร้อมกัน อัปเดตบันทึกการตรวจสอบ และส่งข้อความที่แปลแล้วไปยังผู้ปฏิบัติงาน |
| โอเปอเรเตอร์ `close_session` | ลบดัชนีทั้งสองออก แจ้งให้การเชื่อมต่ออุปกรณ์เดิมยุติ รับทราบการปิดตัวต่อผู้ปฏิบัติงานทันที และอัปเดตบันทึกการตรวจสอบการยกเลิก การปิดซ้ำไม่ทำอะไรเลย |
| อุปกรณ์ตัดการเชื่อมต่อ | ใช้เซสชันทั้งหมดสำหรับการเชื่อมต่อนั้น ส่ง `session_failed` / `device_disconnected` ไปยังผู้ให้บริการแต่ละราย และอัปเดตบันทึกการตรวจสอบด้วย `{"outcome":"device_disconnected"}` |
| ตัวดำเนินการตัดการเชื่อมต่อ | สำรวจดัชนีย้อนกลับเพื่อค้นหาเซสชันบนอุปกรณ์แต่ละเครื่อง ส่งคำแนะนำในการยุติสำหรับเซสชันที่ไม่ระงับ สำหรับ Hold exec ส่งเฉพาะ `exec_detach` เพื่อให้กระบวนการทำงานต่อไป **ล้างเฉพาะเซสชันของตัวเอง**; ผู้ให้บริการรายอื่นบนอุปกรณ์เดียวกันจะไม่ได้รับผลกระทบ |
| อุปกรณ์เชื่อมต่อใหม่และแทนที่การเชื่อมต่อเก่า | ทำให้เซสชันทั้งหมดในการเชื่อมต่อเก่าใช้ไม่ได้ และแจ้งให้ผู้ให้บริการแต่ละรายทราบด้วย `device_disconnected` |

ดัชนีย้อนกลับเชื่อมโยงกับการเชื่อมต่ออุปกรณ์เฉพาะ จะไม่เปลี่ยนเส้นทางตามรหัสอุปกรณ์ไปยังการเชื่อมต่อใหม่หลังจากการเชื่อมต่อใหม่ การเชื่อมต่อของผู้ให้บริการหลายรายสำหรับบัญชีหนึ่งแต่ละบัญชีเป็นเจ้าของดัชนี ดังนั้นการยกเลิกการเชื่อมต่อจะไม่ยกเลิกเซสชันของการเชื่อมต่ออื่น

การรับทราบสำหรับการยกเลิกอย่างชัดเจน: exec ส่งคืน `exec_exit` (`code: null`, `timed_out: false`), PTY ส่งคืน `pty_closed` (`code: null`) และ ubus ส่งคืน `session_failed` (`reason: device_error`) นี่เป็นการยืนยันว่า exchange ได้ปิดเซสชันแล้ว ไม่ใช่ว่ากระบวนการระยะไกลได้ออกจากไปแล้ว ข้อมูลต่อท้ายและข้อความออกที่ตามมาจากอุปกรณ์จะถูกละทิ้ง

ที่ขีดจำกัดการทำงานพร้อมกัน Exchange จะเรียกคืนเส้นทางที่ตัวดำเนินการหายไปหรือเกินกำหนดเวลาภายใต้การล็อกเซสชันเดียวกันก่อน จากนั้นจึงตรวจสอบขีดจำกัดอีกครั้ง กำหนดเวลาของผู้บริหารคือขีดจำกัดเวลาจัดส่งจริงบวกกับระยะเวลาผ่อนผัน 15 วินาที ไฟล์ใช้เวลา 3600 วินาทีบวก 15 วินาที ประเภทอื่นใช้งานได้ 24 ชม. Hold exec ภายในกำหนดเวลาจะไม่ถูกฆ่าเมื่อผู้ปฏิบัติงานออกไป การเรียกคืน/อย่างชัดเจน `kill_device_session` จะส่งการยกเลิกที่เกี่ยวข้องไปยังอุปกรณ์และลบดัชนีทั้งสองออกทันทีโดยไม่ต้องรอการตอบสนองของอุปกรณ์ ตัวดำเนินการที่พร้อมใช้งานจะได้รับ `exec_exit` (`timed_out:true`) สำหรับ exec หรือ `session_failed` (`device_error`, `session reclaimed`) สำหรับประเภทอื่น การอัปเดตการตรวจสอบเป็นแบบอะซิงโครนัสและพยายามอย่างเต็มที่

### การเชื่อมต่ออุปกรณ์ใหม่และยุค

การเชื่อมต่ออุปกรณ์แต่ละรายการจะได้รับ `epoch` (UUID) แบบสุ่มระหว่างการลงทะเบียน เมื่ออุปกรณ์เดียวกันเชื่อมต่อใหม่ ** การเชื่อมต่อใหม่จะแทนที่อันเก่า** แทนที่จะถูกปฏิเสธ เป็นไปไม่ได้ที่จะแยกแยะการเชื่อมต่อที่ซ้ำกันจากการเชื่อมต่อที่ไม่ทำงานซึ่งเซิร์ฟเวอร์ตรวจไม่พบ (การเชื่อมต่อแบบเปิดครึ่งหนึ่ง) ได้อย่างน่าเชื่อถือ การปฏิเสธอันใหม่จะป้องกันการเชื่อมต่อใหม่จนกว่าอันเก่าจะหมดเวลา

การล้างข้อมูลสำหรับการเชื่อมต่อเก่าอาจดำเนินการลงทะเบียน **after** ของการเชื่อมต่อใหม่ การยกเลิกการลงทะเบียนจึงต้องตรวจสอบยุค: ลบรายการตารางการเชื่อมต่อเฉพาะเมื่อยุคปัจจุบันตรงกับการเชื่อมต่อที่กำลังล้างข้อมูล หากไม่มีการตรวจสอบนี้ การล้างข้อมูลการเชื่อมต่อเก่าจะทำเครื่องหมายอุปกรณ์ที่เชื่อมต่อใหม่เป็นออฟไลน์ทันที

---

## ลำดับการจับมือ

### ด้านอุปกรณ์

```text
  remote-agent                                        exchange
      │                                                 │
      │  ①  WebSocket 握手（wss://host/ws/device）      │
      │────────────────────────────────────────────────▶│
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 启动 15 秒  │
      │                                          │ 注册计时器  │
      │                                          └──────┬──────┘
      │  ②  {"type":"register","protocol_version":1,    │
      │      "device_id":"…","token":"…","info":{…}}    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ a. 比对 protocol_version            │
      │                              │ b. split_token → 前缀查 devices 表  │
      │                              │ c. 校验 enabled                     │
      │                              │ d. argon2 verify（前缀不符也照做，  │
      │                              │    避免用耗时区分前缀是否存在）     │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │  ③  {"type":"register_ack","ok":true,           │
      │      "error":null,"heartbeat_interval_secs":30} │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │                                          ┌──────┴──────┐
      │                                          │ 登记连接表  │
      │                                          │ 顶替旧连接  │
      │                                          │ 回写 info 与│
      │                                          │ last_seen_at│
      │                                          └──────┬──────┘
      │                                                 │
      │  ④  退避曲线复位，写状态文件 state=connected     │
      │                                                 │
      │══════════════ 转发循环 ═══════════════════════════│
      │                                                 │
      │  heartbeat（每 30 秒，携带 Metrics）             │
      │────────────────────────────────────────────────▶│
      │  WebSocket Ping（随每次 heartbeat 发出）         │
      │────────────────────────────────────────────────▶│
      │                                     WebSocket Pong
      │◀────────────────────────────────────────────────│
      │                                     WebSocket Ping（每 30 秒）
      │◀────────────────────────────────────────────────│
      │  Pong（tungstenite 排队，agent 显式 flush）       │
      │────────────────────────────────────────────────▶│
```

**เส้นทางความล้มเหลว:**

- เฟรมแรกไม่ใช่ข้อความ เป็น JSON ไม่ถูกต้อง หรือไม่ใช่ `register` → บันทึกคำเตือนและปิดทันทีโดยไม่มีแอก
- ไม่มี `register` ภายใน 15 วินาที → ปิด
- เวอร์ชันไม่ตรงกัน → ส่งคืน `register_ack{ok:false, error:"协议版本不匹配：设备为 X，服务端为 Y"}` และปิด
- โทเค็นไม่ถูกต้อง / อุปกรณ์ไม่มีอยู่ / อุปกรณ์ที่ปิดใช้งาน → ส่งคืน `"设备 ID 或令牌无效"` เดิมเสมอและปิด **อย่าแยกแยะสาเหตุ**: การทำเช่นนั้นจะทำให้สามารถนับรายการอุปกรณ์ได้

agent จัดการหมวดหมู่ความล้มเหลวทั้งสองนี้แยกกัน:

| หมวด agent | ทริกเกอร์ | กลยุทธ์การเชื่อมต่อใหม่ |
|---|---|---|
| `Rejected` | รับ ack ด้วย `ok: false` | แก้ไขการรอ **300 วินาที** การลองอีกครั้งไม่สามารถแก้ไขโทเค็นที่ไม่ถูกต้องได้ ลองใหม่บ่อยครั้งโดยสิ้นเปลืองทรัพยากรทั้งสองด้านและบันทึกเซิร์ฟเวอร์ท่วม การรอที่นานกว่าจะทำให้ผู้อื่นมีเวลาอัปเดตการกำหนดค่า |
| `Transport` | การเชื่อมต่อล้มเหลว หมดเวลาการจับมือ (45 วินาที) การตอบสนองมีรูปแบบไม่ถูกต้อง การเชื่อมต่อถูกขัดจังหวะ | Exponential Backoff: เริ่มต้นที่ 1 วินาที เพิ่มเป็นสองเท่าในแต่ละครั้งสูงสุด 300 วินาที คูณด้วยความกระวนกระวายใจแบบสุ่มใน [0.5, 1.5) |

**Backoff จะรีเซ็ตหลังจากการลงทะเบียนสำเร็จแล้วเท่านั้น ** ไม่ใช่เฉพาะการสร้าง TCP เท่านั้น มิฉะนั้น การปฏิเสธทันทีหลังจากการเชื่อมต่อจะลดลงเป็นการลองใหม่อย่างหนาแน่นในช่วงเวลามิลลิวินาที

**Jitter เป็นสิ่งจำเป็น ** เมื่อเซิร์ฟเวอร์รีสตาร์ท อุปกรณ์หลายร้อยหรือหลายพันเครื่องจะตรวจพบการตัดการเชื่อมต่อพร้อมกัน ด้วยกราฟการลองซ้ำตามที่กำหนดที่เหมือนกัน พวกมันยังคงซิงโครไนซ์และโจมตีเซิร์ฟเวอร์ที่เพิ่งเริ่มต้นใหม่เป็นระลอก เปลี่ยนการรีสตาร์ทเป็นความล้มเหลวแบบเรียงซ้อนอย่างต่อเนื่อง

### ฝั่งโอเปอเรเตอร์

```text
  remote-cli                                          exchange
      │                                                 │
      │  ①  WebSocket 握手（wss://host/ws/operator）    │
      │      Authorization: Bearer <prefix>.<secret>    │
      │────────────────────────────────────────────────▶│
      │                                                 │
      │                              ┌──────────────────┴──────────────────┐
      │                              │ 在 upgrade 之前完成鉴权：           │
      │                              │ split_token → 前缀查 operators 表   │
      │                              │ → argon2 verify → 校验 enabled      │
      │                              └──────────────────┬──────────────────┘
      │                                                 │
      │      鉴权失败 → HTTP 401「凭证无效」（不升级）   │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │◀────────────────────────────────────────────────│
      │            101 Switching Protocols              │
      │                                                 │
      │  ②  {"type":"welcome","protocol_version":1,     │
      │      "server_version":"0.1.0"}                  │
      │◀────────────────────────────────────────────────│
      │                                                 │
      │  ③  比对 protocol_version（15 秒超时）          │
      │      不一致 → 报错退出，提示升级 CLI            │
      │                                                 │
      │══════════════ 可以发起会话 ═══════════════════════│
      │                                                 │
      │  {"type":"open_pty","session_id":1,             │
      │   "device_id":"lab-router","rows":40,"cols":120}│
      │────────────────────────────────────────────────▶│
      │                                                 │──▶ 查设备在线
      │                                                 │──▶ 查并发上限
      │                                                 │──▶ 查 readonly 角色
      │                                                 │──▶ 写审计（先于下发）
      │                                                 │──▶ 分配设备侧编号
      │  二进制帧（PtyData, session 1, 键盘输入）        │
      │────────────────────────────────────────────────▶│──▶ 改写为设备侧编号
      │                                                 │
      │  二进制帧（PtyData, session 1, 屏幕输出）        │
      │◀────────────────────────────────────────────────│◀── 改写为运维侧编号
```

CLI สามารถส่งเฟรมข้อมูลถัดไป ** โดยไม่ต้องรอการตอบรับใดๆ** นี่เป็นสาเหตุที่แน่ชัดว่าเหตุใดผู้ริเริ่มจึงจัดสรร ID เซสชัน

---

## หมวดหมู่การจัดการข้อผิดพลาดและความล้มเหลว

### `SessionFailure`

ใช้ในฟิลด์ `reason` ของ `session_failed`; ค่า JSON ใช้ Snake_case

| ความคุ้มค่า | ความหมาย | ผู้ผลิต | ลองใหม่ได้ |
|---|---|---|---|
| `device_offline` | ไม่มีอุปกรณ์อยู่หรือออฟไลน์อยู่ในขณะนี้ | exchange ไม่พบอุปกรณ์ในตารางการเชื่อมต่อระหว่าง `open_*` | ✅ |
| `device_disconnected` | อุปกรณ์ถูกตัดการเชื่อมต่อระหว่างเซสชัน | อุปกรณ์ถูกตัดการเชื่อมต่อ / แทนที่ด้วยการเชื่อมต่อใหม่ / คิวการส่งปิดไปแล้วระหว่างการจัดส่ง | ✅ |
| `device_error` | อุปกรณ์ปฏิเสธคำขอหรือการดำเนินการล้มเหลว | agent ส่งคืน `session_error` (การสร้าง PTY การเริ่มต้นคำสั่ง หรือการเรียก ubus ล้มเหลว) หรือเพียร์กินช้าเกินไปในระหว่างการส่งต่อ | ❌ |
| `rate_limited` | เกินขีดจำกัดเซสชันที่เกิดขึ้นพร้อมกัน | exchange ค้นหาเซสชันที่ใช้งานของอุปกรณ์ถึง `--max-sessions-per-device` (ค่าเริ่มต้น 32) ระหว่าง `open_*` | ✅ |
| `forbidden` | ข้อมูลประจำตัวของผู้ให้บริการไม่สามารถเข้าถึงอุปกรณ์ได้ | บัญชีแบบอ่านอย่างเดียว (`role = readonly`) พยายามเริ่มต้นเซสชันใดๆ | ❌ |

`is_retryable()` จงใจยกเว้น `forbidden` และ `device_error`: การลองปัญหาการอนุญาตอีกครั้งหรือข้อผิดพลาดของอุปกรณ์ภายในไม่มีจุดหมายและทำให้เกิดกิจกรรม CLI ที่ไม่จำเป็นเท่านั้น

CLI ผนวกคำใบ้ที่สามารถดำเนินการได้ต่อท้าย `message` ดั้งเดิม ตัวอย่างเช่น `device_offline` แนะนำให้ตรวจสอบสถานะอุปกรณ์ด้วย `remote-cli device list` `device_error` เป็นข้อยกเว้น: ข้อผิดพลาดของอุปกรณ์เองเป็นคำอธิบายที่ถูกต้องที่สุด และไม่ควรเจือจางด้วยแผ่นสำเร็จรูป

### ความหมายความล้มเหลวตามเลเยอร์

| เลเยอร์ | พฤติกรรม | ขอบเขต |
|---|---|---|
| การถอดรหัสเฟรมล้มเหลว | เข้าสู่ระบบและทิ้งเฟรม | หนึ่งเฟรม |
| ควบคุมการแยกวิเคราะห์ข้อความล้มเหลว | เตือนและเพิกเฉยต่อข้อความ | หนึ่งข้อความ; การเชื่อมต่อดำเนินต่อไป |
| ความล้มเหลวระดับเซสชัน | `session_failed` / `session_error` | หนึ่งเซสชั่น; การเชื่อมต่อดำเนินต่อไป |
| การลงทะเบียนล้มเหลว | ปิดหลัง `register_ack{ok:false}` | การเชื่อมต่อทั้งหมด |
| เวอร์ชันไม่ตรงกัน | ปิดหลังจากข้อความแสดงข้อผิดพลาดที่ชัดเจน | การเชื่อมต่อทั้งหมด |
| คิวล่าช้า | ปิดการเชื่อมต่อเชิงรุก | การเชื่อมต่อทั้งหมด |

หลักการ: **รักษาความล้มเหลวในเครื่องทุกครั้งที่เป็นไปได้** เนื่องจากการเชื่อมต่ออุปกรณ์อาจมีเซสชันพร้อมกันสำหรับผู้ให้บริการหลายราย

---

## Keepalive และหมดเวลา

| กลไก | ความคุ้มค่า | คำอธิบาย |
|---|---|---|
| exchange WebSocket ปิง | ทุก ๆ `--heartbeat-secs` (ค่าเริ่มต้น 30 วินาที) | exchange ส่งไปยังทั้งอุปกรณ์และผู้ให้บริการ ไลบรารีไคลเอนต์จัดคิว Pong โดยอัตโนมัติ agent ต้องล้าง sink อย่างชัดเจนหลังจากได้รับ Ping เพื่อส่ง Pong ทันที |
| agent WebSocket ปิง | ทุก `heartbeat_interval_secs` (มีให้ในเซิร์ฟเวอร์ ack) | agent ส่ง Ping ดั้งเดิมเพิ่มเติมจาก JSON `heartbeat` แต่ละตัว หลีกเลี่ยงขอบเขตการยกเลิกการเชื่อมต่อที่ไม่ได้ใช้งานประมาณ 100 วินาทีของ Cloudflare หาก Ping ของ exchange ล่าช้า |
| หมดเวลาความเงียบของการเชื่อมต่อ | `heartbeat × --missed-pings` (ค่าเริ่มต้น 30 × 3 = 90 วินาที) | การเชื่อมต่ออุปกรณ์หรือผู้ให้บริการที่ไม่มีข้อความขาเข้าในช่วงเวลานี้จะถือว่าสูญหายและถูกตัดการเชื่อมต่อ โป่งถือเป็นกิจกรรม `--missed-pings` ตั้งค่าเป็น 0 ย้อนกลับไปที่ช่วงการเต้นของหัวใจอย่างน้อยหนึ่งช่วง |
| หมดเวลาการเขียน exchange | 10 วินาที | ข้อมูล WebSocket ใด ๆ หรือการหมดเวลาการเขียน Ping จะสิ้นสุดการเชื่อมต่อ ป้องกันไม่ให้การเชื่อมต่อที่ช้าบล็อกการล้างข้อมูล |
| การเต้นของหัวใจธุรกิจ | ทุก `heartbeat_interval_secs` (มีให้ในเซิร์ฟเวอร์ ack) | agent ส่ง JSON `heartbeat` พร้อมด้วย `Metrics` และ WebSocket Ping แยกต่างหาก การรายงานตัวชี้วัดและปิงปอง/ปิงปองดั้งเดิมมีจุดประสงค์ที่แยกจากกัน จังหวะทางธุรกิจเพียงอย่างเดียวไม่สามารถระบุสถานะการเชื่อมต่อได้ |
| กิจกรรมออนไลน์/ออฟไลน์ | `tick_count` สำหรับทริกเกอร์การแจ้งเตือน `online` / `offline` (ค่าเริ่มต้น 3, ช่วง 1–100); การออกอากาศ/การตรวจสอบทางเลือก `REMOTE_MISSED_THRESHOLD` / `REMOTE_ONLINE_THRESHOLD` | ผู้ปฏิบัติงานกำหนดค่าทริกเกอร์สองตัว แทนที่จะนับสองครั้งในทริกเกอร์ตัวเดียว `offline` แจ้งเตือนหลังจาก `tick_count` พลาดติดต่อกัน `online` แจ้งเตือนหลังจาก `tick_count` สำเร็จติดต่อกัน `device_ids` ของทริกเกอร์แต่ละตัวสามารถเป็น ID อุปกรณ์ (สตริง), ชุด ID (อาร์เรย์) หรือละไว้ / `null` สำหรับอุปกรณ์พื้นที่ทำงานทั้งหมด ทริกเกอร์ไม่แจ้งเตือนซ้ำในขณะที่อยู่ในสถานะรายงานแล้ว ก่อนอื่นจะต้องทำการจัดเรียงใหม่ผ่านผลลัพธ์ตรงกันข้ามที่ติดต่อกันของ `tick_count` การออกอากาศสถานะและการตรวจสอบสถานะในกระบวนการไม่อ่านทริกเกอร์เหล่านี้และถอยกลับไปเป็น `REMOTE_MISSED_THRESHOLD` / `REMOTE_ONLINE_THRESHOLD` (ยังมีค่าเริ่มต้นเป็น 3) ซ็อกเก็ตจะยังคงอยู่ในตารางการเชื่อมต่อหรือไม่นั้นจะถูกกำหนดแยกต่างหากโดย `--missed-pings` |
| หมดเวลาการลงทะเบียนอุปกรณ์ | 15 วินาที | ต้องส่ง `register` ภายในช่วงเวลานี้หลังจากสร้างการเชื่อมต่อ |
| หมดเวลาการจับมือ agent | 45 วินาทีต่อเฟส (`HANDSHAKE_TIMEOUT`) | `connect_device_ws` (TCP, ตัวเลือก HTTP CONNECT, TLS, WebSocket handshake) และรอ `register_ack` หลังจากส่ง `register` แต่ละรายการจะได้รับขีดจำกัดนี้ 45 วินาทีไม่ใช่ขีดจำกัดรวม |
| agent เขียน / หมดเวลาล้าง Pong | 30 วินาที (`WRITE_TIMEOUT`) | การส่งแต่ละครั้ง (การลงทะเบียน, ฮาร์ทบีท JSON, Ping, เอาต์พุตเซสชัน) และ Pong flush หลังการลงทะเบียนแต่ละครั้งจะมีกำหนดเวลาของตัวเอง การเขียนล้มเหลวหรือการหมดเวลาจะสิ้นสุดการเชื่อมต่อและทริกเกอร์การเชื่อมต่อใหม่ โป่งฟลัชขณะรอการลงทะเบียนจะถูกควบคุมโดยการรอ 45 วินาทีด้านบน |
| agent TCP Keepalive | ตรวจสอบหลังจากไม่ได้ใช้งานเป็นเวลา 30 วินาที | เปิดใช้งาน `TCP_NODELAY` และ TCP Keepalive หลังจากการเชื่อมต่อโดยตรงหรือ HTTP CONNECT การตั้งค่า TCP ไม่ได้แทนที่ WebSocket Ping/Pong |
| หมดเวลาต้อนรับ CLI | 15 วินาที | เวลาที่ CLI รอ `welcome` หลังจากการเชื่อมต่อ |
| หมดเวลาดำเนินการคำสั่ง | ระบุโดย `timeout_secs` ใน `open_exec` | agent หนีบไว้อย่างน้อย 1 วินาที เมื่อหมดเวลา SIGTERM กลุ่มกระบวนการทั้งหมด จากนั้น SIGKILL หลังจากช่วงเวลาผ่อนผัน 3 วินาที |
| หมดเวลาการโทรของ ubus | 15 วินาที (คงที่ภายใน agent) | ubus คือ RPC ท้องถิ่น โดยทั่วไปแล้วการเกินช่วงเวลานี้บ่งชี้ว่าบริการที่เกี่ยวข้องนั้นค้างอยู่ |

ไคลเอ็นต์เชลล์ที่ไม่ได้ใช้งานยังคงตอบกลับด้วย Pong agent ล้างการตอบสนองโดยอัตโนมัติซึ่งจัดคิวโดยไลบรารีอย่างชัดเจน โดยไม่ต้องรอจังหวะการเต้นของหัวใจทางธุรกิจครั้งถัดไป ดังนั้น การไม่มีอินพุตคีย์บอร์ดจึงไม่ทำให้เกิดการตัดการเชื่อมต่อที่ผิดพลาด (#42) `--heartbeat-secs` ต้องมากกว่า 0; ค่าที่ไม่ถูกต้องจะถูกปฏิเสธในระหว่างการแยกวิเคราะห์อาร์กิวเมนต์เริ่มต้น

---

## แรงดันย้อนกลับ

exchange รักษาคิวการส่งแบบมีขอบเขตของข้อความ **512** สำหรับการเชื่อมต่อแต่ละรายการ

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

การนำส่งใช้ `try_send` ที่ไม่ปิดกั้นเสมอ เส้นทางการเราต์มีการล็อคตารางการเชื่อมต่อ `await` ใดๆ จะผูกระยะเวลาการล็อกกับความเร็วในการใช้เพียร์ ซึ่งช่วยให้ไคลเอ็นต์ที่ช้าสามารถหยุดทั้งตารางได้

ความล้มเหลวในการจัดส่งทั้งสองรายการได้รับการจัดการที่แตกต่างกัน:

| ผลลัพธ์ | ความหมาย | การจัดการ |
|---|---|---|
| `Lagging` | คิวเต็ม. | ขัดจังหวะการเชื่อมต่อโดยใช้สัญญาณปิดที่แยกจากกัน ทำความสะอาดเซสชัน และใช้ความพยายามอย่างดีที่สุดเพื่อแจ้งจุดสิ้นสุดอื่น ๆ ของความล้มเหลว |
| `Closed` | รีซีฟเวอร์ออกแล้ว | การยกเลิกการเชื่อมต่อตามปกติ บันทึกการแก้ไขข้อบกพร่องเท่านั้น |

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

agent ยังมีคิวขาออกที่มีความจุ 256 คิวเต็มหมายความว่าเครือข่ายไม่สามารถส่งข้อมูลได้เลย ดังนั้นการสะสมเพิ่มเติมจึงไม่มีประโยชน์ ตัดการเชื่อมต่อและเชื่อมต่อใหม่ เมื่อคิวอินพุต PTY (64) เต็ม การกดแป้นพิมพ์จะถูกละทิ้งไป: การสะสมอินพุตโต้ตอบขนาดเล็กนั้นไม่ได้มีวัตถุประสงค์
## การควบคุมหน้าจอ Android

กำหนดใน `crates/proto/src/android.rs` โดยใช้การกำหนดเส้นทางเซสชันสองเนมสเปซที่มีอยู่ การรับรองความถูกต้อง ขีดจำกัดการทำงานพร้อมกัน และการตรวจสอบ `request` ใน `open_android` / `android_req` เป็นอ็อบเจ็กต์ JSON ที่ถูกแยกโดย `action`:

| `action` | สาขาอื่นๆ | พฤติกรรม |
|---|---|---|
| `status` | ไม่มี | ค้นหาการอนุญาตท้องถิ่นและเวอร์ชัน SDK ไม่จำเป็นต้องเปิดใช้งานสวิตช์ควบคุมหน้าจอ แต่ต้องเรียกใช้บริการระยะไกล |
| `tree` | ไม่มี | โครงสร้างโหนดการเข้าถึงของแอปที่ใช้งานอยู่ในปัจจุบัน |
| `screenshot` | ไม่มี | ภาพหน้าจอ JPEG หนึ่งภาพ |
| `tap` | `x, y: u32` | แตะพิกัดพิกเซลของหน้าจอดั้งเดิม |
| `swipe` | `x1, y1, x2, y2, duration_ms: u32` | ปัดนิ้ว ระยะเวลา 50–5000 ms |
| `swipe_path` | `points: [{x, y}]` (2–64 จุด), `duration_ms: u32` | การปัดแบบหลายส่วน: การกดเพียงครั้งเดียวจะเคลื่อนที่ผ่านทุกจุดอย่างต่อเนื่อง โดยสอดแทรกตามความยาวอย่างสม่ำเสมอ ระยะเวลา 50–5000 ms |
| `global` | `key: string` | `back` / `home` / `recents` / `notifications` / `quick_settings` |
| `input_text` | `text: string` | แทนที่ฟิลด์ที่แก้ไขได้ซึ่งมีขนาดสูงสุด 8192 UTF-8 ไบต์ ไม่มี NUL |

`AndroidResult` ถูกเลือกปฏิบัติโดย `kind`; ต้องระบุข้อมูลทุกช่อง:

| `kind` | เขตข้อมูล |
|---|---|
| `status` | `controls_enabled, accessibility_enabled, screen_capture_enabled: bool`; `sdk: u32` |
| `tree` | `nodes: AndroidNode[]`; `screen_width, screen_height: u32`; `truncated: bool` |
| `screenshot` | `mime_type: "image/jpeg"`; `data_base64: string`; `width, height, screen_width, screen_height: u32` |
| `ok` | ไม่มี; บ่งบอกถึงความสมบูรณ์ของการป้อนข้อมูล ท่าทาง หรือการทำงานของระบบ |

โหนดประกอบด้วยสตริง `class_name, package, view_id, text, description`, `bounds: [left, top, right, bottom]` (พิกัดพิกเซลของหน้าจอที่ลงนาม) และบูลีน `clickable, editable, focused, password` สูงสุด 200 โหนด โดยแต่ละสตริงจำกัดไว้ที่ 256 UTF-16 ยูนิต `truncated` ระบุเฉพาะขีดจำกัดของโหนด ค่ารหัสผ่านและคำอธิบายว่างเปล่า และละเว้นแผนผังย่อยการกำหนดค่าของ agent เอง

ขอบที่ยาวที่สุดของภาพหน้าจอจำกัดไว้ที่ 1600 พิกเซล `width/height` คือขนาดภาพ `screen_width/screen_height` คือขนาดก่อนการปรับขนาด พิกัดท่าทางใช้ขนาดดั้งเดิม JSON ที่ส่งคืนโดย Java Bridge ถูกจำกัดไว้ที่ 2 MiB โดยอนุญาตให้มีไบต์เพิ่มเติม 1024 ไบต์สำหรับซองจดหมาย WebSocket ไม่มีการเพิ่มประเภทเฟรมไบนารีใหม่

```json
{"type":"open_android","session_id":1,"device_id":"phone","request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_req","session_id":31,"request":{"action":"input_text","text":"Hello\nWorld"}}
{"type":"android_resp","session_id":31,"result":{"kind":"ok"}}
{"type":"android_resp","session_id":1,"result":{"kind":"ok"}}
```

`screen_capture_enabled` บ่งชี้ถึงการอนุญาตภาพหน้าจอที่มีอยู่ในปัจจุบัน (MediaProjection หรือการเข้าถึง Android 11+) การใช้งานหน้าจอจริงทั้งหมดยังต้องใช้ `controls_enabled` สิทธิ์ไม่เพียงพอ หน้าต่างที่ไม่สามารถเข้าถึงได้ และการดำเนินการที่ล้มเหลวจะส่งคืน agent `session_error` ซึ่งแปลโดย Exchange เป็น `session_failed` เอเจนต์ที่ไม่ใช่ Android ส่งคืนข้อผิดพลาดที่ไม่สนับสนุนอย่างชัดเจน

Agent จัดการคำขอ Android ได้สูงสุด 4 รายการพร้อมกัน และเข้ารหัสภาพหน้าจอเพียงครั้งละหนึ่งรายการเท่านั้น การดำเนินการ Java รอสูงสุด 10 วินาที (สูงสุด 5 วินาทีสำหรับเฟรมหน้าจอ) การหมดเวลาคำขอ agent คือ 15 วินาที, การหมดเวลาของ CLI 20 วินาที ในการยกเลิกหรือยกเลิกการเชื่อมต่อของผู้ดำเนินการ Exchange จะส่ง `android_cancel`; agent ปิดซ็อกเก็ตในเครื่องและ APK จะยกเลิกงานที่อยู่ในคิวซึ่งยังไม่ได้ทำงาน ท่าทางหรือการเปลี่ยนแปลงข้อความที่เสร็จสมบูรณ์จะไม่ถูกย้อนกลับ

การดำเนินการตรวจสอบคือ `android`; รายละเอียดเก็บการกระทำและพิกัด `input_text` เก็บเฉพาะ `characters` (จำนวนอักขระ Unicode) และผลลัพธ์ที่สำเร็จจะบันทึกเฉพาะ `outcome: "completed"` เท่านั้น เนื้อหาข้อความ แผนผัง UI และรูปภาพไม่ได้ถูกเขียนลงในบันทึกการตรวจสอบ บัญชี `readonly` ไม่สามารถเริ่มต้นได้แม้แต่ `status`, `tree` หรือภาพหน้าจอ

## ส่วนขยายเซสชันของไฟล์

เอเจนต์ที่สนับสนุนการดำเนินการกับไฟล์โฆษณาความสามารถ `files` Exchange แปล `open_file {session_id, device_id, request}` เป็น `file_req {session_id, request}` `request.action` คือ `list`, `download` หรือ `upload`; คำจำกัดความของฟิลด์อยู่ใน `crates/proto/src/files.rs`

ข้อความควบคุมแบบสองทิศทางใช้ JSON: ตัวควบคุมจะส่ง `file_input {session_id, input}` และอุปกรณ์ส่งคืน `file_event {session_id, event}` `input.kind` คือ `chunk {offset, data}`, `ack {offset}` หรือ `finish`; `event.kind` คือ `ready {size}`, `chunk {offset, data}`, `progress {received}`, `complete {size}` หรือ `list {path, entries, truncated}` เนื้อหาแบบก้อนใช้ Base64 มาตรฐาน แต่ละก้อนมีขนาดสูงสุด 64 KiB และผู้ส่งจะรอการตอบรับก่อนดำเนินการต่อ

`ready` เริ่มต้นของการดาวน์โหลดต้องใช้ `ack {offset: 0}`; ส่วนต่อมาจะรับทราบจำนวนไบต์สะสม การอัปโหลดเริ่มต้นด้วย `ready` และ `progress` แต่ละตัวจะขับเคลื่อนส่วนถัดไป หลังจากส่งทุกอย่างแล้ว `finish` จะคอมมิตมัน `complete` และ `list` เป็นข้อความเทอร์มินัล ข้อผิดพลาดใช้ `session_error` / `session_failed` ที่มีอยู่ `close_session` ส่ง `file_cancel`; การยกเลิกและการตัดการเชื่อมต่อทั้งลบเส้นทางเซสชัน ข้อความไฟล์จะถูกส่งต่อเฉพาะเซสชันไฟล์ โดยคงการแยกระหว่างรหัสโอเปอเรเตอร์และเซสชันอุปกรณ์

เวอร์ชันโปรโตคอลยังคงเป็น 1; ข้อความใหม่จำเป็นต้องมีการอัพเกรด Exchange และ Agent ที่ตรงกัน สำหรับขีดจำกัดการทำงานและอินเทอร์เฟซ LAN โปรดดูที่ [การถ่ายโอนไฟล์และการเชื่อมต่อ LAN โดยตรง](/docs/th/file-transfer-and-lan.md)

## ส่วนขยายหลายแพลตฟอร์ม

เวอร์ชันโปรโตคอลยังคงอยู่ 1. การลงทะเบียน `info` เพิ่มตัวเลือก `platform` (`openwrt` / `linux` / `freebsd` / `netbsd` / `windows` / `android` / `macos`) `os_version` และ `capabilities` (`exec` / `pty` / `ubus` / `android_control` / `files` / `sensors` / `net`) OpenWrt รายงาน `openwrt_version`

Heartbeat `metrics.unavailable` เป็นอาร์เรย์สตริงทางเลือกที่ระบุเมทริก `uptime`, `load`, `memory`, `cpu` และ `interfaces` ที่ไม่พร้อมใช้งานในปัจจุบัน หากไม่มี ให้แยกวิเคราะห์โดยใช้รูปแบบเก่า ค่าที่สอดคล้องกับเมตริกที่แสดงเป็นเพียงตัวยึดตำแหน่งที่เข้ากันได้เท่านั้น ลูกค้าควรแสดงว่าไม่พร้อมใช้งาน Windows ไม่ได้จัดเตรียมค่าเฉลี่ยโหลด Unix และ Android อาจจำกัดแหล่งที่มาของ `/proc` บางส่วนผ่านการอนุญาตของระบบ

คำสั่งถูกตีความโดยเชลล์ของอุปกรณ์เป้าหมาย: `/bin/sh` บน Linux/OpenWrt/macOS, PowerShell บน Windows และ `/system/bin/sh` บน Android รูปแบบเฟรม PTY และข้อความควบคุมเหมือนกันในทั้งสามระบบ Windows ใช้ ConPTY อุปกรณ์ที่ไม่ใช่ OpenWrt ส่งคืน `session_error` สำหรับคำขอ ubus

## เซสชันที่ปิดผนึกตั้งแต่ต้นทางถึงปลายทาง

เวอร์ชันโปรโตคอลยังคงอยู่ 1. ข้อความใหม่: OperatorUp `open_sealed {session_id, device_id, hello}`, `sealed_input {session_id, packet}`; อุปกรณ์ลง `sealed_open {session_id, hello}`, `sealed_input`, `sealed_close {session_id, detach}`; อุปกรณ์ขึ้น / ตัวดำเนินการลง `sealed_accept {session_id, accept}`, `sealed_event {session_id, packet, last}` เฟรมไบนารีใหม่ประเภท `4 = Sealed` (เพย์โหลด `counter ‖ AEAD(inner_kind ‖ bytes)`), `DeviceInfo.seal_key` และความสามารถ `sealed`

สำหรับเซสชันที่ปิดผนึก Exchange ดำเนินการเฉพาะการแปล ID การจำกัดการทำงานพร้อมกัน และการกำหนดเส้นทาง ไม่มีการแยกวิเคราะห์หรือตรวจสอบเพย์โหลด (บันทึกการตรวจสอบมีเพียงกุญแจสาธารณะ ระดับการอนุญาตและการหมดอายุ และผลลัพธ์สุดท้าย) และไม่ได้ตัดสินการอนุญาตนอกเหนือจากการตรวจสอบว่าบัญชีสามารถมองเห็นอุปกรณ์ได้หรือไม่ เส้นทางจะถูกเผยแพร่บน `last:true` เส้นทางที่ปิดผนึกยอมรับเฉพาะเฟรม `Sealed` เท่านั้น เส้นทางธรรมดาปฏิเสธเฟรม `Sealed` agent ละทิ้งข้อความธรรมดาใดๆ ที่ส่งไปยัง ID เซสชันที่ปิดผนึก สำหรับการอนุญาต การจับมือกัน การรับคีย์ โมเดลภัยคุกคาม โปรดดูที่ [End-to-End Sealed Sessions](/docs/th/sealed-sessions.md)
