diff --git a/src/main/java/us/cubk/BearerApiClient.java b/src/main/java/us/cubk/BearerApiClient.java index ff0e94e..f3ca1a1 100644 --- a/src/main/java/us/cubk/BearerApiClient.java +++ b/src/main/java/us/cubk/BearerApiClient.java @@ -10,6 +10,12 @@ import java.time.Duration; import java.util.Map; +/** + * 带 Bearer 授权的 HTTP 客户端。 + * 支持普通 POST/GET 请求和 SSE (Server-Sent Events) 流式请求。 + * 所有请求自动添加 Cosy 协议相关的请求头、签名和 Bearer 授权信息。 + * 请求体经过 QoderEncoding 编码后发送。 + */ public final class BearerApiClient { public static final ObjectMapper objectMapper = new ObjectMapper(); private final BearerBuilder.SessionContext sess; @@ -17,17 +23,29 @@ public final class BearerApiClient { public BearerApiClient(BearerBuilder.SessionContext sess) { this.sess = sess; } + /** 发送 POST 请求并返回 JSON 响应 */ public JsonNode callPost(String fullUrl, Object jsonBody) throws Exception { return call("POST", fullUrl, jsonBody, null); } + /** 发送 GET 请求并返回 JSON 响应 */ public JsonNode callGet(String fullUrl) throws Exception { return call("GET", fullUrl, null, null); } + /** 打开 SSE 流式连接,返回原始 InputStream 响应(基于 Java 11 HttpClient) */ public HttpResponse openStream(String fullUrl, Object jsonBody, Map extraHeaders) throws Exception { return openCallStream(fullUrl, jsonBody, extraHeaders); } + /** + * 打开 SSE 流式连接并逐行回调处理(基于 Apache HttpClient 5)。 + * 使用生产者-消费者模式异步读取流数据,支持 3 秒超时自动结束。 + * + * @param fullUrl 完整的请求 URL + * @param jsonBody 请求体对象(将被 JSON 序列化后经 QoderEncoding 编码) + * @param extraHeaders 额外的请求头 + * @param onLine 每读取到一行数据时的回调函数 + */ public void openStreamLines(String fullUrl, Object jsonBody, Map extraHeaders, java.util.function.Consumer onLine) throws Exception { URI u = URI.create(fullUrl); String pathQuery = u.getRawPath(); @@ -73,6 +91,11 @@ public void openStreamLines(String fullUrl, Object jsonBody, Map } } + /** + * SSE 流响应处理器。 + * 启动后台线程读取输入流,通过队列传递字节到主线程逐行解析。 + * 当超过 3 秒无新数据时自动结束。 + */ private Void execHandler(org.apache.hc.core5.http.ClassicHttpResponse response, java.util.function.Consumer onLine) throws Exception { if (response.getCode() != 200) { String errBody = org.apache.hc.core5.http.io.entity.EntityUtils.toString(response.getEntity()); @@ -118,6 +141,10 @@ private Void execHandler(org.apache.hc.core5.http.ClassicHttpResponse response, return null; } + /** + * 发送 HTTP 请求的通用方法(基于 Java 11 HttpClient)。 + * 自动完成: 请求体 QoderEncoding 编码 → 构建 payload → 计算签名 → 组装 Bearer + */ private JsonNode call(String method, String fullUrl, Object jsonBody, Map extraHeaders) throws Exception { URI u = URI.create(fullUrl); String pathQuery = u.getRawPath(); @@ -165,6 +192,10 @@ private JsonNode call(String method, String fullUrl, Object jsonBody, Map openCallStream(String fullUrl, Object jsonBody, Map extraHeaders) throws Exception { URI u = URI.create(fullUrl); diff --git a/src/main/java/us/cubk/BearerBuilder.java b/src/main/java/us/cubk/BearerBuilder.java index 38f164d..b24856b 100644 --- a/src/main/java/us/cubk/BearerBuilder.java +++ b/src/main/java/us/cubk/BearerBuilder.java @@ -16,8 +16,19 @@ import java.util.Map; import java.util.UUID; +/** + * Bearer 认证令牌构建器。 + * 负责创建会话上下文、生成请求签名和组装 Bearer 令牌。 + * 认证流程: + * 1. 生成 16 字节的临时 AES 密钥 + * 2. 使用服务器 RSA 公钥加密临时密钥 → cosyKey + * 3. 使用临时密钥 AES 加密用户身份信息 → info + * 4. 请求时将 info + cosyKey + 请求内容组合后 MD5 签名 + * 5. 组装 "Bearer COSY.{payload}.{signature}" 格式的授权头 + */ public final class BearerBuilder { + /** 服务器 RSA 公钥,用于加密临时密钥 */ public static final String SERVER_PUBKEY_PEM = ( "-----BEGIN PUBLIC KEY-----\n" + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA8iMH5c02LilrsERw9t6Pv5Nc\n" @@ -27,11 +38,17 @@ public final class BearerBuilder { + "-----END PUBLIC KEY-----"); private static final ObjectMapper objectMapper = new ObjectMapper(); + /** 用户身份信息记录,包含用户名、账号 ID、UID、组织信息和认证令牌等 */ public record AuthIdentity(String name, String aid, String uid, String yxUid, String organizationId, String organizationName, String userType, String securityOauthToken, String refreshToken) {} + /** 会话上下文,包含临时密钥、加密后的 cosyKey、身份信息和机器标识等 */ public record SessionContext(byte[] tempKey, String cosyKey, String info, AuthIdentity identity, String machineId, String machineToken, String machineType) { } + /** + * 创建新的认证会话。 + * 流程: 生成随机临时密钥(16字节) → RSA加密临时密钥 → AES加密身份信息 + */ public static SessionContext newSession(AuthIdentity id, String machineId, String machineToken, String machineType) throws Exception { byte[] tempKey = UUID.randomUUID().toString().replace("-", "").substring(0, 16).getBytes(StandardCharsets.US_ASCII); String cosyKey = Base64.getEncoder().encodeToString(rsaEncrypt(tempKey)); @@ -39,11 +56,19 @@ public static SessionContext newSession(AuthIdentity id, String machineId, Strin return new SessionContext(tempKey, cosyKey, info, id, machineId, machineToken, machineType); } + /** + * 生成请求签名。 + * 签名算法: MD5(payload + "\n" + cosyKey + "\n" + date + "\n" + body + "\n" + path) + */ public static String signRequest(String payloadB64, String cosyKey, String cosyDate, String body, String pathWithoutAlgo) throws Exception { String s = payloadB64 + "\n" + cosyKey + "\n" + cosyDate + "\n" + body + "\n" + pathWithoutAlgo; return md5Hex(s); } + /** + * 构建请求负载的 Base64 编码。 + * 负载包含版本号、加密后的身份信息和请求 ID。 + */ public static String buildPayloadB64(String info) throws Exception { Map m = new LinkedHashMap<>(); m.put("cosyVersion", "0.1.43"); @@ -55,10 +80,12 @@ public static String buildPayloadB64(String info) throws Exception { return Base64.getEncoder().encodeToString(objectMapper.writeValueAsBytes(sorted)); } + /** 组装最终的 Bearer 授权头,格式: "Bearer COSY.{payloadB64}.{signature}" */ public static String composeBearer(String payloadB64, String sig) { return "Bearer COSY." + payloadB64 + "." + sig; } + /** 将身份信息序列化为 JSON 字节数组,用于 AES 加密 */ static byte[] authPayloadJson(AuthIdentity id) throws Exception { ObjectNode n = objectMapper.createObjectNode(); n.put("name", id.name()); @@ -73,6 +100,7 @@ static byte[] authPayloadJson(AuthIdentity id) throws Exception { return objectMapper.writeValueAsBytes(n); } + /** 使用服务器 RSA 公钥加密临时密钥(RSA/ECB/PKCS1Padding) */ static byte[] rsaEncrypt(byte[] tempKey) throws Exception { String b64 = SERVER_PUBKEY_PEM.replace("-----BEGIN PUBLIC KEY-----", "").replace("-----END PUBLIC KEY-----", "").replaceAll("\\s+", ""); byte[] der = Base64.getDecoder().decode(b64); @@ -82,12 +110,14 @@ static byte[] rsaEncrypt(byte[] tempKey) throws Exception { return c.doFinal(tempKey); } + /** AES/CBC/PKCS5Padding 加密,IV 与密钥相同 */ static byte[] aesEncrypt(byte[] plain, byte[] key) throws Exception { Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding"); c.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(key)); return c.doFinal(plain); } + /** 计算 MD5 哈希值,返回 32 位小写十六进制字符串 */ static String md5Hex(String s) throws Exception { byte[] h = MessageDigest.getInstance("MD5").digest(s.getBytes(StandardCharsets.UTF_8)); StringBuilder sb = new StringBuilder(32); diff --git a/src/main/java/us/cubk/JobTokenClient.java b/src/main/java/us/cubk/JobTokenClient.java index 8775f2f..2b1901f 100644 --- a/src/main/java/us/cubk/JobTokenClient.java +++ b/src/main/java/us/cubk/JobTokenClient.java @@ -9,11 +9,27 @@ import java.net.http.HttpResponse; import java.time.Duration; +/** + * 作业令牌交换客户端。 + * 通过个人访问令牌(PAT)向 Qoder 中心服务交换会话令牌, + * 获取包含用户 ID、安全令牌、刷新令牌等信息的会话对象。 + */ public final class JobTokenClient { private static final ObjectMapper objectMapper = new ObjectMapper(); + /** 会话信息记录,包含用户 ID、名称、安全令牌、刷新令牌、过期时间、邮箱、计划类型和原始响应 */ public record Session(String userId, String name, String securityOauthToken, String refreshToken, long expireTime, String email, String plan, String raw) {} + /** + * 使用个人令牌交换会话令牌。 + * 请求体经过 QoderEncoding 编码后发送到 Qoder 中心服务。 + * + * @param personalToken 个人访问令牌 (PAT) + * @param machineId 机器唯一标识 + * @param machineToken 机器令牌 + * @param machineType 机器类型标识 + * @return 会话信息 + */ public static Session exchange(String personalToken, String machineId, String machineToken, String machineType) throws Exception { String date = Signature.currentDate(); String sig = Signature.sign(date); diff --git a/src/main/java/us/cubk/LocalAuth.java b/src/main/java/us/cubk/LocalAuth.java index 220872f..ed6f2d8 100644 --- a/src/main/java/us/cubk/LocalAuth.java +++ b/src/main/java/us/cubk/LocalAuth.java @@ -12,19 +12,38 @@ import java.nio.file.Paths; import java.util.Base64; +/** + * 本地认证信息读取器。 + * 从用户主目录下的 ~/.qoder/.auth/ 目录读取机器 ID 和加密存储的用户信息。 + * 用户信息使用 AES/CBC/PKCS5Padding 加密存储,密钥为机器 ID 的前 16 位。 + */ public final class LocalAuth { private static final ObjectMapper OM = new ObjectMapper(); + /** + * 获取默认的认证文件目录: ~/.qoder/.auth/ + */ public static Path defaultDir() { String home = System.getProperty("user.home"); return Paths.get(home, ".qoder", ".auth"); } + /** + * 读取本地存储的机器 ID。 + * 文件路径: ~/.qoder/.auth/id + */ public static String readMachineId() throws Exception { return new String(Files.readAllBytes(defaultDir().resolve("id")), StandardCharsets.UTF_8).trim(); } + /** + * 读取并解密本地存储的用户信息。 + * 文件路径: ~/.qoder/.auth/user(Base64 编码的 AES 密文) + * 解密密钥: 机器 ID 的前 16 字节,同时作为 IV 使用 + * + * @return 解密后的用户信息 JSON + */ public static JsonNode readUserInfo() throws Exception { String mid = readMachineId(); byte[] cipherBytes = Base64.getDecoder().decode(new String(Files.readAllBytes(defaultDir().resolve("user")), StandardCharsets.UTF_8).trim()); diff --git a/src/main/java/us/cubk/OpenAiBridge.java b/src/main/java/us/cubk/OpenAiBridge.java index 41ad8ef..5ba0812 100644 --- a/src/main/java/us/cubk/OpenAiBridge.java +++ b/src/main/java/us/cubk/OpenAiBridge.java @@ -15,6 +15,12 @@ import java.util.Map; import java.util.UUID; +/** + * OpenAI 兼容 API 网关,项目主入口。 + * 在本地启动 HTTP 服务器,提供兼容 OpenAI 的 /v1/chat/completions 接口。 + * 将 OpenAI 格式的请求转换为 Qoder API 格式,并将响应转换回 OpenAI 格式返回。 + * 支持流式 (SSE) 和非流式两种响应模式。 + */ public final class OpenAiBridge { private static final ObjectMapper objectMapper = new ObjectMapper(); @@ -22,6 +28,12 @@ public final class OpenAiBridge { private final BearerApiClient bearerClient; private final JsonNode templateBase; + /** + * 初始化桥接服务。 + * 流程: PAT 令牌交换 → 创建认证会话 → 加载 baseprompt.json 模板 + * + * @param pat 个人访问令牌 (Personal Access Token) + */ public OpenAiBridge(String pat) throws Exception { String mid = UUID.randomUUID().toString(); String mtoken = java.util.Base64.getUrlEncoder().withoutPadding().encodeToString((UUID.randomUUID().toString() + UUID.randomUUID()).substring(0, 50).getBytes()); @@ -42,6 +54,7 @@ public OpenAiBridge(String pat) throws Exception { this.templateBase = objectMapper.readTree(basePrompt); } + /** 启动 HTTP 服务器,监听指定端口的 /v1/chat/completions 路径 */ public void start(int port) throws Exception { HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", port), 0); server.createContext("/v1/chat/completions", this::handleChat); @@ -50,6 +63,14 @@ public void start(int port) throws Exception { System.out.println("[bridge] listening http://127.0.0.1:" + port + "/v1/chat/completions"); } + /** + * 处理 OpenAI 格式的聊天完成请求。 + * 流程: + * 1. 解析 OpenAI 格式的请求(提取 model、messages、stream 参数) + * 2. 构建 Qoder API 格式的请求体(基于 baseprompt.json 模板) + * 3. 调用 Qoder API 获取流式响应 + * 4. 将响应转换为 OpenAI 格式返回(流式或非流式) + */ private void handleChat(HttpExchange ex) throws IOException { try { if (!"POST".equals(ex.getRequestMethod())) { @@ -186,6 +207,7 @@ private void handleChat(HttpExchange ex) throws IOException { } } + /** 从 SSE 输入流中逐行解析并回调内容(备用方法,当前未使用) */ private void streamSseChunks(java.io.InputStream is, java.util.function.Consumer onChunk) throws IOException { java.io.ByteArrayOutputStream lineBuf = new java.io.ByteArrayOutputStream(); int b; @@ -211,6 +233,11 @@ private void streamSseChunks(java.io.InputStream is, java.util.function.Consumer } } + /** + * 从 SSE data 行中提取内容文本。 + * 响应格式: {"body": "{\"choices\":[{\"delta\":{\"content\":\"...\"}]}"} + * 需要两层 JSON 解析:外层取 body 字段,内层取 choices[].delta.content + */ private String extractContent(String dataLine) { try { JsonNode wrapper = objectMapper.readTree(dataLine); @@ -227,6 +254,7 @@ private String extractContent(String dataLine) { return null; } + /** 构建 OpenAI 流式响应的 chunk 对象模板 */ private ObjectNode makeChunk(String id, long created, String model) { ObjectNode root = objectMapper.createObjectNode(); root.put("id", id); root.put("object", "chat.completion.chunk"); @@ -241,6 +269,10 @@ private ObjectNode makeChunk(String id, long created, String model) { return root; } + /** + * 启动桥接服务的便捷方法。 + * 若未提供 PAT,将从系统属性 QODER_PAT 中读取。 + */ public static void run(String pat, int port) throws Exception { if (pat == null || pat.isBlank()) { pat = System.getProperty("QODER_PAT"); @@ -250,7 +282,8 @@ public static void run(String pat, int port) throws Exception { Thread.currentThread().join(); } + /** 程序入口,默认监听 8963 端口 */ public static void main(String[] args) throws Exception { - run(null, 8963); + run("", 8963); } } diff --git a/src/main/java/us/cubk/QoderEncoding.java b/src/main/java/us/cubk/QoderEncoding.java index f6b2695..67f50d0 100644 --- a/src/main/java/us/cubk/QoderEncoding.java +++ b/src/main/java/us/cubk/QoderEncoding.java @@ -2,16 +2,28 @@ import java.util.Base64; +/** + * Qoder 自定义 Base64 编码工具类。 + * 编码流程: 标准 Base64 → 三段重排 → 自定义字母表替换 + * 解码流程: 自定义字母表还原 → 逆向三段重排 → 标准 Base64 解码 + * 用于对 HTTP 请求体进行混淆编码,增加传输数据的不可读性。 + */ public final class QoderEncoding { + /** 自定义字母表(64 个字符,替代标准 Base64 字母表) */ public static final String CUSTOM_ALPHABET = "_doRTgHZBKcGVjlvpC,@aFSx#DPuNJme&i*MzLOEn)sUrthbf%Y^w.(kIQyXqWA!"; + /** 标准 Base64 字母表 */ public static final String STD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + /** 自定义填充符(替代标准 Base64 的 '=') */ public static final char CUSTOM_PAD = '$'; + /** 自定义字母表 → 标准字母表的映射表(用于解码) */ private static final int[] C2S = new int[128]; + /** 标准字母表 → 自定义字母表的映射表(用于编码) */ private static final int[] S2C = new int[128]; static { + // 初始化双向映射表 for (int i = 0; i < 128; i++) { C2S[i] = -1; S2C[i] = -1; } for (int i = 0; i < 64; i++) { C2S[CUSTOM_ALPHABET.charAt(i)] = STD_ALPHABET.charAt(i); @@ -23,11 +35,19 @@ public final class QoderEncoding { private QoderEncoding() {} + /** + * 将明文字节数组编码为 Qoder 自定义格式字符串。 + * 步骤: 1) 标准 Base64 编码 + * 2) 三段重排(将字符串分为三等份,按 第3段+第2段+第1段 重新组合) + * 3) 将标准 Base64 字符替换为自定义字母表字符 + */ public static String encode(byte[] plaintext) { String std = Base64.getEncoder().encodeToString(plaintext); int n = std.length(); int a = n / 3; + // 三段重排: [0,a) + [a, n-a) + [n-a, n) => [n-a,n) + [a,n-a) + [0,a) String rearranged = std.substring(n - a) + std.substring(a, n - a) + std.substring(0, a); + // 字母表替换: 标准 Base64 字符 → 自定义字符 StringBuilder sb = new StringBuilder(n); for (int i = 0; i < n; i++) { int c = rearranged.charAt(i); @@ -38,8 +58,15 @@ public static String encode(byte[] plaintext) { return sb.toString(); } + /** + * 将 Qoder 自定义编码字符串解码为原始字节数组。 + * 步骤: 1) 自定义字母表字符还原为标准 Base64 字符 + * 2) 逆向三段重排(恢复原始顺序) + * 3) 标准 Base64 解码 + */ public static byte[] decode(String encoded) { int n = encoded.length(); + // 字母表还原: 自定义字符 → 标准 Base64 字符 StringBuilder sb = new StringBuilder(n); for (int i = 0; i < n; i++) { int c = encoded.charAt(i); @@ -49,6 +76,7 @@ public static byte[] decode(String encoded) { } String mapped = sb.toString(); int a = n / 3; + // 逆向三段重排: 恢复原始顺序 String std = mapped.substring(n - a) + mapped.substring(a, n - a) + mapped.substring(0, a); return Base64.getDecoder().decode(std); } diff --git a/src/main/java/us/cubk/Signature.java b/src/main/java/us/cubk/Signature.java index fb4aecb..7e21a86 100644 --- a/src/main/java/us/cubk/Signature.java +++ b/src/main/java/us/cubk/Signature.java @@ -6,23 +6,44 @@ import java.time.format.DateTimeFormatter; import java.util.Locale; +/** + * 请求签名工具类。 + * 基于 MD5 算法,使用应用码(APPCODE)、密钥(SECRET)和 RFC1123 格式的日期 + * 生成请求签名,用于 Qoder 中心服务的接口认证。 + */ public final class Signature { + /** 应用标识码 */ public static final String APPCODE = "cosy"; + /** 签名密钥(Base64 编码的固定字符串) */ public static final String SECRET = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw=="; // base64("war, war never changes") + /** 签名各部分的分隔符 */ public static final String SEP = "&"; + /** RFC1123 日期格式化器(UTC 时区) */ private static final DateTimeFormatter RFC1123 = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH).withZone(ZoneOffset.UTC); private Signature() {} + /** + * 获取当前时间的 RFC1123 格式字符串,用作请求头中的日期字段。 + * 示例输出: "Thu, 01 May 2025 12:00:00 GMT" + */ public static String currentDate() { return RFC1123.format(Instant.now()); } + /** + * 生成请求签名。 + * 签名算法: MD5(APPCODE + "&" + SECRET + "&" + date) + * + * @param date RFC1123 格式的日期字符串 + * @return 32 位小写十六进制 MD5 签名 + */ public static String sign(String date) { return md5(APPCODE + SEP + SECRET + SEP + date); } + /** 计算字符串的 MD5 哈希值,返回 32 位小写十六进制字符串 */ private static String md5(String s) { try { byte[] h = MessageDigest.getInstance("MD5").digest(s.getBytes()); diff --git a/src/main/java/us/cubk/SignatureApiClient.java b/src/main/java/us/cubk/SignatureApiClient.java index c730145..2e1d1de 100644 --- a/src/main/java/us/cubk/SignatureApiClient.java +++ b/src/main/java/us/cubk/SignatureApiClient.java @@ -10,12 +10,20 @@ import java.time.Duration; +/** + * 基于签名认证的 HTTP 客户端。 + * 提供与 Qoder 中心服务的通信能力,包括令牌交换、用户状态查询和心跳发送。 + * 所有请求都经过 QoderEncoding 编码并附带时间戳签名。 + */ public final class SignatureApiClient { public static final ObjectMapper objectMapper = new ObjectMapper(); private final HttpClient http = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).connectTimeout(Duration.ofSeconds(15)).build(); + /** 机器唯一标识 */ private final String machineId; + /** 机器令牌 */ private final String machineToken; + /** 机器类型标识 */ private final String machineType; public SignatureApiClient(String machineId, String machineToken, String machineType) { @@ -24,6 +32,12 @@ public SignatureApiClient(String machineId, String machineToken, String machineT this.machineType = machineType; } + /** + * 使用个人访问令牌(PAT)交换作业令牌,获取用户会话信息。 + * + * @param personalToken 个人访问令牌 + * @return 包含用户 ID、名称、安全令牌等信息的 JSON + */ public JsonNode exchangeJobToken(String personalToken) throws Exception { var inner = objectMapper.createObjectNode(); inner.put("personalToken", personalToken); @@ -37,6 +51,12 @@ public JsonNode exchangeJobToken(String personalToken) throws Exception { return postEncoded("https://center.qoder.sh/algo/api/v3/user/jobToken?Encode=1", outer); } + /** + * 查询用户状态。 + * + * @param userId 用户 ID + * @return 用户状态信息 JSON + */ public JsonNode userStatus(String userId) throws Exception { var inner = objectMapper.createObjectNode(); inner.put("userId", userId); @@ -51,6 +71,10 @@ public JsonNode userStatus(String userId) throws Exception { return postEncoded("https://center.qoder.sh/algo/api/v3/user/status?Encode=1", outer); } + /** + * 发送心跳包,向服务器报告客户端在线状态。 + * 包含机器 ID、系统架构、操作系统版本等信息。 + */ public JsonNode heartbeat() throws Exception { var hb = objectMapper.createObjectNode(); hb.put("event_time", System.currentTimeMillis()); @@ -64,6 +88,14 @@ public JsonNode heartbeat() throws Exception { return postEncoded("https://center.qoder.sh/algo/api/v1/heartbeat?Encode=1", hb); } + /** + * 发送经过 QoderEncoding 编码的 POST 请求。 + * 自动添加时间戳、签名和机器标识等请求头。 + * + * @param url 目标 URL + * @param obj 请求体对象(将被 JSON 序列化后编码) + * @return 响应 JSON + */ private JsonNode postEncoded(String url, Object obj) throws Exception { String date = Signature.currentDate(); String sig = Signature.sign(date);