feat: extend benchmark and job action flows
This commit is contained in:
@@ -46,7 +46,9 @@
|
||||
- 对当前 Douyin 对标账号重跑分析
|
||||
- 批量分析高分作品
|
||||
- 查找相似对标账号
|
||||
- 查看任务详情、事件和 artifacts/result
|
||||
- 从相似候选一键保存对标关系
|
||||
- 查看任务详情、事件、子任务和 artifacts/result
|
||||
- 从任务详情直接衔接 AI 视频 / 实拍剪辑 / 文案生成
|
||||
- 使用 Agent 生成文案
|
||||
- 创建 AI 视频任务
|
||||
- 创建实拍剪辑任务
|
||||
@@ -71,6 +73,7 @@ python3 -m http.server 3918
|
||||
## 后续建议
|
||||
|
||||
- 继续补动作型接口,例如导入、绑定 Agent、触发分析与生产
|
||||
- 把对标导入后的 Agent 绑定和知识库入库反馈做得更完整
|
||||
- 把全局搜索和页内搜索合并成统一搜索体验
|
||||
- 为 `生产中心 / 发布与复盘` 接入更完整的任务与成片对象
|
||||
- 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs`
|
||||
|
||||
@@ -670,14 +670,124 @@ function getVideoLink(video) {
|
||||
}
|
||||
|
||||
async function loadJobDetail(jobId) {
|
||||
const [job, events] = await Promise.all([
|
||||
const [job, events, childJobs] = await Promise.all([
|
||||
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}`),
|
||||
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}/events`).catch(() => [])
|
||||
storyforgeFetch(`/v2/explore/jobs/${encodeURIComponent(jobId)}/events`).catch(() => []),
|
||||
storyforgeFetch(`/v2/explore/jobs?parent_job_id=${encodeURIComponent(jobId)}`).catch(() => [])
|
||||
]);
|
||||
appState.lastJobDetail = { job, events: safeArray(events) };
|
||||
appState.lastJobDetail = { job, events: safeArray(events), childJobs: safeArray(childJobs) };
|
||||
return appState.lastJobDetail;
|
||||
}
|
||||
|
||||
function isJobCompleted(job) {
|
||||
return String(job?.status || "").toLowerCase() === "completed";
|
||||
}
|
||||
|
||||
function canDeriveAiVideo(job) {
|
||||
if (!job || !isJobCompleted(job)) return false;
|
||||
return String(job.line_type || "").toLowerCase() !== "ai_video";
|
||||
}
|
||||
|
||||
function canDeriveRealCut(job) {
|
||||
if (!job || !isJobCompleted(job)) return false;
|
||||
const sourceType = String(job.source_type || "").toLowerCase();
|
||||
return ["video_link", "upload_video"].includes(sourceType);
|
||||
}
|
||||
|
||||
function getJobSeedBrief(job) {
|
||||
return [
|
||||
job?.style_summary,
|
||||
job?.transcript_text,
|
||||
job?.result?.summary,
|
||||
job?.artifacts?.brief,
|
||||
job?.title
|
||||
].find((value) => String(value || "").trim()) || "";
|
||||
}
|
||||
|
||||
function collectHttpLinks(input, path = "result", bucket = []) {
|
||||
if (!input) return bucket;
|
||||
if (typeof input === "string") {
|
||||
const value = input.trim();
|
||||
if (/^https?:\/\//i.test(value)) {
|
||||
bucket.push({ label: path, url: value });
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
input.forEach((item, index) => collectHttpLinks(item, `${path}[${index}]`, bucket));
|
||||
return bucket;
|
||||
}
|
||||
if (typeof input === "object") {
|
||||
Object.entries(input).forEach(([key, value]) => collectHttpLinks(value, `${path}.${key}`, bucket));
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
function getJobPreviewLinks(job) {
|
||||
const deduped = [];
|
||||
const seen = new Set();
|
||||
collectHttpLinks(job?.result, "result", deduped);
|
||||
collectHttpLinks(job?.artifacts, "artifacts", deduped);
|
||||
return deduped.filter((item) => {
|
||||
if (!item.url || seen.has(item.url)) return false;
|
||||
seen.add(item.url);
|
||||
return true;
|
||||
}).slice(0, 8);
|
||||
}
|
||||
|
||||
function isCandidateLinked(candidate, links) {
|
||||
const accountId = String(candidate?.candidate_account_id || "");
|
||||
const profileUrl = String(candidate?.candidate_profile_url || "");
|
||||
return safeArray(links).some((link) => (
|
||||
(accountId && String(link.target_account_id || "") === accountId) ||
|
||||
(profileUrl && String(link.target_profile_url || "") === profileUrl)
|
||||
));
|
||||
}
|
||||
|
||||
function markSavedCandidate(candidate, links) {
|
||||
const nextCandidates = safeArray(appState.lastSimilaritySearch?.candidates).map((item) => {
|
||||
const sameAccount = String(item.candidate_account_id || "") && String(item.candidate_account_id || "") === String(candidate.candidate_account_id || "");
|
||||
const sameUrl = String(item.candidate_profile_url || "") && String(item.candidate_profile_url || "") === String(candidate.candidate_profile_url || "");
|
||||
if (!sameAccount && !sameUrl) return item;
|
||||
return { ...item, saved: true };
|
||||
});
|
||||
if (appState.lastSimilaritySearch) {
|
||||
appState.lastSimilaritySearch = {
|
||||
...appState.lastSimilaritySearch,
|
||||
candidates: nextCandidates
|
||||
};
|
||||
}
|
||||
if (appState.selectedWorkspace) {
|
||||
appState.selectedWorkspace = {
|
||||
...appState.selectedWorkspace,
|
||||
linked_accounts: safeArray(links)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCandidateAsBenchmark(candidateIndex, relationType = "benchmark") {
|
||||
const account = requireSelectedAccountRow();
|
||||
const candidate = safeArray(appState.lastSimilaritySearch?.candidates)[Number(candidateIndex)];
|
||||
if (!candidate) throw new Error("当前候选不存在,请先重新查相似");
|
||||
const payload = {
|
||||
target_account_ids: candidate.candidate_account_id ? [candidate.candidate_account_id] : [],
|
||||
target_profile_urls: candidate.candidate_account_id ? [] : [candidate.candidate_profile_url].filter(Boolean),
|
||||
relation_type: relationType,
|
||||
note: brief(candidate.rationale_text || "由相似搜索自动加入对标库", 120),
|
||||
search_id: appState.lastSimilaritySearch?.id || ""
|
||||
};
|
||||
if (!payload.target_account_ids.length && !payload.target_profile_urls.length) {
|
||||
throw new Error("当前候选没有可保存的账号或主页链接");
|
||||
}
|
||||
const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, {
|
||||
method: "POST",
|
||||
body: payload
|
||||
});
|
||||
markSavedCandidate(candidate, result.links);
|
||||
rememberAction("候选已存对标", `已把「${candidate.candidate_nickname || candidate.candidate_profile_url || "候选账号"}」加入对标关系。`, "green", result);
|
||||
renderAll();
|
||||
}
|
||||
|
||||
function screenShell(title, subtitle, actionsHtml, bodyHtml) {
|
||||
return `
|
||||
<div class="screen-head">
|
||||
@@ -1044,7 +1154,11 @@ function renderDiscoveryScreen() {
|
||||
<div class="task-item">
|
||||
<h4>${escapeHtml(link.target_nickname || link.target_profile_url || "未命名对标")}</h4>
|
||||
<p>${escapeHtml(link.note || link.target_profile_url || "已保存对标关系")}</p>
|
||||
<div class="task-meta"><span class="tag">${escapeHtml(link.relation_type || "benchmark")}</span></div>
|
||||
<div class="task-meta">
|
||||
<span class="tag">${escapeHtml(link.relation_type || "benchmark")}</span>
|
||||
${link.target_account_id ? `<span class="tag clickable-tag" data-action="select-account" data-account-id="${escapeHtml(link.target_account_id)}">看详情</span>` : ""}
|
||||
${link.target_profile_url ? `<a class="tag" href="${escapeHtml(link.target_profile_url)}" target="_blank" rel="noreferrer">打开主页</a>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item"><h4>暂无已保存对标</h4><p>当前账号还没有保存过对标关系。</p></div>`}
|
||||
</div>
|
||||
@@ -1052,12 +1166,14 @@ function renderDiscoveryScreen() {
|
||||
<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) => `
|
||||
${similarCandidates.map((candidate, index) => `
|
||||
<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_account_id ? `<span class="tag clickable-tag" data-action="select-account" data-account-id="${escapeHtml(candidate.candidate_account_id)}">看详情</span>` : ""}
|
||||
${isCandidateLinked(candidate, linkedAccounts) || candidate.saved ? `<span class="tag green">已保存</span>` : `<span class="tag clickable-tag" data-action="save-candidate-benchmark" data-candidate-index="${escapeHtml(index)}">存对标</span>`}
|
||||
${candidate.candidate_profile_url ? `<a class="tag" href="${escapeHtml(candidate.candidate_profile_url)}" target="_blank" rel="noreferrer">打开主页</a>` : ""}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1289,6 +1405,8 @@ 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>
|
||||
${canDeriveAiVideo(job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(job.id)}">做 AI 视频</span>` : ""}
|
||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">做实拍剪辑</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1343,7 +1461,13 @@ 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><span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span></div>
|
||||
<div class="task-meta">
|
||||
<span class="tag green">已完成</span>
|
||||
<span class="tag">${escapeHtml(job.line_type || "analysis")}</span>
|
||||
${canDeriveAiVideo(job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(job.id)}">做 AI 视频</span>` : ""}
|
||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">做实拍剪辑</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>
|
||||
@@ -1791,38 +1915,49 @@ function openSimilaritySearchAction() {
|
||||
});
|
||||
}
|
||||
|
||||
function openBenchmarkLinkAction() {
|
||||
function openBenchmarkLinkAction(defaults = {}) {
|
||||
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 }));
|
||||
const candidate = typeof defaults.candidateIndex === "number"
|
||||
? safeArray(appState.lastSimilaritySearch?.candidates)[defaults.candidateIndex] || null
|
||||
: null;
|
||||
openActionModal({
|
||||
title: "保存对标关系",
|
||||
description: "把当前账号和另一个账号关联成对标关系,便于后续持续跟踪。",
|
||||
submitLabel: "保存关系",
|
||||
fields: [
|
||||
{ name: "targetAccountId", label: "目标账号", type: "select", value: options[0]?.value || "", options },
|
||||
{ name: "targetAccountId", label: "目标账号", type: "select", value: defaults.targetAccountId || candidate?.candidate_account_id || options[0]?.value || "", options: [{ value: "", label: "仅保存主页链接" }, ...options] },
|
||||
{ name: "targetProfileUrl", label: "目标主页链接", type: "url", value: defaults.targetProfileUrl || candidate?.candidate_profile_url || "", placeholder: "没有本地账号时可直接保存主页链接" },
|
||||
{ name: "relationType", label: "关系类型", type: "select", value: "benchmark", options: [
|
||||
{ value: "benchmark", label: "对标" },
|
||||
{ value: "learn", label: "学习" },
|
||||
{ value: "watch", label: "跟踪" }
|
||||
] },
|
||||
{ name: "note", label: "备注", placeholder: "例如:开场结构很强,适合持续跟踪" }
|
||||
{ name: "note", label: "备注", value: defaults.note || brief(candidate?.rationale_text || "", 120), placeholder: "例如:开场结构很强,适合持续跟踪" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!values.targetAccountId) throw new Error("请先选择一个目标账号");
|
||||
await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, {
|
||||
if (!values.targetAccountId && !values.targetProfileUrl?.trim()) throw new Error("请先选择一个目标账号或填写主页链接");
|
||||
const result = await storyforgeFetch(`/v2/douyin/accounts/${encodeURIComponent(account.id)}/benchmark-links`, {
|
||||
method: "POST",
|
||||
body: {
|
||||
target_account_ids: [values.targetAccountId],
|
||||
target_profile_urls: [],
|
||||
target_account_ids: values.targetAccountId ? [values.targetAccountId] : [],
|
||||
target_profile_urls: values.targetAccountId ? [] : [values.targetProfileUrl.trim()],
|
||||
relation_type: values.relationType || "benchmark",
|
||||
note: values.note || "",
|
||||
search_id: appState.lastSimilaritySearch?.id || ""
|
||||
}
|
||||
});
|
||||
if (candidate) {
|
||||
markSavedCandidate(candidate, result.links);
|
||||
} else if (appState.selectedWorkspace) {
|
||||
appState.selectedWorkspace = {
|
||||
...appState.selectedWorkspace,
|
||||
linked_accounts: safeArray(result.links)
|
||||
};
|
||||
}
|
||||
rememberAction("对标关系已保存", "当前账号的对标关系已更新。", "green");
|
||||
await loadDouyinAccount(account.id);
|
||||
renderAll();
|
||||
}
|
||||
});
|
||||
@@ -1832,9 +1967,10 @@ function openJobDetailAction(jobId) {
|
||||
if (!jobId) return;
|
||||
setBusy(true, "正在加载任务详情...");
|
||||
loadJobDetail(jobId)
|
||||
.then(({ job, events }) => {
|
||||
.then(({ job, events, childJobs }) => {
|
||||
const artifacts = JSON.stringify(job.artifacts || {}, null, 2);
|
||||
const result = JSON.stringify(job.result || {}, null, 2);
|
||||
const previewLinks = getJobPreviewLinks(job);
|
||||
openActionModal({
|
||||
title: job.title || "任务详情",
|
||||
description: `状态:${job.status || "-"} · 类型:${job.line_type || job.source_type || "-"}`,
|
||||
@@ -1866,6 +2002,53 @@ function openJobDetailAction(jobId) {
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
type: "html",
|
||||
label: "结果预览",
|
||||
html: `
|
||||
<div class="list">
|
||||
${previewLinks.map((item) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(item.label.replace(/^result\./, "").replace(/^artifacts\./, ""))}</h4>
|
||||
<p>${escapeHtml(item.url)}</p>
|
||||
<div class="task-meta">
|
||||
<a class="tag" href="${escapeHtml(item.url)}" target="_blank" rel="noreferrer">打开结果</a>
|
||||
</div>
|
||||
</div>
|
||||
`).join("") || `<div class="task-item compact"><h4>暂无外部结果链接</h4><p>当前先保留 artifacts 和 result 原始数据供查看。</p></div>`}
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
type: "html",
|
||||
label: "下一步动作",
|
||||
html: `
|
||||
<div class="task-meta">
|
||||
${canDeriveAiVideo(job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(job.id)}">继续做 AI 视频</span>` : ""}
|
||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">继续做实拍剪辑</span>` : ""}
|
||||
<span class="tag clickable-tag" data-action="job-to-generate-copy" data-job-id="${escapeHtml(job.id)}">用摘要写文案</span>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
type: "html",
|
||||
label: "子任务",
|
||||
html: `
|
||||
<div class="list">
|
||||
${safeArray(childJobs).slice(0, 6).map((item) => `
|
||||
<div class="task-item compact">
|
||||
<h4>${escapeHtml(item.title || item.id)}</h4>
|
||||
<p>${escapeHtml(brief(item.style_summary || item.transcript_text || item.error || "暂无摘要", 96))}</p>
|
||||
<div class="task-meta">
|
||||
<span class="tag ${statusTone(item.status)}">${escapeHtml(item.status || "-")}</span>
|
||||
<span class="tag">${escapeHtml(item.line_type || "-")}</span>
|
||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(item.id)}">看详情</span>
|
||||
</div>
|
||||
</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 }
|
||||
]
|
||||
@@ -1881,14 +2064,15 @@ function openJobDetailAction(jobId) {
|
||||
});
|
||||
}
|
||||
|
||||
function openGenerateCopyAction() {
|
||||
const assistant = requireSelectedAssistant();
|
||||
function openGenerateCopyAction(defaults = {}) {
|
||||
const assistant = getSelectedAssistant() || requireSelectedAssistant();
|
||||
const sourceJob = defaults.sourceJob || null;
|
||||
openActionModal({
|
||||
title: "生成文案",
|
||||
description: "用当前 Agent 和知识库生成一版短视频文案。",
|
||||
submitLabel: "开始生成",
|
||||
fields: [
|
||||
{ name: "brief", label: "创作需求", type: "textarea", rows: 5, placeholder: "例如:给创业者写一条 60 字内的短视频开场文案" },
|
||||
{ name: "brief", label: "创作需求", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "例如:给创业者写一条 60 字内的短视频开场文案" },
|
||||
{ name: "platform", label: "平台", type: "select", value: "抖音", options: [
|
||||
{ value: "抖音", label: "抖音" },
|
||||
{ value: "小红书", label: "小红书" },
|
||||
@@ -1923,21 +2107,22 @@ function openGenerateCopyAction() {
|
||||
});
|
||||
}
|
||||
|
||||
function openCreateAiVideoAction() {
|
||||
function openCreateAiVideoAction(defaults = {}) {
|
||||
const project = requireSelectedProject();
|
||||
const assistant = getSelectedAssistant();
|
||||
const kb = getProjectKnowledgeBases(project.id)[0];
|
||||
const sourceJob = defaults.sourceJob || null;
|
||||
openActionModal({
|
||||
title: "创建 AI 视频任务",
|
||||
description: "输入 brief 后,直接触发 AI 视频链。",
|
||||
submitLabel: "开始生产",
|
||||
fields: [
|
||||
{ name: "title", label: "任务标题", placeholder: "例如:创业口播 AI 视频测试" },
|
||||
{ name: "brief", label: "视频 brief", type: "textarea", rows: 5, placeholder: "写明主题、风格、镜头和目标受众" },
|
||||
{ name: "sourceJobId", label: "关联源任务", type: "select", value: "", options: [{ value: "", label: "不关联" }, ...getCompletedJobOptions()] },
|
||||
{ name: "style", label: "风格", value: "realistic" },
|
||||
{ name: "shots", label: "镜头数", type: "number", value: 4, min: 1, max: 12 },
|
||||
{ name: "duration", label: "单镜头秒数", type: "number", value: 5, min: 3, max: 12 }
|
||||
{ name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · AI 视频` : ""), placeholder: "例如:创业口播 AI 视频测试" },
|
||||
{ name: "brief", label: "视频 brief", type: "textarea", rows: 5, value: defaults.brief || getJobSeedBrief(sourceJob), placeholder: "写明主题、风格、镜头和目标受众" },
|
||||
{ name: "sourceJobId", label: "关联源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || "", options: [{ value: "", label: "不关联" }, ...getCompletedJobOptions()] },
|
||||
{ name: "style", label: "风格", value: defaults.style || "realistic" },
|
||||
{ name: "shots", label: "镜头数", type: "number", value: defaults.shots || 4, min: 1, max: 12 },
|
||||
{ name: "duration", label: "单镜头秒数", type: "number", value: defaults.duration || 5, min: 3, max: 12 }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写任务标题");
|
||||
@@ -1962,18 +2147,19 @@ function openCreateAiVideoAction() {
|
||||
});
|
||||
}
|
||||
|
||||
function openCreateRealCutAction() {
|
||||
function openCreateRealCutAction(defaults = {}) {
|
||||
const project = requireSelectedProject();
|
||||
const sourceJob = defaults.sourceJob || null;
|
||||
openActionModal({
|
||||
title: "创建实拍剪辑任务",
|
||||
description: "基于已完成的源任务,把素材发到 cutvideo。",
|
||||
submitLabel: "开始剪辑",
|
||||
fields: [
|
||||
{ name: "title", label: "任务标题", placeholder: "例如:创业素材粗剪" },
|
||||
{ name: "sourceJobId", label: "源任务", type: "select", value: getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() },
|
||||
{ name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: 60, min: 10, max: 300 },
|
||||
{ name: "aspectRatio", label: "画幅", value: "9:16" },
|
||||
{ name: "objective", label: "目标", type: "textarea", rows: 4, placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" }
|
||||
{ name: "title", label: "任务标题", value: defaults.title || (sourceJob ? `${sourceJob.title} · 实拍剪辑` : ""), placeholder: "例如:创业素材粗剪" },
|
||||
{ name: "sourceJobId", label: "源任务", type: "select", value: defaults.sourceJobId || sourceJob?.id || getCompletedJobOptions()[0]?.value || "", options: getCompletedJobOptions() },
|
||||
{ name: "targetDurationSec", label: "目标时长(秒)", type: "number", value: defaults.targetDurationSec || 60, min: 10, max: 300 },
|
||||
{ name: "aspectRatio", label: "画幅", value: defaults.aspectRatio || "9:16" },
|
||||
{ name: "objective", label: "目标", type: "textarea", rows: 4, value: defaults.objective || "", placeholder: "例如:保留高信息密度片段,输出适合短视频平台的粗剪结果" }
|
||||
],
|
||||
onSubmit: async (values) => {
|
||||
if (!values.title?.trim()) throw new Error("请填写任务标题");
|
||||
@@ -2101,10 +2287,42 @@ document.addEventListener("click", async (event) => {
|
||||
openBenchmarkLinkAction();
|
||||
return;
|
||||
}
|
||||
if (name === "save-candidate-benchmark") {
|
||||
setBusy(true, "正在保存对标关系...");
|
||||
try {
|
||||
await saveCandidateAsBenchmark(action.dataset.candidateIndex || "");
|
||||
} catch (error) {
|
||||
alert("保存候选失败: " + error.message);
|
||||
} finally {
|
||||
setBusy(false, "");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (name === "open-job-detail") {
|
||||
openJobDetailAction(action.dataset.jobId || "");
|
||||
return;
|
||||
}
|
||||
if (name === "job-to-ai-video") {
|
||||
const jobId = action.dataset.jobId || "";
|
||||
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
|
||||
closeActionModal();
|
||||
openCreateAiVideoAction({ sourceJobId: jobId, sourceJob: detail });
|
||||
return;
|
||||
}
|
||||
if (name === "job-to-real-cut") {
|
||||
const jobId = action.dataset.jobId || "";
|
||||
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
|
||||
closeActionModal();
|
||||
openCreateRealCutAction({ sourceJobId: jobId, sourceJob: detail, objective: detail ? `基于任务「${detail.title}」保留高信息密度片段,输出适合短视频平台的粗剪结果。` : "" });
|
||||
return;
|
||||
}
|
||||
if (name === "job-to-generate-copy") {
|
||||
const jobId = action.dataset.jobId || "";
|
||||
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
|
||||
closeActionModal();
|
||||
openGenerateCopyAction({ sourceJob: detail, brief: detail ? `基于任务「${detail.title}」的结果,生成一版可发布的短视频文案。参考摘要:${getJobSeedBrief(detail)}` : "" });
|
||||
return;
|
||||
}
|
||||
if (name === "create-project") {
|
||||
await createProject();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user