diff --git a/app/api/[provider]/[...path]/route.ts b/app/api/[provider]/[...path]/route.ts index 8975bf971..182862289 100644 --- a/app/api/[provider]/[...path]/route.ts +++ b/app/api/[provider]/[...path]/route.ts @@ -20,7 +20,11 @@ async function handle( req: NextRequest, { params }: { params: { provider: string; path: string[] } }, ) { + // Handle OPTIONS request for CORS preflight + // params.provider = MODEL_PROVIDER; + const apiPath = `/api/${params.provider}`; + console.log(`[${params.provider} Route] params `, params); switch (apiPath) { case ApiPath.Azure: diff --git a/app/api/alibaba.ts b/app/api/alibaba.ts index 20f6caefa..99b53a82b 100644 --- a/app/api/alibaba.ts +++ b/app/api/alibaba.ts @@ -1,14 +1,8 @@ import { getServerSideConfig } from "@/app/config/server"; -import { - ALIBABA_BASE_URL, - ApiPath, - ModelProvider, - ServiceProvider, -} from "@/app/constant"; +import { ALIBABA_BASE_URL, ApiPath, ModelProvider } from "@/app/constant"; import { prettyObject } from "@/app/utils/format"; import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/app/api/auth"; -import { isModelNotavailableInServer } from "@/app/utils/model"; const serverConfig = getServerSideConfig(); @@ -83,28 +77,36 @@ async function request(req: NextRequest) { if (serverConfig.customModels && req.body) { try { const clonedBody = await req.text(); - fetchOptions.body = clonedBody; + let jsonBody: any = {}; - const jsonBody = JSON.parse(clonedBody) as { model?: string }; + try { + jsonBody = JSON.parse(clonedBody); + delete jsonBody.model; // Remove the model key + fetchOptions.body = JSON.stringify(jsonBody); + } catch (e) { + fetchOptions.body = clonedBody; // fallback if not JSON + } + + console.log("[Alibaba] request body", fetchOptions.body); // not undefined and is false - if ( - isModelNotavailableInServer( - serverConfig.customModels, - jsonBody?.model as string, - ServiceProvider.Alibaba as string, - ) - ) { - return NextResponse.json( - { - error: true, - message: `you are not allowed to use ${jsonBody?.model} model`, - }, - { - status: 403, - }, - ); - } + // if ( + // isModelNotavailableInServer( + // serverConfig.customModels, + // jsonBody?.model as string, + // ServiceProvider.Alibaba as string, + // ) + // ) { + // return NextResponse.json( + // { + // error: true, + // message: `you are not allowed to use ${jsonBody?.model} model`, + // }, + // { + // status: 403, + // }, + // ); + // } } catch (e) { console.error(`[Alibaba] filter`, e); } diff --git a/app/client/platforms/alibaba.ts b/app/client/platforms/alibaba.ts index 4875e5c02..f590c0b1d 100644 --- a/app/client/platforms/alibaba.ts +++ b/app/client/platforms/alibaba.ts @@ -18,7 +18,6 @@ import { LLMModel, SpeechOptions, MultimodalContent, - MultimodalContentForAlibaba, } from "../api"; import { getClientConfig } from "@/app/config/client"; import { @@ -156,11 +155,13 @@ export class QwenApi implements LLMApi { ); if (shouldStream) { + // Lấy danh sách các công cụ (tools) và hàm (funcs) từ plugin hiện tại của phiên chat const [tools, funcs] = usePluginStore .getState() .getAsTools( useChatStore.getState().currentSession().mask?.plugin || [], ); + // Gọi hàm streamWithThink để xử lý chat dạng stream (dòng sự kiện server-sent events) return streamWithThink( chatPath, requestPayload, @@ -168,44 +169,19 @@ export class QwenApi implements LLMApi { tools as any, funcs, controller, - // parseSSE + // Updated SSE parse callback for new JSON structure (text: string, runTools: ChatMessageTool[]) => { - // console.log("parseSSE", text, runTools); + // Parse the JSON response const json = JSON.parse(text); - const choices = json.output.choices as Array<{ - message: { - content: string | null | MultimodalContentForAlibaba[]; - tool_calls: ChatMessageTool[]; - reasoning_content: string | null; - }; - }>; - if (!choices?.length) return { isThinking: false, content: "" }; + // console.log("[Alibaba] SSE response", json); - const tool_calls = choices[0]?.message?.tool_calls; - if (tool_calls?.length > 0) { - const index = tool_calls[0]?.index; - const id = tool_calls[0]?.id; - const args = tool_calls[0]?.function?.arguments; - if (id) { - runTools.push({ - id, - type: tool_calls[0]?.type, - function: { - name: tool_calls[0]?.function?.name as string, - arguments: args, - }, - }); - } else { - // @ts-ignore - runTools[index]["function"]["arguments"] += args; - } - } + // Extract content from the new structure + const output = json.output; + const content = output?.text ?? ""; + const reasoning = output?.reasoning_content ?? ""; // If exists in your new structure - const reasoning = choices[0]?.message?.reasoning_content; - const content = choices[0]?.message?.content; - - // Skip if both content and reasoning_content are empty or null + // If both are empty, return default if ( (!reasoning || reasoning.length === 0) && (!content || content.length === 0) @@ -216,31 +192,34 @@ export class QwenApi implements LLMApi { }; } + // If reasoning_content exists, treat as "thinking" if (reasoning && reasoning.length > 0) { return { isThinking: true, content: reasoning, }; - } else if (content && content.length > 0) { + } + // Otherwise, return the main content + else if (content && content.length > 0) { return { isThinking: false, - content: Array.isArray(content) - ? content.map((item) => item.text).join(",") - : content, + content: content, }; } + // Fallback return { isThinking: false, content: "", }; }, - // processToolMessage, include tool_calls message and tool call results + // Hàm xử lý message liên quan đến tool_call và kết quả trả về từ tool_call ( requestPayload: RequestPayload, toolCallMessage: any, toolCallResult: any[], ) => { + // Thêm message gọi tool và kết quả trả về vào cuối mảng messages trong payload gửi lên API requestPayload?.input?.messages?.splice( requestPayload?.input?.messages?.length, 0, @@ -248,7 +227,7 @@ export class QwenApi implements LLMApi { ...toolCallResult, ); }, - options, + options, // Các tuỳ chọn khác cho hàm streamWithThink ); } else { const res = await fetch(chatPath, chatPayload); diff --git a/app/client/platforms/deepseek.ts b/app/client/platforms/deepseek.ts index 1b38b40cc..febaa2ec2 100644 --- a/app/client/platforms/deepseek.ts +++ b/app/client/platforms/deepseek.ts @@ -151,7 +151,8 @@ export class DeepSeekApi implements LLMApi { controller, // parseSSE (text: string, runTools: ChatMessageTool[]) => { - // console.log("parseSSE", text, runTools); + console.log("parseSSE", text, runTools); + const json = JSON.parse(text); const choices = json.choices as Array<{ delta: { diff --git a/app/client/platforms/siliconflow.ts b/app/client/platforms/siliconflow.ts index 34f0844c3..6caf46f11 100644 --- a/app/client/platforms/siliconflow.ts +++ b/app/client/platforms/siliconflow.ts @@ -153,81 +153,35 @@ export class SiliconflowApi implements LLMApi { tools as any, funcs, controller, - // parseSSE + // parseSSE mới cho SiliconFlow response (text: string, runTools: ChatMessageTool[]) => { - // console.log("parseSSE", text, runTools); + // Parse chuỗi JSON trả về thành đối tượng const json = JSON.parse(text); - const choices = json.choices as Array<{ - delta: { - content: string | null; - tool_calls: ChatMessageTool[]; - reasoning_content: string | null; - }; - }>; - const tool_calls = choices[0]?.delta?.tool_calls; - if (tool_calls?.length > 0) { - const index = tool_calls[0]?.index; - const id = tool_calls[0]?.id; - const args = tool_calls[0]?.function?.arguments; - if (id) { - runTools.push({ - id, - type: tool_calls[0]?.type, - function: { - name: tool_calls[0]?.function?.name as string, - arguments: args, - }, - }); - } else { - // @ts-ignore - runTools[index]["function"]["arguments"] += args; - } - } - const reasoning = choices[0]?.delta?.reasoning_content; - const content = choices[0]?.delta?.content; - // Skip if both content and reasoning_content are empty or null - if ( - (!reasoning || reasoning.length === 0) && - (!content || content.length === 0) - ) { + // Lấy nội dung trả lời từ output.text + const content = json?.output?.text ?? ""; + + // Nếu không có nội dung trả lời, trả về trạng thái không suy nghĩ và nội dung rỗng + if (!content || content.length === 0) { return { isThinking: false, content: "", }; } - if (reasoning && reasoning.length > 0) { - return { - isThinking: true, - content: reasoning, - }; - } else if (content && content.length > 0) { - return { - isThinking: false, - content: content, - }; - } - + // Trả về trạng thái không suy nghĩ và nội dung trả lời return { isThinking: false, - content: "", + content: content, }; }, - // processToolMessage, include tool_calls message and tool call results + // processToolMessage: SiliconFlow không có tool_call nên giữ nguyên hoặc để rỗng ( requestPayload: RequestPayload, toolCallMessage: any, toolCallResult: any[], ) => { - // @ts-ignore - requestPayload?.messages?.splice( - // @ts-ignore - requestPayload?.messages?.length, - 0, - toolCallMessage, - ...toolCallResult, - ); + // Không cần xử lý tool_call, có thể để trống hoặc giữ nguyên nếu muốn tương thích }, options, ); diff --git a/app/config/server.ts b/app/config/server.ts index 43d4ff833..53fd2436c 100644 --- a/app/config/server.ts +++ b/app/config/server.ts @@ -58,6 +58,7 @@ declare global { // alibaba only ALIBABA_URL?: string; ALIBABA_API_KEY?: string; + ALIBABA_APP_ID?: string; // alibaba app id, used for some models // tencent only TENCENT_URL?: string; @@ -210,6 +211,7 @@ export const getServerSideConfig = () => { isAlibaba, alibabaUrl: process.env.ALIBABA_URL, alibabaApiKey: getApiKey(process.env.ALIBABA_API_KEY), + alibabaAppId: process.env.ALIBABA_APP_ID, isTencent, tencentUrl: process.env.TENCENT_URL, diff --git a/app/constant.ts b/app/constant.ts index ffbdab26c..c71bfa0d7 100644 --- a/app/constant.ts +++ b/app/constant.ts @@ -21,7 +21,7 @@ export const BAIDU_OATUH_URL = `${BAIDU_BASE_URL}/oauth/2.0/token`; 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-intl.aliyuncs.com"; export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com"; @@ -39,6 +39,9 @@ export const SILICONFLOW_BASE_URL = "https://api.siliconflow.cn"; export const CACHE_URL_PREFIX = "/api/cache"; export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`; +export const ALIBABA_APP_ID = "95072bcf71bf4469a25c45c31e76f37a"; // default alibaba app id, used for some models +export const MODEL_PROVIDER = "alibaba"; + export enum Path { Home = "/", Chat = "/chat", @@ -222,10 +225,13 @@ export const ByteDance = { export const Alibaba = { ExampleEndpoint: ALIBABA_BASE_URL, ChatPath: (modelName: string) => { + const URL = `api/v1/apps/${ALIBABA_APP_ID}/completion`; + if (modelName.includes("vl") || modelName.includes("omni")) { return "v1/services/aigc/multimodal-generation/generation"; } - return `v1/services/aigc/text-generation/generation`; + // return `v1/services/aigc/text-generation/generation`; + return URL; }, }; @@ -681,20 +687,21 @@ const siliconflowModels = [ let seq = 1000; // 内置的模型序号生成器从1000开始 export const DEFAULT_MODELS = [ - ...openaiModels.map((name) => ({ + ...alibabaModes.map((name) => ({ name, - available: true, - sorted: seq++, // Global sequence sort(index) + available: true, // 默认可用 + sorted: seq++, provider: { - id: "openai", - providerName: "OpenAI", - providerType: "openai", - sorted: 1, // 这里是固定的,确保顺序与之前内置的版本一致 + id: "alibaba", + providerName: "Alibaba", + providerType: "alibaba", + sorted: 1, }, })), + ...openaiModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "azure", @@ -705,7 +712,7 @@ export const DEFAULT_MODELS = [ })), ...googleModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "google", @@ -716,7 +723,7 @@ export const DEFAULT_MODELS = [ })), ...anthropicModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "anthropic", @@ -727,7 +734,7 @@ export const DEFAULT_MODELS = [ })), ...baiduModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "baidu", @@ -738,7 +745,7 @@ export const DEFAULT_MODELS = [ })), ...bytedanceModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "bytedance", @@ -747,20 +754,22 @@ export const DEFAULT_MODELS = [ sorted: 6, }, })), - ...alibabaModes.map((name) => ({ + + ...openaiModels.map((name) => ({ name, - available: true, - sorted: seq++, + available: false, + sorted: seq++, // Global sequence sort(index) provider: { - id: "alibaba", - providerName: "Alibaba", - providerType: "alibaba", - sorted: 7, + id: "openai", + providerName: "OpenAI", + providerType: "openai", + sorted: 7, // 这里是固定的,确保顺序与之前内置的版本一致 }, })), + ...tencentModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "tencent", @@ -771,7 +780,7 @@ export const DEFAULT_MODELS = [ })), ...moonshotModes.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "moonshot", @@ -782,7 +791,7 @@ export const DEFAULT_MODELS = [ })), ...iflytekModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "iflytek", @@ -793,7 +802,7 @@ export const DEFAULT_MODELS = [ })), ...xAIModes.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "xai", @@ -804,7 +813,7 @@ export const DEFAULT_MODELS = [ })), ...chatglmModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "chatglm", @@ -815,7 +824,7 @@ export const DEFAULT_MODELS = [ })), ...deepseekModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "deepseek", @@ -826,7 +835,7 @@ export const DEFAULT_MODELS = [ })), ...siliconflowModels.map((name) => ({ name, - available: true, + available: false, sorted: seq++, provider: { id: "siliconflow", diff --git a/app/store/config.ts b/app/store/config.ts index 45e21b026..5f434d7e8 100644 --- a/app/store/config.ts +++ b/app/store/config.ts @@ -64,8 +64,8 @@ export const DEFAULT_CONFIG = { models: DEFAULT_MODELS as any as LLMModel[], modelConfig: { - model: "gpt-4o-mini" as ModelType, - providerName: "OpenAI" as ServiceProvider, + model: "qwen-turbo" as ModelType, + providerName: "Alibaba" as ServiceProvider, temperature: 0.5, top_p: 1, max_tokens: 4000, diff --git a/app/store/mask.ts b/app/store/mask.ts index 850abeef6..03da65626 100644 --- a/app/store/mask.ts +++ b/app/store/mask.ts @@ -65,32 +65,40 @@ export const useMaskStore = createPersistStore( return masks[id]; }, + // Hàm cập nhật một mask dựa trên id và một hàm updater updateMask(id: string, updater: (mask: Mask) => void) { - const masks = get().masks; - const mask = masks[id]; - if (!mask) return; - const updateMask = { ...mask }; - updater(updateMask); - masks[id] = updateMask; - set(() => ({ masks })); - get().markUpdate(); + const masks = get().masks; // Lấy danh sách các mask hiện tại + const mask = masks[id]; // Lấy mask theo id + if (!mask) return; // Nếu không tìm thấy thì thoát + const updateMask = { ...mask }; // Tạo bản sao mask để cập nhật + updater(updateMask); // Gọi hàm updater để chỉnh sửa mask + masks[id] = updateMask; // Gán lại mask đã cập nhật vào danh sách + set(() => ({ masks })); // Cập nhật lại state + get().markUpdate(); // Đánh dấu đã cập nhật }, + // Hàm xóa một mask theo id delete(id: string) { - const masks = get().masks; - delete masks[id]; - set(() => ({ masks })); - get().markUpdate(); + const masks = get().masks; // Lấy danh sách các mask hiện tại + delete masks[id]; // Xóa mask theo id + set(() => ({ masks })); // Cập nhật lại state + get().markUpdate(); // Đánh dấu đã cập nhật }, + // Hàm lấy một mask theo id (nếu không truyền id sẽ lấy id mặc định) get(id?: string) { return get().masks[id ?? 1145141919810]; }, + + // Hàm lấy tất cả các mask (bao gồm cả mask người dùng và mask mặc định) getAll() { + // Lấy danh sách mask của người dùng, sắp xếp theo thời gian tạo mới nhất const userMasks = Object.values(get().masks).sort( (a, b) => b.createdAt - a.createdAt, ); - const config = useAppConfig.getState(); - if (config.hideBuiltinMasks) return userMasks; + const config = useAppConfig.getState(); // Lấy config hiện tại + if (config.hideBuiltinMasks) return userMasks; // Nếu ẩn mask mặc định thì chỉ trả về mask người dùng + + // Tạo danh sách mask mặc định (BUILTIN_MASKS) với cấu hình model hiện tại const buildinMasks = BUILTIN_MASKS.map( (m) => ({ @@ -101,6 +109,7 @@ export const useMaskStore = createPersistStore( }, }) as Mask, ); + // Trả về danh sách mask người dùng + mask mặc định return userMasks.concat(buildinMasks); }, search(text: string) { diff --git a/app/store/plugin.ts b/app/store/plugin.ts index b3d9f6d8c..2fa071145 100644 --- a/app/store/plugin.ts +++ b/app/store/plugin.ts @@ -236,36 +236,38 @@ export const usePluginStore = createPersistStore( return; } - fetch("./plugins.json") - .then((res) => res.json()) - .then((res) => { - Promise.all( - res.map((item: any) => - // skip get schema - state.get(item.id) - ? item - : fetch(item.schema) - .then((res) => res.text()) - .then((content) => ({ - ...item, - content, - })) - .catch((e) => item), - ), - ).then((builtinPlugins: any) => { - builtinPlugins - .filter((item: any) => item?.content) - .forEach((item: any) => { - const plugin = state.create(item); - state.updatePlugin(plugin.id, (plugin) => { - const tool = FunctionToolService.add(plugin, true); - plugin.title = tool.api.definition.info.title; - plugin.version = tool.api.definition.info.version; - plugin.builtin = true; - }); - }); - }); - }); + // fetch("./plugins.json") + // .then((res) => res.json()) + // .then((res) => { + // Promise.all( + // res.map((item: any) => + // // skip get schema + // state.get(item.id) + // ? item + // : fetch(item.schema) + // .then((res) => res.text()) + // .then((content) => ({ + // ...item, + // content, + // })) + // .catch((e) => item), + // ), + // ).then((builtinPlugins: any) => { + + // // builtinPlugins + // // .filter((item: any) => item?.content) + // // .forEach((item: any) => { + // // const plugin = state.create(item); + // // state.updatePlugin(plugin.id, (plugin) => { + // // const tool = FunctionToolService.add(plugin, true); + // // plugin.title = tool.api.definition.info.title; + // // plugin.version = tool.api.definition.info.version; + // // plugin.builtin = true; + // // }); + // // }); + // }); + + // }); }, }, ); diff --git a/app/utils/chat.ts b/app/utils/chat.ts index cae775512..1da5e895f 100644 --- a/app/utils/chat.ts +++ b/app/utils/chat.ts @@ -198,6 +198,7 @@ export function stream( 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")); @@ -211,6 +212,12 @@ export function stream( responseText += fetchText; remainText = remainText.slice(fetchCount); options.onUpdate?.(responseText, fetchText); + + console.log("[Response Animation] update", { + responseText, + fetchText, + remainText, + }); } requestAnimationFrame(animateResponseText); diff --git a/app/utils/store.ts b/app/utils/store.ts index ff15bee14..04a1d6fcf 100644 --- a/app/utils/store.ts +++ b/app/utils/store.ts @@ -1,9 +1,10 @@ -import { create } from "zustand"; -import { combine, persist, createJSONStorage } from "zustand/middleware"; -import { Updater } from "../typing"; -import { deepClone } from "./clone"; -import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; +import { create } from "zustand"; // Thư viện quản lý state cho React +import { combine, persist, createJSONStorage } from "zustand/middleware"; // Các middleware hỗ trợ zustand +import { Updater } from "../typing"; // Kiểu Updater tự định nghĩa +import { deepClone } from "./clone"; // Hàm deepClone để sao chép sâu object +import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; // Lưu trữ dữ liệu bằng IndexedDB +// Lấy kiểu tham số thứ hai của một hàm type SecondParam = T extends ( _f: infer _F, _s: infer S, @@ -12,52 +13,63 @@ type SecondParam = T extends ( ? S : never; +// Định nghĩa các thuộc tính và phương thức bổ sung cho store type MakeUpdater = { - lastUpdateTime: number; - _hasHydrated: boolean; + lastUpdateTime: number; // Thời gian cập nhật cuối cùng + _hasHydrated: boolean; // Đánh dấu đã hydrate (khôi phục dữ liệu từ storage) - markUpdate: () => void; - update: Updater; - setHasHydrated: (state: boolean) => void; + markUpdate: () => void; // Đánh dấu cập nhật (cập nhật lastUpdateTime) + update: Updater; // Hàm cập nhật state bằng một updater + setHasHydrated: (state: boolean) => void; // Đặt trạng thái hydrate }; +// Kiểu hàm set state cho store type SetStoreState = ( partial: T | Partial | ((state: T) => T | Partial), replace?: boolean | undefined, ) => void; +// Hàm tạo store có persist (lưu trữ lâu dài) với các phương thức bổ sung export function createPersistStore( - state: T, + state: T, // State mặc định ban đầu methods: ( set: SetStoreState>, get: () => T & MakeUpdater, - ) => M, - persistOptions: SecondParam>>, + ) => M, // Các phương thức thao tác với store + persistOptions: SecondParam>>, // Tùy chọn lưu trữ ) { + // Thiết lập storage sử dụng IndexedDB persistOptions.storage = createJSONStorage(() => indexedDBStorage); + + // Lưu lại hàm onRehydrateStorage cũ (nếu có) const oldOonRehydrateStorage = persistOptions?.onRehydrateStorage; + + // Gán lại hàm onRehydrateStorage để đánh dấu đã hydrate khi khôi phục dữ liệu persistOptions.onRehydrateStorage = (state) => { oldOonRehydrateStorage?.(state); return () => state.setHasHydrated(true); }; + // Tạo store với zustand, kết hợp các middleware và phương thức bổ sung return create( persist( combine( { ...state, - lastUpdateTime: 0, - _hasHydrated: false, + lastUpdateTime: 0, // Khởi tạo thời gian cập nhật cuối là 0 + _hasHydrated: false, // Chưa hydrate }, (set, get) => { return { - ...methods(set, get as any), + ...methods(set, get as any), // Thêm các phương thức custom + // Đánh dấu cập nhật (cập nhật lastUpdateTime) markUpdate() { set({ lastUpdateTime: Date.now() } as Partial< T & M & MakeUpdater >); }, + // Hàm cập nhật state bằng một updater, đồng thời cập nhật lastUpdateTime update(updater) { const state = deepClone(get()); updater(state); @@ -66,6 +78,7 @@ export function createPersistStore( lastUpdateTime: Date.now(), }); }, + // Đặt trạng thái hydrate setHasHydrated: (state: boolean) => { set({ _hasHydrated: state } as Partial>); }, diff --git a/git.sh b/git.sh new file mode 100644 index 000000000..03c9c6653 --- /dev/null +++ b/git.sh @@ -0,0 +1,8 @@ +# git config --global user.email "quangdn@giahungtech.com.vn" +# git config --global user.name "quangdn-ght" + +git add . +git commit -m "thay doi alibaba module mac dinh - chebichat" +git push + +# mdZddHXcuzsB0Akk \ No newline at end of file