feat: add job details and benchmark actions to web v4
This commit is contained in:
@@ -45,6 +45,8 @@
|
|||||||
- 创建 Agent
|
- 创建 Agent
|
||||||
- 对当前 Douyin 对标账号重跑分析
|
- 对当前 Douyin 对标账号重跑分析
|
||||||
- 批量分析高分作品
|
- 批量分析高分作品
|
||||||
|
- 查找相似对标账号
|
||||||
|
- 查看任务详情、事件和 artifacts/result
|
||||||
- 使用 Agent 生成文案
|
- 使用 Agent 生成文案
|
||||||
- 创建 AI 视频任务
|
- 创建 AI 视频任务
|
||||||
- 创建实拍剪辑任务
|
- 创建实拍剪辑任务
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ const appState = {
|
|||||||
busy: false,
|
busy: false,
|
||||||
message: "",
|
message: "",
|
||||||
lastAction: null,
|
lastAction: null,
|
||||||
lastGeneratedCopy: null
|
lastGeneratedCopy: null,
|
||||||
|
lastSimilaritySearch: null,
|
||||||
|
lastJobDetail: null
|
||||||
};
|
};
|
||||||
|
|
||||||
function safeArray(value) {
|
function safeArray(value) {
|
||||||
@@ -274,6 +276,14 @@ function ensureActionUi() {
|
|||||||
function renderActionFields(fields) {
|
function renderActionFields(fields) {
|
||||||
return fields.map((field) => {
|
return fields.map((field) => {
|
||||||
const common = `data-action-field="${escapeHtml(field.name)}"`;
|
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") {
|
if (field.type === "textarea") {
|
||||||
return `
|
return `
|
||||||
<div class="field-stack">
|
<div class="field-stack">
|
||||||
@@ -342,6 +352,7 @@ function openActionModal(config) {
|
|||||||
message.textContent = "";
|
message.textContent = "";
|
||||||
submit.textContent = config.submitLabel || "执行";
|
submit.textContent = config.submitLabel || "执行";
|
||||||
submit.disabled = false;
|
submit.disabled = false;
|
||||||
|
submit.hidden = Boolean(config.hideSubmit);
|
||||||
modal.classList.remove("hidden");
|
modal.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -468,6 +479,8 @@ async function logoutSession() {
|
|||||||
appState.documents = [];
|
appState.documents = [];
|
||||||
appState.lastAction = null;
|
appState.lastAction = null;
|
||||||
appState.lastGeneratedCopy = null;
|
appState.lastGeneratedCopy = null;
|
||||||
|
appState.lastSimilaritySearch = null;
|
||||||
|
appState.lastJobDetail = null;
|
||||||
renderAll();
|
renderAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -656,6 +669,15 @@ function getVideoLink(video) {
|
|||||||
return video.share_url || video.play_url || "";
|
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) {
|
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
|
||||||
return `
|
return `
|
||||||
<div class="screen-head">
|
<div class="screen-head">
|
||||||
@@ -888,10 +910,11 @@ function renderDiscoveryScreen() {
|
|||||||
const videos = safeArray(appState.selectedVideos?.items);
|
const videos = safeArray(appState.selectedVideos?.items);
|
||||||
const topVideos = getHighScoreVideos(3);
|
const topVideos = getHighScoreVideos(3);
|
||||||
const latestVideos = getLatestVideos(2);
|
const latestVideos = getLatestVideos(2);
|
||||||
|
const similarCandidates = safeArray(appState.lastSimilaritySearch?.candidates).slice(0, 5);
|
||||||
return screenShell(
|
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="panel">
|
||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
@@ -1026,6 +1049,21 @@ function renderDiscoveryScreen() {
|
|||||||
`).join("") || `<div class="task-item"><h4>暂无已保存对标</h4><p>当前账号还没有保存过对标关系。</p></div>`}
|
`).join("") || `<div class="task-item"><h4>暂无已保存对标</h4><p>当前账号还没有保存过对标关系。</p></div>`}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1251,6 +1289,7 @@ function renderProductionScreen() {
|
|||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag ${statusTone(job.status)}">${escapeHtml(job.status)}</span>
|
<span class="tag ${statusTone(job.status)}">${escapeHtml(job.status)}</span>
|
||||||
<span class="tag">${escapeHtml(job.line_type || "analysis")}</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>
|
||||||
</div>
|
</div>
|
||||||
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
|
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
|
||||||
@@ -1304,7 +1343,7 @@ function renderReviewScreen() {
|
|||||||
<div class="review-card">
|
<div class="review-card">
|
||||||
<h4>${escapeHtml(job.title)}</h4>
|
<h4>${escapeHtml(job.title)}</h4>
|
||||||
<p>${escapeHtml(brief(job.style_summary || job.transcript_text || "已完成,待补复盘。", 84))}</p>
|
<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>
|
</div>
|
||||||
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
|
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
|
||||||
</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() {
|
function openGenerateCopyAction() {
|
||||||
const assistant = requireSelectedAssistant();
|
const assistant = requireSelectedAssistant();
|
||||||
openActionModal({
|
openActionModal({
|
||||||
@@ -1929,6 +2093,18 @@ document.addEventListener("click", async (event) => {
|
|||||||
openCreateRealCutAction();
|
openCreateRealCutAction();
|
||||||
return;
|
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") {
|
if (name === "create-project") {
|
||||||
await createProject();
|
await createProject();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -649,6 +649,27 @@ select {
|
|||||||
color: #b24c4c;
|
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 {
|
.two-col {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
|
|||||||
Reference in New Issue
Block a user