From e70fa9e92a71cd2f10e0c883981f324a332162d4 Mon Sep 17 00:00:00 2001 From: Libin YANG Date: Wed, 17 Jun 2026 19:40:35 +0800 Subject: [PATCH] chore: add Prettier formatting config and pre-commit hooks (#253) --- .editorconfig | 13 + .github/ISSUE_TEMPLATE/01-bug-report.yml | 26 +- .github/ISSUE_TEMPLATE/02-feature-request.yml | 20 +- .../ISSUE_TEMPLATE/03-platform-request.yml | 4 +- .github/PULL_REQUEST_TEMPLATE.md | 39 +- .github/workflows/ci.yml | 3 + .prettierignore | 3 + .prettierrc.json | 11 + PRIVACY.md | 18 +- README.md | 43 +- apps/extension/manifest.json | 20 +- apps/extension/package.json | 2 +- apps/extension/scripts/cli.ts | 380 +++++------ apps/extension/scripts/convert-icons.mjs | 30 +- apps/extension/scripts/reload-extension.mjs | 124 ++-- apps/extension/src/background.js | 627 ++++++++++++------ apps/extension/src/content.js | 529 ++++++++------- apps/extension/src/inject.js | 362 ++++++++-- apps/extension/src/offscreen.html | 12 +- apps/extension/src/offscreen.js | 19 +- apps/extension/src/popup.html | 154 ++--- apps/extension/src/popup.js | 2 +- apps/extension/tsconfig.json | 24 +- apps/extension/vite.config.js | 74 +-- package.json | 14 +- packages/core/index.js | 4 +- packages/core/package.json | 28 +- packages/core/src/platforms/alipayopen.js | 23 +- packages/core/src/platforms/aliyun.js | 17 +- packages/core/src/platforms/baijiahao.js | 5 +- packages/core/src/platforms/csdn.js | 5 +- packages/core/src/platforms/cto51.js | 113 ++-- packages/core/src/platforms/douyin.js | 5 +- packages/core/src/platforms/huaweidev.js | 2 - packages/core/src/platforms/index.js | 138 ++-- packages/core/src/platforms/infoq.js | 82 +-- packages/core/src/platforms/jianshu.js | 21 +- packages/core/src/platforms/juejin.js | 1 - packages/core/src/platforms/medium.js | 72 +- packages/core/src/platforms/modelscope.js | 2 - packages/core/src/platforms/oschina.js | 133 ++-- packages/core/src/platforms/qianfan.js | 23 +- packages/core/src/platforms/sspai.js | 14 +- packages/core/src/platforms/tencentcloud.js | 195 +++--- packages/core/src/platforms/toutiao.js | 15 +- packages/core/src/platforms/twitter.js | 33 +- packages/core/src/platforms/volcengine.js | 2 - packages/core/src/platforms/wangyihao.js | 21 +- packages/core/src/platforms/wechat.js | 117 ++-- packages/core/src/platforms/zhihu.js | 142 ++-- packages/core/src/utils.js | 10 +- packages/detection/package.json | 20 +- packages/detection/src/configs.js | 85 +-- packages/detection/src/detect.js | 84 +-- packages/detection/src/platforms/alipay.js | 167 ++--- packages/detection/src/platforms/aliyun.js | 41 +- packages/detection/src/platforms/bilibili.js | 52 +- packages/detection/src/platforms/cnblogs.js | 36 +- packages/detection/src/platforms/csdn.js | 91 +-- packages/detection/src/platforms/cto51.js | 46 +- packages/detection/src/platforms/douban.js | 360 +++++----- packages/detection/src/platforms/elecfans.js | 70 +- .../detection/src/platforms/huaweicloud.js | 69 +- packages/detection/src/platforms/huaweidev.js | 421 ++++++------ packages/detection/src/platforms/infoq.js | 58 +- packages/detection/src/platforms/jianshu.js | 40 +- packages/detection/src/platforms/medium.js | 67 +- .../detection/src/platforms/modelscope.js | 201 +++--- packages/detection/src/platforms/oschina.js | 120 ++-- packages/detection/src/platforms/qianfan.js | 59 +- .../detection/src/platforms/segmentfault.js | 72 +- packages/detection/src/platforms/sohu.js | 46 +- packages/detection/src/platforms/sspai.js | 37 +- .../detection/src/platforms/tencentcloud.js | 68 +- packages/detection/src/platforms/twitter.js | 56 +- .../detection/src/platforms/volcengine.js | 62 +- packages/detection/src/platforms/wangyihao.js | 66 +- packages/detection/src/platforms/wechat.js | 182 ++--- packages/detection/src/platforms/weibo.js | 168 ++--- .../detection/src/platforms/xiaohongshu.js | 177 ++--- packages/detection/src/utils.js | 310 ++++----- pnpm-lock.yaml | 201 +++++- pnpm-workspace.yaml | 1 + tests/wechat.test.mjs | 61 +- 84 files changed, 4125 insertions(+), 3245 deletions(-) create mode 100644 .editorconfig create mode 100644 .prettierignore create mode 100644 .prettierrc.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..bebb345 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# https://editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml index 9dcf732..b65b045 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -1,15 +1,15 @@ name: Bug Report / 问题报告 description: Report a bug or issue with the extension / 报告扩展的 bug 或问题 -title: "[Bug]: " -labels: ["bug", "triage"] +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: @@ -18,7 +18,7 @@ body: placeholder: Describe the bug you encountered / 描述您遇到的问题 validations: required: true - + - type: textarea id: reproduction-steps attributes: @@ -31,7 +31,7 @@ body: 4. See error / 看到错误 validations: required: true - + - type: textarea id: expected-behavior attributes: @@ -40,7 +40,7 @@ body: placeholder: Describe the expected behavior / 描述预期行为 validations: required: true - + - type: textarea id: suggested-implementation attributes: @@ -49,7 +49,7 @@ body: placeholder: Describe your suggested implementation / 描述您建议的实现方式 validations: required: false - + - type: input id: platform attributes: @@ -58,7 +58,7 @@ body: placeholder: e.g., CSDN, Zhihu, Juejin, Toutiao / 如:CSDN、知乎、掘金、头条 validations: required: true - + - type: dropdown id: browser attributes: @@ -72,7 +72,7 @@ body: - Other / 其他 validations: required: true - + - type: input id: extension-version attributes: @@ -81,7 +81,7 @@ body: placeholder: e.g., 1.2.0 / 如:1.2.0 validations: required: true - + - type: textarea id: environment attributes: @@ -93,7 +93,7 @@ body: - Additional context / 其他信息 validations: required: false - + - type: textarea id: screenshots attributes: @@ -102,7 +102,7 @@ body: placeholder: Drag and drop images here / 拖放图片到这里 validations: required: false - + - type: textarea id: additional-context attributes: diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml index b4d0d64..0beccdb 100644 --- a/.github/ISSUE_TEMPLATE/02-feature-request.yml +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -1,15 +1,15 @@ name: Feature Request / 功能请求 description: Suggest a new feature or enhancement / 建议新功能或改进 -title: "[Feature]: " -labels: ["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: @@ -18,7 +18,7 @@ body: placeholder: "I'm frustrated when... / 我感到困扰的是..." validations: required: true - + - type: textarea id: proposed-solution attributes: @@ -27,7 +27,7 @@ body: placeholder: A clear and concise description of what you want to happen / 清晰简洁地描述您希望实现的功能 validations: required: true - + - type: textarea id: alternatives attributes: @@ -36,7 +36,7 @@ body: placeholder: What other approaches could solve this problem? / 还有哪些方法可以解决这个问题? validations: required: false - + - type: dropdown id: feature-type attributes: @@ -51,7 +51,7 @@ body: - Other / 其他 validations: required: true - + - type: textarea id: additional-context attributes: @@ -60,7 +60,7 @@ body: placeholder: Any mockups, examples, or additional details / 任何原型图、示例或其他细节 validations: required: false - + - type: checkboxes id: willingness attributes: @@ -70,4 +70,4 @@ body: - 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 \ No newline at end of file + required: false diff --git a/.github/ISSUE_TEMPLATE/03-platform-request.yml b/.github/ISSUE_TEMPLATE/03-platform-request.yml index 03830fe..0864c1a 100644 --- a/.github/ISSUE_TEMPLATE/03-platform-request.yml +++ b/.github/ISSUE_TEMPLATE/03-platform-request.yml @@ -1,7 +1,7 @@ name: Platform Request / 平台支持 description: Request support for a new platform / 请求支持新平台 -title: "[Platform]: " -labels: ["enhancement", "new platform"] +title: '[Platform]: ' +labels: ['enhancement', 'new platform'] body: - type: input id: platform-name diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 03d597e..a25c8d6 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,20 @@ ## Summary / 概述 + - - ## Related Issue / 关联 Issue + + Closes # - ## Type of Change / 更改类型 + + - [ ] 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 / 会导致现有功能无法正常工作的修复或功能) @@ -21,31 +23,33 @@ Closes # - [ ] Code refactoring / 代码重构 - [ ] Other / 其他 (please describe / 请描述): - ## Changes Made / 更改内容 + -- -- -- +- +- +- ## Implementation Details / 实现细节 + **Key Changes / 主要更改:** -- -**Technical Notes / 技术说明:** -- +- **Technical Notes / 技术说明:** +- ## Testing / 测试 + ### Testing Checklist / 测试清单 + - [ ] I have tested this code locally / 我已在本地测试此代码 - [ ] All existing tests pass / 所有现有测试通过 - [ ] I have added tests for new functionality / 我已为新功能添加测试 @@ -53,20 +57,21 @@ Closes # - [ ] I have verified the changes work in the target browser(s) / 我已验证更改在目标浏览器中有效 ### Manual Testing Steps / 手动测试步骤 -1. -2. -3. +1. +2. +3. ## Screenshots/Videos / 截图/视频 + - - ## Reviewer Checklist / 审阅者清单 + + - [ ] Code follows the project's style guidelines / 代码遵循项目的风格指南 - [ ] Changes are well-documented / 更改有良好的文档说明 - [ ] No breaking changes or clearly documented if present / 无破坏性更改,或已清楚记录 @@ -74,9 +79,7 @@ Closes # - [ ] Performance impact has been evaluated / 已评估性能影响 - [ ] All discussions have been resolved / 所有讨论已解决 - ## Additional Notes / 补充说明 + - - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a95036..4f30cd9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,9 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile + - name: Check formatting + run: pnpm format:check + - name: Build extension (Chromium) run: pnpm build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..29c69b2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +pnpm-lock.yaml diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..49c3d8d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,11 @@ +{ + "semi": false, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 100, + "endOfLine": "lf", + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/PRIVACY.md b/PRIVACY.md index b192d18..cc3cb03 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -28,15 +28,15 @@ All data processed by COSE remains on your local device: ## 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 | +| 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 diff --git a/README.md b/README.md index 496192f..0885a51 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_ ## 使用方法 -> 点击观看视频:[![Bilibili](https://img.shields.io/badge/Video-Bilibili-00A1D6?logo=bilibili&logoColor=white)](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [![YouTube](https://img.shields.io/badge/Video-YouTube-FF0000?logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=KTskiA8Xaj4) +> 点击观看视频:[![Bilibili](https://img.shields.io/badge/Video-Bilibili-00A1D6?logo=bilibili&logoColor=white)](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [![YouTube](https://img.shields.io/badge/Video-YouTube-FF0000?logo=youtube&logoColor=white)](https://www.youtube.com/watch?v=KTskiA8Xaj4) 1. 先点击安装扩展 [![Chrome Web Store](https://img.shields.io/badge/Install-Chrome%20Web%20Store-4285F4?logo=googlechrome&logoColor=white)](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk) 然后打开 [md.doocs.org](https://md.doocs.org) 或本地开发环境 2. 编辑 Markdown 内容 @@ -34,7 +34,6 @@ _**C**reate **O**nce **S**ync **E**verywhere_ - 同步的标签页自动归入分组,便于管理 - 微信公众号同步时完整保留渲染样式并自动保存为草稿 - ## 已支持的平台 > 更多想要添加的平台欢迎提 [Issue](https://github.com/doocs/cose/issues) ! @@ -42,28 +41,26 @@ _**C**reate **O**nce **S**ync **E**verywhere_ >
> 已支持平台速查表(点击展开) > -> | 字母 | 平台 | -> |:---:|:---| -> | 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 | +> | 字母 | 平台 | +> | :--: | :--------------------------------------- | +> | 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 | > >
- - ### 媒体平台

@@ -82,8 +79,8 @@ _**C**reate **O**nce **S**ync **E**verywhere_ X(Formerly Twitter) Articles

- ### 博客平台 +

CSDN 博客园 @@ -97,6 +94,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_

### 云平台及其开发者社区 +

@@ -119,7 +117,6 @@ _**C**reate **O**nce **S**ync **E**verywhere_ 电子发烧友

- ## 开发者模式测试 1. 克隆或下载本项目 diff --git a/apps/extension/manifest.json b/apps/extension/manifest.json index ac79a6d..bbaeef2 100644 --- a/apps/extension/manifest.json +++ b/apps/extension/manifest.json @@ -72,13 +72,8 @@ }, "content_scripts": [ { - "matches": [ - "http://*/*", - "https://*/*" - ], - "js": [ - "bundles/content.js" - ], + "matches": ["http://*/*", "https://*/*"], + "js": ["bundles/content.js"], "run_at": "document_idle" } ], @@ -89,13 +84,8 @@ }, "web_accessible_resources": [ { - "resources": [ - "bundles/inject.js", - "bundles/platforms/*.js" - ], - "matches": [ - "" - ] + "resources": ["bundles/inject.js", "bundles/platforms/*.js"], + "matches": [""] } ] -} \ No newline at end of file +} diff --git a/apps/extension/package.json b/apps/extension/package.json index e2eeb2d..97c9a1a 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -27,4 +27,4 @@ "web-ext": "^10.4.0", "ws": "^8.21.0" } -} \ No newline at end of file +} diff --git a/apps/extension/scripts/cli.ts b/apps/extension/scripts/cli.ts index 4485eca..ac6a5ca 100644 --- a/apps/extension/scripts/cli.ts +++ b/apps/extension/scripts/cli.ts @@ -14,209 +14,209 @@ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8')) // Firefox background uses scripts array, Chrome uses service_worker interface FirefoxBackgroundOptions { - scripts: string[] - type: 'module' + scripts: string[] + type: 'module' } interface ChromeBackgroundOptions { - service_worker: string - type: 'module' + 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 - default_title: string - } - background: FirefoxBackgroundOptions | ChromeBackgroundOptions - content_scripts: Array<{ - matches: string[] - js: string[] - run_at: string - }> - icons: Record - web_accessible_resources: Array<{ - resources: string[] - matches: string[] - }> - browser_specific_settings?: { - gecko: { - id: string - } + manifest_version: number + name: string + version: string + description: string + permissions: string[] + host_permissions: string[] + action: { + default_icon: Record + default_title: string + } + background: FirefoxBackgroundOptions | ChromeBackgroundOptions + content_scripts: Array<{ + matches: string[] + js: string[] + run_at: string + }> + icons: Record + web_accessible_resources: Array<{ + resources: string[] + matches: string[] + }> + browser_specific_settings?: { + gecko: { + id: string } + } } const readManifest = async (manifestPath: string): Promise => { - try { - const fileContent = await fs.readFile(manifestPath, 'utf8') - const json = JSON.parse(fileContent) as Manifest - return json - } catch (error) { - console.error(error) - return 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 + watch: boolean + release: boolean + target: 'chromium' | 'firefox' | 'safari' + bundleId?: string } const buildWithVite = async (options: BuildOptions) => { - console.log('Building with Vite...') + console.log('Building with Vite...') - await viteBuild({ - root: rootDir, - mode: options.release ? 'production' : 'development', - build: { - minify: options.release, - watch: options.watch ? {} : null, - }, - }) + await viteBuild({ + root: rootDir, + mode: options.release ? 'production' : 'development', + build: { + minify: options.release, + watch: options.watch ? {} : null, + }, + }) } const copyResources = async () => { - console.log('Copying resources...') + console.log('Copying resources...') - interface CopyEntry { - from: string - to: string + interface CopyEntry { + from: string + to: string + } + + const copyEntries: CopyEntry[] = [ + { + from: path.join(rootDir, 'icons'), + to: path.join(rootDir, 'dist/icons'), + }, + { + // 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 copyEntries: CopyEntry[] = [ - { - from: path.join(rootDir, 'icons'), - to: path.join(rootDir, 'dist/icons'), - }, - { - // Copy platform scripts from core package - from: path.resolve(rootDir, '../../packages/core/src/platforms'), - to: path.join(rootDir, 'dist/bundles/platforms'), - }, - ] + for (const name of ['popup.html', 'offscreen.html'] as const) { + const from = path.join(rootDir, 'src', name) + const to = path.join(rootDir, 'dist', name) + await fs.copyFile(from, to) + console.log(` ✓ Copied ${name}`) + } - 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)`) - } - } - - for (const name of ['popup.html', 'offscreen.html'] as const) { - const from = path.join(rootDir, 'src', name) - const to = path.join(rootDir, 'dist', name) - await fs.copyFile(from, to) - console.log(` ✓ Copied ${name}`) - } - - // vite-plugin-static-copy v4 may leave stale HTML under dist/src - await fs.rm(path.join(rootDir, 'dist/src'), { recursive: true, force: true }) + // vite-plugin-static-copy v4 may leave stale HTML under dist/src + await fs.rm(path.join(rootDir, 'dist/src'), { recursive: true, force: true }) } const genManifest = async (options: BuildOptions) => { - console.log('Generating manifest.json...') + console.log('Generating manifest.json...') - const manifest = await readManifest(path.join(rootDir, 'manifest.json')) + const manifest = await readManifest(path.join(rootDir, 'manifest.json')) - if (!manifest) { - throw new Error('manifest.json not found') + 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', } - if (!manifest.background) { - throw new Error('manifest.background not found') + // Add Firefox-specific settings + manifest.browser_specific_settings = { + gecko: { + id: 'cose@doocs.org', + }, } - // 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', - } + console.log(' ✓ Converted to Firefox manifest format') + } - // Add Firefox-specific settings - manifest.browser_specific_settings = { - gecko: { - id: 'cose@doocs.org', - }, - } + // Sync version from package.json + manifest.version = packageJson.version - console.log(' ✓ Converted to Firefox manifest format') - } + // Write manifest to dist + const outputPath = path.join(rootDir, 'dist/manifest.json') + await fs.writeFile(outputPath, JSON.stringify(manifest, null, options.release ? undefined : 2)) - // 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})`) + console.log(` ✓ Generated manifest.json (version: ${manifest.version})`) } const buildSafariExtension = async (options: BuildOptions) => { - console.log('\nConverting to Safari extension...') + 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' - ) - } + // 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' + 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 }) + // Remove existing Safari project + await fs.rm(safariProjectDir, { recursive: true, force: true }) - console.log(` Bundle ID: ${bundleId}`) - console.log(` Project location: ${safariProjectDir}`) + 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 - } + 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 @@ -224,43 +224,45 @@ 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 ', 'Browser target: "chromium", "firefox", or "safari"', { default: 'chromium' }) - .option('--bundle-id ', '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".`) - } + .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 ', 'Browser target: "chromium", "firefox", or "safari"', { + default: 'chromium', + }) + .option('--bundle-id ', '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`) + console.log(`\n=== COSE Build (target: ${options.target}, release: ${options.release}) ===\n`) - // Step 1: Build with Vite - await buildWithVite(options) + // Step 1: Build with Vite + await buildWithVite(options) - // Step 2: Copy resources - await copyResources() + // Step 2: Copy resources + await copyResources() - // Step 3: Generate manifest - await genManifest(options) + // 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) - } + // 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`) - }) + console.log(`\n=== Build complete! ===\n`) + }) cli.parse(process.argv, { run: false }) await cli.runMatchedCommand() diff --git a/apps/extension/scripts/convert-icons.mjs b/apps/extension/scripts/convert-icons.mjs index 5339fbe..2576390 100644 --- a/apps/extension/scripts/convert-icons.mjs +++ b/apps/extension/scripts/convert-icons.mjs @@ -15,9 +15,16 @@ function createPlaceholderPng(size) { // 创建一个简单的 PNG 占位符 // 这是一个最小的有效 PNG 文件(绿色方块) const header = Buffer.from([ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, // PNG signature ]) - + console.log(`请使用以下命令安装 sharp 并重新运行,或手动转换 SVG:`) console.log(` npm install sharp`) console.log(` 或使用在线工具: https://svgtopng.com/`) @@ -26,33 +33,30 @@ function createPlaceholderPng(size) { 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) - + await sharp.default(svgBuffer).resize(size, size).png().toFile(pngPath) + console.log(`✓ 已转换: icon${size}.svg -> icon${size}.png`) } - + console.log('') console.log('✔ 图标转换完成') } catch (e) { diff --git a/apps/extension/scripts/reload-extension.mjs b/apps/extension/scripts/reload-extension.mjs index 6f60578..697a208 100644 --- a/apps/extension/scripts/reload-extension.mjs +++ b/apps/extension/scripts/reload-extension.mjs @@ -15,32 +15,38 @@ 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' }) + 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() + 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 - } + const extPage = pages.find(p => p.url.includes('chrome://extensions')) + if (!extPage) { + console.log(`[reload ${ts()}] 未找到 chrome://extensions 页面,请打开该页面`) + return + } - console.log(`[reload ${ts()}] 正在重新加载扩展...`) + console.log(`[reload ${ts()}] 正在重新加载扩展...`) - const ws = new WebSocket(extPage.webSocketDebuggerUrl) + 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: ` + 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'); @@ -68,44 +74,48 @@ async function reloadExtension() { return 'not-found'; })() `, - awaitPromise: true - } - })) - }) + 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('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() - }) + ws.on('error', e => { + console.log(`[reload ${ts()}] 连接错误:`, e.message) + resolve() + }) - setTimeout(() => { ws.close(); resolve() }, 3000) - }) - } catch (e) { - console.log(`[reload ${ts()}] 失败:`, e.message) - } + setTimeout(() => { + ws.close() + resolve() + }, 3000) + }) + } catch (e) { + console.log(`[reload ${ts()}] 失败:`, e.message) + } } function debounceReload() { - if (reloadTimeout) clearTimeout(reloadTimeout) - reloadTimeout = setTimeout(reloadExtension, 800) + if (reloadTimeout) clearTimeout(reloadTimeout) + reloadTimeout = setTimeout(reloadExtension, 800) } console.log(`[reload ${ts()}] 监听 ${distDir} 目录变化...`) @@ -115,13 +125,13 @@ 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() - } + 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) + console.log(`\n[reload ${ts()}] 已停止`) + process.exit(0) }) diff --git a/apps/extension/src/background.js b/apps/extension/src/background.js index fb0504e..b5ceb25 100644 --- a/apps/extension/src/background.js +++ b/apps/extension/src/background.js @@ -117,7 +117,7 @@ function sendOffscreenMessage(msg, timeoutMs = 15000) { * 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'. */ @@ -132,7 +132,9 @@ async function tabContextFetch(siteUrl, apiUrl, options = {}) { // 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'}`) + 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) @@ -169,13 +171,17 @@ async function tabContextFetch(siteUrl, apiUrl, options = {}) { const resp = await fetch(fetchUrl, { method: 'GET', credentials: 'include', - headers: { 'Accept': respType === 'json' ? 'application/json' : 'text/html' }, + 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 } + try { + body = await resp.json() + } catch (e) { + body = null + } } else { body = await resp.text() } @@ -190,15 +196,26 @@ async function tabContextFetch(siteUrl, apiUrl, options = {}) { // Clean up created tab if (createdTabId) { - try { await chrome.tabs.remove(createdTabId) } catch (e) { /* ignore */ } + try { + await chrome.tabs.remove(createdTabId) + } catch (e) { + /* ignore */ + } } - console.log(`[COSE] tabContextFetch result:`, JSON.stringify(results?.[0]?.result).substring(0, 200)) + 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 */ } + try { + await chrome.tabs.remove(createdTabId) + } catch (e2) { + /* ignore */ + } } console.log(`[COSE] tabContextFetch failed for ${apiUrl}:`, e.message) return null @@ -291,7 +308,7 @@ async function initDynamicRules() { const existingIds = existingRules.map(r => r.id) if (existingIds.length > 0) { await chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: existingIds + removeRuleIds: existingIds, }) } @@ -305,16 +322,16 @@ async function initDynamicRules() { type: 'modifyHeaders', requestHeaders: [ { header: 'Referer', operation: 'set', value: 'https://weibo.com/' }, - { header: 'Origin', operation: 'set', value: 'https://weibo.com' } + { header: 'Origin', operation: 'set', value: 'https://weibo.com' }, ], responseHeaders: [ - { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' } - ] + { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }, + ], }, condition: { urlFilter: '*sinaimg.cn*', - resourceTypes: ['image', 'xmlhttprequest'] - } + resourceTypes: ['image', 'xmlhttprequest'], + }, }, { id: 2, @@ -323,18 +340,18 @@ async function initDynamicRules() { type: 'modifyHeaders', requestHeaders: [ { header: 'Referer', operation: 'set', value: 'https://sspai.com/' }, - { header: 'Origin', operation: 'set', value: 'https://sspai.com' } + { header: 'Origin', operation: 'set', value: 'https://sspai.com' }, ], responseHeaders: [ - { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' } - ] + { header: 'Access-Control-Allow-Origin', operation: 'set', value: '*' }, + ], }, condition: { urlFilter: '*cdnfile.sspai.com*', - resourceTypes: ['image', 'xmlhttprequest'] - } - } - ] + resourceTypes: ['image', 'xmlhttprequest'], + }, + }, + ], }) console.log('[COSE] 动态规则初始化完成') } catch (e) { @@ -424,13 +441,13 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { } if (request.type === 'GET_DEBUG_LOGS') { - chrome.storage.local.get('debug_logs', (result) => { + chrome.storage.local.get('debug_logs', result => { sendResponse({ logs: result.debug_logs || [] }) }) return true } - (async () => { + ;(async () => { try { const result = await handleMessage(request, sender) sendResponse(result) @@ -470,14 +487,20 @@ async function handleMessage(request, sender) { } 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/') + 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/') + hwdInfo.avatar = await convertAvatarToBase64( + hwdInfo.avatar, + 'https://developer.huawei.com/' + ) } await chrome.storage.local.set({ huaweidev_user: hwdInfo }) console.log('[COSE] 华为开发者用户信息已缓存:', hwdInfo.username) @@ -495,7 +518,7 @@ async function checkAllPlatforms(platforms) { // 过滤掉无效的平台配置 const validPlatforms = (platforms || []).filter(p => p && p.id) const results = await Promise.allSettled( - validPlatforms.map(async (platform) => { + validPlatforms.map(async platform => { try { const result = await checkPlatformLogin(platform) return { id: platform.id, result } @@ -504,7 +527,7 @@ async function checkAllPlatforms(platforms) { } }) ) - results.forEach((res) => { + results.forEach(res => { if (res.status === 'fulfilled' && res.value?.id) { status[res.value.id] = res.value.result } @@ -522,7 +545,7 @@ async function checkAllPlatformsProgressive(platforms, tabId) { const total = validPlatforms.length // 并行检查所有平台,每个完成后立即发送结果 - const promises = validPlatforms.map(async (platform) => { + const promises = validPlatforms.map(async platform => { try { const result = await checkPlatformLogin(platform) completed++ @@ -536,7 +559,7 @@ async function checkAllPlatformsProgressive(platforms, tabId) { platform: platform, result: result, completed: completed, - total: total + total: total, }) } catch (e) { console.log('[COSE] 发送平台状态更新失败:', platform.id, e.message) @@ -556,7 +579,7 @@ async function checkAllPlatformsProgressive(platforms, tabId) { platform: platform, result: errorResult, completed: completed, - total: total + total: total, }) } catch (e2) { console.log('[COSE] 发送平台状态更新失败:', platform.id, e2.message) @@ -574,7 +597,7 @@ async function checkAllPlatformsProgressive(platforms, tabId) { try { await chrome.tabs.sendMessage(tabId, { type: 'PLATFORM_STATUS_COMPLETE', - total: total + total: total, }) } catch (e) { console.log('[COSE] 发送完成消息失败:', e.message) @@ -605,7 +628,7 @@ async function pasteWithDebugger(tabId) { modifiers: 2, // Ctrl windowsVirtualKeyCode: 17, code: 'ControlLeft', - key: 'Control' + key: 'Control', }) // 发送 V 按下(带 Ctrl 修饰符) @@ -614,7 +637,7 @@ async function pasteWithDebugger(tabId) { modifiers: 2, // Ctrl windowsVirtualKeyCode: 86, code: 'KeyV', - key: 'v' + key: 'v', }) // 发送 V 释放 @@ -623,7 +646,7 @@ async function pasteWithDebugger(tabId) { modifiers: 2, windowsVirtualKeyCode: 86, code: 'KeyV', - key: 'v' + key: 'v', }) // 发送 Ctrl 释放 @@ -632,14 +655,13 @@ async function pasteWithDebugger(tabId) { modifiers: 0, windowsVirtualKeyCode: 17, code: 'ControlLeft', - key: 'Control' + 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 { @@ -693,8 +715,8 @@ async function syncToPlatform(platformId, content) { credentials: 'include', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json', - } + Accept: 'application/json', + }, }) const data = await response.json() @@ -722,8 +744,8 @@ async function syncToPlatform(platformId, content) { method: 'GET', credentials: 'include', headers: { - 'Accept': 'application/json', - } + Accept: 'application/json', + }, }) const notebooks = await notebooksResp.json() @@ -741,13 +763,13 @@ async function syncToPlatform(platformId, content) { credentials: 'include', headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json', + Accept: 'application/json', }, body: JSON.stringify({ notebook_id: String(notebookId), title: content.title || '无标题', - at_bottom: false - }) + at_bottom: false, + }), }) const noteData = await createResp.json() @@ -783,11 +805,12 @@ async function syncToPlatform(platformId, content) { const clickResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) // 查找"新的创作"按钮 - const createBtn = Array.from(document.querySelectorAll('button')) - .find(el => el.textContent.includes('新的创作')) + const createBtn = Array.from(document.querySelectorAll('button')).find(el => + el.textContent.includes('新的创作') + ) if (createBtn) { createBtn.click() @@ -798,7 +821,8 @@ async function syncToPlatform(platformId, content) { const start = Date.now() while (Date.now() - start < timeout) { // 查找编辑器元素,可能是 contenteditable 或 textarea - const editor = document.querySelector('[contenteditable="true"]') || + const editor = + document.querySelector('[contenteditable="true"]') || document.querySelector('textarea') || document.querySelector('.editor') || document.querySelector('.content-editor') @@ -809,15 +833,21 @@ async function syncToPlatform(platformId, content) { } const editorLoaded = await waitForEditor() - return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' } + 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 || '未知错误') } + return { + success: false, + message: '小红书创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误'), + } } // 等待页面稳定 @@ -831,11 +861,11 @@ async function syncToPlatform(platformId, content) { const fillResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async (title, htmlBody) => { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) // 等待元素出现的工具函数 const waitForElement = (selector, timeout = 15000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -859,11 +889,17 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] 小红书开始填充内容...') // 等待并查找标题输入框 - const titleInput = await waitForElement('input[placeholder*="标题"], textarea[placeholder*="标题"], .title-input', 5000) + 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 + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -878,14 +914,19 @@ async function syncToPlatform(platformId, content) { await new Promise(r => setTimeout(r, 300)) // 等待并查找内容编辑器 - const contentEditor = await waitForElement('[contenteditable="true"], .editor-content, .content-editor', 5000) + const contentEditor = await waitForElement( + '[contenteditable="true"], .editor-content, .content-editor', + 5000 + ) if (contentEditor && htmlBody) { contentEditor.focus() // 清空现有占位符内容 - if (contentEditor.textContent.includes('从这里开始写正文') || + if ( + contentEditor.textContent.includes('从这里开始写正文') || contentEditor.textContent.includes('请输入正文') || - contentEditor.textContent.includes('写点什么')) { + contentEditor.textContent.includes('写点什么') + ) { contentEditor.innerHTML = '' } @@ -897,7 +938,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) contentEditor.dispatchEvent(pasteEvent) @@ -953,12 +994,13 @@ async function syncToPlatform(platformId, content) { const clickResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async () => { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + 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' + const createBtn = + document.querySelector('button[aria-label="create"]') || + Array.from(document.querySelectorAll('button')).find( + b => b.getAttribute('aria-label')?.toLowerCase() === 'create' ) if (createBtn) { @@ -977,7 +1019,10 @@ async function syncToPlatform(platformId, content) { } const editorLoaded = await waitForEditor() - return { success: editorLoaded, message: editorLoaded ? 'Editor loaded' : 'Editor timeout' } + return { + success: editorLoaded, + message: editorLoaded ? 'Editor loaded' : 'Editor timeout', + } } return { success: false, message: 'Create button not found' } @@ -998,7 +1043,11 @@ async function syncToPlatform(platformId, content) { } if (!clickResult[0]?.result?.success) { - return { success: false, message: 'Twitter Articles 创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误') } + return { + success: false, + message: + 'Twitter Articles 创建文章失败: ' + (clickResult[0]?.result?.message || '未知错误'), + } } // 等待页面稳定 @@ -1013,7 +1062,7 @@ async function syncToPlatform(platformId, content) { target: { tabId: tab.id }, func: async (title, markdown) => { // ========== 工具函数 ========== - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForElement = async (selector, timeout = 10000) => { const start = Date.now() @@ -1025,8 +1074,6 @@ async function syncToPlatform(platformId, content) { return null } - - // ========== 内置 Markdown 解析器(支持代码块和公式)========== // 使用占位符保护机制,避免正则冲突 // 代码块使用样式化 HTML,公式使用 CodeCogs API 渲染为图片 @@ -1090,7 +1137,10 @@ async function syncToPlatform(platformId, content) { html = html.replace(/^\*\*\*$/gm, '

***

') // 处理图片 - html = html.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '$1') + html = html.replace( + /!\[([^\]]*)\]\(([^)]+)\)/g, + '$1' + ) // 处理链接 html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '$1') @@ -1160,7 +1210,7 @@ async function syncToPlatform(platformId, content) { html = html.replace(/^\d+[\.\)] (.+)$/gm, '
  • $1
  • ') // 将连续的
  • 包装成
      - html = html.replace(/(
    • [\s\S]*?<\/li>\n?)+/g, (match) => { + html = html.replace(/(
    • [\s\S]*?<\/li>\n?)+/g, match => { return `
        ${match}
      ` }) @@ -1169,9 +1219,10 @@ async function syncToPlatform(platformId, content) { const result = [] let paragraphLines = [] - const isBlockElement = (line) => { + const isBlockElement = line => { const trimmed = line.trim() - return !trimmed || + return ( + !trimmed || trimmed.startsWith(' { @@ -1222,10 +1274,16 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] Markdown 已转换为 HTML') // 第一步:填充标题 - const titleInput = await waitForElement('textarea[placeholder="Add a title"], textarea[name="Article Title"]', 5000) + 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 + 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 })) @@ -1237,7 +1295,10 @@ async function syncToPlatform(platformId, content) { await sleep(500) // 第二步:填充内容 - const contentEl = await waitForElement('.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', 5000) + const contentEl = await waitForElement( + '.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', + 5000 + ) if (contentEl && htmlContent) { contentEl.focus() @@ -1248,7 +1309,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) contentEl.dispatchEvent(pasteEvent) @@ -1288,16 +1349,18 @@ async function syncToPlatform(platformId, content) { 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'] - } - }] + 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) { @@ -1342,18 +1405,24 @@ async function syncToPlatform(platformId, content) { const fillResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: async (title, markdown) => { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) const waitForElement = (selector, timeout = 5000) => { - return new Promise((resolve) => { + 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) } + if (el) { + observer.disconnect() + resolve(el) + } }) observer.observe(document.body, { childList: true, subtree: true }) - setTimeout(() => { observer.disconnect(); resolve(null) }, timeout) + setTimeout(() => { + observer.disconnect() + resolve(null) + }, timeout) }) } @@ -1362,7 +1431,10 @@ async function syncToPlatform(platformId, content) { const titleInput = await waitForElement('textarea[placeholder="请输入文章标题"]') if (titleInput && title) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + ).set nativeSetter.call(titleInput, title) titleInput.dispatchEvent(new Event('input', { bubbles: true })) console.log('[COSE] 百度千帆标题填充成功') @@ -1371,7 +1443,9 @@ async function syncToPlatform(platformId, content) { await sleep(300) // 填充内容 - 使用 paste 事件注入 Markdown - const contentEditor = await waitForElement('.mp-editor-container[contenteditable="true"]') + const contentEditor = await waitForElement( + '.mp-editor-container[contenteditable="true"]' + ) if (contentEditor && markdown) { contentEditor.focus() await sleep(100) @@ -1379,7 +1453,9 @@ async function syncToPlatform(platformId, content) { const dt = new DataTransfer() dt.setData('text/plain', markdown) const pasteEvent = new ClipboardEvent('paste', { - bubbles: true, cancelable: true, clipboardData: dt + bubbles: true, + cancelable: true, + clipboardData: dt, }) contentEditor.dispatchEvent(pasteEvent) console.log('[COSE] 百度千帆内容填充成功') @@ -1422,7 +1498,7 @@ async function syncToPlatform(platformId, content) { chrome.tabs.onUpdated.removeListener(tabUpdateListener) try { await chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: [QIANFAN_BLOCK_RULE_ID] + removeRuleIds: [QIANFAN_BLOCK_RULE_ID], }) console.log('[COSE] 千帆登录页阻止规则已移除') } catch (_) {} @@ -1433,7 +1509,7 @@ async function syncToPlatform(platformId, content) { chrome.tabs.onUpdated.removeListener(tabUpdateListener) try { await chrome.declarativeNetRequest.updateDynamicRules({ - removeRuleIds: [QIANFAN_BLOCK_RULE_ID] + removeRuleIds: [QIANFAN_BLOCK_RULE_ID], }) } catch (_) {} return { success: false, message: '千帆同步失败: ' + e.message } @@ -1510,7 +1586,7 @@ async function syncToPlatform(platformId, content) { func: async (title, htmlBody) => { // 等待元素出现的工具函数 const waitForElement = (selector, timeout = 15000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -1544,7 +1620,10 @@ async function syncToPlatform(platformId, content) { if (titleInput && title) { titleInput.focus() // 使用 native setter 确保 React/Vue 等框架能检测到变化 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -1575,7 +1654,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) @@ -1596,7 +1675,7 @@ async function syncToPlatform(platformId, content) { return { success: true, wordCount: editor.textContent?.length || 0, - titleFilled: titleInput?.value === title + titleFilled: titleInput?.value === title, } } @@ -1643,8 +1722,9 @@ async function syncToPlatform(platformId, content) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - const saveDraftBtn = Array.from(document.querySelectorAll('button')) - .find(b => b.textContent.includes('保存为草稿')) + const saveDraftBtn = Array.from(document.querySelectorAll('button')).find(b => + b.textContent.includes('保存为草稿') + ) if (saveDraftBtn) { saveDraftBtn.click() console.log('[COSE] 已点击保存为草稿') @@ -1670,7 +1750,7 @@ async function syncToPlatform(platformId, content) { func: async (title, htmlBody) => { // 等待元素出现的工具函数(检测到立即返回) const waitForElement = (selector, timeout = 10000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -1704,7 +1784,10 @@ async function syncToPlatform(platformId, content) { if (titleInput && title) { titleInput.focus() // 使用 native setter 确保 React 等框架能检测到变化 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -1730,7 +1813,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) @@ -1748,7 +1831,7 @@ async function syncToPlatform(platformId, content) { return { success: true, wordCount: editor.textContent?.length || 0, - titleFilled: titleInput?.value === title + titleFilled: titleInput?.value === title, } } @@ -1797,7 +1880,10 @@ async function syncToPlatform(platformId, content) { const titleInput = document.querySelector('input[placeholder*="标题"]') if (titleInput && title) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 })) @@ -1820,7 +1906,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) @@ -1849,7 +1935,7 @@ async function syncToPlatform(platformId, content) { const waitForEditor = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - return new Promise((resolve) => { + return new Promise(resolve => { const startTime = Date.now() const maxWait = 10000 @@ -1913,7 +1999,7 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] B站专栏内容已填充') return { success: true, - contentLength: editor.getContentLength() + contentLength: editor.getContentLength(), } }, args: [content.title, htmlContent], @@ -1929,8 +2015,9 @@ async function syncToPlatform(platformId, content) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - const saveDraftBtn = Array.from(document.querySelectorAll('button')) - .find(b => b.textContent && b.textContent.includes('存草稿')) + const saveDraftBtn = Array.from(document.querySelectorAll('button')).find( + b => b.textContent && b.textContent.includes('存草稿') + ) if (saveDraftBtn) { saveDraftBtn.click() console.log('[COSE] B站专栏已点击存草稿') @@ -1992,8 +2079,9 @@ async function syncToPlatform(platformId, content) { await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - const saveBtn = Array.from(document.querySelectorAll('button')) - .find(b => b.textContent && b.textContent.includes('保存草稿')) + const saveBtn = Array.from(document.querySelectorAll('button')).find( + b => b.textContent && b.textContent.includes('保存草稿') + ) if (saveBtn) { saveBtn.click() console.log('[COSE] 微博头条已点击保存草稿') @@ -2026,7 +2114,10 @@ async function syncToPlatform(platformId, content) { if (titleInput && title) { titleInput.focus() // 使用 native setter 来绕过 React 的受控组件 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 })) @@ -2034,14 +2125,18 @@ async function syncToPlatform(platformId, content) { } // 填充内容 - 阿里云使用 textarea 作为 Markdown 编辑器 - const contentTextarea = document.querySelector('textarea[class*="editor"]') || + 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 + 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 })) @@ -2077,13 +2172,17 @@ async function syncToPlatform(platformId, content) { target: { tabId: tab.id }, func: (title, markdown) => { // 填充标题 - const titleInput = document.querySelector('input[placeholder*="标题"]') || + 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 + 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 })) @@ -2099,12 +2198,16 @@ async function syncToPlatform(platformId, content) { } // 备用方案:尝试直接操作 textarea - const contentTextarea = document.querySelector('.bytemd-editor 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 + 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 })) @@ -2181,7 +2284,7 @@ async function syncToPlatform(platformId, content) { // 填充标题 await chrome.scripting.executeScript({ target: { tabId: tab.id }, - func: (title) => { + func: title => { const titleInput = document.querySelector('input[placeholder*="标题"]') if (titleInput && title) { titleInput.focus() @@ -2199,10 +2302,10 @@ async function syncToPlatform(platformId, content) { // 使用 MutationObserver 监听 iframe 出现,message 事件监听内容确认 const fillResult = await chrome.scripting.executeScript({ target: { tabId: tab.id }, - func: async (markdown) => { + func: async markdown => { // 工具函数:使用 MutationObserver 等待 iframe 元素出现并加载 const waitForEditorReady = (timeout = 15000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const check = () => { const editor = window.tinymceModal?.currentEditor if (editor && typeof editor.setContent === 'function') { @@ -2244,20 +2347,25 @@ async function syncToPlatform(platformId, content) { // 工具函数:使用 message 事件监听 setContent 确认(setMdDataSucc) const setContentWithConfirm = (editor, iframe, content, timeout = 3000) => { - return new Promise((resolve) => { + return new Promise(resolve => { let resolved = false - const onMessage = (event) => { + const onMessage = event => { try { const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data - if (data.mdEventAction === 'setMdDataSucc' || data.mdEventAction === 'mdContent') { + if ( + data.mdEventAction === 'setMdDataSucc' || + data.mdEventAction === 'mdContent' + ) { if (!resolved) { resolved = true window.removeEventListener('message', onMessage) resolve({ confirmed: true }) } } - } catch (e) { /* 忽略非 JSON 消息 */ } + } catch (e) { + /* 忽略非 JSON 消息 */ + } } window.addEventListener('message', onMessage) @@ -2301,10 +2409,13 @@ async function syncToPlatform(platformId, content) { // 3. 所有重试失败,直接 postMessage 作为最后手段 console.log('[COSE] 重试耗尽,尝试直接 postMessage') - ready.iframe.contentWindow.postMessage(JSON.stringify({ - mdEditorEventAction: 'setMdEditorContent', - data: encodeURIComponent(markdown) - }), '*') + 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 } }, @@ -2356,7 +2467,8 @@ async function syncToPlatform(platformId, content) { const modalBtns = document.querySelector('.ant-modal-confirm-btns') if (modalBtns) { const buttons = modalBtns.querySelectorAll('button') - const modalText = document.querySelector('.ant-modal-confirm-content')?.textContent || '' + const modalText = + document.querySelector('.ant-modal-confirm-content')?.textContent || '' console.log('[COSE] 检测到 Ant Modal:', modalText.substring(0, 50)) @@ -2414,7 +2526,7 @@ async function syncToPlatform(platformId, content) { } // ========== 工具函数 ========== - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) // 轮询检查弹窗(比 MutationObserver 更可靠) let dialogCheckInterval = null @@ -2551,7 +2663,10 @@ async function syncToPlatform(platformId, content) { const titleInput = document.querySelector('input[placeholder*="标题"]') if (titleInput && title) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 })) @@ -2568,7 +2683,6 @@ async function syncToPlatform(platformId, content) { } return { success: true, method: 'ace', length: markdown.length } - } finally { // 清理检查器 stopDialogChecker() @@ -2597,8 +2711,11 @@ async function syncToPlatform(platformId, content) { 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"]') || + 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) { @@ -2623,16 +2740,18 @@ async function syncToPlatform(platformId, content) { 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 - }) - } - }) + 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 会被过滤) @@ -2644,7 +2763,9 @@ async function syncToPlatform(platformId, content) { const iframe = document.querySelector('iframe') if (iframe && iframe.contentDocument) { const iframeDoc = iframe.contentDocument - const emptyFormulas = iframeDoc.querySelectorAll('.katex-inline, .katex-block, section.katex-block') + const emptyFormulas = iframeDoc.querySelectorAll( + '.katex-inline, .katex-block, section.katex-block' + ) emptyFormulas.forEach((emptyFormula, index) => { const original = originalFormulas[index] @@ -2665,8 +2786,8 @@ async function syncToPlatform(platformId, content) { }, 300) } - editor.fireEvent('contentChange'); - editor.fireEvent('selectionchange'); + editor.fireEvent('contentChange') + editor.fireEvent('selectionchange') console.log('[COSE] 百家号通过 UEditor API 填充成功') return } catch (e) { @@ -2699,17 +2820,21 @@ async function syncToPlatform(platformId, content) { target: { tabId: tab.id }, func: (title, htmlBody) => { // 填充标题 - 少数派使用 textarea - const titleInput = document.querySelector('textarea[placeholder*="标题"]') || + 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 || + 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 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] 少数派标题已填充') @@ -2718,7 +2843,8 @@ async function syncToPlatform(platformId, content) { // 等待一下再填充内容 setTimeout(() => { // 找到 ProseMirror 编辑器 - const editor = document.querySelector('.ProseMirror') || + const editor = + document.querySelector('.ProseMirror') || document.querySelector('[contenteditable="true"]') if (editor && htmlBody) { @@ -2732,7 +2858,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) @@ -2765,7 +2891,7 @@ async function syncToPlatform(platformId, content) { func: async (title, htmlBody) => { // 等待元素出现的工具函数 const waitForElement = (selector, timeout = 10000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -2789,7 +2915,9 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] 支付宝开放平台开始填充内容...') // 等待并查找标题输入框 - const titleInput = await waitForElement('#title', 5000) || await waitForElement('input[placeholder*="标题"]', 5000) + const titleInput = + (await waitForElement('#title', 5000)) || + (await waitForElement('input[placeholder*="标题"]', 5000)) if (titleInput && title) { titleInput.focus() @@ -2798,7 +2926,10 @@ async function syncToPlatform(platformId, content) { titleInput.value = '' // 使用 native setter 确保 React 能检测到变化 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -2825,7 +2956,7 @@ async function syncToPlatform(platformId, content) { bubbles: true, cancelable: true, data: title, - inputType: 'insertText' + inputType: 'insertText', }) titleInput.dispatchEvent(inputEvent) @@ -2851,7 +2982,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) @@ -2905,7 +3036,7 @@ async function syncToPlatform(platformId, content) { func: async (title, markdown) => { // 等待元素出现的工具函数 const waitForElement = (selector, timeout = 10000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -2929,11 +3060,17 @@ async function syncToPlatform(platformId, content) { console.log('[COSE] 电子发烧友开始填充内容...') // 等待并查找标题输入框 - const titleInput = await waitForElement('input[placeholder*="标题"], input.title-input, input[name="title"]', 5000) + 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 + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -2960,7 +3097,7 @@ async function syncToPlatform(platformId, content) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) vditorWysiwyg.dispatchEvent(pasteEvent) @@ -2993,10 +3130,16 @@ async function syncToPlatform(platformId, content) { } // 尝试查找 textarea - const textarea = await waitForElement('textarea.content-textarea, textarea[name="content"], textarea', 5000) + 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 + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(textarea, markdown) } else { @@ -3059,8 +3202,9 @@ async function syncToPlatform(platformId, 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"]') + const editable = + document.querySelector('div.DRE-inputor.DRE-root[contenteditable="true"]') || + document.querySelector('[contenteditable="true"][role="textbox"]') if (editable) { editable.focus() @@ -3070,9 +3214,19 @@ async function syncToPlatform(platformId, content) { if (lexicalEditor?.parseEditorState && lexicalEditor?.setEditorState) { try { const lines = fullText.split('\n') - const makeParagraph = (lineText) => ({ + const makeParagraph = lineText => ({ children: lineText - ? [{ detail: 0, format: 0, mode: 'normal', style: '', text: lineText, type: 'text', version: 1 }] + ? [ + { + detail: 0, + format: 0, + mode: 'normal', + style: '', + text: lineText, + type: 'text', + version: 1, + }, + ] : [], direction: 'ltr', format: '', @@ -3134,11 +3288,13 @@ async function syncToPlatform(platformId, content) { if (!inserted) { // 回退:直接赋值并触发输入事件 editable.textContent = fullText - editable.dispatchEvent(new InputEvent('input', { - bubbles: true, - inputType: 'insertText', - data: fullText, - })) + editable.dispatchEvent( + new InputEvent('input', { + bubbles: true, + inputType: 'insertText', + data: fullText, + }) + ) } editable.dispatchEvent(new Event('change', { bubbles: true })) @@ -3153,13 +3309,17 @@ async function syncToPlatform(platformId, content) { } // 兼容旧版 textarea 结构 - const textarea = document.querySelector('textarea[placeholder*="此刻你想要分享"]') - || document.querySelector('textarea[placeholder*="分享"]') - || document.querySelector('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 + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(textarea, fullText) } else { @@ -3219,7 +3379,7 @@ function fillContentOnPage(content, platformId) { // 等待元素出现的工具函数 function waitFor(selector, timeout = 10000) { - return new Promise((resolve) => { + return new Promise(resolve => { const start = Date.now() const check = () => { const el = document.querySelector(selector) @@ -3261,9 +3421,14 @@ function fillContentOnPage(content, platformId) { if (titleInput) { titleInput.focus() // 模拟用户输入 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 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) @@ -3334,8 +3499,9 @@ function fillContentOnPage(content, platformId) { 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() === '确定切换') + confirmBtn = Array.from(document.querySelectorAll('button')).find( + btn => btn.textContent.trim() === '确定切换' + ) if (confirmBtn) break } if (confirmBtn) { @@ -3355,7 +3521,10 @@ function fillContentOnPage(content, platformId) { const titleInput = await waitFor('input[placeholder*="标题"]') if (titleInput) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) } else { @@ -3378,7 +3547,10 @@ function fillContentOnPage(content, platformId) { } if (textarea) { textarea.focus() - const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set + const textareaSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + )?.set if (textareaSetter) { textareaSetter.call(textarea, mdContent) } else { @@ -3397,7 +3569,8 @@ function fillContentOnPage(content, platformId) { await new Promise(resolve => setTimeout(resolve, 1000)) // 填充标题 - 博客园标题输入框 - const titleInput = await waitFor('input[placeholder="标题"]') || document.querySelector('input') + const titleInput = + (await waitFor('input[placeholder="标题"]')) || document.querySelector('input') if (titleInput) { titleInput.focus() titleInput.value = title @@ -3412,7 +3585,8 @@ function fillContentOnPage(content, platformId) { await new Promise(resolve => setTimeout(resolve, 500)) // 博客园使用 id="md-editor" 的 textarea 作为 Markdown 编辑器 - const editor = document.querySelector('#md-editor') || document.querySelector('textarea.not-resizable') + const editor = + document.querySelector('#md-editor') || document.querySelector('textarea.not-resizable') if (editor) { editor.focus() editor.value = contentToFill @@ -3426,7 +3600,9 @@ function fillContentOnPage(content, platformId) { // InfoQ else if (host.includes('infoq.cn')) { // 填充标题 - const titleInput = await waitFor('input[placeholder*="标题"], .title-input input, input.article-title') + const titleInput = await waitFor( + 'input[placeholder*="标题"], .title-input input, input.article-title' + ) if (titleInput) { setInputValue(titleInput, title) console.log('[COSE] InfoQ 标题填充成功') @@ -3511,9 +3687,14 @@ function fillContentOnPage(content, platformId) { const titleInput = await waitFor('input._24i7u, input[class*="title"]') if (titleInput) { titleInput.focus() - const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 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] 简书标题填充成功') @@ -3525,12 +3706,18 @@ function fillContentOnPage(content, platformId) { await new Promise(resolve => setTimeout(resolve, 500)) // 简书使用 textarea#arthur-editor 作为 Markdown 编辑器 - const editor = document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR') + const editor = + document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR') if (editor) { editor.focus() - const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }) + ) editor.dispatchEvent(new Event('change', { bubbles: true })) console.log('[COSE] 简书内容填充成功') } else { @@ -3598,7 +3785,10 @@ function fillContentOnPage(content, platformId) { const titleInput = document.querySelector('textarea[placeholder*="标题"]') if (titleInput && title) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 })) @@ -3649,7 +3839,7 @@ function fillContentOnPage(content, platformId) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) contentEl.dispatchEvent(pasteEvent) @@ -3683,18 +3873,23 @@ function fillContentOnPage(content, platformId) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: clipboardData + clipboardData: clipboardData, }) textarea.dispatchEvent(pasteEvent) } catch (e) { // 如果 ClipboardEvent 失败,降级到手动设置 - const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 InputEvent('input', { + bubbles: true, + data: contentToFill, + inputType: 'insertText', + }) + ) } textarea.dispatchEvent(new Event('change', { bubbles: true })) @@ -3707,7 +3902,9 @@ function fillContentOnPage(content, platformId) { // 查找并点击"转为富文本"按钮 const findAndClickRichTextBtn = () => { // 使用特定选择器 - const richTextBtn = document.querySelector('[data-testid="menu-item-markdownToDoc"][data-role="markdownToDoc"]') + const richTextBtn = document.querySelector( + '[data-testid="menu-item-markdownToDoc"][data-role="markdownToDoc"]' + ) if (richTextBtn) { console.log('[COSE] ModelScope 找到"转为富文本"按钮,点击中...') richTextBtn.click() @@ -3751,13 +3948,26 @@ function fillContentOnPage(content, platformId) { } // 通用处理 else { - const titleSelectors = ['input[placeholder*="标题"]', 'input[name="title"]', 'textarea[placeholder*="标题"]'] + 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 } + if (el) { + setInputValue(el, title) + break + } } - const contentSelectors = ['.CodeMirror', '.ProseMirror', '.ql-editor', '[contenteditable="true"]', 'textarea'] + const contentSelectors = [ + '.CodeMirror', + '.ProseMirror', + '.ql-editor', + '[contenteditable="true"]', + 'textarea', + ] for (const sel of contentSelectors) { const el = document.querySelector(sel) if (el) { @@ -3795,7 +4005,12 @@ function waitForTab(tabId, timeout = 60000) { } // 如果 URL 已经不是 about:blank/chrome:// 且处于 loading 状态超过 10 秒, // 说明主文档已加载但第三方资源可能超时,提前 resolve - if (!urlReady && tab.url && !tab.url.startsWith('about:') && !tab.url.startsWith('chrome:')) { + if ( + !urlReady && + tab.url && + !tab.url.startsWith('about:') && + !tab.url.startsWith('chrome:') + ) { urlReady = true urlReadyTime = Date.now() } diff --git a/apps/extension/src/content.js b/apps/extension/src/content.js index 1a2a4bf..e965e46 100644 --- a/apps/extension/src/content.js +++ b/apps/extension/src/content.js @@ -4,85 +4,97 @@ 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' - ; (function () { - 'use strict' + // 华为云开发者博客页面:自动获取并缓存用户信息 + if (window.location.hostname.includes('huaweicloud.com')) { + console.log('[COSE] 检测到华为云页面') - // 华为云开发者博客页面:自动获取并缓存用户信息 - 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', { + 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 + 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(), - } + 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({ + if (typeof chrome !== 'undefined' && chrome.runtime) { + chrome.runtime + .sendMessage({ type: 'CACHE_USER_INFO', platform: 'huaweicloud', userInfo, - }).then(() => { + }) + .then(() => { console.log('[COSE] 华为云用户信息已缓存:', userInfo.username) - }).catch(e => { + }) + .catch(e => { console.log('[COSE] 缓存失败:', e.message) }) - } } - } catch (e) { - console.log('[COSE] 华为云用户信息缓存失败:', e.message) } - }, 3000) // 华为云 SSO 登录需要几秒延迟 - } + } catch (e) { + console.log('[COSE] 华为云用户信息缓存失败:', e.message) + } + }, 3000) // 华为云 SSO 登录需要几秒延迟 + } - // 华为开发者页面:自动获取并缓存用户信息 - if (window.location.hostname.includes('developer.huawei.com')) { - console.log('[COSE] 检测到华为开发者页面') + // 华为开发者页面:自动获取并缓存用户信息 + 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() : '' + 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 + // 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}/, '') - 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', { + 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', + Accept: 'application/json', 'x-hd-csrf': csrfToken, 'x-hd-date': hdDate, }, @@ -91,232 +103,245 @@ console.log('[COSE Content Script] Hostname:', window.location.hostname) 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 (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 (domUsername || avatar) { + const userInfo = { + loggedIn: true, + username: domUsername, + avatar, + cachedAt: Date.now(), + } - if (typeof chrome !== 'undefined' && chrome.runtime) { - chrome.runtime.sendMessage({ + if (typeof chrome !== 'undefined' && chrome.runtime) { + chrome.runtime + .sendMessage({ type: 'CACHE_USER_INFO', platform: 'huaweidev', userInfo, - }).then(() => { + }) + .then(() => { console.log('[COSE] 华为开发者用户信息已缓存:', userInfo.username) - }).catch(e => { + }) + .catch(e => { console.log('[COSE] 缓存失败:', e.message) }) - } } - } catch (e) { - console.log('[COSE] 华为开发者用户信息缓存失败:', e.message) } - }, 3000) - } + } catch (e) { + console.log('[COSE] 华为开发者用户信息缓存失败:', e.message) + } + }, 3000) + } - // 小红书页面:自动获取并缓存用户信息 - if (window.location.hostname.includes('xiaohongshu.com')) { - console.log('[COSE] 检测到小红书页面') + // 小红书页面:自动获取并缓存用户信息 + 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 + // 通过 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() - } + 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({ + // 发送给 background script 保存 + if (typeof chrome !== 'undefined' && chrome.runtime) { + chrome.runtime + .sendMessage({ type: 'CACHE_USER_INFO', platform: 'xiaohongshu', - userInfo - }).then(() => { + userInfo, + }) + .then(() => { console.log('[COSE] 小红书用户信息已缓存:', userInfo.username) - }).catch(e => { + }) + .catch(e => { console.log('[COSE] 缓存失败:', e.message) }) - } else { - console.log('[COSE] Chrome runtime 不可用') - } + } 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) } + } catch (e) { + console.log('[COSE] 缓存失败:', e.message) } + }, 2000) + } - // 页面加载完成后获取用户信息 - 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 - + // 支付宝开放平台页面:自动获取并缓存用户信息 + if (window.location.hostname.includes('alipay.com')) { + const cacheAlipayUserInfo = async () => { 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, + 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', }, - '*' - ) - } catch (error) { - window.postMessage( - { - source: 'cose-extension', - requestId, - error: error.message, - }, - '*' - ) - } - }) + body: 'data=%5B%7B%7D%5D&serviceName=alipay.open.developerops.forum.user.query', + }) + if (!response.ok) return - // 监听来自页面的缓存请求(用于手动触发缓存) - 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 不可用,无法保存缓存') + 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) } - }) - // 监听来自 background 的消息并转发到页面(用于渐进式状态更新) - chrome.runtime.onMessage.addListener((message) => { - if (message.type === 'PLATFORM_STATUS_UPDATE' || message.type === 'PLATFORM_STATUS_COMPLETE') { - window.postMessage({ + } + + // 页面加载完成后获取用户信息 + 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 - }, '*') - } - }) - })() + ...message, + }, + '*' + ) + } + }) +})() diff --git a/apps/extension/src/inject.js b/apps/extension/src/inject.js index 9a2d13b..5bacb23 100644 --- a/apps/extension/src/inject.js +++ b/apps/extension/src/inject.js @@ -1,25 +1,34 @@ // 注入到页面主世界的脚本 // 在 window 上暴露 $cose 对象供 Vue 组件使用 -; (function () { +;(function () { 'use strict' let requestId = 0 const pendingRequests = new Map() - + // 渐进式更新的回调 let progressiveCallbacks = { onProgress: null, - onComplete: null + onComplete: null, } // 监听来自 content script 的响应 - window.addEventListener('message', (event) => { + 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 - + const { + type, + requestId: resId, + result, + error, + platformId, + platform, + completed, + total, + } = event.data + // 处理渐进式平台状态更新 if (type === 'PLATFORM_STATUS_UPDATE') { if (progressiveCallbacks.onProgress && platform && event.data.result) { @@ -28,19 +37,21 @@ uid: platform.id, type: platform.type, title: platform.title, - displayName: platformResult.loggedIn ? (platformResult.username || platform.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 + isChecking: false, } progressiveCallbacks.onProgress(account, completed, total) } return } - + // 处理渐进式检测完成 if (type === 'PLATFORM_STATUS_COMPLETE') { if (progressiveCallbacks.onComplete) { @@ -99,37 +110,254 @@ // 平台配置(与 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/' }, + { + 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 全局对象 @@ -162,7 +390,7 @@ uid: p.id, type: p.type, title: p.title, - displayName: isLoggedIn ? (platformStatus.username || p.title) : p.title, + displayName: isLoggedIn ? platformStatus.username || p.title : p.title, icon: p.icon, avatar: platformStatus.avatar, home: p.url || '', @@ -178,7 +406,10 @@ } catch (error) { console.error('获取账号列表失败:', error) // 检测是否是扩展重新加载的错误 - if (error.message && (error.message.includes('扩展已重新加载') || error.message.includes('Extension context'))) { + if ( + error.message && + (error.message.includes('扩展已重新加载') || error.message.includes('Extension context')) + ) { throw new Error('扩展已重新加载,请刷新页面后重试') } const accounts = PLATFORMS.map(p => ({ @@ -204,16 +435,15 @@ 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() - } - }) + sendMessage('CHECK_PLATFORM_STATUS_PROGRESSIVE', { platforms: PLATFORMS }).catch(error => { + console.error('[COSE] 渐进式检测启动失败:', error) + // 如果启动失败,调用完成回调 + if (typeof onComplete === 'function') { + onComplete() + } + }) }, // 添加发布任务(兼容 wechatsync 的 addTask 接口) @@ -267,12 +497,24 @@ 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) { + if ( + hasWechat || + hasBaijiahao || + hasWangyihao || + hasMedium || + hasSspai || + hasBilibili || + hasWeibo || + hasXiaohongshu + ) { // 先点击复制按钮,将带样式的内容复制到剪贴板 - const copyBtn = document.querySelector('.copy-btn') || + 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('复制')) + Array.from(document.querySelectorAll('button')).find(b => + b.textContent.includes('复制') + ) if (copyBtn && typeof copyBtn.click === 'function') { copyBtn.click() // 等待复制完成 @@ -312,7 +554,17 @@ thumb: post.thumb, desc: post.desc, // 微信公众号、百家号、网易号、Medium、少数派、B站专栏、微博头条和小红书使用剪贴板中带样式的 HTML - wechatHtml: (platformId === 'wechat' || platformId === 'baijiahao' || platformId === 'wangyihao' || platformId === 'medium' || platformId === 'sspai' || platformId === 'bilibili' || platformId === 'weibo' || platformId === 'xiaohongshu') ? clipboardHtmlContent : null, + wechatHtml: + platformId === 'wechat' || + platformId === 'baijiahao' || + platformId === 'wangyihao' || + platformId === 'medium' || + platformId === 'sspai' || + platformId === 'bilibili' || + platformId === 'weibo' || + platformId === 'xiaohongshu' + ? clipboardHtmlContent + : null, }, }) diff --git a/apps/extension/src/offscreen.html b/apps/extension/src/offscreen.html index b033770..c625a44 100644 --- a/apps/extension/src/offscreen.html +++ b/apps/extension/src/offscreen.html @@ -1,7 +1,9 @@ - + -COSE Offscreen - - - + + COSE Offscreen + + + + diff --git a/apps/extension/src/offscreen.js b/apps/extension/src/offscreen.js index ce95753..c658f62 100644 --- a/apps/extension/src/offscreen.js +++ b/apps/extension/src/offscreen.js @@ -107,7 +107,11 @@ async function handleApiFetch(payload) { const finalUrl = resp.url let body = null if (responseType === 'json') { - try { body = await resp.json() } catch (e) { body = null } + try { + body = await resp.json() + } catch (e) { + body = null + } } else { body = await resp.text() } @@ -117,7 +121,6 @@ async function handleApiFetch(payload) { } } - /** * 51CTO detection: fetch home.51cto.com/space and parse with DOMParser. * Same approach as 爱贝壳 extension - runs in document context (offscreen). @@ -148,7 +151,10 @@ async function handleDetectCto51() { if (!username && !uid) { // Return debug info to help diagnose const title = doc.querySelector('title')?.textContent || '' - return { loggedIn: false, _debug: { status: resp.status, url: resp.url, htmlLen: html.length, title } } + return { + loggedIn: false, + _debug: { status: resp.status, url: resp.url, htmlLen: html.length, title }, + } } return { loggedIn: true, username, avatar, uid } @@ -157,7 +163,6 @@ async function handleDetectCto51() { } } - /** * Cnblogs detection: fetch account.cnblogs.com/user/userinfo in document context. * Cookies are sent automatically (unlike service worker fetch which strips Cookie headers). @@ -167,7 +172,7 @@ async function handleDetectCnblogs() { const resp = await fetch('https://account.cnblogs.com/user/userinfo', { method: 'GET', credentials: 'include', - headers: { 'Accept': 'application/json' }, + headers: { Accept: 'application/json' }, }) if (!resp.ok) return { loggedIn: false } @@ -186,7 +191,6 @@ async function handleDetectCnblogs() { } } - /** * Xiaohongshu detection: fetch creator API in document context. * Cookies are sent automatically with credentials: 'include'. @@ -196,7 +200,7 @@ async function handleDetectXiaohongshu() { const resp = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', { method: 'GET', credentials: 'include', - headers: { 'Accept': 'application/json' }, + headers: { Accept: 'application/json' }, }) if (!resp.ok) return { loggedIn: false } @@ -214,4 +218,3 @@ async function handleDetectXiaohongshu() { return { loggedIn: false, error: e.message } } } - diff --git a/apps/extension/src/popup.html b/apps/extension/src/popup.html index 18ab19b..49e7a28 100644 --- a/apps/extension/src/popup.html +++ b/apps/extension/src/popup.html @@ -1,76 +1,82 @@ - + - - - - - -
      -

      COSE - 多平台文章同步

      -

      插件安装完成!

      -

      打开自托管的编辑器即可自动检测到,无需多余配置。

      -
      -

      没有自托管编辑器?

      - 也可使用官网编辑器 -
      - - + + + + + +
      +

      COSE - 多平台文章同步

      +

      插件安装完成!

      +

      + 打开自托管的编辑器即可自动检测到,无需多余配置。 +

      +
      +

      没有自托管编辑器?

      + 也可使用官网编辑器 +
      + + diff --git a/apps/extension/src/popup.js b/apps/extension/src/popup.js index c5eaad9..e2f277a 100644 --- a/apps/extension/src/popup.js +++ b/apps/extension/src/popup.js @@ -1,5 +1,5 @@ // Popup script for COSE extension -document.getElementById('openOfficial').addEventListener('click', (e) => { +document.getElementById('openOfficial').addEventListener('click', e => { e.preventDefault() chrome.tabs.create({ url: 'https://md.doocs.org' }) window.close() diff --git a/apps/extension/tsconfig.json b/apps/extension/tsconfig.json index 4380f6a..9935635 100644 --- a/apps/extension/tsconfig.json +++ b/apps/extension/tsconfig.json @@ -1,14 +1,12 @@ { - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "resolveJsonModule": true - }, - "include": [ - "scripts/**/*.ts" - ] -} \ No newline at end of file + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["scripts/**/*.ts"] +} diff --git a/apps/extension/vite.config.js b/apps/extension/vite.config.js index 837915a..2b1d9b6 100644 --- a/apps/extension/vite.config.js +++ b/apps/extension/vite.config.js @@ -3,43 +3,43 @@ import { join, resolve } from 'path' import { viteStaticCopy } from 'vite-plugin-static-copy' export default defineConfig({ - root: '.', // 项目根目录 - resolve: { - alias: { - '@cose/core': resolve(__dirname, '../../packages/core'), - '@cose/detection': resolve(__dirname, '../../packages/detection') - } + root: '.', // 项目根目录 + resolve: { + alias: { + '@cose/core': resolve(__dirname, '../../packages/core'), + '@cose/detection': resolve(__dirname, '../../packages/detection'), }, - build: { - outDir: 'dist', - emptyOutDir: true, - minify: false, // 方便调试,发布时可开启 - modulePreload: false, - rollupOptions: { - input: { - background: resolve(__dirname, 'src/background.js'), - content: resolve(__dirname, 'src/content.js'), - inject: resolve(__dirname, 'src/inject.js'), - offscreen: resolve(__dirname, 'src/offscreen.js'), - popup: resolve(__dirname, 'src/popup.js'), - }, - output: { - entryFileNames: 'bundles/[name].js', - chunkFileNames: 'bundles/chunks/[name].js', - assetFileNames: 'assets/[name].[ext]', - format: 'es', // ES Modules,适配 Manifest V3 - }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + minify: false, // 方便调试,发布时可开启 + modulePreload: false, + rollupOptions: { + input: { + background: resolve(__dirname, 'src/background.js'), + content: resolve(__dirname, 'src/content.js'), + inject: resolve(__dirname, 'src/inject.js'), + offscreen: resolve(__dirname, 'src/offscreen.js'), + popup: resolve(__dirname, 'src/popup.js'), + }, + output: { + entryFileNames: 'bundles/[name].js', + chunkFileNames: 'bundles/chunks/[name].js', + assetFileNames: 'assets/[name].[ext]', + format: 'es', // ES Modules,适配 Manifest V3 + }, + }, + }, + plugins: [ + viteStaticCopy({ + targets: [ + // manifest.json 由 scripts/cli.ts 生成 + { + src: 'icons', + dest: '.', }, - }, - plugins: [ - viteStaticCopy({ - targets: [ - // manifest.json 由 scripts/cli.ts 生成 - { - src: 'icons', - dest: '.', - }, - ], - }), - ], + ], + }), + ], }) diff --git a/package.json b/package.json index aedbb73..a38f7f9 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,21 @@ "dev:chrome": "pnpm -C apps/extension dev:chrome", "dev:watch": "pnpm -C apps/extension dev:watch", "lint": "pnpm -r lint", - "test": "node --test" + "format": "prettier --write .", + "format:check": "prettier --check .", + "test": "node --test", + "prepare": "simple-git-hooks" + }, + "simple-git-hooks": { + "pre-commit": "pnpm lint-staged" + }, + "lint-staged": { + "*.{js,ts,mjs,cjs,json,md,yml,yaml,html}": "prettier --write" }, "devDependencies": { + "lint-staged": "^16.1.6", + "prettier": "^3.6.2", + "simple-git-hooks": "^2.13.1", "typescript": "^5.9.3" } } diff --git a/packages/core/index.js b/packages/core/index.js index ebc2824..45b9be4 100644 --- a/packages/core/index.js +++ b/packages/core/index.js @@ -1,4 +1,4 @@ -export * from './src/utils.js'; +export * from './src/utils.js' // Re-export login detection from @cose/detection -export * from '@cose/detection'; +export * from '@cose/detection' // Platform exports will be handled dynamically or imported directly diff --git a/packages/core/package.json b/packages/core/package.json index 8a1f9f5..bb36d10 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,15 +1,15 @@ { - "name": "@cose/core", - "version": "1.0.0", - "description": "Core publishing logic for COSE", - "main": "index.js", - "type": "module", - "exports": { - ".": "./index.js", - "./platforms/*": "./src/platforms/*.js", - "./utils": "./src/utils.js" - }, - "dependencies": { - "@cose/detection": "workspace:*" - } -} \ No newline at end of file + "name": "@cose/core", + "version": "1.0.0", + "description": "Core publishing logic for COSE", + "main": "index.js", + "type": "module", + "exports": { + ".": "./index.js", + "./platforms/*": "./src/platforms/*.js", + "./utils": "./src/utils.js" + }, + "dependencies": { + "@cose/detection": "workspace:*" + } +} diff --git a/packages/core/src/platforms/alipayopen.js b/packages/core/src/platforms/alipayopen.js index cff87b1..00266f9 100644 --- a/packages/core/src/platforms/alipayopen.js +++ b/packages/core/src/platforms/alipayopen.js @@ -17,8 +17,8 @@ const AlipayOpenPlatform = { * @param {string} markdown - Markdown 内容 */ function fillAlipayOpenContent(title, markdown) { - const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)) - + const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) + return (async () => { try { console.log('[COSE] 支付宝开放平台 开始填充, 标题:', title) @@ -40,12 +40,15 @@ function fillAlipayOpenContent(title, markdown) { } } } - + console.log('[COSE] 支付宝开放平台 查找标题输入框:', !!titleInput) - + if (titleInput && title) { titleInput.focus() - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 })) @@ -66,13 +69,13 @@ function fillAlipayOpenContent(title, markdown) { // 使用 ClipboardEvent 模拟粘贴 Markdown 内容 const dt = new DataTransfer() dt.setData('text/plain', markdown) - + const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) - + editor.dispatchEvent(pasteEvent) console.log('[COSE] 支付宝开放平台 内容粘贴成功') @@ -80,8 +83,8 @@ function fillAlipayOpenContent(title, markdown) { let confirmed = false for (let i = 0; i < 15; i++) { await sleep(200) - const convertBtn = Array.from(document.querySelectorAll('button')).find( - btn => btn.textContent.includes('立即转换') + const convertBtn = Array.from(document.querySelectorAll('button')).find(btn => + btn.textContent.includes('立即转换') ) if (convertBtn) { convertBtn.click() diff --git a/packages/core/src/platforms/aliyun.js b/packages/core/src/platforms/aliyun.js index 77cb907..dd5b488 100644 --- a/packages/core/src/platforms/aliyun.js +++ b/packages/core/src/platforms/aliyun.js @@ -12,12 +12,12 @@ const AliyunPlatform = { // 阿里云开发者社区内容填充函数 async function fillAliyunContent(content) { const { title, markdown } = content - + console.log('[COSE] 阿里云开发者社区:开始填充内容') - + // 等待页面加载 await new Promise(resolve => setTimeout(resolve, 2000)) - + // 填充标题 const titleInput = document.querySelector('input[placeholder*="标题"]') if (titleInput && title) { @@ -27,16 +27,17 @@ async function fillAliyunContent(content) { titleInput.dispatchEvent(new Event('change', { bubbles: true })) console.log('[COSE] 阿里云开发者社区:标题已填充') } - + // 等待一下再填充正文 await new Promise(resolve => setTimeout(resolve, 500)) - + // 填充正文(markdown 编辑器) // 阿里云开发者社区使用的是 markdown 编辑器,textarea 是主要输入区域 - const contentTextarea = document.querySelector('textarea[class*="editor"]') || + const contentTextarea = + document.querySelector('textarea[class*="editor"]') || document.querySelector('.markdown-editor textarea') || document.querySelector('textarea') - + if (contentTextarea && markdown) { contentTextarea.focus() contentTextarea.value = markdown @@ -44,7 +45,7 @@ async function fillAliyunContent(content) { contentTextarea.dispatchEvent(new Event('change', { bubbles: true })) console.log('[COSE] 阿里云开发者社区:正文已填充') } - + console.log('[COSE] 阿里云开发者社区:内容填充完成') } diff --git a/packages/core/src/platforms/baijiahao.js b/packages/core/src/platforms/baijiahao.js index fe3dc86..bf9456c 100644 --- a/packages/core/src/platforms/baijiahao.js +++ b/packages/core/src/platforms/baijiahao.js @@ -17,8 +17,9 @@ async function fillBaijiahaoContent(content, waitFor, setInputValue) { // 1. 填充标题 // 百家号标题输入框在 .client_components_titleInput 内的 contenteditable div await new Promise(resolve => setTimeout(resolve, 1000)) - - const titleEditor = document.querySelector('.client_components_titleInput [contenteditable="true"]') || + + const titleEditor = + document.querySelector('.client_components_titleInput [contenteditable="true"]') || document.querySelector('.client_pages_edit_components_titleInput [contenteditable="true"]') || document.querySelector('[class*="titleInput"] [contenteditable="true"]') diff --git a/packages/core/src/platforms/csdn.js b/packages/core/src/platforms/csdn.js index 0a4089c..46c5a19 100644 --- a/packages/core/src/platforms/csdn.js +++ b/packages/core/src/platforms/csdn.js @@ -30,7 +30,9 @@ function fillCSDNContent(title, markdown, body) { await new Promise(resolve => setTimeout(resolve, 1000)) // CSDN 使用 contenteditable 的 PRE 元素 - const editor = document.querySelector('.editor__inner[contenteditable="true"], [contenteditable="true"].markdown-highlighting') + const editor = document.querySelector( + '.editor__inner[contenteditable="true"], [contenteditable="true"].markdown-highlighting' + ) if (editor) { editor.focus() @@ -93,4 +95,3 @@ async function syncCSDNContent(tab, content, helpers) { // 导出 export { CSDNPlatform, fillCSDNContent, syncCSDNContent } - diff --git a/packages/core/src/platforms/cto51.js b/packages/core/src/platforms/cto51.js index 6f2dfc2..6848562 100644 --- a/packages/core/src/platforms/cto51.js +++ b/packages/core/src/platforms/cto51.js @@ -1,70 +1,71 @@ // 51CTO 平台配置 const CTO51Platform = { - id: 'cto51', - name: '51CTO', - icon: 'https://blog.51cto.com/favicon.ico', - url: 'https://blog.51cto.com', - loginUrl: 'https://home.51cto.com/index/login', - publishUrl: 'https://blog.51cto.com/blogger/publish', - title: '51CTO', - type: 'cto51', + id: 'cto51', + name: '51CTO', + icon: 'https://blog.51cto.com/favicon.ico', + url: 'https://blog.51cto.com', + loginUrl: 'https://home.51cto.com/index/login', + publishUrl: 'https://blog.51cto.com/blogger/publish', + title: '51CTO', + type: 'cto51', } // 51CTO 内容填充函数 async function fillCTO51Content(content, waitFor, setInputValue) { - const { title, body, markdown } = content - const contentToFill = markdown || body || '' + const { title, body, markdown } = content + const contentToFill = markdown || body || '' - // 1. 填充标题 - // 51CTO 标题输入框通常是 input#title 或 placeholder="请输入标题" - const titleInput = await waitFor('#title, input[placeholder*="标题"]') - if (titleInput) { - setInputValue(titleInput, title) - console.log('[COSE] 51CTO 标题填充成功') + // 1. 填充标题 + // 51CTO 标题输入框通常是 input#title 或 placeholder="请输入标题" + const titleInput = await waitFor('#title, input[placeholder*="标题"]') + if (titleInput) { + setInputValue(titleInput, title) + console.log('[COSE] 51CTO 标题填充成功') + } + + // 2. 等待编辑器加载 + await new Promise(resolve => setTimeout(resolve, 2000)) + + // 3. 填充内容 + // 51CTO 有 Markdown 编辑器和富文本编辑器,通常默认 Markdown + // 尝试寻找 Markdown 编辑器的 textarea 或 CodeMirror + const editor = + document.querySelector('.editormd-markdown-textarea') || // Editor.md + document.querySelector('#my-editormd-markdown-doc') || // 常见 ID + document.querySelector('.CodeMirror textarea') || // CodeMirror 核心 + document.querySelector('textarea[name="content"]') // 通用 fallback + + if (editor) { + // 如果是 CodeMirror,通常需要操作 DOM 或使用 setValue + // 尝试直接设置 value 并触发事件 + editor.focus() + editor.value = contentToFill + editor.dispatchEvent(new Event('input', { bubbles: true })) + editor.dispatchEvent(new Event('change', { bubbles: true })) + + // 如果页面上有 editor.md 的全局实例,尝试调用 + // 这需要在 page context 执行,目前 fillContentOnPage 是在 Main world 执行的,所以可以访问 window + if (window.editor) { + try { + window.editor.setMarkdown(contentToFill) + console.log('[COSE] 51CTO 通过 window.editor 设置成功') + return + } catch (e) { + console.log('[COSE] 51CTO window.editor 调用失败', e) + } } - // 2. 等待编辑器加载 - await new Promise(resolve => setTimeout(resolve, 2000)) + console.log('[COSE] 51CTO textarea 填充尝试完成') + } else { + console.log('[COSE] 51CTO 未找到编辑器元素,尝试降级 contenteditable') - // 3. 填充内容 - // 51CTO 有 Markdown 编辑器和富文本编辑器,通常默认 Markdown - // 尝试寻找 Markdown 编辑器的 textarea 或 CodeMirror - const editor = document.querySelector('.editormd-markdown-textarea') || // Editor.md - document.querySelector('#my-editormd-markdown-doc') || // 常见 ID - document.querySelector('.CodeMirror textarea') || // CodeMirror 核心 - document.querySelector('textarea[name="content"]') // 通用 fallback - - if (editor) { - // 如果是 CodeMirror,通常需要操作 DOM 或使用 setValue - // 尝试直接设置 value 并触发事件 - editor.focus() - editor.value = contentToFill - editor.dispatchEvent(new Event('input', { bubbles: true })) - editor.dispatchEvent(new Event('change', { bubbles: true })) - - // 如果页面上有 editor.md 的全局实例,尝试调用 - // 这需要在 page context 执行,目前 fillContentOnPage 是在 Main world 执行的,所以可以访问 window - if (window.editor) { - try { - window.editor.setMarkdown(contentToFill) - console.log('[COSE] 51CTO 通过 window.editor 设置成功') - return - } catch (e) { - console.log('[COSE] 51CTO window.editor 调用失败', e) - } - } - - console.log('[COSE] 51CTO textarea 填充尝试完成') - } else { - console.log('[COSE] 51CTO 未找到编辑器元素,尝试降级 contenteditable') - - // 可能是富文本模式的 contenteditable - const contentEditable = document.querySelector('[contenteditable="true"]') - if (contentEditable) { - contentEditable.innerHTML = contentToFill.replace(/\n/g, '
      ') - console.log('[COSE] 51CTO contenteditable 填充成功') - } + // 可能是富文本模式的 contenteditable + const contentEditable = document.querySelector('[contenteditable="true"]') + if (contentEditable) { + contentEditable.innerHTML = contentToFill.replace(/\n/g, '
      ') + console.log('[COSE] 51CTO contenteditable 填充成功') } + } } // 导出 diff --git a/packages/core/src/platforms/douyin.js b/packages/core/src/platforms/douyin.js index 7666495..71589c6 100644 --- a/packages/core/src/platforms/douyin.js +++ b/packages/core/src/platforms/douyin.js @@ -4,12 +4,11 @@ const DouyinPlatform = { name: 'Douyin', icon: 'https://lf3-static.bytednsdoc.com/obj/eden-cn/yvahlyj_upfbvk_zlp/ljhwZthlaukjlkulzlp/pc_creator/favicon_v2_7145ff0.ico', url: 'https://creator.douyin.com/', - publishUrl: 'https://creator.douyin.com/creator-micro/content/post/article?default-tab=5&enter_from=publish_page&media_type=article&type=new', + publishUrl: + 'https://creator.douyin.com/creator-micro/content/post/article?default-tab=5&enter_from=publish_page&media_type=article&type=new', title: '抖音', type: 'douyin', } - - // 导出 export { DouyinPlatform } diff --git a/packages/core/src/platforms/huaweidev.js b/packages/core/src/platforms/huaweidev.js index 162f3ea..7fc8063 100644 --- a/packages/core/src/platforms/huaweidev.js +++ b/packages/core/src/platforms/huaweidev.js @@ -9,7 +9,5 @@ const HuaweiDevPlatform = { type: 'huaweidev', } - - // 导出 export { HuaweiDevPlatform } diff --git a/packages/core/src/platforms/index.js b/packages/core/src/platforms/index.js index f104479..1e91692 100644 --- a/packages/core/src/platforms/index.js +++ b/packages/core/src/platforms/index.js @@ -37,85 +37,85 @@ import { DoubanPlatform } from './douban.js' // 合并平台配置 const PLATFORMS = [ - CSDNPlatform, - JuejinPlatform, - WechatPlatform, - ZhihuPlatform, - ToutiaoPlatform, - SegmentFaultPlatform, - CnblogsPlatform, - OSChinaPlatform, - CTO51Platform, - InfoQPlatform, - JianshuPlatform, - BaijiahaoPlatform, - WangyihaoPlatform, - TencentCloudPlatform, - MediumPlatform, - SspaiPlatform, - SohuPlatform, - BilibiliPlatform, - WeiboPlatform, - AliyunPlatform, - HuaweiCloudPlatform, - HuaweiDevPlatform, - TwitterPlatform, - QianfanPlatform, - AlipayOpenPlatform, - ModelScopePlatform, - VolcenginePlatform, - DouyinPlatform, - XiaohongshuPlatform, - ElecfansPlatform, - DoubanPlatform, + CSDNPlatform, + JuejinPlatform, + WechatPlatform, + ZhihuPlatform, + ToutiaoPlatform, + SegmentFaultPlatform, + CnblogsPlatform, + OSChinaPlatform, + CTO51Platform, + InfoQPlatform, + JianshuPlatform, + BaijiahaoPlatform, + WangyihaoPlatform, + TencentCloudPlatform, + MediumPlatform, + SspaiPlatform, + SohuPlatform, + BilibiliPlatform, + WeiboPlatform, + AliyunPlatform, + HuaweiCloudPlatform, + HuaweiDevPlatform, + TwitterPlatform, + QianfanPlatform, + AlipayOpenPlatform, + ModelScopePlatform, + VolcenginePlatform, + DouyinPlatform, + XiaohongshuPlatform, + ElecfansPlatform, + DoubanPlatform, ] // 根据 hostname 获取平台填充函数 function getPlatformFiller(hostname) { - if (hostname.includes('csdn.net')) return 'csdn' - if (hostname.includes('juejin.cn')) return 'juejin' - if (hostname.includes('mp.weixin.qq.com')) return 'wechat' - if (hostname.includes('zhihu.com')) return 'zhihu' - if (hostname.includes('toutiao.com')) return 'toutiao' - if (hostname.includes('segmentfault.com')) return 'segmentfault' - if (hostname.includes('cnblogs.com')) return 'cnblogs' - if (hostname.includes('oschina.net')) return 'oschina' - if (hostname.includes('51cto.com')) return 'cto51' - if (hostname.includes('infoq.cn')) return 'infoq' - if (hostname.includes('jianshu.com')) return 'jianshu' - if (hostname.includes('baijiahao.baidu.com')) return 'baijiahao' - if (hostname.includes('mp.163.com')) return 'wangyihao' - if (hostname.includes('cloud.tencent.com')) return 'tencentcloud' - if (hostname.includes('medium.com')) return 'medium' - if (hostname.includes('sspai.com')) return 'sspai' - if (hostname.includes('mp.sohu.com')) return 'sohu' - if (hostname.includes('member.bilibili.com')) return 'bilibili' - if (hostname.includes('card.weibo.com')) return 'weibo' - if (hostname.includes('developer.aliyun.com')) return 'aliyun' - if (hostname.includes('bbs.huaweicloud.com')) return 'huaweicloud' - if (hostname.includes('developer.huawei.com')) return 'huaweidev' - if (hostname.includes('x.com') || hostname.includes('twitter.com')) return 'twitter' - if (hostname.includes('qianfan.cloud.baidu.com')) return 'qianfan' - if (hostname.includes('open.alipay.com')) return 'alipayopen' - if (hostname.includes('modelscope.cn')) return 'modelscope' - if (hostname.includes('developer.volcengine.com')) return 'volcengine' - if (hostname.includes('creator.douyin.com')) return 'douyin' - if (hostname.includes('creator.xiaohongshu.com')) return 'xiaohongshu' - if (hostname.includes('elecfans.com')) return 'elecfans' - if (hostname.includes('douban.com')) return 'douban' - return 'generic' + if (hostname.includes('csdn.net')) return 'csdn' + if (hostname.includes('juejin.cn')) return 'juejin' + if (hostname.includes('mp.weixin.qq.com')) return 'wechat' + if (hostname.includes('zhihu.com')) return 'zhihu' + if (hostname.includes('toutiao.com')) return 'toutiao' + if (hostname.includes('segmentfault.com')) return 'segmentfault' + if (hostname.includes('cnblogs.com')) return 'cnblogs' + if (hostname.includes('oschina.net')) return 'oschina' + if (hostname.includes('51cto.com')) return 'cto51' + if (hostname.includes('infoq.cn')) return 'infoq' + if (hostname.includes('jianshu.com')) return 'jianshu' + if (hostname.includes('baijiahao.baidu.com')) return 'baijiahao' + if (hostname.includes('mp.163.com')) return 'wangyihao' + if (hostname.includes('cloud.tencent.com')) return 'tencentcloud' + if (hostname.includes('medium.com')) return 'medium' + if (hostname.includes('sspai.com')) return 'sspai' + if (hostname.includes('mp.sohu.com')) return 'sohu' + if (hostname.includes('member.bilibili.com')) return 'bilibili' + if (hostname.includes('card.weibo.com')) return 'weibo' + if (hostname.includes('developer.aliyun.com')) return 'aliyun' + if (hostname.includes('bbs.huaweicloud.com')) return 'huaweicloud' + if (hostname.includes('developer.huawei.com')) return 'huaweidev' + if (hostname.includes('x.com') || hostname.includes('twitter.com')) return 'twitter' + if (hostname.includes('qianfan.cloud.baidu.com')) return 'qianfan' + if (hostname.includes('open.alipay.com')) return 'alipayopen' + if (hostname.includes('modelscope.cn')) return 'modelscope' + if (hostname.includes('developer.volcengine.com')) return 'volcengine' + if (hostname.includes('creator.douyin.com')) return 'douyin' + if (hostname.includes('creator.xiaohongshu.com')) return 'xiaohongshu' + if (hostname.includes('elecfans.com')) return 'elecfans' + if (hostname.includes('douban.com')) return 'douban' + return 'generic' } // 同步处理器映射 // 如果平台有自定义同步逻辑,在此注册处理器 // 未注册的平台将使用 background.js 中的通用填充逻辑 const SYNC_HANDLERS = { - csdn: syncCSDNContent, - juejin: syncJuejinContent, - wechat: syncWechatContent, - zhihu: syncZhihuContent, - toutiao: syncToutiaoContent, - wangyihao: syncWangyihaoContent, + csdn: syncCSDNContent, + juejin: syncJuejinContent, + wechat: syncWechatContent, + zhihu: syncZhihuContent, + toutiao: syncToutiaoContent, + wangyihao: syncWangyihaoContent, } // 导出 diff --git a/packages/core/src/platforms/infoq.js b/packages/core/src/platforms/infoq.js index 3c39f85..3849ae9 100644 --- a/packages/core/src/platforms/infoq.js +++ b/packages/core/src/platforms/infoq.js @@ -1,55 +1,57 @@ // InfoQ 平台配置 const InfoQPlatform = { - id: 'infoq', - name: 'InfoQ', - icon: 'https://static001.infoq.cn/static/write/img/write-favicon.jpg', - url: 'https://xie.infoq.cn', - // InfoQ 需要先调用 API 创建草稿获取 ID,不能直接访问 /draft/write - publishUrl: 'https://xie.infoq.cn/draft/write', // 这个 URL 仅作为占位,实际会被动态替换 - createDraftApi: 'https://xie.infoq.cn/api/v1/draft/create', - title: 'InfoQ', - type: 'infoq', + id: 'infoq', + name: 'InfoQ', + icon: 'https://static001.infoq.cn/static/write/img/write-favicon.jpg', + url: 'https://xie.infoq.cn', + // InfoQ 需要先调用 API 创建草稿获取 ID,不能直接访问 /draft/write + publishUrl: 'https://xie.infoq.cn/draft/write', // 这个 URL 仅作为占位,实际会被动态替换 + createDraftApi: 'https://xie.infoq.cn/api/v1/draft/create', + title: 'InfoQ', + type: 'infoq', } // InfoQ 内容填充函数 async function fillInfoQContent(content, waitFor, setInputValue) { - const { title, body, markdown } = content - const contentToFill = markdown || body || '' + const { title, body, markdown } = content + const contentToFill = markdown || body || '' - // 填充标题 - const titleInput = await waitFor('input[placeholder*="标题"], .title-input input, input.article-title') - if (titleInput) { - setInputValue(titleInput, title) - console.log('[COSE] InfoQ 标题填充成功') - } + // 填充标题 + const titleInput = await waitFor( + 'input[placeholder*="标题"], .title-input input, input.article-title' + ) + if (titleInput) { + setInputValue(titleInput, title) + console.log('[COSE] InfoQ 标题填充成功') + } - // 等待编辑器加载 - await new Promise(resolve => setTimeout(resolve, 1000)) + // 等待编辑器加载 + await new Promise(resolve => setTimeout(resolve, 1000)) - // InfoQ 使用自定义 Vue 编辑器,通过 readMarkdown 方法填充内容 - const gkEditor = document.querySelector('.gk-editor') - if (gkEditor && gkEditor.__vue__) { - const vm = gkEditor.__vue__ - if (typeof vm.readMarkdown === 'function') { - try { - vm.readMarkdown(contentToFill) - console.log('[COSE] InfoQ readMarkdown 填充成功') - return - } catch (e) { - console.log('[COSE] InfoQ readMarkdown 失败:', e.message) - } - } - } - - // 备用方案:尝试 CodeMirror - const cmElement = document.querySelector('.CodeMirror') - if (cmElement && cmElement.CodeMirror) { - cmElement.CodeMirror.setValue(contentToFill) - console.log('[COSE] InfoQ CodeMirror 填充成功') + // InfoQ 使用自定义 Vue 编辑器,通过 readMarkdown 方法填充内容 + const gkEditor = document.querySelector('.gk-editor') + if (gkEditor && gkEditor.__vue__) { + const vm = gkEditor.__vue__ + if (typeof vm.readMarkdown === 'function') { + try { + vm.readMarkdown(contentToFill) + console.log('[COSE] InfoQ readMarkdown 填充成功') return + } catch (e) { + console.log('[COSE] InfoQ readMarkdown 失败:', e.message) + } } + } - console.log('[COSE] InfoQ 未找到编辑器') + // 备用方案:尝试 CodeMirror + const cmElement = document.querySelector('.CodeMirror') + if (cmElement && cmElement.CodeMirror) { + cmElement.CodeMirror.setValue(contentToFill) + console.log('[COSE] InfoQ CodeMirror 填充成功') + return + } + + console.log('[COSE] InfoQ 未找到编辑器') } // 导出 diff --git a/packages/core/src/platforms/jianshu.js b/packages/core/src/platforms/jianshu.js index 9165cb6..0c11ad4 100644 --- a/packages/core/src/platforms/jianshu.js +++ b/packages/core/src/platforms/jianshu.js @@ -18,9 +18,14 @@ async function fillJianshuContent(content, waitFor, setInputValue) { const titleInput = await waitFor('input._24i7u, input[class*="title"]') if (titleInput) { titleInput.focus() - const inputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set + 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 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] 简书标题填充成功') @@ -32,12 +37,18 @@ async function fillJianshuContent(content, waitFor, setInputValue) { await new Promise(resolve => setTimeout(resolve, 500)) // 简书使用 textarea#arthur-editor 作为 Markdown 编辑器 - const editor = document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR') + const editor = + document.querySelector('#arthur-editor') || document.querySelector('textarea._3swFR') if (editor) { editor.focus() - const textareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 InputEvent('input', { bubbles: true, data: contentToFill, inputType: 'insertText' }) + ) editor.dispatchEvent(new Event('change', { bubbles: true })) console.log('[COSE] 简书内容填充成功') } else { diff --git a/packages/core/src/platforms/juejin.js b/packages/core/src/platforms/juejin.js index e48e0b7..e577d1f 100644 --- a/packages/core/src/platforms/juejin.js +++ b/packages/core/src/platforms/juejin.js @@ -87,4 +87,3 @@ async function syncJuejinContent(tab, content, helpers) { // 导出 export { JuejinPlatform, fillJuejinContent, syncJuejinContent } - diff --git a/packages/core/src/platforms/medium.js b/packages/core/src/platforms/medium.js index 85d5a0e..9c64a89 100644 --- a/packages/core/src/platforms/medium.js +++ b/packages/core/src/platforms/medium.js @@ -1,12 +1,12 @@ // Medium 平台配置 const MediumPlatform = { - id: 'medium', - name: 'Medium', - icon: 'https://cdn.simpleicons.org/medium', - url: 'https://medium.com', - publishUrl: 'https://medium.com/new-story', - title: 'Medium', - type: 'medium', + id: 'medium', + name: 'Medium', + icon: 'https://cdn.simpleicons.org/medium', + url: 'https://medium.com', + publishUrl: 'https://medium.com/new-story', + title: 'Medium', + type: 'medium', } // Medium 登录检测配置 @@ -19,42 +19,42 @@ const MediumPlatform = { * 3. 通过 paste 事件填充 HTML 内容到编辑器 */ async function fillMediumContent(content, waitFor, setInputValue) { - const { title, body, wechatHtml } = content - const htmlContent = wechatHtml || body || '' + const { title, body, wechatHtml } = content + const htmlContent = wechatHtml || body || '' - console.log('[COSE] Medium 开始同步...') + console.log('[COSE] Medium 开始同步...') - // 等待编辑器加载 - await new Promise(resolve => setTimeout(resolve, 2000)) + // 等待编辑器加载 + await new Promise(resolve => setTimeout(resolve, 2000)) - // 第一步:填充标题 - 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 标题填充成功') - } + // 第一步:填充标题 + 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 标题填充成功') + } - // 第二步:填充内容 - 使用 paste 事件 - const contentEl = document.querySelector('p.graf--p') - if (contentEl && htmlContent) { - contentEl.focus() + // 第二步:填充内容 - 使用 paste 事件 + 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, '')) + // 创建 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 - }) + const pasteEvent = new ClipboardEvent('paste', { + bubbles: true, + cancelable: true, + clipboardData: dt, + }) - contentEl.dispatchEvent(pasteEvent) - console.log('[COSE] Medium 内容填充成功') - } + contentEl.dispatchEvent(pasteEvent) + console.log('[COSE] Medium 内容填充成功') + } } // 导出 diff --git a/packages/core/src/platforms/modelscope.js b/packages/core/src/platforms/modelscope.js index ef3f592..746790d 100644 --- a/packages/core/src/platforms/modelscope.js +++ b/packages/core/src/platforms/modelscope.js @@ -11,7 +11,5 @@ const ModelScopePlatform = { type: 'modelscope', } - - // 导出 export { ModelScopePlatform } diff --git a/packages/core/src/platforms/oschina.js b/packages/core/src/platforms/oschina.js index c3cb78f..659ceda 100644 --- a/packages/core/src/platforms/oschina.js +++ b/packages/core/src/platforms/oschina.js @@ -1,77 +1,84 @@ // OSChina 平台配置 const OSChinaPlatform = { - id: 'oschina', - name: 'OSChina', - icon: 'https://wsrv.nl/?url=static.oschina.net/new-osc/img/favicon.ico', - url: 'https://www.oschina.net', - publishUrl: 'https://my.oschina.net/blog/ai-write', - title: '开源中国', - type: 'oschina', + id: 'oschina', + name: 'OSChina', + icon: 'https://wsrv.nl/?url=static.oschina.net/new-osc/img/favicon.ico', + url: 'https://www.oschina.net', + publishUrl: 'https://my.oschina.net/blog/ai-write', + title: '开源中国', + type: 'oschina', } // OSChina 内容填充函数 (AI 写作平台 - 切换到 Markdown 编辑器) async function fillOSChinaContent(content, waitFor, setInputValue) { - const { title, markdown, body } = content - const mdContent = markdown || body || '' + const { title, markdown, body } = content + const mdContent = markdown || body || '' - // 1. 切换到 MD 编辑器(如果当前不是) - const switchText = document.querySelector('.editor-switch-text') - if (switchText && switchText.textContent.includes('切换到MD编辑器')) { - const switchBtn = document.querySelector('.editor-switch-btn') || switchText.parentElement - if (switchBtn) { - switchBtn.click() - 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编辑器') - } - await new Promise(resolve => setTimeout(resolve, 2000)) - } + // 1. 切换到 MD 编辑器(如果当前不是) + const switchText = document.querySelector('.editor-switch-text') + if (switchText && switchText.textContent.includes('切换到MD编辑器')) { + const switchBtn = document.querySelector('.editor-switch-btn') || switchText.parentElement + if (switchBtn) { + switchBtn.click() + 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编辑器') + } + await new Promise(resolve => setTimeout(resolve, 2000)) } + } - // 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)) - 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 内容填充成功') + // 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 { - console.log('[COSE] OSChina 未找到 Markdown textarea') + 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)) + 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 内容填充成功') + } else { + console.log('[COSE] OSChina 未找到 Markdown textarea') + } } // 导出 diff --git a/packages/core/src/platforms/qianfan.js b/packages/core/src/platforms/qianfan.js index bf0092f..a3a1b69 100644 --- a/packages/core/src/platforms/qianfan.js +++ b/packages/core/src/platforms/qianfan.js @@ -11,8 +11,6 @@ const QianfanPlatform = { type: 'qianfan', } - - // 千帆平台拦截函数 // 在 MAIN world 中执行,拦截所有可能导致跳转到登录页的行为 // 包括:fetch, XHR, sendBeacon, location 跳转, window.open, Navigation API, History API @@ -26,7 +24,7 @@ function qianfanIntercept() { const FAKE_RESPONSE = JSON.stringify({ success: true, status: 200, - result: { id: 'cose-intercepted' } + result: { id: 'cose-intercepted' }, }) console.log('[COSE] 千帆拦截器开始安装...') @@ -36,14 +34,16 @@ function qianfanIntercept() { window.fetch = function (...args) { const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '' const opts = args[1] || {} - const method = (opts.method || (args[0]?.method) || 'GET').toUpperCase() + const method = (opts.method || args[0]?.method || 'GET').toUpperCase() if (url.includes(INTERCEPT_PATTERN) && method === 'POST') { console.log('[COSE] 拦截 fetch POST:', url, '(已拦截', ++blockedCount, '个)') - return Promise.resolve(new Response(FAKE_RESPONSE, { - status: 200, - headers: { 'Content-Type': 'application/json' } - })) + return Promise.resolve( + new Response(FAKE_RESPONSE, { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }) + ) } return originalFetch.apply(this, args) } @@ -66,7 +66,10 @@ function qianfanIntercept() { Object.defineProperty(self, 'readyState', { get: () => 4, configurable: true }) Object.defineProperty(self, 'status', { get: () => 200, configurable: true }) Object.defineProperty(self, 'statusText', { get: () => 'OK', configurable: true }) - Object.defineProperty(self, 'responseText', { get: () => FAKE_RESPONSE, configurable: true }) + Object.defineProperty(self, 'responseText', { + get: () => FAKE_RESPONSE, + configurable: true, + }) Object.defineProperty(self, 'response', { get: () => FAKE_RESPONSE, configurable: true }) self.dispatchEvent(new Event('readystatechange')) self.dispatchEvent(new Event('load')) @@ -123,7 +126,7 @@ function qianfanIntercept() { // ========== 拦截 Navigation API ========== if (window.navigation) { - window.navigation.addEventListener('navigate', (e) => { + window.navigation.addEventListener('navigate', e => { const destUrl = e.destination?.url || '' console.log('[COSE] Navigation API navigate 事件:', destUrl) if (destUrl.includes(LOGIN_URL_PATTERN)) { diff --git a/packages/core/src/platforms/sspai.js b/packages/core/src/platforms/sspai.js index d5d3e4e..3d98475 100644 --- a/packages/core/src/platforms/sspai.js +++ b/packages/core/src/platforms/sspai.js @@ -18,17 +18,21 @@ async function fillSspaiContent(content, waitFor, setInputValue) { // 1. 填充标题 - 少数派使用 textbox await new Promise(resolve => setTimeout(resolve, 1000)) - const titleInput = document.querySelector('textarea[placeholder*="标题"]') || + const titleInput = + document.querySelector('textarea[placeholder*="标题"]') || document.querySelector('input[placeholder*="标题"]') if (titleInput) { titleInput.focus() // 使用 native setter 来绕过 React/Vue 的受控组件 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set || + 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 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] 少数派标题填充成功') @@ -41,8 +45,8 @@ async function fillSspaiContent(content, waitFor, setInputValue) { // 3. 填充正文内容 // 少数派使用 ProseMirror 富文本编辑器 - const editor = document.querySelector('.ProseMirror') || - document.querySelector('[contenteditable="true"]') + const editor = + document.querySelector('.ProseMirror') || document.querySelector('[contenteditable="true"]') if (editor) { editor.focus() diff --git a/packages/core/src/platforms/tencentcloud.js b/packages/core/src/platforms/tencentcloud.js index 5e34571..a7d7c6f 100644 --- a/packages/core/src/platforms/tencentcloud.js +++ b/packages/core/src/platforms/tencentcloud.js @@ -1,12 +1,12 @@ // 腾讯云开发者平台配置 const TencentCloudPlatform = { - id: 'tencentcloud', - name: 'TencentCloud', - icon: 'https://cloudcache.tencent-cloud.com/qcloud/favicon.ico', - url: 'https://cloud.tencent.com/developer', - publishUrl: 'https://cloud.tencent.com/developer/article/write-new', - title: '腾讯云开发者社区', - type: 'tencentcloud', + id: 'tencentcloud', + name: 'TencentCloud', + icon: 'https://cloudcache.tencent-cloud.com/qcloud/favicon.ico', + url: 'https://cloud.tencent.com/developer', + publishUrl: 'https://cloud.tencent.com/developer/article/write-new', + title: '腾讯云开发者社区', + type: 'tencentcloud', } /** @@ -17,15 +17,15 @@ const TencentCloudPlatform = { * @returns {HTMLElement|null} 返回切换按钮元素,如果已经是 MD 编辑器则返回 null */ function findSwitchToMDButton() { - const headerBtns = document.querySelectorAll('.header-btn') - for (const btn of headerBtns) { - // 只有当按钮文本包含"MD"时才需要切换(说明当前是富文本编辑器) - // 如果按钮文本包含"富文本",说明已经是 MD 编辑器,不需要切换 - if (btn.textContent.includes('切换') && btn.textContent.includes('MD')) { - return btn - } + const headerBtns = document.querySelectorAll('.header-btn') + for (const btn of headerBtns) { + // 只有当按钮文本包含"MD"时才需要切换(说明当前是富文本编辑器) + // 如果按钮文本包含"富文本",说明已经是 MD 编辑器,不需要切换 + if (btn.textContent.includes('切换') && btn.textContent.includes('MD')) { + return btn } - return null + } + return null } /** @@ -36,51 +36,51 @@ function findSwitchToMDButton() { * @returns {Promise} 是否成功进入 MD 编辑器模式 */ async function ensureMarkdownEditor() { - // 等待页面加载完成 - await new Promise(resolve => setTimeout(resolve, 1000)) - - const switchBtn = findSwitchToMDButton() - - if (switchBtn) { - // 找到了"切换 MD 编辑器"按钮,说明当前是富文本编辑器,需要切换 - console.log('[COSE] TencentCloud 检测到富文本编辑器,正在切换到 MD 编辑器...') - switchBtn.click() - - // 等待切换完成 - await new Promise(resolve => setTimeout(resolve, 1500)) - - // 验证切换是否成功:检查 CodeMirror 是否存在 - const cm = document.querySelector('.CodeMirror') - if (cm && cm.CodeMirror) { - console.log('[COSE] TencentCloud 成功切换到 MD 编辑器') - return true - } - - // 如果 CodeMirror 还没加载,再等待一下 - await new Promise(resolve => setTimeout(resolve, 1000)) - const cmRetry = document.querySelector('.CodeMirror') - if (cmRetry && cmRetry.CodeMirror) { - console.log('[COSE] TencentCloud 成功切换到 MD 编辑器(延迟加载)') - return true - } - - console.error('[COSE] TencentCloud 切换失败:CodeMirror 未加载') - return false - } else { - // 没有找到"切换 MD 编辑器"按钮,说明已经是 MD 编辑器 - console.log('[COSE] TencentCloud 当前已是 MD 编辑器') - - // 验证 CodeMirror 是否存在 - const cm = document.querySelector('.CodeMirror') - if (cm && cm.CodeMirror) { - return true - } - - // 等待 CodeMirror 加载 - await new Promise(resolve => setTimeout(resolve, 1000)) - const cmRetry = document.querySelector('.CodeMirror') - return !!(cmRetry && cmRetry.CodeMirror) + // 等待页面加载完成 + await new Promise(resolve => setTimeout(resolve, 1000)) + + const switchBtn = findSwitchToMDButton() + + if (switchBtn) { + // 找到了"切换 MD 编辑器"按钮,说明当前是富文本编辑器,需要切换 + console.log('[COSE] TencentCloud 检测到富文本编辑器,正在切换到 MD 编辑器...') + switchBtn.click() + + // 等待切换完成 + await new Promise(resolve => setTimeout(resolve, 1500)) + + // 验证切换是否成功:检查 CodeMirror 是否存在 + const cm = document.querySelector('.CodeMirror') + if (cm && cm.CodeMirror) { + console.log('[COSE] TencentCloud 成功切换到 MD 编辑器') + return true } + + // 如果 CodeMirror 还没加载,再等待一下 + await new Promise(resolve => setTimeout(resolve, 1000)) + const cmRetry = document.querySelector('.CodeMirror') + if (cmRetry && cmRetry.CodeMirror) { + console.log('[COSE] TencentCloud 成功切换到 MD 编辑器(延迟加载)') + return true + } + + console.error('[COSE] TencentCloud 切换失败:CodeMirror 未加载') + return false + } else { + // 没有找到"切换 MD 编辑器"按钮,说明已经是 MD 编辑器 + console.log('[COSE] TencentCloud 当前已是 MD 编辑器') + + // 验证 CodeMirror 是否存在 + const cm = document.querySelector('.CodeMirror') + if (cm && cm.CodeMirror) { + return true + } + + // 等待 CodeMirror 加载 + await new Promise(resolve => setTimeout(resolve, 1000)) + const cmRetry = document.querySelector('.CodeMirror') + return !!(cmRetry && cmRetry.CodeMirror) + } } /** @@ -89,15 +89,15 @@ async function ensureMarkdownEditor() { * @returns {Promise} */ async function getCodeMirror(maxWait = 3000) { - const startTime = Date.now() - while (Date.now() - startTime < maxWait) { - const cm = document.querySelector('.CodeMirror') - if (cm && cm.CodeMirror) { - return cm.CodeMirror - } - await new Promise(resolve => setTimeout(resolve, 200)) + const startTime = Date.now() + while (Date.now() - startTime < maxWait) { + const cm = document.querySelector('.CodeMirror') + if (cm && cm.CodeMirror) { + return cm.CodeMirror } - return null + await new Promise(resolve => setTimeout(resolve, 200)) + } + return null } /** @@ -108,39 +108,42 @@ async function getCodeMirror(maxWait = 3000) { * 3. 填充内容到 CodeMirror */ async function fillTencentCloudContent(content, waitFor, setInputValue) { - const { title, body, markdown } = content - const contentToFill = markdown || body || '' + const { title, body, markdown } = content + const contentToFill = markdown || body || '' - console.log('[COSE] TencentCloud 开始同步...') - - // 第一步:确保进入 MD 编辑器模式 - const isMarkdownMode = await ensureMarkdownEditor() - if (!isMarkdownMode) { - console.error('[COSE] TencentCloud 错误:无法进入 MD 编辑器模式,请手动切换后重试') - return - } + console.log('[COSE] TencentCloud 开始同步...') - // 第二步:获取 CodeMirror 实例 - const codeMirror = await getCodeMirror(3000) - if (!codeMirror) { - console.error('[COSE] TencentCloud 错误:CodeMirror 未加载,请刷新页面后重试') - return - } + // 第一步:确保进入 MD 编辑器模式 + const isMarkdownMode = await ensureMarkdownEditor() + if (!isMarkdownMode) { + console.error('[COSE] TencentCloud 错误:无法进入 MD 编辑器模式,请手动切换后重试') + 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 实例 + const codeMirror = await getCodeMirror(3000) + if (!codeMirror) { + console.error('[COSE] TencentCloud 错误:CodeMirror 未加载,请刷新页面后重试') + return + } - // 第四步:填充内容到 CodeMirror - codeMirror.setValue(contentToFill) - console.log('[COSE] TencentCloud 内容填充成功') + // 第三步:填充标题 + 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 内容填充成功') } // 导出 diff --git a/packages/core/src/platforms/toutiao.js b/packages/core/src/platforms/toutiao.js index 63e8ccf..d40d8e0 100644 --- a/packages/core/src/platforms/toutiao.js +++ b/packages/core/src/platforms/toutiao.js @@ -15,7 +15,7 @@ import { injectUtils } from './common.js' function fillToutiaoContentInPage(title, body) { // 等待满足条件的元素出现 function waitForElement(predicate, timeout = 10000) { - return new Promise((resolve) => { + return new Promise(resolve => { const el = predicate() if (el) return resolve(el) @@ -43,9 +43,14 @@ function fillToutiaoContentInPage(title, body) { if (titleInput && title) { titleInput.focus() // 模拟用户输入 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 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) @@ -57,9 +62,7 @@ function fillToutiaoContentInPage(title, body) { await new Promise(resolve => setTimeout(resolve, 500)) // 头条使用 ProseMirror 富文本编辑器 - const editor = await waitForElement(() => - document.querySelector('.ProseMirror') - ) + const editor = await waitForElement(() => document.querySelector('.ProseMirror')) if (editor && body) { editor.focus() diff --git a/packages/core/src/platforms/twitter.js b/packages/core/src/platforms/twitter.js index 0831669..0264008 100644 --- a/packages/core/src/platforms/twitter.js +++ b/packages/core/src/platforms/twitter.js @@ -11,8 +11,6 @@ const TwitterPlatform = { type: 'twitter', } - - /** * 自定义 Markdown 渲染器 * 将 Markdown 转换为 Twitter Articles 支持的 HTML 格式 @@ -64,10 +62,7 @@ function createTwitterRenderer(marked) { // 行内代码 renderer.codespan = function (code) { - const escapedCode = code - .replace(/&/g, '&') - .replace(//g, '>') + const escapedCode = code.replace(/&/g, '&').replace(//g, '>') return `${escapedCode}` } @@ -119,7 +114,9 @@ function createTwitterRenderer(marked) { renderer.tablecell = function (content, flags) { const tag = flags.header ? 'th' : 'td' - const align = flags.align ? ` style="text-align: ${flags.align}; border: 1px solid #cfd9de; padding: 8px;"` : ' style="border: 1px solid #cfd9de; padding: 8px;"' + const align = flags.align + ? ` style="text-align: ${flags.align}; border: 1px solid #cfd9de; padding: 8px;"` + : ' style="border: 1px solid #cfd9de; padding: 8px;"' return `<${tag}${align}>${content}\n` } @@ -222,17 +219,26 @@ async function fillTwitterContent(content, waitFor, setInputValue) { htmlContent = body || markdown || '' if (!htmlContent.includes('<')) { // 如果是纯文本,简单转换为段落 - htmlContent = htmlContent.split('\n\n').map(p => `

      ${p}

      `).join('\n') + htmlContent = htmlContent + .split('\n\n') + .map(p => `

      ${p}

      `) + .join('\n') } } // 第一步:填充标题 // Twitter Articles 使用 textarea[placeholder="Add a title"] - const titleInput = await waitFor('textarea[placeholder="Add a title"], textarea[name="Article Title"]', 5000) + const titleInput = await waitFor( + 'textarea[placeholder="Add a title"], textarea[name="Article Title"]', + 5000 + ) if (titleInput && title) { titleInput.focus() // 使用 native setter 来绑定 React 的受控组件 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + 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 })) @@ -241,7 +247,10 @@ async function fillTwitterContent(content, waitFor, setInputValue) { // 第二步:填充内容 // Twitter Articles 使用 Draft.js 编辑器 - const contentEl = await waitFor('.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', 5000) + const contentEl = await waitFor( + '.public-DraftEditor-content[contenteditable="true"], .DraftEditor-root [contenteditable="true"]', + 5000 + ) if (contentEl && htmlContent) { contentEl.focus() @@ -253,7 +262,7 @@ async function fillTwitterContent(content, waitFor, setInputValue) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) contentEl.dispatchEvent(pasteEvent) diff --git a/packages/core/src/platforms/volcengine.js b/packages/core/src/platforms/volcengine.js index 9de43a6..70ae40c 100644 --- a/packages/core/src/platforms/volcengine.js +++ b/packages/core/src/platforms/volcengine.js @@ -9,7 +9,5 @@ const VolcenginePlatform = { type: 'volcengine', } - - // 导出 export { VolcenginePlatform } diff --git a/packages/core/src/platforms/wangyihao.js b/packages/core/src/platforms/wangyihao.js index cda83b4..f82c230 100644 --- a/packages/core/src/platforms/wangyihao.js +++ b/packages/core/src/platforms/wangyihao.js @@ -16,16 +16,22 @@ import { injectUtils } from './common.js' function fillWangyihaoContent(title, htmlBody) { async function fill() { // 1. 等待并填充标题 - 网易号使用 textarea.netease-textarea - const titleInput = await window.waitFor('textarea.netease-textarea', 10000) || - await window.waitFor('textarea[placeholder*="标题"]', 3000) + const titleInput = + (await window.waitFor('textarea.netease-textarea', 10000)) || + (await window.waitFor('textarea[placeholder*="标题"]', 3000)) if (titleInput && title) { titleInput.focus() // 使用 native setter 来绕过 React 的受控组件 - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set + const nativeSetter = Object.getOwnPropertyDescriptor( + window.HTMLTextAreaElement.prototype, + 'value' + ).set nativeSetter.call(titleInput, title) // 触发 React 能识别的事件 - titleInput.dispatchEvent(new InputEvent('input', { bubbles: true, data: title, inputType: 'insertText' })) + 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] 网易号标题已填充') @@ -34,8 +40,9 @@ function fillWangyihaoContent(title, htmlBody) { } // 2. 等待 Draft.js 编辑器出现 - const editor = await window.waitFor('.public-DraftEditor-content', 10000) || - await window.waitFor('[contenteditable="true"]', 3000) + const editor = + (await window.waitFor('.public-DraftEditor-content', 10000)) || + (await window.waitFor('[contenteditable="true"]', 3000)) if (editor && htmlBody) { editor.focus() @@ -54,7 +61,7 @@ function fillWangyihaoContent(title, htmlBody) { const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) editor.dispatchEvent(pasteEvent) diff --git a/packages/core/src/platforms/wechat.js b/packages/core/src/platforms/wechat.js index f3c4eae..812e3bb 100644 --- a/packages/core/src/platforms/wechat.js +++ b/packages/core/src/platforms/wechat.js @@ -7,7 +7,8 @@ const WechatPlatform = { icon: 'https://res.wx.qq.com/a/wx_fed/assets/res/NTI4MWU5.ico', url: 'https://mp.weixin.qq.com', // 先打开草稿箱,再自动点击新建 - publishUrl: 'https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10', + publishUrl: + 'https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10', title: '微信公众号', type: 'wechat', } @@ -17,26 +18,24 @@ function getEditorArea(editor) { } function isWechatTitleEditor(editor, titleEditor) { - return Boolean(editor) - && (editor === titleEditor || Boolean(editor.closest?.('.title-editor__input'))) + return ( + Boolean(editor) && (editor === titleEditor || Boolean(editor.closest?.('.title-editor__input'))) + ) } function pickWechatBodyProseMirrorCandidate(nodes, { titleInput, titleEditor } = {}) { const bodyCandidates = nodes.filter(editor => !isWechatTitleEditor(editor, titleEditor)) - if (bodyCandidates.length === 0) - return null - if (bodyCandidates.length === 1) - return bodyCandidates[0] + if (bodyCandidates.length === 0) return null + if (bodyCandidates.length === 1) return bodyCandidates[0] const byPlaceholder = bodyCandidates.find(editor => - (editor.textContent || '').includes('从这里开始写正文'), + (editor.textContent || '').includes('从这里开始写正文') ) - if (byPlaceholder) - return byPlaceholder + if (byPlaceholder) return byPlaceholder if (titleInput) { const band = titleInput.getBoundingClientRect() - const belowTitle = bodyCandidates.filter((editor) => { + const belowTitle = bodyCandidates.filter(editor => { const rect = editor.getBoundingClientRect() return rect.top >= band.bottom - 8 }) @@ -62,25 +61,24 @@ async function fillWechatContent(title, htmlBody) { return (editor?.clientHeight || 0) * (editor?.clientWidth || 0) } function isWechatTitleEditor(editor, titleEditor) { - return Boolean(editor) - && (editor === titleEditor || Boolean(editor.closest?.('.title-editor__input'))) + return ( + Boolean(editor) && + (editor === titleEditor || Boolean(editor.closest?.('.title-editor__input'))) + ) } function pickCandidate(nodes, { titleInput, titleEditor } = {}) { const bodyCandidates = nodes.filter(editor => !isWechatTitleEditor(editor, titleEditor)) - if (bodyCandidates.length === 0) - return null - if (bodyCandidates.length === 1) - return bodyCandidates[0] + if (bodyCandidates.length === 0) return null + if (bodyCandidates.length === 1) return bodyCandidates[0] const byPlaceholder = bodyCandidates.find(editor => - (editor.textContent || '').includes('从这里开始写正文'), + (editor.textContent || '').includes('从这里开始写正文') ) - if (byPlaceholder) - return byPlaceholder + if (byPlaceholder) return byPlaceholder if (titleInput) { const band = titleInput.getBoundingClientRect() - const belowTitle = bodyCandidates.filter((editor) => { + const belowTitle = bodyCandidates.filter(editor => { const rect = editor.getBoundingClientRect() return rect.top >= band.bottom - 8 }) @@ -93,8 +91,7 @@ async function fillWechatContent(title, htmlBody) { } const nodes = [...document.querySelectorAll('.ProseMirror')] - if (nodes.length === 0) - return null + if (nodes.length === 0) return null const titleInput = document.querySelector('#title') const titleEditor = document.querySelector('.title-editor__input .ProseMirror') @@ -105,8 +102,7 @@ async function fillWechatContent(title, htmlBody) { const start = Date.now() while (Date.now() - start < timeout) { const el = pickWechatBodyProseMirror() - if (el) - return el + if (el) return el await new Promise(r => setTimeout(r, 100)) } return pickWechatBodyProseMirror() @@ -129,12 +125,12 @@ async function fillWechatContent(title, htmlBody) { if (titleInput) { titleInput.focus() } - const nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set - || Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set + const nativeSetter = + Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set || + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set if (titleInput && nativeSetter) { nativeSetter.call(titleInput, title) - } - else if (titleInput) { + } else if (titleInput) { titleInput.value = title } if (titleInput) { @@ -168,14 +164,12 @@ async function fillWechatContent(title, htmlBody) { // 优先使用微信编辑器 JSAPI。合成 Ctrl+V 事件不会触发真实粘贴; // 直接写 innerHTML 也不会可靠同步到 ProseMirror 的文档模型。 if (window.__MP_Editor_JSAPI__ && typeof window.__MP_Editor_JSAPI__.invoke === 'function') { - injected = await new Promise((resolve) => { + injected = await new Promise(resolve => { let done = false const finish = (ok, err) => { - if (done) - return + if (done) return done = true - if (err) - injectError = err.message || String(err) + if (err) injectError = err.message || String(err) resolve(ok) } @@ -186,8 +180,7 @@ async function fillWechatContent(title, htmlBody) { sucCb: () => finish(true), errCb: err => finish(false, err), }) - } - catch (err) { + } catch (err) { finish(false, err) } @@ -197,8 +190,7 @@ async function fillWechatContent(title, htmlBody) { if (injected) { console.log('[COSE] 微信内容已通过 mp_editor_set_content 注入') await new Promise(r => setTimeout(r, 800)) - } - else { + } else { console.warn('[COSE] mp_editor_set_content 注入失败:', injectError) } } @@ -232,7 +224,7 @@ async function fillWechatContent(title, htmlBody) { return { success: hasEditorContent, - error: hasEditorContent ? undefined : (injectError || '正文注入后未检测到有效内容'), + error: hasEditorContent ? undefined : injectError || '正文注入后未检测到有效内容', wordCount, imageCount, titleFilled: titleInput?.value === title || titleEditor?.textContent?.trim() === title, @@ -240,16 +232,16 @@ async function fillWechatContent(title, htmlBody) { } return { success: false, error: '内容为空' } - } - catch (err) { + } catch (err) { return { success: false, error: err.message } } } // 微信公众号保存草稿函数(在页面主世界中执行) function saveWechatDraft() { - const saveDraftBtn = Array.from(document.querySelectorAll('button')) - .find(b => b.textContent.includes('保存为草稿')) + const saveDraftBtn = Array.from(document.querySelectorAll('button')).find(b => + b.textContent.includes('保存为草稿') + ) if (saveDraftBtn) { saveDraftBtn.click() console.log('[COSE] 已点击保存为草稿') @@ -271,27 +263,27 @@ async function syncWechatContent(tab, content, helpers) { // 步骤1:等待首页加载完成 console.log('[COSE] 微信公众号等待页面加载') await waitForTab(tab.id) - + // 注入公共工具函数(waitFor, setInputValue) await injectUtils(chrome, tab.id) - + // 步骤2:使用 MutationObserver 监听获取 token console.log('[COSE] 开始检测 token...') const [tokenResult] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - return new Promise((resolve) => { + return new Promise(resolve => { // 先检查当前页面是否已有 token const checkToken = () => { const urlMatch = window.location.href.match(/token=(\d+)/) if (urlMatch) return urlMatch[1] - + const links = document.querySelectorAll('a[href*="token"]') for (const link of links) { const match = link.href?.match(/token=(\d+)/) if (match) return match[1] } - + const scripts = document.querySelectorAll('script:not([src])') for (const script of scripts) { const content = script.textContent @@ -321,23 +313,23 @@ async function syncWechatContent(tab, content, helpers) { }, 10000) }) }, - world: 'MAIN' + world: 'MAIN', }) - + const token = tokenResult?.result - + if (!token) { console.error('[COSE] 无法从页面获取 token') return { success: false, message: '无法获取微信公众号 token,请确保已登录', tabId: tab.id } } - + // 步骤3:跳转到编辑器页面 const editorUrl = `https://mp.weixin.qq.com/cgi-bin/appmsg?t=media/appmsg_edit_v2&action=edit&isNew=1&type=10&token=${token}&lang=zh_CN` console.log('[COSE] 获取到 token:', token, '跳转到编辑器') - + await chrome.tabs.update(tab.id, { url: editorUrl }) await waitForTab(tab.id) - + // 使用剪贴板 HTML(带完整样式)或降级到 body const htmlContent = content.wechatHtml || content.body console.log('[COSE] 微信 HTML 内容长度:', htmlContent?.length || 0) @@ -347,7 +339,7 @@ async function syncWechatContent(tab, content, helpers) { const [editorResult] = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => { - return new Promise((resolve) => { + return new Promise(resolve => { const existing = document.querySelector('.ProseMirror') if (existing) return resolve(true) @@ -365,7 +357,7 @@ async function syncWechatContent(tab, content, helpers) { }, 15000) }) }, - world: 'MAIN' + world: 'MAIN', }) if (!editorResult?.result) { @@ -374,10 +366,10 @@ async function syncWechatContent(tab, content, helpers) { } console.log('[COSE] 编辑器已就绪,开始注入内容...') - + // 页面跳转后需要重新注入工具函数(waitFor, setInputValue) await injectUtils(chrome, tab.id) - + // 步骤5:填充内容 let result try { @@ -394,7 +386,7 @@ async function syncWechatContent(tab, content, helpers) { const fillResult = result?.[0]?.result console.log('[COSE] 微信填充结果:', JSON.stringify(fillResult, null, 2)) - + if (!fillResult?.success) { console.error('[COSE] 微信内容填充失败:', fillResult?.error) return { success: false, message: fillResult?.error || '内容填充失败', tabId: tab.id } @@ -414,9 +406,4 @@ async function syncWechatContent(tab, content, helpers) { } // 导出 -export { - WechatPlatform, - fillWechatContent, - pickWechatBodyProseMirrorCandidate, - syncWechatContent, -} +export { WechatPlatform, fillWechatContent, pickWechatBodyProseMirrorCandidate, syncWechatContent } diff --git a/packages/core/src/platforms/zhihu.js b/packages/core/src/platforms/zhihu.js index 902e070..0ccd4e5 100644 --- a/packages/core/src/platforms/zhihu.js +++ b/packages/core/src/platforms/zhihu.js @@ -17,7 +17,7 @@ import { injectUtils } from './common.js' function fillZhihuContent(title, markdown) { // 等待满足条件的元素出现(使用 MutationObserver) function waitForElement(predicate, timeout = 10000) { - return new Promise((resolve) => { + return new Promise(resolve => { const el = predicate() if (el) return resolve(el) @@ -57,7 +57,7 @@ function fillZhihuContent(title, markdown) { async function fillContent() { // 第一步:等待知乎编辑器完全加载(避免"草稿加载中"提示) await new Promise(resolve => setTimeout(resolve, 2000)) - + // 第二步:填充标题 async function fillTitle() { const titleInput = await window.waitFor('textarea[placeholder*="标题"]') @@ -65,7 +65,8 @@ function fillZhihuContent(title, markdown) { titleInput.focus() // 使用 nativeInputValueSetter 确保 React 识别变更 const nativeSetter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' + window.HTMLTextAreaElement.prototype, + 'value' )?.set if (nativeSetter) { nativeSetter.call(titleInput, title) @@ -77,10 +78,10 @@ function fillZhihuContent(title, markdown) { console.log('[COSE] 知乎标题填充成功') } } - + // 先填充标题 await fillTitle() - + // 再等待一下确保标题已保存 await new Promise(resolve => setTimeout(resolve, 500)) @@ -88,15 +89,15 @@ function fillZhihuContent(title, markdown) { const editorSelectors = [ '.public-DraftEditor-content', '[contenteditable="true"]', - '.DraftEditor-root' + '.DraftEditor-root', ] - + let editor = null for (const selector of editorSelectors) { editor = document.querySelector(selector) if (editor) break } - + if (!editor) { console.log('[COSE] 未找到知乎编辑器') return { success: false, error: 'Editor not found' } @@ -106,7 +107,7 @@ function fillZhihuContent(title, markdown) { const rect = editor.getBoundingClientRect() const centerX = rect.left + rect.width / 2 const centerY = rect.top + rect.height / 2 - + // 触发鼠标事件序列激活编辑器 for (const eventType of ['mousedown', 'mouseup', 'click']) { const event = new MouseEvent(eventType, { @@ -115,25 +116,25 @@ function fillZhihuContent(title, markdown) { view: window, clientX: centerX, clientY: centerY, - button: 0 + button: 0, }) editor.dispatchEvent(event) } - + // 聚焦编辑器 editor.focus() - + // 清空现有内容 document.execCommand('selectAll', false) document.execCommand('delete', false) - + // 等待编辑器状态更新 await new Promise(resolve => setTimeout(resolve, 100)) // 第三步:通过剪贴板 + 键盘事件模拟真实粘贴 // 这是触发知乎 Markdown 检测弹窗的关键方法 const contentToFill = markdown || '' - + if (!contentToFill) { console.log('[COSE] 没有 Markdown 内容需要填充') await fillTitle() @@ -143,44 +144,38 @@ function fillZhihuContent(title, markdown) { try { // 使用 ClipboardEvent 模拟粘贴 - 这是触发 Markdown 检测弹窗的关键 // execCommand('insertText') 不会触发弹窗 - + // 检查浏览器兼容性 if (typeof DataTransfer === 'undefined' || typeof ClipboardEvent === 'undefined') { throw new Error('浏览器不支持 DataTransfer 或 ClipboardEvent') } - + const dt = new DataTransfer() dt.setData('text/plain', contentToFill) - + const pasteEvent = new ClipboardEvent('paste', { bubbles: true, cancelable: true, - clipboardData: dt + clipboardData: dt, }) - + editor.focus() const dispatched = editor.dispatchEvent(pasteEvent) console.log('[COSE] 已触发 ClipboardEvent,dispatched:', dispatched) - + // 等待 Markdown 检测弹窗出现并点击"确认并解析" await new Promise(resolve => setTimeout(resolve, 500)) - - const parseClicked = await waitAndClickButton( - text => text.includes('确认并解析'), - 5000 - ) - + + const parseClicked = await waitAndClickButton(text => text.includes('确认并解析'), 5000) + if (parseClicked) { console.log('[COSE] 已点击"确认并解析"') - + // 等待解析完成并点击"确认" await new Promise(resolve => setTimeout(resolve, 500)) - - const confirmClicked = await waitAndClickButton( - text => text === '确认', - 5000 - ) - + + const confirmClicked = await waitAndClickButton(text => text === '确认', 5000) + if (confirmClicked) { console.log('[COSE] 已点击"确认",Markdown 解析完成') } @@ -239,11 +234,11 @@ async function syncZhihuContent(tab, content, helpers) { if (fillResult?.success) { // 等待 2 秒确保内容已保存 await new Promise(resolve => setTimeout(resolve, 2000)) - + // 等待图片上传完成后再刷新 console.log('[COSE] 开始监听图片上传请求...') const uploadComplete = await waitForImageUploadComplete(tab.id) - + if (uploadComplete) { console.log('[COSE] 图片上传完成,准备刷新页面') try { @@ -259,7 +254,7 @@ async function syncZhihuContent(tab, content, helpers) { } else { console.log('[COSE] 未检测到图片上传请求或超时,跳过刷新') } - + return { success: true, message: '已打开知乎并同步内容', tabId: tab.id } } else { return { success: false, message: fillResult?.error || '内容同步失败', tabId: tab.id } @@ -274,40 +269,40 @@ async function syncZhihuContent(tab, content, helpers) { */ async function waitForImageUploadComplete(tabId, timeout = 30000) { const startTime = Date.now() - + // 在页面中注入监听脚本 const result = await globalThis.chrome.scripting.executeScript({ target: { tabId: tabId }, func: () => { - return new Promise((resolve) => { + return new Promise(resolve => { const pendingUploads = new Map() // uploadId -> { url, completed } let hasUploadRequests = false let lastUploadTime = 0 - + // 检查是否所有上传都完成 const checkAllComplete = () => { if (pendingUploads.size === 0) return false - + for (const [id, info] of pendingUploads) { if (!info.completed) return false } return true } - + // 监听 fetch 请求 const originalFetch = window.fetch - window.fetch = function(...args) { + window.fetch = function (...args) { const url = args[0] const options = args[1] || {} - + // 检测图片上传请求(知乎的图片上传通常包含这些特征) - const isImageUpload = typeof url === 'string' && ( - url.includes('/api/v4/images') || - url.includes('/api/v4/upload') || - url.includes('upload') || - (options.method === 'POST' && url.includes('zhihu.com')) - ) - + const isImageUpload = + typeof url === 'string' && + (url.includes('/api/v4/images') || + url.includes('/api/v4/upload') || + url.includes('upload') || + (options.method === 'POST' && url.includes('zhihu.com'))) + if (isImageUpload) { hasUploadRequests = true lastUploadTime = Date.now() @@ -315,8 +310,9 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { pendingUploads.set(uploadId, { url, completed: false }) console.log('[COSE] 检测到图片上传请求:', url, uploadId) } - - return originalFetch.apply(this, args) + + return originalFetch + .apply(this, args) .then(response => { if (isImageUpload) { console.log('[COSE] 图片上传请求完成:', url, response.status) @@ -344,32 +340,32 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { throw error }) } - + // 监听 XMLHttpRequest const originalOpen = XMLHttpRequest.prototype.open const originalSend = XMLHttpRequest.prototype.send - - XMLHttpRequest.prototype.open = function(method, url, ...rest) { + + XMLHttpRequest.prototype.open = function (method, url, ...rest) { this._url = url this._method = method return originalOpen.apply(this, [method, url, ...rest]) } - - XMLHttpRequest.prototype.send = function(...args) { - const isImageUpload = this._url && ( - this._url.includes('/api/v4/images') || - this._url.includes('/api/v4/upload') || - this._url.includes('upload') || - (this._method === 'POST' && this._url.includes('zhihu.com')) - ) - + + XMLHttpRequest.prototype.send = function (...args) { + const isImageUpload = + this._url && + (this._url.includes('/api/v4/images') || + this._url.includes('/api/v4/upload') || + this._url.includes('upload') || + (this._method === 'POST' && this._url.includes('zhihu.com'))) + if (isImageUpload) { hasUploadRequests = true lastUploadTime = Date.now() const uploadId = Date.now() + Math.random() pendingUploads.set(uploadId, { url: this._url, completed: false }) console.log('[COSE] 检测到图片上传 XHR:', this._url, uploadId) - + this.addEventListener('loadend', () => { console.log('[COSE] 图片上传 XHR 完成:', this._url, this.status) // 标记为已完成 @@ -381,10 +377,10 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { } }) } - + return originalSend.apply(this, args) } - + // 定期检查是否所有上传都完成 const checkTimer = setInterval(() => { // 如果没有检测到任何上传请求,说明可能没有图片需要上传 @@ -394,7 +390,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { resolve(true) return } - + // 如果所有上传都完成,并且距离最后一个上传请求已经过去2秒(确保没有新请求) if (checkAllComplete() && Date.now() - lastUploadTime > 2000) { console.log('[COSE] 所有图片上传请求已完成') @@ -403,7 +399,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { return } }, 500) - + // 10秒后如果没有检测到上传请求,认为没有图片需要上传 setTimeout(() => { if (!hasUploadRequests) { @@ -412,7 +408,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { resolve(true) } }, 10000) - + // 超时后无论如何都返回 setTimeout(() => { console.log('[COSE] 等待图片上传超时') @@ -423,14 +419,14 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) { }, world: 'MAIN', }) - + // 等待监听结果 const uploadResult = result?.[0]?.result console.log('[COSE] 图片上传监听结果:', uploadResult) - + // 给一个额外的缓冲时间,确保图片已经完全加载和渲染 await new Promise(resolve => setTimeout(resolve, 2000)) - + return uploadResult !== false } diff --git a/packages/core/src/utils.js b/packages/core/src/utils.js index 0692368..baaffe5 100644 --- a/packages/core/src/utils.js +++ b/packages/core/src/utils.js @@ -7,7 +7,7 @@ function injectCommonUtils() { // 等待元素出现的工具函数(使用 MutationObserver) window.waitFor = (selector, timeout = 10000) => { - return new Promise((resolve) => { + return new Promise(resolve => { const el = document.querySelector(selector) if (el) return resolve(el) @@ -33,11 +33,9 @@ function injectCommonUtils() { el.focus() if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { // 使用 native setter 确保 React/Vue 等框架能检测到变化 - const nativeSetter = Object.getOwnPropertyDescriptor( - window.HTMLTextAreaElement.prototype, 'value' - )?.set || Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, 'value' - )?.set + const nativeSetter = + Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set || + Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set if (nativeSetter) { nativeSetter.call(el, value) } else { diff --git a/packages/detection/package.json b/packages/detection/package.json index 9e51970..0879f65 100644 --- a/packages/detection/package.json +++ b/packages/detection/package.json @@ -1,11 +1,11 @@ { - "name": "@cose/detection", - "version": "1.0.0", - "description": "Platform login detection for COSE", - "main": "index.js", - "type": "module", - "exports": { - ".": "./index.js", - "./platforms/*": "./src/platforms/*.js" - } -} \ No newline at end of file + "name": "@cose/detection", + "version": "1.0.0", + "description": "Platform login detection for COSE", + "main": "index.js", + "type": "module", + "exports": { + ".": "./index.js", + "./platforms/*": "./src/platforms/*.js" + } +} diff --git a/packages/detection/src/configs.js b/packages/detection/src/configs.js index b423ade..29ec674 100644 --- a/packages/detection/src/configs.js +++ b/packages/detection/src/configs.js @@ -1,6 +1,6 @@ /** * @cose/detection - Platform login detection module - * + * * This package provides login detection configurations for all supported platforms. * Each config includes: * - api: The API endpoint to check login status @@ -11,65 +11,68 @@ // 掘金 export const JuejinLoginConfig = { - api: 'https://api.juejin.cn/user_api/v1/user/get', - method: 'GET', - checkLogin: (response) => response?.err_no === 0 && response?.data?.user_id, - getUserInfo: (response) => ({ - username: response?.data?.user_name, - avatar: response?.data?.avatar_large, - }), + api: 'https://api.juejin.cn/user_api/v1/user/get', + method: 'GET', + checkLogin: response => response?.err_no === 0 && response?.data?.user_id, + getUserInfo: response => ({ + username: response?.data?.user_name, + avatar: response?.data?.avatar_large, + }), } // 知乎 export const ZhihuLoginConfig = { - api: 'https://www.zhihu.com/api/v4/me', - method: 'GET', - checkLogin: (response) => response?.id, - getUserInfo: (response) => ({ - username: response?.name, - avatar: response?.avatar_url, - }), + api: 'https://www.zhihu.com/api/v4/me', + method: 'GET', + checkLogin: response => response?.id, + getUserInfo: response => ({ + username: response?.name, + avatar: response?.avatar_url, + }), } // 头条号 export const ToutiaoLoginConfig = { - api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231', - method: 'GET', - checkLogin: (response) => response?.code === 0 && response?.name, - getUserInfo: (response) => ({ - username: response?.name, - avatar: response?.avatar_url, - }), + api: 'https://mp.toutiao.com/mp/agw/creator_center/user_info?app_id=1231', + method: 'GET', + checkLogin: response => response?.code === 0 && response?.name, + getUserInfo: response => ({ + username: response?.name, + avatar: response?.avatar_url, + }), } // 百家号 export const BaijiahaoLoginConfig = { - api: 'https://baijiahao.baidu.com/builder/app/appinfo', - method: 'GET', - checkLogin: (response) => response?.errno === 0 && response?.data?.user?.name, - getUserInfo: (response) => ({ - username: response?.data?.user?.name, - avatar: response?.data?.user?.avatar, - }), + api: 'https://baijiahao.baidu.com/builder/app/appinfo', + method: 'GET', + checkLogin: response => response?.errno === 0 && response?.data?.user?.name, + getUserInfo: response => ({ + username: response?.data?.user?.name, + avatar: response?.data?.user?.avatar, + }), } // 抖音 export const DouyinLoginConfig = { - api: 'https://creator.douyin.com/web/api/media/user/info/', - method: 'GET', - checkLogin: (response) => response?.status_code === 0 && (response?.user?.uid || response?.user_info?.uid), - getUserInfo: (response) => ({ - username: response?.user?.nickname || response?.user_info?.nickname, - avatar: (response?.user?.avatar_thumb?.url_list?.[0] || response?.user_info?.avatar_thumb?.url_list?.[0]), - }), + api: 'https://creator.douyin.com/web/api/media/user/info/', + method: 'GET', + checkLogin: response => + response?.status_code === 0 && (response?.user?.uid || response?.user_info?.uid), + getUserInfo: response => ({ + username: response?.user?.nickname || response?.user_info?.nickname, + avatar: + response?.user?.avatar_thumb?.url_list?.[0] || + response?.user_info?.avatar_thumb?.url_list?.[0], + }), } // 统一的 LOGIN_CHECK_CONFIG 对象(按平台 ID 索引) export const LOGIN_CHECK_CONFIG = { - juejin: JuejinLoginConfig, - zhihu: ZhihuLoginConfig, - toutiao: ToutiaoLoginConfig, + juejin: JuejinLoginConfig, + zhihu: ZhihuLoginConfig, + toutiao: ToutiaoLoginConfig, - baijiahao: BaijiahaoLoginConfig, - douyin: DouyinLoginConfig, + baijiahao: BaijiahaoLoginConfig, + douyin: DouyinLoginConfig, } diff --git a/packages/detection/src/detect.js b/packages/detection/src/detect.js index 0bc9ad3..4561594 100644 --- a/packages/detection/src/detect.js +++ b/packages/detection/src/detect.js @@ -29,54 +29,54 @@ import { detectDoubanUser } from './platforms/douban.js' // Platform-specific detectors map const PLATFORM_DETECTORS = { - 'csdn': detectCSDNUser, - 'oschina': detectOSChinaUser, - 'alipayopen': detectAlipayUser, - 'weibo': detectWeiboUser, - 'wechat': detectWechatUser, - 'xiaohongshu': detectXiaohongshuUser, - 'elecfans': detectElecfansUser, - 'huaweicloud': detectHuaweiCloudUser, - 'huaweidev': detectHuaweiDevUser, - 'sspai': detectSspaiUser, - 'aliyun': detectAliyunUser, - 'sohu': detectSohuUser, - 'medium': detectMediumUser, - 'tencentcloud': detectTencentCloudUser, - 'qianfan': detectQianfanUser, - 'twitter': detectTwitterUser, - 'bilibili': detectBilibiliUser, - 'cto51': detectCTO51User, - 'jianshu': detectJianshuUser, - 'segmentfault': detectSegmentFaultUser, - 'infoq': detectInfoQUser, - 'modelscope': detectModelScopeUser, - 'volcengine': detectVolcengineUser, - 'cnblogs': detectCnblogsUser, - 'wangyihao': detectWangyihaoUser, - 'douban': detectDoubanUser, + csdn: detectCSDNUser, + oschina: detectOSChinaUser, + alipayopen: detectAlipayUser, + weibo: detectWeiboUser, + wechat: detectWechatUser, + xiaohongshu: detectXiaohongshuUser, + elecfans: detectElecfansUser, + huaweicloud: detectHuaweiCloudUser, + huaweidev: detectHuaweiDevUser, + sspai: detectSspaiUser, + aliyun: detectAliyunUser, + sohu: detectSohuUser, + medium: detectMediumUser, + tencentcloud: detectTencentCloudUser, + qianfan: detectQianfanUser, + twitter: detectTwitterUser, + bilibili: detectBilibiliUser, + cto51: detectCTO51User, + jianshu: detectJianshuUser, + segmentfault: detectSegmentFaultUser, + infoq: detectInfoQUser, + modelscope: detectModelScopeUser, + volcengine: detectVolcengineUser, + cnblogs: detectCnblogsUser, + wangyihao: detectWangyihaoUser, + douban: detectDoubanUser, } export async function detectUser(platformId) { - console.log(`[COSE] Detection: Checking ${platformId}`) + console.log(`[COSE] Detection: Checking ${platformId}`) - // 1. Platform-specific Detectors - if (PLATFORM_DETECTORS[platformId]) { - return PLATFORM_DETECTORS[platformId]() + // 1. Platform-specific Detectors + if (PLATFORM_DETECTORS[platformId]) { + return PLATFORM_DETECTORS[platformId]() + } + + // 2. Generic Config-based Detection + const config = LOGIN_CHECK_CONFIG[platformId] + if (config) { + if (config.useCookie || (config.cookieNames && config.cookieNames.length > 0)) { + return checkLoginByCookie(platformId, config) } - // 2. Generic Config-based Detection - const config = LOGIN_CHECK_CONFIG[platformId] - if (config) { - if (config.useCookie || (config.cookieNames && config.cookieNames.length > 0)) { - return checkLoginByCookie(platformId, config) - } - - // Default to API check if API is defined - if (config.api) { - return detectByApi(platformId, config) - } + // Default to API check if API is defined + if (config.api) { + return detectByApi(platformId, config) } + } - return { loggedIn: false, error: 'No detection available' } + return { loggedIn: false, error: 'No detection available' } } diff --git a/packages/detection/src/platforms/alipay.js b/packages/detection/src/platforms/alipay.js index 96a032a..d2f5a88 100644 --- a/packages/detection/src/platforms/alipay.js +++ b/packages/detection/src/platforms/alipay.js @@ -5,90 +5,93 @@ * 2. If cache miss, inject script into open Alipay tab to call API */ export async function detectAlipayUser() { - const platformId = 'alipayopen' - try { - // 从 storage 读取缓存的用户信息 - const stored = await chrome.storage.local.get('alipayopen_user') - const cachedUser = stored.alipayopen_user + const platformId = 'alipayopen' + try { + // 从 storage 读取缓存的用户信息 + const stored = await chrome.storage.local.get('alipayopen_user') + const cachedUser = stored.alipayopen_user - if (cachedUser && cachedUser.loggedIn) { - // 检查缓存是否过期(1小时) - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 1 * 60 * 60 * 1000 // 1 hour + if (cachedUser && cachedUser.loggedIn) { + // 检查缓存是否过期(1小时) + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 1 * 60 * 60 * 1000 // 1 hour - if (cacheAge < maxAge) { - console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username) - return { - loggedIn: true, - username: cachedUser.username || '', - avatar: cachedUser.avatar || '' - } - } else { - console.log(`[COSE] alipayopen 缓存已过期`) - // 清除过期缓存 - await chrome.storage.local.remove('alipayopen_user') - } + if (cacheAge < maxAge) { + console.log(`[COSE] alipayopen 从缓存读取用户信息:`, cachedUser.username) + return { + loggedIn: true, + username: cachedUser.username || '', + avatar: cachedUser.avatar || '', } - - // 尝试从已打开的支付宝页面获取用户信息并缓存 - let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' }) - if (tabs.length === 0) { - tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' }) - } - - if (tabs.length > 0) { - try { - // 在已打开的页面上下文中调用 API - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: 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 null - return await response.json() - } catch (e) { - return null - } - } - }) - - const data = results?.[0]?.result - console.log(`[COSE] alipayopen API 数据:`, data) - - if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) { - const username = data.data.nickname || '' - const avatar = data.data.avatar || '' - - // 缓存用户信息 - await chrome.storage.local.set({ - alipayopen_user: { - loggedIn: true, - username, - avatar, - cachedAt: Date.now() - } - }) - - console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像') - return { loggedIn: true, username, avatar } - } - } catch (e) { - console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message) - } - } - - console.log(`[COSE] alipayopen 未检测到登录状态`) - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] alipayopen 检测失败:`, e.message) - return { loggedIn: false, error: e.message } + } else { + console.log(`[COSE] alipayopen 缓存已过期`) + // 清除过期缓存 + await chrome.storage.local.remove('alipayopen_user') + } } + + // 尝试从已打开的支付宝页面获取用户信息并缓存 + let tabs = await chrome.tabs.query({ url: 'https://open.alipay.com/*' }) + if (tabs.length === 0) { + tabs = await chrome.tabs.query({ url: 'https://*.alipay.com/*' }) + } + + if (tabs.length > 0) { + try { + // 在已打开的页面上下文中调用 API + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: 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 null + return await response.json() + } catch (e) { + return null + } + }, + }) + + const data = results?.[0]?.result + console.log(`[COSE] alipayopen API 数据:`, data) + + if (data?.stat === 'ok' && data?.data?.isLoginUser === 1) { + const username = data.data.nickname || '' + const avatar = data.data.avatar || '' + + // 缓存用户信息 + await chrome.storage.local.set({ + alipayopen_user: { + loggedIn: true, + username, + avatar, + cachedAt: Date.now(), + }, + }) + + console.log(`[COSE] alipayopen 用户信息:`, username, avatar ? '有头像' : '无头像') + return { loggedIn: true, username, avatar } + } + } catch (e) { + console.log(`[COSE] alipayopen 从页面获取用户信息失败:`, e.message) + } + } + + console.log(`[COSE] alipayopen 未检测到登录状态`) + return { loggedIn: false } + } catch (e) { + console.log(`[COSE] alipayopen 检测失败:`, e.message) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/aliyun.js b/packages/detection/src/platforms/aliyun.js index afb2b36..d67b92a 100644 --- a/packages/detection/src/platforms/aliyun.js +++ b/packages/detection/src/platforms/aliyun.js @@ -8,24 +8,29 @@ import { convertAvatarToBase64 } from '../utils.js' * 3. Convert avatar to base64 to bypass CORS/ORB */ export async function detectAliyunUser() { - try { - const ticketCookie = await chrome.cookies.get({ url: 'https://developer.aliyun.com', name: 'login_aliyunid_ticket' }) - if (!ticketCookie || !ticketCookie.value) return { loggedIn: false } + try { + const ticketCookie = await chrome.cookies.get({ + url: 'https://developer.aliyun.com', + name: 'login_aliyunid_ticket', + }) + if (!ticketCookie || !ticketCookie.value) return { loggedIn: false } - const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json' } - }) - const data = await response.json() + const response = await fetch('https://developer.aliyun.com/developer/api/my/user/getUser', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' }, + }) + const data = await response.json() - if (data.success && data.data?.nickname) { - let avatar = data.data.avatar || '' - if (avatar) { - avatar = await convertAvatarToBase64(avatar, 'https://developer.aliyun.com/') - } - return { loggedIn: true, username: data.data.nickname, avatar } - } - return { loggedIn: false } - } catch (e) { return { loggedIn: false } } + if (data.success && data.data?.nickname) { + let avatar = data.data.avatar || '' + if (avatar) { + avatar = await convertAvatarToBase64(avatar, 'https://developer.aliyun.com/') + } + return { loggedIn: true, username: data.data.nickname, avatar } + } + return { loggedIn: false } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/bilibili.js b/packages/detection/src/platforms/bilibili.js index cb48473..c1e7a2e 100644 --- a/packages/detection/src/platforms/bilibili.js +++ b/packages/detection/src/platforms/bilibili.js @@ -8,32 +8,32 @@ import { convertAvatarToBase64 } from '../utils.js' * 3. Convert hdslb.com avatar to base64 data URL to bypass CORS/ORB */ export async function detectBilibiliUser() { - try { - const response = await fetch('https://api.bilibili.com/x/web-interface/nav', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - 'Cache-Control': 'no-cache', - }, - }) - const data = await response.json() + try { + const response = await fetch('https://api.bilibili.com/x/web-interface/nav', { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + 'Cache-Control': 'no-cache', + }, + }) + const data = await response.json() - if (data?.code !== 0 || !data?.data?.isLogin) { - return { loggedIn: false } - } - - const username = data.data.uname || '' - let avatar = data.data.face || '' - - // Convert hdslb.com avatar to base64 data URL to bypass CORS/ORB - if (avatar && avatar.includes('hdslb.com')) { - avatar = await convertAvatarToBase64(avatar, 'https://www.bilibili.com/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.log(`[COSE] bilibili 检测失败:`, e.message) - return { loggedIn: false } + if (data?.code !== 0 || !data?.data?.isLogin) { + return { loggedIn: false } } + + const username = data.data.uname || '' + let avatar = data.data.face || '' + + // Convert hdslb.com avatar to base64 data URL to bypass CORS/ORB + if (avatar && avatar.includes('hdslb.com')) { + avatar = await convertAvatarToBase64(avatar, 'https://www.bilibili.com/') + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.log(`[COSE] bilibili 检测失败:`, e.message) + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/cnblogs.js b/packages/detection/src/platforms/cnblogs.js index f07a96e..90503ff 100644 --- a/packages/detection/src/platforms/cnblogs.js +++ b/packages/detection/src/platforms/cnblogs.js @@ -6,25 +6,25 @@ import { convertAvatarToBase64 } from '../utils.js' * where cookies are sent automatically in document context. */ export async function detectCnblogsUser() { - try { - console.log('[COSE] Cnblogs Detection: Starting (offscreen)') + try { + console.log('[COSE] Cnblogs Detection: Starting (offscreen)') - if (typeof globalThis.__coseDetectCnblogs === 'function') { - const result = await globalThis.__coseDetectCnblogs() - if (result && result.loggedIn) { - let avatar = result.avatar || '' - if (avatar && avatar.includes('cnblogs.com')) { - avatar = await convertAvatarToBase64(avatar, 'https://www.cnblogs.com/') - } - console.log('[COSE] Cnblogs: Logged in:', result.username) - return { loggedIn: true, username: result.username || '', avatar } - } + if (typeof globalThis.__coseDetectCnblogs === 'function') { + const result = await globalThis.__coseDetectCnblogs() + if (result && result.loggedIn) { + let avatar = result.avatar || '' + if (avatar && avatar.includes('cnblogs.com')) { + avatar = await convertAvatarToBase64(avatar, 'https://www.cnblogs.com/') } - - console.log('[COSE] Cnblogs: Not logged in') - return { loggedIn: false } - } catch (e) { - console.error('[COSE] Cnblogs Detection Error:', e) - return { loggedIn: false, error: e.message } + console.log('[COSE] Cnblogs: Logged in:', result.username) + return { loggedIn: true, username: result.username || '', avatar } + } } + + console.log('[COSE] Cnblogs: Not logged in') + return { loggedIn: false } + } catch (e) { + console.error('[COSE] Cnblogs Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/csdn.js b/packages/detection/src/platforms/csdn.js index 3a091b6..7ec3f62 100644 --- a/packages/detection/src/platforms/csdn.js +++ b/packages/detection/src/platforms/csdn.js @@ -8,53 +8,62 @@ import { convertAvatarToBase64 } from '../utils.js' * 3. If logged in, fetch public blog page to get avatar */ export async function detectCSDNUser() { - try { - console.log('[COSE] CSDN Detection: Starting cookie check') - const userNameCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserName' }) + try { + console.log('[COSE] CSDN Detection: Starting cookie check') + const userNameCookie = await chrome.cookies.get({ + url: 'https://www.csdn.net', + name: 'UserName', + }) - if (userNameCookie && userNameCookie.value) { - const userId = userNameCookie.value - console.log(`[COSE] CSDN UserName cookie found: ${userId}`) + if (userNameCookie && userNameCookie.value) { + const userId = userNameCookie.value + console.log(`[COSE] CSDN UserName cookie found: ${userId}`) - // UserNick cookie contains the display name (e.g. 'xxxxxx') - const userNickCookie = await chrome.cookies.get({ url: 'https://www.csdn.net', name: 'UserNick' }) - const username = (userNickCookie && userNickCookie.value) ? decodeURIComponent(userNickCookie.value) : userId - console.log(`[COSE] CSDN display name: ${username}`) + // UserNick cookie contains the display name (e.g. 'xxxxxx') + const userNickCookie = await chrome.cookies.get({ + url: 'https://www.csdn.net', + name: 'UserNick', + }) + const username = + userNickCookie && userNickCookie.value ? decodeURIComponent(userNickCookie.value) : userId + console.log(`[COSE] CSDN display name: ${username}`) - let avatar = '' - try { - // Fetch public blog page for avatar - const blogUrl = `https://blog.csdn.net/${userId}` - const blogResp = await fetch(blogUrl, { method: 'GET' }) - const blogHtml = await blogResp.text() + let avatar = '' + try { + // Fetch public blog page for avatar + const blogUrl = `https://blog.csdn.net/${userId}` + const blogResp = await fetch(blogUrl, { method: 'GET' }) + const blogHtml = await blogResp.text() - // Extract avatar - const avatarMatch = blogHtml.match(/]*src=["'](https:\/\/(?:profile|i-avatar)\.csdnimg\.cn\/[^"']+)["']/i) || - blogHtml.match(/]*class=["']avatar[^"']*["'][^>]*src=["']([^"']+)["']/i) + // Extract avatar + const avatarMatch = + blogHtml.match( + /]*src=["'](https:\/\/(?:profile|i-avatar)\.csdnimg\.cn\/[^"']+)["']/i + ) || blogHtml.match(/]*class=["']avatar[^"']*["'][^>]*src=["']([^"']+)["']/i) - if (avatarMatch) { - avatar = avatarMatch[1] - } - } catch (e) { - console.warn('[COSE] CSDN Avatar fetch failed:', e) - } - - // Convert csdnimg.cn avatar to base64 data URL to bypass CORS/ORB - if (avatar && avatar.includes('csdnimg.cn')) { - avatar = await convertAvatarToBase64(avatar, 'https://blog.csdn.net/') - } - - return { - loggedIn: true, - username: username, - avatar: avatar - } + if (avatarMatch) { + avatar = avatarMatch[1] } + } catch (e) { + console.warn('[COSE] CSDN Avatar fetch failed:', e) + } - console.log('[COSE] CSDN: No login detected') - return { loggedIn: false } - } catch (e) { - console.error('[COSE] CSDN Detection Error:', e) - return { loggedIn: false, error: e.message } + // Convert csdnimg.cn avatar to base64 data URL to bypass CORS/ORB + if (avatar && avatar.includes('csdnimg.cn')) { + avatar = await convertAvatarToBase64(avatar, 'https://blog.csdn.net/') + } + + return { + loggedIn: true, + username: username, + avatar: avatar, + } } + + console.log('[COSE] CSDN: No login detected') + return { loggedIn: false } + } catch (e) { + console.error('[COSE] CSDN Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/cto51.js b/packages/detection/src/platforms/cto51.js index f15732f..f5be458 100644 --- a/packages/detection/src/platforms/cto51.js +++ b/packages/detection/src/platforms/cto51.js @@ -6,30 +6,30 @@ import { convertAvatarToBase64 } from '../utils.js' * parse HTML with DOMParser to extract avatar, uid, nickname. */ export async function detectCTO51User() { - try { - console.log('[COSE] 51CTO Detection: Starting (offscreen)') + try { + console.log('[COSE] 51CTO Detection: Starting (offscreen)') - if (typeof globalThis.__coseDetectCto51 === 'function') { - const result = await globalThis.__coseDetectCto51() - if (result && result.loggedIn) { - let avatar = result.avatar || '' - if (avatar && avatar.startsWith('http') && avatar.includes('51cto.com')) { - avatar = await convertAvatarToBase64(avatar, 'https://home.51cto.com/') - } - console.log('[COSE] 51CTO: Logged in:', result.username) - return { loggedIn: true, username: result.username || '', avatar } - } - // Pass through debug info if present - if (result && result._debug) { - console.log('[COSE] 51CTO: Not logged in, debug:', JSON.stringify(result._debug)) - return { loggedIn: false, _debug: result._debug } - } + if (typeof globalThis.__coseDetectCto51 === 'function') { + const result = await globalThis.__coseDetectCto51() + if (result && result.loggedIn) { + let avatar = result.avatar || '' + if (avatar && avatar.startsWith('http') && avatar.includes('51cto.com')) { + avatar = await convertAvatarToBase64(avatar, 'https://home.51cto.com/') } - - console.log('[COSE] 51CTO: Not logged in') - return { loggedIn: false } - } catch (e) { - console.error('[COSE] 51CTO Detection Error:', e) - return { loggedIn: false, error: e.message } + console.log('[COSE] 51CTO: Logged in:', result.username) + return { loggedIn: true, username: result.username || '', avatar } + } + // Pass through debug info if present + if (result && result._debug) { + console.log('[COSE] 51CTO: Not logged in, debug:', JSON.stringify(result._debug)) + return { loggedIn: false, _debug: result._debug } + } } + + console.log('[COSE] 51CTO: Not logged in') + return { loggedIn: false } + } catch (e) { + console.error('[COSE] 51CTO Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/douban.js b/packages/detection/src/platforms/douban.js index 617d246..389eae1 100644 --- a/packages/detection/src/platforms/douban.js +++ b/packages/detection/src/platforms/douban.js @@ -1,45 +1,45 @@ import { convertAvatarToBase64 } from '../utils.js' async function convertToBase64WithFallback(avatarUrl) { - if (!avatarUrl) return '' + if (!avatarUrl) return '' - // Use shared utility only - try { - const converted = await convertAvatarToBase64(avatarUrl, 'https://www.douban.com/') - if (converted && converted.startsWith('data:')) { - return converted - } - } catch (e) { - console.log('[COSE] douban 通用头像转换失败:', e.message) + // Use shared utility only + try { + const converted = await convertAvatarToBase64(avatarUrl, 'https://www.douban.com/') + if (converted && converted.startsWith('data:')) { + return converted } + } catch (e) { + console.log('[COSE] douban 通用头像转换失败:', e.message) + } - // Fallback: manual fetch with cookies - try { - const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' }) - const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ') - const imgResp = await fetch(avatarUrl, { - method: 'GET', - headers: { - 'Referer': 'https://www.douban.com/', - ...(cookieHeader ? { 'Cookie': cookieHeader } : {}) - }, - credentials: 'include' - }) - if (!imgResp.ok) { - return avatarUrl - } - const blob = await imgResp.blob() - const buffer = await blob.arrayBuffer() - const bytes = new Uint8Array(buffer) - let binary = '' - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]) - } - return `data:${blob.type || 'image/jpeg'};base64,${btoa(binary)}` - } catch (e) { - console.log('[COSE] douban 手动头像转换失败:', e.message) - return avatarUrl + // Fallback: manual fetch with cookies + try { + const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' }) + const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ') + const imgResp = await fetch(avatarUrl, { + method: 'GET', + headers: { + Referer: 'https://www.douban.com/', + ...(cookieHeader ? { Cookie: cookieHeader } : {}), + }, + credentials: 'include', + }) + if (!imgResp.ok) { + return avatarUrl } + const blob = await imgResp.blob() + const buffer = await blob.arrayBuffer() + const bytes = new Uint8Array(buffer) + let binary = '' + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]) + } + return `data:${blob.type || 'image/jpeg'};base64,${btoa(binary)}` + } catch (e) { + console.log('[COSE] douban 手动头像转换失败:', e.message) + return avatarUrl + } } /** @@ -51,152 +51,156 @@ async function convertToBase64WithFallback(avatarUrl) { * 4. If avatar missing but uid exists, fetch profile page */ export async function detectDoubanUser() { + try { + // 1. Check dbcl2 cookie as login indicator + const dbcl2Cookie = await chrome.cookies.get({ + url: 'https://www.douban.com', + name: 'dbcl2', + }) + + if (!dbcl2Cookie || !dbcl2Cookie.value) { + console.log('[COSE] douban 未找到登录 cookie,未登录') + return { loggedIn: false } + } + + // Logged in — now try to get user details + let username = '' + let avatar = '' + let uid = '' + let loginConfirmed = false + + // 2. Parse /mine/ HTML to get user info try { - // 1. Check dbcl2 cookie as login indicator - const dbcl2Cookie = await chrome.cookies.get({ - url: 'https://www.douban.com', - name: 'dbcl2' + const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' }) + const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ') + + const response = await fetch('https://www.douban.com/mine/', { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'text/html,application/xhtml+xml', + ...(cookieHeader ? { Cookie: cookieHeader } : {}), + }, + }) + + if (response.ok) { + const finalUrl = response.url || '' + const html = await response.text() + const redirectedToLogin = + /\/accounts\/login/i.test(finalUrl) || + /name=["']form_email["']/i.test(html) || + /登录豆瓣|扫码登录/i.test(html) + const hasUserSignals = + /的账号([^<\n]+)的账号 `${c.name}=${c.value}`).join('; ') + const profileResp = await fetch(`https://www.douban.com/people/${uid}/`, { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'text/html,application/xhtml+xml', + ...(cookieHeader ? { Cookie: cookieHeader } : {}), + }, }) - if (!dbcl2Cookie || !dbcl2Cookie.value) { - console.log('[COSE] douban 未找到登录 cookie,未登录') - return { loggedIn: false } + if (profileResp.ok) { + const profileHtml = await profileResp.text() + const profileAvatar = + profileHtml.match(/https?:\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) || + profileHtml.match(/\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) + if (profileAvatar?.[0]) { + avatar = profileAvatar[0] + console.log('[COSE] douban 从个人页补充头像成功') + } } - - // Logged in — now try to get user details - let username = '' - let avatar = '' - let uid = '' - let loginConfirmed = false - - // 2. Parse /mine/ HTML to get user info - try { - const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' }) - const cookieHeader = doubanCookies.map(c => `${c.name}=${c.value}`).join('; ') - - const response = await fetch('https://www.douban.com/mine/', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'text/html,application/xhtml+xml', - ...(cookieHeader ? { 'Cookie': cookieHeader } : {}) - } - }) - - if (response.ok) { - const finalUrl = response.url || '' - const html = await response.text() - const redirectedToLogin = /\/accounts\/login/i.test(finalUrl) - || /name=["']form_email["']/i.test(html) - || /登录豆瓣|扫码登录/i.test(html) - const hasUserSignals = /的账号([^<\n]+)的账号 `${c.name}=${c.value}`).join('; ') - const profileResp = await fetch(`https://www.douban.com/people/${uid}/`, { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'text/html,application/xhtml+xml', - ...(cookieHeader ? { 'Cookie': cookieHeader } : {}) - } - }) - - if (profileResp.ok) { - const profileHtml = await profileResp.text() - const profileAvatar = profileHtml.match(/https?:\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) - || profileHtml.match(/\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) - if (profileAvatar?.[0]) { - avatar = profileAvatar[0] - console.log('[COSE] douban 从个人页补充头像成功') - } - } - } catch (e) { - console.log('[COSE] douban 从个人页补充头像失败:', e.message) - } - } - - if (avatar && avatar.startsWith('//')) { - avatar = `https:${avatar}` - } - - // Convert douban avatar to base64 if needed - if (avatar && avatar.startsWith('http')) { - try { - avatar = await convertToBase64WithFallback(avatar) - } catch (e) { - console.log('[COSE] douban 头像转换失败:', e.message) - } - } - - // Cookie exists means logged in; return best-effort user details - return { loggedIn: true, username: username || '', avatar: avatar || '' } - } catch (e) { - console.log('[COSE] douban 检测失败:', e.message) - return { loggedIn: false } + } catch (e) { + console.log('[COSE] douban 从个人页补充头像失败:', e.message) + } } + + if (avatar && avatar.startsWith('//')) { + avatar = `https:${avatar}` + } + + // Convert douban avatar to base64 if needed + if (avatar && avatar.startsWith('http')) { + try { + avatar = await convertToBase64WithFallback(avatar) + } catch (e) { + console.log('[COSE] douban 头像转换失败:', e.message) + } + } + + // Cookie exists means logged in; return best-effort user details + return { loggedIn: true, username: username || '', avatar: avatar || '' } + } catch (e) { + console.log('[COSE] douban 检测失败:', e.message) + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/elecfans.js b/packages/detection/src/platforms/elecfans.js index 6a1a616..5d63975 100644 --- a/packages/detection/src/platforms/elecfans.js +++ b/packages/detection/src/platforms/elecfans.js @@ -7,45 +7,47 @@ import { convertAvatarToBase64 } from '../utils.js' * Uses chrome.cookies.getAll to attach cookies manually (SameSite workaround) */ export async function detectElecfansUser() { - try { - // Collect cookies for bbs.elecfans.com - const cookies = await chrome.cookies.getAll({ domain: '.elecfans.com' }) - const bbsCookies = await chrome.cookies.getAll({ url: 'https://bbs.elecfans.com' }) - const allCookies = [...cookies, ...bbsCookies] - const seen = new Set() - const uniqueCookies = allCookies.filter(c => { - const key = `${c.name}=${c.value}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') + try { + // Collect cookies for bbs.elecfans.com + const cookies = await chrome.cookies.getAll({ domain: '.elecfans.com' }) + const bbsCookies = await chrome.cookies.getAll({ url: 'https://bbs.elecfans.com' }) + const allCookies = [...cookies, ...bbsCookies] + const seen = new Set() + const uniqueCookies = allCookies.filter(c => { + const key = `${c.name}=${c.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') - if (!cookieStr) return { loggedIn: false } + if (!cookieStr) return { loggedIn: false } - const response = await fetch('https://bbs.elecfans.com/api/mobile/index.php?module=profile', { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Cookie': cookieStr, - }, - }) + const response = await fetch('https://bbs.elecfans.com/api/mobile/index.php?module=profile', { + method: 'GET', + headers: { + Accept: 'application/json', + Cookie: cookieStr, + }, + }) - if (!response.ok) return { loggedIn: false } + if (!response.ok) return { loggedIn: false } - const data = await response.json() - if (!data?.Variables?.member_uid) return { loggedIn: false } + const data = await response.json() + if (!data?.Variables?.member_uid) return { loggedIn: false } - const username = data.Variables.space?.username - || data.Variables.space?.realname - || data.Variables.member_username || '' - let avatar = data.Variables.member_avatar || '' + const username = + data.Variables.space?.username || + data.Variables.space?.realname || + data.Variables.member_username || + '' + let avatar = data.Variables.member_avatar || '' - if (!username) return { loggedIn: false } + if (!username) return { loggedIn: false } - if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://bbs.elecfans.com/') - return { loggedIn: true, username, avatar } - } catch (e) { - return { loggedIn: false } - } + if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://bbs.elecfans.com/') + return { loggedIn: true, username, avatar } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/huaweicloud.js b/packages/detection/src/platforms/huaweicloud.js index 000a951..026d5b7 100644 --- a/packages/detection/src/platforms/huaweicloud.js +++ b/packages/detection/src/platforms/huaweicloud.js @@ -1,45 +1,46 @@ import { convertAvatarToBase64, detectByApi } from '../utils.js' -const HUAWEICLOUD_API = 'https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info' +const HUAWEICLOUD_API = + 'https://devdata.huaweicloud.com/rest/developer/fwdu/rest/developer/user/hdcommunityservice/v1/member/get-personal-info' /** * Huawei Cloud platform detection logic * 直接通过 API + 手动附加 cookie 检测,无需打开华为云页面 */ export async function detectHuaweiCloudUser() { - try { - const result = await detectByApi('huaweicloud', { - api: HUAWEICLOUD_API, - method: 'GET', - checkLogin: (data) => data && data.memName, - getUserInfo: (data) => ({ - username: data.memAlias || data.memName || '', - avatar: data.memPhoto || '', - }), - }) + try { + const result = await detectByApi('huaweicloud', { + api: HUAWEICLOUD_API, + method: 'GET', + checkLogin: data => data && data.memName, + getUserInfo: data => ({ + username: data.memAlias || data.memName || '', + avatar: data.memPhoto || '', + }), + }) - if (result.loggedIn) { - let avatar = result.avatar || '' - if (avatar && avatar.startsWith('http')) { - avatar = await convertAvatarToBase64(avatar, 'https://bbs.huaweicloud.com/') - } - // 更新缓存(仅用于头像 base64 缓存,不用于登录判断) - await chrome.storage.local.set({ - huaweicloud_user: { - loggedIn: true, - username: result.username, - avatar, - cachedAt: Date.now(), - } - }) - return { loggedIn: true, username: result.username, avatar } - } - - // 未登录,清除可能过期的缓存 - await chrome.storage.local.remove('huaweicloud_user') - return { loggedIn: false } - } catch (e) { - console.error('[COSE] HuaweiCloud Detection Error:', e) - return { loggedIn: false, error: e.message } + if (result.loggedIn) { + let avatar = result.avatar || '' + if (avatar && avatar.startsWith('http')) { + avatar = await convertAvatarToBase64(avatar, 'https://bbs.huaweicloud.com/') + } + // 更新缓存(仅用于头像 base64 缓存,不用于登录判断) + await chrome.storage.local.set({ + huaweicloud_user: { + loggedIn: true, + username: result.username, + avatar, + cachedAt: Date.now(), + }, + }) + return { loggedIn: true, username: result.username, avatar } } + + // 未登录,清除可能过期的缓存 + await chrome.storage.local.remove('huaweicloud_user') + return { loggedIn: false } + } catch (e) { + console.error('[COSE] HuaweiCloud Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/huaweidev.js b/packages/detection/src/platforms/huaweidev.js index cb36a25..8959592 100644 --- a/packages/detection/src/platforms/huaweidev.js +++ b/packages/detection/src/platforms/huaweidev.js @@ -9,206 +9,231 @@ import { convertAvatarToBase64 } from '../utils.js' * 4. Fallback: check developer_userdata cookie existence for basic login status */ export async function detectHuaweiDevUser() { - try { - // 1. 先检查缓存 - const stored = await chrome.storage.local.get('huaweidev_user') - const cachedUser = stored.huaweidev_user + try { + // 1. 先检查缓存 + const stored = await chrome.storage.local.get('huaweidev_user') + const cachedUser = stored.huaweidev_user - if (cachedUser && cachedUser.loggedIn) { - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days - if (cacheAge < maxAge && cachedUser.username) { - // 验证 cookie 是否仍然存在,防止用户已登出但缓存未过期的误判 - const userCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'developer_userdata' }) - if (userCookie && userCookie.value) { - console.log('[COSE] HuaweiDev: using cached user info:', cachedUser.username) - let avatar = cachedUser.avatar || '' - // 如果缓存中的头像还是原始 URL(旧缓存),转换为 base64 并更新缓存 - if (avatar && avatar.startsWith('http')) { - avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') - await chrome.storage.local.set({ huaweidev_user: { ...cachedUser, avatar } }) - } - return { loggedIn: true, username: cachedUser.username, avatar } - } - // cookie 已失效,清除缓存 - console.log('[COSE] HuaweiDev: cache exists but cookie gone, clearing cache') - await chrome.storage.local.remove('huaweidev_user') - } else { - await chrome.storage.local.remove('huaweidev_user') - } - } - - // 2. 尝试在已打开的华为开发者页面中检测 - const tabs = await chrome.tabs.query({ url: 'https://developer.huawei.com/*' }) - if (tabs.length > 0) { - try { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: 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) { - return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null - } - - const udValue = decodeURIComponent(udCookie.split('=').slice(1).join('=')) - let csrfToken = '' - try { - const udJson = JSON.parse(udValue) - csrfToken = udJson.csrf || udJson.csrftoken || '' - } catch (e) { - return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null - } - if (!csrfToken) { - return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null - } - - const now = new Date() - const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '') - - let avatar = '' - try { - const resp = 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 (resp.ok) { - const data = await resp.json() - if (data && data.returnCode === '0' && data.resJson) { - const userInfo = JSON.parse(data.resJson) - avatar = userInfo.headPictureURL || '' - } - } - } catch (e) { /* avatar fetch failed, continue with DOM username */ } - - // 优先使用 DOM 中的社区昵称 - return { - loggedIn: true, - username: domUsername || '', - avatar, - } - } catch (e) { return null } - }, - }) - - const result = results?.[0]?.result - if (result && result.loggedIn) { - let avatar = result.avatar || '' - if (avatar && avatar.startsWith('http')) { - avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') - } - const userInfo = { ...result, avatar, cachedAt: Date.now() } - await chrome.storage.local.set({ huaweidev_user: userInfo }) - return { loggedIn: true, username: userInfo.username, avatar } - } - } catch (e) { - console.log('[COSE] HuaweiDev: executeScript failed:', e.message) - } - } - - // 3. 没有打开的华为开发者页面,检查 developer_userdata cookie 并通过 offscreen fetch 获取用户信息 - const userCookie = await chrome.cookies.get({ url: 'https://developer.huawei.com', name: 'developer_userdata' }) + if (cachedUser && cachedUser.loggedIn) { + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days + if (cacheAge < maxAge && cachedUser.username) { + // 验证 cookie 是否仍然存在,防止用户已登出但缓存未过期的误判 + const userCookie = await chrome.cookies.get({ + url: 'https://developer.huawei.com', + name: 'developer_userdata', + }) if (userCookie && userCookie.value) { - console.log('[COSE] HuaweiDev: developer_userdata cookie found, trying offscreen fetch for user info') - let username = '' - let avatar = '' - let apiSuccess = false - try { - const udValue = decodeURIComponent(userCookie.value) - const udJson = JSON.parse(udValue) - const csrfToken = udJson.csrftoken || udJson.csrf || '' - if (csrfToken) { - const now = new Date() - const hdDate = now.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '') - - // 确保 offscreen document 存在 - try { - await chrome.offscreen.createDocument({ - url: 'offscreen.html', - reasons: ['DOM_SCRAPING'], - justification: 'Fetch Huawei Developer user info with cookies', - }) - } catch (e) { - // 已存在则忽略 - if (!e.message.includes('Only a single offscreen')) { - throw e - } - } - - const response = await chrome.runtime.sendMessage({ - type: 'OFFSCREEN_FETCH', - payload: { - url: 'https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'x-hd-csrf': csrfToken, - 'x-hd-date': hdDate, - }, - body: { - svc: 'GOpen.User.getInfo', - reqType: 0, - reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }), - }, - }, - }) - - if (response?.success && response.data) { - const data = response.data - if (data.returnCode === '0' && data.resJson) { - const userInfo = JSON.parse(data.resJson) - username = userInfo.displayName || '' - avatar = userInfo.headPictureURL || '' - if (avatar && avatar.startsWith('http')) { - avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') - } - apiSuccess = true - } - } - - // 关闭 offscreen document - try { await chrome.offscreen.closeDocument() } catch (e) { /* ignore */ } - } - } catch (e) { - console.log('[COSE] HuaweiDev: offscreen fetch failed:', e.message) - } - - // API 成功才认为已登录,否则视为登录过期 - if (apiSuccess) { - if (username) { - const userInfo = { loggedIn: true, username, avatar, cachedAt: Date.now() } - await chrome.storage.local.set({ huaweidev_user: userInfo }) - } - return { loggedIn: true, username, avatar } - } else { - // API 失败,清除可能残留的缓存 - await chrome.storage.local.remove('huaweidev_user') - console.log('[COSE] HuaweiDev: API verification failed, treating as logged out') - return { loggedIn: false } - } + console.log('[COSE] HuaweiDev: using cached user info:', cachedUser.username) + let avatar = cachedUser.avatar || '' + // 如果缓存中的头像还是原始 URL(旧缓存),转换为 base64 并更新缓存 + if (avatar && avatar.startsWith('http')) { + avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') + await chrome.storage.local.set({ huaweidev_user: { ...cachedUser, avatar } }) + } + return { loggedIn: true, username: cachedUser.username, avatar } } - - return { loggedIn: false } - } catch (e) { - console.error('[COSE] HuaweiDev Detection Error:', e) - return { loggedIn: false, error: e.message } + // cookie 已失效,清除缓存 + console.log('[COSE] HuaweiDev: cache exists but cookie gone, clearing cache') + await chrome.storage.local.remove('huaweidev_user') + } else { + await chrome.storage.local.remove('huaweidev_user') + } } + + // 2. 尝试在已打开的华为开发者页面中检测 + const tabs = await chrome.tabs.query({ url: 'https://developer.huawei.com/*' }) + if (tabs.length > 0) { + try { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: 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) { + return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null + } + + const udValue = decodeURIComponent(udCookie.split('=').slice(1).join('=')) + let csrfToken = '' + try { + const udJson = JSON.parse(udValue) + csrfToken = udJson.csrf || udJson.csrftoken || '' + } catch (e) { + return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null + } + if (!csrfToken) { + return domUsername ? { loggedIn: true, username: domUsername, avatar: '' } : null + } + + const now = new Date() + const hdDate = now + .toISOString() + .replace(/[-:]/g, '') + .replace(/\.\d{3}/, '') + + let avatar = '' + try { + const resp = 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 (resp.ok) { + const data = await resp.json() + if (data && data.returnCode === '0' && data.resJson) { + const userInfo = JSON.parse(data.resJson) + avatar = userInfo.headPictureURL || '' + } + } + } catch (e) { + /* avatar fetch failed, continue with DOM username */ + } + + // 优先使用 DOM 中的社区昵称 + return { + loggedIn: true, + username: domUsername || '', + avatar, + } + } catch (e) { + return null + } + }, + }) + + const result = results?.[0]?.result + if (result && result.loggedIn) { + let avatar = result.avatar || '' + if (avatar && avatar.startsWith('http')) { + avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') + } + const userInfo = { ...result, avatar, cachedAt: Date.now() } + await chrome.storage.local.set({ huaweidev_user: userInfo }) + return { loggedIn: true, username: userInfo.username, avatar } + } + } catch (e) { + console.log('[COSE] HuaweiDev: executeScript failed:', e.message) + } + } + + // 3. 没有打开的华为开发者页面,检查 developer_userdata cookie 并通过 offscreen fetch 获取用户信息 + const userCookie = await chrome.cookies.get({ + url: 'https://developer.huawei.com', + name: 'developer_userdata', + }) + if (userCookie && userCookie.value) { + console.log( + '[COSE] HuaweiDev: developer_userdata cookie found, trying offscreen fetch for user info' + ) + let username = '' + let avatar = '' + let apiSuccess = false + try { + const udValue = decodeURIComponent(userCookie.value) + const udJson = JSON.parse(udValue) + const csrfToken = udJson.csrftoken || udJson.csrf || '' + if (csrfToken) { + const now = new Date() + const hdDate = now + .toISOString() + .replace(/[-:]/g, '') + .replace(/\.\d{3}/, '') + + // 确保 offscreen document 存在 + try { + await chrome.offscreen.createDocument({ + url: 'offscreen.html', + reasons: ['DOM_SCRAPING'], + justification: 'Fetch Huawei Developer user info with cookies', + }) + } catch (e) { + // 已存在则忽略 + if (!e.message.includes('Only a single offscreen')) { + throw e + } + } + + const response = await chrome.runtime.sendMessage({ + type: 'OFFSCREEN_FETCH', + payload: { + url: 'https://svc-drcn.developer.huawei.com/codeserver/Common/v1/delegate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'x-hd-csrf': csrfToken, + 'x-hd-date': hdDate, + }, + body: { + svc: 'GOpen.User.getInfo', + reqType: 0, + reqJson: JSON.stringify({ queryRangeFlag: '00000000000001' }), + }, + }, + }) + + if (response?.success && response.data) { + const data = response.data + if (data.returnCode === '0' && data.resJson) { + const userInfo = JSON.parse(data.resJson) + username = userInfo.displayName || '' + avatar = userInfo.headPictureURL || '' + if (avatar && avatar.startsWith('http')) { + avatar = await convertAvatarToBase64(avatar, 'https://developer.huawei.com/') + } + apiSuccess = true + } + } + + // 关闭 offscreen document + try { + await chrome.offscreen.closeDocument() + } catch (e) { + /* ignore */ + } + } + } catch (e) { + console.log('[COSE] HuaweiDev: offscreen fetch failed:', e.message) + } + + // API 成功才认为已登录,否则视为登录过期 + if (apiSuccess) { + if (username) { + const userInfo = { loggedIn: true, username, avatar, cachedAt: Date.now() } + await chrome.storage.local.set({ huaweidev_user: userInfo }) + } + return { loggedIn: true, username, avatar } + } else { + // API 失败,清除可能残留的缓存 + await chrome.storage.local.remove('huaweidev_user') + console.log('[COSE] HuaweiDev: API verification failed, treating as logged out') + return { loggedIn: false } + } + } + + return { loggedIn: false } + } catch (e) { + console.error('[COSE] HuaweiDev Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/infoq.js b/packages/detection/src/platforms/infoq.js index 848e4fe..2b7eeeb 100644 --- a/packages/detection/src/platforms/infoq.js +++ b/packages/detection/src/platforms/infoq.js @@ -6,36 +6,36 @@ import { convertAvatarToBase64 } from '../utils.js' * The old /public/v1/my/menu endpoint returns 404. */ export async function detectInfoQUser() { - try { - console.log('[COSE] InfoQ Detection: Starting') - const response = await fetch('https://www.infoq.cn/public/v1/user/get_user', { - method: 'POST', - credentials: 'include', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - body: JSON.stringify({}), - }) + try { + console.log('[COSE] InfoQ Detection: Starting') + const response = await fetch('https://www.infoq.cn/public/v1/user/get_user', { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({}), + }) - if (!response.ok) return { loggedIn: false } + if (!response.ok) return { loggedIn: false } - const json = await response.json() - if (json?.code !== 0 || !json?.data?.uid) { - console.log('[COSE] InfoQ: Not logged in', json?.code) - return { loggedIn: false } - } - - const username = json.data.nickname || '' - let avatar = json.data.avatar || '' - - if (avatar && avatar.includes('geekbang.org')) { - avatar = await convertAvatarToBase64(avatar, 'https://www.infoq.cn/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.error('[COSE] InfoQ Detection Error:', e) - return { loggedIn: false, error: e.message } + const json = await response.json() + if (json?.code !== 0 || !json?.data?.uid) { + console.log('[COSE] InfoQ: Not logged in', json?.code) + return { loggedIn: false } } + + const username = json.data.nickname || '' + let avatar = json.data.avatar || '' + + if (avatar && avatar.includes('geekbang.org')) { + avatar = await convertAvatarToBase64(avatar, 'https://www.infoq.cn/') + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.error('[COSE] InfoQ Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/jianshu.js b/packages/detection/src/platforms/jianshu.js index f2bf579..9c87ba9 100644 --- a/packages/detection/src/platforms/jianshu.js +++ b/packages/detection/src/platforms/jianshu.js @@ -5,29 +5,29 @@ import { convertAvatarToBase64 } from '../utils.js' * Strategy: Fetch settings JSON API to get nickname and avatar */ export async function detectJianshuUser() { - try { - console.log('[COSE] Jianshu Detection: Starting') - const response = await fetch('https://www.jianshu.com/settings/basic.json', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json' }, - }) + try { + console.log('[COSE] Jianshu Detection: Starting') + const response = await fetch('https://www.jianshu.com/settings/basic.json', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' }, + }) - if (!response.ok) return { loggedIn: false } + if (!response.ok) return { loggedIn: false } - const json = await response.json() - if (!json?.data) return { loggedIn: false } + const json = await response.json() + if (!json?.data) return { loggedIn: false } - const username = json.data.nickname || '' - let avatar = json.data.avatar || '' + const username = json.data.nickname || '' + let avatar = json.data.avatar || '' - if (avatar && avatar.includes('jianshu.io')) { - avatar = await convertAvatarToBase64(avatar, 'https://www.jianshu.com/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.error('[COSE] Jianshu Detection Error:', e) - return { loggedIn: false, error: e.message } + if (avatar && avatar.includes('jianshu.io')) { + avatar = await convertAvatarToBase64(avatar, 'https://www.jianshu.com/') } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.error('[COSE] Jianshu Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/medium.js b/packages/detection/src/platforms/medium.js index b0f7129..9b95d22 100644 --- a/packages/detection/src/platforms/medium.js +++ b/packages/detection/src/platforms/medium.js @@ -5,39 +5,50 @@ * 2. Fetch stats page and extract username/avatar via regex */ export async function detectMediumUser() { - try { - const sidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'sid' }) - const uidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'uid' }) + try { + const sidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'sid' }) + const uidCookie = await chrome.cookies.get({ url: 'https://medium.com', name: 'uid' }) - if (!sidCookie && !uidCookie) return { loggedIn: false } + if (!sidCookie && !uidCookie) return { loggedIn: false } - const response = await fetch('https://medium.com/me/stats', { - method: 'GET', - credentials: 'include', - }) - const html = await response.text() - const finalUrl = response.url + const response = await fetch('https://medium.com/me/stats', { + method: 'GET', + credentials: 'include', + }) + const html = await response.text() + const finalUrl = response.url - if (finalUrl.includes('/m/signin') || finalUrl.includes('?signIn')) return { loggedIn: false } + if (finalUrl.includes('/m/signin') || finalUrl.includes('?signIn')) return { loggedIn: false } - const profileMatch = html.match(/"username"\s*:\s*"([^"]+)"/) || - html.match(/href="https:\/\/medium\.com\/@([^"?\/]+)"/) || - html.match(/medium\.com\/@([a-zA-Z0-9_]+)/) + const profileMatch = + html.match(/"username"\s*:\s*"([^"]+)"/) || + html.match(/href="https:\/\/medium\.com\/@([^"?\/]+)"/) || + html.match(/medium\.com\/@([a-zA-Z0-9_]+)/) - if (profileMatch && profileMatch[1] && profileMatch[1] !== 'gmail' && profileMatch[1] !== 'medium') { - const username = profileMatch[1] + if ( + profileMatch && + profileMatch[1] && + profileMatch[1] !== 'gmail' && + profileMatch[1] !== 'medium' + ) { + const username = profileMatch[1] - // Extract avatar via imageId from JSON data near the username - let avatar = '' - const imageIdMatch = html.match(new RegExp(`"imageId"\\s*:\\s*"([^"]+)"[^}]*"username"\\s*:\\s*"${username}"`)) || - html.match(new RegExp(`"username"\\s*:\\s*"${username}"[^}]*"imageId"\\s*:\\s*"([^"]+)"`)) - if (imageIdMatch) { - avatar = `https://miro.medium.com/v2/resize:fill:64:64/${imageIdMatch[1]}` - } + // Extract avatar via imageId from JSON data near the username + let avatar = '' + const imageIdMatch = + html.match( + new RegExp(`"imageId"\\s*:\\s*"([^"]+)"[^}]*"username"\\s*:\\s*"${username}"`) + ) || + html.match(new RegExp(`"username"\\s*:\\s*"${username}"[^}]*"imageId"\\s*:\\s*"([^"]+)"`)) + if (imageIdMatch) { + avatar = `https://miro.medium.com/v2/resize:fill:64:64/${imageIdMatch[1]}` + } - return { loggedIn: true, username, avatar } - } else { - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { return { loggedIn: false } } + return { loggedIn: true, username, avatar } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/modelscope.js b/packages/detection/src/platforms/modelscope.js index f987b5f..fd9aad3 100644 --- a/packages/detection/src/platforms/modelscope.js +++ b/packages/detection/src/platforms/modelscope.js @@ -9,103 +9,114 @@ import { convertAvatarToBase64 } from '../utils.js' * 4. Convert avatar to base64 to bypass CORS/ORB */ export async function detectModelScopeUser() { + try { + let username = '' + let avatar = '' + + // Try the correct API endpoint (found via Chrome DevTools network inspection) try { - let username = '' - let avatar = '' - - // Try the correct API endpoint (found via Chrome DevTools network inspection) - try { - const response = await fetch('https://modelscope.cn/api/v1/users/login/info', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - }, - }) - if (response.ok) { - const data = await response.json() - if (data?.Success !== false && data?.Code !== 10019901001) { - const user = data?.Data?.User || data?.Data || {} - username = user.Nickname || user.NickName || user.Name - || user.nickname || user.name || user.Login || user.login || '' - avatar = user.Avatar || user.avatar || '' - } - } - } catch (e) { } - - if (username) { - if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/') - return { loggedIn: true, username, avatar } + const response = await fetch('https://modelscope.cn/api/v1/users/login/info', { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }) + if (response.ok) { + const data = await response.json() + if (data?.Success !== false && data?.Code !== 10019901001) { + const user = data?.Data?.User || data?.Data || {} + username = + user.Nickname || + user.NickName || + user.Name || + user.nickname || + user.name || + user.Login || + user.login || + '' + avatar = user.Avatar || user.avatar || '' } + } + } catch (e) {} - // Fallback: extract from open tab DOM - try { - const tabs = await chrome.tabs.query({ url: 'https://modelscope.cn/*' }) - if (tabs.length > 0) { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: () => { - // Look for avatar img in the page - let avatarSrc = '' - const avatarSelectors = [ - 'img[src*="avatar"]', - '.ant-avatar img', - 'img[class*="avatar" i]', - 'img[class*="Avatar" i]', - ] - for (const sel of avatarSelectors) { - const img = document.querySelector(sel) - if (img && img.src && !img.src.includes('data:image/svg')) { - avatarSrc = img.src - break - } - } - // Look for username from the page's user info - let name = '' - // Try to get from the header/nav user dropdown area - const allLinks = document.querySelectorAll('a[href*="/profile/"]') - for (const a of allLinks) { - const href = a.getAttribute('href') || '' - const match = href.match(/\/profile\/([^/?#]+)/) - if (match && match[1]) { - name = match[1] - break - } - } - // Also try my/overview link text or nearby elements - if (!name) { - const myLink = document.querySelector('a[href="/my/overview"]') - if (myLink) { - const parent = myLink.closest('[class*="dropdown"]') || myLink.parentElement - if (parent) { - const spans = parent.querySelectorAll('span') - for (const s of spans) { - const t = s.textContent.trim() - if (t && t.length > 1 && t.length < 30 - && !['登录', '注册', '退出', '设置'].includes(t)) { - name = t - break - } - } - } - } - } - return { username: name, avatar: avatarSrc } - }, - }) - if (results?.[0]?.result) { - username = results[0].result.username || '' - avatar = results[0].result.avatar || '' - } - if (username || avatar) { - if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/') - return { loggedIn: true, username, avatar } - } - } - } catch (e) { } - - return { loggedIn: false } - } catch (e) { - return { loggedIn: false } + if (username) { + if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/') + return { loggedIn: true, username, avatar } } + + // Fallback: extract from open tab DOM + try { + const tabs = await chrome.tabs.query({ url: 'https://modelscope.cn/*' }) + if (tabs.length > 0) { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + // Look for avatar img in the page + let avatarSrc = '' + const avatarSelectors = [ + 'img[src*="avatar"]', + '.ant-avatar img', + 'img[class*="avatar" i]', + 'img[class*="Avatar" i]', + ] + for (const sel of avatarSelectors) { + const img = document.querySelector(sel) + if (img && img.src && !img.src.includes('data:image/svg')) { + avatarSrc = img.src + break + } + } + // Look for username from the page's user info + let name = '' + // Try to get from the header/nav user dropdown area + const allLinks = document.querySelectorAll('a[href*="/profile/"]') + for (const a of allLinks) { + const href = a.getAttribute('href') || '' + const match = href.match(/\/profile\/([^/?#]+)/) + if (match && match[1]) { + name = match[1] + break + } + } + // Also try my/overview link text or nearby elements + if (!name) { + const myLink = document.querySelector('a[href="/my/overview"]') + if (myLink) { + const parent = myLink.closest('[class*="dropdown"]') || myLink.parentElement + if (parent) { + const spans = parent.querySelectorAll('span') + for (const s of spans) { + const t = s.textContent.trim() + if ( + t && + t.length > 1 && + t.length < 30 && + !['登录', '注册', '退出', '设置'].includes(t) + ) { + name = t + break + } + } + } + } + } + return { username: name, avatar: avatarSrc } + }, + }) + if (results?.[0]?.result) { + username = results[0].result.username || '' + avatar = results[0].result.avatar || '' + } + if (username || avatar) { + if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://modelscope.cn/') + return { loggedIn: true, username, avatar } + } + } + } catch (e) {} + + return { loggedIn: false } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/oschina.js b/packages/detection/src/platforms/oschina.js index c629968..63df3c2 100644 --- a/packages/detection/src/platforms/oschina.js +++ b/packages/detection/src/platforms/oschina.js @@ -7,63 +7,67 @@ import { convertAvatarToBase64 } from '../utils.js' * 2. Best-effort: fetch user info via API for username and avatar */ export async function detectOSChinaUser() { - try { - // Check oscid cookie as login indicator - const oscidCookie = await chrome.cookies.get({ url: 'https://www.oschina.net', name: 'oscid' }) - if (!oscidCookie || !oscidCookie.value) { - return { loggedIn: false } - } - - // Collect all cookies for API request - const cookies = await chrome.cookies.getAll({ domain: '.oschina.net' }) - const wwwCookies = await chrome.cookies.getAll({ url: 'https://www.oschina.net' }) - const apiCookies = await chrome.cookies.getAll({ url: 'https://apiv1.oschina.net' }) - const allCookies = [...cookies, ...wwwCookies, ...apiCookies] - const seen = new Set() - const uniqueCookies = allCookies.filter(c => { - const key = `${c.name}=${c.value}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') - - // Best-effort: try to get username and avatar via API - let username = '' - let avatar = '' - let userId = '' - try { - const response = await fetch('https://apiv1.oschina.net/oschinapi/user/myDetails', { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Cookie': cookieStr, - }, - }) - if (response.ok) { - const data = await response.json() - if (data?.success && data?.result?.userVo) { - username = data.result.userVo.name || '' - avatar = data.result.userVo.portraitUrl || '' - userId = String(data.result.userVo.id || '') - } - } - } catch (e) { - console.log('[COSE] OSChina: API fetch failed, using cookie-only detection') - } - - if (avatar && (avatar.includes('oschina.net') || avatar.includes('oscimg'))) { - avatar = await convertAvatarToBase64(avatar, 'https://www.oschina.net/') - } - - // Store userId for sync URL construction - if (userId) { - try { await chrome.storage.local.set({ oschina_userId: userId }) } catch (e) { /* ignore */ } - } - - return { loggedIn: true, username, avatar, userId } - } catch (e) { - console.error('[COSE] OSChina Detection Error:', e) - return { loggedIn: false, error: e.message } + try { + // Check oscid cookie as login indicator + const oscidCookie = await chrome.cookies.get({ url: 'https://www.oschina.net', name: 'oscid' }) + if (!oscidCookie || !oscidCookie.value) { + return { loggedIn: false } } + + // Collect all cookies for API request + const cookies = await chrome.cookies.getAll({ domain: '.oschina.net' }) + const wwwCookies = await chrome.cookies.getAll({ url: 'https://www.oschina.net' }) + const apiCookies = await chrome.cookies.getAll({ url: 'https://apiv1.oschina.net' }) + const allCookies = [...cookies, ...wwwCookies, ...apiCookies] + const seen = new Set() + const uniqueCookies = allCookies.filter(c => { + const key = `${c.name}=${c.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') + + // Best-effort: try to get username and avatar via API + let username = '' + let avatar = '' + let userId = '' + try { + const response = await fetch('https://apiv1.oschina.net/oschinapi/user/myDetails', { + method: 'GET', + headers: { + Accept: 'application/json', + Cookie: cookieStr, + }, + }) + if (response.ok) { + const data = await response.json() + if (data?.success && data?.result?.userVo) { + username = data.result.userVo.name || '' + avatar = data.result.userVo.portraitUrl || '' + userId = String(data.result.userVo.id || '') + } + } + } catch (e) { + console.log('[COSE] OSChina: API fetch failed, using cookie-only detection') + } + + if (avatar && (avatar.includes('oschina.net') || avatar.includes('oscimg'))) { + avatar = await convertAvatarToBase64(avatar, 'https://www.oschina.net/') + } + + // Store userId for sync URL construction + if (userId) { + try { + await chrome.storage.local.set({ oschina_userId: userId }) + } catch (e) { + /* ignore */ + } + } + + return { loggedIn: true, username, avatar, userId } + } catch (e) { + console.error('[COSE] OSChina Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/qianfan.js b/packages/detection/src/platforms/qianfan.js index 06059a0..7dc34b6 100644 --- a/packages/detection/src/platforms/qianfan.js +++ b/packages/detection/src/platforms/qianfan.js @@ -7,38 +7,41 @@ import { convertAvatarToBase64 } from '../utils.js' * 2. Call current user API with csrftoken header */ export async function detectQianfanUser() { - try { - console.log('[COSE] Qianfan Detection: Starting') + try { + console.log('[COSE] Qianfan Detection: Starting') - // Read csrftoken from cookie - const csrfCookie = await chrome.cookies.get({ url: 'https://qianfan.cloud.baidu.com', name: 'bce-user-info-ct-id' }) - const csrfToken = csrfCookie?.value ? csrfCookie.value.replace(/"/g, '') : '' + // Read csrftoken from cookie + const csrfCookie = await chrome.cookies.get({ + url: 'https://qianfan.cloud.baidu.com', + name: 'bce-user-info-ct-id', + }) + const csrfToken = csrfCookie?.value ? csrfCookie.value.replace(/"/g, '') : '' - const response = await fetch('https://qianfan.cloud.baidu.com/api/community/user/current', { - method: 'GET', - credentials: 'include', - headers: { - 'Accept': 'application/json', - ...(csrfToken ? { 'csrftoken': csrfToken } : {}), - } - }) - if (!response.ok) return { loggedIn: false } + const response = await fetch('https://qianfan.cloud.baidu.com/api/community/user/current', { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + ...(csrfToken ? { csrftoken: csrfToken } : {}), + }, + }) + if (!response.ok) return { loggedIn: false } - const data = await response.json() - if (data.success && data.result) { - const username = data.result.displayName || data.result.nickname || '' - let avatar = data.result.avatar || '' + const data = await response.json() + if (data.success && data.result) { + const username = data.result.displayName || data.result.nickname || '' + let avatar = data.result.avatar || '' - if (avatar && avatar.includes('bdimg.com')) { - avatar = await convertAvatarToBase64(avatar, 'https://qianfan.cloud.baidu.com/') - } + if (avatar && avatar.includes('bdimg.com')) { + avatar = await convertAvatarToBase64(avatar, 'https://qianfan.cloud.baidu.com/') + } - return { loggedIn: true, username, avatar } - } else { - return { loggedIn: false } - } - } catch (e) { - console.error('[COSE] Qianfan Detection Error:', e) - return { loggedIn: false, error: e.message } + return { loggedIn: true, username, avatar } + } else { + return { loggedIn: false } } + } catch (e) { + console.error('[COSE] Qianfan Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/segmentfault.js b/packages/detection/src/platforms/segmentfault.js index c43c9c1..2edb37e 100644 --- a/packages/detection/src/platforms/segmentfault.js +++ b/packages/detection/src/platforms/segmentfault.js @@ -6,42 +6,42 @@ import { convertAvatarToBase64 } from '../utils.js' * The /gateway/user/me API requires CSRF tokens (ivd param), so we use HTML scraping instead. */ export async function detectSegmentFaultUser() { - try { - console.log('[COSE] SegmentFault Detection: Starting') - const response = await fetch('https://segmentfault.com/', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'text/html' }, - }) - const html = await response.text() + try { + console.log('[COSE] SegmentFault Detection: Starting') + const response = await fetch('https://segmentfault.com/', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'text/html' }, + }) + const html = await response.text() - // Extract __NEXT_DATA__ JSON - const nextDataMatch = html.match(/]*>([\s\S]*?)<\/script>/) - if (!nextDataMatch) { - console.log('[COSE] SegmentFault: No __NEXT_DATA__ found') - return { loggedIn: false } - } - - const nextData = JSON.parse(nextDataMatch[1]) - const sessionUser = nextData?.props?.pageProps?.initialState?.global?.sessionUser - const sessionInfo = nextData?.props?.pageProps?.initialState?.global?.sessionInfo - - if (!sessionUser?.user?.id && !sessionInfo?.login) { - console.log('[COSE] SegmentFault: Not logged in') - return { loggedIn: false } - } - - const user = sessionUser?.user || {} - const username = user.name || user.slug || '' - let avatar = user.avatar_url || '' - - if (avatar && avatar.includes('segmentfault.com')) { - avatar = await convertAvatarToBase64(avatar, 'https://segmentfault.com/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.error('[COSE] SegmentFault Detection Error:', e) - return { loggedIn: false, error: e.message } + // Extract __NEXT_DATA__ JSON + const nextDataMatch = html.match(/]*>([\s\S]*?)<\/script>/) + if (!nextDataMatch) { + console.log('[COSE] SegmentFault: No __NEXT_DATA__ found') + return { loggedIn: false } } + + const nextData = JSON.parse(nextDataMatch[1]) + const sessionUser = nextData?.props?.pageProps?.initialState?.global?.sessionUser + const sessionInfo = nextData?.props?.pageProps?.initialState?.global?.sessionInfo + + if (!sessionUser?.user?.id && !sessionInfo?.login) { + console.log('[COSE] SegmentFault: Not logged in') + return { loggedIn: false } + } + + const user = sessionUser?.user || {} + const username = user.name || user.slug || '' + let avatar = user.avatar_url || '' + + if (avatar && avatar.includes('segmentfault.com')) { + avatar = await convertAvatarToBase64(avatar, 'https://segmentfault.com/') + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.error('[COSE] SegmentFault Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/sohu.js b/packages/detection/src/platforms/sohu.js index 471a382..012cfb9 100644 --- a/packages/detection/src/platforms/sohu.js +++ b/packages/detection/src/platforms/sohu.js @@ -5,27 +5,29 @@ * 2. Call account list API for nickname/avatar */ export async function detectSohuUser() { - try { - const ppinfCookie = await chrome.cookies.get({ url: 'https://mp.sohu.com', name: 'ppinf' }) - if (!ppinfCookie || !ppinfCookie.value) return { loggedIn: false } + try { + const ppinfCookie = await chrome.cookies.get({ url: 'https://mp.sohu.com', name: 'ppinf' }) + if (!ppinfCookie || !ppinfCookie.value) return { loggedIn: false } - try { - const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json' } - }) - const data = await response.json() - if (data.success && data.data?.data?.[0]?.accounts?.[0]) { - const account = data.data.data[0].accounts[0] - let avatar = account.avatar || '' - if (avatar.startsWith('//')) avatar = 'https:' + avatar - return { loggedIn: true, username: account.nickName, avatar } - } else { - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { - return { loggedIn: true, username: '', avatar: '' } - } - } catch (e) { return { loggedIn: false } } + try { + const response = await fetch('https://mp.sohu.com/mpbp/bp/account/list', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' }, + }) + const data = await response.json() + if (data.success && data.data?.data?.[0]?.accounts?.[0]) { + const account = data.data.data[0].accounts[0] + let avatar = account.avatar || '' + if (avatar.startsWith('//')) avatar = 'https:' + avatar + return { loggedIn: true, username: account.nickName, avatar } + } else { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: true, username: '', avatar: '' } + } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/sspai.js b/packages/detection/src/platforms/sspai.js index 0a5d6f0..fa5f212 100644 --- a/packages/detection/src/platforms/sspai.js +++ b/packages/detection/src/platforms/sspai.js @@ -5,22 +5,27 @@ * 2. Call user info API with Bearer token */ export async function detectSspaiUser() { - try { - const jwtCookie = await chrome.cookies.get({ url: 'https://sspai.com', name: 'sspai_jwt_token' }) - if (!jwtCookie || !jwtCookie.value) return { loggedIn: false } + try { + const jwtCookie = await chrome.cookies.get({ + url: 'https://sspai.com', + name: 'sspai_jwt_token', + }) + if (!jwtCookie || !jwtCookie.value) return { loggedIn: false } - const token = jwtCookie.value - const response = await fetch('https://sspai.com/api/v1/user/info/get', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}` } - }) - const data = await response.json() + const token = jwtCookie.value + const response = await fetch('https://sspai.com/api/v1/user/info/get', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json', Authorization: `Bearer ${token}` }, + }) + const data = await response.json() - if (data.error === 0 && data.data?.nickname) { - return { loggedIn: true, username: data.data.nickname, avatar: data.data.avatar || '' } - } else { - return { loggedIn: false } - } - } catch (e) { return { loggedIn: false } } + if (data.error === 0 && data.data?.nickname) { + return { loggedIn: true, username: data.data.nickname, avatar: data.data.avatar || '' } + } else { + return { loggedIn: false } + } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/tencentcloud.js b/packages/detection/src/platforms/tencentcloud.js index 3625cd8..8b308b0 100644 --- a/packages/detection/src/platforms/tencentcloud.js +++ b/packages/detection/src/platforms/tencentcloud.js @@ -7,37 +7,49 @@ import { convertAvatarToBase64 } from '../utils.js' * 2. Check redirect and parse HTML for nickname/avatar */ export async function detectTencentCloudUser() { - try { - const response = await fetch('https://cloud.tencent.com/developer/creator', { - method: 'GET', - credentials: 'include', - }) - const html = await response.text() - const finalUrl = response.url + try { + const response = await fetch('https://cloud.tencent.com/developer/creator', { + method: 'GET', + credentials: 'include', + }) + const html = await response.text() + const finalUrl = response.url - if (!finalUrl.includes('/creator')) return { loggedIn: false } - if (html.includes('登录/注册') || html.includes('"isLogin":false') || html.includes('"login":false')) return { loggedIn: false } + if (!finalUrl.includes('/creator')) return { loggedIn: false } + if ( + html.includes('登录/注册') || + html.includes('"isLogin":false') || + html.includes('"login":false') + ) + return { loggedIn: false } - const userInfoMatch = html.match(/"userInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || - html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) + const userInfoMatch = + html.match(/"userInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || + html.match(/"creatorInfo"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) || + html.match(/"currentUser"\s*:\s*\{[^}]*"nickname"\s*:\s*"([^"]+)"[^}]*\}/) - const creatorNicknameMatch = html.match(/class="creator-info[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+)]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+) { - const key = `${c.name}=${c.value}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') + try { + // Collect cookies for volcengine.com to attach to API request + const cookies = await chrome.cookies.getAll({ domain: '.volcengine.com' }) + const devCookies = await chrome.cookies.getAll({ url: 'https://developer.volcengine.com' }) + const allCookies = [...cookies, ...devCookies] + const seen = new Set() + const uniqueCookies = allCookies.filter(c => { + const key = `${c.name}=${c.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') - if (!cookieStr) return { loggedIn: false } + if (!cookieStr) return { loggedIn: false } - const response = await fetch('https://developer.volcengine.com/api/fe/v1/user', { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Cookie': cookieStr, - }, - }) + const response = await fetch('https://developer.volcengine.com/api/fe/v1/user', { + method: 'GET', + headers: { + Accept: 'application/json', + Cookie: cookieStr, + }, + }) - if (!response.ok) return { loggedIn: false } + if (!response.ok) return { loggedIn: false } - const data = await response.json() - if (data?.err_no !== 0 || !data?.data?.name) return { loggedIn: false } + const data = await response.json() + if (data?.err_no !== 0 || !data?.data?.name) return { loggedIn: false } - let username = data.data.name - let avatar = data.data.avatar?.url || '' + let username = data.data.name + let avatar = data.data.avatar?.url || '' - if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://developer.volcengine.com/') - return { loggedIn: true, username, avatar } - } catch (e) { - return { loggedIn: false } - } + if (avatar) avatar = await convertAvatarToBase64(avatar, 'https://developer.volcengine.com/') + return { loggedIn: true, username, avatar } + } catch (e) { + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/wangyihao.js b/packages/detection/src/platforms/wangyihao.js index 9ee123c..bd79af7 100644 --- a/packages/detection/src/platforms/wangyihao.js +++ b/packages/detection/src/platforms/wangyihao.js @@ -8,44 +8,44 @@ import { convertAvatarToBase64 } from '../utils.js' * 3. Extract username and avatar from API response */ export async function detectWangyihaoUser() { - try { - const cookies = await chrome.cookies.getAll({ domain: '.163.com' }) - const mpCookies = await chrome.cookies.getAll({ url: 'https://mp.163.com' }) - const allCookies = [...cookies, ...mpCookies] - const seen = new Set() - const uniqueCookies = allCookies.filter(c => { - const key = `${c.name}=${c.value}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') + try { + const cookies = await chrome.cookies.getAll({ domain: '.163.com' }) + const mpCookies = await chrome.cookies.getAll({ url: 'https://mp.163.com' }) + const allCookies = [...cookies, ...mpCookies] + const seen = new Set() + const uniqueCookies = allCookies.filter(c => { + const key = `${c.name}=${c.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + const cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') - if (!cookieStr) return { loggedIn: false } + if (!cookieStr) return { loggedIn: false } - const response = await fetch(`https://mp.163.com/wemedia/navinfo.do?_=${Date.now()}`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Cookie': cookieStr, - }, - }) + const response = await fetch(`https://mp.163.com/wemedia/navinfo.do?_=${Date.now()}`, { + method: 'GET', + headers: { + Accept: 'application/json', + Cookie: cookieStr, + }, + }) - if (!response.ok) return { loggedIn: false } + if (!response.ok) return { loggedIn: false } - const data = await response.json() - if (data?.code !== 1 || !data?.data?.wemediaId) return { loggedIn: false } + const data = await response.json() + if (data?.code !== 1 || !data?.data?.wemediaId) return { loggedIn: false } - const username = data.data.tname || '' - let avatar = data.data.icon || '' + const username = data.data.tname || '' + let avatar = data.data.icon || '' - if (avatar && (avatar.includes('126.net') || avatar.includes('163.com'))) { - avatar = await convertAvatarToBase64(avatar, 'https://mp.163.com/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.error('[COSE] Wangyihao Detection Error:', e) - return { loggedIn: false, error: e.message } + if (avatar && (avatar.includes('126.net') || avatar.includes('163.com'))) { + avatar = await convertAvatarToBase64(avatar, 'https://mp.163.com/') } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.error('[COSE] Wangyihao Detection Error:', e) + return { loggedIn: false, error: e.message } + } } diff --git a/packages/detection/src/platforms/wechat.js b/packages/detection/src/platforms/wechat.js index 8c8d19b..5f19b07 100644 --- a/packages/detection/src/platforms/wechat.js +++ b/packages/detection/src/platforms/wechat.js @@ -6,99 +6,99 @@ * 3. Fallback: fetch mp.weixin.qq.com HTML and parse nick_name/head_img */ export async function detectWechatUser() { - const platformId = 'wechat' - try { - // 先检查缓存 - const stored = await chrome.storage.local.get('wechat_user') - const cachedUser = stored.wechat_user + const platformId = 'wechat' + try { + // 先检查缓存 + const stored = await chrome.storage.local.get('wechat_user') + const cachedUser = stored.wechat_user - if (cachedUser && cachedUser.loggedIn) { - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 1 * 60 * 60 * 1000 // 1 hour + if (cachedUser && cachedUser.loggedIn) { + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 1 * 60 * 60 * 1000 // 1 hour - if (cacheAge < maxAge) { - console.log(`[COSE] wechat 从缓存读取:`, cachedUser.username) - return { - loggedIn: true, - username: cachedUser.username || '', - avatar: cachedUser.avatar || '' - } - } else { - await chrome.storage.local.remove('wechat_user') - } + if (cacheAge < maxAge) { + console.log(`[COSE] wechat 从缓存读取:`, cachedUser.username) + return { + loggedIn: true, + username: cachedUser.username || '', + avatar: cachedUser.avatar || '', } - - // 优先尝试在已打开的微信公众号页面中检测 - const tabs = await chrome.tabs.query({ url: 'https://mp.weixin.qq.com/*' }) - if (tabs.length > 0) { - try { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: () => { - const wxData = window.wx?.data - if (wxData && wxData.nick_name) { - return { - loggedIn: true, - username: wxData.nick_name || wxData.user_name || '', - avatar: wxData.head_img || '', - token: wxData.t || '' - } - } - return null - } - }) - - const result = results?.[0]?.result - if (result && result.loggedIn) { - const userInfo = { ...result, cachedAt: Date.now() } - await chrome.storage.local.set({ wechat_user: userInfo }) - return { - loggedIn: true, - username: userInfo.username || '', - avatar: userInfo.avatar || '' - } - } - } catch (e) { - console.log(`[COSE] wechat 页面脚本执行失败:`, e.message) - } - } - - // 备用方案:fetch 首页并解析 HTML - try { - const response = await fetch('https://mp.weixin.qq.com/', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'text/html' } - }) - const html = await response.text() - - if (html.includes('请使用微信扫描') || html.includes('扫码登录')) { - return { loggedIn: false } - } - - const nickMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/) - const avatarMatch = html.match(/head_img\s*[:=]\s*["']([^"']+)["']/) - - if (nickMatch) { - const username = nickMatch[1] - const avatar = avatarMatch ? avatarMatch[1] : '' - await chrome.storage.local.set({ - wechat_user: { - loggedIn: true, - username, - avatar, - cachedAt: Date.now() - } - }) - return { loggedIn: true, username, avatar } - } - } catch (e) { - console.log(`[COSE] wechat fetch 失败:`, e.message) - } - - return { loggedIn: false } - } catch (e) { - console.log(`[COSE] wechat 检测失败:`, e.message) - return { loggedIn: false } + } else { + await chrome.storage.local.remove('wechat_user') + } } + + // 优先尝试在已打开的微信公众号页面中检测 + const tabs = await chrome.tabs.query({ url: 'https://mp.weixin.qq.com/*' }) + if (tabs.length > 0) { + try { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: () => { + const wxData = window.wx?.data + if (wxData && wxData.nick_name) { + return { + loggedIn: true, + username: wxData.nick_name || wxData.user_name || '', + avatar: wxData.head_img || '', + token: wxData.t || '', + } + } + return null + }, + }) + + const result = results?.[0]?.result + if (result && result.loggedIn) { + const userInfo = { ...result, cachedAt: Date.now() } + await chrome.storage.local.set({ wechat_user: userInfo }) + return { + loggedIn: true, + username: userInfo.username || '', + avatar: userInfo.avatar || '', + } + } + } catch (e) { + console.log(`[COSE] wechat 页面脚本执行失败:`, e.message) + } + } + + // 备用方案:fetch 首页并解析 HTML + try { + const response = await fetch('https://mp.weixin.qq.com/', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'text/html' }, + }) + const html = await response.text() + + if (html.includes('请使用微信扫描') || html.includes('扫码登录')) { + return { loggedIn: false } + } + + const nickMatch = html.match(/nick_name\s*[:=]\s*["']([^"']+)["']/) + const avatarMatch = html.match(/head_img\s*[:=]\s*["']([^"']+)["']/) + + if (nickMatch) { + const username = nickMatch[1] + const avatar = avatarMatch ? avatarMatch[1] : '' + await chrome.storage.local.set({ + wechat_user: { + loggedIn: true, + username, + avatar, + cachedAt: Date.now(), + }, + }) + return { loggedIn: true, username, avatar } + } + } catch (e) { + console.log(`[COSE] wechat fetch 失败:`, e.message) + } + + return { loggedIn: false } + } catch (e) { + console.log(`[COSE] wechat 检测失败:`, e.message) + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/weibo.js b/packages/detection/src/platforms/weibo.js index 09ef95d..b0e5ec4 100644 --- a/packages/detection/src/platforms/weibo.js +++ b/packages/detection/src/platforms/weibo.js @@ -7,91 +7,91 @@ import { convertAvatarToBase64 } from '../utils.js' * 2. Fetch editor page HTML and extract nick/avatar via regex */ export async function detectWeiboUser() { - const platformId = 'weibo' - try { - // 检查 card.weibo.com 的 SUBP cookie - const subpCookie = await chrome.cookies.get({ - url: 'https://card.weibo.com', - name: 'SUBP' - }) + const platformId = 'weibo' + try { + // 检查 card.weibo.com 的 SUBP cookie + const subpCookie = await chrome.cookies.get({ + url: 'https://card.weibo.com', + name: 'SUBP', + }) - // 也检查 ALF cookie - const alfCookie = await chrome.cookies.get({ - url: 'https://card.weibo.com', - name: 'ALF' - }) + // 也检查 ALF cookie + const alfCookie = await chrome.cookies.get({ + url: 'https://card.weibo.com', + name: 'ALF', + }) - if (!subpCookie && !alfCookie) { - console.log(`[COSE] weibo 未找到登录 cookie,未登录`) - return { loggedIn: false } - } - - // 有 cookie,通过 fetch HTML 获取用户信息 - let username = '' - let avatar = '' - - try { - // 获取所有相关 cookies 并手动添加到请求 - const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' }) - const cardCookies = await chrome.cookies.getAll({ domain: 'card.weibo.com' }) - const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' }) - const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies] - const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ') - - const response = await fetch('https://card.weibo.com/article/v5/editor', { - method: 'GET', - headers: { - 'Cookie': cookieString, - }, - credentials: 'include', - }) - const html = await response.text() - - // 从 HTML 中提取用户名 - const nickMatch = html.match(/"nick"\s*:\s*"([^"]+)"/) - if (nickMatch) { - username = nickMatch[1] - } else { - // 深度查找 nick - const altNickMatch = html.match(/\\"nick\\"\s*:\s*\\"([^\\"]+)\\"/) - if (altNickMatch) { - username = altNickMatch[1] - } - } - - // 从 HTML 中提取头像 - const avatarMatch = html.match(/"avatar_large"\s*:\s*"([^"]+)"/) - if (avatarMatch) { - avatar = avatarMatch[1].replace(/\\/g, '') - } else { - const altAvatarMatch = html.match(/\\"avatar_large\\"\s*:\s*\\"([^\\"]+)\\"/) - if (altAvatarMatch) { - let rawAvatar = altAvatarMatch[1].replace(/\\\\\\\//g, '/') - if (rawAvatar.includes('sinaimg.cn')) { - avatar = rawAvatar.split('?')[0] - } else { - avatar = rawAvatar - } - } - } - - console.log(`[COSE] weibo 用户信息: ${username}`) - } catch (e) { - console.log(`[COSE] weibo 获取用户详情失败:`, e.message) - } - - if (!username) { - return { loggedIn: false } - } - - // Convert sinaimg.cn avatar to base64 data URL to bypass CORS/ORB - if (avatar && avatar.includes('sinaimg.cn')) { - avatar = await convertAvatarToBase64(avatar, 'https://weibo.com/') - } - - return { loggedIn: true, username, avatar } - } catch (e) { - console.log(`[COSE] weibo 检测失败:`, e.message) - return { loggedIn: false } + if (!subpCookie && !alfCookie) { + console.log(`[COSE] weibo 未找到登录 cookie,未登录`) + return { loggedIn: false } } + + // 有 cookie,通过 fetch HTML 获取用户信息 + let username = '' + let avatar = '' + + try { + // 获取所有相关 cookies 并手动添加到请求 + const weiboCookies = await chrome.cookies.getAll({ domain: '.weibo.com' }) + const cardCookies = await chrome.cookies.getAll({ domain: 'card.weibo.com' }) + const sinaCookies = await chrome.cookies.getAll({ domain: '.sina.com.cn' }) + const allCookies = [...weiboCookies, ...cardCookies, ...sinaCookies] + const cookieString = allCookies.map(c => `${c.name}=${c.value}`).join('; ') + + const response = await fetch('https://card.weibo.com/article/v5/editor', { + method: 'GET', + headers: { + Cookie: cookieString, + }, + credentials: 'include', + }) + const html = await response.text() + + // 从 HTML 中提取用户名 + const nickMatch = html.match(/"nick"\s*:\s*"([^"]+)"/) + if (nickMatch) { + username = nickMatch[1] + } else { + // 深度查找 nick + const altNickMatch = html.match(/\\"nick\\"\s*:\s*\\"([^\\"]+)\\"/) + if (altNickMatch) { + username = altNickMatch[1] + } + } + + // 从 HTML 中提取头像 + const avatarMatch = html.match(/"avatar_large"\s*:\s*"([^"]+)"/) + if (avatarMatch) { + avatar = avatarMatch[1].replace(/\\/g, '') + } else { + const altAvatarMatch = html.match(/\\"avatar_large\\"\s*:\s*\\"([^\\"]+)\\"/) + if (altAvatarMatch) { + let rawAvatar = altAvatarMatch[1].replace(/\\\\\\\//g, '/') + if (rawAvatar.includes('sinaimg.cn')) { + avatar = rawAvatar.split('?')[0] + } else { + avatar = rawAvatar + } + } + } + + console.log(`[COSE] weibo 用户信息: ${username}`) + } catch (e) { + console.log(`[COSE] weibo 获取用户详情失败:`, e.message) + } + + if (!username) { + return { loggedIn: false } + } + + // Convert sinaimg.cn avatar to base64 data URL to bypass CORS/ORB + if (avatar && avatar.includes('sinaimg.cn')) { + avatar = await convertAvatarToBase64(avatar, 'https://weibo.com/') + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.log(`[COSE] weibo 检测失败:`, e.message) + return { loggedIn: false } + } } diff --git a/packages/detection/src/platforms/xiaohongshu.js b/packages/detection/src/platforms/xiaohongshu.js index 06878ac..e9a97e8 100644 --- a/packages/detection/src/platforms/xiaohongshu.js +++ b/packages/detection/src/platforms/xiaohongshu.js @@ -6,85 +6,102 @@ * 3. Fall back to chrome.storage.local cache for user details */ export async function detectXiaohongshuUser() { - try { - // 1. Check a1 cookie as login indicator (MV3 service worker compatible) - const a1Cookie = await chrome.cookies.get({ url: 'https://creator.xiaohongshu.com', name: 'a1' }) - if (!a1Cookie || !a1Cookie.value) { - return { loggedIn: false } - } - - // Logged in — now try to get user details - - // 2a. Try offscreen fetch (document context, cookies sent automatically) - try { - const offscreenDetect = globalThis.__coseDetectXiaohongshu - if (offscreenDetect) { - const offResult = await offscreenDetect() - if (offResult && offResult.loggedIn) { - // Update cache - const userInfo = { ...offResult, cachedAt: Date.now() } - await chrome.storage.local.set({ xiaohongshu_user: userInfo }) - return { loggedIn: true, username: offResult.username || '', avatar: offResult.avatar || '' } - } - } - } catch (e) { - console.log('[COSE] xiaohongshu offscreen detection failed:', e.message) - } - - // 2b. Try open tab injection - try { - const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' }) - if (tabs.length > 0) { - const results = await chrome.scripting.executeScript({ - target: { tabId: tabs[0].id }, - func: async () => { - try { - const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', { - method: 'GET', - credentials: 'include', - headers: { 'Accept': 'application/json' } - }) - if (!response.ok) return null - const data = await response.json() - if (data?.success === true && data?.code === 0 && data?.data?.userId) { - return { - loggedIn: true, - username: data.data.userName || data.data.redId || '', - avatar: data.data.userAvatar || '', - userId: data.data.userId - } - } - return null - } catch (e) { return null } - } - }) - const result = results?.[0]?.result - if (result && result.loggedIn) { - const userInfo = { ...result, cachedAt: Date.now() } - await chrome.storage.local.set({ xiaohongshu_user: userInfo }) - return { loggedIn: true, username: userInfo.username || '', avatar: userInfo.avatar || '' } - } - } - } catch (e) { - console.log('[COSE] xiaohongshu tab detection failed:', e.message) - } - - // 2c. Fall back to cache for user details - const stored = await chrome.storage.local.get('xiaohongshu_user') - const cachedUser = stored.xiaohongshu_user - if (cachedUser && cachedUser.username) { - const cacheAge = Date.now() - (cachedUser.cachedAt || 0) - const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days - if (cacheAge < maxAge) { - console.log('[COSE] xiaohongshu 从缓存读取:', cachedUser.username) - return { loggedIn: true, username: cachedUser.username || '', avatar: cachedUser.avatar || '' } - } - } - - // Cookie exists but couldn't get user details — still logged in - return { loggedIn: true, username: '', avatar: '' } - } catch (e) { - console.log('[COSE] xiaohongshu 检测失败:', e.message) - return { loggedIn: false } + try { + // 1. Check a1 cookie as login indicator (MV3 service worker compatible) + const a1Cookie = await chrome.cookies.get({ + url: 'https://creator.xiaohongshu.com', + name: 'a1', + }) + if (!a1Cookie || !a1Cookie.value) { + return { loggedIn: false } } + + // Logged in — now try to get user details + + // 2a. Try offscreen fetch (document context, cookies sent automatically) + try { + const offscreenDetect = globalThis.__coseDetectXiaohongshu + if (offscreenDetect) { + const offResult = await offscreenDetect() + if (offResult && offResult.loggedIn) { + // Update cache + const userInfo = { ...offResult, cachedAt: Date.now() } + await chrome.storage.local.set({ xiaohongshu_user: userInfo }) + return { + loggedIn: true, + username: offResult.username || '', + avatar: offResult.avatar || '', + } + } + } + } catch (e) { + console.log('[COSE] xiaohongshu offscreen detection failed:', e.message) + } + + // 2b. Try open tab injection + try { + const tabs = await chrome.tabs.query({ url: 'https://creator.xiaohongshu.com/*' }) + if (tabs.length > 0) { + const results = await chrome.scripting.executeScript({ + target: { tabId: tabs[0].id }, + func: async () => { + try { + const response = await fetch('https://creator.xiaohongshu.com/api/galaxy/user/info', { + method: 'GET', + credentials: 'include', + headers: { Accept: 'application/json' }, + }) + if (!response.ok) return null + const data = await response.json() + if (data?.success === true && data?.code === 0 && data?.data?.userId) { + return { + loggedIn: true, + username: data.data.userName || data.data.redId || '', + avatar: data.data.userAvatar || '', + userId: data.data.userId, + } + } + return null + } catch (e) { + return null + } + }, + }) + const result = results?.[0]?.result + if (result && result.loggedIn) { + const userInfo = { ...result, cachedAt: Date.now() } + await chrome.storage.local.set({ xiaohongshu_user: userInfo }) + return { + loggedIn: true, + username: userInfo.username || '', + avatar: userInfo.avatar || '', + } + } + } + } catch (e) { + console.log('[COSE] xiaohongshu tab detection failed:', e.message) + } + + // 2c. Fall back to cache for user details + const stored = await chrome.storage.local.get('xiaohongshu_user') + const cachedUser = stored.xiaohongshu_user + if (cachedUser && cachedUser.username) { + const cacheAge = Date.now() - (cachedUser.cachedAt || 0) + const maxAge = 7 * 24 * 60 * 60 * 1000 // 7 days + if (cacheAge < maxAge) { + console.log('[COSE] xiaohongshu 从缓存读取:', cachedUser.username) + return { + loggedIn: true, + username: cachedUser.username || '', + avatar: cachedUser.avatar || '', + } + } + } + + // Cookie exists but couldn't get user details — still logged in + return { loggedIn: true, username: '', avatar: '' } + } catch (e) { + console.log('[COSE] xiaohongshu 检测失败:', e.message) + return { loggedIn: false } + } } diff --git a/packages/detection/src/utils.js b/packages/detection/src/utils.js index cd1f2ce..52172f8 100644 --- a/packages/detection/src/utils.js +++ b/packages/detection/src/utils.js @@ -6,171 +6,179 @@ * @returns {Promise} - base64 data URL, or original URL if conversion fails */ export async function convertAvatarToBase64(avatarUrl, referer) { - try { - const imgResp = await fetch(avatarUrl, { - headers: { 'Referer': referer } - }) - if (imgResp.ok) { - const blob = await imgResp.blob() - const buffer = await blob.arrayBuffer() - const bytes = new Uint8Array(buffer) - let binary = '' - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]) - } - const base64 = btoa(binary) - const mime = blob.type || 'image/jpeg' - return `data:${mime};base64,${base64}` - } - } catch (e) { - console.log('[COSE] avatar base64 conversion failed:', e.message) + try { + const imgResp = await fetch(avatarUrl, { + headers: { Referer: referer }, + }) + if (imgResp.ok) { + const blob = await imgResp.blob() + const buffer = await blob.arrayBuffer() + const bytes = new Uint8Array(buffer) + let binary = '' + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]) + } + const base64 = btoa(binary) + const mime = blob.type || 'image/jpeg' + return `data:${mime};base64,${base64}` } - return avatarUrl + } catch (e) { + console.log('[COSE] avatar base64 conversion failed:', e.message) + } + return avatarUrl } export async function logToStorage(msg, data = null) { - if (data) { - console.log(msg, data) - } else { - console.log(msg) - } + if (data) { + console.log(msg, data) + } else { + console.log(msg) + } } // 通过 Cookie 检测登录状态 export async function checkLoginByCookie(platformId, config) { - try { - // 直接按名称查找 cookie - const cookieMap = {} - if (config.cookieNames) { - for (const name of config.cookieNames) { - const cookie = await chrome.cookies.get({ - url: config.cookieUrl || `https://${config.cookieDomain}`, - name: name - }) - if (cookie) { - cookieMap[name] = cookie.value - } - } + try { + // 直接按名称查找 cookie + const cookieMap = {} + if (config.cookieNames) { + for (const name of config.cookieNames) { + const cookie = await chrome.cookies.get({ + url: config.cookieUrl || `https://${config.cookieDomain}`, + name: name, + }) + if (cookie) { + cookieMap[name] = cookie.value } - - console.log(`[COSE] ${platformId} 找到的cookies:`, Object.keys(cookieMap)) - - const hasLoginCookie = config.cookieNames && config.cookieNames.some(name => cookieMap[name]) - - if (!hasLoginCookie) { - console.log(`[COSE] ${platformId} 未找到登录 cookie`) - return { loggedIn: false } - } - - // 自定义 cookie 值检测逻辑(如 InfoQ) - if (config.customCheck && config.checkCookieValue) { - console.log(`[COSE] ${platformId} 使用自定义 cookie 检测`) - const result = config.checkCookieValue(cookieMap) - console.log(`[COSE] ${platformId} 自定义检测结果:`, result) - return result - } - - let username = '' - let avatar = '' - - // 如果配置了从 cookie 获取用户名 - if (config.getUsernameFromCookie && config.usernameCookie) { - username = decodeURIComponent(cookieMap[config.usernameCookie] || '') - } - - - // 使用平台配置的 fetchAvatar 回调获取头像 - if (config.fetchAvatar && typeof config.fetchAvatar === 'function') { - try { - const fetchedAvatar = await config.fetchAvatar(cookieMap) - if (fetchedAvatar) { - avatar = fetchedAvatar - console.log(`[COSE] ${platformId} 找到头像:`, avatar) - } - } catch (e) { - console.log(`[COSE] ${platformId} 获取头像失败:`, e.message) - } - } - - return { loggedIn: true, username, avatar } - - } catch (e) { - console.log(`[COSE] ${platformId} Cookie 检测失败:`, e.message) - return { loggedIn: false, error: e.message } + } } + + console.log(`[COSE] ${platformId} 找到的cookies:`, Object.keys(cookieMap)) + + const hasLoginCookie = config.cookieNames && config.cookieNames.some(name => cookieMap[name]) + + if (!hasLoginCookie) { + console.log(`[COSE] ${platformId} 未找到登录 cookie`) + return { loggedIn: false } + } + + // 自定义 cookie 值检测逻辑(如 InfoQ) + if (config.customCheck && config.checkCookieValue) { + console.log(`[COSE] ${platformId} 使用自定义 cookie 检测`) + const result = config.checkCookieValue(cookieMap) + console.log(`[COSE] ${platformId} 自定义检测结果:`, result) + return result + } + + let username = '' + let avatar = '' + + // 如果配置了从 cookie 获取用户名 + if (config.getUsernameFromCookie && config.usernameCookie) { + username = decodeURIComponent(cookieMap[config.usernameCookie] || '') + } + + // 使用平台配置的 fetchAvatar 回调获取头像 + if (config.fetchAvatar && typeof config.fetchAvatar === 'function') { + try { + const fetchedAvatar = await config.fetchAvatar(cookieMap) + if (fetchedAvatar) { + avatar = fetchedAvatar + console.log(`[COSE] ${platformId} 找到头像:`, avatar) + } + } catch (e) { + console.log(`[COSE] ${platformId} 获取头像失败:`, e.message) + } + } + + return { loggedIn: true, username, avatar } + } catch (e) { + console.log(`[COSE] ${platformId} Cookie 检测失败:`, e.message) + return { loggedIn: false, error: e.message } + } } // 通用 API 检测逻辑 export async function detectByApi(platformId, config) { + try { + console.log(`[COSE] ${platformId} 开始 API 检测: ${config.api}`) + const controller = new AbortController() + const timeoutId = setTimeout(() => controller.abort(), 8000) + + // Collect cookies via chrome.cookies.getAll for MV3 service worker compatibility + let cookieStr = '' try { - console.log(`[COSE] ${platformId} 开始 API 检测: ${config.api}`) - const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 8000) - - // Collect cookies via chrome.cookies.getAll for MV3 service worker compatibility - let cookieStr = '' - try { - const apiUrl = new URL(config.api) - const domain = apiUrl.hostname.split('.').slice(-2).join('.') - const domainCookies = await chrome.cookies.getAll({ domain: `.${domain}` }) - const urlCookies = await chrome.cookies.getAll({ url: config.api }) - const allCookies = [...domainCookies, ...urlCookies] - const seen = new Set() - const uniqueCookies = allCookies.filter(c => { - const key = `${c.name}=${c.value}` - if (seen.has(key)) return false - seen.add(key) - return true - }) - cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') - } catch (e) { - console.log(`[COSE] ${platformId} cookie 收集失败:`, e.message) - } - - const apiUrl = new URL(config.api) - const origin = apiUrl.origin - - const fetchOptions = { - method: config.method || 'GET', - headers: { - 'Accept': config.isHtml ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' : 'application/json', - 'Cache-Control': 'no-cache', - ...(cookieStr ? { 'Cookie': cookieStr } : {}), - 'Origin': origin, - 'Referer': origin + '/', - ...(config.headers || {}) - }, - signal: controller.signal, - } - - if (config.body) { - fetchOptions.body = config.body - } - - const response = await fetch(config.api, fetchOptions) - clearTimeout(timeoutId) - console.log(`[COSE] ${platformId} API 响应状态: ${response.status}`) - - let data = null - if (config.isHtml) { - // 明确指定为 HTML 响应时,使用 text() 解析 - try { data = await response.text() } catch (e) { data = '' } - } else { - // 其他情况尝试 JSON 解析 - try { data = await response.json() } catch (e) { data = null } - } - // console.log(`[COSE] ${platformId} API 数据:`, data) - - const loggedIn = config.checkLogin(data) - // console.log(`[COSE] ${platformId} checkLogin 结果: ${loggedIn}`) - if (loggedIn && config.getUserInfo) { - const userInfo = config.getUserInfo(data) - console.log(`[COSE] ${platformId} 用户信息:`, userInfo) - return { loggedIn: true, ...userInfo } - } - return { loggedIn: !!loggedIn } - } catch (error) { - console.log(`[COSE] ${platformId} API 检测失败: ${error.message}`) - return { loggedIn: false, error: error.message } + const apiUrl = new URL(config.api) + const domain = apiUrl.hostname.split('.').slice(-2).join('.') + const domainCookies = await chrome.cookies.getAll({ domain: `.${domain}` }) + const urlCookies = await chrome.cookies.getAll({ url: config.api }) + const allCookies = [...domainCookies, ...urlCookies] + const seen = new Set() + const uniqueCookies = allCookies.filter(c => { + const key = `${c.name}=${c.value}` + if (seen.has(key)) return false + seen.add(key) + return true + }) + cookieStr = uniqueCookies.map(c => `${c.name}=${c.value}`).join('; ') + } catch (e) { + console.log(`[COSE] ${platformId} cookie 收集失败:`, e.message) } + + const apiUrl = new URL(config.api) + const origin = apiUrl.origin + + const fetchOptions = { + method: config.method || 'GET', + headers: { + Accept: config.isHtml + ? 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' + : 'application/json', + 'Cache-Control': 'no-cache', + ...(cookieStr ? { Cookie: cookieStr } : {}), + Origin: origin, + Referer: origin + '/', + ...(config.headers || {}), + }, + signal: controller.signal, + } + + if (config.body) { + fetchOptions.body = config.body + } + + const response = await fetch(config.api, fetchOptions) + clearTimeout(timeoutId) + console.log(`[COSE] ${platformId} API 响应状态: ${response.status}`) + + let data = null + if (config.isHtml) { + // 明确指定为 HTML 响应时,使用 text() 解析 + try { + data = await response.text() + } catch (e) { + data = '' + } + } else { + // 其他情况尝试 JSON 解析 + try { + data = await response.json() + } catch (e) { + data = null + } + } + // console.log(`[COSE] ${platformId} API 数据:`, data) + + const loggedIn = config.checkLogin(data) + // console.log(`[COSE] ${platformId} checkLogin 结果: ${loggedIn}`) + if (loggedIn && config.getUserInfo) { + const userInfo = config.getUserInfo(data) + console.log(`[COSE] ${platformId} 用户信息:`, userInfo) + return { loggedIn: true, ...userInfo } + } + return { loggedIn: !!loggedIn } + } catch (error) { + console.log(`[COSE] ${platformId} API 检测失败: ${error.message}`) + return { loggedIn: false, error: error.message } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cef61c..77a7a05 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: devDependencies: + lint-staged: + specifier: ^16.1.6 + version: 16.4.0 + prettier: + specifier: ^3.6.2 + version: 3.8.4 + simple-git-hooks: + specifier: ^2.13.1 + version: 2.13.1 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -32,10 +41,10 @@ importers: version: 4.22.4 vite: specifier: ^8.0.16 - version: 8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4) + version: 8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0) vite-plugin-static-copy: specifier: ^4.1.1 - version: 4.1.1(vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)) + version: 4.1.1(vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0)) web-ext: specifier: ^10.4.0 version: 10.4.0 @@ -510,6 +519,10 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -631,6 +644,14 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -646,6 +667,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + columnify@1.6.0: resolution: {integrity: sha512-lomjuFZKfM6MSAnV9aCZC9sc0qGbmZdfygNv+nCpqVkSKdCxCklLtd16O0EILGkImHw9ZpHkAnHaB+8Zxq5W6Q==} engines: {node: '>=8.0.0'} @@ -654,6 +678,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -788,6 +816,10 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + es6-error@4.1.1: resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} @@ -876,6 +908,9 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + execa@9.6.1: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} @@ -1074,6 +1109,10 @@ packages: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1280,6 +1319,15 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lint-staged@16.4.0: + resolution: {integrity: sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==} + engines: {node: '>=20.17'} + hasBin: true + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} @@ -1308,6 +1356,10 @@ packages: lodash.once@4.1.1: resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -1317,6 +1369,10 @@ packages: marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} @@ -1365,6 +1421,10 @@ packages: resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} engines: {node: '>=14.0.0'} + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + open@11.0.0: resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} engines: {node: '>=20'} @@ -1468,6 +1528,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.8.4: + resolution: {integrity: sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==} + engines: {node: '>=14'} + hasBin: true + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -1534,6 +1599,13 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -1593,6 +1665,18 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + simple-git-hooks@2.13.1: + resolution: {integrity: sha512-WszCLXwT4h2k1ufIXAgsbiTOazqqevFCIncOuUBZJ91DdvWcC5+OFkluWRQPrcuSYd8fjq+o2y1QfWqYMoAToQ==} + hasBin: true + + slice-ansi@7.1.0: + resolution: {integrity: sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + sonic-boom@4.2.1: resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} @@ -1614,6 +1698,10 @@ packages: split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -1626,6 +1714,10 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} @@ -1682,6 +1774,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -1890,6 +1986,11 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -2287,6 +2388,10 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -2418,6 +2523,15 @@ snapshots: cli-boxes@3.0.0: {} + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -2432,6 +2546,8 @@ snapshots: color-name@1.1.4: {} + colorette@2.0.20: {} + columnify@1.6.0: dependencies: strip-ansi: 6.0.1 @@ -2439,6 +2555,8 @@ snapshots: commander@12.1.0: {} + commander@14.0.3: {} + commander@9.5.0: {} common-tags@1.8.2: {} @@ -2553,6 +2671,8 @@ snapshots: entities@7.0.1: {} + environment@1.1.0: {} + es6-error@4.1.1: {} esbuild@0.28.1: @@ -2678,6 +2798,8 @@ snapshots: esutils@2.0.3: {} + eventemitter3@5.0.4: {} + execa@9.6.1: dependencies: '@sindresorhus/merge-streams': 4.0.0 @@ -2858,6 +2980,10 @@ snapshots: is-fullwidth-code-point@3.0.0: {} + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3041,6 +3167,24 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lint-staged@16.4.0: + dependencies: + commander: 14.0.3 + listr2: 9.0.5 + picomatch: 4.0.4 + string-argv: 0.3.2 + tinyexec: 1.2.4 + yaml: 2.9.0 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.0 + locate-path@6.0.0: dependencies: p-locate: 5.0.0 @@ -3061,12 +3205,22 @@ snapshots: lodash.once@4.1.1: {} + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.0 + lru-cache@10.4.3: {} make-error@1.3.6: {} marky@1.3.0: {} + mimic-function@5.0.1: {} + minimatch@3.1.5: dependencies: brace-expansion: 1.1.15 @@ -3107,6 +3261,10 @@ snapshots: on-exit-leak-free@2.1.2: {} + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + open@11.0.0: dependencies: default-browser: 5.5.0 @@ -3220,6 +3378,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.8.4: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -3281,6 +3441,13 @@ snapshots: resolve-from@4.0.0: {} + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rfdc@1.4.1: {} + rimraf@5.0.10: dependencies: glob: 10.5.0 @@ -3334,6 +3501,18 @@ snapshots: signal-exit@4.1.0: {} + simple-git-hooks@2.13.1: {} + + slice-ansi@7.1.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + sonic-boom@4.2.1: dependencies: atomic-sleep: 1.0.0 @@ -3353,6 +3532,8 @@ snapshots: dependencies: through: 2.3.8 + string-argv@0.3.2: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -3371,6 +3552,11 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + string_decoder@1.1.1: dependencies: safe-buffer: 5.1.2 @@ -3418,6 +3604,8 @@ snapshots: through@2.3.8: {} + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -3479,15 +3667,15 @@ snapshots: util-deprecate@1.0.2: {} - vite-plugin-static-copy@4.1.1(vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)): + vite-plugin-static-copy@4.1.1(vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0)): dependencies: chokidar: 3.6.0 p-map: 7.0.4 picocolors: 1.1.1 tinyglobby: 0.2.17 - vite: 8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4) + vite: 8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0) - vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4): + vite@8.0.16(@types/node@25.2.0)(esbuild@0.28.1)(tsx@4.22.4)(yaml@2.9.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -3499,6 +3687,7 @@ snapshots: esbuild: 0.28.1 fsevents: 2.3.3 tsx: 4.22.4 + yaml: 2.9.0 watchpack@2.5.1: dependencies: @@ -3603,6 +3792,8 @@ snapshots: y18n@5.0.8: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.2: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0071a96..3a87953 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,5 @@ allowBuilds: core-js: false dtrace-provider: false esbuild: true + simple-git-hooks: true spawn-sync: false diff --git a/tests/wechat.test.mjs b/tests/wechat.test.mjs index 7947629..2bfd6b2 100644 --- a/tests/wechat.test.mjs +++ b/tests/wechat.test.mjs @@ -6,13 +6,7 @@ import { pickWechatBodyProseMirrorCandidate, } from '../packages/core/src/platforms/wechat.js' -function createEditor({ - textContent = '', - top = 0, - height = 120, - width = 640, - classes = [], -} = {}) { +function createEditor({ textContent = '', top = 0, height = 120, width = 640, classes = [] } = {}) { const classSet = new Set(classes) const rect = { top, @@ -28,7 +22,7 @@ function createEditor({ clientHeight: height, clientWidth: width, getBoundingClientRect: () => rect, - closest: (selector) => { + closest: selector => { const selectors = selector.split(',').map(item => item.trim().replace(/^\./, '')) return selectors.some(item => classSet.has(item)) ? { selector } : null }, @@ -56,11 +50,7 @@ function createInputElement() { } } -function createProseMirrorElement({ - textContent = '', - classes = [], - onDispatchEvent, -} = {}) { +function createProseMirrorElement({ textContent = '', classes = [], onDispatchEvent } = {}) { const classSet = new Set(classes) let html = '' let text = textContent @@ -99,8 +89,7 @@ function createProseMirrorElement({ imageCount = (value.match(/ ({})) + if (selector === 'img') return Array.from({ length: imageCount }, () => ({})) return [] }, getBoundingClientRect: () => ({ @@ -111,7 +100,7 @@ function createProseMirrorElement({ width: 720, height: classSet.has('title-editor__input') ? 40 : 480, }), - closest: (selector) => { + closest: selector => { const selectors = selector.split(',').map(item => item.trim().replace(/^\./, '')) return selectors.some(item => classSet.has(item)) ? { selector } : null }, @@ -155,17 +144,15 @@ async function withWechatFillEnvironment({ jsapiInvoke, bodyEditor, callback }) globalThis.Event = TestEvent globalThis.ClipboardEvent = TestEvent globalThis.DataTransfer = TestDataTransfer - globalThis.setTimeout = (fn) => { + globalThis.setTimeout = fn => { fn() return 0 } globalThis.window = { - waitFor: async (selector) => { - if (selector === '#title') - return titleInput - if (selector === '.title-editor__input .ProseMirror') - return titleEditor + waitFor: async selector => { + if (selector === '#title') return titleInput + if (selector === '.title-editor__input .ProseMirror') return titleEditor return null }, HTMLTextAreaElement: class HTMLTextAreaElement {}, @@ -179,27 +166,20 @@ async function withWechatFillEnvironment({ jsapiInvoke, bodyEditor, callback }) } globalThis.document = { - querySelectorAll: selector => selector === '.ProseMirror' - ? [titleEditor, bodyEditor] - : [], - querySelector: (selector) => { - if (selector === '#title') - return titleInput - if (selector === '.title-editor__input .ProseMirror') - return titleEditor + querySelectorAll: selector => (selector === '.ProseMirror' ? [titleEditor, bodyEditor] : []), + querySelector: selector => { + if (selector === '#title') return titleInput + if (selector === '.title-editor__input .ProseMirror') return titleEditor return null }, } try { return await callback({ titleInput, titleEditor }) - } - finally { + } finally { for (const [key, value] of Object.entries(previousGlobals)) { - if (value === undefined) - delete globalThis[key] - else - globalThis[key] = value + if (value === undefined) delete globalThis[key] + else globalThis[key] = value } } } @@ -232,7 +212,10 @@ test('标题和正文同时存在时优先选择正文编辑器', () => { width: 720, }) - const picked = pickWechatBodyProseMirrorCandidate([titleEditor, bodyEditor], { titleInput, titleEditor }) + const picked = pickWechatBodyProseMirrorCandidate([titleEditor, bodyEditor], { + titleInput, + titleEditor, + }) assert.equal(picked, bodyEditor) }) @@ -257,7 +240,7 @@ test('正文填充优先使用微信编辑器 JSAPI 注入含图 HTML', async () const result = await withWechatFillEnvironment({ bodyEditor, - jsapiInvoke: (params) => { + jsapiInvoke: params => { calls.push(params) bodyEditor.setRenderedContent(params.apiParam.content) params.sucCb({}) @@ -278,7 +261,7 @@ test('微信 JSAPI 不可用时使用 paste 事件兜底而不是直接写 inner let pasteEvent const bodyEditor = createProseMirrorElement({ textContent: '从这里开始写正文', - onDispatchEvent: (event) => { + onDispatchEvent: event => { if (event.type === 'paste') { pasteEvent = event bodyEditor.setRenderedContent(event.clipboardData.getData('text/html'))