magic-builder 1.2.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 +67 -1
- package/bin/magic-builder.js +1 -0
- package/package.json +5 -2
- package/src/commands/config.js +3 -1
- package/src/commands/doc.js +29 -6
- package/src/commands/extract-cookie.js +33 -2
- package/src/commands/page.js +108 -2
- package/src/commands/performance.js +367 -22
- package/src/commands/skill.js +8 -4
- package/src/commands/widget-publish.js +339 -0
- package/src/index.js +1 -0
- package/src/lib/config.js +28 -1
- package/src/lib/help.js +105 -9
- package/src/lib/multipart.js +3 -0
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ This includes:
|
|
|
72
72
|
```text
|
|
73
73
|
magic-token
|
|
74
74
|
magic-apps.json
|
|
75
|
+
widget-publish.json
|
|
75
76
|
```
|
|
76
77
|
|
|
77
78
|
## Common Commands
|
|
@@ -119,6 +120,8 @@ magic-builder file list --title logo
|
|
|
119
120
|
magic-builder file delete --id <id>
|
|
120
121
|
```
|
|
121
122
|
|
|
123
|
+
文件上传采用 `sign -> PUT -> audit confirm` 流程。预签名成功后会返回 `audit_id`,CLI 在对象上传完成后自动确认审计;确认失败时命令返回失败,重复执行确认不会新增审计记录。
|
|
124
|
+
|
|
122
125
|
Generate a Magic link:
|
|
123
126
|
|
|
124
127
|
```bash
|
|
@@ -145,6 +148,27 @@ magic-builder performance --review-url <people-review-url> --markdown ./performa
|
|
|
145
148
|
|
|
146
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.
|
|
147
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
|
+
|
|
163
|
+
Formally submit the latest draft to the current review stage:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
magic-builder perf submit --review-url <people-review-url> --template-group-id <id> --dry-run
|
|
167
|
+
magic-builder perf submit --review-url <people-review-url> --template-group-id <id> --yes
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The command posts to `/perf/api/review/v2/stage`. It always reloads the latest draft, derives the root review, template, stage, and work units from it, and serializes rich-text values into the stage payload. `--template-group-id` is optional when the draft already contains it. A real submission requires `--yes`; use `--dry-run` to inspect the payload first.
|
|
171
|
+
|
|
148
172
|
Extract Cookie from a copied curl command. This is a top-level general-purpose command. The default output is `./cookie.txt`; the file is created with mode `0600`:
|
|
149
173
|
|
|
150
174
|
```bash
|
|
@@ -156,10 +180,48 @@ cat ./request.curl | magic-builder extract-cookie --out-dir ./secrets
|
|
|
156
180
|
Create or append a Feishu document HTML Box:
|
|
157
181
|
|
|
158
182
|
```bash
|
|
159
|
-
magic-builder doc create --html app.html --title "Demo"
|
|
183
|
+
magic-builder doc create --html app.html --title "Demo" # defaults to --edition external
|
|
160
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
|
|
161
186
|
```
|
|
162
187
|
|
|
188
|
+
Publish a document widget draft as a new app version:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
magic-builder widget-publish \
|
|
192
|
+
--app-id cli_a98afe875979500d \
|
|
193
|
+
--block-type-id blk_6900429af84180025ce76527 \
|
|
194
|
+
--change-log "修复 HTML Box 高度反馈循环"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The command reads Open Platform browser authentication from
|
|
198
|
+
`~/.magic-builder/widget-publish.json` by default:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"cookie": "open_locale=zh-CN; session=...",
|
|
203
|
+
"csrfToken": "...",
|
|
204
|
+
"timezoneOffset": -480
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Keep this file local and set its mode to `0600`. Use `--config <file>` to select
|
|
209
|
+
another file. The command fetches the widget detail, selects the latest uploaded
|
|
210
|
+
test package before the existing draft/current package, updates the widget pkgId,
|
|
211
|
+
increments the latest patch version, creates the app
|
|
212
|
+
version with the application's actual default abilities and audit settings, and
|
|
213
|
+
submits it for publishing. Use `--version` to override the generated
|
|
214
|
+
version or `--dry-run` to inspect the payload without updating or publishing.
|
|
215
|
+
|
|
216
|
+
You can generate the local config directly from a browser-copied curl command:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
magic-builder extract-cookie --curl-file ./widget-request.curl --widget-publish
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This extracts only the Cookie and `x-csrf-token`, writes them to the default
|
|
223
|
+
widget publish config with mode `0600`, and does not print either secret.
|
|
224
|
+
|
|
163
225
|
Install or update the Magic Builder Codex skill:
|
|
164
226
|
|
|
165
227
|
```bash
|
|
@@ -168,6 +230,10 @@ magic-builder skill check-update
|
|
|
168
230
|
magic-builder skill update
|
|
169
231
|
```
|
|
170
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
|
+
|
|
171
237
|
Check local environment:
|
|
172
238
|
|
|
173
239
|
```bash
|
package/bin/magic-builder.js
CHANGED
|
@@ -14,6 +14,7 @@ const COMMANDS = {
|
|
|
14
14
|
feedback: require('../src/commands/feedback'),
|
|
15
15
|
performance: require('../src/commands/performance'),
|
|
16
16
|
perf: require('../src/commands/performance'),
|
|
17
|
+
'widget-publish': require('../src/commands/widget-publish'),
|
|
17
18
|
'extract-cookie': require('../src/commands/extract-cookie'),
|
|
18
19
|
skill: require('../src/commands/skill'),
|
|
19
20
|
version: require('../src/commands/version'),
|
package/package.json
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magic-builder",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for Magic Builder — publish pages, functions, files, and performance
|
|
3
|
+
"version": "1.3.1",
|
|
4
|
+
"description": "CLI for Magic Builder — publish pages, functions, files, and performance reviews",
|
|
5
5
|
"bin": {
|
|
6
6
|
"magic-builder": "bin/magic-builder.js",
|
|
7
7
|
"magic-cli": "bin/magic-builder.js",
|
|
8
8
|
"miaobi": "bin/magic-builder.js"
|
|
9
9
|
},
|
|
10
10
|
"main": "./src/index.js",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --test test/*.test.js"
|
|
13
|
+
},
|
|
11
14
|
"engines": {
|
|
12
15
|
"node": ">=18.0.0"
|
|
13
16
|
},
|
package/src/commands/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl } = require('../lib/config');
|
|
3
|
+
const { CONFIG_DIR, getAppsConfigPath, getBaseUrl, getWidgetPublishConfigPath, getPerformanceConfigPath } = require('../lib/config');
|
|
4
4
|
const { TOKEN_FILE, showToken } = require('../lib/auth');
|
|
5
5
|
const { success, fail } = require('../lib/output');
|
|
6
6
|
|
|
@@ -10,6 +10,8 @@ async function run(args, opts) {
|
|
|
10
10
|
success({
|
|
11
11
|
base_url: getBaseUrl(opts),
|
|
12
12
|
config_dir: CONFIG_DIR,
|
|
13
|
+
widget_publish_config_file: getWidgetPublishConfigPath(),
|
|
14
|
+
performance_config_file: getPerformanceConfigPath(),
|
|
13
15
|
token_file: TOKEN_FILE,
|
|
14
16
|
apps_config_file: getAppsConfigPath(),
|
|
15
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 };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
3
|
+
const { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } = require('fs');
|
|
4
4
|
const { basename, dirname, resolve } = require('path');
|
|
5
|
+
const { getWidgetPublishConfigPath } = require('../lib/config');
|
|
5
6
|
const { success, fail } = require('../lib/output');
|
|
6
7
|
|
|
7
8
|
function run(args, opts) {
|
|
@@ -11,9 +12,24 @@ function run(args, opts) {
|
|
|
11
12
|
const cookie = extractCookieFromCurl(curl);
|
|
12
13
|
if (!cookie) fail('No Cookie found in curl. Expected -b/--cookie or a Cookie request header.', 'E_INVALID_ARGS');
|
|
13
14
|
|
|
15
|
+
if (opts.widgetPublish) {
|
|
16
|
+
const csrfToken = extractHeaderFromCurl(curl, 'x-csrf-token');
|
|
17
|
+
if (!csrfToken) fail('No x-csrf-token header found in curl.', 'E_INVALID_ARGS');
|
|
18
|
+
const output = resolve(String(opts.out || getWidgetPublishConfigPath()));
|
|
19
|
+
mkdirSync(dirname(output), { recursive: true });
|
|
20
|
+
writeFileSync(output, `${JSON.stringify({ cookie, csrfToken, timezoneOffset: -480 }, null, 2)}\n`, {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
mode: 0o600,
|
|
23
|
+
});
|
|
24
|
+
chmodSync(output, 0o600);
|
|
25
|
+
success({ path: output, filename: basename(output), widgetPublish: true }, opts);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
const output = resolveOutput(opts.out, opts.outDir);
|
|
15
30
|
mkdirSync(dirname(output), { recursive: true });
|
|
16
31
|
writeFileSync(output, `${cookie}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
32
|
+
chmodSync(output, 0o600);
|
|
17
33
|
success({ path: output, filename: basename(output), cookieCount: cookie.split(';').filter(Boolean).length }, opts);
|
|
18
34
|
}
|
|
19
35
|
|
|
@@ -43,6 +59,21 @@ function extractCookieFromCurl(curl) {
|
|
|
43
59
|
return '';
|
|
44
60
|
}
|
|
45
61
|
|
|
62
|
+
function extractHeaderFromCurl(curl, headerName) {
|
|
63
|
+
const expected = String(headerName || '').toLowerCase();
|
|
64
|
+
const tokens = tokenizeShell(String(curl || '').replace(/\\\r?\n/g, ' '));
|
|
65
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
66
|
+
let header = '';
|
|
67
|
+
if (tokens[i] === '-H' || tokens[i] === '--header') header = tokens[i + 1] || '';
|
|
68
|
+
else if (tokens[i].startsWith('--header=')) header = tokens[i].slice('--header='.length);
|
|
69
|
+
if (!header) continue;
|
|
70
|
+
const separator = header.indexOf(':');
|
|
71
|
+
if (separator < 0 || header.slice(0, separator).trim().toLowerCase() !== expected) continue;
|
|
72
|
+
return header.slice(separator + 1).trim();
|
|
73
|
+
}
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
|
|
46
77
|
function normalizeCookie(raw) {
|
|
47
78
|
return String(raw || '').replace(/^cookie\s*:\s*/i, '').trim();
|
|
48
79
|
}
|
|
@@ -82,4 +113,4 @@ function resolveOutput(out, outDir) {
|
|
|
82
113
|
return resolve(String(outDir || process.cwd()), 'cookie.txt');
|
|
83
114
|
}
|
|
84
115
|
|
|
85
|
-
module.exports = { run, extractCookieFromCurl, resolveOutput };
|
|
116
|
+
module.exports = { run, extractCookieFromCurl, extractHeaderFromCurl, resolveOutput };
|
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 };
|