@skrillex1224/chrome-media-publish-extension 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,490 @@
1
+ # chrome-media-publish-extension
2
+
3
+ `@skrillex1224/chrome-media-publish-extension` 是 Chrome extension 场景下的媒体发布业务内核。它不是正式插件 UI,而是给第三方 Chrome 插件使用的 npm 包。
4
+
5
+ 当前版本提供干净的包框架、公开 API、协议校验、状态码、adapter 目录、本地测试夹具,以及 Douyin 图文/视频、Bilibili 图文/视频 adapter。
6
+
7
+ 旧工程只作为平台行为和历史 E2E 经验参考,不作为代码复用来源:
8
+
9
+ ```text
10
+ /Users/galedekarios/Documents/Codex/2026-07-01/wechatsync-wechatsync-https-github-com-wechatsync/work/Wechatsync/article-publish-extension
11
+ ```
12
+
13
+ ## 安装
14
+
15
+ ```bash
16
+ npm install @skrillex1224/chrome-media-publish-extension @skrillex1224/extension-toolkit
17
+ ```
18
+
19
+ Chrome extension 不能直接在浏览器里裸 import npm 包,宿主插件需要用 Vite、esbuild、Webpack、Rollup 等 bundler 将业务代码打包进 extension。
20
+
21
+ `@skrillex1224/extension-toolkit` 是 peer dependency。发布本包前,需要先发布对应版本的 toolkit;本仓库本地开发使用 `file:../extension-toolkit` 作为 dev dependency。
22
+
23
+ ## 测试插件 UI
24
+
25
+ 本仓库自带一个 debug-first 的 Chrome 测试插件,用来真实验证 npm 包和 adapter。它不是正式产品插件,也不会进入 npm 发布包。
26
+
27
+ 目录:
28
+
29
+ ```text
30
+ tests/extension/app # React/Vite 测试 UI 源码
31
+ tests/extension/dist # npm run test:pack 后的可安装插件
32
+ ```
33
+
34
+ 打包:
35
+
36
+ ```bash
37
+ npm run test:pack
38
+ ```
39
+
40
+ 安装:
41
+
42
+ 1. 打开 Chrome Extensions。
43
+ 2. 选择 Load unpacked。
44
+ 3. 选择 `tests/extension/dist`。
45
+
46
+ 入口:
47
+
48
+ - 点击插件图标会打开 Chrome side panel。
49
+ - side panel 用于选择平台、检测登录态、选择媒体并创建单平台发布任务。
50
+ - `app/index.html` 是完整 debug console,包含媒体库、新建媒体、批量导入、发布任务详情和运行日志。
51
+ - `e2e.html` 保留 `window.__mediaPublishE2E` 兼容入口,便于 CDP/控制台直接调用。
52
+
53
+ 测试 UI 的核心规则:
54
+
55
+ - 平台单选,未登录平台不能发布。
56
+ - 同一平台同一时间只允许一个发布任务。
57
+ - 发布前会再次 `checkAuth`,避免账号状态和 UI 缓存不一致。
58
+ - 每个任务项按 `checkAuth -> publish -> getStatus` 执行。
59
+ - 成功、审核中、公开链接同步都算任务项完成,不会卡在“发布中”。
60
+ - 只有 `state === 50000` 的失败可重试项允许重试。
61
+ - 每次 API 调用都会截取 `Logger.exportLogs()` 增量并写入任务项,任务详情页可导出 JSON 排障。
62
+
63
+ 发布边界检查:
64
+
65
+ ```bash
66
+ npm pack --dry-run
67
+ ```
68
+
69
+ 预期 tarball 只包含 `README.md`、`dist/index.js`、`dist/index.js.map`、`index.d.ts`、`package.json`。测试 UI、React、Ant Design 和打包产物都不应出现在 npm tarball 中。
70
+
71
+ ## Runtime 原则
72
+
73
+ 详细决策记录见:
74
+
75
+ ```text
76
+ ../docs/superpowers/specs/2026-07-16-media-publish-runtime-decisions.md
77
+ ```
78
+
79
+ 核心原则:
80
+
81
+ - 不点击平台发布 UI。
82
+ - 能通过公开前端 API 完成的,就用 API。
83
+ - 能不开 tab 的,不开 tab。
84
+ - 必须使用页面 runtime 时,才通过 `Runtime.tabs.createManagedTab()` 打开受管 tab。
85
+ - 所有受管 tab 必须进入 tab group,用完关闭。
86
+ - 验证码、二次验证、扫码确认、账号风控不能绕过,统一映射为 `SecurityCheckRequired`。
87
+
88
+ 当前没有为了 Douyin/Bilibili 而必须补到 toolkit 的能力缺口。`withDouyinHeaderRules()`、`withBilibiliHeaderRules()`、`withCreatorTab()` 和 `withBilibiliPage()` 暂时保留在平台目录内;它们是业务包内部组合 helper,不是新的 toolkit primitive。
89
+
90
+ ## 快速接入
91
+
92
+ ```js
93
+ import { useMediaPublishExtension } from '@skrillex1224/chrome-media-publish-extension';
94
+
95
+ const {
96
+ publish,
97
+ checkAuth,
98
+ getStatus,
99
+ Constants,
100
+ } = useMediaPublishExtension();
101
+
102
+ const auth = await checkAuth({
103
+ platform: 'douyin',
104
+ });
105
+
106
+ const result = await publish({
107
+ platform: 'douyin',
108
+ media: {
109
+ type: 'album',
110
+ title: '人为什么会饿',
111
+ intro: '饥饿是一套由大脑、胃肠、血糖、激素和习惯共同参与的提醒系统。',
112
+ cover: 'https://example.test/cover.jpg',
113
+ tags: ['健康', '科普'],
114
+ content: [
115
+ { url: 'https://example.test/1.jpg' },
116
+ { url: 'https://example.test/2.jpg' },
117
+ ],
118
+ },
119
+ });
120
+
121
+ if (result.code === Constants.StatusCode.Success && result.data.statusRef) {
122
+ const status = await getStatus({
123
+ platform: 'douyin',
124
+ statusRef: result.data.statusRef,
125
+ });
126
+ console.log(status);
127
+ }
128
+ ```
129
+
130
+ ## 公开 API
131
+
132
+ 包只公开一个入口:
133
+
134
+ ```js
135
+ import { useMediaPublishExtension } from '@skrillex1224/chrome-media-publish-extension';
136
+ ```
137
+
138
+ `useMediaPublishExtension()` 返回:
139
+
140
+ ```ts
141
+ {
142
+ publish(input): Promise<ApiResult<PublishData>>
143
+ checkAuth(input): Promise<ApiResult<CheckAuthData>>
144
+ getStatus(input): Promise<ApiResult<GetStatusData>>
145
+ Constants
146
+ }
147
+ ```
148
+
149
+ 不公开 toolkit、runtime、logger、adapter registry,也不公开 `createMediaPublishApi()`。
150
+
151
+ ### `checkAuth(input)`
152
+
153
+ 检查指定平台是否已登录。
154
+
155
+ ```ts
156
+ type CheckAuthInput = {
157
+ platform: string
158
+ timeoutMs?: number
159
+ }
160
+
161
+ type CheckAuthData = {
162
+ isAuthenticated: boolean
163
+ platform: string
164
+ accountId?: string
165
+ username?: string
166
+ avatar?: string
167
+ reason?: string
168
+ }
169
+ ```
170
+
171
+ ### `publish(input)`
172
+
173
+ 发布内容到指定平台。
174
+
175
+ ```ts
176
+ type PublishInput = {
177
+ platform: string
178
+ media: Media
179
+ timeoutMs?: number
180
+ }
181
+
182
+ type PublishData = {
183
+ platform: string
184
+ statusRef?: PublishStatusRef
185
+ reason?: string
186
+ }
187
+ ```
188
+
189
+ `publish()` 成功只代表平台接受了提交,并返回可以轮询的 `statusRef`,不代表内容一定已经公开。发布阶段不返回顶层 `postUrl`;如果 adapter 在发布时拿到了候选链接,应写入 `statusRef.url`。真正公开链接以 `getStatus().data.postUrl` 为准。
190
+
191
+ ### `getStatus(input)`
192
+
193
+ 查询发布状态。
194
+
195
+ ```ts
196
+ type GetStatusInput = {
197
+ platform: string
198
+ statusRef: PublishStatusRef
199
+ timeoutMs?: number
200
+ }
201
+
202
+ type GetStatusData = {
203
+ platform: string
204
+ statusRef: PublishStatusRef
205
+ postUrl?: string
206
+ rawStatus?: string | number
207
+ checkedAt?: number
208
+ reason?: string
209
+ }
210
+ ```
211
+
212
+ `reviewing`、`rejected`、`published` 都表示查询动作本身完成。宿主插件应根据 `code` 和 `state` 决定是否继续轮询、重试或提示用户处理。
213
+
214
+ ## Media 协议
215
+
216
+ Media 协议使用 `type + content`。包只消费当前协议字段;如果调用方额外带了旧字段或业务自定义字段,核心校验会忽略它们,不做兼容映射。
217
+
218
+ ### Markdown 文章
219
+
220
+ ```js
221
+ {
222
+ type: 'article',
223
+ title: '人为什么会饿',
224
+ intro: '摘要或简介',
225
+ cover: 'https://example.test/cover.jpg',
226
+ tags: ['健康', '科普'],
227
+ content: '# 人为什么会饿\n\n正文 markdown'
228
+ }
229
+ ```
230
+
231
+ ### 图集/图文
232
+
233
+ ```js
234
+ {
235
+ type: 'album',
236
+ title: '人为什么会饿',
237
+ intro: '图文描述',
238
+ cover: 'https://example.test/cover.jpg',
239
+ tags: ['健康', '科普'],
240
+ content: [
241
+ { url: 'https://example.test/1.jpg' },
242
+ { url: 'https://example.test/2.jpg' }
243
+ ]
244
+ }
245
+ ```
246
+
247
+ ### 视频
248
+
249
+ ```js
250
+ {
251
+ type: 'video',
252
+ title: '人为什么会饿',
253
+ intro: '视频描述',
254
+ cover: 'https://example.test/cover.jpg',
255
+ tags: ['健康', '科普'],
256
+ content: 'https://example.test/video.mp4'
257
+ }
258
+ ```
259
+
260
+ ## PublishStatusRef
261
+
262
+ `PublishStatusRef` 是 `publish()` 返回、`getStatus()` 消费的持久引用。
263
+
264
+ ```ts
265
+ type PublishStatusRef = {
266
+ platform: string
267
+ type: 'article' | 'album' | 'video'
268
+ id: string
269
+ url?: string
270
+ accountId?: string
271
+ meta?: Record<string, string | number | boolean | null>
272
+ }
273
+ ```
274
+
275
+ `url` 是已知或可推导的公开链接候选,不是状态权威来源。是否真正公开必须看 `getStatus()`。
276
+
277
+ ## 支持平台
278
+
279
+ ### Douyin
280
+
281
+ `platform: 'douyin'` 当前支持:
282
+
283
+ | 能力 | 支持情况 | 说明 |
284
+ |---|---|---|
285
+ | `checkAuth` | 支持 | 静默请求创作者中心用户接口,不打开 UI |
286
+ | `publish` 图文 | 支持 | `media.type: 'album'`,`content` 是图片 URL 数组 |
287
+ | `publish` 视频 | 支持 | `media.type: 'video'`,`content` 是视频 URL |
288
+ | `getStatus` | 支持 | 查询创作者中心详情接口,必要时兜底 mget |
289
+ | markdown 文章 | 不支持 | Douyin adapter 不支持 `type: 'article'` |
290
+ | `onEvent` | 不支持 | 遇到验证码/风控直接返回 `SecurityCheckRequired` |
291
+
292
+ 字段映射:
293
+
294
+ | Media 字段 | Douyin 图文 | Douyin 视频 |
295
+ |---|---|---|
296
+ | `title` | 图文标题段 | `item_title`,同时作为文案第一行 |
297
+ | `intro` | 图文文案主体,不截断 | `caption` 文案主体,不截断 |
298
+ | `tags` | 解析为 Douyin 话题,写入 `text_extra/challenges` | 解析为 Douyin 话题,写入 `text_extra/challenges` |
299
+ | `cover` | 当前使用第一张图作为封面 | 可选自定义封面,上传到 ImageX |
300
+ | `content` | `Array<{ url: string }>` 图片 URL 列表 | 视频 URL 字符串 |
301
+
302
+ Douyin `publish()` 成功返回的 `statusRef.url` 是可推导候选:
303
+
304
+ - 图文:`https://www.douyin.com/note/<id>`
305
+ - 视频:`https://www.douyin.com/video/<id>`
306
+
307
+ 是否真正公开必须以后续 `getStatus()` 返回为准。`spd authorization rejected` 会被视为状态接口临时不可读,adapter 会短延迟复查一次;仍失败时返回 `PublicUrlSyncing`,并在 `data.reason` 保留原始原因。
308
+
309
+ ### Bilibili
310
+
311
+ `platform: 'bilibili'` 当前支持:
312
+
313
+ | 能力 | 支持情况 | 说明 |
314
+ |---|---|---|
315
+ | `checkAuth` | 支持 | 请求 B 站 nav 接口并读取 `bili_jct` cookie |
316
+ | `publish` 图文 | 支持 | `media.type: 'article'`,`content` 是 markdown 字符串 |
317
+ | `publish` 视频 | 支持 | `media.type: 'video'`,`content` 是视频 URL |
318
+ | `getStatus` 图文 | 支持 | 查询空间动态流和动态详情 |
319
+ | `getStatus` 视频 | 支持 | 查询创作中心稿件状态 |
320
+ | 图集/图片集 | 不支持 | Bilibili adapter 不支持 `type: 'album'` |
321
+ | `onEvent` | 不支持 | 遇到验证码/风控直接返回 `SecurityCheckRequired` |
322
+
323
+ 字段映射:
324
+
325
+ | Media 字段 | Bilibili 图文 | Bilibili 视频 |
326
+ |---|---|---|
327
+ | `title` | opus 标题 | 视频标题和分 P 标题 |
328
+ | `intro` | 当前不单独提交 | 视频 `desc`,不截断 |
329
+ | `tags` | 写入图文业务 tags | 逗号拼接为投稿 `tag`,最多取前 10 个 |
330
+ | `cover` | 上传为图文封面 | 上传为视频封面 |
331
+ | `content` | markdown 转 B 站 opus paragraphs | 视频 URL 字符串,下载后 multipart 上传 |
332
+
333
+ Bilibili 图文 markdown 转换会尽量保留标题、段落、粗体、斜体、删除线、下划线 HTML、inline code、链接、图片、引用、列表、任务列表、代码块、表格和分割线。B 站接口没有原生支持的格式会转换为可读文本。
334
+
335
+ Bilibili `publish()` 成功返回的 `statusRef.url` 是候选公开链接:
336
+
337
+ - 图文:`https://www.bilibili.com/read/cv<cvid>` 或 `https://t.bilibili.com/<dynamic_id>`
338
+ - 视频:`https://www.bilibili.com/video/<bvid>`
339
+
340
+ 是否真正公开必须以后续 `getStatus()` 返回为准。B 站图文状态查询优先使用空间动态流;视频状态查询使用创作中心稿件详情接口。
341
+
342
+ ## Result 协议
343
+
344
+ 三个方法都返回:
345
+
346
+ ```ts
347
+ type ApiResult<TData extends object> = {
348
+ code: number
349
+ state: number
350
+ text: string
351
+ data: TData
352
+ }
353
+ ```
354
+
355
+ 没有顶层 `message`。平台细节、错误原因、审核原因放在 `data.reason`。
356
+
357
+ ### State
358
+
359
+ | state | 含义 | 宿主插件建议 |
360
+ |---:|---|---|
361
+ | `0` | 成功 | 继续下一步 |
362
+ | `10000` | 审核中或公开链接同步中 | 稍后继续轮询 |
363
+ | `50000` | 可重试失败 | 宿主按业务策略重试 |
364
+ | `60000` | 终态失败 | 修改输入、登录、账号状态、平台状态或代码 |
365
+
366
+ ### Status Code
367
+
368
+ | code | text | state |
369
+ |---:|---|---:|
370
+ | `0` | 成功 | `0` |
371
+ | `10001` | 审核中 | `10000` |
372
+ | `10002` | 公开链接同步中 | `10000` |
373
+ | `50004` | 操作超时 | `50000` |
374
+ | `50005` | 网络请求失败 | `50000` |
375
+ | `60001` | 请求参数无效 | `60000` |
376
+ | `60002` | 平台暂不支持 | `60000` |
377
+ | `60003` | 未登录或登录态失效 | `60000` |
378
+ | `60004` | 发布状态引用无效 | `60000` |
379
+ | `60005` | 发布状态引用平台不匹配 | `60000` |
380
+ | `60006` | 需要验证码或安全校验 | `60000` |
381
+ | `61003` | 内容审核未通过 | `60000` |
382
+ | `66001` | 平台适配器协议错误 | `60000` |
383
+ | `69001` | 未知错误 | `60000` |
384
+
385
+ `Unknown` 只是兜底。真实 adapter 应尽量把已知平台状态映射到明确 code。
386
+
387
+ ## Timeout 和 Retry
388
+
389
+ 本包不定义业务级默认超时对象。不传 `timeoutMs` 时,步骤执行完全使用 `@skrillex1224/extension-toolkit` 的默认 timeout。每个公开方法仍可以传 `timeoutMs` 覆盖本次调用。
390
+
391
+ Phase 1 不在公开方法层做自动 retry。宿主插件负责业务级 retry,例如发布失败后重试 3 次、状态查询定时轮询等。adapter 内部只有在明确幂等且安全的局部步骤,才可以使用 `extension-toolkit` 的显式 retry。
392
+
393
+ ## 安全校验和 onEvent
394
+
395
+ Phase 1 不支持 `onEvent`,也不支持验证码、二维码、扫码确认、账号风控的暂停和恢复。
396
+
397
+ 如果平台要求人工操作,adapter 应返回或抛出可映射为 `SecurityCheckRequired` 的结果。宿主插件收到后应终止当前任务并提示用户处理。
398
+
399
+ ## 本地开发
400
+
401
+ ```bash
402
+ cd chrome-media-publish-extension
403
+ npm install
404
+ npm run check
405
+ ```
406
+
407
+ `npm run check` 会执行:
408
+
409
+ - 构建 `dist/index.js`
410
+ - 公开入口 smoke test
411
+ - 未支持平台测试
412
+ - 协议校验测试
413
+ - fake adapter 编排测试
414
+ - Douyin adapter、auth、payload、tag、status、AWS4、album、video 单测
415
+ - Bilibili adapter、auth、markdown、opus、status、video 单测
416
+
417
+ 打包最小 Chrome extension 测试夹具:
418
+
419
+ ```bash
420
+ npm run test:pack
421
+ ```
422
+
423
+ 产物目录:
424
+
425
+ ```text
426
+ chrome-media-publish-extension/tests/extension/dist
427
+ ```
428
+
429
+ 在 Chrome 扩展管理页选择 “Load unpacked” 并加载该目录。它只用于验证 npm 包能在 Chrome extension popup 中被 bundler 打包和调用,不是正式插件 UI。
430
+
431
+ ## npm 发布
432
+
433
+ ```bash
434
+ npm run publish:npm
435
+ ```
436
+
437
+ 发布前会先执行 `npm run check`。正式发布顺序应先发布 `@skrillex1224/extension-toolkit`,再发布本包。
438
+
439
+ ## Adapter 开发规范
440
+
441
+ 真实平台 adapter 放在:
442
+
443
+ ```text
444
+ src/adapters/<platform>/
445
+ ```
446
+
447
+ 推荐结构:
448
+
449
+ ```text
450
+ src/adapters/<platform>/
451
+ index.js # 平台 adapter 对象,对外只暴露 checkAuth/publish/getStatus
452
+ auth.js # checkAuth 请求与响应解析
453
+ article.js # 仅平台支持 markdown 文章时存在
454
+ album.js # 仅平台支持图文/图片集时存在
455
+ video.js # 仅平台支持视频时存在
456
+ get-status.js # getStatus 请求与状态映射
457
+ parts/ # 平台内部请求、payload、签名、上传、状态映射等实现
458
+ README.md # 字段映射、已知行为、E2E 记录、statusRef meta key
459
+ ```
460
+
461
+ 不是每个平台都必须有全部文件。拆分原则是职责清晰,而不是机械凑目录。
462
+
463
+ adapter 对象协议:
464
+
465
+ ```ts
466
+ type PlatformAdapter = {
467
+ platform: string
468
+ checkAuth(ctx, input): Promise<{ isAuthenticated: boolean; reason?: string }>
469
+ publish(ctx, media, input): Promise<{ statusRef: PublishStatusRef; reason?: string }>
470
+ getStatus(ctx, statusRef, input): Promise<{ status: 'published' | 'reviewing' | 'public_url_syncing' | 'rejected'; postUrl?: string; rawStatus?: string | number; reason?: string }>
471
+ }
472
+ ```
473
+
474
+ 要求:
475
+
476
+ - 使用函数和普通对象,不使用 adapter base class 或继承体系。
477
+ - `index.js` 只做编排,并在平台目录内按 `media.type` 分流。
478
+ - 请求构造、响应解析、上传算法、签名、状态映射、平台常量拆到 `parts/`。
479
+ - 每个有意义的平台步骤都用 `ctx.Kit.runStep()` 包裹。
480
+ - 通用 Chrome 能力优先补到 `@skrillex1224/extension-toolkit`,不要在业务包里重复造 runtime。
481
+
482
+ ## 当前限制
483
+
484
+ - 当前只注册 Douyin 和 Bilibili adapter;其它平台返回 `UnsupportedPlatform`。
485
+ - Douyin 真实发布依赖 Chrome extension 环境和已登录创作者中心。
486
+ - Douyin 视频发布依赖创作者中心页面内官方 uploader;如果平台前端改版,可能需要重新 E2E 校准。
487
+ - Bilibili 真实发布依赖 Chrome extension 环境和已登录 B 站创作中心。
488
+ - Bilibili 图文发布依赖 read-editor 页面上下文;视频发布依赖 videoup 页面上下文。如果平台前端改版,可能需要重新 E2E 校准。
489
+ - 不支持验证码、二维码、扫码确认、账号风控的暂停和恢复。
490
+ - 不做旧字段映射;额外字段会被忽略。