magic-builder 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/bin/magic-builder.js +1 -0
- package/package.json +5 -2
- package/src/commands/config.js +2 -1
- package/src/commands/extract-cookie.js +33 -2
- package/src/commands/performance.js +130 -19
- package/src/commands/skill.js +1 -1
- package/src/commands/widget-publish.js +339 -0
- package/src/index.js +1 -0
- package/src/lib/config.js +15 -1
- package/src/lib/help.js +33 -2
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ This includes:
|
|
|
72
72
|
```text
|
|
73
73
|
magic-token
|
|
74
74
|
magic-apps.json
|
|
75
|
+
widget-publish.json
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
## Common Commands
|
|
@@ -145,6 +146,15 @@ magic-builder performance --review-url <people-review-url> --markdown ./performa
|
|
|
145
146
|
|
|
146
147
|
With `--review-url`, the CLI loads the operator and tenant from `/perf/api/user/settings`, then reuses the current draft version, template, units, fields, source ids, and root review id from `/perf/api/foundation/draft`. Business ids are not hard-coded into the generated request.
|
|
147
148
|
|
|
149
|
+
Formally submit the latest draft to the current review stage:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
magic-builder perf submit --review-url <people-review-url> --template-group-id <id> --dry-run
|
|
153
|
+
magic-builder perf submit --review-url <people-review-url> --template-group-id <id> --yes
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The command posts to `/perf/api/review/v2/stage`. It always reloads the latest draft, derives the root review, template, stage, and work units from it, and serializes rich-text values into the stage payload. `--template-group-id` is optional when the draft already contains it. A real submission requires `--yes`; use `--dry-run` to inspect the payload first.
|
|
157
|
+
|
|
148
158
|
Extract Cookie from a copied curl command. This is a top-level general-purpose command. The default output is `./cookie.txt`; the file is created with mode `0600`:
|
|
149
159
|
|
|
150
160
|
```bash
|
|
@@ -160,6 +170,43 @@ magic-builder doc create --html app.html --title "Demo"
|
|
|
160
170
|
magic-builder doc append --html app.html --doc-token <docx-token>
|
|
161
171
|
```
|
|
162
172
|
|
|
173
|
+
Publish a document widget draft as a new app version:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
magic-builder widget-publish \
|
|
177
|
+
--app-id cli_a98afe875979500d \
|
|
178
|
+
--block-type-id blk_6900429af84180025ce76527 \
|
|
179
|
+
--change-log "修复 HTML Box 高度反馈循环"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The command reads Open Platform browser authentication from
|
|
183
|
+
`~/.magic-builder/widget-publish.json` by default:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"cookie": "open_locale=zh-CN; session=...",
|
|
188
|
+
"csrfToken": "...",
|
|
189
|
+
"timezoneOffset": -480
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Keep this file local and set its mode to `0600`. Use `--config <file>` to select
|
|
194
|
+
another file. The command fetches the widget detail, selects the latest uploaded
|
|
195
|
+
test package before the existing draft/current package, updates the widget pkgId,
|
|
196
|
+
increments the latest patch version, creates the app
|
|
197
|
+
version with the application's actual default abilities and audit settings, and
|
|
198
|
+
submits it for publishing. Use `--version` to override the generated
|
|
199
|
+
version or `--dry-run` to inspect the payload without updating or publishing.
|
|
200
|
+
|
|
201
|
+
You can generate the local config directly from a browser-copied curl command:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
magic-builder extract-cookie --curl-file ./widget-request.curl --widget-publish
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
This extracts only the Cookie and `x-csrf-token`, writes them to the default
|
|
208
|
+
widget publish config with mode `0600`, and does not print either secret.
|
|
209
|
+
|
|
163
210
|
Install or update the Magic Builder Codex skill:
|
|
164
211
|
|
|
165
212
|
```bash
|
package/bin/magic-builder.js
CHANGED
|
@@ -14,6 +14,7 @@ const COMMANDS = {
|
|
|
14
14
|
feedback: require('../src/commands/feedback'),
|
|
15
15
|
performance: require('../src/commands/performance'),
|
|
16
16
|
perf: require('../src/commands/performance'),
|
|
17
|
+
'widget-publish': require('../src/commands/widget-publish'),
|
|
17
18
|
'extract-cookie': require('../src/commands/extract-cookie'),
|
|
18
19
|
skill: require('../src/commands/skill'),
|
|
19
20
|
version: require('../src/commands/version'),
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magic-builder",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for Magic Builder — publish pages, functions, files, and performance
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "CLI for Magic Builder — publish pages, functions, files, and performance reviews",
|
|
5
5
|
"bin": {
|
|
6
6
|
"magic-builder": "bin/magic-builder.js",
|
|
7
7
|
"magic-cli": "bin/magic-builder.js",
|
|
8
8
|
"miaobi": "bin/magic-builder.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./src/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/*.test.js"
|
|
13
|
+
},
|
|
11
14
|
"engines": {
|
|
12
15
|
"node": ">=18.0.0"
|
|
13
16
|
},
|
package/src/commands/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl } = require('../lib/config');
|
|
3
|
+
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl, getWidgetPublishConfigPath } = require('../lib/config');
|
|
4
4
|
const { TOKEN_FILE, showToken } = require('../lib/auth');
|
|
5
5
|
const { success, fail } = require('../lib/output');
|
|
6
6
|
|
|
@@ -10,6 +10,7 @@ async function run(args, opts) {
|
|
|
10
10
|
success({
|
|
11
11
|
base_url: getBaseUrl(opts),
|
|
12
12
|
config_dir: CONFIG_DIR,
|
|
13
|
+
widget_publish_config_file: getWidgetPublishConfigPath(),
|
|
13
14
|
token_file: TOKEN_FILE,
|
|
14
15
|
apps_config_file: getAppsConfigPath(),
|
|
15
16
|
token: showToken() || '',
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
3
|
+
const { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
4
4
|
const { basename, dirname, resolve } = require('path');
|
|
5
|
+
const { getWidgetPublishConfigPath } = require('../lib/config');
|
|
5
6
|
const { success, fail } = require('../lib/output');
|
|
6
7
|
|
|
7
8
|
function run(args, opts) {
|
|
@@ -11,9 +12,24 @@ function run(args, opts) {
|
|
|
11
12
|
const cookie = extractCookieFromCurl(curl);
|
|
12
13
|
if (!cookie) fail('No Cookie found in curl. Expected -b/--cookie or a Cookie request header.', 'E_INVALID_ARGS');
|
|
13
14
|
|
|
15
|
+
if (opts.widgetPublish) {
|
|
16
|
+
const csrfToken = extractHeaderFromCurl(curl, 'x-csrf-token');
|
|
17
|
+
if (!csrfToken) fail('No x-csrf-token header found in curl.', 'E_INVALID_ARGS');
|
|
18
|
+
const output = resolve(String(opts.out || getWidgetPublishConfigPath()));
|
|
19
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
20
|
+
writeFileSync(output, `${JSON.stringify({ cookie, csrfToken, timezoneOffset: -480 }, null, 2)}\n`, {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
mode: 0o600,
|
|
23
|
+
});
|
|
24
|
+
chmodSync(output, 0o600);
|
|
25
|
+
success({ path: output, filename: basename(output), widgetPublish: true }, opts);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
const output = resolveOutput(opts.out, opts.outDir);
|
|
15
30
|
mkdirSync(dirname(output), { recursive: true });
|
|
16
31
|
writeFileSync(output, `${cookie}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
32
|
+
chmodSync(output, 0o600);
|
|
17
33
|
success({ path: output, filename: basename(output), cookieCount: cookie.split(';').filter(Boolean).length }, opts);
|
|
18
34
|
}
|
|
19
35
|
|
|
@@ -43,6 +59,21 @@ function extractCookieFromCurl(curl) {
|
|
|
43
59
|
return '';
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
function extractHeaderFromCurl(curl, headerName) {
|
|
63
|
+
const expected = String(headerName || '').toLowerCase();
|
|
64
|
+
const tokens = tokenizeShell(String(curl || '').replace(/\\\r?\n/g, ' '));
|
|
65
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
66
|
+
let header = '';
|
|
67
|
+
if (tokens[i] === '-H' || tokens[i] === '--header') header = tokens[i + 1] || '';
|
|
68
|
+
else if (tokens[i].startsWith('--header=')) header = tokens[i].slice('--header='.length);
|
|
69
|
+
if (!header) continue;
|
|
70
|
+
const separator = header.indexOf(':');
|
|
71
|
+
if (separator < 0 || header.slice(0, separator).trim().toLowerCase() !== expected) continue;
|
|
72
|
+
return header.slice(separator + 1).trim();
|
|
73
|
+
}
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
function normalizeCookie(raw) {
|
|
47
78
|
return String(raw || '').replace(/^cookie\s*:\s*/i, '').trim();
|
|
48
79
|
}
|
|
@@ -82,4 +113,4 @@ function resolveOutput(out, outDir) {
|
|
|
82
113
|
return resolve(String(outDir || process.cwd()), 'cookie.txt');
|
|
83
114
|
}
|
|
84
115
|
|
|
85
|
-
module.exports = { run, extractCookieFromCurl, resolveOutput };
|
|
116
|
+
module.exports = { run, extractCookieFromCurl, extractHeaderFromCurl, resolveOutput };
|
|
@@ -6,23 +6,41 @@ const { success, fail } = require('../lib/output');
|
|
|
6
6
|
|
|
7
7
|
const DEFAULTS = {
|
|
8
8
|
endpoint: 'https://people.bytedance.net/perf/api/foundation/v2/draft',
|
|
9
|
+
submitEndpoint: 'https://people.bytedance.net/perf/api/review/v2/stage',
|
|
9
10
|
settingsEndpoint: 'https://people.bytedance.net/perf/api/user/settings',
|
|
10
11
|
draftEndpoint: 'https://people.bytedance.net/perf/api/foundation/draft',
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
async function run(args, opts) {
|
|
14
|
-
|
|
15
|
+
const action = args[0] || 'draft';
|
|
16
|
+
if (args.length > 1 || !['draft', 'submit'].includes(action)) {
|
|
17
|
+
fail('Usage: magic-builder performance [submit] --review-url <url> --cookie <content|file>', 'E_INVALID_ARGS');
|
|
18
|
+
}
|
|
15
19
|
|
|
16
20
|
const cookie = readCookie(opts.cookie, opts.cookieFile);
|
|
17
21
|
if (!cookie) fail('Missing Cookie. Use --cookie <content|file> or --cookie-file <file>.', 'E_INVALID_ARGS');
|
|
18
22
|
|
|
19
|
-
const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
|
|
20
|
-
if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
|
|
21
|
-
|
|
22
23
|
const csrf = getCookie(cookie, 'x-f-csrf');
|
|
23
24
|
if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
|
|
24
25
|
|
|
25
26
|
const config = await resolveConfig(opts, cookie);
|
|
27
|
+
if (action === 'submit') {
|
|
28
|
+
const payload = buildStagePayloadFromDraft(config.draft, config);
|
|
29
|
+
if (opts.dryRun) {
|
|
30
|
+
success({ dryRun: true, action: 'submit', endpoint: config.submitEndpoint, payload }, opts);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!opts.yes) {
|
|
34
|
+
fail('Formal performance submission requires --yes. Run with --dry-run first to review the payload.', 'E_CONFIRM_REQUIRED');
|
|
35
|
+
}
|
|
36
|
+
const data = await postPerformance(config.submitEndpoint, payload, cookie, csrf, config);
|
|
37
|
+
success(data, opts);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
|
|
42
|
+
if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
|
|
43
|
+
|
|
26
44
|
const payload = config.draft
|
|
27
45
|
? buildPayloadFromDraft(markdown, config.draft, config)
|
|
28
46
|
: buildPayload(markdown, config);
|
|
@@ -31,7 +49,13 @@ async function run(args, opts) {
|
|
|
31
49
|
return;
|
|
32
50
|
}
|
|
33
51
|
|
|
34
|
-
const
|
|
52
|
+
const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
|
|
53
|
+
success(data, opts);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function postPerformance(endpoint, payload, cookie, csrf, config) {
|
|
57
|
+
assertPeoplePerfEndpoint(endpoint);
|
|
58
|
+
const response = await fetch(endpoint, {
|
|
35
59
|
method: 'POST',
|
|
36
60
|
headers: {
|
|
37
61
|
accept: 'application/json, text/plain, */*',
|
|
@@ -55,7 +79,7 @@ async function run(args, opts) {
|
|
|
55
79
|
error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
|
|
56
80
|
throw error;
|
|
57
81
|
}
|
|
58
|
-
|
|
82
|
+
return data === null ? text : data;
|
|
59
83
|
}
|
|
60
84
|
|
|
61
85
|
function readCookie(value, file) {
|
|
@@ -119,6 +143,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
119
143
|
if (!draftResponse?.success || !draftResponse?.data) fail('Unable to load the current performance draft.', 'E_REQUEST_FAILED');
|
|
120
144
|
return {
|
|
121
145
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
146
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
122
147
|
reviewId: review.reviewId,
|
|
123
148
|
formId: review.formId,
|
|
124
149
|
operatorId,
|
|
@@ -126,6 +151,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
126
151
|
referer: review.referer,
|
|
127
152
|
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
128
153
|
version: opts.draftVersion === undefined ? undefined : Number(opts.draftVersion),
|
|
154
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
129
155
|
draft: draftResponse.data,
|
|
130
156
|
};
|
|
131
157
|
}
|
|
@@ -137,6 +163,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
137
163
|
}
|
|
138
164
|
return {
|
|
139
165
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
166
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
140
167
|
reviewId: String(opts.reviewId),
|
|
141
168
|
formId: String(opts.formId),
|
|
142
169
|
templateId: String(opts.templateId),
|
|
@@ -147,6 +174,7 @@ async function resolveConfig(opts, cookie) {
|
|
|
147
174
|
sourceId: String(opts.sourceId),
|
|
148
175
|
fieldSourceId: String(opts.fieldSourceId),
|
|
149
176
|
rootReviewId: String(opts.rootReviewId),
|
|
177
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
150
178
|
version: Number(opts.draftVersion || opts.version),
|
|
151
179
|
tenantId: String(opts.tenantId || ''),
|
|
152
180
|
referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${opts.reviewId}/${opts.formId}?mode=editable`),
|
|
@@ -172,6 +200,7 @@ function parseReviewLocation(value, reviewId, formId) {
|
|
|
172
200
|
}
|
|
173
201
|
|
|
174
202
|
async function requestJson(url, cookie, referer) {
|
|
203
|
+
assertPeoplePerfEndpoint(url);
|
|
175
204
|
const response = await fetch(url, {
|
|
176
205
|
headers: { accept: 'application/json, text/plain, */*', cookie, referer },
|
|
177
206
|
});
|
|
@@ -181,6 +210,16 @@ async function requestJson(url, cookie, referer) {
|
|
|
181
210
|
return data;
|
|
182
211
|
}
|
|
183
212
|
|
|
213
|
+
function assertPeoplePerfEndpoint(value) {
|
|
214
|
+
const url = new URL(String(value || ''));
|
|
215
|
+
if (url.origin !== 'https://people.bytedance.net' || !url.pathname.startsWith('/perf/api/')) {
|
|
216
|
+
const error = new Error('Performance API endpoint must use https://people.bytedance.net/perf/api/.');
|
|
217
|
+
error.code = 'E_INVALID_ARGS';
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
return url;
|
|
221
|
+
}
|
|
222
|
+
|
|
184
223
|
function buildPayload(markdown, config) {
|
|
185
224
|
const now = Date.now();
|
|
186
225
|
const sections = splitMarkdownSections(markdown);
|
|
@@ -256,6 +295,56 @@ function buildPayloadFromDraft(markdown, draft, config) {
|
|
|
256
295
|
};
|
|
257
296
|
}
|
|
258
297
|
|
|
298
|
+
function buildStagePayloadFromDraft(draft, config = {}) {
|
|
299
|
+
if (!draft?.data?.keyWorks) fail('Current draft does not contain keyWorks.', 'E_REQUEST_FAILED');
|
|
300
|
+
const keyWorks = structuredClone(draft.data.keyWorks);
|
|
301
|
+
const stageId = String(config.stageId || config.formId || '');
|
|
302
|
+
const rootReviewId = String(config.rootReviewId || draft.data.rootReviewId || draft.data.root_review_id || '');
|
|
303
|
+
const templateId = String(config.templateId || keyWorks.template_id || keyWorks.templateId || '');
|
|
304
|
+
const templateGroupId = String(
|
|
305
|
+
config.templateGroupId
|
|
306
|
+
|| draft.data.templateGroupId
|
|
307
|
+
|| draft.data.template_group_id
|
|
308
|
+
|| keyWorks.templateGroupId
|
|
309
|
+
|| keyWorks.template_group_id
|
|
310
|
+
|| ''
|
|
311
|
+
);
|
|
312
|
+
const missing = [
|
|
313
|
+
['root_review_id', rootReviewId],
|
|
314
|
+
['template_id', templateId],
|
|
315
|
+
['template_group_id', templateGroupId],
|
|
316
|
+
['stage_id', stageId],
|
|
317
|
+
].filter(([, value]) => !value).map(([name]) => name);
|
|
318
|
+
if (missing.length) {
|
|
319
|
+
fail(`Unable to build formal submission payload; missing ${missing.join(', ')}. Pass --template-group-id when it is absent from the draft.`, 'E_INVALID_ARGS');
|
|
320
|
+
}
|
|
321
|
+
if (!Array.isArray(keyWorks.units) || !keyWorks.units.length) {
|
|
322
|
+
fail('Current draft does not contain keyWorks.units.', 'E_REQUEST_FAILED');
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
root_review_id: rootReviewId,
|
|
326
|
+
template_id: templateId,
|
|
327
|
+
template_group_id: templateGroupId,
|
|
328
|
+
stage_id: stageId,
|
|
329
|
+
stage_ids: [stageId],
|
|
330
|
+
data: { units: keyWorks.units.filter(unit => unit?.type === 'object').map(serializeStageUnit) },
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function serializeStageUnit(unit) {
|
|
335
|
+
const output = structuredClone(unit);
|
|
336
|
+
for (const field of output.fields || []) {
|
|
337
|
+
delete field.json_value;
|
|
338
|
+
for (const group of field.sub_units || []) {
|
|
339
|
+
for (const subUnit of group || []) {
|
|
340
|
+
if (subUnit?.value && typeof subUnit.value !== 'string') subUnit.value = JSON.stringify(subUnit.value);
|
|
341
|
+
delete subUnit.json_value;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return output;
|
|
346
|
+
}
|
|
347
|
+
|
|
259
348
|
function createSubUnits(markdown, textFieldId, sourceId, now = Date.now()) {
|
|
260
349
|
return splitMarkdownSections(markdown).map((section, index) => {
|
|
261
350
|
const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
|
|
@@ -273,17 +362,28 @@ function splitMarkdownSections(markdown) {
|
|
|
273
362
|
for (const line of lines) {
|
|
274
363
|
if (/^##\s+/.test(line)) {
|
|
275
364
|
if (current.some(item => item.trim()) && !current.every(item => /^#\s+/.test(item) || !item.trim())) {
|
|
276
|
-
sections.push(current.join('\n')
|
|
365
|
+
sections.push(cleanMarkdownSection(current.join('\n')));
|
|
277
366
|
}
|
|
278
367
|
current = [`# ${line.replace(/^##\s+/, '')}`];
|
|
279
368
|
} else if (current.length || !/^#\s+/.test(line)) {
|
|
280
369
|
current.push(line);
|
|
281
370
|
}
|
|
282
371
|
}
|
|
283
|
-
if (current.some(item => item.trim())) sections.push(current.join('\n')
|
|
372
|
+
if (current.some(item => item.trim())) sections.push(cleanMarkdownSection(current.join('\n')));
|
|
284
373
|
return sections.length ? sections : [text];
|
|
285
374
|
}
|
|
286
375
|
|
|
376
|
+
function cleanMarkdownSection(value) {
|
|
377
|
+
const cleaned = String(value || '').trim().replace(/(?:\n\s*(?:---+|\*\*\*+|___+)\s*)+$/u, '').trim();
|
|
378
|
+
return cleaned.split('\n').map((line, index) => {
|
|
379
|
+
if (index === 0) return line.replace(/^#{1,6}\s+/, '# ');
|
|
380
|
+
if (!line.trim() || /^\s*(?:[-*+]\s+|\d+[.)]\s+)/u.test(line)) return line;
|
|
381
|
+
if (/^\s*[-*+]\S/u.test(line)) return line.replace(/^(\s*[-*+])\s*/u, '$1 ');
|
|
382
|
+
if (/^\s*\d+[.)]\S/u.test(line)) return line.replace(/^(\s*\d+[.)])\s*/u, '$1 ');
|
|
383
|
+
return `- ${line.trim()}`;
|
|
384
|
+
}).join('\n');
|
|
385
|
+
}
|
|
386
|
+
|
|
287
387
|
function markdownToDelta(markdown) {
|
|
288
388
|
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
289
389
|
const ops = [];
|
|
@@ -298,18 +398,29 @@ function markdownToDelta(markdown) {
|
|
|
298
398
|
ops.push({ insert: '\n', attributes: { 'code-block': true } });
|
|
299
399
|
continue;
|
|
300
400
|
}
|
|
401
|
+
if (!line.trim()) continue;
|
|
301
402
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
302
|
-
const unordered = line.match(
|
|
303
|
-
const ordered = line.match(
|
|
403
|
+
const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
404
|
+
const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
|
|
304
405
|
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
406
|
+
if (heading) {
|
|
407
|
+
ops.push({ insert: `${heading[1]} ${heading[2]}`, attributes: { bold: true } });
|
|
408
|
+
ops.push({ insert: '\n' });
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (unordered || ordered) {
|
|
412
|
+
const indent = (unordered?.[1] || ordered?.[1] || '').replace(/\t/g, ' ').length;
|
|
413
|
+
const level = Math.max(1, Math.floor(indent / 2) + 1);
|
|
414
|
+
const list = `${unordered ? 'bullet' : 'ordered'}${level}`;
|
|
415
|
+
const marker = unordered ? '*' : `${ordered[2]}.`;
|
|
416
|
+
const content = unordered?.[2] ?? ordered?.[3] ?? '';
|
|
417
|
+
ops.push({ insert: marker, attributes: { list, lmkr: '1' } });
|
|
418
|
+
appendInline(ops, content);
|
|
419
|
+
ops.push({ insert: '\n' });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
appendInline(ops, quote?.[1] ?? line);
|
|
423
|
+
ops.push(quote ? { insert: '\n', attributes: { blockquote: true } } : { insert: '\n' });
|
|
313
424
|
}
|
|
314
425
|
if (ops.length > 1 && lines.at(-1) === '') ops.pop();
|
|
315
426
|
return ops.length ? ops : [{ insert: '\n' }];
|
|
@@ -334,4 +445,4 @@ function parseJson(text) {
|
|
|
334
445
|
try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
|
|
335
446
|
}
|
|
336
447
|
|
|
337
|
-
module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft };
|
|
448
|
+
module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft, buildStagePayloadFromDraft, assertPeoplePerfEndpoint };
|
package/src/commands/skill.js
CHANGED
|
@@ -142,7 +142,7 @@ function readSkillMetadata(skillDir) {
|
|
|
142
142
|
if (!fs.existsSync(skillPath)) return { exists: false, name: null, version: null, path: skillPath };
|
|
143
143
|
const text = fs.readFileSync(skillPath, 'utf8');
|
|
144
144
|
const nameMatch = text.match(/^name:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
145
|
-
const versionMatch = text.match(
|
|
145
|
+
const versionMatch = text.match(/^\s*version:\s*["']?([^"'\n]+)["']?\s*$/m);
|
|
146
146
|
return {
|
|
147
147
|
exists: true,
|
|
148
148
|
name: nameMatch ? nameMatch[1].trim() : null,
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { existsSync, readFileSync } = require('fs');
|
|
4
|
+
const { resolve } = require('path');
|
|
5
|
+
const { getWidgetPublishConfigPath } = require('../lib/config');
|
|
6
|
+
const { success, fail } = require('../lib/output');
|
|
7
|
+
|
|
8
|
+
const DEFAULT_ORIGIN = 'https://open.larkoffice.com';
|
|
9
|
+
|
|
10
|
+
async function run(args, opts) {
|
|
11
|
+
if (args.length) {
|
|
12
|
+
fail('Usage: magic-builder widget-publish --app-id <id> --block-type-id <id>', 'E_INVALID_ARGS');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const appId = String(opts.appId || '').trim();
|
|
16
|
+
const blockTypeId = String(opts.blockTypeId || '').trim();
|
|
17
|
+
if (!appId || !blockTypeId) {
|
|
18
|
+
fail('Missing --app-id or --block-type-id.', 'E_INVALID_ARGS');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const auth = loadPublishAuth(opts.config);
|
|
22
|
+
const client = createOpenPlatformClient(auth, opts.origin || DEFAULT_ORIGIN);
|
|
23
|
+
const detail = await client.post(`/developers/v2/block/detail/${appId}/${blockTypeId}`, {});
|
|
24
|
+
const block = extractBlock(detail);
|
|
25
|
+
const pkgId = extractDraftPkgId(detail, block);
|
|
26
|
+
if (!pkgId) throw commandError('Draft widget pkgId was not found in block detail.', 'E_INVALID_RESPONSE');
|
|
27
|
+
|
|
28
|
+
const updatePayload = { block: buildBlockUpdatePayload(block, pkgId, blockTypeId) };
|
|
29
|
+
if (!opts.dryRun) {
|
|
30
|
+
await client.post(`/developers/v2/block/detail/update/${appId}/${blockTypeId}`, updatePayload);
|
|
31
|
+
}
|
|
32
|
+
const versionChangeResponse = await client.post(`/developers/v1/app_version/change/${appId}`, {});
|
|
33
|
+
const versionChange = unwrapData(versionChangeResponse) || {};
|
|
34
|
+
const versionsResponse = await client.post(`/developers/v1/app_version/list/${appId}`, {});
|
|
35
|
+
const versions = extractVersionList(versionsResponse);
|
|
36
|
+
const appVersion = opts.version || getNextPatchVersion(versions);
|
|
37
|
+
const createPayload = buildCreateVersionPayload(
|
|
38
|
+
appVersion,
|
|
39
|
+
opts.changeLog,
|
|
40
|
+
getLatestVersion(versions),
|
|
41
|
+
versionChange
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (opts.dryRun) {
|
|
45
|
+
success({
|
|
46
|
+
dryRun: true,
|
|
47
|
+
appId,
|
|
48
|
+
blockTypeId,
|
|
49
|
+
pkgId,
|
|
50
|
+
appVersion,
|
|
51
|
+
updatePayload,
|
|
52
|
+
createPayload,
|
|
53
|
+
}, opts);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const createResponse = await client.post(`/developers/v1/app_version/create/${appId}`, createPayload);
|
|
58
|
+
const appVersionId = extractAppVersionId(createResponse);
|
|
59
|
+
if (!appVersionId) throw commandError('Created app version id was not found in response.', 'E_INVALID_RESPONSE');
|
|
60
|
+
|
|
61
|
+
const publishResponse = await client.post(`/developers/v1/publish/commit/${appId}/${appVersionId}`, {});
|
|
62
|
+
success({
|
|
63
|
+
appId,
|
|
64
|
+
blockTypeId,
|
|
65
|
+
pkgId,
|
|
66
|
+
appVersion,
|
|
67
|
+
appVersionId,
|
|
68
|
+
published: true,
|
|
69
|
+
response: publishResponse,
|
|
70
|
+
}, opts);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function loadPublishAuth(configFile) {
|
|
74
|
+
const path = resolve(String(configFile || getWidgetPublishConfigPath()));
|
|
75
|
+
if (!existsSync(path)) {
|
|
76
|
+
throw commandError(`Widget publish config not found: ${path}`, 'E_NO_CONFIG');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let config;
|
|
80
|
+
try {
|
|
81
|
+
config = JSON.parse(readFileSync(path, 'utf8'));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
throw commandError(`Invalid widget publish config: ${error.message}`, 'E_INVALID_CONFIG');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const cookie = String(config.cookie || '').trim();
|
|
87
|
+
const csrfToken = String(config.csrfToken || config.csrf_token || '').trim();
|
|
88
|
+
if (!cookie || !csrfToken) {
|
|
89
|
+
throw commandError('Widget publish config must contain cookie and csrfToken.', 'E_INVALID_CONFIG');
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
cookie,
|
|
93
|
+
csrfToken,
|
|
94
|
+
timezoneOffset: Number.isFinite(Number(config.timezoneOffset)) ? Number(config.timezoneOffset) : -480,
|
|
95
|
+
locale: String(config.locale || 'zh-CN'),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function createOpenPlatformClient(auth, origin = DEFAULT_ORIGIN, fetchImpl = fetch) {
|
|
100
|
+
const baseUrl = String(origin || DEFAULT_ORIGIN).replace(/\/+$/, '');
|
|
101
|
+
return {
|
|
102
|
+
async post(path, body) {
|
|
103
|
+
const response = await fetchImpl(`${baseUrl}${path}`, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers: {
|
|
106
|
+
accept: '*/*',
|
|
107
|
+
'accept-language': `${auth.locale},zh;q=0.9`,
|
|
108
|
+
'cache-control': 'no-cache',
|
|
109
|
+
'content-type': 'application/json',
|
|
110
|
+
cookie: auth.cookie,
|
|
111
|
+
origin: baseUrl,
|
|
112
|
+
pragma: 'no-cache',
|
|
113
|
+
referer: getRequestReferer(baseUrl, path),
|
|
114
|
+
'x-csrf-token': auth.csrfToken,
|
|
115
|
+
'x-timezone-offset': String(auth.timezoneOffset),
|
|
116
|
+
},
|
|
117
|
+
body: JSON.stringify(body || {}),
|
|
118
|
+
});
|
|
119
|
+
const text = await response.text();
|
|
120
|
+
const data = parseJson(text);
|
|
121
|
+
if (!response.ok || isApiFailure(data)) {
|
|
122
|
+
const reason = data?.msg || data?.message || `HTTP ${response.status}`;
|
|
123
|
+
throw commandError(
|
|
124
|
+
`Open Platform request failed at ${path}: ${reason}`,
|
|
125
|
+
response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return data ?? text;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function extractBlock(response) {
|
|
134
|
+
const data = unwrapData(response);
|
|
135
|
+
return data?.block || data?.detail?.block || data?.blockDetail?.block || data?.detail || data;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractDraftPkgId(response, block = {}) {
|
|
139
|
+
const candidates = [
|
|
140
|
+
response?.data?.blockVersions?.testVersion?.id,
|
|
141
|
+
response?.data?.blockVersions?.testVersion?.pkgId,
|
|
142
|
+
response?.data?.blockVersions?.draftVersion?.id,
|
|
143
|
+
response?.data?.blockVersions?.draftVersion?.pkgId,
|
|
144
|
+
block?.draftVersion?.pkgId,
|
|
145
|
+
block?.draftPkg?.pkgId,
|
|
146
|
+
block?.draftPackage?.pkgId,
|
|
147
|
+
block?.draft?.pkgId,
|
|
148
|
+
findDraftPkgId(response),
|
|
149
|
+
findValue(response, ['draftPkgId', 'draftPackageId']),
|
|
150
|
+
block?.pkgId,
|
|
151
|
+
];
|
|
152
|
+
return String(candidates.find(Boolean) || '').trim();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function buildBlockUpdatePayload(block = {}, pkgId, blockTypeId) {
|
|
156
|
+
return {
|
|
157
|
+
pkgId: String(pkgId),
|
|
158
|
+
icon: block.icon || '',
|
|
159
|
+
i18nTitle: block.i18nTitle || {},
|
|
160
|
+
i18nDesc: block.i18nDesc || {},
|
|
161
|
+
status: block.status !== false,
|
|
162
|
+
blockTypeId: block.blockTypeId || block.blockTypeID || blockTypeId,
|
|
163
|
+
pkgUpdateType: Number.isFinite(Number(block.pkgUpdateType)) ? Number(block.pkgUpdateType) : 0,
|
|
164
|
+
docXHost: block.docXHost || block.docxHost || {},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function extractVersionList(response) {
|
|
169
|
+
const data = unwrapData(response);
|
|
170
|
+
if (Array.isArray(data)) return data;
|
|
171
|
+
for (const key of ['items', 'list', 'appVersions', 'versions']) {
|
|
172
|
+
if (Array.isArray(data?.[key])) return data[key];
|
|
173
|
+
}
|
|
174
|
+
return findArray(data, ['items', 'list', 'appVersions', 'versions']);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getNextPatchVersion(versions) {
|
|
178
|
+
const parsed = versions
|
|
179
|
+
.map(item => String(item?.appVersion || item?.version || item?.versionName || item || ''))
|
|
180
|
+
.map(parseSemver)
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.sort(compareSemver);
|
|
183
|
+
const latest = parsed.at(-1) || [1, 0, -1];
|
|
184
|
+
return `${latest[0]}.${latest[1]}.${latest[2] + 1}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getLatestVersion(versions) {
|
|
188
|
+
return versions
|
|
189
|
+
.map(item => ({ item, version: parseSemver(item?.appVersion || item?.version || item?.versionName || item) }))
|
|
190
|
+
.filter(entry => entry.version)
|
|
191
|
+
.sort((a, b) => compareSemver(a.version, b.version))
|
|
192
|
+
.at(-1)?.item || {};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function buildCreateVersionPayload(
|
|
196
|
+
appVersion,
|
|
197
|
+
changeLog = '更新文档小组件版本',
|
|
198
|
+
latestVersion = {},
|
|
199
|
+
versionChange = {}
|
|
200
|
+
) {
|
|
201
|
+
const shareConfig = versionChange.changeAppShareConfig?.b2cShareSplitConfigSuggest;
|
|
202
|
+
return {
|
|
203
|
+
appVersion: String(appVersion),
|
|
204
|
+
mobileDefaultAbility: versionChange.mobileDefaultAbility || latestVersion.mobileDefaultAbility || 'bot',
|
|
205
|
+
pcDefaultAbility: versionChange.pcDefaultAbility || latestVersion.pcDefaultAbility || 'bot',
|
|
206
|
+
changeLog: String(changeLog || '更新文档小组件版本'),
|
|
207
|
+
visibleSuggest: latestVersion.visibleSuggest || { departments: [], members: [], groups: [], isAll: 1 },
|
|
208
|
+
applyReasonConfig: versionChange.applyReasonConfig || latestVersion.applyReasonConfig || {
|
|
209
|
+
apiPrivilegeNeedReason: true,
|
|
210
|
+
contactPrivilegeNeedReason: true,
|
|
211
|
+
dataPrivilegeReasonMap: {},
|
|
212
|
+
visibleScopeNeedReason: true,
|
|
213
|
+
apiPrivilegeReasonMap: {},
|
|
214
|
+
contactPrivilegeReason: '',
|
|
215
|
+
isDataPrivilegeExpandMap: { vc: false },
|
|
216
|
+
visibleScopeReason: '',
|
|
217
|
+
dataPrivilegeNeedReason: true,
|
|
218
|
+
isAutoAudit: false,
|
|
219
|
+
isContactExpand: false,
|
|
220
|
+
},
|
|
221
|
+
b2cShareSplitConfigSuggest: shareConfig || latestVersion.b2cShareSplitConfigSuggest || {
|
|
222
|
+
b2cGroupChatShareEnable: false,
|
|
223
|
+
b2cP2PChatShareEnable: false,
|
|
224
|
+
b2cP2PChatNeedAudit: false,
|
|
225
|
+
},
|
|
226
|
+
autoPublish: false,
|
|
227
|
+
blackVisibleSuggest: latestVersion.blackVisibleSuggest || { departments: [], members: [], groups: [], isAll: 0 },
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function extractAppVersionId(response) {
|
|
232
|
+
const data = unwrapData(response);
|
|
233
|
+
return String(
|
|
234
|
+
data?.appVersionId
|
|
235
|
+
|| data?.versionId
|
|
236
|
+
|| data?.appVersion?.id
|
|
237
|
+
|| data?.version?.id
|
|
238
|
+
|| data?.id
|
|
239
|
+
|| findValue(data, ['appVersionId', 'versionId'])
|
|
240
|
+
|| ''
|
|
241
|
+
).trim();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function unwrapData(value) {
|
|
245
|
+
let current = value;
|
|
246
|
+
for (let i = 0; i < 3; i++) {
|
|
247
|
+
if (!current || typeof current !== 'object' || Array.isArray(current) || !('data' in current)) break;
|
|
248
|
+
current = current.data;
|
|
249
|
+
}
|
|
250
|
+
return current;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function findValue(value, names, predicate = () => true, path = '') {
|
|
254
|
+
if (!value || typeof value !== 'object') return '';
|
|
255
|
+
for (const [name, child] of Object.entries(value)) {
|
|
256
|
+
const childPath = path ? `${path}.${name}` : name;
|
|
257
|
+
if (names.includes(name) && child != null && predicate({ name, path: childPath })) return child;
|
|
258
|
+
const nested = findValue(child, names, predicate, childPath);
|
|
259
|
+
if (nested) return nested;
|
|
260
|
+
}
|
|
261
|
+
return '';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function findDraftPkgId(value, path = '') {
|
|
265
|
+
if (!value || typeof value !== 'object') return '';
|
|
266
|
+
const marker = [
|
|
267
|
+
path,
|
|
268
|
+
value.status,
|
|
269
|
+
value.versionStatus,
|
|
270
|
+
value.pkgStatus,
|
|
271
|
+
value.type,
|
|
272
|
+
value.name,
|
|
273
|
+
].filter(Boolean).join(' ');
|
|
274
|
+
if (/draft|草稿/i.test(marker) && value.pkgId) return value.pkgId;
|
|
275
|
+
for (const [name, child] of Object.entries(value)) {
|
|
276
|
+
const found = findDraftPkgId(child, path ? `${path}.${name}` : name);
|
|
277
|
+
if (found) return found;
|
|
278
|
+
}
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function findArray(value, names) {
|
|
283
|
+
if (!value || typeof value !== 'object') return [];
|
|
284
|
+
for (const [name, child] of Object.entries(value)) {
|
|
285
|
+
if (names.includes(name) && Array.isArray(child)) return child;
|
|
286
|
+
const found = findArray(child, names);
|
|
287
|
+
if (found.length) return found;
|
|
288
|
+
}
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function getRequestReferer(baseUrl, path) {
|
|
293
|
+
const blockMatch = path.match(/\/block\/detail(?:\/update)?\/([^/]+)\/([^/]+)/);
|
|
294
|
+
if (blockMatch) return `${baseUrl}/app/${blockMatch[1]}/blocks/${blockMatch[2]}`;
|
|
295
|
+
const publishMatch = path.match(/\/publish\/commit\/([^/]+)\/([^/]+)/);
|
|
296
|
+
if (publishMatch) return `${baseUrl}/app/${publishMatch[1]}/version/${publishMatch[2]}`;
|
|
297
|
+
const appMatch = path.match(/\/app_version\/(?:list|create)\/([^/]+)/);
|
|
298
|
+
if (appMatch) return `${baseUrl}/app/${appMatch[1]}/version/create`;
|
|
299
|
+
return `${baseUrl}/`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseSemver(value) {
|
|
303
|
+
const match = String(value || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
304
|
+
return match ? match.slice(1).map(Number) : null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function compareSemver(a, b) {
|
|
308
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseJson(text) {
|
|
312
|
+
try { return JSON.parse(text); } catch (_) { return null; }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isApiFailure(data) {
|
|
316
|
+
if (!data || typeof data !== 'object') return false;
|
|
317
|
+
if ('code' in data && ![0, '0', 200, '200'].includes(data.code)) return true;
|
|
318
|
+
return data.success === false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function commandError(message, code) {
|
|
322
|
+
const error = new Error(message);
|
|
323
|
+
error.code = code;
|
|
324
|
+
return error;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
module.exports = {
|
|
328
|
+
run,
|
|
329
|
+
loadPublishAuth,
|
|
330
|
+
createOpenPlatformClient,
|
|
331
|
+
extractBlock,
|
|
332
|
+
extractDraftPkgId,
|
|
333
|
+
buildBlockUpdatePayload,
|
|
334
|
+
extractVersionList,
|
|
335
|
+
getNextPatchVersion,
|
|
336
|
+
getLatestVersion,
|
|
337
|
+
buildCreateVersionPayload,
|
|
338
|
+
extractAppVersionId,
|
|
339
|
+
};
|
package/src/index.js
CHANGED
|
@@ -15,6 +15,7 @@ module.exports = {
|
|
|
15
15
|
feedback: require('./commands/feedback'),
|
|
16
16
|
performance: require('./commands/performance'),
|
|
17
17
|
perf: require('./commands/performance'),
|
|
18
|
+
widgetPublish: require('./commands/widget-publish'),
|
|
18
19
|
extractCookie: require('./commands/extract-cookie'),
|
|
19
20
|
skill: require('./commands/skill'),
|
|
20
21
|
version: require('./commands/version'),
|
package/src/lib/config.js
CHANGED
|
@@ -8,6 +8,7 @@ const DEFAULT_BASE_URL = 'https://magic.solutionsuite.cn';
|
|
|
8
8
|
const CONFIG_DIR = path.join(os.homedir(), '.magic-builder');
|
|
9
9
|
const APPS_CONFIG_FILE = 'magic-apps.json';
|
|
10
10
|
const LEGACY_APPS_CONFIG_FILE = '.magic-apps.json';
|
|
11
|
+
const WIDGET_PUBLISH_CONFIG_FILE = 'widget-publish.json';
|
|
11
12
|
|
|
12
13
|
function normalizeMagicBaseUrl(value) {
|
|
13
14
|
const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
|
|
@@ -23,6 +24,10 @@ function getAppsConfigPath() {
|
|
|
23
24
|
return path.join(CONFIG_DIR, APPS_CONFIG_FILE);
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function getWidgetPublishConfigPath() {
|
|
28
|
+
return path.join(CONFIG_DIR, WIDGET_PUBLISH_CONFIG_FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
function loadAppsConfig() {
|
|
27
32
|
const p = fs.existsSync(getAppsConfigPath())
|
|
28
33
|
? getAppsConfigPath()
|
|
@@ -38,4 +43,13 @@ function saveAppsConfig(config) {
|
|
|
38
43
|
fs.writeFileSync(getAppsConfigPath(), JSON.stringify(config, null, 2));
|
|
39
44
|
}
|
|
40
45
|
|
|
41
|
-
module.exports = {
|
|
46
|
+
module.exports = {
|
|
47
|
+
DEFAULT_BASE_URL,
|
|
48
|
+
CONFIG_DIR,
|
|
49
|
+
normalizeMagicBaseUrl,
|
|
50
|
+
getBaseUrl,
|
|
51
|
+
getAppsConfigPath,
|
|
52
|
+
getWidgetPublishConfigPath,
|
|
53
|
+
loadAppsConfig,
|
|
54
|
+
saveAppsConfig,
|
|
55
|
+
};
|
package/src/lib/help.js
CHANGED
|
@@ -21,7 +21,8 @@ COMMANDS:
|
|
|
21
21
|
link Generate Magic share links
|
|
22
22
|
doc Create or append Feishu Doc HTML Box apps
|
|
23
23
|
feedback Create Magic feedback records
|
|
24
|
-
performance
|
|
24
|
+
performance Write or formally submit a People performance review (alias: perf)
|
|
25
|
+
widget-publish Publish a document widget draft as a new app version
|
|
25
26
|
extract-cookie Extract Cookie data from a curl command
|
|
26
27
|
skill Check or update the Magic Builder skill package
|
|
27
28
|
version Show CLI version
|
|
@@ -48,6 +49,7 @@ EXAMPLES:
|
|
|
48
49
|
magic-builder doc create --html app.html --title "Demo"
|
|
49
50
|
magic-builder feedback create --feedback "打不开页面"
|
|
50
51
|
magic-builder performance --cookie ./cookie.txt --markdown ./performance.md
|
|
52
|
+
magic-builder widget-publish --app-id cli_xxx --block-type-id blk_xxx
|
|
51
53
|
magic-builder extract-cookie --curl-file ./request.curl
|
|
52
54
|
magic-builder skill install
|
|
53
55
|
magic-builder skill check-update
|
|
@@ -108,9 +110,13 @@ COMMANDS:
|
|
|
108
110
|
feedback create --feedback-file <file> [--dry-run]
|
|
109
111
|
|
|
110
112
|
performance --review-url <url> --cookie <content|file> --markdown <content|file|url> [--dry-run]
|
|
113
|
+
perf submit --review-url <url> --cookie-file <file> --template-group-id <id> [--dry-run|--yes]
|
|
111
114
|
perf --review-url <url> --cookie-file <file> --markdown-file <file> [--dry-run]
|
|
112
115
|
|
|
113
|
-
|
|
116
|
+
widget-publish --app-id <id> --block-type-id <id> [--version <x.y.z>] [--change-log <text>]
|
|
117
|
+
[--config <file>] [--dry-run]
|
|
118
|
+
|
|
119
|
+
extract-cookie --curl <content|file> [--out <file>|--out-dir <dir>] [--widget-publish]
|
|
114
120
|
|
|
115
121
|
skill install [--environment auto|local|cloud] [--skills-root <dir>]
|
|
116
122
|
skill check-update [--environment auto|local|cloud] [--skills-root <dir>]
|
|
@@ -198,6 +204,8 @@ SYNTAX:
|
|
|
198
204
|
|
|
199
205
|
SYNTAX:
|
|
200
206
|
magic-builder performance --review-url <url> --cookie <content|file> --markdown <content|file|url>
|
|
207
|
+
magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --dry-run
|
|
208
|
+
magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --yes
|
|
201
209
|
magic-builder perf --review-url <url> --cookie-file <file> --markdown-file <file>
|
|
202
210
|
|
|
203
211
|
COOKIE:
|
|
@@ -210,6 +218,9 @@ DRAFT:
|
|
|
210
218
|
OPTIONS:
|
|
211
219
|
--dry-run Print the generated payload without sending it
|
|
212
220
|
--review-url <url> People performance review URL (recommended)
|
|
221
|
+
--template-group-id <id> Template group id (only needed when absent from the draft)
|
|
222
|
+
--submit-endpoint <url> Override the formal submission endpoint
|
|
223
|
+
--yes Confirm the irreversible formal submission
|
|
213
224
|
--draft-version <number> Override the version loaded from the current draft
|
|
214
225
|
--review-id <id> Override the performance review id
|
|
215
226
|
--form-id <id> Override the invitation/form id used by the draft key
|
|
@@ -224,12 +235,31 @@ OPTIONS:
|
|
|
224
235
|
perf: `@HELP magic-builder/performance
|
|
225
236
|
|
|
226
237
|
Alias of "magic-builder performance".
|
|
238
|
+
`,
|
|
239
|
+
'widget-publish': `@HELP magic-builder/widget-publish
|
|
240
|
+
|
|
241
|
+
SYNTAX:
|
|
242
|
+
magic-builder widget-publish --app-id <id> --block-type-id <id>
|
|
243
|
+
[--version <x.y.z>] [--change-log <text>] [--config <file>] [--dry-run]
|
|
244
|
+
|
|
245
|
+
AUTH CONFIG:
|
|
246
|
+
Defaults to ~/.magic-builder/widget-publish.json:
|
|
247
|
+
{
|
|
248
|
+
"cookie": "open_locale=zh-CN; session=...",
|
|
249
|
+
"csrfToken": "...",
|
|
250
|
+
"timezoneOffset": -480
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
FLOW:
|
|
254
|
+
Load widget detail and latest uploaded pkgId, update the widget package, load the app's
|
|
255
|
+
version defaults, calculate the next version, create it, then submit publishing.
|
|
227
256
|
`,
|
|
228
257
|
'extract-cookie': `@HELP magic-builder/extract-cookie
|
|
229
258
|
|
|
230
259
|
SYNTAX:
|
|
231
260
|
magic-builder extract-cookie --curl <content|file> [--out <file>]
|
|
232
261
|
magic-builder extract-cookie --curl-file <file> [--out-dir <dir>]
|
|
262
|
+
magic-builder extract-cookie --curl-file <file> --widget-publish
|
|
233
263
|
cat request.curl | magic-builder extract-cookie [--out <file>]
|
|
234
264
|
|
|
235
265
|
OPTIONS:
|
|
@@ -237,6 +267,7 @@ OPTIONS:
|
|
|
237
267
|
--curl-file <file> Read curl command from a local file
|
|
238
268
|
--out <file> Output file (default: ./cookie.txt)
|
|
239
269
|
--out-dir <dir> Output directory; created when missing
|
|
270
|
+
--widget-publish Write Cookie and x-csrf-token to the widget publish config
|
|
240
271
|
`,
|
|
241
272
|
skill: `@HELP magic-builder/skill
|
|
242
273
|
|