1006 lines
32 KiB
JavaScript
1006 lines
32 KiB
JavaScript
// ==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("&", "&")
|
||
.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();
|
||
})();
|