1
0
mirror of https://github.com/ZeroCatDev/Classworks.git synced 2026-02-04 16:03:10 +00:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Sunwuyuan
7ce2deb61c
Merge pull request #36 from ZeroCatDev/copilot/add-server-polling-for-classworks
Add server failover rotation for classworkscloud provider
2026-01-19 20:08:23 +08:00
copilot-swe-agent[bot]
a997e35162 Add JSDoc clarification for callback snapshot behavior
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2026-01-19 12:00:05 +00:00
copilot-swe-agent[bot]
b084c80b18 Address code review feedback: optimize callbacks and clarify Socket.IO limitation
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2026-01-19 11:57:14 +00:00
copilot-swe-agent[bot]
bf9ff52ee0 Implement server rotation for classworks cloud provider
Co-authored-by: Sunwuyuan <88357633+Sunwuyuan@users.noreply.github.com>
2026-01-19 11:52:50 +00:00
copilot-swe-agent[bot]
f8f1b5d494 Initial plan 2026-01-19 11:47:48 +00:00
6 changed files with 260 additions and 22 deletions

View File

@ -90,6 +90,7 @@
import SettingsCard from "@/components/SettingsCard.vue";
import {getSetting} from "@/utils/settings";
import axios from "axios";
import {tryWithRotation, isRotationEnabled} from "@/utils/serverRotation";
export default {
name: "DataProviderSettingsCard",
@ -132,8 +133,9 @@ export default {
async checkServerConnection() {
this.loading = true;
this.serverchecktime = new Date();
const triedServers = [];
try {
const domain = getSetting("server.domain");
const siteKey = getSetting("server.siteKey");
// Prepare headers including site key if available
@ -142,21 +144,71 @@ export default {
headers["x-site-key"] = siteKey;
}
const response = await axios.get(`${domain}/check`, {
method: "GET",
headers,
});
if (response.data.status === "success") {
this.$message.success(
"连接成功",
"服务器连接正常 延迟" + (new Date() - this.serverchecktime) + "ms"
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
const response = await tryWithRotation(
async (serverUrl) => {
const res = await axios.get(`${serverUrl}/check`, {
method: "GET",
headers,
});
if (res.data.status !== "success") {
throw new Error("服务器响应异常");
}
return res;
},
{
onServerTried: ({url, status, tried}) => {
triedServers.length = 0;
triedServers.push(...tried);
}
}
);
// Build success message with tried servers info
const latency = new Date() - this.serverchecktime;
const successServer = triedServers.find(s => s.status === "success");
let message = `服务器连接正常 延迟${latency}ms`;
if (triedServers.length > 1) {
const serverList = triedServers.map((s, i) =>
`${i + 1}. ${s.url} (${s.status === "success" ? "成功" : "失败"})`
).join("\n");
message += `\n\n依次尝试的服务器:\n${serverList}`;
} else if (successServer) {
message += `\n服务器: ${successServer.url}`;
}
this.$message.success("连接成功", message);
} else {
throw new Error("服务器响应异常");
// Standard single-server check for other providers
const domain = getSetting("server.domain");
const response = await axios.get(`${domain}/check`, {
method: "GET",
headers,
});
if (response.data.status === "success") {
this.$message.success(
"连接成功",
"服务器连接正常 延迟" + (new Date() - this.serverchecktime) + "ms"
);
} else {
throw new Error("服务器响应异常");
}
}
} catch (error) {
this.$message.error("连接失败", error.message || "无法连接到服务器");
// Build error message with tried servers info
let errorMessage = error.message || "无法连接到服务器";
if (triedServers.length > 0) {
const serverList = triedServers.map((s, i) =>
`${i + 1}. ${s.url} (失败${s.error ? `: ${s.error}` : ""})`
).join("\n");
errorMessage += `\n\n依次尝试的服务器:\n${serverList}\n\n所有服务器均连接失败`;
}
this.$message.error("连接失败", errorMessage);
} finally {
this.loading = false;
}

View File

@ -1,5 +1,6 @@
import axios from "@/axios/axios";
import {getSetting} from "@/utils/settings";
import {tryWithRotation, isRotationEnabled} from "@/utils/serverRotation";
// Helper function to check if provider is valid for API calls
const isValidProvider = () => {
@ -33,9 +34,18 @@ export const getNamespaceInfo = async () => {
throw new Error("当前数据提供者不支持此操作");
}
const serverUrl = getSetting("server.domain");
try {
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
const response = await tryWithRotation(async (serverUrl) => {
return await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});
});
return response.data;
}
const serverUrl = getSetting("server.domain");
const response = await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});

View File

@ -1,6 +1,7 @@
import {kvLocalProvider} from "./providers/kvLocalProvider";
import {kvServerProvider} from "./providers/kvServerProvider";
import {getSetting, setSetting} from "./settings";
import {getEffectiveServerUrl} from "./serverRotation";
export const formatResponse = (data) => data;
@ -173,7 +174,16 @@ export default {
} = options;
try {
let serverUrl = getSetting("server.domain");
const provider = getSetting("server.provider");
let serverUrl;
// Use effective server URL for classworkscloud provider
if (provider === "classworkscloud") {
serverUrl = getEffectiveServerUrl();
} else {
serverUrl = getSetting("server.domain");
}
let siteKey = getSetting("server.siteKey");
const machineId = getSetting("device.uuid");
let configured = false;
@ -200,6 +210,8 @@ export default {
// 设置provider为classworkscloud
setSetting("server.provider", "classworkscloud");
// Get effective URL after setting provider
serverUrl = getEffectiveServerUrl();
} else {
return formatError("云端配置无效请检查服务器域名和设备UUID", "CONFIG_ERROR");
}

View File

@ -1,6 +1,7 @@
import axios from "@/axios/axios";
import {formatResponse, formatError} from "../dataProvider";
import {getSetting} from "../settings";
import {tryWithRotation, isRotationEnabled} from "../serverRotation";
// Helper function to get request headers with kvtoken
const getHeaders = () => {
@ -22,9 +23,18 @@ const getHeaders = () => {
export const kvServerProvider = {
async loadNamespaceInfo() {
try {
// 使用 Classworks Cloud 或者用户配置的服务器域名
const serverUrl = getSetting("server.domain");
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
return await tryWithRotation(async (serverUrl) => {
const res = await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});
return formatResponse(res.data);
});
}
// Standard single-server mode
const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});
@ -42,8 +52,17 @@ export const kvServerProvider = {
async updateNamespaceInfo(data) {
try {
const serverUrl = getSetting("server.domain");
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
return await tryWithRotation(async (serverUrl) => {
const res = await axios.put(`${serverUrl}/kv/_info`, data, {
headers: getHeaders(),
});
return res;
});
}
const serverUrl = getSetting("server.domain");
const res = await axios.put(`${serverUrl}/kv/_info`, data, {
headers: getHeaders(),
});
@ -59,8 +78,17 @@ export const kvServerProvider = {
async loadData(key) {
try {
const serverUrl = getSetting("server.domain");
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
return await tryWithRotation(async (serverUrl) => {
const res = await axios.get(`${serverUrl}/kv/${key}`, {
headers: getHeaders(),
});
return formatResponse(res.data);
});
}
const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/${key}`, {
headers: getHeaders(),
});
@ -80,6 +108,16 @@ export const kvServerProvider = {
async saveData(key, data) {
try {
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
return await tryWithRotation(async (serverUrl) => {
await axios.post(`${serverUrl}/kv/${key}`, data, {
headers: getHeaders(),
});
return formatResponse(true);
});
}
const serverUrl = getSetting("server.domain");
await axios.post(`${serverUrl}/kv/${key}`, data, {
headers: getHeaders(),
@ -117,8 +155,6 @@ export const kvServerProvider = {
*/
async loadKeys(options = {}) {
try {
const serverUrl = getSetting("server.domain");
// 设置默认参数
const {
sortBy = "key",
@ -135,6 +171,17 @@ export const kvServerProvider = {
skip: skip.toString()
});
// Use rotation for classworkscloud provider
if (isRotationEnabled()) {
return await tryWithRotation(async (serverUrl) => {
const res = await axios.get(`${serverUrl}/kv/_keys?${params}`, {
headers: getHeaders(),
});
return formatResponse(res.data);
});
}
const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/_keys?${params}`, {
headers: getHeaders(),
});

106
src/utils/serverRotation.js Normal file
View File

@ -0,0 +1,106 @@
/**
* Server rotation utility for Classworks Cloud provider
* Provides fallback mechanism across multiple server endpoints
*/
import { getSetting } from "./settings";
// Server list for classworkscloud provider (in priority order)
const CLASSWORKS_CLOUD_SERVERS = [
"https://kv-service.houlang.cloud",
"https://kv-service.wuyuan.dev",
];
/**
* Get the list of servers to try for the given provider
* @param {string} provider - The provider type
* @returns {string[]} Array of server URLs to try
*/
export function getServerList(provider) {
if (provider === "classworkscloud") {
return [...CLASSWORKS_CLOUD_SERVERS];
}
// For other providers, use the configured domain
const domain = getSetting("server.domain");
return domain ? [domain] : [];
}
/**
* Try an operation with server rotation fallback
* @param {Function} operation - Async function that takes a serverUrl and returns a promise
* @param {Object} options - Options
* @param {string} options.provider - Provider type (optional, defaults to current setting)
* @param {Function} options.onServerTried - Callback called when a server is tried (optional)
* Receives: { url, status, tried } where tried is a snapshot of attempts
* @returns {Promise} Result from the first successful server, or throws the last error
*/
export async function tryWithRotation(operation, options = {}) {
const provider = options.provider || getSetting("server.provider");
const onServerTried = options.onServerTried;
const hasCallback = typeof onServerTried === 'function';
const servers = getServerList(provider);
const triedServers = [];
let lastError = null;
for (const serverUrl of servers) {
try {
triedServers.push({ url: serverUrl, status: "trying" });
if (hasCallback) {
// Provide a snapshot to prevent callback from mutating internal state
onServerTried({ url: serverUrl, status: "trying", tried: [...triedServers] });
}
const result = await operation(serverUrl);
triedServers[triedServers.length - 1].status = "success";
if (hasCallback) {
onServerTried({ url: serverUrl, status: "success", tried: [...triedServers] });
}
return result;
} catch (error) {
lastError = error;
triedServers[triedServers.length - 1].status = "failed";
triedServers[triedServers.length - 1].error = error.message || String(error);
if (hasCallback) {
onServerTried({ url: serverUrl, status: "failed", error, tried: [...triedServers] });
}
// Continue to next server
console.warn(`Server ${serverUrl} failed:`, error.message);
}
}
// All servers failed
console.error("All servers failed. Tried:", triedServers);
const error = lastError || new Error("All servers failed");
error.triedServers = triedServers;
throw error;
}
/**
* Get the effective server URL for the current provider
* For classworkscloud, returns the first server in the list
* For other providers, returns the configured domain
* @returns {string} Server URL
*/
export function getEffectiveServerUrl() {
const provider = getSetting("server.provider");
if (provider === "classworkscloud") {
return CLASSWORKS_CLOUD_SERVERS[0];
}
return getSetting("server.domain") || "";
}
/**
* Check if rotation is enabled for the current provider
* @returns {boolean}
*/
export function isRotationEnabled() {
const provider = getSetting("server.provider");
return provider === "classworkscloud";
}

View File

@ -4,12 +4,18 @@
import {io} from 'socket.io-client';
import {getSetting} from '@/utils/settings';
import {getEffectiveServerUrl, isRotationEnabled, tryWithRotation} from '@/utils/serverRotation';
let socket = null;
let connectedDomain = null;
const listeners = new Set();
export function getServerUrl() {
// For classworkscloud provider, use the effective server URL from rotation
if (isRotationEnabled()) {
return getEffectiveServerUrl();
}
// Prefer configured server domain; fallback to env; then current origin
const cfg = getSetting('server.domain');
const envUrl = import.meta?.env?.VITE_SERVER_URL;
@ -28,6 +34,11 @@ export function getSocket() {
socket = null;
}
connectedDomain = serverUrl;
// For classworkscloud, create socket with the first server in rotation
// Note: Socket.IO's built-in reconnection will retry the same server URL.
// Server rotation is handled at the HTTP request level, not Socket.IO level.
// If the Socket.IO server goes down, the connection will fail until the server recovers.
socket = io(serverUrl, {transports: ["polling","websocket"]});
// Re-attach previously registered event handlers on new socket instance