redskillhub-upload 1.0.2 → 1.0.3-beta.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 +38 -18
- package/cli/config.mjs +1 -1
- package/cli/index.mjs +47 -25
- package/cli/pack.mjs +1 -1
- package/cli/prompt.mjs +31 -22
- package/cli/submit.mjs +47 -2
- package/package.json +4 -3
- package/skill/SKILL.md +101 -81
- package/skill/agents/openai.yaml +2 -2
- package/skill/scripts/ensure-cli.mjs +99 -52
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
CLI 负责完整发布链路:登录授权、本地校验、重新打包、上传、参数确认和最终提交。输入可以是 Skill 目录,也可以是 `.zip` 文件。
|
|
6
6
|
|
|
7
|
+
当前“对话内发布确认与修改”能力仅面向 Beta 验证。本文发布示例都显式使用 `--env beta`;不要将本次改动用于 Prod 发布。
|
|
8
|
+
|
|
7
9
|
## 环境要求
|
|
8
10
|
|
|
9
11
|
- Node.js 18 或更高版本
|
|
@@ -14,14 +16,26 @@ CLI 负责完整发布链路:登录授权、本地校验、重新打包、上
|
|
|
14
16
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
19
|
+
Beta 测试安装:
|
|
20
|
+
|
|
17
21
|
```bash
|
|
18
|
-
npm install -g redskillhub-upload
|
|
22
|
+
npm install -g redskillhub-upload@beta
|
|
19
23
|
```
|
|
20
24
|
|
|
25
|
+
`@beta` 与生产使用的 `@latest` 是两个独立 npm dist-tag。Beta 验收期间不得把测试版本发布到 `latest`;当前包的 `publishConfig.tag` 已固定为 `beta`,避免裸 `npm publish` 误更新生产安装通道。
|
|
26
|
+
|
|
27
|
+
Beta Apollo 使用固定版本门禁策略:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
node "<SKILL_DIR>/scripts/ensure-cli.mjs" --env beta --npm-policy beta-first
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
它会在目标版本更高时优先安装 `@beta`,正式 `@latest` 追平后自动切回正式版。Prod 不使用该策略,始终安装 `@latest`。
|
|
34
|
+
|
|
21
35
|
确认安装成功:
|
|
22
36
|
|
|
23
37
|
```bash
|
|
24
|
-
redskillhub-upload whoami
|
|
38
|
+
redskillhub-upload whoami --env beta
|
|
25
39
|
```
|
|
26
40
|
|
|
27
41
|
## 发布 Skill
|
|
@@ -29,19 +43,19 @@ redskillhub-upload whoami
|
|
|
29
43
|
发布本地目录:
|
|
30
44
|
|
|
31
45
|
```bash
|
|
32
|
-
redskillhub-upload publish "/absolute/path/to/skill"
|
|
46
|
+
redskillhub-upload publish "/absolute/path/to/skill" --env beta
|
|
33
47
|
```
|
|
34
48
|
|
|
35
49
|
发布 ZIP:
|
|
36
50
|
|
|
37
51
|
```bash
|
|
38
|
-
redskillhub-upload publish "/absolute/path/to/skill.zip"
|
|
52
|
+
redskillhub-upload publish "/absolute/path/to/skill.zip" --env beta
|
|
39
53
|
```
|
|
40
54
|
|
|
41
55
|
Agent 模式:
|
|
42
56
|
|
|
43
57
|
```bash
|
|
44
|
-
redskillhub-upload publish "/absolute/path/to/skill" --agent
|
|
58
|
+
redskillhub-upload publish "/absolute/path/to/skill" --agent --env beta
|
|
45
59
|
```
|
|
46
60
|
|
|
47
61
|
`publish` 会自动检查登录状态,无需预先执行 `login` 或 `whoami`。
|
|
@@ -59,31 +73,31 @@ CLI 会复用有效的 access token,并在 refresh token 可用时静默续期
|
|
|
59
73
|
手动登录:
|
|
60
74
|
|
|
61
75
|
```bash
|
|
62
|
-
redskillhub-upload login
|
|
76
|
+
redskillhub-upload login --env beta
|
|
63
77
|
```
|
|
64
78
|
|
|
65
79
|
Agent 模式登录:
|
|
66
80
|
|
|
67
81
|
```bash
|
|
68
|
-
redskillhub-upload login --agent
|
|
82
|
+
redskillhub-upload login --agent --env beta
|
|
69
83
|
```
|
|
70
84
|
|
|
71
85
|
取消正在等待的登录:
|
|
72
86
|
|
|
73
87
|
```bash
|
|
74
|
-
redskillhub-upload login --cancel
|
|
88
|
+
redskillhub-upload login --cancel --env beta
|
|
75
89
|
```
|
|
76
90
|
|
|
77
91
|
查看登录状态:
|
|
78
92
|
|
|
79
93
|
```bash
|
|
80
|
-
redskillhub-upload whoami
|
|
94
|
+
redskillhub-upload whoami --env beta
|
|
81
95
|
```
|
|
82
96
|
|
|
83
97
|
退出登录:
|
|
84
98
|
|
|
85
99
|
```bash
|
|
86
|
-
redskillhub-upload logout
|
|
100
|
+
redskillhub-upload logout --env beta
|
|
87
101
|
```
|
|
88
102
|
|
|
89
103
|
## 发布参数
|
|
@@ -91,41 +105,47 @@ redskillhub-upload logout
|
|
|
91
105
|
查看可用标签:
|
|
92
106
|
|
|
93
107
|
```bash
|
|
94
|
-
redskillhub-upload tags
|
|
108
|
+
redskillhub-upload tags --env beta
|
|
95
109
|
```
|
|
96
110
|
|
|
97
111
|
指定原创内容和标签:
|
|
98
112
|
|
|
99
113
|
```bash
|
|
100
|
-
redskillhub-upload publish "/absolute/path/to/skill" --source original --tag "效率工具,内容创作"
|
|
114
|
+
redskillhub-upload publish "/absolute/path/to/skill" --source original --tag "效率工具,内容创作" --env beta
|
|
101
115
|
```
|
|
102
116
|
|
|
103
117
|
转载内容需要提供来源:
|
|
104
118
|
|
|
105
119
|
```bash
|
|
106
|
-
redskillhub-upload publish "/absolute/path/to/skill" --source repost --repost-source "来源名称" --tag "内容创作"
|
|
120
|
+
redskillhub-upload publish "/absolute/path/to/skill" --source repost --repost-source "来源名称" --tag "内容创作" --env beta
|
|
107
121
|
```
|
|
108
122
|
|
|
109
123
|
只生成并检查待提交数据,不上传或提交:
|
|
110
124
|
|
|
111
125
|
```bash
|
|
112
|
-
redskillhub-upload publish "/absolute/path/to/skill" --dry-run --agent --source original --tag "效率工具"
|
|
126
|
+
redskillhub-upload publish "/absolute/path/to/skill" --dry-run --agent --source original --tag "效率工具" --env beta
|
|
113
127
|
```
|
|
114
128
|
|
|
115
|
-
真实发布前,CLI
|
|
129
|
+
真实发布前,CLI 会展示名称、Skill ID、版本、描述、Skill 介绍、来源、转载来源、场景标签、打包大小和 SHA-256。卡片后会提示作者可直接提出修改;未收到 `submit` 时不会登录、上传或执行最终提交。
|
|
130
|
+
|
|
131
|
+
Agent 展示 Skill 介绍时,会把原始 Markdown 全文放在一个显式标记为 `text` 的代码块中。不能省略 `text`,否则聊天渲染器可能自动识别 Markdown 并着色;也不能使用 `markdown/md/yaml`。这样不会渲染标题和段落,所有正文保持统一单色,以贴近平台编辑器中的最终内容。
|
|
132
|
+
|
|
133
|
+
确认阶段可输入 `edit`,随后逐行输入 `name`、`description`、`version`、`detail`、`identifier`、`tag`、`source` 或 `repost_source` 的 `key=value`,最后输入空行。多行 `detail` 使用 JSON 字符串。所有修改只保存在当前发布会话的提交信息中,不写回本地文件;每次修改后都会展示新的完整确认卡。
|
|
134
|
+
|
|
135
|
+
目录输入和 zip 输入都保持不变,不会生成 `-updated.zip`。由于 bundle 内容不变,修改提交信息后打包大小和 SHA-256 也保持不变。
|
|
116
136
|
|
|
117
137
|
## Windows 示例
|
|
118
138
|
|
|
119
139
|
PowerShell 和 cmd 中均使用双引号包裹路径:
|
|
120
140
|
|
|
121
141
|
```powershell
|
|
122
|
-
redskillhub-upload publish "C:\Users\name\path\to\skill" --agent
|
|
142
|
+
redskillhub-upload publish "C:\Users\name\path\to\skill" --agent --env beta
|
|
123
143
|
```
|
|
124
144
|
|
|
125
145
|
向 Agent 模式写入确认指令:
|
|
126
146
|
|
|
127
147
|
```powershell
|
|
128
|
-
node -e "process.stdout.write('submit\n')" | redskillhub-upload publish "C:\Users\name\path\to\skill" --agent --source original --tag "效率工具"
|
|
148
|
+
node -e "process.stdout.write('submit\n')" | redskillhub-upload publish "C:\Users\name\path\to\skill" --agent --source original --tag "效率工具" --env beta
|
|
129
149
|
```
|
|
130
150
|
|
|
131
151
|
## Agent 输出协议
|
|
@@ -161,5 +181,5 @@ npm pack --dry-run
|
|
|
161
181
|
执行 dry-run:
|
|
162
182
|
|
|
163
183
|
```bash
|
|
164
|
-
node cli/index.mjs publish "/absolute/path/to/skill" --dry-run --agent --source original --tag-id 101,102 --
|
|
184
|
+
node cli/index.mjs publish "/absolute/path/to/skill" --dry-run --agent --source original --tag-id 101,102 --env beta
|
|
165
185
|
```
|
package/cli/config.mjs
CHANGED
|
@@ -5,7 +5,7 @@ export const DEFAULT_API_BASE = 'https://edith.xiaohongshu.com';
|
|
|
5
5
|
export const BETA_API_BASE = 'https://edith-redskill.sl.beta.xiaohongshu.com';
|
|
6
6
|
export const DEFAULT_SCOPES = ['base_info', 'skill_publish'];
|
|
7
7
|
export const DEFAULT_SCOPE = DEFAULT_SCOPES.join(',');
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
export const PATHS = {
|
|
10
10
|
UPLOAD_TOKEN: '/api/sns/v2/red_skill/upload/permit',
|
|
11
11
|
SUBMIT_SKILL_VERSION: '/api/sns/v1/creator/red_skill/cli_submit_skill_version',
|
package/cli/index.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { ExitCodes, SkillhubUploadError, toResultError } from './errors.mjs';
|
|
|
6
6
|
import { prepareBundle } from './pack.mjs';
|
|
7
7
|
import {
|
|
8
8
|
applyConfirmEdit,
|
|
9
|
+
buildConfirmationCard,
|
|
9
10
|
buildDraftPayload,
|
|
10
11
|
formatContentTagOptions,
|
|
11
12
|
submitSkillVersion
|
|
@@ -64,17 +65,6 @@ function buildValidatedDraftPayload(input) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
function wrapLocalValidation(error) {
|
|
68
|
-
if (error instanceof SkillhubUploadError) {
|
|
69
|
-
return error;
|
|
70
|
-
}
|
|
71
|
-
return new SkillhubUploadError(
|
|
72
|
-
'LOCAL_VALIDATION_FAILED',
|
|
73
|
-
error?.message || String(error),
|
|
74
|
-
ExitCodes.LOCAL_VALIDATION
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
68
|
function assertUsableCredentials(credentials, nowMs = Date.now()) {
|
|
79
69
|
if (!credentials?.accessToken) {
|
|
80
70
|
throw new SkillhubUploadError('NEED_LOGIN', '请先执行 redskillhub-upload login', ExitCodes.NEED_LOGIN);
|
|
@@ -117,18 +107,27 @@ export async function ensurePublishCredentials({
|
|
|
117
107
|
return credentials;
|
|
118
108
|
}
|
|
119
109
|
|
|
120
|
-
async function confirmBeforeSubmit(payload, io = {},
|
|
121
|
-
|
|
110
|
+
async function confirmBeforeSubmit(payload, bundle, io = {}, options = {}) {
|
|
111
|
+
const { tagOptions, showConfirmationCard = false } = options;
|
|
112
|
+
let currentPayload = payload;
|
|
122
113
|
while (true) {
|
|
123
|
-
const answer = await confirmAgent(
|
|
114
|
+
const answer = await confirmAgent(currentPayload, io, {
|
|
115
|
+
card: showConfirmationCard ? buildConfirmationCard(currentPayload, tagOptions) : undefined
|
|
116
|
+
});
|
|
124
117
|
if (answer.action === 'submit') {
|
|
125
|
-
return
|
|
118
|
+
return { payload: currentPayload, bundle };
|
|
126
119
|
}
|
|
127
120
|
if (answer.action === 'edit') {
|
|
128
121
|
try {
|
|
129
|
-
|
|
122
|
+
currentPayload = applyConfirmEdit(currentPayload, answer.values || {}, { tagOptions });
|
|
130
123
|
} catch (error) {
|
|
131
|
-
throw
|
|
124
|
+
throw error instanceof SkillhubUploadError
|
|
125
|
+
? error
|
|
126
|
+
: new SkillhubUploadError(
|
|
127
|
+
'LOCAL_VALIDATION_FAILED',
|
|
128
|
+
error?.message || String(error),
|
|
129
|
+
ExitCodes.LOCAL_VALIDATION
|
|
130
|
+
);
|
|
132
131
|
}
|
|
133
132
|
continue;
|
|
134
133
|
}
|
|
@@ -187,8 +186,9 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
187
186
|
ExitCodes.INVALID_ARGS
|
|
188
187
|
);
|
|
189
188
|
}
|
|
189
|
+
let apiEnvironment;
|
|
190
190
|
try {
|
|
191
|
-
resolveApiEnvironment(flags);
|
|
191
|
+
apiEnvironment = resolveApiEnvironment(flags);
|
|
192
192
|
} catch (error) {
|
|
193
193
|
throw new SkillhubUploadError('INVALID_ARGS', error.message, ExitCodes.INVALID_ARGS);
|
|
194
194
|
}
|
|
@@ -239,7 +239,7 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
239
239
|
|
|
240
240
|
const dryRun = Boolean(flags.dryRun);
|
|
241
241
|
let credentials = null;
|
|
242
|
-
if (!dryRun) {
|
|
242
|
+
if (!dryRun && apiEnvironment !== 'beta') {
|
|
243
243
|
const { readCredentials, login } = await import('./auth.mjs');
|
|
244
244
|
credentials = await ensurePublishCredentials({
|
|
245
245
|
flags,
|
|
@@ -250,7 +250,6 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
250
250
|
login
|
|
251
251
|
});
|
|
252
252
|
}
|
|
253
|
-
|
|
254
253
|
const bundle = await prepareBundle(pathArg, { env, flags });
|
|
255
254
|
const bundleMetadata = {
|
|
256
255
|
bundleSha256: bundle.bundleSha256,
|
|
@@ -277,17 +276,40 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
277
276
|
|
|
278
277
|
if (dryRun) {
|
|
279
278
|
const upload = await uploadBundle(bundle, { dryRun: true });
|
|
280
|
-
|
|
279
|
+
const payload = attachUploadResult(draftPayload, upload);
|
|
280
|
+
const result = {
|
|
281
|
+
status: 'dry_run',
|
|
282
|
+
payload
|
|
283
|
+
};
|
|
284
|
+
if (apiEnvironment === 'beta') {
|
|
285
|
+
result.confirmationCard = buildConfirmationCard(payload, tagOptions);
|
|
286
|
+
}
|
|
287
|
+
writeResult(result, out);
|
|
281
288
|
return;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
const
|
|
285
|
-
|
|
291
|
+
const confirmed = await confirmBeforeSubmit(draftPayload, bundle, promptSession, {
|
|
292
|
+
tagOptions,
|
|
293
|
+
showConfirmationCard: apiEnvironment === 'beta'
|
|
294
|
+
});
|
|
295
|
+
if (!confirmed) {
|
|
286
296
|
writeResult({ status: 'cancelled' }, out);
|
|
287
297
|
return;
|
|
288
298
|
}
|
|
289
299
|
|
|
290
|
-
|
|
300
|
+
if (!credentials) {
|
|
301
|
+
const { readCredentials, login } = await import('./auth.mjs');
|
|
302
|
+
credentials = await ensurePublishCredentials({
|
|
303
|
+
flags,
|
|
304
|
+
env,
|
|
305
|
+
io: { ...io, out },
|
|
306
|
+
nowMs: io.now ? io.now() : Date.now(),
|
|
307
|
+
readCredentials,
|
|
308
|
+
login
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const upload = await uploadBundle(confirmed.bundle, {
|
|
291
313
|
dryRun: false,
|
|
292
314
|
flags: publishFlags,
|
|
293
315
|
accessToken: credentials.accessToken,
|
|
@@ -297,7 +319,7 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
297
319
|
progressStream: io.progressStream || out,
|
|
298
320
|
env
|
|
299
321
|
});
|
|
300
|
-
const submitted = await submitSkillVersion(attachUploadResult(
|
|
322
|
+
const submitted = await submitSkillVersion(attachUploadResult(confirmed.payload, upload), {
|
|
301
323
|
flags: publishFlags,
|
|
302
324
|
accessToken: credentials.accessToken,
|
|
303
325
|
fetchImpl: io.fetchImpl,
|
package/cli/pack.mjs
CHANGED
package/cli/prompt.mjs
CHANGED
|
@@ -10,6 +10,31 @@ async function readLine(iterator) {
|
|
|
10
10
|
return next.done ? '' : String(next.value).trim();
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function parseEditValue(rawValue, line) {
|
|
14
|
+
const value = rawValue.trim();
|
|
15
|
+
if (!value.startsWith('"')) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(value);
|
|
20
|
+
} catch {
|
|
21
|
+
throw new Error(`确认信息修改格式不正确,JSON 字符串无法解析:${line}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function parseEditLine(line) {
|
|
26
|
+
const idx = line.indexOf('=');
|
|
27
|
+
if (idx < 0) {
|
|
28
|
+
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${line}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const key = line.slice(0, idx).trim();
|
|
32
|
+
if (!key) {
|
|
33
|
+
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${line}`);
|
|
34
|
+
}
|
|
35
|
+
return [key, parseEditValue(line.slice(idx + 1), line)];
|
|
36
|
+
}
|
|
37
|
+
|
|
13
38
|
export function createPromptSession(io = {}) {
|
|
14
39
|
const input = io.input || process.stdin;
|
|
15
40
|
const out = io.out || process.stdout;
|
|
@@ -40,10 +65,10 @@ export async function askAgent(prompt, io = {}) {
|
|
|
40
65
|
}
|
|
41
66
|
}
|
|
42
67
|
|
|
43
|
-
export async function confirmAgent(payload, io = {}) {
|
|
68
|
+
export async function confirmAgent(payload, io = {}, options = {}) {
|
|
44
69
|
const input = io.input || process.stdin;
|
|
45
70
|
const out = io.out || process.stdout;
|
|
46
|
-
writePrompt({ type: 'confirm', payload }, out);
|
|
71
|
+
writePrompt({ type: 'confirm', payload, card: options.card }, out);
|
|
47
72
|
|
|
48
73
|
if (io.readLine) {
|
|
49
74
|
const first = await io.readLine();
|
|
@@ -61,16 +86,8 @@ export async function confirmAgent(payload, io = {}) {
|
|
|
61
86
|
break;
|
|
62
87
|
}
|
|
63
88
|
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${trimmed}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const key = trimmed.slice(0, idx).trim();
|
|
70
|
-
if (!key) {
|
|
71
|
-
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${trimmed}`);
|
|
72
|
-
}
|
|
73
|
-
values[key] = trimmed.slice(idx + 1).trim();
|
|
89
|
+
const [key, value] = parseEditLine(trimmed);
|
|
90
|
+
values[key] = value;
|
|
74
91
|
}
|
|
75
92
|
return { action: 'edit', values };
|
|
76
93
|
}
|
|
@@ -95,16 +112,8 @@ export async function confirmAgent(payload, io = {}) {
|
|
|
95
112
|
break;
|
|
96
113
|
}
|
|
97
114
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${trimmed}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const key = trimmed.slice(0, idx).trim();
|
|
104
|
-
if (!key) {
|
|
105
|
-
throw new Error(`确认信息修改格式不正确,请使用 key=value 格式:${trimmed}`);
|
|
106
|
-
}
|
|
107
|
-
values[key] = trimmed.slice(idx + 1).trim();
|
|
115
|
+
const [key, value] = parseEditLine(trimmed);
|
|
116
|
+
values[key] = value;
|
|
108
117
|
}
|
|
109
118
|
return { action: 'edit', values };
|
|
110
119
|
}
|
package/cli/submit.mjs
CHANGED
|
@@ -5,7 +5,16 @@ import { compatibleFetch } from './fetch.mjs';
|
|
|
5
5
|
import { redactMessage } from './redact.mjs';
|
|
6
6
|
import { FALLBACK_CONTENT_TAGS } from './tags.mjs';
|
|
7
7
|
|
|
8
|
-
const EDITABLE_FIELDS = new Set([
|
|
8
|
+
const EDITABLE_FIELDS = new Set([
|
|
9
|
+
'name',
|
|
10
|
+
'identifier',
|
|
11
|
+
'version',
|
|
12
|
+
'description',
|
|
13
|
+
'detail',
|
|
14
|
+
'tag',
|
|
15
|
+
'source',
|
|
16
|
+
'repost_source'
|
|
17
|
+
]);
|
|
9
18
|
const VALID_SOURCES = new Set(['original', 'repost']);
|
|
10
19
|
const SENSITIVE_PAYLOAD_FIELDS = new Set([
|
|
11
20
|
'access_token',
|
|
@@ -47,6 +56,28 @@ export function formatContentTagOptions(tagOptions = FALLBACK_CONTENT_TAGS) {
|
|
|
47
56
|
return tagOptions.map((tag) => tag.name).join(' / ');
|
|
48
57
|
}
|
|
49
58
|
|
|
59
|
+
export function buildConfirmationCard(payload, tagOptions = FALLBACK_CONTENT_TAGS) {
|
|
60
|
+
const contentTags = (payload.content_tag_ids || []).map((tagId) => {
|
|
61
|
+
const matched = tagOptions.find((tag) => String(tag.tagId) === String(tagId));
|
|
62
|
+
return {
|
|
63
|
+
tagId: String(tagId),
|
|
64
|
+
name: matched?.name || String(tagId)
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
skillName: payload.name || '',
|
|
69
|
+
skillId: payload.skill_identifier || '',
|
|
70
|
+
version: payload.version || '',
|
|
71
|
+
description: payload.description || '',
|
|
72
|
+
skillIntroduction: payload.skill_md_content || '',
|
|
73
|
+
original: payload.original,
|
|
74
|
+
repostSource: payload.repost_source || '',
|
|
75
|
+
contentTags,
|
|
76
|
+
bundleSizeBytes: payload.bundle_size_bytes,
|
|
77
|
+
bundleSha256: payload.bundle_sha256 || ''
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
50
81
|
function resolveTagIds(flags, tagOptions = FALLBACK_CONTENT_TAGS) {
|
|
51
82
|
const rawTagId = flags.tagId ?? flags['tag-id'] ?? flags.tag;
|
|
52
83
|
const rawTags = splitTagValues(rawTagId);
|
|
@@ -137,6 +168,19 @@ export function applyConfirmEdit(payload, values, { tagOptions = FALLBACK_CONTEN
|
|
|
137
168
|
}
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
const hasSourceEdit = Object.prototype.hasOwnProperty.call(values, 'source');
|
|
172
|
+
const hasRepostSourceEdit = Object.prototype.hasOwnProperty.call(values, 'repost_source');
|
|
173
|
+
let sourceFields = {};
|
|
174
|
+
if (hasSourceEdit || hasRepostSourceEdit) {
|
|
175
|
+
const source = hasSourceEdit
|
|
176
|
+
? values.source
|
|
177
|
+
: payload.original === false ? 'repost' : 'original';
|
|
178
|
+
sourceFields = resolveSourceFields({
|
|
179
|
+
source,
|
|
180
|
+
repostSource: hasRepostSourceEdit ? values.repost_source : payload.repost_source
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
140
184
|
return {
|
|
141
185
|
...payload,
|
|
142
186
|
skill_identifier: values.identifier || payload.skill_identifier,
|
|
@@ -144,7 +188,8 @@ export function applyConfirmEdit(payload, values, { tagOptions = FALLBACK_CONTEN
|
|
|
144
188
|
name: values.name || payload.name,
|
|
145
189
|
description: values.description || payload.description,
|
|
146
190
|
skill_md_content: values.detail || payload.skill_md_content,
|
|
147
|
-
content_tag_ids: values.tag ? resolveTagIds({ tag: values.tag }, tagOptions) : payload.content_tag_ids
|
|
191
|
+
content_tag_ids: values.tag ? resolveTagIds({ tag: values.tag }, tagOptions) : payload.content_tag_ids,
|
|
192
|
+
...sourceFields
|
|
148
193
|
};
|
|
149
194
|
}
|
|
150
195
|
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "redskillhub-upload",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3-beta.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
7
|
-
"registry": "https://registry.npmjs.org/"
|
|
7
|
+
"registry": "https://registry.npmjs.org/",
|
|
8
|
+
"tag": "beta"
|
|
8
9
|
},
|
|
9
10
|
"bin": {
|
|
10
11
|
"redskillhub-upload": "cli/index.mjs"
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
],
|
|
17
18
|
"scripts": {
|
|
18
19
|
"test": "node --test test/*.test.mjs",
|
|
19
|
-
"dry-run": "node cli/index.mjs publish test/fixtures/minimal-skill --dry-run --agent --
|
|
20
|
+
"dry-run": "node cli/index.mjs publish test/fixtures/minimal-skill --dry-run --agent --source original --tag-id 1001 --env beta"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
23
|
"adm-zip": "^0.5.16",
|
package/skill/SKILL.md
CHANGED
|
@@ -1,149 +1,169 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: redskillhub-upload
|
|
3
|
-
description:
|
|
3
|
+
description: 当用户要求在 Beta 环境按照 upload.md 将本地 Skill 上传到小红书 SkillHub,或要求检查 CLI 版本、登录授权、取消登录时使用。发布前必须在对话内展示完整确认卡;修改只更新本次提交信息,不改本地文件;只有用户明确确认后才提交。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# redskillhub-upload
|
|
6
|
+
# redskillhub-upload(Beta)
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## 环境硬门禁
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
本需求当前只开放 **Beta 环境**。本流程的每一条 CLI 命令都必须显式追加 `--env beta`,禁止省略,禁止使用 `--env prod`,也不要通过 `--api-base` 绕过环境选择。
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
Beta 凭证、待授权状态、二维码、标签缓存和临时包位于 `~/.skillhub-upload/beta`,不得与 Prod 状态混用。
|
|
13
13
|
|
|
14
14
|
## 操作系统兼容
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
执行前通过 Node.js 判断系统,不依赖 `uname`:
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
node -p "process.platform"
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- 需要向 stdin 写入确认值时,统一使用 `node -e "process.stdout.write('submit\n')"` 或 `node -e "process.stdout.write('cancel\n')"`,不要按系统拼两套管道命令。
|
|
22
|
+
- `darwin` 和 `win32` 都受支持。
|
|
23
|
+
- 所有本地路径使用双引号包裹,兼容空格和中文。
|
|
24
|
+
- 命令写成单行,不使用 shell 专属续行语法。
|
|
25
|
+
- 不由 Agent 解压、过滤或重新打包 zip;这些操作全部交给 CLI。
|
|
27
26
|
|
|
28
27
|
## CLI 版本门禁
|
|
29
28
|
|
|
30
|
-
|
|
29
|
+
执行任何发布相关命令前,从本 Skill 目录运行:
|
|
31
30
|
|
|
32
31
|
```bash
|
|
33
|
-
node "<SKILL_DIR>/scripts/ensure-cli.mjs" --env
|
|
32
|
+
node "<SKILL_DIR>/scripts/ensure-cli.mjs" --env beta
|
|
34
33
|
```
|
|
35
34
|
|
|
36
|
-
|
|
35
|
+
`--env beta` 只决定 Beta 接口和凭证,不决定 npm 包版本。npm 默认安装 `latest` tag 指向的 `1.0.z` 正式版本。可信的外层 Beta 测试口令应把上述命令替换为固定的自动择优策略:
|
|
37
36
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- `status=error` 时如实告诉用户并停止,不得改用其他 major/minor、不得静默安装本地源码、不得使用 `sudo`。
|
|
42
|
-
- 不要在 Agent 中重写版本比较逻辑;以 `scripts/ensure-cli.mjs` 为唯一实现。
|
|
37
|
+
```bash
|
|
38
|
+
node "<SKILL_DIR>/scripts/ensure-cli.mjs" --env beta --npm-policy beta-first
|
|
39
|
+
```
|
|
43
40
|
|
|
44
|
-
|
|
41
|
+
该策略同时读取 `latest` 和 `beta`:仅当 `beta` 对应的目标正式版本高于当前 `latest` 时安装预发布版;正式版追平或超过预发布版后自动回到 `latest`;没有 `beta` tag 时也使用 `latest`。`--npm-policy beta-first` 只允许搭配 `--env beta`,Prod 禁止使用。不得根据 `--env beta` 自行推断使用预发布包。读取 `CLI_VERSION_JSON`;只有 `status=current|installed|updated` 才继续。失败时如实告知用户并停止,不改用其他版本线、不使用本地源码替代、不使用 `sudo`。
|
|
45
42
|
|
|
46
|
-
|
|
47
|
-
2. 执行「CLI 版本门禁」,确保 PATH 中的 `redskillhub-upload` 是允许版本线内的最新包。
|
|
48
|
-
3. 查找用户给出的本地 Skill 目录或 `.zip` 源包。用户只给名称时,先在当前工作区和常见目录查找唯一匹配目录;找不到或多匹配时询问用户绝对路径。zip 只能作为 CLI 输入源包,不能由 agent 解压或代替 CLI 重新打包。
|
|
49
|
-
4. 直接执行下面的「publish 子流程」,不要预先调用 `whoami` 或 `login`。真实 `publish` 会自动复用或刷新已有凭证;首次登录或 refresh token 失效时,会在同一进程中输出授权 `PROMPT`,授权成功后原地继续发布。
|
|
50
|
-
5. 遇到授权 `PROMPT` 时,必须按以下顺序展示:先提示用户使用小红书 App 扫码,再把 `prompt.qrCodePath` 的本地 PNG 展示或发送给用户,最后才提供手机浏览器兜底链接。文字链接使用 `prompt.authorizeH5Url`(兼容字段 `prompt.authorizeUrl` 也是同一 H5 URL)。二维码由 CLI 使用接口的 `authorize_common_url` 生成,Agent 不得自行换字段或重新编码。授权内容必须照抄模板,只替换 `<QR_CODE_IMAGE>`、`<AUTH_URL>`、`<USER_CODE>`、`<MINUTES>`,不增删措辞;不得在二维码之前引导用户打开链接。
|
|
43
|
+
## 发布流程
|
|
51
44
|
|
|
52
|
-
|
|
53
|
-
请使用小红书 App 扫描下面的二维码完成授权:
|
|
45
|
+
### 1. 定位输入并收集会话参数
|
|
54
46
|
|
|
55
|
-
|
|
47
|
+
输入可以是本地 Skill 目录或 `.zip` 文件。用户只给名称时,在当前工作区和常见目录定位唯一匹配;找不到或多匹配时询问绝对路径。
|
|
56
48
|
|
|
57
|
-
|
|
49
|
+
先查询 Beta 标签:
|
|
58
50
|
|
|
59
|
-
```
|
|
60
|
-
|
|
51
|
+
```bash
|
|
52
|
+
redskillhub-upload tags --env beta
|
|
61
53
|
```
|
|
62
54
|
|
|
63
|
-
|
|
64
|
-
有效期:<MINUTES> 分钟
|
|
55
|
+
从 `RESULT_JSON.tags` 取得中文标签后,必须沿用线上现有交互,一次性按下面的固定结构询问。只替换 `<标签列表>` 和 `<absolute-path>`,不得压缩、省略或改写转载来源提示:
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
57
|
+
```text
|
|
58
|
+
CLI 版本门禁已通过,实时标签已获取。请一次回复:
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
7. 所有发布字段值取自用户当前会话答复,不要从 SKILL.md / 目录名 / 文件结构 / 上下文对话推断。
|
|
72
|
-
8. publish 子流程结束后,把 `RESULT_JSON` 的成功结果、失败原因或取消状态如实转述给用户。
|
|
60
|
+
1. source:原创 或 转载
|
|
61
|
+
2. tag:可多选,用逗号分隔
|
|
73
62
|
|
|
74
|
-
|
|
63
|
+
可选标签:<标签列表>。
|
|
75
64
|
|
|
76
|
-
|
|
65
|
+
例如:原创;编程开发、学习成长
|
|
77
66
|
|
|
78
|
-
|
|
67
|
+
若选择转载,还请附上不超过 15 个字符的转载来源。待上传目录为 <absolute-path>。
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
用户选择转载但没有提供来源时,再单独追问 `repost_source`;选择原创时不追问。
|
|
79
71
|
|
|
80
|
-
|
|
72
|
+
### 2. 启动发布会话并展示确认卡
|
|
73
|
+
|
|
74
|
+
直接启动真实 publish,但保持进程等待输入;不要预先执行 login,也不要提前向 stdin 写入 submit:
|
|
81
75
|
|
|
82
76
|
```bash
|
|
83
|
-
redskillhub-upload
|
|
77
|
+
redskillhub-upload publish "<absolute-path>" --agent --source <original-or-repost> --tag "<中文标签名[,中文标签名...]>" [--repost-source "<来源名>"] --env beta
|
|
84
78
|
```
|
|
85
79
|
|
|
86
|
-
|
|
80
|
+
CLI 会先在本地解析、执行安全校验并打包,然后输出 `PROMPT.type=confirm`。此时尚未上传,也未调用 submit。
|
|
87
81
|
|
|
88
|
-
|
|
82
|
+
必须把 `PROMPT.card` 完整回显为确认卡,不得省略字段:
|
|
89
83
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
84
|
+
- Skill 名称:`skillName`
|
|
85
|
+
- Skill ID:`skillId`
|
|
86
|
+
- 版本:`version`
|
|
87
|
+
- 简介:`description`
|
|
88
|
+
- Skill 介绍:`skillIntroduction`
|
|
89
|
+
- 来源:`original=true` 显示“原创”,`original=false` 显示“转载”
|
|
90
|
+
- 转载来源:`repostSource`,原创时显示“无”
|
|
91
|
+
- 场景标签:`contentTags[].name`
|
|
92
|
+
- 打包大小:`bundleSizeBytes` bytes
|
|
93
|
+
- SHA-256:`bundleSha256`
|
|
92
94
|
|
|
93
|
-
|
|
95
|
+
Skill 介绍的展示方式必须与平台编辑器尽量一致,首次确认和每轮修改后的确认卡都遵守以下规则:
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
- 在“Skill 介绍:”后,把 `skillIntroduction` 全文原样放进一个显式标记为 `text` 的围栏代码块;不得省略 `text` 标记,避免聊天渲染器自动猜测为 Markdown 并着色。
|
|
98
|
+
- 外层围栏的反引号数量必须多于正文中最长的连续反引号;正文含三个反引号时,外层至少使用四个,并在起始围栏后紧跟 `text`,确保内部 Markdown 围栏仍在同一个展示块内。
|
|
99
|
+
- 不得把 Skill 介绍渲染成聊天标题、段落、列表或多个代码块。
|
|
100
|
+
- 不得使用 `markdown`、`md`、`yaml` 等会触发语法高亮的标记,只能使用 `text`。
|
|
101
|
+
- 所有正文字符使用代码块统一的字号、字重和颜色;`#`、`##`、`**` 等 Markdown 标记只能作为普通原始字符显示,不能产生标题字号或粗体差异。
|
|
102
|
+
- 不改写、不美化、不截断正文;用户看到的原始 Markdown 必须与当前待提交的 `skill_md_content` 一致。
|
|
96
103
|
|
|
97
|
-
|
|
98
|
-
redskillhub-upload publish "<absolute-path>" --dry-run --agent --source <original-or-repost> --tag "<中文标签名[,中文标签名...]>" [--repost-source "<来源名>"] --env <ENV>
|
|
99
|
-
```
|
|
104
|
+
确认卡后必须直接提示:作者如需修改,可告诉 Agent 要修改的字段和新内容,修改后会重新展示完整确认卡;如无需修改,明确回复“确认”“提交”或 `submit`;如需终止,回复“取消”。不要等用户追问“可以修改吗”才说明。
|
|
100
105
|
|
|
101
|
-
|
|
106
|
+
如果进程或对话会话失效,重新运行原 publish 命令、重新解析并回显完整确认卡;不得沿用一张失效卡直接提交。
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
### 3. 在同一会话中修改
|
|
104
109
|
|
|
105
|
-
|
|
106
|
-
2. 用 `AskUserQuestion` 给用户「采纳推荐 `<推荐值>` / 自定义其他名字」两个选项
|
|
107
|
-
3. 必须告知用户:**Skill ID 是平台上的 skill 主键,提交后跨版本不可改名**,请慎重
|
|
108
|
-
4. 拿到最终 Skill ID 后追加 `--identifier <值>` 重跑 Step 2
|
|
110
|
+
用户提出修改时,只向当前 publish 进程写入一次 `edit` 块;不要由 Agent 直接编辑文件:
|
|
109
111
|
|
|
110
|
-
|
|
112
|
+
```text
|
|
113
|
+
edit
|
|
114
|
+
name=<新名称>
|
|
115
|
+
description=<新简介>
|
|
116
|
+
version=<新版本号>
|
|
117
|
+
detail=<新的 Skill 介绍;多行内容使用 JSON 字符串>
|
|
118
|
+
identifier=<新的 Skill ID>
|
|
119
|
+
tag=<中文标签名[,中文标签名...]>
|
|
120
|
+
source=<original|repost>
|
|
121
|
+
repost_source=<转载来源>
|
|
111
122
|
|
|
112
|
-
```bash
|
|
113
|
-
node -e "process.stdout.write('submit\n')" | redskillhub-upload publish "<absolute-path>" --agent --source <original-or-repost> --tag "<中文标签名[,中文标签名...]>" [--repost-source "<来源名>"] --env <ENV>
|
|
114
123
|
```
|
|
115
124
|
|
|
116
|
-
|
|
125
|
+
只发送用户明确要求修改的键。用户改为转载但未提供转载来源时,先在对话中追问,收齐后把 `source` 和 `repost_source` 放在同一个 edit 块中。多行 Skill 介绍必须编码为 JSON 字符串,例如 `detail="# 标题\n\n正文"`。
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
- `name`、`description`、`version`、`detail`、`identifier`、`tag`、`source`、`repost_source`:所有修改字段只更新本次发布会话参数。
|
|
128
|
+
- 不得修改本地 `SKILL.md/skill.md` 或原 zip,也不会生成 `-updated.zip`。
|
|
129
|
+
- 其他键禁止修改。
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
每次 edit 后,CLI 都会基于最新提交信息输出新的 `PROMPT.type=confirm`。每次都要重新展示完整确认卡,不能只展示差异。bundle 没有变化,因此打包大小和 SHA-256 保持不变。
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
### 4. 明确确认或取消
|
|
123
134
|
|
|
124
|
-
|
|
135
|
+
只有用户明确回复“确认”“提交”或“submit”时,才向仍在等待的同一进程写入:
|
|
125
136
|
|
|
126
|
-
|
|
127
|
-
|
|
137
|
+
```text
|
|
138
|
+
submit
|
|
139
|
+
```
|
|
128
140
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
任何近义表达、用户回复“好了”或授权完成都不能当作提交确认。
|
|
142
|
+
|
|
143
|
+
用户回复“取消”或“cancel”时写入:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
cancel
|
|
132
147
|
```
|
|
133
148
|
|
|
134
|
-
`
|
|
149
|
+
取消后应得到 `RESULT_JSON.status=cancelled`;此路径不得上传、不得调用 submit、不得创建 Version 或审核单。
|
|
150
|
+
|
|
151
|
+
### 5. 授权与结果
|
|
135
152
|
|
|
136
|
-
|
|
153
|
+
CLI 只会在明确 submit 之后检查或恢复登录。需要授权时,按 `PROMPT.type=auth_device_code` 先展示二维码,再提供手机浏览器兜底链接。二维码路径 `qrCodePath` 必须作为图片发给用户,不能只显示路径。
|
|
154
|
+
|
|
155
|
+
用户要求取消登录等待时执行:
|
|
137
156
|
|
|
138
157
|
```bash
|
|
139
|
-
|
|
140
|
-
# 或调试用:--tag-id 1001,1002
|
|
158
|
+
redskillhub-upload login --cancel --env beta
|
|
141
159
|
```
|
|
142
160
|
|
|
161
|
+
最终如实转述 `RESULT_JSON` 的 `submitted / cancelled / error` 状态及关键回执。不得访问或修改 Prod,不得执行 npm publish、部署或远端提交。
|
|
162
|
+
|
|
143
163
|
## 禁止事项
|
|
144
164
|
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
165
|
+
- 用户明确确认前,禁止上传 bundle、禁止调用 submit。
|
|
166
|
+
- 不自行拼授权链接,不要求用户复制 token、cookie 或 authorization code。
|
|
167
|
+
- 不显示 access token / refresh token。
|
|
168
|
+
- 不修改本地 SKILL.md 或 zip;所有确认阶段修改只写入等待中的 CLI 会话。
|
|
169
|
+
- 不使用浏览器自动化或临时信号文件绕过确认门禁。
|
package/skill/agents/openai.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
interface:
|
|
2
2
|
display_name: "Red SkillHub 上传"
|
|
3
|
-
short_description: "
|
|
4
|
-
default_prompt: "使用 $redskillhub-upload
|
|
3
|
+
short_description: "在 Beta 对话中确认、修改并发布本地 Skill"
|
|
4
|
+
default_prompt: "使用 $redskillhub-upload 在 Beta 环境解析我的本地 Skill,展示含 Skill 介绍的完整确认卡并提示我可修改;修改只更新提交信息,只有我明确确认后才提交。"
|
|
@@ -8,6 +8,9 @@ const COMMAND_NAME = 'redskillhub-upload';
|
|
|
8
8
|
const REGISTRY = 'https://registry.npmjs.org/';
|
|
9
9
|
const REQUIRED_MAJOR = 1;
|
|
10
10
|
const REQUIRED_MINOR = 0;
|
|
11
|
+
const BETA_RELEASE_TAG = 'beta';
|
|
12
|
+
const PROD_RELEASE_TAG = 'latest';
|
|
13
|
+
const BETA_FIRST_POLICY = 'beta-first';
|
|
11
14
|
|
|
12
15
|
function parseVersion(version) {
|
|
13
16
|
const match = String(version).match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
@@ -21,49 +24,6 @@ function parseVersion(version) {
|
|
|
21
24
|
};
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
function comparePrerelease(a, b) {
|
|
25
|
-
if (!a && !b) return 0;
|
|
26
|
-
if (!a) return 1;
|
|
27
|
-
if (!b) return -1;
|
|
28
|
-
const left = a.split('.');
|
|
29
|
-
const right = b.split('.');
|
|
30
|
-
const length = Math.max(left.length, right.length);
|
|
31
|
-
for (let index = 0; index < length; index += 1) {
|
|
32
|
-
if (left[index] === undefined) return -1;
|
|
33
|
-
if (right[index] === undefined) return 1;
|
|
34
|
-
const leftNumber = /^\d+$/.test(left[index]) ? Number(left[index]) : null;
|
|
35
|
-
const rightNumber = /^\d+$/.test(right[index]) ? Number(right[index]) : null;
|
|
36
|
-
if (leftNumber !== null && rightNumber !== null && leftNumber !== rightNumber) {
|
|
37
|
-
return leftNumber - rightNumber;
|
|
38
|
-
}
|
|
39
|
-
if (leftNumber !== null && rightNumber === null) return -1;
|
|
40
|
-
if (leftNumber === null && rightNumber !== null) return 1;
|
|
41
|
-
const compared = left[index].localeCompare(right[index]);
|
|
42
|
-
if (compared) return compared;
|
|
43
|
-
}
|
|
44
|
-
return 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function compareVersions(a, b) {
|
|
48
|
-
if (a.patch !== b.patch) return a.patch - b.patch;
|
|
49
|
-
return comparePrerelease(a.prerelease, b.prerelease);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function selectLatestCompatibleVersion(versions, options = {}) {
|
|
53
|
-
const major = options.major ?? REQUIRED_MAJOR;
|
|
54
|
-
const minor = options.minor ?? REQUIRED_MINOR;
|
|
55
|
-
const compatible = versions
|
|
56
|
-
.map(parseVersion)
|
|
57
|
-
.filter((version) => version && version.major === major && version.minor === minor);
|
|
58
|
-
if (!compatible.length) {
|
|
59
|
-
throw new Error(`npm 中没有找到 ${major}.${minor} 版本线的 ${PACKAGE_NAME}`);
|
|
60
|
-
}
|
|
61
|
-
const stable = compatible.filter((version) => !version.prerelease);
|
|
62
|
-
const candidates = stable.length ? stable : compatible;
|
|
63
|
-
candidates.sort(compareVersions);
|
|
64
|
-
return candidates.at(-1).raw;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
27
|
export function shouldUseSystemShell(platform = process.platform) {
|
|
68
28
|
return platform === 'win32';
|
|
69
29
|
}
|
|
@@ -83,16 +43,18 @@ function run(command, args, options = {}) {
|
|
|
83
43
|
return result;
|
|
84
44
|
}
|
|
85
45
|
|
|
86
|
-
function
|
|
46
|
+
function queryTaggedVersion(tag, options = {}) {
|
|
87
47
|
const result = run('npm', [
|
|
88
48
|
'view',
|
|
89
|
-
PACKAGE_NAME
|
|
90
|
-
'
|
|
49
|
+
`${PACKAGE_NAME}@${tag}`,
|
|
50
|
+
'version',
|
|
91
51
|
'--json',
|
|
92
52
|
`--registry=${REGISTRY}`
|
|
93
|
-
], { capture: true });
|
|
94
|
-
|
|
95
|
-
|
|
53
|
+
], { capture: true, allowFailure: options.optional });
|
|
54
|
+
if (result.status !== 0 && options.optional) return '';
|
|
55
|
+
const version = JSON.parse(result.stdout);
|
|
56
|
+
if (Array.isArray(version)) return version.at(-1) || '';
|
|
57
|
+
return String(version || '');
|
|
96
58
|
}
|
|
97
59
|
|
|
98
60
|
function readInstalledVersion() {
|
|
@@ -127,9 +89,73 @@ export function normalizeCliEnvironment(value = 'prod') {
|
|
|
127
89
|
return envName;
|
|
128
90
|
}
|
|
129
91
|
|
|
92
|
+
export function normalizeNpmTag(value = PROD_RELEASE_TAG) {
|
|
93
|
+
const tag = String(value).trim().toLowerCase();
|
|
94
|
+
if (tag !== BETA_RELEASE_TAG && tag !== PROD_RELEASE_TAG) {
|
|
95
|
+
throw new Error(`--npm-tag 仅支持 beta 或 latest,当前值:${value}`);
|
|
96
|
+
}
|
|
97
|
+
return tag;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveCliRelease(npmTag, version) {
|
|
101
|
+
const tag = normalizeNpmTag(npmTag);
|
|
102
|
+
const parsed = parseVersion(version);
|
|
103
|
+
if (!parsed || parsed.major !== REQUIRED_MAJOR || parsed.minor !== REQUIRED_MINOR) {
|
|
104
|
+
throw new Error(`${tag} tag 仅支持 ${REQUIRED_MAJOR}.${REQUIRED_MINOR} 版本线,当前值:${version || '缺失'}`);
|
|
105
|
+
}
|
|
106
|
+
if (tag === BETA_RELEASE_TAG && !parsed.prerelease) {
|
|
107
|
+
throw new Error(`beta tag 必须指向预发布版本,当前值:${version}`);
|
|
108
|
+
}
|
|
109
|
+
if (tag === PROD_RELEASE_TAG && parsed.prerelease) {
|
|
110
|
+
throw new Error(`latest tag 必须指向正式版本,当前值:${version}`);
|
|
111
|
+
}
|
|
112
|
+
return { tag, version: parsed.raw };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compareCoreVersion(left, right) {
|
|
116
|
+
if (left.major !== right.major) return left.major - right.major;
|
|
117
|
+
if (left.minor !== right.minor) return left.minor - right.minor;
|
|
118
|
+
return left.patch - right.patch;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function selectBetaFirstRelease(latestVersion, betaVersion) {
|
|
122
|
+
const latest = resolveCliRelease(PROD_RELEASE_TAG, latestVersion);
|
|
123
|
+
if (!betaVersion) return latest;
|
|
124
|
+
|
|
125
|
+
const beta = resolveCliRelease(BETA_RELEASE_TAG, betaVersion);
|
|
126
|
+
return compareCoreVersion(parseVersion(beta.version), parseVersion(latest.version)) > 0
|
|
127
|
+
? beta
|
|
128
|
+
: latest;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function normalizeNpmPolicy(value = '') {
|
|
132
|
+
const policy = String(value || '').trim().toLowerCase();
|
|
133
|
+
if (policy && policy !== BETA_FIRST_POLICY) {
|
|
134
|
+
throw new Error(`--npm-policy 仅支持 beta-first,当前值:${value}`);
|
|
135
|
+
}
|
|
136
|
+
return policy;
|
|
137
|
+
}
|
|
138
|
+
|
|
130
139
|
export function ensureCli(options = {}) {
|
|
131
140
|
const envName = normalizeCliEnvironment(options.env);
|
|
132
|
-
const
|
|
141
|
+
const npmPolicy = normalizeNpmPolicy(options.npmPolicy);
|
|
142
|
+
if (npmPolicy && options.npmTag !== undefined) {
|
|
143
|
+
throw new Error('--npm-policy 与 --npm-tag 不能同时使用');
|
|
144
|
+
}
|
|
145
|
+
if (npmPolicy === BETA_FIRST_POLICY && envName !== 'beta') {
|
|
146
|
+
throw new Error('--npm-policy beta-first 仅允许与 --env beta 一起使用');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const release = npmPolicy === BETA_FIRST_POLICY
|
|
150
|
+
? selectBetaFirstRelease(
|
|
151
|
+
queryTaggedVersion(PROD_RELEASE_TAG),
|
|
152
|
+
queryTaggedVersion(BETA_RELEASE_TAG, { optional: true })
|
|
153
|
+
)
|
|
154
|
+
: (() => {
|
|
155
|
+
const releaseTag = normalizeNpmTag(options.npmTag);
|
|
156
|
+
return resolveCliRelease(releaseTag, queryTaggedVersion(releaseTag));
|
|
157
|
+
})();
|
|
158
|
+
const latestVersion = release.version;
|
|
133
159
|
const previousVersion = readInstalledVersion();
|
|
134
160
|
let status = 'current';
|
|
135
161
|
if (previousVersion !== latestVersion) {
|
|
@@ -141,7 +167,14 @@ export function ensureCli(options = {}) {
|
|
|
141
167
|
throw new Error(`版本校验失败:期望 ${latestVersion},实际 ${installedVersion || '未安装'}`);
|
|
142
168
|
}
|
|
143
169
|
run(COMMAND_NAME, ['whoami', '--env', envName]);
|
|
144
|
-
return {
|
|
170
|
+
return {
|
|
171
|
+
status,
|
|
172
|
+
previousVersion: previousVersion || null,
|
|
173
|
+
version: installedVersion,
|
|
174
|
+
tag: release.tag,
|
|
175
|
+
policy: npmPolicy || null,
|
|
176
|
+
env: envName
|
|
177
|
+
};
|
|
145
178
|
}
|
|
146
179
|
|
|
147
180
|
function readEnvironmentArg(argv) {
|
|
@@ -150,6 +183,18 @@ function readEnvironmentArg(argv) {
|
|
|
150
183
|
return argv[index + 1];
|
|
151
184
|
}
|
|
152
185
|
|
|
186
|
+
function readNpmTagArg(argv) {
|
|
187
|
+
const index = argv.indexOf('--npm-tag');
|
|
188
|
+
if (index === -1) return undefined;
|
|
189
|
+
return argv[index + 1];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function readNpmPolicyArg(argv) {
|
|
193
|
+
const index = argv.indexOf('--npm-policy');
|
|
194
|
+
if (index === -1) return undefined;
|
|
195
|
+
return argv[index + 1];
|
|
196
|
+
}
|
|
197
|
+
|
|
153
198
|
function isMainModule() {
|
|
154
199
|
if (!process.argv[1]) return false;
|
|
155
200
|
try {
|
|
@@ -162,7 +207,9 @@ function isMainModule() {
|
|
|
162
207
|
if (isMainModule()) {
|
|
163
208
|
try {
|
|
164
209
|
process.stdout.write(`CLI_VERSION_JSON:${JSON.stringify(ensureCli({
|
|
165
|
-
env: readEnvironmentArg(process.argv.slice(2))
|
|
210
|
+
env: readEnvironmentArg(process.argv.slice(2)),
|
|
211
|
+
npmTag: readNpmTagArg(process.argv.slice(2)),
|
|
212
|
+
npmPolicy: readNpmPolicyArg(process.argv.slice(2))
|
|
166
213
|
}))}\n`);
|
|
167
214
|
} catch (error) {
|
|
168
215
|
process.stderr.write(`CLI_VERSION_JSON:${JSON.stringify({
|