Initial 智念AIGC platform

This commit is contained in:
inman
2026-05-29 10:26:02 +08:00
commit f9c3393f84
86 changed files with 14741 additions and 0 deletions

102
lib/volcengine/signature.ts Normal file
View 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");
}

View 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;
}