redskillhub-upload 1.0.0-alpha.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/cli/submit.mjs ADDED
@@ -0,0 +1,253 @@
1
+ import { deriveIdentifier } from './pack.mjs';
2
+ import { resolveApiBase, PATHS } from './config.mjs';
3
+ import { ExitCodes, SkillhubUploadError } from './errors.mjs';
4
+ import { compatibleFetch } from './fetch.mjs';
5
+ import { redactMessage } from './redact.mjs';
6
+ import { FALLBACK_CONTENT_TAGS } from './tags.mjs';
7
+
8
+ const EDITABLE_FIELDS = new Set(['name', 'identifier', 'version', 'description', 'detail', 'tag']);
9
+ const VALID_SOURCES = new Set(['original', 'repost']);
10
+ const SENSITIVE_PAYLOAD_FIELDS = new Set([
11
+ 'access_token',
12
+ 'refresh_token',
13
+ 'token',
14
+ 'bundle_file_id',
15
+ 'bundle_sha256',
16
+ 'skill_md_content'
17
+ ]);
18
+
19
+ export const CONTENT_TAG_OPTIONS = FALLBACK_CONTENT_TAGS;
20
+
21
+ function normalizeTagText(value) {
22
+ return String(value || '').trim().toLowerCase().replace(/\s+/g, '');
23
+ }
24
+
25
+ function resolveNamedTagId(rawTag, tagOptions) {
26
+ const normalized = normalizeTagText(rawTag);
27
+ const matched = tagOptions.find((tag) => {
28
+ const names = [tag.tagId, tag.name, ...(tag.aliases || [])];
29
+ return names.some((name) => normalizeTagText(name) === normalized);
30
+ });
31
+ return matched?.tagId || '';
32
+ }
33
+
34
+ function splitTagValues(value) {
35
+ if (value === undefined || value === null || value === '') {
36
+ return [];
37
+ }
38
+ if (Array.isArray(value)) {
39
+ return value.flatMap(splitTagValues);
40
+ }
41
+ return String(value).split(/[,,]/)
42
+ .map((item) => item.trim())
43
+ .filter(Boolean);
44
+ }
45
+
46
+ export function formatContentTagOptions(tagOptions = FALLBACK_CONTENT_TAGS) {
47
+ return tagOptions.map((tag) => tag.name).join(' / ');
48
+ }
49
+
50
+ function resolveTagIds(flags, tagOptions = FALLBACK_CONTENT_TAGS) {
51
+ const rawTagId = flags.tagId ?? flags['tag-id'] ?? flags.tag;
52
+ const rawTags = splitTagValues(rawTagId);
53
+ if (rawTags.length === 0) {
54
+ return [];
55
+ }
56
+
57
+ const tagIds = [];
58
+ const seen = new Set();
59
+ for (const rawTag of rawTags) {
60
+ const tagId = /^\d+$/.test(rawTag) ? rawTag : resolveNamedTagId(rawTag, tagOptions);
61
+ if (!tagId) {
62
+ throw new Error(`未找到内容标签:${rawTag},请选择:${formatContentTagOptions(tagOptions)}`);
63
+ }
64
+ if (seen.has(tagId)) {
65
+ continue;
66
+ }
67
+ seen.add(tagId);
68
+ tagIds.push(tagId);
69
+ }
70
+ return tagIds;
71
+ }
72
+
73
+ function resolveSourceFields(flags) {
74
+ if (flags.source === undefined || flags.source === null || flags.source === '') {
75
+ return {};
76
+ }
77
+
78
+ const source = String(flags.source).trim();
79
+ if (!VALID_SOURCES.has(source)) {
80
+ throw new Error('内容来源只能选择“原创”或“转载”');
81
+ }
82
+
83
+ if (source === 'original') {
84
+ return { original: true, repost_source: '' };
85
+ }
86
+
87
+ const repostSource = String(flags.repostSource ?? flags['repost-source'] ?? '').trim();
88
+ if (!repostSource) {
89
+ throw new Error('选择转载时,请填写转载来源');
90
+ }
91
+ return { original: false, repost_source: repostSource };
92
+ }
93
+
94
+ function stripVersionSuffix(value) {
95
+ return String(value || '').replace(/-v?\d+(\.\d+)*$/, '');
96
+ }
97
+
98
+ function resolveIdentifier(flags, metadata) {
99
+ if (flags.identifier) {
100
+ return String(flags.identifier).trim();
101
+ }
102
+ const fromName = deriveIdentifier(metadata.name || '');
103
+ if (fromName) {
104
+ return fromName;
105
+ }
106
+ const fromBaseName = deriveIdentifier(stripVersionSuffix(metadata.sourcePathBaseName || ''));
107
+ if (fromBaseName) {
108
+ return fromBaseName;
109
+ }
110
+ throw new Error(
111
+ `Skill ID 为空,无法从名称“${metadata.name || ''}”或目录名“${metadata.sourcePathBaseName || ''}”自动生成,请输入 Skill ID`
112
+ );
113
+ }
114
+
115
+ export function buildDraftPayload({ flags = {}, metadata = {}, bundle = {}, tagOptions = FALLBACK_CONTENT_TAGS }) {
116
+ const identifier = resolveIdentifier(flags, metadata);
117
+ const contentTagIds = resolveTagIds(flags, tagOptions);
118
+ const sourceFields = resolveSourceFields(flags);
119
+ return {
120
+ skill_identifier: identifier,
121
+ version: flags.version || metadata.version || '1.0.0',
122
+ name: flags.name || metadata.name || identifier,
123
+ description: flags.description || metadata.description || '',
124
+ skill_md_content: flags.detail || metadata.detail || '',
125
+ ...sourceFields,
126
+ content_tag_ids: contentTagIds,
127
+ bundle_file_id: bundle.bundleFileId || '',
128
+ bundle_sha256: bundle.bundleSha256,
129
+ bundle_size_bytes: bundle.bundleSizeBytes
130
+ };
131
+ }
132
+
133
+ export function applyConfirmEdit(payload, values, { tagOptions = FALLBACK_CONTENT_TAGS } = {}) {
134
+ for (const key of Object.keys(values)) {
135
+ if (!EDITABLE_FIELDS.has(key)) {
136
+ throw new Error(`提交确认阶段不能修改 ${key},请取消后重新发布`);
137
+ }
138
+ }
139
+
140
+ return {
141
+ ...payload,
142
+ skill_identifier: values.identifier || payload.skill_identifier,
143
+ version: values.version || payload.version,
144
+ name: values.name || payload.name,
145
+ description: values.description || payload.description,
146
+ skill_md_content: values.detail || payload.skill_md_content,
147
+ content_tag_ids: values.tag ? resolveTagIds({ tag: values.tag }, tagOptions) : payload.content_tag_ids
148
+ };
149
+ }
150
+
151
+ function collectSensitiveValues(payload = {}, accessToken = '') {
152
+ const values = [];
153
+ if (accessToken) {
154
+ values.push(String(accessToken));
155
+ }
156
+ for (const [key, value] of Object.entries(payload || {})) {
157
+ if (!SENSITIVE_PAYLOAD_FIELDS.has(key) || value === undefined || value === null || value === '') {
158
+ continue;
159
+ }
160
+ values.push(String(value));
161
+ }
162
+ return values;
163
+ }
164
+
165
+ function unwrapResponseBody(body) {
166
+ return body?.data && typeof body.data === 'object' ? body.data : body;
167
+ }
168
+
169
+ function getRejectMessage(body) {
170
+ const data = unwrapResponseBody(body);
171
+ return data?.result?.message
172
+ || data?.result?.errorMessage
173
+ || data?.message
174
+ || data?.msg
175
+ || body?.result?.message
176
+ || body?.result?.errorMessage
177
+ || body?.message
178
+ || body?.msg
179
+ || '提交被拒绝';
180
+ }
181
+
182
+ function isRejectedBody(body) {
183
+ const data = unwrapResponseBody(body);
184
+ if (
185
+ body?.result?.success === false
186
+ || body?.success === false
187
+ || data?.result?.success === false
188
+ || data?.success === false
189
+ ) {
190
+ return true;
191
+ }
192
+ const codes = [
193
+ body?.code,
194
+ body?.result?.code,
195
+ data?.code,
196
+ data?.result?.code
197
+ ].filter((code) => code !== undefined && code !== null && code !== '');
198
+
199
+ return codes.some((code) => code !== 0 && code !== '0' && code !== 'OK');
200
+ }
201
+
202
+ export async function submitSkillVersion(payload, options = {}) {
203
+ const apiBase = options.apiBase || resolveApiBase(options.flags || {});
204
+ const fetchImpl = options.fetchImpl || compatibleFetch;
205
+ const sensitiveValues = collectSensitiveValues(payload, options.accessToken);
206
+ let response;
207
+
208
+ try {
209
+ response = await fetchImpl(`${apiBase}${PATHS.SUBMIT_SKILL_VERSION}`, {
210
+ method: 'POST',
211
+ headers: {
212
+ authorization: `Bearer ${options.accessToken}`,
213
+ 'content-type': 'application/json'
214
+ },
215
+ body: JSON.stringify(payload)
216
+ });
217
+ } catch (error) {
218
+ throw new SkillhubUploadError(
219
+ 'SUBMIT_FAILED',
220
+ redactMessage(`提交失败: ${error?.message || String(error)}`, sensitiveValues),
221
+ ExitCodes.SUBMIT_FAILED
222
+ );
223
+ }
224
+
225
+ let body;
226
+ try {
227
+ body = await response.json();
228
+ } catch (error) {
229
+ throw new SkillhubUploadError(
230
+ 'SUBMIT_FAILED',
231
+ redactMessage(`提交失败: 响应解析失败`, sensitiveValues),
232
+ ExitCodes.SUBMIT_FAILED
233
+ );
234
+ }
235
+
236
+ if (!response.ok) {
237
+ throw new SkillhubUploadError(
238
+ 'SUBMIT_FAILED',
239
+ redactMessage(`提交失败: HTTP ${response.status}`, sensitiveValues),
240
+ ExitCodes.SUBMIT_FAILED
241
+ );
242
+ }
243
+
244
+ if (isRejectedBody(body)) {
245
+ throw new SkillhubUploadError(
246
+ 'SUBMIT_REJECTED',
247
+ redactMessage(getRejectMessage(body), sensitiveValues),
248
+ ExitCodes.SUBMIT_REJECTED
249
+ );
250
+ }
251
+
252
+ return unwrapResponseBody(body);
253
+ }
package/cli/tags.mjs ADDED
@@ -0,0 +1,149 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {
4
+ CONTENT_TAG_CACHE_TTL_MS,
5
+ CONTENT_TAG_FETCH_TIMEOUT_MS,
6
+ CONTENT_TAG_MATERIAL_ID,
7
+ CONTENT_TAG_MODULE_ID,
8
+ PATHS,
9
+ getContentTagCachePath,
10
+ getHomeDir,
11
+ resolveApiBase
12
+ } from './config.mjs';
13
+ import { compatibleFetch } from './fetch.mjs';
14
+
15
+ export const FALLBACK_CONTENT_TAGS = Object.freeze([
16
+ Object.freeze({ tagId: '1001', name: '效率工具' }),
17
+ Object.freeze({ tagId: '1002', name: '内容创作' }),
18
+ Object.freeze({ tagId: '1003', name: '学习成长' }),
19
+ Object.freeze({ tagId: '1004', name: '职场办公' }),
20
+ Object.freeze({ tagId: '1005', name: '编程开发' }),
21
+ Object.freeze({ tagId: '1006', name: '生活决策' }),
22
+ Object.freeze({ tagId: '1007', name: '金融理财' }),
23
+ Object.freeze({ tagId: '1008', name: '其它' })
24
+ ]);
25
+
26
+ function normalizeRemoteTag(raw) {
27
+ const tagId = String(raw?.tagId ?? raw?.tag_id ?? '').trim();
28
+ const name = String(raw?.tagName ?? raw?.tag_name ?? raw?.name ?? '').trim();
29
+ if (!tagId || !name) {
30
+ return null;
31
+ }
32
+ return { tagId, name };
33
+ }
34
+
35
+ function normalizeRemoteList(data) {
36
+ if (!Array.isArray(data)) {
37
+ return [];
38
+ }
39
+ const seen = new Set();
40
+ const list = [];
41
+ for (const item of data) {
42
+ const tag = normalizeRemoteTag(item);
43
+ if (!tag || seen.has(tag.tagId)) {
44
+ continue;
45
+ }
46
+ seen.add(tag.tagId);
47
+ list.push(tag);
48
+ }
49
+ return list;
50
+ }
51
+
52
+ export async function fetchContentTags({
53
+ fetchImpl = compatibleFetch,
54
+ apiBase,
55
+ signal,
56
+ flags = {}
57
+ } = {}) {
58
+ const base = apiBase || resolveApiBase(flags);
59
+ const url = `${base}${PATHS.QUERY_CONTENT_TAG_CONFIG}?material_id=${CONTENT_TAG_MATERIAL_ID}&module_id=${CONTENT_TAG_MODULE_ID}`;
60
+ const response = await fetchImpl(url, { method: 'GET', signal });
61
+ if (!response.ok) {
62
+ throw new Error(`tag config HTTP ${response.status}`);
63
+ }
64
+ const body = await response.json();
65
+ if (body?.success === false || (body?.code !== undefined && body.code !== 0 && body.code !== '0')) {
66
+ throw new Error(body?.msg || body?.message || 'tag config rejected');
67
+ }
68
+ const list = normalizeRemoteList(body?.data);
69
+ if (list.length === 0) {
70
+ throw new Error('tag config returned empty list');
71
+ }
72
+ return list;
73
+ }
74
+
75
+ async function readCachedContentTags(cachePath) {
76
+ try {
77
+ const raw = await fs.readFile(cachePath, 'utf8');
78
+ const parsed = JSON.parse(raw);
79
+ const tags = normalizeRemoteList(parsed?.tags);
80
+ if (tags.length === 0) {
81
+ return null;
82
+ }
83
+ const fetchedAtMs = Number(parsed?.fetchedAtMs);
84
+ return { tags, fetchedAtMs: Number.isFinite(fetchedAtMs) ? fetchedAtMs : 0 };
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+
90
+ async function writeCachedContentTags(cachePath, tags, nowMs) {
91
+ try {
92
+ await fs.mkdir(path.dirname(cachePath), { recursive: true });
93
+ await fs.writeFile(
94
+ cachePath,
95
+ JSON.stringify({ fetchedAtMs: nowMs, tags }, null, 2),
96
+ 'utf8'
97
+ );
98
+ } catch (error) {
99
+ // best-effort cache write; do not break main flow
100
+ process.stderr.write(`[redskillhub-upload] warn: cache write failed: ${error?.message || error}\n`);
101
+ }
102
+ }
103
+
104
+ function withTimeout(timeoutMs) {
105
+ if (!timeoutMs || timeoutMs <= 0) {
106
+ return { signal: undefined, cancel: () => {} };
107
+ }
108
+ const controller = new AbortController();
109
+ const timer = setTimeout(() => controller.abort(new Error(`tag config fetch timed out after ${timeoutMs}ms`)), timeoutMs);
110
+ return { signal: controller.signal, cancel: () => clearTimeout(timer) };
111
+ }
112
+
113
+ export async function loadContentTags({
114
+ fetchImpl = compatibleFetch,
115
+ env = process.env,
116
+ now = Date.now,
117
+ flags = {},
118
+ timeoutMs = CONTENT_TAG_FETCH_TIMEOUT_MS,
119
+ cacheTtlMs = CONTENT_TAG_CACHE_TTL_MS
120
+ } = {}) {
121
+ if (env.SKILLHUB_UPLOAD_SKIP_REMOTE_TAGS === '1') {
122
+ return FALLBACK_CONTENT_TAGS.map((tag) => ({ ...tag }));
123
+ }
124
+ const cachePath = getContentTagCachePath(env);
125
+ const nowMs = typeof now === 'function' ? now() : Number(now) || Date.now();
126
+ const cached = await readCachedContentTags(cachePath);
127
+ if (cached && nowMs - cached.fetchedAtMs < cacheTtlMs) {
128
+ return cached.tags;
129
+ }
130
+
131
+ const { signal, cancel } = withTimeout(timeoutMs);
132
+ try {
133
+ const tags = await fetchContentTags({ fetchImpl, flags, signal });
134
+ cancel();
135
+ await writeCachedContentTags(cachePath, tags, nowMs);
136
+ return tags;
137
+ } catch (error) {
138
+ cancel();
139
+ process.stderr.write(`[redskillhub-upload] warn: load content tags failed: ${error?.message || error}\n`);
140
+ if (cached?.tags?.length) {
141
+ return cached.tags;
142
+ }
143
+ return FALLBACK_CONTENT_TAGS.map((tag) => ({ ...tag }));
144
+ }
145
+ }
146
+
147
+ export function getContentTagsHomeDir(env = process.env) {
148
+ return getHomeDir(env);
149
+ }
package/cli/upload.mjs ADDED
@@ -0,0 +1,214 @@
1
+ import { resolveApiBase, PATHS } from './config.mjs';
2
+ import { ExitCodes, SkillhubUploadError } from './errors.mjs';
3
+ import { compatibleFetch } from './fetch.mjs';
4
+ import { redactMessage } from './redact.mjs';
5
+ import { writeUploadProgress } from './output.mjs';
6
+ const SLICE_SIZE = 1024 * 1024;
7
+
8
+ function firstPresent(...values) {
9
+ return values.find((value) => value !== undefined && value !== null && value !== '');
10
+ }
11
+
12
+ function resolvePermitFields(permit = {}) {
13
+ return {
14
+ fileId: firstPresent(permit.fileId, permit.fileIds?.[0], permit.file_id, permit.file_ids?.[0]),
15
+ bucket: firstPresent(permit.bucket, permit.Bucket),
16
+ region: firstPresent(permit.region, permit.Region),
17
+ uploadAddr: firstPresent(permit.uploadAddr, permit.upload_addr, permit.Domain),
18
+ secretId: firstPresent(permit.secretId, permit.tmpSecretId, permit.tmp_secret_id, permit.TmpSecretId, permit.secret?.secretId),
19
+ secretKey: firstPresent(permit.secretKey, permit.tmpSecretKey, permit.tmp_secret_key, permit.TmpSecretKey, permit.secret?.secretKey),
20
+ token: firstPresent(permit.token, permit.securityToken, permit.security_token, permit.SecurityToken, permit.secret?.token),
21
+ expireTime: firstPresent(permit.expireTime, permit.expire_time, permit.expiredTime, permit.expired_time, permit.ExpiredTime)
22
+ };
23
+ }
24
+
25
+ function collectSensitiveValues(...items) {
26
+ return items
27
+ .flatMap((item) => {
28
+ if (!item) return [];
29
+ if (typeof item === 'string') return [item];
30
+ const fields = resolvePermitFields(item);
31
+ return [
32
+ item.accessToken,
33
+ fields.secretId,
34
+ fields.secretKey,
35
+ fields.token
36
+ ];
37
+ })
38
+ .filter((value) => typeof value === 'string' && value.length > 0);
39
+ }
40
+
41
+ function buildPermitError(message) {
42
+ return new SkillhubUploadError('PERMIT_FAILED', message, ExitCodes.PERMIT_FAILED);
43
+ }
44
+
45
+ function isRosSelfBuiltChannel(fields) {
46
+ return fields.bucket === 'unknown';
47
+ }
48
+
49
+ function validatePermit(permit) {
50
+ const fields = resolvePermitFields(permit);
51
+ const ros = isRosSelfBuiltChannel(fields);
52
+ const requiredFields = ros
53
+ ? ['fileId', 'bucket', 'region', 'uploadAddr', 'token']
54
+ : ['fileId', 'bucket', 'region', 'uploadAddr', 'secretId', 'secretKey', 'token'];
55
+ const missing = requiredFields.find((key) => !fields[key]);
56
+ if (missing) {
57
+ throw buildPermitError(`上传授权字段不完整 (${missing})`);
58
+ }
59
+
60
+ return fields;
61
+ }
62
+
63
+ function unwrapResponseBody(body) {
64
+ return body?.data && typeof body.data === 'object' ? body.data : body;
65
+ }
66
+
67
+ function resolvePermits(token = {}) {
68
+ const data = unwrapResponseBody(token);
69
+ return data.uploadTempPermits || data.upload_temp_permits || data.permits || [];
70
+ }
71
+
72
+ async function defaultUploadFile(cos, params) {
73
+ await new Promise((resolve, reject) => {
74
+ cos.uploadFile(params, (error) => {
75
+ if (error) reject(error);
76
+ else resolve();
77
+ });
78
+ });
79
+ }
80
+
81
+ async function createDefaultCos(options) {
82
+ const { default: COS } = await import('cos-nodejs-sdk-v5');
83
+ return new COS(options);
84
+ }
85
+
86
+ function buildCosUploadError(error, sensitiveValues) {
87
+ return new SkillhubUploadError(
88
+ 'COS_UPLOAD_FAILED',
89
+ `COS 上传失败: ${redactMessage(error?.message || String(error), sensitiveValues)}`,
90
+ ExitCodes.COS_UPLOAD_FAILED
91
+ );
92
+ }
93
+
94
+ export async function requestUploadToken({ apiBase, accessToken, fetchImpl = compatibleFetch }) {
95
+ const url = new URL(PATHS.UPLOAD_TOKEN, apiBase);
96
+ url.searchParams.set('biz_name', 'spectrum');
97
+ url.searchParams.set('scene', 'skill');
98
+ url.searchParams.set('file_count', '1');
99
+ url.searchParams.set('file_format', 'zip');
100
+ url.searchParams.set('version', '1');
101
+ url.searchParams.set('source', 'web');
102
+
103
+ try {
104
+ const response = await fetchImpl(url, {
105
+ headers: { authorization: `Bearer ${accessToken}` }
106
+ });
107
+
108
+ if (!response.ok) {
109
+ throw buildPermitError(`获取上传授权失败: HTTP ${response.status}`);
110
+ }
111
+
112
+ return unwrapResponseBody(await response.json());
113
+ } catch (error) {
114
+ if (error instanceof SkillhubUploadError) {
115
+ throw error;
116
+ }
117
+ throw buildPermitError(`获取上传授权失败: ${redactMessage(error?.message || String(error), [accessToken])}`);
118
+ }
119
+ }
120
+
121
+ const ROS_SELF_BUILT_APP_ID = '1251524319';
122
+
123
+ export async function uploadWithPermit(permit, zipPath, options = {}) {
124
+ const fields = validatePermit(permit);
125
+ const ros = isRosSelfBuiltChannel(fields);
126
+ const uploadFileImpl = options.uploadFileImpl;
127
+ const cosFactory = options.cosFactory || createDefaultCos;
128
+
129
+ const cosInit = {
130
+ Domain: fields.uploadAddr,
131
+ getAuthorization: (_, callback) => callback({
132
+ TmpSecretId: fields.secretId || 'null',
133
+ TmpSecretKey: fields.secretKey || 'null',
134
+ SecurityToken: fields.token,
135
+ StartTime: Math.floor(Date.now() / 1000),
136
+ ExpiredTime: fields.expireTime ? Math.floor(Number(fields.expireTime) / 1000) : undefined
137
+ })
138
+ };
139
+ if (ros) {
140
+ cosInit.AppId = ROS_SELF_BUILT_APP_ID;
141
+ }
142
+ const cos = uploadFileImpl ? null : await cosFactory(cosInit);
143
+
144
+ const params = {
145
+ Bucket: fields.bucket,
146
+ Region: fields.region,
147
+ Key: fields.fileId,
148
+ FilePath: zipPath,
149
+ SliceSize: SLICE_SIZE,
150
+ onProgress: (event = {}) => {
151
+ const percent = Math.floor(Number(event.percent || 0) * 100);
152
+ writeUploadProgress(percent, options.progressStream || process.stdout);
153
+ }
154
+ };
155
+
156
+ try {
157
+ if (uploadFileImpl) {
158
+ await uploadFileImpl(params, { permit, fields });
159
+ } else {
160
+ await defaultUploadFile(cos, params);
161
+ }
162
+ } catch (error) {
163
+ throw buildCosUploadError(error, collectSensitiveValues(permit));
164
+ }
165
+
166
+ return fields.fileId;
167
+ }
168
+
169
+ export async function uploadBundle(bundle, options = {}) {
170
+ if (options.dryRun) {
171
+ return {
172
+ bundleFileId: 'dry-run-bundle-file-id',
173
+ bundleSha256: bundle.bundleSha256,
174
+ bundleSizeBytes: bundle.bundleSizeBytes,
175
+ uploaded: false
176
+ };
177
+ }
178
+
179
+ const apiBase = options.apiBase || resolveApiBase(options.flags || {});
180
+ const token = await requestUploadToken({
181
+ apiBase,
182
+ accessToken: options.accessToken,
183
+ fetchImpl: options.fetchImpl || compatibleFetch
184
+ });
185
+ const permits = [...resolvePermits(token)].sort((a, b) => Number(b.qos || 0) - Number(a.qos || 0));
186
+
187
+ if (permits.length === 0) {
188
+ throw new SkillhubUploadError('COS_UPLOAD_FAILED', '没有可用上传授权', ExitCodes.COS_UPLOAD_FAILED);
189
+ }
190
+
191
+ let lastError;
192
+ for (const permit of permits) {
193
+ try {
194
+ const fileId = await uploadWithPermit(permit, bundle.zipPath, {
195
+ cosFactory: options.cosFactory,
196
+ uploadFileImpl: options.uploadFileImpl,
197
+ progressStream: options.progressStream
198
+ });
199
+ return {
200
+ bundleFileId: fileId,
201
+ bundleSha256: bundle.bundleSha256,
202
+ bundleSizeBytes: bundle.bundleSizeBytes,
203
+ uploaded: true
204
+ };
205
+ } catch (error) {
206
+ lastError = error;
207
+ }
208
+ }
209
+
210
+ throw buildCosUploadError(lastError, [
211
+ options.accessToken,
212
+ ...collectSensitiveValues(...permits)
213
+ ]);
214
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "redskillhub-upload",
3
+ "version": "1.0.0-alpha.1",
4
+ "type": "module",
5
+ "publishConfig": {
6
+ "access": "public",
7
+ "registry": "https://registry.npmjs.org/"
8
+ },
9
+ "bin": {
10
+ "redskillhub-upload": "cli/index.mjs"
11
+ },
12
+ "files": [
13
+ "cli/",
14
+ "skill/",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test test/*.test.mjs",
19
+ "dry-run": "node cli/index.mjs publish test/fixtures/minimal-skill --dry-run --agent --yes"
20
+ },
21
+ "dependencies": {
22
+ "adm-zip": "^0.5.16",
23
+ "cos-nodejs-sdk-v5": "^2.14.7",
24
+ "qrcode": "^1.5.4"
25
+ },
26
+ "engines": {
27
+ "node": ">=18"
28
+ }
29
+ }