网页翻译JSSDK

功能描述

基于翻译大模型的页面翻译插件,提供页面翻译功能。您可以通过JSSDK,一键完成网站的多语言改造。

技术流程

image

接入JSSDK

步骤一:在HTML插入脚本

<html>
  <head></head>
  <body>
    <!-- 在文档的末尾插入翻译脚本 -->
    <script src="https://g.alicdn.com/translate-js-sdk/translate-js-sdk-stable/2.0.1/light.js"></script>
  </body>
</html>

步骤二:配置JSSDK

interface ITokenResponseData {
  code: 200;
  data: {
    url: string,
    host: string,
    method: "POST",
    headers: {
      "host": string,
      "x-acs-action": "BatchTranslate",
      "x-acs-version": string,
      "x-acs-date": string;
      "x-acs-signature-nonce": string;
      "content-type": "application/json",
      "x-acs-content-sha256": string;
      Authorization: string;
    };
    body: string;
  };
}

interface ITokenRequestData {
  sourceLanguage: string;
  targetLanguage: string;
  streaming: false,
  text: {[index: number]: string}
  scene: 'mt-turbo',
  fallbackTimeoutMs: number;
}

interface ISetupConfig {
  getToken: (data: ITokenRequestData) => Promise<ITokenResponseData>;
}

// 只用初始化一次,不需要每次翻译都初始化
__AliTranslate.setup({
  getToken: async (data) => {
    const res = await fetch('YOUR_GET_TOEKN_URL', { method: 'post', body: JSON.stringify(data) });
    return (await res.json()).data;
  }
});

步骤三:调用页面翻译

interface IPageTranslate {
  srcLanguage?: string; // 默认语种,默认为auto
  tgtLanguage?: string; // 目标语种,默认为en
  lazyload?: boolean; // 是否只翻译可视区域,默认为false
  lazyOffset?: number; // 可视区域的扩展
  target?: HTMLElement; // 要翻译的目标区域,默认为body
  except?: string; // 要排除翻译的区域,默认为空
}

const instance = __AliTranslate.pageTranslate({
  // 详细参数见PageTranslate参数
  lazyload: true,
  lazyOffset: 500
});

步骤四:取消翻译

// 触发取消翻译后,页面将回到原始文案
instance.destroy();

PageTranslate参数

参数

说明

类型

默认值

srcLanguage

默认的原语种

String

auto(会根据页面文案自动选择)

tgtLanguage

默认的目标语种

String

en

lazyload

是否只翻译可视区域

Boolean

false

lazyOffset

lazyloadtrue时,可视区域扩展

Number

-1 (-1表示不做任何offset)

target

要翻译的目标区域

HTMLElement | HTMLElement[]

body

except

不翻译的区域

Selector(String)

获取Token服务

使用阿里云SDKV3版本请求体&签名机制进行请求签名

// 通过calculateSignature方法生成签名
import * as crypto from 'crypto';

export interface SignatureRequest {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  headers: Record<string, string>;
  body: any;
  queryParam: Record<string, any>;
}

export interface BatchTranslateRequest {
  scene: string;
  sourceLanguage: string;
  streaming: boolean;
  targetLanguage: string;
  text: { [key: string]: string };
}

export class Request implements SignatureRequest {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  headers: Record<string, string>;
  body: any;
  queryParam: Record<string, any>;

  constructor(httpMethod: string, canonicalUri: string, host: string, xAcsAction: string, xAcsVersion: string) {
    this.httpMethod = httpMethod;
    this.canonicalUri = canonicalUri || '/';
    this.host = host;
    this.xAcsAction = xAcsAction;
    this.xAcsVersion = xAcsVersion;
    this.headers = {};
    this.body = null;
    this.queryParam = {};
    this.initHeader();
  }

  private initHeader() {
    const date = new Date();
    this.headers = {
      'host': this.host,
      'x-acs-action': this.xAcsAction,
      'x-acs-version': this.xAcsVersion,
      'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
      'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
    }
  }
}

const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ACCESS_KEY_ID; // 这里填入阿里云的AK
const accessKeySecret = process.env.ACCESS_KEY_SECRET; // 这里填入阿里云的SK
const workspaceId = process.env.WORKSPACE_ID; // 这里填入百炼的WorkspaceId
const encoder = new TextEncoder();
const httpMethod = 'POST';
const canonicalUri = '/anytrans/translate/batch';
const xAcsAction = 'BatchTranslate';
const xAcsVersion = '2025-07-07';
const host = 'anytrans.cn-hangzhou.aliyuncs.com';

export interface SignedRequest {
  url: string;
  method: string;
  host: string;
  headers: Record<string, string>;
  body?: any; // 可能是 Uint8Array 或 Base64 字符串
}

export function getAuthorization(signRequest: SignatureRequest): SignedRequest {
  try {
    const newQueryParam: Record<string, any> = {};
    processObject(newQueryParam, "", signRequest.queryParam);
    signRequest.queryParam = newQueryParam;

    // 步骤 1:拼接规范请求串
    const canonicalQueryString = Object.entries(signRequest.queryParam)
      .sort(([a], [b]) => a.localeCompare(b))
      .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
      .join('&');

    // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
    const requestPayload = signRequest.body || encoder.encode('');
    const hashedRequestPayload = sha256Hex(requestPayload);
    signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;

    // 将所有key都转换为小写
    signRequest.headers = Object.fromEntries(
      Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
    );

    const sortedKeys = Object.keys(signRequest.headers)
      .filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
      .sort();

    // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
    const signedHeaders = sortedKeys.join(";")

    // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
    const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';

    const canonicalRequest = [
      signRequest.httpMethod,
      signRequest.canonicalUri,
      canonicalQueryString,
      canonicalHeaders,
      signedHeaders,
      hashedRequestPayload
    ].join('\n');
    console.log('canonicalRequest=========>\n', canonicalRequest);

    // 步骤 2:拼接待签名字符串
    const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
    const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
    console.log('stringToSign=========>', stringToSign);

    // 步骤 3:计算签名
    const signature = hmac256(accessKeySecret!, stringToSign);
    console.log('signature=========>', signature);

    // 步骤 4:拼接 Authorization
    const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
    console.log('authorization=========>', authorization);
    signRequest.headers['Authorization'] = authorization;

    // 构建完整的URL
    let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
    if (signRequest.queryParam && Object.keys(signRequest.queryParam).length > 0) {
      const query = new URLSearchParams(signRequest.queryParam);
      url += '?' + query.toString();
    }

    return {
      url,
      host: signRequest.host,
      method: signRequest.httpMethod.toUpperCase(),
      headers: signRequest.headers,
      body: signRequest.body
    };
  } catch (error) {
    console.error('Failed to get authorization');
    console.error(error);
    throw error;
  }
}

function percentCode(str: string): string {
  return encodeURIComponent(str)
    .replace(/\+/g, '%20')
    .replace(/\*/g, '%2A')
    .replace(/~/g, '%7E');
}

function hmac256(key: string, data: string): string {
  const hmac = crypto.createHmac('sha256', key);
  hmac.update(data, 'utf8');
  return hmac.digest('hex').toLowerCase();
}

function sha256Hex(bytes: any): string {
  const hash = crypto.createHash('sha256');
  const digest = hash.update(bytes).digest('hex');
  return digest.toLowerCase();
}

function processObject(map: Record<string, any>, key: string, value: any): void {
  // 如果值为空,则无需进一步处理
  if (value === null) {
    return;
  }
  if (key === null) {
    key = "";
  }

  // 当值为Array类型时,遍历Array中的每个元素,并递归处理
  if (Array.isArray(value)) {
    value.forEach((item, index) => {
      processObject(map, `${key}.${index + 1}`, item);
    });
  } else if (typeof value === 'object' && value !== null) {
    // 当值为Object类型时,遍历Object中的每个键值对,并递归处理
    Object.entries(value).forEach(([subKey, subValue]) => {
      processObject(map, `${key}.${subKey}`, subValue);
    });
  } else {
    // 对于以"."开头的键,移除开头的"."以保持键的连续性
    if (key.startsWith('.')) {
      key = key.slice(1);
    }
    map[key] = String(value);
  }
}

export function formDataToString(formData: Record<string, any>): string {
  const tmp: Record<string, any> = {};
  processObject(tmp, "", formData);
  let queryString = '';
  for (let [key, value] of Object.entries(tmp)) {
    if (queryString !== '') {
      queryString += '&';
    }
    queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
  }
  return queryString;
}

// 固定变量签名计算方法
export interface FixedSignatureResult {
  httpMethod: string;
  canonicalUri: string;
  host: string;
  xAcsAction: string;
  xAcsVersion: string;
  signature: string;
  date: string;
  nonce: string;
  workspaceId: string;
  authorization: string;
}

export function calculateSignature(req: BatchTranslateRequest): SignedRequest {
  // 创建请求实例
  const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

  // 设置查询参数
  signRequest.queryParam = {
    RegionId: 'cn-hangzhou',
  };

  // 创建 body 的 Uint8Array
  const bodyUint8Array = encoder.encode(JSON.stringify({
    ...req,
    workspaceId,
  }));

  // 用于签名计算的 body
  signRequest.body = bodyUint8Array;

  signRequest.headers['content-type'] = 'application/json';

  // 获取签名
  const result = getAuthorization(signRequest);

  // 将 body 转换为 Base64 字符串用于传输
  const bodyBase64 = Buffer.from(bodyUint8Array).toString('base64');

  return {
    ...result,
    body: bodyBase64 // 返回 Base64 字符串而不是 Uint8Array
  };
} 
// 通过calculateSignature方法生成签名
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.net.URLEncoder;
import java.io.UnsupportedEncodingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;

/**
 * 签名请求接口
 */
interface SignatureRequest {
    String getHttpMethod();
    String getCanonicalUri();
    String getHost();
    String getXAcsAction();
    String getXAcsVersion();
    Map<String, String> getHeaders();
    byte[] getBody();
    Map<String, Object> getQueryParam();
    
    void setHeaders(Map<String, String> headers);
    void setBody(byte[] body);
    void setQueryParam(Map<String, Object> queryParam);
}

/**
 * 批量翻译请求数据类
 */
class BatchTranslateRequest {
    private String scene;
    private String sourceLanguage;
    private boolean streaming;
    private String targetLanguage;
    private Map<String, String> text;
    
    // 构造函数
    public BatchTranslateRequest() {}
    
    public BatchTranslateRequest(String scene, 
                               String sourceLanguage, boolean streaming, 
                               String targetLanguage, Map<String, String> text) {
        this.scene = scene;
        this.sourceLanguage = sourceLanguage;
        this.streaming = streaming;
        this.targetLanguage = targetLanguage;
        this.text = text;
    }
    
    // Getters and Setters
    public String getScene() { return scene; }
    public void setScene(String scene) { this.scene = scene; }
    
    public String getSourceLanguage() { return sourceLanguage; }
    public void setSourceLanguage(String sourceLanguage) { this.sourceLanguage = sourceLanguage; }
    
    public boolean isStreaming() { return streaming; }
    public void setStreaming(boolean streaming) { this.streaming = streaming; }
    
    public String getTargetLanguage() { return targetLanguage; }
    public void setTargetLanguage(String targetLanguage) { this.targetLanguage = targetLanguage; }
    
    public Map<String, String> getText() { return text; }
    public void setText(Map<String, String> text) { this.text = text; }
}

/**
 * 请求实现类
 */
class Request implements SignatureRequest {
    private String httpMethod;
    private String canonicalUri;
    private String host;
    private String xAcsAction;
    private String xAcsVersion;
    private Map<String, String> headers;
    private byte[] body;
    private Map<String, Object> queryParam;
    
    public Request(String httpMethod, String canonicalUri, String host, 
                  String xAcsAction, String xAcsVersion) {
        this.httpMethod = httpMethod;
        this.canonicalUri = canonicalUri != null ? canonicalUri : "/";
        this.host = host;
        this.xAcsAction = xAcsAction;
        this.xAcsVersion = xAcsVersion;
        this.headers = new HashMap<>();
        this.body = null;
        this.queryParam = new HashMap<>();
        initHeader();
    }
    
    private void initHeader() {
        Instant now = Instant.now();
        String date = DateTimeFormatter.ISO_INSTANT.format(now);
        String nonce = generateRandomHex(16);
        
        this.headers.put("host", this.host);
        this.headers.put("x-acs-action", this.xAcsAction);
        this.headers.put("x-acs-version", this.xAcsVersion);
        this.headers.put("x-acs-date", date);
        this.headers.put("x-acs-signature-nonce", nonce);
    }
    
    private String generateRandomHex(int length) {
        SecureRandom random = new SecureRandom();
        byte[] bytes = new byte[length];
        random.nextBytes(bytes);
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    // Getters
    @Override
    public String getHttpMethod() { return httpMethod; }
    @Override
    public String getCanonicalUri() { return canonicalUri; }
    @Override
    public String getHost() { return host; }
    @Override
    public String getXAcsAction() { return xAcsAction; }
    @Override
    public String getXAcsVersion() { return xAcsVersion; }
    @Override
    public Map<String, String> getHeaders() { return headers; }
    @Override
    public byte[] getBody() { return body; }
    @Override
    public Map<String, Object> getQueryParam() { return queryParam; }
    
    // Setters
    @Override
    public void setHeaders(Map<String, String> headers) { this.headers = headers; }
    @Override
    public void setBody(byte[] body) { this.body = body; }
    @Override
    public void setQueryParam(Map<String, Object> queryParam) { this.queryParam = queryParam; }
}

/**
 * 签名后的请求结果
 */
class SignedRequest {
    private String url;
    private String method;
    private String host;
    private Map<String, String> headers;
    private String body; // Base64 字符串
    
    public SignedRequest(String url, String method, String host, 
                        Map<String, String> headers, String body) {
        this.url = url;
        this.method = method;
        this.host = host;
        this.headers = headers;
        this.body = body;
    }
    
    // Getters
    public String getUrl() { return url; }
    public String getMethod() { return method; }
    public String getHost() { return host; }
    public Map<String, String> getHeaders() { return headers; }
    public String getBody() { return body; }
}

/**
 * 固定签名结果接口
 */
interface FixedSignatureResult {
    String getHttpMethod();
    String getCanonicalUri();
    String getHost();
    String getXAcsAction();
    String getXAcsVersion();
    String getSignature();
    String getDate();
    String getNonce();
    String getWorkspaceId();
    String getAuthorization();
}

/**
 * Node签名计算主类
 */
public class NodeSignature {
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";
    private static final String ACCESS_KEY_ID = System.getenv("ACCESS_KEY_ID"); // 这里填入阿里云的AK
    private static final String ACCESS_KEY_SECRET = System.getenv("ACCESS_KEY_SECRET"); // 这里填入阿里云的SK
    private static final String WORKSPACE_ID = System.getenv("WORKSPACE_ID"); // 这里填入百炼的WorkspaceId
    
    private static final String HTTP_METHOD = "POST";
    private static final String CANONICAL_URI = "/anytrans/translate/batch";
    private static final String X_ACS_ACTION = "BatchTranslate";
    private static final String X_ACS_VERSION = "2025-07-07";
    private static final String HOST = "anytrans.cn-hangzhou.aliyuncs.com";
    
    private static final ObjectMapper objectMapper = new ObjectMapper();
    
    /**
     * 获取授权信息
     */
    public static SignedRequest getAuthorization(SignatureRequest signRequest) {
        try {
            Map<String, Object> newQueryParam = new HashMap<>();
            processObject(newQueryParam, "", signRequest.getQueryParam());
            signRequest.setQueryParam(newQueryParam);
            
            // 步骤 1:拼接规范请求串
            String canonicalQueryString = signRequest.getQueryParam().entrySet().stream()
                .sorted(Map.Entry.comparingByKey())
                .map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue())))
                .collect(Collectors.joining("&"));
            
            // 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
            byte[] requestPayload = signRequest.getBody() != null ? signRequest.getBody() : new byte[0];
            String hashedRequestPayload = sha256Hex(requestPayload);
            signRequest.getHeaders().put("x-acs-content-sha256", hashedRequestPayload);
            
            // 将所有key都转换为小写
            Map<String, String> lowerCaseHeaders = signRequest.getHeaders().entrySet().stream()
                .collect(Collectors.toMap(
                    entry -> entry.getKey().toLowerCase(),
                    Map.Entry::getValue
                ));
            signRequest.setHeaders(lowerCaseHeaders);
            
            List<String> sortedKeys = lowerCaseHeaders.keySet().stream()
                .filter(key -> key.startsWith("x-acs-") || key.equals("host") || key.equals("content-type"))
                .sorted()
                .collect(Collectors.toList());
            
            // 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
            String signedHeaders = String.join(";", sortedKeys);
            
            // 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
            String canonicalHeaders = sortedKeys.stream()
                .map(key -> key + ":" + lowerCaseHeaders.get(key))
                .collect(Collectors.joining("\n")) + "\n";
            
            String canonicalRequest = String.join("\n", Arrays.asList(
                signRequest.getHttpMethod(),
                signRequest.getCanonicalUri(),
                canonicalQueryString,
                canonicalHeaders,
                signedHeaders,
                hashedRequestPayload
            ));
            System.out.println("canonicalRequest=========>\n" + canonicalRequest);
            
            // 步骤 2:拼接待签名字符串
            String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
            System.out.println("stringToSign=========>" + stringToSign);
            
            // 步骤 3:计算签名
            String signature = hmac256(ACCESS_KEY_SECRET, stringToSign);
            System.out.println("signature=========>" + signature);
            
            // 步骤 4:拼接 Authorization
            String authorization = String.format("%s Credential=%s,SignedHeaders=%s,Signature=%s",
                ALGORITHM, ACCESS_KEY_ID, signedHeaders, signature);
            System.out.println("authorization=========>" + authorization);
            lowerCaseHeaders.put("authorization", authorization);
            
            // 构建完整的URL
            String url = "https://" + signRequest.getHost() + signRequest.getCanonicalUri();
            if (signRequest.getQueryParam() != null && !signRequest.getQueryParam().isEmpty()) {
                String query = signRequest.getQueryParam().entrySet().stream()
                    .map(entry -> {
                        try {
                            return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + 
                                   URLEncoder.encode(String.valueOf(entry.getValue()), "UTF-8");
                        } catch (UnsupportedEncodingException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .collect(Collectors.joining("&"));
                url += "?" + query;
            }
            
            String bodyString = signRequest.getBody() != null ? 
                Base64.getEncoder().encodeToString(signRequest.getBody()) : null;
            
            return new SignedRequest(url, signRequest.getHttpMethod().toUpperCase(), 
                                   signRequest.getHost(), lowerCaseHeaders, bodyString);
                                   
        } catch (Exception error) {
            System.err.println("Failed to get authorization");
            error.printStackTrace();
            throw new RuntimeException(error);
        }
    }
    
    /**
     * URL编码
     */
    private static String percentCode(String str) {
        try {
            return URLEncoder.encode(str, "UTF-8")
                .replace("+", "%20")
                .replace("*", "%2A")
                .replace("~", "%7E");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * HMAC-SHA256加密
     */
    private static String hmac256(String key, String data) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKeySpec);
            byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return bytesToHex(hmacBytes).toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * SHA256哈希
     */
    private static String sha256Hex(byte[] bytes) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] hashBytes = digest.digest(bytes);
            return bytesToHex(hashBytes).toLowerCase();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 字节数组转十六进制字符串
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
    
    /**
     * 处理对象,将嵌套对象展平
     */
    private static void processObject(Map<String, Object> map, String key, Object value) {
        // 如果值为空,则无需进一步处理
        if (value == null) {
            return;
        }
        if (key == null) {
            key = "";
        }
        
        // 当值为Array类型时,遍历Array中的每个元素,并递归处理
        if (value instanceof List) {
            List<?> list = (List<?>) value;
            for (int i = 0; i < list.size(); i++) {
                processObject(map, key + "." + (i + 1), list.get(i));
            }
        } else if (value instanceof Object[] || value.getClass().isArray()) {
            Object[] array = (Object[]) value;
            for (int i = 0; i < array.length; i++) {
                processObject(map, key + "." + (i + 1), array[i]);
            }
        } else if (value instanceof Map) {
            // 当值为Object类型时,遍历Object中的每个键值对,并递归处理
            Map<?, ?> objectMap = (Map<?, ?>) value;
            for (Map.Entry<?, ?> entry : objectMap.entrySet()) {
                processObject(map, key + "." + entry.getKey(), entry.getValue());
            }
        } else {
            // 对于以"."开头的键,移除开头的"."以保持键的连续性
            if (key.startsWith(".")) {
                key = key.substring(1);
            }
            map.put(key, String.valueOf(value));
        }
    }
    
    /**
     * 表单数据转字符串
     */
    public static String formDataToString(Map<String, Object> formData) {
        Map<String, Object> tmp = new HashMap<>();
        processObject(tmp, "", formData);
        
        return tmp.entrySet().stream()
            .map(entry -> {
                try {
                    return URLEncoder.encode(entry.getKey(), "UTF-8") + "=" + 
                           URLEncoder.encode(String.valueOf(entry.getValue()), "UTF-8");
                } catch (UnsupportedEncodingException e) {
                    throw new RuntimeException(e);
                }
            })
            .collect(Collectors.joining("&"));
    }
    
    /**
     * 固定变量签名计算方法
     */
    public static SignedRequest calculateSignature(BatchTranslateRequest req) {
        try {
            // 创建请求实例
            Request signRequest = new Request(HTTP_METHOD, CANONICAL_URI, HOST, X_ACS_ACTION, X_ACS_VERSION);
            
            // 设置查询参数
            Map<String, Object> queryParam = new HashMap<>();
            queryParam.put("RegionId", "cn-hangzhou");
            signRequest.setQueryParam(queryParam);
            
            // 创建请求体数据
            Map<String, Object> bodyData = new HashMap<>();
            bodyData.put("scene", req.getScene());
            bodyData.put("sourceLanguage", req.getSourceLanguage());
            bodyData.put("streaming", req.isStreaming());
            bodyData.put("targetLanguage", req.getTargetLanguage());
            bodyData.put("text", req.getText());
            bodyData.put("workspaceId", WORKSPACE_ID);
            
            // 创建 body 的字节数组
            String jsonString = objectMapper.writeValueAsString(bodyData);
            byte[] bodyBytes = jsonString.getBytes(StandardCharsets.UTF_8);
            
            // 用于签名计算的 body
            signRequest.setBody(bodyBytes);
            signRequest.getHeaders().put("content-type", "application/json");
            
            // 获取签名
            SignedRequest result = getAuthorization(signRequest);
            
            return result;
            
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JSON序列化失败", e);
        }
    }
}
# 通过calculate_signature方法生成签名
import hashlib
import hmac
import json
import os
import secrets
import urllib.parse
from datetime import datetime
from typing import Dict, Any, Optional, Union, List
from dataclasses import dataclass, field
import base64


@dataclass
class SignatureRequest:
    """签名请求接口"""
    http_method: str
    canonical_uri: str
    host: str
    x_acs_action: str
    x_acs_version: str
    headers: Dict[str, str] = field(default_factory=dict)
    body: Optional[bytes] = None
    query_param: Dict[str, Any] = field(default_factory=dict)


@dataclass
class BatchTranslateRequest:
    """批量翻译请求数据类"""
    app_name: str
    platform: str
    scene: str
    source_language: str
    streaming: bool
    target_language: str
    text: Dict[str, str]


@dataclass
class SignedRequest:
    """签名后的请求结果"""
    url: str
    method: str
    host: str
    headers: Dict[str, str]
    body: Optional[str] = None  # Base64字符串


class Request:
    """请求实现类"""
    
    def __init__(self, http_method: str, canonical_uri: str, host: str, 
                 x_acs_action: str, x_acs_version: str):
        self.http_method = http_method
        self.canonical_uri = canonical_uri or '/'
        self.host = host
        self.x_acs_action = x_acs_action
        self.x_acs_version = x_acs_version
        self.headers: Dict[str, str] = {}
        self.body: Optional[bytes] = None
        self.query_param: Dict[str, Any] = {}
        self._init_header()
    
    def _init_header(self):
        """初始化请求头"""
        date = datetime.utcnow().isoformat().replace('T', ' ').replace(' ', 'T') + 'Z'
        # 移除微秒部分
        if '.' in date:
            date = date.split('.')[0] + 'Z'
        
        nonce = secrets.token_hex(16)
        
        self.headers = {
            'host': self.host,
            'x-acs-action': self.x_acs_action,
            'x-acs-version': self.x_acs_version,
            'x-acs-date': date,
            'x-acs-signature-nonce': nonce
        }


class NodeSignature:
    """Node签名计算主类"""
    
    ALGORITHM = 'ACS3-HMAC-SHA256'
    ACCESS_KEY_ID = os.getenv('ACCESS_KEY_ID')  # 这里填入阿里云的AK
    ACCESS_KEY_SECRET = os.getenv('ACCESS_KEY_SECRET')  # 这里填入阿里云的SK
    WORKSPACE_ID = os.getenv('WORKSPACE_ID')  # 这里填入百炼的WorkspaceId
    
    HTTP_METHOD = 'POST'
    CANONICAL_URI = '/anytrans/translate/batch'
    X_ACS_ACTION = 'BatchTranslate'
    X_ACS_VERSION = '2025-07-07'
    HOST = 'anytrans.cn-hangzhou.aliyuncs.com'
    
    @staticmethod
    def get_authorization(sign_request: SignatureRequest) -> SignedRequest:
        """获取授权信息"""
        try:
            new_query_param: Dict[str, Any] = {}
            NodeSignature._process_object(new_query_param, "", sign_request.query_param)
            sign_request.query_param = new_query_param
            
            # 步骤 1:拼接规范请求串
            canonical_query_string = '&'.join([
                f"{NodeSignature._percent_code(key)}={NodeSignature._percent_code(str(value))}"
                for key, value in sorted(sign_request.query_param.items())
            ])
            
            # 请求体,当请求正文为空时,比如GET请求,RequestPayload固定为空字符串
            request_payload = sign_request.body if sign_request.body is not None else b''
            hashed_request_payload = NodeSignature._sha256_hex(request_payload)
            sign_request.headers['x-acs-content-sha256'] = hashed_request_payload
            
            # 将所有key都转换为小写
            sign_request.headers = {k.lower(): v for k, v in sign_request.headers.items()}
            
            sorted_keys = sorted([
                key for key in sign_request.headers.keys()
                if key.startswith('x-acs-') or key == 'host' or key == 'content-type'
            ])
            
            # 已签名消息头列表,多个请求头名称(小写)按首字母升序排列并以英文分号(;)分隔
            signed_headers = ';'.join(sorted_keys)
            
            # 构造请求头,多个规范化消息头,按照消息头名称(小写)的字符代码顺序以升序排列后拼接在一起
            canonical_headers = '\n'.join([f"{key}:{sign_request.headers[key]}" for key in sorted_keys]) + '\n'
            
            canonical_request = '\n'.join([
                sign_request.http_method,
                sign_request.canonical_uri,
                canonical_query_string,
                canonical_headers,
                signed_headers,
                hashed_request_payload
            ])
            print(f'canonicalRequest=========>\n{canonical_request}')
            
            # 步骤 2:拼接待签名字符串
            hashed_canonical_request = NodeSignature._sha256_hex(canonical_request.encode('utf-8'))
            string_to_sign = f"{NodeSignature.ALGORITHM}\n{hashed_canonical_request}"
            print(f'stringToSign=========>{string_to_sign}')
            
            # 步骤 3:计算签名
            signature = NodeSignature._hmac256(NodeSignature.ACCESS_KEY_SECRET, string_to_sign)
            print(f'signature=========>{signature}')
            
            # 步骤 4:拼接 Authorization
            authorization = (f"{NodeSignature.ALGORITHM} "
                           f"Credential={NodeSignature.ACCESS_KEY_ID},"
                           f"SignedHeaders={signed_headers},"
                           f"Signature={signature}")
            print(f'authorization=========>{authorization}')
            sign_request.headers['authorization'] = authorization
            
            # 构建完整的URL
            url = f"https://{sign_request.host}{sign_request.canonical_uri}"
            if sign_request.query_param:
                query_string = urllib.parse.urlencode(sign_request.query_param)
                url += f"?{query_string}"
            
            body_string = None
            if sign_request.body is not None:
                body_string = base64.b64encode(sign_request.body).decode('utf-8')
            
            return SignedRequest(
                url=url,
                method=sign_request.http_method.upper(),
                host=sign_request.host,
                headers=sign_request.headers,
                body=body_string
            )
            
        except Exception as error:
            print('Failed to get authorization')
            print(error)
            raise error
    
    @staticmethod
    def _percent_code(string: str) -> str:
        """URL编码"""
        return urllib.parse.quote(string, safe='').replace('+', '%20').replace('*', '%2A').replace('~', '%7E')
    
    @staticmethod
    def _hmac256(key: str, data: str) -> str:
        """HMAC-SHA256加密"""
        return hmac.new(
            key.encode('utf-8'),
            data.encode('utf-8'),
            hashlib.sha256
        ).hexdigest().lower()
    
    @staticmethod
    def _sha256_hex(data: Union[bytes, str]) -> str:
        """SHA256哈希"""
        if isinstance(data, str):
            data = data.encode('utf-8')
        return hashlib.sha256(data).hexdigest().lower()
    
    @staticmethod
    def _process_object(result_map: Dict[str, Any], key: str, value: Any) -> None:
        """处理对象,将嵌套对象展平"""
        # 如果值为空,则无需进一步处理
        if value is None:
            return
        if key is None:
            key = ""
        
        # 当值为List类型时,遍历List中的每个元素,并递归处理
        if isinstance(value, list):
            for index, item in enumerate(value):
                NodeSignature._process_object(result_map, f"{key}.{index + 1}", item)
        elif isinstance(value, dict):
            # 当值为dict类型时,遍历dict中的每个键值对,并递归处理
            for sub_key, sub_value in value.items():
                NodeSignature._process_object(result_map, f"{key}.{sub_key}", sub_value)
        else:
            # 对于以"."开头的键,移除开头的"."以保持键的连续性
            if key.startswith('.'):
                key = key[1:]
            result_map[key] = str(value)
    
    @staticmethod
    def form_data_to_string(form_data: Dict[str, Any]) -> str:
        """表单数据转字符串"""
        tmp: Dict[str, Any] = {}
        NodeSignature._process_object(tmp, "", form_data)
        
        query_parts = []
        for key, value in tmp.items():
            encoded_key = urllib.parse.quote_plus(key)
            encoded_value = urllib.parse.quote_plus(str(value))
            query_parts.append(f"{encoded_key}={encoded_value}")
        
        return '&'.join(query_parts)
    
    @staticmethod
    def calculate_signature(req: BatchTranslateRequest) -> SignedRequest:
        """固定变量签名计算方法"""
        # 创建请求实例
        sign_request = Request(
            NodeSignature.HTTP_METHOD,
            NodeSignature.CANONICAL_URI,
            NodeSignature.HOST,
            NodeSignature.X_ACS_ACTION,
            NodeSignature.X_ACS_VERSION
        )
        
        # 设置查询参数
        sign_request.query_param = {
            'RegionId': 'cn-hangzhou'
        }
        
        # 创建请求体数据
        body_data = {
            'scene': req.scene,
            'sourceLanguage': req.source_language,
            'streaming': req.streaming,
            'targetLanguage': req.target_language,
            'text': req.text,
            'workspaceId': NodeSignature.WORKSPACE_ID
        }
        
        # 创建 body 的字节数组
        json_string = json.dumps(body_data, ensure_ascii=False)
        body_bytes = json_string.encode('utf-8')
        
        # 用于签名计算的 body
        sign_request.body = body_bytes
        sign_request.headers['content-type'] = 'application/json'
        
        # 创建SignatureRequest对象
        signature_request = SignatureRequest(
            http_method=sign_request.http_method,
            canonical_uri=sign_request.canonical_uri,
            host=sign_request.host,
            x_acs_action=sign_request.x_acs_action,
            x_acs_version=sign_request.x_acs_version,
            headers=sign_request.headers,
            body=sign_request.body,
            query_param=sign_request.query_param
        )
        
        # 获取签名
        result = NodeSignature.get_authorization(signature_request)
        
        return result


if __name__ == "__main__":
    # 使用示例
    try:
        # 注意:在实际使用时,请设置环境变量
        # os.environ['ACCESS_KEY_ID'] = 'your_access_key_id'
        # os.environ['ACCESS_KEY_SECRET'] = 'your_access_key_secret'  
        # os.environ['WORKSPACE_ID'] = 'your_workspace_id'
        
        # 创建翻译请求
        request = BatchTranslateRequest(
            app_name='your_app_name',
            platform='web',
            scene='general',
            source_language='zh',
            streaming=False,
            target_language='en',
            text={
                'text1': '你好,世界!',
                'text2': '这是一个测试。'
            }
        )
        
        # 计算签名并获取请求信息
        signed_request = NodeSignature.calculate_signature(request)
        
        # 打印结果
        print("=== 签名计算结果 ===")
        print(f"URL: {signed_request.url}")
        print(f"Method: {signed_request.method}")
        print(f"Host: {signed_request.host}")
        print("\n=== 请求头 ===")
        for key, value in signed_request.headers.items():
            print(f"{key}: {value}")
        print(f"\n=== 请求体 (Base64) ===")
        print(signed_request.body)
        
    except Exception as e:
        print(f"签名计算失败:{e}")
        import traceback
        traceback.print_exc()

常见问题

1. AK/SK在哪里获取?

阿里云平台的AccessKey模块中获取

2. 获取Token服务中的workspaceId从哪里获取?

登录AK/SK对应的阿里云账号后,在百炼左下角的业务空间详情中获取

imageimage