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 CHANGED
@@ -1,32 +1,26 @@
1
1
  # apifm-admin-mcp
2
2
 
3
- `apifm-admin-mcp` is a stdio MCP server for AI agents that need to operate APIFM admin APIs through the [`apifm-admin`](https://www.npmjs.com/package/apifm-admin) SDK.
3
+ `apifm-admin-mcp` 是一个通过 `apifm-admin` SDK 调用 APIFM 后台接口的 MCP Server,可用于 Kiro、Cursor、Claude Code、Codex、Windsurf、Trae、Qoder 等支持 MCP Agent。
4
4
 
5
- It is designed for Kiro, Cursor, Claude Code, Codex, Windsurf, Trae, Qoder, and other MCP-compatible agents.
5
+ ## 安全原则
6
6
 
7
- ## Security Model
7
+ 密码、商户秘钥、X-Token 等敏感信息不要粘贴到聊天窗口。
8
8
 
9
- Secrets must not be pasted into model chat.
9
+ MCP 通过 `apifm_admin_start_auth` 返回一个本机 `127.0.0.1` 授权页面。用户在浏览器里填写敏感信息,信息只保存在当前 MCP 进程内存中,不会作为工具参数发送给大模型。
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`, username/password login details, email/password login details, or email signup details in that local browser page. Those secrets stay in the local MCP process memory and are never part of MCP tool arguments.
11
+ 如果尚未授权就调用后台 API,`apifm_admin_call` `apifm_admin_find_and_call` 会直接返回 `authUrl`,Agent 应该立即把这个 URL 展示给用户打开。
12
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.
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
- The package depends on `apifm-admin` as a normal npm dependency. It does not bundle SDK source code, so newer compatible `apifm-admin` releases can add methods without requiring this MCP package to be regenerated.
19
+ 本包依赖 `apifm-admin`,但不会把 SDK 源码打包进 MCP。`apifm-admin` 后续升级新增的方法,会在运行时动态发现。
26
20
 
27
- ## MCP Configuration
21
+ ## MCP 配置
28
22
 
29
- Use stdio:
23
+ 使用 `npx`:
30
24
 
31
25
  ```json
32
26
  {
@@ -39,7 +33,7 @@ Use stdio:
39
33
  }
40
34
  ```
41
35
 
42
- If installed globally:
36
+ 全局安装后:
43
37
 
44
38
  ```json
45
39
  {
@@ -51,28 +45,43 @@ If installed globally:
51
45
  }
52
46
  ```
53
47
 
54
- ## Tools
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
- - `apifm_admin_start_auth`: Starts a local secure authorization page for username login, email login, Basic Auth, X-Token, or email registration. The page supports Simplified Chinese, Traditional Chinese, and English.
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
- For real backend data, agents should call `apifm_admin_find_and_call` or `apifm_admin_call`. The final API response is returned under `apiResult`.
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
- ## Example Agent Flow
75
+ ## 使用流程
68
76
 
69
- 1. User: "Log in to my admin account and read the user list."
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 username login, email login, Basic Auth, X-Token, or email signup.
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.
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
- ## Local Check
84
+ ## 本地检查
76
85
 
77
86
  ```bash
78
87
  npm run check
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "apifm-admin-mcp",
3
- "version": "26.5.3",
3
+ "version": "26.5.5",
4
4
  "description": "MCP server for safely operating APIFM admin APIs through the apifm-admin SDK.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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: '安全连接你的 APIFM 后台',
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
- methodUsername: '用户名登录',
23
- methodUsernameSub: '用户名 + 密码',
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
- methodRegister: '邮箱注册',
31
- methodRegisterSub: '开通新后台',
32
- usernameHint: '使用后台用户名和密码登录,MCP 会通过 SDK 换取 X-Token。',
33
- emailHint: '使用后台邮箱和密码登录,MCP 会通过 SDK 换取 X-Token。',
34
- basicHint: '填写 Basic Authentication 信息,将作为 Authorization 请求头调用后台接口。',
35
- tokenHint: '直接填写管理员登录后的 X-Token,用于后续所有后台接口调用。',
36
- registerHint: '使用邮箱注册开通新后台账号。需要验证码时,请先通过 SDK 获取邮箱验证码。',
37
- username: '用户名',
38
- usernamePh: '请输入后台用户名',
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
- pdomain: '专属域名,可选',
42
- pdomainPh: '例如 yourdomain',
67
+ imgcode: '图形验证码,可选',
68
+ imgcodePh: '请输入图形验证码',
69
+ k: '验证码随机数,可选',
70
+ kPh: '请输入验证码随机数',
43
71
  email: '邮箱地址',
44
72
  emailPh: 'name@example.com',
45
- basicUser: 'Basic 用户名',
46
- basicUserPh: '请输入 Basic 用户名',
47
- basicPass: 'Basic 密码',
48
- basicPassPh: '请输入 Basic 密码',
73
+ mobile: '手机号码',
74
+ mobilePh: '请输入手机号码',
75
+ merchantNo: '商户号',
76
+ merchantNoPh: '请输入商户号',
77
+ merchantKey: '商户秘钥',
78
+ merchantKeyPh: '请输入商户秘钥',
49
79
  xToken: 'X-Token',
50
80
  xTokenPh: '粘贴管理员 X-Token',
51
- newPassword: '登录密码',
52
- newPasswordPh: '设置登录密码',
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: '安全連接你的 APIFM 後台',
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
- methodUsername: '使用者名稱登入',
74
- methodUsernameSub: '使用者名稱 + 密碼',
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
- methodRegister: '郵箱註冊',
82
- methodRegisterSub: '開通新後台',
83
- usernameHint: '使用後台使用者名稱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
84
- emailHint: '使用後台郵箱和密碼登入,MCP 會透過 SDK 換取 X-Token。',
85
- basicHint: '填寫 Basic Authentication 資訊,將作為 Authorization 請求頭呼叫後台介面。',
86
- tokenHint: '直接填寫管理員登入後的 X-Token,用於後續所有後台介面呼叫。',
87
- registerHint: '使用郵箱註冊開通新後台帳號。需要驗證碼時,請先透過 SDK 取得郵箱驗證碼。',
88
- username: '使用者名稱',
89
- usernamePh: '請輸入後台使用者名稱',
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
- pdomain: '專屬網域,可選',
93
- pdomainPh: '例如 yourdomain',
133
+ imgcode: '圖形驗證碼,可選',
134
+ imgcodePh: '請輸入圖形驗證碼',
135
+ k: '驗證碼隨機數,可選',
136
+ kPh: '請輸入驗證碼隨機數',
94
137
  email: '郵箱地址',
95
138
  emailPh: 'name@example.com',
96
- basicUser: 'Basic 使用者名稱',
97
- basicUserPh: '請輸入 Basic 使用者名稱',
98
- basicPass: 'Basic 密碼',
99
- basicPassPh: '請輸入 Basic 密碼',
139
+ mobile: '手機號碼',
140
+ mobilePh: '請輸入手機號碼',
141
+ merchantNo: '商戶號',
142
+ merchantNoPh: '請輸入商戶號',
143
+ merchantKey: '商戶秘鑰',
144
+ merchantKeyPh: '請輸入商戶秘鑰',
100
145
  xToken: 'X-Token',
101
146
  xTokenPh: '貼上管理員 X-Token',
102
- newPassword: '登入密碼',
103
- newPasswordPh: '設定登入密碼',
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 your APIFM admin safely',
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: 'Choose an authorization method',
173
+ formTitle: 'Authorization method',
121
174
  expires: 'Page expires at',
122
175
  alias: 'Account alias',
123
176
  aliasPh: 'default-admin',
124
- methodUsername: 'Username login',
125
- methodUsernameSub: 'Username + password',
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: 'Username + password',
184
+ methodBasicSub: 'Merchant no. + key',
130
185
  methodToken: 'X-Token',
131
186
  methodTokenSub: 'Use an existing token',
132
- methodRegister: 'Email signup',
133
- methodRegisterSub: 'Create new admin',
134
- usernameHint: 'Log in with an admin username and password. The MCP will exchange them for an X-Token through the SDK.',
135
- emailHint: 'Log in with an admin email and password. The MCP will exchange them for an X-Token through the SDK.',
136
- basicHint: 'Enter Basic Authentication credentials. They will be sent as the Authorization header for API calls.',
137
- tokenHint: 'Enter an existing admin X-Token for later backend API calls.',
138
- registerHint: 'Create a new admin account by email. If a verification code is required, request it through the SDK first.',
139
- username: 'Username',
140
- usernamePh: 'Enter admin username',
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
- pdomain: 'Private domain, optional',
144
- pdomainPh: 'Example: yourdomain',
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
- basicUser: 'Basic username',
148
- basicUserPh: 'Enter Basic username',
149
- basicPass: 'Basic password',
150
- basicPassPh: 'Enter Basic password',
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
- newPassword: 'Login password',
154
- newPasswordPh: 'Set login password',
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 verification code',
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 username = required(fields.basicUsername, 'Basic username')
229
- const password = required(fields.basicPassword, 'Basic password')
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(`${username}:${password}`).toString('base64')}`,
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 === 'username-password' || selectedAuthType === 'email-password' || selectedAuthType === 'password') {
248
- const loginMode = selectedAuthType === 'email-password' ? 'email' : 'username'
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
- const sdk = getSdk()
287
- resetSdkConfig()
288
- if (domains && Object.keys(domains).length) {
289
- sdk.setDomains(domains)
290
- }
291
- const result = await sdk.registerAdminSaveEmail({
292
- email: required(fields.registerEmail, 'email'),
293
- pwd: required(fields.registerPassword, 'password'),
294
- name: fields.registerName || undefined,
295
- mailCode: fields.mailCode || fields.code || undefined
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 = extractToken(result)
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 normalizeAuthType(authType) {
310
- if (authType === 'password') return 'username-password'
311
- if (
312
- ['all', 'basic', 'x-token', 'username-password', 'email-password', 'register-email'].includes(authType)
313
- ) {
314
- return authType
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 candidates = [
321
- response?.data?.token,
322
- response?.data?.xToken,
323
- response?.data?.xtoken,
324
- response?.data?.x_token,
325
- response?.data?.['x-token'],
326
- response?.token,
327
- response?.xToken,
328
- response?.xtoken
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
- return candidates.find((item) => typeof item === 'string' && item.trim()) || ''
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' ? 'username-password' : authType
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
- width: min(1080px, calc(100vw - 32px));
410
- min-height: 100vh;
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: 22px; }
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
- appearance: none;
457
- border: 0;
458
- font: inherit;
459
- cursor: pointer;
460
- }
461
- .lang button { padding: 6px 10px; border-radius: 999px; color: var(--muted); background: transparent; }
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
- margin: 12px 0 0;
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: 860px) {
537
- .panel { grid-template-columns: 1fr; }
538
- .aside { padding: 26px; }
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">APIFM Admin MCP</span></div>
560
- <h1 id="title" data-i18n="title">安全连接你的 APIFM 后台</h1>
561
- <p class="lead" data-i18n="lead">在本机页面完成授权,密钥只保存在当前 MCP 进程内存,不会进入聊天上下文。</p>
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">本地 127.0.0.1 页面提交</span></li>
564
- <li><span class="dot"></span><span data-i18n="trustMemory">凭证仅在内存中保存</span></li>
565
- <li><span class="dot"></span><span data-i18n="trustSwitch">支持多个后台账号切换</span></li>
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">授权完成后回到 Agent,重新执行刚才的后台操作。</p>
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">选择授权方式</strong>
574
- <div class="meta"><span data-i18n="expires">页面过期时间</span>: ${escapeHtml(new Date(expiresAt).toLocaleString())}</div>
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
- <div class="method" role="tablist" aria-label="Auth methods">
584
- ${renderMethodButton('username-password', initialType)}
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">账号别名</label>
595
- <input id="alias" name="alias" value="${escapeHtml(alias)}" placeholder="default-admin" data-i18n-placeholder="aliasPh">
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
- <section class="section" data-section="username-password">
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">请不要把密码、Token 或 Basic Authentication 内容粘贴到聊天窗口。</div>
677
- <p class="note" data-i18n="closeNote">提交成功后可以关闭本页面。</p>
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
- node.textContent = t(node.dataset.i18n)
701
- })
702
- document.querySelectorAll('[data-i18n-placeholder]').forEach((node) => {
703
- node.placeholder = t(node.dataset.i18nPlaceholder)
704
- })
705
- langButtons.forEach((button) => {
706
- button.setAttribute('aria-pressed', String(button.dataset.lang === activeLang))
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 && input.type !== 'hidden' && !['pdomain', 'registerName', 'mailCode'].includes(input.name)
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
- 'username-password': ['methodUsername', 'methodUsernameSub'],
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': ['methodRegister', 'methodRegisterSub']
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"><h1>Authorization failed</h1><p>${escapeHtml(error.message)}</p><p>Go back, check the values, and submit again.</p></body></html>`
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.3'
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 entering Basic Auth, X-Token, username/password, or email registration details. Secrets are stored only in this MCP process memory and must not be pasted into chat.',
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', 'username-password', 'email-password', 'register-email'])
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, username-password/email-password = SDK login, register-email = create admin account by email.'
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 ['data-lang="zh-CN"', 'data-lang="zh-TW"', 'data-lang="en"', 'data-method="basic"', 'data-method="x-token"']) {
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',