Initial 智念AIGC platform
This commit is contained in:
102
lib/volcengine/signature.ts
Normal file
102
lib/volcengine/signature.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { createHash, createHmac } from "node:crypto";
|
||||
|
||||
export type VolcengineSignatureInput = {
|
||||
method: "POST" | "GET";
|
||||
endpoint: string;
|
||||
query: Record<string, string>;
|
||||
body: string;
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
service: string;
|
||||
date?: Date;
|
||||
};
|
||||
|
||||
export type SignedVolcengineRequest = {
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
canonicalRequest: string;
|
||||
stringToSign: string;
|
||||
};
|
||||
|
||||
export function sha256Hex(value: string | Buffer): string {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
export function signVolcengineRequest(input: VolcengineSignatureInput): SignedVolcengineRequest {
|
||||
const endpoint = new URL(input.endpoint);
|
||||
const date = input.date || new Date();
|
||||
const xDate = formatAmzDate(date);
|
||||
const shortDate = xDate.slice(0, 8);
|
||||
const bodyHash = sha256Hex(input.body);
|
||||
const canonicalQuery = canonicalizeQuery(input.query);
|
||||
const canonicalHeaders = [
|
||||
`content-type:application/json`,
|
||||
`host:${endpoint.host}`,
|
||||
`x-content-sha256:${bodyHash}`,
|
||||
`x-date:${xDate}`
|
||||
].join("\n");
|
||||
const signedHeaders = "content-type;host;x-content-sha256;x-date";
|
||||
const canonicalRequest = [
|
||||
input.method,
|
||||
endpoint.pathname || "/",
|
||||
canonicalQuery,
|
||||
`${canonicalHeaders}\n`,
|
||||
signedHeaders,
|
||||
bodyHash
|
||||
].join("\n");
|
||||
const credentialScope = `${shortDate}/${input.region}/${input.service}/request`;
|
||||
const stringToSign = [
|
||||
"HMAC-SHA256",
|
||||
xDate,
|
||||
credentialScope,
|
||||
sha256Hex(canonicalRequest)
|
||||
].join("\n");
|
||||
const signingKey = getSigningKey(input.secretAccessKey, shortDate, input.region, input.service);
|
||||
const signature = hmacHex(signingKey, stringToSign);
|
||||
const authorization = [
|
||||
`HMAC-SHA256 Credential=${input.accessKeyId}/${credentialScope}`,
|
||||
`SignedHeaders=${signedHeaders}`,
|
||||
`Signature=${signature}`
|
||||
].join(", ");
|
||||
|
||||
endpoint.search = canonicalQuery;
|
||||
return {
|
||||
url: endpoint.toString(),
|
||||
headers: {
|
||||
"Authorization": authorization,
|
||||
"Content-Type": "application/json",
|
||||
"Host": endpoint.host,
|
||||
"X-Content-Sha256": bodyHash,
|
||||
"X-Date": xDate
|
||||
},
|
||||
canonicalRequest,
|
||||
stringToSign
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalizeQuery(query: Record<string, string>): string {
|
||||
return Object.entries(query)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||
.join("&");
|
||||
}
|
||||
|
||||
function formatAmzDate(date: Date): string {
|
||||
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
||||
}
|
||||
|
||||
function hmac(key: string | Buffer, value: string): Buffer {
|
||||
return createHmac("sha256", key).update(value).digest();
|
||||
}
|
||||
|
||||
function hmacHex(key: string | Buffer, value: string): string {
|
||||
return createHmac("sha256", key).update(value).digest("hex");
|
||||
}
|
||||
|
||||
function getSigningKey(secretAccessKey: string, date: string, region: string, service: string): Buffer {
|
||||
const kDate = hmac(secretAccessKey, date);
|
||||
const kRegion = hmac(kDate, region);
|
||||
const kService = hmac(kRegion, service);
|
||||
return hmac(kService, "request");
|
||||
}
|
||||
84
lib/volcengine/visual-client.ts
Normal file
84
lib/volcengine/visual-client.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import type { VisualTaskQueryResponse, VisualTaskSubmitResponse } from "@/lib/types";
|
||||
import { signVolcengineRequest } from "@/lib/volcengine/signature";
|
||||
|
||||
export type VisualClientConfig = {
|
||||
accessKeyId: string;
|
||||
secretAccessKey: string;
|
||||
region: string;
|
||||
service: string;
|
||||
endpoint: string;
|
||||
};
|
||||
|
||||
export function getVisualClientConfig(): VisualClientConfig | null {
|
||||
const accessKeyId = process.env.VOLCENGINE_ACCESS_KEY_ID;
|
||||
const secretAccessKey = process.env.VOLCENGINE_SECRET_ACCESS_KEY;
|
||||
if (!accessKeyId || !secretAccessKey) return null;
|
||||
return {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
region: process.env.VOLCENGINE_REGION || "cn-north-1",
|
||||
service: process.env.VOLCENGINE_SERVICE || "cv",
|
||||
endpoint: process.env.VOLCENGINE_VISUAL_ENDPOINT || "https://visual.volcengineapi.com"
|
||||
};
|
||||
}
|
||||
|
||||
export function shouldMockVisualApi(): boolean {
|
||||
const flag = process.env.JIMENG_VISUAL_MOCK || "auto";
|
||||
if (flag === "1" || flag === "true") return true;
|
||||
if (flag === "0" || flag === "false") return false;
|
||||
return getVisualClientConfig() === null;
|
||||
}
|
||||
|
||||
export async function submitVisualTask(
|
||||
payload: Record<string, unknown>,
|
||||
config = getVisualClientConfig()
|
||||
): Promise<VisualTaskSubmitResponse> {
|
||||
if (!config) throw new Error("Volcengine Visual credentials are not configured.");
|
||||
return callVisualApi<VisualTaskSubmitResponse>("CVSync2AsyncSubmitTask", payload, config);
|
||||
}
|
||||
|
||||
export async function queryVisualTask(
|
||||
payload: Record<string, unknown>,
|
||||
config = getVisualClientConfig()
|
||||
): Promise<VisualTaskQueryResponse> {
|
||||
if (!config) throw new Error("Volcengine Visual credentials are not configured.");
|
||||
return callVisualApi<VisualTaskQueryResponse>("CVSync2AsyncGetResult", payload, config);
|
||||
}
|
||||
|
||||
async function callVisualApi<T>(
|
||||
action: "CVSync2AsyncSubmitTask" | "CVSync2AsyncGetResult",
|
||||
payload: Record<string, unknown>,
|
||||
config: VisualClientConfig
|
||||
): Promise<T> {
|
||||
const body = JSON.stringify(payload);
|
||||
const signed = signVolcengineRequest({
|
||||
method: "POST",
|
||||
endpoint: config.endpoint,
|
||||
query: {
|
||||
Action: action,
|
||||
Version: "2022-08-31"
|
||||
},
|
||||
body,
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
region: config.region,
|
||||
service: config.service
|
||||
});
|
||||
const response = await fetch(signed.url, {
|
||||
method: "POST",
|
||||
headers: signed.headers,
|
||||
body
|
||||
});
|
||||
const text = await response.text();
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(text);
|
||||
} catch {
|
||||
throw new Error(`Volcengine Visual returned non-JSON response: ${response.status} ${text.slice(0, 240)}`);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const message = typeof json === "object" && json && "message" in json ? String(json.message) : text;
|
||||
throw new Error(`Volcengine Visual HTTP ${response.status}: ${message}`);
|
||||
}
|
||||
return json as T;
|
||||
}
|
||||
Reference in New Issue
Block a user