apifm-admin-mcp 26.5.2 → 26.5.4
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 +8 -6
- package/package.json +1 -1
- package/src/browser-auth.js +823 -127
- package/src/index.js +22 -9
- package/src/self-check.js +44 -4
package/README.md
CHANGED
|
@@ -8,7 +8,9 @@ It is designed for Kiro, Cursor, Claude Code, Codex, Windsurf, Trae, Qoder, and
|
|
|
8
8
|
|
|
9
9
|
Secrets must not be pasted into model chat.
|
|
10
10
|
|
|
11
|
-
This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`,
|
|
11
|
+
This MCP exposes `apifm_admin_start_auth`, which returns a one-time localhost URL. The user enters Basic Authentication credentials, an existing `X-Token`, username/password login details, email/password login details, email signup details, or mobile signup details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
|
|
12
|
+
|
|
13
|
+
If an API call is attempted before authorization, `apifm_admin_call` and `apifm_admin_find_and_call` return the authorization URL directly in both visible text and structured content as `authUrl`. The agent should show that URL immediately instead of asking the user to call another tool.
|
|
12
14
|
|
|
13
15
|
The API callers reject payloads and headers containing sensitive field names such as `pwd`, `password`, `token`, `x-token`, `authorization`, `secret`, or `key`.
|
|
14
16
|
|
|
@@ -51,7 +53,7 @@ If installed globally:
|
|
|
51
53
|
|
|
52
54
|
## Tools
|
|
53
55
|
|
|
54
|
-
- `apifm_admin_start_auth`: Starts a local secure authorization page
|
|
56
|
+
- `apifm_admin_start_auth`: Starts a local secure authorization page. The page separates "Log in" from "Register", supports username login, email login, Basic Auth, X-Token, email registration, and mobile registration. Basic Auth fields are merchant number and merchant key. The page supports Simplified Chinese, Traditional Chinese, and English.
|
|
55
57
|
- `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
|
|
56
58
|
- `apifm_admin_switch_account`: Switches the active account alias.
|
|
57
59
|
- `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
|
|
@@ -65,10 +67,10 @@ For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_
|
|
|
65
67
|
## Example Agent Flow
|
|
66
68
|
|
|
67
69
|
1. User: "Log in to my admin account and read the user list."
|
|
68
|
-
2. Agent calls `
|
|
69
|
-
3.
|
|
70
|
-
4.
|
|
71
|
-
5. Agent answers from the returned `apiResult`, not from method documentation.
|
|
70
|
+
2. Agent calls `apifm_admin_find_and_call` with `query: "user list"` and a non-sensitive payload such as `{ "page": 1, "pageSize": 20 }`.
|
|
71
|
+
3. If not authorized yet, the tool returns `authUrl` immediately. The user opens that URL and chooses a login method or a registration method.
|
|
72
|
+
4. User completes authorization in the local browser page.
|
|
73
|
+
5. Agent retries the same API call and answers from the returned `apiResult`, not from method documentation.
|
|
72
74
|
|
|
73
75
|
## Local Check
|
|
74
76
|
|
package/package.json
CHANGED
package/src/browser-auth.js
CHANGED
|
@@ -6,7 +6,246 @@ import { getSdk, resetSdkConfig } from './sdk.js'
|
|
|
6
6
|
|
|
7
7
|
const pendingSessions = new Map()
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const AUTH_TYPES = new Set([
|
|
10
|
+
'all',
|
|
11
|
+
'basic',
|
|
12
|
+
'x-token',
|
|
13
|
+
'username-password',
|
|
14
|
+
'email-password',
|
|
15
|
+
'register-email',
|
|
16
|
+
'register-mobile'
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
const OPTIONAL_FIELDS = new Set([
|
|
20
|
+
'alias',
|
|
21
|
+
'pdomain',
|
|
22
|
+
'imgcode',
|
|
23
|
+
'k',
|
|
24
|
+
'registerName',
|
|
25
|
+
'registerType',
|
|
26
|
+
'referrer',
|
|
27
|
+
'agentKey',
|
|
28
|
+
'mailCode',
|
|
29
|
+
'smsCode',
|
|
30
|
+
'smsImgCode',
|
|
31
|
+
'smsK'
|
|
32
|
+
])
|
|
33
|
+
|
|
34
|
+
const I18N = {
|
|
35
|
+
'zh-CN': {
|
|
36
|
+
brand: 'APIFM Admin MCP',
|
|
37
|
+
title: '安全连接 APIFM 后台',
|
|
38
|
+
lead: '在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。',
|
|
39
|
+
trustLocal: '本地 127.0.0.1 页面提交',
|
|
40
|
+
trustMemory: '凭证仅在内存中保存',
|
|
41
|
+
trustSwitch: '支持多个后台账号切换',
|
|
42
|
+
footer: '授权完成后回到 Agent,重新执行刚才的后台操作。',
|
|
43
|
+
formTitle: '授权方式',
|
|
44
|
+
expires: '页面过期时间',
|
|
45
|
+
alias: '账号别名',
|
|
46
|
+
aliasPh: 'default-admin',
|
|
47
|
+
tabLogin: '登录现有账号',
|
|
48
|
+
tabRegister: '注册新账号',
|
|
49
|
+
methodUsername: '用户名登录',
|
|
50
|
+
methodUsernameSub: '用户名 + 密码',
|
|
51
|
+
methodEmail: '邮箱登录',
|
|
52
|
+
methodEmailSub: '邮箱 + 密码',
|
|
53
|
+
methodBasic: 'Basic Auth',
|
|
54
|
+
methodBasicSub: '商户号 + 商户秘钥',
|
|
55
|
+
methodToken: 'X-Token',
|
|
56
|
+
methodTokenSub: '直接使用令牌',
|
|
57
|
+
methodRegisterEmail: '邮箱注册',
|
|
58
|
+
methodRegisterEmailSub: '邮箱验证码',
|
|
59
|
+
methodRegisterMobile: '手机号注册',
|
|
60
|
+
methodRegisterMobileSub: '短信验证码',
|
|
61
|
+
usernameHint: '使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。',
|
|
62
|
+
emailHint: '使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。',
|
|
63
|
+
basicHint: '填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。',
|
|
64
|
+
tokenHint: '直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。',
|
|
65
|
+
registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
|
|
66
|
+
registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试自动登录。',
|
|
67
|
+
username: '用户名',
|
|
68
|
+
usernamePh: '请输入后台用户名',
|
|
69
|
+
password: '密码',
|
|
70
|
+
passwordPh: '请输入登录密码',
|
|
71
|
+
pdomain: '专属域名,可选',
|
|
72
|
+
pdomainPh: '例如 yourdomain',
|
|
73
|
+
imgcode: '图形验证码,可选',
|
|
74
|
+
imgcodePh: '请输入图形验证码',
|
|
75
|
+
k: '验证码随机数,可选',
|
|
76
|
+
kPh: '请输入验证码随机数',
|
|
77
|
+
email: '邮箱地址',
|
|
78
|
+
emailPh: 'name@example.com',
|
|
79
|
+
mobile: '手机号码',
|
|
80
|
+
mobilePh: '请输入手机号码',
|
|
81
|
+
merchantNo: '商户号',
|
|
82
|
+
merchantNoPh: '请输入商户号',
|
|
83
|
+
merchantKey: '商户秘钥',
|
|
84
|
+
merchantKeyPh: '请输入商户秘钥',
|
|
85
|
+
xToken: 'X-Token',
|
|
86
|
+
xTokenPh: '粘贴管理员 X-Token',
|
|
87
|
+
registerPassword: '登录密码',
|
|
88
|
+
registerPasswordPh: '设置登录密码',
|
|
89
|
+
name: '姓名或昵称',
|
|
90
|
+
namePh: '可选',
|
|
91
|
+
mailCode: '邮箱验证码',
|
|
92
|
+
mailCodePh: '请输入邮箱验证码',
|
|
93
|
+
smsCode: '短信验证码',
|
|
94
|
+
smsCodePh: '请输入短信验证码',
|
|
95
|
+
type: '注册类型,可选',
|
|
96
|
+
typePh: '不填默认为 apifm',
|
|
97
|
+
referrer: '推荐人用户ID,可选',
|
|
98
|
+
referrerPh: '请输入推荐人用户ID',
|
|
99
|
+
agentKey: 'Agent Key,可选',
|
|
100
|
+
agentKeyPh: '请输入 agentKey',
|
|
101
|
+
submit: '完成授权',
|
|
102
|
+
security: '请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。',
|
|
103
|
+
closeNote: '提交成功后可以关闭本页面。'
|
|
104
|
+
},
|
|
105
|
+
'zh-TW': {
|
|
106
|
+
brand: 'APIFM Admin MCP',
|
|
107
|
+
title: '安全連接 APIFM 後台',
|
|
108
|
+
lead: '在本機頁面完成授權,密鑰只保存在目前 MCP 程序記憶體,不會進入聊天上下文。',
|
|
109
|
+
trustLocal: '本地 127.0.0.1 頁面提交',
|
|
110
|
+
trustMemory: '憑證僅在記憶體中保存',
|
|
111
|
+
trustSwitch: '支援多個後台帳號切換',
|
|
112
|
+
footer: '授權完成後回到 Agent,重新執行剛才的後台操作。',
|
|
113
|
+
formTitle: '授權方式',
|
|
114
|
+
expires: '頁面過期時間',
|
|
115
|
+
alias: '帳號別名',
|
|
116
|
+
aliasPh: 'default-admin',
|
|
117
|
+
tabLogin: '登入現有帳號',
|
|
118
|
+
tabRegister: '註冊新帳號',
|
|
119
|
+
methodUsername: '使用者名稱登入',
|
|
120
|
+
methodUsernameSub: '使用者名稱 + 密碼',
|
|
121
|
+
methodEmail: '郵箱登入',
|
|
122
|
+
methodEmailSub: '郵箱 + 密碼',
|
|
123
|
+
methodBasic: 'Basic Auth',
|
|
124
|
+
methodBasicSub: '商戶號 + 商戶秘鑰',
|
|
125
|
+
methodToken: 'X-Token',
|
|
126
|
+
methodTokenSub: '直接使用令牌',
|
|
127
|
+
methodRegisterEmail: '郵箱註冊',
|
|
128
|
+
methodRegisterEmailSub: '郵箱驗證碼',
|
|
129
|
+
methodRegisterMobile: '手機號註冊',
|
|
130
|
+
methodRegisterMobileSub: '簡訊驗證碼',
|
|
131
|
+
usernameHint: '使用後台使用者名稱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
|
|
132
|
+
emailHint: '使用後台郵箱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
|
|
133
|
+
basicHint: '填寫 Basic Authentication 資訊,將作為 Authorization 請求頭呼叫後台介面。',
|
|
134
|
+
tokenHint: '直接填寫管理員登入後的 X-Token,用於後續所有後台介面呼叫。',
|
|
135
|
+
registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
|
|
136
|
+
registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試自動登入。',
|
|
137
|
+
username: '使用者名稱',
|
|
138
|
+
usernamePh: '請輸入後台使用者名稱',
|
|
139
|
+
password: '密碼',
|
|
140
|
+
passwordPh: '請輸入登入密碼',
|
|
141
|
+
pdomain: '專屬網域,可選',
|
|
142
|
+
pdomainPh: '例如 yourdomain',
|
|
143
|
+
imgcode: '圖形驗證碼,可選',
|
|
144
|
+
imgcodePh: '請輸入圖形驗證碼',
|
|
145
|
+
k: '驗證碼隨機數,可選',
|
|
146
|
+
kPh: '請輸入驗證碼隨機數',
|
|
147
|
+
email: '郵箱地址',
|
|
148
|
+
emailPh: 'name@example.com',
|
|
149
|
+
mobile: '手機號碼',
|
|
150
|
+
mobilePh: '請輸入手機號碼',
|
|
151
|
+
merchantNo: '商戶號',
|
|
152
|
+
merchantNoPh: '請輸入商戶號',
|
|
153
|
+
merchantKey: '商戶秘鑰',
|
|
154
|
+
merchantKeyPh: '請輸入商戶秘鑰',
|
|
155
|
+
xToken: 'X-Token',
|
|
156
|
+
xTokenPh: '貼上管理員 X-Token',
|
|
157
|
+
registerPassword: '登入密碼',
|
|
158
|
+
registerPasswordPh: '設定登入密碼',
|
|
159
|
+
name: '姓名或暱稱',
|
|
160
|
+
namePh: '可選',
|
|
161
|
+
mailCode: '郵箱驗證碼',
|
|
162
|
+
mailCodePh: '請輸入郵箱驗證碼',
|
|
163
|
+
smsCode: '簡訊驗證碼',
|
|
164
|
+
smsCodePh: '請輸入簡訊驗證碼',
|
|
165
|
+
type: '註冊類型,可選',
|
|
166
|
+
typePh: '不填預設為 apifm',
|
|
167
|
+
referrer: '推薦人使用者ID,可選',
|
|
168
|
+
referrerPh: '請輸入推薦人使用者ID',
|
|
169
|
+
agentKey: 'Agent Key,可選',
|
|
170
|
+
agentKeyPh: '請輸入 agentKey',
|
|
171
|
+
submit: '完成授權',
|
|
172
|
+
security: '請不要把密碼、Token 或 Basic Authentication 內容貼到聊天視窗。',
|
|
173
|
+
closeNote: '提交成功後可以關閉本頁面。'
|
|
174
|
+
},
|
|
175
|
+
en: {
|
|
176
|
+
brand: 'APIFM Admin MCP',
|
|
177
|
+
title: 'Connect APIFM admin safely',
|
|
178
|
+
lead: 'Authorize on this local page. Secrets stay in this MCP process memory and never enter the chat transcript.',
|
|
179
|
+
trustLocal: 'Submitted to local 127.0.0.1 only',
|
|
180
|
+
trustMemory: 'Credentials are memory-only',
|
|
181
|
+
trustSwitch: 'Multiple admin accounts supported',
|
|
182
|
+
footer: 'After authorization, return to the agent and run the backend action again.',
|
|
183
|
+
formTitle: 'Authorization method',
|
|
184
|
+
expires: 'Page expires at',
|
|
185
|
+
alias: 'Account alias',
|
|
186
|
+
aliasPh: 'default-admin',
|
|
187
|
+
tabLogin: 'Log in',
|
|
188
|
+
tabRegister: 'Register',
|
|
189
|
+
methodUsername: 'Username login',
|
|
190
|
+
methodUsernameSub: 'Username + password',
|
|
191
|
+
methodEmail: 'Email login',
|
|
192
|
+
methodEmailSub: 'Email + password',
|
|
193
|
+
methodBasic: 'Basic Auth',
|
|
194
|
+
methodBasicSub: 'Merchant no. + key',
|
|
195
|
+
methodToken: 'X-Token',
|
|
196
|
+
methodTokenSub: 'Use an existing token',
|
|
197
|
+
methodRegisterEmail: 'Email signup',
|
|
198
|
+
methodRegisterEmailSub: 'Email code',
|
|
199
|
+
methodRegisterMobile: 'Mobile signup',
|
|
200
|
+
methodRegisterMobileSub: 'SMS code',
|
|
201
|
+
usernameHint: 'Log in with an admin username and password. The MCP will exchange them for an X-Token through the SDK.',
|
|
202
|
+
emailHint: 'Log in with an admin email and password. The MCP will exchange them for an X-Token through the SDK.',
|
|
203
|
+
basicHint: 'Enter Basic Authentication credentials. They will be sent as the Authorization header for API calls.',
|
|
204
|
+
tokenHint: 'Enter an existing admin X-Token for later backend API calls.',
|
|
205
|
+
registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try to log in automatically.',
|
|
206
|
+
registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try to log in automatically.',
|
|
207
|
+
username: 'Username',
|
|
208
|
+
usernamePh: 'Enter admin username',
|
|
209
|
+
password: 'Password',
|
|
210
|
+
passwordPh: 'Enter login password',
|
|
211
|
+
pdomain: 'Private domain, optional',
|
|
212
|
+
pdomainPh: 'Example: yourdomain',
|
|
213
|
+
imgcode: 'Image code, optional',
|
|
214
|
+
imgcodePh: 'Enter image code',
|
|
215
|
+
k: 'Captcha nonce, optional',
|
|
216
|
+
kPh: 'Enter captcha nonce',
|
|
217
|
+
email: 'Email address',
|
|
218
|
+
emailPh: 'name@example.com',
|
|
219
|
+
mobile: 'Mobile number',
|
|
220
|
+
mobilePh: 'Enter mobile number',
|
|
221
|
+
merchantNo: 'Merchant number',
|
|
222
|
+
merchantNoPh: 'Enter merchant number',
|
|
223
|
+
merchantKey: 'Merchant key',
|
|
224
|
+
merchantKeyPh: 'Enter merchant key',
|
|
225
|
+
xToken: 'X-Token',
|
|
226
|
+
xTokenPh: 'Paste admin X-Token',
|
|
227
|
+
registerPassword: 'Login password',
|
|
228
|
+
registerPasswordPh: 'Set login password',
|
|
229
|
+
name: 'Name or nickname',
|
|
230
|
+
namePh: 'Optional',
|
|
231
|
+
mailCode: 'Email verification code',
|
|
232
|
+
mailCodePh: 'Enter email code',
|
|
233
|
+
smsCode: 'SMS verification code',
|
|
234
|
+
smsCodePh: 'Enter SMS code',
|
|
235
|
+
type: 'Signup type, optional',
|
|
236
|
+
typePh: 'Defaults to apifm',
|
|
237
|
+
referrer: 'Referrer user ID, optional',
|
|
238
|
+
referrerPh: 'Enter referrer user ID',
|
|
239
|
+
agentKey: 'Agent Key, optional',
|
|
240
|
+
agentKeyPh: 'Enter agentKey',
|
|
241
|
+
submit: 'Authorize account',
|
|
242
|
+
security: 'Do not paste passwords, tokens, or Basic Authentication values into the chat window.',
|
|
243
|
+
closeNote: 'You can close this page after authorization succeeds.'
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function createAuthSession({ authType = 'all', alias = '', domains = {}, port = 0 }) {
|
|
248
|
+
const initialAuthType = normalizeAuthType(authType)
|
|
10
249
|
const sessionId = crypto.randomUUID()
|
|
11
250
|
const createdAt = Date.now()
|
|
12
251
|
const expiresAt = createdAt + 10 * 60 * 1000
|
|
@@ -21,7 +260,7 @@ export async function createAuthSession({ authType, alias = '', domains = {}, po
|
|
|
21
260
|
}
|
|
22
261
|
|
|
23
262
|
if (req.method === 'GET') {
|
|
24
|
-
sendHtml(res, renderForm({ authType, alias,
|
|
263
|
+
sendHtml(res, renderForm({ authType: initialAuthType, alias, expiresAt }))
|
|
25
264
|
return
|
|
26
265
|
}
|
|
27
266
|
|
|
@@ -31,7 +270,7 @@ export async function createAuthSession({ authType, alias = '', domains = {}, po
|
|
|
31
270
|
}
|
|
32
271
|
|
|
33
272
|
const fields = await readForm(req)
|
|
34
|
-
const account = await finishAuth({ authType, alias, domains, fields })
|
|
273
|
+
const account = await finishAuth({ authType: fields.authType || initialAuthType, alias, domains, fields })
|
|
35
274
|
pendingSessions.delete(sessionId)
|
|
36
275
|
sendHtml(res, renderSuccess(account))
|
|
37
276
|
setTimeout(() => server.close(), 250)
|
|
@@ -46,7 +285,7 @@ export async function createAuthSession({ authType, alias = '', domains = {}, po
|
|
|
46
285
|
|
|
47
286
|
const address = server.address()
|
|
48
287
|
const url = `http://127.0.0.1:${address.port}/${sessionId}`
|
|
49
|
-
pendingSessions.set(sessionId, { server, authType, alias, createdAt, expiresAt })
|
|
288
|
+
pendingSessions.set(sessionId, { server, authType: initialAuthType, alias, createdAt, expiresAt })
|
|
50
289
|
setTimeout(() => {
|
|
51
290
|
const pending = pendingSessions.get(sessionId)
|
|
52
291
|
if (pending) {
|
|
@@ -58,6 +297,7 @@ export async function createAuthSession({ authType, alias = '', domains = {}, po
|
|
|
58
297
|
return {
|
|
59
298
|
url,
|
|
60
299
|
sessionId,
|
|
300
|
+
authType: initialAuthType,
|
|
61
301
|
expiresAt: new Date(expiresAt).toISOString(),
|
|
62
302
|
message:
|
|
63
303
|
'Open this local URL in a browser and enter credentials there. Do not paste secrets into the chat.'
|
|
@@ -65,80 +305,77 @@ export async function createAuthSession({ authType, alias = '', domains = {}, po
|
|
|
65
305
|
}
|
|
66
306
|
|
|
67
307
|
async function finishAuth({ authType, alias, domains, fields }) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
308
|
+
const selectedAuthType = normalizeAuthType(authType)
|
|
309
|
+
|
|
310
|
+
if (selectedAuthType === 'basic') {
|
|
311
|
+
const merchantNo = required(fields.basicUsername, 'merchant number')
|
|
312
|
+
const merchantKey = required(fields.basicPassword, 'merchant key')
|
|
71
313
|
return upsertAccount({
|
|
72
314
|
alias: fields.alias || alias,
|
|
73
|
-
authType,
|
|
74
|
-
basicAuth: `Basic ${Buffer.from(`${
|
|
315
|
+
authType: 'basic',
|
|
316
|
+
basicAuth: `Basic ${Buffer.from(`${merchantNo}:${merchantKey}`).toString('base64')}`,
|
|
75
317
|
domains
|
|
76
318
|
})
|
|
77
319
|
}
|
|
78
320
|
|
|
79
|
-
if (
|
|
321
|
+
if (selectedAuthType === 'x-token') {
|
|
80
322
|
return upsertAccount({
|
|
81
323
|
alias: fields.alias || alias,
|
|
82
|
-
authType,
|
|
83
|
-
token: required(fields.
|
|
324
|
+
authType: selectedAuthType,
|
|
325
|
+
token: required(fields.xToken, 'X-Token'),
|
|
84
326
|
domains
|
|
85
327
|
})
|
|
86
328
|
}
|
|
87
329
|
|
|
88
|
-
if (
|
|
89
|
-
const
|
|
90
|
-
const loginPayload =
|
|
91
|
-
loginMode === 'email'
|
|
92
|
-
? {
|
|
93
|
-
email: required(fields.email, 'email'),
|
|
94
|
-
pwd: required(fields.password, 'password'),
|
|
95
|
-
rememberMe: true
|
|
96
|
-
}
|
|
97
|
-
: {
|
|
98
|
-
userName: required(fields.username, 'username'),
|
|
99
|
-
pwd: required(fields.password, 'password'),
|
|
100
|
-
rememberMe: true,
|
|
101
|
-
pdomain: fields.pdomain || undefined
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const sdk = getSdk()
|
|
105
|
-
resetSdkConfig()
|
|
106
|
-
if (domains && Object.keys(domains).length) {
|
|
107
|
-
sdk.setDomains(domains)
|
|
108
|
-
}
|
|
109
|
-
const methodName = loginMode === 'email' ? 'loginAdminEmail' : 'loginAdminUserName'
|
|
110
|
-
if (typeof sdk[methodName] !== 'function') {
|
|
111
|
-
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
112
|
-
}
|
|
113
|
-
const result = await sdk[methodName](loginPayload)
|
|
114
|
-
const token = extractToken(result)
|
|
115
|
-
if (!token) {
|
|
116
|
-
throw new Error('Login succeeded but no X-Token was found in the SDK response.')
|
|
117
|
-
}
|
|
330
|
+
if (selectedAuthType === 'username-password' || selectedAuthType === 'email-password') {
|
|
331
|
+
const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
|
|
118
332
|
return upsertAccount({
|
|
119
333
|
alias: fields.alias || alias,
|
|
120
|
-
authType,
|
|
334
|
+
authType: selectedAuthType,
|
|
121
335
|
token,
|
|
122
336
|
domains
|
|
123
337
|
})
|
|
124
338
|
}
|
|
125
339
|
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
340
|
+
if (selectedAuthType === 'register-email') {
|
|
341
|
+
await callSdkMethod({
|
|
342
|
+
methodName: 'registerAdminSaveEmail',
|
|
343
|
+
domains,
|
|
344
|
+
payload: {
|
|
345
|
+
email: required(fields.registerEmail, 'email'),
|
|
346
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
347
|
+
name: fields.registerName || undefined,
|
|
348
|
+
mailCode: fields.mailCode || undefined,
|
|
349
|
+
referrer: fields.referrer || undefined
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
const token = await loginAndExtractToken({ authType: 'email-password', domains, fields })
|
|
353
|
+
return upsertAccount({
|
|
354
|
+
alias: fields.alias || alias,
|
|
355
|
+
authType: selectedAuthType,
|
|
356
|
+
token,
|
|
357
|
+
domains
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (selectedAuthType === 'register-mobile') {
|
|
362
|
+
await callSdkMethod({
|
|
363
|
+
methodName: 'registerAdminSave',
|
|
364
|
+
domains,
|
|
365
|
+
payload: {
|
|
366
|
+
type: fields.registerType || undefined,
|
|
367
|
+
mobile: required(fields.registerMobile, 'mobile'),
|
|
368
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
369
|
+
name: fields.registerName || undefined,
|
|
370
|
+
smsCode: fields.smsCode || undefined,
|
|
371
|
+
referrer: fields.referrer || undefined,
|
|
372
|
+
agentKey: fields.agentKey || undefined
|
|
373
|
+
}
|
|
137
374
|
})
|
|
138
|
-
const token =
|
|
375
|
+
const token = await loginAndExtractToken({ authType: 'mobile-password', domains, fields })
|
|
139
376
|
return upsertAccount({
|
|
140
377
|
alias: fields.alias || alias,
|
|
141
|
-
authType,
|
|
378
|
+
authType: selectedAuthType,
|
|
142
379
|
token,
|
|
143
380
|
domains
|
|
144
381
|
})
|
|
@@ -147,18 +384,141 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
147
384
|
throw new Error(`Unsupported auth type: ${authType}`)
|
|
148
385
|
}
|
|
149
386
|
|
|
387
|
+
async function loginAndExtractToken({ authType, domains, fields }) {
|
|
388
|
+
const loginConfig = {
|
|
389
|
+
'username-password': {
|
|
390
|
+
methodName: 'loginAdminUserName',
|
|
391
|
+
payload: {
|
|
392
|
+
userName: required(fields.loginUsername, 'username'),
|
|
393
|
+
pwd: required(fields.loginPassword, 'password'),
|
|
394
|
+
rememberMe: true,
|
|
395
|
+
pdomain: fields.pdomain || undefined,
|
|
396
|
+
imgcode: fields.imgcode || undefined,
|
|
397
|
+
k: fields.k || undefined
|
|
398
|
+
}
|
|
399
|
+
},
|
|
400
|
+
'email-password': {
|
|
401
|
+
methodName: 'loginAdminEmail',
|
|
402
|
+
payload: {
|
|
403
|
+
email: required(fields.loginEmail || fields.registerEmail, 'email'),
|
|
404
|
+
pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
|
|
405
|
+
rememberMe: true,
|
|
406
|
+
imgcode: fields.imgcode || undefined,
|
|
407
|
+
k: fields.k || undefined
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
'mobile-password': {
|
|
411
|
+
methodName: 'loginAdminMobile',
|
|
412
|
+
payload: {
|
|
413
|
+
mobile: required(fields.registerMobile, 'mobile'),
|
|
414
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
415
|
+
rememberMe: true,
|
|
416
|
+
imgcode: fields.smsImgCode || fields.imgcode || undefined,
|
|
417
|
+
k: fields.smsK || fields.k || undefined
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}[authType]
|
|
421
|
+
|
|
422
|
+
const result = await callSdkMethod({
|
|
423
|
+
methodName: loginConfig.methodName,
|
|
424
|
+
domains,
|
|
425
|
+
payload: loginConfig.payload
|
|
426
|
+
})
|
|
427
|
+
const token = extractToken(result)
|
|
428
|
+
if (!token) {
|
|
429
|
+
throw new Error(
|
|
430
|
+
`${loginConfig.methodName} did not return an X-Token. Response shape was: ${JSON.stringify(maskResponseForError(result)).slice(0, 600)}`
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
return token
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async function callSdkMethod({ methodName, domains, payload }) {
|
|
437
|
+
const sdk = getSdk()
|
|
438
|
+
resetSdkConfig()
|
|
439
|
+
if (domains && Object.keys(domains).length) {
|
|
440
|
+
sdk.setDomains(domains)
|
|
441
|
+
}
|
|
442
|
+
if (typeof sdk[methodName] !== 'function') {
|
|
443
|
+
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
444
|
+
}
|
|
445
|
+
return sdk[methodName](compactObject(payload))
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function compactObject(input) {
|
|
449
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ''))
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function normalizeAuthType(authType) {
|
|
453
|
+
if (authType === 'password') return 'username-password'
|
|
454
|
+
if (AUTH_TYPES.has(authType)) return authType
|
|
455
|
+
return 'all'
|
|
456
|
+
}
|
|
457
|
+
|
|
150
458
|
function extractToken(response) {
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
459
|
+
const directPaths = [
|
|
460
|
+
['data', 'token'],
|
|
461
|
+
['data', 'xToken'],
|
|
462
|
+
['data', 'xtoken'],
|
|
463
|
+
['data', 'x_token'],
|
|
464
|
+
['data', 'x-token'],
|
|
465
|
+
['data', 'X-Token'],
|
|
466
|
+
['data', 'loginToken'],
|
|
467
|
+
['data', 'adminToken'],
|
|
468
|
+
['token'],
|
|
469
|
+
['xToken'],
|
|
470
|
+
['xtoken'],
|
|
471
|
+
['x_token'],
|
|
472
|
+
['x-token'],
|
|
473
|
+
['X-Token']
|
|
160
474
|
]
|
|
161
|
-
|
|
475
|
+
|
|
476
|
+
for (const path of directPaths) {
|
|
477
|
+
const value = getPath(response, path)
|
|
478
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const found = findTokenDeep(response)
|
|
482
|
+
return found || ''
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function getPath(value, path) {
|
|
486
|
+
return path.reduce((current, key) => (current && typeof current === 'object' ? current[key] : undefined), value)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function findTokenDeep(value, depth = 0) {
|
|
490
|
+
if (!value || typeof value !== 'object' || depth > 5) return ''
|
|
491
|
+
if (Array.isArray(value)) {
|
|
492
|
+
for (const item of value) {
|
|
493
|
+
const found = findTokenDeep(item, depth + 1)
|
|
494
|
+
if (found) return found
|
|
495
|
+
}
|
|
496
|
+
return ''
|
|
497
|
+
}
|
|
498
|
+
for (const [key, child] of Object.entries(value)) {
|
|
499
|
+
const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
500
|
+
if (
|
|
501
|
+
typeof child === 'string' &&
|
|
502
|
+
child.trim() &&
|
|
503
|
+
['token', 'xtoken', 'xToken', 'xstoken', 'admintoken', 'logintoken'].map((item) => item.toLowerCase()).includes(compactKey)
|
|
504
|
+
) {
|
|
505
|
+
return child.trim()
|
|
506
|
+
}
|
|
507
|
+
const found = findTokenDeep(child, depth + 1)
|
|
508
|
+
if (found) return found
|
|
509
|
+
}
|
|
510
|
+
return ''
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function maskResponseForError(value) {
|
|
514
|
+
if (!value || typeof value !== 'object') return value
|
|
515
|
+
if (Array.isArray(value)) return value.map(maskResponseForError)
|
|
516
|
+
return Object.fromEntries(
|
|
517
|
+
Object.entries(value).map(([key, child]) => [
|
|
518
|
+
key,
|
|
519
|
+
/token|password|pwd|secret|key/i.test(key) ? '[REDACTED]' : maskResponseForError(child)
|
|
520
|
+
])
|
|
521
|
+
)
|
|
162
522
|
}
|
|
163
523
|
|
|
164
524
|
function required(value, label) {
|
|
@@ -203,7 +563,8 @@ function escapeHtml(value) {
|
|
|
203
563
|
}
|
|
204
564
|
|
|
205
565
|
function renderForm({ authType, alias, expiresAt }) {
|
|
206
|
-
const
|
|
566
|
+
const initialType = authType === 'all' ? 'username-password' : authType
|
|
567
|
+
const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
|
|
207
568
|
return `<!doctype html>
|
|
208
569
|
<html lang="zh-CN">
|
|
209
570
|
<head>
|
|
@@ -211,79 +572,414 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
211
572
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
212
573
|
<title>APIFM Admin MCP Authorization</title>
|
|
213
574
|
<style>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
575
|
+
:root {
|
|
576
|
+
color-scheme: light;
|
|
577
|
+
--bg: oklch(0.965 0.006 250);
|
|
578
|
+
--surface: oklch(1 0 0);
|
|
579
|
+
--surface-2: oklch(0.982 0.006 250);
|
|
580
|
+
--ink: oklch(0.215 0.028 255);
|
|
581
|
+
--muted: oklch(0.43 0.028 255);
|
|
582
|
+
--line: oklch(0.875 0.013 250);
|
|
583
|
+
--accent: oklch(0.56 0.19 255);
|
|
584
|
+
--accent-ink: oklch(1 0 0);
|
|
585
|
+
--accent-soft: oklch(0.94 0.035 255);
|
|
586
|
+
--success: oklch(0.58 0.15 155);
|
|
587
|
+
}
|
|
588
|
+
* { box-sizing: border-box; }
|
|
589
|
+
body {
|
|
590
|
+
margin: 0;
|
|
591
|
+
min-height: 100vh;
|
|
592
|
+
font: 15px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
593
|
+
background:
|
|
594
|
+
radial-gradient(circle at top left, oklch(0.9 0.06 255), transparent 34rem),
|
|
595
|
+
linear-gradient(135deg, var(--bg), oklch(0.955 0.011 230));
|
|
596
|
+
color: var(--ink);
|
|
597
|
+
}
|
|
598
|
+
.shell {
|
|
599
|
+
width: min(1120px, calc(100vw - 32px));
|
|
600
|
+
min-height: 100vh;
|
|
601
|
+
margin: 0 auto;
|
|
602
|
+
display: grid;
|
|
603
|
+
place-items: center;
|
|
604
|
+
padding: 28px 0;
|
|
605
|
+
}
|
|
606
|
+
.panel {
|
|
607
|
+
width: 100%;
|
|
608
|
+
display: grid;
|
|
609
|
+
grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr);
|
|
610
|
+
background: var(--surface);
|
|
611
|
+
border: 1px solid var(--line);
|
|
612
|
+
border-radius: 16px;
|
|
613
|
+
overflow: hidden;
|
|
614
|
+
box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12);
|
|
615
|
+
}
|
|
616
|
+
.aside {
|
|
617
|
+
padding: 34px;
|
|
618
|
+
background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255));
|
|
619
|
+
color: white;
|
|
620
|
+
display: flex;
|
|
621
|
+
flex-direction: column;
|
|
622
|
+
justify-content: space-between;
|
|
623
|
+
gap: 32px;
|
|
624
|
+
}
|
|
625
|
+
.brand { display: flex; align-items: center; gap: 12px; font-weight: 760; }
|
|
626
|
+
.mark {
|
|
627
|
+
width: 38px;
|
|
628
|
+
height: 38px;
|
|
629
|
+
display: grid;
|
|
630
|
+
place-items: center;
|
|
631
|
+
border-radius: 10px;
|
|
632
|
+
background: oklch(0.74 0.16 210);
|
|
633
|
+
color: oklch(0.18 0.04 255);
|
|
634
|
+
font-weight: 850;
|
|
635
|
+
}
|
|
636
|
+
h1 { margin: 28px 0 12px; font-size: 2rem; line-height: 1.12; text-wrap: balance; letter-spacing: 0; }
|
|
637
|
+
.lead { margin: 0; color: oklch(0.88 0.018 250); max-width: 48ch; }
|
|
638
|
+
.trust { display: grid; gap: 10px; margin: 28px 0 0; padding: 0; list-style: none; }
|
|
639
|
+
.trust li { display: flex; gap: 10px; align-items: flex-start; color: oklch(0.91 0.015 250); }
|
|
640
|
+
.dot { width: 8px; height: 8px; margin-top: 7px; border-radius: 999px; background: oklch(0.78 0.16 155); flex: 0 0 auto; }
|
|
641
|
+
.content { padding: 30px; }
|
|
642
|
+
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
643
|
+
.meta { color: var(--muted); font-size: 0.92rem; }
|
|
644
|
+
.lang, .tabs {
|
|
645
|
+
display: inline-grid;
|
|
646
|
+
grid-auto-flow: column;
|
|
647
|
+
gap: 4px;
|
|
648
|
+
padding: 4px;
|
|
649
|
+
background: var(--surface-2);
|
|
650
|
+
border: 1px solid var(--line);
|
|
651
|
+
border-radius: 999px;
|
|
652
|
+
}
|
|
653
|
+
.lang button, .tabs button, .method button {
|
|
654
|
+
appearance: none;
|
|
655
|
+
border: 0;
|
|
656
|
+
font: inherit;
|
|
657
|
+
cursor: pointer;
|
|
658
|
+
}
|
|
659
|
+
.lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
|
|
660
|
+
.lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
661
|
+
.tabs { margin-bottom: 14px; }
|
|
662
|
+
.method {
|
|
663
|
+
display: grid;
|
|
664
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
665
|
+
gap: 8px;
|
|
666
|
+
margin-bottom: 22px;
|
|
667
|
+
}
|
|
668
|
+
.method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
669
|
+
.method button {
|
|
670
|
+
min-height: 62px;
|
|
671
|
+
padding: 10px;
|
|
672
|
+
border: 1px solid var(--line);
|
|
673
|
+
border-radius: 10px;
|
|
674
|
+
background: var(--surface-2);
|
|
675
|
+
color: var(--ink);
|
|
676
|
+
text-align: left;
|
|
677
|
+
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
|
678
|
+
}
|
|
679
|
+
.method button:hover { border-color: oklch(0.72 0.04 255); transform: translateY(-1px); }
|
|
680
|
+
.method button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
681
|
+
.method strong { display: block; font-size: 0.92rem; line-height: 1.2; }
|
|
682
|
+
.method span { display: block; margin-top: 4px; color: var(--muted); font-size: 0.78rem; line-height: 1.25; }
|
|
683
|
+
form { display: grid; gap: 18px; }
|
|
684
|
+
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
685
|
+
.field { display: grid; gap: 7px; }
|
|
686
|
+
label { color: var(--ink); font-weight: 660; }
|
|
687
|
+
input {
|
|
688
|
+
width: 100%;
|
|
689
|
+
min-height: 44px;
|
|
690
|
+
padding: 10px 12px;
|
|
691
|
+
border: 1px solid var(--line);
|
|
692
|
+
border-radius: 8px;
|
|
693
|
+
background: white;
|
|
694
|
+
color: var(--ink);
|
|
695
|
+
font: inherit;
|
|
696
|
+
outline: none;
|
|
697
|
+
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
698
|
+
}
|
|
699
|
+
input::placeholder { color: oklch(0.48 0.025 255); }
|
|
700
|
+
input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px oklch(0.62 0.16 255 / 0.18); }
|
|
701
|
+
.section {
|
|
702
|
+
display: none;
|
|
703
|
+
padding: 18px;
|
|
704
|
+
border: 1px solid var(--line);
|
|
705
|
+
border-radius: 12px;
|
|
706
|
+
background: var(--surface-2);
|
|
707
|
+
}
|
|
708
|
+
.section.active { display: block; }
|
|
709
|
+
.section-head { margin: 0 0 14px; color: var(--muted); }
|
|
710
|
+
.submit {
|
|
711
|
+
min-height: 46px;
|
|
712
|
+
border: 0;
|
|
713
|
+
border-radius: 8px;
|
|
714
|
+
background: var(--accent);
|
|
715
|
+
color: var(--accent-ink);
|
|
716
|
+
font: inherit;
|
|
717
|
+
font-weight: 760;
|
|
718
|
+
cursor: pointer;
|
|
719
|
+
transition: transform 160ms ease, filter 160ms ease;
|
|
720
|
+
}
|
|
721
|
+
.submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
|
|
722
|
+
.note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
|
|
723
|
+
.security {
|
|
724
|
+
margin-top: 20px;
|
|
725
|
+
padding: 12px 14px;
|
|
726
|
+
border-radius: 10px;
|
|
727
|
+
background: oklch(0.96 0.025 155);
|
|
728
|
+
color: oklch(0.31 0.07 155);
|
|
729
|
+
font-size: 0.9rem;
|
|
730
|
+
}
|
|
731
|
+
[hidden] { display: none !important; }
|
|
732
|
+
@media (max-width: 900px) {
|
|
733
|
+
.panel { grid-template-columns: 1fr; }
|
|
734
|
+
.aside { padding: 26px; }
|
|
735
|
+
.method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
736
|
+
.grid { grid-template-columns: 1fr; }
|
|
737
|
+
}
|
|
738
|
+
@media (max-width: 520px) {
|
|
739
|
+
.shell { width: min(100vw - 20px, 1120px); padding: 10px 0; }
|
|
740
|
+
.content { padding: 20px; }
|
|
741
|
+
.topbar { align-items: flex-start; flex-direction: column; }
|
|
742
|
+
.method, .method[data-group="register"] { grid-template-columns: 1fr; }
|
|
743
|
+
h1 { font-size: 1.55rem; }
|
|
744
|
+
}
|
|
745
|
+
@media (prefers-reduced-motion: reduce) {
|
|
746
|
+
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
747
|
+
}
|
|
222
748
|
</style>
|
|
223
749
|
</head>
|
|
224
750
|
<body>
|
|
225
|
-
<main>
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
751
|
+
<main class="shell">
|
|
752
|
+
<section class="panel" aria-labelledby="title">
|
|
753
|
+
<aside class="aside">
|
|
754
|
+
<div>
|
|
755
|
+
<div class="brand"><span class="mark">A</span><span data-i18n="brand"></span></div>
|
|
756
|
+
<h1 id="title" data-i18n="title"></h1>
|
|
757
|
+
<p class="lead" data-i18n="lead"></p>
|
|
758
|
+
<ul class="trust">
|
|
759
|
+
<li><span class="dot"></span><span data-i18n="trustLocal"></span></li>
|
|
760
|
+
<li><span class="dot"></span><span data-i18n="trustMemory"></span></li>
|
|
761
|
+
<li><span class="dot"></span><span data-i18n="trustSwitch"></span></li>
|
|
762
|
+
</ul>
|
|
763
|
+
</div>
|
|
764
|
+
<p class="lead" data-i18n="footer"></p>
|
|
765
|
+
</aside>
|
|
766
|
+
<div class="content">
|
|
767
|
+
<div class="topbar">
|
|
768
|
+
<div>
|
|
769
|
+
<strong data-i18n="formTitle"></strong>
|
|
770
|
+
<div class="meta"><span data-i18n="expires"></span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
|
|
771
|
+
</div>
|
|
772
|
+
<div class="lang" aria-label="Language">
|
|
773
|
+
<button type="button" data-lang="zh-CN" aria-pressed="true">简</button>
|
|
774
|
+
<button type="button" data-lang="zh-TW" aria-pressed="false">繁</button>
|
|
775
|
+
<button type="button" data-lang="en" aria-pressed="false">EN</button>
|
|
776
|
+
</div>
|
|
777
|
+
</div>
|
|
778
|
+
|
|
779
|
+
<div class="tabs" role="tablist" aria-label="Account mode">
|
|
780
|
+
<button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
|
|
781
|
+
<button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
|
|
782
|
+
</div>
|
|
783
|
+
|
|
784
|
+
<div class="method" data-group="login">
|
|
785
|
+
${renderMethodButton('username-password', initialType)}
|
|
786
|
+
${renderMethodButton('email-password', initialType)}
|
|
787
|
+
${renderMethodButton('basic', initialType)}
|
|
788
|
+
${renderMethodButton('x-token', initialType)}
|
|
789
|
+
</div>
|
|
790
|
+
<div class="method" data-group="register" hidden>
|
|
791
|
+
${renderMethodButton('register-email', initialType)}
|
|
792
|
+
${renderMethodButton('register-mobile', initialType)}
|
|
793
|
+
</div>
|
|
794
|
+
|
|
795
|
+
<form method="post" autocomplete="off" id="authForm">
|
|
796
|
+
<input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
|
|
797
|
+
<div class="field">
|
|
798
|
+
<label for="alias" data-i18n="alias"></label>
|
|
799
|
+
<input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
|
|
800
|
+
</div>
|
|
801
|
+
|
|
802
|
+
${renderSections()}
|
|
803
|
+
|
|
804
|
+
<button class="submit" type="submit" data-i18n="submit"></button>
|
|
805
|
+
</form>
|
|
806
|
+
<div class="security" data-i18n="security"></div>
|
|
807
|
+
<p class="note" data-i18n="closeNote"></p>
|
|
808
|
+
</div>
|
|
809
|
+
</section>
|
|
235
810
|
</main>
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
811
|
+
<script>
|
|
812
|
+
const messages = ${JSON.stringify(I18N)}
|
|
813
|
+
const initialType = ${JSON.stringify(initialType)}
|
|
814
|
+
const methodGroups = {
|
|
815
|
+
'username-password': 'login',
|
|
816
|
+
'email-password': 'login',
|
|
817
|
+
basic: 'login',
|
|
818
|
+
'x-token': 'login',
|
|
819
|
+
'register-email': 'register',
|
|
820
|
+
'register-mobile': 'register'
|
|
821
|
+
}
|
|
822
|
+
const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
|
|
823
|
+
let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
|
|
824
|
+
|
|
825
|
+
const authTypeInput = document.querySelector('#authType')
|
|
826
|
+
const sections = [...document.querySelectorAll('[data-section]')]
|
|
827
|
+
const methodButtons = [...document.querySelectorAll('[data-method]')]
|
|
828
|
+
const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
|
|
829
|
+
const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
|
|
830
|
+
const langButtons = [...document.querySelectorAll('[data-lang]')]
|
|
239
831
|
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
return `
|
|
243
|
-
<label for="username">Basic username</label>
|
|
244
|
-
<input id="username" name="username" required>
|
|
245
|
-
<label for="password">Basic password</label>
|
|
246
|
-
<input id="password" name="password" type="password" required>`
|
|
832
|
+
function t(key) {
|
|
833
|
+
return messages[activeLang][key] || messages['zh-CN'][key] || key
|
|
247
834
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
835
|
+
|
|
836
|
+
function setLang(lang) {
|
|
837
|
+
activeLang = messages[lang] ? lang : 'zh-CN'
|
|
838
|
+
localStorage.setItem('apifm-auth-lang', activeLang)
|
|
839
|
+
document.documentElement.lang = activeLang
|
|
840
|
+
document.querySelectorAll('[data-i18n]').forEach((node) => {
|
|
841
|
+
node.textContent = t(node.dataset.i18n)
|
|
842
|
+
})
|
|
843
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
|
|
844
|
+
node.placeholder = t(node.dataset.i18nPlaceholder)
|
|
845
|
+
})
|
|
846
|
+
langButtons.forEach((button) => {
|
|
847
|
+
button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang))
|
|
848
|
+
})
|
|
252
849
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
<label for="password">Password</label>
|
|
265
|
-
<input id="password" name="password" type="password" required>
|
|
266
|
-
<label for="pdomain">Private domain (optional)</label>
|
|
267
|
-
<input id="pdomain" name="pdomain">`
|
|
850
|
+
|
|
851
|
+
function setGroup(group) {
|
|
852
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
853
|
+
methodGroupsEl.forEach((node) => {
|
|
854
|
+
node.hidden = node.dataset.group !== group
|
|
855
|
+
})
|
|
856
|
+
const currentMethod = authTypeInput.value
|
|
857
|
+
if (methodGroups[currentMethod] !== group) {
|
|
858
|
+
const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
|
|
859
|
+
setMethod(firstMethod)
|
|
860
|
+
}
|
|
268
861
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
862
|
+
|
|
863
|
+
function setMethod(method) {
|
|
864
|
+
if (!method) return
|
|
865
|
+
authTypeInput.value = method
|
|
866
|
+
const group = methodGroups[method]
|
|
867
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
868
|
+
methodGroupsEl.forEach((node) => {
|
|
869
|
+
node.hidden = node.dataset.group !== group
|
|
870
|
+
})
|
|
871
|
+
sections.forEach((section) => {
|
|
872
|
+
const active = section.dataset.section === method
|
|
873
|
+
section.classList.toggle('active', active)
|
|
874
|
+
section.querySelectorAll('input').forEach((input) => {
|
|
875
|
+
input.disabled = !active
|
|
876
|
+
input.required = active && !optionalFields.has(input.name)
|
|
877
|
+
})
|
|
878
|
+
})
|
|
879
|
+
methodButtons.forEach((button) => {
|
|
880
|
+
button.setAttribute('aria-pressed', String(button.dataset.method === method))
|
|
881
|
+
})
|
|
279
882
|
}
|
|
280
|
-
|
|
883
|
+
|
|
884
|
+
methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
|
|
885
|
+
groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
|
|
886
|
+
langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
|
|
887
|
+
setLang(activeLang)
|
|
888
|
+
setMethod(initialType)
|
|
889
|
+
</script>
|
|
890
|
+
</body>
|
|
891
|
+
</html>`
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function renderSections() {
|
|
895
|
+
return `
|
|
896
|
+
<section class="section" data-section="username-password">
|
|
897
|
+
<p class="section-head" data-i18n="usernameHint"></p>
|
|
898
|
+
<div class="grid">
|
|
899
|
+
${field('loginUsername', 'loginUsername', 'username', 'usernamePh')}
|
|
900
|
+
${field('loginPasswordUser', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
901
|
+
${field('pdomain', 'pdomain', 'pdomain', 'pdomainPh')}
|
|
902
|
+
${field('imgcode', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
903
|
+
${field('k', 'k', 'k', 'kPh')}
|
|
904
|
+
</div>
|
|
905
|
+
</section>
|
|
906
|
+
|
|
907
|
+
<section class="section" data-section="email-password">
|
|
908
|
+
<p class="section-head" data-i18n="emailHint"></p>
|
|
909
|
+
<div class="grid">
|
|
910
|
+
${field('loginEmail', 'loginEmail', 'email', 'emailPh', 'email')}
|
|
911
|
+
${field('loginPasswordEmail', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
912
|
+
${field('imgcodeEmail', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
913
|
+
${field('kEmail', 'k', 'k', 'kPh')}
|
|
914
|
+
</div>
|
|
915
|
+
</section>
|
|
916
|
+
|
|
917
|
+
<section class="section" data-section="basic">
|
|
918
|
+
<p class="section-head" data-i18n="basicHint"></p>
|
|
919
|
+
<div class="grid">
|
|
920
|
+
${field('basicUsername', 'basicUsername', 'merchantNo', 'merchantNoPh')}
|
|
921
|
+
${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
|
|
922
|
+
</div>
|
|
923
|
+
</section>
|
|
924
|
+
|
|
925
|
+
<section class="section" data-section="x-token">
|
|
926
|
+
<p class="section-head" data-i18n="tokenHint"></p>
|
|
927
|
+
${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
|
|
928
|
+
</section>
|
|
929
|
+
|
|
930
|
+
<section class="section" data-section="register-email">
|
|
931
|
+
<p class="section-head" data-i18n="registerEmailHint"></p>
|
|
932
|
+
<div class="grid">
|
|
933
|
+
${field('registerEmail', 'registerEmail', 'email', 'emailPh', 'email')}
|
|
934
|
+
${field('registerPasswordEmail', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
935
|
+
${field('registerNameEmail', 'registerName', 'name', 'namePh')}
|
|
936
|
+
${field('mailCode', 'mailCode', 'mailCode', 'mailCodePh')}
|
|
937
|
+
${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
|
|
938
|
+
</div>
|
|
939
|
+
</section>
|
|
940
|
+
|
|
941
|
+
<section class="section" data-section="register-mobile">
|
|
942
|
+
<p class="section-head" data-i18n="registerMobileHint"></p>
|
|
943
|
+
<div class="grid">
|
|
944
|
+
${field('registerMobile', 'registerMobile', 'mobile', 'mobilePh', 'tel')}
|
|
945
|
+
${field('registerPasswordMobile', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
946
|
+
${field('registerNameMobile', 'registerName', 'name', 'namePh')}
|
|
947
|
+
${field('smsCode', 'smsCode', 'smsCode', 'smsCodePh')}
|
|
948
|
+
${field('registerType', 'registerType', 'type', 'typePh')}
|
|
949
|
+
${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
|
|
950
|
+
${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
|
|
951
|
+
${field('smsImgCode', 'smsImgCode', 'imgcode', 'imgcodePh')}
|
|
952
|
+
${field('smsK', 'smsK', 'k', 'kPh')}
|
|
953
|
+
</div>
|
|
954
|
+
</section>`
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function field(id, name, labelKey, placeholderKey, type = 'text') {
|
|
958
|
+
return `<div class="field">
|
|
959
|
+
<label for="${id}" data-i18n="${labelKey}"></label>
|
|
960
|
+
<input id="${id}" name="${name}" type="${type}" data-i18n-placeholder="${placeholderKey}">
|
|
961
|
+
</div>`
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function renderMethodButton(method, activeType) {
|
|
965
|
+
const keys = {
|
|
966
|
+
'username-password': ['methodUsername', 'methodUsernameSub'],
|
|
967
|
+
'email-password': ['methodEmail', 'methodEmailSub'],
|
|
968
|
+
basic: ['methodBasic', 'methodBasicSub'],
|
|
969
|
+
'x-token': ['methodToken', 'methodTokenSub'],
|
|
970
|
+
'register-email': ['methodRegisterEmail', 'methodRegisterEmailSub'],
|
|
971
|
+
'register-mobile': ['methodRegisterMobile', 'methodRegisterMobileSub']
|
|
972
|
+
}[method]
|
|
973
|
+
return `<button type="button" role="tab" data-method="${method}" aria-pressed="${method === activeType}">
|
|
974
|
+
<strong data-i18n="${keys[0]}"></strong>
|
|
975
|
+
<span data-i18n="${keys[1]}"></span>
|
|
976
|
+
</button>`
|
|
281
977
|
}
|
|
282
978
|
|
|
283
979
|
function renderSuccess(account) {
|
|
284
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px"><h1
|
|
980
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px;background:#f7f8fb;color:#202124"><main style="max-width:560px;margin:10vh auto;background:white;border:1px solid #dde2ea;border-radius:14px;padding:32px"><h1>授权成功</h1><p>账号 <strong>${escapeHtml(account.alias)}</strong> 已准备好。你可以关闭本页面,回到 Agent 继续操作。</p></main></body></html>`
|
|
285
981
|
}
|
|
286
982
|
|
|
287
983
|
function renderError(error) {
|
|
288
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px"><
|
|
984
|
+
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px;background:#f7f8fb;color:#202124"><main style="max-width:620px;margin:10vh auto;background:white;border:1px solid #dde2ea;border-radius:14px;padding:32px"><h1>授权失败</h1><p>${escapeHtml(error.message)}</p><p>请返回上一页检查填写内容后重新提交。</p></main></body></html>`
|
|
289
985
|
}
|
package/src/index.js
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
summarizeMetadata
|
|
22
22
|
} from './sdk.js'
|
|
23
23
|
|
|
24
|
-
const VERSION = '26.5.
|
|
24
|
+
const VERSION = '26.5.4'
|
|
25
25
|
|
|
26
26
|
const server = new McpServer(
|
|
27
27
|
{
|
|
@@ -32,7 +32,7 @@ const server = new McpServer(
|
|
|
32
32
|
instructions: [
|
|
33
33
|
'Use this MCP server to operate APIFM admin APIs through the apifm-admin SDK.',
|
|
34
34
|
'Never ask users to paste passwords, Basic Authentication values, X-Token values, or API keys into chat.',
|
|
35
|
-
'Use apifm_admin_start_auth to collect secrets through a local browser page, then call APIs by account alias.',
|
|
35
|
+
'Use apifm_admin_start_auth to collect secrets through a local browser page, then call APIs by account alias. If an API call is attempted before authorization, the tool will return an authorization URL directly.',
|
|
36
36
|
'For user requests that ask to read, create, update, delete, or operate backend data, call apifm_admin_find_and_call or apifm_admin_call and return the live apiResult.',
|
|
37
37
|
'apifm_admin_search_methods and apifm_admin_method_info are only planning helpers; do not treat their parameter documentation as the final answer.'
|
|
38
38
|
].join('\n')
|
|
@@ -44,12 +44,13 @@ server.registerTool(
|
|
|
44
44
|
{
|
|
45
45
|
title: 'Start secure APIFM admin authorization',
|
|
46
46
|
description:
|
|
47
|
-
'Creates a localhost browser page for
|
|
47
|
+
'Creates a localhost browser page for login, Basic Auth, X-Token, email registration, or mobile registration. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
|
|
48
48
|
inputSchema: z.object({
|
|
49
49
|
authType: z
|
|
50
|
-
.enum(['basic', 'x-token', 'password', 'register-email'])
|
|
50
|
+
.enum(['all', 'basic', 'x-token', 'password', 'username-password', 'email-password', 'register-email', 'register-mobile'])
|
|
51
|
+
.optional()
|
|
51
52
|
.describe(
|
|
52
|
-
'basic = Basic Authentication, x-token = existing admin X-Token, password
|
|
53
|
+
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, username-password/email-password = SDK login, register-email/register-mobile = create admin account.'
|
|
53
54
|
),
|
|
54
55
|
alias: z.string().optional().describe('Friendly local account alias, for example prod or test-shop.'),
|
|
55
56
|
domains: z
|
|
@@ -59,7 +60,7 @@ server.registerTool(
|
|
|
59
60
|
port: z.number().int().min(0).max(65535).optional().describe('Optional localhost port. 0 chooses a free port.')
|
|
60
61
|
})
|
|
61
62
|
},
|
|
62
|
-
async ({ authType, alias, domains, port }) => {
|
|
63
|
+
async ({ authType = 'all', alias, domains, port }) => {
|
|
63
64
|
const session = await createAuthSession({ authType, alias, domains, port })
|
|
64
65
|
return toolJson({
|
|
65
66
|
...session,
|
|
@@ -276,8 +277,20 @@ async function callToolResult({
|
|
|
276
277
|
|
|
277
278
|
const account = getAccount(alias)
|
|
278
279
|
if (!account) {
|
|
280
|
+
const session = await createAuthSession({ authType: 'all', alias, domains })
|
|
279
281
|
return toolError(
|
|
280
|
-
|
|
282
|
+
[
|
|
283
|
+
'No APIFM admin account is authorized.',
|
|
284
|
+
`Open this local authorization URL now: ${session.url}`,
|
|
285
|
+
'Complete authorization in the browser, then run the same API call again.'
|
|
286
|
+
].join('\n'),
|
|
287
|
+
{
|
|
288
|
+
reason: 'authorization_required',
|
|
289
|
+
authUrl: session.url,
|
|
290
|
+
sessionId: session.sessionId,
|
|
291
|
+
expiresAt: session.expiresAt,
|
|
292
|
+
nextStep: 'Open authUrl in a browser, complete authorization, then retry the same tool call.'
|
|
293
|
+
}
|
|
281
294
|
)
|
|
282
295
|
}
|
|
283
296
|
|
|
@@ -322,7 +335,7 @@ function findCallableMethod(query) {
|
|
|
322
335
|
return candidates.find((candidate) => typeof sdk[candidate.methodName] === 'function')?.methodName || ''
|
|
323
336
|
}
|
|
324
337
|
|
|
325
|
-
function toolError(message) {
|
|
338
|
+
function toolError(message, extra = {}) {
|
|
326
339
|
return {
|
|
327
340
|
isError: true,
|
|
328
341
|
content: [
|
|
@@ -331,7 +344,7 @@ function toolError(message) {
|
|
|
331
344
|
text: message
|
|
332
345
|
}
|
|
333
346
|
],
|
|
334
|
-
structuredContent: { error: message }
|
|
347
|
+
structuredContent: { error: message, ...extra }
|
|
335
348
|
}
|
|
336
349
|
}
|
|
337
350
|
|
package/src/self-check.js
CHANGED
|
@@ -32,9 +32,45 @@ try {
|
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
const auth = await client.callTool({
|
|
36
|
+
name: 'apifm_admin_start_auth',
|
|
37
|
+
arguments: {}
|
|
38
|
+
})
|
|
39
|
+
const authData = JSON.parse(auth.content[0].text)
|
|
40
|
+
if (!authData.url?.startsWith('http://127.0.0.1:')) {
|
|
41
|
+
throw new Error('start_auth should return a local authorization URL.')
|
|
42
|
+
}
|
|
43
|
+
const authPage = await fetch(authData.url).then((response) => response.text())
|
|
44
|
+
for (const expected of [
|
|
45
|
+
'data-lang="zh-CN"',
|
|
46
|
+
'data-lang="zh-TW"',
|
|
47
|
+
'data-lang="en"',
|
|
48
|
+
'data-group-tab="login"',
|
|
49
|
+
'data-group-tab="register"',
|
|
50
|
+
'data-method="basic"',
|
|
51
|
+
'data-method="x-token"',
|
|
52
|
+
'data-method="register-mobile"',
|
|
53
|
+
'merchantNo',
|
|
54
|
+
'merchantKey'
|
|
55
|
+
]) {
|
|
56
|
+
if (!authPage.includes(expected)) {
|
|
57
|
+
throw new Error(`Authorization page is missing ${expected}.`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const mobileAuth = await client.callTool({
|
|
62
|
+
name: 'apifm_admin_start_auth',
|
|
63
|
+
arguments: { authType: 'register-mobile' }
|
|
64
|
+
})
|
|
65
|
+
const mobileAuthData = JSON.parse(mobileAuth.content[0].text)
|
|
66
|
+
const mobileAuthPage = await fetch(mobileAuthData.url).then((response) => response.text())
|
|
67
|
+
if (!mobileAuthPage.includes('value="register-mobile"')) {
|
|
68
|
+
throw new Error('register-mobile should be accepted as an initial auth type.')
|
|
69
|
+
}
|
|
70
|
+
|
|
35
71
|
const search = await client.callTool({
|
|
36
72
|
name: 'apifm_admin_search_methods',
|
|
37
|
-
arguments: { query: '
|
|
73
|
+
arguments: { query: 'user list', limit: 5 }
|
|
38
74
|
})
|
|
39
75
|
const searchData = JSON.parse(search.content[0].text)
|
|
40
76
|
if (!searchData.sdkMethodCount || !Array.isArray(searchData.results)) {
|
|
@@ -55,12 +91,16 @@ try {
|
|
|
55
91
|
const findAndCall = await client.callTool({
|
|
56
92
|
name: 'apifm_admin_find_and_call',
|
|
57
93
|
arguments: {
|
|
58
|
-
query: '
|
|
94
|
+
query: 'user list',
|
|
59
95
|
payload: { page: 1, pageSize: 1 }
|
|
60
96
|
}
|
|
61
97
|
})
|
|
62
|
-
|
|
63
|
-
|
|
98
|
+
const findAndCallData = findAndCall.structuredContent || {}
|
|
99
|
+
if (!findAndCall.isError || !findAndCallData.authUrl?.startsWith('http://127.0.0.1:')) {
|
|
100
|
+
throw new Error('find_and_call should return a local authUrl when authorization is missing.')
|
|
101
|
+
}
|
|
102
|
+
if (!findAndCall.content[0].text.includes(findAndCallData.authUrl)) {
|
|
103
|
+
throw new Error('find_and_call should show the authUrl in the visible tool text.')
|
|
64
104
|
}
|
|
65
105
|
|
|
66
106
|
await client.close()
|