From c07df0cfe17f6bb0b15191012f20a0e00a38224d Mon Sep 17 00:00:00 2001 From: wxs Date: Thu, 23 Apr 2026 19:59:04 +0800 Subject: [PATCH] feat: add yuntu evaluation task watcher script --- ...tcat-yuntu-evaluation-task-watcher.user.js | 1005 +++++++++++++++++ 1 file changed, 1005 insertions(+) create mode 100644 yuntu/yuntuEvaluateReportSubmit/scriptcat-yuntu-evaluation-task-watcher.user.js diff --git a/yuntu/yuntuEvaluateReportSubmit/scriptcat-yuntu-evaluation-task-watcher.user.js b/yuntu/yuntuEvaluateReportSubmit/scriptcat-yuntu-evaluation-task-watcher.user.js new file mode 100644 index 0000000..fdecb70 --- /dev/null +++ b/yuntu/yuntuEvaluateReportSubmit/scriptcat-yuntu-evaluation-task-watcher.user.js @@ -0,0 +1,1005 @@ +// ==UserScript== +// @name 结案报告监工 +// @namespace yuntu-evaluation-task-watcher +// @version 0.2.0 +// @description 监听云图坑位,按配置的报告ID列表依次启动已有报告 +// @match https://yuntu.oceanengine.com/yuntu_brand/ecom/evaluation/task_list* +// @require https://cdn.jsdelivr.net/npm/sweetalert2@11 +// @grant none +// @run-at document-idle +// ==/UserScript== + +(function () { + "use strict"; + + const STORAGE_KEY = "yuntu_evaluation_task_watcher_state_v1"; + const LAUNCHER_POSITION_KEY = "yuntu_evaluation_task_watcher_launcher_position_v1"; + const DEFAULT_PAGE_STATE = { + enabled: false, + importMode: "id", + manualTaskIds: [], + importKeyword: "", + watchTaskIds: [], + submittedTaskIds: [], + selectedBrandKey: "", + webhookUrl: "", + }; + const DEFAULT_CONFIG = { + industryVersion: "10005", + pollIntervalMs: 30000, + taskListRefreshIntervalMs: 60000, + submitCooldownMs: 4000, + }; + + const runtime = { + aadvid: "", + pageState: { ...DEFAULT_PAGE_STATE }, + brandContext: null, + busy: false, + timerId: null, + launcherEl: null, + dragState: null, + suppressClickOnce: false, + lastQuotaStatus: "未检查", + lastTaskListStatus: "未同步", + lastTaskListSyncAt: 0, + lastAction: "未执行", + latestSourceTasks: [], + logs: [], + }; + + const toast = Swal.mixin({ + toast: true, + position: "top-end", + showConfirmButton: false, + timer: 2500, + timerProgressBar: true, + }); + + function readStore() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : {}; + } catch (error) { + console.error("[结案报告监工] 读取本地存储失败", error); + return {}; + } + } + + function writeStore(store) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); + } + + function loadLauncherPosition() { + try { + const raw = localStorage.getItem(LAUNCHER_POSITION_KEY); + if (!raw) { + return null; + } + const data = JSON.parse(raw); + if (typeof data?.x !== "number" || typeof data?.y !== "number") { + return null; + } + return data; + } catch (error) { + console.error("[结案报告监工] 读取悬浮按钮位置失败", error); + return null; + } + } + + function saveLauncherPosition(position) { + localStorage.setItem(LAUNCHER_POSITION_KEY, JSON.stringify(position)); + } + + function loadPageState(aadvid) { + const store = readStore(); + const saved = store[aadvid] || {}; + return { + ...DEFAULT_PAGE_STATE, + ...saved, + importMode: ["id", "keyword", "mine"].includes(saved.importMode) ? saved.importMode : "id", + manualTaskIds: dedupeTaskIds(saved.manualTaskIds || saved.watchTaskIds || []), + importKeyword: String(saved.importKeyword || ""), + watchTaskIds: dedupeTaskIds(saved.watchTaskIds || []), + submittedTaskIds: dedupeTaskIds(saved.submittedTaskIds || []), + selectedBrandKey: String(saved.selectedBrandKey || ""), + webhookUrl: String(saved.webhookUrl || ""), + }; + } + + function savePageState() { + const store = readStore(); + store[runtime.aadvid] = { + enabled: runtime.pageState.enabled, + importMode: runtime.pageState.importMode, + manualTaskIds: runtime.pageState.manualTaskIds, + importKeyword: runtime.pageState.importKeyword, + watchTaskIds: runtime.pageState.watchTaskIds, + submittedTaskIds: runtime.pageState.submittedTaskIds, + selectedBrandKey: runtime.pageState.selectedBrandKey, + webhookUrl: runtime.pageState.webhookUrl, + }; + writeStore(store); + } + + function dedupeTaskIds(values) { + const unique = new Set(); + for (const value of values) { + const taskId = String(value || "").trim(); + if (taskId) { + unique.add(taskId); + } + } + return Array.from(unique); + } + + function parseTaskIds(text) { + return dedupeTaskIds(String(text || "").split(/[\s,\n,]+/)); + } + + function clampLauncherPosition(x, y) { + if (!runtime.launcherEl) { + return { x, y }; + } + + const rect = runtime.launcherEl.getBoundingClientRect(); + const maxX = Math.max(8, window.innerWidth - rect.width - 8); + const maxY = Math.max(8, window.innerHeight - rect.height - 8); + + return { + x: Math.min(Math.max(8, x), maxX), + y: Math.min(Math.max(8, y), maxY), + }; + } + + function applyLauncherPosition(position) { + if (!runtime.launcherEl) { + return; + } + + const next = clampLauncherPosition(position.x, position.y); + runtime.launcherEl.style.left = `${next.x}px`; + runtime.launcherEl.style.top = `${next.y}px`; + runtime.launcherEl.style.right = "auto"; + runtime.launcherEl.style.bottom = "auto"; + } + + function getPendingTaskIds() { + const submitted = new Set(runtime.pageState.submittedTaskIds); + return runtime.pageState.watchTaskIds.filter((taskId) => !submitted.has(taskId)); + } + + function getAadvidFromUrl() { + return new URL(window.location.href).searchParams.get("aadvid") || ""; + } + + function pushLog(message) { + const entry = `${new Date().toLocaleTimeString()} ${message}`; + runtime.logs.unshift(entry); + runtime.logs = runtime.logs.slice(0, 12); + console.log("[结案报告监工]", message); + } + + function showToast(icon, title, text) { + pushLog(text || title); + renderLauncher(); + return toast.fire({ + icon, + title, + text, + }); + } + + function getModeLabel(mode = runtime.pageState.importMode) { + if (mode === "keyword") { + return "关键词导入"; + } + if (mode === "mine") { + return "我创建的"; + } + return "ID导入"; + } + + function formatSyncTime(timestamp) { + if (!timestamp) { + return "未同步"; + } + return new Date(timestamp).toLocaleTimeString(); + } + + function buildHeaders(pathId, contentType) { + const headers = { + Accept: "application/json, text/plain, */*", + "x-request-start": String(Date.now()), + }; + + if (pathId) { + headers["x-path-id"] = pathId; + } + + if (contentType) { + headers["Content-Type"] = contentType; + } + + if (runtime.brandContext?.industryVersion) { + headers["x-industry-version"] = runtime.brandContext.industryVersion; + } + + return headers; + } + + async function requestJson(url, options = {}) { + const response = await fetch(url, { + method: options.method || "GET", + credentials: "include", + headers: buildHeaders(options.pathId, options.contentType), + body: options.body ? JSON.stringify(options.body) : undefined, + referrer: options.referrer || window.location.href, + referrerPolicy: "strict-origin-when-cross-origin", + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + return response.json(); + } + + async function sendWebhookNotification(taskId) { + const webhookUrl = String(runtime.pageState.webhookUrl || "").trim(); + if (!webhookUrl) { + return; + } + + const pendingCount = getPendingTaskIds().length; + const message = + `结案报告监工通知:品牌 ${runtime.brandContext.brandName}(${runtime.brandContext.mainBrandId}) ` + + `已启动报告 ${taskId},剩余待提交 ${pendingCount} 个。`; + + const response = await fetch(webhookUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + msg_type: "text", + content: { + text: message, + }, + }), + }); + + if (!response.ok) { + throw new Error(`Webhook HTTP ${response.status}`); + } + } + + function formatBrandOption(meta) { + return `${meta.brand_name} (${meta.brand_id}) / ${meta.industry_name} (${meta.industry_id})`; + } + + function normalizeBrandMetadata(list) { + const candidates = []; + const seen = new Set(); + + for (const item of Array.isArray(list) ? list : []) { + const industryId = String(item?.industry_id || ""); + if (industryId.length !== 2) { + continue; + } + + const brandId = String(item?.brand_id || ""); + const brandName = String(item?.brand_name || ""); + const industryName = String(item?.industry_name || ""); + const key = `${brandId}|${industryId}`; + + if (!brandId || seen.has(key)) { + continue; + } + + seen.add(key); + candidates.push({ + key, + brandId, + brandName, + industryId, + industryName, + }); + } + + return candidates; + } + + async function fetchUserInfoContext() { + const url = `https://yuntu.oceanengine.com/yuntu_ng/api/v1/get_user_info?aadvid=${encodeURIComponent(runtime.aadvid)}`; + const data = await requestJson(url, { + method: "GET", + referrer: window.location.href, + }); + + const brandMetadata = normalizeBrandMetadata(data?.data?.brandMetadata); + if (brandMetadata.length === 0) { + throw new Error("未从 get_user_info 中解析到品牌和行业信息"); + } + + let selected = brandMetadata.find((item) => item.key === runtime.pageState.selectedBrandKey) || null; + + if (!selected && brandMetadata.length > 1) { + const inputOptions = {}; + for (const item of brandMetadata) { + inputOptions[item.key] = formatBrandOption(item); + } + + const result = await Swal.fire({ + title: "选择品牌", + input: "select", + inputOptions, + inputPlaceholder: "请选择当前要监听的品牌", + confirmButtonText: "确认", + allowOutsideClick: false, + allowEscapeKey: false, + inputValidator: (value) => { + if (!value) { + return "需要先选择品牌"; + } + return undefined; + }, + text: `aadvid=${runtime.aadvid}`, + }); + + if (!result.isConfirmed) { + throw new Error("未选择品牌,脚本未启动"); + } + + selected = brandMetadata.find((item) => item.key === result.value) || null; + } + + if (!selected) { + selected = brandMetadata[0]; + } + + runtime.pageState.selectedBrandKey = selected.key; + savePageState(); + + return { + aadvid: runtime.aadvid, + mainBrandId: selected.brandId, + brandName: selected.brandName, + level1IndustryId: selected.industryId, + industryName: selected.industryName, + industryVersion: DEFAULT_CONFIG.industryVersion, + currentUserId: String(data?.data?.userInfo?.user?.id || ""), + }; + } + + async function checkQuota() { + const url = + `https://yuntu.oceanengine.com/yuntu_common/api/v1/evaluation/create/check_task_creation_rate_limit` + + `?aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const payload = { + main_brand_id: runtime.brandContext.mainBrandId, + level_1_industry_id: runtime.brandContext.level1IndustryId, + }; + + const referrer = + `https://yuntu.oceanengine.com/yuntu_brand/ecom/evaluation/task_create` + + `?business_type=0&industry_version_type=${encodeURIComponent(runtime.brandContext.industryVersion)}` + + `&version=0&aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const data = await requestJson(url, { + method: "POST", + pathId: "evaluation_task_create", + contentType: "application/json", + body: payload, + referrer, + }); + + return { + isAbleToCreate: String(data?.data?.is_able_to_create || "") === "1", + message: data?.data?.msg || data?.msg || "无响应信息", + }; + } + + async function startExistingTask(taskId) { + const url = + `https://yuntu.oceanengine.com/yuntu_common/api/v1/ecomTaskList/start_calaulate_evaluation_task` + + `?aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const payload = { + main_brand_id: runtime.brandContext.mainBrandId, + level_1_industry_id: runtime.brandContext.level1IndustryId, + task_id: String(taskId), + }; + + const referrer = + `https://yuntu.oceanengine.com/yuntu_brand/ecom/evaluation/task_list` + + `?aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const data = await requestJson(url, { + method: "POST", + pathId: "evaluation_task_list", + contentType: "application/json", + body: payload, + referrer, + }); + + return { + success: data?.status === 0 && data?.data?.is_success === true, + message: data?.msg || "无响应信息", + }; + } + + async function fetchEvaluationTaskList(searchWord = "") { + const url = + `https://yuntu.oceanengine.com/yuntu_common/api/v1/ecomTaskList/get_evaluation_task_list` + + `?aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const payload = { + main_brand_id: runtime.brandContext.mainBrandId, + level_1_industry_id: runtime.brandContext.level1IndustryId, + page_num: "1", + page_size: "100", + order_type: 1, + }; + + if (searchWord) { + payload.search_word = searchWord; + } + + const referrer = + `https://yuntu.oceanengine.com/yuntu_brand/ecom/evaluation/task_list` + + `?aadvid=${encodeURIComponent(runtime.brandContext.aadvid)}`; + + const data = await requestJson(url, { + method: "POST", + pathId: "evaluation_task_list", + contentType: "application/json", + body: payload, + referrer, + }); + + const taskList = Array.isArray(data?.data?.task_list) ? data.data.task_list : []; + return taskList.map((item) => ({ + taskId: String(item?.task_id || "").trim(), + taskName: String(item?.task_name || "").trim(), + taskStatus: Number(item?.task_status), + userId: String(item?.user_id || "").trim(), + createTime: String(item?.create_time || "").trim(), + })).filter((item) => item.taskId); + } + + async function syncTaskSource(force = false) { + const now = Date.now(); + if (!force && now - runtime.lastTaskListSyncAt < DEFAULT_CONFIG.taskListRefreshIntervalMs) { + return; + } + + const mode = runtime.pageState.importMode; + let sourceTasks = []; + + if (mode === "id") { + const manualIds = dedupeTaskIds(runtime.pageState.manualTaskIds || []); + if (manualIds.length === 0) { + runtime.latestSourceTasks = []; + runtime.pageState.watchTaskIds = []; + runtime.lastTaskListStatus = "ID列表为空"; + runtime.lastTaskListSyncAt = now; + savePageState(); + renderLauncher(); + return; + } + + const selectedIdSet = new Set(manualIds); + const taskList = await fetchEvaluationTaskList(); + sourceTasks = taskList.filter((item) => item.taskStatus === 0 && selectedIdSet.has(item.taskId)); + } else if (mode === "keyword") { + const keyword = String(runtime.pageState.importKeyword || "").trim(); + if (!keyword) { + runtime.latestSourceTasks = []; + runtime.pageState.watchTaskIds = []; + runtime.lastTaskListStatus = "关键词为空"; + runtime.lastTaskListSyncAt = now; + savePageState(); + renderLauncher(); + return; + } + + const taskList = await fetchEvaluationTaskList(keyword); + sourceTasks = taskList.filter((item) => item.taskStatus === 0); + } else { + const currentUserId = String(runtime.brandContext.currentUserId || ""); + const taskList = await fetchEvaluationTaskList(); + sourceTasks = taskList.filter((item) => item.taskStatus === 0 && item.userId === currentUserId); + } + + runtime.latestSourceTasks = sourceTasks; + runtime.pageState.watchTaskIds = dedupeTaskIds(sourceTasks.map((item) => item.taskId)); + runtime.lastTaskListStatus = `${getModeLabel(mode)}匹配 ${sourceTasks.length} 个待提交`; + runtime.lastTaskListSyncAt = now; + savePageState(); + renderLauncher(); + } + + function persistControlDialogValues(root = document) { + const modeInput = root.querySelector("#ysw-import-mode"); + const taskIdsInput = root.querySelector("#ysw-task-ids"); + const keywordInput = root.querySelector("#ysw-keyword"); + const webhookInput = root.querySelector("#ysw-webhook-url"); + + runtime.pageState.importMode = String(modeInput ? modeInput.value : "id"); + runtime.pageState.manualTaskIds = parseTaskIds(taskIdsInput ? taskIdsInput.value : ""); + runtime.pageState.importKeyword = String(keywordInput ? keywordInput.value : "").trim(); + runtime.pageState.webhookUrl = String(webhookInput ? webhookInput.value : "").trim(); + savePageState(); + renderLauncher(); + } + + function stopPolling(showNotice = true) { + if (runtime.timerId !== null) { + window.clearInterval(runtime.timerId); + runtime.timerId = null; + } + + runtime.pageState.enabled = false; + savePageState(); + renderLauncher(); + + if (showNotice) { + showToast("info", "监听已停止", `aadvid=${runtime.aadvid}`); + } + } + + function startPolling() { + stopPolling(false); + runtime.pageState.enabled = true; + savePageState(); + runtime.timerId = window.setInterval(runCycle, DEFAULT_CONFIG.pollIntervalMs); + renderLauncher(); + showToast("success", "开始监听", `${runtime.brandContext.brandName} / aadvid=${runtime.aadvid}`); + runCycle(); + } + + async function runCycle() { + if (!runtime.pageState.enabled || runtime.busy) { + return; + } + + runtime.busy = true; + renderLauncher(); + + try { + await syncTaskSource(); + } catch (error) { + runtime.lastAction = "同步报告列表失败"; + runtime.lastTaskListStatus = `同步失败: ${error.message}`; + await showToast("error", "同步报告列表失败", error.message); + runtime.busy = false; + renderLauncher(); + return; + } + + const pendingTaskIds = getPendingTaskIds(); + if (pendingTaskIds.length === 0) { + runtime.lastAction = "当前无待提交报告,继续等待"; + renderLauncher(); + runtime.busy = false; + renderLauncher(); + return; + } + + try { + const quota = await checkQuota(); + runtime.lastQuotaStatus = quota.isAbleToCreate ? "有坑位" : quota.message; + + if (!quota.isAbleToCreate) { + runtime.lastAction = "本轮未提交"; + pushLog(`暂无坑位: ${quota.message}`); + return; + } + + const nextTaskId = pendingTaskIds[0]; + runtime.lastAction = `准备启动报告 ${nextTaskId}`; + + const startResult = await startExistingTask(nextTaskId); + if (!startResult.success) { + runtime.lastAction = `启动失败 ${nextTaskId}`; + await showToast("error", "启动失败", `报告 ${nextTaskId} 启动失败: ${startResult.message}`); + return; + } + + runtime.pageState.submittedTaskIds = dedupeTaskIds([ + ...runtime.pageState.submittedTaskIds, + nextTaskId, + ]); + savePageState(); + renderLauncher(); + + runtime.lastAction = `已启动报告 ${nextTaskId}`; + try { + await sendWebhookNotification(nextTaskId); + } catch (error) { + pushLog(`Webhook 发送失败: ${error.message}`); + } + runtime.lastTaskListSyncAt = 0; + await showToast("success", "启动成功", `报告 ${nextTaskId} 已提交计算`); + window.setTimeout(runCycle, DEFAULT_CONFIG.submitCooldownMs); + } catch (error) { + runtime.lastAction = "执行异常"; + await showToast("error", "执行异常", error.message); + } finally { + runtime.busy = false; + renderLauncher(); + } + } + + function buildControlHtml() { + const pendingCount = getPendingTaskIds().length; + const submittedCount = runtime.pageState.submittedTaskIds.length; + const logs = runtime.logs.length > 0 ? runtime.logs.slice(0, 8).join("
") : "暂无日志"; + const currentPendingList = + runtime.latestSourceTasks.length > 0 + ? runtime.latestSourceTasks + .slice(0, 20) + .map((item) => `
${escapeHtml(item.taskName || `报告 ${item.taskId}`)} (${escapeHtml(item.taskId)})
`) + .join("") + : "
暂无匹配的待提交报告
"; + + return [ + `
`, + `
aadvid:${runtime.aadvid}
`, + `
品牌:${runtime.brandContext.brandName} (${runtime.brandContext.mainBrandId})
`, + `
行业:${runtime.brandContext.industryName} (${runtime.brandContext.level1IndustryId})
`, + `
当前用户ID:${runtime.brandContext.currentUserId || "未识别"}
`, + `
导入方式:${getModeLabel()}
`, + `
运行状态:${runtime.pageState.enabled ? "监听中" : "已停止"}
`, + `
坑位状态:${runtime.lastQuotaStatus}
`, + `
报告同步:${runtime.lastTaskListStatus}
`, + `
上次同步:${formatSyncTime(runtime.lastTaskListSyncAt)}
`, + `
最近动作:${runtime.lastAction}
`, + `
待提交 / 已提交:${pendingCount} / ${submittedCount}
`, + `
Webhook:${runtime.pageState.webhookUrl ? "已配置" : "未配置"}
`, + `
当前待提交报告:
${currentPendingList}${runtime.latestSourceTasks.length > 20 ? `
其余 ${runtime.latestSourceTasks.length - 20} 个未展开
` : ""}
`, + `
最近日志:
${logs}
`, + `
点击右下角悬浮按钮可再次打开配置
`, + `
`, + ].join(""); + } + + async function openControlDialog() { + const stateLabel = runtime.pageState.enabled ? "保存并继续监听" : "保存并开始监听"; + const denyLabel = runtime.pageState.enabled ? "停止监听" : "仅保存"; + + const result = await Swal.fire({ + title: "结案报告监工", + html: + `${buildControlHtml()}` + + `
` + + `` + + `` + + `
` + + `
` + + `` + + `` + + `
` + + `
` + + `` + + `` + + `
` + + `
` + + `` + + `` + + `
`, + showCancelButton: true, + showDenyButton: true, + showCloseButton: true, + focusConfirm: false, + confirmButtonText: stateLabel, + denyButtonText: denyLabel, + cancelButtonText: "取消", + footer: '', + didOpen: (popup) => { + const modeSelect = popup.querySelector("#ysw-import-mode"); + const idWrapper = popup.querySelector("#ysw-id-wrapper"); + const keywordWrapper = popup.querySelector("#ysw-keyword-wrapper"); + const clearButton = popup.querySelector("#ysw-clear-submitted"); + if (modeSelect && idWrapper && keywordWrapper) { + const toggleModeFields = () => { + const mode = modeSelect.value; + idWrapper.style.display = mode === "id" ? "block" : "none"; + keywordWrapper.style.display = mode === "keyword" ? "block" : "none"; + }; + toggleModeFields(); + modeSelect.addEventListener("change", toggleModeFields); + } + + if (!clearButton) { + return; + } + + clearButton.addEventListener("click", async () => { + const confirmResult = await Swal.fire({ + title: "确认清空已提交记录?", + text: "清空之后,脚本会重新启动列表里已经成功提交过的报告。", + icon: "warning", + showCancelButton: true, + confirmButtonText: "确认清空", + cancelButtonText: "取消", + }); + + if (!confirmResult.isConfirmed) { + return; + } + + runtime.pageState.submittedTaskIds = []; + savePageState(); + renderLauncher(); + await showToast("success", "已清空", "已提交记录已经清空"); + Swal.close(); + openControlDialog(); + }); + }, + preConfirm: () => { + persistControlDialogValues(document); + if (runtime.pageState.importMode === "id" && runtime.pageState.manualTaskIds.length === 0) { + Swal.showValidationMessage("ID导入模式下请至少填写一个报告ID"); + return false; + } + if (runtime.pageState.importMode === "keyword" && !runtime.pageState.importKeyword) { + Swal.showValidationMessage("关键词导入模式下请输入关键词"); + return false; + } + return true; + }, + preDeny: () => { + persistControlDialogValues(document); + return true; + }, + }); + + if (result.isConfirmed) { + try { + await syncTaskSource(true); + } catch (error) { + await showToast("error", "同步报告列表失败", error.message); + openControlDialog(); + return; + } + startPolling(); + return; + } + + if (result.isDenied) { + if (runtime.pageState.enabled) { + stopPolling(); + } else { + try { + await syncTaskSource(true); + } catch (error) { + pushLog(`保存后同步失败: ${error.message}`); + } + await showToast("success", "已保存", `当前模式:${getModeLabel(runtime.pageState.importMode)}`); + } + } + } + + function ensureLauncher() { + if (runtime.launcherEl) { + return; + } + + const style = document.createElement("style"); + style.textContent = ` + #ysw-launcher { + position: fixed; + right: 16px; + bottom: 20px; + z-index: 999999; + width: 196px; + padding: 10px 12px; + border: 0; + border-radius: 14px; + background: linear-gradient(135deg, #1677ff, #0f56c3); + color: #fff; + box-shadow: 0 10px 30px rgba(22, 119, 255, 0.28); + cursor: pointer; + user-select: none; + text-align: left; + font: 12px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + } + #ysw-launcher:hover { + filter: brightness(1.05); + } + #ysw-launcher .ysw-launcher-title { + display: block; + font-size: 14px; + font-weight: 700; + } + #ysw-launcher .ysw-launcher-status, + #ysw-launcher .ysw-launcher-count { + display: block; + opacity: 0.92; + } + `; + + const button = document.createElement("button"); + button.id = "ysw-launcher"; + button.type = "button"; + button.innerHTML = ` + 结案报告监工 + + + `; + button.addEventListener("click", () => { + if (runtime.suppressClickOnce) { + runtime.suppressClickOnce = false; + return; + } + openControlDialog(); + }); + + button.addEventListener("mousedown", (event) => { + if (event.button !== 0) { + return; + } + + const rect = button.getBoundingClientRect(); + runtime.dragState = { + startX: event.clientX, + startY: event.clientY, + offsetX: event.clientX - rect.left, + offsetY: event.clientY - rect.top, + moved: false, + }; + }); + + document.head.appendChild(style); + document.body.appendChild(button); + runtime.launcherEl = button; + + const savedPosition = loadLauncherPosition(); + if (savedPosition) { + applyLauncherPosition(savedPosition); + } else { + const rect = button.getBoundingClientRect(); + applyLauncherPosition({ + x: window.innerWidth - rect.width - 16, + y: window.innerHeight - rect.height - 20, + }); + } + + window.addEventListener("mousemove", (event) => { + if (!runtime.dragState || !runtime.launcherEl) { + return; + } + + const nextX = event.clientX - runtime.dragState.offsetX; + const nextY = event.clientY - runtime.dragState.offsetY; + const movedDistance = Math.abs(event.clientX - runtime.dragState.startX) + Math.abs(event.clientY - runtime.dragState.startY); + + if (movedDistance > 4) { + runtime.dragState.moved = true; + } + + applyLauncherPosition({ x: nextX, y: nextY }); + }); + + window.addEventListener("mouseup", () => { + if (!runtime.dragState || !runtime.launcherEl) { + return; + } + + if (runtime.dragState.moved) { + const rect = runtime.launcherEl.getBoundingClientRect(); + saveLauncherPosition({ x: rect.left, y: rect.top }); + runtime.suppressClickOnce = true; + } + + runtime.dragState = null; + }); + + window.addEventListener("resize", () => { + if (!runtime.launcherEl) { + return; + } + + const rect = runtime.launcherEl.getBoundingClientRect(); + applyLauncherPosition({ x: rect.left, y: rect.top }); + }); + + renderLauncher(); + } + + function renderLauncher() { + if (!runtime.launcherEl) { + return; + } + + const statusEl = runtime.launcherEl.querySelector(".ysw-launcher-status"); + const countEl = runtime.launcherEl.querySelector(".ysw-launcher-count"); + const pendingCount = getPendingTaskIds().length; + const statusText = runtime.pageState.enabled + ? runtime.busy + ? "状态:执行中" + : "状态:监听中" + : "状态:已停止"; + + statusEl.textContent = statusText; + countEl.textContent = `待提交:${pendingCount} 已提交:${runtime.pageState.submittedTaskIds.length}`; + } + + function escapeHtml(value) { + return String(value || "") + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); + } + + async function init() { + runtime.aadvid = getAadvidFromUrl(); + if (!runtime.aadvid) { + await Swal.fire({ + icon: "error", + title: "缺少 aadvid", + text: "当前 URL 中没有解析到 aadvid,脚本无法运行。", + }); + return; + } + + runtime.pageState = loadPageState(runtime.aadvid); + ensureLauncher(); + + try { + runtime.brandContext = await fetchUserInfoContext(); + } catch (error) { + await Swal.fire({ + icon: "error", + title: "初始化失败", + text: error.message, + }); + return; + } + + const hasConfig = + (runtime.pageState.importMode === "id" && runtime.pageState.manualTaskIds.length > 0) || + (runtime.pageState.importMode === "keyword" && !!runtime.pageState.importKeyword) || + runtime.pageState.importMode === "mine"; + + if (hasConfig) { + try { + await syncTaskSource(true); + } catch (error) { + pushLog(`初始化同步失败: ${error.message}`); + } + } + + if (!hasConfig) { + await Swal.fire({ + icon: "info", + title: "首次配置", + text: `已识别到 ${runtime.brandContext.brandName},请先选择导入方式并完成配置。`, + }); + openControlDialog(); + return; + } + + if (runtime.pageState.enabled) { + startPolling(); + return; + } + + await showToast( + "info", + "脚本已加载", + `${runtime.brandContext.brandName} 已识别,点击右下角“结案报告监工”打开配置` + ); + } + + init(); +})();