启用翻译功能
`;
document.body.appendChild(panel);
return {
panel,
closeBtn: panel.querySelector('.settings-panel-close-btn'),
header: panel.querySelector('.settings-panel-header'),
masterSwitch: panel.querySelector('#setting-master-switch'),
swapLangBtn: panel.querySelector('#swap-lang-btn'),
engineSelect: panel.querySelector('#setting-trans-engine'),
serviceDetailsToggleContainer: panel.querySelector('#service-details-toggle-container'),
serviceDetailsToggleBtn: panel.querySelector('.service-details-toggle-btn'),
fromLangSelect: panel.querySelector('#setting-from-lang'),
toLangSelect: panel.querySelector('#setting-to-lang'),
modelGroup: panel.querySelector('#setting-model-group'),
modelSelect: panel.querySelector('#setting-trans-model'),
displayModeSelect: panel.querySelector('#setting-display-mode'),
apiKeyGroup: panel.querySelector('#api-key-group'),
apiKeyInput: panel.querySelector('#setting-input-apikey'),
apiKeySaveBtn: panel.querySelector('#setting-btn-apikey-save'),
customServiceContainer: panel.querySelector('#custom-service-container'),
glossaryActionsSelect: panel.querySelector('#setting-glossary-actions'),
editableSections: panel.querySelectorAll('.editable-section'),
aiSettingsSection: panel.querySelector('#editable-section-ai-settings'),
aiParamSelect: panel.querySelector('#ai-param-select'),
aiParamInputArea: panel.querySelector('#ai-param-input-area'),
langDetectSection: panel.querySelector('#editable-section-lang-detect'),
langDetectSelect: panel.querySelector('#setting-lang-detector'),
localManageSection: panel.querySelector('#editable-section-local-manage'),
localGlossarySelect: panel.querySelector('#setting-local-glossary-select'),
localEditModeSelect: panel.querySelector('#setting-local-edit-mode'),
localContainerName: panel.querySelector('#local-edit-container-name'),
localContainerTranslation: panel.querySelector('#local-edit-container-translation'),
localContainerForbidden: panel.querySelector('#local-edit-container-forbidden'),
localGlossaryNameInput: panel.querySelector('#setting-local-glossary-name'),
localGlossarySaveNameBtn: panel.querySelector('#setting-btn-local-glossary-save-name'),
localSensitiveInput: panel.querySelector('#setting-input-local-sensitive'),
localSensitiveSaveBtn: panel.querySelector('#setting-btn-local-sensitive-save'),
localInsensitiveInput: panel.querySelector('#setting-input-local-insensitive'),
localInsensitiveSaveBtn: panel.querySelector('#setting-btn-local-insensitive-save'),
localForbiddenInput: panel.querySelector('#setting-input-local-forbidden'),
localForbiddenSaveBtn: panel.querySelector('#setting-btn-local-forbidden-save'),
onlineManageSection: panel.querySelector('#editable-section-online-manage'),
glossaryImportUrlInput: panel.querySelector('#setting-input-glossary-import-url'),
glossaryImportSaveBtn: panel.querySelector('#setting-btn-glossary-import-save'),
glossaryManageSelect: panel.querySelector('#setting-select-glossary-manage'),
glossaryManageDetailsContainer: panel.querySelector('#online-glossary-details-container'),
glossaryManageInfo: panel.querySelector('#online-glossary-info'),
glossaryManageDeleteBtn: panel.querySelector('#online-glossary-delete-btn'),
postReplaceSection: panel.querySelector('#editable-section-post-replace'),
postReplaceInput: panel.querySelector('#setting-input-post-replace'),
postReplaceSaveBtn: panel.querySelector('#setting-btn-post-replace-save'),
dataSyncActionsContainer: panel.querySelector('#data-sync-actions-container'),
debugActionsContainer: panel.querySelector('#debug-actions-container'),
toggleDebugBtn: panel.querySelector('#btn-toggle-debug'),
exportLogBtn: panel.querySelector('#btn-export-log'),
importDataBtn: panel.querySelector('#btn-import-data'),
exportDataBtn: panel.querySelector('#btn-export-data'),
};
}
/**
* 显示一个自定义的确认模态框
*/
function showCustomConfirm(message, title = '提示', options = {}) {
const { textAlign = 'left', useTextIndent = false } = options;
return new Promise((resolve, reject) => {
if (document.getElementById('ao3-custom-confirm-overlay')) {
return reject(new Error('已有提示框正在显示中。'));
}
GM_addStyle(`
#ao3-custom-confirm-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 2147483647; display: flex; align-items: center; justify-content: center;
}
#ao3-custom-confirm-modal {
background-color: #fff; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.2);
width: 90%; max-width: 360px; overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex; flex-direction: column;
}
.ao3-custom-confirm-header {
padding: 12px 16px; border-bottom: 1px solid rgba(0,0,0,0.12);
text-align: center;
}
.ao3-custom-confirm-header h3 {
margin: 0; font-size: 16px; font-weight: 600; color: #000000DE;
}
.ao3-custom-confirm-body {
padding: 20px 16px; font-size: 14px; line-height: 1.6; color: #000000DE;
white-space: pre-wrap;
}
.ao3-custom-confirm-body p {
margin: 0;
}
.ao3-custom-confirm-body.with-indent p {
text-indent: 2em;
}
.ao3-custom-confirm-footer {
padding: 12px 16px; background-color: #fff;
display: flex; flex-direction: row; justify-content: space-between; align-items: center; gap: 8px;
}
.ao3-custom-confirm-btn {
flex: 1;
padding: 6px 0; border: none; border-radius: 4px;
font-size: 14px; font-weight: 500; cursor: pointer;
background: transparent !important;
color: #333; text-align: center;
transition: opacity 0.2s;
-webkit-tap-highlight-color: transparent;
outline: none;
}
.ao3-custom-confirm-btn:focus { outline: none; }
.ao3-custom-confirm-btn:hover { opacity: 0.7; }
/* 深色模式适配 */
@media (prefers-color-scheme: dark) {
#ao3-custom-confirm-modal { background-color: #1e1e1e; color: #e0e0e0; }
.ao3-custom-confirm-header { border-bottom-color: rgba(255,255,255,0.12); }
.ao3-custom-confirm-header h3 { color: #e0e0e0; }
.ao3-custom-confirm-body { color: #e0e0e0; }
.ao3-custom-confirm-footer { background-color: #1e1e1e; }
.ao3-custom-confirm-btn { color: #e0e0e0; }
}
`);
const overlay = document.createElement('div');
overlay.id = 'ao3-custom-confirm-overlay';
const modal = document.createElement('div');
modal.id = 'ao3-custom-confirm-modal';
const indentClass = useTextIndent ? ' with-indent' : '';
modal.innerHTML = `
${message.split('\n').map(line => `
${line}
`).join('')}
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const cleanup = () => {
overlay.remove();
};
const confirmBtn = modal.querySelector('.confirm');
const cancelBtn = modal.querySelector('.cancel');
confirmBtn.addEventListener('click', () => {
cleanup();
resolve(true);
});
cancelBtn.addEventListener('click', () => {
cleanup();
reject(new Error('User cancelled.'));
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
cleanup();
reject(new Error('User cancelled by clicking overlay.'));
}
});
});
}
/**
* 创建并管理自定义翻译服务的 UI 和逻辑
*/
function createCustomServiceManager(panelElements, syncPanelStateCallback) {
const { customServiceContainer, modelGroup, modelSelect, apiKeyGroup } = panelElements;
let currentServiceId = null;
let currentEditSection = 'name';
let isPendingCreation = false;
let pendingServiceData = {};
const CUSTOM_URL_FIRST_SAVE_DONE = 'custom_url_first_save_done';
const getServices = () => GM_getValue(CUSTOM_SERVICES_LIST_KEY, []);
const setServices = (services) => GM_setValue(CUSTOM_SERVICES_LIST_KEY, services);
const ensureServiceExists = () => {
if (!isPendingCreation) return currentServiceId;
const services = getServices();
const newService = { ...pendingServiceData, id: `custom_${Date.now()}` };
services.push(newService);
setServices(services);
isPendingCreation = false;
currentServiceId = newService.id;
const lastActionKey = `custom_service_last_action_${currentServiceId}`;
GM_setValue(lastActionKey, currentEditSection);
GM_setValue('transEngine', currentServiceId);
return newService.id;
};
const saveServiceField = (field, value) => {
const serviceId = isPendingCreation ? ensureServiceExists() : currentServiceId;
if (field === 'apiKey') {
GM_setValue(`${serviceId}_keys_string`, value);
} else {
const services = getServices();
const serviceIndex = services.findIndex(s => s.id === serviceId);
if (serviceIndex > -1) {
services[serviceIndex][field] = value;
setServices(services);
}
}
return serviceId;
};
const saveAndSyncCustomServiceField = (field, value) => {
const serviceId = saveServiceField(field, value);
synchronizeAllSettings(syncPanelStateCallback);
triggerModelFetchIfReady(serviceId);
};
const triggerModelFetchIfReady = (serviceId) => {
if (!serviceId) return;
const services = getServices();
const service = services.find(s => s.id === serviceId);
if (!service) return;
const apiKey = (GM_getValue(`${serviceId}_keys_array`, [])[0] || '').trim();
const modelsExist = service.models && service.models.length > 0;
if (service.url && apiKey && !modelsExist) {
fetchModelsForService(serviceId, service.url);
}
};
const fetchModelsForService = async (serviceId, url) => {
const serviceName = (getServices().find(s => s.id === serviceId) || {}).name || '新服务';
try {
const apiKey = (GM_getValue(`${serviceId}_keys_array`, [])[0] || '').trim();
if (!apiKey) return;
const modelsUrl = url.replace(/\/chat\/?(completions)?\/?$/, '') + '/models';
const response = await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: modelsUrl,
headers: { 'Authorization': `Bearer ${apiKey}`, 'Accept': 'application/json' },
responseType: 'json',
timeout: 15000,
onload: res => {
if (res.status === 200 && res.response) {
resolve(res.response);
} else {
reject(new Error(`服务器返回状态 ${res.status}。请检查接口地址和 API Key。`));
}
},
onerror: () => reject(new Error('网络请求失败,请检查您的网络连接和浏览器控制台。')),
ontimeout: () => reject(new Error('请求超时。'))
});
});
const models = getNestedProperty(response, 'data');
if (!Array.isArray(models) || models.length === 0) {
throw new Error('API 返回的数据格式不正确或模型列表为空。');
}
const modelIds = models.map(m => m.id).filter(Boolean);
if (modelIds.length === 0) {
throw new Error('未能从 API 响应中提取任何有效的模型 ID。');
}
saveServiceField('models', modelIds);
saveServiceField('modelsRaw', modelIds.join(', '));
Logger.info('网络', `成功为自定义服务 ${serviceName} 获取 ${modelIds.length} 个模型`);
const actionSelect = customServiceContainer.querySelector('#custom-service-action-select');
if (actionSelect) {
actionSelect.value = 'models';
actionSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
if (syncPanelStateCallback) {
syncPanelStateCallback();
}
} catch (error) {
Logger.error('网络', `自动获取模型失败: ${error.message}`);
notifyAndLog(`自动获取模型失败:${error.message}`, '操作失败', 'error');
}
};
function renderEditMode(serviceId) {
currentServiceId = serviceId;
if (serviceId) {
const lastActionKey = `custom_service_last_action_${serviceId}`;
currentEditSection = GM_getValue(lastActionKey, 'name');
}
let serviceData;
if (isPendingCreation) {
serviceData = pendingServiceData;
} else {
const services = getServices();
serviceData = services.find(s => s.id === serviceId) || {};
}
customServiceContainer.innerHTML = `