scriptCat/yuntu/yuntuEvaluateReportSubmit/scriptcat-yuntu-evaluation-task-watcher.user.js

1006 lines
32 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ==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("<br>") : "暂无日志";
const currentPendingList =
runtime.latestSourceTasks.length > 0
? runtime.latestSourceTasks
.slice(0, 20)
.map((item) => `<div>${escapeHtml(item.taskName || `报告 ${item.taskId}`)} (${escapeHtml(item.taskId)})</div>`)
.join("")
: "<div>暂无匹配的待提交报告</div>";
return [
`<div style="text-align:left;font-size:14px;line-height:1.7">`,
`<div><b>aadvid</b>${runtime.aadvid}</div>`,
`<div><b>品牌:</b>${runtime.brandContext.brandName} (${runtime.brandContext.mainBrandId})</div>`,
`<div><b>行业:</b>${runtime.brandContext.industryName} (${runtime.brandContext.level1IndustryId})</div>`,
`<div><b>当前用户ID</b>${runtime.brandContext.currentUserId || "未识别"}</div>`,
`<div><b>导入方式:</b>${getModeLabel()}</div>`,
`<div><b>运行状态:</b>${runtime.pageState.enabled ? "监听中" : "已停止"}</div>`,
`<div><b>坑位状态:</b>${runtime.lastQuotaStatus}</div>`,
`<div><b>报告同步:</b>${runtime.lastTaskListStatus}</div>`,
`<div><b>上次同步:</b>${formatSyncTime(runtime.lastTaskListSyncAt)}</div>`,
`<div><b>最近动作:</b>${runtime.lastAction}</div>`,
`<div><b>待提交 / 已提交:</b>${pendingCount} / ${submittedCount}</div>`,
`<div><b>Webhook</b>${runtime.pageState.webhookUrl ? "已配置" : "未配置"}</div>`,
`<div style="margin-top:10px"><b>当前待提交报告:</b><div style="max-height:180px;overflow:auto;background:#f7f7f7;border-radius:8px;padding:8px;margin-top:4px">${currentPendingList}${runtime.latestSourceTasks.length > 20 ? `<div style="margin-top:6px;color:#666">其余 ${runtime.latestSourceTasks.length - 20} 个未展开</div>` : ""}</div></div>`,
`<div style="margin-top:10px"><b>最近日志:</b><div style="max-height:120px;overflow:auto;background:#f7f7f7;border-radius:8px;padding:8px;margin-top:4px">${logs}</div></div>`,
`<div style="margin-top:10px;font-size:12px;color:#666">点击右下角悬浮按钮可再次打开配置</div>`,
`</div>`,
].join("");
}
async function openControlDialog() {
const stateLabel = runtime.pageState.enabled ? "保存并继续监听" : "保存并开始监听";
const denyLabel = runtime.pageState.enabled ? "停止监听" : "仅保存";
const result = await Swal.fire({
title: "结案报告监工",
html:
`${buildControlHtml()}` +
`<div style="text-align:left;margin-top:14px">` +
`<label for="ysw-import-mode" style="display:block;margin-bottom:6px;font-weight:600">导入方式</label>` +
`<select id="ysw-import-mode" class="swal2-select" style="display:block;margin:0;width:100%">` +
`<option value="id"${runtime.pageState.importMode === "id" ? " selected" : ""}>ID导入</option>` +
`<option value="keyword"${runtime.pageState.importMode === "keyword" ? " selected" : ""}>关键词导入</option>` +
`<option value="mine"${runtime.pageState.importMode === "mine" ? " selected" : ""}>我创建的</option>` +
`</select>` +
`</div>` +
`<div id="ysw-id-wrapper" style="text-align:left;margin-top:14px;display:${runtime.pageState.importMode === "id" ? "block" : "none"}">` +
`<label for="ysw-task-ids" style="display:block;margin-bottom:6px;font-weight:600">报告ID列表</label>` +
`<textarea id="ysw-task-ids" class="swal2-textarea" style="display:block;height:140px;margin:0;width:100%" placeholder="一行一个报告ID也支持逗号或空格分隔">${escapeHtml(runtime.pageState.manualTaskIds.join("\n"))}</textarea>` +
`</div>` +
`<div id="ysw-keyword-wrapper" style="text-align:left;margin-top:14px;display:${runtime.pageState.importMode === "keyword" ? "block" : "none"}">` +
`<label for="ysw-keyword" style="display:block;margin-bottom:6px;font-weight:600">关键词</label>` +
`<input id="ysw-keyword" class="swal2-input" style="display:block;margin:0;width:100%" placeholder="例如:新建报告" value="${escapeHtml(runtime.pageState.importKeyword)}">` +
`</div>` +
`<div style="text-align:left;margin-top:14px">` +
`<label for="ysw-webhook-url" style="display:block;margin-bottom:6px;font-weight:600">飞书 Webhook</label>` +
`<input id="ysw-webhook-url" class="swal2-input" style="display:block;margin:0;width:100%" placeholder="https://open.feishu.cn/..." value="${escapeHtml(runtime.pageState.webhookUrl)}">` +
`</div>`,
showCancelButton: true,
showDenyButton: true,
showCloseButton: true,
focusConfirm: false,
confirmButtonText: stateLabel,
denyButtonText: denyLabel,
cancelButtonText: "取消",
footer: '<button type="button" id="ysw-clear-submitted" class="swal2-styled" style="background:#d33">清空已提交记录</button>',
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 = `
<span class="ysw-launcher-title">结案报告监工</span>
<span class="ysw-launcher-status"></span>
<span class="ysw-launcher-count"></span>
`;
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}
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();
})();