chore: add Prettier formatting config and pre-commit hooks (#253)

This commit is contained in:
Libin YANG
2026-06-17 19:40:35 +08:00
committed by GitHub
parent 5a7d170044
commit e70fa9e92a
84 changed files with 4125 additions and 3245 deletions
+13
View File
@@ -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
+13 -13
View File
@@ -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:
+10 -10
View File
@@ -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
required: false
@@ -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
+21 -18
View File
@@ -1,18 +1,20 @@
## Summary / 概述
<!-- Provide a brief description of the changes in this PR -->
<!-- 简要描述此 PR 中的更改 -->
## Related Issue / 关联 Issue
<!-- Link to the related issue(s) -->
<!-- 链接到相关的 issue -->
Closes #
## Type of Change / 更改类型
<!-- Mark the relevant option with an "x" -->
<!-- 用 "x" 标记相关选项 -->
- [ ] Bug fix / 修复 Bug (non-breaking change that fixes an issue / 修复问题的非破坏性更改)
- [ ] New feature / 新功能 (non-breaking change that adds functionality / 添加功能的非破坏性更改)
- [ ] Breaking change / 破坏性更改 (fix or feature that would cause existing functionality to not work as expected / 会导致现有功能无法正常工作的修复或功能)
@@ -21,31 +23,33 @@ Closes #
- [ ] Code refactoring / 代码重构
- [ ] Other / 其他 (please describe / 请描述):
## Changes Made / 更改内容
<!-- Describe the specific changes made in this PR -->
<!-- 描述此 PR 中的具体更改 -->
-
-
-
-
-
-
## Implementation Details / 实现细节
<!-- Provide technical details about your implementation -->
<!-- 提供实现的技术细节 -->
**Key Changes / 主要更改:**
-
**Technical Notes / 技术说明:**
-
- **Technical Notes / 技术说明:**
-
## Testing / 测试
<!-- Describe the testing you performed -->
<!-- 描述您执行的测试 -->
### Testing Checklist / 测试清单
- [ ] I have tested this code locally / 我已在本地测试此代码
- [ ] All existing tests pass / 所有现有测试通过
- [ ] I have added tests for new functionality / 我已为新功能添加测试
@@ -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 / 截图/视频
<!-- If applicable, add screenshots or videos to demonstrate the changes -->
<!-- 如适用,请添加截图或视频来展示更改 -->
## Reviewer Checklist / 审阅者清单
<!-- For reviewers to verify before merging -->
<!-- 供审阅者在合并前验证 -->
- [ ] Code follows the project's style guidelines / 代码遵循项目的风格指南
- [ ] Changes are well-documented / 更改有良好的文档说明
- [ ] No breaking changes or clearly documented if present / 无破坏性更改,或已清楚记录
@@ -74,9 +79,7 @@ Closes #
- [ ] Performance impact has been evaluated / 已评估性能影响
- [ ] All discussions have been resolved / 所有讨论已解决
## Additional Notes / 补充说明
<!-- Any additional information that reviewers should know -->
<!-- 审阅者需要了解的其他信息 -->
+3
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
dist/
node_modules/
pnpm-lock.yaml
+11
View File
@@ -0,0 +1,11 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "es5",
"printWidth": 100,
"endOfLine": "lf",
"bracketSpacing": true,
"arrowParens": "avoid"
}
+9 -9
View File
@@ -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
+20 -23
View File
@@ -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_
> <details>
> <summary>已支持平台速查表(点击展开)</summary>
>
> | 字母 | 平台 |
> |:---:|:---|
> | A | 阿里云社区 |
> | B | B站专栏、百度云千帆、百家号、博客园 |
> | C | CSDN |
> | D | 豆瓣、电子发烧友、抖音文章 |
> | H | 华为开发者文章、华为云博客、火山引擎社区 |
> | I | InfoQ |
> | J | 简书、掘金、今日头条 |
> | K | 开源中国 |
> | M | Medium、ModelScope 魔搭社区 |
> | S | 少数派、搜狐号、思否 |
> | T | 腾讯云 |
> | W | 网易号、微博文章、微信公众号 |
> | X | 小红书长文、X(Formerly Twitter) Articles |
> | Z | 支付宝开放平台、知乎 |
> | 5 | 51CTO |
> | 字母 | 平台 |
> | :--: | :--------------------------------------- |
> | A | 阿里云社区 |
> | B | B站专栏、百度云千帆、百家号、博客园 |
> | C | CSDN |
> | D | 豆瓣、电子发烧友、抖音文章 |
> | H | 华为开发者文章、华为云博客、火山引擎社区 |
> | I | InfoQ |
> | J | 简书、掘金、今日头条 |
> | K | 开源中国 |
> | M | Medium、ModelScope 魔搭社区 |
> | S | 少数派、搜狐号、思否 |
> | T | 腾讯云 |
> | W | 网易号、微博文章、微信公众号 |
> | X | 小红书长文、X(Formerly Twitter) Articles |
> | Z | 支付宝开放平台、知乎 |
> | 5 | 51CTO |
>
> </details>
### 媒体平台
<p align="center">
@@ -82,8 +79,8 @@ _**C**reate **O**nce **S**ync **E**verywhere_
<img src="https://cdn.simpleicons.org/x" alt="X(Formerly Twitter) Articles" width="50" />
</p>
### 博客平台
<p align="center">
<img src="https://cdn.simpleicons.org/csdn/FC5531" alt="CSDN" width="80" />
<img src="https://favicon.im/cnblogs.com?larger=true" alt="博客园" width="60" />
@@ -97,6 +94,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_
</p>
### 云平台及其开发者社区
<br/>
<p align="center">
@@ -119,7 +117,6 @@ _**C**reate **O**nce **S**ync **E**verywhere_
<img src="https://www.elecfans.com/static/main/img/elecfans-logo.jpg" alt="电子发烧友" width="35" />
</p>
## 开发者模式测试
1. 克隆或下载本项目
+5 -15
View File
@@ -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": [
"<all_urls>"
]
"resources": ["bundles/inject.js", "bundles/platforms/*.js"],
"matches": ["<all_urls>"]
}
]
}
}
+1 -1
View File
@@ -27,4 +27,4 @@
"web-ext": "^10.4.0",
"ws": "^8.21.0"
}
}
}
+191 -189
View File
@@ -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<string, string>
default_title: string
}
background: FirefoxBackgroundOptions | ChromeBackgroundOptions
content_scripts: Array<{
matches: string[]
js: string[]
run_at: string
}>
icons: Record<string, string>
web_accessible_resources: Array<{
resources: string[]
matches: string[]
}>
browser_specific_settings?: {
gecko: {
id: string
}
manifest_version: number
name: string
version: string
description: string
permissions: string[]
host_permissions: string[]
action: {
default_icon: Record<string, string>
default_title: string
}
background: FirefoxBackgroundOptions | ChromeBackgroundOptions
content_scripts: Array<{
matches: string[]
js: string[]
run_at: string
}>
icons: Record<string, string>
web_accessible_resources: Array<{
resources: string[]
matches: string[]
}>
browser_specific_settings?: {
gecko: {
id: string
}
}
}
const readManifest = async (manifestPath: string): Promise<Manifest | undefined> => {
try {
const fileContent = await fs.readFile(manifestPath, 'utf8')
const json = JSON.parse(fileContent) as Manifest
return json
} catch (error) {
console.error(error)
return undefined
}
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 <target>', 'Browser target: "chromium", "firefox", or "safari"', { default: 'chromium' })
.option('--bundle-id <bundleId>', 'Bundle ID for Safari (default: org.doocs.cose)')
.action(async (options: BuildOptions) => {
const validTargets = ['chromium', 'firefox', 'safari']
if (!validTargets.includes(options.target)) {
throw new Error(`Invalid target: ${options.target}. Use "chromium", "firefox", or "safari".`)
}
.command('build', 'Build the COSE browser extension')
.option('-w, --watch', 'Watch mode', { default: false })
.option('-r, --release', 'Build in release mode with optimizations', { default: false })
.option('--target <target>', 'Browser target: "chromium", "firefox", or "safari"', {
default: 'chromium',
})
.option('--bundle-id <bundleId>', 'Bundle ID for Safari (default: org.doocs.cose)')
.action(async (options: BuildOptions) => {
const validTargets = ['chromium', 'firefox', 'safari']
if (!validTargets.includes(options.target)) {
throw new Error(`Invalid target: ${options.target}. Use "chromium", "firefox", or "safari".`)
}
console.log(`\n=== COSE Build (target: ${options.target}, release: ${options.release}) ===\n`)
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()
+17 -13
View File
@@ -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) {
+67 -57
View File
@@ -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)
})
File diff suppressed because it is too large Load Diff
+277 -252
View File
@@ -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,
},
'*'
)
}
})
})()
+307 -55
View File
@@ -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,
},
})
+7 -5
View File
@@ -1,7 +1,9 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head><title>COSE Offscreen</title></head>
<body>
<script src="bundles/offscreen.js" type="module"></script>
</body>
<head>
<title>COSE Offscreen</title>
</head>
<body>
<script src="bundles/offscreen.js" type="module"></script>
</body>
</html>
+11 -8
View File
@@ -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 }
}
}
+80 -74
View File
@@ -1,76 +1,82 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 320px;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #fff;
color: #333;
}
.container {
text-align: center;
}
h1 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
p {
font-size: 13px;
line-height: 1.6;
color: #333;
margin-bottom: 16px;
}
p a {
color: #FE5200;
text-decoration: none;
}
p a:hover {
text-decoration: underline;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #FE5200;
color: #fff;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
border: 1px solid #e64a00;
cursor: pointer;
}
.btn:hover {
background: #e64a00;
}
.divider {
margin: 16px 0;
border-top: 1px solid #e5e5e5;
}
.hint {
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>COSE - 多平台文章同步</h1>
<p>插件安装完成!</p>
<p>打开<a href="https://github.com/doocs/md" target="_blank">自托管的编辑器</a>即可自动检测到,无需多余配置。</p>
<div class="divider"></div>
<p class="hint">没有自托管编辑器</p>
<a href="https://md.doocs.org" target="_blank" class="btn" id="openOfficial">也可使用官网编辑器</a>
</div>
<script src="bundles/popup.js" type="module"></script>
</body>
<head>
<meta charset="UTF-8" />
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 320px;
padding: 20px;
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #fff;
color: #333;
}
.container {
text-align: center;
}
h1 {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
}
p {
font-size: 13px;
line-height: 1.6;
color: #333;
margin-bottom: 16px;
}
p a {
color: #fe5200;
text-decoration: none;
}
p a:hover {
text-decoration: underline;
}
.btn {
display: inline-block;
padding: 10px 20px;
background: #fe5200;
color: #fff;
text-decoration: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
transition: background 0.2s;
border: 1px solid #e64a00;
cursor: pointer;
}
.btn:hover {
background: #e64a00;
}
.divider {
margin: 16px 0;
border-top: 1px solid #e5e5e5;
}
.hint {
font-size: 12px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1>COSE - 多平台文章同步</h1>
<p>插件安装完成!</p>
<p>
打开<a href="https://github.com/doocs/md" target="_blank">自托管编辑器</a
>即可自动检测到,无需多余配置。
</p>
<div class="divider"></div>
<p class="hint">没有自托管编辑器?</p>
<a href="https://md.doocs.org" target="_blank" class="btn" id="openOfficial"
>也可使用官网编辑器</a
>
</div>
<script src="bundles/popup.js" type="module"></script>
</body>
</html>
+1 -1
View File
@@ -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()
+11 -13
View File
@@ -1,14 +1,12 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": [
"scripts/**/*.ts"
]
}
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["scripts/**/*.ts"]
}
+37 -37
View File
@@ -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: '.',
},
],
}),
],
],
}),
],
})
+13 -1
View File
@@ -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"
}
}
+2 -2
View File
@@ -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
+14 -14
View File
@@ -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:*"
}
}
"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:*"
}
}
+13 -10
View File
@@ -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()
+9 -8
View File
@@ -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] 阿里云开发者社区:内容填充完成')
}
+3 -2
View File
@@ -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"]')
+3 -2
View File
@@ -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 }
+57 -56
View File
@@ -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, '<br>')
console.log('[COSE] 51CTO contenteditable 填充成功')
}
// 可能是富文本模式的 contenteditable
const contentEditable = document.querySelector('[contenteditable="true"]')
if (contentEditable) {
contentEditable.innerHTML = contentToFill.replace(/\n/g, '<br>')
console.log('[COSE] 51CTO contenteditable 填充成功')
}
}
}
// 导出
+2 -3
View File
@@ -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 }
-2
View File
@@ -9,7 +9,5 @@ const HuaweiDevPlatform = {
type: 'huaweidev',
}
// 导出
export { HuaweiDevPlatform }
+69 -69
View File
@@ -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,
}
// 导出
+42 -40
View File
@@ -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 未找到编辑器')
}
// 导出
+16 -5
View File
@@ -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 {
-1
View File
@@ -87,4 +87,3 @@ async function syncJuejinContent(tab, content, helpers) {
// 导出
export { JuejinPlatform, fillJuejinContent, syncJuejinContent }
+36 -36
View File
@@ -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 内容填充成功')
}
}
// 导出
@@ -11,7 +11,5 @@ const ModelScopePlatform = {
type: 'modelscope',
}
// 导出
export { ModelScopePlatform }
+70 -63
View File
@@ -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')
}
}
// 导出
+13 -10
View File
@@ -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)) {
+9 -5
View File
@@ -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()
+99 -96
View File
@@ -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<boolean>} 是否成功进入 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<CodeMirror|null>}
*/
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 内容填充成功')
}
// 导出
+9 -6
View File
@@ -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()
+21 -12
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
const escapedCode = code.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
return `<code style="background-color: #f6f8fa; padding: 2px 6px; border-radius: 3px; font-family: 'SF Mono', Consolas, monospace; font-size: 0.9em;">${escapedCode}</code>`
}
@@ -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}</${tag}>\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>${p}</p>`).join('\n')
htmlContent = htmlContent
.split('\n\n')
.map(p => `<p>${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)
@@ -9,7 +9,5 @@ const VolcenginePlatform = {
type: 'volcengine',
}
// 导出
export { VolcenginePlatform }
+14 -7
View File
@@ -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)
+52 -65
View File
@@ -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 }
+69 -73
View File
@@ -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] 已触发 ClipboardEventdispatched:', 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
}
+4 -6
View File
@@ -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 {
+10 -10
View File
@@ -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"
}
}
"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"
}
}
+44 -41
View File
@@ -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,
}
+42 -42
View File
@@ -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' }
}
+85 -82
View File
@@ -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 }
}
}
+23 -18
View File
@@ -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 }
}
}
+26 -26
View File
@@ -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 }
}
}
+18 -18
View File
@@ -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 }
}
}
+50 -41
View File
@@ -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(/<img[^>]*src=["'](https:\/\/(?:profile|i-avatar)\.csdnimg\.cn\/[^"']+)["']/i) ||
blogHtml.match(/<img[^>]*class=["']avatar[^"']*["'][^>]*src=["']([^"']+)["']/i)
// Extract avatar
const avatarMatch =
blogHtml.match(
/<img[^>]*src=["'](https:\/\/(?:profile|i-avatar)\.csdnimg\.cn\/[^"']+)["']/i
) || blogHtml.match(/<img[^>]*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 }
}
}
+23 -23
View File
@@ -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 }
}
}
+182 -178
View File
@@ -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 =
/的账号</.test(html) ||
/https?:\/\/www\.douban\.com\/people\/([^/"?#]+)\/?/.test(html) ||
/\/people\/([^/"?#]+)\/?/.test(html) ||
/doubanio\.com\/icon\//i.test(html)
loginConfirmed = !redirectedToLogin && hasUserSignals
if (!username) {
const accountMatch = html.match(/>([^<\n]+)的账号</)
if (accountMatch?.[1]) {
username = accountMatch[1].trim()
}
}
if (!username || !uid) {
const profileLinkMatch = html.match(/https?:\/\/www\.douban\.com\/people\/([^/"?#]+)\/?/)
if (profileLinkMatch?.[1]) {
uid = profileLinkMatch[1]
}
}
if (!avatar) {
const avatarMatch =
html.match(/https?:\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) ||
html.match(/\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i) ||
html.match(/\/icon\/up\d+-\d+\.jpg/i)
if (avatarMatch?.[1]) {
avatar = avatarMatch[1]
} else if (avatarMatch?.[0]) {
avatar = avatarMatch[0]
}
if (avatar && avatar.startsWith('//')) {
avatar = `https:${avatar}`
} else if (avatar && avatar.startsWith('/icon/')) {
avatar = `https://img3.doubanio.com${avatar}`
}
}
if (username || avatar || uid) {
console.log('[COSE] douban 从 /mine/ HTML 获取用户信息:', username || uid)
}
}
} catch (e) {
console.log('[COSE] douban /mine/ 解析失败:', e.message)
}
// 3. Fallback: derive uid from dbcl2 cookie as username placeholder
if (loginConfirmed && !username && !uid && dbcl2Cookie.value) {
const uidFromCookie = dbcl2Cookie.value.match(/"?([^:"]+):/)
if (uidFromCookie?.[1]) {
uid = uidFromCookie[1]
}
}
if (!loginConfirmed) {
console.log('[COSE] douban 仅有 cookie,未确认登录态,按未登录处理')
return { loggedIn: false }
}
if (!username && uid) {
username = uid
}
// 5. If avatar still missing but uid exists, fetch profile page and extract avatar
if (!avatar && uid) {
try {
const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' })
const cookieHeader = doubanCookies.map(c => `${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 = /的账号</.test(html)
|| /https?:\/\/www\.douban\.com\/people\/([^/"?#]+)\/?/.test(html)
|| /\/people\/([^/"?#]+)\/?/.test(html)
|| /doubanio\.com\/icon\//i.test(html)
loginConfirmed = !redirectedToLogin && hasUserSignals
if (!username) {
const accountMatch = html.match(/>([^<\n]+)的账号</)
if (accountMatch?.[1]) {
username = accountMatch[1].trim()
}
}
if (!username || !uid) {
const profileLinkMatch = html.match(/https?:\/\/www\.douban\.com\/people\/([^/"?#]+)\/?/)
if (profileLinkMatch?.[1]) {
uid = profileLinkMatch[1]
}
}
if (!avatar) {
const avatarMatch = html.match(/https?:\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i)
|| html.match(/\/\/img\d\.doubanio\.com\/icon\/[^"'\s<]+/i)
|| html.match(/\/icon\/up\d+-\d+\.jpg/i)
if (avatarMatch?.[1]) {
avatar = avatarMatch[1]
} else if (avatarMatch?.[0]) {
avatar = avatarMatch[0]
}
if (avatar && avatar.startsWith('//')) {
avatar = `https:${avatar}`
} else if (avatar && avatar.startsWith('/icon/')) {
avatar = `https://img3.doubanio.com${avatar}`
}
}
if (username || avatar || uid) {
console.log('[COSE] douban 从 /mine/ HTML 获取用户信息:', username || uid)
}
}
} catch (e) {
console.log('[COSE] douban /mine/ 解析失败:', e.message)
}
// 3. Fallback: derive uid from dbcl2 cookie as username placeholder
if (loginConfirmed && !username && !uid && dbcl2Cookie.value) {
const uidFromCookie = dbcl2Cookie.value.match(/"?([^:"]+):/)
if (uidFromCookie?.[1]) {
uid = uidFromCookie[1]
}
}
if (!loginConfirmed) {
console.log('[COSE] douban 仅有 cookie,未确认登录态,按未登录处理')
return { loggedIn: false }
}
if (!username && uid) {
username = uid
}
// 5. If avatar still missing but uid exists, fetch profile page and extract avatar
if (!avatar && uid) {
try {
const doubanCookies = await chrome.cookies.getAll({ domain: '.douban.com' })
const cookieHeader = doubanCookies.map(c => `${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 }
}
}
+36 -34
View File
@@ -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 }
}
}
+35 -34
View File
@@ -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 }
}
}
+223 -198
View File
@@ -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 }
}
}
+29 -29
View File
@@ -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 }
}
}
+20 -20
View File
@@ -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 }
}
}
+39 -28
View File
@@ -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 }
}
}
+106 -95
View File
@@ -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 }
}
}
+62 -58
View File
@@ -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 }
}
}
+31 -28
View File
@@ -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 }
}
}
@@ -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(/<script\s+id="__NEXT_DATA__"[^>]*>([\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(/<script\s+id="__NEXT_DATA__"[^>]*>([\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 }
}
}
+24 -22
View File
@@ -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 }
}
}
+21 -16
View File
@@ -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 }
}
}
@@ -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[^"]*"[^>]*>([^<]+)</) ||
html.match(/"isCreator"\s*:\s*true[\s\S]*?"nickname"\s*:\s*"([^"]+)"/)
const creatorNicknameMatch =
html.match(
/class="creator-info[^"]*"[^>]*>[\s\S]*?<[^>]*class="[^"]*name[^"]*"[^>]*>([^<]+)</
) || html.match(/"isCreator"\s*:\s*true[\s\S]*?"nickname"\s*:\s*"([^"]+)"/)
const nicknameMatch = userInfoMatch || creatorNicknameMatch
const avatarMatch = html.match(/"userInfo"[\s\S]*?"avatarUrl"\s*:\s*"([^"]+)"/) ||
html.match(/"avatar"\s*:\s*"(https?:\/\/[^"]+)"/)
const nicknameMatch = userInfoMatch || creatorNicknameMatch
const avatarMatch =
html.match(/"userInfo"[\s\S]*?"avatarUrl"\s*:\s*"([^"]+)"/) ||
html.match(/"avatar"\s*:\s*"(https?:\/\/[^"]+)"/)
if (nicknameMatch && nicknameMatch[1]) {
let avatar = avatarMatch ? avatarMatch[1] : ''
if (avatar && avatar.includes('qcloudimg.com')) {
avatar = await convertAvatarToBase64(avatar, 'https://cloud.tencent.com/')
}
return { loggedIn: true, username: nicknameMatch[1], avatar }
} else {
if (html.includes('创作中心') || html.includes('我的文章')) return { loggedIn: true, username: '', avatar: '' }
return { loggedIn: false }
}
} catch (e) { return { loggedIn: false } }
if (nicknameMatch && nicknameMatch[1]) {
let avatar = avatarMatch ? avatarMatch[1] : ''
if (avatar && avatar.includes('qcloudimg.com')) {
avatar = await convertAvatarToBase64(avatar, 'https://cloud.tencent.com/')
}
return { loggedIn: true, username: nicknameMatch[1], avatar }
} else {
if (html.includes('创作中心') || html.includes('我的文章'))
return { loggedIn: true, username: '', avatar: '' }
return { loggedIn: false }
}
} catch (e) {
return { loggedIn: false }
}
}
+29 -27
View File
@@ -5,34 +5,36 @@
* 2. Fetch home page HTML and extract screen_name/avatar via regex
*/
export async function detectTwitterUser() {
try {
const authTokenCookie = await chrome.cookies.get({ url: 'https://x.com', name: 'auth_token' })
const ct0Cookie = await chrome.cookies.get({ url: 'https://x.com', name: 'ct0' })
if (!authTokenCookie) return { loggedIn: false }
let username = ''
let avatar = ''
try {
const authTokenCookie = await chrome.cookies.get({ url: 'https://x.com', name: 'auth_token' })
const ct0Cookie = await chrome.cookies.get({ url: 'https://x.com', name: 'ct0' })
const response = await fetch('https://x.com/home', {
method: 'GET',
credentials: 'include',
headers: { Accept: 'text/html' },
})
if (response.ok) {
const html = await response.text()
const screenNameMatch = html.match(/"screen_name"\s*:\s*"([^"]+)"/)
if (screenNameMatch) username = screenNameMatch[1]
const avatarMatch = html.match(/"profile_image_url_https"\s*:\s*"([^"]+)"/)
if (avatarMatch) avatar = avatarMatch[1].replace('_normal.', '_x96.')
}
} catch (e) {}
if (!authTokenCookie) return { loggedIn: false }
// If fetch failed or regex failed, try explicit fallback scraping logic (simplified here)
// Note: Full scrape logic from background.js is complex.
// We will assume basic fetch works or just return loggedIn:true if cookie exists
let username = ''
let avatar = ''
try {
const response = await fetch('https://x.com/home', {
method: 'GET',
credentials: 'include',
headers: { 'Accept': 'text/html' }
})
if (response.ok) {
const html = await response.text()
const screenNameMatch = html.match(/"screen_name"\s*:\s*"([^"]+)"/)
if (screenNameMatch) username = screenNameMatch[1]
const avatarMatch = html.match(/"profile_image_url_https"\s*:\s*"([^"]+)"/)
if (avatarMatch) avatar = avatarMatch[1].replace('_normal.', '_x96.')
}
} catch (e) { }
// If fetch failed or regex failed, try explicit fallback scraping logic (simplified here)
// Note: Full scrape logic from background.js is complex.
// We will assume basic fetch works or just return loggedIn:true if cookie exists
return { loggedIn: true, username, avatar }
} catch (e) { return { loggedIn: false } }
return { loggedIn: true, username, avatar }
} catch (e) {
return { loggedIn: false }
}
}
+31 -31
View File
@@ -8,41 +8,41 @@ import { convertAvatarToBase64 } from '../utils.js'
* fetch with credentials:'include' doesn't reliably send SameSite cookies.
*/
export async function detectVolcengineUser() {
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('; ')
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 }
}
}
+33 -33
View File
@@ -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 }
}
}
+91 -91
View File
@@ -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 }
}
}
+84 -84
View File
@@ -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 }
}
}
+97 -80
View File
@@ -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 }
}
}
+159 -151
View File
@@ -6,171 +6,179 @@
* @returns {Promise<string>} - 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 }
}
}
+196 -5
View File
@@ -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:
+1
View File
@@ -5,4 +5,5 @@ allowBuilds:
core-js: false
dtrace-provider: false
esbuild: true
simple-git-hooks: true
spawn-sync: false
+22 -39
View File
@@ -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(/<img\b/gi) || []).length
},
querySelectorAll(selector) {
if (selector === 'img')
return Array.from({ length: imageCount }, () => ({}))
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'))