magic-builder 1.3.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -1
- package/bin/magic-builder.js +1 -0
- package/package.json +3 -2
- package/src/commands/auth.js +2 -1
- package/src/commands/bot.js +170 -0
- package/src/commands/faas.js +54 -30
- package/src/index.js +1 -0
- package/src/lib/help.js +19 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# magic-builder
|
|
2
2
|
|
|
3
|
-
CLI for Magic Builder (妙笔): publish HTML pages, deploy FaaS functions, upload files, generate links, and install the Magic Builder Codex skill.
|
|
3
|
+
CLI for Magic Builder (妙笔): publish HTML pages, create Feishu app bots, deploy FaaS functions, upload files, generate links, and install the Magic Builder Codex skill.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -103,6 +103,18 @@ Deploy a FaaS function:
|
|
|
103
103
|
magic-builder faas publish handler.js --name report-api
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
Create a Feishu app bot backed by Magic FaaS:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
chmod 600 ./bot.credentials.json
|
|
110
|
+
magic-builder bot create \
|
|
111
|
+
--handler ./bot.js \
|
|
112
|
+
--name my_bot \
|
|
113
|
+
--credentials ./bot.credentials.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Pass `--faas-id <existing-id>` when updating an existing bot so its event callback URL stays unchanged.
|
|
117
|
+
|
|
106
118
|
Publish a Tools-only MCP service (ordinary HTTP `handler` may coexist):
|
|
107
119
|
|
|
108
120
|
```bash
|
package/bin/magic-builder.js
CHANGED
|
@@ -8,6 +8,7 @@ const COMMANDS = {
|
|
|
8
8
|
auth: require('../src/commands/auth'),
|
|
9
9
|
page: require('../src/commands/page'),
|
|
10
10
|
faas: require('../src/commands/faas'),
|
|
11
|
+
bot: require('../src/commands/bot'),
|
|
11
12
|
file: require('../src/commands/file'),
|
|
12
13
|
link: require('../src/commands/link'),
|
|
13
14
|
doc: require('../src/commands/doc'),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magic-builder",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for Magic Builder — publish pages, functions, files, and performance reviews",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "CLI for Magic Builder — publish pages, bots, functions, files, and performance reviews",
|
|
5
5
|
"bin": {
|
|
6
6
|
"magic-builder": "bin/magic-builder.js",
|
|
7
7
|
"magic-cli": "bin/magic-builder.js",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"miaobi",
|
|
27
27
|
"cli",
|
|
28
28
|
"faas",
|
|
29
|
+
"bot",
|
|
29
30
|
"publish"
|
|
30
31
|
],
|
|
31
32
|
"license": "MIT",
|
package/src/commands/auth.js
CHANGED
|
@@ -29,6 +29,7 @@ async function login(opts) {
|
|
|
29
29
|
const requestId = String(data.request_id || '');
|
|
30
30
|
const pollToken = String(data.poll_token || '');
|
|
31
31
|
const authUrl = String(data.auth_url || '');
|
|
32
|
+
const pollUrl = String(data.poll_url || '').trim() || `${baseUrl}/api/dev-token/auth/token`;
|
|
32
33
|
if (!requestId || !pollToken || !authUrl) {
|
|
33
34
|
fail(`Invalid auth start response: ${JSON.stringify(start)}`, 'E_AUTH_FAILED', 2);
|
|
34
35
|
}
|
|
@@ -45,7 +46,7 @@ async function login(opts) {
|
|
|
45
46
|
|
|
46
47
|
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
47
48
|
while (Date.now() < deadline) {
|
|
48
|
-
const tokenJson = await request(
|
|
49
|
+
const tokenJson = await request(pollUrl, {
|
|
49
50
|
method: 'POST',
|
|
50
51
|
body: {
|
|
51
52
|
request_id: requestId,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { getBaseUrl } = require('../lib/config');
|
|
5
|
+
const { request } = require('../lib/http');
|
|
6
|
+
const { formatOutput, success, fail, failFromHttpError } = require('../lib/output');
|
|
7
|
+
const { publishFaas } = require('./faas');
|
|
8
|
+
|
|
9
|
+
const REQUIRED_CREDENTIAL_FIELDS = ['app_id', 'app_secret', 'verification_token', 'app_name'];
|
|
10
|
+
const ALLOWED_CREDENTIAL_FIELDS = [...REQUIRED_CREDENTIAL_FIELDS, 'encrypted_key', 'developer_open_id'];
|
|
11
|
+
|
|
12
|
+
async function run(args, opts) {
|
|
13
|
+
if (args[0] !== 'create') {
|
|
14
|
+
fail('Usage: magic-builder bot create --handler <file.js> --name <name> --credentials <file.json> [--faas-id <id>]', 'E_INVALID_ARGS');
|
|
15
|
+
}
|
|
16
|
+
for (const required of ['handler', 'name', 'credentials']) {
|
|
17
|
+
if (!String(opts[required] || '').trim()) fail(`--${required} is required`, 'E_INVALID_ARGS');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await createMagicBot({
|
|
22
|
+
handler: opts.handler,
|
|
23
|
+
name: opts.name,
|
|
24
|
+
credentials: readCredentials(opts.credentials),
|
|
25
|
+
faasId: opts.faasId,
|
|
26
|
+
baseUrl: opts.baseUrl,
|
|
27
|
+
}, { cliOptions: opts });
|
|
28
|
+
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
process.stdout.write(formatOutput(result, opts.format) + '\n');
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
success(result, opts);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (error.code === 'E_NO_TOKEN') fail(error.message, error.code, 2);
|
|
37
|
+
if (error.code === 'E_PUBLISH_FAILED') fail(error.message, error.code);
|
|
38
|
+
if (error.code) failFromHttpError(error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readCredentials(filePath, { enforcePermissions = true } = {}) {
|
|
44
|
+
if (!fs.existsSync(filePath)) throw new Error(`Credentials file not found: ${filePath}`);
|
|
45
|
+
const stat = fs.statSync(filePath);
|
|
46
|
+
if (!stat.isFile()) throw new Error(`Credentials path is not a file: ${filePath}`);
|
|
47
|
+
if (enforcePermissions && process.platform !== 'win32' && (stat.mode & 0o077) !== 0) {
|
|
48
|
+
throw new Error('credentials file permissions must be 0600 or stricter');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let parsed;
|
|
52
|
+
try {
|
|
53
|
+
parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(`Invalid credentials JSON: ${error.message}`);
|
|
56
|
+
}
|
|
57
|
+
for (const field of REQUIRED_CREDENTIAL_FIELDS) {
|
|
58
|
+
if (!String(parsed?.[field] || '').trim()) throw new Error(`credentials.${field} is required`);
|
|
59
|
+
}
|
|
60
|
+
return Object.fromEntries(ALLOWED_CREDENTIAL_FIELDS.flatMap((field) => (
|
|
61
|
+
parsed[field] === undefined ? [] : [[field, String(parsed[field]).trim()]]
|
|
62
|
+
)));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function validateHandler(filePath) {
|
|
66
|
+
if (!fs.existsSync(filePath)) throw new Error(`Handler file not found: ${filePath}`);
|
|
67
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
68
|
+
if (!source.trim()) throw new Error('Handler file is empty');
|
|
69
|
+
if (!/module\.exports\s*=/.test(source)) throw new Error('handler must use module.exports');
|
|
70
|
+
Function(source);
|
|
71
|
+
return source;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeFaasId(value) {
|
|
75
|
+
return String(value || '').trim().replace(/^rec/, '');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildCallbackUrl(baseUrl, appId, faasId) {
|
|
79
|
+
const url = new URL(`/api/event/${encodeURIComponent(appId)}`, baseUrl);
|
|
80
|
+
url.searchParams.set('faas_id', normalizeFaasId(faasId));
|
|
81
|
+
return url.toString();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function createMagicBot(options, dependencies = {}) {
|
|
85
|
+
const publishFaasImpl = dependencies.publishFaasImpl || publishFaas;
|
|
86
|
+
const requestImpl = dependencies.requestImpl || request;
|
|
87
|
+
const fetchImpl = dependencies.fetchImpl || globalThis.fetch;
|
|
88
|
+
const cliOptions = dependencies.cliOptions || {};
|
|
89
|
+
const baseUrl = String(options.baseUrl || getBaseUrl(cliOptions)).replace(/\/+$/, '');
|
|
90
|
+
const source = validateHandler(options.handler);
|
|
91
|
+
|
|
92
|
+
const published = await publishFaasImpl({
|
|
93
|
+
code: source,
|
|
94
|
+
name: options.name,
|
|
95
|
+
id: options.faasId,
|
|
96
|
+
}, { ...cliOptions, baseUrl });
|
|
97
|
+
const publishedId = published?.id || published?.record_id;
|
|
98
|
+
const faasId = normalizeFaasId(publishedId || options.faasId);
|
|
99
|
+
if (!faasId) throw new Error('FaaS publish response is missing id');
|
|
100
|
+
const faasUrl = published?.faas_url || `${baseUrl}/api/faas/${faasId}`;
|
|
101
|
+
const callbackUrl = buildCallbackUrl(baseUrl, options.credentials.app_id, faasId);
|
|
102
|
+
|
|
103
|
+
let registration;
|
|
104
|
+
try {
|
|
105
|
+
registration = await requestImpl(`${baseUrl}/api/app`, {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
body: options.credentials,
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return partialFailure('app_registration', error.message, faasId, faasUrl, callbackUrl);
|
|
111
|
+
}
|
|
112
|
+
if (registration?.code !== 0) {
|
|
113
|
+
return partialFailure('app_registration', registration?.msg || 'App registration failed', faasId, faasUrl, callbackUrl);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const challengeValue = `magic-bot-${Date.now().toString(36)}`;
|
|
117
|
+
let callbackVerified = false;
|
|
118
|
+
let challengeError = '';
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetchImpl(callbackUrl, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: { 'content-type': 'application/json' },
|
|
123
|
+
body: JSON.stringify({ challenge: challengeValue }),
|
|
124
|
+
});
|
|
125
|
+
const challenge = await response.json().catch(() => ({}));
|
|
126
|
+
callbackVerified = response.ok && challenge?.challenge === challengeValue;
|
|
127
|
+
if (!callbackVerified) challengeError = challenge?.msg || `HTTP ${response.status}`;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
challengeError = error.message || 'Challenge request failed';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const result = {
|
|
133
|
+
ok: callbackVerified,
|
|
134
|
+
stage: callbackVerified ? 'complete' : 'challenge_verification',
|
|
135
|
+
app_id: options.credentials.app_id,
|
|
136
|
+
app_name: options.credentials.app_name,
|
|
137
|
+
faas_id: faasId,
|
|
138
|
+
faas_url: faasUrl,
|
|
139
|
+
event_callback_url: callbackUrl,
|
|
140
|
+
registration_action: registration?.data?.action || 'unknown',
|
|
141
|
+
callback_verified: callbackVerified,
|
|
142
|
+
next_steps: [
|
|
143
|
+
'在飞书开放平台事件订阅中填写 event_callback_url',
|
|
144
|
+
'勾选 handler 实际处理的事件并发布应用版本',
|
|
145
|
+
'把机器人加入目标会话并发送真实消息验收',
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
if (!callbackVerified) result.error = challengeError || 'Challenge verification failed';
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function partialFailure(stage, error, faasId, faasUrl, callbackUrl) {
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
stage,
|
|
156
|
+
faas_id: faasId,
|
|
157
|
+
faas_url: faasUrl,
|
|
158
|
+
event_callback_url: callbackUrl,
|
|
159
|
+
error: String(error || 'Unknown error'),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = {
|
|
164
|
+
run,
|
|
165
|
+
buildCallbackUrl,
|
|
166
|
+
createMagicBot,
|
|
167
|
+
normalizeFaasId,
|
|
168
|
+
readCredentials,
|
|
169
|
+
validateHandler,
|
|
170
|
+
};
|
package/src/commands/faas.js
CHANGED
|
@@ -29,53 +29,77 @@ async function publish(args, opts) {
|
|
|
29
29
|
if (!code.trim()) fail('JS file is empty', 'E_EMPTY_FILE');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const name = opts.name || generateName(args[0]);
|
|
33
|
+
|
|
34
|
+
if (!opts.quiet) process.stderr.write(`Deploying FaaS${opts.name ? ` "${opts.name}"` : ''}... `);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await publishFaas({
|
|
38
|
+
code,
|
|
39
|
+
name,
|
|
40
|
+
id: opts.id,
|
|
41
|
+
mcp: opts.mcp,
|
|
42
|
+
auth: opts.auth,
|
|
43
|
+
oauthUser: opts.oauthUser,
|
|
44
|
+
oauthPolicy: opts.oauthPolicy,
|
|
45
|
+
}, opts);
|
|
46
|
+
|
|
47
|
+
if (!opts.quiet) process.stderr.write('done\n');
|
|
48
|
+
success(result, opts);
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if (e.code === 'E_NO_TOKEN') fail(e.message, e.code, 2);
|
|
51
|
+
if (e.code === 'E_PUBLISH_FAILED') fail(e.message, e.code);
|
|
52
|
+
failFromHttpError(e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function publishFaas(input, opts = {}, dependencies = {}) {
|
|
57
|
+
const getTokenImpl = dependencies.getTokenImpl || getToken;
|
|
58
|
+
const requestImpl = dependencies.requestImpl || request;
|
|
32
59
|
let token;
|
|
33
|
-
try {
|
|
60
|
+
try {
|
|
61
|
+
token = await getTokenImpl(opts);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
error.code = error.code || 'E_NO_TOKEN';
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
34
66
|
|
|
35
67
|
const baseUrl = getBaseUrl(opts);
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (opts.id) body.id = opts.id;
|
|
40
|
-
if (opts.mcp) {
|
|
68
|
+
const body = { code: input.code, name: input.name };
|
|
69
|
+
if (input.id) body.id = input.id;
|
|
70
|
+
if (input.mcp) {
|
|
41
71
|
body.mcp = true;
|
|
42
|
-
body.auth = String(
|
|
43
|
-
if (
|
|
44
|
-
body.oauth_users = String(
|
|
72
|
+
body.auth = String(input.auth || 'key').split(',').map((item) => item.trim()).filter(Boolean);
|
|
73
|
+
if (input.oauthUser !== undefined) {
|
|
74
|
+
body.oauth_users = String(input.oauthUser || '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
45
75
|
}
|
|
46
|
-
if (
|
|
76
|
+
if (input.oauthPolicy !== undefined) body.oauth_policy = String(input.oauthPolicy).trim().toLowerCase();
|
|
47
77
|
else if (body.oauth_users?.length) body.oauth_policy = 'whitelist';
|
|
48
78
|
}
|
|
49
79
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (res.code !== 0) fail(res.msg || 'FaaS publish failed', 'E_PUBLISH_FAILED');
|
|
80
|
+
const res = await requestImpl(`${baseUrl}/api/faas`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
token,
|
|
83
|
+
body,
|
|
84
|
+
});
|
|
85
|
+
if (res.code !== 0) {
|
|
86
|
+
const error = new Error(res.msg || 'FaaS publish failed');
|
|
87
|
+
error.code = 'E_PUBLISH_FAILED';
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
62
90
|
|
|
63
91
|
const data = res.data || res;
|
|
64
92
|
const id = data.id || data.record_id;
|
|
65
93
|
const faasUrl = data.faas_url || `/api/faas/${id}`;
|
|
66
94
|
const mcpUrl = data.mcp_url || `/api/faas/${id}/mcp`;
|
|
67
|
-
|
|
68
|
-
if (!opts.quiet) process.stderr.write('done\n');
|
|
69
|
-
|
|
70
95
|
const wsBaseUrl = baseUrl.replace(/^https?/, 'wss');
|
|
71
|
-
|
|
72
|
-
success({
|
|
96
|
+
return {
|
|
73
97
|
id: data.record_id || id,
|
|
74
98
|
faas_url: `${baseUrl}${faasUrl}`,
|
|
75
99
|
preview_url: `${baseUrl}/r?fid=${id}`,
|
|
76
100
|
wss_url: `${wsBaseUrl}${faasUrl}`,
|
|
77
|
-
...(
|
|
78
|
-
}
|
|
101
|
+
...(input.mcp ? { mcp_url: `${baseUrl}${mcpUrl}`, mcp_auth: data.mcp_auth || body.auth } : {}),
|
|
102
|
+
};
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
async function mcp(args, opts) {
|
|
@@ -179,4 +203,4 @@ function generateName(filePath) {
|
|
|
179
203
|
return base.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 50);
|
|
180
204
|
}
|
|
181
205
|
|
|
182
|
-
module.exports = { run };
|
|
206
|
+
module.exports = { run, publishFaas };
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,7 @@ module.exports = {
|
|
|
9
9
|
auth: require('./commands/auth'),
|
|
10
10
|
page: require('./commands/page'),
|
|
11
11
|
faas: require('./commands/faas'),
|
|
12
|
+
bot: require('./commands/bot'),
|
|
12
13
|
file: require('./commands/file'),
|
|
13
14
|
link: require('./commands/link'),
|
|
14
15
|
doc: require('./commands/doc'),
|
package/src/lib/help.js
CHANGED
|
@@ -17,6 +17,7 @@ COMMANDS:
|
|
|
17
17
|
auth Login and manage Magic developer tokens
|
|
18
18
|
page Publish and manage Magic pages, collaborators, and access
|
|
19
19
|
faas Publish and manage Magic FaaS HTTP/WSS/MCP functions
|
|
20
|
+
bot Create or update a Feishu app bot backed by Magic FaaS
|
|
20
21
|
file Upload, list, and delete TOS files
|
|
21
22
|
link Generate Magic share links
|
|
22
23
|
doc Create or append Feishu Doc HTML Box apps
|
|
@@ -45,6 +46,7 @@ EXAMPLES:
|
|
|
45
46
|
magic-builder faas publish server.js --name weather-mcp --mcp --auth key,oauth
|
|
46
47
|
magic-builder faas list
|
|
47
48
|
magic-builder faas delete --id recxxx
|
|
49
|
+
magic-builder bot create --handler bot.js --name echo-bot --credentials ./bot.credentials.json
|
|
48
50
|
magic-builder file upload logo.png
|
|
49
51
|
magic-builder file list
|
|
50
52
|
magic-builder file delete --id recxxx
|
|
@@ -112,6 +114,8 @@ COMMANDS:
|
|
|
112
114
|
faas mcp auth get --id <id>
|
|
113
115
|
faas mcp auth update --id <id> [--auth key,oauth] [--oauth-policy all|whitelist] [--oauth-user ou_xxx]
|
|
114
116
|
|
|
117
|
+
bot create --handler <file.js> --name <name> --credentials <file.json> [--faas-id <id>]
|
|
118
|
+
|
|
115
119
|
file upload <file> [--key <key>] [--content-type <mime>]
|
|
116
120
|
file list [--title <keyword>]
|
|
117
121
|
file delete --id <id>
|
|
@@ -209,6 +213,21 @@ SYNTAX:
|
|
|
209
213
|
magic-builder faas mcp key revoke --id <id> --key-id <key_id>
|
|
210
214
|
magic-builder faas mcp auth get --id <id>
|
|
211
215
|
magic-builder faas mcp auth update --id <id> --oauth-policy whitelist --oauth-user ou_xxx
|
|
216
|
+
`,
|
|
217
|
+
bot: `@HELP magic-builder/bot
|
|
218
|
+
|
|
219
|
+
BRIEF: Create or update a Feishu app bot backed by Magic FaaS.
|
|
220
|
+
|
|
221
|
+
SYNTAX:
|
|
222
|
+
magic-builder bot create --handler <file.js> --name <name> --credentials <file.json> [--faas-id <id>]
|
|
223
|
+
|
|
224
|
+
CREDENTIALS:
|
|
225
|
+
The JSON file must contain app_id, app_secret, verification_token, and app_name.
|
|
226
|
+
encrypted_key and developer_open_id are optional. On POSIX systems the file
|
|
227
|
+
permissions must be 0600 or stricter.
|
|
228
|
+
|
|
229
|
+
UPDATE:
|
|
230
|
+
Pass the existing --faas-id to update a bot without changing its callback URL.
|
|
212
231
|
`,
|
|
213
232
|
file: `@HELP magic-builder/file
|
|
214
233
|
|