| 1 | # 浏览器插件模块 |
| 2 | |
| 3 | ## 📁 文件结构 |
| 4 | |
| 5 | ``` |
| 6 | src/store/plugin/ |
| 7 | ├── index.ts # 统一导出 |
| 8 | ├── request.ts # 插件代理请求封装 |
| 9 | ├── store.ts # Zustand Store 状态管理 |
| 10 | ├── hooks.ts # 自定义 Hooks |
| 11 | ├── baseTypes.ts # TypeScript 类型定义 |
| 12 | ├── constants.ts # 常量定义 |
| 13 | ├── utils.ts # 工具函数 |
| 14 | ├── README.md # 本文档 |
| 15 | └── examples/ # 使用示例 |
| 16 | ├── basic.example.tsx |
| 17 | └── advanced.example.tsx |
| 18 | ``` |
| 19 | |
| 20 | ## 🚀 快速开始 |
| 21 | |
| 22 | ### 1. 基础使用 |
| 23 | |
| 24 | ```typescript |
| 25 | import { usePlugin } from '@/store/plugin'; |
| 26 | |
| 27 | export function MyComponent() { |
| 28 | // 自动轮询插件状态,每2秒检测一次 |
| 29 | const { isConnected, status } = usePlugin(true, 2000); |
| 30 | |
| 31 | return ( |
| 32 | <div> |
| 33 | 状态: {isConnected ? '已连接' : '未连接'} |
| 34 | </div> |
| 35 | ); |
| 36 | } |
| 37 | ``` |
| 38 | |
| 39 | ### 2. 登录功能 |
| 40 | |
| 41 | ```typescript |
| 42 | import { usePluginLogin } from '@/store/plugin'; |
| 43 | |
| 44 | export function LoginButton() { |
| 45 | const { login } = usePluginLogin(); |
| 46 | |
| 47 | const handleLogin = async () => { |
| 48 | const result = await login('douyin'); |
| 49 | if (result.success) { |
| 50 | console.log('登录成功:', result.data?.nickname); |
| 51 | } |
| 52 | }; |
| 53 | |
| 54 | return <button onClick={handleLogin}>登录抖音</button>; |
| 55 | } |
| 56 | ``` |
| 57 | |
| 58 | ### 3. 发布视频 |
| 59 | |
| 60 | ```typescript |
| 61 | import { usePluginPublish } from '@/store/plugin'; |
| 62 | |
| 63 | export function PublishVideo() { |
| 64 | const { publishVideo, isPublishing, publishProgress } = usePluginPublish(); |
| 65 | |
| 66 | const handlePublish = async (videoFile: File, coverFile: File) => { |
| 67 | const result = await publishVideo( |
| 68 | 'douyin', |
| 69 | videoFile, |
| 70 | coverFile, |
| 71 | { title: '我的视频', desc: '描述' }, |
| 72 | (progress) => console.log(`进度: ${progress.progress}%`) |
| 73 | ); |
| 74 | |
| 75 | if (result.success) { |
| 76 | alert('发布成功!'); |
| 77 | } |
| 78 | }; |
| 79 | |
| 80 | return ( |
| 81 | <div> |
| 82 | <button onClick={() => handlePublish(...)} disabled={isPublishing}> |
| 83 | {isPublishing ? '发布中...' : '发布视频'} |
| 84 | </button> |
| 85 | {publishProgress && <div>进度: {publishProgress.progress}%</div>} |
| 86 | </div> |
| 87 | ); |
| 88 | } |
| 89 | ``` |
| 90 | |
| 91 | ### 4. 完整工作流 |
| 92 | |
| 93 | ```typescript |
| 94 | import { usePluginWorkflow } from '@/store/plugin'; |
| 95 | |
| 96 | export function OneClickPublish() { |
| 97 | const { loginAndPublishVideo } = usePluginWorkflow(); |
| 98 | |
| 99 | const handleOneClick = async (videoFile: File, coverFile: File) => { |
| 100 | // 自动登录 + 发布 |
| 101 | const result = await loginAndPublishVideo( |
| 102 | 'douyin', |
| 103 | videoFile, |
| 104 | coverFile, |
| 105 | { title: '标题' } |
| 106 | ); |
| 107 | |
| 108 | if (result.success) { |
| 109 | alert('发布成功!'); |
| 110 | } |
| 111 | }; |
| 112 | |
| 113 | return <button onClick={() => handleOneClick(...)}>一键发布</button>; |
| 114 | } |
| 115 | ``` |
| 116 | |
| 117 | ### 5. 代理请求 |
| 118 | |
| 119 | ```typescript |
| 120 | import { proxyRequest } from '@/store/plugin' |
| 121 | |
| 122 | const response = await proxyRequest({ |
| 123 | url: 'https://httpbin.org/anything', |
| 124 | headers: { |
| 125 | accept: 'application/json', |
| 126 | }, |
| 127 | body: { |
| 128 | source: 'aitoearn-website', |
| 129 | }, |
| 130 | }) |
| 131 | |
| 132 | console.log(response.status, response.body) |
| 133 | ``` |
| 134 | |
| 135 | ## 📚 API 文档 |
| 136 | |
| 137 | ### Hooks |
| 138 | |
| 139 | #### `usePlugin(autoPolling?, pollingInterval?)` |
| 140 | |
| 141 | 基础插件状态和方法 |
| 142 | |
| 143 | **参数:** |
| 144 | |
| 145 | - `autoPolling` - 是否自动轮询,默认 `true` |
| 146 | - `pollingInterval` - 轮询间隔(毫秒),默认 `2000` |
| 147 | |
| 148 | **返回:** |
| 149 | |
| 150 | ```typescript |
| 151 | { |
| 152 | status: PluginStatus; |
| 153 | isConnected: boolean; |
| 154 | isNotInstalled: boolean; |
| 155 | isChecking: boolean; |
| 156 | isPublishing: boolean; |
| 157 | publishProgress: ProgressEvent | null; |
| 158 | checkPlugin: () => boolean; |
| 159 | startPolling: (interval?: number) => void; |
| 160 | stopPolling: () => void; |
| 161 | login: (platform: PlatformType) => Promise<PlatAccountInfo>; |
| 162 | publish: (params: PublishParams, onProgress?) => Promise<PublishResult>; |
| 163 | resetPublishState: () => void; |
| 164 | } |
| 165 | ``` |
| 166 | |
| 167 | #### `usePluginLogin()` |
| 168 | |
| 169 | 登录功能 |
| 170 | |
| 171 | **返回:** |
| 172 | |
| 173 | ```typescript |
| 174 | { |
| 175 | login: (platform: PlatformType) => Promise<OperationResult<PlatAccountInfo>> |
| 176 | } |
| 177 | ``` |
| 178 | |
| 179 | #### `usePluginPublish()` |
| 180 | |
| 181 | 发布功能 |
| 182 | |
| 183 | **返回:** |
| 184 | |
| 185 | ```typescript |
| 186 | { |
| 187 | publish: (params: PublishParams, onProgress?) => Promise<OperationResult>; |
| 188 | publishVideo: (platform, video, cover, options?, onProgress?) => Promise<OperationResult>; |
| 189 | publishImages: (platform, images, options?, onProgress?) => Promise<OperationResult>; |
| 190 | isPublishing: boolean; |
| 191 | publishProgress: ProgressEvent | null; |
| 192 | resetPublishState: () => void; |
| 193 | } |
| 194 | ``` |
| 195 | |
| 196 | #### `usePluginWorkflow()` |
| 197 | |
| 198 | 完整工作流(登录+发布) |
| 199 | |
| 200 | **返回:** |
| 201 | |
| 202 | ```typescript |
| 203 | { |
| 204 | isConnected: boolean; |
| 205 | loginAndPublishVideo: (...) => Promise<OperationResult>; |
| 206 | loginAndPublishImages: (...) => Promise<OperationResult>; |
| 207 | } |
| 208 | ``` |
| 209 | |
| 210 | ### 工具函数 |
| 211 | |
| 212 | ```typescript |
| 213 | // 状态文本 |
| 214 | getPluginStatusText(status: PluginStatus): string; |
| 215 | getPublishStageText(stage: ProgressEvent['stage']): string; |
| 216 | |
| 217 | // 状态判断 |
| 218 | isPluginConnected(status: PluginStatus): boolean; |
| 219 | isPluginNotInstalled(status: PluginStatus): boolean; |
| 220 | |
| 221 | // 格式化 |
| 222 | formatProgress(progress: number): string; |
| 223 | formatFileSize(bytes: number): string; |
| 224 | |
| 225 | // 文件验证 |
| 226 | validateFileType(file: File, acceptTypes: string[]): boolean; |
| 227 | isValidVideoFile(file: File): boolean; |
| 228 | isValidImageFile(file: File): boolean; |
| 229 | validateFileSize(file: File, maxSize: number): boolean; |
| 230 | |
| 231 | // 重试机制 |
| 232 | withRetry<T>(fn: () => Promise<T>, maxRetries?, delay?): () => Promise<T>; |
| 233 | ``` |
| 234 | |
| 235 | ### 类型定义 |
| 236 | |
| 237 | ```typescript |
| 238 | // 插件状态 |
| 239 | enum PluginStatus { |
| 240 | UNKNOWN = 'UNKNOWN', |
| 241 | CHECKING = 'CHECKING', |
| 242 | CONNECTED = 'CONNECTED', |
| 243 | NOT_INSTALLED = 'NOT_INSTALLED', |
| 244 | } |
| 245 | |
| 246 | // 平台类型 |
| 247 | type PlatformType = 'douyin' | 'xhs' | 'kwai' | 'bilibili' |
| 248 | |
| 249 | // 发布参数 |
| 250 | interface PublishParams { |
| 251 | platform: PlatformType |
| 252 | type: 'video' | 'image' |
| 253 | title?: string |
| 254 | desc?: string |
| 255 | video?: File | string |
| 256 | cover?: File | string |
| 257 | images?: (File | string)[] |
| 258 | topics?: string[] |
| 259 | visibility?: 'public' | 'private' | 'friends' |
| 260 | // ...更多字段 |
| 261 | } |
| 262 | |
| 263 | // 操作结果 |
| 264 | interface OperationResult<T = any> { |
| 265 | success: boolean |
| 266 | data?: T |
| 267 | error?: string |
| 268 | } |
| 269 | ``` |
| 270 | |
| 271 | ### 常量 |
| 272 | |
| 273 | ```typescript |
| 274 | // 默认轮询间隔(2秒) |
| 275 | DEFAULT_POLLING_INTERVAL = 2000 |
| 276 | |
| 277 | // 插件状态文本 |
| 278 | PLUGIN_STATUS_TEXT = { |
| 279 | UNKNOWN: '未检测', |
| 280 | CHECKING: '检测中...', |
| 281 | CONNECTED: '已连接', |
| 282 | NOT_INSTALLED: '未安装', |
| 283 | } |
| 284 | |
| 285 | // 发布阶段文本 |
| 286 | PUBLISH_STAGE_TEXT = { |
| 287 | download: '下载资源', |
| 288 | upload: '上传文件', |
| 289 | publish: '发布中', |
| 290 | complete: '完成', |
| 291 | error: '错误', |
| 292 | } |
| 293 | |
| 294 | // 错误消息 |
| 295 | ERROR_MESSAGES = { |
| 296 | PLUGIN_NOT_INSTALLED: '请先安装 Aitoearn 浏览器插件', |
| 297 | PUBLISHING_IN_PROGRESS: '当前正在发布中,请稍后再试', |
| 298 | LOGIN_FAILED: '登录失败', |
| 299 | PUBLISH_FAILED: '发布失败', |
| 300 | } |
| 301 | ``` |
| 302 | |
| 303 | ## 💡 使用示例 |
| 304 | |
| 305 | ### 示例 1: 插件状态显示 |
| 306 | |
| 307 | ```typescript |
| 308 | import { usePlugin, getPluginStatusText } from '@/store/plugin'; |
| 309 | |
| 310 | export function PluginStatus() { |
| 311 | const { status } = usePlugin(); |
| 312 | |
| 313 | return ( |
| 314 | <div> |
| 315 | 插件状态: {getPluginStatusText(status)} |
| 316 | </div> |
| 317 | ); |
| 318 | } |
| 319 | ``` |
| 320 | |
| 321 | ### 示例 2: 文件验证 |
| 322 | |
| 323 | ```typescript |
| 324 | import { isValidVideoFile, validateFileSize } from '@/store/plugin' |
| 325 | |
| 326 | function handleFileSelect(file: File) { |
| 327 | if (!isValidVideoFile(file)) { |
| 328 | alert('请选择有效的视频文件') |
| 329 | return |
| 330 | } |
| 331 | |
| 332 | if (!validateFileSize(file, 500 * 1024 * 1024)) { |
| 333 | alert('视频文件不能超过 500MB') |
| 334 | return |
| 335 | } |
| 336 | |
| 337 | // 处理文件... |
| 338 | } |
| 339 | ``` |
| 340 | |
| 341 | ### 示例 3: 带重试的发布 |
| 342 | |
| 343 | ```typescript |
| 344 | import { usePluginPublish, withRetry } from '@/store/plugin'; |
| 345 | |
| 346 | export function PublishWithRetry() { |
| 347 | const { publish } = usePluginPublish(); |
| 348 | |
| 349 | const handlePublish = async (params: PublishParams) => { |
| 350 | // 最多重试 3 次,每次延迟 2 秒 |
| 351 | const publishWithRetry = withRetry( |
| 352 | () => publish(params), |
| 353 | 3, |
| 354 | 2000 |
| 355 | ); |
| 356 | |
| 357 | try { |
| 358 | const result = await publishWithRetry(); |
| 359 | console.log('发布成功:', result); |
| 360 | } catch (error) { |
| 361 | console.error('重试后仍然失败:', error); |
| 362 | } |
| 363 | }; |
| 364 | |
| 365 | return <button onClick={() => handlePublish(...)}>发布</button>; |
| 366 | } |
| 367 | ``` |
| 368 | |
| 369 | ### 示例 4: 进度展示 |
| 370 | |
| 371 | ```typescript |
| 372 | import { usePluginPublish, formatProgress, getPublishStageText } from '@/store/plugin'; |
| 373 | import { Progress } from 'antd'; |
| 374 | |
| 375 | export function PublishWithProgress() { |
| 376 | const { publishVideo, publishProgress } = usePluginPublish(); |
| 377 | |
| 378 | return ( |
| 379 | <div> |
| 380 | {publishProgress && ( |
| 381 | <> |
| 382 | <Progress percent={publishProgress.progress} /> |
| 383 | <p> |
| 384 | {getPublishStageText(publishProgress.stage)}: {formatProgress(publishProgress.progress)} |
| 385 | </p> |
| 386 | </> |
| 387 | )} |
| 388 | </div> |
| 389 | ); |
| 390 | } |
| 391 | ``` |
| 392 | |
| 393 | ## 📝 注意事项 |
| 394 | |
| 395 | 1. **轮询管理** |
| 396 | - 使用 `usePlugin` 时会自动管理轮询 |
| 397 | - 组件卸载时会自动清理定时器 |
| 398 | - 无需手动调用 `stopPolling()` |
| 399 | |
| 400 | 2. **错误处理** |
| 401 | - Hooks 返回的方法都使用 `OperationResult` 格式 |
| 402 | - 包含 `success`、`data` 和 `error` 字段 |
| 403 | - 建议使用这种方式而不是 try-catch |
| 404 | |
| 405 | 3. **文件大小限制** |
| 406 | - 视频: 建议 ≤ 500MB |
| 407 | - 图片: 建议每张 ≤ 10MB |
| 408 | - 图文: 最多 9 张 |
| 409 | |
| 410 | 4. **并发限制** |
| 411 | - 同一时间只能有一个发布任务 |
| 412 | - `isPublishing` 为 `true` 时无法开始新发布 |
| 413 | |
| 414 | 5. **平台差异** |
| 415 | - 不同平台支持的功能可能不同 |
| 416 | - 参考各平台的具体文档 |
| 417 | |
| 418 | ## 🔗 相关链接 |
| 419 | |
| 420 | - [Web API 文档](../../../demo/docs/WEB_API.md) |
| 421 | - [类型定义](../../../demo/PublishType/) |
| 422 | - [示例代码](./examples/) |
| 423 | |
| 424 | ## 📞 技术支持 |
| 425 | |
| 426 | 如有问题或建议,请联系技术支持团队。 |
| 427 | |
| 428 | --- |
| 429 | |
| 430 | **版本**: 1.0.0 |
| 431 | **最后更新**: 2025-12-01 |
| 432 |