dsh-scnet-connect 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -0
- package/lib/client.js +5 -0
- package/lib/index.js +8 -1
- package/lib/scnet-login.js +46 -15
- package/lib/scnet-platform.js +60 -3
- package/lib/scnet-web-login.js +113 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,6 +141,27 @@ dsh plugin --profile desktop add link:.
|
|
|
141
141
|
|
|
142
142
|
登录成功后插件会自动换取模型 Key,无需其他操作。
|
|
143
143
|
|
|
144
|
+
**没有 SCNet 账号也能用。** 插件会根据平台返回的「该手机号是否已注册」自动选路:
|
|
145
|
+
|
|
146
|
+
| 手机号状态 | 插件行为 |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| 已注册 | 走短信快捷登录,直接进入 |
|
|
149
|
+
| 未注册 | **自动为你注册一个 SCNet 账号**,然后继续登录 |
|
|
150
|
+
|
|
151
|
+
注册由平台完成(等同于你在官网用手机号注册),插件只负责发起与跟随跳转。
|
|
152
|
+
完成后设置页会提示「登录成功,已为你自动注册 SCNet 账号」,不会静默建号。
|
|
153
|
+
|
|
154
|
+
新账号第一次换取模型 Key 时,平台会回「模型服务尚未开通」;插件会自动调用
|
|
155
|
+
平台的 `account/config/authorize` 完成开通再重试,全程无需你去控制台操作。
|
|
156
|
+
|
|
157
|
+
> **如果提示「平台把这次提交当成了账号密码登录」**:说明平台把这次短信提交
|
|
158
|
+
> 落到了它的密码分支。请点「刷新验证码」重新获取图形验证码,再重新发送一次
|
|
159
|
+
> 短信码重试。插件会把平台返回的原始码与原文记进
|
|
160
|
+
> `$DSH_HOME/scnet-connect-status.json` 的 `login.lastFailure`,
|
|
161
|
+
> 方便反馈问题时定位。
|
|
162
|
+
>
|
|
163
|
+
> 登录页面的报错一律显示中文;平台原始英文文案只进诊断文件,不会出现在界面上。
|
|
164
|
+
|
|
144
165
|
### 方式二:用本机 SCNet 客户端
|
|
145
166
|
|
|
146
167
|
先装好 SCNet 客户端并登录,正常发起一次对话(让客户端把凭据落到本地)。
|
package/lib/client.js
CHANGED
|
@@ -558,6 +558,11 @@ window.__ModuleLoader__.load({
|
|
|
558
558
|
setSmsCode('');
|
|
559
559
|
setSmsSent(false);
|
|
560
560
|
setCooldown(0);
|
|
561
|
+
// 未注册手机号走注册通道:账号是这次顺带建出来的,必须告知用户,
|
|
562
|
+
// 否则他只会疑惑「我没注册过,怎么就能用了」。
|
|
563
|
+
if (result.via === 'register') {
|
|
564
|
+
setNotice({ kind: 'ok', text: '登录成功,已为你自动注册 SCNet 账号' });
|
|
565
|
+
}
|
|
561
566
|
} else if (result && result.error) {
|
|
562
567
|
setNotice({ kind: 'error', text: result.error });
|
|
563
568
|
}
|
package/lib/index.js
CHANGED
|
@@ -676,6 +676,10 @@ export async function apply(ctx, config) {
|
|
|
676
676
|
lastValidatedAt: loginState.lastValidatedAt,
|
|
677
677
|
pending: loginState.pending,
|
|
678
678
|
error: loginState.error,
|
|
679
|
+
// 平台原始失败信息(码 + 原文)。界面只用中文提示,这里是排查依据:
|
|
680
|
+
// 「短信登录却回 Username or password is incorrect」这类问题,
|
|
681
|
+
// 没有原始码就只能靠猜。
|
|
682
|
+
lastFailure: loginState.lastFailure,
|
|
679
683
|
loginPageUrl: loginState.loginPageUrl,
|
|
680
684
|
sessionFile: login.sessionFilePath(),
|
|
681
685
|
// 现读,不沿用启动时的快照:用户中途装了客户端也要如实反映。
|
|
@@ -904,7 +908,10 @@ export async function apply(ctx, config) {
|
|
|
904
908
|
await catalog.refresh();
|
|
905
909
|
rebuild();
|
|
906
910
|
}
|
|
907
|
-
await loginResponse(
|
|
911
|
+
await loginResponse(
|
|
912
|
+
// `via` 会把「这次是登录还是顺手注册了新账号」带到设置页。
|
|
913
|
+
result.ok ? { ok: true, via: result.via } : { ok: false, error: result.error },
|
|
914
|
+
)(res);
|
|
908
915
|
}),
|
|
909
916
|
);
|
|
910
917
|
|
package/lib/scnet-login.js
CHANGED
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
sessionFilePath,
|
|
31
31
|
webLoginUrl,
|
|
32
32
|
} from './scnet-platform.js';
|
|
33
|
-
import { createWebLogin, mapLoginError } from './scnet-web-login.js';
|
|
33
|
+
import { createWebLogin, mapLoginError, translatePlatformMessage } from './scnet-web-login.js';
|
|
34
34
|
import { maskKey } from './scnet-credential.js';
|
|
35
35
|
|
|
36
36
|
/** 平台会话换来的 key 缓存 6 天;过期或被清空时自动重换。 */
|
|
@@ -99,6 +99,13 @@ export function createLoginManager(options = {}) {
|
|
|
99
99
|
let pending;
|
|
100
100
|
let phase = session ? 'authorized' : 'idle';
|
|
101
101
|
let lastError;
|
|
102
|
+
/**
|
|
103
|
+
* 最近一次登录失败的原始码与原文(诊断用)。
|
|
104
|
+
*
|
|
105
|
+
* 界面只显示中文提示;但「平台到底回了什么」是排查这类
|
|
106
|
+
* 「明明是短信登录却收到密码错误」问题的唯一线索,必须留下来。
|
|
107
|
+
*/
|
|
108
|
+
let lastFailure;
|
|
102
109
|
let inflight;
|
|
103
110
|
let lastValidatedAt = 0;
|
|
104
111
|
/**
|
|
@@ -123,6 +130,8 @@ export function createLoginManager(options = {}) {
|
|
|
123
130
|
lastValidatedAt: lastValidatedAt ? new Date(lastValidatedAt).toISOString() : undefined,
|
|
124
131
|
pending: pending ? { startedAt: pending.startedAt, expiresAt: pending.expiresAt } : undefined,
|
|
125
132
|
error: lastError,
|
|
133
|
+
/** 原始失败信息(码 + 平台原文),供诊断;不含任何凭据。 */
|
|
134
|
+
lastFailure,
|
|
126
135
|
loginPageUrl: webLoginUrl(),
|
|
127
136
|
};
|
|
128
137
|
}
|
|
@@ -154,10 +163,17 @@ export function createLoginManager(options = {}) {
|
|
|
154
163
|
return { ok: false, error: reason, ...snapshot() };
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
/**
|
|
158
|
-
|
|
166
|
+
/**
|
|
167
|
+
* 会话到手后立刻换 API Key。失败时保留会话(用户不必重新登录)。
|
|
168
|
+
*
|
|
169
|
+
* @param {'register' | 'login'} [via] 这条会话是「自动注册」还是「登录」拿到的
|
|
170
|
+
*/
|
|
171
|
+
async function finishAuthorization(raw, signal, via) {
|
|
159
172
|
const startedAt = epoch;
|
|
160
|
-
|
|
173
|
+
// 带上 accountId:新账号首次换 key 会回「模型服务尚未开通」,
|
|
174
|
+
// ensureApiKey 需要用它在平台侧开通服务后重试。
|
|
175
|
+
const accountId = raw.account?.id ?? raw.account?.accountId ?? raw.account?.userId;
|
|
176
|
+
const keyed = await ensureApiKey({ cookies: raw.cookies, accountId, signal });
|
|
161
177
|
// 换 key 期间用户又点了退出/重新登录:这次结果不再代表当前意图。
|
|
162
178
|
if (startedAt !== epoch) return { ok: false, error: '登录已被取消', ...snapshot() };
|
|
163
179
|
if (!keyed.ok) {
|
|
@@ -176,15 +192,15 @@ export function createLoginManager(options = {}) {
|
|
|
176
192
|
});
|
|
177
193
|
lastValidatedAt = now.getTime();
|
|
178
194
|
logger.info(
|
|
179
|
-
`免客户端登录完成:${raw.account?.userName || raw.account?.userId || '未知账号'}
|
|
180
|
-
'短信登录'
|
|
195
|
+
`免客户端登录完成:${raw.account?.userName || raw.account?.userId || '未知账号'}(${
|
|
196
|
+
via === 'register' ? '短信登录 · 已自动注册新账号' : '短信登录'
|
|
181
197
|
},key=${maskKey(keyed.key)})`,
|
|
182
198
|
);
|
|
183
199
|
return { ok: true, ...snapshot() };
|
|
184
200
|
}
|
|
185
201
|
|
|
186
202
|
/** 把任意会话来源收敛成「验证身份 → 换 key」。 */
|
|
187
|
-
async function authorizeFromCookies(cookies, source, signal) {
|
|
203
|
+
async function authorizeFromCookies(cookies, source, signal, via) {
|
|
188
204
|
const verified = await resolveSession({ cookies, signal });
|
|
189
205
|
if (!verified.ok) {
|
|
190
206
|
return fail(
|
|
@@ -201,6 +217,7 @@ export function createLoginManager(options = {}) {
|
|
|
201
217
|
source,
|
|
202
218
|
},
|
|
203
219
|
signal,
|
|
220
|
+
via,
|
|
204
221
|
);
|
|
205
222
|
}
|
|
206
223
|
|
|
@@ -234,7 +251,9 @@ export function createLoginManager(options = {}) {
|
|
|
234
251
|
return undefined;
|
|
235
252
|
}
|
|
236
253
|
lastValidatedAt = Date.now();
|
|
237
|
-
const
|
|
254
|
+
const renewId =
|
|
255
|
+
verified.user?.id ?? verified.user?.accountId ?? session.account?.id ?? session.account?.accountId;
|
|
256
|
+
const keyed = await ensureApiKey({ cookies: verified.cookies, accountId: renewId, signal });
|
|
238
257
|
if (startedAt !== epoch) return undefined;
|
|
239
258
|
if (!keyed.ok) {
|
|
240
259
|
phase = 'failed';
|
|
@@ -296,17 +315,29 @@ export function createLoginManager(options = {}) {
|
|
|
296
315
|
mapped === 'captcha-invalid'
|
|
297
316
|
? '短信验证码不正确或已失效,请重新获取'
|
|
298
317
|
: mapped === 'auth-failed'
|
|
299
|
-
? '手机号或验证码不正确'
|
|
300
|
-
: mapped === '
|
|
301
|
-
? '
|
|
302
|
-
: mapped === '
|
|
303
|
-
? '
|
|
304
|
-
:
|
|
318
|
+
? translatePlatformMessage(result.message) || '手机号或验证码不正确'
|
|
319
|
+
: mapped === 'user-not-exist'
|
|
320
|
+
? '该手机号尚未注册 SCNet 账号,请先在平台注册'
|
|
321
|
+
: mapped === 'invalid-params'
|
|
322
|
+
? '参数不完整,请检查手机号/验证码'
|
|
323
|
+
: mapped === 'too-many-attempts'
|
|
324
|
+
? '尝试次数过多,请稍后再试'
|
|
325
|
+
: mapped === 'server-busy'
|
|
326
|
+
? '平台繁忙,请稍后重试'
|
|
327
|
+
// 兜底也要说人话:平台原文多是英文,不能直接甩给用户。
|
|
328
|
+
: translatePlatformMessage(result.message) || '登录失败';
|
|
329
|
+
// 记下原始码与原文,供状态文件/诊断用(界面只显示上面的中文)。
|
|
330
|
+
lastFailure = {
|
|
331
|
+
code: result.code !== undefined ? String(result.code) : undefined,
|
|
332
|
+
rawMessage: result.message,
|
|
333
|
+
reason: result.reason,
|
|
334
|
+
at: new Date().toISOString(),
|
|
335
|
+
};
|
|
305
336
|
// 验证码/风控状态可能需要重开表单。
|
|
306
337
|
if (mapped === 'too-many-attempts' || mapped === 'captcha-invalid') webLogin = undefined;
|
|
307
338
|
return fail(message);
|
|
308
339
|
}
|
|
309
|
-
return authorizeFromCookies(result.cookies, 'web', form.signal);
|
|
340
|
+
return authorizeFromCookies(result.cookies, 'web', form.signal, result.via);
|
|
310
341
|
},
|
|
311
342
|
|
|
312
343
|
/** 平台会话是否还活着。 */
|
package/lib/scnet-platform.js
CHANGED
|
@@ -198,19 +198,51 @@ export async function resolveSession(options = {}) {
|
|
|
198
198
|
* 走「活动 Key」端点(客户端刷新凭据时优先用它):同一账号拿到的 key 与客户端
|
|
199
199
|
* 落盘的是同一个,不会每次刷新都新建 key。
|
|
200
200
|
*
|
|
201
|
-
*
|
|
201
|
+
* **新账号要自动开通模型服务**:全新注册的账号调这个端点会得到
|
|
202
|
+
* `{"msg":"模型服务尚未开通"}`。平台客户端遇到它时会先调
|
|
203
|
+
* `/acx/llm/account/config/authorize` 开通,再重试 —— 这里照做,
|
|
204
|
+
* 否则「自动注册」只能走到「账号建好了但拿不到 key」,等于白注册。
|
|
205
|
+
*
|
|
206
|
+
* @param {{ cookies: any, accountId?: string, signal?: AbortSignal }} options
|
|
202
207
|
* @returns {Promise<{ ok: true, key: string, baseUrl?: string } | { ok: false, reason: string, message?: string }>}
|
|
203
208
|
*/
|
|
204
209
|
export async function ensureApiKey(options = {}) {
|
|
205
210
|
const cookies = normalizeCookies(options.cookies);
|
|
206
211
|
if (!cookies) return { ok: false, reason: 'missing-cookies' };
|
|
207
|
-
|
|
212
|
+
|
|
213
|
+
async function createOnce() {
|
|
208
214
|
const result = await platformRequest('/acx/llm/api-key/createActivityApiKey', {
|
|
209
215
|
method: 'POST',
|
|
210
216
|
cookies,
|
|
211
217
|
body: {},
|
|
212
218
|
signal: options.signal,
|
|
213
219
|
});
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
let result = await createOnce();
|
|
225
|
+
|
|
226
|
+
// 「模型服务尚未开通」→ 开通一次后重试。只重试一次,避免开通失败时死循环。
|
|
227
|
+
const firstMsg = String(result.body?.msg || result.body?.message || '');
|
|
228
|
+
if (result.status === 200 && /尚未开通|not\s*(been\s*)?(opened|enabled|activated)/i.test(firstMsg)) {
|
|
229
|
+
const accountId = options.accountId || (await resolveAccountId(cookies, options.signal));
|
|
230
|
+
if (accountId) {
|
|
231
|
+
try {
|
|
232
|
+
await platformRequest('/acx/llm/account/config/authorize', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
cookies,
|
|
235
|
+
// accountId 必须放 body:放 query 平台会回 API010006「无权限访问」。
|
|
236
|
+
body: { accountId },
|
|
237
|
+
signal: options.signal,
|
|
238
|
+
});
|
|
239
|
+
} catch {
|
|
240
|
+
// 开通失败就继续往下走,让调用方看到原始的「尚未开通」,而不是被这里吞掉。
|
|
241
|
+
}
|
|
242
|
+
result = await createOnce();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
214
246
|
if (result.status === 401) return { ok: false, reason: 'expired' };
|
|
215
247
|
if (result.status !== 200 || !result.body) {
|
|
216
248
|
const msg = result.body?.msg || result.body?.message || `HTTP ${result.status}`;
|
|
@@ -219,7 +251,13 @@ export async function ensureApiKey(options = {}) {
|
|
|
219
251
|
const data = unwrap(result.body);
|
|
220
252
|
const key = typeof data === 'string' ? data : data?.apiKey;
|
|
221
253
|
if (typeof key !== 'string' || !key.startsWith('sk-')) {
|
|
222
|
-
|
|
254
|
+
// 走到这里多半还是「模型服务尚未开通」一类,把平台原话带上,别只说「没返回 key」。
|
|
255
|
+
const msg = result.body?.msg || result.body?.message;
|
|
256
|
+
return {
|
|
257
|
+
ok: false,
|
|
258
|
+
reason: 'invalid-response',
|
|
259
|
+
message: msg ? `平台返回:${msg}` : '平台没有返回 sk- 开头的模型服务 Key',
|
|
260
|
+
};
|
|
223
261
|
}
|
|
224
262
|
return {
|
|
225
263
|
ok: true,
|
|
@@ -235,6 +273,25 @@ export async function ensureApiKey(options = {}) {
|
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
|
|
276
|
+
/**
|
|
277
|
+
* 取当前登录账号的 id(开通模型服务要用)。
|
|
278
|
+
*
|
|
279
|
+
* 失败返回 `undefined`:拿不到 id 时上层仍然会尝试换 key,只是跳过开通。
|
|
280
|
+
*/
|
|
281
|
+
async function resolveAccountId(cookies, signal) {
|
|
282
|
+
try {
|
|
283
|
+
const result = await platformRequest('/acx/user/users/current-user-info?includeToken=true&refresh=true', {
|
|
284
|
+
cookies,
|
|
285
|
+
signal,
|
|
286
|
+
});
|
|
287
|
+
const data = unwrap(result.body);
|
|
288
|
+
const id = data?.id ?? data?.accountId ?? data?.userId;
|
|
289
|
+
return id ? String(id) : undefined;
|
|
290
|
+
} catch {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
238
295
|
/** 让平台侧会话失效(尽力而为,失败不影响本地清理)。 */
|
|
239
296
|
export async function logout(options = {}) {
|
|
240
297
|
const cookies = normalizeCookies(options.cookies);
|
package/lib/scnet-web-login.js
CHANGED
|
@@ -177,6 +177,12 @@ export function createWebLogin(options = {}) {
|
|
|
177
177
|
const signal = options.signal;
|
|
178
178
|
/** 上一次发码用的手机号(提交时要与之一致)。 */
|
|
179
179
|
let pendingPhone;
|
|
180
|
+
/**
|
|
181
|
+
* 上一条短信码发给的号是否已注册(`sendSmsCode` 写入,`submit` 读取)。
|
|
182
|
+
*
|
|
183
|
+
* `undefined` = 还没发过码,不知道;`true`/`false` = 平台明确告知过。
|
|
184
|
+
*/
|
|
185
|
+
let pendingRegistered;
|
|
180
186
|
|
|
181
187
|
/** 抓一张图形验证码(平台在短信登录/风控时要求)。 */
|
|
182
188
|
async function captcha() {
|
|
@@ -192,7 +198,15 @@ export function createWebLogin(options = {}) {
|
|
|
192
198
|
/**
|
|
193
199
|
* 发送手机验证码。
|
|
194
200
|
*
|
|
195
|
-
* `
|
|
201
|
+
* 返回值里的 `registered` 是**后续分派通道的依据**,不能丢:
|
|
202
|
+
*
|
|
203
|
+
* code === '1004' → 该手机号已注册 → 提交时走 `/sso/auth/login.action`
|
|
204
|
+
* code === '0' → 该手机号未注册 → 提交时走 `/sso/registerByMobilephoneNum.action`
|
|
205
|
+
*
|
|
206
|
+
* 平台前端就是这么做的(`Login-*.js` 里 `T.value="phone"` / `"quick"`)。
|
|
207
|
+
* 早期版本把两者都当「成功」但**没记住区别**,对未注册号仍调登录接口,
|
|
208
|
+
* 平台查无此人 → `401 Username or password is incorrect` —— 用户看到的是
|
|
209
|
+
* 密码错误,实际原因却是「这个手机号没有账号」。
|
|
196
210
|
*/
|
|
197
211
|
async function sendSmsCode(phone, captchaCode) {
|
|
198
212
|
if (!isPlausiblePhone(phone)) return { ok: false, reason: 'invalid-phone', message: '手机号格式不对' };
|
|
@@ -220,8 +234,9 @@ export function createWebLogin(options = {}) {
|
|
|
220
234
|
const code = res.json ? String(res.json.code) : '';
|
|
221
235
|
if (res.json && SMS_OK_CODES.has(code)) {
|
|
222
236
|
pendingPhone = mobilephoneNum;
|
|
223
|
-
|
|
224
|
-
|
|
237
|
+
/** 记住这个号注册没注册,`submit()` 要用它选通道。 */
|
|
238
|
+
pendingRegistered = code === '1004';
|
|
239
|
+
return { ok: true, registered: pendingRegistered, code };
|
|
225
240
|
}
|
|
226
241
|
return {
|
|
227
242
|
ok: false,
|
|
@@ -245,20 +260,47 @@ export function createWebLogin(options = {}) {
|
|
|
245
260
|
return { ok: false, reason: 'invalid-params', message: '请填写短信验证码' };
|
|
246
261
|
}
|
|
247
262
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
/**
|
|
264
|
+
* 按发码时平台告知的注册状态选通道。
|
|
265
|
+
*
|
|
266
|
+
* 这一步是整套免客户端登录能否覆盖「新用户」的关键:
|
|
267
|
+
* · 已注册号 → `/sso/auth/login.action`(mode=2 快捷登录)
|
|
268
|
+
* · 未注册号 → `/sso/registerByMobilephoneNum.action`(注册即登录)
|
|
269
|
+
*
|
|
270
|
+
* 两者拿到的 `data` 都是带 ticket 的跳转地址,后续处理完全一致。
|
|
271
|
+
*
|
|
272
|
+
* `pendingRegistered` 为 `undefined`(没发过码,或用户直接提交)时退回登录
|
|
273
|
+
* 接口:平台对已注册号必然成功;对未注册号会回 401,届时错误信息已由
|
|
274
|
+
* `mapLoginError` + `translatePlatformMessage` 翻成可读中文。
|
|
275
|
+
*/
|
|
276
|
+
const viaRegister = pendingRegistered === false;
|
|
277
|
+
const path = viaRegister ? '/sso/registerByMobilephoneNum.action' : '/sso/auth/login.action';
|
|
278
|
+
|
|
279
|
+
const fields = viaRegister
|
|
280
|
+
? {
|
|
281
|
+
mobilephoneNum,
|
|
282
|
+
messageCode: form.smsCode,
|
|
283
|
+
// 注册接口把这几项当必填(缺 createBy 会 400)。
|
|
284
|
+
createBy: 'scnet',
|
|
285
|
+
siteCode: 'sacp',
|
|
286
|
+
invitationCode: '',
|
|
287
|
+
fullName: '',
|
|
288
|
+
institution: '',
|
|
289
|
+
service: serviceUrl(),
|
|
290
|
+
originalUrl: webLoginUrl(),
|
|
291
|
+
sensorsAnonId: '',
|
|
292
|
+
}
|
|
293
|
+
: {
|
|
294
|
+
service: serviceUrl(),
|
|
295
|
+
originalUrl: webLoginUrl(),
|
|
296
|
+
sensorsAnonId: '',
|
|
297
|
+
captcha: form.captcha || '',
|
|
298
|
+
mobilephoneNum,
|
|
299
|
+
messageCode: form.smsCode,
|
|
300
|
+
mode: '2',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const res = await webPost(jar, path, encodeBody(fields), { signal, timeoutMs: 25_000 });
|
|
262
304
|
|
|
263
305
|
if (!res.json) {
|
|
264
306
|
return { ok: false, reason: 'invalid-response', message: `HTTP ${res.status}` };
|
|
@@ -268,7 +310,9 @@ export function createWebLogin(options = {}) {
|
|
|
268
310
|
ok: false,
|
|
269
311
|
reason: mapLoginError(String(res.json.code), res.json.msg),
|
|
270
312
|
code: res.json.code,
|
|
271
|
-
message: res.json.msg || '登录失败',
|
|
313
|
+
message: res.json.msg || (viaRegister ? '注册失败' : '登录失败'),
|
|
314
|
+
/** 便于上层区分「注册通道失败」与「登录通道失败」。 */
|
|
315
|
+
via: viaRegister ? 'register' : 'login',
|
|
272
316
|
};
|
|
273
317
|
}
|
|
274
318
|
|
|
@@ -283,10 +327,12 @@ export function createWebLogin(options = {}) {
|
|
|
283
327
|
return {
|
|
284
328
|
ok: false,
|
|
285
329
|
reason: 'no-session-cookie',
|
|
286
|
-
message: '
|
|
330
|
+
message: '平台返回成功,但没有下发会话 Cookie(可能是风控拦截)。',
|
|
287
331
|
};
|
|
288
332
|
}
|
|
289
|
-
|
|
333
|
+
// 到此为止这个号一定已经在平台上存在了。
|
|
334
|
+
pendingRegistered = true;
|
|
335
|
+
return { ok: true, cookies, via: viaRegister ? 'register' : 'login' };
|
|
290
336
|
}
|
|
291
337
|
|
|
292
338
|
/** 跟随 ticket 跳转,收集平台下发的会话 Cookie。 */
|
|
@@ -361,15 +407,32 @@ export function mapSmsError(code, message) {
|
|
|
361
407
|
}
|
|
362
408
|
}
|
|
363
409
|
|
|
364
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* 登录错误码 → 插件判定(平台码表见网页 `Login-*.js` 的 `Ce` 表)。
|
|
412
|
+
*
|
|
413
|
+
* 平台对「短信验证码提交」的 `mode=2` 分支实测只会回 1009 / 1005 / 1004 / 3002;
|
|
414
|
+
* 但**密码**分支(`mode=0`)会回 `401 Username or password is incorrect`。
|
|
415
|
+
* v1.1.1 及更早版本的表里没有 `401`,于是它落到 `default` 被归成 `rejected`,
|
|
416
|
+
* 提示文案直接回退成平台那串英文原文 —— 用户在设置页看到
|
|
417
|
+
* 「Username or password is incorrect」却明明是在用短信登录,正是这个缺口。
|
|
418
|
+
*
|
|
419
|
+
* 因此这里补上 `401`/`1008` 分支,并给 `default` 加英文兜底识别:
|
|
420
|
+
* 不管平台以后新增什么码,都不该把英文原文直接甩给用户。
|
|
421
|
+
*/
|
|
365
422
|
export function mapLoginError(code, message) {
|
|
366
423
|
const text = String(message || '');
|
|
367
424
|
switch (code) {
|
|
425
|
+
case '401':
|
|
426
|
+
// 平台密码分支的「账号或密码错」。短信登录收到它说明提交没被当成 mode=2,
|
|
427
|
+
// 归到 auth-failed,由上层给出可操作的中文提示。
|
|
428
|
+
return 'auth-failed';
|
|
368
429
|
case '1004':
|
|
369
430
|
// 对登录来说 1004 不是错误;真出现时按「账号问题」提示,不要误导成失败。
|
|
370
431
|
return 'auth-failed';
|
|
371
432
|
case '1005':
|
|
372
433
|
return 'auth-failed';
|
|
434
|
+
case '1008':
|
|
435
|
+
return 'user-not-exist';
|
|
373
436
|
case '1009':
|
|
374
437
|
case '3002':
|
|
375
438
|
return 'captcha-invalid';
|
|
@@ -381,7 +444,36 @@ export function mapLoginError(code, message) {
|
|
|
381
444
|
return 'too-many-attempts';
|
|
382
445
|
default:
|
|
383
446
|
if (/too many|频繁|次数/i.test(text)) return 'too-many-attempts';
|
|
447
|
+
if (/username or password/i.test(text)) return 'auth-failed';
|
|
448
|
+
if (/user (not exist|does not exist)|not registered/i.test(text)) return 'user-not-exist';
|
|
449
|
+
if (/expired|过期/i.test(text)) return 'captcha-invalid';
|
|
384
450
|
if (/验证码|captcha/i.test(text)) return 'captcha-invalid';
|
|
385
451
|
return 'rejected';
|
|
386
452
|
}
|
|
387
453
|
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* 把平台英文原文翻成中文,保证设置页永远不出现英文报错。
|
|
457
|
+
*
|
|
458
|
+
* 平台很多接口的 `msg` 是英文(`Username or password is incorrect`、
|
|
459
|
+
* `Invalid parameters`…),而插件此前只对**已知码**给中文、
|
|
460
|
+
* 未知码直接把原文抛给用户。这里做最后一道兜底。
|
|
461
|
+
*
|
|
462
|
+
* @param {string} message 平台原始 msg
|
|
463
|
+
* @returns {string} 中文提示;无匹配时给出通用文案
|
|
464
|
+
*/
|
|
465
|
+
export function translatePlatformMessage(message) {
|
|
466
|
+
const text = String(message || '').trim();
|
|
467
|
+
if (!text) return '';
|
|
468
|
+
// 已经是中文就不用再翻。
|
|
469
|
+
if (/[\u4e00-\u9fa5]/.test(text)) return text;
|
|
470
|
+
if (/username or password/i.test(text)) return '平台把这次提交当成了「账号密码登录」,请重新获取短信验证码再试';
|
|
471
|
+
if (/invalid verification code|code has expired/i.test(text)) return '短信验证码不正确或已失效,请重新获取';
|
|
472
|
+
if (/invalid parameters/i.test(text)) return '提交参数不完整,请检查手机号与验证码';
|
|
473
|
+
if (/too often|too frequent|in 60 seconds/i.test(text)) return '发送太频繁,请稍后再试';
|
|
474
|
+
if (/virtual/i.test(text)) return '不支持虚拟号段';
|
|
475
|
+
if (/not exist|not registered/i.test(text)) return '该手机号尚未注册 SCNet 账号';
|
|
476
|
+
if (/too many/i.test(text)) return '尝试次数过多,请稍后再试';
|
|
477
|
+
if (/signature failed/i.test(text)) return '图形验证码签名校验失败,请刷新验证码';
|
|
478
|
+
return '平台拒绝了这次登录请求,请稍后重试';
|
|
479
|
+
}
|