apifm-admin-mcp 26.5.3 → 26.5.5
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 +42 -33
- package/package.json +1 -1
- package/src/browser-auth.js +438 -374
- package/src/index.js +5 -4
- package/src/self-check.js +28 -1
package/README.md
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
# apifm-admin-mcp
|
|
2
2
|
|
|
3
|
-
`apifm-admin-mcp`
|
|
3
|
+
`apifm-admin-mcp` 是一个通过 `apifm-admin` SDK 调用 APIFM 后台接口的 MCP Server,可用于 Kiro、Cursor、Claude Code、Codex、Windsurf、Trae、Qoder 等支持 MCP 的 Agent。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 安全原则
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
密码、商户秘钥、X-Token 等敏感信息不要粘贴到聊天窗口。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
本 MCP 通过 `apifm_admin_start_auth` 返回一个本机 `127.0.0.1` 授权页面。用户在浏览器里填写敏感信息,信息只保存在当前 MCP 进程内存中,不会作为工具参数发送给大模型。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
如果尚未授权就调用后台 API,`apifm_admin_call` 和 `apifm_admin_find_and_call` 会直接返回 `authUrl`,Agent 应该立即把这个 URL 展示给用户打开。
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
The API callers reject payloads and headers containing sensitive field names such as `pwd`, `password`, `token`, `x-token`, `authorization`, `secret`, or `key`.
|
|
16
|
-
|
|
17
|
-
Accounts are in-memory only. Restarting the MCP server clears all tokens and credentials.
|
|
18
|
-
|
|
19
|
-
## Install
|
|
13
|
+
## 安装
|
|
20
14
|
|
|
21
15
|
```bash
|
|
22
16
|
npm install -g apifm-admin-mcp
|
|
23
17
|
```
|
|
24
18
|
|
|
25
|
-
|
|
19
|
+
本包依赖 `apifm-admin`,但不会把 SDK 源码打包进 MCP。`apifm-admin` 后续升级新增的方法,会在运行时动态发现。
|
|
26
20
|
|
|
27
|
-
## MCP
|
|
21
|
+
## MCP 配置
|
|
28
22
|
|
|
29
|
-
|
|
23
|
+
使用 `npx`:
|
|
30
24
|
|
|
31
25
|
```json
|
|
32
26
|
{
|
|
@@ -39,7 +33,7 @@ Use stdio:
|
|
|
39
33
|
}
|
|
40
34
|
```
|
|
41
35
|
|
|
42
|
-
|
|
36
|
+
全局安装后:
|
|
43
37
|
|
|
44
38
|
```json
|
|
45
39
|
{
|
|
@@ -51,28 +45,43 @@ If installed globally:
|
|
|
51
45
|
}
|
|
52
46
|
```
|
|
53
47
|
|
|
54
|
-
##
|
|
48
|
+
## 授权方式
|
|
49
|
+
|
|
50
|
+
授权页面分为“登录现有账号”和“注册新账号”。
|
|
51
|
+
|
|
52
|
+
登录现有账号:
|
|
53
|
+
|
|
54
|
+
- 手机号登录:调用 `loginAdminMobile`,也就是“手机号码登录”方法,成功后取返回的 X-Token 作为后续 API 凭证。
|
|
55
|
+
- 邮箱登录:调用 `loginAdminEmail`,也就是“邮箱登录获取X-TOKEN”方法,成功后取返回的 X-Token 作为后续 API 凭证。
|
|
56
|
+
- 直接填写 X-Token:不调用登录接口,直接把填写的 X-Token 作为后续 API 凭证。
|
|
57
|
+
- Basic Authentication:填写商户号和商户秘钥,后续请求同时写入 `Authentication` 和 `Authorization` 请求头。
|
|
58
|
+
|
|
59
|
+
注册新账号:
|
|
60
|
+
|
|
61
|
+
- 邮箱注册:调用邮箱注册保存接口,成功后尝试邮箱登录获取 X-Token。
|
|
62
|
+
- 手机号注册:调用手机号注册保存接口,成功后尝试手机号登录获取 X-Token。
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
- `apifm_admin_accounts`: Lists local account aliases without revealing secrets.
|
|
58
|
-
- `apifm_admin_switch_account`: Switches the active account alias.
|
|
59
|
-
- `apifm_admin_remove_account`: Clears an account alias and its in-memory secrets.
|
|
60
|
-
- `apifm_admin_find_and_call`: Searches for the best matching SDK method, calls it, and returns the live backend response in `apiResult`.
|
|
61
|
-
- `apifm_admin_call`: Calls an exact SDK method using the selected local account and returns the live backend response in `apiResult`.
|
|
62
|
-
- `apifm_admin_search_methods`: Planning helper only. Searches methods dynamically from the installed `apifm-admin` SDK, but does not call the API.
|
|
63
|
-
- `apifm_admin_method_info`: Planning helper only. Shows route, method, parameters, and usage for one SDK method, but does not call the API.
|
|
64
|
+
## 工具
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
- `apifm_admin_start_auth`:启动本机授权页面,支持简体中文、繁体中文、英文。
|
|
67
|
+
- `apifm_admin_accounts`:查看当前 MCP 进程内已授权账号,不返回密钥。
|
|
68
|
+
- `apifm_admin_switch_account`:切换当前使用的账号别名。
|
|
69
|
+
- `apifm_admin_remove_account`:删除当前 MCP 进程内保存的账号凭证。
|
|
70
|
+
- `apifm_admin_find_and_call`:按自然语言搜索 SDK 方法并调用,真实接口返回在 `apiResult`。
|
|
71
|
+
- `apifm_admin_call`:按指定 SDK 方法名调用后台 API,真实接口返回在 `apiResult`。
|
|
72
|
+
- `apifm_admin_search_methods`:只搜索 SDK 方法和参数说明,不会调用 API。
|
|
73
|
+
- `apifm_admin_method_info`:只查看某个 SDK 方法说明,不会调用 API。
|
|
66
74
|
|
|
67
|
-
##
|
|
75
|
+
## 使用流程
|
|
68
76
|
|
|
69
|
-
1.
|
|
70
|
-
2. Agent
|
|
71
|
-
3.
|
|
72
|
-
4.
|
|
73
|
-
5. Agent
|
|
77
|
+
1. 用户让 Agent 读取或操作后台数据。
|
|
78
|
+
2. Agent 调用 `apifm_admin_find_and_call` 或 `apifm_admin_call`。
|
|
79
|
+
3. 如果还没授权,工具返回 `authUrl`。
|
|
80
|
+
4. 用户打开 `authUrl`,选择手机号登录、邮箱登录、X-Token 或 Basic Authentication。
|
|
81
|
+
5. 授权成功后,Agent 重新调用刚才的 API。
|
|
82
|
+
6. Agent 使用 `apiResult` 中的真实接口返回回答用户。
|
|
74
83
|
|
|
75
|
-
##
|
|
84
|
+
## 本地检查
|
|
76
85
|
|
|
77
86
|
```bash
|
|
78
87
|
npm run check
|
package/package.json
CHANGED
package/src/browser-auth.js
CHANGED
|
@@ -6,156 +6,224 @@ import { getSdk, resetSdkConfig } from './sdk.js'
|
|
|
6
6
|
|
|
7
7
|
const pendingSessions = new Map()
|
|
8
8
|
|
|
9
|
+
const AUTH_TYPES = new Set([
|
|
10
|
+
'all',
|
|
11
|
+
'basic',
|
|
12
|
+
'x-token',
|
|
13
|
+
'password',
|
|
14
|
+
'mobile-password',
|
|
15
|
+
'email-password',
|
|
16
|
+
'register-email',
|
|
17
|
+
'register-mobile'
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
const OPTIONAL_FIELDS = new Set([
|
|
21
|
+
'alias',
|
|
22
|
+
'imgcode',
|
|
23
|
+
'k',
|
|
24
|
+
'registerName',
|
|
25
|
+
'registerType',
|
|
26
|
+
'referrer',
|
|
27
|
+
'agentKey',
|
|
28
|
+
'mailCode',
|
|
29
|
+
'smsCode'
|
|
30
|
+
])
|
|
31
|
+
|
|
9
32
|
const I18N = {
|
|
10
33
|
'zh-CN': {
|
|
11
34
|
brand: 'APIFM Admin MCP',
|
|
12
|
-
title: '
|
|
35
|
+
title: '安全连接 APIFM 后台',
|
|
13
36
|
lead: '在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。',
|
|
14
37
|
trustLocal: '本地 127.0.0.1 页面提交',
|
|
15
38
|
trustMemory: '凭证仅在内存中保存',
|
|
16
39
|
trustSwitch: '支持多个后台账号切换',
|
|
17
40
|
footer: '授权完成后回到 Agent,重新执行刚才的后台操作。',
|
|
18
|
-
formTitle: '
|
|
41
|
+
formTitle: '授权方式',
|
|
19
42
|
expires: '页面过期时间',
|
|
20
43
|
alias: '账号别名',
|
|
21
44
|
aliasPh: 'default-admin',
|
|
22
|
-
|
|
23
|
-
|
|
45
|
+
tabLogin: '登录现有账号',
|
|
46
|
+
tabRegister: '注册新账号',
|
|
47
|
+
methodMobile: '手机号登录',
|
|
48
|
+
methodMobileSub: '手机号码 + 密码',
|
|
24
49
|
methodEmail: '邮箱登录',
|
|
25
50
|
methodEmailSub: '邮箱 + 密码',
|
|
26
51
|
methodBasic: 'Basic Auth',
|
|
27
|
-
methodBasicSub: '
|
|
52
|
+
methodBasicSub: '商户号 + 商户秘钥',
|
|
28
53
|
methodToken: 'X-Token',
|
|
29
54
|
methodTokenSub: '直接使用令牌',
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
55
|
+
methodRegisterEmail: '邮箱注册',
|
|
56
|
+
methodRegisterEmailSub: '邮箱验证码',
|
|
57
|
+
methodRegisterMobile: '手机号注册',
|
|
58
|
+
methodRegisterMobileSub: '短信验证码',
|
|
59
|
+
mobileHint: '使用后台手机号码和密码登录,MCP 会调用 loginAdminMobile 手机号码登录方法获取 X-Token。',
|
|
60
|
+
emailHint: '使用后台邮箱和密码登录,MCP 会调用 loginAdminEmail 邮箱登录获取 X-TOKEN 方法。',
|
|
61
|
+
basicHint: '填写 Basic Authentication 信息,后续请求会写入 Authentication 和 Authorization 请求头。',
|
|
62
|
+
tokenHint: '直接填写管理员登录后的 X-Token,MCP 会把它作为后续调用后台 API 的凭证。',
|
|
63
|
+
registerEmailHint: '使用邮箱注册开通新后台账号,提交成功后 MCP 会尝试邮箱登录获取 X-Token。',
|
|
64
|
+
registerMobileHint: '使用手机号注册开通新后台账号,提交成功后 MCP 会尝试手机号登录获取 X-Token。',
|
|
39
65
|
password: '密码',
|
|
40
66
|
passwordPh: '请输入登录密码',
|
|
41
|
-
|
|
42
|
-
|
|
67
|
+
imgcode: '图形验证码,可选',
|
|
68
|
+
imgcodePh: '请输入图形验证码',
|
|
69
|
+
k: '验证码随机数,可选',
|
|
70
|
+
kPh: '请输入验证码随机数',
|
|
43
71
|
email: '邮箱地址',
|
|
44
72
|
emailPh: 'name@example.com',
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
73
|
+
mobile: '手机号码',
|
|
74
|
+
mobilePh: '请输入手机号码',
|
|
75
|
+
merchantNo: '商户号',
|
|
76
|
+
merchantNoPh: '请输入商户号',
|
|
77
|
+
merchantKey: '商户秘钥',
|
|
78
|
+
merchantKeyPh: '请输入商户秘钥',
|
|
49
79
|
xToken: 'X-Token',
|
|
50
80
|
xTokenPh: '粘贴管理员 X-Token',
|
|
51
|
-
|
|
52
|
-
|
|
81
|
+
registerPassword: '登录密码',
|
|
82
|
+
registerPasswordPh: '设置登录密码',
|
|
53
83
|
name: '姓名或昵称',
|
|
54
84
|
namePh: '可选',
|
|
55
85
|
mailCode: '邮箱验证码',
|
|
56
|
-
mailCodePh: '
|
|
86
|
+
mailCodePh: '请输入邮箱验证码',
|
|
87
|
+
smsCode: '短信验证码',
|
|
88
|
+
smsCodePh: '请输入短信验证码',
|
|
89
|
+
type: '注册类型,可选',
|
|
90
|
+
typePh: '不填默认为 apifm',
|
|
91
|
+
referrer: '推荐人用户ID,可选',
|
|
92
|
+
referrerPh: '请输入推荐人用户ID',
|
|
93
|
+
agentKey: 'Agent Key,可选',
|
|
94
|
+
agentKeyPh: '请输入 agentKey',
|
|
57
95
|
submit: '完成授权',
|
|
58
96
|
security: '请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。',
|
|
59
97
|
closeNote: '提交成功后可以关闭本页面。'
|
|
60
98
|
},
|
|
61
99
|
'zh-TW': {
|
|
62
100
|
brand: 'APIFM Admin MCP',
|
|
63
|
-
title: '
|
|
101
|
+
title: '安全連接 APIFM 後台',
|
|
64
102
|
lead: '在本機頁面完成授權,密鑰只保存在目前 MCP 程序記憶體,不會進入聊天上下文。',
|
|
65
103
|
trustLocal: '本地 127.0.0.1 頁面提交',
|
|
66
104
|
trustMemory: '憑證僅在記憶體中保存',
|
|
67
105
|
trustSwitch: '支援多個後台帳號切換',
|
|
68
106
|
footer: '授權完成後回到 Agent,重新執行剛才的後台操作。',
|
|
69
|
-
formTitle: '
|
|
107
|
+
formTitle: '授權方式',
|
|
70
108
|
expires: '頁面過期時間',
|
|
71
109
|
alias: '帳號別名',
|
|
72
110
|
aliasPh: 'default-admin',
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
tabLogin: '登入現有帳號',
|
|
112
|
+
tabRegister: '註冊新帳號',
|
|
113
|
+
methodMobile: '手機號登入',
|
|
114
|
+
methodMobileSub: '手機號碼 + 密碼',
|
|
75
115
|
methodEmail: '郵箱登入',
|
|
76
116
|
methodEmailSub: '郵箱 + 密碼',
|
|
77
117
|
methodBasic: 'Basic Auth',
|
|
78
|
-
methodBasicSub: '
|
|
118
|
+
methodBasicSub: '商戶號 + 商戶秘鑰',
|
|
79
119
|
methodToken: 'X-Token',
|
|
80
120
|
methodTokenSub: '直接使用令牌',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
121
|
+
methodRegisterEmail: '郵箱註冊',
|
|
122
|
+
methodRegisterEmailSub: '郵箱驗證碼',
|
|
123
|
+
methodRegisterMobile: '手機號註冊',
|
|
124
|
+
methodRegisterMobileSub: '簡訊驗證碼',
|
|
125
|
+
mobileHint: '使用後台手機號碼和密碼登入,MCP 會呼叫 loginAdminMobile 手機號碼登入方法取得 X-Token。',
|
|
126
|
+
emailHint: '使用後台郵箱和密碼登入,MCP 會呼叫 loginAdminEmail 郵箱登入取得 X-TOKEN 方法。',
|
|
127
|
+
basicHint: '填寫 Basic Authentication 資訊,後續請求會寫入 Authentication 和 Authorization 請求頭。',
|
|
128
|
+
tokenHint: '直接填寫管理員登入後的 X-Token,MCP 會把它作為後續呼叫後台 API 的憑證。',
|
|
129
|
+
registerEmailHint: '使用郵箱註冊開通新後台帳號,提交成功後 MCP 會嘗試郵箱登入取得 X-Token。',
|
|
130
|
+
registerMobileHint: '使用手機號註冊開通新後台帳號,提交成功後 MCP 會嘗試手機號登入取得 X-Token。',
|
|
90
131
|
password: '密碼',
|
|
91
132
|
passwordPh: '請輸入登入密碼',
|
|
92
|
-
|
|
93
|
-
|
|
133
|
+
imgcode: '圖形驗證碼,可選',
|
|
134
|
+
imgcodePh: '請輸入圖形驗證碼',
|
|
135
|
+
k: '驗證碼隨機數,可選',
|
|
136
|
+
kPh: '請輸入驗證碼隨機數',
|
|
94
137
|
email: '郵箱地址',
|
|
95
138
|
emailPh: 'name@example.com',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
139
|
+
mobile: '手機號碼',
|
|
140
|
+
mobilePh: '請輸入手機號碼',
|
|
141
|
+
merchantNo: '商戶號',
|
|
142
|
+
merchantNoPh: '請輸入商戶號',
|
|
143
|
+
merchantKey: '商戶秘鑰',
|
|
144
|
+
merchantKeyPh: '請輸入商戶秘鑰',
|
|
100
145
|
xToken: 'X-Token',
|
|
101
146
|
xTokenPh: '貼上管理員 X-Token',
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
registerPassword: '登入密碼',
|
|
148
|
+
registerPasswordPh: '設定登入密碼',
|
|
104
149
|
name: '姓名或暱稱',
|
|
105
150
|
namePh: '可選',
|
|
106
151
|
mailCode: '郵箱驗證碼',
|
|
107
|
-
mailCodePh: '
|
|
152
|
+
mailCodePh: '請輸入郵箱驗證碼',
|
|
153
|
+
smsCode: '簡訊驗證碼',
|
|
154
|
+
smsCodePh: '請輸入簡訊驗證碼',
|
|
155
|
+
type: '註冊類型,可選',
|
|
156
|
+
typePh: '不填預設為 apifm',
|
|
157
|
+
referrer: '推薦人使用者ID,可選',
|
|
158
|
+
referrerPh: '請輸入推薦人使用者ID',
|
|
159
|
+
agentKey: 'Agent Key,可選',
|
|
160
|
+
agentKeyPh: '請輸入 agentKey',
|
|
108
161
|
submit: '完成授權',
|
|
109
162
|
security: '請不要把密碼、Token 或 Basic Authentication 內容貼到聊天視窗。',
|
|
110
163
|
closeNote: '提交成功後可以關閉本頁面。'
|
|
111
164
|
},
|
|
112
165
|
en: {
|
|
113
166
|
brand: 'APIFM Admin MCP',
|
|
114
|
-
title: 'Connect
|
|
167
|
+
title: 'Connect APIFM admin safely',
|
|
115
168
|
lead: 'Authorize on this local page. Secrets stay in this MCP process memory and never enter the chat transcript.',
|
|
116
169
|
trustLocal: 'Submitted to local 127.0.0.1 only',
|
|
117
170
|
trustMemory: 'Credentials are memory-only',
|
|
118
171
|
trustSwitch: 'Multiple admin accounts supported',
|
|
119
172
|
footer: 'After authorization, return to the agent and run the backend action again.',
|
|
120
|
-
formTitle: '
|
|
173
|
+
formTitle: 'Authorization method',
|
|
121
174
|
expires: 'Page expires at',
|
|
122
175
|
alias: 'Account alias',
|
|
123
176
|
aliasPh: 'default-admin',
|
|
124
|
-
|
|
125
|
-
|
|
177
|
+
tabLogin: 'Log in',
|
|
178
|
+
tabRegister: 'Register',
|
|
179
|
+
methodMobile: 'Mobile login',
|
|
180
|
+
methodMobileSub: 'Mobile + password',
|
|
126
181
|
methodEmail: 'Email login',
|
|
127
182
|
methodEmailSub: 'Email + password',
|
|
128
183
|
methodBasic: 'Basic Auth',
|
|
129
|
-
methodBasicSub: '
|
|
184
|
+
methodBasicSub: 'Merchant no. + key',
|
|
130
185
|
methodToken: 'X-Token',
|
|
131
186
|
methodTokenSub: 'Use an existing token',
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
187
|
+
methodRegisterEmail: 'Email signup',
|
|
188
|
+
methodRegisterEmailSub: 'Email code',
|
|
189
|
+
methodRegisterMobile: 'Mobile signup',
|
|
190
|
+
methodRegisterMobileSub: 'SMS code',
|
|
191
|
+
mobileHint: 'Log in with an admin mobile number and password. The MCP calls the mobile login SDK method to get X-Token.',
|
|
192
|
+
emailHint: 'Log in with an admin email and password. The MCP calls the email login SDK method to get X-Token.',
|
|
193
|
+
basicHint: 'Enter Basic Authentication credentials. API calls will include both Authentication and Authorization headers.',
|
|
194
|
+
tokenHint: 'Enter an existing admin X-Token. The MCP will use it as the credential for later admin API calls.',
|
|
195
|
+
registerEmailHint: 'Create a new admin account by email. After signup, the MCP will try email login to get X-Token.',
|
|
196
|
+
registerMobileHint: 'Create a new admin account by mobile. After signup, the MCP will try mobile login to get X-Token.',
|
|
141
197
|
password: 'Password',
|
|
142
198
|
passwordPh: 'Enter login password',
|
|
143
|
-
|
|
144
|
-
|
|
199
|
+
imgcode: 'Image code, optional',
|
|
200
|
+
imgcodePh: 'Enter image code',
|
|
201
|
+
k: 'Captcha nonce, optional',
|
|
202
|
+
kPh: 'Enter captcha nonce',
|
|
145
203
|
email: 'Email address',
|
|
146
204
|
emailPh: 'name@example.com',
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
205
|
+
mobile: 'Mobile number',
|
|
206
|
+
mobilePh: 'Enter mobile number',
|
|
207
|
+
merchantNo: 'Merchant number',
|
|
208
|
+
merchantNoPh: 'Enter merchant number',
|
|
209
|
+
merchantKey: 'Merchant key',
|
|
210
|
+
merchantKeyPh: 'Enter merchant key',
|
|
151
211
|
xToken: 'X-Token',
|
|
152
212
|
xTokenPh: 'Paste admin X-Token',
|
|
153
|
-
|
|
154
|
-
|
|
213
|
+
registerPassword: 'Login password',
|
|
214
|
+
registerPasswordPh: 'Set login password',
|
|
155
215
|
name: 'Name or nickname',
|
|
156
216
|
namePh: 'Optional',
|
|
157
217
|
mailCode: 'Email verification code',
|
|
158
|
-
mailCodePh: 'Enter
|
|
218
|
+
mailCodePh: 'Enter email code',
|
|
219
|
+
smsCode: 'SMS verification code',
|
|
220
|
+
smsCodePh: 'Enter SMS code',
|
|
221
|
+
type: 'Signup type, optional',
|
|
222
|
+
typePh: 'Defaults to apifm',
|
|
223
|
+
referrer: 'Referrer user ID, optional',
|
|
224
|
+
referrerPh: 'Enter referrer user ID',
|
|
225
|
+
agentKey: 'Agent Key, optional',
|
|
226
|
+
agentKeyPh: 'Enter agentKey',
|
|
159
227
|
submit: 'Authorize account',
|
|
160
228
|
security: 'Do not paste passwords, tokens, or Basic Authentication values into the chat window.',
|
|
161
229
|
closeNote: 'You can close this page after authorization succeeds.'
|
|
@@ -224,13 +292,14 @@ export async function createAuthSession({ authType = 'all', alias = '', domains
|
|
|
224
292
|
|
|
225
293
|
async function finishAuth({ authType, alias, domains, fields }) {
|
|
226
294
|
const selectedAuthType = normalizeAuthType(authType)
|
|
295
|
+
|
|
227
296
|
if (selectedAuthType === 'basic') {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
297
|
+
const merchantNo = required(fields.basicUsername, 'merchant number')
|
|
298
|
+
const merchantKey = required(fields.basicPassword, 'merchant key')
|
|
230
299
|
return upsertAccount({
|
|
231
300
|
alias: fields.alias || alias,
|
|
232
301
|
authType: 'basic',
|
|
233
|
-
basicAuth: `Basic ${Buffer.from(`${
|
|
302
|
+
basicAuth: `Basic ${Buffer.from(`${merchantNo}:${merchantKey}`).toString('base64')}`,
|
|
234
303
|
domains
|
|
235
304
|
})
|
|
236
305
|
}
|
|
@@ -244,36 +313,8 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
244
313
|
})
|
|
245
314
|
}
|
|
246
315
|
|
|
247
|
-
if (selectedAuthType === '
|
|
248
|
-
const
|
|
249
|
-
const loginPayload =
|
|
250
|
-
loginMode === 'email'
|
|
251
|
-
? {
|
|
252
|
-
email: required(fields.loginEmail, 'email'),
|
|
253
|
-
pwd: required(fields.loginPassword, 'password'),
|
|
254
|
-
rememberMe: true
|
|
255
|
-
}
|
|
256
|
-
: {
|
|
257
|
-
userName: required(fields.loginUsername, 'username'),
|
|
258
|
-
pwd: required(fields.loginPassword, 'password'),
|
|
259
|
-
rememberMe: true,
|
|
260
|
-
pdomain: fields.pdomain || undefined
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const sdk = getSdk()
|
|
264
|
-
resetSdkConfig()
|
|
265
|
-
if (domains && Object.keys(domains).length) {
|
|
266
|
-
sdk.setDomains(domains)
|
|
267
|
-
}
|
|
268
|
-
const methodName = loginMode === 'email' ? 'loginAdminEmail' : 'loginAdminUserName'
|
|
269
|
-
if (typeof sdk[methodName] !== 'function') {
|
|
270
|
-
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
271
|
-
}
|
|
272
|
-
const result = await sdk[methodName](loginPayload)
|
|
273
|
-
const token = extractToken(result)
|
|
274
|
-
if (!token) {
|
|
275
|
-
throw new Error('Login succeeded but no X-Token was found in the SDK response.')
|
|
276
|
-
}
|
|
316
|
+
if (selectedAuthType === 'mobile-password' || selectedAuthType === 'email-password') {
|
|
317
|
+
const token = await loginAndExtractToken({ authType: selectedAuthType, domains, fields })
|
|
277
318
|
return upsertAccount({
|
|
278
319
|
alias: fields.alias || alias,
|
|
279
320
|
authType: selectedAuthType,
|
|
@@ -283,18 +324,41 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
283
324
|
}
|
|
284
325
|
|
|
285
326
|
if (selectedAuthType === 'register-email') {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
await callSdkMethod({
|
|
328
|
+
methodName: 'registerAdminSaveEmail',
|
|
329
|
+
domains,
|
|
330
|
+
payload: {
|
|
331
|
+
email: required(fields.registerEmail, 'email'),
|
|
332
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
333
|
+
name: fields.registerName || undefined,
|
|
334
|
+
mailCode: fields.mailCode || undefined,
|
|
335
|
+
referrer: fields.referrer || undefined
|
|
336
|
+
}
|
|
296
337
|
})
|
|
297
|
-
const token =
|
|
338
|
+
const token = await loginAndExtractToken({ authType: 'email-password', domains, fields })
|
|
339
|
+
return upsertAccount({
|
|
340
|
+
alias: fields.alias || alias,
|
|
341
|
+
authType: selectedAuthType,
|
|
342
|
+
token,
|
|
343
|
+
domains
|
|
344
|
+
})
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (selectedAuthType === 'register-mobile') {
|
|
348
|
+
await callSdkMethod({
|
|
349
|
+
methodName: 'registerAdminSave',
|
|
350
|
+
domains,
|
|
351
|
+
payload: {
|
|
352
|
+
type: fields.registerType || undefined,
|
|
353
|
+
mobile: required(fields.registerMobile, 'mobile'),
|
|
354
|
+
pwd: required(fields.registerPassword, 'password'),
|
|
355
|
+
name: fields.registerName || undefined,
|
|
356
|
+
smsCode: fields.smsCode || undefined,
|
|
357
|
+
referrer: fields.referrer || undefined,
|
|
358
|
+
agentKey: fields.agentKey || undefined
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
const token = await loginAndExtractToken({ authType: 'mobile-password', domains, fields })
|
|
298
362
|
return upsertAccount({
|
|
299
363
|
alias: fields.alias || alias,
|
|
300
364
|
authType: selectedAuthType,
|
|
@@ -306,28 +370,129 @@ async function finishAuth({ authType, alias, domains, fields }) {
|
|
|
306
370
|
throw new Error(`Unsupported auth type: ${authType}`)
|
|
307
371
|
}
|
|
308
372
|
|
|
309
|
-
function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
373
|
+
async function loginAndExtractToken({ authType, domains, fields }) {
|
|
374
|
+
const loginConfig = {
|
|
375
|
+
'email-password': {
|
|
376
|
+
methodName: 'loginAdminEmail',
|
|
377
|
+
payload: {
|
|
378
|
+
email: required(fields.loginEmail || fields.registerEmail, 'email'),
|
|
379
|
+
pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
|
|
380
|
+
rememberMe: true,
|
|
381
|
+
imgcode: fields.imgcode || undefined,
|
|
382
|
+
k: fields.k || undefined
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
'mobile-password': {
|
|
386
|
+
methodName: 'loginAdminMobile',
|
|
387
|
+
payload: {
|
|
388
|
+
mobile: required(fields.loginMobile || fields.registerMobile, 'mobile'),
|
|
389
|
+
pwd: required(fields.loginPassword || fields.registerPassword, 'password'),
|
|
390
|
+
rememberMe: true,
|
|
391
|
+
imgcode: fields.imgcode || undefined,
|
|
392
|
+
k: fields.k || undefined
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}[authType]
|
|
396
|
+
|
|
397
|
+
const result = await callSdkMethod({
|
|
398
|
+
methodName: loginConfig.methodName,
|
|
399
|
+
domains,
|
|
400
|
+
payload: loginConfig.payload
|
|
401
|
+
})
|
|
402
|
+
const token = extractToken(result)
|
|
403
|
+
if (!token) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`${loginConfig.methodName} did not return an X-Token. Response shape was: ${JSON.stringify(maskResponseForError(result)).slice(0, 600)}`
|
|
406
|
+
)
|
|
315
407
|
}
|
|
408
|
+
return token
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function callSdkMethod({ methodName, domains, payload }) {
|
|
412
|
+
const sdk = getSdk()
|
|
413
|
+
resetSdkConfig()
|
|
414
|
+
if (domains && Object.keys(domains).length) {
|
|
415
|
+
sdk.setDomains(domains)
|
|
416
|
+
}
|
|
417
|
+
if (typeof sdk[methodName] !== 'function') {
|
|
418
|
+
throw new Error(`apifm-admin does not expose ${methodName}`)
|
|
419
|
+
}
|
|
420
|
+
return sdk[methodName](compactObject(payload))
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function compactObject(input) {
|
|
424
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value !== ''))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function normalizeAuthType(authType) {
|
|
428
|
+
if (authType === 'password' || authType === 'username-password') return 'mobile-password'
|
|
429
|
+
if (AUTH_TYPES.has(authType)) return authType
|
|
316
430
|
return 'all'
|
|
317
431
|
}
|
|
318
432
|
|
|
319
433
|
function extractToken(response) {
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
434
|
+
const directPaths = [
|
|
435
|
+
['data', 'token'],
|
|
436
|
+
['data', 'xToken'],
|
|
437
|
+
['data', 'xtoken'],
|
|
438
|
+
['data', 'x_token'],
|
|
439
|
+
['data', 'x-token'],
|
|
440
|
+
['data', 'X-Token'],
|
|
441
|
+
['data', 'loginToken'],
|
|
442
|
+
['data', 'adminToken'],
|
|
443
|
+
['token'],
|
|
444
|
+
['xToken'],
|
|
445
|
+
['xtoken'],
|
|
446
|
+
['x_token'],
|
|
447
|
+
['x-token'],
|
|
448
|
+
['X-Token']
|
|
329
449
|
]
|
|
330
|
-
|
|
450
|
+
|
|
451
|
+
for (const path of directPaths) {
|
|
452
|
+
const value = getPath(response, path)
|
|
453
|
+
if (typeof value === 'string' && value.trim()) return value.trim()
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return findTokenDeep(response) || ''
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function getPath(value, path) {
|
|
460
|
+
return path.reduce((current, key) => (current && typeof current === 'object' ? current[key] : undefined), value)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function findTokenDeep(value, depth = 0) {
|
|
464
|
+
if (!value || typeof value !== 'object' || depth > 5) return ''
|
|
465
|
+
if (Array.isArray(value)) {
|
|
466
|
+
for (const item of value) {
|
|
467
|
+
const found = findTokenDeep(item, depth + 1)
|
|
468
|
+
if (found) return found
|
|
469
|
+
}
|
|
470
|
+
return ''
|
|
471
|
+
}
|
|
472
|
+
for (const [key, child] of Object.entries(value)) {
|
|
473
|
+
const compactKey = key.toLowerCase().replace(/[^a-z0-9]/g, '')
|
|
474
|
+
if (
|
|
475
|
+
typeof child === 'string' &&
|
|
476
|
+
child.trim() &&
|
|
477
|
+
['token', 'xtoken', 'xstoken', 'admintoken', 'logintoken'].includes(compactKey)
|
|
478
|
+
) {
|
|
479
|
+
return child.trim()
|
|
480
|
+
}
|
|
481
|
+
const found = findTokenDeep(child, depth + 1)
|
|
482
|
+
if (found) return found
|
|
483
|
+
}
|
|
484
|
+
return ''
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function maskResponseForError(value) {
|
|
488
|
+
if (!value || typeof value !== 'object') return value
|
|
489
|
+
if (Array.isArray(value)) return value.map(maskResponseForError)
|
|
490
|
+
return Object.fromEntries(
|
|
491
|
+
Object.entries(value).map(([key, child]) => [
|
|
492
|
+
key,
|
|
493
|
+
/token|password|pwd|secret|key/i.test(key) ? '[REDACTED]' : maskResponseForError(child)
|
|
494
|
+
])
|
|
495
|
+
)
|
|
331
496
|
}
|
|
332
497
|
|
|
333
498
|
function required(value, label) {
|
|
@@ -372,7 +537,8 @@ function escapeHtml(value) {
|
|
|
372
537
|
}
|
|
373
538
|
|
|
374
539
|
function renderForm({ authType, alias, expiresAt }) {
|
|
375
|
-
const initialType = authType === 'all' ? '
|
|
540
|
+
const initialType = authType === 'all' ? 'mobile-password' : authType
|
|
541
|
+
const initialGroup = initialType.startsWith('register-') ? 'register' : 'login'
|
|
376
542
|
return `<!doctype html>
|
|
377
543
|
<html lang="zh-CN">
|
|
378
544
|
<head>
|
|
@@ -391,9 +557,6 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
391
557
|
--accent: oklch(0.56 0.19 255);
|
|
392
558
|
--accent-ink: oklch(1 0 0);
|
|
393
559
|
--accent-soft: oklch(0.94 0.035 255);
|
|
394
|
-
--success: oklch(0.58 0.15 155);
|
|
395
|
-
--danger: oklch(0.55 0.18 25);
|
|
396
|
-
--radius: 12px;
|
|
397
560
|
}
|
|
398
561
|
* { box-sizing: border-box; }
|
|
399
562
|
body {
|
|
@@ -405,77 +568,27 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
405
568
|
linear-gradient(135deg, var(--bg), oklch(0.955 0.011 230));
|
|
406
569
|
color: var(--ink);
|
|
407
570
|
}
|
|
408
|
-
.shell {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
margin: 0 auto;
|
|
412
|
-
display: grid;
|
|
413
|
-
place-items: center;
|
|
414
|
-
padding: 28px 0;
|
|
415
|
-
}
|
|
416
|
-
.panel {
|
|
417
|
-
width: 100%;
|
|
418
|
-
display: grid;
|
|
419
|
-
grid-template-columns: minmax(280px, 0.84fr) minmax(320px, 1.16fr);
|
|
420
|
-
background: var(--surface);
|
|
421
|
-
border: 1px solid var(--line);
|
|
422
|
-
border-radius: 16px;
|
|
423
|
-
overflow: hidden;
|
|
424
|
-
box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12);
|
|
425
|
-
}
|
|
426
|
-
.aside {
|
|
427
|
-
padding: 34px;
|
|
428
|
-
background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255));
|
|
429
|
-
color: white;
|
|
430
|
-
display: flex;
|
|
431
|
-
flex-direction: column;
|
|
432
|
-
justify-content: space-between;
|
|
433
|
-
gap: 32px;
|
|
434
|
-
}
|
|
571
|
+
.shell { width: min(1120px, calc(100vw - 32px)); min-height: 100vh; margin: 0 auto; display: grid; place-items: center; padding: 28px 0; }
|
|
572
|
+
.panel { width: 100%; display: grid; grid-template-columns: minmax(280px, 0.78fr) minmax(340px, 1.22fr); background: var(--surface); border: 1px solid var(--line); border-radius: 16px; overflow: hidden; box-shadow: 0 8px 28px oklch(0.25 0.02 255 / 0.12); }
|
|
573
|
+
.aside { padding: 34px; background: linear-gradient(180deg, oklch(0.26 0.07 255), oklch(0.19 0.04 255)); color: white; display: flex; flex-direction: column; justify-content: space-between; gap: 32px; }
|
|
435
574
|
.brand { display: flex; align-items: center; gap: 12px; font-weight: 760; }
|
|
436
|
-
.mark {
|
|
437
|
-
width: 38px;
|
|
438
|
-
height: 38px;
|
|
439
|
-
display: grid;
|
|
440
|
-
place-items: center;
|
|
441
|
-
border-radius: 10px;
|
|
442
|
-
background: oklch(0.74 0.16 210);
|
|
443
|
-
color: oklch(0.18 0.04 255);
|
|
444
|
-
font-weight: 850;
|
|
445
|
-
}
|
|
575
|
+
.mark { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 10px; background: oklch(0.74 0.16 210); color: oklch(0.18 0.04 255); font-weight: 850; }
|
|
446
576
|
h1 { margin: 28px 0 12px; font-size: 2rem; line-height: 1.12; text-wrap: balance; letter-spacing: 0; }
|
|
447
577
|
.lead { margin: 0; color: oklch(0.88 0.018 250); max-width: 48ch; }
|
|
448
578
|
.trust { display: grid; gap: 10px; margin: 28px 0 0; padding: 0; list-style: none; }
|
|
449
579
|
.trust li { display: flex; gap: 10px; align-items: flex-start; color: oklch(0.91 0.015 250); }
|
|
450
580
|
.dot { width: 8px; height: 8px; margin-top: 7px; border-radius: 999px; background: oklch(0.78 0.16 155); flex: 0 0 auto; }
|
|
451
581
|
.content { padding: 30px; }
|
|
452
|
-
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom:
|
|
582
|
+
.topbar { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 20px; }
|
|
453
583
|
.meta { color: var(--muted); font-size: 0.92rem; }
|
|
454
|
-
.lang { display: inline-grid; grid-auto-flow: column; gap: 4px; padding: 4px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
|
|
455
|
-
.lang button, .method button {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
}
|
|
461
|
-
.
|
|
462
|
-
.lang button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
463
|
-
.method {
|
|
464
|
-
display: grid;
|
|
465
|
-
grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
466
|
-
gap: 8px;
|
|
467
|
-
margin-bottom: 22px;
|
|
468
|
-
}
|
|
469
|
-
.method button {
|
|
470
|
-
min-height: 58px;
|
|
471
|
-
padding: 10px;
|
|
472
|
-
border: 1px solid var(--line);
|
|
473
|
-
border-radius: 10px;
|
|
474
|
-
background: var(--surface-2);
|
|
475
|
-
color: var(--ink);
|
|
476
|
-
text-align: left;
|
|
477
|
-
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease;
|
|
478
|
-
}
|
|
584
|
+
.lang, .tabs { display: inline-grid; grid-auto-flow: column; gap: 4px; padding: 4px; background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px; }
|
|
585
|
+
.lang button, .tabs button, .method button { appearance: none; border: 0; font: inherit; cursor: pointer; }
|
|
586
|
+
.lang button, .tabs button { padding: 7px 12px; border-radius: 999px; color: var(--muted); background: transparent; }
|
|
587
|
+
.lang button[aria-pressed="true"], .tabs button[aria-pressed="true"] { background: var(--ink); color: white; }
|
|
588
|
+
.tabs { margin-bottom: 14px; }
|
|
589
|
+
.method { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; margin-bottom: 22px; }
|
|
590
|
+
.method[data-group="register"] { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
591
|
+
.method button { min-height: 62px; padding: 10px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); color: var(--ink); text-align: left; transition: border-color 160ms ease, background 160ms ease, transform 160ms ease; }
|
|
479
592
|
.method button:hover { border-color: oklch(0.72 0.04 255); transform: translateY(-1px); }
|
|
480
593
|
.method button[aria-pressed="true"] { border-color: var(--accent); background: var(--accent-soft); }
|
|
481
594
|
.method strong { display: block; font-size: 0.92rem; line-height: 1.2; }
|
|
@@ -484,71 +597,20 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
484
597
|
.grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
485
598
|
.field { display: grid; gap: 7px; }
|
|
486
599
|
label { color: var(--ink); font-weight: 660; }
|
|
487
|
-
input {
|
|
488
|
-
width: 100%;
|
|
489
|
-
min-height: 44px;
|
|
490
|
-
padding: 10px 12px;
|
|
491
|
-
border: 1px solid var(--line);
|
|
492
|
-
border-radius: 8px;
|
|
493
|
-
background: white;
|
|
494
|
-
color: var(--ink);
|
|
495
|
-
font: inherit;
|
|
496
|
-
outline: none;
|
|
497
|
-
transition: border-color 160ms ease, box-shadow 160ms ease;
|
|
498
|
-
}
|
|
600
|
+
input { width: 100%; min-height: 44px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; background: white; color: var(--ink); font: inherit; outline: none; transition: border-color 160ms ease, box-shadow 160ms ease; }
|
|
499
601
|
input::placeholder { color: oklch(0.48 0.025 255); }
|
|
500
602
|
input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px oklch(0.62 0.16 255 / 0.18); }
|
|
501
|
-
.section {
|
|
502
|
-
display: none;
|
|
503
|
-
padding: 18px;
|
|
504
|
-
border: 1px solid var(--line);
|
|
505
|
-
border-radius: 12px;
|
|
506
|
-
background: var(--surface-2);
|
|
507
|
-
}
|
|
603
|
+
.section { display: none; padding: 18px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-2); }
|
|
508
604
|
.section.active { display: block; }
|
|
509
605
|
.section-head { margin: 0 0 14px; color: var(--muted); }
|
|
510
|
-
.submit {
|
|
511
|
-
min-height: 46px;
|
|
512
|
-
border: 0;
|
|
513
|
-
border-radius: 8px;
|
|
514
|
-
background: var(--accent);
|
|
515
|
-
color: var(--accent-ink);
|
|
516
|
-
font: inherit;
|
|
517
|
-
font-weight: 760;
|
|
518
|
-
cursor: pointer;
|
|
519
|
-
transition: transform 160ms ease, filter 160ms ease;
|
|
520
|
-
}
|
|
606
|
+
.submit { min-height: 46px; border: 0; border-radius: 8px; background: var(--accent); color: var(--accent-ink); font: inherit; font-weight: 760; cursor: pointer; transition: transform 160ms ease, filter 160ms ease; }
|
|
521
607
|
.submit:hover { filter: brightness(1.03); transform: translateY(-1px); }
|
|
522
|
-
.note {
|
|
523
|
-
|
|
524
|
-
color: var(--muted);
|
|
525
|
-
font-size: 0.9rem;
|
|
526
|
-
}
|
|
527
|
-
.security {
|
|
528
|
-
margin-top: 20px;
|
|
529
|
-
padding: 12px 14px;
|
|
530
|
-
border-radius: 10px;
|
|
531
|
-
background: oklch(0.96 0.025 155);
|
|
532
|
-
color: oklch(0.31 0.07 155);
|
|
533
|
-
font-size: 0.9rem;
|
|
534
|
-
}
|
|
608
|
+
.note { margin: 12px 0 0; color: var(--muted); font-size: 0.9rem; }
|
|
609
|
+
.security { margin-top: 20px; padding: 12px 14px; border-radius: 10px; background: oklch(0.96 0.025 155); color: oklch(0.31 0.07 155); font-size: 0.9rem; }
|
|
535
610
|
[hidden] { display: none !important; }
|
|
536
|
-
@media (max-width:
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.method { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
540
|
-
.grid { grid-template-columns: 1fr; }
|
|
541
|
-
}
|
|
542
|
-
@media (max-width: 520px) {
|
|
543
|
-
.shell { width: min(100vw - 20px, 1080px); padding: 10px 0; }
|
|
544
|
-
.content { padding: 20px; }
|
|
545
|
-
.topbar { align-items: flex-start; flex-direction: column; }
|
|
546
|
-
.method { grid-template-columns: 1fr; }
|
|
547
|
-
h1 { font-size: 1.55rem; }
|
|
548
|
-
}
|
|
549
|
-
@media (prefers-reduced-motion: reduce) {
|
|
550
|
-
*, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
551
|
-
}
|
|
611
|
+
@media (max-width: 900px) { .panel { grid-template-columns: 1fr; } .aside { padding: 26px; } .method { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid { grid-template-columns: 1fr; } }
|
|
612
|
+
@media (max-width: 520px) { .shell { width: min(100vw - 20px, 1120px); padding: 10px 0; } .content { padding: 20px; } .topbar { align-items: flex-start; flex-direction: column; } .method, .method[data-group="register"] { grid-template-columns: 1fr; } h1 { font-size: 1.55rem; } }
|
|
613
|
+
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; } }
|
|
552
614
|
</style>
|
|
553
615
|
</head>
|
|
554
616
|
<body>
|
|
@@ -556,22 +618,22 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
556
618
|
<section class="panel" aria-labelledby="title">
|
|
557
619
|
<aside class="aside">
|
|
558
620
|
<div>
|
|
559
|
-
<div class="brand"><span class="mark">A</span><span data-i18n="brand"
|
|
560
|
-
<h1 id="title" data-i18n="title"
|
|
561
|
-
<p class="lead" data-i18n="lead"
|
|
621
|
+
<div class="brand"><span class="mark">A</span><span data-i18n="brand"></span></div>
|
|
622
|
+
<h1 id="title" data-i18n="title"></h1>
|
|
623
|
+
<p class="lead" data-i18n="lead"></p>
|
|
562
624
|
<ul class="trust">
|
|
563
|
-
<li><span class="dot"></span><span data-i18n="trustLocal"
|
|
564
|
-
<li><span class="dot"></span><span data-i18n="trustMemory"
|
|
565
|
-
<li><span class="dot"></span><span data-i18n="trustSwitch"
|
|
625
|
+
<li><span class="dot"></span><span data-i18n="trustLocal"></span></li>
|
|
626
|
+
<li><span class="dot"></span><span data-i18n="trustMemory"></span></li>
|
|
627
|
+
<li><span class="dot"></span><span data-i18n="trustSwitch"></span></li>
|
|
566
628
|
</ul>
|
|
567
629
|
</div>
|
|
568
|
-
<p class="lead" data-i18n="footer"
|
|
630
|
+
<p class="lead" data-i18n="footer"></p>
|
|
569
631
|
</aside>
|
|
570
632
|
<div class="content">
|
|
571
633
|
<div class="topbar">
|
|
572
634
|
<div>
|
|
573
|
-
<strong data-i18n="formTitle"
|
|
574
|
-
<div class="meta"><span data-i18n="expires"
|
|
635
|
+
<strong data-i18n="formTitle"></strong>
|
|
636
|
+
<div class="meta"><span data-i18n="expires"></span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
|
|
575
637
|
</div>
|
|
576
638
|
<div class="lang" aria-label="Language">
|
|
577
639
|
<button type="button" data-lang="zh-CN" aria-pressed="true">简</button>
|
|
@@ -579,150 +641,89 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
579
641
|
<button type="button" data-lang="en" aria-pressed="false">EN</button>
|
|
580
642
|
</div>
|
|
581
643
|
</div>
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
${
|
|
644
|
+
<div class="tabs" role="tablist" aria-label="Account mode">
|
|
645
|
+
<button type="button" data-group-tab="login" aria-pressed="${initialGroup === 'login'}" data-i18n="tabLogin"></button>
|
|
646
|
+
<button type="button" data-group-tab="register" aria-pressed="${initialGroup === 'register'}" data-i18n="tabRegister"></button>
|
|
647
|
+
</div>
|
|
648
|
+
<div class="method" data-group="login">
|
|
649
|
+
${renderMethodButton('mobile-password', initialType)}
|
|
585
650
|
${renderMethodButton('email-password', initialType)}
|
|
586
651
|
${renderMethodButton('basic', initialType)}
|
|
587
652
|
${renderMethodButton('x-token', initialType)}
|
|
653
|
+
</div>
|
|
654
|
+
<div class="method" data-group="register" hidden>
|
|
588
655
|
${renderMethodButton('register-email', initialType)}
|
|
656
|
+
${renderMethodButton('register-mobile', initialType)}
|
|
589
657
|
</div>
|
|
590
|
-
|
|
591
658
|
<form method="post" autocomplete="off" id="authForm">
|
|
592
659
|
<input type="hidden" id="authType" name="authType" value="${escapeHtml(initialType)}">
|
|
593
660
|
<div class="field">
|
|
594
|
-
<label for="alias" data-i18n="alias"
|
|
595
|
-
<input id="alias" name="alias" value="${escapeHtml(alias)}"
|
|
661
|
+
<label for="alias" data-i18n="alias"></label>
|
|
662
|
+
<input id="alias" name="alias" value="${escapeHtml(alias)}" data-i18n-placeholder="aliasPh">
|
|
596
663
|
</div>
|
|
597
|
-
|
|
598
|
-
<
|
|
599
|
-
<p class="section-head" data-i18n="usernameHint">使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。</p>
|
|
600
|
-
<div class="grid">
|
|
601
|
-
<div class="field">
|
|
602
|
-
<label for="loginUsername" data-i18n="username">用户名</label>
|
|
603
|
-
<input id="loginUsername" name="loginUsername" data-i18n-placeholder="usernamePh">
|
|
604
|
-
</div>
|
|
605
|
-
<div class="field">
|
|
606
|
-
<label for="loginPasswordUser" data-i18n="password">密码</label>
|
|
607
|
-
<input id="loginPasswordUser" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
|
|
608
|
-
</div>
|
|
609
|
-
</div>
|
|
610
|
-
<div class="field" style="margin-top:14px">
|
|
611
|
-
<label for="pdomain" data-i18n="pdomain">专属域名,可选</label>
|
|
612
|
-
<input id="pdomain" name="pdomain" data-i18n-placeholder="pdomainPh">
|
|
613
|
-
</div>
|
|
614
|
-
</section>
|
|
615
|
-
|
|
616
|
-
<section class="section" data-section="email-password">
|
|
617
|
-
<p class="section-head" data-i18n="emailHint">使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。</p>
|
|
618
|
-
<div class="grid">
|
|
619
|
-
<div class="field">
|
|
620
|
-
<label for="loginEmail" data-i18n="email">邮箱地址</label>
|
|
621
|
-
<input id="loginEmail" name="loginEmail" type="email" data-i18n-placeholder="emailPh">
|
|
622
|
-
</div>
|
|
623
|
-
<div class="field">
|
|
624
|
-
<label for="loginPasswordEmail" data-i18n="password">密码</label>
|
|
625
|
-
<input id="loginPasswordEmail" name="loginPassword" type="password" data-i18n-placeholder="passwordPh">
|
|
626
|
-
</div>
|
|
627
|
-
</div>
|
|
628
|
-
</section>
|
|
629
|
-
|
|
630
|
-
<section class="section" data-section="basic">
|
|
631
|
-
<p class="section-head" data-i18n="basicHint">填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。</p>
|
|
632
|
-
<div class="grid">
|
|
633
|
-
<div class="field">
|
|
634
|
-
<label for="basicUsername" data-i18n="basicUser">Basic 用户名</label>
|
|
635
|
-
<input id="basicUsername" name="basicUsername" data-i18n-placeholder="basicUserPh">
|
|
636
|
-
</div>
|
|
637
|
-
<div class="field">
|
|
638
|
-
<label for="basicPassword" data-i18n="basicPass">Basic 密码</label>
|
|
639
|
-
<input id="basicPassword" name="basicPassword" type="password" data-i18n-placeholder="basicPassPh">
|
|
640
|
-
</div>
|
|
641
|
-
</div>
|
|
642
|
-
</section>
|
|
643
|
-
|
|
644
|
-
<section class="section" data-section="x-token">
|
|
645
|
-
<p class="section-head" data-i18n="tokenHint">直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。</p>
|
|
646
|
-
<div class="field">
|
|
647
|
-
<label for="xToken" data-i18n="xToken">X-Token</label>
|
|
648
|
-
<input id="xToken" name="xToken" type="password" data-i18n-placeholder="xTokenPh">
|
|
649
|
-
</div>
|
|
650
|
-
</section>
|
|
651
|
-
|
|
652
|
-
<section class="section" data-section="register-email">
|
|
653
|
-
<p class="section-head" data-i18n="registerHint">使用邮箱注册开通新后台账号。需要验证码时,请先通过 SDK 获取邮箱验证码。</p>
|
|
654
|
-
<div class="grid">
|
|
655
|
-
<div class="field">
|
|
656
|
-
<label for="registerEmail" data-i18n="email">邮箱地址</label>
|
|
657
|
-
<input id="registerEmail" name="registerEmail" type="email" data-i18n-placeholder="emailPh">
|
|
658
|
-
</div>
|
|
659
|
-
<div class="field">
|
|
660
|
-
<label for="registerPassword" data-i18n="newPassword">登录密码</label>
|
|
661
|
-
<input id="registerPassword" name="registerPassword" type="password" data-i18n-placeholder="newPasswordPh">
|
|
662
|
-
</div>
|
|
663
|
-
<div class="field">
|
|
664
|
-
<label for="registerName" data-i18n="name">姓名或昵称</label>
|
|
665
|
-
<input id="registerName" name="registerName" data-i18n-placeholder="namePh">
|
|
666
|
-
</div>
|
|
667
|
-
<div class="field">
|
|
668
|
-
<label for="mailCode" data-i18n="mailCode">邮箱验证码</label>
|
|
669
|
-
<input id="mailCode" name="mailCode" data-i18n-placeholder="mailCodePh">
|
|
670
|
-
</div>
|
|
671
|
-
</div>
|
|
672
|
-
</section>
|
|
673
|
-
|
|
674
|
-
<button class="submit" type="submit" data-i18n="submit">完成授权</button>
|
|
664
|
+
${renderSections()}
|
|
665
|
+
<button class="submit" type="submit" data-i18n="submit"></button>
|
|
675
666
|
</form>
|
|
676
|
-
<div class="security" data-i18n="security"
|
|
677
|
-
<p class="note" data-i18n="closeNote"
|
|
667
|
+
<div class="security" data-i18n="security"></div>
|
|
668
|
+
<p class="note" data-i18n="closeNote"></p>
|
|
678
669
|
</div>
|
|
679
670
|
</section>
|
|
680
671
|
</main>
|
|
681
672
|
<script>
|
|
682
673
|
const messages = ${JSON.stringify(I18N)}
|
|
683
674
|
const initialType = ${JSON.stringify(initialType)}
|
|
675
|
+
const methodGroups = {
|
|
676
|
+
'mobile-password': 'login',
|
|
677
|
+
'email-password': 'login',
|
|
678
|
+
basic: 'login',
|
|
679
|
+
'x-token': 'login',
|
|
680
|
+
'register-email': 'register',
|
|
681
|
+
'register-mobile': 'register'
|
|
682
|
+
}
|
|
683
|
+
const optionalFields = new Set(${JSON.stringify([...OPTIONAL_FIELDS])})
|
|
684
684
|
let activeLang = localStorage.getItem('apifm-auth-lang') || 'zh-CN'
|
|
685
|
-
|
|
686
685
|
const authTypeInput = document.querySelector('#authType')
|
|
687
686
|
const sections = [...document.querySelectorAll('[data-section]')]
|
|
688
687
|
const methodButtons = [...document.querySelectorAll('[data-method]')]
|
|
688
|
+
const groupTabs = [...document.querySelectorAll('[data-group-tab]')]
|
|
689
|
+
const methodGroupsEl = [...document.querySelectorAll('.method[data-group]')]
|
|
689
690
|
const langButtons = [...document.querySelectorAll('[data-lang]')]
|
|
690
|
-
|
|
691
|
-
function t(key) {
|
|
692
|
-
return messages[activeLang][key] || messages['zh-CN'][key] || key
|
|
693
|
-
}
|
|
694
|
-
|
|
691
|
+
function t(key) { return messages[activeLang][key] || messages['zh-CN'][key] || key }
|
|
695
692
|
function setLang(lang) {
|
|
696
693
|
activeLang = messages[lang] ? lang : 'zh-CN'
|
|
697
694
|
localStorage.setItem('apifm-auth-lang', activeLang)
|
|
698
695
|
document.documentElement.lang = activeLang
|
|
699
|
-
document.querySelectorAll('[data-i18n]').forEach((node) => {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
696
|
+
document.querySelectorAll('[data-i18n]').forEach((node) => { node.textContent = t(node.dataset.i18n) })
|
|
697
|
+
document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => { node.placeholder = t(node.dataset.i18nPlaceholder) })
|
|
698
|
+
langButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang)))
|
|
699
|
+
}
|
|
700
|
+
function setGroup(group) {
|
|
701
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
702
|
+
methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
|
|
703
|
+
const currentMethod = authTypeInput.value
|
|
704
|
+
if (methodGroups[currentMethod] !== group) {
|
|
705
|
+
const firstMethod = methodButtons.find((button) => methodGroups[button.dataset.method] === group)?.dataset.method
|
|
706
|
+
setMethod(firstMethod)
|
|
707
|
+
}
|
|
708
708
|
}
|
|
709
|
-
|
|
710
709
|
function setMethod(method) {
|
|
710
|
+
if (!method) return
|
|
711
711
|
authTypeInput.value = method
|
|
712
|
+
const group = methodGroups[method]
|
|
713
|
+
groupTabs.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.groupTab === group)))
|
|
714
|
+
methodGroupsEl.forEach((node) => { node.hidden = node.dataset.group !== group })
|
|
712
715
|
sections.forEach((section) => {
|
|
713
716
|
const active = section.dataset.section === method
|
|
714
717
|
section.classList.toggle('active', active)
|
|
715
718
|
section.querySelectorAll('input').forEach((input) => {
|
|
716
719
|
input.disabled = !active
|
|
717
|
-
input.required = active &&
|
|
720
|
+
input.required = active && !optionalFields.has(input.name)
|
|
718
721
|
})
|
|
719
722
|
})
|
|
720
|
-
methodButtons.forEach((button) =>
|
|
721
|
-
button.setAttribute('aria-pressed', String(button.dataset.method === method))
|
|
722
|
-
})
|
|
723
|
+
methodButtons.forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.method === method)))
|
|
723
724
|
}
|
|
724
|
-
|
|
725
725
|
methodButtons.forEach((button) => button.addEventListener('click', () => setMethod(button.dataset.method)))
|
|
726
|
+
groupTabs.forEach((button) => button.addEventListener('click', () => setGroup(button.dataset.groupTab)))
|
|
726
727
|
langButtons.forEach((button) => button.addEventListener('click', () => setLang(button.dataset.lang)))
|
|
727
728
|
setLang(activeLang)
|
|
728
729
|
setMethod(initialType)
|
|
@@ -731,13 +732,76 @@ function renderForm({ authType, alias, expiresAt }) {
|
|
|
731
732
|
</html>`
|
|
732
733
|
}
|
|
733
734
|
|
|
735
|
+
function renderSections() {
|
|
736
|
+
return `
|
|
737
|
+
<section class="section" data-section="mobile-password">
|
|
738
|
+
<p class="section-head" data-i18n="mobileHint"></p>
|
|
739
|
+
<div class="grid">
|
|
740
|
+
${field('loginMobile', 'loginMobile', 'mobile', 'mobilePh', 'tel')}
|
|
741
|
+
${field('loginPasswordMobile', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
742
|
+
${field('imgcodeMobile', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
743
|
+
${field('kMobile', 'k', 'k', 'kPh')}
|
|
744
|
+
</div>
|
|
745
|
+
</section>
|
|
746
|
+
<section class="section" data-section="email-password">
|
|
747
|
+
<p class="section-head" data-i18n="emailHint"></p>
|
|
748
|
+
<div class="grid">
|
|
749
|
+
${field('loginEmail', 'loginEmail', 'email', 'emailPh', 'email')}
|
|
750
|
+
${field('loginPasswordEmail', 'loginPassword', 'password', 'passwordPh', 'password')}
|
|
751
|
+
${field('imgcodeEmail', 'imgcode', 'imgcode', 'imgcodePh')}
|
|
752
|
+
${field('kEmail', 'k', 'k', 'kPh')}
|
|
753
|
+
</div>
|
|
754
|
+
</section>
|
|
755
|
+
<section class="section" data-section="basic">
|
|
756
|
+
<p class="section-head" data-i18n="basicHint"></p>
|
|
757
|
+
<div class="grid">
|
|
758
|
+
${field('basicUsername', 'basicUsername', 'merchantNo', 'merchantNoPh')}
|
|
759
|
+
${field('basicPassword', 'basicPassword', 'merchantKey', 'merchantKeyPh', 'password')}
|
|
760
|
+
</div>
|
|
761
|
+
</section>
|
|
762
|
+
<section class="section" data-section="x-token">
|
|
763
|
+
<p class="section-head" data-i18n="tokenHint"></p>
|
|
764
|
+
${field('xToken', 'xToken', 'xToken', 'xTokenPh', 'password')}
|
|
765
|
+
</section>
|
|
766
|
+
<section class="section" data-section="register-email">
|
|
767
|
+
<p class="section-head" data-i18n="registerEmailHint"></p>
|
|
768
|
+
<div class="grid">
|
|
769
|
+
${field('registerEmail', 'registerEmail', 'email', 'emailPh', 'email')}
|
|
770
|
+
${field('registerPasswordEmail', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
771
|
+
${field('registerNameEmail', 'registerName', 'name', 'namePh')}
|
|
772
|
+
${field('mailCode', 'mailCode', 'mailCode', 'mailCodePh')}
|
|
773
|
+
${field('referrerEmail', 'referrer', 'referrer', 'referrerPh')}
|
|
774
|
+
</div>
|
|
775
|
+
</section>
|
|
776
|
+
<section class="section" data-section="register-mobile">
|
|
777
|
+
<p class="section-head" data-i18n="registerMobileHint"></p>
|
|
778
|
+
<div class="grid">
|
|
779
|
+
${field('registerMobile', 'registerMobile', 'mobile', 'mobilePh', 'tel')}
|
|
780
|
+
${field('registerPasswordMobile', 'registerPassword', 'registerPassword', 'registerPasswordPh', 'password')}
|
|
781
|
+
${field('registerNameMobile', 'registerName', 'name', 'namePh')}
|
|
782
|
+
${field('smsCode', 'smsCode', 'smsCode', 'smsCodePh')}
|
|
783
|
+
${field('registerType', 'registerType', 'type', 'typePh')}
|
|
784
|
+
${field('referrerMobile', 'referrer', 'referrer', 'referrerPh')}
|
|
785
|
+
${field('agentKey', 'agentKey', 'agentKey', 'agentKeyPh')}
|
|
786
|
+
</div>
|
|
787
|
+
</section>`
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function field(id, name, labelKey, placeholderKey, type = 'text') {
|
|
791
|
+
return `<div class="field">
|
|
792
|
+
<label for="${id}" data-i18n="${labelKey}"></label>
|
|
793
|
+
<input id="${id}" name="${name}" type="${type}" data-i18n-placeholder="${placeholderKey}">
|
|
794
|
+
</div>`
|
|
795
|
+
}
|
|
796
|
+
|
|
734
797
|
function renderMethodButton(method, activeType) {
|
|
735
798
|
const keys = {
|
|
736
|
-
'
|
|
799
|
+
'mobile-password': ['methodMobile', 'methodMobileSub'],
|
|
737
800
|
'email-password': ['methodEmail', 'methodEmailSub'],
|
|
738
801
|
basic: ['methodBasic', 'methodBasicSub'],
|
|
739
802
|
'x-token': ['methodToken', 'methodTokenSub'],
|
|
740
|
-
'register-email': ['
|
|
803
|
+
'register-email': ['methodRegisterEmail', 'methodRegisterEmailSub'],
|
|
804
|
+
'register-mobile': ['methodRegisterMobile', 'methodRegisterMobileSub']
|
|
741
805
|
}[method]
|
|
742
806
|
return `<button type="button" role="tab" data-method="${method}" aria-pressed="${method === activeType}">
|
|
743
807
|
<strong data-i18n="${keys[0]}"></strong>
|
|
@@ -750,5 +814,5 @@ function renderSuccess(account) {
|
|
|
750
814
|
}
|
|
751
815
|
|
|
752
816
|
function renderError(error) {
|
|
753
|
-
return `<!doctype html><html lang="zh-CN"><meta charset="utf-8"><body style="font:16px system-ui;padding:40px"><
|
|
817
|
+
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>`
|
|
754
818
|
}
|
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.5'
|
|
25
25
|
|
|
26
26
|
const server = new McpServer(
|
|
27
27
|
{
|
|
@@ -44,13 +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 mobile login, email 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(['all', 'basic', 'x-token', 'password', '
|
|
50
|
+
.enum(['all', 'basic', 'x-token', 'password', 'mobile-password', 'email-password', 'register-email', 'register-mobile'])
|
|
51
51
|
.optional()
|
|
52
52
|
.describe(
|
|
53
|
-
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token,
|
|
53
|
+
'Optional initial auth method. all shows every method in one browser page. basic = Basic Authentication, x-token = existing admin X-Token, mobile-password/email-password = SDK login, register-email/register-mobile = create admin account.'
|
|
54
54
|
),
|
|
55
55
|
alias: z.string().optional().describe('Friendly local account alias, for example prod or test-shop.'),
|
|
56
56
|
domains: z
|
|
@@ -304,6 +304,7 @@ async function callToolResult({
|
|
|
304
304
|
const sdkHeaders = { ...headers }
|
|
305
305
|
if (account.basicAuth) {
|
|
306
306
|
sdkHeaders.Authorization = account.basicAuth
|
|
307
|
+
sdkHeaders.Authentication = account.basicAuth
|
|
307
308
|
}
|
|
308
309
|
|
|
309
310
|
sdk.setConfig({
|
package/src/self-check.js
CHANGED
|
@@ -41,11 +41,38 @@ try {
|
|
|
41
41
|
throw new Error('start_auth should return a local authorization URL.')
|
|
42
42
|
}
|
|
43
43
|
const authPage = await fetch(authData.url).then((response) => response.text())
|
|
44
|
-
for (const expected of [
|
|
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="mobile-password"',
|
|
51
|
+
'data-method="basic"',
|
|
52
|
+
'data-method="x-token"',
|
|
53
|
+
'data-method="register-mobile"',
|
|
54
|
+
'merchantNo',
|
|
55
|
+
'merchantKey',
|
|
56
|
+
'loginAdminMobile',
|
|
57
|
+
'Authentication'
|
|
58
|
+
]) {
|
|
45
59
|
if (!authPage.includes(expected)) {
|
|
46
60
|
throw new Error(`Authorization page is missing ${expected}.`)
|
|
47
61
|
}
|
|
48
62
|
}
|
|
63
|
+
if (authPage.includes('data-method="username-password"')) {
|
|
64
|
+
throw new Error('Authorization page must not expose username-password login.')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const mobileAuth = await client.callTool({
|
|
68
|
+
name: 'apifm_admin_start_auth',
|
|
69
|
+
arguments: { authType: 'register-mobile' }
|
|
70
|
+
})
|
|
71
|
+
const mobileAuthData = JSON.parse(mobileAuth.content[0].text)
|
|
72
|
+
const mobileAuthPage = await fetch(mobileAuthData.url).then((response) => response.text())
|
|
73
|
+
if (!mobileAuthPage.includes('value="register-mobile"')) {
|
|
74
|
+
throw new Error('register-mobile should be accepted as an initial auth type.')
|
|
75
|
+
}
|
|
49
76
|
|
|
50
77
|
const search = await client.callTool({
|
|
51
78
|
name: 'apifm_admin_search_methods',
|