mirror of
https://github.com/doocs/cose.git
synced 2026-08-28 23:02:10 +08:00
chore: add Prettier formatting config and pre-commit hooks (#253)
This commit is contained in:
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
<!-- 审阅者需要了解的其他信息 -->
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dist/
|
||||
node_modules/
|
||||
pnpm-lock.yaml
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 100,
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
+9
-9
@@ -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
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ _**C**reate **O**nce **S**ync **E**verywhere_
|
||||
|
||||
## 使用方法
|
||||
|
||||
> 点击观看视频:[](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [](https://www.youtube.com/watch?v=KTskiA8Xaj4)
|
||||
> 点击观看视频:[](https://www.bilibili.com/video/BV1ZxqnB1E2C/) [](https://www.youtube.com/watch?v=KTskiA8Xaj4)
|
||||
|
||||
1. 先点击安装扩展 [](https://chromewebstore.google.com/detail/ilhikcdphhpjofhlnbojifbihhfmmhfk) 然后打开 [md.doocs.org](https://md.doocs.org) 或本地开发环境
|
||||
2. 编辑 Markdown 内容
|
||||
@@ -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. 克隆或下载本项目
|
||||
|
||||
@@ -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>"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@
|
||||
"web-ext": "^10.4.0",
|
||||
"ws": "^8.21.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+191
-189
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
+421
-206
File diff suppressed because it is too large
Load Diff
+277
-252
@@ -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
@@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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] 阿里云开发者社区:内容填充完成')
|
||||
}
|
||||
|
||||
|
||||
@@ -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"]')
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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 填充成功')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -9,7 +9,5 @@ const HuaweiDevPlatform = {
|
||||
type: 'huaweidev',
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 导出
|
||||
export { HuaweiDevPlatform }
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -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 未找到编辑器')
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -87,4 +87,3 @@ async function syncJuejinContent(tab, content, helpers) {
|
||||
|
||||
// 导出
|
||||
export { JuejinPlatform, fillJuejinContent, syncJuejinContent }
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 内容填充成功')
|
||||
}
|
||||
|
||||
// 导出
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -11,8 +11,6 @@ const TwitterPlatform = {
|
||||
type: 'twitter',
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 自定义 Markdown 渲染器
|
||||
* 将 Markdown 转换为 Twitter Articles 支持的 HTML 格式
|
||||
@@ -64,10 +62,7 @@ function createTwitterRenderer(marked) {
|
||||
|
||||
// 行内代码
|
||||
renderer.codespan = function (code) {
|
||||
const escapedCode = code
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
const escapedCode = code.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
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 }
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -17,7 +17,7 @@ import { injectUtils } from './common.js'
|
||||
function fillZhihuContent(title, markdown) {
|
||||
// 等待满足条件的元素出现(使用 MutationObserver)
|
||||
function waitForElement(predicate, timeout = 10000) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const el = predicate()
|
||||
if (el) return resolve(el)
|
||||
|
||||
@@ -57,7 +57,7 @@ function fillZhihuContent(title, markdown) {
|
||||
async function fillContent() {
|
||||
// 第一步:等待知乎编辑器完全加载(避免"草稿加载中"提示)
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
|
||||
// 第二步:填充标题
|
||||
async function fillTitle() {
|
||||
const titleInput = await window.waitFor('textarea[placeholder*="标题"]')
|
||||
@@ -65,7 +65,8 @@ function fillZhihuContent(title, markdown) {
|
||||
titleInput.focus()
|
||||
// 使用 nativeInputValueSetter 确保 React 识别变更
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype, 'value'
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
'value'
|
||||
)?.set
|
||||
if (nativeSetter) {
|
||||
nativeSetter.call(titleInput, title)
|
||||
@@ -77,10 +78,10 @@ function fillZhihuContent(title, markdown) {
|
||||
console.log('[COSE] 知乎标题填充成功')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 先填充标题
|
||||
await fillTitle()
|
||||
|
||||
|
||||
// 再等待一下确保标题已保存
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
@@ -88,15 +89,15 @@ function fillZhihuContent(title, markdown) {
|
||||
const editorSelectors = [
|
||||
'.public-DraftEditor-content',
|
||||
'[contenteditable="true"]',
|
||||
'.DraftEditor-root'
|
||||
'.DraftEditor-root',
|
||||
]
|
||||
|
||||
|
||||
let editor = null
|
||||
for (const selector of editorSelectors) {
|
||||
editor = document.querySelector(selector)
|
||||
if (editor) break
|
||||
}
|
||||
|
||||
|
||||
if (!editor) {
|
||||
console.log('[COSE] 未找到知乎编辑器')
|
||||
return { success: false, error: 'Editor not found' }
|
||||
@@ -106,7 +107,7 @@ function fillZhihuContent(title, markdown) {
|
||||
const rect = editor.getBoundingClientRect()
|
||||
const centerX = rect.left + rect.width / 2
|
||||
const centerY = rect.top + rect.height / 2
|
||||
|
||||
|
||||
// 触发鼠标事件序列激活编辑器
|
||||
for (const eventType of ['mousedown', 'mouseup', 'click']) {
|
||||
const event = new MouseEvent(eventType, {
|
||||
@@ -115,25 +116,25 @@ function fillZhihuContent(title, markdown) {
|
||||
view: window,
|
||||
clientX: centerX,
|
||||
clientY: centerY,
|
||||
button: 0
|
||||
button: 0,
|
||||
})
|
||||
editor.dispatchEvent(event)
|
||||
}
|
||||
|
||||
|
||||
// 聚焦编辑器
|
||||
editor.focus()
|
||||
|
||||
|
||||
// 清空现有内容
|
||||
document.execCommand('selectAll', false)
|
||||
document.execCommand('delete', false)
|
||||
|
||||
|
||||
// 等待编辑器状态更新
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
|
||||
// 第三步:通过剪贴板 + 键盘事件模拟真实粘贴
|
||||
// 这是触发知乎 Markdown 检测弹窗的关键方法
|
||||
const contentToFill = markdown || ''
|
||||
|
||||
|
||||
if (!contentToFill) {
|
||||
console.log('[COSE] 没有 Markdown 内容需要填充')
|
||||
await fillTitle()
|
||||
@@ -143,44 +144,38 @@ function fillZhihuContent(title, markdown) {
|
||||
try {
|
||||
// 使用 ClipboardEvent 模拟粘贴 - 这是触发 Markdown 检测弹窗的关键
|
||||
// execCommand('insertText') 不会触发弹窗
|
||||
|
||||
|
||||
// 检查浏览器兼容性
|
||||
if (typeof DataTransfer === 'undefined' || typeof ClipboardEvent === 'undefined') {
|
||||
throw new Error('浏览器不支持 DataTransfer 或 ClipboardEvent')
|
||||
}
|
||||
|
||||
|
||||
const dt = new DataTransfer()
|
||||
dt.setData('text/plain', contentToFill)
|
||||
|
||||
|
||||
const pasteEvent = new ClipboardEvent('paste', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clipboardData: dt
|
||||
clipboardData: dt,
|
||||
})
|
||||
|
||||
|
||||
editor.focus()
|
||||
const dispatched = editor.dispatchEvent(pasteEvent)
|
||||
console.log('[COSE] 已触发 ClipboardEvent,dispatched:', dispatched)
|
||||
|
||||
|
||||
// 等待 Markdown 检测弹窗出现并点击"确认并解析"
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const parseClicked = await waitAndClickButton(
|
||||
text => text.includes('确认并解析'),
|
||||
5000
|
||||
)
|
||||
|
||||
|
||||
const parseClicked = await waitAndClickButton(text => text.includes('确认并解析'), 5000)
|
||||
|
||||
if (parseClicked) {
|
||||
console.log('[COSE] 已点击"确认并解析"')
|
||||
|
||||
|
||||
// 等待解析完成并点击"确认"
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
const confirmClicked = await waitAndClickButton(
|
||||
text => text === '确认',
|
||||
5000
|
||||
)
|
||||
|
||||
|
||||
const confirmClicked = await waitAndClickButton(text => text === '确认', 5000)
|
||||
|
||||
if (confirmClicked) {
|
||||
console.log('[COSE] 已点击"确认",Markdown 解析完成')
|
||||
}
|
||||
@@ -239,11 +234,11 @@ async function syncZhihuContent(tab, content, helpers) {
|
||||
if (fillResult?.success) {
|
||||
// 等待 2 秒确保内容已保存
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
|
||||
// 等待图片上传完成后再刷新
|
||||
console.log('[COSE] 开始监听图片上传请求...')
|
||||
const uploadComplete = await waitForImageUploadComplete(tab.id)
|
||||
|
||||
|
||||
if (uploadComplete) {
|
||||
console.log('[COSE] 图片上传完成,准备刷新页面')
|
||||
try {
|
||||
@@ -259,7 +254,7 @@ async function syncZhihuContent(tab, content, helpers) {
|
||||
} else {
|
||||
console.log('[COSE] 未检测到图片上传请求或超时,跳过刷新')
|
||||
}
|
||||
|
||||
|
||||
return { success: true, message: '已打开知乎并同步内容', tabId: tab.id }
|
||||
} else {
|
||||
return { success: false, message: fillResult?.error || '内容同步失败', tabId: tab.id }
|
||||
@@ -274,40 +269,40 @@ async function syncZhihuContent(tab, content, helpers) {
|
||||
*/
|
||||
async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
const startTime = Date.now()
|
||||
|
||||
|
||||
// 在页面中注入监听脚本
|
||||
const result = await globalThis.chrome.scripting.executeScript({
|
||||
target: { tabId: tabId },
|
||||
func: () => {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(resolve => {
|
||||
const pendingUploads = new Map() // uploadId -> { url, completed }
|
||||
let hasUploadRequests = false
|
||||
let lastUploadTime = 0
|
||||
|
||||
|
||||
// 检查是否所有上传都完成
|
||||
const checkAllComplete = () => {
|
||||
if (pendingUploads.size === 0) return false
|
||||
|
||||
|
||||
for (const [id, info] of pendingUploads) {
|
||||
if (!info.completed) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// 监听 fetch 请求
|
||||
const originalFetch = window.fetch
|
||||
window.fetch = function(...args) {
|
||||
window.fetch = function (...args) {
|
||||
const url = args[0]
|
||||
const options = args[1] || {}
|
||||
|
||||
|
||||
// 检测图片上传请求(知乎的图片上传通常包含这些特征)
|
||||
const isImageUpload = typeof url === 'string' && (
|
||||
url.includes('/api/v4/images') ||
|
||||
url.includes('/api/v4/upload') ||
|
||||
url.includes('upload') ||
|
||||
(options.method === 'POST' && url.includes('zhihu.com'))
|
||||
)
|
||||
|
||||
const isImageUpload =
|
||||
typeof url === 'string' &&
|
||||
(url.includes('/api/v4/images') ||
|
||||
url.includes('/api/v4/upload') ||
|
||||
url.includes('upload') ||
|
||||
(options.method === 'POST' && url.includes('zhihu.com')))
|
||||
|
||||
if (isImageUpload) {
|
||||
hasUploadRequests = true
|
||||
lastUploadTime = Date.now()
|
||||
@@ -315,8 +310,9 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
pendingUploads.set(uploadId, { url, completed: false })
|
||||
console.log('[COSE] 检测到图片上传请求:', url, uploadId)
|
||||
}
|
||||
|
||||
return originalFetch.apply(this, args)
|
||||
|
||||
return originalFetch
|
||||
.apply(this, args)
|
||||
.then(response => {
|
||||
if (isImageUpload) {
|
||||
console.log('[COSE] 图片上传请求完成:', url, response.status)
|
||||
@@ -344,32 +340,32 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
throw error
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// 监听 XMLHttpRequest
|
||||
const originalOpen = XMLHttpRequest.prototype.open
|
||||
const originalSend = XMLHttpRequest.prototype.send
|
||||
|
||||
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
||||
|
||||
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
||||
this._url = url
|
||||
this._method = method
|
||||
return originalOpen.apply(this, [method, url, ...rest])
|
||||
}
|
||||
|
||||
XMLHttpRequest.prototype.send = function(...args) {
|
||||
const isImageUpload = this._url && (
|
||||
this._url.includes('/api/v4/images') ||
|
||||
this._url.includes('/api/v4/upload') ||
|
||||
this._url.includes('upload') ||
|
||||
(this._method === 'POST' && this._url.includes('zhihu.com'))
|
||||
)
|
||||
|
||||
|
||||
XMLHttpRequest.prototype.send = function (...args) {
|
||||
const isImageUpload =
|
||||
this._url &&
|
||||
(this._url.includes('/api/v4/images') ||
|
||||
this._url.includes('/api/v4/upload') ||
|
||||
this._url.includes('upload') ||
|
||||
(this._method === 'POST' && this._url.includes('zhihu.com')))
|
||||
|
||||
if (isImageUpload) {
|
||||
hasUploadRequests = true
|
||||
lastUploadTime = Date.now()
|
||||
const uploadId = Date.now() + Math.random()
|
||||
pendingUploads.set(uploadId, { url: this._url, completed: false })
|
||||
console.log('[COSE] 检测到图片上传 XHR:', this._url, uploadId)
|
||||
|
||||
|
||||
this.addEventListener('loadend', () => {
|
||||
console.log('[COSE] 图片上传 XHR 完成:', this._url, this.status)
|
||||
// 标记为已完成
|
||||
@@ -381,10 +377,10 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return originalSend.apply(this, args)
|
||||
}
|
||||
|
||||
|
||||
// 定期检查是否所有上传都完成
|
||||
const checkTimer = setInterval(() => {
|
||||
// 如果没有检测到任何上传请求,说明可能没有图片需要上传
|
||||
@@ -394,7 +390,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果所有上传都完成,并且距离最后一个上传请求已经过去2秒(确保没有新请求)
|
||||
if (checkAllComplete() && Date.now() - lastUploadTime > 2000) {
|
||||
console.log('[COSE] 所有图片上传请求已完成')
|
||||
@@ -403,7 +399,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
return
|
||||
}
|
||||
}, 500)
|
||||
|
||||
|
||||
// 10秒后如果没有检测到上传请求,认为没有图片需要上传
|
||||
setTimeout(() => {
|
||||
if (!hasUploadRequests) {
|
||||
@@ -412,7 +408,7 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
resolve(true)
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
|
||||
// 超时后无论如何都返回
|
||||
setTimeout(() => {
|
||||
console.log('[COSE] 等待图片上传超时')
|
||||
@@ -423,14 +419,14 @@ async function waitForImageUploadComplete(tabId, timeout = 30000) {
|
||||
},
|
||||
world: 'MAIN',
|
||||
})
|
||||
|
||||
|
||||
// 等待监听结果
|
||||
const uploadResult = result?.[0]?.result
|
||||
console.log('[COSE] 图片上传监听结果:', uploadResult)
|
||||
|
||||
|
||||
// 给一个额外的缓冲时间,确保图片已经完全加载和渲染
|
||||
await new Promise(resolve => setTimeout(resolve, 2000))
|
||||
|
||||
|
||||
return uploadResult !== false
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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' }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+196
-5
@@ -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:
|
||||
|
||||
@@ -5,4 +5,5 @@ allowBuilds:
|
||||
core-js: false
|
||||
dtrace-provider: false
|
||||
esbuild: true
|
||||
simple-git-hooks: true
|
||||
spawn-sync: false
|
||||
|
||||
+22
-39
@@ -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'))
|
||||
|
||||
Reference in New Issue
Block a user