From 5caf7bc52e676be06c07fb789ae948870fccb6ab Mon Sep 17 00:00:00 2001 From: Yuzhiran Dev Date: Wed, 17 Jun 2026 16:13:48 +0800 Subject: [PATCH] feat: Wave 2 - API org isolation + factory.html + insights.html MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add org_id auto-population to articles.py, publishing.py, metrics.py - Add org_id filtering to search_rankings.py geo endpoints - Add org_id to calendar.py create/update/delete endpoints - Add org_id to tasks.py create endpoints - Create factory.html content pipeline page (3-tab: 待创作/进行中/待审查) - Create insights.html analytics page (4-tab: 概览/搜索排名/GEO/平台对比) --- .omo/ralph-loop.local.md | 13 - .../ses_1dede8a7affeG6DxtfPUOviXrp.json | 4 +- platform/backend/app/api/articles.py | 3 +- platform/backend/app/api/calendar.py | 14 +- platform/backend/app/api/metrics.py | 3 +- platform/backend/app/api/publishing.py | 3 +- platform/backend/app/api/search_rankings.py | 14 +- platform/backend/app/api/tasks.py | 15 +- platform/frontend/factory.html | 1097 +++++++++++++++++ platform/frontend/insights.html | 945 ++++++++++++++ platform/frontend/uni-nav.js | 17 +- 11 files changed, 2091 insertions(+), 37 deletions(-) delete mode 100644 .omo/ralph-loop.local.md create mode 100644 platform/frontend/factory.html create mode 100644 platform/frontend/insights.html diff --git a/.omo/ralph-loop.local.md b/.omo/ralph-loop.local.md deleted file mode 100644 index 8e0eb9b..0000000 --- a/.omo/ralph-loop.local.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -active: true -iteration: 1 -max_iterations: 500 -completion_promise: "DONE" -initial_completion_promise: "DONE" -started_at: "2026-06-17T06:11:18.800Z" -session_id: "ses_1dede8a7affeG6DxtfPUOviXrp" -ultrawork: true -strategy: "continue" -message_count_at_start: 8133 ---- -全部按照你的建议执行启动 diff --git a/.omo/run-continuation/ses_1dede8a7affeG6DxtfPUOviXrp.json b/.omo/run-continuation/ses_1dede8a7affeG6DxtfPUOviXrp.json index a2d4ea2..77e46da 100644 --- a/.omo/run-continuation/ses_1dede8a7affeG6DxtfPUOviXrp.json +++ b/.omo/run-continuation/ses_1dede8a7affeG6DxtfPUOviXrp.json @@ -1,10 +1,10 @@ { "sessionID": "ses_1dede8a7affeG6DxtfPUOviXrp", - "updatedAt": "2026-06-15T14:52:36.537Z", + "updatedAt": "2026-06-17T07:11:26.600Z", "sources": { "background-task": { "state": "idle", - "updatedAt": "2026-06-15T14:52:36.537Z" + "updatedAt": "2026-06-17T07:11:26.600Z" } } } \ No newline at end of file diff --git a/platform/backend/app/api/articles.py b/platform/backend/app/api/articles.py index 844e579..6068346 100644 --- a/platform/backend/app/api/articles.py +++ b/platform/backend/app/api/articles.py @@ -160,7 +160,8 @@ def update_article_content( if not article: article = Article( id=article_id, topic_id=topic_id, platform=platform, - file_path=f"db:{article_id}", status="draft" + file_path=f"db:{article_id}", status="draft", + org_id=topic.org_id ) db.add(article) article.html_content = html_content diff --git a/platform/backend/app/api/calendar.py b/platform/backend/app/api/calendar.py index ae754ad..e6e67da 100644 --- a/platform/backend/app/api/calendar.py +++ b/platform/backend/app/api/calendar.py @@ -97,7 +97,7 @@ def create_entry( if current_user.role != "admin" and topic.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="选题不存在") - entry = ContentCalendar(**data.model_dump()) + entry = ContentCalendar(**data.model_dump(), org_id=current_user.org_id or "default") db.add(entry) db.commit() db.refresh(entry) @@ -119,6 +119,13 @@ def update_entry( entry = db.query(ContentCalendar).filter(ContentCalendar.id == entry_id).first() if not entry: raise HTTPException(status_code=404, detail="日历条目不存在") + # Verify ownership via topic org_id + if entry.topic_id: + topic_check = db.query(Topic).filter(Topic.id == entry.topic_id).first() + if topic_check and current_user.role != "admin" and topic_check.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="日历条目不存在") + if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="日历条目不存在") if entry.topic_id: topic = db.query(Topic).filter(Topic.id == entry.topic_id).first() @@ -156,6 +163,8 @@ def delete_entry( topic = db.query(Topic).filter(Topic.id == entry.topic_id).first() if topic and current_user.role != "admin" and topic.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="日历条目不存在") + if not entry.topic_id and current_user.role != "admin" and entry.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="日历条目不存在") db.delete(entry) db.commit() return {"ok": True} @@ -232,7 +241,8 @@ def create_from_topic( title=topic.title, planned_date=planned_date, platform=platform, - created_by=current_user.username + created_by=current_user.username, + org_id=current_user.org_id or "default" ) db.add(entry) db.commit() diff --git a/platform/backend/app/api/metrics.py b/platform/backend/app/api/metrics.py index 199299d..f73395e 100644 --- a/platform/backend/app/api/metrics.py +++ b/platform/backend/app/api/metrics.py @@ -179,7 +179,7 @@ def create_metric( db.refresh(existing) return existing - metric = ContentMetrics(**data.model_dump(), last_fetched=datetime.now()) + metric = ContentMetrics(**data.model_dump(), last_fetched=datetime.now(), org_id=topic.org_id) db.add(metric) db.commit() db.refresh(metric) @@ -401,6 +401,7 @@ def fetch_zhihu_metrics( metric = ContentMetrics( topic_id=data.topic_id, platform=platform, + org_id=topic.org_id, **metric_data, last_fetched=datetime.now() ) diff --git a/platform/backend/app/api/publishing.py b/platform/backend/app/api/publishing.py index be13748..8ec2530 100644 --- a/platform/backend/app/api/publishing.py +++ b/platform/backend/app/api/publishing.py @@ -69,7 +69,8 @@ async def create_publish_record( action='publish', status='success', operator=operator, - description=f"选题 {req.topic_id} 发布到 {PLATFORM_LABELS.get(platform, platform)}" + description=f"选题 {req.topic_id} 发布到 {PLATFORM_LABELS.get(platform, platform)}", + org_id=topic.org_id ) db.add(record) results.append(PublishResult( diff --git a/platform/backend/app/api/search_rankings.py b/platform/backend/app/api/search_rankings.py index 56d0a69..168c5ff 100644 --- a/platform/backend/app/api/search_rankings.py +++ b/platform/backend/app/api/search_rankings.py @@ -86,7 +86,10 @@ def get_geo_overview( """GEO 概览 — AI 搜索引用 + 就绪度评分""" cutoff = datetime.now(timezone.utc) - timedelta(days=days) - base = db.query(SearchRanking).filter(SearchRanking.checked_at >= cutoff) + # Filter geo checks by org via articles + base = db.query(SearchRanking) + base = _apply_org_filter(base, current_user, db) + base = base.filter(SearchRanking.checked_at >= cutoff) total_geo = base.filter(SearchRanking.search_engine == "geo").count() total_cited = base.filter( SearchRanking.search_engine == "geo", @@ -138,7 +141,14 @@ def get_geo_readiness_scores( ): """获取 GEO 就绪度评分列表""" cutoff = datetime.now(timezone.utc) - timedelta(days=days) - scores = db.query(GeoReadinessScore).filter( + # Join via articles to filter by org + scores_query = db.query(GeoReadinessScore).join( + ArticleModel, GeoReadinessScore.article_id == ArticleModel.id, isouter=True + ) + of = org_filter(current_user, ArticleModel) + if of is not True: + scores_query = scores_query.filter(of) + scores = scores_query.filter( GeoReadinessScore.checked_at >= cutoff ).order_by(desc(GeoReadinessScore.checked_at)).limit(limit).all() return {"ok": True, "data": [s.to_dict() for s in scores]} diff --git a/platform/backend/app/api/tasks.py b/platform/backend/app/api/tasks.py index d7ea0c9..15a3af3 100644 --- a/platform/backend/app/api/tasks.py +++ b/platform/backend/app/api/tasks.py @@ -16,10 +16,13 @@ router = APIRouter(prefix="/api/tasks", tags=["tasks"]) def _check_task_org(task, current_user, db): """Check if task's topic belongs to user's org""" - if current_user.role == "admin" or not task.topic_id: + if current_user.role == "admin": return True - topic = db.query(Topic).filter(Topic.id == task.topic_id).first() - if topic and topic.org_id != current_user.org_id: + if task.topic_id: + topic = db.query(Topic).filter(Topic.id == task.topic_id).first() + if topic and topic.org_id != current_user.org_id: + raise HTTPException(status_code=404, detail="任务不存在") + elif current_user.role != "admin" and task.org_id != current_user.org_id: raise HTTPException(status_code=404, detail="任务不存在") return True @@ -87,7 +90,8 @@ def create_task( topic_id=data.topic_id, stage=data.stage, status="pending", - created_by=data.created_by or current_user.username + created_by=data.created_by or current_user.username, + org_id=current_user.org_id or "default" ) db.add(task) db.commit() @@ -438,7 +442,8 @@ def run_creator_task( stage="creator", status="running", started_at=now, - created_by=current_user.username + created_by=current_user.username, + org_id=current_user.org_id or "default" ) db.add(task) db.commit() diff --git a/platform/frontend/factory.html b/platform/frontend/factory.html new file mode 100644 index 0000000..f849fff --- /dev/null +++ b/platform/frontend/factory.html @@ -0,0 +1,1097 @@ + + + + + + 宇之然内容创作平台 - 内容工厂 + + + + + + +
+ + + +
+
+ +
+
+ 内容工厂 Content Pipeline +
+
+
+
+
1
+ 选题 +
+
+
+
2
+ 大纲 +
+
+
+
3
+ 创作 +
+
+
+
4
+ 合规 +
+
+
+
5
+ 发布 +
+
+
+
+ + +
+
+ 📋 + 待创作 + {{ readyTopics.length }} +
+
+ ⚙️ + 进行中 + {{ runningTasks.length }} +
+
+ 🔎 + 待审查 + {{ reviewArticles.length }} +
+
+ + +
+
+ + + 批量创作 ({{ selectedReadyTopics.length }}) + + + 刷新 + + + + 自动刷新中 + +
+ +
+ +
加载选题中...
+
+
+
📚
+
暂无待创作选题
+
选题采集完成后会自动出现在这里
+
+
+
+
+ {{ topic.id }} + + {{ topic.priority || '中' }} + +
+
{{ topic.title }}
+
+ {{ topic.field }} + {{ topic.format }} + + 评分 {{ topic.total_score.toFixed(1) }} + +
+
+ 📅 {{ formatDate(topic.created_at) }} + 分数: {{ topic.priority_score }} +
+
+ + + 🔧 开始创作 + +
+
+
+
+ + +
+
+ + 刷新 + + + + 每30秒自动刷新 + +
+ +
+ +
加载任务中...
+
+
+
🚀
+
暂无进行中的任务
+
点击「待创作」标签页开始创作选题
+
+
+
+
+
+ {{ task.topic_title || task.topic_id || '通用任务' }} + + {{ getStageLabel(task.stage) }} + + + {{ getStatusLabel(task.status) }} + +
+
+ {{ task.progress }}% + 取消 + {{ expandedTasks[task.task_id] ? '收起' : '展开' }} +
+
+
+ +
+
+
+ + + {{ idx + 1 }} +
+
{{ stg }}
+
+
+
+ +
+ +
+ +
+ 💬{{ task.message }} +
+ +
+ 错误: {{ task.error_msg }} +
+ +
+ ID: {{ task.task_id.slice(0,16) }} + 创建人: {{ task.created_by }} + 开始: {{ formatDate(task.started_at) }} + 耗时: {{ task.duration }}秒 +
+
+
+
+
+ + +
+
+ + 刷新 + + + + + + + +
+ +
+ +
加载文章中...
+
+
+
📝
+
暂无待审查文章
+
创作完成后文章会自动出现在这里
+
+
+
+
+ {{ platformEmoji(article.platform) }} {{ platformLabel(article.platform) }} +
+
{{ article.topic_title || article.topic_id }}
+
+ 📝 {{ article.word_count || 0 }} 字 + 📅 {{ formatDate(article.created_at) }} +
+
+ 合规分 + {{ article.compliance_score ?? '--' }} +
+
+ + 👁 审阅 + + + 文章管理 + +
+
+
+
+
+
+ + + +
+
+
+ + 合规分: {{ previewArticleData.compliance_score ?? '--' }} + + + {{ platformLabel(previewArticleData.platform) }} · {{ previewArticleData.word_count ?? '--' }} 字 + +
+
+ {{ previewFullscreen ? '退出全屏' : '全屏' }} + 关闭 +
+
+
+ +
+
暂无 HTML 内容
+
+
+
+ + + + + + + + diff --git a/platform/frontend/insights.html b/platform/frontend/insights.html new file mode 100644 index 0000000..55f592d --- /dev/null +++ b/platform/frontend/insights.html @@ -0,0 +1,945 @@ + + + + + + 宇之然内容创作平台 - 数据洞察 + + + + + + +
+ + +
+
+ +
+

数据洞察

+

内容表现 · 搜索排名 · GEO 就绪度 · 平台对比

+
+
+ {{ summary.total_published }} + 已发布 +
+
+ {{ summary.total_views }} + 总阅读 +
+
+ {{ summary.total_likes }} + 总点赞 +
+
+ {{ summary.avg_engagement }}% + 平均互动率 +
+
+
+ + + + + +
+ + + + +
{{ item.value }}
+
{{ item.label }}
+
+
+
+ + + + +
+ + 暂无数据 +
+
+
+ + + + + + + + + + + + + +
+
+
+
+ #{{ i+1 }} + {{ formatNum(t.total_views) }} 阅读 +
+
{{ t.title }}
+
+ 点赞 {{ formatNum(t.total_likes) }} + 互动率 {{ (t.engagement_pct||0).toFixed(1) }}% +
+
+
+
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
{{ m.topic_id }} · {{ platformName(m.platform) }}
+
+ 阅读 {{ formatNum(m.views) }} + 点赞 {{ formatNum(m.likes) }} + 评论 {{ formatNum(m.comments) }} +
+
+ 分享 {{ formatNum(m.shares) }} + 互动率 {{ (m.engagement_rate||0).toFixed(1) }}% + {{ formatDate(m.last_fetched) }} +
+
+
+
+
+
+ + + +
+ + + + +
+
+
{{ rankingStats.total }}
+
总记录
+
+
+
{{ rankingStats.top10 }}
+
Top 10
+
+
+
{{ rankingStats.aiCited }}
+
AI 引用
+
+
+ +
+ + 加载中... +
+
+ + 暂无排名数据 +
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
{{ r.keyword }}
+
+ 引擎: {{ engineShort(r.search_engine) }} + 日期: {{ formatDate(r.checked_at) }} +
+
+ 排名: #{{ r.position }}- + AI: {{ r.ai_cited?'是':'否' }} +
+
+
+
+
+
+ + + +
+ + + + +
{{ item.value }}
+
{{ item.label }}
+
+
+
+ + + + +
+
+ {{ factor.pass ? '✓' : '×' }} + {{ factor.label }} +
+
+
+ + + + +
+ + 加载中... +
+
+ + 暂无 GEO 评分数据 +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ {{ g.article_id || g.topic_id || '-' }} + {{ g.total_score }}/100 +
+
+
+
+
+ 平台: {{ platformName(g.platform) }} + Schema: {{ g.has_schema?'有':'无' }} + 可读性: {{ g.readability_score||0 }} + 标题: {{ g.heading_structure_score||0 }} +
+
{{ formatDate(g.checked_at) }}
+
+
+
+
+
+ + + +
+ + +
+ + 加载中... +
+
+ + 暂无平台数据 +
+
+ +
+

阅读对比

+
+
{{ platformName(p.platform) }}
+
+
+ {{ formatNum(p.total_views) }} +
+
+
+
+
+

点赞对比

+
+
{{ platformName(p.platform) }}
+
+
+ {{ formatNum(p.total_likes) }} +
+
+
+
+
+

文章数对比

+
+
{{ platformName(p.platform) }}
+
+
+ {{ p.count }} 篇 +
+
+
+
+
+
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
{{ platformName(p.platform) }}
+
+ 文章数{{ p.count }} +
+
+ 总阅读{{ formatNum(p.total_views) }} +
+
+ 平均阅读{{ Math.round(p.avg_views||0) }} +
+
+ 总点赞{{ formatNum(p.total_likes) }} +
+
+ 平均点赞{{ Math.round(p.avg_likes||0) }} +
+
+
+
+
+
+
+
+
+
+ + + + + + + + diff --git a/platform/frontend/uni-nav.js b/platform/frontend/uni-nav.js index 01238cd..8fdb2b1 100644 --- a/platform/frontend/uni-nav.js +++ b/platform/frontend/uni-nav.js @@ -121,23 +121,20 @@ function getCurrentPage() { var path = window.location.pathname; - if (path === '/index.html') return 'dashboard'; + if (path === '/index.html') return 'workspace'; var m = path.match(/\/(\w+)\.html/); - return m ? m[1] : 'dashboard'; + return m ? m[1] : 'workspace'; } function navItems(isAdmin) { var items = [ - { key: 'dashboard', label: '仪表盘', page: 'index.html' }, - { key: 'topics', label: '选题', page: 'topics.html' }, - { key: 'metrics', label: '数据', page: 'metrics.html' }, - { key: 'calendar', label: '日历', page: 'calendar.html' }, - { key: 'assets', label: '素材', page: 'assets.html' }, - { key: 'tasks', label: '任务', page: 'tasks.html' }, - + { key: 'workspace', label: '工作台', page: 'index.html' }, + { key: 'factory', label: '内容工厂', page: 'factory.html' }, + { key: 'insights', label: '数据洞察', page: 'insights.html' }, + { key: 'assets', label: '资产库', page: 'assets.html' }, ]; if (isAdmin) { - items.push({ key: 'admin', label: '系统', page: 'admin.html', admin: true }); + items.push({ key: 'admin', label: '系统管理', page: 'admin.html', admin: true }); } return items; }