Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 65 additions & 13 deletions src/components/settings/cards/DataProviderSettingsCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -132,31 +133,82 @@ 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
const headers = {Accept: "application/json"};
if (siteKey) {
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}` : ""})`
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message formatting has inconsistent spacing. Line 206 shows "失败${s.error ? : ${s.error} : ""}" which will produce output like "失败: error message" (with Chinese characters followed by a colon and space). However, in Chinese text formatting, it's more conventional to use Chinese punctuation (如:"失败:error message") or omit the space. Consider using either "失败: ${s.error}" with proper Chinese formatting or ensuring consistent spacing throughout the user-facing messages.

Suggested change
`${i + 1}. ${s.url} (失败${s.error ? `: ${s.error}` : ""})`
`${i + 1}. ${s.url} (失败${s.error ? `${s.error}` : ""})`

Copilot uses AI. Check for mistakes.
).join("\n");
errorMessage += `\n\n依次尝试的服务器:\n${serverList}\n\n所有服务器均连接失败`;
}

this.$message.error("连接失败", errorMessage);
} finally {
this.loading = false;
}
Expand Down
14 changes: 12 additions & 2 deletions src/utils/api.js
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -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;
Comment on lines +39 to +45
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent error handling: when tryWithRotation fails, it throws an error that may not have the error.response structure expected by line 55 when checking error.response?.data?.message. This could result in less informative error messages when all servers fail during rotation.

Copilot uses AI. Check for mistakes.
}

const serverUrl = getSetting("server.domain");
const response = await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});
Expand Down
14 changes: 13 additions & 1 deletion src/utils/dataProvider.js
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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");
}
Expand Down
59 changes: 53 additions & 6 deletions src/utils/providers/kvServerProvider.js
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand All @@ -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);
});
Comment on lines +27 to +33
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling for rotation failures is inconsistent with the non-rotation path. When tryWithRotation fails, the error object will be thrown from serverRotation.js line 80 and caught here. However, this error may not have the same structure as axios errors (e.g., error.response), which the catch block at lines 44-49 expects when checking error.response?.data?.message. This could result in less informative error messages when all servers fail during rotation. Consider normalizing the error structure from tryWithRotation or adjusting the error handling to work with both error types.

Copilot uses AI. Check for mistakes.
}

// Standard single-server mode
const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/_info`, {
headers: getHeaders(),
});
Expand All @@ -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;
});
Comment on lines +56 to +62
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same error handling inconsistency as in loadNamespaceInfo: when tryWithRotation fails, the error thrown from serverRotation.js may not have the error.response structure expected by the catch block at lines 71-76. This affects the quality of error messages presented to users when all servers fail.

Copilot uses AI. Check for mistakes.
}

const serverUrl = getSetting("server.domain");
const res = await axios.put(`${serverUrl}/kv/_info`, data, {
headers: getHeaders(),
});
Expand All @@ -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);
});
Comment on lines +82 to +88
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same error handling inconsistency as in other methods: when tryWithRotation fails, the error thrown from serverRotation.js may not have the error.response structure expected by the catch block at lines 97-106, particularly when checking error.response?.status and error.response?.data?.message.

Copilot uses AI. Check for mistakes.
}

const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/${key}`, {
headers: getHeaders(),
});
Expand All @@ -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);
});
Comment on lines +112 to +118
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same error handling inconsistency as in other methods: when tryWithRotation fails, the error thrown from serverRotation.js may not have the error.response structure expected by the catch block at lines 126-132.

Copilot uses AI. Check for mistakes.
}

const serverUrl = getSetting("server.domain");
await axios.post(`${serverUrl}/kv/${key}`, data, {
headers: getHeaders(),
Expand Down Expand Up @@ -117,8 +155,6 @@ export const kvServerProvider = {
*/
async loadKeys(options = {}) {
try {
const serverUrl = getSetting("server.domain");

// 设置默认参数
const {
sortBy = "key",
Expand All @@ -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);
});
Comment on lines +175 to +181
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same error handling inconsistency as in other methods: when tryWithRotation fails, the error thrown from serverRotation.js may not have the error.response structure expected by the catch block at lines 190-205, particularly when checking error.response?.status for 404, 403, and 401 errors.

Copilot uses AI. Check for mistakes.
}

const serverUrl = getSetting("server.domain");
const res = await axios.get(`${serverUrl}/kv/_keys?${params}`, {
headers: getHeaders(),
});
Expand Down
106 changes: 106 additions & 0 deletions src/utils/serverRotation.js
Original file line number Diff line number Diff line change
@@ -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;
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When all servers fail, the error thrown here preserves the lastError from the final server attempt, which is an axios error with error.response structure. However, if the operation function throws a non-axios error (e.g., a validation error or custom error), the error handling in calling code that expects error.response will break. Consider documenting that the operation function should throw axios errors, or wrapping errors consistently to ensure they have the expected structure for downstream error handlers.

Suggested change
error.triedServers = triedServers;
error.triedServers = triedServers;
// Ensure downstream handlers that expect an axios-style error.response do not break
if (!error.response) {
error.response = {
data: {
message: error.message || "All servers failed",
triedServers,
},
};
}

Copilot uses AI. Check for mistakes.
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";
}
Loading
Loading