mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 01:10:23 +08:00
feat: 为 Provider 认证添加代理配置支持
- 前端新增可折叠的代理配置字段组(URL/用户名/密码) - 更新 anyrouter/cubence/new-api/yescode 模板支持代理 - 后端 provider_ops 服务验证和数据获取时支持代理 - 移除 Dockerfile 中的 ca-certificates(回退之前改动)
This commit is contained in:
@@ -2,43 +2,32 @@
|
|||||||
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
# 构建命令: docker build -f Dockerfile.app -t aether-app:latest .
|
||||||
# 用于 GitHub Actions CI(官方源)
|
# 用于 GitHub Actions CI(官方源)
|
||||||
FROM aether-base:latest AS builder
|
FROM aether-base:latest AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制前端源码并构建
|
# 复制前端源码并构建
|
||||||
COPY frontend/ ./frontend/
|
COPY frontend/ ./frontend/
|
||||||
RUN cd frontend && npm run build
|
RUN cd frontend && npm run build
|
||||||
|
|
||||||
# ==================== 运行时镜像 ====================
|
# ==================== 运行时镜像 ====================
|
||||||
FROM python:3.12-slim
|
FROM python:3.12-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 运行时依赖(无 gcc/nodejs/npm)
|
# 运行时依赖(无 gcc/nodejs/npm)
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
nginx \
|
nginx \
|
||||||
supervisor \
|
supervisor \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 从 base 镜像复制 Python 包
|
# 从 base 镜像复制 Python 包
|
||||||
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
|
||||||
|
|
||||||
# 只复制需要的 Python 可执行文件
|
# 只复制需要的 Python 可执行文件
|
||||||
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
COPY --from=builder /usr/local/bin/gunicorn /usr/local/bin/
|
||||||
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
COPY --from=builder /usr/local/bin/uvicorn /usr/local/bin/
|
||||||
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
COPY --from=builder /usr/local/bin/alembic /usr/local/bin/
|
||||||
|
|
||||||
# 从 builder 阶段复制前端构建产物
|
# 从 builder 阶段复制前端构建产物
|
||||||
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
COPY --from=builder /app/frontend/dist /usr/share/nginx/html
|
||||||
|
|
||||||
# 复制后端代码
|
# 复制后端代码
|
||||||
COPY src/ ./src/
|
COPY src/ ./src/
|
||||||
COPY alembic.ini ./
|
COPY alembic.ini ./
|
||||||
COPY alembic/ ./alembic/
|
COPY alembic/ ./alembic/
|
||||||
|
|
||||||
# Nginx 配置模板
|
# Nginx 配置模板
|
||||||
# 智能处理 IP:有外层代理头就透传,没有就用直连 IP
|
# 智能处理 IP:有外层代理头就透传,没有就用直连 IP
|
||||||
RUN printf '%s\n' \
|
RUN printf '%s\n' \
|
||||||
@@ -119,7 +108,6 @@ RUN printf '%s\n' \
|
|||||||
' proxy_read_timeout 600s;' \
|
' proxy_read_timeout 600s;' \
|
||||||
' }' \
|
' }' \
|
||||||
'}' > /etc/nginx/sites-available/default.template
|
'}' > /etc/nginx/sites-available/default.template
|
||||||
|
|
||||||
# Supervisor 配置
|
# Supervisor 配置
|
||||||
RUN printf '%s\n' \
|
RUN printf '%s\n' \
|
||||||
'[supervisord]' \
|
'[supervisord]' \
|
||||||
@@ -144,14 +132,11 @@ RUN printf '%s\n' \
|
|||||||
'stderr_logfile=/dev/stderr' \
|
'stderr_logfile=/dev/stderr' \
|
||||||
'stderr_logfile_maxbytes=0' \
|
'stderr_logfile_maxbytes=0' \
|
||||||
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
'environment=PYTHONUNBUFFERED=1,PYTHONIOENCODING=utf-8,LANG=C.UTF-8,LC_ALL=C.UTF-8,DOCKER_CONTAINER=true' > /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
# 创建目录
|
# 创建目录
|
||||||
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
RUN mkdir -p /var/log/supervisor /app/logs /app/data
|
||||||
|
|
||||||
# 入口脚本(启动前执行迁移)
|
# 入口脚本(启动前执行迁移)
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# 环境变量
|
# 环境变量
|
||||||
ENV PYTHONUNBUFFERED=1 \
|
ENV PYTHONUNBUFFERED=1 \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
@@ -160,11 +145,8 @@ ENV PYTHONUNBUFFERED=1 \
|
|||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
PORT=8084 \
|
PORT=8084 \
|
||||||
GUNICORN_WORKERS=4
|
GUNICORN_WORKERS=4
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD curl -f http://localhost/health || exit 1
|
CMD curl -f http://localhost/health || exit 1
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
|
|||||||
supervisor \
|
supervisor \
|
||||||
libpq5 \
|
libpq5 \
|
||||||
curl \
|
curl \
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# 从 base 镜像复制 Python 包
|
# 从 base 镜像复制 Python 包
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ RUN apt-get update && apt-get install -y \
|
|||||||
gcc \
|
gcc \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Python 依赖
|
# Python 依赖
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ RUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.li
|
|||||||
gcc \
|
gcc \
|
||||||
nodejs \
|
nodejs \
|
||||||
npm \
|
npm \
|
||||||
ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# pip 镜像源
|
# pip 镜像源
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import type { AuthTemplate, AuthTemplateFieldGroup } from './types'
|
import type { AuthTemplate, AuthTemplateFieldGroup } from './types'
|
||||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||||
|
import { PROXY_FIELD_GROUP, buildProxyUrl, parseProxyUrl } from './types'
|
||||||
|
|
||||||
export const anyrouterTemplate: AuthTemplate = {
|
export const anyrouterTemplate: AuthTemplate = {
|
||||||
id: 'anyrouter',
|
id: 'anyrouter',
|
||||||
@@ -38,6 +39,7 @@ export const anyrouterTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
PROXY_FIELD_GROUP,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -49,7 +51,9 @@ export const anyrouterTemplate: AuthTemplate = {
|
|||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
connector: {
|
connector: {
|
||||||
auth_type: 'cookie',
|
auth_type: 'cookie',
|
||||||
config: {},
|
config: {
|
||||||
|
proxy: buildProxyUrl(formData),
|
||||||
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
session_cookie: formData.session_cookie,
|
session_cookie: formData.session_cookie,
|
||||||
},
|
},
|
||||||
@@ -60,9 +64,11 @@ export const anyrouterTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseConfig(config: any): Record<string, any> {
|
parseConfig(config: any): Record<string, any> {
|
||||||
|
const proxyData = parseProxyUrl(config?.connector?.config?.proxy)
|
||||||
return {
|
return {
|
||||||
base_url: config?.base_url || '',
|
base_url: config?.base_url || '',
|
||||||
session_cookie: config?.connector?.credentials?.session_cookie || '',
|
session_cookie: config?.connector?.credentials?.session_cookie || '',
|
||||||
|
...proxyData,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
||||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||||
|
import { PROXY_FIELD_GROUP, buildProxyUrl, parseProxyUrl } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化窗口限额显示(百分比格式)
|
* 格式化窗口限额显示(百分比格式)
|
||||||
@@ -78,6 +79,7 @@ export const cubenceTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
PROXY_FIELD_GROUP,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -89,7 +91,9 @@ export const cubenceTemplate: AuthTemplate = {
|
|||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
connector: {
|
connector: {
|
||||||
auth_type: 'cookie',
|
auth_type: 'cookie',
|
||||||
config: {},
|
config: {
|
||||||
|
proxy: buildProxyUrl(formData),
|
||||||
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
token_cookie: formData.token_cookie,
|
token_cookie: formData.token_cookie,
|
||||||
},
|
},
|
||||||
@@ -100,9 +104,11 @@ export const cubenceTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseConfig(config: any): Record<string, any> {
|
parseConfig(config: any): Record<string, any> {
|
||||||
|
const proxyData = parseProxyUrl(config?.connector?.config?.proxy)
|
||||||
return {
|
return {
|
||||||
base_url: config?.base_url || '',
|
base_url: config?.base_url || '',
|
||||||
token_cookie: config?.connector?.credentials?.token_cookie || '',
|
token_cookie: config?.connector?.credentials?.token_cookie || '',
|
||||||
|
...proxyData,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import type { AuthTemplate, AuthTemplateFieldGroup } from './types'
|
import type { AuthTemplate, AuthTemplateFieldGroup } from './types'
|
||||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||||
|
import { PROXY_FIELD_GROUP, buildProxyUrl, parseProxyUrl } from './types'
|
||||||
|
|
||||||
export const newApiTemplate: AuthTemplate = {
|
export const newApiTemplate: AuthTemplate = {
|
||||||
id: 'new_api',
|
id: 'new_api',
|
||||||
@@ -45,6 +46,7 @@ export const newApiTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
PROXY_FIELD_GROUP,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -58,6 +60,7 @@ export const newApiTemplate: AuthTemplate = {
|
|||||||
auth_type: 'api_key',
|
auth_type: 'api_key',
|
||||||
config: {
|
config: {
|
||||||
auth_method: 'bearer',
|
auth_method: 'bearer',
|
||||||
|
proxy: buildProxyUrl(formData),
|
||||||
},
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
api_key: formData.api_key,
|
api_key: formData.api_key,
|
||||||
@@ -70,10 +73,12 @@ export const newApiTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseConfig(config: any): Record<string, any> {
|
parseConfig(config: any): Record<string, any> {
|
||||||
|
const proxyData = parseProxyUrl(config?.connector?.config?.proxy)
|
||||||
return {
|
return {
|
||||||
base_url: config?.base_url || '',
|
base_url: config?.base_url || '',
|
||||||
api_key: config?.connector?.credentials?.api_key || '',
|
api_key: config?.connector?.credentials?.api_key || '',
|
||||||
user_id: config?.connector?.credentials?.user_id || '',
|
user_id: config?.connector?.credentials?.user_id || '',
|
||||||
|
...proxyData,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,14 @@ export interface AuthTemplateFieldGroup {
|
|||||||
title?: string
|
title?: string
|
||||||
/** 分组内的字段 */
|
/** 分组内的字段 */
|
||||||
fields: AuthTemplateField[]
|
fields: AuthTemplateField[]
|
||||||
|
/** 是否可折叠(默认展开) */
|
||||||
|
collapsible?: boolean
|
||||||
|
/** 折叠时的默认状态:true=展开,false=收起 */
|
||||||
|
defaultExpanded?: boolean
|
||||||
|
/** 是否有启用开关(与 collapsible 配合使用) */
|
||||||
|
hasToggle?: boolean
|
||||||
|
/** 启用开关对应的表单字段 key */
|
||||||
|
toggleKey?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,3 +149,134 @@ export interface AuthTemplateRegistry {
|
|||||||
/** 注册模板 */
|
/** 注册模板 */
|
||||||
register(template: AuthTemplate): void
|
register(template: AuthTemplate): void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 通用字段定义 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理地址字段
|
||||||
|
*/
|
||||||
|
export const PROXY_URL_FIELD: AuthTemplateField = {
|
||||||
|
key: 'proxy_url',
|
||||||
|
label: '代理地址',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'http://proxy:port 或 socks5://',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理用户名字段
|
||||||
|
*/
|
||||||
|
export const PROXY_USERNAME_FIELD: AuthTemplateField = {
|
||||||
|
key: 'proxy_username',
|
||||||
|
label: '用户名',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: '可选',
|
||||||
|
required: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理密码字段
|
||||||
|
*/
|
||||||
|
export const PROXY_PASSWORD_FIELD: AuthTemplateField = {
|
||||||
|
key: 'proxy_password',
|
||||||
|
label: '密码',
|
||||||
|
type: 'password',
|
||||||
|
placeholder: '可选',
|
||||||
|
required: false,
|
||||||
|
sensitive: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用代理配置字段组(可折叠,带启用开关)
|
||||||
|
*/
|
||||||
|
export const PROXY_FIELD_GROUP: AuthTemplateFieldGroup = {
|
||||||
|
title: '代理配置',
|
||||||
|
fields: [PROXY_URL_FIELD, PROXY_USERNAME_FIELD, PROXY_PASSWORD_FIELD],
|
||||||
|
collapsible: true,
|
||||||
|
defaultExpanded: false,
|
||||||
|
hasToggle: true,
|
||||||
|
toggleKey: 'proxy_enabled',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建代理 URL(包含认证信息)
|
||||||
|
*
|
||||||
|
* @param formData 表单数据
|
||||||
|
* @returns 完整的代理 URL,或 undefined
|
||||||
|
*/
|
||||||
|
export function buildProxyUrl(formData: Record<string, any>): string | undefined {
|
||||||
|
if (!formData.proxy_enabled || !formData.proxy_url) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const proxyUrl = formData.proxy_url.trim()
|
||||||
|
const username = formData.proxy_username?.trim()
|
||||||
|
const password = formData.proxy_password?.trim()
|
||||||
|
|
||||||
|
// 如果没有认证信息,直接返回 URL
|
||||||
|
if (!username) {
|
||||||
|
return proxyUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 URL 并添加认证信息
|
||||||
|
try {
|
||||||
|
const url = new URL(proxyUrl)
|
||||||
|
url.username = username
|
||||||
|
if (password) {
|
||||||
|
url.password = password
|
||||||
|
}
|
||||||
|
return url.toString()
|
||||||
|
} catch {
|
||||||
|
// URL 解析失败,尝试简单拼接
|
||||||
|
const protocol = proxyUrl.includes('://') ? proxyUrl.split('://')[0] : 'http'
|
||||||
|
const host = proxyUrl.includes('://') ? proxyUrl.split('://')[1] : proxyUrl
|
||||||
|
const auth = password ? `${username}:${password}` : username
|
||||||
|
return `${protocol}://${auth}@${host}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从代理 URL 解析表单数据
|
||||||
|
*
|
||||||
|
* @param proxyUrl 代理 URL
|
||||||
|
* @returns 表单数据
|
||||||
|
*/
|
||||||
|
export function parseProxyUrl(proxyUrl: string | undefined): Record<string, any> {
|
||||||
|
if (!proxyUrl) {
|
||||||
|
return {
|
||||||
|
proxy_enabled: false,
|
||||||
|
proxy_url: '',
|
||||||
|
proxy_username: '',
|
||||||
|
proxy_password: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = new URL(proxyUrl)
|
||||||
|
const username = url.username || ''
|
||||||
|
const password = url.password || ''
|
||||||
|
|
||||||
|
// 移除认证信息后的 URL
|
||||||
|
url.username = ''
|
||||||
|
url.password = ''
|
||||||
|
const cleanUrl = url.toString()
|
||||||
|
|
||||||
|
return {
|
||||||
|
proxy_enabled: true,
|
||||||
|
proxy_url: cleanUrl,
|
||||||
|
proxy_username: username,
|
||||||
|
proxy_password: password,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 解析失败,直接使用原始 URL
|
||||||
|
return {
|
||||||
|
proxy_enabled: true,
|
||||||
|
proxy_url: proxyUrl,
|
||||||
|
proxy_username: '',
|
||||||
|
proxy_password: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧的导出(已废弃,请使用 PROXY_FIELD_GROUP)
|
||||||
|
export const PROXY_FIELD: AuthTemplateField = PROXY_URL_FIELD
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
import type { AuthTemplate, AuthTemplateFieldGroup, BalanceExtraItem } from './types'
|
||||||
import type { SaveConfigRequest } from '@/api/providerOps'
|
import type { SaveConfigRequest } from '@/api/providerOps'
|
||||||
|
import { PROXY_FIELD_GROUP, buildProxyUrl, parseProxyUrl } from './types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化限额显示(百分比格式)
|
* 格式化限额显示(百分比格式)
|
||||||
@@ -51,6 +52,7 @@ export const yescodeTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
PROXY_FIELD_GROUP,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -62,7 +64,9 @@ export const yescodeTemplate: AuthTemplate = {
|
|||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
connector: {
|
connector: {
|
||||||
auth_type: 'cookie',
|
auth_type: 'cookie',
|
||||||
config: {},
|
config: {
|
||||||
|
proxy: buildProxyUrl(formData),
|
||||||
|
},
|
||||||
credentials: {
|
credentials: {
|
||||||
auth_cookie: formData.auth_cookie,
|
auth_cookie: formData.auth_cookie,
|
||||||
},
|
},
|
||||||
@@ -73,9 +77,11 @@ export const yescodeTemplate: AuthTemplate = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
parseConfig(config: any): Record<string, any> {
|
parseConfig(config: any): Record<string, any> {
|
||||||
|
const proxyData = parseProxyUrl(config?.connector?.config?.proxy)
|
||||||
return {
|
return {
|
||||||
base_url: config?.base_url || '',
|
base_url: config?.base_url || '',
|
||||||
auth_cookie: config?.connector?.credentials?.auth_cookie || '',
|
auth_cookie: config?.connector?.credentials?.auth_cookie || '',
|
||||||
|
...proxyData,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -48,80 +48,139 @@
|
|||||||
v-for="(group, groupIndex) in fieldGroups"
|
v-for="(group, groupIndex) in fieldGroups"
|
||||||
:key="groupIndex"
|
:key="groupIndex"
|
||||||
>
|
>
|
||||||
<!-- 分组标题 -->
|
<!-- 可折叠的分组(如代理配置) -->
|
||||||
<div
|
<div
|
||||||
v-if="group.title"
|
v-if="group.collapsible && group.hasToggle && group.toggleKey"
|
||||||
class="pt-2 text-sm font-medium text-muted-foreground"
|
|
||||||
>
|
|
||||||
{{ group.title }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 字段列表 -->
|
|
||||||
<div
|
|
||||||
v-for="field in group.fields"
|
|
||||||
:key="field.key"
|
|
||||||
class="space-y-2"
|
class="space-y-2"
|
||||||
>
|
>
|
||||||
<Label>
|
<!-- 标题栏:标题在左,开关在右(在卡片外) -->
|
||||||
{{ field.label }}
|
<div class="flex items-center justify-between">
|
||||||
<span
|
<span class="text-sm font-medium text-foreground">{{ group.title }}</span>
|
||||||
v-if="field.required"
|
<div class="flex items-center gap-2">
|
||||||
class="text-muted-foreground/70"
|
<span class="text-xs text-muted-foreground">启用代理</span>
|
||||||
>*</span>
|
<Switch
|
||||||
</Label>
|
:model-value="formData[group.toggleKey] || false"
|
||||||
|
@update:model-value="formData[group.toggleKey] = $event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 文本输入 -->
|
<!-- 展开内容(卡片) -->
|
||||||
<Input
|
<div
|
||||||
v-if="field.type === 'text'"
|
v-if="formData[group.toggleKey]"
|
||||||
v-model="formData[field.key]"
|
class="rounded-lg border border-border bg-muted/30 px-4 py-3"
|
||||||
:placeholder="field.placeholder"
|
|
||||||
disable-autofill
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 密码/敏感输入 -->
|
|
||||||
<Input
|
|
||||||
v-else-if="field.type === 'password'"
|
|
||||||
v-model="formData[field.key]"
|
|
||||||
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
|
||||||
masked
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 下拉选择 -->
|
|
||||||
<Select
|
|
||||||
v-else-if="field.type === 'select'"
|
|
||||||
v-model="formData[field.key]"
|
|
||||||
@update:model-value="handleFieldChange(field.key, $event)"
|
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<!-- 横向排列的字段:代理地址占更多空间 -->
|
||||||
<SelectValue :placeholder="field.placeholder || '请选择'" />
|
<div class="flex gap-3">
|
||||||
</SelectTrigger>
|
<div
|
||||||
<SelectContent>
|
v-for="(field, fieldIndex) in group.fields"
|
||||||
<SelectItem
|
:key="field.key"
|
||||||
v-for="option in field.options"
|
class="space-y-1"
|
||||||
:key="option.value"
|
:class="fieldIndex === 0 ? 'flex-[2]' : 'flex-1'"
|
||||||
:value="option.value"
|
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
<Label class="text-xs text-muted-foreground">
|
||||||
</SelectItem>
|
{{ field.label }}
|
||||||
</SelectContent>
|
</Label>
|
||||||
</Select>
|
|
||||||
|
|
||||||
<!-- 多行文本 -->
|
<!-- 文本输入 -->
|
||||||
<Textarea
|
<Input
|
||||||
v-else-if="field.type === 'textarea'"
|
v-if="field.type === 'text'"
|
||||||
v-model="formData[field.key]"
|
v-model="formData[field.key]"
|
||||||
:placeholder="field.placeholder"
|
:placeholder="field.placeholder"
|
||||||
rows="3"
|
disable-autofill
|
||||||
/>
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- 帮助文本 -->
|
<!-- 密码/敏感输入 -->
|
||||||
<p
|
<Input
|
||||||
v-if="field.helpText"
|
v-else-if="field.type === 'password'"
|
||||||
class="text-xs text-muted-foreground"
|
v-model="formData[field.key]"
|
||||||
>
|
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
||||||
{{ field.helpText }}
|
masked
|
||||||
</p>
|
class="h-8 text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 普通分组(非折叠) -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 分组标题 -->
|
||||||
|
<div
|
||||||
|
v-if="group.title"
|
||||||
|
class="pt-2 text-sm font-medium text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ group.title }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 字段列表 -->
|
||||||
|
<div
|
||||||
|
v-for="field in group.fields"
|
||||||
|
:key="field.key"
|
||||||
|
class="space-y-2"
|
||||||
|
>
|
||||||
|
<Label>
|
||||||
|
{{ field.label }}
|
||||||
|
<span
|
||||||
|
v-if="field.required"
|
||||||
|
class="text-muted-foreground/70"
|
||||||
|
>*</span>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<!-- 文本输入 -->
|
||||||
|
<Input
|
||||||
|
v-if="field.type === 'text'"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
disable-autofill
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 密码/敏感输入 -->
|
||||||
|
<Input
|
||||||
|
v-else-if="field.type === 'password'"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
:placeholder="sensitivePlaceholders[field.key] || field.placeholder"
|
||||||
|
masked
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 下拉选择 -->
|
||||||
|
<Select
|
||||||
|
v-else-if="field.type === 'select'"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
@update:model-value="handleFieldChange(field.key, $event)"
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue :placeholder="field.placeholder || '请选择'" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem
|
||||||
|
v-for="option in field.options"
|
||||||
|
:key="option.value"
|
||||||
|
:value="option.value"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<!-- 多行文本 -->
|
||||||
|
<Textarea
|
||||||
|
v-else-if="field.type === 'textarea'"
|
||||||
|
v-model="formData[field.key]"
|
||||||
|
:placeholder="field.placeholder"
|
||||||
|
rows="3"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 帮助文本 -->
|
||||||
|
<p
|
||||||
|
v-if="field.helpText"
|
||||||
|
class="text-xs text-muted-foreground"
|
||||||
|
>
|
||||||
|
{{ field.helpText }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -165,6 +224,7 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
|
Switch,
|
||||||
} from '@/components/ui'
|
} from '@/components/ui'
|
||||||
import { saveProviderOpsConfig, verifyProviderAuth, getProviderOpsConfig } from '@/api/providerOps'
|
import { saveProviderOpsConfig, verifyProviderAuth, getProviderOpsConfig } from '@/api/providerOps'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
@@ -175,7 +235,7 @@ import {
|
|||||||
} from '../auth-templates'
|
} from '../auth-templates'
|
||||||
|
|
||||||
// 敏感字段列表(用于验证和加载配置时的特殊处理)
|
// 敏感字段列表(用于验证和加载配置时的特殊处理)
|
||||||
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cookie', 'token_cookie', 'auth_cookie', 'cookie_string', 'cookies'] as const
|
const SENSITIVE_FIELDS = ['api_key', 'password', 'session_token', 'session_cookie', 'token_cookie', 'auth_cookie', 'cookie_string', 'cookies', 'proxy_password'] as const
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|||||||
@@ -180,7 +180,9 @@ def _parse_session_user_id(cookie_input: str) -> Tuple[Optional[str], Optional[s
|
|||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
async def _get_acw_cookie(base_url: str, timeout: float = 10) -> Optional[str]:
|
async def _get_acw_cookie(
|
||||||
|
base_url: str, timeout: float = 10, proxy: Optional[str] = None
|
||||||
|
) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
获取 acw_sc__v2 Cookie
|
获取 acw_sc__v2 Cookie
|
||||||
|
|
||||||
@@ -189,12 +191,22 @@ async def _get_acw_cookie(base_url: str, timeout: float = 10) -> Optional[str]:
|
|||||||
Args:
|
Args:
|
||||||
base_url: 目标站点 URL
|
base_url: 目标站点 URL
|
||||||
timeout: 请求超时时间
|
timeout: 请求超时时间
|
||||||
|
proxy: 代理地址
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Cookie 字符串 (acw_sc__v2=xxx),如果不需要或获取失败则返回 None
|
Cookie 字符串 (acw_sc__v2=xxx),如果不需要或获取失败则返回 None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=timeout, verify=get_ssl_context()) as client:
|
# 构建 client 参数
|
||||||
|
client_kwargs: Dict[str, Any] = {
|
||||||
|
"timeout": timeout,
|
||||||
|
"verify": get_ssl_context(),
|
||||||
|
}
|
||||||
|
if proxy:
|
||||||
|
client_kwargs["proxy"] = proxy
|
||||||
|
logger.debug(f"获取 acw_sc__v2 Cookie 使用代理: {proxy}")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||||
resp = await client.get(
|
resp = await client.get(
|
||||||
base_url,
|
base_url,
|
||||||
headers={
|
headers={
|
||||||
@@ -253,8 +265,8 @@ class AnyrouterConnector(ProviderConnector):
|
|||||||
# 解析 user_id
|
# 解析 user_id
|
||||||
self._user_id, _ = _parse_session_user_id(session_cookie)
|
self._user_id, _ = _parse_session_user_id(session_cookie)
|
||||||
|
|
||||||
# 尝试获取反爬 Cookie
|
# 尝试获取反爬 Cookie(使用配置中的代理)
|
||||||
self._acw_cookie = await _get_acw_cookie(self.base_url)
|
self._acw_cookie = await _get_acw_cookie(self.base_url, proxy=self._proxy)
|
||||||
|
|
||||||
self._set_connected()
|
self._set_connected()
|
||||||
return True
|
return True
|
||||||
@@ -371,7 +383,9 @@ class AnyrouterArchitecture(ProviderArchitecture):
|
|||||||
Returns:
|
Returns:
|
||||||
包含 acw_cookie 的配置
|
包含 acw_cookie 的配置
|
||||||
"""
|
"""
|
||||||
acw_cookie = await _get_acw_cookie(base_url)
|
# 从 config 获取代理配置
|
||||||
|
proxy = config.get("proxy")
|
||||||
|
acw_cookie = await _get_acw_cookie(base_url, proxy=proxy)
|
||||||
if acw_cookie:
|
if acw_cookie:
|
||||||
return {"acw_cookie": acw_cookie}
|
return {"acw_cookie": acw_cookie}
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -193,13 +193,21 @@ class YesCodeArchitecture(ProviderArchitecture):
|
|||||||
|
|
||||||
cookie_header = _build_cookie_header(cookie_input)
|
cookie_header = _build_cookie_header(cookie_input)
|
||||||
|
|
||||||
|
# 获取代理配置
|
||||||
|
proxy = config.get("proxy")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# 构建 client 参数
|
||||||
|
client_kwargs: Dict[str, Any] = {
|
||||||
|
"headers": {"Cookie": cookie_header},
|
||||||
|
"timeout": 10.0,
|
||||||
|
"verify": get_ssl_context(),
|
||||||
|
}
|
||||||
|
if proxy:
|
||||||
|
client_kwargs["proxy"] = proxy
|
||||||
|
|
||||||
# 创建临时 client 获取合并数据
|
# 创建临时 client 获取合并数据
|
||||||
async with httpx.AsyncClient(
|
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||||
headers={"Cookie": cookie_header},
|
|
||||||
timeout=10.0,
|
|
||||||
verify=get_ssl_context(),
|
|
||||||
) as client:
|
|
||||||
combined_data = await fetch_yescode_combined_data(client, base_url)
|
combined_data = await fetch_yescode_combined_data(client, base_url)
|
||||||
extra_config["_combined_data"] = combined_data
|
extra_config["_combined_data"] = combined_data
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -727,8 +727,20 @@ class ProviderOpsService:
|
|||||||
f"endpoint={verify_endpoint}, headers={list(headers.keys())}"
|
f"endpoint={verify_endpoint}, headers={list(headers.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 获取代理配置
|
||||||
|
proxy = config.get("proxy")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=30.0, verify=get_ssl_context()) as client:
|
# 构建 httpx client 参数
|
||||||
|
client_kwargs: Dict[str, Any] = {
|
||||||
|
"timeout": 30.0,
|
||||||
|
"verify": get_ssl_context(),
|
||||||
|
}
|
||||||
|
if proxy:
|
||||||
|
client_kwargs["proxy"] = proxy
|
||||||
|
logger.debug(f"使用代理: {proxy}")
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(**client_kwargs) as client:
|
||||||
response = await client.get(verify_endpoint, headers=headers)
|
response = await client.get(verify_endpoint, headers=headers)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
|||||||
Reference in New Issue
Block a user