@dsh-plugins/dsh-museav-assets 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/lib/museav.js ADDED
@@ -0,0 +1,320 @@
1
+ /**
2
+ * dsh-museav-assets —— museav CLI 适配层(纯解析 + 进程调用)。
3
+ *
4
+ * 这是**唯一**知道 museav CLI 输出长什么样的文件。路由和 UI 只认这里吐出的结构化
5
+ * 对象;CLI 改文案,改这一个文件 + 它的测试就够,不用动 UI。
6
+ *
7
+ * CLI 的 stdout/stderr 分工是它自己的**契约**(真源 museav-cli/src/commands/projects.ts):
8
+ * - stdout 给脚本:projects → id 换行;projects assets → `id\turl`;jobs → 完整 JSON
9
+ * 数组;projects create → 新建工作区 id
10
+ * - stderr 给人看:名称 / 出图计数 / 素材数 / media_type / tags
11
+ * 所以**真值一律取 stdout**,stderr 只用来补展示字段。stderr 解析不出来就降级成
12
+ * 只显示 id —— 上游改一句中文不能让整个 tab 变空白。
13
+ *
14
+ * 认证:CLI 自己管 ~/.museav.json 的 token,本插件既不读它也不转发它。
15
+ * 没登录时 CLI 报错,这里原样把 stderr 带回去。
16
+ */
17
+
18
+ import { execFile } from 'node:child_process'
19
+
20
+ /** 上游工作区数量上限(museav projects create 的服务端限制)。 */
21
+ export const PROJECT_LIMIT = 5
22
+
23
+ /** UUID —— 只认标准 8-4-4-4-12,避免把表格里的别的 token 误当 id。 */
24
+ const UUID = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
25
+
26
+ /**
27
+ * 跑一条 museav CLI 命令。
28
+ *
29
+ * 不用 shell —— 参数走数组,project 名里的空格/引号不会被解释成别的命令。
30
+ *
31
+ * @param {string[]} args 例如 `['projects', '--project', 'IP 账户运营']`
32
+ * @param {{bin?: string, timeoutMs?: number}} [opts]
33
+ * @returns {Promise<{ok: boolean, code: number|null, stdout: string, stderr: string, error: string|null}>}
34
+ */
35
+ export function runMuseav (args, opts = {}) {
36
+ const bin = opts.bin || 'museav'
37
+ const timeoutMs = opts.timeoutMs ?? 30000
38
+ return new Promise((resolve) => {
39
+ execFile(
40
+ bin,
41
+ args,
42
+ { timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024, env: process.env },
43
+ (error, stdout, stderr) => {
44
+ if (!error) {
45
+ resolve({ ok: true, code: 0, stdout: stdout ?? '', stderr: stderr ?? '', error: null })
46
+ return
47
+ }
48
+ // 超时也会走到这里;code 为 null 时不要伪装成 exit 1。
49
+ const timedOut = error.killed === true || error.code === 'ETIMEDOUT'
50
+ resolve({
51
+ ok: false,
52
+ code: typeof error.code === 'number' ? error.code : null,
53
+ stdout: stdout ?? '',
54
+ stderr: stderr ?? '',
55
+ error: timedOut
56
+ ? `museav ${args[0] ?? ''} 超时(${timeoutMs}ms)`
57
+ : (stderr || error.message || '').trim(),
58
+ })
59
+ },
60
+ )
61
+ })
62
+ }
63
+
64
+ /**
65
+ * `museav projects` 的 stdout —— 每行一个 id,这是契约。
66
+ * @param {string} stdout
67
+ * @returns {string[]}
68
+ */
69
+ export function parseProjectIds (stdout) {
70
+ return String(stdout ?? '')
71
+ .split('\n')
72
+ .map((line) => line.trim())
73
+ .filter((line) => new RegExp(`^${UUID}$`).test(line))
74
+ }
75
+
76
+ /**
77
+ * `museav projects` 的 stderr 表格 —— 人看的补展示字段,解析失败不致命。
78
+ *
79
+ * 表格行长这样(注意 name 是 padEnd(16),名字 ≥16 字时列间只有 1 个空格,
80
+ * 所以分隔一律用「非贪婪 + \s+ 出图」而不是固定宽度):
81
+ * ` <uuid> 默认项目 出图 39/43 素材 0 brand:brandcheck`
82
+ *
83
+ * @param {string} stderr
84
+ * @returns {Map<string, {name: string, genDone: number, genTotal: number, assetCount: number, brand: string|null}>}
85
+ */
86
+ export function parseProjectTable (stderr) {
87
+ const out = new Map()
88
+ const re = new RegExp(
89
+ `^\\s{2}(${UUID})\\s+(\\S.*?)\\s+出图\\s+(\\d+)\\/(\\d+)\\s+素材\\s+(\\d+)(?:\\s+brand:(\\S+))?\\s*$`,
90
+ )
91
+ for (const line of String(stderr ?? '').split('\n')) {
92
+ const m = re.exec(line)
93
+ if (!m) continue
94
+ out.set(m[1], {
95
+ name: m[2].trim(),
96
+ genDone: Number(m[3]),
97
+ genTotal: Number(m[4]),
98
+ assetCount: Number(m[5]),
99
+ brand: m[6] ?? null,
100
+ })
101
+ }
102
+ return out
103
+ }
104
+
105
+ /**
106
+ * stdout 的 id 列表 + stderr 的展示字段合并。
107
+ * stdout 说了算:stderr 里多/少的行都不会改变 id 集合与顺序。
108
+ *
109
+ * @param {string} stdout
110
+ * @param {string} stderr
111
+ * @returns {{id: string, name: string, genDone: number, genTotal: number, assetCount: number, brand: string|null}[]}
112
+ */
113
+ export function mergeProjects (stdout, stderr) {
114
+ const table = parseProjectTable(stderr)
115
+ return parseProjectIds(stdout).map((id) => {
116
+ const row = table.get(id)
117
+ return {
118
+ id,
119
+ name: row?.name ?? id.slice(0, 8),
120
+ genDone: row?.genDone ?? null,
121
+ genTotal: row?.genTotal ?? null,
122
+ assetCount: row?.assetCount ?? null,
123
+ brand: row?.brand ?? null,
124
+ }
125
+ })
126
+ }
127
+
128
+ /**
129
+ * `museav projects assets` 的 stdout —— `id\turl` 每行一条(agent 直接拿去当 --ref)。
130
+ * @param {string} stdout
131
+ * @returns {{id: string, url: string}[]}
132
+ */
133
+ export function parseAssetTsv (stdout) {
134
+ const out = []
135
+ for (const line of String(stdout ?? '').split('\n')) {
136
+ if (!line.includes('\t')) continue
137
+ const idx = line.indexOf('\t')
138
+ const id = line.slice(0, idx).trim()
139
+ const url = line.slice(idx + 1).trim()
140
+ if (!url) continue
141
+ if (!new RegExp(`^${UUID}$`).test(id)) continue
142
+ out.push({ id, url })
143
+ }
144
+ return out
145
+ }
146
+
147
+ /**
148
+ * stderr 表格:` <uuid> (未命名) image [tag1,tag2]`
149
+ * @param {string} stderr
150
+ * @returns {Map<string, {name: string, mediaType: string, tags: string[]}>}
151
+ */
152
+ export function parseAssetTable (stderr) {
153
+ const out = new Map()
154
+ const re = new RegExp(`^\\s{2}(${UUID})\\s+(\\S.*?)\\s+([a-zA-Z]+)\\s*(?:\\[([^\\]]*)\\])?\\s*$`)
155
+ for (const line of String(stderr ?? '').split('\n')) {
156
+ const m = re.exec(line)
157
+ if (!m) continue
158
+ out.set(m[1], {
159
+ name: m[2].trim(),
160
+ mediaType: m[3].trim(),
161
+ tags: m[4] ? m[4].split(',').map((t) => t.trim()).filter(Boolean) : [],
162
+ })
163
+ }
164
+ return out
165
+ }
166
+
167
+ /**
168
+ * @param {string} stdout
169
+ * @param {string} stderr
170
+ * @returns {{id: string, url: string, name: string, mediaType: string, tags: string[]}[]}
171
+ */
172
+ export function mergeAssets (stdout, stderr) {
173
+ const table = parseAssetTable(stderr)
174
+ return parseAssetTsv(stdout).map(({ id, url }) => {
175
+ const row = table.get(id)
176
+ return {
177
+ id,
178
+ url,
179
+ name: row?.name ?? '(未命名)',
180
+ mediaType: row?.mediaType ?? (/\.(mp4|mov|webm)(\?|$)/i.test(url) ? 'video' : /\.(mp3|wav|m4a)(\?|$)/i.test(url) ? 'audio' : 'image'),
181
+ tags: row?.tags ?? [],
182
+ }
183
+ })
184
+ }
185
+
186
+ /**
187
+ * `museav templates` 的 stderr 表格。
188
+ *
189
+ * 格式(真机 2026-09-29,`museav templates` 102 条):
190
+ * ` <uuid> 演唱会海报·小红书风 v1.0.0 webkubor@163.com 演唱会 3:4 [图片] 字段:subject,title [平台]`
191
+ *
192
+ * **不能按列位置切**:CLI 用 padEnd 对齐,而 padEnd 按 UTF-16 码元算宽度,CJK 只占
193
+ * 一半 —— 实测 `webkubor@163.com`(15 字符)超出 padEnd(10) 不补空格,于是创建人和
194
+ * 分类粘成了一列 `webkubor@163.com 演唱会`。所以从右侧的括号标签锚定,只取无歧义的
195
+ * 字段:归属 / 类型 / 字段列表 / 版本 / 画幅 / 名称。创建人与分类不显示 —— 猜出来的
196
+ * 分类是错的,比没有更坏。搜索走 CLI 自己的 `--category` 过滤。
197
+ *
198
+ * @param {string} stderr
199
+ * @returns {Map<string, {name: string, version: string|null, ratio: string|null, fieldCount: number, fields: string[], source: string|null, type: string|null}>}
200
+ */
201
+ export function parseTemplateTable (stderr) {
202
+ const out = new Map()
203
+ const re = new RegExp(`^\\s{2}(${UUID})\\s+(.*)$`)
204
+ for (const line of String(stderr ?? '').split('\n')) {
205
+ const m = re.exec(line)
206
+ if (!m) continue
207
+ const id = m[1]
208
+ let rest = m[2].trim()
209
+
210
+ // 归属:[平台] / [个人] / [租户],永远在最后
211
+ const source = /\[(平台|个人|租户)\]\s*$/.exec(rest)
212
+ if (source) rest = rest.slice(0, source.index).trim()
213
+
214
+ // 字段清单:字段:key1,key2
215
+ const fields = /字段:([^\s[]+)/.exec(rest)
216
+ const fieldList = fields ? fields[1].split(',').map((f) => f.trim()).filter(Boolean) : []
217
+ if (fields) rest = (rest.slice(0, fields.index) + rest.slice(fields.index + fields[0].length)).trim()
218
+
219
+ // 类型:[图片] / [文字]
220
+ const type = /\[(图片|文字)\]/.exec(rest)
221
+ if (type) rest = (rest.slice(0, type.index) + rest.slice(type.index + type[0].length)).trim()
222
+
223
+ // 画幅:形如 3:4 / 16:9
224
+ const ratio = /(\d{1,2}:\d{1,2})\s*$/.exec(rest)
225
+ if (ratio) rest = rest.slice(0, ratio.index).trim()
226
+
227
+ // 版本:v1.0.0 / 1.0 —— 取第一个像版本的 token,它前面就是名称
228
+ const version = /(?:^|\s)(v?\d+\.\d+(?:\.\d+)?)(?=\s|$)/.exec(rest)
229
+ const name = (version ? rest.slice(0, version.index) : rest).trim() || null
230
+
231
+ out.set(id, {
232
+ name: name || id.slice(0, 8),
233
+ version: version ? version[1] : null,
234
+ ratio: ratio ? ratio[1] : null,
235
+ fieldCount: fieldList.length,
236
+ fields: fieldList,
237
+ source: source ? source[1] : null,
238
+ type: type ? type[1] : null,
239
+ })
240
+ }
241
+ return out
242
+ }
243
+
244
+ /**
245
+ * 合并 templates 的 stdout(id 换行)与 stderr 表格。
246
+ * @param {string} stdout
247
+ * @param {string} stderr
248
+ * @returns {{id: string, name: string, version: string|null, ratio: string|null, fieldCount: number, fields: string[], source: string|null, type: string|null}[]}
249
+ */
250
+ export function mergeTemplates (stdout, stderr) {
251
+ const table = parseTemplateTable(stderr)
252
+ return parseProjectIds(stdout).map((id) => {
253
+ const row = table.get(id)
254
+ return {
255
+ id,
256
+ name: row?.name ?? id.slice(0, 8),
257
+ version: row?.version ?? null,
258
+ ratio: row?.ratio ?? null,
259
+ fieldCount: row?.fieldCount ?? 0,
260
+ // fields 是模板的占位符 key 列表 —— 出图台要按它动态渲染输入框,
261
+ // 再拼成 `museav gen --template <id> --fields '{"key":"值"}'`
262
+ fields: row?.fields ?? [],
263
+ source: row?.source ?? null,
264
+ type: row?.type ?? null,
265
+ }
266
+ })
267
+ }
268
+
269
+ /**
270
+ * `museav jobs` 的 stdout 是完整 JSON 数组(不是表格),直接 parse。
271
+ * @param {string} stdout
272
+ * @returns {object[]}
273
+ */
274
+ export function parseJobArray (stdout) {
275
+ const text = String(stdout ?? '').trim()
276
+ if (!text) return []
277
+ try {
278
+ const data = JSON.parse(text)
279
+ return Array.isArray(data) ? data : []
280
+ } catch {
281
+ return []
282
+ }
283
+ }
284
+
285
+ /**
286
+ * 出图记录只留 tab 要显示的字段 —— prompt 可能几百字,整包塞进 JSON 响应是浪费。
287
+ *
288
+ * 注意 workspace_id 在客户端登录态下常常是 null(见真机输出),所以 --project
289
+ * 的过滤在 CLI 侧本来就可能筛不出东西;这里不过滤,只如实显示返回的记录。
290
+ *
291
+ * @param {object[]} jobs
292
+ * @returns {{id: string, status: string, mediaType: string, model: string, url: string|null, prompt: string, ratio: string|null, createdAt: string|null, workspaceId: string|null, elapsedMs: number|null, error: string|null}[]}
293
+ */
294
+ export function summarizeJobs (jobs) {
295
+ return jobs.map((job) => ({
296
+ id: job.id,
297
+ status: job.status ?? 'unknown',
298
+ mediaType: job.media_type ?? 'image',
299
+ model: job.model ?? '',
300
+ url: job.cdn_url ?? null,
301
+ prompt: typeof job.prompt === 'string' ? job.prompt : '',
302
+ ratio: job.ratio ?? null,
303
+ createdAt: job.created_at ?? null,
304
+ workspaceId: job.workspace_id ?? null,
305
+ elapsedMs: typeof job.elapsed_ms === 'number' ? job.elapsed_ms : null,
306
+ error: job.error ?? null,
307
+ }))
308
+ }
309
+
310
+ /**
311
+ * 找「可当 --ref 用的素材 URL」—— 出图记录里的垫图 / 生成结果。
312
+ * @param {{id: string, url: string, mediaType: string}[]} assets
313
+ * @param {string} needle
314
+ * @returns {string[]}
315
+ */
316
+ export function pickAssetUrls (assets, needle) {
317
+ const key = String(needle ?? '').trim().toLowerCase()
318
+ if (!key) return []
319
+ return assets.filter((a) => a.url.toLowerCase().includes(key)).map((a) => a.url)
320
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@dsh-plugins/dsh-museav-assets",
3
+ "version": "0.1.1",
4
+ "description": "MUSE AV 资产台:在 DSH 主区加一个「资产」tab,按项目(工作区)看素材库与最近出图,点素材复制直链当垫图用。基于本机 museav CLI,不碰任何密钥。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "./types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./client": {
14
+ "types": "./types/client.d.ts",
15
+ "default": "./lib/client.js"
16
+ },
17
+ "./package.json": {
18
+ "default": "./package.json"
19
+ }
20
+ },
21
+ "dsh": {
22
+ "bundle": {
23
+ "patch": "./cordis.patch.yml"
24
+ },
25
+ "client": {
26
+ "inject": [
27
+ "@deepseek-ai/dsh-client-locale",
28
+ "@deepseek-ai/dsh-client-ui-slots"
29
+ ],
30
+ "platform": "web"
31
+ }
32
+ },
33
+ "scripts": {
34
+ "prepublishOnly": "npm run check && npm test && node scripts/prepublish-gate.mjs && node scripts/readme-gate.mjs",
35
+ "check": "for f in lib/*.js; do node --check \"$f\" || exit 1; done",
36
+ "test": "node --test test/*.test.mjs",
37
+ "deploy": "rsync -av --delete lib/ ~/.dsh/profiles/${DSH_PROFILE:-desktop}/node_modules/@dsh-plugins/dsh-museav-assets/lib/ && rsync -av types package.json cordis.patch.yml README.md README.en.md LICENSE ~/.dsh/profiles/${DSH_PROFILE:-desktop}/node_modules/@dsh-plugins/dsh-museav-assets/"
38
+ },
39
+ "keywords": [
40
+ "dsh",
41
+ "dsh-plugin",
42
+ "deepseek-harness",
43
+ "museav",
44
+ "assets",
45
+ "image-generation"
46
+ ],
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/webkubor/dsh-museav-assets.git"
51
+ },
52
+ "bugs": {
53
+ "url": "https://github.com/webkubor/dsh-museav-assets/issues"
54
+ },
55
+ "homepage": "https://github.com/webkubor/dsh-museav-assets#readme",
56
+ "publishConfig": {
57
+ "access": "public"
58
+ },
59
+ "files": [
60
+ "CHANGELOG.md",
61
+ "LICENSE",
62
+ "README.md",
63
+ "README.en.md",
64
+ "cordis.patch.yml",
65
+ "lib",
66
+ "types"
67
+ ]
68
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * dsh-museav-assets Client Globals
3
+ */
4
+
5
+ declare global {
6
+ interface Window {
7
+ __ModuleLoader__: {
8
+ load(opts: { id: string; factory: (require?: any, exports?: any, module?: any) => any }): void
9
+ }
10
+ }
11
+ }
12
+
13
+ declare function require(id: string): any
14
+
15
+ declare module 'react' {
16
+ const React: any
17
+ export default React
18
+ export = React
19
+ }
20
+
21
+ export {}
@@ -0,0 +1,73 @@
1
+ /**
2
+ * dsh-museav-assets TypeScript Definitions
3
+ */
4
+
5
+ export const name: '@dsh-plugins/dsh-museav-assets'
6
+ export const inject: string[]
7
+
8
+ /** 一个工作区(项目)。id 是真值,name 来自 stderr 表格,解析不到就退回 id 前 8 位。 */
9
+ export interface MuseProject {
10
+ id: string
11
+ name: string
12
+ genDone: number | null
13
+ genTotal: number | null
14
+ assetCount: number | null
15
+ brand: string | null
16
+ }
17
+
18
+ /** 工作区素材库里的一条。url 可直接当 `museav gen --ref` 的垫图直链。 */
19
+ export interface MuseAsset {
20
+ id: string
21
+ url: string
22
+ name: string
23
+ mediaType: string
24
+ tags: string[]
25
+ }
26
+
27
+ /** 最近出图记录(已裁剪过 prompt 之外的大字段)。 */
28
+ export interface MuseJob {
29
+ id: string
30
+ status: string
31
+ mediaType: string
32
+ model: string
33
+ url: string | null
34
+ prompt: string
35
+ ratio: string | null
36
+ createdAt: string | null
37
+ workspaceId: string | null
38
+ elapsedMs: number | null
39
+ error: string | null
40
+ }
41
+
42
+ export interface ProjectsResponse {
43
+ ok: boolean
44
+ error?: string
45
+ projects: MuseProject[]
46
+ used?: number
47
+ limit?: number
48
+ /** 已达服务端上限(每账户 5 个),此时新建会被拒。 */
49
+ full?: boolean
50
+ id?: string | null
51
+ }
52
+
53
+ export interface AssetsResponse {
54
+ ok: boolean
55
+ error?: string
56
+ project?: string
57
+ assets: MuseAsset[]
58
+ }
59
+
60
+ export interface JobsResponse {
61
+ ok: boolean
62
+ error?: string
63
+ jobs: MuseJob[]
64
+ }
65
+
66
+ export interface HealthResponse {
67
+ ok: boolean
68
+ cli: { bin: string; version: string | null; error: string | null }
69
+ projectLimit: number
70
+ }
71
+
72
+ /** 服务端工作区数量上限,与 museav projects create 的限制一致。 */
73
+ export const PROJECT_LIMIT: 5