diff --git a/README.md b/README.md index 256f79d..ac509ba 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ StoryForge 现在拆成独立项目目录,和 `AI-glasses` 分开维护。 - [新媒体运营中台产品逻辑手册](./docs/PRODUCT_LOGIC_NEW_MEDIA_OPERATING_SYSTEM_2026-03-22.md) - [新媒体运营平台 UI 参考包](./output/ui/new-media-ops-reference-2026-03-22/README.md) - [Web V4 UI 原型](./output/ui/storyforge-web-v4-html-prototype-2026-03-22/README.md) +- [Web V4 前端骨架](./web/storyforge-web-v4/README.md) - [Mobile V4 UI 原型](./output/ui/storyforge-mobile-v4-html-prototype-2026-03-22/README.md) ## Android diff --git a/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeScreen.kt b/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeScreen.kt index c2dd881..9811ca0 100644 --- a/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeScreen.kt +++ b/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeScreen.kt @@ -53,14 +53,19 @@ fun StoryForgeScreen( onInstallLatestUpdate: () -> Unit ) { val heroBrush = Brush.linearGradient( - colors = listOf(Color(0xFF0B3C5D), Color(0xFF1F6E5F), Color(0xFFB97524)) + colors = listOf(Color(0xFFEAF3FF), Color(0xFFD6E9FF), Color(0xFFF7FBFF)) ) Scaffold( bottomBar = { if (state.isAuthenticated && state.isApproved) { - NavigationBar(modifier = Modifier.navigationBarsPadding()) { - BottomTabItem(label = "探索", tab = StoryForgeTab.Explore, state = state, onSelect = vm::selectTab) + NavigationBar( + modifier = Modifier.navigationBarsPadding(), + containerColor = MaterialTheme.colorScheme.surface + ) { + BottomTabItem(label = "总览", tab = StoryForgeTab.Overview, state = state, onSelect = vm::selectTab) + BottomTabItem(label = "对标", tab = StoryForgeTab.Benchmark, state = state, onSelect = vm::selectTab) + BottomTabItem(label = "Agent", tab = StoryForgeTab.Agent, state = state, onSelect = vm::selectTab) BottomTabItem(label = "生产", tab = StoryForgeTab.Production, state = state, onSelect = vm::selectTab) BottomTabItem(label = "我的", tab = StoryForgeTab.Mine, state = state, onSelect = vm::selectTab) } @@ -100,13 +105,29 @@ private fun BottomTabItem( modifier = Modifier .clip(RoundedCornerShape(18.dp)) .clickable { onSelect(tab) } - .background(if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent) - .padding(horizontal = 14.dp, vertical = 10.dp), + .background( + if (selected) MaterialTheme.colorScheme.primary.copy(alpha = 0.12f) else Color.Transparent + ) + .padding(horizontal = 10.dp, vertical = 10.dp), contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = label.take(1), fontWeight = FontWeight.Bold) - Text(label, style = MaterialTheme.typography.labelSmall) + Box( + modifier = Modifier + .size(10.dp) + .clip(RoundedCornerShape(999.dp)) + .background( + if (selected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.outline.copy(alpha = 0.45f) + ) + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = label, + style = MaterialTheme.typography.labelSmall, + fontWeight = if (selected) FontWeight.Bold else FontWeight.Medium, + color = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.72f) + ) } } } @@ -127,7 +148,7 @@ private fun AuthScreen( Card( colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface), shape = RoundedCornerShape(28.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 4.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), modifier = Modifier.fillMaxWidth() ) { Column( @@ -136,15 +157,21 @@ private fun AuthScreen( .padding(22.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - Text("StoryForge AI", style = MaterialTheme.typography.headlineSmall) + Text("StoryForge", style = MaterialTheme.typography.headlineMedium) Text( - if (state.authMode == StoryForgeAuthMode.Login) "登录账号" else "注册新账号,提交后等待主管理员审批", + if (state.authMode == StoryForgeAuthMode.Login) "登录工作区,继续对标、Agent 和生产流程。" + else "先创建账号,审批通过后就能开始搭项目和 Agent。", style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.72f) ) ChoiceRow( - options = listOf("登录" to (state.authMode == StoryForgeAuthMode.Login), "注册" to (state.authMode == StoryForgeAuthMode.Register)), - onSelect = { label -> vm.setAuthMode(if (label == "登录") StoryForgeAuthMode.Login else StoryForgeAuthMode.Register) } + options = listOf( + "登录" to (state.authMode == StoryForgeAuthMode.Login), + "注册" to (state.authMode == StoryForgeAuthMode.Register) + ), + onSelect = { label -> + vm.setAuthMode(if (label == "登录") StoryForgeAuthMode.Login else StoryForgeAuthMode.Register) + } ) OutlinedTextField( value = state.username, @@ -168,7 +195,7 @@ private fun AuthScreen( if (state.busy) { CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) } else { - Text(if (state.authMode == StoryForgeAuthMode.Login) "登录" else "注册") + Text(if (state.authMode == StoryForgeAuthMode.Login) "进入工作区" else "提交注册") } } if (state.statusMessage.isNotBlank()) { @@ -197,22 +224,22 @@ private fun PendingApprovalScreen( ) { HeroCard( title = "等待审批", - subtitle = "${account?.display_name ?: account?.username ?: "当前账号"} 已登录,但尚未通过主管理员审批。", + subtitle = "${account?.display_name ?: account?.username ?: "当前账号"} 已登录,待主管理员通过后继续使用。", heroBrush = heroBrush, badges = listOf( - "审批状态:${account?.approval_status ?: "pending"}", - if (state.resolvedIp.isNotBlank()) "已解析到 ${state.resolvedIp}" else "" + "状态 ${account?.approval_status ?: "pending"}", + if (state.resolvedIp.isNotBlank()) "已解析 ${state.resolvedIp}" else "" ).filter { it.isNotBlank() } ) SectionCard(title = "当前说明", subtitle = state.statusMessage) { - Text("新注册账号在主管理员通过前,无法访问探索、生产和知识库功能。") + Text("审批通过前,项目、对标、Agent 和生产入口都会先锁定。") Spacer(modifier = Modifier.height(12.dp)) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Button(onClick = vm::refreshApprovalStatus, enabled = !state.busy) { - Text("刷新审批状态") + Text("刷新状态") } OutlinedButton(onClick = vm::logout) { - Text("退出登录") + Text("退出") } } if (state.errorMessage.isNotBlank()) { @@ -241,11 +268,19 @@ private fun AppShell( ) { HeroCard( title = when (state.currentTab) { - StoryForgeTab.Explore -> "探索素材" - StoryForgeTab.Production -> "生产文案" - StoryForgeTab.Mine -> "我的工作台" + StoryForgeTab.Overview -> "项目总览" + StoryForgeTab.Benchmark -> "找对标" + StoryForgeTab.Agent -> "Agent" + StoryForgeTab.Production -> "生产中心" + StoryForgeTab.Mine -> "我的" + }, + subtitle = when (state.currentTab) { + StoryForgeTab.Overview -> "今天先看项目状态、跟踪日报和高价值动作。" + StoryForgeTab.Benchmark -> "导入主页、作品或本地视频,让 Agent 识别并归类学习。" + StoryForgeTab.Agent -> "定义账号方向、主模型和调研目标,再生成内容。" + StoryForgeTab.Production -> "把文案、封面、实拍剪辑和 AI 视频放进同一条生产泳道。" + StoryForgeTab.Mine -> "管理账号、模型、审批、OTA 和系统状态。" }, - subtitle = state.statusMessage, heroBrush = heroBrush, badges = listOf( state.account?.display_name ?: state.account?.username.orEmpty(), @@ -255,7 +290,9 @@ private fun AppShell( ) StatusStrip(state = state, onRefresh = vm::refreshWorkspace) when (state.currentTab) { - StoryForgeTab.Explore -> ExploreTab(state = state, vm = vm, onPickVideo = onPickVideo) + StoryForgeTab.Overview -> OverviewTab(state = state, vm = vm) + StoryForgeTab.Benchmark -> BenchmarkTab(state = state, vm = vm, onPickVideo = onPickVideo) + StoryForgeTab.Agent -> AgentTab(state = state, vm = vm) StoryForgeTab.Production -> ProductionTab(state = state, vm = vm) StoryForgeTab.Mine -> MineTab(state = state, vm = vm, onInstallLatestUpdate = onInstallLatestUpdate) } @@ -267,9 +304,9 @@ private fun StatusStrip(state: StoryForgeUiState, onRefresh: () -> Unit) { SectionCard(title = "连接状态", subtitle = if (state.busy) "正在同步" else "已连接") { Text( text = if (state.originalHost.isNotBlank()) { - "外网域名已解析为 ${state.resolvedIp},请求会携带 Host=${state.originalHost}" + "当前请求会保留 Host=${state.originalHost},解析 IP=${state.resolvedIp.ifBlank { "未解析" }}" } else { - "当前使用地址:${state.baseUrl}" + "当前地址:${state.baseUrl}" }, style = MaterialTheme.typography.bodySmall ) @@ -289,224 +326,292 @@ private fun StatusStrip(state: StoryForgeUiState, onRefresh: () -> Unit) { } @Composable -private fun ExploreTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onPickVideo: () -> Unit) { - SectionCard(title = "素材入口", subtitle = "视频链接、上传视频、输入文字都会转成文本并做风格分析") { +private fun OverviewTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { + SectionCard(title = "今日概览", subtitle = "先看库存、活跃 Agent 和待处理任务。") { + StatsRow( + metrics = listOf( + "知识库" to state.knowledgeBases.size.toString(), + "Agent" to state.assistants.size.toString(), + "任务" to state.jobs.size.toString(), + "素材" to state.documents.size.toString() + ) + ) + } + + SectionCard(title = "跟踪日报", subtitle = "这里先用最近任务和时间线模拟移动端日报摘要。") { + val latest = state.jobs.take(3) + if (latest.isEmpty()) { + Text("今天还没有新的更新,先去找对标导入一个账号或作品。") + } else { + latest.forEach { job -> + MiniCard( + title = job.title, + subtitle = buildString { + append("状态 ${job.status}") + if (job.workflow_key.isNotBlank()) append(" · ${job.workflow_key}") + if (job.style_summary.isNotBlank()) append(" · ${job.style_summary.take(42)}") + } + ) + Spacer(modifier = Modifier.height(10.dp)) + } + } + } + + SectionCard(title = "今天先做什么", subtitle = "把最高频动作放到首屏。") { + ActionRow( + actions = listOf( + "找对标" to { vm.selectTab(StoryForgeTab.Benchmark) }, + "配 Agent" to { vm.selectTab(StoryForgeTab.Agent) }, + "去生产" to { vm.selectTab(StoryForgeTab.Production) }, + "看我的" to { vm.selectTab(StoryForgeTab.Mine) } + ) + ) + } + + SectionCard(title = "最近动态", subtitle = "确认最近一次导入、审批和生成结果。") { + state.timeline.take(6).forEach { item -> + Text(item, style = MaterialTheme.typography.bodySmall) + Spacer(modifier = Modifier.height(6.dp)) + } + } +} + +@Composable +private fun BenchmarkTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onPickVideo: () -> Unit) { + SectionCard(title = "导入对标", subtitle = "导入主页、视频或本地素材,再决定手动绑定还是交给 Agent 自动归类。") { ChoiceRow( options = listOf( - "账号同步" to (state.exploreInputMode == ExploreInputMode.ContentSource), - "视频链接" to (state.exploreInputMode == ExploreInputMode.VideoLink), - "上传视频" to (state.exploreInputMode == ExploreInputMode.UploadVideo), - "输入文字" to (state.exploreInputMode == ExploreInputMode.Text) + "主页" to (state.exploreInputMode == ExploreInputMode.ContentSource), + "视频" to (state.exploreInputMode == ExploreInputMode.VideoLink), + "上传" to (state.exploreInputMode == ExploreInputMode.UploadVideo), + "文本" to (state.exploreInputMode == ExploreInputMode.Text) ), onSelect = { label -> vm.setExploreInputMode( when (label) { - "账号同步" -> ExploreInputMode.ContentSource - "视频链接" -> ExploreInputMode.VideoLink - "上传视频" -> ExploreInputMode.UploadVideo + "主页" -> ExploreInputMode.ContentSource + "视频" -> ExploreInputMode.VideoLink + "上传" -> ExploreInputMode.UploadVideo else -> ExploreInputMode.Text } ) } ) Spacer(modifier = Modifier.height(12.dp)) - KnowledgeBaseSelector(state = state, onSelect = vm::selectKnowledgeBase) - Spacer(modifier = Modifier.height(12.dp)) AssistantSelector(state = state, onSelect = vm::selectAssistant) Spacer(modifier = Modifier.height(12.dp)) - Text( - text = "当前分析模型:${state.modelProfiles.firstOrNull { it.id == state.account?.preferred_analysis_model_id }?.name ?: "本机默认模型"}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) + KnowledgeBaseSelector(state = state, onSelect = vm::selectKnowledgeBase) Spacer(modifier = Modifier.height(12.dp)) - when (state.exploreInputMode) { - ExploreInputMode.ContentSource -> { - Text( - text = "适合导入抖音、B 站、小红书创作者账号主页。抖音 public 页面抓不到时,也可以把分享页链接和账号标识手工填进来。", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + BenchmarkInputPanel(state = state, vm = vm, onPickVideo = onPickVideo) + } + + SectionCard(title = "对标池", subtitle = "已经导入的任务和沉淀素材会先堆在这里。") { + if (state.jobs.isEmpty() && state.documents.isEmpty()) { + Text("先导入一个主页或作品,这里会开始形成你的学习池。") + } else { + state.jobs.take(3).forEach { job -> + MiniCard( + title = job.title, + subtitle = "${job.source_type} · ${job.status} · ${job.line_type.ifBlank { "analysis" }}" ) Spacer(modifier = Modifier.height(10.dp)) - ChoiceRow( - options = listOf( - "抖音" to (state.accountSyncPlatform == "抖音"), - "B站" to (state.accountSyncPlatform == "bilibili"), - "小红书" to (state.accountSyncPlatform == "小红书") - ), - onSelect = { label -> - vm.updateAccountSyncPlatform( - when (label) { - "B站" -> "bilibili" - else -> label - } - ) - } - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.accountSyncUrl, - onValueChange = vm::updateAccountSyncUrl, - modifier = Modifier.fillMaxWidth(), - label = { Text("账号主页或分享页链接") }, - minLines = 2 - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.accountSyncHandle, - onValueChange = vm::updateAccountSyncHandle, - modifier = Modifier.fillMaxWidth(), - label = { Text("账号标识(可选)") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.accountSyncTitle, - onValueChange = vm::updateAccountSyncTitle, - modifier = Modifier.fillMaxWidth(), - label = { Text("任务标题(可选)") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.accountSyncMaxItems, - onValueChange = vm::updateAccountSyncMaxItems, - modifier = Modifier.fillMaxWidth(), - label = { Text("最多拉取视频数(1-20)") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text("跳过已存在视频", style = MaterialTheme.typography.bodySmall) - Switch( - checked = state.accountSyncSkipExisting, - onCheckedChange = vm::setAccountSyncSkipExisting - ) - } - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text("自动触发分析", style = MaterialTheme.typography.bodySmall) - Switch( - checked = state.accountSyncAutoTriggerAnalysis, - onCheckedChange = vm::setAccountSyncAutoTriggerAnalysis - ) - } - } - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = vm::submitContentSourceSync, enabled = !state.busy) { - Text("同步账号内容") - } } - ExploreInputMode.VideoLink -> { - OutlinedTextField( - value = state.videoUrl, - onValueChange = vm::updateVideoUrl, - modifier = Modifier.fillMaxWidth(), - label = { Text("短视频链接") }, - minLines = 2 + state.documents.take(2).forEach { document -> + MiniCard( + title = document.title, + subtitle = document.style_summary.ifBlank { document.transcript_text.take(48) } ) Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.videoTitle, - onValueChange = vm::updateVideoTitle, - modifier = Modifier.fillMaxWidth(), - label = { Text("素材标题(可选)") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = vm::submitVideoLink, enabled = !state.busy) { - Text("提交视频链接") - } - } - ExploreInputMode.UploadVideo -> { - OutlinedTextField( - value = state.videoTitle, - onValueChange = vm::updateVideoTitle, - modifier = Modifier.fillMaxWidth(), - label = { Text("素材标题(可选)") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(10.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { - OutlinedButton(onClick = onPickVideo) { - Text(if (state.pickedVideoName.isBlank()) "选择视频文件" else "重新选择") - } - Text( - text = if (state.pickedVideoName.isBlank()) "未选择文件" else state.pickedVideoName, - modifier = Modifier.weight(1f), - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = vm::submitUploadVideo, enabled = !state.busy && state.pickedVideoName.isNotBlank()) { - Text("上传并开始学习") - } - } - ExploreInputMode.Text -> { - OutlinedTextField( - value = state.textTitle, - onValueChange = vm::updateTextTitle, - modifier = Modifier.fillMaxWidth(), - label = { Text("素材标题") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.textContent, - onValueChange = vm::updateTextContent, - modifier = Modifier.fillMaxWidth(), - label = { Text("素材文字") }, - minLines = 5 - ) - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = vm::submitText, enabled = !state.busy) { - Text("分析并沉淀到知识库") - } } } } - state.latestJob?.let { latestJob -> - SectionCard(title = "最新任务", subtitle = latestJob.title) { - KeyValueRow(label = "状态", value = latestJob.status) - KeyValueRow(label = "上传状态", value = latestJob.upload_status) - if (latestJob.transcript_text.isNotBlank()) { - KeyValueBlock(label = "文本转写", value = latestJob.transcript_text) + state.latestJob?.let { latest -> + SectionCard(title = "参考详情", subtitle = latest.title) { + KeyValueRow(label = "状态", value = latest.status) + KeyValueRow(label = "工作流", value = latest.workflow_key.ifBlank { latest.line_type.ifBlank { "-" } }) + if (latest.transcript_text.isNotBlank()) { + KeyValueBlock(label = "文本转写", value = latest.transcript_text) } - if (latestJob.style_summary.isNotBlank()) { - KeyValueBlock(label = "风格提炼", value = latestJob.style_summary) - } - if (latestJob.error.isNotBlank()) { - Text(latestJob.error, color = MaterialTheme.colorScheme.error) - } - } - } - - if (state.documents.isNotEmpty()) { - SectionCard(title = "当前知识库素材", subtitle = "已经沉淀到所选知识库的文本样本") { - state.documents.forEach { document -> - MiniCard(title = document.title, subtitle = document.style_summary.ifBlank { document.transcript_text.take(100) }) - Spacer(modifier = Modifier.height(10.dp)) + if (latest.style_summary.isNotBlank()) { + KeyValueBlock(label = "学习摘要", value = latest.style_summary) } } } } @Composable -private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { - SectionCard(title = "智能体列表", subtitle = "一个智能体默认关联一个知识库,也可以关联多个知识库") { +private fun BenchmarkInputPanel( + state: StoryForgeUiState, + vm: StoryForgeViewModel, + onPickVideo: () -> Unit +) { + when (state.exploreInputMode) { + ExploreInputMode.ContentSource -> { + ChoiceRow( + options = listOf( + "抖音" to (state.accountSyncPlatform == "抖音"), + "B站" to (state.accountSyncPlatform == "bilibili"), + "小红书" to (state.accountSyncPlatform == "小红书") + ), + onSelect = { label -> + vm.updateAccountSyncPlatform(if (label == "B站") "bilibili" else label) + } + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.accountSyncUrl, + onValueChange = vm::updateAccountSyncUrl, + modifier = Modifier.fillMaxWidth(), + label = { Text("主页链接或分享页") }, + minLines = 2 + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.accountSyncHandle, + onValueChange = vm::updateAccountSyncHandle, + modifier = Modifier.fillMaxWidth(), + label = { Text("账号标识,可选") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.accountSyncTitle, + onValueChange = vm::updateAccountSyncTitle, + modifier = Modifier.fillMaxWidth(), + label = { Text("导入标题,可选") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.accountSyncMaxItems, + onValueChange = vm::updateAccountSyncMaxItems, + modifier = Modifier.fillMaxWidth(), + label = { Text("最近拉取数量") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("跳过旧内容", style = MaterialTheme.typography.bodySmall) + Switch( + checked = state.accountSyncSkipExisting, + onCheckedChange = vm::setAccountSyncSkipExisting + ) + } + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text("自动分析", style = MaterialTheme.typography.bodySmall) + Switch( + checked = state.accountSyncAutoTriggerAnalysis, + onCheckedChange = vm::setAccountSyncAutoTriggerAnalysis + ) + } + } + Spacer(modifier = Modifier.height(12.dp)) + ActionRow( + actions = listOf( + "手动绑定" to vm::submitContentSourceSync, + "交给 Agent" to vm::submitContentSourceSync + ) + ) + } + + ExploreInputMode.VideoLink -> { + OutlinedTextField( + value = state.videoUrl, + onValueChange = vm::updateVideoUrl, + modifier = Modifier.fillMaxWidth(), + label = { Text("作品链接") }, + minLines = 2 + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.videoTitle, + onValueChange = vm::updateVideoTitle, + modifier = Modifier.fillMaxWidth(), + label = { Text("作品标题,可选") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(12.dp)) + ActionRow( + actions = listOf( + "手动导入" to vm::submitVideoLink, + "交给 Agent" to vm::submitVideoLink + ) + ) + } + + ExploreInputMode.UploadVideo -> { + OutlinedTextField( + value = state.videoTitle, + onValueChange = vm::updateVideoTitle, + modifier = Modifier.fillMaxWidth(), + label = { Text("素材标题,可选") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(10.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { + OutlinedButton(onClick = onPickVideo) { + Text(if (state.pickedVideoName.isBlank()) "选择视频" else "重新选择") + } + Text( + text = if (state.pickedVideoName.isBlank()) "未选择文件" else state.pickedVideoName, + modifier = Modifier.weight(1f), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + Spacer(modifier = Modifier.height(12.dp)) + ActionRow( + actions = listOf( + "手动导入" to vm::submitUploadVideo, + "交给 Agent" to vm::submitUploadVideo + ), + enabled = !state.busy && state.pickedVideoName.isNotBlank() + ) + } + + ExploreInputMode.Text -> { + OutlinedTextField( + value = state.textTitle, + onValueChange = vm::updateTextTitle, + modifier = Modifier.fillMaxWidth(), + label = { Text("标题") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + value = state.textContent, + onValueChange = vm::updateTextContent, + modifier = Modifier.fillMaxWidth(), + label = { Text("正文") }, + minLines = 5 + ) + Spacer(modifier = Modifier.height(12.dp)) + ActionRow( + actions = listOf( + "手动导入" to vm::submitText, + "交给 Agent" to vm::submitText + ) + ) + } + } +} + +@Composable +private fun AgentTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { + SectionCard(title = "Agent 列表", subtitle = "一个 Agent 可以学习多个知识库,并服务多个平台。") { ChoiceRow( options = state.assistants.map { it.name to (state.selectedAssistantId == it.id) }, onSelect = { label -> @@ -515,16 +620,16 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { ) Spacer(modifier = Modifier.height(12.dp)) OutlinedButton(onClick = vm::startNewAssistant) { - Text("新建智能体") + Text("新建 Agent") } } - SectionCard(title = "编辑智能体", subtitle = "提示词由用户提供,可随时调整模型和知识库绑定") { + SectionCard(title = "Agent 定义", subtitle = "先定义账号方向、变现方式和主模型,再决定学习哪些知识库。") { OutlinedTextField( value = state.assistantName, onValueChange = vm::updateAssistantName, modifier = Modifier.fillMaxWidth(), - label = { Text("智能体名称") }, + label = { Text("Agent 名称") }, singleLine = true ) Spacer(modifier = Modifier.height(10.dp)) @@ -532,7 +637,7 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { value = state.assistantDescription, onValueChange = vm::updateAssistantDescription, modifier = Modifier.fillMaxWidth(), - label = { Text("智能体说明") }, + label = { Text("账号方向 / 变现方式") }, minLines = 2 ) Spacer(modifier = Modifier.height(10.dp)) @@ -548,11 +653,25 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { value = state.assistantGenerationGoal, onValueChange = vm::updateAssistantGenerationGoal, modifier = Modifier.fillMaxWidth(), - label = { Text("生成目标") }, + label = { Text("Agent 目标") }, minLines = 3 ) Spacer(modifier = Modifier.height(12.dp)) - Text("选择生成模型", style = MaterialTheme.typography.titleSmall) + Text("目标平台", style = MaterialTheme.typography.titleSmall) + Spacer(modifier = Modifier.height(8.dp)) + ChoiceRow( + options = listOf( + "抖音" to state.generationPlatform.contains("抖音"), + "小红书" to state.generationPlatform.contains("小红书"), + "快手" to state.generationPlatform.contains("快手"), + "视频号" to state.generationPlatform.contains("视频号"), + "YouTube" to state.generationPlatform.contains("YouTube"), + "B站" to state.generationPlatform.contains("B站") + ), + onSelect = {} + ) + Spacer(modifier = Modifier.height(12.dp)) + Text("主模型", style = MaterialTheme.typography.titleSmall) Spacer(modifier = Modifier.height(8.dp)) ChoiceRow( options = state.modelProfiles.map { it.name to (state.assistantModelProfileId == it.id) }, @@ -561,7 +680,7 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { } ) Spacer(modifier = Modifier.height(12.dp)) - Text("选择要关联的知识库", style = MaterialTheme.typography.titleSmall) + Text("学习知识库", style = MaterialTheme.typography.titleSmall) Spacer(modifier = Modifier.height(8.dp)) ChoiceRow( options = state.knowledgeBases.map { it.name to state.selectedAssistantKnowledgeBaseIds.contains(it.id) }, @@ -571,16 +690,16 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { ) Spacer(modifier = Modifier.height(14.dp)) Button(onClick = vm::saveAssistant, enabled = !state.busy) { - Text(if (state.assistantEditorId.isNullOrBlank()) "创建智能体" else "保存智能体配置") + Text(if (state.assistantEditorId.isNullOrBlank()) "创建 Agent" else "保存 Agent") } } - SectionCard(title = "生成文案", subtitle = "选择智能体后,直接基于关联知识库输出文案") { + SectionCard(title = "调研与试跑", subtitle = "创建完 Agent 后,先跑一轮调研,再试一次文案输出。") { OutlinedTextField( value = state.generationBrief, onValueChange = vm::updateGenerationBrief, modifier = Modifier.fillMaxWidth(), - label = { Text("文案需求") }, + label = { Text("本轮调研或文案需求") }, minLines = 4 ) Spacer(modifier = Modifier.height(10.dp)) @@ -589,14 +708,14 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { value = state.generationPlatform, onValueChange = vm::updateGenerationPlatform, modifier = Modifier.weight(1f), - label = { Text("平台") }, + label = { Text("主平台") }, singleLine = true ) OutlinedTextField( value = state.generationAudience, onValueChange = vm::updateGenerationAudience, modifier = Modifier.weight(1f), - label = { Text("目标受众") }, + label = { Text("人群") }, singleLine = true ) } @@ -613,12 +732,72 @@ private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { if (state.generateBusy) { CircularProgressIndicator(modifier = Modifier.size(18.dp), strokeWidth = 2.dp) } else { - Text("开始生成") + Text("开始试跑") } } if (state.generationOutput.isNotBlank()) { Spacer(modifier = Modifier.height(16.dp)) - KeyValueBlock(label = "生成结果", value = state.generationOutput) + KeyValueBlock(label = "最近输出", value = state.generationOutput) + } + } +} + +@Composable +private fun ProductionTab(state: StoryForgeUiState, vm: StoryForgeViewModel) { + SectionCard(title = "生产泳道", subtitle = "同一页管理文案、封面、实拍剪辑和 AI 视频。") { + StatsRow( + metrics = listOf( + "文案" to if (state.generationOutput.isNotBlank()) "就绪" else "待生成", + "封面" to "待接入", + "实拍剪辑" to state.jobs.count { it.line_type == "real_cut" }.toString(), + "AI 视频" to state.jobs.count { it.line_type == "ai_video" }.toString() + ) + ) + Spacer(modifier = Modifier.height(12.dp)) + ActionRow( + actions = listOf( + "写文案" to vm::generateCopy, + "补封面" to {}, + "实拍剪辑" to {}, + "AI 视频" to {} + ), + enabled = !state.generateBusy + ) + } + + SectionCard(title = "作品与成片", subtitle = "这里承接生产完成后的作品库和当前任务。") { + state.latestJob?.let { latest -> + MiniCard( + title = latest.title, + subtitle = buildString { + append("状态 ${latest.status}") + if (latest.upload_status.isNotBlank()) append(" · 上传 ${latest.upload_status}") + if (latest.style_summary.isNotBlank()) append(" · ${latest.style_summary.take(32)}") + } + ) + Spacer(modifier = Modifier.height(10.dp)) + } + state.documents.take(3).forEach { document -> + MiniCard( + title = document.title, + subtitle = document.style_summary.ifBlank { document.transcript_text.take(54) } + ) + Spacer(modifier = Modifier.height(10.dp)) + } + if (state.latestJob == null && state.documents.isEmpty()) { + Text("还没有可看的作品,先去找对标导入,或者先创建一个 Agent。") + } + } + + SectionCard(title = "文案结果", subtitle = "先保留当前可用链路,后续把封面和视频能力一起接进来。") { + if (state.generationOutput.isBlank()) { + Text("还没有生成结果,先到 Agent 页完成一次试跑。") + } else { + KeyValueBlock(label = "文案", value = state.generationOutput) + if (state.generationPromptExcerpt.isNotBlank()) { + Spacer(modifier = Modifier.height(10.dp)) + KeyValueBlock(label = "提示词摘要", value = state.generationPromptExcerpt) + } } } } @@ -629,17 +808,17 @@ private fun MineTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onInstall KeyValueRow(label = "用户名", value = state.account?.username ?: "-") KeyValueRow(label = "角色", value = state.account?.role ?: "-") KeyValueRow(label = "审批", value = state.account?.approval_status ?: "-") - KeyValueRow(label = "Base URL", value = state.baseUrl) + KeyValueRow(label = "地址", value = state.baseUrl) if (state.resolvedIp.isNotBlank()) { KeyValueRow(label = "解析 IP", value = state.resolvedIp) } Spacer(modifier = Modifier.height(12.dp)) OutlinedButton(onClick = vm::logout) { - Text("退出登录") + Text("退出") } } - SectionCard(title = "分析模型", subtitle = "探索页默认使用这里选中的模型") { + SectionCard(title = "分析模型", subtitle = "用户不管 Key,只切主模型和默认分析模型。") { ChoiceRow( options = state.modelProfiles.map { it.name to (state.account?.preferred_analysis_model_id == it.id) }, onSelect = { label -> @@ -682,22 +861,22 @@ private fun MineTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onInstall ) Spacer(modifier = Modifier.height(12.dp)) Button(onClick = vm::createModelProfile) { - Text("保存为默认分析模型") + Text("保存默认模型") } } - SectionCard(title = "OTA 更新", subtitle = state.otaStatus.ifBlank { "检查新版本并执行安装" }) { + SectionCard(title = "OTA 更新", subtitle = state.otaStatus.ifBlank { "检查并安装最新版本。" }) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { Button(onClick = vm::checkForUpdates) { - Text("检查更新") + Text("检查") } OutlinedButton(onClick = onInstallLatestUpdate, enabled = state.otaInfo?.hasUpdate == true) { - Text("安装最新版本") + Text("安装") } } state.otaInfo?.let { ota -> Spacer(modifier = Modifier.height(12.dp)) - KeyValueRow(label = "最新版本", value = "${ota.latestVersionName} (${ota.latestVersionCode})") + KeyValueRow(label = "版本", value = "${ota.latestVersionName} (${ota.latestVersionCode})") if (ota.releaseNotes.isNotBlank()) { KeyValueBlock(label = "更新说明", value = ota.releaseNotes) } @@ -705,13 +884,18 @@ private fun MineTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onInstall } if (state.account?.role == "super_admin") { - SectionCard(title = "主管理员审批", subtitle = "新注册账号需要你审批后才能正常使用全部功能") { + SectionCard(title = "审批", subtitle = "主管理员审批新用户。") { if (state.pendingAccounts.isEmpty()) { - Text("当前没有待审批账号") + Text("当前没有待审批账号。") } else { state.pendingAccounts.forEach { account -> Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { - Column(modifier = Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { Text(account.display_name, fontWeight = FontWeight.Bold) Text(account.username, style = MaterialTheme.typography.bodySmall) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { @@ -728,62 +912,10 @@ private fun MineTab(state: StoryForgeUiState, vm: StoryForgeViewModel, onInstall } } } - - SectionCard(title = "发布 OTA", subtitle = "主管理员可直接更新在线版本号和下载地址") { - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedTextField( - value = state.publishVersionCode, - onValueChange = vm::updatePublishVersionCode, - modifier = Modifier.weight(1f), - label = { Text("VersionCode") }, - singleLine = true - ) - OutlinedTextField( - value = state.publishMinSupportedCode, - onValueChange = vm::updatePublishMinSupportedCode, - modifier = Modifier.weight(1f), - label = { Text("最低支持") }, - singleLine = true - ) - } - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.publishVersionName, - onValueChange = vm::updatePublishVersionName, - modifier = Modifier.fillMaxWidth(), - label = { Text("VersionName") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.publishApkUrl, - onValueChange = vm::updatePublishApkUrl, - modifier = Modifier.fillMaxWidth(), - label = { Text("APK 下载地址") }, - minLines = 2 - ) - Spacer(modifier = Modifier.height(10.dp)) - OutlinedTextField( - value = state.publishNotes, - onValueChange = vm::updatePublishNotes, - modifier = Modifier.fillMaxWidth(), - label = { Text("更新说明") }, - minLines = 3 - ) - Spacer(modifier = Modifier.height(12.dp)) - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Text("强制更新") - Switch(checked = state.publishForceUpdate, onCheckedChange = vm::setPublishForceUpdate) - } - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = vm::publishUpdate) { - Text("发布 OTA") - } - } } - SectionCard(title = "最近日志", subtitle = "用于确认审批、解析、任务和 OTA 状态") { - state.timeline.forEach { item -> + SectionCard(title = "最近日志", subtitle = "用来确认审批、解析、任务和 OTA 状态。") { + state.timeline.take(8).forEach { item -> Text(item, style = MaterialTheme.typography.bodySmall) Spacer(modifier = Modifier.height(6.dp)) } @@ -796,7 +928,10 @@ private fun ChoiceRow( options: List>, onSelect: (String) -> Unit ) { - FlowRow(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { options.forEach { (label, selected) -> FilterChip( selected = selected, @@ -807,6 +942,54 @@ private fun ChoiceRow( } } +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ActionRow( + actions: List Unit>>, + enabled: Boolean = true +) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + actions.forEachIndexed { index, (label, action) -> + if (index == 0) { + Button(onClick = action, enabled = enabled) { + Text(label) + } + } else { + OutlinedButton(onClick = action, enabled = enabled) { + Text(label) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun StatsRow(metrics: List>) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + metrics.forEach { (label, value) -> + Box( + modifier = Modifier + .width(140.dp) + .clip(RoundedCornerShape(18.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.55f)) + .padding(14.dp) + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(label, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.68f)) + Text(value, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + } + } + } + } +} + @Composable private fun KnowledgeBaseSelector(state: StoryForgeUiState, onSelect: (String) -> Unit) { Text("选择知识库", style = MaterialTheme.typography.titleSmall) @@ -821,7 +1004,7 @@ private fun KnowledgeBaseSelector(state: StoryForgeUiState, onSelect: (String) - @Composable private fun AssistantSelector(state: StoryForgeUiState, onSelect: (String) -> Unit) { - Text("选择关联智能体", style = MaterialTheme.typography.titleSmall) + Text("绑定 Agent", style = MaterialTheme.typography.titleSmall) Spacer(modifier = Modifier.height(8.dp)) ChoiceRow( options = state.assistants.map { it.name to (state.selectedAssistantId == it.id) }, @@ -838,11 +1021,16 @@ private fun HeroCard(title: String, subtitle: String, heroBrush: Brush, badges: .fillMaxWidth() .clip(RoundedCornerShape(28.dp)) .background(heroBrush) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), + shape = RoundedCornerShape(28.dp) + ) .padding(20.dp) ) { Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { - Text(title, style = MaterialTheme.typography.headlineLarge, color = Color.White) - Text(subtitle, style = MaterialTheme.typography.bodyLarge, color = Color(0xFFF8F5EF)) + Text(title, style = MaterialTheme.typography.headlineLarge, color = MaterialTheme.colorScheme.onSurface) + Text(subtitle, style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.74f)) if (badges.isNotEmpty()) { ChoiceRow(options = badges.map { it to true }, onSelect = {}) } @@ -868,7 +1056,7 @@ private fun SectionCard(title: String, subtitle: String, content: @Composable () Text( subtitle, style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.72f) + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.68f) ) } Spacer(modifier = Modifier.height(6.dp)) @@ -898,7 +1086,11 @@ private fun KeyValueBlock(label: String, value: String) { modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) - .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f), RoundedCornerShape(16.dp)) + .border( + 1.dp, + MaterialTheme.colorScheme.outline.copy(alpha = 0.2f), + RoundedCornerShape(16.dp) + ) .padding(14.dp) ) { Text(value) @@ -907,10 +1099,20 @@ private fun KeyValueBlock(label: String, value: String) { @Composable private fun MiniCard(title: String, subtitle: String) { - Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)) { - Column(modifier = Modifier.fillMaxWidth().padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) { + Card(colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.58f))) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(14.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { Text(title, fontWeight = FontWeight.Bold) - Text(subtitle, maxLines = 4, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall) + Text( + subtitle, + maxLines = 4, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall + ) } } } diff --git a/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeViewModel.kt b/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeViewModel.kt index ce60245..b2267bb 100644 --- a/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeViewModel.kt +++ b/android-app/app/src/main/java/com/aiglasses/app/storyforge/StoryForgeViewModel.kt @@ -15,7 +15,9 @@ import kotlinx.coroutines.launch import retrofit2.HttpException enum class StoryForgeTab { - Explore, + Overview, + Benchmark, + Agent, Production, Mine } @@ -53,7 +55,7 @@ data class StoryForgeUiState( val originalHost: String = "", val isAuthenticated: Boolean = false, val isApproved: Boolean = false, - val currentTab: StoryForgeTab = StoryForgeTab.Explore, + val currentTab: StoryForgeTab = StoryForgeTab.Overview, val busy: Boolean = false, val generateBusy: Boolean = false, val statusMessage: String = "准备连接 StoryForge", @@ -847,7 +849,7 @@ class StoryForgeViewModel(application: Application) : AndroidViewModel(applicati _state.value = state.value.copy( latestJob = job, latestJobId = job.id, - currentTab = StoryForgeTab.Explore + currentTab = StoryForgeTab.Benchmark ) refreshWorkspace() startJobPolling(job.id) diff --git a/android-app/app/src/main/java/com/aiglasses/app/ui/theme/AppTheme.kt b/android-app/app/src/main/java/com/aiglasses/app/ui/theme/AppTheme.kt index 47e1d8c..ec1302b 100644 --- a/android-app/app/src/main/java/com/aiglasses/app/ui/theme/AppTheme.kt +++ b/android-app/app/src/main/java/com/aiglasses/app/ui/theme/AppTheme.kt @@ -13,51 +13,82 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp private val LightColors = lightColorScheme( - primary = Color(0xFF0E4B43), - secondary = Color(0xFF9C6427), - tertiary = Color(0xFF2A5B8A), - background = Color(0xFFF7F3EC), - surface = Color(0xFFFFFCF8), + primary = Color(0xFF4E89F5), + secondary = Color(0xFF87AEEB), + tertiary = Color(0xFF17283A), + background = Color(0xFFF2F7FF), + surface = Color(0xFFFFFFFF), + surfaceVariant = Color(0xFFEAF2FF), onPrimary = Color.White, onSecondary = Color.White, - onBackground = Color(0xFF1A1713), - onSurface = Color(0xFF1A1713) + onBackground = Color(0xFF152332), + onSurface = Color(0xFF152332), + outline = Color(0xFFC9D8EA) ) private val DarkColors = darkColorScheme( - primary = Color(0xFF7FD6C7), - secondary = Color(0xFFFFC27A), - tertiary = Color(0xFF98C7FF), - background = Color(0xFF101714), - surface = Color(0xFF18211D), - onPrimary = Color(0xFF062D29), - onSecondary = Color(0xFF4B2B00), - onBackground = Color(0xFFF0E8DB), - onSurface = Color(0xFFF0E8DB) + primary = Color(0xFF8CB7FF), + secondary = Color(0xFF7EA5DE), + tertiary = Color(0xFFE6EEF9), + background = Color(0xFF101823), + surface = Color(0xFF162131), + surfaceVariant = Color(0xFF1D2B3D), + onPrimary = Color(0xFF0C1B30), + onSecondary = Color(0xFF0C1B30), + onBackground = Color(0xFFEAF1FB), + onSurface = Color(0xFFEAF1FB), + outline = Color(0xFF35506F) ) private val AppTypography = Typography( headlineLarge = TextStyle( - fontFamily = FontFamily.Serif, + fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Bold, - fontSize = 34.sp, - lineHeight = 40.sp + fontSize = 30.sp, + lineHeight = 36.sp + ), + headlineMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 26.sp, + lineHeight = 32.sp ), headlineSmall = TextStyle( - fontFamily = FontFamily.Serif, + fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp ), + titleLarge = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + lineHeight = 26.sp + ), bodyLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontSize = 16.sp, lineHeight = 24.sp ), + bodyMedium = TextStyle( + fontFamily = FontFamily.SansSerif, + fontSize = 14.sp, + lineHeight = 21.sp + ), + bodySmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontSize = 12.sp, + lineHeight = 18.sp + ), labelLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium, fontSize = 14.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 11.sp ) ) diff --git a/web/storyforge-web-v4/README.md b/web/storyforge-web-v4/README.md new file mode 100644 index 0000000..0faa8c7 --- /dev/null +++ b/web/storyforge-web-v4/README.md @@ -0,0 +1,30 @@ +# StoryForge Web V4 + +这是 `StoryForge` 当前面向正式前端实现的 Web 承载目录。 + +## 入口 + +- 页面:`index.html` +- 样式:`assets/styles.css` +- 页面交互:`assets/app.js` + +## 当前定位 + +- 这不是最终生产版,而是从 `Web V4` 高保真原型提升出来的真实前端骨架 +- 目录已经从 `output/ui/` 原型区独立出来,后续应直接在这里继续接真实接口 +- 当前保留的核心页面结构: + - 项目总台 + - 我的项目 + - 找对标 + - 跟踪账号 + - 自动流程 + - Agent + - 生产中心 + - 发布与复盘 + - 额度 + +## 后续建议 + +- 先补前端服务层,再接业务接口 +- 不要把这套页面重新塞回 `scripts/douyin-browser-capture/control_panel.mjs` +- 抖音采集控制台仍作为独立工具存在,这里才是正式业务应用壳 diff --git a/web/storyforge-web-v4/assets/app.js b/web/storyforge-web-v4/assets/app.js new file mode 100644 index 0000000..c2fefee --- /dev/null +++ b/web/storyforge-web-v4/assets/app.js @@ -0,0 +1,24 @@ +const navButtons = document.querySelectorAll("[data-screen-target]"); +const screens = document.querySelectorAll("[data-screen]"); + +function activateScreen(id) { + navButtons.forEach((button) => { + const active = button.dataset.screenTarget === id; + button.classList.toggle("is-active", active); + }); + + screens.forEach((screen) => { + screen.classList.toggle("is-active", screen.dataset.screen === id); + }); +} + +navButtons.forEach((button) => { + button.addEventListener("click", () => { + const next = button.dataset.screenTarget; + activateScreen(next); + window.location.hash = next; + }); +}); + +const initial = window.location.hash.replace("#", "") || "dashboard"; +activateScreen(initial); diff --git a/web/storyforge-web-v4/assets/styles.css b/web/storyforge-web-v4/assets/styles.css new file mode 100644 index 0000000..1ee93af --- /dev/null +++ b/web/storyforge-web-v4/assets/styles.css @@ -0,0 +1,865 @@ +:root { + --bg: #f4f8fd; + --bg-soft: #eef4fb; + --panel: #ffffff; + --panel-soft: #f7fbff; + --line: #d9e5f2; + --line-strong: #c8d8ea; + --text: #182433; + --muted: #66788f; + --blue-50: #f3f8ff; + --blue-100: #e8f1ff; + --blue-200: #d9e8ff; + --blue-300: #c4ddff; + --blue-500: #6aa4ff; + --blue-600: #4f8fee; + --blue-700: #3977d8; + --green: #2db584; + --orange: #f29a38; + --red: #e46767; + --shadow: 0 18px 40px rgba(67, 93, 125, 0.12); + --shadow-soft: 0 10px 24px rgba(67, 93, 125, 0.08); + --radius-xl: 24px; + --radius-lg: 18px; + --radius-md: 14px; + --radius-sm: 10px; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; + background: + radial-gradient(circle at top left, rgba(129, 180, 255, 0.18), transparent 28%), + linear-gradient(180deg, #f8fbff 0%, #eef4fb 100%); + color: var(--text); +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: 272px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + background: rgba(255, 255, 255, 0.82); + border-right: 1px solid rgba(201, 220, 239, 0.75); + backdrop-filter: blur(14px); + padding: 22px 18px 18px; + position: sticky; + top: 0; + height: 100vh; +} + +.brand { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 10px 20px; +} + +.brand-mark { + width: 42px; + height: 42px; + border-radius: 14px; + background: linear-gradient(145deg, #b9d7ff 0%, #6ea8ff 100%); + display: grid; + place-items: center; + color: white; + font-weight: 700; + letter-spacing: 0.04em; +} + +.brand h1 { + margin: 0; + font-size: 18px; +} + +.brand p { + margin: 4px 0 0; + font-size: 12px; + color: var(--muted); +} + +.nav-group { + margin-top: 14px; +} + +.nav-title { + padding: 0 10px 8px; + color: var(--muted); + font-size: 12px; + letter-spacing: 0.04em; +} + +.nav-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + border: none; + background: transparent; + border-radius: 14px; + padding: 11px 12px; + color: var(--text); + cursor: pointer; + text-align: left; + transition: 0.18s ease; +} + +.nav-item:hover { + background: rgba(106, 164, 255, 0.08); +} + +.nav-item.is-active { + background: linear-gradient(180deg, #edf5ff 0%, #e6f0ff 100%); + box-shadow: inset 0 0 0 1px rgba(106, 164, 255, 0.22); + color: var(--blue-700); +} + +.nav-item .icon { + width: 28px; + height: 28px; + border-radius: 10px; + background: var(--blue-50); + display: grid; + place-items: center; + font-size: 14px; +} + +.sidebar-foot { + margin-top: 22px; + padding: 14px; + border-radius: 18px; + background: linear-gradient(180deg, #f7fbff 0%, #eef5ff 100%); + border: 1px solid var(--line); +} + +.sidebar-foot h3 { + margin: 0 0 8px; + font-size: 14px; +} + +.sidebar-foot p { + margin: 0 0 10px; + color: var(--muted); + line-height: 1.55; + font-size: 12px; +} + +.chip-row { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.chip { + padding: 6px 10px; + border-radius: 999px; + background: var(--blue-50); + border: 1px solid var(--line); + color: var(--muted); + font-size: 12px; +} + +.chip.active { + background: var(--blue-100); + color: var(--blue-700); + border-color: rgba(79, 143, 238, 0.22); +} + +.content { + padding: 18px 22px 26px; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 18px; + padding: 16px 18px; + border-radius: 22px; + background: rgba(255, 255, 255, 0.82); + backdrop-filter: blur(12px); + border: 1px solid rgba(201, 220, 239, 0.75); + box-shadow: var(--shadow-soft); +} + +.topbar-left, +.topbar-right { + display: flex; + align-items: center; + gap: 12px; +} + +.workspace-switch, +.search, +.mini-card { + background: var(--panel); + border: 1px solid var(--line); + border-radius: 14px; +} + +.workspace-switch { + padding: 10px 14px; + min-width: 190px; +} + +.workspace-switch strong { + display: block; + font-size: 13px; +} + +.workspace-switch span { + font-size: 12px; + color: var(--muted); +} + +.search { + display: flex; + align-items: center; + gap: 10px; + min-width: 340px; + padding: 12px 14px; + color: var(--muted); +} + +.search input { + border: none; + outline: none; + background: transparent; + width: 100%; + color: var(--text); +} + +.top-pill { + padding: 8px 12px; + border-radius: 999px; + background: var(--blue-50); + color: var(--muted); + border: 1px solid var(--line); + font-size: 12px; +} + +.avatar { + width: 36px; + height: 36px; + border-radius: 12px; + background: linear-gradient(145deg, #bedcff 0%, #82b8ff 100%); + display: grid; + place-items: center; + font-size: 13px; + color: white; + font-weight: 700; +} + +.screen { + display: none; + margin-top: 18px; +} + +.screen.is-active { + display: block; +} + +.screen-head { + display: flex; + align-items: end; + justify-content: space-between; + gap: 18px; + margin-bottom: 18px; +} + +.screen-head h2 { + margin: 0 0 6px; + font-size: 28px; +} + +.screen-head p { + margin: 0; + color: var(--muted); + font-size: 13px; + line-height: 1.45; + max-width: 560px; +} + +.action-row { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.btn { + border: none; + border-radius: 12px; + padding: 10px 13px; + font-size: 13px; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: 0.18s ease; +} + +.btn-primary { + background: linear-gradient(180deg, var(--blue-500) 0%, var(--blue-600) 100%); + color: white; + box-shadow: 0 8px 18px rgba(79, 143, 238, 0.22); +} + +.btn-secondary { + background: white; + color: var(--text); + border: 1px solid var(--line); +} + +.btn:hover { + transform: translateY(-1px); +} + +.layout-grid { + display: grid; + gap: 18px; +} + +.grid-4 { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.grid-5 { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.grid-3 { + grid-template-columns: repeat(3, minmax(0, 1fr)); +} + +.grid-main { + grid-template-columns: minmax(0, 1.45fr) minmax(0, 1fr); +} + +.grid-split { + grid-template-columns: 280px minmax(0, 1fr) 310px; +} + +.panel { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(201, 220, 239, 0.9); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-soft); + overflow: hidden; +} + +.panel.pad { + padding: 17px; +} + +.panel h3, +.panel h4 { + margin: 0; +} + +.panel-subtitle { + margin-top: 6px; + color: var(--muted); + font-size: 11px; + line-height: 1.4; +} + +.stat-card { + padding: 18px; + border-radius: 20px; + background: linear-gradient(180deg, #fbfdff 0%, #f3f8ff 100%); + border: 1px solid rgba(201, 220, 239, 0.9); + box-shadow: var(--shadow); +} + +.stat-card small { + color: var(--muted); +} + +.stat-card strong { + display: block; + margin-top: 10px; + font-size: 28px; +} + +.stat-foot { + margin-top: 10px; + display: flex; + align-items: center; + justify-content: space-between; + color: var(--muted); + font-size: 12px; +} + +.positive { color: var(--green); } +.warn { color: var(--orange); } +.negative { color: var(--red); } + +.list { + display: grid; + gap: 10px; +} + +.task-item, +.entity-card, +.topic-card, +.review-card, +.queue-card { + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fff 0%, #f9fbff 100%); +} + +.task-item, +.queue-card, +.review-card { + padding: 15px; +} + +.task-item h4, +.entity-card h4, +.topic-card h4, +.queue-card h4, +.review-card h4 { + margin: 0 0 6px; + font-size: 15px; +} + +.task-item p, +.entity-card p, +.topic-card p, +.queue-card p, +.review-card p { + margin: 0; + color: var(--muted); + line-height: 1.4; + font-size: 11px; +} + +.task-meta, +.entity-meta, +.row-meta { + display: flex; + flex-wrap: wrap; + gap: 7px; + margin-top: 10px; +} + +.tag { + padding: 5px 9px; + border-radius: 999px; + background: #f6f9fe; + border: 1px solid var(--line); + color: var(--muted); + font-size: 11px; + line-height: 1.1; +} + +.row-meta .tag { + background: var(--blue-50); + border-color: rgba(106, 164, 255, 0.18); + color: var(--blue-700); + font-weight: 600; +} + +.tag.blue { + background: var(--blue-100); + color: var(--blue-700); +} + +.tag.green { + background: rgba(45, 181, 132, 0.1); + border-color: rgba(45, 181, 132, 0.18); + color: #1b8b61; +} + +.tag.orange { + background: rgba(242, 154, 56, 0.1); + border-color: rgba(242, 154, 56, 0.18); + color: #b76d16; +} + +.tag.red { + background: rgba(228, 103, 103, 0.1); + border-color: rgba(228, 103, 103, 0.18); + color: #b24c4c; +} + +.two-col { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 16px; +} + +.three-col { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; +} + +.table-wrap { + overflow: auto; +} + +table { + width: 100%; + border-collapse: collapse; + min-width: 920px; +} + +th, +td { + padding: 12px 12px; + border-bottom: 1px solid var(--line); + text-align: left; + vertical-align: top; +} + +thead th { + background: #f8fbff; + color: var(--muted); + font-weight: 600; + font-size: 12px; + letter-spacing: 0.02em; +} + +tbody tr:hover { + background: rgba(106, 164, 255, 0.055); +} + +.entity-cell { + display: flex; + gap: 12px; + align-items: start; +} + +.avatar-lg { + width: 46px; + height: 46px; + border-radius: 15px; + background: linear-gradient(145deg, #c9e2ff 0%, #8bbcff 100%); + display: grid; + place-items: center; + color: white; + font-weight: 700; +} + +.cell-title { + font-weight: 600; + margin-bottom: 4px; +} + +.cell-desc { + font-size: 12px; + color: var(--muted); + line-height: 1.5; +} + +.kpi-inline { + display: flex; + flex-wrap: wrap; + gap: 14px; + color: var(--muted); + font-size: 12px; +} + +.metric { + font-weight: 600; + color: var(--text); +} + +.toolbar { + display: flex; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding: 16px; + border-bottom: 1px solid var(--line); + background: linear-gradient(180deg, #fbfdff 0%, #f4f9ff 100%); +} + +.toolbar-stack { + display: grid; + gap: 10px; + min-width: min(760px, 100%); +} + +.search-inline { + min-width: 320px; + width: min(720px, 100%); + padding: 10px 12px; +} + +.filters { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.filter { + min-width: 132px; + padding: 10px 11px; + border-radius: 12px; + border: 1px solid var(--line); + background: white; + color: var(--muted); + font-size: 12px; +} + +.panel-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: 14px; + margin-bottom: 12px; +} + +.side-stack { + display: grid; + gap: 16px; +} + +.insight-card { + padding: 15px; + border-radius: 18px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fff 0%, #f6faff 100%); +} + +.insight-card h4 { + margin: 0 0 8px; + font-size: 15px; +} + +.insight-card ul { + margin: 0; + padding-left: 18px; + color: var(--muted); + line-height: 1.5; + font-size: 11px; +} + +.tab-row { + display: flex; + gap: 8px; + flex-wrap: wrap; + margin: 16px 0 18px; +} + +.tab { + padding: 8px 12px; + border-radius: 999px; + border: 1px solid var(--line); + background: #fff; + color: var(--muted); + font-size: 13px; +} + +.tab.active { + background: var(--blue-100); + color: var(--blue-700); + border-color: rgba(79, 143, 238, 0.2); +} + +.hero-card { + padding: 20px; + border-radius: 24px; + background: linear-gradient(145deg, rgba(212, 230, 255, 0.85) 0%, rgba(245, 250, 255, 0.96) 72%); + border: 1px solid rgba(180, 210, 248, 0.85); + box-shadow: var(--shadow-soft); +} + +.hero-card h3 { + margin: 0 0 8px; + font-size: 18px; +} + +.hero-card p { + margin: 0; + color: var(--muted); + line-height: 1.45; + font-size: 13px; +} + +.mini-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; + margin-top: 16px; +} + +.mini-card { + padding: 14px; +} + +.mini-card strong { + display: block; + margin-top: 8px; + font-size: 17px; +} + +.playbook-list { + display: grid; + gap: 12px; +} + +.playbook-item { + padding: 14px; + border-radius: 16px; + border: 1px solid var(--line); + background: linear-gradient(180deg, #fff 0%, #f8fbff 100%); + box-shadow: inset 0 1px 0 rgba(255,255,255,0.8); +} + +.playbook-item.active { + border-color: rgba(79, 143, 238, 0.24); + background: linear-gradient(180deg, #f8fbff 0%, #eef6ff 100%); +} + +.timeline { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.step { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: 999px; + background: #fff; + border: 1px solid var(--line); + color: var(--muted); + font-size: 12px; +} + +.step.done { + color: #167657; + border-color: rgba(45, 181, 132, 0.18); + background: rgba(45, 181, 132, 0.08); +} + +.step.current { + color: var(--blue-700); + border-color: rgba(79, 143, 238, 0.2); + background: var(--blue-100); +} + +.bar-chart { + display: grid; + gap: 10px; +} + +.bar-row { + display: grid; + grid-template-columns: 108px minmax(0, 1fr) 48px; + gap: 10px; + align-items: center; + font-size: 13px; + color: var(--muted); +} + +.bar-track { + height: 10px; + border-radius: 999px; + background: #eef3f8; + overflow: hidden; +} + +.bar-fill { + height: 100%; + border-radius: 999px; + background: linear-gradient(90deg, #93c3ff 0%, #5c95ef 100%); +} + +.calendar { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 10px; +} + +.day { + min-height: 118px; + border: 1px solid var(--line); + border-radius: 16px; + padding: 12px; + background: linear-gradient(180deg, #fff 0%, #f9fbff 100%); +} + +.day strong { + display: block; + margin-bottom: 8px; + font-size: 13px; +} + +.slot { + margin-top: 8px; + padding: 8px 10px; + border-radius: 12px; + background: var(--blue-50); + border: 1px solid rgba(106, 164, 255, 0.16); + font-size: 12px; + color: var(--text); + line-height: 1.5; +} + +.footer-note { + margin-top: 18px; + color: var(--muted); + font-size: 12px; + text-align: right; +} + +@media (max-width: 1320px) { + .grid-main, + .grid-split, + .grid-5, + .grid-4, + .grid-3, + .three-col, + .two-col { + grid-template-columns: 1fr; + } + .calendar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 1080px) { + .app-shell { + grid-template-columns: 1fr; + } + .sidebar { + position: relative; + height: auto; + } + .topbar { + flex-direction: column; + align-items: stretch; + } + .topbar-left, + .topbar-right { + flex-wrap: wrap; + } + .search { + min-width: 0; + } +} diff --git a/web/storyforge-web-v4/index.html b/web/storyforge-web-v4/index.html new file mode 100644 index 0000000..a48731e --- /dev/null +++ b/web/storyforge-web-v4/index.html @@ -0,0 +1,1916 @@ + + + + + + StoryForge Web V4 Prototype + + + +
+ + +
+
+
+
+ 项目:星流内容组 + 1 个已绑定账号项目 · 2 个预调研项目 +
+
+ 全平台 + 抖音 + 小红书 + B站 + YouTube +
+
+
+ + 流程 7/8 + 更新 12 + 待调研 4 +
KR
+
+
+ +
+
+
+

项目总台

+

先做最能推进项目的事。

+
+
+ + + +
+
+ +
+
+ 活跃项目 + 5 +
其中 2 个为预调研项目3 个已绑定平台账号
+
+
+ 待接入参考内容 + 14 +
近 24 小时导入8 条待 Agent 归类
+
+
+ 跟踪更新 + 12 +
自上次打开后新增7 条有借鉴点
+
+
+ 待完成首轮调研 Agent + 4 +
创建后未跑市场调研2 个变现定义不完整
+
+
+ +
+
+
+

主流程

+

项目 -> Agent -> 调研 -> 导入 -> 生产 -> 复盘

+
+ 1. 我的项目 + 2. 创建 Agent + 3. 首轮调研 + 4. 导入并绑定 + 5. 生产与封面 + 6. 发布复盘 +
+
+ +
+
+
+

今日重点动作

+
按优先级
+
+ 优先执行 4 项 +
+
+
+

为“副业增长实验室”新建预调研项目

+

先开项目,再补账号绑定。

+
+ 项目 + 预调研 + 无账号也可启动 +
+
+
+

给“教育切片增长助手”补完平台与变现定义

+

补齐平台和变现后再跑调研。

+
+ Agent 创建 + 待补字段 +
+
+
+

把 5 条新导入的抖音作品交给导入分析 Agent 自动归类

+

交给导入分析 Agent 判归属。

+
+ 导入分析 Agent + 自动绑定 +
+
+
+

补齐 2 条待生产内容的封面生成方案

+

先补封面对比,再进生产。

+
+ 封面生成 + 多模型对比 +
+
+
+
+ +
+
+
+

高分对标

+
先选要学的
+
+ 找对标 +
+
+
+
+
A
+
+
阿元创业手记
+
抖音 · 创业成长 · 强观点短视频
+
+
+
+ 涨粉快 + 高互动 + 可学习度 93 +
+
+
+
+
+
+
晨风老师
+
小红书 · 教育规划 · 图文 + 视频
+
+
+
+ 稳定更新 + 收藏高 + 商业价值 88 +
+
+
+
+
YT
+
+
Grow With Data
+
YouTube · Knowledge Shorts
+
+
+
+ 短视频强 + 系列化明显 + 结构清晰 +
+
+
+
+ +
+
+
+

跟踪日报

+
自上次打开后
+
+ 5 天汇总 +
+
+
+

秋芝2046 新增 3 条作品

+

导入分析 Agent 判断其中 2 条适合“教育切片助手”学习。

+
+ 抖音 + 已标借鉴点 +
+
+
+

晨风老师 新增 2 条图文

+

更适合补进小红书搜索承接模板,建议今天加入 Playbook。

+
+ 小红书 + 适合图文线 +
+
+
+
+
+ +
+
+

本周计划

+

先把“高信任知识切片”做稳。

+
+
+ 待生成选题 + 11 +
+
+ 待脚本 + 6 +
+
+ 待制作 + 4 +
+
+ 待复盘 + 5 +
+
+
+ +
+
+
+

高分内容

+
直接找可复制点
+
+
+
+
+

3 秒抓住家长注意力的开头怎么写

+

适合做系列开场。

+
+ 小红书 + AI 综合分 94 + 收藏率高 +
+
+
+

副业失败的 3 个真实坑

+

适合走 AI 视频线。

+
+ 抖音 + 传播热度 91 + 商业价值 87 +
+
+
+
+ +
+
+
+

异常提醒

+
先处理阻塞
+
+
+
+
+

小红书竞品同步任务延迟 48 分钟

+

建议先暂停低优先级同步。

+
+
+

2 个 Agent 学习集 7 天未更新

+

今天优先补学习集。

+
+
+
+
+
+
+ +
+
+
+

我的项目

+

先建项目,再决定是否绑定自己的账号。

+
+
+ + + +
+
+ +
+

推荐流程

+

建项目 → 建 Agent → 跑调研 → 导入 → 开始生产

+
+ 项目 + 账号接入 + Agent 创建 + 市场调研 + 导入绑定 + 生产 +
+
+ +
+
+
+
+
+

项目状态

+
预调研也能先跑
+
+
+
+
+
星流主号增长项目
+
已绑定抖音创作者平台 / 小红书专业号
+
+ 已绑定账号 + 进入分析中 +
+
+
+
副业增长实验室
+
还没绑账号,先做调研
+
+ 预调研 + 可导入参考内容 +
+
+
+
海外 Shorts 试水项目
+
已确定平台为 YouTube,尚未正式开账号,先做题材验证
+
+ 待绑定账号 + 先做调研 +
+
+
+
+ +
+
+
+

导入中心

+
导入后直接进入学习
+
+
+
+
+

导入单条作品

+
    +
  • 支持抖音 / 小红书 / B站 / 视频号 / YouTube 链接
  • +
  • 可选择“手动关联 Agent”或“自动关联最近使用 Agent”
  • +
  • 进入导入分析 Agent 队列后再分类
  • +
+
+
+

导入创作者主页

+
    +
  • 输入抖音、小红书、B站、快手、视频号、YouTube 主页链接
  • +
  • 自动建立参考账号卡片
  • +
  • 也可手动指定 Agent
  • +
+
+
+

上传本地视频

+
    +
  • 适合内部素材和竞品录屏
  • +
  • 自动做 ASR 和摘要
  • +
  • 可直接送入学习集
  • +
+
+
+
+ +
+
+
+

导入绑定策略

+
导入时定归属
+
+
+
+
+

手动关联 Agent

+

已知归属时直接指定。

+
+ 导入前选择 + 最稳妥 +
+
+
+

自动关联最近使用 Agent

+

适合连续导入同批内容。

+
+ 半自动 + 效率更高 +
+
+
+

暂不绑定,进入待归类池

+

信息不全时先放待归类。

+
+ 待归类 + 需要人工确认 +
+
+
+
+
+ +
+
+
+
+

平台账号同步

+
先绑账号再看现状
+
+
+
+
+

抖音创作者平台

+

已绑定 · 最近同步:今天 09:12 · 当前账号健康度:中上

+
+
+

小红书专业号

+

已绑定 · 最近同步:今天 08:46 · 当前图文收藏信号强于视频信号

+
+
+

视频号 / 快手 / YouTube / 哔哩哔哩

+

不绑定也能先做预调研。

+
+
+
+ +
+
+
+

最近导入队列

+
统一交给导入 Agent
+
+
+
+
+

抖音作品《副业失败的 3 个真实坑》

+

单条作品 · 已建议绑定“副业增长助手”

+
+ 自动绑定候选 +
+
+
+

抖音主页《秋芝2046》

+

主页链接 · 已建卡片,等待确认 Agent

+
+ 待人工确认 +
+
+
+
+
+
+
+ +
+
+
+

找对标

+

在这里找值得持续学习的对标账号。

+
+
+ + + +
+
+ +
+
+
+ +
+
平台:全平台
+
模式:榜单发现
+
赛道:教育 / 创业
+
地区:全国
+
粉丝量级:10w - 500w
+
变现:不限
+
+
+
+
+ 不限 + 知识付费 + 广告合作 + 带货转化 + 私域咨询 +
+
+ 涨粉 + 互动 + 商业价值 + 可学习度 + 最近活跃 +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
账号平台 / 赛道粉丝近 30 天涨粉AI 可学习度商业价值最近高分作品动作
+
+
+
+
阿元创业手记
+
强观点开头 · 案例拆解 · 行动建议
+
+
+
抖音创业成长
182.4w+28.7w9388《副业失败的 3 个坑》
查看导入绑 Agent
+
+
+
+
晨风老师
+
家长决策内容 · 图文收藏高 · 节奏稳定
+
+
+
小红书教育规划
95.2w+14.2w9186《3 秒抓住家长注意力的开头》
查看导入交给 Agent
+
+
+
+
数据见闻录
+
B站长视频切片能力强 · 系列化明显
+
+
+
B站商业认知
48.1w+6.4w8479《做二创最容易踩的 3 个坑》
查看导入加对标池
+
+ +
+
+
+
+
+

当前选中对标

+
点开后直接看详情和动作
+
+ 阿元创业手记 +
+ +
+
+
+
+

阿元创业手记

+

抖音 · 创业成长 · 最近 30 天涨粉 28.7w

+
+
+
+
AI 可学习度93
+
商业价值88
+
高分作品17
+
已提炼打法6
+
+
+ +
+ 总览 + 高分作品 + 账号画像 + 相似账号 +
+ +
+
+

账号画像

+
    +
  • 反常识切入
  • +
  • 案例推进强
  • +
  • 结尾行动建议明确
  • +
+
+
+

高分作品

+
    +
  • 《副业失败的 3 个真实坑》
  • +
  • 《为什么努力的人反而更容易做错副业》
  • +
  • 适合提炼成系列模板
  • +
+
+
+

下一步动作

+
    +
  • 导入项目
  • +
  • 创建 Agent
  • +
  • 加入 Playbook
  • +
+
+
+
+
+ +
+
+
+
+

相似对标

+
别只学一个
+
+
+
+
+

小口方法论

+

更强结论表达。

+
+
+

Grow With Data

+

更强结构和镜头节奏。

+
+
+
+
+
+
+
+ +
+
+
+

跟踪账号

+

跟踪创作者更新,系统自动汇总成日报。

+
+
+ + + +
+
+ +
+

日报逻辑

+

按上次打开后汇总更新。5 天没登录,就看 5 天汇总,再由 Agent 标借鉴点。

+
+ 按上次打开汇总 + 支持多天补报 + Agent 自动标借鉴点 +
+
+ +
+
+
+
+
+

跟踪列表

+
手动选择长期跟踪
+
+ 共 18 个 +
+
+
+

秋芝2046

+

抖音 · 关联 Agent:教育切片助手 · 最近更新:今天 08:14

+
+ 自动跟踪中 + 日报已生成 +
+
+
+

晨风老师

+

小红书 · 关联 Agent:图文增长助手 · 最近更新:昨天 21:40

+
+ 自动跟踪中 + 2 条新内容 +
+
+
+

Grow With Data

+

YouTube · 关联 Agent:海外 Shorts 助手 · 最近更新:3 天前

+
+ 等待下次抓取 +
+
+
+
+ +
+
+
+

本次汇总范围

+
不是自然日,而是上次打开后
+
+
+
+
+

上次打开

+
    +
  • 2026/03/17 19:42
  • +
  • 距今 5 天
  • +
+
+
+

本次汇总

+
    +
  • 新增内容 12 条
  • +
  • 涉及 5 个跟踪账号
  • +
+
+
+

Agent 标注

+
    +
  • 7 条有借鉴点
  • +
  • 3 条建议入学习集
  • +
+
+
+
+
+ +
+
+
+
+

更新日报

+
打开平台先看这组更新
+
+ 汇总 5 天 +
+
+
+

秋芝2046 · 《家长最容易忽略的一步》

+

教育切片助手判断:开头“错误认知”很强,适合借去做你的下一条 30 秒口播。

+
+ 有借鉴点 + 建议加入学习集 +
+
+
+

晨风老师 · 《志愿填报前先别急着选学校》

+

图文增长助手判断:标题结构和收藏导向很强,适合转成图文选题。

+
+ 适合图文线 + 建议加入 Playbook +
+
+
+

Grow With Data · 《3 hooks for Shorts》

+

海外 Shorts 助手判断:更适合补充节奏和结构,不建议直接照搬表达。

+
+ 结构可借 + 表达不直接照搬 +
+
+
+
+ +
+
+
+

自动流程规则

+
把跟踪变成日常动作
+
+
+
+
+

抓取触发

+

账号更新即入队;用户再次打开平台时补齐未读期间的全部更新。

+
+
+

日报生成

+

按账号聚合,再按 Agent 给出借鉴点、风险点和建议动作。

+
+
+

结果回写

+

高价值更新可一键加入 Agent 学习集、Playbook 或生产中心作品区。

+
+
+
+
+
+
+ +
+
+
+

自动流程

+

把同步、日报、补跑和提醒交给系统。

+
+
+ + + +
+
+ +
+
+
+
+

自动流程

+

负责自动同步、生成日报、补跑失败任务和提醒异常。

+
+
+
+
自动同步7
+
日报生成3
+
失败重跑2
+
异常提醒4
+
+
+ +
+
+
+
+
+

常用自动流程

+
适合后台持续运行
+
+
+
+
+

账号同步

+
    +
  • 同步自运营账号数据
  • +
  • 更新项目现状
  • +
  • 补充账号诊断
  • +
+
+
+

跟踪日报

+
    +
  • 抓取跟踪账号更新
  • +
  • 按上次打开后汇总
  • +
  • 由 Agent 标借鉴点
  • +
+
+
+

失败补跑

+
    +
  • 剪辑失败自动重试
  • +
  • AI 视频失败补跑
  • +
  • 超时任务自动提醒
  • +
+
+
+
+ +
+
+
+

当前流程列表

+
按计划持续执行
+
+
+
+
+

每天 08:00 同步自运营账号

+

刷新抖音 / 小红书 / 视频号项目数据。

+
+ 运行正常 +
+
+
+

每次登录前生成跟踪日报

+

汇总用户上次打开后的全部更新,并由 Agent 给借鉴点。

+
+ 高频使用 +
+
+
+
+
+ +
+
+
+
+

为什么需要它

+
把重复动作交给系统
+
+
+
+
+

减少重复手工操作

+

不用每天手动点同步、点抓取、点汇总。

+
+
+

让更新更快变成学习资产

+

抓到新内容后,直接由 Agent 标注借鉴点和建议动作。

+
+
+

把异常收口到后台

+

失败补跑、超时提醒、额度预警都放在这里看。

+
+
+
+
+
+
+ +
+
+
+

我的账号

+

这里看自己账号的现状和下一步动作。

+
+
+ + + +
+
+ +
+
+
+
+

你自己的知识成长号

+

主平台:抖音 / 小红书 · 当前目标:做稳“高信任知识切片”栏目。

+
+
+
+
本周发布目标14 条
+
在产内容6 条
+
待脚本4 条
+
待复盘5 条
+
+
+ +
+ 总览 + 内容计划 + 候选选题 + 在产内容 + 已发布 + 复盘 +
+ +
+
+
+
+
+

本周策略卡

+
先看本周重点
+
+
+
+

阶段策略

+
    +
  • 先把“副业避坑”做成 7 条系列内容,形成稳定栏目。
  • +
  • 内容结构优先借参考账号的强观点开头,但案例改成你自己的客户场景。
  • +
  • 本周实拍剪辑和 AI 视频各做 2 条,验证哪条转化更高。
  • +
+
+
+ +
+
+
+

下一批建议选题

+
Agent 输出直接接业务目标
+
+
+
+
+

为什么越努力的人越容易做错副业

+

适合先走短视频观点切片。

+
+ 钩子强 + 预估价值高 + 待脚本 +
+
+
+

普通家长最容易忽略的教育规划错误

+

适合小红书图文 + 抖音口播双测。

+
+ 跨平台 + 收藏潜力高 +
+
+
+
+
+ +
+
+
+
+

已绑定 Playbook

+
这里是 Agent 的真实学习源
+
+
+
+
+

强观点短视频开头模板

+

来源:抖音创业类高分账号 14 条作品

+
+
+

高信任教育图文结构模板

+

来源:小红书教育规划账号 9 条高收藏图文

+
+
+
+
+
+
+

已绑定 Agent

+
看产出,不看技术配置
+
+
+
+
+

选题助手-教育切片

+

最近产出:4 个候选选题,2 个已进入脚本阶段。

+
+
+

分镜助手-副业避坑

+

最近产出:2 条 AI 视频分镜,1 条进入生产。

+
+
+
+
+
+
+ +
+
+
+

Agent

+

项目之后先建 Agent。

+
+
+ + + +
+
+ +
+

创建前置

+

先定平台、变现和主模型,再跑首轮调研。

+
+ 先选项目 + 定义账号类型 + 选择平台 + 选择变现 + 设置主模型 + 启动调研 +
+
+ +
+
+
+
+

策略资产与 Playbook

+
打法资产服务 Agent
+
+
+
+
+

强观点短视频开头模板

+

抖音 / YouTube Shorts · 反常识开头 · 高评论

+
+
+

高信任教育图文结构模板

+

小红书 · 高收藏 · 适合家长决策内容

+
+
+

案例拆解型长视频切片模板

+

B站 / YouTube · 适合做知识型切片

+
+
+
+ +
+
+
+

Agent 创建器

+
先定目标,再调研
+
+
+
+
+

账号类型

+
    +
  • 知识 IP / 教育型账号
  • +
  • 品牌型账号 / 带货账号
  • +
  • 个人成长 / 生活方式 / 案例拆解账号
  • +
+
+
+

变现方式

+
    +
  • 知识付费
  • +
  • 广告接单 / 品牌合作
  • +
  • 带货转化 / 私域咨询 / 会员订阅
  • +
+
+
+
+
+

覆盖平台

+
    +
  • 抖音 / 小红书 / 快手
  • +
  • 微信视频号 / YouTube / 哔哩哔哩
  • +
  • 按勾选平台先跑市场调研
  • +
+
+
+

默认主大模型

+
    +
  • 设置 1 个主模型作为标准输出
  • +
  • 可同时启用多个对比模型
  • +
  • 用户不管理 API Key,只选择模型
  • +
+
+
+

首轮动作

+
    +
  • 先做市场调研
  • +
  • 再吸收导入的主页与作品
  • +
  • 最后进入文案和内容计划生成
  • +
+
+
+ +
+ 抖音 + 小红书 + 快手 + 微信视频号 + YouTube + 哔哩哔哩 +
+ +
+ 知识付费 + 广告合作 + 带货转化 + 私域咨询 + 会员订阅 +
+ +
+
+

文案模型

+

主模型:通义千问 Max,可开对比模型。

+
+ 主模型 + 多模型对比 +
+
+
+

封面模型

+

支持阿里、火山和统一图像模型。

+
+ 封面生成前置 +
+
+
+

视频模型

+

脚本、分镜、封面确认后再进视频队列。

+
+ 后台统一管理 Key +
+
+
+
+ +
+
+
+

已创建 Agent

+
导入分析也交给 Agent
+
+
+
+
+

选题助手-教育切片

+

学习源:高信任图文 + 强观点短视频 · 平台:抖音 / 小红书 / 视频号 · 变现:知识付费 + 私域咨询

+
+ 最近产出 6 条 + 主模型:通义千问 + 首轮调研已完成 +
+
+
+

分镜助手-副业避坑

+

学习源:反常识开头 + 失败案例 Playbook · 平台:抖音 / YouTube Shorts · 变现:广告合作

+
+ 待补学习集 + 主模型:豆包 + 最近产出 2 条 +
+
+
+

导入分析 Agent

+

负责解析主页、单条作品和本地视频,自动给出分类和绑定建议。

+
+ 替代规则判断 + 支持复核 +
+
+
+
+
+
+ +
+
+
+

生产中心

+

文案、封面、剪辑、AI 视频都在这里。

+
+
+ + + + +
+
+ +
+
+
+

生产队列

+
统一看进度
+
+
+
+ 已选题 + 文案生成 + 封面生成 + 分镜 / 素材 + 生产中 + 待审核 + 待发布 +
+
+
+

文案生成

+

《孩子为什么总在临门一脚放弃》

+
+ 主模型:通义 + 对比 2 个模型 +
+
+
+

封面生成

+

《副业避坑:别把努力浪费在错方向》

+
+ 阿里 / 火山 / 通用图像 + 待最终选图 +
+
+
+

实拍剪辑

+

《教育规划里最容易拖延的一步》

+
+ 来源:自营账号 + ETA 2h +
+
+
+

AI 视频

+

《副业避坑:别把努力浪费在错方向》

+
+ 来源:Playbook + 待审核 +
+
+
+
+ +
+
+
+
+
+

本周生产看板

+
先看卡点
+
+
+
+
+

AI 视频试投版《低预算露营也能拍出高级感》

+

卡在封面确认,今天 17:30 前可进待审核。

+
+ AI 视频 + 卡在封面 +
+
+
+

《副业避坑》实拍快剪版

+

已回片,等人工看样后排期。

+
+ 待审核 + 实拍剪辑 +
+
+
+

《办公室反差感穿搭》图文版

+

文案已完成,还要补两组封面版本。

+
+ 图文制作 + 待补封面 +
+
+
+
+ +
+
+
+

生产瓶颈

+
直接看阻塞
+
+
+
+
+ 封面确认 +
+ 4 条 +
+
+ 人工审核 +
+ 3 条 +
+
+ 排期决策 +
+ 5 条 +
+
+
+
+

当前最容易拖慢效率的是“封面确认”

+

把封面确认单独拆出来,并支持多模型 AB 对比。

+
+ 流程优化建议 +
+
+
+
+
+ +
+
+
+
+

流程运行与异常

+
先看哪里要人工兜底
+
+
+
+
+

实拍剪辑线

+

Windows cutvideo 调度正常,最近 24 小时成功 6 / 6。

+
+
+

AI 视频线

+

本周 2 条任务等待凭证刷新,建议保留重试入口。

+
+
+

封面多模型协作

+

3 条任务都卡在首图方案,建议加封面优先队列。

+
+
+
+
+
+ +
+
+
+
+

作品与成片

+
生产中和已沉淀内容都在这里
+
+
+
+
平台:全平台
+
类型:视频 / 图文 / AI 视频 / 实拍
+
来源:对标导入 / 我的账号 / 生产任务
+
状态:生产中 / 待审核 / 已发布
+
时间:近 30 天
+
排序:综合分 / 热度 / 商业价值 / 发布时间
+
+
+
+

快捷动作

+
+
+
+ 高分 + 最新 + 补封面 + 加 Playbook +
+
+ +
+
+
+

当前内容列表

+
从结果反看最值得推进的内容
+
+
+ 高分内容 + 最新产出 + 全部内容 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
内容来源平台当前阶段发布时间综合分热度商业价值
+
3 秒抓住家长注意力的开头怎么写
+
图文 · 教育规划 · 高收藏
+
晨风老师 / 对标导入小红书已进入文案2026/03/19949185
+
副业失败的 3 个真实坑
+
视频 · 创业成长 · 高评论
+
阿元创业手记 / 对标导入抖音待封面确认2026/03/18949387
+
做二创最容易踩的 3 个坑
+
长视频切片 · 结构清晰
+
数据见闻录 / 对标导入B站已入 Playbook2026/03/17887976
+
+
+ +
+
+
+

当前选中内容

+
当前选中:副业失败的 3 个真实坑
+
+
+
+
+

AI 摘要

+
    +
  • 高互动原因在于“反常识 + 具体案例 + 明确建议”的组合。
  • +
  • 节奏短促,适合口播或 AI 视频切片。
  • +
+
+
+

可借鉴点

+
    +
  • 开头直接否定常见认知
  • +
  • 中段按 1 / 2 / 3 结构推进
  • +
  • 结尾给出可执行动作
  • +
+
+
+

下一步建议

+
    +
  • 优先送入“副业增长助手”继续生成脚本
  • +
  • 封面先跑 3 个模型做对比
  • +
  • 成片进入待审核后再决定走实拍还是 AI 视频
  • +
+
+
+

风险提醒

+
    +
  • 如果过度强化“失败”情绪,容易降低信任感。
  • +
  • 商业承接不能直接硬转化,需要加过渡案例。
  • +
+
+
+ 继续做 + 补封面 + 加 Playbook + 看成片 +
+
+
+
+ +
+ +
+
+
+

发布与复盘

+

在这里看发布结果和下一轮动作。

+
+
+ + + +
+
+ +
+
+
+
+
+

发布排期

+
先保栏目节奏
+
+
+
+
周一
抖音:副业避坑短视频
19:30
+
周二
小红书:教育图文
12:00
+
周三
抖音:AI 视频版
20:00
+
周四
B站:长视频切片
18:30
+
周五
抖音:栏目连载 03
20:00
+
周六
小红书:案例图文
13:00
+
周日
YouTube Shorts:A/B 版
21:00
+
+
+ +
+
+
+

复盘对比

+
同主题不同生产方式对比
+
+
+
+
+ 实拍剪辑版 +
+ 82 +
+
+ AI 视频版 +
+ 68 +
+
+
+
+

结论:建议继续做实拍剪辑版

+

下周优先做实拍剪辑版,AI 视频继续低成本测试。

+
+ 建议继续 + 可变体复刻 +
+
+
+
+
+ +
+
+
+
+

最新复盘卡

+
把结果写回策略和学习集
+
+
+
+
+

《副业失败的 3 个真实坑》

+

播放 128w · 点赞 8.9w · 评论 1.9w · 收藏 2.1w · 咨询转化 3.2%

+
+ 建议继续 + 适合系列化 +
+
+
+

《教育规划里最容易忽略的一步》

+

更适合做图文矩阵延展。

+
+ 适合图文扩展 + 建议补系列 +
+
+
+
+ +
+
+
+

下一轮动作

+
复盘后进入下一轮
+
+
+
+
+

更新 Playbook

+

把“评论区问题前置”写回模板。

+
+
+

更新 Agent 学习源

+

把 2 条高表现内容写回 Agent 学习集。

+
+
+

安排下一轮测试

+

同主题保留实拍主线,AI 视频继续低成本验证。

+
+
+
+
+
+ +
+ +
+
+
+

额度

+

用户只看可用额度,不看底层密钥。

+
+
+ + + +
+
+ +
+
+ 文案积分 + 12,640 +
本周消耗 1,120主模型 + 对比模型共用
+
+
+ 封面积分 + 4,320 +
本周消耗 680多模型对比消耗更快
+
+
+ 视频积分 + 1,180 +
本周消耗 290需控制试投数量
+
+
+ 当前计费策略 + Phase 1 +
按额度池表达后续再细化成本模型
+
+
+ +
+
+
+
+
+

额度池逻辑

+
先让用户看懂自己还能做什么
+
+
+
+
+

文案积分

+
    +
  • 覆盖选题、文案、脚本、复盘建议
  • +
  • 支持 1 个主模型 + 多个对比模型
  • +
  • 适合高频日常使用
  • +
+
+
+

封面积分

+
    +
  • 覆盖封面主图、首图、首尾帧辅助图
  • +
  • 支持阿里、火山和统一图像模型
  • +
  • 适合多版本 AB 测试
  • +
+
+
+

视频积分

+
    +
  • 覆盖 AI 视频生成与重试
  • +
  • 适合关键选题的低成本试投
  • +
  • 建议和封面额度联动观察
  • +
+
+
+
+ +
+
+
+

项目消耗排行

+
先看哪个项目最吃额度
+
+
+
+
+

星流主号增长项目

+

文案积分 420 / 封面积分 110 / 视频积分 68。主要消耗在抖音 + 小红书双平台连续出稿。

+
+
+

副业增长实验室

+

文案积分 265 / 封面积分 182 / 视频积分 41。主要消耗在高分参考内容导入后的封面多版本测试。

+
+
+

海外 Shorts 试水项目

+

视频积分占比偏高,建议先多做文案和封面验证,再放大视频试投。

+
+
+
+
+ +
+
+
+
+

额度产品化建议

+
先用简单表达上线
+
+
+
+
+

第一阶段:三类额度池

+

先统一成文案 / 封面 / 视频三类额度池。

+
+ 可快速上线 +
+
+
+

第二阶段:项目包 + 增值包

+

后续再加项目包、试投包、封面增强包。

+
+ 后续迭代 +
+
+
+

后台托管密钥

+

密钥统一后台托管,用户只选模型和看额度。

+
+ 降低使用门槛 +
+
+
+
+
+
+ +
+
+
+ + + +