@xiaopuai/dsh-media 0.0.0-stage → 0.5.0
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/LICENSE +21 -0
- package/README.md +19 -2
- package/client.js +5657 -0
- package/cordis.patch.yml +9 -0
- package/icon.svg +6 -0
- package/lib/index.js +45860 -0
- package/lib/schemas-B63HZw_j.js +12935 -0
- package/lib/typert.host.js +6913 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +74 -4
- package/ui.js +177 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,76 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaopuai/dsh-media",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "自媒体:抖音、小红书、公众号、视频号多账号数据看板(照抖音数据中心排版),右边是读得到这些数据的 AI 助手。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "xiaopu-ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/xiaopu-ai/dsh-plugin-media.git",
|
|
10
|
+
"directory": "dsh-media"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/xiaopu-ai/dsh-plugin-media#readme",
|
|
13
|
+
"bugs": "https://github.com/xiaopu-ai/dsh-plugin-media/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"deepseek-harness",
|
|
16
|
+
"dsh-plugin",
|
|
17
|
+
"douyin",
|
|
18
|
+
"xiaohongshu",
|
|
19
|
+
"wechat",
|
|
20
|
+
"creator-analytics"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "lib/index.js",
|
|
24
|
+
"icon": "./icon.svg",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./lib/index.js",
|
|
27
|
+
"./typert": "./lib/typert.host.js",
|
|
28
|
+
"./ui": "./ui.js",
|
|
29
|
+
"./client": "./client.js",
|
|
30
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
31
|
+
"./locale/*.json": "./locale/*.json",
|
|
32
|
+
"./icon": "./icon.svg",
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"lib",
|
|
37
|
+
"ui.js",
|
|
38
|
+
"client.js",
|
|
39
|
+
"cordis.patch.yml",
|
|
40
|
+
"locale/*.json",
|
|
41
|
+
"icon.svg",
|
|
42
|
+
"README.md",
|
|
43
|
+
"LICENSE"
|
|
44
|
+
],
|
|
45
|
+
"dsh": {
|
|
46
|
+
"bundle": {
|
|
47
|
+
"patch": "./cordis.patch.yml"
|
|
48
|
+
},
|
|
49
|
+
"client": {
|
|
50
|
+
"platform": "web",
|
|
51
|
+
"immediately": true,
|
|
52
|
+
"inject": [
|
|
53
|
+
"@deepseek-ai/dsh-client-connection",
|
|
54
|
+
"@deepseek-ai/dsh-client-locale",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
56
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
57
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"dsh": ">=0.2.2-rc.1 <0.3.0-0"
|
|
64
|
+
},
|
|
65
|
+
"peerDependencies": {
|
|
66
|
+
"@deepseek-ai/cordis": "*",
|
|
67
|
+
"@deepseek-ai/schemastery": "*",
|
|
68
|
+
"@deepseek-ai/dsh-brand": ">=0.2.2-rc.1 <0.3.0-0",
|
|
69
|
+
"@deepseek-ai/dsh-llm": ">=0.2.2-rc.1 <0.3.0-0",
|
|
70
|
+
"@deepseek-ai/dsh-tools": ">=0.2.2-rc.1 <0.3.0-0",
|
|
71
|
+
"@deepseek-ai/dsh-typert-protocol": ">=0.2.2-rc.1 <0.3.0-0"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"playwright-core": "1.61.1"
|
|
75
|
+
}
|
|
76
|
+
}
|
package/ui.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自媒体界面 · 画面转接(Host)
|
|
3
|
+
*
|
|
4
|
+
* 数据、抓取、AI 都在同一个包的后台里(lib/index.js,id: media-suite),
|
|
5
|
+
* 页面直接调它的接口(client.js 里的 api('accounts') 这类),这里不用再写一遍。
|
|
6
|
+
*
|
|
7
|
+
* 这里做两件事:
|
|
8
|
+
* 1. 转接「登录平台用的小浏览器」的画面。后台是一张一张往外推画面的(一条不断开的流),
|
|
9
|
+
* 手写的页面接不了这种流,所以这里替页面接住,页面每隔一会儿来问一次「有新画面吗」(action: frames)。
|
|
10
|
+
* 2. 给右边的 AI 助手准备一个工作区(action: openChat):一个专门的文件夹,默认「文稿/自媒体」。
|
|
11
|
+
* AI 导出的表格、写的稿子都放在这里,用户在访达里找得到。
|
|
12
|
+
*
|
|
13
|
+
* 配置(cordis.patch.yml 里 id: media-ui 那一行的 config,都可以不写):
|
|
14
|
+
* workspaceDir AI 助手工作区的绝对路径,默认 ~/Documents/自媒体
|
|
15
|
+
*
|
|
16
|
+
* 改了这个文件要重启 DSH 才生效(页面文件 client.js 改了会自动生效)。
|
|
17
|
+
*/
|
|
18
|
+
import { mkdir } from 'node:fs/promises'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { isAbsolute, join } from 'node:path'
|
|
21
|
+
|
|
22
|
+
export const name = 'media-ui'
|
|
23
|
+
export const inject = ['connection', 'mediaSuite']
|
|
24
|
+
|
|
25
|
+
/** 页面调这里走的地址。页面里的 BRIDGE 要和它对上(去掉开头的 /api/)。 */
|
|
26
|
+
const ROUTE = '/api/mediaUi/call'
|
|
27
|
+
|
|
28
|
+
/** 页面问一次最多等多久(毫秒)。没有新画面就空手回去,页面会接着再问。 */
|
|
29
|
+
const WAIT_MS = 10_000
|
|
30
|
+
/** 页面多久没来问,就认为它关掉了,停止接画面(毫秒)。 */
|
|
31
|
+
const IDLE_MS = 30_000
|
|
32
|
+
/** 最多攒多少条还没被取走的消息。 */
|
|
33
|
+
const MAX_QUEUE = 200
|
|
34
|
+
|
|
35
|
+
/** 想让用户直接看到的错误用它抛。 */
|
|
36
|
+
export class UserError extends Error {}
|
|
37
|
+
|
|
38
|
+
// ------------------------------------------------------------------ 接住画面流
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 每个打开的小浏览器(id)一个「接收器」:把后台推来的消息排好队、编上号,等页面来取。
|
|
42
|
+
* 消息有几种:frame 画面、login 登录状态、qr 二维码、identity 认出的账号、closed 已关闭。
|
|
43
|
+
* 画面只留最新一张(旧的没人要看),其它几种一条都不能丢。
|
|
44
|
+
*/
|
|
45
|
+
function makeFrameHub(suite, log) {
|
|
46
|
+
const hubs = new Map()
|
|
47
|
+
|
|
48
|
+
function start(id) {
|
|
49
|
+
const hub = { seq: 0, queue: [], waiters: new Set(), abort: new AbortController(), ended: false, error: null, idle: null }
|
|
50
|
+
hubs.set(id, hub)
|
|
51
|
+
const wake = () => { for (const fn of hub.waiters) fn(); hub.waiters.clear() }
|
|
52
|
+
;(async () => {
|
|
53
|
+
try {
|
|
54
|
+
for await (const frame of suite.browserFrames(id, hub.abort.signal)) {
|
|
55
|
+
if (frame.kind === 'frame') hub.queue = hub.queue.filter(item => item.frame.kind !== 'frame')
|
|
56
|
+
hub.queue.push({ seq: ++hub.seq, frame })
|
|
57
|
+
if (hub.queue.length > MAX_QUEUE) hub.queue.splice(0, hub.queue.length - MAX_QUEUE)
|
|
58
|
+
wake()
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!hub.abort.signal.aborted) {
|
|
62
|
+
hub.error = { code: error?.code ?? `${name}/internal`, message: error?.message ?? String(error) }
|
|
63
|
+
log(`小浏览器 ${id} 的画面断了:${hub.error.message}`)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
hub.ended = true
|
|
67
|
+
wake()
|
|
68
|
+
})()
|
|
69
|
+
return hub
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stop(id) {
|
|
73
|
+
const hub = hubs.get(id)
|
|
74
|
+
if (!hub) return
|
|
75
|
+
hubs.delete(id)
|
|
76
|
+
clearTimeout(hub.idle)
|
|
77
|
+
hub.abort.abort()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** 页面来取:给出 after(上次取到第几号),拿回之后的新消息;没有就等一会儿。 */
|
|
81
|
+
async function take(id, after) {
|
|
82
|
+
const hub = hubs.get(id) ?? start(id)
|
|
83
|
+
clearTimeout(hub.idle)
|
|
84
|
+
hub.idle = setTimeout(() => stop(id), IDLE_MS)
|
|
85
|
+
const fresh = () => hub.queue.filter(item => item.seq > after)
|
|
86
|
+
if (fresh().length === 0 && !hub.ended) {
|
|
87
|
+
await new Promise(resolve => {
|
|
88
|
+
const timer = setTimeout(done, WAIT_MS)
|
|
89
|
+
function done() { clearTimeout(timer); hub.waiters.delete(done); resolve() }
|
|
90
|
+
hub.waiters.add(done)
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
const items = fresh()
|
|
94
|
+
const result = { cursor: items.length ? items[items.length - 1].seq : after, frames: items.map(item => item.frame), ended: hub.ended, error: hub.error }
|
|
95
|
+
if (hub.ended) stop(id)
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { take, stop, stopAll: () => { for (const id of [...hubs.keys()]) stop(id) } }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ------------------------------------------------------------------ 能做的事:一个 action 一个函数
|
|
103
|
+
|
|
104
|
+
function makeHandlers(hub, chat) {
|
|
105
|
+
return {
|
|
106
|
+
/** AI 助手工作区在哪个文件夹:页面用它认出「这是自媒体的对话」,只在这种对话里显示输入框上方的快捷模块。 */
|
|
107
|
+
async chatInfo() {
|
|
108
|
+
return { folder: chat.dir }
|
|
109
|
+
},
|
|
110
|
+
/** 建好(或找到)AI 助手的工作区,返回它的 id;页面拿它开一段新对话。 */
|
|
111
|
+
async openChat() {
|
|
112
|
+
const registry = chat.registry()
|
|
113
|
+
if (registry === undefined) throw new UserError('这个 DSH 没有工作区功能,没法开对话')
|
|
114
|
+
await mkdir(chat.dir, { recursive: true })
|
|
115
|
+
const workspace = await registry.create(chat.dir, '自媒体助手')
|
|
116
|
+
return { workspaceId: workspace.id, folder: chat.dir }
|
|
117
|
+
},
|
|
118
|
+
/** 取小浏览器的新画面和状态。id 是 browserOpen 返回的 id,after 是上次拿到的 cursor(第一次传 0)。 */
|
|
119
|
+
async frames({ id, after }) {
|
|
120
|
+
if (typeof id !== 'string' || !id) throw new UserError('缺少小浏览器的 id')
|
|
121
|
+
return hub.take(id, Number(after) || 0)
|
|
122
|
+
},
|
|
123
|
+
/** 页面不看了:停止接这个小浏览器的画面(不会关掉小浏览器本身)。 */
|
|
124
|
+
async stopFrames({ id }) {
|
|
125
|
+
hub.stop(String(id))
|
|
126
|
+
return null
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ------------------------------------------------------------------ 把 handlers 接到页面能调的地址上(一般不用改)
|
|
132
|
+
|
|
133
|
+
async function respond(request, handlers, log) {
|
|
134
|
+
let message
|
|
135
|
+
try {
|
|
136
|
+
message = await request.json()
|
|
137
|
+
} catch {
|
|
138
|
+
// 不是 JSON 的请求没有 rpcId 可以回,只能直接拒绝。
|
|
139
|
+
return new Response('bad request', { status: 400 })
|
|
140
|
+
}
|
|
141
|
+
if (message?.type !== 'client-request' || typeof message.rpcId !== 'string') return new Response('bad request', { status: 400 })
|
|
142
|
+
const { action, ...args } = message.payload ?? {}
|
|
143
|
+
let result
|
|
144
|
+
try {
|
|
145
|
+
const handler = Object.hasOwn(handlers, action) ? handlers[action] : null
|
|
146
|
+
if (!handler) throw new UserError(`后台没有「${action}」这个操作`)
|
|
147
|
+
result = { ok: true, value: (await handler(args)) ?? null }
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (error instanceof UserError) {
|
|
150
|
+
result = { ok: false, error: { code: `${name}/user`, message: error.message, details: {} } }
|
|
151
|
+
} else {
|
|
152
|
+
log(`操作「${action}」出错:${error?.stack ?? error}`)
|
|
153
|
+
result = { ok: false, error: { code: `${name}/internal`, message: `插件内部出错了,不是你操作的问题(${error?.message ?? error})`, details: {} } }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return new Response(JSON.stringify({ type: 'server-response', rpcId: message.rpcId, result }), { headers: { 'content-type': 'application/json' } })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 插件入口。
|
|
161
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
162
|
+
*/
|
|
163
|
+
export function apply(ctx, config) {
|
|
164
|
+
const log = text => ctx.logger?.warn?.(`${name}: ${text}`)
|
|
165
|
+
const hub = makeFrameHub(ctx.mediaSuite, log)
|
|
166
|
+
const configured = config?.workspaceDir
|
|
167
|
+
if (configured !== undefined && (typeof configured !== 'string' || !isAbsolute(configured))) throw new Error(`${name}: workspaceDir 要写绝对路径`)
|
|
168
|
+
const chat = { dir: configured ?? join(homedir(), 'Documents', '自媒体'), registry: () => ctx.get('workspaceRegistry') }
|
|
169
|
+
const handlers = makeHandlers(hub, chat)
|
|
170
|
+
ctx.effect(() => () => hub.stopAll(), `${name}: 停止接画面`)
|
|
171
|
+
ctx.effect(() => ctx.connection.fetch.register({
|
|
172
|
+
path: ROUTE,
|
|
173
|
+
methods: ['POST'],
|
|
174
|
+
requestBody: 'buffered',
|
|
175
|
+
fetch: request => respond(request, handlers, log),
|
|
176
|
+
}), `${name}: 页面调后台的地址`)
|
|
177
|
+
}
|