feat: Implement system configuration page for site settings and proxy management, and add team registration functionality.
This commit is contained in:
@@ -160,6 +160,7 @@ func startServer(cfg *config.Config) {
|
|||||||
mux.HandleFunc("/api/team-reg/status", api.CORS(api.HandleTeamRegStatus))
|
mux.HandleFunc("/api/team-reg/status", api.CORS(api.HandleTeamRegStatus))
|
||||||
mux.HandleFunc("/api/team-reg/logs", api.HandleTeamRegLogs) // SSE
|
mux.HandleFunc("/api/team-reg/logs", api.HandleTeamRegLogs) // SSE
|
||||||
mux.HandleFunc("/api/team-reg/import", api.CORS(api.HandleTeamRegImport))
|
mux.HandleFunc("/api/team-reg/import", api.CORS(api.HandleTeamRegImport))
|
||||||
|
mux.HandleFunc("/api/team-reg/clear-logs", api.CORS(api.HandleTeamRegClearLogs))
|
||||||
|
|
||||||
// 嵌入的前端静态文件
|
// 嵌入的前端静态文件
|
||||||
if web.IsEmbedded() {
|
if web.IsEmbedded() {
|
||||||
@@ -228,6 +229,8 @@ func handleConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
"team_reg_proxy": config.Global.TeamRegProxy,
|
"team_reg_proxy": config.Global.TeamRegProxy,
|
||||||
"proxy_test_status": getProxyTestStatus(),
|
"proxy_test_status": getProxyTestStatus(),
|
||||||
"proxy_test_ip": getProxyTestIP(),
|
"proxy_test_ip": getProxyTestIP(),
|
||||||
|
"team_reg_proxy_test_status": getTeamRegProxyTestStatus(),
|
||||||
|
"team_reg_proxy_test_ip": getTeamRegProxyTestIP(),
|
||||||
"site_name": config.Global.SiteName,
|
"site_name": config.Global.SiteName,
|
||||||
"mail_services_count": len(config.Global.MailServices),
|
"mail_services_count": len(config.Global.MailServices),
|
||||||
"mail_services": config.Global.MailServices,
|
"mail_services": config.Global.MailServices,
|
||||||
@@ -1008,6 +1011,7 @@ func handleProxyTest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
ProxyURL string `json:"proxy_url"`
|
ProxyURL string `json:"proxy_url"`
|
||||||
|
ProxyType string `json:"proxy_type"` // "default" 或 "team_reg"
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
api.Error(w, http.StatusBadRequest, "请求格式错误")
|
api.Error(w, http.StatusBadRequest, "请求格式错误")
|
||||||
@@ -1020,6 +1024,14 @@ func handleProxyTest(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确定保存状态的 key 前缀
|
||||||
|
statusKey := "proxy_test_status"
|
||||||
|
ipKey := "proxy_test_ip"
|
||||||
|
if req.ProxyType == "team_reg" {
|
||||||
|
statusKey = "team_reg_proxy_test_status"
|
||||||
|
ipKey = "team_reg_proxy_test_ip"
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info(fmt.Sprintf("测试代理连接: %s", proxyURL), "", "proxy")
|
logger.Info(fmt.Sprintf("测试代理连接: %s", proxyURL), "", "proxy")
|
||||||
|
|
||||||
// 解析代理 URL
|
// 解析代理 URL
|
||||||
@@ -1052,8 +1064,8 @@ func handleProxyTest(w http.ResponseWriter, r *http.Request) {
|
|||||||
logger.Error(fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode), "", "proxy")
|
logger.Error(fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode), "", "proxy")
|
||||||
// 保存失败状态
|
// 保存失败状态
|
||||||
if database.Instance != nil {
|
if database.Instance != nil {
|
||||||
database.Instance.SetConfig("proxy_test_status", "error")
|
database.Instance.SetConfig(statusKey, "error")
|
||||||
database.Instance.SetConfig("proxy_test_ip", "")
|
database.Instance.SetConfig(ipKey, "")
|
||||||
}
|
}
|
||||||
api.Error(w, http.StatusBadGateway, fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode))
|
api.Error(w, http.StatusBadGateway, fmt.Sprintf("代理测试失败: HTTP %d", resp.StatusCode))
|
||||||
return
|
return
|
||||||
@@ -1071,8 +1083,8 @@ func handleProxyTest(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// 保存成功状态到数据库
|
// 保存成功状态到数据库
|
||||||
if database.Instance != nil {
|
if database.Instance != nil {
|
||||||
database.Instance.SetConfig("proxy_test_status", "success")
|
database.Instance.SetConfig(statusKey, "success")
|
||||||
database.Instance.SetConfig("proxy_test_ip", ipResp.Origin)
|
database.Instance.SetConfig(ipKey, ipResp.Origin)
|
||||||
}
|
}
|
||||||
|
|
||||||
api.Success(w, map[string]interface{}{
|
api.Success(w, map[string]interface{}{
|
||||||
@@ -1102,6 +1114,26 @@ func getProxyTestIP() string {
|
|||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getTeamRegProxyTestStatus 获取注册代理测试状态
|
||||||
|
func getTeamRegProxyTestStatus() string {
|
||||||
|
if database.Instance == nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
if val, _ := database.Instance.GetConfig("team_reg_proxy_test_status"); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTeamRegProxyTestIP 获取注册代理测试出口IP
|
||||||
|
func getTeamRegProxyTestIP() string {
|
||||||
|
if database.Instance == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val, _ := database.Instance.GetConfig("team_reg_proxy_test_ip")
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
// parseProxyURL 解析代理 URL
|
// parseProxyURL 解析代理 URL
|
||||||
func parseProxyURL(proxyURL string) (*url.URL, error) {
|
func parseProxyURL(proxyURL string) (*url.URL, error) {
|
||||||
// 如果没有协议前缀,默认添加 http://
|
// 如果没有协议前缀,默认添加 http://
|
||||||
|
|||||||
@@ -573,6 +573,21 @@ func addTeamRegLog(log string) {
|
|||||||
logger.Info(fmt.Sprintf("[TeamReg] %s", cleanLog), "", "team-reg")
|
logger.Info(fmt.Sprintf("[TeamReg] %s", cleanLog), "", "team-reg")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleTeamRegClearLogs POST /api/team-reg/clear-logs - 清除日志
|
||||||
|
func HandleTeamRegClearLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
Error(w, http.StatusMethodNotAllowed, "仅支持 POST")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamRegState.mu.Lock()
|
||||||
|
teamRegState.Logs = make([]string, 0)
|
||||||
|
teamRegState.mu.Unlock()
|
||||||
|
|
||||||
|
logger.Info("[TeamReg] 日志已清除", "", "team-reg")
|
||||||
|
Success(w, map[string]string{"message": "日志已清除"})
|
||||||
|
}
|
||||||
|
|
||||||
// findTeamRegExecutable 查找 team-reg 可执行文件
|
// findTeamRegExecutable 查找 team-reg 可执行文件
|
||||||
func findTeamRegExecutable() string {
|
func findTeamRegExecutable() string {
|
||||||
// 可能的文件名
|
// 可能的文件名
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function Config() {
|
|||||||
setSiteName(data.data.site_name || 'Codex Pool')
|
setSiteName(data.data.site_name || 'Codex Pool')
|
||||||
setDefaultProxy(data.data.default_proxy || '')
|
setDefaultProxy(data.data.default_proxy || '')
|
||||||
setTeamRegProxy(data.data.team_reg_proxy || '')
|
setTeamRegProxy(data.data.team_reg_proxy || '')
|
||||||
// 恢复代理测试状态
|
// 恢复全局代理测试状态
|
||||||
const testStatus = data.data.proxy_test_status
|
const testStatus = data.data.proxy_test_status
|
||||||
if (testStatus === 'success' || testStatus === 'error') {
|
if (testStatus === 'success' || testStatus === 'error') {
|
||||||
setProxyStatus(testStatus)
|
setProxyStatus(testStatus)
|
||||||
@@ -53,6 +53,14 @@ export default function Config() {
|
|||||||
if (data.data.proxy_test_ip) {
|
if (data.data.proxy_test_ip) {
|
||||||
setProxyOriginIP(data.data.proxy_test_ip)
|
setProxyOriginIP(data.data.proxy_test_ip)
|
||||||
}
|
}
|
||||||
|
// 恢复注册代理测试状态
|
||||||
|
const teamRegTestStatus = data.data.team_reg_proxy_test_status
|
||||||
|
if (teamRegTestStatus === 'success' || teamRegTestStatus === 'error') {
|
||||||
|
setTeamRegProxyStatus(teamRegTestStatus)
|
||||||
|
}
|
||||||
|
if (data.data.team_reg_proxy_test_ip) {
|
||||||
|
setTeamRegProxyIP(data.data.team_reg_proxy_test_ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch config:', error)
|
console.error('Failed to fetch config:', error)
|
||||||
@@ -122,7 +130,7 @@ export default function Config() {
|
|||||||
const res = await fetch('/api/proxy/test', {
|
const res = await fetch('/api/proxy/test', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ proxy_url: defaultProxy }),
|
body: JSON.stringify({ proxy_url: defaultProxy, proxy_type: 'default' }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.code === 0 && data.data?.connected) {
|
if (data.code === 0 && data.data?.connected) {
|
||||||
@@ -177,7 +185,7 @@ export default function Config() {
|
|||||||
const res = await fetch('/api/proxy/test', {
|
const res = await fetch('/api/proxy/test', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ proxy_url: teamRegProxy }),
|
body: JSON.stringify({ proxy_url: teamRegProxy, proxy_type: 'team_reg' }),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (data.code === 0 && data.data?.connected) {
|
if (data.code === 0 && data.data?.connected) {
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ export default function TeamReg() {
|
|||||||
loadProxyConfig()
|
loadProxyConfig()
|
||||||
}, [loadProxyConfig])
|
}, [loadProxyConfig])
|
||||||
|
|
||||||
// 获取状态
|
// 获取状态(silent=true 时不显示加载状态)
|
||||||
const fetchStatus = useCallback(async () => {
|
const fetchStatus = useCallback(async (silent = false) => {
|
||||||
setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/team-reg/status')
|
const res = await fetch('/api/team-reg/status')
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -80,16 +80,16 @@ export default function TeamReg() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('获取状态失败:', e)
|
console.error('获取状态失败:', e)
|
||||||
}
|
}
|
||||||
setLoading(false)
|
if (!silent) setLoading(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// 初始化和轮询
|
// 初始化和轮询
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchStatus()
|
fetchStatus()
|
||||||
|
|
||||||
// 如果正在运行,每秒刷新状态
|
// 如果正在运行,每秒刷新状态(静默模式)
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
fetchStatus()
|
fetchStatus(true)
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
@@ -202,7 +202,7 @@ export default function TeamReg() {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={fetchStatus}
|
onClick={() => fetchStatus()}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
icon={<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />}
|
icon={<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />}
|
||||||
>
|
>
|
||||||
@@ -414,13 +414,13 @@ export default function TeamReg() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 实时日志 */}
|
{/* 实时日志 */}
|
||||||
<Card className="glass-card lg:col-span-2">
|
<Card className="glass-card lg:col-span-2 flex flex-col">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Terminal className="h-5 w-5 text-green-500" />
|
<Terminal className="h-5 w-5 text-green-500" />
|
||||||
实时日志
|
实时日志
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-3">
|
||||||
<label className="flex items-center gap-2 text-sm text-slate-500">
|
<label className="flex items-center gap-2 text-sm text-slate-500">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -433,12 +433,25 @@ export default function TeamReg() {
|
|||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-400">
|
||||||
{logs.length} 条日志
|
{logs.length} 条日志
|
||||||
</span>
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/team-reg/clear-logs', { method: 'POST' })
|
||||||
|
fetchStatus(true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除日志失败:', e)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="text-xs px-2 py-1 rounded bg-slate-700 hover:bg-slate-600 text-slate-300 transition-colors"
|
||||||
|
>
|
||||||
|
🗑️ 清除
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="flex-1 flex flex-col">
|
||||||
<div
|
<div
|
||||||
ref={logsContainerRef}
|
ref={logsContainerRef}
|
||||||
className="h-[500px] overflow-y-auto bg-slate-900 rounded-lg p-4 font-mono text-sm"
|
className="min-h-[400px] flex-1 overflow-y-auto bg-slate-900 rounded-lg p-4 font-mono text-sm"
|
||||||
>
|
>
|
||||||
{logs.length === 0 ? (
|
{logs.length === 0 ? (
|
||||||
<div className="flex items-center justify-center h-full text-slate-500">
|
<div className="flex items-center justify-center h-full text-slate-500">
|
||||||
|
|||||||
Reference in New Issue
Block a user