服务端 API 手册
这份手册描述的是 apps/modlink_server 当前已经实现的 Web wire contract。
modlink_server 当前的角色不是“已经完成的 Web UI”,而是服务化 host 边界:它把 backend 以 HTTP + SSE + WebSocket 的形式暴露出来,为后续 HTML / Web UI 提前固定协议。
目标读者是当前仓库里的前端和全栈开发者。默认前提是:
- 服务只运行在本机开发环境
- 默认地址是
127.0.0.1:8000 - 当前没有认证
- CORS 只允许
localhost/127.0.0.1任意端口
当前服务端接口分成 3 条线:
HTTP:命令与 snapshotSSE:低频事件WebSocket:高频 frame
如果只需要一个最短结论,可以先记住这条接入顺序:
GET /health- 拉
drivers、acquisition、settings、streams/descriptors - 建立
GET /events - 建立
WS /frames - 发命令后按需重新拉相关 snapshot
- 收到
resync_required后,全量重拉并重连 SSE
错误约定
当前 HTTP 错误响应统一是:
{
"error": {
"type": "RuntimeError",
"message": "recording is not active"
}
}状态码映射固定为:
400:ValueError404:显式HTTPException409:RuntimeError504:TimeoutError500:其他未处理异常
这意味着:
- 命令接口返回
200时,后端命令已经执行完成,而不是“只是成功入队” 409一般表示当前状态不允许该操作504表示底层等待超时
Snapshot 形状
DriverSnapshot
GET /drivers 和 GET /drivers/{driver_id} 返回的是 DriverSnapshot:
{
"driver_id": "api_demo.01",
"display_name": "ApiDemoDriver",
"supported_providers": ["demo"],
"is_running": true,
"is_connected": false,
"is_streaming": false
}RecordingSnapshot
GET /acquisition 和 GET /health 里的 acquisition 字段返回的是 RecordingSnapshot:
{
"state": "idle",
"is_started": true,
"is_recording": false,
"root_dir": "D:/Projects/ModLink-Studio/recordings"
}HTTP API
Health
GET /health
用途:
- 确认服务可用
- 获取 driver 数量
- 顺手拿到 acquisition 的当前 snapshot
成功响应:
{
"ok": true,
"driver_count": 1,
"acquisition": {
"state": "idle",
"is_started": true,
"is_recording": false,
"root_dir": "D:/Projects/ModLink-Studio/recordings"
}
}Drivers
GET /drivers
用途:
- 拉取所有 driver 的当前 snapshot
成功响应:
[
{
"driver_id": "api_demo.01",
"display_name": "ApiDemoDriver",
"supported_providers": ["demo"],
"is_running": true,
"is_connected": false,
"is_streaming": false
}
]GET /drivers/{driver_id}
用途:
- 拉取单个 driver 的当前 snapshot
常见失败:
404:driver_id不存在
POST /drivers/{driver_id}/search
请求体:
{
"provider": "demo"
}成功响应是 SearchResult[]:
[
{
"title": "API Demo Device",
"subtitle": "demo",
"device_id": null,
"extra": {
"token": "demo"
}
}
]常见失败:
400:provider 不支持504:driver search 超时
POST /drivers/{driver_id}/connect
请求体直接对应 SearchResult 形状:
{
"title": "API Demo Device",
"subtitle": "demo",
"device_id": null,
"extra": {
"token": "demo"
}
}成功响应:
{
"ok": true
}POST /drivers/{driver_id}/disconnect
成功响应:
{
"ok": true
}POST /drivers/{driver_id}/start-streaming
成功响应:
{
"ok": true
}常见失败:
409:driver 当前未连接,或状态不允许开始流
POST /drivers/{driver_id}/stop-streaming
成功响应:
{
"ok": true
}Streams
GET /streams/descriptors
返回当前所有已注册 stream descriptor,key 是 stream_id:
{
"api_demo.01.demo": {
"device_id": "api_demo.01",
"stream_key": "demo",
"stream_id": "api_demo.01.demo",
"payload_type": "signal",
"nominal_sample_rate_hz": 10.0,
"chunk_size": 4,
"channel_names": ["demo"],
"display_name": null,
"metadata": {}
}
}Acquisition
GET /acquisition
返回当前 acquisition snapshot。
POST /acquisition/start-recording
请求体:
{
"recording_label": "baseline"
}成功响应:
{
"ok": true
}常见失败:
409:已经在录制、或当前状态不允许开始录制
POST /acquisition/stop-recording
成功响应:
{
"ok": true
}POST /acquisition/markers
请求体:
{
"label": "blink"
}成功响应:
{
"ok": true
}POST /acquisition/segments
请求体:
{
"start_ns": 1000000000,
"end_ns": 2000000000,
"label": "trial_01"
}成功响应:
{
"ok": true
}Settings
GET /settings
返回完整 nested settings snapshot。
示例:
{
"ui": {
"preview": {
"rate_hz": 30
}
},
"acquisition": {
"storage": {
"root_dir": "D:/Projects/ModLink-Studio/recordings"
}
}
}PUT /settings/{key:path}
请求体:
{
"value": 30,
"persist": false
}成功响应:
{
"ok": true
}DELETE /settings/{key:path}?persist=true
示例:
DELETE /settings/ui.preview.rate_hz?persist=false成功响应:
{
"ok": true
}SSE 事件流
GET /events
这条连接只承载低频事件。
当前首版会推 5 类消息:
driver_connection_lostdriver_executor_failedrecording_failedsetting_changedresync_required
其中前 4 类是 backend 业务/运行时事件,resync_required 是 transport 控制事件。
driver_connection_lost
event: driver_connection_lost
data: {"driver_id":"api_demo.01","detail":{"code":"DEMO_LOST"},"kind":"driver_connection_lost"}payload 字段:
driver_iddetailkind
driver_executor_failed
event: driver_executor_failed
data: {"driver_id":"api_demo.01","detail":"driver executor exited unexpectedly","kind":"driver_executor_failed"}payload 字段:
driver_iddetailkind
recording_failed
event: recording_failed
data: {"recording_id":"20260403_120000","recording_path":"D:/Projects/ModLink-Studio/recordings/rec_20260403_120000","frame_counts_by_stream":{"api_demo.01:demo":128},"reason":"frame_stream_overflow","ts_ns":1770000000000000000,"kind":"recording_failed"}payload 字段:
recording_idrecording_pathframe_counts_by_streamreasonts_nskind
setting_changed
event: setting_changed
data: {"key":"ui.preview.rate_hz","value":30,"ts":1770000000.0,"kind":"setting_changed"}payload 字段:
keyvaluetskind
resync_required
resync_required 只在 event stream 自己 overflow 时出现:
event: resync_required
data: {"reason":"event_stream_overflow"}前端收到后建议做这 3 件事:
- 重新拉
drivers、acquisition、settings、streams/descriptors - 丢弃本地旧的低频状态缓存
- 重新连接
GET /events
WebSocket 帧流
WS /frames
这条连接只承载高频 frame。
stream_id query 规则:
- 支持重复 query:
/frames?stream_id=a&stream_id=b - 不传
stream_id时,表示订阅全部 stream - 首版不支持连接建立后的动态 subscribe / unsubscribe
当前服务端为每个客户端单独打开一个 FrameStream,并使用 drop_oldest 策略。这意味着前端预览更偏“保最新”,而不是“绝不丢帧”。
消息 schema
当前所有 payload type 都统一走 JSON 文本消息:
{
"kind": "frame",
"stream_id": "api_demo.01:demo",
"device_id": "api_demo.01",
"stream_key": "demo",
"payload_type": "signal",
"timestamp_ns": 123,
"seq": 7,
"dtype": "float32",
"shape": [1, 4],
"data_base64": "AACAPwAAAEAAAEBAAACAQA=="
}字段说明:
kind:当前固定为"frame"stream_iddevice_idstream_keypayload_typetimestamp_nsseqdtypeshapedata_base64
编码规则固定为:
data_base64 = base64(frame.data.tobytes(order="C"))dtype和shape用来在前端还原数组
首版不支持这些能力:
- 连接建立后的动态订阅切换
- 二进制 WebSocket frame
- 压缩协议
- 分块协议
推荐的前端接入顺序
可以按下面这条固定流程接:
GET /health,确认服务在线- 拉
GET /drivers - 拉
GET /acquisition - 拉
GET /settings - 拉
GET /streams/descriptors - 建立
GET /events - 建立
WS /frames - 发送 driver / acquisition / settings 命令后,按需重拉相关 snapshot
- 如果收到
resync_required,全量重拉 snapshot,并重连 SSE
这样接的好处是:
- 低频状态始终以 snapshot 为准
- SSE 只负责增量提醒,不负责承担全部状态
- WebSocket 只负责高频 frame,不混低频业务事件