magic-builder 1.3.0 → 1.3.2
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 +31 -1
- package/package.json +1 -1
- package/src/commands/config.js +2 -1
- package/src/commands/doc.js +29 -6
- package/src/commands/faas.js +66 -1
- package/src/commands/page.js +108 -2
- package/src/commands/performance.js +240 -6
- package/src/commands/skill.js +7 -3
- package/src/lib/config.js +13 -0
- package/src/lib/help.js +87 -11
- package/src/lib/multipart.js +3 -0
package/README.md
CHANGED
|
@@ -103,6 +103,17 @@ Deploy a FaaS function:
|
|
|
103
103
|
magic-builder faas publish handler.js --name report-api
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Publish a Tools-only MCP service (ordinary HTTP `handler` may coexist):
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
magic-builder faas publish server.js --name weather-mcp --mcp --auth key,oauth --oauth-policy all
|
|
110
|
+
magic-builder faas mcp key create --id <id> --name production
|
|
111
|
+
magic-builder faas mcp key list --id <id>
|
|
112
|
+
magic-builder faas mcp key revoke --id <id> --key-id <key_id>
|
|
113
|
+
magic-builder faas mcp auth get --id <id>
|
|
114
|
+
magic-builder faas mcp auth update --id <id> --oauth-policy whitelist --oauth-user ou_xxx
|
|
115
|
+
```
|
|
116
|
+
|
|
106
117
|
List or delete FaaS functions:
|
|
107
118
|
|
|
108
119
|
```bash
|
|
@@ -120,6 +131,8 @@ magic-builder file list --title logo
|
|
|
120
131
|
magic-builder file delete --id <id>
|
|
121
132
|
```
|
|
122
133
|
|
|
134
|
+
文件上传采用 `sign -> PUT -> audit confirm` 流程。预签名成功后会返回 `audit_id`,CLI 在对象上传完成后自动确认审计;确认失败时命令返回失败,重复执行确认不会新增审计记录。
|
|
135
|
+
|
|
123
136
|
Generate a Magic link:
|
|
124
137
|
|
|
125
138
|
```bash
|
|
@@ -146,6 +159,18 @@ magic-builder performance --review-url <people-review-url> --markdown ./performa
|
|
|
146
159
|
|
|
147
160
|
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
161
|
|
|
162
|
+
Update the text fields in the self-review draft while preserving both rating fields and any omitted text:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
magic-builder perf self-review --review-url <people-review-url> \
|
|
166
|
+
--good "What went well" \
|
|
167
|
+
--improve-file ./improvements.md \
|
|
168
|
+
--values-comment-file ./values-comment.md
|
|
169
|
+
magic-builder perf self-review --review-url <people-review-url> --good-file ./good.md --dry-run
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
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.
|
|
173
|
+
|
|
149
174
|
Formally submit the latest draft to the current review stage:
|
|
150
175
|
|
|
151
176
|
```bash
|
|
@@ -166,8 +191,9 @@ cat ./request.curl | magic-builder extract-cookie --out-dir ./secrets
|
|
|
166
191
|
Create or append a Feishu document HTML Box:
|
|
167
192
|
|
|
168
193
|
```bash
|
|
169
|
-
magic-builder doc create --html app.html --title "Demo"
|
|
194
|
+
magic-builder doc create --html app.html --title "Demo" # defaults to --edition external
|
|
170
195
|
magic-builder doc append --html app.html --doc-token <docx-token>
|
|
196
|
+
magic-builder doc create --html app.html --title "ByteDance Demo" --edition bytedance
|
|
171
197
|
```
|
|
172
198
|
|
|
173
199
|
Publish a document widget draft as a new app version:
|
|
@@ -215,6 +241,10 @@ magic-builder skill check-update
|
|
|
215
241
|
magic-builder skill update
|
|
216
242
|
```
|
|
217
243
|
|
|
244
|
+
Skills are installed to `~/.agents/skills` by default so Codex, Trae, Doubao,
|
|
245
|
+
and other compatible agents can share them. Use `--skills-root <dir>` to
|
|
246
|
+
override the destination.
|
|
247
|
+
|
|
218
248
|
Check local environment:
|
|
219
249
|
|
|
220
250
|
```bash
|
package/package.json
CHANGED
package/src/commands/config.js
CHANGED
|
@@ -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() || '',
|
package/src/commands/doc.js
CHANGED
|
@@ -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
|
|
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:
|
|
106
|
+
component_type_id: componentTypeId,
|
|
91
107
|
record,
|
|
92
108
|
},
|
|
93
109
|
}],
|
|
94
110
|
index: -1,
|
|
95
111
|
}),
|
|
96
112
|
]);
|
|
97
|
-
if (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("'", ''');
|
|
150
173
|
}
|
|
151
174
|
|
|
152
|
-
module.exports = { run };
|
|
175
|
+
module.exports = { HTML_BOX_COMPONENT_TYPE_IDS, resolveEdition, isSuccessfulLarkResponse, run };
|
package/src/commands/faas.js
CHANGED
|
@@ -12,7 +12,8 @@ async function run(args, opts) {
|
|
|
12
12
|
if (sub === 'publish') return publish(args.slice(1), opts);
|
|
13
13
|
if (sub === 'list') return list(opts);
|
|
14
14
|
if (sub === 'delete') return deleteFaas(opts);
|
|
15
|
-
|
|
15
|
+
if (sub === 'mcp') return mcp(args.slice(1), opts);
|
|
16
|
+
fail('Usage: magic-builder faas <publish|list|delete|mcp>', 'E_INVALID_ARGS');
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
async function publish(args, opts) {
|
|
@@ -36,6 +37,15 @@ async function publish(args, opts) {
|
|
|
36
37
|
|
|
37
38
|
const body = { code, name };
|
|
38
39
|
if (opts.id) body.id = opts.id;
|
|
40
|
+
if (opts.mcp) {
|
|
41
|
+
body.mcp = true;
|
|
42
|
+
body.auth = String(opts.auth || 'key').split(',').map((item) => item.trim()).filter(Boolean);
|
|
43
|
+
if (opts.oauthUser !== undefined) {
|
|
44
|
+
body.oauth_users = String(opts.oauthUser || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
45
|
+
}
|
|
46
|
+
if (opts.oauthPolicy !== undefined) body.oauth_policy = String(opts.oauthPolicy).trim().toLowerCase();
|
|
47
|
+
else if (body.oauth_users?.length) body.oauth_policy = 'whitelist';
|
|
48
|
+
}
|
|
39
49
|
|
|
40
50
|
if (!opts.quiet) process.stderr.write(`Deploying FaaS${opts.name ? ` "${opts.name}"` : ''}... `);
|
|
41
51
|
|
|
@@ -53,6 +63,7 @@ async function publish(args, opts) {
|
|
|
53
63
|
const data = res.data || res;
|
|
54
64
|
const id = data.id || data.record_id;
|
|
55
65
|
const faasUrl = data.faas_url || `/api/faas/${id}`;
|
|
66
|
+
const mcpUrl = data.mcp_url || `/api/faas/${id}/mcp`;
|
|
56
67
|
|
|
57
68
|
if (!opts.quiet) process.stderr.write('done\n');
|
|
58
69
|
|
|
@@ -63,9 +74,63 @@ async function publish(args, opts) {
|
|
|
63
74
|
faas_url: `${baseUrl}${faasUrl}`,
|
|
64
75
|
preview_url: `${baseUrl}/r?fid=${id}`,
|
|
65
76
|
wss_url: `${wsBaseUrl}${faasUrl}`,
|
|
77
|
+
...(opts.mcp ? { mcp_url: `${baseUrl}${mcpUrl}`, mcp_auth: data.mcp_auth || body.auth } : {}),
|
|
66
78
|
}, opts);
|
|
67
79
|
}
|
|
68
80
|
|
|
81
|
+
async function mcp(args, opts) {
|
|
82
|
+
const area = args[0];
|
|
83
|
+
const action = args[1];
|
|
84
|
+
if (area === 'key' && action === 'create') return mcpKeyCreate(opts);
|
|
85
|
+
if (area === 'key' && action === 'list') return mcpKeyList(opts);
|
|
86
|
+
if (area === 'key' && action === 'revoke') return mcpKeyRevoke(opts);
|
|
87
|
+
if (area === 'auth' && action === 'get') return mcpAuthGet(opts);
|
|
88
|
+
if (area === 'auth' && action === 'update') return mcpAuthUpdate(opts);
|
|
89
|
+
fail('Usage: magic-builder faas mcp <key create|key list|key revoke|auth get|auth update>', 'E_INVALID_ARGS');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function mcpRequest(opts, path, init = {}) {
|
|
93
|
+
if (!opts.id) fail('Missing --id', 'E_INVALID_ARGS');
|
|
94
|
+
let token;
|
|
95
|
+
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
|
96
|
+
const baseUrl = getBaseUrl(opts);
|
|
97
|
+
try {
|
|
98
|
+
const result = await request(`${baseUrl}/api/faas/${encodeURIComponent(opts.id)}/mcp/${path}`, { token, ...init });
|
|
99
|
+
if (result.code !== 0) fail(result.msg || 'MCP operation failed', 'E_MCP_FAILED');
|
|
100
|
+
return result.data || result;
|
|
101
|
+
} catch (e) { failFromHttpError(e); }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function mcpKeyCreate(opts) {
|
|
105
|
+
success(await mcpRequest(opts, 'keys', { method: 'POST', body: { name: opts.name || 'default' } }), opts);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function mcpKeyList(opts) {
|
|
109
|
+
success(await mcpRequest(opts, 'keys'), opts);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function mcpKeyRevoke(opts) {
|
|
113
|
+
if (!opts.keyId) fail('Missing --key-id', 'E_INVALID_ARGS');
|
|
114
|
+
success(await mcpRequest(opts, `keys?key_id=${encodeURIComponent(opts.keyId)}`, { method: 'DELETE' }), opts);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function mcpAuthGet(opts) {
|
|
118
|
+
success(await mcpRequest(opts, 'auth'), opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function mcpAuthUpdate(opts) {
|
|
122
|
+
const body = {};
|
|
123
|
+
if (opts.enabled !== undefined) body.enabled = String(opts.enabled).toLowerCase() !== 'false';
|
|
124
|
+
if (opts.auth !== undefined) body.auth = String(opts.auth).split(',').map((item) => item.trim()).filter(Boolean);
|
|
125
|
+
if (opts.oauthUser !== undefined) {
|
|
126
|
+
body.oauth_users = String(opts.oauthUser || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
127
|
+
}
|
|
128
|
+
if (opts.oauthPolicy !== undefined) body.oauth_policy = String(opts.oauthPolicy).trim().toLowerCase();
|
|
129
|
+
else if (body.oauth_users?.length) body.oauth_policy = 'whitelist';
|
|
130
|
+
if (!Object.keys(body).length) fail('No MCP auth changes supplied. Use auth get to inspect the current config.', 'E_INVALID_ARGS');
|
|
131
|
+
success(await mcpRequest(opts, 'auth', { method: 'PUT', body }), opts);
|
|
132
|
+
}
|
|
133
|
+
|
|
69
134
|
async function list(opts) {
|
|
70
135
|
let token;
|
|
71
136
|
try { token = await getToken(opts); } catch (e) { fail(e.message, 'E_NO_TOKEN', 2); }
|
package/src/commands/page.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 };
|
package/src/commands/skill.js
CHANGED
|
@@ -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 ||
|
|
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 ~/.
|
|
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
|
-
|
|
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,8 +15,8 @@ SYNTAX:
|
|
|
15
15
|
|
|
16
16
|
COMMANDS:
|
|
17
17
|
auth Login and manage Magic developer tokens
|
|
18
|
-
page Publish
|
|
19
|
-
faas Publish
|
|
18
|
+
page Publish and manage Magic pages, collaborators, and access
|
|
19
|
+
faas Publish and manage Magic FaaS HTTP/WSS/MCP functions
|
|
20
20
|
file Upload, list, and delete TOS files
|
|
21
21
|
link Generate Magic share links
|
|
22
22
|
doc Create or append Feishu Doc HTML Box apps
|
|
@@ -39,7 +39,10 @@ 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
|
|
45
|
+
magic-builder faas publish server.js --name weather-mcp --mcp --auth key,oauth
|
|
43
46
|
magic-builder faas list
|
|
44
47
|
magic-builder faas delete --id recxxx
|
|
45
48
|
magic-builder file upload logo.png
|
|
@@ -85,16 +88,29 @@ COMMANDS:
|
|
|
85
88
|
auth set <token>
|
|
86
89
|
auth show
|
|
87
90
|
|
|
88
|
-
page publish <html> --title <title> [--id <id>] [--open-source]
|
|
91
|
+
page publish <html> --title <title> [--id <id>] [--custom-id <id>] [--open-source] [--comments]
|
|
89
92
|
page list [--title <keyword>] [--scope mine|public]
|
|
90
93
|
page export --id <id> --out <file|dir> [--scope mine|public]
|
|
91
94
|
page export --title <keyword> --out <dir> [--all] [--scope mine|public]
|
|
92
95
|
page delete --id <id>
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
page collaborators list --id <id>
|
|
97
|
+
page collaborators add|remove|set --id <id> --open-ids <ou_a,ou_b>
|
|
98
|
+
page collaborators set --id <id> --file <collaborators.json>
|
|
99
|
+
page access get|enable|disable --id <id>
|
|
100
|
+
page access require-login --id <id> --enabled true|false
|
|
101
|
+
page access users add|remove|set --id <id> --open-ids <ou_a,ou_b>
|
|
102
|
+
page access groups add|remove|set --id <id> --chat-ids <oc_a,oc_b>
|
|
103
|
+
page access set --id <id> --file <access.json>
|
|
104
|
+
|
|
105
|
+
faas publish <file.js> --name <name> [--id <id>] [--mcp --auth key,oauth --oauth-policy all|whitelist --oauth-user ou_xxx]
|
|
95
106
|
faas publish --code <code> --name <name> [--id <id>]
|
|
96
107
|
faas list [--title <keyword>]
|
|
97
108
|
faas delete --id <id>
|
|
109
|
+
faas mcp key create --id <id> --name <name>
|
|
110
|
+
faas mcp key list --id <id>
|
|
111
|
+
faas mcp key revoke --id <id> --key-id <key_id>
|
|
112
|
+
faas mcp auth get --id <id>
|
|
113
|
+
faas mcp auth update --id <id> [--auth key,oauth] [--oauth-policy all|whitelist] [--oauth-user ou_xxx]
|
|
98
114
|
|
|
99
115
|
file upload <file> [--key <key>] [--content-type <mime>]
|
|
100
116
|
file list [--title <keyword>]
|
|
@@ -103,14 +119,20 @@ COMMANDS:
|
|
|
103
119
|
link create --title <text>
|
|
104
120
|
link create --fid <id>
|
|
105
121
|
|
|
106
|
-
doc create --html <file> --title <title> [--summary <text>] [--as bot|user]
|
|
107
|
-
doc append --html <file> --doc-token <token> [--as bot|user]
|
|
122
|
+
doc create --html <file> --title <title> [--summary <text>] [--as bot|user] [--edition external|bytedance]
|
|
123
|
+
doc append --html <file> --doc-token <token> [--as bot|user] [--edition external|bytedance]
|
|
108
124
|
|
|
109
125
|
feedback create --feedback <text> [--title <title>] [--summary <text>]
|
|
110
126
|
feedback create --feedback-file <file> [--dry-run]
|
|
111
127
|
|
|
112
128
|
performance --review-url <url> --cookie <content|file> --markdown <content|file|url> [--dry-run]
|
|
129
|
+
perf self-review --review-url <url> [--good <text>|--good-file <file>]
|
|
130
|
+
[--improve <text>|--improve-file <file>] [--values-comment <text>|--values-comment-file <file>] [--dry-run]
|
|
113
131
|
perf submit --review-url <url> --cookie-file <file> --template-group-id <id> [--dry-run|--yes]
|
|
132
|
+
perf key-works --root-review-id <id> [--template-id <id>] --cookie-file <file>
|
|
133
|
+
perf review-users --review-id <id> --stage-id <id> --cookie-file <file>
|
|
134
|
+
perf review-users --review-url <url> --cookie-file <file>
|
|
135
|
+
perf invite-review --payload-file <file> --review-url <url> --cookie-file <file> [--dry-run]
|
|
114
136
|
perf --review-url <url> --cookie-file <file> --markdown-file <file> [--dry-run]
|
|
115
137
|
|
|
116
138
|
widget-publish --app-id <id> --block-type-id <id> [--version <x.y.z>] [--change-log <text>]
|
|
@@ -161,19 +183,32 @@ SYNTAX:
|
|
|
161
183
|
page: `@HELP magic-builder/page
|
|
162
184
|
|
|
163
185
|
SYNTAX:
|
|
164
|
-
magic-builder page publish <html> --title <title> [--id <id>] [--open-source]
|
|
186
|
+
magic-builder page publish <html> --title <title> [--id <id>] [--custom-id <id>] [--open-source] [--comments]
|
|
165
187
|
magic-builder page list [--title <keyword>] [--scope mine|public]
|
|
166
188
|
magic-builder page export --id <id> --out <file|dir> [--scope mine|public]
|
|
167
189
|
magic-builder page export --title <keyword> --out <dir> [--all] [--scope mine|public]
|
|
168
190
|
magic-builder page delete --id <id>
|
|
191
|
+
magic-builder page collaborators list --id <id>
|
|
192
|
+
magic-builder page collaborators add|remove|set --id <id> --open-ids <ou_a,ou_b>
|
|
193
|
+
magic-builder page collaborators set --id <id> --file <collaborators.json>
|
|
194
|
+
magic-builder page access get|enable|disable --id <id>
|
|
195
|
+
magic-builder page access require-login --id <id> --enabled true|false
|
|
196
|
+
magic-builder page access users add|remove|set --id <id> --open-ids <ou_a,ou_b>
|
|
197
|
+
magic-builder page access groups add|remove|set --id <id> --chat-ids <oc_a,oc_b>
|
|
198
|
+
magic-builder page access set --id <id> --file <access.json>
|
|
169
199
|
`,
|
|
170
200
|
faas: `@HELP magic-builder/faas
|
|
171
201
|
|
|
172
202
|
SYNTAX:
|
|
173
|
-
magic-builder faas publish <file.js> --name <name> [--id <id>]
|
|
203
|
+
magic-builder faas publish <file.js> --name <name> [--id <id>] [--mcp --auth key,oauth --oauth-policy all|whitelist --oauth-user ou_xxx]
|
|
174
204
|
magic-builder faas publish --code <code> --name <name> [--id <id>]
|
|
175
205
|
magic-builder faas list [--title <keyword>]
|
|
176
206
|
magic-builder faas delete --id <id>
|
|
207
|
+
magic-builder faas mcp key create --id <id> --name production
|
|
208
|
+
magic-builder faas mcp key list --id <id>
|
|
209
|
+
magic-builder faas mcp key revoke --id <id> --key-id <key_id>
|
|
210
|
+
magic-builder faas mcp auth get --id <id>
|
|
211
|
+
magic-builder faas mcp auth update --id <id> --oauth-policy whitelist --oauth-user ou_xxx
|
|
177
212
|
`,
|
|
178
213
|
file: `@HELP magic-builder/file
|
|
179
214
|
|
|
@@ -191,8 +226,8 @@ SYNTAX:
|
|
|
191
226
|
doc: `@HELP magic-builder/doc
|
|
192
227
|
|
|
193
228
|
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]
|
|
229
|
+
magic-builder doc create --html <file> --title <title> [--summary <text>] [--as bot|user] [--edition external|bytedance]
|
|
230
|
+
magic-builder doc append --html <file> --doc-token <token> [--as bot|user] [--edition external|bytedance]
|
|
196
231
|
`,
|
|
197
232
|
feedback: `@HELP magic-builder/feedback
|
|
198
233
|
|
|
@@ -204,8 +239,14 @@ SYNTAX:
|
|
|
204
239
|
|
|
205
240
|
SYNTAX:
|
|
206
241
|
magic-builder performance --review-url <url> --cookie <content|file> --markdown <content|file|url>
|
|
242
|
+
magic-builder perf self-review --review-url <url> --good <text> --improve <text> --values-comment <text>
|
|
243
|
+
magic-builder perf self-review --review-url <url> --good-file <file> --improve-file <file> --values-comment-file <file>
|
|
207
244
|
magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --dry-run
|
|
208
245
|
magic-builder perf submit --review-url <url> --cookie-file <file> --template-group-id <id> --yes
|
|
246
|
+
magic-builder perf key-works --root-review-id <id> [--template-id <id>] --cookie-file <file>
|
|
247
|
+
magic-builder perf review-users --review-id <id> --stage-id <id> --cookie-file <file>
|
|
248
|
+
magic-builder perf review-users --review-url <url> --cookie-file <file>
|
|
249
|
+
magic-builder perf invite-review --payload-file <file> --review-url <url> --cookie-file <file> [--dry-run]
|
|
209
250
|
magic-builder perf --review-url <url> --cookie-file <file> --markdown-file <file>
|
|
210
251
|
|
|
211
252
|
COOKIE:
|
|
@@ -215,9 +256,38 @@ DRAFT:
|
|
|
215
256
|
--review-url resolves operator, tenant, form ids, and the current version
|
|
216
257
|
from /perf/api/user/settings and /perf/api/foundation/draft.
|
|
217
258
|
|
|
259
|
+
SELF REVIEW:
|
|
260
|
+
self-review updates only the explicitly passed text fields and preserves
|
|
261
|
+
the current performance and values ratings. It writes by default; use
|
|
262
|
+
--dry-run to inspect the generated payload without saving it.
|
|
263
|
+
|
|
264
|
+
KEY WORKS:
|
|
265
|
+
key-works reads the review-stage data for the specified template and root
|
|
266
|
+
review. The template id defaults to invite_review_template_id in
|
|
267
|
+
~/.magic-builder/performance.json and can be overridden with --template-id.
|
|
268
|
+
It is read-only and prints the complete API response as JSON.
|
|
269
|
+
|
|
270
|
+
REVIEW USERS:
|
|
271
|
+
review-users reads the invited review-user list for a review and stage. It
|
|
272
|
+
accepts either --review-url or explicit --review-id and --stage-id. Explicit
|
|
273
|
+
ids override values parsed from the URL. It does not modify review data and
|
|
274
|
+
prints the complete API response as JSON.
|
|
275
|
+
|
|
276
|
+
INVITE REVIEW:
|
|
277
|
+
invite-review writes a complete invite-review draft payload to People. Pass
|
|
278
|
+
the current payload as JSON or a JSON file. Use --dry-run to inspect it
|
|
279
|
+
without writing. The payload must contain key, data, and version, and its key
|
|
280
|
+
must identify an invite_review draft.
|
|
281
|
+
|
|
218
282
|
OPTIONS:
|
|
219
283
|
--dry-run Print the generated payload without sending it
|
|
220
284
|
--review-url <url> People performance review URL (recommended)
|
|
285
|
+
--good <text> Update the "did well" self-review text
|
|
286
|
+
--good-file <file> Read the "did well" text from a file
|
|
287
|
+
--improve <text> Update the "to improve" self-review text
|
|
288
|
+
--improve-file <file> Read the "to improve" text from a file
|
|
289
|
+
--values-comment <text> Update the values self-review comment
|
|
290
|
+
--values-comment-file <file> Read the values comment from a file
|
|
221
291
|
--template-group-id <id> Template group id (only needed when absent from the draft)
|
|
222
292
|
--submit-endpoint <url> Override the formal submission endpoint
|
|
223
293
|
--yes Confirm the irreversible formal submission
|
|
@@ -230,6 +300,12 @@ OPTIONS:
|
|
|
230
300
|
--source-id <id> Override the form source id
|
|
231
301
|
--field-source-id <id> Override the writable field source id
|
|
232
302
|
--tenant-id <id> Override the People tenant id
|
|
303
|
+
--referer <url> Override the People request Referer
|
|
304
|
+
--stage-endpoint <url> Override the key-works query endpoint
|
|
305
|
+
--stage-id <id> Review stage id used by review-users
|
|
306
|
+
--review-users-endpoint <url> Override the review-users API origin
|
|
307
|
+
--payload <json> Complete invite-review draft payload
|
|
308
|
+
--payload-file <file> Read invite-review draft payload from a file
|
|
233
309
|
--endpoint <url> Override the draft API endpoint
|
|
234
310
|
`,
|
|
235
311
|
perf: `@HELP magic-builder/performance
|
package/src/lib/multipart.js
CHANGED
|
@@ -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,
|