{"repo": "6mmliu/video-caption-ai-summarizer", "files": {"options/options.js": {"d5ac437217dffe78b166d9257db039738a971798": {"t": 1785746002.1919193, "n": 41337, "h": "68b087d953be71f6", "c": "const SETTINGS_KEY = \"vcsSettings\";\nconst HISTORY_KEY = \"vcsHistory\";\nconst HISTORY_LIMIT = 30;\nconst I18N = globalThis.VCS_I18N;\n\nconst OLD_DEFAULT_PROMPT_TEMPLATE = [\n \"你是一个擅长处理视频字幕的中文研究助理。\",\n \"请根据下面的视频字幕生成高质量总结。\",\n \"\",\n \"标题:{{title}}\",\n \"平台:{{platform}}\",\n \"链接:{{url}}\",\n \"输出语言:{{language}}\",\n \"\",\n \"字幕:\",\n \"{{transcript}}\"\n].join(\"\\n\");\n\nconst OLD_DEFAULT_OUTPUT_TEMPLATE = [\n \"请使用 Markdown 输出:\",\n \"## 一句话结论\",\n \"## 核心摘要\",\n \"## 关键观点\",\n \"- 每条观点尽量带上相关时间点\",\n \"## 章节时间线\",\n \"## 可执行事项 / 值得追问的问题\"\n].join(\"\\n\");\n\nconst DEFAULT_PROMPT_TEMPLATE = [\n \"你是一位视频内容提炼专家。请基于字幕文本,生成一份精炼、高信息密度的结构化总结。\",\n \"\",\n \"## 视频信息\",\n \"📌 标题:{{title}}|🌐 平台:{{platform}}|🔗 链接:{{url}}\",\n \"\",\n \"## 核心原则\",\n \"- 总结 ≠ 复述。你的任务是**压缩与重构**,不是逐段改写\",\n \"- 保留硬信息(数据、人名、术语、方法论),砍掉软信息(过渡语、重复论证、情绪渲染)\",\n \"- 每句话必须承载独立信息量,删掉后会造成信息缺失才有资格留下\",\n \"- 仅基于字幕实际内容,不推测、不补充、不评价\",\n \"\",\n \"## 输出语言:{{language}}\",\n \"\",\n \"## 输出格式\",\n \"{{outputTemplate}}\",\n \"\",\n \"## 字幕正文\",\n \"{{transcript}}\"\n].join(\"\\n\");\n\nconst DEFAULT_OUTPUT_TEMPLATE = [\n \"请使用 Markdown 输出:\",\n \"\",\n \"## 💡 一句话结论\",\n \"≤ 30字。视频最核心的一个判断或发现。\",\n \"\",\n \"## 📋 核心摘要\",\n \"3~4句话覆盖完整逻辑链:什么问题 → 什么观点/方案 → 什么结论。不用条目,写成连贯段落。\",\n \"\",\n \"## 🎯 关键观点\",\n \"3~7条,每条格式:\",\n \"- ⏱️ [时间戳] **关键词**:一句话说明(无时间戳则省略⏱️)\",\n \"\",\n \"只保留\\\"删掉就损失信息\\\"的观点,不凑数。\",\n \"\",\n \"## 🗂️ 章节时间线\",\n \"按叙事顺序分3~6段,每段格式:\",\n \"- ⏱️ [起止时间] **章节名**:一句话概括(无时间戳则用 1️⃣2️⃣3️⃣ 编号)\"\n].join(\"\\n\");\n\nconst DEFAULT_SETTINGS = {\n settingsVersion: 7,\n theme: \"auto\",\n uiLanguage: \"zh-CN\",\n language: \"中文(简体)\",\n panelEnabled: true,\n activeProfileId: \"custom\",\n profiles: [\n {\n ...profilePreset(\"deepseek\"),\n id: \"custom\",\n name: \"我的 API 配置\"\n }\n ],\n promptTemplate: DEFAULT_PROMPT_TEMPLATE,\n outputTemplate: DEFAULT_OUTPUT_TEMPLATE,\n chunkSize: 12000,\n chunkOverlap: 600,\n requestTimeoutSeconds: 60,\n includeTimestamps: true,\n includeTitleAndUrl: true,\n redactTerms: \"\",\n saveHistory: false\n};\n\nlet settings = structuredClone(DEFAULT_SETTINGS);\n\nconst fields = {\n uiLanguage: document.querySelector(\"#uiLanguage\"),\n language: document.querySelector(\"#language\"),\n panelEnabled: document.querySelector(\"#panelEnabled\"),\n profileName: document.querySelector(\"#profileName\"),\n provider: document.querySelector(\"#provider\"),\n endpoint: document.querySelector(\"#endpoint\"),\n accessToken: document.querySelector(\"#accessToken\"),\n model: document.querySelector(\"#model\"),\n temperature: document.querySelector(\"#temperature\"),\n maxTokens: document.querySelector(\"#maxTokens\"),\n promptTemplate: document.querySelector(\"#promptTemplate\"),\n outputTemplate: document.querySelector(\"#outputTemplate\"),\n includeTimestamps: document.querySelector(\"#includeTimestamps\"),\n saveHistory: document.querySelector(\"#saveHistory\"),\n redactTerms: document.querySelector(\"#redactTerms\"),\n chunkSize: document.querySelector(\"#chunkSize\"),\n chunkOverlap: document.querySelector(\"#chunkOverlap\"),\n requestTimeoutSeconds: document.querySelector(\"#requestTimeoutSeconds\"),\n importText: document.querySelector(\"#importText\"),\n importFile: document.querySelector(\"#importFile\"),\n historyList: document.querySelector(\"#historyList\"),\n historyCount: document.querySelector(\"#historyCount\"),\n apiStatus: document.querySelector(\"#apiStatus\"),\n saveStatus: document.querySelector(\"#saveStatus\")\n};\n\ninit();\n\nfunction i18n() {\n return I18N.create(settings);\n}\n\nfunction t(key, variables) {\n return i18n().t(key, variables);\n}\n\nasync function init() {\n const result = await chrome.storage.local.get(SETTINGS_KEY);\n settings = normalizeSettings(result[SETTINGS_KEY]);\n if (JSON.stringify(result[SETTINGS_KEY] || {}) !== JSON.stringify(settings)) {\n await chrome.storage.local.set({ [SETTINGS_KEY]: settings });\n }\n hydrateForm();\n bindEvents();\n await renderHistory();\n}\n\nfunction bindEvents() {\n document.querySelector(\"#saveAll\").addEventListener(\"click\", () => saveAll());\n document.querySelector(\"#saveProfile\").addEventListener(\"click\", () => saveCurrentProfile());\n document.querySelector(\"#addProfile\").addEventListener(\"click\", addProfile);\n document.querySelector(\"#deleteProfile\").addEventListener(\"click\", deleteProfile);\n document.querySelector(\"#testProfile\").addEventListener(\"click\", testProfile);\n document.querySelector(\"#exportSettings\").addEventListener(\"click\", exportSettings);\n document.querySelector(\"#exportCompleteSettings\").addEventListener(\"click\", exportCompleteSettings);\n document.querySelector(\"#importSettings\").addEventListener(\"click\", importSettings);\n document.querySelector(\"#resetSettings\").addEventListener(\"click\", resetSettings);\n document.querySelector(\"#clearHistory\").addEventListener(\"click\", clearHistory);\n document.querySelector(\"#exportHistory\").addEventListener(\"click\", exportHistory);\n fields.importFile.addEventListener(\"change\", importSettingsFile);\n\n document.querySelectorAll(\"input[name='theme']\").forEach((input) => {\n input.addEventListener(\"change\", async () => {\n settings.theme = input.value;\n applyTheme();\n await saveAll(t(\"options.status.themeSaved\"));\n });\n });\n\n fields.uiLanguage.addEventListener(\"change\", async () => {\n settings.uiLanguage = fields.uiLanguage.value;\n applyTranslations();\n await renderHistory();\n await saveAll(t(\"options.status.uiLanguageSaved\"));\n });\n\n document.querySelectorAll(\".preset\").forEach((button) => {\n button.addEventListener(\"click\", () => applyPreset(button.dataset.preset));\n });\n\n fields.provider.addEventListener(\"change\", handleProviderChange);\n fields.historyList.addEventListener(\"click\", copyHistoryFromList);\n initSectionNav();\n\n chrome.storage.onChanged.addListener((changes, area) => {\n if (area !== \"local\") {\n return;\n }\n if (changes[HISTORY_KEY]) {\n renderHistory();\n }\n if (changes[SETTINGS_KEY] && changes[SETTINGS_KEY].newValue) {\n settings = normalizeSettings(changes[SETTINGS_KEY].newValue);\n hydrateForm();\n renderHistory();\n }\n });\n\n window.matchMedia(\"(prefers-color-scheme: dark)\").addEventListener(\"change\", () => {\n if (settings.theme === \"auto\") {\n applyTheme();\n }\n });\n}\n\nfunction hydrateForm() {\n const themeInput = document.querySelector(`input[name='theme'][value='${settings.theme || \"auto\"}']`)\n || document.querySelector(\"input[name='theme'][value='auto']\");\n if (themeInput) {\n themeInput.checked = true;\n }\n fields.uiLanguage.value = settings.uiLanguage || DEFAULT_SETTINGS.uiLanguage;\n fields.language.value = settings.language || DEFAULT_SETTINGS.language;\n fields.panelEnabled.checked = settings.panelEnabled !== false;\n fields.promptTemplate.value = settings.promptTemplate || \"\";\n fields.outputTemplate.value = settings.outputTemplate || \"\";\n fields.includeTimestamps.checked = settings.includeTimestamps !== false;\n fields.saveHistory.checked = settings.saveHistory !== false;\n fields.redactTerms.value = settings.redactTerms || \"\";\n fields.chunkSize.value = settings.chunkSize || 12000;\n fields.chunkOverlap.value = settings.chunkOverlap || 600;\n fields.requestTimeoutSeconds.value = settings.requestTimeoutSeconds || DEFAULT_SETTINGS.requestTimeoutSeconds;\n applyTheme();\n applyTranslations();\n renderProfileFields();\n}\n\nfunction renderProfileFields() {\n const profile = getActiveProfile();\n if (!profile) {\n return;\n }\n\n fields.profileName.value = profile.name || \"\";\n fields.provider.value = profile.provider || \"proxy\";\n fields.endpoint.value = profile.endpoint || \"\";\n fields.accessToken.value = getProfileSecret(profile);\n fields.model.value = profile.model || \"\";\n fields.temperature.value = profile.temperature ?? 1;\n fields.maxTokens.value = profile.maxTokens ?? 4096;\n updateProviderHints();\n}\n\nasync function saveCurrentProfile() {\n const profile = getActiveProfile();\n if (!profile) {\n return;\n }\n\n profile.name = fields.profileName.value.trim() || profile.id;\n profile.provider = fields.provider.value;\n profile.endpoint = fields.endpoint.value.trim();\n setProfileSecret(profile, fields.accessToken.value.trim());\n profile.model = fields.model.value.trim();\n profile.temperature = Number(fields.temperature.value || 1);\n profile.maxTokens = Number(fields.maxTokens.value || 4096);\n await saveAll(t(\"options.status.profileSaved\"));\n}\n\nasync function saveAll(message = t(\"options.status.settingsSaved\")) {\n if (typeof message !== \"string\") {\n message = t(\"options.status.settingsSaved\");\n }\n settings.theme = document.querySelector(\"input[name='theme']:checked\")?.value || \"auto\";\n settings.uiLanguage = I18N.normalizeUiLanguage(fields.uiLanguage.value);\n settings.language = fields.language.value.trim() || \"中文(简体)\";\n settings.panelEnabled = fields.panelEnabled.checked;\n settings.promptTemplate = fields.promptTemplate.value;\n settings.outputTemplate = fields.outputTemplate.value;\n settings.includeTimestamps = fields.includeTimestamps.checked;\n settings.saveHistory = fields.saveHistory.checked;\n settings.redactTerms = fields.redactTerms.value;\n settings.chunkSize = Number(fields.chunkSize.value || 12000);\n settings.chunkOverlap = Number(fields.chunkOverlap.value || 600);\n settings.requestTimeoutSeconds = Number(fields.requestTimeoutSeconds.value || DEFAULT_SETTINGS.requestTimeoutSeconds);\n saveCurrentProfileValuesOnly();\n settings = normalizeSettings(settings);\n if (!await ensureEndpointPermissions(settings.profiles)) {\n setStatus(fields.saveStatus, t(\"options.status.endpointPermissionDenied\"), \"error\");\n return;\n }\n\n await chrome.storage.local.set({ [SETTINGS_KEY]: settings });\n setStatus(fields.saveStatus, message, \"ok\");\n}\n\nfunction saveCurrentProfileValuesOnly() {\n const profile = getActiveProfile();\n if (!profile) {\n return;\n }\n\n profile.name = fields.profileName.value.trim() || profile.id;\n profile.provider = fields.provider.value;\n profile.endpoint = fields.endpoint.value.trim();\n setProfileSecret(profile, fields.accessToken.value.trim());\n profile.model = fields.model.value.trim();\n profile.temperature = Number(fields.temperature.value || 1);\n profile.maxTokens = Number(fields.maxTokens.value || 4096);\n}\n\nfunction addProfile() {\n const profile = {\n id: `custom-${Date.now()}`,\n name: settings.uiLanguage === \"en\" ? \"New Profile\" : \"新配置\",\n provider: \"openai-compatible\",\n endpoint: \"\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n };\n settings.profiles.push(profile);\n settings.activeProfileId = profile.id;\n renderProfileFields();\n}\n\nfunction deleteProfile() {\n if (settings.profiles.length <= 1) {\n setStatus(fields.apiStatus, t(\"options.status.keepOneProfile\"), \"error\");\n return;\n }\n\n const index = settings.profiles.findIndex((profile) => profile.id === settings.activeProfileId);\n if (index >= 0) {\n settings.profiles.splice(index, 1);\n }\n settings.activeProfileId = settings.profiles[0].id;\n renderProfileFields();\n}\n\nfunction applyPreset(name) {\n const preset = profilePreset(name);\n const profile = getActiveProfile();\n profile.provider = preset.provider;\n profile.endpoint = preset.endpoint;\n profile.apiKey = \"\";\n profile.accessToken = \"\";\n fields.provider.value = profile.provider;\n fields.endpoint.value = profile.endpoint;\n fields.accessToken.value = \"\";\n updateProviderHints();\n setStatus(fields.apiStatus, t(\"options.status.presetApplied\"), \"ok\");\n}\n\nfunction handleProviderChange() {\n const preset = profilePreset(providerDefaultPresetName(fields.provider.value));\n fields.endpoint.value = preset.endpoint || \"\";\n fields.accessToken.value = \"\";\n updateProviderHints();\n}\n\nfunction providerDefaultPresetName(provider) {\n const names = {\n proxy: \"proxy\",\n \"openai-compatible\": \"deepseek\",\n anthropic: \"claude\",\n gemini: \"gemini\",\n ollama: \"ollama\"\n };\n return names[provider] || \"deepseek\";\n}\n\nfunction profilePreset(name) {\n const presets = {\n deepseek: {\n id: \"deepseek\",\n name: \"DeepSeek\",\n provider: \"openai-compatible\",\n endpoint: \"https://api.deepseek.com/v1/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n openai: {\n id: \"openai\",\n name: \"OpenAI / ChatGPT\",\n provider: \"openai-compatible\",\n endpoint: \"https://api.openai.com/v1/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n claude: {\n id: \"claude\",\n name: \"Claude\",\n provider: \"anthropic\",\n endpoint: \"https://api.anthropic.com/v1/messages\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n gemini: {\n id: \"gemini\",\n name: \"Gemini\",\n provider: \"gemini\",\n endpoint: \"\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n kimi: {\n id: \"kimi\",\n name: \"Kimi (Moonshot)\",\n provider: \"openai-compatible\",\n endpoint: \"https://api.moonshot.ai/v1/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n qwen: {\n id: \"qwen\",\n name: \"通义千问\",\n provider: \"openai-compatible\",\n endpoint: \"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n glm: {\n id: \"glm\",\n name: \"智谱 GLM\",\n provider: \"openai-compatible\",\n endpoint: \"https://open.bigmodel.cn/api/paas/v4/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n proxy: {\n id: \"proxy\",\n name: \"专用服务\",\n provider: \"proxy\",\n endpoint: \"\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n },\n ollama: {\n id: \"ollama\",\n name: \"Ollama Local\",\n provider: \"ollama\",\n endpoint: \"http://localhost:11434/v1/chat/completions\",\n apiKey: \"\",\n accessToken: \"\",\n model: \"\",\n temperature: 1,\n maxTokens: 4096\n }\n };\n\n return structuredClone(presets[name] || presets.deepseek);\n}\n\nasync function testProfile() {\n saveCurrentProfileValuesOnly();\n settings = normalizeSettings(settings);\n if (!await ensureEndpointPermissions([getActiveProfile()])) {\n setStatus(fields.apiStatus, t(\"options.status.endpointPermissionDenied\"), \"error\");\n return;\n }\n setStatus(fields.apiStatus, t(\"options.status.testing\"), \"\");\n\n const response = await chrome.runtime.sendMessage({\n type: \"VCS_TEST_API\",\n profile: getActiveProfile(),\n stream: true\n });\n\n if (response?.ok) {\n setStatus(fields.apiStatus, t(\"options.status.connectionSuccess\", { text: response.payload.text }), \"ok\");\n } else {\n setStatus(fields.apiStatus, response?.error || t(\"options.status.connectionFailed\"), \"error\");\n }\n}\n\nasync function exportSettings() {\n await saveAll(t(\"options.status.savedForExport\"));\n const payload = {\n type: \"video-caption-ai-settings\",\n version: 1,\n exportedAt: new Date().toISOString(),\n settings: sanitizeSettingsForExport(settings)\n };\n const json = JSON.stringify(payload, null, 2);\n downloadJson(json, `video-caption-ai-settings-${formatDateForFilename(new Date())}.json`);\n const copied = await writeClipboard(json);\n setStatus(fields.saveStatus, copied ? t(\"options.status.settingsExportedCopied\") : t(\"options.status.settingsExported\"), \"ok\");\n}\n\nasync function exportCompleteSettings() {\n saveCurrentProfileValuesOnly();\n settings = normalizeSettings(settings);\n const confirmed = window.confirm(t(\"options.confirm.exportSecrets\"));\n if (!confirmed) {\n return;\n }\n\n await chrome.storage.local.set({ [SETTINGS_KEY]: settings });\n const payload = {\n type: \"video-caption-ai-settings\",\n version: 2,\n exportedAt: new Date().toISOString(),\n includesSecrets: true,\n settings\n };\n const json = JSON.stringify(payload, null, 2);\n downloadJson(json, `video-caption-ai-settings-full-${formatDateForFilename(new Date())}.json`);\n setStatus(fields.saveStatus, t(\"options.status.fullSettingsExported\"), \"ok\");\n}\n\nasync function importSettings() {\n try {\n const imported = parseImportedSettings(fields.importText.value);\n settings = normalizeSettings(imported);\n hydrateForm();\n await chrome.storage.local.set({ [SETTINGS_KEY]: settings });\n setStatus(fields.saveStatus, t(\"options.status.settingsImported\"), \"ok\");\n } catch (error) {\n setStatus(fields.saveStatus, t(\"options.status.importFailed\", { message: error.message }), \"error\");\n }\n}\n\nasync function resetSettings() {\n settings = structuredClone(DEFAULT_SETTINGS);\n hydrateForm();\n setStatus(fields.saveStatus, t(\"options.status.resetRequiresSave\"), \"ok\");\n}\n\nasync function importSettingsFile() {\n const file = fields.importFile.files?.[0];\n if (!file) {\n return;\n }\n\n try {\n fields.importText.value = await file.text();\n await importSettings();\n } finally {\n fields.importFile.value = \"\";\n }\n}\n\nasync function renderHistory() {\n const result = await chrome.storage.local.get(HISTORY_KEY);\n const history = normalizeHistory(result[HISTORY_KEY]);\n setDigits(fields.historyCount, `${history.length} / ${HISTORY_LIMIT}`);\n\n if (!history.length) {\n fields.historyList.innerHTML = `