fix(密钥检测): 直连失败重试一次,超时/网络提示中文化
上游 ai.t8star.org 实测约 1/8 单请求完全不应答(主机 A 上 JDK 客户端 HTTP/1.1 与 HTTP/2 均复现),检测只有一次机会时用户会看到 「网络不可达:HttpTimeoutException: request timed out」。 - 传输层失败(超时/网络不可达)对直连最后一跳重试一次(800ms 间隔); 代理模块不重试——jikip 按提取次数计费,重试会多扣一次 - 失败文案中文化(新增 CODE_TIMEOUT),英文异常串只进服务端日志 - 探测提问改「你好」(最简一次调用) - 检测面板标明检测对象(配置密钥 sk-**** / 输入值(未保存)), 检测接口超时单独放宽(客户端 60s / 后台 180s),避免重试期间前端先超时
This commit is contained in:
@@ -66,9 +66,10 @@ export async function fetchUserSecretList(params: UserSecretQuery): Promise<Admi
|
||||
return unwrap<AdminUserSecretPage>(data)
|
||||
}
|
||||
|
||||
/** 立即检测该用户全部已配置项:POST /api/admin/user-secrets/{userId}/check */
|
||||
/** 立即检测该用户全部已配置项:POST /api/admin/user-secrets/{userId}/check
|
||||
* 逐模块检测(每项最多「首查 + 传输层失败重试一次」,最坏约 31s/项),故放宽超时到 180s。 */
|
||||
export async function checkUserSecret(userId: number) {
|
||||
const { data } = await http.post(`/api/admin/user-secrets/${userId}/check`)
|
||||
const { data } = await http.post(`/api/admin/user-secrets/${userId}/check`, undefined, { timeout: 180_000 })
|
||||
return unwrap<
|
||||
Array<{
|
||||
moduleKey: string
|
||||
|
||||
+89
-24
@@ -47,15 +47,22 @@ public class UserApiSecretCheckService {
|
||||
public static final String CODE_RATE_LIMITED = "rate_limited";
|
||||
public static final String CODE_SERVER_ERROR = "server_error";
|
||||
public static final String CODE_NETWORK_ERROR = "network_error";
|
||||
public static final String CODE_TIMEOUT = "timeout";
|
||||
public static final String CODE_PROVIDER_ERROR = "provider_error";
|
||||
public static final String CODE_INSUFFICIENT_BALANCE = "insufficient_balance";
|
||||
|
||||
/** 供应商欠费提示文案(前后端都按 code 识别展示)。 */
|
||||
public static final String INSUFFICIENT_BALANCE_MESSAGE = "代理服务商余额不足,请充值后重试";
|
||||
|
||||
/** 超时与网络不可达提示文案:面向用户展示,技术细节只进服务端日志。 */
|
||||
public static final String TIMEOUT_MESSAGE = "检测超时:上游服务响应超时,请稍后重试";
|
||||
public static final String NETWORK_ERROR_MESSAGE = "网络不可达:无法连接上游服务,请检查网络后重试";
|
||||
|
||||
private static final MediaType APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
|
||||
private static final int READ_TIMEOUT_MILLIS = 15_000;
|
||||
private static final int PROXY_READ_TIMEOUT_MILLIS = 10_000;
|
||||
/** 传输层失败后的重试间隔(单次检测总耗时上界:15s + 0.8s + 15s ≈ 31s)。 */
|
||||
private static final long RETRY_BACKOFF_MILLIS = 800L;
|
||||
private static final int MAX_RESPONSE_BYTES = 1024 * 1024;
|
||||
private static final int CHECK_MAX_TOKENS = 8;
|
||||
/** 代理探测目标:自家域名(http 无 CONNECT 依赖,兼容各类转发型代理)。 */
|
||||
@@ -72,7 +79,14 @@ public class UserApiSecretCheckService {
|
||||
|
||||
private volatile RestClient directClient;
|
||||
|
||||
/** 探测入口:代理模块走代理连通性探测,LLM 密钥模块优先经检测出口代理(若配置提取链接),代理网络不可达回退直连。 */
|
||||
/**
|
||||
* 探测入口:代理模块走代理连通性探测,LLM 密钥模块优先经检测出口代理(若配置提取链接),代理网络不可达回退直连。
|
||||
*
|
||||
* <p>2026-09-13:上游(ai.t8star.org)存在约 5%~10% 的单请求不响应(实测随机出现,
|
||||
* 与协议、连接复用无关),检测只有一次机会时用户会看到「检测超时」。
|
||||
* 因此对最后一跳(直连)在传输层失败(超时/网络不可达)时重试一次;
|
||||
* 代理模块不重试——jikip 按提取次数计费,重试会多扣一次。
|
||||
*/
|
||||
public CheckOutcome probe(UserSecretModule module, String plainApiKey) {
|
||||
if (module == UserSecretModule.PROXY) {
|
||||
return probeProxy(plainApiKey);
|
||||
@@ -86,10 +100,13 @@ public class UserApiSecretCheckService {
|
||||
}
|
||||
if (proxyUrl != null) {
|
||||
CheckOutcome viaProxy = probeOnce(module, plainApiKey, proxyUrl, true);
|
||||
if (CODE_NETWORK_ERROR.equals(viaProxy.code())) {
|
||||
log.warn("[user-secret][check] 经代理检测网络不可达 module={} proxy={},回退直连重试",
|
||||
module.key(), proxyUrl);
|
||||
if (isTransportFailure(viaProxy)) {
|
||||
log.warn("[user-secret][check] 经代理检测失败 module={} proxy={} code={},回退直连重试",
|
||||
module.key(), proxyUrl, viaProxy.code());
|
||||
CheckOutcome direct = probeOnce(module, plainApiKey, null, false);
|
||||
if (isTransportFailure(direct)) {
|
||||
direct = retryOnce(module, plainApiKey, direct);
|
||||
}
|
||||
return new CheckOutcome(
|
||||
direct.status(),
|
||||
direct.code(),
|
||||
@@ -99,7 +116,38 @@ public class UserApiSecretCheckService {
|
||||
}
|
||||
return viaProxy;
|
||||
}
|
||||
return probeOnce(module, plainApiKey, null, false);
|
||||
CheckOutcome direct = probeOnce(module, plainApiKey, null, false);
|
||||
return isTransportFailure(direct) ? retryOnce(module, plainApiKey, direct) : direct;
|
||||
}
|
||||
|
||||
/** 传输层失败重试一次(间隔 {@link #RETRY_BACKOFF_MILLIS}):抗上游偶发单请求不响应。 */
|
||||
private CheckOutcome retryOnce(UserSecretModule module, String plainApiKey, CheckOutcome failed) {
|
||||
log.warn("[user-secret][check] 直连探测失败 module={} code={} latency={}ms,{}ms 后重试一次",
|
||||
module.key(), failed.code(), failed.latencyMs(), RETRY_BACKOFF_MILLIS);
|
||||
sleepQuietly(RETRY_BACKOFF_MILLIS);
|
||||
CheckOutcome retried = probeOnce(module, plainApiKey, null, false);
|
||||
if (isTransportFailure(retried)) {
|
||||
log.warn("[user-secret][check] 重试仍失败 module={} code={} latency={}ms",
|
||||
module.key(), retried.code(), retried.latencyMs());
|
||||
} else {
|
||||
log.info("[user-secret][check] 重试成功 module={} status={} code={} latency={}ms",
|
||||
module.key(), retried.status(), retried.code(), retried.latencyMs());
|
||||
}
|
||||
return retried;
|
||||
}
|
||||
|
||||
/** 传输层失败(超时/网络不可达)才值得重试;HTTP 层的 401/429/5xx 等重试无意义。 */
|
||||
static boolean isTransportFailure(CheckOutcome outcome) {
|
||||
return outcome != null
|
||||
&& (CODE_NETWORK_ERROR.equals(outcome.code()) || CODE_TIMEOUT.equals(outcome.code()));
|
||||
}
|
||||
|
||||
private void sleepQuietly(long millis) {
|
||||
try {
|
||||
Thread.sleep(millis);
|
||||
} catch (InterruptedException interruptedException) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +180,7 @@ public class UserApiSecretCheckService {
|
||||
long latency = System.currentTimeMillis() - startMillis;
|
||||
log.warn("[user-secret][check] 代理提取接口不可达 latency={}ms err={}", latency, ex.getMessage());
|
||||
return new CheckOutcome(STATUS_FAILED, CODE_NETWORK_ERROR,
|
||||
"代理提取接口不可达:" + rootCauseMessage(ex), (int) latency, false);
|
||||
"代理提取接口无法访问,请检查提取链接或稍后重试", (int) latency, false);
|
||||
}
|
||||
long extractLatency = System.currentTimeMillis() - startMillis;
|
||||
if (isInsufficientBalance(body)) {
|
||||
@@ -172,7 +220,7 @@ public class UserApiSecretCheckService {
|
||||
long latency = System.currentTimeMillis() - startMillis;
|
||||
log.warn("[user-secret][check] 代理转发失败 latency={}ms err={}", latency, ex.getMessage());
|
||||
return new CheckOutcome(STATUS_FAILED, CODE_NETWORK_ERROR,
|
||||
"代理转发失败:" + rootCauseMessage(ex), (int) latency, true);
|
||||
"代理转发失败,请检查代理地址或稍后重试", (int) latency, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,13 +316,41 @@ public class UserApiSecretCheckService {
|
||||
return outcome;
|
||||
} catch (Exception ex) {
|
||||
long latency = System.currentTimeMillis() - startMillis;
|
||||
log.warn("[user-secret][check] {}探测异常 module={} latency={}ms err={}",
|
||||
viaText, module.key(), latency, ex.getMessage());
|
||||
return new CheckOutcome(STATUS_ERROR, CODE_NETWORK_ERROR,
|
||||
"网络不可达:" + rootCauseMessage(ex), (int) latency, viaProxy);
|
||||
CheckOutcome outcome = classifyTransportFailure(ex, (int) latency, viaProxy);
|
||||
log.warn("[user-secret][check] {}探测异常 module={} latency={}ms code={} err={}",
|
||||
viaText, module.key(), latency, outcome.code(), ex.getMessage());
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 传输层失败分类(超时 / 网络不可达):用户可见文案固定中文,
|
||||
* 具体异常类型与消息只进服务端日志,避免面板出现英文异常串。
|
||||
* package-private 供单测覆盖。
|
||||
*/
|
||||
CheckOutcome classifyTransportFailure(Throwable error, int latencyMs, boolean viaProxy) {
|
||||
if (isTimeout(error)) {
|
||||
return new CheckOutcome(STATUS_ERROR, CODE_TIMEOUT, TIMEOUT_MESSAGE, latencyMs, viaProxy);
|
||||
}
|
||||
return new CheckOutcome(STATUS_ERROR, CODE_NETWORK_ERROR, NETWORK_ERROR_MESSAGE, latencyMs, viaProxy);
|
||||
}
|
||||
|
||||
/** 超时判定:异常链上任一层为 JDK 请求超时或并发超时都算。 */
|
||||
private boolean isTimeout(Throwable error) {
|
||||
Throwable current = error;
|
||||
while (current != null) {
|
||||
if (current instanceof java.net.http.HttpTimeoutException
|
||||
|| current instanceof java.util.concurrent.TimeoutException) {
|
||||
return true;
|
||||
}
|
||||
if (current.getCause() == current) {
|
||||
break;
|
||||
}
|
||||
current = current.getCause();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 按 HTTP 状态码与响应体分类检测结果(package-private 供单测覆盖分类矩阵)。 */
|
||||
CheckOutcome classify(int statusCode, String body, int latencyMs, boolean viaProxy) {
|
||||
String responseBody = body == null ? "" : body;
|
||||
@@ -346,6 +422,7 @@ public class UserApiSecretCheckService {
|
||||
return body.contains("预扣费") && body.contains("额度");
|
||||
}
|
||||
|
||||
/** 检测请求体:最简一次对话调用(max_tokens 极小),只验证密钥可用与链路连通。 */
|
||||
private Map<String, Object> buildCheckBody(String model) {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("model", model);
|
||||
@@ -354,7 +431,7 @@ public class UserApiSecretCheckService {
|
||||
List<Map<String, Object>> messages = new ArrayList<>(1);
|
||||
Map<String, Object> userMessage = new LinkedHashMap<>();
|
||||
userMessage.put("role", "user");
|
||||
userMessage.put("content", "ping");
|
||||
userMessage.put("content", "你好");
|
||||
messages.add(userMessage);
|
||||
body.put("messages", messages);
|
||||
return body;
|
||||
@@ -427,18 +504,6 @@ public class UserApiSecretCheckService {
|
||||
return normalized.regionMatches(true, 0, "Bearer ", 0, 7) ? normalized.substring(7).trim() : normalized;
|
||||
}
|
||||
|
||||
private String rootCauseMessage(Throwable throwable) {
|
||||
Throwable current = throwable;
|
||||
while (current.getCause() != null && current.getCause() != current) {
|
||||
current = current.getCause();
|
||||
}
|
||||
String message = current.getMessage();
|
||||
if (message == null || message.isBlank()) {
|
||||
return current.getClass().getSimpleName();
|
||||
}
|
||||
return current.getClass().getSimpleName() + ": " + message;
|
||||
}
|
||||
|
||||
private String firstNonBlank(String preferred, String fallback) {
|
||||
return preferred == null || preferred.isBlank() ? fallback : preferred.trim();
|
||||
}
|
||||
|
||||
+61
@@ -137,6 +137,67 @@ class UserApiSecretCheckServiceTest {
|
||||
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// ===== 传输层失败分类(2026-09-13:面板不再出现英文异常串,超时单列 code)=====
|
||||
|
||||
@Test
|
||||
void transportTimeoutIsClassifiedAsTimeoutWithChineseMessage() {
|
||||
// 生产形态:ResourceAccessException 包装 HttpTimeoutException
|
||||
Exception failure = new org.springframework.web.client.ResourceAccessException(
|
||||
"I/O error on POST request", new java.net.http.HttpTimeoutException("request timed out"));
|
||||
UserApiSecretCheckService.CheckOutcome outcome = service.classifyTransportFailure(failure, 15001, false);
|
||||
|
||||
assertThat(outcome.status()).isEqualTo(UserApiSecretCheckService.STATUS_ERROR);
|
||||
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_TIMEOUT);
|
||||
assertThat(outcome.message()).isEqualTo(UserApiSecretCheckService.TIMEOUT_MESSAGE);
|
||||
assertThat(outcome.message()).doesNotContain("HttpTimeoutException");
|
||||
assertThat(outcome.latencyMs()).isEqualTo(15001);
|
||||
}
|
||||
|
||||
@Test
|
||||
void concurrentTimeoutExceptionAlsoClassifiedAsTimeout() {
|
||||
UserApiSecretCheckService.CheckOutcome outcome = service.classifyTransportFailure(
|
||||
new java.util.concurrent.TimeoutException("timed out"), 100, true);
|
||||
|
||||
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_TIMEOUT);
|
||||
assertThat(outcome.viaProxy()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void transportConnectFailureIsClassifiedAsNetworkError() {
|
||||
UserApiSecretCheckService.CheckOutcome outcome = service.classifyTransportFailure(
|
||||
new java.net.ConnectException("Connection refused"), 300, true);
|
||||
|
||||
assertThat(outcome.status()).isEqualTo(UserApiSecretCheckService.STATUS_ERROR);
|
||||
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_NETWORK_ERROR);
|
||||
assertThat(outcome.message()).isEqualTo(UserApiSecretCheckService.NETWORK_ERROR_MESSAGE);
|
||||
assertThat(outcome.viaProxy()).isTrue();
|
||||
assertThat(outcome.message()).doesNotContain("ConnectException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void transportNestedTimeoutInsideGenericIOExceptionIsTimeout() {
|
||||
Exception failure = new java.io.IOException("boom", new java.util.concurrent.TimeoutException("timed out"));
|
||||
UserApiSecretCheckService.CheckOutcome outcome = service.classifyTransportFailure(failure, 200, false);
|
||||
|
||||
assertThat(outcome.code()).isEqualTo(UserApiSecretCheckService.CODE_TIMEOUT);
|
||||
}
|
||||
|
||||
/** 只有传输层失败值得重试;HTTP 层的 401/429/5xx 重试无意义。 */
|
||||
@Test
|
||||
void onlyTransportFailuresAreRetryable() {
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("error", UserApiSecretCheckService.CODE_TIMEOUT))).isTrue();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("error", UserApiSecretCheckService.CODE_NETWORK_ERROR))).isTrue();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("error", UserApiSecretCheckService.CODE_RATE_LIMITED))).isFalse();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("error", UserApiSecretCheckService.CODE_SERVER_ERROR))).isFalse();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("failed", UserApiSecretCheckService.CODE_INVALID_KEY))).isFalse();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(outcome("passed", UserApiSecretCheckService.CODE_OK))).isFalse();
|
||||
assertThat(UserApiSecretCheckService.isTransportFailure(null)).isFalse();
|
||||
}
|
||||
|
||||
private static UserApiSecretCheckService.CheckOutcome outcome(String status, String code) {
|
||||
return new UserApiSecretCheckService.CheckOutcome(status, code, "msg", 100, false);
|
||||
}
|
||||
|
||||
// ===== 代理配置探测(提取链接语义;2026-09-13 修复「直连自家站点假通过」)=====
|
||||
|
||||
@Test
|
||||
|
||||
@@ -71,11 +71,13 @@ export function deleteMyApiSecret(moduleKey: string) {
|
||||
}
|
||||
|
||||
/** value 非空时检测输入值(不落库);为空时检测服务端已存密钥并把结果落库。 */
|
||||
/** 检测连通性:服务端最多「首查 + 传输层失败重试一次」,最坏约 31s,故单独放宽超时。 */
|
||||
export function checkMyApiSecret(moduleKey: string, value?: string) {
|
||||
return unwrapJavaResponse(
|
||||
post<JavaApiResponse<UserApiSecretCheckResult>, { value?: string }>(
|
||||
buildJavaUrl(API_ENDPOINTS.userSecret.check.replace('{moduleKey}', encodeURIComponent(moduleKey))),
|
||||
{ value },
|
||||
{ timeout: 60000 },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
:disabled="busy || moduleStates[module.moduleKey].checking"
|
||||
@click="runCheck(module.moduleKey as ApiSecretModuleKey)"
|
||||
>
|
||||
{{ moduleStates[module.moduleKey].checking ? '检测中...' : (moduleStates[module.moduleKey].input.trim() ? '检测输入值' : '检测已存密钥') }}
|
||||
{{ moduleStates[module.moduleKey].checking ? '检测中...' : (moduleStates[module.moduleKey].input.trim() ? '检测输入值(未保存)' : '检测配置密钥') }}
|
||||
</button>
|
||||
<span class="check-result" :class="resultClassOf(module.moduleKey)">
|
||||
{{ statusTextOf(module.moduleKey) }}
|
||||
@@ -93,7 +93,7 @@
|
||||
:disabled="!proxyReady || busy || proxyChecking"
|
||||
@click="runProxyCheck"
|
||||
>
|
||||
{{ proxyChecking ? '检测中...' : (proxyUrl.trim() && proxyDirty ? '检测输入地址' : '检测已存代理') }}
|
||||
{{ proxyChecking ? '检测中...' : (proxyUrl.trim() && proxyDirty ? '检测输入地址(未保存)' : '检测配置代理') }}
|
||||
</button>
|
||||
<span class="check-result" :class="proxyResultClass">
|
||||
{{ proxyStatusText }}
|
||||
@@ -147,13 +147,15 @@ type ModuleState = {
|
||||
checking: boolean
|
||||
result: UserApiSecretCheckResult | null
|
||||
error: string
|
||||
/** 最近一次检测的来源:saved=密钥配置里已保存的密钥,input=输入框里的未保存值。 */
|
||||
lastCheckSource: 'saved' | 'input' | ''
|
||||
}
|
||||
|
||||
const modules = ref(listApiSecretModules())
|
||||
const moduleStates = reactive<Record<string, ModuleState>>({})
|
||||
// 首次渲染即需可读:按模块清单预初始化状态(后续 refreshSnapshots 兜底补齐)
|
||||
for (const module of modules.value) {
|
||||
moduleStates[module.moduleKey] = { input: '', checking: false, result: null, error: '' }
|
||||
moduleStates[module.moduleKey] = { input: '', checking: false, result: null, error: '', lastCheckSource: '' }
|
||||
}
|
||||
const snapshots = ref<Record<string, ApiSecretSnapshot>>({})
|
||||
const busy = ref(false)
|
||||
@@ -240,7 +242,7 @@ const balanceText = computed(() => {
|
||||
|
||||
function ensureModuleState(moduleKey: string) {
|
||||
if (!moduleStates[moduleKey]) {
|
||||
moduleStates[moduleKey] = { input: '', checking: false, result: null, error: '' }
|
||||
moduleStates[moduleKey] = { input: '', checking: false, result: null, error: '', lastCheckSource: '' }
|
||||
}
|
||||
return moduleStates[moduleKey]
|
||||
}
|
||||
@@ -288,13 +290,18 @@ function formatTime(millis: number | null) {
|
||||
|
||||
function statusTextOf(moduleKey: string) {
|
||||
const state = ensureModuleState(moduleKey)
|
||||
if (state.checking) return '正在检测...'
|
||||
if (state.checking) return state.input.trim() ? '正在检测输入值...' : '正在检测配置密钥...'
|
||||
if (state.result) {
|
||||
const latency = state.result.checkLatencyMs != null ? `(${state.result.checkLatencyMs}ms)` : ''
|
||||
const suffix = state.result.viaProxy === true ? ' · 经代理' : ''
|
||||
if (state.result.checkStatus === 'passed') return `检测通过${latency}${suffix}`
|
||||
if (state.result.checkCode === 'insufficient_balance') return `欠费:${state.result.checkMessage || '代理服务商余额不足'}${suffix}`
|
||||
return `${state.result.checkMessage || '检测失败'}${suffix}`
|
||||
const snapshot = snapshotOf(moduleKey)
|
||||
// 明确展示本次检测的对象:输入值(未保存)或该用户密钥配置里保存的密钥(脱敏)
|
||||
const prefix = state.lastCheckSource === 'input'
|
||||
? '输入值(未保存):'
|
||||
: `配置密钥${snapshot.masked ? ` ${snapshot.masked}` : ''}:`
|
||||
if (state.result.checkStatus === 'passed') return `${prefix}检测通过${latency}${suffix}`
|
||||
if (state.result.checkCode === 'insufficient_balance') return `${prefix}欠费:${state.result.checkMessage || '代理服务商余额不足'}${suffix}`
|
||||
return `${prefix}${state.result.checkMessage || '检测失败'}${suffix}`
|
||||
}
|
||||
if (state.error) return state.error
|
||||
const snapshot = snapshotOf(moduleKey)
|
||||
@@ -302,7 +309,8 @@ function statusTextOf(moduleKey: string) {
|
||||
const status = CHECK_STATUS_TEXT[snapshot.checkStatus] || '已保存'
|
||||
const time = formatTime(snapshot.checkedAt)
|
||||
const message = snapshot.checkStatus === 'unknown' ? '' : `:${snapshot.checkMessage || ''}`
|
||||
return `${status}${message}${time ? ` · ${time}` : ''}`
|
||||
const masked = snapshot.masked ? `配置密钥 ${snapshot.masked} · ` : ''
|
||||
return `${masked}${status}${message}${time ? ` · ${time}` : ''}`
|
||||
}
|
||||
|
||||
function resultClassOf(moduleKey: string) {
|
||||
@@ -322,7 +330,9 @@ async function runCheck(moduleKey: ApiSecretModuleKey) {
|
||||
state.error = ''
|
||||
state.result = null
|
||||
const inputValue = state.input.trim()
|
||||
state.lastCheckSource = inputValue ? 'input' : 'saved'
|
||||
try {
|
||||
// 不传 override 时服务端检测的是当前登录用户在密钥配置里保存的密钥(按 uid 绑定)
|
||||
const result = await checkApiSecret(moduleKey, inputValue || undefined)
|
||||
state.result = result
|
||||
if (result.checkStatus === 'failed') {
|
||||
@@ -330,7 +340,7 @@ async function runCheck(moduleKey: ApiSecretModuleKey) {
|
||||
} else if (result.checkStatus === 'error') {
|
||||
ElMessage.warning(result.checkMessage || '暂时无法判定密钥有效性')
|
||||
} else {
|
||||
ElMessage.success(inputValue ? '输入值检测通过(未保存)' : '密钥检测通过')
|
||||
ElMessage.success(inputValue ? '输入值检测通过(未保存)' : '配置密钥检测通过')
|
||||
}
|
||||
} catch (error) {
|
||||
state.error = error instanceof Error ? error.message : '检测失败'
|
||||
|
||||
Reference in New Issue
Block a user