diff --git a/src/apis/history.js b/src/apis/history.js index 2649630..fffb0e6 100644 --- a/src/apis/history.js +++ b/src/apis/history.js @@ -28,12 +28,12 @@ const MsgHistory = (maxSize = DEFAULT_CONTEXT_SIZE) => { }; }; -export const getMsgHistory = (translator, maxSize) => { - if (historyMap.has(translator)) { - return historyMap.get(translator); +export const getMsgHistory = (apiSlug, maxSize) => { + if (historyMap.has(apiSlug)) { + return historyMap.get(apiSlug); } const msgHistory = MsgHistory(maxSize); - historyMap.set(translator, msgHistory); + historyMap.set(apiSlug, msgHistory); return msgHistory; }; diff --git a/src/apis/index.js b/src/apis/index.js index 3589c8c..c899457 100644 --- a/src/apis/index.js +++ b/src/apis/index.js @@ -7,7 +7,8 @@ import { OPT_LANGS_TENCENT, OPT_LANGS_SPECIAL, OPT_LANGS_MICROSOFT, - OPT_TRANS_BATCH, + API_SPE_TYPES, + DEFAULT_API_SETTING, } from "../config"; import { sha256 } from "../libs/utils"; import { msAuth } from "../libs/auth"; @@ -200,11 +201,10 @@ export const apiTencentLangdetect = async (text) => { * @returns */ export const apiTranslate = async ({ - translator, text, fromLang, toLang, - apiSetting = {}, + apiSetting = DEFAULT_API_SETTING, docInfo = {}, useCache = true, usePool = true, @@ -213,23 +213,23 @@ export const apiTranslate = async ({ return ["", false]; } + const { apiType, apiSlug, useBatchFetch } = apiSetting; const from = - OPT_LANGS_SPECIAL[translator].get(fromLang) ?? - OPT_LANGS_SPECIAL[translator].get("auto"); - const to = OPT_LANGS_SPECIAL[translator].get(toLang); + OPT_LANGS_SPECIAL[apiType].get(fromLang) ?? + OPT_LANGS_SPECIAL[apiType].get("auto"); + const to = OPT_LANGS_SPECIAL[apiType].get(toLang); if (!to) { - kissLog(`target lang: ${toLang} not support`, "translate"); + kissLog(`target lang: ${toLang} not support`); return ["", false]; } - // TODO: 优化缓存失效因素 + // todo: 优化缓存失效因素 const [v1, v2] = process.env.REACT_APP_VERSION.split("."); const cacheOpts = { - translator, + apiSlug, text, fromLang, toLang, - model: apiSetting.model, // model改变,缓存失效 version: [v1, v2].join("."), }; const cacheInput = `${URL_CACHE_TRAN}?${queryString.stringify(cacheOpts)}`; @@ -245,26 +245,21 @@ export const apiTranslate = async ({ // 请求接口数据 let trText = ""; let srLang = ""; - if (apiSetting.useBatchFetch && OPT_TRANS_BATCH.has(translator)) { - const queue = getBatchQueue( - { - translator, - from, - to, - docInfo, - apiSetting, - usePool, - taskFn: handleTranslate, - }, - apiSetting - ); + if (useBatchFetch && API_SPE_TYPES.batch.has(apiType)) { + const queue = getBatchQueue({ + from, + to, + docInfo, + apiSetting, + usePool, + taskFn: handleTranslate, + }); const tranlation = await queue.addTask({ text }); if (Array.isArray(tranlation)) { [trText, srLang = ""] = tranlation; } } else { const translations = await handleTranslate({ - translator, texts: [text], from, to, @@ -281,7 +276,7 @@ export const apiTranslate = async ({ // 插入缓存 if (useCache && trText) { - await putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang }); + putHttpCachePolyfill(cacheInput, null, { trText, isSame, srLang }); } return [trText, isSame]; diff --git a/src/apis/trans.js b/src/apis/trans.js index a60e91f..9b70949 100644 --- a/src/apis/trans.js +++ b/src/apis/trans.js @@ -11,25 +11,17 @@ import { OPT_TRANS_TENCENT, OPT_TRANS_VOLCENGINE, OPT_TRANS_OPENAI, - OPT_TRANS_OPENAI_2, - OPT_TRANS_OPENAI_3, OPT_TRANS_GEMINI, OPT_TRANS_GEMINI_2, OPT_TRANS_CLAUDE, OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_OLLAMA, - OPT_TRANS_OLLAMA_2, - OPT_TRANS_OLLAMA_3, OPT_TRANS_OPENROUTER, OPT_TRANS_CUSTOMIZE, - OPT_TRANS_CUSTOMIZE_2, - OPT_TRANS_CUSTOMIZE_3, - OPT_TRANS_CUSTOMIZE_4, - OPT_TRANS_CUSTOMIZE_5, - OPT_TRANS_CONTEXT, + API_SPE_TYPES, INPUT_PLACE_FROM, INPUT_PLACE_TO, - INPUT_PLACE_TEXT, + // INPUT_PLACE_TEXT, INPUT_PLACE_KEY, INPUT_PLACE_MODEL, } from "../config"; @@ -46,7 +38,7 @@ const keyMap = new Map(); const urlMap = new Map(); // 轮询key/url -const keyPick = (translator, key = "", cacheMap) => { +const keyPick = (apiSlug, key = "", cacheMap) => { const keys = key .split(/\n|,/) .map((item) => item.trim()) @@ -56,9 +48,9 @@ const keyPick = (translator, key = "", cacheMap) => { return ""; } - const preIndex = cacheMap.get(translator) ?? -1; + const preIndex = cacheMap.get(apiSlug) ?? -1; const curIndex = (preIndex + 1) % keys.length; - cacheMap.set(translator, curIndex); + cacheMap.set(apiSlug, curIndex); return keys[curIndex]; }; @@ -68,20 +60,30 @@ const genSystemPrompt = ({ systemPrompt, from, to }) => .replaceAll(INPUT_PLACE_FROM, from) .replaceAll(INPUT_PLACE_TO, to); -const genUserPrompt = ({ userPrompt, from, to, texts, docInfo }) => { +const genUserPrompt = ({ + // userPrompt, + tone, + glossary = {}, + // from, + to, + texts, + docInfo, +}) => { const prompt = JSON.stringify({ targetLanguage: to, title: docInfo.title, description: docInfo.description, segments: texts.map((text, i) => ({ id: i, text })), + glossary, + tone, }); - if (userPrompt.includes(INPUT_PLACE_TEXT)) { - return userPrompt - .replaceAll(INPUT_PLACE_FROM, from) - .replaceAll(INPUT_PLACE_TO, to) - .replaceAll(INPUT_PLACE_TEXT, prompt); - } + // if (userPrompt.includes(INPUT_PLACE_TEXT)) { + // return userPrompt + // .replaceAll(INPUT_PLACE_FROM, from) + // .replaceAll(INPUT_PLACE_TO, to) + // .replaceAll(INPUT_PLACE_TEXT, prompt); + // } return prompt; }; @@ -93,15 +95,19 @@ const parseAIRes = (raw) => { const jsonString = extractJson(raw); data = JSON.parse(jsonString); } catch (err) { - kissLog(err, "parseAIRes"); - data = { translations: [] }; + kissLog("parseAIRes", err); + return []; } if (!Array.isArray(data.translations)) { - data.translations = []; + return []; } - return data.translations.map((item) => [item.text]); + // todo: 考虑序号id可能会打乱 + return data.translations.map((item) => [ + item?.text ?? "", + item?.sourceLanguage ?? "", + ]); }; const genGoogle = ({ texts, from, to, url, key }) => { @@ -675,35 +681,27 @@ const genCustom = ({ * @param {*} * @returns */ -export const genTransReq = (translator, args) => { - switch (translator) { +export const genTransReq = ({ apiType, apiSlug, ...args }) => { + switch (apiType) { case OPT_TRANS_DEEPL: case OPT_TRANS_OPENAI: - case OPT_TRANS_OPENAI_2: - case OPT_TRANS_OPENAI_3: case OPT_TRANS_GEMINI: case OPT_TRANS_GEMINI_2: case OPT_TRANS_CLAUDE: case OPT_TRANS_CLOUDFLAREAI: case OPT_TRANS_OLLAMA: - case OPT_TRANS_OLLAMA_2: - case OPT_TRANS_OLLAMA_3: case OPT_TRANS_OPENROUTER: case OPT_TRANS_NIUTRANS: case OPT_TRANS_CUSTOMIZE: - case OPT_TRANS_CUSTOMIZE_2: - case OPT_TRANS_CUSTOMIZE_3: - case OPT_TRANS_CUSTOMIZE_4: - case OPT_TRANS_CUSTOMIZE_5: - args.key = keyPick(translator, args.key, keyMap); + args.key = keyPick(apiSlug, args.key, keyMap); break; case OPT_TRANS_DEEPLX: - args.url = keyPick(translator, args.url, urlMap); + args.url = keyPick(apiSlug, args.url, urlMap); break; default: } - switch (translator) { + switch (apiType) { case OPT_TRANS_GOOGLE: return genGoogle(args); case OPT_TRANS_GOOGLE_2: @@ -725,8 +723,6 @@ export const genTransReq = (translator, args) => { case OPT_TRANS_VOLCENGINE: return genVolcengine(args); case OPT_TRANS_OPENAI: - case OPT_TRANS_OPENAI_2: - case OPT_TRANS_OPENAI_3: return genOpenAI(args); case OPT_TRANS_GEMINI: return genGemini(args); @@ -737,37 +733,29 @@ export const genTransReq = (translator, args) => { case OPT_TRANS_CLOUDFLAREAI: return genCloudflareAI(args); case OPT_TRANS_OLLAMA: - case OPT_TRANS_OLLAMA_2: - case OPT_TRANS_OLLAMA_3: return genOllama(args); case OPT_TRANS_OPENROUTER: return genOpenRouter(args); case OPT_TRANS_CUSTOMIZE: - case OPT_TRANS_CUSTOMIZE_2: - case OPT_TRANS_CUSTOMIZE_3: - case OPT_TRANS_CUSTOMIZE_4: - case OPT_TRANS_CUSTOMIZE_5: return genCustom(args); default: - throw new Error(`[trans] translator: ${translator} not support`); + throw new Error(`[trans] ${apiType} not support`); } }; /** * 解析翻译接口返回数据 - * @param {*} translator * @param {*} res * @param {*} param3 * @returns */ export const parseTransRes = ( - translator, res, - { texts, from, to, resHook, thinkIgnore, history, userMsg } + { texts, from, to, resHook, thinkIgnore, history, userMsg, apiType } ) => { let modelMsg = ""; - switch (translator) { + switch (apiType) { case OPT_TRANS_GOOGLE: return [[res?.sentences?.map((item) => item.trans).join(" "), res?.src]]; case OPT_TRANS_GOOGLE_2: @@ -814,8 +802,6 @@ export const parseTransRes = ( case OPT_TRANS_VOLCENGINE: return [[res?.translation, res?.detected_language]]; case OPT_TRANS_OPENAI: - case OPT_TRANS_OPENAI_2: - case OPT_TRANS_OPENAI_3: case OPT_TRANS_GEMINI_2: case OPT_TRANS_OPENROUTER: modelMsg = res?.choices?.[0]?.message; @@ -844,8 +830,6 @@ export const parseTransRes = ( case OPT_TRANS_CLOUDFLAREAI: return [[res?.result?.translated_text]]; case OPT_TRANS_OLLAMA: - case OPT_TRANS_OLLAMA_2: - case OPT_TRANS_OLLAMA_3: modelMsg = res?.choices?.[0]?.message; const deepModels = thinkIgnore.split(",").filter((model) => model.trim()); @@ -861,10 +845,6 @@ export const parseTransRes = ( } return parseAIRes(modelMsg?.content); case OPT_TRANS_CUSTOMIZE: - case OPT_TRANS_CUSTOMIZE_2: - case OPT_TRANS_CUSTOMIZE_3: - case OPT_TRANS_CUSTOMIZE_4: - case OPT_TRANS_CUSTOMIZE_5: if (resHook?.trim()) { interpreter.run(`exports.resHook = ${resHook}`); if (history) { @@ -894,7 +874,6 @@ export const parseTransRes = ( * @returns */ export const handleTranslate = async ({ - translator, texts, from, to, @@ -904,12 +883,21 @@ export const handleTranslate = async ({ }) => { let history = null; let hisMsgs = []; - if (apiSetting.useContext && OPT_TRANS_CONTEXT.has(translator)) { - history = getMsgHistory(translator, apiSetting.contextSize); + const { + apiType, + apiSlug, + contextSize, + useContext, + fetchInterval, + fetchLimit, + httpTimeout, + } = apiSetting; + if (useContext && API_SPE_TYPES.context.has(apiType)) { + history = getMsgHistory(apiSlug, contextSize); hisMsgs = history.getAll(); } - const [input, init, userMsg] = await genTransReq(translator, { + const [input, init, userMsg] = await genTransReq({ texts, from, to, @@ -921,15 +909,15 @@ export const handleTranslate = async ({ const res = await fetchData(input, init, { useCache: false, usePool, - fetchInterval: apiSetting.fetchInterval, - fetchLimit: apiSetting.fetchLimit, - httpTimeout: apiSetting.httpTimeout, + fetchInterval, + fetchLimit, + httpTimeout, }); if (!res) { throw new Error("tranlate got empty response"); } - return parseTransRes(translator, res, { + return parseTransRes(res, { texts, from, to, diff --git a/src/background.js b/src/background.js index fddb5a4..79a818f 100644 --- a/src/background.js +++ b/src/background.js @@ -49,7 +49,7 @@ async function addContextMenus(contextMenuType = 1) { try { await browser.contextMenus.removeAll(); } catch (err) { - kissLog(err, "remove contextMenus"); + kissLog("remove contextMenus", err); } switch (contextMenuType) { @@ -122,7 +122,7 @@ async function updateCspRules(csplist = DEFAULT_CSPLIST.join(",\n")) { addRules: newRules, }); } catch (err) { - kissLog(err, "update csp rules"); + kissLog("update csp rules", err); } } diff --git a/src/config/api.js b/src/config/api.js index 297fb8a..8668cc4 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -26,22 +26,16 @@ export const OPT_TRANS_BAIDU = "Baidu"; export const OPT_TRANS_TENCENT = "Tencent"; export const OPT_TRANS_VOLCENGINE = "Volcengine"; export const OPT_TRANS_OPENAI = "OpenAI"; -export const OPT_TRANS_OPENAI_2 = "OpenAI2"; -export const OPT_TRANS_OPENAI_3 = "OpenAI3"; export const OPT_TRANS_GEMINI = "Gemini"; export const OPT_TRANS_GEMINI_2 = "Gemini2"; export const OPT_TRANS_CLAUDE = "Claude"; export const OPT_TRANS_CLOUDFLAREAI = "CloudflareAI"; export const OPT_TRANS_OLLAMA = "Ollama"; -export const OPT_TRANS_OLLAMA_2 = "Ollama2"; -export const OPT_TRANS_OLLAMA_3 = "Ollama3"; export const OPT_TRANS_OPENROUTER = "OpenRouter"; export const OPT_TRANS_CUSTOMIZE = "Custom"; -export const OPT_TRANS_CUSTOMIZE_2 = "Custom2"; -export const OPT_TRANS_CUSTOMIZE_3 = "Custom3"; -export const OPT_TRANS_CUSTOMIZE_4 = "Custom4"; -export const OPT_TRANS_CUSTOMIZE_5 = "Custom5"; -export const OPT_TRANS_ALL = [ + +// 内置支持的翻译引擎 +export const OPT_ALL_TYPES = [ OPT_TRANS_GOOGLE, OPT_TRANS_GOOGLE_2, OPT_TRANS_MICROSOFT, @@ -53,64 +47,74 @@ export const OPT_TRANS_ALL = [ OPT_TRANS_DEEPLX, OPT_TRANS_NIUTRANS, OPT_TRANS_OPENAI, - OPT_TRANS_OPENAI_2, - OPT_TRANS_OPENAI_3, OPT_TRANS_GEMINI, OPT_TRANS_GEMINI_2, OPT_TRANS_CLAUDE, OPT_TRANS_CLOUDFLAREAI, OPT_TRANS_OLLAMA, - OPT_TRANS_OLLAMA_2, - OPT_TRANS_OLLAMA_3, OPT_TRANS_OPENROUTER, OPT_TRANS_CUSTOMIZE, - OPT_TRANS_CUSTOMIZE_2, - OPT_TRANS_CUSTOMIZE_3, - OPT_TRANS_CUSTOMIZE_4, - OPT_TRANS_CUSTOMIZE_5, ]; -// 可使用批处理的翻译引擎 -export const OPT_TRANS_BATCH = new Set([ - OPT_TRANS_GOOGLE_2, - OPT_TRANS_MICROSOFT, - OPT_TRANS_TENCENT, - OPT_TRANS_DEEPL, - OPT_TRANS_OPENAI, - OPT_TRANS_OPENAI_2, - OPT_TRANS_OPENAI_3, - OPT_TRANS_GEMINI, - OPT_TRANS_GEMINI_2, - OPT_TRANS_CLAUDE, - OPT_TRANS_OLLAMA, - OPT_TRANS_OLLAMA_2, - OPT_TRANS_OLLAMA_3, - OPT_TRANS_OPENROUTER, - OPT_TRANS_CUSTOMIZE, - OPT_TRANS_CUSTOMIZE_2, - OPT_TRANS_CUSTOMIZE_3, - OPT_TRANS_CUSTOMIZE_4, - OPT_TRANS_CUSTOMIZE_5, -]); - -// 可使用上下文的翻译引擎 -export const OPT_TRANS_CONTEXT = new Set([ - OPT_TRANS_OPENAI, - OPT_TRANS_OPENAI_2, - OPT_TRANS_OPENAI_3, - OPT_TRANS_GEMINI, - OPT_TRANS_GEMINI_2, - OPT_TRANS_CLAUDE, - OPT_TRANS_OLLAMA, - OPT_TRANS_OLLAMA_2, - OPT_TRANS_OLLAMA_3, - OPT_TRANS_OPENROUTER, - OPT_TRANS_CUSTOMIZE, - OPT_TRANS_CUSTOMIZE_2, - OPT_TRANS_CUSTOMIZE_3, - OPT_TRANS_CUSTOMIZE_4, - OPT_TRANS_CUSTOMIZE_5, -]); +// 翻译引擎特殊集合 +export const API_SPE_TYPES = { + // 内置翻译 + builtin: new Set(OPT_ALL_TYPES), + // 机器翻译 + machine: new Set([ + OPT_TRANS_MICROSOFT, + OPT_TRANS_DEEPLFREE, + OPT_TRANS_BAIDU, + OPT_TRANS_TENCENT, + OPT_TRANS_VOLCENGINE, + ]), + // AI翻译 + ai: new Set([ + OPT_TRANS_OPENAI, + OPT_TRANS_GEMINI, + OPT_TRANS_GEMINI_2, + OPT_TRANS_CLAUDE, + OPT_TRANS_OLLAMA, + OPT_TRANS_OPENROUTER, + ]), + // 支持多key + mulkeys: new Set([ + OPT_TRANS_DEEPL, + OPT_TRANS_OPENAI, + OPT_TRANS_GEMINI, + OPT_TRANS_GEMINI_2, + OPT_TRANS_CLAUDE, + OPT_TRANS_CLOUDFLAREAI, + OPT_TRANS_OLLAMA, + OPT_TRANS_OPENROUTER, + OPT_TRANS_NIUTRANS, + OPT_TRANS_CUSTOMIZE, + ]), + // 支持批处理 + batch: new Set([ + OPT_TRANS_GOOGLE_2, + OPT_TRANS_MICROSOFT, + OPT_TRANS_TENCENT, + OPT_TRANS_DEEPL, + OPT_TRANS_OPENAI, + OPT_TRANS_GEMINI, + OPT_TRANS_GEMINI_2, + OPT_TRANS_CLAUDE, + OPT_TRANS_OLLAMA, + OPT_TRANS_OPENROUTER, + OPT_TRANS_CUSTOMIZE, + ]), + // 支持上下文 + context: new Set([ + OPT_TRANS_OPENAI, + OPT_TRANS_GEMINI, + OPT_TRANS_GEMINI_2, + OPT_TRANS_CLAUDE, + OPT_TRANS_OLLAMA, + OPT_TRANS_OPENROUTER, + OPT_TRANS_CUSTOMIZE, + ]), +}; export const OPT_LANGDETECTOR_ALL = [ OPT_TRANS_GOOGLE, @@ -119,6 +123,24 @@ export const OPT_LANGDETECTOR_ALL = [ OPT_TRANS_TENCENT, ]; +export const BUILTIN_STONES = [ + "formal", // 正式风格 + "casual", // 口语风格 + "neutral", // 中性风格 + "technical", // 技术风格 + "marketing", // 营销风格 + "Literary", // 文学风格 + "academic", // 学术风格 + "legal", // 法律风格 + "literal", // 直译风格 + "ldiomatic", // 意译风格 + "transcreation", // 创译风格 + "machine-like", // 机器风格 + "concise", // 简明风格 +]; +export const BUILTIN_PLACEHOULDERS = ["{ }", "{{ }}", "[ ]", "[[ ]]"]; +export const BUILTIN_TAG_NAMES = ["i", "a", "span"]; + export const OPT_LANGS_TO = [ ["en", "English - English"], ["zh-CN", "Simplified Chinese - 简体中文"], @@ -250,12 +272,6 @@ export const OPT_LANGS_SPECIAL = { [OPT_TRANS_OPENAI]: new Map( OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) ), - [OPT_TRANS_OPENAI_2]: new Map( - OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) - ), - [OPT_TRANS_OPENAI_3]: new Map( - OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) - ), [OPT_TRANS_GEMINI]: new Map( OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) ), @@ -268,12 +284,6 @@ export const OPT_LANGS_SPECIAL = { [OPT_TRANS_OLLAMA]: new Map( OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) ), - [OPT_TRANS_OLLAMA_2]: new Map( - OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) - ), - [OPT_TRANS_OLLAMA_3]: new Map( - OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) - ), [OPT_TRANS_OPENROUTER]: new Map( OPT_LANGS_FROM.map(([key, val]) => [key, val.split(" - ")[0]]) ), @@ -294,18 +304,6 @@ export const OPT_LANGS_SPECIAL = { [OPT_TRANS_CUSTOMIZE]: new Map([ ...OPT_LANGS_FROM.map(([key]) => [key, key]), ]), - [OPT_TRANS_CUSTOMIZE_2]: new Map([ - ...OPT_LANGS_FROM.map(([key]) => [key, key]), - ]), - [OPT_TRANS_CUSTOMIZE_3]: new Map([ - ...OPT_LANGS_FROM.map(([key]) => [key, key]), - ]), - [OPT_TRANS_CUSTOMIZE_4]: new Map([ - ...OPT_LANGS_FROM.map(([key]) => [key, key]), - ]), - [OPT_TRANS_CUSTOMIZE_5]: new Map([ - ...OPT_LANGS_FROM.map(([key]) => [key, key]), - ]), }; export const OPT_LANGS_LIST = OPT_LANGS_TO.map(([lang]) => lang); export const OPT_LANGS_MICROSOFT = new Map( @@ -328,37 +326,44 @@ export const OPT_LANGS_TENCENT = new Map( ); OPT_LANGS_TENCENT.set("zh", "zh-CN"); -// 翻译接口 +const defaultSystemPrompt = `Act as a translation API. Output a single raw JSON object only. No extra text or fences. + +Input: +{"targetLanguage":"","title":"","description":"","segments":[{"id":1,"text":"..."}],"glossary":{"sourceTerm":"targetTerm"},"tone":""} + +Output: +{"translations":[{"id":1,"text":"...","sourceLanguage":""}]} + +Rules: +1. Use title/description for context only; do not output them. +2. Keep id, order, and count of segments. +3. Preserve whitespace, HTML entities, and all HTML-like tags (e.g., , ). Translate inner text only. +4. Highest priority: Follow 'glossary'. Use value for translation; if value is "", keep the key. +5. Do not translate: content in ,
, text enclosed in backticks, or placeholders like {1}, {{1}}, [1], [[1]].
+6.  Apply the specified tone to the translation.
+7.  Detect sourceLanguage for each segment.
+8.  Return empty or unchanged inputs as is.
+
+Example:
+Input: {"targetLanguage":"zh-CN","segments":[{"id":1,"text":"A React component."}],"glossary":{"component":"组件","React":""}}
+Output: {"translations":[{"id":1,"text":"一个React组件","sourceLanguage":"en"}]}
+
+Fail-safe: On any error, return {"translations":[]}.`;
+
+// 翻译接口默认参数
 const defaultApi = {
   apiSlug: "", // 唯一标识
   apiName: "", // 接口名称
+  apiType: "", // 接口类型
   url: "",
   key: "",
   model: "", // 模型名称
-  systemPrompt: `You are a translation API.
-
-Output:
-- Return one raw JSON object only.
-- Start with "{" and end with "}".
-- No fences or extra text.
-
-Input JSON:
-{"targetLanguage":"","title":"","description":"<desc>","segments":[{"id":1,"text":"..."}]}
-
-Output JSON:
-{"translations":[{"id":1,"text":"...","sourceLanguage":"<detected-language>"}]}
-
-Rules:
-1. Use title/description as context only, do not output them.
-2. Keep ids/order/count.
-3. Translate inner text only, not HTML tags.
-4. Do not translate <code>, <pre>, backticks, or terms like React, Docker, JavaScript, API.
-5. Preserve whitespace & entities.
-6. Automatically detect the source language of each segment and add it in the "sourceLanguage" field.
-7. Empty/unchanged input → unchanged.
-
-Fail-safe: {"translations":[]}`,
-  userPrompt: `${INPUT_PLACE_TEXT}`,
+  systemPrompt: defaultSystemPrompt,
+  userPrompt: "",
+  tone: "neutral", // 翻译风格
+  placeholder: "{ }", // 占位符(todo: 备用)
+  tagName: "i", // 标签符 (todo: 备用)
+  aiTerms: false, // AI智能专业术语 (todo: 备用)
   customHeader: "",
   customBody: "",
   reqHook: "", // request 钩子函数
@@ -370,7 +375,6 @@ Fail-safe: {"translations":[]}`,
   batchSize: DEFAULT_BATCH_SIZE, // 每次最多发送段落数量
   batchLength: DEFAULT_BATCH_LENGTH, // 每次发送最大文字数量
   useBatchFetch: false, // 是否启用聚合发送请求
-  useRichText: false, // 是否启用富文本翻译
   useContext: false, // 是否启用智能上下文
   contextSize: DEFAULT_CONTEXT_SIZE, // 智能上下文保留会话数
   temperature: 0,
@@ -379,10 +383,97 @@ Fail-safe: {"translations":[]}`,
   thinkIgnore: "qwen3,deepseek-r1",
   isDisabled: false, // 是否不显示
 };
-const defaultCustomApi = {
-  ...defaultApi,
-  url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
-  reqHook: `// Request Hook
+
+const defaultApiOpts = {
+  [OPT_TRANS_GOOGLE]: {
+    ...defaultApi,
+    url: "https://translate.googleapis.com/translate_a/single",
+  },
+  [OPT_TRANS_GOOGLE_2]: {
+    ...defaultApi,
+    url: "https://translate-pa.googleapis.com/v1/translateHtml",
+    key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_MICROSOFT]: {
+    ...defaultApi,
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_BAIDU]: {
+    ...defaultApi,
+  },
+  [OPT_TRANS_TENCENT]: {
+    ...defaultApi,
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_VOLCENGINE]: {
+    ...defaultApi,
+  },
+  [OPT_TRANS_DEEPL]: {
+    ...defaultApi,
+    url: "https://api-free.deepl.com/v2/translate",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_DEEPLFREE]: {
+    ...defaultApi,
+    fetchLimit: 1,
+  },
+  [OPT_TRANS_DEEPLX]: {
+    ...defaultApi,
+    url: "http://localhost:1188/translate",
+    fetchLimit: 1,
+  },
+  [OPT_TRANS_NIUTRANS]: {
+    ...defaultApi,
+    url: "https://api.niutrans.com/NiuTransServer/translation",
+    dictNo: "",
+    memoryNo: "",
+  },
+  [OPT_TRANS_OPENAI]: {
+    ...defaultApi,
+    url: "https://api.openai.com/v1/chat/completions",
+    model: "gpt-4",
+    useBatchFetch: true,
+    fetchLimit: 1,
+  },
+  [OPT_TRANS_GEMINI]: {
+    ...defaultApi,
+    url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
+    model: "gemini-2.5-flash",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_GEMINI_2]: {
+    ...defaultApi,
+    url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
+    model: "gemini-2.0-flash",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_CLAUDE]: {
+    ...defaultApi,
+    url: "https://api.anthropic.com/v1/messages",
+    model: "claude-3-haiku-20240307",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_CLOUDFLAREAI]: {
+    ...defaultApi,
+    url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
+  },
+  [OPT_TRANS_OLLAMA]: {
+    ...defaultApi,
+    url: "http://localhost:11434/v1/chat/completions",
+    model: "llama3.1",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_OPENROUTER]: {
+    ...defaultApi,
+    url: "https://openrouter.ai/api/v1/chat/completions",
+    model: "openai/gpt-4o",
+    useBatchFetch: true,
+  },
+  [OPT_TRANS_CUSTOMIZE]: {
+    ...defaultApi,
+    url: "https://translate.googleapis.com/translate_a/single?client=gtx&dj=1&dt=t&ie=UTF-8&q={{text}}&sl=en&tl=zh-CN",
+    reqHook: `// Request Hook
 (text, from, to, url, key) => [url, {
   headers: {
       "Content-type": "application/json",
@@ -390,172 +481,18 @@ const defaultCustomApi = {
   method: "GET",
   body: null,
 }]`,
-  resHook: `// Response Hook
+    resHook: `// Response Hook
 (res, text, from, to) => [res.sentences.map((item) => item.trans).join(" "), to === res.src]`,
-};
-const defaultOpenaiApi = {
-  ...defaultApi,
-  url: "https://api.openai.com/v1/chat/completions",
-  model: "gpt-4",
-  fetchLimit: 1,
-};
-const defaultOllamaApi = {
-  ...defaultApi,
-  url: "http://localhost:11434/v1/chat/completions",
-  model: "llama3.1",
-};
-export const DEFAULT_TRANS_APIS = {
-  [OPT_TRANS_GOOGLE]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_GOOGLE,
-    apiName: OPT_TRANS_GOOGLE,
-    url: "https://translate.googleapis.com/translate_a/single",
-  },
-  [OPT_TRANS_GOOGLE_2]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_GOOGLE_2,
-    apiName: OPT_TRANS_GOOGLE_2,
-    url: "https://translate-pa.googleapis.com/v1/translateHtml",
-    key: "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520",
-    useBatchFetch: true,
-  },
-  [OPT_TRANS_MICROSOFT]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_MICROSOFT,
-    apiName: OPT_TRANS_MICROSOFT,
-    useBatchFetch: true,
-  },
-  [OPT_TRANS_BAIDU]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_BAIDU,
-    apiName: OPT_TRANS_BAIDU,
-  },
-  [OPT_TRANS_TENCENT]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_TENCENT,
-    apiName: OPT_TRANS_TENCENT,
-    useBatchFetch: true,
-  },
-  [OPT_TRANS_VOLCENGINE]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_VOLCENGINE,
-    apiName: OPT_TRANS_VOLCENGINE,
-  },
-  [OPT_TRANS_DEEPL]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_DEEPL,
-    apiName: OPT_TRANS_DEEPL,
-    url: "https://api-free.deepl.com/v2/translate",
-    useBatchFetch: true,
-  },
-  [OPT_TRANS_DEEPLFREE]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_DEEPLFREE,
-    apiName: OPT_TRANS_DEEPLFREE,
-    fetchLimit: 1,
-  },
-  [OPT_TRANS_DEEPLX]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_DEEPLX,
-    apiName: OPT_TRANS_DEEPLX,
-    url: "http://localhost:1188/translate",
-    fetchLimit: 1,
-  },
-  [OPT_TRANS_NIUTRANS]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_NIUTRANS,
-    apiName: OPT_TRANS_NIUTRANS,
-    url: "https://api.niutrans.com/NiuTransServer/translation",
-    dictNo: "",
-    memoryNo: "",
-  },
-  [OPT_TRANS_OPENAI]: {
-    ...defaultOpenaiApi,
-    apiSlug: OPT_TRANS_OPENAI,
-    apiName: OPT_TRANS_OPENAI,
-  },
-  [OPT_TRANS_OPENAI_2]: {
-    ...defaultOpenaiApi,
-    apiSlug: OPT_TRANS_OPENAI_2,
-    apiName: OPT_TRANS_OPENAI_2,
-  },
-  [OPT_TRANS_OPENAI_3]: {
-    ...defaultOpenaiApi,
-    apiSlug: OPT_TRANS_OPENAI_3,
-    apiName: OPT_TRANS_OPENAI_3,
-  },
-  [OPT_TRANS_GEMINI]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_GEMINI,
-    apiName: OPT_TRANS_GEMINI,
-    url: `https://generativelanguage.googleapis.com/v1/models/${INPUT_PLACE_MODEL}:generateContent?key=${INPUT_PLACE_KEY}`,
-    model: "gemini-2.5-flash",
-  },
-  [OPT_TRANS_GEMINI_2]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_GEMINI_2,
-    apiName: OPT_TRANS_GEMINI_2,
-    url: `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions`,
-    model: "gemini-2.0-flash",
-  },
-  [OPT_TRANS_CLAUDE]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_CLAUDE,
-    apiName: OPT_TRANS_CLAUDE,
-    url: "https://api.anthropic.com/v1/messages",
-    model: "claude-3-haiku-20240307",
-  },
-  [OPT_TRANS_CLOUDFLAREAI]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_CLOUDFLAREAI,
-    apiName: OPT_TRANS_CLOUDFLAREAI,
-    url: "https://api.cloudflare.com/client/v4/accounts/{{ACCOUNT_ID}}/ai/run/@cf/meta/m2m100-1.2b",
-  },
-  [OPT_TRANS_OLLAMA]: {
-    ...defaultOllamaApi,
-    apiSlug: OPT_TRANS_OLLAMA,
-    apiName: OPT_TRANS_OLLAMA,
-  },
-  [OPT_TRANS_OLLAMA_2]: {
-    ...defaultOllamaApi,
-    apiSlug: OPT_TRANS_OLLAMA_2,
-    apiName: OPT_TRANS_OLLAMA_2,
-  },
-  [OPT_TRANS_OLLAMA_3]: {
-    ...defaultOllamaApi,
-    apiSlug: OPT_TRANS_OLLAMA_3,
-    apiName: OPT_TRANS_OLLAMA_3,
-  },
-  [OPT_TRANS_OPENROUTER]: {
-    ...defaultApi,
-    apiSlug: OPT_TRANS_OPENROUTER,
-    apiName: "",
-    url: "https://openrouter.ai/api/v1/chat/completions",
-    model: "openai/gpt-4o",
-  },
-  [OPT_TRANS_CUSTOMIZE]: {
-    ...defaultCustomApi,
-    apiSlug: OPT_TRANS_CUSTOMIZE,
-    apiName: OPT_TRANS_CUSTOMIZE,
-  },
-  [OPT_TRANS_CUSTOMIZE_2]: {
-    ...defaultCustomApi,
-    apiSlug: OPT_TRANS_CUSTOMIZE_2,
-    apiName: OPT_TRANS_CUSTOMIZE_2,
-  },
-  [OPT_TRANS_CUSTOMIZE_3]: {
-    ...defaultCustomApi,
-    apiSlug: OPT_TRANS_CUSTOMIZE_3,
-    apiName: OPT_TRANS_CUSTOMIZE_3,
-  },
-  [OPT_TRANS_CUSTOMIZE_4]: {
-    ...defaultCustomApi,
-    apiSlug: OPT_TRANS_CUSTOMIZE_4,
-    apiName: OPT_TRANS_CUSTOMIZE_4,
-  },
-  [OPT_TRANS_CUSTOMIZE_5]: {
-    ...defaultCustomApi,
-    apiSlug: OPT_TRANS_CUSTOMIZE_5,
-    apiName: OPT_TRANS_CUSTOMIZE_5,
   },
 };
+
+// 内置翻译接口列表(带参数)
+export const DEFAULT_API_LIST = OPT_ALL_TYPES.map((apiType) => ({
+  ...defaultApiOpts[apiType],
+  apiSlug: apiType,
+  apiName: apiType,
+  apiType,
+}));
+
+export const DEFAULT_API_TYPE = OPT_TRANS_MICROSOFT;
+export const DEFAULT_API_SETTING = DEFAULT_API_LIST[DEFAULT_API_TYPE];
diff --git a/src/config/i18n.js b/src/config/i18n.js
index 04826c6..9c4ccba 100644
--- a/src/config/i18n.js
+++ b/src/config/i18n.js
@@ -1342,4 +1342,59 @@ export const I18N = {
     en: `Hide Original`,
     zh_TW: `隱藏原文`,
   },
+  confirm_title: {
+    zh: `确认`,
+    en: `Confirm`,
+    zh_TW: `確認`,
+  },
+  confirm_message: {
+    zh: `确定操作吗?`,
+    en: `Are you sure you want to proceed?`,
+    zh_TW: `確定操作嗎?`,
+  },
+  confirm_action: {
+    zh: `确定`,
+    en: `Confirm`,
+    zh_TW: `確定`,
+  },
+  cancel_action: {
+    zh: `取消`,
+    en: `Cancel`,
+    zh_TW: `取消`,
+  },
+  pls_press_shortcut: {
+    zh: `请按下快捷键组合`,
+    en: `Please press the shortcut key combination`,
+    zh_TW: `請按下快速鍵組合`,
+  },
+  load_setting_err: {
+    zh: `数据加载出错,请刷新页面或卸载后重新安装。`,
+    en: `Please press the shortcut key combination`,
+    zh_TW: `請按下快速鍵組合`,
+  },
+  translation_style: {
+    zh: `翻译风格`,
+    en: `Translation style`,
+    zh_TW: `翻譯風格`,
+  },
+  placeholder: {
+    zh: `占位符`,
+    en: `Placeholder`,
+    zh_TW: `佔位符`,
+  },
+  tag_name: {
+    zh: `占位标签名`,
+    en: `Placeholder tag name`,
+    zh_TW: `佔位標名`,
+  },
+  ai_terms: {
+    zh: `AI识别术语表`,
+    en: `AI Identification Glossary`,
+    zh_TW: `AI辨識術語表`,
+  },
+  system_prompt_helper: {
+    zh: `在未完全理解默认Prompt的情况下,请勿随意修改,否则可能翻译失败。`,
+    en: `If you do not fully understand the default prompt, please do not modify it at will, otherwise the translation may fail.`,
+    zh_TW: `在未完全理解預設Prompt的情況下,請勿隨意修改,否則可能翻譯失敗。`,
+  },
 };
diff --git a/src/config/rules.js b/src/config/rules.js
index b31ba7e..a499a26 100644
--- a/src/config/rules.js
+++ b/src/config/rules.js
@@ -1,4 +1,3 @@
-import { FIXER_BR, FIXER_BN, FIXER_BR_DIV, FIXER_BN_DIV } from "../libs/webfix";
 import { OPT_TRANS_MICROSOFT } from "./api";
 
 export const GLOBAL_KEY = "*";
@@ -58,6 +57,19 @@ export const OPT_TIMING_ALL = [
   OPT_TIMING_ALT,
 ];
 
+const DEFAULT_DIY_STYLE = `color: #666;
+background: linear-gradient(
+  45deg,
+  LightGreen 20%,
+  LightPink 20% 40%,
+  LightSalmon 40% 60%,
+  LightSeaGreen 60% 80%,
+  LightSkyBlue 80%
+);
+&:hover {
+  color: #333;
+};`;
+
 export const DEFAULT_SELECTOR =
   "h1, h2, h3, h4, h5, h6, li, p, dd, blockquote, figcaption, label, legend";
 export const DEFAULT_IGNORE_SELECTOR =
@@ -68,13 +80,13 @@ export const DEFAULT_RULE = {
   selector: "", // 选择器
   keepSelector: "", // 保留元素选择器
   terms: "", // 专业术语
-  translator: GLOBAL_KEY, // 翻译服务
+  apiSlug: GLOBAL_KEY, // 翻译服务
   fromLang: GLOBAL_KEY, // 源语言
   toLang: GLOBAL_KEY, // 目标语言
   textStyle: GLOBAL_KEY, // 译文样式
   transOpen: GLOBAL_KEY, // 开启翻译
   bgColor: "", // 译文颜色
-  textDiyStyle: "", // 自定义译文样式
+  textDiyStyle: DEFAULT_DIY_STYLE, // 自定义译文样式
   selectStyle: "", // 选择器节点样式
   parentStyle: "", // 选择器父节点样式
   injectJs: "", // 注入JS
@@ -104,7 +116,7 @@ export const GLOBLA_RULE = {
   selector: DEFAULT_SELECTOR, // 选择器
   keepSelector: DEFAULT_KEEP_SELECTOR, // 保留元素选择器
   terms: "", // 专业术语
-  translator: OPT_TRANS_MICROSOFT, // 翻译服务
+  apiSlug: OPT_TRANS_MICROSOFT, // 翻译服务
   fromLang: "auto", // 源语言
   toLang: "zh-CN", // 目标语言
   textStyle: OPT_STYLE_NONE, // 译文样式
@@ -136,21 +148,8 @@ export const GLOBLA_RULE = {
 
 export const DEFAULT_RULES = [GLOBLA_RULE];
 
-const DEFAULT_DIY_STYLE = `color: #666;
-background: linear-gradient(
-  45deg,
-  LightGreen 20%,
-  LightPink 20% 40%,
-  LightSalmon 40% 60%,
-  LightSeaGreen 60% 80%,
-  LightSkyBlue 80%
-);
-&:hover {
-  color: #333;
-};`;
-
 export const DEFAULT_OW_RULE = {
-  translator: REMAIN_KEY,
+  apiSlug: REMAIN_KEY,
   fromLang: REMAIN_KEY,
   toLang: REMAIN_KEY,
   textStyle: REMAIN_KEY,
@@ -159,258 +158,33 @@ export const DEFAULT_OW_RULE = {
   textDiyStyle: DEFAULT_DIY_STYLE,
 };
 
+// todo: 校验几个内置规则
 const RULES_MAP = {
   "www.google.com/search": {
     selector: `h3, .IsZvec, .VwiC3b`,
   },
-  "news.google.com": {
-    selector: `[data-n-tid], ${DEFAULT_SELECTOR}`,
-  },
-  "www.foxnews.com": {
-    selector: `h1, h2, .title, .sidebar [data-type="Title"], .article-content ${DEFAULT_SELECTOR}; [data-spotim-module="conversation"]>div >>> [data-spot-im-class="message-text"] p,  [data-spot-im-class="message-text"]`,
-  },
-  "bearblog.dev, www.theverge.com, www.tampermonkey.net/documentation.php": {
-    selector: `${DEFAULT_SELECTOR}`,
-  },
-  "themessenger.com": {
-    selector: `.leading-tight, .leading-tighter, .my-2 p, .font-body p, article ${DEFAULT_SELECTOR}`,
-  },
-  "www.telegraph.co.uk, go.dev/doc/": {
-    selector: `article ${DEFAULT_SELECTOR}`,
-  },
-  "www.theguardian.com": {
-    selector: `.show-underline, .dcr-hup5wm div, .dcr-7vl6y8 div, .dcr-12evv1c, figcaption, article ${DEFAULT_SELECTOR}, [data-cy="mostviewed-footer"] h4`,
-  },
-  "www.semafor.com": {
-    selector: `${DEFAULT_SELECTOR}, .styles_intro__IYj__, [class*="styles_description"]`,
-  },
-  "www.noemamag.com": {
-    selector: `.splash__title, .single-card__title, .single-card__type, .single-card__topic, .highlighted-content__title, .single-card__author, article ${DEFAULT_SELECTOR}, .quote__text, .wp-caption-text div`,
-  },
-  "restofworld.org": {
-    selector: `${DEFAULT_SELECTOR}, .recirc-story__headline, .recirc-story__dek`,
-  },
-  "www.axios.com": {
-    selector: `.h7, ${DEFAULT_SELECTOR}`,
-  },
-  "www.newyorker.com": {
-    selector: `.summary-item__hed, .summary-item__dek, .summary-collection-grid__dek, .dqtvfu, .rubric__link, .caption, article ${DEFAULT_SELECTOR}, .HEhan ${DEFAULT_SELECTOR}, .ContributorBioBio-fBolsO, .BaseText-ewhhUZ`,
-  },
-  "time.com": {
-    selector: `h1, h3, .summary, .video-title, #article-body ${DEFAULT_SELECTOR}, .image-wrap-container .credit.body-caption, .media-heading`,
-  },
-  "www.dw.com": {
-    selector: `.ts-teaser-title a, .news-title a, .title a, .teaser-description a, .hbudab h3, .hbudab p, figcaption ,article ${DEFAULT_SELECTOR}`,
-  },
-  "www.bbc.com": {
-    selector: `h1, h2, .media__link, .media__summary, article ${DEFAULT_SELECTOR}, .ssrcss-y7krbn-Stack, .ssrcss-17zglt8-PromoHeadline, .ssrcss-18cjaf3-Headline, .gs-c-promo-heading__title, .gs-c-promo-summary, .media__content h3, .article__intro, .lx-c-summary-points>li`,
-  },
-  "www.chinadaily.com.cn": {
-    selector: `h1, .tMain [shape="rect"], .cMain [shape="rect"], .photo_art [shape="rect"], .mai_r [shape="rect"], .lisBox li, #Content ${DEFAULT_SELECTOR}`,
-  },
-  "www.facebook.com": {
-    selector: `[role="main"] [dir="auto"]`,
-  },
-  "www.reddit.com, new.reddit.com, sh.reddit.com": {
-    selector: `:is(#AppRouter-main-content, #overlayScrollContainer) :is([class^=tbIA],[class^=_1zP],[class^=ULWj],[class^=_2Jj], [class^=_334],[class^=_2Gr],[class^=_7T4],[class^=_1WO], ${DEFAULT_SELECTOR}); [id^="post-title"], :is([slot="text-body"], [slot="comment"]) ${DEFAULT_SELECTOR}, recent-posts h3, aside :is(span:has(>h2), p); shreddit-subreddit-header >>> :is(#title, #description)`,
-  },
-  "www.quora.com": {
-    selector: `.qu-wordBreak--break-word`,
-  },
-  "edition.cnn.com": {
-    selector: `.container__title, .container__headline, .headline__text, .image__caption, [data-type="Title"], .article__content ${DEFAULT_SELECTOR}`,
-  },
-  "www.reuters.com": {
-    selector: `#main-content [data-testid="Heading"], #main-content [data-testid="Body"], .article-body__content__17Yit ${DEFAULT_SELECTOR}`,
-  },
-  "www.bloomberg.com": {
-    selector: `[data-component="headline"], [data-component="related-item-headline"], [data-component="title"], article ${DEFAULT_SELECTOR}`,
-  },
-  "deno.land, docs.github.com": {
-    selector: `main ${DEFAULT_SELECTOR}`,
-    keepSelector: DEFAULT_KEEP_SELECTOR,
-  },
-  "doc.rust-lang.org": {
-    selector: `.content ${DEFAULT_SELECTOR}`,
-    keepSelector: DEFAULT_KEEP_SELECTOR,
-  },
-  "www.indiehackers.com": {
-    selector: `h1, h3, .content ${DEFAULT_SELECTOR}, .feed-item__title-link`,
-  },
-  "platform.openai.com/docs": {
-    selector: `.docs-body ${DEFAULT_SELECTOR}`,
-    keepSelector: DEFAULT_KEEP_SELECTOR,
-  },
   "en.wikipedia.org": {
     selector: `h1, .mw-parser-output ${DEFAULT_SELECTOR}`,
     keepSelector: `.mwe-math-element`,
   },
-  "stackoverflow.com, serverfault.com, superuser.com, stackexchange.com, askubuntu.com, stackapps.com, mathoverflow.net":
-    {
-      selector: `.s-prose ${DEFAULT_SELECTOR}, .comment-copy, .question-hyperlink, .s-post-summary--content-title, .s-post-summary--content-excerpt`,
-      keepSelector: `${DEFAULT_KEEP_SELECTOR}, .math-container`,
-    },
-  "www.npmjs.com/package, developer.chrome.com/docs, medium.com, react.dev, create-react-app.dev, pytorch.org":
-    {
-      selector: `article ${DEFAULT_SELECTOR}`,
-    },
   "news.ycombinator.com": {
     selector: `.title, p`,
     fixerSelector: `.toptext, .commtext`,
-    fixerFunc: FIXER_BR,
   },
   "github.com": {
     selector: `.markdown-body ${DEFAULT_SELECTOR}, .repo-description p, .Layout-sidebar .f4, .container-lg .py-4 .f5, .container-lg .my-4 .f5, .Box-row .pr-4, .Box-row article .mt-1, [itemprop="description"], .markdown-title, bdi, .ws-pre-wrap, .status-meta, span.status-meta, .col-10.color-fg-muted, .TimelineItem-body, .pinned-item-list-item-content .color-fg-muted, .markdown-body td, .markdown-body th`,
     keepSelector: DEFAULT_KEEP_SELECTOR,
   },
-  "twitter.com": {
+  "twitter.com, https://x.com": {
     selector: `[data-testid="tweetText"], [data-testid="birdwatch-pivot"]>div.css-1rynq56`,
     keepSelector: `img, a, .r-18u37iz, .css-175oi2r`,
   },
-  "m.youtube.com": {
-    selector: `.slim-video-information-title .yt-core-attributed-string, .media-item-headline .yt-core-attributed-string, .comment-text .yt-core-attributed-string, .typography-body-2b .yt-core-attributed-string, #ytp-caption-window-container .ytp-caption-segment`,
-    selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
-    parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
-    keepSelector: `img, #content-text>a`,
-  },
   "www.youtube.com": {
     selector: `h1, #video-title, #content-text, #title, yt-attributed-string>span>span, #ytp-caption-window-container .ytp-caption-segment`,
     selectStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
     parentStyle: `-webkit-line-clamp: unset; max-height: none; height: auto;`,
     keepSelector: `img, #content-text>a`,
   },
-  "bard.google.com": {
-    selector: `.query-content ${DEFAULT_SELECTOR}, message-content ${DEFAULT_SELECTOR}`,
-  },
-  "www.bing.com, copilot.microsoft.com": {
-    selector: `.b_algoSlug, .rwrl_padref; .cib-serp-main >>> .ac-textBlock ${DEFAULT_SELECTOR}, .text-message-content div`,
-  },
-  "www.phoronix.com": {
-    selector: `article ${DEFAULT_SELECTOR}`,
-    fixerSelector: `.content`,
-    fixerFunc: FIXER_BR,
-  },
-  "wx2.qq.com": {
-    selector: `.js_message_plain`,
-  },
-  "app.slack.com/client/": {
-    selector: `.p-rich_text_section, .c-message_attachment__text, .p-rich_text_list li`,
-  },
-  "discord.com/channels/": {
-    selector: `div[class^=message], div[class^=headerText], div[class^=name_], section[aria-label='Search Results'] div[id^=message-content], div[id^=message]`,
-    keepSelector: `li[class^='card'] div[class^='message'], [class^='embedFieldValue'], [data-list-item-id^='forum-channel-list'] div[class^='headerText']`,
-  },
-  "t.me/s/": {
-    selector: `.js-message_text ${DEFAULT_SELECTOR}`,
-    fixerSelector: `.tgme_widget_message_text`,
-    fixerFunc: FIXER_BR,
-  },
-  "web.telegram.org/k": {
-    selector: `div.kiss-p`,
-    keepSelector: `div[class^=time], .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element, reactions-element`,
-    fixerSelector: `.message`,
-    fixerFunc: FIXER_BN_DIV,
-  },
-  "web.telegram.org/a": {
-    selector: `.text-content > .kiss-p`,
-    keepSelector: `.Reactions, .time, .peer-title, .document-wrapper, .message.spoilers-container custom-emoji-element`,
-    fixerSelector: `.text-content`,
-    fixerFunc: FIXER_BR_DIV,
-  },
-  "www.instagram.com/": {
-    selector: `h1, article span[dir=auto] > span[dir=auto], ._ab1y`,
-  },
-  "www.instagram.com/p/,www.instagram.com/reels/": {
-    selector: `h1, div[class='x9f619 xjbqb8w x78zum5 x168nmei x13lgxp2 x5pf9jr xo71vjh x1uhb9sk x1plvlek xryxfnj x1c4vz4f x2lah0s xdt5ytf xqjyukv x1cy8zhl x1oa3qoh x1nhvcw1'] > span[class='x1lliihq x1plvlek xryxfnj x1n2onr6 x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs x1s928wv xhkezso x1gmr53x x1cpjm7i x1fgarty x1943h6x x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj'], span[class='x193iq5w xeuugli x1fj9vlw x13faqbe x1vvkbs xt0psk2 x1i0vuye xvs91rp xo1l8bm x5n08af x10wh9bi x1wdrske x8viiok x18hxmgj']`,
-  },
-  "mail.google.com": {
-    selector: `.a3s.aiL ${DEFAULT_SELECTOR}, span[data-thread-id]`,
-    fixerSelector: `.a3s.aiL`,
-    fixerFunc: FIXER_BR,
-  },
-  "web.whatsapp.com": {
-    selector: `.copyable-text > span`,
-  },
-  "chat.openai.com": {
-    selector: `div[data-message-author-role] > div ${DEFAULT_SELECTOR}`,
-    fixerSelector: `div[data-message-author-role='user'] > div`,
-    fixerFunc: FIXER_BN,
-  },
-  "forum.ru-board.com": {
-    selector: `.tit, .dats, .kiss-p, .lgf ${DEFAULT_SELECTOR}`,
-    fixerSelector: `span.post`,
-    fixerFunc: FIXER_BR,
-  },
-  "education.github.com": {
-    selector: `${DEFAULT_SELECTOR}, a, summary, span.Button-content`,
-  },
-  "blogs.windows.com": {
-    selector: `${DEFAULT_SELECTOR}, .c-uhf-nav-link, figcaption`,
-    fixerSelector: `.t-content>div>ul>li`,
-    fixerFunc: FIXER_BR,
-  },
-  "developer.apple.com/documentation/": {
-    selector: `#main ${DEFAULT_SELECTOR}, #main .abstract .content, #main .abstract.content, #main .link span`,
-    keepSelector: DEFAULT_KEEP_SELECTOR,
-  },
-  "greasyfork.org": {
-    selector: `h2, .script-link, .script-description, #additional-info ${DEFAULT_SELECTOR}`,
-  },
-  "www.fmkorea.com": {
-    selector: `#container ${DEFAULT_SELECTOR}`,
-  },
-  "forum.arduino.cc": {
-    selector: `.top-row>.title, .featured-topic>.title, .link-top-line>.title, .category-description, .topic-excerpt, .fancy-title, .cooked ${DEFAULT_SELECTOR}`,
-  },
-  "docs.arduino.cc": {
-    selector: `[class^="tutorial-module--left"] ${DEFAULT_SELECTOR}`,
-  },
-  "www.historydefined.net": {
-    selector: `.wp-element-caption, ${DEFAULT_SELECTOR}`,
-  },
-  "gobyexample.com": {
-    selector: `.docs p`,
-    keepSelector: `code`,
-  },
-  "go.dev/tour": {
-    selector: `#left-side ${DEFAULT_SELECTOR}`,
-    keepSelector: `code, img, svg >>> code`,
-  },
-  "pkg.go.dev": {
-    selector: `.Documentation-content ${DEFAULT_SELECTOR}`,
-    keepSelector: `${DEFAULT_KEEP_SELECTOR}, a, span`,
-  },
-  "docs.rs": {
-    selector: `.docblock ${DEFAULT_SELECTOR}, .docblock-short`,
-    keepSelector: `code >>> code`,
-  },
-  "randomnerdtutorials.com": {
-    selector: `article ${DEFAULT_SELECTOR}`,
-  },
-  "notebooks.githubusercontent.com/view/ipynb": {
-    selector: `#notebook-container ${DEFAULT_SELECTOR}`,
-    keepSelector: DEFAULT_KEEP_SELECTOR,
-  },
-  "developers.cloudflare.com": {
-    selector: `article ${DEFAULT_SELECTOR}, .WorkerStarter--description`,
-    keepSelector: `a[rel='noopener'], code`,
-  },
-  "ubuntuforums.org": {
-    fixerSelector: `.postcontent`,
-    fixerFunc: FIXER_BR,
-  },
-  "play.google.com/store/apps/details": {
-    fixerSelector: `[data-g-id="description"]`,
-    fixerFunc: FIXER_BR,
-  },
-  "news.yahoo.co.jp/articles/": {
-    fixerSelector: `.sc-cTsKDU`,
-    fixerFunc: FIXER_BN,
-  },
-  "chromereleases.googleblog.com": {
-    fixerSelector: `.post-content, .post-content > span, li > span`,
-    fixerFunc: FIXER_BR,
-  },
 };
 
 export const BUILTIN_RULES = Object.entries(RULES_MAP)
diff --git a/src/config/setting.js b/src/config/setting.js
index 94f97d0..8f98f0a 100644
--- a/src/config/setting.js
+++ b/src/config/setting.js
@@ -2,7 +2,7 @@ import {
   OPT_DICT_BAIDU,
   DEFAULT_HTTP_TIMEOUT,
   OPT_TRANS_MICROSOFT,
-  DEFAULT_TRANS_APIS,
+  DEFAULT_API_LIST,
 } from "./api";
 import { DEFAULT_OW_RULE } from "./rules";
 
@@ -50,7 +50,7 @@ export const OPT_INPUT_TRANS_SIGNS = ["/", "//", "\\", "\\\\", ">", ">>"];
 export const DEFAULT_INPUT_SHORTCUT = ["AltLeft", "KeyI"];
 export const DEFAULT_INPUT_RULE = {
   transOpen: true,
-  translator: OPT_TRANS_MICROSOFT,
+  apiSlug: OPT_TRANS_MICROSOFT,
   fromLang: "auto",
   toLang: "en",
   triggerShortcut: DEFAULT_INPUT_SHORTCUT,
@@ -75,7 +75,7 @@ export const OPT_TRANBOX_TRIGGER_ALL = [
 export const DEFAULT_TRANBOX_SHORTCUT = ["AltLeft", "KeyS"];
 export const DEFAULT_TRANBOX_SETTING = {
   // transOpen: true, // 是否启用划词翻译(作废,移至rule)
-  translator: OPT_TRANS_MICROSOFT,
+  apiSlug: OPT_TRANS_MICROSOFT,
   fromLang: "auto",
   toLang: "zh-CN",
   toLang2: "en",
@@ -109,17 +109,17 @@ export const DEFAULT_SUBRULES_LIST = [
   },
 ];
 
-export const DEFAULT__MOUSEHOVER_KEY = ["ControlLeft"];
+export const DEFAULT_MOUSEHOVER_KEY = ["ControlLeft"];
 export const DEFAULT_MOUSE_HOVER_SETTING = {
   useMouseHover: true, // 是否启用鼠标悬停翻译
-  mouseHoverKey: DEFAULT__MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
+  mouseHoverKey: DEFAULT_MOUSEHOVER_KEY, // 鼠标悬停翻译组合键
 };
 
 export const DEFAULT_SETTING = {
   darkMode: false, // 深色模式
   uiLang: "en", // 界面语言
-  // fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至transApis,作废)
-  // fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至transApis,作废)
+  // fetchLimit: DEFAULT_FETCH_LIMIT, // 最大任务数量(移至rule,作废)
+  // fetchInterval: DEFAULT_FETCH_INTERVAL, // 任务间隔时间(移至rule,作废)
   minLength: TRANS_MIN_LENGTH,
   maxLength: TRANS_MAX_LENGTH,
   newlineLength: TRANS_NEWLINE_LENGTH,
@@ -136,7 +136,7 @@ export const DEFAULT_SETTING = {
   // transTitle: false, // 是否同时翻译页面标题(移至rule,作废)
   subrulesList: DEFAULT_SUBRULES_LIST, // 订阅列表
   owSubrule: DEFAULT_OW_RULE, // 覆写订阅规则
-  transApis: DEFAULT_TRANS_APIS, // 翻译接口
+  transApis: DEFAULT_API_LIST, // 翻译接口 (v2.0 对象改为数组)
   // mouseKey: OPT_TIMING_PAGESCROLL, // 翻译时机/鼠标悬停翻译(移至rule,作废)
   shortcuts: DEFAULT_SHORTCUTS, // 快捷键
   inputRule: DEFAULT_INPUT_RULE, // 输入框设置
@@ -145,7 +145,7 @@ export const DEFAULT_SETTING = {
   blacklist: DEFAULT_BLACKLIST.join(",\n"), // 禁用翻译名单
   csplist: DEFAULT_CSPLIST.join(",\n"), // 禁用CSP名单
   // disableLangs: [], // 不翻译的语言(移至rule,作废)
-  transInterval: 200, // 翻译等待时间
+  transInterval: 100, // 翻译等待时间
   langDetector: OPT_TRANS_MICROSOFT, // 远程语言识别服务
   mouseHoverSetting: DEFAULT_MOUSE_HOVER_SETTING, // 鼠标悬停翻译
 };
diff --git a/src/hooks/Api.js b/src/hooks/Api.js
index 9575657..2842784 100644
--- a/src/hooks/Api.js
+++ b/src/hooks/Api.js
@@ -1,34 +1,107 @@
-import { useCallback } from "react";
-import { DEFAULT_TRANS_APIS } from "../config";
+import { useCallback, useMemo } from "react";
+import { DEFAULT_API_LIST, API_SPE_TYPES } from "../config";
 import { useSetting } from "./Setting";
 
-export function useApi(translator) {
+function useApiState() {
   const { setting, updateSetting } = useSetting();
-  const transApis = setting?.transApis || DEFAULT_TRANS_APIS;
+  const transApis = setting?.transApis || [];
 
-  const updateApi = useCallback(
-    async (obj) => {
-      const api = {
-        ...DEFAULT_TRANS_APIS[translator],
-        ...(transApis[translator] || {}),
-      };
-      Object.assign(transApis, { [translator]: { ...api, ...obj } });
-      await updateSetting({ transApis });
-    },
-    [translator, transApis, updateSetting]
+  return { transApis, updateSetting };
+}
+
+export function useApiList() {
+  const { transApis, updateSetting } = useApiState();
+
+  const userApis = useMemo(
+    () =>
+      transApis
+        .filter((api) => !API_SPE_TYPES.builtin.has(api.apiSlug))
+        .sort((a, b) => a.apiSlug.localeCompare(b.apiSlug)),
+    [transApis]
   );
 
-  const resetApi = useCallback(async () => {
-    Object.assign(transApis, { [translator]: DEFAULT_TRANS_APIS[translator] });
-    await updateSetting({ transApis });
-  }, [translator, transApis, updateSetting]);
+  const builtinApis = useMemo(
+    () => transApis.filter((api) => API_SPE_TYPES.builtin.has(api.apiSlug)),
+    [transApis]
+  );
 
-  return {
-    api: {
-      ...DEFAULT_TRANS_APIS[translator],
-      ...(transApis[translator] || {}),
+  const enabledApis = useMemo(
+    () => transApis.filter((api) => !api.isDisabled),
+    [transApis]
+  );
+
+  const addApi = useCallback(
+    (apiType) => {
+      const defaultApiOpt =
+        DEFAULT_API_LIST.find((da) => da.apiType === apiType) || {};
+      const uuid = crypto.randomUUID();
+      const apiSlug = `${apiType}_${crypto.randomUUID()}`;
+      const apiName = `${apiType}_${uuid.slice(0, 8)}`;
+      const newApi = {
+        ...defaultApiOpt,
+        apiSlug,
+        apiName,
+        apiType,
+      };
+      updateSetting((prev) => ({
+        ...prev,
+        transApis: [...(prev?.transApis || []), newApi],
+      }));
     },
-    updateApi,
-    resetApi,
-  };
+    [updateSetting]
+  );
+
+  const deleteApi = useCallback(
+    (apiSlug) => {
+      updateSetting((prev) => ({
+        ...prev,
+        transApis: (prev?.transApis || []).filter((api) => api.apiSlug !== apiSlug),
+      }));
+    },
+    [updateSetting]
+  );
+
+  return { transApis, userApis, builtinApis, enabledApis, addApi, deleteApi };
+}
+
+export function useApiItem(apiSlug) {
+  const { transApis, updateSetting } = useApiState();
+
+  const api = useMemo(
+    () => transApis.find((a) => a.apiSlug === apiSlug),
+    [transApis, apiSlug]
+  );
+
+  const update = useCallback(
+    (updateData) => {
+      updateSetting((prev) => ({
+        ...prev,
+        transApis: (prev?.transApis || []).map((item) =>
+          item.apiSlug === apiSlug ? { ...item, ...updateData, apiSlug } : item
+        ),
+      }));
+    },
+    [apiSlug, updateSetting]
+  );
+
+  const reset = useCallback(() => {
+    updateSetting((prev) => ({
+      ...prev,
+      transApis: (prev?.transApis || []).map((item) => {
+        if (item.apiSlug === apiSlug) {
+          const defaultApiOpt =
+            DEFAULT_API_LIST.find((da) => da.apiType === item.apiType) || {};
+          return {
+            ...defaultApiOpt,
+            apiSlug: item.apiSlug,
+            apiName: item.apiName,
+            apiType: item.apiType,
+          };
+        }
+        return item;
+      }),
+    }));
+  }, [apiSlug, updateSetting]);
+
+  return { api, update, reset };
 }
diff --git a/src/hooks/Audio.js b/src/hooks/Audio.js
index c3c9a1d..ba28c12 100644
--- a/src/hooks/Audio.js
+++ b/src/hooks/Audio.js
@@ -52,7 +52,7 @@ export function useTextAudio(text, lan = "uk", spd = 3) {
       try {
         setSrc(await apiBaiduTTS(text, lan, spd));
       } catch (err) {
-        kissLog(err, "baidu tts");
+        kissLog("baidu tts", err);
       }
     })();
   }, [text, lan, spd]);
diff --git a/src/hooks/ColorMode.js b/src/hooks/ColorMode.js
index 1ffbb0c..e99f689 100644
--- a/src/hooks/ColorMode.js
+++ b/src/hooks/ColorMode.js
@@ -11,8 +11,8 @@ export function useDarkMode() {
     updateSetting,
   } = useSetting();
 
-  const toggleDarkMode = useCallback(async () => {
-    await updateSetting({ darkMode: !darkMode });
+  const toggleDarkMode = useCallback(() => {
+    updateSetting({ darkMode: !darkMode });
   }, [darkMode, updateSetting]);
 
   return { darkMode, toggleDarkMode };
diff --git a/src/hooks/Confirm.js b/src/hooks/Confirm.js
new file mode 100644
index 0000000..1243c21
--- /dev/null
+++ b/src/hooks/Confirm.js
@@ -0,0 +1,97 @@
+import {
+  useState,
+  useContext,
+  createContext,
+  useCallback,
+  useRef,
+  useMemo,
+} from "react";
+import Dialog from "@mui/material/Dialog";
+import DialogActions from "@mui/material/DialogActions";
+import DialogContent from "@mui/material/DialogContent";
+import DialogContentText from "@mui/material/DialogContentText";
+import DialogTitle from "@mui/material/DialogTitle";
+import Button from "@mui/material/Button";
+import { useI18n } from "./I18n";
+
+const ConfirmContext = createContext(null);
+
+export function ConfirmProvider({ children }) {
+  const [dialogConfig, setDialogConfig] = useState(null);
+  const resolveRef = useRef(null);
+  const i18n = useI18n();
+
+  const translatedDefaults = useMemo(
+    () => ({
+      title: i18n("confirm_title", "Confirm"),
+      message: i18n("confirm_message", "Are you sure you want to proceed?"),
+      confirmText: i18n("confirm_action", "Confirm"),
+      cancelText: i18n("cancel_action", "Cancel"),
+    }),
+    [i18n]
+  );
+
+  const confirm = useCallback(
+    (config) => {
+      return new Promise((resolve) => {
+        setDialogConfig({ ...translatedDefaults, ...config });
+        resolveRef.current = resolve;
+      });
+    },
+    [translatedDefaults]
+  );
+
+  const handleClose = () => {
+    if (resolveRef.current) {
+      resolveRef.current(false);
+    }
+    setDialogConfig(null);
+  };
+
+  const handleConfirm = () => {
+    if (resolveRef.current) {
+      resolveRef.current(true);
+    }
+    setDialogConfig(null);
+  };
+
+  return (
+    <ConfirmContext.Provider value={confirm}>
+      {children}
+
+      <Dialog
+        open={!!dialogConfig}
+        onClose={handleClose}
+        aria-labelledby="confirm-dialog-title"
+        aria-describedby="confirm-dialog-description"
+      >
+        {dialogConfig && (
+          <>
+            <DialogTitle id="confirm-dialog-title">
+              {dialogConfig.title}
+            </DialogTitle>
+            <DialogContent>
+              <DialogContentText id="confirm-dialog-description">
+                {dialogConfig.message}
+              </DialogContentText>
+            </DialogContent>
+            <DialogActions>
+              <Button onClick={handleClose}>{dialogConfig.cancelText}</Button>
+              <Button onClick={handleConfirm} color="primary" autoFocus>
+                {dialogConfig.confirmText}
+              </Button>
+            </DialogActions>
+          </>
+        )}
+      </Dialog>
+    </ConfirmContext.Provider>
+  );
+}
+
+export function useConfirm() {
+  const context = useContext(ConfirmContext);
+  if (!context) {
+    throw new Error("useConfirm must be used within a ConfirmProvider");
+  }
+  return context;
+}
diff --git a/src/hooks/DebouncedCallback.js b/src/hooks/DebouncedCallback.js
new file mode 100644
index 0000000..9f2daf4
--- /dev/null
+++ b/src/hooks/DebouncedCallback.js
@@ -0,0 +1,17 @@
+import { useMemo, useEffect, useRef } from "react";
+import { debounce } from "../libs/utils";
+
+export function useDebouncedCallback(callback, delay) {
+  const callbackRef = useRef(callback);
+
+  useEffect(() => {
+    callbackRef.current = callback;
+  }, [callback]);
+
+  const debouncedCallback = useMemo(
+    () => debounce((...args) => callbackRef.current(...args), delay),
+    [delay]
+  );
+
+  return debouncedCallback;
+}
diff --git a/src/hooks/Fab.js b/src/hooks/Fab.js
index 36220a4..0d82e20 100644
--- a/src/hooks/Fab.js
+++ b/src/hooks/Fab.js
@@ -1,11 +1,13 @@
 import { STOKEY_FAB } from "../config";
 import { useStorage } from "./Storage";
 
+const DEFAULT_FAB = {};
+
 /**
  * fab hook
  * @returns
  */
 export function useFab() {
-  const { data, update } = useStorage(STOKEY_FAB);
+  const { data, update } = useStorage(STOKEY_FAB, DEFAULT_FAB);
   return { fab: data, updateFab: update };
 }
diff --git a/src/hooks/FavWords.js b/src/hooks/FavWords.js
index edfd361..4406116 100644
--- a/src/hooks/FavWords.js
+++ b/src/hooks/FavWords.js
@@ -1,68 +1,55 @@
-import { KV_WORDS_KEY } from "../config";
-import { useCallback, useEffect, useState } from "react";
-import { trySyncWords } from "../libs/sync";
-import { getWordsWithDefault, setWords } from "../libs/storage";
-import { useSyncMeta } from "./Sync";
-import { kissLog } from "../libs/log";
+import { STOKEY_WORDS, KV_WORDS_KEY } from "../config";
+import { useCallback, useMemo } from "react";
+import { useStorage } from "./Storage";
+
+const DEFAULT_FAVWORDS = {};
 
 export function useFavWords() {
-  const [loading, setLoading] = useState(false);
-  const [favWords, setFavWords] = useState({});
-  const { updateSyncMeta } = useSyncMeta();
+  const { data: favWords, save } = useStorage(
+    STOKEY_WORDS,
+    DEFAULT_FAVWORDS,
+    KV_WORDS_KEY
+  );
 
   const toggleFav = useCallback(
-    async (word) => {
-      const favs = { ...favWords };
-      if (favs[word]) {
+    (word) => {
+      save((prev) => {
+        if (!prev[word]) {
+          return { ...prev, [word]: { createdAt: Date.now() } };
+        }
+
+        const favs = { ...prev };
         delete favs[word];
-      } else {
-        favs[word] = { createdAt: Date.now() };
-      }
-      await setWords(favs);
-      await updateSyncMeta(KV_WORDS_KEY);
-      await trySyncWords();
-      setFavWords(favs);
+        return favs;
+      });
     },
-    [updateSyncMeta, favWords]
+    [save]
   );
 
   const mergeWords = useCallback(
-    async (newWords) => {
-      const favs = { ...favWords };
-      newWords.forEach((word) => {
-        if (!favs[word]) {
-          favs[word] = { createdAt: Date.now() };
-        }
-      });
-      await setWords(favs);
-      await updateSyncMeta(KV_WORDS_KEY);
-      await trySyncWords();
-      setFavWords(favs);
+    (words) => {
+      save((prev) => ({
+        ...words.reduce((acc, key) => {
+          acc[key] = { createdAt: Date.now() };
+          return acc;
+        }, {}),
+        ...prev,
+      }));
     },
-    [updateSyncMeta, favWords]
+    [save]
   );
 
-  const clearWords = useCallback(async () => {
-    await setWords({});
-    await updateSyncMeta(KV_WORDS_KEY);
-    await trySyncWords();
-    setFavWords({});
-  }, [updateSyncMeta]);
+  const clearWords = useCallback(() => {
+    save({});
+  }, [save]);
 
-  useEffect(() => {
-    (async () => {
-      try {
-        setLoading(true);
-        await trySyncWords();
-        const favWords = await getWordsWithDefault();
-        setFavWords(favWords);
-      } catch (err) {
-        kissLog(err, "query fav");
-      } finally {
-        setLoading(false);
-      }
-    })();
-  }, []);
+  const favList = useMemo(
+    () =>
+      Object.entries(favWords || {}).sort((a, b) => a[0].localeCompare(b[0])),
+    [favWords]
+  );
 
-  return { loading, favWords, toggleFav, mergeWords, clearWords };
+  const wordList = useMemo(() => favList.map(([word]) => word), [favList]);
+
+  return { favWords, favList, wordList, toggleFav, mergeWords, clearWords };
 }
diff --git a/src/hooks/InputRule.js b/src/hooks/InputRule.js
index 6d14450..1fddfbc 100644
--- a/src/hooks/InputRule.js
+++ b/src/hooks/InputRule.js
@@ -1,18 +1,10 @@
-import { useCallback } from "react";
 import { DEFAULT_INPUT_RULE } from "../config";
 import { useSetting } from "./Setting";
 
 export function useInputRule() {
-  const { setting, updateSetting } = useSetting();
+  const { setting, updateChild } = useSetting();
   const inputRule = setting?.inputRule || DEFAULT_INPUT_RULE;
-
-  const updateInputRule = useCallback(
-    async (obj) => {
-      Object.assign(inputRule, obj);
-      await updateSetting({ inputRule });
-    },
-    [inputRule, updateSetting]
-  );
+  const updateInputRule = updateChild("inputRule");
 
   return { inputRule, updateInputRule };
 }
diff --git a/src/hooks/Loading.js b/src/hooks/Loading.js
new file mode 100644
index 0000000..a51dc4f
--- /dev/null
+++ b/src/hooks/Loading.js
@@ -0,0 +1,16 @@
+import CircularProgress from "@mui/material/CircularProgress";
+import Link from "@mui/material/Link";
+import Divider from "@mui/material/Divider";
+
+export default function Loading() {
+  return (
+    <center>
+      <Divider>
+        <Link
+          href={process.env.REACT_APP_HOMEPAGE}
+        >{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
+      </Divider>
+      <CircularProgress />
+    </center>
+  );
+}
diff --git a/src/hooks/MouseHover.js b/src/hooks/MouseHover.js
index a2f682b..2dceaae 100644
--- a/src/hooks/MouseHover.js
+++ b/src/hooks/MouseHover.js
@@ -1,19 +1,11 @@
-import { useCallback } from "react";
 import { DEFAULT_MOUSE_HOVER_SETTING } from "../config";
 import { useSetting } from "./Setting";
 
 export function useMouseHoverSetting() {
-  const { setting, updateSetting } = useSetting();
+  const { setting, updateChild } = useSetting();
   const mouseHoverSetting =
     setting?.mouseHoverSetting || DEFAULT_MOUSE_HOVER_SETTING;
-
-  const updateMouseHoverSetting = useCallback(
-    async (obj) => {
-      Object.assign(mouseHoverSetting, obj);
-      await updateSetting({ mouseHoverSetting });
-    },
-    [mouseHoverSetting, updateSetting]
-  );
+  const updateMouseHoverSetting = updateChild("mouseHoverSetting");
 
   return { mouseHoverSetting, updateMouseHoverSetting };
 }
diff --git a/src/hooks/Rules.js b/src/hooks/Rules.js
index ca914db..9972d20 100644
--- a/src/hooks/Rules.js
+++ b/src/hooks/Rules.js
@@ -1,90 +1,88 @@
 import { STOKEY_RULES, DEFAULT_RULES, KV_RULES_KEY } from "../config";
 import { useStorage } from "./Storage";
-import { trySyncRules } from "../libs/sync";
 import { checkRules } from "../libs/rules";
 import { useCallback } from "react";
-import { useSyncMeta } from "./Sync";
 
 /**
  * 规则 hook
  * @returns
  */
 export function useRules() {
-  const { data: list, save } = useStorage(STOKEY_RULES, DEFAULT_RULES);
-  const { updateSyncMeta } = useSyncMeta();
-
-  const updateRules = useCallback(
-    async (rules) => {
-      await save(rules);
-      await updateSyncMeta(KV_RULES_KEY);
-      trySyncRules();
-    },
-    [save, updateSyncMeta]
+  const { data: list, save } = useStorage(
+    STOKEY_RULES,
+    DEFAULT_RULES,
+    KV_RULES_KEY
   );
 
   const add = useCallback(
-    async (rule) => {
-      const rules = [...list];
-      if (rule.pattern === "*") {
-        return;
-      }
-      if (rules.map((item) => item.pattern).includes(rule.pattern)) {
-        return;
-      }
-      rules.unshift(rule);
-      await updateRules(rules);
+    (rule) => {
+      save((prev) => {
+        if (
+          rule.pattern === "*" ||
+          prev.some((item) => item.pattern === rule.pattern)
+        ) {
+          return prev;
+        }
+        return [rule, ...prev];
+      });
     },
-    [list, updateRules]
+    [save]
   );
 
   const del = useCallback(
-    async (pattern) => {
-      let rules = [...list];
-      if (pattern === "*") {
-        return;
-      }
-      rules = rules.filter((item) => item.pattern !== pattern);
-      await updateRules(rules);
+    (pattern) => {
+      save((prev) => {
+        if (pattern === "*") {
+          return prev;
+        }
+        return prev.filter((item) => item.pattern !== pattern);
+      });
     },
-    [list, updateRules]
+    [save]
   );
 
-  const clear = useCallback(async () => {
-    let rules = [...list];
-    rules = rules.filter((item) => item.pattern === "*");
-    await updateRules(rules);
-  }, [list, updateRules]);
+  const clear = useCallback(() => {
+    save((prev) => prev.filter((item) => item.pattern === "*"));
+  }, [save]);
 
   const put = useCallback(
-    async (pattern, obj) => {
-      const rules = [...list];
-      if (pattern === "*") {
-        obj.pattern = "*";
-      }
-      const rule = rules.find((r) => r.pattern === pattern);
-      rule && Object.assign(rule, obj);
-      await updateRules(rules);
+    (pattern, obj) => {
+      save((prev) => {
+        if (
+          prev.some(
+            (item) => item.pattern === obj.pattern && item.pattern !== pattern
+          )
+        ) {
+          return prev;
+        }
+        return prev.map((item) =>
+          item.pattern === pattern ? { ...item, ...obj } : item
+        );
+      });
     },
-    [list, updateRules]
+    [save]
   );
 
   const merge = useCallback(
-    async (newRules) => {
-      const rules = [...list];
-      newRules = checkRules(newRules);
-      newRules.forEach((newRule) => {
-        const rule = rules.find(
-          (oldRule) => oldRule.pattern === newRule.pattern
-        );
-        if (rule) {
-          Object.assign(rule, newRule);
-        } else {
-          rules.unshift(newRule);
+    (rules) => {
+      save((prev) => {
+        const adds = checkRules(rules);
+        if (adds.length === 0) {
+          return prev;
         }
+
+        const map = new Map();
+        // 不进行深度合并
+        // [...prev, ...adds].forEach((item) => {
+        //   const k = item.pattern;
+        //   map.set(k, { ...(map.get(k) || {}), ...item });
+        // });
+        prev.forEach((item) => map.set(item.pattern, item));
+        adds.forEach((item) => map.set(item.pattern, item));
+        return [...map.values()];
       });
-      await updateRules(rules);
     },
-    [list, updateRules]
+    [save]
   );
 
   return { list, add, del, clear, put, merge };
diff --git a/src/hooks/Setting.js b/src/hooks/Setting.js
index 8167a42..f9b2957 100644
--- a/src/hooks/Setting.js
+++ b/src/hooks/Setting.js
@@ -1,51 +1,70 @@
+import { createContext, useCallback, useContext, useMemo } from "react";
+import Alert from "@mui/material/Alert";
 import { STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY } from "../config";
 import { useStorage } from "./Storage";
-import { trySyncSetting } from "../libs/sync";
-import { createContext, useCallback, useContext, useMemo } from "react";
-import { debounce } from "../libs/utils";
-import { useSyncMeta } from "./Sync";
+import { debounceSyncMeta } from "../libs/storage";
+import Loading from "./Loading";
 
 const SettingContext = createContext({
-  setting: null,
-  updateSetting: async () => {},
-  reloadSetting: async () => {},
+  setting: DEFAULT_SETTING,
+  updateSetting: () => {},
+  reloadSetting: () => {},
 });
 
 export function SettingProvider({ children }) {
-  const { data, update, reload } = useStorage(STOKEY_SETTING, DEFAULT_SETTING);
-  const { updateSyncMeta } = useSyncMeta();
-
-  const syncSetting = useMemo(
-    () =>
-      debounce(() => {
-        trySyncSetting();
-      }, [2000]),
-    []
-  );
+  const {
+    data: setting,
+    isLoading,
+    update,
+    reload,
+  } = useStorage(STOKEY_SETTING, DEFAULT_SETTING, KV_SETTING_KEY);
 
   const updateSetting = useCallback(
-    async (obj) => {
-      await update(obj);
-      await updateSyncMeta(KV_SETTING_KEY);
-      syncSetting();
+    (objOrFn) => {
+      update(objOrFn);
+      debounceSyncMeta(KV_SETTING_KEY);
     },
-    [update, syncSetting, updateSyncMeta]
+    [update]
   );
 
-  if (!data) {
-    return;
+  const updateChild = useCallback(
+    (key) => async (obj) => {
+      updateSetting((prev) => ({
+        ...prev,
+        [key]: { ...(prev?.[key] || {}), ...obj },
+      }));
+    },
+    [updateSetting]
+  );
+
+  const value = useMemo(
+    () => ({
+      setting,
+      updateSetting,
+      updateChild,
+      reloadSetting: reload,
+    }),
+    [setting, updateSetting, updateChild, reload]
+  );
+
+  if (isLoading) {
+    return <Loading />;
+  }
+
+  if (!setting) {
+    <center>
+      <Alert severity="error" sx={{ maxWidth: 600, margin: "60px auto" }}>
+        <p>数据加载出错,请刷新页面或卸载后重新安装。</p>
+        <p>
+          Data loading error, please refresh the page or uninstall and
+          reinstall.
+        </p>
+      </Alert>
+    </center>;
   }
 
   return (
-    <SettingContext.Provider
-      value={{
-        setting: data,
-        updateSetting,
-        reloadSetting: reload,
-      }}
-    >
-      {children}
-    </SettingContext.Provider>
+    <SettingContext.Provider value={value}>{children}</SettingContext.Provider>
   );
 }
 
diff --git a/src/hooks/Shortcut.js b/src/hooks/Shortcut.js
index e20fc32..1a1ce73 100644
--- a/src/hooks/Shortcut.js
+++ b/src/hooks/Shortcut.js
@@ -6,13 +6,14 @@ export function useShortcut(action) {
   const { setting, updateSetting } = useSetting();
   const shortcuts = setting?.shortcuts || DEFAULT_SHORTCUTS;
   const shortcut = shortcuts[action] || [];
-
   const setShortcut = useCallback(
-    async (val) => {
-      Object.assign(shortcuts, { [action]: val });
-      await updateSetting({ shortcuts });
+    (val) => {
+      updateSetting((prev) => ({
+        ...prev,
+        shortcuts: { ...(prev?.shortcuts || {}), [action]: val },
+      }));
     },
-    [action, shortcuts, updateSetting]
+    [action, updateSetting]
   );
 
   return { shortcut, setShortcut };
diff --git a/src/hooks/Storage.js b/src/hooks/Storage.js
index aa8a316..165e789 100644
--- a/src/hooks/Storage.js
+++ b/src/hooks/Storage.js
@@ -1,70 +1,144 @@
 import { useCallback, useEffect, useState } from "react";
 import { storage } from "../libs/storage";
 import { kissLog } from "../libs/log";
+import { syncData } from "../libs/sync";
+import { useDebouncedCallback } from "./DebouncedCallback";
 
 /**
+ * 用于将组件状态与 Storage 同步
  *
- * @param {*} key
- * @param {*} defaultVal 需为调用hook外的常量
- * @returns
+ * @param {string} key 用于在 Storage 中存取值的键
+ * @param {*} defaultVal 默认值。建议在组件外定义为常量。
+ * @param {string} [syncKey=""] 用于远端同步的可选键名
+ * @returns {{
+ * data: *,
+ * save: (valueOrFn: any | ((prevData: any) => any)) => void,
+ * update: (partialDataOrFn: object | ((prevData: object) => object)) => void,
+ * remove: () => Promise<void>,
+ * reload: () => Promise<void>
+ * }}
  */
-export function useStorage(key, defaultVal) {
-  const [loading, setLoading] = useState(false);
-  const [data, setData] = useState(null);
+export function useStorage(key, defaultVal = null, syncKey = "") {
+  const [isLoading, setIsLoading] = useState(true);
+  const [data, setData] = useState(defaultVal);
 
-  const save = useCallback(
-    async (val) => {
-      setData(val);
-      await storage.setObj(key, val);
-    },
-    [key]
-  );
+  // 首次加载数据
+  useEffect(() => {
+    let isMounted = true;
 
-  const update = useCallback(
-    async (obj) => {
-      setData((pre = {}) => ({ ...pre, ...obj }));
-      await storage.putObj(key, obj);
-    },
-    [key]
-  );
-
-  const remove = useCallback(async () => {
-    setData(null);
-    await storage.del(key);
-  }, [key]);
-
-  const reload = useCallback(async () => {
-    try {
-      setLoading(true);
-      const val = await storage.getObj(key);
-      if (val) {
-        setData(val);
+    const loadInitialData = async () => {
+      try {
+        const storedVal = await storage.getObj(key);
+        if (storedVal === undefined || storedVal === null) {
+          await storage.setObj(key, defaultVal);
+        } else if (isMounted) {
+          setData(storedVal);
+        }
+      } catch (err) {
+        kissLog(`storage load error for key: ${key}`, err);
+      } finally {
+        if (isMounted) {
+          setIsLoading(false);
+        }
       }
+    };
+
+    loadInitialData();
+
+    return () => {
+      isMounted = false;
+    };
+  }, [key, defaultVal]);
+
+  // 远端同步
+  const runSync = useCallback(async (keyToSync, valueToSync) => {
+    try {
+      const { value, isNew } = await syncData(keyToSync, valueToSync);
+      if (isNew) {
+        setData(value);
+      }
+    } catch (error) {
+      kissLog("Sync failed", keyToSync);
+    }
+  }, []);
+
+  const debouncedSync = useDebouncedCallback(runSync, 3000);
+
+  // 持久化
+  useEffect(() => {
+    if (isLoading) {
+      return;
+    }
+
+    if (data === null) {
+      return;
+    }
+
+    storage.setObj(key, data).catch((err) => {
+      kissLog(`storage save error for key: ${key}`, err);
+    });
+
+    // 触发远端同步
+    if (syncKey) {
+      debouncedSync(syncKey, data);
+    }
+  }, [key, syncKey, isLoading, data, debouncedSync]);
+
+  /**
+   * 全量替换状态值
+   * @param {any | ((prevData: any) => any)} valueOrFn 新的值或一个返回新值的函数。
+   */
+  const save = useCallback((valueOrFn) => {
+    // kissLog("save storage:", valueOrFn);
+    setData((prevData) =>
+      typeof valueOrFn === "function" ? valueOrFn(prevData) : valueOrFn
+    );
+  }, []);
+
+  /**
+   * 合并对象到当前状态(假设状态是一个对象)。
+   * @param {object | ((prevData: object) => object)} partialDataOrFn 要合并的对象或一个返回该对象的函数。
+   */
+  const update = useCallback((partialDataOrFn) => {
+    // kissLog("update storage:", partialDataOrFn);
+    setData((prevData) => {
+      const partialData =
+        typeof partialDataOrFn === "function"
+          ? partialDataOrFn(prevData)
+          : partialDataOrFn;
+      // 确保 preData 是一个对象,避免展开 null 或 undefined
+      const baseObj =
+        typeof prevData === "object" && prevData !== null ? prevData : {};
+      return { ...baseObj, ...partialData };
+    });
+  }, []);
+
+  /**
+   * 从 Storage 中删除该值,并将状态重置为 null。
+   */
+  const remove = useCallback(async () => {
+    // kissLog("remove storage:");
+    try {
+      await storage.del(key);
+      setData(null);
     } catch (err) {
-      kissLog(err, "storage reload");
-    } finally {
-      setLoading(false);
+      kissLog(`storage remove error for key: ${key}`, err);
     }
   }, [key]);
 
-  useEffect(() => {
-    (async () => {
-      try {
-        setLoading(true);
-        const val = await storage.getObj(key);
-        if (val) {
-          setData(val);
-        } else if (defaultVal) {
-          setData(defaultVal);
-          await storage.setObj(key, defaultVal);
-        }
-      } catch (err) {
-        kissLog(err, "storage load");
-      } finally {
-        setLoading(false);
-      }
-    })();
+  /**
+   * 从 Storage 重新加载数据以覆盖当前状态。
+   */
+  const reload = useCallback(async () => {
+    // kissLog("reload storage:");
+    try {
+      const storedVal = await storage.getObj(key);
+      setData(storedVal ?? defaultVal);
+    } catch (err) {
+      kissLog(`storage reload error for key: ${key}`, err);
+      // setData(defaultVal);
+    }
   }, [key, defaultVal]);
 
-  return { data, save, update, remove, reload, loading };
+  return { data, save, update, remove, reload, isLoading };
 }
diff --git a/src/hooks/SubRules.js b/src/hooks/SubRules.js
index ee74e2c..642e8ce 100644
--- a/src/hooks/SubRules.js
+++ b/src/hooks/SubRules.js
@@ -2,7 +2,6 @@ import { DEFAULT_SUBRULES_LIST, DEFAULT_OW_RULE } from "../config";
 import { useSetting } from "./Setting";
 import { useCallback, useEffect, useMemo, useState } from "react";
 import { loadOrFetchSubRules } from "../libs/subRules";
-import { delSubRules } from "../libs/storage";
 import { kissLog } from "../libs/log";
 
 /**
@@ -19,50 +18,36 @@ export function useSubRules() {
   const selectedUrl = selectedSub.url;
 
   const selectSub = useCallback(
-    async (url) => {
-      const subrulesList = [...list];
-      subrulesList.forEach((item) => {
-        if (item.url === url) {
-          item.selected = true;
-        } else {
-          item.selected = false;
-        }
-      });
-      await updateSetting({ subrulesList });
+    (url) => {
+      updateSetting((prev) => ({
+        ...prev,
+        subrulesList: prev.subrulesList.map((item) => ({
+          ...item,
+          selected: item.url === url,
+        })),
+      }));
     },
-    [list, updateSetting]
-  );
-
-  const updateSub = useCallback(
-    async (url, obj) => {
-      const subrulesList = [...list];
-      subrulesList.forEach((item) => {
-        if (item.url === url) {
-          Object.assign(item, obj);
-        }
-      });
-      await updateSetting({ subrulesList });
-    },
-    [list, updateSetting]
+    [updateSetting]
   );
 
   const addSub = useCallback(
-    async (url) => {
-      const subrulesList = [...list];
-      subrulesList.push({ url, selected: false });
-      await updateSetting({ subrulesList });
+    (url) => {
+      updateSetting((prev) => ({
+        ...prev,
+        subrulesList: [...prev.subrulesList, { url, selected: false }],
+      }));
     },
-    [list, updateSetting]
+    [updateSetting]
   );
 
   const delSub = useCallback(
-    async (url) => {
-      let subrulesList = [...list];
-      subrulesList = subrulesList.filter((item) => item.url !== url);
-      await updateSetting({ subrulesList });
-      await delSubRules(url);
+    (url) => {
+      updateSetting((prev) => ({
+        ...prev,
+        subrulesList: prev.subrulesList.filter((item) => item.url !== url),
+      }));
     },
-    [list, updateSetting]
+    [updateSetting]
   );
 
   useEffect(() => {
@@ -73,7 +58,7 @@ export function useSubRules() {
           const rules = await loadOrFetchSubRules(selectedUrl);
           setSelectedRules(rules);
         } catch (err) {
-          kissLog(err, "loadOrFetchSubRules");
+          kissLog("loadOrFetchSubRules", err);
         } finally {
           setLoading(false);
         }
@@ -84,7 +69,6 @@ export function useSubRules() {
   return {
     subList: list,
     selectSub,
-    updateSub,
     addSub,
     delSub,
     selectedSub,
@@ -100,15 +84,9 @@ export function useSubRules() {
  * @returns
  */
 export function useOwSubRule() {
-  const { setting, updateSetting } = useSetting();
-  const { owSubrule = DEFAULT_OW_RULE } = setting;
-
-  const updateOwSubrule = useCallback(
-    async (obj) => {
-      await updateSetting({ owSubrule: { ...owSubrule, ...obj } });
-    },
-    [owSubrule, updateSetting]
-  );
+  const { setting, updateChild } = useSetting();
+  const owSubrule = setting?.owSubrule || DEFAULT_OW_RULE;
+  const updateOwSubrule = updateChild("owSubrule");
 
   return { owSubrule, updateOwSubrule };
 }
diff --git a/src/hooks/Sync.js b/src/hooks/Sync.js
index 63e1948..66ce292 100644
--- a/src/hooks/Sync.js
+++ b/src/hooks/Sync.js
@@ -1,4 +1,4 @@
-import { useCallback } from "react";
+import { useCallback, useMemo } from "react";
 import { STOKEY_SYNC, DEFAULT_SYNC } from "../config";
 import { useStorage } from "./Storage";
 
@@ -16,15 +16,24 @@ export function useSync() {
  * @returns
  */
 export function useSyncMeta() {
-  const { sync, updateSync } = useSync();
+  const { updateSync } = useSync();
+
   const updateSyncMeta = useCallback(
-    async (key) => {
-      const syncMeta = sync?.syncMeta || {};
-      syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
-      await updateSync({ syncMeta });
+    (key) => {
+      updateSync((prevSync) => {
+        const newSyncMeta = {
+          ...(prevSync?.syncMeta || {}),
+          [key]: {
+            ...(prevSync?.syncMeta?.[key] || {}),
+            updateAt: Date.now(),
+          },
+        };
+        return { syncMeta: newSyncMeta };
+      });
     },
-    [sync?.syncMeta, updateSync]
+    [updateSync]
   );
+
   return { updateSyncMeta };
 }
 
@@ -37,25 +46,32 @@ export function useSyncCaches() {
   const { sync, updateSync, reloadSync } = useSync();
 
   const updateDataCache = useCallback(
-    async (url) => {
-      const dataCaches = sync?.dataCaches || {};
-      dataCaches[url] = Date.now();
-      await updateSync({ dataCaches });
+    (url) => {
+      updateSync((prevSync) => ({
+        dataCaches: {
+          ...(prevSync?.dataCaches || {}),
+          [url]: Date.now(),
+        },
+      }));
     },
-    [sync, updateSync]
+    [updateSync]
   );
 
   const deleteDataCache = useCallback(
-    async (url) => {
-      const dataCaches = sync?.dataCaches || {};
-      delete dataCaches[url];
-      await updateSync({ dataCaches });
+    (url) => {
+      updateSync((prevSync) => {
+        const newDataCaches = { ...(prevSync?.dataCaches || {}) };
+        delete newDataCaches[url];
+        return { dataCaches: newDataCaches };
+      });
     },
-    [sync, updateSync]
+    [updateSync]
   );
 
+  const dataCaches = useMemo(() => sync?.dataCaches || {}, [sync?.dataCaches]);
+
   return {
-    dataCaches: sync?.dataCaches || {},
+    dataCaches,
     updateDataCache,
     deleteDataCache,
     reloadSync,
diff --git a/src/hooks/Tranbox.js b/src/hooks/Tranbox.js
index 119dbd8..ecd1e42 100644
--- a/src/hooks/Tranbox.js
+++ b/src/hooks/Tranbox.js
@@ -1,18 +1,10 @@
-import { useCallback } from "react";
 import { DEFAULT_TRANBOX_SETTING } from "../config";
 import { useSetting } from "./Setting";
 
 export function useTranbox() {
-  const { setting, updateSetting } = useSetting();
+  const { setting, updateChild } = useSetting();
   const tranboxSetting = setting?.tranboxSetting || DEFAULT_TRANBOX_SETTING;
-
-  const updateTranbox = useCallback(
-    async (obj) => {
-      Object.assign(tranboxSetting, obj);
-      await updateSetting({ tranboxSetting });
-    },
-    [tranboxSetting, updateSetting]
-  );
+  const updateTranbox = updateChild("tranboxSetting");
 
   return { tranboxSetting, updateTranbox };
 }
diff --git a/src/hooks/Translate.js b/src/hooks/Translate.js
deleted file mode 100644
index c71f11f..0000000
--- a/src/hooks/Translate.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import { useEffect } from "react";
-import { useState } from "react";
-import { tryDetectLang } from "../libs";
-import { apiTranslate } from "../apis";
-import { DEFAULT_TRANS_APIS } from "../config";
-import { kissLog } from "../libs/log";
-
-/**
- * 翻译hook
- * @param {*} q
- * @param {*} rule
- * @param {*} setting
- * @returns
- */
-export function useTranslate(q, rule, setting, docInfo) {
-  const [text, setText] = useState("");
-  const [loading, setLoading] = useState(true);
-  const [sameLang, setSamelang] = useState(false);
-
-  const { translator, fromLang, toLang, detectRemote, skipLangs = [] } = rule;
-
-  useEffect(() => {
-    (async () => {
-      try {
-        if (!q.replace(/\[(\d+)\]/g, "").trim()) {
-          setText(q);
-          setSamelang(false);
-          return;
-        }
-
-        let deLang = "";
-        if (fromLang === "auto") {
-          deLang = await tryDetectLang(
-            q,
-            detectRemote === "true",
-            setting.langDetector
-          );
-        }
-        if (deLang && (toLang.includes(deLang) || skipLangs.includes(deLang))) {
-          setSamelang(true);
-        } else {
-          const [trText, isSame] = await apiTranslate({
-            translator,
-            text: q,
-            fromLang,
-            toLang,
-            apiSetting: {
-              ...DEFAULT_TRANS_APIS[translator],
-              ...(setting.transApis[translator] || {}),
-            },
-            docInfo,
-          });
-          setText(trText);
-          setSamelang(isSame);
-        }
-      } catch (err) {
-        kissLog(err, "translate");
-      } finally {
-        setLoading(false);
-      }
-    })();
-  }, [
-    q,
-    translator,
-    fromLang,
-    toLang,
-    detectRemote,
-    skipLangs,
-    setting,
-    docInfo,
-  ]);
-
-  return { text, sameLang, loading };
-}
diff --git a/src/libs/auth.js b/src/libs/auth.js
index ca2301c..d4e01e4 100644
--- a/src/libs/auth.js
+++ b/src/libs/auth.js
@@ -6,7 +6,7 @@ const parseMSToken = (token) => {
   try {
     return JSON.parse(atob(token.split(".")[1])).exp;
   } catch (err) {
-    kissLog(err, "parseMSToken");
+    kissLog("parseMSToken", err);
   }
   return 0;
 };
diff --git a/src/libs/batchQueue.js b/src/libs/batchQueue.js
index 224015d..85ef89c 100644
--- a/src/libs/batchQueue.js
+++ b/src/libs/batchQueue.js
@@ -131,17 +131,15 @@ const queueMap = new Map();
 
 /**
  * 获取批处理实例
- * @param {*} translator
- * @returns
  */
-export const getBatchQueue = (args, opts) => {
-  const { translator, from, to } = args;
-  const key = `${translator}_${from}_${to}`;
+export const getBatchQueue = (args) => {
+  const { from, to, apiSetting } = args;
+  const key = `${apiSetting.apiSlug}_${from}_${to}`;
   if (queueMap.has(key)) {
     return queueMap.get(key);
   }
 
-  const queue = BatchQueue(args, opts);
+  const queue = BatchQueue(args, apiSetting);
   queueMap.set(key, queue);
   return queue;
 };
diff --git a/src/libs/browser.js b/src/libs/browser.js
index 0be44cb..2745e27 100644
--- a/src/libs/browser.js
+++ b/src/libs/browser.js
@@ -8,7 +8,7 @@ function _browser() {
   try {
     return require("webextension-polyfill");
   } catch (err) {
-    // kissLog(err, "browser");
+    // kissLog("browser", err);
   }
 }
 
diff --git a/src/libs/cache.js b/src/libs/cache.js
index 20d6ca8..28f19df 100644
--- a/src/libs/cache.js
+++ b/src/libs/cache.js
@@ -43,7 +43,7 @@ export const getHttpCache = async (input, init) => {
       return await parseResponse(res);
     }
   } catch (err) {
-    kissLog(err, "get cache");
+    kissLog("get cache", err);
   }
   return null;
 };
@@ -54,7 +54,12 @@ export const getHttpCache = async (input, init) => {
  * @param {*} init
  * @param {*} data
  */
-export const putHttpCache = async (input, init, data) => {
+export const putHttpCache = async (
+  input,
+  init,
+  data,
+  maxAge = DEFAULT_CACHE_TIMEOUT // todo: 从设置里面读取最大缓存时间
+) => {
   try {
     const req = await newCacheReq(input, init);
     const cache = await caches.open(CACHE_NAME);
@@ -62,13 +67,13 @@ export const putHttpCache = async (input, init, data) => {
       status: 200,
       headers: {
         "Content-Type": "application/json",
-        "Cache-Control": `max-age=${DEFAULT_CACHE_TIMEOUT}`,
+        "Cache-Control": `max-age=${maxAge}`,
       },
     });
-    // res.headers.set("Cache-Control", `max-age=${DEFAULT_CACHE_TIMEOUT}`);
+    // res.headers.set("Cache-Control", `max-age=${maxAge}`);
     await cache.put(req, res);
   } catch (err) {
-    kissLog(err, "put cache");
+    kissLog("put cache", err);
   }
 };
 
diff --git a/src/libs/fetch.js b/src/libs/fetch.js
index 7b19228..bdad2d7 100644
--- a/src/libs/fetch.js
+++ b/src/libs/fetch.js
@@ -57,7 +57,7 @@ export const fetchPatcher = async (input, init = {}, opts) => {
     try {
       timeout = (await getSettingWithDefault()).httpTimeout;
     } catch (err) {
-      kissLog(err, "getSettingWithDefault");
+      kissLog("getSettingWithDefault", err);
     }
   }
   if (!timeout) {
diff --git a/src/libs/index.js b/src/libs/index.js
index 36759b1..2d2e288 100644
--- a/src/libs/index.js
+++ b/src/libs/index.js
@@ -28,7 +28,7 @@ export const tryClearCaches = async () => {
   try {
     caches.delete(CACHE_NAME);
   } catch (err) {
-    kissLog(err, "clean caches");
+    kissLog("clean caches", err);
   }
 };
 
@@ -48,7 +48,7 @@ export const tryDetectLang = async (
     try {
       lang = await langdetectMap[langDetector](q);
     } catch (err) {
-      kissLog(err, "detect lang remote");
+      kissLog("detect lang remote", err);
     }
   }
 
@@ -57,7 +57,7 @@ export const tryDetectLang = async (
       const res = await browser?.i18n?.detectLanguage(q);
       lang = res?.languages?.[0]?.language;
     } catch (err) {
-      kissLog(err, "detect lang local");
+      kissLog("detect lang local", err);
     }
   }
 
diff --git a/src/libs/inputTranslate.js b/src/libs/inputTranslate.js
index 5aed7b9..54a6172 100644
--- a/src/libs/inputTranslate.js
+++ b/src/libs/inputTranslate.js
@@ -1,8 +1,8 @@
 import {
   DEFAULT_INPUT_RULE,
-  DEFAULT_TRANS_APIS,
   DEFAULT_INPUT_SHORTCUT,
   OPT_LANGS_LIST,
+  DEFAULT_API_SETTING,
 } from "../config";
 import { genEventName, removeEndchar, matchInputStr, sleep } from "./utils";
 import { stepShortcutRegister } from "./shortcut";
@@ -87,7 +87,7 @@ export default function inputTranslate({
   inputRule: {
     transOpen,
     triggerShortcut,
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     triggerCount,
@@ -100,7 +100,8 @@ export default function inputTranslate({
     return;
   }
 
-  const apiSetting = transApis?.[translator] || DEFAULT_TRANS_APIS[translator];
+  const apiSetting =
+    transApis.find((api) => api.apiSlug === apiSlug) || DEFAULT_API_SETTING;
   if (triggerShortcut.length === 0) {
     triggerShortcut = DEFAULT_INPUT_SHORTCUT;
     triggerCount = 1;
@@ -156,7 +157,7 @@ export default function inputTranslate({
         addLoading(node, loadingId);
 
         const [trText, isSame] = await apiTranslate({
-          translator,
+          apiSlug,
           text,
           fromLang,
           toLang,
@@ -188,7 +189,7 @@ export default function inputTranslate({
           collapseToEnd(node);
         }
       } catch (err) {
-        kissLog(err, "translate input");
+        kissLog("translate input", err);
       } finally {
         removeLoading(node, loadingId);
       }
diff --git a/src/libs/log.js b/src/libs/log.js
index e70dfb9..0b04577 100644
--- a/src/libs/log.js
+++ b/src/libs/log.js
@@ -1,12 +1,126 @@
-/**
- * 日志函数
- * @param {*} msg
- * @param {*} type
- */
-export const kissLog = (msg, type) => {
-  let prefix = `[KISS-Translator]`;
-  if (type) {
-    prefix += `[${type}]`;
-  }
-  console.log(`${prefix} ${msg}`);
+// 定义日志级别
+export const LogLevel = {
+  DEBUG: { value: 0, name: "DEBUG", color: "#6495ED" }, // 宝蓝色
+  INFO: { value: 1, name: "INFO", color: "#4CAF50" }, // 绿色
+  WARN: { value: 2, name: "WARN", color: "#FFC107" }, // 琥珀色
+  ERROR: { value: 3, name: "ERROR", color: "#F44336" }, // 红色
+  SILENT: { value: 4, name: "SILENT" }, // 特殊级别,用于关闭所有日志
 };
+
+class Logger {
+  /**
+   * @param {object} [options={}] 配置选项
+   * @param {LogLevel} [options.level=LogLevel.INFO]  要显示的最低日志级别
+   * @param {string}   [options.prefix='App']         日志前缀,用于区分模块
+   */
+  constructor(options = {}) {
+    this.config = {
+      level: options.level || LogLevel.INFO,
+      prefix: options.prefix || "KISS-Translator",
+    };
+  }
+
+  /**
+   * 动态设置日志级别
+   * @param {LogLevel} level - 新的日志级别
+   */
+  setLevel(level) {
+    if (level && typeof level.value === "number") {
+      this.config.level = level;
+      console.log(`[${this.config.prefix}] Log level set to ${level.name}`);
+    }
+  }
+
+  /**
+   * 核心日志记录方法
+   * @private
+   * @param {LogLevel} level - 当前消息的日志级别
+   * @param {...any} args - 要记录的多个参数,可以是任何类型
+   */
+  _log(level, ...args) {
+    // 如果当前级别低于配置的最低级别,则不打印
+    if (level.value < this.config.level.value) {
+      return;
+    }
+
+    const timestamp = new Date().toISOString();
+    const prefixStr = `[${this.config.prefix}]`;
+    const levelStr = `[${level.name}]`;
+
+    // 判断是否在浏览器环境并且浏览器支持 console 样式
+    const isBrowser =
+      typeof window !== "undefined" && typeof window.document !== "undefined";
+
+    if (isBrowser) {
+      // 在浏览器中使用颜色高亮
+      const consoleMethod = this._getConsoleMethod(level);
+      consoleMethod(
+        `%c${timestamp} %c${prefixStr} %c${levelStr}`,
+        "color: gray; font-weight: lighter;", // 时间戳样式
+        "color: #7c57e0; font-weight: bold;", // 前缀样式 (紫色)
+        `color: ${level.color}; font-weight: bold;`, // 日志级别样式
+        ...args
+      );
+    } else {
+      // 在 Node.js 或不支持样式的环境中,输出纯文本
+      const consoleMethod = this._getConsoleMethod(level);
+      consoleMethod(timestamp, prefixStr, levelStr, ...args);
+    }
+  }
+
+  /**
+   * 根据日志级别获取对应的 console 方法
+   * @private
+   */
+  _getConsoleMethod(level) {
+    switch (level) {
+      case LogLevel.ERROR:
+        return console.error;
+      case LogLevel.WARN:
+        return console.warn;
+      case LogLevel.INFO:
+        return console.info;
+      default:
+        return console.log;
+    }
+  }
+
+  /**
+   * 记录 DEBUG 级别的日志
+   * @param {...any} args
+   */
+  debug(...args) {
+    this._log(LogLevel.DEBUG, ...args);
+  }
+
+  /**
+   * 记录 INFO 级别的日志
+   * @param {...any} args
+   */
+  info(...args) {
+    this._log(LogLevel.INFO, ...args);
+  }
+
+  /**
+   * 记录 WARN 级别的日志
+   * @param {...any} args
+   */
+  warn(...args) {
+    this._log(LogLevel.WARN, ...args);
+  }
+
+  /**
+   * 记录 ERROR 级别的日志
+   * @param {...any} args
+   */
+  error(...args) {
+    this._log(LogLevel.ERROR, ...args);
+  }
+}
+
+const isDevelopment =
+  typeof process === "undefined" || process.env.NODE_ENV !== "development";
+const defaultLevel = isDevelopment ? LogLevel.DEBUG : LogLevel.INFO;
+
+export const logger = new Logger({ level: defaultLevel });
+export const kissLog = logger.info.bind(logger);
diff --git a/src/libs/pool.js b/src/libs/pool.js
index 482ba95..bcdb542 100644
--- a/src/libs/pool.js
+++ b/src/libs/pool.js
@@ -70,7 +70,7 @@ class TaskPool {
       const res = await fn(args);
       resolve(res);
     } catch (err) {
-      kissLog(err, "task");
+      kissLog("task pool", err);
       if (retry < this.#maxRetry) {
         setTimeout(() => {
           this.#pool.unshift({ ...task, retry: retry + 1 }); // unshift 保证重试任务优先
diff --git a/src/libs/rules.js b/src/libs/rules.js
index 3bac69d..953b01e 100644
--- a/src/libs/rules.js
+++ b/src/libs/rules.js
@@ -2,12 +2,12 @@ import { matchValue, type, isMatch } from "./utils";
 import {
   GLOBAL_KEY,
   REMAIN_KEY,
-  OPT_TRANS_ALL,
   OPT_STYLE_ALL,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
   // OPT_TIMING_ALL,
   GLOBLA_RULE,
+  DEFAULT_API_TYPE,
 } from "../config";
 import { loadOrFetchSubRules } from "./subRules";
 import { getRulesWithDefault, setRules } from "./storage";
@@ -50,7 +50,7 @@ export const matchRule = async (
         rules.splice(-1, 0, ...subRules);
       }
     } catch (err) {
-      kissLog(err, "load injectRules");
+      kissLog("load injectRules", err);
     }
   }
 
@@ -86,7 +86,7 @@ export const matchRule = async (
   });
 
   [
-    "translator",
+    "apiSlug",
     "fromLang",
     "toLang",
     "transOpen",
@@ -158,7 +158,7 @@ export const checkRules = (rules) => {
         parentStyle,
         injectJs,
         injectCss,
-        translator,
+        apiSlug,
         fromLang,
         toLang,
         textStyle,
@@ -193,7 +193,7 @@ export const checkRules = (rules) => {
         injectCss: type(injectCss) === "string" ? injectCss : "",
         bgColor: type(bgColor) === "string" ? bgColor : "",
         textDiyStyle: type(textDiyStyle) === "string" ? textDiyStyle : "",
-        translator: matchValue([GLOBAL_KEY, ...OPT_TRANS_ALL], translator),
+        apiSlug: apiSlug?.trim() || DEFAULT_API_TYPE,
         fromLang: matchValue([GLOBAL_KEY, ...fromLangs], fromLang),
         toLang: matchValue([GLOBAL_KEY, ...toLangs], toLang),
         textStyle: matchValue([GLOBAL_KEY, ...OPT_STYLE_ALL], textStyle),
diff --git a/src/libs/shadowroot.js b/src/libs/shadowroot.js
index 22461c7..82548bb 100644
--- a/src/libs/shadowroot.js
+++ b/src/libs/shadowroot.js
@@ -33,7 +33,7 @@ export class ShadowRootMonitor {
         try {
           monitorInstance.callback(shadowRoot);
         } catch (error) {
-          kissLog(error, "Error in ShadowRootMonitor callback");
+          kissLog("Error in ShadowRootMonitor callback", error);
         }
       }
       return shadowRoot;
diff --git a/src/libs/shortcut.js b/src/libs/shortcut.js
index 0396402..a0432e4 100644
--- a/src/libs/shortcut.js
+++ b/src/libs/shortcut.js
@@ -1,112 +1,106 @@
 import { isSameSet } from "./utils";
 
 /**
- * 键盘快捷键监听
- * @param {*} fn
- * @param {*} target
- * @param {*} timeout
- * @returns
+ * 键盘快捷键监听器
+ * @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyDown - Keydown 回调
+ * @param {(pressedKeys: Set<string>, event: KeyboardEvent) => void} onKeyUp - Keyup 回调
+ * @param {EventTarget} target - 监听的目标元素
+ * @returns {() => void} - 用于注销监听的函数
  */
-export const shortcutListener = (fn, target = document, timeout = 3000) => {
-  const allkeys = new Set();
-  const curkeys = new Set();
-  let timer = null;
+export const shortcutListener = (
+  onKeyDown = () => {},
+  onKeyUp = () => {},
+  target = document
+) => {
+  const pressedKeys = new Set();
 
-  const handleKeydown = (e) => {
-    timer && clearTimeout(timer);
-    timer = setTimeout(() => {
-      allkeys.clear();
-      curkeys.clear();
-      clearTimeout(timer);
-      timer = null;
-    }, timeout);
-
-    if (e.code) {
-      allkeys.add(e.code);
-      curkeys.add(e.code);
-      fn([...curkeys], [...allkeys]);
-    }
+  const handleKeyDown = (e) => {
+    if (pressedKeys.has(e.code)) return;
+    pressedKeys.add(e.code);
+    onKeyDown(new Set(pressedKeys), e);
   };
 
-  const handleKeyup = (e) => {
-    curkeys.delete(e.code);
-    if (curkeys.size === 0) {
-      fn([...curkeys], [...allkeys]);
-      allkeys.clear();
-    }
+  const handleKeyUp = (e) => {
+    // onKeyUp 应该在 key 从集合中移除前触发,以便判断组合键
+    onKeyUp(new Set(pressedKeys), e);
+    pressedKeys.delete(e.code);
   };
 
-  target.addEventListener("keydown", handleKeydown, true);
-  target.addEventListener("keyup", handleKeyup, true);
+  target.addEventListener("keydown", handleKeyDown);
+  target.addEventListener("keyup", handleKeyUp);
+
   return () => {
-    if (timer) {
-      clearTimeout(timer);
-      timer = null;
-    }
-    target.removeEventListener("keydown", handleKeydown);
-    target.removeEventListener("keyup", handleKeyup);
+    target.removeEventListener("keydown", handleKeyDown);
+    target.removeEventListener("keyup", handleKeyUp);
+    pressedKeys.clear();
   };
 };
 
 /**
  * 注册键盘快捷键
- * @param {*} targetKeys
- * @param {*} fn
- * @param {*} target
- * @returns
+ * @param {string[]} targetKeys - 目标快捷键数组
+ * @param {() => void} fn - 匹配成功后执行的回调
+ * @param {EventTarget} target - 监听目标
+ * @returns {() => void} - 注销函数
  */
 export const shortcutRegister = (targetKeys = [], fn, target = document) => {
-  return shortcutListener((curkeys) => {
-    if (
-      targetKeys.length > 0 &&
-      isSameSet(new Set(targetKeys), new Set(curkeys))
-    ) {
+  if (targetKeys.length === 0) return () => {};
+
+  const targetKeySet = new Set(targetKeys);
+  const onKeyDown = (pressedKeys, event) => {
+    if (targetKeySet.size > 0 && isSameSet(targetKeySet, pressedKeys)) {
+      event.preventDefault();
+      event.stopPropagation();
       fn();
     }
-  }, target);
+  };
+  const onKeyUp = () => {};
+
+  return shortcutListener(onKeyDown, onKeyUp, target);
+};
+
+/**
+ * 高阶函数:为目标函数增加计次和超时重置功能
+ * @param {() => void} fn - 需要被包装的函数
+ * @param {number} step - 需要触发的次数
+ * @param {number} timeout - 超时毫秒数
+ * @returns {() => void} - 包装后的新函数
+ */
+const withStepCounter = (fn, step, timeout) => {
+  let count = 0;
+  let timer = null;
+
+  return () => {
+    timer && clearTimeout(timer);
+    timer = setTimeout(() => {
+      count = 0;
+    }, timeout);
+
+    count++;
+    if (count === step) {
+      count = 0;
+      clearTimeout(timer);
+      fn();
+    }
+  };
 };
 
 /**
  * 注册连续快捷键
- * @param {*} targetKeys
- * @param {*} fn
- * @param {*} step
- * @param {*} timeout
- * @param {*} target
- * @returns
+ * @param {string[]} targetKeys - 目标快捷键数组
+ * @param {() => void} fn - 成功回调
+ * @param {number} step - 连续触发次数
+ * @param {number} timeout - 每次触发的间隔超时
+ * @param {EventTarget} target - 监听目标
+ * @returns {() => void} - 注销函数
  */
 export const stepShortcutRegister = (
   targetKeys = [],
   fn,
-  step = 3,
+  step = 2,
   timeout = 500,
   target = document
 ) => {
-  let count = 0;
-  let pre = Date.now();
-  let timer;
-  return shortcutListener((curkeys, allkeys) => {
-    timer && clearTimeout(timer);
-    timer = setTimeout(() => {
-      clearTimeout(timer);
-      count = 0;
-    }, timeout);
-
-    if (targetKeys.length > 0 && curkeys.length === 0) {
-      const now = Date.now();
-      if (
-        (count === 0 || now - pre < timeout) &&
-        isSameSet(new Set(targetKeys), new Set(allkeys))
-      ) {
-        count++;
-        if (count === step) {
-          count = 0;
-          fn();
-        }
-      } else {
-        count = 0;
-      }
-      pre = now;
-    }
-  }, target);
+  const steppedFn = withStepCounter(fn, step, timeout);
+  return shortcutRegister(targetKeys, steppedFn, target);
 };
diff --git a/src/libs/storage.js b/src/libs/storage.js
index 0824187..9aefd7d 100644
--- a/src/libs/storage.js
+++ b/src/libs/storage.js
@@ -15,6 +15,7 @@ import {
 import { isExt, isGm } from "./client";
 import { browser } from "./browser";
 import { kissLog } from "./log";
+import { debounce } from "./utils";
 
 async function set(key, val) {
   if (isExt) {
@@ -90,7 +91,7 @@ export const getSettingWithDefault = async () => ({
   ...((await getSetting()) || {}),
 });
 export const setSetting = (val) => setObj(STOKEY_SETTING, val);
-export const updateSetting = (obj) => putObj(STOKEY_SETTING, obj);
+export const putSetting = (obj) => putObj(STOKEY_SETTING, obj);
 
 /**
  * 规则列表
@@ -122,14 +123,20 @@ export const setSubRules = (url, val) =>
 export const getFab = () => getObj(STOKEY_FAB);
 export const getFabWithDefault = async () => (await getFab()) || {};
 export const setFab = (obj) => setObj(STOKEY_FAB, obj);
-export const updateFab = (obj) => putObj(STOKEY_FAB, obj);
+export const putFab = (obj) => putObj(STOKEY_FAB, obj);
 
 /**
  * 数据同步
  */
 export const getSync = () => getObj(STOKEY_SYNC);
 export const getSyncWithDefault = async () => (await getSync()) || DEFAULT_SYNC;
-export const updateSync = (obj) => putObj(STOKEY_SYNC, obj);
+export const putSync = (obj) => putObj(STOKEY_SYNC, obj);
+export const putSyncMeta = async (key) => {
+  const { syncMeta = {} } = await getSyncWithDefault();
+  syncMeta[key] = { ...(syncMeta[key] || {}), updateAt: Date.now() };
+  await putSync({ syncMeta });
+};
+export const debounceSyncMeta = debounce(putSyncMeta, 300);
 
 /**
  * ms auth
@@ -156,6 +163,6 @@ export const tryInitDefaultData = async () => {
       BUILTIN_RULES
     );
   } catch (err) {
-    kissLog(err, "init default");
+    kissLog("init default", err);
   }
 };
diff --git a/src/libs/subRules.js b/src/libs/subRules.js
index f8fedc7..b50f9d4 100644
--- a/src/libs/subRules.js
+++ b/src/libs/subRules.js
@@ -1,7 +1,7 @@
 import { GLOBAL_KEY } from "../config";
 import {
   getSyncWithDefault,
-  updateSync,
+  putSync,
   setSubRules,
   getSubRules,
 } from "./storage";
@@ -17,7 +17,7 @@ import { kissLog } from "./log";
 const updateSyncDataCache = async (url) => {
   const { dataCaches = {} } = await getSyncWithDefault();
   dataCaches[url] = Date.now();
-  await updateSync({ dataCaches });
+  await putSync({ dataCaches });
 };
 
 /**
@@ -47,7 +47,7 @@ export const syncAllSubRules = async (subrulesList) => {
       await syncSubRules(subrules.url);
       await updateSyncDataCache(subrules.url);
     } catch (err) {
-      kissLog(err, `sync subrule error: ${subrules.url}`);
+      kissLog(`sync subrule error: ${subrules.url}`, err);
     }
   }
 };
@@ -65,10 +65,10 @@ export const trySyncAllSubRules = async ({ subrulesList }) => {
     if (now - subRulesSyncAt > interval) {
       // 同步订阅规则
       await syncAllSubRules(subrulesList);
-      await updateSync({ subRulesSyncAt: now });
+      await putSync({ subRulesSyncAt: now });
     }
   } catch (err) {
-    kissLog(err, "try sync all subrules");
+    kissLog("try sync all subrules", err);
   }
 };
 
diff --git a/src/libs/sync.js b/src/libs/sync.js
index 37c5c6c..f6b6cbf 100644
--- a/src/libs/sync.js
+++ b/src/libs/sync.js
@@ -9,7 +9,7 @@ import {
 } from "../config";
 import {
   getSyncWithDefault,
-  updateSync,
+  putSync,
   getSettingWithDefault,
   getRulesWithDefault,
   getWordsWithDefault,
@@ -61,7 +61,7 @@ const syncByWorker = async (data, { syncUrl, syncKey }) => {
   return await apiSyncData(`${syncUrl}/sync`, syncKey, data);
 };
 
-const syncData = async (key, valueFn) => {
+export const syncData = async (key, value) => {
   const {
     syncType,
     syncUrl,
@@ -70,13 +70,14 @@ const syncData = async (key, valueFn) => {
     syncMeta = {},
   } = await getSyncWithDefault();
   if (!syncUrl || !syncKey || (syncType === OPT_SYNCTYPE_WEBDAV && !syncUser)) {
-    return;
+    throw new Error("sync args err");
   }
 
   let { updateAt = 0, syncAt = 0 } = syncMeta[key] || {};
-  syncAt === 0 && (updateAt = 0);
+  if (syncAt === 0) {
+    updateAt = 0; // 没有同步过,更新时间置零
+  }
 
-  const value = await valueFn();
   const data = {
     key,
     value: JSON.stringify(value),
@@ -93,13 +94,20 @@ const syncData = async (key, valueFn) => {
       ? await syncByWebdav(data, args)
       : await syncByWorker(data, args);
 
+  if (!res) {
+    throw new Error("sync data got err", key);
+  }
+
+  const newVal = JSON.parse(res.value);
+  const isNew = res.updateAt > updateAt;
+
   syncMeta[key] = {
     updateAt: res.updateAt,
     syncAt: Date.now(),
   };
-  await updateSync({ syncMeta });
+  await putSync({ syncMeta });
 
-  return { value: JSON.parse(res.value), isNew: res.updateAt > updateAt };
+  return { value: newVal, isNew };
 };
 
 /**
@@ -107,7 +115,8 @@ const syncData = async (key, valueFn) => {
  * @returns
  */
 const syncSetting = async () => {
-  const res = await syncData(KV_SETTING_KEY, getSettingWithDefault);
+  const value = await getSettingWithDefault();
+  const res = await syncData(KV_SETTING_KEY, value);
   if (res?.isNew) {
     await setSetting(res.value);
   }
@@ -117,7 +126,7 @@ export const trySyncSetting = async () => {
   try {
     await syncSetting();
   } catch (err) {
-    kissLog(err, "sync setting");
+    kissLog("sync setting", err.message);
   }
 };
 
@@ -126,7 +135,8 @@ export const trySyncSetting = async () => {
  * @returns
  */
 const syncRules = async () => {
-  const res = await syncData(KV_RULES_KEY, getRulesWithDefault);
+  const value = await getRulesWithDefault();
+  const res = await syncData(KV_RULES_KEY, value);
   if (res?.isNew) {
     await setRules(res.value);
   }
@@ -136,7 +146,7 @@ export const trySyncRules = async () => {
   try {
     await syncRules();
   } catch (err) {
-    kissLog(err, "sync user rules");
+    kissLog("sync user rules", err.message);
   }
 };
 
@@ -145,7 +155,8 @@ export const trySyncRules = async () => {
  * @returns
  */
 const syncWords = async () => {
-  const res = await syncData(KV_WORDS_KEY, getWordsWithDefault);
+  const value = await getWordsWithDefault();
+  const res = await syncData(KV_WORDS_KEY, value);
   if (res?.isNew) {
     await setWords(res.value);
   }
@@ -155,7 +166,7 @@ export const trySyncWords = async () => {
   try {
     await syncWords();
   } catch (err) {
-    kissLog(err, "sync fav words");
+    kissLog("sync fav words", err.message);
   }
 };
 
diff --git a/src/libs/translator.js b/src/libs/translator.js
index ead1cc7..f32ee9e 100644
--- a/src/libs/translator.js
+++ b/src/libs/translator.js
@@ -7,9 +7,9 @@ import {
   OPT_STYLE_FUZZY,
   GLOBLA_RULE,
   DEFAULT_SETTING,
-  DEFAULT_TRANS_APIS,
-  DEFAULT__MOUSEHOVER_KEY,
+  DEFAULT_MOUSEHOVER_KEY,
   OPT_STYLE_NONE,
+  DEFAULT_API_SETTING,
 } from "../config";
 import interpreter from "./interpreter";
 import { ShadowRootMonitor } from "./shadowroot";
@@ -356,7 +356,7 @@ export class Translator {
           this.#startObserveShadowRoot(shadowRoot);
         });
       } catch (err) {
-        kissLog(err, "findAllShadowRoots");
+        kissLog("findAllShadowRoots", err);
       }
     }
   }
@@ -419,7 +419,7 @@ export class Translator {
           termPatterns.push(`(${key})`);
           this.#termValues.push(value);
         } catch (err) {
-          kissLog(err, `Invalid RegExp for term: "${key}"`);
+          kissLog(`Invalid RegExp for term: "${key}"`, err);
         }
       }
     }
@@ -556,7 +556,7 @@ export class Translator {
         }
       }
     } catch (err) {
-      kissLog(err, "无法访问某个 shadowRoot");
+      kissLog("无法访问某个 shadowRoot", err);
     }
     // const end = performance.now();
     // const duration = end - start;
@@ -839,7 +839,7 @@ export class Translator {
           nodes,
         });
       } catch (err) {
-        kissLog(err, "transStartHook");
+        kissLog("transStartHook", err);
       }
     }
 
@@ -913,14 +913,14 @@ export class Translator {
             innerNode: inner,
           });
         } catch (err) {
-          kissLog(err, "transEndHook");
+          kissLog("transEndHook", err);
         }
       }
     } catch (err) {
       // inner.textContent = `[失败]...`;
       // todo: 失败重试按钮
       wrapper.remove();
-      kissLog(err, "translateNodeGroup");
+      kissLog("translateNodeGroup", err);
     }
   }
 
@@ -1037,16 +1037,13 @@ export class Translator {
 
   // 发起翻译请求
   #translateFetch(text) {
-    const { translator, fromLang, toLang } = this.#rule;
-    // const apiSetting = this.#setting.transApis[translator];
-    const apiSetting = {
-      ...DEFAULT_TRANS_APIS[translator],
-      ...(this.#setting.transApis[translator] || {}),
-    };
+    const { apiSlug, fromLang, toLang } = this.#rule;
+    const apiSetting =
+      this.#setting.transApis.find((api) => api.apiSlug === apiSlug) ||
+      DEFAULT_API_SETTING;
 
     return apiTranslate({
       text,
-      translator,
       fromLang,
       toLang,
       apiSetting,
@@ -1150,11 +1147,11 @@ export class Translator {
       return;
     }
 
-    const { translator, fromLang, toLang, hasRichText, textStyle, transOnly } =
+    const { apiSlug, fromLang, toLang, hasRichText, textStyle, transOnly } =
       this.#rule;
 
     const needsRefresh =
-      appliedRule.translator !== translator ||
+      appliedRule.apiSlug !== apiSlug ||
       appliedRule.fromLang !== fromLang ||
       appliedRule.toLang !== toLang ||
       appliedRule.hasRichText !== hasRichText;
@@ -1162,7 +1159,7 @@ export class Translator {
     // 需要重新翻译
     if (needsRefresh) {
       Object.assign(appliedRule, {
-        translator,
+        apiSlug,
         fromLang,
         toLang,
         hasRichText,
@@ -1207,7 +1204,7 @@ export class Translator {
     document.addEventListener("mousemove", this.#boundMouseMoveHandler);
     let { mouseHoverKey } = this.#setting.mouseHoverSetting;
     if (mouseHoverKey.length === 0) {
-      mouseHoverKey = DEFAULT__MOUSEHOVER_KEY;
+      mouseHoverKey = DEFAULT_MOUSEHOVER_KEY;
     }
     this.#removeKeydownHandler = shortcutRegister(
       mouseHoverKey,
@@ -1273,7 +1270,7 @@ export class Translator {
           document.title = trText || title;
         })
         .catch((err) => {
-          kissLog(err, "tanslate title");
+          kissLog("tanslate title", err);
         });
     }
   }
diff --git a/src/views/Action/Draggable.js b/src/views/Action/Draggable.js
index b503240..2e3a575 100644
--- a/src/views/Action/Draggable.js
+++ b/src/views/Action/Draggable.js
@@ -1,7 +1,7 @@
 import { useEffect, useMemo, useState } from "react";
 import { limitNumber } from "../../libs/utils";
 import { isMobile } from "../../libs/mobile";
-import { updateFab } from "../../libs/storage";
+import { putFab } from "../../libs/storage";
 import { debounce } from "../../libs/utils";
 import Paper from "@mui/material/Paper";
 
@@ -61,7 +61,7 @@ export default function Draggable({
   const [hover, setHover] = useState(false);
   const [origin, setOrigin] = useState(null);
   const [position, setPosition] = useState({ x: left, y: top });
-  const setFabPosition = useMemo(() => debounce(updateFab, 500), []);
+  const setFabPosition = useMemo(() => debounce(putFab, 500), []);
 
   const handlePointerDown = (e) => {
     !isMobile && e.target.setPointerCapture(e.pointerId);
diff --git a/src/views/Action/index.js b/src/views/Action/index.js
index 118080c..00fe1d2 100644
--- a/src/views/Action/index.js
+++ b/src/views/Action/index.js
@@ -142,7 +142,7 @@ export default function Action({ translator, fab }) {
         });
       };
     } catch (err) {
-      kissLog(err, "registerMenuCommand");
+      kissLog("registerMenuCommand", err);
     }
   }, [translator]);
 
diff --git a/src/views/Content/LoadingIcon.js b/src/views/Content/LoadingIcon.js
deleted file mode 100644
index 0cf4eb1..0000000
--- a/src/views/Content/LoadingIcon.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { loadingSvg } from "../../libs/svg";
-
-export default function LoadingIcon() {
-  return (
-    <span
-      style={{
-        display: "inline-block",
-        width: "1.2em",
-        height: "1em",
-      }}
-      dangerouslySetInnerHTML={{ __html: loadingSvg }}
-    />
-  );
-}
diff --git a/src/views/Content/index.js b/src/views/Content/index.js
deleted file mode 100644
index d56aa53..0000000
--- a/src/views/Content/index.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import { useState, useEffect, useMemo } from "react";
-import LoadingIcon from "./LoadingIcon";
-import {
-  OPT_STYLE_LINE,
-  OPT_STYLE_DOTLINE,
-  OPT_STYLE_DASHLINE,
-  OPT_STYLE_WAVYLINE,
-  OPT_STYLE_DASHBOX,
-  OPT_STYLE_FUZZY,
-  OPT_STYLE_HIGHLIGHT,
-  OPT_STYLE_BLOCKQUOTE,
-  OPT_STYLE_DIY,
-  DEFAULT_COLOR,
-  MSG_TRANS_CURRULE,
-} from "../../config";
-import { useTranslate } from "../../hooks/Translate";
-import { styled, css } from "@mui/material/styles";
-import { APP_LCNAME } from "../../config";
-import interpreter from "../../libs/interpreter";
-
-const LINE_STYLES = {
-  [OPT_STYLE_LINE]: "solid",
-  [OPT_STYLE_DOTLINE]: "dotted",
-  [OPT_STYLE_DASHLINE]: "dashed",
-  [OPT_STYLE_WAVYLINE]: "wavy",
-};
-
-const StyledSpan = styled("span")`
-  ${({ textStyle, textDiyStyle, bgColor }) => {
-    switch (textStyle) {
-      case OPT_STYLE_LINE: // 下划线
-      case OPT_STYLE_DOTLINE: // 点状线
-      case OPT_STYLE_DASHLINE: // 虚线
-      case OPT_STYLE_WAVYLINE: // 波浪线
-        return css`
-          opacity: 0.6;
-          -webkit-opacity: 0.6;
-          text-decoration-line: underline;
-          text-decoration-style: ${LINE_STYLES[textStyle]};
-          text-decoration-color: ${bgColor};
-          text-decoration-thickness: 2px;
-          text-underline-offset: 0.3em;
-          -webkit-text-decoration-line: underline;
-          -webkit-text-decoration-style: ${LINE_STYLES[textStyle]};
-          -webkit-text-decoration-color: ${bgColor};
-          -webkit-text-decoration-thickness: 2px;
-          -webkit-text-underline-offset: 0.3em;
-          &:hover {
-            opacity: 1;
-            -webkit-opacity: 1;
-          }
-        `;
-      case OPT_STYLE_DASHBOX: // 虚线框
-        return css`
-          color: ${bgColor || DEFAULT_COLOR};
-          border: 1px dashed ${bgColor || DEFAULT_COLOR};
-          background: transparent;
-          display: block;
-          padding: 0.2em;
-          box-sizing: border-box;
-          white-space: normal;
-          word-wrap: break-word;
-          overflow-wrap: break-word;
-        `;
-      case OPT_STYLE_FUZZY: // 模糊
-        return css`
-          filter: blur(0.2em);
-          -webkit-filter: blur(0.2em);
-          &:hover {
-            filter: none;
-            -webkit-filter: none;
-          }
-        `;
-      case OPT_STYLE_HIGHLIGHT: // 高亮
-        return css`
-          color: #fff;
-          background-color: ${bgColor || DEFAULT_COLOR};
-        `;
-      case OPT_STYLE_BLOCKQUOTE: // 引用
-        return css`
-          opacity: 0.6;
-          -webkit-opacity: 0.6;
-          display: block;
-          padding: 0 0.75em;
-          border-left: 0.25em solid ${bgColor || DEFAULT_COLOR};
-          &:hover {
-            opacity: 1;
-            -webkit-opacity: 1;
-          }
-        `;
-      case OPT_STYLE_DIY: // 自定义
-        return textDiyStyle;
-      default:
-        return ``;
-    }
-  }}
-`;
-
-export default function Content({ q, keeps, translator, $el }) {
-  const [rule, setRule] = useState(translator.rule);
-  const { text, sameLang, loading } = useTranslate(
-    q,
-    rule,
-    translator.setting,
-    translator.docInfo
-  );
-  const {
-    transOpen,
-    textStyle,
-    bgColor,
-    textDiyStyle,
-    transOnly,
-    transTag,
-    transEndHook,
-  } = rule;
-
-  const { newlineLength } = translator.setting;
-
-  const handleKissEvent = (e) => {
-    const { action, args } = e.detail;
-    switch (action) {
-      case MSG_TRANS_CURRULE:
-        setRule(args);
-        break;
-      default:
-    }
-  };
-
-  useEffect(() => {
-    window.addEventListener(translator.eventName, handleKissEvent);
-    return () => {
-      window.removeEventListener(translator.eventName, handleKissEvent);
-    };
-  }, [translator.eventName]);
-
-  const gap = useMemo(() => {
-    if (transOnly === "true") {
-      return "";
-    }
-    return q.length >= newlineLength ? <br /> : " ";
-  }, [q, transOnly, newlineLength]);
-
-  const styles = useMemo(
-    () => ({
-      textStyle,
-      textDiyStyle,
-      bgColor,
-      as: transTag,
-    }),
-    [textStyle, textDiyStyle, bgColor, transTag]
-  );
-
-  const trText = useMemo(() => {
-    if (loading || !transEndHook?.trim()) {
-      return text;
-    }
-
-    // 翻译完成钩子函数
-    interpreter.run(`exports.transEndHook = ${transEndHook}`);
-    return interpreter.exports.transEndHook($el, text, q, keeps);
-  }, [loading, $el, q, text, keeps, transEndHook]);
-
-  if (loading) {
-    return (
-      <>
-        {gap}
-        <LoadingIcon />
-      </>
-    );
-  }
-
-  if (!trText || sameLang) {
-    return;
-  }
-
-  if (
-    transOnly === "true" &&
-    transOpen === "true" &&
-    $el.querySelector(APP_LCNAME)
-  ) {
-    Array.from($el.childNodes).forEach((el) => {
-      if (el.localName !== APP_LCNAME) {
-        el.remove();
-      }
-    });
-  }
-
-  if (keeps.length > 0) {
-    return (
-      <>
-        {gap}
-        <StyledSpan
-          {...styles}
-          dangerouslySetInnerHTML={{
-            __html: trText.replace(/\[(\d+)\]/g, (_, p) => keeps[parseInt(p)]),
-          }}
-        />
-      </>
-    );
-  }
-
-  return (
-    <>
-      {gap}
-      <StyledSpan {...styles}>{trText}</StyledSpan>
-    </>
-  );
-}
diff --git a/src/views/Options/Apis.js b/src/views/Options/Apis.js
index bfc291b..fd100d4 100644
--- a/src/views/Options/Apis.js
+++ b/src/views/Options/Apis.js
@@ -1,3 +1,4 @@
+import { useState, useEffect, useMemo } from "react";
 import Stack from "@mui/material/Stack";
 import TextField from "@mui/material/TextField";
 import Button from "@mui/material/Button";
@@ -5,58 +6,44 @@ import LoadingButton from "@mui/lab/LoadingButton";
 import MenuItem from "@mui/material/MenuItem";
 import FormControlLabel from "@mui/material/FormControlLabel";
 import Switch from "@mui/material/Switch";
-import {
-  OPT_TRANS_ALL,
-  OPT_TRANS_MICROSOFT,
-  OPT_TRANS_DEEPL,
-  OPT_TRANS_DEEPLX,
-  OPT_TRANS_DEEPLFREE,
-  OPT_TRANS_BAIDU,
-  OPT_TRANS_TENCENT,
-  OPT_TRANS_VOLCENGINE,
-  OPT_TRANS_OPENAI,
-  OPT_TRANS_OPENAI_2,
-  OPT_TRANS_OPENAI_3,
-  OPT_TRANS_GEMINI,
-  OPT_TRANS_GEMINI_2,
-  OPT_TRANS_CLAUDE,
-  OPT_TRANS_CLOUDFLAREAI,
-  OPT_TRANS_OLLAMA,
-  OPT_TRANS_OLLAMA_2,
-  OPT_TRANS_OLLAMA_3,
-  OPT_TRANS_OPENROUTER,
-  OPT_TRANS_CUSTOMIZE,
-  OPT_TRANS_CUSTOMIZE_2,
-  OPT_TRANS_CUSTOMIZE_3,
-  OPT_TRANS_CUSTOMIZE_4,
-  OPT_TRANS_CUSTOMIZE_5,
-  OPT_TRANS_NIUTRANS,
-  DEFAULT_FETCH_LIMIT,
-  DEFAULT_FETCH_INTERVAL,
-  DEFAULT_HTTP_TIMEOUT,
-  OPT_TRANS_BATCH,
-  OPT_TRANS_CONTEXT,
-  DEFAULT_BATCH_INTERVAL,
-  DEFAULT_BATCH_SIZE,
-  DEFAULT_BATCH_LENGTH,
-  DEFAULT_CONTEXT_SIZE,
-} from "../../config";
-import { useState } from "react";
 import { useI18n } from "../../hooks/I18n";
 import Typography from "@mui/material/Typography";
 import Accordion from "@mui/material/Accordion";
 import AccordionSummary from "@mui/material/AccordionSummary";
 import AccordionDetails from "@mui/material/AccordionDetails";
 import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
+import AddIcon from "@mui/icons-material/Add";
 import Alert from "@mui/material/Alert";
+import Menu from "@mui/material/Menu";
+import Grid from "@mui/material/Grid";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
 import { useAlert } from "../../hooks/Alert";
-import { useApi } from "../../hooks/Api";
+import { useApiList, useApiItem } from "../../hooks/Api";
+import { useConfirm } from "../../hooks/Confirm";
 import { apiTranslate } from "../../apis";
 import Box from "@mui/material/Box";
-import Link from "@mui/material/Link";
 import { limitNumber, limitFloat } from "../../libs/utils";
+import ReusableAutocomplete from "./ReusableAutocomplete";
+import {
+  OPT_TRANS_DEEPLX,
+  OPT_TRANS_OLLAMA,
+  OPT_TRANS_CUSTOMIZE,
+  OPT_TRANS_NIUTRANS,
+  DEFAULT_FETCH_LIMIT,
+  DEFAULT_FETCH_INTERVAL,
+  DEFAULT_HTTP_TIMEOUT,
+  DEFAULT_BATCH_INTERVAL,
+  DEFAULT_BATCH_SIZE,
+  DEFAULT_BATCH_LENGTH,
+  DEFAULT_CONTEXT_SIZE,
+  OPT_ALL_TYPES,
+  API_SPE_TYPES,
+  BUILTIN_STONES,
+  // BUILTIN_PLACEHOULDERS,
+  // BUILTIN_TAG_NAMES,
+} from "../../config";
 
-function TestButton({ translator, api }) {
+function TestButton({ apiSlug, api }) {
   const i18n = useI18n();
   const alert = useAlert();
   const [loading, setLoading] = useState(false);
@@ -64,7 +51,7 @@ function TestButton({ translator, api }) {
     try {
       setLoading(true);
       const [text] = await apiTranslate({
-        translator,
+        apiSlug,
         text: "hello world",
         fromLang: "en",
         toLang: "zh-CN",
@@ -114,7 +101,7 @@ function TestButton({ translator, api }) {
   return (
     <LoadingButton
       size="small"
-      variant="contained"
+      variant="outlined"
       onClick={handleApiTest}
       loading={loading}
     >
@@ -123,39 +110,34 @@ function TestButton({ translator, api }) {
   );
 }
 
-function ApiFields({ translator, api, updateApi, resetApi }) {
+function ApiFields({ apiSlug, isUserApi, deleteApi }) {
+  const { api, update, reset } = useApiItem(apiSlug);
   const i18n = useI18n();
-  const {
-    url = "",
-    key = "",
-    model = "",
-    systemPrompt = "",
-    userPrompt = "",
-    customHeader = "",
-    customBody = "",
-    think = false,
-    thinkIgnore = "",
-    fetchLimit = DEFAULT_FETCH_LIMIT,
-    fetchInterval = DEFAULT_FETCH_INTERVAL,
-    httpTimeout = DEFAULT_HTTP_TIMEOUT,
-    dictNo = "",
-    memoryNo = "",
-    reqHook = "",
-    resHook = "",
-    temperature = 0,
-    maxTokens = 256,
-    apiName = "",
-    isDisabled = false,
-    useBatchFetch = false,
-    batchInterval = DEFAULT_BATCH_INTERVAL,
-    batchSize = DEFAULT_BATCH_SIZE,
-    batchLength = DEFAULT_BATCH_LENGTH,
-    useContext = false,
-    contextSize = DEFAULT_CONTEXT_SIZE,
-  } = api;
+  const [formData, setFormData] = useState({});
+  const [isModified, setIsModified] = useState(false);
+  const confirm = useConfirm();
+
+  useEffect(() => {
+    if (api) {
+      setFormData(api);
+    }
+  }, [api]);
+
+  useEffect(() => {
+    if (!api) return;
+    const hasChanged = JSON.stringify(api) !== JSON.stringify(formData);
+    setIsModified(hasChanged);
+  }, [api, formData]);
 
   const handleChange = (e) => {
-    let { name, value } = e.target;
+    let { name, value, type, checked } = e.target;
+
+    if (type === "checkbox" || type === "switch") {
+      value = checked;
+    }
+    // if (value === "true") value = true;
+    // if (value === "false") value = false;
+
     switch (name) {
       case "fetchLimit":
         value = limitNumber(value, 1, 100);
@@ -186,56 +168,78 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         break;
       default:
     }
-    updateApi({
+
+    setFormData((prevData) => ({
+      ...prevData,
       [name]: value,
-    });
+    }));
   };
 
-  const builtinTranslators = [
-    OPT_TRANS_MICROSOFT,
-    OPT_TRANS_DEEPLFREE,
-    OPT_TRANS_BAIDU,
-    OPT_TRANS_TENCENT,
-    OPT_TRANS_VOLCENGINE,
-  ];
+  const handleSave = () => {
+    // 过滤掉 api 对象中不存在的字段
+    // const updatedFields = Object.keys(formData).reduce((acc, key) => {
+    //   if (api && Object.keys(api).includes(key)) {
+    //     acc[key] = formData[key];
+    //   }
+    //   return acc;
+    // }, {});
+    // update(updatedFields);
+    update(formData);
+  };
 
-  const mulkeysTranslators = [
-    OPT_TRANS_DEEPL,
-    OPT_TRANS_OPENAI,
-    OPT_TRANS_OPENAI_2,
-    OPT_TRANS_OPENAI_3,
-    OPT_TRANS_GEMINI,
-    OPT_TRANS_GEMINI_2,
-    OPT_TRANS_CLAUDE,
-    OPT_TRANS_CLOUDFLAREAI,
-    OPT_TRANS_OLLAMA,
-    OPT_TRANS_OLLAMA_2,
-    OPT_TRANS_OLLAMA_3,
-    OPT_TRANS_OPENROUTER,
-    OPT_TRANS_NIUTRANS,
-    OPT_TRANS_CUSTOMIZE,
-    OPT_TRANS_CUSTOMIZE_2,
-    OPT_TRANS_CUSTOMIZE_3,
-    OPT_TRANS_CUSTOMIZE_4,
-    OPT_TRANS_CUSTOMIZE_5,
-  ];
+  const handleReset = () => {
+    reset();
+  };
 
-  const keyHelper =
-    translator === OPT_TRANS_NIUTRANS ? (
-      <>
-        {i18n("mulkeys_help")}
-        <Link
-          href="https://niutrans.com/login?active=3&userSource=kiss-translator"
-          target="_blank"
-        >
-          {i18n("reg_niutrans")}
-        </Link>
-      </>
-    ) : mulkeysTranslators.includes(translator) ? (
-      i18n("mulkeys_help")
-    ) : (
-      ""
-    );
+  const handleDelete = async () => {
+    const isConfirmed = await confirm({
+      confirmText: i18n("delete"),
+      cancelText: i18n("cancel"),
+    });
+
+    if (isConfirmed) {
+      deleteApi(apiSlug);
+    }
+  };
+
+  const {
+    url = "",
+    key = "",
+    model = "",
+    apiType,
+    systemPrompt = "",
+    // userPrompt = "",
+    customHeader = "",
+    customBody = "",
+    think = false,
+    thinkIgnore = "",
+    fetchLimit = DEFAULT_FETCH_LIMIT,
+    fetchInterval = DEFAULT_FETCH_INTERVAL,
+    httpTimeout = DEFAULT_HTTP_TIMEOUT,
+    dictNo = "",
+    memoryNo = "",
+    reqHook = "",
+    resHook = "",
+    temperature = 0,
+    maxTokens = 256,
+    apiName = "",
+    isDisabled = false,
+    useBatchFetch = false,
+    batchInterval = DEFAULT_BATCH_INTERVAL,
+    batchSize = DEFAULT_BATCH_SIZE,
+    batchLength = DEFAULT_BATCH_LENGTH,
+    useContext = false,
+    contextSize = DEFAULT_CONTEXT_SIZE,
+    tone = "neutral",
+    // placeholder = "{ }",
+    // tagName = "i",
+    // aiTerms = false,
+  } = formData;
+
+  const keyHelper = useMemo(
+    () => (API_SPE_TYPES.mulkeys.has(apiType) ? i18n("mulkeys_help") : ""),
+    [apiType, i18n]
+  );
 
   return (
     <Stack spacing={3}>
@@ -247,7 +251,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         onChange={handleChange}
       />
 
-      {!builtinTranslators.includes(translator) && (
+      {!API_SPE_TYPES.machine.has(apiType) && (
         <>
           <TextField
             size="small"
@@ -255,10 +259,10 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
             name="url"
             value={url}
             onChange={handleChange}
-            multiline={translator === OPT_TRANS_DEEPLX}
+            multiline={apiType === OPT_TRANS_DEEPLX}
             maxRows={10}
             helperText={
-              translator === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
+              apiType === OPT_TRANS_DEEPLX ? i18n("mulkeys_help") : ""
             }
           />
           <TextField
@@ -267,26 +271,66 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
             name="key"
             value={key}
             onChange={handleChange}
-            multiline={mulkeysTranslators.includes(translator)}
+            multiline={API_SPE_TYPES.mulkeys.has(apiType)}
             maxRows={10}
             helperText={keyHelper}
           />
         </>
       )}
 
-      {(translator.startsWith(OPT_TRANS_OPENAI) ||
-        translator.startsWith(OPT_TRANS_OLLAMA) ||
-        translator === OPT_TRANS_CLAUDE ||
-        translator === OPT_TRANS_OPENROUTER ||
-        translator.startsWith(OPT_TRANS_GEMINI)) && (
+      {API_SPE_TYPES.ai.has(apiType) && (
         <>
-          <TextField
-            size="small"
-            label={"MODEL"}
-            name="model"
-            value={model}
-            onChange={handleChange}
-          />
+          <Box>
+            <Grid container spacing={2} columns={12}>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                {/* todo: 改成 ReusableAutocomplete 可选择和填写模型 */}
+                <TextField
+                  size="small"
+                  fullWidth
+                  label={"MODEL"}
+                  name="model"
+                  value={model}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <ReusableAutocomplete
+                  freeSolo
+                  size="small"
+                  fullWidth
+                  options={BUILTIN_STONES}
+                  name="tone"
+                  label={i18n("translation_style")}
+                  value={tone}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <TextField
+                  size="small"
+                  fullWidth
+                  label={"Temperature"}
+                  type="number"
+                  name="temperature"
+                  value={temperature}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <TextField
+                  size="small"
+                  fullWidth
+                  label={"Max Tokens"}
+                  type="number"
+                  name="maxTokens"
+                  value={maxTokens}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}></Grid>
+            </Grid>
+          </Box>
+
           <TextField
             size="small"
             label={"SYSTEM PROMPT"}
@@ -295,8 +339,9 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
             onChange={handleChange}
             multiline
             maxRows={10}
+            helperText={i18n("system_prompt_helper")}
           />
-          <TextField
+          {/* <TextField
             size="small"
             label={"USER PROMPT"}
             name="userPrompt"
@@ -304,7 +349,51 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
             onChange={handleChange}
             multiline
             maxRows={10}
-          />
+          /> */}
+
+          {/* <Box>
+            <Grid container spacing={2} columns={12}>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <ReusableAutocomplete
+                  freeSolo
+                  size="small"
+                  fullWidth
+                  options={BUILTIN_PLACEHOULDERS}
+                  name="placeholder"
+                  label={i18n("placeholder")}
+                  value={placeholder}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <ReusableAutocomplete
+                  freeSolo
+                  size="small"
+                  fullWidth
+                  options={BUILTIN_TAG_NAMES}
+                  name="tagName"
+                  label={i18n("tag_name")}
+                  value={tagName}
+                  onChange={handleChange}
+                />
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                <TextField
+                  select
+                  size="small"
+                  fullWidth
+                  name="aiTerms"
+                  value={aiTerms}
+                  label={i18n("ai_terms")}
+                  onChange={handleChange}
+                >
+                  <MenuItem value={true}>{i18n("enable")}</MenuItem>
+                  <MenuItem value={false}>{i18n("disable")}</MenuItem>
+                </TextField>
+              </Grid>
+            </Grid>
+          </Box> */}
+
           <TextField
             size="small"
             label={i18n("custom_header")}
@@ -328,7 +417,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         </>
       )}
 
-      {translator.startsWith(OPT_TRANS_OLLAMA) && (
+      {apiType === OPT_TRANS_OLLAMA && (
         <>
           <TextField
             select
@@ -351,32 +440,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         </>
       )}
 
-      {(translator.startsWith(OPT_TRANS_OPENAI) ||
-        translator === OPT_TRANS_CLAUDE ||
-        translator === OPT_TRANS_OPENROUTER ||
-        translator === OPT_TRANS_GEMINI ||
-        translator === OPT_TRANS_GEMINI_2) && (
-        <>
-          <TextField
-            size="small"
-            label={"Temperature"}
-            type="number"
-            name="temperature"
-            value={temperature}
-            onChange={handleChange}
-          />
-          <TextField
-            size="small"
-            label={"Max Tokens"}
-            type="number"
-            name="maxTokens"
-            value={maxTokens}
-            onChange={handleChange}
-          />
-        </>
-      )}
-
-      {translator === OPT_TRANS_NIUTRANS && (
+      {apiType === OPT_TRANS_NIUTRANS && (
         <>
           <TextField
             size="small"
@@ -395,7 +459,7 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         </>
       )}
 
-      {translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
+      {apiType === OPT_TRANS_CUSTOMIZE && (
         <>
           <TextField
             size="small"
@@ -418,140 +482,180 @@ function ApiFields({ translator, api, updateApi, resetApi }) {
         </>
       )}
 
-      {OPT_TRANS_BATCH.has(translator) && (
-        <>
-          <TextField
-            select
-            size="small"
-            name="useBatchFetch"
-            value={useBatchFetch}
-            label={i18n("use_batch_fetch")}
-            onChange={handleChange}
-          >
-            <MenuItem value={false}>{i18n("disable")}</MenuItem>
-            <MenuItem value={true}>{i18n("enable")}</MenuItem>
-          </TextField>
-          {useBatchFetch && (
-            <>
+      {API_SPE_TYPES.batch.has(api.apiType) && (
+        <Box>
+          <Grid container spacing={2} columns={12}>
+            <Grid item xs={6} sm={6} md={6} lg={3}>
+              <TextField
+                select
+                fullWidth
+                size="small"
+                name="useBatchFetch"
+                value={useBatchFetch}
+                label={i18n("use_batch_fetch")}
+                onChange={handleChange}
+              >
+                <MenuItem value={false}>{i18n("disable")}</MenuItem>
+                <MenuItem value={true}>{i18n("enable")}</MenuItem>
+              </TextField>
+            </Grid>
+            <Grid item xs={6} sm={6} md={6} lg={3}>
               <TextField
                 size="small"
+                fullWidth
                 label={i18n("batch_interval")}
                 type="number"
                 name="batchInterval"
                 value={batchInterval}
                 onChange={handleChange}
               />
+            </Grid>
+            <Grid item xs={6} sm={6} md={6} lg={3}>
               <TextField
                 size="small"
+                fullWidth
                 label={i18n("batch_size")}
                 type="number"
                 name="batchSize"
                 value={batchSize}
                 onChange={handleChange}
               />
+            </Grid>
+            <Grid item xs={6} sm={6} md={6} lg={3}>
               <TextField
                 size="small"
+                fullWidth
                 label={i18n("batch_length")}
                 type="number"
                 name="batchLength"
                 value={batchLength}
                 onChange={handleChange}
               />
-            </>
-          )}
+            </Grid>
+          </Grid>
+        </Box>
+      )}
+
+      {API_SPE_TYPES.context.has(api.apiType) && (
+        <>
+          <Box>
+            <Grid container spacing={2} columns={12}>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                {" "}
+                <TextField
+                  select
+                  size="small"
+                  fullWidth
+                  name="useContext"
+                  value={useContext}
+                  label={i18n("use_context")}
+                  onChange={handleChange}
+                >
+                  <MenuItem value={false}>{i18n("disable")}</MenuItem>
+                  <MenuItem value={true}>{i18n("enable")}</MenuItem>
+                </TextField>
+              </Grid>
+              <Grid item xs={6} sm={6} md={6} lg={3}>
+                {" "}
+                <TextField
+                  size="small"
+                  fullWidth
+                  label={i18n("context_size")}
+                  type="number"
+                  name="contextSize"
+                  value={contextSize}
+                  onChange={handleChange}
+                />
+              </Grid>
+            </Grid>
+          </Box>
         </>
       )}
 
-      {OPT_TRANS_CONTEXT.has(translator) && (
-        <>
-          <TextField
-            select
-            size="small"
-            name="useContext"
-            value={useContext}
-            label={i18n("use_context")}
-            onChange={handleChange}
-          >
-            <MenuItem value={false}>{i18n("disable")}</MenuItem>
-            <MenuItem value={true}>{i18n("enable")}</MenuItem>
-          </TextField>
-          {useBatchFetch && (
+      <Box>
+        <Grid container spacing={2} columns={12}>
+          <Grid item xs={6} sm={6} md={6} lg={3}>
             <TextField
               size="small"
-              label={i18n("context_size")}
+              fullWidth
+              label={i18n("fetch_limit")}
               type="number"
-              name="contextSize"
-              value={contextSize}
+              name="fetchLimit"
+              value={fetchLimit}
               onChange={handleChange}
             />
-          )}
-        </>
-      )}
-
-      <TextField
-        size="small"
-        label={i18n("fetch_limit")}
-        type="number"
-        name="fetchLimit"
-        value={fetchLimit}
-        onChange={handleChange}
-      />
-
-      <TextField
-        size="small"
-        label={i18n("fetch_interval")}
-        type="number"
-        name="fetchInterval"
-        value={fetchInterval}
-        onChange={handleChange}
-      />
-
-      <TextField
-        size="small"
-        label={i18n("http_timeout")}
-        type="number"
-        name="httpTimeout"
-        defaultValue={httpTimeout}
-        onChange={handleChange}
-      />
-
-      <FormControlLabel
-        control={
-          <Switch
-            size="small"
-            name="isDisabled"
-            checked={isDisabled}
-            onChange={() => {
-              updateApi({ isDisabled: !isDisabled });
-            }}
-          />
-        }
-        label={i18n("is_disabled")}
-      />
+          </Grid>
+          <Grid item xs={6} sm={6} md={6} lg={3}>
+            <TextField
+              size="small"
+              fullWidth
+              label={i18n("fetch_interval")}
+              type="number"
+              name="fetchInterval"
+              value={fetchInterval}
+              onChange={handleChange}
+            />
+          </Grid>
+          <Grid item xs={6} sm={6} md={6} lg={3}>
+            <TextField
+              size="small"
+              fullWidth
+              label={i18n("http_timeout")}
+              type="number"
+              name="httpTimeout"
+              value={httpTimeout}
+              onChange={handleChange}
+            />
+          </Grid>
+          <Grid item xs={6} sm={6} md={6} lg={3}></Grid>
+        </Grid>
+      </Box>
 
       <Stack direction="row" spacing={2}>
-        <TestButton translator={translator} api={api} />
         <Button
           size="small"
-          variant="outlined"
-          onClick={() => {
-            resetApi();
-          }}
+          variant="contained"
+          onClick={handleSave}
+          disabled={!isModified}
         >
+          {i18n("save")}
+        </Button>
+        <TestButton apiSlug={apiSlug} api={api} />
+        <Button size="small" variant="outlined" onClick={handleReset}>
           {i18n("restore_default")}
         </Button>
+        {isUserApi && (
+          <Button
+            size="small"
+            variant="outlined"
+            color="error"
+            onClick={handleDelete}
+          >
+            {i18n("delete")}
+          </Button>
+        )}
+
+        <FormControlLabel
+          control={
+            <Switch
+              size="small"
+              fullWidth
+              name="isDisabled"
+              checked={isDisabled}
+              onChange={handleChange}
+            />
+          }
+          label={i18n("is_disabled")}
+        />
       </Stack>
 
-      {translator.startsWith(OPT_TRANS_CUSTOMIZE) && (
-        <pre>{i18n("custom_api_help")}</pre>
-      )}
+      {apiType === OPT_TRANS_CUSTOMIZE && <pre>{i18n("custom_api_help")}</pre>}
     </Stack>
   );
 }
 
-function ApiAccordion({ translator }) {
+function ApiAccordion({ api, isUserApi, deleteApi }) {
   const [expanded, setExpanded] = useState(false);
-  const { api, updateApi, resetApi } = useApi(translator);
 
   const handleChange = (e) => {
     setExpanded((pre) => !pre);
@@ -566,16 +670,15 @@ function ApiAccordion({ translator }) {
             overflowWrap: "anywhere",
           }}
         >
-          {api.apiName ? `${translator} (${api.apiName})` : translator}
+          {`[${api.apiType}] ${api.apiName}`}
         </Typography>
       </AccordionSummary>
       <AccordionDetails>
         {expanded && (
           <ApiFields
-            translator={translator}
-            api={api}
-            updateApi={updateApi}
-            resetApi={resetApi}
+            apiSlug={api.apiSlug}
+            isUserApi={isUserApi}
+            deleteApi={deleteApi}
           />
         )}
       </AccordionDetails>
@@ -585,14 +688,85 @@ function ApiAccordion({ translator }) {
 
 export default function Apis() {
   const i18n = useI18n();
+  const { userApis, builtinApis, addApi, deleteApi } = useApiList();
+
+  const apiTypes = useMemo(
+    () =>
+      OPT_ALL_TYPES.map((type) => ({
+        type,
+        label: type,
+      })),
+    []
+  );
+
+  const [anchorEl, setAnchorEl] = useState(null);
+  const open = Boolean(anchorEl);
+
+  const handleClick = (event) => {
+    setAnchorEl(event.currentTarget);
+  };
+
+  const handleClose = () => {
+    setAnchorEl(null);
+  };
+
+  const handleMenuItemClick = (apiType) => {
+    addApi(apiType);
+    handleClose();
+  };
+
   return (
     <Box>
       <Stack spacing={3}>
         <Alert severity="info">{i18n("about_api")}</Alert>
 
         <Box>
-          {OPT_TRANS_ALL.map((translator) => (
-            <ApiAccordion key={translator} translator={translator} />
+          <Button
+            size="small"
+            id="add-api-button"
+            variant="contained"
+            onClick={handleClick}
+            aria-controls={open ? "add-api-menu" : undefined}
+            aria-haspopup="true"
+            aria-expanded={open ? "true" : undefined}
+            endIcon={<KeyboardArrowDownIcon />}
+            startIcon={<AddIcon />}
+          >
+            {i18n("add")}
+          </Button>
+          <Menu
+            id="add-api-menu"
+            anchorEl={anchorEl}
+            open={open}
+            onClose={handleClose}
+            MenuListProps={{
+              "aria-labelledby": "add-api-button",
+            }}
+          >
+            {apiTypes.map((apiOption) => (
+              <MenuItem
+                key={apiOption.type}
+                onClick={() => handleMenuItemClick(apiOption.type)}
+              >
+                {apiOption.label}
+              </MenuItem>
+            ))}
+          </Menu>
+        </Box>
+
+        <Box>
+          {userApis.map((api) => (
+            <ApiAccordion
+              key={api.apiSlug}
+              api={api}
+              isUserApi={true}
+              deleteApi={deleteApi}
+            />
+          ))}
+        </Box>
+        <Box>
+          {builtinApis.map((api) => (
+            <ApiAccordion key={api.apiSlug} api={api} />
           ))}
         </Box>
       </Stack>
diff --git a/src/views/Options/DownloadButton.js b/src/views/Options/DownloadButton.js
index d882ff9..4d865d3 100644
--- a/src/views/Options/DownloadButton.js
+++ b/src/views/Options/DownloadButton.js
@@ -18,7 +18,7 @@ export default function DownloadButton({ handleData, text, fileName }) {
       link.click();
       link.remove();
     } catch (err) {
-      kissLog(err, "download");
+      kissLog("download", err);
     } finally {
       setLoading(false);
     }
diff --git a/src/views/Options/FavWords.js b/src/views/Options/FavWords.js
index 9f0165a..d9d401a 100644
--- a/src/views/Options/FavWords.js
+++ b/src/views/Options/FavWords.js
@@ -5,7 +5,7 @@ import Accordion from "@mui/material/Accordion";
 import AccordionSummary from "@mui/material/AccordionSummary";
 import AccordionDetails from "@mui/material/AccordionDetails";
 import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
-import CircularProgress from "@mui/material/CircularProgress";
+// import CircularProgress from "@mui/material/CircularProgress";
 import { useI18n } from "../../hooks/I18n";
 import Box from "@mui/material/Box";
 import { useFavWords } from "../../hooks/FavWords";
@@ -49,29 +49,25 @@ function FavAccordion({ word, index }) {
 
 export default function FavWords() {
   const i18n = useI18n();
-  const { loading, favWords, mergeWords, clearWords } = useFavWords();
-  const favList = Object.entries(favWords).sort((a, b) =>
-    a[0].localeCompare(b[0])
-  );
-  const downloadList = favList.map(([word]) => word);
+  const { favList, wordList, mergeWords, clearWords } = useFavWords();
 
-  const handleImport = async (data) => {
+  const handleImport = (data) => {
     try {
       const newWords = data
         .split("\n")
         .map((line) => line.split(",")[0].trim())
         .filter(isValidWord);
-      await mergeWords(newWords);
+      mergeWords(newWords);
     } catch (err) {
-      kissLog(err, "import rules");
+      kissLog("import rules", err);
     }
   };
 
   const handleTranslation = async () => {
     const tranList = [];
-    for (const text of downloadList) {
+    for (const text of wordList) {
       try {
-        // todo
+        // todo: 修复
         const dictRes = await apiTranslate({
           text,
           translator: OPT_TRANS_BAIDU,
@@ -122,7 +118,7 @@ export default function FavWords() {
             fileExts={[".txt", ".csv"]}
           />
           <DownloadButton
-            handleData={() => downloadList.join("\n")}
+            handleData={() => wordList.join("\n")}
             text={i18n("export")}
             fileName={`kiss-words_${Date.now()}.txt`}
           />
@@ -144,18 +140,14 @@ export default function FavWords() {
         </Stack>
 
         <Box>
-          {loading ? (
-            <CircularProgress size={24} />
-          ) : (
-            favList.map(([word, { createdAt }], index) => (
-              <FavAccordion
-                key={word}
-                index={index}
-                word={word}
-                createdAt={createdAt}
-              />
-            ))
-          )}
+          {favList.map(([word, { createdAt }], index) => (
+            <FavAccordion
+              key={word}
+              index={index}
+              word={word}
+              createdAt={createdAt}
+            />
+          ))}
         </Box>
       </Stack>
     </Box>
diff --git a/src/views/Options/InputSetting.js b/src/views/Options/InputSetting.js
index 519ba2d..9d4e92c 100644
--- a/src/views/Options/InputSetting.js
+++ b/src/views/Options/InputSetting.js
@@ -4,7 +4,6 @@ import TextField from "@mui/material/TextField";
 import MenuItem from "@mui/material/MenuItem";
 import { useI18n } from "../../hooks/I18n";
 import {
-  OPT_TRANS_ALL,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
   OPT_INPUT_TRANS_SIGNS,
@@ -16,10 +15,12 @@ import { useInputRule } from "../../hooks/InputRule";
 import { useCallback } from "react";
 import Grid from "@mui/material/Grid";
 import { limitNumber } from "../../libs/utils";
+import { useApiList } from "../../hooks/Api";
 
 export default function InputSetting() {
   const i18n = useI18n();
   const { inputRule, updateInputRule } = useInputRule();
+  const { enabledApis } = useApiList();
 
   const handleChange = (e) => {
     e.preventDefault();
@@ -44,7 +45,7 @@ export default function InputSetting() {
 
   const {
     transOpen,
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     triggerShortcut,
@@ -73,14 +74,14 @@ export default function InputSetting() {
         <TextField
           select
           size="small"
-          name="translator"
-          value={translator}
+          name="apiSlug"
+          value={apiSlug}
           label={i18n("translate_service")}
           onChange={handleChange}
         >
-          {OPT_TRANS_ALL.map((item) => (
-            <MenuItem key={item} value={item}>
-              {item}
+          {enabledApis.map((api) => (
+            <MenuItem key={api.apiSlug} value={api.apiSlug}>
+              {api.apiName}
             </MenuItem>
           ))}
         </TextField>
@@ -166,7 +167,7 @@ export default function InputSetting() {
                 label={i18n("combo_timeout")}
                 type="number"
                 name="triggerTime"
-                defaultValue={triggerTime}
+                value={triggerTime}
                 onChange={handleChange}
               />
             </Grid>
diff --git a/src/views/Options/MouseHover.js b/src/views/Options/MouseHover.js
index 4dc3714..44ae590 100644
--- a/src/views/Options/MouseHover.js
+++ b/src/views/Options/MouseHover.js
@@ -7,6 +7,7 @@ import Switch from "@mui/material/Switch";
 import { useMouseHoverSetting } from "../../hooks/MouseHover";
 import { useCallback } from "react";
 import Grid from "@mui/material/Grid";
+import { DEFAULT_MOUSEHOVER_KEY } from "../../config";
 
 export default function MouseHoverSetting() {
   const i18n = useI18n();
@@ -19,7 +20,7 @@ export default function MouseHoverSetting() {
     [updateMouseHoverSetting]
   );
 
-  const { useMouseHover = true, mouseHoverKey = ["ControlLeft"] } =
+  const { useMouseHover = true, mouseHoverKey = DEFAULT_MOUSEHOVER_KEY } =
     mouseHoverSetting;
 
   return (
diff --git a/src/views/Options/OwSubRule.js b/src/views/Options/OwSubRule.js
index 582c508..97fe60e 100644
--- a/src/views/Options/OwSubRule.js
+++ b/src/views/Options/OwSubRule.js
@@ -6,7 +6,6 @@ import {
   REMAIN_KEY,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
-  OPT_TRANS_ALL,
   OPT_STYLE_ALL,
   OPT_STYLE_DIY,
   OPT_STYLE_USE_COLOR,
@@ -15,10 +14,12 @@ import { useI18n } from "../../hooks/I18n";
 import MenuItem from "@mui/material/MenuItem";
 import Grid from "@mui/material/Grid";
 import { useOwSubRule } from "../../hooks/SubRules";
+import { useApiList } from "../../hooks/Api";
 
 export default function OwSubRule() {
   const i18n = useI18n();
   const { owSubrule, updateOwSubrule } = useOwSubRule();
+  const { enabledApis } = useApiList();
 
   const handleChange = (e) => {
     e.preventDefault();
@@ -27,7 +28,7 @@ export default function OwSubRule() {
   };
 
   const {
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     textStyle,
@@ -73,16 +74,16 @@ export default function OwSubRule() {
               select
               size="small"
               fullWidth
-              name="translator"
-              value={translator}
+              name="apiSlug"
+              value={apiSlug}
               label={i18n("translate_service")}
               onChange={handleChange}
             >
               {RemainItem}
               {GlobalItem}
-              {OPT_TRANS_ALL.map((item) => (
-                <MenuItem key={item} value={item}>
-                  {item}
+              {enabledApis.map((api) => (
+                <MenuItem key={api.apiSlug} value={api.apiSlug}>
+                  {api.apiName}
                 </MenuItem>
               ))}
             </TextField>
diff --git a/src/views/Options/ReusableAutocomplete.js b/src/views/Options/ReusableAutocomplete.js
new file mode 100644
index 0000000..3421b39
--- /dev/null
+++ b/src/views/Options/ReusableAutocomplete.js
@@ -0,0 +1,74 @@
+import { useState, useEffect, useRef } from "react";
+import Autocomplete from "@mui/material/Autocomplete";
+import TextField from "@mui/material/TextField";
+
+/**
+ * 一个可复用的 Autocomplete 组件,增加了 name 属性和标准化的 onChange 事件
+ * @param {object} props - 组件的 props
+ * @param {string} props.name - 表单字段的名称,会包含在 onChange 的 event.target 中
+ * @param {string} props.label - TextField 的标签
+ * @param {any} props.value - 受控组件的当前值
+ * @param {function} props.onChange - 值改变时的回调函数 (event) => {}
+ * @param {Array} props.options - Autocomplete 的选项列表
+ */
+export default function ReusableAutocomplete({
+  name,
+  label,
+  value,
+  onChange,
+  ...rest
+}) {
+  const [inputValue, setInputValue] = useState(value || "");
+  const isChangeCommitted = useRef(false);
+
+  useEffect(() => {
+    setInputValue(value || "");
+  }, [value]);
+
+  const triggerOnChange = (newValue) => {
+    if (onChange) {
+      const syntheticEvent = {
+        target: {
+          name: name,
+          value: newValue,
+        },
+      };
+      onChange(syntheticEvent);
+    }
+  };
+
+  const handleBlur = () => {
+    if (isChangeCommitted.current) {
+      isChangeCommitted.current = false;
+      return;
+    }
+
+    if (inputValue !== value) {
+      triggerOnChange(inputValue);
+    }
+  };
+
+  const handleChange = (event, newValue) => {
+    isChangeCommitted.current = true;
+    triggerOnChange(newValue);
+  };
+
+  const handleInputChange = (event, newInputValue) => {
+    isChangeCommitted.current = false;
+    setInputValue(newInputValue);
+  };
+
+  return (
+    <Autocomplete
+      value={value}
+      onChange={handleChange}
+      inputValue={inputValue}
+      onInputChange={handleInputChange}
+      onBlur={handleBlur}
+      {...rest}
+      renderInput={(params) => (
+        <TextField {...params} name={name} label={label} />
+      )}
+    />
+  );
+}
diff --git a/src/views/Options/Rules.js b/src/views/Options/Rules.js
index bdac01f..d008b50 100644
--- a/src/views/Options/Rules.js
+++ b/src/views/Options/Rules.js
@@ -10,7 +10,6 @@ import {
   GLOBLA_RULE,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
-  OPT_TRANS_ALL,
   OPT_STYLE_ALL,
   OPT_STYLE_DIY,
   OPT_STYLE_USE_COLOR,
@@ -58,19 +57,26 @@ import EditIcon from "@mui/icons-material/Edit";
 import CancelIcon from "@mui/icons-material/Cancel";
 import SaveIcon from "@mui/icons-material/Save";
 import { kissLog } from "../../libs/log";
+import { useApiList } from "../../hooks/Api";
 
 function RuleFields({ rule, rules, setShow, setKeyword }) {
-  const initFormValues = {
-    ...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
-    ...(rule || {}),
-  };
-  const editMode = !!rule;
+  const initFormValues = useMemo(
+    () => ({
+      ...(rule?.pattern === "*" ? GLOBLA_RULE : DEFAULT_RULE),
+      ...(rule || {}),
+    }),
+    [rule]
+  );
+  const editMode = useMemo(() => !!rule, [rule]);
 
   const i18n = useI18n();
   const [disabled, setDisabled] = useState(editMode);
   const [errors, setErrors] = useState({});
   const [formValues, setFormValues] = useState(initFormValues);
   const [showMore, setShowMore] = useState(!rules);
+  const [isModified, setIsModified] = useState(false);
+  const { enabledApis } = useApiList();
+
   const {
     pattern,
     selector,
@@ -82,7 +88,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
     parentStyle = "",
     injectJs = "",
     injectCss = "",
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     textStyle,
@@ -106,6 +112,13 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
     // transRemoveHook = "",
   } = formValues;
 
+  useEffect(() => {
+    if (!initFormValues) return;
+    const hasChanged =
+      JSON.stringify(initFormValues) !== JSON.stringify(formValues);
+    setIsModified(hasChanged);
+  }, [initFormValues, formValues]);
+
   const hasSamePattern = (str) => {
     for (const item of rules.list) {
       if (item.pattern === str && rule?.pattern !== str) {
@@ -417,16 +430,16 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
                 select
                 size="small"
                 fullWidth
-                name="translator"
-                value={translator}
+                name="apiSlug"
+                value={apiSlug}
                 label={i18n("translate_service")}
                 disabled={disabled}
                 onChange={handleChange}
               >
                 {GlobalItem}
-                {OPT_TRANS_ALL.map((item) => (
-                  <MenuItem key={item} value={item}>
-                    {item}
+                {enabledApis.map((api) => (
+                  <MenuItem key={api.apiSlug} value={api.apiSlug}>
+                    {api.apiName}
                   </MenuItem>
                 ))}
               </TextField>
@@ -738,6 +751,7 @@ function RuleFields({ rule, rules, setShow, setKeyword }) {
                     variant="contained"
                     type="submit"
                     startIcon={<SaveIcon />}
+                    disabled={!isModified}
                   >
                     {i18n("save")}
                   </Button>
@@ -842,7 +856,7 @@ function ShareButton({ rules, injectRules, selectedUrl }) {
       window.open(url, "_blank");
     } catch (err) {
       alert.warning(i18n("error_got_some_wrong"));
-      kissLog(err, "share rules");
+      kissLog("share rules", err);
     }
   };
 
@@ -871,7 +885,7 @@ function UserRules({ subRules, rules }) {
     try {
       await rules.merge(JSON.parse(data));
     } catch (err) {
-      kissLog(err, "import rules");
+      kissLog("import rules", err);
     }
   };
 
@@ -1004,7 +1018,7 @@ function SubRulesItem({
       await delSubRules(url);
       await deleteDataCache(url);
     } catch (err) {
-      kissLog(err, "del subrules");
+      kissLog("del subrules", err);
     }
   };
 
@@ -1017,7 +1031,7 @@ function SubRulesItem({
       }
       await updateDataCache(url);
     } catch (err) {
-      kissLog(err, "sync sub rules");
+      kissLog("sync sub rules", err);
     } finally {
       setLoading(false);
     }
@@ -1096,7 +1110,7 @@ function SubRulesEdit({ subList, addSub, updateDataCache }) {
       setShowInput(false);
       setInputText("");
     } catch (err) {
-      kissLog(err, "fetch rules");
+      kissLog("fetch rules", err);
       setInputError(i18n("error_fetch_url"));
     } finally {
       setLoading(false);
diff --git a/src/views/Options/Setting.js b/src/views/Options/Setting.js
index 6cfff4b..abe1ce3 100644
--- a/src/views/Options/Setting.js
+++ b/src/views/Options/Setting.js
@@ -96,7 +96,7 @@ export default function Settings() {
       caches.delete(CACHE_NAME);
       alert.success(i18n("clear_success"));
     } catch (err) {
-      kissLog(err, "clear cache");
+      kissLog("clear cache", err);
     }
   };
 
@@ -104,7 +104,7 @@ export default function Settings() {
     try {
       await updateSetting(JSON.parse(data));
     } catch (err) {
-      kissLog(err, "import setting");
+      kissLog("import setting", err);
     }
   };
 
@@ -119,10 +119,10 @@ export default function Settings() {
     touchTranslate = 2,
     blacklist = DEFAULT_BLACKLIST.join(",\n"),
     csplist = DEFAULT_CSPLIST.join(",\n"),
-    transInterval = 200,
+    transInterval = 100,
     langDetector = OPT_TRANS_MICROSOFT,
   } = setting;
-  const { isHide = false, fabClickAction = 0  } = fab || {};
+  const { isHide = false, fabClickAction = 0 } = fab || {};
 
   return (
     <Box>
@@ -163,7 +163,7 @@ export default function Settings() {
           label={i18n("min_translate_length")}
           type="number"
           name="minLength"
-          defaultValue={minLength}
+          value={minLength}
           onChange={handleChange}
         />
 
@@ -172,7 +172,7 @@ export default function Settings() {
           label={i18n("max_translate_length")}
           type="number"
           name="maxLength"
-          defaultValue={maxLength}
+          value={maxLength}
           onChange={handleChange}
         />
 
@@ -181,7 +181,7 @@ export default function Settings() {
           label={i18n("num_of_newline_characters")}
           type="number"
           name="newlineLength"
-          defaultValue={newlineLength}
+          value={newlineLength}
           onChange={handleChange}
         />
 
@@ -190,7 +190,7 @@ export default function Settings() {
           label={i18n("translate_interval")}
           type="number"
           name="transInterval"
-          defaultValue={transInterval}
+          value={transInterval}
           onChange={handleChange}
         />
         <TextField
@@ -198,7 +198,7 @@ export default function Settings() {
           label={i18n("http_timeout")}
           type="number"
           name="httpTimeout"
-          defaultValue={httpTimeout}
+          value={httpTimeout}
           onChange={handleChange}
         />
         <FormControl size="small">
@@ -236,9 +236,9 @@ export default function Settings() {
           <InputLabel>{i18n("fab_click_action")}</InputLabel>
           <Select
             name="fabClickAction"
-            value={fabClickAction}  
+            value={fabClickAction}
             label={i18n("fab_click_action")}
-            onChange= {(e) => updateFab({ fabClickAction: e.target.value })}
+            onChange={(e) => updateFab({ fabClickAction: e.target.value })}
           >
             <MenuItem value={0}>{i18n("fab_click_menu")}</MenuItem>
             <MenuItem value={1}>{i18n("fab_click_translate")}</MenuItem>
@@ -302,7 +302,7 @@ export default function Settings() {
                 i18n("pattern_helper") + " " + i18n("disabled_csplist_helper")
               }
               name="csplist"
-              defaultValue={csplist}
+              value={csplist}
               onChange={handleChange}
               multiline
             />
@@ -345,7 +345,7 @@ export default function Settings() {
           label={i18n("translate_blacklist")}
           helperText={i18n("pattern_helper")}
           name="blacklist"
-          defaultValue={blacklist}
+          value={blacklist}
           onChange={handleChange}
           maxRows={10}
           multiline
diff --git a/src/views/Options/ShortcutInput.js b/src/views/Options/ShortcutInput.js
index a5aae80..4d3ac26 100644
--- a/src/views/Options/ShortcutInput.js
+++ b/src/views/Options/ShortcutInput.js
@@ -2,32 +2,61 @@ import Stack from "@mui/material/Stack";
 import TextField from "@mui/material/TextField";
 import IconButton from "@mui/material/IconButton";
 import EditIcon from "@mui/icons-material/Edit";
+import CheckIcon from "@mui/icons-material/Check";
 import { useEffect, useState, useRef } from "react";
 import { shortcutListener } from "../../libs/shortcut";
+import { useI18n } from "../../hooks/I18n";
 
-export default function ShortcutInput({ value, onChange, label, helperText }) {
-  const [disabled, setDisabled] = useState(true);
+export default function ShortcutInput({
+  value: keys,
+  onChange,
+  label,
+  helperText,
+}) {
+  const [isEditing, setIsEditing] = useState(false);
+  const [editingKeys, setEditingKeys] = useState([]);
   const inputRef = useRef(null);
+  const i18n = useI18n();
+
+  const commitChanges = () => {
+    if (editingKeys.length > 0) {
+      onChange(editingKeys);
+    }
+    setIsEditing(false);
+  };
+
+  const handleBlur = () => {
+    commitChanges();
+  };
+
+  const handleEditClick = () => {
+    setEditingKeys([]);
+    setIsEditing(true);
+  };
 
   useEffect(() => {
-    if (disabled) {
+    if (!isEditing) {
       return;
     }
-
-    inputRef.current.focus();
-    onChange([]);
-
-    const clearShortcut = shortcutListener((curkeys, allkeys) => {
-      onChange(allkeys);
-      if (curkeys.length === 0) {
-        setDisabled(true);
-      }
-    }, inputRef.current);
+    const inputElement = inputRef.current;
+    if (inputElement) {
+      inputElement.focus();
+    }
+    const clearShortcut = shortcutListener((pressedKeys, event) => {
+      event.preventDefault();
+      event.stopPropagation();
+      setEditingKeys([...pressedKeys]);
+    });
 
     return () => {
       clearShortcut();
     };
-  }, [disabled, onChange]);
+  }, [isEditing]);
+
+  const displayValue = isEditing ? editingKeys : keys;
+  const formattedValue = displayValue
+    .map((item) => (item === " " ? "Space" : item))
+    .join(" + ");
 
   return (
     <Stack direction="row" alignItems="flex-start">
@@ -35,22 +64,22 @@ export default function ShortcutInput({ value, onChange, label, helperText }) {
         size="small"
         label={label}
         name={label}
-        value={value.map((item) => (item === " " ? "Space" : item)).join(" + ")}
+        value={formattedValue}
         fullWidth
         inputRef={inputRef}
-        disabled={disabled}
-        onBlur={() => {
-          setDisabled(true);
-        }}
-        helperText={helperText}
+        disabled={!isEditing}
+        onBlur={handleBlur}
+        helperText={isEditing ? i18n("pls_press_shortcut") : helperText}
       />
-      <IconButton
-        onClick={() => {
-          setDisabled(false);
-        }}
-      >
-        {<EditIcon />}
-      </IconButton>
+      {isEditing ? (
+        <IconButton onClick={commitChanges} color="primary">
+          <CheckIcon />
+        </IconButton>
+      ) : (
+        <IconButton onClick={handleEditClick}>
+          <EditIcon />
+        </IconButton>
+      )}
     </Stack>
   );
 }
diff --git a/src/views/Options/SyncSetting.js b/src/views/Options/SyncSetting.js
index d260596..ef23103 100644
--- a/src/views/Options/SyncSetting.js
+++ b/src/views/Options/SyncSetting.js
@@ -21,8 +21,8 @@ import { useAlert } from "../../hooks/Alert";
 import { useSetting } from "../../hooks/Setting";
 import { kissLog } from "../../libs/log";
 import SyncIcon from "@mui/icons-material/Sync";
-import ContentCopyIcon from '@mui/icons-material/ContentCopy';
-import ContentPasteIcon from '@mui/icons-material/ContentPaste';
+import ContentCopyIcon from "@mui/icons-material/ContentCopy";
+import ContentPasteIcon from "@mui/icons-material/ContentPaste";
 
 export default function SyncSetting() {
   const i18n = useI18n();
@@ -44,10 +44,10 @@ export default function SyncSetting() {
     try {
       setLoading(true);
       await syncSettingAndRules();
-      await reloadSetting();
+      reloadSetting();
       alert.success(i18n("sync_success"));
     } catch (err) {
-      kissLog(err, "sync all");
+      kissLog("sync all", err);
       alert.error(i18n("sync_failed"));
     } finally {
       setLoading(false);
@@ -56,37 +56,37 @@ export default function SyncSetting() {
 
   const handleGenerateShareString = async () => {
     try {
-      const base64Config = btoa(JSON.stringify({
-        syncType: syncType,
-        syncUrl: syncUrl,
-        syncUser: syncUser,
-        syncKey: syncKey,
-      }));
+      const base64Config = btoa(
+        JSON.stringify({
+          syncType: syncType,
+          syncUrl: syncUrl,
+          syncUser: syncUser,
+          syncKey: syncKey,
+        })
+      );
       const shareString = `${OPT_SYNCTOKEN_PERFIX}${base64Config}`;
       await navigator.clipboard.writeText(shareString);
-      console.debug("Share string copied to clipboard", shareString);
+      kissLog("Share string copied to clipboard", shareString);
     } catch (error) {
-      console.error("Failed to copy share string to clipboard", error);
+      kissLog("Failed to copy share string to clipboard", error);
     }
   };
 
   const handleImportFromClipboard = async () => {
     try {
       const text = await navigator.clipboard.readText();
-      console.debug('read_clipboard', text)
+      kissLog("read_clipboard", text);
       if (text.startsWith(OPT_SYNCTOKEN_PERFIX)) {
         const base64Config = text.slice(OPT_SYNCTOKEN_PERFIX.length);
         const jsonString = atob(base64Config);
         const updatedConfig = JSON.parse(jsonString);
 
         if (!OPT_SYNCTYPE_ALL.includes(updatedConfig.syncType)) {
-          console.error('error syncType', updatedConfig.syncType)
+          kissLog("error syncType", updatedConfig.syncType);
           return;
         }
 
-        if (
-          updatedConfig.syncUrl
-        ) {
+        if (updatedConfig.syncUrl) {
           updateSync({
             syncType: updatedConfig.syncType,
             syncUrl: updatedConfig.syncUrl,
@@ -94,17 +94,16 @@ export default function SyncSetting() {
             syncKey: updatedConfig.syncKey,
           });
         } else {
-          console.error("Invalid config structure");
+          kissLog("Invalid config structure");
         }
       } else {
-        console.error("Invalid share string", text);
+        kissLog("Invalid share string", text);
       }
     } catch (error) {
-      console.error("Failed to read from clipboard or parse JSON", error);
+      kissLog("Failed to read from clipboard or parse JSON", error);
     }
   };
 
-
   if (!sync) {
     return;
   }
diff --git a/src/views/Options/Tranbox.js b/src/views/Options/Tranbox.js
index 41157e4..03ec314 100644
--- a/src/views/Options/Tranbox.js
+++ b/src/views/Options/Tranbox.js
@@ -4,7 +4,6 @@ import TextField from "@mui/material/TextField";
 import MenuItem from "@mui/material/MenuItem";
 import { useI18n } from "../../hooks/I18n";
 import {
-  OPT_TRANS_ALL,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
   OPT_TRANBOX_TRIGGER_CLICK,
@@ -16,11 +15,13 @@ import { useCallback } from "react";
 import { limitNumber } from "../../libs/utils";
 import { useTranbox } from "../../hooks/Tranbox";
 import { isExt } from "../../libs/client";
+import { useApiList } from "../../hooks/Api";
 import Alert from "@mui/material/Alert";
 
 export default function Tranbox() {
   const i18n = useI18n();
   const { tranboxSetting, updateTranbox } = useTranbox();
+  const { enabledApis } = useApiList();
 
   const handleChange = (e) => {
     e.preventDefault();
@@ -47,7 +48,7 @@ export default function Tranbox() {
   );
 
   const {
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     toLang2 = "en",
@@ -72,14 +73,14 @@ export default function Tranbox() {
         <TextField
           select
           size="small"
-          name="translator"
-          value={translator}
+          name="apiSlug"
+          value={apiSlug}
           label={i18n("translate_service")}
           onChange={handleChange}
         >
-          {OPT_TRANS_ALL.map((item) => (
-            <MenuItem key={item} value={item}>
-              {item}
+          {enabledApis.map((api) => (
+            <MenuItem key={api.apiSlug} value={api.apiSlug}>
+              {api.apiName}
             </MenuItem>
           ))}
         </TextField>
@@ -147,7 +148,7 @@ export default function Tranbox() {
           label={i18n("tranbtn_offset_x")}
           type="number"
           name="btnOffsetX"
-          defaultValue={btnOffsetX}
+          value={btnOffsetX}
           onChange={handleChange}
         />
 
@@ -156,7 +157,7 @@ export default function Tranbox() {
           label={i18n("tranbtn_offset_y")}
           type="number"
           name="btnOffsetY"
-          defaultValue={btnOffsetY}
+          value={btnOffsetY}
           onChange={handleChange}
         />
 
@@ -165,7 +166,7 @@ export default function Tranbox() {
           label={i18n("tranbox_offset_x")}
           type="number"
           name="boxOffsetX"
-          defaultValue={boxOffsetX}
+          value={boxOffsetX}
           onChange={handleChange}
         />
 
@@ -174,7 +175,7 @@ export default function Tranbox() {
           label={i18n("tranbox_offset_y")}
           type="number"
           name="boxOffsetY"
-          defaultValue={boxOffsetY}
+          value={boxOffsetY}
           onChange={handleChange}
         />
 
@@ -245,7 +246,7 @@ export default function Tranbox() {
           size="small"
           label={i18n("extend_styles")}
           name="extStyles"
-          defaultValue={extStyles}
+          value={extStyles}
           onChange={handleChange}
           maxRows={10}
           multiline
diff --git a/src/views/Options/index.js b/src/views/Options/index.js
index 4d5f50e..a167713 100644
--- a/src/views/Options/index.js
+++ b/src/views/Options/index.js
@@ -9,9 +9,9 @@ import ThemeProvider from "../../hooks/Theme";
 import { useEffect, useState } from "react";
 import { isGm } from "../../libs/client";
 import { sleep } from "../../libs/utils";
-import CircularProgress from "@mui/material/CircularProgress";
 import { trySyncSettingAndRules } from "../../libs/sync";
 import { AlertProvider } from "../../hooks/Alert";
+import { ConfirmProvider } from "../../hooks/Confirm";
 import Link from "@mui/material/Link";
 import Divider from "@mui/material/Divider";
 import Stack from "@mui/material/Stack";
@@ -22,6 +22,7 @@ import InputSetting from "./InputSetting";
 import Tranbox from "./Tranbox";
 import FavWords from "./FavWords";
 import MouseHoverSetting from "./MouseHover";
+import Loading from "../../hooks/Loading";
 
 export default function Options() {
   const [error, setError] = useState("");
@@ -91,37 +92,30 @@ export default function Options() {
   }
 
   if (!ready) {
-    return (
-      <center>
-        <Divider>
-          <Link
-            href={process.env.REACT_APP_HOMEPAGE}
-          >{`KISS Translator v${process.env.REACT_APP_VERSION}`}</Link>
-        </Divider>
-        <CircularProgress />
-      </center>
-    );
+    return <Loading />;
   }
 
   return (
     <SettingProvider>
       <ThemeProvider>
         <AlertProvider>
-          <HashRouter>
-            <Routes>
-              <Route path="/" element={<Layout />}>
-                <Route index element={<Setting />} />
-                <Route path="rules" element={<Rules />} />
-                <Route path="input" element={<InputSetting />} />
-                <Route path="tranbox" element={<Tranbox />} />
-                <Route path="mousehover" element={<MouseHoverSetting />} />
-                <Route path="apis" element={<Apis />} />
-                <Route path="sync" element={<SyncSetting />} />
-                <Route path="words" element={<FavWords />} />
-                <Route path="about" element={<About />} />
-              </Route>
-            </Routes>
-          </HashRouter>
+          <ConfirmProvider>
+            <HashRouter>
+              <Routes>
+                <Route path="/" element={<Layout />}>
+                  <Route index element={<Setting />} />
+                  <Route path="rules" element={<Rules />} />
+                  <Route path="input" element={<InputSetting />} />
+                  <Route path="tranbox" element={<Tranbox />} />
+                  <Route path="mousehover" element={<MouseHoverSetting />} />
+                  <Route path="apis" element={<Apis />} />
+                  <Route path="sync" element={<SyncSetting />} />
+                  <Route path="words" element={<FavWords />} />
+                  <Route path="about" element={<About />} />
+                </Route>
+              </Routes>
+            </HashRouter>
+          </ConfirmProvider>
         </AlertProvider>
       </ThemeProvider>
     </SettingProvider>
diff --git a/src/views/Popup/index.js b/src/views/Popup/index.js
index 5979e27..d55c79e 100644
--- a/src/views/Popup/index.js
+++ b/src/views/Popup/index.js
@@ -20,11 +20,9 @@ import {
   MSG_OPEN_OPTIONS,
   MSG_SAVE_RULE,
   MSG_COMMAND_SHORTCUTS,
-  OPT_TRANS_ALL,
   OPT_LANGS_FROM,
   OPT_LANGS_TO,
   OPT_STYLE_ALL,
-  DEFAULT_TRANS_APIS,
 } from "../../config";
 import { sendIframeMsg } from "../../libs/iframe";
 import { saveRule } from "../../libs/rules";
@@ -33,14 +31,14 @@ import { kissLog } from "../../libs/log";
 
 // 插件popup没有参数
 // 网页弹框有
-export default function Popup({ setShowPopup, translator: tran }) {
+export default function Popup({ setShowPopup, translator }) {
   const i18n = useI18n();
-  const [rule, setRule] = useState(tran?.rule);
-  const [transApis, setTransApis] = useState(tran?.setting?.transApis || []);
+  const [rule, setRule] = useState(translator?.rule);
+  const [transApis, setTransApis] = useState(translator?.setting?.transApis || []);
   const [commands, setCommands] = useState({});
 
   const handleOpenSetting = () => {
-    if (!tran) {
+    if (!translator) {
       browser?.runtime.openOptionsPage();
     } else if (isExt) {
       sendBgMsg(MSG_OPEN_OPTIONS);
@@ -54,14 +52,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
     try {
       setRule({ ...rule, transOpen: e.target.checked ? "true" : "false" });
 
-      if (!tran) {
+      if (!translator) {
         await sendTabMsg(MSG_TRANS_TOGGLE);
       } else {
-        tran.toggle();
+        translator.toggle();
         sendIframeMsg(MSG_TRANS_TOGGLE);
       }
     } catch (err) {
-      kissLog(err, "toggle trans");
+      kissLog("toggle trans", err);
     }
   };
 
@@ -70,14 +68,14 @@ export default function Popup({ setShowPopup, translator: tran }) {
       const { name, value } = e.target;
       setRule((pre) => ({ ...pre, [name]: value }));
 
-      if (!tran) {
+      if (!translator) {
         await sendTabMsg(MSG_TRANS_PUTRULE, { [name]: value });
       } else {
-        tran.updateRule({ [name]: value });
+        translator.updateRule({ [name]: value });
         sendIframeMsg(MSG_TRANS_PUTRULE, { [name]: value });
       }
     } catch (err) {
-      kissLog(err, "update rule");
+      kissLog("update rule", err);
     }
   };
 
@@ -88,23 +86,23 @@ export default function Popup({ setShowPopup, translator: tran }) {
   const handleSaveRule = async () => {
     try {
       let href = window.location.href;
-      if (!tran) {
+      if (!translator) {
         const tab = await getCurTab();
         href = tab.url;
       }
       const newRule = { ...rule, pattern: href.split("/")[2] };
-      if (isExt && tran) {
+      if (isExt && translator) {
         sendBgMsg(MSG_SAVE_RULE, newRule);
       } else {
         saveRule(newRule);
       }
     } catch (err) {
-      kissLog(err, "save rule");
+      kissLog("save rule", err);
     }
   };
 
   useEffect(() => {
-    if (tran) {
+    if (translator) {
       return;
     }
     (async () => {
@@ -115,10 +113,10 @@ export default function Popup({ setShowPopup, translator: tran }) {
           setTransApis(res.setting.transApis);
         }
       } catch (err) {
-        kissLog(err, "query rule");
+        kissLog("query rule", err);
       }
     })();
-  }, [tran]);
+  }, [translator]);
 
   useEffect(() => {
     (async () => {
@@ -130,7 +128,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
             commands[name] = shortcut;
           });
         } else {
-          const shortcuts = tran.setting.shortcuts;
+          const shortcuts = translator.setting.shortcuts;
           if (shortcuts) {
             Object.entries(shortcuts).forEach(([key, val]) => {
               commands[key] = val.join("+");
@@ -139,21 +137,18 @@ export default function Popup({ setShowPopup, translator: tran }) {
         }
         setCommands(commands);
       } catch (err) {
-        kissLog(err, "query cmds");
+        kissLog("query cmds", err);
       }
     })();
-  }, [tran]);
+  }, [translator]);
 
   const optApis = useMemo(
     () =>
-      OPT_TRANS_ALL.map((key) => ({
-        ...(transApis[key] || DEFAULT_TRANS_APIS[key]),
-        apiKey: key,
-      }))
-        .filter((item) => !item.isDisabled)
-        .map(({ apiKey, apiName }) => ({
-          key: apiKey,
-          name: apiName?.trim() || apiKey,
+      transApis
+        .filter((api) => !api.isDisabled)
+        .map((api) => ({
+          key: api.apiSlug,
+          name: api.apiName || api.apiSlug,
         })),
     [transApis]
   );
@@ -161,7 +156,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
   if (!rule) {
     return (
       <Box minWidth={300}>
-        {!tran && (
+        {!translator && (
           <>
             <Header />
             <Divider />
@@ -178,7 +173,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
 
   const {
     transOpen,
-    translator,
+    apiSlug,
     fromLang,
     toLang,
     textStyle,
@@ -190,7 +185,7 @@ export default function Popup({ setShowPopup, translator: tran }) {
 
   return (
     <Box width={320}>
-      {!tran && (
+      {!translator && (
         <>
           <Header />
           <Divider />
@@ -275,8 +270,8 @@ export default function Popup({ setShowPopup, translator: tran }) {
           select
           SelectProps={{ MenuProps: { disablePortal: true } }}
           size="small"
-          value={translator}
-          name="translator"
+          value={apiSlug}
+          name="apiSlug"
           label={i18n("translate_service")}
           onChange={handleChange}
         >
diff --git a/src/views/Selection/DictCont.js b/src/views/Selection/DictCont.js
index 5674676..c2b706c 100644
--- a/src/views/Selection/DictCont.js
+++ b/src/views/Selection/DictCont.js
@@ -1,4 +1,4 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
 import Stack from "@mui/material/Stack";
 import FavBtn from "./FavBtn";
 import Typography from "@mui/material/Typography";
@@ -26,10 +26,10 @@ export default function DictCont({ text }) {
           return;
         }
 
-        // todo
+        // todo: 修复
         const dictRes = await apiTranslate({
           text,
-          translator: OPT_TRANS_BAIDU,
+          apiSlug: OPT_TRANS_BAIDU,
           fromLang: "en",
           toLang: "zh-CN",
         });
@@ -45,70 +45,74 @@ export default function DictCont({ text }) {
     })();
   }, [text]);
 
-  if (error) {
-    return <Alert severity="error">{error}</Alert>;
-  }
+  const copyText = useMemo(() => {
+    if (!dictResult) {
+      return text;
+    }
+
+    return [
+      dictResult.src,
+      dictResult.voice
+        ?.map(Object.entries)
+        .map((item) => item[0])
+        .map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
+        .join(" "),
+      dictResult.content[0].mean
+        .map(({ pre, cont }) => {
+          return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
+        })
+        .join("\n"),
+    ].join("\n");
+  }, [text, dictResult]);
 
   if (loading) {
     return <CircularProgress size={16} />;
   }
 
-  if (!text || !dictResult) {
-    return;
-  }
-
-  const copyText = [
-    dictResult.src,
-    dictResult.voice
-      ?.map(Object.entries)
-      .map((item) => item[0])
-      .map(([key, val]) => `${PHONIC_MAP[key]?.[0] || key} ${val}`)
-      .join(" "),
-    dictResult.content[0].mean
-      .map(({ pre, cont }) => {
-        return `${pre ? `[${pre}] ` : ""}${Object.keys(cont).join("; ")}`;
-      })
-      .join("\n"),
-  ].join("\n");
-
   return (
     <Stack className="KT-transbox-dict" spacing={1}>
-      <Stack direction="row" justifyContent="space-between">
-        <Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
-          {dictResult.src}
-        </Typography>
+      {text && (
         <Stack direction="row" justifyContent="space-between">
-          <CopyBtn text={copyText} />
-          <FavBtn word={dictResult.src} />
+          <Typography variant="subtitle1" style={{ fontWeight: "bold" }}>
+            {dictResult?.src || text}
+          </Typography>
+          <Stack direction="row" justifyContent="space-between">
+            <CopyBtn text={copyText} />
+            <FavBtn word={dictResult?.src || text} />
+          </Stack>
         </Stack>
-      </Stack>
+      )}
 
-      <Typography component="div">
+      {error && <Alert severity="error">{error}</Alert>}
+
+      {dictResult && (
         <Typography component="div">
-          {dictResult.voice
-            ?.map(Object.entries)
-            .map((item) => item[0])
-            .map(([key, val]) => (
-              <Typography
-                component="div"
-                key={key}
-                style={{ display: "inline-block" }}
-              >
-                <Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
-                <AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
+          <Typography component="div">
+            {dictResult.voice
+              ?.map(Object.entries)
+              .map((item) => item[0])
+              .map(([key, val]) => (
+                <Typography
+                  component="div"
+                  key={key}
+                  style={{ display: "inline-block" }}
+                >
+                  <Typography component="span">{`${PHONIC_MAP[key]?.[0] || key} ${val}`}</Typography>
+                  <AudioBtn text={dictResult.src} lan={PHONIC_MAP[key]?.[1]} />
+                </Typography>
+              ))}
+          </Typography>
+
+          <Typography component="ul">
+            {dictResult.content[0].mean.map(({ pre, cont }, idx) => (
+              <Typography component="li" key={idx}>
+                {pre && `[${pre}] `}
+                {Object.keys(cont).join("; ")}
               </Typography>
             ))}
+          </Typography>
         </Typography>
-
-        <Typography component="ul">
-          {dictResult.content[0].mean.map(({ pre, cont }, idx) => (
-            <Typography component="li" key={idx}>
-              {pre && `[${pre}] `}
-              {Object.keys(cont).join("; ")}
-            </Typography>
-          ))}
-        </Typography>
-      </Typography>
+      )}
     </Stack>
   );
 }
diff --git a/src/views/Selection/FavBtn.js b/src/views/Selection/FavBtn.js
index 417b900..553efc2 100644
--- a/src/views/Selection/FavBtn.js
+++ b/src/views/Selection/FavBtn.js
@@ -9,12 +9,12 @@ export default function FavBtn({ word }) {
   const { favWords, toggleFav } = useFavWords();
   const [loading, setLoading] = useState(false);
 
-  const handleClick = async () => {
+  const handleClick = () => {
     try {
       setLoading(true);
-      await toggleFav(word);
+      toggleFav(word);
     } catch (err) {
-      kissLog(err, "set fav");
+      kissLog("set fav", err);
     } finally {
       setLoading(false);
     }
diff --git a/src/views/Selection/TranBox.js b/src/views/Selection/TranBox.js
index 843d927..b6e34f6 100644
--- a/src/views/Selection/TranBox.js
+++ b/src/views/Selection/TranBox.js
@@ -18,12 +18,7 @@ import LockIcon from "@mui/icons-material/Lock";
 import LockOpenIcon from "@mui/icons-material/LockOpen";
 import CloseIcon from "@mui/icons-material/Close";
 import { useI18n } from "../../hooks/I18n";
-import {
-  OPT_TRANS_ALL,
-  OPT_LANGS_FROM,
-  OPT_LANGS_TO,
-  DEFAULT_TRANS_APIS,
-} from "../../config";
+import { OPT_LANGS_FROM, OPT_LANGS_TO } from "../../config";
 import { useState, useRef, useMemo } from "react";
 import TranCont from "./TranCont";
 import DictCont from "./DictCont";
@@ -119,21 +114,18 @@ function TranForm({
 
   const [editMode, setEditMode] = useState(false);
   const [editText, setEditText] = useState("");
-  const [translator, setTranslator] = useState(tranboxSetting.translator);
+  const [apiSlug, setApiSlug] = useState(tranboxSetting.apiSlug);
   const [fromLang, setFromLang] = useState(tranboxSetting.fromLang);
   const [toLang, setToLang] = useState(tranboxSetting.toLang);
   const inputRef = useRef(null);
 
   const optApis = useMemo(
     () =>
-      OPT_TRANS_ALL.map((key) => ({
-        ...(transApis[key] || DEFAULT_TRANS_APIS[key]),
-        apiKey: key,
-      }))
-        .filter((item) => !item.isDisabled)
-        .map(({ apiKey, apiName }) => ({
-          key: apiKey,
-          name: apiName?.trim() || apiKey,
+      transApis
+        .filter((api) => !api.isDisabled)
+        .map((api) => ({
+          key: api.apiSlug,
+          name: api.apiName || api.apiSlug,
         })),
     [transApis]
   );
@@ -194,11 +186,11 @@ function TranForm({
                   SelectProps={{ MenuProps: { disablePortal: true } }}
                   fullWidth
                   size="small"
-                  value={translator}
-                  name="translator"
+                  value={apiSlug}
+                  name="apiSlug"
                   label={i18n("translate_service")}
                   onChange={(e) => {
-                    setTranslator(e.target.value);
+                    setApiSlug(e.target.value);
                   }}
                 >
                   {optApis.map(({ key, name }) => (
@@ -266,7 +258,7 @@ function TranForm({
         enDict === "-") && (
         <TranCont
           text={text}
-          translator={translator}
+          apiSlug={apiSlug}
           fromLang={fromLang}
           toLang={toLang}
           toLang2={tranboxSetting.toLang2}
@@ -307,6 +299,7 @@ export default function TranBox({
   enDict,
 }) {
   const [mouseHover, setMouseHover] = useState(false);
+  // todo: 这里的 SettingProvider 不应和 background 的共用
   return (
     <SettingProvider>
       <ThemeProvider styles={extStyles}>
diff --git a/src/views/Selection/TranCont.js b/src/views/Selection/TranCont.js
index 8175a89..f2e9d85 100644
--- a/src/views/Selection/TranCont.js
+++ b/src/views/Selection/TranCont.js
@@ -3,7 +3,7 @@ import Box from "@mui/material/Box";
 import CircularProgress from "@mui/material/CircularProgress";
 import Stack from "@mui/material/Stack";
 import { useI18n } from "../../hooks/I18n";
-import { DEFAULT_TRANS_APIS } from "../../config";
+import { DEFAULT_API_SETTING } from "../../config";
 import { useEffect, useState } from "react";
 import { apiTranslate } from "../../apis";
 import CopyBtn from "./CopyBtn";
@@ -13,7 +13,7 @@ import { tryDetectLang } from "../../libs";
 
 export default function TranCont({
   text,
-  translator,
+  apiSlug,
   fromLang,
   toLang,
   toLang2 = "en",
@@ -42,10 +42,11 @@ export default function TranCont({
         }
 
         const apiSetting =
-          transApis[translator] || DEFAULT_TRANS_APIS[translator];
+          transApis.find((api) => api.apiSlug === apiSlug) ||
+          DEFAULT_API_SETTING;
         const [trText] = await apiTranslate({
           text,
-          translator,
+          apiSlug,
           fromLang,
           toLang: to,
           apiSetting,
@@ -57,7 +58,7 @@ export default function TranCont({
         setLoading(false);
       }
     })();
-  }, [text, translator, fromLang, toLang, toLang2, transApis, langDetector]);
+  }, [text, apiSlug, fromLang, toLang, toLang2, transApis, langDetector]);
 
   if (simpleStyle) {
     return (
diff --git a/src/views/Selection/index.js b/src/views/Selection/index.js
index 17431af..b32feba 100644
--- a/src/views/Selection/index.js
+++ b/src/views/Selection/index.js
@@ -201,7 +201,7 @@ export default function Slection({
         });
       };
     } catch (err) {
-      kissLog(err, "registerMenuCommand");
+      kissLog("registerMenuCommand", err);
     }
   }, [handleTranbox, contextMenuType, langMap]);