feat: add oneliner control plane ui

This commit is contained in:
kris
2026-03-23 15:01:15 +08:00
parent 56255688c1
commit 8d54c21786
4 changed files with 2354 additions and 21 deletions

View File

@@ -24,6 +24,7 @@ from pydantic import BaseModel, Field
from .database import Database, utc_now
from .douyin_features import register_douyin_routes
from .integrations import AsrHttpClient, CutVideoClient, HuobaoDramaClient, N8NClient
from .oneliner_features import register_oneliner_routes
from .openai_compat import OpenAICompatClient
BASE_DIR = Path(__file__).resolve().parents[2]
@@ -60,6 +61,8 @@ CUTVIDEO_UPLOAD_TIMEOUT_SEC = int(os.getenv("CUTVIDEO_UPLOAD_TIMEOUT_SEC", "1800
HUOBAO_POLL_INTERVAL_SEC = int(os.getenv("HUOBAO_POLL_INTERVAL_SEC", "10"))
HUOBAO_MAX_WAIT_SEC = int(os.getenv("HUOBAO_MAX_WAIT_SEC", "900"))
DOMESTIC_PLATFORMS = {"douyin", "xiaohongshu", "bilibili", "kuaishou", "wechat_video"}
for path in (DATA_DIR, DOWNLOADS_DIR, JOBS_DIR, MODELS_DIR):
path.mkdir(parents=True, exist_ok=True)
@@ -3319,3 +3322,4 @@ def publish_app_update(request: PublishAppUpdateRequest, admin: dict[str, Any] =
register_douyin_routes(app, sys.modules[__name__])
register_oneliner_routes(app, sys.modules[__name__])

File diff suppressed because it is too large Load Diff

View File

@@ -31,6 +31,12 @@ const appState = {
integrationHealth: null,
localModelCatalog: null,
backendCapabilities: null,
onelinerProfile: null,
onelinerSessions: [],
selectedOnelinerSessionId: "",
onelinerMessages: [],
platformAgents: [],
adminOpsOverview: null,
busy: false,
message: "",
lastAction: null,
@@ -145,6 +151,23 @@ const PIPELINE_GUARDS = {
}
};
const ONELINER_INTENT_LABELS = {
create_project: "创建项目",
create_assistant: "创建 Agent",
import_homepage: "导入主页",
track_account: "跟踪账号",
analyze_account: "分析账号",
analyze_top_videos: "分析高分作品",
generate_copy: "生成文案",
ai_video: "AI 视频",
real_cut: "实拍剪辑",
review: "发布复盘",
live_recorder: "直播录制",
storage_status: "存储状态",
ops_admin: "运维巡检",
custom: "自定义任务"
};
function safeArray(value) {
return Array.isArray(value) ? value : [];
}
@@ -619,6 +642,149 @@ function closeActionModal() {
document.querySelector(".action-modal-backdrop")?.classList.add("hidden");
}
function ensureOneLinerUi() {
if (!document.querySelector(".oneliner-fab")) {
const fab = document.createElement("button");
fab.className = "oneliner-fab";
fab.type = "button";
fab.dataset.action = "open-oneliner";
fab.innerHTML = `
<span class="oneliner-fab-mark">1</span>
<span class="oneliner-fab-text">OneLiner</span>
`;
document.body.appendChild(fab);
}
if (!document.querySelector(".oneliner-backdrop")) {
const panel = document.createElement("div");
panel.className = "oneliner-backdrop hidden";
panel.innerHTML = `
<div class="oneliner-panel">
<div class="oneliner-head">
<div>
<h3>OneLiner</h3>
<p>前端没上的需求,先由总控主 Agent 承接。</p>
</div>
<div class="task-meta">
<button class="btn btn-secondary" type="button" data-action="open-oneliner-profile">配置</button>
<button class="btn btn-secondary" type="button" data-action="close-oneliner">关闭</button>
</div>
</div>
<div class="oneliner-meta" data-role="oneliner-meta"></div>
<div class="oneliner-sessions" data-role="oneliner-sessions"></div>
<div class="oneliner-messages" data-role="oneliner-messages"></div>
<form class="oneliner-composer" data-role="oneliner-form">
<textarea data-role="oneliner-input" rows="4" placeholder="比如:帮我把这个账号加入跟踪并绑定快手 Agent或者这个需求前端还没有你直接帮我拆任务。"></textarea>
<div class="oneliner-actions">
<div class="helper-text" data-role="oneliner-status"></div>
<button class="btn btn-primary" type="submit" data-action="submit-oneliner">发送给 OneLiner</button>
</div>
</form>
</div>
`;
document.body.appendChild(panel);
}
}
function renderOneLinerSessionTabs() {
const sessions = safeArray(appState.onelinerSessions).slice(0, 6);
if (!sessions.length) {
return `<div class="task-meta"><span class="tag blue">还没有会话</span><span class="tag">发送第一条需求后自动创建</span></div>`;
}
const currentId = getCurrentOneLinerSession()?.id || "";
return `
<div class="chip-row">
${sessions.map((session) => `
<span class="chip clickable-tag ${session.id === currentId ? "active" : ""}" data-action="select-oneliner-session" data-session-id="${escapeHtml(session.id)}">
${escapeHtml(brief(session.title || "新会话", 14))}
</span>
`).join("")}
</div>
`;
}
function renderOneLinerMessagesHtml() {
const messages = safeArray(appState.onelinerMessages);
if (!messages.length) {
return `
<div class="task-item">
<h4>还没有对话</h4>
<p>你可以直接说目标不用先理解平台有什么按钮。OneLiner 会先拆目标,再决定交给哪个平台 Agent。</p>
</div>
`;
}
return messages.map((message) => {
const roleClass = message.role === "assistant" ? "assistant" : "user";
const result = message.result || {};
const plan = message.plan || {};
const actions = safeArray(plan.suggested_actions);
return `
<div class="oneliner-message ${roleClass}">
<div class="oneliner-bubble">
<strong>${escapeHtml(message.role === "assistant" ? "OneLiner" : "你")}</strong>
<p>${escapeHtml(message.content || result.summary_text || "")}</p>
${plan.intent_key ? `
<div class="task-meta">
<span class="tag blue">${escapeHtml(onelinerIntentLabel(plan.intent_key))}</span>
${plan.platform_label ? `<span class="tag">${escapeHtml(plan.platform_label)}</span>` : ""}
${plan.delivery_mode ? `<span class="tag ${plan.delivery_mode === "oneliner" ? "orange" : "green"}">${escapeHtml(plan.delivery_mode === "oneliner" ? "对话承接" : "可走前端")}</span>` : ""}
</div>
` : ""}
${actions.length ? `
<div class="task-meta" style="margin-top:10px;">
${actions.map((item) => `<span class="tag clickable-tag" data-action="${escapeHtml(item.key)}">${escapeHtml(item.label)}</span>`).join("")}
</div>
` : ""}
</div>
</div>
`;
}).join("");
}
function renderOneLinerUi() {
ensureOneLinerUi();
const fab = document.querySelector(".oneliner-fab");
const meta = document.querySelector('[data-role="oneliner-meta"]');
const sessions = document.querySelector('[data-role="oneliner-sessions"]');
const messages = document.querySelector('[data-role="oneliner-messages"]');
const status = document.querySelector('[data-role="oneliner-status"]');
const input = document.querySelector('[data-role="oneliner-input"]');
const profile = appState.onelinerProfile;
if (fab) {
fab.hidden = !appState.session;
}
if (meta) {
meta.innerHTML = `
<div class="task-meta">
<span class="tag blue">${escapeHtml(profile?.display_name || "OneLiner")}</span>
<span class="tag">${escapeHtml(getSelectedProject()?.name || "未选项目")}</span>
<span class="tag">${escapeHtml(profile?.default_platform ? platformLabel(profile.default_platform) : "未设默认平台")}</span>
<span class="tag green">${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))} 个平台 Agent</span>
</div>
<div class="helper-text">${escapeHtml(profile?.long_term_goal || "当前没有设置长期目标。你可以先在这里说目标,后续再逐步产品化。")}</div>
`;
}
if (sessions) sessions.innerHTML = renderOneLinerSessionTabs();
if (messages) {
messages.innerHTML = renderOneLinerMessagesHtml();
messages.scrollTop = messages.scrollHeight;
}
if (status) {
status.textContent = appState.busy ? appState.message || "处理中..." : "";
}
if (input && !input.value && !safeArray(appState.onelinerMessages).length) {
input.value = "";
}
}
function openOneLinerPanel() {
ensureOneLinerUi();
document.querySelector(".oneliner-backdrop")?.classList.remove("hidden");
}
function closeOneLinerPanel() {
document.querySelector(".oneliner-backdrop")?.classList.add("hidden");
}
function readActionForm() {
const values = {};
document.querySelectorAll("[data-action-field]").forEach((element) => {
@@ -778,6 +944,12 @@ async function logoutSession() {
appState.trackingAccounts = [];
appState.trackingDigest = null;
appState.reviews = [];
appState.onelinerProfile = null;
appState.onelinerSessions = [];
appState.selectedOnelinerSessionId = "";
appState.onelinerMessages = [];
appState.platformAgents = [];
appState.adminOpsOverview = null;
appState.integrationHealth = null;
appState.storageStatus = null;
appState.backendCapabilities = null;
@@ -811,6 +983,96 @@ async function loadStorageStatus(projectId = "") {
return payload;
}
async function loadAgentControlSurfaces(projectId = "") {
const normalizedProjectId = projectId || getOneLinerProjectId();
const supportsOneLinerProfile = backendSupports("/v2/oneliner/profile");
const supportsOneLinerSessions = backendSupports("/v2/oneliner/sessions");
const supportsPlatformAgents = backendSupports("/v2/platform-agents");
const supportsAdminOps = backendSupports("/v2/admin/ops/overview");
const [profile, sessionsPayload, platformAgentsPayload, adminOpsOverview] = await Promise.all([
supportsOneLinerProfile
? storyforgeFetch(`/v2/oneliner/profile?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => null)
: Promise.resolve(null),
supportsOneLinerSessions
? storyforgeFetch(`/v2/oneliner/sessions?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsPlatformAgents
? storyforgeFetch(`/v2/platform-agents?project_id=${encodeURIComponent(normalizedProjectId)}`).catch(() => ({ items: [] }))
: Promise.resolve({ items: [] }),
supportsAdminOps && isSuperAdmin()
? storyforgeFetch("/v2/admin/ops/overview").catch(() => null)
: Promise.resolve(null)
]);
appState.onelinerProfile = profile;
appState.onelinerSessions = safeArray(sessionsPayload?.items || sessionsPayload);
if (!appState.selectedOnelinerSessionId || !safeArray(appState.onelinerSessions).some((item) => item.id === appState.selectedOnelinerSessionId)) {
appState.selectedOnelinerSessionId = safeArray(appState.onelinerSessions)[0]?.id || "";
}
appState.platformAgents = safeArray(platformAgentsPayload?.items || platformAgentsPayload);
appState.adminOpsOverview = adminOpsOverview;
}
async function loadOneLinerMessages(sessionId) {
if (!sessionId || !backendSupports("/v2/oneliner/sessions/{session_id}/messages")) {
appState.onelinerMessages = [];
return [];
}
const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(sessionId)}/messages`).catch(() => ({ items: [] }));
appState.onelinerMessages = safeArray(payload?.items || payload);
return appState.onelinerMessages;
}
async function ensureOneLinerSession() {
const projectId = getOneLinerProjectId();
if (!projectId) throw new Error("当前还没有项目OneLiner 需要先绑定项目上下文。");
if (!backendSupports("/v2/oneliner/sessions")) {
throw new Error("当前后端还没有接入 OneLiner 会话接口。");
}
let session = getCurrentOneLinerSession();
if (!session) {
session = await storyforgeFetch("/v2/oneliner/sessions", {
method: "POST",
body: {
project_id: projectId,
preferred_platform: getPreferredPlatform()
}
});
appState.onelinerSessions = [session, ...safeArray(appState.onelinerSessions)];
appState.selectedOnelinerSessionId = session.id;
}
await loadOneLinerMessages(session.id);
return session;
}
async function submitOneLinerMessage(content) {
const projectId = getOneLinerProjectId();
const session = await ensureOneLinerSession();
const payload = await storyforgeFetch(`/v2/oneliner/sessions/${encodeURIComponent(session.id)}/messages`, {
method: "POST",
body: {
content,
project_id: projectId,
platform: getPreferredPlatform()
}
});
appState.selectedOnelinerSessionId = payload.session?.id || session.id;
await loadAgentControlSurfaces(projectId);
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [
...safeArray(appState.onelinerMessages),
payload.user_message,
payload.assistant_message
].filter(Boolean);
}
appState.onelinerProfile = payload.result?.context?.oneliner_profile || appState.onelinerProfile || null;
rememberAction("OneLiner 已响应", payload.result?.summary_text || "已返回一版任务拆解。", "blue", payload);
return payload;
}
async function loadPlatformAccount(platform, accountId) {
if (!accountId) return;
const normalizedPlatform = normalizePlatformValue(platform, getPreferredPlatform());
@@ -929,6 +1191,12 @@ async function bootstrap() {
} else {
appState.storageStatus = null;
}
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [];
}
const selectedAssistantExists = safeArray(dashboard.assistants).some((item) => item.id === appState.selectedAssistantId);
appState.selectedAssistantId = selectedAssistantExists ? appState.selectedAssistantId : (dashboard.assistants?.[0]?.id || "");
const selectedAccountExists = appState.accounts.some((item) => item.id === appState.selectedAccountId);
@@ -1029,6 +1297,23 @@ function getSelectedProject() {
return projects.find((item) => item.id === appState.selectedProjectId) || projects[0] || null;
}
function isSuperAdmin() {
return appState.me?.role === "super_admin";
}
function getOneLinerProjectId() {
return getSelectedProject()?.id || appState.selectedProjectId || safeArray(appState.dashboard?.projects)[0]?.id || "";
}
function getCurrentOneLinerSession() {
const sessions = safeArray(appState.onelinerSessions);
return sessions.find((item) => item.id === appState.selectedOnelinerSessionId) || sessions[0] || null;
}
function onelinerIntentLabel(value) {
return ONELINER_INTENT_LABELS[value] || value || "自定义任务";
}
function getProjectKnowledgeBases(projectId) {
return safeArray(appState.dashboard?.knowledge_bases).filter((item) => item.project_id === projectId);
}
@@ -1617,6 +1902,91 @@ function renderStorageStatusPanel() {
`;
}
function renderPlatformAgentPanel() {
const items = safeArray(appState.platformAgents);
if (!items.length) {
return `
<div class="panel pad">
<div class="panel-head"><div><h3>平台 Agent</h3><div class="panel-subtitle">当前后端还没接入平台 Agent 控制面。</div></div></div>
<div class="task-item"><h4>暂未接入</h4><p>等 live collector 同步 `/v2/platform-agents` 后,这里会切成真实视图。</p></div>
</div>
`;
}
return `
<div class="panel pad">
<div class="panel-head">
<div>
<h3>平台 Agent</h3>
<div class="panel-subtitle">按用户 + 平台隔离,沉淀该平台的方法论、记忆和技能。</div>
</div>
<span class="tag blue">${escapeHtml(formatNumber(items.length))} 个</span>
</div>
<div class="three-col">
${items.map((item) => `
<div class="entity-card pad">
<div class="cell-title">${escapeHtml(item.name || item.platform_label)}</div>
<div class="cell-desc">${escapeHtml(item.mission || item.notes || "先绑定执行 Agent再补任务目标和方法论。")}</div>
<div class="entity-meta">
<span class="tag ${item.status === "active" ? "green" : "blue"}">${escapeHtml(item.status || "draft")}</span>
<span class="tag">记忆 ${escapeHtml(formatNumber(item.memory_count))}</span>
<span class="tag">技能 ${escapeHtml(formatNumber(item.skill_count))}</span>
<span class="tag">${escapeHtml(item.assistant?.name || "未绑 Agent")}</span>
</div>
<div class="task-meta" style="margin-top:10px;">
<span class="tag clickable-tag" data-action="open-platform-agent-profile" data-platform="${escapeHtml(item.platform)}">配置</span>
<span class="tag clickable-tag" data-action="open-platform-agent-memory" data-platform="${escapeHtml(item.platform)}">补记忆</span>
<span class="tag clickable-tag" data-action="open-platform-agent-skill" data-platform="${escapeHtml(item.platform)}">补技能</span>
</div>
</div>
`).join("")}
</div>
</div>
`;
}
function renderAdminOpsPanel() {
if (!isSuperAdmin()) return "";
const overview = appState.adminOpsOverview;
if (!overview) {
return `
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head"><div><h3>运维与审计 Agent</h3><div class="panel-subtitle">仅平台最高权限用户可见。</div></div></div>
<div class="task-item"><h4>尚未拉到概览</h4><p>刷新后会自动读取失败任务、集成健康和待审事件。</p></div>
</div>
`;
}
const incidents = safeArray(overview.incidents).slice(0, 6);
return `
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>运维与审计 Agent</h3>
<div class="panel-subtitle">只给管理员开放,主要盯日志、失败任务和集成异常。</div>
</div>
<div class="task-meta">
<span class="tag blue">${escapeHtml(formatNumber(overview.incident_count))} 条事件</span>
<span class="tag">${escapeHtml(formatNumber(overview.failed_job_count))} 个失败任务</span>
<span class="tag clickable-tag" data-action="scan-admin-ops">重新扫描</span>
</div>
</div>
<div class="list">
${incidents.map((item) => `
<div class="task-item compact">
<h4>${escapeHtml(item.title)}</h4>
<p>${escapeHtml(item.summary || "待补详情")}</p>
<div class="task-meta">
<span class="tag ${item.severity === "error" ? "red" : "orange"}">${escapeHtml(item.severity || "warn")}</span>
<span class="tag">${escapeHtml(item.status || "open")}</span>
${item.tenant_user_id ? `<span class="tag">租户 ${escapeHtml(brief(item.tenant_user_id, 12))}</span>` : ""}
<span class="tag clickable-tag" data-action="review-admin-incident" data-incident-id="${escapeHtml(item.id)}">审计处理</span>
</div>
</div>
`).join("") || `<div class="task-item"><h4>当前没有待处理事件</h4><p>最近主链比较稳定,继续观察即可。</p></div>`}
</div>
</div>
`;
}
function getIntegrationOverview() {
const cards = getIntegrationCards();
const reachableCount = cards.filter((item) => item.detail.available && item.detail.reachable).length;
@@ -1896,7 +2266,7 @@ function renderDashboardScreen() {
return screenShell(
"项目总台",
"先看项目状态、待办动作和高价值对标。",
`${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
`${button("新建项目", "create-project")} ${button("导入主页", "open-import-homepage")} ${button("OneLiner", "open-oneliner")} ${button("创建 Agent", "open-create-assistant", "primary")}`,
`
<div class="layout-grid grid-5">
<div class="stat-card"><small>活跃项目</small><strong>${escapeHtml(formatNumber(projects.length))}</strong><div class="stat-foot"><span>项目总数</span><span class="positive">${escapeHtml(formatNumber(projects.filter((item) => item.description).length))} 个有说明</span></div></div>
@@ -1966,6 +2336,9 @@ function renderDashboardScreen() {
<div class="mini-card"><small>来源</small><strong>${escapeHtml(formatNumber(getSelectedProject() ? getProjectStats(getSelectedProject().id).sources.length : 0))}</strong></div>
</div>
</div>
<div style="margin-top:18px;">
${renderPlatformAgentPanel()}
</div>
${renderStorageStatusPanel()}
<div class="panel pad">
<div class="panel-head"><div><h3>跟踪摘要</h3><div class="panel-subtitle">按最近同步的账号作品生成</div></div><span class="tag blue">${escapeHtml(daysSince(appState.lastSeenAt))} 天汇总</span></div>
@@ -2400,7 +2773,7 @@ function renderAutomationScreen() {
return screenShell(
"自动流程",
"自动同步、日报生成和失败补跑先统一看这里。",
`${button("刷新", "refresh-data")} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
`${button("刷新", "refresh-data")} ${button("OneLiner", "open-oneliner")} ${renderPipelineButton("aiVideo")} ${renderPipelineButton("realCut")} ${button("去生产", "goto-production", "primary")}`,
`
<div class="hero-card">
<h3>自动流程</h3>
@@ -2434,6 +2807,7 @@ function renderAutomationScreen() {
</div>
<div class="integration-note" style="margin-top:12px;">${escapeHtml(overview.subtitle)}</div>
</div>
${renderAdminOpsPanel()}
`
);
}
@@ -2481,7 +2855,7 @@ function renderPlaybookScreen() {
return screenShell(
"Agent",
"这里接真实 Agent 列表,当前已经支持切换和编辑 Agent。",
`${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("生成文案", "open-generate-copy")} ${button("去生产", "goto-production", "primary")}`,
`${button("配置 OneLiner", "open-oneliner-profile")} ${button("设主模型", "open-preferred-model")} ${button("新建 Agent", "open-create-assistant")} ${button("生成文案", "open-generate-copy")} ${button("去生产", "goto-production", "primary")}`,
`
<div class="hero-card">
<h3>Agent 概览</h3>
@@ -2490,6 +2864,31 @@ function renderPlaybookScreen() {
${models.slice(0, 6).map((model) => `<span class="chip ${model.is_default ? "active" : ""}">${escapeHtml(model.name)}</span>`).join("") || `<span class="chip active">暂无模型</span>`}
</div>
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
<h3>OneLiner Agent</h3>
<div class="panel-subtitle">前端还没上的功能由它兜底承接并调度平台 Agent</div>
</div>
<div class="task-meta">
<span class="tag blue">${escapeHtml(appState.onelinerProfile?.display_name || "OneLiner")}</span>
<span class="tag">${escapeHtml(appState.onelinerProfile?.default_platform ? platformLabel(appState.onelinerProfile.default_platform) : "未设默认平台")}</span>
<span class="tag clickable-tag" data-action="open-oneliner">打开对话</span>
</div>
</div>
<div class="task-item compact">
<h4>${escapeHtml(appState.onelinerProfile?.long_term_goal || "还没有设置长期目标")}</h4>
<p>${escapeHtml(appState.onelinerProfile?.notes || "你可以把用户长期目标、账号目标、默认平台都绑给 OneLiner再让它去调度平台 Agent。")}</p>
<div class="task-meta">
<span class="tag">会话 ${escapeHtml(formatNumber(safeArray(appState.onelinerSessions).length))}</span>
<span class="tag">平台 Agent ${escapeHtml(formatNumber(safeArray(appState.platformAgents).length))}</span>
<span class="tag clickable-tag" data-action="open-oneliner-profile">编辑配置</span>
</div>
</div>
</div>
<div style="margin-top:18px;">
${renderPlatformAgentPanel()}
</div>
<div class="panel pad" style="margin-top:18px;">
<div class="panel-head">
<div>
@@ -2801,6 +3200,7 @@ function renderAll() {
screenMap.production.innerHTML = renderProductionScreen();
screenMap.review.innerHTML = renderReviewScreen();
screenMap.credits.innerHTML = renderCreditsScreen();
renderOneLinerUi();
setScreen(screenMap[appState.screen] ? appState.screen : "dashboard");
}
@@ -3215,6 +3615,161 @@ function openUploadVideoAction() {
});
}
function openOneLinerProfileAction() {
const project = requireSelectedProject();
const assistants = getAssistantOptions(project.id);
const profile = appState.onelinerProfile || {};
openActionModal({
title: "配置 OneLiner",
description: "绑定总控主 Agent 的默认平台、长期目标和默认执行 Agent。",
submitLabel: "保存配置",
fields: [
{ name: "assistantId", label: "默认执行 Agent", type: "select", value: profile.assistant_id || getSelectedAssistant()?.id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "displayName", label: "显示名", value: profile.display_name || "OneLiner", placeholder: "例如:增长总控 OneLiner" },
{ name: "defaultPlatform", label: "默认平台", type: "select", value: normalizePlatformValue(profile.default_platform || getPreferredPlatform(), "douyin"), options: getPlatformOptions() },
{ name: "longTermGoal", label: "长期目标", type: "textarea", rows: 4, value: profile.long_term_goal || "", placeholder: "例如:围绕创业 IP 做跨平台增长与成交转化" },
{ name: "notes", label: "补充说明", type: "textarea", rows: 4, value: profile.notes || "", placeholder: "例如:前端没产品化的需求先由 OneLiner 承接,不允许直接改核心代码" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch("/v2/oneliner/profile", {
method: "PUT",
body: {
project_id: project.id,
assistant_id: values.assistantId || "",
display_name: values.displayName || "OneLiner",
default_platform: values.defaultPlatform || "douyin",
long_term_goal: values.longTermGoal || "",
notes: values.notes || "",
config: {
chat_only_for_unreleased_ui: true,
commercial_ready: true,
tenant_isolation_required: true
}
}
});
appState.onelinerProfile = saved;
rememberAction("OneLiner 已保存", `已更新 OneLiner「${saved.display_name || "OneLiner"}」配置。`, "green", saved);
renderAll();
}
});
}
function openPlatformAgentProfileAction(platform) {
const project = requireSelectedProject();
const agents = safeArray(appState.platformAgents);
const current = agents.find((item) => item.platform === platform) || {};
const assistants = getAssistantOptions(project.id);
openActionModal({
title: `配置 ${platformLabel(platform)} Agent`,
description: "给这个平台绑定自己的执行 Agent并补充任务目标和方法论定位。",
submitLabel: "保存平台 Agent",
fields: [
{ name: "assistantId", label: "绑定执行 Agent", type: "select", value: current.assistant_id || assistants[0]?.value || "", options: [{ value: "", label: "先不绑定" }, ...assistants] },
{ name: "name", label: "名称", value: current.name || `${platformLabel(platform)} Agent`, placeholder: "例如:快手增长 Agent" },
{ name: "mission", label: "任务目标", type: "textarea", rows: 4, value: current.mission || "", placeholder: "例如:沉淀快手平台的开场结构、停留逻辑和转化方法论" },
{ name: "notes", label: "补充说明", type: "textarea", rows: 4, value: current.notes || "", placeholder: "例如:优先观察短句节奏、直播切片和成交句式" },
{ name: "status", label: "状态", type: "select", value: current.status || "active", options: [{ value: "active", label: "启用" }, { value: "draft", label: "草稿" }, { value: "paused", label: "暂停" }] }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/profile`, {
method: "PUT",
body: {
project_id: project.id,
assistant_id: values.assistantId || "",
name: values.name || `${platformLabel(platform)} Agent`,
mission: values.mission || "",
notes: values.notes || "",
status: values.status || "active",
config: {
self_optimize: true,
tenant_scoped_memory: true,
ui_escalation_via_oneliner: true
}
}
});
appState.platformAgents = safeArray(appState.platformAgents).filter((item) => item.platform !== platform).concat(saved).sort((a, b) => String(a.platform).localeCompare(String(b.platform)));
rememberAction("平台 Agent 已保存", `已更新 ${platformLabel(platform)} Agent。`, "green", saved);
renderAll();
}
});
}
function openPlatformAgentMemoryAction(platform) {
const project = requireSelectedProject();
openActionModal({
title: `补充 ${platformLabel(platform)} Agent 记忆`,
description: "把当前阶段已经验证有效的平台方法、结论或注意事项沉淀为租户级长期记忆。",
submitLabel: "保存记忆",
fields: [
{ name: "memoryKey", label: "记忆键", value: "lesson.current", placeholder: "例如hook.pattern.v1" },
{ name: "title", label: "标题", placeholder: "例如:快手直播切片更吃冲突前置" },
{ name: "summary", label: "摘要", type: "textarea", rows: 4, placeholder: "写清楚这条记忆的结论和适用场景" }
],
onSubmit: async (values) => {
if (!values.memoryKey?.trim()) throw new Error("请填写记忆键");
if (!values.summary?.trim()) throw new Error("请填写记忆摘要");
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/memories`, {
method: "POST",
body: {
project_id: project.id,
memory_key: values.memoryKey.trim(),
title: values.title || values.memoryKey.trim(),
summary: values.summary.trim(),
subject_type: "project",
subject_id: project.id,
details: {
source: "manual-ui",
platform,
captured_at: new Date().toISOString()
},
confidence: 0.82
}
});
rememberAction("平台记忆已保存", `已把这条方法沉淀到 ${platformLabel(platform)} Agent 记忆中。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openPlatformAgentSkillAction(platform) {
const project = requireSelectedProject();
openActionModal({
title: `补充 ${platformLabel(platform)} Agent 技能`,
description: "把子 Agent 当前阶段验证过的方法论固化成可复用技能,并保留测试规范。",
submitLabel: "保存技能",
fields: [
{ name: "skillKey", label: "技能键", value: "skill.current", placeholder: "例如crawler.profile.dom.v2" },
{ name: "name", label: "名称", placeholder: "例如:主页结构适配技能" },
{ name: "status", label: "状态", type: "select", value: "validated", options: [{ value: "draft", label: "草稿" }, { value: "validated", label: "已验证" }, { value: "paused", label: "暂停" }] },
{ name: "method", label: "方法摘要", type: "textarea", rows: 4, placeholder: "写清楚当前方法是怎么拿到结果的" },
{ name: "testSpec", label: "验收标准", type: "textarea", rows: 4, placeholder: "例如:主页抓取成功率 >= 95%,作品标题和发布时间都齐全" }
],
onSubmit: async (values) => {
if (!values.skillKey?.trim()) throw new Error("请填写技能键");
if (!values.name?.trim()) throw new Error("请填写技能名称");
const saved = await storyforgeFetch(`/v2/platform-agents/${encodeURIComponent(platform)}/skills`, {
method: "POST",
body: {
project_id: project.id,
skill_key: values.skillKey.trim(),
name: values.name.trim(),
status: values.status || "validated",
method: { summary: values.method || "" },
test_spec: { summary: values.testSpec || "" },
last_result: { source: "manual-ui" },
success_count: 1,
failure_count: 0,
last_score: 0.9
}
});
rememberAction("平台技能已保存", `已把方法固化到 ${platformLabel(platform)} Agent 技能中。`, "green", saved);
await loadAgentControlSurfaces(project.id);
renderAll();
}
});
}
function openCreateAssistantAction() {
const project = requireSelectedProject();
const kbOptions = getKnowledgeBaseOptions(project.id);
@@ -3465,6 +4020,56 @@ function openBenchmarkLinkAction(defaults = {}) {
});
}
async function scanAdminOpsAction() {
if (!isSuperAdmin()) throw new Error("只有平台管理者才能调用运维 Agent。");
setBusy(true, "运维 Agent 正在扫描故障事件...");
try {
const payload = await storyforgeFetch("/v2/admin/ops/incidents/scan", {
method: "POST",
body: {}
});
rememberAction("运维扫描已完成", `本轮共归集 ${formatNumber(payload.count)} 条故障事件。`, payload.count ? "orange" : "green", payload);
await loadAgentControlSurfaces(getOneLinerProjectId());
} finally {
setBusy(false, "");
renderAll();
}
}
function openAdminIncidentReviewAction(incidentId) {
if (!isSuperAdmin()) {
alert("只有平台管理者才能审计处理故障事件。");
return;
}
const incident = safeArray(appState.adminOpsOverview?.incidents).find((item) => item.id === incidentId);
if (!incident) {
alert("没有找到这条故障事件。");
return;
}
openActionModal({
title: "审计处理故障事件",
description: "这里代表管理员侧审计 Agent 的放行/退回动作。",
submitLabel: "保存审计结果",
fields: [
{ name: "summary", label: "事件摘要", type: "html", html: `<div class="sheet-html"><strong>${escapeHtml(incident.title)}</strong><p>${escapeHtml(incident.summary || "暂无摘要")}</p></div>` },
{ name: "status", label: "处理状态", type: "select", value: incident.status || "reviewed", options: [{ value: "reviewed", label: "已审阅" }, { value: "watching", label: "继续观察" }, { value: "resolved", label: "已解决" }, { value: "rejected", label: "驳回修复方案" }] },
{ name: "reviewNotes", label: "审计备注", type: "textarea", rows: 5, value: incident.review_notes || "", placeholder: "写清楚为什么放行、退回或继续观察" }
],
onSubmit: async (values) => {
const saved = await storyforgeFetch(`/v2/admin/ops/incidents/${encodeURIComponent(incident.id)}`, {
method: "PATCH",
body: {
status: values.status || "reviewed",
review_notes: values.reviewNotes || ""
}
});
rememberAction("审计结果已保存", `事件「${saved.title}」已更新为 ${saved.status}`, "green", saved);
await loadAgentControlSurfaces(getOneLinerProjectId());
renderAll();
}
});
}
function openJobDetailAction(jobId) {
if (!jobId) return;
setBusy(true, "正在加载任务详情...");
@@ -3842,6 +4447,10 @@ function openReviewAction(defaults = {}) {
}
document.addEventListener("click", async (event) => {
if (event.target instanceof HTMLElement && event.target.classList.contains("oneliner-backdrop")) {
closeOneLinerPanel();
return;
}
const action = event.target.closest("[data-action]");
if (action) {
const name = action.dataset.action;
@@ -3853,6 +4462,28 @@ document.addEventListener("click", async (event) => {
closeAuthModal();
return;
}
if (name === "open-oneliner") {
try {
setBusy(true, "正在打开 OneLiner...");
if (appState.session) {
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else if (backendSupports("/v2/oneliner/sessions")) {
await ensureOneLinerSession();
}
}
openOneLinerPanel();
renderAll();
} finally {
setBusy(false, "");
}
return;
}
if (name === "close-oneliner") {
closeOneLinerPanel();
return;
}
if (name === "close-sheet") {
closeActionModal();
return;
@@ -3907,6 +4538,14 @@ document.addEventListener("click", async (event) => {
setScreen("discovery");
return;
}
if (name === "goto-intake") {
setScreen("intake");
return;
}
if (name === "goto-automation") {
setScreen("automation");
return;
}
if (name === "goto-playbook") {
setScreen("playbook");
return;
@@ -3951,6 +4590,16 @@ document.addEventListener("click", async (event) => {
openCreateAssistantAction();
return;
}
if (name === "open-oneliner-profile") {
openOneLinerProfileAction();
return;
}
if (name === "select-oneliner-session") {
appState.selectedOnelinerSessionId = action.dataset.sessionId || "";
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
renderAll();
return;
}
if (name === "select-assistant") {
appState.selectedAssistantId = action.dataset.assistantId || "";
rememberAction("已切换当前 Agent", `当前默认 Agent 已更新为「${getSelectedAssistant()?.name || "未选择"}」。`, "green");
@@ -3961,6 +4610,18 @@ document.addEventListener("click", async (event) => {
openEditAssistantAction(action.dataset.assistantId || "");
return;
}
if (name === "open-platform-agent-profile") {
openPlatformAgentProfileAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-memory") {
openPlatformAgentMemoryAction(action.dataset.platform || "");
return;
}
if (name === "open-platform-agent-skill") {
openPlatformAgentSkillAction(action.dataset.platform || "");
return;
}
if (name === "analyze-selected-account") {
openAnalyzeSelectedAccountAction();
return;
@@ -4032,6 +4693,14 @@ document.addEventListener("click", async (event) => {
openJobDetailAction(action.dataset.jobId || "");
return;
}
if (name === "scan-admin-ops") {
await scanAdminOpsAction();
return;
}
if (name === "review-admin-incident") {
openAdminIncidentReviewAction(action.dataset.incidentId || "");
return;
}
if (name === "job-to-ai-video") {
const jobId = action.dataset.jobId || "";
const detail = appState.lastJobDetail?.job?.id === jobId ? appState.lastJobDetail.job : null;
@@ -4059,13 +4728,21 @@ document.addEventListener("click", async (event) => {
}
if (name === "select-project") {
appState.selectedProjectId = action.dataset.projectId || "";
if (backendSupports("/v2/storage/status")) {
setBusy(true, "正在切换项目存储视图...");
try {
setBusy(true, "正在切换项目视图...");
try {
if (backendSupports("/v2/storage/status")) {
await loadStorageStatus(appState.selectedProjectId || "");
} finally {
setBusy(false, "");
} else {
appState.storageStatus = null;
}
await loadAgentControlSurfaces(appState.selectedProjectId || "");
if (appState.selectedOnelinerSessionId) {
await loadOneLinerMessages(appState.selectedOnelinerSessionId);
} else {
appState.onelinerMessages = [];
}
} finally {
setBusy(false, "");
}
renderAll();
return;
@@ -4103,18 +4780,38 @@ document.addEventListener("input", (event) => {
document.addEventListener("submit", async (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement) || form.dataset.role !== "auth-form") return;
event.preventDefault();
setBusy(true, "正在登录并加载...");
try {
await loginWithForm();
closeAuthModal();
await bootstrap();
} catch (error) {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = error.message;
} finally {
setBusy(false, "");
if (!(form instanceof HTMLFormElement)) return;
if (form.dataset.role === "auth-form") {
event.preventDefault();
setBusy(true, "正在登录并加载...");
try {
await loginWithForm();
closeAuthModal();
await bootstrap();
} catch (error) {
const message = document.querySelector('[data-role="auth-message"]');
if (message) message.textContent = error.message;
} finally {
setBusy(false, "");
}
return;
}
if (form.dataset.role === "oneliner-form") {
event.preventDefault();
const input = form.querySelector('[data-role="oneliner-input"]');
const value = input instanceof HTMLTextAreaElement ? input.value.trim() : "";
if (!value) return;
setBusy(true, "OneLiner 正在拆解任务...");
try {
await submitOneLinerMessage(value);
if (input instanceof HTMLTextAreaElement) input.value = "";
openOneLinerPanel();
renderAll();
} catch (error) {
alert("OneLiner 调度失败: " + error.message);
} finally {
setBusy(false, "");
}
}
});

View File

@@ -851,6 +851,173 @@ select {
gap: 12px;
}
.oneliner-fab {
position: fixed;
right: 22px;
bottom: 22px;
z-index: 52;
display: inline-flex;
align-items: center;
gap: 10px;
border: 1px solid rgba(79, 143, 238, 0.18);
border-radius: 999px;
padding: 12px 14px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(239, 247, 255, 0.98) 100%);
box-shadow: var(--shadow);
color: var(--text);
cursor: pointer;
}
.oneliner-fab-mark {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border-radius: 10px;
background: linear-gradient(180deg, var(--blue-500) 0%, var(--blue-700) 100%);
color: white;
font-size: 12px;
font-weight: 700;
}
.oneliner-fab-text {
font-size: 13px;
font-weight: 700;
color: var(--blue-700);
}
.oneliner-backdrop {
position: fixed;
inset: 0;
z-index: 51;
display: flex;
justify-content: flex-end;
align-items: stretch;
padding: 20px;
background: rgba(15, 28, 45, 0.26);
backdrop-filter: blur(8px);
}
.oneliner-backdrop.hidden {
display: none;
}
.oneliner-panel {
width: min(560px, 100%);
height: min(90vh, 980px);
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr) auto;
gap: 14px;
border-radius: 28px;
border: 1px solid var(--line-strong);
background: rgba(255, 255, 255, 0.985);
box-shadow: var(--shadow);
padding: 20px;
}
.oneliner-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 14px;
}
.oneliner-head h3 {
margin: 0 0 6px;
font-size: 24px;
}
.oneliner-head p {
margin: 0;
color: var(--muted);
font-size: 12px;
line-height: 1.5;
}
.oneliner-meta,
.oneliner-sessions {
display: grid;
gap: 8px;
}
.oneliner-messages {
min-height: 0;
overflow: auto;
display: grid;
gap: 12px;
padding-right: 4px;
}
.oneliner-message {
display: flex;
}
.oneliner-message.user {
justify-content: flex-end;
}
.oneliner-message.assistant {
justify-content: flex-start;
}
.oneliner-bubble {
width: min(100%, 460px);
padding: 14px;
border-radius: 20px;
border: 1px solid var(--line);
background: linear-gradient(180deg, #fbfdff 0%, #f5f9ff 100%);
box-shadow: var(--shadow-soft);
}
.oneliner-message.user .oneliner-bubble {
background: linear-gradient(180deg, #edf5ff 0%, #e6f0ff 100%);
border-color: rgba(79, 143, 238, 0.2);
}
.oneliner-bubble strong {
display: block;
margin-bottom: 6px;
font-size: 13px;
}
.oneliner-bubble p {
margin: 0;
color: var(--text);
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
}
.oneliner-composer {
display: grid;
gap: 10px;
padding-top: 4px;
border-top: 1px solid var(--line);
}
.oneliner-composer textarea {
width: 100%;
min-height: 108px;
resize: vertical;
border: 1px solid var(--line);
border-radius: 18px;
padding: 14px;
background: linear-gradient(180deg, #fff 0%, #f9fbff 100%);
color: var(--text);
}
.oneliner-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.oneliner-actions .helper-text {
min-height: 18px;
flex: 1 1 auto;
}
.mobile-only {
display: none;
}
@@ -1419,7 +1586,8 @@ tbody tr:hover {
}
.auth-modal-backdrop,
.action-modal-backdrop {
.action-modal-backdrop,
.oneliner-backdrop {
padding: 12px;
align-items: end;
}
@@ -1432,6 +1600,27 @@ tbody tr:hover {
padding: 18px;
}
.oneliner-panel {
width: 100%;
height: min(88vh, 100%);
border-radius: 22px;
padding: 18px;
}
.oneliner-head {
flex-direction: column;
align-items: flex-start;
}
.oneliner-actions {
flex-direction: column;
align-items: stretch;
}
.oneliner-actions .btn {
width: 100%;
}
.auth-head {
flex-direction: column;
align-items: flex-start;
@@ -1721,6 +1910,16 @@ tbody tr:hover {
display: none;
}
.oneliner-fab {
right: 14px;
bottom: 14px;
padding: 11px 12px;
}
.oneliner-fab-text {
font-size: 12px;
}
.compact-summary-row .tag {
width: calc(50% - 4px);
text-align: center;
@@ -1764,6 +1963,33 @@ tbody tr:hover {
font-size: 22px;
}
.oneliner-fab {
right: 12px;
bottom: 12px;
gap: 8px;
}
.oneliner-fab-mark {
width: 24px;
height: 24px;
border-radius: 8px;
}
.oneliner-panel {
gap: 12px;
padding: 16px;
}
.oneliner-head h3 {
font-size: 20px;
}
.oneliner-bubble {
width: 100%;
padding: 12px;
border-radius: 18px;
}
table {
min-width: 620px;
}