feat: sync upstream code

This commit is contained in:
Hk-Gosuto
2024-11-08 10:57:17 +08:00
parent 52726d42e9
commit c948a28ef2
49 changed files with 5999 additions and 801 deletions

View File

@@ -1,19 +0,0 @@
import { getClientConfig } from "../config/client";
import { ApiPath, DEFAULT_API_HOST } from "../constant";
export function corsPath(path: string) {
const baseUrl = getClientConfig()?.isApp ? `${DEFAULT_API_HOST}` : "";
if (baseUrl === "" && path === "") {
return "";
}
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith("/")) {
path += "/";
}
return `${baseUrl}${path}`;
}

246
app/utils/hmac.ts Normal file
View File

@@ -0,0 +1,246 @@
// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58
// From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8
// To ensure cross-browser support even without a proper SubtleCrypto
// impelmentation (or without access to the impelmentation, as is the case with
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
// HMAC signatures using nothing but raw JavaScript
/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */
// By giving internal functions names that we can mangle, future calls to
// them are reduced to a single byte (minor space savings in minified file)
const uint8Array = Uint8Array;
const uint32Array = Uint32Array;
const pow = Math.pow;
// Will be initialized below
// Using a Uint32Array instead of a simple array makes the minified code
// a bit bigger (we lose our `unshift()` hack), but comes with huge
// performance gains
const DEFAULT_STATE = new uint32Array(8);
const ROUND_CONSTANTS: number[] = [];
// Reusable object for expanded message
// Using a Uint32Array instead of a simple array makes the minified code
// 7 bytes larger, but comes with huge performance gains
const M = new uint32Array(64);
// After minification the code to compute the default state and round
// constants is smaller than the output. More importantly, this serves as a
// good educational aide for anyone wondering where the magic numbers come
// from. No magic numbers FTW!
function getFractionalBits(n: number) {
return ((n - (n | 0)) * pow(2, 32)) | 0;
}
let n = 2;
let nPrime = 0;
while (nPrime < 64) {
// isPrime() was in-lined from its original function form to save
// a few bytes
let isPrime = true;
// Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
// var sqrtN = pow(n, 1 / 2);
// So technically to determine if a number is prime you only need to
// check numbers up to the square root. However this function only runs
// once and we're only computing the first 64 primes (up to 311), so on
// any modern CPU this whole function runs in a couple milliseconds.
// By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
// scaling performance cost
for (let factor = 2; factor <= n / 2; factor++) {
if (n % factor === 0) {
isPrime = false;
}
}
if (isPrime) {
if (nPrime < 8) {
DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
}
ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));
nPrime++;
}
n++;
}
// For cross-platform support we need to ensure that all 32-bit words are
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
// if our system is LittleEndian (which is about 99% of CPUs)
const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];
function convertEndian(word: number) {
if (LittleEndian) {
return (
// byte 1 -> byte 4
(word >>> 24) |
// byte 2 -> byte 3
(((word >>> 16) & 0xff) << 8) |
// byte 3 -> byte 2
((word & 0xff00) << 8) |
// byte 4 -> byte 1
(word << 24)
);
} else {
return word;
}
}
function rightRotate(word: number, bits: number) {
return (word >>> bits) | (word << (32 - bits));
}
function sha256(data: Uint8Array) {
// Copy default state
const STATE = DEFAULT_STATE.slice();
// Caching this reduces occurrences of ".length" in minified JavaScript
// 3 more byte savings! :D
const legth = data.length;
// Pad data
const bitLength = legth * 8;
const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;
// "bytes" and "words" are stored BigEndian
const bytes = new uint8Array(newBitLength / 8);
const words = new uint32Array(bytes.buffer);
bytes.set(data, 0);
// Append a 1
bytes[legth] = 0b10000000;
// Store length in BigEndian
words[words.length - 1] = convertEndian(bitLength);
// Loop iterator (avoid two instances of "var") -- saves 2 bytes
let round;
// Process blocks (512 bits / 64 bytes / 16 words at a time)
for (let block = 0; block < newBitLength / 32; block += 16) {
const workingState = STATE.slice();
// Rounds
for (round = 0; round < 64; round++) {
let MRound;
// Expand message
if (round < 16) {
// Convert to platform Endianness for later math
MRound = convertEndian(words[block + round]);
} else {
const gamma0x = M[round - 15];
const gamma1x = M[round - 2];
MRound =
M[round - 7] +
M[round - 16] +
(rightRotate(gamma0x, 7) ^
rightRotate(gamma0x, 18) ^
(gamma0x >>> 3)) +
(rightRotate(gamma1x, 17) ^
rightRotate(gamma1x, 19) ^
(gamma1x >>> 10));
}
// M array matches platform endianness
M[round] = MRound |= 0;
// Computation
const t1 =
(rightRotate(workingState[4], 6) ^
rightRotate(workingState[4], 11) ^
rightRotate(workingState[4], 25)) +
((workingState[4] & workingState[5]) ^
(~workingState[4] & workingState[6])) +
workingState[7] +
MRound +
ROUND_CONSTANTS[round];
const t2 =
(rightRotate(workingState[0], 2) ^
rightRotate(workingState[0], 13) ^
rightRotate(workingState[0], 22)) +
((workingState[0] & workingState[1]) ^
(workingState[2] & (workingState[0] ^ workingState[1])));
for (let i = 7; i > 0; i--) {
workingState[i] = workingState[i - 1];
}
workingState[0] = (t1 + t2) | 0;
workingState[4] = (workingState[4] + t1) | 0;
}
// Update state
for (round = 0; round < 8; round++) {
STATE[round] = (STATE[round] + workingState[round]) | 0;
}
}
// Finally the state needs to be converted to BigEndian for output
// And we want to return a Uint8Array, not a Uint32Array
return new uint8Array(
new uint32Array(
STATE.map(function (val) {
return convertEndian(val);
}),
).buffer,
);
}
function hmac(key: Uint8Array, data: ArrayLike<number>) {
if (key.length > 64) key = sha256(key);
if (key.length < 64) {
const tmp = new Uint8Array(64);
tmp.set(key, 0);
key = tmp;
}
// Generate inner and outer keys
const innerKey = new Uint8Array(64);
const outerKey = new Uint8Array(64);
for (let i = 0; i < 64; i++) {
innerKey[i] = 0x36 ^ key[i];
outerKey[i] = 0x5c ^ key[i];
}
// Append the innerKey
const msg = new Uint8Array(data.length + 64);
msg.set(innerKey, 0);
msg.set(data, 64);
// Has the previous message and append the outerKey
const result = new Uint8Array(64 + 32);
result.set(outerKey, 0);
result.set(sha256(msg), 64);
// Hash the previous message
return sha256(result);
}
// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
const encoder = new TextEncoder();
export function sign(
inputKey: string | Uint8Array,
inputData: string | Uint8Array,
) {
const key =
typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
const data =
typeof inputData === "string" ? encoder.encode(inputData) : inputData;
return hmac(key, data);
}
export function hex(bin: Uint8Array) {
return bin.reduce((acc, val) => {
const hexVal = "00" + val.toString(16);
return acc + hexVal.substring(hexVal.length - 2);
}, "");
}
export function hash(str: string) {
return hex(sha256(encoder.encode(str)));
}
export function hashWithSecret(str: string, secret: string) {
return hex(sign(secret, str)).toString();
}

View File

@@ -1,12 +1,53 @@
import { DEFAULT_MODELS } from "../constant";
import { LLMModel } from "../client/api";
const CustomSeq = {
val: -1000, //To ensure the custom model located at front, start from -1000, refer to constant.ts
cache: new Map<string, number>(),
next: (id: string) => {
if (CustomSeq.cache.has(id)) {
return CustomSeq.cache.get(id) as number;
} else {
let seq = CustomSeq.val++;
CustomSeq.cache.set(id, seq);
return seq;
}
},
};
const customProvider = (providerName: string) => ({
id: providerName.toLowerCase(),
providerName: providerName,
providerType: "custom",
sorted: CustomSeq.next(providerName),
});
/**
* Sorts an array of models based on specified rules.
*
* First, sorted by provider; if the same, sorted by model
*/
const sortModelTable = (models: ReturnType<typeof collectModels>) =>
models.sort((a, b) => {
if (a.provider && b.provider) {
let cmp = a.provider.sorted - b.provider.sorted;
return cmp === 0 ? a.sorted - b.sorted : cmp;
} else {
return a.sorted - b.sorted;
}
});
/**
* get model name and provider from a formatted string,
* e.g. `gpt-4@OpenAi` or `claude-3-5-sonnet@20240620@Google`
* @param modelWithProvider model name with provider separated by last `@` char,
* @returns [model, provider] tuple, if no `@` char found, provider is undefined
*/
export function getModelProvider(modelWithProvider: string): [string, string?] {
const [model, provider] = modelWithProvider.split(/@(?!.*@)/);
return [model, provider];
}
export function collectModelTable(
models: readonly LLMModel[],
customModels: string,
@@ -17,6 +58,7 @@ export function collectModelTable(
available: boolean;
name: string;
displayName: string;
sorted: number;
provider?: LLMModel["provider"]; // Marked as optional
isDefault?: boolean;
}
@@ -48,10 +90,10 @@ export function collectModelTable(
);
} else {
// 1. find model by name, and set available value
const [customModelName, customProviderName] = name.split("@");
const [customModelName, customProviderName] = getModelProvider(name);
let count = 0;
for (const fullName in modelTable) {
const [modelName, providerName] = fullName.split("@");
const [modelName, providerName] = getModelProvider(fullName);
if (
customModelName == modelName &&
(customProviderName === undefined ||
@@ -71,7 +113,7 @@ export function collectModelTable(
}
// 2. if model not exists, create new model with available value
if (count === 0) {
let [customModelName, customProviderName] = name.split("@");
let [customModelName, customProviderName] = getModelProvider(name);
const provider = customProvider(
customProviderName || customModelName,
);
@@ -84,6 +126,7 @@ export function collectModelTable(
displayName: displayName || customModelName,
available,
provider, // Use optional chaining
sorted: CustomSeq.next(`${customModelName}@${provider?.id}`),
};
}
}
@@ -99,13 +142,16 @@ export function collectModelTableWithDefaultModel(
) {
let modelTable = collectModelTable(models, customModels);
if (defaultModel && defaultModel !== "") {
if (defaultModel.includes('@')) {
if (defaultModel.includes("@")) {
if (defaultModel in modelTable) {
modelTable[defaultModel].isDefault = true;
}
} else {
for (const key of Object.keys(modelTable)) {
if (modelTable[key].available && key.split('@').shift() == defaultModel) {
if (
modelTable[key].available &&
getModelProvider(key)[0] == defaultModel
) {
modelTable[key].isDefault = true;
break;
}
@@ -123,7 +169,9 @@ export function collectModels(
customModels: string,
) {
const modelTable = collectModelTable(models, customModels);
const allModels = Object.values(modelTable);
let allModels = Object.values(modelTable);
allModels = sortModelTable(allModels);
return allModels;
}
@@ -138,7 +186,10 @@ export function collectModelsWithDefaultModel(
customModels,
defaultModel,
);
const allModels = Object.values(modelTable);
let allModels = Object.values(modelTable);
allModels = sortModelTable(allModels);
return allModels;
}

108
app/utils/stream.ts Normal file
View File

@@ -0,0 +1,108 @@
// using tauri command to send request
// see src-tauri/src/stream.rs, and src-tauri/src/main.rs
// 1. invoke('stream_fetch', {url, method, headers, body}), get response with headers.
// 2. listen event: `stream-response` multi times to get body
type ResponseEvent = {
id: number;
payload: {
request_id: number;
status?: number;
chunk?: number[];
};
};
type StreamResponse = {
request_id: number;
status: number;
status_text: string;
headers: Record<string, string>;
};
export function fetch(url: string, options?: RequestInit): Promise<Response> {
if (window.__TAURI__) {
const {
signal,
method = "GET",
headers: _headers = {},
body = [],
} = options || {};
let unlisten: Function | undefined;
let setRequestId: Function | undefined;
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
const ts = new TransformStream();
const writer = ts.writable.getWriter();
let closed = false;
const close = () => {
if (closed) return;
closed = true;
unlisten && unlisten();
writer.ready.then(() => {
writer.close().catch((e) => console.error(e));
});
};
if (signal) {
signal.addEventListener("abort", () => close());
}
// @ts-ignore 2. listen response multi times, and write to Response.body
window.__TAURI__.event
.listen("stream-response", (e: ResponseEvent) =>
requestIdPromise.then((request_id) => {
const { request_id: rid, chunk, status } = e?.payload || {};
if (request_id != rid) {
return;
}
if (chunk) {
writer.ready.then(() => {
writer.write(new Uint8Array(chunk));
});
} else if (status === 0) {
// end of body
close();
}
}),
)
.then((u: Function) => (unlisten = u));
const headers: Record<string, string> = {
Accept: "application/json, text/plain, */*",
"Accept-Language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7",
"User-Agent": navigator.userAgent,
};
for (const item of new Headers(_headers || {})) {
headers[item[0]] = item[1];
}
return window.__TAURI__
.invoke("stream_fetch", {
method: method.toUpperCase(),
url,
headers,
// TODO FormData
body:
typeof body === "string"
? Array.from(new TextEncoder().encode(body))
: [],
})
.then((res: StreamResponse) => {
const { request_id, status, status_text: statusText, headers } = res;
setRequestId?.(request_id);
const response = new Response(ts.readable, {
status,
statusText,
headers,
});
if (status >= 300) {
setTimeout(close, 100);
}
return response;
})
.catch((e) => {
console.error("stream error", e);
// throw e;
return new Response("", { status: 599 });
});
}
return window.fetch(url, options);
}

102
app/utils/tencent.ts Normal file
View File

@@ -0,0 +1,102 @@
import { sign, hash as getHash, hex } from "./hmac";
// 使用 SHA-256 和 secret 进行 HMAC 加密
function sha256(message: any, secret: any, encoding?: string) {
const result = sign(secret, message);
return encoding == "hex" ? hex(result).toString() : result;
}
function getDate(timestamp: number) {
const date = new Date(timestamp * 1000);
const year = date.getUTCFullYear();
const month = ("0" + (date.getUTCMonth() + 1)).slice(-2);
const day = ("0" + date.getUTCDate()).slice(-2);
return `${year}-${month}-${day}`;
}
export async function getHeader(
payload: any,
SECRET_ID: string,
SECRET_KEY: string,
) {
// https://cloud.tencent.com/document/api/1729/105701
const endpoint = "hunyuan.tencentcloudapi.com";
const service = "hunyuan";
const region = ""; // optional
const action = "ChatCompletions";
const version = "2023-09-01";
const timestamp = Math.floor(Date.now() / 1000);
//时间处理, 获取世界时间日期
const date = getDate(timestamp);
// ************* 步骤 1拼接规范请求串 *************
const hashedRequestPayload = getHash(payload);
const httpRequestMethod = "POST";
const contentType = "application/json";
const canonicalUri = "/";
const canonicalQueryString = "";
const canonicalHeaders =
`content-type:${contentType}\n` +
"host:" +
endpoint +
"\n" +
"x-tc-action:" +
action.toLowerCase() +
"\n";
const signedHeaders = "content-type;host;x-tc-action";
const canonicalRequest = [
httpRequestMethod,
canonicalUri,
canonicalQueryString,
canonicalHeaders,
signedHeaders,
hashedRequestPayload,
].join("\n");
// ************* 步骤 2拼接待签名字符串 *************
const algorithm = "TC3-HMAC-SHA256";
const hashedCanonicalRequest = getHash(canonicalRequest);
const credentialScope = date + "/" + service + "/" + "tc3_request";
const stringToSign =
algorithm +
"\n" +
timestamp +
"\n" +
credentialScope +
"\n" +
hashedCanonicalRequest;
// ************* 步骤 3计算签名 *************
const kDate = sha256(date, "TC3" + SECRET_KEY);
const kService = sha256(service, kDate);
const kSigning = sha256("tc3_request", kService);
const signature = sha256(stringToSign, kSigning, "hex");
// ************* 步骤 4拼接 Authorization *************
const authorization =
algorithm +
" " +
"Credential=" +
SECRET_ID +
"/" +
credentialScope +
", " +
"SignedHeaders=" +
signedHeaders +
", " +
"Signature=" +
signature;
return {
Authorization: authorization,
"Content-Type": contentType,
Host: endpoint,
"X-TC-Action": action,
"X-TC-Timestamp": timestamp.toString(),
"X-TC-Version": version,
"X-TC-Region": region,
};
}