# RoboHub 自动接入指南

版本：`robohub-v1`。这是新项目的默认接入入口；完整字段与高级边界仍在 `universal-project-integration.md`。

## 目标

项目后端只需提供自身的已认证用户标识，以及首次使用时的 RoboCloud 账户输入。RoboHub 自动完成：

```text
账户绑定 -> 套餐选择 -> 节点读取 -> 真实测速 -> 只保留通过节点
-> 多出口租约 -> 短期任务凭据 -> 心跳 -> 释放
```

项目不创建 Runner、不写 Mihomo 配置、不扫描端口、不保存代理密码。

## 1. 项目配置

```text
ROBOHUB_BASE_URL=http://<test-or-production-host>:18883-or-18884
ROBOHUB_PROJECT_ID=<管理员登记的项目 ID>
ROBOHUB_HMAC_KEY=<项目后端 HMAC 密钥>
ROBOHUB_EXPECTED_ENVIRONMENT=test | production
```

项目启动时调用 `GET /v1/health`，并确认：

```text
environment == ROBOHUB_EXPECTED_ENVIRONMENT
api_contract_version == "robohub-v1"
runtime_dependency_contract.status == "matched"
```

环境或契约不匹配时拒绝启动代理任务，禁止把本地、Test、正式地址混用。

所有 Test/正式请求都由项目后端签名，不能从浏览器直接调用：

```http
X-RoboHub-Project: <固定 project_id>
X-RoboHub-User: <项目后端从自身登录会话取得的用户 ID>
X-RoboHub-Timestamp: <Unix 秒>
X-RoboHub-Nonce: <每次请求唯一随机值>
X-RoboHub-Signature: <HMAC-SHA256 十六进制值>
```

签名原文严格为六行：

```text
METHOD
PATH_ONLY
SHA256_HEX_OF_EXACT_UTF8_BODY
UNIX_TIMESTAMP_SECONDS
NONCE
PROJECT_ID
```

`PATH_ONLY` 不含查询字符串；GET 的 body hash 是空字节串。每次 HTTP 重试都重新生成 timestamp、nonce 和签名。

## 2. 账户数据规则

- 首次绑定：项目后端只在内存里把 `account_email`、`account_password` 提交到 `POST /v1/bindings/login`。
- 成功后：只保存响应中的不透明 `account_ref`，并且只能存于项目自己的加密服务端配置。
- 后续任务、其他项目或同项目不同 CDK：只提交 `account_ref`，不得重复提交密码。
- 同一个 `account_ref` 可被多个项目并发使用。RoboHub 共享账户级 Session、Runner 与测速证据；每个项目用户仍拥有独立租约、端口、fencing token 和任务凭据。
- 账户密码、Session、代理用户名/密码和 fencing token 不得进入 WebUI、日志、普通业务数据库或任务结果。

## 3. 直接使用自动客户端

下载：

```text
GET /v1/clients/robohub_client_reference.py
GET /v1/clients/robohub-client-reference.mjs
```

两个文件都包含低层 `RoboHubClient` 与高层 `RoboHubManagedPool`。后者只在当前进程内保存 pool、fencing token 和短期凭据；不会写文件，也不会自动保存密码。

### Python

```python
from robohub_client_reference import RoboHubClient, RoboHubManagedPool, PoolCredentialAllocator

client = RoboHubClient(config.robohub_base_url, config.robohub_project_id, config.robohub_hmac_key)
pool = RoboHubManagedPool(client, current_user.id)

# 后续只保存 binding["account_ref"]；不要保存密码。
binding = pool.bind(account_ref=encrypted_config.account_ref)
# 首次没有 account_ref 时：
# binding = pool.bind(account_email=input_email, account_password=input_password)

ready = pool.prepare(task_ref=job.id, max_workers=job.concurrency)
credential, safe_metadata = PoolCredentialAllocator(ready["credentials"]).assign(worker_index=0)

# credential 仅传给当前 worker 的代理客户端；safe_metadata 才能写入结果。
pool.heartbeat()
pool.renew_credentials(job.id)
pool.close()  # 先停止 worker，再释放池
```

### Node.js

```js
import { RoboHubClient, RoboHubManagedPool, PoolCredentialAllocator } from "./robohub-client-reference.mjs";

const client = new RoboHubClient({ baseUrl, projectId, hmacKey });
const pool = new RoboHubManagedPool(client, currentUser.id);

await pool.bind({ accountRef: encryptedConfig.accountRef });
const ready = await pool.prepare({ taskRef: job.id, maxWorkers: job.concurrency });
const { credential, metadata } = new PoolCredentialAllocator(ready.credentials).assign(0);

await pool.heartbeat();
await pool.renewCredentials({ taskRef: job.id });
await pool.close();
```

`credential` / `credentials` 是敏感运行时对象；`safe_metadata` / `metadata` 才能进入业务结果。

## 4. 自动选择规则

`RoboHubManagedPool.prepare()` 固定做以下事情：

1. 只选 `status=active`、`subscription_ready!=false`、`node_count>0` 的套餐，优先 `kind=home-payg`。
2. 只把目录中 `available=true` 的节点作为测速候选。
3. 提交 `POST /v1/node-tests`，轮询到终态。
4. 再次读取节点，只把 `runnable=true` 的节点送入池；如果项目协议字段存在，还必须满足 `project_runnable!=false`。
5. 允许部分节点通过：至少一条通过即可建池。实际并发上限以 `credentials.length` 为准，不能假定等于请求并发数。
6. 普通注册默认 `egress_policy=line_usable`。仅明确需要不同公网出口族时使用 `distinct_family`。

套餐目录数、`available=true` 数、测速通过数、端口数和公网出口族数是不同指标，项目不得混用。

## 5. 状态与重试

项目只持久化：项目用户 ID、业务任务 ID、加密 `account_ref`、业务结果。以下只允许留在内存：pool、fencing token、凭据、代理 URL。

```text
binding_active -> catalog_ready -> node_test_queued/running
-> node_test_completed -> pool_active -> credential_active
-> heartbeat_loop -> released
```

| 返回码 | 后端处理 |
|---|---|
| `UPSTREAM_SESSION_RECOVERING`、`UPSTREAM_UNAVAILABLE`、`UPSTREAM_RATE_LIMITED`、`RUNNER_QUEUE_PENDING`、`RUNNER_PROVISIONING`、`RUNNER_CAPACITY_PENDING` | 按 `retry_after_ms` 或指数退避等待；保留 `account_ref` 与原业务任务；不回退直连、不重建账户。 |
| `NODE_TEST_REQUIRED` | 对同一服务/候选节点重新测速，只采用新结果中的 `runnable=true` 节点。 |
| `LEASE_FENCED`、`CONFIG_REVISION_CONFLICT`、`POOL_SELECTION_MISMATCH`、`RUNNER_EXPORT_NOT_READY` | 停止 worker，丢弃内存 pool/凭据，重新读取目录并重新 `prepare()`；不得复用旧端口或旧凭据。 |
| `UPSTREAM_AUTH_REQUIRED`、`UPSTREAM_ACCOUNT_NOT_FOUND`、`UPSTREAM_PROFILE_NOT_FOUND` | 重新绑定账户；不得猜测或复用过期 `account_ref`。 |
| 节点任务终态失败 | 展示稳定 `error_code`；不可把 Runner/Session 控制面错误伪装成节点延迟或 0ms。 |

同一写操作因网络重试时，保留原 `Idempotency-Key`；每次 HTTP 重试仍生成新的 HMAC timestamp/nonce。凭据签发/续期例外：每次用新幂等键，只采用最后一次响应凭据。

## 6. WebUI 与后台职责

- WebUI：选择账户、套餐、任务参数，显示脱敏状态。
- 项目后端：唯一持有 HMAC、调用 RoboHub、接收短期凭据、运行 heartbeat/release 的位置。
- Worker：只接收自己的短期凭据；失败时由 allocator 轮换出口，不得读取其他 worker 的敏感对象。

“套餐已读取、测速部分通过、代理池已建立”只证明 RoboHub 链路可用。邮箱、验证码、Cookie、目标站账户创建与上传失败仍属于项目业务链路，不能直接归因于中控台。

## 7. Test 与正式验收

1. 本地完整回归通过后，使用同一代码覆盖 Test。
2. Test WebUI 必须完成：绑定、套餐读取、节点读取、至少一条测速通过、建池、凭据使用、释放。
3. Test 通过后使用同一不可变归档覆盖正式。
4. 正式 WebUI 再做一次相同验收；业务失败时先按项目日志与 RoboHub request_id 对齐原因。

## 8. 其他入口

- 完整字段、所有端点、项目协议准入、动态线路与高级边界：`GET /v1/integration-guide.md` 与 `GET /v1/openapi.json`
- 稳定错误码：`docs/api/error-codes.md`
- 机器可读接口定义：`GET /v1/openapi.json`

新项目优先复制参考客户端里的 `RoboHubManagedPool`。只有需要主动动态轮转、项目协议验证或自定义出口族策略时，才使用低层端点并阅读完整规范。
