[
  {
    "path": ".github/ISSUE_TEMPLATE/01-bug-report.yml",
    "content": "name: Bug Report / 问题报告\ndescription: Report a bug or issue with the extension / 报告扩展的 bug 或问题\ntitle: \"[Bug]: \"\nlabels: [\"bug\", \"triage\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for taking the time to report this bug! Please fill out the information below to help us resolve the issue.\n        \n        感谢您花时间报告此问题！请填写以下信息以帮助我们解决问题。\n  \n  - type: textarea\n    id: description\n    attributes:\n      label: Description / 问题描述\n      description: A clear and concise description of the bug / 清晰简洁地描述问题\n      placeholder: Describe the bug you encountered / 描述您遇到的问题\n    validations:\n      required: true\n  \n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: Steps to Reproduce / 复现步骤\n      description: Detailed steps to reproduce the behavior / 详细的复现步骤\n      placeholder: |\n        1. Go to '...' / 前往 '...'\n        2. Click on '...' / 点击 '...'\n        3. Scroll down to '...' / 滚动到 '...'\n        4. See error / 看到错误\n    validations:\n      required: true\n  \n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: Expected Behavior / 预期行为\n      description: What you expected to happen / 您期望发生什么\n      placeholder: Describe the expected behavior / 描述预期行为\n    validations:\n      required: true\n  \n  - type: textarea\n    id: suggested-implementation\n    attributes:\n      label: Suggested Implementation / 实现建议\n      description: Any suggestions on how to fix this issue (optional) / 对如何修复此问题的建议\n      placeholder: Describe your suggested implementation / 描述您建议的实现方式\n    validations:\n      required: false\n  \n  - type: input\n    id: platform\n    attributes:\n      label: Platform / 平台\n      description: Which platform is affected? / 哪个平台受到影响？\n      placeholder: e.g., CSDN, Zhihu, Juejin, Toutiao / 如：CSDN、知乎、掘金、头条\n    validations:\n      required: true\n  \n  - type: dropdown\n    id: browser\n    attributes:\n      label: Browser / 浏览器\n      description: Which browser are you using? / 您使用的是哪个浏览器？\n      options:\n        - Chrome\n        - Firefox\n        - Edge\n        - Safari\n        - Other / 其他\n    validations:\n      required: true\n  \n  - type: input\n    id: extension-version\n    attributes:\n      label: Extension Version / 扩展版本\n      description: What version of the extension are you running? / 您运行的扩展版本是多少？\n      placeholder: e.g., 1.2.0 / 如：1.2.0\n    validations:\n      required: true\n  \n  - type: textarea\n    id: environment\n    attributes:\n      label: Environment Details / 环境信息\n      description: Any additional environment information / 其他环境信息\n      placeholder: |\n        - OS / 操作系统: [e.g., Windows 10, macOS 13.0]\n        - Browser Version / 浏览器版本: [e.g., Chrome 120.0]\n        - Additional context / 其他信息\n    validations:\n      required: false\n  \n  - type: textarea\n    id: screenshots\n    attributes:\n      label: Screenshots / 截图\n      description: If applicable, add screenshots to help explain your problem / 如有需要，请添加截图帮助说明问题\n      placeholder: Drag and drop images here / 拖放图片到这里\n    validations:\n      required: false\n  \n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context / 补充信息\n      description: Any other context about the problem / 关于此问题的其他补充信息\n    validations:\n      required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/02-feature-request.yml",
    "content": "name: Feature Request / 功能请求\ndescription: Suggest a new feature or enhancement / 建议新功能或改进\ntitle: \"[Feature]: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Thanks for your interest in improving this project! Please describe your feature request below.\n        \n        感谢您对改进本项目的兴趣！请在下方描述您的功能请求。\n  \n  - type: textarea\n    id: problem-description\n    attributes:\n      label: Problem Description / 问题描述\n      description: Is your feature request related to a problem? Describe what the problem is / 您的功能请求是否与某个问题相关？请描述问题\n      placeholder: \"I'm frustrated when... / 我感到困扰的是...\"\n    validations:\n      required: true\n  \n  - type: textarea\n    id: proposed-solution\n    attributes:\n      label: Proposed Solution / 建议的解决方案\n      description: Describe the solution you'd like to see / 描述您希望看到的解决方案\n      placeholder: A clear and concise description of what you want to happen / 清晰简洁地描述您希望实现的功能\n    validations:\n      required: true\n  \n  - type: textarea\n    id: alternatives\n    attributes:\n      label: Alternatives Considered / 考虑过的替代方案\n      description: Describe any alternative solutions or features you've considered / 描述您考虑过的其他解决方案或功能\n      placeholder: What other approaches could solve this problem? / 还有哪些方法可以解决这个问题？\n    validations:\n      required: false\n  \n  - type: dropdown\n    id: feature-type\n    attributes:\n      label: Feature Type / 功能类型\n      description: What type of feature is this? / 这是什么类型的功能？\n      options:\n        - New platform support / 新平台支持\n        - UI/UX improvement / 界面/体验改进\n        - Performance enhancement / 性能优化\n        - Content formatting / 内容格式化\n        - Authentication/Login / 认证/登录\n        - Other / 其他\n    validations:\n      required: true\n  \n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional Context / 补充信息\n      description: Add any other context, screenshots, or examples about the feature request / 添加其他相关信息、截图或示例\n      placeholder: Any mockups, examples, or additional details / 任何原型图、示例或其他细节\n    validations:\n      required: false\n  \n  - type: checkboxes\n    id: willingness\n    attributes:\n      label: Implementation / 实现意愿\n      description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能？\n      options:\n        - label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR\n          required: false\n        - label: I'd prefer to wait for community implementation / 我希望等待社区的实现\n          required: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/03-platform-request.yml",
    "content": "name: Platform Request / 平台支持\ndescription: Request support for a new platform / 请求支持新平台\ntitle: \"[Platform]: \"\nlabels: [\"enhancement\", \"new platform\"]\nbody:\n  - type: input\n    id: platform-name\n    attributes:\n      label: Platform Name / 平台名称\n      description: Name of the platform you want supported / 您希望支持的平台名称\n      placeholder: e.g., 简书、博客园 / e.g., Jianshu, cnblogs\n    validations:\n      required: true\n\n  - type: input\n    id: platform-url\n    attributes:\n      label: Platform URL / 平台网址\n      description: Main URL of the platform / 平台的主要网址\n      placeholder: e.g., https://www.jianshu.com\n    validations:\n      required: true\n\n  - type: textarea\n    id: reason\n    attributes:\n      label: Why this platform? / 为什么需要支持此平台？\n      description: Brief reason for supporting this platform / 简要说明支持此平台的原因\n      placeholder: e.g., Popular blogging platform in China / 如：国内热门博客平台\n    validations:\n      required: false\n\n  - type: checkboxes\n    id: willingness\n    attributes:\n      label: Implementation / 实现意愿\n      description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能？\n      options:\n        - label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR\n          required: false\n        - label: I'd prefer to wait for community implementation / 我希望等待社区的实现\n          required: false\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "## Summary / 概述\n<!-- Provide a brief description of the changes in this PR -->\n<!-- 简要描述此 PR 中的更改 -->\n\n\n\n## Related Issue / 关联 Issue\n<!-- Link to the related issue(s) -->\n<!-- 链接到相关的 issue -->\nCloses #\n\n\n## Type of Change / 更改类型\n<!-- Mark the relevant option with an \"x\" -->\n<!-- 用 \"x\" 标记相关选项 -->\n- [ ] Bug fix / 修复 Bug (non-breaking change that fixes an issue / 修复问题的非破坏性更改)\n- [ ] New feature / 新功能 (non-breaking change that adds functionality / 添加功能的非破坏性更改)\n- [ ] Breaking change / 破坏性更改 (fix or feature that would cause existing functionality to not work as expected / 会导致现有功能无法正常工作的修复或功能)\n- [ ] Documentation update / 文档更新\n- [ ] Performance improvement / 性能优化\n- [ ] Code refactoring / 代码重构\n- [ ] Other / 其他 (please describe / 请描述):\n\n\n## Changes Made / 更改内容\n<!-- Describe the specific changes made in this PR -->\n<!-- 描述此 PR 中的具体更改 -->\n- \n- \n- \n\n\n## Implementation Details / 实现细节\n<!-- Provide technical details about your implementation -->\n<!-- 提供实现的技术细节 -->\n\n**Key Changes / 主要更改:**\n- \n\n**Technical Notes / 技术说明:**\n- \n\n\n## Testing / 测试\n<!-- Describe the testing you performed -->\n<!-- 描述您执行的测试 -->\n\n### Testing Checklist / 测试清单\n- [ ] I have tested this code locally / 我已在本地测试此代码\n- [ ] All existing tests pass / 所有现有测试通过\n- [ ] I have added tests for new functionality / 我已为新功能添加测试\n- [ ] I have tested on the affected platform(s) / 我已在受影响的平台上测试\n- [ ] I have verified the changes work in the target browser(s) / 我已验证更改在目标浏览器中有效\n\n### Manual Testing Steps / 手动测试步骤\n1. \n2. \n3. \n\n\n## Screenshots/Videos / 截图/视频\n<!-- If applicable, add screenshots or videos to demonstrate the changes -->\n<!-- 如适用，请添加截图或视频来展示更改 -->\n\n\n\n## Reviewer Checklist / 审阅者清单\n<!-- For reviewers to verify before merging -->\n<!-- 供审阅者在合并前验证 -->\n- [ ] Code follows the project's style guidelines / 代码遵循项目的风格指南\n- [ ] Changes are well-documented / 更改有良好的文档说明\n- [ ] No breaking changes or clearly documented if present / 无破坏性更改，或已清楚记录\n- [ ] Security implications have been considered / 已考虑安全影响\n- [ ] Performance impact has been evaluated / 已评估性能影响\n- [ ] All discussions have been resolved / 所有讨论已解决\n\n\n## Additional Notes / 补充说明\n<!-- Any additional information that reviewers should know -->\n<!-- 审阅者需要了解的其他信息 -->\n\n\n"
  },
  {
    "path": ".gitignore",
    "content": "# misc\n.DS_Store\n*.zip\n.vscode\n.idea\n.cursor\n.fleet\n.zed\n.windsurf\n.kiro\ndist/\nnode_modules/\n"
  },
  {
    "path": "PRIVACY.md",
    "content": "# Privacy Policy for COSE - 多平台文章同步\n\n**Last Updated: December 13, 2025**\n\n## Overview\n\nCOSE (Create Once, Sync Everywhere) is a browser extension that helps users sync articles from the md.doocs.org Markdown editor to multiple publishing platforms. We are committed to protecting your privacy.\n\n## Data Collection\n\n**We do not collect any personal data.**\n\nCOSE operates entirely locally within your browser. The extension:\n\n- Does **NOT** collect personally identifiable information\n- Does **NOT** collect health, financial, or authentication information\n- Does **NOT** track your browsing history or web activity\n- Does **NOT** send any data to external servers\n- Does **NOT** use analytics or tracking services\n\n## Data Usage\n\nAll data processed by COSE remains on your local device:\n\n- **Article Content**: Your article title, body, and formatting are only read from md.doocs.org and transferred directly to the target publishing platforms within your browser.\n- **Login Status**: The extension checks login cookies on target platforms (CSDN, Juejin, WeChat, etc.) solely to verify if you are logged in. This information is not stored or transmitted.\n- **User Preferences**: COSE does not persist user preferences or settings.\n\n## Permissions Explained\n\n| Permission | Purpose |\n|------------|---------|\n| `tabGroups` | Organize sync tabs into groups |\n| `activeTab` | Temporarily access the current tab when you initiate a sync |\n| `scripting` | Fill article content into platform editors |\n| `cookies` | Check platform login status |\n| `debugger` | Simulate paste events for WeChat editor |\n| `clipboardRead` | Read formatted content (HTML) from the clipboard for syncing |\n| `clipboardWrite` | Write content to the clipboard when needed for syncing |\n\n## Third-Party Services\n\nCOSE interacts with the following third-party publishing platforms only when you explicitly initiate a sync:\n\n- CSDN (csdn.net)\n- Juejin (juejin.cn)\n- WeChat Official Account (mp.weixin.qq.com)\n- And other supported platforms\n\nThese interactions are solely for the purpose of publishing your content. We have no control over the privacy practices of these platforms.\n\n## Data Security\n\nSince no data is collected or transmitted to our servers, there is no risk of data breach from our end. All operations occur locally in your browser.\n\n## Children's Privacy\n\nCOSE is not directed at children under 13 years of age, and we do not knowingly collect information from children.\n\n## Changes to This Policy\n\nWe may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated revision date.\n\n## Contact\n\nIf you have questions about this Privacy Policy, please open an issue at:\n\nhttps://github.com/doocs/cose/issues\n\n## Open Source\n\nCOSE is open source. You can review the complete source code at:\n\nhttps://github.com/doocs/cose\n"
  },
  {
    "path": "README.md",
    "content": "<div align=\"center\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"assets/headerDark.svg\" />\n    <img src=\"assets/headerLight.svg\" alt=\"COSE\" />\n  </picture>\n\n_**C**reate **O**nce **S**ync **E**verywhere_\n\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![Chrome Web Store](https://img.shields.io/badge/Install-Chrome%20Web%20Store-4285F4?logo=googlechrome&logoColor=white)](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk)\n[![YouTube](https://img.shields.io/badge/Video-YouTube-FF0000?logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=KTskiA8Xaj4)\n[![Bilibili](https://img.shields.io/badge/Video-Bilibili-00A1D6?logo=bilibili&logoColor=white)](https://www.bilibili.com/video/BV1ZxqnB1E2C/)\n\n</div>\n\n配合 [doocs/md](https://github.com/doocs/md) Markdown 编辑器使用的浏览器扩展，支持一键将文章同步到多个内容平台。\n\n> 本插件完全本地运行，不收集、不存储任何用户信息。**如需添加更多平台或改善同步准确度，欢迎提 [Issue](https://github.com/doocs/cose/issues) 或 [PR](https://github.com/doocs/cose/pulls)**。\n\n## 使用方法\n\n> 点击观看视频：[![Bilibili](https://img.shields.io/badge/Video-Bilibili-00A1D6?logo=bilibili&logoColor=white)](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [![YouTube](https://img.shields.io/badge/Video-YouTube-FF0000?logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=KTskiA8Xaj4) \n\n1. 先点击安装扩展 [![Chrome Web Store](https://img.shields.io/badge/Install-Chrome%20Web%20Store-4285F4?logo=googlechrome&logoColor=white)](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk) 然后打开 [md.doocs.org](https://md.doocs.org) 或本地开发环境\n2. 编辑 Markdown 内容\n3. 点击顶部的 **发布** 按钮\n4. 在弹出的对话框中选择要同步的平台\n5. 点击 **确定** 开始同步\n\n## 特性\n\n- 编辑一次，同步到多个平台\n- 自动检测各平台登录状态\n- 同步的标签页自动归入分组，便于管理\n- 微信公众号同步时完整保留渲染样式并自动保存为草稿\n\n\n## 已支持的平台\n\n> 更多想要添加的平台欢迎提 [Issue](https://github.com/doocs/cose/issues) ！\n>\n> <details>\n> <summary>已支持平台速查表(点击展开)</summary>\n>\n> | 字母 | 平台 |\n> |:---:|:---|\n> | A | 阿里云社区 |\n> | B | B站专栏、百度云千帆、百家号、博客园 |\n> | C | CSDN |\n> | D | 豆瓣、电子发烧友、抖音文章 |\n> | H | 华为开发者文章、华为云博客、火山引擎社区 |\n> | I | InfoQ |\n> | J | 简书、掘金、今日头条 |\n> | K | 开源中国 |\n> | M | Medium、ModelScope 魔搭社区 |\n> | S | 少数派、搜狐号、思否 |\n> | T | 腾讯云 |\n> | W | 网易号、微博文章、微信公众号 |\n> | X | 小红书长文、X(Formerly Twitter) Articles |\n> | Z | 支付宝开放平台、知乎 |\n> | 5 | 51CTO |\n>\n> </details>\n\n\n\n### 媒体平台\n\n<p align=\"center\">\n<img src=\"https://cdn.simpleicons.org/wechat\" alt=\"微信公众号\" width=\"60\" />\n<img src=\"https://api.iconify.design/icon-park-solid/jinritoutiao.svg?color=%23ED1C24\" alt=\"今日头条\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/zhihu\" alt=\"知乎\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/tiktok\" alt=\"抖音文章\" width=\"60\"/>\n<img src=\"https://cdn.simpleicons.org/xiaohongshu\" alt=\"小红书\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/baidu\" alt=\"百家号\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/neteasecloudmusic\" alt=\"网易号\" width=\"60\" />\n<img src=\"https://favicon.im/sohu.com?larger=true\" alt=\"搜狐号\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/sinaweibo\" alt=\"微博文章\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/bilibili\" alt=\"B站专栏\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/douban\" alt=\"豆瓣\" width=\"55\" />\n<img src=\"https://favicon.im/sspai.com?larger=true\" alt=\"少数派\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/x\" alt=\"X(Formerly Twitter) Articles\" width=\"50\" />\n</p>\n\n\n### 博客平台\n<p align=\"center\">\n<img src=\"https://cdn.simpleicons.org/csdn/FC5531\" alt=\"CSDN\" width=\"80\" />\n<img src=\"https://favicon.im/cnblogs.com?larger=true\" alt=\"博客园\" width=\"60\" />\n<img src=\"https://cdn.simpleicons.org/juejin/1E80FF\" alt=\"掘金\" width=\"60\" />\n<img src=\"https://favicon.im/medium.com?larger=true\" alt=\"Medium\" width=\"60\" />\n<img src=\"https://cdn.brandfetch.io/id5v32DsU5/w/400/h/400/theme/dark/icon.png?c=1bxid64Mup7aczewSAYMX&t=1767149702768\" alt=\"思否\" width=\"60\" />\n<img src=\"https://www.google.com/s2/favicons?domain=infoq.cn&sz=128\" alt=\"InfoQ\" width=\"60\" />\n<img src=\"https://favicon.im/jianshu.com?larger=true\" alt=\"简书\" width=\"60\" />\n<img src=\"https://static.oschina.net/new-osc/img/logo_osc.svg\" alt=\"开源中国\" width=\"120\" />\n<img src=\"https://cdn.brandfetch.io/id4vYQbdaC/w/168/h/62/theme/dark/logo.png?c=1bxid64Mup7aczewSAYMX&t=1743144928777\" alt=\"51CTO\" width=\"80\" />\n</p>\n\n### 云平台及其开发者社区\n<br/>\n\n<p align=\"center\">\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/tencentcloud-color.png\" alt=\"腾讯云\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/tencentcloud-text.png\" alt=\"腾讯云\" width=\"70\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/alibabacloud-color.png\" alt=\"阿里云社区\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/alibabacloud-text.png\" alt=\"阿里云社区\" width=\"140\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/huaweicloud-color.png\" alt=\"华为云博客\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/huaweicloud-text-cn.png\" alt=\"华为云博客\" width=\"70\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/huawei-color.png\" alt=\"华为开发者文章\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/huawei-text.png\" alt=\"华为开发者文章\" width=\"100\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/baiducloud-color.png\" alt=\"百度云千帆\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/baiducloud-text.png\" alt=\"百度云千帆\" width=\"110\" />\n  <br/>\n  <img src=\"https://cdn.simpleicons.org/alipay/1677FF\" alt=\"支付宝开放平台\" width=\"30\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/modelscope-color.png\" alt=\"魔搭社区\" width=\"36\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/modelscope-text.png\" alt=\"魔搭社区\" width=\"160\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/volcengine-color.png\" alt=\"火山引擎\" width=\"36\" />\n  <img src=\"https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/volcengine-text.png\" alt=\"火山引擎\" width=\"90\" />\n  <img src=\"https://www.elecfans.com/static/main/img/elecfans-logo.jpg\" alt=\"电子发烧友\" width=\"35\" />\n</p>\n\n\n## 开发者模式测试\n\n1. 克隆或下载本项目\n2. 打开 Chrome 浏览器，进入 `chrome://extensions/`\n3. 开启右上角的 **开发者模式**\n4. 点击 **加载已解压的扩展程序**\n5. 选择 `cose` 目录\n"
  },
  {
    "path": "apps/extension/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"COSE - 多平台文章同步\",\n  \"description\": \"Create Once, Sync Everywhere. 一键将文章同步到多个平台\",\n  \"permissions\": [\n    \"tabGroups\",\n    \"activeTab\",\n    \"scripting\",\n    \"cookies\",\n    \"debugger\",\n    \"clipboardRead\",\n    \"storage\",\n    \"declarativeNetRequest\",\n    \"offscreen\"\n  ],\n  \"host_permissions\": [\n    \"https://*.csdn.net/*\",\n    \"https://*.juejin.cn/*\",\n    \"https://*.jianshu.com/*\",\n    \"https://*.segmentfault.com/*\",\n    \"https://*.toutiao.com/*\",\n    \"https://*.douban.com/*\",\n    \"https://*.bilibili.com/*\",\n    \"https://*.weibo.com/*\",\n    \"https://*.sinaimg.cn/*\",\n    \"https://mp.weixin.qq.com/*\",\n    \"https://*.zhihu.com/*\",\n    \"https://*.sspai.com/*\",\n    \"https://cdnfile.sspai.com/*\",\n    \"https://*.xueqiu.com/*\",\n    \"https://*.eastmoney.com/*\",\n    \"https://*.wordpress.com/*\",\n    \"https://*.wordpress.org/*\",\n    \"https://md.doocs.org/*\",\n    \"https://*.cnblogs.com/*\",\n    \"https://*.oschina.net/*\",\n    \"https://*.51cto.com/*\",\n    \"https://*.infoq.cn/*\",\n    \"https://*.baijiahao.baidu.com/*\",\n    \"https://*.163.com/*\",\n    \"https://*.cloud.tencent.com/*\",\n    \"https://*.medium.com/*\",\n    \"https://*.sohu.com/*\",\n    \"https://*.aliyun.com/*\",\n    \"https://*.huaweicloud.com/*\",\n    \"https://*.huawei.com/*\",\n    \"https://*.hicloud.com/*\",\n    \"https://*.x.com/*\",\n    \"https://*.twitter.com/*\",\n    \"https://qianfan.cloud.baidu.com/*\",\n    \"https://*.alipay.com/*\",\n    \"https://*.modelscope.cn/*\",\n    \"https://*.alicdn.com/*\",\n    \"https://*.volcengine.com/*\",\n    \"https://*.byteacctimg.com/*\",\n    \"https://*.douyin.com/*\",\n    \"https://*.xiaohongshu.com/*\",\n    \"https://*.elecfans.com/*\"\n  ],\n  \"action\": {\n    \"default_icon\": {\n      \"16\": \"icons/cose_16.png\",\n      \"48\": \"icons/cose_48.png\",\n      \"128\": \"icons/cose_128.png\"\n    },\n    \"default_title\": \"COSE\",\n    \"default_popup\": \"popup.html\"\n  },\n  \"background\": {\n    \"service_worker\": \"bundles/background.js\",\n    \"type\": \"module\"\n  },\n  \"content_scripts\": [\n    {\n      \"matches\": [\n        \"http://*/*\",\n        \"https://*/*\"\n      ],\n      \"js\": [\n        \"bundles/content.js\"\n      ],\n      \"run_at\": \"document_idle\"\n    }\n  ],\n  \"icons\": {\n    \"16\": \"icons/cose_16.png\",\n    \"48\": \"icons/cose_48.png\",\n    \"128\": \"icons/cose_128.png\"\n  },\n  \"web_accessible_resources\": [\n    {\n      \"resources\": [\n        \"bundles/inject.js\",\n        \"bundles/platforms/*.js\"\n      ],\n      \"matches\": [\n        \"<all_urls>\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "apps/extension/package.json",
    "content": "{\n  \"name\": \"cose-extension\",\n  \"version\": \"1.3.4\",\n  \"description\": \"Create Once, Sync Everywhere. 一键将文章同步到多个平台\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"mkdir -p /tmp/chrome-debug-profile && web-ext run --source-dir ./dist --target=chromium --chromium-binary \\\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\\\" --chromium-profile /tmp/chrome-debug-profile --keep-profile-changes\",\n    \"dev:chrome\": \"mkdir -p /tmp/chrome-debug-profile && '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug-profile --load-extension=$(pwd)/dist\",\n    \"dev:watch\": \"node scripts/reload-extension.mjs\",\n    \"build\": \"tsx scripts/cli.ts build\",\n    \"build:firefox\": \"tsx scripts/cli.ts build --target firefox\",\n    \"build:safari\": \"tsx scripts/cli.ts build --target safari\",\n    \"build:release\": \"tsx scripts/cli.ts build --release\",\n    \"watch\": \"tsx scripts/cli.ts build --watch\",\n    \"lint\": \"web-ext lint --source-dir ./dist\"\n  },\n  \"dependencies\": {\n    \"@cose/core\": \"workspace:*\",\n    \"@cose/detection\": \"workspace:*\"\n  },\n  \"devDependencies\": {\n    \"cac\": \"^6.7.14\",\n    \"execa\": \"^9.6.1\",\n    \"rimraf\": \"^5.0.5\",\n    \"tsx\": \"^4.21.0\",\n    \"vite\": \"^5.0.12\",\n    \"vite-plugin-static-copy\": \"^1.0.1\",\n    \"web-ext\": \"^7.11.0\",\n    \"ws\": \"^8.19.0\"\n  }\n}"
  },
  {
    "path": "apps/extension/scripts/cli.ts",
    "content": "import { cac } from 'cac'\nimport { execa } from 'execa'\nimport { build as viteBuild } from 'vite'\nimport fs from 'node:fs/promises'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst dirname = fileURLToPath(new URL('./', import.meta.url))\nconst rootDir = path.join(dirname, '..')\n\n// Read package.json for version\nconst packageJsonPath = path.join(rootDir, 'package.json')\nconst packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))\n\n// Firefox background uses scripts array, Chrome uses service_worker\ninterface FirefoxBackgroundOptions {\n    scripts: string[]\n    type: 'module'\n}\n\ninterface ChromeBackgroundOptions {\n    service_worker: string\n    type: 'module'\n}\n\n// Full manifest type for COSE extension\ninterface Manifest {\n    manifest_version: number\n    name: string\n    version: string\n    description: string\n    permissions: string[]\n    host_permissions: string[]\n    action: {\n        default_icon: Record<string, string>\n        default_title: string\n    }\n    background: FirefoxBackgroundOptions | ChromeBackgroundOptions\n    content_scripts: Array<{\n        matches: string[]\n        js: string[]\n        run_at: string\n    }>\n    icons: Record<string, string>\n    web_accessible_resources: Array<{\n        resources: string[]\n        matches: string[]\n    }>\n    browser_specific_settings?: {\n        gecko: {\n            id: string\n        }\n    }\n}\n\nconst readManifest = async (manifestPath: string): Promise<Manifest | undefined> => {\n    try {\n        const fileContent = await fs.readFile(manifestPath, 'utf8')\n        const json = JSON.parse(fileContent) as Manifest\n        return json\n    } catch (error) {\n        console.error(error)\n        return undefined\n    }\n}\n\ninterface BuildOptions {\n    watch: boolean\n    release: boolean\n    target: 'chromium' | 'firefox' | 'safari'\n    bundleId?: string\n}\n\nconst buildWithVite = async (options: BuildOptions) => {\n    console.log('Building with Vite...')\n\n    await viteBuild({\n        root: rootDir,\n        mode: options.release ? 'production' : 'development',\n        build: {\n            minify: options.release,\n            watch: options.watch ? {} : null,\n        },\n    })\n}\n\nconst copyResources = async () => {\n    console.log('Copying resources...')\n\n    interface CopyEntry {\n        from: string\n        to: string\n    }\n\n    const copyEntries: CopyEntry[] = [\n        {\n            from: path.join(rootDir, 'icons'),\n            to: path.join(rootDir, 'dist/icons'),\n        },\n        {\n            from: path.join(rootDir, 'assets'),\n            to: path.join(rootDir, 'dist/assets'),\n        },\n        {\n            // Copy platform scripts from core package\n            from: path.resolve(rootDir, '../../packages/core/src/platforms'),\n            to: path.join(rootDir, 'dist/bundles/platforms'),\n        },\n    ]\n\n    for (const entry of copyEntries) {\n        try {\n            // Check if source exists\n            await fs.access(entry.from)\n            // Remove destination if exists\n            await fs.rm(entry.to, { recursive: true, force: true })\n            // Copy\n            await fs.cp(entry.from, entry.to, { recursive: true })\n            console.log(`  ✓ Copied ${path.basename(entry.from)}`)\n        } catch (error) {\n            // Source doesn't exist, skip\n            console.log(`  ⚠ Skipped ${path.basename(entry.from)} (not found)`)\n        }\n    }\n}\n\nconst genManifest = async (options: BuildOptions) => {\n    console.log('Generating manifest.json...')\n\n    const manifest = await readManifest(path.join(rootDir, 'manifest.json'))\n\n    if (!manifest) {\n        throw new Error('manifest.json not found')\n    }\n\n    if (!manifest.background) {\n        throw new Error('manifest.background not found')\n    }\n\n    // Firefox-specific adjustments\n    if (options.target === 'firefox' && 'service_worker' in manifest.background) {\n        // Convert service_worker to scripts array for Firefox\n        manifest.background = {\n            scripts: [manifest.background.service_worker],\n            type: 'module',\n        }\n\n        // Add Firefox-specific settings\n        manifest.browser_specific_settings = {\n            gecko: {\n                id: 'cose@doocs.org',\n            },\n        }\n\n        console.log('  ✓ Converted to Firefox manifest format')\n    }\n\n    // Sync version from package.json\n    manifest.version = packageJson.version\n\n    // Write manifest to dist\n    const outputPath = path.join(rootDir, 'dist/manifest.json')\n    await fs.writeFile(\n        outputPath,\n        JSON.stringify(manifest, null, options.release ? undefined : 2)\n    )\n\n    console.log(`  ✓ Generated manifest.json (version: ${manifest.version})`)\n}\n\nconst buildSafariExtension = async (options: BuildOptions) => {\n    console.log('\\nConverting to Safari extension...')\n\n    // Check if xcrun is available (macOS only)\n    try {\n        await execa('xcrun', ['--version'])\n    } catch {\n        throw new Error(\n            'xcrun not found. Safari extension conversion requires:\\n' +\n            '  1. macOS\\n' +\n            '  2. Xcode installed (with Command Line Tools)\\n' +\n            '  3. Run: xcode-select --install'\n        )\n    }\n\n    const safariProjectDir = path.join(rootDir, 'safari-extension')\n    const bundleId = options.bundleId || 'org.doocs.cose'\n\n    // Remove existing Safari project\n    await fs.rm(safariProjectDir, { recursive: true, force: true })\n\n    console.log(`  Bundle ID: ${bundleId}`)\n    console.log(`  Project location: ${safariProjectDir}`)\n\n    try {\n        const result = await execa('xcrun', [\n            'safari-web-extension-converter',\n            path.join(rootDir, 'dist'),\n            '--project-location', safariProjectDir,\n            '--app-name', 'COSE',\n            '--bundle-identifier', bundleId,\n            '--swift',\n            '--no-prompt',\n            '--no-open'\n        ])\n        console.log(result.stdout)\n        console.log('\\n  ✓ Safari extension project created!')\n        console.log(`  ✓ Open in Xcode: open ${safariProjectDir}/COSE/COSE.xcodeproj`)\n    } catch (error: unknown) {\n        const err = error as { stderr?: string; message?: string }\n        console.error('Safari conversion failed:', err.stderr || err.message)\n        throw error\n    }\n}\n\n// CLI setup\nconst cli = cac('cose-build')\ncli.help().version(packageJson.version)\n\ncli\n    .command('build', 'Build the COSE browser extension')\n    .option('-w, --watch', 'Watch mode', { default: false })\n    .option('-r, --release', 'Build in release mode with optimizations', { default: false })\n    .option('--target <target>', 'Browser target: \"chromium\", \"firefox\", or \"safari\"', { default: 'chromium' })\n    .option('--bundle-id <bundleId>', 'Bundle ID for Safari (default: org.doocs.cose)')\n    .action(async (options: BuildOptions) => {\n        const validTargets = ['chromium', 'firefox', 'safari']\n        if (!validTargets.includes(options.target)) {\n            throw new Error(`Invalid target: ${options.target}. Use \"chromium\", \"firefox\", or \"safari\".`)\n        }\n\n        console.log(`\\n=== COSE Build (target: ${options.target}, release: ${options.release}) ===\\n`)\n\n        // Step 1: Build with Vite\n        await buildWithVite(options)\n\n        // Step 2: Copy resources\n        await copyResources()\n\n        // Step 3: Generate manifest\n        await genManifest(options)\n\n        // Step 4: Target-specific post-processing\n        if (options.target === 'firefox') {\n            console.log('\\nRunning web-ext lint...')\n            try {\n                const result = await execa('pnpm', ['exec', 'web-ext', 'lint', '--source-dir', 'dist'])\n                console.log(result.stdout)\n            } catch (error) {\n                console.error('web-ext lint failed:', error)\n            }\n        } else if (options.target === 'safari') {\n            await buildSafariExtension(options)\n        }\n\n        console.log(`\\n=== Build complete! ===\\n`)\n    })\n\ncli.parse(process.argv, { run: false })\nawait cli.runMatchedCommand()\n"
  },
  {
    "path": "apps/extension/scripts/convert-icons.mjs",
    "content": "#!/usr/bin/env node\n/**\n * 将 SVG 图标转换为 PNG\n * 需要安装: npm install sharp\n */\nimport { readFileSync, writeFileSync, existsSync } from 'fs'\nimport { join, dirname } from 'path'\nimport { fileURLToPath } from 'url'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst iconsDir = join(__dirname, '..', 'icons')\n\n// 简单的 SVG 转 PNG 占位符生成（纯绿色方块带 M 字母）\nfunction createPlaceholderPng(size) {\n  // 创建一个简单的 PNG 占位符\n  // 这是一个最小的有效 PNG 文件（绿色方块）\n  const header = Buffer.from([\n    0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature\n  ])\n  \n  console.log(`请使用以下命令安装 sharp 并重新运行，或手动转换 SVG:`)\n  console.log(`  npm install sharp`)\n  console.log(`  或使用在线工具: https://svgtopng.com/`)\n  return null\n}\n\nasync function main() {\n  const sizes = [16, 48, 128]\n  \n  console.log('SVG 图标转换工具')\n  console.log('=================')\n  console.log('')\n  \n  try {\n    // 尝试动态导入 sharp\n    const sharp = await import('sharp')\n    \n    for (const size of sizes) {\n      const svgPath = join(iconsDir, `icon${size}.svg`)\n      const pngPath = join(iconsDir, `icon${size}.png`)\n      \n      if (!existsSync(svgPath)) {\n        console.log(`跳过: ${svgPath} 不存在`)\n        continue\n      }\n      \n      const svgBuffer = readFileSync(svgPath)\n      await sharp.default(svgBuffer)\n        .resize(size, size)\n        .png()\n        .toFile(pngPath)\n      \n      console.log(`✓ 已转换: icon${size}.svg -> icon${size}.png`)\n    }\n    \n    console.log('')\n    console.log('✔ 图标转换完成')\n  } catch (e) {\n    if (e.code === 'ERR_MODULE_NOT_FOUND') {\n      console.log('sharp 模块未安装，请运行:')\n      console.log('  npm install sharp')\n      console.log('')\n      console.log('或者手动转换 SVG 文件:')\n      sizes.forEach(size => {\n        console.log(`  - icons/icon${size}.svg -> icons/icon${size}.png`)\n      })\n      console.log('')\n      console.log('在线工具: https://svgtopng.com/')\n    } else {\n      throw e\n    }\n  }\n}\n\nmain().catch(console.error)\n"
  },
  {
    "path": "apps/extension/scripts/reload-extension.mjs",
    "content": "#!/usr/bin/env node\n/**\n * 监听 dist 目录变化，自动刷新 Chrome 扩展\n */\n\nimport { watch } from 'fs'\nimport { join, dirname } from 'path'\nimport { fileURLToPath } from 'url'\nimport WebSocket from 'ws'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url))\nconst distDir = join(__dirname, '..', 'dist')\nconst CDP_URL = 'http://127.0.0.1:9222'\n\nlet reloadTimeout = null\n\nfunction ts() {\n  return new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })\n}\n\nasync function reloadExtension() {\n    try {\n        const res = await fetch(`${CDP_URL}/json/list`)\n        const pages = await res.json()\n\n        const extPage = pages.find(p => p.url.includes('chrome://extensions'))\n        if (!extPage) {\n            console.log(`[reload ${ts()}] 未找到 chrome://extensions 页面，请打开该页面`)\n            return\n        }\n\n        console.log(`[reload ${ts()}] 正在重新加载扩展...`)\n\n        const ws = new WebSocket(extPage.webSocketDebuggerUrl)\n\n        await new Promise((resolve) => {\n            ws.on('open', () => {\n                // 在 extensions 页面中找到 COSE 扩展并点击刷新按钮\n                ws.send(JSON.stringify({\n                    id: 1,\n                    method: 'Runtime.evaluate',\n                    params: {\n                        expression: `\n              (async () => {\n                // 获取 extensions-manager\n                const manager = document.querySelector('extensions-manager');\n                if (!manager) return 'no-manager';\n                \n                // 获取 extensions-item-list\n                const itemList = manager.shadowRoot.querySelector('extensions-item-list');\n                if (!itemList) return 'no-item-list';\n                \n                // 获取所有扩展卡片\n                const items = itemList.shadowRoot.querySelectorAll('extensions-item');\n                \n                for (const item of items) {\n                  const name = item.shadowRoot.querySelector('#name')?.textContent || '';\n                  if (name.includes('COSE') || name.includes('多平台')) {\n                    // 找到刷新按钮并点击\n                    const reloadBtn = item.shadowRoot.querySelector('#dev-reload-button');\n                    if (reloadBtn) {\n                      reloadBtn.click();\n                      return 'ok';\n                    }\n                    return 'no-reload-btn';\n                  }\n                }\n                return 'not-found';\n              })()\n            `,\n                        awaitPromise: true\n                    }\n                }))\n            })\n\n            ws.on('message', (data) => {\n                const msg = JSON.parse(data.toString())\n                if (msg.id === 1) {\n                    const result = msg.result?.result?.value\n                    if (result === 'ok') {\n                        console.log(`[reload ${ts()}] ✓ 扩展已重新加载`)\n                    } else if (result === 'no-reload-btn') {\n                        console.log(`[reload ${ts()}] ⚠ 未找到刷新按钮，请开启 Developer mode`)\n                    } else if (result === 'not-found') {\n                        console.log(`[reload ${ts()}] ⚠ 未找到 COSE 扩展`)\n                    } else {\n                        console.log(`[reload ${ts()}] ⚠ 刷新失败:`, result)\n                    }\n                    ws.close()\n                    resolve()\n                }\n            })\n\n            ws.on('error', (e) => {\n                console.log(`[reload ${ts()}] 连接错误:`, e.message)\n                resolve()\n            })\n\n            setTimeout(() => { ws.close(); resolve() }, 3000)\n        })\n    } catch (e) {\n        console.log(`[reload ${ts()}] 失败:`, e.message)\n    }\n}\n\nfunction debounceReload() {\n    if (reloadTimeout) clearTimeout(reloadTimeout)\n    reloadTimeout = setTimeout(reloadExtension, 800)\n}\n\nconsole.log(`[reload ${ts()}] 监听 ${distDir} 目录变化...`)\nconsole.log('[reload] 确保:')\nconsole.log('  1. Chrome 已用 --remote-debugging-port=9222 启动')\nconsole.log('  2. chrome://extensions 页面已打开')\nconsole.log('  3. Developer mode 已开启')\n\nwatch(distDir, { recursive: true }, (eventType, filename) => {\n    if (filename && !filename.includes('.DS_Store') && !filename.includes('_metadata')) {\n        console.log(`[reload ${ts()}] 检测到变化: ${filename}`)\n        debounceReload()\n    }\n})\n\nprocess.on('SIGINT', () => {\n    console.log(`\\n[reload ${ts()}] 已停止`)\n    process.exit(0)\n})\n"
  },
  {
    "path": "apps/extension/src/background.js",
    "content": "// 平台配置\nimport { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS } from '@cose/core/src/platforms/index.js'\nimport { qianfanIntercept } from '@cose/core/src/platforms/qianfan.js'\nimport { convertAvatarToBase64 } from '@cose/detection/src/utils.js'\n// [DISABLED] import { fillAlipayOpenContent } from '@cose/core/src/platforms/alipayopen.js'\n\n// ===== Offscreen helper =====\n// Used for login detection in document context (cookies sent automatically)\n\nasync function ensureOffscreen() {\n  try {\n    const existing = await chrome.offscreen.hasDocument()\n    if (!existing) {\n      await chrome.offscreen.createDocument({\n        url: 'offscreen.html',\n        reasons: ['DOM_SCRAPING'],\n        justification: 'Fetch with credentials in document context for login detection',\n      })\n      // Wait for the offscreen document's scripts to load and register listeners.\n      // createDocument resolves when the document is created, but scripts may not\n      // have executed yet. Ping until the offscreen listener responds.\n      const ready = await _waitForOffscreenReady(3000)\n      if (!ready) {\n        console.warn('[COSE] ensureOffscreen: offscreen document did not become ready in time')\n      }\n    }\n  } catch (e) {\n    console.log('[COSE] ensureOffscreen error:', e.message)\n  }\n}\n\n/**\n * Ping the offscreen document until it responds, confirming its listener is active.\n */\nasync function _waitForOffscreenReady(timeoutMs = 3000) {\n  const start = Date.now()\n  while (Date.now() - start < timeoutMs) {\n    try {\n      const resp = await chrome.runtime.sendMessage({ type: 'OFFSCREEN_PING' })\n      if (resp && resp.pong) return true\n    } catch (e) {\n      // listener not ready yet\n    }\n    await new Promise(r => setTimeout(r, 50))\n  }\n  return false\n}\n\n/**\n * Warm-up fetch via offscreen document.\n * This triggers the browser's cookie restoration (SSO, session cookies)\n * by making a fetch with credentials: 'include' in a document context.\n */\nasync function warmUpFetch(url) {\n  try {\n    const result = await sendOffscreenMessage({\n      type: 'OFFSCREEN_WARM_FETCH',\n      payload: { url },\n    })\n    console.log(`[COSE] Warm-up fetch ${url}: status=${result?.data?.status}`)\n    return result\n  } catch (e) {\n    console.log(`[COSE] Warm-up fetch failed for ${url}:`, e.message)\n    return null\n  }\n}\n\n/**\n * API fetch via offscreen document.\n * Makes a fetch with credentials: 'include' in a document context,\n * so cookies are automatically attached (unlike service worker fetch which strips Cookie headers).\n */\nasync function offscreenApiFetch(url, options = {}) {\n  try {\n    const result = await sendOffscreenMessage({\n      type: 'OFFSCREEN_API_FETCH',\n      payload: { url, ...options },\n    })\n    console.log(`[COSE] Offscreen API fetch ${url}: status=${result?.data?.status}`)\n    return result?.data || null\n  } catch (e) {\n    console.log(`[COSE] Offscreen API fetch failed for ${url}:`, e.message)\n    return null\n  }\n}\n\n// Export for use by detection modules\nglobalThis.__coseWarmUpFetch = warmUpFetch\nglobalThis.__coseOffscreenApiFetch = offscreenApiFetch\n\n/**\n * Serialized message sender for offscreen document.\n * chrome.runtime.sendMessage is broadcast-based; when multiple OFFSCREEN_*\n * messages are sent concurrently, responses can get mixed up or lost.\n * This queue ensures only one offscreen message is in-flight at a time.\n * Includes a timeout to prevent the queue from getting stuck if the offscreen\n * document is garbage-collected or fails to respond.\n */\nlet _offscreenQueue = Promise.resolve()\nfunction sendOffscreenMessage(msg, timeoutMs = 15000) {\n  const p = _offscreenQueue.then(async () => {\n    await ensureOffscreen()\n    // Race the actual message against a timeout so the queue never gets stuck\n    return Promise.race([\n      chrome.runtime.sendMessage(msg),\n      new Promise((_, reject) =>\n        setTimeout(() => reject(new Error(`Offscreen message timeout (${msg.type})`)), timeoutMs)\n      ),\n    ])\n  })\n  // Chain but don't let errors break the queue\n  _offscreenQueue = p.catch(() => {})\n  return p\n}\n\n/**\n * Execute a fetch in the context of a target site's tab.\n * This is needed for sites whose auth cookies are SameSite=Lax (default),\n * which won't be sent from cross-site contexts like offscreen documents.\n * \n * Strategy: find an existing tab for the domain, or create a temporary one,\n * then inject a script that makes the fetch with credentials: 'include'.\n */\nasync function tabContextFetch(siteUrl, apiUrl, options = {}) {\n  const { responseType = 'json', timeout = 15000 } = options\n  let createdTabId = null\n  try {\n    const urlObj = new URL(siteUrl)\n    const pattern = `*://*.${urlObj.hostname.replace(/^www\\./, '')}/*`\n    console.log(`[COSE] tabContextFetch: looking for tabs matching ${pattern}`)\n\n    // Find existing tab\n    let tabs = await chrome.tabs.query({ url: pattern })\n    let tab = tabs.find(t => t.id && !t.discarded)\n    console.log(`[COSE] tabContextFetch: found ${tabs.length} tabs, usable: ${tab ? tab.id : 'none'}`)\n\n    if (!tab) {\n      // Create a background tab (not active, for other platforms that need it)\n      const newTab = await chrome.tabs.create({ url: siteUrl, active: false })\n      tab = newTab\n      createdTabId = tab.id\n      console.log(`[COSE] tabContextFetch: created background tab ${tab.id}`)\n      // Wait for the tab to finish loading\n      const currentTab = await chrome.tabs.get(tab.id)\n      if (currentTab.status !== 'complete') {\n        await new Promise((resolve, reject) => {\n          const timer = setTimeout(() => {\n            chrome.tabs.onUpdated.removeListener(listener)\n            reject(new Error('Tab load timeout'))\n          }, timeout)\n          const listener = (tabId, info) => {\n            if (tabId === tab.id && info.status === 'complete') {\n              chrome.tabs.onUpdated.removeListener(listener)\n              clearTimeout(timer)\n              resolve()\n            }\n          }\n          chrome.tabs.onUpdated.addListener(listener)\n        })\n      }\n    }\n\n    // Inject script to make the fetch in the page's main world\n    // so that credentials: 'include' sends the page's cookies\n    const results = await chrome.scripting.executeScript({\n      target: { tabId: tab.id },\n      func: async (fetchUrl, respType) => {\n        try {\n          const resp = await fetch(fetchUrl, {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': respType === 'json' ? 'application/json' : 'text/html' },\n          })\n          const status = resp.status\n          const finalUrl = resp.url\n          let body = null\n          if (respType === 'json') {\n            try { body = await resp.json() } catch (e) { body = null }\n          } else {\n            body = await resp.text()\n          }\n          return { status, url: finalUrl, body }\n        } catch (e) {\n          return { error: e.message }\n        }\n      },\n      args: [apiUrl, responseType],\n      world: 'MAIN',\n    })\n\n    // Clean up created tab\n    if (createdTabId) {\n      try { await chrome.tabs.remove(createdTabId) } catch (e) { /* ignore */ }\n    }\n\n    console.log(`[COSE] tabContextFetch result:`, JSON.stringify(results?.[0]?.result).substring(0, 200))\n    return results?.[0]?.result || null\n  } catch (e) {\n    // Clean up on error\n    if (createdTabId) {\n      try { await chrome.tabs.remove(createdTabId) } catch (e2) { /* ignore */ }\n    }\n    console.log(`[COSE] tabContextFetch failed for ${apiUrl}:`, e.message)\n    return null\n  }\n}\n\nglobalThis.__coseTabContextFetch = tabContextFetch\n\n/**\n * 51CTO detection via offscreen document (爱贝壳 approach).\n * Offscreen has document context so DOMParser is available.\n * Includes retry logic in case the offscreen document wasn't ready on first attempt.\n */\nasync function detectCto51ViaOffscreen() {\n  for (let attempt = 1; attempt <= 2; attempt++) {\n    try {\n      console.log(`[COSE] 51CTO: Sending OFFSCREEN_DETECT_CTO51 (attempt ${attempt})...`)\n      const result = await sendOffscreenMessage({\n        type: 'OFFSCREEN_DETECT_CTO51',\n      })\n      console.log('[COSE] 51CTO: Offscreen response:', JSON.stringify(result))\n      if (result === undefined || result === null) {\n        // No listener responded — offscreen might not be ready\n        console.warn('[COSE] 51CTO: Got empty response, offscreen may not be ready')\n        if (attempt < 2) {\n          await new Promise(r => setTimeout(r, 500))\n          continue\n        }\n      }\n      return result?.data || null\n    } catch (e) {\n      console.log(`[COSE] 51CTO offscreen detection failed (attempt ${attempt}):`, e.message)\n      if (attempt < 2) {\n        await new Promise(r => setTimeout(r, 500))\n        continue\n      }\n      return null\n    }\n  }\n  return null\n}\n\nglobalThis.__coseDetectCto51 = detectCto51ViaOffscreen\n\n/**\n * Cnblogs detection via offscreen document.\n * Offscreen has document context so cookies are sent automatically.\n */\nasync function detectCnblogsViaOffscreen() {\n  try {\n    console.log('[COSE] Cnblogs: Sending OFFSCREEN_DETECT_CNBLOGS message...')\n    const result = await sendOffscreenMessage({\n      type: 'OFFSCREEN_DETECT_CNBLOGS',\n    })\n    console.log('[COSE] Cnblogs: Offscreen response:', JSON.stringify(result))\n    return result?.data || null\n  } catch (e) {\n    console.log('[COSE] Cnblogs offscreen detection failed:', e.message)\n    return null\n  }\n}\n\nglobalThis.__coseDetectCnblogs = detectCnblogsViaOffscreen\n\n/**\n * Xiaohongshu detection via offscreen document.\n * Offscreen has document context so cookies are sent automatically.\n */\nasync function detectXiaohongshuViaOffscreen() {\n  try {\n    console.log('[COSE] Xiaohongshu: Sending OFFSCREEN_DETECT_XIAOHONGSHU message...')\n    const result = await sendOffscreenMessage({\n      type: 'OFFSCREEN_DETECT_XIAOHONGSHU',\n    })\n    console.log('[COSE] Xiaohongshu: Offscreen response:', JSON.stringify(result))\n    return result?.data || null\n  } catch (e) {\n    console.log('[COSE] Xiaohongshu offscreen detection failed:', e.message)\n    return null\n  }\n}\n\nglobalThis.__coseDetectXiaohongshu = detectXiaohongshuViaOffscreen\n\n// 初始化动态规则：为 sinaimg 和 sspai 头像添加 CORS 头\nasync function initDynamicRules() {\n  try {\n    // 先移除已有的规则\n    const existingRules = await chrome.declarativeNetRequest.getDynamicRules()\n    const existingIds = existingRules.map(r => r.id)\n    if (existingIds.length > 0) {\n      await chrome.declarativeNetRequest.updateDynamicRules({\n        removeRuleIds: existingIds\n      })\n    }\n\n    // 添加新规则\n    await chrome.declarativeNetRequest.updateDynamicRules({\n      addRules: [\n        {\n          id: 1,\n          priority: 100,\n          action: {\n            type: 'modifyHeaders',\n            requestHeaders: [\n              { header: 'Referer', operation: 'set', value: 'https://weibo.com/' },\n              { header: 'Origin', operation: 'set', value: 'https://weibo.com' }\n            ],\n            responseHeaders: [\n              { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }\n            ]\n          },\n          condition: {\n            urlFilter: '*sinaimg.cn*',\n            resourceTypes: ['image', 'xmlhttprequest']\n          }\n        },\n        {\n          id: 2,\n          priority: 100,\n          action: {\n            type: 'modifyHeaders',\n            requestHeaders: [\n              { header: 'Referer', operation: 'set', value: 'https://sspai.com/' },\n              { header: 'Origin', operation: 'set', value: 'https://sspai.com' }\n            ],\n            responseHeaders: [\n              { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }\n            ]\n          },\n          condition: {\n            urlFilter: '*cdnfile.sspai.com*',\n            resourceTypes: ['image', 'xmlhttprequest']\n          }\n        }\n      ]\n    })\n    console.log('[COSE] 动态规则初始化完成')\n  } catch (e) {\n    console.error('[COSE] 动态规则初始化失败:', e)\n  }\n}\n\n// 扩展安装/更新/启动时初始化规则\nchrome.runtime.onInstalled.addListener(() => {\n  initDynamicRules()\n})\nchrome.runtime.onStartup.addListener(() => {\n  initDynamicRules()\n})\n\n// 当前同步任务的 Tab Group ID\nlet currentSyncGroupId = null\n// 存储平台用户信息\nconst PLATFORM_USER_INFO = {}\n\n// 获取或创建同步标签组\nasync function getOrCreateSyncGroup(windowId) {\n  // 如果已有 group 且仍然有效，直接返回\n  if (currentSyncGroupId !== null) {\n    try {\n      const groups = await chrome.tabGroups.query({ windowId })\n      const existingGroup = groups.find(g => g.id === currentSyncGroupId)\n      if (existingGroup) {\n        return currentSyncGroupId\n      }\n    } catch (e) {\n      // Group 不存在，需要创建新的\n    }\n  }\n\n  // 创建新的标签组（先创建一个空组是不行的，需要先有 tab）\n  currentSyncGroupId = null\n  return null\n}\n\n// 将标签添加到同步组\nasync function addTabToSyncGroup(tabId, windowId) {\n  try {\n    if (currentSyncGroupId === null) {\n      // 创建新组\n      currentSyncGroupId = await chrome.tabs.group({ tabIds: tabId })\n      // 设置组的样式，使用时间戳作为标题\n      const now = new Date()\n      const timestamp = `${now.getMonth() + 1}/${now.getDate()} ${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`\n      await chrome.tabGroups.update(currentSyncGroupId, {\n        title: `${timestamp}`,\n        color: 'blue',\n        collapsed: false,\n      })\n    } else {\n      // 添加到现有组\n      await chrome.tabs.group({ tabIds: tabId, groupId: currentSyncGroupId })\n    }\n  } catch (error) {\n    console.error('[COSE] 添加标签到组失败:', error)\n  }\n}\n\n// 登录检测配置\n// 登录检测配置由 import 导入\n\nasync function logToStorage(msg, data = null) {\n  try {\n    const timestamp = new Date().toISOString()\n    const logMsg = data ? `${msg} ${JSON.stringify(data)}` : msg\n    const { debug_logs = [] } = await chrome.storage.local.get('debug_logs')\n    debug_logs.push(`[${timestamp}] ${logMsg}`)\n    if (debug_logs.length > 500) debug_logs.shift()\n    await chrome.storage.local.set({ debug_logs })\n  } catch (e) {\n    console.error('Error logging to storage:', e)\n  }\n  console.log(msg, data || '')\n}\n\n// 消息监听\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  // Let OFFSCREEN_* messages pass through to the offscreen document listener.\n  // If we handle them here, sendResponse fires before the offscreen doc can reply.\n  if (request.type && request.type.startsWith('OFFSCREEN_')) {\n    return false\n  }\n\n  if (request.type === 'GET_DEBUG_LOGS') {\n    chrome.storage.local.get('debug_logs', (result) => {\n      sendResponse({ logs: result.debug_logs || [] })\n    })\n    return true\n  }\n\n  (async () => {\n    try {\n      const result = await handleMessage(request, sender)\n      sendResponse(result)\n    } catch (err) {\n      console.error('[COSE] 消息处理错误:', err)\n      sendResponse({ error: err.message || '未知错误' })\n    }\n  })()\n  return true // 表示异步响应\n})\n\nasync function handleMessage(request, sender) {\n  console.log(`[COSE] handleMessage received type: ${request.type}`, request)\n  switch (request.type) {\n    case 'GET_PLATFORMS':\n      return { platforms: PLATFORMS }\n    case 'CHECK_PLATFORM_STATUS':\n      return { status: await checkAllPlatforms(request.platforms || PLATFORMS) }\n    case 'CHECK_PLATFORM_STATUS_PROGRESSIVE':\n      // 渐进式检测：每个平台检测完成后立即返回结果\n      checkAllPlatformsProgressive(request.platforms || PLATFORMS, sender.tab?.id)\n      return { started: true, total: (request.platforms || PLATFORMS).length }\n    case 'START_SYNC_BATCH':\n      // 开始新的同步批次，重置 tab group\n      currentSyncGroupId = null\n      return { success: true }\n    case 'SYNC_TO_PLATFORM':\n      return await syncToPlatform(request.platformId, request.content)\n    case 'CACHE_USER_INFO':\n      // 缓存用户信息\n      if (request.platform === 'xiaohongshu' && request.userInfo) {\n        await chrome.storage.local.set({ xiaohongshu_user: request.userInfo })\n        console.log('[COSE] 小红书用户信息已缓存:', request.userInfo.username)\n      } else if (request.platform === 'alipayopen' && request.userInfo) {\n        await chrome.storage.local.set({ alipayopen_user: request.userInfo })\n        console.log('[COSE] 支付宝用户信息已缓存:', request.userInfo.username)\n      } else if (request.platform === 'huaweicloud' && request.userInfo) {\n        const hwcInfo = { ...request.userInfo }\n        if (hwcInfo.avatar && hwcInfo.avatar.startsWith('http')) {\n          hwcInfo.avatar = await convertAvatarToBase64(hwcInfo.avatar, 'https://bbs.huaweicloud.com/')\n        }\n        await chrome.storage.local.set({ huaweicloud_user: hwcInfo })\n        console.log('[COSE] 华为云用户信息已缓存:', hwcInfo.username)\n      } else if (request.platform === 'huaweidev' && request.userInfo) {\n        const hwdInfo = { ...request.userInfo }\n        if (hwdInfo.avatar && hwdInfo.avatar.startsWith('http')) {\n          hwdInfo.avatar = await convertAvatarToBase64(hwdInfo.avatar, 'https://developer.huawei.com/')\n        }\n        await chrome.storage.local.set({ huaweidev_user: hwdInfo })\n        console.log('[COSE] 华为开发者用户信息已缓存:', hwdInfo.username)\n      }\n      return { success: true }\n    default:\n      return { error: 'Unknown message type' }\n  }\n}\n\n// 检查所有平台登录状态\nasync function checkAllPlatforms(platforms) {\n  const status = {}\n  try {\n    // 过滤掉无效的平台配置\n    const validPlatforms = (platforms || []).filter(p => p && p.id)\n    const results = await Promise.allSettled(\n      validPlatforms.map(async (platform) => {\n        try {\n          const result = await checkPlatformLogin(platform)\n          return { id: platform.id, result }\n        } catch (e) {\n          return { id: platform.id, result: { loggedIn: false, error: e.message } }\n        }\n      })\n    )\n    results.forEach((res) => {\n      if (res.status === 'fulfilled' && res.value?.id) {\n        status[res.value.id] = res.value.result\n      }\n    })\n  } catch (e) {\n    console.error('[COSE] 检查平台状态失败:', e)\n  }\n  return status\n}\n\n// 渐进式检查所有平台登录状态（每个平台完成后立即返回结果）\nasync function checkAllPlatformsProgressive(platforms, tabId) {\n  const validPlatforms = (platforms || []).filter(p => p && p.id)\n  let completed = 0\n  const total = validPlatforms.length\n\n  // 并行检查所有平台，每个完成后立即发送结果\n  const promises = validPlatforms.map(async (platform) => {\n    try {\n      const result = await checkPlatformLogin(platform)\n      completed++\n\n      // 通过 content script 发送单个平台结果回页面\n      if (tabId) {\n        try {\n          await chrome.tabs.sendMessage(tabId, {\n            type: 'PLATFORM_STATUS_UPDATE',\n            platformId: platform.id,\n            platform: platform,\n            result: result,\n            completed: completed,\n            total: total\n          })\n        } catch (e) {\n          console.log('[COSE] 发送平台状态更新失败:', platform.id, e.message)\n        }\n      }\n\n      return { id: platform.id, result }\n    } catch (e) {\n      completed++\n      const errorResult = { loggedIn: false, error: e.message }\n\n      if (tabId) {\n        try {\n          await chrome.tabs.sendMessage(tabId, {\n            type: 'PLATFORM_STATUS_UPDATE',\n            platformId: platform.id,\n            platform: platform,\n            result: errorResult,\n            completed: completed,\n            total: total\n          })\n        } catch (e2) {\n          console.log('[COSE] 发送平台状态更新失败:', platform.id, e2.message)\n        }\n      }\n\n      return { id: platform.id, result: errorResult }\n    }\n  })\n\n  // 等待所有完成后发送完成消息\n  await Promise.allSettled(promises)\n\n  if (tabId) {\n    try {\n      await chrome.tabs.sendMessage(tabId, {\n        type: 'PLATFORM_STATUS_COMPLETE',\n        total: total\n      })\n    } catch (e) {\n      console.log('[COSE] 发送完成消息失败:', e.message)\n    }\n  }\n}\n\nimport { detectUser } from '@cose/detection'\n\n// 检查单个平台登录状态\nasync function checkPlatformLogin(platform) {\n  if (!platform || !platform.id) {\n    return { loggedIn: false, error: '无效的平台配置' }\n  }\n  return await detectUser(platform.id)\n}\nasync function pasteWithDebugger(tabId) {\n  const debuggee = { tabId }\n\n  try {\n    // 附加调试器\n    await chrome.debugger.attach(debuggee, '1.3')\n    console.log('[COSE] Debugger attached')\n\n    // 发送 Ctrl/Cmd 按下\n    await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {\n      type: 'keyDown',\n      modifiers: 2, // Ctrl\n      windowsVirtualKeyCode: 17,\n      code: 'ControlLeft',\n      key: 'Control'\n    })\n\n    // 发送 V 按下（带 Ctrl 修饰符）\n    await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {\n      type: 'keyDown',\n      modifiers: 2, // Ctrl\n      windowsVirtualKeyCode: 86,\n      code: 'KeyV',\n      key: 'v'\n    })\n\n    // 发送 V 释放\n    await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {\n      type: 'keyUp',\n      modifiers: 2,\n      windowsVirtualKeyCode: 86,\n      code: 'KeyV',\n      key: 'v'\n    })\n\n    // 发送 Ctrl 释放\n    await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {\n      type: 'keyUp',\n      modifiers: 0,\n      windowsVirtualKeyCode: 17,\n      code: 'ControlLeft',\n      key: 'Control'\n    })\n\n    console.log('[COSE] Paste command sent via debugger')\n\n    // 等待粘贴完成\n    await new Promise(resolve => setTimeout(resolve, 1000))\n\n  } catch (error) {\n    console.error('[COSE] Debugger paste failed:', error)\n  } finally {\n    // 分离调试器\n    try {\n      await chrome.debugger.detach(debuggee)\n      console.log('[COSE] Debugger detached')\n    } catch (e) {\n      // 忽略分离错误\n    }\n  }\n}\n\n// 同步到平台\nasync function syncToPlatform(platformId, content) {\n  const platform = PLATFORMS.find(p => p && p.id === platformId)\n  if (!platform || !platform.publishUrl) {\n    return { success: false, message: '暂不支持该平台' }\n  }\n\n  try {\n    let tab\n\n    // 检查是否有平台特定的同步处理器\n    const syncHandler = SYNC_HANDLERS[platformId]\n    if (syncHandler) {\n      console.log(`[COSE] 使用 ${platformId} 平台特定同步处理器`)\n      // 创建新标签页（对于微信等需要特殊处理的平台，使用首页）\n      const initialUrl = platformId === 'wechat' ? 'https://mp.weixin.qq.com/' : platform.publishUrl\n      tab = await chrome.tabs.create({ url: initialUrl, active: false })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n\n      // 调用平台特定处理器\n      const helpers = {\n        chrome,\n        waitForTab,\n        addTabToSyncGroup,\n        PLATFORMS,\n      }\n      return await syncHandler(tab, content, helpers)\n    }\n\n    // ==== 以下是原有的平台特定逻辑（待迁移）====\n\n    if (platformId === 'infoq') {\n      // InfoQ：需要先调用 API 创建草稿获取 ID，不能直接访问 /draft/write\n      try {\n        // 调用创建草稿 API\n        const response = await fetch('https://xie.infoq.cn/api/v1/draft/create', {\n          method: 'POST',\n          credentials: 'include',\n          headers: {\n            'Content-Type': 'application/json',\n            'Accept': 'application/json',\n          }\n        })\n        const data = await response.json()\n\n        if (data.code === 0 && data.data?.id) {\n          const draftId = data.data.id\n          const targetUrl = `https://xie.infoq.cn/draft/${draftId}`\n          console.log('[COSE] InfoQ 创建草稿成功，ID:', draftId)\n\n          tab = await chrome.tabs.create({ url: targetUrl, active: false })\n          await addTabToSyncGroup(tab.id, tab.windowId)\n          await waitForTab(tab.id)\n        } else {\n          console.error('[COSE] InfoQ 创建草稿失败:', data)\n          return { success: false, message: 'InfoQ 创建草稿失败，请确保已登录' }\n        }\n      } catch (e) {\n        console.error('[COSE] InfoQ API 调用失败:', e)\n        return { success: false, message: 'InfoQ API 调用失败: ' + e.message }\n      }\n    } else if (platformId === 'jianshu') {\n      // 简书：需要先获取文集列表，然后创建新文章\n      try {\n        // 获取用户的文集列表\n        const notebooksResp = await fetch('https://www.jianshu.com/author/notebooks', {\n          method: 'GET',\n          credentials: 'include',\n          headers: {\n            'Accept': 'application/json',\n          }\n        })\n        const notebooks = await notebooksResp.json()\n\n        if (!notebooks || notebooks.length === 0) {\n          return { success: false, message: '简书未找到文集，请先创建一个文集' }\n        }\n\n        // 使用第一个文集\n        const notebookId = notebooks[0].id\n        console.log('[COSE] 简书使用文集:', notebooks[0].name, 'ID:', notebookId)\n\n        // 创建新文章\n        const createResp = await fetch('https://www.jianshu.com/author/notes', {\n          method: 'POST',\n          credentials: 'include',\n          headers: {\n            'Content-Type': 'application/json',\n            'Accept': 'application/json',\n          },\n          body: JSON.stringify({\n            notebook_id: String(notebookId),\n            title: content.title || '无标题',\n            at_bottom: false\n          })\n        })\n        const noteData = await createResp.json()\n\n        if (noteData && noteData.id) {\n          const noteId = noteData.id\n          const targetUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${noteId}`\n          console.log('[COSE] 简书创建文章成功，ID:', noteId)\n\n          tab = await chrome.tabs.create({ url: targetUrl, active: false })\n          await addTabToSyncGroup(tab.id, tab.windowId)\n          await waitForTab(tab.id)\n        } else {\n          console.error('[COSE] 简书创建文章失败:', noteData)\n          return { success: false, message: '简书创建文章失败，请确保已登录' }\n        }\n      } catch (e) {\n        console.error('[COSE] 简书 API 调用失败:', e)\n        return { success: false, message: '简书 API 调用失败: ' + e.message }\n      }\n    } else if (platformId === 'xiaohongshu') {\n      // 小红书：需要先点击\"新的创作\"按钮，等待编辑器加载后填充\n      console.log('[COSE] 开始处理小红书同步...')\n\n      // 打开发布页面\n      tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n      await waitForTab(tab.id)\n\n      // 等待页面加载完成\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 在页面中执行：点击\"新的创作\"并等待编辑器加载\n      const clickResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async () => {\n          const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n          // 查找\"新的创作\"按钮\n          const createBtn = Array.from(document.querySelectorAll('button'))\n            .find(el => el.textContent.includes('新的创作'))\n\n          if (createBtn) {\n            createBtn.click()\n            console.log('[COSE] 小红书已点击\"新的创作\"按钮')\n\n            // 等待编辑器加载（等待富文本编辑器出现）\n            const waitForEditor = async (timeout = 10000) => {\n              const start = Date.now()\n              while (Date.now() - start < timeout) {\n                // 查找编辑器元素，可能是 contenteditable 或 textarea\n                const editor = document.querySelector('[contenteditable=\"true\"]') ||\n                  document.querySelector('textarea') ||\n                  document.querySelector('.editor') ||\n                  document.querySelector('.content-editor')\n                if (editor) return true\n                await sleep(200)\n              }\n              return false\n            }\n\n            const editorLoaded = await waitForEditor()\n            return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' }\n          }\n\n          return { success: false, message: 'Create button not found' }\n        }\n      })\n\n      if (!clickResult[0]?.result?.success) {\n        return { success: false, message: '小红书创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') }\n      }\n\n      // 等待页面稳定\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 小红书 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, htmlBody) => {\n          const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n          // 等待元素出现的工具函数\n          const waitForElement = (selector, timeout = 15000) => {\n            return new Promise((resolve) => {\n              const el = document.querySelector(selector)\n              if (el) return resolve(el)\n\n              const observer = new MutationObserver(() => {\n                const el = document.querySelector(selector)\n                if (el) {\n                  observer.disconnect()\n                  resolve(el)\n                }\n              })\n              observer.observe(document.body, { childList: true, subtree: true })\n\n              setTimeout(() => {\n                observer.disconnect()\n                resolve(document.querySelector(selector))\n              }, timeout)\n            })\n          }\n\n          try {\n            console.log('[COSE] 小红书开始填充内容...')\n\n            // 等待并查找标题输入框\n            const titleInput = await waitForElement('input[placeholder*=\"标题\"], textarea[placeholder*=\"标题\"], .title-input', 5000)\n            if (titleInput && title) {\n              titleInput.focus()\n              // 使用 native setter 确保 React/Vue 等框架能检测到变化\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n              if (nativeSetter) {\n                nativeSetter.call(titleInput, title)\n              } else {\n                titleInput.value = title\n              }\n              titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n              titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] 小红书标题已填充:', title)\n            }\n\n            // 稍等一下让标题生效\n            await new Promise(r => setTimeout(r, 300))\n\n            // 等待并查找内容编辑器\n            const contentEditor = await waitForElement('[contenteditable=\"true\"], .editor-content, .content-editor', 5000)\n            if (contentEditor && htmlBody) {\n              contentEditor.focus()\n\n              // 清空现有占位符内容\n              if (contentEditor.textContent.includes('从这里开始写正文') ||\n                contentEditor.textContent.includes('请输入正文') ||\n                contentEditor.textContent.includes('写点什么')) {\n                contentEditor.innerHTML = ''\n              }\n\n              // 使用 ClipboardEvent + DataTransfer 注入 HTML\n              const dt = new DataTransfer()\n              dt.setData('text/html', htmlBody)\n              dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n              const pasteEvent = new ClipboardEvent('paste', {\n                bubbles: true,\n                cancelable: true,\n                clipboardData: dt\n              })\n\n              contentEditor.dispatchEvent(pasteEvent)\n              console.log('[COSE] 小红书内容已通过 paste 事件注入')\n\n              // 等待内容渲染\n              await new Promise(r => setTimeout(r, 500))\n\n              // 验证内容是否注入成功\n              const wordCount = contentEditor.textContent?.length || 0\n              if (wordCount === 0) {\n                // 备用方案：直接设置 innerHTML\n                console.log('[COSE] paste 事件未生效，尝试备用方案')\n                contentEditor.innerHTML = htmlBody\n              }\n\n              return { success: true, method: 'paste-html', length: htmlBody.length }\n            }\n\n            return { success: false, error: 'Content editor not found' }\n          } catch (e) {\n            console.error('[COSE] 小红书同步失败:', e)\n            return { success: false, error: e.message }\n          }\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 小红书填充结果:', fillResult[0]?.result)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到小红书', tabId: tab.id }\n    } else if (platformId === 'twitter') {\n      // Twitter Articles：需要先打开草稿列表页，然后点击 create 按钮创建新文章\n      // 注意：Twitter 使用 Page Visibility API，后台标签页不会渲染编辑器\n      // 解决方案：短暂激活 Tab，等编辑器加载后切回原 Tab\n\n      // 记录当前活动的 Tab\n      const [currentTab] = await chrome.tabs.query({ active: true, currentWindow: true })\n\n      // 第一步：打开草稿列表页（激活状态，触发编辑器渲染）\n      tab = await chrome.tabs.create({ url: platform.publishUrl, active: true })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n      await waitForTab(tab.id)\n\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 第二步：点击 create 按钮并等待编辑器加载完成\n      const clickResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async () => {\n          const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n          // 查找 create 按钮\n          const createBtn = document.querySelector('button[aria-label=\"create\"]') ||\n            Array.from(document.querySelectorAll('button')).find(b =>\n              b.getAttribute('aria-label')?.toLowerCase() === 'create'\n            )\n\n          if (createBtn) {\n            createBtn.click()\n            console.log('[COSE] Twitter Articles 已点击 create 按钮')\n\n            // 等待编辑器加载（等待标题输入框出现）\n            const waitForEditor = async (timeout = 10000) => {\n              const start = Date.now()\n              while (Date.now() - start < timeout) {\n                const titleInput = document.querySelector('textarea[placeholder=\"Add a title\"]')\n                if (titleInput) return true\n                await sleep(200)\n              }\n              return false\n            }\n\n            const editorLoaded = await waitForEditor()\n            return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' }\n          }\n\n          return { success: false, message: 'Create button not found' }\n        },\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] Twitter Articles create 结果:', clickResult[0]?.result)\n\n      // 编辑器加载完成后，切回原 Tab\n      if (currentTab?.id) {\n        try {\n          await chrome.tabs.update(currentTab.id, { active: true })\n          console.log('[COSE] Twitter 已切回原 Tab')\n        } catch (e) {\n          // 原 Tab 可能已关闭，忽略\n        }\n      }\n\n      if (!clickResult[0]?.result?.success) {\n        return { success: false, message: 'Twitter Articles 创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') }\n      }\n\n      // 等待页面稳定\n      await new Promise(resolve => setTimeout(resolve, 500))\n\n      // 使用 Markdown 内容\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] Twitter Articles Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 第三步：填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, markdown) => {\n          // ========== 工具函数 ==========\n          const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n          const waitForElement = async (selector, timeout = 10000) => {\n            const start = Date.now()\n            while (Date.now() - start < timeout) {\n              const el = document.querySelector(selector)\n              if (el) return el\n              await sleep(200)\n            }\n            return null\n          }\n\n\n\n          // ========== 内置 Markdown 解析器（支持代码块和公式）==========\n          // 使用占位符保护机制，避免正则冲突\n          // 代码块使用样式化 HTML，公式使用 CodeCogs API 渲染为图片\n          function parseMarkdownToHtml(md) {\n            if (!md) return ''\n\n            // 存储需要保护的内容\n            const codeBlocks = []\n            const inlineCodes = []\n            const blockFormulas = []\n            const inlineFormulas = []\n\n            let html = md\n\n            // ========== 第一阶段：提取并保护特殊内容 ==========\n\n            // 1. 提取代码块 ```...```\n            html = html.replace(/```(\\w*)\\n?([\\s\\S]*?)```/g, (match, lang, code) => {\n              const index = codeBlocks.length\n              codeBlocks.push({ lang: lang || '', code: code })\n              return `__CODE_BLOCK_${index}__`\n            })\n\n            // 2. 提取行内代码 `...`\n            html = html.replace(/`([^`\\n]+)`/g, (match, code) => {\n              const index = inlineCodes.length\n              inlineCodes.push(code)\n              return `__INLINE_CODE_${index}__`\n            })\n\n            // 3. 提取块级公式 $$...$$\n            html = html.replace(/\\$\\$([\\s\\S]+?)\\$\\$/g, (match, formula) => {\n              const index = blockFormulas.length\n              blockFormulas.push(formula.trim())\n              return `__BLOCK_FORMULA_${index}__`\n            })\n\n            // 4. 提取行内公式 $...$\n            html = html.replace(/\\$([^\\$\\n]+)\\$/g, (match, formula) => {\n              const index = inlineFormulas.length\n              inlineFormulas.push(formula.trim())\n              return `__INLINE_FORMULA_${index}__`\n            })\n\n            // ========== 第二阶段：处理标准 Markdown 语法 ==========\n\n            // 处理标题\n            html = html.replace(/^#### (.+)$/gm, '<h3>$1</h3>')\n            html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')\n            html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')\n            html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')\n\n            // 处理引用块\n            html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')\n\n            // 处理水平分割线\n            // 注意: X Articles 忽略 <hr> 标签，需要通过 Insert > Divider 菜单插入\n            // 自动同步无法使用菜单，这里保留 hr 但用户可能需要手动调整\n            // 或者可以考虑用视觉分隔符如 --- 文本替代\n            html = html.replace(/^---$/gm, '<p>---</p>')\n            html = html.replace(/^\\*\\*\\*$/gm, '<p>***</p>')\n\n            // 处理图片\n            html = html.replace(/!\\[([^\\]]*)\\]\\(([^)]+)\\)/g, '<img src=\"$2\" alt=\"$1\" style=\"max-width: 100%;\" />')\n\n            // 处理链接\n            html = html.replace(/\\[([^\\]]+)\\]\\(([^)]+)\\)/g, '<a href=\"$2\">$1</a>')\n\n            // 处理粗体、斜体、删除线\n            html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n            html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n            html = html.replace(/~~([^~]+)~~/g, '<s>$1</s>')\n\n            // ========== 第三阶段：恢复保护的内容 ==========\n\n            // 恢复代码块 - X Articles 不支持 <pre><code>，转换为 blockquote\n            // 参考 x-article-publisher skill 的实现\n            codeBlocks.forEach((block, index) => {\n              const escapedCode = block.code\n                .replace(/&/g, '&amp;')\n                .replace(/</g, '&lt;')\n                .replace(/>/g, '&gt;')\n                .replace(/\"/g, '&quot;')\n                .replace(/'/g, '&#039;')\n\n              // 将代码行用 <br> 连接，包装在 blockquote 中\n              // X Articles 原生支持 blockquote，这是最可靠的代码块显示方式\n              const lines = escapedCode.split('\\n').filter(line => line.trim())\n              const formattedCode = lines.join('<br>')\n              const langPrefix = block.lang ? `<strong>${block.lang}</strong><br>` : ''\n              const codeHtml = `<blockquote>${langPrefix}${formattedCode}</blockquote>`\n\n              html = html.replace(`__CODE_BLOCK_${index}__`, codeHtml)\n            })\n\n            // 恢复行内代码 - X Articles 对 inline style 支持有限\n            // 使用简单的 <code> 标签，依赖平台默认样式\n            inlineCodes.forEach((code, index) => {\n              const escapedCode = code\n                .replace(/&/g, '&amp;')\n                .replace(/</g, '&lt;')\n                .replace(/>/g, '&gt;')\n              // 简化为纯 code 标签，X Articles 会应用默认样式\n              const codeHtml = `<code>${escapedCode}</code>`\n\n              html = html.replace(`__INLINE_CODE_${index}__`, codeHtml)\n            })\n\n            // 恢复块级公式（使用 CodeCogs API 渲染为图片）\n            blockFormulas.forEach((formula, index) => {\n              const encodedFormula = encodeURIComponent(formula)\n              const formulaHtml = `<div style=\"text-align: center; margin: 16px 0;\"><img src=\"https://latex.codecogs.com/svg.image?${encodedFormula}\" alt=\"${formula.replace(/\"/g, '&quot;')}\" style=\"max-width: 100%;\" /></div>`\n\n              html = html.replace(`__BLOCK_FORMULA_${index}__`, formulaHtml)\n            })\n\n            // 恢复行内公式\n            inlineFormulas.forEach((formula, index) => {\n              const encodedFormula = encodeURIComponent(formula)\n              const formulaHtml = `<img src=\"https://latex.codecogs.com/svg.image?${encodedFormula}\" alt=\"${formula.replace(/\"/g, '&quot;')}\" style=\"vertical-align: middle;\" />`\n\n              html = html.replace(`__INLINE_FORMULA_${index}__`, formulaHtml)\n            })\n\n            // ========== 第四阶段：处理列表和段落 ==========\n\n            // 处理无序列表项\n            html = html.replace(/^[\\*\\-\\+] (.+)$/gm, '<li>$1</li>')\n\n            // 处理有序列表项\n            html = html.replace(/^\\d+[\\.\\)] (.+)$/gm, '<li>$1</li>')\n\n            // 将连续的 <li> 包装成 <ul>\n            html = html.replace(/(<li>[\\s\\S]*?<\\/li>\\n?)+/g, (match) => {\n              return `<ul>${match}</ul>`\n            })\n\n            // 处理段落\n            const lines = html.split('\\n')\n            const result = []\n            let paragraphLines = []\n\n            const isBlockElement = (line) => {\n              const trimmed = line.trim()\n              return !trimmed ||\n                trimmed.startsWith('<h') ||\n                trimmed.startsWith('<pre') ||\n                trimmed.startsWith('<blockquote') ||\n                trimmed.startsWith('<ul') ||\n                trimmed.startsWith('<ol') ||\n                trimmed.startsWith('<hr') ||\n                trimmed.startsWith('<div') ||\n                trimmed.startsWith('<li') ||\n                trimmed.startsWith('<img') ||\n                trimmed.startsWith('</ul') ||\n                trimmed.startsWith('</ol')\n            }\n\n            const flushParagraph = () => {\n              if (paragraphLines.length > 0) {\n                result.push(`<p>${paragraphLines.join('<br />')}</p>`)\n                paragraphLines = []\n              }\n            }\n\n            for (const line of lines) {\n              const trimmed = line.trim()\n\n              if (!trimmed) {\n                flushParagraph()\n                continue\n              }\n\n              if (isBlockElement(line)) {\n                flushParagraph()\n                result.push(trimmed)\n              } else {\n                paragraphLines.push(trimmed)\n              }\n            }\n\n            flushParagraph()\n\n            return result.join('\\n')\n          }\n\n          // ========== 主流程 ==========\n          try {\n            console.log('[COSE] Twitter Articles 开始填充内容...')\n\n            // 使用内置解析器转换 Markdown 为 HTML\n            const htmlContent = parseMarkdownToHtml(markdown)\n            console.log('[COSE] Markdown 已转换为 HTML')\n\n            // 第一步：填充标题\n            const titleInput = await waitForElement('textarea[placeholder=\"Add a title\"], textarea[name=\"Article Title\"]', 5000)\n            if (titleInput && title) {\n              titleInput.focus()\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n              nativeSetter.call(titleInput, title)\n              titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n              titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] Twitter Articles 标题填充成功')\n            } else {\n              console.log('[COSE] Twitter Articles 未找到标题输入框')\n            }\n\n            await sleep(500)\n\n            // 第二步：填充内容\n            const contentEl = await waitForElement('.public-DraftEditor-content[contenteditable=\"true\"], .DraftEditor-root [contenteditable=\"true\"]', 5000)\n            if (contentEl && htmlContent) {\n              contentEl.focus()\n\n              const dt = new DataTransfer()\n              dt.setData('text/html', htmlContent)\n              dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))\n\n              const pasteEvent = new ClipboardEvent('paste', {\n                bubbles: true,\n                cancelable: true,\n                clipboardData: dt\n              })\n\n              contentEl.dispatchEvent(pasteEvent)\n              console.log('[COSE] Twitter Articles 内容填充成功')\n              return { success: true, method: 'paste-html', length: htmlContent.length }\n            } else {\n              console.log('[COSE] Twitter Articles 未找到内容编辑器')\n              return { success: false, error: 'Content editor not found' }\n            }\n          } catch (e) {\n            console.error('[COSE] Twitter Articles 同步失败:', e)\n            return { success: false, error: e.message }\n          }\n        },\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] Twitter Articles 填充结果:', fillResult[0]?.result)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到 Twitter Articles', tabId: tab.id }\n    }\n\n    // 百度千帆开发者社区：注入 Markdown 并确认转换\n    // 注意：千帆编辑器有自动保存机制，会触发 POST /api/community/topic\n    // 该 API 被 OpenRASP WAF 拦截，返回\"校验错误，可能是跨站点攻击\"，导致前端跳转登录页\n    // 解决方案：\n    // 1. 使用 declarativeNetRequest 阻止千帆 tab 导航到登录页（网络层拦截）\n    // 2. 内容脚本 qianfan-intercept.js 拦截 fetch/XHR/sendBeacon/location 跳转（JS 层拦截）\n    // 3. 监听 tab 的 URL 变化，如果跳转到登录页则导航回编辑器\n    if (platformId === 'qianfan') {\n      // 添加 declarativeNetRequest 规则：阻止千帆 tab 导航到登录页\n      const QIANFAN_BLOCK_RULE_ID = 9999\n      try {\n        await chrome.declarativeNetRequest.updateDynamicRules({\n          removeRuleIds: [QIANFAN_BLOCK_RULE_ID],\n          addRules: [{\n            id: QIANFAN_BLOCK_RULE_ID,\n            priority: 1000,\n            action: { type: 'block' },\n            condition: {\n              urlFilter: '*login.bce.baidu.com*',\n              initiatorDomains: ['qianfan.cloud.baidu.com'],\n              resourceTypes: ['main_frame', 'sub_frame']\n            }\n          }]\n        })\n        console.log('[COSE] 千帆登录页阻止规则已添加')\n      } catch (e) {\n        console.warn('[COSE] 千帆登录页阻止规则添加失败:', e)\n      }\n\n      // 打开发布页面\n      tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n\n      // 动态注入千帆拦截脚本（MAIN world，尽早执行）\n      try {\n        await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: qianfanIntercept,\n          world: 'MAIN',\n          injectImmediately: true,\n        })\n        console.log('[COSE] 千帆拦截脚本已动态注入')\n      } catch (e) {\n        console.warn('[COSE] 千帆拦截脚本注入失败:', e)\n      }\n\n      // 监听 tab URL 变化，如果跳转到登录页则导航回编辑器\n      const tabUpdateListener = (tabId, changeInfo) => {\n        if (tabId === tab.id && changeInfo.url && changeInfo.url.includes('login.bce.baidu.com')) {\n          console.log('[COSE] 检测到千帆 tab 跳转到登录页，导航回编辑器')\n          chrome.tabs.update(tabId, { url: platform.publishUrl })\n        }\n      }\n      chrome.tabs.onUpdated.addListener(tabUpdateListener)\n\n      try {\n        // 等待页面加载\n        await waitForTab(tab.id)\n        await new Promise(resolve => setTimeout(resolve, 2000))\n\n        const markdownContent = content.markdown || content.body || ''\n        console.log('[COSE] 百度千帆 Markdown 内容长度:', markdownContent?.length || 0)\n\n        // 填充标题和内容\n        const fillResult = await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: async (title, markdown) => {\n            const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n            const waitForElement = (selector, timeout = 5000) => {\n              return new Promise((resolve) => {\n                const el = document.querySelector(selector)\n                if (el) return resolve(el)\n                const observer = new MutationObserver(() => {\n                  const el = document.querySelector(selector)\n                  if (el) { observer.disconnect(); resolve(el) }\n                })\n                observer.observe(document.body, { childList: true, subtree: true })\n                setTimeout(() => { observer.disconnect(); resolve(null) }, timeout)\n              })\n            }\n\n            try {\n              // 填充标题\n              const titleInput = await waitForElement('textarea[placeholder=\"请输入文章标题\"]')\n              if (titleInput && title) {\n                titleInput.focus()\n                const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n                nativeSetter.call(titleInput, title)\n                titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n                console.log('[COSE] 百度千帆标题填充成功')\n              }\n\n              await sleep(300)\n\n              // 填充内容 - 使用 paste 事件注入 Markdown\n              const contentEditor = await waitForElement('.mp-editor-container[contenteditable=\"true\"]')\n              if (contentEditor && markdown) {\n                contentEditor.focus()\n                await sleep(100)\n\n                const dt = new DataTransfer()\n                dt.setData('text/plain', markdown)\n                const pasteEvent = new ClipboardEvent('paste', {\n                  bubbles: true, cancelable: true, clipboardData: dt\n                })\n                contentEditor.dispatchEvent(pasteEvent)\n                console.log('[COSE] 百度千帆内容填充成功')\n\n                // 等待并点击 Markdown 转换确认按钮\n                let confirmed = false\n                for (let i = 0; i < 15; i++) {\n                  await sleep(200)\n                  if (document.body.innerText.includes('检测到 Markdown')) {\n                    const confirmBtn = document.querySelector('.mp-modal-enter-btn')\n                    if (confirmBtn) {\n                      confirmBtn.click()\n                      confirmed = true\n                      console.log('[COSE] 百度千帆已确认 Markdown 转换')\n                      break\n                    }\n                  }\n                }\n\n                await sleep(1000)\n                return { success: true, confirmed }\n              }\n\n              return { success: false, error: 'Editor not found' }\n            } catch (e) {\n              console.error('[COSE] 百度千帆同步失败:', e)\n              return { success: false, error: e.message }\n            }\n          },\n          args: [content.title, markdownContent],\n          world: 'MAIN',\n        })\n\n        console.log('[COSE] 百度千帆填充结果:', fillResult[0]?.result)\n\n        // 等待内容稳定\n        await new Promise(resolve => setTimeout(resolve, 2000))\n\n        // 清理：移除 tab 监听器和 declarativeNetRequest 规则\n        chrome.tabs.onUpdated.removeListener(tabUpdateListener)\n        try {\n          await chrome.declarativeNetRequest.updateDynamicRules({\n            removeRuleIds: [QIANFAN_BLOCK_RULE_ID]\n          })\n          console.log('[COSE] 千帆登录页阻止规则已移除')\n        } catch (_) {}\n\n        return { success: true, message: '已同步到百度云千帆，请手动点击发布', tabId: tab.id }\n      } catch (e) {\n        console.error('[COSE] 千帆同步失败:', e)\n        chrome.tabs.onUpdated.removeListener(tabUpdateListener)\n        try {\n          await chrome.declarativeNetRequest.updateDynamicRules({\n            removeRuleIds: [QIANFAN_BLOCK_RULE_ID]\n          })\n        } catch (_) {}\n        return { success: false, message: '千帆同步失败: ' + e.message }\n      }\n    }\n\n    /* [DISABLED] 支付宝开放平台：使用 ne-engine 富文本编辑器，支持 Markdown 转换\n    if (platformId === 'alipayopen') {\n      // 先打开发布页面\n      tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n      await waitForTab(tab.id)\n\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 支付宝开放平台 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 使用导入的填充函数\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: fillAlipayOpenContent,\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 支付宝开放平台填充结果:', fillResult[0]?.result)\n\n      // 等待内容处理完成\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }\n    } else [DISABLED] */\n    if (platformId !== 'wechat' && !tab) {\n      // 其他平台（排除微信，因为微信在上面已经处理）\n      let targetUrl = platform.publishUrl\n\n      // 开源中国：使用 ai-write 编辑器，需要用户 ID\n      if (platformId === 'oschina') {\n        const stored = await chrome.storage.local.get('oschina_userId')\n        const userId = stored?.oschina_userId\n        if (userId) {\n          targetUrl = `https://my.oschina.net/u/${userId}/blog/ai-write`\n          console.log('[COSE] 使用 OSChina AI 写作 URL:', targetUrl)\n        } else {\n          console.warn('[COSE] 未找到 OSChina 用户 ID，使用默认 URL')\n        }\n      }\n\n      // 直接打开发布页面\n      tab = await chrome.tabs.create({ url: targetUrl, active: false })\n      await addTabToSyncGroup(tab.id, tab.windowId)\n      await waitForTab(tab.id)\n    }\n\n    // 微信公众号：直接注入 HTML 到编辑器\n    if (platformId === 'wechat') {\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 等待额外时间确保编辑器完全加载\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      // 等待编辑器就绪并注入内容\n      console.log('[COSE] 开始注入微信内容...')\n      console.log('[COSE] 目标 tab ID:', tab.id)\n\n      let result\n      try {\n        result = await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: async (title, htmlBody) => {\n            // 等待元素出现的工具函数\n            const waitForElement = (selector, timeout = 15000) => {\n              return new Promise((resolve) => {\n                const el = document.querySelector(selector)\n                if (el) return resolve(el)\n\n                const observer = new MutationObserver(() => {\n                  const el = document.querySelector(selector)\n                  if (el) {\n                    observer.disconnect()\n                    resolve(el)\n                  }\n                })\n                observer.observe(document.body, { childList: true, subtree: true })\n\n                setTimeout(() => {\n                  observer.disconnect()\n                  resolve(document.querySelector(selector))\n                }, timeout)\n              })\n            }\n\n            try {\n              // 等待编辑器加载完成\n              const editor = await waitForElement('.ProseMirror')\n              if (!editor) {\n                return { success: false, error: '未找到编辑器' }\n              }\n\n              // 等待标题输入框\n              const titleInput = await waitForElement('#title')\n\n              // 填充标题\n              if (titleInput && title) {\n                titleInput.focus()\n                // 使用 native setter 确保 React/Vue 等框架能检测到变化\n                const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n                if (nativeSetter) {\n                  nativeSetter.call(titleInput, title)\n                } else {\n                  titleInput.value = title\n                }\n                titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n                titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n                console.log('[COSE] 微信标题已填充:', title)\n              }\n\n              // 稍等一下让标题生效\n              await new Promise(r => setTimeout(r, 300))\n\n              // 填充正文内容\n              if (editor && htmlBody) {\n                editor.focus()\n\n                // 清空现有占位符内容\n                if (editor.textContent.includes('从这里开始写正文')) {\n                  editor.innerHTML = ''\n                }\n\n                // 使用 ClipboardEvent + DataTransfer 注入 HTML\n                const dt = new DataTransfer()\n                dt.setData('text/html', htmlBody)\n                dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n                const pasteEvent = new ClipboardEvent('paste', {\n                  bubbles: true,\n                  cancelable: true,\n                  clipboardData: dt\n                })\n\n                editor.dispatchEvent(pasteEvent)\n                console.log('[COSE] 微信内容已通过 paste 事件注入')\n\n                // 等待内容渲染\n                await new Promise(r => setTimeout(r, 500))\n\n                // 验证内容是否注入成功\n                const wordCount = editor.textContent?.length || 0\n                if (wordCount === 0) {\n                  // 备用方案：直接设置 innerHTML\n                  console.log('[COSE] paste 事件未生效，尝试备用方案')\n                  editor.innerHTML = htmlBody\n                  editor.dispatchEvent(new Event('input', { bubbles: true }))\n                }\n\n                return {\n                  success: true,\n                  wordCount: editor.textContent?.length || 0,\n                  titleFilled: titleInput?.value === title\n                }\n              }\n\n              return { success: false, error: '内容为空' }\n            } catch (err) {\n              return { success: false, error: err.message }\n            }\n          },\n          args: [content.title, htmlContent],\n          world: 'MAIN',\n        })\n      } catch (e) {\n        console.error('[COSE] executeScript 执行失败:', e)\n        return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }\n      }\n\n      console.log('[COSE] executeScript 返回数组长度:', result?.length)\n      console.log('[COSE] executeScript 完整返回:', JSON.stringify(result, null, 2))\n\n      if (!result || result.length === 0) {\n        console.error('[COSE] executeScript 返回空数组')\n        return { success: false, message: '脚本执行失败：无返回值', tabId: tab.id }\n      }\n\n      const fillResult = result[0].result\n      console.log('[COSE] 微信填充结果:', JSON.stringify(fillResult, null, 2))\n\n      // 检查 result 结构\n      if (!result || !result[0]) {\n        console.error('[COSE] executeScript 没有返回有效结果')\n        return { success: false, message: '内容注入失败：脚本执行无返回值', tabId: tab.id }\n      }\n\n      if (!fillResult?.success) {\n        console.error('[COSE] 微信内容填充失败:', fillResult?.error)\n        console.error('[COSE] 完整 result 对象:', result)\n        return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n      }\n\n      console.log('[COSE] 微信内容填充成功，字数:', fillResult.wordCount)\n\n      // 等待内容稳定后，点击保存为草稿按钮\n      await new Promise(resolve => setTimeout(resolve, 1000))\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          const saveDraftBtn = Array.from(document.querySelectorAll('button'))\n            .find(b => b.textContent.includes('保存为草稿'))\n          if (saveDraftBtn) {\n            saveDraftBtn.click()\n            console.log('[COSE] 已点击保存为草稿')\n          }\n        },\n        world: 'MAIN',\n      })\n\n      return { success: true, message: '已同步并保存为草稿', tabId: tab.id }\n    }\n\n    // 抖音：使用剪贴板 HTML 粘贴到编辑器（类似微信公众号）\n    if (platformId === 'douyin') {\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 抖音 HTML 内容长度:', htmlContent?.length || 0)\n      console.log('[COSE] 开始注入抖音内容...')\n\n      let result\n      try {\n        result = await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: async (title, htmlBody) => {\n            // 等待元素出现的工具函数（检测到立即返回）\n            const waitForElement = (selector, timeout = 10000) => {\n              return new Promise((resolve) => {\n                const el = document.querySelector(selector)\n                if (el) return resolve(el)\n\n                const observer = new MutationObserver(() => {\n                  const el = document.querySelector(selector)\n                  if (el) {\n                    observer.disconnect()\n                    resolve(el)\n                  }\n                })\n                observer.observe(document.body, { childList: true, subtree: true })\n\n                setTimeout(() => {\n                  observer.disconnect()\n                  resolve(document.querySelector(selector))\n                }, timeout)\n              })\n            }\n\n            try {\n              // 等待编辑器加载完成 - 抖音使用 contenteditable div\n              const editor = await waitForElement('[contenteditable=\"true\"]')\n              if (!editor) {\n                return { success: false, error: '未找到编辑器' }\n              }\n\n              // 等待标题输入框\n              const titleInput = await waitForElement('input[placeholder*=\"标题\"]')\n\n              // 填充标题\n              if (titleInput && title) {\n                titleInput.focus()\n                // 使用 native setter 确保 React 等框架能检测到变化\n                const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set\n                if (nativeSetter) {\n                  nativeSetter.call(titleInput, title)\n                } else {\n                  titleInput.value = title\n                }\n                titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n                titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n                console.log('[COSE] 抖音标题已填充:', title)\n              }\n\n              // 填充正文内容\n              if (editor && htmlBody) {\n                editor.focus()\n\n                // 清空现有内容\n                editor.innerHTML = ''\n\n                // 使用 ClipboardEvent + DataTransfer 注入 HTML\n                const dt = new DataTransfer()\n                dt.setData('text/html', htmlBody)\n                dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n                const pasteEvent = new ClipboardEvent('paste', {\n                  bubbles: true,\n                  cancelable: true,\n                  clipboardData: dt\n                })\n\n                editor.dispatchEvent(pasteEvent)\n                console.log('[COSE] 抖音内容已通过 paste 事件注入')\n\n                // 立即验证内容是否注入成功\n                const wordCount = editor.textContent?.length || 0\n                if (wordCount === 0) {\n                  // 备用方案：直接设置 innerHTML\n                  console.log('[COSE] paste 事件未生效，尝试备用方案')\n                  editor.innerHTML = htmlBody\n                  editor.dispatchEvent(new Event('input', { bubbles: true }))\n                }\n\n                return {\n                  success: true,\n                  wordCount: editor.textContent?.length || 0,\n                  titleFilled: titleInput?.value === title\n                }\n              }\n\n              return { success: false, error: '内容为空' }\n            } catch (err) {\n              return { success: false, error: err.message }\n            }\n          },\n          args: [content.title, htmlContent],\n          world: 'MAIN',\n        })\n      } catch (e) {\n        console.error('[COSE] executeScript 执行失败:', e)\n        return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }\n      }\n\n      console.log('[COSE] 抖音填充结果:', JSON.stringify(result, null, 2))\n\n      if (!result || result.length === 0) {\n        return { success: false, message: '脚本执行失败：无返回值', tabId: tab.id }\n      }\n\n      const fillResult = result[0].result\n      if (!fillResult?.success) {\n        return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n      }\n\n      console.log('[COSE] 抖音内容填充成功，字数:', fillResult.wordCount)\n      return { success: true, message: '已同步到抖音', tabId: tab.id }\n    }\n\n    // 搜狐号：使用剪贴板 HTML 粘贴到编辑器（类似微信公众号）\n    if (platformId === 'sohu') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 搜狐号 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, htmlBody) => {\n          // 填充标题\n          const titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n          if (titleInput && title) {\n            titleInput.focus()\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n            nativeSetter.call(titleInput, title)\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 搜狐号标题填充成功')\n          }\n\n          // 找到 Quill 编辑器\n          const editor = document.querySelector('.ql-editor')\n          if (editor && htmlBody) {\n            editor.focus()\n\n            // 清空现有内容\n            editor.innerHTML = ''\n\n            // 使用 DataTransfer 触发 paste 事件\n            const dt = new DataTransfer()\n            dt.setData('text/html', htmlBody)\n            dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n            const pasteEvent = new ClipboardEvent('paste', {\n              bubbles: true,\n              cancelable: true,\n              clipboardData: dt\n            })\n\n            editor.dispatchEvent(pasteEvent)\n            console.log('[COSE] 搜狐号内容已通过 paste 事件注入')\n          } else {\n            console.log('[COSE] 搜狐号未找到编辑器')\n          }\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      return { success: true, message: '已同步到搜狐号', tabId: tab.id }\n    }\n\n    // B站专栏：使用 UEditor execCommand 插入 HTML\n    if (platformId === 'bilibili') {\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] B站专栏 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 等待 UEditor 就绪\n      const waitForEditor = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          return new Promise((resolve) => {\n            const startTime = Date.now()\n            const maxWait = 10000\n\n            const check = () => {\n              const UE = window.UE\n              if (UE && UE.instants && UE.instants['ueditorInstant0']) {\n                const editor = UE.instants['ueditorInstant0']\n                if (editor.isReady) {\n                  console.log('[COSE] UEditor 已就绪，耗时:', Date.now() - startTime, 'ms')\n                  resolve({ ready: true, time: Date.now() - startTime })\n                  return\n                }\n              }\n\n              if (Date.now() - startTime > maxWait) {\n                console.log('[COSE] UEditor 等待超时')\n                resolve({ ready: false, timeout: true })\n                return\n              }\n\n              setTimeout(check, 100)\n            }\n            check()\n          })\n        },\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] B站专栏编辑器状态:', waitForEditor)\n\n      // 填充标题和内容（一次性完成）\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, htmlBody) => {\n          // 填充标题\n          const titleInput = document.querySelector('textarea')\n          if (titleInput && title) {\n            titleInput.focus()\n            titleInput.value = title\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] B站专栏标题填充成功')\n          }\n\n          // 填充内容\n          const UE = window.UE\n          if (!UE || !UE.instants) {\n            return { success: false, error: 'UEditor not found' }\n          }\n\n          const editor = UE.instants['ueditorInstant0']\n          if (!editor) {\n            return { success: false, error: 'UEditor instance not found' }\n          }\n\n          // 清空并插入内容\n          editor.setContent('')\n          editor.execCommand('inserthtml', htmlBody)\n          editor.fireEvent('contentchange')\n\n          console.log('[COSE] B站专栏内容已填充')\n          return {\n            success: true,\n            contentLength: editor.getContentLength()\n          }\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] B站专栏填充结果:', fillResult)\n\n      // 短暂等待后点击存草稿\n      await new Promise(resolve => setTimeout(resolve, 300))\n\n      // 点击存草稿按钮\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          const saveDraftBtn = Array.from(document.querySelectorAll('button'))\n            .find(b => b.textContent && b.textContent.includes('存草稿'))\n          if (saveDraftBtn) {\n            saveDraftBtn.click()\n            console.log('[COSE] B站专栏已点击存草稿')\n          }\n        },\n        world: 'MAIN',\n      })\n\n      // 等待保存完成\n      await new Promise(resolve => setTimeout(resolve, 500))\n\n      return { success: true, message: '已同步并保存草稿到B站专栏', tabId: tab.id }\n    }\n\n    // 微博头条：使用 ProseMirror 编辑器\n    if (platformId === 'weibo') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 微博头条 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, htmlBody) => {\n          // 填充标题\n          const titleInput = document.querySelector('textarea[placeholder*=\"标题\"]')\n          if (titleInput && title) {\n            titleInput.focus()\n            titleInput.value = title\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 微博头条标题填充成功')\n          }\n\n          // 填充内容 - 微博使用 ProseMirror/TipTap 编辑器\n          const editor = document.querySelector('.ProseMirror')\n          if (editor && htmlBody) {\n            editor.innerHTML = htmlBody\n            editor.dispatchEvent(new Event('input', { bubbles: true }))\n            console.log('[COSE] 微博头条内容填充成功')\n            return { success: true }\n          }\n\n          return { success: false, error: 'Editor not found' }\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 微博头条填充结果:', fillResult)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 点击保存草稿按钮\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          const saveBtn = Array.from(document.querySelectorAll('button'))\n            .find(b => b.textContent && b.textContent.includes('保存草稿'))\n          if (saveBtn) {\n            saveBtn.click()\n            console.log('[COSE] 微博头条已点击保存草稿')\n          }\n        },\n        world: 'MAIN',\n      })\n\n      // 等待保存完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到微博头条', tabId: tab.id }\n    }\n\n    // 阿里云开发者社区：使用 Markdown 编辑器\n    if (platformId === 'aliyun') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 阿里云使用 Markdown 编辑器\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 阿里云开发者社区 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, markdown) => {\n          // 填充标题\n          const titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n          if (titleInput && title) {\n            titleInput.focus()\n            // 使用 native setter 来绕过 React 的受控组件\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n            nativeSetter.call(titleInput, title)\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 阿里云开发者社区标题填充成功')\n          }\n\n          // 填充内容 - 阿里云使用 textarea 作为 Markdown 编辑器\n          const contentTextarea = document.querySelector('textarea[class*=\"editor\"]') ||\n            document.querySelector('.markdown-editor textarea') ||\n            document.querySelector('textarea:not([placeholder*=\"标题\"])')\n\n          if (contentTextarea && markdown) {\n            contentTextarea.focus()\n            // 使用 native setter 来绕过 React 的受控组件\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n            nativeSetter.call(contentTextarea, markdown)\n            contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))\n            contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 阿里云开发者社区内容填充成功')\n            return { success: true }\n          }\n\n          return { success: false, error: 'Editor not found' }\n        },\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 阿里云开发者社区填充结果:', fillResult)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到阿里云开发者社区', tabId: tab.id }\n    }\n\n    // 火山引擎开发者社区：使用 ByteMD 编辑器（基于 CodeMirror）\n    if (platformId === 'volcengine') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 火山引擎使用 Markdown 编辑器\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 火山引擎开发者社区 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, markdown) => {\n          // 填充标题\n          const titleInput = document.querySelector('input[placeholder*=\"标题\"]') ||\n            document.querySelector('input[class*=\"title\"]') ||\n            document.querySelector('.article-title input')\n          if (titleInput && title) {\n            titleInput.focus()\n            // 使用 native setter 来绕过 React 的受控组件\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n            nativeSetter.call(titleInput, title)\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 火山引擎开发者社区标题填充成功')\n          }\n\n          // 火山引擎使用 ByteMD 编辑器（基于 CodeMirror）\n          const codeMirrorEl = document.querySelector('.CodeMirror')\n          if (codeMirrorEl && codeMirrorEl.CodeMirror && markdown) {\n            codeMirrorEl.CodeMirror.setValue(markdown)\n            console.log('[COSE] 火山引擎开发者社区内容填充成功')\n            return { success: true, method: 'CodeMirror' }\n          }\n\n          // 备用方案：尝试直接操作 textarea\n          const contentTextarea = document.querySelector('.bytemd-editor textarea') ||\n            document.querySelector('textarea:not([placeholder*=\"标题\"])')\n\n          if (contentTextarea && markdown) {\n            contentTextarea.focus()\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n            nativeSetter.call(contentTextarea, markdown)\n            contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))\n            contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 火山引擎开发者社区内容填充成功（textarea）')\n            return { success: true, method: 'textarea' }\n          }\n\n          return { success: false, error: 'Editor not found' }\n        },\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 火山引擎开发者社区填充结果:', fillResult)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到火山引擎开发者社区', tabId: tab.id }\n    }\n\n    // 华为云开发者博客：使用 Markdown 编辑器（在 iframe 中）\n    if (platformId === 'huaweicloud') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 华为云使用 Markdown 编辑器\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 华为云开发者博客 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 检查当前编辑器类型，如果不是 Markdown 则切换\n      const switchResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          if (window.tinymceModal?.currentEditorType === 'markdown') {\n            console.log('[COSE] 华为云已经是 Markdown 编辑器')\n            return { alreadyMarkdown: true }\n          }\n          const allElements = document.querySelectorAll('*')\n          for (const el of allElements) {\n            if (el.textContent === 'Markdown格式编辑' && el.children.length === 0) {\n              el.click()\n              console.log('[COSE] 华为云已点击 Markdown 编辑器标签')\n              return { clicked: true }\n            }\n          }\n          return { clicked: false }\n        },\n        world: 'MAIN',\n      })\n\n      // 如果点击了切换按钮，需要等待确认对话框并点击确定\n      if (switchResult[0]?.result?.clicked) {\n        await new Promise(resolve => setTimeout(resolve, 500))\n        await chrome.scripting.executeScript({\n          target: { tabId: tab.id },\n          func: () => {\n            const allElements = document.querySelectorAll('*')\n            for (const el of allElements) {\n              if (el.textContent === '确定' && el.children.length === 0) {\n                el.click()\n                console.log('[COSE] 华为云已点击确定按钮')\n                return { confirmed: true }\n              }\n            }\n            return { confirmed: false }\n          },\n          world: 'MAIN',\n        })\n        // 等待编辑器切换完成\n        await new Promise(resolve => setTimeout(resolve, 3000))\n      }\n\n      // 填充标题\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title) => {\n          const titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n          if (titleInput && title) {\n            titleInput.focus()\n            titleInput.value = title\n            titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            console.log('[COSE] 华为云开发者博客标题填充成功')\n          }\n        },\n        args: [content.title],\n        world: 'MAIN',\n      })\n\n      // 等待 Markdown 编辑器 iframe 完全就绪，然后填充内容\n      // 使用 MutationObserver 监听 iframe 出现，message 事件监听内容确认\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (markdown) => {\n          // 工具函数：使用 MutationObserver 等待 iframe 元素出现并加载\n          const waitForEditorReady = (timeout = 15000) => {\n            return new Promise((resolve) => {\n              const check = () => {\n                const editor = window.tinymceModal?.currentEditor\n                if (editor && typeof editor.setContent === 'function') {\n                  const iframe = document.getElementById(editor.editor_id)\n                  if (iframe && iframe.contentWindow) {\n                    return { editor, iframe }\n                  }\n                }\n                return null\n              }\n\n              // 先立即检查一次\n              const immediate = check()\n              if (immediate) return resolve(immediate)\n\n              // 使用 MutationObserver 监听 DOM 变化（iframe 插入）\n              let resolved = false\n              const observer = new MutationObserver(() => {\n                if (resolved) return\n                const result = check()\n                if (result) {\n                  resolved = true\n                  observer.disconnect()\n                  resolve(result)\n                }\n              })\n              observer.observe(document.body, { childList: true, subtree: true })\n\n              // 超时兜底\n              setTimeout(() => {\n                if (!resolved) {\n                  resolved = true\n                  observer.disconnect()\n                  resolve(null)\n                }\n              }, timeout)\n            })\n          }\n\n          // 工具函数：使用 message 事件监听 setContent 确认（setMdDataSucc）\n          const setContentWithConfirm = (editor, iframe, content, timeout = 3000) => {\n            return new Promise((resolve) => {\n              let resolved = false\n\n              const onMessage = (event) => {\n                try {\n                  const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data\n                  if (data.mdEventAction === 'setMdDataSucc' || data.mdEventAction === 'mdContent') {\n                    if (!resolved) {\n                      resolved = true\n                      window.removeEventListener('message', onMessage)\n                      resolve({ confirmed: true })\n                    }\n                  }\n                } catch (e) { /* 忽略非 JSON 消息 */ }\n              }\n\n              window.addEventListener('message', onMessage)\n              editor.setContent(content)\n\n              // 超时兜底\n              setTimeout(() => {\n                if (!resolved) {\n                  resolved = true\n                  window.removeEventListener('message', onMessage)\n                  resolve({ confirmed: false })\n                }\n              }, timeout)\n            })\n          }\n\n          // 1. 等待编辑器和 iframe 就绪\n          console.log('[COSE] 华为云：等待 Markdown 编辑器 iframe 就绪...')\n          const ready = await waitForEditorReady()\n          if (!ready) {\n            console.log('[COSE] 华为云：编辑器等待超时')\n            return { success: false, error: '编辑器 iframe 等待超时' }\n          }\n          console.log('[COSE] 华为云：编辑器 iframe 已就绪')\n\n          // 2. 带重试的内容填充，通过 message 事件确认\n          const maxRetries = 6\n          for (let attempt = 1; attempt <= maxRetries; attempt++) {\n            console.log(`[COSE] 华为云内容填充尝试 ${attempt}/${maxRetries}`)\n\n            const result = await setContentWithConfirm(ready.editor, ready.iframe, markdown)\n            if (result.confirmed) {\n              console.log(`[COSE] 华为云内容填充成功（第${attempt}次），已收到 iframe 确认`)\n              return { success: true, method: 'message-confirm', attempt, length: markdown.length }\n            }\n\n            console.log(`[COSE] 华为云：未收到 iframe 确认，等待后重试...`)\n            // iframe 内部应用可能还在初始化，等待后重试\n            await new Promise(r => setTimeout(r, 2000))\n          }\n\n          // 3. 所有重试失败，直接 postMessage 作为最后手段\n          console.log('[COSE] 重试耗尽，尝试直接 postMessage')\n          ready.iframe.contentWindow.postMessage(JSON.stringify({\n            mdEditorEventAction: 'setMdEditorContent',\n            data: encodeURIComponent(markdown)\n          }), '*')\n          await new Promise(r => setTimeout(r, 1000))\n          return { success: true, method: 'direct-postMessage', length: markdown.length }\n        },\n        args: [markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 华为云开发者博客填充结果:', fillResult)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 点击保存草稿按钮\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: () => {\n          const allLinks = document.querySelectorAll('a')\n          for (const link of allLinks) {\n            if (link.textContent && link.textContent.includes('保存草稿')) {\n              link.click()\n              console.log('[COSE] 华为云开发者博客已点击保存草稿')\n              return { clicked: true }\n            }\n          }\n          return { clicked: false }\n        },\n        world: 'MAIN',\n      })\n\n      // 等待保存完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到华为云开发者博客', tabId: tab.id }\n    }\n\n    // 华为开发者文章：使用 ACE Editor (Markdown 编辑器)\n    if (platformId === 'huaweidev') {\n      // 华为开发者文章使用 Markdown 编辑器\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 华为开发者文章 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 注入异步弹窗监听器，并执行主流程\n      const result = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, markdown) => {\n          // ========== 弹窗处理函数 ==========\n          const handleDialog = () => {\n            // 方法1: 查找 Ant Design Modal 中的按钮\n            const modalBtns = document.querySelector('.ant-modal-confirm-btns')\n            if (modalBtns) {\n              const buttons = modalBtns.querySelectorAll('button')\n              const modalText = document.querySelector('.ant-modal-confirm-content')?.textContent || ''\n\n              console.log('[COSE] 检测到 Ant Modal:', modalText.substring(0, 50))\n\n              // 处理\"温馨提示\"弹窗 - 点击\"取消\"\n              if (modalText.includes('温馨提示') || modalText.includes('未保存')) {\n                for (const btn of buttons) {\n                  if (btn.textContent?.trim() === '取消') {\n                    console.log('[COSE] 点击温馨提示弹窗的取消按钮')\n                    btn.click()\n                    return true\n                  }\n                }\n              }\n\n              // 处理 MD 编辑器切换确认对话框 - 点击\"确认\"\n              if (modalText.includes('Markdown') || modalText.includes('切换')) {\n                for (const btn of buttons) {\n                  if (btn.textContent?.trim() === '确认') {\n                    console.log('[COSE] 点击 MD 切换确认按钮')\n                    btn.click()\n                    return true\n                  }\n                }\n              }\n            }\n\n            // 方法2: 查找 HTML5 dialog 元素（备用）\n            const dialog = document.querySelector('dialog[open]')\n            if (dialog) {\n              const dialogText = dialog.textContent || ''\n              const buttons = dialog.querySelectorAll('button')\n\n              console.log('[COSE] 检测到 dialog:', dialogText.substring(0, 50))\n\n              if (dialogText.includes('温馨提示') || dialogText.includes('未保存')) {\n                for (const btn of buttons) {\n                  if (btn.textContent?.trim() === '取消') {\n                    btn.click()\n                    return true\n                  }\n                }\n              }\n\n              if (dialogText.includes('Markdown') || dialogText.includes('切换')) {\n                for (const btn of buttons) {\n                  if (btn.textContent?.trim() === '确认') {\n                    btn.click()\n                    return true\n                  }\n                }\n              }\n            }\n\n            return false\n          }\n\n          // ========== 工具函数 ==========\n          const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n          // 轮询检查弹窗（比 MutationObserver 更可靠）\n          let dialogCheckInterval = null\n          const startDialogChecker = () => {\n            // 先检查已存在的弹窗\n            handleDialog()\n\n            // 定时检查新弹窗\n            dialogCheckInterval = setInterval(() => {\n              handleDialog()\n            }, 200)\n\n            console.log('[COSE] 华为开发者文章弹窗检查器已启动')\n          }\n\n          const stopDialogChecker = () => {\n            if (dialogCheckInterval) {\n              clearInterval(dialogCheckInterval)\n              dialogCheckInterval = null\n              console.log('[COSE] 华为开发者文章弹窗检查器已停止')\n            }\n          }\n\n          const waitForElement = async (selector, timeout = 5000) => {\n            const start = Date.now()\n            while (Date.now() - start < timeout) {\n              const el = document.querySelector(selector)\n              if (el) return el\n              await sleep(100)\n            }\n            return null\n          }\n\n          // 等待按钮出现（支持 button 和 a 标签）\n          const waitForMdButton = async (timeout = 15000) => {\n            const start = Date.now()\n            while (Date.now() - start < timeout) {\n              // 方法1: 通过 CKEditor 的 class 选择器（最精确）\n              let btn = document.querySelector('a.cke_button__cktomd')\n              if (btn) return btn\n\n              // 方法2: 查找包含 \"MD编辑器\" 文本的 <a> 标签\n              const allLinks = document.querySelectorAll('a')\n              for (const link of allLinks) {\n                if (link.textContent?.trim() === 'MD编辑器') {\n                  return link\n                }\n              }\n\n              // 方法3: 查找包含 \"MD编辑器\" 文本的 <button> 标签（备用）\n              const allButtons = document.querySelectorAll('button')\n              for (const b of allButtons) {\n                if (b.textContent?.trim() === 'MD编辑器') {\n                  return b\n                }\n              }\n\n              await sleep(300)\n            }\n            return null\n          }\n\n          // 等待富文本编辑器按钮出现\n          const waitForRichTextButton = async (timeout = 2000) => {\n            const start = Date.now()\n            while (Date.now() - start < timeout) {\n              // 查找 \"富文本编辑器\" 按钮（可能是 <a> 或 <button>）\n              const allElements = document.querySelectorAll('a, button')\n              for (const el of allElements) {\n                if (el.textContent?.trim() === '富文本编辑器') {\n                  return el\n                }\n              }\n              await sleep(200)\n            }\n            return null\n          }\n\n          // ========== 主流程 ==========\n          try {\n            // 启动弹窗检查器\n            startDialogChecker()\n\n            // 等待 DOM 完全加载\n            if (document.readyState !== 'complete') {\n              console.log('[COSE] 等待 DOM 加载完成...')\n              await new Promise(resolve => {\n                if (document.readyState === 'complete') {\n                  resolve()\n                } else {\n                  window.addEventListener('load', resolve, { once: true })\n                }\n              })\n            }\n\n            // 等待页面加载完成（等待 MD编辑器 或 富文本编辑器 按钮出现）\n            // 华为开发者页面的编辑器工具栏是异步加载的，需要较长时间\n            console.log('[COSE] 等待编辑器工具栏加载...')\n            let mdButton = await waitForMdButton(15000)\n            let richTextButton = await waitForRichTextButton(2000)\n\n            // 检查是否已经是 Markdown 编辑器\n            let aceEditor = document.querySelector('.ace_editor')\n\n            // 如果有富文本编辑器按钮，说明当前已经是 Markdown 编辑器\n            if (richTextButton || aceEditor) {\n              console.log('[COSE] 已经是 Markdown 编辑器')\n              aceEditor = aceEditor || document.querySelector('.ace_editor')\n            } else if (!aceEditor) {\n              // 需要切换到 Markdown 编辑器\n              if (mdButton) {\n                console.log('[COSE] 点击 MD编辑器 按钮')\n                mdButton.click()\n\n                // 等待 ACE Editor 出现（弹窗会被检查器自动处理）\n                aceEditor = await waitForElement('.ace_editor', 10000)\n\n                if (!aceEditor) {\n                  console.error('[COSE] 等待 ACE Editor 超时')\n                  return { success: false, error: 'ACE Editor not found after timeout' }\n                }\n              } else {\n                console.error('[COSE] 未找到 MD编辑器 按钮')\n                return { success: false, error: 'MD Editor button not found' }\n              }\n            }\n\n            console.log('[COSE] 华为开发者文章已进入 Markdown 编辑器')\n\n            // 等待编辑器完全初始化\n            await sleep(500)\n\n            // 填充标题\n            const titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n            if (titleInput && title) {\n              titleInput.focus()\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n              nativeSetter.call(titleInput, title)\n              titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n              titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] 华为开发者文章标题填充成功')\n            }\n\n            // 填充 Markdown 内容\n            if (typeof ace !== 'undefined') {\n              const editor = ace.edit(aceEditor)\n              if (editor) {\n                editor.session.setValue(markdown)\n                console.log('[COSE] 华为开发者文章内容填充成功，长度:', markdown.length)\n              }\n            }\n\n            return { success: true, method: 'ace', length: markdown.length }\n\n          } finally {\n            // 清理检查器\n            stopDialogChecker()\n          }\n        },\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 华为开发者文章填充结果:', result[0]?.result)\n\n      return { success: true, message: '已同步到华为开发者文章', tabId: tab.id }\n    }\n\n    // 百家号：使用剪贴板 HTML 粘贴到编辑器\n    if (platformId === 'baijiahao') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 百家号 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, htmlBody) => {\n          // 填充标题 - 百家号标题在 contenteditable div 中\n          const titleEditor = document.querySelector('.client_components_titleInput [contenteditable=\"true\"]') ||\n            document.querySelector('.client_pages_edit_components_titleInput [contenteditable=\"true\"]') ||\n            document.querySelector('[class*=\"titleInput\"] [contenteditable=\"true\"]')\n\n          if (titleEditor && title) {\n            titleEditor.focus()\n            titleEditor.innerHTML = ''\n            document.execCommand('insertText', false, title)\n            if (!titleEditor.textContent) {\n              titleEditor.innerHTML = `<p dir=\"auto\">${title}</p>`\n            }\n            titleEditor.dispatchEvent(new Event('input', { bubbles: true }))\n            console.log('[COSE] 百家号标题已填充')\n          }\n\n          // 等待一下再填充内容\n          setTimeout(() => {\n            // 尝试通过 UEditor API 填充\n            if (window.UE_V2 && window.UE_V2.instants && window.UE_V2.instants.ueditorInstant0) {\n              try {\n                const editor = window.UE_V2.instants.ueditorInstant0\n\n                // 提取原始 HTML 中的公式（包含完整 SVG）\n                const tempDiv = document.createElement('div')\n                tempDiv.innerHTML = htmlBody\n                const originalFormulas = []\n                tempDiv.querySelectorAll('.katex-inline, .katex-block, section.katex-block').forEach((formula, index) => {\n                  const svg = formula.querySelector('svg')\n                  if (svg && svg.innerHTML) {\n                    originalFormulas.push({\n                      index,\n                      className: formula.className,\n                      fullHtml: formula.outerHTML\n                    })\n                  }\n                })\n                console.log('[COSE] 百家号提取到', originalFormulas.length, '个公式')\n\n                // 先用 setContent 设置内容（公式 SVG 会被过滤）\n                editor.setContent(htmlBody)\n\n                // 然后直接向 iframe 注入完整的公式 SVG\n                if (originalFormulas.length > 0) {\n                  setTimeout(() => {\n                    const iframe = document.querySelector('iframe')\n                    if (iframe && iframe.contentDocument) {\n                      const iframeDoc = iframe.contentDocument\n                      const emptyFormulas = iframeDoc.querySelectorAll('.katex-inline, .katex-block, section.katex-block')\n\n                      emptyFormulas.forEach((emptyFormula, index) => {\n                        const original = originalFormulas[index]\n                        if (original) {\n                          // 创建新元素并替换\n                          const newElement = document.createElement('div')\n                          newElement.innerHTML = original.fullHtml\n                          const newFormula = newElement.firstElementChild\n                          if (newFormula && emptyFormula.parentNode) {\n                            emptyFormula.parentNode.replaceChild(newFormula, emptyFormula)\n                          }\n                        }\n                      })\n\n                      console.log('[COSE] 百家号公式 SVG 已恢复')\n                      editor.fireEvent('contentChange')\n                    }\n                  }, 300)\n                }\n\n                editor.fireEvent('contentChange');\n                editor.fireEvent('selectionchange');\n                console.log('[COSE] 百家号通过 UEditor API 填充成功')\n                return\n              } catch (e) {\n                console.log('[COSE] 百家号 UEditor API 调用失败', e)\n              }\n            }\n          }, 500)\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      return { success: true, message: '已同步到百家号', tabId: tab.id }\n    }\n\n    // 少数派：使用剪贴板 HTML 粘贴到编辑器\n    if (platformId === 'sspai') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 少数派 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: (title, htmlBody) => {\n          // 填充标题 - 少数派使用 textarea\n          const titleInput = document.querySelector('textarea[placeholder*=\"标题\"]') ||\n            document.querySelector('input[placeholder*=\"标题\"]')\n\n          if (titleInput && title) {\n            titleInput.focus()\n            // 使用 native setter 来绕过 Vue/React 的受控组件\n            const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set ||\n              Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n            nativeSetter.call(titleInput, title)\n            // 触发事件\n            titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n            titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n            titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n            console.log('[COSE] 少数派标题已填充')\n          }\n\n          // 等待一下再填充内容\n          setTimeout(() => {\n            // 找到 ProseMirror 编辑器\n            const editor = document.querySelector('.ProseMirror') ||\n              document.querySelector('[contenteditable=\"true\"]')\n\n            if (editor && htmlBody) {\n              editor.focus()\n\n              // 方法：创建 DataTransfer 并触发 paste 事件\n              const dt = new DataTransfer()\n              dt.setData('text/html', htmlBody)\n              dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n              const pasteEvent = new ClipboardEvent('paste', {\n                bubbles: true,\n                cancelable: true,\n                clipboardData: dt\n              })\n\n              editor.dispatchEvent(pasteEvent)\n              console.log('[COSE] 少数派内容已通过 paste 事件注入')\n            }\n          }, 500)\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      return { success: true, message: '已同步到少数派', tabId: tab.id }\n    }\n\n    // 支付宝开放平台：使用 ne-engine 富文本编辑器\n    if (platformId === 'alipayopen') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用剪贴板 HTML（带完整样式）或降级到 body\n      const htmlContent = content.wechatHtml || content.body\n      console.log('[COSE] 支付宝开放平台 HTML 内容长度:', htmlContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, htmlBody) => {\n          // 等待元素出现的工具函数\n          const waitForElement = (selector, timeout = 10000) => {\n            return new Promise((resolve) => {\n              const el = document.querySelector(selector)\n              if (el) return resolve(el)\n\n              const observer = new MutationObserver(() => {\n                const el = document.querySelector(selector)\n                if (el) {\n                  observer.disconnect()\n                  resolve(el)\n                }\n              })\n              observer.observe(document.body, { childList: true, subtree: true })\n\n              setTimeout(() => {\n                observer.disconnect()\n                resolve(document.querySelector(selector))\n              }, timeout)\n            })\n          }\n\n          try {\n            console.log('[COSE] 支付宝开放平台开始填充内容...')\n\n            // 等待并查找标题输入框\n            const titleInput = await waitForElement('#title', 5000) || await waitForElement('input[placeholder*=\"标题\"]', 5000)\n            if (titleInput && title) {\n              titleInput.focus()\n\n              // Ant Design 输入框需要特殊处理\n              // 先清空\n              titleInput.value = ''\n\n              // 使用 native setter 确保 React 能检测到变化\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set\n              if (nativeSetter) {\n                nativeSetter.call(titleInput, title)\n              } else {\n                titleInput.value = title\n              }\n\n              // 触发多个事件确保 Ant Design 组件能识别\n              titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n              titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n              titleInput.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }))\n              titleInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }))\n\n              // 使用 setValue 方法（如果存在）\n              if (titleInput._valueTracker) {\n                titleInput._valueTracker.setValue('')\n              }\n\n              // 再次设置值\n              titleInput.value = title\n              titleInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }))\n\n              // 模拟用户输入\n              const inputEvent = new InputEvent('input', {\n                bubbles: true,\n                cancelable: true,\n                data: title,\n                inputType: 'insertText'\n              })\n              titleInput.dispatchEvent(inputEvent)\n\n              console.log('[COSE] 支付宝开放平台标题已填充:', title, '当前值:', titleInput.value)\n            }\n\n            // 稍等一下让标题生效\n            await new Promise(r => setTimeout(r, 300))\n\n            // 等待并查找 ne-engine 编辑器\n            const editor = await waitForElement('.ne-engine[contenteditable=\"true\"]', 5000)\n            if (editor && htmlBody) {\n              editor.focus()\n\n              // 清空现有内容\n              editor.innerHTML = ''\n\n              // 使用 ClipboardEvent + DataTransfer 注入 HTML\n              const dt = new DataTransfer()\n              dt.setData('text/html', htmlBody)\n              dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n              const pasteEvent = new ClipboardEvent('paste', {\n                bubbles: true,\n                cancelable: true,\n                clipboardData: dt\n              })\n\n              editor.dispatchEvent(pasteEvent)\n              console.log('[COSE] 支付宝开放平台内容已通过 paste 事件注入')\n\n              // 等待内容渲染\n              await new Promise(r => setTimeout(r, 500))\n\n              // 验证内容是否注入成功\n              const wordCount = editor.textContent?.length || 0\n              if (wordCount === 0) {\n                // 备用方案：直接设置 innerHTML\n                console.log('[COSE] paste 事件未生效，尝试备用方案')\n                editor.innerHTML = htmlBody\n                editor.dispatchEvent(new Event('input', { bubbles: true }))\n              }\n\n              return { success: true, method: 'paste-html', length: htmlBody.length }\n            }\n\n            return { success: false, error: 'ne-engine editor not found' }\n          } catch (e) {\n            console.error('[COSE] 支付宝开放平台同步失败:', e)\n            return { success: false, error: e.message }\n          }\n        },\n        args: [content.title, htmlContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 支付宝开放平台填充结果:', fillResult[0]?.result)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }\n    }\n\n    // 电子发烧友：等待 Vditor 编辑器加载后填充内容\n    if (platformId === 'elecfans') {\n      // 等待页面完全加载\n      await new Promise(resolve => setTimeout(resolve, 3000))\n\n      // 使用 Markdown 内容\n      const markdownContent = content.markdown || content.body || ''\n      console.log('[COSE] 电子发烧友 Markdown 内容长度:', markdownContent?.length || 0)\n\n      // 填充标题和内容\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, markdown) => {\n          // 等待元素出现的工具函数\n          const waitForElement = (selector, timeout = 10000) => {\n            return new Promise((resolve) => {\n              const el = document.querySelector(selector)\n              if (el) return resolve(el)\n\n              const observer = new MutationObserver(() => {\n                const el = document.querySelector(selector)\n                if (el) {\n                  observer.disconnect()\n                  resolve(el)\n                }\n              })\n              observer.observe(document.body, { childList: true, subtree: true })\n\n              setTimeout(() => {\n                observer.disconnect()\n                resolve(document.querySelector(selector))\n              }, timeout)\n            })\n          }\n\n          try {\n            console.log('[COSE] 电子发烧友开始填充内容...')\n\n            // 等待并查找标题输入框\n            const titleInput = await waitForElement('input[placeholder*=\"标题\"], input.title-input, input[name=\"title\"]', 5000)\n            if (titleInput && title) {\n              titleInput.focus()\n              // 使用 native setter 确保框架能检测到变化\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set\n              if (nativeSetter) {\n                nativeSetter.call(titleInput, title)\n              } else {\n                titleInput.value = title\n              }\n              titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n              titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] 电子发烧友标题已填充:', title)\n            }\n\n            // 稍等一下让标题生效\n            await new Promise(r => setTimeout(r, 500))\n\n            // 优先查找 Vditor 编辑器（电子发烧友使用 Vditor）\n            const vditorWysiwyg = document.querySelector('.vditor-wysiwyg .vditor-reset')\n            if (vditorWysiwyg) {\n              vditorWysiwyg.focus()\n\n              // Vditor 需要 HTML 内容，将 Markdown 转换为简单 HTML\n              // 先尝试使用 ClipboardEvent 粘贴 Markdown\n              const dt = new DataTransfer()\n              dt.setData('text/plain', markdown)\n\n              const pasteEvent = new ClipboardEvent('paste', {\n                bubbles: true,\n                cancelable: true,\n                clipboardData: dt\n              })\n\n              vditorWysiwyg.dispatchEvent(pasteEvent)\n              console.log('[COSE] 电子发烧友 Vditor paste 事件已触发')\n\n              // 等待内容渲染\n              await new Promise(r => setTimeout(r, 500))\n\n              // 验证内容是否注入成功\n              const wordCount = vditorWysiwyg.textContent?.length || 0\n              if (wordCount > 10) {\n                console.log('[COSE] 电子发烧友 Vditor 内容已填充，字数:', wordCount)\n                return { success: true, method: 'vditor-paste', length: wordCount }\n              }\n\n              // 备用方案：直接设置 textContent（Vditor 会自动解析 Markdown）\n              console.log('[COSE] paste 事件未生效，尝试直接输入')\n              vditorWysiwyg.textContent = markdown\n              vditorWysiwyg.dispatchEvent(new Event('input', { bubbles: true }))\n\n              return { success: true, method: 'vditor-direct', length: markdown.length }\n            }\n\n            // 等待并查找 CodeMirror 编辑器或 textarea\n            const cmElement = document.querySelector('.CodeMirror')\n            if (cmElement && cmElement.CodeMirror) {\n              cmElement.CodeMirror.setValue(markdown)\n              console.log('[COSE] 电子发烧友 CodeMirror 内容已填充')\n              return { success: true, method: 'codemirror', length: markdown.length }\n            }\n\n            // 尝试查找 textarea\n            const textarea = await waitForElement('textarea.content-textarea, textarea[name=\"content\"], textarea', 5000)\n            if (textarea && markdown) {\n              textarea.focus()\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n              if (nativeSetter) {\n                nativeSetter.call(textarea, markdown)\n              } else {\n                textarea.value = markdown\n              }\n              textarea.dispatchEvent(new Event('input', { bubbles: true }))\n              textarea.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] 电子发烧友 textarea 内容已填充')\n              return { success: true, method: 'textarea', length: markdown.length }\n            }\n\n            // 尝试查找通用 contenteditable 编辑器\n            const editor = await waitForElement('[contenteditable=\"true\"]', 5000)\n            if (editor && markdown) {\n              editor.focus()\n              editor.textContent = markdown\n              editor.dispatchEvent(new Event('input', { bubbles: true }))\n              console.log('[COSE] 电子发烧友 contenteditable 内容已填充')\n              return { success: true, method: 'contenteditable', length: markdown.length }\n            }\n\n            return { success: false, error: 'editor not found' }\n          } catch (e) {\n            console.error('[COSE] 电子发烧友同步失败:', e)\n            return { success: false, error: e.message }\n          }\n        },\n        args: [content.title, markdownContent],\n        world: 'MAIN',\n      })\n\n      console.log('[COSE] 电子发烧友填充结果:', fillResult[0]?.result)\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到电子发烧友', tabId: tab.id }\n    }\n\n    // 豆瓣：向首页分享框注入内容\n    if (platformId === 'douban') {\n      // 使用纯文本内容（豆瓣分享框不支持富文本）\n      const textContent = content.markdown || content.body || ''\n      console.log('[COSE] 豆瓣文本内容长度:', textContent?.length || 0)\n\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      // 填充分享框\n      const fillResult = await chrome.scripting.executeScript({\n        target: { tabId: tab.id },\n        func: async (title, text) => {\n          try {\n            console.log('[COSE] 豆瓣开始填充内容...')\n\n            if (!text) {\n              return { success: false, error: 'Empty content' }\n            }\n\n            const fullText = title ? `${title}\\n\\n${text}` : text\n\n            // 豆瓣当前输入框：Lexical contenteditable\n            const editable = document.querySelector('div.DRE-inputor.DRE-root[contenteditable=\"true\"]')\n              || document.querySelector('[contenteditable=\"true\"][role=\"textbox\"]')\n\n            if (editable) {\n              editable.focus()\n\n              // 优先使用 Lexical 编辑器 API（豆瓣当前实现）\n              const lexicalEditor = editable.__lexicalEditor\n              if (lexicalEditor?.parseEditorState && lexicalEditor?.setEditorState) {\n                try {\n                  const lines = fullText.split('\\n')\n                  const makeParagraph = (lineText) => ({\n                    children: lineText\n                      ? [{ detail: 0, format: 0, mode: 'normal', style: '', text: lineText, type: 'text', version: 1 }]\n                      : [],\n                    direction: 'ltr',\n                    format: '',\n                    indent: 0,\n                    type: 'paragraph',\n                    version: 1,\n                    textFormat: 0,\n                    textStyle: '',\n                  })\n\n                  const nextState = {\n                    root: {\n                      children: lines.map(makeParagraph),\n                      direction: 'ltr',\n                      format: '',\n                      indent: 0,\n                      type: 'root',\n                      version: 1,\n                    },\n                  }\n\n                  const parsedState = lexicalEditor.parseEditorState(JSON.stringify(nextState))\n                  lexicalEditor.setEditorState(parsedState)\n                  lexicalEditor.focus()\n\n                  const lexicalLength = (editable.textContent || '').trim().length\n                  if (lexicalLength > 0) {\n                    console.log('[COSE] 豆瓣 lexical API 内容已填充，长度:', lexicalLength)\n                    return { success: true, length: lexicalLength, mode: 'lexical-api' }\n                  }\n                } catch (e) {\n                  console.log('[COSE] 豆瓣 lexical API 填充失败，回退 execCommand:', e.message)\n                }\n              }\n\n              // Lexical 编辑器对 direct textContent 赋值不稳定，优先使用 execCommand 输入\n              try {\n                const selection = window.getSelection()\n                const range = document.createRange()\n                range.selectNodeContents(editable)\n                selection?.removeAllRanges()\n                selection?.addRange(range)\n              } catch (_) {}\n\n              try {\n                document.execCommand('selectAll', false)\n              } catch (_) {}\n              try {\n                document.execCommand('delete', false)\n              } catch (_) {}\n\n              let inserted = false\n              try {\n                inserted = document.execCommand('insertText', false, fullText)\n              } catch (_) {\n                inserted = false\n              }\n\n              if (!inserted) {\n                // 回退：直接赋值并触发输入事件\n                editable.textContent = fullText\n                editable.dispatchEvent(new InputEvent('input', {\n                  bubbles: true,\n                  inputType: 'insertText',\n                  data: fullText,\n                }))\n              }\n\n              editable.dispatchEvent(new Event('change', { bubbles: true }))\n\n              const actualLength = (editable.textContent || '').trim().length\n              if (actualLength === 0) {\n                return { success: false, error: 'Editor accepted no text' }\n              }\n\n              console.log('[COSE] 豆瓣 contenteditable 内容已填充，长度:', actualLength)\n              return { success: true, length: actualLength, mode: 'contenteditable' }\n            }\n\n            // 兼容旧版 textarea 结构\n            const textarea = document.querySelector('textarea[placeholder*=\"此刻你想要分享\"]')\n              || document.querySelector('textarea[placeholder*=\"分享\"]')\n              || document.querySelector('textarea')\n\n            if (textarea) {\n              textarea.focus()\n              const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n              if (nativeSetter) {\n                nativeSetter.call(textarea, fullText)\n              } else {\n                textarea.value = fullText\n              }\n              textarea.dispatchEvent(new Event('input', { bubbles: true }))\n              textarea.dispatchEvent(new Event('change', { bubbles: true }))\n              console.log('[COSE] 豆瓣 textarea 内容已填充，长度:', fullText.length)\n              return { success: true, length: fullText.length, mode: 'textarea' }\n            }\n\n            return { success: false, error: 'Editor not found' }\n          } catch (e) {\n            console.error('[COSE] 豆瓣同步失败:', e)\n            return { success: false, error: e.message }\n          }\n        },\n        args: [content.title, textContent],\n        world: 'MAIN',\n      })\n\n      const doubanResult = fillResult[0]?.result\n      console.log('[COSE] 豆瓣填充结果:', doubanResult)\n\n      if (!doubanResult?.success) {\n        return {\n          success: false,\n          message: doubanResult?.error || '豆瓣内容填充失败',\n          tabId: tab.id,\n        }\n      }\n\n      // 等待内容注入完成\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      return { success: true, message: '已同步到豆瓣，请手动点击发布', tabId: tab.id }\n    }\n\n    // 其他平台使用 scripting API 直接注入填充脚本\n    // 使用 MAIN world 才能访问页面的 CodeMirror 实例\n    await chrome.scripting.executeScript({\n      target: { tabId: tab.id },\n      func: fillContentOnPage,\n      args: [content, platformId],\n      world: 'MAIN',\n    })\n\n    return { success: true, message: '已打开发布页面并填充内容', tabId: tab.id }\n  } catch (error) {\n    return { success: false, message: error.message }\n  }\n}\n\n// 在目标页面执行的填充函数\nfunction fillContentOnPage(content, platformId) {\n  const { title, body, markdown, wechatHtml } = content\n\n  // 等待元素出现的工具函数\n  function waitFor(selector, timeout = 10000) {\n    return new Promise((resolve) => {\n      const start = Date.now()\n      const check = () => {\n        const el = document.querySelector(selector)\n        if (el) resolve(el)\n        else if (Date.now() - start > timeout) resolve(null)\n        else setTimeout(check, 200)\n      }\n      check()\n    })\n  }\n\n  // 设置输入值\n  function setInputValue(el, value) {\n    if (!el || !value) return\n    el.focus()\n    if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {\n      el.value = value\n      el.dispatchEvent(new Event('input', { bubbles: true }))\n      el.dispatchEvent(new Event('change', { bubbles: true }))\n    } else if (el.contentEditable === 'true') {\n      el.innerHTML = value.replace(/\\n/g, '<br>')\n      el.dispatchEvent(new Event('input', { bubbles: true }))\n    }\n  }\n\n  // 根据平台填充内容\n  async function fill() {\n    const host = window.location.hostname\n    const contentToFill = markdown || body || ''\n\n    // 知乎专栏 - 由 syncToPlatform 单独处理（使用导入文档功能）\n    if (host.includes('zhihu.com')) {\n      console.log('[COSE] 知乎由导入文档功能处理')\n    }\n    // 今日头条\n    else if (host.includes('toutiao.com')) {\n      // 填充标题 - 头条使用 textarea\n      const titleInput = await waitFor('textarea[placeholder*=\"标题\"]')\n      if (titleInput) {\n        titleInput.focus()\n        // 模拟用户输入\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n        nativeSetter.call(titleInput, title)\n        titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n        console.log('[COSE] 头条标题填充成功:', title)\n      } else {\n        console.log('[COSE] 头条未找到标题输入框')\n      }\n\n      // 等待编辑器加载\n      await new Promise(resolve => setTimeout(resolve, 500))\n\n      // 头条使用 ProseMirror 富文本编辑器\n      const editor = document.querySelector('.ProseMirror')\n      if (editor) {\n        editor.focus()\n        editor.innerHTML = body || contentToFill.replace(/\\n/g, '<br>')\n        editor.dispatchEvent(new InputEvent('input', { bubbles: true }))\n        console.log('[COSE] 头条内容填充成功')\n      } else {\n        console.log('[COSE] 头条未找到编辑器')\n      }\n    }\n    // 思否 SegmentFault\n    else if (host.includes('segmentfault.com')) {\n      // 填充标题\n      const titleInput = await waitFor('input#title, input[placeholder*=\"标题\"]')\n      if (titleInput) {\n        titleInput.focus()\n        titleInput.value = title\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] 思否标题填充成功')\n      } else {\n        console.log('[COSE] 思否未找到标题输入框')\n      }\n\n      // 等待编辑器加载\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 思否使用 CodeMirror 编辑器\n      const cmElement = document.querySelector('.CodeMirror')\n      if (cmElement && cmElement.CodeMirror) {\n        cmElement.CodeMirror.setValue(contentToFill)\n        console.log('[COSE] 思否 CodeMirror 填充成功')\n      } else {\n        // 降级到 textarea\n        const textarea = document.querySelector('textarea')\n        if (textarea) {\n          textarea.focus()\n          textarea.value = contentToFill\n          textarea.dispatchEvent(new Event('input', { bubbles: true }))\n          console.log('[COSE] 思否 textarea 填充成功')\n        } else {\n          console.log('[COSE] 思否 未找到编辑器')\n        }\n      }\n    }\n    // 开源中国 OSChina (AI 写作平台 - 切换到 Markdown 编辑器)\n    else if (host.includes('oschina.net')) {\n      // 1. 切换到 MD 编辑器（如果当前不是）\n      const switchText = document.querySelector('.editor-switch-text')\n      if (switchText && switchText.textContent.includes('切换到MD编辑器')) {\n        // Click the switch button to trigger confirmation dialog\n        const switchBtn = document.querySelector('.editor-switch-btn') || switchText.parentElement\n        if (switchBtn) {\n          switchBtn.click()\n          console.log('[COSE] OSChina 已点击切换按钮')\n          // Poll for the confirmation button (Ant Design Modal animation)\n          let confirmBtn = null\n          for (let i = 0; i < 20; i++) {\n            await new Promise(resolve => setTimeout(resolve, 200))\n            confirmBtn = Array.from(document.querySelectorAll('button'))\n              .find(btn => btn.textContent.trim() === '确定切换')\n            if (confirmBtn) break\n          }\n          if (confirmBtn) {\n            confirmBtn.click()\n            console.log('[COSE] OSChina 已确认切换到MD编辑器')\n          } else {\n            console.log('[COSE] OSChina 未找到确认切换按钮')\n          }\n          // Wait for MD editor to load after switch\n          await new Promise(resolve => setTimeout(resolve, 2000))\n        }\n      } else {\n        console.log('[COSE] OSChina 已在MD编辑器模式')\n      }\n\n      // 2. 填充标题\n      const titleInput = await waitFor('input[placeholder*=\"标题\"]')\n      if (titleInput) {\n        titleInput.focus()\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set\n        if (nativeSetter) {\n          nativeSetter.call(titleInput, title)\n        } else {\n          titleInput.value = title\n        }\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] OSChina 标题填充成功')\n      }\n\n      // 3. 填充 Markdown 内容到 textarea\n      await new Promise(resolve => setTimeout(resolve, 500))\n      const mdContent = markdown || contentToFill\n      // Poll for textarea (may take time after editor switch)\n      let textarea = null\n      for (let i = 0; i < 10; i++) {\n        textarea = document.querySelector('textarea')\n        if (textarea) break\n        await new Promise(resolve => setTimeout(resolve, 300))\n      }\n      if (textarea) {\n        textarea.focus()\n        const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n        if (textareaSetter) {\n          textareaSetter.call(textarea, mdContent)\n        } else {\n          textarea.value = mdContent\n        }\n        textarea.dispatchEvent(new Event('input', { bubbles: true }))\n        textarea.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] OSChina Markdown 内容填充成功，长度:', mdContent.length)\n      } else {\n        console.log('[COSE] OSChina 未找到 Markdown textarea')\n      }\n    }\n    // 博客园\n    else if (host.includes('cnblogs.com')) {\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 1000))\n\n      // 填充标题 - 博客园标题输入框\n      const titleInput = await waitFor('input[placeholder=\"标题\"]') || document.querySelector('input')\n      if (titleInput) {\n        titleInput.focus()\n        titleInput.value = title\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] 博客园标题填充成功')\n      } else {\n        console.log('[COSE] 博客园未找到标题输入框')\n      }\n\n      // 等待编辑器加载\n      await new Promise(resolve => setTimeout(resolve, 500))\n\n      // 博客园使用 id=\"md-editor\" 的 textarea 作为 Markdown 编辑器\n      const editor = document.querySelector('#md-editor') || document.querySelector('textarea.not-resizable')\n      if (editor) {\n        editor.focus()\n        editor.value = contentToFill\n        editor.dispatchEvent(new Event('input', { bubbles: true }))\n        editor.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] 博客园内容填充成功')\n      } else {\n        console.log('[COSE] 博客园未找到编辑器')\n      }\n    }\n    // InfoQ\n    else if (host.includes('infoq.cn')) {\n      // 填充标题\n      const titleInput = await waitFor('input[placeholder*=\"标题\"], .title-input input, input.article-title')\n      if (titleInput) {\n        setInputValue(titleInput, title)\n        console.log('[COSE] InfoQ 标题填充成功')\n      } else {\n        console.log('[COSE] InfoQ 未找到标题输入框')\n      }\n\n      // InfoQ 使用 Vue 编辑器，需要在主世界执行才能访问 __vue__\n      // 通过注入 script 标签的方式在主世界执行\n      // 使用 ProseMirror view + 剪贴板粘贴方式填充内容\n      const script = document.createElement('script')\n      script.textContent = `\n        (async function() {\n          const content = ${JSON.stringify(contentToFill)};\n          \n          // 等待编辑器完全初始化的函数\n          const waitForEditor = () => {\n            return new Promise((resolve) => {\n              let attempts = 0;\n              const maxAttempts = 30; // 最多等待 15 秒\n              \n              const check = () => {\n                attempts++;\n                const gkEditor = document.querySelector('.gk-editor');\n                if (gkEditor && gkEditor.__vue__) {\n                  const vm = gkEditor.__vue__;\n                  const api = vm.editorAPI;\n                  // 检查 ProseMirror view 是否就绪\n                  if (api && api.editor && api.editor.view) {\n                    resolve(api.editor.view);\n                    return;\n                  }\n                }\n                if (attempts < maxAttempts) {\n                  setTimeout(check, 500);\n                } else {\n                  resolve(null);\n                }\n              };\n              check();\n            });\n          };\n          \n          const view = await waitForEditor();\n          if (!view) {\n            console.log('[COSE] InfoQ 编辑器初始化超时');\n            return;\n          }\n          \n          try {\n            // 清空编辑器现有内容\n            const state = view.state;\n            const tr = state.tr.delete(0, state.doc.content.size);\n            view.dispatch(tr);\n            \n            // 聚焦编辑器\n            view.focus();\n            \n            // 使用剪贴板粘贴方式插入内容（会自动解析 Markdown）\n            const clipboardData = new DataTransfer();\n            clipboardData.setData('text/plain', content);\n            \n            const pasteEvent = new ClipboardEvent('paste', {\n              bubbles: true,\n              cancelable: true,\n              clipboardData: clipboardData\n            });\n            \n            view.dom.dispatchEvent(pasteEvent);\n            console.log('[COSE] InfoQ 内容填充成功');\n          } catch (e) {\n            console.log('[COSE] InfoQ 内容填充失败:', e.message);\n          }\n        })();\n      `\n      document.head.appendChild(script)\n      script.remove()\n    }\n    // 简书\n    else if (host.includes('jianshu.com')) {\n      // 填充标题 - 简书使用 input._24i7u，需要使用 native setter\n      const titleInput = await waitFor('input._24i7u, input[class*=\"title\"]')\n      if (titleInput) {\n        titleInput.focus()\n        const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n        inputSetter.call(titleInput, title)\n        titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n        console.log('[COSE] 简书标题填充成功')\n      } else {\n        console.log('[COSE] 简书未找到标题输入框')\n      }\n\n      // 等待编辑器加载\n      await new Promise(resolve => setTimeout(resolve, 500))\n\n      // 简书使用 textarea#arthur-editor 作为 Markdown 编辑器\n      const editor = document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR')\n      if (editor) {\n        editor.focus()\n        const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n        textareaSetter.call(editor, contentToFill)\n        editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }))\n        editor.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] 简书内容填充成功')\n      } else {\n        console.log('[COSE] 简书未找到编辑器')\n      }\n    }\n    // 腾讯云开发者社区\n    else if (host.includes('cloud.tencent.com')) {\n      console.log('[COSE] TencentCloud 开始同步...')\n\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 1500))\n\n      /**\n       * 第一步：确保进入 MD 编辑器模式\n       * 检查是否有\"切换 MD 编辑器\"按钮：\n       * - 如果有，说明当前是富文本编辑器，需要点击切换\n       * - 如果没有（显示\"切换 富文本 编辑器\"），说明已经是 MD 编辑器\n       */\n      const headerBtns = document.querySelectorAll('.header-btn')\n      let needSwitch = false\n      let switchBtn = null\n\n      for (const btn of headerBtns) {\n        if (btn.textContent.includes('切换') && btn.textContent.includes('MD')) {\n          needSwitch = true\n          switchBtn = btn\n          break\n        }\n      }\n\n      if (needSwitch && switchBtn) {\n        console.log('[COSE] TencentCloud 检测到富文本编辑器，正在切换到 MD 编辑器...')\n        switchBtn.click()\n        // 等待切换完成和 CodeMirror 加载\n        await new Promise(resolve => setTimeout(resolve, 2000))\n      } else {\n        console.log('[COSE] TencentCloud 当前已是 MD 编辑器')\n      }\n\n      /**\n       * 第二步：等待 CodeMirror 加载完成\n       */\n      let codeMirror = null\n      const maxWait = 5000\n      const startTime = Date.now()\n\n      while (Date.now() - startTime < maxWait) {\n        const cm = document.querySelector('.CodeMirror')\n        if (cm && cm.CodeMirror) {\n          codeMirror = cm.CodeMirror\n          break\n        }\n        await new Promise(resolve => setTimeout(resolve, 200))\n      }\n\n      if (!codeMirror) {\n        console.error('[COSE] TencentCloud 错误：CodeMirror 未加载，请刷新页面后重试')\n        return\n      }\n\n      /**\n       * 第三步：填充标题\n       */\n      const titleInput = document.querySelector('textarea[placeholder*=\"标题\"]')\n      if (titleInput && title) {\n        titleInput.focus()\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n        nativeSetter.call(titleInput, title)\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] TencentCloud 标题填充成功')\n      }\n\n      /**\n       * 第四步：填充内容到 CodeMirror\n       */\n      codeMirror.setValue(contentToFill)\n      console.log('[COSE] TencentCloud 内容填充成功')\n    }\n    // Medium\n    else if (host.includes('medium.com')) {\n      console.log('[COSE] Medium 开始同步...')\n\n      // 等待编辑器加载\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      /**\n       * 第一步：填充标题\n       * Medium 的标题在 h3.graf--title 元素中\n       */\n      const titleEl = document.querySelector('h3.graf--title')\n      if (titleEl && title) {\n        titleEl.focus()\n        titleEl.textContent = title\n        titleEl.dispatchEvent(new Event('input', { bubbles: true }))\n        console.log('[COSE] Medium 标题填充成功')\n      }\n\n      /**\n       * 第二步：填充内容\n       * Medium 使用 contenteditable 编辑器，通过 paste 事件注入 HTML 内容\n       * 使用剪贴板 HTML（带完整样式）或降级到 body\n       */\n      const htmlContent = wechatHtml || body || ''\n      const contentEl = document.querySelector('p.graf--p')\n\n      if (contentEl && htmlContent) {\n        contentEl.focus()\n\n        // 创建 DataTransfer 并设置 HTML 内容\n        const dt = new DataTransfer()\n        dt.setData('text/html', htmlContent)\n        dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))\n\n        const pasteEvent = new ClipboardEvent('paste', {\n          bubbles: true,\n          cancelable: true,\n          clipboardData: dt\n        })\n\n        contentEl.dispatchEvent(pasteEvent)\n        console.log('[COSE] Medium 内容填充成功')\n      }\n    }\n    // 搜狐号 - 由 syncToPlatform 单独处理，这里跳过\n    else if (host.includes('mp.sohu.com')) {\n      console.log('[COSE] 搜狐号由 syncToPlatform 处理')\n    }\n    // ModelScope 魔搭社区\n    // 使用仓颉(cangjie)富文本编辑器，需要特殊的事件序列来触发\"转为富文本\"\n    else if (host.includes('modelscope.cn')) {\n      console.log('[COSE] ModelScope 开始同步...')\n\n      // 等待页面加载\n      await new Promise(resolve => setTimeout(resolve, 2000))\n\n      const textarea = document.querySelector('textarea')\n      const cangjieEditor = document.querySelector('[data-cangjie-editable=\"true\"]')\n\n      if (textarea) {\n        // 聚焦到 textarea\n        textarea.focus()\n\n        // 触发 paste 事件（这会设置内容并触发\"转为富文本\"）\n        // 注意：不要预先设置 textarea.value，否则会导致内容重复\n        try {\n          const clipboardData = new DataTransfer()\n          clipboardData.setData('text/plain', contentToFill)\n          const pasteEvent = new ClipboardEvent('paste', {\n            bubbles: true,\n            cancelable: true,\n            clipboardData: clipboardData\n          })\n          textarea.dispatchEvent(pasteEvent)\n        } catch (e) {\n          // 如果 ClipboardEvent 失败，降级到手动设置\n          const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n          textareaSetter.call(textarea, contentToFill)\n          textarea.dispatchEvent(new InputEvent('input', {\n            bubbles: true,\n            data: contentToFill,\n            inputType: 'insertText'\n          }))\n        }\n\n        textarea.dispatchEvent(new Event('change', { bubbles: true }))\n\n        console.log('[COSE] ModelScope 内容填充成功')\n\n        // 等待\"转为富文本\"按钮出现并点击\n        await new Promise(resolve => setTimeout(resolve, 800))\n\n        // 查找并点击\"转为富文本\"按钮\n        const findAndClickRichTextBtn = () => {\n          // 使用特定选择器\n          const richTextBtn = document.querySelector('[data-testid=\"menu-item-markdownToDoc\"][data-role=\"markdownToDoc\"]')\n          if (richTextBtn) {\n            console.log('[COSE] ModelScope 找到\"转为富文本\"按钮，点击中...')\n            richTextBtn.click()\n            return true\n          }\n\n          // 降级：查找文本匹配\n          const allElements = document.querySelectorAll('button, span, div, a, [role=\"button\"]')\n          for (const el of allElements) {\n            const text = el.textContent?.trim()\n            if (text === '转为富文本' || text?.includes('转为富文本')) {\n              console.log('[COSE] ModelScope 找到\"转为富文本\"按钮（通过文本），点击中...')\n              el.click()\n              return true\n            }\n          }\n          return false\n        }\n\n        // 尝试多次查找\n        let found = findAndClickRichTextBtn()\n        for (let i = 0; i < 5 && !found; i++) {\n          await new Promise(resolve => setTimeout(resolve, 500))\n          found = findAndClickRichTextBtn()\n        }\n\n        if (found) {\n          console.log('[COSE] ModelScope 已点击\"转为富文本\"')\n        } else {\n          console.log('[COSE] ModelScope 未找到\"转为富文本\"按钮（可能已自动转换）')\n        }\n      } else if (cangjieEditor) {\n        // 降级：直接操作仓颉编辑器\n        cangjieEditor.focus()\n        document.execCommand('selectAll', false, null)\n        document.execCommand('insertText', false, contentToFill)\n        console.log('[COSE] ModelScope 通过 execCommand 填充')\n      } else {\n        console.log('[COSE] ModelScope 未找到编辑器')\n      }\n    }\n    // 通用处理\n    else {\n      const titleSelectors = ['input[placeholder*=\"标题\"]', 'input[name=\"title\"]', 'textarea[placeholder*=\"标题\"]']\n      for (const sel of titleSelectors) {\n        const el = document.querySelector(sel)\n        if (el) { setInputValue(el, title); break }\n      }\n\n      const contentSelectors = ['.CodeMirror', '.ProseMirror', '.ql-editor', '[contenteditable=\"true\"]', 'textarea']\n      for (const sel of contentSelectors) {\n        const el = document.querySelector(sel)\n        if (el) {\n          if (el.CodeMirror) {\n            el.CodeMirror.setValue(contentToFill)\n          } else {\n            setInputValue(el, contentToFill)\n          }\n          break\n        }\n      }\n    }\n\n    console.log('[COSE] 内容已填充，请检查并发布')\n  }\n\n  fill().catch(console.error)\n}\n\n// 等待标签页加载\nfunction waitForTab(tabId, timeout = 60000) {\n  return new Promise((resolve, reject) => {\n    const start = Date.now()\n    let urlReady = false\n    let urlReadyTime = 0\n    const check = () => {\n      chrome.tabs.get(tabId, tab => {\n        if (chrome.runtime.lastError) {\n          reject(new Error(chrome.runtime.lastError.message))\n          return\n        }\n        if (tab.status === 'complete') {\n          setTimeout(resolve, 1500)\n          return\n        }\n        // 如果 URL 已经不是 about:blank/chrome:// 且处于 loading 状态超过 10 秒，\n        // 说明主文档已加载但第三方资源可能超时，提前 resolve\n        if (!urlReady && tab.url && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome:')) {\n          urlReady = true\n          urlReadyTime = Date.now()\n        }\n        if (urlReady && Date.now() - urlReadyTime > 10000) {\n          console.log('[COSE] waitForTab: 页面 URL 已就绪但 status 仍为 loading，提前继续')\n          setTimeout(resolve, 1500)\n          return\n        }\n        if (Date.now() - start > timeout) {\n          console.log('[COSE] waitForTab: 超时，继续执行')\n          resolve()\n        } else {\n          setTimeout(check, 300)\n        }\n      })\n    }\n    check()\n  })\n}\n\n// 安装时初始化\nchrome.runtime.onInstalled.addListener(() => {\n  console.log('MD 文章同步助手已安装')\n})\n"
  },
  {
    "path": "apps/extension/src/content.js",
    "content": "// Content Script - 在 md.doocs.org 或本地开发环境中运行\n// 注入 $cose 全局对象供页面使用\n\nconsole.log('[COSE Content Script] Loaded!')\nconsole.log('[COSE Content Script] URL:', window.location.href)\nconsole.log('[COSE Content Script] Hostname:', window.location.hostname)\n\n  ; (function () {\n    'use strict'\n\n    // 华为云开发者博客页面：自动获取并缓存用户信息\n    if (window.location.hostname.includes('huaweicloud.com')) {\n      console.log('[COSE] 检测到华为云页面')\n\n      setTimeout(async () => {\n        try {\n          const response = await fetch('https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'application/json' },\n          })\n          if (!response.ok) return\n\n          const data = await response.json()\n          if (data && data.memName) {\n            const userInfo = {\n              loggedIn: true,\n              username: data.memAlias || data.memName || '',\n              avatar: data.memPhoto || '',\n              cachedAt: Date.now(),\n            }\n\n            if (typeof chrome !== 'undefined' && chrome.runtime) {\n              chrome.runtime.sendMessage({\n                type: 'CACHE_USER_INFO',\n                platform: 'huaweicloud',\n                userInfo,\n              }).then(() => {\n                console.log('[COSE] 华为云用户信息已缓存:', userInfo.username)\n              }).catch(e => {\n                console.log('[COSE] 缓存失败:', e.message)\n              })\n            }\n          }\n        } catch (e) {\n          console.log('[COSE] 华为云用户信息缓存失败:', e.message)\n        }\n      }, 3000) // 华为云 SSO 登录需要几秒延迟\n    }\n\n    // 华为开发者页面：自动获取并缓存用户信息\n    if (window.location.hostname.includes('developer.huawei.com')) {\n      console.log('[COSE] 检测到华为开发者页面')\n\n      setTimeout(async () => {\n        try {\n          // 1. 从 DOM 获取社区用户名（真实昵称，非脱敏手机号）\n          const userNameEl = document.querySelector('.user_name')\n          const domUsername = userNameEl ? userNameEl.textContent.trim() : ''\n\n          // 2. 从 API 获取头像\n          const cookies = document.cookie.split(';').map(c => c.trim())\n          const udCookie = cookies.find(c => c.startsWith('developer_userdata='))\n          if (!udCookie && !domUsername) return\n\n          let avatar = ''\n          if (udCookie) {\n            const udValue = decodeURIComponent(udCookie.split('=').slice(1).join('='))\n            let csrfToken = ''\n            try {\n              const udJson = JSON.parse(udValue)\n              csrfToken = udJson.csrf || udJson.csrftoken || ''\n            } catch (e) { /* ignore */ }\n\n            if (csrfToken) {\n              const now = new Date()\n              const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}/, '')\n\n              try {\n                const response = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', {\n                  method: 'POST',\n                  credentials: 'include',\n                  headers: {\n                    'Content-Type': 'application/json',\n                    'Accept': 'application/json',\n                    'x-hd-csrf': csrfToken,\n                    'x-hd-date': hdDate,\n                  },\n                  body: JSON.stringify({\n                    svc: 'GOpen.User.getInfo',\n                    reqType: 0,\n                    reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }),\n                  }),\n                })\n                if (response.ok) {\n                  const data = await response.json()\n                  if (data && data.returnCode === '0' && data.resJson) {\n                    const userRes = JSON.parse(data.resJson)\n                    avatar = userRes.headPictureURL || ''\n                  }\n                }\n              } catch (e) { /* avatar fetch failed, continue */ }\n            }\n          }\n\n          if (domUsername || avatar) {\n            const userInfo = {\n              loggedIn: true,\n              username: domUsername,\n              avatar,\n              cachedAt: Date.now(),\n            }\n\n            if (typeof chrome !== 'undefined' && chrome.runtime) {\n              chrome.runtime.sendMessage({\n                type: 'CACHE_USER_INFO',\n                platform: 'huaweidev',\n                userInfo,\n              }).then(() => {\n                console.log('[COSE] 华为开发者用户信息已缓存:', userInfo.username)\n              }).catch(e => {\n                console.log('[COSE] 缓存失败:', e.message)\n              })\n            }\n          }\n        } catch (e) {\n          console.log('[COSE] 华为开发者用户信息缓存失败:', e.message)\n        }\n      }, 3000)\n    }\n\n    // 小红书页面：自动获取并缓存用户信息\n    if (window.location.hostname.includes('xiaohongshu.com')) {\n      console.log('[COSE] 检测到小红书页面')\n\n      // 通过 background script 处理缓存\n      setTimeout(async () => {\n        try {\n          console.log('[COSE] 开始获取小红书用户信息')\n          const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'application/json' }\n          })\n          console.log('[COSE] API 响应:', response.status)\n          if (!response.ok) return\n\n          const data = await response.json()\n          console.log('[COSE] API 数据:', data?.success, data?.code)\n          if (data?.success === true && data?.code === 0 && data?.data?.userId) {\n            const userInfo = {\n              loggedIn: true,\n              username: data.data.userName || data.data.redId || '',\n              avatar: data.data.userAvatar || '',\n              userId: data.data.userId,\n              cachedAt: Date.now()\n            }\n\n            // 发送给 background script 保存\n            if (typeof chrome !== 'undefined' && chrome.runtime) {\n              chrome.runtime.sendMessage({\n                type: 'CACHE_USER_INFO',\n                platform: 'xiaohongshu',\n                userInfo\n              }).then(() => {\n                console.log('[COSE] 小红书用户信息已缓存:', userInfo.username)\n              }).catch(e => {\n                console.log('[COSE] 缓存失败:', e.message)\n              })\n            } else {\n              console.log('[COSE] Chrome runtime 不可用')\n            }\n          }\n        } catch (e) {\n          console.log('[COSE] 缓存失败:', e.message)\n        }\n      }, 2000)\n    }\n\n    // 支付宝开放平台页面：自动获取并缓存用户信息\n    if (window.location.hostname.includes('alipay.com')) {\n      const cacheAlipayUserInfo = async () => {\n        try {\n          const response = await fetch('https://developerportal.alipay.com/octopus/service.do', {\n            method: 'POST',\n            credentials: 'include',\n            headers: {\n              'Accept': 'application/json',\n              'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',\n            },\n            body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query',\n          })\n          if (!response.ok) return\n\n          const data = await response.json()\n          if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) {\n            const userInfo = {\n              loggedIn: true,\n              username: data.data.nickname || '',\n              avatar: data.data.avatar || '',\n              cachedAt: Date.now()\n            }\n            // 通过 background script 保存\n            if (typeof chrome !== 'undefined' && chrome.runtime) {\n              chrome.runtime.sendMessage({\n                type: 'CACHE_USER_INFO',\n                platform: 'alipayopen',\n                userInfo\n              })\n            }\n            console.log('[COSE] 支付宝用户信息已缓存:', userInfo.username)\n          }\n        } catch (e) {\n          console.log('[COSE] 支付宝用户信息缓存失败:', e.message)\n        }\n      }\n\n      // 页面加载完成后获取用户信息\n      if (document.readyState === 'complete') {\n        cacheAlipayUserInfo()\n      } else {\n        window.addEventListener('load', cacheAlipayUserInfo)\n      }\n    }\n\n    // 注入脚本到页面主世界\n    const script = document.createElement('script')\n    script.src = chrome.runtime.getURL('bundles/inject.js')\n    script.onload = function () {\n      this.remove()\n    }\n      ; (document.head || document.documentElement).appendChild(script)\n\n    // 监听来自页面的消息\n    window.addEventListener('message', async (event) => {\n      if (event.source !== window) return\n      if (!event.data || event.data.source !== 'cose-page') return\n\n      const { type, requestId, payload } = event.data\n\n      try {\n        let result\n        switch (type) {\n          case 'GET_PLATFORMS':\n            result = await chrome.runtime.sendMessage({ type: 'GET_PLATFORMS' })\n            break\n          case 'CHECK_PLATFORM_STATUS':\n            result = await chrome.runtime.sendMessage({\n              type: 'CHECK_PLATFORM_STATUS',\n              platforms: payload?.platforms,\n            })\n            break\n          case 'CHECK_PLATFORM_STATUS_PROGRESSIVE':\n            result = await chrome.runtime.sendMessage({\n              type: 'CHECK_PLATFORM_STATUS_PROGRESSIVE',\n              platforms: payload?.platforms,\n            })\n            break\n          case 'START_SYNC_BATCH':\n            result = await chrome.runtime.sendMessage({ type: 'START_SYNC_BATCH' })\n            break\n          case 'GET_DEBUG_LOGS':\n            result = await chrome.runtime.sendMessage({ type: 'GET_DEBUG_LOGS' })\n            break\n          case 'SYNC_TO_PLATFORM':\n            result = await chrome.runtime.sendMessage({\n              type: 'SYNC_TO_PLATFORM',\n              platformId: payload?.platformId,\n              content: payload?.content,\n            })\n            break\n          default:\n            result = { error: 'Unknown type' }\n        }\n\n        // 发送响应回页面\n        window.postMessage(\n          {\n            source: 'cose-extension',\n            requestId,\n            result,\n          },\n          '*'\n        )\n      } catch (error) {\n        window.postMessage(\n          {\n            source: 'cose-extension',\n            requestId,\n            error: error.message,\n          },\n          '*'\n        )\n      }\n    })\n\n    // 监听来自页面的缓存请求（用于手动触发缓存）\n    window.addEventListener('message', (event) => {\n      if (event.source !== window) return\n\n      if (event.data.type === 'COSE_CACHE_USER' && event.data.platform === 'xiaohongshu') {\n        if (typeof chrome !== 'undefined' && chrome.storage) {\n          console.log('[COSE] 收到缓存请求，保存用户信息')\n          chrome.storage.local.set({ xiaohongshu_user: event.data.userInfo })\n            .then(() => console.log('[COSE] 小红书用户信息已手动缓存'))\n            .catch(e => console.log('[COSE] 缓存失败:', e))\n        } else {\n          console.log('[COSE] Chrome API 不可用，无法保存缓存')\n        }\n      }\n    })\n    // 监听来自 background 的消息并转发到页面（用于渐进式状态更新）\n    chrome.runtime.onMessage.addListener((message) => {\n      if (message.type === 'PLATFORM_STATUS_UPDATE' || message.type === 'PLATFORM_STATUS_COMPLETE') {\n        window.postMessage({\n          source: 'cose-extension',\n          type: message.type,\n          ...message\n        }, '*')\n      }\n    })\n  })()\n"
  },
  {
    "path": "apps/extension/src/inject.js",
    "content": "// 注入到页面主世界的脚本\n// 在 window 上暴露 $cose 对象供 Vue 组件使用\n\n; (function () {\n  'use strict'\n\n  let requestId = 0\n  const pendingRequests = new Map()\n  \n  // 渐进式更新的回调\n  let progressiveCallbacks = {\n    onProgress: null,\n    onComplete: null\n  }\n\n  // 监听来自 content script 的响应\n  window.addEventListener('message', (event) => {\n    if (event.source !== window) return\n    if (!event.data || event.data.source !== 'cose-extension') return\n\n    const { type, requestId: resId, result, error, platformId, platform, completed, total } = event.data\n    \n    // 处理渐进式平台状态更新\n    if (type === 'PLATFORM_STATUS_UPDATE') {\n      if (progressiveCallbacks.onProgress && platform && event.data.result) {\n        const platformResult = event.data.result\n        const account = {\n          uid: platform.id,\n          type: platform.type,\n          title: platform.title,\n          displayName: platformResult.loggedIn ? (platformResult.username || platform.title) : platform.title,\n          icon: platform.icon,\n          avatar: platformResult.avatar,\n          home: platform.url || '',\n          checked: false,\n          loggedIn: platformResult.loggedIn || false,\n          isChecking: false\n        }\n        progressiveCallbacks.onProgress(account, completed, total)\n      }\n      return\n    }\n    \n    // 处理渐进式检测完成\n    if (type === 'PLATFORM_STATUS_COMPLETE') {\n      if (progressiveCallbacks.onComplete) {\n        progressiveCallbacks.onComplete()\n      }\n      return\n    }\n\n    const pending = pendingRequests.get(resId)\n    if (pending) {\n      pendingRequests.delete(resId)\n      try {\n        if (error) {\n          // 检查是否是扩展上下文失效的错误\n          if (error.includes && error.includes('Extension context invalidated')) {\n            console.warn('[COSE] 扩展已重新加载，请刷新页面')\n            pending.reject(new Error('扩展已重新加载，请刷新页面'))\n          } else {\n            pending.reject(new Error(error))\n          }\n        } else {\n          pending.resolve(result)\n        }\n      } catch (e) {\n        console.warn('[COSE] 扩展上下文已失效，请刷新页面')\n        pending.reject(new Error('扩展上下文已失效，请刷新页面'))\n      }\n    }\n  })\n\n  // 发送消息到 content script 并等待响应\n  function sendMessage(type, payload) {\n    return new Promise((resolve, reject) => {\n      const id = ++requestId\n      pendingRequests.set(id, { resolve, reject })\n\n      window.postMessage(\n        {\n          source: 'cose-page',\n          type,\n          requestId: id,\n          payload,\n        },\n        '*'\n      )\n\n      // 超时处理\n      setTimeout(() => {\n        if (pendingRequests.has(id)) {\n          pendingRequests.delete(id)\n          reject(new Error('Request timeout'))\n        }\n      }, 120000)\n    })\n  }\n\n  // 平台配置（与 background.js 保持一致）\n  const PLATFORMS = [\n    { id: 'csdn', name: 'CSDN', icon: 'https://g.csdnimg.cn/static/logo/favicon32.ico', title: 'CSDN', type: 'csdn', url: 'https://blog.csdn.net/' },\n    { id: 'juejin', name: 'Juejin', icon: 'https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/static/favicons/favicon-32x32.png', title: '掘金', type: 'juejin', url: 'https://juejin.cn/' },\n    { id: 'wechat', name: 'WeChat', icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico', title: '微信公众号', type: 'wechat', url: 'https://mp.weixin.qq.com/' },\n    { id: 'zhihu', name: 'Zhihu', icon: 'https://static.zhihu.com/heifetz/favicon.ico', title: '知乎', type: 'zhihu', url: 'https://www.zhihu.com/signin' },\n    { id: 'toutiao', name: 'Toutiao', icon: 'https://sf3-cdn-tos.toutiaostatic.com/obj/eden-cn/uhbfnupkbps/toutiao_favicon.ico', title: '今日头条', type: 'toutiao', url: 'https://mp.toutiao.com/' },\n    { id: 'segmentfault', name: 'SegmentFault', icon: 'https://fastly.jsdelivr.net/gh/bucketio/img16@main/2026/02/01/1769960912823-e037663a-7f65-414e-a114-ed86b4e86964.png', title: '思否', type: 'segmentfault', url: 'https://segmentfault.com/user/login' },\n    { id: 'cnblogs', name: 'Cnblogs', icon: 'https://www.cnblogs.com/favicon.ico', title: '博客园', type: 'cnblogs', url: 'https://account.cnblogs.com/signin' },\n    { id: 'oschina', name: 'OSChina', icon: 'https://wsrv.nl/?url=static.oschina.net/new-osc/img/favicon.ico', title: '开源中国', type: 'oschina', url: 'https://www.oschina.net/home/login' },\n    { id: 'cto51', name: '51CTO', icon: 'https://blog.51cto.com/favicon.ico', title: '51CTO', type: 'cto51', url: 'https://home.51cto.com/index' },\n    { id: 'infoq', name: 'InfoQ', icon: 'https://static001.infoq.cn/static/write/img/write-favicon.jpg', title: 'InfoQ', type: 'infoq', url: 'https://xie.infoq.cn/' },\n    { id: 'jianshu', name: 'Jianshu', icon: 'https://www.jianshu.com/favicon.ico', title: '简书', type: 'jianshu', url: 'https://www.jianshu.com/sign_in' },\n    { id: 'baijiahao', name: 'Baijiahao', icon: 'https://pic.rmb.bdstatic.com/10e1e2b43c35577e1315f0f6aad6ba24.vnd.microsoft.icon', title: '百家号', type: 'baijiahao', url: 'https://baijiahao.baidu.com/' },\n    { id: 'wangyihao', name: 'Wangyihao', icon: 'https://static.ws.126.net/163/f2e/news/yxybd_pc/resource/static/share-icon.png', title: '网易号', type: 'wangyihao', url: 'https://mp.163.com/' },\n    { id: 'tencentcloud', name: 'TencentCloud', icon: 'https://cloudcache.tencent-cloud.com/qcloud/favicon.ico', title: '腾讯云开发者社区', type: 'tencentcloud', url: 'https://cloud.tencent.com/developer' },\n    { id: 'medium', name: 'Medium', icon: 'https://cdn.simpleicons.org/medium', title: 'Medium', type: 'medium', url: 'https://medium.com' },\n    { id: 'sspai', name: 'Sspai', icon: 'https://cdn-static.sspai.com/favicon/sspai.ico', title: '少数派', type: 'sspai', url: 'https://sspai.com' },\n    { id: 'sohu', name: 'Sohu', icon: 'https://statics.itc.cn/mp-new/icon/1.1/favicon.ico', title: '搜狐号', type: 'sohu', url: 'https://mp.sohu.com' },\n    { id: 'bilibili', name: 'Bilibili', icon: 'https://www.bilibili.com/favicon.ico', title: 'B站专栏', type: 'bilibili', url: 'https://member.bilibili.com/article-text/home?newEditor=-1' },\n    { id: 'weibo', name: 'Weibo', icon: 'https://weibo.com/favicon.ico', title: '微博头条', type: 'weibo', url: 'https://card.weibo.com/article/v5/editor#/draft' },\n    { id: 'aliyun', name: 'Aliyun', icon: 'https://img.alicdn.com/tfs/TB1_ZXuNcfpK1RjSZFOXXa6nFXa-32-32.ico', title: '阿里云开发者社区', type: 'aliyun', url: 'https://developer.aliyun.com/article/new#/' },\n    { id: 'huaweicloud', name: 'HuaweiCloud', icon: 'https://www.huaweicloud.com/favicon.ico', title: '华为云开发者博客', type: 'huaweicloud', url: 'https://bbs.huaweicloud.com/blogs/article' },\n    { id: 'huaweidev', name: 'HuaweiDev', icon: 'https://developer.huawei.com/favicon.ico', title: '华为开发者文章', type: 'huaweidev', url: 'https://developer.huawei.com/consumer/cn/blog/create' },\n    { id: 'twitter', name: 'Twitter', icon: 'https://abs.twimg.com/favicons/twitter.3.ico', title: 'Twitter Articles', type: 'twitter', url: 'https://x.com/compose/articles/edit/' },\n    { id: 'qianfan', name: 'Qianfan', icon: 'https://bce.bdstatic.com/img/favicon.ico', title: '百度云千帆', type: 'qianfan', url: 'https://qianfan.cloud.baidu.com/qianfandev/topic/create' },\n    { id: 'alipayopen', name: 'AlipayOpen', icon: 'https://www.alipay.com/favicon.ico', title: '支付宝开放平台', type: 'alipayopen', url: 'https://open.alipay.com/portal/forum/post/add#article' },\n    { id: 'modelscope', name: 'ModelScope', icon: 'https://img.alicdn.com/imgextra/i4/O1CN01fvt4it25rEZU4Gjso_!!6000000007579-2-tps-128-128.png', title: 'ModelScope 魔搭社区', type: 'modelscope', url: 'https://modelscope.cn/learn/create' },\n    { id: 'volcengine', name: 'Volcengine', icon: 'https://lf1-cdn-tos.bytegoofy.com/goofy/tech-fe/fav.png', title: '火山引擎开发者社区', type: 'volcengine', url: 'https://developer.volcengine.com/articles/draft' },\n    { id: 'douyin', name: 'Douyin', icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico', title: '抖音文章', type: 'douyin', url: 'https://creator.douyin.com/creator-micro/content/post/article?default-tab=5&enter_from=publish_page&media_type=article&type=new' },\n    { id: 'xiaohongshu', name: 'Xiaohongshu', icon: 'https://www.xiaohongshu.com/favicon.ico', title: '小红书', type: 'xiaohongshu', url: 'https://creator.xiaohongshu.com/publish/publish?from=menu&target=article' },\n    { id: 'elecfans', name: 'Elecfans', icon: 'https://www.elecfans.com/favicon.ico', title: '电子发烧友', type: 'elecfans', url: 'https://www.elecfans.com/d/article/md/' },\n    { id: 'douban', name: 'Douban', icon: 'https://cdn.simpleicons.org/douban/07C160', title: '豆瓣', type: 'douban', url: 'https://www.douban.com/' },\n  ]\n\n  // 暴露 $cose 全局对象\n  window.$cose = {\n    // 版本标识\n    version: '1.0.0',\n\n    // 获取支持的平台列表\n    getPlatforms() {\n      return PLATFORMS.map(p => ({\n        ...p,\n        uid: p.id,\n        displayName: p.title,\n        home: '',\n        checked: false,\n      }))\n    },\n\n    // 获取账号列表（带登录状态）\n    async getAccounts(callback) {\n      try {\n        // 获取登录状态\n        const result = await sendMessage('CHECK_PLATFORM_STATUS', { platforms: PLATFORMS })\n        const status = result?.status || {}\n\n        const accounts = PLATFORMS.map(p => {\n          const platformStatus = status[p.id] || {}\n          const isLoggedIn = platformStatus.loggedIn || false\n          return {\n            uid: p.id,\n            type: p.type,\n            title: p.title,\n            displayName: isLoggedIn ? (platformStatus.username || p.title) : p.title,\n            icon: p.icon,\n            avatar: platformStatus.avatar,\n            home: p.url || '',\n            checked: false,\n            loggedIn: isLoggedIn,\n          }\n        })\n\n        if (typeof callback === 'function') {\n          callback(accounts)\n        }\n        return accounts\n      } catch (error) {\n        console.error('获取账号列表失败:', error)\n        // 检测是否是扩展重新加载的错误\n        if (error.message && (error.message.includes('扩展已重新加载') || error.message.includes('Extension context'))) {\n          throw new Error('扩展已重新加载，请刷新页面后重试')\n        }\n        const accounts = PLATFORMS.map(p => ({\n          uid: p.id,\n          type: p.type,\n          title: p.title,\n          displayName: p.title,\n          icon: p.icon,\n          home: p.url || '',\n          checked: false,\n          loggedIn: false,\n        }))\n        if (typeof callback === 'function') {\n          callback(accounts)\n        }\n        return accounts\n      }\n    },\n\n    // 渐进式获取账号列表（每个平台检测完成后立即返回）\n    // onProgress(account, completed, total) - 每个平台完成时调用\n    // onComplete() - 所有平台完成时调用\n    getAccountsProgressive(onProgress, onComplete) {\n      progressiveCallbacks.onProgress = onProgress\n      progressiveCallbacks.onComplete = onComplete\n      \n      // 发送渐进式检测请求\n      sendMessage('CHECK_PLATFORM_STATUS_PROGRESSIVE', { platforms: PLATFORMS })\n        .catch(error => {\n          console.error('[COSE] 渐进式检测启动失败:', error)\n          // 如果启动失败，调用完成回调\n          if (typeof onComplete === 'function') {\n            onComplete()\n          }\n        })\n    },\n\n    // 添加发布任务（兼容 wechatsync 的 addTask 接口）\n    addTask(taskData, onProgress, onComplete) {\n      const { post, accounts } = taskData\n      const selectedAccounts = accounts.filter(a => a.checked)\n      const seenPlatformIds = new Set()\n      const syncAccounts = []\n\n      for (const account of selectedAccounts) {\n        const platformId = account.uid || account.type\n        if (!platformId) continue\n        if (seenPlatformIds.has(platformId)) {\n          console.log('[COSE] 跳过重复同步平台:', platformId)\n          continue\n        }\n        seenPlatformIds.add(platformId)\n        syncAccounts.push(account)\n      }\n\n      if (syncAccounts.length === 0) {\n        if (typeof onComplete === 'function') onComplete()\n        return\n      }\n\n      // 初始化状态\n      const status = {\n        accounts: syncAccounts.map(a => ({\n          ...a,\n          status: 'pending',\n          msg: '等待中',\n        })),\n      }\n\n      if (typeof onProgress === 'function') {\n        onProgress(status)\n      }\n\n      // 依次同步到各平台\n      const syncAll = async () => {\n        // 开始新的同步批次，将所有 tab 放入一个 group\n        await sendMessage('START_SYNC_BATCH', {})\n\n        // 检查是否需要同步到微信公众号或百家号或网易号或 Medium 或少数派或B站专栏或微博头条或小红书（需要使用剪贴板 HTML）\n        const hasWechat = syncAccounts.some(a => (a.uid || a.type) === 'wechat')\n        const hasBaijiahao = syncAccounts.some(a => (a.uid || a.type) === 'baijiahao')\n        const hasWangyihao = syncAccounts.some(a => (a.uid || a.type) === 'wangyihao')\n        const hasMedium = syncAccounts.some(a => (a.uid || a.type) === 'medium')\n        const hasSspai = syncAccounts.some(a => (a.uid || a.type) === 'sspai')\n        const hasBilibili = syncAccounts.some(a => (a.uid || a.type) === 'bilibili')\n        const hasWeibo = syncAccounts.some(a => (a.uid || a.type) === 'weibo')\n        const hasXiaohongshu = syncAccounts.some(a => (a.uid || a.type) === 'xiaohongshu')\n        let clipboardHtmlContent = null\n        if (hasWechat || hasBaijiahao || hasWangyihao || hasMedium || hasSspai || hasBilibili || hasWeibo || hasXiaohongshu) {\n          // 先点击复制按钮，将带样式的内容复制到剪贴板\n          const copyBtn = document.querySelector('.copy-btn') ||\n            document.querySelector('button[class*=\"copy\"]') ||\n            document.querySelector('button:has(.lucide-copy)') ||\n            Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('复制'))\n          if (copyBtn && typeof copyBtn.click === 'function') {\n            copyBtn.click()\n            // 等待复制完成\n            await new Promise(resolve => setTimeout(resolve, 2000))\n\n            // 读取剪贴板中的 HTML 内容\n            try {\n              const clipboardItems = await navigator.clipboard.read()\n              for (const item of clipboardItems) {\n                if (item.types.includes('text/html')) {\n                  const blob = await item.getType('text/html')\n                  clipboardHtmlContent = await blob.text()\n                  console.log('[COSE] 已读取剪贴板 HTML 内容，长度:', clipboardHtmlContent.length)\n                  break\n                }\n              }\n            } catch (e) {\n              console.log('[COSE] 读取剪贴板失败:', e.message)\n            }\n          }\n        }\n\n        for (let i = 0; i < syncAccounts.length; i++) {\n          const account = syncAccounts[i]\n          status.accounts[i].status = 'uploading'\n          status.accounts[i].msg = '同步中...'\n          if (typeof onProgress === 'function') onProgress({ ...status })\n\n          try {\n            const platformId = account.uid || account.type\n            const result = await sendMessage('SYNC_TO_PLATFORM', {\n              platformId,\n              content: {\n                title: post.title,\n                body: post.content,\n                markdown: post.markdown,\n                thumb: post.thumb,\n                desc: post.desc,\n                // 微信公众号、百家号、网易号、Medium、少数派、B站专栏、微博头条和小红书使用剪贴板中带样式的 HTML\n                wechatHtml: (platformId === 'wechat' || platformId === 'baijiahao' || platformId === 'wangyihao' || platformId === 'medium' || platformId === 'sspai' || platformId === 'bilibili' || platformId === 'weibo' || platformId === 'xiaohongshu') ? clipboardHtmlContent : null,\n              },\n            })\n\n            if (result?.success) {\n              status.accounts[i].status = 'done'\n              status.accounts[i].msg = '同步成功'\n              status.accounts[i].editResp = { draftLink: '' }\n            } else {\n              status.accounts[i].status = 'failed'\n              status.accounts[i].error = result?.message || '同步失败'\n            }\n          } catch (error) {\n            status.accounts[i].status = 'failed'\n            status.accounts[i].error = error.message || '同步失败'\n          }\n\n          if (typeof onProgress === 'function') onProgress({ ...status })\n        }\n\n        if (typeof onComplete === 'function') onComplete()\n      }\n\n      syncAll()\n    },\n  }\n\n  // 通知页面插件已加载\n  console.log('[COSE] 文章同步助手已加载')\n  window.dispatchEvent(new CustomEvent('cose-ready'))\n})()\n"
  },
  {
    "path": "apps/extension/src/offscreen.html",
    "content": "<!DOCTYPE html>\n<html>\n<head><title>COSE Offscreen</title></head>\n<body>\n<script src=\"bundles/offscreen.js\" type=\"module\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "apps/extension/src/offscreen.js",
    "content": "// Offscreen document for making fetch requests with cookies\n// This runs in a document context where credentials: 'include' actually works\n// (unlike the service worker where cookies are not sent/received automatically)\nchrome.runtime.onMessage.addListener((message, sender, sendResponse) => {\n  if (message.type === 'OFFSCREEN_PING') {\n    sendResponse({ pong: true })\n    return false\n  }\n\n  if (message.type === 'OFFSCREEN_FETCH') {\n    handleFetch(message.payload)\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n\n  if (message.type === 'OFFSCREEN_WARM_FETCH') {\n    handleWarmFetch(message.payload)\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n\n  if (message.type === 'OFFSCREEN_API_FETCH') {\n    handleApiFetch(message.payload)\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n\n  if (message.type === 'OFFSCREEN_DETECT_CTO51') {\n    handleDetectCto51()\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n\n  if (message.type === 'OFFSCREEN_DETECT_CNBLOGS') {\n    handleDetectCnblogs()\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n\n  if (message.type === 'OFFSCREEN_DETECT_XIAOHONGSHU') {\n    handleDetectXiaohongshu()\n      .then(result => sendResponse({ success: true, data: result }))\n      .catch(err => sendResponse({ success: false, error: err.message }))\n    return true\n  }\n})\n\nasync function handleFetch(payload) {\n  const { url, method, headers, body } = payload\n  const resp = await fetch(url, {\n    method: method || 'POST',\n    credentials: 'include',\n    headers: headers || {},\n    body: body ? JSON.stringify(body) : undefined,\n  })\n  if (!resp.ok) {\n    throw new Error(`HTTP ${resp.status}`)\n  }\n  return await resp.json()\n}\n\n/**\n * Warm-up fetch: makes a request with credentials: 'include' to trigger\n * the browser's cookie restoration (SSO, session cookies, etc.)\n * Returns status and response headers info, not the full body.\n */\nasync function handleWarmFetch(payload) {\n  const { url, redirect } = payload\n  try {\n    const resp = await fetch(url, {\n      method: 'GET',\n      credentials: 'include',\n      redirect: redirect || 'follow',\n    })\n    // Read a small portion to ensure the response is consumed\n    const text = await resp.text()\n    return {\n      status: resp.status,\n      url: resp.url,\n      length: text.length,\n    }\n  } catch (e) {\n    return { error: e.message }\n  }\n}\n\n/**\n * API fetch: makes a request with credentials: 'include' and returns the response body.\n * Used for API calls that need cookies automatically attached (since service worker\n * fetch() strips manually-set Cookie headers in MV3).\n */\nasync function handleApiFetch(payload) {\n  const { url, method, headers, responseType, redirect } = payload\n  try {\n    const resp = await fetch(url, {\n      method: method || 'GET',\n      credentials: 'include',\n      headers: headers || {},\n      redirect: redirect || 'follow',\n    })\n    const status = resp.status\n    const finalUrl = resp.url\n    let body = null\n    if (responseType === 'json') {\n      try { body = await resp.json() } catch (e) { body = null }\n    } else {\n      body = await resp.text()\n    }\n    return { status, url: finalUrl, body }\n  } catch (e) {\n    return { error: e.message }\n  }\n}\n\n\n/**\n * 51CTO detection: fetch home.51cto.com/space and parse with DOMParser.\n * Same approach as 爱贝壳 extension - runs in document context (offscreen).\n */\nasync function handleDetectCto51() {\n  try {\n    const resp = await fetch('https://home.51cto.com/space', {\n      credentials: 'include',\n    })\n    const html = await resp.text()\n    const doc = new DOMParser().parseFromString(html, 'text/html')\n\n    // Avatar: <img alt=\"头像\">\n    const avatarEl = doc.querySelector(\"img[alt='头像']\")\n    const avatar = avatarEl ? avatarEl.getAttribute('src') : ''\n\n    // UID from avatar URL: uid=(\\d+)\n    let uid = ''\n    if (avatar) {\n      const m = avatar.match(/uid=(\\d+)/)\n      if (m) uid = m[1]\n    }\n\n    // Nickname: div.name > a\n    const nameEl = doc.querySelector('div.name > a')\n    const username = nameEl ? nameEl.textContent.trim() : ''\n\n    if (!username && !uid) {\n      // Return debug info to help diagnose\n      const title = doc.querySelector('title')?.textContent || ''\n      return { loggedIn: false, _debug: { status: resp.status, url: resp.url, htmlLen: html.length, title } }\n    }\n\n    return { loggedIn: true, username, avatar, uid }\n  } catch (e) {\n    return { loggedIn: false, error: e.message }\n  }\n}\n\n\n/**\n * Cnblogs detection: fetch account.cnblogs.com/user/userinfo in document context.\n * Cookies are sent automatically (unlike service worker fetch which strips Cookie headers).\n */\nasync function handleDetectCnblogs() {\n  try {\n    const resp = await fetch('https://account.cnblogs.com/user/userinfo', {\n      method: 'GET',\n      credentials: 'include',\n      headers: { 'Accept': 'application/json' },\n    })\n    if (!resp.ok) return { loggedIn: false }\n\n    const data = await resp.json()\n    if (!data?.spaceUserId) return { loggedIn: false }\n\n    const username = data.displayName || ''\n    let avatar = data.iconName || ''\n    if (avatar && !avatar.startsWith('http')) {\n      avatar = 'https:' + avatar\n    }\n\n    return { loggedIn: true, username, avatar }\n  } catch (e) {\n    return { loggedIn: false, error: e.message }\n  }\n}\n\n\n/**\n * Xiaohongshu detection: fetch creator API in document context.\n * Cookies are sent automatically with credentials: 'include'.\n */\nasync function handleDetectXiaohongshu() {\n  try {\n    const resp = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', {\n      method: 'GET',\n      credentials: 'include',\n      headers: { 'Accept': 'application/json' },\n    })\n    if (!resp.ok) return { loggedIn: false }\n\n    const data = await resp.json()\n    if (data?.success === true && data?.code === 0 && data?.data?.userId) {\n      return {\n        loggedIn: true,\n        username: data.data.userName || data.data.redId || '',\n        avatar: data.data.userAvatar || '',\n        userId: data.data.userId,\n      }\n    }\n    return { loggedIn: false }\n  } catch (e) {\n    return { loggedIn: false, error: e.message }\n  }\n}\n\n"
  },
  {
    "path": "apps/extension/src/popup.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n  <meta charset=\"UTF-8\">\n  <style>\n    * {\n      margin: 0;\n      padding: 0;\n      box-sizing: border-box;\n    }\n    body {\n      width: 320px;\n      padding: 20px;\n      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n      background: #fff;\n      color: #333;\n    }\n    .container {\n      text-align: center;\n    }\n    h1 {\n      font-size: 18px;\n      font-weight: 600;\n      margin-bottom: 12px;\n    }\n    p {\n      font-size: 13px;\n      line-height: 1.6;\n      color: #333;\n      margin-bottom: 16px;\n    }\n    p a {\n      color: #FE5200;\n      text-decoration: none;\n    }\n    p a:hover {\n      text-decoration: underline;\n    }\n    .btn {\n      display: inline-block;\n      padding: 10px 20px;\n      background: #FE5200;\n      color: #fff;\n      text-decoration: none;\n      border-radius: 8px;\n      font-size: 14px;\n      font-weight: 500;\n      transition: background 0.2s;\n      border: 1px solid #e64a00;\n      cursor: pointer;\n    }\n    .btn:hover {\n      background: #e64a00;\n    }\n    .divider {\n      margin: 16px 0;\n      border-top: 1px solid #e5e5e5;\n    }\n    .hint {\n      font-size: 12px;\n      color: #666;\n    }\n  </style>\n</head>\n<body>\n  <div class=\"container\">\n    <h1>COSE - 多平台文章同步</h1>\n    <p>插件安装完成！</p>\n    <p>打开<a href=\"https://github.com/doocs/md\" target=\"_blank\">自托管的编辑器</a>即可自动检测到，无需多余配置。</p>\n    <div class=\"divider\"></div>\n    <p class=\"hint\">没有自托管编辑器？</p>\n    <a href=\"https://md.doocs.org\" target=\"_blank\" class=\"btn\" id=\"openOfficial\">也可使用官网编辑器</a>\n  </div>\n  <script src=\"bundles/popup.js\" type=\"module\"></script>\n</body>\n</html>\n"
  },
  {
    "path": "apps/extension/src/popup.js",
    "content": "// Popup script for COSE extension\ndocument.getElementById('openOfficial').addEventListener('click', (e) => {\n  e.preventDefault()\n  chrome.tabs.create({ url: 'https://md.doocs.org' })\n  window.close()\n})\n"
  },
  {
    "path": "apps/extension/tsconfig.json",
    "content": "{\n    \"compilerOptions\": {\n        \"target\": \"ESNext\",\n        \"module\": \"ESNext\",\n        \"moduleResolution\": \"bundler\",\n        \"strict\": true,\n        \"esModuleInterop\": true,\n        \"skipLibCheck\": true,\n        \"resolveJsonModule\": true\n    },\n    \"include\": [\n        \"scripts/**/*.ts\"\n    ]\n}"
  },
  {
    "path": "apps/extension/vite.config.js",
    "content": "import { defineConfig } from 'vite'\nimport { join, resolve } from 'path'\nimport { viteStaticCopy } from 'vite-plugin-static-copy'\n\nexport default defineConfig({\n    root: '.', // 项目根目录\n    resolve: {\n        alias: {\n            '@cose/core': resolve(__dirname, '../../packages/core'),\n            '@cose/detection': resolve(__dirname, '../../packages/detection')\n        }\n    },\n    build: {\n        outDir: 'dist',\n        emptyOutDir: true,\n        minify: false, // 方便调试，发布时可开启\n        modulePreload: false,\n        rollupOptions: {\n            input: {\n                background: resolve(__dirname, 'src/background.js'),\n                content: resolve(__dirname, 'src/content.js'),\n                inject: resolve(__dirname, 'src/inject.js'),\n                offscreen: resolve(__dirname, 'src/offscreen.js'),\n                popup: resolve(__dirname, 'src/popup.js'),\n            },\n            output: {\n                entryFileNames: 'bundles/[name].js',\n                chunkFileNames: 'bundles/chunks/[name].js',\n                assetFileNames: 'assets/[name].[ext]',\n                format: 'es', // ES Modules，适配 Manifest V3\n            },\n        },\n    },\n    plugins: [\n        viteStaticCopy({\n            targets: [\n                // manifest.json 由 scripts/cli.ts 生成\n                {\n                    src: 'icons',\n                    dest: '.',\n                },\n                {\n                    src: 'src/offscreen.html',\n                    dest: '.',\n                },\n                {\n                    src: 'src/popup.html',\n                    dest: '.',\n                },\n                {\n                    src: 'assets',\n                    dest: '.',\n                },\n            ],\n        }),\n    ],\n})\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"cose-monorepo\",\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"pnpm -C apps/extension build\",\n    \"build:firefox\": \"pnpm -C apps/extension build:firefox\",\n    \"build:safari\": \"pnpm -C apps/extension build:safari\",\n    \"dev\": \"pnpm -C apps/extension dev\",\n    \"dev:chrome\": \"pnpm -C apps/extension dev:chrome\",\n    \"dev:watch\": \"pnpm -C apps/extension dev:watch\",\n    \"lint\": \"pnpm -r lint\"\n  },\n  \"devDependencies\": {\n    \"typescript\": \"^5.0.0\"\n  }\n}"
  },
  {
    "path": "packages/core/index.js",
    "content": "export * from './src/utils.js';\n// Re-export login detection from @cose/detection\nexport * from '@cose/detection';\n// Platform exports will be handled dynamically or imported directly\n"
  },
  {
    "path": "packages/core/package.json",
    "content": "{\n    \"name\": \"@cose/core\",\n    \"version\": \"1.0.0\",\n    \"description\": \"Core publishing logic for COSE\",\n    \"main\": \"index.js\",\n    \"type\": \"module\",\n    \"exports\": {\n        \".\": \"./index.js\",\n        \"./platforms/*\": \"./src/platforms/*.js\",\n        \"./utils\": \"./src/utils.js\"\n    },\n    \"dependencies\": {\n        \"@cose/detection\": \"workspace:*\"\n    }\n}"
  },
  {
    "path": "packages/core/src/platforms/alipayopen.js",
    "content": "// 支付宝开放平台配置\nconst AlipayOpenPlatform = {\n  id: 'alipayopen',\n  name: 'AlipayOpen',\n  icon: 'https://www.alipay.com/favicon.ico',\n  url: 'https://open.alipay.com',\n  publishUrl: 'https://open.alipay.com/portal/forum/post/add#article',\n  title: '支付宝开放平台',\n  type: 'alipayopen',\n}\n\n/**\n * 支付宝开放平台内容填充函数\n * 注意：此函数会被序列化后通过 chrome.scripting.executeScript 注入页面执行\n * 因此必须是自包含的，不能依赖外部模块或闭包\n * @param {string} title - 文章标题\n * @param {string} markdown - Markdown 内容\n */\nfunction fillAlipayOpenContent(title, markdown) {\n  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n  \n  return (async () => {\n    try {\n      console.log('[COSE] 支付宝开放平台 开始填充, 标题:', title)\n\n      // 等待页面加载\n      await sleep(500)\n\n      // 填充标题 - 尝试多种选择器\n      let titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n      if (!titleInput) {\n        titleInput = document.querySelector('input[placeholder*=\"请输入\"]')\n      }\n      if (!titleInput) {\n        const allInputs = document.querySelectorAll('input')\n        for (const inp of allInputs) {\n          if (inp.placeholder && inp.placeholder.includes('标题')) {\n            titleInput = inp\n            break\n          }\n        }\n      }\n      \n      console.log('[COSE] 支付宝开放平台 查找标题输入框:', !!titleInput)\n      \n      if (titleInput && title) {\n        titleInput.focus()\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n        nativeSetter.call(titleInput, title)\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        titleInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }))\n        console.log('[COSE] 支付宝开放平台 标题填充成功:', title)\n      } else {\n        console.log('[COSE] 支付宝开放平台 标题填充失败 - input:', !!titleInput, 'title:', !!title)\n      }\n\n      await sleep(300)\n\n      // 填充内容 - 支付宝开放平台使用 ne-engine 富文本编辑器\n      const editor = document.querySelector('.ne-engine')\n      if (editor && markdown) {\n        editor.focus()\n        await sleep(100)\n\n        // 使用 ClipboardEvent 模拟粘贴 Markdown 内容\n        const dt = new DataTransfer()\n        dt.setData('text/plain', markdown)\n        \n        const pasteEvent = new ClipboardEvent('paste', {\n          bubbles: true,\n          cancelable: true,\n          clipboardData: dt\n        })\n        \n        editor.dispatchEvent(pasteEvent)\n        console.log('[COSE] 支付宝开放平台 内容粘贴成功')\n\n        // 等待并点击\"立即转换\"按钮\n        let confirmed = false\n        for (let i = 0; i < 15; i++) {\n          await sleep(200)\n          const convertBtn = Array.from(document.querySelectorAll('button')).find(\n            btn => btn.textContent.includes('立即转换')\n          )\n          if (convertBtn) {\n            convertBtn.click()\n            confirmed = true\n            console.log('[COSE] 支付宝开放平台 Markdown 转换成功')\n            break\n          }\n        }\n\n        return { success: true, confirmed }\n      }\n\n      return { success: false, error: '未找到编辑器' }\n    } catch (e) {\n      console.error('[COSE] 支付宝开放平台 填充失败:', e)\n      return { success: false, error: e.message }\n    }\n  })()\n}\n\n// 导出\nexport { AlipayOpenPlatform, fillAlipayOpenContent }\n"
  },
  {
    "path": "packages/core/src/platforms/aliyun.js",
    "content": "// 阿里云开发者社区平台配置\nconst AliyunPlatform = {\n  id: 'aliyun',\n  name: 'Aliyun',\n  icon: 'https://img.alicdn.com/tfs/TB1_ZXuNcfpK1RjSZFOXXa6nFXa-32-32.ico',\n  url: 'https://developer.aliyun.com/',\n  publishUrl: 'https://developer.aliyun.com/article/new#/',\n  title: '阿里云开发者社区',\n  type: 'aliyun',\n}\n\n// 阿里云开发者社区内容填充函数\nasync function fillAliyunContent(content) {\n  const { title, markdown } = content\n  \n  console.log('[COSE] 阿里云开发者社区：开始填充内容')\n  \n  // 等待页面加载\n  await new Promise(resolve => setTimeout(resolve, 2000))\n  \n  // 填充标题\n  const titleInput = document.querySelector('input[placeholder*=\"标题\"]')\n  if (titleInput && title) {\n    titleInput.focus()\n    titleInput.value = title\n    titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 阿里云开发者社区：标题已填充')\n  }\n  \n  // 等待一下再填充正文\n  await new Promise(resolve => setTimeout(resolve, 500))\n  \n  // 填充正文（markdown 编辑器）\n  // 阿里云开发者社区使用的是 markdown 编辑器，textarea 是主要输入区域\n  const contentTextarea = document.querySelector('textarea[class*=\"editor\"]') ||\n    document.querySelector('.markdown-editor textarea') ||\n    document.querySelector('textarea')\n  \n  if (contentTextarea && markdown) {\n    contentTextarea.focus()\n    contentTextarea.value = markdown\n    contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))\n    contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 阿里云开发者社区：正文已填充')\n  }\n  \n  console.log('[COSE] 阿里云开发者社区：内容填充完成')\n}\n\n// 导出\nexport { AliyunPlatform, fillAliyunContent }\n"
  },
  {
    "path": "packages/core/src/platforms/baijiahao.js",
    "content": "// 百家号平台配置\nconst BaijiahaoPlat = {\n  id: 'baijiahao',\n  name: 'Baijiahao',\n  icon: 'https://pic.rmb.bdstatic.com/10e1e2b43c35577e1315f0f6aad6ba24.vnd.microsoft.icon',\n  url: 'https://baijiahao.baidu.com',\n  publishUrl: 'https://baijiahao.baidu.com/builder/rc/edit?type=news',\n  title: '百家号',\n  type: 'baijiahao',\n}\n\n// 百家号内容填充函数\nasync function fillBaijiahaoContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n  const contentToFill = markdown || body || ''\n\n  // 1. 填充标题\n  // 百家号标题输入框在 .client_components_titleInput 内的 contenteditable div\n  await new Promise(resolve => setTimeout(resolve, 1000))\n  \n  const titleEditor = document.querySelector('.client_components_titleInput [contenteditable=\"true\"]') ||\n    document.querySelector('.client_pages_edit_components_titleInput [contenteditable=\"true\"]') ||\n    document.querySelector('[class*=\"titleInput\"] [contenteditable=\"true\"]')\n\n  if (titleEditor) {\n    titleEditor.focus()\n    // 清空现有内容\n    titleEditor.innerHTML = ''\n    // 使用 document.execCommand 插入文本\n    document.execCommand('insertText', false, title)\n    // 如果 execCommand 不生效，使用备用方案\n    if (!titleEditor.textContent) {\n      titleEditor.innerHTML = `<p dir=\"auto\">${title}</p>`\n    }\n    titleEditor.dispatchEvent(new Event('input', { bubbles: true }))\n    titleEditor.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 百家号标题填充成功')\n  } else {\n    console.log('[COSE] 百家号未找到标题输入框')\n  }\n\n  // 2. 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 1500))\n\n  // 3. 填充正文内容\n  // 百家号使用 UEditor，内容在 iframe 中\n  const iframe = document.querySelector('iframe')\n  if (iframe && iframe.contentDocument) {\n    const iframeBody = iframe.contentDocument.body\n    if (iframeBody && iframeBody.contentEditable === 'true') {\n      iframeBody.focus()\n      // 将 markdown 转换为简单的 HTML 段落\n      const htmlContent = contentToFill\n        .split('\\n\\n')\n        .map(p => `<p>${p.replace(/\\n/g, '<br>')}</p>`)\n        .join('')\n      iframeBody.innerHTML = htmlContent\n      iframeBody.dispatchEvent(new Event('input', { bubbles: true }))\n      console.log('[COSE] 百家号 iframe 编辑器填充成功')\n      return\n    }\n  }\n\n  // 尝试通过 UEditor API 填充\n  if (window.UE_V2 && window.UE_V2.instants && window.UE_V2.instants.ueditorInstant0) {\n    try {\n      const editor = window.UE_V2.instants.ueditorInstant0\n      const htmlContent = contentToFill\n        .split('\\n\\n')\n        .map(p => `<p>${p.replace(/\\n/g, '<br>')}</p>`)\n        .join('')\n      editor.setContent(htmlContent)\n      console.log('[COSE] 百家号通过 UEditor API 填充成功')\n      return\n    } catch (e) {\n      console.log('[COSE] 百家号 UEditor API 调用失败', e)\n    }\n  }\n\n  // 降级：尝试直接操作 contenteditable\n  const contentEditor = document.querySelector('[contenteditable=\"true\"]:not([class*=\"title\"])')\n  if (contentEditor) {\n    contentEditor.focus()\n    contentEditor.innerHTML = contentToFill.replace(/\\n/g, '<br>')\n    contentEditor.dispatchEvent(new Event('input', { bubbles: true }))\n    console.log('[COSE] 百家号 contenteditable 降级填充成功')\n  } else {\n    console.log('[COSE] 百家号未找到编辑器元素')\n  }\n}\n\n// 导出\nexport { BaijiahaoPlat as BaijiahaoPlatform, fillBaijiahaoContent }\n"
  },
  {
    "path": "packages/core/src/platforms/bilibili.js",
    "content": "// B站专栏平台配置（使用旧版编辑器，基于 UEditor）\n// 同步方式：使用 UEditor execCommand('inserthtml') 插入 HTML\nconst BilibiliPlatform = {\n  id: 'bilibili',\n  name: 'Bilibili',\n  icon: 'https://www.bilibili.com/favicon.ico',\n  url: 'https://member.bilibili.com',\n  publishUrl: 'https://member.bilibili.com/article-text/home?newEditor=-1',\n  title: 'B站专栏',\n  type: 'bilibili',\n}\n\n// B站专栏内容填充函数（由 background.js 处理）\n// 使用 UEditor 的 execCommand('inserthtml') 方法插入 HTML 内容\nasync function fillBilibiliContent(content, waitFor, setInputValue) {\n  console.log('[COSE] B站专栏填充由 background.js 处理')\n}\n\n// 导出\nexport { BilibiliPlatform, fillBilibiliContent }\n"
  },
  {
    "path": "packages/core/src/platforms/cnblogs.js",
    "content": "// 博客园平台配置\nconst CnblogsPlatform = {\n  id: 'cnblogs',\n  name: 'Cnblogs',\n  icon: 'https://www.cnblogs.com/favicon.ico',\n  url: 'https://www.cnblogs.com',\n  publishUrl: 'https://i.cnblogs.com/posts/edit',\n  title: '博客园',\n  type: 'cnblogs',\n}\n\n// 博客园内容填充函数\nasync function fillCnblogsContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n  const contentToFill = markdown || body || ''\n\n  // 填充标题\n  const titleInput = await waitFor('#post-title, input[placeholder*=\"标题\"]')\n  if (titleInput) {\n    titleInput.focus()\n    titleInput.value = title\n    titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 博客园标题填充成功')\n  }\n\n  // 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 1000))\n\n  // 博客园使用 TinyMCE 或 Markdown 编辑器\n  // 尝试 Markdown 模式\n  const cmElement = document.querySelector('.CodeMirror')\n  if (cmElement && cmElement.CodeMirror) {\n    cmElement.CodeMirror.setValue(contentToFill)\n    console.log('[COSE] 博客园 CodeMirror 填充成功')\n    return\n  }\n\n  // 尝试 TinyMCE\n  if (window.tinymce && window.tinymce.activeEditor) {\n    window.tinymce.activeEditor.setContent(contentToFill)\n    console.log('[COSE] 博客园 TinyMCE 填充成功')\n    return\n  }\n\n  // 降级到 textarea\n  const textarea = document.querySelector('#post-body, textarea')\n  if (textarea) {\n    textarea.focus()\n    textarea.value = contentToFill\n    textarea.dispatchEvent(new Event('input', { bubbles: true }))\n    console.log('[COSE] 博客园 textarea 填充成功')\n  } else {\n    console.log('[COSE] 博客园 未找到编辑器')\n  }\n}\n\n// 导出\nexport { CnblogsPlatform, fillCnblogsContent }\n"
  },
  {
    "path": "packages/core/src/platforms/common.js",
    "content": "// Re-export from utils.js for backward compatibility\nexport * from '../utils.js'\nexport { injectUtils } from '../utils.js'\n"
  },
  {
    "path": "packages/core/src/platforms/csdn.js",
    "content": "// CSDN 平台配置\nconst CSDNPlatform = {\n  id: 'csdn',\n  name: 'CSDN',\n  icon: 'https://g.csdnimg.cn/static/logo/favicon32.ico',\n  url: 'https://blog.csdn.net',\n  publishUrl: 'https://editor.csdn.net/md/',\n  title: 'CSDN',\n  type: 'csdn',\n}\n\nimport { injectUtils } from './common.js'\n\n// CSDN 内容填充函数（在页面主世界中执行）\n// 此函数会被序列化后注入到页面中执行\n// 注意：需要先调用 injectUtils 注入 window.waitFor\nfunction fillCSDNContent(title, markdown, body) {\n  const contentToFill = markdown || body || ''\n\n  async function fill() {\n    // 填充标题（使用注入的 window.waitFor）\n    const titleInput = await window.waitFor('.article-bar__title input, input[placeholder*=\"标题\"]')\n    if (titleInput && title) {\n      titleInput.focus()\n      titleInput.value = title\n      titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    }\n\n    // 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 1000))\n\n    // CSDN 使用 contenteditable 的 PRE 元素\n    const editor = document.querySelector('.editor__inner[contenteditable=\"true\"], [contenteditable=\"true\"].markdown-highlighting')\n\n    if (editor) {\n      editor.focus()\n      // 清空现有内容\n      editor.textContent = ''\n      // 直接设置文本内容\n      editor.textContent = contentToFill\n      // 触发 input 事件让编辑器识别变化\n      editor.dispatchEvent(new Event('input', { bubbles: true }))\n      console.log('[COSE] CSDN contenteditable 填充成功')\n      return { success: true, method: 'contenteditable' }\n    } else {\n      // 降级尝试其他方式\n      const cmElement = document.querySelector('.CodeMirror')\n      if (cmElement && cmElement.CodeMirror) {\n        cmElement.CodeMirror.setValue(contentToFill)\n        console.log('[COSE] CSDN CodeMirror 填充成功')\n        return { success: true, method: 'CodeMirror' }\n      } else {\n        console.log('[COSE] CSDN 未找到编辑器元素')\n        return { success: false, error: 'Editor not found' }\n      }\n    }\n  }\n\n  return fill()\n}\n\n/**\n * CSDN 同步处理器\n * @param {object} tab - Chrome tab 对象\n * @param {object} content - 内容对象 { title, body, markdown }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncCSDNContent(tab, content, helpers) {\n  const { chrome } = helpers\n\n  // 等待页面加载\n  await new Promise(resolve => setTimeout(resolve, 2000))\n\n  // 先注入公共工具函数（waitFor, setInputValue）\n  await injectUtils(chrome, tab.id)\n\n  // 在页面中执行填充脚本\n  const result = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: fillCSDNContent,\n    args: [content.title, content.markdown, content.body],\n    world: 'MAIN',\n  })\n\n  const fillResult = result?.[0]?.result\n  if (fillResult?.success) {\n    return { success: true, message: '已同步到 CSDN', tabId: tab.id }\n  } else {\n    return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n  }\n}\n\n// 导出\nexport { CSDNPlatform, fillCSDNContent, syncCSDNContent }\n\n"
  },
  {
    "path": "packages/core/src/platforms/cto51.js",
    "content": "// 51CTO 平台配置\nconst CTO51Platform = {\n    id: 'cto51',\n    name: '51CTO',\n    icon: 'https://blog.51cto.com/favicon.ico',\n    url: 'https://blog.51cto.com',\n    loginUrl: 'https://home.51cto.com/index/login',\n    publishUrl: 'https://blog.51cto.com/blogger/publish',\n    title: '51CTO',\n    type: 'cto51',\n}\n\n// 51CTO 内容填充函数\nasync function fillCTO51Content(content, waitFor, setInputValue) {\n    const { title, body, markdown } = content\n    const contentToFill = markdown || body || ''\n\n    // 1. 填充标题\n    // 51CTO 标题输入框通常是 input#title 或 placeholder=\"请输入标题\"\n    const titleInput = await waitFor('#title, input[placeholder*=\"标题\"]')\n    if (titleInput) {\n        setInputValue(titleInput, title)\n        console.log('[COSE] 51CTO 标题填充成功')\n    }\n\n    // 2. 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 2000))\n\n    // 3. 填充内容\n    // 51CTO 有 Markdown 编辑器和富文本编辑器，通常默认 Markdown\n    // 尝试寻找 Markdown 编辑器的 textarea 或 CodeMirror\n    const editor = document.querySelector('.editormd-markdown-textarea') || // Editor.md\n        document.querySelector('#my-editormd-markdown-doc') ||   // 常见 ID\n        document.querySelector('.CodeMirror textarea') ||          // CodeMirror 核心\n        document.querySelector('textarea[name=\"content\"]')         // 通用 fallback\n\n    if (editor) {\n        // 如果是 CodeMirror，通常需要操作 DOM 或使用 setValue\n        // 尝试直接设置 value 并触发事件\n        editor.focus()\n        editor.value = contentToFill\n        editor.dispatchEvent(new Event('input', { bubbles: true }))\n        editor.dispatchEvent(new Event('change', { bubbles: true }))\n\n        // 如果页面上有 editor.md 的全局实例，尝试调用\n        // 这需要在 page context 执行，目前 fillContentOnPage 是在 Main world 执行的，所以可以访问 window\n        if (window.editor) {\n            try {\n                window.editor.setMarkdown(contentToFill)\n                console.log('[COSE] 51CTO 通过 window.editor 设置成功')\n                return\n            } catch (e) {\n                console.log('[COSE] 51CTO window.editor 调用失败', e)\n            }\n        }\n\n        console.log('[COSE] 51CTO textarea 填充尝试完成')\n    } else {\n        console.log('[COSE] 51CTO 未找到编辑器元素，尝试降级 contenteditable')\n\n        // 可能是富文本模式的 contenteditable\n        const contentEditable = document.querySelector('[contenteditable=\"true\"]')\n        if (contentEditable) {\n            contentEditable.innerHTML = contentToFill.replace(/\\n/g, '<br>')\n            console.log('[COSE] 51CTO contenteditable 填充成功')\n        }\n    }\n}\n\n// 导出\nexport { CTO51Platform, fillCTO51Content }\n"
  },
  {
    "path": "packages/core/src/platforms/douban.js",
    "content": "// 豆瓣平台配置\nconst DoubanPlatform = {\n  id: 'douban',\n  name: 'Douban',\n  icon: 'https://cdn.simpleicons.org/douban/07C160',\n  url: 'https://www.douban.com',\n  publishUrl: 'https://www.douban.com/',\n  title: '豆瓣',\n  type: 'douban',\n}\n\n// 导出\nexport { DoubanPlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/douyin.js",
    "content": "// 抖音创作者平台配置\nconst DouyinPlatform = {\n  id: 'douyin',\n  name: 'Douyin',\n  icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico',\n  url: 'https://creator.douyin.com/',\n  publishUrl: 'https://creator.douyin.com/creator-micro/content/post/article?default-tab=5&enter_from=publish_page&media_type=article&type=new',\n  title: '抖音',\n  type: 'douyin',\n}\n\n\n\n// 导出\nexport { DouyinPlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/elecfans.js",
    "content": "// 电子发烧友平台配置\n\nexport const ElecfansPlatform = {\n  id: 'elecfans',\n  name: '电子发烧友',\n  icon: 'https://www.elecfans.com/favicon.ico',\n  publishUrl: 'https://www.elecfans.com/d/article/md/',\n  loginUrl: 'https://bbs.elecfans.com/member.php?mod=logging&action=login',\n}\n"
  },
  {
    "path": "packages/core/src/platforms/huaweicloud.js",
    "content": "// 华为云开发者博客平台配置\nconst HuaweiCloudPlatform = {\n  id: 'huaweicloud',\n  name: 'HuaweiCloud',\n  icon: 'https://www.huaweicloud.com/favicon.ico',\n  url: 'https://bbs.huaweicloud.com/blogs/article',\n  publishUrl: 'https://bbs.huaweicloud.com/blogs/article',\n  title: '华为云开发者博客',\n  type: 'huaweicloud',\n}\n\nexport { HuaweiCloudPlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/huaweidev.js",
    "content": "// 华为开发者文章平台配置\nconst HuaweiDevPlatform = {\n  id: 'huaweidev',\n  name: 'HuaweiDev',\n  icon: 'https://developer.huawei.com/favicon.ico',\n  url: 'https://developer.huawei.com/consumer/cn/',\n  publishUrl: 'https://developer.huawei.com/consumer/cn/blog/create',\n  title: '华为开发者文章',\n  type: 'huaweidev',\n}\n\n\n\n// 导出\nexport { HuaweiDevPlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/index.js",
    "content": "// 平台配置汇总\n// 从 @cose/detection 导入登录检测配置\nimport { LOGIN_CHECK_CONFIG } from '@cose/detection'\n\n// 平台元数据和同步函数从各平台文件导入\nimport { CSDNPlatform, syncCSDNContent } from './csdn.js'\nimport { JuejinPlatform, syncJuejinContent } from './juejin.js'\nimport { WechatPlatform, syncWechatContent } from './wechat.js'\nimport { ZhihuPlatform, syncZhihuContent } from './zhihu.js'\nimport { ToutiaoPlatform, syncToutiaoContent } from './toutiao.js'\nimport { SegmentFaultPlatform } from './segmentfault.js'\nimport { CnblogsPlatform } from './cnblogs.js'\nimport { OSChinaPlatform } from './oschina.js'\nimport { CTO51Platform } from './cto51.js'\nimport { InfoQPlatform } from './infoq.js'\nimport { JianshuPlatform } from './jianshu.js'\nimport { BaijiahaoPlatform } from './baijiahao.js'\nimport { WangyihaoPlatform, syncWangyihaoContent } from './wangyihao.js'\nimport { TencentCloudPlatform } from './tencentcloud.js'\nimport { MediumPlatform } from './medium.js'\nimport { SspaiPlatform } from './sspai.js'\nimport { SohuPlatform } from './sohu.js'\nimport { BilibiliPlatform } from './bilibili.js'\nimport { WeiboPlatform } from './weibo.js'\nimport { AliyunPlatform } from './aliyun.js'\nimport { HuaweiCloudPlatform } from './huaweicloud.js'\nimport { HuaweiDevPlatform } from './huaweidev.js'\nimport { TwitterPlatform } from './twitter.js'\nimport { QianfanPlatform } from './qianfan.js'\nimport { AlipayOpenPlatform } from './alipayopen.js'\nimport { ModelScopePlatform } from './modelscope.js'\nimport { VolcenginePlatform } from './volcengine.js'\nimport { DouyinPlatform } from './douyin.js'\nimport { XiaohongshuPlatform } from './xiaohongshu.js'\nimport { ElecfansPlatform } from './elecfans.js'\nimport { DoubanPlatform } from './douban.js'\n\n// 合并平台配置\nconst PLATFORMS = [\n    CSDNPlatform,\n    JuejinPlatform,\n    WechatPlatform,\n    ZhihuPlatform,\n    ToutiaoPlatform,\n    SegmentFaultPlatform,\n    CnblogsPlatform,\n    OSChinaPlatform,\n    CTO51Platform,\n    InfoQPlatform,\n    JianshuPlatform,\n    BaijiahaoPlatform,\n    WangyihaoPlatform,\n    TencentCloudPlatform,\n    MediumPlatform,\n    SspaiPlatform,\n    SohuPlatform,\n    BilibiliPlatform,\n    WeiboPlatform,\n    AliyunPlatform,\n    HuaweiCloudPlatform,\n    HuaweiDevPlatform,\n    TwitterPlatform,\n    QianfanPlatform,\n    AlipayOpenPlatform,\n    ModelScopePlatform,\n    VolcenginePlatform,\n    DouyinPlatform,\n    XiaohongshuPlatform,\n    ElecfansPlatform,\n    DoubanPlatform,\n]\n\n// 根据 hostname 获取平台填充函数\nfunction getPlatformFiller(hostname) {\n    if (hostname.includes('csdn.net')) return 'csdn'\n    if (hostname.includes('juejin.cn')) return 'juejin'\n    if (hostname.includes('mp.weixin.qq.com')) return 'wechat'\n    if (hostname.includes('zhihu.com')) return 'zhihu'\n    if (hostname.includes('toutiao.com')) return 'toutiao'\n    if (hostname.includes('segmentfault.com')) return 'segmentfault'\n    if (hostname.includes('cnblogs.com')) return 'cnblogs'\n    if (hostname.includes('oschina.net')) return 'oschina'\n    if (hostname.includes('51cto.com')) return 'cto51'\n    if (hostname.includes('infoq.cn')) return 'infoq'\n    if (hostname.includes('jianshu.com')) return 'jianshu'\n    if (hostname.includes('baijiahao.baidu.com')) return 'baijiahao'\n    if (hostname.includes('mp.163.com')) return 'wangyihao'\n    if (hostname.includes('cloud.tencent.com')) return 'tencentcloud'\n    if (hostname.includes('medium.com')) return 'medium'\n    if (hostname.includes('sspai.com')) return 'sspai'\n    if (hostname.includes('mp.sohu.com')) return 'sohu'\n    if (hostname.includes('member.bilibili.com')) return 'bilibili'\n    if (hostname.includes('card.weibo.com')) return 'weibo'\n    if (hostname.includes('developer.aliyun.com')) return 'aliyun'\n    if (hostname.includes('bbs.huaweicloud.com')) return 'huaweicloud'\n    if (hostname.includes('developer.huawei.com')) return 'huaweidev'\n    if (hostname.includes('x.com') || hostname.includes('twitter.com')) return 'twitter'\n    if (hostname.includes('qianfan.cloud.baidu.com')) return 'qianfan'\n    if (hostname.includes('open.alipay.com')) return 'alipayopen'\n    if (hostname.includes('modelscope.cn')) return 'modelscope'\n    if (hostname.includes('developer.volcengine.com')) return 'volcengine'\n    if (hostname.includes('creator.douyin.com')) return 'douyin'\n    if (hostname.includes('creator.xiaohongshu.com')) return 'xiaohongshu'\n    if (hostname.includes('elecfans.com')) return 'elecfans'\n    if (hostname.includes('douban.com')) return 'douban'\n    return 'generic'\n}\n\n// 同步处理器映射\n// 如果平台有自定义同步逻辑，在此注册处理器\n// 未注册的平台将使用 background.js 中的通用填充逻辑\nconst SYNC_HANDLERS = {\n    csdn: syncCSDNContent,\n    juejin: syncJuejinContent,\n    wechat: syncWechatContent,\n    zhihu: syncZhihuContent,\n    toutiao: syncToutiaoContent,\n    wangyihao: syncWangyihaoContent,\n}\n\n// 导出\nexport { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS, getPlatformFiller }\n"
  },
  {
    "path": "packages/core/src/platforms/infoq.js",
    "content": "// InfoQ 平台配置\nconst InfoQPlatform = {\n    id: 'infoq',\n    name: 'InfoQ',\n    icon: 'https://static001.infoq.cn/static/write/img/write-favicon.jpg',\n    url: 'https://xie.infoq.cn',\n    // InfoQ 需要先调用 API 创建草稿获取 ID，不能直接访问 /draft/write\n    publishUrl: 'https://xie.infoq.cn/draft/write', // 这个 URL 仅作为占位，实际会被动态替换\n    createDraftApi: 'https://xie.infoq.cn/api/v1/draft/create',\n    title: 'InfoQ',\n    type: 'infoq',\n}\n\n// InfoQ 内容填充函数\nasync function fillInfoQContent(content, waitFor, setInputValue) {\n    const { title, body, markdown } = content\n    const contentToFill = markdown || body || ''\n\n    // 填充标题\n    const titleInput = await waitFor('input[placeholder*=\"标题\"], .title-input input, input.article-title')\n    if (titleInput) {\n        setInputValue(titleInput, title)\n        console.log('[COSE] InfoQ 标题填充成功')\n    }\n\n    // 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 1000))\n\n    // InfoQ 使用自定义 Vue 编辑器，通过 readMarkdown 方法填充内容\n    const gkEditor = document.querySelector('.gk-editor')\n    if (gkEditor && gkEditor.__vue__) {\n        const vm = gkEditor.__vue__\n        if (typeof vm.readMarkdown === 'function') {\n            try {\n                vm.readMarkdown(contentToFill)\n                console.log('[COSE] InfoQ readMarkdown 填充成功')\n                return\n            } catch (e) {\n                console.log('[COSE] InfoQ readMarkdown 失败:', e.message)\n            }\n        }\n    }\n\n    // 备用方案：尝试 CodeMirror\n    const cmElement = document.querySelector('.CodeMirror')\n    if (cmElement && cmElement.CodeMirror) {\n        cmElement.CodeMirror.setValue(contentToFill)\n        console.log('[COSE] InfoQ CodeMirror 填充成功')\n        return\n    }\n\n    console.log('[COSE] InfoQ 未找到编辑器')\n}\n\n// 导出\nexport { InfoQPlatform, fillInfoQContent }\n"
  },
  {
    "path": "packages/core/src/platforms/jianshu.js",
    "content": "// 简书平台配置\nconst JianshuPlatform = {\n  id: 'jianshu',\n  name: 'Jianshu',\n  icon: 'https://www.jianshu.com/favicon.ico',\n  url: 'https://www.jianshu.com',\n  publishUrl: 'https://www.jianshu.com/writer',\n  title: '简书',\n  type: 'jianshu',\n}\n\n// 简书内容填充函数\nasync function fillJianshuContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n  const contentToFill = markdown || body || ''\n\n  // 填充标题 - 简书使用 input._24i7u，需要使用 native setter\n  const titleInput = await waitFor('input._24i7u, input[class*=\"title\"]')\n  if (titleInput) {\n    titleInput.focus()\n    const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n    inputSetter.call(titleInput, title)\n    titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n    console.log('[COSE] 简书标题填充成功')\n  } else {\n    console.log('[COSE] 简书未找到标题输入框')\n  }\n\n  // 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 500))\n\n  // 简书使用 textarea#arthur-editor 作为 Markdown 编辑器\n  const editor = document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR')\n  if (editor) {\n    editor.focus()\n    const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n    textareaSetter.call(editor, contentToFill)\n    editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }))\n    editor.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 简书内容填充成功')\n  } else {\n    console.log('[COSE] 简书未找到编辑器')\n  }\n}\n\n// 导出\nif (typeof module !== 'undefined' && module.exports) {\n  module.exports = { JianshuPlatform, fillJianshuContent }\n}\n\nexport { JianshuPlatform, fillJianshuContent }\n"
  },
  {
    "path": "packages/core/src/platforms/juejin.js",
    "content": "// 掘金平台配置\nconst JuejinPlatform = {\n  id: 'juejin',\n  name: 'Juejin',\n  icon: 'https://lf-web-assets.juejin.cn/obj/juejin-web/xitu_juejin_web/static/favicons/favicon-32x32.png',\n  url: 'https://juejin.cn',\n  publishUrl: 'https://juejin.cn/editor/drafts/new',\n  title: '掘金',\n  type: 'juejin',\n}\n\nimport { injectUtils } from './common.js'\n\n// 掘金内容填充函数（在页面主世界中执行）\n// 注意：需要先调用 injectUtils 注入 window.waitFor\nfunction fillJuejinContent(title, markdown, body) {\n  const contentToFill = markdown || body || ''\n\n  async function fill() {\n    // 填充标题（使用注入的 window.waitFor）\n    const titleInput = await window.waitFor('input[placeholder*=\"标题\"]')\n    if (titleInput && title) {\n      titleInput.focus()\n      titleInput.value = title\n      titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    }\n\n    // 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 1000))\n\n    // 掘金使用 ByteMD 编辑器（基于 CodeMirror）\n    const cmElement = document.querySelector('.CodeMirror')\n    if (cmElement && cmElement.CodeMirror) {\n      cmElement.CodeMirror.setValue(contentToFill)\n      console.log('[COSE] 掘金 CodeMirror 填充成功')\n      return { success: true, method: 'CodeMirror' }\n    } else {\n      // 降级到 textarea\n      const textarea = document.querySelector('.bytemd-body textarea')\n      if (textarea) {\n        textarea.focus()\n        textarea.value = contentToFill\n        textarea.dispatchEvent(new Event('input', { bubbles: true }))\n        console.log('[COSE] 掘金 textarea 填充成功')\n        return { success: true, method: 'textarea' }\n      } else {\n        console.log('[COSE] 掘金 未找到编辑器')\n        return { success: false, error: 'Editor not found' }\n      }\n    }\n  }\n\n  return fill()\n}\n\n/**\n * 掘金同步处理器\n * @param {object} tab - Chrome tab 对象\n * @param {object} content - 内容对象 { title, body, markdown }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncJuejinContent(tab, content, helpers) {\n  const { chrome } = helpers\n\n  // 等待页面加载\n  await new Promise(resolve => setTimeout(resolve, 2000))\n\n  // 先注入公共工具函数（waitFor, setInputValue）\n  await injectUtils(chrome, tab.id)\n\n  // 在页面中执行填充脚本\n  const result = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: fillJuejinContent,\n    args: [content.title, content.markdown, content.body],\n    world: 'MAIN',\n  })\n\n  const fillResult = result?.[0]?.result\n  if (fillResult?.success) {\n    return { success: true, message: '已同步到掘金', tabId: tab.id }\n  } else {\n    return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n  }\n}\n\n// 导出\nexport { JuejinPlatform, fillJuejinContent, syncJuejinContent }\n\n"
  },
  {
    "path": "packages/core/src/platforms/medium.js",
    "content": "// Medium 平台配置\nconst MediumPlatform = {\n    id: 'medium',\n    name: 'Medium',\n    icon: 'https://cdn.simpleicons.org/medium',\n    url: 'https://medium.com',\n    publishUrl: 'https://medium.com/new-story',\n    title: 'Medium',\n    type: 'medium',\n}\n\n// Medium 登录检测配置\n// Medium 使用 sid 和 uid HttpOnly cookies 进行身份验证\n/**\n * Medium 内容填充函数\n * 流程：\n * 1. 等待编辑器加载\n * 2. 填充标题到 h3.graf--title\n * 3. 通过 paste 事件填充 HTML 内容到编辑器\n */\nasync function fillMediumContent(content, waitFor, setInputValue) {\n    const { title, body, wechatHtml } = content\n    const htmlContent = wechatHtml || body || ''\n\n    console.log('[COSE] Medium 开始同步...')\n\n    // 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 2000))\n\n    // 第一步：填充标题\n    const titleEl = document.querySelector('h3.graf--title')\n    if (titleEl && title) {\n        titleEl.focus()\n        titleEl.textContent = title\n        titleEl.dispatchEvent(new Event('input', { bubbles: true }))\n        console.log('[COSE] Medium 标题填充成功')\n    }\n\n    // 第二步：填充内容 - 使用 paste 事件\n    const contentEl = document.querySelector('p.graf--p')\n    if (contentEl && htmlContent) {\n        contentEl.focus()\n\n        // 创建 DataTransfer 并设置 HTML 内容\n        const dt = new DataTransfer()\n        dt.setData('text/html', htmlContent)\n        dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))\n\n        const pasteEvent = new ClipboardEvent('paste', {\n            bubbles: true,\n            cancelable: true,\n            clipboardData: dt\n        })\n\n        contentEl.dispatchEvent(pasteEvent)\n        console.log('[COSE] Medium 内容填充成功')\n    }\n}\n\n// 导出\nexport { MediumPlatform, fillMediumContent }\n"
  },
  {
    "path": "packages/core/src/platforms/modelscope.js",
    "content": "// ModelScope 魔搭社区平台配置\n// 编辑器支持 Markdown，注入后需要点击\"转为富文本\"按钮\n\nconst ModelScopePlatform = {\n  id: 'modelscope',\n  name: 'ModelScope',\n  icon: 'https://img.alicdn.com/imgextra/i4/O1CN01fvt4it25rEZU4Gjso_!!6000000007579-2-tps-128-128.png',\n  url: 'https://modelscope.cn',\n  publishUrl: 'https://modelscope.cn/learn/create',\n  title: 'ModelScope 魔搭社区',\n  type: 'modelscope',\n}\n\n\n\n// 导出\nexport { ModelScopePlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/oschina.js",
    "content": "// OSChina 平台配置\nconst OSChinaPlatform = {\n    id: 'oschina',\n    name: 'OSChina',\n    icon: 'https://wsrv.nl/?url=static.oschina.net/new-osc/img/favicon.ico',\n    url: 'https://www.oschina.net',\n    publishUrl: 'https://my.oschina.net/blog/ai-write',\n    title: '开源中国',\n    type: 'oschina',\n}\n\n// OSChina 内容填充函数 (AI 写作平台 - 切换到 Markdown 编辑器)\nasync function fillOSChinaContent(content, waitFor, setInputValue) {\n    const { title, markdown, body } = content\n    const mdContent = markdown || body || ''\n\n    // 1. 切换到 MD 编辑器（如果当前不是）\n    const switchText = document.querySelector('.editor-switch-text')\n    if (switchText && switchText.textContent.includes('切换到MD编辑器')) {\n        const switchBtn = document.querySelector('.editor-switch-btn') || switchText.parentElement\n        if (switchBtn) {\n            switchBtn.click()\n            let confirmBtn = null\n            for (let i = 0; i < 20; i++) {\n                await new Promise(resolve => setTimeout(resolve, 200))\n                confirmBtn = Array.from(document.querySelectorAll('button'))\n                    .find(btn => btn.textContent.trim() === '确定切换')\n                if (confirmBtn) break\n            }\n            if (confirmBtn) {\n                confirmBtn.click()\n                console.log('[COSE] OSChina 已确认切换到MD编辑器')\n            }\n            await new Promise(resolve => setTimeout(resolve, 2000))\n        }\n    }\n\n    // 2. 填充标题\n    const titleInput = await waitFor('input[placeholder*=\"标题\"]')\n    if (titleInput) {\n        titleInput.focus()\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set\n        if (nativeSetter) {\n            nativeSetter.call(titleInput, title)\n        } else {\n            titleInput.value = title\n        }\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] OSChina 标题填充成功')\n    }\n\n    // 3. 填充 Markdown 内容到 textarea\n    await new Promise(resolve => setTimeout(resolve, 500))\n    let textarea = null\n    for (let i = 0; i < 10; i++) {\n        textarea = document.querySelector('textarea')\n        if (textarea) break\n        await new Promise(resolve => setTimeout(resolve, 300))\n    }\n    if (textarea) {\n        textarea.focus()\n        const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n        if (textareaSetter) {\n            textareaSetter.call(textarea, mdContent)\n        } else {\n            textarea.value = mdContent\n        }\n        textarea.dispatchEvent(new Event('input', { bubbles: true }))\n        textarea.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] OSChina Markdown 内容填充成功')\n    } else {\n        console.log('[COSE] OSChina 未找到 Markdown textarea')\n    }\n}\n\n// 导出\nexport { OSChinaPlatform, fillOSChinaContent }\n"
  },
  {
    "path": "packages/core/src/platforms/qianfan.js",
    "content": "// 百度千帆开发者社区平台配置\n// 编辑器支持 Markdown 自动转换功能\n\nconst QianfanPlatform = {\n  id: 'qianfan',\n  name: 'Qianfan',\n  icon: 'https://bce.bdstatic.com/img/favicon.ico',\n  url: 'https://qianfan.cloud.baidu.com/qianfandev',\n  publishUrl: 'https://qianfan.cloud.baidu.com/qianfandev/topic/create',\n  title: '百度云千帆',\n  type: 'qianfan',\n}\n\n\n\n// 千帆平台拦截函数\n// 在 MAIN world 中执行，拦截所有可能导致跳转到登录页的行为\n// 包括：fetch, XHR, sendBeacon, location 跳转, window.open, Navigation API, History API\nfunction qianfanIntercept() {\n  if (!location.href.includes('qianfan.cloud.baidu.com')) return\n\n  const INTERCEPT_PATTERN = '/api/community/topic'\n  const LOGIN_URL_PATTERN = 'login.bce.baidu.com'\n  let blockedCount = 0\n\n  const FAKE_RESPONSE = JSON.stringify({\n    success: true,\n    status: 200,\n    result: { id: 'cose-intercepted' }\n  })\n\n  console.log('[COSE] 千帆拦截器开始安装...')\n\n  // ========== 拦截 fetch ==========\n  const originalFetch = window.fetch\n  window.fetch = function (...args) {\n    const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''\n    const opts = args[1] || {}\n    const method = (opts.method || (args[0]?.method) || 'GET').toUpperCase()\n\n    if (url.includes(INTERCEPT_PATTERN) && method === 'POST') {\n      console.log('[COSE] 拦截 fetch POST:', url, '(已拦截', ++blockedCount, '个)')\n      return Promise.resolve(new Response(FAKE_RESPONSE, {\n        status: 200,\n        headers: { 'Content-Type': 'application/json' }\n      }))\n    }\n    return originalFetch.apply(this, args)\n  }\n\n  // ========== 拦截 XMLHttpRequest ==========\n  const originalXHROpen = XMLHttpRequest.prototype.open\n  const originalXHRSend = XMLHttpRequest.prototype.send\n\n  XMLHttpRequest.prototype.open = function (method, url, ...rest) {\n    this._coseUrl = url\n    this._coseMethod = (method || 'GET').toUpperCase()\n    return originalXHROpen.call(this, method, url, ...rest)\n  }\n\n  XMLHttpRequest.prototype.send = function (body) {\n    if (this._coseUrl?.includes(INTERCEPT_PATTERN) && this._coseMethod === 'POST') {\n      console.log('[COSE] 拦截 XHR POST:', this._coseUrl, '(已拦截', ++blockedCount, '个)')\n      const self = this\n      setTimeout(() => {\n        Object.defineProperty(self, 'readyState', { get: () => 4, configurable: true })\n        Object.defineProperty(self, 'status', { get: () => 200, configurable: true })\n        Object.defineProperty(self, 'statusText', { get: () => 'OK', configurable: true })\n        Object.defineProperty(self, 'responseText', { get: () => FAKE_RESPONSE, configurable: true })\n        Object.defineProperty(self, 'response', { get: () => FAKE_RESPONSE, configurable: true })\n        self.dispatchEvent(new Event('readystatechange'))\n        self.dispatchEvent(new Event('load'))\n        self.dispatchEvent(new Event('loadend'))\n        if (typeof self.onreadystatechange === 'function') self.onreadystatechange()\n        if (typeof self.onload === 'function') self.onload()\n      }, 10)\n      return\n    }\n    return originalXHRSend.call(this, body)\n  }\n\n  // ========== 拦截 navigator.sendBeacon ==========\n  const originalSendBeacon = navigator.sendBeacon?.bind(navigator)\n  if (originalSendBeacon) {\n    navigator.sendBeacon = function (url, data) {\n      if (url?.includes(INTERCEPT_PATTERN)) {\n        console.log('[COSE] 拦截 sendBeacon:', url, '(已拦截', ++blockedCount, '个)')\n        return true\n      }\n      return originalSendBeacon(url, data)\n    }\n  }\n\n  // ========== 拦截 location 跳转到登录页 ==========\n  const origAssign = window.location.assign.bind(window.location)\n  const origReplace = window.location.replace.bind(window.location)\n\n  window.location.assign = function (url) {\n    if (typeof url === 'string' && url.includes(LOGIN_URL_PATTERN)) {\n      console.log('[COSE] 拦截 location.assign 跳转到登录页:', url)\n      return\n    }\n    return origAssign(url)\n  }\n\n  window.location.replace = function (url) {\n    if (typeof url === 'string' && url.includes(LOGIN_URL_PATTERN)) {\n      console.log('[COSE] 拦截 location.replace 跳转到登录页:', url)\n      return\n    }\n    return origReplace(url)\n  }\n\n  // ========== 拦截 window.open 到登录页 ==========\n  const originalOpen = window.open\n  window.open = function (url, ...rest) {\n    if (typeof url === 'string' && url.includes(LOGIN_URL_PATTERN)) {\n      console.log('[COSE] 拦截 window.open 跳转到登录页:', url)\n      return null\n    }\n    return originalOpen.call(this, url, ...rest)\n  }\n\n  // ========== 拦截 Navigation API ==========\n  if (window.navigation) {\n    window.navigation.addEventListener('navigate', (e) => {\n      const destUrl = e.destination?.url || ''\n      console.log('[COSE] Navigation API navigate 事件:', destUrl)\n      if (destUrl.includes(LOGIN_URL_PATTERN)) {\n        console.log('[COSE] 拦截 Navigation API 跳转到登录页')\n        e.preventDefault()\n      }\n    })\n  }\n\n  // ========== 拦截 History API ==========\n  const origPushState = history.pushState.bind(history)\n  const origReplaceState = history.replaceState.bind(history)\n\n  history.pushState = function (state, title, url) {\n    if (typeof url === 'string' && url.includes(LOGIN_URL_PATTERN)) {\n      console.log('[COSE] 拦截 pushState 跳转到登录页:', url)\n      return\n    }\n    return origPushState(state, title, url)\n  }\n\n  history.replaceState = function (state, title, url) {\n    if (typeof url === 'string' && url.includes(LOGIN_URL_PATTERN)) {\n      console.log('[COSE] 拦截 replaceState 跳转到登录页:', url)\n      return\n    }\n    return origReplaceState(state, title, url)\n  }\n\n  console.log('[COSE] 千帆拦截器安装完成（fetch/XHR/sendBeacon/location/navigation）')\n}\n\n// 导出\nexport { QianfanPlatform, qianfanIntercept }\n"
  },
  {
    "path": "packages/core/src/platforms/segmentfault.js",
    "content": "// 思否平台配置\nconst SegmentFaultPlatform = {\n  id: 'segmentfault',\n  name: 'SegmentFault',\n  icon: 'https://fastly.jsdelivr.net/gh/bucketio/img16@main/2026/02/01/1769960912823-e037663a-7f65-414e-a114-ed86b4e86964.png',\n  url: 'https://segmentfault.com',\n  publishUrl: 'https://segmentfault.com/write',\n  title: '思否',\n  type: 'segmentfault',\n}\n\n// 思否内容填充函数\nasync function fillSegmentFaultContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n  const contentToFill = markdown || body || ''\n\n  // 填充标题\n  const titleInput = await waitFor('input#title, input[placeholder*=\"标题\"]')\n  if (titleInput) {\n    titleInput.focus()\n    titleInput.value = title\n    titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] 思否标题填充成功')\n  }\n\n  // 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 1000))\n\n  // 思否使用 CodeMirror 编辑器\n  const cmElement = document.querySelector('.CodeMirror')\n  if (cmElement && cmElement.CodeMirror) {\n    cmElement.CodeMirror.setValue(contentToFill)\n    console.log('[COSE] 思否 CodeMirror 填充成功')\n  } else {\n    // 降级到 textarea\n    const textarea = document.querySelector('textarea')\n    if (textarea) {\n      textarea.focus()\n      textarea.value = contentToFill\n      textarea.dispatchEvent(new Event('input', { bubbles: true }))\n      console.log('[COSE] 思否 textarea 填充成功')\n    } else {\n      console.log('[COSE] 思否 未找到编辑器')\n    }\n  }\n}\n\n// 导出\nexport { SegmentFaultPlatform, fillSegmentFaultContent }\n"
  },
  {
    "path": "packages/core/src/platforms/sohu.js",
    "content": "// 搜狐号平台配置\nconst SohuPlatform = {\n  id: 'sohu',\n  name: 'Sohu',\n  icon: 'https://statics.itc.cn/mp-new/icon/1.1/favicon.ico',\n  url: 'https://mp.sohu.com',\n  publishUrl: 'https://mp.sohu.com/mpfe/v4/contentManagement/news/addarticle?contentStatus=1',\n  title: '搜狐号',\n  type: 'sohu',\n}\n\n// 搜狐号内容填充函数\n// 注意：搜狐号由 syncToPlatform 单独处理，此函数作为备用\nasync function fillSohuContent(content, waitFor) {\n  console.log('[COSE] 搜狐号由 syncToPlatform 处理')\n}\n\n// 导出\nexport { SohuPlatform, fillSohuContent }\n"
  },
  {
    "path": "packages/core/src/platforms/sspai.js",
    "content": "// 少数派平台配置\nconst SspaiPlatform = {\n  id: 'sspai',\n  name: 'Sspai',\n  icon: 'https://cdn-static.sspai.com/favicon/sspai.ico',\n  url: 'https://sspai.com',\n  loginUrl: 'https://sspai.com/write',\n  publishUrl: 'https://sspai.com/write',\n  title: '少数派',\n  type: 'sspai',\n}\n\n// 少数派内容填充函数（备用，主要使用剪贴板方式）\nasync function fillSspaiContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n  const contentToFill = markdown || body || ''\n\n  // 1. 填充标题 - 少数派使用 textbox\n  await new Promise(resolve => setTimeout(resolve, 1000))\n\n  const titleInput = document.querySelector('textarea[placeholder*=\"标题\"]') ||\n    document.querySelector('input[placeholder*=\"标题\"]')\n\n  if (titleInput) {\n    titleInput.focus()\n    // 使用 native setter 来绕过 React/Vue 的受控组件\n    const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set ||\n      Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set\n    nativeSetter.call(titleInput, title)\n    // 触发事件\n    titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n    console.log('[COSE] 少数派标题填充成功')\n  } else {\n    console.log('[COSE] 少数派未找到标题输入框')\n  }\n\n  // 2. 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 1500))\n\n  // 3. 填充正文内容\n  // 少数派使用 ProseMirror 富文本编辑器\n  const editor = document.querySelector('.ProseMirror') ||\n    document.querySelector('[contenteditable=\"true\"]')\n\n  if (editor) {\n    editor.focus()\n    editor.innerHTML = contentToFill.replace(/\\n/g, '<br>')\n    editor.dispatchEvent(new Event('input', { bubbles: true }))\n    console.log('[COSE] 少数派编辑器填充成功')\n  } else {\n    console.log('[COSE] 少数派未找到编辑器元素')\n  }\n}\n\n// 导出\nexport { SspaiPlatform, fillSspaiContent }\n"
  },
  {
    "path": "packages/core/src/platforms/tencentcloud.js",
    "content": "// 腾讯云开发者平台配置\nconst TencentCloudPlatform = {\n    id: 'tencentcloud',\n    name: 'TencentCloud',\n    icon: 'https://cloudcache.tencent-cloud.com/qcloud/favicon.ico',\n    url: 'https://cloud.tencent.com/developer',\n    publishUrl: 'https://cloud.tencent.com/developer/article/write-new',\n    title: '腾讯云开发者社区',\n    type: 'tencentcloud',\n}\n\n/**\n * 检查当前是否需要切换到 MD 编辑器\n * 判断依据：页面中是否存在\"切换 MD 编辑器\"的按钮\n * - 如果存在，说明当前是富文本编辑器，需要切换\n * - 如果不存在（显示\"切换 富文本 编辑器\"），说明已经是 MD 编辑器\n * @returns {HTMLElement|null} 返回切换按钮元素，如果已经是 MD 编辑器则返回 null\n */\nfunction findSwitchToMDButton() {\n    const headerBtns = document.querySelectorAll('.header-btn')\n    for (const btn of headerBtns) {\n        // 只有当按钮文本包含\"MD\"时才需要切换（说明当前是富文本编辑器）\n        // 如果按钮文本包含\"富文本\"，说明已经是 MD 编辑器，不需要切换\n        if (btn.textContent.includes('切换') && btn.textContent.includes('MD')) {\n            return btn\n        }\n    }\n    return null\n}\n\n/**\n * 确保编辑器处于 Markdown 模式\n * 1. 检查是否有\"切换 MD 编辑器\"按钮\n * 2. 如果有，点击切换到 MD 编辑器\n * 3. 如果没有，说明已经是 MD 编辑器\n * @returns {Promise<boolean>} 是否成功进入 MD 编辑器模式\n */\nasync function ensureMarkdownEditor() {\n    // 等待页面加载完成\n    await new Promise(resolve => setTimeout(resolve, 1000))\n    \n    const switchBtn = findSwitchToMDButton()\n    \n    if (switchBtn) {\n        // 找到了\"切换 MD 编辑器\"按钮，说明当前是富文本编辑器，需要切换\n        console.log('[COSE] TencentCloud 检测到富文本编辑器，正在切换到 MD 编辑器...')\n        switchBtn.click()\n        \n        // 等待切换完成\n        await new Promise(resolve => setTimeout(resolve, 1500))\n        \n        // 验证切换是否成功：检查 CodeMirror 是否存在\n        const cm = document.querySelector('.CodeMirror')\n        if (cm && cm.CodeMirror) {\n            console.log('[COSE] TencentCloud 成功切换到 MD 编辑器')\n            return true\n        }\n        \n        // 如果 CodeMirror 还没加载，再等待一下\n        await new Promise(resolve => setTimeout(resolve, 1000))\n        const cmRetry = document.querySelector('.CodeMirror')\n        if (cmRetry && cmRetry.CodeMirror) {\n            console.log('[COSE] TencentCloud 成功切换到 MD 编辑器（延迟加载）')\n            return true\n        }\n        \n        console.error('[COSE] TencentCloud 切换失败：CodeMirror 未加载')\n        return false\n    } else {\n        // 没有找到\"切换 MD 编辑器\"按钮，说明已经是 MD 编辑器\n        console.log('[COSE] TencentCloud 当前已是 MD 编辑器')\n        \n        // 验证 CodeMirror 是否存在\n        const cm = document.querySelector('.CodeMirror')\n        if (cm && cm.CodeMirror) {\n            return true\n        }\n        \n        // 等待 CodeMirror 加载\n        await new Promise(resolve => setTimeout(resolve, 1000))\n        const cmRetry = document.querySelector('.CodeMirror')\n        return !!(cmRetry && cmRetry.CodeMirror)\n    }\n}\n\n/**\n * 获取 CodeMirror 实例\n * @param {number} maxWait 最大等待时间（毫秒）\n * @returns {Promise<CodeMirror|null>}\n */\nasync function getCodeMirror(maxWait = 3000) {\n    const startTime = Date.now()\n    while (Date.now() - startTime < maxWait) {\n        const cm = document.querySelector('.CodeMirror')\n        if (cm && cm.CodeMirror) {\n            return cm.CodeMirror\n        }\n        await new Promise(resolve => setTimeout(resolve, 200))\n    }\n    return null\n}\n\n/**\n * 腾讯云内容填充函数\n * 流程：\n * 1. 确保进入 MD 编辑器模式\n * 2. 填充标题\n * 3. 填充内容到 CodeMirror\n */\nasync function fillTencentCloudContent(content, waitFor, setInputValue) {\n    const { title, body, markdown } = content\n    const contentToFill = markdown || body || ''\n\n    console.log('[COSE] TencentCloud 开始同步...')\n    \n    // 第一步：确保进入 MD 编辑器模式\n    const isMarkdownMode = await ensureMarkdownEditor()\n    if (!isMarkdownMode) {\n        console.error('[COSE] TencentCloud 错误：无法进入 MD 编辑器模式，请手动切换后重试')\n        return\n    }\n\n    // 第二步：获取 CodeMirror 实例\n    const codeMirror = await getCodeMirror(3000)\n    if (!codeMirror) {\n        console.error('[COSE] TencentCloud 错误：CodeMirror 未加载，请刷新页面后重试')\n        return\n    }\n\n    // 第三步：填充标题\n    const titleInput = document.querySelector('textarea[placeholder*=\"标题\"]')\n    if (titleInput && title) {\n        titleInput.focus()\n        const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n        nativeSetter.call(titleInput, title)\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] TencentCloud 标题填充成功')\n    }\n\n    // 第四步：填充内容到 CodeMirror\n    codeMirror.setValue(contentToFill)\n    console.log('[COSE] TencentCloud 内容填充成功')\n}\n\n// 导出\nexport { TencentCloudPlatform, fillTencentCloudContent, ensureMarkdownEditor, getCodeMirror }\n"
  },
  {
    "path": "packages/core/src/platforms/toutiao.js",
    "content": "// 今日头条平台配置\nconst ToutiaoPlatform = {\n  id: 'toutiao',\n  name: 'Toutiao',\n  icon: 'https://sf3-cdn-tos.toutiaostatic.com/obj/eden-cn/uhbfnupkbps/toutiao_favicon.ico',\n  url: 'https://mp.toutiao.com',\n  publishUrl: 'https://mp.toutiao.com/profile_v4/graphic/publish',\n  title: '今日头条',\n  type: 'toutiao',\n}\n\nimport { injectUtils } from './common.js'\n\n// 今日头条内容填充函数（在页面主世界中执行）\nfunction fillToutiaoContentInPage(title, body) {\n  // 等待满足条件的元素出现\n  function waitForElement(predicate, timeout = 10000) {\n    return new Promise((resolve) => {\n      const el = predicate()\n      if (el) return resolve(el)\n\n      const observer = new MutationObserver(() => {\n        const el = predicate()\n        if (el) {\n          observer.disconnect()\n          resolve(el)\n        }\n      })\n      observer.observe(document.body, { childList: true, subtree: true })\n\n      setTimeout(() => {\n        observer.disconnect()\n        resolve(predicate())\n      }, timeout)\n    })\n  }\n\n  async function fillContent() {\n    // 填充标题 - 头条使用 textarea\n    const titleInput = await waitForElement(() =>\n      document.querySelector('textarea[placeholder*=\"标题\"]')\n    )\n    if (titleInput && title) {\n      titleInput.focus()\n      // 模拟用户输入\n      const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n      nativeSetter.call(titleInput, title)\n      titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n      titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n      titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n      console.log('[COSE] 头条标题填充成功:', title)\n    } else {\n      console.log('[COSE] 头条未找到标题输入框')\n    }\n\n    // 等待编辑器加载\n    await new Promise(resolve => setTimeout(resolve, 500))\n\n    // 头条使用 ProseMirror 富文本编辑器\n    const editor = await waitForElement(() =>\n      document.querySelector('.ProseMirror')\n    )\n\n    if (editor && body) {\n      editor.focus()\n\n      // 对于 ProseMirror，我们需要更智能的方式来填充内容\n      // 清空现有内容\n      editor.innerHTML = ''\n\n      // 将内容分割成段落\n      const lines = body.split('\\n').filter(line => line.trim() !== '')\n\n      // 使用 document.execCommand 插入内容（ProseMirror 兼容）\n      const selection = window.getSelection()\n      const range = document.createRange()\n      range.selectNodeContents(editor)\n      range.collapse(false)\n      selection.removeAllRanges()\n      selection.addRange(range)\n\n      // 创建 HTML 内容\n      const htmlContent = lines.map(line => `<p>${line}</p>`).join('')\n\n      // 使用 insertHTML 命令\n      document.execCommand('insertHTML', false, htmlContent)\n\n      // 触发事件让 ProseMirror 同步\n      editor.dispatchEvent(new InputEvent('input', { bubbles: true }))\n      editor.dispatchEvent(new Event('change', { bubbles: true }))\n\n      console.log('[COSE] 头条内容填充成功')\n      return { success: true }\n    } else {\n      console.log('[COSE] 头条未找到编辑器')\n      return { success: false, error: '未找到编辑器' }\n    }\n  }\n\n  return fillContent()\n}\n\n/**\n * 今日头条同步处理器\n * @param {object} tab - Chrome tab 对象\n * @param {object} content - 内容对象 { title, body, markdown }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncToutiaoContent(tab, content, helpers) {\n  const { chrome, waitForTab } = helpers\n\n  // 等待页面加载完成\n  await waitForTab(tab.id)\n\n  // 额外等待一下让编辑器完全加载\n  await new Promise(resolve => setTimeout(resolve, 2500))\n\n  // 先注入公共工具函数\n  await injectUtils(chrome, tab.id)\n\n  // 在页面中执行填充\n  const result = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: fillToutiaoContentInPage,\n    args: [content.title, content.body || content.markdown || ''],\n    world: 'MAIN',\n  })\n\n  const fillResult = result?.[0]?.result\n  if (fillResult?.success) {\n    return { success: true, message: '已打开头条号并填充内容', tabId: tab.id }\n  } else {\n    return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n  }\n}\n\n// 导出\nexport { ToutiaoPlatform, fillToutiaoContentInPage, syncToutiaoContent }\n"
  },
  {
    "path": "packages/core/src/platforms/twitter.js",
    "content": "// Twitter Articles 平台配置\n// 使用 marked 进行 Markdown 解析，并转换为 Twitter Articles 支持的格式\n\nconst TwitterPlatform = {\n  id: 'twitter',\n  name: 'Twitter',\n  icon: 'https://abs.twimg.com/favicons/twitter.3.ico',\n  url: 'https://x.com',\n  publishUrl: 'https://x.com/compose/articles/edit/',\n  title: 'Twitter Articles',\n  type: 'twitter',\n}\n\n\n\n/**\n * 自定义 Markdown 渲染器\n * 将 Markdown 转换为 Twitter Articles 支持的 HTML 格式\n */\nfunction createTwitterRenderer(marked) {\n  const renderer = new marked.Renderer()\n\n  // 标题转换 - Twitter Articles 支持 h1, h2, h3\n  renderer.heading = function (text, level) {\n    // Twitter Articles 使用 h1 作为标题，h2 作为副标题\n    // 文章内容中的标题映射：# -> h2, ## -> h3, ### -> h4\n    const mappedLevel = Math.min(level + 1, 4)\n    return `<h${mappedLevel}>${text}</h${mappedLevel}>\\n`\n  }\n\n  // 段落\n  renderer.paragraph = function (text) {\n    return `<p>${text}</p>\\n`\n  }\n\n  // 粗体\n  renderer.strong = function (text) {\n    return `<strong>${text}</strong>`\n  }\n\n  // 斜体\n  renderer.em = function (text) {\n    return `<em>${text}</em>`\n  }\n\n  // 删除线\n  renderer.del = function (text) {\n    return `<s>${text}</s>`\n  }\n\n  // 代码块 - Twitter Articles 不原生支持代码高亮，使用 pre + code 格式\n  renderer.code = function (code, language) {\n    // 转义 HTML 特殊字符\n    const escapedCode = code\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n      .replace(/\"/g, '&quot;')\n      .replace(/'/g, '&#039;')\n\n    // 使用带样式的 pre 标签，模拟代码块效果\n    return `<pre style=\"background-color: #f6f8fa; padding: 16px; border-radius: 6px; overflow-x: auto; font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace; font-size: 14px; line-height: 1.45;\"><code>${escapedCode}</code></pre>\\n`\n  }\n\n  // 行内代码\n  renderer.codespan = function (code) {\n    const escapedCode = code\n      .replace(/&/g, '&amp;')\n      .replace(/</g, '&lt;')\n      .replace(/>/g, '&gt;')\n    return `<code style=\"background-color: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-family: 'SF Mono', Consolas, monospace; font-size: 0.9em;\">${escapedCode}</code>`\n  }\n\n  // 引用块\n  renderer.blockquote = function (quote) {\n    return `<blockquote style=\"border-left: 4px solid #1d9bf0; padding-left: 16px; margin: 16px 0; color: #536471;\">${quote}</blockquote>\\n`\n  }\n\n  // 无序列表\n  renderer.list = function (body, ordered) {\n    const tag = ordered ? 'ol' : 'ul'\n    return `<${tag}>${body}</${tag}>\\n`\n  }\n\n  // 列表项\n  renderer.listitem = function (text) {\n    return `<li>${text}</li>\\n`\n  }\n\n  // 链接\n  renderer.link = function (href, title, text) {\n    const titleAttr = title ? ` title=\"${title}\"` : ''\n    return `<a href=\"${href}\"${titleAttr} target=\"_blank\" rel=\"noopener noreferrer\">${text}</a>`\n  }\n\n  // 图片\n  renderer.image = function (href, title, text) {\n    const titleAttr = title ? ` title=\"${title}\"` : ''\n    const altAttr = text ? ` alt=\"${text}\"` : ''\n    return `<img src=\"${href}\"${altAttr}${titleAttr} style=\"max-width: 100%; height: auto;\" />`\n  }\n\n  // 水平分割线\n  renderer.hr = function () {\n    return `<hr style=\"border: none; border-top: 1px solid #cfd9de; margin: 24px 0;\" />\\n`\n  }\n\n  // 表格\n  renderer.table = function (header, body) {\n    return `<table style=\"border-collapse: collapse; width: 100%; margin: 16px 0;\">\n<thead>${header}</thead>\n<tbody>${body}</tbody>\n</table>\\n`\n  }\n\n  renderer.tablerow = function (content) {\n    return `<tr>${content}</tr>\\n`\n  }\n\n  renderer.tablecell = function (content, flags) {\n    const tag = flags.header ? 'th' : 'td'\n    const align = flags.align ? ` style=\"text-align: ${flags.align}; border: 1px solid #cfd9de; padding: 8px;\"` : ' style=\"border: 1px solid #cfd9de; padding: 8px;\"'\n    return `<${tag}${align}>${content}</${tag}>\\n`\n  }\n\n  return renderer\n}\n\n/**\n * 处理数学公式\n * 将 LaTeX 公式转换为可显示的格式\n * Twitter Articles 不原生支持 LaTeX，这里转换为图片或文本格式\n */\nfunction processLatexFormulas(markdown) {\n  // 处理行内公式 $...$\n  let processed = markdown.replace(/\\$([^\\$\\n]+)\\$/g, (match, formula) => {\n    // 使用 CodeCogs API 将 LaTeX 转换为图片\n    const encodedFormula = encodeURIComponent(formula.trim())\n    return `<img src=\"https://latex.codecogs.com/svg.image?${encodedFormula}\" alt=\"${formula}\" style=\"vertical-align: middle;\" />`\n  })\n\n  // 处理块级公式 $$...$$\n  processed = processed.replace(/\\$\\$([^\\$]+)\\$\\$/g, (match, formula) => {\n    const encodedFormula = encodeURIComponent(formula.trim())\n    return `<div style=\"text-align: center; margin: 16px 0;\"><img src=\"https://latex.codecogs.com/svg.image?${encodedFormula}\" alt=\"${formula}\" /></div>`\n  })\n\n  return processed\n}\n\n/**\n * 将 Markdown 转换为 Twitter Articles 支持的 HTML\n * @param {string} markdown - 原始 Markdown 内容\n * @param {object} marked - marked 库实例\n * @returns {string} - 转换后的 HTML\n */\nfunction convertMarkdownToTwitterHtml(markdown, marked) {\n  if (!markdown) return ''\n\n  // 先处理 LaTeX 公式\n  let processedMarkdown = processLatexFormulas(markdown)\n\n  // 配置 marked\n  const renderer = createTwitterRenderer(marked)\n  marked.setOptions({\n    renderer: renderer,\n    gfm: true,\n    breaks: true,\n    pedantic: false,\n  })\n\n  // 转换 Markdown 为 HTML\n  const html = marked.parse(processedMarkdown)\n\n  return html\n}\n\n/**\n * Twitter Articles 内容填充函数\n * 流程：\n * 1. 等待编辑器加载\n * 2. 使用 marked 解析 Markdown 并转换格式\n * 3. 填充标题到 textarea[placeholder=\"Add a title\"]\n * 4. 通过 paste 事件填充 HTML 内容到 Draft.js 编辑器\n */\nasync function fillTwitterContent(content, waitFor, setInputValue) {\n  const { title, body, markdown } = content\n\n  console.log('[COSE] Twitter Articles 开始同步...')\n\n  // 等待编辑器加载\n  await new Promise(resolve => setTimeout(resolve, 3000))\n\n  // 动态加载 marked 库\n  let marked\n  try {\n    // 尝试从 CDN 加载 marked\n    if (!window.marked) {\n      const script = document.createElement('script')\n      script.src = 'https://cdn.jsdelivr.net/npm/marked/marked.min.js'\n      document.head.appendChild(script)\n      await new Promise((resolve, reject) => {\n        script.onload = resolve\n        script.onerror = reject\n        setTimeout(reject, 5000) // 5秒超时\n      })\n    }\n    marked = window.marked\n  } catch (e) {\n    console.error('[COSE] 加载 marked 库失败:', e)\n    // 降级处理：直接使用原始内容\n    marked = null\n  }\n\n  // 转换 Markdown 为 Twitter Articles HTML\n  let htmlContent\n  if (marked && markdown) {\n    htmlContent = convertMarkdownToTwitterHtml(markdown, marked)\n    console.log('[COSE] Markdown 已转换为 HTML')\n  } else {\n    // 降级：使用原始 body 或简单转换\n    htmlContent = body || markdown || ''\n    if (!htmlContent.includes('<')) {\n      // 如果是纯文本，简单转换为段落\n      htmlContent = htmlContent.split('\\n\\n').map(p => `<p>${p}</p>`).join('\\n')\n    }\n  }\n\n  // 第一步：填充标题\n  // Twitter Articles 使用 textarea[placeholder=\"Add a title\"]\n  const titleInput = await waitFor('textarea[placeholder=\"Add a title\"], textarea[name=\"Article Title\"]', 5000)\n  if (titleInput && title) {\n    titleInput.focus()\n    // 使用 native setter 来绑定 React 的受控组件\n    const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n    nativeSetter.call(titleInput, title)\n    titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n    titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n    console.log('[COSE] Twitter Articles 标题填充成功')\n  }\n\n  // 第二步：填充内容\n  // Twitter Articles 使用 Draft.js 编辑器\n  const contentEl = await waitFor('.public-DraftEditor-content[contenteditable=\"true\"], .DraftEditor-root [contenteditable=\"true\"]', 5000)\n  if (contentEl && htmlContent) {\n    contentEl.focus()\n\n    // 创建 DataTransfer 并设置 HTML 内容\n    const dt = new DataTransfer()\n    dt.setData('text/html', htmlContent)\n    dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))\n\n    const pasteEvent = new ClipboardEvent('paste', {\n      bubbles: true,\n      cancelable: true,\n      clipboardData: dt\n    })\n\n    contentEl.dispatchEvent(pasteEvent)\n    console.log('[COSE] Twitter Articles 内容填充成功 (Draft.js)')\n  } else {\n    console.log('[COSE] Twitter Articles 未找到内容编辑器')\n  }\n}\n\n// 导出\nexport { TwitterPlatform, fillTwitterContent, convertMarkdownToTwitterHtml }\n"
  },
  {
    "path": "packages/core/src/platforms/volcengine.js",
    "content": "// 火山引擎开发者社区平台配置\nconst VolcenginePlatform = {\n  id: 'volcengine',\n  name: 'Volcengine',\n  icon: 'https://lf1-cdn-tos.bytegoofy.com/goofy/tech-fe/fav.png',\n  url: 'https://developer.volcengine.com/',\n  publishUrl: 'https://developer.volcengine.com/articles/draft',\n  title: '火山引擎开发者社区',\n  type: 'volcengine',\n}\n\n\n\n// 导出\nexport { VolcenginePlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/wangyihao.js",
    "content": "// 网易号平台配置\nconst WangyihaoPlatform = {\n  id: 'wangyihao',\n  name: 'Wangyihao',\n  icon: 'https://static.ws.126.net/163/f2e/news/yxybd_pc/resource/static/share-icon.png',\n  url: 'https://mp.163.com',\n  publishUrl: 'https://mp.163.com/#/article-publish',\n  title: '网易号',\n  type: 'wangyihao',\n}\n\nimport { injectUtils } from './common.js'\n\n// 网易号内容填充函数（在页面主世界中执行）\n// 网易号使用剪贴板 HTML 粘贴到 Draft.js 编辑器\nfunction fillWangyihaoContent(title, htmlBody) {\n  async function fill() {\n    // 1. 等待并填充标题 - 网易号使用 textarea.netease-textarea\n    const titleInput = await window.waitFor('textarea.netease-textarea', 10000) ||\n      await window.waitFor('textarea[placeholder*=\"标题\"]', 3000)\n\n    if (titleInput && title) {\n      titleInput.focus()\n      // 使用 native setter 来绕过 React 的受控组件\n      const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set\n      nativeSetter.call(titleInput, title)\n      // 触发 React 能识别的事件\n      titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))\n      titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n      titleInput.dispatchEvent(new Event('blur', { bubbles: true }))\n      console.log('[COSE] 网易号标题已填充')\n    } else {\n      console.log('[COSE] 网易号未找到标题输入框')\n    }\n\n    // 2. 等待 Draft.js 编辑器出现\n    const editor = await window.waitFor('.public-DraftEditor-content', 10000) ||\n      await window.waitFor('[contenteditable=\"true\"]', 3000)\n\n    if (editor && htmlBody) {\n      editor.focus()\n\n      // 清空 Draft.js 占位符\n      const placeholder = editor.querySelector('[data-text=\"true\"]')\n      if (placeholder && placeholder.textContent.includes('请输入正文')) {\n        editor.innerHTML = ''\n      }\n\n      // 通过 paste 事件注入 HTML 内容\n      const dt = new DataTransfer()\n      dt.setData('text/html', htmlBody)\n      dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n      const pasteEvent = new ClipboardEvent('paste', {\n        bubbles: true,\n        cancelable: true,\n        clipboardData: dt\n      })\n\n      editor.dispatchEvent(pasteEvent)\n      console.log('[COSE] 网易号内容已通过 paste 事件注入')\n      return { success: true }\n    } else {\n      console.log('[COSE] 网易号未找到编辑器元素')\n      return { success: false, error: 'Editor not found' }\n    }\n  }\n\n  return fill()\n}\n\n/**\n * 网易号同步处理器\n * 网易号使用剪贴板 HTML 粘贴到 Draft.js 编辑器\n * @param {object} tab - Chrome tab 对象\n * @param {object} content - 内容对象 { title, body, markdown, wechatHtml }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncWangyihaoContent(tab, content, helpers) {\n  const { chrome, waitForTab } = helpers\n\n  // 等待页面加载完成（waitForTab 使用 chrome.tabs.onUpdated 监听）\n  await waitForTab(tab.id)\n\n  // 先注入公共工具函数（waitFor 使用 MutationObserver）\n  await injectUtils(chrome, tab.id)\n\n  // 使用剪贴板 HTML（带完整样式）或降级到 body\n  const htmlContent = content.wechatHtml || content.body\n  console.log('[COSE] 网易号 HTML 内容长度:', htmlContent?.length || 0)\n\n  // 在页面中执行：填充标题和粘贴 HTML 内容\n  const result = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: fillWangyihaoContent,\n    args: [content.title, htmlContent],\n    world: 'MAIN',\n  })\n\n  const fillResult = result?.[0]?.result\n  if (fillResult?.success) {\n    return { success: true, message: '已同步到网易号', tabId: tab.id }\n  } else {\n    return { success: false, message: fillResult?.error || '网易号内容填充失败', tabId: tab.id }\n  }\n}\n\n// 导出\nexport { WangyihaoPlatform, fillWangyihaoContent, syncWangyihaoContent }\n"
  },
  {
    "path": "packages/core/src/platforms/wechat.js",
    "content": "import { injectUtils } from './common.js'\n\n// 微信公众号平台配置\nconst WechatPlatform = {\n  id: 'wechat',\n  name: 'WeChat',\n  icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico',\n  url: 'https://mp.weixin.qq.com',\n  // 先打开草稿箱，再自动点击新建\n  publishUrl: 'https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10',\n  title: '微信公众号',\n  type: 'wechat',\n}\n\n// 微信公众号内容填充函数（在页面主世界中执行）\n// 注意：需要先调用 injectUtils 注入 window.waitFor\nasync function fillWechatContent(title, htmlBody) {\n  try {\n    // 等待编辑器加载完成\n    const editor = await window.waitFor('.ProseMirror', 15000)\n    if (!editor) {\n      return { success: false, error: '未找到编辑器' }\n    }\n\n    // 等待标题输入框\n    const titleInput = await window.waitFor('#title')\n\n    // 填充标题\n    if (titleInput && title) {\n      titleInput.focus()\n      // 使用 native setter 确保 React/Vue 等框架能检测到变化\n      const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set\n      if (nativeSetter) {\n        nativeSetter.call(titleInput, title)\n      }\n      else {\n        titleInput.value = title\n      }\n      titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n      titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n      console.log('[COSE] 微信标题已填充:', title)\n    }\n\n    // 稍等一下让标题生效\n    await new Promise(r => setTimeout(r, 300))\n\n    // 填充正文内容\n    if (editor && htmlBody) {\n      editor.focus()\n\n      // 清空现有占位符内容\n      if (editor.textContent.includes('从这里开始写正文')) {\n        editor.innerHTML = ''\n      }\n\n      // 使用真实剪贴板 API 写入 HTML，然后模拟 Ctrl+V\n      try {\n        // 创建 ClipboardItem 并写入剪贴板\n        const blob = new Blob([htmlBody], { type: 'text/html' })\n        const plainBlob = new Blob([htmlBody.replace(/<[^>]*>/g, '')], { type: 'text/plain' })\n        const clipboardItem = new ClipboardItem({\n          'text/html': blob,\n          'text/plain': plainBlob,\n        })\n        await navigator.clipboard.write([clipboardItem])\n        console.log('[COSE] HTML 已写入真实剪贴板')\n\n        // 模拟 Ctrl+V 粘贴\n        editor.dispatchEvent(new KeyboardEvent('keydown', {\n          key: 'v',\n          code: 'KeyV',\n          ctrlKey: true,\n          bubbles: true,\n        }))\n        editor.dispatchEvent(new KeyboardEvent('keyup', {\n          key: 'v',\n          code: 'KeyV',\n          ctrlKey: true,\n          bubbles: true,\n        }))\n        console.log('[COSE] 已模拟 Ctrl+V 粘贴')\n\n        // 等待内容渲染\n        await new Promise(r => setTimeout(r, 800))\n      }\n      catch (clipboardErr) {\n        console.log('[COSE] 真实剪贴板失败，降级到 DataTransfer:', clipboardErr.message)\n        // 降级到 DataTransfer 方案\n        const dt = new DataTransfer()\n        dt.setData('text/html', htmlBody)\n        dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))\n\n        const pasteEvent = new ClipboardEvent('paste', {\n          bubbles: true,\n          cancelable: true,\n          clipboardData: dt,\n        })\n\n        editor.dispatchEvent(pasteEvent)\n        console.log('[COSE] 微信内容已通过 paste 事件注入（降级方案）')\n\n        // 等待内容渲染\n        await new Promise(r => setTimeout(r, 500))\n      }\n\n      // 验证内容是否注入成功\n      const wordCount = editor.textContent?.length || 0\n      if (wordCount === 0) {\n        // 备用方案：直接设置 innerHTML\n        console.log('[COSE] 粘贴未生效，尝试直接设置 innerHTML')\n        editor.innerHTML = htmlBody\n        editor.dispatchEvent(new Event('input', { bubbles: true }))\n      }\n\n      return {\n        success: true,\n        wordCount: editor.textContent?.length || 0,\n        titleFilled: titleInput?.value === title,\n      }\n    }\n\n    return { success: false, error: '内容为空' }\n  }\n  catch (err) {\n    return { success: false, error: err.message }\n  }\n}\n\n// 微信公众号保存草稿函数（在页面主世界中执行）\nfunction saveWechatDraft() {\n  const saveDraftBtn = Array.from(document.querySelectorAll('button'))\n    .find(b => b.textContent.includes('保存为草稿'))\n  if (saveDraftBtn) {\n    saveDraftBtn.click()\n    console.log('[COSE] 已点击保存为草稿')\n    return { success: true }\n  }\n  return { success: false, error: '未找到保存按钮' }\n}\n\n/**\n * 微信公众号同步处理器\n * @param {object} tab - Chrome tab 对象（初始为首页）\n * @param {object} content - 内容对象 { title, body, markdown, wechatHtml }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup, PLATFORMS }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncWechatContent(tab, content, helpers) {\n  const { chrome, waitForTab } = helpers\n\n  // 步骤1：等待首页加载完成\n  console.log('[COSE] 微信公众号等待页面加载')\n  await waitForTab(tab.id)\n  \n  // 注入公共工具函数（waitFor, setInputValue）\n  await injectUtils(chrome, tab.id)\n  \n  // 步骤2：使用 MutationObserver 监听获取 token\n  console.log('[COSE] 开始检测 token...')\n  const [tokenResult] = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: () => {\n      return new Promise((resolve) => {\n        // 先检查当前页面是否已有 token\n        const checkToken = () => {\n          const urlMatch = window.location.href.match(/token=(\\d+)/)\n          if (urlMatch) return urlMatch[1]\n          \n          const links = document.querySelectorAll('a[href*=\"token\"]')\n          for (const link of links) {\n            const match = link.href?.match(/token=(\\d+)/)\n            if (match) return match[1]\n          }\n          \n          const scripts = document.querySelectorAll('script:not([src])')\n          for (const script of scripts) {\n            const content = script.textContent\n            const match = content.match(/token[\"']?\\s*[:=]\\s*[\"']?(\\d+)[\"']?/i)\n            if (match && match[1]) return match[1]\n          }\n          return null\n        }\n\n        const existing = checkToken()\n        if (existing) return resolve(existing)\n\n        // 使用 MutationObserver 监听 DOM 变化\n        const observer = new MutationObserver(() => {\n          const token = checkToken()\n          if (token) {\n            observer.disconnect()\n            resolve(token)\n          }\n        })\n        observer.observe(document.documentElement, { childList: true, subtree: true })\n\n        // 超时保护\n        setTimeout(() => {\n          observer.disconnect()\n          resolve(checkToken())\n        }, 10000)\n      })\n    },\n    world: 'MAIN'\n  })\n  \n  const token = tokenResult?.result\n  \n  if (!token) {\n    console.error('[COSE] 无法从页面获取 token')\n    return { success: false, message: '无法获取微信公众号 token，请确保已登录', tabId: tab.id }\n  }\n  \n  // 步骤3：跳转到编辑器页面\n  const editorUrl = `https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&token=${token}&lang=zh_CN`\n  console.log('[COSE] 获取到 token:', token, '跳转到编辑器')\n  \n  await chrome.tabs.update(tab.id, { url: editorUrl })\n  await waitForTab(tab.id)\n  \n  // 使用剪贴板 HTML（带完整样式）或降级到 body\n  const htmlContent = content.wechatHtml || content.body\n  console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)\n\n  // 步骤4：使用 MutationObserver 监听编辑器出现\n  console.log('[COSE] 正在等待编辑器...')\n  const [editorResult] = await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: () => {\n      return new Promise((resolve) => {\n        const existing = document.querySelector('.ProseMirror')\n        if (existing) return resolve(true)\n\n        const observer = new MutationObserver(() => {\n          if (document.querySelector('.ProseMirror')) {\n            observer.disconnect()\n            resolve(true)\n          }\n        })\n        observer.observe(document.documentElement, { childList: true, subtree: true })\n\n        setTimeout(() => {\n          observer.disconnect()\n          resolve(!!document.querySelector('.ProseMirror'))\n        }, 15000)\n      })\n    },\n    world: 'MAIN'\n  })\n\n  if (!editorResult?.result) {\n    console.error('[COSE] 编辑器等待超时')\n    return { success: false, message: '编辑器加载超时', tabId: tab.id }\n  }\n\n  console.log('[COSE] 编辑器已就绪，开始注入内容...')\n  \n  // 页面跳转后需要重新注入工具函数（waitFor, setInputValue）\n  await injectUtils(chrome, tab.id)\n  \n  // 步骤5：填充内容\n  let result\n  try {\n    result = await chrome.scripting.executeScript({\n      target: { tabId: tab.id },\n      func: fillWechatContent,\n      args: [content.title, htmlContent],\n      world: 'MAIN',\n    })\n  } catch (e) {\n    console.error('[COSE] executeScript 执行失败:', e)\n    return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }\n  }\n\n  const fillResult = result?.[0]?.result\n  console.log('[COSE] 微信填充结果:', JSON.stringify(fillResult, null, 2))\n  \n  if (!fillResult?.success) {\n    console.error('[COSE] 微信内容填充失败:', fillResult?.error)\n    return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }\n  }\n\n  console.log('[COSE] 微信内容填充成功，字数:', fillResult.wordCount)\n\n  // 步骤6：等待内容稳定后，点击保存为草稿按钮\n  await new Promise(resolve => setTimeout(resolve, 500))\n  await chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: saveWechatDraft,\n    world: 'MAIN',\n  })\n\n  return { success: true, message: '已同步并保存为草稿', tabId: tab.id }\n}\n\n// 导出\nexport { WechatPlatform, fillWechatContent, syncWechatContent }\n\n"
  },
  {
    "path": "packages/core/src/platforms/weibo.js",
    "content": "// 微博头条文章平台配置\nconst WeiboPlatform = {\n  id: 'weibo',\n  name: 'Weibo',\n  icon: 'https://weibo.com/favicon.ico',\n  url: 'https://weibo.com',\n  publishUrl: 'https://card.weibo.com/article/v5/editor#/draft',\n  title: '微博头条',\n  type: 'weibo',\n}\n\n// 导出\nexport { WeiboPlatform }\n"
  },
  {
    "path": "packages/core/src/platforms/xiaohongshu.js",
    "content": "// 小红书平台配置\n// 同步方式：使用剪贴板 HTML 粘贴到编辑器\nconst XiaohongshuPlatform = {\n  id: 'xiaohongshu',\n  name: 'Xiaohongshu',\n  icon: 'https://www.xiaohongshu.com/favicon.ico',\n  url: 'https://creator.xiaohongshu.com',\n  publishUrl: 'https://creator.xiaohongshu.com/publish/publish?from=menu&target=article',\n  title: '小红书',\n  type: 'xiaohongshu',\n}\n\n// 小红书内容填充函数（由 background.js 处理）\n// 使用剪贴板粘贴方式填充内容\nasync function fillXiaohongshuContent(content, waitFor, setInputValue) {\n  console.log('[COSE] 小红书填充由 background.js 处理')\n}\n\n// 导出\nexport { XiaohongshuPlatform, fillXiaohongshuContent }\n"
  },
  {
    "path": "packages/core/src/platforms/zhihu.js",
    "content": "// 知乎平台配置\nconst ZhihuPlatform = {\n  id: 'zhihu',\n  name: 'Zhihu',\n  icon: 'https://static.zhihu.com/heifetz/favicon.ico',\n  url: 'https://www.zhihu.com',\n  publishUrl: 'https://zhuanlan.zhihu.com/write',\n  title: '知乎',\n  type: 'zhihu',\n}\n\nimport { injectUtils } from './common.js'\n\n// 知乎内容填充函数（在页面主世界中执行）\n// 知乎现在支持直接粘贴 Markdown，然后弹窗提示转换\n// 注意：需要先调用 injectUtils 注入 window.waitFor\nfunction fillZhihuContent(title, markdown) {\n  // 等待满足条件的元素出现（使用 MutationObserver）\n  function waitForElement(predicate, timeout = 10000) {\n    return new Promise((resolve) => {\n      const el = predicate()\n      if (el) return resolve(el)\n\n      const observer = new MutationObserver(() => {\n        const el = predicate()\n        if (el) {\n          observer.disconnect()\n          resolve(el)\n        }\n      })\n      observer.observe(document.body, { childList: true, subtree: true })\n\n      setTimeout(() => {\n        observer.disconnect()\n        resolve(predicate())\n      }, timeout)\n    })\n  }\n\n  // 等待按钮出现并点击\n  async function waitAndClickButton(textMatcher, timeout = 5000) {\n    const startTime = Date.now()\n    while (Date.now() - startTime < timeout) {\n      const buttons = document.querySelectorAll('button')\n      for (const btn of buttons) {\n        if (textMatcher(btn.textContent)) {\n          btn.click()\n          console.log('[COSE] 已点击按钮:', btn.textContent)\n          return true\n        }\n      }\n      await new Promise(resolve => setTimeout(resolve, 200))\n    }\n    return false\n  }\n\n  async function fillContent() {\n    // 第一步：等待知乎编辑器完全加载（避免\"草稿加载中\"提示）\n    await new Promise(resolve => setTimeout(resolve, 2000))\n    \n    // 第二步：填充标题\n    async function fillTitle() {\n      const titleInput = await window.waitFor('textarea[placeholder*=\"标题\"]')\n      if (titleInput && title) {\n        titleInput.focus()\n        // 使用 nativeInputValueSetter 确保 React 识别变更\n        const nativeSetter = Object.getOwnPropertyDescriptor(\n          window.HTMLTextAreaElement.prototype, 'value'\n        )?.set\n        if (nativeSetter) {\n          nativeSetter.call(titleInput, title)\n        } else {\n          titleInput.value = title\n        }\n        titleInput.dispatchEvent(new Event('input', { bubbles: true }))\n        titleInput.dispatchEvent(new Event('change', { bubbles: true }))\n        console.log('[COSE] 知乎标题填充成功')\n      }\n    }\n    \n    // 先填充标题\n    await fillTitle()\n    \n    // 再等待一下确保标题已保存\n    await new Promise(resolve => setTimeout(resolve, 500))\n\n    // 第三步：找到并激活知乎编辑器\n    const editorSelectors = [\n      '.public-DraftEditor-content',\n      '[contenteditable=\"true\"]',\n      '.DraftEditor-root'\n    ]\n    \n    let editor = null\n    for (const selector of editorSelectors) {\n      editor = document.querySelector(selector)\n      if (editor) break\n    }\n    \n    if (!editor) {\n      console.log('[COSE] 未找到知乎编辑器')\n      return { success: false, error: 'Editor not found' }\n    }\n\n    // 激活编辑器：模拟真实点击序列\n    const rect = editor.getBoundingClientRect()\n    const centerX = rect.left + rect.width / 2\n    const centerY = rect.top + rect.height / 2\n    \n    // 触发鼠标事件序列激活编辑器\n    for (const eventType of ['mousedown', 'mouseup', 'click']) {\n      const event = new MouseEvent(eventType, {\n        bubbles: true,\n        cancelable: true,\n        view: window,\n        clientX: centerX,\n        clientY: centerY,\n        button: 0\n      })\n      editor.dispatchEvent(event)\n    }\n    \n    // 聚焦编辑器\n    editor.focus()\n    \n    // 清空现有内容\n    document.execCommand('selectAll', false)\n    document.execCommand('delete', false)\n    \n    // 等待编辑器状态更新\n    await new Promise(resolve => setTimeout(resolve, 100))\n\n    // 第三步：通过剪贴板 + 键盘事件模拟真实粘贴\n    // 这是触发知乎 Markdown 检测弹窗的关键方法\n    const contentToFill = markdown || ''\n    \n    if (!contentToFill) {\n      console.log('[COSE] 没有 Markdown 内容需要填充')\n      await fillTitle()\n      return { success: true, method: 'empty' }\n    }\n\n    try {\n      // 使用 ClipboardEvent 模拟粘贴 - 这是触发 Markdown 检测弹窗的关键\n      // execCommand('insertText') 不会触发弹窗\n      \n      // 检查浏览器兼容性\n      if (typeof DataTransfer === 'undefined' || typeof ClipboardEvent === 'undefined') {\n        throw new Error('浏览器不支持 DataTransfer 或 ClipboardEvent')\n      }\n      \n      const dt = new DataTransfer()\n      dt.setData('text/plain', contentToFill)\n      \n      const pasteEvent = new ClipboardEvent('paste', {\n        bubbles: true,\n        cancelable: true,\n        clipboardData: dt\n      })\n      \n      editor.focus()\n      const dispatched = editor.dispatchEvent(pasteEvent)\n      console.log('[COSE] 已触发 ClipboardEvent，dispatched:', dispatched)\n      \n      // 等待 Markdown 检测弹窗出现并点击\"确认并解析\"\n      await new Promise(resolve => setTimeout(resolve, 500))\n      \n      const parseClicked = await waitAndClickButton(\n        text => text.includes('确认并解析'),\n        5000\n      )\n      \n      if (parseClicked) {\n        console.log('[COSE] 已点击\"确认并解析\"')\n        \n        // 等待解析完成并点击\"确认\"\n        await new Promise(resolve => setTimeout(resolve, 500))\n        \n        const confirmClicked = await waitAndClickButton(\n          text => text === '确认',\n          5000\n        )\n        \n        if (confirmClicked) {\n          console.log('[COSE] 已点击\"确认\"，Markdown 解析完成')\n        }\n      } else {\n        console.log('[COSE] 未检测到 Markdown 弹窗')\n      }\n    } catch (err) {\n      console.log('[COSE] 内容插入失败:', err.message || err)\n    }\n\n    // 等待内容渲染\n    await new Promise(resolve => setTimeout(resolve, 300))\n\n    return { success: true, method: 'paste-markdown' }\n  }\n\n  return fillContent()\n}\n\n/**\n * 知乎同步处理器\n * 知乎现在支持直接粘贴 Markdown，然后弹窗提示转换\n * @param {object} tab - Chrome tab 对象\n * @param {object} content - 内容对象 { title, body, markdown }\n * @param {object} helpers - 帮助函数 { chrome, waitForTab, addTabToSyncGroup }\n * @returns {Promise<{success: boolean, message?: string, tabId?: number}>}\n */\nasync function syncZhihuContent(tab, content, helpers) {\n  const { waitForTab } = helpers\n\n  // 等待页面加载完成（waitForTab 使用 chrome.tabs.onUpdated 监听）\n  await waitForTab(tab.id)\n\n  // 激活知乎标签页（避免后台标签页限制导致填充失败）\n  try {\n    await chrome.tabs.update(tab.id, { active: true })\n    console.log('[COSE] 已激活知乎标签页')\n    // 等待标签页激活完成\n    await new Promise(resolve => setTimeout(resolve, 500))\n  } catch (err) {\n    console.log('[COSE] 激活标签页失败:', err.message || err)\n  }\n\n  // 先注入公共工具函数（waitFor 使用 MutationObserver）\n  await injectUtils(globalThis.chrome, tab.id)\n\n  // 在页面中执行内容填充\n  const result = await globalThis.chrome.scripting.executeScript({\n    target: { tabId: tab.id },\n    func: fillZhihuContent,\n    args: [content.title, content.markdown],\n    world: 'MAIN',\n  })\n\n  const fillResult = result?.[0]?.result\n  if (fillResult?.success) {\n    // 等待 2 秒确保内容已保存\n    await new Promise(resolve => setTimeout(resolve, 2000))\n    \n    // 等待图片上传完成后再刷新\n    console.log('[COSE] 开始监听图片上传请求...')\n    const uploadComplete = await waitForImageUploadComplete(tab.id)\n    \n    if (uploadComplete) {\n      console.log('[COSE] 图片上传完成，准备刷新页面')\n      try {\n        if (chrome?.tabs && tab?.id) {\n          await chrome.tabs.reload(tab.id, { bypassCache: false })\n          console.log('[COSE] 已模拟用户刷新知乎页面')\n        } else {\n          console.log('[COSE] chrome.tabs 或 tab.id 不可用，跳过刷新')\n        }\n      } catch (err) {\n        console.log('[COSE] 刷新页面失败:', err.message || err)\n      }\n    } else {\n      console.log('[COSE] 未检测到图片上传请求或超时，跳过刷新')\n    }\n    \n    return { success: true, message: '已打开知乎并同步内容', tabId: tab.id }\n  } else {\n    return { success: false, message: fillResult?.error || '内容同步失败', tabId: tab.id }\n  }\n}\n\n/**\n * 等待图片上传完成\n * @param {number} tabId - 标签页 ID\n * @param {number} timeout - 超时时间（毫秒）\n * @returns {Promise<boolean>}\n */\nasync function waitForImageUploadComplete(tabId, timeout = 30000) {\n  const startTime = Date.now()\n  \n  // 在页面中注入监听脚本\n  const result = await globalThis.chrome.scripting.executeScript({\n    target: { tabId: tabId },\n    func: () => {\n      return new Promise((resolve) => {\n        const pendingUploads = new Map() // uploadId -> { url, completed }\n        let hasUploadRequests = false\n        let lastUploadTime = 0\n        \n        // 检查是否所有上传都完成\n        const checkAllComplete = () => {\n          if (pendingUploads.size === 0) return false\n          \n          for (const [id, info] of pendingUploads) {\n            if (!info.completed) return false\n          }\n          return true\n        }\n        \n        // 监听 fetch 请求\n        const originalFetch = window.fetch\n        window.fetch = function(...args) {\n          const url = args[0]\n          const options = args[1] || {}\n          \n          // 检测图片上传请求（知乎的图片上传通常包含这些特征）\n          const isImageUpload = typeof url === 'string' && (\n            url.includes('/api/v4/images') ||\n            url.includes('/api/v4/upload') ||\n            url.includes('upload') ||\n            (options.method === 'POST' && url.includes('zhihu.com'))\n          )\n          \n          if (isImageUpload) {\n            hasUploadRequests = true\n            lastUploadTime = Date.now()\n            const uploadId = Date.now() + Math.random()\n            pendingUploads.set(uploadId, { url, completed: false })\n            console.log('[COSE] 检测到图片上传请求:', url, uploadId)\n          }\n          \n          return originalFetch.apply(this, args)\n            .then(response => {\n              if (isImageUpload) {\n                console.log('[COSE] 图片上传请求完成:', url, response.status)\n                // 标记为已完成\n                for (const [id, info] of pendingUploads) {\n                  if (info.url === url) {\n                    info.completed = true\n                    break\n                  }\n                }\n              }\n              return response\n            })\n            .catch(error => {\n              if (isImageUpload) {\n                console.log('[COSE] 图片上传请求失败:', url, error)\n                // 即使失败也标记为已完成（有反馈结果）\n                for (const [id, info] of pendingUploads) {\n                  if (info.url === url) {\n                    info.completed = true\n                    break\n                  }\n                }\n              }\n              throw error\n            })\n        }\n        \n        // 监听 XMLHttpRequest\n        const originalOpen = XMLHttpRequest.prototype.open\n        const originalSend = XMLHttpRequest.prototype.send\n        \n        XMLHttpRequest.prototype.open = function(method, url, ...rest) {\n          this._url = url\n          this._method = method\n          return originalOpen.apply(this, [method, url, ...rest])\n        }\n        \n        XMLHttpRequest.prototype.send = function(...args) {\n          const isImageUpload = this._url && (\n            this._url.includes('/api/v4/images') ||\n            this._url.includes('/api/v4/upload') ||\n            this._url.includes('upload') ||\n            (this._method === 'POST' && this._url.includes('zhihu.com'))\n          )\n          \n          if (isImageUpload) {\n            hasUploadRequests = true\n            lastUploadTime = Date.now()\n            const uploadId = Date.now() + Math.random()\n            pendingUploads.set(uploadId, { url: this._url, completed: false })\n            console.log('[COSE] 检测到图片上传 XHR:', this._url, uploadId)\n            \n            this.addEventListener('loadend', () => {\n              console.log('[COSE] 图片上传 XHR 完成:', this._url, this.status)\n              // 标记为已完成\n              for (const [id, info] of pendingUploads) {\n                if (info.url === this._url) {\n                  info.completed = true\n                  break\n                }\n              }\n            })\n          }\n          \n          return originalSend.apply(this, args)\n        }\n        \n        // 定期检查是否所有上传都完成\n        const checkTimer = setInterval(() => {\n          // 如果没有检测到任何上传请求，说明可能没有图片需要上传\n          if (!hasUploadRequests) {\n            console.log('[COSE] 未检测到图片上传请求')\n            clearInterval(checkTimer)\n            resolve(true)\n            return\n          }\n          \n          // 如果所有上传都完成，并且距离最后一个上传请求已经过去2秒（确保没有新请求）\n          if (checkAllComplete() && Date.now() - lastUploadTime > 2000) {\n            console.log('[COSE] 所有图片上传请求已完成')\n            clearInterval(checkTimer)\n            resolve(true)\n            return\n          }\n        }, 500)\n        \n        // 10秒后如果没有检测到上传请求，认为没有图片需要上传\n        setTimeout(() => {\n          if (!hasUploadRequests) {\n            console.log('[COSE] 10秒内未检测到上传请求，认为无图片')\n            clearInterval(checkTimer)\n            resolve(true)\n          }\n        }, 10000)\n        \n        // 超时后无论如何都返回\n        setTimeout(() => {\n          console.log('[COSE] 等待图片上传超时')\n          clearInterval(checkTimer)\n          resolve(true) // 即使超时也刷新\n        }, timeout)\n      })\n    },\n    world: 'MAIN',\n  })\n  \n  // 等待监听结果\n  const uploadResult = result?.[0]?.result\n  console.log('[COSE] 图片上传监听结果:', uploadResult)\n  \n  // 给一个额外的缓冲时间，确保图片已经完全加载和渲染\n  await new Promise(resolve => setTimeout(resolve, 2000))\n  \n  return uploadResult !== false\n}\n\n// 导出\nexport { ZhihuPlatform, fillZhihuContent, syncZhihuContent }\n"
  },
  {
    "path": "packages/core/src/utils.js",
    "content": "// 通用平台工具函数\n\n/**\n * 注入通用工具函数到页面主世界\n * 此函数会在页面中定义 window.waitFor 和 window.setInputValue\n */\nfunction injectCommonUtils() {\n  // 等待元素出现的工具函数（使用 MutationObserver）\n  window.waitFor = (selector, timeout = 10000) => {\n    return new Promise((resolve) => {\n      const el = document.querySelector(selector)\n      if (el) return resolve(el)\n\n      const observer = new MutationObserver(() => {\n        const el = document.querySelector(selector)\n        if (el) {\n          observer.disconnect()\n          resolve(el)\n        }\n      })\n      observer.observe(document.body, { childList: true, subtree: true })\n\n      setTimeout(() => {\n        observer.disconnect()\n        resolve(document.querySelector(selector))\n      }, timeout)\n    })\n  }\n\n  // 设置输入值的工具函数\n  window.setInputValue = (el, value) => {\n    if (!el || !value) return\n    el.focus()\n    if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {\n      // 使用 native setter 确保 React/Vue 等框架能检测到变化\n      const nativeSetter = Object.getOwnPropertyDescriptor(\n        window.HTMLTextAreaElement.prototype, 'value'\n      )?.set || Object.getOwnPropertyDescriptor(\n        window.HTMLInputElement.prototype, 'value'\n      )?.set\n      if (nativeSetter) {\n        nativeSetter.call(el, value)\n      } else {\n        el.value = value\n      }\n      el.dispatchEvent(new Event('input', { bubbles: true }))\n      el.dispatchEvent(new Event('change', { bubbles: true }))\n    } else if (el.contentEditable === 'true') {\n      el.innerHTML = value.replace(/\\n/g, '<br>')\n      el.dispatchEvent(new Event('input', { bubbles: true }))\n    }\n  }\n\n  return true\n}\n\n/**\n * 在页面中注入通用工具函数\n * @param {object} chrome - Chrome API 对象\n * @param {number} tabId - 目标 tab ID\n * @returns {Promise<void>}\n */\nasync function injectUtils(chrome, tabId) {\n  await chrome.scripting.executeScript({\n    target: { tabId },\n    func: injectCommonUtils,\n    world: 'MAIN',\n  })\n}\n\n// 导出\nexport { injectCommonUtils, injectUtils }\n"
  },
  {
    "path": "packages/detection/index.js",
    "content": "export * from './src/configs.js'\nexport * from './src/detect.js'\nexport * from './src/utils.js'\n"
  },
  {
    "path": "packages/detection/package.json",
    "content": "{\n    \"name\": \"@cose/detection\",\n    \"version\": \"1.0.0\",\n    \"description\": \"Platform login detection for COSE\",\n    \"main\": \"index.js\",\n    \"type\": \"module\",\n    \"exports\": {\n        \".\": \"./index.js\",\n        \"./platforms/*\": \"./src/platforms/*.js\"\n    }\n}"
  },
  {
    "path": "packages/detection/src/configs.js",
    "content": "/**\n * @cose/detection - Platform login detection module\n * \n * This package provides login detection configurations for all supported platforms.\n * Each config includes:\n * - api: The API endpoint to check login status\n * - method: HTTP method (GET/POST)\n * - checkLogin: Function to determine if user is logged in from response\n * - getUserInfo: Function to extract username and avatar from response\n */\n\n// 掘金\nexport const JuejinLoginConfig = {\n    api: 'https://api.juejin.cn/user_api/v1/user/get',\n    method: 'GET',\n    checkLogin: (response) => response?.err_no === 0 && response?.data?.user_id,\n    getUserInfo: (response) => ({\n        username: response?.data?.user_name,\n        avatar: response?.data?.avatar_large,\n    }),\n}\n\n// 知乎\nexport const ZhihuLoginConfig = {\n    api: 'https://www.zhihu.com/api/v4/me',\n    method: 'GET',\n    checkLogin: (response) => response?.id,\n    getUserInfo: (response) => ({\n        username: response?.name,\n        avatar: response?.avatar_url,\n    }),\n}\n\n// 头条号\nexport const ToutiaoLoginConfig = {\n    api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231',\n    method: 'GET',\n    checkLogin: (response) => response?.code === 0 && response?.name,\n    getUserInfo: (response) => ({\n        username: response?.name,\n        avatar: response?.avatar_url,\n    }),\n}\n\n// 百家号\nexport const BaijiahaoLoginConfig = {\n    api: 'https://baijiahao.baidu.com/builder/app/appinfo',\n    method: 'GET',\n    checkLogin: (response) => response?.errno === 0 && response?.data?.user?.name,\n    getUserInfo: (response) => ({\n        username: response?.data?.user?.name,\n        avatar: response?.data?.user?.avatar,\n    }),\n}\n\n// 抖音\nexport const DouyinLoginConfig = {\n    api: 'https://creator.douyin.com/web/api/media/user/info/',\n    method: 'GET',\n    checkLogin: (response) => response?.status_code === 0 && (response?.user?.uid || response?.user_info?.uid),\n    getUserInfo: (response) => ({\n        username: response?.user?.nickname || response?.user_info?.nickname,\n        avatar: (response?.user?.avatar_thumb?.url_list?.[0] || response?.user_info?.avatar_thumb?.url_list?.[0]),\n    }),\n}\n\n// 统一的 LOGIN_CHECK_CONFIG 对象（按平台 ID 索引）\nexport const LOGIN_CHECK_CONFIG = {\n    juejin: JuejinLoginConfig,\n    zhihu: ZhihuLoginConfig,\n    toutiao: ToutiaoLoginConfig,\n\n    baijiahao: BaijiahaoLoginConfig,\n    douyin: DouyinLoginConfig,\n}\n"
  },
  {
    "path": "packages/detection/src/detect.js",
    "content": "import { LOGIN_CHECK_CONFIG } from './configs.js'\nimport { checkLoginByCookie, detectByApi } from './utils.js'\nimport { detectCSDNUser } from './platforms/csdn.js'\nimport { detectOSChinaUser } from './platforms/oschina.js'\nimport { detectAlipayUser } from './platforms/alipay.js'\nimport { detectWeiboUser } from './platforms/weibo.js'\nimport { detectWechatUser } from './platforms/wechat.js'\nimport { detectXiaohongshuUser } from './platforms/xiaohongshu.js'\nimport { detectElecfansUser } from './platforms/elecfans.js'\nimport { detectHuaweiCloudUser } from './platforms/huaweicloud.js'\nimport { detectHuaweiDevUser } from './platforms/huaweidev.js'\nimport { detectSspaiUser } from './platforms/sspai.js'\nimport { detectAliyunUser } from './platforms/aliyun.js'\nimport { detectSohuUser } from './platforms/sohu.js'\nimport { detectMediumUser } from './platforms/medium.js'\nimport { detectTencentCloudUser } from './platforms/tencentcloud.js'\nimport { detectQianfanUser } from './platforms/qianfan.js'\nimport { detectTwitterUser } from './platforms/twitter.js'\nimport { detectBilibiliUser } from './platforms/bilibili.js'\nimport { detectCTO51User } from './platforms/cto51.js'\nimport { detectJianshuUser } from './platforms/jianshu.js'\nimport { detectSegmentFaultUser } from './platforms/segmentfault.js'\nimport { detectInfoQUser } from './platforms/infoq.js'\nimport { detectModelScopeUser } from './platforms/modelscope.js'\nimport { detectVolcengineUser } from './platforms/volcengine.js'\nimport { detectCnblogsUser } from './platforms/cnblogs.js'\nimport { detectWangyihaoUser } from './platforms/wangyihao.js'\nimport { detectDoubanUser } from './platforms/douban.js'\n\n// Platform-specific detectors map\nconst PLATFORM_DETECTORS = {\n    'csdn': detectCSDNUser,\n    'oschina': detectOSChinaUser,\n    'alipayopen': detectAlipayUser,\n    'weibo': detectWeiboUser,\n    'wechat': detectWechatUser,\n    'xiaohongshu': detectXiaohongshuUser,\n    'elecfans': detectElecfansUser,\n    'huaweicloud': detectHuaweiCloudUser,\n    'huaweidev': detectHuaweiDevUser,\n    'sspai': detectSspaiUser,\n    'aliyun': detectAliyunUser,\n    'sohu': detectSohuUser,\n    'medium': detectMediumUser,\n    'tencentcloud': detectTencentCloudUser,\n    'qianfan': detectQianfanUser,\n    'twitter': detectTwitterUser,\n    'bilibili': detectBilibiliUser,\n    'cto51': detectCTO51User,\n    'jianshu': detectJianshuUser,\n    'segmentfault': detectSegmentFaultUser,\n    'infoq': detectInfoQUser,\n    'modelscope': detectModelScopeUser,\n    'volcengine': detectVolcengineUser,\n    'cnblogs': detectCnblogsUser,\n    'wangyihao': detectWangyihaoUser,\n    'douban': detectDoubanUser,\n}\n\nexport async function detectUser(platformId) {\n    console.log(`[COSE] Detection: Checking ${platformId}`)\n\n    // 1. Platform-specific Detectors\n    if (PLATFORM_DETECTORS[platformId]) {\n        return PLATFORM_DETECTORS[platformId]()\n    }\n\n    // 2. Generic Config-based Detection\n    const config = LOGIN_CHECK_CONFIG[platformId]\n    if (config) {\n        if (config.useCookie || (config.cookieNames && config.cookieNames.length > 0)) {\n            return checkLoginByCookie(platformId, config)\n        }\n\n        // Default to API check if API is defined\n        if (config.api) {\n            return detectByApi(platformId, config)\n        }\n    }\n\n    return { loggedIn: false, error: 'No detection available' }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/alipay.js",
    "content": "/**\n * Alipay Open platform detection logic\n * Strategy:\n * 1. Check chrome.storage.local cache (1 hour TTL)\n * 2. If cache miss, inject script into open Alipay tab to call API\n */\nexport async function detectAlipayUser() {\n    const platformId = 'alipayopen'\n    try {\n        // 从 storage 读取缓存的用户信息\n        const stored = await chrome.storage.local.get('alipayopen_user')\n        const cachedUser = stored.alipayopen_user\n\n        if (cachedUser && cachedUser.loggedIn) {\n            // 检查缓存是否过期（1小时）\n            const cacheAge = Date.now() - (cachedUser.cachedAt || 0)\n            const maxAge = 1 * 60 * 60 * 1000 // 1 hour\n\n            if (cacheAge < maxAge) {\n                console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username)\n                return {\n                    loggedIn: true,\n                    username: cachedUser.username || '',\n                    avatar: cachedUser.avatar || ''\n                }\n            } else {\n                console.log(`[COSE] alipayopen 缓存已过期`)\n                // 清除过期缓存\n                await chrome.storage.local.remove('alipayopen_user')\n            }\n        }\n\n        // 尝试从已打开的支付宝页面获取用户信息并缓存\n        let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' })\n        if (tabs.length === 0) {\n            tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' })\n        }\n\n        if (tabs.length > 0) {\n            try {\n                // 在已打开的页面上下文中调用 API\n                const results = await chrome.scripting.executeScript({\n                    target: { tabId: tabs[0].id },\n                    func: async () => {\n                        try {\n                            const response = await fetch('https://developerportal.alipay.com/octopus/service.do', {\n                                method: 'POST',\n                                credentials: 'include',\n                                headers: {\n                                    'Accept': 'application/json',\n                                    'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',\n                                },\n                                body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query',\n                            })\n                            if (!response.ok) return null\n                            return await response.json()\n                        } catch (e) {\n                            return null\n                        }\n                    }\n                })\n\n                const data = results?.[0]?.result\n                console.log(`[COSE] alipayopen API 数据:`, data)\n\n                if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) {\n                    const username = data.data.nickname || ''\n                    const avatar = data.data.avatar || ''\n\n                    // 缓存用户信息\n                    await chrome.storage.local.set({\n                        alipayopen_user: {\n                            loggedIn: true,\n                            username,\n                            avatar,\n                            cachedAt: Date.now()\n                        }\n                    })\n\n                    console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像')\n                    return { loggedIn: true, username, avatar }\n                }\n            } catch (e) {\n                console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message)\n            }\n        }\n\n        console.log(`[COSE] alipayopen 未检测到登录状态`)\n        return { loggedIn: false }\n    } catch (e) {\n        console.log(`[COSE] alipayopen 检测失败:`, e.message)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/aliyun.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Aliyun Developer platform detection logic\n * Strategy:\n * 1. Check login_aliyunid_ticket cookie\n * 2. Call getUser API for username/avatar\n * 3. Convert avatar to base64 to bypass CORS/ORB\n */\nexport async function detectAliyunUser() {\n    try {\n        const ticketCookie = await chrome.cookies.get({ url: 'https://developer.aliyun.com', name: 'login_aliyunid_ticket' })\n        if (!ticketCookie || !ticketCookie.value) return { loggedIn: false }\n\n        const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'application/json' }\n        })\n        const data = await response.json()\n\n        if (data.success && data.data?.nickname) {\n            let avatar = data.data.avatar || ''\n            if (avatar) {\n                avatar = await convertAvatarToBase64(avatar, 'https://developer.aliyun.com/')\n            }\n            return { loggedIn: true, username: data.data.nickname, avatar }\n        }\n        return { loggedIn: false }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/bilibili.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Bilibili platform detection logic\n * Strategy:\n * 1. Call https://api.bilibili.com/x/web-interface/nav API\n * 2. Extract username (uname) and avatar (face)\n * 3. Convert hdslb.com avatar to base64 data URL to bypass CORS/ORB\n */\nexport async function detectBilibiliUser() {\n    try {\n        const response = await fetch('https://api.bilibili.com/x/web-interface/nav', {\n            method: 'GET',\n            credentials: 'include',\n            headers: {\n                'Accept': 'application/json',\n                'Cache-Control': 'no-cache',\n            },\n        })\n        const data = await response.json()\n\n        if (data?.code !== 0 || !data?.data?.isLogin) {\n            return { loggedIn: false }\n        }\n\n        const username = data.data.uname || ''\n        let avatar = data.data.face || ''\n\n        // Convert hdslb.com avatar to base64 data URL to bypass CORS/ORB\n        if (avatar && avatar.includes('hdslb.com')) {\n            avatar = await convertAvatarToBase64(avatar, 'https://www.bilibili.com/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.log(`[COSE] bilibili 检测失败:`, e.message)\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/cnblogs.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Cnblogs (博客园) detection logic (offscreen approach)\n * Fetch https://account.cnblogs.com/user/userinfo via offscreen document,\n * where cookies are sent automatically in document context.\n */\nexport async function detectCnblogsUser() {\n    try {\n        console.log('[COSE] Cnblogs Detection: Starting (offscreen)')\n\n        if (typeof globalThis.__coseDetectCnblogs === 'function') {\n            const result = await globalThis.__coseDetectCnblogs()\n            if (result && result.loggedIn) {\n                let avatar = result.avatar || ''\n                if (avatar && avatar.includes('cnblogs.com')) {\n                    avatar = await convertAvatarToBase64(avatar, 'https://www.cnblogs.com/')\n                }\n                console.log('[COSE] Cnblogs: Logged in:', result.username)\n                return { loggedIn: true, username: result.username || '', avatar }\n            }\n        }\n\n        console.log('[COSE] Cnblogs: Not logged in')\n        return { loggedIn: false }\n    } catch (e) {\n        console.error('[COSE] Cnblogs Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/csdn.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * CSDN platform detection logic\n * Strategy:\n * 1. Check 'UserName' cookie (reliable indicator of login)\n * 2. Use 'UserNick' cookie for display name (UserName is the user ID, UserNick is the display name)\n * 3. If logged in, fetch public blog page to get avatar\n */\nexport async function detectCSDNUser() {\n    try {\n        console.log('[COSE] CSDN Detection: Starting cookie check')\n        const userNameCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserName' })\n\n        if (userNameCookie && userNameCookie.value) {\n            const userId = userNameCookie.value\n            console.log(`[COSE] CSDN UserName cookie found: ${userId}`)\n\n            // UserNick cookie contains the display name (e.g. 'xxxxxx')\n            const userNickCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserNick' })\n            const username = (userNickCookie && userNickCookie.value) ? decodeURIComponent(userNickCookie.value) : userId\n            console.log(`[COSE] CSDN display name: ${username}`)\n\n            let avatar = ''\n            try {\n                // Fetch public blog page for avatar\n                const blogUrl = `https://blog.csdn.net/${userId}`\n                const blogResp = await fetch(blogUrl, { method: 'GET' })\n                const blogHtml = await blogResp.text()\n\n                // Extract avatar\n                const avatarMatch = blogHtml.match(/<img[^>]*src=[\"'](https:\\/\\/(?:profile|i-avatar)\\.csdnimg\\.cn\\/[^\"']+)[\"']/i) ||\n                    blogHtml.match(/<img[^>]*class=[\"']avatar[^\"']*[\"'][^>]*src=[\"']([^\"']+)[\"']/i)\n\n                if (avatarMatch) {\n                    avatar = avatarMatch[1]\n                }\n            } catch (e) {\n                console.warn('[COSE] CSDN Avatar fetch failed:', e)\n            }\n\n            // Convert csdnimg.cn avatar to base64 data URL to bypass CORS/ORB\n            if (avatar && avatar.includes('csdnimg.cn')) {\n                avatar = await convertAvatarToBase64(avatar, 'https://blog.csdn.net/')\n            }\n\n            return {\n                loggedIn: true,\n                username: username,\n                avatar: avatar\n            }\n        }\n\n        console.log('[COSE] CSDN: No login detected')\n        return { loggedIn: false }\n    } catch (e) {\n        console.error('[COSE] CSDN Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/cto51.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * 51CTO platform detection logic (same approach as 爱贝壳)\n * Fetch https://home.51cto.com/space via offscreen document,\n * parse HTML with DOMParser to extract avatar, uid, nickname.\n */\nexport async function detectCTO51User() {\n    try {\n        console.log('[COSE] 51CTO Detection: Starting (offscreen)')\n\n        if (typeof globalThis.__coseDetectCto51 === 'function') {\n            const result = await globalThis.__coseDetectCto51()\n            if (result && result.loggedIn) {\n                let avatar = result.avatar || ''\n                if (avatar && avatar.startsWith('http') && avatar.includes('51cto.com')) {\n                    avatar = await convertAvatarToBase64(avatar, 'https://home.51cto.com/')\n                }\n                console.log('[COSE] 51CTO: Logged in:', result.username)\n                return { loggedIn: true, username: result.username || '', avatar }\n            }\n            // Pass through debug info if present\n            if (result && result._debug) {\n                console.log('[COSE] 51CTO: Not logged in, debug:', JSON.stringify(result._debug))\n                return { loggedIn: false, _debug: result._debug }\n            }\n        }\n\n        console.log('[COSE] 51CTO: Not logged in')\n        return { loggedIn: false }\n    } catch (e) {\n        console.error('[COSE] 51CTO Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/douban.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\nasync function convertToBase64WithFallback(avatarUrl) {\n    if (!avatarUrl) return ''\n\n    // Use shared utility only\n    try {\n        const converted = await convertAvatarToBase64(avatarUrl, 'https://www.douban.com/')\n        if (converted && converted.startsWith('data:')) {\n            return converted\n        }\n    } catch (e) {\n        console.log('[COSE] douban 通用头像转换失败:', e.message)\n    }\n\n    // Fallback: manual fetch with cookies\n    try {\n        const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' })\n        const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ')\n        const imgResp = await fetch(avatarUrl, {\n            method: 'GET',\n            headers: {\n                'Referer': 'https://www.douban.com/',\n                ...(cookieHeader ? { 'Cookie': cookieHeader } : {})\n            },\n            credentials: 'include'\n        })\n        if (!imgResp.ok) {\n            return avatarUrl\n        }\n        const blob = await imgResp.blob()\n        const buffer = await blob.arrayBuffer()\n        const bytes = new Uint8Array(buffer)\n        let binary = ''\n        for (let i = 0; i < bytes.length; i++) {\n            binary += String.fromCharCode(bytes[i])\n        }\n        return `data:${blob.type || 'image/jpeg'};base64,${btoa(binary)}`\n    } catch (e) {\n        console.log('[COSE] douban 手动头像转换失败:', e.message)\n        return avatarUrl\n    }\n}\n\n/**\n * Douban platform detection logic\n * Strategy:\n * 1. Check dbcl2 cookie on douban.com as login indicator\n * 2. Parse /mine/ HTML to get user info\n * 3. Fallback: derive uid from dbcl2 cookie\n * 4. If avatar missing but uid exists, fetch profile page\n */\nexport async function detectDoubanUser() {\n    try {\n        // 1. Check dbcl2 cookie as login indicator\n        const dbcl2Cookie = await chrome.cookies.get({\n            url: 'https://www.douban.com',\n            name: 'dbcl2'\n        })\n\n        if (!dbcl2Cookie || !dbcl2Cookie.value) {\n            console.log('[COSE] douban 未找到登录 cookie，未登录')\n            return { loggedIn: false }\n        }\n\n        // Logged in — now try to get user details\n        let username = ''\n        let avatar = ''\n        let uid = ''\n        let loginConfirmed = false\n\n        // 2. Parse /mine/ HTML to get user info\n        try {\n            const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' })\n            const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n            const response = await fetch('https://www.douban.com/mine/', {\n                method: 'GET',\n                credentials: 'include',\n                headers: {\n                    'Accept': 'text/html,application/xhtml+xml',\n                    ...(cookieHeader ? { 'Cookie': cookieHeader } : {})\n                }\n            })\n\n            if (response.ok) {\n                const finalUrl = response.url || ''\n                const html = await response.text()\n                const redirectedToLogin = /\\/accounts\\/login/i.test(finalUrl)\n                    || /name=[\"']form_email[\"']/i.test(html)\n                    || /登录豆瓣|扫码登录/i.test(html)\n                const hasUserSignals = /的账号</.test(html)\n                    || /https?:\\/\\/www\\.douban\\.com\\/people\\/([^/\"?#]+)\\/?/.test(html)\n                    || /\\/people\\/([^/\"?#]+)\\/?/.test(html)\n                    || /doubanio\\.com\\/icon\\//i.test(html)\n\n                loginConfirmed = !redirectedToLogin && hasUserSignals\n\n                if (!username) {\n                    const accountMatch = html.match(/>([^<\\n]+)的账号</)\n                    if (accountMatch?.[1]) {\n                        username = accountMatch[1].trim()\n                    }\n                }\n\n                if (!username || !uid) {\n                    const profileLinkMatch = html.match(/https?:\\/\\/www\\.douban\\.com\\/people\\/([^/\"?#]+)\\/?/)\n                    if (profileLinkMatch?.[1]) {\n                        uid = profileLinkMatch[1]\n                    }\n                }\n\n                if (!avatar) {\n                    const avatarMatch = html.match(/https?:\\/\\/img\\d\\.doubanio\\.com\\/icon\\/[^\"'\\s<]+/i)\n                        || html.match(/\\/\\/img\\d\\.doubanio\\.com\\/icon\\/[^\"'\\s<]+/i)\n                        || html.match(/\\/icon\\/up\\d+-\\d+\\.jpg/i)\n                    if (avatarMatch?.[1]) {\n                        avatar = avatarMatch[1]\n                    } else if (avatarMatch?.[0]) {\n                        avatar = avatarMatch[0]\n                    }\n\n                    if (avatar && avatar.startsWith('//')) {\n                        avatar = `https:${avatar}`\n                    } else if (avatar && avatar.startsWith('/icon/')) {\n                        avatar = `https://img3.doubanio.com${avatar}`\n                    }\n                }\n\n                if (username || avatar || uid) {\n                    console.log('[COSE] douban 从 /mine/ HTML 获取用户信息:', username || uid)\n                }\n            }\n        } catch (e) {\n            console.log('[COSE] douban /mine/ 解析失败:', e.message)\n        }\n\n        // 3. Fallback: derive uid from dbcl2 cookie as username placeholder\n        if (loginConfirmed && !username && !uid && dbcl2Cookie.value) {\n            const uidFromCookie = dbcl2Cookie.value.match(/\"?([^:\"]+):/)\n            if (uidFromCookie?.[1]) {\n                uid = uidFromCookie[1]\n            }\n        }\n\n        if (!loginConfirmed) {\n            console.log('[COSE] douban 仅有 cookie，未确认登录态，按未登录处理')\n            return { loggedIn: false }\n        }\n\n        if (!username && uid) {\n            username = uid\n        }\n\n        // 5. If avatar still missing but uid exists, fetch profile page and extract avatar\n        if (!avatar && uid) {\n            try {\n                const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' })\n                const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ')\n                const profileResp = await fetch(`https://www.douban.com/people/${uid}/`, {\n                    method: 'GET',\n                    credentials: 'include',\n                    headers: {\n                        'Accept': 'text/html,application/xhtml+xml',\n                        ...(cookieHeader ? { 'Cookie': cookieHeader } : {})\n                    }\n                })\n\n                if (profileResp.ok) {\n                    const profileHtml = await profileResp.text()\n                    const profileAvatar = profileHtml.match(/https?:\\/\\/img\\d\\.doubanio\\.com\\/icon\\/[^\"'\\s<]+/i)\n                        || profileHtml.match(/\\/\\/img\\d\\.doubanio\\.com\\/icon\\/[^\"'\\s<]+/i)\n                    if (profileAvatar?.[0]) {\n                        avatar = profileAvatar[0]\n                        console.log('[COSE] douban 从个人页补充头像成功')\n                    }\n                }\n            } catch (e) {\n                console.log('[COSE] douban 从个人页补充头像失败:', e.message)\n            }\n        }\n\n        if (avatar && avatar.startsWith('//')) {\n            avatar = `https:${avatar}`\n        }\n\n        // Convert douban avatar to base64 if needed\n        if (avatar && avatar.startsWith('http')) {\n            try {\n                avatar = await convertToBase64WithFallback(avatar)\n            } catch (e) {\n                console.log('[COSE] douban 头像转换失败:', e.message)\n            }\n        }\n\n        // Cookie exists means logged in; return best-effort user details\n        return { loggedIn: true, username: username || '', avatar: avatar || '' }\n    } catch (e) {\n        console.log('[COSE] douban 检测失败:', e.message)\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/elecfans.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Elecfans (电子发烧友) detection logic\n * API: /api/mobile/index.php?module=profile (Discuz standard mobile API)\n * Response: { Variables: { member_uid, space: { username, realname }, member_avatar } }\n * Uses chrome.cookies.getAll to attach cookies manually (SameSite workaround)\n */\nexport async function detectElecfansUser() {\n    try {\n        // Collect cookies for bbs.elecfans.com\n        const cookies = await chrome.cookies.getAll({ domain: '.elecfans.com' })\n        const bbsCookies = await chrome.cookies.getAll({ url: 'https://bbs.elecfans.com' })\n        const allCookies = [...cookies, ...bbsCookies]\n        const seen = new Set()\n        const uniqueCookies = allCookies.filter(c => {\n            const key = `${c.name}=${c.value}`\n            if (seen.has(key)) return false\n            seen.add(key)\n            return true\n        })\n        const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n        if (!cookieStr) return { loggedIn: false }\n\n        const response = await fetch('https://bbs.elecfans.com/api/mobile/index.php?module=profile', {\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json',\n                'Cookie': cookieStr,\n            },\n        })\n\n        if (!response.ok) return { loggedIn: false }\n\n        const data = await response.json()\n        if (!data?.Variables?.member_uid) return { loggedIn: false }\n\n        const username = data.Variables.space?.username\n            || data.Variables.space?.realname\n            || data.Variables.member_username || ''\n        let avatar = data.Variables.member_avatar || ''\n\n        if (!username) return { loggedIn: false }\n\n        if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://bbs.elecfans.com/')\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/huaweicloud.js",
    "content": "import { convertAvatarToBase64, detectByApi } from '../utils.js'\n\nconst HUAWEICLOUD_API = 'https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info'\n\n/**\n * Huawei Cloud platform detection logic\n * 直接通过 API + 手动附加 cookie 检测，无需打开华为云页面\n */\nexport async function detectHuaweiCloudUser() {\n    try {\n        const result = await detectByApi('huaweicloud', {\n            api: HUAWEICLOUD_API,\n            method: 'GET',\n            checkLogin: (data) => data && data.memName,\n            getUserInfo: (data) => ({\n                username: data.memAlias || data.memName || '',\n                avatar: data.memPhoto || '',\n            }),\n        })\n\n        if (result.loggedIn) {\n            let avatar = result.avatar || ''\n            if (avatar && avatar.startsWith('http')) {\n                avatar = await convertAvatarToBase64(avatar, 'https://bbs.huaweicloud.com/')\n            }\n            // 更新缓存（仅用于头像 base64 缓存，不用于登录判断）\n            await chrome.storage.local.set({\n                huaweicloud_user: {\n                    loggedIn: true,\n                    username: result.username,\n                    avatar,\n                    cachedAt: Date.now(),\n                }\n            })\n            return { loggedIn: true, username: result.username, avatar }\n        }\n\n        // 未登录，清除可能过期的缓存\n        await chrome.storage.local.remove('huaweicloud_user')\n        return { loggedIn: false }\n    } catch (e) {\n        console.error('[COSE] HuaweiCloud Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/huaweidev.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Huawei Developer platform detection logic\n * Strategy (cache detection pattern):\n * 1. Check chrome.storage.local cache (7 days TTL) + cookie validation\n * 2. Try executeScript on open developer.huawei.com tab to call API with credentials\n * 3. Content script auto-caches user info when visiting huawei developer pages\n * 4. Fallback: check developer_userdata cookie existence for basic login status\n */\nexport async function detectHuaweiDevUser() {\n    try {\n        // 1. 先检查缓存\n        const stored = await chrome.storage.local.get('huaweidev_user')\n        const cachedUser = stored.huaweidev_user\n\n        if (cachedUser && cachedUser.loggedIn) {\n            const cacheAge = Date.now() - (cachedUser.cachedAt || 0)\n            const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days\n            if (cacheAge < maxAge && cachedUser.username) {\n                // 验证 cookie 是否仍然存在，防止用户已登出但缓存未过期的误判\n                const userCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'developer_userdata' })\n                if (userCookie && userCookie.value) {\n                    console.log('[COSE] HuaweiDev: using cached user info:', cachedUser.username)\n                    let avatar = cachedUser.avatar || ''\n                    // 如果缓存中的头像还是原始 URL（旧缓存），转换为 base64 并更新缓存\n                    if (avatar && avatar.startsWith('http')) {\n                        avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/')\n                        await chrome.storage.local.set({ huaweidev_user: { ...cachedUser, avatar } })\n                    }\n                    return { loggedIn: true, username: cachedUser.username, avatar }\n                }\n                // cookie 已失效，清除缓存\n                console.log('[COSE] HuaweiDev: cache exists but cookie gone, clearing cache')\n                await chrome.storage.local.remove('huaweidev_user')\n            } else {\n                await chrome.storage.local.remove('huaweidev_user')\n            }\n        }\n\n        // 2. 尝试在已打开的华为开发者页面中检测\n        const tabs = await chrome.tabs.query({ url: 'https://developer.huawei.com/*' })\n        if (tabs.length > 0) {\n            try {\n                const results = await chrome.scripting.executeScript({\n                    target: { tabId: tabs[0].id },\n                    func: async () => {\n                        try {\n                            // 1. 从 DOM 获取社区用户名（真实昵称，非脱敏手机号）\n                            const userNameEl = document.querySelector('.user_name')\n                            const domUsername = userNameEl ? userNameEl.textContent.trim() : ''\n\n                            // 2. 从 API 获取头像\n                            const cookies = document.cookie.split(';').map(c => c.trim())\n                            const udCookie = cookies.find(c => c.startsWith('developer_userdata='))\n                            if (!udCookie) {\n                                return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null\n                            }\n\n                            const udValue = decodeURIComponent(udCookie.split('=').slice(1).join('='))\n                            let csrfToken = ''\n                            try {\n                                const udJson = JSON.parse(udValue)\n                                csrfToken = udJson.csrf || udJson.csrftoken || ''\n                            } catch (e) {\n                                return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null\n                            }\n                            if (!csrfToken) {\n                                return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null\n                            }\n\n                            const now = new Date()\n                            const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}/, '')\n\n                            let avatar = ''\n                            try {\n                                const resp = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', {\n                                    method: 'POST',\n                                    credentials: 'include',\n                                    headers: {\n                                        'Content-Type': 'application/json',\n                                        'Accept': 'application/json',\n                                        'x-hd-csrf': csrfToken,\n                                        'x-hd-date': hdDate,\n                                    },\n                                    body: JSON.stringify({\n                                        svc: 'GOpen.User.getInfo',\n                                        reqType: 0,\n                                        reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }),\n                                    }),\n                                })\n                                if (resp.ok) {\n                                    const data = await resp.json()\n                                    if (data && data.returnCode === '0' && data.resJson) {\n                                        const userInfo = JSON.parse(data.resJson)\n                                        avatar = userInfo.headPictureURL || ''\n                                    }\n                                }\n                            } catch (e) { /* avatar fetch failed, continue with DOM username */ }\n\n                            // 优先使用 DOM 中的社区昵称\n                            return {\n                                loggedIn: true,\n                                username: domUsername || '',\n                                avatar,\n                            }\n                        } catch (e) { return null }\n                    },\n                })\n\n                const result = results?.[0]?.result\n                if (result && result.loggedIn) {\n                    let avatar = result.avatar || ''\n                    if (avatar && avatar.startsWith('http')) {\n                        avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/')\n                    }\n                    const userInfo = { ...result, avatar, cachedAt: Date.now() }\n                    await chrome.storage.local.set({ huaweidev_user: userInfo })\n                    return { loggedIn: true, username: userInfo.username, avatar }\n                }\n            } catch (e) {\n                console.log('[COSE] HuaweiDev: executeScript failed:', e.message)\n            }\n        }\n\n        // 3. 没有打开的华为开发者页面，检查 developer_userdata cookie 并通过 offscreen fetch 获取用户信息\n        const userCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'developer_userdata' })\n        if (userCookie && userCookie.value) {\n            console.log('[COSE] HuaweiDev: developer_userdata cookie found, trying offscreen fetch for user info')\n            let username = ''\n            let avatar = ''\n            let apiSuccess = false\n            try {\n                const udValue = decodeURIComponent(userCookie.value)\n                const udJson = JSON.parse(udValue)\n                const csrfToken = udJson.csrftoken || udJson.csrf || ''\n                if (csrfToken) {\n                    const now = new Date()\n                    const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\\.\\d{3}/, '')\n\n                    // 确保 offscreen document 存在\n                    try {\n                        await chrome.offscreen.createDocument({\n                            url: 'offscreen.html',\n                            reasons: ['DOM_SCRAPING'],\n                            justification: 'Fetch Huawei Developer user info with cookies',\n                        })\n                    } catch (e) {\n                        // 已存在则忽略\n                        if (!e.message.includes('Only a single offscreen')) {\n                            throw e\n                        }\n                    }\n\n                    const response = await chrome.runtime.sendMessage({\n                        type: 'OFFSCREEN_FETCH',\n                        payload: {\n                            url: 'https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate',\n                            method: 'POST',\n                            headers: {\n                                'Content-Type': 'application/json',\n                                'Accept': 'application/json',\n                                'x-hd-csrf': csrfToken,\n                                'x-hd-date': hdDate,\n                            },\n                            body: {\n                                svc: 'GOpen.User.getInfo',\n                                reqType: 0,\n                                reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }),\n                            },\n                        },\n                    })\n\n                    if (response?.success && response.data) {\n                        const data = response.data\n                        if (data.returnCode === '0' && data.resJson) {\n                            const userInfo = JSON.parse(data.resJson)\n                            username = userInfo.displayName || ''\n                            avatar = userInfo.headPictureURL || ''\n                            if (avatar && avatar.startsWith('http')) {\n                                avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/')\n                            }\n                            apiSuccess = true\n                        }\n                    }\n\n                    // 关闭 offscreen document\n                    try { await chrome.offscreen.closeDocument() } catch (e) { /* ignore */ }\n                }\n            } catch (e) {\n                console.log('[COSE] HuaweiDev: offscreen fetch failed:', e.message)\n            }\n\n            // API 成功才认为已登录，否则视为登录过期\n            if (apiSuccess) {\n                if (username) {\n                    const userInfo = { loggedIn: true, username, avatar, cachedAt: Date.now() }\n                    await chrome.storage.local.set({ huaweidev_user: userInfo })\n                }\n                return { loggedIn: true, username, avatar }\n            } else {\n                // API 失败，清除可能残留的缓存\n                await chrome.storage.local.remove('huaweidev_user')\n                console.log('[COSE] HuaweiDev: API verification failed, treating as logged out')\n                return { loggedIn: false }\n            }\n        }\n\n        return { loggedIn: false }\n    } catch (e) {\n        console.error('[COSE] HuaweiDev Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/infoq.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * InfoQ platform detection logic\n * Strategy: POST to /public/v1/user/get_user API to get user info\n * The old /public/v1/my/menu endpoint returns 404.\n */\nexport async function detectInfoQUser() {\n    try {\n        console.log('[COSE] InfoQ Detection: Starting')\n        const response = await fetch('https://www.infoq.cn/public/v1/user/get_user', {\n            method: 'POST',\n            credentials: 'include',\n            headers: {\n                'Content-Type': 'application/json',\n                'Accept': 'application/json',\n            },\n            body: JSON.stringify({}),\n        })\n\n        if (!response.ok) return { loggedIn: false }\n\n        const json = await response.json()\n        if (json?.code !== 0 || !json?.data?.uid) {\n            console.log('[COSE] InfoQ: Not logged in', json?.code)\n            return { loggedIn: false }\n        }\n\n        const username = json.data.nickname || ''\n        let avatar = json.data.avatar || ''\n\n        if (avatar && avatar.includes('geekbang.org')) {\n            avatar = await convertAvatarToBase64(avatar, 'https://www.infoq.cn/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.error('[COSE] InfoQ Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/jianshu.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Jianshu platform detection logic\n * Strategy: Fetch settings JSON API to get nickname and avatar\n */\nexport async function detectJianshuUser() {\n    try {\n        console.log('[COSE] Jianshu Detection: Starting')\n        const response = await fetch('https://www.jianshu.com/settings/basic.json', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'application/json' },\n        })\n\n        if (!response.ok) return { loggedIn: false }\n\n        const json = await response.json()\n        if (!json?.data) return { loggedIn: false }\n\n        const username = json.data.nickname || ''\n        let avatar = json.data.avatar || ''\n\n        if (avatar && avatar.includes('jianshu.io')) {\n            avatar = await convertAvatarToBase64(avatar, 'https://www.jianshu.com/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.error('[COSE] Jianshu Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/medium.js",
    "content": "/**\n * Medium platform detection logic\n * Strategy:\n * 1. Check sid/uid cookies\n * 2. Fetch stats page and extract username/avatar via regex\n */\nexport async function detectMediumUser() {\n    try {\n        const sidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'sid' })\n        const uidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'uid' })\n\n        if (!sidCookie && !uidCookie) return { loggedIn: false }\n\n        const response = await fetch('https://medium.com/me/stats', {\n            method: 'GET',\n            credentials: 'include',\n        })\n        const html = await response.text()\n        const finalUrl = response.url\n\n        if (finalUrl.includes('/m/signin') || finalUrl.includes('?signIn')) return { loggedIn: false }\n\n        const profileMatch = html.match(/\"username\"\\s*:\\s*\"([^\"]+)\"/) ||\n            html.match(/href=\"https:\\/\\/medium\\.com\\/@([^\"?\\/]+)\"/) ||\n            html.match(/medium\\.com\\/@([a-zA-Z0-9_]+)/)\n\n        if (profileMatch && profileMatch[1] && profileMatch[1] !== 'gmail' && profileMatch[1] !== 'medium') {\n            const username = profileMatch[1]\n\n            // Extract avatar via imageId from JSON data near the username\n            let avatar = ''\n            const imageIdMatch = html.match(new RegExp(`\"imageId\"\\\\s*:\\\\s*\"([^\"]+)\"[^}]*\"username\"\\\\s*:\\\\s*\"${username}\"`)) ||\n                html.match(new RegExp(`\"username\"\\\\s*:\\\\s*\"${username}\"[^}]*\"imageId\"\\\\s*:\\\\s*\"([^\"]+)\"`))\n            if (imageIdMatch) {\n                avatar = `https://miro.medium.com/v2/resize:fill:64:64/${imageIdMatch[1]}`\n            }\n\n            return { loggedIn: true, username, avatar }\n        } else {\n            return { loggedIn: true, username: '', avatar: '' }\n        }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/modelscope.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * ModelScope platform detection logic\n * Strategy:\n * 1. Try /api/v1/users/login/info API (correct endpoint found via network inspection)\n * 2. Fallback: find an open ModelScope tab and extract username/avatar from DOM\n * 3. Fallback: check for auth cookies on modelscope.cn\n * 4. Convert avatar to base64 to bypass CORS/ORB\n */\nexport async function detectModelScopeUser() {\n    try {\n        let username = ''\n        let avatar = ''\n\n        // Try the correct API endpoint (found via Chrome DevTools network inspection)\n        try {\n            const response = await fetch('https://modelscope.cn/api/v1/users/login/info', {\n                method: 'GET',\n                credentials: 'include',\n                headers: {\n                    'Accept': 'application/json',\n                },\n            })\n            if (response.ok) {\n                const data = await response.json()\n                if (data?.Success !== false && data?.Code !== 10019901001) {\n                    const user = data?.Data?.User || data?.Data || {}\n                    username = user.Nickname || user.NickName || user.Name\n                        || user.nickname || user.name || user.Login || user.login || ''\n                    avatar = user.Avatar || user.avatar || ''\n                }\n            }\n        } catch (e) { }\n\n        if (username) {\n            if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/')\n            return { loggedIn: true, username, avatar }\n        }\n\n        // Fallback: extract from open tab DOM\n        try {\n            const tabs = await chrome.tabs.query({ url: 'https://modelscope.cn/*' })\n            if (tabs.length > 0) {\n                const results = await chrome.scripting.executeScript({\n                    target: { tabId: tabs[0].id },\n                    func: () => {\n                        // Look for avatar img in the page\n                        let avatarSrc = ''\n                        const avatarSelectors = [\n                            'img[src*=\"avatar\"]',\n                            '.ant-avatar img',\n                            'img[class*=\"avatar\" i]',\n                            'img[class*=\"Avatar\" i]',\n                        ]\n                        for (const sel of avatarSelectors) {\n                            const img = document.querySelector(sel)\n                            if (img && img.src && !img.src.includes('data:image/svg')) {\n                                avatarSrc = img.src\n                                break\n                            }\n                        }\n                        // Look for username from the page's user info\n                        let name = ''\n                        // Try to get from the header/nav user dropdown area\n                        const allLinks = document.querySelectorAll('a[href*=\"/profile/\"]')\n                        for (const a of allLinks) {\n                            const href = a.getAttribute('href') || ''\n                            const match = href.match(/\\/profile\\/([^/?#]+)/)\n                            if (match && match[1]) {\n                                name = match[1]\n                                break\n                            }\n                        }\n                        // Also try my/overview link text or nearby elements\n                        if (!name) {\n                            const myLink = document.querySelector('a[href=\"/my/overview\"]')\n                            if (myLink) {\n                                const parent = myLink.closest('[class*=\"dropdown\"]') || myLink.parentElement\n                                if (parent) {\n                                    const spans = parent.querySelectorAll('span')\n                                    for (const s of spans) {\n                                        const t = s.textContent.trim()\n                                        if (t && t.length > 1 && t.length < 30\n                                            && !['登录', '注册', '退出', '设置'].includes(t)) {\n                                            name = t\n                                            break\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        return { username: name, avatar: avatarSrc }\n                    },\n                })\n                if (results?.[0]?.result) {\n                    username = results[0].result.username || ''\n                    avatar = results[0].result.avatar || ''\n                }\n                if (username || avatar) {\n                    if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/')\n                    return { loggedIn: true, username, avatar }\n                }\n            }\n        } catch (e) { }\n\n        return { loggedIn: false }\n    } catch (e) {\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/oschina.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * OSChina platform detection logic\n * Strategy:\n * 1. Check oscid cookie existence as login indicator (MV3 service worker compatible)\n * 2. Best-effort: fetch user info via API for username and avatar\n */\nexport async function detectOSChinaUser() {\n    try {\n        // Check oscid cookie as login indicator\n        const oscidCookie = await chrome.cookies.get({ url: 'https://www.oschina.net', name: 'oscid' })\n        if (!oscidCookie || !oscidCookie.value) {\n            return { loggedIn: false }\n        }\n\n        // Collect all cookies for API request\n        const cookies = await chrome.cookies.getAll({ domain: '.oschina.net' })\n        const wwwCookies = await chrome.cookies.getAll({ url: 'https://www.oschina.net' })\n        const apiCookies = await chrome.cookies.getAll({ url: 'https://apiv1.oschina.net' })\n        const allCookies = [...cookies, ...wwwCookies, ...apiCookies]\n        const seen = new Set()\n        const uniqueCookies = allCookies.filter(c => {\n            const key = `${c.name}=${c.value}`\n            if (seen.has(key)) return false\n            seen.add(key)\n            return true\n        })\n        const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n        // Best-effort: try to get username and avatar via API\n        let username = ''\n        let avatar = ''\n        let userId = ''\n        try {\n            const response = await fetch('https://apiv1.oschina.net/oschinapi/user/myDetails', {\n                method: 'GET',\n                headers: {\n                    'Accept': 'application/json',\n                    'Cookie': cookieStr,\n                },\n            })\n            if (response.ok) {\n                const data = await response.json()\n                if (data?.success && data?.result?.userVo) {\n                    username = data.result.userVo.name || ''\n                    avatar = data.result.userVo.portraitUrl || ''\n                    userId = String(data.result.userVo.id || '')\n                }\n            }\n        } catch (e) {\n            console.log('[COSE] OSChina: API fetch failed, using cookie-only detection')\n        }\n\n        if (avatar && (avatar.includes('oschina.net') || avatar.includes('oscimg'))) {\n            avatar = await convertAvatarToBase64(avatar, 'https://www.oschina.net/')\n        }\n\n        // Store userId for sync URL construction\n        if (userId) {\n            try { await chrome.storage.local.set({ oschina_userId: userId }) } catch (e) { /* ignore */ }\n        }\n\n        return { loggedIn: true, username, avatar, userId }\n    } catch (e) {\n        console.error('[COSE] OSChina Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/qianfan.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Baidu Qianfan platform detection logic\n * Strategy:\n * 1. Read csrftoken from bce-user-info-ct-id cookie\n * 2. Call current user API with csrftoken header\n */\nexport async function detectQianfanUser() {\n    try {\n        console.log('[COSE] Qianfan Detection: Starting')\n\n        // Read csrftoken from cookie\n        const csrfCookie = await chrome.cookies.get({ url: 'https://qianfan.cloud.baidu.com', name: 'bce-user-info-ct-id' })\n        const csrfToken = csrfCookie?.value ? csrfCookie.value.replace(/\"/g, '') : ''\n\n        const response = await fetch('https://qianfan.cloud.baidu.com/api/community/user/current', {\n            method: 'GET',\n            credentials: 'include',\n            headers: {\n                'Accept': 'application/json',\n                ...(csrfToken ? { 'csrftoken': csrfToken } : {}),\n            }\n        })\n        if (!response.ok) return { loggedIn: false }\n\n        const data = await response.json()\n        if (data.success && data.result) {\n            const username = data.result.displayName || data.result.nickname || ''\n            let avatar = data.result.avatar || ''\n\n            if (avatar && avatar.includes('bdimg.com')) {\n                avatar = await convertAvatarToBase64(avatar, 'https://qianfan.cloud.baidu.com/')\n            }\n\n            return { loggedIn: true, username, avatar }\n        } else {\n            return { loggedIn: false }\n        }\n    } catch (e) {\n        console.error('[COSE] Qianfan Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/segmentfault.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * SegmentFault platform detection logic\n * Strategy: Fetch homepage HTML and extract user info from __NEXT_DATA__ JSON\n * The /gateway/user/me API requires CSRF tokens (ivd param), so we use HTML scraping instead.\n */\nexport async function detectSegmentFaultUser() {\n    try {\n        console.log('[COSE] SegmentFault Detection: Starting')\n        const response = await fetch('https://segmentfault.com/', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'text/html' },\n        })\n        const html = await response.text()\n\n        // Extract __NEXT_DATA__ JSON\n        const nextDataMatch = html.match(/<script\\s+id=\"__NEXT_DATA__\"[^>]*>([\\s\\S]*?)<\\/script>/)\n        if (!nextDataMatch) {\n            console.log('[COSE] SegmentFault: No __NEXT_DATA__ found')\n            return { loggedIn: false }\n        }\n\n        const nextData = JSON.parse(nextDataMatch[1])\n        const sessionUser = nextData?.props?.pageProps?.initialState?.global?.sessionUser\n        const sessionInfo = nextData?.props?.pageProps?.initialState?.global?.sessionInfo\n\n        if (!sessionUser?.user?.id && !sessionInfo?.login) {\n            console.log('[COSE] SegmentFault: Not logged in')\n            return { loggedIn: false }\n        }\n\n        const user = sessionUser?.user || {}\n        const username = user.name || user.slug || ''\n        let avatar = user.avatar_url || ''\n\n        if (avatar && avatar.includes('segmentfault.com')) {\n            avatar = await convertAvatarToBase64(avatar, 'https://segmentfault.com/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.error('[COSE] SegmentFault Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/sohu.js",
    "content": "/**\n * Sohu (搜狐号) platform detection logic\n * Strategy:\n * 1. Check ppinf cookie on mp.sohu.com\n * 2. Call account list API for nickname/avatar\n */\nexport async function detectSohuUser() {\n    try {\n        const ppinfCookie = await chrome.cookies.get({ url: 'https://mp.sohu.com', name: 'ppinf' })\n        if (!ppinfCookie || !ppinfCookie.value) return { loggedIn: false }\n\n        try {\n            const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', {\n                method: 'GET',\n                credentials: 'include',\n                headers: { 'Accept': 'application/json' }\n            })\n            const data = await response.json()\n            if (data.success && data.data?.data?.[0]?.accounts?.[0]) {\n                const account = data.data.data[0].accounts[0]\n                let avatar = account.avatar || ''\n                if (avatar.startsWith('//')) avatar = 'https:' + avatar\n                return { loggedIn: true, username: account.nickName, avatar }\n            } else {\n                return { loggedIn: true, username: '', avatar: '' }\n            }\n        } catch (e) {\n            return { loggedIn: true, username: '', avatar: '' }\n        }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/sspai.js",
    "content": "/**\n * Sspai (少数派) platform detection logic\n * Strategy:\n * 1. Check sspai_jwt_token cookie\n * 2. Call user info API with Bearer token\n */\nexport async function detectSspaiUser() {\n    try {\n        const jwtCookie = await chrome.cookies.get({ url: 'https://sspai.com', name: 'sspai_jwt_token' })\n        if (!jwtCookie || !jwtCookie.value) return { loggedIn: false }\n\n        const token = jwtCookie.value\n        const response = await fetch('https://sspai.com/api/v1/user/info/get', {\n            method: 'GET',\n            credentials: 'include',\n            headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}` }\n        })\n        const data = await response.json()\n\n        if (data.error === 0 && data.data?.nickname) {\n            return { loggedIn: true, username: data.data.nickname, avatar: data.data.avatar || '' }\n        } else {\n            return { loggedIn: false }\n        }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/tencentcloud.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Tencent Cloud platform detection logic\n * Strategy:\n * 1. Fetch creator page\n * 2. Check redirect and parse HTML for nickname/avatar\n */\nexport async function detectTencentCloudUser() {\n    try {\n        const response = await fetch('https://cloud.tencent.com/developer/creator', {\n            method: 'GET',\n            credentials: 'include',\n        })\n        const html = await response.text()\n        const finalUrl = response.url\n\n        if (!finalUrl.includes('/creator')) return { loggedIn: false }\n        if (html.includes('登录/注册') || html.includes('\"isLogin\":false') || html.includes('\"login\":false')) return { loggedIn: false }\n\n        const userInfoMatch = html.match(/\"userInfo\"\\s*:\\s*\\{[^}]*\"nickname\"\\s*:\\s*\"([^\"]+)\"[^}]*\\}/) ||\n            html.match(/\"creatorInfo\"\\s*:\\s*\\{[^}]*\"nickname\"\\s*:\\s*\"([^\"]+)\"[^}]*\\}/) ||\n            html.match(/\"currentUser\"\\s*:\\s*\\{[^}]*\"nickname\"\\s*:\\s*\"([^\"]+)\"[^}]*\\}/)\n\n        const creatorNicknameMatch = html.match(/class=\"creator-info[^\"]*\"[^>]*>[\\s\\S]*?<[^>]*class=\"[^\"]*name[^\"]*\"[^>]*>([^<]+)</) ||\n            html.match(/\"isCreator\"\\s*:\\s*true[\\s\\S]*?\"nickname\"\\s*:\\s*\"([^\"]+)\"/)\n\n        const nicknameMatch = userInfoMatch || creatorNicknameMatch\n        const avatarMatch = html.match(/\"userInfo\"[\\s\\S]*?\"avatarUrl\"\\s*:\\s*\"([^\"]+)\"/) ||\n            html.match(/\"avatar\"\\s*:\\s*\"(https?:\\/\\/[^\"]+)\"/)\n\n        if (nicknameMatch && nicknameMatch[1]) {\n            let avatar = avatarMatch ? avatarMatch[1] : ''\n            if (avatar && avatar.includes('qcloudimg.com')) {\n                avatar = await convertAvatarToBase64(avatar, 'https://cloud.tencent.com/')\n            }\n            return { loggedIn: true, username: nicknameMatch[1], avatar }\n        } else {\n            if (html.includes('创作中心') || html.includes('我的文章')) return { loggedIn: true, username: '', avatar: '' }\n            return { loggedIn: false }\n        }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/twitter.js",
    "content": "/**\n * Twitter/X platform detection logic\n * Strategy:\n * 1. Check auth_token/ct0 cookies on x.com\n * 2. Fetch home page HTML and extract screen_name/avatar via regex\n */\nexport async function detectTwitterUser() {\n    try {\n        const authTokenCookie = await chrome.cookies.get({ url: 'https://x.com', name: 'auth_token' })\n        const ct0Cookie = await chrome.cookies.get({ url: 'https://x.com', name: 'ct0' })\n\n        if (!authTokenCookie) return { loggedIn: false }\n\n        let username = ''\n        let avatar = ''\n\n        try {\n            const response = await fetch('https://x.com/home', {\n                method: 'GET',\n                credentials: 'include',\n                headers: { 'Accept': 'text/html' }\n            })\n            if (response.ok) {\n                const html = await response.text()\n                const screenNameMatch = html.match(/\"screen_name\"\\s*:\\s*\"([^\"]+)\"/)\n                if (screenNameMatch) username = screenNameMatch[1]\n                const avatarMatch = html.match(/\"profile_image_url_https\"\\s*:\\s*\"([^\"]+)\"/)\n                if (avatarMatch) avatar = avatarMatch[1].replace('_normal.', '_x96.')\n            }\n        } catch (e) { }\n\n        // If fetch failed or regex failed, try explicit fallback scraping logic (simplified here)\n        // Note: Full scrape logic from background.js is complex. \n        // We will assume basic fetch works or just return loggedIn:true if cookie exists\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) { return { loggedIn: false } }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/volcengine.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Volcengine (火山引擎开发者社区) detection logic\n * API: /api/fe/v1/user (found via Chrome DevTools network inspection)\n * Response: { data: { name, avatar: { url } }, err_no: 0 }\n * Uses chrome.cookies.getAll to attach cookies manually since service worker\n * fetch with credentials:'include' doesn't reliably send SameSite cookies.\n */\nexport async function detectVolcengineUser() {\n    try {\n        // Collect cookies for volcengine.com to attach to API request\n        const cookies = await chrome.cookies.getAll({ domain: '.volcengine.com' })\n        const devCookies = await chrome.cookies.getAll({ url: 'https://developer.volcengine.com' })\n        const allCookies = [...cookies, ...devCookies]\n        const seen = new Set()\n        const uniqueCookies = allCookies.filter(c => {\n            const key = `${c.name}=${c.value}`\n            if (seen.has(key)) return false\n            seen.add(key)\n            return true\n        })\n        const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n        if (!cookieStr) return { loggedIn: false }\n\n        const response = await fetch('https://developer.volcengine.com/api/fe/v1/user', {\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json',\n                'Cookie': cookieStr,\n            },\n        })\n\n        if (!response.ok) return { loggedIn: false }\n\n        const data = await response.json()\n        if (data?.err_no !== 0 || !data?.data?.name) return { loggedIn: false }\n\n        let username = data.data.name\n        let avatar = data.data.avatar?.url || ''\n\n        if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://developer.volcengine.com/')\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/wangyihao.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * 网易号 detection logic\n * Strategy:\n * 1. Collect cookies via chrome.cookies.getAll (MV3 service worker compatible)\n * 2. Fetch user info via mp.163.com/wemedia/navinfo.do with cookies attached manually\n * 3. Extract username and avatar from API response\n */\nexport async function detectWangyihaoUser() {\n    try {\n        const cookies = await chrome.cookies.getAll({ domain: '.163.com' })\n        const mpCookies = await chrome.cookies.getAll({ url: 'https://mp.163.com' })\n        const allCookies = [...cookies, ...mpCookies]\n        const seen = new Set()\n        const uniqueCookies = allCookies.filter(c => {\n            const key = `${c.name}=${c.value}`\n            if (seen.has(key)) return false\n            seen.add(key)\n            return true\n        })\n        const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n        if (!cookieStr) return { loggedIn: false }\n\n        const response = await fetch(`https://mp.163.com/wemedia/navinfo.do?_=${Date.now()}`, {\n            method: 'GET',\n            headers: {\n                'Accept': 'application/json',\n                'Cookie': cookieStr,\n            },\n        })\n\n        if (!response.ok) return { loggedIn: false }\n\n        const data = await response.json()\n        if (data?.code !== 1 || !data?.data?.wemediaId) return { loggedIn: false }\n\n        const username = data.data.tname || ''\n        let avatar = data.data.icon || ''\n\n        if (avatar && (avatar.includes('126.net') || avatar.includes('163.com'))) {\n            avatar = await convertAvatarToBase64(avatar, 'https://mp.163.com/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.error('[COSE] Wangyihao Detection Error:', e)\n        return { loggedIn: false, error: e.message }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/wechat.js",
    "content": "/**\n * WeChat Official Account platform detection logic\n * Strategy:\n * 1. Check chrome.storage.local cache (1 hour TTL)\n * 2. Inject script into open mp.weixin.qq.com tab to read wx.data\n * 3. Fallback: fetch mp.weixin.qq.com HTML and parse nick_name/head_img\n */\nexport async function detectWechatUser() {\n    const platformId = 'wechat'\n    try {\n        // 先检查缓存\n        const stored = await chrome.storage.local.get('wechat_user')\n        const cachedUser = stored.wechat_user\n\n        if (cachedUser && cachedUser.loggedIn) {\n            const cacheAge = Date.now() - (cachedUser.cachedAt || 0)\n            const maxAge = 1 * 60 * 60 * 1000 // 1 hour\n\n            if (cacheAge < maxAge) {\n                console.log(`[COSE] wechat 从缓存读取:`, cachedUser.username)\n                return {\n                    loggedIn: true,\n                    username: cachedUser.username || '',\n                    avatar: cachedUser.avatar || ''\n                }\n            } else {\n                await chrome.storage.local.remove('wechat_user')\n            }\n        }\n\n        // 优先尝试在已打开的微信公众号页面中检测\n        const tabs = await chrome.tabs.query({ url: 'https://mp.weixin.qq.com/*' })\n        if (tabs.length > 0) {\n            try {\n                const results = await chrome.scripting.executeScript({\n                    target: { tabId: tabs[0].id },\n                    func: () => {\n                        const wxData = window.wx?.data\n                        if (wxData && wxData.nick_name) {\n                            return {\n                                loggedIn: true,\n                                username: wxData.nick_name || wxData.user_name || '',\n                                avatar: wxData.head_img || '',\n                                token: wxData.t || ''\n                            }\n                        }\n                        return null\n                    }\n                })\n\n                const result = results?.[0]?.result\n                if (result && result.loggedIn) {\n                    const userInfo = { ...result, cachedAt: Date.now() }\n                    await chrome.storage.local.set({ wechat_user: userInfo })\n                    return {\n                        loggedIn: true,\n                        username: userInfo.username || '',\n                        avatar: userInfo.avatar || ''\n                    }\n                }\n            } catch (e) {\n                console.log(`[COSE] wechat 页面脚本执行失败:`, e.message)\n            }\n        }\n\n        // 备用方案：fetch 首页并解析 HTML\n        try {\n            const response = await fetch('https://mp.weixin.qq.com/', {\n                method: 'GET',\n                credentials: 'include',\n                headers: { 'Accept': 'text/html' }\n            })\n            const html = await response.text()\n\n            if (html.includes('请使用微信扫描') || html.includes('扫码登录')) {\n                return { loggedIn: false }\n            }\n\n            const nickMatch = html.match(/nick_name\\s*[:=]\\s*[\"']([^\"']+)[\"']/)\n            const avatarMatch = html.match(/head_img\\s*[:=]\\s*[\"']([^\"']+)[\"']/)\n\n            if (nickMatch) {\n                const username = nickMatch[1]\n                const avatar = avatarMatch ? avatarMatch[1] : ''\n                await chrome.storage.local.set({\n                    wechat_user: {\n                        loggedIn: true,\n                        username,\n                        avatar,\n                        cachedAt: Date.now()\n                    }\n                })\n                return { loggedIn: true, username, avatar }\n            }\n        } catch (e) {\n            console.log(`[COSE] wechat fetch 失败:`, e.message)\n        }\n\n        return { loggedIn: false }\n    } catch (e) {\n        console.log(`[COSE] wechat 检测失败:`, e.message)\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/weibo.js",
    "content": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Weibo platform detection logic\n * Strategy:\n * 1. Check SUBP/ALF cookies on card.weibo.com\n * 2. Fetch editor page HTML and extract nick/avatar via regex\n */\nexport async function detectWeiboUser() {\n    const platformId = 'weibo'\n    try {\n        // 检查 card.weibo.com 的 SUBP cookie\n        const subpCookie = await chrome.cookies.get({\n            url: 'https://card.weibo.com',\n            name: 'SUBP'\n        })\n\n        // 也检查 ALF cookie\n        const alfCookie = await chrome.cookies.get({\n            url: 'https://card.weibo.com',\n            name: 'ALF'\n        })\n\n        if (!subpCookie && !alfCookie) {\n            console.log(`[COSE] weibo 未找到登录 cookie，未登录`)\n            return { loggedIn: false }\n        }\n\n        // 有 cookie，通过 fetch HTML 获取用户信息\n        let username = ''\n        let avatar = ''\n\n        try {\n            // 获取所有相关 cookies 并手动添加到请求\n            const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' })\n            const cardCookies = await chrome.cookies.getAll({ domain: 'card.weibo.com' })\n            const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' })\n            const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies]\n            const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ')\n\n            const response = await fetch('https://card.weibo.com/article/v5/editor', {\n                method: 'GET',\n                headers: {\n                    'Cookie': cookieString,\n                },\n                credentials: 'include',\n            })\n            const html = await response.text()\n\n            // 从 HTML 中提取用户名\n            const nickMatch = html.match(/\"nick\"\\s*:\\s*\"([^\"]+)\"/)\n            if (nickMatch) {\n                username = nickMatch[1]\n            } else {\n                // 深度查找 nick\n                const altNickMatch = html.match(/\\\\\"nick\\\\\"\\s*:\\s*\\\\\"([^\\\\\"]+)\\\\\"/)\n                if (altNickMatch) {\n                    username = altNickMatch[1]\n                }\n            }\n\n            // 从 HTML 中提取头像\n            const avatarMatch = html.match(/\"avatar_large\"\\s*:\\s*\"([^\"]+)\"/)\n            if (avatarMatch) {\n                avatar = avatarMatch[1].replace(/\\\\/g, '')\n            } else {\n                const altAvatarMatch = html.match(/\\\\\"avatar_large\\\\\"\\s*:\\s*\\\\\"([^\\\\\"]+)\\\\\"/)\n                if (altAvatarMatch) {\n                    let rawAvatar = altAvatarMatch[1].replace(/\\\\\\\\\\\\\\//g, '/')\n                    if (rawAvatar.includes('sinaimg.cn')) {\n                        avatar = rawAvatar.split('?')[0]\n                    } else {\n                        avatar = rawAvatar\n                    }\n                }\n            }\n\n            console.log(`[COSE] weibo 用户信息: ${username}`)\n        } catch (e) {\n            console.log(`[COSE] weibo 获取用户详情失败:`, e.message)\n        }\n\n        if (!username) {\n            return { loggedIn: false }\n        }\n\n        // Convert sinaimg.cn avatar to base64 data URL to bypass CORS/ORB\n        if (avatar && avatar.includes('sinaimg.cn')) {\n            avatar = await convertAvatarToBase64(avatar, 'https://weibo.com/')\n        }\n\n        return { loggedIn: true, username, avatar }\n    } catch (e) {\n        console.log(`[COSE] weibo 检测失败:`, e.message)\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/platforms/xiaohongshu.js",
    "content": "/**\n * Xiaohongshu (Little Red Book) platform detection logic\n * Strategy:\n * 1. Check `a1` cookie on creator.xiaohongshu.com as login indicator\n * 2. Best-effort: fetch user info via offscreen document or open tab\n * 3. Fall back to chrome.storage.local cache for user details\n */\nexport async function detectXiaohongshuUser() {\n    try {\n        // 1. Check a1 cookie as login indicator (MV3 service worker compatible)\n        const a1Cookie = await chrome.cookies.get({ url: 'https://creator.xiaohongshu.com', name: 'a1' })\n        if (!a1Cookie || !a1Cookie.value) {\n            return { loggedIn: false }\n        }\n\n        // Logged in — now try to get user details\n\n        // 2a. Try offscreen fetch (document context, cookies sent automatically)\n        try {\n            const offscreenDetect = globalThis.__coseDetectXiaohongshu\n            if (offscreenDetect) {\n                const offResult = await offscreenDetect()\n                if (offResult && offResult.loggedIn) {\n                    // Update cache\n                    const userInfo = { ...offResult, cachedAt: Date.now() }\n                    await chrome.storage.local.set({ xiaohongshu_user: userInfo })\n                    return { loggedIn: true, username: offResult.username || '', avatar: offResult.avatar || '' }\n                }\n            }\n        } catch (e) {\n            console.log('[COSE] xiaohongshu offscreen detection failed:', e.message)\n        }\n\n        // 2b. Try open tab injection\n        try {\n            const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' })\n            if (tabs.length > 0) {\n                const results = await chrome.scripting.executeScript({\n                    target: { tabId: tabs[0].id },\n                    func: async () => {\n                        try {\n                            const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', {\n                                method: 'GET',\n                                credentials: 'include',\n                                headers: { 'Accept': 'application/json' }\n                            })\n                            if (!response.ok) return null\n                            const data = await response.json()\n                            if (data?.success === true && data?.code === 0 && data?.data?.userId) {\n                                return {\n                                    loggedIn: true,\n                                    username: data.data.userName || data.data.redId || '',\n                                    avatar: data.data.userAvatar || '',\n                                    userId: data.data.userId\n                                }\n                            }\n                            return null\n                        } catch (e) { return null }\n                    }\n                })\n                const result = results?.[0]?.result\n                if (result && result.loggedIn) {\n                    const userInfo = { ...result, cachedAt: Date.now() }\n                    await chrome.storage.local.set({ xiaohongshu_user: userInfo })\n                    return { loggedIn: true, username: userInfo.username || '', avatar: userInfo.avatar || '' }\n                }\n            }\n        } catch (e) {\n            console.log('[COSE] xiaohongshu tab detection failed:', e.message)\n        }\n\n        // 2c. Fall back to cache for user details\n        const stored = await chrome.storage.local.get('xiaohongshu_user')\n        const cachedUser = stored.xiaohongshu_user\n        if (cachedUser && cachedUser.username) {\n            const cacheAge = Date.now() - (cachedUser.cachedAt || 0)\n            const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days\n            if (cacheAge < maxAge) {\n                console.log('[COSE] xiaohongshu 从缓存读取:', cachedUser.username)\n                return { loggedIn: true, username: cachedUser.username || '', avatar: cachedUser.avatar || '' }\n            }\n        }\n\n        // Cookie exists but couldn't get user details — still logged in\n        return { loggedIn: true, username: '', avatar: '' }\n    } catch (e) {\n        console.log('[COSE] xiaohongshu 检测失败:', e.message)\n        return { loggedIn: false }\n    }\n}\n"
  },
  {
    "path": "packages/detection/src/utils.js",
    "content": "/**\n * Convert an avatar URL to a base64 data URL to bypass CORS/ORB blocking.\n * The service worker can fetch with a custom Referer header.\n * @param {string} avatarUrl - The original avatar URL\n * @param {string} referer - The Referer header to use for the fetch\n * @returns {Promise<string>} - base64 data URL, or original URL if conversion fails\n */\nexport async function convertAvatarToBase64(avatarUrl, referer) {\n    try {\n        const imgResp = await fetch(avatarUrl, {\n            headers: { 'Referer': referer }\n        })\n        if (imgResp.ok) {\n            const blob = await imgResp.blob()\n            const buffer = await blob.arrayBuffer()\n            const bytes = new Uint8Array(buffer)\n            let binary = ''\n            for (let i = 0; i < bytes.length; i++) {\n                binary += String.fromCharCode(bytes[i])\n            }\n            const base64 = btoa(binary)\n            const mime = blob.type || 'image/jpeg'\n            return `data:${mime};base64,${base64}`\n        }\n    } catch (e) {\n        console.log('[COSE] avatar base64 conversion failed:', e.message)\n    }\n    return avatarUrl\n}\n\nexport async function logToStorage(msg, data = null) {\n    if (data) {\n        console.log(msg, data)\n    } else {\n        console.log(msg)\n    }\n}\n\n// 通过 Cookie 检测登录状态\nexport async function checkLoginByCookie(platformId, config) {\n    try {\n        // 直接按名称查找 cookie\n        const cookieMap = {}\n        if (config.cookieNames) {\n            for (const name of config.cookieNames) {\n                const cookie = await chrome.cookies.get({\n                    url: config.cookieUrl || `https://${config.cookieDomain}`,\n                    name: name\n                })\n                if (cookie) {\n                    cookieMap[name] = cookie.value\n                }\n            }\n        }\n\n        console.log(`[COSE] ${platformId} 找到的cookies:`, Object.keys(cookieMap))\n\n        const hasLoginCookie = config.cookieNames && config.cookieNames.some(name => cookieMap[name])\n\n        if (!hasLoginCookie) {\n            console.log(`[COSE] ${platformId} 未找到登录 cookie`)\n            return { loggedIn: false }\n        }\n\n        // 自定义 cookie 值检测逻辑（如 InfoQ）\n        if (config.customCheck && config.checkCookieValue) {\n            console.log(`[COSE] ${platformId} 使用自定义 cookie 检测`)\n            const result = config.checkCookieValue(cookieMap)\n            console.log(`[COSE] ${platformId} 自定义检测结果:`, result)\n            return result\n        }\n\n        let username = ''\n        let avatar = ''\n\n        // 如果配置了从 cookie 获取用户名\n        if (config.getUsernameFromCookie && config.usernameCookie) {\n            username = decodeURIComponent(cookieMap[config.usernameCookie] || '')\n        }\n\n\n        // 使用平台配置的 fetchAvatar 回调获取头像\n        if (config.fetchAvatar && typeof config.fetchAvatar === 'function') {\n            try {\n                const fetchedAvatar = await config.fetchAvatar(cookieMap)\n                if (fetchedAvatar) {\n                    avatar = fetchedAvatar\n                    console.log(`[COSE] ${platformId} 找到头像:`, avatar)\n                }\n            } catch (e) {\n                console.log(`[COSE] ${platformId} 获取头像失败:`, e.message)\n            }\n        }\n\n        return { loggedIn: true, username, avatar }\n\n    } catch (e) {\n        console.log(`[COSE] ${platformId} Cookie 检测失败:`, e.message)\n        return { loggedIn: false, error: e.message }\n    }\n}\n\n// 通用 API 检测逻辑\nexport async function detectByApi(platformId, config) {\n    try {\n        console.log(`[COSE] ${platformId} 开始 API 检测: ${config.api}`)\n        const controller = new AbortController()\n        const timeoutId = setTimeout(() => controller.abort(), 8000)\n\n        // Collect cookies via chrome.cookies.getAll for MV3 service worker compatibility\n        let cookieStr = ''\n        try {\n            const apiUrl = new URL(config.api)\n            const domain = apiUrl.hostname.split('.').slice(-2).join('.')\n            const domainCookies = await chrome.cookies.getAll({ domain: `.${domain}` })\n            const urlCookies = await chrome.cookies.getAll({ url: config.api })\n            const allCookies = [...domainCookies, ...urlCookies]\n            const seen = new Set()\n            const uniqueCookies = allCookies.filter(c => {\n                const key = `${c.name}=${c.value}`\n                if (seen.has(key)) return false\n                seen.add(key)\n                return true\n            })\n            cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ')\n        } catch (e) {\n            console.log(`[COSE] ${platformId} cookie 收集失败:`, e.message)\n        }\n\n        const apiUrl = new URL(config.api)\n        const origin = apiUrl.origin\n\n        const fetchOptions = {\n            method: config.method || 'GET',\n            headers: {\n                'Accept': config.isHtml ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' : 'application/json',\n                'Cache-Control': 'no-cache',\n                ...(cookieStr ? { 'Cookie': cookieStr } : {}),\n                'Origin': origin,\n                'Referer': origin + '/',\n                ...(config.headers || {})\n            },\n            signal: controller.signal,\n        }\n\n        if (config.body) {\n            fetchOptions.body = config.body\n        }\n\n        const response = await fetch(config.api, fetchOptions)\n        clearTimeout(timeoutId)\n        console.log(`[COSE] ${platformId} API 响应状态: ${response.status}`)\n\n        let data = null\n        if (config.isHtml) {\n            // 明确指定为 HTML 响应时，使用 text() 解析\n            try { data = await response.text() } catch (e) { data = '' }\n        } else {\n            // 其他情况尝试 JSON 解析\n            try { data = await response.json() } catch (e) { data = null }\n        }\n        // console.log(`[COSE] ${platformId} API 数据:`, data)\n\n        const loggedIn = config.checkLogin(data)\n        // console.log(`[COSE] ${platformId} checkLogin 结果: ${loggedIn}`)\n        if (loggedIn && config.getUserInfo) {\n            const userInfo = config.getUserInfo(data)\n            console.log(`[COSE] ${platformId} 用户信息:`, userInfo)\n            return { loggedIn: true, ...userInfo }\n        }\n        return { loggedIn: !!loggedIn }\n    } catch (error) {\n        console.log(`[COSE] ${platformId} API 检测失败: ${error.message}`)\n        return { loggedIn: false, error: error.message }\n    }\n}\n"
  },
  {
    "path": "pnpm-workspace.yaml",
    "content": "packages:\n  - 'apps/*'\n  - 'packages/*'\n"
  }
]