redskillhub-upload 1.0.0-alpha.4 → 1.0.0-alpha.6
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 +20 -9
- package/cli/auth-prompt.mjs +2 -1
- package/cli/auth.mjs +44 -75
- package/cli/config.mjs +17 -15
- package/cli/index.mjs +18 -8
- package/cli/pack.mjs +7 -6
- package/cli/tags.mjs +4 -4
- package/package.json +1 -1
- package/skill/SKILL.md +27 -13
- package/skill/scripts/ensure-cli.mjs +7 -1
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Node.js CLI for uploading local Skills to 小红书 SkillHub.
|
|
4
4
|
|
|
5
|
+
支持 macOS 和 Windows(PowerShell / cmd)。要求 Node.js 18 或更高版本;包含空格或中文的本地路径应使用双引号包裹。
|
|
6
|
+
|
|
5
7
|
Install from the public npm registry:
|
|
6
8
|
|
|
7
9
|
```bash
|
|
@@ -14,7 +16,7 @@ Publish input can be a local Skill directory or a `.zip` file. User-provided zip
|
|
|
14
16
|
|
|
15
17
|
## API Endpoints
|
|
16
18
|
|
|
17
|
-
All paths and hosts are centralized in `cli/config.mjs` (`PATHS` + `DEFAULT_API_BASE` / `
|
|
19
|
+
All paths and hosts are centralized in `cli/config.mjs` (`PATHS` + `DEFAULT_API_BASE` / `BETA_API_BASE`).
|
|
18
20
|
|
|
19
21
|
| Path Key | Path | Host | Description |
|
|
20
22
|
|----------|------|------|-------------|
|
|
@@ -22,15 +24,15 @@ All paths and hosts are centralized in `cli/config.mjs` (`PATHS` + `DEFAULT_API_
|
|
|
22
24
|
| `SUBMIT_SKILL_VERSION` | `/api/sns/v1/openapi/skillhub/submit_skill_version` | `DEFAULT_API_BASE` | 提交 skill 版本 |
|
|
23
25
|
| `CREATE_CLI_OAUTH_DEVICE_CODE` | `/api/sns/v1/creator/red_skill/create_cli_oauth_device_code` | `DEFAULT_API_BASE` | 创建 CLI OAuth 设备码 |
|
|
24
26
|
| `POLL_CLI_OAUTH_DEVICE_TOKEN` | `/api/sns/v1/creator/red_skill/poll_cli_oauth_device_token` | `DEFAULT_API_BASE` | 轮询 CLI OAuth 授权状态 |
|
|
25
|
-
| `
|
|
26
|
-
| `
|
|
27
|
+
| `EXCHANGE_CLI_OAUTH_TOKEN` | `/api/sns/v1/creator/red_skill/exchange_cli_oauth_token` | `DEFAULT_API_BASE` / `BETA_API_BASE` | 通过 SkillHub 服务端换取 access_token |
|
|
28
|
+
| `REFRESH_CLI_OAUTH_TOKEN` | `/api/sns/v1/creator/red_skill/refresh_cli_oauth_token` | `DEFAULT_API_BASE` / `BETA_API_BASE` | 通过 SkillHub 服务端续期 access_token |
|
|
27
29
|
|
|
28
30
|
- `DEFAULT_API_BASE` = `https://edith.xiaohongshu.com` — 生产业务接口
|
|
29
31
|
- `BETA_API_BASE` = `https://edith.beta.xiaohongshu.com` — beta 业务接口
|
|
30
|
-
- `DEFAULT_OAS_BASE` = `https://openaccount.xiaohongshu.com` — OAuth2 开放平台接口
|
|
31
32
|
- 所有命令都支持 `--env prod|beta`,默认 `prod`;`--env beta` 时业务接口使用 `BETA_API_BASE`
|
|
33
|
+
- prod 沿用 `~/.skillhub-upload`,beta 使用 `~/.skillhub-upload/beta`,凭证、待授权、二维码、标签缓存和临时包相互隔离
|
|
32
34
|
- 业务接口 host 可通过 `--api-base` 参数或 `SKILLHUB_UPLOAD_API_BASE` 环境变量覆盖
|
|
33
|
-
-
|
|
35
|
+
- OAuth token 换取与刷新统一走 SkillHub 服务端 broker,CLI 不保存 appSecret
|
|
34
36
|
|
|
35
37
|
## Auth Flow
|
|
36
38
|
|
|
@@ -60,9 +62,10 @@ Dry-run examples may use numeric tag IDs because the backend contract accepts `c
|
|
|
60
62
|
redskillhub-upload login --env prod
|
|
61
63
|
redskillhub-upload login --agent --env prod
|
|
62
64
|
redskillhub-upload login --cancel --env prod
|
|
63
|
-
redskillhub-upload
|
|
64
|
-
redskillhub-upload publish /absolute/path/to/skill --
|
|
65
|
-
redskillhub-upload publish /absolute/path/to/skill
|
|
65
|
+
redskillhub-upload tags --env prod
|
|
66
|
+
redskillhub-upload publish "/absolute/path/to/skill" --env prod
|
|
67
|
+
redskillhub-upload publish "/absolute/path/to/skill" --agent --env prod
|
|
68
|
+
redskillhub-upload publish "/absolute/path/to/skill.zip" --agent --env prod
|
|
66
69
|
redskillhub-upload whoami --env prod
|
|
67
70
|
redskillhub-upload logout --env prod
|
|
68
71
|
```
|
|
@@ -71,11 +74,19 @@ redskillhub-upload logout --env prod
|
|
|
71
74
|
|
|
72
75
|
```bash
|
|
73
76
|
redskillhub-upload login --agent --env beta
|
|
74
|
-
redskillhub-upload
|
|
77
|
+
redskillhub-upload tags --env beta
|
|
78
|
+
redskillhub-upload publish "/absolute/path/to/skill" --agent --env beta
|
|
75
79
|
redskillhub-upload whoami --env beta
|
|
76
80
|
redskillhub-upload logout --env beta
|
|
77
81
|
```
|
|
78
82
|
|
|
83
|
+
Windows 路径示例:
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
redskillhub-upload publish "C:\Users\name\path\to\skill" --agent --env prod
|
|
87
|
+
node -e "process.stdout.write('submit\n')" | redskillhub-upload publish "C:\Users\name\path\to\skill" --agent --source original --tag "效率工具" --env prod
|
|
88
|
+
```
|
|
89
|
+
|
|
79
90
|
## Agent Protocol
|
|
80
91
|
|
|
81
92
|
See `docs/protocol.md` for the line protocol:
|
package/cli/auth-prompt.mjs
CHANGED
|
@@ -8,6 +8,7 @@ export async function renderDeviceAuthPrompt(payload, options = {}) {
|
|
|
8
8
|
const {
|
|
9
9
|
agent = false,
|
|
10
10
|
env = process.env,
|
|
11
|
+
flags = {},
|
|
11
12
|
stream = process.stdout,
|
|
12
13
|
qrCode = QRCode
|
|
13
14
|
} = options;
|
|
@@ -25,7 +26,7 @@ export async function renderDeviceAuthPrompt(payload, options = {}) {
|
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
if (agent) {
|
|
28
|
-
const qrCodePath = getAuthQrPath(env);
|
|
29
|
+
const qrCodePath = getAuthQrPath(env, flags);
|
|
29
30
|
await fs.mkdir(path.dirname(qrCodePath), { recursive: true, mode: 0o700 });
|
|
30
31
|
await qrCode.toFile(qrCodePath, authorizeCommonUrl, {
|
|
31
32
|
type: 'png',
|
package/cli/auth.mjs
CHANGED
|
@@ -3,16 +3,13 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_API_BASE,
|
|
6
|
-
DEFAULT_APP_ID,
|
|
7
|
-
DEFAULT_APP_SECRET,
|
|
8
|
-
DEFAULT_OAS_BASE,
|
|
9
6
|
DEFAULT_SCOPE,
|
|
10
7
|
DEFAULT_SCOPES,
|
|
11
8
|
PATHS,
|
|
12
9
|
TOKEN_REFRESH_BUFFER_MS,
|
|
13
10
|
getAuthQrPath,
|
|
14
11
|
getCredentialsPath,
|
|
15
|
-
|
|
12
|
+
getEnvironmentHomeDir,
|
|
16
13
|
getPendingAuthPath,
|
|
17
14
|
resolveApiBase
|
|
18
15
|
} from './config.mjs';
|
|
@@ -46,9 +43,9 @@ function canRefreshCredentials(credentials, nowMs = Date.now()) {
|
|
|
46
43
|
return Number.isFinite(refreshExpireTimeMs) && refreshExpireTimeMs > nowMs + TOKEN_REFRESH_BUFFER_MS;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
export async function readCredentials(env = process.env) {
|
|
46
|
+
export async function readCredentials(env = process.env, flags = {}) {
|
|
50
47
|
try {
|
|
51
|
-
const file = getCredentialsPath(env);
|
|
48
|
+
const file = getCredentialsPath(env, flags);
|
|
52
49
|
await fs.chmod(file, 0o600).catch(() => {});
|
|
53
50
|
return JSON.parse(await fs.readFile(file, 'utf8'));
|
|
54
51
|
} catch (error) {
|
|
@@ -57,9 +54,9 @@ export async function readCredentials(env = process.env) {
|
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
|
-
export async function writeCredentials(credentials, env = process.env) {
|
|
61
|
-
const home =
|
|
62
|
-
const file = getCredentialsPath(env);
|
|
57
|
+
export async function writeCredentials(credentials, env = process.env, flags = {}) {
|
|
58
|
+
const home = getEnvironmentHomeDir(env, flags);
|
|
59
|
+
const file = getCredentialsPath(env, flags);
|
|
63
60
|
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
64
61
|
await fs.mkdir(home, { recursive: true, mode: 0o700 });
|
|
65
62
|
try {
|
|
@@ -73,8 +70,8 @@ export async function writeCredentials(credentials, env = process.env) {
|
|
|
73
70
|
}
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
export async function removeCredentials(env = process.env) {
|
|
77
|
-
await fs.rm(getCredentialsPath(env), { force: true });
|
|
73
|
+
export async function removeCredentials(env = process.env, flags = {}) {
|
|
74
|
+
await fs.rm(getCredentialsPath(env, flags), { force: true });
|
|
78
75
|
}
|
|
79
76
|
|
|
80
77
|
export function maskCredentials(credentials) {
|
|
@@ -108,34 +105,17 @@ function unwrapResponseBody(body) {
|
|
|
108
105
|
return body?.data && typeof body.data === 'object' ? body.data : body;
|
|
109
106
|
}
|
|
110
107
|
|
|
111
|
-
function resolveAppSecret(flags = {}, env = process.env) {
|
|
112
|
-
return flags.appSecret
|
|
113
|
-
|| flags['app-secret']
|
|
114
|
-
|| env.SKILLHUB_UPLOAD_APP_SECRET
|
|
115
|
-
|| DEFAULT_APP_SECRET;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
108
|
function buildTokenRequestBody(fields = {}) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
code: fields.authorizationCode,
|
|
109
|
+
return {
|
|
110
|
+
authorization_code: fields.authorizationCode,
|
|
122
111
|
code_verifier: fields.codeVerifier
|
|
123
112
|
};
|
|
124
|
-
if (fields.appSecret) {
|
|
125
|
-
body.app_secret = fields.appSecret;
|
|
126
|
-
}
|
|
127
|
-
return body;
|
|
128
113
|
}
|
|
129
114
|
|
|
130
115
|
function buildRefreshRequestBody(fields = {}) {
|
|
131
|
-
|
|
132
|
-
app_id: fields.appId,
|
|
116
|
+
return {
|
|
133
117
|
refresh_token: fields.refreshToken
|
|
134
118
|
};
|
|
135
|
-
if (fields.appSecret) {
|
|
136
|
-
body.app_secret = fields.appSecret;
|
|
137
|
-
}
|
|
138
|
-
return body;
|
|
139
119
|
}
|
|
140
120
|
|
|
141
121
|
function getRejectMessage(body, fallback) {
|
|
@@ -231,14 +211,12 @@ export async function pollDeviceToken({ apiBase, deviceCode, fetchImpl = compati
|
|
|
231
211
|
}
|
|
232
212
|
|
|
233
213
|
export async function exchangeToken({
|
|
234
|
-
tokenUrl = `${
|
|
235
|
-
appId = DEFAULT_APP_ID,
|
|
236
|
-
appSecret = '',
|
|
214
|
+
tokenUrl = `${DEFAULT_API_BASE}${PATHS.EXCHANGE_CLI_OAUTH_TOKEN}`,
|
|
237
215
|
authorizationCode,
|
|
238
216
|
codeVerifier,
|
|
239
217
|
fetchImpl = compatibleFetch
|
|
240
218
|
}) {
|
|
241
|
-
const body = buildTokenRequestBody({
|
|
219
|
+
const body = buildTokenRequestBody({ authorizationCode, codeVerifier });
|
|
242
220
|
const response = await fetchImpl(tokenUrl, {
|
|
243
221
|
method: 'POST',
|
|
244
222
|
headers: { 'content-type': 'application/json' },
|
|
@@ -257,13 +235,11 @@ export async function exchangeToken({
|
|
|
257
235
|
}
|
|
258
236
|
|
|
259
237
|
export async function refreshToken({
|
|
260
|
-
tokenUrl = `${
|
|
261
|
-
appId = DEFAULT_APP_ID,
|
|
262
|
-
appSecret = '',
|
|
238
|
+
tokenUrl = `${DEFAULT_API_BASE}${PATHS.REFRESH_CLI_OAUTH_TOKEN}`,
|
|
263
239
|
refreshToken: rt,
|
|
264
240
|
fetchImpl = compatibleFetch
|
|
265
241
|
}) {
|
|
266
|
-
const body = buildRefreshRequestBody({
|
|
242
|
+
const body = buildRefreshRequestBody({ refreshToken: rt });
|
|
267
243
|
const response = await fetchImpl(tokenUrl, {
|
|
268
244
|
method: 'POST',
|
|
269
245
|
headers: { 'content-type': 'application/json' },
|
|
@@ -283,7 +259,6 @@ export async function refreshToken({
|
|
|
283
259
|
|
|
284
260
|
function buildCredentialsFromToken(token, nowMs, errorCode) {
|
|
285
261
|
const credentials = {
|
|
286
|
-
appId: token.app_id || token.appId || DEFAULT_APP_ID,
|
|
287
262
|
accessToken: token.access_token || token.accessToken || '',
|
|
288
263
|
refreshToken: token.refresh_token || token.refreshToken || '',
|
|
289
264
|
expireTimeMs: String(token.expire_time_ms || token.expireTimeMs || nowMs + Number(token.expires_in || 7200) * 1000),
|
|
@@ -306,9 +281,9 @@ function buildCredentialsFromToken(token, nowMs, errorCode) {
|
|
|
306
281
|
return credentials;
|
|
307
282
|
}
|
|
308
283
|
|
|
309
|
-
async function readPendingAuth(env, now) {
|
|
284
|
+
async function readPendingAuth(env, flags, now) {
|
|
310
285
|
try {
|
|
311
|
-
const raw = JSON.parse(await fs.readFile(getPendingAuthPath(env), 'utf8'));
|
|
286
|
+
const raw = JSON.parse(await fs.readFile(getPendingAuthPath(env, flags), 'utf8'));
|
|
312
287
|
if (raw?.expiresAtMs && raw.expiresAtMs > now() && raw.deviceCode && raw.codeVerifier) {
|
|
313
288
|
return raw;
|
|
314
289
|
}
|
|
@@ -318,26 +293,26 @@ async function readPendingAuth(env, now) {
|
|
|
318
293
|
return null;
|
|
319
294
|
}
|
|
320
295
|
|
|
321
|
-
async function writePendingAuth(env, state) {
|
|
322
|
-
const file = getPendingAuthPath(env);
|
|
296
|
+
async function writePendingAuth(env, flags, state) {
|
|
297
|
+
const file = getPendingAuthPath(env, flags);
|
|
323
298
|
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
324
299
|
await fs.writeFile(file, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
325
300
|
}
|
|
326
301
|
|
|
327
|
-
async function clearPendingAuth(env) {
|
|
328
|
-
await fs.rm(getPendingAuthPath(env), { force: true });
|
|
329
|
-
await fs.rm(getAuthQrPath(env), { force: true });
|
|
302
|
+
async function clearPendingAuth(env, flags) {
|
|
303
|
+
await fs.rm(getPendingAuthPath(env, flags), { force: true });
|
|
304
|
+
await fs.rm(getAuthQrPath(env, flags), { force: true });
|
|
330
305
|
}
|
|
331
306
|
|
|
332
|
-
export async function cancelLogin(env = process.env) {
|
|
307
|
+
export async function cancelLogin(env = process.env, flags = {}) {
|
|
333
308
|
let hadPending = true;
|
|
334
309
|
try {
|
|
335
|
-
await fs.access(getPendingAuthPath(env));
|
|
310
|
+
await fs.access(getPendingAuthPath(env, flags));
|
|
336
311
|
} catch (error) {
|
|
337
312
|
if (error.code !== 'ENOENT') throw error;
|
|
338
313
|
hadPending = false;
|
|
339
314
|
}
|
|
340
|
-
await clearPendingAuth(env);
|
|
315
|
+
await clearPendingAuth(env, flags);
|
|
341
316
|
return { hadPending };
|
|
342
317
|
}
|
|
343
318
|
|
|
@@ -349,7 +324,8 @@ export async function login({
|
|
|
349
324
|
now = () => Date.now(),
|
|
350
325
|
promptStream = process.stdout
|
|
351
326
|
} = {}) {
|
|
352
|
-
const
|
|
327
|
+
const apiBase = resolveApiBase(flags, env);
|
|
328
|
+
const cachedCredentials = await readCredentials(env, flags);
|
|
353
329
|
if (isValidCredentials(cachedCredentials, now())) {
|
|
354
330
|
return maskCredentials(cachedCredentials);
|
|
355
331
|
}
|
|
@@ -357,16 +333,13 @@ export async function login({
|
|
|
357
333
|
// access_token 过期但 refresh_token 仍有效,静默续期
|
|
358
334
|
if (canRefreshCredentials(cachedCredentials, now())) {
|
|
359
335
|
try {
|
|
360
|
-
const appSecret = resolveAppSecret(flags, env);
|
|
361
336
|
const token = await refreshToken({
|
|
362
|
-
appId: cachedCredentials.appId || DEFAULT_APP_ID,
|
|
363
|
-
appSecret,
|
|
364
337
|
refreshToken: cachedCredentials.refreshToken,
|
|
365
|
-
tokenUrl:
|
|
338
|
+
tokenUrl: `${apiBase}${PATHS.REFRESH_CLI_OAUTH_TOKEN}`,
|
|
366
339
|
fetchImpl
|
|
367
340
|
});
|
|
368
|
-
await writeCredentials(buildCredentialsFromToken(token, now(), 'OAS_TOKEN_REFRESH_INVALID_RESPONSE'), env);
|
|
369
|
-
return maskCredentials(await readCredentials(env));
|
|
341
|
+
await writeCredentials(buildCredentialsFromToken(token, now(), 'OAS_TOKEN_REFRESH_INVALID_RESPONSE'), env, flags);
|
|
342
|
+
return maskCredentials(await readCredentials(env, flags));
|
|
370
343
|
} catch (error) {
|
|
371
344
|
if (error instanceof SkillhubUploadError) {
|
|
372
345
|
// 刷新失败,降级走设备码
|
|
@@ -376,10 +349,8 @@ export async function login({
|
|
|
376
349
|
}
|
|
377
350
|
}
|
|
378
351
|
|
|
379
|
-
const apiBase = resolveApiBase(flags, env);
|
|
380
|
-
|
|
381
352
|
// 从磁盘恢复未完成的授权状态,避免进程重启后要求用户重新授权
|
|
382
|
-
let pending = await readPendingAuth(env, now);
|
|
353
|
+
let pending = await readPendingAuth(env, flags, now);
|
|
383
354
|
let codeVerifier;
|
|
384
355
|
if (pending) {
|
|
385
356
|
codeVerifier = pending.codeVerifier;
|
|
@@ -390,12 +361,12 @@ export async function login({
|
|
|
390
361
|
userCode: pending.userCode,
|
|
391
362
|
expiresInSeconds: Math.round((pending.expiresAtMs - now()) / 1000),
|
|
392
363
|
message: '检测到未完成的授权,继续等待中(无需重新打开授权链接)'
|
|
393
|
-
}, { agent: Boolean(flags.agent), env, stream: promptStream });
|
|
364
|
+
}, { agent: Boolean(flags.agent), env, flags, stream: promptStream });
|
|
394
365
|
} else {
|
|
395
366
|
const pkce = await createPkcePair();
|
|
396
367
|
codeVerifier = pkce.codeVerifier;
|
|
397
368
|
const device = await requestDeviceCode({ apiBase, pkce, fetchImpl });
|
|
398
|
-
await writePendingAuth(env, {
|
|
369
|
+
await writePendingAuth(env, flags, {
|
|
399
370
|
deviceCode: device.deviceCode,
|
|
400
371
|
codeVerifier: pkce.codeVerifier,
|
|
401
372
|
authorizeCommonUrl: device.authorizeCommonUrl,
|
|
@@ -405,7 +376,7 @@ export async function login({
|
|
|
405
376
|
pollIntervalSeconds: device.pollIntervalSeconds || 5,
|
|
406
377
|
expiresAtMs: now() + (device.expiresInSeconds || 600) * 1000
|
|
407
378
|
});
|
|
408
|
-
pending = await readPendingAuth(env, now);
|
|
379
|
+
pending = await readPendingAuth(env, flags, now);
|
|
409
380
|
await renderDeviceAuthPrompt({
|
|
410
381
|
type: 'auth_device_code',
|
|
411
382
|
authorizeCommonUrl: device.authorizeCommonUrl,
|
|
@@ -413,7 +384,7 @@ export async function login({
|
|
|
413
384
|
userCode: device.userCode,
|
|
414
385
|
expiresInSeconds: device.expiresInSeconds || 600,
|
|
415
386
|
message: '请用手机自带浏览器打开授权链接,页面会自动跳转至小红书 App 授权;授权页输入授权码后 CLI 会自动轮询'
|
|
416
|
-
}, { agent: Boolean(flags.agent), env, stream: promptStream });
|
|
387
|
+
}, { agent: Boolean(flags.agent), env, flags, stream: promptStream });
|
|
417
388
|
}
|
|
418
389
|
|
|
419
390
|
const deadline = pending.expiresAtMs;
|
|
@@ -422,7 +393,7 @@ export async function login({
|
|
|
422
393
|
while (now() < deadline) {
|
|
423
394
|
await sleep((pending.pollIntervalSeconds || 5) * 1000);
|
|
424
395
|
// 这里只检测跨进程取消,不把自然过期误判为取消。
|
|
425
|
-
const activePending = await readPendingAuth(env, () => 0);
|
|
396
|
+
const activePending = await readPendingAuth(env, flags, () => 0);
|
|
426
397
|
if (!activePending || activePending.deviceCode !== pending.deviceCode) {
|
|
427
398
|
throw new SkillhubUploadError(
|
|
428
399
|
'AUTH_CANCELLED',
|
|
@@ -437,34 +408,32 @@ export async function login({
|
|
|
437
408
|
break;
|
|
438
409
|
}
|
|
439
410
|
if (authStatus === 'expired') {
|
|
440
|
-
await clearPendingAuth(env);
|
|
411
|
+
await clearPendingAuth(env, flags);
|
|
441
412
|
throw new SkillhubUploadError('AUTH_TIMEOUT', '授权超时,请重新发起', ExitCodes.AUTH);
|
|
442
413
|
}
|
|
443
414
|
if (authStatus === 'cancelled' || authStatus === 'denied' || authStatus === 'rejected') {
|
|
444
|
-
await clearPendingAuth(env);
|
|
415
|
+
await clearPendingAuth(env, flags);
|
|
445
416
|
throw new SkillhubUploadError('AUTH_DENIED', '用户拒绝授权', ExitCodes.AUTH);
|
|
446
417
|
}
|
|
447
418
|
}
|
|
448
419
|
if (!authorizationCode) {
|
|
449
|
-
await clearPendingAuth(env);
|
|
420
|
+
await clearPendingAuth(env, flags);
|
|
450
421
|
throw new SkillhubUploadError('AUTH_TIMEOUT', '授权超时,请重新发起', ExitCodes.AUTH);
|
|
451
422
|
}
|
|
452
423
|
} catch (error) {
|
|
453
424
|
if (!(error instanceof SkillhubUploadError)) {
|
|
454
|
-
await clearPendingAuth(env);
|
|
425
|
+
await clearPendingAuth(env, flags);
|
|
455
426
|
}
|
|
456
427
|
throw error;
|
|
457
428
|
}
|
|
458
429
|
|
|
459
430
|
const token = await exchangeToken({
|
|
460
|
-
appId: DEFAULT_APP_ID,
|
|
461
|
-
appSecret: resolveAppSecret(flags, env),
|
|
462
431
|
authorizationCode,
|
|
463
432
|
codeVerifier,
|
|
464
|
-
tokenUrl:
|
|
433
|
+
tokenUrl: `${apiBase}${PATHS.EXCHANGE_CLI_OAUTH_TOKEN}`,
|
|
465
434
|
fetchImpl
|
|
466
435
|
});
|
|
467
|
-
await writeCredentials(buildCredentialsFromToken(token, now(), 'OAS_TOKEN_EXCHANGE_INVALID_RESPONSE'), env);
|
|
468
|
-
await clearPendingAuth(env);
|
|
469
|
-
return maskCredentials(await readCredentials(env));
|
|
436
|
+
await writeCredentials(buildCredentialsFromToken(token, now(), 'OAS_TOKEN_EXCHANGE_INVALID_RESPONSE'), env, flags);
|
|
437
|
+
await clearPendingAuth(env, flags);
|
|
438
|
+
return maskCredentials(await readCredentials(env, flags));
|
|
470
439
|
}
|
package/cli/config.mjs
CHANGED
|
@@ -3,9 +3,6 @@ import path from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export const DEFAULT_API_BASE = 'https://edith.xiaohongshu.com';
|
|
5
5
|
export const BETA_API_BASE = 'https://edith-redskill.sl.beta.xiaohongshu.com';
|
|
6
|
-
export const DEFAULT_OAS_BASE = 'https://openaccount.xiaohongshu.com';
|
|
7
|
-
export const DEFAULT_APP_ID = 'xhsyfHfmZQAQ24o';
|
|
8
|
-
export const DEFAULT_APP_SECRET = 'skHlFZ8BTk2bVmsSt1aYjv1NxcJjuOwhSev4oraniDa6eyn8L9oqBWvCtsEBnmCmWG';
|
|
9
6
|
export const DEFAULT_SCOPES = ['base_info', 'skill_publish'];
|
|
10
7
|
export const DEFAULT_SCOPE = DEFAULT_SCOPES.join(',');
|
|
11
8
|
|
|
@@ -14,8 +11,8 @@ export const PATHS = {
|
|
|
14
11
|
SUBMIT_SKILL_VERSION: '/api/sns/v1/creator/red_skill/cli_submit_skill_version',
|
|
15
12
|
CREATE_CLI_OAUTH_DEVICE_CODE: '/api/sns/v1/creator/red_skill/create_cli_oauth_device_code',
|
|
16
13
|
POLL_CLI_OAUTH_DEVICE_TOKEN: '/api/sns/v1/creator/red_skill/poll_cli_oauth_device_token',
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
EXCHANGE_CLI_OAUTH_TOKEN: '/api/sns/v1/creator/red_skill/exchange_cli_oauth_token',
|
|
15
|
+
REFRESH_CLI_OAUTH_TOKEN: '/api/sns/v1/creator/red_skill/refresh_cli_oauth_token',
|
|
19
16
|
QUERY_CONTENT_TAG_CONFIG: '/api/sns/v1/activity_platform/config/query_config'
|
|
20
17
|
};
|
|
21
18
|
export const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
|
@@ -28,24 +25,29 @@ export function getHomeDir(env = process.env) {
|
|
|
28
25
|
return env.SKILLHUB_UPLOAD_HOME || path.join(os.homedir(), '.skillhub-upload');
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
export function
|
|
32
|
-
|
|
28
|
+
export function getEnvironmentHomeDir(env = process.env, flags = {}) {
|
|
29
|
+
const home = getHomeDir(env);
|
|
30
|
+
return resolveApiEnvironment(flags) === 'beta' ? path.join(home, 'beta') : home;
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
export function
|
|
36
|
-
return path.join(
|
|
33
|
+
export function getCredentialsPath(env = process.env, flags = {}) {
|
|
34
|
+
return path.join(getEnvironmentHomeDir(env, flags), 'credentials.json');
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
export function
|
|
40
|
-
return path.join(
|
|
37
|
+
export function getTmpDir(env = process.env, flags = {}) {
|
|
38
|
+
return path.join(getEnvironmentHomeDir(env, flags), 'tmp');
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
export function
|
|
44
|
-
return path.join(
|
|
41
|
+
export function getContentTagCachePath(env = process.env, flags = {}) {
|
|
42
|
+
return path.join(getEnvironmentHomeDir(env, flags), 'content-tags.json');
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
export function
|
|
48
|
-
return path.join(
|
|
45
|
+
export function getPendingAuthPath(env = process.env, flags = {}) {
|
|
46
|
+
return path.join(getEnvironmentHomeDir(env, flags), 'pending-auth.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getAuthQrPath(env = process.env, flags = {}) {
|
|
50
|
+
return path.join(getEnvironmentHomeDir(env, flags), 'auth-qr.png');
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export function resolveApiEnvironment(flags = {}) {
|
package/cli/index.mjs
CHANGED
|
@@ -93,7 +93,7 @@ export async function ensurePublishCredentials({
|
|
|
93
93
|
readCredentials,
|
|
94
94
|
login
|
|
95
95
|
}) {
|
|
96
|
-
let credentials = await readCredentials(env);
|
|
96
|
+
let credentials = await readCredentials(env, flags);
|
|
97
97
|
try {
|
|
98
98
|
assertUsableCredentials(credentials, nowMs);
|
|
99
99
|
return credentials;
|
|
@@ -111,7 +111,7 @@ export async function ensurePublishCredentials({
|
|
|
111
111
|
now: io.now,
|
|
112
112
|
promptStream: io.out || process.stdout
|
|
113
113
|
});
|
|
114
|
-
credentials = await readCredentials(env);
|
|
114
|
+
credentials = await readCredentials(env, flags);
|
|
115
115
|
assertUsableCredentials(credentials, io.now ? io.now() : nowMs);
|
|
116
116
|
return credentials;
|
|
117
117
|
}
|
|
@@ -182,7 +182,7 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
182
182
|
if (!command) {
|
|
183
183
|
throw new SkillhubUploadError(
|
|
184
184
|
'INVALID_ARGS',
|
|
185
|
-
'请指定要执行的操作:login、publish、whoami 或 logout',
|
|
185
|
+
'请指定要执行的操作:login、publish、tags、whoami 或 logout',
|
|
186
186
|
ExitCodes.INVALID_ARGS
|
|
187
187
|
);
|
|
188
188
|
}
|
|
@@ -193,19 +193,29 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
193
193
|
}
|
|
194
194
|
if (command === 'whoami') {
|
|
195
195
|
const { readCredentials, maskCredentials } = await import('./auth.mjs');
|
|
196
|
-
writeResult({ status: 'ok', command, credentials: maskCredentials(await readCredentials(env)) }, out);
|
|
196
|
+
writeResult({ status: 'ok', command, credentials: maskCredentials(await readCredentials(env, flags)) }, out);
|
|
197
197
|
return;
|
|
198
198
|
}
|
|
199
199
|
if (command === 'logout') {
|
|
200
200
|
const { removeCredentials } = await import('./auth.mjs');
|
|
201
|
-
await removeCredentials(env);
|
|
201
|
+
await removeCredentials(env, flags);
|
|
202
202
|
writeResult({ status: 'ok', command }, out);
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
205
|
+
if (command === 'tags') {
|
|
206
|
+
const tags = await loadContentTags({
|
|
207
|
+
fetchImpl: io.fetchImpl,
|
|
208
|
+
env,
|
|
209
|
+
now: io.now,
|
|
210
|
+
flags
|
|
211
|
+
});
|
|
212
|
+
writeResult({ status: 'ok', command, tags }, out);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
205
215
|
if (command === 'login') {
|
|
206
216
|
if (flags.cancel) {
|
|
207
217
|
const { cancelLogin } = await import('./auth.mjs');
|
|
208
|
-
const cancelled = await cancelLogin(env);
|
|
218
|
+
const cancelled = await cancelLogin(env, flags);
|
|
209
219
|
writeResult({ status: 'cancelled', command, ...cancelled }, out);
|
|
210
220
|
return;
|
|
211
221
|
}
|
|
@@ -240,7 +250,7 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
240
250
|
});
|
|
241
251
|
}
|
|
242
252
|
|
|
243
|
-
const bundle = await prepareBundle(pathArg, { env });
|
|
253
|
+
const bundle = await prepareBundle(pathArg, { env, flags });
|
|
244
254
|
const bundleMetadata = {
|
|
245
255
|
bundleSha256: bundle.bundleSha256,
|
|
246
256
|
bundleSizeBytes: bundle.bundleSizeBytes
|
|
@@ -298,7 +308,7 @@ export async function main(argv = process.argv.slice(2), env = process.env, io =
|
|
|
298
308
|
promptSession.close();
|
|
299
309
|
}
|
|
300
310
|
}
|
|
301
|
-
throw new SkillhubUploadError('INVALID_ARGS', `不支持的操作:${command},可用操作:login、publish、whoami、logout`, ExitCodes.INVALID_ARGS);
|
|
311
|
+
throw new SkillhubUploadError('INVALID_ARGS', `不支持的操作:${command},可用操作:login、publish、tags、whoami、logout`, ExitCodes.INVALID_ARGS);
|
|
302
312
|
}
|
|
303
313
|
|
|
304
314
|
function isMainModule() {
|
package/cli/pack.mjs
CHANGED
|
@@ -267,8 +267,8 @@ function isAllowedExtension(file) {
|
|
|
267
267
|
return ALLOWED_EXTENSIONS.has(path.extname(file).toLowerCase());
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
async function createTempZipPath(env) {
|
|
271
|
-
const dir = await fs.mkdtemp(path.join(getTmpDir(env), `bundle-${process.pid}-`));
|
|
270
|
+
async function createTempZipPath(env, flags) {
|
|
271
|
+
const dir = await fs.mkdtemp(path.join(getTmpDir(env, flags), `bundle-${process.pid}-`));
|
|
272
272
|
return path.join(dir, `${crypto.randomUUID()}.zip`);
|
|
273
273
|
}
|
|
274
274
|
|
|
@@ -441,14 +441,14 @@ async function prepareZipSource(inputPath) {
|
|
|
441
441
|
};
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
-
async function buildBundle(source, env) {
|
|
444
|
+
async function buildBundle(source, env, flags) {
|
|
445
445
|
const parsed = parseSkillMarkdown(source.markdown);
|
|
446
446
|
const zip = new AdmZip();
|
|
447
447
|
for (const file of source.files) {
|
|
448
448
|
zip.addFile(file.entryName, file.data);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
const zipPath = await createTempZipPath(env);
|
|
451
|
+
const zipPath = await createTempZipPath(env, flags);
|
|
452
452
|
zip.writeZip(zipPath);
|
|
453
453
|
const zipStat = await fs.stat(zipPath);
|
|
454
454
|
|
|
@@ -468,6 +468,7 @@ async function buildBundle(source, env) {
|
|
|
468
468
|
|
|
469
469
|
export async function prepareBundle(inputPath, options = {}) {
|
|
470
470
|
const env = options.env || process.env;
|
|
471
|
+
const flags = options.flags || {};
|
|
471
472
|
const stat = await fs.lstat(inputPath).catch(() => null);
|
|
472
473
|
if (!stat) {
|
|
473
474
|
throw new SkillhubUploadError('LOCAL_VALIDATION_FAILED', `路径不存在:${inputPath},请输入正确的 skill 目录或 zip 文件`, ExitCodes.LOCAL_VALIDATION);
|
|
@@ -475,7 +476,7 @@ export async function prepareBundle(inputPath, options = {}) {
|
|
|
475
476
|
if (stat.isSymbolicLink()) {
|
|
476
477
|
throw new SkillhubUploadError('LOCAL_VALIDATION_FAILED', '输入路径是符号链接,请输入真实的 skill 目录或 zip 文件', ExitCodes.LOCAL_VALIDATION);
|
|
477
478
|
}
|
|
478
|
-
await fs.mkdir(getTmpDir(env), { recursive: true, mode: 0o700 });
|
|
479
|
+
await fs.mkdir(getTmpDir(env, flags), { recursive: true, mode: 0o700 });
|
|
479
480
|
|
|
480
481
|
let source;
|
|
481
482
|
if (stat.isDirectory()) {
|
|
@@ -490,5 +491,5 @@ export async function prepareBundle(inputPath, options = {}) {
|
|
|
490
491
|
);
|
|
491
492
|
}
|
|
492
493
|
|
|
493
|
-
return await buildBundle(source, env);
|
|
494
|
+
return await buildBundle(source, env, flags);
|
|
494
495
|
}
|
package/cli/tags.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
CONTENT_TAG_MODULE_ID,
|
|
8
8
|
PATHS,
|
|
9
9
|
getContentTagCachePath,
|
|
10
|
-
|
|
10
|
+
getEnvironmentHomeDir,
|
|
11
11
|
resolveApiBase
|
|
12
12
|
} from './config.mjs';
|
|
13
13
|
import { compatibleFetch } from './fetch.mjs';
|
|
@@ -122,7 +122,7 @@ export async function loadContentTags({
|
|
|
122
122
|
if (env.SKILLHUB_UPLOAD_SKIP_REMOTE_TAGS === '1') {
|
|
123
123
|
return FALLBACK_CONTENT_TAGS.map((tag) => ({ ...tag }));
|
|
124
124
|
}
|
|
125
|
-
const cachePath = getContentTagCachePath(env);
|
|
125
|
+
const cachePath = getContentTagCachePath(env, flags);
|
|
126
126
|
const nowMs = typeof now === 'function' ? now() : Number(now) || Date.now();
|
|
127
127
|
const cached = await readCachedContentTags(cachePath);
|
|
128
128
|
if (cached && nowMs - cached.fetchedAtMs < cacheTtlMs) {
|
|
@@ -145,6 +145,6 @@ export async function loadContentTags({
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
export function getContentTagsHomeDir(env = process.env) {
|
|
149
|
-
return
|
|
148
|
+
export function getContentTagsHomeDir(env = process.env, flags = {}) {
|
|
149
|
+
return getEnvironmentHomeDir(env, flags);
|
|
150
150
|
}
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -9,12 +9,28 @@ description: 当用户要求安装或更新 redskillhub-upload、按照 upload.m
|
|
|
9
9
|
|
|
10
10
|
所有 CLI 命令都必须显式带上同一个环境参数:生产环境用 `--env prod`(也是 CLI 默认值),测试环境用 `--env beta`。用户未指定时选择 `prod`;用户明确说 beta、测试环境或联调环境时选择 `beta`。确定后记为 `<ENV>`,本次流程中的标签、dry-run、真实 publish、login、whoami、logout 和取消等待命令均追加 `--env <ENV>`,不得混用。beta 业务接口由 CLI 自动选择 `https://edith.beta.xiaohongshu.com`,Agent 不自行传 `--api-base`。
|
|
11
11
|
|
|
12
|
+
CLI 按环境隔离本地状态:prod 保留在 `~/.skillhub-upload`,beta 存放在其 `beta` 子目录。凭证、待授权状态、二维码、标签缓存和临时包不会跨环境复用。设置 `SKILLHUB_UPLOAD_HOME` 时,该路径作为环境目录的共同根目录。
|
|
13
|
+
|
|
14
|
+
## 操作系统兼容
|
|
15
|
+
|
|
16
|
+
本 Skill 必须同时支持 macOS 和 Windows。执行前通过 Node.js 判断系统,不依赖 `uname`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
node -p "process.platform"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- 输出 `darwin`:按 macOS 处理,路径形如 `/Users/name/path/to/skill`。
|
|
23
|
+
- 输出 `win32`:按 Windows 处理,优先使用 PowerShell,路径形如 `C:\Users\name\path\to\skill`。
|
|
24
|
+
- 所有传给 CLI 的本地路径都使用双引号包裹,兼容空格和中文。
|
|
25
|
+
- 命令必须写成单行,不使用 Bash 的 `\` 续行、`printf`、`export`,也不使用 PowerShell 反引号续行或 `$env:`。
|
|
26
|
+
- 需要向 stdin 写入确认值时,统一使用 `node -e "process.stdout.write('submit\n')"` 或 `node -e "process.stdout.write('cancel\n')"`,不要按系统拼两套管道命令。
|
|
27
|
+
|
|
12
28
|
## CLI 版本门禁
|
|
13
29
|
|
|
14
30
|
在执行任何 `redskillhub-upload` 命令前,先从本 Skill 目录执行:
|
|
15
31
|
|
|
16
32
|
```bash
|
|
17
|
-
node <SKILL_DIR>/scripts/ensure-cli.mjs --env <ENV>
|
|
33
|
+
node "<SKILL_DIR>/scripts/ensure-cli.mjs" --env <ENV>
|
|
18
34
|
```
|
|
19
35
|
|
|
20
36
|
`<SKILL_DIR>` 是包含本文件的目录,执行前替换为其绝对路径。
|
|
@@ -53,17 +69,17 @@ node <SKILL_DIR>/scripts/ensure-cli.mjs --env <ENV>
|
|
|
53
69
|
|
|
54
70
|
## publish 子流程
|
|
55
71
|
|
|
56
|
-
固定 4 步,按顺序执行;步骤 1 用 AskUserQuestion 一次性收齐参数,步骤 2
|
|
72
|
+
固定 4 步,按顺序执行;步骤 1 用 AskUserQuestion 一次性收齐参数,步骤 2 把参数全部以 CLI flag 传入,步骤 3 仅通过跨平台 Node 管道写入提交或取消指令。
|
|
57
73
|
|
|
58
74
|
### Step 1 — 一次性收齐 publish 参数
|
|
59
75
|
|
|
60
|
-
(a)
|
|
76
|
+
(a) 通过已安装的 CLI 拉一次当前环境标签列表,命令:
|
|
61
77
|
|
|
62
78
|
```bash
|
|
63
|
-
|
|
79
|
+
redskillhub-upload tags --env <ENV>
|
|
64
80
|
```
|
|
65
81
|
|
|
66
|
-
|
|
82
|
+
读取 `RESULT_JSON.tags` 中的 `name` 和 `tagId`。命令失败时如实报告并停止,不直接 curl 内部接口、不导入仓库源码,也**不硬编码标签清单**。
|
|
67
83
|
|
|
68
84
|
(b) 用 AskUserQuestion 一次发两题:
|
|
69
85
|
|
|
@@ -75,8 +91,7 @@ node -e "import('./cli/tags.mjs').then(({loadContentTags}) => loadContentTags({f
|
|
|
75
91
|
### Step 2 — dry-run 出待提交载荷给用户审阅
|
|
76
92
|
|
|
77
93
|
```bash
|
|
78
|
-
redskillhub-upload publish <absolute-path> --dry-run --agent
|
|
79
|
-
--source <original|repost> --tag <中文标签名[,中文标签名...]> [--repost-source <来源名>] --env <ENV>
|
|
94
|
+
redskillhub-upload publish "<absolute-path>" --dry-run --agent --source <original-or-repost> --tag "<中文标签名[,中文标签名...]>" [--repost-source "<来源名>"] --env <ENV>
|
|
80
95
|
```
|
|
81
96
|
|
|
82
97
|
读 `RESULT_JSON.payload`,把关键字段摘出来给用户看:`name`、Skill ID(payload 字段 `skill_identifier`)、`version`、`description`、`original`、`repost_source`、`content_tag_ids`。标签字段展示**中文名列表**,不要只甩 `tagId`。
|
|
@@ -91,11 +106,10 @@ redskillhub-upload publish <absolute-path> --dry-run --agent \
|
|
|
91
106
|
### Step 3 — 用户明确说「提交 / 确认 / submit」后真实提交
|
|
92
107
|
|
|
93
108
|
```bash
|
|
94
|
-
|
|
95
|
-
--source <original|repost> --tag <中文标签名[,中文标签名...]> [--repost-source <来源名>] --env <ENV>
|
|
109
|
+
node -e "process.stdout.write('submit\n')" | redskillhub-upload publish "<absolute-path>" --agent --source <original-or-repost> --tag "<中文标签名[,中文标签名...]>" [--repost-source "<来源名>"] --env <ENV>
|
|
96
110
|
```
|
|
97
111
|
|
|
98
|
-
不要带 `--yes`,让 CLI 进 confirm 阶段;`submit\n` 从 stdin 推给它即可。用户说「取消 / cancel
|
|
112
|
+
不要带 `--yes`,让 CLI 进 confirm 阶段;`submit\n` 从 stdin 推给它即可。用户说「取消 / cancel」就把输入命令换成 `node -e "process.stdout.write('cancel\n')"`。用户回复「好了」不能当成提交触发词。
|
|
99
113
|
|
|
100
114
|
该命令会自动检查并恢复登录:凭证有效时直接发布,access token 过期时尝试静默刷新,必须重新授权时输出设备码 `PROMPT`;授权成功后仍由当前进程继续上传和提交,不要另起 `login` 命令。
|
|
101
115
|
|
|
@@ -109,8 +123,8 @@ printf 'submit\n' | redskillhub-upload publish <absolute-path> --agent \
|
|
|
109
123
|
上传入口接受本地 skill 目录或 `.zip` 源包;zip 必须由 CLI 在本地解包、过滤、校验并重新生成上传包,不能由 agent 直接上传或改包。
|
|
110
124
|
|
|
111
125
|
```bash
|
|
112
|
-
redskillhub-upload publish
|
|
113
|
-
redskillhub-upload publish
|
|
126
|
+
redskillhub-upload publish "<absolute-path-to-skill>" --agent --env <ENV>
|
|
127
|
+
redskillhub-upload publish "<absolute-path-to-skill.zip>" --agent --env <ENV>
|
|
114
128
|
```
|
|
115
129
|
|
|
116
130
|
`redskillhub-upload whoami --env <ENV>`、`redskillhub-upload login --agent --env <ENV>`、`redskillhub-upload login --cancel --env <ENV>` 仅用于诊断、显式预登录或取消等待,不属于标准发布前置步骤。
|
|
@@ -118,7 +132,7 @@ redskillhub-upload publish /absolute/path/to/skill.zip --agent --env <ENV>
|
|
|
118
132
|
本地验证可使用 dry-run。标签可直接传一个或多个中文名(CLI 内部完成 name→id 映射,多个用逗号分隔),也允许调试时传数字 id:
|
|
119
133
|
|
|
120
134
|
```bash
|
|
121
|
-
node cli/index.mjs publish test/fixtures/minimal-skill --dry-run --agent --source original --tag 效率工具,内容创作 --yes --env beta
|
|
135
|
+
node cli/index.mjs publish "test/fixtures/minimal-skill" --dry-run --agent --source original --tag "效率工具,内容创作" --yes --env beta
|
|
122
136
|
# 或调试用:--tag-id 1001,1002
|
|
123
137
|
```
|
|
124
138
|
|
|
@@ -64,10 +64,16 @@ export function selectLatestCompatibleVersion(versions, options = {}) {
|
|
|
64
64
|
return candidates.at(-1).raw;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function shouldUseSystemShell(platform = process.platform) {
|
|
68
|
+
return platform === 'win32';
|
|
69
|
+
}
|
|
70
|
+
|
|
67
71
|
function run(command, args, options = {}) {
|
|
68
72
|
const result = spawnSync(command, args, {
|
|
69
73
|
encoding: 'utf8',
|
|
70
|
-
stdio: options.capture ? 'pipe' : 'inherit'
|
|
74
|
+
stdio: options.capture ? 'pipe' : 'inherit',
|
|
75
|
+
// npm 与全局 CLI 在 Windows 上是 .cmd shim,需要通过系统 shell 启动。
|
|
76
|
+
shell: shouldUseSystemShell(options.platform)
|
|
71
77
|
});
|
|
72
78
|
if (result.error) throw result.error;
|
|
73
79
|
if (!options.allowFailure && result.status !== 0) {
|