// ==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) => `