Showing preview only (430K chars total). Download the full file or copy to clipboard to get everything.
Repository: doocs/cose
Branch: main
Commit: 4e32015064b3
Files: 90
Total size: 372.0 KB
Directory structure:
gitextract_uy5ndwh6/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── 01-bug-report.yml
│ │ ├── 02-feature-request.yml
│ │ └── 03-platform-request.yml
│ └── PULL_REQUEST_TEMPLATE.md
├── .gitignore
├── PRIVACY.md
├── README.md
├── apps/
│ └── extension/
│ ├── manifest.json
│ ├── package.json
│ ├── scripts/
│ │ ├── cli.ts
│ │ ├── convert-icons.mjs
│ │ └── reload-extension.mjs
│ ├── src/
│ │ ├── background.js
│ │ ├── content.js
│ │ ├── inject.js
│ │ ├── offscreen.html
│ │ ├── offscreen.js
│ │ ├── popup.html
│ │ └── popup.js
│ ├── tsconfig.json
│ └── vite.config.js
├── package.json
├── packages/
│ ├── core/
│ │ ├── index.js
│ │ ├── package.json
│ │ └── src/
│ │ ├── platforms/
│ │ │ ├── alipayopen.js
│ │ │ ├── aliyun.js
│ │ │ ├── baijiahao.js
│ │ │ ├── bilibili.js
│ │ │ ├── cnblogs.js
│ │ │ ├── common.js
│ │ │ ├── csdn.js
│ │ │ ├── cto51.js
│ │ │ ├── douban.js
│ │ │ ├── douyin.js
│ │ │ ├── elecfans.js
│ │ │ ├── huaweicloud.js
│ │ │ ├── huaweidev.js
│ │ │ ├── index.js
│ │ │ ├── infoq.js
│ │ │ ├── jianshu.js
│ │ │ ├── juejin.js
│ │ │ ├── medium.js
│ │ │ ├── modelscope.js
│ │ │ ├── oschina.js
│ │ │ ├── qianfan.js
│ │ │ ├── segmentfault.js
│ │ │ ├── sohu.js
│ │ │ ├── sspai.js
│ │ │ ├── tencentcloud.js
│ │ │ ├── toutiao.js
│ │ │ ├── twitter.js
│ │ │ ├── volcengine.js
│ │ │ ├── wangyihao.js
│ │ │ ├── wechat.js
│ │ │ ├── weibo.js
│ │ │ ├── xiaohongshu.js
│ │ │ └── zhihu.js
│ │ └── utils.js
│ └── detection/
│ ├── index.js
│ ├── package.json
│ └── src/
│ ├── configs.js
│ ├── detect.js
│ ├── platforms/
│ │ ├── alipay.js
│ │ ├── aliyun.js
│ │ ├── bilibili.js
│ │ ├── cnblogs.js
│ │ ├── csdn.js
│ │ ├── cto51.js
│ │ ├── douban.js
│ │ ├── elecfans.js
│ │ ├── huaweicloud.js
│ │ ├── huaweidev.js
│ │ ├── infoq.js
│ │ ├── jianshu.js
│ │ ├── medium.js
│ │ ├── modelscope.js
│ │ ├── oschina.js
│ │ ├── qianfan.js
│ │ ├── segmentfault.js
│ │ ├── sohu.js
│ │ ├── sspai.js
│ │ ├── tencentcloud.js
│ │ ├── twitter.js
│ │ ├── volcengine.js
│ │ ├── wangyihao.js
│ │ ├── wechat.js
│ │ ├── weibo.js
│ │ └── xiaohongshu.js
│ └── utils.js
└── pnpm-workspace.yaml
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/01-bug-report.yml
================================================
name: Bug Report / 问题报告
description: Report a bug or issue with the extension / 报告扩展的 bug 或问题
title: "[Bug]: "
labels: ["bug", "triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report this bug! Please fill out the information below to help us resolve the issue.
感谢您花时间报告此问题!请填写以下信息以帮助我们解决问题。
- type: textarea
id: description
attributes:
label: Description / 问题描述
description: A clear and concise description of the bug / 清晰简洁地描述问题
placeholder: Describe the bug you encountered / 描述您遇到的问题
validations:
required: true
- type: textarea
id: reproduction-steps
attributes:
label: Steps to Reproduce / 复现步骤
description: Detailed steps to reproduce the behavior / 详细的复现步骤
placeholder: |
1. Go to '...' / 前往 '...'
2. Click on '...' / 点击 '...'
3. Scroll down to '...' / 滚动到 '...'
4. See error / 看到错误
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior / 预期行为
description: What you expected to happen / 您期望发生什么
placeholder: Describe the expected behavior / 描述预期行为
validations:
required: true
- type: textarea
id: suggested-implementation
attributes:
label: Suggested Implementation / 实现建议
description: Any suggestions on how to fix this issue (optional) / 对如何修复此问题的建议
placeholder: Describe your suggested implementation / 描述您建议的实现方式
validations:
required: false
- type: input
id: platform
attributes:
label: Platform / 平台
description: Which platform is affected? / 哪个平台受到影响?
placeholder: e.g., CSDN, Zhihu, Juejin, Toutiao / 如:CSDN、知乎、掘金、头条
validations:
required: true
- type: dropdown
id: browser
attributes:
label: Browser / 浏览器
description: Which browser are you using? / 您使用的是哪个浏览器?
options:
- Chrome
- Firefox
- Edge
- Safari
- Other / 其他
validations:
required: true
- type: input
id: extension-version
attributes:
label: Extension Version / 扩展版本
description: What version of the extension are you running? / 您运行的扩展版本是多少?
placeholder: e.g., 1.2.0 / 如:1.2.0
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment Details / 环境信息
description: Any additional environment information / 其他环境信息
placeholder: |
- OS / 操作系统: [e.g., Windows 10, macOS 13.0]
- Browser Version / 浏览器版本: [e.g., Chrome 120.0]
- Additional context / 其他信息
validations:
required: false
- type: textarea
id: screenshots
attributes:
label: Screenshots / 截图
description: If applicable, add screenshots to help explain your problem / 如有需要,请添加截图帮助说明问题
placeholder: Drag and drop images here / 拖放图片到这里
validations:
required: false
- type: textarea
id: additional-context
attributes:
label: Additional Context / 补充信息
description: Any other context about the problem / 关于此问题的其他补充信息
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/02-feature-request.yml
================================================
name: Feature Request / 功能请求
description: Suggest a new feature or enhancement / 建议新功能或改进
title: "[Feature]: "
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Thanks for your interest in improving this project! Please describe your feature request below.
感谢您对改进本项目的兴趣!请在下方描述您的功能请求。
- type: textarea
id: problem-description
attributes:
label: Problem Description / 问题描述
description: Is your feature request related to a problem? Describe what the problem is / 您的功能请求是否与某个问题相关?请描述问题
placeholder: "I'm frustrated when... / 我感到困扰的是..."
validations:
required: true
- type: textarea
id: proposed-solution
attributes:
label: Proposed Solution / 建议的解决方案
description: Describe the solution you'd like to see / 描述您希望看到的解决方案
placeholder: A clear and concise description of what you want to happen / 清晰简洁地描述您希望实现的功能
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered / 考虑过的替代方案
description: Describe any alternative solutions or features you've considered / 描述您考虑过的其他解决方案或功能
placeholder: What other approaches could solve this problem? / 还有哪些方法可以解决这个问题?
validations:
required: false
- type: dropdown
id: feature-type
attributes:
label: Feature Type / 功能类型
description: What type of feature is this? / 这是什么类型的功能?
options:
- New platform support / 新平台支持
- UI/UX improvement / 界面/体验改进
- Performance enhancement / 性能优化
- Content formatting / 内容格式化
- Authentication/Login / 认证/登录
- Other / 其他
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context / 补充信息
description: Add any other context, screenshots, or examples about the feature request / 添加其他相关信息、截图或示例
placeholder: Any mockups, examples, or additional details / 任何原型图、示例或其他细节
validations:
required: false
- type: checkboxes
id: willingness
attributes:
label: Implementation / 实现意愿
description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能?
options:
- label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR
required: false
- label: I'd prefer to wait for community implementation / 我希望等待社区的实现
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/03-platform-request.yml
================================================
name: Platform Request / 平台支持
description: Request support for a new platform / 请求支持新平台
title: "[Platform]: "
labels: ["enhancement", "new platform"]
body:
- type: input
id: platform-name
attributes:
label: Platform Name / 平台名称
description: Name of the platform you want supported / 您希望支持的平台名称
placeholder: e.g., 简书、博客园 / e.g., Jianshu, cnblogs
validations:
required: true
- type: input
id: platform-url
attributes:
label: Platform URL / 平台网址
description: Main URL of the platform / 平台的主要网址
placeholder: e.g., https://www.jianshu.com
validations:
required: true
- type: textarea
id: reason
attributes:
label: Why this platform? / 为什么需要支持此平台?
description: Brief reason for supporting this platform / 简要说明支持此平台的原因
placeholder: e.g., Popular blogging platform in China / 如:国内热门博客平台
validations:
required: false
- type: checkboxes
id: willingness
attributes:
label: Implementation / 实现意愿
description: Would you be willing to help implement this feature? / 您是否愿意帮助实现此功能?
options:
- label: I'm willing to submit a PR for this feature / 我愿意为此功能提交 PR
required: false
- label: I'd prefer to wait for community implementation / 我希望等待社区的实现
required: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Summary / 概述
<!-- Provide a brief description of the changes in this PR -->
<!-- 简要描述此 PR 中的更改 -->
## Related Issue / 关联 Issue
<!-- Link to the related issue(s) -->
<!-- 链接到相关的 issue -->
Closes #
## Type of Change / 更改类型
<!-- Mark the relevant option with an "x" -->
<!-- 用 "x" 标记相关选项 -->
- [ ] Bug fix / 修复 Bug (non-breaking change that fixes an issue / 修复问题的非破坏性更改)
- [ ] New feature / 新功能 (non-breaking change that adds functionality / 添加功能的非破坏性更改)
- [ ] Breaking change / 破坏性更改 (fix or feature that would cause existing functionality to not work as expected / 会导致现有功能无法正常工作的修复或功能)
- [ ] Documentation update / 文档更新
- [ ] Performance improvement / 性能优化
- [ ] Code refactoring / 代码重构
- [ ] Other / 其他 (please describe / 请描述):
## Changes Made / 更改内容
<!-- Describe the specific changes made in this PR -->
<!-- 描述此 PR 中的具体更改 -->
-
-
-
## Implementation Details / 实现细节
<!-- Provide technical details about your implementation -->
<!-- 提供实现的技术细节 -->
**Key Changes / 主要更改:**
-
**Technical Notes / 技术说明:**
-
## Testing / 测试
<!-- Describe the testing you performed -->
<!-- 描述您执行的测试 -->
### Testing Checklist / 测试清单
- [ ] I have tested this code locally / 我已在本地测试此代码
- [ ] All existing tests pass / 所有现有测试通过
- [ ] I have added tests for new functionality / 我已为新功能添加测试
- [ ] I have tested on the affected platform(s) / 我已在受影响的平台上测试
- [ ] I have verified the changes work in the target browser(s) / 我已验证更改在目标浏览器中有效
### Manual Testing Steps / 手动测试步骤
1.
2.
3.
## Screenshots/Videos / 截图/视频
<!-- If applicable, add screenshots or videos to demonstrate the changes -->
<!-- 如适用,请添加截图或视频来展示更改 -->
## Reviewer Checklist / 审阅者清单
<!-- For reviewers to verify before merging -->
<!-- 供审阅者在合并前验证 -->
- [ ] Code follows the project's style guidelines / 代码遵循项目的风格指南
- [ ] Changes are well-documented / 更改有良好的文档说明
- [ ] No breaking changes or clearly documented if present / 无破坏性更改,或已清楚记录
- [ ] Security implications have been considered / 已考虑安全影响
- [ ] Performance impact has been evaluated / 已评估性能影响
- [ ] All discussions have been resolved / 所有讨论已解决
## Additional Notes / 补充说明
<!-- Any additional information that reviewers should know -->
<!-- 审阅者需要了解的其他信息 -->
================================================
FILE: .gitignore
================================================
# misc
.DS_Store
*.zip
.vscode
.idea
.cursor
.fleet
.zed
.windsurf
.kiro
dist/
node_modules/
================================================
FILE: PRIVACY.md
================================================
# Privacy Policy for COSE - 多平台文章同步
**Last Updated: December 13, 2025**
## Overview
COSE (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.
## Data Collection
**We do not collect any personal data.**
COSE operates entirely locally within your browser. The extension:
- Does **NOT** collect personally identifiable information
- Does **NOT** collect health, financial, or authentication information
- Does **NOT** track your browsing history or web activity
- Does **NOT** send any data to external servers
- Does **NOT** use analytics or tracking services
## Data Usage
All data processed by COSE remains on your local device:
- **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.
- **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.
- **User Preferences**: COSE does not persist user preferences or settings.
## Permissions Explained
| Permission | Purpose |
|------------|---------|
| `tabGroups` | Organize sync tabs into groups |
| `activeTab` | Temporarily access the current tab when you initiate a sync |
| `scripting` | Fill article content into platform editors |
| `cookies` | Check platform login status |
| `debugger` | Simulate paste events for WeChat editor |
| `clipboardRead` | Read formatted content (HTML) from the clipboard for syncing |
| `clipboardWrite` | Write content to the clipboard when needed for syncing |
## Third-Party Services
COSE interacts with the following third-party publishing platforms only when you explicitly initiate a sync:
- CSDN (csdn.net)
- Juejin (juejin.cn)
- WeChat Official Account (mp.weixin.qq.com)
- And other supported platforms
These interactions are solely for the purpose of publishing your content. We have no control over the privacy practices of these platforms.
## Data Security
Since 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.
## Children's Privacy
COSE is not directed at children under 13 years of age, and we do not knowingly collect information from children.
## Changes to This Policy
We may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated revision date.
## Contact
If you have questions about this Privacy Policy, please open an issue at:
https://github.com/doocs/cose/issues
## Open Source
COSE is open source. You can review the complete source code at:
https://github.com/doocs/cose
================================================
FILE: README.md
================================================
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/headerDark.svg" />
<img src="assets/headerLight.svg" alt="COSE" />
</picture>
_**C**reate **O**nce **S**ync **E**verywhere_
[](LICENSE)
[](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk)
[](https://www.youtube.com/watch?v=KTskiA8Xaj4)
[](https://www.bilibili.com/video/BV1ZxqnB1E2C/)
</div>
配合 [doocs/md](https://github.com/doocs/md) Markdown 编辑器使用的浏览器扩展,支持一键将文章同步到多个内容平台。
> 本插件完全本地运行,不收集、不存储任何用户信息。**如需添加更多平台或改善同步准确度,欢迎提 [Issue](https://github.com/doocs/cose/issues) 或 [PR](https://github.com/doocs/cose/pulls)**。
## 使用方法
> 点击观看视频:[](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [](https://www.youtube.com/watch?v=KTskiA8Xaj4)
1. 先点击安装扩展 [](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk) 然后打开 [md.doocs.org](https://md.doocs.org) 或本地开发环境
2. 编辑 Markdown 内容
3. 点击顶部的 **发布** 按钮
4. 在弹出的对话框中选择要同步的平台
5. 点击 **确定** 开始同步
## 特性
- 编辑一次,同步到多个平台
- 自动检测各平台登录状态
- 同步的标签页自动归入分组,便于管理
- 微信公众号同步时完整保留渲染样式并自动保存为草稿
## 已支持的平台
> 更多想要添加的平台欢迎提 [Issue](https://github.com/doocs/cose/issues) !
>
> <details>
> <summary>已支持平台速查表(点击展开)</summary>
>
> | 字母 | 平台 |
> |:---:|:---|
> | A | 阿里云社区 |
> | B | B站专栏、百度云千帆、百家号、博客园 |
> | C | CSDN |
> | D | 豆瓣、电子发烧友、抖音文章 |
> | H | 华为开发者文章、华为云博客、火山引擎社区 |
> | I | InfoQ |
> | J | 简书、掘金、今日头条 |
> | K | 开源中国 |
> | M | Medium、ModelScope 魔搭社区 |
> | S | 少数派、搜狐号、思否 |
> | T | 腾讯云 |
> | W | 网易号、微博文章、微信公众号 |
> | X | 小红书长文、X(Formerly Twitter) Articles |
> | Z | 支付宝开放平台、知乎 |
> | 5 | 51CTO |
>
> </details>
### 媒体平台
<p align="center">
<img src="https://cdn.simpleicons.org/wechat" alt="微信公众号" width="60" />
<img src="https://api.iconify.design/icon-park-solid/jinritoutiao.svg?color=%23ED1C24" alt="今日头条" width="60" />
<img src="https://cdn.simpleicons.org/zhihu" alt="知乎" width="60" />
<img src="https://cdn.simpleicons.org/tiktok" alt="抖音文章" width="60"/>
<img src="https://cdn.simpleicons.org/xiaohongshu" alt="小红书" width="60" />
<img src="https://cdn.simpleicons.org/baidu" alt="百家号" width="60" />
<img src="https://cdn.simpleicons.org/neteasecloudmusic" alt="网易号" width="60" />
<img src="https://favicon.im/sohu.com?larger=true" alt="搜狐号" width="60" />
<img src="https://cdn.simpleicons.org/sinaweibo" alt="微博文章" width="60" />
<img src="https://cdn.simpleicons.org/bilibili" alt="B站专栏" width="60" />
<img src="https://cdn.simpleicons.org/douban" alt="豆瓣" width="55" />
<img src="https://favicon.im/sspai.com?larger=true" alt="少数派" width="60" />
<img src="https://cdn.simpleicons.org/x" alt="X(Formerly Twitter) Articles" width="50" />
</p>
### 博客平台
<p align="center">
<img src="https://cdn.simpleicons.org/csdn/FC5531" alt="CSDN" width="80" />
<img src="https://favicon.im/cnblogs.com?larger=true" alt="博客园" width="60" />
<img src="https://cdn.simpleicons.org/juejin/1E80FF" alt="掘金" width="60" />
<img src="https://favicon.im/medium.com?larger=true" alt="Medium" width="60" />
<img src="https://cdn.brandfetch.io/id5v32DsU5/w/400/h/400/theme/dark/icon.png?c=1bxid64Mup7aczewSAYMX&t=1767149702768" alt="思否" width="60" />
<img src="https://www.google.com/s2/favicons?domain=infoq.cn&sz=128" alt="InfoQ" width="60" />
<img src="https://favicon.im/jianshu.com?larger=true" alt="简书" width="60" />
<img src="https://static.oschina.net/new-osc/img/logo_osc.svg" alt="开源中国" width="120" />
<img src="https://cdn.brandfetch.io/id4vYQbdaC/w/168/h/62/theme/dark/logo.png?c=1bxid64Mup7aczewSAYMX&t=1743144928777" alt="51CTO" width="80" />
</p>
### 云平台及其开发者社区
<br/>
<p align="center">
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/tencentcloud-color.png" alt="腾讯云" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/tencentcloud-text.png" alt="腾讯云" width="70" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/alibabacloud-color.png" alt="阿里云社区" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/alibabacloud-text.png" alt="阿里云社区" width="140" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/huaweicloud-color.png" alt="华为云博客" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/huaweicloud-text-cn.png" alt="华为云博客" width="70" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/huawei-color.png" alt="华为开发者文章" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/huawei-text.png" alt="华为开发者文章" width="100" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/baiducloud-color.png" alt="百度云千帆" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/baiducloud-text.png" alt="百度云千帆" width="110" />
<br/>
<img src="https://cdn.simpleicons.org/alipay/1677FF" alt="支付宝开放平台" width="30" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/modelscope-color.png" alt="魔搭社区" width="36" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/modelscope-text.png" alt="魔搭社区" width="160" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/dark/volcengine-color.png" alt="火山引擎" width="36" />
<img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png/light/volcengine-text.png" alt="火山引擎" width="90" />
<img src="https://www.elecfans.com/static/main/img/elecfans-logo.jpg" alt="电子发烧友" width="35" />
</p>
## 开发者模式测试
1. 克隆或下载本项目
2. 打开 Chrome 浏览器,进入 `chrome://extensions/`
3. 开启右上角的 **开发者模式**
4. 点击 **加载已解压的扩展程序**
5. 选择 `cose` 目录
================================================
FILE: apps/extension/manifest.json
================================================
{
"manifest_version": 3,
"name": "COSE - 多平台文章同步",
"description": "Create Once, Sync Everywhere. 一键将文章同步到多个平台",
"permissions": [
"tabGroups",
"activeTab",
"scripting",
"cookies",
"debugger",
"clipboardRead",
"storage",
"declarativeNetRequest",
"offscreen"
],
"host_permissions": [
"https://*.csdn.net/*",
"https://*.juejin.cn/*",
"https://*.jianshu.com/*",
"https://*.segmentfault.com/*",
"https://*.toutiao.com/*",
"https://*.douban.com/*",
"https://*.bilibili.com/*",
"https://*.weibo.com/*",
"https://*.sinaimg.cn/*",
"https://mp.weixin.qq.com/*",
"https://*.zhihu.com/*",
"https://*.sspai.com/*",
"https://cdnfile.sspai.com/*",
"https://*.xueqiu.com/*",
"https://*.eastmoney.com/*",
"https://*.wordpress.com/*",
"https://*.wordpress.org/*",
"https://md.doocs.org/*",
"https://*.cnblogs.com/*",
"https://*.oschina.net/*",
"https://*.51cto.com/*",
"https://*.infoq.cn/*",
"https://*.baijiahao.baidu.com/*",
"https://*.163.com/*",
"https://*.cloud.tencent.com/*",
"https://*.medium.com/*",
"https://*.sohu.com/*",
"https://*.aliyun.com/*",
"https://*.huaweicloud.com/*",
"https://*.huawei.com/*",
"https://*.hicloud.com/*",
"https://*.x.com/*",
"https://*.twitter.com/*",
"https://qianfan.cloud.baidu.com/*",
"https://*.alipay.com/*",
"https://*.modelscope.cn/*",
"https://*.alicdn.com/*",
"https://*.volcengine.com/*",
"https://*.byteacctimg.com/*",
"https://*.douyin.com/*",
"https://*.xiaohongshu.com/*",
"https://*.elecfans.com/*"
],
"action": {
"default_icon": {
"16": "icons/cose_16.png",
"48": "icons/cose_48.png",
"128": "icons/cose_128.png"
},
"default_title": "COSE",
"default_popup": "popup.html"
},
"background": {
"service_worker": "bundles/background.js",
"type": "module"
},
"content_scripts": [
{
"matches": [
"http://*/*",
"https://*/*"
],
"js": [
"bundles/content.js"
],
"run_at": "document_idle"
}
],
"icons": {
"16": "icons/cose_16.png",
"48": "icons/cose_48.png",
"128": "icons/cose_128.png"
},
"web_accessible_resources": [
{
"resources": [
"bundles/inject.js",
"bundles/platforms/*.js"
],
"matches": [
"<all_urls>"
]
}
]
}
================================================
FILE: apps/extension/package.json
================================================
{
"name": "cose-extension",
"version": "1.3.4",
"description": "Create Once, Sync Everywhere. 一键将文章同步到多个平台",
"type": "module",
"scripts": {
"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",
"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",
"dev:watch": "node scripts/reload-extension.mjs",
"build": "tsx scripts/cli.ts build",
"build:firefox": "tsx scripts/cli.ts build --target firefox",
"build:safari": "tsx scripts/cli.ts build --target safari",
"build:release": "tsx scripts/cli.ts build --release",
"watch": "tsx scripts/cli.ts build --watch",
"lint": "web-ext lint --source-dir ./dist"
},
"dependencies": {
"@cose/core": "workspace:*",
"@cose/detection": "workspace:*"
},
"devDependencies": {
"cac": "^6.7.14",
"execa": "^9.6.1",
"rimraf": "^5.0.5",
"tsx": "^4.21.0",
"vite": "^5.0.12",
"vite-plugin-static-copy": "^1.0.1",
"web-ext": "^7.11.0",
"ws": "^8.19.0"
}
}
================================================
FILE: apps/extension/scripts/cli.ts
================================================
import { cac } from 'cac'
import { execa } from 'execa'
import { build as viteBuild } from 'vite'
import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const dirname = fileURLToPath(new URL('./', import.meta.url))
const rootDir = path.join(dirname, '..')
// Read package.json for version
const packageJsonPath = path.join(rootDir, 'package.json')
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'))
// Firefox background uses scripts array, Chrome uses service_worker
interface FirefoxBackgroundOptions {
scripts: string[]
type: 'module'
}
interface ChromeBackgroundOptions {
service_worker: string
type: 'module'
}
// Full manifest type for COSE extension
interface Manifest {
manifest_version: number
name: string
version: string
description: string
permissions: string[]
host_permissions: string[]
action: {
default_icon: Record<string, string>
default_title: string
}
background: FirefoxBackgroundOptions | ChromeBackgroundOptions
content_scripts: Array<{
matches: string[]
js: string[]
run_at: string
}>
icons: Record<string, string>
web_accessible_resources: Array<{
resources: string[]
matches: string[]
}>
browser_specific_settings?: {
gecko: {
id: string
}
}
}
const readManifest = async (manifestPath: string): Promise<Manifest | undefined> => {
try {
const fileContent = await fs.readFile(manifestPath, 'utf8')
const json = JSON.parse(fileContent) as Manifest
return json
} catch (error) {
console.error(error)
return undefined
}
}
interface BuildOptions {
watch: boolean
release: boolean
target: 'chromium' | 'firefox' | 'safari'
bundleId?: string
}
const buildWithVite = async (options: BuildOptions) => {
console.log('Building with Vite...')
await viteBuild({
root: rootDir,
mode: options.release ? 'production' : 'development',
build: {
minify: options.release,
watch: options.watch ? {} : null,
},
})
}
const copyResources = async () => {
console.log('Copying resources...')
interface CopyEntry {
from: string
to: string
}
const copyEntries: CopyEntry[] = [
{
from: path.join(rootDir, 'icons'),
to: path.join(rootDir, 'dist/icons'),
},
{
from: path.join(rootDir, 'assets'),
to: path.join(rootDir, 'dist/assets'),
},
{
// Copy platform scripts from core package
from: path.resolve(rootDir, '../../packages/core/src/platforms'),
to: path.join(rootDir, 'dist/bundles/platforms'),
},
]
for (const entry of copyEntries) {
try {
// Check if source exists
await fs.access(entry.from)
// Remove destination if exists
await fs.rm(entry.to, { recursive: true, force: true })
// Copy
await fs.cp(entry.from, entry.to, { recursive: true })
console.log(` ✓ Copied ${path.basename(entry.from)}`)
} catch (error) {
// Source doesn't exist, skip
console.log(` ⚠ Skipped ${path.basename(entry.from)} (not found)`)
}
}
}
const genManifest = async (options: BuildOptions) => {
console.log('Generating manifest.json...')
const manifest = await readManifest(path.join(rootDir, 'manifest.json'))
if (!manifest) {
throw new Error('manifest.json not found')
}
if (!manifest.background) {
throw new Error('manifest.background not found')
}
// Firefox-specific adjustments
if (options.target === 'firefox' && 'service_worker' in manifest.background) {
// Convert service_worker to scripts array for Firefox
manifest.background = {
scripts: [manifest.background.service_worker],
type: 'module',
}
// Add Firefox-specific settings
manifest.browser_specific_settings = {
gecko: {
id: 'cose@doocs.org',
},
}
console.log(' ✓ Converted to Firefox manifest format')
}
// Sync version from package.json
manifest.version = packageJson.version
// Write manifest to dist
const outputPath = path.join(rootDir, 'dist/manifest.json')
await fs.writeFile(
outputPath,
JSON.stringify(manifest, null, options.release ? undefined : 2)
)
console.log(` ✓ Generated manifest.json (version: ${manifest.version})`)
}
const buildSafariExtension = async (options: BuildOptions) => {
console.log('\nConverting to Safari extension...')
// Check if xcrun is available (macOS only)
try {
await execa('xcrun', ['--version'])
} catch {
throw new Error(
'xcrun not found. Safari extension conversion requires:\n' +
' 1. macOS\n' +
' 2. Xcode installed (with Command Line Tools)\n' +
' 3. Run: xcode-select --install'
)
}
const safariProjectDir = path.join(rootDir, 'safari-extension')
const bundleId = options.bundleId || 'org.doocs.cose'
// Remove existing Safari project
await fs.rm(safariProjectDir, { recursive: true, force: true })
console.log(` Bundle ID: ${bundleId}`)
console.log(` Project location: ${safariProjectDir}`)
try {
const result = await execa('xcrun', [
'safari-web-extension-converter',
path.join(rootDir, 'dist'),
'--project-location', safariProjectDir,
'--app-name', 'COSE',
'--bundle-identifier', bundleId,
'--swift',
'--no-prompt',
'--no-open'
])
console.log(result.stdout)
console.log('\n ✓ Safari extension project created!')
console.log(` ✓ Open in Xcode: open ${safariProjectDir}/COSE/COSE.xcodeproj`)
} catch (error: unknown) {
const err = error as { stderr?: string; message?: string }
console.error('Safari conversion failed:', err.stderr || err.message)
throw error
}
}
// CLI setup
const cli = cac('cose-build')
cli.help().version(packageJson.version)
cli
.command('build', 'Build the COSE browser extension')
.option('-w, --watch', 'Watch mode', { default: false })
.option('-r, --release', 'Build in release mode with optimizations', { default: false })
.option('--target <target>', 'Browser target: "chromium", "firefox", or "safari"', { default: 'chromium' })
.option('--bundle-id <bundleId>', 'Bundle ID for Safari (default: org.doocs.cose)')
.action(async (options: BuildOptions) => {
const validTargets = ['chromium', 'firefox', 'safari']
if (!validTargets.includes(options.target)) {
throw new Error(`Invalid target: ${options.target}. Use "chromium", "firefox", or "safari".`)
}
console.log(`\n=== COSE Build (target: ${options.target}, release: ${options.release}) ===\n`)
// Step 1: Build with Vite
await buildWithVite(options)
// Step 2: Copy resources
await copyResources()
// Step 3: Generate manifest
await genManifest(options)
// Step 4: Target-specific post-processing
if (options.target === 'firefox') {
console.log('\nRunning web-ext lint...')
try {
const result = await execa('pnpm', ['exec', 'web-ext', 'lint', '--source-dir', 'dist'])
console.log(result.stdout)
} catch (error) {
console.error('web-ext lint failed:', error)
}
} else if (options.target === 'safari') {
await buildSafariExtension(options)
}
console.log(`\n=== Build complete! ===\n`)
})
cli.parse(process.argv, { run: false })
await cli.runMatchedCommand()
================================================
FILE: apps/extension/scripts/convert-icons.mjs
================================================
#!/usr/bin/env node
/**
* 将 SVG 图标转换为 PNG
* 需要安装: npm install sharp
*/
import { readFileSync, writeFileSync, existsSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const iconsDir = join(__dirname, '..', 'icons')
// 简单的 SVG 转 PNG 占位符生成(纯绿色方块带 M 字母)
function createPlaceholderPng(size) {
// 创建一个简单的 PNG 占位符
// 这是一个最小的有效 PNG 文件(绿色方块)
const header = Buffer.from([
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature
])
console.log(`请使用以下命令安装 sharp 并重新运行,或手动转换 SVG:`)
console.log(` npm install sharp`)
console.log(` 或使用在线工具: https://svgtopng.com/`)
return null
}
async function main() {
const sizes = [16, 48, 128]
console.log('SVG 图标转换工具')
console.log('=================')
console.log('')
try {
// 尝试动态导入 sharp
const sharp = await import('sharp')
for (const size of sizes) {
const svgPath = join(iconsDir, `icon${size}.svg`)
const pngPath = join(iconsDir, `icon${size}.png`)
if (!existsSync(svgPath)) {
console.log(`跳过: ${svgPath} 不存在`)
continue
}
const svgBuffer = readFileSync(svgPath)
await sharp.default(svgBuffer)
.resize(size, size)
.png()
.toFile(pngPath)
console.log(`✓ 已转换: icon${size}.svg -> icon${size}.png`)
}
console.log('')
console.log('✔ 图标转换完成')
} catch (e) {
if (e.code === 'ERR_MODULE_NOT_FOUND') {
console.log('sharp 模块未安装,请运行:')
console.log(' npm install sharp')
console.log('')
console.log('或者手动转换 SVG 文件:')
sizes.forEach(size => {
console.log(` - icons/icon${size}.svg -> icons/icon${size}.png`)
})
console.log('')
console.log('在线工具: https://svgtopng.com/')
} else {
throw e
}
}
}
main().catch(console.error)
================================================
FILE: apps/extension/scripts/reload-extension.mjs
================================================
#!/usr/bin/env node
/**
* 监听 dist 目录变化,自动刷新 Chrome 扩展
*/
import { watch } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
import WebSocket from 'ws'
const __dirname = dirname(fileURLToPath(import.meta.url))
const distDir = join(__dirname, '..', 'dist')
const CDP_URL = 'http://127.0.0.1:9222'
let reloadTimeout = null
function ts() {
return new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' })
}
async function reloadExtension() {
try {
const res = await fetch(`${CDP_URL}/json/list`)
const pages = await res.json()
const extPage = pages.find(p => p.url.includes('chrome://extensions'))
if (!extPage) {
console.log(`[reload ${ts()}] 未找到 chrome://extensions 页面,请打开该页面`)
return
}
console.log(`[reload ${ts()}] 正在重新加载扩展...`)
const ws = new WebSocket(extPage.webSocketDebuggerUrl)
await new Promise((resolve) => {
ws.on('open', () => {
// 在 extensions 页面中找到 COSE 扩展并点击刷新按钮
ws.send(JSON.stringify({
id: 1,
method: 'Runtime.evaluate',
params: {
expression: `
(async () => {
// 获取 extensions-manager
const manager = document.querySelector('extensions-manager');
if (!manager) return 'no-manager';
// 获取 extensions-item-list
const itemList = manager.shadowRoot.querySelector('extensions-item-list');
if (!itemList) return 'no-item-list';
// 获取所有扩展卡片
const items = itemList.shadowRoot.querySelectorAll('extensions-item');
for (const item of items) {
const name = item.shadowRoot.querySelector('#name')?.textContent || '';
if (name.includes('COSE') || name.includes('多平台')) {
// 找到刷新按钮并点击
const reloadBtn = item.shadowRoot.querySelector('#dev-reload-button');
if (reloadBtn) {
reloadBtn.click();
return 'ok';
}
return 'no-reload-btn';
}
}
return 'not-found';
})()
`,
awaitPromise: true
}
}))
})
ws.on('message', (data) => {
const msg = JSON.parse(data.toString())
if (msg.id === 1) {
const result = msg.result?.result?.value
if (result === 'ok') {
console.log(`[reload ${ts()}] ✓ 扩展已重新加载`)
} else if (result === 'no-reload-btn') {
console.log(`[reload ${ts()}] ⚠ 未找到刷新按钮,请开启 Developer mode`)
} else if (result === 'not-found') {
console.log(`[reload ${ts()}] ⚠ 未找到 COSE 扩展`)
} else {
console.log(`[reload ${ts()}] ⚠ 刷新失败:`, result)
}
ws.close()
resolve()
}
})
ws.on('error', (e) => {
console.log(`[reload ${ts()}] 连接错误:`, e.message)
resolve()
})
setTimeout(() => { ws.close(); resolve() }, 3000)
})
} catch (e) {
console.log(`[reload ${ts()}] 失败:`, e.message)
}
}
function debounceReload() {
if (reloadTimeout) clearTimeout(reloadTimeout)
reloadTimeout = setTimeout(reloadExtension, 800)
}
console.log(`[reload ${ts()}] 监听 ${distDir} 目录变化...`)
console.log('[reload] 确保:')
console.log(' 1. Chrome 已用 --remote-debugging-port=9222 启动')
console.log(' 2. chrome://extensions 页面已打开')
console.log(' 3. Developer mode 已开启')
watch(distDir, { recursive: true }, (eventType, filename) => {
if (filename && !filename.includes('.DS_Store') && !filename.includes('_metadata')) {
console.log(`[reload ${ts()}] 检测到变化: ${filename}`)
debounceReload()
}
})
process.on('SIGINT', () => {
console.log(`\n[reload ${ts()}] 已停止`)
process.exit(0)
})
================================================
FILE: apps/extension/src/background.js
================================================
// 平台配置
import { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS } from '@cose/core/src/platforms/index.js'
import { qianfanIntercept } from '@cose/core/src/platforms/qianfan.js'
import { convertAvatarToBase64 } from '@cose/detection/src/utils.js'
// [DISABLED] import { fillAlipayOpenContent } from '@cose/core/src/platforms/alipayopen.js'
// ===== Offscreen helper =====
// Used for login detection in document context (cookies sent automatically)
async function ensureOffscreen() {
try {
const existing = await chrome.offscreen.hasDocument()
if (!existing) {
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_SCRAPING'],
justification: 'Fetch with credentials in document context for login detection',
})
// Wait for the offscreen document's scripts to load and register listeners.
// createDocument resolves when the document is created, but scripts may not
// have executed yet. Ping until the offscreen listener responds.
const ready = await _waitForOffscreenReady(3000)
if (!ready) {
console.warn('[COSE] ensureOffscreen: offscreen document did not become ready in time')
}
}
} catch (e) {
console.log('[COSE] ensureOffscreen error:', e.message)
}
}
/**
* Ping the offscreen document until it responds, confirming its listener is active.
*/
async function _waitForOffscreenReady(timeoutMs = 3000) {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const resp = await chrome.runtime.sendMessage({ type: 'OFFSCREEN_PING' })
if (resp && resp.pong) return true
} catch (e) {
// listener not ready yet
}
await new Promise(r => setTimeout(r, 50))
}
return false
}
/**
* Warm-up fetch via offscreen document.
* This triggers the browser's cookie restoration (SSO, session cookies)
* by making a fetch with credentials: 'include' in a document context.
*/
async function warmUpFetch(url) {
try {
const result = await sendOffscreenMessage({
type: 'OFFSCREEN_WARM_FETCH',
payload: { url },
})
console.log(`[COSE] Warm-up fetch ${url}: status=${result?.data?.status}`)
return result
} catch (e) {
console.log(`[COSE] Warm-up fetch failed for ${url}:`, e.message)
return null
}
}
/**
* API fetch via offscreen document.
* Makes a fetch with credentials: 'include' in a document context,
* so cookies are automatically attached (unlike service worker fetch which strips Cookie headers).
*/
async function offscreenApiFetch(url, options = {}) {
try {
const result = await sendOffscreenMessage({
type: 'OFFSCREEN_API_FETCH',
payload: { url, ...options },
})
console.log(`[COSE] Offscreen API fetch ${url}: status=${result?.data?.status}`)
return result?.data || null
} catch (e) {
console.log(`[COSE] Offscreen API fetch failed for ${url}:`, e.message)
return null
}
}
// Export for use by detection modules
globalThis.__coseWarmUpFetch = warmUpFetch
globalThis.__coseOffscreenApiFetch = offscreenApiFetch
/**
* Serialized message sender for offscreen document.
* chrome.runtime.sendMessage is broadcast-based; when multiple OFFSCREEN_*
* messages are sent concurrently, responses can get mixed up or lost.
* This queue ensures only one offscreen message is in-flight at a time.
* Includes a timeout to prevent the queue from getting stuck if the offscreen
* document is garbage-collected or fails to respond.
*/
let _offscreenQueue = Promise.resolve()
function sendOffscreenMessage(msg, timeoutMs = 15000) {
const p = _offscreenQueue.then(async () => {
await ensureOffscreen()
// Race the actual message against a timeout so the queue never gets stuck
return Promise.race([
chrome.runtime.sendMessage(msg),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(`Offscreen message timeout (${msg.type})`)), timeoutMs)
),
])
})
// Chain but don't let errors break the queue
_offscreenQueue = p.catch(() => {})
return p
}
/**
* Execute a fetch in the context of a target site's tab.
* This is needed for sites whose auth cookies are SameSite=Lax (default),
* which won't be sent from cross-site contexts like offscreen documents.
*
* Strategy: find an existing tab for the domain, or create a temporary one,
* then inject a script that makes the fetch with credentials: 'include'.
*/
async function tabContextFetch(siteUrl, apiUrl, options = {}) {
const { responseType = 'json', timeout = 15000 } = options
let createdTabId = null
try {
const urlObj = new URL(siteUrl)
const pattern = `*://*.${urlObj.hostname.replace(/^www\./, '')}/*`
console.log(`[COSE] tabContextFetch: looking for tabs matching ${pattern}`)
// Find existing tab
let tabs = await chrome.tabs.query({ url: pattern })
let tab = tabs.find(t => t.id && !t.discarded)
console.log(`[COSE] tabContextFetch: found ${tabs.length} tabs, usable: ${tab ? tab.id : 'none'}`)
if (!tab) {
// Create a background tab (not active, for other platforms that need it)
const newTab = await chrome.tabs.create({ url: siteUrl, active: false })
tab = newTab
createdTabId = tab.id
console.log(`[COSE] tabContextFetch: created background tab ${tab.id}`)
// Wait for the tab to finish loading
const currentTab = await chrome.tabs.get(tab.id)
if (currentTab.status !== 'complete') {
await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
chrome.tabs.onUpdated.removeListener(listener)
reject(new Error('Tab load timeout'))
}, timeout)
const listener = (tabId, info) => {
if (tabId === tab.id && info.status === 'complete') {
chrome.tabs.onUpdated.removeListener(listener)
clearTimeout(timer)
resolve()
}
}
chrome.tabs.onUpdated.addListener(listener)
})
}
}
// Inject script to make the fetch in the page's main world
// so that credentials: 'include' sends the page's cookies
const results = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (fetchUrl, respType) => {
try {
const resp = await fetch(fetchUrl, {
method: 'GET',
credentials: 'include',
headers: { 'Accept': respType === 'json' ? 'application/json' : 'text/html' },
})
const status = resp.status
const finalUrl = resp.url
let body = null
if (respType === 'json') {
try { body = await resp.json() } catch (e) { body = null }
} else {
body = await resp.text()
}
return { status, url: finalUrl, body }
} catch (e) {
return { error: e.message }
}
},
args: [apiUrl, responseType],
world: 'MAIN',
})
// Clean up created tab
if (createdTabId) {
try { await chrome.tabs.remove(createdTabId) } catch (e) { /* ignore */ }
}
console.log(`[COSE] tabContextFetch result:`, JSON.stringify(results?.[0]?.result).substring(0, 200))
return results?.[0]?.result || null
} catch (e) {
// Clean up on error
if (createdTabId) {
try { await chrome.tabs.remove(createdTabId) } catch (e2) { /* ignore */ }
}
console.log(`[COSE] tabContextFetch failed for ${apiUrl}:`, e.message)
return null
}
}
globalThis.__coseTabContextFetch = tabContextFetch
/**
* 51CTO detection via offscreen document (爱贝壳 approach).
* Offscreen has document context so DOMParser is available.
* Includes retry logic in case the offscreen document wasn't ready on first attempt.
*/
async function detectCto51ViaOffscreen() {
for (let attempt = 1; attempt <= 2; attempt++) {
try {
console.log(`[COSE] 51CTO: Sending OFFSCREEN_DETECT_CTO51 (attempt ${attempt})...`)
const result = await sendOffscreenMessage({
type: 'OFFSCREEN_DETECT_CTO51',
})
console.log('[COSE] 51CTO: Offscreen response:', JSON.stringify(result))
if (result === undefined || result === null) {
// No listener responded — offscreen might not be ready
console.warn('[COSE] 51CTO: Got empty response, offscreen may not be ready')
if (attempt < 2) {
await new Promise(r => setTimeout(r, 500))
continue
}
}
return result?.data || null
} catch (e) {
console.log(`[COSE] 51CTO offscreen detection failed (attempt ${attempt}):`, e.message)
if (attempt < 2) {
await new Promise(r => setTimeout(r, 500))
continue
}
return null
}
}
return null
}
globalThis.__coseDetectCto51 = detectCto51ViaOffscreen
/**
* Cnblogs detection via offscreen document.
* Offscreen has document context so cookies are sent automatically.
*/
async function detectCnblogsViaOffscreen() {
try {
console.log('[COSE] Cnblogs: Sending OFFSCREEN_DETECT_CNBLOGS message...')
const result = await sendOffscreenMessage({
type: 'OFFSCREEN_DETECT_CNBLOGS',
})
console.log('[COSE] Cnblogs: Offscreen response:', JSON.stringify(result))
return result?.data || null
} catch (e) {
console.log('[COSE] Cnblogs offscreen detection failed:', e.message)
return null
}
}
globalThis.__coseDetectCnblogs = detectCnblogsViaOffscreen
/**
* Xiaohongshu detection via offscreen document.
* Offscreen has document context so cookies are sent automatically.
*/
async function detectXiaohongshuViaOffscreen() {
try {
console.log('[COSE] Xiaohongshu: Sending OFFSCREEN_DETECT_XIAOHONGSHU message...')
const result = await sendOffscreenMessage({
type: 'OFFSCREEN_DETECT_XIAOHONGSHU',
})
console.log('[COSE] Xiaohongshu: Offscreen response:', JSON.stringify(result))
return result?.data || null
} catch (e) {
console.log('[COSE] Xiaohongshu offscreen detection failed:', e.message)
return null
}
}
globalThis.__coseDetectXiaohongshu = detectXiaohongshuViaOffscreen
// 初始化动态规则:为 sinaimg 和 sspai 头像添加 CORS 头
async function initDynamicRules() {
try {
// 先移除已有的规则
const existingRules = await chrome.declarativeNetRequest.getDynamicRules()
const existingIds = existingRules.map(r => r.id)
if (existingIds.length > 0) {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: existingIds
})
}
// 添加新规则
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [
{
id: 1,
priority: 100,
action: {
type: 'modifyHeaders',
requestHeaders: [
{ header: 'Referer', operation: 'set', value: 'https://weibo.com/' },
{ header: 'Origin', operation: 'set', value: 'https://weibo.com' }
],
responseHeaders: [
{ header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }
]
},
condition: {
urlFilter: '*sinaimg.cn*',
resourceTypes: ['image', 'xmlhttprequest']
}
},
{
id: 2,
priority: 100,
action: {
type: 'modifyHeaders',
requestHeaders: [
{ header: 'Referer', operation: 'set', value: 'https://sspai.com/' },
{ header: 'Origin', operation: 'set', value: 'https://sspai.com' }
],
responseHeaders: [
{ header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }
]
},
condition: {
urlFilter: '*cdnfile.sspai.com*',
resourceTypes: ['image', 'xmlhttprequest']
}
}
]
})
console.log('[COSE] 动态规则初始化完成')
} catch (e) {
console.error('[COSE] 动态规则初始化失败:', e)
}
}
// 扩展安装/更新/启动时初始化规则
chrome.runtime.onInstalled.addListener(() => {
initDynamicRules()
})
chrome.runtime.onStartup.addListener(() => {
initDynamicRules()
})
// 当前同步任务的 Tab Group ID
let currentSyncGroupId = null
// 存储平台用户信息
const PLATFORM_USER_INFO = {}
// 获取或创建同步标签组
async function getOrCreateSyncGroup(windowId) {
// 如果已有 group 且仍然有效,直接返回
if (currentSyncGroupId !== null) {
try {
const groups = await chrome.tabGroups.query({ windowId })
const existingGroup = groups.find(g => g.id === currentSyncGroupId)
if (existingGroup) {
return currentSyncGroupId
}
} catch (e) {
// Group 不存在,需要创建新的
}
}
// 创建新的标签组(先创建一个空组是不行的,需要先有 tab)
currentSyncGroupId = null
return null
}
// 将标签添加到同步组
async function addTabToSyncGroup(tabId, windowId) {
try {
if (currentSyncGroupId === null) {
// 创建新组
currentSyncGroupId = await chrome.tabs.group({ tabIds: tabId })
// 设置组的样式,使用时间戳作为标题
const now = new Date()
const timestamp = `${now.getMonth() + 1}/${now.getDate()} ${now.getHours()}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
await chrome.tabGroups.update(currentSyncGroupId, {
title: `${timestamp}`,
color: 'blue',
collapsed: false,
})
} else {
// 添加到现有组
await chrome.tabs.group({ tabIds: tabId, groupId: currentSyncGroupId })
}
} catch (error) {
console.error('[COSE] 添加标签到组失败:', error)
}
}
// 登录检测配置
// 登录检测配置由 import 导入
async function logToStorage(msg, data = null) {
try {
const timestamp = new Date().toISOString()
const logMsg = data ? `${msg} ${JSON.stringify(data)}` : msg
const { debug_logs = [] } = await chrome.storage.local.get('debug_logs')
debug_logs.push(`[${timestamp}] ${logMsg}`)
if (debug_logs.length > 500) debug_logs.shift()
await chrome.storage.local.set({ debug_logs })
} catch (e) {
console.error('Error logging to storage:', e)
}
console.log(msg, data || '')
}
// 消息监听
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// Let OFFSCREEN_* messages pass through to the offscreen document listener.
// If we handle them here, sendResponse fires before the offscreen doc can reply.
if (request.type && request.type.startsWith('OFFSCREEN_')) {
return false
}
if (request.type === 'GET_DEBUG_LOGS') {
chrome.storage.local.get('debug_logs', (result) => {
sendResponse({ logs: result.debug_logs || [] })
})
return true
}
(async () => {
try {
const result = await handleMessage(request, sender)
sendResponse(result)
} catch (err) {
console.error('[COSE] 消息处理错误:', err)
sendResponse({ error: err.message || '未知错误' })
}
})()
return true // 表示异步响应
})
async function handleMessage(request, sender) {
console.log(`[COSE] handleMessage received type: ${request.type}`, request)
switch (request.type) {
case 'GET_PLATFORMS':
return { platforms: PLATFORMS }
case 'CHECK_PLATFORM_STATUS':
return { status: await checkAllPlatforms(request.platforms || PLATFORMS) }
case 'CHECK_PLATFORM_STATUS_PROGRESSIVE':
// 渐进式检测:每个平台检测完成后立即返回结果
checkAllPlatformsProgressive(request.platforms || PLATFORMS, sender.tab?.id)
return { started: true, total: (request.platforms || PLATFORMS).length }
case 'START_SYNC_BATCH':
// 开始新的同步批次,重置 tab group
currentSyncGroupId = null
return { success: true }
case 'SYNC_TO_PLATFORM':
return await syncToPlatform(request.platformId, request.content)
case 'CACHE_USER_INFO':
// 缓存用户信息
if (request.platform === 'xiaohongshu' && request.userInfo) {
await chrome.storage.local.set({ xiaohongshu_user: request.userInfo })
console.log('[COSE] 小红书用户信息已缓存:', request.userInfo.username)
} else if (request.platform === 'alipayopen' && request.userInfo) {
await chrome.storage.local.set({ alipayopen_user: request.userInfo })
console.log('[COSE] 支付宝用户信息已缓存:', request.userInfo.username)
} else if (request.platform === 'huaweicloud' && request.userInfo) {
const hwcInfo = { ...request.userInfo }
if (hwcInfo.avatar && hwcInfo.avatar.startsWith('http')) {
hwcInfo.avatar = await convertAvatarToBase64(hwcInfo.avatar, 'https://bbs.huaweicloud.com/')
}
await chrome.storage.local.set({ huaweicloud_user: hwcInfo })
console.log('[COSE] 华为云用户信息已缓存:', hwcInfo.username)
} else if (request.platform === 'huaweidev' && request.userInfo) {
const hwdInfo = { ...request.userInfo }
if (hwdInfo.avatar && hwdInfo.avatar.startsWith('http')) {
hwdInfo.avatar = await convertAvatarToBase64(hwdInfo.avatar, 'https://developer.huawei.com/')
}
await chrome.storage.local.set({ huaweidev_user: hwdInfo })
console.log('[COSE] 华为开发者用户信息已缓存:', hwdInfo.username)
}
return { success: true }
default:
return { error: 'Unknown message type' }
}
}
// 检查所有平台登录状态
async function checkAllPlatforms(platforms) {
const status = {}
try {
// 过滤掉无效的平台配置
const validPlatforms = (platforms || []).filter(p => p && p.id)
const results = await Promise.allSettled(
validPlatforms.map(async (platform) => {
try {
const result = await checkPlatformLogin(platform)
return { id: platform.id, result }
} catch (e) {
return { id: platform.id, result: { loggedIn: false, error: e.message } }
}
})
)
results.forEach((res) => {
if (res.status === 'fulfilled' && res.value?.id) {
status[res.value.id] = res.value.result
}
})
} catch (e) {
console.error('[COSE] 检查平台状态失败:', e)
}
return status
}
// 渐进式检查所有平台登录状态(每个平台完成后立即返回结果)
async function checkAllPlatformsProgressive(platforms, tabId) {
const validPlatforms = (platforms || []).filter(p => p && p.id)
let completed = 0
const total = validPlatforms.length
// 并行检查所有平台,每个完成后立即发送结果
const promises = validPlatforms.map(async (platform) => {
try {
const result = await checkPlatformLogin(platform)
completed++
// 通过 content script 发送单个平台结果回页面
if (tabId) {
try {
await chrome.tabs.sendMessage(tabId, {
type: 'PLATFORM_STATUS_UPDATE',
platformId: platform.id,
platform: platform,
result: result,
completed: completed,
total: total
})
} catch (e) {
console.log('[COSE] 发送平台状态更新失败:', platform.id, e.message)
}
}
return { id: platform.id, result }
} catch (e) {
completed++
const errorResult = { loggedIn: false, error: e.message }
if (tabId) {
try {
await chrome.tabs.sendMessage(tabId, {
type: 'PLATFORM_STATUS_UPDATE',
platformId: platform.id,
platform: platform,
result: errorResult,
completed: completed,
total: total
})
} catch (e2) {
console.log('[COSE] 发送平台状态更新失败:', platform.id, e2.message)
}
}
return { id: platform.id, result: errorResult }
}
})
// 等待所有完成后发送完成消息
await Promise.allSettled(promises)
if (tabId) {
try {
await chrome.tabs.sendMessage(tabId, {
type: 'PLATFORM_STATUS_COMPLETE',
total: total
})
} catch (e) {
console.log('[COSE] 发送完成消息失败:', e.message)
}
}
}
import { detectUser } from '@cose/detection'
// 检查单个平台登录状态
async function checkPlatformLogin(platform) {
if (!platform || !platform.id) {
return { loggedIn: false, error: '无效的平台配置' }
}
return await detectUser(platform.id)
}
async function pasteWithDebugger(tabId) {
const debuggee = { tabId }
try {
// 附加调试器
await chrome.debugger.attach(debuggee, '1.3')
console.log('[COSE] Debugger attached')
// 发送 Ctrl/Cmd 按下
await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {
type: 'keyDown',
modifiers: 2, // Ctrl
windowsVirtualKeyCode: 17,
code: 'ControlLeft',
key: 'Control'
})
// 发送 V 按下(带 Ctrl 修饰符)
await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {
type: 'keyDown',
modifiers: 2, // Ctrl
windowsVirtualKeyCode: 86,
code: 'KeyV',
key: 'v'
})
// 发送 V 释放
await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: 2,
windowsVirtualKeyCode: 86,
code: 'KeyV',
key: 'v'
})
// 发送 Ctrl 释放
await chrome.debugger.sendCommand(debuggee, 'Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: 0,
windowsVirtualKeyCode: 17,
code: 'ControlLeft',
key: 'Control'
})
console.log('[COSE] Paste command sent via debugger')
// 等待粘贴完成
await new Promise(resolve => setTimeout(resolve, 1000))
} catch (error) {
console.error('[COSE] Debugger paste failed:', error)
} finally {
// 分离调试器
try {
await chrome.debugger.detach(debuggee)
console.log('[COSE] Debugger detached')
} catch (e) {
// 忽略分离错误
}
}
}
// 同步到平台
async function syncToPlatform(platformId, content) {
const platform = PLATFORMS.find(p => p && p.id === platformId)
if (!platform || !platform.publishUrl) {
return { success: false, message: '暂不支持该平台' }
}
try {
let tab
// 检查是否有平台特定的同步处理器
const syncHandler = SYNC_HANDLERS[platformId]
if (syncHandler) {
console.log(`[COSE] 使用 ${platformId} 平台特定同步处理器`)
// 创建新标签页(对于微信等需要特殊处理的平台,使用首页)
const initialUrl = platformId === 'wechat' ? 'https://mp.weixin.qq.com/' : platform.publishUrl
tab = await chrome.tabs.create({ url: initialUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
// 调用平台特定处理器
const helpers = {
chrome,
waitForTab,
addTabToSyncGroup,
PLATFORMS,
}
return await syncHandler(tab, content, helpers)
}
// ==== 以下是原有的平台特定逻辑(待迁移)====
if (platformId === 'infoq') {
// InfoQ:需要先调用 API 创建草稿获取 ID,不能直接访问 /draft/write
try {
// 调用创建草稿 API
const response = await fetch('https://xie.infoq.cn/api/v1/draft/create', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
})
const data = await response.json()
if (data.code === 0 && data.data?.id) {
const draftId = data.data.id
const targetUrl = `https://xie.infoq.cn/draft/${draftId}`
console.log('[COSE] InfoQ 创建草稿成功,ID:', draftId)
tab = await chrome.tabs.create({ url: targetUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
} else {
console.error('[COSE] InfoQ 创建草稿失败:', data)
return { success: false, message: 'InfoQ 创建草稿失败,请确保已登录' }
}
} catch (e) {
console.error('[COSE] InfoQ API 调用失败:', e)
return { success: false, message: 'InfoQ API 调用失败: ' + e.message }
}
} else if (platformId === 'jianshu') {
// 简书:需要先获取文集列表,然后创建新文章
try {
// 获取用户的文集列表
const notebooksResp = await fetch('https://www.jianshu.com/author/notebooks', {
method: 'GET',
credentials: 'include',
headers: {
'Accept': 'application/json',
}
})
const notebooks = await notebooksResp.json()
if (!notebooks || notebooks.length === 0) {
return { success: false, message: '简书未找到文集,请先创建一个文集' }
}
// 使用第一个文集
const notebookId = notebooks[0].id
console.log('[COSE] 简书使用文集:', notebooks[0].name, 'ID:', notebookId)
// 创建新文章
const createResp = await fetch('https://www.jianshu.com/author/notes', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
notebook_id: String(notebookId),
title: content.title || '无标题',
at_bottom: false
})
})
const noteData = await createResp.json()
if (noteData && noteData.id) {
const noteId = noteData.id
const targetUrl = `https://www.jianshu.com/writer#/notebooks/${notebookId}/notes/${noteId}`
console.log('[COSE] 简书创建文章成功,ID:', noteId)
tab = await chrome.tabs.create({ url: targetUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
} else {
console.error('[COSE] 简书创建文章失败:', noteData)
return { success: false, message: '简书创建文章失败,请确保已登录' }
}
} catch (e) {
console.error('[COSE] 简书 API 调用失败:', e)
return { success: false, message: '简书 API 调用失败: ' + e.message }
}
} else if (platformId === 'xiaohongshu') {
// 小红书:需要先点击"新的创作"按钮,等待编辑器加载后填充
console.log('[COSE] 开始处理小红书同步...')
// 打开发布页面
tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
// 等待页面加载完成
await new Promise(resolve => setTimeout(resolve, 3000))
// 在页面中执行:点击"新的创作"并等待编辑器加载
const clickResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async () => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
// 查找"新的创作"按钮
const createBtn = Array.from(document.querySelectorAll('button'))
.find(el => el.textContent.includes('新的创作'))
if (createBtn) {
createBtn.click()
console.log('[COSE] 小红书已点击"新的创作"按钮')
// 等待编辑器加载(等待富文本编辑器出现)
const waitForEditor = async (timeout = 10000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
// 查找编辑器元素,可能是 contenteditable 或 textarea
const editor = document.querySelector('[contenteditable="true"]') ||
document.querySelector('textarea') ||
document.querySelector('.editor') ||
document.querySelector('.content-editor')
if (editor) return true
await sleep(200)
}
return false
}
const editorLoaded = await waitForEditor()
return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' }
}
return { success: false, message: 'Create button not found' }
}
})
if (!clickResult[0]?.result?.success) {
return { success: false, message: '小红书创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') }
}
// 等待页面稳定
await new Promise(resolve => setTimeout(resolve, 1000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 小红书 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
// 等待元素出现的工具函数
const waitForElement = (selector, timeout = 15000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
console.log('[COSE] 小红书开始填充内容...')
// 等待并查找标题输入框
const titleInput = await waitForElement('input[placeholder*="标题"], textarea[placeholder*="标题"], .title-input', 5000)
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保 React/Vue 等框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 小红书标题已填充:', title)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 300))
// 等待并查找内容编辑器
const contentEditor = await waitForElement('[contenteditable="true"], .editor-content, .content-editor', 5000)
if (contentEditor && htmlBody) {
contentEditor.focus()
// 清空现有占位符内容
if (contentEditor.textContent.includes('从这里开始写正文') ||
contentEditor.textContent.includes('请输入正文') ||
contentEditor.textContent.includes('写点什么')) {
contentEditor.innerHTML = ''
}
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
contentEditor.dispatchEvent(pasteEvent)
console.log('[COSE] 小红书内容已通过 paste 事件注入')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = contentEditor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
contentEditor.innerHTML = htmlBody
}
return { success: true, method: 'paste-html', length: htmlBody.length }
}
return { success: false, error: 'Content editor not found' }
} catch (e) {
console.error('[COSE] 小红书同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] 小红书填充结果:', fillResult[0]?.result)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到小红书', tabId: tab.id }
} else if (platformId === 'twitter') {
// Twitter Articles:需要先打开草稿列表页,然后点击 create 按钮创建新文章
// 注意:Twitter 使用 Page Visibility API,后台标签页不会渲染编辑器
// 解决方案:短暂激活 Tab,等编辑器加载后切回原 Tab
// 记录当前活动的 Tab
const [currentTab] = await chrome.tabs.query({ active: true, currentWindow: true })
// 第一步:打开草稿列表页(激活状态,触发编辑器渲染)
tab = await chrome.tabs.create({ url: platform.publishUrl, active: true })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 第二步:点击 create 按钮并等待编辑器加载完成
const clickResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async () => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
// 查找 create 按钮
const createBtn = document.querySelector('button[aria-label="create"]') ||
Array.from(document.querySelectorAll('button')).find(b =>
b.getAttribute('aria-label')?.toLowerCase() === 'create'
)
if (createBtn) {
createBtn.click()
console.log('[COSE] Twitter Articles 已点击 create 按钮')
// 等待编辑器加载(等待标题输入框出现)
const waitForEditor = async (timeout = 10000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
const titleInput = document.querySelector('textarea[placeholder="Add a title"]')
if (titleInput) return true
await sleep(200)
}
return false
}
const editorLoaded = await waitForEditor()
return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' }
}
return { success: false, message: 'Create button not found' }
},
world: 'MAIN',
})
console.log('[COSE] Twitter Articles create 结果:', clickResult[0]?.result)
// 编辑器加载完成后,切回原 Tab
if (currentTab?.id) {
try {
await chrome.tabs.update(currentTab.id, { active: true })
console.log('[COSE] Twitter 已切回原 Tab')
} catch (e) {
// 原 Tab 可能已关闭,忽略
}
}
if (!clickResult[0]?.result?.success) {
return { success: false, message: 'Twitter Articles 创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') }
}
// 等待页面稳定
await new Promise(resolve => setTimeout(resolve, 500))
// 使用 Markdown 内容
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] Twitter Articles Markdown 内容长度:', markdownContent?.length || 0)
// 第三步:填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
// ========== 工具函数 ==========
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const waitForElement = async (selector, timeout = 10000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
const el = document.querySelector(selector)
if (el) return el
await sleep(200)
}
return null
}
// ========== 内置 Markdown 解析器(支持代码块和公式)==========
// 使用占位符保护机制,避免正则冲突
// 代码块使用样式化 HTML,公式使用 CodeCogs API 渲染为图片
function parseMarkdownToHtml(md) {
if (!md) return ''
// 存储需要保护的内容
const codeBlocks = []
const inlineCodes = []
const blockFormulas = []
const inlineFormulas = []
let html = md
// ========== 第一阶段:提取并保护特殊内容 ==========
// 1. 提取代码块 ```...```
html = html.replace(/```(\w*)\n?([\s\S]*?)```/g, (match, lang, code) => {
const index = codeBlocks.length
codeBlocks.push({ lang: lang || '', code: code })
return `__CODE_BLOCK_${index}__`
})
// 2. 提取行内代码 `...`
html = html.replace(/`([^`\n]+)`/g, (match, code) => {
const index = inlineCodes.length
inlineCodes.push(code)
return `__INLINE_CODE_${index}__`
})
// 3. 提取块级公式 $$...$$
html = html.replace(/\$\$([\s\S]+?)\$\$/g, (match, formula) => {
const index = blockFormulas.length
blockFormulas.push(formula.trim())
return `__BLOCK_FORMULA_${index}__`
})
// 4. 提取行内公式 $...$
html = html.replace(/\$([^\$\n]+)\$/g, (match, formula) => {
const index = inlineFormulas.length
inlineFormulas.push(formula.trim())
return `__INLINE_FORMULA_${index}__`
})
// ========== 第二阶段:处理标准 Markdown 语法 ==========
// 处理标题
html = html.replace(/^#### (.+)$/gm, '<h3>$1</h3>')
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>')
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>')
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>')
// 处理引用块
html = html.replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>')
// 处理水平分割线
// 注意: X Articles 忽略 <hr> 标签,需要通过 Insert > Divider 菜单插入
// 自动同步无法使用菜单,这里保留 hr 但用户可能需要手动调整
// 或者可以考虑用视觉分隔符如 --- 文本替代
html = html.replace(/^---$/gm, '<p>---</p>')
html = html.replace(/^\*\*\*$/gm, '<p>***</p>')
// 处理图片
html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1" style="max-width: 100%;" />')
// 处理链接
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>')
// 处理粗体、斜体、删除线
html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>')
html = html.replace(/~~([^~]+)~~/g, '<s>$1</s>')
// ========== 第三阶段:恢复保护的内容 ==========
// 恢复代码块 - X Articles 不支持 <pre><code>,转换为 blockquote
// 参考 x-article-publisher skill 的实现
codeBlocks.forEach((block, index) => {
const escapedCode = block.code
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
// 将代码行用 <br> 连接,包装在 blockquote 中
// X Articles 原生支持 blockquote,这是最可靠的代码块显示方式
const lines = escapedCode.split('\n').filter(line => line.trim())
const formattedCode = lines.join('<br>')
const langPrefix = block.lang ? `<strong>${block.lang}</strong><br>` : ''
const codeHtml = `<blockquote>${langPrefix}${formattedCode}</blockquote>`
html = html.replace(`__CODE_BLOCK_${index}__`, codeHtml)
})
// 恢复行内代码 - X Articles 对 inline style 支持有限
// 使用简单的 <code> 标签,依赖平台默认样式
inlineCodes.forEach((code, index) => {
const escapedCode = code
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
// 简化为纯 code 标签,X Articles 会应用默认样式
const codeHtml = `<code>${escapedCode}</code>`
html = html.replace(`__INLINE_CODE_${index}__`, codeHtml)
})
// 恢复块级公式(使用 CodeCogs API 渲染为图片)
blockFormulas.forEach((formula, index) => {
const encodedFormula = encodeURIComponent(formula)
const formulaHtml = `<div style="text-align: center; margin: 16px 0;"><img src="https://latex.codecogs.com/svg.image?${encodedFormula}" alt="${formula.replace(/"/g, '"')}" style="max-width: 100%;" /></div>`
html = html.replace(`__BLOCK_FORMULA_${index}__`, formulaHtml)
})
// 恢复行内公式
inlineFormulas.forEach((formula, index) => {
const encodedFormula = encodeURIComponent(formula)
const formulaHtml = `<img src="https://latex.codecogs.com/svg.image?${encodedFormula}" alt="${formula.replace(/"/g, '"')}" style="vertical-align: middle;" />`
html = html.replace(`__INLINE_FORMULA_${index}__`, formulaHtml)
})
// ========== 第四阶段:处理列表和段落 ==========
// 处理无序列表项
html = html.replace(/^[\*\-\+] (.+)$/gm, '<li>$1</li>')
// 处理有序列表项
html = html.replace(/^\d+[\.\)] (.+)$/gm, '<li>$1</li>')
// 将连续的 <li> 包装成 <ul>
html = html.replace(/(<li>[\s\S]*?<\/li>\n?)+/g, (match) => {
return `<ul>${match}</ul>`
})
// 处理段落
const lines = html.split('\n')
const result = []
let paragraphLines = []
const isBlockElement = (line) => {
const trimmed = line.trim()
return !trimmed ||
trimmed.startsWith('<h') ||
trimmed.startsWith('<pre') ||
trimmed.startsWith('<blockquote') ||
trimmed.startsWith('<ul') ||
trimmed.startsWith('<ol') ||
trimmed.startsWith('<hr') ||
trimmed.startsWith('<div') ||
trimmed.startsWith('<li') ||
trimmed.startsWith('<img') ||
trimmed.startsWith('</ul') ||
trimmed.startsWith('</ol')
}
const flushParagraph = () => {
if (paragraphLines.length > 0) {
result.push(`<p>${paragraphLines.join('<br />')}</p>`)
paragraphLines = []
}
}
for (const line of lines) {
const trimmed = line.trim()
if (!trimmed) {
flushParagraph()
continue
}
if (isBlockElement(line)) {
flushParagraph()
result.push(trimmed)
} else {
paragraphLines.push(trimmed)
}
}
flushParagraph()
return result.join('\n')
}
// ========== 主流程 ==========
try {
console.log('[COSE] Twitter Articles 开始填充内容...')
// 使用内置解析器转换 Markdown 为 HTML
const htmlContent = parseMarkdownToHtml(markdown)
console.log('[COSE] Markdown 已转换为 HTML')
// 第一步:填充标题
const titleInput = await waitForElement('textarea[placeholder="Add a title"], textarea[name="Article Title"]', 5000)
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] Twitter Articles 标题填充成功')
} else {
console.log('[COSE] Twitter Articles 未找到标题输入框')
}
await sleep(500)
// 第二步:填充内容
const contentEl = await waitForElement('.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', 5000)
if (contentEl && htmlContent) {
contentEl.focus()
const dt = new DataTransfer()
dt.setData('text/html', htmlContent)
dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
contentEl.dispatchEvent(pasteEvent)
console.log('[COSE] Twitter Articles 内容填充成功')
return { success: true, method: 'paste-html', length: htmlContent.length }
} else {
console.log('[COSE] Twitter Articles 未找到内容编辑器')
return { success: false, error: 'Content editor not found' }
}
} catch (e) {
console.error('[COSE] Twitter Articles 同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] Twitter Articles 填充结果:', fillResult[0]?.result)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到 Twitter Articles', tabId: tab.id }
}
// 百度千帆开发者社区:注入 Markdown 并确认转换
// 注意:千帆编辑器有自动保存机制,会触发 POST /api/community/topic
// 该 API 被 OpenRASP WAF 拦截,返回"校验错误,可能是跨站点攻击",导致前端跳转登录页
// 解决方案:
// 1. 使用 declarativeNetRequest 阻止千帆 tab 导航到登录页(网络层拦截)
// 2. 内容脚本 qianfan-intercept.js 拦截 fetch/XHR/sendBeacon/location 跳转(JS 层拦截)
// 3. 监听 tab 的 URL 变化,如果跳转到登录页则导航回编辑器
if (platformId === 'qianfan') {
// 添加 declarativeNetRequest 规则:阻止千帆 tab 导航到登录页
const QIANFAN_BLOCK_RULE_ID = 9999
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID],
addRules: [{
id: QIANFAN_BLOCK_RULE_ID,
priority: 1000,
action: { type: 'block' },
condition: {
urlFilter: '*login.bce.baidu.com*',
initiatorDomains: ['qianfan.cloud.baidu.com'],
resourceTypes: ['main_frame', 'sub_frame']
}
}]
})
console.log('[COSE] 千帆登录页阻止规则已添加')
} catch (e) {
console.warn('[COSE] 千帆登录页阻止规则添加失败:', e)
}
// 打开发布页面
tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
// 动态注入千帆拦截脚本(MAIN world,尽早执行)
try {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: qianfanIntercept,
world: 'MAIN',
injectImmediately: true,
})
console.log('[COSE] 千帆拦截脚本已动态注入')
} catch (e) {
console.warn('[COSE] 千帆拦截脚本注入失败:', e)
}
// 监听 tab URL 变化,如果跳转到登录页则导航回编辑器
const tabUpdateListener = (tabId, changeInfo) => {
if (tabId === tab.id && changeInfo.url && changeInfo.url.includes('login.bce.baidu.com')) {
console.log('[COSE] 检测到千帆 tab 跳转到登录页,导航回编辑器')
chrome.tabs.update(tabId, { url: platform.publishUrl })
}
}
chrome.tabs.onUpdated.addListener(tabUpdateListener)
try {
// 等待页面加载
await waitForTab(tab.id)
await new Promise(resolve => setTimeout(resolve, 2000))
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 百度千帆 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
const waitForElement = (selector, timeout = 5000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) { observer.disconnect(); resolve(el) }
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => { observer.disconnect(); resolve(null) }, timeout)
})
}
try {
// 填充标题
const titleInput = await waitForElement('textarea[placeholder="请输入文章标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 百度千帆标题填充成功')
}
await sleep(300)
// 填充内容 - 使用 paste 事件注入 Markdown
const contentEditor = await waitForElement('.mp-editor-container[contenteditable="true"]')
if (contentEditor && markdown) {
contentEditor.focus()
await sleep(100)
const dt = new DataTransfer()
dt.setData('text/plain', markdown)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true, cancelable: true, clipboardData: dt
})
contentEditor.dispatchEvent(pasteEvent)
console.log('[COSE] 百度千帆内容填充成功')
// 等待并点击 Markdown 转换确认按钮
let confirmed = false
for (let i = 0; i < 15; i++) {
await sleep(200)
if (document.body.innerText.includes('检测到 Markdown')) {
const confirmBtn = document.querySelector('.mp-modal-enter-btn')
if (confirmBtn) {
confirmBtn.click()
confirmed = true
console.log('[COSE] 百度千帆已确认 Markdown 转换')
break
}
}
}
await sleep(1000)
return { success: true, confirmed }
}
return { success: false, error: 'Editor not found' }
} catch (e) {
console.error('[COSE] 百度千帆同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 百度千帆填充结果:', fillResult[0]?.result)
// 等待内容稳定
await new Promise(resolve => setTimeout(resolve, 2000))
// 清理:移除 tab 监听器和 declarativeNetRequest 规则
chrome.tabs.onUpdated.removeListener(tabUpdateListener)
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID]
})
console.log('[COSE] 千帆登录页阻止规则已移除')
} catch (_) {}
return { success: true, message: '已同步到百度云千帆,请手动点击发布', tabId: tab.id }
} catch (e) {
console.error('[COSE] 千帆同步失败:', e)
chrome.tabs.onUpdated.removeListener(tabUpdateListener)
try {
await chrome.declarativeNetRequest.updateDynamicRules({
removeRuleIds: [QIANFAN_BLOCK_RULE_ID]
})
} catch (_) {}
return { success: false, message: '千帆同步失败: ' + e.message }
}
}
/* [DISABLED] 支付宝开放平台:使用 ne-engine 富文本编辑器,支持 Markdown 转换
if (platformId === 'alipayopen') {
// 先打开发布页面
tab = await chrome.tabs.create({ url: platform.publishUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 2000))
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 支付宝开放平台 Markdown 内容长度:', markdownContent?.length || 0)
// 使用导入的填充函数
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillAlipayOpenContent,
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 支付宝开放平台填充结果:', fillResult[0]?.result)
// 等待内容处理完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }
} else [DISABLED] */
if (platformId !== 'wechat' && !tab) {
// 其他平台(排除微信,因为微信在上面已经处理)
let targetUrl = platform.publishUrl
// 开源中国:使用 ai-write 编辑器,需要用户 ID
if (platformId === 'oschina') {
const stored = await chrome.storage.local.get('oschina_userId')
const userId = stored?.oschina_userId
if (userId) {
targetUrl = `https://my.oschina.net/u/${userId}/blog/ai-write`
console.log('[COSE] 使用 OSChina AI 写作 URL:', targetUrl)
} else {
console.warn('[COSE] 未找到 OSChina 用户 ID,使用默认 URL')
}
}
// 直接打开发布页面
tab = await chrome.tabs.create({ url: targetUrl, active: false })
await addTabToSyncGroup(tab.id, tab.windowId)
await waitForTab(tab.id)
}
// 微信公众号:直接注入 HTML 到编辑器
if (platformId === 'wechat') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0)
// 等待额外时间确保编辑器完全加载
await new Promise(resolve => setTimeout(resolve, 2000))
// 等待编辑器就绪并注入内容
console.log('[COSE] 开始注入微信内容...')
console.log('[COSE] 目标 tab ID:', tab.id)
let result
try {
result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
// 等待元素出现的工具函数
const waitForElement = (selector, timeout = 15000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
// 等待编辑器加载完成
const editor = await waitForElement('.ProseMirror')
if (!editor) {
return { success: false, error: '未找到编辑器' }
}
// 等待标题输入框
const titleInput = await waitForElement('#title')
// 填充标题
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保 React/Vue 等框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 微信标题已填充:', title)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 300))
// 填充正文内容
if (editor && htmlBody) {
editor.focus()
// 清空现有占位符内容
if (editor.textContent.includes('从这里开始写正文')) {
editor.innerHTML = ''
}
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 微信内容已通过 paste 事件注入')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = editor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
return {
success: true,
wordCount: editor.textContent?.length || 0,
titleFilled: titleInput?.value === title
}
}
return { success: false, error: '内容为空' }
} catch (err) {
return { success: false, error: err.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
} catch (e) {
console.error('[COSE] executeScript 执行失败:', e)
return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }
}
console.log('[COSE] executeScript 返回数组长度:', result?.length)
console.log('[COSE] executeScript 完整返回:', JSON.stringify(result, null, 2))
if (!result || result.length === 0) {
console.error('[COSE] executeScript 返回空数组')
return { success: false, message: '脚本执行失败:无返回值', tabId: tab.id }
}
const fillResult = result[0].result
console.log('[COSE] 微信填充结果:', JSON.stringify(fillResult, null, 2))
// 检查 result 结构
if (!result || !result[0]) {
console.error('[COSE] executeScript 没有返回有效结果')
return { success: false, message: '内容注入失败:脚本执行无返回值', tabId: tab.id }
}
if (!fillResult?.success) {
console.error('[COSE] 微信内容填充失败:', fillResult?.error)
console.error('[COSE] 完整 result 对象:', result)
return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }
}
console.log('[COSE] 微信内容填充成功,字数:', fillResult.wordCount)
// 等待内容稳定后,点击保存为草稿按钮
await new Promise(resolve => setTimeout(resolve, 1000))
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveDraftBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent.includes('保存为草稿'))
if (saveDraftBtn) {
saveDraftBtn.click()
console.log('[COSE] 已点击保存为草稿')
}
},
world: 'MAIN',
})
return { success: true, message: '已同步并保存为草稿', tabId: tab.id }
}
// 抖音:使用剪贴板 HTML 粘贴到编辑器(类似微信公众号)
if (platformId === 'douyin') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 抖音 HTML 内容长度:', htmlContent?.length || 0)
console.log('[COSE] 开始注入抖音内容...')
let result
try {
result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
// 等待元素出现的工具函数(检测到立即返回)
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
// 等待编辑器加载完成 - 抖音使用 contenteditable div
const editor = await waitForElement('[contenteditable="true"]')
if (!editor) {
return { success: false, error: '未找到编辑器' }
}
// 等待标题输入框
const titleInput = await waitForElement('input[placeholder*="标题"]')
// 填充标题
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保 React 等框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 抖音标题已填充:', title)
}
// 填充正文内容
if (editor && htmlBody) {
editor.focus()
// 清空现有内容
editor.innerHTML = ''
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 抖音内容已通过 paste 事件注入')
// 立即验证内容是否注入成功
const wordCount = editor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
return {
success: true,
wordCount: editor.textContent?.length || 0,
titleFilled: titleInput?.value === title
}
}
return { success: false, error: '内容为空' }
} catch (err) {
return { success: false, error: err.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
} catch (e) {
console.error('[COSE] executeScript 执行失败:', e)
return { success: false, message: '脚本执行失败: ' + e.message, tabId: tab.id }
}
console.log('[COSE] 抖音填充结果:', JSON.stringify(result, null, 2))
if (!result || result.length === 0) {
return { success: false, message: '脚本执行失败:无返回值', tabId: tab.id }
}
const fillResult = result[0].result
if (!fillResult?.success) {
return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id }
}
console.log('[COSE] 抖音内容填充成功,字数:', fillResult.wordCount)
return { success: true, message: '已同步到抖音', tabId: tab.id }
}
// 搜狐号:使用剪贴板 HTML 粘贴到编辑器(类似微信公众号)
if (platformId === 'sohu') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 搜狐号 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 搜狐号标题填充成功')
}
// 找到 Quill 编辑器
const editor = document.querySelector('.ql-editor')
if (editor && htmlBody) {
editor.focus()
// 清空现有内容
editor.innerHTML = ''
// 使用 DataTransfer 触发 paste 事件
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 搜狐号内容已通过 paste 事件注入')
} else {
console.log('[COSE] 搜狐号未找到编辑器')
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到搜狐号', tabId: tab.id }
}
// B站专栏:使用 UEditor execCommand 插入 HTML
if (platformId === 'bilibili') {
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] B站专栏 HTML 内容长度:', htmlContent?.length || 0)
// 等待 UEditor 就绪
const waitForEditor = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
return new Promise((resolve) => {
const startTime = Date.now()
const maxWait = 10000
const check = () => {
const UE = window.UE
if (UE && UE.instants && UE.instants['ueditorInstant0']) {
const editor = UE.instants['ueditorInstant0']
if (editor.isReady) {
console.log('[COSE] UEditor 已就绪,耗时:', Date.now() - startTime, 'ms')
resolve({ ready: true, time: Date.now() - startTime })
return
}
}
if (Date.now() - startTime > maxWait) {
console.log('[COSE] UEditor 等待超时')
resolve({ ready: false, timeout: true })
return
}
setTimeout(check, 100)
}
check()
})
},
world: 'MAIN',
})
console.log('[COSE] B站专栏编辑器状态:', waitForEditor)
// 填充标题和内容(一次性完成)
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('textarea')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] B站专栏标题填充成功')
}
// 填充内容
const UE = window.UE
if (!UE || !UE.instants) {
return { success: false, error: 'UEditor not found' }
}
const editor = UE.instants['ueditorInstant0']
if (!editor) {
return { success: false, error: 'UEditor instance not found' }
}
// 清空并插入内容
editor.setContent('')
editor.execCommand('inserthtml', htmlBody)
editor.fireEvent('contentchange')
console.log('[COSE] B站专栏内容已填充')
return {
success: true,
contentLength: editor.getContentLength()
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] B站专栏填充结果:', fillResult)
// 短暂等待后点击存草稿
await new Promise(resolve => setTimeout(resolve, 300))
// 点击存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveDraftBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent && b.textContent.includes('存草稿'))
if (saveDraftBtn) {
saveDraftBtn.click()
console.log('[COSE] B站专栏已点击存草稿')
}
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 500))
return { success: true, message: '已同步并保存草稿到B站专栏', tabId: tab.id }
}
// 微博头条:使用 ProseMirror 编辑器
if (platformId === 'weibo') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 微博头条 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题
const titleInput = document.querySelector('textarea[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 微博头条标题填充成功')
}
// 填充内容 - 微博使用 ProseMirror/TipTap 编辑器
const editor = document.querySelector('.ProseMirror')
if (editor && htmlBody) {
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 微博头条内容填充成功')
return { success: true }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] 微博头条填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
// 点击保存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const saveBtn = Array.from(document.querySelectorAll('button'))
.find(b => b.textContent && b.textContent.includes('保存草稿'))
if (saveBtn) {
saveBtn.click()
console.log('[COSE] 微博头条已点击保存草稿')
}
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到微博头条', tabId: tab.id }
}
// 阿里云开发者社区:使用 Markdown 编辑器
if (platformId === 'aliyun') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 阿里云使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 阿里云开发者社区 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, markdown) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 阿里云开发者社区标题填充成功')
}
// 填充内容 - 阿里云使用 textarea 作为 Markdown 编辑器
const contentTextarea = document.querySelector('textarea[class*="editor"]') ||
document.querySelector('.markdown-editor textarea') ||
document.querySelector('textarea:not([placeholder*="标题"])')
if (contentTextarea && markdown) {
contentTextarea.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(contentTextarea, markdown)
contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))
contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 阿里云开发者社区内容填充成功')
return { success: true }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 阿里云开发者社区填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到阿里云开发者社区', tabId: tab.id }
}
// 火山引擎开发者社区:使用 ByteMD 编辑器(基于 CodeMirror)
if (platformId === 'volcengine') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 火山引擎使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 火山引擎开发者社区 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, markdown) => {
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]') ||
document.querySelector('input[class*="title"]') ||
document.querySelector('.article-title input')
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 来绕过 React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 火山引擎开发者社区标题填充成功')
}
// 火山引擎使用 ByteMD 编辑器(基于 CodeMirror)
const codeMirrorEl = document.querySelector('.CodeMirror')
if (codeMirrorEl && codeMirrorEl.CodeMirror && markdown) {
codeMirrorEl.CodeMirror.setValue(markdown)
console.log('[COSE] 火山引擎开发者社区内容填充成功')
return { success: true, method: 'CodeMirror' }
}
// 备用方案:尝试直接操作 textarea
const contentTextarea = document.querySelector('.bytemd-editor textarea') ||
document.querySelector('textarea:not([placeholder*="标题"])')
if (contentTextarea && markdown) {
contentTextarea.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(contentTextarea, markdown)
contentTextarea.dispatchEvent(new Event('input', { bubbles: true }))
contentTextarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 火山引擎开发者社区内容填充成功(textarea)')
return { success: true, method: 'textarea' }
}
return { success: false, error: 'Editor not found' }
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 火山引擎开发者社区填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到火山引擎开发者社区', tabId: tab.id }
}
// 华为云开发者博客:使用 Markdown 编辑器(在 iframe 中)
if (platformId === 'huaweicloud') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 华为云使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 华为云开发者博客 Markdown 内容长度:', markdownContent?.length || 0)
// 检查当前编辑器类型,如果不是 Markdown 则切换
const switchResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
if (window.tinymceModal?.currentEditorType === 'markdown') {
console.log('[COSE] 华为云已经是 Markdown 编辑器')
return { alreadyMarkdown: true }
}
const allElements = document.querySelectorAll('*')
for (const el of allElements) {
if (el.textContent === 'Markdown格式编辑' && el.children.length === 0) {
el.click()
console.log('[COSE] 华为云已点击 Markdown 编辑器标签')
return { clicked: true }
}
}
return { clicked: false }
},
world: 'MAIN',
})
// 如果点击了切换按钮,需要等待确认对话框并点击确定
if (switchResult[0]?.result?.clicked) {
await new Promise(resolve => setTimeout(resolve, 500))
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const allElements = document.querySelectorAll('*')
for (const el of allElements) {
if (el.textContent === '确定' && el.children.length === 0) {
el.click()
console.log('[COSE] 华为云已点击确定按钮')
return { confirmed: true }
}
}
return { confirmed: false }
},
world: 'MAIN',
})
// 等待编辑器切换完成
await new Promise(resolve => setTimeout(resolve, 3000))
}
// 填充标题
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title) => {
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 华为云开发者博客标题填充成功')
}
},
args: [content.title],
world: 'MAIN',
})
// 等待 Markdown 编辑器 iframe 完全就绪,然后填充内容
// 使用 MutationObserver 监听 iframe 出现,message 事件监听内容确认
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (markdown) => {
// 工具函数:使用 MutationObserver 等待 iframe 元素出现并加载
const waitForEditorReady = (timeout = 15000) => {
return new Promise((resolve) => {
const check = () => {
const editor = window.tinymceModal?.currentEditor
if (editor && typeof editor.setContent === 'function') {
const iframe = document.getElementById(editor.editor_id)
if (iframe && iframe.contentWindow) {
return { editor, iframe }
}
}
return null
}
// 先立即检查一次
const immediate = check()
if (immediate) return resolve(immediate)
// 使用 MutationObserver 监听 DOM 变化(iframe 插入)
let resolved = false
const observer = new MutationObserver(() => {
if (resolved) return
const result = check()
if (result) {
resolved = true
observer.disconnect()
resolve(result)
}
})
observer.observe(document.body, { childList: true, subtree: true })
// 超时兜底
setTimeout(() => {
if (!resolved) {
resolved = true
observer.disconnect()
resolve(null)
}
}, timeout)
})
}
// 工具函数:使用 message 事件监听 setContent 确认(setMdDataSucc)
const setContentWithConfirm = (editor, iframe, content, timeout = 3000) => {
return new Promise((resolve) => {
let resolved = false
const onMessage = (event) => {
try {
const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data
if (data.mdEventAction === 'setMdDataSucc' || data.mdEventAction === 'mdContent') {
if (!resolved) {
resolved = true
window.removeEventListener('message', onMessage)
resolve({ confirmed: true })
}
}
} catch (e) { /* 忽略非 JSON 消息 */ }
}
window.addEventListener('message', onMessage)
editor.setContent(content)
// 超时兜底
setTimeout(() => {
if (!resolved) {
resolved = true
window.removeEventListener('message', onMessage)
resolve({ confirmed: false })
}
}, timeout)
})
}
// 1. 等待编辑器和 iframe 就绪
console.log('[COSE] 华为云:等待 Markdown 编辑器 iframe 就绪...')
const ready = await waitForEditorReady()
if (!ready) {
console.log('[COSE] 华为云:编辑器等待超时')
return { success: false, error: '编辑器 iframe 等待超时' }
}
console.log('[COSE] 华为云:编辑器 iframe 已就绪')
// 2. 带重试的内容填充,通过 message 事件确认
const maxRetries = 6
for (let attempt = 1; attempt <= maxRetries; attempt++) {
console.log(`[COSE] 华为云内容填充尝试 ${attempt}/${maxRetries}`)
const result = await setContentWithConfirm(ready.editor, ready.iframe, markdown)
if (result.confirmed) {
console.log(`[COSE] 华为云内容填充成功(第${attempt}次),已收到 iframe 确认`)
return { success: true, method: 'message-confirm', attempt, length: markdown.length }
}
console.log(`[COSE] 华为云:未收到 iframe 确认,等待后重试...`)
// iframe 内部应用可能还在初始化,等待后重试
await new Promise(r => setTimeout(r, 2000))
}
// 3. 所有重试失败,直接 postMessage 作为最后手段
console.log('[COSE] 重试耗尽,尝试直接 postMessage')
ready.iframe.contentWindow.postMessage(JSON.stringify({
mdEditorEventAction: 'setMdEditorContent',
data: encodeURIComponent(markdown)
}), '*')
await new Promise(r => setTimeout(r, 1000))
return { success: true, method: 'direct-postMessage', length: markdown.length }
},
args: [markdownContent],
world: 'MAIN',
})
console.log('[COSE] 华为云开发者博客填充结果:', fillResult)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
// 点击保存草稿按钮
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => {
const allLinks = document.querySelectorAll('a')
for (const link of allLinks) {
if (link.textContent && link.textContent.includes('保存草稿')) {
link.click()
console.log('[COSE] 华为云开发者博客已点击保存草稿')
return { clicked: true }
}
}
return { clicked: false }
},
world: 'MAIN',
})
// 等待保存完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到华为云开发者博客', tabId: tab.id }
}
// 华为开发者文章:使用 ACE Editor (Markdown 编辑器)
if (platformId === 'huaweidev') {
// 华为开发者文章使用 Markdown 编辑器
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 华为开发者文章 Markdown 内容长度:', markdownContent?.length || 0)
// 注入异步弹窗监听器,并执行主流程
const result = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
// ========== 弹窗处理函数 ==========
const handleDialog = () => {
// 方法1: 查找 Ant Design Modal 中的按钮
const modalBtns = document.querySelector('.ant-modal-confirm-btns')
if (modalBtns) {
const buttons = modalBtns.querySelectorAll('button')
const modalText = document.querySelector('.ant-modal-confirm-content')?.textContent || ''
console.log('[COSE] 检测到 Ant Modal:', modalText.substring(0, 50))
// 处理"温馨提示"弹窗 - 点击"取消"
if (modalText.includes('温馨提示') || modalText.includes('未保存')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '取消') {
console.log('[COSE] 点击温馨提示弹窗的取消按钮')
btn.click()
return true
}
}
}
// 处理 MD 编辑器切换确认对话框 - 点击"确认"
if (modalText.includes('Markdown') || modalText.includes('切换')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '确认') {
console.log('[COSE] 点击 MD 切换确认按钮')
btn.click()
return true
}
}
}
}
// 方法2: 查找 HTML5 dialog 元素(备用)
const dialog = document.querySelector('dialog[open]')
if (dialog) {
const dialogText = dialog.textContent || ''
const buttons = dialog.querySelectorAll('button')
console.log('[COSE] 检测到 dialog:', dialogText.substring(0, 50))
if (dialogText.includes('温馨提示') || dialogText.includes('未保存')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '取消') {
btn.click()
return true
}
}
}
if (dialogText.includes('Markdown') || dialogText.includes('切换')) {
for (const btn of buttons) {
if (btn.textContent?.trim() === '确认') {
btn.click()
return true
}
}
}
}
return false
}
// ========== 工具函数 ==========
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))
// 轮询检查弹窗(比 MutationObserver 更可靠)
let dialogCheckInterval = null
const startDialogChecker = () => {
// 先检查已存在的弹窗
handleDialog()
// 定时检查新弹窗
dialogCheckInterval = setInterval(() => {
handleDialog()
}, 200)
console.log('[COSE] 华为开发者文章弹窗检查器已启动')
}
const stopDialogChecker = () => {
if (dialogCheckInterval) {
clearInterval(dialogCheckInterval)
dialogCheckInterval = null
console.log('[COSE] 华为开发者文章弹窗检查器已停止')
}
}
const waitForElement = async (selector, timeout = 5000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
const el = document.querySelector(selector)
if (el) return el
await sleep(100)
}
return null
}
// 等待按钮出现(支持 button 和 a 标签)
const waitForMdButton = async (timeout = 15000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
// 方法1: 通过 CKEditor 的 class 选择器(最精确)
let btn = document.querySelector('a.cke_button__cktomd')
if (btn) return btn
// 方法2: 查找包含 "MD编辑器" 文本的 <a> 标签
const allLinks = document.querySelectorAll('a')
for (const link of allLinks) {
if (link.textContent?.trim() === 'MD编辑器') {
return link
}
}
// 方法3: 查找包含 "MD编辑器" 文本的 <button> 标签(备用)
const allButtons = document.querySelectorAll('button')
for (const b of allButtons) {
if (b.textContent?.trim() === 'MD编辑器') {
return b
}
}
await sleep(300)
}
return null
}
// 等待富文本编辑器按钮出现
const waitForRichTextButton = async (timeout = 2000) => {
const start = Date.now()
while (Date.now() - start < timeout) {
// 查找 "富文本编辑器" 按钮(可能是 <a> 或 <button>)
const allElements = document.querySelectorAll('a, button')
for (const el of allElements) {
if (el.textContent?.trim() === '富文本编辑器') {
return el
}
}
await sleep(200)
}
return null
}
// ========== 主流程 ==========
try {
// 启动弹窗检查器
startDialogChecker()
// 等待 DOM 完全加载
if (document.readyState !== 'complete') {
console.log('[COSE] 等待 DOM 加载完成...')
await new Promise(resolve => {
if (document.readyState === 'complete') {
resolve()
} else {
window.addEventListener('load', resolve, { once: true })
}
})
}
// 等待页面加载完成(等待 MD编辑器 或 富文本编辑器 按钮出现)
// 华为开发者页面的编辑器工具栏是异步加载的,需要较长时间
console.log('[COSE] 等待编辑器工具栏加载...')
let mdButton = await waitForMdButton(15000)
let richTextButton = await waitForRichTextButton(2000)
// 检查是否已经是 Markdown 编辑器
let aceEditor = document.querySelector('.ace_editor')
// 如果有富文本编辑器按钮,说明当前已经是 Markdown 编辑器
if (richTextButton || aceEditor) {
console.log('[COSE] 已经是 Markdown 编辑器')
aceEditor = aceEditor || document.querySelector('.ace_editor')
} else if (!aceEditor) {
// 需要切换到 Markdown 编辑器
if (mdButton) {
console.log('[COSE] 点击 MD编辑器 按钮')
mdButton.click()
// 等待 ACE Editor 出现(弹窗会被检查器自动处理)
aceEditor = await waitForElement('.ace_editor', 10000)
if (!aceEditor) {
console.error('[COSE] 等待 ACE Editor 超时')
return { success: false, error: 'ACE Editor not found after timeout' }
}
} else {
console.error('[COSE] 未找到 MD编辑器 按钮')
return { success: false, error: 'MD Editor button not found' }
}
}
console.log('[COSE] 华为开发者文章已进入 Markdown 编辑器')
// 等待编辑器完全初始化
await sleep(500)
// 填充标题
const titleInput = document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 华为开发者文章标题填充成功')
}
// 填充 Markdown 内容
if (typeof ace !== 'undefined') {
const editor = ace.edit(aceEditor)
if (editor) {
editor.session.setValue(markdown)
console.log('[COSE] 华为开发者文章内容填充成功,长度:', markdown.length)
}
}
return { success: true, method: 'ace', length: markdown.length }
} finally {
// 清理检查器
stopDialogChecker()
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 华为开发者文章填充结果:', result[0]?.result)
return { success: true, message: '已同步到华为开发者文章', tabId: tab.id }
}
// 百家号:使用剪贴板 HTML 粘贴到编辑器
if (platformId === 'baijiahao') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 百家号 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题 - 百家号标题在 contenteditable div 中
const titleEditor = document.querySelector('.client_components_titleInput [contenteditable="true"]') ||
document.querySelector('.client_pages_edit_components_titleInput [contenteditable="true"]') ||
document.querySelector('[class*="titleInput"] [contenteditable="true"]')
if (titleEditor && title) {
titleEditor.focus()
titleEditor.innerHTML = ''
document.execCommand('insertText', false, title)
if (!titleEditor.textContent) {
titleEditor.innerHTML = `<p dir="auto">${title}</p>`
}
titleEditor.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 百家号标题已填充')
}
// 等待一下再填充内容
setTimeout(() => {
// 尝试通过 UEditor API 填充
if (window.UE_V2 && window.UE_V2.instants && window.UE_V2.instants.ueditorInstant0) {
try {
const editor = window.UE_V2.instants.ueditorInstant0
// 提取原始 HTML 中的公式(包含完整 SVG)
const tempDiv = document.createElement('div')
tempDiv.innerHTML = htmlBody
const originalFormulas = []
tempDiv.querySelectorAll('.katex-inline, .katex-block, section.katex-block').forEach((formula, index) => {
const svg = formula.querySelector('svg')
if (svg && svg.innerHTML) {
originalFormulas.push({
index,
className: formula.className,
fullHtml: formula.outerHTML
})
}
})
console.log('[COSE] 百家号提取到', originalFormulas.length, '个公式')
// 先用 setContent 设置内容(公式 SVG 会被过滤)
editor.setContent(htmlBody)
// 然后直接向 iframe 注入完整的公式 SVG
if (originalFormulas.length > 0) {
setTimeout(() => {
const iframe = document.querySelector('iframe')
if (iframe && iframe.contentDocument) {
const iframeDoc = iframe.contentDocument
const emptyFormulas = iframeDoc.querySelectorAll('.katex-inline, .katex-block, section.katex-block')
emptyFormulas.forEach((emptyFormula, index) => {
const original = originalFormulas[index]
if (original) {
// 创建新元素并替换
const newElement = document.createElement('div')
newElement.innerHTML = original.fullHtml
const newFormula = newElement.firstElementChild
if (newFormula && emptyFormula.parentNode) {
emptyFormula.parentNode.replaceChild(newFormula, emptyFormula)
}
}
})
console.log('[COSE] 百家号公式 SVG 已恢复')
editor.fireEvent('contentChange')
}
}, 300)
}
editor.fireEvent('contentChange');
editor.fireEvent('selectionchange');
console.log('[COSE] 百家号通过 UEditor API 填充成功')
return
} catch (e) {
console.log('[COSE] 百家号 UEditor API 调用失败', e)
}
}
}, 500)
},
args: [content.title, htmlContent],
world: 'MAIN',
})
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到百家号', tabId: tab.id }
}
// 少数派:使用剪贴板 HTML 粘贴到编辑器
if (platformId === 'sspai') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 少数派 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: (title, htmlBody) => {
// 填充标题 - 少数派使用 textarea
const titleInput = document.querySelector('textarea[placeholder*="标题"]') ||
document.querySelector('input[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 来绕过 Vue/React 的受控组件
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set ||
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
// 触发事件
titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
titleInput.dispatchEvent(new Event('blur', { bubbles: true }))
console.log('[COSE] 少数派标题已填充')
}
// 等待一下再填充内容
setTimeout(() => {
// 找到 ProseMirror 编辑器
const editor = document.querySelector('.ProseMirror') ||
document.querySelector('[contenteditable="true"]')
if (editor && htmlBody) {
editor.focus()
// 方法:创建 DataTransfer 并触发 paste 事件
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 少数派内容已通过 paste 事件注入')
}
}, 500)
},
args: [content.title, htmlContent],
world: 'MAIN',
})
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 2000))
return { success: true, message: '已同步到少数派', tabId: tab.id }
}
// 支付宝开放平台:使用 ne-engine 富文本编辑器
if (platformId === 'alipayopen') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用剪贴板 HTML(带完整样式)或降级到 body
const htmlContent = content.wechatHtml || content.body
console.log('[COSE] 支付宝开放平台 HTML 内容长度:', htmlContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, htmlBody) => {
// 等待元素出现的工具函数
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
console.log('[COSE] 支付宝开放平台开始填充内容...')
// 等待并查找标题输入框
const titleInput = await waitForElement('#title', 5000) || await waitForElement('input[placeholder*="标题"]', 5000)
if (titleInput && title) {
titleInput.focus()
// Ant Design 输入框需要特殊处理
// 先清空
titleInput.value = ''
// 使用 native setter 确保 React 能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
// 触发多个事件确保 Ant Design 组件能识别
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
titleInput.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }))
titleInput.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }))
// 使用 setValue 方法(如果存在)
if (titleInput._valueTracker) {
titleInput._valueTracker.setValue('')
}
// 再次设置值
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }))
// 模拟用户输入
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
data: title,
inputType: 'insertText'
})
titleInput.dispatchEvent(inputEvent)
console.log('[COSE] 支付宝开放平台标题已填充:', title, '当前值:', titleInput.value)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 300))
// 等待并查找 ne-engine 编辑器
const editor = await waitForElement('.ne-engine[contenteditable="true"]', 5000)
if (editor && htmlBody) {
editor.focus()
// 清空现有内容
editor.innerHTML = ''
// 使用 ClipboardEvent + DataTransfer 注入 HTML
const dt = new DataTransfer()
dt.setData('text/html', htmlBody)
dt.setData('text/plain', htmlBody.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
editor.dispatchEvent(pasteEvent)
console.log('[COSE] 支付宝开放平台内容已通过 paste 事件注入')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = editor.textContent?.length || 0
if (wordCount === 0) {
// 备用方案:直接设置 innerHTML
console.log('[COSE] paste 事件未生效,尝试备用方案')
editor.innerHTML = htmlBody
editor.dispatchEvent(new Event('input', { bubbles: true }))
}
return { success: true, method: 'paste-html', length: htmlBody.length }
}
return { success: false, error: 'ne-engine editor not found' }
} catch (e) {
console.error('[COSE] 支付宝开放平台同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, htmlContent],
world: 'MAIN',
})
console.log('[COSE] 支付宝开放平台填充结果:', fillResult[0]?.result)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到支付宝开放平台', tabId: tab.id }
}
// 电子发烧友:等待 Vditor 编辑器加载后填充内容
if (platformId === 'elecfans') {
// 等待页面完全加载
await new Promise(resolve => setTimeout(resolve, 3000))
// 使用 Markdown 内容
const markdownContent = content.markdown || content.body || ''
console.log('[COSE] 电子发烧友 Markdown 内容长度:', markdownContent?.length || 0)
// 填充标题和内容
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, markdown) => {
// 等待元素出现的工具函数
const waitForElement = (selector, timeout = 10000) => {
return new Promise((resolve) => {
const el = document.querySelector(selector)
if (el) return resolve(el)
const observer = new MutationObserver(() => {
const el = document.querySelector(selector)
if (el) {
observer.disconnect()
resolve(el)
}
})
observer.observe(document.body, { childList: true, subtree: true })
setTimeout(() => {
observer.disconnect()
resolve(document.querySelector(selector))
}, timeout)
})
}
try {
console.log('[COSE] 电子发烧友开始填充内容...')
// 等待并查找标题输入框
const titleInput = await waitForElement('input[placeholder*="标题"], input.title-input, input[name="title"]', 5000)
if (titleInput && title) {
titleInput.focus()
// 使用 native setter 确保框架能检测到变化
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 电子发烧友标题已填充:', title)
}
// 稍等一下让标题生效
await new Promise(r => setTimeout(r, 500))
// 优先查找 Vditor 编辑器(电子发烧友使用 Vditor)
const vditorWysiwyg = document.querySelector('.vditor-wysiwyg .vditor-reset')
if (vditorWysiwyg) {
vditorWysiwyg.focus()
// Vditor 需要 HTML 内容,将 Markdown 转换为简单 HTML
// 先尝试使用 ClipboardEvent 粘贴 Markdown
const dt = new DataTransfer()
dt.setData('text/plain', markdown)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
vditorWysiwyg.dispatchEvent(pasteEvent)
console.log('[COSE] 电子发烧友 Vditor paste 事件已触发')
// 等待内容渲染
await new Promise(r => setTimeout(r, 500))
// 验证内容是否注入成功
const wordCount = vditorWysiwyg.textContent?.length || 0
if (wordCount > 10) {
console.log('[COSE] 电子发烧友 Vditor 内容已填充,字数:', wordCount)
return { success: true, method: 'vditor-paste', length: wordCount }
}
// 备用方案:直接设置 textContent(Vditor 会自动解析 Markdown)
console.log('[COSE] paste 事件未生效,尝试直接输入')
vditorWysiwyg.textContent = markdown
vditorWysiwyg.dispatchEvent(new Event('input', { bubbles: true }))
return { success: true, method: 'vditor-direct', length: markdown.length }
}
// 等待并查找 CodeMirror 编辑器或 textarea
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(markdown)
console.log('[COSE] 电子发烧友 CodeMirror 内容已填充')
return { success: true, method: 'codemirror', length: markdown.length }
}
// 尝试查找 textarea
const textarea = await waitForElement('textarea.content-textarea, textarea[name="content"], textarea', 5000)
if (textarea && markdown) {
textarea.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(textarea, markdown)
} else {
textarea.value = markdown
}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
textarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 电子发烧友 textarea 内容已填充')
return { success: true, method: 'textarea', length: markdown.length }
}
// 尝试查找通用 contenteditable 编辑器
const editor = await waitForElement('[contenteditable="true"]', 5000)
if (editor && markdown) {
editor.focus()
editor.textContent = markdown
editor.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 电子发烧友 contenteditable 内容已填充')
return { success: true, method: 'contenteditable', length: markdown.length }
}
return { success: false, error: 'editor not found' }
} catch (e) {
console.error('[COSE] 电子发烧友同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, markdownContent],
world: 'MAIN',
})
console.log('[COSE] 电子发烧友填充结果:', fillResult[0]?.result)
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到电子发烧友', tabId: tab.id }
}
// 豆瓣:向首页分享框注入内容
if (platformId === 'douban') {
// 使用纯文本内容(豆瓣分享框不支持富文本)
const textContent = content.markdown || content.body || ''
console.log('[COSE] 豆瓣文本内容长度:', textContent?.length || 0)
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 2000))
// 填充分享框
const fillResult = await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: async (title, text) => {
try {
console.log('[COSE] 豆瓣开始填充内容...')
if (!text) {
return { success: false, error: 'Empty content' }
}
const fullText = title ? `${title}\n\n${text}` : text
// 豆瓣当前输入框:Lexical contenteditable
const editable = document.querySelector('div.DRE-inputor.DRE-root[contenteditable="true"]')
|| document.querySelector('[contenteditable="true"][role="textbox"]')
if (editable) {
editable.focus()
// 优先使用 Lexical 编辑器 API(豆瓣当前实现)
const lexicalEditor = editable.__lexicalEditor
if (lexicalEditor?.parseEditorState && lexicalEditor?.setEditorState) {
try {
const lines = fullText.split('\n')
const makeParagraph = (lineText) => ({
children: lineText
? [{ detail: 0, format: 0, mode: 'normal', style: '', text: lineText, type: 'text', version: 1 }]
: [],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
textFormat: 0,
textStyle: '',
})
const nextState = {
root: {
children: lines.map(makeParagraph),
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
}
const parsedState = lexicalEditor.parseEditorState(JSON.stringify(nextState))
lexicalEditor.setEditorState(parsedState)
lexicalEditor.focus()
const lexicalLength = (editable.textContent || '').trim().length
if (lexicalLength > 0) {
console.log('[COSE] 豆瓣 lexical API 内容已填充,长度:', lexicalLength)
return { success: true, length: lexicalLength, mode: 'lexical-api' }
}
} catch (e) {
console.log('[COSE] 豆瓣 lexical API 填充失败,回退 execCommand:', e.message)
}
}
// Lexical 编辑器对 direct textContent 赋值不稳定,优先使用 execCommand 输入
try {
const selection = window.getSelection()
const range = document.createRange()
range.selectNodeContents(editable)
selection?.removeAllRanges()
selection?.addRange(range)
} catch (_) {}
try {
document.execCommand('selectAll', false)
} catch (_) {}
try {
document.execCommand('delete', false)
} catch (_) {}
let inserted = false
try {
inserted = document.execCommand('insertText', false, fullText)
} catch (_) {
inserted = false
}
if (!inserted) {
// 回退:直接赋值并触发输入事件
editable.textContent = fullText
editable.dispatchEvent(new InputEvent('input', {
bubbles: true,
inputType: 'insertText',
data: fullText,
}))
}
editable.dispatchEvent(new Event('change', { bubbles: true }))
const actualLength = (editable.textContent || '').trim().length
if (actualLength === 0) {
return { success: false, error: 'Editor accepted no text' }
}
console.log('[COSE] 豆瓣 contenteditable 内容已填充,长度:', actualLength)
return { success: true, length: actualLength, mode: 'contenteditable' }
}
// 兼容旧版 textarea 结构
const textarea = document.querySelector('textarea[placeholder*="此刻你想要分享"]')
|| document.querySelector('textarea[placeholder*="分享"]')
|| document.querySelector('textarea')
if (textarea) {
textarea.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(textarea, fullText)
} else {
textarea.value = fullText
}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
textarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 豆瓣 textarea 内容已填充,长度:', fullText.length)
return { success: true, length: fullText.length, mode: 'textarea' }
}
return { success: false, error: 'Editor not found' }
} catch (e) {
console.error('[COSE] 豆瓣同步失败:', e)
return { success: false, error: e.message }
}
},
args: [content.title, textContent],
world: 'MAIN',
})
const doubanResult = fillResult[0]?.result
console.log('[COSE] 豆瓣填充结果:', doubanResult)
if (!doubanResult?.success) {
return {
success: false,
message: doubanResult?.error || '豆瓣内容填充失败',
tabId: tab.id,
}
}
// 等待内容注入完成
await new Promise(resolve => setTimeout(resolve, 1000))
return { success: true, message: '已同步到豆瓣,请手动点击发布', tabId: tab.id }
}
// 其他平台使用 scripting API 直接注入填充脚本
// 使用 MAIN world 才能访问页面的 CodeMirror 实例
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: fillContentOnPage,
args: [content, platformId],
world: 'MAIN',
})
return { success: true, message: '已打开发布页面并填充内容', tabId: tab.id }
} catch (error) {
return { success: false, message: error.message }
}
}
// 在目标页面执行的填充函数
function fillContentOnPage(content, platformId) {
const { title, body, markdown, wechatHtml } = content
// 等待元素出现的工具函数
function waitFor(selector, timeout = 10000) {
return new Promise((resolve) => {
const start = Date.now()
const check = () => {
const el = document.querySelector(selector)
if (el) resolve(el)
else if (Date.now() - start > timeout) resolve(null)
else setTimeout(check, 200)
}
check()
})
}
// 设置输入值
function setInputValue(el, value) {
if (!el || !value) return
el.focus()
if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') {
el.value = value
el.dispatchEvent(new Event('input', { bubbles: true }))
el.dispatchEvent(new Event('change', { bubbles: true }))
} else if (el.contentEditable === 'true') {
el.innerHTML = value.replace(/\n/g, '<br>')
el.dispatchEvent(new Event('input', { bubbles: true }))
}
}
// 根据平台填充内容
async function fill() {
const host = window.location.hostname
const contentToFill = markdown || body || ''
// 知乎专栏 - 由 syncToPlatform 单独处理(使用导入文档功能)
if (host.includes('zhihu.com')) {
console.log('[COSE] 知乎由导入文档功能处理')
}
// 今日头条
else if (host.includes('toutiao.com')) {
// 填充标题 - 头条使用 textarea
const titleInput = await waitFor('textarea[placeholder*="标题"]')
if (titleInput) {
titleInput.focus()
// 模拟用户输入
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
titleInput.dispatchEvent(new Event('blur', { bubbles: true }))
console.log('[COSE] 头条标题填充成功:', title)
} else {
console.log('[COSE] 头条未找到标题输入框')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 500))
// 头条使用 ProseMirror 富文本编辑器
const editor = document.querySelector('.ProseMirror')
if (editor) {
editor.focus()
editor.innerHTML = body || contentToFill.replace(/\n/g, '<br>')
editor.dispatchEvent(new InputEvent('input', { bubbles: true }))
console.log('[COSE] 头条内容填充成功')
} else {
console.log('[COSE] 头条未找到编辑器')
}
}
// 思否 SegmentFault
else if (host.includes('segmentfault.com')) {
// 填充标题
const titleInput = await waitFor('input#title, input[placeholder*="标题"]')
if (titleInput) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 思否标题填充成功')
} else {
console.log('[COSE] 思否未找到标题输入框')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 思否使用 CodeMirror 编辑器
const cmElement = document.querySelector('.CodeMirror')
if (cmElement && cmElement.CodeMirror) {
cmElement.CodeMirror.setValue(contentToFill)
console.log('[COSE] 思否 CodeMirror 填充成功')
} else {
// 降级到 textarea
const textarea = document.querySelector('textarea')
if (textarea) {
textarea.focus()
textarea.value = contentToFill
textarea.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] 思否 textarea 填充成功')
} else {
console.log('[COSE] 思否 未找到编辑器')
}
}
}
// 开源中国 OSChina (AI 写作平台 - 切换到 Markdown 编辑器)
else if (host.includes('oschina.net')) {
// 1. 切换到 MD 编辑器(如果当前不是)
const switchText = document.querySelector('.editor-switch-text')
if (switchText && switchText.textContent.includes('切换到MD编辑器')) {
// Click the switch button to trigger confirmation dialog
const switchBtn = document.querySelector('.editor-switch-btn') || switchText.parentElement
if (switchBtn) {
switchBtn.click()
console.log('[COSE] OSChina 已点击切换按钮')
// Poll for the confirmation button (Ant Design Modal animation)
let confirmBtn = null
for (let i = 0; i < 20; i++) {
await new Promise(resolve => setTimeout(resolve, 200))
confirmBtn = Array.from(document.querySelectorAll('button'))
.find(btn => btn.textContent.trim() === '确定切换')
if (confirmBtn) break
}
if (confirmBtn) {
confirmBtn.click()
console.log('[COSE] OSChina 已确认切换到MD编辑器')
} else {
console.log('[COSE] OSChina 未找到确认切换按钮')
}
// Wait for MD editor to load after switch
await new Promise(resolve => setTimeout(resolve, 2000))
}
} else {
console.log('[COSE] OSChina 已在MD编辑器模式')
}
// 2. 填充标题
const titleInput = await waitFor('input[placeholder*="标题"]')
if (titleInput) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set
if (nativeSetter) {
nativeSetter.call(titleInput, title)
} else {
titleInput.value = title
}
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] OSChina 标题填充成功')
}
// 3. 填充 Markdown 内容到 textarea
await new Promise(resolve => setTimeout(resolve, 500))
const mdContent = markdown || contentToFill
// Poll for textarea (may take time after editor switch)
let textarea = null
for (let i = 0; i < 10; i++) {
textarea = document.querySelector('textarea')
if (textarea) break
await new Promise(resolve => setTimeout(resolve, 300))
}
if (textarea) {
textarea.focus()
const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
if (textareaSetter) {
textareaSetter.call(textarea, mdContent)
} else {
textarea.value = mdContent
}
textarea.dispatchEvent(new Event('input', { bubbles: true }))
textarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] OSChina Markdown 内容填充成功,长度:', mdContent.length)
} else {
console.log('[COSE] OSChina 未找到 Markdown textarea')
}
}
// 博客园
else if (host.includes('cnblogs.com')) {
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 1000))
// 填充标题 - 博客园标题输入框
const titleInput = await waitFor('input[placeholder="标题"]') || document.querySelector('input')
if (titleInput) {
titleInput.focus()
titleInput.value = title
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 博客园标题填充成功')
} else {
console.log('[COSE] 博客园未找到标题输入框')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 500))
// 博客园使用 id="md-editor" 的 textarea 作为 Markdown 编辑器
const editor = document.querySelector('#md-editor') || document.querySelector('textarea.not-resizable')
if (editor) {
editor.focus()
editor.value = contentToFill
editor.dispatchEvent(new Event('input', { bubbles: true }))
editor.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 博客园内容填充成功')
} else {
console.log('[COSE] 博客园未找到编辑器')
}
}
// InfoQ
else if (host.includes('infoq.cn')) {
// 填充标题
const titleInput = await waitFor('input[placeholder*="标题"], .title-input input, input.article-title')
if (titleInput) {
setInputValue(titleInput, title)
console.log('[COSE] InfoQ 标题填充成功')
} else {
console.log('[COSE] InfoQ 未找到标题输入框')
}
// InfoQ 使用 Vue 编辑器,需要在主世界执行才能访问 __vue__
// 通过注入 script 标签的方式在主世界执行
// 使用 ProseMirror view + 剪贴板粘贴方式填充内容
const script = document.createElement('script')
script.textContent = `
(async function() {
const content = ${JSON.stringify(contentToFill)};
// 等待编辑器完全初始化的函数
const waitForEditor = () => {
return new Promise((resolve) => {
let attempts = 0;
const maxAttempts = 30; // 最多等待 15 秒
const check = () => {
attempts++;
const gkEditor = document.querySelector('.gk-editor');
if (gkEditor && gkEditor.__vue__) {
const vm = gkEditor.__vue__;
const api = vm.editorAPI;
// 检查 ProseMirror view 是否就绪
if (api && api.editor && api.editor.view) {
resolve(api.editor.view);
return;
}
}
if (attempts < maxAttempts) {
setTimeout(check, 500);
} else {
resolve(null);
}
};
check();
});
};
const view = await waitForEditor();
if (!view) {
console.log('[COSE] InfoQ 编辑器初始化超时');
return;
}
try {
// 清空编辑器现有内容
const state = view.state;
const tr = state.tr.delete(0, state.doc.content.size);
view.dispatch(tr);
// 聚焦编辑器
view.focus();
// 使用剪贴板粘贴方式插入内容(会自动解析 Markdown)
const clipboardData = new DataTransfer();
clipboardData.setData('text/plain', content);
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: clipboardData
});
view.dom.dispatchEvent(pasteEvent);
console.log('[COSE] InfoQ 内容填充成功');
} catch (e) {
console.log('[COSE] InfoQ 内容填充失败:', e.message);
}
})();
`
document.head.appendChild(script)
script.remove()
}
// 简书
else if (host.includes('jianshu.com')) {
// 填充标题 - 简书使用 input._24i7u,需要使用 native setter
const titleInput = await waitFor('input._24i7u, input[class*="title"]')
if (titleInput) {
titleInput.focus()
const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set
inputSetter.call(titleInput, title)
titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
titleInput.dispatchEvent(new Event('blur', { bubbles: true }))
console.log('[COSE] 简书标题填充成功')
} else {
console.log('[COSE] 简书未找到标题输入框')
}
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 500))
// 简书使用 textarea#arthur-editor 作为 Markdown 编辑器
const editor = document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR')
if (editor) {
editor.focus()
const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
textareaSetter.call(editor, contentToFill)
editor.dispatchEvent(new InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }))
editor.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] 简书内容填充成功')
} else {
console.log('[COSE] 简书未找到编辑器')
}
}
// 腾讯云开发者社区
else if (host.includes('cloud.tencent.com')) {
console.log('[COSE] TencentCloud 开始同步...')
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 1500))
/**
* 第一步:确保进入 MD 编辑器模式
* 检查是否有"切换 MD 编辑器"按钮:
* - 如果有,说明当前是富文本编辑器,需要点击切换
* - 如果没有(显示"切换 富文本 编辑器"),说明已经是 MD 编辑器
*/
const headerBtns = document.querySelectorAll('.header-btn')
let needSwitch = false
let switchBtn = null
for (const btn of headerBtns) {
if (btn.textContent.includes('切换') && btn.textContent.includes('MD')) {
needSwitch = true
switchBtn = btn
break
}
}
if (needSwitch && switchBtn) {
console.log('[COSE] TencentCloud 检测到富文本编辑器,正在切换到 MD 编辑器...')
switchBtn.click()
// 等待切换完成和 CodeMirror 加载
await new Promise(resolve => setTimeout(resolve, 2000))
} else {
console.log('[COSE] TencentCloud 当前已是 MD 编辑器')
}
/**
* 第二步:等待 CodeMirror 加载完成
*/
let codeMirror = null
const maxWait = 5000
const startTime = Date.now()
while (Date.now() - startTime < maxWait) {
const cm = document.querySelector('.CodeMirror')
if (cm && cm.CodeMirror) {
codeMirror = cm.CodeMirror
break
}
await new Promise(resolve => setTimeout(resolve, 200))
}
if (!codeMirror) {
console.error('[COSE] TencentCloud 错误:CodeMirror 未加载,请刷新页面后重试')
return
}
/**
* 第三步:填充标题
*/
const titleInput = document.querySelector('textarea[placeholder*="标题"]')
if (titleInput && title) {
titleInput.focus()
const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
nativeSetter.call(titleInput, title)
titleInput.dispatchEvent(new Event('input', { bubbles: true }))
titleInput.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] TencentCloud 标题填充成功')
}
/**
* 第四步:填充内容到 CodeMirror
*/
codeMirror.setValue(contentToFill)
console.log('[COSE] TencentCloud 内容填充成功')
}
// Medium
else if (host.includes('medium.com')) {
console.log('[COSE] Medium 开始同步...')
// 等待编辑器加载
await new Promise(resolve => setTimeout(resolve, 2000))
/**
* 第一步:填充标题
* Medium 的标题在 h3.graf--title 元素中
*/
const titleEl = document.querySelector('h3.graf--title')
if (titleEl && title) {
titleEl.focus()
titleEl.textContent = title
titleEl.dispatchEvent(new Event('input', { bubbles: true }))
console.log('[COSE] Medium 标题填充成功')
}
/**
* 第二步:填充内容
* Medium 使用 contenteditable 编辑器,通过 paste 事件注入 HTML 内容
* 使用剪贴板 HTML(带完整样式)或降级到 body
*/
const htmlContent = wechatHtml || body || ''
const contentEl = document.querySelector('p.graf--p')
if (contentEl && htmlContent) {
contentEl.focus()
// 创建 DataTransfer 并设置 HTML 内容
const dt = new DataTransfer()
dt.setData('text/html', htmlContent)
dt.setData('text/plain', htmlContent.replace(/<[^>]*>/g, ''))
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: dt
})
contentEl.dispatchEvent(pasteEvent)
console.log('[COSE] Medium 内容填充成功')
}
}
// 搜狐号 - 由 syncToPlatform 单独处理,这里跳过
else if (host.includes('mp.sohu.com')) {
console.log('[COSE] 搜狐号由 syncToPlatform 处理')
}
// ModelScope 魔搭社区
// 使用仓颉(cangjie)富文本编辑器,需要特殊的事件序列来触发"转为富文本"
else if (host.includes('modelscope.cn')) {
console.log('[COSE] ModelScope 开始同步...')
// 等待页面加载
await new Promise(resolve => setTimeout(resolve, 2000))
const textarea = document.querySelector('textarea')
const cangjieEditor = document.querySelector('[data-cangjie-editable="true"]')
if (textarea) {
// 聚焦到 textarea
textarea.focus()
// 触发 paste 事件(这会设置内容并触发"转为富文本")
// 注意:不要预先设置 textarea.value,否则会导致内容重复
try {
const clipboardData = new DataTransfer()
clipboardData.setData('text/plain', contentToFill)
const pasteEvent = new ClipboardEvent('paste', {
bubbles: true,
cancelable: true,
clipboardData: clipboardData
})
textarea.dispatchEvent(pasteEvent)
} catch (e) {
// 如果 ClipboardEvent 失败,降级到手动设置
const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set
textareaSetter.call(textarea, contentToFill)
textarea.dispatchEvent(new InputEvent('input', {
bubbles: true,
data: contentToFill,
inputType: 'insertText'
}))
}
textarea.dispatchEvent(new Event('change', { bubbles: true }))
console.log('[COSE] ModelScope 内容填充成功')
// 等待"转为富文本"按钮出现并点击
await new Promise(resolve => setTimeout(resolve, 800))
// 查找并点击"转为富文本"按钮
const findAndClickRichTextBtn = () => {
// 使用特定选择器
const richTextBtn = document.querySelector('[data-testid="menu-item-markdownToDoc"][data-role="markdownToDoc"]')
if (richTextBtn) {
console.log('[COSE] ModelScope 找到"转为富文本"按钮,点击中...')
richTextBtn.click()
return true
}
// 降级:查找文本匹配
const allElements = document.querySelectorAll('button, span, div, a, [role="button"]')
for (const el of allElements) {
const text = el.textContent?.trim()
if (text === '转为富文本' || text?.includes('转为富文本')) {
console.log('[COSE] ModelScope 找到"转为富文本"按钮(通过文本),点击中...')
el.click()
return true
}
}
return false
}
// 尝试多次查找
let found = findAndClickRichTextBtn()
for (let i = 0; i < 5 && !found; i++) {
await new Promise(resolve => setTimeout(resolve, 500))
found = findAndClickRichTextBtn()
}
if (found) {
console.log('[COSE] ModelScope 已点击"转为富文本"')
} else {
console.log('[COSE] ModelScope 未找到"转为富文本"按钮(可能已自动转换)')
}
} else if (cangjieEditor) {
// 降级:直接操作仓颉编辑器
cangjieEditor.focus()
document.execCommand('selectAll', false, null)
document.execCommand('insertText', false, contentToFill)
console.log('[COSE] ModelScope 通过 execCommand 填充')
} else {
console.log('[COSE] ModelScope 未找到编辑器')
}
}
// 通用处理
else {
const titleSelectors = ['input[placeholder*="标题"]', 'input[name="title"]', 'textarea[placeholder*="标题"]']
for (const sel of titleSelectors) {
const el = document.querySelector(sel)
if (el) { setInputValue(el, title); break }
}
const contentSelectors = ['.CodeMirror', '.ProseMirror', '.ql-editor', '[contenteditable="true"]', 'textarea']
for (const sel of contentSelectors) {
const el = document.querySelector(sel)
if (el) {
if (el.CodeMirror) {
el.CodeMirror.setValue(contentToFill)
} else {
setInputValue(el, contentToFill)
}
break
}
}
}
console.log('[COSE] 内容已填充,请检查并发布')
}
fill().catch(console.error)
}
// 等待标签页加载
function waitForTab(tabId, timeout = 60000) {
return new Promise((resolve, reject) => {
const start = Date.now()
let urlReady = false
let urlReadyTime = 0
const check = () => {
chrome.tabs.get(tabId, tab => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
return
}
if (tab.status === 'complete') {
setTimeout(resolve, 1500)
return
}
// 如果 URL 已经不是 about:blank/chrome:// 且处于 loading 状态超过 10 秒,
// 说明主文档已加载但第三方资源可能超时,提前 resolve
if (!urlReady && tab.url && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome:')) {
urlReady = true
urlReadyTime = Date.now()
}
if (urlReady && Date.now() - urlReadyTime > 10000) {
console.log('[COSE] waitForTab: 页面 URL 已就绪但 status 仍为 loading,提前继续')
setTimeout(resolve, 1500)
return
}
if (Date.now() - start > timeout) {
console.log('[COSE] waitForTab: 超时,继续执行')
resolve()
} else {
setTimeout(check, 300)
}
})
}
check()
})
}
// 安装时初始化
chrome.runtime.onInstalled.addListener(() => {
console.log('MD 文章同步助手已安装')
})
================================================
FILE: apps/extension/src/content.js
================================================
// Content Script - 在 md.doocs.org 或本地开发环境中运行
// 注入 $cose 全局对象供页面使用
console.log('[COSE Content Script] Loaded!')
console.log('[COSE Content Script] URL:', window.location.href)
console.log('[COSE Content Script] Hostname:', window.location.hostname)
; (function () {
'use strict'
// 华为云开发者博客页面:自动获取并缓存用户信息
if (window.location.hostname.includes('huaweicloud.com')) {
console.log('[COSE] 检测到华为云页面')
setTimeout(async () => {
try {
const response = await fetch('https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info', {
method: 'GET',
credentials: 'include',
headers: { 'Accept': 'application/json' },
})
if (!response.ok) return
const data = await response.json()
if (data && data.memName) {
const userInfo = {
loggedIn: true,
username: data.memAlias || data.memName || '',
avatar: data.memPhoto || '',
cachedAt: Date.now(),
}
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.sendMessage({
type: 'CACHE_USER_INFO',
platform: 'huaweicloud',
userInfo,
}).then(() => {
console.log('[COSE] 华为云用户信息已缓存:', userInfo.username)
}).catch(e => {
console.log('[COSE] 缓存失败:', e.message)
})
}
}
} catch (e) {
console.log('[COSE] 华为云用户信息缓存失败:', e.message)
}
}, 3000) // 华为云 SSO 登录需要几秒延迟
}
// 华为开发者页面:自动获取并缓存用户信息
if (window.location.hostname.includes('developer.huawei.com')) {
console.log('[COSE] 检测到华为开发者页面')
setTimeout(async () => {
try {
// 1. 从 DOM 获取社区用户名(真实昵称,非脱敏手机号)
const userNameEl = document.querySelector('.user_name')
const domUsername = userNameEl ? userNameEl.textContent.trim() : ''
// 2. 从 API 获取头像
const cookies = document.cookie.split(';').map(c => c.trim())
const udCookie = cookies.find(c => c.startsWith('developer_userdata='))
if (!udCookie && !domUsername) return
let avatar = ''
if (udCookie) {
const udValue = decodeURIComponent(udCookie.split('=').slice(1).join('='))
let csrfToken = ''
try {
const udJson = JSON.parse(udValue)
csrfToken = udJson.csrf || udJson.csrftoken || ''
} catch (e) { /* ignore */ }
if (csrfToken) {
const now = new Date()
const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
try {
const response = await fetch('https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'x-hd-csrf': csrfToken,
'x-hd-date': hdDate,
},
body: JSON.stringify({
svc: 'GOpen.User.getInfo',
reqType: 0,
reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }),
}),
})
if (response.ok) {
const data = await response.json()
if (data && data.returnCode === '0' && data.resJson) {
const userRes = JSON.parse(data.resJson)
avatar = userRes.headPictureURL || ''
}
}
} catch (e) { /* avatar fetch failed, continue */ }
}
}
if (domUsername || avatar) {
const userInfo = {
loggedIn: true,
username: domUsername,
avatar,
cachedAt: Date.now(),
}
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.sendMessage({
type: 'CACHE_USER_INFO',
platform: 'huaweidev',
userInfo,
}).then(() => {
console.log('[COSE] 华为开发者用户信息已缓存:', userInfo.username)
}).catch(e => {
console.log('[COSE] 缓存失败:', e.message)
})
}
}
} catch (e) {
console.log('[COSE] 华为开发者用户信息缓存失败:', e.message)
}
}, 3000)
}
// 小红书页面:自动获取并缓存用户信息
if (window.location.hostname.includes('xiaohongshu.com')) {
console.log('[COSE] 检测到小红书页面')
// 通过 background script 处理缓存
setTimeout(async () => {
try {
console.log('[COSE] 开始获取小红书用户信息')
const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', {
method: 'GET',
credentials: 'include',
headers: { 'Accept': 'application/json' }
})
console.log('[COSE] API 响应:', response.status)
if (!response.ok) return
const data = await response.json()
console.log('[COSE] API 数据:', data?.success, data?.code)
if (data?.success === true && data?.code === 0 && data?.data?.userId) {
const userInfo = {
loggedIn: true,
username: data.data.userName || data.data.redId || '',
avatar: data.data.userAvatar || '',
userId: data.data.userId,
cachedAt: Date.now()
}
// 发送给 background script 保存
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.sendMessage({
type: 'CACHE_USER_INFO',
platform: 'xiaohongshu',
userInfo
}).then(() => {
console.log('[COSE] 小红书用户信息已缓存:', userInfo.username)
}).catch(e => {
console.log('[COSE] 缓存失败:', e.message)
})
} else {
console.log('[COSE] Chrome runtime 不可用')
}
}
} catch (e) {
console.log('[COSE] 缓存失败:', e.message)
}
}, 2000)
}
// 支付宝开放平台页面:自动获取并缓存用户信息
if (window.location.hostname.includes('alipay.com')) {
const cacheAlipayUserInfo = async () => {
try {
const response = await fetch('https://developerportal.alipay.com/octopus/service.do', {
method: 'POST',
credentials: 'include',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
},
body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query',
})
if (!response.ok) return
const data = await response.json()
if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) {
const userInfo = {
loggedIn: true,
username: data.data.nickname || '',
avatar: data.data.avatar || '',
cachedAt: Date.now()
}
// 通过 background script 保存
if (typeof chrome !== 'undefined' && chrome.runtime) {
chrome.runtime.sendMessage({
type: 'CACHE_USER_INFO',
platform: 'alipayopen',
userInfo
})
}
console.log('[COSE] 支付宝用户信息已缓存:', userInfo.username)
}
} catch (e) {
console.log('[COSE] 支付宝用户信息缓存失败:', e.message)
}
}
// 页面加载完成后获取用户信息
if (document.readyState === 'complete') {
cacheAlipayUserInfo()
} else {
window.addEventListener('load', cacheAlipayUserInfo)
}
}
// 注入脚本到页面主世界
const script = document.createElement('script')
script.src = chrome.runtime.getURL('bundles/inject.js')
script.onload = function () {
this.remove()
}
; (document.head || document.documentElement).appendChild(script)
// 监听来自页面的消息
window.addEventListener('message', async (event) => {
if (event.source !== window) return
if (!event.data || event.data.source !== 'cose-page') return
const { type, requestId, payload } = event.data
try {
let result
switch (type) {
case 'GET_PLATFORMS':
result = await chrome.runtime.sendMessage({ type: 'GET_PLATFORMS' })
break
case 'CHECK_PLATFORM_STATUS':
result = await chrome.runtime.sendMessage({
type: 'CHECK_PLATFORM_STATUS',
platforms: payload?.platforms,
})
break
case 'CHECK_PLATFORM_STATUS_PROGRESSIVE':
result = await chrome.runtime.sendMessage({
type: 'CHECK_PLATFORM_STATUS_PROGRESSIVE',
platforms: payload?.platforms,
})
break
case 'START_SYNC_BATCH':
result = await chrome.runtime.sendMessage({ type: 'START_SYNC_BATCH' })
break
case 'GET_DEBUG_LOGS':
result = await chrome.runtime.sendMessage({ type: 'GET_DEBUG_LOGS' })
break
case 'SYNC_TO_PLATFORM':
result = await chrome.runtime.sendMessage({
type: 'SYNC_TO_PLATFORM',
platformId: payload?.platformId,
content: payload?.content,
})
break
default:
result = { error: 'Unknown type' }
}
// 发送响应回页面
window.postMessage(
{
source: 'cose-extension',
requestId,
result,
},
'*'
)
} catch (error) {
window.postMessage(
{
source: 'cose-extension',
requestId,
error: error.message,
},
'*'
)
}
})
// 监听来自页面的缓存请求(用于手动触发缓存)
window.addEventListener('message', (event) => {
if (event.source !== window) return
if (event.data.type === 'COSE_CACHE_USER' && event.data.platform === 'xiaohongshu') {
if (typeof chrome !== 'undefined' && chrome.storage) {
console.log('[COSE] 收到缓存请求,保存用户信息')
chrome.storage.local.set({ xiaohongshu_user: event.data.userInfo })
.then(() => console.log('[COSE] 小红书用户信息已手动缓存'))
.catch(e => console.log('[COSE] 缓存失败:', e))
} else {
console.log('[COSE] Chrome API 不可用,无法保存缓存')
}
}
})
// 监听来自 background 的消息并转发到页面(用于渐进式状态更新)
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'PLATFORM_STATUS_UPDATE' || message.type === 'PLATFORM_STATUS_COMPLETE') {
window.postMessage({
source: 'cose-extension',
type: message.type,
...message
}, '*')
}
})
})()
================================================
FILE: apps/extension/src/inject.js
================================================
// 注入到页面主世界的脚本
// 在 window 上暴露 $cose 对象供 Vue 组件使用
; (function () {
'use strict'
let requestId = 0
const pendingRequests = new Map()
// 渐进式更新的回调
let progressiveCallbacks = {
onProgress: null,
onComplete: null
}
// 监听来自 content script 的响应
window.addEventListener('message', (event) => {
if (event.source !== window) return
if (!event.data || event.data.source !== 'cose-extension') return
const { type, requestId: resId, result, error, platformId, platform, completed, total } = event.data
// 处理渐进式平台状态更新
if (type === 'PLATFORM_STATUS_UPDATE') {
if (progressiveCallbacks.onProgress && platform && event.data.result) {
const platformResult = event.data.result
const account = {
uid: platform.id,
type: platform.type,
title: platform.title,
displayName: platformResult.loggedIn ? (platformResult.username || platform.title) : platform.title,
icon: platform.icon,
avatar: platformResult.avatar,
home: platform.url || '',
checked: false,
loggedIn: platformResult.loggedIn || false,
isChecking: false
}
progressiveCallbacks.onProgress(account, completed, total)
}
return
}
// 处理渐进式检测完成
if (type === 'PLATFORM_STATUS_COMPLETE') {
if (progressiveCallbacks.onComplete) {
progressiveCallbacks.onComplete()
}
return
}
const pending = pendingRequests.get(resId)
if (pending) {
pendingRequests.delete(resId)
try {
if (error) {
// 检查是否是扩展上下文失效的错误
if (error.includes && error.includes('Extension context invalidated')) {
console.warn('[COSE] 扩展已重新加载,请刷新页面')
pending.reject(new Error('扩展已重新加载,请刷新页面'))
} else {
pending.reject(new Error(error))
}
} else {
pending.resolve(result)
}
} catch (e) {
console.warn('[COSE] 扩展上下文已失效,请刷新页面')
pending.reject(new Error('扩展上下文已失效,请刷新页面'))
}
}
})
// 发送消息到 content script 并等待响应
function sendMessage(type, payload) {
return new Promise((resolve, reject) => {
const id = ++requestId
pendingRequests.set(id, { resolve, reject })
window.postMessage(
{
source: 'cose-page',
type,
requestId: id,
payload,
},
'*'
)
// 超时处理
setTimeout(() => {
if (pendingRequests.has(id)) {
pendingRequests.delete(id)
reject(new Error('Request timeout'))
}
}, 120000)
})
}
// 平台配置(与 background.js 保持一致)
const PLATFORMS = [
{ id: 'csdn', name: 'CSDN', icon: 'https://g.csdnimg.cn/static/logo/favicon32.ico', title: 'CSDN', type: 'csdn', url: 'https://blog.csdn.net/' },
{ 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/' },
{ 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/' },
{ id: 'zhihu', name: 'Zhihu', icon: 'https://static.zhihu.com/heifetz/favicon.ico', title: '知乎', type: 'zhihu', url: 'https://www.zhihu.com/signin' },
{ 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/' },
{ 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' },
{ id: 'cnblogs', name: 'Cnblogs', icon: 'https://www.cnblogs.com/favicon.ico', title: '博客园', type: 'cnblogs', url: 'https://account.cnblogs.com/signin' },
{ 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' },
{ id: 'cto51', name: '51CTO', icon: 'https://blog.51cto.com/favicon.ico', title: '51CTO', type: 'cto51', url: 'https://home.51cto.com/index' },
{ id: 'infoq', name: 'InfoQ', icon: 'https://static001.infoq.cn/static/write/img/write-favicon.jpg', title: 'InfoQ', type: 'infoq', url: 'https://xie.infoq.cn/' },
{ id: 'jianshu', name: 'Jianshu', icon: 'https://www.jianshu.com/favicon.ico', title: '简书', type: 'jianshu', url: 'https://www.jianshu.com/sign_in' },
{ id: 'baijiahao', name: 'Baijiahao', icon: 'https://pic.rmb.bdstatic.com/10e1e2b43c35577e1315f0f6aad6ba24.vnd.microsoft.icon', title: '百家号', type: 'baijiahao', url: 'https://baijiahao.baidu.com/' },
{ 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/' },
{ id: 'tencentcloud', name: 'TencentCloud', icon: 'https://cloudcache.tencent-cloud.com/qcloud/favicon.ico', title: '腾讯云开发者社区', type: 'tencentcloud', url: 'https://cloud.tencent.com/developer' },
{ id: 'medium', name: 'Medium', icon: 'https://cdn.simpleicons.org/medium', title: 'Medium', type: 'medium', url: 'https://medium.com' },
{ id: 'sspai', name: 'Sspai', icon: 'https://cdn-static.sspai.com/favicon/sspai.ico', title: '少数派', type: 'sspai', url: 'https://sspai.com' },
{ id: 'sohu', name: 'Sohu', icon: 'https://statics.itc.cn/mp-new/icon/1.1/favicon.ico', title: '搜狐号', type: 'sohu', url: 'https://mp.sohu.com' },
{ 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' },
{ id: 'weibo', name: 'Weibo', icon: 'https://weibo.com/favicon.ico', title: '微博头条', type: 'weibo', url: 'https://card.weibo.com/article/v5/editor#/draft' },
{ 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#/' },
{ id: 'huaweicloud', name: 'HuaweiCloud', icon: 'https://www.huaweicloud.com/favicon.ico', title: '华为云开发者博客', type: 'huaweicloud', url: 'https://bbs.huaweicloud.com/blogs/article' },
{ id: 'huaweidev', name: 'HuaweiDev', icon: 'https://developer.huawei.com/favicon.ico', title: '华为开发者文章', type: 'huaweidev', url: 'https://developer.huawei.com/consumer/cn/blog/create' },
{ 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/' },
{ id: 'qianfan', name: 'Qianfan', icon: 'https://bce.bdstatic.com/img/favicon.ico', title: '百度云千帆', type: 'qianfan', url: 'https://qianfan.cloud.baidu.com/qianfandev/topic/create' },
{ id: 'alipayopen', name: 'AlipayOpen', icon: 'https://www.alipay.com/favicon.ico', title: '支付宝开放平台', type: 'alipayopen', url: 'https://open.alipay.com/portal/forum/post/add#article' },
{ 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' },
{ 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' },
{ 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' },
{ 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' },
{ id: 'elecfans', name: 'Elecfans', icon: 'https://www.elecfans.com/favicon.ico', title: '电子发烧友', type: 'elecfans', url: 'https://www.elecfans.com/d/article/md/' },
{ id: 'douban', name: 'Douban', icon: 'https://cdn.simpleicons.org/douban/07C160', title: '豆瓣', type: 'douban', url: 'https://www.douban.com/' },
]
// 暴露 $cose 全局对象
window.$cose = {
// 版本标识
version: '1.0.0',
// 获取支持的平台列表
getPlatforms() {
return PLATFORMS.map(p => ({
...p,
uid: p.id,
displayName: p.title,
home: '',
checked: false,
}))
},
// 获取账号列表(带登录状态)
async getAccounts(callback) {
try {
// 获取登录状态
const result = await sendMessage('CHECK_PLATFORM_STATUS', { platforms: PLATFORMS })
const status = result?.status || {}
const accounts = PLATFORMS.map(p => {
const platformStatus = status[p.id] || {}
const isLoggedIn = platformStatus.loggedIn || false
return {
uid: p.id,
type: p.type,
title: p.title,
displayName: isLoggedIn ? (platformStatus.username || p.title) : p.title,
icon: p.icon,
avatar: platformStatus.avatar,
home: p.url || '',
checked: false,
loggedIn: isLoggedIn,
}
})
if (typeof callback === 'function') {
callback(accounts)
}
return accounts
} catch (error) {
console.error('获取账号列表失败:', error)
// 检测是否是扩展重新加载的错误
if (error.message && (error.message.includes('扩展已重新加载') || error.message.includes('Extension context'))) {
throw new Error('扩展已重新加载,请刷新页面后重试')
}
const accounts = PLATFORMS.map(p => ({
uid: p.id,
type: p.type,
title: p.title,
displayName: p.title,
icon: p.icon,
home: p.url || '',
checked: false,
loggedIn: false,
}))
if (typeof callback === 'function') {
callback(accounts)
}
return accounts
}
},
// 渐进式获取账号列表(每个平台检测完成后立即返回)
// onProgress(account, completed, total) - 每个平台完成时调用
// onComplete() - 所有平台完成时调用
getAccountsProgressive(onProgress, onComplete) {
progressiveCallbacks.onProgress = onProgress
progressiveCallbacks.onComplete = onComplete
// 发送渐进式检测请求
sendMessage('CHECK_PLATFORM_STATUS_PROGRESSIVE', { platforms: PLATFORMS })
.catch(error => {
console.error('[COSE] 渐进式检测启动失败:', error)
// 如果启动失败,调用完成回调
if (typeof onComplete === 'function') {
onComplete()
}
})
},
// 添加发布任务(兼容 wechatsync 的 addTask 接口)
addTask(taskData, onProgress, onComplete) {
const { post, accounts } = taskData
const selectedAccounts = accounts.filter(a => a.checked)
const seenPlatformIds = new Set()
const syncAccounts = []
for (const account of selectedAccounts) {
const platformId = account.uid || account.type
if (!platformId) continue
if (seenPlatformIds.has(platformId)) {
console.log('[COSE] 跳过重复同步平台:', platformId)
continue
}
seenPlatformIds.add(platformId)
syncAccounts.push(account)
}
if (syncAccounts.length === 0) {
if (typeof onComplete === 'function') onComplete()
return
}
// 初始化状态
const status = {
accounts: syncAccounts.map(a => ({
...a,
status: 'pending',
msg: '等待中',
})),
}
if (typeof onProgress === 'function') {
onProgress(status)
}
// 依次同步到各平台
const syncAll = async () => {
// 开始新的同步批次,将所有 tab 放入一个 group
await sendMessage('START_SYNC_BATCH', {})
// 检查是否需要同步到微信公众号或百家号或网易号或 Medium 或少数派或B站专栏或微博头条或小红书(需要使用剪贴板 HTML)
const hasWechat = syncAccounts.some(a => (a.uid || a.type) === 'wechat')
const hasBaijiahao = syncAccounts.some(a => (a.uid || a.type) === 'baijiahao')
const hasWangyihao = syncAccounts.some(a => (a.uid || a.type) === 'wangyihao')
const hasMedium = syncAccounts.some(a => (a.uid || a.type) === 'medium')
const hasSspai = syncAccounts.some(a => (a.uid || a.type) === 'sspai')
const hasBilibili = syncAccounts.some(a => (a.uid || a.type) === 'bilibili')
const hasWeibo = syncAccounts.some(a => (a.uid || a.type) === 'weibo')
const hasXiaohongshu = syncAccounts.some(a => (a.uid || a.type) === 'xiaohongshu')
let clipboardHtmlContent = null
if (hasWechat || hasBaijiahao || hasWangyihao || hasMedium || hasSspai || hasBilibili || hasWeibo || hasXiaohongshu) {
// 先点击复制按钮,将带样式的内容复制到剪贴板
const copyBtn = document.querySelector('.copy-btn') ||
document.querySelector('button[class*="copy"]') ||
document.querySelector('button:has(.lucide-copy)') ||
Array.from(document.querySelectorAll('button')).find(b => b.textContent.includes('复制'))
if (copyBtn && typeof copyBtn.click === 'function') {
copyBtn.click()
// 等待复制完成
await new Promise(resolve => setTimeout(resolve, 2000))
// 读取剪贴板中的 HTML 内容
try {
const clipboardItems = await navigator.clipboard.read()
for (const item of clipboardItems) {
if (item.types.includes('text/html')) {
const blob = await item.getType('text/html')
clipboardHtmlContent = await blob.text()
console.log('[COSE] 已读取剪贴板 HTML 内容,长度:', clipboardHtmlContent.length)
break
}
}
} catch (e) {
console.log('[COSE] 读取剪贴板失败:', e.message)
}
}
}
for (let i = 0; i < syncAccounts.length; i++) {
const account = syncAccounts[i]
status.accounts[i].status = 'uploading'
status.accounts[i].msg = '同步中...'
if (typeof onProgress === 'function') onProgress({ ...status })
try {
const platformId = account.uid || account.type
const result = await sendMessage('SYNC_TO_PLATFORM', {
platformId,
content: {
title: post.title,
body: post.content,
markdown: post.markdown,
thumb: post.thumb,
desc: post.desc,
// 微信公众号、百家号、网易号、Medium、少数派、B站专栏、微博头条和小红书使用剪贴板中带样式的 HTML
wechatHtml: (platformId === 'wechat' || platformId === 'baijiahao' || platformId === 'wangyihao' || platformId === 'medium' || platfor
gitextract_uy5ndwh6/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── 01-bug-report.yml │ │ ├── 02-feature-request.yml │ │ └── 03-platform-request.yml │ └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── PRIVACY.md ├── README.md ├── apps/ │ └── extension/ │ ├── manifest.json │ ├── package.json │ ├── scripts/ │ │ ├── cli.ts │ │ ├── convert-icons.mjs │ │ └── reload-extension.mjs │ ├── src/ │ │ ├── background.js │ │ ├── content.js │ │ ├── inject.js │ │ ├── offscreen.html │ │ ├── offscreen.js │ │ ├── popup.html │ │ └── popup.js │ ├── tsconfig.json │ └── vite.config.js ├── package.json ├── packages/ │ ├── core/ │ │ ├── index.js │ │ ├── package.json │ │ └── src/ │ │ ├── platforms/ │ │ │ ├── alipayopen.js │ │ │ ├── aliyun.js │ │ │ ├── baijiahao.js │ │ │ ├── bilibili.js │ │ │ ├── cnblogs.js │ │ │ ├── common.js │ │ │ ├── csdn.js │ │ │ ├── cto51.js │ │ │ ├── douban.js │ │ │ ├── douyin.js │ │ │ ├── elecfans.js │ │ │ ├── huaweicloud.js │ │ │ ├── huaweidev.js │ │ │ ├── index.js │ │ │ ├── infoq.js │ │ │ ├── jianshu.js │ │ │ ├── juejin.js │ │ │ ├── medium.js │ │ │ ├── modelscope.js │ │ │ ├── oschina.js │ │ │ ├── qianfan.js │ │ │ ├── segmentfault.js │ │ │ ├── sohu.js │ │ │ ├── sspai.js │ │ │ ├── tencentcloud.js │ │ │ ├── toutiao.js │ │ │ ├── twitter.js │ │ │ ├── volcengine.js │ │ │ ├── wangyihao.js │ │ │ ├── wechat.js │ │ │ ├── weibo.js │ │ │ ├── xiaohongshu.js │ │ │ └── zhihu.js │ │ └── utils.js │ └── detection/ │ ├── index.js │ ├── package.json │ └── src/ │ ├── configs.js │ ├── detect.js │ ├── platforms/ │ │ ├── alipay.js │ │ ├── aliyun.js │ │ ├── bilibili.js │ │ ├── cnblogs.js │ │ ├── csdn.js │ │ ├── cto51.js │ │ ├── douban.js │ │ ├── elecfans.js │ │ ├── huaweicloud.js │ │ ├── huaweidev.js │ │ ├── infoq.js │ │ ├── jianshu.js │ │ ├── medium.js │ │ ├── modelscope.js │ │ ├── oschina.js │ │ ├── qianfan.js │ │ ├── segmentfault.js │ │ ├── sohu.js │ │ ├── sspai.js │ │ ├── tencentcloud.js │ │ ├── twitter.js │ │ ├── volcengine.js │ │ ├── wangyihao.js │ │ ├── wechat.js │ │ ├── weibo.js │ │ └── xiaohongshu.js │ └── utils.js └── pnpm-workspace.yaml
SYMBOL INDEX (121 symbols across 60 files)
FILE: apps/extension/scripts/cli.ts
type FirefoxBackgroundOptions (line 16) | interface FirefoxBackgroundOptions {
type ChromeBackgroundOptions (line 21) | interface ChromeBackgroundOptions {
type Manifest (line 27) | interface Manifest {
type BuildOptions (line 67) | interface BuildOptions {
type CopyEntry (line 90) | interface CopyEntry {
FILE: apps/extension/scripts/convert-icons.mjs
function createPlaceholderPng (line 14) | function createPlaceholderPng(size) {
function main (line 27) | async function main() {
FILE: apps/extension/scripts/reload-extension.mjs
constant CDP_URL (line 13) | const CDP_URL = 'http://127.0.0.1:9222'
function ts (line 17) | function ts() {
function reloadExtension (line 21) | async function reloadExtension() {
function debounceReload (line 106) | function debounceReload() {
FILE: apps/extension/src/background.js
function ensureOffscreen (line 10) | async function ensureOffscreen() {
function _waitForOffscreenReady (line 35) | async function _waitForOffscreenReady(timeoutMs = 3000) {
function warmUpFetch (line 54) | async function warmUpFetch(url) {
function offscreenApiFetch (line 73) | async function offscreenApiFetch(url, options = {}) {
function sendOffscreenMessage (line 100) | function sendOffscreenMessage(msg, timeoutMs = 15000) {
function tabContextFetch (line 124) | async function tabContextFetch(siteUrl, apiUrl, options = {}) {
function detectCto51ViaOffscreen (line 215) | async function detectCto51ViaOffscreen() {
function detectCnblogsViaOffscreen (line 250) | async function detectCnblogsViaOffscreen() {
function detectXiaohongshuViaOffscreen (line 270) | async function detectXiaohongshuViaOffscreen() {
function initDynamicRules (line 287) | async function initDynamicRules() {
constant PLATFORM_USER_INFO (line 356) | const PLATFORM_USER_INFO = {}
function getOrCreateSyncGroup (line 359) | async function getOrCreateSyncGroup(windowId) {
function addTabToSyncGroup (line 379) | async function addTabToSyncGroup(tabId, windowId) {
function logToStorage (line 404) | async function logToStorage(msg, data = null) {
function handleMessage (line 445) | async function handleMessage(request, sender) {
function checkAllPlatforms (line 492) | async function checkAllPlatforms(platforms) {
function checkAllPlatformsProgressive (line 519) | async function checkAllPlatformsProgressive(platforms, tabId) {
function checkPlatformLogin (line 588) | async function checkPlatformLogin(platform) {
function pasteWithDebugger (line 594) | async function pasteWithDebugger(tabId) {
function syncToPlatform (line 657) | async function syncToPlatform(platformId, content) {
function fillContentOnPage (line 3217) | function fillContentOnPage(content, platformId) {
function waitForTab (line 3781) | function waitForTab(tabId, timeout = 60000) {
FILE: apps/extension/src/inject.js
function sendMessage (line 75) | function sendMessage(type, payload) {
method getPlatforms (line 141) | getPlatforms() {
method getAccounts (line 152) | async getAccounts(callback) {
method getAccountsProgressive (line 204) | getAccountsProgressive(onProgress, onComplete) {
method addTask (line 220) | addTask(taskData, onProgress, onComplete) {
FILE: apps/extension/src/offscreen.js
function handleFetch (line 53) | async function handleFetch(payload) {
function handleWarmFetch (line 72) | async function handleWarmFetch(payload) {
function handleApiFetch (line 97) | async function handleApiFetch(payload) {
function handleDetectCto51 (line 125) | async function handleDetectCto51() {
function handleDetectCnblogs (line 165) | async function handleDetectCnblogs() {
function handleDetectXiaohongshu (line 194) | async function handleDetectXiaohongshu() {
FILE: packages/core/src/platforms/alipayopen.js
function fillAlipayOpenContent (line 19) | function fillAlipayOpenContent(title, markdown) {
FILE: packages/core/src/platforms/aliyun.js
function fillAliyunContent (line 13) | async function fillAliyunContent(content) {
FILE: packages/core/src/platforms/baijiahao.js
function fillBaijiahaoContent (line 13) | async function fillBaijiahaoContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/bilibili.js
function fillBilibiliContent (line 15) | async function fillBilibiliContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/cnblogs.js
function fillCnblogsContent (line 13) | async function fillCnblogsContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/csdn.js
function fillCSDNContent (line 17) | function fillCSDNContent(title, markdown, body) {
function syncCSDNContent (line 69) | async function syncCSDNContent(tab, content, helpers) {
FILE: packages/core/src/platforms/cto51.js
function fillCTO51Content (line 14) | async function fillCTO51Content(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/index.js
constant PLATFORMS (line 39) | const PLATFORMS = [
function getPlatformFiller (line 74) | function getPlatformFiller(hostname) {
constant SYNC_HANDLERS (line 112) | const SYNC_HANDLERS = {
FILE: packages/core/src/platforms/infoq.js
function fillInfoQContent (line 15) | async function fillInfoQContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/jianshu.js
function fillJianshuContent (line 13) | async function fillJianshuContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/juejin.js
function fillJuejinContent (line 16) | function fillJuejinContent(title, markdown, body) {
function syncJuejinContent (line 63) | async function syncJuejinContent(tab, content, helpers) {
FILE: packages/core/src/platforms/medium.js
function fillMediumContent (line 21) | async function fillMediumContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/oschina.js
function fillOSChinaContent (line 13) | async function fillOSChinaContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/qianfan.js
function qianfanIntercept (line 19) | function qianfanIntercept() {
FILE: packages/core/src/platforms/segmentfault.js
function fillSegmentFaultContent (line 13) | async function fillSegmentFaultContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/sohu.js
function fillSohuContent (line 14) | async function fillSohuContent(content, waitFor) {
FILE: packages/core/src/platforms/sspai.js
function fillSspaiContent (line 14) | async function fillSspaiContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/tencentcloud.js
function findSwitchToMDButton (line 19) | function findSwitchToMDButton() {
function ensureMarkdownEditor (line 38) | async function ensureMarkdownEditor() {
function getCodeMirror (line 91) | async function getCodeMirror(maxWait = 3000) {
function fillTencentCloudContent (line 110) | async function fillTencentCloudContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/toutiao.js
function fillToutiaoContentInPage (line 15) | function fillToutiaoContentInPage(title, body) {
function syncToutiaoContent (line 110) | async function syncToutiaoContent(tab, content, helpers) {
FILE: packages/core/src/platforms/twitter.js
function createTwitterRenderer (line 20) | function createTwitterRenderer(marked) {
function processLatexFormulas (line 134) | function processLatexFormulas(markdown) {
function convertMarkdownToTwitterHtml (line 157) | function convertMarkdownToTwitterHtml(markdown, marked) {
function fillTwitterContent (line 186) | async function fillTwitterContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/wangyihao.js
function fillWangyihaoContent (line 16) | function fillWangyihaoContent(title, htmlBody) {
function syncWangyihaoContent (line 80) | async function syncWangyihaoContent(tab, content, helpers) {
FILE: packages/core/src/platforms/wechat.js
function fillWechatContent (line 17) | async function fillWechatContent(title, htmlBody) {
function saveWechatDraft (line 130) | function saveWechatDraft() {
function syncWechatContent (line 148) | async function syncWechatContent(tab, content, helpers) {
FILE: packages/core/src/platforms/xiaohongshu.js
function fillXiaohongshuContent (line 15) | async function fillXiaohongshuContent(content, waitFor, setInputValue) {
FILE: packages/core/src/platforms/zhihu.js
function fillZhihuContent (line 17) | function fillZhihuContent(title, markdown) {
function syncZhihuContent (line 211) | async function syncZhihuContent(tab, content, helpers) {
function waitForImageUploadComplete (line 275) | async function waitForImageUploadComplete(tabId, timeout = 30000) {
FILE: packages/core/src/utils.js
function injectCommonUtils (line 7) | function injectCommonUtils() {
function injectUtils (line 63) | async function injectUtils(chrome, tabId) {
FILE: packages/detection/src/configs.js
constant LOGIN_CHECK_CONFIG (line 68) | const LOGIN_CHECK_CONFIG = {
FILE: packages/detection/src/detect.js
constant PLATFORM_DETECTORS (line 31) | const PLATFORM_DETECTORS = {
function detectUser (line 60) | async function detectUser(platformId) {
FILE: packages/detection/src/platforms/alipay.js
function detectAlipayUser (line 7) | async function detectAlipayUser() {
FILE: packages/detection/src/platforms/aliyun.js
function detectAliyunUser (line 10) | async function detectAliyunUser() {
FILE: packages/detection/src/platforms/bilibili.js
function detectBilibiliUser (line 10) | async function detectBilibiliUser() {
FILE: packages/detection/src/platforms/cnblogs.js
function detectCnblogsUser (line 8) | async function detectCnblogsUser() {
FILE: packages/detection/src/platforms/csdn.js
function detectCSDNUser (line 10) | async function detectCSDNUser() {
FILE: packages/detection/src/platforms/cto51.js
function detectCTO51User (line 8) | async function detectCTO51User() {
FILE: packages/detection/src/platforms/douban.js
function convertToBase64WithFallback (line 3) | async function convertToBase64WithFallback(avatarUrl) {
function detectDoubanUser (line 53) | async function detectDoubanUser() {
FILE: packages/detection/src/platforms/elecfans.js
function detectElecfansUser (line 9) | async function detectElecfansUser() {
FILE: packages/detection/src/platforms/huaweicloud.js
constant HUAWEICLOUD_API (line 3) | const HUAWEICLOUD_API = 'https://devdata.huaweicloud.com/rest/developer/...
function detectHuaweiCloudUser (line 9) | async function detectHuaweiCloudUser() {
FILE: packages/detection/src/platforms/huaweidev.js
function detectHuaweiDevUser (line 11) | async function detectHuaweiDevUser() {
FILE: packages/detection/src/platforms/infoq.js
function detectInfoQUser (line 8) | async function detectInfoQUser() {
FILE: packages/detection/src/platforms/jianshu.js
function detectJianshuUser (line 7) | async function detectJianshuUser() {
FILE: packages/detection/src/platforms/medium.js
function detectMediumUser (line 7) | async function detectMediumUser() {
FILE: packages/detection/src/platforms/modelscope.js
function detectModelScopeUser (line 11) | async function detectModelScopeUser() {
FILE: packages/detection/src/platforms/oschina.js
function detectOSChinaUser (line 9) | async function detectOSChinaUser() {
FILE: packages/detection/src/platforms/qianfan.js
function detectQianfanUser (line 9) | async function detectQianfanUser() {
FILE: packages/detection/src/platforms/segmentfault.js
function detectSegmentFaultUser (line 8) | async function detectSegmentFaultUser() {
FILE: packages/detection/src/platforms/sohu.js
function detectSohuUser (line 7) | async function detectSohuUser() {
FILE: packages/detection/src/platforms/sspai.js
function detectSspaiUser (line 7) | async function detectSspaiUser() {
FILE: packages/detection/src/platforms/tencentcloud.js
function detectTencentCloudUser (line 9) | async function detectTencentCloudUser() {
FILE: packages/detection/src/platforms/twitter.js
function detectTwitterUser (line 7) | async function detectTwitterUser() {
FILE: packages/detection/src/platforms/volcengine.js
function detectVolcengineUser (line 10) | async function detectVolcengineUser() {
FILE: packages/detection/src/platforms/wangyihao.js
function detectWangyihaoUser (line 10) | async function detectWangyihaoUser() {
FILE: packages/detection/src/platforms/wechat.js
function detectWechatUser (line 8) | async function detectWechatUser() {
FILE: packages/detection/src/platforms/weibo.js
function detectWeiboUser (line 9) | async function detectWeiboUser() {
FILE: packages/detection/src/platforms/xiaohongshu.js
function detectXiaohongshuUser (line 8) | async function detectXiaohongshuUser() {
FILE: packages/detection/src/utils.js
function convertAvatarToBase64 (line 8) | async function convertAvatarToBase64(avatarUrl, referer) {
function logToStorage (line 31) | async function logToStorage(msg, data = null) {
function checkLoginByCookie (line 40) | async function checkLoginByCookie(platformId, config) {
function detectByApi (line 104) | async function detectByApi(platformId, config) {
Condensed preview — 90 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (432K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/01-bug-report.yml",
"chars": 3224,
"preview": "name: Bug Report / 问题报告\ndescription: Report a bug or issue with the extension / 报告扩展的 bug 或问题\ntitle: \"[Bug]: \"\nlabels: ["
},
{
"path": ".github/ISSUE_TEMPLATE/02-feature-request.yml",
"chars": 2463,
"preview": "name: Feature Request / 功能请求\ndescription: Suggest a new feature or enhancement / 建议新功能或改进\ntitle: \"[Feature]: \"\nlabels: ["
},
{
"path": ".github/ISSUE_TEMPLATE/03-platform-request.yml",
"chars": 1326,
"preview": "name: Platform Request / 平台支持\ndescription: Request support for a new platform / 请求支持新平台\ntitle: \"[Platform]: \"\nlabels: [\""
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 2180,
"preview": "## Summary / 概述\n<!-- Provide a brief description of the changes in this PR -->\n<!-- 简要描述此 PR 中的更改 -->\n\n\n\n## Related Issu"
},
{
"path": ".gitignore",
"chars": 93,
"preview": "# 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",
"chars": 2849,
"preview": "# Privacy Policy for COSE - 多平台文章同步\n\n**Last Updated: December 13, 2025**\n\n## Overview\n\nCOSE (Create Once, Sync Everywher"
},
{
"path": "README.md",
"chars": 6227,
"preview": "<div align=\"center\">\n <picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"assets/headerDark.svg\" />\n "
},
{
"path": "apps/extension/manifest.json",
"chars": 2456,
"preview": "{\n \"manifest_version\": 3,\n \"name\": \"COSE - 多平台文章同步\",\n \"description\": \"Create Once, Sync Everywhere. 一键将文章同步到多个平台\",\n "
},
{
"path": "apps/extension/package.json",
"chars": 1334,
"preview": "{\n \"name\": \"cose-extension\",\n \"version\": \"1.3.4\",\n \"description\": \"Create Once, Sync Everywhere. 一键将文章同步到多个平台\",\n \"ty"
},
{
"path": "apps/extension/scripts/cli.ts",
"chars": 8038,
"preview": "import { cac } from 'cac'\nimport { execa } from 'execa'\nimport { build as viteBuild } from 'vite'\nimport fs from 'node:f"
},
{
"path": "apps/extension/scripts/convert-icons.mjs",
"chars": 1904,
"preview": "#!/usr/bin/env node\n/**\n * 将 SVG 图标转换为 PNG\n * 需要安装: npm install sharp\n */\nimport { readFileSync, writeFileSync, existsSy"
},
{
"path": "apps/extension/scripts/reload-extension.mjs",
"chars": 4349,
"preview": "#!/usr/bin/env node\n/**\n * 监听 dist 目录变化,自动刷新 Chrome 扩展\n */\n\nimport { watch } from 'fs'\nimport { join, dirname } from 'pa"
},
{
"path": "apps/extension/src/background.js",
"chars": 135572,
"preview": "// 平台配置\nimport { PLATFORMS, LOGIN_CHECK_CONFIG, SYNC_HANDLERS } from '@cose/core/src/platforms/index.js'\nimport { qianfa"
},
{
"path": "apps/extension/src/content.js",
"chars": 11045,
"preview": "// Content Script - 在 md.doocs.org 或本地开发环境中运行\n// 注入 $cose 全局对象供页面使用\n\nconsole.log('[COSE Content Script] Loaded!')\nconsol"
},
{
"path": "apps/extension/src/inject.js",
"chars": 15844,
"preview": "// 注入到页面主世界的脚本\n// 在 window 上暴露 $cose 对象供 Vue 组件使用\n\n; (function () {\n 'use strict'\n\n let requestId = 0\n const pendingR"
},
{
"path": "apps/extension/src/offscreen.html",
"chars": 148,
"preview": "<!DOCTYPE html>\n<html>\n<head><title>COSE Offscreen</title></head>\n<body>\n<script src=\"bundles/offscreen.js\" type=\"module"
},
{
"path": "apps/extension/src/offscreen.js",
"chars": 6659,
"preview": "// Offscreen document for making fetch requests with cookies\n// This runs in a document context where credentials: 'incl"
},
{
"path": "apps/extension/src/popup.html",
"chars": 1661,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <style>\n * {\n margin: 0;\n padding: 0;\n box-si"
},
{
"path": "apps/extension/src/popup.js",
"chars": 205,
"preview": "// Popup script for COSE extension\ndocument.getElementById('openOfficial').addEventListener('click', (e) => {\n e.preven"
},
{
"path": "apps/extension/tsconfig.json",
"chars": 300,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ESNext\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"bundler\""
},
{
"path": "apps/extension/vite.config.js",
"chars": 1759,
"preview": "import { defineConfig } from 'vite'\nimport { join, resolve } from 'path'\nimport { viteStaticCopy } from 'vite-plugin-sta"
},
{
"path": "package.json",
"chars": 464,
"preview": "{\n \"name\": \"cose-monorepo\",\n \"private\": true,\n \"scripts\": {\n \"build\": \"pnpm -C apps/extension build\",\n \"build:f"
},
{
"path": "packages/core/index.js",
"chars": 184,
"preview": "export * from './src/utils.js';\n// Re-export login detection from @cose/detection\nexport * from '@cose/detection';\n// Pl"
},
{
"path": "packages/core/package.json",
"chars": 357,
"preview": "{\n \"name\": \"@cose/core\",\n \"version\": \"1.0.0\",\n \"description\": \"Core publishing logic for COSE\",\n \"main\": \"in"
},
{
"path": "packages/core/src/platforms/alipayopen.js",
"chars": 3194,
"preview": "// 支付宝开放平台配置\nconst AlipayOpenPlatform = {\n id: 'alipayopen',\n name: 'AlipayOpen',\n icon: 'https://www.alipay.com/favi"
},
{
"path": "packages/core/src/platforms/aliyun.js",
"chars": 1580,
"preview": "// 阿里云开发者社区平台配置\nconst AliyunPlatform = {\n id: 'aliyun',\n name: 'Aliyun',\n icon: 'https://img.alicdn.com/tfs/TB1_ZXuNc"
},
{
"path": "packages/core/src/platforms/baijiahao.js",
"chars": 3165,
"preview": "// 百家号平台配置\nconst BaijiahaoPlat = {\n id: 'baijiahao',\n name: 'Baijiahao',\n icon: 'https://pic.rmb.bdstatic.com/10e1e2b"
},
{
"path": "packages/core/src/platforms/bilibili.js",
"chars": 618,
"preview": "// B站专栏平台配置(使用旧版编辑器,基于 UEditor)\n// 同步方式:使用 UEditor execCommand('inserthtml') 插入 HTML\nconst BilibiliPlatform = {\n id: 'b"
},
{
"path": "packages/core/src/platforms/cnblogs.js",
"chars": 1654,
"preview": "// 博客园平台配置\nconst CnblogsPlatform = {\n id: 'cnblogs',\n name: 'Cnblogs',\n icon: 'https://www.cnblogs.com/favicon.ico',\n"
},
{
"path": "packages/core/src/platforms/common.js",
"chars": 124,
"preview": "// Re-export from utils.js for backward compatibility\nexport * from '../utils.js'\nexport { injectUtils } from '../utils."
},
{
"path": "packages/core/src/platforms/csdn.js",
"chars": 2871,
"preview": "// CSDN 平台配置\nconst CSDNPlatform = {\n id: 'csdn',\n name: 'CSDN',\n icon: 'https://g.csdnimg.cn/static/logo/favicon32.ic"
},
{
"path": "packages/core/src/platforms/cto51.js",
"chars": 2485,
"preview": "// 51CTO 平台配置\nconst CTO51Platform = {\n id: 'cto51',\n name: '51CTO',\n icon: 'https://blog.51cto.com/favicon.ico'"
},
{
"path": "packages/core/src/platforms/douban.js",
"chars": 264,
"preview": "// 豆瓣平台配置\nconst DoubanPlatform = {\n id: 'douban',\n name: 'Douban',\n icon: 'https://cdn.simpleicons.org/douban/07C160'"
},
{
"path": "packages/core/src/platforms/douyin.js",
"chars": 455,
"preview": "// 抖音创作者平台配置\nconst DouyinPlatform = {\n id: 'douyin',\n name: 'Douyin',\n icon: 'https://lf3-static.bytednsdoc.com/obj/e"
},
{
"path": "packages/core/src/platforms/elecfans.js",
"chars": 265,
"preview": "// 电子发烧友平台配置\n\nexport const ElecfansPlatform = {\n id: 'elecfans',\n name: '电子发烧友',\n icon: 'https://www.elecfans.com/fav"
},
{
"path": "packages/core/src/platforms/huaweicloud.js",
"chars": 330,
"preview": "// 华为云开发者博客平台配置\nconst HuaweiCloudPlatform = {\n id: 'huaweicloud',\n name: 'HuaweiCloud',\n icon: 'https://www.huaweiclo"
},
{
"path": "packages/core/src/platforms/huaweidev.js",
"chars": 338,
"preview": "// 华为开发者文章平台配置\nconst HuaweiDevPlatform = {\n id: 'huaweidev',\n name: 'HuaweiDev',\n icon: 'https://developer.huawei.com"
},
{
"path": "packages/core/src/platforms/index.js",
"chars": 4833,
"preview": "// 平台配置汇总\n// 从 @cose/detection 导入登录检测配置\nimport { LOGIN_CHECK_CONFIG } from '@cose/detection'\n\n// 平台元数据和同步函数从各平台文件导入\nimpo"
},
{
"path": "packages/core/src/platforms/infoq.js",
"chars": 1769,
"preview": "// InfoQ 平台配置\nconst InfoQPlatform = {\n id: 'infoq',\n name: 'InfoQ',\n icon: 'https://static001.infoq.cn/static/w"
},
{
"path": "packages/core/src/platforms/jianshu.js",
"chars": 1909,
"preview": "// 简书平台配置\nconst JianshuPlatform = {\n id: 'jianshu',\n name: 'Jianshu',\n icon: 'https://www.jianshu.com/favicon.ico',\n "
},
{
"path": "packages/core/src/platforms/juejin.js",
"chars": 2712,
"preview": "// 掘金平台配置\nconst JuejinPlatform = {\n id: 'juejin',\n name: 'Juejin',\n icon: 'https://lf-web-assets.juejin.cn/obj/juejin"
},
{
"path": "packages/core/src/platforms/medium.js",
"chars": 1635,
"preview": "// Medium 平台配置\nconst MediumPlatform = {\n id: 'medium',\n name: 'Medium',\n icon: 'https://cdn.simpleicons.org/med"
},
{
"path": "packages/core/src/platforms/modelscope.js",
"chars": 409,
"preview": "// ModelScope 魔搭社区平台配置\n// 编辑器支持 Markdown,注入后需要点击\"转为富文本\"按钮\n\nconst ModelScopePlatform = {\n id: 'modelscope',\n name: 'Mod"
},
{
"path": "packages/core/src/platforms/oschina.js",
"chars": 2910,
"preview": "// OSChina 平台配置\nconst OSChinaPlatform = {\n id: 'oschina',\n name: 'OSChina',\n icon: 'https://wsrv.nl/?url=static"
},
{
"path": "packages/core/src/platforms/qianfan.js",
"chars": 5515,
"preview": "// 百度千帆开发者社区平台配置\n// 编辑器支持 Markdown 自动转换功能\n\nconst QianfanPlatform = {\n id: 'qianfan',\n name: 'Qianfan',\n icon: 'https:"
},
{
"path": "packages/core/src/platforms/segmentfault.js",
"chars": 1552,
"preview": "// 思否平台配置\nconst SegmentFaultPlatform = {\n id: 'segmentfault',\n name: 'SegmentFault',\n icon: 'https://fastly.jsdelivr."
},
{
"path": "packages/core/src/platforms/sohu.js",
"chars": 486,
"preview": "// 搜狐号平台配置\nconst SohuPlatform = {\n id: 'sohu',\n name: 'Sohu',\n icon: 'https://statics.itc.cn/mp-new/icon/1.1/favicon."
},
{
"path": "packages/core/src/platforms/sspai.js",
"chars": 1887,
"preview": "// 少数派平台配置\nconst SspaiPlatform = {\n id: 'sspai',\n name: 'Sspai',\n icon: 'https://cdn-static.sspai.com/favicon/sspai.i"
},
{
"path": "packages/core/src/platforms/tencentcloud.js",
"chars": 4479,
"preview": "// 腾讯云开发者平台配置\nconst TencentCloudPlatform = {\n id: 'tencentcloud',\n name: 'TencentCloud',\n icon: 'https://cloudc"
},
{
"path": "packages/core/src/platforms/toutiao.js",
"chars": 4072,
"preview": "// 今日头条平台配置\nconst ToutiaoPlatform = {\n id: 'toutiao',\n name: 'Toutiao',\n icon: 'https://sf3-cdn-tos.toutiaostatic.com"
},
{
"path": "packages/core/src/platforms/twitter.js",
"chars": 7914,
"preview": "// Twitter Articles 平台配置\n// 使用 marked 进行 Markdown 解析,并转换为 Twitter Articles 支持的格式\n\nconst TwitterPlatform = {\n id: 'twitt"
},
{
"path": "packages/core/src/platforms/volcengine.js",
"chars": 349,
"preview": "// 火山引擎开发者社区平台配置\nconst VolcenginePlatform = {\n id: 'volcengine',\n name: 'Volcengine',\n icon: 'https://lf1-cdn-tos.byt"
},
{
"path": "packages/core/src/platforms/wangyihao.js",
"chars": 3562,
"preview": "// 网易号平台配置\nconst WangyihaoPlatform = {\n id: 'wangyihao',\n name: 'Wangyihao',\n icon: 'https://static.ws.126.net/163/f2"
},
{
"path": "packages/core/src/platforms/wechat.js",
"chars": 8896,
"preview": "import { injectUtils } from './common.js'\n\n// 微信公众号平台配置\nconst WechatPlatform = {\n id: 'wechat',\n name: 'WeChat',\n ico"
},
{
"path": "packages/core/src/platforms/weibo.js",
"chars": 272,
"preview": "// 微博头条文章平台配置\nconst WeiboPlatform = {\n id: 'weibo',\n name: 'Weibo',\n icon: 'https://weibo.com/favicon.ico',\n url: 'h"
},
{
"path": "packages/core/src/platforms/xiaohongshu.js",
"chars": 571,
"preview": "// 小红书平台配置\n// 同步方式:使用剪贴板 HTML 粘贴到编辑器\nconst XiaohongshuPlatform = {\n id: 'xiaohongshu',\n name: 'Xiaohongshu',\n icon: '"
},
{
"path": "packages/core/src/platforms/zhihu.js",
"chars": 12943,
"preview": "// 知乎平台配置\nconst ZhihuPlatform = {\n id: 'zhihu',\n name: 'Zhihu',\n icon: 'https://static.zhihu.com/heifetz/favicon.ico'"
},
{
"path": "packages/core/src/utils.js",
"chars": 1948,
"preview": "// 通用平台工具函数\n\n/**\n * 注入通用工具函数到页面主世界\n * 此函数会在页面中定义 window.waitFor 和 window.setInputValue\n */\nfunction injectCommonUtils() "
},
{
"path": "packages/detection/index.js",
"chars": 96,
"preview": "export * from './src/configs.js'\nexport * from './src/detect.js'\nexport * from './src/utils.js'\n"
},
{
"path": "packages/detection/package.json",
"chars": 258,
"preview": "{\n \"name\": \"@cose/detection\",\n \"version\": \"1.0.0\",\n \"description\": \"Platform login detection for COSE\",\n \"ma"
},
{
"path": "packages/detection/src/configs.js",
"chars": 2388,
"preview": "/**\n * @cose/detection - Platform login detection module\n * \n * This package provides login detection configurations for"
},
{
"path": "packages/detection/src/detect.js",
"chars": 3365,
"preview": "import { LOGIN_CHECK_CONFIG } from './configs.js'\nimport { checkLoginByCookie, detectByApi } from './utils.js'\nimport { "
},
{
"path": "packages/detection/src/platforms/alipay.js",
"chars": 3692,
"preview": "/**\n * Alipay Open platform detection logic\n * Strategy:\n * 1. Check chrome.storage.local cache (1 hour TTL)\n * 2. If ca"
},
{
"path": "packages/detection/src/platforms/aliyun.js",
"chars": 1186,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Aliyun Developer platform detection logic\n * Strategy:\n * 1."
},
{
"path": "packages/detection/src/platforms/bilibili.js",
"chars": 1268,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Bilibili platform detection logic\n * Strategy:\n * 1. Call ht"
},
{
"path": "packages/detection/src/platforms/cnblogs.js",
"chars": 1189,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Cnblogs (博客园) detection logic (offscreen approach)\n * Fetch "
},
{
"path": "packages/detection/src/platforms/csdn.js",
"chars": 2477,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * CSDN platform detection logic\n * Strategy:\n * 1. Check 'User"
},
{
"path": "packages/detection/src/platforms/cto51.js",
"chars": 1464,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * 51CTO platform detection logic (same approach as 爱贝壳)\n * Fet"
},
{
"path": "packages/detection/src/platforms/douban.js",
"chars": 7693,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\nasync function convertToBase64WithFallback(avatarUrl) {\n if (!av"
},
{
"path": "packages/detection/src/platforms/elecfans.js",
"chars": 1944,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Elecfans (电子发烧友) detection logic\n * API: /api/mobile/index.p"
},
{
"path": "packages/detection/src/platforms/huaweicloud.js",
"chars": 1606,
"preview": "import { convertAvatarToBase64, detectByApi } from '../utils.js'\n\nconst HUAWEICLOUD_API = 'https://devdata.huaweicloud.c"
},
{
"path": "packages/detection/src/platforms/huaweidev.js",
"chars": 10777,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Huawei Developer platform detection logic\n * Strategy (cache"
},
{
"path": "packages/detection/src/platforms/infoq.js",
"chars": 1359,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * InfoQ platform detection logic\n * Strategy: POST to /public/"
},
{
"path": "packages/detection/src/platforms/jianshu.js",
"chars": 1076,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Jianshu platform detection logic\n * Strategy: Fetch settings"
},
{
"path": "packages/detection/src/platforms/medium.js",
"chars": 1813,
"preview": "/**\n * Medium platform detection logic\n * Strategy:\n * 1. Check sid/uid cookies\n * 2. Fetch stats page and extract usern"
},
{
"path": "packages/detection/src/platforms/modelscope.js",
"chars": 5038,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * ModelScope platform detection logic\n * Strategy:\n * 1. Try /"
},
{
"path": "packages/detection/src/platforms/oschina.js",
"chars": 2765,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * OSChina platform detection logic\n * Strategy:\n * 1. Check os"
},
{
"path": "packages/detection/src/platforms/qianfan.js",
"chars": 1611,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Baidu Qianfan platform detection logic\n * Strategy:\n * 1. Re"
},
{
"path": "packages/detection/src/platforms/segmentfault.js",
"chars": 1825,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * SegmentFault platform detection logic\n * Strategy: Fetch hom"
},
{
"path": "packages/detection/src/platforms/sohu.js",
"chars": 1248,
"preview": "/**\n * Sohu (搜狐号) platform detection logic\n * Strategy:\n * 1. Check ppinf cookie on mp.sohu.com\n * 2. Call account list "
},
{
"path": "packages/detection/src/platforms/sspai.js",
"chars": 966,
"preview": "/**\n * Sspai (少数派) platform detection logic\n * Strategy:\n * 1. Check sspai_jwt_token cookie\n * 2. Call user info API wit"
},
{
"path": "packages/detection/src/platforms/tencentcloud.js",
"chars": 2008,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Tencent Cloud platform detection logic\n * Strategy:\n * 1. Fe"
},
{
"path": "packages/detection/src/platforms/twitter.js",
"chars": 1552,
"preview": "/**\n * Twitter/X platform detection logic\n * Strategy:\n * 1. Check auth_token/ct0 cookies on x.com\n * 2. Fetch home page"
},
{
"path": "packages/detection/src/platforms/volcengine.js",
"chars": 1860,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Volcengine (火山引擎开发者社区) detection logic\n * API: /api/fe/v1/us"
},
{
"path": "packages/detection/src/platforms/wangyihao.js",
"chars": 1857,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * 网易号 detection logic\n * Strategy:\n * 1. Collect cookies via c"
},
{
"path": "packages/detection/src/platforms/wechat.js",
"chars": 3841,
"preview": "/**\n * WeChat Official Account platform detection logic\n * Strategy:\n * 1. Check chrome.storage.local cache (1 hour TTL)"
},
{
"path": "packages/detection/src/platforms/weibo.js",
"chars": 3375,
"preview": "import { convertAvatarToBase64 } from '../utils.js'\n\n/**\n * Weibo platform detection logic\n * Strategy:\n * 1. Check SUBP"
},
{
"path": "packages/detection/src/platforms/xiaohongshu.js",
"chars": 4252,
"preview": "/**\n * Xiaohongshu (Little Red Book) platform detection logic\n * Strategy:\n * 1. Check `a1` cookie on creator.xiaohongsh"
},
{
"path": "packages/detection/src/utils.js",
"chars": 6389,
"preview": "/**\n * Convert an avatar URL to a base64 data URL to bypass CORS/ORB blocking.\n * The service worker can fetch with a cu"
},
{
"path": "pnpm-workspace.yaml",
"chars": 40,
"preview": "packages:\n - 'apps/*'\n - 'packages/*'\n"
}
]
About this extraction
This page contains the full source code of the doocs/cose GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 90 files (372.0 KB), approximately 100.1k tokens, and a symbol index with 121 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.