// ==UserScript==
// @name 超星自行火炮
// @namespace chaoxing-helper
// @version 4.6.2
// @author isMobile
// @description 超星自行火炮——自动化完成超星视频/音频/文档/答题(选择/简答)任务,不连接题库,使用大模型完成答题,仅供内部使用,用以测试课程流程,不外传
// @license MIT
// @icon https://vitejs.dev/logo.svg
// @match *://*.chaoxing.com/*
// @match *://*.edu.cn/*
// @match *://*.nbdlib.cn/*
// @match *://*.hnsyu.net/*
// @match *://*.gdhkmooc.com/*
// @match *://www.bing.com/*
// @match *://cn.bing.com/*
// @require https://lib.baomitu.com/vue/3.4.31/vue.global.prod.js
// @require https://lib.baomitu.com/vue-demi/0.14.7/index.iife.js
// @require data:application/javascript,window.Vue%3DVue%3B
// @require https://lib.baomitu.com/element-plus/2.7.2/index.full.min.js
// @require https://lib.baomitu.com/pinia/2.1.7/pinia.iife.min.js
// @require https://lib.baomitu.com/blueimp-md5/2.19.0/js/md5.min.js
// @resource ElementPlusStyle https://lib.baomitu.com/element-plus/2.8.2/index.min.css
// @resource ttf https://www.forestpolice.org/ttf/2.0/table.json
// @connect localhost
// @connect chaoxing-artillery.cloud.caqing.top
// @connect *
// @grant GM_getResourceText
// @grant GM_getValue
// @grant GM_info
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_openInTab
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function(vue, pinia, md5, ElementPlus) {
'use strict';
// ==================== 0. 环境钩子与防检测 ====================
const patchVisibilityProps = (doc, win) => {
if (!doc || !win) return;
try {
const docProto = win.Document?.prototype;
if (docProto && !docProto.__cxHelperVisibilityPatched) {
Object.defineProperty(docProto, 'hidden', { get: () => false, configurable: true });
Object.defineProperty(docProto, 'visibilityState', { get: () => 'visible', configurable: true });
docProto.__cxHelperVisibilityPatched = true;
}
} catch {}
try { Object.defineProperty(doc, 'hidden', { get: () => false, configurable: true }); } catch {}
try { Object.defineProperty(doc, 'visibilityState', { get: () => 'visible', configurable: true }); } catch {}
try { doc.hasFocus = () => true; } catch {}
try { doc.onvisibilitychange = null; } catch {}
try { Object.defineProperty(doc, 'onvisibilitychange', { get: () => null, set: () => {}, configurable: true }); } catch {}
try {
win.onblur = null;
win.onpagehide = null;
} catch {}
};
const applyDocumentHooks = (doc, win) => {
if (!doc || !win) return;
patchVisibilityProps(doc, win);
};
// ==================== 1. 顶层窗口检测 ====================
const isTopWindow = (function() {
try {
return window.self === window.top;
} catch (e) {
return false;
}
})();
// ==================== 2. 单例检查,防止多次初始化 ====================
const SCRIPT_ID = 'chaoxing-helper-initialized-v3';
const getTopWindow = () => {
try {
return window.top;
} catch (e) {
return window;
}
};
const topWin = getTopWindow();
if (topWin[SCRIPT_ID]) {
console.log('[超星自行火炮] 已初始化,跳过重复加载');
return;
}
if (isTopWindow) {
topWin[SCRIPT_ID] = true;
}
// ==================== 3. GM API 封装 ====================
const _GM_getResourceText = typeof GM_getResourceText !== 'undefined' ? GM_getResourceText : (name) => '';
const _GM_getValue = typeof GM_getValue !== 'undefined' ? GM_getValue : (key, defaultVal) => defaultVal;
const _GM_info = typeof GM_info !== 'undefined' ? GM_info : { script: { name: '超星自行火炮', author: 'isMobile', version: '4.5.4' } };
const _GM_setValue = typeof GM_setValue !== 'undefined' ? GM_setValue : () => {};
const _GM_addValueChangeListener = typeof GM_addValueChangeListener !== 'undefined' ? GM_addValueChangeListener : null;
const _GM_openInTab = typeof GM_openInTab !== 'undefined' ? GM_openInTab : null;
const _GM_xmlhttpRequest = typeof GM_xmlhttpRequest !== 'undefined' ? GM_xmlhttpRequest : () => {};
const _unsafeWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
// ==================== 4. 工具函数 ====================
const sleep = (second) => new Promise(resolve => setTimeout(resolve, second * 1000));
const getScriptInfo = () => ({
name: _GM_info.script.name || '超星自行火炮',
author: _GM_info.script.author || 'isMobile',
namespace: _GM_info.script.namespace || 'chaoxing-helper',
version: _GM_info.script.version || '4.5.4'
});
const formatDateTime = (dt) => {
const pad = n => n < 10 ? "0" + n : n.toString();
return `${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
};
const getDateTime = () => formatDateTime(new Date());
const normalizeApiUrl = (url) => {
if (!url) return '';
url = url.trim();
url = url.replace(/\/+$/, '');
if (!url.startsWith('http://') && !url.startsWith('https://')) {
url = 'http://' + url;
}
return url;
};
// ==================== 5. 自定义样式 ====================
const customStyles = `
.script-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
}
/* 日志组件 */
.script-log-container {
font-size: 13px;
padding: 4px;
}
.script-log-container .log-group-header {
cursor: pointer;
user-select: none;
}
.script-log-container .log-group-count {
font-size: 11px;
color: #909399;
}
.script-log-container .log-group-toggle {
margin-left: auto;
font-size: 11px;
color: #409eff;
}
.script-log-container .log-item {
padding: 8px 10px;
margin: 4px 0;
border-radius: 6px;
background: #ffffff;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
line-height: 1.4;
word-break: break-all;
display: flex;
align-items: center;
gap: 8px;
border-left: 3px solid transparent;
}
.script-log-container .log-time {
color: #909399;
font-family: 'Menlo', 'Monaco', monospace;
font-size: 10px;
flex-shrink: 0;
background: #f4f6f8;
padding: 2px 5px;
border-radius: 3px;
}
.script-log-container .log-message {
flex: 1;
font-size: 12px;
}
.log-item.type-success { border-left-color: #67c23a; }
.log-item.type-warning { border-left-color: #e6a23c; }
.log-item.type-danger { border-left-color: #f56c6c; }
.log-item.type-primary { border-left-color: #409eff; }
.log-item.type-info { border-left-color: #909399; }
.script-log-container .empty-log {
text-align: center;
color: #909399;
padding: 30px 0;
font-size: 12px;
}
/* 设置组件 */
.script-setting {
font-size: 13px;
padding: 4px 0;
}
.script-setting .setting-section {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 12px 14px;
margin-bottom: 10px;
}
.script-setting .section-title {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 10px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f2f5;
display: flex;
align-items: center;
gap: 6px;
}
.script-setting .setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px dashed #f0f2f5;
}
.script-setting .setting-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.script-setting .setting-label {
font-size: 12px;
color: #606266;
}
.script-setting .setting-tip {
font-size: 10px;
color: #909399;
margin-top: 2px;
}
.script-setting .el-switch {
--el-switch-on-color: #764ba2;
}
.script-setting .el-input-number {
width: 90px;
}
/* API管理 */
.api-manager {
padding: 4px 0;
}
.api-manager .api-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
margin: 4px 0;
background: #f8f9fa;
border-radius: 6px;
border: 1px solid #ebeef5;
cursor: pointer;
transition: all 0.2s;
}
.api-manager .api-item:hover {
background: #f0f2f5;
}
.api-manager .api-item.active {
background: linear-gradient(135deg, #667eea20 0%, #764ba220 100%);
border-color: #764ba2;
}
.api-manager .api-item .api-url {
flex: 1;
font-size: 12px;
color: #606266;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.api-manager .api-item.active .api-url {
color: #764ba2;
font-weight: 500;
}
.api-manager .api-item .action-btns {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.api-manager .api-item:hover .action-btns {
opacity: 1;
}
.api-manager .api-item .action-btn {
cursor: pointer;
padding: 2px;
border-radius: 3px;
transition: background 0.2s;
}
.api-manager .api-item .action-btn:hover {
background: rgba(0,0,0,0.05);
}
.api-manager .add-api {
display: flex;
gap: 8px;
margin-top: 10px;
}
.api-manager .add-api .el-input {
flex: 1;
}
.api-manager .edit-row {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
margin: 4px 0;
background: #fff;
border-radius: 6px;
border: 1px solid #764ba2;
box-shadow: 0 2px 8px rgba(118, 75, 162, 0.15);
}
.api-manager .edit-row .el-input {
flex: 1;
}
.api-manager .edit-row .edit-actions {
display: flex;
gap: 4px;
}
.api-manager .edit-row .edit-action-btn {
width: 24px;
height: 24px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.api-manager .edit-row .edit-action-btn.confirm {
background: #67c23a;
color: #fff;
}
.api-manager .edit-row .edit-action-btn.confirm:hover {
background: #85ce61;
}
.api-manager .edit-row .edit-action-btn.cancel {
background: #f0f2f5;
color: #909399;
}
.api-manager .edit-row .edit-action-btn.cancel:hover {
background: #e4e7ed;
color: #606266;
}
.api-manager .empty-tip {
text-align: center;
color: #909399;
padding: 20px 0;
font-size: 12px;
}
/* 题目表格 */
.script-question-table {
width: 100%;
}
.script-question-table .el-table {
font-size: 11px;
border-radius: 6px;
--el-table-header-bg-color: #f5f7fa;
}
/* 主面板 */
.main-page {
z-index: 100003;
position: fixed;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.main-page * {
box-sizing: border-box;
}
/* 最小化圆环 */
.mini-circle {
width: 18px;
height: 18px;
border-radius: 50%;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.25) 0%, rgba(118, 75, 162, 0.25) 100%);
box-shadow: 0 1px 4px rgba(118, 75, 162, 0.1);
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
opacity: 0.4;
user-select: none;
}
.mini-circle:hover {
transform: scale(1.2);
opacity: 0.6;
box-shadow: 0 2px 8px rgba(118, 75, 162, 0.2);
}
.mini-circle:active {
cursor: grabbing;
}
.mini-circle::after {
content: '';
width: 6px;
height: 6px;
border: 1.5px solid rgba(255,255,255,0.5);
border-radius: 50%;
}
.mini-circle.paused {
background: linear-gradient(135deg, rgba(245, 108, 108, 0.3) 0%, rgba(230, 162, 60, 0.3) 100%);
}
/* 展开面板 */
.main-panel {
width: 340px;
}
.main-panel .el-card {
border: none;
border-radius: 12px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
background: #fff;
}
.main-panel .el-card__header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
padding: 12px 16px;
border-bottom: none;
}
.main-panel .el-card__header.paused {
background: linear-gradient(135deg, #f56c6c 0%, #e6a23c 100%);
}
.main-panel .el-card__body {
padding: 12px;
height: 320px;
overflow: hidden;
}
.main-panel .card-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: move;
user-select: none;
}
.main-panel .card-header .title {
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 6px;
}
.main-panel .card-header .header-btns {
display: flex;
align-items: center;
gap: 8px;
}
.main-panel .pause-btn,
.main-panel .minimize-btn {
width: 24px;
height: 24px;
border-radius: 50%;
background: rgba(255,255,255,0.2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.main-panel .pause-btn:hover,
.main-panel .minimize-btn:hover {
background: rgba(255,255,255,0.3);
}
.main-panel .minimize-btn::after {
content: '';
width: 10px;
height: 2px;
background: #fff;
border-radius: 1px;
}
.main-panel .pause-btn .pause-icon {
width: 8px;
height: 10px;
display: flex;
justify-content: space-between;
}
.main-panel .pause-btn .pause-icon::before,
.main-panel .pause-btn .pause-icon::after {
content: '';
width: 3px;
height: 100%;
background: #fff;
border-radius: 1px;
}
.main-panel .pause-btn.playing .pause-icon {
width: 0;
height: 0;
border-style: solid;
border-width: 5px 0 5px 8px;
border-color: transparent transparent transparent #fff;
}
.main-panel .pause-btn.playing .pause-icon::before,
.main-panel .pause-btn.playing .pause-icon::after {
display: none;
}
/* 标签页 */
.main-panel .el-tabs {
height: 100%;
display: flex;
flex-direction: column;
}
.main-panel .el-tabs__header {
margin-bottom: 8px;
flex-shrink: 0;
}
.main-panel .el-tabs__content {
flex: 1;
overflow: hidden;
}
.main-panel .el-tab-pane {
height: 100%;
}
.main-panel .el-tabs__nav-wrap::after {
height: 1px;
background: #ebeef5;
}
.main-panel .el-tabs__active-bar {
background: linear-gradient(90deg, #667eea, #764ba2);
height: 2px;
border-radius: 2px;
}
.main-panel .el-tabs__item {
font-size: 13px;
color: #606266;
padding: 0 16px;
height: 36px;
line-height: 36px;
}
.main-panel .el-tabs__item.is-active {
color: #764ba2;
font-weight: 600;
}
/* 滚动条 */
.main-panel .el-scrollbar__bar.is-vertical {
width: 4px;
}
.main-panel .el-scrollbar__thumb {
background-color: #c0c4cc;
}
/* 按钮 */
.main-panel .el-button--primary {
--el-button-bg-color: #764ba2;
--el-button-border-color: #764ba2;
}
/* 状态指示器 */
.status-indicator {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
margin-bottom: 8px;
border-radius: 6px;
font-size: 12px;
}
.status-indicator.running {
background: linear-gradient(135deg, #67c23a20 0%, #85ce6120 100%);
color: #67c23a;
}
.status-indicator.paused {
background: linear-gradient(135deg, #e6a23c20 0%, #f56c6c20 100%);
color: #e6a23c;
}
.status-indicator .status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
animation: pulse 1.5s ease-in-out infinite;
}
.status-indicator.paused .status-dot {
animation: none;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
`;
// ==================== 6. Element Plus 图标 ====================
const createIconComponent = (name, pathD) => ({
name,
render() {
return vue.h('svg', {
xmlns: "http://www.w3.org/2000/svg",
viewBox: "0 0 1024 1024",
style: { width: '1em', height: '1em', fill: 'currentColor' }
}, [vue.h('path', { fill: "currentColor", d: pathD })]);
}
});
const DeleteIcon = createIconComponent('Delete',
"M160 256H96a32 32 0 0 1 0-64h256V95.936a32 32 0 0 1 32-32h256a32 32 0 0 1 32 32V192h256a32 32 0 1 1 0 64h-64v672a32 32 0 0 1-32 32H192a32 32 0 0 1-32-32V256zm448-64v-64H416v64h192zM224 896h576V256H224v640zm192-128a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32zm192 0a32 32 0 0 1-32-32V416a32 32 0 0 1 64 0v320a32 32 0 0 1-32 32z");
const CheckIcon = createIconComponent('Check',
"M512 896a384 384 0 1 0 0-768 384 384 0 0 0 0 768zm0 64a448 448 0 1 1 0-896 448 448 0 0 1 0 896zm-55.808-536.384l-99.52-99.584a38.4 38.4 0 1 0-54.336 54.336l126.72 126.72a38.272 38.272 0 0 0 54.336 0l262.4-262.464a38.4 38.4 0 1 0-54.272-54.336L456.192 423.616z");
const EditIcon = createIconComponent('Edit',
"M832 512a32 32 0 1 1 64 0v352a32 32 0 0 1-32 32H160a32 32 0 0 1-32-32V160a32 32 0 0 1 32-32h352a32 32 0 0 1 0 64H192v640h640V512z M469.952 554.24l52.8-7.552L847.104 222.4a32 32 0 1 0-45.248-45.248L477.504 501.44l-7.552 52.8zm422.4-422.4a96 96 0 0 1 0 135.808l-331.84 331.84a32 32 0 0 1-18.112 9.088L436.8 623.68a32 32 0 0 1-36.224-36.224l15.104-105.6a32 32 0 0 1 9.024-18.112l331.904-331.84a96 96 0 0 1 135.744 0z");
const CloseIcon = createIconComponent('Close',
"M764.288 214.592 512 466.88 259.712 214.592a31.936 31.936 0 0 0-45.12 45.12L466.752 512 214.528 764.224a31.936 31.936 0 1 0 45.12 45.184L512 557.184l252.288 252.288a31.936 31.936 0 0 0 45.12-45.12L557.12 512.064l252.288-252.352a31.936 31.936 0 1 0-45.12-45.184z");
// ==================== 7. Pinia Stores ====================
// 全局暂停状态 (响应式)
const globalPauseState = vue.reactive({ isPaused: false });
// 媒体处理标记 (WeakMap 防止内存泄漏)
const __mediaProcessedMap = new WeakMap();
const DEFAULT_API_BASE_URL = "https://chaoxing-artillery.cloud.caqing.top";
const OTHER_PARAM_KEYS = {
operationIntervalSec: "operationIntervalSec",
retryCount: "retryCount",
licenseKey: "licenseKey",
enableWebSearch: "enableWebSearch"
};
const CX_PART_KEYS = {
chapter: "chapter",
exam: "exam",
video: "video"
};
const CX_PARAM_KEYS = {
chapter: {
autoSubmit: "autoSubmit",
autoNext: "autoNext",
skipCompleted: "skipCompleted",
onlyQuiz: "onlyQuiz"
},
exam: {
autoSwitchQuestion: "autoSwitchQuestion"
},
video: {
muted: "muted"
}
};
const CX_PART_KEY_ORDER = [
CX_PART_KEYS.chapter,
CX_PART_KEYS.exam,
CX_PART_KEYS.video
];
const CX_PARAM_KEY_ORDER = {
[CX_PART_KEYS.chapter]: [
CX_PARAM_KEYS.chapter.autoSubmit,
CX_PARAM_KEYS.chapter.autoNext,
CX_PARAM_KEYS.chapter.skipCompleted,
CX_PARAM_KEYS.chapter.onlyQuiz
],
[CX_PART_KEYS.exam]: [
CX_PARAM_KEYS.exam.autoSwitchQuestion
],
[CX_PART_KEYS.video]: [
CX_PARAM_KEYS.video.muted
]
};
const getDefaultCxPartKey = (partIndex) => CX_PART_KEY_ORDER[partIndex] || `part-${partIndex}`;
const getDefaultCxParamKey = (partKey, paramIndex) =>
CX_PARAM_KEY_ORDER[partKey]?.[paramIndex] || `${partKey}-param-${paramIndex}`;
const assignCxConfigKeys = (parts = []) => {
parts.forEach((part, partIndex) => {
if (!part || typeof part !== "object") return;
const partKey = typeof part.key === "string" && part.key
? part.key
: getDefaultCxPartKey(partIndex);
part.key = partKey;
if (!Array.isArray(part.params)) {
part.params = [];
return;
}
part.params.forEach((param, paramIndex) => {
if (!param || typeof param !== "object") return;
if (typeof param.key === "string" && param.key) return;
param.key = getDefaultCxParamKey(partKey, paramIndex);
});
});
return parts;
};
const findCxPart = (parts = [], partKey) => {
if (!Array.isArray(parts)) return null;
return parts.find((part) => part?.key === partKey)
|| parts[CX_PART_KEY_ORDER.indexOf(partKey)]
|| null;
};
const findCxParam = (parts = [], partKey, paramKey) => {
const part = findCxPart(parts, partKey);
if (!part?.params) return null;
return part.params.find((param) => param?.key === paramKey)
|| part.params[CX_PARAM_KEY_ORDER[partKey]?.indexOf(paramKey)]
|| null;
};
const getCxParamValue = (parts = [], partKey, paramKey, fallback) => {
const param = findCxParam(parts, partKey, paramKey);
return param?.value ?? fallback;
};
const mergeCxPartValues = (targetParts = [], sourceParts = []) => {
assignCxConfigKeys(targetParts);
assignCxConfigKeys(sourceParts);
sourceParts.forEach((sourcePart, partIndex) => {
const partKey = sourcePart?.key || getDefaultCxPartKey(partIndex);
const targetPart = findCxPart(targetParts, partKey);
if (!targetPart?.params || !Array.isArray(sourcePart?.params)) return;
sourcePart.params.forEach((sourceParam, paramIndex) => {
const paramKey = sourceParam?.key || getDefaultCxParamKey(partKey, paramIndex);
const targetParam = findCxParam(targetParts, partKey, paramKey);
if (!targetParam) return;
targetParam.value = sourceParam.value;
});
});
return targetParts;
};
const LICENSE_STATES = {
CHECKING: "checking",
VALID: "valid",
INVALID: "invalid"
};
const LicenseService = {
state: LICENSE_STATES.INVALID,
pendingPromise: null,
lockedByLicense: false,
validListeners: new Set(),
isValid() {
return this.state === LICENSE_STATES.VALID;
},
markDirty() {
if (this.state !== LICENSE_STATES.CHECKING) {
this.state = LICENSE_STATES.INVALID;
}
this.lockedByLicense = false;
},
onValid(listener) {
if (typeof listener !== "function") return () => {};
this.validListeners.add(listener);
return () => this.validListeners.delete(listener);
},
notifyValid() {
this.validListeners.forEach((listener) => {
try { listener(); } catch {}
});
},
setInvalid(message, addLog) {
this.state = LICENSE_STATES.INVALID;
this.lockedByLicense = true;
globalPauseState.isPaused = true;
if (typeof addLog === "function") {
addLog(message || "许可验证失败,功能已锁定", "danger");
return;
}
try {
useLogStore().addLog(message || "许可验证失败,功能已锁定", "danger");
} catch {}
},
async ensureValid({ force = false, addLog } = {}) {
const configStore = useConfigStore();
const logStore = useLogStore();
const log = typeof addLog === "function"
? addLog
: (message, type = "info") => logStore.addLog(message, type);
const apiUrl = configStore.currentApiUrl;
const licenseKey = String(configStore.licenseKey || configStore.otherSettings.licenseKey || "").trim();
if (!licenseKey) {
this.setInvalid("未填写许可密钥,功能已锁定", log);
return false;
}
if (this.state === LICENSE_STATES.VALID && !force) return true;
if (this.pendingPromise) return this.pendingPromise;
this.state = LICENSE_STATES.CHECKING;
this.pendingPromise = new Promise((resolve) => {
_GM_xmlhttpRequest({
url: `${apiUrl}/licenses/validate`,
method: "POST",
headers: { "Content-Type": "application/json" },
data: JSON.stringify({ licenseKey }),
timeout: 15000,
onload: (resp) => {
if (resp.status === 200) {
this.state = LICENSE_STATES.VALID;
if (this.lockedByLicense) {
globalPauseState.isPaused = false;
}
this.lockedByLicense = false;
log("许可验证通过", "success");
this.notifyValid();
resolve(true);
return;
}
this.setInvalid(`许可验证失败:HTTP ${resp.status}`, log);
resolve(false);
},
onerror: () => {
this.setInvalid("许可验证失败:网络错误", log);
resolve(false);
},
ontimeout: () => {
this.setInvalid("许可验证失败:请求超时", log);
resolve(false);
}
});
}).finally(() => {
this.pendingPromise = null;
});
return this.pendingPromise;
}
};
const KEEPALIVE_CHECKPOINT_KEY = "cx_keepalive_checkpoint_v2";
const KeepAliveEngine = {
stateMap: (() => {
try {
const raw = _GM_getValue(KEEPALIVE_CHECKPOINT_KEY, "{}");
const parsed = typeof raw === "string" ? JSON.parse(raw) : raw;
if (parsed && typeof parsed === "object") return parsed;
} catch {}
return {};
})(),
saveTimer: null,
scheduleSave() {
if (this.saveTimer) clearTimeout(this.saveTimer);
this.saveTimer = setTimeout(() => {
this.saveTimer = null;
try {
_GM_setValue(KEEPALIVE_CHECKPOINT_KEY, JSON.stringify(this.stateMap));
} catch {}
}, 600);
},
touch(taskMeta, patch = {}) {
const key = taskMeta?.taskKey;
if (!key) return;
const prev = this.stateMap[key] || {};
this.stateMap[key] = {
...prev,
taskKey: key,
taskLabel: taskMeta?.taskLabel || prev.taskLabel || "",
taskType: taskMeta?.taskType || prev.taskType || "",
updatedAt: Date.now(),
...patch
};
this.scheduleSave();
},
clear(taskMetaOrKey) {
const key = typeof taskMetaOrKey === "string"
? taskMetaOrKey
: taskMetaOrKey?.taskKey;
if (!key || !this.stateMap[key]) return;
delete this.stateMap[key];
this.scheduleSave();
},
applyCheckpoint(taskMeta, mediaEl, log) {
const key = taskMeta?.taskKey;
if (!key || !mediaEl) return;
const snapshot = this.stateMap[key];
if (!snapshot) return;
const savedTime = Number(snapshot.currentTime || 0);
const ageMs = Date.now() - Number(snapshot.updatedAt || 0);
if (!savedTime || ageMs > 12 * 60 * 60 * 1000) return;
try {
if (savedTime > mediaEl.currentTime + 2) {
mediaEl.currentTime = savedTime;
if (typeof log === "function") {
log(`检测到断点,已恢复到 ${Math.floor(savedTime)} 秒`, "primary");
}
}
} catch {}
}
};
const BackgroundStability = (() => {
const state = {
initialized: false,
audioCtx: null,
audioOscillator: null,
audioGainNode: null,
ensureTimer: null,
log: null,
audioWarned: false
};
const log = (message, type = "info") => {
if (typeof state.log === "function") state.log(message, type);
};
const ensureAudioContext = async () => {
try {
if (!state.audioCtx) {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return;
state.audioCtx = new AudioContextClass();
state.audioOscillator = state.audioCtx.createOscillator();
state.audioGainNode = state.audioCtx.createGain();
state.audioOscillator.type = "sine";
state.audioOscillator.frequency.value = 18000;
state.audioGainNode.gain.value = 0.00001;
state.audioOscillator.connect(state.audioGainNode);
state.audioGainNode.connect(state.audioCtx.destination);
state.audioOscillator.start();
}
if (state.audioCtx.state !== "running") {
await state.audioCtx.resume();
}
} catch {
if (!state.audioWarned) {
state.audioWarned = true;
log("AudioContext 保活启动失败,继续使用其他保活机制", "warning");
}
}
};
const ensure = async () => {
await Promise.allSettled([ensureAudioContext()]);
};
const handleForeground = () => { ensure(); };
const init = (addLog) => {
if (state.initialized) return;
state.initialized = true;
state.log = addLog;
ensure();
document.addEventListener("visibilitychange", handleForeground);
window.addEventListener("focus", handleForeground);
window.addEventListener("pageshow", handleForeground);
state.ensureTimer = setInterval(ensure, 45000);
};
return { init, ensure };
})();
const useConfigStore = pinia.defineStore("configStore", {
state: () => {
const scriptInfo = getScriptInfo();
const defaultConfig = {
version: scriptInfo.version,
isMinus: false,
licenseKey: "",
position: { x: "calc(100vw - 380px)", y: "100px" },
menuIndex: "0",
platformName: "cx",
platformParams: {
cx: {
name: "超星自行火炮",
parts: [
{
name: "章节设置",
params: [
{ name: "章节作业自动提交", value: false, type: "boolean", tip: "答题完成后自动提交" },
{ name: "是否自动下一章节", value: true, type: "boolean", tip: "完成后自动跳转" },
{ name: "跳过已完成任务点", value: true, type: "boolean", tip: "已完成的不再处理" },
{ name: "只答题,不做其他", value: false, type: "boolean", tip: "跳过视频音频等任务" }
]
},
{
name: "考试设置",
params: [
{ name: "是否自动切换题目", value: true, type: "boolean", tip: "答完自动下一题" }
]
},
{
name: "视频设置",
params: [
{ name: "静音播放", value: true, type: "boolean", tip: "播放时静音" }
]
}
]
}
},
otherParams: {
name: "其他参数",
params: [
{
key: OTHER_PARAM_KEYS.operationIntervalSec,
name: "操作间隔(秒)",
value: 3,
type: "number",
min: 1,
max: 30,
tip: "每次操作的等待时间"
},
{
key: OTHER_PARAM_KEYS.retryCount,
name: "重试次数",
value: 3,
type: "number",
min: 1,
max: 10,
tip: "失败后的重试次数"
},
{
key: OTHER_PARAM_KEYS.enableWebSearch,
name: "联网搜索",
value: false,
type: "boolean",
tip: "默认关闭,开启后会在答题前联网搜索"
},
{
key: OTHER_PARAM_KEYS.licenseKey,
name: "许可密钥",
value: "",
type: "string",
tip: "后台发放的许可 Key,必填"
}
]
},
apiList: [
"http://localhost:3000",
DEFAULT_API_BASE_URL
],
currentApiIndex: 1
};
assignCxConfigKeys(defaultConfig.platformParams.cx.parts);
let globalConfig = defaultConfig;
const storedConfig = _GM_getValue("config", null);
if (storedConfig) {
try {
const parsed = typeof storedConfig === 'string' ? JSON.parse(storedConfig) : storedConfig;
globalConfig = {
...defaultConfig,
...parsed,
version: scriptInfo.version,
platformParams: defaultConfig.platformParams,
otherParams: defaultConfig.otherParams
};
assignCxConfigKeys(globalConfig.platformParams.cx.parts);
if (parsed.platformParams?.cx?.parts) {
mergeCxPartValues(globalConfig.platformParams.cx.parts, parsed.platformParams.cx.parts);
}
if (parsed.otherParams?.params) {
const incoming = Array.isArray(parsed.otherParams.params) ? parsed.otherParams.params : [];
const valuesByKey = new Map();
incoming.forEach((param, i) => {
const mappedKey = typeof param?.key === "string" && param.key
? param.key
: globalConfig.otherParams.params[i]?.key;
if (!mappedKey) return;
valuesByKey.set(mappedKey, param.value);
});
globalConfig.otherParams.params.forEach((param) => {
if (valuesByKey.has(param.key)) {
param.value = valuesByKey.get(param.key);
}
});
}
if (Array.isArray(parsed.apiList) && parsed.apiList.length > 0) {
const normalizedList = parsed.apiList
.map((item) => normalizeApiUrl(item))
.filter(Boolean);
if (normalizedList.length > 0) {
globalConfig.apiList = Array.from(new Set(normalizedList));
}
}
if (typeof parsed.currentApiIndex === "number") {
globalConfig.currentApiIndex = parsed.currentApiIndex;
}
if (typeof parsed.apiBaseUrl === "string" && parsed.apiBaseUrl.trim()) {
const normalizedApiBaseUrl = normalizeApiUrl(parsed.apiBaseUrl);
if (normalizedApiBaseUrl) {
if (!globalConfig.apiList.includes(normalizedApiBaseUrl)) {
globalConfig.apiList.push(normalizedApiBaseUrl);
}
globalConfig.currentApiIndex = globalConfig.apiList.indexOf(normalizedApiBaseUrl);
}
}
if (parsed.licenseKey) {
globalConfig.licenseKey = parsed.licenseKey;
}
const storedLicenseParam = globalConfig.otherParams.params.find(
(param) => param.key === OTHER_PARAM_KEYS.licenseKey
);
if (storedLicenseParam?.value) {
globalConfig.licenseKey = storedLicenseParam.value;
}
} catch (e) {
console.error("配置解析错误:", e);
}
}
if (!Array.isArray(globalConfig.apiList)) {
globalConfig.apiList = [...defaultConfig.apiList];
}
assignCxConfigKeys(globalConfig.platformParams?.cx?.parts);
globalConfig.apiList = globalConfig.apiList
.map((item) => normalizeApiUrl(item))
.filter(Boolean);
if (globalConfig.apiList.length === 0) {
globalConfig.apiList = [...defaultConfig.apiList];
}
globalConfig.currentApiIndex = Math.max(
0,
Math.min(
Number(globalConfig.currentApiIndex) || 0,
globalConfig.apiList.length - 1
)
);
const licenseParam = globalConfig.otherParams.params.find(
(param) => param.key === OTHER_PARAM_KEYS.licenseKey
);
if (licenseParam) {
licenseParam.value = globalConfig.licenseKey || "";
}
const webSearchParam = globalConfig.otherParams.params.find(
(param) => param.key === OTHER_PARAM_KEYS.enableWebSearch
);
if (!webSearchParam) {
globalConfig.otherParams.params.push({
key: OTHER_PARAM_KEYS.enableWebSearch,
name: "联网搜索",
value: false,
type: "boolean",
tip: "默认关闭,开启后会在答题前联网搜索"
});
}
return globalConfig;
},
getters: {
currentApiUrl: (state) => {
const idx = state.currentApiIndex;
if (idx >= 0 && idx < state.apiList.length) {
return normalizeApiUrl(state.apiList[idx]);
}
return state.apiList[0] ? normalizeApiUrl(state.apiList[0]) : DEFAULT_API_BASE_URL;
},
isPaused: () => globalPauseState.isPaused,
chapterSettings: (state) => {
return {
autoSubmit: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.chapter, CX_PARAM_KEYS.chapter.autoSubmit, false),
autoNext: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.chapter, CX_PARAM_KEYS.chapter.autoNext, true),
skipCompleted: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.chapter, CX_PARAM_KEYS.chapter.skipCompleted, true),
onlyQuiz: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.chapter, CX_PARAM_KEYS.chapter.onlyQuiz, false)
};
},
videoSettings: (state) => {
return {
muted: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.video, CX_PARAM_KEYS.video.muted, true)
};
},
examSettings: (state) => {
return {
autoSwitchQuestion: !!getCxParamValue(state.platformParams?.cx?.parts, CX_PART_KEYS.exam, CX_PARAM_KEYS.exam.autoSwitchQuestion, true)
};
},
otherSettings: (state) => {
const getVal = (key, fallback) => {
const target = state.otherParams.params.find((param) => param.key === key);
return target?.value ?? fallback;
};
return {
operationIntervalSec: Number(getVal(OTHER_PARAM_KEYS.operationIntervalSec, 3)) || 3,
retryCount: Number(getVal(OTHER_PARAM_KEYS.retryCount, 3)) || 3,
licenseKey: String(getVal(OTHER_PARAM_KEYS.licenseKey, "") || ""),
enableWebSearch: !!getVal(OTHER_PARAM_KEYS.enableWebSearch, false)
};
}
},
actions: {
addApi(url) {
const normalized = normalizeApiUrl(url);
if (!normalized || this.apiList.includes(normalized)) return false;
this.apiList.push(normalized);
return true;
},
updateApi(index, url) {
const normalized = normalizeApiUrl(url);
if (index < 0 || index >= this.apiList.length || !normalized) return false;
const isDuplicate = this.apiList.some((api, i) => i !== index && api === normalized);
if (isDuplicate) return false;
this.apiList[index] = normalized;
if (index === this.currentApiIndex) {
LicenseService.markDirty();
}
return true;
},
removeApi(index) {
if (this.apiList.length <= 1 || index < 0 || index >= this.apiList.length) return false;
const removingCurrent = index === this.currentApiIndex;
this.apiList.splice(index, 1);
if (this.currentApiIndex >= this.apiList.length) {
this.currentApiIndex = this.apiList.length - 1;
} else if (index < this.currentApiIndex) {
this.currentApiIndex -= 1;
}
if (removingCurrent) {
LicenseService.markDirty();
}
return true;
},
selectApi(index) {
if (index < 0 || index >= this.apiList.length) return;
if (index === this.currentApiIndex) return;
this.currentApiIndex = index;
LicenseService.markDirty();
},
setPlatformParam(partKey, paramKey, value) {
const target = findCxParam(this.platformParams?.cx?.parts, partKey, paramKey);
if (!target) return false;
target.value = value;
return true;
},
setOtherParam(key, value) {
const target = this.otherParams.params.find((param) => param.key === key);
if (!target) return false;
target.value = value;
if (key === OTHER_PARAM_KEYS.licenseKey) {
this.licenseKey = String(value || "");
LicenseService.markDirty();
}
return true;
},
togglePause() {
globalPauseState.isPaused = !globalPauseState.isPaused;
return globalPauseState.isPaused;
},
setPaused(value) {
globalPauseState.isPaused = value;
}
}
});
const useLogStore = pinia.defineStore("logStore", {
state: () => ({ logList: [] }),
actions: {
addLog(message, type = 'info', meta = {}) {
if (type === "info" && !meta.force) return;
const now = Date.now();
const taskKey = meta.taskKey || "";
const last = this.logList[this.logList.length - 1];
if (last && last.message === message && last.type === type && last.taskKey === taskKey) {
const lastTs = Number(last._ts) || 0;
if (now - lastTs < 3000) return;
}
const entry = {
message,
time: getDateTime(),
type,
taskKey,
taskLabel: meta.taskLabel || "",
_ts: now
};
this.logList.push(entry);
if (this.logList.length > 200) {
this.logList = this.logList.slice(-200);
}
},
clearLogs() {
this.logList = [];
}
}
});
const useQuestionStore = pinia.defineStore("questionStore", {
state: () => ({ questionList: [] }),
actions: {
addQuestion(question) {
this.questionList.push(question);
},
clearQuestion() {
this.questionList = [];
}
}
});
// ==================== 8. IframeUtils ====================
class IframeUtils {
static getIframes(element) {
return Array.from(element.querySelectorAll("iframe"));
}
static getAllNestedIframes(element, visited = new Set()) {
const result = [];
const iframes = IframeUtils.getIframes(element);
for (const iframe of iframes) {
if (!iframe || visited.has(iframe)) continue;
visited.add(iframe);
result.push(iframe);
try {
const doc = iframe.contentDocument;
const root = doc?.documentElement;
if (root) {
const nested = IframeUtils.getAllNestedIframes(root, visited);
result.push(...nested);
}
} catch {}
}
return result;
}
}
// ==================== 9. Typr 字体解析库 ====================
const Typr = {};
Typr._bin = {
readFixed: (data, o) => (data[o] << 8 | data[o + 1]) + (data[o + 2] << 8 | data[o + 3]) / (256 * 256 + 4),
readF2dot14: (data, o) => Typr._bin.readShort(data, o) / 16384,
readInt: (buff, p) => Typr._bin._view(buff).getInt32(p),
readInt8: (buff, p) => Typr._bin._view(buff).getInt8(p),
readShort: (buff, p) => Typr._bin._view(buff).getInt16(p),
readUshort: (buff, p) => Typr._bin._view(buff).getUint16(p),
readUshorts: (buff, p, len) => {
const arr = [];
for (let i = 0; i < len; i++) arr.push(Typr._bin.readUshort(buff, p + i * 2));
return arr;
},
readUint: (buff, p) => Typr._bin._view(buff).getUint32(p),
readUint64: (buff, p) => Typr._bin.readUint(buff, p) * (4294967295 + 1) + Typr._bin.readUint(buff, p + 4),
readASCII: (buff, p, l) => {
let s = "";
for (let i = 0; i < l; i++) s += String.fromCharCode(buff[p + i]);
return s;
},
readBytes: (buff, p, l) => {
const arr = [];
for (let i = 0; i < l; i++) arr.push(buff[p + i]);
return arr;
},
_view: (buff) => buff._dataView || (buff._dataView = buff.buffer
? new DataView(buff.buffer, buff.byteOffset, buff.byteLength)
: new DataView(new Uint8Array(buff).buffer))
};
Typr.parse = function(buff) {
const bin = Typr._bin;
const data = new Uint8Array(buff);
const tag = bin.readASCII(data, 0, 4);
if (tag === "ttcf") {
let offset = 4;
offset += 4;
const numF = bin.readUint(data, offset); offset += 4;
const fnts = [];
for (let i = 0; i < numF; i++) {
const foff = bin.readUint(data, offset); offset += 4;
fnts.push(Typr._readFont(data, foff));
}
return fnts;
}
return [Typr._readFont(data, 0)];
};
Typr._readFont = function(data, offset) {
const bin = Typr._bin;
const ooff = offset;
offset += 4;
const numTables = bin.readUshort(data, offset); offset += 8;
const tags = ["cmap", "head", "hhea", "maxp", "hmtx", "loca", "glyf"];
const obj = { _data: data, _offset: ooff };
const tabs = {};
for (let i = 0; i < numTables; i++) {
const tag = bin.readASCII(data, offset, 4); offset += 4;
offset += 4;
const toffset = bin.readUint(data, offset); offset += 4;
const length = bin.readUint(data, offset); offset += 4;
tabs[tag] = { offset: toffset, length };
}
for (const t of tags) {
if (tabs[t] && Typr[t]) {
obj[t] = Typr[t].parse(data, tabs[t].offset, tabs[t].length, obj);
}
}
return obj;
};
Typr._tabOffset = function(data, tab, foff) {
const bin = Typr._bin;
const numTables = bin.readUshort(data, foff + 4);
let offset = foff + 12;
for (let i = 0; i < numTables; i++) {
const tag = bin.readASCII(data, offset, 4); offset += 4;
offset += 4;
const toffset = bin.readUint(data, offset); offset += 4;
offset += 4;
if (tag === tab) return toffset;
}
return 0;
};
Typr.cmap = {
parse: function(data, offset, length) {
data = new Uint8Array(data.buffer, offset, length);
offset = 0;
const bin = Typr._bin;
const obj = { tables: [] };
offset += 2;
const numTables = bin.readUshort(data, offset); offset += 2;
const offs = [];
for (let i = 0; i < numTables; i++) {
const platformID = bin.readUshort(data, offset); offset += 2;
const encodingID = bin.readUshort(data, offset); offset += 2;
const noffset = bin.readUint(data, offset); offset += 4;
const id = "p" + platformID + "e" + encodingID;
let tind = offs.indexOf(noffset);
if (tind === -1) {
tind = obj.tables.length;
offs.push(noffset);
const format = bin.readUshort(data, noffset);
let subt;
if (format === 4) subt = Typr.cmap.parse4(data, noffset);
else if (format === 12) subt = Typr.cmap.parse12(data, noffset);
else subt = { format };
obj.tables.push(subt);
}
if (obj[id] == null) obj[id] = tind;
}
return obj;
},
parse4: function(data, offset) {
const bin = Typr._bin;
const offset0 = offset;
const obj = {};
obj.format = bin.readUshort(data, offset); offset += 2;
const length = bin.readUshort(data, offset); offset += 2;
offset += 2;
const segCountX2 = bin.readUshort(data, offset); offset += 2;
const segCount = segCountX2 / 2;
offset += 6;
obj.endCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2;
offset += 2;
obj.startCount = bin.readUshorts(data, offset, segCount); offset += segCount * 2;
obj.idDelta = [];
for (let i = 0; i < segCount; i++) { obj.idDelta.push(bin.readShort(data, offset)); offset += 2; }
obj.idRangeOffset = bin.readUshorts(data, offset, segCount); offset += segCount * 2;
obj.glyphIdArray = [];
while (offset < offset0 + length) { obj.glyphIdArray.push(bin.readUshort(data, offset)); offset += 2; }
return obj;
},
parse12: function(data, offset) {
const bin = Typr._bin;
const obj = {};
obj.format = bin.readUshort(data, offset); offset += 2;
offset += 10;
const nGroups = bin.readUint(data, offset); offset += 4;
obj.groups = [];
for (let i = 0; i < nGroups; i++) {
const off = offset + i * 12;
obj.groups.push([bin.readUint(data, off), bin.readUint(data, off + 4), bin.readUint(data, off + 8)]);
}
return obj;
}
};
Typr.head = {
parse: function(data, offset) {
const bin = Typr._bin;
const obj = {};
offset += 18;
obj.unitsPerEm = bin.readUshort(data, offset); offset += 2;
offset += 30;
obj.indexToLocFormat = bin.readShort(data, offset);
return obj;
}
};
Typr.hhea = {
parse: function(data, offset) {
const bin = Typr._bin;
const obj = {};
offset += 34;
obj.numberOfHMetrics = bin.readUshort(data, offset);
return obj;
}
};
Typr.maxp = {
parse: function(data, offset) {
const bin = Typr._bin;
return { numGlyphs: bin.readUshort(data, offset + 4) };
}
};
Typr.hmtx = {
parse: function(data, offset, length, font) {
const bin = Typr._bin;
const obj = { aWidth: [], lsBearing: [] };
let aw = 0, lsb = 0;
for (let i = 0; i < font.maxp.numGlyphs; i++) {
if (i < font.hhea.numberOfHMetrics) {
aw = bin.readUshort(data, offset); offset += 2;
lsb = bin.readShort(data, offset); offset += 2;
}
obj.aWidth.push(aw);
obj.lsBearing.push(lsb);
}
return obj;
}
};
Typr.loca = {
parse: function(data, offset, length, font) {
const bin = Typr._bin;
const obj = [];
const ver = font.head.indexToLocFormat;
const len = font.maxp.numGlyphs + 1;
if (ver === 0) for (let i = 0; i < len; i++) obj.push(bin.readUshort(data, offset + (i << 1)) << 1);
if (ver === 1) for (let i = 0; i < len; i++) obj.push(bin.readUint(data, offset + (i << 2)));
return obj;
}
};
Typr.glyf = {
parse: function(data, offset, length, font) {
const obj = [];
for (let g = 0; g < font.maxp.numGlyphs; g++) obj.push(null);
return obj;
},
_parseGlyf: function(font, g) {
const bin = Typr._bin;
const data = font._data;
let offset = Typr._tabOffset(data, "glyf", font._offset) + font.loca[g];
if (font.loca[g] === font.loca[g + 1]) return null;
const gl = {};
gl.noc = bin.readShort(data, offset); offset += 2;
gl.xMin = bin.readShort(data, offset); offset += 2;
gl.yMin = bin.readShort(data, offset); offset += 2;
gl.xMax = bin.readShort(data, offset); offset += 2;
gl.yMax = bin.readShort(data, offset); offset += 2;
if (gl.xMin >= gl.xMax || gl.yMin >= gl.yMax) return null;
if (gl.noc > 0) {
gl.endPts = [];
for (let i = 0; i < gl.noc; i++) { gl.endPts.push(bin.readUshort(data, offset)); offset += 2; }
const instructionLength = bin.readUshort(data, offset); offset += 2;
if (data.length - offset < instructionLength) return null;
offset += instructionLength;
const crdnum = gl.endPts[gl.noc - 1] + 1;
gl.flags = [];
for (let i = 0; i < crdnum; i++) {
const flag = data[offset]; offset++;
gl.flags.push(flag);
if ((flag & 8) !== 0) {
const rep = data[offset]; offset++;
for (let j = 0; j < rep; j++) { gl.flags.push(flag); i++; }
}
}
gl.xs = [];
for (let i = 0; i < crdnum; i++) {
const i8 = (gl.flags[i] & 2) !== 0, same = (gl.flags[i] & 16) !== 0;
if (i8) { gl.xs.push(same ? data[offset] : -data[offset]); offset++; }
else { if (same) gl.xs.push(0); else { gl.xs.push(bin.readShort(data, offset)); offset += 2; } }
}
gl.ys = [];
for (let i = 0; i < crdnum; i++) {
const i8 = (gl.flags[i] & 4) !== 0, same = (gl.flags[i] & 32) !== 0;
if (i8) { gl.ys.push(same ? data[offset] : -data[offset]); offset++; }
else { if (same) gl.ys.push(0); else { gl.ys.push(bin.readShort(data, offset)); offset += 2; } }
}
let x = 0, y = 0;
for (let i = 0; i < crdnum; i++) { x += gl.xs[i]; y += gl.ys[i]; gl.xs[i] = x; gl.ys[i] = y; }
} else {
gl.parts = [];
let flags;
do {
flags = bin.readUshort(data, offset); offset += 2;
const part = { m: { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }, p1: -1, p2: -1 };
gl.parts.push(part);
part.glyphIndex = bin.readUshort(data, offset); offset += 2;
let arg1, arg2;
if (flags & 1) { arg1 = bin.readShort(data, offset); offset += 2; arg2 = bin.readShort(data, offset); offset += 2; }
else { arg1 = bin.readInt8(data, offset); offset++; arg2 = bin.readInt8(data, offset); offset++; }
if (flags & 2) { part.m.tx = arg1; part.m.ty = arg2; }
else { part.p1 = arg1; part.p2 = arg2; }
if (flags & 8) { part.m.a = part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2; }
else if (flags & 64) {
part.m.a = Typr._bin.readF2dot14(data, offset); offset += 2;
part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2;
} else if (flags & 128) {
part.m.a = Typr._bin.readF2dot14(data, offset); offset += 2;
part.m.b = Typr._bin.readF2dot14(data, offset); offset += 2;
part.m.c = Typr._bin.readF2dot14(data, offset); offset += 2;
part.m.d = Typr._bin.readF2dot14(data, offset); offset += 2;
}
} while (flags & 32);
}
return gl;
}
};
Typr.U = {
codeToGlyph: function(font, code) {
const cmap = font.cmap;
let tind = -1;
if (cmap.p0e4 != null) tind = cmap.p0e4;
else if (cmap.p3e1 != null) tind = cmap.p3e1;
else if (cmap.p1e0 != null) tind = cmap.p1e0;
else if (cmap.p0e3 != null) tind = cmap.p0e3;
if (tind === -1) return 0;
const tab = cmap.tables[tind];
if (tab.format === 4) {
let sind = -1;
for (let i = 0; i < tab.endCount.length; i++) { if (code <= tab.endCount[i]) { sind = i; break; } }
if (sind === -1 || tab.startCount[sind] > code) return 0;
let gli = 0;
if (tab.idRangeOffset[sind] !== 0) {
gli = tab.glyphIdArray[code - tab.startCount[sind] + (tab.idRangeOffset[sind] >> 1) - (tab.idRangeOffset.length - sind)];
} else { gli = code + tab.idDelta[sind]; }
return gli & 65535;
} else if (tab.format === 12) {
if (code > tab.groups[tab.groups.length - 1][1]) return 0;
for (const grp of tab.groups) {
if (grp[0] <= code && code <= grp[1]) return grp[2] + (code - grp[0]);
}
}
return 0;
},
glyphToPath: function(font, gid) {
const path = { cmds: [], crds: [] };
if (font.glyf) Typr.U._drawGlyf(gid, font, path);
return path;
},
_drawGlyf: function(gid, font, path) {
let gl = font.glyf[gid];
if (gl == null) gl = font.glyf[gid] = Typr.glyf._parseGlyf(font, gid);
if (gl != null) {
if (gl.noc > -1) Typr.U._simpleGlyph(gl, path);
else Typr.U._compoGlyph(gl, font, path);
}
},
_simpleGlyph: function(gl, p) {
for (let c = 0; c < gl.noc; c++) {
const i0 = c === 0 ? 0 : gl.endPts[c - 1] + 1;
const il = gl.endPts[c];
for (let i = i0; i <= il; i++) {
const pr = i === i0 ? il : i - 1;
const nx = i === il ? i0 : i + 1;
const onCurve = gl.flags[i] & 1;
const prOnCurve = gl.flags[pr] & 1;
const nxOnCurve = gl.flags[nx] & 1;
const x = gl.xs[i], y = gl.ys[i];
if (i === i0) {
if (onCurve) {
if (prOnCurve) { p.cmds.push("M"); p.crds.push(gl.xs[pr], gl.ys[pr]); }
else { p.cmds.push("M"); p.crds.push(x, y); continue; }
} else {
if (prOnCurve) { p.cmds.push("M"); p.crds.push(gl.xs[pr], gl.ys[pr]); }
else { p.cmds.push("M"); p.crds.push((gl.xs[pr] + x) / 2, (gl.ys[pr] + y) / 2); }
}
}
if (onCurve) { if (prOnCurve) { p.cmds.push("L"); p.crds.push(x, y); } }
else {
if (nxOnCurve) { p.cmds.push("Q"); p.crds.push(x, y, gl.xs[nx], gl.ys[nx]); }
else { p.cmds.push("Q"); p.crds.push(x, y, (x + gl.xs[nx]) / 2, (y + gl.ys[nx]) / 2); }
}
}
p.cmds.push("Z");
}
},
_compoGlyph: function(gl, font, p) {
for (const prt of gl.parts) {
const path = { cmds: [], crds: [] };
Typr.U._drawGlyf(prt.glyphIndex, font, path);
const m = prt.m;
for (let i = 0; i < path.crds.length; i += 2) {
const x = path.crds[i], y = path.crds[i + 1];
p.crds.push(x * m.a + y * m.b + m.tx);
p.crds.push(x * m.c + y * m.d + m.ty);
}
for (const cmd of path.cmds) p.cmds.push(cmd);
}
}
};
// ==================== 11. Font 类 ====================
class Font {
constructor(data) {
const obj = Typr.parse(data);
if (!obj?.length || typeof obj[0] !== "object") throw new Error("unable to parse font");
Object.assign(this, obj[0]);
}
codeToGlyph(code) { return Typr.U.codeToGlyph(this, code); }
glyphToPath(gid) { return Typr.U.glyphToPath(this, gid); }
}
// ==================== 12. 字体解密 ====================
const FONT_TABLE_CACHE_KEY = "cx_font_table_cache_v1";
let cachedFontTable = null;
const getFontTable = () => {
if (cachedFontTable) return cachedFontTable;
let tableText = _GM_getResourceText("ttf");
if (!tableText) {
tableText = _GM_getValue(FONT_TABLE_CACHE_KEY, "");
}
if (!tableText) return null;
try {
const table = JSON.parse(tableText);
if (table && typeof table === "object") {
_GM_setValue(FONT_TABLE_CACHE_KEY, tableText);
cachedFontTable = table;
return table;
}
} catch {}
return null;
};
const FontDecoderModule = (() => {
const decrypt = (iframeDocument) => {
try {
const styles = iframeDocument.querySelectorAll("style");
let tip = null;
for (const style of styles) {
if (style.textContent?.includes("font-cxsecret")) { tip = style; break; }
}
if (!tip) return;
const fontMatch = tip.textContent.match(/base64,([\w\W]+?)'/);
if (!fontMatch?.[1]) return;
const fontArray = Uint8Array.from(atob(fontMatch[1]), c => c.charCodeAt(0));
const font = new Font(fontArray);
const table = getFontTable();
if (!table) return;
const match = {};
for (let i = 19968; i < 40870; i++) {
const glyph = font.codeToGlyph(i);
if (!glyph) continue;
const path = font.glyphToPath(glyph);
const hash = md5(JSON.stringify(path)).slice(24);
if (table[hash]) match[i] = table[hash];
}
for (const el of iframeDocument.querySelectorAll(".font-cxsecret")) {
let html = el.innerHTML;
for (const key in match) {
html = html.replace(new RegExp(String.fromCharCode(Number(key)), "g"), String.fromCharCode(match[key]));
}
el.innerHTML = html;
el.classList.remove("font-cxsecret");
}
} catch (e) {
console.warn("字体解密失败:", e);
}
};
return {
decryptDocument(iframeDocument) {
if (!iframeDocument?.querySelector?.(".font-cxsecret")) return;
decrypt(iframeDocument);
}
};
})();
// ==================== 13. API call ====================
const BROWSER_SEARCH_MAX_RESULTS = 2;
const BROWSER_SEARCH_CONTEXT_MAX_CHARS = 12000;
const BROWSER_SEARCH_RESULT_MAX_CONTENT_CHARS = 700;
const BROWSER_BING_SEARCH_ENDPOINT = "https://www.bing.com/search";
const DEFAULT_BROWSER_SEARCH_SITE = "www.jhq8.cn";
const BROWSER_SEARCH_BRIDGE_TASK_KEY = "__cx_browser_search_task__";
const BROWSER_SEARCH_BRIDGE_RESULT_PREFIX = "__cx_browser_search_result__:";
const BROWSER_SEARCH_BRIDGE_PARAM = "cx_search_req";
const BROWSER_SEARCH_BRIDGE_TIMEOUT_MS = 20000;
const BROWSER_SEARCH_BRIDGE_POLL_MS = 250;
const browserRequest = ({ url, method = "GET", headers = {}, data, timeout = 20000, responseType = "text", overrideMimeType = "" }) =>
new Promise((resolve, reject) => {
_GM_xmlhttpRequest({
url,
method,
headers,
data,
timeout,
responseType,
overrideMimeType,
onload: response => resolve(response),
onerror: err => reject(err || new Error("request failed")),
ontimeout: () => reject(new Error("request timeout"))
});
});
const createBrowserSearchRequestId = () =>
`${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
const getBrowserSearchResultKey = (requestId) =>
`${BROWSER_SEARCH_BRIDGE_RESULT_PREFIX}${requestId}`;
const readBrowserBridgeValue = (key, fallback = null) => {
try {
const raw = _GM_getValue(key, "");
if (!raw) return fallback;
return JSON.parse(String(raw));
} catch {
return fallback;
}
};
const writeBrowserBridgeValue = (key, value) => {
try {
_GM_setValue(key, JSON.stringify(value));
} catch {}
};
const clearBrowserBridgeValue = (key) => {
try {
_GM_setValue(key, "");
} catch {}
};
const parseHeaderValue = (headersText, key) => {
const match = String(headersText || "").match(new RegExp(`^${key}:\\s*([^\\r\\n]+)`, "im"));
return match?.[1]?.trim() || "";
};
const detectBrowserCharset = (contentType = "", htmlText = "") => {
const headerMatch = String(contentType || "").match(/charset=([^;]+)/i);
if (headerMatch?.[1]) return headerMatch[1].trim().toLowerCase();
const metaMatch = String(htmlText || "").match(/]+charset=["']?\\s*([^"'>/\\s]+)/i);
if (metaMatch?.[1]) return metaMatch[1].trim().toLowerCase();
return "";
};
const decodeBrowserArrayBuffer = (buffer, contentType = "") => {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || []);
const utf8Text = new TextDecoder("utf-8").decode(bytes);
const charset = detectBrowserCharset(contentType, utf8Text.slice(0, 2048));
if (!charset || /^utf-?8$/i.test(charset)) return utf8Text;
if (/^(gb2312|gbk|gb18030)$/i.test(charset)) {
try {
return new TextDecoder("gb18030").decode(bytes);
} catch {}
}
return utf8Text;
};
const normalizeBrowserHtmlEntities = (text) => String(text || "")
.replace(/ /gi, " ")
.replace(/&/gi, "&")
.replace(/</gi, "<")
.replace(/>/gi, ">")
.replace(/"/gi, '"')
.replace(/'/gi, "'")
.replace(/([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
.replace(/([0-9]+);/g, (_, num) => String.fromCharCode(parseInt(num, 10)));
const resolveBrowserUrl = (rawUrl, baseUrl = "") => {
const normalized = normalizeBrowserHtmlEntities(String(rawUrl || "").trim());
if (!normalized) return "";
try {
return new URL(normalized, baseUrl || window.location.href).href;
} catch {
return normalized;
}
};
const stripBrowserHtmlTags = (htmlText) => normalizeBrowserHtmlEntities(
String(htmlText || "")
.replace(/