magic-builder 1.1.1 → 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 +53 -4
- 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 +234 -50
- 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 +43 -6
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
|
|
@@ -129,9 +130,9 @@ magic-builder link create --fid <faas-id>
|
|
|
129
130
|
Submit Markdown to a People performance draft (`perf` is an alias):
|
|
130
131
|
|
|
131
132
|
```bash
|
|
132
|
-
magic-builder performance --cookie ./cookie.txt --markdown ./performance.md
|
|
133
|
-
magic-builder perf --cookie "$PEOPLE_COOKIE" --markdown "# Performance summary"
|
|
134
|
-
magic-builder perf --cookie ./cookie.txt --markdown https://example.com/performance.md --dry-run
|
|
133
|
+
magic-builder performance --review-url <people-review-url> --cookie ./cookie.txt --markdown ./performance.md
|
|
134
|
+
magic-builder perf --review-url <people-review-url> --cookie "$PEOPLE_COOKIE" --markdown "# Performance summary"
|
|
135
|
+
magic-builder perf --review-url <people-review-url> --cookie ./cookie.txt --markdown https://example.com/performance.md --dry-run
|
|
135
136
|
```
|
|
136
137
|
|
|
137
138
|
Cookie input accepts a raw `Cookie` header, a local path, or a Netscape cookie file. Markdown input accepts literal content, a local path, an HTTP(S) URL, or stdin. The command extracts `x-f-csrf` from the Cookie and does not print the Cookie in its output.
|
|
@@ -140,9 +141,20 @@ When `--cookie` and `--cookie-file` are omitted, `performance` automatically rea
|
|
|
140
141
|
|
|
141
142
|
```bash
|
|
142
143
|
magic-builder extract-cookie --curl-file ./request.curl
|
|
143
|
-
magic-builder performance --markdown ./performance.md
|
|
144
|
+
magic-builder performance --review-url <people-review-url> --markdown ./performance.md
|
|
144
145
|
```
|
|
145
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.
|
|
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
|
+
|
|
146
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`:
|
|
147
159
|
|
|
148
160
|
```bash
|
|
@@ -158,6 +170,43 @@ magic-builder doc create --html app.html --title "Demo"
|
|
|
158
170
|
magic-builder doc append --html app.html --doc-token <docx-token>
|
|
159
171
|
```
|
|
160
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
|
+
|
|
161
210
|
Install or update the Magic Builder Codex skill:
|
|
162
211
|
|
|
163
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,39 +6,56 @@ 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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
unitId: '7654466592117358775',
|
|
13
|
-
fieldId: '7654466592117358799',
|
|
14
|
-
textFieldId: '7654466592117358824',
|
|
15
|
-
operatorId: '6687792427639817740',
|
|
16
|
-
sourceId: '7784085065561419062',
|
|
17
|
-
fieldSourceId: '7662587009851985528',
|
|
18
|
-
rootReviewId: '7657808756893306935',
|
|
19
|
-
version: 5,
|
|
9
|
+
submitEndpoint: 'https://people.bytedance.net/perf/api/review/v2/stage',
|
|
10
|
+
settingsEndpoint: 'https://people.bytedance.net/perf/api/user/settings',
|
|
11
|
+
draftEndpoint: 'https://people.bytedance.net/perf/api/foundation/draft',
|
|
20
12
|
};
|
|
21
13
|
|
|
22
14
|
async function run(args, opts) {
|
|
23
|
-
|
|
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
|
+
}
|
|
24
19
|
|
|
25
20
|
const cookie = readCookie(opts.cookie, opts.cookieFile);
|
|
26
21
|
if (!cookie) fail('Missing Cookie. Use --cookie <content|file> or --cookie-file <file>.', 'E_INVALID_ARGS');
|
|
27
22
|
|
|
28
|
-
const markdown = await readMarkdown(opts.markdown, opts.markdownFile);
|
|
29
|
-
if (!markdown.trim()) fail('Missing Markdown. Use --markdown <content|file|url>, --markdown-file <file>, or stdin.', 'E_INVALID_ARGS');
|
|
30
|
-
|
|
31
23
|
const csrf = getCookie(cookie, 'x-f-csrf');
|
|
32
24
|
if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
|
|
33
25
|
|
|
34
|
-
const config = resolveConfig(opts);
|
|
35
|
-
|
|
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
|
+
|
|
44
|
+
const payload = config.draft
|
|
45
|
+
? buildPayloadFromDraft(markdown, config.draft, config)
|
|
46
|
+
: buildPayload(markdown, config);
|
|
36
47
|
if (opts.dryRun) {
|
|
37
48
|
success({ dryRun: true, endpoint: config.endpoint, payload }, opts);
|
|
38
49
|
return;
|
|
39
50
|
}
|
|
40
51
|
|
|
41
|
-
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, {
|
|
42
59
|
method: 'POST',
|
|
43
60
|
headers: {
|
|
44
61
|
accept: 'application/json, text/plain, */*',
|
|
@@ -62,7 +79,7 @@ async function run(args, opts) {
|
|
|
62
79
|
error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
|
|
63
80
|
throw error;
|
|
64
81
|
}
|
|
65
|
-
|
|
82
|
+
return data === null ? text : data;
|
|
66
83
|
}
|
|
67
84
|
|
|
68
85
|
function readCookie(value, file) {
|
|
@@ -112,30 +129,97 @@ function getCookie(cookie, name) {
|
|
|
112
129
|
return '';
|
|
113
130
|
}
|
|
114
131
|
|
|
115
|
-
function resolveConfig(opts) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
132
|
+
async function resolveConfig(opts, cookie) {
|
|
133
|
+
const review = parseReviewLocation(opts.reviewUrl || opts.referer, opts.reviewId, opts.formId);
|
|
134
|
+
if (review.reviewId && review.formId) {
|
|
135
|
+
const settings = await requestJson(opts.settingsEndpoint || DEFAULTS.settingsEndpoint, cookie, review.referer);
|
|
136
|
+
const operatorId = String(opts.operatorId || settings?.data?.user?.id || '');
|
|
137
|
+
const tenantId = String(opts.tenantId || settings?.data?.tenant?.id || '');
|
|
138
|
+
if (!operatorId) fail('Unable to resolve operator id from /perf/api/user/settings.', 'E_REQUEST_FAILED');
|
|
139
|
+
const key = `${review.reviewId}__confirm_invitation__${operatorId}__${review.formId}`;
|
|
140
|
+
const draftUrl = new URL(opts.draftEndpoint || DEFAULTS.draftEndpoint);
|
|
141
|
+
draftUrl.searchParams.set('key', key);
|
|
142
|
+
const draftResponse = await requestJson(draftUrl.toString(), cookie, review.referer);
|
|
143
|
+
if (!draftResponse?.success || !draftResponse?.data) fail('Unable to load the current performance draft.', 'E_REQUEST_FAILED');
|
|
144
|
+
return {
|
|
145
|
+
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
146
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
147
|
+
reviewId: review.reviewId,
|
|
148
|
+
formId: review.formId,
|
|
149
|
+
operatorId,
|
|
150
|
+
tenantId,
|
|
151
|
+
referer: review.referer,
|
|
152
|
+
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
153
|
+
version: opts.draftVersion === undefined ? undefined : Number(opts.draftVersion),
|
|
154
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
155
|
+
draft: draftResponse.data,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const required = ['templateId', 'unitId', 'fieldId', 'textFieldId', 'operatorId', 'sourceId', 'fieldSourceId', 'rootReviewId'];
|
|
160
|
+
const missing = required.filter(name => !opts[name]);
|
|
161
|
+
if (!opts.reviewId || !opts.formId || missing.length) {
|
|
162
|
+
fail('Pass --review-url <People review URL> to resolve the current draft automatically.', 'E_INVALID_ARGS');
|
|
163
|
+
}
|
|
120
164
|
return {
|
|
121
165
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
166
|
+
submitEndpoint: String(opts.submitEndpoint || DEFAULTS.submitEndpoint),
|
|
167
|
+
reviewId: String(opts.reviewId),
|
|
168
|
+
formId: String(opts.formId),
|
|
169
|
+
templateId: String(opts.templateId),
|
|
170
|
+
unitId: String(opts.unitId),
|
|
171
|
+
fieldId: String(opts.fieldId),
|
|
172
|
+
textFieldId: String(opts.textFieldId),
|
|
173
|
+
operatorId: String(opts.operatorId),
|
|
174
|
+
sourceId: String(opts.sourceId),
|
|
175
|
+
fieldSourceId: String(opts.fieldSourceId),
|
|
176
|
+
rootReviewId: String(opts.rootReviewId),
|
|
177
|
+
templateGroupId: String(opts.templateGroupId || ''),
|
|
178
|
+
version: Number(opts.draftVersion || opts.version),
|
|
179
|
+
tenantId: String(opts.tenantId || ''),
|
|
180
|
+
referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${opts.reviewId}/${opts.formId}?mode=editable`),
|
|
135
181
|
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
136
182
|
};
|
|
137
183
|
}
|
|
138
184
|
|
|
185
|
+
function parseReviewLocation(value, reviewId, formId) {
|
|
186
|
+
if (reviewId && formId) {
|
|
187
|
+
return {
|
|
188
|
+
reviewId: String(reviewId),
|
|
189
|
+
formId: String(formId),
|
|
190
|
+
referer: `https://people.bytedance.net/performance/perf/review/${reviewId}/${formId}?mode=editable`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const raw = String(value || '').trim();
|
|
194
|
+
if (!raw) return { reviewId: '', formId: '', referer: '' };
|
|
195
|
+
const url = new URL(raw);
|
|
196
|
+
if (url.origin !== 'https://people.bytedance.net') fail('Review URL must use https://people.bytedance.net.', 'E_INVALID_ARGS');
|
|
197
|
+
const match = url.pathname.match(/\/performance\/perf\/review\/(\d+)\/(\d+)/);
|
|
198
|
+
if (!match) fail('Unable to parse reviewId and formId from --review-url.', 'E_INVALID_ARGS');
|
|
199
|
+
return { reviewId: match[1], formId: match[2], referer: url.toString() };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function requestJson(url, cookie, referer) {
|
|
203
|
+
assertPeoplePerfEndpoint(url);
|
|
204
|
+
const response = await fetch(url, {
|
|
205
|
+
headers: { accept: 'application/json, text/plain, */*', cookie, referer },
|
|
206
|
+
});
|
|
207
|
+
const text = await response.text();
|
|
208
|
+
const data = parseJson(text);
|
|
209
|
+
if (!response.ok || data === null) throw new Error(`GET ${new URL(url).pathname} failed: HTTP ${response.status}`);
|
|
210
|
+
return data;
|
|
211
|
+
}
|
|
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
|
+
|
|
139
223
|
function buildPayload(markdown, config) {
|
|
140
224
|
const now = Date.now();
|
|
141
225
|
const sections = splitMarkdownSections(markdown);
|
|
@@ -191,6 +275,84 @@ function buildPayload(markdown, config) {
|
|
|
191
275
|
};
|
|
192
276
|
}
|
|
193
277
|
|
|
278
|
+
function buildPayloadFromDraft(markdown, draft, config) {
|
|
279
|
+
const snapshot = structuredClone(draft);
|
|
280
|
+
const data = snapshot.data;
|
|
281
|
+
const keyWorks = data?.keyWorks;
|
|
282
|
+
if (!keyWorks || !Array.isArray(keyWorks.units)) fail('Current draft does not contain keyWorks.units.', 'E_REQUEST_FAILED');
|
|
283
|
+
const field = keyWorks.units.flatMap(unit => unit.fields || []).find(item => item.entityType === 'multiple_texts');
|
|
284
|
+
if (!field) fail('Current draft does not contain a multiple_texts field.', 'E_REQUEST_FAILED');
|
|
285
|
+
const firstText = field.sub_units?.flat()?.find(item => item?.id);
|
|
286
|
+
if (!firstText?.id) fail('Current draft does not contain a writable text sub-unit.', 'E_REQUEST_FAILED');
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
field.sub_units = createSubUnits(markdown, firstText.id, firstText.source_id || keyWorks.valueSetting?.[0]?.source_id, now);
|
|
289
|
+
field.updated_time = now;
|
|
290
|
+
if (config.uid) data.uid = config.uid;
|
|
291
|
+
return {
|
|
292
|
+
key: snapshot.key,
|
|
293
|
+
data,
|
|
294
|
+
version: config.version ?? Number(snapshot.version),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
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
|
+
|
|
348
|
+
function createSubUnits(markdown, textFieldId, sourceId, now = Date.now()) {
|
|
349
|
+
return splitMarkdownSections(markdown).map((section, index) => {
|
|
350
|
+
const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
|
|
351
|
+
if (index > 0) return [{ key: `${index - 1}-${now + index}-${Math.random()}`, id: textFieldId, entityType: 'text', value }];
|
|
352
|
+
return [{ id: textFieldId, source_id: sourceId, value, json_value: {}, created_time: now, updated_time: now, entityType: 'text' }];
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
194
356
|
function splitMarkdownSections(markdown) {
|
|
195
357
|
const text = String(markdown || '').replace(/\r\n?/g, '\n').trim();
|
|
196
358
|
if (!text) return [''];
|
|
@@ -200,17 +362,28 @@ function splitMarkdownSections(markdown) {
|
|
|
200
362
|
for (const line of lines) {
|
|
201
363
|
if (/^##\s+/.test(line)) {
|
|
202
364
|
if (current.some(item => item.trim()) && !current.every(item => /^#\s+/.test(item) || !item.trim())) {
|
|
203
|
-
sections.push(current.join('\n')
|
|
365
|
+
sections.push(cleanMarkdownSection(current.join('\n')));
|
|
204
366
|
}
|
|
205
367
|
current = [`# ${line.replace(/^##\s+/, '')}`];
|
|
206
368
|
} else if (current.length || !/^#\s+/.test(line)) {
|
|
207
369
|
current.push(line);
|
|
208
370
|
}
|
|
209
371
|
}
|
|
210
|
-
if (current.some(item => item.trim())) sections.push(current.join('\n')
|
|
372
|
+
if (current.some(item => item.trim())) sections.push(cleanMarkdownSection(current.join('\n')));
|
|
211
373
|
return sections.length ? sections : [text];
|
|
212
374
|
}
|
|
213
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
|
+
|
|
214
387
|
function markdownToDelta(markdown) {
|
|
215
388
|
const lines = String(markdown || '').replace(/\r\n?/g, '\n').split('\n');
|
|
216
389
|
const ops = [];
|
|
@@ -225,18 +398,29 @@ function markdownToDelta(markdown) {
|
|
|
225
398
|
ops.push({ insert: '\n', attributes: { 'code-block': true } });
|
|
226
399
|
continue;
|
|
227
400
|
}
|
|
401
|
+
if (!line.trim()) continue;
|
|
228
402
|
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
229
|
-
const unordered = line.match(
|
|
230
|
-
const ordered = line.match(
|
|
403
|
+
const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
|
|
404
|
+
const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
|
|
231
405
|
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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' });
|
|
240
424
|
}
|
|
241
425
|
if (ops.length > 1 && lines.at(-1) === '') ops.pop();
|
|
242
426
|
return ops.length ? ops : [{ insert: '\n' }];
|
|
@@ -261,4 +445,4 @@ function parseJson(text) {
|
|
|
261
445
|
try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
|
|
262
446
|
}
|
|
263
447
|
|
|
264
|
-
module.exports = { run, readCookie, normalizeCookie, getCookie, splitMarkdownSections, markdownToDelta, buildPayload };
|
|
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
|
|
@@ -107,10 +109,14 @@ COMMANDS:
|
|
|
107
109
|
feedback create --feedback <text> [--title <title>] [--summary <text>]
|
|
108
110
|
feedback create --feedback-file <file> [--dry-run]
|
|
109
111
|
|
|
110
|
-
performance --cookie <content|file> --markdown <content|file|url> [--dry-run]
|
|
111
|
-
perf --cookie-file <file> --
|
|
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]
|
|
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>]
|
|
@@ -197,14 +203,25 @@ SYNTAX:
|
|
|
197
203
|
performance: `@HELP magic-builder/performance
|
|
198
204
|
|
|
199
205
|
SYNTAX:
|
|
200
|
-
magic-builder performance --cookie <content|file> --markdown <content|file|url>
|
|
201
|
-
magic-builder perf --cookie-file <file> --
|
|
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
|
|
209
|
+
magic-builder perf --review-url <url> --cookie-file <file> --markdown-file <file>
|
|
202
210
|
|
|
203
211
|
COOKIE:
|
|
204
212
|
When --cookie and --cookie-file are omitted, ./cookie.txt is used.
|
|
205
213
|
|
|
214
|
+
DRAFT:
|
|
215
|
+
--review-url resolves operator, tenant, form ids, and the current version
|
|
216
|
+
from /perf/api/user/settings and /perf/api/foundation/draft.
|
|
217
|
+
|
|
206
218
|
OPTIONS:
|
|
207
219
|
--dry-run Print the generated payload without sending it
|
|
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
|
|
224
|
+
--draft-version <number> Override the version loaded from the current draft
|
|
208
225
|
--review-id <id> Override the performance review id
|
|
209
226
|
--form-id <id> Override the invitation/form id used by the draft key
|
|
210
227
|
--template-id <id> Override the review template id
|
|
@@ -218,12 +235,31 @@ OPTIONS:
|
|
|
218
235
|
perf: `@HELP magic-builder/performance
|
|
219
236
|
|
|
220
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.
|
|
221
256
|
`,
|
|
222
257
|
'extract-cookie': `@HELP magic-builder/extract-cookie
|
|
223
258
|
|
|
224
259
|
SYNTAX:
|
|
225
260
|
magic-builder extract-cookie --curl <content|file> [--out <file>]
|
|
226
261
|
magic-builder extract-cookie --curl-file <file> [--out-dir <dir>]
|
|
262
|
+
magic-builder extract-cookie --curl-file <file> --widget-publish
|
|
227
263
|
cat request.curl | magic-builder extract-cookie [--out <file>]
|
|
228
264
|
|
|
229
265
|
OPTIONS:
|
|
@@ -231,6 +267,7 @@ OPTIONS:
|
|
|
231
267
|
--curl-file <file> Read curl command from a local file
|
|
232
268
|
--out <file> Output file (default: ./cookie.txt)
|
|
233
269
|
--out-dir <dir> Output directory; created when missing
|
|
270
|
+
--widget-publish Write Cookie and x-csrf-token to the widget publish config
|
|
234
271
|
`,
|
|
235
272
|
skill: `@HELP magic-builder/skill
|
|
236
273
|
|