feat: 设备鉴权迁移至客户端 + 插件卸载保护

安全修复(客户端鉴权):
- remotedevice 服务端移除授权状态存储(authorized map/SetAuthorized/handleDeviceAuth)
- DeviceMeta.Authorized 改为设备 hello 自报,服务端仅透传展示
- device_ctl_* 工具移除服务端授权检查,无条件转发,设备端自行决定是否执行
- 共享设备桥库 Bridge 新增本地 authorized 状态,未授权收到 cmd 直接拒绝
- waiter: --device-authorized / device_authorized 配置控制本地授权
- GUI: 授权存 gui-prefs 本地文件;设备页仅本机可切换开关
- webui /device/auth 旧路径返回 410 Gone
- 根因:agent 可经 config_set 篡改服务端授权配置自行授权设备

插件管理强化:
- 内置插件禁止卸载(IsBuiltinPlugin + 409),外部插件卸载即时生效
- 卸载不存在插件返回 404;移除误导性 reload_required 提示
- webui 插件路由:名称白名单校验防路径穿越、保留字路径保护
This commit is contained in:
JianFeeeee
2026-08-24 19:26:11 +08:00
parent 5163ce51a7
commit ba5785036a
21 changed files with 616 additions and 383 deletions

View File

@ -12,10 +12,9 @@ import (
// devicectlDevice 把设备网关暴露为 IOManager 的一个 Device
// Tools() 提供 devicedetect / device_ctl_status / device_ctl_cmdrun / device_ctl_cmdresult / screensee
// Execute() 检查授权并路由到 WS 在线设备
// Execute() 推送命令到设备,设备端自行鉴权(客户端存储授权)
type devicectlDevice struct {
reg *Registry
persist func() // 授权变更后持久化
reg *Registry
// screensee 回调:设备截屏回传后由 agent 核心消费(视觉描述)。
// 由插件 Start 注入nil 时退化为仅返回 base64 数据。
@ -121,9 +120,9 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef {
"properties": map[string]interface{}{
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
"action": map[string]interface{}{
"type": "string",
"type": "string",
"description": "操作类型click(单击) / doubleclick(双击) / rightclick(右键) / move(移动) / scroll(滚动) / keypress(按键) / type(输入文字)",
"enum": []interface{}{"click", "doubleclick", "rightclick", "move", "scroll", "keypress", "type"},
"enum": []interface{}{"click", "doubleclick", "rightclick", "move", "scroll", "keypress", "type"},
},
"x": map[string]interface{}{"type": "integer", "description": "鼠标 X 坐标像素。click/doubleclick/rightclick/move 必填"},
"y": map[string]interface{}{"type": "integer", "description": "鼠标 Y 坐标像素。click/doubleclick/rightclick/move 必填"},
@ -257,9 +256,7 @@ func (d *devicectlDevice) status(args map[string]interface{}) (interface{}, erro
if !ok {
return nil, fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权,无法查询状态(需先在设备管理页或经 bind 授权)", id)
}
// 服务端不检查授权;设备端收到请求后自行决定是否执行。
if !m.Online {
return publicDevices([]DeviceMeta{m}), nil // 带 offline=true
}
@ -277,9 +274,7 @@ func (d *devicectlDevice) cmdrun(args map[string]interface{}) (interface{}, erro
if !ok {
return nil, fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权,无法执行命令(请先在设备管理页授权)", id)
}
// 服务端不检查授权;设备端收到请求后自行决定是否执行。
if !m.Online {
return nil, fmt.Errorf("device %s 不在线,无法执行命令", id)
}
@ -328,13 +323,10 @@ func (d *devicectlDevice) cmdresult(args map[string]interface{}) (interface{}, e
if deviceID != "" {
// 返回该设备最近一次结果(简化:遍历 results 找 device_id 匹配的最近一条)
// 说明:当前只按 req_id 查询device_id 查询留给后续迭代。
m, ok := d.reg.Get(deviceID)
_, ok := d.reg.Get(deviceID)
if !ok {
return nil, fmt.Errorf("device %s 不存在", deviceID)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权", deviceID)
}
return map[string]interface{}{"device_id": deviceID, "note": "请用 device_ctl_cmdrun 返回的 req_id 查询命令结果"}, nil
}
return nil, fmt.Errorf("req_id 或 device_id 至少提供一个")
@ -350,9 +342,6 @@ func (d *devicectlDevice) info(args map[string]interface{}) (interface{}, error)
if !ok {
return nil, fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权,无法探查信息(请先在设备管理页授权)", id)
}
out := map[string]interface{}{
"device_id": m.DeviceID,
"name": m.Name,
@ -385,9 +374,7 @@ func (d *devicectlDevice) screensee(args map[string]interface{}) (interface{}, e
if !ok {
return nil, fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权,无法查看屏幕(请先在设备管理页授权)", id)
}
// 服务端不检查授权;设备端收到请求后自行决定是否执行。
if !m.Online {
return nil, fmt.Errorf("device %s 不在线", id)
}
@ -436,9 +423,7 @@ func (d *devicectlDevice) computeruse(args map[string]interface{}) (interface{},
if !ok {
return nil, fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return nil, fmt.Errorf("device %s 未授权,无法远程操控(请先在设备管理页授权)", id)
}
// 服务端不检查授权;设备端收到请求后自行决定是否执行。
if !m.Online {
return nil, fmt.Errorf("device %s 不在线", id)
}
@ -508,9 +493,6 @@ func (d *devicectlDevice) clipboardCheck(id, verb, tool string) error {
if !ok {
return fmt.Errorf("device %s 不存在", id)
}
if !m.Authorized {
return fmt.Errorf("device %s 未授权,无法%s剪切板请先在设备管理页授权", id, verb)
}
if !m.Online {
return fmt.Errorf("device %s 不在线", id)
}