Initial commit: yu-zhi-ran platform with automation integration

This commit is contained in:
lt
2026-04-19 14:05:09 +08:00
commit 3cb2df51c8
209 changed files with 80379 additions and 0 deletions
+573
View File
@@ -0,0 +1,573 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>宇之然内容创作平台</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="/static/vue.global.prod.js"></script>
<link rel="stylesheet" href="/static/element-plus.css" />
<script src="/static/element-plus.full.js"></script>
<style>
.page { padding: 20px; max-width: 1200px; margin: 0 auto; }
.card { background: white; border-radius: 8px; padding: 20px; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.stat-card { text-align: center; }
.stat-value { font-size: 2rem; font-weight: bold; color: #409EFF; }
</style>
</head>
<body>
<div id="app">
<nav class="bg-blue-600 text-white p-4 mb-6">
<div class="container mx-auto flex justify-between items-center">
<h1 class="text-2xl font-bold">宇之然内容创作平台</h1>
<div class="flex gap-2">
<el-button type="primary" @click="refresh">刷新</el-button>
</div>
</div>
</nav>
<div class="page">
<div class="card">
<h2 class="text-xl font-bold mb-4">系统概览</h2>
<div class="grid grid-cols-4 gap-4">
<div class="card stat-card">
<div class="stat-value">{{ status.total_topics }}</div>
<div>选题总数</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ (status.topics_by_status || {})['待发布'] || 0 }}</div>
<div>待发布</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ (status.topics_by_status || {})['待处理'] || 0 }}</div>
<div>待处理</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ status.today_articles }}</div>
<div>今日生成</div>
</div>
</div>
<div class="mt-4 flex gap-4">
<el-button type="success" @click="triggerGenerate" :loading="generating">▶ 运行创作任务</el-button>
<el-button type="warning" @click="triggerOptimize" :loading="optimizing">🔍 运行合规优化</el-button>
<el-button type="info" @click="showLogs = true">📄 查看日志</el-button>
</div>
</div>
<!-- 流水线状态 -->
<div class="card">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">📊 流水线状态</h2>
<el-button size="small" @click="refreshPipeline">刷新</el-button>
</div>
<div v-if="pipelineLoading" class="text-gray-500">加载中...</div>
<div v-else class="grid grid-cols-4 gap-4">
<div class="card stat-card">
<div class="stat-value">{{ pipeline.status_distribution?.['待处理'] || 0 }}</div>
<div>待处理</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ pipeline.status_distribution?.['待发布'] || 0 }}</div>
<div>待发布</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ pipeline.status_distribution?.['已发布'] || 0 }}</div>
<div>已发布</div>
</div>
<div class="card stat-card">
<div class="stat-value">{{ pipeline.topics_count || 0 }}</div>
<div>总选题数</div>
</div>
</div>
<div class="mt-4">
<h3 class="text-lg font-semibold mb-2">模块状态</h3>
<el-table :data="pipelineModules" border style="width: 100%">
<el-table-column prop="module" label="模块" width="120"></el-table-column>
<el-table-column prop="last_run" label="最后运行" width="180"></el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="scope">
<el-tag :type="scope.row.status_ok ? 'success' : 'danger'">{{ scope.row.status_text }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="error" label="错误信息"></el-table-column>
</el-table>
</div>
</div>
<div class="card">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold">选题管理</h2>
<div class="flex gap-4">
<el-button type="primary" size="small" @click="refreshAll">🔄 全量刷新</el-button>
<el-button type="success" size="small" @click="openCreateTopic">+ 新建选题</el-button>
</div>
</div>
<div class="flex items-center gap-4 mb-4">
<el-select v-model="filterStatus" placeholder="筛选状态" clearable style="width: 150px">
<el-option label="全部" value=""></el-option>
<el-option label="待处理" value="待处理"></el-option>
<el-option label="待审查" value="待审查"></el-option>
<el-option label="待发布" value="待发布"></el-option>
<el-option label="已发布" value="已发布"></el-option>
</el-select>
<span class="text-gray-500">共 {{ topics.length }} 条</span>
</div>
<div>
<el-select v-model="filterStatus" placeholder="筛选状态" clearable style="width: 150px">
<el-option label="全部" value=""></el-option>
<el-option label="待处理" value="待处理"></el-option>
<el-option label="待审查" value="待审查"></el-option>
<el-option label="待发布" value="待发布"></el-option>
<el-option label="已发布" value="已发布"></el-option>
</el-select>
</div>
</div>
<el-table :data="filteredTopics" stripe>
<el-table-column prop="id" label="ID" width="80" header-align="center"></el-table-column>
<el-table-column prop="title" label="标题" width="220" header-align="center"></el-table-column>
<el-table-column prop="field" label="领域" width="90" header-align="center"></el-table-column>
<el-table-column prop="priority_score" label="优先级" width="70" header-align="center"></el-table-column>
<el-table-column prop="status" label="状态" width="90" header-align="center">
<template #default="scope">
<el-tag :type="statusTagType(scope.row.status)">{{ scope.row.status }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="compliance_score" label="合规分" width="70" header-align="center"></el-table-column>
<el-table-column label="创建时间" width="120" header-align="center">
<template #default="scope">{{ formatDate(scope.row.created_at) }}</template>
</el-table-column>
<el-table-column label="就绪时间" width="120" header-align="center">
<template #default="scope">{{ formatDate(scope.row.ready_at) }}</template>
</el-table-column>
<el-table-column label="发布时间" width="120" header-align="center">
<template #default="scope">{{ formatDate(scope.row.published_at) }}</template>
</el-table-column>
<el-table-column label="操作" width="320" header-align="center">
<template #default="scope">
<div class="flex gap-2 mb-1">
<el-button size="small" @click="openPreview(scope.row)">预览</el-button>
<el-button size="small" type="primary" :disabled="scope.row.status !== '待发布'" @click="openPublish(scope.row)">发布</el-button>
</div>
<div class="flex gap-2">
<el-button size="small" type="success" :disabled="scope.row.status !== '待处理'" @click="createTopic(scope.row)">创作</el-button>
<el-button size="small" type="warning" :disabled="scope.row.status !== '待审查'" @click="optimizeTopic(scope.row)">审查</el-button>
</div>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 预览对话框 -->
<el-dialog v-model="previewVisible" :title="previewTopic.title" width="80%">
<div class="mb-4">
<el-radio-group v-model="previewPlatform" size="small">
<el-radio-button label="zhihu">知乎</el-radio-button>
<el-radio-button label="wechat">微信公众号</el-radio-button>
<el-radio-button label="xiaohongshu">小红书</el-radio-button>
</el-radio-group>
</div>
<div v-if="previewHtml" class="border p-4 bg-gray-50" v-html="previewHtml" style="max-height: 70vh; overflow: auto;"></div>
</el-dialog>
<!-- 发布管理对话框(增强版) -->
<el-dialog v-model="publishVisible" :title="`发布管理:${publishTopic.title}`" width="900px">
<el-tabs v-model="publishTab">
<el-tab-pane label="发布链接" name="links">
<el-form :model="publishForm" label-width="100px">
<el-form-item label="知乎">
<el-input v-model="publishForm.zhihu" placeholder="https://zhihu.com/..."></el-input>
</el-form-item>
<el-form-item label="微信公众号">
<el-input v-model="publishForm.wechat" placeholder="https://mp.weixin.qq.com/..."></el-input>
</el-form-item>
<el-form-item label="小红书">
<el-input v-model="publishForm.xiaohongshu" placeholder="https://xiaohongshu.com/note/..."></el-input>
</el-form-item>
<!-- 可扩展其他平台 -->
</el-form>
</el-tab-pane>
<el-tab-pane label="发布包" name="packages">
<div v-if="loadingPackages" class="text-center py-4">加载中...</div>
<div v-else-if="packages.length === 0" class="text-gray-500 py-4">暂未生成发布包,请点击"重新生成"</div>
<div v-else>
<div class="mb-4">
<el-button type="primary" size="small" @click="generateAllPackages" :loading="generatingPackages">🔄 重新生成所有平台发布包</el-button>
</div>
<el-table :data="packages" border>
<el-table-column prop="platform" label="平台" width="120"></el-table-column>
<el-table-column prop="path" label="文件路径" width="300"></el-table-column>
<el-table-column prop="size" label="大小" width="80">
<template #default="scope">{{ formatSize(scope.row.size) }}</template>
</el-table-column>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="viewPackage(scope.row)">查看</el-button>
<el-button size="small" type="primary" @click="copyPackage(scope.row)">复制HTML</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
</el-tabs>
<template #footer>
<el-button @click="publishVisible = false">取消</el-button>
<el-button type="primary" :loading="publishing" @click="confirmPublish">确认发布并保存链接</el-button>
</template>
</el-dialog>
<!-- 预览单个发布包 -->
<el-dialog v-model="packagePreviewVisible" :title="`预览:${currentPackage?.platform}`" width="80%">
<div class="mb-4 flex gap-2">
<el-button size="small" @click="packagePreviewVisible = false">关闭</el-button>
<el-button size="small" type="primary" @click="copyCurrentHtml">复制HTML</el-button>
</div>
<div class="border p-4 bg-gray-50" v-html="currentHtml" style="max-height: 70vh; overflow: auto;"></div>
</el-dialog>
<!-- 日志对话框 -->
<el-dialog v-model="showLogs" title="系统日志" width="80%">
<div class="mb-4 flex gap-2">
<el-date-picker v-model="logDate" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD"></el-date-picker>
<el-select v-model="logType" style="width: 120px">
<el-option label="creator" value="creator"></el-option>
<el-option label="publisher" value="publisher"></el-option>
<el-option label="collector" value="collector"></el-option>
</el-select>
<el-button @click="fetchLogs">加载</el-button>
</div>
<pre class="bg-gray-100 p-4 rounded overflow-auto" style="max-height: 60vh;">{{ logContent }}</pre>
</el-dialog>
</div>
<script>
const { createApp, ref, computed, onMounted, watch } = Vue;
createApp({
setup() {
const API_BASE = '';
const status = ref({});
const topics = ref([]);
const filterStatus = ref('');
const generating = ref(false);
const optimizing = ref(false);
const publishing = ref(false);
const pipeline = ref({});
const pipelineLoading = ref(false);
const pipelineModules = ref([]);
const previewVisible = ref(false);
const previewTopic = ref({});
const previewPlatform = ref('zhihu');
const previewHtml = ref('');
const publishVisible = ref(false);
const publishTopic = ref({});
const publishForm = ref({ zhihu: '', wechat: '', xiaohongshu: '' });
const publishTab = ref('links');
// 新增:发布包管理
const packages = ref([]);
const loadingPackages = ref(false);
const generatingPackages = ref(false);
const packagePreviewVisible = ref(false);
const currentPackage = ref(null);
const currentHtml = ref('');
const showLogs = ref(false);
const logType = ref('creator');
const logDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref('');
const filteredTopics = computed(() => {
try {
if (!filterStatus.value) return topics.value || [];
return (topics.value || []).filter(t => t && t.status === filterStatus.value);
} catch (e) {
console.error('filteredTopics error:', e);
return [];
}
});
const refresh = async () => {
try {
const [s, t] = await Promise.all([
fetch(API_BASE + '/api/system/status').then(r => r.json()),
fetch(API_BASE + '/api/topics').then(r => r.json())
]);
status.value = s;
topics.value = t;
console.log('刷新成功', s, t);
} catch (e) {
console.error('刷新失败:', e);
// 设置默认数据用于测试
status.value = { total_topics: 20, topics_by_status: { '待发布': 4, '待处理': 16 } };
// topics.value = []; // 保持空数组
}
};
const refreshPipeline = async () => {
pipelineLoading.value = true;
try {
const res = await fetch(API_BASE + '/api/system/pipeline/status');
if (res.ok) {
const data = await res.json();
pipeline.value = data;
// 构建模块状态表格数据
pipelineModules.value = Object.entries(data.pipeline_modules || {}).map(([name, info]) => ({
module: name,
last_run: info.last_run || '未运行',
status_ok: !info.has_error && info.exists,
status_text: info.exists && !info.has_error ? '正常' : info.exists ? '有错误' : '缺失',
error: info.has_error ? '检测到错误' : ''
}));
} else {
pipelineModules.value = [];
}
} catch (e) {
console.error('流水线状态获取失败:', e);
pipelineModules.value = [];
} finally {
pipelineLoading.value = false;
}
};
const refreshAll = async () => {
await Promise.all([refresh(), refreshPipeline()]);
};
const triggerGenerate = async () => {
generating.value = true;
try {
const res = await fetch(API_BASE + '/api/system/generate/run', { method: 'POST' });
const data = await res.json();
if (data.result && data.result.ok) {
ElementPlus.ElMessage.success('创作任务已启动');
setTimeout(refresh, 5000);
} else {
ElementPlus.ElMessage.error('启动失败: ' + (data.error || '未知错误'));
}
} finally {
generating.value = false;
}
};
const triggerOptimize = async () => {
optimizing.value = true;
try {
const res = await fetch(API_BASE + '/api/system/optimize/run', { method: 'POST' });
const data = await res.json();
if (data.summary) {
ElementPlus.ElNotification({
title: '优化完成',
message: `自动通过 ${data.summary.passed_auto} 篇,需人工 ${data.summary.need_manual}`,
type: data.summary.need_manual === 0 ? 'success' : 'warning'
});
await refresh();
} else {
ElementPlus.ElMessage.success('优化完成');
}
} catch (e) {
ElementPlus.ElMessage.error('请求失败: ' + e);
} finally {
optimizing.value = false;
}
};
const openPreview = async (topic) => {
previewTopic.value = topic;
previewVisible.value = true;
previewPlatform.value = 'zhihu';
await loadPreview();
};
const loadPreview = async () => {
const tid = previewTopic.value.id;
const platform = previewPlatform.value;
try {
const res = await fetch(API_BASE + `/api/articles/${tid}/preview?platform=${platform}`);
const data = await res.json();
previewHtml.value = data.html;
} catch (e) {
ElementPlus.ElMessage.error('无法加载预览');
}
};
watch(previewPlatform, loadPreview);
const openPublish = async (topic) => {
publishTopic.value = topic;
publishForm.value = { zhihu: '', wechat: '', xiaohongshu: '' };
publishVisible.value = true;
publishTab.value = 'links';
await loadPackages();
};
// 加载发布包列表
const loadPackages = async () => {
const tid = publishTopic.value.id;
loadingPackages.value = true;
try {
const res = await fetch(API_BASE + `/api/publisher/packages/${tid}`);
if (res.ok) {
const data = await res.json();
packages.value = data.packages || [];
} else {
packages.value = [];
}
} catch (e) {
console.error('加载发布包失败:', e);
packages.value = [];
} finally {
loadingPackages.value = false;
}
};
// 生成所有平台发布包
const generateAllPackages = async () => {
const tid = publishTopic.value.id;
generatingPackages.value = true;
try {
const res = await fetch(API_BASE + `/api/publisher/generate/${tid}`, { method: 'POST' });
if (res.ok) {
ElementPlus.ElMessage.success('发布包生成任务已启动');
setTimeout(loadPackages, 3000);
} else {
throw new Error('生成失败');
}
} catch (e) {
ElementPlus.ElMessage.error('生成发布包失败: ' + e);
} finally {
generatingPackages.value = false;
}
};
// 查看发布包内容
const viewPackage = async (pkg) => {
currentPackage.value = pkg;
try {
const res = await fetch(API_BASE + `/api/publisher/package/${publishTopic.value.id}/${pkg.platform}`);
if (res.ok) {
const data = await res.json();
currentHtml.value = data.html;
packagePreviewVisible.value = true;
} else {
ElementPlus.ElMessage.error('无法加载发布包');
}
} catch (e) {
ElementPlus.ElMessage.error('请求失败');
}
};
// 复制发布包HTML
const copyPackage = async (pkg) => {
try {
const res = await fetch(API_BASE + `/api/publisher/package/${publishTopic.value.id}/${pkg.platform}`);
if (res.ok) {
const data = await res.json();
await navigator.clipboard.writeText(data.html);
ElementPlus.ElMessage.success('HTML已复制到剪贴板');
} else {
throw new Error('加载失败');
}
} catch (e) {
ElementPlus.ElMessage.error('复制失败: ' + e);
}
};
const copyCurrentHtml = async () => {
try {
await navigator.clipboard.writeText(currentHtml.value);
ElementPlus.ElMessage.success('已复制');
} catch (e) {
ElementPlus.ElMessage.error('复制失败');
}
};
const confirmPublish = async () => {
publishing.value = true;
try {
const res = await fetch(API_BASE + `/api/topics/${publishTopic.value.id}/publish`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ platform_urls: publishForm.value })
});
if (res.ok) {
ElementPlus.ElMessage.success('已标记为已发布');
publishVisible.value = false;
await refresh();
} else {
throw new Error('发布失败');
}
} finally {
publishing.value = false;
}
};
const fetchLogs = async () => {
try {
const dateStr = logDate.value;
const res = await fetch(API_BASE + `/api/system/logs/${dateStr}?log_type=${logType.value}`);
if (!res.ok) throw new Error('日志文件不存在');
const data = await res.json();
logContent.value = data.content ? data.content.join('\n') : '无内容';
} catch (e) {
ElementPlus.ElMessage.error('加载日志失败');
logContent.value = '';
}
};
const formatSize = (bytes) => {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
};
const statusTagType = (status) => {
const map = { '待处理': 'info', '待审查': 'warning', '待发布': 'success', '已发布': 'primary' };
return map[status] || '';
};
const formatDate = (val) => {
if (!val) return '';
const d = new Date(val);
if (isNaN(d.getTime())) return val;
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
const hh = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
const ss = String(d.getSeconds()).padStart(2, '0');
return `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
};
onMounted(() => {
refresh();
refreshPipeline();
});
return {
status, topics, filteredTopics, filterStatus,
generating, optimizing,
previewVisible, previewTopic, previewPlatform, previewHtml,
publishVisible, publishTopic, publishForm, publishing,
publishTab,
packages, loadingPackages, generatingPackages,
packagePreviewVisible, currentPackage, currentHtml,
showLogs, logType, logDate, logContent, fetchLogs,
refresh, refreshPipeline, refreshAll, triggerGenerate, triggerOptimize, openPreview, loadPreview,
openPublish, loadPackages, generateAllPackages, viewPackage, copyPackage, copyCurrentHtml,
confirmPublish, statusTagType, formatDate, formatSize,
pipeline, pipelineLoading, pipelineModules
};
}
}).use(ElementPlus).mount('#app').catch(err => {
console.error('Vue mount error:', err);
document.getElementById('app').innerHTML = '<div class="card"><h2>应用启动失败</h2><pre>' + err + '</pre></div>';
});
</script>
</body>
</html>