museav-cli 3.0.2 → 3.1.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/.github/workflows/publish.yml +14 -0
- package/AGENTS.md +22 -0
- package/CHANGELOG.md +43 -0
- package/README.md +53 -0
- package/dist/commands/skillhub.d.ts +26 -0
- package/dist/commands/skillhub.js +195 -0
- package/dist/index.js +34 -0
- package/package.json +6 -4
- package/src/commands/skillhub.ts +214 -0
- package/src/index.ts +40 -0
- package/test/skillhub.test.mjs +42 -0
|
@@ -41,6 +41,20 @@ jobs:
|
|
|
41
41
|
fi
|
|
42
42
|
echo "版本一致:$PKG"
|
|
43
43
|
|
|
44
|
+
# 防空介绍(2026-08-29 教训):发布卡片的内容取 CHANGELOG.md 顶部一节,
|
|
45
|
+
# npm version patch 不会写 CHANGELOG——不校验的话,卡片会带着上一个版本的旧介绍
|
|
46
|
+
# 发给所有租户群(3.0.2 就是这么把 3.0.1 的 reel-kit 内容发出去的)。
|
|
47
|
+
- name: 校验 CHANGELOG 顶部版本与发版一致
|
|
48
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
49
|
+
run: |
|
|
50
|
+
TOP=$(awk '/^## /{print $2; exit}' CHANGELOG.md)
|
|
51
|
+
PKG=$(node -p "require('./package.json').version")
|
|
52
|
+
if [ "$TOP" != "$PKG" ]; then
|
|
53
|
+
echo "::error::CHANGELOG.md 顶部是 $TOP,与要发的 $PKG 不一致。发版前先写好本版本的更新说明(npm version 不会自动写)。"
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
echo "CHANGELOG 顶部版本一致:$TOP"
|
|
57
|
+
|
|
44
58
|
- name: 冒烟测试(命令能起、版本对得上)
|
|
45
59
|
run: |
|
|
46
60
|
node dist/index.js --version
|
package/AGENTS.md
CHANGED
|
@@ -120,6 +120,28 @@ museav jobs --limit 10 --status failed
|
|
|
120
120
|
museav products
|
|
121
121
|
museav assets
|
|
122
122
|
|
|
123
|
+
# Publish a local Agent Skill to Xiaohongshu SkillHub. NOT the same as `museav skills`
|
|
124
|
+
# (that lists the platform's image-generation skills) — this ships a local SKILL.md
|
|
125
|
+
# directory to 小红书 SkillHub. Packing, device-code auth, upload and submit are all done
|
|
126
|
+
# by Xiaohongshu's own CLI (`redskillhub-upload`, a dependency of this package); museav is
|
|
127
|
+
# just the entry point plus one guard rail.
|
|
128
|
+
#
|
|
129
|
+
# WITHOUT --yes it is a dry run: local pack + validation only, no login, no upload, no
|
|
130
|
+
# submit. That's the default on purpose — a submit is irreversible (the Skill ID is the
|
|
131
|
+
# platform's primary key and cannot be renamed across versions). Add --yes to really submit;
|
|
132
|
+
# an unauthenticated --yes prints a QR code for the user to scan in the Xiaohongshu app.
|
|
133
|
+
#
|
|
134
|
+
# --tag is REQUIRED (the platform demands at least one content tag and there is no sane
|
|
135
|
+
# default). List the live options first — do not hardcode them.
|
|
136
|
+
museav skillhub tags
|
|
137
|
+
museav skillhub publish ./my-skill --tag 效率工具,编程开发 # dry run, stdout: payload JSON
|
|
138
|
+
museav skillhub publish ./my-skill --tag 效率工具 --yes # real submit
|
|
139
|
+
museav skillhub publish ./my-skill --tag 内容创作 --source repost --repost-source 知乎 --yes
|
|
140
|
+
|
|
141
|
+
# Platform limits worth knowing before you package anything: text-ish extensions only
|
|
142
|
+
# (.md/.js/.py/.json/.sh/.html/.css …; .mjs, .ts, .yaml and images are rejected outright),
|
|
143
|
+
# 10MB per file, 30MB per bundle, and a SKILL.md must exist at the root.
|
|
144
|
+
|
|
123
145
|
# Check who you're logged in as and whether the account is affiliated with a tenant
|
|
124
146
|
# Works for both personal login and apiKey (platform account or tenant). For apiKey:
|
|
125
147
|
# platform-account → 账户: nickname + 邮箱 + 累计出图 + credits
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.1.0 (2026-09-08)
|
|
4
|
+
|
|
5
|
+
### 发布 Skill 到小红书 SkillHub:`museav skillhub`
|
|
6
|
+
|
|
7
|
+
本地写好的 Agent Skill 现在可以一条命令发到小红书 SkillHub,不用再单独记一套官方 CLI:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
museav skillhub tags # 查可选内容标签
|
|
11
|
+
museav skillhub publish ./my-skill --tag 效率工具,编程开发 # 预演,不外发
|
|
12
|
+
museav skillhub publish ./my-skill --tag 效率工具 --yes # 真提交(扫码授权)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- 打包、设备码授权、上传、提交全部委托小红书官方 `redskillhub-upload`(已作为依赖内置),
|
|
16
|
+
**不重写平台协议**,平台改版跟着升依赖即可
|
|
17
|
+
- **不带 `--yes` 一定不外发**:默认只跑 dry-run,把名称 / Skill ID / 版本 / 简介 / 来源 / 标签
|
|
18
|
+
摊出来给人核对。提交不可逆——Skill ID 是平台主键,跨版本不可改名
|
|
19
|
+
- `--tag` 强制必填(平台要求且无合理默认),`--source` 默认 `original`;转载必须带
|
|
20
|
+
`--repost-source`,原创带了会被拒
|
|
21
|
+
- 跟 `museav skills` 是两件事:那个查中台出图技能,这个发 Agent Skill
|
|
22
|
+
|
|
23
|
+
> 注意平台只收文本类扩展名(`.md` / `.js` / `.py` / `.json` / `.sh` 等),`.mjs`、`.ts`、
|
|
24
|
+
> `.yaml` 一律被拒;单文件 10MB、整包 30MB。
|
|
25
|
+
|
|
26
|
+
## 3.0.3 (2026-08-29)
|
|
27
|
+
|
|
28
|
+
### 批量出图:`gen --batch`,一次提交一批,不用再写循环
|
|
29
|
+
|
|
30
|
+
电商套图、海报批量这种「一口气 N 张」的场景,原来要自己逐张提交、自己处理限流退避。
|
|
31
|
+
现在把每条写进一个文件,一行一条,中台排队消化:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
museav gen --batch prompts.txt --skill xhs-cover
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- 每行一条(配合 `--skill` / `--template` 时是业务描述,否则是完整提示词);
|
|
38
|
+
`#` 注释行和空行跳过,`--batch -` 读 stdin
|
|
39
|
+
- `--ratio` / `--ref` / `--transparent` / `--project` 等选项作为公共参数作用于每一行
|
|
40
|
+
- 撞限流自动等冷却后补交剩余项,**已提交的不会重复扣费**
|
|
41
|
+
- 完成后按行序把图片 URL 逐行输出,方便接管道
|
|
42
|
+
|
|
43
|
+
> 3.0.2 的发布卡片误带了 3.0.1 的旧介绍(发版时 CHANGELOG 未更新),本版一并修正。
|
|
44
|
+
> 功能本体在 3.0.2 已可用,3.0.3 无代码变更。
|
|
45
|
+
|
|
3
46
|
## 3.0.1 (2026-08-28)
|
|
4
47
|
|
|
5
48
|
### 把 reel-kit 的推荐位做实
|
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@
|
|
|
105
105
|
| **语音**(直连小米 MiMo,需 `MIMO_API_KEY`) | 文字转语音 / 音色设计 / 音色克隆 | `speak` |
|
|
106
106
|
| | 语音转文字 | `transcribe` |
|
|
107
107
|
| **素材与统计** | 上传素材 / 查任务 / 查余额 / 查模型 / 查身份 | `upload` / `jobs` / `balance` / `models` / `whoami` |
|
|
108
|
+
| **发布**(小红书 SkillHub) | 把本地 Agent Skill 发到小红书 SkillHub | `skillhub publish` |
|
|
108
109
|
|
|
109
110
|
> 本地工具(`compress` / `remove-bg` / `upscale` / `remove-watermark`)**不用登录、不花一分钱**,装了就能用;其余命令需要一个凭证(个人 `login` 或租户 apiKey)。
|
|
110
111
|
>
|
|
@@ -553,6 +554,56 @@ IMG=$(museav products | node -e "process.stdin.once('data',d=>console.log(JSON.p
|
|
|
553
554
|
museav gen --template <模板id> --ref "$IMG"
|
|
554
555
|
```
|
|
555
556
|
|
|
557
|
+
### 发布 Skill 到小红书 SkillHub `skillhub`
|
|
558
|
+
|
|
559
|
+
> 2026-09-08 新增。跟 `museav skills` **不是一回事**:`skills` 查的是中台的出图技能,
|
|
560
|
+
> `skillhub` 是把本地写好的 Agent Skill(`SKILL.md` 那种)发到小红书 SkillHub。
|
|
561
|
+
|
|
562
|
+
打包、扫码授权、上传、提交全部由小红书官方 CLI [`redskillhub-upload`](https://www.npmjs.com/package/redskillhub-upload)
|
|
563
|
+
完成(它是本包的依赖,装 museav 就带上了,不用另外全局装)。这里只做入口和护栏,
|
|
564
|
+
**不重写平台协议**——平台改版跟着升依赖就行。
|
|
565
|
+
|
|
566
|
+
```bash
|
|
567
|
+
# 1. 先看可选内容标签(发布必须带,没有默认值)
|
|
568
|
+
museav skillhub tags
|
|
569
|
+
|
|
570
|
+
# 2. 预演:只本地打包 + 校验,不登录、不上传、不提交
|
|
571
|
+
museav skillhub publish ./my-skill --tag 效率工具,编程开发
|
|
572
|
+
|
|
573
|
+
# 3. 核对无误再真提交(未登录会出二维码,用小红书 App 扫)
|
|
574
|
+
museav skillhub publish ./my-skill --tag 效率工具,编程开发 --yes
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**不带 `--yes` 一定不会外发**——它只跑 dry-run 把待提交内容摊给你看:
|
|
578
|
+
|
|
579
|
+
```
|
|
580
|
+
待提交内容:
|
|
581
|
+
名称 museav-gen
|
|
582
|
+
Skill ID museav-gen ← 平台主键,提交后跨版本不可改
|
|
583
|
+
版本 0.1.0
|
|
584
|
+
简介 用 museav CLI 在命令行出图、出视频、读图逆向提示词。
|
|
585
|
+
来源 原创
|
|
586
|
+
标签 效率工具,编程开发
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
转载稿要多带一个来源,原创稿不许带:
|
|
590
|
+
|
|
591
|
+
```bash
|
|
592
|
+
museav skillhub publish ./my-skill --tag 内容创作 --source repost --repost-source 知乎 --yes
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
其他:`--identifier <kebab-case>` 显式指定 Skill ID(不传由官方 CLI 从名称/目录名派生,
|
|
596
|
+
派生不出来会报错);`museav skillhub whoami` 查登录态,`skillhub logout` 清凭证,
|
|
597
|
+
`skillhub login --cancel` 取消等待中的扫码。
|
|
598
|
+
|
|
599
|
+
几个平台限制,踩之前先知道:
|
|
600
|
+
|
|
601
|
+
- **只收文本类文件**:`.md` / `.js` / `.py` / `.json` / `.sh` / `.html` / `.css` 等;
|
|
602
|
+
`.mjs`、`.ts`、`.yaml`、图片都会被拒(报「目录中包含不支持上传的文件」)
|
|
603
|
+
- 单文件 10MB、整包 30MB 上限
|
|
604
|
+
- 目录里必须有 `SKILL.md`
|
|
605
|
+
- **Skill ID 是平台主键,提交后跨版本不可改名**,第一次提交前想清楚
|
|
606
|
+
|
|
556
607
|
## 编程调用
|
|
557
608
|
|
|
558
609
|
CLI 背后是一个干净的 `StudioClient` class,也可以当库用:
|
|
@@ -593,6 +644,8 @@ console.log(r.sculpt.light) // 光影分析
|
|
|
593
644
|
| `whoami` | 查当前账户 + 租户归属(仅个人 login) | JSON |
|
|
594
645
|
| `gen` | 出图 / 出视频(`--prompt` / `--skill` / `--template` 三选一;`--video` 切视频,`--image` 图生视频) | 图片/视频 URL |
|
|
595
646
|
| `skills` | 查可用技能:私有 + 租户专属 + 公共库(配合 `gen --skill`) | slug 列表(每行一个) |
|
|
647
|
+
| `skillhub tags` | 查小红书 SkillHub 的内容标签(发布必须带 `--tag`) | 中文标签名(每行一个) |
|
|
648
|
+
| `skillhub publish <path>` | 发布本地 Skill 到小红书 SkillHub;默认只预演,`--yes` 才真提交 | 待提交载荷 JSON |
|
|
596
649
|
| `templates` | 查可用图片模板(配合 `gen --template`) | JSON |
|
|
597
650
|
| `templates create` | 新建图片模板,归属按账号身份自动关联租户 | 新模板 id |
|
|
598
651
|
| `products` | 查所属租户自己的产品目录(数据在租户自己后台,非中台;仅租户 apiKey) | JSON |
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface SkillhubPublishOptions {
|
|
2
|
+
source?: string;
|
|
3
|
+
tag?: string;
|
|
4
|
+
repostSource?: string;
|
|
5
|
+
identifier?: string;
|
|
6
|
+
yes?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* 拼官方 publish 参数。抽成纯函数是为了能断言——参数拼错的代价是把错的
|
|
10
|
+
* source/tag 提交到平台,而 Skill ID 提交后跨版本不可改。
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildPublishArgs(skillPath: string, opts: SkillhubPublishOptions): string[];
|
|
13
|
+
/** museav skillhub tags —— 列平台实时内容标签。stdout 出中文名,一行一个,好接管道 */
|
|
14
|
+
export declare function skillhubTags(): Promise<void>;
|
|
15
|
+
/** museav skillhub whoami / logout / login [--cancel] —— 原样透传给官方 CLI */
|
|
16
|
+
export declare function skillhubPassthrough(command: 'whoami' | 'logout' | 'login', opts?: {
|
|
17
|
+
cancel?: boolean;
|
|
18
|
+
}): Promise<void>;
|
|
19
|
+
/**
|
|
20
|
+
* museav skillhub publish —— 默认只做 dry-run(不登录、不上传、不提交),
|
|
21
|
+
* 把待提交载荷摊给人看;确认无误再加 --yes 真提交。
|
|
22
|
+
*
|
|
23
|
+
* 提交是外发且不可逆(Skill ID 是平台主键,跨版本不可改名),所以护栏不省:
|
|
24
|
+
* 没有 --yes 一律不出网提交。
|
|
25
|
+
*/
|
|
26
|
+
export declare function skillhubPublish(skillPath: string, opts: SkillhubPublishOptions): Promise<void>;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* museav skillhub —— 把本地 Skill 发布到小红书 SkillHub。
|
|
3
|
+
*
|
|
4
|
+
* 这里只做**薄封装**:打包、设备码授权、上传、提交全部由官方 CLI
|
|
5
|
+
* `redskillhub-upload`(本包的 dependency)完成,我们不复制它的任何协议。
|
|
6
|
+
* 平台改版时升这条 dependency 就行,不用动这个文件。
|
|
7
|
+
*
|
|
8
|
+
* 官方 CLI 的输出契约(cli/output.mjs):stdout 逐行 `RESULT_JSON:{...}` /
|
|
9
|
+
* `PROMPT:{...}` / `UPLOAD_PROGRESS:<n>`。我们只挑最后一行 RESULT_JSON 出摘要。
|
|
10
|
+
*
|
|
11
|
+
* 两个不能省的坑:
|
|
12
|
+
* 1. 官方 `--yes` 是**死 flag**——只在 parseArgs 里被赋值,全流程没有消费点。
|
|
13
|
+
* 要免交互提交,只能往 stdin 推 `submit\n`(官方 SKILL.md 也是这个口径)。
|
|
14
|
+
* 2. 缺 `--source` / `--tag` 时官方会转成交互提问读 stdin,和我们推的 `submit`
|
|
15
|
+
* 抢同一条管道。所以这两个参数在本层就补齐/校验,绝不让它进交互分支。
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import { spawn } from 'node:child_process';
|
|
19
|
+
/** dry-run 只是本地打包 + 校验,两分钟够;真提交要等扫码授权,给 15 分钟 */
|
|
20
|
+
const DRY_RUN_TIMEOUT_MS = 120_000;
|
|
21
|
+
const PUBLISH_TIMEOUT_MS = 900_000;
|
|
22
|
+
/**
|
|
23
|
+
* 定位官方 CLI 入口。走 createRequire 解析包内文件,而不是指望 PATH 上有
|
|
24
|
+
* `redskillhub-upload` 命令——它是我们的 dependency,不要求用户全局装过。
|
|
25
|
+
* 后面用 process.execPath 直接跑这个 .mjs,也就绕开了 .bin shim 与 Windows
|
|
26
|
+
* 的 .cmd 差异(AGENTS.md 的跨平台约定)。
|
|
27
|
+
*/
|
|
28
|
+
function cliEntry() {
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
30
|
+
try {
|
|
31
|
+
return require.resolve('redskillhub-upload/cli/index.mjs');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new Error('找不到官方上传 CLI(redskillhub-upload)。重装一次 museav 即可带上:npm i -g museav-cli');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 拼官方 publish 参数。抽成纯函数是为了能断言——参数拼错的代价是把错的
|
|
39
|
+
* source/tag 提交到平台,而 Skill ID 提交后跨版本不可改。
|
|
40
|
+
*/
|
|
41
|
+
export function buildPublishArgs(skillPath, opts) {
|
|
42
|
+
const tag = (opts.tag || '').trim();
|
|
43
|
+
if (!tag) {
|
|
44
|
+
throw new Error('缺 --tag:平台要求至少一个内容标签,且没有合理默认值。\n' +
|
|
45
|
+
'先看可选标签: museav skillhub tags\n' +
|
|
46
|
+
'再带上,多个用逗号分隔: museav skillhub publish <目录> --tag 效率工具,内容创作');
|
|
47
|
+
}
|
|
48
|
+
// 默认原创;转载必须说清来源,否则平台侧校验会拒
|
|
49
|
+
const source = (opts.source || 'original').trim();
|
|
50
|
+
if (source !== 'original' && source !== 'repost') {
|
|
51
|
+
throw new Error(`--source 只能是 original(原创)或 repost(转载),收到「${source}」`);
|
|
52
|
+
}
|
|
53
|
+
const repostSource = (opts.repostSource || '').trim();
|
|
54
|
+
if (source === 'repost' && !repostSource) {
|
|
55
|
+
throw new Error('--source repost 必须同时给 --repost-source <来源平台名>(15 字以内)');
|
|
56
|
+
}
|
|
57
|
+
if (source === 'original' && repostSource) {
|
|
58
|
+
throw new Error('--repost-source 只在 --source repost 时有意义,原创稿不要带');
|
|
59
|
+
}
|
|
60
|
+
// --agent 是官方验证过的调用形态(SKILL.md Step 2/3),让它走结构化输出而不是彩色 TUI
|
|
61
|
+
const args = [cliEntry(), 'publish', skillPath, '--agent', '--source', source, '--tag', tag];
|
|
62
|
+
if (repostSource)
|
|
63
|
+
args.push('--repost-source', repostSource);
|
|
64
|
+
if (opts.identifier)
|
|
65
|
+
args.push('--identifier', String(opts.identifier).trim());
|
|
66
|
+
if (!opts.yes)
|
|
67
|
+
args.push('--dry-run');
|
|
68
|
+
return args;
|
|
69
|
+
}
|
|
70
|
+
/** 取最后一行 RESULT_JSON 的载荷;官方在 dry-run/提交/取消/报错时都会写这一行 */
|
|
71
|
+
function lastResultJson(stdout) {
|
|
72
|
+
const line = stdout.split('\n').filter((l) => l.startsWith('RESULT_JSON:')).at(-1);
|
|
73
|
+
if (!line)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
return JSON.parse(line.slice('RESULT_JSON:'.length));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 跑官方 CLI。capture=true 时收走两条流供解析(成功的 RESULT_JSON 在 stdout,
|
|
84
|
+
* 失败的在 stderr——见官方 index.mjs 顶层 catch),否则原样透传给终端(二维码/进度要人看见)。
|
|
85
|
+
* stderr 即使被 capture 也照样转发出去,不然人看不到失败原文。
|
|
86
|
+
*/
|
|
87
|
+
function runCli(args, { capture, feedStdin, timeoutMs }) {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const child = spawn(process.execPath, args, {
|
|
90
|
+
stdio: [feedStdin ? 'pipe' : 'ignore', capture ? 'pipe' : 'inherit', capture ? 'pipe' : 'inherit'],
|
|
91
|
+
timeout: timeoutMs,
|
|
92
|
+
killSignal: 'SIGTERM',
|
|
93
|
+
});
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
child.stdout?.on('data', (b) => {
|
|
97
|
+
stdout += b.toString();
|
|
98
|
+
});
|
|
99
|
+
child.stderr?.on('data', (b) => {
|
|
100
|
+
stderr += b.toString();
|
|
101
|
+
process.stderr.write(b);
|
|
102
|
+
});
|
|
103
|
+
if (feedStdin) {
|
|
104
|
+
// confirm 阶段之前官方不读 stdin,先写进管道等它来取即可
|
|
105
|
+
child.stdin.on('error', () => { });
|
|
106
|
+
child.stdin.end(feedStdin);
|
|
107
|
+
}
|
|
108
|
+
child.on('error', reject);
|
|
109
|
+
child.on('close', (code, signal) => {
|
|
110
|
+
if (signal) {
|
|
111
|
+
reject(new Error(`官方 CLI 超时被终止(${Math.round(timeoutMs / 1000)}s,signal=${signal})`));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
/** museav skillhub tags —— 列平台实时内容标签。stdout 出中文名,一行一个,好接管道 */
|
|
119
|
+
export async function skillhubTags() {
|
|
120
|
+
const { code, stdout, stderr } = await runCli([cliEntry(), 'tags'], { capture: true, timeoutMs: 30_000 });
|
|
121
|
+
const result = lastResultJson(stdout + stderr);
|
|
122
|
+
if (code !== 0 || !result || result.status !== 'ok') {
|
|
123
|
+
throw new Error(`拉标签失败:${result?.message || `官方 CLI 退出码 ${code}`}`);
|
|
124
|
+
}
|
|
125
|
+
const tags = result.tags || [];
|
|
126
|
+
if (!tags.length) {
|
|
127
|
+
process.stderr.write('平台没有返回可选标签\n');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
process.stderr.write(`SkillHub 内容标签(${tags.length} 个):\n`);
|
|
131
|
+
for (const t of tags)
|
|
132
|
+
process.stderr.write(` ${String(t.name || '').padEnd(16)} ${t.tagId ?? ''}\n`);
|
|
133
|
+
process.stderr.write('\n发布: museav skillhub publish <目录> --tag <中文名[,中文名...]>\n');
|
|
134
|
+
console.log(tags.map((t) => t.name).filter(Boolean).join('\n'));
|
|
135
|
+
}
|
|
136
|
+
/** museav skillhub whoami / logout / login [--cancel] —— 原样透传给官方 CLI */
|
|
137
|
+
export async function skillhubPassthrough(command, opts = {}) {
|
|
138
|
+
const args = [cliEntry(), command];
|
|
139
|
+
if (command === 'login' && opts.cancel)
|
|
140
|
+
args.push('--cancel');
|
|
141
|
+
// login 要出二维码给人扫,不能 capture
|
|
142
|
+
const capture = command !== 'login';
|
|
143
|
+
const { code, stdout } = await runCli(args, {
|
|
144
|
+
capture,
|
|
145
|
+
timeoutMs: command === 'login' ? PUBLISH_TIMEOUT_MS : 30_000,
|
|
146
|
+
});
|
|
147
|
+
if (capture && stdout)
|
|
148
|
+
process.stdout.write(stdout);
|
|
149
|
+
if (code !== 0)
|
|
150
|
+
throw new Error(`官方 CLI ${command} 退出码 ${code}`);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* museav skillhub publish —— 默认只做 dry-run(不登录、不上传、不提交),
|
|
154
|
+
* 把待提交载荷摊给人看;确认无误再加 --yes 真提交。
|
|
155
|
+
*
|
|
156
|
+
* 提交是外发且不可逆(Skill ID 是平台主键,跨版本不可改名),所以护栏不省:
|
|
157
|
+
* 没有 --yes 一律不出网提交。
|
|
158
|
+
*/
|
|
159
|
+
export async function skillhubPublish(skillPath, opts) {
|
|
160
|
+
const args = buildPublishArgs(skillPath, opts);
|
|
161
|
+
if (!opts.yes) {
|
|
162
|
+
process.stderr.write(`预演发布 ${skillPath}(dry-run,不上传不提交)...\n`);
|
|
163
|
+
const { code, stdout, stderr } = await runCli(args, { capture: true, timeoutMs: DRY_RUN_TIMEOUT_MS });
|
|
164
|
+
const result = lastResultJson(stdout + stderr);
|
|
165
|
+
if (code !== 0 || !result || result.status === 'error') {
|
|
166
|
+
const why = result?.message || `官方 CLI 退出码 ${code}`;
|
|
167
|
+
// 官方只说「移除后重试」,不说什么能留。白名单是它的规则(pack.mjs),不在这里复制一份,
|
|
168
|
+
// 只点一句最常踩的:.mjs / .ts / .yaml 这些都不在名单里。
|
|
169
|
+
const hint = why.includes('不支持上传的文件')
|
|
170
|
+
? '\n提示:平台只收文本类扩展名(.md / .js / .py / .json / .sh 等),.mjs、.ts、.yaml 都不在名单里——改扩展名或把该文件移出 skill 目录'
|
|
171
|
+
: '';
|
|
172
|
+
throw new Error(`预演失败:${why}${hint}`);
|
|
173
|
+
}
|
|
174
|
+
const p = result.payload || {};
|
|
175
|
+
process.stderr.write('待提交内容:\n');
|
|
176
|
+
process.stderr.write(` 名称 ${p.name ?? ''}\n`);
|
|
177
|
+
process.stderr.write(` Skill ID ${p.skill_identifier ?? ''} ← 平台主键,提交后跨版本不可改\n`);
|
|
178
|
+
process.stderr.write(` 版本 ${p.version ?? ''}\n`);
|
|
179
|
+
process.stderr.write(` 简介 ${p.description ?? ''}\n`);
|
|
180
|
+
process.stderr.write(` 来源 ${p.original ? '原创' : `转载 · ${p.repost_source || ''}`}\n`);
|
|
181
|
+
process.stderr.write(` 标签 ${opts.tag}\n`);
|
|
182
|
+
process.stderr.write(`\n核对无误后真提交(会要求小红书 App 扫码授权):\n`);
|
|
183
|
+
process.stderr.write(` museav skillhub publish ${skillPath} --tag ${opts.tag} --yes\n`);
|
|
184
|
+
console.log(JSON.stringify(p));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
process.stderr.write(`发布 ${skillPath} 到 SkillHub...\n`);
|
|
188
|
+
process.stderr.write('未登录会先出二维码,用小红书 App 扫一下;授权后同一进程继续上传\n');
|
|
189
|
+
// 官方 --yes 是死 flag,免交互提交只能推 stdin;参数已在 buildPublishArgs 里补齐,
|
|
190
|
+
// 不会有别的提问来抢这个 submit
|
|
191
|
+
const { code } = await runCli(args, { capture: false, feedStdin: 'submit\n', timeoutMs: PUBLISH_TIMEOUT_MS });
|
|
192
|
+
if (code !== 0)
|
|
193
|
+
throw new Error(`发布失败(官方 CLI 退出码 ${code}),上面的 RESULT_JSON 有具体原因`);
|
|
194
|
+
process.stderr.write('✅ 已提交,等平台审核\n');
|
|
195
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { imageToTemplate } from './commands/image-to-template.js';
|
|
|
21
21
|
import { upload } from './commands/upload.js';
|
|
22
22
|
import { models } from './commands/models.js';
|
|
23
23
|
import { skills } from './commands/skills.js';
|
|
24
|
+
import { skillhubTags, skillhubPublish, skillhubPassthrough } from './commands/skillhub.js';
|
|
24
25
|
import { templates, createTemplate } from './commands/templates.js';
|
|
25
26
|
import { videoTemplates, createVideoTemplate } from './commands/video-templates.js';
|
|
26
27
|
import { balance } from './commands/balance.js';
|
|
@@ -313,6 +314,39 @@ program
|
|
|
313
314
|
.description('查可用技能:自己的私有技能 + 所属租户专属模板 + 公共技能库')
|
|
314
315
|
.option('--genre <name>', '按分类过滤,如 电商 / 人像写真')
|
|
315
316
|
.action(withClient((client, opts) => skills(client, opts)));
|
|
317
|
+
// skillhub 跟上面的 skills 是两回事:skills 查的是中台出图技能,skillhub 是把
|
|
318
|
+
// 本地 Agent Skill 发到小红书 SkillHub。打包/授权/上传/提交全由官方
|
|
319
|
+
// redskillhub-upload 完成,这里只是入口 + 提交护栏,见 commands/skillhub.ts 顶部注释。
|
|
320
|
+
const skillhubCmd = program
|
|
321
|
+
.command('skillhub')
|
|
322
|
+
.description('把本地 Skill 发布到小红书 SkillHub(不带子命令时列可选内容标签)')
|
|
323
|
+
.action(asyncRun(() => skillhubTags()));
|
|
324
|
+
skillhubCmd
|
|
325
|
+
.command('tags')
|
|
326
|
+
.description('列平台实时内容标签(发布必须带 --tag,没有默认值)')
|
|
327
|
+
.action(asyncRun(() => skillhubTags()));
|
|
328
|
+
skillhubCmd
|
|
329
|
+
.command('publish <path>')
|
|
330
|
+
.description('发布本地 Skill 目录或 .zip 源包。默认只预演(dry-run,不登录不上传),加 --yes 才真提交')
|
|
331
|
+
.requiredOption('--tag <中文名>', '内容标签,多个用逗号分隔;清单见 museav skillhub tags')
|
|
332
|
+
.option('--source <type>', '内容来源:original(原创,默认) / repost(转载)', 'original')
|
|
333
|
+
.option('--repost-source <来源>', '转载来源平台名(15 字以内),--source repost 时必填')
|
|
334
|
+
.option('--identifier <id>', 'Skill ID(kebab-case,平台主键,跨版本不可改);不传由官方 CLI 从名称/目录名派生')
|
|
335
|
+
.option('--yes', '真提交(会要求用小红书 App 扫码授权);不带则只预演')
|
|
336
|
+
.action(asyncRun((path, opts) => skillhubPublish(path, opts)));
|
|
337
|
+
skillhubCmd
|
|
338
|
+
.command('whoami')
|
|
339
|
+
.description('查 SkillHub 登录态(脱敏)')
|
|
340
|
+
.action(asyncRun(() => skillhubPassthrough('whoami')));
|
|
341
|
+
skillhubCmd
|
|
342
|
+
.command('login')
|
|
343
|
+
.description('显式预登录(正常发布会自动登录,这条只用于诊断)')
|
|
344
|
+
.option('--cancel', '取消等待中的授权,并清理待授权状态与二维码')
|
|
345
|
+
.action(asyncRun((opts) => skillhubPassthrough('login', opts)));
|
|
346
|
+
skillhubCmd
|
|
347
|
+
.command('logout')
|
|
348
|
+
.description('退出 SkillHub 登录(清除本地凭证)')
|
|
349
|
+
.action(asyncRun(() => skillhubPassthrough('logout')));
|
|
316
350
|
const templatesCmd = program
|
|
317
351
|
.command('templates')
|
|
318
352
|
.description('查可用图片/文字模板:自己租户建的 + 平台共享的(跟技能是两套不同的机制,见 gen --template)')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "museav-cli",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "MUSE AV 出图中台官方 CLI —— 命令行调中台 API 出图、出视频、读图逆向、图生模板",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsc",
|
|
20
20
|
"dev": "tsx src/index.ts",
|
|
21
|
-
"prepublishOnly": "npm run build"
|
|
21
|
+
"prepublishOnly": "npm run build",
|
|
22
|
+
"test": "node --test test/*.test.mjs"
|
|
22
23
|
},
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=20.19"
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
"license": "MIT",
|
|
40
41
|
"dependencies": {
|
|
41
42
|
"commander": "^14.0.3",
|
|
43
|
+
"redskillhub-upload": "^1.0.2",
|
|
42
44
|
"update-notifier": "^7.3.1"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
"access": "public"
|
|
54
56
|
},
|
|
55
57
|
"optionalDependencies": {
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
+
"onnxruntime-node": "^1.27.0",
|
|
59
|
+
"sharp": "^0.35.3"
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* museav skillhub —— 把本地 Skill 发布到小红书 SkillHub。
|
|
3
|
+
*
|
|
4
|
+
* 这里只做**薄封装**:打包、设备码授权、上传、提交全部由官方 CLI
|
|
5
|
+
* `redskillhub-upload`(本包的 dependency)完成,我们不复制它的任何协议。
|
|
6
|
+
* 平台改版时升这条 dependency 就行,不用动这个文件。
|
|
7
|
+
*
|
|
8
|
+
* 官方 CLI 的输出契约(cli/output.mjs):stdout 逐行 `RESULT_JSON:{...}` /
|
|
9
|
+
* `PROMPT:{...}` / `UPLOAD_PROGRESS:<n>`。我们只挑最后一行 RESULT_JSON 出摘要。
|
|
10
|
+
*
|
|
11
|
+
* 两个不能省的坑:
|
|
12
|
+
* 1. 官方 `--yes` 是**死 flag**——只在 parseArgs 里被赋值,全流程没有消费点。
|
|
13
|
+
* 要免交互提交,只能往 stdin 推 `submit\n`(官方 SKILL.md 也是这个口径)。
|
|
14
|
+
* 2. 缺 `--source` / `--tag` 时官方会转成交互提问读 stdin,和我们推的 `submit`
|
|
15
|
+
* 抢同一条管道。所以这两个参数在本层就补齐/校验,绝不让它进交互分支。
|
|
16
|
+
*/
|
|
17
|
+
import { createRequire } from 'node:module'
|
|
18
|
+
import { spawn } from 'node:child_process'
|
|
19
|
+
|
|
20
|
+
/** dry-run 只是本地打包 + 校验,两分钟够;真提交要等扫码授权,给 15 分钟 */
|
|
21
|
+
const DRY_RUN_TIMEOUT_MS = 120_000
|
|
22
|
+
const PUBLISH_TIMEOUT_MS = 900_000
|
|
23
|
+
|
|
24
|
+
export interface SkillhubPublishOptions {
|
|
25
|
+
source?: string
|
|
26
|
+
tag?: string
|
|
27
|
+
repostSource?: string
|
|
28
|
+
identifier?: string
|
|
29
|
+
yes?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 定位官方 CLI 入口。走 createRequire 解析包内文件,而不是指望 PATH 上有
|
|
34
|
+
* `redskillhub-upload` 命令——它是我们的 dependency,不要求用户全局装过。
|
|
35
|
+
* 后面用 process.execPath 直接跑这个 .mjs,也就绕开了 .bin shim 与 Windows
|
|
36
|
+
* 的 .cmd 差异(AGENTS.md 的跨平台约定)。
|
|
37
|
+
*/
|
|
38
|
+
function cliEntry(): string {
|
|
39
|
+
const require = createRequire(import.meta.url)
|
|
40
|
+
try {
|
|
41
|
+
return require.resolve('redskillhub-upload/cli/index.mjs')
|
|
42
|
+
} catch {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'找不到官方上传 CLI(redskillhub-upload)。重装一次 museav 即可带上:npm i -g museav-cli',
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 拼官方 publish 参数。抽成纯函数是为了能断言——参数拼错的代价是把错的
|
|
51
|
+
* source/tag 提交到平台,而 Skill ID 提交后跨版本不可改。
|
|
52
|
+
*/
|
|
53
|
+
export function buildPublishArgs(skillPath: string, opts: SkillhubPublishOptions): string[] {
|
|
54
|
+
const tag = (opts.tag || '').trim()
|
|
55
|
+
if (!tag) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'缺 --tag:平台要求至少一个内容标签,且没有合理默认值。\n' +
|
|
58
|
+
'先看可选标签: museav skillhub tags\n' +
|
|
59
|
+
'再带上,多个用逗号分隔: museav skillhub publish <目录> --tag 效率工具,内容创作',
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 默认原创;转载必须说清来源,否则平台侧校验会拒
|
|
64
|
+
const source = (opts.source || 'original').trim()
|
|
65
|
+
if (source !== 'original' && source !== 'repost') {
|
|
66
|
+
throw new Error(`--source 只能是 original(原创)或 repost(转载),收到「${source}」`)
|
|
67
|
+
}
|
|
68
|
+
const repostSource = (opts.repostSource || '').trim()
|
|
69
|
+
if (source === 'repost' && !repostSource) {
|
|
70
|
+
throw new Error('--source repost 必须同时给 --repost-source <来源平台名>(15 字以内)')
|
|
71
|
+
}
|
|
72
|
+
if (source === 'original' && repostSource) {
|
|
73
|
+
throw new Error('--repost-source 只在 --source repost 时有意义,原创稿不要带')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --agent 是官方验证过的调用形态(SKILL.md Step 2/3),让它走结构化输出而不是彩色 TUI
|
|
77
|
+
const args = [cliEntry(), 'publish', skillPath, '--agent', '--source', source, '--tag', tag]
|
|
78
|
+
if (repostSource) args.push('--repost-source', repostSource)
|
|
79
|
+
if (opts.identifier) args.push('--identifier', String(opts.identifier).trim())
|
|
80
|
+
if (!opts.yes) args.push('--dry-run')
|
|
81
|
+
return args
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 取最后一行 RESULT_JSON 的载荷;官方在 dry-run/提交/取消/报错时都会写这一行 */
|
|
85
|
+
function lastResultJson(stdout: string): any {
|
|
86
|
+
const line = stdout.split('\n').filter((l) => l.startsWith('RESULT_JSON:')).at(-1)
|
|
87
|
+
if (!line) return null
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(line.slice('RESULT_JSON:'.length))
|
|
90
|
+
} catch {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 跑官方 CLI。capture=true 时收走两条流供解析(成功的 RESULT_JSON 在 stdout,
|
|
97
|
+
* 失败的在 stderr——见官方 index.mjs 顶层 catch),否则原样透传给终端(二维码/进度要人看见)。
|
|
98
|
+
* stderr 即使被 capture 也照样转发出去,不然人看不到失败原文。
|
|
99
|
+
*/
|
|
100
|
+
function runCli(
|
|
101
|
+
args: string[],
|
|
102
|
+
{ capture, feedStdin, timeoutMs }: { capture: boolean; feedStdin?: string; timeoutMs: number },
|
|
103
|
+
): Promise<{ code: number; stdout: string; stderr: string }> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const child = spawn(process.execPath, args, {
|
|
106
|
+
stdio: [feedStdin ? 'pipe' : 'ignore', capture ? 'pipe' : 'inherit', capture ? 'pipe' : 'inherit'],
|
|
107
|
+
timeout: timeoutMs,
|
|
108
|
+
killSignal: 'SIGTERM',
|
|
109
|
+
})
|
|
110
|
+
let stdout = ''
|
|
111
|
+
let stderr = ''
|
|
112
|
+
child.stdout?.on('data', (b: Buffer) => {
|
|
113
|
+
stdout += b.toString()
|
|
114
|
+
})
|
|
115
|
+
child.stderr?.on('data', (b: Buffer) => {
|
|
116
|
+
stderr += b.toString()
|
|
117
|
+
process.stderr.write(b)
|
|
118
|
+
})
|
|
119
|
+
if (feedStdin) {
|
|
120
|
+
// confirm 阶段之前官方不读 stdin,先写进管道等它来取即可
|
|
121
|
+
child.stdin!.on('error', () => { /* 进程先退了,忽略断管 */ })
|
|
122
|
+
child.stdin!.end(feedStdin)
|
|
123
|
+
}
|
|
124
|
+
child.on('error', reject)
|
|
125
|
+
child.on('close', (code, signal) => {
|
|
126
|
+
if (signal) {
|
|
127
|
+
reject(new Error(`官方 CLI 超时被终止(${Math.round(timeoutMs / 1000)}s,signal=${signal})`))
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
resolve({ code: code ?? 1, stdout, stderr })
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** museav skillhub tags —— 列平台实时内容标签。stdout 出中文名,一行一个,好接管道 */
|
|
136
|
+
export async function skillhubTags(): Promise<void> {
|
|
137
|
+
const { code, stdout, stderr } = await runCli([cliEntry(), 'tags'], { capture: true, timeoutMs: 30_000 })
|
|
138
|
+
const result = lastResultJson(stdout + stderr)
|
|
139
|
+
if (code !== 0 || !result || result.status !== 'ok') {
|
|
140
|
+
throw new Error(`拉标签失败:${result?.message || `官方 CLI 退出码 ${code}`}`)
|
|
141
|
+
}
|
|
142
|
+
const tags: Array<{ name?: string; tagId?: string | number }> = result.tags || []
|
|
143
|
+
if (!tags.length) {
|
|
144
|
+
process.stderr.write('平台没有返回可选标签\n')
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
process.stderr.write(`SkillHub 内容标签(${tags.length} 个):\n`)
|
|
148
|
+
for (const t of tags) process.stderr.write(` ${String(t.name || '').padEnd(16)} ${t.tagId ?? ''}\n`)
|
|
149
|
+
process.stderr.write('\n发布: museav skillhub publish <目录> --tag <中文名[,中文名...]>\n')
|
|
150
|
+
console.log(tags.map((t) => t.name).filter(Boolean).join('\n'))
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** museav skillhub whoami / logout / login [--cancel] —— 原样透传给官方 CLI */
|
|
154
|
+
export async function skillhubPassthrough(
|
|
155
|
+
command: 'whoami' | 'logout' | 'login',
|
|
156
|
+
opts: { cancel?: boolean } = {},
|
|
157
|
+
): Promise<void> {
|
|
158
|
+
const args = [cliEntry(), command]
|
|
159
|
+
if (command === 'login' && opts.cancel) args.push('--cancel')
|
|
160
|
+
// login 要出二维码给人扫,不能 capture
|
|
161
|
+
const capture = command !== 'login'
|
|
162
|
+
const { code, stdout } = await runCli(args, {
|
|
163
|
+
capture,
|
|
164
|
+
timeoutMs: command === 'login' ? PUBLISH_TIMEOUT_MS : 30_000,
|
|
165
|
+
})
|
|
166
|
+
if (capture && stdout) process.stdout.write(stdout)
|
|
167
|
+
if (code !== 0) throw new Error(`官方 CLI ${command} 退出码 ${code}`)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* museav skillhub publish —— 默认只做 dry-run(不登录、不上传、不提交),
|
|
172
|
+
* 把待提交载荷摊给人看;确认无误再加 --yes 真提交。
|
|
173
|
+
*
|
|
174
|
+
* 提交是外发且不可逆(Skill ID 是平台主键,跨版本不可改名),所以护栏不省:
|
|
175
|
+
* 没有 --yes 一律不出网提交。
|
|
176
|
+
*/
|
|
177
|
+
export async function skillhubPublish(skillPath: string, opts: SkillhubPublishOptions): Promise<void> {
|
|
178
|
+
const args = buildPublishArgs(skillPath, opts)
|
|
179
|
+
|
|
180
|
+
if (!opts.yes) {
|
|
181
|
+
process.stderr.write(`预演发布 ${skillPath}(dry-run,不上传不提交)...\n`)
|
|
182
|
+
const { code, stdout, stderr } = await runCli(args, { capture: true, timeoutMs: DRY_RUN_TIMEOUT_MS })
|
|
183
|
+
const result = lastResultJson(stdout + stderr)
|
|
184
|
+
if (code !== 0 || !result || result.status === 'error') {
|
|
185
|
+
const why = result?.message || `官方 CLI 退出码 ${code}`
|
|
186
|
+
// 官方只说「移除后重试」,不说什么能留。白名单是它的规则(pack.mjs),不在这里复制一份,
|
|
187
|
+
// 只点一句最常踩的:.mjs / .ts / .yaml 这些都不在名单里。
|
|
188
|
+
const hint = why.includes('不支持上传的文件')
|
|
189
|
+
? '\n提示:平台只收文本类扩展名(.md / .js / .py / .json / .sh 等),.mjs、.ts、.yaml 都不在名单里——改扩展名或把该文件移出 skill 目录'
|
|
190
|
+
: ''
|
|
191
|
+
throw new Error(`预演失败:${why}${hint}`)
|
|
192
|
+
}
|
|
193
|
+
const p = result.payload || {}
|
|
194
|
+
process.stderr.write('待提交内容:\n')
|
|
195
|
+
process.stderr.write(` 名称 ${p.name ?? ''}\n`)
|
|
196
|
+
process.stderr.write(` Skill ID ${p.skill_identifier ?? ''} ← 平台主键,提交后跨版本不可改\n`)
|
|
197
|
+
process.stderr.write(` 版本 ${p.version ?? ''}\n`)
|
|
198
|
+
process.stderr.write(` 简介 ${p.description ?? ''}\n`)
|
|
199
|
+
process.stderr.write(` 来源 ${p.original ? '原创' : `转载 · ${p.repost_source || ''}`}\n`)
|
|
200
|
+
process.stderr.write(` 标签 ${opts.tag}\n`)
|
|
201
|
+
process.stderr.write(`\n核对无误后真提交(会要求小红书 App 扫码授权):\n`)
|
|
202
|
+
process.stderr.write(` museav skillhub publish ${skillPath} --tag ${opts.tag} --yes\n`)
|
|
203
|
+
console.log(JSON.stringify(p))
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
process.stderr.write(`发布 ${skillPath} 到 SkillHub...\n`)
|
|
208
|
+
process.stderr.write('未登录会先出二维码,用小红书 App 扫一下;授权后同一进程继续上传\n')
|
|
209
|
+
// 官方 --yes 是死 flag,免交互提交只能推 stdin;参数已在 buildPublishArgs 里补齐,
|
|
210
|
+
// 不会有别的提问来抢这个 submit
|
|
211
|
+
const { code } = await runCli(args, { capture: false, feedStdin: 'submit\n', timeoutMs: PUBLISH_TIMEOUT_MS })
|
|
212
|
+
if (code !== 0) throw new Error(`发布失败(官方 CLI 退出码 ${code}),上面的 RESULT_JSON 有具体原因`)
|
|
213
|
+
process.stderr.write('✅ 已提交,等平台审核\n')
|
|
214
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { imageToTemplate } from './commands/image-to-template.js'
|
|
|
21
21
|
import { upload } from './commands/upload.js'
|
|
22
22
|
import { models } from './commands/models.js'
|
|
23
23
|
import { skills } from './commands/skills.js'
|
|
24
|
+
import { skillhubTags, skillhubPublish, skillhubPassthrough } from './commands/skillhub.js'
|
|
24
25
|
import { templates, createTemplate } from './commands/templates.js'
|
|
25
26
|
import { videoTemplates, createVideoTemplate } from './commands/video-templates.js'
|
|
26
27
|
import { balance } from './commands/balance.js'
|
|
@@ -339,6 +340,45 @@ program
|
|
|
339
340
|
.option('--genre <name>', '按分类过滤,如 电商 / 人像写真')
|
|
340
341
|
.action(withClient((client: StudioClient, opts: any) => skills(client, opts)))
|
|
341
342
|
|
|
343
|
+
// skillhub 跟上面的 skills 是两回事:skills 查的是中台出图技能,skillhub 是把
|
|
344
|
+
// 本地 Agent Skill 发到小红书 SkillHub。打包/授权/上传/提交全由官方
|
|
345
|
+
// redskillhub-upload 完成,这里只是入口 + 提交护栏,见 commands/skillhub.ts 顶部注释。
|
|
346
|
+
const skillhubCmd = program
|
|
347
|
+
.command('skillhub')
|
|
348
|
+
.description('把本地 Skill 发布到小红书 SkillHub(不带子命令时列可选内容标签)')
|
|
349
|
+
.action(asyncRun(() => skillhubTags()))
|
|
350
|
+
|
|
351
|
+
skillhubCmd
|
|
352
|
+
.command('tags')
|
|
353
|
+
.description('列平台实时内容标签(发布必须带 --tag,没有默认值)')
|
|
354
|
+
.action(asyncRun(() => skillhubTags()))
|
|
355
|
+
|
|
356
|
+
skillhubCmd
|
|
357
|
+
.command('publish <path>')
|
|
358
|
+
.description('发布本地 Skill 目录或 .zip 源包。默认只预演(dry-run,不登录不上传),加 --yes 才真提交')
|
|
359
|
+
.requiredOption('--tag <中文名>', '内容标签,多个用逗号分隔;清单见 museav skillhub tags')
|
|
360
|
+
.option('--source <type>', '内容来源:original(原创,默认) / repost(转载)', 'original')
|
|
361
|
+
.option('--repost-source <来源>', '转载来源平台名(15 字以内),--source repost 时必填')
|
|
362
|
+
.option('--identifier <id>', 'Skill ID(kebab-case,平台主键,跨版本不可改);不传由官方 CLI 从名称/目录名派生')
|
|
363
|
+
.option('--yes', '真提交(会要求用小红书 App 扫码授权);不带则只预演')
|
|
364
|
+
.action(asyncRun((path: string, opts: any) => skillhubPublish(path, opts)))
|
|
365
|
+
|
|
366
|
+
skillhubCmd
|
|
367
|
+
.command('whoami')
|
|
368
|
+
.description('查 SkillHub 登录态(脱敏)')
|
|
369
|
+
.action(asyncRun(() => skillhubPassthrough('whoami')))
|
|
370
|
+
|
|
371
|
+
skillhubCmd
|
|
372
|
+
.command('login')
|
|
373
|
+
.description('显式预登录(正常发布会自动登录,这条只用于诊断)')
|
|
374
|
+
.option('--cancel', '取消等待中的授权,并清理待授权状态与二维码')
|
|
375
|
+
.action(asyncRun((opts: any) => skillhubPassthrough('login', opts)))
|
|
376
|
+
|
|
377
|
+
skillhubCmd
|
|
378
|
+
.command('logout')
|
|
379
|
+
.description('退出 SkillHub 登录(清除本地凭证)')
|
|
380
|
+
.action(asyncRun(() => skillhubPassthrough('logout')))
|
|
381
|
+
|
|
342
382
|
const templatesCmd = program
|
|
343
383
|
.command('templates')
|
|
344
384
|
.description('查可用图片/文字模板:自己租户建的 + 平台共享的(跟技能是两套不同的机制,见 gen --template)')
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skillhub 参数拼装自检:这里错的代价是把错的 source/tag 提交到平台,
|
|
3
|
+
* 而 Skill ID 提交后跨版本不可改,所以校验分支必须有断言兜着。
|
|
4
|
+
* 跑: npm test(需要先 npm run build)
|
|
5
|
+
*/
|
|
6
|
+
import { test } from 'node:test'
|
|
7
|
+
import assert from 'node:assert/strict'
|
|
8
|
+
import { buildPublishArgs } from '../dist/commands/skillhub.js'
|
|
9
|
+
|
|
10
|
+
test('缺 tag 直接拒,并指向 tags 命令', () => {
|
|
11
|
+
assert.throws(() => buildPublishArgs('/tmp/s', {}), /缺 --tag[\s\S]*skillhub tags/)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('source 只收 original / repost', () => {
|
|
15
|
+
assert.throws(() => buildPublishArgs('/tmp/s', { tag: '效率工具', source: '原创' }), /只能是 original/)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('转载必须带来源,原创不许带', () => {
|
|
19
|
+
assert.throws(() => buildPublishArgs('/tmp/s', { tag: '效率工具', source: 'repost' }), /repost-source/)
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => buildPublishArgs('/tmp/s', { tag: '效率工具', repostSource: '知乎' }),
|
|
22
|
+
/只在 --source repost 时有意义/,
|
|
23
|
+
)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('默认原创,且不带 --yes 时强制 dry-run(不外发)', () => {
|
|
27
|
+
const args = buildPublishArgs('/tmp/s', { tag: '效率工具,编程开发' })
|
|
28
|
+
assert.deepEqual(args.slice(1), [
|
|
29
|
+
'publish', '/tmp/s', '--agent', '--source', 'original', '--tag', '效率工具,编程开发', '--dry-run',
|
|
30
|
+
])
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test('--yes 才去掉 dry-run,转载参数与 identifier 一并透传', () => {
|
|
34
|
+
const args = buildPublishArgs('/tmp/s', {
|
|
35
|
+
tag: '内容创作', source: 'repost', repostSource: '知乎', identifier: 'my-skill', yes: true,
|
|
36
|
+
})
|
|
37
|
+
assert.ok(!args.includes('--dry-run'))
|
|
38
|
+
assert.deepEqual(args.slice(1), [
|
|
39
|
+
'publish', '/tmp/s', '--agent', '--source', 'repost', '--tag', '内容创作',
|
|
40
|
+
'--repost-source', '知乎', '--identifier', 'my-skill',
|
|
41
|
+
])
|
|
42
|
+
})
|