magic-builder 1.1.0 → 1.2.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 +6 -4
- package/package.json +1 -1
- package/src/commands/performance.js +105 -32
- package/src/lib/help.js +10 -4
package/README.md
CHANGED
|
@@ -129,9 +129,9 @@ magic-builder link create --fid <faas-id>
|
|
|
129
129
|
Submit Markdown to a People performance draft (`perf` is an alias):
|
|
130
130
|
|
|
131
131
|
```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
|
|
132
|
+
magic-builder performance --review-url <people-review-url> --cookie ./cookie.txt --markdown ./performance.md
|
|
133
|
+
magic-builder perf --review-url <people-review-url> --cookie "$PEOPLE_COOKIE" --markdown "# Performance summary"
|
|
134
|
+
magic-builder perf --review-url <people-review-url> --cookie ./cookie.txt --markdown https://example.com/performance.md --dry-run
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
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 +140,11 @@ When `--cookie` and `--cookie-file` are omitted, `performance` automatically rea
|
|
|
140
140
|
|
|
141
141
|
```bash
|
|
142
142
|
magic-builder extract-cookie --curl-file ./request.curl
|
|
143
|
-
magic-builder performance --markdown ./performance.md
|
|
143
|
+
magic-builder performance --review-url <people-review-url> --markdown ./performance.md
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
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
|
+
|
|
146
148
|
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
149
|
|
|
148
150
|
```bash
|
package/package.json
CHANGED
|
@@ -6,17 +6,8 @@ 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
|
-
templateId: '7654466592117358609',
|
|
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
|
+
settingsEndpoint: 'https://people.bytedance.net/perf/api/user/settings',
|
|
10
|
+
draftEndpoint: 'https://people.bytedance.net/perf/api/foundation/draft',
|
|
20
11
|
};
|
|
21
12
|
|
|
22
13
|
async function run(args, opts) {
|
|
@@ -31,8 +22,10 @@ async function run(args, opts) {
|
|
|
31
22
|
const csrf = getCookie(cookie, 'x-f-csrf');
|
|
32
23
|
if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
|
|
33
24
|
|
|
34
|
-
const config = resolveConfig(opts);
|
|
35
|
-
const payload =
|
|
25
|
+
const config = await resolveConfig(opts, cookie);
|
|
26
|
+
const payload = config.draft
|
|
27
|
+
? buildPayloadFromDraft(markdown, config.draft, config)
|
|
28
|
+
: buildPayload(markdown, config);
|
|
36
29
|
if (opts.dryRun) {
|
|
37
30
|
success({ dryRun: true, endpoint: config.endpoint, payload }, opts);
|
|
38
31
|
return;
|
|
@@ -112,30 +105,82 @@ function getCookie(cookie, name) {
|
|
|
112
105
|
return '';
|
|
113
106
|
}
|
|
114
107
|
|
|
115
|
-
function resolveConfig(opts) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
108
|
+
async function resolveConfig(opts, cookie) {
|
|
109
|
+
const review = parseReviewLocation(opts.reviewUrl || opts.referer, opts.reviewId, opts.formId);
|
|
110
|
+
if (review.reviewId && review.formId) {
|
|
111
|
+
const settings = await requestJson(opts.settingsEndpoint || DEFAULTS.settingsEndpoint, cookie, review.referer);
|
|
112
|
+
const operatorId = String(opts.operatorId || settings?.data?.user?.id || '');
|
|
113
|
+
const tenantId = String(opts.tenantId || settings?.data?.tenant?.id || '');
|
|
114
|
+
if (!operatorId) fail('Unable to resolve operator id from /perf/api/user/settings.', 'E_REQUEST_FAILED');
|
|
115
|
+
const key = `${review.reviewId}__confirm_invitation__${operatorId}__${review.formId}`;
|
|
116
|
+
const draftUrl = new URL(opts.draftEndpoint || DEFAULTS.draftEndpoint);
|
|
117
|
+
draftUrl.searchParams.set('key', key);
|
|
118
|
+
const draftResponse = await requestJson(draftUrl.toString(), cookie, review.referer);
|
|
119
|
+
if (!draftResponse?.success || !draftResponse?.data) fail('Unable to load the current performance draft.', 'E_REQUEST_FAILED');
|
|
120
|
+
return {
|
|
121
|
+
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
122
|
+
reviewId: review.reviewId,
|
|
123
|
+
formId: review.formId,
|
|
124
|
+
operatorId,
|
|
125
|
+
tenantId,
|
|
126
|
+
referer: review.referer,
|
|
127
|
+
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
128
|
+
version: opts.draftVersion === undefined ? undefined : Number(opts.draftVersion),
|
|
129
|
+
draft: draftResponse.data,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const required = ['templateId', 'unitId', 'fieldId', 'textFieldId', 'operatorId', 'sourceId', 'fieldSourceId', 'rootReviewId'];
|
|
134
|
+
const missing = required.filter(name => !opts[name]);
|
|
135
|
+
if (!opts.reviewId || !opts.formId || missing.length) {
|
|
136
|
+
fail('Pass --review-url <People review URL> to resolve the current draft automatically.', 'E_INVALID_ARGS');
|
|
137
|
+
}
|
|
120
138
|
return {
|
|
121
139
|
endpoint: String(opts.endpoint || DEFAULTS.endpoint),
|
|
122
|
-
reviewId,
|
|
123
|
-
formId,
|
|
124
|
-
templateId,
|
|
125
|
-
unitId: String(opts.unitId
|
|
126
|
-
fieldId: String(opts.fieldId
|
|
127
|
-
textFieldId: String(opts.textFieldId
|
|
128
|
-
operatorId: String(opts.operatorId
|
|
129
|
-
sourceId: String(opts.sourceId
|
|
130
|
-
fieldSourceId: String(opts.fieldSourceId
|
|
131
|
-
rootReviewId: String(opts.rootReviewId
|
|
132
|
-
version: Number(opts.
|
|
133
|
-
tenantId,
|
|
134
|
-
referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${reviewId}/${formId}?mode=editable`),
|
|
140
|
+
reviewId: String(opts.reviewId),
|
|
141
|
+
formId: String(opts.formId),
|
|
142
|
+
templateId: String(opts.templateId),
|
|
143
|
+
unitId: String(opts.unitId),
|
|
144
|
+
fieldId: String(opts.fieldId),
|
|
145
|
+
textFieldId: String(opts.textFieldId),
|
|
146
|
+
operatorId: String(opts.operatorId),
|
|
147
|
+
sourceId: String(opts.sourceId),
|
|
148
|
+
fieldSourceId: String(opts.fieldSourceId),
|
|
149
|
+
rootReviewId: String(opts.rootReviewId),
|
|
150
|
+
version: Number(opts.draftVersion || opts.version),
|
|
151
|
+
tenantId: String(opts.tenantId || ''),
|
|
152
|
+
referer: String(opts.referer || `https://people.bytedance.net/performance/perf/review/${opts.reviewId}/${opts.formId}?mode=editable`),
|
|
135
153
|
uid: String(opts.uid || `magic-builder-${Date.now()}`),
|
|
136
154
|
};
|
|
137
155
|
}
|
|
138
156
|
|
|
157
|
+
function parseReviewLocation(value, reviewId, formId) {
|
|
158
|
+
if (reviewId && formId) {
|
|
159
|
+
return {
|
|
160
|
+
reviewId: String(reviewId),
|
|
161
|
+
formId: String(formId),
|
|
162
|
+
referer: `https://people.bytedance.net/performance/perf/review/${reviewId}/${formId}?mode=editable`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const raw = String(value || '').trim();
|
|
166
|
+
if (!raw) return { reviewId: '', formId: '', referer: '' };
|
|
167
|
+
const url = new URL(raw);
|
|
168
|
+
if (url.origin !== 'https://people.bytedance.net') fail('Review URL must use https://people.bytedance.net.', 'E_INVALID_ARGS');
|
|
169
|
+
const match = url.pathname.match(/\/performance\/perf\/review\/(\d+)\/(\d+)/);
|
|
170
|
+
if (!match) fail('Unable to parse reviewId and formId from --review-url.', 'E_INVALID_ARGS');
|
|
171
|
+
return { reviewId: match[1], formId: match[2], referer: url.toString() };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function requestJson(url, cookie, referer) {
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
headers: { accept: 'application/json, text/plain, */*', cookie, referer },
|
|
177
|
+
});
|
|
178
|
+
const text = await response.text();
|
|
179
|
+
const data = parseJson(text);
|
|
180
|
+
if (!response.ok || data === null) throw new Error(`GET ${new URL(url).pathname} failed: HTTP ${response.status}`);
|
|
181
|
+
return data;
|
|
182
|
+
}
|
|
183
|
+
|
|
139
184
|
function buildPayload(markdown, config) {
|
|
140
185
|
const now = Date.now();
|
|
141
186
|
const sections = splitMarkdownSections(markdown);
|
|
@@ -191,6 +236,34 @@ function buildPayload(markdown, config) {
|
|
|
191
236
|
};
|
|
192
237
|
}
|
|
193
238
|
|
|
239
|
+
function buildPayloadFromDraft(markdown, draft, config) {
|
|
240
|
+
const snapshot = structuredClone(draft);
|
|
241
|
+
const data = snapshot.data;
|
|
242
|
+
const keyWorks = data?.keyWorks;
|
|
243
|
+
if (!keyWorks || !Array.isArray(keyWorks.units)) fail('Current draft does not contain keyWorks.units.', 'E_REQUEST_FAILED');
|
|
244
|
+
const field = keyWorks.units.flatMap(unit => unit.fields || []).find(item => item.entityType === 'multiple_texts');
|
|
245
|
+
if (!field) fail('Current draft does not contain a multiple_texts field.', 'E_REQUEST_FAILED');
|
|
246
|
+
const firstText = field.sub_units?.flat()?.find(item => item?.id);
|
|
247
|
+
if (!firstText?.id) fail('Current draft does not contain a writable text sub-unit.', 'E_REQUEST_FAILED');
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
field.sub_units = createSubUnits(markdown, firstText.id, firstText.source_id || keyWorks.valueSetting?.[0]?.source_id, now);
|
|
250
|
+
field.updated_time = now;
|
|
251
|
+
if (config.uid) data.uid = config.uid;
|
|
252
|
+
return {
|
|
253
|
+
key: snapshot.key,
|
|
254
|
+
data,
|
|
255
|
+
version: config.version ?? Number(snapshot.version),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function createSubUnits(markdown, textFieldId, sourceId, now = Date.now()) {
|
|
260
|
+
return splitMarkdownSections(markdown).map((section, index) => {
|
|
261
|
+
const value = { '0': { ops: markdownToDelta(section), zoneId: '0', zoneType: 'Z' } };
|
|
262
|
+
if (index > 0) return [{ key: `${index - 1}-${now + index}-${Math.random()}`, id: textFieldId, entityType: 'text', value }];
|
|
263
|
+
return [{ id: textFieldId, source_id: sourceId, value, json_value: {}, created_time: now, updated_time: now, entityType: 'text' }];
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
194
267
|
function splitMarkdownSections(markdown) {
|
|
195
268
|
const text = String(markdown || '').replace(/\r\n?/g, '\n').trim();
|
|
196
269
|
if (!text) return [''];
|
|
@@ -261,4 +334,4 @@ function parseJson(text) {
|
|
|
261
334
|
try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
|
|
262
335
|
}
|
|
263
336
|
|
|
264
|
-
module.exports = { run, readCookie, normalizeCookie, getCookie, splitMarkdownSections, markdownToDelta, buildPayload };
|
|
337
|
+
module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft };
|
package/src/lib/help.js
CHANGED
|
@@ -107,8 +107,8 @@ COMMANDS:
|
|
|
107
107
|
feedback create --feedback <text> [--title <title>] [--summary <text>]
|
|
108
108
|
feedback create --feedback-file <file> [--dry-run]
|
|
109
109
|
|
|
110
|
-
performance --cookie <content|file> --markdown <content|file|url> [--dry-run]
|
|
111
|
-
perf --cookie-file <file> --markdown-file <file> [--dry-run]
|
|
110
|
+
performance --review-url <url> --cookie <content|file> --markdown <content|file|url> [--dry-run]
|
|
111
|
+
perf --review-url <url> --cookie-file <file> --markdown-file <file> [--dry-run]
|
|
112
112
|
|
|
113
113
|
extract-cookie --curl <content|file> [--out <file>|--out-dir <dir>]
|
|
114
114
|
|
|
@@ -197,14 +197,20 @@ SYNTAX:
|
|
|
197
197
|
performance: `@HELP magic-builder/performance
|
|
198
198
|
|
|
199
199
|
SYNTAX:
|
|
200
|
-
magic-builder performance --cookie <content|file> --markdown <content|file|url>
|
|
201
|
-
magic-builder perf --cookie-file <file> --markdown-file <file>
|
|
200
|
+
magic-builder performance --review-url <url> --cookie <content|file> --markdown <content|file|url>
|
|
201
|
+
magic-builder perf --review-url <url> --cookie-file <file> --markdown-file <file>
|
|
202
202
|
|
|
203
203
|
COOKIE:
|
|
204
204
|
When --cookie and --cookie-file are omitted, ./cookie.txt is used.
|
|
205
205
|
|
|
206
|
+
DRAFT:
|
|
207
|
+
--review-url resolves operator, tenant, form ids, and the current version
|
|
208
|
+
from /perf/api/user/settings and /perf/api/foundation/draft.
|
|
209
|
+
|
|
206
210
|
OPTIONS:
|
|
207
211
|
--dry-run Print the generated payload without sending it
|
|
212
|
+
--review-url <url> People performance review URL (recommended)
|
|
213
|
+
--draft-version <number> Override the version loaded from the current draft
|
|
208
214
|
--review-id <id> Override the performance review id
|
|
209
215
|
--form-id <id> Override the invitation/form id used by the draft key
|
|
210
216
|
--template-id <id> Override the review template id
|