1 line
175 KiB
JSON
1 line
175 KiB
JSON
{"repo": "1095327780/flownote", "files": {"runtime/mobile/mobile-ai-service.js": {"b9a52c8e1971ac33e6e52994cdfd9e51748d92c2": {"t": 1785745999.5847034, "n": 19249, "h": "519b7cf66acb7fbc", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\nconst { getProviderSpec } = require(\"../providers/registry\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n openai: { name: \"OpenAI\", baseUrl: \"https://api.openai.com\", defaultModel: \"gpt-5.5\", keyUrl: \"https://platform.openai.com/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen3.6-flash\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.ai\", defaultModel: \"kimi-k2.6\", keyUrl: \"https://platform.kimi.ai/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-5.1\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n openrouter: { name: \"OpenRouter\", baseUrl: \"https://openrouter.ai/api/v1\", defaultModel: \"openrouter/auto\", keyUrl: \"https://openrouter.ai/keys\", chatPath: \"/chat/completions\", modelsPath: \"/models\", models: [\"openrouter/auto\"] },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst AGENT_TO_CAPTURE_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n \"moonshot-kimi\": \"moonshot\",\n \"zhipu-glm\": \"zhipu\",\n \"openai-official\": \"openai\",\n \"openai-compat-custom\": \"custom\",\n};\n\nconst CAPTURE_TO_AGENT_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n moonshot: \"moonshot-kimi\",\n zhipu: \"zhipu-glm\",\n openai: \"openai-official\",\n custom: \"openai-compat-custom\",\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_RU = [\n \"Вы помощник для очистки голосовой расшифровки. Ваша задача - превратить быстрые устные заметки в ясный текст, который можно сразу записать в ежедневную заметку.\",\n \"Что можно делать:\",\n \"1. Удалять слова-паразиты и речевые заполнители, например: эм, ну, типа, значит, как бы.\",\n \"2. Удалять очевидные повторы, фальстарты и запинки.\",\n \"3. Исправлять ошибки распознавания речи, омонимы и явные опечатки, когда правильная формулировка понятна из контекста.\",\n \"4. Добавлять нужную пунктуацию и делить текст на предложения; длинный ввод можно разделить на 2-4 естественных абзаца.\",\n \"5. Сохранять основные мысли, факты, имена собственные, числа, URL и личный стиль пользователя.\",\n \"Нельзя:\",\n \"1. Не добавляйте идеи, примеры или выводы, которых нет в исходном тексте.\",\n \"2. Не превращайте заметку в резюме, заголовок, список или эссе, если исходный текст не имеет такой структуры.\",\n \"3. Не переписывайте текст в маркетинговом или чрезмерно официальном стиле.\",\n \"4. Не удаляйте содержательную информацию.\",\n \"Верните только очищенный текст, без объяснений, префиксов и кавычек.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_RU = [\n \"Вы помощник для краткого описания URL. В пользовательском тексте есть URL, и содержимое страниц уже получено.\",\n \"Верните исходный текст без изменений и добавьте строки с кратким описанием.\",\n \"Формат: каждая строка `> 📎 OriginalURL - Краткое описание`\",\n \"Правила:\",\n \"- Сохраняйте все исходные URL без изменений\",\n \"- Описание не длиннее 50 символов\",\n \"- Не меняйте текст вне URL\",\n \"- Если данных недостаточно, используйте \\\"Не удалось обработать, исходная ссылка сохранена\\\"\",\n \"- Верните полный обработанный текст без объяснений\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_RU = [\n \"Вы помощник для резервной обработки URL.\",\n \"Задача: выполнить минимальную обработку, когда содержимое страницы недоступно.\",\n \"Правила:\",\n \"1. Сохраняйте исходный текст и все URL точно как есть\",\n \"2. Не переписывайте текст и не выдумывайте содержимое страницы\",\n \"3. Можно только добавить строки вида `> 📎 OriginalURL - Не удалось обработать, исходная ссылка сохранена`\",\n \"4. Верните полный обработанный текст напрямую\",\n].join(\"\\n\");\n\nfunction localeCode(locale) {\n return normalizeSupportedLocale(locale, \"en\");\n}\n\nfunction getCaptureSystemPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return CAPTURE_SYSTEM_PROMPT;\n if (code === \"ru\") return CAPTURE_SYSTEM_PROMPT_RU;\n return CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_SUMMARY_PROMPT;\n if (code === \"ru\") return URL_SUMMARY_PROMPT_RU;\n return URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_FALLBACK_PROMPT;\n if (code === \"ru\") return URL_FALLBACK_PROMPT_RU;\n return URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n const code = localeCode(locale);\n if (id === \"custom\") return code === \"zh-CN\" ? \"自定义\" : code === \"ru\" ? \"Пользовательский\" : \"Custom\";\n if (id === \"qwen\") return code === \"zh-CN\" ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return code === \"zh-CN\" ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction mobileAiMessage(locale, key, params = {}) {\n const code = localeCode(locale);\n const messages = {\n aiMissingConfig: {\n \"zh-CN\": \"AI 服务未配置:缺少 Base URL 或 API Key\",\n en: \"AI is not configured: missing Base URL or API Key\",\n ru: \"AI не настроен: отсутствует Base URL или API Key\",\n },\n aiRequestFailed: {\n \"zh-CN\": \"AI 请求失败 ({status}): {snippet}\",\n en: \"AI request failed ({status}): {snippet}\",\n ru: \"Запрос к AI не удался ({status}): {snippet}\",\n },\n aiResponseEmpty: {\n \"zh-CN\": \"AI 返回内容为空\",\n en: \"AI returned empty content\",\n ru: \"AI вернул пустой ответ\",\n },\n modelListFailed: {\n \"zh-CN\": \"模型列表请求失败 ({status}): {snippet}\",\n en: \"Model list request failed ({status}): {snippet}\",\n ru: \"Не удалось получить список моделей ({status}): {snippet}\",\n },\n testProbe: {\n \"zh-CN\": \"嗯,这是一个测试\",\n en: \"um, this is a test\",\n ru: \"эм, это тест\",\n },\n connected: {\n \"zh-CN\": \"连接成功,返回: \\\"{result}\\\"\",\n en: \"Connected. Response: \\\"{result}\\\"\",\n ru: \"Подключено. Ответ: \\\"{result}\\\"\",\n },\n };\n const template = (messages[key] && (messages[key][code] || messages[key].en)) || key;\n return String(template).replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_match, name) => {\n const value = params[name];\n return value === undefined || value === null ? \"\" : String(value);\n });\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const activeAgentProviderId = String(direct && direct.providerId || \"\").trim();\n\n if (direct && activeAgentProviderId && activeAgentProviderId !== \"ollama\") {\n const mappedProviderId = AGENT_TO_CAPTURE_PROVIDER[activeAgentProviderId];\n if (mappedProviderId) {\n const preset = PROVIDER_PRESETS[mappedProviderId] || PROVIDER_PRESETS.deepseek;\n out.provider = mappedProviderId;\n out.apiKey = String(\n (direct.apiKeys && typeof direct.apiKeys === \"object\" && direct.apiKeys[activeAgentProviderId])\n || \"\",\n ).trim();\n out.model = String(direct.model || preset.defaultModel || \"\").trim();\n out.baseUrl = activeAgentProviderId === \"openai-compat-custom\"\n ? String(direct.baseUrlOverride || \"\").trim()\n : \"\";\n return out;\n }\n }\n\n const providerId = String(out.provider || \"deepseek\").trim();\n out.provider = providerId;\n return out;\n}\n\nfunction buildMobileAgentSettingsOverride(pluginSettings) {\n const settings = pluginSettings && typeof pluginSettings === \"object\" ? pluginSettings : {};\n const agent = settings.agentProvider && typeof settings.agentProvider === \"object\" ? settings.agentProvider : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n if (!agent || !direct || String(direct.providerId || \"\").trim() !== \"ollama\") return null;\n\n const capture = resolveEffectiveCaptureSettings(settings);\n const captureProviderId = String(capture.provider || \"\").trim();\n const agentProviderId = CAPTURE_TO_AGENT_PROVIDER[captureProviderId];\n if (!agentProviderId) return null;\n const spec = getProviderSpec(agentProviderId);\n if (!spec) return null;\n\n const preset = PROVIDER_PRESETS[captureProviderId] || {};\n const apiKey = String(capture.apiKey || \"\").trim();\n const baseUrlOverride = captureProviderId === \"custom\" ? String(capture.baseUrl || \"\").trim() : \"\";\n if (!apiKey && !spec.apiKeyOptional) return null;\n if (agentProviderId === \"openai-compat-custom\" && !baseUrlOverride) return null;\n\n const model = String(capture.model || preset.defaultModel || spec.defaultModel || \"\").trim();\n if (!model) return null;\n\n const providerMode = spec.modes && spec.modes.api ? \"api\" : spec.defaultMode;\n return {\n ...agent,\n mode: \"direct\",\n direct: {\n ...(agent.direct || {}),\n providerId: agentProviderId,\n providerMode,\n model,\n baseUrlOverride,\n region: \"\",\n stream: direct.stream,\n apiKeys: {\n ...(direct.apiKeys || {}),\n [agentProviderId]: apiKey,\n },\n },\n };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && (ai.apiKey || ai.preset.apiKeyOptional));\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"aiRequestFailed\", { status: response.status, snippet }));\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(mobileAiMessage(locale, \"aiResponseEmpty\"));\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers,\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"modelListFailed\", { status: response.status, snippet }));\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = mobileAiMessage(locale, \"testProbe\");\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: mobileAiMessage(locale, \"connected\", { result }),\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n CAPTURE_SYSTEM_PROMPT_RU,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n buildMobileAgentSettingsOverride,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "28e8675afad2d95e6cb0e3b2464ec352ac4e4f45": {"t": 1785758658.7533448, "n": 19249, "h": "519b7cf66acb7fbc", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\nconst { getProviderSpec } = require(\"../providers/registry\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n openai: { name: \"OpenAI\", baseUrl: \"https://api.openai.com\", defaultModel: \"gpt-5.5\", keyUrl: \"https://platform.openai.com/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen3.6-flash\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.ai\", defaultModel: \"kimi-k2.6\", keyUrl: \"https://platform.kimi.ai/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-5.1\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n openrouter: { name: \"OpenRouter\", baseUrl: \"https://openrouter.ai/api/v1\", defaultModel: \"openrouter/auto\", keyUrl: \"https://openrouter.ai/keys\", chatPath: \"/chat/completions\", modelsPath: \"/models\", models: [\"openrouter/auto\"] },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst AGENT_TO_CAPTURE_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n \"moonshot-kimi\": \"moonshot\",\n \"zhipu-glm\": \"zhipu\",\n \"openai-official\": \"openai\",\n \"openai-compat-custom\": \"custom\",\n};\n\nconst CAPTURE_TO_AGENT_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n moonshot: \"moonshot-kimi\",\n zhipu: \"zhipu-glm\",\n openai: \"openai-official\",\n custom: \"openai-compat-custom\",\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_RU = [\n \"Вы помощник для очистки голосовой расшифровки. Ваша задача - превратить быстрые устные заметки в ясный текст, который можно сразу записать в ежедневную заметку.\",\n \"Что можно делать:\",\n \"1. Удалять слова-паразиты и речевые заполнители, например: эм, ну, типа, значит, как бы.\",\n \"2. Удалять очевидные повторы, фальстарты и запинки.\",\n \"3. Исправлять ошибки распознавания речи, омонимы и явные опечатки, когда правильная формулировка понятна из контекста.\",\n \"4. Добавлять нужную пунктуацию и делить текст на предложения; длинный ввод можно разделить на 2-4 естественных абзаца.\",\n \"5. Сохранять основные мысли, факты, имена собственные, числа, URL и личный стиль пользователя.\",\n \"Нельзя:\",\n \"1. Не добавляйте идеи, примеры или выводы, которых нет в исходном тексте.\",\n \"2. Не превращайте заметку в резюме, заголовок, список или эссе, если исходный текст не имеет такой структуры.\",\n \"3. Не переписывайте текст в маркетинговом или чрезмерно официальном стиле.\",\n \"4. Не удаляйте содержательную информацию.\",\n \"Верните только очищенный текст, без объяснений, префиксов и кавычек.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_RU = [\n \"Вы помощник для краткого описания URL. В пользовательском тексте есть URL, и содержимое страниц уже получено.\",\n \"Верните исходный текст без изменений и добавьте строки с кратким описанием.\",\n \"Формат: каждая строка `> 📎 OriginalURL - Краткое описание`\",\n \"Правила:\",\n \"- Сохраняйте все исходные URL без изменений\",\n \"- Описание не длиннее 50 символов\",\n \"- Не меняйте текст вне URL\",\n \"- Если данных недостаточно, используйте \\\"Не удалось обработать, исходная ссылка сохранена\\\"\",\n \"- Верните полный обработанный текст без объяснений\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_RU = [\n \"Вы помощник для резервной обработки URL.\",\n \"Задача: выполнить минимальную обработку, когда содержимое страницы недоступно.\",\n \"Правила:\",\n \"1. Сохраняйте исходный текст и все URL точно как есть\",\n \"2. Не переписывайте текст и не выдумывайте содержимое страницы\",\n \"3. Можно только добавить строки вида `> 📎 OriginalURL - Не удалось обработать, исходная ссылка сохранена`\",\n \"4. Верните полный обработанный текст напрямую\",\n].join(\"\\n\");\n\nfunction localeCode(locale) {\n return normalizeSupportedLocale(locale, \"en\");\n}\n\nfunction getCaptureSystemPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return CAPTURE_SYSTEM_PROMPT;\n if (code === \"ru\") return CAPTURE_SYSTEM_PROMPT_RU;\n return CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_SUMMARY_PROMPT;\n if (code === \"ru\") return URL_SUMMARY_PROMPT_RU;\n return URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_FALLBACK_PROMPT;\n if (code === \"ru\") return URL_FALLBACK_PROMPT_RU;\n return URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n const code = localeCode(locale);\n if (id === \"custom\") return code === \"zh-CN\" ? \"自定义\" : code === \"ru\" ? \"Пользовательский\" : \"Custom\";\n if (id === \"qwen\") return code === \"zh-CN\" ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return code === \"zh-CN\" ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction mobileAiMessage(locale, key, params = {}) {\n const code = localeCode(locale);\n const messages = {\n aiMissingConfig: {\n \"zh-CN\": \"AI 服务未配置:缺少 Base URL 或 API Key\",\n en: \"AI is not configured: missing Base URL or API Key\",\n ru: \"AI не настроен: отсутствует Base URL или API Key\",\n },\n aiRequestFailed: {\n \"zh-CN\": \"AI 请求失败 ({status}): {snippet}\",\n en: \"AI request failed ({status}): {snippet}\",\n ru: \"Запрос к AI не удался ({status}): {snippet}\",\n },\n aiResponseEmpty: {\n \"zh-CN\": \"AI 返回内容为空\",\n en: \"AI returned empty content\",\n ru: \"AI вернул пустой ответ\",\n },\n modelListFailed: {\n \"zh-CN\": \"模型列表请求失败 ({status}): {snippet}\",\n en: \"Model list request failed ({status}): {snippet}\",\n ru: \"Не удалось получить список моделей ({status}): {snippet}\",\n },\n testProbe: {\n \"zh-CN\": \"嗯,这是一个测试\",\n en: \"um, this is a test\",\n ru: \"эм, это тест\",\n },\n connected: {\n \"zh-CN\": \"连接成功,返回: \\\"{result}\\\"\",\n en: \"Connected. Response: \\\"{result}\\\"\",\n ru: \"Подключено. Ответ: \\\"{result}\\\"\",\n },\n };\n const template = (messages[key] && (messages[key][code] || messages[key].en)) || key;\n return String(template).replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_match, name) => {\n const value = params[name];\n return value === undefined || value === null ? \"\" : String(value);\n });\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const activeAgentProviderId = String(direct && direct.providerId || \"\").trim();\n\n if (direct && activeAgentProviderId && activeAgentProviderId !== \"ollama\") {\n const mappedProviderId = AGENT_TO_CAPTURE_PROVIDER[activeAgentProviderId];\n if (mappedProviderId) {\n const preset = PROVIDER_PRESETS[mappedProviderId] || PROVIDER_PRESETS.deepseek;\n out.provider = mappedProviderId;\n out.apiKey = String(\n (direct.apiKeys && typeof direct.apiKeys === \"object\" && direct.apiKeys[activeAgentProviderId])\n || \"\",\n ).trim();\n out.model = String(direct.model || preset.defaultModel || \"\").trim();\n out.baseUrl = activeAgentProviderId === \"openai-compat-custom\"\n ? String(direct.baseUrlOverride || \"\").trim()\n : \"\";\n return out;\n }\n }\n\n const providerId = String(out.provider || \"deepseek\").trim();\n out.provider = providerId;\n return out;\n}\n\nfunction buildMobileAgentSettingsOverride(pluginSettings) {\n const settings = pluginSettings && typeof pluginSettings === \"object\" ? pluginSettings : {};\n const agent = settings.agentProvider && typeof settings.agentProvider === \"object\" ? settings.agentProvider : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n if (!agent || !direct || String(direct.providerId || \"\").trim() !== \"ollama\") return null;\n\n const capture = resolveEffectiveCaptureSettings(settings);\n const captureProviderId = String(capture.provider || \"\").trim();\n const agentProviderId = CAPTURE_TO_AGENT_PROVIDER[captureProviderId];\n if (!agentProviderId) return null;\n const spec = getProviderSpec(agentProviderId);\n if (!spec) return null;\n\n const preset = PROVIDER_PRESETS[captureProviderId] || {};\n const apiKey = String(capture.apiKey || \"\").trim();\n const baseUrlOverride = captureProviderId === \"custom\" ? String(capture.baseUrl || \"\").trim() : \"\";\n if (!apiKey && !spec.apiKeyOptional) return null;\n if (agentProviderId === \"openai-compat-custom\" && !baseUrlOverride) return null;\n\n const model = String(capture.model || preset.defaultModel || spec.defaultModel || \"\").trim();\n if (!model) return null;\n\n const providerMode = spec.modes && spec.modes.api ? \"api\" : spec.defaultMode;\n return {\n ...agent,\n mode: \"direct\",\n direct: {\n ...(agent.direct || {}),\n providerId: agentProviderId,\n providerMode,\n model,\n baseUrlOverride,\n region: \"\",\n stream: direct.stream,\n apiKeys: {\n ...(direct.apiKeys || {}),\n [agentProviderId]: apiKey,\n },\n },\n };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && (ai.apiKey || ai.preset.apiKeyOptional));\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"aiRequestFailed\", { status: response.status, snippet }));\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(mobileAiMessage(locale, \"aiResponseEmpty\"));\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers,\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"modelListFailed\", { status: response.status, snippet }));\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = mobileAiMessage(locale, \"testProbe\");\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: mobileAiMessage(locale, \"connected\", { result }),\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n CAPTURE_SYSTEM_PROMPT_RU,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n buildMobileAgentSettingsOverride,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "6e73484d6fd41e38b4162f6c16b6f130cc7fa1aa": {"t": 1785758660.279488, "n": 19249, "h": "e3343298337553bf", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\nconst { getProviderSpec } = require(\"../providers/registry\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n openai: { name: \"OpenAI\", baseUrl: \"https://api.openai.com\", defaultModel: \"gpt-5.5\", keyUrl: \"https://platform.openai.com/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen3.6-flash\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.ai\", defaultModel: \"kimi-k2.6\", keyUrl: \"https://platform.kimi.ai/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-5.1\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n openrouter: { name: \"OpenRouter\", baseUrl: \"https://openrouter.ai/api/v1\", defaultModel: \"openrouter/free\", keyUrl: \"https://openrouter.ai/keys\", chatPath: \"/chat/completions\", modelsPath: \"/models\", models: [\"openrouter/free\"] },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst AGENT_TO_CAPTURE_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n \"moonshot-kimi\": \"moonshot\",\n \"zhipu-glm\": \"zhipu\",\n \"openai-official\": \"openai\",\n \"openai-compat-custom\": \"custom\",\n};\n\nconst CAPTURE_TO_AGENT_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n moonshot: \"moonshot-kimi\",\n zhipu: \"zhipu-glm\",\n openai: \"openai-official\",\n custom: \"openai-compat-custom\",\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_RU = [\n \"Вы помощник для очистки голосовой расшифровки. Ваша задача - превратить быстрые устные заметки в ясный текст, который можно сразу записать в ежедневную заметку.\",\n \"Что можно делать:\",\n \"1. Удалять слова-паразиты и речевые заполнители, например: эм, ну, типа, значит, как бы.\",\n \"2. Удалять очевидные повторы, фальстарты и запинки.\",\n \"3. Исправлять ошибки распознавания речи, омонимы и явные опечатки, когда правильная формулировка понятна из контекста.\",\n \"4. Добавлять нужную пунктуацию и делить текст на предложения; длинный ввод можно разделить на 2-4 естественных абзаца.\",\n \"5. Сохранять основные мысли, факты, имена собственные, числа, URL и личный стиль пользователя.\",\n \"Нельзя:\",\n \"1. Не добавляйте идеи, примеры или выводы, которых нет в исходном тексте.\",\n \"2. Не превращайте заметку в резюме, заголовок, список или эссе, если исходный текст не имеет такой структуры.\",\n \"3. Не переписывайте текст в маркетинговом или чрезмерно официальном стиле.\",\n \"4. Не удаляйте содержательную информацию.\",\n \"Верните только очищенный текст, без объяснений, префиксов и кавычек.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_RU = [\n \"Вы помощник для краткого описания URL. В пользовательском тексте есть URL, и содержимое страниц уже получено.\",\n \"Верните исходный текст без изменений и добавьте строки с кратким описанием.\",\n \"Формат: каждая строка `> 📎 OriginalURL - Краткое описание`\",\n \"Правила:\",\n \"- Сохраняйте все исходные URL без изменений\",\n \"- Описание не длиннее 50 символов\",\n \"- Не меняйте текст вне URL\",\n \"- Если данных недостаточно, используйте \\\"Не удалось обработать, исходная ссылка сохранена\\\"\",\n \"- Верните полный обработанный текст без объяснений\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_RU = [\n \"Вы помощник для резервной обработки URL.\",\n \"Задача: выполнить минимальную обработку, когда содержимое страницы недоступно.\",\n \"Правила:\",\n \"1. Сохраняйте исходный текст и все URL точно как есть\",\n \"2. Не переписывайте текст и не выдумывайте содержимое страницы\",\n \"3. Можно только добавить строки вида `> 📎 OriginalURL - Не удалось обработать, исходная ссылка сохранена`\",\n \"4. Верните полный обработанный текст напрямую\",\n].join(\"\\n\");\n\nfunction localeCode(locale) {\n return normalizeSupportedLocale(locale, \"en\");\n}\n\nfunction getCaptureSystemPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return CAPTURE_SYSTEM_PROMPT;\n if (code === \"ru\") return CAPTURE_SYSTEM_PROMPT_RU;\n return CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_SUMMARY_PROMPT;\n if (code === \"ru\") return URL_SUMMARY_PROMPT_RU;\n return URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_FALLBACK_PROMPT;\n if (code === \"ru\") return URL_FALLBACK_PROMPT_RU;\n return URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n const code = localeCode(locale);\n if (id === \"custom\") return code === \"zh-CN\" ? \"自定义\" : code === \"ru\" ? \"Пользовательский\" : \"Custom\";\n if (id === \"qwen\") return code === \"zh-CN\" ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return code === \"zh-CN\" ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction mobileAiMessage(locale, key, params = {}) {\n const code = localeCode(locale);\n const messages = {\n aiMissingConfig: {\n \"zh-CN\": \"AI 服务未配置:缺少 Base URL 或 API Key\",\n en: \"AI is not configured: missing Base URL or API Key\",\n ru: \"AI не настроен: отсутствует Base URL или API Key\",\n },\n aiRequestFailed: {\n \"zh-CN\": \"AI 请求失败 ({status}): {snippet}\",\n en: \"AI request failed ({status}): {snippet}\",\n ru: \"Запрос к AI не удался ({status}): {snippet}\",\n },\n aiResponseEmpty: {\n \"zh-CN\": \"AI 返回内容为空\",\n en: \"AI returned empty content\",\n ru: \"AI вернул пустой ответ\",\n },\n modelListFailed: {\n \"zh-CN\": \"模型列表请求失败 ({status}): {snippet}\",\n en: \"Model list request failed ({status}): {snippet}\",\n ru: \"Не удалось получить список моделей ({status}): {snippet}\",\n },\n testProbe: {\n \"zh-CN\": \"嗯,这是一个测试\",\n en: \"um, this is a test\",\n ru: \"эм, это тест\",\n },\n connected: {\n \"zh-CN\": \"连接成功,返回: \\\"{result}\\\"\",\n en: \"Connected. Response: \\\"{result}\\\"\",\n ru: \"Подключено. Ответ: \\\"{result}\\\"\",\n },\n };\n const template = (messages[key] && (messages[key][code] || messages[key].en)) || key;\n return String(template).replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_match, name) => {\n const value = params[name];\n return value === undefined || value === null ? \"\" : String(value);\n });\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const activeAgentProviderId = String(direct && direct.providerId || \"\").trim();\n\n if (direct && activeAgentProviderId && activeAgentProviderId !== \"ollama\") {\n const mappedProviderId = AGENT_TO_CAPTURE_PROVIDER[activeAgentProviderId];\n if (mappedProviderId) {\n const preset = PROVIDER_PRESETS[mappedProviderId] || PROVIDER_PRESETS.deepseek;\n out.provider = mappedProviderId;\n out.apiKey = String(\n (direct.apiKeys && typeof direct.apiKeys === \"object\" && direct.apiKeys[activeAgentProviderId])\n || \"\",\n ).trim();\n out.model = String(direct.model || preset.defaultModel || \"\").trim();\n out.baseUrl = activeAgentProviderId === \"openai-compat-custom\"\n ? String(direct.baseUrlOverride || \"\").trim()\n : \"\";\n return out;\n }\n }\n\n const providerId = String(out.provider || \"deepseek\").trim();\n out.provider = providerId;\n return out;\n}\n\nfunction buildMobileAgentSettingsOverride(pluginSettings) {\n const settings = pluginSettings && typeof pluginSettings === \"object\" ? pluginSettings : {};\n const agent = settings.agentProvider && typeof settings.agentProvider === \"object\" ? settings.agentProvider : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n if (!agent || !direct || String(direct.providerId || \"\").trim() !== \"ollama\") return null;\n\n const capture = resolveEffectiveCaptureSettings(settings);\n const captureProviderId = String(capture.provider || \"\").trim();\n const agentProviderId = CAPTURE_TO_AGENT_PROVIDER[captureProviderId];\n if (!agentProviderId) return null;\n const spec = getProviderSpec(agentProviderId);\n if (!spec) return null;\n\n const preset = PROVIDER_PRESETS[captureProviderId] || {};\n const apiKey = String(capture.apiKey || \"\").trim();\n const baseUrlOverride = captureProviderId === \"custom\" ? String(capture.baseUrl || \"\").trim() : \"\";\n if (!apiKey && !spec.apiKeyOptional) return null;\n if (agentProviderId === \"openai-compat-custom\" && !baseUrlOverride) return null;\n\n const model = String(capture.model || preset.defaultModel || spec.defaultModel || \"\").trim();\n if (!model) return null;\n\n const providerMode = spec.modes && spec.modes.api ? \"api\" : spec.defaultMode;\n return {\n ...agent,\n mode: \"direct\",\n direct: {\n ...(agent.direct || {}),\n providerId: agentProviderId,\n providerMode,\n model,\n baseUrlOverride,\n region: \"\",\n stream: direct.stream,\n apiKeys: {\n ...(direct.apiKeys || {}),\n [agentProviderId]: apiKey,\n },\n },\n };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && (ai.apiKey || ai.preset.apiKeyOptional));\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"aiRequestFailed\", { status: response.status, snippet }));\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(mobileAiMessage(locale, \"aiResponseEmpty\"));\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers,\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"modelListFailed\", { status: response.status, snippet }));\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = mobileAiMessage(locale, \"testProbe\");\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: mobileAiMessage(locale, \"connected\", { result }),\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n CAPTURE_SYSTEM_PROMPT_RU,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n buildMobileAgentSettingsOverride,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "facc9dd7e7084046f47619216b502ad61e9c860c": {"t": 1785758661.7331107, "n": 16791, "h": "07c6cb4c3777c35c", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen-turbo\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.cn\", defaultModel: \"moonshot-v1-8k\", keyUrl: \"https://platform.moonshot.cn/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-4-flash\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n openrouter: { name: \"OpenRouter\", baseUrl: \"https://openrouter.ai/api/v1\", defaultModel: \"openrouter/free\", keyUrl: \"https://openrouter.ai/keys\", chatPath: \"/chat/completions\", modelsPath: \"/models\", models: [\"openrouter/free\"] },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_RU = [\n \"Вы помощник для очистки голосовой расшифровки. Ваша задача - превратить быстрые устные заметки в ясный текст, который можно сразу записать в ежедневную заметку.\",\n \"Что можно делать:\",\n \"1. Удалять слова-паразиты и речевые заполнители, например: эм, ну, типа, значит, как бы.\",\n \"2. Удалять очевидные повторы, фальстарты и запинки.\",\n \"3. Исправлять ошибки распознавания речи, омонимы и явные опечатки, когда правильная формулировка понятна из контекста.\",\n \"4. Добавлять нужную пунктуацию и делить текст на предложения; длинный ввод можно разделить на 2-4 естественных абзаца.\",\n \"5. Сохранять основные мысли, факты, имена собственные, числа, URL и личный стиль пользователя.\",\n \"Нельзя:\",\n \"1. Не добавляйте идеи, примеры или выводы, которых нет в исходном тексте.\",\n \"2. Не превращайте заметку в резюме, заголовок, список или эссе, если исходный текст не имеет такой структуры.\",\n \"3. Не переписывайте текст в маркетинговом или чрезмерно официальном стиле.\",\n \"4. Не удаляйте содержательную информацию.\",\n \"Верните только очищенный текст, без объяснений, префиксов и кавычек.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_RU = [\n \"Вы помощник для краткого описания URL. В пользовательском тексте есть URL, и содержимое страниц уже получено.\",\n \"Верните исходный текст без изменений и добавьте строки с кратким описанием.\",\n \"Формат: каждая строка `> 📎 OriginalURL - Краткое описание`\",\n \"Правила:\",\n \"- Сохраняйте все исходные URL без изменений\",\n \"- Описание не длиннее 50 символов\",\n \"- Не меняйте текст вне URL\",\n \"- Если данных недостаточно, используйте \\\"Не удалось обработать, исходная ссылка сохранена\\\"\",\n \"- Верните полный обработанный текст без объяснений\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_RU = [\n \"Вы помощник для резервной обработки URL.\",\n \"Задача: выполнить минимальную обработку, когда содержимое страницы недоступно.\",\n \"Правила:\",\n \"1. Сохраняйте исходный текст и все URL точно как есть\",\n \"2. Не переписывайте текст и не выдумывайте содержимое страницы\",\n \"3. Можно только добавить строки вида `> 📎 OriginalURL - Не удалось обработать, исходная ссылка сохранена`\",\n \"4. Верните полный обработанный текст напрямую\",\n].join(\"\\n\");\n\nfunction localeCode(locale) {\n return normalizeSupportedLocale(locale, \"en\");\n}\n\nfunction getCaptureSystemPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return CAPTURE_SYSTEM_PROMPT;\n if (code === \"ru\") return CAPTURE_SYSTEM_PROMPT_RU;\n return CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_SUMMARY_PROMPT;\n if (code === \"ru\") return URL_SUMMARY_PROMPT_RU;\n return URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_FALLBACK_PROMPT;\n if (code === \"ru\") return URL_FALLBACK_PROMPT_RU;\n return URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n const code = localeCode(locale);\n if (id === \"custom\") return code === \"zh-CN\" ? \"自定义\" : code === \"ru\" ? \"Пользовательский\" : \"Custom\";\n if (id === \"qwen\") return code === \"zh-CN\" ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return code === \"zh-CN\" ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction mobileAiMessage(locale, key, params = {}) {\n const code = localeCode(locale);\n const messages = {\n aiMissingConfig: {\n \"zh-CN\": \"AI 服务未配置:缺少 Base URL 或 API Key\",\n en: \"AI is not configured: missing Base URL or API Key\",\n ru: \"AI не настроен: отсутствует Base URL или API Key\",\n },\n aiRequestFailed: {\n \"zh-CN\": \"AI 请求失败 ({status}): {snippet}\",\n en: \"AI request failed ({status}): {snippet}\",\n ru: \"Запрос к AI не удался ({status}): {snippet}\",\n },\n aiResponseEmpty: {\n \"zh-CN\": \"AI 返回内容为空\",\n en: \"AI returned empty content\",\n ru: \"AI вернул пустой ответ\",\n },\n modelListFailed: {\n \"zh-CN\": \"模型列表请求失败 ({status}): {snippet}\",\n en: \"Model list request failed ({status}): {snippet}\",\n ru: \"Не удалось получить список моделей ({status}): {snippet}\",\n },\n testProbe: {\n \"zh-CN\": \"嗯,这是一个测试\",\n en: \"um, this is a test\",\n ru: \"эм, это тест\",\n },\n connected: {\n \"zh-CN\": \"连接成功,返回: \\\"{result}\\\"\",\n en: \"Connected. Response: \\\"{result}\\\"\",\n ru: \"Подключено. Ответ: \\\"{result}\\\"\",\n },\n };\n const template = (messages[key] && (messages[key][code] || messages[key].en)) || key;\n return String(template).replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_match, name) => {\n const value = params[name];\n return value === undefined || value === null ? \"\" : String(value);\n });\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const providerId = String(out.provider || (direct && direct.providerId) || \"deepseek\").trim();\n out.provider = providerId;\n\n if (!String(out.apiKey || \"\").trim() && direct && direct.apiKeys && typeof direct.apiKeys === \"object\") {\n const keyForCaptureProvider = String(direct.apiKeys[providerId] || \"\").trim();\n const keyForActiveProvider = String(direct.apiKeys[direct.providerId] || \"\").trim();\n out.apiKey = keyForCaptureProvider || (direct.providerId === providerId ? keyForActiveProvider : \"\");\n }\n\n if (\n providerId === \"custom\"\n && !String(out.baseUrl || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.baseUrl = String(direct.baseUrlOverride || \"\").trim();\n }\n\n if (\n providerId === \"custom\"\n && !String(out.model || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.model = String(direct.model || \"\").trim();\n }\n\n return out;\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && ai.apiKey);\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"aiRequestFailed\", { status: response.status, snippet }));\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(mobileAiMessage(locale, \"aiResponseEmpty\"));\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"modelListFailed\", { status: response.status, snippet }));\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = mobileAiMessage(locale, \"testProbe\");\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: mobileAiMessage(locale, \"connected\", { result }),\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n CAPTURE_SYSTEM_PROMPT_RU,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "ec314224277ac69b39172f7fce92c3edd849dae3": {"t": 1785758663.0819657, "n": 16558, "h": "4a6e794e15b85304", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen-turbo\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.cn\", defaultModel: \"moonshot-v1-8k\", keyUrl: \"https://platform.moonshot.cn/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-4-flash\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_RU = [\n \"Вы помощник для очистки голосовой расшифровки. Ваша задача - превратить быстрые устные заметки в ясный текст, который можно сразу записать в ежедневную заметку.\",\n \"Что можно делать:\",\n \"1. Удалять слова-паразиты и речевые заполнители, например: эм, ну, типа, значит, как бы.\",\n \"2. Удалять очевидные повторы, фальстарты и запинки.\",\n \"3. Исправлять ошибки распознавания речи, омонимы и явные опечатки, когда правильная формулировка понятна из контекста.\",\n \"4. Добавлять нужную пунктуацию и делить текст на предложения; длинный ввод можно разделить на 2-4 естественных абзаца.\",\n \"5. Сохранять основные мысли, факты, имена собственные, числа, URL и личный стиль пользователя.\",\n \"Нельзя:\",\n \"1. Не добавляйте идеи, примеры или выводы, которых нет в исходном тексте.\",\n \"2. Не превращайте заметку в резюме, заголовок, список или эссе, если исходный текст не имеет такой структуры.\",\n \"3. Не переписывайте текст в маркетинговом или чрезмерно официальном стиле.\",\n \"4. Не удаляйте содержательную информацию.\",\n \"Верните только очищенный текст, без объяснений, префиксов и кавычек.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_RU = [\n \"Вы помощник для краткого описания URL. В пользовательском тексте есть URL, и содержимое страниц уже получено.\",\n \"Верните исходный текст без изменений и добавьте строки с кратким описанием.\",\n \"Формат: каждая строка `> 📎 OriginalURL - Краткое описание`\",\n \"Правила:\",\n \"- Сохраняйте все исходные URL без изменений\",\n \"- Описание не длиннее 50 символов\",\n \"- Не меняйте текст вне URL\",\n \"- Если данных недостаточно, используйте \\\"Не удалось обработать, исходная ссылка сохранена\\\"\",\n \"- Верните полный обработанный текст без объяснений\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_RU = [\n \"Вы помощник для резервной обработки URL.\",\n \"Задача: выполнить минимальную обработку, когда содержимое страницы недоступно.\",\n \"Правила:\",\n \"1. Сохраняйте исходный текст и все URL точно как есть\",\n \"2. Не переписывайте текст и не выдумывайте содержимое страницы\",\n \"3. Можно только добавить строки вида `> 📎 OriginalURL - Не удалось обработать, исходная ссылка сохранена`\",\n \"4. Верните полный обработанный текст напрямую\",\n].join(\"\\n\");\n\nfunction localeCode(locale) {\n return normalizeSupportedLocale(locale, \"en\");\n}\n\nfunction getCaptureSystemPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return CAPTURE_SYSTEM_PROMPT;\n if (code === \"ru\") return CAPTURE_SYSTEM_PROMPT_RU;\n return CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_SUMMARY_PROMPT;\n if (code === \"ru\") return URL_SUMMARY_PROMPT_RU;\n return URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n const code = localeCode(locale);\n if (code === \"zh-CN\") return URL_FALLBACK_PROMPT;\n if (code === \"ru\") return URL_FALLBACK_PROMPT_RU;\n return URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n const code = localeCode(locale);\n if (id === \"custom\") return code === \"zh-CN\" ? \"自定义\" : code === \"ru\" ? \"Пользовательский\" : \"Custom\";\n if (id === \"qwen\") return code === \"zh-CN\" ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return code === \"zh-CN\" ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction mobileAiMessage(locale, key, params = {}) {\n const code = localeCode(locale);\n const messages = {\n aiMissingConfig: {\n \"zh-CN\": \"AI 服务未配置:缺少 Base URL 或 API Key\",\n en: \"AI is not configured: missing Base URL or API Key\",\n ru: \"AI не настроен: отсутствует Base URL или API Key\",\n },\n aiRequestFailed: {\n \"zh-CN\": \"AI 请求失败 ({status}): {snippet}\",\n en: \"AI request failed ({status}): {snippet}\",\n ru: \"Запрос к AI не удался ({status}): {snippet}\",\n },\n aiResponseEmpty: {\n \"zh-CN\": \"AI 返回内容为空\",\n en: \"AI returned empty content\",\n ru: \"AI вернул пустой ответ\",\n },\n modelListFailed: {\n \"zh-CN\": \"模型列表请求失败 ({status}): {snippet}\",\n en: \"Model list request failed ({status}): {snippet}\",\n ru: \"Не удалось получить список моделей ({status}): {snippet}\",\n },\n testProbe: {\n \"zh-CN\": \"嗯,这是一个测试\",\n en: \"um, this is a test\",\n ru: \"эм, это тест\",\n },\n connected: {\n \"zh-CN\": \"连接成功,返回: \\\"{result}\\\"\",\n en: \"Connected. Response: \\\"{result}\\\"\",\n ru: \"Подключено. Ответ: \\\"{result}\\\"\",\n },\n };\n const template = (messages[key] && (messages[key][code] || messages[key].en)) || key;\n return String(template).replace(/\\{([a-zA-Z0-9_]+)\\}/g, (_match, name) => {\n const value = params[name];\n return value === undefined || value === null ? \"\" : String(value);\n });\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const providerId = String(out.provider || (direct && direct.providerId) || \"deepseek\").trim();\n out.provider = providerId;\n\n if (!String(out.apiKey || \"\").trim() && direct && direct.apiKeys && typeof direct.apiKeys === \"object\") {\n const keyForCaptureProvider = String(direct.apiKeys[providerId] || \"\").trim();\n const keyForActiveProvider = String(direct.apiKeys[direct.providerId] || \"\").trim();\n out.apiKey = keyForCaptureProvider || (direct.providerId === providerId ? keyForActiveProvider : \"\");\n }\n\n if (\n providerId === \"custom\"\n && !String(out.baseUrl || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.baseUrl = String(direct.baseUrlOverride || \"\").trim();\n }\n\n if (\n providerId === \"custom\"\n && !String(out.model || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.model = String(direct.model || \"\").trim();\n }\n\n return out;\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && ai.apiKey);\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"aiRequestFailed\", { status: response.status, snippet }));\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(mobileAiMessage(locale, \"aiResponseEmpty\"));\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(mobileAiMessage(locale, \"aiMissingConfig\"));\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n throw new Error(mobileAiMessage(locale, \"modelListFailed\", { status: response.status, snippet }));\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = mobileAiMessage(locale, \"testProbe\");\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: mobileAiMessage(locale, \"connected\", { result }),\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n CAPTURE_SYSTEM_PROMPT_RU,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "c9097d27e292247741dffe4f8510492cb7bec194": {"t": 1785758664.3433952, "n": 15431, "h": "a8a7bcaf28d23316", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\nconst { getProviderSpec } = require(\"../providers/registry\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n openai: { name: \"OpenAI\", baseUrl: \"https://api.openai.com\", defaultModel: \"gpt-5.5\", keyUrl: \"https://platform.openai.com/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen3.6-flash\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.ai\", defaultModel: \"kimi-k2.6\", keyUrl: \"https://platform.kimi.ai/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-5.1\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst AGENT_TO_CAPTURE_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n \"moonshot-kimi\": \"moonshot\",\n \"zhipu-glm\": \"zhipu\",\n \"openai-official\": \"openai\",\n \"openai-compat-custom\": \"custom\",\n};\n\nconst CAPTURE_TO_AGENT_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n moonshot: \"moonshot-kimi\",\n zhipu: \"zhipu-glm\",\n openai: \"openai-official\",\n custom: \"openai-compat-custom\",\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nfunction isZh(locale) {\n return normalizeSupportedLocale(locale) === \"zh-CN\";\n}\n\nfunction getCaptureSystemPrompt(locale) {\n return isZh(locale) ? CAPTURE_SYSTEM_PROMPT : CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n return isZh(locale) ? URL_SUMMARY_PROMPT : URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n return isZh(locale) ? URL_FALLBACK_PROMPT : URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n if (id === \"custom\") return isZh(locale) ? \"自定义\" : \"Custom\";\n if (id === \"openai\") return \"OpenAI\";\n if (id === \"qwen\") return isZh(locale) ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return isZh(locale) ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const activeAgentProviderId = String(direct && direct.providerId || \"\").trim();\n\n if (direct && activeAgentProviderId && activeAgentProviderId !== \"ollama\") {\n const mappedProviderId = AGENT_TO_CAPTURE_PROVIDER[activeAgentProviderId];\n if (mappedProviderId) {\n const preset = PROVIDER_PRESETS[mappedProviderId] || PROVIDER_PRESETS.deepseek;\n out.provider = mappedProviderId;\n out.apiKey = String(\n (direct.apiKeys && typeof direct.apiKeys === \"object\" && direct.apiKeys[activeAgentProviderId])\n || \"\",\n ).trim();\n out.model = String(direct.model || preset.defaultModel || \"\").trim();\n out.baseUrl = activeAgentProviderId === \"openai-compat-custom\"\n ? String(direct.baseUrlOverride || \"\").trim()\n : \"\";\n return out;\n }\n }\n\n const providerId = String(out.provider || \"deepseek\").trim();\n out.provider = providerId;\n return out;\n}\n\nfunction buildMobileAgentSettingsOverride(pluginSettings) {\n const settings = pluginSettings && typeof pluginSettings === \"object\" ? pluginSettings : {};\n const agent = settings.agentProvider && typeof settings.agentProvider === \"object\" ? settings.agentProvider : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n if (!agent || !direct || String(direct.providerId || \"\").trim() !== \"ollama\") return null;\n\n const capture = resolveEffectiveCaptureSettings(settings);\n const captureProviderId = String(capture.provider || \"\").trim();\n const agentProviderId = CAPTURE_TO_AGENT_PROVIDER[captureProviderId];\n if (!agentProviderId) return null;\n const spec = getProviderSpec(agentProviderId);\n if (!spec) return null;\n\n const preset = PROVIDER_PRESETS[captureProviderId] || {};\n const apiKey = String(capture.apiKey || \"\").trim();\n const baseUrlOverride = captureProviderId === \"custom\" ? String(capture.baseUrl || \"\").trim() : \"\";\n if (!apiKey && !spec.apiKeyOptional) return null;\n if (agentProviderId === \"openai-compat-custom\" && !baseUrlOverride) return null;\n\n const model = String(capture.model || preset.defaultModel || spec.defaultModel || \"\").trim();\n if (!model) return null;\n\n const providerMode = spec.modes && spec.modes.api ? \"api\" : spec.defaultMode;\n return {\n ...agent,\n mode: \"direct\",\n direct: {\n ...(agent.direct || {}),\n providerId: agentProviderId,\n providerMode,\n model,\n baseUrlOverride,\n region: \"\",\n stream: direct.stream,\n apiKeys: {\n ...(direct.apiKeys || {}),\n [agentProviderId]: apiKey,\n },\n },\n };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && (ai.apiKey || ai.preset.apiKeyOptional));\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || (!ai.apiKey && !ai.preset.apiKeyOptional)) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`AI 请求失败 (${response.status}): ${snippet}`);\n throw new Error(`AI request failed (${response.status}): ${snippet}`);\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(isZh(locale) ? \"AI 返回内容为空\" : \"AI returned empty content\");\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || (!ai.apiKey && !ai.preset.apiKeyOptional)) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers,\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`模型列表请求失败 (${response.status}): ${snippet}`);\n throw new Error(`Model list request failed (${response.status}): ${snippet}`);\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = isZh(locale) ? \"嗯,这是一个测试\" : \"um, this is a test\";\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: isZh(locale) ? `连接成功,返回: \"${result}\"` : `Connected. Response: \"${result}\"`,\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n buildMobileAgentSettingsOverride,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "6543e8e67960e9d2483e37652c07993ee39a6979": {"t": 1785758665.7321553, "n": 15441, "h": "c2d8faa3cac2f6cd", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\nconst { getProviderSpec } = require(\"../providers/registry\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n openai: { name: \"OpenAI\", baseUrl: \"https://api.openai.com\", defaultModel: \"gpt-5.4\", keyUrl: \"https://platform.openai.com/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen-turbo\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.cn\", defaultModel: \"moonshot-v1-8k\", keyUrl: \"https://platform.moonshot.cn/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-4-flash\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst AGENT_TO_CAPTURE_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n \"moonshot-kimi\": \"moonshot\",\n \"zhipu-glm\": \"zhipu\",\n \"openai-official\": \"openai\",\n \"openai-compat-custom\": \"custom\",\n};\n\nconst CAPTURE_TO_AGENT_PROVIDER = {\n deepseek: \"deepseek\",\n qwen: \"qwen\",\n moonshot: \"moonshot-kimi\",\n zhipu: \"zhipu-glm\",\n openai: \"openai-official\",\n custom: \"openai-compat-custom\",\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nfunction isZh(locale) {\n return normalizeSupportedLocale(locale) === \"zh-CN\";\n}\n\nfunction getCaptureSystemPrompt(locale) {\n return isZh(locale) ? CAPTURE_SYSTEM_PROMPT : CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n return isZh(locale) ? URL_SUMMARY_PROMPT : URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n return isZh(locale) ? URL_FALLBACK_PROMPT : URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n if (id === \"custom\") return isZh(locale) ? \"自定义\" : \"Custom\";\n if (id === \"openai\") return \"OpenAI\";\n if (id === \"qwen\") return isZh(locale) ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return isZh(locale) ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const activeAgentProviderId = String(direct && direct.providerId || \"\").trim();\n\n if (direct && activeAgentProviderId && activeAgentProviderId !== \"ollama\") {\n const mappedProviderId = AGENT_TO_CAPTURE_PROVIDER[activeAgentProviderId];\n if (mappedProviderId) {\n const preset = PROVIDER_PRESETS[mappedProviderId] || PROVIDER_PRESETS.deepseek;\n out.provider = mappedProviderId;\n out.apiKey = String(\n (direct.apiKeys && typeof direct.apiKeys === \"object\" && direct.apiKeys[activeAgentProviderId])\n || \"\",\n ).trim();\n out.model = String(direct.model || preset.defaultModel || \"\").trim();\n out.baseUrl = activeAgentProviderId === \"openai-compat-custom\"\n ? String(direct.baseUrlOverride || \"\").trim()\n : \"\";\n return out;\n }\n }\n\n const providerId = String(out.provider || \"deepseek\").trim();\n out.provider = providerId;\n return out;\n}\n\nfunction buildMobileAgentSettingsOverride(pluginSettings) {\n const settings = pluginSettings && typeof pluginSettings === \"object\" ? pluginSettings : {};\n const agent = settings.agentProvider && typeof settings.agentProvider === \"object\" ? settings.agentProvider : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n if (!agent || !direct || String(direct.providerId || \"\").trim() !== \"ollama\") return null;\n\n const capture = resolveEffectiveCaptureSettings(settings);\n const captureProviderId = String(capture.provider || \"\").trim();\n const agentProviderId = CAPTURE_TO_AGENT_PROVIDER[captureProviderId];\n if (!agentProviderId) return null;\n const spec = getProviderSpec(agentProviderId);\n if (!spec) return null;\n\n const preset = PROVIDER_PRESETS[captureProviderId] || {};\n const apiKey = String(capture.apiKey || \"\").trim();\n const baseUrlOverride = captureProviderId === \"custom\" ? String(capture.baseUrl || \"\").trim() : \"\";\n if (!apiKey && !spec.apiKeyOptional) return null;\n if (agentProviderId === \"openai-compat-custom\" && !baseUrlOverride) return null;\n\n const model = String(capture.model || preset.defaultModel || spec.defaultModel || \"\").trim();\n if (!model) return null;\n\n const providerMode = spec.modes && spec.modes.api ? \"api\" : spec.defaultMode;\n return {\n ...agent,\n mode: \"direct\",\n direct: {\n ...(agent.direct || {}),\n providerId: agentProviderId,\n providerMode,\n model,\n baseUrlOverride,\n region: \"\",\n stream: direct.stream,\n apiKeys: {\n ...(direct.apiKeys || {}),\n [agentProviderId]: apiKey,\n },\n },\n };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && (ai.apiKey || ai.preset.apiKeyOptional));\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || (!ai.apiKey && !ai.preset.apiKeyOptional)) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers,\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`AI 请求失败 (${response.status}): ${snippet}`);\n throw new Error(`AI request failed (${response.status}): ${snippet}`);\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(isZh(locale) ? \"AI 返回内容为空\" : \"AI returned empty content\");\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || (!ai.apiKey && !ai.preset.apiKeyOptional)) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n const headers = { \"Content-Type\": \"application/json\" };\n if (ai.apiKey || !ai.preset.apiKeyOptional) {\n headers.Authorization = `Bearer ${ai.apiKey}`;\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers,\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`模型列表请求失败 (${response.status}): ${snippet}`);\n throw new Error(`Model list request failed (${response.status}): ${snippet}`);\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = isZh(locale) ? \"嗯,这是一个测试\" : \"um, this is a test\";\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: isZh(locale) ? `连接成功,返回: \"${result}\"` : `Connected. Response: \"${result}\"`,\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n buildMobileAgentSettingsOverride,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "6ae05216a1c503bb8fc9bf6ab7526e7cdc78660a": {"t": 1785758666.9723516, "n": 12838, "h": "40d17a561b201e17", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-v4-flash\",\n keyUrl: \"https://platform.deepseek.com/api_keys\",\n chatPath: \"/chat/completions\",\n modelsPath: \"/models\",\n models: [\"deepseek-v4-flash\", \"deepseek-v4-pro\", \"deepseek-chat\", \"deepseek-reasoner\"],\n },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen-turbo\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.cn\", defaultModel: \"moonshot-v1-8k\", keyUrl: \"https://platform.moonshot.cn/console/api-keys\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-4-flash\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\", chatPath: \"/v4/chat/completions\", modelsPath: \"/v4/models\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\", chatPath: \"/v1/chat/completions\", modelsPath: \"/v1/models\" },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个语音转录清理助手。你的任务是把快速捕获的口语化输入整理成可以直接写入日记的清楚文字。\",\n \"处理范围:\",\n \"1. 删除语气词、填充词和口头禅,如:嗯、啊、那个、就是、然后、对、哦、emmm、额。\",\n \"2. 删除明显的重复、自我修正和卡顿片段,如“他自己那自己他自己”“陪陪伴式”。\",\n \"3. 修正上下文能确定的语音识别错字、同音错词和明显笔误,如“人社”应按语境修为“人设”。\",\n \"4. 补充必要标点,调整断句;内容较长时可以分成 2-4 个自然段。\",\n \"5. 保留原文的核心观点、事实、专有名词、数字、URL 和用户自己的表达倾向。\",\n \"禁止:\",\n \"1. 不要新增原文没有的观点、例子或结论。\",\n \"2. 不要总结成标题、要点清单或读书笔记结构,除非原文就是这种结构。\",\n \"3. 不要改成营销文、正式文章或过度润色的风格。\",\n \"4. 不要删除有信息量的内容。\",\n \"直接返回整理后的文本,不要任何解释、前缀或引号。\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a voice-transcript cleanup assistant. Your task is to turn quick spoken captures into clear text that can be written directly into a daily note.\",\n \"Allowed cleanup:\",\n \"1. Remove filler words and verbal tics such as um, uh, like, you know, so, right.\",\n \"2. Remove obvious repetitions, false starts, and stutters.\",\n \"3. Fix speech-recognition mistakes, homophone errors, and obvious typos when the intended wording is clear from context.\",\n \"4. Add necessary punctuation and sentence breaks; split long input into 2-4 natural paragraphs when helpful.\",\n \"5. Preserve the user's core ideas, facts, proper nouns, numbers, URLs, and personal voice.\",\n \"Do not:\",\n \"1. Do not add ideas, examples, or conclusions that are not in the source.\",\n \"2. Do not convert the capture into a summary, title, bullet list, or essay unless the source already has that structure.\",\n \"3. Do not over-polish it into marketing or formal prose.\",\n \"4. Do not remove substantive content.\",\n \"Return only the cleaned text, with no explanation, prefix, or quotes.\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nfunction isZh(locale) {\n return normalizeSupportedLocale(locale) === \"zh-CN\";\n}\n\nfunction getCaptureSystemPrompt(locale) {\n return isZh(locale) ? CAPTURE_SYSTEM_PROMPT : CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n return isZh(locale) ? URL_SUMMARY_PROMPT : URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n return isZh(locale) ? URL_FALLBACK_PROMPT : URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n if (id === \"custom\") return isZh(locale) ? \"自定义\" : \"Custom\";\n if (id === \"qwen\") return isZh(locale) ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return isZh(locale) ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction joinApiPath(baseUrl, path) {\n const base = String(baseUrl || \"\").replace(/\\/+$/, \"\");\n const suffix = String(path || \"\").trim();\n if (!base) return suffix;\n if (!suffix) return base;\n return `${base}/${suffix.replace(/^\\/+/, \"\")}`;\n}\n\nfunction getChatCompletionsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.chatPath || \"/v1/chat/completions\");\n}\n\nfunction getModelsUrl(ai) {\n const preset = ai && ai.preset ? ai.preset : {};\n return joinApiPath(ai && ai.baseUrl, preset.modelsPath || \"/v1/models\");\n}\n\nfunction resolveEffectiveCaptureSettings(settingsOrCapture) {\n const source = settingsOrCapture && typeof settingsOrCapture === \"object\" ? settingsOrCapture : {};\n const hasNestedCapture = source.mobileCapture && typeof source.mobileCapture === \"object\";\n const capture = hasNestedCapture ? source.mobileCapture : source;\n const out = { ...(capture || {}) };\n\n const agent = hasNestedCapture && source.agentProvider && typeof source.agentProvider === \"object\"\n ? source.agentProvider\n : null;\n const direct = agent && agent.direct && typeof agent.direct === \"object\" ? agent.direct : null;\n const providerId = String(out.provider || (direct && direct.providerId) || \"deepseek\").trim();\n out.provider = providerId;\n\n if (!String(out.apiKey || \"\").trim() && direct && direct.apiKeys && typeof direct.apiKeys === \"object\") {\n const keyForCaptureProvider = String(direct.apiKeys[providerId] || \"\").trim();\n const keyForActiveProvider = String(direct.apiKeys[direct.providerId] || \"\").trim();\n out.apiKey = keyForCaptureProvider || (direct.providerId === providerId ? keyForActiveProvider : \"\");\n }\n\n if (\n providerId === \"custom\"\n && !String(out.baseUrl || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.baseUrl = String(direct.baseUrlOverride || \"\").trim();\n }\n\n if (\n providerId === \"custom\"\n && !String(out.model || \"\").trim()\n && direct\n && String(direct.providerId || \"\").trim() === \"custom\"\n ) {\n out.model = String(direct.model || \"\").trim();\n }\n\n return out;\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n return Boolean(ai.baseUrl && ai.apiKey);\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n\n const response = await requestUrl({\n url: getChatCompletionsUrl(ai),\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`AI 请求失败 (${response.status}): ${snippet}`);\n throw new Error(`AI request failed (${response.status}): ${snippet}`);\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(isZh(locale) ? \"AI 返回内容为空\" : \"AI returned empty content\");\n }\n return content;\n}\n\nasync function listCaptureModels(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(resolveEffectiveCaptureSettings(mcSettings || {}));\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n\n const response = await requestUrl({\n url: getModelsUrl(ai),\n method: \"GET\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`模型列表请求失败 (${response.status}): ${snippet}`);\n throw new Error(`Model list request failed (${response.status}): ${snippet}`);\n }\n const data = response.json;\n const items = data && Array.isArray(data.data) ? data.data : [];\n const models = [];\n for (const item of items) {\n const id = item && typeof item.id === \"string\" ? item.id.trim() : \"\";\n if (id) models.push({ id, label: id });\n }\n models.sort((a, b) => a.id.localeCompare(b.id));\n return models;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = isZh(locale) ? \"嗯,这是一个测试\" : \"um, this is a test\";\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: isZh(locale) ? `连接成功,返回: \"${result}\"` : `Connected. Response: \"${result}\"`,\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n getChatCompletionsUrl,\n getModelsUrl,\n resolveEffectiveCaptureSettings,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n listCaptureModels,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "57eff765b6921c3696e6e857de847680581805b6": {"t": 1785758668.20244, "n": 7966, "h": "8fa9b65e03b37c7f", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\n\nconst PROVIDER_PRESETS = {\n deepseek: { name: \"DeepSeek\", baseUrl: \"https://api.deepseek.com\", defaultModel: \"deepseek-chat\", keyUrl: \"https://platform.deepseek.com/api_keys\" },\n qwen: { name: \"Qwen\", baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\", defaultModel: \"qwen-turbo\", keyUrl: \"https://dashscope.console.aliyun.com/apiKey\" },\n moonshot: { name: \"Moonshot (Kimi)\", baseUrl: \"https://api.moonshot.cn\", defaultModel: \"moonshot-v1-8k\", keyUrl: \"https://platform.moonshot.cn/console/api-keys\" },\n zhipu: { name: \"Zhipu (GLM)\", baseUrl: \"https://open.bigmodel.cn/api/paas\", defaultModel: \"glm-4-flash\", keyUrl: \"https://open.bigmodel.cn/usercenter/apikeys\" },\n siliconflow: { name: \"SiliconFlow\", baseUrl: \"https://api.siliconflow.cn\", defaultModel: \"deepseek-ai/DeepSeek-V3\", keyUrl: \"https://cloud.siliconflow.cn/account/ak\" },\n custom: { name: \"Custom\", baseUrl: \"\", defaultModel: \"\", keyUrl: \"\" },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个文字清理助手。你的唯一任务是去除口语中的语气词和填充词(如:嗯、啊、那个、就是、然后、对、哦、emmm、额 等),\",\n \"让句子更简洁。\",\n \"规则:\",\n \"1. 只去除语气词和填充词\",\n \"2. 不要改写、润色或美化原文\",\n \"3. 不要添加任何新内容\",\n \"4. 不要改变原文的意思和表达方式\",\n \"5. 保留所有实质内容和原始用词\",\n \"6. 保留所有 URL 原样不变\",\n \"7. 直接返回清理后的文本,不要任何解释或前缀\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a text cleanup assistant. Your only task is to remove filler words from spoken text (such as um, uh, like, you know, etc.) and keep the sentence concise.\",\n \"Rules:\",\n \"1. Only remove filler words\",\n \"2. Do not rewrite, polish, or beautify the text\",\n \"3. Do not add new content\",\n \"4. Keep original meaning and wording\",\n \"5. Preserve all substantive content\",\n \"6. Preserve all URLs exactly as-is\",\n \"7. Return cleaned text only, no explanation\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT = [\n \"你是一个 URL 摘要助手。用户文本中包含 URL,且页面内容已抓取。\",\n \"输出时保留原文不变,并追加摘要行。\",\n \"格式:每行 `> 📎 原始URL - 摘要`\",\n \"规则:\",\n \"- 保留所有 URL 原样\",\n \"- 摘要不超过 50 字\",\n \"- 不改动 URL 之外内容\",\n \"- 内容不足时用“暂无法解析,已保留原始链接”\",\n \"- 直接返回完整处理后的文本\",\n].join(\"\\n\");\n\nconst URL_SUMMARY_PROMPT_EN = [\n \"You are a URL summary assistant. User text includes URLs and page content has been fetched.\",\n \"Output the original text unchanged, and append summary lines.\",\n \"Format: each line is `> 📎 OriginalURL - Summary`\",\n \"Rules:\",\n \"- Keep all original URLs unchanged\",\n \"- Summary <= 50 chars\",\n \"- Do not alter non-URL content\",\n \"- If insufficient content, use \\\"Unable to resolve, original URL preserved\\\"\",\n \"- Return full processed text without explanation\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT = [\n \"你是一个 URL 兜底助手。\",\n \"任务:当页面内容不可用时,仅做最小处理。\",\n \"规则:\",\n \"1. 保留原文和所有 URL 原样\",\n \"2. 不改写,不虚构页面内容\",\n \"3. 只允许追加 `> 📎 原始URL - 暂无法解析,已保留原始链接`\",\n \"4. 直接返回完整文本\",\n].join(\"\\n\");\n\nconst URL_FALLBACK_PROMPT_EN = [\n \"You are a URL fallback assistant.\",\n \"Task: apply minimal processing when page content is unavailable.\",\n \"Rules:\",\n \"1. Preserve original text and all URLs exactly\",\n \"2. Do not rewrite or fabricate page content\",\n \"3. You may only append lines like `> 📎 OriginalURL - Unable to resolve, original URL preserved`\",\n \"4. Return processed full text directly\",\n].join(\"\\n\");\n\nfunction isZh(locale) {\n return normalizeSupportedLocale(locale) === \"zh-CN\";\n}\n\nfunction getCaptureSystemPrompt(locale) {\n return isZh(locale) ? CAPTURE_SYSTEM_PROMPT : CAPTURE_SYSTEM_PROMPT_EN;\n}\n\nfunction getUrlSummaryPrompt(locale) {\n return isZh(locale) ? URL_SUMMARY_PROMPT : URL_SUMMARY_PROMPT_EN;\n}\n\nfunction getUrlFallbackPrompt(locale) {\n return isZh(locale) ? URL_FALLBACK_PROMPT : URL_FALLBACK_PROMPT_EN;\n}\n\nfunction getAiProviderDisplayName(providerId, fallbackName, locale = \"en\") {\n const id = String(providerId || \"\").trim().toLowerCase();\n if (id === \"custom\") return isZh(locale) ? \"自定义\" : \"Custom\";\n if (id === \"qwen\") return isZh(locale) ? \"通义千问\" : \"Qwen\";\n if (id === \"zhipu\") return isZh(locale) ? \"智谱 (GLM)\" : \"Zhipu (GLM)\";\n return fallbackName || String(providerId || \"\");\n}\n\nfunction resolveAiConfig(mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = String(mcSettings.baseUrl || preset.baseUrl || \"\").replace(/\\/+$/, \"\");\n const model = String(mcSettings.model || preset.defaultModel || \"\").trim();\n const apiKey = String(mcSettings.apiKey || \"\").trim();\n return { providerId, preset, baseUrl, model, apiKey };\n}\n\nfunction hasAiConfig(mcSettings) {\n const ai = resolveAiConfig(mcSettings || {});\n return Boolean(ai.baseUrl && ai.apiKey);\n}\n\nfunction pickFirstText(values) {\n for (const value of values) {\n if (typeof value === \"string\") {\n const text = value.trim();\n if (text) return text;\n }\n }\n return \"\";\n}\n\nasync function requestAiCompletion(messages, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const ai = resolveAiConfig(mcSettings || {});\n if (!ai.baseUrl || !ai.apiKey) {\n throw new Error(isZh(locale)\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n\n const response = await requestUrl({\n url: `${ai.baseUrl}/v1/chat/completions`,\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${ai.apiKey}`,\n },\n body: JSON.stringify({\n model: ai.model,\n temperature: Number.isFinite(Number(options.temperature)) ? Number(options.temperature) : 0.1,\n messages,\n }),\n throw: false,\n timeout: Number.isFinite(Number(options.timeoutMs)) ? Number(options.timeoutMs) : 30000,\n });\n\n if (response.status !== 200) {\n const snippet = JSON.stringify(response.json || response.text).slice(0, 200);\n if (isZh(locale)) throw new Error(`AI 请求失败 (${response.status}): ${snippet}`);\n throw new Error(`AI request failed (${response.status}): ${snippet}`);\n }\n\n const data = response.json;\n const content = pickFirstText([\n data && data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content,\n ]);\n if (!content) {\n throw new Error(isZh(locale) ? \"AI 返回内容为空\" : \"AI returned empty content\");\n }\n return content;\n}\n\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const content = await requestAiCompletion([\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 30000 });\n return content.trim();\n}\n\nasync function summarizeTextWithAi(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n const systemPrompt = options.useFallbackPrompt ? getUrlFallbackPrompt(locale) : getUrlSummaryPrompt(locale);\n const content = await requestAiCompletion([\n { role: \"system\", content: systemPrompt },\n { role: \"user\", content: String(text || \"\") },\n ], mcSettings, { locale, temperature: 0.1, timeoutMs: 45000 });\n return content.trim();\n}\n\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || \"en\");\n try {\n const probe = isZh(locale) ? \"嗯,这是一个测试\" : \"um, this is a test\";\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: isZh(locale) ? `连接成功,返回: \"${result}\"` : `Connected. Response: \"${result}\"`,\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n getAiProviderDisplayName,\n getCaptureSystemPrompt,\n getUrlSummaryPrompt,\n getUrlFallbackPrompt,\n resolveAiConfig,\n hasAiConfig,\n pickFirstText,\n requestAiCompletion,\n cleanupCapture,\n summarizeTextWithAi,\n testConnection,\n};\n"}, "60439c2eb0f29cc777379414b0e925a8c04c6b21": {"t": 1785758669.615503, "n": 4531, "h": "033cd5381ddb2d18", "c": "const { requestUrl } = require(\"obsidian\");\nconst { normalizeSupportedLocale } = require(\"../i18n-locale-utils\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-chat\",\n },\n qwen: {\n name: \"通义千问\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\",\n defaultModel: \"qwen-turbo\",\n },\n moonshot: {\n name: \"Moonshot (Kimi)\",\n baseUrl: \"https://api.moonshot.cn\",\n defaultModel: \"moonshot-v1-8k\",\n },\n zhipu: {\n name: \"智谱 (GLM)\",\n baseUrl: \"https://open.bigmodel.cn/api/paas\",\n defaultModel: \"glm-4-flash\",\n },\n siliconflow: {\n name: \"SiliconFlow\",\n baseUrl: \"https://api.siliconflow.cn\",\n defaultModel: \"deepseek-ai/DeepSeek-V3\",\n },\n custom: {\n name: \"自定义\",\n baseUrl: \"\",\n defaultModel: \"\",\n },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个文字清理助手。你的唯一任务是去除口语中的语气词和填充词(如:嗯、啊、那个、就是、然后、对、哦、emmm、额 等),\",\n \"让句子更简洁。\",\n \"规则:\",\n \"1. 只去除语气词和填充词\",\n \"2. 不要改写、润色或美化原文\",\n \"3. 不要添加任何新内容\",\n \"4. 不要改变原文的意思和表达方式\",\n \"5. 保留所有实质内容和原始用词\",\n \"6. 直接返回清理后的文本,不要任何解释或前缀\",\n].join(\"\\n\");\n\nconst CAPTURE_SYSTEM_PROMPT_EN = [\n \"You are a text cleanup assistant. Remove filler words from spoken text (such as um, uh, like, you know).\",\n \"Rules:\",\n \"1. Only remove filler words\",\n \"2. Do not rewrite or polish text\",\n \"3. Do not add new content\",\n \"4. Keep original meaning and wording\",\n \"5. Preserve all substantive content\",\n \"6. Return cleaned text only, no explanation\",\n].join(\"\\n\");\n\nfunction getCaptureSystemPrompt(locale) {\n return normalizeSupportedLocale(locale) === \"zh-CN\" ? CAPTURE_SYSTEM_PROMPT : CAPTURE_SYSTEM_PROMPT_EN;\n}\n\n/**\n * Call AI to clean up captured text (remove filler words).\n * @param {string} text - raw captured text\n * @param {object} mcSettings - mobileCapture settings object\n * @param {object} [options]\n * @returns {Promise<string>} cleaned text\n */\nasync function cleanupCapture(text, mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || (mcSettings && mcSettings.locale) || \"en\");\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = (mcSettings.baseUrl || preset.baseUrl).replace(/\\/+$/, \"\");\n const model = mcSettings.model || preset.defaultModel;\n const apiKey = mcSettings.apiKey;\n\n if (!baseUrl || !apiKey) {\n throw new Error(locale === \"zh-CN\"\n ? \"AI 服务未配置:缺少 Base URL 或 API Key\"\n : \"AI is not configured: missing Base URL or API Key\");\n }\n\n const url = `${baseUrl}/v1/chat/completions`;\n\n const response = await requestUrl({\n url,\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n temperature: 0.1,\n messages: [\n { role: \"system\", content: getCaptureSystemPrompt(locale) },\n { role: \"user\", content: text },\n ],\n }),\n throw: false,\n timeout: 30000,\n });\n\n if (response.status !== 200) {\n if (locale === \"zh-CN\") {\n throw new Error(`AI 请求失败 (${response.status}): ${JSON.stringify(response.json || response.text).slice(0, 200)}`);\n }\n throw new Error(`AI request failed (${response.status}): ${JSON.stringify(response.json || response.text).slice(0, 200)}`);\n }\n\n const data = response.json;\n const content =\n data &&\n data.choices &&\n data.choices[0] &&\n data.choices[0].message &&\n data.choices[0].message.content;\n\n if (!content) {\n throw new Error(locale === \"zh-CN\" ? \"AI 返回内容为空\" : \"AI returned empty content\");\n }\n\n return content.trim();\n}\n\n/**\n * Quick test of AI connectivity.\n * @param {object} mcSettings\n * @param {object} [options]\n * @returns {Promise<{ok: boolean, message: string}>}\n */\nasync function testConnection(mcSettings, options = {}) {\n const locale = normalizeSupportedLocale(options.locale || (mcSettings && mcSettings.locale) || \"en\");\n try {\n const probe = locale === \"zh-CN\" ? \"嗯,这是一个测试\" : \"um, this is a test\";\n const result = await cleanupCapture(probe, mcSettings, { locale });\n return {\n ok: true,\n message: locale === \"zh-CN\" ? `连接成功,返回: \"${result}\"` : `Connected. Response: \"${result}\"`,\n };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n CAPTURE_SYSTEM_PROMPT_EN,\n getCaptureSystemPrompt,\n cleanupCapture,\n testConnection,\n};\n"}, "81529baab99bce91fe35f9d35e45f2dc940dc935": {"t": 1785758670.9261036, "n": 3087, "h": "043d24c015dd9fd0", "c": "const { requestUrl } = require(\"obsidian\");\n\nconst PROVIDER_PRESETS = {\n deepseek: {\n name: \"DeepSeek\",\n baseUrl: \"https://api.deepseek.com\",\n defaultModel: \"deepseek-chat\",\n },\n qwen: {\n name: \"通义千问\",\n baseUrl: \"https://dashscope.aliyuncs.com/compatible-mode\",\n defaultModel: \"qwen-turbo\",\n },\n moonshot: {\n name: \"Moonshot (Kimi)\",\n baseUrl: \"https://api.moonshot.cn\",\n defaultModel: \"moonshot-v1-8k\",\n },\n zhipu: {\n name: \"智谱 (GLM)\",\n baseUrl: \"https://open.bigmodel.cn/api/paas\",\n defaultModel: \"glm-4-flash\",\n },\n siliconflow: {\n name: \"SiliconFlow\",\n baseUrl: \"https://api.siliconflow.cn\",\n defaultModel: \"deepseek-ai/DeepSeek-V3\",\n },\n custom: {\n name: \"自定义\",\n baseUrl: \"\",\n defaultModel: \"\",\n },\n};\n\nconst CAPTURE_SYSTEM_PROMPT = [\n \"你是一个文字清理助手。你的唯一任务是去除口语中的语气词和填充词(如:嗯、啊、那个、就是、然后、对、哦、emmm、额 等),\",\n \"让句子更简洁。\",\n \"规则:\",\n \"1. 只去除语气词和填充词\",\n \"2. 不要改写、润色或美化原文\",\n \"3. 不要添加任何新内容\",\n \"4. 不要改变原文的意思和表达方式\",\n \"5. 保留所有实质内容和原始用词\",\n \"6. 直接返回清理后的文本,不要任何解释或前缀\",\n].join(\"\\n\");\n\n/**\n * Call AI to clean up captured text (remove filler words).\n * @param {string} text - raw captured text\n * @param {object} mcSettings - mobileCapture settings object\n * @returns {Promise<string>} cleaned text\n */\nasync function cleanupCapture(text, mcSettings) {\n const providerId = mcSettings.provider || \"deepseek\";\n const preset = PROVIDER_PRESETS[providerId] || PROVIDER_PRESETS.deepseek;\n const baseUrl = (mcSettings.baseUrl || preset.baseUrl).replace(/\\/+$/, \"\");\n const model = mcSettings.model || preset.defaultModel;\n const apiKey = mcSettings.apiKey;\n\n if (!baseUrl || !apiKey) {\n throw new Error(\"AI 服务未配置:缺少 Base URL 或 API Key\");\n }\n\n const url = `${baseUrl}/v1/chat/completions`;\n\n const response = await requestUrl({\n url,\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({\n model,\n temperature: 0.1,\n messages: [\n { role: \"system\", content: CAPTURE_SYSTEM_PROMPT },\n { role: \"user\", content: text },\n ],\n }),\n throw: false,\n timeout: 30000,\n });\n\n if (response.status !== 200) {\n throw new Error(`AI 请求失败 (${response.status}): ${JSON.stringify(response.json || response.text).slice(0, 200)}`);\n }\n\n const data = response.json;\n const content =\n data &&\n data.choices &&\n data.choices[0] &&\n data.choices[0].message &&\n data.choices[0].message.content;\n\n if (!content) {\n throw new Error(\"AI 返回内容为空\");\n }\n\n return content.trim();\n}\n\n/**\n * Quick test of AI connectivity.\n * @param {object} mcSettings\n * @returns {Promise<{ok: boolean, message: string}>}\n */\nasync function testConnection(mcSettings) {\n try {\n const result = await cleanupCapture(\"嗯,这是一个测试\", mcSettings);\n return { ok: true, message: `连接成功,返回: \"${result}\"` };\n } catch (e) {\n return { ok: false, message: e instanceof Error ? e.message : String(e) };\n }\n}\n\nmodule.exports = {\n PROVIDER_PRESETS,\n CAPTURE_SYSTEM_PROMPT,\n cleanupCapture,\n testConnection,\n};\n"}}}} |