跳到主要内容

注册与授权

在线演示

体验注册与授权

用 PalmxApp 扫描下方二维码,亲身体验 Agent 注册与真人授权的完整流程。

1
生成注册请求
2
在 PalmxApp 扫码刷掌
3
兑换 open_id

⚡ 演示沙箱,数据不持久化。

Step 1 · Agent 本地生成密钥 + 自签发起注册

Agent 客户端本地生成 Ed25519 密钥对,私钥永不出本机。注册时用私钥对固定格式 message 自签,证明自己持有私钥。

待签名串(固定格式):

message = "clawkey-register-" + timestamp_ms

请求:

POST /palm/openai/x/agent/register/init
Content-Type: application/json

{
"AppId": "your_app_id", // 控制台拿到的公开 app_id
"DeviceId": "trader-001", // Agent 对外身份,^[A-Za-z0-9_-]{1,64}$,注册后不可变更
"PubkeyDer": "MCowBQYDK2VwAyEA...", // base64(Ed25519 SPKI DER)
"Message": "clawkey-register-1717488000000",
"Signature": "h3Js9k...", // base64(Ed25519(message)),64 字节裸签名
"Timestamp": 1717488000000 // Unix 毫秒,须与 message 中严格一致,±5min
}

响应:

{
"code": 0,
"data": {
"SessionId": "ars_xxxx",
"RegistrationUrl": "https://open.palmoa.youtu.qq.com/agent/register?sid=ars_xxxx",
"ExpiresAt": 1717488600
}
}

device_id vs agent_name

  • device_id:Agent 的稳定对外身份,给机器看,(app_id, device_id) 应用内唯一,注册后不可变更。
  • agent_name:真人侧友好名(如「我的炒币机器人」),UTF-8 ≤64 字符,允许中文 / emoji / 重名,由真人在确认页填写,用于 PalmxApp 展示。

客户端签名(Go 示例):

import (
"crypto/ed25519"
"crypto/rand"
"crypto/x509"
"encoding/base64"
"fmt"
"time"
)

// 一次性生成并持久化保存(私钥务必安全存储,永不外传)
pub, priv, _ := ed25519.GenerateKey(rand.Reader)
spkiDER, _ := x509.MarshalPKIXPublicKey(pub)

tsMs := time.Now().UnixMilli()
message := fmt.Sprintf("clawkey-register-%d", tsMs)
sig := ed25519.Sign(priv, []byte(message))

req := map[string]interface{}{
"AppId": APP_ID,
"DeviceId": DEVICE_ID,
"PubkeyDer": base64.StdEncoding.EncodeToString(spkiDER),
"Message": message,
"Signature": base64.StdEncoding.EncodeToString(sig),
"Timestamp": tsMs,
}
// POST 到 /palm/openai/x/agent/register/init

Step 2 · 真人扫码刷掌确认

Agent 把 RegistrationUrl 交给真人(展示二维码 / 发送链接)。真人浏览器打开后看到:

应用「{app_name}」下的「{agent_name}」想以你的身份运行,请用 PalmxApp 扫码刷掌确认。

真人用 PalmxApp 扫码、选择授权时长(1d / 3d / 7d / 30d,默认 7d)并刷掌确认。服务端单事务完成:

  • t_agent.statusactive
  • 写入 t_agent_user_binding(绑定该真人 open_id + 授权过期时间)
  • 生成一次性 code(绑定 (app_id, device_id, user_id, open_id),Redis TTL 5min

这一段是 PalmxApp / 落地页的职责,业务方无需实现。

Step 3 · Agent 轮询拿一次性 code

POST /palm/openai/x/agent/register/status
Content-Type: application/json

{ "SessionId": "ars_xxxx" }

响应:

{
"code": 0,
"data": {
"Status": "completed", // pending / completed / expired / failed
"DeviceId": "trader-001",
"Code": "ab12...cd34" // completed 时返回,一次性,TTL 5min;不返回 open_id
}
}

建议 2s 间隔轮询。注意:Agent 自身永远拿不到 open_id,只拿到一次性 code,交给业务方兑换。

Step 4 · 业务方用 code 换取 open_id(授权真正完成点)

Agent 把 code 交给业务方后端,业务方用 api_key 兑换:

POST /palm/openai/x/agent/code/exchange
Authorization: Bearer <api_key>
Content-Type: application/json

{ "Code": "ab12...cd34" }

响应:

{
"code": 0,
"data": {
"DeviceId": "trader-001",
"OpenId": "u_a1b2c3...",
"Registered": true
}
}

业务方拿到 open_id 后,查自己系统里 open_id ↔ 本地账号 的绑定关系

  • 命中本地账号 → 记录 (open_id, device_id) ↔ 本地用户,授权完成;
  • 未命中 → 返回失败(前置:用户须事先用 Palm 在你系统绑过号)。

code 一次性消费,过期或重复兑换返回错误。code.app_id 必须等于你的 api_key 对应的 oc_appid,否则 ErrAgentForbidden

业务方后端(Go 示例):

// POST /your-api/agent/complete —— Agent 把 code 发到你的后端
func CompleteAgentAuth(w http.ResponseWriter, r *http.Request) {
var in struct{ Code string `json:"code"` }
json.NewDecoder(r.Body).Decode(&in)

body, _ := json.Marshal(map[string]string{"Code": in.Code})
req, _ := http.NewRequest("POST",
"https://open.palmoa.youtu.qq.com/palm/openai/x/agent/code/exchange",
bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+API_KEY)

resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()

var out struct {
Data struct {
DeviceId string `json:"DeviceId"`
OpenId string `json:"OpenId"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&out)

// 查本地账号映射
localUser, ok := db.FindByPalmOpenId(out.Data.OpenId)
if !ok {
http.Error(w, "open_id not bound to any local account", http.StatusForbidden)
return
}
// 记录 (open_id, device_id) ↔ localUser,授权完成
db.BindAgent(localUser.ID, out.Data.OpenId, out.Data.DeviceId)
json.NewEncoder(w).Encode(map[string]bool{"success": true})
}