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 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
@@ -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.1.1",
4
- "description": "CLI for Magic Builder — publish pages, functions, files, and performance drafts",
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
  },
@@ -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
- 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
+ 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
- if (args.length) fail('Usage: magic-builder performance --cookie <content|file> --markdown <content|file|url>', 'E_INVALID_ARGS');
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
- const payload = buildPayload(markdown, config);
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 response = await fetch(config.endpoint, {
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
- success(data === null ? text : data, opts);
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 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');
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
- 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`),
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').trim());
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').trim());
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(/^\s*[-*+]\s+(.+)$/);
230
- const ordered = line.match(/^\s*\d+[.)]\s+(.+)$/);
403
+ const unordered = line.match(/^(\s*)[-*+]\s+(.+)$/);
404
+ const ordered = line.match(/^(\s*)(\d+)[.)]\s+(.+)$/);
231
405
  const quote = line.match(/^\s*>\s?(.*)$/);
232
- const content = heading?.[2] ?? unordered?.[1] ?? ordered?.[1] ?? quote?.[1] ?? line;
233
- appendInline(ops, content);
234
- const attributes = heading ? { header: heading[1].length }
235
- : unordered ? { list: 'bullet' }
236
- : ordered ? { list: 'ordered' }
237
- : quote ? { blockquote: true }
238
- : undefined;
239
- ops.push(attributes ? { insert: '\n', attributes } : { insert: '\n' });
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 };
@@ -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(/^version:\s*["']?([^"'\n]+)["']?\s*$/m);
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 = { DEFAULT_BASE_URL, CONFIG_DIR, normalizeMagicBaseUrl, getBaseUrl, getAppsConfigPath, loadAppsConfig, saveAppsConfig };
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 Submit Markdown content to a People performance draft (alias: perf)
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> --markdown-file <file> [--dry-run]
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
- extract-cookie --curl <content|file> [--out <file>|--out-dir <dir>]
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> --markdown-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