feat: add job details and benchmark actions to web v4
This commit is contained in:
@@ -45,6 +45,8 @@
|
||||
- 创建 Agent
|
||||
- 对当前 Douyin 对标账号重跑分析
|
||||
- 批量分析高分作品
|
||||
- 查找相似对标账号
|
||||
- 查看任务详情、事件和 artifacts/result
|
||||
- 使用 Agent 生成文案
|
||||
- 创建 AI 视频任务
|
||||
- 创建实拍剪辑任务
|
||||
|
||||
@@ -23,7 +23,9 @@ const appState = {
|
||||
busy: false,
|
||||
message: "",
|
||||
lastAction: null,
|
||||
lastGeneratedCopy: null
|
||||
lastGeneratedCopy: null,
|
||||
lastSimilaritySearch: null,
|
||||
lastJobDetail: null
|
||||
};
|
||||
|
||||
function safeArray(value) {
|
||||
@@ -274,6 +276,14 @@ function ensureActionUi() {
|
||||
function renderActionFields(fields) {
|
||||
return fields.map((field) => {
|
||||
const common = `data-action-field="${escapeHtml(field.name)}"`;
|
||||
if (field.type === "html") {
|
||||
return `
|
||||
<div class="field-stack">
|
||||
<label>${escapeHtml(field.label || "")}</label>
|
||||
<div class="sheet-html">${field.html || ""}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (field.type === "textarea") {
|
||||
return `
|
||||
<div class="field-stack">
|
||||
@@ -342,6 +352,7 @@ function openActionModal(config) {
|
||||
message.textContent = "";
|
||||
submit.textContent = config.submitLabel || "执行";
|
||||
submit.disabled = false;
|
||||
submit.hidden = Boolean(config.hideSubmit);
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
|
||||
@@ -468,6 +479,8 @@ async function logoutSession() {
|
||||
appState.documents = [];
|
||||
appState.lastAction = null;
|
||||
appState.lastGeneratedCopy = null;
|
||||
appState.lastSimilaritySearch = null;
|
||||
appState.lastJobDetail = null;
|
||||
renderAll();
|
||||
}
|
||||
|
||||
@@ -656,6 +669,15 @@ function getVideoLink(video) {
|
||||
return video.share_url || video.play_url || "";
|
||||
}
|
||||
|
||||
async function loadJobDetail(jobId) {
|
||||
const [job, events] = await Promise.all([
|
||||
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}`),
|
||||
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}/events`).catch(() => [])
|
||||
]);
|
||||
appState.lastJobDetail = { job, events: safeArray(events) };
|
||||
return appState.lastJobDetail;
|
||||
}
|
||||
|
||||
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
|
||||
return `
|
||||
<div class="screen-head">
|
||||
@@ -888,10 +910,11 @@ function renderDiscoveryScreen() {
|
||||
const videos = safeArray(appState.selectedVideos?.items);
|
||||
const topVideos = getHighScoreVideos(3);
|
||||
const latestVideos = getLatestVideos(2);
|
||||
const similarCandidates = safeArray(appState.lastSimilaritySearch?.candidates).slice(0, 5);
|
||||
return screenShell(
|
||||
"找对标",
|
||||
"这里已经接入真实抖音账号列表和单账号详情。",
|
||||
`${button("导入主页", "open-import-homepage")} ${button("账号分析", "analyze-selected-account")} ${button("高分分析", "analyze-top-videos")} ${button("切到生产", "goto-production", "primary")}`,
|
||||
`${button("导入主页", "open-import-homepage")} ${button("账号分析", "analyze-selected-account")} ${button("高分分析", "analyze-top-videos")} ${button("查相似", "open-similar-search")} ${button("存对标", "open-benchmark-link", "primary")}`,
|
||||
`
|
||||
<div class="panel">
|
||||
<div class="toolbar">
|
||||
@@ -1026,6 +1049,21 @@ function renderDiscoveryScreen() {
|
||||
`).join("") || `<div class="task-item"><h4>暂无已保存对标</h4><p>当前账号还没有保存过对标关系。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel pad" style="box-shadow:none;">
|
||||
<div class="panel-head"><div><h3>最近相似候选</h3><div class="panel-subtitle">由 Agent 辅助生成</div></div><span class="tag">${escapeHtml(formatNumber(similarCandidates.length))} 个</span></div>
|
||||
<div class="list">
|
||||
${similarCandidates.map((candidate) => `
|
||||
<div class="task-item">
|
||||
<h4>${escapeHtml(candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号")}</h4>
|
||||
<p>${escapeHtml(brief(candidate.rationale_text || "暂无理由", 96))}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag blue">启发分 ${escapeHtml(formatNumber(candidate.agent_score || candidate.heuristic_score || 0))}</span>
|
||||
${candidate.candidate_profile_url ? `<a class="tag" href="${escapeHtml(candidate.candidate_profile_url)}" target="_blank" rel="noreferrer">打开主页</a>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item"><h4>还没有相似候选</h4><p>先点“查相似”,这里会展示最近一轮结果。</p></div>`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1251,6 +1289,7 @@ function renderProductionScreen() {
|
||||
<div class="task-meta">
|
||||
<span class="tag ${statusTone(job.status)}">${escapeHtml(job.status)}</span>
|
||||
<span class="tag">${escapeHtml(job.line_type || "analysis")}</span>
|
||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
|
||||
@@ -1304,7 +1343,7 @@ function renderReviewScreen() {
|
||||
<div class="review-card">
|
||||
<h4>${escapeHtml(job.title)}</h4>
|
||||
<p>${escapeHtml(brief(job.style_summary || job.transcript_text || "已完成,待补复盘。", 84))}</p>
|
||||
<div class="task-meta"><span class="tag green">已完成</span><span class="tag">${escapeHtml(job.line_type || "analysis")}</span></div>
|
||||
<div class="task-meta"><span class="tag green">已完成</span><span class="tag">${escapeHtml(job.line_type || "analysis")}</span><span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span></div>
|
||||
</div>
|
||||
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
|
||||
</div>
|
||||
@@ -1717,6 +1756,131 @@ function openAnalyzeTopVideosAction() {
|
||||
});
|
||||
}
|
||||
|
||||
function openSimilaritySearchAction() {
|
||||
const account = requireSelectedAccountRow();
|
||||
openActionModal({
|
||||
title: "查相似账号",
|
||||
description: "让 Agent 基于当前账号画像找更多可借鉴对象。",
|
||||
submitLabel: "开始查找",
|
||||
fields: [
|
||||
{ name: "maxCandidates", label: "最多候选数", type: "number", value: 8, min: 3, max: 20 },
|
||||
{ name: "extraRequirements", label: "额外要求", type: "textarea", rows: 4, placeholder: "例如:优先找创业成交类、口播结构强的账号" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
const created = await storyforgeFetch("/v2/douyin/similar-searches", {
|
||||
method: "POST",
|
||||
body: {
|
||||
source_account_id: account.id,
|
||||
candidate_urls: [],
|
||||
seed_linked_accounts: true,
|
||||
search_public_pages: true,
|
||||
model_profile_id: "",
|
||||
max_candidates: Number(values.maxCandidates || 8),
|
||||
extra_requirements: values.extraRequirements || ""
|
||||
}
|
||||
});
|
||||
const searchId = created.id || created.search_id;
|
||||
const detail = searchId
|
||||
? await storyforgeFetch(`/v2/douyin/similar-searches/${encodeURIComponent(searchId)}`)
|
||||
: created;
|
||||
appState.lastSimilaritySearch = detail;
|
||||
rememberAction("相似账号已生成", `已生成 ${formatNumber(safeArray(detail.candidates).length)} 个候选账号。`, "green", detail);
|
||||
await loadDouyinAccount(account.id);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openBenchmarkLinkAction() {
|
||||
const account = requireSelectedAccountRow();
|
||||
const options = safeArray(appState.accounts)
|
||||
.filter((item) => item.id !== account.id)
|
||||
.map((item) => ({ value: item.id, label: item.nickname || item.douyin_id || item.id }));
|
||||
openActionModal({
|
||||
title: "保存对标关系",
|
||||
description: "把当前账号和另一个账号关联成对标关系,便于后续持续跟踪。",
|
||||
submitLabel: "保存关系",
|
||||
fields: [
|
||||
{ name: "targetAccountId", label: "目标账号", type: "select", value: options[0]?.value || "", options },
|
||||
{ name: "relationType", label: "关系类型", type: "select", value: "benchmark", options: [
|
||||
{ value: "benchmark", label: "对标" },
|
||||
{ value: "learn", label: "学习" },
|
||||
{ value: "watch", label: "跟踪" }
|
||||
] },
|
||||
{ name: "note", label: "备注", placeholder: "例如:开场结构很强,适合持续跟踪" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!values.targetAccountId) throw new Error("请先选择一个目标账号");
|
||||
await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
target_account_ids: [values.targetAccountId],
|
||||
target_profile_urls: [],
|
||||
relation_type: values.relationType || "benchmark",
|
||||
note: values.note || "",
|
||||
search_id: appState.lastSimilaritySearch?.id || ""
|
||||
}
|
||||
});
|
||||
rememberAction("对标关系已保存", "当前账号的对标关系已更新。", "green");
|
||||
await loadDouyinAccount(account.id);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function openJobDetailAction(jobId) {
|
||||
if (!jobId) return;
|
||||
setBusy(true, "正在加载任务详情...");
|
||||
loadJobDetail(jobId)
|
||||
.then(({ job, events }) => {
|
||||
const artifacts = JSON.stringify(job.artifacts || {}, null, 2);
|
||||
const result = JSON.stringify(job.result || {}, null, 2);
|
||||
openActionModal({
|
||||
title: job.title || "任务详情",
|
||||
description: `状态:${job.status || "-"} · 类型:${job.line_type || job.source_type || "-"}`,
|
||||
hideSubmit: true,
|
||||
fields: [
|
||||
{
|
||||
type: "html",
|
||||
label: "任务摘要",
|
||||
html: `
|
||||
<div class="detail-grid">
|
||||
<div class="mini-card"><small>任务 ID</small><strong>${escapeHtml(job.id)}</strong></div>
|
||||
<div class="mini-card"><small>状态</small><strong>${escapeHtml(job.status || "-")}</strong></div>
|
||||
<div class="mini-card"><small>链路</small><strong>${escapeHtml(job.line_type || "-")}</strong></div>
|
||||
<div class="mini-card"><small>创建时间</small><strong>${escapeHtml(formatDateTime(job.created_at))}</strong></div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
type: "html",
|
||||
label: "事件时间线",
|
||||
html: `
|
||||
<div class="list">
|
||||
${safeArray(events).slice(-6).map((event) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(event.event_type || "event")}</h4>
|
||||
<p>${escapeHtml(brief(event.message || JSON.stringify(event.payload || {}), 120))}</p>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>暂无事件</h4><p>当前任务还没有可显示的事件。</p></div>`}
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{ type: "textarea", name: "artifactsReadonly", label: "Artifacts", value: artifacts, rows: 8 },
|
||||
{ type: "textarea", name: "resultReadonly", label: "Result", value: result, rows: 8 }
|
||||
]
|
||||
});
|
||||
document.querySelector('[data-action-field="artifactsReadonly"]')?.setAttribute("readonly", "readonly");
|
||||
document.querySelector('[data-action-field="resultReadonly"]')?.setAttribute("readonly", "readonly");
|
||||
})
|
||||
.catch((error) => {
|
||||
alert("加载任务详情失败: " + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
setBusy(false, "");
|
||||
});
|
||||
}
|
||||
|
||||
function openGenerateCopyAction() {
|
||||
const assistant = requireSelectedAssistant();
|
||||
openActionModal({
|
||||
@@ -1929,6 +2093,18 @@ document.addEventListener("click", async (event) => {
|
||||
openCreateRealCutAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-similar-search") {
|
||||
openSimilaritySearchAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-benchmark-link") {
|
||||
openBenchmarkLinkAction();
|
||||
return;
|
||||
}
|
||||
if (name === "open-job-detail") {
|
||||
openJobDetailAction(action.dataset.jobId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "create-project") {
|
||||
await createProject();
|
||||
return;
|
||||
|
||||
@@ -649,6 +649,27 @@ select {
|
||||
color: #b24c4c;
|
||||
}
|
||||
|
||||
.clickable-tag {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sheet-html {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.task-item.compact {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.two-col {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
|
||||
Reference in New Issue
Block a user