1 line
136 KiB
JSON
1 line
136 KiB
JSON
{"repo": "aiden-promax/2026zhihuhacks", "files": {"server/index.js": {"01b22f38182ed9d0912018a88fb2f7046254f7ac": {"t": 1785745528.1772923, "n": 9733, "h": "0bca2903240cd04d", "c": "import express from \"express\";\n\nconst app = express();\napp.use(express.json({ limit: \"1mb\" }));\n\n// Allow Chrome extension origins\napp.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n next();\n});\napp.options(\"*\", (_req, res) => res.sendStatus(204));\n\nconst PORT = process.env.PORT || 8080;\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_API_URL = \"https://api.deepseek.com/chat/completions\";\n\n// ────────────────────────────────────────────────────────────────────────────\n// Route\n// ────────────────────────────────────────────────────────────────────────────\n\napp.post(\"/summarize\", async (req, res) => {\n if (!DEEPSEEK_API_KEY) {\n return res.status(500).json({\n status: \"error\",\n errorMessage: \"Server misconfigured: DEEPSEEK_API_KEY not set.\",\n });\n }\n\n const request = req.body;\n if (!request?.answer?.contentText) {\n return res.status(400).json({\n status: \"error\",\n errorMessage: \"Missing required field: answer.contentText\",\n });\n }\n\n const model = request.model || \"deepseek-chat\";\n const summaryLength = request.summaryLength || \"medium\";\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: \"POST\",\n signal: controller.signal,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: buildSystemContent() },\n { role: \"user\", content: buildPrompt(request) },\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(summaryLength),\n response_format: { type: \"json_object\" },\n thinking: { type: \"disabled\" },\n }),\n });\n\n if (!response.ok) {\n const errText = await response.text().catch(() => \"\");\n return res.status(502).json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}${errText ? \" — \" + errText.slice(0, 200) : \"\"}`,\n });\n }\n\n const data = await response.json();\n const text = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return res.json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: \"DeepSeek 返回内容无法解析。\",\n });\n }\n\n return res.json({\n status: \"success\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags,\n });\n } catch (error) {\n const isTimeout =\n error instanceof Error &&\n (error.name === \"AbortError\" || error.message.includes(\"aborted\"));\n return res.status(504).json({\n status: \"error\",\n answerId: request.answer?.answerId,\n contentHash: request.answer?.contentHash,\n errorMessage: isTimeout ? \"DeepSeek 请求超时。\" : \"DeepSeek 服务暂时不可用。\",\n });\n } finally {\n clearTimeout(timeoutId);\n }\n});\n\napp.get(\"/health\", (_req, res) => res.json({ ok: true }));\n\napp.listen(PORT, () => {\n console.log(`看山 proxy listening on port ${PORT}`);\n});\n\n// ────────────────────────────────────────────────────────────────────────────\n// Prompt helpers (kept in sync with extension/src/background/prompt.ts)\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction buildSystemContent() {\n return [\n \"你是「看山先看」知乎阅读决策助手。\",\n \"你的任务不是写普通摘要,而是把一条知乎回答压缩成可直接帮助用户决定要不要读的阅读决策卡。\",\n \"只分析用户提供的回答内容,不添加外部事实;如果回答本身含糊、极端或证据不足,要在短判断和 aiTags 中体现。\",\n \"使用简体中文,语气轻盈、准确、贴近知乎语境;可以有轻微锋利感,但不要讽刺、人身攻击或道德审判。\",\n \"只返回 JSON 对象,不要返回 Markdown、代码块、解释文字、思考过程、分数或额外字段。\",\n \"\",\n \"# 输出字段\",\n \"shortJudgment:10 个汉字以内的阅读建议,去掉标点和空格;自然、简短,并贴合实际阅读价值。\",\n \"coreView:一句高密度摘要,讲述本文的大概脉络。不要写\\u201C本文主要讲述了\\u201D。要写作者通过哪些核心观点、结论、框架或故事线来传达这个脉络的;不要写成段落摘要。\",\n \"aiTags:从固定标签中选 1-3 个,优先覆盖主要阅读价值,再覆盖明显风险信号;不得自造标签。\",\n \"\",\n \"# 内部工作流(不要输出)\",\n \"1. 判断回答主导类型:知识、经验、观点、情绪、讨论引子,或混合类型。\",\n \"2. 提取作者真正想让读者带走的结论、警告、建议或立场。\",\n \"3. 判断阅读密度:信息密度、专业深度、主观性、情绪强度、娱乐性,以及是否有钓鱼、串子、伪科普、阵营叙事或软广带货等风险信号。\",\n \"4. 用适合场景的思路辅助判断阅读负担和价值,例如碎片时间、摸鱼、睡前、深度阅读、收藏后读;但不要输出场景标签或场景理由。\",\n \"5. 选择一个短判断、一个核心观点和 1-3 个 aiTags。\",\n \"6. 最终只输出符合约束的 JSON。\",\n \"\",\n \"# 固定 aiTags 及判断标准\",\n \"🧠 思辨文 — 提供分析框架,拆概念/前提/因果,能改变对问题的理解方式。\",\n \"📚 干货文 — 信息密度高、资料多,适合收藏查阅,可能观点不强。\",\n \"🏗️ 工具文 — 清单、流程、模板、网站、书单、路径建议,可执行性强。\",\n \"🎭 情绪文 — 情绪感染为主,事实或论证较少,读完主要获得情绪共鸣。\",\n \"观点鲜明 — 立场很强、结论明确、有冲击力,但证据链未必完整。\",\n \"💭 个人经历分享 — 以作者或身边人的经历为核心,有参考价值但样本有限。\",\n \"🧃 爽文 — 强烈站队、打脸叙事、敌我分明,读起来爽但未必严谨。\",\n \"🧱 阵营文 — 先有立场再找材料,放大对方问题、淡化己方问题,很少承认复杂性。\",\n \"🪝 钓鱼文 — 疑似故意设置极端观点吸引争吵或引战,论证薄但攻击性强。\",\n \"🧨 串子文 — 疑似伪装成某立场,故意说夸张、低级或招黑的话来反向抹黑某群体。\",\n \"🤡 乐子文 — 主要价值是看热闹、荒诞、反差和娱乐感,而不是获取严肃信息。\",\n \"🧪 伪科普 — 包装成科普,但概念混乱、证据不足、偷换概念或结论跳跃。\",\n \"🛒 软广带货文 — 表面是经验、测评或科普,实际导向产品、课程、服务、机构、私信或链接。\",\n \"\",\n \"# 区分原则\",\n \"🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度。\",\n \"📚 干货文 vs 🏗️ 工具文:前者重知识和资料,后者重操作路径和可复用工具。\",\n \"🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供心理爽感。\",\n \"🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑。\",\n \"如果回答同时有价值内容和风险信号,可以并列标签,例如 📚 干货文 + 观点鲜明,或 🧃 爽文 + 🧱 阵营文。\",\n \"如果出现明显伪科普或软广带货,优先保留对应风险标签。\",\n ].join(\"\\n\");\n}\n\nfunction buildPrompt(request) {\n const a = request.answer;\n const lines = [];\n\n lines.push(`问题标题:${a.questionTitle || \"未知\"}`);\n if (a.questionDescription) lines.push(`问题补充说明:${a.questionDescription}`);\n if (a.questionTopics?.length > 0) lines.push(`问题话题标签:${a.questionTopics.join(\" / \")}`);\n if (a.authorName) {\n lines.push(`回答作者:${a.authorHeadline ? `${a.authorName}(${a.authorHeadline})` : a.authorName}`);\n }\n if (a.upvoteCount) lines.push(`回答赞同数:${a.upvoteCount}`);\n if (a.commentCount) lines.push(`回答评论数:${a.commentCount}`);\n\n lines.push(\n \"\",\n '任务:为这条知乎回答生成\"看山先看\"阅读决策卡。',\n \"\",\n \"# 输出要求\",\n \"只返回 JSON 对象,且只能包含 shortJudgment、coreView、aiTags 三个字段。\",\n '不要输出 Markdown,不要输出\"适合场景\",不要输出阅读时间,不要输出解释。',\n \"shortJudgment:10 个汉字以内,优先从这些短语中选择或仿写:值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看。\",\n \"coreView:一句话,高密度、结论导向,必须基于回答正文;不要写成段落摘要。\",\n \"aiTags:从系统提示的固定标签中选 1-3 个,放入数组。\",\n \"\",\n \"# 标签校准案例\",\n '这些案例只用于选择 aiTags 和短判断,不要输出\"应选/不应选/选择依据\"。',\n 'NVIDIA/Huawei 技术争论:有 NVLink、InfiniBand、Fat-tree 等技术词,但表达主要是质问、嘲讽和反击,情绪强度高于解释密度;应选 [\"🎭 情绪文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"情绪输出偏多\",\"coreView\":\"这类回答的主要价值在于呈现争论立场和反击情绪,而不是建立稳定的技术解释框架。\",\"aiTags\":[\"🎭 情绪文\"]}',\n '黄仁勋吃饭调研段子:主要价值来自荒诞反差、现场戏剧感和结尾包袱,信息量不高;应选 [\"🤡 乐子文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"可以当个乐子看\",\"coreView\":\"这类回答主要靠荒诞反差和叙事包袱提供娱乐感,而不是提供可复用的信息或判断。\",\"aiTags\":[\"🤡 乐子文\"]}',\n '有机化工基础介绍:解释烃、有机化工、烯烃、塑料分类等基础知识,事实、概念和例子密集,可作为背景知识查阅;应选 [\"📚 干货文\"],不选 [\"🧃 爽文\",\"🎭 情绪文\"]。JSON 示例:{\"shortJudgment\":\"干货很多\",\"coreView\":\"这类回答的核心价值在于集中解释基础概念和背景知识,适合当作入门材料查阅。\",\"aiTags\":[\"📚 干货文\"]}',\n 'UCLA 遇到陶哲轩:以作者个人偶遇经历为核心,事件本身有荒诞感和娱乐性,不提供普遍规律或系统信息;应选 [\"💭 个人经历分享\",\"🤡 乐子文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"偏个人经历\",\"coreView\":\"这类回答的吸引力来自亲历细节和事件反差,参考价值有限但阅读趣味较强。\",\"aiTags\":[\"💭 个人经历分享\",\"🤡 乐子文\"]}',\n '女性完美身材短回答:内容极短,几乎没有论证或信息,价值主要来自猎奇、玩笑或看热闹;应选 [\"🤡 乐子文\"],不选 [\"观点鲜明\",\"📚 干货文\"]。JSON 示例:{\"shortJudgment\":\"可以当个乐子看\",\"coreView\":\"这类短回答缺少可分析观点,主要阅读价值是玩笑、猎奇或看热闹。\",\"aiTags\":[\"🤡 乐子文\"]}',\n '大一计算机分流建议:给出明确路径建议,信息密度高,包含学习、科研、实习、就业和保底方向,语气判断强;应选 [\"📚 干货文\",\"观点鲜明\",\"🏗️ 工具文\"],不选 [\"🎭 情绪文\",\"🤡 乐子文\"]。JSON 示例:{\"shortJudgment\":\"适合收藏\",\"coreView\":\"这类回答用明确立场组织路径建议和操作清单,适合带着判断筛选后复用。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\",\"🏗️ 工具文\"]}',\n '明显伪科普:用专业词汇包装巨大结论,但概念、证据链或因果跳跃不稳;应优先保留 [\"🧪 伪科普\"]。JSON 示例:{\"shortJudgment\":\"谨慎看待\",\"coreView\":\"这类回答用专业词汇包装跳跃结论,真正需要警惕的是概念和证据链并不稳。\",\"aiTags\":[\"🧪 伪科普\"]}',\n '明显软广带货:表面分享经验、测评或科普,实际导向特定产品、课程、服务、机构、私信或链接;应优先保留 [\"🛒 软广带货文\"]。JSON 示例:{\"shortJudgment\":\"广告味较重\",\"coreView\":\"这类回答表面分享经验或知识,实际阅读导向是引出特定产品、课程或服务。\",\"aiTags\":[\"🛒 软广带货文\"]}',\n \"\",\n \"返回格式示例:\",\n '{\"shortJudgment\":\"干货很多\",\"coreView\":\"真正影响职业发展的不是努力本身,而是能否持续进入高质量的信息和机会网络。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\"]}',\n \"\",\n \"回答正文:\",\n a.contentText,\n );\n\n return lines.join(\"\\n\");\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n \"🧠 思辨文\", \"📚 干货文\", \"🏗️ 工具文\", \"🎭 情绪文\", \"观点鲜明\",\n \"💭 个人经历分享\", \"🧃 爽文\", \"🧱 阵营文\", \"🪝 钓鱼文\", \"🧨 串子文\",\n \"🤡 乐子文\", \"🧪 伪科普\", \"🛒 软广带货文\",\n]);\n\nfunction parseReadingCard(content) {\n if (typeof content !== \"string\") return null;\n\n function validateCard(parsed) {\n if (typeof parsed?.shortJudgment !== \"string\" || typeof parsed?.coreView !== \"string\") return null;\n const rawTags = parsed.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t) => typeof t === \"string\" && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, \"\").slice(0, 10),\n coreView: parsed.coreView.trim(),\n aiTags,\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nfunction getMaxTokens(summaryLength) {\n if (summaryLength === \"short\") return 160;\n if (summaryLength === \"long\") return 520;\n return 320;\n}\n"}, "e775dbbbc8fe45764df506caf6d6d191c9e58909": {"t": 1785753882.0643177, "n": 9733, "h": "0bca2903240cd04d", "c": "import express from \"express\";\n\nconst app = express();\napp.use(express.json({ limit: \"1mb\" }));\n\n// Allow Chrome extension origins\napp.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n next();\n});\napp.options(\"*\", (_req, res) => res.sendStatus(204));\n\nconst PORT = process.env.PORT || 8080;\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_API_URL = \"https://api.deepseek.com/chat/completions\";\n\n// ────────────────────────────────────────────────────────────────────────────\n// Route\n// ────────────────────────────────────────────────────────────────────────────\n\napp.post(\"/summarize\", async (req, res) => {\n if (!DEEPSEEK_API_KEY) {\n return res.status(500).json({\n status: \"error\",\n errorMessage: \"Server misconfigured: DEEPSEEK_API_KEY not set.\",\n });\n }\n\n const request = req.body;\n if (!request?.answer?.contentText) {\n return res.status(400).json({\n status: \"error\",\n errorMessage: \"Missing required field: answer.contentText\",\n });\n }\n\n const model = request.model || \"deepseek-chat\";\n const summaryLength = request.summaryLength || \"medium\";\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: \"POST\",\n signal: controller.signal,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: buildSystemContent() },\n { role: \"user\", content: buildPrompt(request) },\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(summaryLength),\n response_format: { type: \"json_object\" },\n thinking: { type: \"disabled\" },\n }),\n });\n\n if (!response.ok) {\n const errText = await response.text().catch(() => \"\");\n return res.status(502).json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}${errText ? \" — \" + errText.slice(0, 200) : \"\"}`,\n });\n }\n\n const data = await response.json();\n const text = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return res.json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: \"DeepSeek 返回内容无法解析。\",\n });\n }\n\n return res.json({\n status: \"success\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags,\n });\n } catch (error) {\n const isTimeout =\n error instanceof Error &&\n (error.name === \"AbortError\" || error.message.includes(\"aborted\"));\n return res.status(504).json({\n status: \"error\",\n answerId: request.answer?.answerId,\n contentHash: request.answer?.contentHash,\n errorMessage: isTimeout ? \"DeepSeek 请求超时。\" : \"DeepSeek 服务暂时不可用。\",\n });\n } finally {\n clearTimeout(timeoutId);\n }\n});\n\napp.get(\"/health\", (_req, res) => res.json({ ok: true }));\n\napp.listen(PORT, () => {\n console.log(`看山 proxy listening on port ${PORT}`);\n});\n\n// ────────────────────────────────────────────────────────────────────────────\n// Prompt helpers (kept in sync with extension/src/background/prompt.ts)\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction buildSystemContent() {\n return [\n \"你是「看山先看」知乎阅读决策助手。\",\n \"你的任务不是写普通摘要,而是把一条知乎回答压缩成可直接帮助用户决定要不要读的阅读决策卡。\",\n \"只分析用户提供的回答内容,不添加外部事实;如果回答本身含糊、极端或证据不足,要在短判断和 aiTags 中体现。\",\n \"使用简体中文,语气轻盈、准确、贴近知乎语境;可以有轻微锋利感,但不要讽刺、人身攻击或道德审判。\",\n \"只返回 JSON 对象,不要返回 Markdown、代码块、解释文字、思考过程、分数或额外字段。\",\n \"\",\n \"# 输出字段\",\n \"shortJudgment:10 个汉字以内的阅读建议,去掉标点和空格;自然、简短,并贴合实际阅读价值。\",\n \"coreView:一句高密度摘要,讲述本文的大概脉络。不要写\\u201C本文主要讲述了\\u201D。要写作者通过哪些核心观点、结论、框架或故事线来传达这个脉络的;不要写成段落摘要。\",\n \"aiTags:从固定标签中选 1-3 个,优先覆盖主要阅读价值,再覆盖明显风险信号;不得自造标签。\",\n \"\",\n \"# 内部工作流(不要输出)\",\n \"1. 判断回答主导类型:知识、经验、观点、情绪、讨论引子,或混合类型。\",\n \"2. 提取作者真正想让读者带走的结论、警告、建议或立场。\",\n \"3. 判断阅读密度:信息密度、专业深度、主观性、情绪强度、娱乐性,以及是否有钓鱼、串子、伪科普、阵营叙事或软广带货等风险信号。\",\n \"4. 用适合场景的思路辅助判断阅读负担和价值,例如碎片时间、摸鱼、睡前、深度阅读、收藏后读;但不要输出场景标签或场景理由。\",\n \"5. 选择一个短判断、一个核心观点和 1-3 个 aiTags。\",\n \"6. 最终只输出符合约束的 JSON。\",\n \"\",\n \"# 固定 aiTags 及判断标准\",\n \"🧠 思辨文 — 提供分析框架,拆概念/前提/因果,能改变对问题的理解方式。\",\n \"📚 干货文 — 信息密度高、资料多,适合收藏查阅,可能观点不强。\",\n \"🏗️ 工具文 — 清单、流程、模板、网站、书单、路径建议,可执行性强。\",\n \"🎭 情绪文 — 情绪感染为主,事实或论证较少,读完主要获得情绪共鸣。\",\n \"观点鲜明 — 立场很强、结论明确、有冲击力,但证据链未必完整。\",\n \"💭 个人经历分享 — 以作者或身边人的经历为核心,有参考价值但样本有限。\",\n \"🧃 爽文 — 强烈站队、打脸叙事、敌我分明,读起来爽但未必严谨。\",\n \"🧱 阵营文 — 先有立场再找材料,放大对方问题、淡化己方问题,很少承认复杂性。\",\n \"🪝 钓鱼文 — 疑似故意设置极端观点吸引争吵或引战,论证薄但攻击性强。\",\n \"🧨 串子文 — 疑似伪装成某立场,故意说夸张、低级或招黑的话来反向抹黑某群体。\",\n \"🤡 乐子文 — 主要价值是看热闹、荒诞、反差和娱乐感,而不是获取严肃信息。\",\n \"🧪 伪科普 — 包装成科普,但概念混乱、证据不足、偷换概念或结论跳跃。\",\n \"🛒 软广带货文 — 表面是经验、测评或科普,实际导向产品、课程、服务、机构、私信或链接。\",\n \"\",\n \"# 区分原则\",\n \"🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度。\",\n \"📚 干货文 vs 🏗️ 工具文:前者重知识和资料,后者重操作路径和可复用工具。\",\n \"🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供心理爽感。\",\n \"🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑。\",\n \"如果回答同时有价值内容和风险信号,可以并列标签,例如 📚 干货文 + 观点鲜明,或 🧃 爽文 + 🧱 阵营文。\",\n \"如果出现明显伪科普或软广带货,优先保留对应风险标签。\",\n ].join(\"\\n\");\n}\n\nfunction buildPrompt(request) {\n const a = request.answer;\n const lines = [];\n\n lines.push(`问题标题:${a.questionTitle || \"未知\"}`);\n if (a.questionDescription) lines.push(`问题补充说明:${a.questionDescription}`);\n if (a.questionTopics?.length > 0) lines.push(`问题话题标签:${a.questionTopics.join(\" / \")}`);\n if (a.authorName) {\n lines.push(`回答作者:${a.authorHeadline ? `${a.authorName}(${a.authorHeadline})` : a.authorName}`);\n }\n if (a.upvoteCount) lines.push(`回答赞同数:${a.upvoteCount}`);\n if (a.commentCount) lines.push(`回答评论数:${a.commentCount}`);\n\n lines.push(\n \"\",\n '任务:为这条知乎回答生成\"看山先看\"阅读决策卡。',\n \"\",\n \"# 输出要求\",\n \"只返回 JSON 对象,且只能包含 shortJudgment、coreView、aiTags 三个字段。\",\n '不要输出 Markdown,不要输出\"适合场景\",不要输出阅读时间,不要输出解释。',\n \"shortJudgment:10 个汉字以内,优先从这些短语中选择或仿写:值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看。\",\n \"coreView:一句话,高密度、结论导向,必须基于回答正文;不要写成段落摘要。\",\n \"aiTags:从系统提示的固定标签中选 1-3 个,放入数组。\",\n \"\",\n \"# 标签校准案例\",\n '这些案例只用于选择 aiTags 和短判断,不要输出\"应选/不应选/选择依据\"。',\n 'NVIDIA/Huawei 技术争论:有 NVLink、InfiniBand、Fat-tree 等技术词,但表达主要是质问、嘲讽和反击,情绪强度高于解释密度;应选 [\"🎭 情绪文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"情绪输出偏多\",\"coreView\":\"这类回答的主要价值在于呈现争论立场和反击情绪,而不是建立稳定的技术解释框架。\",\"aiTags\":[\"🎭 情绪文\"]}',\n '黄仁勋吃饭调研段子:主要价值来自荒诞反差、现场戏剧感和结尾包袱,信息量不高;应选 [\"🤡 乐子文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"可以当个乐子看\",\"coreView\":\"这类回答主要靠荒诞反差和叙事包袱提供娱乐感,而不是提供可复用的信息或判断。\",\"aiTags\":[\"🤡 乐子文\"]}',\n '有机化工基础介绍:解释烃、有机化工、烯烃、塑料分类等基础知识,事实、概念和例子密集,可作为背景知识查阅;应选 [\"📚 干货文\"],不选 [\"🧃 爽文\",\"🎭 情绪文\"]。JSON 示例:{\"shortJudgment\":\"干货很多\",\"coreView\":\"这类回答的核心价值在于集中解释基础概念和背景知识,适合当作入门材料查阅。\",\"aiTags\":[\"📚 干货文\"]}',\n 'UCLA 遇到陶哲轩:以作者个人偶遇经历为核心,事件本身有荒诞感和娱乐性,不提供普遍规律或系统信息;应选 [\"💭 个人经历分享\",\"🤡 乐子文\"],不选 [\"📚 干货文\",\"🧠 思辨文\"]。JSON 示例:{\"shortJudgment\":\"偏个人经历\",\"coreView\":\"这类回答的吸引力来自亲历细节和事件反差,参考价值有限但阅读趣味较强。\",\"aiTags\":[\"💭 个人经历分享\",\"🤡 乐子文\"]}',\n '女性完美身材短回答:内容极短,几乎没有论证或信息,价值主要来自猎奇、玩笑或看热闹;应选 [\"🤡 乐子文\"],不选 [\"观点鲜明\",\"📚 干货文\"]。JSON 示例:{\"shortJudgment\":\"可以当个乐子看\",\"coreView\":\"这类短回答缺少可分析观点,主要阅读价值是玩笑、猎奇或看热闹。\",\"aiTags\":[\"🤡 乐子文\"]}',\n '大一计算机分流建议:给出明确路径建议,信息密度高,包含学习、科研、实习、就业和保底方向,语气判断强;应选 [\"📚 干货文\",\"观点鲜明\",\"🏗️ 工具文\"],不选 [\"🎭 情绪文\",\"🤡 乐子文\"]。JSON 示例:{\"shortJudgment\":\"适合收藏\",\"coreView\":\"这类回答用明确立场组织路径建议和操作清单,适合带着判断筛选后复用。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\",\"🏗️ 工具文\"]}',\n '明显伪科普:用专业词汇包装巨大结论,但概念、证据链或因果跳跃不稳;应优先保留 [\"🧪 伪科普\"]。JSON 示例:{\"shortJudgment\":\"谨慎看待\",\"coreView\":\"这类回答用专业词汇包装跳跃结论,真正需要警惕的是概念和证据链并不稳。\",\"aiTags\":[\"🧪 伪科普\"]}',\n '明显软广带货:表面分享经验、测评或科普,实际导向特定产品、课程、服务、机构、私信或链接;应优先保留 [\"🛒 软广带货文\"]。JSON 示例:{\"shortJudgment\":\"广告味较重\",\"coreView\":\"这类回答表面分享经验或知识,实际阅读导向是引出特定产品、课程或服务。\",\"aiTags\":[\"🛒 软广带货文\"]}',\n \"\",\n \"返回格式示例:\",\n '{\"shortJudgment\":\"干货很多\",\"coreView\":\"真正影响职业发展的不是努力本身,而是能否持续进入高质量的信息和机会网络。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\"]}',\n \"\",\n \"回答正文:\",\n a.contentText,\n );\n\n return lines.join(\"\\n\");\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n \"🧠 思辨文\", \"📚 干货文\", \"🏗️ 工具文\", \"🎭 情绪文\", \"观点鲜明\",\n \"💭 个人经历分享\", \"🧃 爽文\", \"🧱 阵营文\", \"🪝 钓鱼文\", \"🧨 串子文\",\n \"🤡 乐子文\", \"🧪 伪科普\", \"🛒 软广带货文\",\n]);\n\nfunction parseReadingCard(content) {\n if (typeof content !== \"string\") return null;\n\n function validateCard(parsed) {\n if (typeof parsed?.shortJudgment !== \"string\" || typeof parsed?.coreView !== \"string\") return null;\n const rawTags = parsed.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t) => typeof t === \"string\" && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, \"\").slice(0, 10),\n coreView: parsed.coreView.trim(),\n aiTags,\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nfunction getMaxTokens(summaryLength) {\n if (summaryLength === \"short\") return 160;\n if (summaryLength === \"long\") return 520;\n return 320;\n}\n"}, "6a1692218588643b87670ef9d3edd2decdb6d137": {"t": 1785753883.6754951, "n": 7081, "h": "fb0de893420f3e42", "c": "import express from \"express\";\n\nconst app = express();\napp.use(express.json({ limit: \"1mb\" }));\n\n// Allow Chrome extension origins\napp.use((_req, res, next) => {\n res.setHeader(\"Access-Control-Allow-Origin\", \"*\");\n res.setHeader(\"Access-Control-Allow-Methods\", \"GET, POST, OPTIONS\");\n res.setHeader(\"Access-Control-Allow-Headers\", \"Content-Type\");\n next();\n});\napp.options(\"*\", (_req, res) => res.sendStatus(204));\n\nconst PORT = process.env.PORT || 8080;\nconst DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY;\nconst DEEPSEEK_API_URL = \"https://api.deepseek.com/chat/completions\";\n\n// ────────────────────────────────────────────────────────────────────────────\n// Route\n// ────────────────────────────────────────────────────────────────────────────\n\napp.post(\"/summarize\", async (req, res) => {\n if (!DEEPSEEK_API_KEY) {\n return res.status(500).json({\n status: \"error\",\n errorMessage: \"Server misconfigured: DEEPSEEK_API_KEY not set.\",\n });\n }\n\n const request = req.body;\n if (!request?.answer?.contentText) {\n return res.status(400).json({\n status: \"error\",\n errorMessage: \"Missing required field: answer.contentText\",\n });\n }\n\n const model = request.model || \"deepseek-chat\";\n const summaryLength = request.summaryLength || \"medium\";\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: \"POST\",\n signal: controller.signal,\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${DEEPSEEK_API_KEY}`,\n },\n body: JSON.stringify({\n model,\n messages: [\n { role: \"system\", content: buildSystemContent() },\n { role: \"user\", content: buildPrompt(request) },\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(summaryLength),\n response_format: { type: \"json_object\" },\n thinking: { type: \"disabled\" },\n }),\n });\n\n if (!response.ok) {\n const errText = await response.text().catch(() => \"\");\n return res.status(502).json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}${errText ? \" — \" + errText.slice(0, 200) : \"\"}`,\n });\n }\n\n const data = await response.json();\n const text = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return res.json({\n status: \"error\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: \"DeepSeek 返回内容无法解析。\",\n });\n }\n\n return res.json({\n status: \"success\",\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags,\n });\n } catch (error) {\n const isTimeout =\n error instanceof Error &&\n (error.name === \"AbortError\" || error.message.includes(\"aborted\"));\n return res.status(504).json({\n status: \"error\",\n answerId: request.answer?.answerId,\n contentHash: request.answer?.contentHash,\n errorMessage: isTimeout ? \"DeepSeek 请求超时。\" : \"DeepSeek 服务暂时不可用。\",\n });\n } finally {\n clearTimeout(timeoutId);\n }\n});\n\napp.get(\"/health\", (_req, res) => res.json({ ok: true }));\n\napp.listen(PORT, () => {\n console.log(`看山 proxy listening on port ${PORT}`);\n});\n\n// ────────────────────────────────────────────────────────────────────────────\n// Prompt helpers (kept in sync with extension/dist/background/serviceWorker.js)\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction buildSystemContent() {\n return [\n \"你是「看山」知乎阅读决策助手。\",\n \"只分析用户提供的回答内容,不添加外部事实。\",\n \"使用简体中文,语气轻盈、准确、贴近知乎语境。\",\n \"你的任务是帮用户判断这条回答是否值得读、适合哪类读者,而不是写普通摘要。\",\n \"只返回 JSON,不要返回 Markdown 或任何额外解释。\",\n \"\",\n \"# AI 评价标签(aiTags)使用规则\",\n \"从下方 13 个固定标签中选 1-3 个,输出到 aiTags 数组,不得自造新标签:\",\n \"🧠 思辨文 — 有分析框架,拆概念/拆因果,能改变对问题的理解方式\",\n \"📚 干货文 — 信息密度高、资料多,适合收藏,可能观点不强\",\n \"🏗️ 工具文 — 清单/流程/工具/书单,可执行性强\",\n \"🎭 情绪文 — 情绪感染为主,事实较少,读完主要获得情绪共鸣\",\n \"观点鲜明 — 立场很强,有冲击力,但证据链未必完整\",\n \"💭 个人经历分享 — 以作者或身边人的亲身经历为核心,样本有限\",\n \"🧃 爽文 — 强烈站队,打脸叙事,读起来爽但未必严谨\",\n \"🧱 阵营文 — 先有立场再找材料,不承认复杂性,喜欢划分敌我\",\n \"🪝 钓鱼文 — 疑似故意用极端观点引战,论证薄但攻击性强\",\n \"🧨 串子文 — 疑似伪装立场、故意说夸张低级的话来反向抹黑某群体\",\n \"🤡 乐子文 — 主要价值是看热闹/荒诞/反差,娱乐性强\",\n \"🧪 伪科普 — 堆叠专业词汇但概念混乱,结论跳跃,制造虚假权威感\",\n \"🛒 软广带货文 — 前半段讲痛点后半段引出产品/课程/服务\",\n \"\",\n \"# 区分原则\",\n \"🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度\",\n \"📚 干货文 vs 🏗️ 工具文:前者重知识资料,后者重操作路径\",\n \"🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供爽感\",\n \"🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑\",\n ].join(\"\\n\");\n}\n\nfunction buildPrompt(request) {\n const a = request.answer;\n const lines = [];\n\n lines.push(`问题标题:${a.questionTitle || \"未知\"}`);\n if (a.questionDescription) lines.push(`问题补充说明:${a.questionDescription}`);\n if (a.questionTopics?.length > 0) lines.push(`问题话题标签:${a.questionTopics.join(\" / \")}`);\n if (a.authorName) {\n lines.push(`回答作者:${a.authorHeadline ? `${a.authorName}(${a.authorHeadline})` : a.authorName}`);\n }\n if (a.upvoteCount) lines.push(`回答赞同数:${a.upvoteCount}`);\n if (a.commentCount) lines.push(`回答评论数:${a.commentCount}`);\n\n lines.push(\n \"\",\n '任务:为这条知乎回答生成\"看山\"阅读决策卡。',\n \"\",\n \"# 字段说明\",\n \"shortJudgment:10 个汉字以内的阅读建议,优先从以下短语中选择或仿写:\",\n \" 值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看\",\n 'coreView:只写一句高密度核心观点。要求:贴近作者结论而非泛泛摘要;不写\"本文主要讲述了\";必须基于正文;不超过两行显示。',\n \"aiTags:从系统提示的 13 个固定标签中选 1-3 个,放入数组。根据内容主要价值和明显风险信号选择。\",\n \"\",\n \"# 校准示例\",\n '示例 A(技术争论文,情绪强于解释)→ aiTags: [\"🎭 情绪文\"],不选 📚 干货文',\n '示例 B(段子/荒诞反差故事)→ aiTags: [\"🤡 乐子文\"],不选 📚 干货文',\n '示例 C(知识密集的背景介绍)→ aiTags: [\"📚 干货文\"]',\n '示例 D(作者亲历有趣经历)→ aiTags: [\"💭 个人经历分享\", \"🤡 乐子文\"]',\n '示例 E(路径建议+立场鲜明+工具清单)→ aiTags: [\"📚 干货文\", \"观点鲜明\", \"🏗️ 工具文\"]',\n \"\",\n \"返回格式必须是 JSON,示例:\",\n '{\"shortJudgment\":\"干货很多\",\"coreView\":\"作者认为真正影响职业发展的是能否持续进入高质量的信息和机会网络。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\"]}',\n \"\",\n \"回答正文:\",\n a.contentText,\n );\n\n return lines.join(\"\\n\");\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n \"🧠 思辨文\", \"📚 干货文\", \"🏗️ 工具文\", \"🎭 情绪文\", \"观点鲜明\",\n \"💭 个人经历分享\", \"🧃 爽文\", \"🧱 阵营文\", \"🪝 钓鱼文\", \"🧨 串子文\",\n \"🤡 乐子文\", \"🧪 伪科普\", \"🛒 软广带货文\",\n]);\n\nfunction parseReadingCard(content) {\n if (typeof content !== \"string\") return null;\n try {\n const parsed = JSON.parse(content);\n if (typeof parsed.shortJudgment === \"string\" && typeof parsed.coreView === \"string\") {\n const rawTags = parsed.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t) => typeof t === \"string\" && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, \"\").slice(0, 10),\n coreView: parsed.coreView.trim(),\n aiTags,\n };\n }\n } catch {\n return null;\n }\n return null;\n}\n\nfunction getMaxTokens(summaryLength) {\n if (summaryLength === \"short\") return 160;\n if (summaryLength === \"long\") return 520;\n return 320;\n}\n"}}, "extension/src/background/summaryClient.ts": {"01b22f38182ed9d0912018a88fb2f7046254f7ac": {"t": 1785746581.6414824, "n": 13682, "h": "29a0d4c04bb6e857", "c": "import type { AiProvider, ExtensionSettings, ModelOption, SummaryRequest, SummaryResponse } from '../shared/types';\nimport { LOCAL_AI_API_KEY, LOCAL_DEEPSEEK_API_KEY } from './localConfig';\nimport { buildPrompt, buildSystemContent, isAllowedAiTag } from './prompt';\n\ntype ProviderStyle = 'openai' | 'anthropic';\n\ninterface ProviderConfig {\n url: string;\n style: ProviderStyle;\n}\n\ninterface ModelsConfig {\n url: (apiKey: string) => string;\n headers: (apiKey: string) => Record<string, string>;\n parse: (data: unknown) => ModelOption[];\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst DEFAULT_PROXY_URL = 'https://kanshan-proxy-110580126301.us-central1.run.app/summarize';\n\nconst PROVIDER_CONFIG: Record<Exclude<AiProvider, 'mock'>, ProviderConfig> = {\n deepseek: { url: 'https://api.deepseek.com/chat/completions', style: 'openai' },\n openai: { url: 'https://api.openai.com/v1/chat/completions', style: 'openai' },\n anthropic: { url: 'https://api.anthropic.com/v1/messages', style: 'anthropic' },\n google: { url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', style: 'openai' },\n zhipu: { url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', style: 'openai' },\n qwen: { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', style: 'openai' },\n moonshot: { url: 'https://api.moonshot.cn/v1/chat/completions', style: 'openai' }\n};\n\nconst MODELS_CONFIG: Partial<Record<AiProvider, ModelsConfig>> = {\n openai: {\n url: () => 'https://api.openai.com/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => {\n const items = asArray(asRecord(data).data);\n return items\n .map(asRecord)\n .filter((m) => typeof m.id === 'string' && /^(gpt-|o1|o3|o4)/.test(m.id))\n .sort((a, b) => Number(b.created ?? 0) - Number(a.created ?? 0))\n .slice(0, 15)\n .map((m) => ({ id: m.id as string, label: m.id as string }));\n }\n },\n google: {\n url: (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}&pageSize=50`,\n headers: () => ({}),\n parse: (data) => {\n const items = asArray(asRecord(data).models);\n return items\n .map(asRecord)\n .filter((m) => {\n const methods = asArray(m.supportedGenerationMethods);\n return typeof m.name === 'string' && methods.includes('generateContent') && /gemini/.test(m.name);\n })\n .map((m) => {\n const id = (m.name as string).replace('models/', '');\n return { id, label: typeof m.displayName === 'string' ? m.displayName : id };\n });\n }\n },\n deepseek: {\n url: () => 'https://api.deepseek.com/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n },\n moonshot: {\n url: () => 'https://api.moonshot.cn/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n }\n};\n\nconst FALLBACK_MODELS: Record<AiProvider, ModelOption[]> = {\n mock: [],\n deepseek: [\n { id: 'deepseek-chat', label: 'deepseek-chat (V3)' },\n { id: 'deepseek-reasoner', label: 'deepseek-reasoner (R1)' }\n ],\n openai: [\n { id: 'gpt-4.1', label: 'GPT-4.1' },\n { id: 'gpt-4.1-mini', label: 'GPT-4.1 mini' },\n { id: 'gpt-4o', label: 'GPT-4o' },\n { id: 'gpt-4o-mini', label: 'GPT-4o mini' }\n ],\n anthropic: [\n { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },\n { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },\n { id: 'claude-haiku-3-5', label: 'Claude Haiku 3.5' }\n ],\n google: [\n { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },\n { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },\n { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }\n ],\n zhipu: [\n { id: 'glm-4-plus', label: 'GLM-4 Plus' },\n { id: 'glm-4-air', label: 'GLM-4 Air' },\n { id: 'glm-4-flash', label: 'GLM-4 Flash(免费)' }\n ],\n qwen: [\n { id: 'qwen-max', label: 'Qwen Max' },\n { id: 'qwen-plus', label: 'Qwen Plus' },\n { id: 'qwen-turbo', label: 'Qwen Turbo' }\n ],\n moonshot: [\n { id: 'moonshot-v1-128k', label: 'Kimi 128k' },\n { id: 'moonshot-v1-32k', label: 'Kimi 32k' },\n { id: 'moonshot-v1-8k', label: 'Kimi 8k' }\n ]\n};\n\nexport async function getModels(payload: { provider?: AiProvider | string; apiKey?: string } = {}): Promise<{ models: ModelOption[] }> {\n const provider = normalizeProvider(payload.provider);\n const fallback = provider ? FALLBACK_MODELS[provider] : [];\n const config = provider ? MODELS_CONFIG[provider] : undefined;\n const apiKey = payload.apiKey?.trim();\n if (!config || !apiKey) return { models: fallback };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 8000);\n try {\n const response = await fetch(config.url(apiKey), {\n headers: { 'Content-Type': 'application/json', ...config.headers(apiKey) },\n signal: controller.signal\n });\n if (!response.ok) return { models: fallback };\n const data = await response.json();\n const models = config.parse(data);\n return { models: models.length > 0 ? models : fallback };\n } catch {\n return { models: fallback };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'remote-api') {\n return generateRemoteSummary(request, settings.backendUrl || DEFAULT_PROXY_URL);\n }\n\n if (settings.backendType === 'mock') {\n return generateMockSummary(request, settings);\n }\n\n const provider = normalizeProvider(settings.aiProvider) ?? (settings.backendType === 'deepseek' ? 'deepseek' : undefined);\n if (provider && provider !== 'mock' && PROVIDER_CONFIG[provider]) {\n return generateAiSummary(request, settings, provider);\n }\n\n return generateMockSummary(request, settings);\n}\n\nasync function generateRemoteSummary(request: SummaryRequest, backendUrl: string): Promise<SummaryResponse> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 28000);\n\n try {\n const response = await fetch(backendUrl, {\n method: 'POST',\n signal: controller.signal,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request)\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `代理服务请求失败:HTTP ${response.status}`\n };\n }\n\n return await response.json() as SummaryResponse;\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? '代理服务请求超时。'\n : '代理服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nasync function generateAiSummary(\n request: SummaryRequest,\n settings: ExtensionSettings,\n provider: Exclude<AiProvider, 'mock'>\n): Promise<SummaryResponse> {\n const config = PROVIDER_CONFIG[provider];\n const apiKey = settings.aiApiKey?.trim()\n || LOCAL_AI_API_KEY.trim()\n || (provider === 'deepseek' ? (settings.deepseekApiKey?.trim() || LOCAL_DEEPSEEK_API_KEY.trim()) : '');\n\n if (!apiKey || apiKey === 'sk-YOUR_KEY_HERE') {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `请先在扩展弹窗中填写 ${provider} 的 API Key。`\n };\n }\n\n const model = settings.aiModel || settings.deepseekModel || 'deepseek-chat';\n const systemContent = buildSystemContent();\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n let response: Response;\n\n if (config.style === 'anthropic') {\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01'\n },\n body: JSON.stringify({\n model,\n max_tokens: getMaxTokens(request.summaryLength),\n system: systemContent,\n messages: [{ role: 'user', content: buildPrompt(request) }]\n })\n });\n } else {\n const body: UnknownRecord = {\n model,\n messages: [\n { role: 'system', content: systemContent },\n { role: 'user', content: buildPrompt(request) }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' }\n };\n if (provider === 'deepseek') body.thinking = { type: 'disabled' };\n\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(body)\n });\n }\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = asRecord(await response.json());\n const text = config.style === 'anthropic'\n ? asRecord(asArray(data.content)[0]).text\n : asRecord(asRecord(asArray(data.choices)[0]).message).content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 返回内容无法解析。`\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? `${provider} 请求超时。`\n : `${provider} 服务暂时不可用。`\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n function validateCard(parsed: unknown): ReturnType<typeof parseReadingCard> {\n const p = asRecord(parsed);\n if (typeof p.shortJudgment !== 'string' || typeof p.coreView !== 'string') return null;\n const aiTags = asArray(p.aiTags)\n .filter(isAllowedAiTag)\n .slice(0, 3);\n return {\n shortJudgment: p.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: p.coreView.trim(),\n aiTags\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < settings.minSummarizableChars) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction normalizeProvider(provider: unknown): AiProvider | undefined {\n return typeof provider === 'string' && provider in FALLBACK_MODELS ? provider as AiProvider : undefined;\n}\n\nfunction asRecord(value: unknown): UnknownRecord {\n return value && typeof value === 'object' ? value as UnknownRecord : {};\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n"}, "3f89b0a9050afc3645394c9f21a25388bbea4676": {"t": 1785753883.6655555, "n": 13682, "h": "29a0d4c04bb6e857", "c": "import type { AiProvider, ExtensionSettings, ModelOption, SummaryRequest, SummaryResponse } from '../shared/types';\nimport { LOCAL_AI_API_KEY, LOCAL_DEEPSEEK_API_KEY } from './localConfig';\nimport { buildPrompt, buildSystemContent, isAllowedAiTag } from './prompt';\n\ntype ProviderStyle = 'openai' | 'anthropic';\n\ninterface ProviderConfig {\n url: string;\n style: ProviderStyle;\n}\n\ninterface ModelsConfig {\n url: (apiKey: string) => string;\n headers: (apiKey: string) => Record<string, string>;\n parse: (data: unknown) => ModelOption[];\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst DEFAULT_PROXY_URL = 'https://kanshan-proxy-110580126301.us-central1.run.app/summarize';\n\nconst PROVIDER_CONFIG: Record<Exclude<AiProvider, 'mock'>, ProviderConfig> = {\n deepseek: { url: 'https://api.deepseek.com/chat/completions', style: 'openai' },\n openai: { url: 'https://api.openai.com/v1/chat/completions', style: 'openai' },\n anthropic: { url: 'https://api.anthropic.com/v1/messages', style: 'anthropic' },\n google: { url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', style: 'openai' },\n zhipu: { url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', style: 'openai' },\n qwen: { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', style: 'openai' },\n moonshot: { url: 'https://api.moonshot.cn/v1/chat/completions', style: 'openai' }\n};\n\nconst MODELS_CONFIG: Partial<Record<AiProvider, ModelsConfig>> = {\n openai: {\n url: () => 'https://api.openai.com/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => {\n const items = asArray(asRecord(data).data);\n return items\n .map(asRecord)\n .filter((m) => typeof m.id === 'string' && /^(gpt-|o1|o3|o4)/.test(m.id))\n .sort((a, b) => Number(b.created ?? 0) - Number(a.created ?? 0))\n .slice(0, 15)\n .map((m) => ({ id: m.id as string, label: m.id as string }));\n }\n },\n google: {\n url: (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}&pageSize=50`,\n headers: () => ({}),\n parse: (data) => {\n const items = asArray(asRecord(data).models);\n return items\n .map(asRecord)\n .filter((m) => {\n const methods = asArray(m.supportedGenerationMethods);\n return typeof m.name === 'string' && methods.includes('generateContent') && /gemini/.test(m.name);\n })\n .map((m) => {\n const id = (m.name as string).replace('models/', '');\n return { id, label: typeof m.displayName === 'string' ? m.displayName : id };\n });\n }\n },\n deepseek: {\n url: () => 'https://api.deepseek.com/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n },\n moonshot: {\n url: () => 'https://api.moonshot.cn/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n }\n};\n\nconst FALLBACK_MODELS: Record<AiProvider, ModelOption[]> = {\n mock: [],\n deepseek: [\n { id: 'deepseek-chat', label: 'deepseek-chat (V3)' },\n { id: 'deepseek-reasoner', label: 'deepseek-reasoner (R1)' }\n ],\n openai: [\n { id: 'gpt-4.1', label: 'GPT-4.1' },\n { id: 'gpt-4.1-mini', label: 'GPT-4.1 mini' },\n { id: 'gpt-4o', label: 'GPT-4o' },\n { id: 'gpt-4o-mini', label: 'GPT-4o mini' }\n ],\n anthropic: [\n { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },\n { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },\n { id: 'claude-haiku-3-5', label: 'Claude Haiku 3.5' }\n ],\n google: [\n { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },\n { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },\n { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }\n ],\n zhipu: [\n { id: 'glm-4-plus', label: 'GLM-4 Plus' },\n { id: 'glm-4-air', label: 'GLM-4 Air' },\n { id: 'glm-4-flash', label: 'GLM-4 Flash(免费)' }\n ],\n qwen: [\n { id: 'qwen-max', label: 'Qwen Max' },\n { id: 'qwen-plus', label: 'Qwen Plus' },\n { id: 'qwen-turbo', label: 'Qwen Turbo' }\n ],\n moonshot: [\n { id: 'moonshot-v1-128k', label: 'Kimi 128k' },\n { id: 'moonshot-v1-32k', label: 'Kimi 32k' },\n { id: 'moonshot-v1-8k', label: 'Kimi 8k' }\n ]\n};\n\nexport async function getModels(payload: { provider?: AiProvider | string; apiKey?: string } = {}): Promise<{ models: ModelOption[] }> {\n const provider = normalizeProvider(payload.provider);\n const fallback = provider ? FALLBACK_MODELS[provider] : [];\n const config = provider ? MODELS_CONFIG[provider] : undefined;\n const apiKey = payload.apiKey?.trim();\n if (!config || !apiKey) return { models: fallback };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 8000);\n try {\n const response = await fetch(config.url(apiKey), {\n headers: { 'Content-Type': 'application/json', ...config.headers(apiKey) },\n signal: controller.signal\n });\n if (!response.ok) return { models: fallback };\n const data = await response.json();\n const models = config.parse(data);\n return { models: models.length > 0 ? models : fallback };\n } catch {\n return { models: fallback };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'remote-api') {\n return generateRemoteSummary(request, settings.backendUrl || DEFAULT_PROXY_URL);\n }\n\n if (settings.backendType === 'mock') {\n return generateMockSummary(request, settings);\n }\n\n const provider = normalizeProvider(settings.aiProvider) ?? (settings.backendType === 'deepseek' ? 'deepseek' : undefined);\n if (provider && provider !== 'mock' && PROVIDER_CONFIG[provider]) {\n return generateAiSummary(request, settings, provider);\n }\n\n return generateMockSummary(request, settings);\n}\n\nasync function generateRemoteSummary(request: SummaryRequest, backendUrl: string): Promise<SummaryResponse> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 28000);\n\n try {\n const response = await fetch(backendUrl, {\n method: 'POST',\n signal: controller.signal,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request)\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `代理服务请求失败:HTTP ${response.status}`\n };\n }\n\n return await response.json() as SummaryResponse;\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? '代理服务请求超时。'\n : '代理服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nasync function generateAiSummary(\n request: SummaryRequest,\n settings: ExtensionSettings,\n provider: Exclude<AiProvider, 'mock'>\n): Promise<SummaryResponse> {\n const config = PROVIDER_CONFIG[provider];\n const apiKey = settings.aiApiKey?.trim()\n || LOCAL_AI_API_KEY.trim()\n || (provider === 'deepseek' ? (settings.deepseekApiKey?.trim() || LOCAL_DEEPSEEK_API_KEY.trim()) : '');\n\n if (!apiKey || apiKey === 'sk-YOUR_KEY_HERE') {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `请先在扩展弹窗中填写 ${provider} 的 API Key。`\n };\n }\n\n const model = settings.aiModel || settings.deepseekModel || 'deepseek-chat';\n const systemContent = buildSystemContent();\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n let response: Response;\n\n if (config.style === 'anthropic') {\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01'\n },\n body: JSON.stringify({\n model,\n max_tokens: getMaxTokens(request.summaryLength),\n system: systemContent,\n messages: [{ role: 'user', content: buildPrompt(request) }]\n })\n });\n } else {\n const body: UnknownRecord = {\n model,\n messages: [\n { role: 'system', content: systemContent },\n { role: 'user', content: buildPrompt(request) }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' }\n };\n if (provider === 'deepseek') body.thinking = { type: 'disabled' };\n\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(body)\n });\n }\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = asRecord(await response.json());\n const text = config.style === 'anthropic'\n ? asRecord(asArray(data.content)[0]).text\n : asRecord(asRecord(asArray(data.choices)[0]).message).content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 返回内容无法解析。`\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? `${provider} 请求超时。`\n : `${provider} 服务暂时不可用。`\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n function validateCard(parsed: unknown): ReturnType<typeof parseReadingCard> {\n const p = asRecord(parsed);\n if (typeof p.shortJudgment !== 'string' || typeof p.coreView !== 'string') return null;\n const aiTags = asArray(p.aiTags)\n .filter(isAllowedAiTag)\n .slice(0, 3);\n return {\n shortJudgment: p.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: p.coreView.trim(),\n aiTags\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < settings.minSummarizableChars) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction normalizeProvider(provider: unknown): AiProvider | undefined {\n return typeof provider === 'string' && provider in FALLBACK_MODELS ? provider as AiProvider : undefined;\n}\n\nfunction asRecord(value: unknown): UnknownRecord {\n return value && typeof value === 'object' ? value as UnknownRecord : {};\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n"}, "af50a571971b3fc5e228c7c313d95261e7ef86c1": {"t": 1785753885.132808, "n": 13589, "h": "391aa2c9c4981667", "c": "import type { AiProvider, ExtensionSettings, ModelOption, SummaryRequest, SummaryResponse } from '../shared/types';\nimport { LOCAL_AI_API_KEY, LOCAL_DEEPSEEK_API_KEY } from './localConfig';\nimport { buildPrompt, buildSystemContent, isAllowedAiTag } from './prompt';\n\ntype ProviderStyle = 'openai' | 'anthropic';\n\ninterface ProviderConfig {\n url: string;\n style: ProviderStyle;\n}\n\ninterface ModelsConfig {\n url: (apiKey: string) => string;\n headers: (apiKey: string) => Record<string, string>;\n parse: (data: unknown) => ModelOption[];\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst PROVIDER_CONFIG: Record<Exclude<AiProvider, 'mock'>, ProviderConfig> = {\n deepseek: { url: 'https://api.deepseek.com/chat/completions', style: 'openai' },\n openai: { url: 'https://api.openai.com/v1/chat/completions', style: 'openai' },\n anthropic: { url: 'https://api.anthropic.com/v1/messages', style: 'anthropic' },\n google: { url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', style: 'openai' },\n zhipu: { url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', style: 'openai' },\n qwen: { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', style: 'openai' },\n moonshot: { url: 'https://api.moonshot.cn/v1/chat/completions', style: 'openai' }\n};\n\nconst MODELS_CONFIG: Partial<Record<AiProvider, ModelsConfig>> = {\n openai: {\n url: () => 'https://api.openai.com/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => {\n const items = asArray(asRecord(data).data);\n return items\n .map(asRecord)\n .filter((m) => typeof m.id === 'string' && /^(gpt-|o1|o3|o4)/.test(m.id))\n .sort((a, b) => Number(b.created ?? 0) - Number(a.created ?? 0))\n .slice(0, 15)\n .map((m) => ({ id: m.id as string, label: m.id as string }));\n }\n },\n google: {\n url: (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}&pageSize=50`,\n headers: () => ({}),\n parse: (data) => {\n const items = asArray(asRecord(data).models);\n return items\n .map(asRecord)\n .filter((m) => {\n const methods = asArray(m.supportedGenerationMethods);\n return typeof m.name === 'string' && methods.includes('generateContent') && /gemini/.test(m.name);\n })\n .map((m) => {\n const id = (m.name as string).replace('models/', '');\n return { id, label: typeof m.displayName === 'string' ? m.displayName : id };\n });\n }\n },\n deepseek: {\n url: () => 'https://api.deepseek.com/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n },\n moonshot: {\n url: () => 'https://api.moonshot.cn/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n }\n};\n\nconst FALLBACK_MODELS: Record<AiProvider, ModelOption[]> = {\n mock: [],\n deepseek: [\n { id: 'deepseek-chat', label: 'deepseek-chat (V3)' },\n { id: 'deepseek-reasoner', label: 'deepseek-reasoner (R1)' }\n ],\n openai: [\n { id: 'gpt-4.1', label: 'GPT-4.1' },\n { id: 'gpt-4.1-mini', label: 'GPT-4.1 mini' },\n { id: 'gpt-4o', label: 'GPT-4o' },\n { id: 'gpt-4o-mini', label: 'GPT-4o mini' }\n ],\n anthropic: [\n { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },\n { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },\n { id: 'claude-haiku-3-5', label: 'Claude Haiku 3.5' }\n ],\n google: [\n { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },\n { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },\n { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }\n ],\n zhipu: [\n { id: 'glm-4-plus', label: 'GLM-4 Plus' },\n { id: 'glm-4-air', label: 'GLM-4 Air' },\n { id: 'glm-4-flash', label: 'GLM-4 Flash(免费)' }\n ],\n qwen: [\n { id: 'qwen-max', label: 'Qwen Max' },\n { id: 'qwen-plus', label: 'Qwen Plus' },\n { id: 'qwen-turbo', label: 'Qwen Turbo' }\n ],\n moonshot: [\n { id: 'moonshot-v1-128k', label: 'Kimi 128k' },\n { id: 'moonshot-v1-32k', label: 'Kimi 32k' },\n { id: 'moonshot-v1-8k', label: 'Kimi 8k' }\n ]\n};\n\nexport async function getModels(payload: { provider?: AiProvider | string; apiKey?: string } = {}): Promise<{ models: ModelOption[] }> {\n const provider = normalizeProvider(payload.provider);\n const fallback = provider ? FALLBACK_MODELS[provider] : [];\n const config = provider ? MODELS_CONFIG[provider] : undefined;\n const apiKey = payload.apiKey?.trim();\n if (!config || !apiKey) return { models: fallback };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 8000);\n try {\n const response = await fetch(config.url(apiKey), {\n headers: { 'Content-Type': 'application/json', ...config.headers(apiKey) },\n signal: controller.signal\n });\n if (!response.ok) return { models: fallback };\n const data = await response.json();\n const models = config.parse(data);\n return { models: models.length > 0 ? models : fallback };\n } catch {\n return { models: fallback };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'remote-api' && settings.backendUrl) {\n return generateRemoteSummary(request, settings.backendUrl);\n }\n\n if (settings.backendType === 'mock') {\n return generateMockSummary(request, settings);\n }\n\n const provider = normalizeProvider(settings.aiProvider) ?? (settings.backendType === 'deepseek' ? 'deepseek' : undefined);\n if (provider && provider !== 'mock' && PROVIDER_CONFIG[provider]) {\n return generateAiSummary(request, settings, provider);\n }\n\n return generateMockSummary(request, settings);\n}\n\nasync function generateRemoteSummary(request: SummaryRequest, backendUrl: string): Promise<SummaryResponse> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 28000);\n\n try {\n const response = await fetch(backendUrl, {\n method: 'POST',\n signal: controller.signal,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request)\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `代理服务请求失败:HTTP ${response.status}`\n };\n }\n\n return await response.json() as SummaryResponse;\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? '代理服务请求超时。'\n : '代理服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nasync function generateAiSummary(\n request: SummaryRequest,\n settings: ExtensionSettings,\n provider: Exclude<AiProvider, 'mock'>\n): Promise<SummaryResponse> {\n const config = PROVIDER_CONFIG[provider];\n const apiKey = settings.aiApiKey?.trim()\n || LOCAL_AI_API_KEY.trim()\n || (provider === 'deepseek' ? (settings.deepseekApiKey?.trim() || LOCAL_DEEPSEEK_API_KEY.trim()) : '');\n\n if (!apiKey || apiKey === 'sk-YOUR_KEY_HERE') {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `请先在扩展弹窗中填写 ${provider} 的 API Key。`\n };\n }\n\n const model = settings.aiModel || settings.deepseekModel || 'deepseek-chat';\n const systemContent = buildSystemContent();\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n let response: Response;\n\n if (config.style === 'anthropic') {\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01'\n },\n body: JSON.stringify({\n model,\n max_tokens: getMaxTokens(request.summaryLength),\n system: systemContent,\n messages: [{ role: 'user', content: buildPrompt(request) }]\n })\n });\n } else {\n const body: UnknownRecord = {\n model,\n messages: [\n { role: 'system', content: systemContent },\n { role: 'user', content: buildPrompt(request) }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' }\n };\n if (provider === 'deepseek') body.thinking = { type: 'disabled' };\n\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(body)\n });\n }\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = asRecord(await response.json());\n const text = config.style === 'anthropic'\n ? asRecord(asArray(data.content)[0]).text\n : asRecord(asRecord(asArray(data.choices)[0]).message).content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 返回内容无法解析。`\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? `${provider} 请求超时。`\n : `${provider} 服务暂时不可用。`\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n function validateCard(parsed: unknown): ReturnType<typeof parseReadingCard> {\n const p = asRecord(parsed);\n if (typeof p.shortJudgment !== 'string' || typeof p.coreView !== 'string') return null;\n const aiTags = asArray(p.aiTags)\n .filter(isAllowedAiTag)\n .slice(0, 3);\n return {\n shortJudgment: p.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: p.coreView.trim(),\n aiTags\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < settings.minSummarizableChars) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction normalizeProvider(provider: unknown): AiProvider | undefined {\n return typeof provider === 'string' && provider in FALLBACK_MODELS ? provider as AiProvider : undefined;\n}\n\nfunction asRecord(value: unknown): UnknownRecord {\n return value && typeof value === 'object' ? value as UnknownRecord : {};\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n"}, "df5f9ca779e49cc039068361ebce7ba895adde49": {"t": 1785753886.2762482, "n": 13513, "h": "1b543c9944d2fcd0", "c": "import type { AiProvider, ExtensionSettings, ModelOption, SummaryRequest, SummaryResponse } from '../shared/types';\nimport { LOCAL_AI_API_KEY, LOCAL_DEEPSEEK_API_KEY } from './localConfig';\nimport { buildPrompt, buildSystemContent, isAllowedAiTag } from './prompt';\n\ntype ProviderStyle = 'openai' | 'anthropic';\n\ninterface ProviderConfig {\n url: string;\n style: ProviderStyle;\n}\n\ninterface ModelsConfig {\n url: (apiKey: string) => string;\n headers: (apiKey: string) => Record<string, string>;\n parse: (data: unknown) => ModelOption[];\n}\n\ntype UnknownRecord = Record<string, unknown>;\n\nconst PROVIDER_CONFIG: Record<Exclude<AiProvider, 'mock'>, ProviderConfig> = {\n deepseek: { url: 'https://api.deepseek.com/chat/completions', style: 'openai' },\n openai: { url: 'https://api.openai.com/v1/chat/completions', style: 'openai' },\n anthropic: { url: 'https://api.anthropic.com/v1/messages', style: 'anthropic' },\n google: { url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', style: 'openai' },\n zhipu: { url: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', style: 'openai' },\n qwen: { url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', style: 'openai' },\n moonshot: { url: 'https://api.moonshot.cn/v1/chat/completions', style: 'openai' }\n};\n\nconst MODELS_CONFIG: Partial<Record<AiProvider, ModelsConfig>> = {\n openai: {\n url: () => 'https://api.openai.com/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => {\n const items = asArray(asRecord(data).data);\n return items\n .map(asRecord)\n .filter((m) => typeof m.id === 'string' && /^(gpt-|o1|o3|o4)/.test(m.id))\n .sort((a, b) => Number(b.created ?? 0) - Number(a.created ?? 0))\n .slice(0, 15)\n .map((m) => ({ id: m.id as string, label: m.id as string }));\n }\n },\n google: {\n url: (key) => `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(key)}&pageSize=50`,\n headers: () => ({}),\n parse: (data) => {\n const items = asArray(asRecord(data).models);\n return items\n .map(asRecord)\n .filter((m) => {\n const methods = asArray(m.supportedGenerationMethods);\n return typeof m.name === 'string' && methods.includes('generateContent') && /gemini/.test(m.name);\n })\n .map((m) => {\n const id = (m.name as string).replace('models/', '');\n return { id, label: typeof m.displayName === 'string' ? m.displayName : id };\n });\n }\n },\n deepseek: {\n url: () => 'https://api.deepseek.com/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n },\n moonshot: {\n url: () => 'https://api.moonshot.cn/v1/models',\n headers: (key) => ({ Authorization: `Bearer ${key}` }),\n parse: (data) => asArray(asRecord(data).data)\n .map(asRecord)\n .filter((m) => typeof m.id === 'string')\n .map((m) => ({ id: m.id as string, label: m.id as string }))\n }\n};\n\nconst FALLBACK_MODELS: Record<AiProvider, ModelOption[]> = {\n mock: [],\n deepseek: [\n { id: 'deepseek-chat', label: 'deepseek-chat (V3)' },\n { id: 'deepseek-reasoner', label: 'deepseek-reasoner (R1)' }\n ],\n openai: [\n { id: 'gpt-4.1', label: 'GPT-4.1' },\n { id: 'gpt-4.1-mini', label: 'GPT-4.1 mini' },\n { id: 'gpt-4o', label: 'GPT-4o' },\n { id: 'gpt-4o-mini', label: 'GPT-4o mini' }\n ],\n anthropic: [\n { id: 'claude-opus-4-5', label: 'Claude Opus 4.5' },\n { id: 'claude-sonnet-4-5', label: 'Claude Sonnet 4.5' },\n { id: 'claude-haiku-3-5', label: 'Claude Haiku 3.5' }\n ],\n google: [\n { id: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },\n { id: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },\n { id: 'gemini-2.0-flash', label: 'Gemini 2.0 Flash' }\n ],\n zhipu: [\n { id: 'glm-4-plus', label: 'GLM-4 Plus' },\n { id: 'glm-4-air', label: 'GLM-4 Air' },\n { id: 'glm-4-flash', label: 'GLM-4 Flash(免费)' }\n ],\n qwen: [\n { id: 'qwen-max', label: 'Qwen Max' },\n { id: 'qwen-plus', label: 'Qwen Plus' },\n { id: 'qwen-turbo', label: 'Qwen Turbo' }\n ],\n moonshot: [\n { id: 'moonshot-v1-128k', label: 'Kimi 128k' },\n { id: 'moonshot-v1-32k', label: 'Kimi 32k' },\n { id: 'moonshot-v1-8k', label: 'Kimi 8k' }\n ]\n};\n\nexport async function getModels(payload: { provider?: AiProvider | string; apiKey?: string } = {}): Promise<{ models: ModelOption[] }> {\n const provider = normalizeProvider(payload.provider);\n const fallback = provider ? FALLBACK_MODELS[provider] : [];\n const config = provider ? MODELS_CONFIG[provider] : undefined;\n const apiKey = payload.apiKey?.trim();\n if (!config || !apiKey) return { models: fallback };\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 8000);\n try {\n const response = await fetch(config.url(apiKey), {\n headers: { 'Content-Type': 'application/json', ...config.headers(apiKey) },\n signal: controller.signal\n });\n if (!response.ok) return { models: fallback };\n const data = await response.json();\n const models = config.parse(data);\n return { models: models.length > 0 ? models : fallback };\n } catch {\n return { models: fallback };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'remote-api' && settings.backendUrl) {\n return generateRemoteSummary(request, settings.backendUrl);\n }\n\n if (settings.backendType === 'mock') {\n return generateMockSummary(request);\n }\n\n const provider = normalizeProvider(settings.aiProvider) ?? (settings.backendType === 'deepseek' ? 'deepseek' : undefined);\n if (provider && provider !== 'mock' && PROVIDER_CONFIG[provider]) {\n return generateAiSummary(request, settings, provider);\n }\n\n return generateMockSummary(request);\n}\n\nasync function generateRemoteSummary(request: SummaryRequest, backendUrl: string): Promise<SummaryResponse> {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 28000);\n\n try {\n const response = await fetch(backendUrl, {\n method: 'POST',\n signal: controller.signal,\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(request)\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `代理服务请求失败:HTTP ${response.status}`\n };\n }\n\n return await response.json() as SummaryResponse;\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? '代理服务请求超时。'\n : '代理服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nasync function generateAiSummary(\n request: SummaryRequest,\n settings: ExtensionSettings,\n provider: Exclude<AiProvider, 'mock'>\n): Promise<SummaryResponse> {\n const config = PROVIDER_CONFIG[provider];\n const apiKey = settings.aiApiKey?.trim()\n || LOCAL_AI_API_KEY.trim()\n || (provider === 'deepseek' ? (settings.deepseekApiKey?.trim() || LOCAL_DEEPSEEK_API_KEY.trim()) : '');\n\n if (!apiKey || apiKey === 'sk-YOUR_KEY_HERE') {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `请先在扩展弹窗中填写 ${provider} 的 API Key。`\n };\n }\n\n const model = settings.aiModel || settings.deepseekModel || 'deepseek-chat';\n const systemContent = buildSystemContent();\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n let response: Response;\n\n if (config.style === 'anthropic') {\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n 'x-api-key': apiKey,\n 'anthropic-version': '2023-06-01'\n },\n body: JSON.stringify({\n model,\n max_tokens: getMaxTokens(request.summaryLength),\n system: systemContent,\n messages: [{ role: 'user', content: buildPrompt(request) }]\n })\n });\n } else {\n const body: UnknownRecord = {\n model,\n messages: [\n { role: 'system', content: systemContent },\n { role: 'user', content: buildPrompt(request) }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' }\n };\n if (provider === 'deepseek') body.thinking = { type: 'disabled' };\n\n response = await fetch(config.url, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify(body)\n });\n }\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = asRecord(await response.json());\n const text = config.style === 'anthropic'\n ? asRecord(asArray(data.content)[0]).text\n : asRecord(asRecord(asArray(data.choices)[0]).message).content;\n const card = parseReadingCard(text);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `${provider} 返回内容无法解析。`\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? `${provider} 请求超时。`\n : `${provider} 服务暂时不可用。`\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n function validateCard(parsed: unknown): ReturnType<typeof parseReadingCard> {\n const p = asRecord(parsed);\n if (typeof p.shortJudgment !== 'string' || typeof p.coreView !== 'string') return null;\n const aiTags = asArray(p.aiTags)\n .filter(isAllowedAiTag)\n .slice(0, 3);\n return {\n shortJudgment: p.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: p.coreView.trim(),\n aiTags\n };\n }\n\n const text = content.trim();\n\n try {\n const result = validateCard(JSON.parse(text));\n if (result) return result;\n } catch {}\n\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try {\n const result = validateCard(JSON.parse(fenceMatch[1].trim()));\n if (result) return result;\n } catch {}\n }\n\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try {\n const result = validateCard(JSON.parse(braceMatch[0]));\n if (result) return result;\n } catch {}\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < 80) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nfunction normalizeProvider(provider: unknown): AiProvider | undefined {\n return typeof provider === 'string' && provider in FALLBACK_MODELS ? provider as AiProvider : undefined;\n}\n\nfunction asRecord(value: unknown): UnknownRecord {\n return value && typeof value === 'object' ? value as UnknownRecord : {};\n}\n\nfunction asArray(value: unknown): unknown[] {\n return Array.isArray(value) ? value : [];\n}\n"}, "6a1692218588643b87670ef9d3edd2decdb6d137": {"t": 1785753887.5912662, "n": 8393, "h": "00e4ad7fe5f390ac", "c": "import type { ExtensionSettings, SummaryRequest, SummaryResponse } from '../shared/types';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'deepseek') {\n return generateDeepSeekSummary(request, settings);\n }\n\n return generateMockSummary(request);\n}\n\nasync function generateDeepSeekSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n const apiKey = settings.deepseekApiKey?.trim();\n if (!apiKey) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '请先在扩展弹窗中填写 DeepSeek API Key。'\n };\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: settings.deepseekModel,\n messages: [\n {\n role: 'system',\n content: [\n '你是「看山」知乎阅读决策助手。',\n '只分析用户提供的回答内容,不添加外部事实。',\n '使用简体中文,语气轻盈、准确、贴近知乎语境。',\n '你的任务是帮用户判断这条回答是否值得读、适合哪类读者,而不是写普通摘要。',\n '只返回 JSON,不要返回 Markdown 或任何额外解释。',\n '',\n '# AI 评价标签(aiTags)使用规则',\n '从下方 13 个固定标签中选 1-3 个,输出到 aiTags 数组,不得自造新标签:',\n '🧠 思辨文 — 有分析框架,拆概念/拆因果,能改变对问题的理解方式',\n '📚 干货文 — 信息密度高、资料多,适合收藏,可能观点不强',\n '🏗️ 工具文 — 清单/流程/工具/书单,可执行性强',\n '🎭 情绪文 — 情绪感染为主,事实较少,读完主要获得情绪共鸣',\n '观点鲜明 — 立场很强,有冲击力,但证据链未必完整',\n '💭 个人经历分享 — 以作者或身边人的亲身经历为核心,样本有限',\n '🧃 爽文 — 强烈站队,打脸叙事,读起来爽但未必严谨',\n '🧱 阵营文 — 先有立场再找材料,不承认复杂性,喜欢划分敌我',\n '🪝 钓鱼文 — 疑似故意用极端观点引战,论证薄但攻击性强',\n '🧨 串子文 — 疑似伪装立场、故意说夸张低级的话来反向抹黑某群体',\n '🤡 乐子文 — 主要价值是看热闹/荒诞/反差,娱乐性强',\n '🧪 伪科普 — 堆叠专业词汇但概念混乱,结论跳跃,制造虚假权威感',\n '🛒 软广带货文 — 前半段讲痛点后半段引出产品/课程/服务',\n '',\n '# 区分原则',\n '🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度',\n '📚 干货文 vs 🏗️ 工具文:前者重知识资料,后者重操作路径',\n '🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供爽感',\n '🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑'\n ].join('\\n')\n },\n {\n role: 'user',\n content: buildPrompt(request)\n }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' },\n thinking: { type: 'disabled' }\n })\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(content);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: 'DeepSeek 返回内容无法解析。'\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? 'DeepSeek 请求超时。'\n : 'DeepSeek 服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction buildPrompt(request: SummaryRequest): string {\n const a = request.answer;\n const lines: string[] = [];\n\n lines.push(`问题标题:${a.questionTitle || '未知'}`);\n\n if (a.questionDescription) {\n lines.push(`问题补充说明:${a.questionDescription}`);\n }\n if (a.questionTopics && a.questionTopics.length > 0) {\n lines.push(`问题话题标签:${a.questionTopics.join(' / ')}`);\n }\n if (a.authorName) {\n const authorLine = a.authorHeadline\n ? `${a.authorName}(${a.authorHeadline})`\n : a.authorName;\n lines.push(`回答作者:${authorLine}`);\n }\n if (a.upvoteCount) {\n lines.push(`回答赞同数:${a.upvoteCount}`);\n }\n if (a.commentCount) {\n lines.push(`回答评论数:${a.commentCount}`);\n }\n\n lines.push(\n '',\n '任务:为这条知乎回答生成“看山”阅读决策卡。',\n '',\n '# 字段说明',\n 'shortJudgment:10 个汉字以内的阅读建议,优先从以下短语中选择或仿写:',\n ' 值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看',\n 'coreView:只写一句高密度核心观点。要求:贴近作者结论而非泛泛摘要;不写“本文主要讲述了”;必须基于正文;不超过两行显示。',\n 'aiTags:从系统提示的 13 个固定标签中选 1-3 个,放入数组。根据内容主要价值和明显风险信号选择。',\n '',\n '# 校准示例',\n '示例 A(技术争论文,情绪强于解释)→ aiTags: [“🎭 情绪文”],不选 📚 干货文',\n '示例 B(段子/荒诞反差故事)→ aiTags: [“🤡 乐子文”],不选 📚 干货文',\n '示例 C(知识密集的背景介绍)→ aiTags: [“📚 干货文”]',\n '示例 D(作者亲历有趣经历)→ aiTags: [“💭 个人经历分享”, “🤡 乐子文”]',\n '示例 E(路径建议+立场鲜明+工具清单)→ aiTags: [“📚 干货文”, “观点鲜明”, “🏗️ 工具文”]',\n '',\n '返回格式必须是 JSON,示例:',\n '{“shortJudgment”:“干货很多”,“coreView”:“作者认为真正影响职业发展的是能否持续进入高质量的信息和机会网络。”,“aiTags”:[“📚 干货文”,“观点鲜明”]}',\n '',\n '回答正文:',\n a.contentText\n );\n\n return lines.join('\n');\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n '🧠 思辨文', '📚 干货文', '🏗️ 工具文', '🎭 情绪文', '观点鲜明',\n '💭 个人经历分享', '🧃 爽文', '🧱 阵营文', '🪝 钓鱼文', '🧨 串子文',\n '🤡 乐子文', '🧪 伪科普', '🛒 软广带货文'\n]);\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n function validateCard(parsed: unknown): ReturnType<typeof parseReadingCard> {\n if (typeof (parsed as any)?.shortJudgment !== 'string' || typeof (parsed as any)?.coreView !== 'string') return null;\n const p = parsed as any;\n const rawTags: unknown = p.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t): t is string => typeof t === 'string' && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: p.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: p.coreView.trim(),\n aiTags,\n };\n }\n\n const text = content.trim();\n\n // Try 1: direct parse\n try { const r = validateCard(JSON.parse(text)); if (r) return r; } catch {}\n\n // Try 2: strip markdown code fences\n const fenceMatch = text.match(/```(?:json)?\\s*\\n([\\s\\S]*?)\\n```/);\n if (fenceMatch) {\n try { const r = validateCard(JSON.parse(fenceMatch[1].trim())); if (r) return r; } catch {}\n }\n\n // Try 3: extract first {...} block (handles preamble text, thinking tokens, etc.)\n const braceMatch = text.match(/\\{[\\s\\S]*\\}/);\n if (braceMatch) {\n try { const r = validateCard(JSON.parse(braceMatch[0])); if (r) return r; } catch {}\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < 80) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"}, "279bed656f66e0a2cd7a8188ed9cba4ca27188f0": {"t": 1785753888.8876128, "n": 7756, "h": "2ef9a0644079b40a", "c": "import type { ExtensionSettings, SummaryRequest, SummaryResponse } from '../shared/types';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'deepseek') {\n return generateDeepSeekSummary(request, settings);\n }\n\n return generateMockSummary(request);\n}\n\nasync function generateDeepSeekSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n const apiKey = settings.deepseekApiKey?.trim();\n if (!apiKey) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '请先在扩展弹窗中填写 DeepSeek API Key。'\n };\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: settings.deepseekModel,\n messages: [\n {\n role: 'system',\n content: [\n '你是「看山」知乎阅读决策助手。',\n '只分析用户提供的回答内容,不添加外部事实。',\n '使用简体中文,语气轻盈、准确、贴近知乎语境。',\n '你的任务是帮用户判断这条回答是否值得读、适合哪类读者,而不是写普通摘要。',\n '只返回 JSON,不要返回 Markdown 或任何额外解释。',\n '',\n '# AI 评价标签(aiTags)使用规则',\n '从下方 13 个固定标签中选 1-3 个,输出到 aiTags 数组,不得自造新标签:',\n '🧠 思辨文 — 有分析框架,拆概念/拆因果,能改变对问题的理解方式',\n '📚 干货文 — 信息密度高、资料多,适合收藏,可能观点不强',\n '🏗️ 工具文 — 清单/流程/工具/书单,可执行性强',\n '🎭 情绪文 — 情绪感染为主,事实较少,读完主要获得情绪共鸣',\n '观点鲜明 — 立场很强,有冲击力,但证据链未必完整',\n '💭 个人经历分享 — 以作者或身边人的亲身经历为核心,样本有限',\n '🧃 爽文 — 强烈站队,打脸叙事,读起来爽但未必严谨',\n '🧱 阵营文 — 先有立场再找材料,不承认复杂性,喜欢划分敌我',\n '🪝 钓鱼文 — 疑似故意用极端观点引战,论证薄但攻击性强',\n '🧨 串子文 — 疑似伪装立场、故意说夸张低级的话来反向抹黑某群体',\n '🤡 乐子文 — 主要价值是看热闹/荒诞/反差,娱乐性强',\n '🧪 伪科普 — 堆叠专业词汇但概念混乱,结论跳跃,制造虚假权威感',\n '🛒 软广带货文 — 前半段讲痛点后半段引出产品/课程/服务',\n '',\n '# 区分原则',\n '🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度',\n '📚 干货文 vs 🏗️ 工具文:前者重知识资料,后者重操作路径',\n '🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供爽感',\n '🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑'\n ].join('\\n')\n },\n {\n role: 'user',\n content: buildPrompt(request)\n }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' },\n thinking: { type: 'disabled' }\n })\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(content);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: 'DeepSeek 返回内容无法解析。'\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? 'DeepSeek 请求超时。'\n : 'DeepSeek 服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction buildPrompt(request: SummaryRequest): string {\n const a = request.answer;\n const lines: string[] = [];\n\n lines.push(`问题标题:${a.questionTitle || '未知'}`);\n\n if (a.questionDescription) {\n lines.push(`问题补充说明:${a.questionDescription}`);\n }\n if (a.questionTopics && a.questionTopics.length > 0) {\n lines.push(`问题话题标签:${a.questionTopics.join(' / ')}`);\n }\n if (a.authorName) {\n const authorLine = a.authorHeadline\n ? `${a.authorName}(${a.authorHeadline})`\n : a.authorName;\n lines.push(`回答作者:${authorLine}`);\n }\n if (a.upvoteCount) {\n lines.push(`回答赞同数:${a.upvoteCount}`);\n }\n if (a.commentCount) {\n lines.push(`回答评论数:${a.commentCount}`);\n }\n\n lines.push(\n '',\n '任务:为这条知乎回答生成“看山”阅读决策卡。',\n '',\n '# 字段说明',\n 'shortJudgment:10 个汉字以内的阅读建议,优先从以下短语中选择或仿写:',\n ' 值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看',\n 'coreView:只写一句高密度核心观点。要求:贴近作者结论而非泛泛摘要;不写“本文主要讲述了”;必须基于正文;不超过两行显示。',\n 'aiTags:从系统提示的 13 个固定标签中选 1-3 个,放入数组。根据内容主要价值和明显风险信号选择。',\n '',\n '# 校准示例',\n '示例 A(技术争论文,情绪强于解释)→ aiTags: [“🎭 情绪文”],不选 📚 干货文',\n '示例 B(段子/荒诞反差故事)→ aiTags: [“🤡 乐子文”],不选 📚 干货文',\n '示例 C(知识密集的背景介绍)→ aiTags: [“📚 干货文”]',\n '示例 D(作者亲历有趣经历)→ aiTags: [“💭 个人经历分享”, “🤡 乐子文”]',\n '示例 E(路径建议+立场鲜明+工具清单)→ aiTags: [“📚 干货文”, “观点鲜明”, “🏗️ 工具文”]',\n '',\n '返回格式必须是 JSON,示例:',\n '{“shortJudgment”:“干货很多”,“coreView”:“作者认为真正影响职业发展的是能否持续进入高质量的信息和机会网络。”,“aiTags”:[“📚 干货文”,“观点鲜明”]}',\n '',\n '回答正文:',\n a.contentText\n );\n\n return lines.join('\n');\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n '🧠 思辨文', '📚 干货文', '🏗️ 工具文', '🎭 情绪文', '观点鲜明',\n '💭 个人经历分享', '🧃 爽文', '🧱 阵营文', '🪝 钓鱼文', '🧨 串子文',\n '🤡 乐子文', '🧪 伪科普', '🛒 软广带货文'\n]);\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n try {\n const parsed = JSON.parse(content);\n if (typeof parsed.shortJudgment === 'string' && typeof parsed.coreView === 'string') {\n const rawTags: unknown = parsed.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t): t is string => typeof t === 'string' && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: parsed.coreView.trim(),\n aiTags\n };\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < 80) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"}, "e19092337b318f90133e9637c0b6dd9f1101106b": {"t": 1785753890.3925211, "n": 7215, "h": "ce924302f35200b4", "c": "import type { ExtensionSettings, SummaryRequest, SummaryResponse } from '../shared/types';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'deepseek') {\n return generateDeepSeekSummary(request, settings);\n }\n\n return generateMockSummary(request);\n}\n\nasync function generateDeepSeekSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n const apiKey = settings.deepseekApiKey?.trim();\n if (!apiKey) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '请先在扩展弹窗中填写 DeepSeek API Key。'\n };\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: settings.deepseekModel,\n messages: [\n {\n role: 'system',\n content: [\n '你是「看山」知乎阅读决策助手。',\n '只分析用户提供的回答内容,不添加外部事实。',\n '使用简体中文,语气轻盈、准确、贴近知乎语境。',\n '你的任务是帮用户判断这条回答是否值得读、适合哪类读者,而不是写普通摘要。',\n '只返回 JSON,不要返回 Markdown 或任何额外解释。',\n '',\n '# AI 评价标签(aiTags)使用规则',\n '从下方 13 个固定标签中选 1-3 个,输出到 aiTags 数组,不得自造新标签:',\n '🧠 思辨文 — 有分析框架,拆概念/拆因果,能改变对问题的理解方式',\n '📚 干货文 — 信息密度高、资料多,适合收藏,可能观点不强',\n '🏗️ 工具文 — 清单/流程/工具/书单,可执行性强',\n '🎭 情绪文 — 情绪感染为主,事实较少,读完主要获得情绪共鸣',\n '观点鲜明 — 立场很强,有冲击力,但证据链未必完整',\n '💭 个人经历分享 — 以作者或身边人的亲身经历为核心,样本有限',\n '🧃 爽文 — 强烈站队,打脸叙事,读起来爽但未必严谨',\n '🧱 阵营文 — 先有立场再找材料,不承认复杂性,喜欢划分敌我',\n '🪝 钓鱼文 — 疑似故意用极端观点引战,论证薄但攻击性强',\n '🧨 串子文 — 疑似伪装立场、故意说夸张低级的话来反向抹黑某群体',\n '🤡 乐子文 — 主要价值是看热闹/荒诞/反差,娱乐性强',\n '🧪 伪科普 — 堆叠专业词汇但概念混乱,结论跳跃,制造虚假权威感',\n '🛒 软广带货文 — 前半段讲痛点后半段引出产品/课程/服务',\n '',\n '# 区分原则',\n '🧠 思辨文 vs 📚 干货文:前者重分析框架,后者重材料密度',\n '📚 干货文 vs 🏗️ 工具文:前者重知识资料,后者重操作路径',\n '🎭 情绪文 vs 🧃 爽文:前者重情绪表达,后者重给特定立场提供爽感',\n '🪝 钓鱼文 vs 🧨 串子文:前者重引战,后者重伪装立场反向抹黑'\n ].join('\\n')\n },\n {\n role: 'user',\n content: buildPrompt(request)\n }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' },\n thinking: { type: 'disabled' }\n })\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(content);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: 'DeepSeek 返回内容无法解析。'\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView,\n aiTags: card.aiTags\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? 'DeepSeek 请求超时。'\n : 'DeepSeek 服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction buildPrompt(request: SummaryRequest): string {\n return [\n `问题标题:${request.answer.questionTitle || '未知'}`,\n `回答作者:${request.answer.authorName || '未知'}`,\n '任务:为这条知乎回答生成“看山”阅读决策卡。',\n '',\n '# 字段说明',\n 'shortJudgment:10 个汉字以内的阅读建议,优先从以下短语中选择或仿写:',\n ' 值得一读 / 干货很多 / 适合收藏 / 观点挺有意思 / 情绪输出偏多 / 评论区可能更精彩 / 可以当个乐子看',\n 'coreView:只写一句高密度核心观点。要求:贴近作者结论而非泛泛摘要;不写\"本文主要讲述了\";必须基于正文;不超过两行显示。',\n 'aiTags:从系统提示的 13 个固定标签中选 1-3 个,放入数组。根据内容主要价值和明显风险信号选择。',\n '',\n '# 校准示例',\n '示例 A(技术争论文,情绪强于解释)→ aiTags: [\"🎭 情绪文\"],不选 📚 干货文',\n '示例 B(段子/荒诞反差故事)→ aiTags: [\"🤡 乐子文\"],不选 📚 干货文',\n '示例 C(知识密集的背景介绍)→ aiTags: [\"📚 干货文\"]',\n '示例 D(作者亲历有趣经历)→ aiTags: [\"💭 个人经历分享\", \"🤡 乐子文\"]',\n '示例 E(路径建议+立场鲜明+工具清单)→ aiTags: [\"📚 干货文\", \"观点鲜明\", \"🏗️ 工具文\"]',\n '',\n '返回格式必须是 JSON,示例:',\n '{\"shortJudgment\":\"干货很多\",\"coreView\":\"作者认为真正影响职业发展的是能否持续进入高质量的信息和机会网络。\",\"aiTags\":[\"📚 干货文\",\"观点鲜明\"]}',\n '',\n '回答正文:',\n request.answer.contentText\n ].join('\\n');\n}\n\nconst ALLOWED_AI_TAGS = new Set([\n '🧠 思辨文', '📚 干货文', '🏗️ 工具文', '🎭 情绪文', '观点鲜明',\n '💭 个人经历分享', '🧃 爽文', '🧱 阵营文', '🪝 钓鱼文', '🧨 串子文',\n '🤡 乐子文', '🧪 伪科普', '🛒 软广带货文'\n]);\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string; aiTags: string[] } | null {\n if (typeof content !== 'string') return null;\n\n try {\n const parsed = JSON.parse(content);\n if (typeof parsed.shortJudgment === 'string' && typeof parsed.coreView === 'string') {\n const rawTags: unknown = parsed.aiTags;\n const aiTags = Array.isArray(rawTags)\n ? rawTags.filter((t): t is string => typeof t === 'string' && ALLOWED_AI_TAGS.has(t)).slice(0, 3)\n : [];\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: parsed.coreView.trim(),\n aiTags\n };\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < 80) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"}, "832aa7f78fede99f98b4c4f988f206069fb6bbff": {"t": 1785753891.6788661, "n": 5315, "h": "7f925217bdbba8aa", "c": "import type { ExtensionSettings, SummaryRequest, SummaryResponse } from '../shared/types';\n\nconst DEEPSEEK_API_URL = 'https://api.deepseek.com/chat/completions';\n\nexport async function generateSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n if (settings.backendType === 'deepseek') {\n return generateDeepSeekSummary(request, settings);\n }\n\n return generateMockSummary(request);\n}\n\nasync function generateDeepSeekSummary(request: SummaryRequest, settings: ExtensionSettings): Promise<SummaryResponse> {\n const apiKey = settings.deepseekApiKey?.trim();\n if (!apiKey) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '请先在扩展弹窗中填写 DeepSeek API Key。'\n };\n }\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), 25000);\n\n try {\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n signal: controller.signal,\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${apiKey}`\n },\n body: JSON.stringify({\n model: settings.deepseekModel,\n messages: [\n {\n role: 'system',\n content: [\n '你是知乎回答总结助手。',\n '只总结用户提供的回答内容,不添加外部事实。',\n '使用简体中文,语气中立、准确、简洁。',\n '你要帮助用户判断这条知乎回答是否值得读,而不是写普通摘要。',\n '只返回 JSON,不要返回 Markdown。'\n ].join('\\n')\n },\n {\n role: 'user',\n content: buildPrompt(request)\n }\n ],\n temperature: 0.2,\n max_tokens: getMaxTokens(request.summaryLength),\n response_format: { type: 'json_object' },\n thinking: { type: 'disabled' }\n })\n });\n\n if (!response.ok) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: `DeepSeek 请求失败:HTTP ${response.status}`\n };\n }\n\n const data = await response.json();\n const content = data?.choices?.[0]?.message?.content;\n const card = parseReadingCard(content);\n\n if (!card) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: 'DeepSeek 返回内容无法解析。'\n };\n }\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: card.shortJudgment,\n coreView: card.coreView\n };\n } catch (error) {\n return {\n status: 'error',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: error instanceof DOMException && error.name === 'AbortError'\n ? 'DeepSeek 请求超时。'\n : 'DeepSeek 服务暂时不可用。'\n };\n } finally {\n clearTimeout(timeoutId);\n }\n}\n\nfunction buildPrompt(request: SummaryRequest): string {\n return [\n `问题标题:${request.answer.questionTitle || '未知'}`,\n `回答作者:${request.answer.authorName || '未知'}`,\n '任务:为这条知乎回答生成“看山”阅读决策卡。',\n 'shortJudgment:非常短,最好 10 个汉字以内,像“值得一读”“干货很多”“偏个人经验”“情绪输出偏多”。',\n 'coreView:只写一句高密度核心观点,最多两行显示;像文章结论,不要写泛泛摘要;必须基于回答正文。',\n '返回格式必须是 JSON:{\"shortJudgment\":\"值得一读\",\"coreView\":\"作者认为,真正影响职业发展的不是努力本身,而是能否持续进入高质量的信息和机会网络。\"}',\n '',\n '回答正文:',\n request.answer.contentText\n ].join('\\n');\n}\n\nfunction parseReadingCard(content: unknown): { shortJudgment: string; coreView: string } | null {\n if (typeof content !== 'string') return null;\n\n try {\n const parsed = JSON.parse(content);\n if (typeof parsed.shortJudgment === 'string' && typeof parsed.coreView === 'string') {\n return {\n shortJudgment: parsed.shortJudgment.trim().replace(/[。!?!?\\s]/g, '').slice(0, 10),\n coreView: parsed.coreView.trim()\n };\n }\n } catch {\n return null;\n }\n\n return null;\n}\n\nasync function generateMockSummary(request: SummaryRequest): Promise<SummaryResponse> {\n await delay(450);\n\n const text = request.answer.contentText.trim();\n if (text.length < 80) {\n return {\n status: 'skipped',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n errorMessage: '该回答内容较短,未生成总结。'\n };\n }\n\n const sentences = splitIntoSentences(text);\n const coreView = sentences[0]?.replace(/^[-•\\d.、\\s]+/, '').slice(0, 90) || '这条回答主要围绕原文内容展开。';\n\n return {\n status: 'success',\n answerId: request.answer.answerId,\n contentHash: request.answer.contentHash,\n shortJudgment: pickMockJudgment(text),\n coreView\n };\n}\n\nfunction pickMockJudgment(text: string): string {\n if (text.length > 3000) return '信息密度挺高';\n if (/经历|我|当时|后来|朋友|公司/.test(text)) return '偏个人经验';\n if (/建议|方法|步骤|核心|关键/.test(text)) return '干货很多';\n return '值得看看';\n}\n\nfunction getMaxTokens(summaryLength: SummaryRequest['summaryLength']): number {\n if (summaryLength === 'short') return 160;\n if (summaryLength === 'long') return 520;\n return 320;\n}\n\nfunction splitIntoSentences(text: string): string[] {\n return text\n .replace(/\\s+/g, ' ')\n .split(/(?<=[。!?!?;;])\\s*/)\n .map((item) => item.trim())\n .filter(Boolean);\n}\n\nfunction delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"}}}} |