feat(密钥管理): 列表按用户聚合三字段列 + 代理配置服务端上报

- Java:后台列表一行一用户(货源查询密钥/外观专利密钥/代理设置),行级状态三项全通过才算通过;检测/清空改为按用户;搜索仅按用户名(去 UID);新增代理检测(经用户代理请求自家域名)与代理掩码(隐去账密)
- 后台前端:三字段列改版 + 行级状态筛选 + 用户名筛选
- 桌面前端:登录加载密钥时补报本地代理(只填空缺不覆盖)、保存/清空代理实时上报
This commit is contained in:
2026-09-13 10:47:19 +08:00
parent 82a782550e
commit b7d4d325e0
14 changed files with 665 additions and 226 deletions
@@ -2,8 +2,8 @@ import { del, get, post, put, type JavaApiResponse, unwrapJavaResponse } from '.
import { buildJavaUrl } from '../../url.ts'
import { API_ENDPOINTS } from '../../endpoints.ts'
/** 用户密钥模块 key:与服务端 UserSecretModule 枚举一致。 */
export type ApiSecretModuleKey = 'appearance-patent' | 'similar-asin'
/** 用户密钥模块 key:与服务端 UserSecretModule 枚举一致(proxy=代理设置,由设置面板单独区块管理)。 */
export type ApiSecretModuleKey = 'appearance-patent' | 'similar-asin' | 'proxy'
/** 连通性状态:unknown=未检测出结果(拦截)/ passed=通过 / failed=密钥无效(拦截)/ error=无法判定(放行)。 */
export type ApiSecretCheckStatus = 'unknown' | 'passed' | 'failed' | 'error'
@@ -115,6 +115,7 @@ import {
type UserApiSecretCheckResult,
} from '@/shared/utils/api-secret-store'
import { getPywebviewApi, type ProxyMode, type DesktopConfigUpdate } from '@/shared/bridges/pywebview'
import { deleteMyApiSecret, putMyApiSecret } from '@/shared/api/types/modules/user-secret'
const props = withDefaults(
defineProps<{
@@ -352,6 +353,21 @@ async function loadProxyConfig() {
}
}
/** 代理配置上报服务端(后台密钥管理观测/检测):有地址则保存、清空则删除;失败不阻断本地保存。 */
async function syncProxyToServer(url: string) {
if (proxyUserId() === '0') return
try {
if (url) {
await putMyApiSecret('proxy', url)
} else {
await deleteMyApiSecret('proxy')
}
} catch (error) {
console.warn('[api-secret] 代理配置上报失败(不影响本地任务):', error)
ElMessage.warning('代理已保存到本地,但上报后台失败')
}
}
async function loadBalance() {
balanceLoading.value = true
try {
@@ -404,6 +420,7 @@ async function saveAll(options: { requireAll?: boolean } = {}): Promise<boolean>
await api.save_config(userProxyPatch(nextProxyUrl, proxyMode.value) as DesktopConfigUpdate)
proxyUrl.value = nextProxyUrl
proxyDirty.value = false
await syncProxyToServer(nextProxyUrl)
}
// 保存成功的模块立即自动检测,结果直接反映在卡片状态行
@@ -342,6 +342,7 @@ async function doLoad(): Promise<ApiSecretLoadState> {
}
notify()
}
await tryBackfillProxyToServer()
return computeApiSecretsComplete() ? 'complete' : 'incomplete'
} catch (error) {
console.warn('[api-secret] 服务端密钥拉取失败,本次按未知处理(不阻断使用):', error)
@@ -388,6 +389,37 @@ async function tryMigrateLocalSecrets(serverItems: UserApiSecretItem[]): Promise
}
}
/**
* 本地代理配置补报服务端(只写空缺、不覆盖;后台密钥管理观测/检测用)。
* 非桌面环境、未登录、无本地代理时静默跳过;失败静默,下次加载重试。
* 桥模块动态导入:node 测试环境不触发(currentUid 为 0 时直接返回),避免加载浏览器侧依赖。
*/
async function tryBackfillProxyToServer(): Promise<void> {
try {
if (currentUid() === '0') return
const { getPywebviewApi } = await import('../bridges/pywebview.ts')
const api = getPywebviewApi()
if (!api?.read_config) return
const config = (await api.read_config()) as Record<string, unknown> | null
const url = readLocalProxyUrl(config)
if (!url) return
await migrateMyApiSecrets([{ moduleKey: 'proxy', value: url }])
} catch (error) {
console.warn('[api-secret] 代理配置补报失败(下次加载重试):', error)
}
}
/** 读取当前登录用户的本地代理地址:proxy_users[uid] 优先,未登录回退全局 proxy_url。 */
function readLocalProxyUrl(config: Record<string, unknown> | null | undefined): string {
const uid = currentUid()
if (uid !== '0') {
const users = (config?.proxy_users ?? {}) as Record<string, unknown>
const own = (users[uid] ?? {}) as Record<string, unknown>
return typeof own.proxy_url === 'string' ? own.proxy_url.trim() : ''
}
return typeof config?.proxy_url === 'string' ? config.proxy_url.trim() : ''
}
/** 保存密钥到服务端;成功后刷新本地缓存(失败不写本地,避免本地有值服务端没有的假象)。 */
export async function saveApiSecret(moduleKey: ApiSecretModuleKey, value: string): Promise<ApiSecretSnapshot> {
const trimmed = value.trim()