状态: 草案 目标: 将 Syscity 的前后端通信统一到单一的 WebSocket-native 协议,并支持
assistant-ui作为主力 Web 前端。
Syscity 目前使用混合架构(REST API + SSE + WebSocket),这带来了以下问题:
- 协议碎片化: Web UI 用
POST /api/chat+SSE /api/events;CLI 用裸 HTTP;WebSocket 存在但未被充分利用。 - 鉴权不一致: Web 端用 OAuth2,CLI 无鉴权,WS 用 query token。
- 多端接入困难: 接入 App 或 CLI 需要重新实现两套传输层。
转向 WebSocket-native RPC 协议 可解决这些问题:
| 问题 | 之前 | 之后 |
|---|---|---|
| 传输层 | REST + SSE + WS | 单一 WebSocket |
| 鉴权 | OAuth2 / 无 / token | 统一设备配对 + 作用域 |
| 前端 | 自建 React | assistant-ui |
| 配置管理 | Web UI + REST API | 仅 CLI (syscity config) |
| 多端接入 | 每个客户端单独集成 | Web/App/CLI 共享一套协议 |
- WebSocket 为主,HTTP 为辅: 所有实时和交互式 API 走 WebSocket。HTTP 仅保留 OpenAI 兼容端点 (
/v1/*) 和健康探针。 - 统一鉴权: 每个客户端(Web、App、CLI)使用相同的鉴权方式。
- 配置仅限 CLI: 管理/配置类 API 从 Web 面移除。管理员通过
syscity二进制或配置文件配置 Syscity。 - 原生集成 Assistant-UI: Web 前端基于
assistant-ui重建,直接消费 WebSocket 协议。
ws://<host>:<port>/ws
wss://<host>:<port>/ws
Query 参数:
| 参数 | 必填 | 说明 |
|---|---|---|
token |
否 | 共享鉴权 token 或设备 token |
session_id |
否 | 连接时预订阅某个 session |
client |
否 | 客户端标识: web, ios, android, cli |
version |
否 | 客户端请求的协议版本 |
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /health |
存活探针 |
| GET | /ready |
就绪探针 |
| POST | /v1/chat/completions |
OpenAI 兼容 API (SSE 流式) |
| GET | /v1/models |
OpenAI 兼容模型列表 |
| GET | /api/v1/artifacts/*path |
文档预览产物(write_report 生成的报告) |
其余所有
/api/*和/api/v1/*端点已 废弃,将在 v2.0 中移除。
产物 URL 形态(通配路由,只读):
/api/v1/artifacts/@<agent_id>/<file>— 某个 Agent 工作区内的 artifacts(@default= 共享默认工作区)/api/v1/artifacts/@<agent_id>/<root>/<task>/<file>— 该 Agent 工作区内的 delegation 作用域报告/api/v1/artifacts/[<root>/<task>/]<file>— 旧版全局~/.syscity/artifacts/目录(向后兼容,迁移窗口内仍可访问)
路径段均经过遍历防护校验(拒绝绝对路径、..、反斜杠及百分号编码变体);@owner 段仅允许 agent id 字符集(字母数字、-、_)。
所有 WebSocket 消息均为 JSON text 帧,采用可辨识联合类型(discriminated union)。
{
"type": "req",
"id": "req_abc123",
"method": "chat.send",
"params": { ... }
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
"req" |
是 | 帧类型辨识字段 |
id |
string | 是 | 客户端生成的请求 ID(用于匹配 res) |
method |
string | 是 | 方法名,点号命名空间 |
params |
object | 否 | 方法特定参数 |
{
"type": "res",
"id": "req_abc123",
"ok": true,
"payload": { ... }
}错误时:
{
"type": "res",
"id": "req_abc123",
"ok": false,
"error": {
"code": "INVALID_SESSION",
"message": "Session not found"
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
"res" |
是 | 帧类型辨识字段 |
id |
string | 是 | 与对应 req.id 一致 |
ok |
boolean | 是 | 成功标志 |
payload |
any | 否 | 响应数据(ok: true 时) |
error |
object | 否 | 错误详情(ok: false 时) |
{
"type": "event",
"event": "chat.delta",
"payload": { ... },
"seq": 42
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
type |
"event" |
是 | 帧类型辨识字段 |
event |
string | 是 | 事件名称 |
payload |
any | 否 | 事件特定数据 |
seq |
integer | 否 | 单调递增序列号,用于排序/去重 |
客户端 服务端
| |
| ---- 1. WebSocket upgrade ---|->
| |
| <--- 2. Connected ---------- | (协议已接受)
| |
| ---- 3. connect req --------|->
| { auth, device, scopes } |
| |
| <--- 4. hello-ok res ------- | (鉴权结果 + 特性列表)
| |
| ==== 5. 正常业务流量 ========|
| |
第 1 步: 客户端向 /ws 发起 WebSocket 连接。
第 2 步: 服务端接受 upgrade。如果协议版本不匹配,以 code 1002 关闭连接。
第 3 步: 客户端 必须 将 connect 请求作为第一条 req 帧发送:
{
"type": "req",
"id": "conn_1",
"method": "connect",
"params": {
"protocol_version": 1,
"client": {
"id": "web",
"version": "1.0.0"
},
"auth": {
"token": "syscity_shared_token_xxx"
},
"device": {
"id": "device_abc",
"public_key": "...",
"signature": "...",
"nonce": "..."
},
"scopes": ["chat", "read"]
}
}第 4 步: 服务端回复 hello-ok 或 connect.error:
{
"type": "res",
"id": "conn_1",
"ok": true,
"payload": {
"protocol_version": 1,
"session_key": "web:web_user",
"features": ["chat", "canvas", "tools"],
"scopes_granted": ["chat", "read"]
}
}鉴权失败时:
{
"type": "res",
"id": "conn_1",
"ok": false,
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or missing authentication"
}
}客户端应定期发送 ping 请求:
{ "type": "req", "id": "p_1", "method": "ping" }服务端回复:
{ "type": "res", "id": "p_1", "ok": true, "payload": {} }若 60 秒内未收到任何帧,任一方可主动关闭连接。
意外断开后,客户端应:
- 指数退避:起始 800ms,上限 15s,乘数 1.7x
- 使用相同的
device.id重新发送connect(若已配对) - 重新订阅之前活跃的 sessions
通过 syscity.yaml 中的 gateway.auth.mode 配置:
| 模式 | 说明 |
|---|---|
none |
无鉴权。仅用于本地开发。 |
token |
共享 secret token(通过 SYSCITY_GATEWAY_TOKEN 环境变量或配置) |
device |
需要设备配对。新设备须经管理员批准。 |
tailscale |
通过 Tailscale identity header 自动鉴权 |
共享 Token 模式(最简单):
- 客户端在
connect参数中发送auth.token。 - 服务端与配置的共享 token 进行校验。
- 授予请求的作用域(或默认值)。
设备配对模式(推荐用于多端):
- 首次连接:客户端发送
device.id+device.public_key+ 签名。 - 服务端检查设备是否已配对。
- 若未配对:
- 服务端生成一个短配对码(如
A3F7K)。 - 向管理员客户端广播
device.pair.requested事件。 - 管理员通过 CLI 执行
syscity device approve <code>。
- 服务端生成一个短配对码(如
- 批准后,服务端向客户端颁发 设备 token。
- 后续重连使用设备 token,无需再使用共享 token。
所有 API 方法均受作用域保护。默认拒绝:无作用域 = 无权限。
| 作用域 | 可访问接口 |
|---|---|
chat |
chat.send, chat.history, chat.abort |
read |
只读查询: sessions.list, agents.list 等 |
write |
创建/修改: sessions.create, sessions.delete |
admin |
完全访问(绕过所有作用域检查) |
pairing |
设备配对管理 |
作用域校验按方法执行。connect 请求声明请求的作用域;服务端授予「请求的作用域」与「允许的作用域」的交集。
这些方法对所有客户端(Web、App、CLI)开放,只需具备相应作用域。
| 方法 | 作用域 | 说明 |
|---|---|---|
chat.send |
chat |
向 Agent 发送消息 |
chat.history |
read |
获取某个 session 的对话历史 |
chat.abort |
chat |
中止当前生成 |
ask.respond |
chat |
回答 ask_user 工具挂起的问题(human-in-the-loop) |
| 方法 | 作用域 | 说明 |
|---|---|---|
sessions.list |
read |
列出活跃 sessions |
sessions.create |
write |
创建新 session |
sessions.delete |
write |
删除 session |
sessions.reset |
write |
清空 session 上下文 |
sessions.subscribe |
read |
订阅 session 事件 |
sessions.unsubscribe |
read |
取消订阅 session 事件 |
| 方法 | 作用域 | 说明 |
|---|---|---|
agents.list |
read |
列出可用 Agents |
agents.get |
read |
获取 Agent 详情 |
只读访问某个 Agent 的工作区目录,供 Web UI 文件浏览器使用。客户端路径始终相对于解析后的工作区根目录,并经过遍历防护校验(段级校验 + 规范化前缀比对,拒绝绝对路径、..、反斜杠与符号链接逃逸)。agent_id 缺省/空/"default" 解析为默认 Agent 的共享工作区;未知 Agent 返回 AGENT_NOT_FOUND。
| 方法 | 作用域 | 说明 |
|---|---|---|
workspace.list |
read |
列出目录内容(每目录最多 1000 条;目录在前,按名称排序) |
workspace.read |
read |
读取文本文件内容(超过 256 KiB 截断并置 truncated: true;二进制文件返回 binary: true 且无 content) |
workspace.list 参数:{ agent_id?, path? };响应含 root、path、entries(每项为 name / path / kind / size / modified)。工作区目录不存在时返回空列表而非错误。
workspace.read 参数:{ agent_id?, path }(path 必填);响应含 path、size、truncated、binary、content(仅文本)。路径越界返回 PATH_FORBIDDEN,不存在返回 NOT_FOUND。
| 方法 | 作用域 | 说明 |
|---|---|---|
health |
(无) | 快速健康检查 |
system.presence |
read |
Gateway 在线/存在状态 |
| 事件 | 说明 |
|---|---|
chat.delta |
流式内容分片 |
chat.final |
生成完成 |
chat.error |
生成错误 |
tool.calling |
工具执行开始 |
tool.result |
工具执行完成 |
agent.thinking |
Agent 正在生成(打字指示器) |
session.created |
自动创建了新 session |
device.pair.requested |
新设备等待批准 |
ask.required |
ask_user 工具挂起等待人类回答(payload: ask_id, session_id, question, options, required, default) |
ask.resolved |
问题已被回答或取消(payload: ask_id, cancelled) |
这些操作从 WebSocket 协议和 HTTP 面中 移除。仅通过 syscity CLI 二进制可用。
# Agent 管理
syscity agent list
syscity agent create --name myagent --model gpt-4
syscity agent delete <id>
# Provider 管理
syscity provider list
syscity provider enable <id>
syscity provider disable <id>
syscity provider switch <alias>
# Plugin 管理
syscity plugin list
syscity plugin reload
syscity plugin enable/disable <id>
# Skill 管理
syscity skill list
syscity skill run <id>
# Cron 管理
syscity cron list
syscity cron add --schedule "0 9 * * *" --prompt "Daily summary"
syscity cron remove/enable/disable <id>
# Memory 管理
syscity memory search <query>
syscity memory add --content "..."
# 配置
syscity config get
syscity config set key=value
syscity config validate
# 初始化配置(交互式向导)
syscity setup
# 引导用户完成:
# - 选择鉴权模式(none/token/device)
# - 设置共享 token / 密码
# - 配置默认模型和 Provider
# - 设置数据目录路径
# - 启用/禁用内置 Channel(web/cli/telegram 等)
# - 生成初始 `syscity.yaml` 并写入磁盘
# 设备配对
syscity device list
syscity device approve <code>
syscity device revoke <id>
# 审批(高危工具)
syscity approval list
syscity approval approve/deny <id>
# 审计
syscity audit log
# 安全
syscity security gate set <user> <level>
syscity security pairing list
syscity security pairing approve <channel> <code>理由: 管理操作仅限管理员、频率低、需要严格校验。保留在 CLI 中可确保:
- Web UI 不会误触管理操作。
- 前端代码更简洁(无需管理后台)。
- 便于通过 shell 脚本自动化。
- 降低 Web 面的攻击面。
assistant-ui 支持自定义 transport。Syscity 提供 SyscityWebSocketTransport 适配器,负责:
- 管理 WebSocket 连接(连接、重连、鉴权)。
- 将
assistant-ui的消息流映射到chat.send+chat.delta/chat.final事件。 - 通过
tool.calling/tool.result事件暴露工具调用。 - 管理 session 生命周期(创建、重置、切换)。
| Assistant-UI 概念 | Syscity 协议 |
|---|---|
Message |
chat.final payload |
TextStreamPart |
chat.delta 事件 |
ToolCall |
tool.calling 事件 |
ToolResult |
tool.result 事件 |
Thread |
Session |
CreateThread |
sessions.create |
DeleteThread |
sessions.delete |
AppendMessage |
chat.send |
CancelRun |
chat.abort |
Session key 统一采用 {channel}:{user_id} 格式:
- Web:
web:{device_id}(从设备身份派生) - App (iOS/Android):
ios:{device_id}/android:{device_id} - CLI:
cli:{device_id}
当调用 chat.send 且未显式指定 session 时,服务端自动创建或复用由 channel + user_id 派生的 session。
- 定稿
docs/protocol.md。 - 评审并冻结方法/事件名称。
- 实现新的
req/res/event帧处理器。 - 实现
connect握手 + 鉴权 + 作用域校验。 - 在 WebSocket 上实现所有 普通使用 API 方法。
- 废弃(但保留)现有的 REST API。
- 将所有 管理 API 命令加入
syscityCLI。 - 从 REST 面中移除管理端点。
- 确保配置持久化到磁盘(而非仅内存)。
- 用
assistant-ui替换现有 Web 前端。 - 实现
SyscityWebSocketTransport。 - 移除所有 Admin UI 组件。
- 移除废弃的 REST 端点。
- 移除 SSE handler。
- 移除旧前端代码。
- 更新文档。
| 错误码 | HTTP 等价 | 含义 |
|---|---|---|
UNAUTHORIZED |
401 | 鉴权缺失或无效 |
FORBIDDEN |
403 | 作用域不足 |
INVALID_REQUEST |
400 | 请求参数错误 |
METHOD_NOT_FOUND |
404 | 未知方法名 |
SESSION_NOT_FOUND |
404 | Session 不存在 |
AGENT_NOT_FOUND |
404 | Agent 不存在 |
RATE_LIMITED |
429 | 请求过于频繁 |
REVISION_CONFLICT |
409 | 配置乐观锁冲突:config.set 携带的 base_revision 已过期,payload 含 current_revision / expected_revision,重新 config.get 后重试 |
PATH_FORBIDDEN |
403 | 路径越出允许的根目录(如 workspace 浏览) |
INTERNAL_ERROR |
500 | 服务端错误 |
- 协议版本为整数(从 1 开始)。
- 服务端接受
protocol_version <= server_version的连接。 - 若客户端版本过低,服务端回复
connect.error,错误码为VERSION_MISMATCH。
迁移窗口期间:
- 现有 REST API 和 SSE 端点继续运行,但标记为废弃。
- REST 端点被调用时记录废弃警告日志。
- 目标在 v2.0 中彻底移除。
| 维度 | 之前(当前) | 之后(本规范) |
|---|---|---|
| 传输层 | REST + SSE + WS | WebSocket-native |
| 鉴权 | OAuth2 / 无 / token | 统一: token + 设备配对 + 作用域 |
| Web 前端 | 自建 React | assistant-ui |
| 配置 UI | Web dashboard + REST | 仅 CLI |
| 多端接入 | 每个客户端单独集成 | 一套协议走天下 |
| Session Key | 随机 UUID | {channel}:{user_id} |
| 事件推送 | SSE 广播 | WebSocket event 帧 |
| API 数量 | 100+ REST 端点 | ~15 个 WS 方法 + CLI 命令 |
| 管理面 | Web + REST | 仅 CLI |