merge main

This commit is contained in:
lloydzhou 2024-08-01 13:18:33 +08:00
commit fd9e94e078
8 changed files with 719 additions and 71 deletions

View File

@ -0,0 +1,309 @@
import { getServerSideConfig } from "@/app/config/server";
import {
TENCENT_BASE_URL,
ApiPath,
ModelProvider,
ServiceProvider,
Tencent,
} from "@/app/constant";
import { prettyObject } from "@/app/utils/format";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/app/api/auth";
import { isModelAvailableInServer } from "@/app/utils/model";
import CryptoJS from "crypto-js";
import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
import isObject from "lodash-es/isObject";
const serverConfig = getServerSideConfig();
async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Tencent Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
const authResult = auth(req, ModelProvider.Hunyuan);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const response = await request(req);
return response;
} catch (e) {
console.error("[Tencent] ", e);
return NextResponse.json(prettyObject(e));
}
}
export const GET = handle;
export const POST = handle;
export const runtime = "edge";
export const preferredRegion = [
"arn1",
"bom1",
"cdg1",
"cle1",
"cpt1",
"dub1",
"fra1",
"gru1",
"hnd1",
"iad1",
"icn1",
"kix1",
"lhr1",
"pdx1",
"sfo1",
"sin1",
"syd1",
];
async function request(req: NextRequest) {
const controller = new AbortController();
// tencent just use base url or just remove the path
let path = `${req.nextUrl.pathname}`.replaceAll(
ApiPath.Tencent + "/" + Tencent.ChatPath,
"",
);
let baseUrl = serverConfig.tencentUrl || TENCENT_BASE_URL;
if (!baseUrl.startsWith("http")) {
baseUrl = `https://${baseUrl}`;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, -1);
}
console.log("[Proxy] ", path);
console.log("[Base Url]", baseUrl);
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
const fetchUrl = `${baseUrl}${path}`;
let body = null;
if (req.body) {
const bodyText = await req.text();
console.log(
"Dogtiti ~ request ~ capitalizeKeys(JSON.parse(bodyText):",
capitalizeKeys(JSON.parse(bodyText)),
);
body = JSON.stringify(capitalizeKeys(JSON.parse(bodyText)));
}
const fetchOptions: RequestInit = {
headers: {
...getHeader(body),
},
method: req.method,
body: '{"Model":"hunyuan-pro","Messages":[{"Role":"user","Content":"你好"}]}', // FIXME
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
// #1815 try to refuse some request to some models
if (serverConfig.customModels && req.body) {
try {
const clonedBody = await req.text();
fetchOptions.body = clonedBody;
const jsonBody = JSON.parse(clonedBody) as { model?: string };
// not undefined and is false
if (
isModelAvailableInServer(
serverConfig.customModels,
jsonBody?.model as string,
ServiceProvider.Tencent as string,
)
) {
return NextResponse.json(
{
error: true,
message: `you are not allowed to use ${jsonBody?.model} model`,
},
{
status: 403,
},
);
}
} catch (e) {
console.error(`[Tencent] filter`, e);
}
}
console.log("[Tencent request]", fetchOptions.headers, req.method);
try {
const res = await fetch(fetchUrl, fetchOptions);
console.log("[Tencent response]", res.status, " ", res.headers, res.url);
// to prevent browser prompt for credentials
const newHeaders = new Headers(res.headers);
newHeaders.delete("www-authenticate");
// to disable nginx buffering
newHeaders.set("X-Accel-Buffering", "no");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}
function capitalizeKeys(obj: any): any {
if (isArray(obj)) {
return obj.map(capitalizeKeys);
} else if (isObject(obj)) {
return mapValues(
mapKeys(
obj,
(value: any, key: string) => key.charAt(0).toUpperCase() + key.slice(1),
),
capitalizeKeys,
);
} else {
return obj;
}
}
// 使用 SHA-256 和 secret 进行 HMAC 加密
function sha256(message: any, secret = "", encoding = "hex") {
const hmac = CryptoJS.HmacSHA256(message, secret);
if (encoding === "hex") {
return hmac.toString(CryptoJS.enc.Hex);
} else if (encoding === "base64") {
return hmac.toString(CryptoJS.enc.Base64);
} else {
return hmac.toString();
}
}
// 使用 SHA-256 进行哈希
function getHash(message: any, encoding = "hex") {
const hash = CryptoJS.SHA256(message);
if (encoding === "hex") {
return hash.toString(CryptoJS.enc.Hex);
} else if (encoding === "base64") {
return hash.toString(CryptoJS.enc.Base64);
} else {
return hash.toString();
}
}
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}`;
}
function getHeader(payload: any) {
// https://cloud.tencent.com/document/api/1729/105701
// 密钥参数
const SECRET_ID = serverConfig.tencentSecretId;
const SECRET_KEY = serverConfig.tencentSecretKey;
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 canonicalUri = "/";
const canonicalQueryString = "";
const canonicalHeaders =
"content-type:application/json; charset=utf-8\n" +
"host:" +
endpoint +
"\n" +
"x-tc-action:" +
action.toLowerCase() +
"\n";
const signedHeaders = "content-type;host;x-tc-action";
const canonicalRequest =
httpRequestMethod +
"\n" +
canonicalUri +
"\n" +
canonicalQueryString +
"\n" +
canonicalHeaders +
"\n" +
signedHeaders +
"\n" +
hashedRequestPayload;
// ************* 步骤 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": "application/json; charset=utf-8",
Host: endpoint,
"X-TC-Action": action,
"X-TC-Timestamp": timestamp.toString(),
"X-TC-Version": version,
"X-TC-Region": region,
};
}

View File

@ -12,6 +12,7 @@ import { ClaudeApi } from "./platforms/anthropic";
import { ErnieApi } from "./platforms/baidu"; import { ErnieApi } from "./platforms/baidu";
import { DoubaoApi } from "./platforms/bytedance"; import { DoubaoApi } from "./platforms/bytedance";
import { QwenApi } from "./platforms/alibaba"; import { QwenApi } from "./platforms/alibaba";
import { HunyuanApi } from "./platforms/tencent";
export const ROLES = ["system", "user", "assistant"] as const; export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number]; export type MessageRole = (typeof ROLES)[number];
@ -116,6 +117,8 @@ export class ClientApi {
break; break;
case ModelProvider.Qwen: case ModelProvider.Qwen:
this.llm = new QwenApi(); this.llm = new QwenApi();
case ModelProvider.Hunyuan:
this.llm = new HunyuanApi();
break; break;
default: default:
this.llm = new ChatGPTApi(); this.llm = new ChatGPTApi();
@ -267,6 +270,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
return new ClientApi(ModelProvider.Doubao); return new ClientApi(ModelProvider.Doubao);
case ServiceProvider.Alibaba: case ServiceProvider.Alibaba:
return new ClientApi(ModelProvider.Qwen); return new ClientApi(ModelProvider.Qwen);
case ServiceProvider.Tencent:
return new ClientApi(ModelProvider.Hunyuan);
default: default:
return new ClientApi(ModelProvider.GPT); return new ClientApi(ModelProvider.GPT);
} }

View File

@ -0,0 +1,259 @@
"use client";
import {
ApiPath,
DEFAULT_API_HOST,
REQUEST_TIMEOUT_MS,
Tencent,
} from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
import {
ChatOptions,
getHeaders,
LLMApi,
LLMModel,
MultimodalContent,
} from "../api";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
export interface OpenAIListModelResponse {
object: string;
data: Array<{
id: string;
object: string;
root: string;
}>;
}
interface RequestPayload {
messages: {
role: "system" | "user" | "assistant";
content: string | MultimodalContent[];
}[];
stream?: boolean;
model: string;
temperature: number;
presence_penalty: number;
frequency_penalty: number;
top_p: number;
max_tokens?: number;
}
export class HunyuanApi implements LLMApi {
path(path: string): string {
const accessStore = useAccessStore.getState();
let baseUrl = "";
if (accessStore.useCustomConfig) {
baseUrl = accessStore.tencentUrl;
}
if (baseUrl.length === 0) {
const isApp = !!getClientConfig()?.isApp;
baseUrl = isApp
? DEFAULT_API_HOST + "/api/proxy/bytedance"
: ApiPath.Tencent;
}
if (baseUrl.endsWith("/")) {
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
}
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.Tencent)) {
baseUrl = "https://" + baseUrl;
}
console.log("[Proxy Endpoint] ", baseUrl);
return [baseUrl, path].join("/");
}
extractMessage(res: any) {
return res.choices?.at(0)?.message?.content ?? "";
}
async chat(options: ChatOptions) {
const visionModel = isVisionModel(options.config.model);
const messages = options.messages.map((v) => ({
role: v.role,
content: visionModel ? v.content : getMessageTextContent(v),
}));
const modelConfig = {
...useAppConfig.getState().modelConfig,
...useChatStore.getState().currentSession().mask.modelConfig,
...{
model: options.config.model,
},
};
const requestPayload: RequestPayload = {
messages,
stream: options.config.stream,
model: modelConfig.model,
temperature: modelConfig.temperature,
presence_penalty: modelConfig.presence_penalty,
frequency_penalty: modelConfig.frequency_penalty,
top_p: modelConfig.top_p,
};
console.log("[Request] Tencent payload: ", requestPayload);
const shouldStream = !!options.config.stream;
const controller = new AbortController();
options.onController?.(controller);
try {
const chatPath = this.path(Tencent.ChatPath);
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
if (shouldStream) {
let responseText = "";
let remainText = "";
let finished = false;
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
}
return;
}
if (remainText.length > 0) {
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
const fetchText = remainText.slice(0, fetchCount);
responseText += fetchText;
remainText = remainText.slice(fetchCount);
options.onUpdate?.(responseText, fetchText);
}
requestAnimationFrame(animateResponseText);
}
// start animaion
animateResponseText();
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[Tencent] request response content type: ",
contentType,
);
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
if (msg.data === "[DONE]" || finished) {
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
if (delta) {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId);
const resJson = await res.json();
const message = this.extractMessage(resJson);
options.onFinish(message);
}
} catch (e) {
console.log("[Request] failed to make a chat request", e);
options.onError?.(e as Error);
}
}
async usage() {
return {
used: 0,
total: 0,
};
}
async models(): Promise<LLMModel[]> {
return [];
}
}

View File

@ -57,6 +57,11 @@ declare global {
ALIBABA_URL?: string; ALIBABA_URL?: string;
ALIBABA_API_KEY?: string; ALIBABA_API_KEY?: string;
// tencent only
TENCENT_URL?: string;
TENCENT_SECRET_KEY?: string;
TENCENT_SECRET_ID?: string;
// custom template for preprocessing user input // custom template for preprocessing user input
DEFAULT_INPUT_TEMPLATE?: string; DEFAULT_INPUT_TEMPLATE?: string;
} }
@ -116,6 +121,7 @@ export const getServerSideConfig = () => {
const isAzure = !!process.env.AZURE_URL; const isAzure = !!process.env.AZURE_URL;
const isGoogle = !!process.env.GOOGLE_API_KEY; const isGoogle = !!process.env.GOOGLE_API_KEY;
const isAnthropic = !!process.env.ANTHROPIC_API_KEY; const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
const isTencent = !!process.env.TENCENT_API_KEY;
const isBaidu = !!process.env.BAIDU_API_KEY; const isBaidu = !!process.env.BAIDU_API_KEY;
const isBytedance = !!process.env.BYTEDANCE_API_KEY; const isBytedance = !!process.env.BYTEDANCE_API_KEY;
@ -168,6 +174,11 @@ export const getServerSideConfig = () => {
alibabaUrl: process.env.ALIBABA_URL, alibabaUrl: process.env.ALIBABA_URL,
alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY),
isTencent,
tencentUrl: process.env.TENCENT_URL,
tencentSecretKey: getApiKey(process.env.TENCENT_SECRET_KEY),
tencentSecretId: process.env.TENCENT_SECRET_ID,
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID, cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID, cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY), cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),

View File

@ -23,6 +23,8 @@ export const BYTEDANCE_BASE_URL = "https://ark.cn-beijing.volces.com";
export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/"; export const ALIBABA_BASE_URL = "https://dashscope.aliyuncs.com/api/";
export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
export const CACHE_URL_PREFIX = "/api/cache"; export const CACHE_URL_PREFIX = "/api/cache";
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`; export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
@ -47,6 +49,7 @@ export enum ApiPath {
Baidu = "/api/baidu", Baidu = "/api/baidu",
ByteDance = "/api/bytedance", ByteDance = "/api/bytedance",
Alibaba = "/api/alibaba", Alibaba = "/api/alibaba",
Tencent = "/api/tencent",
Stability = "/api/stability", Stability = "/api/stability",
Artifacts = "/api/artifacts", Artifacts = "/api/artifacts",
} }
@ -100,6 +103,7 @@ export enum ServiceProvider {
Baidu = "Baidu", Baidu = "Baidu",
ByteDance = "ByteDance", ByteDance = "ByteDance",
Alibaba = "Alibaba", Alibaba = "Alibaba",
Tencent = "Tencent",
Stability = "Stability", Stability = "Stability",
} }
@ -120,6 +124,7 @@ export enum ModelProvider {
Ernie = "Ernie", Ernie = "Ernie",
Doubao = "Doubao", Doubao = "Doubao",
Qwen = "Qwen", Qwen = "Qwen",
Hunyuan = "Hunyuan",
} }
export const Stability = { export const Stability = {
@ -183,6 +188,10 @@ export const Alibaba = {
ChatPath: "v1/services/aigc/text-generation/generation", ChatPath: "v1/services/aigc/text-generation/generation",
}; };
export const Tencent = {
ChatPath: "chat/completions",
};
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
// export const DEFAULT_SYSTEM_TEMPLATE = ` // export const DEFAULT_SYSTEM_TEMPLATE = `
// You are ChatGPT, a large language model trained by {{ServiceProvider}}. // You are ChatGPT, a large language model trained by {{ServiceProvider}}.
@ -289,6 +298,16 @@ const alibabaModes = [
"qwen-max-longcontext", "qwen-max-longcontext",
]; ];
const tencentModels = [
"hunyuan-pro",
"hunyuan-standard",
"hunyuan-lite",
"hunyuan-role",
"hunyuan-functioncall",
"hunyuan-code",
"hunyuan-vision",
];
export const DEFAULT_MODELS = [ export const DEFAULT_MODELS = [
...openaiModels.map((name) => ({ ...openaiModels.map((name) => ({
name, name,
@ -353,6 +372,15 @@ export const DEFAULT_MODELS = [
providerType: "alibaba", providerType: "alibaba",
}, },
})), })),
...tencentModels.map((name) => ({
name,
available: true,
provider: {
id: "tencent",
providerName: "Tencent",
providerType: "tencent",
},
})),
] as const; ] as const;
export const CHAT_PAGE_SIZE = 15; export const CHAT_PAGE_SIZE = 15;

View File

@ -86,6 +86,11 @@ const DEFAULT_ACCESS_STATE = {
stabilityUrl: DEFAULT_STABILITY_URL, stabilityUrl: DEFAULT_STABILITY_URL,
stabilityApiKey: "", stabilityApiKey: "",
// tencent
tencentUrl: "",
tencentSecretKey: "",
tencentSecretId: "",
// server config // server config
needCode: true, needCode: true,
hideUserApiKey: false, hideUserApiKey: false,
@ -134,6 +139,10 @@ export const useAccessStore = createPersistStore(
return ensure(get(), ["alibabaApiKey"]); return ensure(get(), ["alibabaApiKey"]);
}, },
isValidTencent() {
return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
},
isAuthorized() { isAuthorized() {
this.fetch(); this.fetch();
@ -146,6 +155,7 @@ export const useAccessStore = createPersistStore(
this.isValidBaidu() || this.isValidBaidu() ||
this.isValidByteDance() || this.isValidByteDance() ||
this.isValidAlibaba() || this.isValidAlibaba() ||
this.isValidTencent ||
!this.enabledAccessControl() || !this.enabledAccessControl() ||
(this.enabledAccessControl() && ensure(get(), ["accessCode"])) (this.enabledAccessControl() && ensure(get(), ["accessCode"]))
); );

View File

@ -1,73 +1,77 @@
{ {
"name": "nextchat", "name": "nextchat",
"private": false, "private": false,
"license": "mit", "license": "mit",
"scripts": { "scripts": {
"mask": "npx tsx app/masks/build.ts", "mask": "npx tsx app/masks/build.ts",
"mask:watch": "npx watch 'yarn mask' app/masks", "mask:watch": "npx watch 'yarn mask' app/masks",
"dev": "yarn run mask:watch & next dev", "dev": "yarn run mask:watch & next dev",
"build": "yarn mask && cross-env BUILD_MODE=standalone next build", "build": "yarn mask && cross-env BUILD_MODE=standalone next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build", "export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
"export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev", "export:dev": "yarn mask:watch & cross-env BUILD_MODE=export BUILD_APP=1 next dev",
"app:dev": "yarn mask:watch & yarn tauri dev", "app:dev": "yarn mask:watch & yarn tauri dev",
"app:build": "yarn mask && yarn tauri build", "app:build": "yarn mask && yarn tauri build",
"prompts": "node ./scripts/fetch-prompts.mjs", "prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install", "prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev"
}, },
"dependencies": { "dependencies": {
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
"@hello-pangea/dnd": "^16.5.0", "@hello-pangea/dnd": "^16.5.0",
"@next/third-parties": "^14.1.0", "@next/third-parties": "^14.1.0",
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2", "@vercel/speed-insights": "^1.0.2",
"emoji-picker-react": "^4.9.2", "crypto-js": "^4.2.0",
"fuse.js": "^7.0.0", "emoji-picker-react": "^4.9.2",
"heic2any": "^0.0.4", "fuse.js": "^7.0.0",
"html-to-image": "^1.11.11", "heic2any": "^0.0.4",
"mermaid": "^10.6.1", "html-to-image": "^1.11.11",
"nanoid": "^5.0.3", "lodash-es": "^4.17.21",
"next": "^14.1.1", "mermaid": "^10.6.1",
"node-fetch": "^3.3.1", "nanoid": "^5.0.3",
"react": "^18.2.0", "next": "^14.1.1",
"react-dom": "^18.2.0", "node-fetch": "^3.3.1",
"react-markdown": "^8.0.7", "react": "^18.2.0",
"react-router-dom": "^6.15.0", "react-dom": "^18.2.0",
"rehype-highlight": "^6.0.0", "react-markdown": "^8.0.7",
"rehype-katex": "^6.0.3", "react-router-dom": "^6.15.0",
"remark-breaks": "^3.0.2", "rehype-highlight": "^6.0.0",
"remark-gfm": "^3.0.1", "rehype-katex": "^6.0.3",
"remark-math": "^5.1.1", "remark-breaks": "^3.0.2",
"sass": "^1.59.2", "remark-gfm": "^3.0.1",
"spark-md5": "^3.0.2", "remark-math": "^5.1.1",
"use-debounce": "^9.0.4", "sass": "^1.59.2",
"zustand": "^4.3.8" "spark-md5": "^3.0.2",
}, "use-debounce": "^9.0.4",
"devDependencies": { "zustand": "^4.3.8"
"@tauri-apps/cli": "1.5.11", },
"@types/node": "^20.11.30", "devDependencies": {
"@types/react": "^18.2.70", "@tauri-apps/cli": "1.5.11",
"@types/react-dom": "^18.2.7", "@types/crypto-js": "^4.2.2",
"@types/react-katex": "^3.0.0", "@types/lodash-es": "^4.17.12",
"@types/spark-md5": "^3.0.4", "@types/node": "^20.11.30",
"cross-env": "^7.0.3", "@types/react": "^18.2.70",
"eslint": "^8.49.0", "@types/react-dom": "^18.2.7",
"eslint-config-next": "13.4.19", "@types/react-katex": "^3.0.0",
"eslint-config-prettier": "^8.8.0", "@types/spark-md5": "^3.0.4",
"eslint-plugin-prettier": "^5.1.3", "cross-env": "^7.0.3",
"husky": "^8.0.0", "eslint": "^8.49.0",
"lint-staged": "^13.2.2", "eslint-config-next": "13.4.19",
"prettier": "^3.0.2", "eslint-config-prettier": "^8.8.0",
"tsx": "^4.16.0", "eslint-plugin-prettier": "^5.1.3",
"typescript": "5.2.2", "husky": "^8.0.0",
"watch": "^1.0.2", "lint-staged": "^13.2.2",
"webpack": "^5.88.1" "prettier": "^3.0.2",
}, "tsx": "^4.16.0",
"resolutions": { "typescript": "5.2.2",
"lint-staged/yaml": "^2.2.2" "watch": "^1.0.2",
}, "webpack": "^5.88.1"
"packageManager": "yarn@1.22.19" },
"resolutions": {
"lint-staged/yaml": "^2.2.2"
},
"packageManager": "yarn@1.22.19"
} }

View File

@ -1617,6 +1617,11 @@
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@types/crypto-js@^4.2.2":
version "4.2.2"
resolved "https://registry.npmmirror.com/@types/crypto-js/-/crypto-js-4.2.2.tgz#771c4a768d94eb5922cc202a3009558204df0cea"
integrity sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==
"@types/d3-scale-chromatic@^3.0.0": "@types/d3-scale-chromatic@^3.0.0":
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954" resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#103124777e8cdec85b20b51fd3397c682ee1e954"
@ -1697,6 +1702,18 @@
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe" resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.14.0.tgz#b84c0afc3218069a5ad64fe2a95321881021b5fe"
integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA== integrity sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==
"@types/lodash-es@^4.17.12":
version "4.17.12"
resolved "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz#65f6d1e5f80539aa7cfbfc962de5def0cf4f341b"
integrity sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==
dependencies:
"@types/lodash" "*"
"@types/lodash@*":
version "4.17.6"
resolved "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.6.tgz#193ced6a40c8006cfc1ca3f4553444fb38f0e543"
integrity sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==
"@types/mdast@^3.0.0": "@types/mdast@^3.0.0":
version "3.0.11" version "3.0.11"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0"
@ -2447,6 +2464,11 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
crypto-js@^4.2.0:
version "4.2.0"
resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
css-box-model@^1.2.1: css-box-model@^1.2.1:
version "1.2.1" version "1.2.1"
resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" resolved "https://registry.npmmirror.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1"