feat: extend web tracking and integration controls
This commit is contained in:
@@ -1885,6 +1885,33 @@ def register_douyin_routes(app: Any, legacy: Any) -> None:
|
|||||||
"items": items[: max(1, min(limit, 100))]
|
"items": items[: max(1, min(limit, 100))]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async def _refresh_tracked_account_workspace(
|
||||||
|
owner: dict[str, Any],
|
||||||
|
tracked_account_id: str,
|
||||||
|
discovery_note: str = "tracking_refresh"
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
account_row = _require_owned_account(tracked_account_id, owner["id"])
|
||||||
|
profile_url = _first_non_empty(
|
||||||
|
account_row.get("canonical_profile_url"),
|
||||||
|
account_row.get("profile_url")
|
||||||
|
)
|
||||||
|
if not profile_url:
|
||||||
|
raise HTTPException(status_code=400, detail="Tracked account has no profile_url to refresh")
|
||||||
|
request = DouyinAccountSyncRequest(
|
||||||
|
profile_url=profile_url,
|
||||||
|
compact_response=True,
|
||||||
|
discovery_note=discovery_note
|
||||||
|
)
|
||||||
|
public_data = await _collect_public_profile(profile_url, None)
|
||||||
|
creator_data = {"pages": [], "errors": []}
|
||||||
|
return await run_in_threadpool(
|
||||||
|
_finalize_sync_workspace,
|
||||||
|
owner,
|
||||||
|
request,
|
||||||
|
public_data,
|
||||||
|
creator_data
|
||||||
|
)
|
||||||
|
|
||||||
def _normalize_report_text(value: Any) -> str:
|
def _normalize_report_text(value: Any) -> str:
|
||||||
text = str(value or "").strip()
|
text = str(value or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -3374,6 +3401,71 @@ def register_douyin_routes(app: Any, legacy: Any) -> None:
|
|||||||
"items": _list_tracked_accounts(account["id"])
|
"items": _list_tracked_accounts(account["id"])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@app.post("/v2/douyin/tracking/accounts/{tracked_account_id}/refresh")
|
||||||
|
async def refresh_douyin_tracked_account(
|
||||||
|
tracked_account_id: str,
|
||||||
|
account: dict[str, Any] = Depends(legacy.require_approved)
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
account_row = _require_owned_account(tracked_account_id, account["id"])
|
||||||
|
account_payload = _build_account_payload(account_row, include_recent_videos=6)
|
||||||
|
try:
|
||||||
|
refreshed = await _refresh_tracked_account_workspace(account, tracked_account_id)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"tracked_account_id": tracked_account_id,
|
||||||
|
"account": refreshed.get("account", {}),
|
||||||
|
"sync_errors": refreshed.get("sync_errors", []),
|
||||||
|
"public_video_count": refreshed.get("public_video_count", 0),
|
||||||
|
"creator_page_count": refreshed.get("creator_page_count", 0)
|
||||||
|
}
|
||||||
|
except HTTPException as exc:
|
||||||
|
detail = exc.detail if isinstance(exc.detail, dict) else {"message": str(exc.detail)}
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"tracked_account_id": tracked_account_id,
|
||||||
|
"account": account_payload,
|
||||||
|
"message": detail.get("message") or str(exc.detail),
|
||||||
|
"detail": detail,
|
||||||
|
"sync_errors": detail.get("public_errors", []) + detail.get("creator_errors", [])
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/v2/douyin/tracking/refresh")
|
||||||
|
async def refresh_all_douyin_tracked_accounts(
|
||||||
|
account: dict[str, Any] = Depends(legacy.require_approved)
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
tracked_accounts = _list_tracked_accounts(account["id"])
|
||||||
|
items: list[dict[str, Any]] = []
|
||||||
|
errors: list[dict[str, Any]] = []
|
||||||
|
for tracked in tracked_accounts:
|
||||||
|
try:
|
||||||
|
refreshed = await _refresh_tracked_account_workspace(account, tracked["tracked_account_id"])
|
||||||
|
items.append({
|
||||||
|
"tracking_id": tracked["id"],
|
||||||
|
"tracked_account_id": tracked["tracked_account_id"],
|
||||||
|
"nickname": (refreshed.get("account") or {}).get("nickname", ""),
|
||||||
|
"sync_errors": refreshed.get("sync_errors", []),
|
||||||
|
"public_video_count": refreshed.get("public_video_count", 0)
|
||||||
|
})
|
||||||
|
except HTTPException as exc:
|
||||||
|
errors.append({
|
||||||
|
"tracking_id": tracked["id"],
|
||||||
|
"tracked_account_id": tracked["tracked_account_id"],
|
||||||
|
"message": str(exc.detail)
|
||||||
|
})
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append({
|
||||||
|
"tracking_id": tracked["id"],
|
||||||
|
"tracked_account_id": tracked["tracked_account_id"],
|
||||||
|
"message": str(exc)
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"tracked_count": len(tracked_accounts),
|
||||||
|
"refreshed": len(items),
|
||||||
|
"failed": len(errors),
|
||||||
|
"items": items,
|
||||||
|
"errors": errors
|
||||||
|
}
|
||||||
|
|
||||||
@app.post("/v2/douyin/tracking/cursor")
|
@app.post("/v2/douyin/tracking/cursor")
|
||||||
def update_douyin_tracking_cursor(
|
def update_douyin_tracking_cursor(
|
||||||
request: DouyinTrackingCursorRequest,
|
request: DouyinTrackingCursorRequest,
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
- 单账号作品列表 `/v2/douyin/accounts/{id}/videos`
|
- 单账号作品列表 `/v2/douyin/accounts/{id}/videos`
|
||||||
- 跟踪账号 `/v2/douyin/tracking/accounts`
|
- 跟踪账号 `/v2/douyin/tracking/accounts`
|
||||||
- 跟踪日报 `/v2/douyin/tracking/digest`
|
- 跟踪日报 `/v2/douyin/tracking/digest`
|
||||||
|
- 发布复盘 `/v2/reviews`
|
||||||
|
- 集成健康 `/v2/integrations/health`
|
||||||
- 最近知识库文档 `/v2/knowledge-bases/{id}/documents`
|
- 最近知识库文档 `/v2/knowledge-bases/{id}/documents`
|
||||||
|
|
||||||
## 当前已接入的真实动作
|
## 当前已接入的真实动作
|
||||||
@@ -51,10 +53,16 @@
|
|||||||
- 查找相似对标账号
|
- 查找相似对标账号
|
||||||
- 从相似候选一键保存对标关系
|
- 从相似候选一键保存对标关系
|
||||||
- 把当前对标账号加入跟踪,并绑定 Agent
|
- 把当前对标账号加入跟踪,并绑定 Agent
|
||||||
|
- 单账号立即同步跟踪对象
|
||||||
|
- 批量同步全部跟踪对象
|
||||||
|
- 日报手动标记已读,不再在刷新页面时自动吞掉未读摘要
|
||||||
- 按上次打开后生成跟踪日报与借鉴点摘要
|
- 按上次打开后生成跟踪日报与借鉴点摘要
|
||||||
- 查看任务详情、事件、子任务和 artifacts/result
|
- 查看任务详情、事件、子任务和 artifacts/result
|
||||||
- 从任务详情直接衔接 AI 视频 / 实拍剪辑 / 文案生成
|
- 从任务详情直接衔接 AI 视频 / 实拍剪辑 / 文案生成
|
||||||
- 在生产中心 / 发布与复盘常驻最近一次任务详情摘要
|
- 在生产中心 / 发布与复盘常驻最近一次任务详情摘要
|
||||||
|
- 在 Web 中直接创建和编辑复盘
|
||||||
|
- 在页面里直接看到 `cutvideo / huobao / n8n / ASR` 的真实健康状态
|
||||||
|
- 依赖不可达时,自动拦住 AI 视频 / 实拍剪辑动作并展示原因
|
||||||
- 使用 Agent 生成文案
|
- 使用 Agent 生成文案
|
||||||
- 创建 AI 视频任务
|
- 创建 AI 视频任务
|
||||||
- 创建实拍剪辑任务
|
- 创建实拍剪辑任务
|
||||||
@@ -78,10 +86,10 @@ python3 -m http.server 3918
|
|||||||
|
|
||||||
## 后续建议
|
## 后续建议
|
||||||
|
|
||||||
- 继续补动作型接口,例如导入、绑定 Agent、触发分析与生产
|
- 继续补多平台真实接入,而不只是一套 Douyin 工作流
|
||||||
- 把对标导入后的 Agent 绑定和知识库入库反馈做得更完整
|
- 把对标导入后的 Agent 绑定和知识库入库反馈做得更完整
|
||||||
- 把跟踪日报从 Douyin 扩到多平台统一模型
|
- 把跟踪日报从 Douyin 扩到多平台统一模型,并接入真正的定时调度
|
||||||
- 把全局搜索和页内搜索合并成统一搜索体验
|
- 把全局搜索和页内搜索合并成统一搜索体验
|
||||||
- 为 `生产中心 / 发布与复盘` 接入更完整的任务与成片对象
|
- 为 `生产中心 / 发布与复盘` 接入更完整的成片预览与封面对象
|
||||||
- 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs`
|
- 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs`
|
||||||
- 抖音采集控制台仍作为独立工具存在,这里才是正式业务应用壳
|
- 抖音采集控制台仍作为独立工具存在,这里才是正式业务应用壳
|
||||||
|
|||||||
@@ -32,6 +32,44 @@ const appState = {
|
|||||||
lastJobDetail: null
|
lastJobDetail: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const INTEGRATION_ORDER = ["cutvideo", "huobao", "n8n", "asr"];
|
||||||
|
const INTEGRATION_META = {
|
||||||
|
cutvideo: {
|
||||||
|
label: "自动剪辑",
|
||||||
|
hint: "Windows cutvideo",
|
||||||
|
impacts: ["实拍剪辑"]
|
||||||
|
},
|
||||||
|
huobao: {
|
||||||
|
label: "AI 视频",
|
||||||
|
hint: "huobao-drama",
|
||||||
|
impacts: ["AI 视频"]
|
||||||
|
},
|
||||||
|
n8n: {
|
||||||
|
label: "编排",
|
||||||
|
hint: "n8n workflow",
|
||||||
|
impacts: ["AI 视频", "实拍剪辑", "自动链路"]
|
||||||
|
},
|
||||||
|
asr: {
|
||||||
|
label: "ASR",
|
||||||
|
hint: "素材转写",
|
||||||
|
impacts: ["分析转写"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const PIPELINE_GUARDS = {
|
||||||
|
aiVideo: {
|
||||||
|
label: "AI 视频",
|
||||||
|
openAction: "open-ai-video",
|
||||||
|
jobAction: "job-to-ai-video",
|
||||||
|
dependencies: ["n8n", "huobao"]
|
||||||
|
},
|
||||||
|
realCut: {
|
||||||
|
label: "实拍剪辑",
|
||||||
|
openAction: "open-real-cut",
|
||||||
|
jobAction: "job-to-real-cut",
|
||||||
|
dependencies: ["n8n", "cutvideo"]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function safeArray(value) {
|
function safeArray(value) {
|
||||||
return Array.isArray(value) ? value : [];
|
return Array.isArray(value) ? value : [];
|
||||||
}
|
}
|
||||||
@@ -592,12 +630,6 @@ async function bootstrap() {
|
|||||||
appState.selectedWorkspace = null;
|
appState.selectedWorkspace = null;
|
||||||
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 };
|
appState.selectedVideos = { items: [], meta: {}, top_scored_video_ids: [], latest_video_ids: [], high_score_threshold: 60 };
|
||||||
}
|
}
|
||||||
const nextSeenAt = new Date().toISOString();
|
|
||||||
storyforgeFetch("/v2/douyin/tracking/cursor", {
|
|
||||||
method: "POST",
|
|
||||||
body: { last_seen_at: nextSeenAt }
|
|
||||||
}).catch(() => null);
|
|
||||||
setLastSeenAt(nextSeenAt);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
appState.message = error.message;
|
appState.message = error.message;
|
||||||
if (String(error.message || "").includes("401") || String(error.message || "").includes("Not authenticated")) {
|
if (String(error.message || "").includes("401") || String(error.message || "").includes("Not authenticated")) {
|
||||||
@@ -609,6 +641,57 @@ async function bootstrap() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function markTrackingDigestRead() {
|
||||||
|
const nextSeenAt = new Date().toISOString();
|
||||||
|
await storyforgeFetch("/v2/douyin/tracking/cursor", {
|
||||||
|
method: "POST",
|
||||||
|
body: { last_seen_at: nextSeenAt }
|
||||||
|
});
|
||||||
|
setLastSeenAt(nextSeenAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTrackingAccountsAction() {
|
||||||
|
setBusy(true, "正在同步跟踪账号...");
|
||||||
|
try {
|
||||||
|
const payload = await storyforgeFetch("/v2/douyin/tracking/refresh", {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
rememberAction(
|
||||||
|
"跟踪已同步",
|
||||||
|
`已刷新 ${formatNumber(payload.refreshed || 0)} 个账号${payload.failed ? `,失败 ${formatNumber(payload.failed)} 个` : ""}。`,
|
||||||
|
payload.failed ? "orange" : "green",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
await bootstrap();
|
||||||
|
} finally {
|
||||||
|
setBusy(false, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshTrackedAccountAction(trackedAccountId) {
|
||||||
|
if (!trackedAccountId) {
|
||||||
|
throw new Error("trackedAccountId is required");
|
||||||
|
}
|
||||||
|
setBusy(true, "正在同步该跟踪账号...");
|
||||||
|
try {
|
||||||
|
const payload = await storyforgeFetch(`/v2/douyin/tracking/accounts/${encodeURIComponent(trackedAccountId)}/refresh`, {
|
||||||
|
method: "POST"
|
||||||
|
});
|
||||||
|
const success = payload.success !== false;
|
||||||
|
rememberAction(
|
||||||
|
success ? "单账号已同步" : "单账号刷新失败",
|
||||||
|
success
|
||||||
|
? `已刷新「${payload.account?.nickname || trackedAccountId}」的最新作品。`
|
||||||
|
: `暂时无法刷新「${payload.account?.nickname || trackedAccountId}」:${payload.message || "请稍后重试"}`,
|
||||||
|
success ? (safeArray(payload.sync_errors).length ? "orange" : "green") : "orange",
|
||||||
|
payload
|
||||||
|
);
|
||||||
|
await bootstrap();
|
||||||
|
} finally {
|
||||||
|
setBusy(false, "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getSelectedProject() {
|
function getSelectedProject() {
|
||||||
const projects = safeArray(appState.dashboard?.projects);
|
const projects = safeArray(appState.dashboard?.projects);
|
||||||
return projects.find((item) => item.id === appState.selectedProjectId) || projects[0] || null;
|
return projects.find((item) => item.id === appState.selectedProjectId) || projects[0] || null;
|
||||||
@@ -778,6 +861,126 @@ function canDeriveRealCut(job) {
|
|||||||
return ["video_link", "upload_video"].includes(sourceType);
|
return ["video_link", "upload_video"].includes(sourceType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasIntegrationHealthData() {
|
||||||
|
return Boolean(appState.integrationHealth && typeof appState.integrationHealth === "object");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntegrationDetail(key) {
|
||||||
|
const raw = hasIntegrationHealthData() ? appState.integrationHealth?.[key] : null;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
available: Boolean(raw && typeof raw === "object"),
|
||||||
|
configured: Boolean(raw?.configured),
|
||||||
|
reachable: Boolean(raw?.reachable),
|
||||||
|
statusCode: Number(raw?.status_code || 0),
|
||||||
|
error: String(raw?.error || ""),
|
||||||
|
url: String(raw?.url || raw?.base_url || ""),
|
||||||
|
baseUrl: String(raw?.base_url || "")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntegrationStatus(detail) {
|
||||||
|
if (!detail.available) {
|
||||||
|
return { tone: "blue", summary: "未拉取" };
|
||||||
|
}
|
||||||
|
if (detail.reachable) {
|
||||||
|
return { tone: "green", summary: "在线" };
|
||||||
|
}
|
||||||
|
if (detail.configured) {
|
||||||
|
return { tone: "red", summary: "不可达" };
|
||||||
|
}
|
||||||
|
return { tone: "orange", summary: "未配置" };
|
||||||
|
}
|
||||||
|
|
||||||
|
function describeIntegrationFailure(key) {
|
||||||
|
const detail = getIntegrationDetail(key);
|
||||||
|
const meta = INTEGRATION_META[key] || { label: key };
|
||||||
|
if (!detail.available) return `${meta.label}健康状态未拉取`;
|
||||||
|
if (!detail.configured) return `${meta.label}未配置`;
|
||||||
|
if (detail.statusCode) return `${meta.label}返回 HTTP ${detail.statusCode}`;
|
||||||
|
if (detail.error) return `${meta.label}${brief(detail.error, 42)}`;
|
||||||
|
return `${meta.label}不可达`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPipelineGuard(kind) {
|
||||||
|
const config = PIPELINE_GUARDS[kind];
|
||||||
|
if (!config) {
|
||||||
|
return { enabled: true, reason: "", blocked: [] };
|
||||||
|
}
|
||||||
|
const blocked = config.dependencies
|
||||||
|
.map((key) => ({ key, detail: getIntegrationDetail(key), meta: INTEGRATION_META[key] || { label: key } }))
|
||||||
|
.filter((item) => item.detail.available && !item.detail.reachable);
|
||||||
|
if (!blocked.length) {
|
||||||
|
return { enabled: true, reason: "", blocked: [] };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
blocked,
|
||||||
|
reason: `${config.label}暂不可用:${blocked.map((item) => describeIntegrationFailure(item.key)).join(";")}`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntegrationCards() {
|
||||||
|
return INTEGRATION_ORDER.map((key) => {
|
||||||
|
const detail = getIntegrationDetail(key);
|
||||||
|
const status = getIntegrationStatus(detail);
|
||||||
|
const meta = INTEGRATION_META[key] || { label: key, hint: key, impacts: [] };
|
||||||
|
let note = "尚未获取健康检查数据";
|
||||||
|
if (detail.available) {
|
||||||
|
if (detail.reachable) {
|
||||||
|
note = detail.statusCode
|
||||||
|
? `健康探测返回 HTTP ${detail.statusCode}`
|
||||||
|
: "TCP 探测已通过";
|
||||||
|
} else if (!detail.configured) {
|
||||||
|
note = "后端还没有配置该依赖地址";
|
||||||
|
} else if (detail.statusCode) {
|
||||||
|
note = `探测返回 HTTP ${detail.statusCode}`;
|
||||||
|
} else if (detail.error) {
|
||||||
|
note = brief(detail.error, 72);
|
||||||
|
} else {
|
||||||
|
note = "探测失败,请检查服务进程和网络";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
meta,
|
||||||
|
detail,
|
||||||
|
status,
|
||||||
|
note
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIntegrationOverview() {
|
||||||
|
const cards = getIntegrationCards();
|
||||||
|
const reachableCount = cards.filter((item) => item.detail.available && item.detail.reachable).length;
|
||||||
|
const availableCount = cards.filter((item) => item.detail.available).length;
|
||||||
|
const aiVideoGuard = getPipelineGuard("aiVideo");
|
||||||
|
const realCutGuard = getPipelineGuard("realCut");
|
||||||
|
const blockedActions = [
|
||||||
|
!aiVideoGuard.enabled ? aiVideoGuard.reason : "",
|
||||||
|
!realCutGuard.enabled ? realCutGuard.reason : ""
|
||||||
|
].filter(Boolean);
|
||||||
|
const tone = !availableCount
|
||||||
|
? "blue"
|
||||||
|
: blockedActions.length
|
||||||
|
? "red"
|
||||||
|
: cards.some((item) => item.detail.available && !item.detail.reachable)
|
||||||
|
? "orange"
|
||||||
|
: "green";
|
||||||
|
const headline = !availableCount
|
||||||
|
? "依赖健康尚未拉取"
|
||||||
|
: blockedActions.length
|
||||||
|
? `自动链路受阻:${blockedActions.length} 项`
|
||||||
|
: `${reachableCount}/${cards.length} 项依赖在线`;
|
||||||
|
const subtitle = !availableCount
|
||||||
|
? "刷新后会显示 cutvideo / huobao / n8n / ASR 的真实状态。"
|
||||||
|
: blockedActions.length
|
||||||
|
? blockedActions.join(";")
|
||||||
|
: "AI 视频与实拍剪辑链路当前可直接发起。";
|
||||||
|
return { cards, tone, headline, subtitle };
|
||||||
|
}
|
||||||
|
|
||||||
function getJobSeedBrief(job) {
|
function getJobSeedBrief(job) {
|
||||||
return [
|
return [
|
||||||
job?.style_summary,
|
job?.style_summary,
|
||||||
@@ -885,8 +1088,103 @@ function screenShell(title, subtitle, actionsHtml, bodyHtml) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function button(label, action, tone = "secondary") {
|
function button(label, action, tone = "secondary", options = {}) {
|
||||||
return `<button class="btn btn-${tone}" type="button" data-action="${escapeHtml(action)}">${escapeHtml(label)}</button>`;
|
const classes = ["btn", `btn-${tone}`];
|
||||||
|
if (options.className) classes.push(options.className);
|
||||||
|
if (options.disabledReason) classes.push("is-disabled");
|
||||||
|
const targetAction = options.disabledReason ? "show-disabled-reason" : action;
|
||||||
|
const title = options.disabledReason || options.title || "";
|
||||||
|
return `
|
||||||
|
<button
|
||||||
|
class="${classes.join(" ")}"
|
||||||
|
type="button"
|
||||||
|
data-action="${escapeHtml(targetAction)}"
|
||||||
|
${options.disabledReason ? `data-disabled-reason="${escapeHtml(options.disabledReason)}" aria-disabled="true"` : ""}
|
||||||
|
${title ? `title="${escapeHtml(title)}"` : ""}
|
||||||
|
>${escapeHtml(label)}</button>
|
||||||
|
`.replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionTag(label, action, attrs = "", options = {}) {
|
||||||
|
const classes = ["tag"];
|
||||||
|
const targetAction = options.disabledReason ? "show-disabled-reason" : action;
|
||||||
|
if (options.disabledReason) {
|
||||||
|
classes.push("tag-disabled");
|
||||||
|
} else if (targetAction) {
|
||||||
|
classes.push("clickable-tag");
|
||||||
|
}
|
||||||
|
const title = options.disabledReason || options.title || "";
|
||||||
|
return `
|
||||||
|
<span
|
||||||
|
class="${classes.join(" ")}"
|
||||||
|
${targetAction ? `data-action="${escapeHtml(targetAction)}"` : ""}
|
||||||
|
${options.disabledReason ? `data-disabled-reason="${escapeHtml(options.disabledReason)}" aria-disabled="true"` : ""}
|
||||||
|
${title ? `title="${escapeHtml(title)}"` : ""}
|
||||||
|
${attrs}
|
||||||
|
>${escapeHtml(label)}</span>
|
||||||
|
`.replace(/\s+/g, " ").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPipelineButton(kind, tone = "secondary") {
|
||||||
|
const config = PIPELINE_GUARDS[kind];
|
||||||
|
if (!config) return "";
|
||||||
|
const guard = getPipelineGuard(kind);
|
||||||
|
return button(config.label, config.openAction, tone, {
|
||||||
|
disabledReason: guard.enabled ? "" : guard.reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPipelineJobTag(kind, job, label) {
|
||||||
|
const config = PIPELINE_GUARDS[kind];
|
||||||
|
if (!config || !job?.id) return "";
|
||||||
|
const guard = getPipelineGuard(kind);
|
||||||
|
return actionTag(label, config.jobAction, `data-job-id="${escapeHtml(job.id)}"`, {
|
||||||
|
disabledReason: guard.enabled ? "" : guard.reason
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIntegrationOverviewPanel(options = {}) {
|
||||||
|
const overview = getIntegrationOverview();
|
||||||
|
const cards = overview.cards;
|
||||||
|
const showActions = options.showActions !== false;
|
||||||
|
return `
|
||||||
|
<div class="panel pad integration-panel ${options.compact ? "integration-panel-compact" : ""}">
|
||||||
|
<div class="integration-summary ${overview.tone}">
|
||||||
|
<div>
|
||||||
|
<strong>${escapeHtml(overview.headline)}</strong>
|
||||||
|
<p>${escapeHtml(overview.subtitle)}</p>
|
||||||
|
</div>
|
||||||
|
${showActions ? `
|
||||||
|
<div class="integration-actions">
|
||||||
|
${renderPipelineButton("aiVideo", "primary")}
|
||||||
|
${renderPipelineButton("realCut")}
|
||||||
|
</div>
|
||||||
|
` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="task-meta integration-highlights">
|
||||||
|
${cards.map((item) => `<span class="tag ${item.status.tone}">${escapeHtml(item.meta.label)} ${escapeHtml(item.status.summary)}</span>`).join("")}
|
||||||
|
</div>
|
||||||
|
<div class="layout-grid grid-4 integration-grid">
|
||||||
|
${cards.map((item) => `
|
||||||
|
<div class="integration-card ${item.status.tone}">
|
||||||
|
<div class="integration-card-head">
|
||||||
|
<div>
|
||||||
|
<h4>${escapeHtml(item.meta.label)}</h4>
|
||||||
|
<p>${escapeHtml(item.meta.hint)}</p>
|
||||||
|
</div>
|
||||||
|
<span class="tag ${item.status.tone}">${escapeHtml(item.status.summary)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-meta">
|
||||||
|
${safeArray(item.meta.impacts).map((impact) => `<span class="tag">${escapeHtml(impact)}</span>`).join("")}
|
||||||
|
${item.detail.statusCode ? `<span class="tag">HTTP ${escapeHtml(item.detail.statusCode)}</span>` : ""}
|
||||||
|
</div>
|
||||||
|
<div class="integration-note">${escapeHtml(item.note)}</div>
|
||||||
|
<div class="integration-url">${escapeHtml(item.detail.url || item.detail.baseUrl || "未提供探测地址")}</div>
|
||||||
|
</div>
|
||||||
|
`).join("")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderEmptyState(title, description) {
|
function renderEmptyState(title, description) {
|
||||||
@@ -936,6 +1234,9 @@ function renderDashboardScreen() {
|
|||||||
<div class="stat-card"><small>Agent</small><strong>${escapeHtml(formatNumber(assistants.length))}</strong><div class="stat-foot"><span>已创建</span><span class="warn">${escapeHtml(formatNumber(assistants.filter((item) => !(item.model_profile_id || "")).length))} 个待补模型</span></div></div>
|
<div class="stat-card"><small>Agent</small><strong>${escapeHtml(formatNumber(assistants.length))}</strong><div class="stat-foot"><span>已创建</span><span class="warn">${escapeHtml(formatNumber(assistants.filter((item) => !(item.model_profile_id || "")).length))} 个待补模型</span></div></div>
|
||||||
<div class="stat-card"><small>生产任务</small><strong>${escapeHtml(formatNumber(jobs.length))}</strong><div class="stat-foot"><span>最近 20 条</span><span class="positive">${escapeHtml(formatNumber(jobs.filter((item) => item.status === "completed").length))} 条已完成</span></div></div>
|
<div class="stat-card"><small>生产任务</small><strong>${escapeHtml(formatNumber(jobs.length))}</strong><div class="stat-foot"><span>最近 20 条</span><span class="positive">${escapeHtml(formatNumber(jobs.filter((item) => item.status === "completed").length))} 条已完成</span></div></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="margin-top:18px;">
|
||||||
|
${renderIntegrationOverviewPanel({ compact: true })}
|
||||||
|
</div>
|
||||||
<div class="layout-grid grid-main" style="margin-top:18px;">
|
<div class="layout-grid grid-main" style="margin-top:18px;">
|
||||||
<div class="side-stack">
|
<div class="side-stack">
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
@@ -1330,10 +1631,11 @@ function renderTrackingScreen() {
|
|||||||
}
|
}
|
||||||
const trackedAccounts = safeArray(appState.trackingAccounts);
|
const trackedAccounts = safeArray(appState.trackingAccounts);
|
||||||
const digestItems = getTrackingDigestItems(12);
|
const digestItems = getTrackingDigestItems(12);
|
||||||
|
const cursorLabel = appState.lastSeenAt ? formatDateTime(appState.lastSeenAt) : "尚未记录";
|
||||||
return screenShell(
|
return screenShell(
|
||||||
"跟踪账号",
|
"跟踪账号",
|
||||||
"这里已经接上真实跟踪对象和按上次打开后的更新日报。",
|
"这里已经接上真实跟踪对象和按上次打开后的更新日报。",
|
||||||
`${button("刷新日报", "refresh-data")} ${button("跳到找对标", "goto-discovery", "primary")}`,
|
`${button("同步全部", "refresh-tracking")} ${button("标记已读", "mark-tracking-read")} ${button("跳到找对标", "goto-discovery", "primary")}`,
|
||||||
`
|
`
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<h3>日报逻辑</h3>
|
<h3>日报逻辑</h3>
|
||||||
@@ -1342,6 +1644,7 @@ function renderTrackingScreen() {
|
|||||||
<span class="chip active">按上次打开汇总</span>
|
<span class="chip active">按上次打开汇总</span>
|
||||||
<span class="chip">Agent 标借鉴点</span>
|
<span class="chip">Agent 标借鉴点</span>
|
||||||
<span class="chip">高价值内容可进学习集</span>
|
<span class="chip">高价值内容可进学习集</span>
|
||||||
|
<span class="chip">上次已读 ${escapeHtml(cursorLabel)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="layout-grid grid-main" style="margin-top:18px;">
|
<div class="layout-grid grid-main" style="margin-top:18px;">
|
||||||
@@ -1361,7 +1664,8 @@ function renderTrackingScreen() {
|
|||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag green">已跟踪</span>
|
<span class="tag green">已跟踪</span>
|
||||||
<span class="tag">${escapeHtml(item.assistant_name || "未绑 Agent")}</span>
|
<span class="tag">${escapeHtml(item.assistant_name || "未绑 Agent")}</span>
|
||||||
<span class="tag clickable-tag" data-action="select-account" data-account-id="${escapeHtml(item.tracked_account_id)}">看详情</span>
|
${actionTag("立即同步", "refresh-tracked-account", `data-tracked-account-id="${escapeHtml(item.tracked_account_id)}"`)}
|
||||||
|
${actionTag("看详情", "select-account", `data-account-id="${escapeHtml(item.tracked_account_id)}"`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join("") || `<div class="task-item"><h4>暂无跟踪账号</h4><p>先去找对标把重点账号加入跟踪。</p></div>`}
|
`).join("") || `<div class="task-item"><h4>暂无跟踪账号</h4><p>先去找对标把重点账号加入跟踪。</p></div>`}
|
||||||
@@ -1402,21 +1706,15 @@ function renderAutomationScreen() {
|
|||||||
const analysisJobs = jobs.filter((item) => item.line_type === "analysis").length;
|
const analysisJobs = jobs.filter((item) => item.line_type === "analysis").length;
|
||||||
const aiVideoJobs = jobs.filter((item) => item.line_type === "ai_video").length;
|
const aiVideoJobs = jobs.filter((item) => item.line_type === "ai_video").length;
|
||||||
const realCutJobs = jobs.filter((item) => item.line_type === "real_cut").length;
|
const realCutJobs = jobs.filter((item) => item.line_type === "real_cut").length;
|
||||||
const integrations = appState.integrationHealth || {};
|
const overview = getIntegrationOverview();
|
||||||
const integrationCards = [
|
|
||||||
{ key: "cutvideo", label: "自动剪辑", hint: "Windows cutvideo" },
|
|
||||||
{ key: "huobao", label: "AI 视频", hint: "huobao-drama" },
|
|
||||||
{ key: "n8n", label: "编排", hint: "n8n workflow" },
|
|
||||||
{ key: "asr", label: "ASR", hint: "转写服务" }
|
|
||||||
];
|
|
||||||
return screenShell(
|
return screenShell(
|
||||||
"自动流程",
|
"自动流程",
|
||||||
"自动同步、日报生成和失败补跑先统一看这里。",
|
"自动同步、日报生成和失败补跑先统一看这里。",
|
||||||
`${button("刷新", "refresh-data")} ${button("去生产", "goto-production", "primary")}`,
|
`${button("刷新", "refresh-data")} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
|
||||||
`
|
`
|
||||||
<div class="hero-card">
|
<div class="hero-card">
|
||||||
<h3>自动流程</h3>
|
<h3>自动流程</h3>
|
||||||
<p>当前按真实任务量给出一版轻量看板,后续再接更完整的定时与重试配置。</p>
|
<p>当前按真实任务量和依赖健康状态给出看板,自动流程受阻时会直接在这里拦住动作。</p>
|
||||||
<div class="mini-grid">
|
<div class="mini-grid">
|
||||||
<div class="mini-card"><small>分析任务</small><strong>${escapeHtml(formatNumber(analysisJobs))}</strong></div>
|
<div class="mini-card"><small>分析任务</small><strong>${escapeHtml(formatNumber(analysisJobs))}</strong></div>
|
||||||
<div class="mini-card"><small>AI 视频</small><strong>${escapeHtml(formatNumber(aiVideoJobs))}</strong></div>
|
<div class="mini-card"><small>AI 视频</small><strong>${escapeHtml(formatNumber(aiVideoJobs))}</strong></div>
|
||||||
@@ -1424,25 +1722,27 @@ function renderAutomationScreen() {
|
|||||||
<div class="mini-card"><small>内容源</small><strong>${escapeHtml(formatNumber(appState.contentSources.length))}</strong></div>
|
<div class="mini-card"><small>内容源</small><strong>${escapeHtml(formatNumber(appState.contentSources.length))}</strong></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel pad" style="margin-top:18px;">
|
<div style="margin-top:18px;">
|
||||||
<div class="panel-head"><div><h3>集成状态</h3><div class="panel-subtitle">直接看关键依赖是否在线</div></div></div>
|
${renderIntegrationOverviewPanel({ showActions: false })}
|
||||||
<div class="layout-grid grid-4" style="margin-top:14px;">
|
|
||||||
${integrationCards.map((item) => {
|
|
||||||
const detail = integrations[item.key] || {};
|
|
||||||
const tone = detail.reachable ? "green" : (detail.configured ? "red" : "orange");
|
|
||||||
const summary = detail.reachable ? "在线" : (detail.configured ? "不可达" : "未配置");
|
|
||||||
return `
|
|
||||||
<div class="queue-card">
|
|
||||||
<h4>${escapeHtml(item.label)}</h4>
|
|
||||||
<p>${escapeHtml(item.hint)}</p>
|
|
||||||
<div class="task-meta">
|
|
||||||
<span class="tag ${tone}">${escapeHtml(summary)}</span>
|
|
||||||
${detail.status_code ? `<span class="tag">HTTP ${escapeHtml(detail.status_code)}</span>` : ""}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel pad automation-guard-panel" style="margin-top:18px;">
|
||||||
|
<div class="panel-head">
|
||||||
|
<div>
|
||||||
|
<h3>动作防呆</h3>
|
||||||
|
<div class="panel-subtitle">依赖不可用时,相关动作会在这里和生产页一起被拦住。</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
<span class="tag ${escapeHtml(overview.tone)}">${escapeHtml(overview.headline)}</span>
|
||||||
}).join("")}
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="task-meta integration-highlights">
|
||||||
|
<span class="tag ${getPipelineGuard("aiVideo").enabled ? "green" : "red"}">AI 视频 ${escapeHtml(getPipelineGuard("aiVideo").enabled ? "可执行" : "已拦截")}</span>
|
||||||
|
<span class="tag ${getPipelineGuard("realCut").enabled ? "green" : "red"}">实拍剪辑 ${escapeHtml(getPipelineGuard("realCut").enabled ? "可执行" : "已拦截")}</span>
|
||||||
|
<span class="tag blue">ASR ${escapeHtml(getIntegrationStatus(getIntegrationDetail("asr")).summary)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="integration-actions" style="margin-top:14px;">
|
||||||
|
${renderPipelineButton("aiVideo", "primary")}
|
||||||
|
${renderPipelineButton("realCut")}
|
||||||
|
</div>
|
||||||
|
<div class="integration-note" style="margin-top:12px;">${escapeHtml(overview.subtitle)}</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
@@ -1563,7 +1863,7 @@ function renderProductionScreen() {
|
|||||||
return screenShell(
|
return screenShell(
|
||||||
"生产中心",
|
"生产中心",
|
||||||
"这里已经接上真实任务和知识库文档,后续再继续补任务创建动作。",
|
"这里已经接上真实任务和知识库文档,后续再继续补任务创建动作。",
|
||||||
`${button("AI 视频", "open-ai-video")} ${button("实拍剪辑", "open-real-cut")} ${button("去复盘", "goto-review", "primary")}`,
|
`${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去复盘", "goto-review", "primary")}`,
|
||||||
`
|
`
|
||||||
<div class="panel pad">
|
<div class="panel pad">
|
||||||
<div class="panel-head"><div><h3>生产队列</h3><div class="panel-subtitle">最近任务的真实状态</div></div></div>
|
<div class="panel-head"><div><h3>生产队列</h3><div class="panel-subtitle">最近任务的真实状态</div></div></div>
|
||||||
@@ -1591,9 +1891,9 @@ 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>
|
||||||
${canDeriveAiVideo(job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(job.id)}">做 AI 视频</span>` : ""}
|
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""}
|
||||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">做实拍剪辑</span>` : ""}
|
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""}
|
||||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span>
|
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
|
`).join("") || `<div class="task-item"><h4>还没有任务</h4><p>先去找对标导入内容。</p></div>`}
|
||||||
@@ -1674,10 +1974,10 @@ function renderReviewScreen() {
|
|||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag green">已完成</span>
|
<span class="tag green">已完成</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-review-from-job" data-job-id="${escapeHtml(job.id)}">写复盘</span>
|
${actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(job.id)}"`)}
|
||||||
${canDeriveAiVideo(job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(job.id)}">做 AI 视频</span>` : ""}
|
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "做 AI 视频") : ""}
|
||||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">做实拍剪辑</span>` : ""}
|
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "做实拍剪辑") : ""}
|
||||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(job.id)}">看详情</span>
|
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(job.id)}"`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
|
`).join("") || `<div class="review-card"><h4>还没有完成任务</h4><p>先去生产中心跑一条链路。</p></div>`}
|
||||||
@@ -1858,10 +2158,10 @@ function renderLastJobDetailCard() {
|
|||||||
<p>${escapeHtml(brief(detail.job.style_summary || detail.job.transcript_text || detail.job.error || "暂无摘要", 120))}</p>
|
<p>${escapeHtml(brief(detail.job.style_summary || detail.job.transcript_text || detail.job.error || "暂无摘要", 120))}</p>
|
||||||
<div class="task-meta">
|
<div class="task-meta">
|
||||||
<span class="tag">${escapeHtml(detail.job.line_type || "-")}</span>
|
<span class="tag">${escapeHtml(detail.job.line_type || "-")}</span>
|
||||||
${detail.job.status === "completed" ? `<span class="tag clickable-tag" data-action="open-review-from-job" data-job-id="${escapeHtml(detail.job.id)}">写复盘</span>` : ""}
|
${detail.job.status === "completed" ? actionTag("写复盘", "open-review-from-job", `data-job-id="${escapeHtml(detail.job.id)}"`) : ""}
|
||||||
${canDeriveAiVideo(detail.job) ? `<span class="tag clickable-tag" data-action="job-to-ai-video" data-job-id="${escapeHtml(detail.job.id)}">做 AI 视频</span>` : ""}
|
${canDeriveAiVideo(detail.job) ? renderPipelineJobTag("aiVideo", detail.job, "做 AI 视频") : ""}
|
||||||
${canDeriveRealCut(detail.job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(detail.job.id)}">做实拍剪辑</span>` : ""}
|
${canDeriveRealCut(detail.job) ? renderPipelineJobTag("realCut", detail.job, "做实拍剪辑") : ""}
|
||||||
<span class="tag clickable-tag" data-action="open-job-detail" data-job-id="${escapeHtml(detail.job.id)}">看详情</span>
|
${actionTag("看详情", "open-job-detail", `data-job-id="${escapeHtml(detail.job.id)}"`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${previewLinks.length ? `
|
${previewLinks.length ? `
|
||||||
@@ -2405,9 +2705,9 @@ function openJobDetailAction(jobId) {
|
|||||||
label: "下一步动作",
|
label: "下一步动作",
|
||||||
html: `
|
html: `
|
||||||
<div class="task-meta">
|
<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>` : ""}
|
${canDeriveAiVideo(job) ? renderPipelineJobTag("aiVideo", job, "继续做 AI 视频") : ""}
|
||||||
${canDeriveRealCut(job) ? `<span class="tag clickable-tag" data-action="job-to-real-cut" data-job-id="${escapeHtml(job.id)}">继续做实拍剪辑</span>` : ""}
|
${canDeriveRealCut(job) ? renderPipelineJobTag("realCut", job, "继续做实拍剪辑") : ""}
|
||||||
<span class="tag clickable-tag" data-action="job-to-generate-copy" data-job-id="${escapeHtml(job.id)}">用摘要写文案</span>
|
${actionTag("用摘要写文案", "job-to-generate-copy", `data-job-id="${escapeHtml(job.id)}"`)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
},
|
},
|
||||||
@@ -2489,6 +2789,11 @@ function openGenerateCopyAction(defaults = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreateAiVideoAction(defaults = {}) {
|
function openCreateAiVideoAction(defaults = {}) {
|
||||||
|
const guard = getPipelineGuard("aiVideo");
|
||||||
|
if (!guard.enabled) {
|
||||||
|
alert(guard.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const project = requireSelectedProject();
|
const project = requireSelectedProject();
|
||||||
const assistant = getSelectedAssistant();
|
const assistant = getSelectedAssistant();
|
||||||
const kb = getProjectKnowledgeBases(project.id)[0];
|
const kb = getProjectKnowledgeBases(project.id)[0];
|
||||||
@@ -2529,6 +2834,11 @@ function openCreateAiVideoAction(defaults = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openCreateRealCutAction(defaults = {}) {
|
function openCreateRealCutAction(defaults = {}) {
|
||||||
|
const guard = getPipelineGuard("realCut");
|
||||||
|
if (!guard.enabled) {
|
||||||
|
alert(guard.reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const project = requireSelectedProject();
|
const project = requireSelectedProject();
|
||||||
const sourceJob = defaults.sourceJob || null;
|
const sourceJob = defaults.sourceJob || null;
|
||||||
openActionModal({
|
openActionModal({
|
||||||
@@ -2670,10 +2980,27 @@ document.addEventListener("click", async (event) => {
|
|||||||
// button and pressing Enter share the same code path.
|
// button and pressing Enter share the same code path.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name === "show-disabled-reason") {
|
||||||
|
const reason = action.dataset.disabledReason || action.title || "当前动作暂不可用";
|
||||||
|
rememberAction("动作已拦截", reason, "orange");
|
||||||
|
renderAll();
|
||||||
|
alert(reason);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (name === "auth-refresh" || name === "refresh-data") {
|
if (name === "auth-refresh" || name === "refresh-data") {
|
||||||
await bootstrap();
|
await bootstrap();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name === "refresh-tracking") {
|
||||||
|
await refreshTrackingAccountsAction();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (name === "mark-tracking-read") {
|
||||||
|
await markTrackingDigestRead();
|
||||||
|
rememberAction("日报已标记", "当前跟踪摘要已更新为已读,下次会从新的时间点继续汇总。", "green");
|
||||||
|
await bootstrap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (name === "logout-session") {
|
if (name === "logout-session") {
|
||||||
await logoutSession();
|
await logoutSession();
|
||||||
return;
|
return;
|
||||||
@@ -2706,6 +3033,10 @@ document.addEventListener("click", async (event) => {
|
|||||||
openTrackSelectedAccountAction();
|
openTrackSelectedAccountAction();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name === "refresh-tracked-account") {
|
||||||
|
await refreshTrackedAccountAction(action.dataset.trackedAccountId || "");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (name === "open-import-video-link") {
|
if (name === "open-import-video-link") {
|
||||||
openImportVideoLinkAction();
|
openImportVideoLinkAction();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -503,6 +503,28 @@ select {
|
|||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.is-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.62;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
background: linear-gradient(180deg, #f4f7fb 0%, #e9eff7 100%);
|
||||||
|
color: var(--muted);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.is-disabled:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.is-disabled,
|
||||||
|
.btn[aria-disabled="true"] {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.68;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
.layout-grid {
|
.layout-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@@ -676,10 +698,153 @@ select {
|
|||||||
color: #b24c4c;
|
color: #b24c4c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.72;
|
||||||
|
background: #f3f6fa;
|
||||||
|
border-color: var(--line-strong);
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
.clickable-tag {
|
.clickable-tag {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(245, 249, 255, 0.96) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-panel-compact .integration-grid {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary.green {
|
||||||
|
border-color: rgba(45, 181, 132, 0.2);
|
||||||
|
background: linear-gradient(180deg, rgba(45, 181, 132, 0.12) 0%, rgba(255, 255, 255, 0.96) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary.orange {
|
||||||
|
border-color: rgba(242, 154, 56, 0.24);
|
||||||
|
background: linear-gradient(180deg, rgba(242, 154, 56, 0.13) 0%, rgba(255, 255, 255, 0.96) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary.red {
|
||||||
|
border-color: rgba(228, 103, 103, 0.24);
|
||||||
|
background: linear-gradient(180deg, rgba(228, 103, 103, 0.13) 0%, rgba(255, 255, 255, 0.97) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-summary.blue {
|
||||||
|
border-color: rgba(79, 143, 238, 0.18);
|
||||||
|
background: linear-gradient(180deg, rgba(79, 143, 238, 0.11) 0%, rgba(255, 255, 255, 0.97) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-highlights {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-grid {
|
||||||
|
gap: 14px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card {
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
background: linear-gradient(180deg, #fff 0%, #f8fbff 100%);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card.green {
|
||||||
|
border-color: rgba(45, 181, 132, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card.orange {
|
||||||
|
border-color: rgba(242, 154, 56, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card.red {
|
||||||
|
border-color: rgba(228, 103, 103, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card.blue {
|
||||||
|
border-color: rgba(79, 143, 238, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card-head h4 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-card-head p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-note {
|
||||||
|
margin-top: 12px;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-url {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 9px 11px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: var(--blue-50);
|
||||||
|
border: 1px solid rgba(106, 164, 255, 0.14);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.45;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.automation-guard-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-only {
|
.mobile-only {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -1300,6 +1465,20 @@ tbody tr:hover {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-actions {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-actions .btn {
|
||||||
|
flex: 1 1 calc(50% - 10px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-4,
|
.grid-4,
|
||||||
.grid-5,
|
.grid-5,
|
||||||
.mini-grid,
|
.mini-grid,
|
||||||
@@ -1328,6 +1507,20 @@ tbody tr:hover {
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-summary {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-actions {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.integration-actions .btn {
|
||||||
|
flex: 1 1 calc(50% - 5px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.task-meta,
|
.task-meta,
|
||||||
.entity-meta,
|
.entity-meta,
|
||||||
.row-meta,
|
.row-meta,
|
||||||
@@ -1527,6 +1720,10 @@ tbody tr:hover {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.integration-card-head {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.task-item.compact,
|
.task-item.compact,
|
||||||
.review-card.compact {
|
.review-card.compact {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
|||||||
Reference in New Issue
Block a user