@soulspacex/cli 0.1.0 → 0.2.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 +4 -1
- package/dist/commands/asset.js +149 -0
- package/dist/commands/misc.js +5 -0
- package/dist/commands/node.js +1 -1
- package/dist/index.js +15 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -33,6 +33,7 @@ ssx node list # 看看都生成完了没
|
|
|
33
33
|
- **生成默认阻塞到出结果**(最长 55 秒)。不要自己写 while 循环轮询——每轮一次都是一次模型调用,token 是用户在付。要立刻返回就 `--wait 0`。
|
|
34
34
|
- **节点可以用名字指代**,重名时才需要用 id。
|
|
35
35
|
- **命令行创作按积分计费,不使用无限模式额度。**
|
|
36
|
+
- **想让产物在网页素材库里看得到,要显式存**:`ssx asset save 主视觉 --category scene`。分类是 `character` / `scene` / `item`(默认)/ `voice`,**素材库不收视频**。
|
|
36
37
|
|
|
37
38
|
## 命令
|
|
38
39
|
|
|
@@ -47,7 +48,9 @@ ssx node list # 看看都生成完了没
|
|
|
47
48
|
| `ssx generate <kind> --model <名> --prompt <描述>` | 不走画布的一次性生成 |
|
|
48
49
|
| `ssx model list` / `ssx model search <词>` | 可用模型与计价 |
|
|
49
50
|
| `ssx schema` | 节点字段契约 |
|
|
50
|
-
| `ssx upload <文件>` / `ssx download <url>` |
|
|
51
|
+
| `ssx upload <文件>` / `ssx download <url>` | 素材进出(默认只拿 URL,不进素材库) |
|
|
52
|
+
| `ssx asset save <节点名\|url>` / `asset list` | 存进素材库、看库里有什么 |
|
|
53
|
+
| `ssx space list` | 素材空间,不指定就落默认个人空间 |
|
|
51
54
|
|
|
52
55
|
## 环境变量
|
|
53
56
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, resolve as resolvePath } from 'node:path';
|
|
3
|
+
import { api } from '../api.js';
|
|
4
|
+
import { emit, fail } from '../output.js';
|
|
5
|
+
import { find } from './node.js';
|
|
6
|
+
import { resolveWorkflowId } from './workflow.js';
|
|
7
|
+
export async function asset(parsed) {
|
|
8
|
+
const sub = parsed.positional[1] ?? 'list';
|
|
9
|
+
switch (sub) {
|
|
10
|
+
case 'list':
|
|
11
|
+
return listItems(parsed);
|
|
12
|
+
case 'save':
|
|
13
|
+
return save(parsed);
|
|
14
|
+
default:
|
|
15
|
+
return fail(`未知的子命令 ${sub}`, '可用:list / save');
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function space(parsed) {
|
|
19
|
+
const sub = parsed.positional[1] ?? 'list';
|
|
20
|
+
if (sub !== 'list') {
|
|
21
|
+
return fail(`未知的子命令 ${sub}`, '可用:list');
|
|
22
|
+
}
|
|
23
|
+
emit(await api.get('/openapi/library/spaces'));
|
|
24
|
+
}
|
|
25
|
+
async function listItems(parsed) {
|
|
26
|
+
const query = new URLSearchParams();
|
|
27
|
+
const spaceId = str(parsed.options.space);
|
|
28
|
+
const category = str(parsed.options.category);
|
|
29
|
+
const keyword = str(parsed.options.keyword) ?? parsed.positional[2];
|
|
30
|
+
if (spaceId)
|
|
31
|
+
query.set('spaceId', spaceId);
|
|
32
|
+
if (category)
|
|
33
|
+
query.set('category', category);
|
|
34
|
+
if (keyword)
|
|
35
|
+
query.set('keyword', keyword);
|
|
36
|
+
if (str(parsed.options.limit))
|
|
37
|
+
query.set('limit', str(parsed.options.limit));
|
|
38
|
+
const page = await api.get(`/openapi/library?${query}`);
|
|
39
|
+
emit({
|
|
40
|
+
total: page.total,
|
|
41
|
+
items: page.items.map((i) => ({
|
|
42
|
+
id: i.id,
|
|
43
|
+
name: i.name,
|
|
44
|
+
category: i.category,
|
|
45
|
+
type: i.type,
|
|
46
|
+
url: i.url,
|
|
47
|
+
})),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 存进素材库。参数既接节点名也接裸 URL——agent 手里通常是刚跑完的节点名,
|
|
52
|
+
* 让它先去翻 URL 再传回来是多一轮往返,而每一轮都是用户在付 token。
|
|
53
|
+
*/
|
|
54
|
+
async function save(parsed) {
|
|
55
|
+
const ref = parsed.positional[2];
|
|
56
|
+
if (!ref) {
|
|
57
|
+
return fail('要指定节点名或 URL', '用法:ssx asset save 主视觉 --category scene');
|
|
58
|
+
}
|
|
59
|
+
let url;
|
|
60
|
+
let sourceAssetId;
|
|
61
|
+
let name = str(parsed.options.name);
|
|
62
|
+
if (/^https?:\/\//i.test(ref)) {
|
|
63
|
+
url = ref;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const node = await find(resolveWorkflowId(parsed), ref);
|
|
67
|
+
const data = (node.data ?? {});
|
|
68
|
+
const found = artifactOf(data);
|
|
69
|
+
if (found.url === null) {
|
|
70
|
+
return found.reason === 'pending'
|
|
71
|
+
? fail(`节点「${ref}」还没有产物(当前 ${String(data.status ?? 'idle')})`, `先跑 ssx node run ${ref}`)
|
|
72
|
+
: fail(`节点「${ref}」的产物不是图片或音频`, '素材库只收图片与音频,文本节点存不进去');
|
|
73
|
+
}
|
|
74
|
+
url = found.url;
|
|
75
|
+
sourceAssetId = found.sourceAssetId;
|
|
76
|
+
name = name ?? str(data.title);
|
|
77
|
+
}
|
|
78
|
+
const item = await api.post('/openapi/library/from-url', {
|
|
79
|
+
url,
|
|
80
|
+
category: str(parsed.options.category),
|
|
81
|
+
name,
|
|
82
|
+
description: str(parsed.options.description),
|
|
83
|
+
spaceId: numeric(str(parsed.options.space)),
|
|
84
|
+
sourceAssetId,
|
|
85
|
+
});
|
|
86
|
+
emit({ assetId: item.id, spaceId: item.spaceId, category: item.category, url: item.url });
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 从节点 data 里取可入库的产物。
|
|
90
|
+
*
|
|
91
|
+
* 优先 assets[0]:它带 assetId,能让服务端继承源素材的尺寸和 mime。退回 result 时只有 URL,
|
|
92
|
+
* 入库后宽高是空的——素材库列表按宽高排版,缺了会塌。
|
|
93
|
+
*
|
|
94
|
+
* 取不到时区分两种原因:还没跑(让用户去 run)与产物压根不是媒体(文本节点,重试多少次都一样)。
|
|
95
|
+
* 合成一句「存不了」会让 agent 反复重跑那个节点。
|
|
96
|
+
*/
|
|
97
|
+
export function artifactOf(data) {
|
|
98
|
+
const assets = Array.isArray(data.assets) ? data.assets : [];
|
|
99
|
+
const first = assets[0];
|
|
100
|
+
if (first?.url) {
|
|
101
|
+
return { url: first.url, sourceAssetId: first.assetId ?? numeric(data.assetId) };
|
|
102
|
+
}
|
|
103
|
+
if (typeof data.result === 'string' && /^https?:\/\//i.test(data.result)) {
|
|
104
|
+
return { url: data.result, sourceAssetId: numeric(data.assetId) };
|
|
105
|
+
}
|
|
106
|
+
return { url: null, reason: data.status === 'completed' ? 'not-media' : 'pending' };
|
|
107
|
+
}
|
|
108
|
+
/** 供 `ssx upload --library` 用:直接把本地文件传成素材库条目。 */
|
|
109
|
+
export async function uploadToLibrary(parsed, path) {
|
|
110
|
+
const abs = resolvePath(path);
|
|
111
|
+
const bytes = await readFile(abs).catch(() => fail(`读不到文件 ${abs}`));
|
|
112
|
+
const form = new FormData();
|
|
113
|
+
form.append('file', new Blob([bytes], { type: mimeOf(abs) }), basename(abs));
|
|
114
|
+
const category = str(parsed.options.category);
|
|
115
|
+
const spaceId = str(parsed.options.space);
|
|
116
|
+
const name = str(parsed.options.name);
|
|
117
|
+
const query = new URLSearchParams();
|
|
118
|
+
if (category)
|
|
119
|
+
query.set('category', category);
|
|
120
|
+
if (spaceId)
|
|
121
|
+
query.set('spaceId', spaceId);
|
|
122
|
+
if (name)
|
|
123
|
+
query.set('name', name);
|
|
124
|
+
const item = await api.upload(`/openapi/library/upload?${query}`, form);
|
|
125
|
+
emit({ assetId: item.id, spaceId: item.spaceId, category: item.category, url: item.url });
|
|
126
|
+
}
|
|
127
|
+
function str(v) {
|
|
128
|
+
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
|
129
|
+
}
|
|
130
|
+
function numeric(v) {
|
|
131
|
+
const n = Number(v);
|
|
132
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
133
|
+
}
|
|
134
|
+
// 与 misc.ts 的 upload 同源:Blob 不带 type 时 multipart 会退成 application/octet-stream,
|
|
135
|
+
// 服务端按 MIME 判类型会直接拒
|
|
136
|
+
function mimeOf(path) {
|
|
137
|
+
const ext = path.slice(path.lastIndexOf('.') + 1).toLowerCase();
|
|
138
|
+
const map = {
|
|
139
|
+
png: 'image/png',
|
|
140
|
+
jpg: 'image/jpeg',
|
|
141
|
+
jpeg: 'image/jpeg',
|
|
142
|
+
webp: 'image/webp',
|
|
143
|
+
gif: 'image/gif',
|
|
144
|
+
mp3: 'audio/mpeg',
|
|
145
|
+
wav: 'audio/wav',
|
|
146
|
+
m4a: 'audio/mp4',
|
|
147
|
+
};
|
|
148
|
+
return map[ext] ?? 'application/octet-stream';
|
|
149
|
+
}
|
package/dist/commands/misc.js
CHANGED
|
@@ -6,6 +6,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
6
6
|
import { api } from '../api.js';
|
|
7
7
|
import { emit, fail, log } from '../output.js';
|
|
8
8
|
import { parseKeyValues } from '../args.js';
|
|
9
|
+
import { uploadToLibrary } from './asset.js';
|
|
9
10
|
export async function model(parsed) {
|
|
10
11
|
// 类型只认 --type。位置参数留给关键词:`ssx model search nano` 里的 nano 是要搜的词,
|
|
11
12
|
// 不是模型类型——把它当成 type 会让后端过滤出空列表,而 agent 看到空列表会以为没有可用模型
|
|
@@ -71,6 +72,10 @@ export async function upload(parsed) {
|
|
|
71
72
|
if (!path) {
|
|
72
73
|
fail('要指定文件', '用法:ssx upload ./ref.png');
|
|
73
74
|
}
|
|
75
|
+
// 默认只把文件放上 OSS 拿个 URL 给画布用;--library 才落成素材库里看得到的条目
|
|
76
|
+
if (parsed.options.library) {
|
|
77
|
+
return uploadToLibrary(parsed, path);
|
|
78
|
+
}
|
|
74
79
|
const abs = resolve(path);
|
|
75
80
|
const bytes = await readFile(abs).catch(() => fail(`读不到文件 ${abs}`));
|
|
76
81
|
const form = new FormData();
|
package/dist/commands/node.js
CHANGED
|
@@ -235,7 +235,7 @@ async function runNode(workflowId, nodeId, wait) {
|
|
|
235
235
|
});
|
|
236
236
|
}
|
|
237
237
|
/** 支持按名字或 id 定位。agent 记名字比记 uuid 容易,这一点对它的可用性影响很大。 */
|
|
238
|
-
async function find(workflowId, ref) {
|
|
238
|
+
export async function find(workflowId, ref) {
|
|
239
239
|
const canvas = await api.get(`/openapi/workflows/${workflowId}`);
|
|
240
240
|
return locate(canvas.nodes, ref);
|
|
241
241
|
}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import { login, logout, whoami } from './commands/login.js';
|
|
|
5
5
|
import { workflow } from './commands/workflow.js';
|
|
6
6
|
import { node } from './commands/node.js';
|
|
7
7
|
import { balance, downloadCommand, generate, model, schema, upload } from './commands/misc.js';
|
|
8
|
-
|
|
8
|
+
import { asset, space } from './commands/asset.js';
|
|
9
|
+
const VERSION = '0.2.1';
|
|
9
10
|
/**
|
|
10
11
|
* 这段 help 的第一读者是 AI Agent,不是人。
|
|
11
12
|
*
|
|
@@ -56,9 +57,17 @@ const HELP = `ssx — SoulSpaceX 命令行工具
|
|
|
56
57
|
ssx model search nano 按名字搜
|
|
57
58
|
|
|
58
59
|
素材
|
|
59
|
-
ssx upload ./ref.png 上传参考图,拿到可用的 URL
|
|
60
|
+
ssx upload ./ref.png 上传参考图,拿到可用的 URL(不进素材库)
|
|
60
61
|
ssx download <url> -o out.png 下载产物到本地
|
|
61
62
|
|
|
63
|
+
素材库(用户在网页上看得到的那个库,只收图片与音频)
|
|
64
|
+
ssx asset save 主视觉 把跑完的节点产物存进素材库,也接裸 URL
|
|
65
|
+
ssx asset save 主视觉 --category scene --name 赛博街道
|
|
66
|
+
ssx asset list --category scene 看库里已有什么,省得重复生成
|
|
67
|
+
ssx upload ./ref.png --library 本地文件直接入库
|
|
68
|
+
ssx space list 素材空间列表,不指定就落默认个人空间
|
|
69
|
+
分类:character 人物 / scene 场景 / item 道具(默认)/ voice 音频
|
|
70
|
+
|
|
62
71
|
其他
|
|
63
72
|
ssx schema 节点字段契约。拼 --set 之前先看这个,别猜字段名
|
|
64
73
|
ssx balance 积分余额
|
|
@@ -105,6 +114,10 @@ async function main() {
|
|
|
105
114
|
return balance();
|
|
106
115
|
case 'generate':
|
|
107
116
|
return generate(parsed);
|
|
117
|
+
case 'asset':
|
|
118
|
+
return asset(parsed);
|
|
119
|
+
case 'space':
|
|
120
|
+
return space(parsed);
|
|
108
121
|
case 'upload':
|
|
109
122
|
return upload(parsed);
|
|
110
123
|
case 'download':
|