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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-builder",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "CLI for Magic Builder — publish pages, functions, files, and performance drafts",
5
5
  "bin": {
6
6
  "magic-builder": "bin/magic-builder.js",
@@ -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
- reviewId: '7654473253011852917',
10
- formId: '7654466592117358592',
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 = buildPayload(markdown, config);
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 reviewId = String(opts.reviewId || DEFAULTS.reviewId);
117
- const formId = String(opts.formId || DEFAULTS.formId);
118
- const templateId = String(opts.templateId || DEFAULTS.templateId);
119
- const tenantId = String(opts.tenantId || getCookie(String(opts.cookie || ''), 'tenant_id') || '6685321562717324807');
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 || DEFAULTS.unitId),
126
- fieldId: String(opts.fieldId || DEFAULTS.fieldId),
127
- textFieldId: String(opts.textFieldId || DEFAULTS.textFieldId),
128
- operatorId: String(opts.operatorId || DEFAULTS.operatorId),
129
- sourceId: String(opts.sourceId || DEFAULTS.sourceId),
130
- fieldSourceId: String(opts.fieldSourceId || DEFAULTS.fieldSourceId),
131
- rootReviewId: String(opts.rootReviewId || DEFAULTS.rootReviewId),
132
- version: Number(opts.version || DEFAULTS.version),
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