magic-builder 1.3.0 → 1.3.1

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
@@ -120,6 +120,8 @@ magic-builder file list --title logo
120
120
  magic-builder file delete --id <id>
121
121
  ```
122
122
 
123
+ 文件上传采用 `sign -> PUT -> audit confirm` 流程。预签名成功后会返回 `audit_id`,CLI 在对象上传完成后自动确认审计;确认失败时命令返回失败,重复执行确认不会新增审计记录。
124
+
123
125
  Generate a Magic link:
124
126
 
125
127
  ```bash
@@ -146,6 +148,18 @@ magic-builder performance --review-url <people-review-url> --markdown ./performa
146
148
 
147
149
  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
150
 
151
+ Update the text fields in the self-review draft while preserving both rating fields and any omitted text:
152
+
153
+ ```bash
154
+ magic-builder perf self-review --review-url <people-review-url> \
155
+ --good "What went well" \
156
+ --improve-file ./improvements.md \
157
+ --values-comment-file ./values-comment.md
158
+ magic-builder perf self-review --review-url <people-review-url> --good-file ./good.md --dry-run
159
+ ```
160
+
161
+ Each field can be provided as literal text or through its matching `--*-file` option. At least one field is required. The command reloads the latest `self_review` draft before every run and writes by default; `--dry-run` prints the payload without saving it.
162
+
149
163
  Formally submit the latest draft to the current review stage:
150
164
 
151
165
  ```bash
@@ -166,8 +180,9 @@ cat ./request.curl | magic-builder extract-cookie --out-dir ./secrets
166
180
  Create or append a Feishu document HTML Box:
167
181
 
168
182
  ```bash
169
- magic-builder doc create --html app.html --title "Demo"
183
+ magic-builder doc create --html app.html --title "Demo" # defaults to --edition external
170
184
  magic-builder doc append --html app.html --doc-token <docx-token>
185
+ magic-builder doc create --html app.html --title "ByteDance Demo" --edition bytedance
171
186
  ```
172
187
 
173
188
  Publish a document widget draft as a new app version:
@@ -215,6 +230,10 @@ magic-builder skill check-update
215
230
  magic-builder skill update
216
231
  ```
217
232
 
233
+ Skills are installed to `~/.agents/skills` by default so Codex, Trae, Doubao,
234
+ and other compatible agents can share them. Use `--skills-root <dir>` to
235
+ override the destination.
236
+
218
237
  Check local environment:
219
238
 
220
239
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-builder",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "CLI for Magic Builder — publish pages, functions, files, and performance reviews",
5
5
  "bin": {
6
6
  "magic-builder": "bin/magic-builder.js",
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { CONFIG_DIR, getAppsConfigPath, getBaseUrl, getWidgetPublishConfigPath } = require('../lib/config');
3
+ const { CONFIG_DIR, getAppsConfigPath, getBaseUrl, getWidgetPublishConfigPath, getPerformanceConfigPath } = require('../lib/config');
4
4
  const { TOKEN_FILE, showToken } = require('../lib/auth');
5
5
  const { success, fail } = require('../lib/output');
6
6
 
@@ -11,6 +11,7 @@ async function run(args, opts) {
11
11
  base_url: getBaseUrl(opts),
12
12
  config_dir: CONFIG_DIR,
13
13
  widget_publish_config_file: getWidgetPublishConfigPath(),
14
+ performance_config_file: getPerformanceConfigPath(),
14
15
  token_file: TOKEN_FILE,
15
16
  apps_config_file: getAppsConfigPath(),
16
17
  token: showToken() || '',
@@ -4,7 +4,10 @@ const { readFileSync } = require('fs');
4
4
  const { execFileSync } = require('child_process');
5
5
  const { success, fail } = require('../lib/output');
6
6
 
7
- const HTML_BOX_COMPONENT_TYPE_ID = 'blk_6900429af84180025ce76527';
7
+ const HTML_BOX_COMPONENT_TYPE_IDS = {
8
+ external: 'blk_6358a421bca0001c190a9805',
9
+ bytedance: 'blk_6900429af84180025ce76527',
10
+ };
8
11
 
9
12
  async function run(args, opts) {
10
13
  const sub = args[0];
@@ -16,14 +19,17 @@ async function run(args, opts) {
16
19
  const html = readFileSync(opts.html, 'utf8');
17
20
  const record = buildRecord(opts, html);
18
21
  const identity = resolveIdentity(opts.as);
22
+ const { edition, componentTypeId } = resolveEdition(opts.edition);
19
23
  const created = sub === 'append'
20
24
  ? { docToken: opts.docToken, url: `https://bytedance.larkoffice.com/docx/${opts.docToken}` }
21
25
  : createDocument(opts.title, opts.summary, identity);
22
- const htmlBoxBlockId = insertHtmlBox(created.docToken, record, identity);
26
+ const htmlBoxBlockId = insertHtmlBox(created.docToken, record, identity, componentTypeId);
23
27
 
24
28
  success({
25
29
  ok: true,
26
30
  identity,
31
+ edition,
32
+ component_type_id: componentTypeId,
27
33
  doc_token: created.docToken,
28
34
  doc_url: created.url,
29
35
  html_box_block_id: htmlBoxBlockId,
@@ -51,6 +57,16 @@ function resolveIdentity(requestedIdentity) {
51
57
  return 'bot';
52
58
  }
53
59
 
60
+ function resolveEdition(requestedEdition) {
61
+ const edition = requestedEdition || 'external';
62
+ if (!Object.prototype.hasOwnProperty.call(HTML_BOX_COMPONENT_TYPE_IDS, edition)) {
63
+ const error = new Error('--edition must be external or bytedance');
64
+ error.code = 'E_INVALID_ARGS';
65
+ throw error;
66
+ }
67
+ return { edition, componentTypeId: HTML_BOX_COMPONENT_TYPE_IDS[edition] };
68
+ }
69
+
54
70
  function createDocument(title, summary, identity) {
55
71
  const intro = summary || `这个妙笔应用以交互式网页的形式展示「${title}」,可直接在文档中查看和体验。`;
56
72
  const content = `<title>${escapeXml(title)}</title><p>${escapeXml(intro)}</p>`;
@@ -73,7 +89,7 @@ function createDocument(title, summary, identity) {
73
89
  };
74
90
  }
75
91
 
76
- function insertHtmlBox(docToken, recordObject, identity) {
92
+ function insertHtmlBox(docToken, recordObject, identity, componentTypeId) {
77
93
  const record = JSON.stringify(recordObject);
78
94
  const resp = runJson([
79
95
  'api',
@@ -87,15 +103,18 @@ function insertHtmlBox(docToken, recordObject, identity) {
87
103
  block_type: 40,
88
104
  add_ons: {
89
105
  component_id: '',
90
- component_type_id: HTML_BOX_COMPONENT_TYPE_ID,
106
+ component_type_id: componentTypeId,
91
107
  record,
92
108
  },
93
109
  }],
94
110
  index: -1,
95
111
  }),
96
112
  ]);
97
- if (resp?.code !== 0) throw new Error(`failed to insert HTML Box: ${JSON.stringify(resp)}`);
113
+ if (!isSuccessfulLarkResponse(resp)) throw new Error(`failed to insert HTML Box: ${JSON.stringify(resp)}`);
98
114
  const block = resp?.data?.children?.[0];
115
+ if (block?.add_ons?.component_type_id && block.add_ons.component_type_id !== componentTypeId) {
116
+ throw new Error(`inserted HTML Box component type mismatch: ${JSON.stringify(block?.add_ons)}`);
117
+ }
99
118
  const returnedRecord = parseRecordValue(block?.add_ons?.record);
100
119
  if (!recordIncludes(recordObject, returnedRecord)) {
101
120
  throw new Error(`inserted HTML Box record mismatch: ${JSON.stringify(block?.add_ons)}`);
@@ -103,6 +122,10 @@ function insertHtmlBox(docToken, recordObject, identity) {
103
122
  return block?.block_id || '';
104
123
  }
105
124
 
125
+ function isSuccessfulLarkResponse(resp) {
126
+ return resp?.ok === true || resp?.code === 0;
127
+ }
128
+
106
129
  function runJson(commandArgs) {
107
130
  const out = execFileSync('lark-cli', commandArgs, {
108
131
  encoding: 'utf8',
@@ -149,4 +172,4 @@ function escapeXml(value) {
149
172
  .replaceAll("'", '&apos;');
150
173
  }
151
174
 
152
- module.exports = { run };
175
+ module.exports = { HTML_BOX_COMPONENT_TYPE_IDS, resolveEdition, isSuccessfulLarkResponse, run };
@@ -17,7 +17,103 @@ async function run(args, opts) {
17
17
  if (sub === 'list') return list(opts);
18
18
  if (sub === 'export') return exportApps(opts);
19
19
  if (sub === 'delete') return deletePage(opts);
20
- fail('Usage: magic-builder page <publish|list|export|delete>', 'E_INVALID_ARGS');
20
+ if (sub === 'collaborators') return collaborators(args.slice(1), opts);
21
+ if (sub === 'access') return access(args.slice(1), opts);
22
+ fail('Usage: magic-builder page <publish|list|export|delete|collaborators|access>', 'E_INVALID_ARGS');
23
+ }
24
+
25
+ async function authenticatedRequest(opts, endpoint, requestOpts = {}) {
26
+ let token;
27
+ try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
28
+ try { return await request(`${getBaseUrl(opts)}${endpoint}`, { token, ...requestOpts }); }
29
+ catch (e) { failFromHttpError(e); }
30
+ }
31
+
32
+ function requiredPageId(opts, command) {
33
+ const id = normalizeId(opts.id);
34
+ if (!id) fail(`Missing --id. Usage: magic-builder page ${command} --id <id>`, 'E_INVALID_ARGS');
35
+ return id;
36
+ }
37
+
38
+ function readJsonFile(file) {
39
+ if (!file) return null;
40
+ if (!fs.existsSync(file)) fail(`File not found: ${file}`, 'E_NOT_FOUND');
41
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
42
+ catch (e) { fail(`Invalid JSON file: ${e.message}`, 'E_INVALID_ARGS'); }
43
+ }
44
+
45
+ function parseIds(value, pattern, label, allowEmpty = false) {
46
+ const raw = Array.isArray(value) ? value : String(value ?? '').split(/[,,;;\n\r\t|]/);
47
+ const values = Array.from(new Set(raw.map((item) => String(item || '').trim()).filter(Boolean)));
48
+ const invalid = values.filter((id) => !pattern.test(id));
49
+ if (invalid.length) fail(`Invalid ${label}: ${invalid.join(', ')}`, 'E_INVALID_ARGS');
50
+ if (!allowEmpty && !values.length) fail(`Missing ${label}`, 'E_INVALID_ARGS');
51
+ return values;
52
+ }
53
+
54
+ async function collaborators(args, opts) {
55
+ const action = String(args[0] || '').toLowerCase();
56
+ const id = requiredPageId(opts, `collaborators ${action || '<list|add|remove|set>'}`);
57
+ const endpoint = `/api/html-box/${encodeURIComponent(id)}/collaborators`;
58
+ if (action === 'list') {
59
+ const res = await authenticatedRequest(opts, endpoint);
60
+ if (res.code !== 0) fail(res.msg || 'Collaborator query failed', 'E_COLLABORATORS_FAILED');
61
+ return success(res.data || res, opts);
62
+ }
63
+ if (!['add', 'remove', 'set'].includes(action)) fail('Usage: magic-builder page collaborators <list|add|remove|set> --id <id>', 'E_INVALID_ARGS');
64
+ const file = readJsonFile(opts.file);
65
+ const openIds = parseIds(file?.open_ids ?? file?.openIds ?? opts.openIds, /^ou_[A-Za-z0-9_-]+$/, 'open_id');
66
+ const res = await authenticatedRequest(opts, endpoint, { method: 'PATCH', body: { operation: action, open_ids: openIds } });
67
+ if (res.code !== 0) fail(res.msg || 'Collaborator update failed', 'E_COLLABORATORS_FAILED');
68
+ success(res.data || res, opts);
69
+ }
70
+
71
+ function parseBoolean(value, label) {
72
+ if (typeof value === 'boolean') return value;
73
+ const normalized = String(value ?? '').trim().toLowerCase();
74
+ if (['true', '1', 'yes', 'on', 'enabled'].includes(normalized)) return true;
75
+ if (['false', '0', 'no', 'off', 'disabled'].includes(normalized)) return false;
76
+ fail(`${label} must be true or false`, 'E_INVALID_ARGS');
77
+ }
78
+
79
+ async function access(args, opts) {
80
+ const action = String(args[0] || '').toLowerCase();
81
+ const id = requiredPageId(opts, `access ${action || '<get|enable|disable|require-login|users|groups|set>'}`);
82
+ const endpoint = `/api/html-box/${encodeURIComponent(id)}/access`;
83
+ if (action === 'get') {
84
+ const res = await authenticatedRequest(opts, endpoint);
85
+ if (res.code !== 0) fail(res.msg || 'Access query failed', 'E_ACCESS_FAILED');
86
+ return success(res.data || res, opts);
87
+ }
88
+ let body;
89
+ if (action === 'enable' || action === 'disable') {
90
+ body = { access_control_enabled: action === 'enable' };
91
+ } else if (action === 'require-login') {
92
+ if (opts.enabled === undefined) fail('Missing --enabled true|false', 'E_INVALID_ARGS');
93
+ body = { require_feishu_login: parseBoolean(opts.enabled, '--enabled') };
94
+ } else if (action === 'users' || action === 'groups') {
95
+ const operation = String(args[1] || '').toLowerCase();
96
+ if (!['add', 'remove', 'set'].includes(operation)) fail(`Usage: magic-builder page access ${action} <add|remove|set> --id <id>`, 'E_INVALID_ARGS');
97
+ const file = readJsonFile(opts.file);
98
+ const isUsers = action === 'users';
99
+ const source = isUsers ? (file?.open_ids ?? file?.user_open_ids ?? opts.openIds) : (file?.chat_ids ?? file?.group_chat_ids ?? opts.chatIds);
100
+ const ids = parseIds(source, isUsers ? /^ou_[A-Za-z0-9_-]+$/ : /^oc_[A-Za-z0-9_-]+$/, isUsers ? 'open_id' : 'chat_id', operation === 'set');
101
+ body = { operation, target: action, [isUsers ? 'access_users' : 'access_groups']: ids.map((value) => ({ id: value })) };
102
+ } else if (action === 'set') {
103
+ const file = readJsonFile(opts.file);
104
+ if (!file || typeof file !== 'object' || Array.isArray(file)) fail('Missing --file <access.json>', 'E_INVALID_ARGS');
105
+ body = {};
106
+ if (Object.prototype.hasOwnProperty.call(file, 'access_control_enabled')) body.access_control_enabled = parseBoolean(file.access_control_enabled, 'access_control_enabled');
107
+ if (Object.prototype.hasOwnProperty.call(file, 'require_feishu_login')) body.require_feishu_login = parseBoolean(file.require_feishu_login, 'require_feishu_login');
108
+ if (Object.prototype.hasOwnProperty.call(file, 'user_open_ids')) body.access_users = parseIds(file.user_open_ids, /^ou_[A-Za-z0-9_-]+$/, 'open_id', true).map((value) => ({ id: value }));
109
+ if (Object.prototype.hasOwnProperty.call(file, 'group_chat_ids')) body.access_groups = parseIds(file.group_chat_ids, /^oc_[A-Za-z0-9_-]+$/, 'chat_id', true).map((value) => ({ id: value }));
110
+ if (!Object.keys(body).length) fail('Access JSON contains no supported settings', 'E_INVALID_ARGS');
111
+ } else {
112
+ fail('Usage: magic-builder page access <get|enable|disable|require-login|users|groups|set> --id <id>', 'E_INVALID_ARGS');
113
+ }
114
+ const res = await authenticatedRequest(opts, endpoint, { method: 'PATCH', body });
115
+ if (res.code !== 0) fail(res.msg || 'Access update failed', 'E_ACCESS_FAILED');
116
+ success(res.data || res, opts);
21
117
  }
22
118
 
23
119
  async function publish(args, opts) {
@@ -39,7 +135,9 @@ async function publish(args, opts) {
39
135
  const existingId = opts.id || config[relPath]?.id || config[relPath]?.remoteId;
40
136
 
41
137
  const body = { html, title };
138
+ if (opts.customId !== undefined) body.custom_id = String(opts.customId).trim();
42
139
  if (opts.openSource) body.is_open_source = true;
140
+ if (opts.comments !== undefined) body.comments_enabled = opts.comments === true || String(opts.comments).toLowerCase() !== 'false';
43
141
 
44
142
  if (!opts.quiet) process.stderr.write(`${existingId ? 'Updating' : 'Publishing'} ${relPath}... `);
45
143
 
@@ -56,13 +154,17 @@ async function publish(args, opts) {
56
154
  const data = res.data || res;
57
155
  const id = normalizeId(data.id || data.record_id || existingId);
58
156
  const htmlBoxUrl = data.html_box_url || `${baseUrl}/html-box/${id}`;
157
+ const appUrl = data.app_url || `${baseUrl}/app/${id}`;
158
+ const url = data.url || (data.runtime === 'html-box' ? htmlBoxUrl : appUrl);
59
159
  const dashboardUrl = data.dashboard_url || `${baseUrl}/dashboard/${id}`;
60
160
 
61
161
  config[relPath] = {
62
162
  id,
63
163
  remoteId: id,
64
164
  title,
165
+ customId: opts.customId !== undefined ? String(opts.customId).trim() : (config[relPath]?.customId || ''),
65
166
  urls: {
167
+ app: appUrl,
66
168
  html_box: htmlBoxUrl,
67
169
  dashboard: dashboardUrl,
68
170
  panel: data.panel_url || '',
@@ -76,6 +178,10 @@ async function publish(args, opts) {
76
178
  success({
77
179
  id,
78
180
  title,
181
+ custom_id: data.custom_id || body.custom_id || '',
182
+ url,
183
+ runtime: data.runtime || (url === htmlBoxUrl ? 'html-box' : 'app'),
184
+ app_url: appUrl,
79
185
  html_box_url: htmlBoxUrl,
80
186
  dashboard_url: dashboardUrl,
81
187
  panel_url: data.panel_url || '',
@@ -273,4 +379,4 @@ function formatHtmlLimitMessage(html) {
273
379
  return `HTML exceeds publish limit (${HTML_LIMIT} characters; ${HTML_CODE_FIELD_COUNT} HTML code fields x ${HTML_CODE_FIELD_LIMIT} characters). Current: ${html.length}; over by: ${overflow}. ${hint}`;
274
380
  }
275
381
 
276
- module.exports = { run };
382
+ module.exports = { run, parseIds, parseBoolean };
@@ -3,6 +3,7 @@
3
3
  const { existsSync, readFileSync } = require('fs');
4
4
  const { resolve } = require('path');
5
5
  const { success, fail } = require('../lib/output');
6
+ const { getPerformanceConfigPath, loadPerformanceConfig } = require('../lib/config');
6
7
 
7
8
  const DEFAULTS = {
8
9
  endpoint: 'https://people.bytedance.net/perf/api/foundation/v2/draft',
@@ -13,8 +14,8 @@ const DEFAULTS = {
13
14
 
14
15
  async function run(args, opts) {
15
16
  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');
17
+ if (args.length > 1 || !['draft', 'submit', 'self-review', 'key-works', 'review-users', 'invite-review'].includes(action)) {
18
+ fail('Usage: magic-builder performance [submit|self-review|key-works|review-users|invite-review] --review-url <url> --cookie <content|file>', 'E_INVALID_ARGS');
18
19
  }
19
20
 
20
21
  const cookie = readCookie(opts.cookie, opts.cookieFile);
@@ -23,7 +24,44 @@ async function run(args, opts) {
23
24
  const csrf = getCookie(cookie, 'x-f-csrf');
24
25
  if (!csrf) fail('Cookie does not contain x-f-csrf.', 'E_INVALID_ARGS');
25
26
 
26
- const config = await resolveConfig(opts, cookie);
27
+ if (action === 'key-works') {
28
+ const data = await getKeyWorks(opts, cookie, csrf);
29
+ success(data, opts);
30
+ return;
31
+ }
32
+ if (action === 'review-users') {
33
+ const data = await getReviewUsers(opts, cookie, csrf);
34
+ success(data, opts);
35
+ return;
36
+ }
37
+ if (action === 'invite-review') {
38
+ const payload = readInviteReviewPayload(opts);
39
+ const endpoint = String(opts.endpoint || DEFAULTS.endpoint);
40
+ if (opts.dryRun) {
41
+ success({ dryRun: true, action: 'invite-review', endpoint, payload }, opts);
42
+ return;
43
+ }
44
+ const review = parseReviewLocation(opts.reviewUrl || opts.referer);
45
+ const data = await postPerformance(endpoint, payload, cookie, csrf, {
46
+ referer: String(opts.referer || review.referer || 'https://people.bytedance.net/performance/perf/review'),
47
+ tenantId: String(opts.tenantId || ''),
48
+ });
49
+ success(data, opts);
50
+ return;
51
+ }
52
+
53
+ const config = await resolveConfig(opts, cookie, action === 'self-review' ? 'self_review' : 'confirm_invitation');
54
+ if (action === 'self-review') {
55
+ const updates = readSelfReviewInputs(opts);
56
+ const payload = buildSelfReviewPayloadFromDraft(config.draft, updates);
57
+ if (opts.dryRun) {
58
+ success({ dryRun: true, action: 'self-review', endpoint: config.endpoint, payload }, opts);
59
+ return;
60
+ }
61
+ const data = await postPerformance(config.endpoint, payload, cookie, csrf, config);
62
+ success(data, opts);
63
+ return;
64
+ }
27
65
  if (action === 'submit') {
28
66
  const payload = buildStagePayloadFromDraft(config.draft, config);
29
67
  if (opts.dryRun) {
@@ -53,6 +91,136 @@ async function run(args, opts) {
53
91
  success(data, opts);
54
92
  }
55
93
 
94
+ function readInviteReviewPayload(opts) {
95
+ if (opts.payload && opts.payloadFile) {
96
+ fail('Pass either --payload or --payload-file, not both.', 'E_INVALID_ARGS');
97
+ }
98
+ const input = opts.payloadFile
99
+ ? readFileSync(resolve(String(opts.payloadFile)), 'utf8')
100
+ : String(opts.payload || '');
101
+ if (!input.trim()) fail('invite-review requires --payload <json> or --payload-file <file>.', 'E_INVALID_ARGS');
102
+ const payload = parseJson(input);
103
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
104
+ fail('Invite-review payload must be a valid JSON object.', 'E_INVALID_ARGS');
105
+ }
106
+ const missing = ['key', 'data', 'version'].filter(key => payload[key] === undefined || payload[key] === null);
107
+ if (missing.length) fail(`Invite-review payload is missing ${missing.join(', ')}.`, 'E_INVALID_ARGS');
108
+ if (!String(payload.key).includes('__invite_review__')) {
109
+ fail('Invite-review payload key must contain __invite_review__.', 'E_INVALID_ARGS');
110
+ }
111
+ return normalizePerformancePayload(payload);
112
+ }
113
+
114
+ function normalizePerformancePayload(payload) {
115
+ const normalized = structuredClone(payload);
116
+ normalizePerformanceNode(normalized);
117
+ return normalized;
118
+ }
119
+
120
+ function normalizePerformanceNode(node) {
121
+ if (!node || typeof node !== 'object') return;
122
+ if (Array.isArray(node)) {
123
+ node.forEach(normalizePerformanceNode);
124
+ return;
125
+ }
126
+ for (const [key, value] of Object.entries(node)) {
127
+ if (key === 'insert' && typeof value === 'string') {
128
+ node[key] = value.replace(/\\n(?=\r?\n|$)/g, '\n');
129
+ continue;
130
+ }
131
+ if (typeof value === 'string' && ['text', 'value', 'rich_text'].includes(key)) {
132
+ const embedded = parseJson(value);
133
+ if (embedded && typeof embedded === 'object') {
134
+ normalizePerformanceNode(embedded);
135
+ node[key] = JSON.stringify(embedded);
136
+ }
137
+ continue;
138
+ }
139
+ normalizePerformanceNode(value);
140
+ }
141
+ }
142
+
143
+ async function getKeyWorks(opts, cookie, csrf) {
144
+ if (!opts.rootReviewId) {
145
+ fail('key-works requires --root-review-id <id>.', 'E_INVALID_ARGS');
146
+ }
147
+ const personalConfig = opts.performanceConfig || loadPerformanceConfig();
148
+ const templateId = String(opts.templateId || personalConfig.invite_review_template_id || personalConfig.inviteReviewTemplateId || '');
149
+ if (!templateId) {
150
+ fail(`Missing template id. Use --template-id <id> or set invite_review_template_id in ${getPerformanceConfigPath()}.`, 'E_INVALID_ARGS');
151
+ }
152
+ const endpoint = assertPeoplePerfEndpoint(opts.stageEndpoint || DEFAULTS.submitEndpoint);
153
+ endpoint.searchParams.set('template_id', templateId);
154
+ endpoint.searchParams.set('root_review_id', String(opts.rootReviewId));
155
+ const referer = String(opts.referer || opts.reviewUrl || 'https://people.bytedance.net/performance/perf/review');
156
+ const headers = {
157
+ accept: 'application/json, text/plain, */*',
158
+ 'accept-language': 'zh-CN,zh;q=0.9',
159
+ cookie,
160
+ referer,
161
+ 'x-f-csrf': csrf,
162
+ 'x-f-lang': 'zh-CN',
163
+ 'x-f-timezone': 'Asia/Shanghai',
164
+ };
165
+ if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
166
+ const response = await fetch(endpoint, { method: 'GET', headers });
167
+ const text = await response.text();
168
+ const data = parseJson(text);
169
+ if (!response.ok) {
170
+ const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
171
+ error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
172
+ throw error;
173
+ }
174
+ if (data === null) {
175
+ const error = new Error('Performance response is not valid JSON.');
176
+ error.code = 'E_REQUEST_FAILED';
177
+ throw error;
178
+ }
179
+ return data;
180
+ }
181
+
182
+ async function getReviewUsers(opts, cookie, csrf) {
183
+ const location = opts.reviewUrl ? parseReviewLocation(opts.reviewUrl) : {};
184
+ const reviewId = String(opts.reviewId || location.reviewId || '');
185
+ const stageId = String(opts.stageId || location.formId || '');
186
+ if (!reviewId || !stageId) {
187
+ fail('review-users requires --review-url <url> or both --review-id <id> and --stage-id <id>.', 'E_INVALID_ARGS');
188
+ }
189
+ const base = assertPeoplePerfEndpoint(opts.reviewUsersEndpoint || DEFAULTS.submitEndpoint);
190
+ const endpoint = new URL(`${base.origin}/perf/api/review/v2/stage/${encodeURIComponent(reviewId)}/list_invite_review`);
191
+ const referer = String(opts.referer || location.referer || `https://people.bytedance.net/performance/perf/review/${reviewId}/${stageId}/members`);
192
+ const headers = {
193
+ accept: 'application/json, text/plain, */*',
194
+ 'accept-language': 'zh-CN,zh;q=0.9',
195
+ 'content-type': 'application/json;charset=UTF-8',
196
+ cookie,
197
+ origin: 'https://people.bytedance.net',
198
+ referer,
199
+ 'x-f-csrf': csrf,
200
+ 'x-f-lang': 'zh-CN',
201
+ 'x-f-timezone': 'Asia/Shanghai',
202
+ };
203
+ if (opts.tenantId) headers['rpc-persist-lane-c-perfx-tenant-id'] = String(opts.tenantId);
204
+ const response = await fetch(endpoint, {
205
+ method: 'POST',
206
+ headers,
207
+ body: JSON.stringify({ stage_id: stageId }),
208
+ });
209
+ const text = await response.text();
210
+ const data = parseJson(text);
211
+ if (!response.ok) {
212
+ const error = new Error(data?.message || data?.msg || `Performance request failed: HTTP ${response.status}`);
213
+ error.code = response.status === 401 || response.status === 403 ? 'E_AUTH_FAILED' : 'E_REQUEST_FAILED';
214
+ throw error;
215
+ }
216
+ if (data === null) {
217
+ const error = new Error('Performance response is not valid JSON.');
218
+ error.code = 'E_REQUEST_FAILED';
219
+ throw error;
220
+ }
221
+ return data;
222
+ }
223
+
56
224
  async function postPerformance(endpoint, payload, cookie, csrf, config) {
57
225
  assertPeoplePerfEndpoint(endpoint);
58
226
  const response = await fetch(endpoint, {
@@ -129,14 +297,14 @@ function getCookie(cookie, name) {
129
297
  return '';
130
298
  }
131
299
 
132
- async function resolveConfig(opts, cookie) {
300
+ async function resolveConfig(opts, cookie, draftKind = 'confirm_invitation') {
133
301
  const review = parseReviewLocation(opts.reviewUrl || opts.referer, opts.reviewId, opts.formId);
134
302
  if (review.reviewId && review.formId) {
135
303
  const settings = await requestJson(opts.settingsEndpoint || DEFAULTS.settingsEndpoint, cookie, review.referer);
136
304
  const operatorId = String(opts.operatorId || settings?.data?.user?.id || '');
137
305
  const tenantId = String(opts.tenantId || settings?.data?.tenant?.id || '');
138
306
  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}`;
307
+ const key = buildDraftKey(review.reviewId, draftKind, operatorId, review.formId);
140
308
  const draftUrl = new URL(opts.draftEndpoint || DEFAULTS.draftEndpoint);
141
309
  draftUrl.searchParams.set('key', key);
142
310
  const draftResponse = await requestJson(draftUrl.toString(), cookie, review.referer);
@@ -182,6 +350,36 @@ async function resolveConfig(opts, cookie) {
182
350
  };
183
351
  }
184
352
 
353
+ function buildDraftKey(reviewId, draftKind, operatorId, formId) {
354
+ return `${reviewId}__${draftKind}__${operatorId}__${formId}`;
355
+ }
356
+
357
+ function readSelfReviewInputs(opts) {
358
+ const inputs = [
359
+ ['good', 'goodFile'],
360
+ ['improve', 'improveFile'],
361
+ ['valuesComment', 'valuesCommentFile'],
362
+ ];
363
+ const updates = {};
364
+ for (const [valueKey, fileKey] of inputs) {
365
+ const hasValue = Object.prototype.hasOwnProperty.call(opts, valueKey);
366
+ const hasFile = Object.prototype.hasOwnProperty.call(opts, fileKey);
367
+ if (hasValue && hasFile) {
368
+ fail(`Pass either --${toKebab(valueKey)} or --${toKebab(fileKey)}, not both.`, 'E_INVALID_ARGS');
369
+ }
370
+ if (hasFile) updates[valueKey] = readFileSync(resolve(String(opts[fileKey])), 'utf8');
371
+ else if (hasValue) updates[valueKey] = String(opts[valueKey]);
372
+ }
373
+ if (!Object.keys(updates).length) {
374
+ fail('Pass at least one of --good, --improve, or --values-comment (or the matching --*-file option).', 'E_INVALID_ARGS');
375
+ }
376
+ return updates;
377
+ }
378
+
379
+ function toKebab(value) {
380
+ return String(value).replace(/[A-Z]/g, letter => `-${letter.toLowerCase()}`);
381
+ }
382
+
185
383
  function parseReviewLocation(value, reviewId, formId) {
186
384
  if (reviewId && formId) {
187
385
  return {
@@ -295,6 +493,42 @@ function buildPayloadFromDraft(markdown, draft, config) {
295
493
  };
296
494
  }
297
495
 
496
+ function buildSelfReviewPayloadFromDraft(draft, updates) {
497
+ const snapshot = structuredClone(draft);
498
+ const units = snapshot?.data?.units;
499
+ if (!Array.isArray(units)) throw selfReviewSchemaError('Current self-review draft does not contain data.units.');
500
+ const fields = units.flatMap(unit => Array.isArray(unit?.fields) ? unit.fields : []);
501
+ const textFields = fields.filter(field => field?.entityType === 'text');
502
+ const tagTextFields = fields.filter(field => field?.entityType === 'tag_text');
503
+ if (textFields.length !== 2 || tagTextFields.length !== 1) {
504
+ throw selfReviewSchemaError('Unable to identify the self-review text fields from the current template.');
505
+ }
506
+ if (Object.prototype.hasOwnProperty.call(updates, 'good')) {
507
+ textFields[0].value = selfReviewTextValue(updates.good);
508
+ }
509
+ if (Object.prototype.hasOwnProperty.call(updates, 'improve')) {
510
+ textFields[1].value = selfReviewTextValue(updates.improve);
511
+ }
512
+ if (Object.prototype.hasOwnProperty.call(updates, 'valuesComment')) {
513
+ tagTextFields[0].json_value = selfReviewTextValue(updates.valuesComment);
514
+ }
515
+ return {
516
+ key: snapshot.key,
517
+ data: snapshot.data,
518
+ version: Number(snapshot.version),
519
+ };
520
+ }
521
+
522
+ function selfReviewSchemaError(message) {
523
+ const error = new Error(message);
524
+ error.code = 'E_REQUEST_FAILED';
525
+ return error;
526
+ }
527
+
528
+ function selfReviewTextValue(text) {
529
+ return { '0': { ops: markdownToDelta(String(text)), zoneId: '0', zoneType: 'Z' } };
530
+ }
531
+
298
532
  function buildStagePayloadFromDraft(draft, config = {}) {
299
533
  if (!draft?.data?.keyWorks) fail('Current draft does not contain keyWorks.', 'E_REQUEST_FAILED');
300
534
  const keyWorks = structuredClone(draft.data.keyWorks);
@@ -445,4 +679,4 @@ function parseJson(text) {
445
679
  try { return text ? JSON.parse(text) : {}; } catch (_) { return null; }
446
680
  }
447
681
 
448
- module.exports = { run, readCookie, normalizeCookie, getCookie, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft, buildStagePayloadFromDraft, assertPeoplePerfEndpoint };
682
+ module.exports = { run, getKeyWorks, getReviewUsers, readInviteReviewPayload, normalizePerformancePayload, readCookie, normalizeCookie, getCookie, readSelfReviewInputs, buildDraftKey, parseReviewLocation, splitMarkdownSections, markdownToDelta, buildPayload, buildPayloadFromDraft, buildSelfReviewPayloadFromDraft, buildStagePayloadFromDraft, assertPeoplePerfEndpoint };
@@ -24,7 +24,7 @@ async function run(args, opts) {
24
24
  if (sub !== 'check-update' && sub !== 'update' && sub !== 'install') {
25
25
  fail('Usage: magic-builder skill <check-update|update|install>', 'E_INVALID_ARGS');
26
26
  }
27
- const skillsRoot = path.resolve(opts.skillsRoot || path.join(os.homedir(), '.codex', 'skills'));
27
+ const skillsRoot = path.resolve(opts.skillsRoot || defaultSkillsRoot());
28
28
  const environment = resolveEnvironment(String(opts.environment || 'auto'), skillsRoot);
29
29
  const remote = await prepareRemotePackage();
30
30
  const report = buildReport(remote, skillsRoot, environment);
@@ -106,7 +106,7 @@ function buildCloudUpdateDescriptor(report, options = {}) {
106
106
  }] : [],
107
107
  instructions: [
108
108
  'Use the cloud runtime managed skill update/install mechanism.',
109
- 'Do not copy files into ~/.codex/skills or overwrite local filesystem paths in cloud mode.',
109
+ 'Do not copy files into ~/.agents/skills or overwrite local filesystem paths in cloud mode.',
110
110
  'If no managed cloud updater is exposed, report this descriptor and do not claim the update was applied.',
111
111
  ],
112
112
  };
@@ -189,4 +189,8 @@ function ensureSafeSkillId(id) {
189
189
  if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) throw new Error(`Unsafe skill id in update package: ${id}`);
190
190
  }
191
191
 
192
- module.exports = { run };
192
+ function defaultSkillsRoot(homeDir = os.homedir()) {
193
+ return path.join(homeDir, '.agents', 'skills');
194
+ }
195
+
196
+ module.exports = { run, defaultSkillsRoot };
package/src/lib/config.js CHANGED
@@ -9,6 +9,7 @@ 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
11
  const WIDGET_PUBLISH_CONFIG_FILE = 'widget-publish.json';
12
+ const PERFORMANCE_CONFIG_FILE = 'performance.json';
12
13
 
13
14
  function normalizeMagicBaseUrl(value) {
14
15
  const raw = String(value || DEFAULT_BASE_URL).trim().replace(/\/+$/, '');
@@ -28,6 +29,16 @@ function getWidgetPublishConfigPath() {
28
29
  return path.join(CONFIG_DIR, WIDGET_PUBLISH_CONFIG_FILE);
29
30
  }
30
31
 
32
+ function getPerformanceConfigPath() {
33
+ return path.join(CONFIG_DIR, PERFORMANCE_CONFIG_FILE);
34
+ }
35
+
36
+ function loadPerformanceConfig() {
37
+ const p = getPerformanceConfigPath();
38
+ if (!fs.existsSync(p)) return {};
39
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch (_) { return {}; }
40
+ }
41
+
31
42
  function loadAppsConfig() {
32
43
  const p = fs.existsSync(getAppsConfigPath())
33
44
  ? getAppsConfigPath()
@@ -50,6 +61,8 @@ module.exports = {
50
61
  getBaseUrl,
51
62
  getAppsConfigPath,
52
63
  getWidgetPublishConfigPath,
64
+ getPerformanceConfigPath,
65
+ loadPerformanceConfig,
53
66
  loadAppsConfig,
54
67
  saveAppsConfig,
55
68
  };
package/src/lib/help.js CHANGED
@@ -15,7 +15,7 @@ SYNTAX:
15
15
 
16
16
  COMMANDS:
17
17
  auth Login and manage Magic developer tokens
18
- page Publish, list, export, and delete Magic pages
18
+ page Publish and manage Magic pages, collaborators, and access
19
19
  faas Publish, list, and delete Magic FaaS functions
20
20
  file Upload, list, and delete TOS files
21
21
  link Generate Magic share links
@@ -39,6 +39,8 @@ EXAMPLES:
39
39
  magic-builder page list --scope mine
40
40
  magic-builder page export --id recxxx --out app.html
41
41
  magic-builder page delete --id recxxx
42
+ magic-builder page collaborators add --id recxxx --open-ids ou_xxx,ou_yyy
43
+ magic-builder page access groups add --id recxxx --chat-ids oc_xxx
42
44
  magic-builder faas publish handler.js --name report-api
43
45
  magic-builder faas list
44
46
  magic-builder faas delete --id recxxx
@@ -85,11 +87,19 @@ COMMANDS:
85
87
  auth set <token>
86
88
  auth show
87
89
 
88
- page publish <html> --title <title> [--id <id>] [--open-source]
90
+ page publish <html> --title <title> [--id <id>] [--custom-id <id>] [--open-source] [--comments]
89
91
  page list [--title <keyword>] [--scope mine|public]
90
92
  page export --id <id> --out <file|dir> [--scope mine|public]
91
93
  page export --title <keyword> --out <dir> [--all] [--scope mine|public]
92
94
  page delete --id <id>
95
+ page collaborators list --id <id>
96
+ page collaborators add|remove|set --id <id> --open-ids <ou_a,ou_b>
97
+ page collaborators set --id <id> --file <collaborators.json>
98
+ page access get|enable|disable --id <id>
99
+ page access require-login --id <id> --enabled true|false
100
+ page access users add|remove|set --id <id> --open-ids <ou_a,ou_b>
101
+ page access groups add|remove|set --id <id> --chat-ids <oc_a,oc_b>
102
+ page access set --id <id> --file <access.json>
93
103
 
94
104
  faas publish <file.js> --name <name> [--id <id>]
95
105
  faas publish --code <code> --name <name> [--id <id>]
@@ -103,14 +113,20 @@ COMMANDS:
103
113
  link create --title <text>
104
114
  link create --fid <id>
105
115
 
106
- doc create --html <file> --title <title> [--summary <text>] [--as bot|user]
107
- doc append --html <file> --doc-token <token> [--as bot|user]
116
+ doc create --html <file> --title <title> [--summary <text>] [--as bot|user] [--edition external|bytedance]
117
+ doc append --html <file> --doc-token <token> [--as bot|user] [--edition external|bytedance]
108
118
 
109
119
  feedback create --feedback <text> [--title <title>] [--summary <text>]
110
120
  feedback create --feedback-file <file> [--dry-run]
111
121
 
112
122
  performance --review-url <url> --cookie <content|file> --markdown <content|file|url> [--dry-run]
123
+ perf self-review --review-url <url> [--good <text>|--good-file <file>]
124
+ [--improve <text>|--improve-file <file>] [--values-comment <text>|--values-comment-file <file>] [--dry-run]
113
125
  perf submit --review-url <url> --cookie-file <file> --template-group-id <id> [--dry-run|--yes]
126
+ perf key-works --root-review-id <id> [--template-id <id>] --cookie-file <file>
127
+ perf review-users --review-id <id> --stage-id <id> --cookie-file <file>
128
+ perf review-users --review-url <url> --cookie-file <file>
129
+ perf invite-review --payload-file <file> --review-url <url> --cookie-file <file> [--dry-run]
114
130
  perf --review-url <url> --cookie-file <file> --markdown-file <file> [--dry-run]
115
131
 
116
132
  widget-publish --app-id <id> --block-type-id <id> [--version <x.y.z>] [--change-log <text>]
@@ -161,11 +177,19 @@ SYNTAX:
161
177
  page: `@HELP magic-builder/page
162
178
 
163
179
  SYNTAX:
164
- magic-builder page publish <html> --title <title> [--id <id>] [--open-source]
180
+ magic-builder page publish <html> --title <title> [--id <id>] [--custom-id <id>] [--open-source] [--comments]
165
181
  magic-builder page list [--title <keyword>] [--scope mine|public]
166
182
  magic-builder page export --id <id> --out <file|dir> [--scope mine|public]
167
183
  magic-builder page export --title <keyword> --out <dir> [--all] [--scope mine|public]
168
184
  magic-builder page delete --id <id>
185
+ magic-builder page collaborators list --id <id>
186
+ magic-builder page collaborators add|remove|set --id <id> --open-ids <ou_a,ou_b>
187
+ magic-builder page collaborators set --id <id> --file <collaborators.json>
188
+ magic-builder page access get|enable|disable --id <id>
189
+ magic-builder page access require-login --id <id> --enabled true|false
190
+ magic-builder page access users add|remove|set --id <id> --open-ids <ou_a,ou_b>
191
+ magic-builder page access groups add|remove|set --id <id> --chat-ids <oc_a,oc_b>
192
+ magic-builder page access set --id <id> --file <access.json>
169
193
  `,
170
194
  faas: `@HELP magic-builder/faas
171
195
 
@@ -191,8 +215,8 @@ SYNTAX:
191
215
  doc: `@HELP magic-builder/doc
192
216
 
193
217
  SYNTAX:
194
- magic-builder doc create --html <file> --title <title> [--summary <text>] [--as bot|user]
195
- magic-builder doc append --html <file> --doc-token <token> [--as bot|user]
218
+ magic-builder doc create --html <file> --title <title> [--summary <text>] [--as bot|user] [--edition external|bytedance]
219
+ magic-builder doc append --html <file> --doc-token <token> [--as bot|user] [--edition external|bytedance]
196
220
  `,
197
221
  feedback: `@HELP magic-builder/feedback
198
222
 
@@ -204,8 +228,14 @@ SYNTAX:
204
228
 
205
229
  SYNTAX:
206
230
  magic-builder performance --review-url <url> --cookie <content|file> --markdown <content|file|url>
231
+ magic-builder perf self-review --review-url <url> --good <text> --improve <text> --values-comment <text>
232
+ magic-builder perf self-review --review-url <url> --good-file <file> --improve-file <file> --values-comment-file <file>
207
233
  magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --dry-run
208
234
  magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --yes
235
+ magic-builder perf key-works --root-review-id <id> [--template-id <id>] --cookie-file <file>
236
+ magic-builder perf review-users --review-id <id> --stage-id <id> --cookie-file <file>
237
+ magic-builder perf review-users --review-url <url> --cookie-file <file>
238
+ magic-builder perf invite-review --payload-file <file> --review-url <url> --cookie-file <file> [--dry-run]
209
239
  magic-builder perf --review-url <url> --cookie-file <file> --markdown-file <file>
210
240
 
211
241
  COOKIE:
@@ -215,9 +245,38 @@ DRAFT:
215
245
  --review-url resolves operator, tenant, form ids, and the current version
216
246
  from /perf/api/user/settings and /perf/api/foundation/draft.
217
247
 
248
+ SELF REVIEW:
249
+ self-review updates only the explicitly passed text fields and preserves
250
+ the current performance and values ratings. It writes by default; use
251
+ --dry-run to inspect the generated payload without saving it.
252
+
253
+ KEY WORKS:
254
+ key-works reads the review-stage data for the specified template and root
255
+ review. The template id defaults to invite_review_template_id in
256
+ ~/.magic-builder/performance.json and can be overridden with --template-id.
257
+ It is read-only and prints the complete API response as JSON.
258
+
259
+ REVIEW USERS:
260
+ review-users reads the invited review-user list for a review and stage. It
261
+ accepts either --review-url or explicit --review-id and --stage-id. Explicit
262
+ ids override values parsed from the URL. It does not modify review data and
263
+ prints the complete API response as JSON.
264
+
265
+ INVITE REVIEW:
266
+ invite-review writes a complete invite-review draft payload to People. Pass
267
+ the current payload as JSON or a JSON file. Use --dry-run to inspect it
268
+ without writing. The payload must contain key, data, and version, and its key
269
+ must identify an invite_review draft.
270
+
218
271
  OPTIONS:
219
272
  --dry-run Print the generated payload without sending it
220
273
  --review-url <url> People performance review URL (recommended)
274
+ --good <text> Update the "did well" self-review text
275
+ --good-file <file> Read the "did well" text from a file
276
+ --improve <text> Update the "to improve" self-review text
277
+ --improve-file <file> Read the "to improve" text from a file
278
+ --values-comment <text> Update the values self-review comment
279
+ --values-comment-file <file> Read the values comment from a file
221
280
  --template-group-id <id> Template group id (only needed when absent from the draft)
222
281
  --submit-endpoint <url> Override the formal submission endpoint
223
282
  --yes Confirm the irreversible formal submission
@@ -230,6 +289,12 @@ OPTIONS:
230
289
  --source-id <id> Override the form source id
231
290
  --field-source-id <id> Override the writable field source id
232
291
  --tenant-id <id> Override the People tenant id
292
+ --referer <url> Override the People request Referer
293
+ --stage-endpoint <url> Override the key-works query endpoint
294
+ --stage-id <id> Review stage id used by review-users
295
+ --review-users-endpoint <url> Override the review-users API origin
296
+ --payload <json> Complete invite-review draft payload
297
+ --payload-file <file> Read invite-review draft payload from a file
233
298
  --endpoint <url> Override the draft API endpoint
234
299
  `,
235
300
  perf: `@HELP magic-builder/performance
@@ -21,6 +21,7 @@ async function uploadSingle(filePath, opts = {}) {
21
21
 
22
22
  const signRes = await request(`${baseUrl}/api/tos/sign`, {
23
23
  method: 'POST',
24
+ token,
24
25
  body: signBody,
25
26
  });
26
27
 
@@ -48,6 +49,8 @@ async function uploadSingle(filePath, opts = {}) {
48
49
  token,
49
50
  body: {
50
51
  action: 'record',
52
+ audit_id: signRes.data.audit_id,
53
+ audit_table_id: signRes.data.audit_table_id,
51
54
  url,
52
55
  key: signRes.data.key,
53
56
  filename,