两种方式共用同一套卡密/账号体系与签名密钥,登录成功拿到的是同一个 token,按需二选一。
客户软件不用自己写登录界面和协议:启动这个 EXE,登录成功后输出 token + 到期时间 + 卡信息 到文件/命令行,你的软件拿到后自己跑心跳即可。
| 对接方式 | 登录界面 | 协议 | 心跳 | 适用 |
|---|---|---|---|---|
| 纯 API 对接(本文档) | 客户自己做 | 客户自己调 | 客户自己跑 | 有界面开发能力的团队 |
| 登录组件(本说明) | 组件内置模板 | 组件内部完成 | 客户自己跑 | 只要登录窗、不想写界面/协议的客户 |
| 加密壳打包(加密工具) | 壳内置模板 | 壳内置 | 壳自动 | 用加密工具打包的客户 |
# 客户只拿 exe + ini 就能用;改界面只需换模板,不用重新编译 [soo] base_url = https://sooyz.com ; 服务端地址(主站或实例域名) app_key = 你的开放API应用Key ; 用户中心 → 开放平台 生成 app_secret = 你的开放API应用密钥 ; 与 Key 配对,组件自动 HMAC 签名 tpl_url = ; 云端模板地址(可空);优先于 tpl_file tpl_file = templates\index.html ; 本地模板文件(相对 exe 目录) product_name = 软件名称 ; 显示在窗口标题 out_file = soo-login-result.json ; 登录成功结果输出文件(相对 exe 目录) machine = ; 留空=自动取本机机器码;可固定填写 width = 1440 ; 窗口宽(模板 meta soo-window 优先) height = 900 ; 窗口高 topmost = 0 ; 1=置顶 exit_on_success = 1 ; 登录成功是否自动退出 stdout_mode = 1 ; 同时向标准输出打印结果(供父进程读取) http_timeout = 15 ; 网络请求超时(秒)
tpl_url 填模板访问地址,组件每次启动下载最新模板到本地缓存(webview2-data\tpl\index.html),改界面不用重新发 exe。index_soo.html 改名为 index.html 放进 exe 同目录 templates\,tpl_file = templates\index.html 即可。登录成功后组件写 out_file(JSON)+ 打印到标准输出:
{"ok": true, "type": "card|user", "token": "登录返回的会话token", "expire": "2027-01-01 12:00:00", "expire_ts": 1752300000, "machine": "8430-915d-ed9e-ba14-95e8", "card": "卡号(卡密登录时有)", "user": "用户名(账号登录时有)", "plan_name": "套餐名", "notice": "登录成功提示"}
out_file 出现且 "ok": true 即登录成功;⚠ 心跳:组件不做心跳。你的软件拿到 token 后,按本文档 /api/open/user/login、/api/open/card/login 返回的 heartbeat_sec(后台「心跳时长」设置)定时调心跳接口续期/校验即可(心跳不计 API 日限额)。
tpl_url 或 tpl_file 指向的模板即可,无需重新编译。base_url 填实例域名,app_key/app_secret 用该实例生成的开放 API 密钥。获取:交付包「05-登录组件」(exe + ini 示例 + bridge.js + 对接说明.md + src),或联系管理员获取。
app_key 与 api_secret。https://sooyz.com 下的 /api/open/*(本文档示例用 https://sooyz.com 作 BaseURL)。每次请求需携带以下 4 个公共参数 + 业务参数:
| 参数 | 说明 |
|---|---|
app_key | 应用的公开标识(可明文传输) |
timestamp | 当前 Unix 秒级时间戳,允许误差 ±300 秒 |
nonce | 随机字符串(每次请求不同,防重放,600 秒内不可重复) |
sign | 签名:HMAC-SHA256(api_secret, canon) 的十六进制小写串 |
canon 构造规则:取「全部参数(含公共参数与业务参数,不含 sign)」,剔除值为空的参数,按 key 字典序排序,拼接为 k1=v1&k2=v2&...。
# 伪代码:canon 示例 params = {"app_key": "a1b2...", "timestamp": "1792146000", "nonce": "x9f2...", "card": "ABCD-1234"} canon = "app_key=a1b2...&card=ABCD-1234&nonce=x9f2...×tamp=1792146000" # 字典序、排除空值、排除 sign sign = HMAC_SHA256(hex, api_secret, canon)
⚠ GET 请求:签名参数与业务参数全部放 URL query;⚠ POST 请求:全部放 JSON body(GET/POST 的签名键集必须一致,都含 4 个公共参数)。
{"code": 0, "msg": "ok", "data": {...}} # code=0 成功;非 0 为业务错误;HTTP 401=鉴权失败,429=限流
| code | 含义 |
|---|---|
| 0 | 成功 |
| 4001 / 4003 / 4004 | 应用不存在 / 签名错误 / 卡密不存在 |
| 4010 / 4011 / 4012 | 卡密不存在 / 已封禁 / 机器码不匹配(需解绑) |
| 4015 | 会话不存在或已下线(心跳需重登) |
| 4101 / 4110 / 4111 | 登录参数缺失 / 账号不存在或密码错误 / 账号已封禁 |
| 400 / 401 / 429 (HTTP) | 参数错误 / 鉴权失败(签名、时间窗、重放)/ QPS 或日调用超限 |
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /api/open/card/login | 卡密登录(自动激活/续期、绑定机器码) |
| POST | /api/open/user/login | 账号密码模式登录 |
| POST | /api/open/heartbeat | 心跳续活(在线状态保活) |
| GET | /api/open/card/info | 查询卡密状态(到期/次数/绑机/在线) |
| GET | /api/open/sessions | 在线会话列表(kw/page/size) |
| POST | /api/open/session/kick | 踢下线(按 token 或 card) |
| GET | /api/open/info | 公告 / 最新版本 / 远程算法配置 |
| POST | /api/open/card/issue | 批量发卡(免费版拒绝;单次 1-500 张) |
| POST | /api/open/card/{ban|unban|delete} | 卡密封禁 / 解封 / 删除 |
| POST | /api/open/user/{ban|unban|resetpwd} | 账号封禁 / 解封 / 重置密码 |
| GET/POST/DELETE | /api/open/vars | 全局变量读取 / 写入 / 删除(跨客户端同步) |
参数:card 卡密号(必填)· machine 机器码(必填,首次登录即绑定)· ip 客户端 IP(可选)
返回 data:token(后续心跳/踢线用)· card_type · expire_at · remaining_count · status · heartbeat_sec(服务端下发的心跳间隔秒数,后台「软件设置/用户中心-应用设置」可调,缺省 60,范围 5~600)
参数:username · password · machine · ip(可选)
返回 data:同卡密登录,token 直接使用。
参数:token(必填)· machine(可选,校验机器一致性)· ip(可选)
建议客户端按登录返回的 data.heartbeat_sec 作为心跳间隔调用(缺省 60 秒);连续心跳失败 / 被踢后返回 4015,客户端应重新登录。
参数:card 卡密号(必填)
返回 data:status · card_type · duration · expire_at · remaining_count · bound_machine · online 等。
参数:kw(卡密/账号模糊,可选)· page · size
参数:token(按会话踢,可选)或 card(按卡密踢,可选)——至少传一个。
参数:card_type(time 时间卡 / count 次数卡 / forever 永久卡 / points 积分卡,必填)· count(1-500,必填)· duration(时间卡天数,必填)· points(积分卡数值,可选)· prefix(卡密前缀,可选)· length(去前缀后长度,默认 14,可选)· note(备注,可选)
返回 data:cards 新卡列表。
参数:card 卡密号(必填)。delete 会同时清除该卡会话。
参数:username(必填)· password(仅 resetpwd 时必填,新密码)。
无业务参数,返回该应用全部全局变量(跨客户端实时同步,适合授权开关/配置下发)。
参数:vkey(必填)· vvalue(必填)· alias(别名,可选)。
参数:vkey(必填)。
以下示例均为可直接运行的最小实现:签名 → 卡密登录 → 心跳(间隔跟随服务端 heartbeat_sec,含 4015 自动重登)。把 APP_KEY / API_SECRET / CARD_NO 换成你自己的即可。
# Python3 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖) import hashlib, hmac, json, time, secrets, urllib.request BASE = "https://sooyz.com" APP_KEY = "你的app_key" SECRET = "你的api_secret" CARD = "你的卡密号" MACHINE = "本机机器码" def sign_params(params): params["app_key"] = APP_KEY params["timestamp"] = str(int(time.time())) params["nonce"] = secrets.token_hex(8) canon = "&".join(f"{k}={v}" for k, v in sorted(params.items()) if k != "sign" and v != "") params["sign"] = hmac.new(SECRET.encode(), canon.encode(), hashlib.sha256).hexdigest() return params def post(path, params): body = json.dumps(sign_params(params)).encode() req = urllib.request.Request(BASE + path, data=body, headers={"Content-Type": "application/json"}) return json.loads(urllib.request.urlopen(req).read()) def card_login(): r = post("/api/open/card/login", {"card": CARD, "machine": MACHINE}) if r["code"] != 0: raise SystemExit(r) d = r["data"] print("登录成功", d) # 心跳间隔(秒)以服务端下发 heartbeat_sec 为准,缺省 60 return d["token"], int(d.get("heartbeat_sec") or 60) # 1. 卡密登录(首次自动绑定机器码) token, hb_sec = card_login() # 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登) while True: time.sleep(hb_sec) r = post("/api/open/heartbeat", {"token": token, "machine": MACHINE}) print("心跳", r) if r["code"] == 4015: # 会话失效/被踢 print("会话失效,重新登录") token, hb_sec = card_login()
// C# (.NET 6+) 最小示例:签名 + 卡密登录 + 心跳(无第三方依赖) using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; var appKey = "你的app_key"; var secret = "你的api_secret"; var card = "你的卡密号"; var machine = "本机机器码"; var http = new HttpClient(); Dictionary<string, string> Sign(Dictionary<string, string> p) { p["app_key"] = appKey; p["timestamp"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(); p["nonce"] = Guid.NewGuid().ToString("N"); var canon = string.Join("&", p.Where(x => x.Key != "sign" && x.Value != "") .OrderBy(x => x.Key).Select(x => $"{x.Key}={x.Value}")); using (var h = new HMACSHA256(Encoding.UTF8.GetBytes(secret))) p["sign"] = Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(canon))).ToLower(); return p; } async Task<string> Post(string path, Dictionary<string, string> p) { var body = new StringContent(JsonSerializer.Serialize(Sign(p)), Encoding.UTF8, "application/json"); var resp = await http.PostAsync("https://sooyz.com" + path, body); return await resp.Content.ReadAsStringAsync(); } async Task<(string token, int hbSec)> CardLogin() { var r = await Post("/api/open/card/login", new Dictionary<string, string> { { "card", card }, { "machine", machine } }); var data = JsonSerializer.Deserialize<JsonElement>(r); if (data.GetProperty("code").GetInt32() != 0) throw new Exception(r); var d = data.GetProperty("data"); Console.WriteLine("登录成功 " + r); // 心跳间隔(秒)以服务端下发 heartbeat_sec 为准,缺省 60 var hb = d.TryGetProperty("heartbeat_sec", out var h) ? h.GetInt32() : 60; return (d.GetProperty("token").GetString(), hb); } // 1. 卡密登录(首次自动绑定机器码) var (token, hbSec) = await CardLogin(); // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登) while (true) { await Task.Delay(hbSec * 1000); var hr = await Post("/api/open/heartbeat", new Dictionary<string, string> { { "token", token }, { "machine", machine } }); Console.WriteLine("心跳 " + hr); if (hr.Contains("\"4015\"")) { Console.WriteLine("会话失效,重新登录"); (token, hbSec) = await CardLogin(); } }
// Java 11+ 最小示例:签名 + 卡密登录 + 心跳(JDK 自带 java.net.http,无第三方依赖) import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.StandardCharsets; import java.util.*; public class SooApi { static String appKey = "你的app_key"; static String secret = "你的api_secret"; static String card = "你的卡密号"; static String machine = "本机机器码"; static HttpClient http = HttpClient.newHttpClient(); static Map<String,String> sign(Map<String,String> p) { p.put("app_key", appKey); p.put("timestamp", String.valueOf(System.currentTimeMillis()/1000)); p.put("nonce", UUID.randomUUID().toString().replace("-","")); List<String> keys = new ArrayList<>(p.keySet()); Collections.sort(keys); StringBuilder canon = new StringBuilder(); boolean first = true; for (String k : keys) { if (k.equals("sign") || p.get(k).isEmpty()) continue; if (!first) canon.append("&"); first = false; canon.append(k).append("=").append(p.get(k)); } try { Mac mac = Mac.getInstance("HmacSHA256"); mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); byte[] h = mac.doFinal(canon.toString().getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(); for (byte b : h) sb.append(String.format("%02x", b)); p.put("sign", sb.toString()); } catch (Exception e) { throw new RuntimeException(e); } return p; } static String toJson(Map<String,String> p) { StringBuilder sb = new StringBuilder("{"); boolean first = true; for (Map.Entry<String,String> e : p.entrySet()) { if (!first) sb.append(","); first = false; sb.append("\"").append(e.getKey()).append("\"") .append(":").append("\"").append(e.getValue()).append("\""); } return sb.append("}").toString(); } static String post(String path, Map<String,String> p) throws Exception { HttpRequest req = HttpRequest.newBuilder() .uri(URI.create("https://sooyz.com" + path)) .header("Content-Type", "application/json") .POST(HttpRequest.BodyPublishers.ofString(toJson(sign(p)))) .build(); return http.send(req, HttpResponse.BodyHandlers.ofString()).body(); } static int hbSec = 60; // 心跳间隔(秒):登录时从 data.heartbeat_sec 读取,缺省 60 static String cardLogin() throws Exception { Map<String,String> p = new HashMap<>(); p.put("card", card); p.put("machine", machine); String r = post("/api/open/card/login", p); if (!r.contains("\"code\":0") && !r.contains("\"code\": 0")) throw new RuntimeException(r); System.out.println("登录成功 " + r); // 解析 data.heartbeat_sec(缺省 60) String[] hs = r.split("\"heartbeat_sec\":"); if (hs.length > 1) hbSec = Integer.parseInt(hs[1].split(",")[0].trim()); return r.split("\"token\":\"")[1].split("\"")[0]; } public static void main(String[] args) throws Exception { // 1. 卡密登录(首次自动绑定机器码) String token = cardLogin(); // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登) while (true) { Thread.sleep(hbSec * 1000L); Map<String,String> h = new HashMap<>(); h.put("token", token); h.put("machine", machine); String hr = post("/api/open/heartbeat", h); System.out.println("心跳 " + hr); if (hr.contains("4015")) { System.out.println("会话失效,重新登录"); token = cardLogin(); } } } }
// Go 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖) package main import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "sort" "strconv" "strings" "time" ) var ( base = "https://sooyz.com" appKey = "你的app_key" secret = "你的api_secret" card = "你的卡密号" machine = "本机机器码" ) func signParams(p map[string]string) map[string]string { p["app_key"] = appKey p["timestamp"] = strconv.FormatInt(time.Now().Unix(), 10) p["nonce"] = fmt.Sprintf("%d", time.Now().UnixNano()) keys := make([]string, 0, len(p)) for k := range p { keys = append(keys, k) } sort.Strings(keys) var parts []string for _, k := range keys { if k == "sign" || p[k] == "" { continue } parts = append(parts, k+"="+p[k]) } mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(strings.Join(parts, "&"))) p["sign"] = hex.EncodeToString(mac.Sum(nil)) return p } func post(path string, p map[string]string) (map[string]interface{}, error) { body, _ := json.Marshal(signParams(p)) resp, err := http.Post(base+path, "application/json", bytes.NewReader(body)) if err != nil { return nil, err } defer resp.Body.Close() var out map[string]interface{} err = json.NewDecoder(resp.Body).Decode(&out) return out, err } func cardLogin() (string, int, error) { r, err := post("/api/open/card/login", map[string]string{"card": card, "machine": machine}) if err != nil { return "", 60, err } if code, _ := r["code"].(float64); code != 0 { return "", 60, fmt.Errorf("%v", r) } data := r["data"].(map[string]interface{}) token := data["token"].(string) hb := 60 // 心跳间隔(秒):以服务端下发 heartbeat_sec 为准,缺省 60 if v, ok := data["heartbeat_sec"].(float64); ok && v > 0 { hb = int(v) } fmt.Println("登录成功", data) return token, hb, nil } func main() { // 1. 卡密登录(首次自动绑定机器码) token, hbSec, err := cardLogin() if err != nil { fmt.Println(err); return } // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登) for { time.Sleep(time.Duration(hbSec) * time.Second) hr, err := post("/api/open/heartbeat", map[string]string{"token": token, "machine": machine}) if err != nil { fmt.Println("心跳错误", err); continue } fmt.Println("心跳", hr) if code, _ := hr["code"].(float64); code == 4015 { fmt.Println("会话失效,重新登录") token, hbSec, _ = cardLogin() } } }
<?php // PHP 最小示例:签名 + 卡密登录 + 心跳(标准库,无第三方依赖,CLI 运行) <?php $base = 'https://sooyz.com'; $appKey = '你的app_key'; $secret = '你的api_secret'; $card = '你的卡密号'; $machine = '本机机器码'; function sign_params($params) { global $appKey, $secret; $params['app_key'] = $appKey; $params['timestamp'] = (string)time(); $params['nonce'] = bin2hex(random_bytes(8)); ksort($params); // 字典序 $canon = ''; foreach ($params as $k => $v) { if ($k === 'sign' || $v === '') continue; $canon .= $k . '=' . $v . '&'; } $params['sign'] = hash_hmac('sha256', rtrim($canon, '&'), $secret); return $params; } function post($path, $params) { global $base; $params = sign_params($params); $ctx = stream_context_create(['http' => [ 'method' => 'POST', 'header' => 'Content-Type: application/json', 'content' => json_encode($params), ]]); return json_decode(file_get_contents($base . $path, false, $ctx), true); } function card_login() { global $card, $machine; $r = post('/api/open/card/login', ['card' => $card, 'machine' => $machine]); if ($r['code'] !== 0) { fwrite(STDERR, json_encode($r) . PHP_EOL); exit(1); } $d = $r['data']; echo "登录成功 " . json_encode($d) . PHP_EOL; // 心跳间隔(秒):以服务端下发 heartbeat_sec 为准,缺省 60 $hb = isset($d['heartbeat_sec']) && $d['heartbeat_sec'] > 0 ? (int)$d['heartbeat_sec'] : 60; return ['token' => $d['token'], 'hb' => $hb]; } // 1. 卡密登录(首次自动绑定机器码) $login = card_login(); $token = $login['token']; $hbSec = $login['hb']; // 2. 心跳保活(间隔=heartbeat_sec;返回 4015 需重登) while (true) { sleep($hbSec); $hr = post('/api/open/heartbeat', ['token' => $token, 'machine' => $machine]); echo "心跳 " . json_encode($hr) . PHP_EOL; if ($hr['code'] === 4015) { echo "会话失效,重新登录" . PHP_EOL; $login = card_login(); $token = $login['token']; $hbSec = $login['hb']; } }
'================================================'
' 易语言 · 签名 + 卡密登录 + 心跳(精易模块 / 或直接 Winhttp)'
'================================================'
.版本 2
.程序集 窗口程序集_启动窗口
.子程序 取签名, 文本型
.参数 参数字典, 文本型 ' 已按字典序排好的 k=v&k=v
.参数 密钥, 文本型
.局部变量 结果, 字节集
结果 = 校验_取HMAC_SHA256 (到字节集 (参数字典), 到字节集 (密钥))
返回 (取数据摘要 (结果)) ' 十六进制小写
.子程序 卡密登录, 文本型
.局部变量 时间戳, 文本型
.局部变量 随机串, 文本型
.局部变量 签名, 文本型
.局部变量 请求体, 文本型
.局部变量 返回文本, 文本型
.局部变量 结果对象, 对象
.局部变量 token, 文本型
时间戳 = 到文本 (取现行时间戳 ())
随机串 = 文本_取随机字符 (16)
' 参数需按 key 字典序拼接:app_key,card,machine,nonce,timestamp
签名 = 取签名 (
“app_key=” + “你的app_key” + “&card=” + “你的卡密号”
+ “&machine=” + “本机机器码” + “&nonce=” + 随机串
+ “×tamp=” + 时间戳,
“你的api_secret”)
请求体 = “{” + “” + “\”app_key\”:” + “\”你的app_key\”” + “,\”card\”:” + “\”你的卡密号\”” + “,\”machine\”:” + “\”本机机器码\”” + “,\”nonce\”:” + “\”” + 随机串 + “\”” + “,\”timestamp\”:” + “\”” + 时间戳 + “\”” + “,\”sign\”:” + “\”” + 签名 + “\”” + “}”
' 返回文本 = 网页_访问_对象 (“https://sooyz.com/api/open/card/login”, 1, 请求体, , , “application/json”)
' 用 编码_Utf8到Ansi / json 解析 返回文本,取 data.token 存入全局变量 token
' 同时取 data.heartbeat_sec 存入全局变量 心跳间隔(缺省 60,5~600)
' 若 code≠0 提示错误(4010 卡密不存在 / 4011 已封禁 / 4012 机器码不匹配)
返回 (token)
.子程序 心跳保活
.局部变量 时间戳, 文本型
.局部变量 随机串, 文本型
.局部变量 签名, 文本型
.局部变量 请求体, 文本型
.局部变量 返回文本, 文本型
' 每 心跳间隔 秒调用一次(间隔=登录返回的 data.heartbeat_sec,缺省 60);参数:token,machine + 公共参数,字典序拼接后签名
时间戳 = 到文本 (取现行时间戳 ())
随机串 = 文本_取随机字符 (16)
签名 = 取签名 (
“app_key=” + “你的app_key” + “&machine=” + “本机机器码”
+ “&nonce=” + 随机串 + “×tamp=” + 时间戳
+ “&token=” + 全局token,
“你的api_secret”)
请求体 = “{” + “\”app_key\”:” + “\”你的app_key\”” + “,\”machine\”:” + “\”本机机器码\”” + “,\”nonce\”:” + “\”” + 随机串 + “\”” + “,\”timestamp\”:” + “\”” + 时间戳 + “\”” + “,\”token\”:” + “\”” + 全局token + “\”” + “,\”sign\”:” + “\”” + 签名 + “\”” + “}”
' 返回文本 = 网页_访问_对象 (“https://sooyz.com/api/open/heartbeat”, 1, 请求体, , , “application/json”)
' 解析返回 code:0=正常;4015=会话失效需重新 卡密登录()'================================================'
' 火山PC · 签名 + 卡密登录 + 心跳'
'================================================'
' 关键步骤(火山视窗,需引用「火山模块」或手写 HMAC):'
' 1. 组装参数:app_key、card、machine、nonce、timestamp(心跳再加 token)'
' 2. 按键名首字母排序后拼 "k=v&k=v"(字典序,排除空值与 sign)'
' 3. 用 api_secret 做 HMAC-SHA256,转十六进制小写 = sign'
' 4. 把全部参数转 JSON 文本,POST https://sooyz.com/api/open/card/login'
' 5. 解析返回 JSON,code==0 取 data.token 保存(4012=机器码不匹配需解绑);并保存 data.heartbeat_sec 为心跳间隔(缺省 60,5~600)'
' 6. 每 心跳间隔 秒 POST /api/open/heartbeat(间隔=登录返回 heartbeat_sec,缺省 60);code==4015 重新登录'
' 火山视窗代码示意(伪码,方法名以你引用的模块为准):'
' 时间戳 = 取Unix时间戳 ()'
' 随机串 = 文本_生成随机字符串 (16)'
' 待签名文本 = "app_key=" + 你的app_key + "&card=" + 你的卡密号'
' 待签名文本 = 待签名文本 + "&machine=" + 本机机器码'
' 待签名文本 = 待签名文本 + "&nonce=" + 随机串 + "×tamp=" + 时间戳'
' 签名 = HMAC_SHA256_十六进制 (待签名文本, 你的api_secret)'
' 发送POST请求(地址="https://sooyz.com/api/open/card/login", 数据=JSON化参数, 内容类型="application/json")'
' 返回 JSON 解析 code==0 后取 data.token 保存到全局变量'
' 心跳:每 心跳间隔 秒 发送POST请求(地址="https://sooyz.com/api/open/heartbeat", 数据=JSON化(token,machine), 内容类型="application/json") (间隔=登录返回 data.heartbeat_sec,缺省 60)'
' 若 code==4015 → 重新执行登录流程'================================================'
' 按键精灵(QUI / 脚本)· 签名 + 卡密登录 + 心跳'
'================================================'
' 需调用系统 COM 或使用第三方脚本插件实现 HMAC-SHA256;'
' 若脚本环境无 HMAC 库,可在本地程序内算好 sign 后由脚本调用(推荐)。'
' 按键精灵 9 / 2014 脚本示意:'
Dim appKey, secret, card, machine, ts, nonce, canon, sign, resp, token
appKey = "你的app_key" : secret = "你的api_secret"
card = "你的卡密号" : machine = "本机机器码"
' —— 卡密登录 ——'
ts = CStr(Round(GetSysTime()/1000)) ' Unix 秒级时间戳
nonce = GetTime() & Int(Rnd*100000) ' 随机串
' 字典序拼接(app_key < card < machine < nonce < timestamp)
canon = "app_key=" & appKey & "&card=" & card & "&machine=" & machine
canon = canon & "&nonce=" & nonce & "×tamp=" & ts
' sign = HMAC_SHA256(secret, canon) —— 用插件/COM 计算后填充
sign = 调用插件计算HMAC(canon, secret)
' 组装 JSON 并 POST https://sooyz.com/api/open/card/login,解析 code==0 取 data.token
' 并记录 data.heartbeat_sec 为心跳间隔(缺省 60,5~600)
' token = 解析返回JSON(data.token)
' —— 心跳保活:每 心跳间隔 秒执行一次(登录返回 heartbeat_sec,缺省 60) ——'
ts = CStr(Round(GetSysTime()/1000)) : nonce = GetTime() & Int(Rnd*100000)
canon = "app_key=" & appKey & "&machine=" & machine & "&nonce=" & nonce
canon = canon & "×tamp=" & ts & "&token=" & token
sign = 调用插件计算HMAC(canon, secret)
' 组装 JSON POST https://sooyz.com/api/open/heartbeat'
' 返回 code==4015 → 回到上方重新执行卡密登录
' (计时可用 Plugin.Window 或延时循环控制,一轮 = 心跳间隔秒数,从登录返回 heartbeat_sec 读取)POST /api/open/card/login(卡密模式)或 user/login(账号模式)→ 校验 code==0 且 expire_at 未过期。token 后进入主流程;按 data.heartbeat_sec(缺省 60 秒)调 POST /api/open/heartbeat 保活。4015(被踢/会话失效)→ 回到第 1 步重新登录。sessions、session/kick、info。code 判断业务错误,HTTP 401/429 判断鉴权与限流;建议对 401 检查时间校准,对 429 做退避重试。更多问题 / 定制接口需求,联系 QQ:39460097