# 魔云腾 — AI 知识库 > 本文档供 AI 工具自动学习使用。内容涵盖 MYTOS 容器安卓、云手机、CECS 云平台等产品技术文档。 > 由魔云腾 AI 工单处理系统自动生成,每次访问均为最新内容。 ## 文档目录 - [MYTOS-内网设备扫码发现协议](#mytos-内网设备扫码发现协议) — 魔云腾设备发现协议规范 - [MYT-AI网关接口功能](#myt-ai网关接口功能) — MYT AI Gateway — 完整 API 文档 - [线上机型数据库查询下载API](#线上机型数据库查询下载api) — 机型模板查询 API - [MYTOS Android WebRTC投屏](#mytos-android-webrtc投屏) — Android WebRTC投屏接口文档,端口映射、播放器调用参数 - [MYTOS Android RPA ](#mytos-android-rpa-) — MYTOS Android RPA SDK 开发文档,Python/Go语言API、端口映射、设备控制、截图、UI节点操作 - [MYTOS Android API](#mytos-android-api) — MYTOS Android API 接口文档,包含设备管理、文件操作、网络配置、端口映射等 - [盒子-SDK-API](#盒子-sdk-api) — 盒子SDK API - AI快速参考 - [盒子-SDK-云机备份与镜像接口](#盒子-sdk-云机备份与镜像接口) — V1/V2云机导入导出、备份压缩文件管理、机型数据备份等21个接口 - [授权管理-查询设备授权状态](#授权管理-查询设备授权状态) — 通过魔云腾云平台API查询设备每个坑位的授权到期状态 - [授权管理-续费设备坑位](#授权管理-续费设备坑位) — 通过魔云腾云平台API为设备坑位续费,包括登录、查询套餐、创建订单、扫码支付 - [授权管理-同步授权到设备](#授权管理-同步授权到设备) — 将授权信息同步到魔云腾ARM算力服务器设备,包括登录API和UDP推送 --- ## MYTOS-内网设备扫码发现协议 *分类: 设备API* ## MYT AI Gateway — API 文档 Base URL: `https://ai.opencecs.com` 协议:HTTPS(HTTP/2),所有非 TLS 请求会被拒绝 --- ### 认证方式 #### API Token 认证 除明确标注无需认证的接口外,所有请求需在 Header 中携带 API Token: ``` Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Token 格式:`myt-` + 48 位十六进制字符,通过 `/v1/auth/login` 获取。 #### 账号密码认证 认证接口(`/v1/auth/*`)使用墨云腾平台账号密码,非 API Token。 --- ### 错误响应格式 #### 网关接口统一格式 `/v1/models`、`/v1/chat/completions` 遵循 OpenAI 错误格式: ```json { "error": { "message": "错误描述", "type": "错误类型", "param": null, "code": null } } ``` | type | 说明 | |------|------| | `authentication_error` | 认证相关(Token 缺失 / 无效 / 用户被封禁) | | `insufficient_quota` | 配额相关(未核算 / 已用完) | | `invalid_request_error` | 请求参数错误 | | `rate_limit_error` | 限速(提交过于频繁) | | `model_not_found` | 模型不存在或已停用 | | `server_error` | 服务端错误 / 后端不可用 | #### 认证接口格式 `/v1/auth/*` 使用以下格式: ```json { "success": false, "message": "错误描述" } ``` --- ### 1. 认证接口 #### 1.1 登录获取 API Token ``` POST /v1/auth/login ``` 使用墨云腾平台账号密码登录,获取 API Token。同一用户重复登录会复用已有活跃 Token。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | username | string | 是 | 墨云腾平台账号 | | password | string | 是 | 墨云腾平台密码 | ```json { "username": "15071042575", "password": "your_password" } ``` ##### 响应 **成功** (200) ```json { "success": true, "token": "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "message": "登录成功,本月配额: 2000000 Token" } ``` **失败** | HTTP 状态码 | message | 说明 | |-------------|---------|------| | 400 | 请求格式错误 | JSON 解析失败 | | 400 | 账号和密码不能为空 | 字段缺失 | | 401 | 账号或密码错误 | SSO 认证失败 | | 403 | 账号已被锁定 | SSO 返回 403 | | 403 | 账号无有效设备,无法使用AI服务 | 无有效坑位 | | 500 | 内部错误 | 服务端异常 | --- #### 1.2 重置 API Token ``` POST /v1/auth/reset-token ``` 重置后旧 Token 立即失效,生成新 Token。配额不受影响。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 同登录接口。 ##### 响应 **成功** (200) ```json { "success": true, "token": "myt-新的48位hex字符串", "message": "Token重置成功,本月配额: 2000000 Token" } ``` --- #### 1.3 查询配额 ``` POST /v1/auth/quota ``` 查询当月 Token 配额使用情况,同时会刷新坑位数和配额。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 同登录接口。 ##### 响应 **成功** (200) ```json { "success": true, "moyunteng_uid": "uid字符串", "total_quota": 2000000, "used_quota": 50000, "remaining_quota": 1950000 } ``` | 字段 | 类型 | 说明 | |------|------|------| | total_quota | int | 本月总配额 | | used_quota | int | 本月已使用 | | remaining_quota | int | 本月剩余(total - used) | **用户未注册** (404) ```json { "success": false, "message": "用户未注册,请先登录获取API Token" } ``` --- ### 2. 模型列表 #### 2.1 获取可用模型 ``` GET /v1/models ``` 无需认证。返回当前可用的所有模型。 ##### 响应 ```json { "object": "list", "data": [ { "id": "deepseek-r1", "object": "model", "created": 0, "owned_by": "MYT" } ] } ``` | 字段 | 说明 | |------|------| | id | 模型名称,用于 `/v1/chat/completions` 的 `model` 参数 | --- ### 3. 聊天补全 #### 3.1 创建补全 ``` POST /v1/chat/completions ``` 支持流式(SSE)和非流式两种响应模式,完全兼容 OpenAI Chat Completions API。 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | model | string | 是 | 模型名称,取自 `/v1/models` 返回的 `id` | | messages | array | 是 | 消息数组,见下方格式 | | stream | boolean | 否 | 是否流式响应,默认 `false` | | stream_options | object | 否 | 流式选项,`{"include_usage": true}` 可在最后一个 chunk 中返回 usage | | max_tokens | integer | 否 | 最大生成 token 数 | | temperature | float | 否 | 温度,0~2 | | top_p | float | 否 | Top-P 采样 | | presence_penalty | float | 否 | 存在惩罚,-2~2 | | frequency_penalty | float | 否 | 频率惩罚,-2~2 | | n | integer | 否 | 生成候选数,默认 1 | | stop | string/array | 否 | 停止词 | | response_format | object | 否 | 响应格式,如 `{"type": "json_object"}` | | seed | integer | 否 | 随机种子,用于可复现输出 | | logprobs | boolean | 否 | 是否返回 log 概率 | | top_logprobs | integer | 否 | 返回 top-N log 概率(0~20) | | tools | array | 否 | 函数调用工具定义 | | tool_choice | any | 否 | 工具选择策略 | | parallel_tool_calls | boolean | 否 | 是否允许并行工具调用,默认 true | **消息格式** ```json { "role": "system|user|assistant|tool", "content": "文本内容 或 多模态数组", "name": "参与者名称(可选)", "tool_calls": [{"id":"...","type":"function","function":{"name":"...","arguments":"..."}}], "tool_call_id": "工具调用ID(tool 角色必填)" } ``` **content 多模态格式** ```json "content": [ {"type": "text", "text": "描述这张图片"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} ] ``` > 所有请求参数原样透传给后端模型,网关仅替换 `model` 为实际后端模型名。当后端模型返回 `reasoning_content` 字段时,网关会原样转发给客户端。 ##### 请求示例 **基本对话** ```json { "model": "deepseek-r1", "messages": [ {"role": "system", "content": "你是一个有帮助的助手"}, {"role": "user", "content": "解释量子计算的基本原理"} ], "stream": true } ``` **函数调用** ```json { "model": "gpt-4o", "messages": [ {"role": "user", "content": "北京今天天气如何?"} ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } } ], "tool_choice": "auto" } ``` ##### 流式响应 (stream=true) **响应头** ``` Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive ``` **SSE 数据行** ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` **含推理内容的流式响应**(后端返回 `reasoning_content` 时) ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"deepseek-r1","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"让我想想..."},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"deepseek-r1","choices":[{"index":0,"delta":{"content":"根据分析..."},"finish_reason":null}]} data: [DONE] ``` **工具调用流式响应** ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_xxx","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]} data: [DONE] ``` ##### 非流式响应 (stream=false) ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "deepseek-r1", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "量子计算利用量子力学的叠加和纠缠...", "refusal": null, "logprobs": null }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 25, "completion_tokens": 150, "total_tokens": 175 } } ``` **含推理内容的非流式响应** ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "deepseek-r1", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "reasoning_content": "让我逐步分析这个问题...", "content": "根据以上分析...", "refusal": null, "logprobs": null }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 25, "completion_tokens": 500, "total_tokens": 525 } } ``` **工具调用非流式响应** ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "gpt-4o", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } } ], "refusal": null, "logprobs": null }, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70 } } ``` ##### 错误码 | HTTP 状态码 | type | 说明 | |-------------|------|------| | 400 | `invalid_request_error` | 请求格式错误、未指定模型、messages 为空 | | 401 | `authentication_error` | Token 缺失 / 无效 / 用户被封禁 | | 403 | `insufficient_quota` | 本月配额未核算,请重新登录 | | 404 | `model_not_found` | 模型不存在或已停用 | | 413 | `invalid_request_error` | 请求体超过 10MB 限制 | | 429 | `insufficient_quota` | 本月配额已用完 | | 500 | `server_error` | 内部错误 | | 502 | `server_error` | 后端服务不可用 | | 503 | `server_error` | 无可用后端 | --- ### 4. Agent 安装包 #### 4.1 获取安装包列表 ``` GET /v1/agent/packages ``` 无需认证。返回所有可用的 Agent 安装包。 ##### 响应 ```json [ { "id": 1, "version": "1.2.3", "filename": "myt-agent-1.2.3.tar.gz", "file_size": 12345678, "md5": "aabbccdd...", "created_at": "2025-01-01T00:00:00Z" } ] ``` | 字段 | 类型 | 说明 | |------|------|------| | id | integer | 安装包 ID | | version | string | 语义化版本号 | | filename | string | 文件名 | | file_size | integer | 文件大小(字节) | | md5 | string | MD5 校验值 | | created_at | string | 上传时间 | --- ### 5. 技能仓库 #### 5.1 获取技能索引 ``` GET /.well-known/skills/index.json ``` 无需认证。Hermes 原生兼容协议。返回所有可用技能列表。 ##### 响应 ```json { "skills": [ { "name": "weather-query", "description": "查询天气信息", "files": ["SKILL.md", "references/api-schema.json"], "tags": ["weather", "api"], "version": "1.0.0", "author": "admin", "updated_at": "2025-05-01T12:00:00Z" } ] } ``` | 字段 | 类型 | 说明 | |------|------|------| | name | string | 技能名称 | | description | string | 技能描述 | | files | string[] | 技能包含的文件列表 | | tags | string[] | 标签 | | version | string | 版本号 | | author | string | 作者 | | updated_at | string | 最后更新时间 | --- ### 6. 健康检查 ``` GET /health ``` 无需认证。返回服务健康状态。 ##### 响应 ```json { "status": "ok" } ``` --- ### 配额说明 #### 配额计算 - 每个有效坑位赠送 **1,000,000 Tokens** 月配额 - 坑位来自墨云腾平台未过期设备(`state == 0` 且 `extime > 当前时间` 的子设备数) - 配额只增不减:新增坑位增加配额,减少坑位不降低已核算配额 - 每月 1 日 00:00 自动重置,用户下次登录时重新核算 #### Token 消耗 | 接口 | 消耗 | |------|------| | `/v1/chat/completions` | 按实际 `prompt_tokens + completion_tokens` 消耗 | #### Token 统计优先级 1. 后端返回的精确 `usage` 数据(通过 `stream_options.include_usage=true` 获取) 2. tiktoken 估算(GPT-4 编码器) --- ### 完整对接示例 #### Python — OpenAI SDK(推荐) 网关完全兼容 OpenAI API,可直接使用官方 SDK,只需修改 `base_url`: ```python from openai import OpenAI client = OpenAI( api_key="myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", base_url="https://ai.opencecs.com/v1" ) # 非流式 response = client.chat.completions.create( model="deepseek-r1", messages=[{"role": "user", "content": "你好"}], stream=False ) print(response.choices[0].message.content) # 流式 stream = client.chat.completions.create( model="deepseek-r1", messages=[{"role": "user", "content": "解释量子计算"}], stream=True ) for chunk in stream: delta = chunk.choices[0].delta if delta.content: print(delta.content, end="") if hasattr(delta, "reasoning_content") and delta.reasoning_content: print(f"[思考] {delta.reasoning_content}", end="") print() # 函数调用 response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "北京今天天气如何?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "获取指定城市天气", "parameters": { "type": "object", "properties": {"city": {"type": "string", "description": "城市名"}}, "required": ["city"] } } }], tool_choice="auto" ) print(response.choices[0].message.tool_calls) ``` #### Node.js — OpenAI SDK ```javascript import OpenAI from "openai"; const client = new OpenAI({ apiKey: "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", baseURL: "https://ai.opencecs.com/v1", }); // 非流式 const response = await client.chat.completions.create({ model: "deepseek-r1", messages: [{ role: "user", content: "你好" }], stream: false, }); console.log(response.choices[0].message.content); // 流式 const stream = await client.chat.completions.create({ model: "deepseek-r1", messages: [{ role: "user", content: "解释量子计算" }], stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0].delta; if (delta.content) process.stdout.write(delta.content); } console.log(); ``` #### cURL — 非流式聊天 ```bash curl -X POST https://ai.opencecs.com/v1/chat/completions \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1", "messages": [{"role": "user", "content": "Hello"}], "stream": false }' ``` #### cURL — 流式聊天 ```bash curl -X POST https://ai.opencecs.com/v1/chat/completions \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1", "messages": [{"role": "user", "content": "Hello"}], "stream": true }' ``` --- ### 限速规则汇总 | 接口 | 限速策略 | 超限响应 | |------|----------|----------| | `/v1/auth/*` | 同一 IP 每分钟 10 次 | 429 + `{"success":false,"message":"请求过于频繁,请稍后再试"}` | | `/v1/chat/completions` | 无 IP 限速,仅配额限制 | 429 + `insufficient_quota` | | `/v1/models` | 无 | — | | `/v1/agent/packages` | 无 | — | | `/.well-known/skills/index.json` | 无 | — | --- ### 请求体大小限制 | 接口 | 限制 | |------|------| | `/v1/chat/completions` | 10 MB | | 其他接口 | 无显式限制 | --- ## MYT-AI网关接口功能 *分类: Container API* ## MYT AI Gateway — 完整 API 文档 Base URL: `https://ai.opencecs.com` 协议:HTTPS(HTTP/2),所有非 TLS 请求会被拒绝 --- ### 认证方式 #### API Token 认证 除明确标注无需认证的接口外,所有请求需在 Header 中携带 API Token: ``` Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Token 格式:`myt-` + 48 位十六进制字符,通过 `/v1/auth/login` 获取。 #### 账号密码认证 认证接口(`/v1/auth/*`)使用墨云腾平台账号密码,非 API Token。 --- ### 错误响应格式 #### 网关接口统一格式 `/v1/models`、`/v1/chat/completions`、`/v1/images/*` 遵循 OpenAI 错误格式: ```json { "error": { "message": "错误描述", "type": "错误类型", "param": null, "code": null } } ``` | type | 说明 | |------|------| | `authentication_error` | 认证相关(Token 缺失 / 无效 / 用户被封禁) | | `insufficient_quota` | 配额相关(未核算 / 已用完) | | `invalid_request_error` | 请求参数错误、模型不存在 | | `rate_limit_error` | 限速(提交过于频繁) | | `server_error` | 服务端错误 / 后端不可用 | | `model_not_found` | 模型不存在或已停用 | #### 认证接口格式 `/v1/auth/*` 使用以下格式: ```json { "success": false, "message": "错误描述" } ``` --- ### 1. 认证接口 #### 1.1 登录获取 API Token ``` POST /v1/auth/login ``` 使用墨云腾平台账号密码登录,获取 API Token。同一用户重复登录会复用已有活跃 Token。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | username | string | 是 | 墨云腾平台账号 | | password | string | 是 | 墨云腾平台密码 | ```json { "username": "15071042575", "password": "your_password" } ``` ##### 响应 **成功** (200) ```json { "success": true, "token": "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "message": "登录成功,本月配额: 2000000 Token" } ``` **失败** | HTTP 状态码 | message | 说明 | |-------------|---------|------| | 400 | 请求格式错误 | JSON 解析失败 | | 400 | 账号和密码不能为空 | 字段缺失 | | 401 | 账号或密码错误 | SSO 认证失败 | | 403 | 账号已被锁定 | SSO 返回 403 | | 403 | 账号无有效设备,无法使用AI服务 | 无有效坑位 | | 500 | 内部错误 | 服务端异常 | --- #### 1.2 重置 API Token ``` POST /v1/auth/reset-token ``` 重置后旧 Token 立即失效,生成新 Token。配额不受影响。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 同登录接口。 ##### 响应 **成功** (200) ```json { "success": true, "token": "myt-新的48位hex字符串", "message": "Token重置成功,本月配额: 2000000 Token" } ``` --- #### 1.3 查询配额 ``` POST /v1/auth/quota ``` 查询当月 Token 配额使用情况,同时会刷新坑位数和配额。 **限速**:同一 IP 每分钟最多 10 次 ##### 请求 同登录接口。 ##### 响应 **成功** (200) ```json { "success": true, "moyunteng_uid": "uid字符串", "total_quota": 2000000, "used_quota": 50000, "remaining_quota": 1950000 } ``` | 字段 | 类型 | 说明 | |------|------|------| | total_quota | int | 本月总配额 | | used_quota | int | 本月已使用 | | remaining_quota | int | 本月剩余(total - used) | **用户未注册** (404) ```json { "success": false, "message": "用户未注册,请先登录获取API Token" } ``` --- ### 2. 模型列表 #### 2.1 获取可用模型 ``` GET /v1/models ``` 无需认证。返回当前可用的所有模型。 ##### 响应 ```json { "object": "list", "data": [ { "id": "MYT-LLM", "object": "model", "owned_by": "MYT" }, { "id": "MYT-VLM", "object": "model", "owned_by": "MYT" } ] } ``` | 字段 | 说明 | |------|------| | id | 模型名称,用于 `/v1/chat/completions` 的 `model` 参数 | **模型说明** | 模型 | 类型 | 说明 | |------|------|------| | `MYT-LLM` | 文本 | 纯文本对话,支持思考模式 | | `MYT-VLM` | 图片 | 多模态对话,支持图片输入(`image_url`) | --- ### 3. 聊天补全 #### 3.1 创建补全 ``` POST /v1/chat/completions ``` 支持流式(SSE)和非流式两种响应模式,完全兼容 OpenAI Chat Completions API。 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | model | string | 是 | 模型名称,取自 `/v1/models` 返回的 `id` | | messages | array | 是 | 消息数组,见下方格式 | | stream | boolean | 否 | 是否流式响应,默认 `false` | | stream_options | object | 否 | 流式选项,`{"include_usage": true}` 可在最后一个 chunk 中返回 usage | | max_tokens | integer | 否 | 最大生成 token 数 | | temperature | float | 否 | 温度,0~2 | | top_p | float | 否 | Top-P 采样 | | presence_penalty | float | 否 | 存在惩罚,-2~2 | | frequency_penalty | float | 否 | 频率惩罚,-2~2 | | n | integer | 否 | 生成候选数,默认 1 | | stop | string/array | 否 | 停止词 | | response_format | object | 否 | 响应格式,如 `{"type": "json_object"}` | | seed | integer | 否 | 随机种子,用于可复现输出 | | logprobs | boolean | 否 | 是否返回 log 概率 | | top_logprobs | integer | 否 | 返回 top-N log 概率(0~20) | | tools | array | 否 | 函数调用工具定义 | | tool_choice | any | 否 | 工具选择策略 | | parallel_tool_calls | boolean | 否 | 是否允许并行工具调用,默认 true | | thinking | object | 否 | 思考/推理配置,见下方说明 | **消息格式** ```json { "role": "system|user|assistant|tool", "content": "文本内容 或 多模态数组", "reasoning_content": "推理内容(仅 assistant 角色,可选)", "name": "参与者名称(可选)", "tool_calls": [{"id":"...","type":"function","function":{"name":"...","arguments":"..."}}], "tool_call_id": "工具调用ID(tool 角色必填)" } ``` **content 多模态格式**(仅图片模型支持) ```json "content": [ {"type": "text", "text": "描述这张图片"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}} ] ``` 若模型不支持图片,`image_url` 类型内容会被自动过滤。 **thinking 配置** ```json { "type": "enabled", "budget_tokens": 10000 } ``` | 字段 | 说明 | |------|------| | type | `"enabled"` 启用思考,`"disabled"` 禁用思考 | | budget_tokens | 思考预算 token 数(部分后端模型要求) | > **注意**:思考模式由模型配置决定。若模型启用了思考,无论请求中 `thinking.type` 设置为何值,网关都会强制启用;若模型未启用思考,则会强制禁用并剥离推理内容。 ##### 请求示例 **基本对话** ```json { "model": "deepseek-r1", "messages": [ {"role": "system", "content": "你是一个有帮助的助手"}, {"role": "user", "content": "解释量子计算的基本原理"} ], "stream": true } ``` **带思考模式** ```json { "model": "deepseek-r1", "messages": [ {"role": "user", "content": "证明根号2是无理数"} ], "stream": true, "thinking": {"type": "enabled", "budget_tokens": 10000} } ``` **函数调用** ```json { "model": "gpt-4o", "messages": [ {"role": "user", "content": "北京今天天气如何?"} ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市天气", "parameters": { "type": "object", "properties": { "city": {"type": "string", "description": "城市名"} }, "required": ["city"] } } } ], "tool_choice": "auto" } ``` ##### 流式响应 (stream=true) **响应头** ``` Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive ``` **SSE 数据行** ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{"content":" world"},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","created":1234567890,"model":"deepseek-r1","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]} data: [DONE] ``` **思考模式流式响应**(模型启用思考时) ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"deepseek-r1","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"让我想想..."},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"deepseek-r1","choices":[{"index":0,"delta":{"content":"根据分析..."},"finish_reason":null}]} data: [DONE] ``` **工具调用流式响应** ``` data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","tool_calls":[{"index":0,"id":"call_xxx","type":"function","function":{"name":"get_weather","arguments":""}}]},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"city\":"}}]},"finish_reason":null}]} data: {"id":"chatcmpl-xxx","object":"chat.completion.chunk","model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}]} data: [DONE] ``` ##### 非流式响应 (stream=false) ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "deepseek-r1", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "量子计算利用量子力学的叠加和纠缠...", "refusal": null, "logprobs": null }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 25, "completion_tokens": 150, "total_tokens": 175 } } ``` **思考模式非流式响应** ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "deepseek-r1", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "reasoning_content": "让我逐步分析这个问题...", "content": "根据以上分析...", "refusal": null, "logprobs": null }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 25, "completion_tokens": 500, "total_tokens": 525 } } ``` **工具调用非流式响应** ```json { "id": "chatcmpl-xxx", "object": "chat.completion", "created": 1234567890, "model": "gpt-4o", "system_fingerprint": "", "choices": [ { "index": 0, "message": { "role": "assistant", "content": null, "tool_calls": [ { "id": "call_xxx", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\":\"北京\"}" } } ], "refusal": null, "logprobs": null }, "finish_reason": "tool_calls" } ], "usage": { "prompt_tokens": 50, "completion_tokens": 20, "total_tokens": 70 } } ``` ##### 错误码 | HTTP 状态码 | type | 说明 | |-------------|------|------| | 400 | `invalid_request_error` | 请求格式错误、未指定模型、messages 为空 | | 401 | `authentication_error` | Token 缺失 / 无效 / 用户被封禁 | | 403 | `insufficient_quota` | 本月配额未核算,请重新登录 | | 404 | `model_not_found` | 模型不存在或已停用 | | 413 | `invalid_request_error` | 请求体超过 10MB 限制 | | 429 | `insufficient_quota` | 本月配额已用完 | | 500 | `server_error` | 内部错误 | | 502 | `server_error` | 后端服务不可用 | | 503 | `server_error` | 无可用后端 | --- ### 4. 图片生成 #### 4.1 提交图片生成 ``` POST /v1/images/generations ``` 提交一个图片生成任务到队列,异步处理。每次调用消耗 **10,000 Tokens** 配额。 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | prompt | string | 是 | 图片描述,最大 2000 字符 | | size | string | 是 | 分辨率比例,可选值:`9:16`、`1:1`、`16:9` | ```json { "prompt": "a cat sitting on a windowsill", "size": "1:1" } ``` ##### 响应 **成功** (200) ```json { "success": true, "task_id": "img_2962032959761378073" } ``` | 字段 | 类型 | 说明 | |------|------|------| | success | boolean | 是否提交成功 | | task_id | string | 任务 ID,格式 `img_xxxxxxxxxxxxxxxx`,后续查询和获取图片需用此 ID | ##### 限速 - 同一用户两次提交间隔最少 **2 秒**,否则返回 429 - 每次调用消耗 10,000 Tokens 配额 ##### 错误码 | HTTP 状态码 | type | 说明 | |-------------|------|------| | 400 | `invalid_request_error` | 参数错误(prompt 为空 / size 无效等) | | 401 | `authentication_error` | Token 无效或缺失 | | 403 | `insufficient_quota` | 本月配额未核算 | | 429 | `insufficient_quota` | 本月配额已用完 | | 429 | `rate_limit_error` | 提交过于频繁(2 秒冷却期内重复提交) | | 503 | `server_error` | 队列已满 | --- #### 4.2 查询生成记录 ``` GET /v1/images/records ``` 查询当前用户的所有图片生成记录,按时间倒序排列。 ##### 响应 ```json { "records": [ { "task_id": "img_2962032959761378073", "prompt": "a beautiful sunset over the ocean", "size": "1:1", "seed": 3820456595451353790, "status": "completed", "image_key": "img_2962032959761378073", "duration_ms": 4927, "created_at": "2026-05-28T18:07:07+08:00", "completed_at": "2026-05-28T18:07:12+08:00" }, { "task_id": "img_69092961307210991", "prompt": "a cat on the roof", "size": "9:16", "seed": 7688086820840863263, "status": "failed", "image_key": "", "duration_ms": 0, "created_at": "2026-05-28T18:05:38+08:00", "completed_at": "2026-05-28T18:05:38+08:00" } ] } ``` | 字段 | 类型 | 说明 | |------|------|------| | task_id | string | 任务 ID | | prompt | string | 图片描述 | | size | string | 分辨率比例 | | seed | integer | 随机种子(系统自动生成) | | status | string | 任务状态:`queued` / `processing` / `completed` / `failed` | | image_key | string | 图片获取 Key(仅 `completed` 状态有值,等于 task_id) | | duration_ms | integer | 生成耗时(毫秒) | | created_at | string | 提交时间 | | completed_at | string | 完成时间(进行中为空) | --- #### 4.3 获取图片(Base64,SSE 流式) ``` POST /v1/images/fetch ``` 根据 image_key 批量获取图片 Base64 数据,通过 SSE 流式返回。 ##### 请求 | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | keys | string[] | 是 | image_key 列表,最多 100 个,重复 key 自动去重 | ```json { "keys": [ "img_2962032959761378073", "img_7763370129993463292" ] } ``` ##### 响应 Content-Type: `text/event-stream` 每条 SSE 数据格式: ``` data: {"key":"img_xxx","image":"data:image/png;base64,iVBOR..."} ``` | 字段 | 类型 | 说明 | |------|------|------| | key | string | 对应请求中的 image_key | | image | string | Base64 编码图片(`data:image/png;base64,...`),获取失败时为空字符串 | 流结束标记: ``` data: [DONE] ``` > **注意**:如果某个 key 对应的图片不存在、未完成或不属于当前用户,`image` 字段返回空字符串,不会报错。 --- #### 分辨率预设 | size | 分辨率 | 适用场景 | |------|--------|----------| | `9:16` | 928 × 1664 | 手机竖屏 | | `1:1` | 1024 × 1024 | 社交媒体方形图 | | `16:9` | 1664 × 928 | 电脑横屏 / 视频 | --- ### 5. Agent 安装包 #### 5.1 获取安装包列表 ``` GET /v1/agent/packages ``` 无需认证。返回所有可用的 Agent 安装包。 ##### 响应 ```json [ { "id": 1, "version": "1.2.3", "filename": "myt-agent-1.2.3.tar.gz", "file_size": 12345678, "md5": "aabbccdd...", "created_at": "2025-01-01T00:00:00Z" } ] ``` | 字段 | 类型 | 说明 | |------|------|------| | id | integer | 安装包 ID | | version | string | 语义化版本号 | | filename | string | 文件名 | | file_size | integer | 文件大小(字节) | | md5 | string | MD5 校验值 | | created_at | string | 上传时间 | --- ### 6. 技能仓库 #### 6.1 获取技能索引 ``` GET /.well-known/skills/index.json ``` 无需认证。Hermes 原生兼容协议。返回所有可用技能列表。 ##### 响应 ```json { "skills": [ { "name": "weather-query", "description": "查询天气信息", "files": ["SKILL.md", "references/api-schema.json"], "tags": ["weather", "api"], "version": "1.0.0", "author": "admin", "updated_at": "2025-05-01T12:00:00Z" } ] } ``` | 字段 | 类型 | 说明 | |------|------|------| | name | string | 技能名称 | | description | string | 技能描述 | | files | string[] | 技能包含的文件列表 | | tags | string[] | 标签 | | version | string | 版本号 | | author | string | 作者 | | updated_at | string | 最后更新时间 | --- ### 7. 健康检查 ``` GET /health ``` 无需认证。返回服务健康状态。 ##### 响应 ```json { "status": "ok" } ``` --- ### 配额说明 #### 配额计算 - 每个有效坑位赠送 **1,000,000 Tokens** 月配额 - 坑位来自墨云腾平台未过期设备(`state == 0` 且 `extime > 当前时间` 的子设备数) - 配额只增不减:新增坑位增加配额,减少坑位不降低已核算配额 - 每月 1 日 00:00 自动重置,用户下次登录时重新核算 #### Token 消耗 | 接口 | 消耗 | |------|------| | `/v1/chat/completions` | 按实际 `prompt_tokens + completion_tokens` 消耗 | | `/v1/images/generations` | 固定消耗 10,000 Tokens | #### Token 统计优先级 1. 后端返回的精确 `usage` 数据(通过 `stream_options.include_usage=true` 获取) 2. tiktoken 估算(GPT-4 编码器) 3. 字符粗估(CJK 每字符 1 token,ASCII 每 4 字符 1 token) --- ### 完整对接示例 #### Python — OpenAI SDK(推荐) 网关完全兼容 OpenAI API,可直接使用官方 SDK,只需修改 `base_url`: ```python from openai import OpenAI client = OpenAI( api_key="myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", base_url="https://ai.opencecs.com/v1" ) # 非流式 response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "你好"}], stream=False ) print(response.choices[0].message.content) # 流式 stream = client.chat.completions.create( model="deepseek-r1", messages=[{"role": "user", "content": "解释量子计算"}], stream=True ) for chunk in stream: delta = chunk.choices[0].delta if delta.content: print(delta.content, end="") if hasattr(delta, "reasoning_content") and delta.reasoning_content: print(f"[思考] {delta.reasoning_content}", end="") print() # 函数调用 response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "北京今天天气如何?"}], tools=[{ "type": "function", "function": { "name": "get_weather", "description": "获取指定城市天气", "parameters": { "type": "object", "properties": {"city": {"type": "string", "description": "城市名"}}, "required": ["city"] } } }], tool_choice="auto" ) print(response.choices[0].message.tool_calls) ``` #### Node.js — OpenAI SDK ```javascript import OpenAI from "openai"; const client = new OpenAI({ apiKey: "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", baseURL: "https://ai.opencecs.com/v1", }); // 非流式 const response = await client.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "你好" }], stream: false, }); console.log(response.choices[0].message.content); // 流式 const stream = await client.chat.completions.create({ model: "deepseek-r1", messages: [{ role: "user", content: "解释量子计算" }], stream: true, }); for await (const chunk of stream) { const delta = chunk.choices[0].delta; if (delta.content) process.stdout.write(delta.content); } console.log(); ``` #### Python — 聊天补全(requests 原始调用) ```python import requests BASE = "https://ai.opencecs.com" TOKEN = "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} # 流式请求 resp = requests.post(f"{BASE}/v1/chat/completions", headers=HEADERS, json={ "model": "deepseek-r1", "messages": [{"role": "user", "content": "你好"}], "stream": True }, stream=True) for line in resp.iter_lines(): if line: line = line.decode("utf-8") if line.startswith("data: ") and line != "data: [DONE]": import json chunk = json.loads(line[6:]) delta = chunk["choices"][0]["delta"] if "content" in delta: print(delta["content"], end="") if "reasoning_content" in delta: print(f"[思考] {delta['reasoning_content']}", end="") print() ``` #### Python — 图片生成 ```python import requests import json import time import sseclient import base64 BASE = "https://ai.opencecs.com" TOKEN = "myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"} # 1. 提交生成 resp = requests.post(f"{BASE}/v1/images/generations", headers=HEADERS, json={"prompt": "a cat on the roof", "size": "1:1"}) task_id = resp.json()["task_id"] print(f"任务已提交: {task_id}") # 2. 轮询等待完成 while True: resp = requests.get(f"{BASE}/v1/images/records", headers=HEADERS) records = resp.json()["records"] record = next((r for r in records if r["task_id"] == task_id), None) if record and record["status"] == "completed": image_key = record["image_key"] break if record and record["status"] == "failed": print("生成失败") exit(1) print(f"状态: {record['status']}, 等待中...") time.sleep(3) # 3. 获取图片 resp = requests.post(f"{BASE}/v1/images/fetch", headers=HEADERS, json={"keys": [image_key]}, stream=True) client = sseclient.SSEClient(resp) for event in client.events(): if event.data == "[DONE]": break item = json.loads(event.data) if item["image"]: base64_data = item["image"].split(",", 1)[1] with open("output.png", "wb") as f: f.write(base64.b64decode(base64_data)) print("图片已保存为 output.png") ``` #### cURL — 非流式聊天 ```bash curl -X POST https://ai.opencecs.com/v1/chat/completions \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o", "messages": [{"role": "user", "content": "Hello"}], "stream": false }' ``` #### cURL — 流式聊天 ```bash curl -X POST https://ai.opencecs.com/v1/chat/completions \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "model": "deepseek-r1", "messages": [{"role": "user", "content": "Hello"}], "stream": true }' ``` #### cURL — 图片生成 ```bash # 提交生成 curl -X POST https://ai.opencecs.com/v1/images/generations \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"prompt": "a cat on the roof", "size": "1:1"}' # 查询记录 curl https://ai.opencecs.com/v1/images/records \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" # 获取图片 curl -X POST https://ai.opencecs.com/v1/images/fetch \ -H "Authorization: Bearer myt-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"keys": ["img_2962032959761378073"]}' ``` --- ### 思考模式说明 网关对思考模式(`thinking`)的处理遵循以下规则: | 模型配置 `enable_thinking` | 请求中的 `thinking` | 网关实际行为 | |---|---|---| | 启用 | 未传 | 自动设置 `thinking.type="enabled"` | | 启用 | `{"type":"enabled"}` | 保持启用 | | 启用 | `{"type":"disabled"}` | 强制启用(忽略禁用请求) | | 禁用 | 任意 | 强制 `thinking.type="disabled"` | 当思考模式启用时: - 响应中包含 `reasoning_content` 字段 - `budget_tokens` 参数会透传到后端 当思考模式禁用时: - 请求和响应中的 `reasoning_content`、`reasoning` 字段会被移除 - `usage` 中的 `reasoning_tokens` 也会被移除 - 自动注入 `chat_template_kwargs.enable_thinking=false` --- ### 限速规则汇总 | 接口 | 限速策略 | 超限响应 | |------|----------|----------| | `/v1/auth/*` | 同一 IP 每分钟 10 次 | 429 + `{"success":false,"message":"请求过于频繁,请稍后再试"}` | | `/v1/chat/completions` | 无 IP 限速,仅配额限制 | 429 + 配额不足错误 | | `/v1/images/generations` | 同一用户 2 秒冷却 + 配额限制 | 429 + `rate_limit_error` 或 `quota_error` | | `/v1/images/records` | 无 | — | | `/v1/images/fetch` | 无 | — | | `/v1/models` | 无 | — | | `/v1/agent/packages` | 无 | — | | `/.well-known/skills/index.json` | 无 | — | --- ### 请求体大小限制 | 接口 | 限制 | |------|------| | `/v1/chat/completions` | 10 MB | | 其他接口 | 无显式限制 | --- ## 线上机型数据库查询下载API *分类: 机型数据库* ## 机型模板查询 API Base URL: `https://newapi.moyunteng.com/api/v1` --- ### 1. 获取机型列表 ``` GET /api/v1/template/list ``` | 参数 | 类型 | 必填 | 默认值 | 说明 | |------|------|------|--------|------| | page | int | 否 | 1 | 页码(每页10条),传 0 返回全部(最多1000条) | | keyword | string | 否 | - | 按机型名称模糊搜索 | | android_version | string | 否 | 14 | Android版本筛选,传 `all` 不筛选 | | sdk_ver | int | 否 | 0 | SDK版本号筛选,过滤不兼容的机型 | #### 示例 ```bash # 获取全部机型 curl "https://newapi.moyunteng.com/api/v1/template/list?page=0" # 搜索机型 curl "https://newapi.moyunteng.com/api/v1/template/list?keyword=PKA110" # 按Android版本筛选 curl "https://newapi.moyunteng.com/api/v1/template/list?android_version=13&page=0" # 只看兼容SDK 112的机型 curl "https://newapi.moyunteng.com/api/v1/template/list?page=0&sdk_ver=112" ``` #### 响应 ```json { "code_id": 200, "msg": "success", "data": { "list": [ { "id": 234, "name": "PKA110", "status": "已编辑", "currentVersion": 20, "md5": "cefa6fe219d9ad86603d8ddff16bc28f", "createdAt": 1772270261, "updatedAt": 1777539971, "androidVersion": "14", "android_version": "14" } ], "total": 923 } } ``` | 字段 | 类型 | 说明 | |------|------|------| | id | int64 | 机型ID,用于查详情/下载 | | name | string | 机型名称 | | status | string | 状态,如 `已编辑`、`新建` | | currentVersion | int | 当前版本号 | | md5 | string | 打包文件MD5 | | createdAt | int64 | 创建时间(Unix秒) | | updatedAt | int64 | 更新时间(Unix秒) | | androidVersion | string | Android版本号 | | android_version | string | 同上(兼容字段) | --- ### 2. 根据机型名称获取最新配置 ``` GET /api/v1/latest_template/:name ``` | 参数 | 位置 | 类型 | 必填 | 说明 | |------|------|------|------|------| | name | path | string | 是 | 机型名称,如 `PKA110` | | sdk_ver | query | int | 否 | SDK版本号,不兼容返回404 | #### 示例 ```bash curl "https://newapi.moyunteng.com/api/v1/latest_template/PKA110" curl "https://newapi.moyunteng.com/api/v1/latest_template/PKA110?sdk_ver=112" ``` #### 响应 ```json { "code_id": 200, "msg": "success", "data": { "androidVersion": "14", "android_version": "14", "createdAt": 1777539971, "model": "PKA110", "overlay": { "EMMCID": "[0-9a-f]{32}", "SERIALNUMBER": "[0-9a-zA-Z]{12}", "android_id": "[0-9a-f]{16}", "bluetooth_mac": "[0-9a-f][02468ace]:...", "wifi_mac": "[0-9a-f][02468ace]:...", "gaid": "[0-9a-f]{8}-...", "oaid": "[0-9a-zA-Z]{64}", "udid": "[0-9a-f]{8}-..." }, "prop": { "ro.build.version.security_patch": "2026-03-05", "ro.vendor.build.security_patch": "2026-03-05" }, "supported_version": "1-990", "updatedAt": 1777539971, "version": 20 } } ``` | 字段 | 类型 | 说明 | |------|------|------| | androidVersion | string | Android版本 | | model | string | 设备型号 | | overlay | object | 覆盖配置(设备参数模板) | | prop | object | 属性配置 | | supported_version | string | 兼容SDK版本范围,如 `1-990`、`112`、`110-` | | version | int | 当前版本号 | | createdAt | int64 | 版本创建时间(Unix秒) | | updatedAt | int64 | 模板更新时间(Unix秒) | #### 错误 - `404 template not found` — 机型不存在 - `404 sdk version not compatible` — SDK版本不兼容 --- ### 3. 通过ID获取机型配置 ``` GET /api/v1/template/:id ``` | 参数 | 位置 | 类型 | 必填 | 说明 | |------|------|------|------|------| | id | path | int | 是 | 机型ID | | sdk_ver | query | int | 否 | SDK版本号 | 响应格式同上。 --- ### 4. 获取机型下载地址 ``` GET /api/v1/template/download-url/:id ``` #### 示例 ```bash curl "https://newapi.moyunteng.com/api/v1/template/download-url/234" ``` #### 响应 ```json { "code_id": 200, "msg": "success", "data": { "download_url": "https://d1.moyunteng.com/newapi/export/PKA110_v20.zip" } } ``` --- ### 5. 下载机型模板文件 ``` GET /api/v1/template/download/:id ``` 直接返回 zip 文件流,优先从 COS 302 重定向,回退到本地文件。 --- ### 6. 获取机型版本历史 ``` GET /api/v1/template/:id/versions ``` #### 响应 ```json { "code_id": 200, "msg": "success", "data": [ { "version": 20, "created_at": 1777539971 }, { "version": 19, "created_at": 1777539447 } ] } ``` --- ### 7. 获取指定版本配置 ``` GET /api/v1/template/:id/version/:version ``` 响应格式同机型详情。 --- ### 8. 获取 Android 版本列表 ``` GET /api/v1/template/android-versions ``` #### 响应 ```json { "code_id": 200, "msg": "success", "data": { "versions": [ { "label": "Android_16", "value": "16" }, { "label": "Android_15", "value": "15" }, { "label": "Android_14", "value": "14" }, { "label": "Android_13", "value": "13" }, { "label": "Android_12", "value": "12" }, { "label": "Android_11", "value": "11" }, { "label": "Android_10", "value": "10" } ] } } ``` --- ### SDK版本兼容性 `supported_version` 格式: | 格式 | 示例 | 说明 | |------|------|------| | 范围 | `1-990` | SDK 1~990 | | 单值 | `112` | 仅 SDK 112 | | 开放上界 | `110-` | SDK 110及以上 | 查询时传 `sdk_ver` 参数自动过滤不兼容机型。 --- ## MYTOS Android WebRTC投屏 *分类: Android API* webrtc 播放器远程 下载地址 https://doc.opencecs.com/api/firmwares/download/73 ## MYTOS Android WebRTC投屏 - AI快速参考 - player.html:投屏主页面,加载视频流并渲染 - jquery-3.2.1.min.js:DOM 操作与事件处理库 - adapter-latest.js:WebRTC 浏览器兼容适配器 - h5.lgair.wt.all.min-2.1.js / h5.lgair.wt.all.min-2.2.js:魔云腾自研 WebRTC SDK,封装信令、媒体协商、重连等逻辑 **功能说明**:调用 webrtc 的播放器 **请求方式**:GET **调用方法**: - 下载播放器,将压缩包解压到本地目录,下载地址:webplayer.zip - 打开浏览器,拼接完整 URL - 确保文件本地目录地址正确 - 检查参数是否正确 - 成功后即可观看 WebRTC 视频流 **请求 URL**: ``` webplayer/play.html?shost={ip}&sport={webrtc_port}&q=1&v=h264&rtc_i={ip}&rtc_p={webrtc_port} ``` **请求参数**: | 参数名 | 是否必选 | 类型 | 说明 | | --- | --- | --- | --- | | shost | 是 | string | WebRTC 流媒体服务器主机地址(如 192.168.99.108) | | sport | 是 | string | WebRTC 流媒体服务器端口(TCP)(如 31207) | | q | 是 | string | 视频质量参数 (0=低 1=高) | | v | 是 | string | 视频编码格式(如 h264) | | rtc_j | 是 | string | RTC 服务端 IP(与 shost 一致,用于建立点对点连接,如192.168.99.108) | | rtc_p | 是 | string | WebRTC 端口(UDP)(如 31208) | **请求示例**: ``` GET "./webplayer/play.html?shost=192.168.99.108&sport=31207&q=1&v=h264&rtc_i=192.168.99.108&rtc_p=31208" ``` **返回示例**: 成功: ``` 成功响应(页面加载完成) ``` 失败: ``` 失败响应(页面加载失败) ``` --- ## MYTOS Android RPA *分类: Android API* 源码:https://cnb.cool/openmyt/myt-sdk-golang 包含RPA-glang 版本源码 包含各类调用方式使用 ## MYTOS Android RPA - AI快速参考 ###### 桥接模式 - **IP 地址**:桥接设备的 IP 地址 - **RPA 端口**:固定为 **9083** ###### 非桥接模式 - **IP 地址**(如 192.168.30.2):用来定位具体的设备或宿主机 - **RPA 端口**:根据设备实例索引计算得出,计算公式为:**30000 + (index - 1) × 100 + 2** **Q1 设备端口列表**(index 1-12): | 实例位 | 实例位(index) | RPA 端口 | | --- | --- | --- | | 坑位1 | 1 | 30002 | | 坑位2 | 2 | 30102 | | 坑位3 | 3 | 30202 | | 坑位4 | 4 | 30302 | | 坑位5 | 5 | 30402 | | 坑位6 | 6 | 30502 | | 坑位7 | 7 | 30602 | | 坑位8 | 8 | 30702 | | 坑位9 | 9 | 30802 | | 坑位10 | 10 | 30902 | | 坑位11 | 11 | 31002 | | 坑位12 | 12 | 31102 | **P1 设备端口列表**(index 1-24): | 实例位 | 坑位号(index) | RPA 端口 | | --- | --- | --- | | 坑位1 | 1 | 30002 | | 坑位2 | 2 | 30102 | | 坑位3 | 3 | 30202 | | 坑位4 | 4 | 30302 | | 坑位5 | 5 | 30402 | | 坑位6 | 6 | 30502 | | 坑位7 | 7 | 30602 | | 坑位8 | 8 | 30702 | | 坑位9 | 9 | 30802 | | 坑位10 | 10 | 30902 | | 坑位11 | 11 | 31002 | | 坑位12 | 12 | 31102 | | 坑位13 | 13 | 31202 | | 坑位14 | 14 | 31302 | | 坑位15 | 15 | 31402 | | 坑位16 | 16 | 31502 | | 坑位17 | 17 | 31602 | | 坑位18 | 18 | 31702 | | 坑位19 | 19 | 31802 | | 坑位20 | 20 | 31902 | | 坑位21 | 21 | 32002 | | 坑位22 | 22 | 32102 | | 坑位23 | 23 | 32202 | | 坑位24 | 24 | 32302 | ### 目录结构 以下是 MYT RPA SDK 的完整目录结构,帮助您理解如何组织和使用 SDK 文件: ``` MYT_RPA_SDK_V10_20250407/ ├── arm/ # ARM架构库文件 │ └── libmytrpc_arm64.so # ARM64 Linux库 ├── centos7/ # CentOS 7库文件 │ └── libmytrpc_centos.so # CentOS 7 Linux库 ├── demo_py_x64/ # Python 64位演示项目 │ ├── rpc_demo.py # Python演示脚本 │ ├── common/ # 公共模块目录 │ │ ├── logger.py # 日志模块 │ │ ├── mytRpc.py # Python SDK核心实现 │ │ ├── mytSelector.py # 选择器实现 │ │ ├── rpcNode.py # 节点操作实现 │ │ ├── ToolsKit.py # 工具函数 │ │ └── __init__.py # 模块初始化 │ ├── lib/ # 库文件目录 │ │ ├── libmytrpc.dll # Windows库文件 │ │ ├── libmytrpc.dylib # macOS库文件 │ │ └── libmytrpc.lib # Windows导入库 │ └── log/ # 日志目录 │ └── myt.log # 日志文件 ├── include/ # 头文件目录 │ └── libmytrpc.h # C头文件 ├── lib/ # Windows 32位库目录 │ ├── libmytrpc.dll # Windows 32位库文件 │ └── libmytrpc.lib # Windows 32位导入库 ├── lib64/ # Windows 64位库目录 │ ├── libmytrpc.dll # Windows 64位库文件 │ └── libmytrpc.lib # Windows 64位导入库 ├── macos(m)/ # macOS M系列芯片库目录 │ └── libmytrpc.dylib # macOS M系列库文件 ├── macos(x86)/ # macOS x86芯片库目录 │ └── libmytrpc.dylib # macOS x86库文件 ├── ubuntu/ # Ubuntu库目录 │ └── libmytrpc_ubuntu_x86_64.so # Ubuntu x86_64库文件 └── README.txt # SDK说明文档 ``` ### 引用文件功能与使用说明 #### 模块导入 根据实际的 SDK 实现,Python 版本采用面向对象的方式进行调用。以下是正确的导入方式: ``` # 导入核心SDK类 from common.mytRpc import MytRpc # 导入选择器类 from common.mytSelector import mytSelector # 导入节点操作类 from common.rpcNode import rpcNode ``` #### 依赖要求 - Python 3.7+ 64位版本 - Windows 操作系统(当前SDK主要支持Windows) - 需要将 `libmytrpc.dll` 文件放置在项目的 `common` 目录下或系统 PATH 目录 - 可选依赖: OpenCV (cv2):用于图像处理和显示 - NumPy (numpy):用于图像数据处理 #### 引用文件功能说明 以下是 SDK 中各个引用文件的详细功能介绍: | 文件名 | 功能描述 | 主要方法/类 | 使用场景 | | --- | --- | --- | --- | | `mytRpc.py` | 核心 SDK 实现,封装了与设备通信的所有主要功能 | `MytRpc` 类 | 所有设备控制操作的入口点 | | `mytSelector.py` | 提供 UI 元素选择器功能,用于查找和筛选 UI 节点 | `mytSelector` 类 | 需要根据条件查找 UI 元素时使用 | | `rpcNode.py` | 封装了 UI 节点的操作和属性获取方法 | `rpcNode` 类 | 获取节点属性、执行节点操作时使用 | | `logger.py` | 提供日志记录功能 | `logger` 对象 | 需要记录日志信息时使用 | | `ToolsKit.py` | 提供各种工具函数 | `ToolsKit` 类 | 获取程序路径、检查进程状态等工具操作 | | `__init__.py` | 模块初始化文件 | - | 确保 common 目录可作为 Python 模块导入 | | `libmytrpc.dll` | 底层动态链接库,实现了与设备通信的核心功能 | - | 提供 SDK 核心功能的底层实现,所有上层操作最终都会调用此 DLL 中的函数 | #### DLL 文件详细说明 ##### libmytrpc.dll - 核心动态链接库 **功能描述**: `libmytrpc.dll` 是 MYT RPA SDK 的核心动态链接库,实现了与远程设备通信的所有底层功能,包括设备连接、截图、触摸操作、按键输入、UI 节点获取等。 **文件位置**: - 在 SDK 包中的位置:`MYT_RPA_SDK_V10_20250407/demo_py_x64/lib/libmytrpc.dll` - 在项目中的位置:项目的 `common` 目录下,即 `项目目录/common/libmytrpc.dll` **引用方式**: **使用说明**: - 开发者无需直接调用 DLL 中的函数,而是通过 `MytRpc` 类提供的方法间接使用 DLL 功能 - DLL 文件会在创建 `MytRpc` 实例时自动加载 - 不同平台有对应的库文件: Windows: `libmytrpc.dll` - Linux: `libmytrpc.so`(不同架构有不同版本) - macOS: `libmytrpc.dylib` **DLL 依赖关系**: - Windows 平台:需要 Visual C++ Redistributable 运行时库 - Linux 平台:需要相应的系统依赖库 - macOS 平台:需要相应的系统框架支持 **使用示例**: ``` from common.mytRpc import MytRpc # 创建 MytRpc 实例时,会自动加载 libmytrpc.dll mytapi = MytRpc() # 以下操作都会间接调用 DLL 中的功能 if mytapi.init("192.168.1.100", 30202, 10) == True: # 调用 DLL 中的截图功能 mytapi.screentshot(1, 90, "screenshot.png") # 调用 DLL 中的触摸操作功能 mytapi.touchClick(0, 500, 500) ``` **常见问题与解决方案**: **DLL 功能模块**: **性能优化建议**: - 避免频繁创建和销毁 `MytRpc` 实例,这会导致 DLL 频繁加载和卸载 - 对于频繁执行的操作,考虑批量处理 - 合理设置超时时间,避免不必要的等待 - 及时释放不再使用的资源,如选择器和节点对象 #### 各文件详细介绍 ##### 1. mytRpc.py - 核心 SDK 实现 `MytRpc` 类是 SDK 的核心类,负责与远程设备建立连接并提供所有设备控制功能。 **主要功能**: - 设备连接与管理 - 截图操作 - 触摸操作 - 按键输入 - App 操作 - 节点树导出 - 选择器创建与管理 **使用示例**: ``` from common.mytRpc import MytRpc # 创建 SDK 实例 mytapi = MytRpc() # 初始化并连接设备 if mytapi.init("192.168.1.100", 30202, 10) == True: print("设备连接成功") # 执行设备命令 output, result = mytapi.exec_cmd("ls -la") print(f"命令输出: {output}") # 截图保存 mytapi.screentshot(1, 90, "screenshot.png") ``` ##### 2. mytSelector.py - UI 元素选择器 `mytSelector` 类用于创建和管理 UI 元素选择条件,查找符合条件的 UI 节点。 **主要功能**: - 支持多种条件筛选(ID、文本、类名、包名等) - 支持布尔属性筛选(可点击、可见、可滚动等) - 支持边界条件筛选 - 支持正则表达式匹配 **使用示例**: ``` from common.mytRpc import MytRpc mytapi = MytRpc() mytapi.init("192.168.1.100", 30202, 10) # 创建选择器 selector = mytapi.create_selector() with selector: # 设置选择条件 selector.addQuery_TextContainWith('登录') # 文本包含"登录" selector.addQuery_Clickable(True) # 可点击 # 查找节点 node = selector.execQueryOne(2000) # 超时2000毫秒 if node is not None: print("找到匹配节点") node.Click_events() # 点击节点 ``` ##### 3. rpcNode.py - UI 节点操作 `rpcNode` 类封装了 UI 节点的属性获取和操作方法。 **主要功能**: - 获取节点属性(文本、描述、ID、类名等) - 获取节点位置和边界信息 - 节点点击和长按操作 - 获取节点父子关系 **使用示例**: ``` # 假设已通过选择器获取到节点 node # 获取节点属性 node_json = node.getNodeJson() node_text = node.getNodeText() node_id = node.getNodeId() # 获取节点位置 bounds = node.getNodeNound() print(f"节点位置: {bounds}") # 点击节点 node.Click_events() # 长按节点 node.longClick_events() ``` ##### 4. logger.py - 日志记录 提供日志记录功能,支持控制台和文件输出。 **主要功能**: - 支持多种日志级别(debug、info、warning、error、crit) - 自动按天分割日志文件 - 同时输出到控制台和文件 **使用示例**: ``` from common.logger import logger logger.debug("调试信息") logger.info("普通信息") logger.warning("警告信息") logger.error("错误信息") ``` ##### 5. ToolsKit.py - 工具函数 提供各种辅助工具函数。 **主要功能**: - 获取程序根路径 - 检查进程是否存在 - 防止程序多次运行 **使用示例**: ``` from common.ToolsKit import ToolsKit tools = ToolsKit() # 获取程序根路径 root_path = tools.GetRootPath() print(f"程序根路径: {root_path}") # 检查进程是否存在 is_running = tools.check_process(1234) print(f"进程1234是否存在: {is_running}") ``` #### 安装说明 --- ### 目录 - 初始化与释放 - 基础设备方法 - 截图方法 - 触摸操作方法 - 按键和文本输入方法 - App操作方法 - 视频流方法 - 选择器方法 - 节点集合操作方法 - 节点属性获取方法 - 节点操作方法 - 选择器筛选方法 布尔属性筛选 - 边界筛选 - ID匹配筛选 - Text匹配筛选 - Class匹配筛选 - Package匹配筛选 - Desc匹配筛选 --- ### 数据结构 #### CaptureResult - 截图结果 | 字段 | 类型 | 说明 | | --- | --- | --- | | data | bytes | 图像数据(RGBA格式) | | width | int | 图像宽度(像素) | | height | int | 图像高度(像素) | | stride | int | 图像步长(每行字节数) | | ptr | int | 原始指针,需要调用 free_rpc_ptr 释放 | #### CompressedCaptureResult - 压缩截图结果 | 字段 | 类型 | 说明 | | --- | --- | --- | | data | bytes | 压缩后的图像数据(PNG或JPG格式) | | ptr | int | 原始指针,需要调用 free_rpc_ptr 释放 | #### Bounds - 节点边界 | 字段 | 类型 | 说明 | | --- | --- | --- | | left | int | 左边界坐标 | | top | int | 上边界坐标 | | right | int | 右边界坐标 | | bottom | int | 下边界坐标 | #### Point - 坐标点 | 字段 | 类型 | 说明 | | --- | --- | --- | | x | int | X坐标 | | y | int | Y坐标 | --- ### 初始化与释放 #### init 初始化 DLL,加载所有函数 ``` def init(dll_path: str = "") -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | dll_path | str | DLL文件的路径,如果为空则使用默认路径 "libmytrpc.dll" | **异常**: Exception - 加载失败时抛出异常 --- #### release 释放 DLL ``` def release() -> None: ``` **异常**: Exception - 释放失败时抛出异常 --- ### 基础设备方法 #### get_sdk_version 获取当前库的版本号 ``` def get_sdk_version() -> int: ``` **返回值**: int - 版本号 --- #### openDevice 远程连接设备 ``` def openDevice(ip: str, port: int, timeout: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | ip | str | 要远程控制的设备的IP地址 | | port | int | 要远程控制的设备的端口 | | timeout | int | 远程连接的超时时间,单位秒 | **返回值**: int - 句柄id,大于0表示成功,失败返回0 --- #### closeDevice 关闭远程连接 ``` def closeDevice(handle: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: int - 0表示成功,失败返回非0 --- #### check_connect_state 检测远程连接是否处于连接状态 ``` def check_connect_state(handle: int) -> bool: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: int - 1表示已连接,0表示已断开 --- #### free_rpc_ptr 释放截图数据 ``` def free_rpc_ptr(ptr: int) -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | ptr | int | 指针数据 | --- #### get_display_rotate 获取当前屏幕的旋转角度 ``` def get_display_rotate(handle: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: int - 返回0,1,2,3 --- #### exec_cmd 执行shell命令 ``` def exec_cmd(handle: int, wait_for_exit: bool, cmdline: str) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | wait_for_exit | bool | 是否等待执行结束才返回 | | cmdline | str | 命令行 | **返回值**: str - 命令执行结果字符串 --- #### dumpNodeXml 获取节点树数据(XML格式) ``` def dumpNodeXml(handle: int, dump_all: bool) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | dump_all | bool | 是否导出所有节点 | **返回值**: str - 节点树XML字符串 --- #### dump_node_xml_ex 获取节点树数据(扩展版本) ``` def dump_node_xml_ex(handle: int, use_new_mode: bool, timeout: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | use_new_mode | bool | 是否使用新模式 | | timeout | int | 超时时间 | **返回值**: str - 节点树XML字符串 --- #### use_new_node_mode 设置节点模式 ``` def use_new_node_mode(handle: int, use: bool) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | use | bool | 是否使用新模式 | **返回值**: int - 操作结果 --- ### 截图方法 #### take_capture 远程截图,获取到的是RGBA的数据流 ``` def take_capture(handle: int) -> CaptureResult: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: CaptureResult - 截图结果 **异常**: Exception - 截图失败时抛出异常 --- #### take_capture_ex 远程截图(指定区域) ``` def take_capture_ex(handle: int, l: int, t: int, r: int, b: int) -> CaptureResult: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | l | int | 截图区域的左坐标 | | t | int | 截图区域的上坐标 | | r | int | 截图区域的右坐标 | | b | int | 截图区域的下坐标 | **返回值**: CaptureResult - 截图结果 **异常**: Exception - 截图失败时抛出异常 --- #### takeCaptrueCompress 远程截图,获取压缩后的png或jpg格式 ``` def takeCaptrueCompress(handle: int, image_type: int, quality: int) -> CompressedCaptureResult: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | image_type | int | 0表示png,1表示jpg | | quality | int | 压缩质量,取值0-100 | **返回值**: CompressedCaptureResult - 压缩截图结果 **异常**: Exception - 截图失败时抛出异常 --- #### setRpaWorkMode 设置RPA工作模式(无障碍模式开关) ``` def setRpaWorkMode(mode: int) -> bool: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | mode | int | 工作模式(0: 关闭无障碍, 1: 开启无障碍) | **返回值**: bool - 设置成功返回True,失败返回False **说明**: - 该方法用于设置RPA的工作模式,主要是控制是否使用无障碍模式 - 开启无障碍模式后,可以获取更加完整的节点信息,但某些应用环境会检测是否开启了无障碍 - 该方法需要最新的固件版本支持 --- #### take_capture_compress_ex 远程截图(指定区域,压缩格式) ``` def take_capture_compress_ex(handle: int, l: int, t: int, r: int, b: int, image_type: int, quality: int) -> CompressedCaptureResult: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | l | int | 截图区域的左坐标 | | t | int | 截图区域的上坐标 | | r | int | 截图区域的右坐标 | | b | int | 截图区域的下坐标 | | image_type | int | 0表示png,1表示jpg | | quality | int | 压缩质量,取值0-100 | **返回值**: CompressedCaptureResult - 压缩截图结果 **异常**: Exception - 截图失败时抛出异常 --- ### 触摸操作方法 #### touchDown 模拟按下 ``` def touchDown(handle: int, id: int, x: int, y: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | id | int | 手指编号(0-9) | | x | int | X坐标 | | y | int | Y坐标 | **返回值**: int - 操作结果 --- #### touchUp 模拟弹起 ``` def touchUp(handle: int, id: int, x: int, y: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | id | int | 手指编号(0-9) | | x | int | X坐标 | | y | int | Y坐标 | **返回值**: int - 操作结果 --- #### touchMove 模拟滑动 ``` def touchMove(handle: int, id: int, x: int, y: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | id | int | 手指编号(0-9) | | x | int | X坐标 | | y | int | Y坐标 | **返回值**: int - 操作结果 --- #### touchClick 模拟单击 ``` def touchClick(handle: int, id: int, x: int, y: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | id | int | 手指编号(0-9) | | x | int | X坐标 | | y | int | Y坐标 | **返回值**: int - 操作结果 --- #### swipe 模拟滑动 ``` def swipe(handle: int, id: int, x0: int, y0: int, x1: int, y1: int, millis: int, async: bool) -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | id | int | 手指编号(0-9) | | x0 | int | 起始X坐标 | | y0 | int | 起始Y坐标 | | x1 | int | 结束X坐标 | | y1 | int | 结束Y坐标 | | millis | int | 滑动持续时间(毫秒) | | async | bool | 是否异步执行 | --- ### 按键和文本输入方法 #### keyPress 模拟按键 ``` def keyPress(handle: int, code: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | code | int | 按键码 | **返回值**: int - 操作结果 --- #### sendText 模拟键盘输入 ``` def sendText(handle: int, text: str) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | text | str | 要输入的字符串 | **返回值**: int - 操作结果 --- ### App操作方法 #### openApp 打开指定包名的app ``` def openApp(handle: int, pkg: str) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | pkg | str | 包名 | **返回值**: int - 操作结果 --- #### stopApp 停止指定的应用 ``` def stopApp(handle: int, pkg: str) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | pkg | str | 包名 | **返回值**: int - 操作结果 --- ### 视频流方法 #### start_video_stream 启动屏幕视频流传输 ``` def start_video_stream(handle: int, w: int, h: int, bitrate: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | | w | int | 希望输出的宽度 | | h | int | 希望输出的高度 | | bitrate | int | 输出的比特率 | **返回值**: int - 操作结果 > 注意: 视频流回调函数的实现需要通过ctypes来完成 > --- #### stop_video_stream 关闭屏幕视频流 ``` def stop_video_stream(handle: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: int - 操作结果 --- ### 选择器方法 #### create_selector 创建一个筛选器 ``` def create_selector(handle: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | handle | int | openDevice返回的id | **返回值**: int - 筛选器唯一标识号 --- #### clear_selector 清空筛选器中所有的筛选条件 ``` def clear_selector(sel: int) -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | sel | int | new_selector返回的筛选器编号 | --- #### free_selector 释放筛选器 ``` def free_selector(sel: int) -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | sel | int | new_selector返回的筛选器编号 | --- #### find_nodes 使用筛选器去查找 ``` def find_nodes(sel: int, max_cnt_ret: int, timeout: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | sel | int | new_selector返回的筛选器编号 | | max_cnt_ret | int | 最大返回节点数 | | timeout | int | 查找超时时间(毫秒) | **返回值**: int - 结果集唯一标识编号 --- #### free_nodes 释放结果集 ``` def free_nodes(nodes: int) -> None: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | nodes | int | find_nodes返回的结果集 | --- ### 节点集合操作方法 #### get_nodes_size 获取结果集中节点的个数 ``` def get_nodes_size(nodes: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | nodes | int | find_nodes返回的结果集 | **返回值**: int - 节点个数 --- #### get_node_by_index 按照顺序从结果集中获取节点 ``` def get_node_by_index(nodes: int, index: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | nodes | int | find_nodes返回的结果集 | | index | int | 节点索引 | **返回值**: int - 节点句柄 --- #### get_node_parent 获取给定节点的父节点句柄 ``` def get_node_parent(node: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: int - 父节点句柄 --- #### get_node_child_count 获取给定节点的子节点数量 ``` def get_node_child_count(node: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: int - 子节点数量 --- #### get_node_child 获取给定节点的子节点 ``` def get_node_child(node: int, index: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | | index | int | 子节点索引 | **返回值**: int - 子节点句柄 --- ### 节点属性获取方法 #### get_node_json 获取节点的JSON字符串格式 ``` def get_node_json(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - JSON字符串 --- #### get_node_text 获取节点的文本属性 ``` def get_node_text(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - 文本内容 --- #### get_node_desc 获取节点的描述属性 ``` def get_node_desc(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - 描述内容 --- #### get_node_package 获取节点的包名属性 ``` def get_node_package(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - 包名 --- #### get_node_class 获取节点的类名属性 ``` def get_node_class(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - 类名 --- #### get_node_id 获取节点的资源ID属性 ``` def get_node_id(node: int) -> str: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: str - 资源ID --- #### get_node_bound 获取节点的范围属性 ``` def get_node_bound(node: int) -> Bounds: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: Bounds - 边界信息 **异常**: Exception - 获取失败时抛出异常 --- #### get_node_bound_center 获取节点的中心坐标 ``` def get_node_bound_center(node: int) -> Point: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: Point - 中心坐标 **异常**: Exception - 获取失败时抛出异常 --- ### 节点操作方法 #### Click_events 点击节点 ``` def Click_events(node: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: int - 1表示成功,0表示失败 --- #### longClick_events 长按节点 ``` def longClick_events(node: int) -> int: ``` | 参数 | 类型 | 说明 | | --- | --- | --- | | node | int | 节点句柄 | **返回值**: int - 1表示成功,0表示失败 --- ### 选择器筛选方法 #### 布尔属性筛选 | 方法名 | 说明 | | --- | --- | | `enable(sel: int, v: bool)` | 设置节点是否可用筛选器 | | `checkable(sel: int, v: bool)` | 设置节点是否可以被选中筛选器 | | `clickable(sel: int, v: bool)` | 设置节点是否可以被点击筛选器 | | `focusable(sel: int, v: bool)` | 设置节点是否可以获取焦点筛选器 | | `focused(sel: int, v: bool)` | 设置节点是否已经获取焦点筛选器 | | `scrollable(sel: int, v: bool)` | 设置节点是否可以滚动筛选器 | | `long_clickable(sel: int, v: bool)` | 设置节点是否可以长按筛选器 | | `password(sel: int, v: bool)` | 设置节点是否是密码筛选器 | | `selected(sel: int, v: bool)` | 设置节点是否被选中筛选器 | | `visible(sel: int, v: bool)` | 设置节点是否可见筛选器 | | `index(sel: int, v: int)` | 设置节点索引筛选器 | --- #### 边界筛选 | 方法名 | 说明 | | --- | --- | | `bounds_inside(sel: int, l: int, t: int, r: int, b: int)` | 设置在节点指定范围内的筛选器 | | `bounds_equal(sel: int, l: int, t: int, r: int, b: int)` | 设置节点的范围等于指定范围的筛选器 | --- #### ID匹配筛选 | 方法名 | 说明 | | --- | --- | | `id_equal(sel: int, str: str)` | 设置节点的资源ID等于指定id的筛选器 | | `id_start_with(sel: int, str: str)` | 设置节点的资源ID以指定字符串开头的筛选器 | | `id_end_with(sel: int, str: str)` | 设置节点的资源ID以指定字符串结尾的筛选器 | | `id_contain_with(sel: int, str: str)` | 设置节点的资源ID包含指定字符串的筛选器 | | `id_match_with(sel: int, str: str)` | 设置节点的资源ID正则匹配指定字符串的筛选器 | --- #### Text匹配筛选 | 方法名 | 说明 | | --- | --- | | `text_start_with(sel: int, str: str)` | 设置节点文本以指定字符串开头的筛选器 | | `text_end_with(sel: int, str: str)` | 设置节点文本以指定字符串结尾的筛选器 | | `text_contain_with(sel: int, str: str)` | 设置节点文本包含指定字符串的筛选器 | | `text_match_with(sel: int, str: str)` | 设置节点文本正则匹配指定字符串的筛选器 | --- #### Class匹配筛选 | 方法名 | 说明 | | --- | --- | | `clz_start_with(sel: int, str: str)` | 设置节点类名以指定字符串开头的筛选器 | | `clz_end_with(sel: int, str: str)` | 设置节点类名以指定字符串结尾的筛选器 | | `clz_contain_with(sel: int, str: str)` | 设置节点类名包含指定字符串的筛选器 | | `clz_match_with(sel: int, str: str)` | 设置节点类名正则匹配指定字符串的筛选器 | --- #### Package匹配筛选 | 方法名 | 说明 | | --- | --- | | `package_start_with(sel: int, str: str)` | 设置节点包名以指定字符串开头的筛选器 | | `package_end_with(sel: int, str: str)` | 设置节点包名以指定字符串结尾的筛选器 | | `package_contain_with(sel: int, str: str)` | 设置节点包名包含指定字符串的筛选器 | | `package_match_with(sel: int, str: str)` | 设置节点包名正则匹配指定字符串的筛选器 | --- #### Desc匹配筛选 | 方法名 | 说明 | | --- | --- | | `desc_start_with(sel: int, str: str)` | 设置节点描述以指定字符串开头的筛选器 | | `desc_end_with(sel: int, str: str)` | 设置节点描述以指定字符串结尾的筛选器 | | `desc_contain_with(sel: int, str: str)` | 设置节点描述包含指定字符串的筛选器 | | `desc_match_with(sel: int, str: str)` | 设置节点描述正则匹配指定字符串的筛选器 | --- ### 使用示例 #### 基础使用示例 ``` from common.mytRpc import MytRpc def main(): # 创建SDK实例 mytapi = MytRpc() # 获取SDK版本 sdk_ver = mytapi.get_sdk_version() print(f"SDK版本: {sdk_ver}") # 初始化并连接设备 # 注意:端口不是adb端口,计算公式为:30000 + (index - 1) × 100 + 2 # 例如:index=3 对应端口 30202 if mytapi.init("192.168.1.100", 30202, 10) == True: print("设备连接成功") try: # 检查连接状态 if mytapi.check_connect_state() == True: print("当前连接状态正常") else: print("当前连接断开") # 设置工作模式 if mytapi.setRpaWorkMode(0): print("设置工作模式为:关闭无障碍") finally: # SDK会自动管理资源,不需要手动释放 pass else: print("设备连接失败") if __name__ == "__main__": main() ``` #### 截图示例 ``` from common.mytRpc import MytRpc import cv2 import numpy as np def main(): mytapi = MytRpc() if mytapi.init("192.168.1.100", 30202, 10) == True: try: # 截图并保存到文件 mytapi.screentshot(1, 90, "screenshot.png") print("截图保存成功") # 截图并进行图像处理 byt_arr = mytapi.takeCaptrueCompress(0, 100) # 0=PNG格式,100=质量 if len(byt_arr) > 0: # 使用NumPy和OpenCV处理图像 img_np = np.frombuffer(byt_arr, dtype=np.uint8) img = cv2.imdecode(img_np, cv2.IMREAD_COLOR) # 显示图像 cv2.imshow("Screenshot", img) cv2.waitKey(3000) # 显示3秒 cv2.destroyAllWindows() print("截图显示成功") else: print("获取截图失败") finally: pass if __name__ == "__main__": main() ``` #### 触摸操作示例 ``` from common.mytRpc import MytRpc import time def main(): mytapi = MytRpc() if mytapi.init("192.168.1.100", 30202, 10) == True: try: # 单击操作 print("执行单击操作") mytapi.click(0, 500, 500) # 手指ID, X, Y time.sleep(1) # 长按操作 print("执行长按操作") mytapi.longClick(0, 500, 500, 1.0) # 手指ID, X, Y, 长按时间(秒) time.sleep(1) # 滑动操作 print("执行滑动操作") # 从(100, 500)滑动到(500, 500),耗时1秒 mytapi.swipe(0, 100, 500, 500, 500, 1000) # 手指ID, 起始X, 起始Y, 结束X, 结束Y, 持续时间(毫秒) finally: pass if __name__ == "__main__": main() ``` #### 按键和文本输入示例 ``` from common.mytRpc import MytRpc def main(): mytapi = MytRpc() if mytapi.init("192.168.1.100", 30202, 10) == True: try: # 发送文本 print("发送文本:Hello MYT RPA!") if mytapi.sendText("Hello MYT RPA!"): print("文本发送成功") # 模拟按键(返回键) print("执行返回键操作") if mytapi.keyPress(4): # 4=返回键 print("返回键执行成功") # 模拟Home键 print("执行Home键操作") if mytapi.keyPress(3): # 3=Home键 print("Home键执行成功") finally: pass if __name__ == "__main__": main() ``` #### App操作示例 ``` from common.mytRpc import MytRpc def main(): mytapi = MytRpc() if mytapi.init("192.168.1.100", 30202, 10) == True: try: # 打开应用 app_package = "com.blue.filemanager" print(f"打开应用:{app_package}") if mytapi.openApp(app_package): print("应用打开成功") # 关闭应用 print(f"关闭应用:{app_package}") if mytapi.stopApp(app_package): print("应用关闭成功") finally: pass if __name__ == "__main__": main() ``` #### 节点操作示例 ``` from common.mytRpc import MytRpc def main(): mytapi = MytRpc() if mytapi.init("192.168.1.100", 30202, 10) == True: try: # 导出节点树数据 print("导出节点树数据") node_xml = mytapi.dumpNodeXml(True) # True=导出所有节点 if node_xml: # 保存节点树到文件 with open("nodes.xml", "w", encoding='utf-8') as f: f.write(node_xml) print("节点树数据导出成功") # 使用选择器查找节点 print("使用选择器查找节点") selector = mytapi.create_selector() with selector: # 设置选择条件 selector.addQuery_TextContainWith('登录') # 文本包含"登录" selector.addQuery_Clickable(True) # 可点击 # 查找节点 node = selector.execQueryOne(2000) # 超时2000毫秒,返回第一个匹配节点 if node is not None: print("找到匹配节点") print(f"节点信息: {node.getNodeJson()}") # 点击节点 node.Click_events() print("节点点击成功") else: print("未找到匹配节点") finally: pass if __name__ == "__main__": main() ``` #### 按键码参考 ``` # 常用按键码列表 KEYCODE_CALL = 5 # 拨号键 KEYCODE_ENDCALL = 6 # 挂机键 KEYCODE_HOME = 3 # Home键 KEYCODE_MENU = 82 # 菜单键 KEYCODE_BACK = 4 # 返回键 KEYCODE_SEARCH = 84 # 搜索键 KEYCODE_CAMERA = 27 # 拍照键 KEYCODE_POWER = 26 # 电源键 KEYCODE_VOLUME_UP = 24 # 音量增加键 KEYCODE_VOLUME_DOWN = 25# 音量减小键 KEYCODE_ENTER = 66 # 回车键 KEYCODE_DEL = 67 # 退格键 KEYCODE_TAB = 61 # Tab键 ``` --- ### MYT RPA SDK Go 语言 API 文档 ##### 📌 端口列表 下载连接:[rpa_sdk-main.zip](https://pan.baidu.com/s/1fmi-S6OMD8Nq0EQ1QY-KLQ?pwd=14kz) ###### 桥接模式 - **IP 地址**:桥接设备的 IP 地址 - **RPA 端口**:固定为 **9083** ###### 非桥接模式 - **IP 地址**(如 192.168.30.2):用来定位具体的设备或宿主机 - **RPA 端口**:根据设备实例索引计算得出,计算公式为:**30000 + (index - 1) × 100 + 2** **Q1 设备端口列表**(index 1-12): | 实例位 | 实例位(index) | RPA 端口 | | --- | --- | --- | | 坑位1 | 1 | 30002 | | 坑位2 | 2 | 30102 | | 坑位3 | 3 | 30202 | | 坑位4 | 4 | 30302 | | 坑位5 | 5 | 30402 | | 坑位6 | 6 | 30502 | | 坑位7 | 7 | 30602 | | 坑位8 | 8 | 30702 | | 坑位9 | 9 | 30802 | | 坑位10 | 10 | 30902 | | 坑位11 | 11 | 31002 | | 坑位12 | 12 | 31102 | **P1 设备端口列表**(index 1-24): | 实例位 | 坑位号(index) | RPA 端口 | | --- | --- | --- | | 坑位1 | 1 | 30002 | | 坑位2 | 2 | 30102 | | 坑位3 | 3 | 30202 | | 坑位4 | 4 | 30302 | | 坑位5 | 5 | 30402 | | 坑位6 | 6 | 30502 | | 坑位7 | 7 | 30602 | | 坑位8 | 8 | 30702 | | 坑位9 | 9 | 30802 | | 坑位10 | 10 | 30902 | | 坑位11 | 11 | 31002 | | 坑位12 | 12 | 31102 | | 坑位13 | 13 | 31202 | | 坑位14 | 14 | 31302 | | 坑位15 | 15 | 31402 | | 坑位16 | 16 | 31502 | | 坑位17 | 17 | 31602 | | 坑位18 | 18 | 31702 | | 坑位19 | 19 | 31802 | | 坑位20 | 20 | 31902 | | 坑位21 | 21 | 32002 | | 坑位22 | 22 | 32102 | | 坑位23 | 23 | 32202 | | 坑位24 | 24 | 32302 | ### 目录结构 以下是 MYT RPA SDK GO 版本的完整目录结构: ``` MYT_RPA_SDK_GO_V10/ ├── rpa_sdk/ # Go SDK 核心包 │ ├── constant.go # 常量、指令码、事件定义 │ └── controller.go # 设备连接、指令发送、所有RPA功能实现 ├── test/ # 测试用例 │ └── rpa_test.go # 完整功能测试示例 │ └── image_q1.jpeg # 截图测试用例图片 │ └── image_q10.jpeg # 截图测试用例图片 ├── go.mod # Go 模块依赖 └── .gitignore # 忽略文件 ``` ### 引用文件功能与使用说明 #### 模块导入 Go 语言采用包管理方式导入,使用如下方式: ``` import ( "rpa_sdk/rpa_sdk" ) ``` #### 引用文件功能说明 以下是 SDK 中各个引用文件的详细功能介绍: | 文件名 | 功能说明 | | --- | --- | | `go.mod` | 定义 Go 模块名称 | | `constant.go` | 定义指令码、魔数、触摸事件、图像数据结构 | | `controller.go` | 设备连接、指令发送、截图、触摸、命令执行、按键、输入、启停 App | | `rpa_test.go` | 完整测试示例,包含所有功能调用演示 | #### 安装说明 1.将 rpa_sdk 文件夹复制到你的 Go 项目 2.执行依赖同步 ``` go mod tidy ``` 3.导入包即可使用 4.使用实例 ``` c := rpa_sdk.NewController() err := c.Connect("192.168.30.2", 30002) ``` ### 目录 - 一、初始化与连接 - 二、基础设备方法 - 三、截图方法 - 四、触摸操作方法 - 五、按键和文本输入 - 六、App操作方法 - 七、常量说明(constant.go) - 八、使用示例 具体查看controller.go文件 #### 一、初始化与连接 ##### 1.1. NewController 创建RPA控制器 ``` func NewController() *Controller ``` ##### 1.2. Connect 连接RPA设备 ``` func (c *Controller) Connect(ip string, port uint16) error ``` ##### 1.3. Close 关闭连接 ``` func (c *Controller) Close() ``` #### 二、基础设备方法 ##### 2.1. CheckAlive 检查设备是否存活 ``` func (c *Controller) CheckAlive() error ``` ##### 2.2. ExecCmd 执行 Shell 命令 ``` func (c *Controller) ExecCmd(wait int, cmd string) (string, error) ``` ##### 2.3. DumpNodeXml 获取当前界面 XML ``` func (c *Controller) DumpNodeXml(all bool) (string, error) ``` #### 三、截图方法 ##### 3.1. TakeCapture 全屏截图(原始数据) ``` func (c *Controller) TakeCapture() (ImageData, error) ``` ##### 3.2. TakeCaptureEx 压缩截图(JPEG/PNG) ``` func (c *Controller) TakeCaptureCompress(typ int, quality int) ([]byte, error) ``` #### 四、触摸操作方法 ##### 4.1. Touch 基础触摸事件 ``` func (c *Controller) Touch(touchId, event, x, y int) error ``` ##### 4.2. Swipe 滑动事件 ``` func (c *Controller) Swipe(touchId, x0, y0, x1, y1, ms int, async bool) error ``` #### 五、按键和文本输入 ##### 5.1. SendText 输入文本 ``` func (c *Controller) SendText(text string) error ``` ##### 5.2. KeyEvent 按键事件 ``` func (c *Controller) KeyEvent(keyCode int) error ``` #### 六、App操作方法 ##### 6.1. RunOrStopApp 运行或停止应用 ``` func (c *Controller) RunOrStopApp(flag int, packageName string) error ``` #### 七、常量说明(constant.go) 指令码 - CMD_CHECK_LIVE = 100 - CMD_TAKECAPTURE = 102 - CMD_TOUCHACTION = 104 - CMD_INPUTTEXT = 106 - CMD_RUNSTOAPP = 107 - CMD_EXEC = 110 触摸事件 - EVENT_TOUCH_DOWN = 1 - EVENT_TOUCH_UP = 2 - EVENT_TOUCH_MOVE = 3 - EVENT_TOUCH_TAP = 4 #### 八、使用示例 打开test目录下的rpa_test.go文件,可以看到完整的测试用例,这里只展示部分代码。 终端运行测试用例: ``` go test -v ./test ``` 使用示例: ``` package test import ( "fmt" //用于打印输出 "image" //用于处理图像数据 "image/png" //用于编码 PNG 图像 "os" //用于文件操作 "rpa_sdk/rpa_sdk" "testing" ) func TestRpa(t *testing.T) { c := rpa_sdk.NewController() //替换成自己的IP和端口 if err := c.Connect("10.10.11.3", 30002); err != nil { t.Errorf("Connect Error: %v", err) return } defer c.Close() /* 0000 00 00 88 77 66 55 04 00 00 00 01 00 00 00 04 00 0010 00 00 00 00 00 00 04 00 00 00 02 00 00 00 04 00 0020 00 00 68 00 00 00 24 00 00 00 88 77 66 55 04 00 0030 00 00 00 00 00 00 04 00 00 00 04 00 00 00 04 00 0040 00 00 64 00 00 00 04 00 00 00 64 00 00 00 */ /* 0040 09 80 88 77 66 55 04 00 00 00 01 00 00 00 04 00 0050 00 00 00 00 00 00 04 00 00 00 02 00 00 00 04 00 0060 00 00 68 00 00 00 24 00 00 00 88 77 66 55 04 00 0070 00 00 00 00 00 00 04 00 00 00 04 00 00 00 04 00 0080 00 00 64 00 00 00 04 00 00 00 64 00 00 00 */ //c.Touch(0, rpa_sdk.EVENT_TOUCH_TAP, 200, 100) //c.Swipe(0, 100, 100, 450, 450, 100, false) //c.SendText("for test") //c.KeyPress(3) //c.ExecCmd(1, "reboot") //c.RunOrStopApp(1, "mark.via") //c.RunOrStopApp(0, "mark.via") //fmt.Println(c.GetDisplayRotate()) // ========================================== 图像 data, err := c.TakeCapture() if err != nil { t.Fatalf("Take Error: %v", err) } fmt.Println(data.Width, data.Height, data.Stride) img := &image.RGBA{ Pix: data.Pixels, Stride: data.Stride, Rect: image.Rect(0, 0, data.Width, data.Height), } f, err := os.Create("./screen.png") if err != nil { t.Fatalf("Create Error: %v", err) } defer f.Close() png.Encode(f, img) // ========================================== 图像 //fmt.Println(c.DumpNodeXml(1)) //fmt.Println(c.DumpNodeXmlEx(true)) //fmt.Println(c.CheckAlive()) //fmt.Println(c.ExecCmd(1, "whoami")) // str -> root //data, err := c.TakeCaptureCompress(1, 1) //if err = os.WriteFile("image_q1.jpeg", data, 0644); err != nil { // t.Fatalf("WriteFile Error: %v", err) //} // //data, err = c.TakeCaptureCompress(1, 10) //if err = os.WriteFile("image_q10.jpeg", data, 0644); err != nil { // t.Fatalf("WriteFile Error: %v", err) //} } ``` --- ## MYTOS Android API *分类: Android API* ## MYTOS Android API - AI快速参考 v3 | 更新日期: 2026-05-07 ======================== ### 基础信息 #### 调用方式 - 所有接口使用 HTTP 协议,基础地址: `http://{ip}:{port}` - GET 用于查询和修改操作,POST 用于文件上传和大数据传输 - 某些参数需 URL 编码或 Base64 编码(见具体接口说明) #### 端口映射 桥接模式 — 每个设备有独立IP,所有设备端口固定: - ADB: 5555 - API (本文档接口): 9082 - RPA: 9083 - 投屏: 10000 - 控制: 10001 - 摄像头 TCP: 10006 - 摄像头 UDP: 10007 - WebRTC TCP/UDP: 10008 非桥接模式 — 所有设备共享宿主IP,端口按公式计算(index为实例编号): - ADB: `30000 + (index-1) x 100` - API: `30000 + (index-1) x 100 + 1` - RPA: `30000 + (index-1) x 100 + 2` - 投屏: `30000 + (index-1) x 100 + 3` - 控制: `30000 + (index-1) x 100 + 4` - 摄像头 TCP: `30000 + (index-1) x 100 + 5` - 摄像头 UDP: `30000 + (index-1) x 100 + 6` - WebRTC TCP: `30000 + (index-1) x 100 + 7` - WebRTC UDP: `30000 + (index-1) x 100 + 8` 示例: index=1 → API端口30001, index=2 → API端口30101 #### 关键概念 - Q1: 虚拟安卓设备型号,可创建1-12个(index 1-12) - P1: 另一虚拟安卓设备型号,可创建1-24个(index 1-24) - 桥接模式: 设备有独立IP,可被外部直接访问 - 非桥接模式(NAT): 设备共享宿主IP,通过宿主转发网络 #### 通用响应结构 成功: ```json { "code": 200, "msg": "ok", "data": {} } ``` 失败: ```json { "code": 201, "error": "错误原因" } ``` 或: ```json { "code": 202, "reason": "失败原因" } ``` #### 错误码 - 200: 成功 - 201: 通用错误,查看 error 字段 - 202: 操作失败,查看 reason 字段 ======================== ### 文件操作 #### 下载文件 GET /download 说明: 从设备下载指定文件到本地 参数: - path (必填): string - 文件完整绝对路径,如 /sdcard/Download/file.apk 请求示例: ``` curl "http://192.168.30.2:9082/download?path=/sdcard/Download/1.jpg" -o 1.jpg ``` 返回示例: 成功时返回文件二进制数据(浏览器自动下载),失败时: ```json { "code": 201, "error": "文件不存在或无权限访问" } ``` #### 文件上传(本地文件) POST /upload 说明: 通过 multipart/form-data 上传本地文件到设备 参数: - file (必填): file - 需要上传的文件 请求示例: ``` curl -X POST "http://192.168.30.2:9082/upload" -H "Content-Type: multipart/form-data" -F "file=@./1.txt" ``` 返回示例: ``` 文件上传完成! ``` #### 文件上传(远程URL) GET / 说明: 通过URL参数传递文件地址,接口自动下载并上传 参数: - task (必填): string - 固定值 upload - file (必填): string - 文件的URL地址 请求示例: ``` curl "http://192.168.30.2:9082/?task=upload&file=http://192.168.99.136:7878/%E6%8A%96%E9%9F%B3.apk" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` ======================== ### 剪贴板 #### 获取剪贴板内容 GET /clipboard 说明: 获取设备剪贴板中的文本内容(仅支持文本) 参数: 无 请求示例: ``` curl "http://192.168.30.2:9082/clipboard" ``` 返回示例: ```json { "code": 200, "msg": "query success", "data": { "text": "123" } } ``` #### 设置剪贴板内容 GET /clipboard 说明: 将文本内容设置到设备剪贴板,设置后立即生效 参数: - cmd (必填): int - 固定值 2 - text (必填): string - 要设置的文本内容(特殊字符需URL编码) 请求示例: ``` curl "http://192.168.30.2:9082/clipboard?cmd=2&text=123" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` ======================== ### S5代理 #### 查询S5代理状态 GET /proxy 说明: 查询设备S5代理服务状态 参数: 无 请求示例: ``` curl "http://192.168.30.2:9082/proxy" ``` 返回示例: 已启动: ```json { "code": 200, "msg": "query success", "data": { "status": 1, "statusText": "已启动", "addr": "socks5://test:123456@192.168.1.100:8080", "type": 2 } } ``` 未启动: ```json { "code": 200, "msg": "query success", "data": { "status": 0, "statusText": "未启动" } } ``` #### 设置S5代理 GET /proxy 说明: 启动或配置设备的S5代理服务 参数: - cmd (必填): int - 固定值 2 - ip (必填): string - S5服务器IP - port (必填): int - S5服务器端口 - usr (必填): string - S5用户名 - pwd (必填): string - S5密码 - type (选填): int - S5域名模式,1=本地域名解析,2=服务端域名解析 请求示例: ``` curl "http://192.168.30.2:9082/proxy?cmd=2&type=2&ip=192.168.1.100&port=8080&usr=test&pwd=123456" ``` 返回示例: ```json { "code": 200, "msg": "start success" } ``` #### 停止S5代理 GET /proxy 说明: 停止设备的S5代理服务 参数: - cmd (必填): int - 固定值 3 请求示例: ``` curl "http://192.168.30.2:9082/proxy?cmd=3" ``` 返回示例: ```json { "code": 200, "msg": "stop success" } ``` #### 设置S5域名过滤 POST /proxy 说明: 为S5代理设置域名过滤规则,变更立即生效 参数: - cmd (必填): int - 固定值 4 - body: JSON数组,逗号分隔的域名列表 请求示例: ``` curl -X POST "http://192.168.30.2:9082/proxy?cmd=4" -H "Content-Type: application/json" -d '["qq.com", "baidu.com"]' ``` 返回示例: ```json { "code": 200, "msg": "set success" } ``` ======================== ### 短信与通讯录 #### 接收短信 POST /sms 说明: 模拟接收短信消息 参数: - cmd (必填): int - 固定值 4 - address (必填): string - 发送者电话号码 - mbody (必填): string - 短信内容 - scaddress (选填): string - 短信中心号码 请求示例: ``` curl -X POST "http://192.168.30.2:9082/sms?cmd=4" -H "Content-Type: application/json" -d '{"address":"13800138000","body":"Hello, this is a test message.","scaddress":"+8613900000000"}' ``` 返回示例: ```json { "code": 200, "msg": "add inbox success", "data": { "status": 0 } } ``` #### 添加联系人 GET /addcontact 说明: 添加联系人到设备 参数: - data (必填): string - 联系人列表JSON字符串,每个对象含 user(姓名) 和 tel(电话) 请求示例: ``` curl "http://192.168.30.2:9082/addcontact?data=[{\"user\":\"张三\",\"tel\":\"13800138000\"},{\"user\":\"李四\",\"tel\":\"13900139000\"}]" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` #### 通话记录 GET /callog 说明: 添加容器通话记录 参数: - number (必填): string - 电话号码 - type (选填): int - 通话类型:1=呼出,2=接收,3=错过(默认) - date (选填): string - 时间戳(毫秒),默认当前时间 - duration (选填): int - 通话时长(秒),默认0 - presentation (选填): int - 显示方式,默认1 - subscription_id (选填): int - SIM卡ID,默认0 - is_read (选填): int - 是否已读,默认1 - new (选填): int - 是否新消息,默认1 - features (选填): int - 特性标志,默认0 请求示例: ``` curl "http://192.168.30.2:9082/callog?number=10086&type=2" ``` 返回示例: ```json { "code": 200, "msg": "query success" } ``` ======================== ### 证书 #### 上传Google证书 POST /uploadkeybox 说明: 上传或更新Google服务的PEM格式证书,上传后需重启设备生效 参数: - file (必填): file - PEM格式证书文件 请求示例: ``` curl -X POST "http://192.168.30.2:9082/uploadkeybox" -F "file=@1.pem" ``` 返回示例: ``` 导入完成permissionabcselinux123 ``` ======================== ### 系统控制 #### ADB切换权限 GET /adb 说明: 查询、开启、关闭ADB root权限。依赖设备已root且系统支持persist.adbd.shell属性,修改后需重新连接ADB 参数: - cmd (必填): string - 1=查询状态,2=开启root权限,3=关闭root权限 请求示例: ``` curl "http://192.168.30.2:9082/adb?cmd=1" curl "http://192.168.30.2:9082/adb?cmd=2" curl "http://192.168.30.2:9082/adb?cmd=3" ``` 返回示例: 查询: ```json { "code": 200, "msg": "query success", "data": { "status": 0, "statusText": "open" } } ``` 开启: ```json { "code": 200, "msg": "open adb root success" } ``` 关闭: ```json { "code": 200, "msg": "close adb root success" } ``` #### 后台保活 GET /background 说明: 为指定应用启用后台保活功能,防止被系统杀死。仅Android 14支持,镜像日期需大于20251217 参数: - cmd (必填): string - 1=查询,2=增加,3=删除,4=更新(被保活应用卸载重装后需调用) - package (选填): string - 应用包名(cmd=2和3时必填) 请求示例: ``` curl "http://192.168.30.2:9082/background?cmd=1" curl "http://192.168.30.2:9082/background?cmd=2&package=com.ss.android.ugc.aweme" curl "http://192.168.30.2:9082/background?cmd=3&package=com.ss.android.ugc.aweme" curl "http://192.168.30.2:9082/background?cmd=4" ``` 返回示例: 查询: ```json { "code": 200, "msg": "获取成功", "data": { "apps": [ "com.ss.android.ugc.aweme" ] } } ``` 添加: ```json { "code": 200, "msg": "添加成功" } ``` 删除: ```json { "code": 200, "msg": "移除成功" } ``` 更新: ```json { "code": 200, "msg": "更新成功", "data": { "apps": [ "com.ss.android.ugc.aweme" ] } } ``` #### 屏蔽按键 GET /disablekey 说明: 屏蔽或启用设备物理按键。仅Android 14支持 参数: - value (选填): string - 1=开启屏蔽,0=关闭屏蔽 请求示例: ``` curl "http://192.168.30.2:9082/disablekey?value=1" curl "http://192.168.30.2:9082/disablekey?value=0" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` #### 版本查询 GET /queryversion 说明: 查询设备或服务的版本信息 参数: 无 请求示例: ``` curl "http://192.168.30.2:9082/queryversion" ``` 返回示例: ```json { "code": 200, "msg": "3" } ``` #### 刷新定位 GET /task 说明: 根据IP刷新设备定位 参数: 无 请求示例: ``` curl "http://192.168.30.2:9082/task" ``` 返回示例: ```json { "code": 200 } ``` ======================== ### 摄像头 #### 虚拟摄像头热启动 GET /camera 说明: 热启动/停止虚拟摄像头,无需重启设备 参数: - cmd (必填): string - start=启动,stop=停止 - path (选填): string - rtmp地址或本地文件路径,首次需传参,后续不传则使用上次值 请求示例: ``` curl "http://192.168.30.2:9082/camera?cmd=start&path=/sdcard/Download/1.jpg" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` #### 设置虚拟摄像头源和类型 GET /modifydev 说明: 配置虚拟摄像头的视频源类型及地址,支持图像、视频、RTMP流、WebRTC、物理摄像头 参数: - cmd (必填): string - 固定值 4 - type (必填): string - 视频源类型:image / video / webrtc / rtmp / camera - path (必填): string - 资源路径:image/video为本地路径;rtmp/webrtc为流地址;camera固定值null - resolution (选填): int - 分辨率预设:1=自动(默认),2=1920x1080@30,3=1280x720@30 注意: 只传resolution不传type和path时,仅修改分辨率不切换源 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=4&type=camera&path=null" curl "http://192.168.30.2:9082/modifydev?cmd=4&type=video&path=/Dowload/11111.mp4&resolution=1" curl "http://192.168.30.2:9082/modifydev?cmd=4&resolution=3" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` 已知错误: "type is not find"、"path is empty"、"image not exist" #### 设置摄像头作为虚拟摄像头源 GET /modifydev 说明: 设置物理摄像头作为虚拟摄像头源,需配合魔云互联使用 参数: - cmd (必填): int - 固定值 4 - type (必填): string - 固定值 camera - path (必填): string - 固定值 null 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=4&type=camera&path=null" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` #### 查询当前虚拟摄像头源类型 GET /modifydev 说明: 查询当前设备视频源配置信息 参数: - cmd (必填): int - 固定值 5 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=5" ``` 返回示例: 初始状态: ```json { "code": 200, "path": "", "type": "", "resolution": "1" } ``` 已设置为摄像头: ```json { "code": 200, "path": "", "type": "camera", "resolution": "1" } ``` ======================== ### 应用管理 #### 导出APP信息 GET /backrestore 说明: 导出已安装应用的数据 参数: - cmd (必填): string - 固定值 backup - pkg (必填): string - 应用包名 - saveto (必填): string - 导出文件保存路径 请求示例: ``` curl "http://192.168.30.2:9082/backrestore?cmd=backup&pkg=com.ss.android.ugc.aweme&saveto=/sdcard/test.tar.gz" ``` 返回示例: ```json { "status": "success", "message": "Backup completed successfully" } ``` #### 导入APP信息 GET /backrestore 说明: 导入应用数据到设备,导入后需重启安卓生效。不需要pkg参数 参数: - cmd (必填): string - 固定值 recovery - backuppath (必填): string - 导入文件路径 请求示例: ``` curl "http://192.168.30.2:9082/backrestore?cmd=recovery&backuppath=/sdcard/test.tar.gz" ``` 返回示例: ```json { "status": "success", "message": "Recovery completed successfully" } ``` #### 批量安装APKS/XAPK分包 POST /installapks 说明: 上传ZIP压缩包批量安装APKS/XAPK文件。支持Android 10、12(v22.9.2起)、14 参数: - file (必填): file - 包含多个apks/xapk文件的ZIP压缩包 请求示例: ``` curl -X POST "http://192.168.30.2:9082/installapks" -H "Content-Type: multipart/form-data" -F "file=@/path/to/apks.zip" ``` 返回示例: ``` 全部安装完成 ``` #### 获取APP开机启动列表 GET /appbootstart 说明: 获取当前已配置为开机启动的应用列表 参数: - cmd (必填): int - 固定值 1 请求示例: ``` curl "http://192.168.30.2:9082/appbootstart?cmd=1" ``` 返回示例: ```json { "code": 200, "msg": "query success", "data": { "pkg": [ "cn.test", "android.ttt" ] } } ``` #### 设置指定APP开机启动 POST /appbootstart 说明: 设置一组应用为开机启动 参数: - cmd (必填): int - 固定值 2 - body (必填): list - APP包名数组(JSON格式) 请求示例: ``` curl "http://192.168.30.2:9082/appbootstart?cmd=2" -X POST -H "Content-Type: application/json" -d '["cn.test", "android.ttt"]' ``` 返回示例: ```json { "code": 200, "msg": "set success" } ``` #### 设置应用权限 GET /modifydev 说明: 授予指定应用所有权限 参数: - cmd (必填): int - 固定值 18 - pkg (必填): string - 应用包名 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=18&pkg=com.ss.android.ugc.aweme" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` ======================== ### 设备信息 #### 容器信息 GET /info 说明: 获取容器基本信息 参数: 无 请求示例: ``` curl "http://192.168.30.2:9082/info" ``` 返回示例: ```json { "code": 200, "msg": "ok", "data": { "hostIp": "-", "instance": "8", "name": "p738c384c1581ad24c3fcf199684f5f5_8", "buildTime": "1766829184" } } ``` #### 谷歌ID (GAID/ADID) GET /adid 说明: 自定义设置或随机生成谷歌广告ID 参数: - cmd (必填): string - 1=自定义设置(需传adid),2=生成随机ID - adid (选填): string - cmd=1时必填,要设置的谷歌ID值 请求示例: ``` curl "http://192.168.30.2:9082/adid?cmd=1&adid=my_adid" curl "http://192.168.30.2:9082/adid?cmd=2" ``` 返回示例: ```json { "succ": true, "msg": "generate random adid success", "data": { "adid": "my_adid" } } ``` #### 安装面具(模块管理) GET /modulemgr 说明: 检查、安装或卸载系统模块(Magisk/GMS)。安装后需重启 参数: - cmd (必填): string - check=检查状态,install=安装,uninstall=卸载 - module (必填): string - 模块名:magisk(Magisk模块) 或 gms(GMS模块) 响应规则: 已安装时msg返回"1",未安装返回"0",不支持的模块类型返回错误 请求示例: ``` curl "http://192.168.30.2:9082/modulemgr?cmd=check&module=gms" curl "http://192.168.30.2:9082/modulemgr?cmd=install&module=gms" curl "http://192.168.30.2:9082/modulemgr?cmd=uninstall&module=gms" ``` 返回示例: 检查(已安装): ```json { "code": 200, "msg": "1" } ``` 检查(未安装): ```json { "code": 200, "msg": "0" } ``` 安装成功: ```json { "code": 200, "msg": "OK" } ``` #### 获取后台允许Root授权的APP列表 GET /modifydev 说明: 获取当前后台配置中允许授予Root权限的应用包名列表 参数: - cmd (必填): string - 固定值 10 - action (必填): string - 固定值 list 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=10&action=list" ``` 返回示例: ```json { "code": 200, "msg": "获取成功", "data": { "apps": [ "com.ss.android.ugc.aweme" ] } } ``` #### 指定包名允许Root GET /modifydev 说明: 为指定包名的应用开启Root权限,需先安装对应APK 参数: - cmd (必填): string - 固定值 10 - pkg (必填): string - 应用包名 - root (必填): string - 固定值 true 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=10&pkg=com.ss.android.ugc.aweme&root=true" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` ======================== ### 截图 #### 截图(二进制返回) GET /snapshot 说明: 获取设备屏幕截图,返回图片二进制数据 参数: - type (选填): string - 截图类型 - quality (选填): int - 截图质量 1-100 请求示例: ``` curl "http://192.168.30.2:9082/snapshot" -o screenshot.jpg curl "http://192.168.30.2:9082/snapshot?quality=80" -o screenshot.jpg ``` 返回示例: 成功时返回图片二进制数据 #### 获取设备截图(按分辨率级别) GET / 说明: 获取安卓云机当前屏幕截图,支持不同分辨率级别,返回PNG二进制数据 参数: - task (必填): string - 固定值 snap - level (必填): int - 截图分辨率级别:1=低,2=中,3=原始分辨率 请求示例: ``` curl -X GET "http://192.168.30.2:9082/?task=snap&level=3" --output screenshot.png ``` 返回示例: 成功时直接返回PNG格式图片二进制数据 失败: ```json { "code": 500, "message": "获取截图失败,容器不存在或未运行", "data": null } ``` ======================== ### 自动操作 #### 自动点击 GET /autoclick 说明: 模拟触摸和按键操作 参数: - action (必填): string - 点击动作:touchdown(按下) / touchup(放开) / touchmove(移动) / tap(点击) / keypress(按键) - id (必填): int - 点击事件编号1-10,多指触控用 - x (必填): int - x坐标(keypress时可不传) - y (必填): int - y坐标(keypress时可不传) - code (选填): int - 按键键码,action为keypress时必填 常用按键码: 3=主菜单键,4=返回键,5=联系人键 请求示例: ``` curl "http://192.168.30.2:9082/autoclick?action=tap&id=1&x=100&y=100" curl "http://192.168.30.2:9082/autoclick?action=touchdown&id=1&x=100&y=100" curl "http://192.168.30.2:9082/autoclick?action=touchup&id=1&x=100&y=100" curl "http://192.168.30.2:9082/autoclick?action=touchmove&id=1&x=200&y=200" curl "http://192.168.30.2:9082/autoclick?action=keypress&id=1&code=4" ``` 返回示例: ```json { "code": 200, "msg": "ok" } ``` ======================== ### WebRTC播放器 #### WebRTC播放器 GET /webplayer/play.html 说明: 通过浏览器调用WebRTC播放器观看设备画面 参数: - shost (必填): string - WebRTC流媒体服务器主机地址 - sport (必填): string - WebRTC流媒体服务器端口(TCP) - q (必填): string - 视频质量:0=低,1=高 - v (必填): string - 视频编码格式,如 h264 - rtc_i (必填): string - RTC服务端IP(与shost一致) - rtc_p (必填): string - WebRTC端口(UDP) 请求示例: ``` http://192.168.99.108/webplayer/play.html?shost=192.168.99.108&sport=31207&q=1&v=h264&rtc_i=192.168.99.108&rtc_p=31208 ``` 返回: 成功时页面加载完成显示视频流 ======================== ### 定位与语言 #### IP定位 GET /modifydev 说明: 根据IP刷新设备定位,同时自动更新区域设置和系统环境 参数: - cmd (必填): string - 固定值 11 - launage (选填): string - 语言代码: zh/en/fr/th/vi/ja/ko/lo/in - ip (选填): string - 用户IP,用于确定地理位置相关设置 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=11&launage=en" curl "http://192.168.30.2:9082/modifydev?cmd=11&ip=23.247.138.215" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` #### 设置语言和国家 GET /modifydev 说明: 设置设备语言和国家 参数: - cmd (必填): string - 固定值 13 - language (必填): string - 语言代码(如 zh, en, fr, th, vi, ja, ko, lo, in) - country (必填): string - 国家代码(如 CN, US, JP, KR, TH, VN) 常见国家-语言对照: US=>en, CN=>zh, JP=>ja, KR=>ko, TH=>th, VN=>vi, FR=>fr, DE=>hsb, GB=>cy, RU=>os, IN=>hi, BR=>pt, AU=>en, SA=>ar, EG=>ar 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=13&language=zh&country=US" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` ======================== ### 指纹与传感器 #### 更新指纹信息 GET /modifydev 说明: 更新设备指纹信息(基站、位置、运营商、设备标识等)。仅适用于CQR14-ALL-v1.4.0版本镜像 参数: - cmd (必填): int - 固定值 7 - data (必填): string - 设备指纹信息JSON字符串(需URL编码) data JSON字段: - lac (选填): string - 位置区编码 - cid (选填): string - 小区标识 - lat (选填): string - 纬度 - lon (选填): string - 经度 - mcc (选填): string - 移动国家代码 - mnc (选填): string - 移动网络代码 - phonenumber (选填): string - 电话号码 - country (选填): string - 国家代码 - language (选填): string - 语言 - timezone (选填): string - 时区 - opercode (选填): string - 运营商代码 - opername (选填): string - 运营商名称 - iccid (选填): string - SIM卡ICCID - imsi (选填): string - IMSI - imei (选填): string - IMEI - gaid (选填): string - Google广告ID 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=7&data=%7B%27lac%27%3A+%2712345%27%7D" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` #### 设置摇一摇状态 GET /modifydev 说明: 设置设备摇一摇功能状态 参数: - cmd (必填): int - 固定值 17 - shake (必填): string - 1=启用,0=不启用 请求示例: ``` curl "http://192.168.30.2:9082/modifydev?cmd=17&shake=1" ``` 返回示例: ```json { "code": 200, "msg": "OK" } ``` ======================== ### 编码参考 Base64编码(用于应用列表等): ```python import base64 import json data = ["com.example.app1", "com.example.app2"] json_str = json.dumps(data) encoded = base64.urlsafe_b64encode(json_str.encode("utf-8")).decode("utf-8") ``` URL编码(用于包含特殊字符的参数): ```python from urllib.parse import quote data = '{"device_id":"abc123"}' encoded = quote(data) ``` --- ## 盒子-SDK-API *分类: 设备API* ## 盒子SDK API - AI快速参考 | 字段 | 类型 | 说明 | | --- | --- | --- | | code | int | 状态码,0表示成功,非0表示失败 | | message | string | 状态信息 | | data | object | 返回数据 | --- ### 📚 接口目录 --- ### 一、基本信息 #### 1. 获取API版本信息 **功能说明**:获取当前API版本信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/info ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/info" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "latestVersion": 110, "currentVersion": 108 } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | latestVersion | int | 线上最新版本号 | | currentVersion | int | 当前本地版本号 | **失败返回**: ``` { "code": 500, "message": "获取版本信息失败", "data": null } ``` **注意事项**: - 当 currentVersion < latestVersion 时,建议更新SDK --- #### 2. 获取设备基本信息 **功能说明**:获取当前设备的硬件和系统信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/info/device ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/info/device" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "ip": "192.168.99.108", "ip_1": "192.168.100.108", "hwaddr": "AA:BB:CC:DD:EE:FF", "hwaddr_1": "AA:BB:CC:DD:EE:F1", "cputemp": 45, "cpuload": "1.5", "memtotal": "8GB", "memuse": "4.2GB", "mmctotal": "256GB", "mmcuse": "120GB", "version": "v1.1.0", "deviceId": "MYT-P1-001", "model": "P1", "speed": "1000Mbps", "mmcread": "150MB/s", "mmcwrite": "120MB/s", "sysuptime": "10天5小时", "mmcmodel": "Samsung EVO", "mmctemp": "38", "network4g": "未连接", "netWork_eth0": "已连接" } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | ip | string | 网口IP | | ip_1 | string | 网口1的IP | | hwaddr | string | MAC地址 | | hwaddr_1 | string | MAC1地址 | | cputemp | int | CPU温度 | | cpuload | string | CPU负载 | | memtotal | string | 内存总大小 | | memuse | string | 内存已使用大小 | | mmctotal | string | 磁盘总大小 | | mmcuse | string | 磁盘已使用大小 | | version | string | 固件版本 | | deviceId | string | 设备ID | | model | string | 型号版本 | | speed | string | 网口速率 | | mmcread | string | 磁盘读取量 | | mmcwrite | string | 磁盘写入量 | | sysuptime | string | 设备运行时间 | | mmcmodel | string | 磁盘型号 | | mmctemp | string | 磁盘温度 | | network4g | string | 4G网卡状态 | | netWork_eth0 | string | ETH0网卡状态 | **失败返回**: ``` { "code": 500, "message": "获取设备信息失败", "data": null } ``` --- ### 二、云机操作 #### 1. 获取安卓云机列表 **功能说明**:获取设备上所有安卓云机容器列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 否 | string | 根据云机名过滤 | | running | 否 | bool | 根据云机是否运行过滤,false查询所有 | | indexNum | 否 | int | 根据云机实例位序号过滤(0-24) | **请求示例**: ``` # 获取所有云机 curl "http://192.168.99.108:8000/android" # 根据名称过滤 curl "http://192.168.99.108:8000/android?name=test" # 只获取运行中的云机 curl "http://192.168.99.108:8000/android?running=true" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 2, "list": [ { "id": "abc123def456", "name": "android-01", "status": "running", "indexNum": 1, "dataPath": "/myt/data/android-01", "modelPath": "/myt/model/android-01", "image": "registry.example.com/android:v12", "ip": "192.168.100.101", "networkName": "myt_bridge", "portBindings": {}, "dns": "223.5.5.5", "doboxFps": "60", "doboxWidth": "1080", "doboxHeight": "1920", "doboxDpi": "480", "mgenable": "0", "gmsenable": "0", "s5User": "", "s5Password": "", "s5IP": "", "s5Port": "", "s5Type": "0", "created": "2024-01-15 10:30:00", "started": "2024-01-15 10:31:00", "finished": "", "customBinds": [], "PINCode": "" } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | id | string | 云机容器ID | | name | string | 云机名称 | | status | string | 状态(running/stopped) | | indexNum | int | 云机实例位序号 | | dataPath | string | 云机Data文件在设备里的路径 | | modelPath | string | 云机机型文件在设备里的路径 | | image | string | 云机所用的镜像 | | ip | string | 云机局域网IP | | networkName | string | 容器网卡名称 | | dns | string | 云机DNS | | doboxFps | string | 云机FPS | | doboxWidth | string | 云机分辨率的宽 | | doboxHeight | string | 云机分辨率的高 | | doboxDpi | string | 云机DPI | | mgenable | string | magisk开关,0-关,1-开 | | gmsenable | string | gms开关,0-关,1-开 | | s5User | string | s5代理用户名 | | s5Password | string | s5代理密码 | | s5IP | string | s5代理IP | | s5Port | string | s5代理端口 | | s5Type | string | 代理类型,0-不开启,1-本地域名解析,2-服务器域名解析 | | created | string | 云机容器创建时间 | | started | string | 云机容器上次开启时间 | | finished | string | 云机容器上次关闭时间 | | customBinds | array | 自定义文件映射 | | PINCode | string | 自定义屏幕锁屏密码 | **失败返回**: ``` { "code": 500, "message": "获取云机列表失败", "data": null } ``` --- #### 2. 创建安卓云机 **功能说明**:创建一个新的安卓云机容器 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | imageUrl | 是 | string | 镜像完整地址 | | dns | 是 | string | 云机DNS,例如223.5.5.5 | | modelId | 否 | string | 线上机型ID | | modelName | 否 | string | 线上机型名称 | | LocalModel | 否 | string | 本地机型名称 | | modelStatic | 否 | string | 本地静态机型名称(优先级最高) | | indexNum | 否 | int | 实例序号,P1范围1-24,Q1范围1-12,传0自动分配 | | sandboxSize | 否 | string | 沙盒大小,例如16GB,32GB | | offset | 否 | string | 云机的开机时间 | | doboxFps | 否 | string | 云机FPS | | doboxWidth | 否 | string | 云机分辨率的宽 | | doboxHeight | 否 | string | 云机分辨率的高 | | doboxDpi | 否 | string | 云机DPI | | network | 否 | object | 独立IP设置 | | start | 否 | bool | 创建完成是否开机,默认true | | mgenable | 否 | string | magisk开关,0-关,1-开,默认0 | | gmsenable | 否 | string | gms开关,0-关,1-开,默认0 | | latitude | 否 | float | 纬度 | | longitude | 否 | float | 经度 | | countryCode | 否 | string | 国家代码,例如CN,US | | portMappings | 否 | array | 自定义端口映射 | | s5User | 否 | string | s5代理用户名 | | s5Password | 否 | string | s5代理密码 | | s5IP | 否 | string | s5代理IP | | s5Port | 否 | string | s5代理端口 | | s5Type | 否 | string | 代理类型,0-不开启,1-tun2socks,2-tun2proxy | | mytBridgeName | 否 | string | myt_bridge网卡名 | | customBinds | 否 | array | 自定义文件映射 | | PINCode | 否 | string | 屏幕锁屏密码,4-8位数字 | **network对象结构**: | 字段 | 类型 | 说明 | | --- | --- | --- | | gw | string | 网关,例如192.168.100.1 | | ip | string | 云机要设置的IP | | subnet | string | 掩码,例如192.168.100.0/24 | **portMappings数组元素结构**: | 字段 | 类型 | 说明 | | --- | --- | --- | | containerPort | int | 容器内端口 | | hostPort | int | 主机端口 | | hostIP | string | 主机IP | | protocol | string | 协议(tcp/udp),默认tcp | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "imageUrl": "registry.example.com/android:v12", "dns": "223.5.5.5", "modelId": "1", "indexNum": 1, "doboxFps": "60", "doboxWidth": "1080", "doboxHeight": "1920", "doboxDpi": "480", "start": true }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "id": "abc123def456789" } } ``` **失败返回**: ``` { "code": 500, "message": "创建云机失败: 镜像不存在", "data": null } ``` **注意事项**: - name必须唯一,不能与已有云机重名 - imageUrl必须是已拉取到本地的镜像地址 - indexNum范围:P1设备1-24,Q1设备1-12 - modelStatic优先级高于LocalModel和线上机型 --- #### 3. 重置安卓云机 **功能说明**:重置指定的安卓云机 **请求方式**:PUT **请求URL**: ``` http://{主机IP}:8000/android ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | latitude | 否 | float | 纬度 | | longitude | 否 | float | 经度 | | countryCode | 否 | string | 国家代码,例如CN,US | **请求示例**: ``` curl -X PUT "http://192.168.99.108:8000/android" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "latitude": 39.9042, "longitude": 116.4074, "countryCode": "CN" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "重置云机失败: 云机不存在", "data": null } ``` **注意事项**: - 重置会清除云机内的所有数据 - 重置前请确保已备份重要数据 --- #### 4. 删除安卓云机 **功能说明**:删除指定的安卓云机容器 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/android ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/android" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除云机失败: 云机正在运行", "data": null } ``` **注意事项**: - 删除操作不可恢复 - 建议删除前先停止云机 --- #### 5. 切换安卓镜像 **功能说明**:切换云机使用的安卓镜像 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/switchImage ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | imageUrl | 是 | string | 镜像完整地址 | | adbPort | 是 | int | 自定义adb端口,默认5555,设置端口0的时候不开启adb,请不要使用9082、9083、10000、10001、10006、10007、10008等端口 | | dns | 否 | string | 云机DNS | | enforce | 否 | boolean | 安全模式,默认开启 | | gmsenable | 否 | string | gms开关,0-关,1-开 | | macVlanIp | 否 | string | 独立IP设置 | | mgenable | 否 | string | magisk开关,0-关,1-开 | | mytBridgeName | 否 | string | myt_bridge网卡名,可以接口查询或在设备终端使用ifconfig查询 | | portMappings | 否 | array | 增加自定义端口映射 | | containerPort | 否 | int | 容器内端口,如80 | | hostIP | 否 | string | 主机IP,如 0.0.0.0 或 不填 | | hostPort | 否 | int | 主机端口,如8080 | | protocol | 否 | string | 协议,如 tcp、udp, 默认tcp | | randomFile | 否 | boolean | 随机系统文件名 | | start | 否 | boolean | 创建完成开机,默认不开机 | | vpcID | 否 | integer | 添加的魔云腾VPC节点ID | | whiteListDns | 否 | array | dns白名单 | | customBinds | 否 | array | 自定义文件映射 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/switchImage" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "imageUrl": "registry.example.com/android:v13", "start": true }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "message": "镜像切换成功" } } ``` **失败返回**: ``` { "code": 500, "message": "切换镜像失败: 镜像不存在", "data": null } ``` **注意事项**: - 切换镜像有风险,有概率无法开机,需要先备份 --- #### 6. 切换机型 **功能说明**:切换云机的机型配置 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/switchModel ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | modelId | 否 | string | 机型ID | | localModel | 否 | string | 本地机型名称 | | modelStatic | 否 | string | 本地静态机型名称 | | latitude | 否 | float | 纬度 | | longitude | 否 | float | 经度 | | countryCode | 否 | string | 国家代码 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/switchModel" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "modelId": "2", "countryCode": "US" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "切换机型失败: 机型不存在", "data": null } ``` --- #### 7. 拉取安卓镜像 **功能说明**:从镜像仓库拉取安卓镜像到本地 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/pullImage ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageUrl | 是 | string | 镜像完整地址 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/pullImage" \ -H "Content-Type: application/json" \ -d '{ "imageUrl": "registry.example.com/android:v12" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "拉取镜像失败: 网络连接超时", "data": null } ``` **注意事项**: - 拉取镜像可能需要较长时间,取决于网络速度和镜像大小 - 确保设备有足够的磁盘空间 --- #### 8. 启动安卓云机 **功能说明**:启动指定的安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/start ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/start" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "启动云机失败: 云机已在运行", "data": null } ``` --- #### 9. 关闭安卓云机 **功能说明**:关闭指定的安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/stop ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/stop" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "关闭云机失败: 云机未运行", "data": null } ``` --- #### 10. 重启安卓云机 **功能说明**:重启指定的安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/restart ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/restart" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "重启云机失败", "data": null } ``` --- #### 11. 获取本地镜像列表 **功能说明**:获取设备上已拉取的Docker镜像列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/image ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageName | 否 | string | 根据镜像名过滤 | **请求示例**: ``` curl "http://192.168.99.108:8000/android/image" # 根据名称过滤 curl "http://192.168.99.108:8000/android/image?imageName=android" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 2, "list": [ { "id": "sha256:abc123", "imageUrl": "registry.example.com/android:v12", "size": "2.5GB", "created": "2024-01-10 08:00:00", "labels": { "version": "12", "type": "android" } }, { "id": "sha256:def456", "imageUrl": "registry.example.com/android:v13", "size": "2.8GB", "created": "2024-01-15 10:00:00", "labels": { "version": "13", "type": "android" } } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | id | string | 镜像ID | | imageUrl | string | 镜像完整地址 | | size | string | 镜像大小 | | created | string | 创建时间 | | labels | object | 镜像labels | **失败返回**: ``` { "code": 500, "message": "获取镜像列表失败", "data": null } ``` --- #### 12. 删除本地镜像 **功能说明**:删除设备上的本地Docker镜像 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/android/image ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageUrl | 是 | string | 要删除的镜像完整地址 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/android/image" \ -H "Content-Type: application/json" \ -d '{ "imageUrl": "registry.example.com/android:v12" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除镜像失败: 镜像正在被使用", "data": null } ``` **注意事项**: - 正在被云机使用的镜像无法删除 - 删除前请确保没有云机依赖该镜像 --- #### 13. 获取本地镜像压缩包列表 **功能说明**:获取设备上的镜像压缩包文件列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/imageTar ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | filename | 否 | string | 根据文件名过滤 | **请求示例**: ``` curl "http://192.168.99.108:8000/android/imageTar" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 2, "list": [ { "name": "android_v12.tar", "size": "2.5GB" }, { "name": "android_v13.tar", "size": "2.8GB" } ] } } ``` **失败返回**: ``` { "code": 500, "message": "获取镜像压缩包列表失败", "data": null } ``` --- #### 14. 删除本地镜像压缩包 **功能说明**:删除设备上的镜像压缩包文件 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/android/imageTar ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | filename | 是 | string | 要删除的镜像压缩包名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/android/imageTar" \ -H "Content-Type: application/json" \ -d '{ "filename": "android_v12.tar" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除镜像压缩包失败: 文件不存在", "data": null } ``` --- #### 15. 导出安卓镜像 **功能说明**:将本地镜像导出为压缩包文件 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/image/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageUrl | 是 | string | 要导出的镜像完整地址 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/image/export" \ -H "Content-Type: application/json" \ -d '{ "imageUrl": "registry.example.com/android:v12" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "filename": "android_v12_20240115.tar" } } ``` **失败返回**: ``` { "code": 500, "message": "导出镜像失败: 磁盘空间不足", "data": null } ``` **注意事项**: - 导出过程可能需要较长时间 - 确保设备有足够的磁盘空间 --- #### 16. 下载导出后的安卓镜像包 **功能说明**:下载已导出的镜像压缩包文件 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/image/download ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | filename | 是 | string | 要下载的镜像包名 | **请求示例**: ``` curl "http://192.168.99.108:8000/android/image/download?filename=android_v12.tar" -o android_v12.tar ``` **成功返回**: - 返回文件的二进制数据,浏览器会自动下载 **失败返回**: ``` { "code": 500, "message": "下载失败: 文件不存在", "data": null } ``` --- #### 17. 导入安卓镜像 **功能说明**:通过上传tar文件导入安卓镜像 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/image/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入镜像包文件,tar格式 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/image/import" \ -F "file=@android_v12.tar" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入镜像失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持tar格式的镜像包 - 导入过程可能需要较长时间 --- #### 18. 导出安卓云机 **功能说明**:将安卓云机导出为压缩包 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/export" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "exportName": "android-01_20240115.zip" } } ``` **失败返回**: ``` { "code": 500, "message": "导出云机失败: 云机正在运行", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 导出文件包含云机的完整数据 --- #### 19. 导入安卓云机 **功能说明**:通过上传文件导入安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入使用本SDK导出的安卓云机 | | indexNum | 是 | int | 实例序号,P1范围1-24,Q1范围1-12 | | name | 否 | string | 导入后云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/import" \ -F "file=@android-01_20240115.zip" \ -F "indexNum=2" \ -F "name=android-02" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-02" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机失败: 实例位已被占用", "data": null } ``` **注意事项**: - 仅支持本SDK导出的云机文件 - indexNum不能与已有云机冲突 --- #### 20. 获取机型列表 **功能说明**:获取线上可用的机型列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/phoneModel ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/android/phoneModel" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "total": 50, "list": [ { "id": "1", "name": "Samsung Galaxy S23", "md5": "abc123def456", "status": "active", "currentVersion": 1, "sdk_ver": "v1.1.0", "createdAt": 1705286400 }, { "id": "2", "name": "Xiaomi 14", "md5": "def456ghi789", "status": "active", "currentVersion": 1, "sdk_ver": "v1.1.0", "createdAt": 1705286400 } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | id | string | 机型ID | | name | string | 机型名称 | | md5 | string | 机型文件MD5 | | status | string | 状态 | | currentVersion | int | 当前版本 | | sdk_ver | string | 对应SDK版本 | | createdAt | int64 | 创建时间戳 | **失败返回**: ``` { "code": 500, "message": "获取机型列表失败", "data": null } ``` --- #### 21. 获取国家代码列表 **功能说明**:获取可用的国家代码列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/countryCode ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/android/countryCode" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 200, "list": [ { "countryName": "中国", "countryCode": "CN" }, { "countryName": "美国", "countryCode": "US" }, { "countryName": "日本", "countryCode": "JP" } ] } } ``` **失败返回**: ``` { "code": 500, "message": "获取国家代码列表失败", "data": null } ``` --- #### 22. 设置Macvlan **功能说明**:设置Macvlan网络配置 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/macvlan ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | gw | 是 | string | 网关,例如192.168.100.1 | | subnet | 是 | string | 掩码,例如192.168.100.0/24 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/macvlan" \ -H "Content-Type: application/json" \ -d '{ "gw": "192.168.100.1", "subnet": "192.168.100.0/24" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置Macvlan失败: 网关格式错误", "data": null } ``` **注意事项**: - 设置后需要重启相关云机才能生效 - 确保网关和子网配置正确 --- #### 23. 设置云机容器IP (Macvlan模式) **功能说明**:在Macvlan模式下设置云机容器的IP地址 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/macvlan ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | ip | 是 | string | 云机要设置的IP | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/macvlan" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "ip": "192.168.100.101" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置IP失败: IP已被占用", "data": null } ``` --- #### 24. 修改云机容器名称 **功能说明**:修改云机容器的名称 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/rename ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机当前名称 | | newName | 是 | string | 云机新名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/rename" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "newName": "my-android" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "修改名称失败: 新名称已存在", "data": null } ``` **注意事项**: - 新名称不能与已有云机重名 - 建议在云机停止状态下修改 --- #### 25. 获取机型备份列表 **功能说明**:获取已备份的机型数据列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/android/backup/model" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 3, "list": [ { "name": "samsung_s23_backup" }, { "name": "xiaomi_14_backup" } ] } } ``` **失败返回**: ``` { "code": 500, "message": "获取机型备份列表失败", "data": null } ``` --- #### 26. 删除机型备份 **功能说明**:删除指定的机型备份数据 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 机型备份文件名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/android/backup/model" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23_backup" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除机型备份失败: 文件不存在", "data": null } ``` --- #### 27. 备份机型数据 **功能说明**:将V3镜像创建的云机里的机型数据完整备份 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 要备份机型数据的云机名称 | | suffix | 是 | string | 备份后机型数据的后缀名 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/model" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "suffix": "backup_20240115" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "备份机型数据失败: 云机不存在", "data": null } ``` **注意事项**: - 仅支持V3镜像创建的云机 - 备份前建议停止云机 --- #### 28. 导出机型备份数据 **功能说明**:导出已备份的机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/modelExport ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份机型数据文件名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/modelExport" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23_backup" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导出机型备份失败: 文件不存在", "data": null } ``` --- #### 29. 导入备份机型数据 **功能说明**:通过上传ZIP包导入备份的机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/modelImport ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入备份机型数据ZIP包 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/modelImport" \ -F "file=@samsung_s23_backup.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入机型备份失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持ZIP格式的备份文件 --- #### 30. 安卓云机执行命令 **功能说明**:在安卓云机内执行命令 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/exec ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | command | 是 | array | 执行的命令,数组形式 | **请求示例**: ``` 下载并安装小红书 APK curl --location --request POST 'http://192.168.99.91:8000/android/exec' \ --header 'User-Agent: Apifox/1.0.0 (https://apifox.com)' \ --header 'Content-Type: application/json' \ --header 'Accept: */*' \ --header 'Host: 192.168.99.91:8000' \ --header 'Connection: keep-alive' \ --data-raw '{ "name": "3333333", "command": ["sd", "-c", "curl -fsSL -o /sdcard/Download/xhs.apk https://redgray.xhscdn.com/ui/androidPublish/prod/xhs/channel/apk/9.17.0/9170809/v9170809-xhs-share_package_common_X64.apk && cp /sdcard/Download/xhs.apk /data/local/tmp/xhs.apk && pm install /data/local/tmp/xhs.apk && rm /data/local/tmp/xhs.apk"] }' ``` **成功返回**: ``` Success ``` **失败返回**: ``` { "code": 500, "message": "执行命令失败: 云机未运行", "data": null } ``` **注意事项**: - 云机必须处于运行状态 - command参数为数组格式,例如 `["sd", "-c", "ls -la"]` --- #### 31. 清理所有未被使用镜像 **功能说明**:清理设备上所有未被云机使用的镜像 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/pruneImages ``` **请求参数**:无 **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/pruneImages" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "pruneCount": 5, "releaseSpace": "12.5GB" } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | pruneCount | int | 清理数量 | | releaseSpace | string | 释放空间 | **失败返回**: ``` { "code": 500, "message": "清理镜像失败", "data": null } ``` **注意事项**: - 此操作不可恢复 - 仅清理未被任何云机使用的镜像 --- #### 32. 批量切换容器镜像 **功能说明**:清理设备上所有未被云机使用的镜像 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/change-image ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerNames | 是 | array | 云机容器名称列表 | | image | 是 | string | 目标镜像完整地址 | **请求示例**: ``` curl http://192.168.99.108:8000/android/change-image \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "containerNames": [ "test-1", "test-2", "test-3" ], "image": "registry.cn-guangzhou.aliyuncs.com/mytos/dobox:myt_supser_sdk_v1.0.14.30.36.1", }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "taskId": "mdezt502491dgy413sp0hpf200goqkoa", "message": "任务已提交,请使用 taskId 查询进度" } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | message | string | 任务提交成功 | | taskId | string | 任务ID | **失败返回**: ``` { "code": 51, "message": "The image field is required", "data": null } ``` --- #### 33. 复制云机 **功能说明**:复制指定云机的配置和数据,创建多个相同的云机实例 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/copy ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | indexNum | 否 | int | 实例序号,P1范围1-24,Q1范围1-12,要复制到的目标实例号 | | count | 否 | int | 复制的数量,一次最大20个 | **请求示例**: ``` GET "http://192.168.99.108:8000/android/copy?name=1772502684783_2_T0002&indexNum=6" 或者 curl 'curl 'http://192.168.99.108:8000/android/copy?name=1772502684783_2_T0002&indexNum=6' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` --- #### 34. 查询指定任务进度 **功能说明**:复制指定云机的配置和数据,创建多个相同的云机实例 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/task-status ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | taskId | 是 | string | 任务 ID(异步任务返回的 taskId) | **请求示例**: ``` GET "http://192.168.99.108:8000/android/copy?task-status?taskId=mdezt502491dgy413sp0hpf200goqkoa" 或者 curl 'http://192.168.99.183:8000/android/task-status?taskId=mdezt502491dgy413sp0hpf200goqkoa' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "taskId": "tmdezt502491dgy413sp0hpf200goqkoa", "taskType": "change-image", "progress": 80, "status": "running", "successCount": 2, "failCount": 0, "totalCount": 3, "failDetail": [], "createTime": "2026-03-09 16:30:00", "updateTime": "2026-03-09 16:35:00" } } ``` **失败返回**: ``` { "code": 50, "message": "任务不存在", "data": null } ``` --- #### 35. 通过URL导入云机 **功能说明**:通过备份包直链地址导入安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/importByUrl ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | url | 是 | string | 备份包直链地址,仅支持http/https | | indexNum | 否 | int | 实例序号,P1范围1-24,Q1范围1-12 | | name | 否 | string | 导入后云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/importByUrl" \ -H "Content-Type: application/json" \ -d '{ "url": "https://example.com/backups/android-01.zip", "indexNum": 3, "name": "android-imported" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-imported" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机失败: 网络连接超时", "data": null } ``` **注意事项**: - 仅支持http/https协议的直链地址 - indexNum不能与已有云机冲突 --- #### 36. 导出安卓云机到共享存储 **功能说明**:将安卓云机导出到共享存储服务 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/exportToOss ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 要导出的云机名称 | | ossUrl | 是 | string | 共享存储地址,例如http://192.168.1.100:8080 | | bucket | 是 | string | 存储桶名称 | | filePath | 否 | string | 文件存储路径,不填则自动生成 | | accessKey | 否 | string | 访问密钥(private Bucket时必填) | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/exportToOss" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "ossUrl": "http://192.168.1.100:8080", "bucket": "my-bucket", "filePath": "backups/android-01.tar.gz" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "url": "http://192.168.1.100:8080/my-bucket/backups/android-01.tar.gz" } } ``` **失败返回**: ``` { "code": 500, "message": "导出云机失败: OSS连接失败", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 确保OSS服务可用且bucket存在 --- ### 三、云机备份 #### 1. 获取备份压缩包文件列表 **功能说明**:获取设备上的云机备份压缩包文件列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/backup ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 否 | string | 根据文件名过滤 | **请求示例**: ``` curl "http://192.168.99.108:8000/backup" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 3, "list": [ { "name": "android-01_20240115.zip", "size": "1.2GB" }, { "name": "android-02_20240116.zip", "size": "1.5GB" } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | name | string | 备份压缩包文件名 | | size | string | 备份压缩包大小 | **失败返回**: ``` { "code": 500, "message": "获取备份列表失败", "data": null } ``` --- #### 2. 下载备份压缩包文件 **功能说明**:下载指定的云机备份压缩包 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/backup/download ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份压缩包文件名 | **请求示例**: ``` GET "http://192.168.99.108:8000/backup/download?name=android-01_20240115.zip" curl "http://192.168.99.108:8000/backup/download?name=android-01_20240115.zip" -o android-01_20240115.zip ``` **成功返回**: - 返回文件的二进制数据,浏览器会自动下载 **失败返回**: ``` { "code": 500, "message": "下载失败: 文件不存在", "data": null } ``` --- #### 3. 删除备份压缩包文件 **功能说明**:删除指定的云机备份压缩包 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/backup ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份压缩包文件名 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/backup" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01_20240115.zip" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除备份失败: 文件不存在", "data": null } ``` --- ### 四、终端连接 #### 1. 连接设备SSH ##### 1.1. 接口概述 **功能**: 通过 WebSocket 在浏览器中连接到设备的 SSH 终端 **协议**: WebSocket (基于 HTTP Upgrade) **端点**: `ws://{host}:8000/link/ssh` 或 `wss://{host}:8000/link/ssh` **认证**: 需要 HTTP Basic Auth (与其他 myt-sdk API 相同) **适用场景**: - Web 界面直接 SSH 到设备 (ARM 板、服务器等) - 无需本地 SSH 客户端 - 云手机平台的设备运维和调试 **与容器终端的区别**: - 容器终端 (`/link/exec`): 连接到 Docker 容器内部 - 设备 SSH (`/link/ssh`): 连接到宿主机或远程设备的 SSH 服务 ##### 1.2. 连接建立 ###### 1.2.1 WebSocket 握手 ``` // 前端示例 const wsUrl = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/link/ssh"; const socket = new WebSocket(wsUrl); socket.binaryType = "arraybuffer"; // 重要:设置为二进制模式 socket.onopen = () => { console.log("WebSocket 连接已建立"); }; socket.onmessage = (event) => { // SSH 输出始终是二进制数据 if (event.data instanceof ArrayBuffer) { const bytes = new Uint8Array(event.data); // 传递给终端模拟器渲染 terminal.write(bytes); } else { // 文本消息 (错误提示) console.log("服务端消息:", event.data); } }; socket.onerror = (error) => { console.error("WebSocket 错误:", error); }; socket.onclose = () => { console.log("WebSocket 连接已关闭"); }; ``` ###### 1.2.2 认证方式 **两层认证**: ``` // WebSocket 层认证 (同域下自动携带 Cookie) // 如果跨域,需要在 URL 中携带凭证或通过代理 // SSH 层认证 (通过 login 消息发送) socket.send( JSON.stringify({ type: "login", ip: "192.168.1.100", port: 22, user: "root", password: "your_password", }), ); ``` ##### 1.3. 消息协议 ###### 1.3.1 消息格式 所有客户端发送的消息均为 **JSON 文本格式**: ``` { "type": "消息类型", "...": "其他字段" } ``` 服务端返回的消息分为两种: - **二进制消息**: SSH 终端的输出 (stdout/stderr) - **文本消息**: 错误提示或状态信息 ###### 1.3.2 消息类型详解 ####### 1.3.2.1 login - SSH 登录 **方向**: 客户端 → 服务端 **时机**: WebSocket 连接建立后立即发送 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"login"` | | `ip` | string | ✅ | 目标设备 IP 地址 | | `port` | int | ✅ | SSH 端口 (通常为 22) | | `user` | string | ✅ | SSH 用户名(默认user) | | `password` | string | ✅ | SSH 密码(默认myt) | **示例**: ``` { "type": "login", "ip": "192.168.1.100", "port": 22, "user": "root", "password": "mypassword" } ``` **说明**: - `ip` 可以是 IPv4 地址、IPv6 地址或域名 - `port` 默认 SSH 端口为 22,如果设备使用自定义端口需指定 - 认证方式仅支持密码认证,不支持密钥认证 - SSH 连接超时时间为 5 秒 **错误响应示例**: ``` SSH连接失败: dial tcp 192.168.1.100:22: i/o timeout SSH连接失败: ssh: handshake failed: ssh: unable to authenticate 创建Session失败 RequestPty 失败 启动 Shell 失败 ``` **成功响应示例**: ``` 成功连接到 192.168.1.100:22 ``` ####### 1.3.2.2 stdin - 用户输入 **方向**: 客户端 → 服务端 **时机**: 用户在终端中输入字符时 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"stdin"` | | `data` | string | ✅ | 用户输入的数据 (包括控制字符) | **示例**: ``` { "type": "stdin", "data": "ls -la\r" } ``` **说明**: - `data` 包含原始按键数据,包括: 普通字符: `a`, `b`, `1`, `2` - 回车: `\r` 或 `\n` - 退格: `\x7f` (DEL) 或 `\x08` (BS) - 方向键: `\x1b[A` (上), `\x1b[B` (下), `\x1b[C` (右), `\x1b[D` (左) - Ctrl+C: `\x03` - Ctrl+D: `\x04` - 数据会直接写入 SSH 会话的 stdin ####### 1.3.2.3 resize - 调整终端大小 **方向**: 客户端 → 服务端 **时机**: 浏览器窗口大小变化时 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"resize"` | | `cols` | int | ✅ | 终端列数 (宽度) | | `rows` | int | ✅ | 终端行数 (高度) | **示例**: ``` { "type": "resize", "cols": 120, "rows": 30 } ``` **说明**: - 必须在 `login` 成功后发送 - 如果不发送 `resize`,SSH 终端会使用默认大小 (80x40) - 终端大小不匹配会导致显示错乱 (如 `vim` 编辑器) ####### 1.3.2.4 heartbeat - 保活心跳 **方向**: 客户端 → 服务端 **时机**: 定期发送 (建议 30 秒间隔) **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"heartbeat"` | **示例**: ``` { "type": "heartbeat" } ``` **说明**: - 防止 WebSocket 连接因空闲超时被中间代理关闭 - 服务端收到后不做任何响应,仅保持连接活跃 ###### 1.3.3 服务端响应 ####### 1.3.3.1 SSH 输出 (二进制消息) **方向**: 服务端 → 客户端 **格式**: WebSocket Binary Frame **内容**: SSH stdout/stderr 的原始字节流 **示例** (伪代码): ``` socket.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 二进制数据 - SSH 输出 const bytes = new Uint8Array(event.data); // 传递给 xterm.js 渲染 terminal.write(bytes); } }; ``` **说明**: - 包含 ANSI 转义序列 (颜色、光标控制等) - 需要使用终端模拟器 (如 xterm.js) 正确渲染 - 服务端启用了**括号粘贴模式** (Bracketed Paste Mode),输出中会包含 `\x1b[?2004h` (启用) 和 `\x1b[?2004l` (关闭) 转义序列。xterm.js 默认支持此模式,无需额外处理。如果使用自定义终端渲染器,需正确解析这些序列以避免显示异常 ####### 1.3.3.2 错误消息 (文本消息) **方向**: 服务端 → 客户端 **格式**: WebSocket Text Frame **内容**: 错误描述字符串 (带 ANSI 颜色代码) **示例**: ``` \r\n\x1b[31mSSH连接失败: dial tcp 192.168.1.100:22: connection refused\x1b[0m\r\n \r\n\x1b[32m成功连接到 192.168.1.100:22\x1b[0m\r\n ``` **说明**: - 错误消息使用红色 (`\x1b[31m`) - 成功消息**不含** ANSI 颜色码,为纯文本 (实测验证: 返回 `"成功连接到 127.0.0.1:22\n"` 无任何转义序列) - 消息末尾有 `\\x1b[0m` 重置颜色 (仅错误消息) ##### 1.4. 完整交互流程 ``` 客户端 服务端 | | |--- WebSocket Upgrade -------------->| || 建立 SSH 连接 | | 创建 SSH Session | | 请求 PTY (xterm) | | 启动 Shell || 调整 PTY 大小 | | |--- {"type":"stdin","data":"ls\r"} ->| 写入 SSH stdin || (无响应) | | |--- {"type":"stdin","data":"exit\r"}>| SSH 会话退出 | #### 1.5. 前端集成示例 ##### 1.5.1 使用 xterm.js (完整示例) ``` Web SSH Terminal - body { margin: 0; padding: 20px; background: #0c0c0c; } #terminal { width: 100%; height: 600px; } #login-form { background: #1e1e1e; padding: 20px; border-radius: 8px; max-width: 400px; margin: 50px auto; color: #fff; } input { width: 100%; padding: 8px; margin: 5px 0 15px; border: 1px solid #444; background: #2d2d2d; color: #fff; border-radius: 4px; } button { width: 100%; padding: 10px; background: #0078d4; color: #fff; border: none; border-radius: 4px; cursor: pointer; } button:hover { background: #005a9e; } SSH 连接 IP 地址: 端口: 用户名: 密码: 连接 let term, fitAddon, socket, heartbeatTimer; function connect() { const ip = document.getElementById("ip").value; const port = parseInt(document.getElementById("port").value); const user = document.getElementById("user").value; const password = document.getElementById("password").value; if (!ip || !user || !password) { alert("请填写完整信息"); return; } // 隐藏登录表单,显示终端 document.getElementById("login-form").style.display = "none"; document.getElementById("terminal").style.display = "block"; // 初始化终端 term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Consolas, "Courier New", monospace', theme: { background: "#0c0c0c" }, }); fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(document.getElementById("terminal")); fitAddon.fit(); term.focus(); // 连接 WebSocket const wsUrl = `ws://${location.host}/link/ssh`; //将${location.host}替换成直接要连接的设备IP和端口,例如:const wsUrl = `ws://10.10.11.4:8000/link/ssh`; socket = new WebSocket(wsUrl); socket.binaryType = "arraybuffer"; socket.onopen = () => { term.write("\r\n\x1b[32m正在建立连接...\x1b[0m\r\n"); // 发送登录认证 socket.send( JSON.stringify({ type: "login", ip: ip, port: port, user: user, password: password, }), ); // 发送终端大小 const dims = fitAddon.proposeDimensions(); socket.send( JSON.stringify({ type: "resize", cols: dims.cols, rows: dims.rows, }), ); // 启动心跳 heartbeatTimer = setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "heartbeat" })); } }, 30000); }; socket.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 二进制数据 - SSH 输出 term.write(new Uint8Array(event.data)); } else { // 文本消息 - 错误或状态 term.write(event.data); } }; socket.onerror = () => { term.write("\r\n\x1b[31mSocket 连接错误\x1b[0m\r\n"); }; socket.onclose = () => { term.write("\r\n\x1b[31m连接已断开, 请刷新页面重试\x1b[0m\r\n"); clearInterval(heartbeatTimer); }; // 用户输入 → WebSocket term.onData((data) => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "stdin", data: data })); } }); // 窗口大小变化 → WebSocket window.addEventListener("resize", () => { fitAddon.fit(); const dims = fitAddon.proposeDimensions(); if (socket.readyState === WebSocket.OPEN) { socket.send( JSON.stringify({ type: "resize", cols: dims.cols, rows: dims.rows, }), ); } }); } ``` ##### 1.5.2 使用原生 WebSocket (无终端模拟器) ``` const socket = new WebSocket("ws://192.168.1.100:8000/link/ssh"); socket.binaryType = "arraybuffer"; socket.onopen = () => { // 登录 socket.send( JSON.stringify({ type: "login", ip: "127.0.0.1", port: 22, user: "root", password: "mypassword", }), ); // 设置终端大小 socket.send( JSON.stringify({ type: "resize", cols: 80, rows: 24, }), ); // 执行命令 setTimeout(() => { socket.send( JSON.stringify({ type: "stdin", data: "uname -a\r", }), ); }, 1000); setTimeout(() => { socket.send( JSON.stringify({ type: "stdin", data: "exit\r", }), ); }, 3000); }; socket.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 二进制数据 - SSH 输出 const text = new TextDecoder().decode(event.data); console.log("输出:", text); } else { // 文本消息 - 错误或状态 console.log("消息:", event.data); } }; ``` 示例图: #### 1.6. 后端实现细节 ##### 1.6.1 技术栈 **Web 框架**: GoFrame v2 - **WebSocket**: gorilla/websocket - **SSH 客户端**: golang.org/x/crypto/ssh ##### 1.6.2 关键实现 ``` // 1. 升级 HTTP 为 WebSocket ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil) // 2. 建立 SSH 连接 config := &ssh.ClientConfig{ User: msg.User, Auth: []ssh.AuthMethod{ ssh.Password(msg.Password), }, HostKeyCallback: ssh.InsecureIgnoreHostKey(), // 生产环境应验证 host key Timeout: 5 * time.Second, } addr := fmt.Sprintf("%s:%d", msg.Ip, msg.Port) sshClient, err := ssh.Dial("tcp", addr, config) // 3. 创建 SSH Session session, err := sshClient.NewSession() // 4. 请求伪终端 (PTY) modes := ssh.TerminalModes{ ssh.ECHO: 1, } session.RequestPty("xterm", 40, 80, modes) // 5. 重定向 SSH 输出到 WebSocket type WebSocketWriter struct { ws *websocket.Conn } func (w *WebSocketWriter) Write(p []byte) (n int, err error) { err = w.ws.WriteMessage(websocket.BinaryMessage, p) if err != nil { return 0, err } return len(p), nil } session.Stdout = &WebSocketWriter{ws: ws} session.Stderr = &WebSocketWriter{ws: ws} // 6. 启动 Shell session.Shell() // 7. 双向转发 // WebSocket 输入 → SSH stdin sshStdin, _ := session.StdinPipe() for { _, msgBytes, err := ws.ReadMessage() if err != nil { break } var msg WsMsg json.Unmarshal(msgBytes, &msg) switch msg.Type { case "stdin": sshStdin.Write([]byte(msg.Data)) case "resize": session.WindowChange(msg.Rows, msg.Cols) } } // 8. 等待 Shell 退出 go func() { session.Wait() ws.Close() }() ``` #### 1.7. 常见问题 ##### 1.7.1 SSH 连接失败 **问题**: 收到 "SSH连接失败" 错误 **可能原因**: - 目标设备 SSH 服务未启动 - IP 地址或端口错误 - 网络不可达 (防火墙、路由问题) - 用户名或密码错误 **解决**: - 确认目标设备 SSH 服务运行: `systemctl status sshd` - 测试网络连通性: `ping ` 和 `telnet 22` - 验证用户名密码: `ssh user@ip` ##### 1.7.2 终端显示乱码 **问题**: 终端显示乱码或控制字符 **原因**: - 未使用终端模拟器渲染 ANSI 转义序列 - 终端大小未正确设置 - 字符编码不匹配 **解决**: - 使用 xterm.js 等终端模拟器 - 在 `login` 后立即发送 `resize` 消息 - 确保 WebSocket 设置为 `binaryType = 'arraybuffer'` ##### 1.7.3 连接意外断开 **问题**: WebSocket 连接在空闲时自动关闭 **原因**: - 中间代理 (Nginx/负载均衡器) 超时 - SSH 服务器超时断开 - 网络不稳定 **解决**: - 定期发送 `heartbeat` 消息 (建议 30 秒) - 配置代理服务器的 WebSocket 超时时间 - 配置 SSH 服务器的 `ClientAliveInterval` 和 `ClientAliveCountMax` ##### 1.7.4 无法执行交互式命令 **问题**: `vim`, `top`, `htop` 等命令显示异常 **原因**: - 终端大小未设置或不匹配 - 未分配伪终端 (PTY) **解决**: - 确保发送了正确的 `resize` 消息 - 后端必须调用 `session.RequestPty()` (已默认启用) ##### 1.7.5 密码认证失败 **问题**: 收到 "ssh: unable to authenticate" 错误 **可能原因**: - 密码错误 - SSH 服务器禁用了密码认证 (仅允许密钥认证) - 用户账户被锁定 **解决**: - 验证密码正确性 - 检查 SSH 服务器配置: `/etc/ssh/sshd_config` 中 `PasswordAuthentication` 应为 `yes` - 检查用户账户状态: `passwd -S ` #### 1.8. 安全建议 ##### 1.8.1 传输安全 ##### 1.8.2 认证鉴权 ##### 1.8.3 访问控制 ##### 1.8.4 防护措施 #### 1.9. 性能优化 #### 1.10. 与容器终端的对比 | 特性 | 设备 SSH (`/link/ssh`) | 容器终端 (`/link/exec`) | | --- | --- | --- | | 连接目标 | 远程设备 SSH 服务 | Docker 容器内部 | | 认证方式 | SSH 用户名+密码 | 容器 ID/名称 | | 网络要求 | 需要网络可达目标设备 | 本地 Docker API | | 权限 | 取决于 SSH 用户权限 | 容器内 root 权限 | | 适用场景 | 设备运维、远程管理 | 容器调试、应用排查 | | login 参数 | `ip`, `port`, `user`, `password` | `container_id`, `shell` | #### 11. 参考资源 - **xterm.js 文档**: https://xtermjs.org/ - **SSH 协议**: https://datatracker.ietf.org/doc/html/rfc4254 - **golang.org/x/crypto/ssh**: https://pkg.go.dev/golang.org/x/crypto/ssh - **WebSocket RFC**: https://datatracker.ietf.org/doc/html/rfc6455 #### 附录: 完整消息类型定义 ``` // TypeScript 类型定义 interface WsMsg { type: "login" | "stdin" | "resize" | "heartbeat"; // login 字段 ip?: string; port?: number; user?: string; password?: string; // stdin 字段 data?: string; // resize 字段 cols?: number; rows?: number; } // 使用示例 const loginMsg: WsMsg = { type: "login", ip: "192.168.1.100", port: 22, user: "root", password: "mypassword", }; const stdinMsg: WsMsg = { type: "stdin", data: "ls -la\r", }; const resizeMsg: WsMsg = { type: "resize", cols: 120, rows: 30, }; const heartbeatMsg: WsMsg = { type: "heartbeat", }; ``` #### 附录: 错误码参考 | 错误消息 | 原因 | 解决方案 | | --- | --- | --- | | `dial tcp ... i/o timeout` | 网络不可达或防火墙阻止 | 检查网络连通性和防火墙规则 | | `connection refused` | SSH 服务未启动 | 启动 SSH 服务: `systemctl start sshd` | | `ssh: unable to authenticate` | 密码错误或认证方式不支持 | 验证密码,检查 SSH 配置 | | `创建Session失败` | SSH 连接建立后无法创建会话 | 检查 SSH 服务器资源和配置 | | `RequestPty 失败` | 无法分配伪终端 | 检查 SSH 服务器 PTY 配置 | | `启动 Shell 失败` | 无法启动 Shell 进程 | 检查用户 Shell 配置 (`/etc/passwd`) | --- ### 2. 修改SSH登录用户密码 **功能说明**:修改SSH登录用户的密码 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/link/ssh/changePwd ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | username | 否 | string | 用户名,默认user | | password | 是 | string | 新密码 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/link/ssh/changePwd" \ -H "Content-Type: application/json" \ -d '{ "username": "user", "password": "newpassword123" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "修改密码失败", "data": null } ``` **注意事项**: - 密码修改后立即生效 - 请牢记新密码 --- ### 3. 开关SSH root登录 **功能说明**:启用或禁止SSH root用户登录 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/link/ssh/switchRoot ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | enable | 是 | bool | true-启用root登录,false-禁止root登录 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/link/ssh/switchRoot" \ -H "Content-Type: application/json" \ -d '{ "enable": true }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置失败", "data": null } ``` **注意事项**: - 出于安全考虑,建议禁用root登录 --- ### 4. 开关SSH服务 **功能说明**:启用或关闭SSH服务 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/link/ssh/enable ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | enable | 是 | bool | true-启用ssh服务,false-关闭ssh服务 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/link/ssh/enable" \ -H "Content-Type: application/json" \ -d '{ "enable": true }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置SSH服务失败", "data": null } ``` --- ### 5. 连接容器终端 #### 5.1. 接口概述 **功能**: 通过 WebSocket 在浏览器中连接到 Docker 容器的交互式终端 **协议**: WebSocket (基于 HTTP Upgrade) **端点**: `ws://{host}:8000/link/exec` 或 `wss://{host}:8000/link/exec` **认证**: 需要 HTTP Basic Auth (与其他 myt-sdk API 相同) **适用场景**: - Web 界面直接操作容器内部 - 无需 SSH 或 `docker exec` 命令 - 云手机平台的容器调试和运维 #### 5.2. 连接建立 ##### 5.2.1 WebSocket 握手 ``` // 前端示例 const wsUrl = (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/link/exec"; const socket = new WebSocket(wsUrl); socket.onopen = () => { console.log("WebSocket 连接已建立"); }; socket.onmessage = (event) => { // event.data 可能是文本或二进制数据 if (typeof event.data === "string") { console.log("收到文本:", event.data); } else { // 二进制数据 (容器输出) const arrayBuffer = event.data; } }; socket.onerror = (error) => { console.error("WebSocket 错误:", error); }; socket.onclose = () => { console.log("WebSocket 连接已关闭"); }; ``` ##### 5.2.2 认证方式 如果 myt-sdk 启用了 Basic Auth,需要在 WebSocket URL 中携带凭证: ``` // 方式1: URL 中携带 (不推荐,凭证会暴露在 URL) const wsUrl = `ws://username:password@${host}:8000/link/exec`; // 方式2: 通过 HTTP 请求头 (推荐) // 注意:浏览器 WebSocket API 不支持自定义请求头 // 需要先通过 HTTP 请求获取 session/token,或使用代理服务器 ``` **推荐方案**: 在同域下先通过普通 HTTP 请求完成认证,浏览器会自动携带 Cookie #### 5.3. 消息协议 ##### 5.3.1 消息格式 所有客户端发送的消息均为 **JSON 文本格式**: ``` { "type": "消息类型", "...": "其他字段" } ``` 服务端返回的消息分为两种: - **文本消息**: 错误提示或状态信息 - **二进制消息**: 容器终端的输出 (stdout/stderr) ##### 5.3.2 消息类型详解 ###### 5.3.2.1 login - 初始化连接 **方向**: 客户端 → 服务端 **时机**: WebSocket 连接建立后立即发送 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"login"` | | `container_id` | string | ✅ | 容器 ID 或容器名称 | | `shell` | string | ❌ | Shell 路径,默认 `/bin/sh` | **示例**: ``` { "type": "login", "container_id": "android_slot_0", "shell": "/bin/bash" } ``` **说明**: - `container_id` 可以是完整的容器 ID (64位十六进制) 或容器名称 - `shell` 可选值: `/bin/bash`, `/bin/sh`, `/bin/zsh` 等 (取决于容器内安装的 shell) - 如果容器不存在或无法连接,服务端会返回错误文本消息 **错误响应示例**: ``` Docker Client Init Error: Cannot connect to the Docker daemon Exec Create Error: No such container: android_slot_0 Exec Attach Error: container is not running ``` ###### 5.3.2.2 stdin - 用户输入 **方向**: 客户端 → 服务端 **时机**: 用户在终端中输入字符时 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"stdin"` | | `data` | string | ✅ | 用户输入的数据 (包括控制字符) | **示例**: ``` { "type": "stdin", "data": "ls -la\r" } ``` **说明**: - `data` 包含原始按键数据,包括: 普通字符: `a`, `b`, `1`, `2` - 回车: `\r` 或 `\n` - 退格: `\x7f` (DEL) 或 `\x08` (BS) - 方向键: `\x1b[A` (上), `\x1b[B` (下), `\x1b[C` (右), `\x1b[D` (左) - Ctrl+C: `\x03` - Ctrl+D: `\x04` - 数据会直接写入容器的 stdin ###### 5.3.2.3 resize - 调整终端大小 **方向**: 客户端 → 服务端 **时机**: 浏览器窗口大小变化时 **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"resize"` | | `cols` | int | ✅ | 终端列数 (宽度) | | `rows` | int | ✅ | 终端行数 (高度) | **示例**: ``` { "type": "resize", "cols": 120, "rows": 30 } ``` **说明**: - 必须在 `login` 成功后发送 - 如果不发送 `resize`,容器终端会使用默认大小 (通常是 80x24) - 终端大小不匹配会导致显示错乱 (如 `vim` 编辑器) ###### 5.3.2.4 heartbeat - 保活心跳 **方向**: 客户端 → 服务端 **时机**: 定期发送 (建议 30 秒间隔) **字段**: | 字段 | 类型 | 必填 | 说明 | | --- | --- | --- | --- | | `type` | string | ✅ | 固定值 `"heartbeat"` | **示例**: ``` { "type": "heartbeat" } ``` **说明**: - 防止 WebSocket 连接因空闲超时被中间代理关闭 - 服务端收到后不做任何响应,仅保持连接活跃 ##### 5.3.3 服务端响应 ###### 5.3.3.1 容器输出 (二进制消息) **方向**: 服务端 → 客户端 **格式**: WebSocket Binary Frame **内容**: 容器 stdout/stderr 的原始字节流 **示例** (伪代码): ``` socket.onmessage = (event) => { if (event.data instanceof ArrayBuffer) { // 二进制数据 - 容器输出 const bytes = new Uint8Array(event.data); // 传递给 xterm.js 渲染 terminal.write(bytes); } }; ``` **说明**: - 包含 ANSI 转义序列 (颜色、光标控制等) - 需要使用终端模拟器 (如 xterm.js) 正确渲染 ###### 5.3.3.2 错误消息 (文本消息) **方向**: 服务端 → 客户端 **格式**: WebSocket Text Frame **内容**: 错误描述字符串 **示例**: ``` Docker Client Init Error: Cannot connect to the Docker daemon at unix:///var/run/docker.sock Exec Create Error: No such container: invalid_container_id Exec Attach Error: container is not running ``` **说明**: - 仅在连接初始化失败时发送 - 收到错误消息后,WebSocket 连接会被服务端关闭 #### 5.4. 完整交互流程 ``` 客户端 服务端 | | |--- WebSocket Upgrade -------------->| || 创建 Docker Exec | | 劫持容器 stdin/stdout || 调整终端大小 | | |--- {"type":"stdin","data":"ls\r"} ->| 写入容器 stdin || (无响应) | | |--- {"type":"stdin","data":"exit\r"}>| 容器进程退出 | ##### 5.5. 前端集成示例 ###### 5.5.1 使用 xterm.js ``` - const term = new Terminal({ cursorBlink: true, fontSize: 14, fontFamily: 'Consolas, "Courier New", monospace', }); const fitAddon = new FitAddon.FitAddon(); term.loadAddon(fitAddon); term.open(document.getElementById("terminal")); fitAddon.fit(); // 连接 WebSocket const wsUrl = `ws://${location.host}/link/exec`; //location.host替换成自己的设备IP,端口固定为8000,例如:10.10.11.4:8000 const socket = new WebSocket(wsUrl); socket.onopen = () => { // 发送 login 消息 socket.send( JSON.stringify({ type: "login", //"android_slot_0"替换成容器id或名称,例如:1777544648515_1_T0001 container_id: "android_slot_0", shell: "/bin/sh", }), ); // 发送初始终端大小 const dims = fitAddon.proposeDimensions(); socket.send( JSON.stringify({ type: "resize", cols: dims.cols, rows: dims.rows, }), ); // 启动心跳 setInterval(() => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "heartbeat" })); } }, 30000); }; socket.onmessage = (event) => { if (typeof event.data === "string") { // 错误消息 term.write("\r\n\x1b[31m" + event.data + "\x1b[0m\r\n"); } else { // 容器输出 const reader = new FileReader(); reader.onload = () => { term.write(new Uint8Array(reader.result)); }; reader.readAsArrayBuffer(event.data); } }; socket.onerror = (error) => { term.write("\r\n\x1b[31mWebSocket Error\x1b[0m\r\n"); }; socket.onclose = () => { term.write("\r\n\x1b[33mConnection Closed\x1b[0m\r\n"); }; // 用户输入 → WebSocket term.onData((data) => { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify({ type: "stdin", data: data })); } }); // 窗口大小变化 → WebSocket window.addEventListener("resize", () => { fitAddon.fit(); const dims = fitAddon.proposeDimensions(); if (socket.readyState === WebSocket.OPEN) { socket.send( JSON.stringify({ type: "resize", cols: dims.cols, rows: dims.rows, }), ); } }); ``` ###### 5.5.2 使用原生 WebSocket (无终端模拟器) ``` const socket = new WebSocket("ws://192.168.1.100:8000/link/exec"); socket.onopen = () => { // 登录 socket.send( JSON.stringify({ type: "login", container_id: "my_container", shell: "/bin/sh", }), ); // 设置终端大小 socket.send( JSON.stringify({ type: "resize", cols: 80, rows: 24, }), ); // 执行命令 setTimeout(() => { socket.send( JSON.stringify({ type: "stdin", data: "uname -a\r", }), ); }, 1000); setTimeout(() => { socket.send( JSON.stringify({ type: "stdin", data: "exit\r", }), ); }, 3000); }; socket.onmessage = (event) => { if (typeof event.data === "string") { console.log("错误:", event.data); } else { // 二进制数据 const reader = new FileReader(); reader.onload = () => { const text = new TextDecoder().decode(reader.result); console.log("输出:", text); }; reader.readAsArrayBuffer(event.data); } }; ``` 示例图: ##### 5.6. 后端实现细节 ###### 5.6.1 技术栈 **Web 框架**: GoFrame v2 - **WebSocket**: gorilla/websocket - **Docker API**: docker/docker/client ###### 5.6.2 关键实现 ``` // 1. 升级 HTTP 为 WebSocket ws, err := wsUpGrader.Upgrade(r.Response.Writer, r.Request, nil) // 2. 创建 Docker Exec execConfig := types.ExecConfig{ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, // 关键:分配伪终端 Cmd: []string{"/bin/bash"}, } execResp, err := cli.ContainerExecCreate(ctx, containerID, execConfig) // 3. 劫持连接 resp, err := cli.ContainerExecAttach(ctx, execResp.ID, types.ExecStartCheck{Tty: true}) // 4. 双向转发 // 容器输出 → WebSocket go func() { buf := make([]byte, 4096) for { n, err := resp.Reader.Read(buf) if err != nil { return } ws.WriteMessage(websocket.BinaryMessage, buf[:n]) } }() // WebSocket 输入 → 容器 for { _, msgBytes, err := ws.ReadMessage() if err != nil { break } var msg ExecWsMsg json.Unmarshal(msgBytes, &msg) switch msg.Type { case "stdin": resp.Conn.Write([]byte(msg.Data)) case "resize": cli.ContainerExecResize(ctx, execID, types.ResizeOptions{ Height: uint(msg.Rows), Width: uint(msg.Cols), }) } } ``` ##### 5.7. 常见问题 ###### 5.7.1 连接失败 **问题**: WebSocket 握手失败 (HTTP 400/401/403) **原因**: - 未通过 Basic Auth 认证 - 请求头缺少 `Upgrade: websocket` **解决**: - 确保 WebSocket URL 包含认证信息或已通过 Cookie 认证 - 使用标准 WebSocket 客户端库 ###### 5.7.2 容器输出乱码 **问题**: 终端显示乱码或控制字符 **原因**: - 未使用终端模拟器渲染 ANSI 转义序列 - 终端大小未正确设置 **解决**: - 使用 xterm.js 等终端模拟器 - 在 `login` 后立即发送 `resize` 消息 ###### 5.7.3 连接意外断开 **问题**: WebSocket 连接在空闲时自动关闭 **原因**: - 中间代理 (Nginx/负载均衡器) 超时 - 网络不稳定 **解决**: - 定期发送 `heartbeat` 消息 (建议 30 秒) - 配置代理服务器的 WebSocket 超时时间 ###### 5.7.4 无法执行交互式命令 **问题**: `vim`, `top`, `htop` 等命令显示异常 **原因**: - 终端大小未设置或不匹配 - 未分配伪终端 (Tty) **解决**: - 确保发送了正确的 `resize` 消息 - 后端必须设置 `Tty: true` (已默认启用) ##### 5.8. 安全建议 ##### 5.9. 性能优化 ##### 5.10. 参考资源 - **xterm.js 文档**: https://xtermjs.org/ - **Docker Exec API**: https://docs.docker.com/engine/api/v1.43/#tag/Exec - **WebSocket RFC**: https://datatracker.ietf.org/doc/html/rfc6455 - **ANSI 转义序列**: https://en.wikipedia.org/wiki/ANSI_escape_code ##### 附录: 完整消息类型定义 ``` // TypeScript 类型定义 interface ExecWsMsg { type: "login" | "stdin" | "resize" | "heartbeat"; // login 字段 container_id?: string; shell?: string; // stdin 字段 data?: string; // resize 字段 cols?: number; rows?: number; } // 使用示例 const loginMsg: ExecWsMsg = { type: "login", container_id: "android_slot_0", shell: "/bin/bash", }; const stdinMsg: ExecWsMsg = { type: "stdin", data: "ls -la\r", }; const resizeMsg: ExecWsMsg = { type: "resize", cols: 120, rows: 30, }; const heartbeatMsg: ExecWsMsg = { type: "heartbeat", }; ``` --- ### 五、myt_bridge网卡管理 #### 1. 获取myt_bridge网卡列表 **功能说明**:获取设备上的myt_bridge网卡列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/mytBridge ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/mytBridge" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 2, "list": [ { "name": "myt_bridge", "cidr": "172.17.0.1/16" }, { "name": "myt_bridge_lan", "cidr": "10.0.0.1/16" } ] } } ``` **失败返回**: ``` { "code": 500, "message": "获取网卡列表失败", "data": null } ``` --- #### 2. 创建myt_bridge网卡 **功能说明**:创建新的myt_bridge网卡 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytBridge ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | customName | 是 | string | 自定义名(最多4字符),会拼接在myt_bridge后面 | | cidr | 是 | string | CIDR,例如10.0.0.1/16 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/mytBridge" \ -H "Content-Type: application/json" \ -d '{ "customName": "lan", "cidr": "10.0.0.1/16" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "创建网卡失败: 名称已存在", "data": null } ``` **注意事项**: - customName最多4个字符 - 创建后网卡名为 myt_bridge_customName --- #### 3. 更新myt_bridge网卡 **功能说明**:更新myt_bridge网卡的CIDR配置 **请求方式**:PUT **请求URL**: ``` http://{主机IP}:8000/mytBridge ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 网卡名(全称或自定义名) | | newCidr | 是 | string | 新CIDR,例如10.0.0.1/16 | **请求示例**: ``` curl -X PUT "http://192.168.99.108:8000/mytBridge" \ -H "Content-Type: application/json" \ -d '{ "name": "myt_bridge_lan", "newCidr": "10.0.0.1/24" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "更新网卡失败: 网卡不存在", "data": null } ``` --- #### 4. 删除myt_bridge网卡 **功能说明**:删除指定的myt_bridge网卡 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/mytBridge ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 网卡名(全称或自定义名) | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/mytBridge" \ -H "Content-Type: application/json" \ -d '{ "name": "myt_bridge_lan" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除网卡失败: 网卡正在被使用", "data": null } ``` **注意事项**: - 正在被云机使用的网卡无法删除 - 删除前请确保没有云机依赖该网卡 --- ### 六、macVlan网卡管理 #### 1. 获取网卡详情 **功能说明**:查询指定 ID 的 macVlan 网卡详细信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/macvlan ``` **请求参数**:无 **请求示例**: ``` GET "http://192.168.99.108:8000/macvlan" ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "macVlan": { "Name": "myt", "Id": "24732efd39a53baa9822d49302f701c878e701f2553574fc217e255f1c5289a9", "Created": "2026-02-04T13:42:39.460109818+08:00", "Scope": "local", "Driver": "macvlan", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "192.168.99.0/24", "Gateway": "192.168.99.1" } ] }, "Internal": false, "Attachable": false, "Ingress": false, "ConfigFrom": { "Network": "" }, "ConfigOnly": false, "Containers": {}, "Options": { "macvlan_mode": "private", "parent": "eth0" }, "Labels": {} } } } ``` **返回参数**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | Attachable | boolean | 是否允许手动附加容器 | | ConfigFrom | object | 配置来源对象 | | ConfigFrom.Network | string | 来源网络名称 | | ConfigOnly | boolean | 是否仅配置(不创建网络) | | Containers | object | 连接到该网络的容器映射,键为容器ID/名称,值为端点信息 | | Created | string (time.Time) | 网络创建时间 | | Driver | string | 网络驱动名称(如 bridge, overlay, macvlan 等) | | EnableIPv6 | boolean | 是否启用IPv6 | | Id | string | 网络ID | | Ingress | boolean | 是否为Swarm Ingress网络 | | Internal | boolean | 是否为内部网络(限制外部访问) | | IPAM | object | IP地址管理(IPAM)配置 | | IPAM.Driver | string | IPAM驱动名称(如 default) | | IPAM.Options | object | IPAM驱动选项 | | IPAM.Config | array | IPAM配置列表,每个元素为子网配置 | | IPAM.Config[].AuxiliaryAddresses | object | 辅助地址映射,键为地址名,值为IP | | IPAM.Config[].Gateway | string | 子网网关IP | | IPAM.Config[].IPRange | string | 分配的IP范围(如 172.17.0.0/24) | | IPAM.Config[].Subnet | string | 子网CIDR(如 172.17.0.0/16) | | Labels | object | 网络标签键值对 | | Name | string | 网络名称 | | Options | object | 网络驱动选项 | | Peers | array | 对等节点列表(仅Swarm模式) | | Peers[].IP | string | 对等节点IP地址 | | Peers[].Name | string | 对等节点名称 | | Scope | string | 网络作用域(local, swarm, global) | | Services | object | 服务列表(仅Swarm模式),键为服务名,值为服务信息 | **失败返回**: ``` { "code": 500, "message": "获取网卡详情失败", "data": null } ``` --- #### 2. 创建网卡 **功能说明**:创建新的macVlan网卡 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/macvlan ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | gw | 是 | string | 网关,例如 192.168.100.1 | | subnet | 是 | string | 掩码,例如 192.168.100.0/24 | | private | 否 | boolean | 是否禁止macvlan容器互相访问,默认为 true | **请求示例**: ``` POST http://192.168.99.108:8000/macvlan Content-Type: application/json 请求体 { "gw": "192.168.100.1", "subnet": "192.168.100.0/24", "private": true } 或者 curl http://192.168.99.108:8000/macvlan \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "gw": "", "subnet": "192.168.100.0/24", "private": true }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "id": "string" } } ``` **返回参数**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | id | string | 创建成功的网卡ID | **失败返回**: ``` { "code": 500, "message": "创建网卡失败", "data": null } ``` --- #### 3. 更新网卡 **功能说明**:更新一个已存在的 macVlan 网卡的配置。 **请求方式**:PUT **请求URL**: ``` http://{主机IP}:8000/macvlan ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | gw | 是 | string | 新的网关,例如 192.168.100.1 | | subnet | 是 | string | 新的掩码,例如 192.168.100.0/24 | | private | 否 | boolean | 是否禁止macvlan容器互相访问,默认为 true | **请求示例**: ``` POST http://192.168.99.108:8000/macvlan Content-Type: application/json 请求体 { "gw": "192.168.100.1", "subnet": "192.168.100.0/24", "private": true } 或者 curl http://192.168.99.108:8000/macvlan \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "gw": "192.168.100.1", "subnet": "192.168.100.0/24", "private": true }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "id": "string" } } ``` **返回参数**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | id | string | 创建成功的网卡ID | **失败返回**: ``` { "code": 500, "message": "创建网卡失败", "data": null } ``` --- #### 4. 删除网卡 **功能说明**:更新一个已存在的 macVlan 网卡的配置。 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/macvlan ``` **请求参数**:无 **请求示例**: ``` curl http://192.168.99.108:8000/macvlan \ --request DELETE ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` --- ### 七、魔云腾VPC #### 1. 获取网络分组列表 **功能说明**:查询所有网络分组列表,支持按别名过滤 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/mytVpc/group ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | alias | 否 | string | 分组别名,传空查询所有 | **请求示例**: ``` # 查询所有分组 GET "http://{主机IP}:8000/mytVpc/group" # 按别名过滤 curl "http://192.168.99.108:8000/mytVpc/group?alias=test-group" ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "count": 1, "list": [ { "id": 11, "alias": "111saa", "url": "", "vpcs": { "vpcCount": 1, "list": [ { "id": 289, "groupId": 11, "remarks": "socks_220csx", "protocol": "socks", "profile": "{\"configType\":3,\"remarks\":\"socks_220csx\",\"server\":\"nub2ccs1.user.wuyouip.com\",\"serverPort\":\"xxxx\",\"password\":\"xxxx\",\"username\":\"xxxx\"}", "outConfig": "{\"protocol\":\"socks\",\"sendThrough\":null,\"tag\":\"socks_220csx_11_1769492725191\",\"settings\":{\"servers\":[{\"address\":\"nub2ccs1.user.wuyouip.com\",\"port\":xxxx,\"level\":8,\"users\":[{\"user\":\"xxxxx\",\"pass\":\"xxxxxx\",\"level\":8}]}]},\"streamSettings\":null,\"proxySettings\":null,\"mux\":{\"enabled\":false,\"concurrency\":-1,\"xudpConcurrency\":0,\"xudpProxyUDP443\":\"\"},\"targetStrategy\":\"\"}", "source": 2, "tag": "socks_220csx_11_1769492725191" } ] } } ] } } ``` --- #### 2. 增加网络分组列表 **功能说明**:创建新的网络分组,支持订阅地址或节点地址模式 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/group ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | alias | 是 | string | 网络分组别名 | | addresses | 否 | array | 批量添加节点列表(source=2 时必填) | | source | 否 | int | 1-订阅地址,2-节点地址,默认 | | url | 否 | string | 批量添加节点列表(source=1 时必填) | **请求示例**: ``` # 订阅地址模式 curl -X POST "http://192.168.99.108:8000/mytVpc/group" \ -H "Content-Type: application/json" \ -d '{ "alias": "test-group", "source": 1, "url": "http://example.com/subscribe" }' # 节点地址模式 curl -X POST "http://192.168.99.108:8000/mytVpc/group" \ -H "Content-Type: application/json" \ -d '{ "alias": "node-group", "source": 2, "addresses": ["192.168.1.100:8080", "192.168.1.101:8080"] }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` # 分组名称未填 { "code": 51, "message": "The alias field is required", "data": null } # 填错地址 { "code": 50, "message": "Get \"\": unsupported protocol scheme \"\"", "data": null } # 分组已存在 { "code": 10021, "message": "Error: 此订阅分组已存在无法新建", "data": null } ``` --- #### 3. 更新网络分组名 **功能说明**:修改指定网络分组的别名 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/group/alias ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | id | 是 | int | 网络分组ID | | newAlias | 是 | string | 分组新别名 | **请求示例**: ``` curl http://192.168.99.108:8000/mytVpc/group/alias \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "id": 1, "newAlias": "111" }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "更新分组别名失败: 新别名已存在", "data": null } ``` --- #### 4. 删除网络分组列表 **功能说明**:删除指定的网络分组,删除前需确保分组内无 VPC 节点 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/mytVpc/group ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | id | 是 | int | 网络分组ID | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/mytVpc/group" \ -H "Content-Type: application/json" \ -d '{ "id": 2 }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 10022, "message": "Error: 此订阅分组不存在", "data": null } ``` --- #### 5. 指定云机 VPC 节点 **功能说明**:为指定云机绑定 VPC 节点及 DNS 白名单 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/addRule ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机容器名称 | | vpcID | 是 | int | VPC 节点 ID | | WhiteListDns | 否 | array | VDNS 白名单列表 | **请求示例**: ``` curl http://192.168.99.108:8000/mytVpc/addRule \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "name": "p738c384c1581ad24c3fcf199684f5f5_13_T00013", "vpcID": 1, "WhiteListDns": [ "" ] } ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 51, "message": "The name field is required", "data": null } 或 { "code": 10023, "message": "Error: 此VPC节点不存在", "data": null } ``` --- #### 6. 已设置云机 VPC 节点 **功能说明**:查询已绑定 VPC 节点的云机规则列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/mytVpc/containerRule ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/mytVpc/containerRule" ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "count": 2, "list": [ { "id": 1, "containerID": "5fd727704a60691f7ed5c13575ac261c0f28eebb9aea8141412818f74fa110d5", "containerIP": "", "containerName": "Test_1", "containerState": "running", "status": 1, "groupName": "111saa", "vpcRemarks": "socks_220csx", "WhiteListDns": [] }, { "id": 2, "containerID": "e2e38a2d7ea53d40e58ba5d54327b807a714139dca0ec69faeb176336686a060", "containerIP": "172.17.0.7", "containerName": "p738c384c1581ad24c3fcf199684f5f5_13_T00013", "containerState": "running", "status": 1, "groupName": "test-grup", "vpcRemarks": "澳洲AX01 50500进阶版 0.3x", "WhiteListDns": [] } ] } } ``` --- #### 8. 删除网络分组内节点 **功能说明**:从指定网络分组中移除一个云机节点。 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/mytVpc ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | vpcID | 是 | int | 要删除的 VPC 节点 ID | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/mytVpc" \ -H "Content-Type: application/json" \ -d '{ "vpcID": 1 }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 10023, "message": "Error: 此VPC节点不存在", "data": null } ``` --- #### 9. 更新指定网络分组 **功能说明**:更新指定网络分组的配置信息。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/group/update ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | ID | 是 | int | 网络分组ID | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/mytVpc" \ -H "Content-Type: application/json" \ -d '{ "vpcID": 1 }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 10022, "message": "Error: 此订阅分组不存在", "data": null } ``` --- #### 10. 增加socks5节点 **功能说明**:向指定网络分组添加 socks5 节点。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/socks ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | alias | 是 | string | 订阅分组别名,如果别名不存在将创建新分组,若存在则加入到此分组中 | | list | 是 | array | socks5 节点列表 | **list 数组元素结构**: | 字段 | 类型 | 说明 | | --- | --- | --- | | remarks | string | 节点别名 | | socksIp | string | s5 IP | | socksPort | int | s5 端口 | | socksUser | string | s5 用户名 | | socksPassword | istring | s5 密码 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/mytVpc/socks" \ -H "Content-Type: application/json" \ -d '{ "alias": "socks-group", "list": [ { "remarks": "node1", "socksIp": "192.168.1.100", "socksPort": 1080, "socksUser": "user1", "socksPassword": "pass123" } ] }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 51, "message": "The alias field is required", "data": null } ``` --- #### 11. 开关DNS白名单 **功能说明**:启用 / 禁用指定 VPC 规则的 DNS 白名单。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/whiteListDns ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | ruleID | 是 | int | 规则 ID | | enable | 否 | bool | 是否启用,默认 true | | whiteListDns | 否 | array | DNS 白名单列表 | **请求示例**: ``` curl http://192.168.99.108:8000/mytVpc/whiteListDns \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "ruleID": 1, "enable": true, "whiteListDns": [ "" ] }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` --- #### 12. VPC节点延迟测试 **功能说明**:测试指定 VPC 节点的网络延迟。 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/mytVpc/test ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | address | 是 | string | 节点地址,address 格式如 "1.1.1.1:80" 或 "www.google.com:443" | **请求示例**: ``` curl 'http://192.168.99.108:8000/mytVpc/test?address=50500.b-vm915x.8h2jssajkd.g-songs.ting-wo-shuo-xiexieni.com' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "msg": "dial tcp: address 50500.b-vm915x.8h2jssajkd.g-songs.ting-wo-shuo-xiexieni.com: missing port in address", "latency": "0ms" } } ``` --- #### 13. 批量指定云机VPC节点 **功能说明**:批量指定云机VPC节点 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/addRule/batch ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | names | 是 | array | 云机容器名称列表 | | vpcID | 是 | integer | vpc节点ID | | WhiteListDns | 否 | array | DNS白名单 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "names": ["android_1", "android_2"], "vpcID": 123, "WhiteListDns": ["8.8.8.8"] }' "http://192.168.99.108:8000/mytVpc/addRule/batch" ``` **成功返回**: ``` { "code": 0, "message": "批量指定成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "批量指定失败", "data": null } ``` --- #### 14. 清除云机VPC节点 **功能说明**:清除指定云机的VPC节点 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/delRule ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机容器名称 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{"name": "android_1"}' "http://192.168.99.108:8000/mytVpc/delRule" ``` **成功返回**: ``` { "code": 0, "message": "清除成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "清除失败,容器不存在", "data": null } ``` --- --- #### 15. 批量清除云机VPC节点 **功能说明**:批量清除云机的VPC节点 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/delRule/batch ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机容器名称列表 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{"name": ["android_1", "android_2"]}' "http://192.168.99.108:8000/mytVpc/delRule/batch" ``` **成功返回**: ``` { "code": 0, "message": "批量清除成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "批量清除失败", "data": null } ``` --- #### 16. 清除容器域名过滤 **功能说明**:清除指定容器的域名过滤设置 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerID | 是 | string | 容器ID或名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/mytVpc/domainFilter?containerID=abc123" ``` **成功返回**: ``` { "code": 0, "message": "清除成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "清除失败,容器不存在", "data": null } ``` --- #### 17. 查询容器域名过滤 **功能说明**:查询指定容器的域名过滤列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerID | 是 | string | 容器ID或名称 | **请求示例**: ``` curl -X GET "http://192.168.99.108:8000/mytVpc/domainFilter?containerID=abc123" ``` **成功返回**: ``` { "code": 0, "message": "查询成功", "data": { "domains": ["example.com", "google.com"] } } ``` **失败返回**: ``` { "code": 500, "message": "查询失败,容器不存在", "data": null } ``` --- #### 18. 设置容器域名过滤 **功能说明**:设置指定容器的域名过滤列表 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerID | 是 | string | 容器ID或名称 | | domains | 是 | array | 域名列表,传空数组则清空 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "containerID": "abc123", "domains": ["example.com", "google.com"] }' "http://192.168.99.108:8000/mytVpc/domainFilter" ``` **成功返回**: ``` { "code": 0, "message": "设置成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置失败", "data": null } ``` --- #### 19. 清除全局域名过滤 **功能说明**:清除全局域名过滤设置 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter/global ``` **请求参数**:无 **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "containerID": "abc123", "domains": ["example.com", "google.com"] }' "http://192.168.99.108:8000/mytVpc/domainFilter" ``` **成功返回**: ``` { "code": 0, "message": "清除成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "清除失败", "data": null } ``` --- #### 20. 查询全局域名过滤 **功能说明**:查询全局域名过滤列表 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter/global ``` **请求参数**:无 **请求示例**: ``` curl -X GET "http://192.168.99.108:8000/mytVpc/domainFilter/global" ``` **成功返回**: ``` { "code": 0, "message": "查询成功", "data": { "domains": ["example.com", "*.blocked.com"] } } ``` **失败返回**: ``` { "code": 500, "message": "查询失败", "data": null } ``` --- #### 21. 设置全局域名过滤 **功能说明**:设置全局域名过滤列表 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/domainFilter/global ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | domains | 是 | array | 域名列表,支持 domain:/full:/keyword:/regexp: 前缀,传空数组则清空 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "domains": ["domain:example.com", "keyword:blocked"] }' "http://192.168.99.108:8000/mytVpc/domainFilter/global" ``` **成功返回**: ``` { "code": 0, "message": "设置成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "设置失败", "data": null } ``` --- #### 22. 更新网络分组别名 **功能说明**:更新网络分组别名 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/mytVpc/group/alias ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | id | 是 | integer | 网络分组ID | | newAlias | 是 | string | 分组新别名 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{"id":123, "newAlias": "newgroup"}' "http://192.168.99.108:8000/mytVpc/group/alias" ``` **成功返回**: ``` { "code": 0, "message": "更新成功", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "更新失败,分组不存在", "data": null } ``` --- ### 八、本地机型数据管理 #### 1. 获取本地机型列表 **功能说明**:获取设备上的本地机型数据列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/phoneModel ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/phoneModel" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 5, "list": [ { "name": "samsung_s23" }, { "name": "xiaomi_14" }, { "name": "pixel_8" } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | name | string | 机型文件名称 | **失败返回**: ``` { "code": 500, "message": "获取本地机型列表失败", "data": null } ``` --- #### 2. 删除本地机型数据 **功能说明**:删除指定的本地机型数据 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/phoneModel ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 机型文件名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/phoneModel" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除本地机型失败: 文件不存在", "data": null } ``` --- #### 3. 导出本地机型数据 **功能说明**:导出指定的本地机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/phoneModel/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 机型文件名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/phoneModel/export" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导出本地机型失败: 文件不存在", "data": null } ``` --- #### 4. 导入机型数据 **功能说明**:通过上传ZIP包导入机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/phoneModel/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入修改后的机型ZIP包 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/phoneModel/import" \ -F "file=@samsung_s23.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入机型数据失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持ZIP格式的机型数据包 --- ### 九、接口认证 #### 1. 修改认证密码 **功能说明**:修改API接口认证密码(默认用户名admin) **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/auth/password ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | newPassword | 是 | string | 新密码 | | confirmPassword | 是 | string | 确认新密码 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/auth/password" \ -H "Content-Type: application/json" \ -d '{ "newPassword": "newpassword123", "confirmPassword": "newpassword123" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "修改密码失败: 两次密码不一致", "data": null } ``` **注意事项**: - 两次输入的密码必须一致 - 默认用户名为admin - 密码修改后立即生效 --- #### 2. 关闭接口认证 **功能说明**:关闭API接口认证功能 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/auth/close ``` **请求参数**:无 **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/auth/close" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "关闭认证失败", "data": null } ``` **注意事项**: - 关闭认证后,所有接口将无需认证即可访问 - 出于安全考虑,建议在内网环境下使用 --- ### 十、服务管理 #### 1. 更新服务 **功能说明**:在线更新SDK服务到最新版本 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/server/upgrade ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/server/upgrade" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "msg": "更新成功,服务将自动重启" } } ``` **失败返回**: ``` { "code": 500, "message": "更新失败: 网络连接超时", "data": null } ``` **注意事项**: - 更新成功后服务会自动重启 - 更新过程中请勿断电或断网 --- #### 2. 通过上传SDK更新服务 **功能说明**:通过上传SDK压缩包更新服务 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/server/upgrade/upload ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | SDK压缩包文件,zip格式 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/server/upgrade/upload" \ -F "file=@myt-sdk-v1.2.0.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "更新失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持zip格式的SDK压缩包 - 更新成功后服务会自动重启 --- #### 3. 清空设备磁盘数据 **功能说明**:清空设备磁盘上的所有数据(高危操作!) **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/server/device/reset ``` **请求参数**:无 **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/server/device/reset" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "清空数据失败", "data": null } ``` **注意事项**: - ⚠️ **高危操作!此操作将清空设备上的所有数据,不可恢复!** - 执行前请确保已备份所有重要数据 - 操作完成后设备将恢复出厂状态 --- #### 4. 重启设备 **功能说明**:重启设备 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/server/device/reboot ``` **请求参数**:无 **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/server/device/reboot" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "message": "设备将在5秒后重启" } } ``` **失败返回**: ``` { "code": 500, "message": "重启设备失败", "data": null } ``` **注意事项**: - 重启过程中所有云机将停止运行 - 重启完成后需要手动启动云机 --- #### 5. 开启和屏蔽dockerApi 2375端口 **功能说明**:控制主机上 Docker Remote API(端口 2375)的开启或屏蔽状态 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/server/dockerApi ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | enable | 是 | boolean | true 表示开启,false 表示屏蔽 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/server/dockerApi" \ -H "Content-Type: application/json" \ -d '{"enable": true}' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "操作失败", "data": null } ``` --- #### 6. 获取主机网络信息 **功能说明**:获取设备当前的网络配置信息,包括网关、子网和网卡接口 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/server/network ``` **请求参数**:无 **请求示例**: ``` curl http://192.168.99.108:8000/server/network ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "info": { "gateway": "192.168.99.1", "networkCIDR": "192.168.99.0/24", "interface": "eth0" } } } ``` **失败返回**: ``` { "code": 500, "message": "获取网络信息失败", "data": null } ``` --- ### 十一、大模型管理 #### 1. 导入大模型 ZIP 包 **功能说明**:导入大模型 ZIP 包到设备中 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/lm/import ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 大模型 ZIP 包文件(multipart/form-data 格式) | **请求示例**: ``` curl -X POST -F "file=@./llm_model.zip" "http://192.168.99.108:8000/lm/import" ``` **成功返回**: ``` { "code": 0, "message": "导入大模型ZIP包成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "导入大模型ZIP包失败,文件格式错误或损坏", "data": null } ``` --- #### 2. 获取系统信息 **功能说明**:获取设备及大模型相关系统信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/lm/info ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/lm/info" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "api_version": "v1.0.0", "nsmi_version": "1.2.3", "pcie_driver_version": "4.5.6", "firmware_version": "v2.1.0", "devices": [ { "device_id": 1, "mode": "PCIE", "global_id": 1001, "internal_id": 101, "fan_speed_ratio": 50, "temp": 42, "power": 35, "voltage": 12, "chips": [ { "chip_id": 10001, "chip_name": "RK3588", "chip_ver": "v1.1", "cpu_id": "CPU001", "health": 0, "temp": 40, "memory_info": { "total_size": 8589934592, "free_size": 4294967296, "util_rate": 50, "hugepage_size": 2097152, "hugepage_total_cnt": 1024, "hugepage_free_cnt": 512 } } ] } ] } } ``` **返回字段说明**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | api_version | string | API 版本 | | firmware_version | string | 固件版本 | | devices | array | 设备列表 | | device_id | integer | 设备全局 ID | | chips | array | 芯片列表 | | memory_info | object | 内存信息 | | total_size | integer | 总内存大小(Bytes) | | util_rate | integer | 内存使用率(%) | **失败返回**: ``` { "code": 500, "message": "获取系统信息失败", "data": null } ``` --- #### 3. 删除本地大模型 **功能说明**:删除设备上指定名称的本地大模型 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/lm/local ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 本地大模型名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/lm/local?name=chatglm3" ``` **成功返回**: ``` { "code": 0, "message": "删除本地大模型成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "删除本地大模型失败,模型不存在", "data": null } ``` --- #### 4. 获取本地大模型列表 **功能说明**:获取设备上所有本地大模型的列表信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/lm/local ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/lm/local" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 2, "list": [ { "name": "chatglm3", "size": "10GB", "files": [ { "filePath": "/models/chatglm3/main.rknn", "size": "8GB" }, { "filePath": "/models/chatglm3/vocab.json", "size": "200MB" } ] }, { "name": "llama2", "size": "15GB", "files": [ { "filePath": "/models/llama2/model.rknn", "size": "12GB" }, { "filePath": "/models/llama2/tokenizer.gguf", "size": "300MB" } ] } ] } } ``` **返回字段说明**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | count | integer | 本地大模型总数 | | name | string | string | | size | array | 大模型总大小 | | files | array | 大模型包含文件列表 | | filePath | string | 文件路径 | **失败返回**: ``` { "code": 500, "message": "获取本地大模型列表失败", "data": null } ``` --- #### 5. 获取模型运行状态 **功能说明**:获取当前设备上大模型的运行状态信息 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/lm/models ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/lm/models" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "object": "model_list", "data": [ { "id": "model_001", "object": "model", "created": 1735689600, "owned_by": "local", "meta": { "ctx_size": 2048, "predict": 1024, "temp": 0.7 } } ] } } ``` **返回字段说明**: | 参数名 | 类型 | 说明 | | --- | --- | --- | | id | string | 模型 ID | | created | integer | 创建时间戳 | | meta | object | 模型元数据 | | ctx_size | integer | 上下文窗口大小 | **失败返回**: ``` { "code": 500, "message": "获取模型运行状态失败", "data": null } ``` --- #### 6. 重置设备 **功能说明**:对指定设备进行硬件或软件重置 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/lm/reset ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | type | 是 | string | 重置类型(hw - 硬件,sw - 软件) | | device_id | 是 | integer | 设备 ID | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{"type":"sw","device_id":1}' "http://192.168.99.108:8000/lm/reset" ``` **成功返回**: ``` { "code": 0, "message": "设备重置成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "设备重置失败,设备ID不存在", "data": null } ``` --- #### 7. 启动 LLM 服务 **功能说明**:启动指定配置的 LLM 服务 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/lm/server/start ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | host | 否 | string | 监听地址,默认值:0.0.0.0 | | port | 否 | integer | 监听端口,默认值:8081 | | timeout | 否 | integer | 超时时间(秒),默认值:30 | | models | 是 | object | 模型配置列表(key 为模型别名) | | alias | 是 | string | 模型别名(与 key 一致) | | model | 是 | string | 主模型文件路径(RKNN) | | weight | 是 | string | 主模型权重路径 | | ctx-size | 否 | integer | 上下文窗口大小,默认值:0 | | temp | 否 | number | 温度参数,默认值:0.8 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "host": "0.0.0.0", "port": 8081, "timeout": 30, "models": { "chatglm3": { "alias": "chatglm3", "model": "/models/chatglm3/main.rknn", "weight": "/models/chatglm3/weight.bin", "ctx-size": 2048, "temp": 0.7 } } }' "http://192.168.99.108:8000/lm/server/start" ``` **成功返回**: ``` { "code": 0, "message": "LLM服务启动成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "LLM服务启动失败,模型文件不存在", "data": null } ``` --- #### 8. 停止 LLM 服务 **功能说明**:停止当前运行的 LLM 服务 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/lm/server/stop ``` **请求参数**:无 **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/lm/server/stop" ``` **成功返回**: ``` { "code": 0, "message": "LLM服务停止成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "LLM服务停止失败,服务未运行", "data": null } ``` --- #### 9. 设置工作模式 **功能说明**:设置指定设备和芯片的工作模式 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/lm/workMode ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | device_id | 是 | integer | 设备 ID | | chip_id | 是 | integer | 芯片 ID | | work_mode | 是 | integer | 工作模式,默认值:2 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{"device_id":1,"chip_id":10001,"work_mode":2}' "http://192.168.99.108:8000/lm/workMode" ``` **成功返回**: ``` { "code": 0, "message": "工作模式设置成功", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "工作模式设置失败,设备或芯片ID不存在", "data": null } ``` --- #### 10. 模型推理对话补全 **功能说明**:模型推理对话补全 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/v1/chat/completions ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | model | 是 | string | 模型别名 | | messages | 是 | array | 对话历史,每个元素包含 role 和 content | | stream | 否 | boolean | 是否流式返回,默认 true | | max_tokens | 否 | integer | 最大生成 Token 数,默认 2048 | | temperature | 否 | number | 随机性 (0-1),默认 0.7 | | top_p | 否 | number | 核采样概率,默认 0.9 | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "model": "llama3", "messages": [ {"role": "user", "content": "Hello, who are you?"} ], "stream": true, "max_tokens": 2048, "temperature": 0.7, "top_p": 0.9 }' "http://192.168.99.108:8000/v1/chat/completions" ``` **成功返回**: ``` { "code": 0, "message": "操作成功", "data": { "id": "chatcmpl-123", "object": "chat.completion", "created": 1677652288, "model": "llama3", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "I am an AI assistant." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 10, "completion_tokens": 20, "total_tokens": 30 } } } ``` **失败返回**: ``` { "code": 500, "message": "模型不存在或服务异常", "data": null } ``` --- #### 11. 模型文本向量化 **功能说明**:模型文本向量化 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/v1/embeddings ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | model | 是 | string | 模型别名 | | input | 是 | string/array | 输入文本,可以是字符串或字符串数组 | | dimensions | 否 | integer | 返回的向量维度(仅V3模型支持) | | encoding_format | 否 | string | 返回向量的格式,float 或 base64,默认 float | **请求示例**: ``` curl -X POST -H "Content-Type: application/json" -d '{ "model": "text-embedding-3-small", "input": "The quick brown fox jumps over the lazy dog", "dimensions": 256, "encoding_format": "float" }' "http://192.168.99.108:8000/v1/embeddings" ``` **成功返回**: ``` { "code": 0, "message": "操作成功", "data": { "object": "list", "data": [ { "object": "embedding", "index": 0, "embedding": [0.1, 0.2, 0.3, 0.4] } ], "model": "text-embedding-3-small", "usage": { "prompt_tokens": 10, "total_tokens": 10 } } } ``` **失败返回**: ``` { "code": 500, "message": "向量化失败,模型不支持或输入格式错误", "data": null } ``` --- ### 十二、云机操作V2镜像 #### 1. 创建云机V2 **功能说明**:创建一个新的安卓云机实例。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2 ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | string | 是 | 云机名称 | | indexNum | integer | 否 | 实例序号,P1范围1-24,Q1范围1-12,传0将自动分配一个空闲实例序号 | | imageUrl | string | 是 | 镜像完整地址 | | sandboxSize | string | 否 | 沙盒大小,例如 "16GB","32GB" | | dns | string | 是 | 云机DNS,例如 "223.5.5.5" | | offset | string | 否 | 云机的开机时间 | | doboxFps | string | 否 | 云机FPS | | doboxWidth | string | 否 | 云机分辨率的宽 | | doboxHeight | string | 否 | 云机分辨率的高 | | doboxDpi | string | 否 | 云机DPI | | enforce | boolean | 否 | 安全模式,默认开启 | | macVlanIp | string | 否 | 独立IP设置 | | start | boolean | 否 | 创建完成是否开机,默认为 true | | vpcID | integer | 否 | 添加的魔云腾VPC节点ID | | portMappings | array | 否 | 增加自定义端口映射,格式见下方示例 | | mytBridgeName | string | 否 | myt_bridge网卡名,可以接口查询或在设备终端使用ifconfig查询 | **请求示例**: ``` POST http://192.168.99.108:8000/androidv2 Content-Type: application/json 请求体 { "name": "test-cloud-phone-v2", "indexNum": 0, "imageUrl": "registry.example.com/android:12.0-base", "sandboxSize": "32GB", "dns": "8.8.8.8", "offset": "08:00", "doboxFps": "60", "doboxWidth": "1080", "doboxHeight": "1920", "doboxDpi": "480", "enforce": true, "macVlanIp": "192.168.100.100", "start": true, "vpcID": 0, "portMappings": [ { "containerPort": 5555, "hostPort": 55550, "hostIP": "0.0.0.0", "protocol": "tcp" } ], "mytBridgeName": "myt_bridge_default" } ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": { "id": "string" } } ``` **失败返回**: ``` { "code": 50, "message": "失败原因", "data": { "id": "" } } ``` --- #### 2. 重置云机V2 **功能说明**:重置指定名称的安卓云机实例,恢复到初始状态。 **请求方式**:PUT **请求URL**: ``` http://{主机IP}:8000/androidV2 ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | string | 是 | 云机名称 | **请求示例**: ``` curl http://192.168.99.108:8000/androidV2 \ --request PUT \ --header 'Content-Type: application/json' \ --data '{ "name": "test-cloud-phone-v2" }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "重置云机失败", "data": null } ``` --- #### 3. 批量切换容器镜像 **功能说明**:将多个指定名称的安卓云机批量切换到一个新的镜像。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/change-image ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerNames | array | 是 | 云机名称列表 | | image | string | 是 | 新的镜像地址 | **请求示例**: ``` POST http://192.168.99.108:8000/androidV2/change-image Content-Type: application/json 请求体 { "containerNames": [ "test-cloud-phone-v2", "test-cloud-phone-v2-2" ], "image": "registry.example.com/android:12.0-base" } 或者 curl http://192.168.99.108:8000/androidV2/change-image \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "containerNames": [ "test-cloud-phone-v2", "test-cloud-phone-v2-2" ], "image": "registry.example.com/android:12.0-base" }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 500, "message": "批量切换容器镜像失败", "data": null } ``` --- #### 4. 复制云机 **功能说明**:复制指定名称的安卓云机实例。 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/androidV2/copy ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | string | 是 | 云机名称 | | indexNum | integer | 否 | 坑位数 | | count | integer | 否 | 复制数量,默认为1 | **请求示例**: ``` GET "http://192.168.99.108:8000/androidV2/copy?name=test&indexNum=1&count=1" 或 curl 'http://192.168.99.108:8000/androidV2/copy?name=test&indexNum=1&count=1' ``` **成功返回**: ``` data: {"current":1,"total":1,"name":"test_copy_1","status":"copying","message":"正在复制 data 目录"} data: {"current":1,"total":1,"name":"test_copy_1","status":"copying","message":"正在创建容器"} data: {"current":1,"total":1,"name":"test_copy_1","status":"success","message":"27cccd9e0ef05c089c92f4831d191bee9cc20c00db8695f920968d6e2d927181"} data: {"current":1,"total":1,"name":"","status":"done","message":"","success":["test_copy_1"]} ``` **失败返回**: ``` { "code": 500, "message": "失败原因", "data": null } ``` --- #### 5. 切换安卓镜像 **功能说明**:切换指定名称的安卓云机实例的镜像。 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/switchImage ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageUrl | 是 | string | 镜像完整地址称 | | name | 是 | string | 云机名称像地址 | | adbPort | 否 | integer | 自定义adb端口,默认5555,设置端口0的时候不开启adb,请不要使用9082、9083、10000、10001、10006、10007、10008等端口 | | dns | 否 | string | 云机DNS,例如223.5.5.5 | | doboxDpi | 否 | string | 云机DPI | | doboxFps | 否 | string | 云机的帧率(FPS) | | doboxWidth | 否 | string | 云机分辨率的宽度 | | doboxHeight | 否 | string | 云机分辨率的高度 | | enforce | 否 | string | 是否启用安全模式,默认开启 | | macVlanIp | 否 | string | 独立 IP 设置,用于为云机分配一个固定的 MACVLAN IP 地址。 | | mytBridgeName | 否 | string | myt_bridge 网卡名称,可通过接口查询或在设备终端使用 ifconfig 查询。 | | offset | 否 | string | 云机的开机时间度 | | portMappings | 否 | string | 增加自定义端口映射 | | start | 否 | boolean | 创建完成开机,默认不开机 | | vpcID | 否 | integer | 添加的魔云腾VPC节点ID | **自定义端口映射**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | containerPort | 否 | integer | 容器内端口,如 80 | | hostIP | 否 | string | 主机IP,如 0.0.0.0 或 不填 | | hostPort | 否 | integer | 主机端口,如 8080 | | protocol | 否 | string | 协议,如 tcp、udp, 默认tcp | **请求示例**: ``` POST http://192.168.99.183:8000/androidV2/switchImage Content-Type: application/json 请求体 { "name": "1773039649840_3_T0003", "imageUrl": "registry.cn-guangzhou.aliyuncs.com/mytos/dobox:P10_base_202509221352" } # 或使用curl curl http://192.168.99.183:8000/androidV2/switchImage \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "name": "1773039649840_3_T0003", "imageUrl": "registry.cn-guangzhou.aliyuncs.com/mytos/dobox:P10_base_202509221352", "dns": "", "offset": "", "doboxFps": "", "doboxWidth": "", "doboxHeight": "", "doboxDpi": "", "enforce": true, "macVlanIp": "", "start": true, "vpcID": 0, "adbPort": 5555, "portMappings": [ { "containerPort": 0, "hostPort": 0, "hostIP": "", "protocol": "tcp" } ] }' ``` **成功返回**: ``` { "code": 0, "message": "OK", "data": null } ``` **失败返回**: ``` { "code": 10006, "message": "Error: 容器不存在无法操作,请确认容器名称是否正确", "data": null } ``` --- ### 十三、用户 #### 1. 登录获取魔云腾Token **功能说明**:登录魔云腾平台获取Token,用于同步授权 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/user/loginMyt ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 用户名 | | password | 是 | string | 密码 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/user/loginMyt" \ -H "Content-Type: application/json" \ -d '{ "name": "username", "password": "password123" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "mytToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | mytToken | string | 魔云腾访问Token | **失败返回**: ``` { "code": 500, "message": "登录失败: 用户名或密码错误", "data": null } ``` --- #### 2. 同步授权 **功能说明**:同步魔云腾平台授权信息到本地 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/user/syncAuthorization ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | mytToken | 是 | string | 魔云腾Token | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/user/syncAuthorization" \ -H "Content-Type: application/json" \ -d '{ "mytToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "同步授权失败: Token无效", "data": null } ``` --- ### 附录 #### 错误码说明 | 错误码 | 说明 | | --- | --- | | 0 | 成功 | | 400 | 请求参数错误 | | 401 | 未授权/认证失败 | | 404 | 资源不存在 | | 500 | 服务器内部错误 | #### 常见问题 --- ## 盒子-SDK-云机备份与镜像接口 *分类: 设备API* ## 盒子SDK 云机备份与镜像接口 - AI快速参考 基础地址:`http://{主机IP}:8000` 协议:HTTP 通用响应格式: ```json { "code": 0, "message": "ok", "data": { ... } } ``` | 字段 | 类型 | 说明 | | --- | --- | --- | | code | int | 状态码,0表示成功,非0表示失败 | | message | string | 状态信息 | | data | object | 返回数据 | --- ### 接口目录 #### V1 镜像管理 - [1. 导出安卓镜像](#1-导出安卓镜像) - [2. 下载导出后的安卓镜像包](#2-下载导出后的安卓镜像包) - [3. 导入安卓镜像](#3-导入安卓镜像) #### V1 云机导入导出 - [4. 导出安卓云机](#4-导出安卓云机) - [5. 导出安卓云机到对象存储](#5-导出安卓云机到对象存储) - [6. 导入安卓云机](#6-导入安卓云机) - [7. 通过URL导入安卓云机](#7-通过url导入安卓云机) #### V2 云机导入导出 - [8. 导出安卓云机V2](#8-导出安卓云机v2) - [9. 导出安卓云机V2到对象存储](#9-导出安卓云机v2到对象存储) - [10. 导入安卓云机V2](#10-导入安卓云机v2) - [11. 通过URL导入安卓云机V2](#11-通过url导入安卓云机v2) #### 备份压缩文件管理 - [12. 获取备份压缩文件列表](#12-获取备份压缩文件列表) - [13. 下载备份压缩文件](#13-下载备份压缩文件) - [14. 删除备份压缩文件](#14-删除备份压缩文件) #### 机型数据备份 - [15. 获取机型备份列表](#15-获取机型备份列表) - [16. 删除机型备份](#16-删除机型备份) - [17. 备份机型数据](#17-备份机型数据) - [18. 导出机型备份数据](#18-导出机型备份数据) - [19. 导入备份机型数据](#19-导入备份机型数据) - [20. 导出本地机型数据](#20-导出本地机型数据) - [21. 导入本地机型数据](#21-导入本地机型数据) --- #### 1. 导出安卓镜像 **功能说明**:将本地镜像导出为压缩包文件 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/image/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | imageUrl | 是 | string | 要导出的镜像完整地址 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/image/export" \ -H "Content-Type: application/json" \ -d '{ "imageUrl": "registry.example.com/android:v12" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "filename": "android_v12_20240115.tar" } } ``` **失败返回**: ``` { "code": 500, "message": "导出镜像失败: 磁盘空间不足", "data": null } ``` **注意事项**: - 导出过程可能需要较长时间 - 确保设备有足够的磁盘空间 --- #### 2. 下载导出后的安卓镜像包 **功能说明**:下载已导出的镜像压缩包文件 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/image/download ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | filename | 是 | string | 要下载的镜像包名 | **请求示例**: ``` curl "http://192.168.99.108:8000/android/image/download?filename=android_v12.tar" -o android_v12.tar ``` **成功返回**: - 返回文件的二进制数据,浏览器会自动下载 **失败返回**: ``` { "code": 500, "message": "下载失败: 文件不存在", "data": null } ``` --- #### 3. 导入安卓镜像 **功能说明**:通过上传tar文件导入安卓镜像 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/image/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入镜像包文件,tar格式 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/image/import" \ -F "file=@android_v12.tar" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入镜像失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持tar格式的镜像包 - 导入过程可能需要较长时间 --- #### 4. 导出安卓云机 **功能说明**:将安卓云机导出为压缩包 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/export" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "exportName": "android-01_20240115.zip" } } ``` **失败返回**: ``` { "code": 500, "message": "导出云机失败: 云机正在运行", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 导出文件包含云机的完整数据 --- #### 5. 导出安卓云机到对象存储 **功能说明**:将安卓云机导出并上传到对象存储(OSS/MinIO等) **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/exportToOss ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 要导出的云机名称 | | ossUrl | 是 | string | 对象存储地址,例如 http://192.168.1.100:8080 | | bucket | 是 | string | 存储桶名称 | | filePath | 否 | string | 文件存储路径,例如 backups/container.tar.gz,不传则自动生成 | | accessKey | 否 | string | 访问密钥(private Bucket时需要) | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/exportToOss" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "ossUrl": "http://192.168.1.100:8080", "bucket": "my-backups", "filePath": "backups/android-01.tar.gz" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "url": "http://192.168.1.100:8080/my-backups/backups/android-01.tar.gz" } } ``` **失败返回**: ``` { "code": 500, "message": "导出到对象存储失败: 连接OSS失败", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 确保对象存储服务可访问且桶存在 --- #### 6. 导入安卓云机 **功能说明**:通过上传文件导入安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入使用本SDK导出的安卓云机 | | indexNum | 是 | int | 实例序号,P1范围1-24,Q1范围1-12 | | name | 否 | string | 导入后云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/import" \ -F "file=@android-01_20240115.zip" \ -F "indexNum=2" \ -F "name=android-02" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-02" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机失败: 实例位已被占用", "data": null } ``` **注意事项**: - 仅支持本SDK导出的云机文件 - indexNum不能与已有云机冲突 --- #### 7. 通过URL导入安卓云机 **功能说明**:通过URL地址远程导入安卓云机数据包 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/importByUrl ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | url | 是 | string | 数据包直链地址,支持http/https | | indexNum | 否 | int | 实例序号,P1范围1-24,Q1范围1-12 | | name | 否 | string | 导入后云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/importByUrl" \ -H "Content-Type: application/json" \ -d '{ "url": "http://192.168.1.100:8080/my-backups/android-01.tar.gz", "indexNum": 3, "name": "android-03" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-03" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机失败: URL不可访问", "data": null } ``` **注意事项**: - 确保URL地址可访问 - 数据包必须为本SDK导出的格式 --- #### 8. 导出安卓云机V2 **功能说明**:将V2安卓云机导出为压缩包 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/androidV2/export" \ -H "Content-Type: application/json" \ -d '{ "name": "android-v2-01" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "exportName": "android-v2-01_20240115.zip" } } ``` **失败返回**: ``` { "code": 500, "message": "导出云机V2失败: 云机正在运行", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 导出文件包含V2云机的完整数据 --- #### 9. 导出安卓云机V2到对象存储 **功能说明**:将V2安卓云机导出并上传到对象存储(OSS/MinIO等) **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/exportToOss ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 云机名称 | | ossUrl | 是 | string | 对象存储地址 | | bucket | 是 | string | 存储桶名称 | | filePath | 否 | string | 文件存储路径,不传则自动生成 | | accessKey | 否 | string | 访问密钥(private Bucket时需要) | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/androidV2/exportToOss" \ -H "Content-Type: application/json" \ -d '{ "name": "android-v2-01", "ossUrl": "http://192.168.1.100:8080", "bucket": "my-backups", "filePath": "backups/android-v2-01.tar.gz" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "url": "http://192.168.1.100:8080/my-backups/backups/android-v2-01.tar.gz" } } ``` **失败返回**: ``` { "code": 500, "message": "导出到对象存储失败: 连接OSS失败", "data": null } ``` **注意事项**: - 建议导出前先停止云机 - 确保对象存储服务可访问且桶存在 --- #### 10. 导入安卓云机V2 **功能说明**:通过上传文件导入V2安卓云机 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入V2导出的云机文件 | | indexNum | 否 | int | 实例序号,0=自动分配,P1范围1-24,Q1范围1-12 | | name | 否 | string | 导入后云机名称 | | start | 否 | bool | 导入后是否开机,默认开机 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/androidV2/import" \ -F "file=@android-v2-01_20240115.zip" \ -F "indexNum=2" \ -F "name=android-v2-02" \ -F "start=true" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-v2-02" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机V2失败: 实例位已被占用", "data": null } ``` **注意事项**: - 仅支持V2导出的云机文件 - indexNum为0时自动分配实例位 - start参数控制导入后是否自动开机 --- #### 11. 通过URL导入安卓云机V2 **功能说明**:通过URL地址远程导入V2安卓云机数据包 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/androidV2/importByUrl ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | url | 是 | string | 数据包直链地址 | | indexNum | 否 | int | 实例序号,0=自动分配 | | name | 否 | string | 导入后云机名称 | | start | 否 | bool | 导入后是否开机,默认开机 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/androidV2/importByUrl" \ -H "Content-Type: application/json" \ -d '{ "url": "http://192.168.1.100:8080/my-backups/android-v2-01.tar.gz", "indexNum": 0, "name": "android-v2-03", "start": true }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "name": "android-v2-03" } } ``` **失败返回**: ``` { "code": 500, "message": "导入云机V2失败: URL不可访问", "data": null } ``` **注意事项**: - 确保URL地址可访问 - 数据包必须为V2导出的格式 - indexNum为0时自动分配实例位 --- #### 12. 获取备份压缩文件列表 **功能说明**:获取设备上所有已导出的备份压缩文件列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/backup ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 否 | string | 按文件名筛选 | **请求示例**: ``` curl "http://192.168.99.108:8000/backup" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 3, "list": [ { "name": "android-01_20240115.zip", "size": "2.3GB", "mtimestamp": 1705286400 }, { "name": "android-v2-01_20240116.zip", "size": "1.8GB", "mtimestamp": 1705372800 } ] } } ``` **返回字段说明**: | 字段 | 类型 | 说明 | | --- | --- | --- | | name | string | 压缩文件名称 | | size | string | 压缩文件大小 | | mtimestamp | int64 | 时间戳 | **失败返回**: ``` { "code": 500, "message": "获取备份文件列表失败", "data": null } ``` --- #### 13. 下载备份压缩文件 **功能说明**:下载指定的备份压缩文件 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/backup/download ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份压缩文件名称 | **请求示例**: ``` curl "http://192.168.99.108:8000/backup/download?name=android-01_20240115.zip" -o android-01.zip ``` **成功返回**: - 返回文件的二进制数据,浏览器会自动下载 **失败返回**: ``` { "code": 500, "message": "下载失败: 文件不存在", "data": null } ``` --- #### 14. 删除备份压缩文件 **功能说明**:删除指定的备份压缩文件 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/backup ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份压缩文件名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/backup?name=android-01_20240115.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除备份文件失败: 文件不存在", "data": null } ``` --- #### 15. 获取机型备份列表 **功能说明**:获取已备份的机型数据列表 **请求方式**:GET **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**:无 **请求示例**: ``` curl "http://192.168.99.108:8000/android/backup/model" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": { "count": 3, "list": [ { "name": "samsung_s23_backup" }, { "name": "xiaomi_14_backup" } ] } } ``` **失败返回**: ``` { "code": 500, "message": "获取机型备份列表失败", "data": null } ``` --- #### 16. 删除机型备份 **功能说明**:删除指定的机型备份数据 **请求方式**:DELETE **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 机型备份文件名称 | **请求示例**: ``` curl -X DELETE "http://192.168.99.108:8000/android/backup/model" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23_backup" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "删除机型备份失败: 文件不存在", "data": null } ``` --- #### 17. 备份机型数据 **功能说明**:将V3镜像创建的云机里的机型数据完整备份 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/model ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 要备份机型数据的云机名称 | | suffix | 是 | string | 备份后机型数据的后缀名 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/model" \ -H "Content-Type: application/json" \ -d '{ "name": "android-01", "suffix": "backup_20240115" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "备份机型数据失败: 云机不存在", "data": null } ``` **注意事项**: - 仅支持V3镜像创建的云机 - 备份前建议停止云机 --- #### 18. 导出机型备份数据 **功能说明**:导出已备份的机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/modelExport ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 备份机型数据文件名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/modelExport" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23_backup" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导出机型备份失败: 文件不存在", "data": null } ``` --- #### 19. 导入备份机型数据 **功能说明**:通过上传ZIP包导入备份的机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/android/backup/modelImport ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入备份机型数据ZIP包 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/android/backup/modelImport" \ -F "file=@samsung_s23_backup.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入机型备份失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持ZIP格式的备份文件 --- #### 20. 导出本地机型数据 **功能说明**:导出本地机型数据为文件 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/phoneModel/export ``` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | name | 是 | string | 机型文件名称 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/phoneModel/export" \ -H "Content-Type: application/json" \ -d '{ "name": "samsung_s23" }' ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导出机型数据失败: 文件不存在", "data": null } ``` --- #### 21. 导入本地机型数据 **功能说明**:通过上传ZIP包导入本地机型数据 **请求方式**:POST **请求URL**: ``` http://{主机IP}:8000/phoneModel/import ``` **Content-Type**:`multipart/form-data` **请求参数**: | 参数名 | 必选 | 类型 | 说明 | | --- | --- | --- | --- | | file | 是 | file | 导入修改后的机型ZIP包 | **请求示例**: ``` curl -X POST "http://192.168.99.108:8000/phoneModel/import" \ -F "file=@samsung_s23_modified.zip" ``` **成功返回**: ``` { "code": 0, "message": "ok", "data": {} } ``` **失败返回**: ``` { "code": 500, "message": "导入机型数据失败: 文件格式错误", "data": null } ``` **注意事项**: - 仅支持ZIP格式的机型文件 --- ## 授权管理-查询设备授权状态 *分类: 授权管理API* ## 查询设备每个坑位的授权状态 > 本文档描述如何通过魔云腾云平台API查询一台或多台设备上每个坑位(安卓云机实例)的授权到期状态。 --- ### 接口信息 | 项目 | 值 | |------|------| | URL | `https://www.moyunteng.com/api/api.php` | | 方法 | `POST` | | Content-Type | `application/x-www-form-urlencoded` | | 认证方式 | 签名校验(无需登录token) | | 超时建议 | 15秒 | --- ### 请求 Headers 所有请求必须携带以下 Headers: ``` Content-Type: application/x-www-form-urlencoded Referer: https://www.moyunteng.com/ Origin: https://www.moyunteng.com User-Agent: Mozilla/5.0 Accept: application/json, text/javascript, */*; q=0.01 ``` > 这些 Headers 模拟浏览器请求,服务端会校验 Referer 和 Origin。缺少可能导致请求被拒绝。 --- ### 请求体 请求体为 `application/x-www-form-urlencoded` 格式,包含两个表单字段: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `type` | string | 是 | 固定值 `term_info_nologn`,表示"查询终端信息(无需登录)" | | `data` | string | 是 | JSON字符串,包含查询参数和签名。见下方 data 字段构造 | #### 请求体编码示例 ``` type=term_info_nologn&data={"host":"[\"MYT-001\"]","_ts":1716100000,"_sign":"a1b2c3d4..."} ``` --- ### data 字段构造 `data` 是一个 JSON 字符串,解析后的结构如下: ```json { "host": "[\"设备ID\"]", "_ts": 1716100000, "_sign": "a1b2c3d4e5f6789012345678abcdef01" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `host` | string | 是 | 设备ID的JSON数组字符串。即使只查一台设备,也必须是数组格式。如 `["MYT-001"]`。查询多台设备时为 `["MYT-001","MYT-002"]` | | `_ts` | int64 | 是 | 当前Unix**秒级**时间戳。注意:本接口使用秒级,登录接口使用毫秒级 | | `_sign` | string | 是 | 请求签名,32位hex小写MD5值。见签名算法 | --- ### 签名算法 本接口通过 MD5 签名校验请求合法性,无需登录 token。 #### 步骤 ``` 1. 获取当前秒级时间戳 ts 2. 将设备ID构造成JSON数组字符串 hostJSON(如 ["MYT-001"]) 3. 拼接签名原文:ts + "#" + hostJSON + 盐值 4. 对签名原文计算 MD5,得到32位hex小写字符串 ``` #### 盐值 ``` #@#1234A98413G=--..234 ``` #### 签名原文拼接规则 ``` 签名原文 = {ts}#{hostJSON}{salt} ``` **关键细节**: - `ts` 和 `hostJSON` 之间用 `#` 分隔 - `hostJSON` 和 `salt` 之间**没有分隔符**,盐值直接追加在 hostJSON 后面 #### 单设备签名示例 设备ID为 `MYT-001`,时间戳为 `1716100000`: ``` hostJSON = ["MYT-001"] 签名原文 = "1716100000#[\"MYT-001\"]#@#1234A98413G=--..234" sign = MD5("1716100005#[\"MYT-001\"]#@#1234A98413G=--..234") = "计算出的32位hex字符串" ``` #### 多设备签名示例 查询 `MYT-001` 和 `MYT-002` 两台设备: ``` hostJSON = ["MYT-001","MYT-002"] 签名原文 = "1716100000#[\"MYT-001\",\"MYT-002\"]#@#1234A98413G=--..234" sign = MD5(签名原文) ``` > `hostJSON` 的值必须与请求体 `data` 中的 `host` 字段完全一致。 --- ### 代码示例 #### Python ```python import json import hashlib import time import requests # ============ 参数配置 ============ device_ids = ["MYT-001"] # 要查询的设备ID列表 api_url = "https://www.moyunteng.com/api/api.php" salt = "#@#1234A98413G=--..234" # ============ 构造签名 ============ ts = int(time.time()) # 秒级时间戳 host_json = json.dumps(device_ids) # '["MYT-001"]' sign_str = f"{ts}#{host_json}{salt}" # 拼接签名原文 sign = hashlib.md5(sign_str.encode()).hexdigest() # MD5签名 # ============ 构造请求体 ============ data = json.dumps({ "host": host_json, # JSON数组字符串 "_ts": ts, # 秒级时间戳 "_sign": sign # MD5签名 }) params = { "type": "term_info_nologn", "data": data } headers = { "Referer": "https://www.moyunteng.com/", "Origin": "https://www.moyunteng.com", "User-Agent": "Mozilla/5.0", "Accept": "application/json, text/javascript, */*; q=0.01", } # ============ 发送请求 ============ resp = requests.post(api_url, data=params, headers=headers, timeout=15) result = resp.json() # ============ 解析结果 ============ if str(result.get("code")) in ("200", "0"): for device in result.get("data", []): device_id = device["rabbet"] slot_count = device["snum"] print(f"设备 {device_id},共 {slot_count} 个坑位:") for slot_num, expire_ts in device["child"].items(): state = compute_expire_state(expire_ts) state_label = {0: "正常", 1: "即将到期", 2: "已过期"}.get(state, "未知") print(f" 坑位 {slot_num}: 到期时间戳={expire_ts}, 状态={state_label}") else: print(f"查询失败: code={result.get('code')}") def compute_expire_state(expire_time: str) -> int: """计算坑位到期状态""" if expire_time in ("0", ""): return 2 # 已过期/无授权 try: ts = int(expire_time) if ts == 0: return 2 now = int(time.time()) if ts < now: return 2 # 已过期 if ts - now < 3 * 24 * 3600: return 1 # 即将到期(3天内) return 0 # 正常 except ValueError: return 2 ``` #### Go ```go package main import ( "encoding/json" "fmt" "log" "net/http" "net/url" "strings" "time" ) const ( apiURL = "https://www.moyunteng.com/api/api.php" salt = "#@#1234A98413G=--..234" ) func QuerySlotInfo(deviceIDs []string) error { ts := time.Now().Unix() // 1. 构造 hostJSON hostJSON, _ := json.Marshal(deviceIDs) // 如 ["MYT-001"] // 2. 计算签名 signStr := fmt.Sprintf("%d#%s%s", ts, string(hostJSON), salt) sign := md5Hash(signStr) // 3. 构造 data reqData := map[string]any{ "host": string(hostJSON), "_ts": ts, "_sign": sign, } jsonData, _ := json.Marshal(reqData) // 4. 构造表单参数 params := url.Values{} params.Set("type", "term_info_nologn") params.Set("data", string(jsonData)) // 5. 发送请求 req, _ := http.NewRequest("POST", apiURL, strings.NewReader(params.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Referer", "https://www.moyunteng.com/") req.Header.Set("Origin", "https://www.moyunteng.com") req.Header.Set("User-Agent", "Mozilla/5.0") req.Header.Set("Accept", "application/json, text/javascript, */*; q=0.01") client := &http.Client{Timeout: 15 * time.Second} resp, err := client.Do(req) if err != nil { return fmt.Errorf("请求失败: %w", err) } defer resp.Body.Close() // 6. 解析响应... return nil } func md5Hash(s string) string { // 实现 MD5 哈希 h := md5.Sum([]byte(s)) return hex.EncodeToString(h[:]) } ``` #### curl ```bash #!/bin/bash # 查询设备坑位授权状态 DEVICE_ID="MYT-001" TS=$(date +%s) SALT="#@#1234A98413G=--..234" # 构造签名原文: ts#["设备ID"]盐值 SIGN_STR="${ts}#[\"${DEVICE_ID}\"]${SALT}" SIGN=$(echo -n "$SIGN_STR" | md5sum | awk '{print $1}') # 构造 data JSON DATA="{\"host\":\"[\\\"${DEVICE_ID}\\\"]\",\"_ts\":${TS},\"_sign\":\"${SIGN}\"}" curl -X POST 'https://www.moyunteng.com/api/api.php' \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Referer: https://www.moyunteng.com/' \ -H 'Origin: https://www.moyunteng.com' \ -H 'User-Agent: Mozilla/5.0' \ -H 'Accept: application/json, text/javascript, */*; q=0.01' \ --data-urlencode "type=term_info_nologn" \ --data-urlencode "data=${DATA}" ``` --- ### 响应结构 #### 成功响应 ```json { "code": "200", "data": [ { "rabbet": "MYT-001", "snum": "12", "child": { "1": "1720100000", "2": "0", "3": "1719500000", "4": "", "5": "1730000000", "6": "1725000000" } } ] } ``` #### 多设备成功响应 ```json { "code": "200", "data": [ { "rabbet": "MYT-001", "snum": "12", "child": { "1": "1720100000", "2": "0" } }, { "rabbet": "MYT-002", "snum": "24", "child": { "1": "1719500000", "2": "1725000000" } } ] } ``` #### 失败响应 ```json { "code": "400", "msg": "签名验证失败" } ``` #### 响应字段说明 | 字段 | 类型 | 说明 | |------|------|------| | `code` | string / int | 状态码。`"200"`、`200`、`"0"`、`0` 均表示成功,其他值表示失败 | | `data` | array | 设备列表,每台设备一个元素。查询失败时可能为空 | | `data[].rabbet` | string | 设备ID | | `data[].snum` | string | 坑位总数。P系列设备=24,其他设备=12 | | `data[].child` | object | 坑位授权信息。key=坑位序号(1-based字符串),value=到期时间戳(字符串) | | `data[].child.{n}` | string | 第n个坑位的到期时间。Unix秒级时间戳字符串。`"0"` 或 `""` 表示已过期/无授权 | --- ### 到期状态判断 API 仅返回到期时间戳,**不直接返回状态**。客户端需根据时间戳自行计算: ```python def compute_expire_state(expire_time: str) -> int: """ 计算坑位到期状态 返回值: 0 = 正常(剩余超过3天) 1 = 即将到期(剩余3天内) 2 = 已过期/无授权 """ if expire_time in ("0", ""): return 2 try: ts = int(expire_time) if ts == 0: return 2 now = int(time.time()) if ts < now: return 2 # 已过期 if ts - now < 3 * 24 * 3600: return 1 # 即将到期(3天内) return 0 # 正常 except ValueError: return 2 ``` | 条件 | state | 含义 | 建议展示 | |------|-------|------|----------| | 时间戳为 `"0"` 或 `""` | 2 | 无授权/已过期 | 红色标记 | | 时间戳 < 当前时间 | 2 | 已过期 | 红色标记 | | 剩余 < 3天(259200秒) | 1 | 即将到期 | 橙色/黄色警告 | | 其他 | 0 | 正常 | 绿色/默认 | --- ### 常见问题 #### Q: 时间戳精度是什么? 本接口的 `_ts` 使用**秒级**时间戳(`time.Now().Unix()`),与登录接口的毫秒级不同。签名计算时务必使用秒级。 #### Q: 成功的 code 值有哪些? 服务端返回的 `code` 可能是字符串或数字,成功值包括:`"200"`、`200`、`"0"`、`0`。判断时统一转为字符串比较即可。 #### Q: 可以同时查询多台设备吗? 可以。`host` 字段支持JSON数组格式传入多个设备ID:`["ID1","ID2","ID3"]`。响应的 `data` 数组会包含每台设备的结果。注意签名中的 `hostJSON` 必须与请求体 `data.host` 完全一致。 #### Q: 坑位序号从0还是1开始? 从1开始(1-based)。`child` 的 key 是字符串类型的坑位序号,`"1"` 表示第1个坑位,`"12"` 表示第12个。 #### Q: P系列设备的坑位数量不同? 是的。P系列设备(设备ID以 `P` 开头)有24个坑位,其他设备有12个坑位。响应中的 `snum` 字段会返回实际数量。 --- ### 错误处理 | 场景 | 判断方式 | 处理建议 | |------|----------|----------| | 签名错误 | code != 200/0 | 检查签名算法、盐值、时间戳精度 | | 设备ID不存在 | data 为空数组 | 确认设备ID是否正确 | | 网络超时 | HTTP 请求超时 | 重试,建议最多3次 | | 服务端异常 | code 为非预期值 | 记录日志,稍后重试 | --- ## 授权管理-续费设备坑位 *分类: 授权管理API* ## 续费设备坑位 > 本文档描述如何通过魔云腾云平台API为设备坑位续费。续费流程包括:登录获取token → 查询套餐 → 创建订单 → 扫码支付 → 确认支付状态。 --- ### 前置条件 续费操作需要登录 token,请先阅读[同步授权到设备](./同步授权到设备.md)文档中的"登录API"部分获取 token。 --- ### 整体流程 ``` ┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 1.登录获取 │───→│ 2.查询套餐列表 │───→│ 3.创建续费订单 │───→│ 4.扫码支付 │───→│ 5.确认支付状态 │ │ token │ │ get_package │ │ get_order_v2 │ │ 二维码 │ │ query_order │ └─────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ ``` --- ### 通用请求格式 所有续费相关接口共用以下请求格式: | 项目 | 值 | |------|------| | URL | `https://www.moyunteng.com/api/api.php` | | 方法 | `POST` | | Content-Type | `application/x-www-form-urlencoded` | | 超时建议 | 15秒 | #### 通用请求 Headers ``` Content-Type: application/x-www-form-urlencoded Referer: https://www.moyunteng.com/ Origin: https://www.moyunteng.com User-Agent: Mozilla/5.0 Accept: application/json, text/javascript, */*; q=0.01 ``` #### 通用请求体结构 请求体包含两个表单字段: | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `type` | string | 是 | 接口类型标识,不同接口值不同 | | `data` | string | 是 | JSON字符串,包含业务参数 | --- ### 接口一:获取套餐列表 获取可续费的套餐列表,用于后续创建订单时选择套餐。 #### 请求 | 参数 | 值 | |------|------| | `type` | `get_package` | #### data 字段 ```json { "token": "登录token" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `token` | string | 是 | 登录接口返回的 token | #### 响应 ```json { "code": "200", "data": [ { "id": "1", "name": "月度套餐", "price": "99", "day": "30" }, { "id": "2", "name": "季度套餐", "price": "259", "day": "90" }, { "id": "3", "name": "年度套餐", "price": "899", "day": "365" } ] } ``` | 字段 | 类型 | 说明 | |------|------|------| | `data[].id` | string | 套餐ID,创建订单时作为 `package` 参数传入 | | `data[].name` | string | 套餐名称 | | `data[].price` | string | 价格(单位:元),创建订单时作为 `money` 参数传入 | | `data[].day` | string | 有效天数 | --- ### 接口二:创建续费订单 选择套餐后,创建续费订单并获取支付二维码。 #### 请求 | 参数 | 值 | |------|------| | `type` | `get_order_v2` | #### data 字段 ```json { "rabbet": "{\"MYT-001\":[\"1\",\"2\",\"3\"]}", "package": "1", "money": "99", "paytype": "zfb", "token": "登录token" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `rabbet` | string | 是 | 续费目标。**双重JSON序列化**,见下方详细说明 | | `package` | string | 是 | 套餐ID,从获取套餐列表接口返回的 `id` 字段 | | `money` | string | 是 | 金额,与套餐 `price` 对应。多坑位时为单价 × 坑位数量 | | `paytype` | string | 是 | 支付方式。`"zfb"` = 支付宝 | | `token` | string | 是 | 登录 token | #### rabbet 字段详细说明 `rabbet` 是一个**JSON字符串**(注意:不是JSON对象),其内容解析后是一个JSON对象: ``` // rabbet 存储的是这个字符串: "{\"MYT-001\":[\"1\",\"2\",\"3\"]}" // 解析后的结构: { "MYT-001": ["1", "2", "3"] } ``` - **key**:设备ID(字符串) - **value**:坑位序号数组,每个元素是**字符串类型**的数字(1-based) ##### 构造方式 ```python # Python 示例 import json # 内层结构:设备ID → 坑位列表 inner = {"MYT-001": ["1", "2", "3"]} # rabbet 就是内层结构的JSON字符串 rabbet = json.dumps(inner) # 结果: '{"MYT-001": ["1", "2", "3"]}' ``` ##### 单台设备续费单个坑位 ```json { "rabbet": "{\"MYT-001\":[\"1\"]}", "package": "1", "money": "99", "paytype": "zfb", "token": "xxx" } ``` ##### 单台设备续费多个坑位 ```json { "rabbet": "{\"MYT-001\":[\"1\",\"2\",\"3\"]}", "package": "1", "money": "297", "paytype": "zfb", "token": "xxx" } ``` > money = 99元 × 3个坑位 = 297元 ##### 多台设备续费 ```json { "rabbet": "{\"MYT-001\":[\"1\",\"2\"],\"MYT-002\":[\"3\"]}", "package": "1", "money": "297", "paytype": "zfb", "token": "xxx" } ``` #### 响应 ##### 成功响应 ```json { "code": "200", "msg": "OK", "qrcode": "iVBORw0KGgoAAAANSUhEUgAA...(base64编码的二维码图片)", "order_id": "202405190001", "data": {} } ``` | 字段 | 类型 | 说明 | |------|------|------| | `code` | string / int | `"200"`、`200`、`"0"`、`0` 均表示成功 | | `msg` | string | 消息 | | `qrcode` | string | 支付二维码图片,base64编码。可直接嵌入HTML `` 展示 | | `order_id` | string | 订单ID,用于后续查询支付状态 | | `data` | object / array | 备用数据。可能包含 `order_id` 和 `qrcode`(兼容不同响应格式) | ##### 响应兼容处理 `qrcode` 和 `order_id` 可能出现在外层,也可能出现在 `data` 中: ```python def extract_order_info(resp: dict) -> tuple[str, str]: """提取订单ID和二维码,兼容不同响应格式""" order_id = resp.get("order_id", "") qrcode = resp.get("qrcode", "") # 外层没有则从 data 中提取 if (not order_id or not qrcode) and resp.get("data"): data = resp["data"] if isinstance(data, list) and len(data) > 0: # 数组格式:取第一个元素 order_id = data[0].get("order_id", order_id) qrcode = data[0].get("qrcode", qrcode) elif isinstance(data, dict): # 对象格式 order_id = data.get("order_id", order_id) qrcode = data.get("qrcode", qrcode) return order_id, qrcode ``` ##### 失败响应 ```json { "code": "400", "msg": "token过期" } ``` --- ### 接口三:查询订单支付状态 创建订单后,轮询此接口检查支付是否完成。 #### 请求 | 参数 | 值 | |------|------| | `type` | `query_order` | #### data 字段 ```json { "id": "202405190001", "token": "登录token" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `id` | string | 是 | 订单ID,创建订单时返回的 `order_id` | | `token` | string | 是 | 登录 token | #### 响应 ```json { "code": "200", "data": { "state": 0, "id": "202405190001" } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `data.state` | int | 订单状态。`0` = 未支付,`1` = 已支付 | | `data.id` | string | 订单ID | --- ### 接口四:查询设备绑定状态 创建订单前,需确认设备已绑定到当前账号。未绑定的设备无法续费。 #### 请求 | 参数 | 值 | |------|------| | `type` | `user_host_oper` | #### data 字段 ```json { "act": "get", "data": "{\"host\":[\"MYT-001\",\"MYT-002\"]}", "token": "登录token" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `act` | string | 是 | 操作类型。查询绑定状态传 `"get"`,绑定设备传 `"bind"` | | `data` | string | 是 | **双重JSON序列化**。查询时内层为 `{"host":["设备ID1","设备ID2"]}` | | `token` | string | 是 | 登录 token | #### 响应 `data` 字段可能返回数组或对象两种格式: ```json // 格式一:数组(按请求中设备ID顺序) { "code": "200", "data": [1, 0, 2] } // 格式二:对象(key是设备ID) { "code": "200", "data": { "MYT-001": 1, "MYT-002": 0, "MYT-003": 2 } } ``` | bind_status | 含义 | 可否续费 | |-------------|------|----------| | 0 | 未绑定 | 需先绑定 | | 1 | 已绑定(当前账号) | 可以续费 | | 2 | 被其他账号绑定 | 不可以续费 | #### 兼容处理 ```python def parse_bind_status(resp_data, device_ids: list[str]) -> dict[str, int]: """解析绑定状态,兼容数组和对象两种格式""" result = {} if isinstance(resp_data, list): # 数组格式:按设备ID顺序对应 for i, status in enumerate(resp_data): if i < len(device_ids): result[device_ids[i]] = int(status) elif isinstance(resp_data, dict): # 对象格式:key是设备ID for device_id, status in resp_data.items(): result[device_id] = int(status) return result ``` --- ### 接口五:绑定设备 设备未绑定时,需先绑定到当前账号。 #### 请求 | 参数 | 值 | |------|------| | `type` | `user_host_oper` | #### data 字段 ```json { "act": "bind", "data": "{\"host\":\"MYT-001\"}", "token": "登录token" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `act` | string | 是 | `"bind"` | | `data` | string | 是 | **双重JSON序列化**。绑定时 `host` 是**单个设备ID字符串**(非数组),与查询的数组格式不同 | | `token` | string | 是 | 登录 token | > **重要区别**:查询绑定状态时 `host` 是数组 `["MYT-001"]`,绑定时 `host` 是字符串 `"MYT-001"`。 #### 响应 ```json { "code": "200", "msg": "绑定成功" } ``` --- ### 完整续费流程代码示例 #### Python ```python import json import hashlib import time import requests API_URL = "https://www.moyunteng.com/api/api.php" LOGIN_URL = "https://moyunteng.com/api/sp_api.php" # ============ 第一步:登录获取 token ============ def login(username: str, password: str) -> str: ts = int(time.time() * 1000) # 毫秒级时间戳 pwd_md5 = hashlib.md5(password.encode()).hexdigest() salt = "454&*&*fsdff" sign_str = f"{ts}#{pwd_md5}#{username}#{salt}" sign = hashlib.md5(sign_str.encode()).hexdigest() data = json.dumps({ "uname": username, "pwd": pwd_md5, "_ts": str(ts), "_sign": sign }) resp = requests.post(LOGIN_URL, data={"type": "login", "data": data}, timeout=15) result = resp.json() if result.get("code") != "200": raise Exception(f"登录失败: {result.get('msg', '未知错误')}") return result["data"]["token"] # ============ 第二步:获取套餐列表 ============ def get_packages(token: str) -> list: data = json.dumps({"token": token}) resp = requests.post(API_URL, data={"type": "get_package", "data": data}, timeout=15) result = resp.json() if str(result.get("code")) not in ("200", "0"): raise Exception(f"获取套餐失败: {result}") return result.get("data", []) # ============ 第三步:创建续费订单 ============ def create_order(token: str, device_id: str, slot_indices: list[str], package_id: str, money: str, pay_type: str = "zfb") -> dict: # 构造 rabbet(双重JSON序列化) inner = {device_id: slot_indices} rabbet = json.dumps(inner) data = json.dumps({ "rabbet": rabbet, "package": package_id, "money": money, "paytype": pay_type, "token": token }) resp = requests.post(API_URL, data={"type": "get_order_v2", "data": data}, timeout=15) result = resp.json() if str(result.get("code")) not in ("200", "0"): raise Exception(f"创建订单失败: {result.get('msg')}") return result # ============ 第四步:查询订单状态 ============ def query_order(token: str, order_id: str) -> int: data = json.dumps({"id": order_id, "token": token}) resp = requests.post(API_URL, data={"type": "query_order", "data": data}, timeout=15) result = resp.json() if str(result.get("code")) not in ("200", "0"): raise Exception(f"查询订单失败: {result}") return result["data"]["state"] # 0=未支付, 1=已支付 # ============ 第五步:等待支付完成 ============ def wait_for_payment(token: str, order_id: str, timeout: int = 300) -> bool: """轮询等待支付完成,最长等待timeout秒""" start = time.time() while time.time() - start < timeout: state = query_order(token, order_id) if state == 1: return True time.sleep(3) # 每3秒查询一次 return False # ============ 完整流程调用 ============ if __name__ == "__main__": # 1. 登录 token = login("用户名", "密码") print(f"登录成功,token: {token[:20]}...") # 2. 获取套餐 packages = get_packages(token) for pkg in packages: print(f"套餐: {pkg['name']}, 价格: {pkg['price']}元, 天数: {pkg['day']}天") # 3. 创建订单(续费 MYT-001 的第1、2号坑位,选择第一个套餐) result = create_order( token=token, device_id="MYT-001", slot_indices=["1", "2"], package_id=packages[0]["id"], money=str(int(packages[0]["price"]) * 2) # 2个坑位 ) order_id = result.get("order_id", "") qrcode = result.get("qrcode", "") print(f"订单ID: {order_id}") print(f"请扫码支付(二维码已生成)") # 4. 等待支付 if wait_for_payment(token, order_id): print("支付成功!") else: print("支付超时") ``` --- ### Token 过期处理 所有需要 token 的接口,token 可能过期。过期时响应特征: - `code` 为非成功值 - 响应体中包含关键词:`"token"`、`"过期"`、`"expired"`、`"unauthorized"`、`"未登录"` 处理方式: ```python def is_token_expired(resp_body: bytes) -> bool: """检测响应是否指示token过期""" lower = resp_body.decode("utf-8", errors="ignore").lower() indicators = ["token", "过期", "expired", "unauthorized", "未登录"] return any(ind in lower for ind in indicators) # 使用示例 if is_token_expired(resp.content): token = login(username, password) # 重新登录 # 重试原请求... ``` --- ### 注意事项 | 项目 | 说明 | |------|------| | rabbet 双重序列化 | `rabbet` 字段是JSON字符串(不是JSON对象)。先构造 `{"设备ID":["坑位1","坑位2"]}` 对象,再序列化为字符串 | | 坑位序号格式 | rabbet 中的坑位序号是**字符串类型**,如 `"1"` 而不是 `1` | | 绑定 vs 查询的 host 格式 | 查询绑定状态时 `host` 是数组 `["ID"]`,绑定时 `host` 是字符串 `"ID"` | | 登录API域名 | `moyunteng.com`(无www),其他API是 `www.moyunteng.com` | | 金额计算 | money = 单价 × 坑位数量,需自行计算 | | 支付方式 | 目前仅支持支付宝(`paytype = "zfb"`) | --- ## 授权管理-同步授权到设备 *分类: 授权管理API* ## 同步授权到设备 > 本文档描述如何将授权信息同步到魔云腾ARM算力服务器设备。同步分为两步:1)通过登录API获取 token;2)通过 UDP 协议将 token 推送到局域网内的每台设备。 --- ### 整体流程 ``` ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │ 1. 调用登录API │───→│ 2. 获取token │───→│ 3. UDP推送到每台设备 │ │ POST sp_api.php │ │ 从响应中提取 │ │ lgtoken:{token} │ │ type=login │ │ │ │ 端口7678 │ └─────────────────┘ └──────────────────┘ └──────────────────────┘ ``` --- ### 第一步:登录获取Token #### 接口信息 | 项目 | 值 | |------|------| | URL | `https://moyunteng.com/api/sp_api.php` | | 方法 | `POST` | | Content-Type | `application/x-www-form-urlencoded`(由 PostForm 自动设置) | | 认证方式 | 签名校验 | | 超时建议 | 15秒 | > **重要**:登录API的域名是 `moyunteng.com`(无 `www`),与其他API的 `www.moyunteng.com` 不同。使用错误域名会导致请求失败。 #### 请求体 (form-urlencoded) | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | `type` | string | 是 | 固定值 `login` | | `data` | string | 是 | JSON字符串,包含登录参数和签名 | #### data 字段构造 ```json { "uname": "15071042575", "pwd": "e10adc3949ba59abbe56e057f20f883e", "_ts": "1716100000000", "_sign": "a1b2c3d4e5f6789012345678abcdef01" } ``` | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| | `uname` | string | 是 | 用户名(手机号) | | `pwd` | string | 是 | 原始密码的MD5值(32位hex小写),**不是明文密码** | | `_ts` | string | 是 | 当前Unix**毫秒级**时间戳的字符串形式。注意:本接口使用毫秒级,坑位查询接口使用秒级 | | `_sign` | string | 是 | 请求签名,32位hex小写MD5值。见签名算法 | #### 签名算法 ``` 1. 计算密码MD5:pwdMD5 = MD5(原始密码) 2. 拼接签名原文:毫秒时间戳 + "#" + pwdMD5 + "#" + 用户名 + "#" + 盐值 3. 计算签名:sign = MD5(签名原文) ``` **盐值**:`454&*&*fsdff` #### 签名原文拼接规则 ``` 签名原文 = {毫秒时间戳}#{pwdMD5}#{用户名}#{盐值} ``` 各部分之间用 `#` 分隔,共3个 `#`。 #### 签名计算示例 用户名 `15071042575`,密码 `123456`,时间戳 `1716100000000`: ``` 1. pwdMD5 = MD5("123456") = "e10adc3949ba59abbe56e057f20f883e" 2. 签名原文 = "1716100000000#e10adc3949ba59abbe56e057f20f883e#15071042575#454&*&*fsdff" 3. sign = MD5("1716100000000#e10adc3949ba59abbe56e057f20f883e#15071042575#454&*&*fsdff") = "计算出的32位hex字符串" ``` #### 登录成功响应 ```json { "code": "200", "msg": "OK", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "uname": "15071042575", "uid": "12345" } } ``` | 字段 | 类型 | 说明 | |------|------|------| | `code` | string | `"200"` 表示成功 | | `msg` | string | 消息 | | `data.token` | string | 登录token,后续推送到设备。请保存此值 | | `data.uname` | string | 用户名 | | `data.uid` | string | 用户ID | #### 登录失败响应 ```json { "code": "3036", "msg": "账号不存在" } ``` #### 错误码 | code | 含义 | 处理建议 | |------|------|----------| | `"3036"` | 账号不存在 | 检查用户名是否正确 | | `"3037"` | 密码错误 | 检查密码及MD5计算是否正确 | | 其他 | 其他错误 | 查看 msg 字段获取详细信息 | --- ### 第二步:UDP推送Token到设备 获取 token 后,通过 UDP 协议将 token 推送到局域网内的每台设备。 #### 协议详情 | 项目 | 值 | |------|------| | 协议 | UDP 单播 | | 目标端口 | `7678`(设备固定监听端口) | | 发送内容 | `lgtoken:` + token字符串(纯文本UTF-8编码) | | 响应 | 无(fire-and-forget) | #### 消息格式 ``` lgtoken:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... ``` - 固定前缀:`lgtoken:`(注意冒号为英文半角) - 紧跟 token 字符串,无空格 - 整体以 UTF-8 编码发送 #### 发送目标 对每台在线设备的IP地址,发送UDP数据报到 `<设备IP>:7678`。 #### 设备接收行为 设备在 `7678` 端口监听UDP消息,收到以 `lgtoken:` 开头的消息后: 1. 提取 `lgtoken:` 后面的 token 字符串 2. 将 token 保存到本地 3. 后续使用该 token 与云平台通信 --- ### 代码示例 #### Python(完整流程) ```python import json import hashlib import time import socket import requests LOGIN_URL = "https://moyunteng.com/api/sp_api.php" LOGIN_SALT = "454&*&*fsdff" DEVICE_PORT = 7678 def login(username: str, password: str) -> str: """登录并返回token""" # 1. 计算密码MD5 pwd_md5 = hashlib.md5(password.encode()).hexdigest() # 2. 毫秒级时间戳 ts = int(time.time() * 1000) # 3. 计算签名 sign_str = f"{ts}#{pwd_md5}#{username}#{LOGIN_SALT}" sign = hashlib.md5(sign_str.encode()).hexdigest() # 4. 构造请求体 data = json.dumps({ "uname": username, "pwd": pwd_md5, "_ts": str(ts), "_sign": sign }) # 5. 发送请求 resp = requests.post(LOGIN_URL, data={"type": "login", "data": data}, timeout=15) result = resp.json() if result.get("code") != "200": raise Exception(f"登录失败: code={result.get('code')}, msg={result.get('msg')}") return result["data"]["token"] def sync_token_to_device(token: str, device_ip: str) -> bool: """将token通过UDP推送到单台设备""" msg = f"lgtoken:{token}".encode("utf-8") try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(3) # 3秒超时 sock.sendto(msg, (device_ip, DEVICE_PORT)) sock.close() return True except Exception as e: print(f"推送到 {device_ip} 失败: {e}") return False def sync_token_to_all_devices(token: str, device_ips: list[str]) -> dict[str, bool]: """将token推送到所有设备""" results = {} for ip in device_ips: results[ip] = sync_token_to_device(token, ip) return results # ============ 完整流程调用 ============ if __name__ == "__main__": # 1. 登录获取token token = login("15071042575", "密码") print(f"登录成功,token: {token[:30]}...") # 2. 推送到所有设备 device_ips = ["10.10.11.1", "10.10.11.2", "10.10.11.3"] results = sync_token_to_all_devices(token, device_ips) for ip, success in results.items(): status = "成功" if success else "失败" print(f" {ip}: {status}") ``` #### Python(并发推送,适合大量设备) ```python import socket from concurrent.futures import ThreadPoolExecutor, as_completed def sync_token_to_devices_concurrent(token: str, device_ips: list[str], max_workers: int = 50) -> dict[str, bool]: """并发推送token到所有设备""" msg = f"lgtoken:{token}".encode("utf-8") results = {} def send_one(ip: str) -> tuple[str, bool]: try: sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(3) sock.sendto(msg, (ip, DEVICE_PORT)) sock.close() return ip, True except Exception: return ip, False with ThreadPoolExecutor(max_workers=max_workers) as executor: futures = {executor.submit(send_one, ip): ip for ip in device_ips} for future in as_completed(futures): ip, success = future.result() results[ip] = success return results ``` #### Go ```go package main import ( "encoding/json" "fmt" "net" "net/http" "net/url" "strings" "sync" "time" ) const ( loginURL = "https://moyunteng.com/api/sp_api.php" loginSalt = "454&*&*fsdff" devicePort = 7678 ) // Login 登录获取token func Login(username, password string) (string, error) { ts := time.Now().UnixMilli() pwdMD5 := md5Hash(password) signStr := fmt.Sprintf("%d#%s#%s#%s", ts, pwdMD5, username, loginSalt) sign := md5Hash(signStr) data := map[string]string{ "uname": username, "pwd": pwdMD5, "_ts": fmt.Sprintf("%d", ts), "_sign": sign, } jsonData, _ := json.Marshal(data) params := url.Values{} params.Set("type", "login") params.Set("data", string(jsonData)) resp, err := http.PostForm(loginURL, params) if err != nil { return "", fmt.Errorf("登录请求失败: %w", err) } defer resp.Body.Close() // 解析响应获取token... return token, nil } // SyncTokenToDevices 将token推送到所有设备 func SyncTokenToDevices(token string, deviceIPs []string) error { conn, err := net.ListenPacket("udp4", "0.0.0.0:0") if err != nil { return fmt.Errorf("创建UDP连接失败: %w", err) } defer conn.Close() msg := []byte("lgtoken:" + token) var wg sync.WaitGroup for _, ip := range deviceIPs { wg.Add(1) go func(targetIP string) { defer wg.Done() addr, err := net.ResolveUDPAddr("udp4", fmt.Sprintf("%s:%d", targetIP, devicePort)) if err != nil { return } _, _ = conn.WriteTo(msg, addr) }(ip) } wg.Wait() return nil } ``` #### Node.js ```javascript const crypto = require('crypto'); const dgram = require('dgram'); const axios = require('axios'); const LOGIN_URL = 'https://moyunteng.com/api/sp_api.php'; const LOGIN_SALT = '454&*&*fsdff'; const DEVICE_PORT = 7678; // ============ 登录 ============ async function login(username, password) { const ts = Date.now(); // 毫秒级时间戳 const pwdMD5 = crypto.createHash('md5').update(password).digest('hex'); const signStr = `${ts}#${pwdMD5}#${username}#${LOGIN_SALT}`; const sign = crypto.createHash('md5').update(signStr).digest('hex'); const data = JSON.stringify({ uname: username, pwd: pwdMD5, _ts: String(ts), _sign: sign }); const params = new URLSearchParams(); params.append('type', 'login'); params.append('data', data); const resp = await axios.post(LOGIN_URL, params); const result = resp.data; if (result.code !== '200') { throw new Error(`登录失败: ${result.msg}`); } return result.data.token; } // ============ UDP推送 ============ function syncTokenToDevice(token, deviceIP) { return new Promise((resolve) => { const msg = Buffer.from(`lgtoken:${token}`); const client = dgram.createSocket('udp4'); client.send(msg, DEVICE_PORT, deviceIP, (err) => { client.close(); resolve(!err); }); // 3秒超时 setTimeout(() => { client.close(); resolve(false); }, 3000); }); } // ============ 完整流程 ============ async function main() { // 1. 登录 const token = await login('15071042575', '密码'); console.log(`登录成功,token: ${token.substring(0, 30)}...`); // 2. 推送到所有设备 const deviceIPs = ['10.10.11.1', '10.10.11.2', '10.10.11.3']; const results = await Promise.all( deviceIPs.map(async (ip) => { const success = await syncTokenToDevice(token, ip); return { ip, success }; }) ); for (const { ip, success } of results) { console.log(` ${ip}: ${success ? '成功' : '失败'}`); } } main(); ``` --- ### 自动同步机制 魔云腾PC客户端启动后会自动执行授权同步: 1. **启动时立即同步**:应用启动后第一时间登录并推送 2. **定时同步**:每 **60秒** 自动重复登录 + 推送 3. **失败重试**:登录失败时采用指数退避重试 #### 指数退避策略 ``` 第1次失败 → 1秒后重试 第2次失败 → 2秒后重试 第3次失败 → 4秒后重试 第4次失败 → 8秒后重试 ... 最大间隔 → 30秒 ``` ```python import time def do_sync_with_retry(login_fn, push_fn, stop_event, max_backoff=30): """带指数退避的同步重试""" backoff = 1 # 初始1秒 while not stop_event.is_set(): try: token = login_fn() push_fn(token) return # 成功,退出 except Exception: pass # 等待退避时间 stop_event.wait(backoff) if stop_event.is_set(): return print(f"同步失败,{backoff}秒后重试...") backoff = min(backoff * 2, max_backoff) ``` --- ### 网络要求 | 项目 | 要求 | |------|------| | 客户端与设备 | 必须在同一局域网内 | | UDP 端口 | 设备 `7678` 端口未被防火墙拦截 | | 出站 HTTPS | 需能访问 `moyunteng.com:443` | | DNS | 需能解析 `moyunteng.com` | --- ### 常见问题 #### Q: 登录API和坑位查询API的签名有什么区别? | 对比项 | 登录API | 坑位查询API | |--------|---------|-------------| | 域名 | `moyunteng.com` | `www.moyunteng.com` | | 时间戳精度 | **毫秒级** | **秒级** | | 签名原文格式 | `{ts}#{pwdMD5}#{username}#{salt}` | `{ts}#{hostJSON}{salt}` | | 盐值 | `454&*&*fsdff` | `#@#1234A98413G=--..234` | | 分隔符 | 各部分之间有 `#` | ts和hostJSON之间有 `#`,hostJSON和salt之间**无分隔符** | | 认证方式 | 签名 + 密码MD5 | 纯签名 | #### Q: UDP推送是否可靠? UDP 不保证送达,但局域网环境下丢包率极低(<0.1%)。如需确保送达: - 可多次发送同一消息(如发送3次) - 结合60秒定时同步,即使偶尔丢包也会在下一轮补上 #### Q: token 有效期多长? token 有效期由服务端控制,客户端不做缓存有效期判断。建议每次同步都重新登录获取新 token。 #### Q: 如何获取在线设备IP列表? 可通过魔云腾设备发现协议(UDP广播)获取局域网内的设备列表。设备会在 `7678` 端口响应广播,返回设备ID和IP地址。 #### Q: 多台设备可以并发推送吗? 可以。Go实现中使用 goroutine 并发推送,Python可用线程池并发,Node.js可用 Promise.all 并发。局域网环境下并发推送不会造成问题。 --- ### 注意事项 | 项目 | 说明 | |------|------| | 登录域名 | `moyunteng.com`(无www),不要与 `www.moyunteng.com` 混淆 | | 时间戳精度 | 登录API使用**毫秒级**时间戳,坑位查询API使用秒级 | | 密码传输 | `pwd` 字段是原始密码的MD5值,**绝不传输明文密码** | | UDP消息前缀 | 必须是 `lgtoken:`,冒号为英文半角 | | 设备端口 | 固定为 `7678`,不可修改 | | 局域网要求 | 客户端与设备必须在同一局域网内,UDP推送无法跨网段 | | 无响应确认 | UDP推送是 fire-and-forget,设备不会回复确认消息 |