@skrillex1224/chrome-article-publish-extension 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -223,4 +223,4 @@ The script sends `GET_PLATFORMS` and `CHECK_AUTH` through `chrome.runtime.sendMe
223
223
  - No retry scheduler.
224
224
  - No cross-profile cookie copying.
225
225
  - No platform button-click publishing.
226
- - No compatibility with Wechatsync's old public result shape.
226
+ - No compatibility promises for legacy result shapes from earlier internal tools.
@@ -12,15 +12,15 @@ import {
12
12
  TencentAdapter,
13
13
  ToutiaoAdapter,
14
14
  WeixinAdapter
15
- } from "../chunk-QM2T4VGM.js";
15
+ } from "../chunk-NKSXZF7J.js";
16
16
  import {
17
17
  markdownToDraft
18
- } from "../chunk-IUAZAMKF.js";
18
+ } from "../chunk-GATX26VC.js";
19
19
  import {
20
20
  createLogger,
21
21
  htmlToMarkdown,
22
22
  parseMarkdownImages
23
- } from "../chunk-7Q3KKWUB.js";
23
+ } from "../chunk-JFXS3MPU.js";
24
24
 
25
25
  // src/adapters/types.ts
26
26
  var DEFAULT_PREPROCESS_CONFIG = {
@@ -120,4 +120,4 @@ function markdownToDraft(markdown, imageDataMap = /* @__PURE__ */ new Map()) {
120
120
  export {
121
121
  markdownToDraft
122
122
  };
123
- //# sourceMappingURL=chunk-IUAZAMKF.js.map
123
+ //# sourceMappingURL=chunk-GATX26VC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/markdown-to-draft.ts"],"sourcesContent":["/**\n * Markdown to Draft.js 转换\n * 用于豆瓣等使用 Draft.js 编辑器的平台\n */\n\n// @ts-ignore - markdown-draft-js 没有类型定义\nimport { markdownToDraft as mdToDraft } from 'markdown-draft-js'\nimport { Remarkable } from 'remarkable'\n\n/**\n * 图片数据接口 - 豆瓣需要完整的图片信息\n */\nexport interface DoubanImageData {\n id: string\n url: string\n thumb: string\n width?: number\n height?: number\n file_name?: string\n file_size?: number\n}\n\n/**\n * 豆瓣图片 Block 解析器 - 处理 ![]() 格式的图片\n */\nconst ImageRegexp = /^!\\[([^\\]]*)]\\s*\\(([^)\"]+)( \"([^)\"]+)\")?\\)/\n\nconst imageBlockPlugin = (remarkable: Remarkable) => {\n // @ts-ignore - remarkable types incomplete\n remarkable.block.ruler.before('paragraph', 'image', (state: any, startLine: number, endLine: number, silent: boolean) => {\n const pos = state.bMarks[startLine] + state.tShift[startLine]\n const max = state.eMarks[startLine]\n\n if (pos >= max) return false\n if (!state.src) return false\n if (state.src[pos] !== '!') return false\n\n const match = ImageRegexp.exec(state.src.slice(pos))\n if (!match) return false\n\n if (!silent) {\n state.tokens.push({\n type: 'image_open',\n src: match[2],\n alt: match[1],\n lines: [startLine, state.line],\n level: state.level\n })\n state.tokens.push({\n type: 'image_close',\n level: state.level\n })\n }\n\n state.line = startLine + 1\n return true\n })\n}\n\n/**\n * 将 Markdown 转换为 Draft.js 格式 (豆瓣专用)\n * @param markdown Markdown 内容\n * @param imageDataMap 图片 URL 到完整数据的映射\n * @returns Draft.js JSON 字符串\n */\nexport function markdownToDraft(markdown: string, imageDataMap: Map<string, DoubanImageData> = new Map()): string {\n // 保证图片换行\n const processedMarkdown = markdown.split('\\n').map(line => {\n const imageBlocks = line.split('![]')\n return imageBlocks.length > 1 ? imageBlocks.join('\\n![]') : line\n }).join('\\n')\n\n let keyCounter = 0\n const generateUniqueKey = () => keyCounter++\n\n const draftState = mdToDraft(processedMarkdown, {\n remarkablePlugins: [imageBlockPlugin],\n blockTypes: {\n image_open: function (item: any) {\n const key = generateUniqueKey()\n const blockEntities: Record<number, any> = {}\n\n // 解析 ?# 格式获取原始 URL 和 ID\n const sourcePair = item.src ? item.src.split('?#') : ['', '']\n const rawSrc = sourcePair[0]\n const sourceId = sourcePair[1] || ''\n\n // 从 imageDataMap 获取完整图片数据\n const imgData = imageDataMap.get(item.src) || imageDataMap.get(rawSrc)\n\n const imageTemplate = imgData ? {\n id: imgData.id,\n src: imgData.url,\n thumb: imgData.thumb,\n url: imgData.url,\n width: imgData.width,\n height: imgData.height,\n file_name: imgData.file_name,\n file_size: imgData.file_size,\n } : {\n id: sourceId,\n src: rawSrc,\n thumb: rawSrc,\n url: rawSrc,\n }\n\n blockEntities[key] = {\n type: 'IMAGE',\n mutability: 'IMMUTABLE',\n data: imageTemplate,\n }\n\n return {\n type: 'atomic',\n blockEntities: blockEntities,\n inlineStyleRanges: [],\n entityRanges: [{ offset: 0, length: 1, key: key }],\n text: ' ',\n }\n }\n },\n blockEntities: {\n image: function (item: any) {\n const sourcePair = item.src ? item.src.split('?#') : ['', '']\n const rawSrc = sourcePair[0]\n const sourceId = sourcePair[1] || ''\n\n // 从 imageDataMap 获取完整图片数据\n const imgData = imageDataMap.get(item.src) || imageDataMap.get(rawSrc)\n\n if (imgData) {\n return {\n type: 'IMAGE',\n mutability: 'IMMUTABLE',\n data: {\n id: imgData.id,\n src: imgData.url,\n thumb: imgData.thumb,\n url: imgData.url,\n width: imgData.width,\n height: imgData.height,\n }\n }\n }\n\n return {\n type: 'IMAGE',\n mutability: 'IMMUTABLE',\n data: {\n id: sourceId,\n src: rawSrc,\n thumb: rawSrc,\n url: rawSrc,\n }\n }\n }\n }\n })\n\n // 将 block.blockEntities 合并到顶层 entityMap (参考 mtd.js)\n if (draftState.blocks) {\n for (const block of draftState.blocks) {\n if (block.blockEntities) {\n Object.assign(draftState.entityMap, block.blockEntities)\n delete block.blockEntities\n }\n }\n }\n\n return JSON.stringify(draftState)\n}\n"],"mappings":"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","names":[]}
@@ -599,4 +599,4 @@ export {
599
599
  signAWS4,
600
600
  crc32
601
601
  };
602
- //# sourceMappingURL=chunk-7Q3KKWUB.js.map
602
+ //# sourceMappingURL=chunk-JFXS3MPU.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lib/logger.ts","../src/lib/markdown-images.ts","../src/lib/turndown.ts","../src/lib/aws4.ts"],"sourcesContent":["/**\n * 通用日志接口 - 不依赖浏览器 API\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error'\n\nexport interface Logger {\n debug: (...args: unknown[]) => void\n info: (...args: unknown[]) => void\n warn: (...args: unknown[]) => void\n error: (...args: unknown[]) => void\n}\n\nexport interface LoggerConfig {\n enabled: boolean\n level: LogLevel\n}\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n}\n\n// 检测生产环境 (支持 Vite 和 Node.js)\nconst isProd = typeof import.meta !== 'undefined' && (import.meta as unknown as { env?: { PROD?: boolean } }).env?.PROD\n || (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production')\n\n// 默认配置:生产环境只输出 warn 和 error\nlet globalConfig: LoggerConfig = {\n enabled: true,\n level: isProd ? 'warn' : 'debug',\n}\n\n/**\n * 设置全局日志配置\n */\nexport function setLoggerConfig(config: Partial<LoggerConfig>) {\n globalConfig = { ...globalConfig, ...config }\n}\n\n/**\n * 获取当前配置\n */\nexport function getLoggerConfig(): LoggerConfig {\n return { ...globalConfig }\n}\n\n/**\n * 创建带前缀的 logger\n */\nexport function createLogger(prefix: string): Logger {\n const shouldLog = (level: LogLevel): boolean => {\n if (!globalConfig.enabled) return false\n return LOG_LEVELS[level] >= LOG_LEVELS[globalConfig.level]\n }\n\n return {\n debug: (...args: unknown[]) => {\n if (shouldLog('debug')) {\n console.log(`[${prefix}]`, ...args)\n }\n },\n info: (...args: unknown[]) => {\n if (shouldLog('info')) {\n console.log(`[${prefix}]`, ...args)\n }\n },\n warn: (...args: unknown[]) => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}]`, ...args)\n }\n },\n error: (...args: unknown[]) => {\n if (shouldLog('error')) {\n console.error(`[${prefix}]`, ...args)\n }\n },\n }\n}\n","export interface MarkdownImageMatch {\n full: string\n alt: string\n src: string\n}\n\n/**\n * 解析 Markdown 图片语法,支持可选标题和括号 URL\n */\nexport function parseMarkdownImages(markdown: string): MarkdownImageMatch[] {\n const results: MarkdownImageMatch[] = []\n const len = markdown.length\n let i = 0\n\n const findClosingBracket = (start: number): number => {\n for (let j = start; j < len; j++) {\n const ch = markdown[j]\n if (ch === '\\\\') {\n j++\n continue\n }\n if (ch === ']') return j\n }\n return -1\n }\n\n while (i < len) {\n const start = markdown.indexOf('![', i)\n if (start === -1) break\n\n const altStart = start + 2\n const altEnd = findClosingBracket(altStart)\n if (altEnd === -1 || markdown[altEnd + 1] !== '(') {\n i = altStart\n continue\n }\n\n let k = altEnd + 2\n while (k < len && /\\s/.test(markdown[k])) k++\n\n let url = ''\n if (markdown[k] === '<') {\n const close = markdown.indexOf('>', k + 1)\n if (close === -1) {\n i = altEnd + 1\n continue\n }\n url = markdown.slice(k + 1, close)\n k = close + 1\n } else {\n const urlStart = k\n let depth = 0\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === '(') {\n depth++\n } else if (ch === ')') {\n if (depth === 0) break\n depth--\n } else if (/\\s/.test(ch) && depth === 0) {\n break\n }\n k++\n }\n url = markdown.slice(urlStart, k)\n }\n\n if (!url) {\n i = altEnd + 1\n continue\n }\n\n while (k < len && /\\s/.test(markdown[k])) k++\n\n if (k < len && (markdown[k] === '\"' || markdown[k] === '\\'')) {\n const quote = markdown[k]\n k++\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === quote) {\n k++\n break\n }\n k++\n }\n while (k < len && /\\s/.test(markdown[k])) k++\n } else if (k < len && markdown[k] === '(') {\n k++\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === ')') {\n k++\n break\n }\n k++\n }\n while (k < len && /\\s/.test(markdown[k])) k++\n }\n\n if (markdown[k] !== ')') {\n i = altEnd + 1\n continue\n }\n\n const full = markdown.slice(start, k + 1)\n const alt = markdown.slice(altStart, altEnd)\n results.push({ full, alt, src: url })\n i = k + 1\n }\n\n return results\n}\n","/**\n * Turndown HTML→Markdown 转换工具\n *\n * 基于 turndown 库,添加表格和代码块扩展规则\n * 移植自旧版 @wechatsync/drivers/tools/turnDownExtend.js\n *\n * 架构说明:\n * - htmlToMarkdownNative: 使用原生 DOM,适用于 Content Script(推荐)\n * - htmlToMarkdown: 使用正则转换,适用于 Service Worker(回退方案)\n */\n\nimport TurndownService from 'turndown'\nimport { createLogger } from './logger'\n\nconst logger = createLogger('Turndown')\n\n// ============ 源平台链接清理规则 ============\n\n/** 需要去除的站内链接域名(去掉 <a> 只保留文本) */\nexport const SOURCE_LINK_REMOVE_DOMAINS = [\n 'zhida.zhihu.com',\n]\n\n/** 跳转中转规则:domain → 真实 URL 所在的 query 参数名 */\nexport const SOURCE_LINK_REDIRECT_RULES: Array<{ domain: string; param: string }> = [\n { domain: 'link.zhihu.com', param: 'target' },\n]\n\n// ============ HTML 实体解码工具 ============\n\n/**\n * 完整的 HTML 实体解码(用于代码块提取)\n * 处理:命名实体、十进制实体、十六进制实体、双重编码\n */\nfunction decodeHtmlEntities(text: string): string {\n let result = text\n\n // 1. 先处理双重编码(如 &amp;lt; → &lt;)\n // 最多处理 3 层嵌套\n for (let i = 0; i < 3; i++) {\n const prev = result\n result = result.replace(/&amp;/g, '&')\n if (prev === result) break\n }\n\n // 2. 解码十六进制实体 &#xNN; 或 &#XNN;\n result = result.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {\n return String.fromCharCode(parseInt(hex, 16))\n })\n\n // 3. 解码十进制实体 &#NN;\n result = result.replace(/&#(\\d+);/g, (_, dec) => {\n return String.fromCharCode(parseInt(dec, 10))\n })\n\n // 4. 解码常用命名实体\n const namedEntities: Record<string, string> = {\n '&lt;': '<',\n '&gt;': '>',\n '&amp;': '&',\n '&quot;': '\"',\n '&apos;': \"'\",\n '&#039;': \"'\",\n '&nbsp;': ' ',\n '&ndash;': '\\u2013',\n '&mdash;': '\\u2014',\n '&lsquo;': '\\u2018',\n '&rsquo;': '\\u2019',\n '&ldquo;': '\\u201C',\n '&rdquo;': '\\u201D',\n '&copy;': '\\u00A9',\n '&reg;': '\\u00AE',\n '&trade;': '\\u2122',\n '&hellip;': '\\u2026',\n }\n\n for (const [entity, char] of Object.entries(namedEntities)) {\n result = result.split(entity).join(char)\n }\n\n return result\n}\n\n/**\n * 已知的 HTML 标签白名单(可能出现在代码块中的格式化标签)\n * 只移除这些标签,避免误删代码中的泛型语法如 List<String>\n */\nconst KNOWN_HTML_TAGS = [\n // 文本格式化\n 'span', 'em', 'strong', 'b', 'i', 'u', 's', 'strike', 'del', 'ins', 'mark',\n 'sub', 'sup', 'small', 'big', 'font', 'a',\n // 代码相关\n 'code', 'pre', 'kbd', 'samp', 'var', 'tt',\n // 块级元素\n 'div', 'p', 'section', 'article', 'header', 'footer', 'aside', 'nav',\n 'main', 'figure', 'figcaption', 'blockquote', 'address',\n // 列表\n 'ul', 'ol', 'li', 'dl', 'dt', 'dd',\n // 表格\n 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',\n // 换行/分隔\n 'br', 'hr', 'wbr',\n // 其他\n 'abbr', 'acronym', 'cite', 'dfn', 'q', 'time', 'ruby', 'rt', 'rp',\n 'bdi', 'bdo', 'data', 'meter', 'progress', 'output', 'details', 'summary',\n // 微信特殊标签\n 'mpvoice', 'mpprofile', 'qqmusic', 'mpcps',\n]\n\n/**\n * 从 HTML 中安全提取代码文本\n *\n * 策略:\n * 1. 先用占位符保护 HTML 实体编码的 < > (如 &lt; &gt;)\n * 2. 只移除已知的 HTML 标签(白名单),保留代码中的泛型语法\n * 3. 恢复占位符并解码所有 HTML 实体\n */\nfunction extractCodeText(html: string): string {\n const LT_PLACEHOLDER = '\\x00__CODE_LT__\\x00'\n const GT_PLACEHOLDER = '\\x00__CODE_GT__\\x00'\n\n let text = html\n\n // 1. 保护所有表示 < > 的 HTML 实体(它们是代码内容,不是标签)\n // 命名实体\n text = text.replace(/&lt;/gi, LT_PLACEHOLDER)\n text = text.replace(/&gt;/gi, GT_PLACEHOLDER)\n // 十进制实体 &#60; &#62;\n text = text.replace(/&#0*60;/gi, LT_PLACEHOLDER)\n text = text.replace(/&#0*62;/gi, GT_PLACEHOLDER)\n // 十六进制实体 &#x3C; &#x3E;\n text = text.replace(/&#x0*3[cC];/gi, LT_PLACEHOLDER)\n text = text.replace(/&#x0*3[eE];/gi, GT_PLACEHOLDER)\n\n // 2. 只移除已知的 HTML 标签(白名单方式)\n // 这样可以保留代码中的泛型语法如 List<String>, Map<K, V> 等\n const tagPattern = KNOWN_HTML_TAGS.join('|')\n // 匹配开标签: <tagname ...> 或自闭合 <tagname ... />\n text = text.replace(new RegExp(`<(${tagPattern})\\\\b[^>]*\\\\/?>`, 'gi'), '')\n // 匹配闭标签: </tagname>\n text = text.replace(new RegExp(`<\\\\/(${tagPattern})>`, 'gi'), '')\n\n // 3. 恢复占位符为实际的 < > 字符\n text = text.replace(new RegExp(LT_PLACEHOLDER.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), '<')\n text = text.replace(new RegExp(GT_PLACEHOLDER.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), '>')\n\n // 4. 解码剩余的 HTML 实体\n text = decodeHtmlEntities(text)\n\n return text\n}\n\n/**\n * 修复代码块中未转义的 < 字符\n * 在 DOM 解析之前调用,防止浏览器将 < 误认为标签开始而截断内容\n *\n * 策略:在 <pre> 和 <code> 标签内,将看起来不像 HTML 标签的 < 转义为 &lt;\n */\nexport function fixUnescapedLtInCode(html: string): string {\n // 处理 <pre>...</pre> 块\n let result = html.replace(/<pre([^>]*)>([\\s\\S]*?)<\\/pre>/gi, (_match, attrs, content) => {\n const fixedContent = escapeNonTagLt(content)\n return `<pre${attrs}>${fixedContent}</pre>`\n })\n\n // 处理独立的 <code>...</code> 块(不在 pre 内的)\n result = result.replace(/<code([^>]*)>([\\s\\S]*?)<\\/code>/gi, (_match, attrs, content) => {\n const fixedContent = escapeNonTagLt(content)\n return `<code${attrs}>${fixedContent}</code>`\n })\n\n return result\n}\n\n/**\n * 转义不是 HTML 标签的 < 字符\n * HTML 标签的特征:< 后紧跟字母或 /\n */\nfunction escapeNonTagLt(content: string): string {\n // 匹配 < 后面不是字母、/ 或 ! 的情况(不是有效标签开始)\n // 例如:< 5, < =, <=, < b (空格后字母)\n return content.replace(/<(?![a-zA-Z\\/!])/g, '&lt;')\n}\n\n/**\n * 从 HTML 代码块中提取纯文本(更安全的方式)\n * 先处理换行标签,再提取文本\n * @exported 供其他模块使用\n */\nexport function extractCodeFromHtml(html: string): string {\n let text = html\n // 保留换行标签的换行效果\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n')\n .replace(/<\\/li>/gi, '\\n')\n // 移除 code 标签但保留内容\n .replace(/<\\/?code[^>]*>/gi, '')\n\n // 提取代码文本(处理实体和标签)\n text = extractCodeText(text)\n\n return text.trim()\n}\n\n/**\n * 转换选项\n */\nexport interface TurndownOptions {\n /** 标题样式: setext (===) 或 atx (#) */\n headingStyle?: 'setext' | 'atx'\n /** 水平线样式 */\n hr?: string\n /** 粗体分隔符 */\n bulletListMarker?: '-' | '+' | '*'\n /** 代码块样式 */\n codeBlockStyle?: 'indented' | 'fenced'\n /** 代码块围栏符号 */\n fence?: '```' | '~~~'\n /** 强调分隔符 */\n emDelimiter?: '_' | '*'\n /** 粗体分隔符 */\n strongDelimiter?: '__' | '**'\n /** 链接样式 */\n linkStyle?: 'inlined' | 'referenced'\n /** 链接引用样式 */\n linkReferenceStyle?: 'full' | 'collapsed' | 'shortcut'\n}\n\n/**\n * 规范化单元格内容\n * markdown 表格要求每行单元格在同一行,且 `|` 需转义\n * - 去首尾空白\n * - 转义管道符\n * - 换行 → <br>(保留视觉换行;CSDN/标准 markdown 渲染器在表格单元格内支持 <br>)\n * - 合并多余空白\n */\nfunction normalizeCellContent(content: string): string {\n let result = content.replace(/^\\s+|\\s+$/g, '')\n result = result.replace(/\\|/g, '\\\\|')\n result = result.replace(/\\s*\\r?\\n\\s*/g, '<br>')\n result = result.replace(/[ \\t]{2,}/g, ' ')\n return result\n}\n\n/**\n * 表格单元格处理\n */\nfunction cell(content: string, node: Element): string {\n const normalized = normalizeCellContent(content)\n // 计算元素在兄弟元素中的位置\n // 使用 querySelectorAll 获取所有同级 th/td(更可靠)\n const parent = node.parentNode as Element | null\n if (!parent) {\n return '| ' + normalized + ' |'\n }\n const siblings = parent.querySelectorAll('th, td')\n const index = Array.from(siblings).indexOf(node)\n const prefix = index === 0 ? '| ' : ' '\n return prefix + normalized + ' |'\n}\n\n/**\n * 获取表格的第一行(兼容 linkedom,不依赖 table.rows)\n */\nfunction getFirstRow(table: Element): Element | null {\n // linkedom 没有 table.rows,直接查询 tr\n const tr = table.querySelector('tr')\n return tr\n}\n\n/**\n * 判断是否为表头行\n */\nfunction isHeadingRow(tr: Element): boolean {\n const parentNode = tr.parentNode as Element | null\n if (!parentNode) return false\n\n // 获取所有元素子节点(th/td),排除文本节点\n // 使用 querySelectorAll 更可靠(兼容 linkedom)\n const cells = tr.querySelectorAll('th, td')\n\n return (\n parentNode.nodeName === 'THEAD' ||\n (\n parentNode.firstChild === tr &&\n (parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&\n cells.length > 0 &&\n Array.from(cells).every((n: Element) => n.nodeName === 'TH')\n )\n )\n}\n\n/**\n * 判断是否为第一个 tbody\n */\nfunction isFirstTbody(element: Element): boolean {\n const previousSibling = element.previousSibling as Element | null\n\n return (\n element.nodeName === 'TBODY' && (\n !previousSibling ||\n (\n previousSibling.nodeName === 'THEAD' &&\n /^\\s*$/i.test(previousSibling.textContent || '')\n )\n )\n )\n}\n\n/**\n * 添加表格和代码块扩展规则\n */\nfunction addExtensionRules(turndownService: TurndownService): void {\n // figure 元素 - 直接透传内容(常包裹 table)\n turndownService.addRule('figure', {\n filter: 'figure',\n replacement: function(content) {\n return content\n }\n })\n\n // figcaption 元素 - 转为斜体文本\n turndownService.addRule('figcaption', {\n filter: 'figcaption',\n replacement: function(content) {\n return content ? '\\n*' + content.trim() + '*\\n' : ''\n }\n })\n\n // 表格单元格\n turndownService.addRule('tableCell', {\n filter: ['th', 'td'],\n replacement: function(content, node) {\n return cell(content, node as Element)\n }\n })\n\n // 表格行\n turndownService.addRule('tableRow', {\n filter: 'tr',\n replacement: function(content, node) {\n const tr = node as Element\n let borderCells = ''\n const alignMap: Record<string, string> = { left: ':--', right: '--:', center: ':-:' }\n\n if (isHeadingRow(tr)) {\n // 使用 querySelectorAll 获取所有 th/td(兼容 linkedom)\n const cells = tr.querySelectorAll('th, td')\n cells.forEach((child) => {\n let border = '---'\n const align = (child.getAttribute?.('align') || '').toLowerCase()\n\n if (align && alignMap[align]) {\n border = alignMap[align]\n }\n\n borderCells += cell(border, child)\n })\n }\n return '\\n' + content + (borderCells ? '\\n' + borderCells : '')\n }\n })\n\n // 表格\n turndownService.addRule('table', {\n filter: function(node) {\n try {\n if (node.nodeName !== 'TABLE') return false\n const table = node as Element\n const firstRow = getFirstRow(table)\n if (!firstRow) return false\n return isHeadingRow(firstRow)\n } catch (err) {\n logger.error('Table filter error:', err)\n return false\n }\n },\n replacement: function(content) {\n // 确保没有空行\n content = content.replace(/\\n\\n/g, '\\n')\n return '\\n\\n' + content + '\\n\\n'\n }\n })\n\n // 表格区段\n turndownService.addRule('tableSection', {\n filter: ['thead', 'tbody', 'tfoot'],\n replacement: function(content) {\n return content\n }\n })\n\n // 代码块\n turndownService.addRule('preCode', {\n filter: ['pre'],\n replacement: function(_content, node) {\n const pre = node as HTMLPreElement\n\n // 尝试获取语言(多种来源)\n let language = ''\n // 1. 从 pre 的 data-lang 属性获取\n const dataLang = pre.getAttribute('data-lang')\n if (dataLang) {\n language = dataLang\n }\n // 2. 从 code 的 class 获取\n if (!language) {\n const code = pre.querySelector('code')\n if (code) {\n const className = code.className || ''\n const langMatch = className.match(/language-(\\w+)/)\n if (langMatch) {\n language = langMatch[1]\n }\n }\n }\n // 3. 从 pre 的 class 获取\n if (!language) {\n const preClassName = pre.className || ''\n const preLangMatch = preClassName.match(/language-(\\w+)/)\n if (preLangMatch) {\n language = preLangMatch[1]\n }\n }\n // 4. 默认使用 bash\n if (!language) {\n language = 'bash'\n }\n\n // 处理微信等平台将每行代码放在单独 <code> 标签的情况\n const codeElements = pre.querySelectorAll('code')\n let text: string\n if (codeElements.length > 1) {\n // 多个 code 标签,提取每个的文本并用换行连接\n const lines: string[] = []\n codeElements.forEach((codeEl) => {\n lines.push(codeEl.innerText || codeEl.textContent || '')\n })\n text = lines.join('\\n')\n } else {\n text = pre.innerText || ''\n }\n\n // 清理文本\n text = text\n .replace(/\\r\\n/g, '\\n') // 统一换行符\n .replace(/\\r/g, '\\n') // 处理旧 Mac 换行符\n .replace(/^\\n+/, '') // 移除开头空行\n .replace(/\\n+$/, '') // 移除结尾空行\n\n // 空代码块不输出\n if (!text.trim()) {\n return ''\n }\n\n // 清理语言标识(只保留字母数字和常见字符)\n language = language.replace(/[^a-zA-Z0-9+#._-]/g, '').toLowerCase()\n\n // 检测内容中最长的连续反引号,使用更多反引号包裹\n // 例如内容有 ``` 则用 ````,内容有 ```` 则用 `````\n let fence = '```'\n const backtickMatches = text.match(/`+/g)\n if (backtickMatches) {\n const maxBackticks = Math.max(...backtickMatches.map(m => m.length))\n if (maxBackticks >= 3) {\n fence = '`'.repeat(maxBackticks + 1)\n }\n }\n\n return '\\n' + fence + language + '\\n' + text + '\\n' + fence + '\\n'\n }\n })\n\n // 源平台链接清理(站内链接去除、跳转中转还原)\n turndownService.addRule('sourcePlatformLinks', {\n filter: function(node) {\n if (node.nodeName !== 'A') return false\n const href = (node as Element).getAttribute('href') || ''\n return SOURCE_LINK_REMOVE_DOMAINS.some(d => href.includes(d))\n || SOURCE_LINK_REDIRECT_RULES.some(r => href.includes(r.domain))\n },\n replacement: function(content, node) {\n const href = (node as Element).getAttribute('href') || ''\n if (SOURCE_LINK_REMOVE_DOMAINS.some(d => href.includes(d))) {\n return content\n }\n const rule = SOURCE_LINK_REDIRECT_RULES.find(r => href.includes(r.domain))\n if (rule) {\n try {\n const url = new URL(href)\n const real = url.searchParams.get(rule.param)\n if (real) return '[' + content + '](' + real + ')'\n } catch {}\n }\n return '[' + content + '](' + href + ')'\n }\n })\n\n // 保留没有表头的表格(作为 HTML)\n turndownService.keep(function(node) {\n try {\n if (node.nodeName !== 'TABLE') return false\n const table = node as Element\n const firstRow = getFirstRow(table)\n if (!firstRow) return true\n return !isHeadingRow(firstRow)\n } catch (err) {\n logger.error('Table keep filter error:', err)\n return false\n }\n })\n}\n\n/**\n * 创建配置好的 Turndown 实例\n */\nexport function createTurndownService(options: TurndownOptions = {}): TurndownService {\n const turndownService = new TurndownService({\n headingStyle: options.headingStyle || 'atx',\n hr: options.hr || '---',\n bulletListMarker: options.bulletListMarker || '-',\n codeBlockStyle: options.codeBlockStyle || 'fenced',\n fence: options.fence || '```',\n emDelimiter: options.emDelimiter || '*',\n strongDelimiter: options.strongDelimiter || '**',\n linkStyle: options.linkStyle || 'inlined',\n linkReferenceStyle: options.linkReferenceStyle || 'full',\n })\n\n // 添加扩展规则\n addExtensionRules(turndownService)\n\n return turndownService\n}\n\n/**\n * HTML 转 Markdown\n * 使用 linkedom 解析 HTML,兼容 Service Worker 环境\n * 如果 turndown 失败,使用简单正则转换作为回退\n */\nexport function htmlToMarkdown(html: string, _options: TurndownOptions = {}): string {\n // linkedom + turndown has compatibility issues in Service Worker environments\n // Use regex-based conversion instead for reliability\n return htmlToMarkdownSimple(html)\n}\n\n/**\n * 从 className 提取编程语言\n */\nfunction extractLangFromClass(className: string): string {\n if (!className) return ''\n\n const patterns = [\n /language-(\\w+)/i, // language-javascript\n /lang-(\\w+)/i, // lang-js\n /\\bhljs\\s+(\\w+)/i, // hljs javascript\n /\\b(javascript|typescript|python|java|cpp|c|csharp|go|rust|ruby|php|swift|kotlin|scala|sql|html|css|json|xml|yaml|markdown|bash|shell|powershell)\\b/i,\n ]\n\n for (const pattern of patterns) {\n const match = className.match(pattern)\n if (match) {\n return match[1].toLowerCase()\n }\n }\n\n return ''\n}\n\n/**\n * 简单的正则 HTML → Markdown 转换 (回退方案)\n * 增强版:支持表格转换、代码块语言识别、LaTeX 公式\n */\nfunction htmlToMarkdownSimple(html: string): string {\n let md = html\n\n // ============ 预处理:移除微信代码块行号 ============\n // 必须在列表转换之前执行,否则 <li> 会被转成 \"- \"\n // 支持 class=\"code-snippet__line-index\" 和 class='code-snippet__line-index'\n md = md.replace(\n /<ul[^>]*class=[\"'][^\"']*code-snippet__line-index[^\"']*[\"'][^>]*>[\\s\\S]*?<\\/ul>/gi,\n ''\n )\n\n // 表格转换 - 必须在移除其他标签之前处理\n md = convertTables(md)\n\n // 标题\n md = md.replace(/<h1[^>]*>([\\s\\S]*?)<\\/h1>/gi, '\\n# $1\\n')\n md = md.replace(/<h2[^>]*>([\\s\\S]*?)<\\/h2>/gi, '\\n## $1\\n')\n md = md.replace(/<h3[^>]*>([\\s\\S]*?)<\\/h3>/gi, '\\n### $1\\n')\n md = md.replace(/<h4[^>]*>([\\s\\S]*?)<\\/h4>/gi, '\\n#### $1\\n')\n md = md.replace(/<h5[^>]*>([\\s\\S]*?)<\\/h5>/gi, '\\n##### $1\\n')\n md = md.replace(/<h6[^>]*>([\\s\\S]*?)<\\/h6>/gi, '\\n###### $1\\n')\n\n // 粗体和斜体\n md = md.replace(/<(strong|b)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '**$2**')\n md = md.replace(/<(em|i)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '*$2*')\n\n // 链接\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, '[$2]($1)')\n\n // 图片\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*alt=\"([^\"]*)\"[^>]*\\/?>/gi, '![$2]($1)')\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*\\/?>/gi, '![]($1)')\n\n // 代码块 - 增强语言检测\n md = md.replace(/<pre[^>]*>([\\s\\S]*?)<\\/pre>/gi, (match, content) => {\n let language = ''\n\n // 从 pre 标签提取语言\n const preLangMatch = match.match(/<pre[^>]*data-lang(?:uage)?=[\"'](\\w+)[\"']/)\n const preClassMatch = match.match(/<pre[^>]*class=\"([^\"]*)\"/)\n if (preLangMatch) {\n language = preLangMatch[1]\n } else if (preClassMatch) {\n language = extractLangFromClass(preClassMatch[1])\n }\n\n // 从 code 标签提取语言\n const codeLangMatch = content.match(/<code[^>]*data-lang(?:uage)?=[\"'](\\w+)[\"']/)\n const codeClassMatch = content.match(/<code[^>]*class=\"([^\"]*)\"/)\n if (!language) {\n if (codeLangMatch) {\n language = codeLangMatch[1]\n } else if (codeClassMatch) {\n language = extractLangFromClass(codeClassMatch[1])\n }\n }\n\n // 使用安全的代码提取函数\n const text = extractCodeFromHtml(content)\n\n return '\\n```' + language + '\\n' + text + '\\n```\\n'\n })\n\n // 行内代码\n md = md.replace(/<code[^>]*>([\\s\\S]*?)<\\/code>/gi, '`$1`')\n\n // 列表\n md = md.replace(/<ul[^>]*>([\\s\\S]*?)<\\/ul>/gi, '$1\\n')\n md = md.replace(/<ol[^>]*>([\\s\\S]*?)<\\/ol>/gi, '$1\\n')\n md = md.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, '- $1\\n')\n\n // 段落和换行\n md = md.replace(/<p[^>]*>([\\s\\S]*?)<\\/p>/gi, '\\n$1\\n')\n md = md.replace(/<br\\s*\\/?>/gi, '\\n')\n md = md.replace(/<hr\\s*\\/?>/gi, '\\n---\\n')\n\n // 块引用\n md = md.replace(/<blockquote[^>]*>([\\s\\S]*?)<\\/blockquote>/gi, (_, content) => {\n return '\\n' + content.trim().split('\\n').map((line: string) => '> ' + line).join('\\n') + '\\n'\n })\n\n // LaTeX 公式 - 块级 (mode=display)\n // 注意:$$ 在替换字符串中需要写成 $$$$ 才能输出 $$\n md = md.replace(/<script[^>]*type=[\"']math\\/tex[^\"']*display[^\"']*[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi, '\\n$$$$\\n$1\\n$$$$\\n')\n // LaTeX 公式 - 行内\n md = md.replace(/<script[^>]*type=[\"']math\\/tex[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi, ' $$$$$1$$$$ ')\n\n // 移除其他标签\n md = md.replace(/<\\/?[^>]+(>|$)/g, '')\n\n // 解码 HTML 实体\n md = md.replace(/&amp;/g, '&')\n md = md.replace(/&lt;/g, '<')\n md = md.replace(/&gt;/g, '>')\n md = md.replace(/&quot;/g, '\"')\n md = md.replace(/&#039;/g, \"'\")\n md = md.replace(/&nbsp;/g, ' ')\n\n // 清理多余空行\n md = md.replace(/\\n{3,}/g, '\\n\\n')\n md = md.trim()\n\n return md\n}\n\n/**\n * 将 HTML 表格转换为 Markdown 表格\n */\nfunction convertTables(html: string): string {\n // 先处理 figure 包裹的表格\n html = html.replace(/<figure[^>]*>([\\s\\S]*?)<\\/figure>/gi, (match, figureContent) => {\n // 检查是否包含表格\n if (/<table[^>]*>/i.test(figureContent)) {\n // 提取表格部分,保留 figcaption\n const tableMatch = figureContent.match(/<table[^>]*>([\\s\\S]*?)<\\/table>/i)\n const captionMatch = figureContent.match(/<figcaption[^>]*>([\\s\\S]*?)<\\/figcaption>/i)\n\n if (tableMatch) {\n let result = tableMatch[0] // 返回表格部分\n if (captionMatch) {\n // 添加 caption 作为斜体\n const caption = captionMatch[1].replace(/<[^>]+>/g, '').trim()\n if (caption) {\n result += '\\n*' + caption + '*\\n'\n }\n }\n return result\n }\n }\n return match // 非表格 figure 保持原样\n })\n\n // 匹配整个表格\n return html.replace(/<table[^>]*>([\\s\\S]*?)<\\/table>/gi, (_, tableContent) => {\n // 检查是否有表头(thead 或 第一行全是 th)\n const hasTheadMatch = tableContent.match(/<thead[^>]*>([\\s\\S]*?)<\\/thead>/i)\n const rows: string[][] = []\n const alignments: string[][] = [] // 存储每行每个单元格的对齐方式\n let headerRowIndex = -1\n\n // 提取所有行\n const rowMatches = tableContent.match(/<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi) || []\n\n for (let i = 0; i < rowMatches.length; i++) {\n const rowContent = rowMatches[i]\n const cells: string[] = []\n const rowAligns: string[] = []\n\n // 检查单元格类型(th 或 td)\n const cellMatches = rowContent.match(/<t[hd][^>]*>([\\s\\S]*?)<\\/t[hd]>/gi) || []\n const isHeaderRow = cellMatches.length > 0 && cellMatches.every((c: string) => c.startsWith('<th'))\n\n for (const cellMatch of cellMatches) {\n // 提取单元格内容和对齐属性\n const fullMatch = cellMatch.match(/<t[hd]([^>]*)>([\\s\\S]*?)<\\/t[hd]>/i)\n if (fullMatch) {\n const attrs = fullMatch[1]\n const rawContent = fullMatch[2]\n\n // 提取对齐方式\n const alignMatch = attrs.match(/align=[\"']?(left|center|right)[\"']?/i) ||\n attrs.match(/style=[\"'][^\"']*text-align:\\s*(left|center|right)/i)\n const align = alignMatch ? alignMatch[1].toLowerCase() : ''\n rowAligns.push(align)\n\n // 清理内容\n let content = rawContent\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/\\s+/g, ' ')\n .trim()\n\n // 转义管道符\n content = content.replace(/\\|/g, '\\\\|')\n cells.push(content)\n }\n }\n\n if (cells.length > 0) {\n rows.push(cells)\n alignments.push(rowAligns)\n // 记录表头行(在 thead 中或是第一行全 th)\n if (hasTheadMatch && rowContent.indexOf('<th') !== -1) {\n headerRowIndex = i\n } else if (i === 0 && isHeaderRow) {\n headerRowIndex = 0\n }\n }\n }\n\n // 如果没有行,返回空\n if (rows.length === 0) {\n return ''\n }\n\n // 如果没有找到表头行,将第一行作为表头(降级处理)\n if (headerRowIndex === -1) {\n headerRowIndex = 0\n }\n\n // 转换为 Markdown\n const mdRows: string[] = []\n const colCount = Math.max(...rows.map(r => r.length))\n\n // 获取表头行的对齐信息\n const headerAligns = alignments[headerRowIndex] || []\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i]\n // 填充缺失的单元格\n while (row.length < colCount) {\n row.push('')\n }\n\n mdRows.push('| ' + row.join(' | ') + ' |')\n\n // 在表头行之后添加分隔行(带对齐信息)\n if (i === headerRowIndex) {\n const separators = []\n for (let j = 0; j < colCount; j++) {\n const align = headerAligns[j] || ''\n if (align === 'left') {\n separators.push(':---')\n } else if (align === 'center') {\n separators.push(':---:')\n } else if (align === 'right') {\n separators.push('---:')\n } else {\n separators.push('---')\n }\n }\n mdRows.push('| ' + separators.join(' | ') + ' |')\n }\n }\n\n return '\\n\\n' + mdRows.join('\\n') + '\\n\\n'\n })\n}\n\n/**\n * 默认的 Turndown 实例(可复用)\n */\nlet defaultService: TurndownService | null = null\n\n/**\n * 获取默认 Turndown 实例\n */\nexport function getDefaultTurndownService(): TurndownService {\n if (!defaultService) {\n defaultService = createTurndownService()\n }\n return defaultService\n}\n\n// 导出 TurndownService 类型供外部使用\nexport { TurndownService }\n\n/**\n * HTML 转 Markdown(使用原生 DOM)\n * 适用于 Content Script / 页面环境,利用浏览器原生 DOM\n * 比 linkedom 兼容性更好,转换质量更高\n */\nexport function htmlToMarkdownNative(html: string, options: TurndownOptions = {}): string {\n if (typeof document === 'undefined') {\n // 回退到简单正则转换(Service Worker 环境)\n logger.warn('No native DOM, falling back to regex conversion')\n return htmlToMarkdownSimple(html)\n }\n\n try {\n const turndownService = new TurndownService({\n headingStyle: options.headingStyle || 'atx',\n hr: options.hr || '---',\n bulletListMarker: options.bulletListMarker || '-',\n codeBlockStyle: options.codeBlockStyle || 'fenced',\n fence: options.fence || '```',\n emDelimiter: options.emDelimiter || '*',\n strongDelimiter: options.strongDelimiter || '**',\n linkStyle: options.linkStyle || 'inlined',\n linkReferenceStyle: options.linkReferenceStyle || 'full',\n })\n\n // 添加扩展规则\n addExtensionRules(turndownService)\n\n // 使用原生 DOM 解析 HTML\n const container = document.createElement('div')\n container.innerHTML = html\n\n // 预处理:移除微信代码块行号元素(必须在 turndown 之前)\n container.querySelectorAll('ul.code-snippet__line-index, ul[class*=\"code-snippet__line-index\"]')\n .forEach(el => el.remove())\n\n return turndownService.turndown(container)\n } catch (err) {\n logger.error('Native DOM conversion failed:', err)\n return htmlToMarkdownSimple(html)\n }\n}\n\n// ============ Markdown → HTML ============\n\nimport { marked } from 'marked'\n\n/**\n * Markdown 转 HTML\n */\nexport function markdownToHtml(markdown: string): string {\n return marked.parse(markdown, { async: false }) as string\n}\n\n/**\n * HTML 标准化\n * 通过 HTML → Markdown → HTML 往返转换,清理所有多余结构\n */\nexport function normalizeHtml(html: string, options: TurndownOptions = {}): string {\n const markdown = htmlToMarkdown(html, options)\n return markdownToHtml(markdown)\n}\n","/**\n * AWS4-HMAC-SHA256 签名实现\n * 用于字节跳动 ImageX 图片上传服务\n */\n\n// 使用 Web Crypto API 进行 HMAC-SHA256 签名\nasync function hmacSha256(key: ArrayBuffer | Uint8Array, message: string): Promise<ArrayBuffer> {\n // 转换为 Uint8Array 然后创建新的 ArrayBuffer\n const keyBytes = key instanceof Uint8Array ? key : new Uint8Array(key)\n const keyBuffer = new Uint8Array(keyBytes).buffer as ArrayBuffer\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n keyBuffer,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n return crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(message))\n}\n\n// SHA256 哈希\nasync function sha256(message: string): Promise<string> {\n const msgBuffer = new TextEncoder().encode(message)\n const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)\n return arrayBufferToHex(hashBuffer)\n}\n\n// ArrayBuffer 转 hex 字符串\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map(b => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n// 格式化日期为 AWS 格式\nfunction formatAmzDate(date: Date): string {\n return date.toISOString().replace(/[:-]|\\.\\d{3}/g, '')\n}\n\n// 格式化日期为 YYYYMMDD 格式\nfunction formatDateStamp(date: Date): string {\n return date.toISOString().slice(0, 10).replace(/-/g, '')\n}\n\nexport interface AWS4SignParams {\n method: string\n url: string\n accessKeyId: string\n secretAccessKey: string\n securityToken?: string\n region?: string\n service?: string\n headers?: Record<string, string>\n body?: string\n}\n\nexport interface AWS4SignResult {\n authorization: string\n amzDate: string\n headers: Record<string, string>\n}\n\n/**\n * 生成 AWS4-HMAC-SHA256 签名\n */\nexport async function signAWS4(params: AWS4SignParams): Promise<AWS4SignResult> {\n const {\n method,\n url,\n accessKeyId,\n secretAccessKey,\n securityToken,\n region = 'cn-north-1',\n service = 'imagex',\n headers = {},\n body = '',\n } = params\n\n const parsedUrl = new URL(url)\n const path = parsedUrl.pathname\n const queryString = parsedUrl.search.slice(1) // 去掉前导 ?\n\n const now = new Date()\n const amzDate = formatAmzDate(now)\n const dateStamp = formatDateStamp(now)\n\n // 规范化查询字符串(按参数名排序)\n const queryParams = new URLSearchParams(queryString)\n const sortedParams = Array.from(queryParams.entries()).sort((a, b) => a[0].localeCompare(b[0]))\n const canonicalQueryString = sortedParams\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join('&')\n\n // 构建签名 headers\n const signedHeadersObj: Record<string, string> = {\n 'x-amz-date': amzDate,\n }\n\n if (securityToken) {\n signedHeadersObj['x-amz-security-token'] = securityToken\n }\n\n // 合并其他 headers\n Object.assign(signedHeadersObj, headers)\n\n // 签名的 header 名称列表(小写,按字母排序)\n const signedHeaderNames = Object.keys(signedHeadersObj)\n .map(k => k.toLowerCase())\n .sort()\n .join(';')\n\n // 规范化 headers(小写 key,按字母排序)\n const canonicalHeaders = Object.entries(signedHeadersObj)\n .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}`)\n .sort()\n .join('\\n') + '\\n'\n\n // 计算 payload 哈希\n const payloadHash = await sha256(body)\n\n // 构建 Canonical Request\n const canonicalRequest = [\n method.toUpperCase(),\n path || '/',\n canonicalQueryString,\n canonicalHeaders,\n signedHeaderNames,\n payloadHash,\n ].join('\\n')\n\n // 构建 String to Sign\n const algorithm = 'AWS4-HMAC-SHA256'\n const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`\n const canonicalRequestHash = await sha256(canonicalRequest)\n\n const stringToSign = [\n algorithm,\n amzDate,\n credentialScope,\n canonicalRequestHash,\n ].join('\\n')\n\n // 计算签名密钥\n const kDate = await hmacSha256(\n new TextEncoder().encode('AWS4' + secretAccessKey),\n dateStamp\n )\n const kRegion = await hmacSha256(kDate, region)\n const kService = await hmacSha256(kRegion, service)\n const kSigning = await hmacSha256(kService, 'aws4_request')\n\n // 计算最终签名\n const signatureBuffer = await hmacSha256(kSigning, stringToSign)\n const signature = arrayBufferToHex(signatureBuffer)\n\n // 构建 Authorization header\n const authorization = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames}, Signature=${signature}`\n\n // 构建返回的 headers\n const resultHeaders: Record<string, string> = {\n authorization,\n 'x-amz-date': amzDate,\n }\n\n if (securityToken) {\n resultHeaders['x-amz-security-token'] = securityToken\n }\n\n return {\n authorization,\n amzDate,\n headers: resultHeaders,\n }\n}\n\n/**\n * 计算文件的 CRC32 校验值\n */\nexport function crc32(data: Uint8Array): string {\n let crc = 0xFFFFFFFF\n const table = getCRC32Table()\n\n for (let i = 0; i < data.length; i++) {\n crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xFF]\n }\n\n return ((crc ^ 0xFFFFFFFF) >>> 0).toString(16).padStart(8, '0')\n}\n\n// CRC32 查找表(懒加载)\nlet crc32Table: Uint32Array | null = null\n\nfunction getCRC32Table(): Uint32Array {\n if (crc32Table) return crc32Table\n\n crc32Table = new Uint32Array(256)\n for (let i = 0; i < 256; i++) {\n let c = i\n for (let j = 0; j < 8; j++) {\n c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)\n }\n crc32Table[i] = c\n }\n return crc32Table\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/lib/logger.ts","../src/lib/markdown-images.ts","../src/lib/turndown.ts","../src/lib/aws4.ts"],"sourcesContent":["/**\n * 通用日志接口 - 不依赖浏览器 API\n */\n\nexport type LogLevel = 'debug' | 'info' | 'warn' | 'error'\n\nexport interface Logger {\n debug: (...args: unknown[]) => void\n info: (...args: unknown[]) => void\n warn: (...args: unknown[]) => void\n error: (...args: unknown[]) => void\n}\n\nexport interface LoggerConfig {\n enabled: boolean\n level: LogLevel\n}\n\nconst LOG_LEVELS: Record<LogLevel, number> = {\n debug: 0,\n info: 1,\n warn: 2,\n error: 3,\n}\n\n// 检测生产环境 (支持 Vite 和 Node.js)\nconst isProd = typeof import.meta !== 'undefined' && (import.meta as unknown as { env?: { PROD?: boolean } }).env?.PROD\n || (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production')\n\n// 默认配置:生产环境只输出 warn 和 error\nlet globalConfig: LoggerConfig = {\n enabled: true,\n level: isProd ? 'warn' : 'debug',\n}\n\n/**\n * 设置全局日志配置\n */\nexport function setLoggerConfig(config: Partial<LoggerConfig>) {\n globalConfig = { ...globalConfig, ...config }\n}\n\n/**\n * 获取当前配置\n */\nexport function getLoggerConfig(): LoggerConfig {\n return { ...globalConfig }\n}\n\n/**\n * 创建带前缀的 logger\n */\nexport function createLogger(prefix: string): Logger {\n const shouldLog = (level: LogLevel): boolean => {\n if (!globalConfig.enabled) return false\n return LOG_LEVELS[level] >= LOG_LEVELS[globalConfig.level]\n }\n\n return {\n debug: (...args: unknown[]) => {\n if (shouldLog('debug')) {\n console.log(`[${prefix}]`, ...args)\n }\n },\n info: (...args: unknown[]) => {\n if (shouldLog('info')) {\n console.log(`[${prefix}]`, ...args)\n }\n },\n warn: (...args: unknown[]) => {\n if (shouldLog('warn')) {\n console.warn(`[${prefix}]`, ...args)\n }\n },\n error: (...args: unknown[]) => {\n if (shouldLog('error')) {\n console.error(`[${prefix}]`, ...args)\n }\n },\n }\n}\n","export interface MarkdownImageMatch {\n full: string\n alt: string\n src: string\n}\n\n/**\n * 解析 Markdown 图片语法,支持可选标题和括号 URL\n */\nexport function parseMarkdownImages(markdown: string): MarkdownImageMatch[] {\n const results: MarkdownImageMatch[] = []\n const len = markdown.length\n let i = 0\n\n const findClosingBracket = (start: number): number => {\n for (let j = start; j < len; j++) {\n const ch = markdown[j]\n if (ch === '\\\\') {\n j++\n continue\n }\n if (ch === ']') return j\n }\n return -1\n }\n\n while (i < len) {\n const start = markdown.indexOf('![', i)\n if (start === -1) break\n\n const altStart = start + 2\n const altEnd = findClosingBracket(altStart)\n if (altEnd === -1 || markdown[altEnd + 1] !== '(') {\n i = altStart\n continue\n }\n\n let k = altEnd + 2\n while (k < len && /\\s/.test(markdown[k])) k++\n\n let url = ''\n if (markdown[k] === '<') {\n const close = markdown.indexOf('>', k + 1)\n if (close === -1) {\n i = altEnd + 1\n continue\n }\n url = markdown.slice(k + 1, close)\n k = close + 1\n } else {\n const urlStart = k\n let depth = 0\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === '(') {\n depth++\n } else if (ch === ')') {\n if (depth === 0) break\n depth--\n } else if (/\\s/.test(ch) && depth === 0) {\n break\n }\n k++\n }\n url = markdown.slice(urlStart, k)\n }\n\n if (!url) {\n i = altEnd + 1\n continue\n }\n\n while (k < len && /\\s/.test(markdown[k])) k++\n\n if (k < len && (markdown[k] === '\"' || markdown[k] === '\\'')) {\n const quote = markdown[k]\n k++\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === quote) {\n k++\n break\n }\n k++\n }\n while (k < len && /\\s/.test(markdown[k])) k++\n } else if (k < len && markdown[k] === '(') {\n k++\n while (k < len) {\n const ch = markdown[k]\n if (ch === '\\\\') {\n k += 2\n continue\n }\n if (ch === ')') {\n k++\n break\n }\n k++\n }\n while (k < len && /\\s/.test(markdown[k])) k++\n }\n\n if (markdown[k] !== ')') {\n i = altEnd + 1\n continue\n }\n\n const full = markdown.slice(start, k + 1)\n const alt = markdown.slice(altStart, altEnd)\n results.push({ full, alt, src: url })\n i = k + 1\n }\n\n return results\n}\n","/**\n * Turndown HTML→Markdown 转换工具\n *\n * 基于 turndown 库,添加表格和代码块扩展规则\n *\n * 架构说明:\n * - htmlToMarkdownNative: 使用原生 DOM,适用于 Content Script(推荐)\n * - htmlToMarkdown: 使用正则转换,适用于 Service Worker(回退方案)\n */\n\nimport TurndownService from 'turndown'\nimport { createLogger } from './logger'\n\nconst logger = createLogger('Turndown')\n\n// ============ 源平台链接清理规则 ============\n\n/** 需要去除的站内链接域名(去掉 <a> 只保留文本) */\nexport const SOURCE_LINK_REMOVE_DOMAINS = [\n 'zhida.zhihu.com',\n]\n\n/** 跳转中转规则:domain → 真实 URL 所在的 query 参数名 */\nexport const SOURCE_LINK_REDIRECT_RULES: Array<{ domain: string; param: string }> = [\n { domain: 'link.zhihu.com', param: 'target' },\n]\n\n// ============ HTML 实体解码工具 ============\n\n/**\n * 完整的 HTML 实体解码(用于代码块提取)\n * 处理:命名实体、十进制实体、十六进制实体、双重编码\n */\nfunction decodeHtmlEntities(text: string): string {\n let result = text\n\n // 1. 先处理双重编码(如 &amp;lt; → &lt;)\n // 最多处理 3 层嵌套\n for (let i = 0; i < 3; i++) {\n const prev = result\n result = result.replace(/&amp;/g, '&')\n if (prev === result) break\n }\n\n // 2. 解码十六进制实体 &#xNN; 或 &#XNN;\n result = result.replace(/&#[xX]([0-9a-fA-F]+);/g, (_, hex) => {\n return String.fromCharCode(parseInt(hex, 16))\n })\n\n // 3. 解码十进制实体 &#NN;\n result = result.replace(/&#(\\d+);/g, (_, dec) => {\n return String.fromCharCode(parseInt(dec, 10))\n })\n\n // 4. 解码常用命名实体\n const namedEntities: Record<string, string> = {\n '&lt;': '<',\n '&gt;': '>',\n '&amp;': '&',\n '&quot;': '\"',\n '&apos;': \"'\",\n '&#039;': \"'\",\n '&nbsp;': ' ',\n '&ndash;': '\\u2013',\n '&mdash;': '\\u2014',\n '&lsquo;': '\\u2018',\n '&rsquo;': '\\u2019',\n '&ldquo;': '\\u201C',\n '&rdquo;': '\\u201D',\n '&copy;': '\\u00A9',\n '&reg;': '\\u00AE',\n '&trade;': '\\u2122',\n '&hellip;': '\\u2026',\n }\n\n for (const [entity, char] of Object.entries(namedEntities)) {\n result = result.split(entity).join(char)\n }\n\n return result\n}\n\n/**\n * 已知的 HTML 标签白名单(可能出现在代码块中的格式化标签)\n * 只移除这些标签,避免误删代码中的泛型语法如 List<String>\n */\nconst KNOWN_HTML_TAGS = [\n // 文本格式化\n 'span', 'em', 'strong', 'b', 'i', 'u', 's', 'strike', 'del', 'ins', 'mark',\n 'sub', 'sup', 'small', 'big', 'font', 'a',\n // 代码相关\n 'code', 'pre', 'kbd', 'samp', 'var', 'tt',\n // 块级元素\n 'div', 'p', 'section', 'article', 'header', 'footer', 'aside', 'nav',\n 'main', 'figure', 'figcaption', 'blockquote', 'address',\n // 列表\n 'ul', 'ol', 'li', 'dl', 'dt', 'dd',\n // 表格\n 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'caption', 'colgroup', 'col',\n // 换行/分隔\n 'br', 'hr', 'wbr',\n // 其他\n 'abbr', 'acronym', 'cite', 'dfn', 'q', 'time', 'ruby', 'rt', 'rp',\n 'bdi', 'bdo', 'data', 'meter', 'progress', 'output', 'details', 'summary',\n // 微信特殊标签\n 'mpvoice', 'mpprofile', 'qqmusic', 'mpcps',\n]\n\n/**\n * 从 HTML 中安全提取代码文本\n *\n * 策略:\n * 1. 先用占位符保护 HTML 实体编码的 < > (如 &lt; &gt;)\n * 2. 只移除已知的 HTML 标签(白名单),保留代码中的泛型语法\n * 3. 恢复占位符并解码所有 HTML 实体\n */\nfunction extractCodeText(html: string): string {\n const LT_PLACEHOLDER = '\\x00__CODE_LT__\\x00'\n const GT_PLACEHOLDER = '\\x00__CODE_GT__\\x00'\n\n let text = html\n\n // 1. 保护所有表示 < > 的 HTML 实体(它们是代码内容,不是标签)\n // 命名实体\n text = text.replace(/&lt;/gi, LT_PLACEHOLDER)\n text = text.replace(/&gt;/gi, GT_PLACEHOLDER)\n // 十进制实体 &#60; &#62;\n text = text.replace(/&#0*60;/gi, LT_PLACEHOLDER)\n text = text.replace(/&#0*62;/gi, GT_PLACEHOLDER)\n // 十六进制实体 &#x3C; &#x3E;\n text = text.replace(/&#x0*3[cC];/gi, LT_PLACEHOLDER)\n text = text.replace(/&#x0*3[eE];/gi, GT_PLACEHOLDER)\n\n // 2. 只移除已知的 HTML 标签(白名单方式)\n // 这样可以保留代码中的泛型语法如 List<String>, Map<K, V> 等\n const tagPattern = KNOWN_HTML_TAGS.join('|')\n // 匹配开标签: <tagname ...> 或自闭合 <tagname ... />\n text = text.replace(new RegExp(`<(${tagPattern})\\\\b[^>]*\\\\/?>`, 'gi'), '')\n // 匹配闭标签: </tagname>\n text = text.replace(new RegExp(`<\\\\/(${tagPattern})>`, 'gi'), '')\n\n // 3. 恢复占位符为实际的 < > 字符\n text = text.replace(new RegExp(LT_PLACEHOLDER.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), '<')\n text = text.replace(new RegExp(GT_PLACEHOLDER.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&'), 'g'), '>')\n\n // 4. 解码剩余的 HTML 实体\n text = decodeHtmlEntities(text)\n\n return text\n}\n\n/**\n * 修复代码块中未转义的 < 字符\n * 在 DOM 解析之前调用,防止浏览器将 < 误认为标签开始而截断内容\n *\n * 策略:在 <pre> 和 <code> 标签内,将看起来不像 HTML 标签的 < 转义为 &lt;\n */\nexport function fixUnescapedLtInCode(html: string): string {\n // 处理 <pre>...</pre> 块\n let result = html.replace(/<pre([^>]*)>([\\s\\S]*?)<\\/pre>/gi, (_match, attrs, content) => {\n const fixedContent = escapeNonTagLt(content)\n return `<pre${attrs}>${fixedContent}</pre>`\n })\n\n // 处理独立的 <code>...</code> 块(不在 pre 内的)\n result = result.replace(/<code([^>]*)>([\\s\\S]*?)<\\/code>/gi, (_match, attrs, content) => {\n const fixedContent = escapeNonTagLt(content)\n return `<code${attrs}>${fixedContent}</code>`\n })\n\n return result\n}\n\n/**\n * 转义不是 HTML 标签的 < 字符\n * HTML 标签的特征:< 后紧跟字母或 /\n */\nfunction escapeNonTagLt(content: string): string {\n // 匹配 < 后面不是字母、/ 或 ! 的情况(不是有效标签开始)\n // 例如:< 5, < =, <=, < b (空格后字母)\n return content.replace(/<(?![a-zA-Z\\/!])/g, '&lt;')\n}\n\n/**\n * 从 HTML 代码块中提取纯文本(更安全的方式)\n * 先处理换行标签,再提取文本\n * @exported 供其他模块使用\n */\nexport function extractCodeFromHtml(html: string): string {\n let text = html\n // 保留换行标签的换行效果\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/div>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n')\n .replace(/<\\/li>/gi, '\\n')\n // 移除 code 标签但保留内容\n .replace(/<\\/?code[^>]*>/gi, '')\n\n // 提取代码文本(处理实体和标签)\n text = extractCodeText(text)\n\n return text.trim()\n}\n\n/**\n * 转换选项\n */\nexport interface TurndownOptions {\n /** 标题样式: setext (===) 或 atx (#) */\n headingStyle?: 'setext' | 'atx'\n /** 水平线样式 */\n hr?: string\n /** 粗体分隔符 */\n bulletListMarker?: '-' | '+' | '*'\n /** 代码块样式 */\n codeBlockStyle?: 'indented' | 'fenced'\n /** 代码块围栏符号 */\n fence?: '```' | '~~~'\n /** 强调分隔符 */\n emDelimiter?: '_' | '*'\n /** 粗体分隔符 */\n strongDelimiter?: '__' | '**'\n /** 链接样式 */\n linkStyle?: 'inlined' | 'referenced'\n /** 链接引用样式 */\n linkReferenceStyle?: 'full' | 'collapsed' | 'shortcut'\n}\n\n/**\n * 规范化单元格内容\n * markdown 表格要求每行单元格在同一行,且 `|` 需转义\n * - 去首尾空白\n * - 转义管道符\n * - 换行 → <br>(保留视觉换行;CSDN/标准 markdown 渲染器在表格单元格内支持 <br>)\n * - 合并多余空白\n */\nfunction normalizeCellContent(content: string): string {\n let result = content.replace(/^\\s+|\\s+$/g, '')\n result = result.replace(/\\|/g, '\\\\|')\n result = result.replace(/\\s*\\r?\\n\\s*/g, '<br>')\n result = result.replace(/[ \\t]{2,}/g, ' ')\n return result\n}\n\n/**\n * 表格单元格处理\n */\nfunction cell(content: string, node: Element): string {\n const normalized = normalizeCellContent(content)\n // 计算元素在兄弟元素中的位置\n // 使用 querySelectorAll 获取所有同级 th/td(更可靠)\n const parent = node.parentNode as Element | null\n if (!parent) {\n return '| ' + normalized + ' |'\n }\n const siblings = parent.querySelectorAll('th, td')\n const index = Array.from(siblings).indexOf(node)\n const prefix = index === 0 ? '| ' : ' '\n return prefix + normalized + ' |'\n}\n\n/**\n * 获取表格的第一行(兼容 linkedom,不依赖 table.rows)\n */\nfunction getFirstRow(table: Element): Element | null {\n // linkedom 没有 table.rows,直接查询 tr\n const tr = table.querySelector('tr')\n return tr\n}\n\n/**\n * 判断是否为表头行\n */\nfunction isHeadingRow(tr: Element): boolean {\n const parentNode = tr.parentNode as Element | null\n if (!parentNode) return false\n\n // 获取所有元素子节点(th/td),排除文本节点\n // 使用 querySelectorAll 更可靠(兼容 linkedom)\n const cells = tr.querySelectorAll('th, td')\n\n return (\n parentNode.nodeName === 'THEAD' ||\n (\n parentNode.firstChild === tr &&\n (parentNode.nodeName === 'TABLE' || isFirstTbody(parentNode)) &&\n cells.length > 0 &&\n Array.from(cells).every((n: Element) => n.nodeName === 'TH')\n )\n )\n}\n\n/**\n * 判断是否为第一个 tbody\n */\nfunction isFirstTbody(element: Element): boolean {\n const previousSibling = element.previousSibling as Element | null\n\n return (\n element.nodeName === 'TBODY' && (\n !previousSibling ||\n (\n previousSibling.nodeName === 'THEAD' &&\n /^\\s*$/i.test(previousSibling.textContent || '')\n )\n )\n )\n}\n\n/**\n * 添加表格和代码块扩展规则\n */\nfunction addExtensionRules(turndownService: TurndownService): void {\n // figure 元素 - 直接透传内容(常包裹 table)\n turndownService.addRule('figure', {\n filter: 'figure',\n replacement: function(content) {\n return content\n }\n })\n\n // figcaption 元素 - 转为斜体文本\n turndownService.addRule('figcaption', {\n filter: 'figcaption',\n replacement: function(content) {\n return content ? '\\n*' + content.trim() + '*\\n' : ''\n }\n })\n\n // 表格单元格\n turndownService.addRule('tableCell', {\n filter: ['th', 'td'],\n replacement: function(content, node) {\n return cell(content, node as Element)\n }\n })\n\n // 表格行\n turndownService.addRule('tableRow', {\n filter: 'tr',\n replacement: function(content, node) {\n const tr = node as Element\n let borderCells = ''\n const alignMap: Record<string, string> = { left: ':--', right: '--:', center: ':-:' }\n\n if (isHeadingRow(tr)) {\n // 使用 querySelectorAll 获取所有 th/td(兼容 linkedom)\n const cells = tr.querySelectorAll('th, td')\n cells.forEach((child) => {\n let border = '---'\n const align = (child.getAttribute?.('align') || '').toLowerCase()\n\n if (align && alignMap[align]) {\n border = alignMap[align]\n }\n\n borderCells += cell(border, child)\n })\n }\n return '\\n' + content + (borderCells ? '\\n' + borderCells : '')\n }\n })\n\n // 表格\n turndownService.addRule('table', {\n filter: function(node) {\n try {\n if (node.nodeName !== 'TABLE') return false\n const table = node as Element\n const firstRow = getFirstRow(table)\n if (!firstRow) return false\n return isHeadingRow(firstRow)\n } catch (err) {\n logger.error('Table filter error:', err)\n return false\n }\n },\n replacement: function(content) {\n // 确保没有空行\n content = content.replace(/\\n\\n/g, '\\n')\n return '\\n\\n' + content + '\\n\\n'\n }\n })\n\n // 表格区段\n turndownService.addRule('tableSection', {\n filter: ['thead', 'tbody', 'tfoot'],\n replacement: function(content) {\n return content\n }\n })\n\n // 代码块\n turndownService.addRule('preCode', {\n filter: ['pre'],\n replacement: function(_content, node) {\n const pre = node as HTMLPreElement\n\n // 尝试获取语言(多种来源)\n let language = ''\n // 1. 从 pre 的 data-lang 属性获取\n const dataLang = pre.getAttribute('data-lang')\n if (dataLang) {\n language = dataLang\n }\n // 2. 从 code 的 class 获取\n if (!language) {\n const code = pre.querySelector('code')\n if (code) {\n const className = code.className || ''\n const langMatch = className.match(/language-(\\w+)/)\n if (langMatch) {\n language = langMatch[1]\n }\n }\n }\n // 3. 从 pre 的 class 获取\n if (!language) {\n const preClassName = pre.className || ''\n const preLangMatch = preClassName.match(/language-(\\w+)/)\n if (preLangMatch) {\n language = preLangMatch[1]\n }\n }\n // 4. 默认使用 bash\n if (!language) {\n language = 'bash'\n }\n\n // 处理微信等平台将每行代码放在单独 <code> 标签的情况\n const codeElements = pre.querySelectorAll('code')\n let text: string\n if (codeElements.length > 1) {\n // 多个 code 标签,提取每个的文本并用换行连接\n const lines: string[] = []\n codeElements.forEach((codeEl) => {\n lines.push(codeEl.innerText || codeEl.textContent || '')\n })\n text = lines.join('\\n')\n } else {\n text = pre.innerText || ''\n }\n\n // 清理文本\n text = text\n .replace(/\\r\\n/g, '\\n') // 统一换行符\n .replace(/\\r/g, '\\n') // 处理旧 Mac 换行符\n .replace(/^\\n+/, '') // 移除开头空行\n .replace(/\\n+$/, '') // 移除结尾空行\n\n // 空代码块不输出\n if (!text.trim()) {\n return ''\n }\n\n // 清理语言标识(只保留字母数字和常见字符)\n language = language.replace(/[^a-zA-Z0-9+#._-]/g, '').toLowerCase()\n\n // 检测内容中最长的连续反引号,使用更多反引号包裹\n // 例如内容有 ``` 则用 ````,内容有 ```` 则用 `````\n let fence = '```'\n const backtickMatches = text.match(/`+/g)\n if (backtickMatches) {\n const maxBackticks = Math.max(...backtickMatches.map(m => m.length))\n if (maxBackticks >= 3) {\n fence = '`'.repeat(maxBackticks + 1)\n }\n }\n\n return '\\n' + fence + language + '\\n' + text + '\\n' + fence + '\\n'\n }\n })\n\n // 源平台链接清理(站内链接去除、跳转中转还原)\n turndownService.addRule('sourcePlatformLinks', {\n filter: function(node) {\n if (node.nodeName !== 'A') return false\n const href = (node as Element).getAttribute('href') || ''\n return SOURCE_LINK_REMOVE_DOMAINS.some(d => href.includes(d))\n || SOURCE_LINK_REDIRECT_RULES.some(r => href.includes(r.domain))\n },\n replacement: function(content, node) {\n const href = (node as Element).getAttribute('href') || ''\n if (SOURCE_LINK_REMOVE_DOMAINS.some(d => href.includes(d))) {\n return content\n }\n const rule = SOURCE_LINK_REDIRECT_RULES.find(r => href.includes(r.domain))\n if (rule) {\n try {\n const url = new URL(href)\n const real = url.searchParams.get(rule.param)\n if (real) return '[' + content + '](' + real + ')'\n } catch {}\n }\n return '[' + content + '](' + href + ')'\n }\n })\n\n // 保留没有表头的表格(作为 HTML)\n turndownService.keep(function(node) {\n try {\n if (node.nodeName !== 'TABLE') return false\n const table = node as Element\n const firstRow = getFirstRow(table)\n if (!firstRow) return true\n return !isHeadingRow(firstRow)\n } catch (err) {\n logger.error('Table keep filter error:', err)\n return false\n }\n })\n}\n\n/**\n * 创建配置好的 Turndown 实例\n */\nexport function createTurndownService(options: TurndownOptions = {}): TurndownService {\n const turndownService = new TurndownService({\n headingStyle: options.headingStyle || 'atx',\n hr: options.hr || '---',\n bulletListMarker: options.bulletListMarker || '-',\n codeBlockStyle: options.codeBlockStyle || 'fenced',\n fence: options.fence || '```',\n emDelimiter: options.emDelimiter || '*',\n strongDelimiter: options.strongDelimiter || '**',\n linkStyle: options.linkStyle || 'inlined',\n linkReferenceStyle: options.linkReferenceStyle || 'full',\n })\n\n // 添加扩展规则\n addExtensionRules(turndownService)\n\n return turndownService\n}\n\n/**\n * HTML 转 Markdown\n * 使用 linkedom 解析 HTML,兼容 Service Worker 环境\n * 如果 turndown 失败,使用简单正则转换作为回退\n */\nexport function htmlToMarkdown(html: string, _options: TurndownOptions = {}): string {\n // linkedom + turndown has compatibility issues in Service Worker environments\n // Use regex-based conversion instead for reliability\n return htmlToMarkdownSimple(html)\n}\n\n/**\n * 从 className 提取编程语言\n */\nfunction extractLangFromClass(className: string): string {\n if (!className) return ''\n\n const patterns = [\n /language-(\\w+)/i, // language-javascript\n /lang-(\\w+)/i, // lang-js\n /\\bhljs\\s+(\\w+)/i, // hljs javascript\n /\\b(javascript|typescript|python|java|cpp|c|csharp|go|rust|ruby|php|swift|kotlin|scala|sql|html|css|json|xml|yaml|markdown|bash|shell|powershell)\\b/i,\n ]\n\n for (const pattern of patterns) {\n const match = className.match(pattern)\n if (match) {\n return match[1].toLowerCase()\n }\n }\n\n return ''\n}\n\n/**\n * 简单的正则 HTML → Markdown 转换 (回退方案)\n * 增强版:支持表格转换、代码块语言识别、LaTeX 公式\n */\nfunction htmlToMarkdownSimple(html: string): string {\n let md = html\n\n // ============ 预处理:移除微信代码块行号 ============\n // 必须在列表转换之前执行,否则 <li> 会被转成 \"- \"\n // 支持 class=\"code-snippet__line-index\" 和 class='code-snippet__line-index'\n md = md.replace(\n /<ul[^>]*class=[\"'][^\"']*code-snippet__line-index[^\"']*[\"'][^>]*>[\\s\\S]*?<\\/ul>/gi,\n ''\n )\n\n // 表格转换 - 必须在移除其他标签之前处理\n md = convertTables(md)\n\n // 标题\n md = md.replace(/<h1[^>]*>([\\s\\S]*?)<\\/h1>/gi, '\\n# $1\\n')\n md = md.replace(/<h2[^>]*>([\\s\\S]*?)<\\/h2>/gi, '\\n## $1\\n')\n md = md.replace(/<h3[^>]*>([\\s\\S]*?)<\\/h3>/gi, '\\n### $1\\n')\n md = md.replace(/<h4[^>]*>([\\s\\S]*?)<\\/h4>/gi, '\\n#### $1\\n')\n md = md.replace(/<h5[^>]*>([\\s\\S]*?)<\\/h5>/gi, '\\n##### $1\\n')\n md = md.replace(/<h6[^>]*>([\\s\\S]*?)<\\/h6>/gi, '\\n###### $1\\n')\n\n // 粗体和斜体\n md = md.replace(/<(strong|b)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '**$2**')\n md = md.replace(/<(em|i)[^>]*>([\\s\\S]*?)<\\/\\1>/gi, '*$2*')\n\n // 链接\n md = md.replace(/<a[^>]*href=\"([^\"]*)\"[^>]*>([\\s\\S]*?)<\\/a>/gi, '[$2]($1)')\n\n // 图片\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*alt=\"([^\"]*)\"[^>]*\\/?>/gi, '![$2]($1)')\n md = md.replace(/<img[^>]*src=\"([^\"]*)\"[^>]*\\/?>/gi, '![]($1)')\n\n // 代码块 - 增强语言检测\n md = md.replace(/<pre[^>]*>([\\s\\S]*?)<\\/pre>/gi, (match, content) => {\n let language = ''\n\n // 从 pre 标签提取语言\n const preLangMatch = match.match(/<pre[^>]*data-lang(?:uage)?=[\"'](\\w+)[\"']/)\n const preClassMatch = match.match(/<pre[^>]*class=\"([^\"]*)\"/)\n if (preLangMatch) {\n language = preLangMatch[1]\n } else if (preClassMatch) {\n language = extractLangFromClass(preClassMatch[1])\n }\n\n // 从 code 标签提取语言\n const codeLangMatch = content.match(/<code[^>]*data-lang(?:uage)?=[\"'](\\w+)[\"']/)\n const codeClassMatch = content.match(/<code[^>]*class=\"([^\"]*)\"/)\n if (!language) {\n if (codeLangMatch) {\n language = codeLangMatch[1]\n } else if (codeClassMatch) {\n language = extractLangFromClass(codeClassMatch[1])\n }\n }\n\n // 使用安全的代码提取函数\n const text = extractCodeFromHtml(content)\n\n return '\\n```' + language + '\\n' + text + '\\n```\\n'\n })\n\n // 行内代码\n md = md.replace(/<code[^>]*>([\\s\\S]*?)<\\/code>/gi, '`$1`')\n\n // 列表\n md = md.replace(/<ul[^>]*>([\\s\\S]*?)<\\/ul>/gi, '$1\\n')\n md = md.replace(/<ol[^>]*>([\\s\\S]*?)<\\/ol>/gi, '$1\\n')\n md = md.replace(/<li[^>]*>([\\s\\S]*?)<\\/li>/gi, '- $1\\n')\n\n // 段落和换行\n md = md.replace(/<p[^>]*>([\\s\\S]*?)<\\/p>/gi, '\\n$1\\n')\n md = md.replace(/<br\\s*\\/?>/gi, '\\n')\n md = md.replace(/<hr\\s*\\/?>/gi, '\\n---\\n')\n\n // 块引用\n md = md.replace(/<blockquote[^>]*>([\\s\\S]*?)<\\/blockquote>/gi, (_, content) => {\n return '\\n' + content.trim().split('\\n').map((line: string) => '> ' + line).join('\\n') + '\\n'\n })\n\n // LaTeX 公式 - 块级 (mode=display)\n // 注意:$$ 在替换字符串中需要写成 $$$$ 才能输出 $$\n md = md.replace(/<script[^>]*type=[\"']math\\/tex[^\"']*display[^\"']*[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi, '\\n$$$$\\n$1\\n$$$$\\n')\n // LaTeX 公式 - 行内\n md = md.replace(/<script[^>]*type=[\"']math\\/tex[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi, ' $$$$$1$$$$ ')\n\n // 移除其他标签\n md = md.replace(/<\\/?[^>]+(>|$)/g, '')\n\n // 解码 HTML 实体\n md = md.replace(/&amp;/g, '&')\n md = md.replace(/&lt;/g, '<')\n md = md.replace(/&gt;/g, '>')\n md = md.replace(/&quot;/g, '\"')\n md = md.replace(/&#039;/g, \"'\")\n md = md.replace(/&nbsp;/g, ' ')\n\n // 清理多余空行\n md = md.replace(/\\n{3,}/g, '\\n\\n')\n md = md.trim()\n\n return md\n}\n\n/**\n * 将 HTML 表格转换为 Markdown 表格\n */\nfunction convertTables(html: string): string {\n // 先处理 figure 包裹的表格\n html = html.replace(/<figure[^>]*>([\\s\\S]*?)<\\/figure>/gi, (match, figureContent) => {\n // 检查是否包含表格\n if (/<table[^>]*>/i.test(figureContent)) {\n // 提取表格部分,保留 figcaption\n const tableMatch = figureContent.match(/<table[^>]*>([\\s\\S]*?)<\\/table>/i)\n const captionMatch = figureContent.match(/<figcaption[^>]*>([\\s\\S]*?)<\\/figcaption>/i)\n\n if (tableMatch) {\n let result = tableMatch[0] // 返回表格部分\n if (captionMatch) {\n // 添加 caption 作为斜体\n const caption = captionMatch[1].replace(/<[^>]+>/g, '').trim()\n if (caption) {\n result += '\\n*' + caption + '*\\n'\n }\n }\n return result\n }\n }\n return match // 非表格 figure 保持原样\n })\n\n // 匹配整个表格\n return html.replace(/<table[^>]*>([\\s\\S]*?)<\\/table>/gi, (_, tableContent) => {\n // 检查是否有表头(thead 或 第一行全是 th)\n const hasTheadMatch = tableContent.match(/<thead[^>]*>([\\s\\S]*?)<\\/thead>/i)\n const rows: string[][] = []\n const alignments: string[][] = [] // 存储每行每个单元格的对齐方式\n let headerRowIndex = -1\n\n // 提取所有行\n const rowMatches = tableContent.match(/<tr[^>]*>([\\s\\S]*?)<\\/tr>/gi) || []\n\n for (let i = 0; i < rowMatches.length; i++) {\n const rowContent = rowMatches[i]\n const cells: string[] = []\n const rowAligns: string[] = []\n\n // 检查单元格类型(th 或 td)\n const cellMatches = rowContent.match(/<t[hd][^>]*>([\\s\\S]*?)<\\/t[hd]>/gi) || []\n const isHeaderRow = cellMatches.length > 0 && cellMatches.every((c: string) => c.startsWith('<th'))\n\n for (const cellMatch of cellMatches) {\n // 提取单元格内容和对齐属性\n const fullMatch = cellMatch.match(/<t[hd]([^>]*)>([\\s\\S]*?)<\\/t[hd]>/i)\n if (fullMatch) {\n const attrs = fullMatch[1]\n const rawContent = fullMatch[2]\n\n // 提取对齐方式\n const alignMatch = attrs.match(/align=[\"']?(left|center|right)[\"']?/i) ||\n attrs.match(/style=[\"'][^\"']*text-align:\\s*(left|center|right)/i)\n const align = alignMatch ? alignMatch[1].toLowerCase() : ''\n rowAligns.push(align)\n\n // 清理内容\n let content = rawContent\n .replace(/<br\\s*\\/?>/gi, ' ')\n .replace(/<[^>]+>/g, '')\n .replace(/&nbsp;/g, ' ')\n .replace(/&amp;/g, '&')\n .replace(/&lt;/g, '<')\n .replace(/&gt;/g, '>')\n .replace(/\\s+/g, ' ')\n .trim()\n\n // 转义管道符\n content = content.replace(/\\|/g, '\\\\|')\n cells.push(content)\n }\n }\n\n if (cells.length > 0) {\n rows.push(cells)\n alignments.push(rowAligns)\n // 记录表头行(在 thead 中或是第一行全 th)\n if (hasTheadMatch && rowContent.indexOf('<th') !== -1) {\n headerRowIndex = i\n } else if (i === 0 && isHeaderRow) {\n headerRowIndex = 0\n }\n }\n }\n\n // 如果没有行,返回空\n if (rows.length === 0) {\n return ''\n }\n\n // 如果没有找到表头行,将第一行作为表头(降级处理)\n if (headerRowIndex === -1) {\n headerRowIndex = 0\n }\n\n // 转换为 Markdown\n const mdRows: string[] = []\n const colCount = Math.max(...rows.map(r => r.length))\n\n // 获取表头行的对齐信息\n const headerAligns = alignments[headerRowIndex] || []\n\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i]\n // 填充缺失的单元格\n while (row.length < colCount) {\n row.push('')\n }\n\n mdRows.push('| ' + row.join(' | ') + ' |')\n\n // 在表头行之后添加分隔行(带对齐信息)\n if (i === headerRowIndex) {\n const separators = []\n for (let j = 0; j < colCount; j++) {\n const align = headerAligns[j] || ''\n if (align === 'left') {\n separators.push(':---')\n } else if (align === 'center') {\n separators.push(':---:')\n } else if (align === 'right') {\n separators.push('---:')\n } else {\n separators.push('---')\n }\n }\n mdRows.push('| ' + separators.join(' | ') + ' |')\n }\n }\n\n return '\\n\\n' + mdRows.join('\\n') + '\\n\\n'\n })\n}\n\n/**\n * 默认的 Turndown 实例(可复用)\n */\nlet defaultService: TurndownService | null = null\n\n/**\n * 获取默认 Turndown 实例\n */\nexport function getDefaultTurndownService(): TurndownService {\n if (!defaultService) {\n defaultService = createTurndownService()\n }\n return defaultService\n}\n\n// 导出 TurndownService 类型供外部使用\nexport { TurndownService }\n\n/**\n * HTML 转 Markdown(使用原生 DOM)\n * 适用于 Content Script / 页面环境,利用浏览器原生 DOM\n * 比 linkedom 兼容性更好,转换质量更高\n */\nexport function htmlToMarkdownNative(html: string, options: TurndownOptions = {}): string {\n if (typeof document === 'undefined') {\n // 回退到简单正则转换(Service Worker 环境)\n logger.warn('No native DOM, falling back to regex conversion')\n return htmlToMarkdownSimple(html)\n }\n\n try {\n const turndownService = new TurndownService({\n headingStyle: options.headingStyle || 'atx',\n hr: options.hr || '---',\n bulletListMarker: options.bulletListMarker || '-',\n codeBlockStyle: options.codeBlockStyle || 'fenced',\n fence: options.fence || '```',\n emDelimiter: options.emDelimiter || '*',\n strongDelimiter: options.strongDelimiter || '**',\n linkStyle: options.linkStyle || 'inlined',\n linkReferenceStyle: options.linkReferenceStyle || 'full',\n })\n\n // 添加扩展规则\n addExtensionRules(turndownService)\n\n // 使用原生 DOM 解析 HTML\n const container = document.createElement('div')\n container.innerHTML = html\n\n // 预处理:移除微信代码块行号元素(必须在 turndown 之前)\n container.querySelectorAll('ul.code-snippet__line-index, ul[class*=\"code-snippet__line-index\"]')\n .forEach(el => el.remove())\n\n return turndownService.turndown(container)\n } catch (err) {\n logger.error('Native DOM conversion failed:', err)\n return htmlToMarkdownSimple(html)\n }\n}\n\n// ============ Markdown → HTML ============\n\nimport { marked } from 'marked'\n\n/**\n * Markdown 转 HTML\n */\nexport function markdownToHtml(markdown: string): string {\n return marked.parse(markdown, { async: false }) as string\n}\n\n/**\n * HTML 标准化\n * 通过 HTML → Markdown → HTML 往返转换,清理所有多余结构\n */\nexport function normalizeHtml(html: string, options: TurndownOptions = {}): string {\n const markdown = htmlToMarkdown(html, options)\n return markdownToHtml(markdown)\n}\n","/**\n * AWS4-HMAC-SHA256 签名实现\n * 用于字节跳动 ImageX 图片上传服务\n */\n\n// 使用 Web Crypto API 进行 HMAC-SHA256 签名\nasync function hmacSha256(key: ArrayBuffer | Uint8Array, message: string): Promise<ArrayBuffer> {\n // 转换为 Uint8Array 然后创建新的 ArrayBuffer\n const keyBytes = key instanceof Uint8Array ? key : new Uint8Array(key)\n const keyBuffer = new Uint8Array(keyBytes).buffer as ArrayBuffer\n const cryptoKey = await crypto.subtle.importKey(\n 'raw',\n keyBuffer,\n { name: 'HMAC', hash: 'SHA-256' },\n false,\n ['sign']\n )\n return crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(message))\n}\n\n// SHA256 哈希\nasync function sha256(message: string): Promise<string> {\n const msgBuffer = new TextEncoder().encode(message)\n const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)\n return arrayBufferToHex(hashBuffer)\n}\n\n// ArrayBuffer 转 hex 字符串\nfunction arrayBufferToHex(buffer: ArrayBuffer): string {\n return Array.from(new Uint8Array(buffer))\n .map(b => b.toString(16).padStart(2, '0'))\n .join('')\n}\n\n// 格式化日期为 AWS 格式\nfunction formatAmzDate(date: Date): string {\n return date.toISOString().replace(/[:-]|\\.\\d{3}/g, '')\n}\n\n// 格式化日期为 YYYYMMDD 格式\nfunction formatDateStamp(date: Date): string {\n return date.toISOString().slice(0, 10).replace(/-/g, '')\n}\n\nexport interface AWS4SignParams {\n method: string\n url: string\n accessKeyId: string\n secretAccessKey: string\n securityToken?: string\n region?: string\n service?: string\n headers?: Record<string, string>\n body?: string\n}\n\nexport interface AWS4SignResult {\n authorization: string\n amzDate: string\n headers: Record<string, string>\n}\n\n/**\n * 生成 AWS4-HMAC-SHA256 签名\n */\nexport async function signAWS4(params: AWS4SignParams): Promise<AWS4SignResult> {\n const {\n method,\n url,\n accessKeyId,\n secretAccessKey,\n securityToken,\n region = 'cn-north-1',\n service = 'imagex',\n headers = {},\n body = '',\n } = params\n\n const parsedUrl = new URL(url)\n const path = parsedUrl.pathname\n const queryString = parsedUrl.search.slice(1) // 去掉前导 ?\n\n const now = new Date()\n const amzDate = formatAmzDate(now)\n const dateStamp = formatDateStamp(now)\n\n // 规范化查询字符串(按参数名排序)\n const queryParams = new URLSearchParams(queryString)\n const sortedParams = Array.from(queryParams.entries()).sort((a, b) => a[0].localeCompare(b[0]))\n const canonicalQueryString = sortedParams\n .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)\n .join('&')\n\n // 构建签名 headers\n const signedHeadersObj: Record<string, string> = {\n 'x-amz-date': amzDate,\n }\n\n if (securityToken) {\n signedHeadersObj['x-amz-security-token'] = securityToken\n }\n\n // 合并其他 headers\n Object.assign(signedHeadersObj, headers)\n\n // 签名的 header 名称列表(小写,按字母排序)\n const signedHeaderNames = Object.keys(signedHeadersObj)\n .map(k => k.toLowerCase())\n .sort()\n .join(';')\n\n // 规范化 headers(小写 key,按字母排序)\n const canonicalHeaders = Object.entries(signedHeadersObj)\n .map(([k, v]) => `${k.toLowerCase()}:${v.trim()}`)\n .sort()\n .join('\\n') + '\\n'\n\n // 计算 payload 哈希\n const payloadHash = await sha256(body)\n\n // 构建 Canonical Request\n const canonicalRequest = [\n method.toUpperCase(),\n path || '/',\n canonicalQueryString,\n canonicalHeaders,\n signedHeaderNames,\n payloadHash,\n ].join('\\n')\n\n // 构建 String to Sign\n const algorithm = 'AWS4-HMAC-SHA256'\n const credentialScope = `${dateStamp}/${region}/${service}/aws4_request`\n const canonicalRequestHash = await sha256(canonicalRequest)\n\n const stringToSign = [\n algorithm,\n amzDate,\n credentialScope,\n canonicalRequestHash,\n ].join('\\n')\n\n // 计算签名密钥\n const kDate = await hmacSha256(\n new TextEncoder().encode('AWS4' + secretAccessKey),\n dateStamp\n )\n const kRegion = await hmacSha256(kDate, region)\n const kService = await hmacSha256(kRegion, service)\n const kSigning = await hmacSha256(kService, 'aws4_request')\n\n // 计算最终签名\n const signatureBuffer = await hmacSha256(kSigning, stringToSign)\n const signature = arrayBufferToHex(signatureBuffer)\n\n // 构建 Authorization header\n const authorization = `${algorithm} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaderNames}, Signature=${signature}`\n\n // 构建返回的 headers\n const resultHeaders: Record<string, string> = {\n authorization,\n 'x-amz-date': amzDate,\n }\n\n if (securityToken) {\n resultHeaders['x-amz-security-token'] = securityToken\n }\n\n return {\n authorization,\n amzDate,\n headers: resultHeaders,\n }\n}\n\n/**\n * 计算文件的 CRC32 校验值\n */\nexport function crc32(data: Uint8Array): string {\n let crc = 0xFFFFFFFF\n const table = getCRC32Table()\n\n for (let i = 0; i < data.length; i++) {\n crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xFF]\n }\n\n return ((crc ^ 0xFFFFFFFF) >>> 0).toString(16).padStart(8, '0')\n}\n\n// CRC32 查找表(懒加载)\nlet crc32Table: Uint32Array | null = null\n\nfunction getCRC32Table(): Uint32Array {\n if (crc32Table) return crc32Table\n\n crc32Table = new Uint32Array(256)\n for (let i = 0; i < 256; i++) {\n let c = i\n for (let j = 0; j < 8; j++) {\n c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1)\n }\n crc32Table[i] = c\n }\n return crc32Table\n}\n"],"mappings":"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","names":[]}
@@ -4,7 +4,7 @@ import {
4
4
  markdownToHtml,
5
5
  parseMarkdownImages,
6
6
  signAWS4
7
- } from "./chunk-7Q3KKWUB.js";
7
+ } from "./chunk-JFXS3MPU.js";
8
8
 
9
9
  // src/adapters/code-adapter.ts
10
10
  var logger = createLogger("CodeAdapter");
@@ -2211,7 +2211,7 @@ var CSDNAdapter = class extends CodeAdapter {
2211
2211
  }
2212
2212
  resolveTags(article) {
2213
2213
  const tags = article.tags?.map((tag) => tag.trim()).filter(Boolean).slice(0, 5);
2214
- return tags?.length ? tags.join(",") : "Wechatsync";
2214
+ return tags?.length ? tags.join(",") : "article-publish";
2215
2215
  }
2216
2216
  async postSaveArticle(apiPath, headers, payload) {
2217
2217
  const pageText = await this.postSaveArticleFromPage(apiPath, headers, payload);
@@ -6685,4 +6685,4 @@ export {
6685
6685
  ToutiaoAdapter,
6686
6686
  WeixinAdapter
6687
6687
  };
6688
- //# sourceMappingURL=chunk-QM2T4VGM.js.map
6688
+ //# sourceMappingURL=chunk-NKSXZF7J.js.map