Procházet zdrojové kódy

优化任务管理器,新增更新 cookie 功能

luojunhui před 1 měsícem
rodič
revize
388421cfa7

+ 1 - 0
applications/service/__init__.py

@@ -3,3 +3,4 @@ from .log_service import LogService
 
 
 # 前端交互
 # 前端交互
 from .task_manager_service import TaskManagerService
 from .task_manager_service import TaskManagerService
+from .gzh_cookie_manager import GzhCookieManager

+ 30 - 0
applications/service/gzh_cookie_manager.py

@@ -0,0 +1,30 @@
+from applications.tasks.crawler_tasks.crawler_gzh_fans import CrawlerGzhFansBase
+
+
+class GzhCookieManager(CrawlerGzhFansBase):
+    def __init__(self, pool, log_client):
+        super().__init__(pool, log_client)
+
+    async def deal(self, data):
+        gh_id = data.get('gzh_id')
+        if not gh_id:
+            return {"error": "gh_id is required"}
+
+        if not gh_id.startswith("gh_"):
+            return {"error": "gh_id is invalid"}
+
+        token = data.get("token")
+        if not token:
+            return {"error": "token is required"}
+
+        cookie = data.get("cookie")
+        if not cookie:
+            return {"error": "cookie is required"}
+
+        insert_row = await self.set_cookie_token_for_each_account(
+            gh_id=gh_id, cookie=cookie, token=token
+        )
+        if not insert_row:
+            return {"error": "insert row failed"}
+
+        return {"success": "cookie and token set successfully"}

+ 3 - 0
applications/tasks/crawler_tasks/__init__.py

@@ -2,10 +2,13 @@ from .crawler_toutiao import CrawlerToutiao
 from .crawler_account_manager import WeixinAccountManager
 from .crawler_account_manager import WeixinAccountManager
 from .crawler_gzh import CrawlerGzhAccountArticles
 from .crawler_gzh import CrawlerGzhAccountArticles
 from .crawler_gzh import CrawlerGzhSearchArticles
 from .crawler_gzh import CrawlerGzhSearchArticles
+from .crawler_gzh_fans import CrawlerGzhFans
+
 
 
 __all__ = [
 __all__ = [
     "CrawlerToutiao",
     "CrawlerToutiao",
     "WeixinAccountManager",
     "WeixinAccountManager",
     "CrawlerGzhAccountArticles",
     "CrawlerGzhAccountArticles",
     "CrawlerGzhSearchArticles",
     "CrawlerGzhSearchArticles",
+    "CrawlerGzhFans",
 ]
 ]

+ 8 - 1
routes/blueprint.py

@@ -3,7 +3,7 @@ from applications.ab_test import GetCoverService
 from applications.utils import generate_task_trace_id
 from applications.utils import generate_task_trace_id
 
 
 from applications.tasks import TaskScheduler
 from applications.tasks import TaskScheduler
-from applications.service import TaskManagerService
+from applications.service import TaskManagerService, GzhCookieManager
 
 
 
 
 server_blueprint = Blueprint("api", __name__, url_prefix="/api")
 server_blueprint = Blueprint("api", __name__, url_prefix="/api")
@@ -36,4 +36,11 @@ def server_routes(pools, log_service):
         res = await TMS.list_tasks()
         res = await TMS.list_tasks()
         return jsonify(res)
         return jsonify(res)
 
 
+    @server_blueprint.route("/save_token", methods=["POST"])
+    async def save_token():
+        data = await request.get_json()
+        GCM = GzhCookieManager(pool=pools, log_client=log_service)
+        res = await GCM.deal(data)
+        return jsonify(res)
+
     return server_blueprint
     return server_blueprint

+ 5 - 1
ui/src/router/index.ts

@@ -1,11 +1,15 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import { createRouter, createWebHistory } from 'vue-router'
 import TaskManager from '../views/TaskManager.vue'
 import TaskManager from '../views/TaskManager.vue'
+import TokenManager from '../views/TokenManager.vue'
+import Welcome from '../views/Welcome.vue'
 
 
 const router = createRouter({
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
   history: createWebHistory(import.meta.env.BASE_URL),
   routes: [
   routes: [
-      { path: '/', redirect: '/tasks' },
+      { path: '/', redirect: '/welcome' },
+      { path: '/welcome', name: 'Welcome', component: Welcome },
       { path: '/tasks', name: 'TaskManager', component: TaskManager },
       { path: '/tasks', name: 'TaskManager', component: TaskManager },
+      { path: '/token', name: 'TokenManager', component: TokenManager },
   ],
   ],
 })
 })
 
 

+ 82 - 28
ui/src/views/TaskManager.vue

@@ -1,9 +1,16 @@
 <template>
 <template>
   <div class="task-page">
   <div class="task-page">
+    <!-- 顶部导航栏 -->
+    <div class="top-bar">
+      <div class="left">
+        <el-button type="info" plain @click="goBack">🏠 返回首页</el-button>
+      </div>
+      <div class="title">📋 任务列表</div>
+    </div>
+
     <el-card class="task-card">
     <el-card class="task-card">
       <!-- 工具栏 -->
       <!-- 工具栏 -->
       <div class="toolbar" ref="toolbarRef">
       <div class="toolbar" ref="toolbarRef">
-        <!-- 你的筛选表单(保持不变) -->
         <el-form :inline="true" :model="filters" label-width="90px" @keyup.enter.native="onSearch">
         <el-form :inline="true" :model="filters" label-width="90px" @keyup.enter.native="onSearch">
           <el-form-item label="ID">
           <el-form-item label="ID">
             <el-input v-model.number="filters.id" placeholder="精确 ID" clearable />
             <el-input v-model.number="filters.id" placeholder="精确 ID" clearable />
@@ -47,7 +54,9 @@
           <el-table-column prop="task_name" label="任务名称" min-width="200" :show-overflow-tooltip="true" />
           <el-table-column prop="task_name" label="任务名称" min-width="200" :show-overflow-tooltip="true" />
           <el-table-column prop="task_status" label="状态" sortable="custom" width="140">
           <el-table-column prop="task_status" label="状态" sortable="custom" width="140">
             <template #default="{ row }">
             <template #default="{ row }">
-              <el-tag :type="statusType(row.task_status)">{{ row.status_text || row.task_status }}</el-tag>
+              <el-tag :type="statusType(row.task_status)">
+                {{ row.status_text || row.task_status }}
+              </el-tag>
             </template>
             </template>
           </el-table-column>
           </el-table-column>
           <el-table-column prop="start_timestamp" label="开始时间" sortable="custom" width="180">
           <el-table-column prop="start_timestamp" label="开始时间" sortable="custom" width="180">
@@ -104,9 +113,16 @@
 
 
 <script setup lang="ts">
 <script setup lang="ts">
 import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue'
 import { onMounted, onBeforeUnmount, reactive, ref, nextTick } from 'vue'
+import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { fetchTasks, fetchTaskDetail, retryTask, cancelTask, type TaskItem } from '../api/task'
 import { fetchTasks, fetchTaskDetail, retryTask, cancelTask, type TaskItem } from '../api/task'
 
 
+const router = useRouter()
+
+const goBack = () => {
+  router.push('/welcome')
+}
+
 const statusOptions = [
 const statusOptions = [
   { label: '初始化(0)', value: 0 },
   { label: '初始化(0)', value: 0 },
   { label: '处理中(1)', value: 1 },
   { label: '处理中(1)', value: 1 },
@@ -146,16 +162,13 @@ const calcTableHeight = () => {
   const winH = window.innerHeight
   const winH = window.innerHeight
   const toolbarH = toolbarRef.value?.offsetHeight ?? 0
   const toolbarH = toolbarRef.value?.offsetHeight ?? 0
   const pagerH = pagerRef.value?.offsetHeight ?? 0
   const pagerH = pagerRef.value?.offsetHeight ?? 0
-  const outerPadding = 16 * 2         // .task-page 上下 padding
-  const cardPadding = 20 * 2          // el-card 内部上下 padding(大约值)
-  const gap = 12 + 12                 // toolbar 下方间距 + pager 上方 padding
-  tableHeight.value = Math.max(
-    260,
-    winH - (outerPadding + cardPadding + toolbarH + pagerH + gap)
-  )
+  const outerPadding = 16 * 2
+  const cardPadding = 20 * 2
+  const gap = 12 + 12
+  tableHeight.value = Math.max(260, winH - (outerPadding + cardPadding + toolbarH + pagerH + gap))
 }
 }
 
 
-/* 加载列表(POST) */
+/* 加载列表 */
 const load = async () => {
 const load = async () => {
   loading.value = true
   loading.value = true
   try {
   try {
@@ -188,7 +201,7 @@ const onSortChange = (e: any) => {
   load()
   load()
 }
 }
 
 
-/* 工具 */
+/* 工具函数 */
 const statusType = (s: number) => (s === 0 ? 'info' : s === 1 ? 'warning' : s === 2 ? 'success' : s === 99 ? 'danger' : '')
 const statusType = (s: number) => (s === 0 ? 'info' : s === 1 ? 'warning' : s === 2 ? 'success' : s === 99 ? 'danger' : '')
 const formatTs = (ts?: number | null) => (!ts ? '-' : new Date(ts * 1000).toLocaleString())
 const formatTs = (ts?: number | null) => (!ts ? '-' : new Date(ts * 1000).toLocaleString())
 const pretty = (v: any) => { try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2) } catch { return String(v) } }
 const pretty = (v: any) => { try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2) } catch { return String(v) } }
@@ -229,34 +242,75 @@ onBeforeUnmount(() => {
 
 
 <style scoped>
 <style scoped>
 .task-page {
 .task-page {
-  padding: 16px;
-  background: #f5f6fa;
-  height: 100vh;          /* 让页面可用高度=视口高度 */
+  padding: 0;
+  background: linear-gradient(180deg, #f7f9fc, #edf1f6);
+  height: 100vh;
   box-sizing: border-box;
   box-sizing: border-box;
+  display: flex;
+  flex-direction: column;
+}
+
+/* 顶部导航栏 */
+.top-bar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  background: #fff;
+  padding: 12px 24px;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
+  border-bottom: 1px solid #e6e8ef;
+}
+.top-bar .title {
+  font-weight: 600;
+  font-size: 18px;
+  color: #333;
 }
 }
+
+/* 主体卡片 */
 .task-card {
 .task-card {
+  flex: 1;
+  margin: 20px;
   display: flex;
   display: flex;
   flex-direction: column;
   flex-direction: column;
-  height: 100%;
-  box-shadow: 0 6px 18px rgba(0,0,0,0.06);
-  border-radius: 10px;
+  box-shadow: 0 8px 20px rgba(0, 0, 0, 0.05);
+  border-radius: 12px;
+  overflow: hidden;
+}
+
+.toolbar {
+  margin-bottom: 12px;
+}
+.table-wrapper {
+  flex: 1;
+  overflow: hidden;
+}
+.custom-table {
+  border-radius: 8px;
+  overflow: hidden;
 }
 }
-.toolbar { margin-bottom: 12px; }
-.table-wrapper { flex: 1; overflow: hidden; }
-.custom-table { border-radius: 8px; overflow: hidden; }
 .pager {
 .pager {
   display: flex;
   display: flex;
   justify-content: flex-end;
   justify-content: flex-end;
-  padding: 12px 0 0 0;   /* 顶部 12px,底部 0,避免留白 */
+  padding: 12px 0 0 0;
   background: #fff;
   background: #fff;
   border-top: 1px solid #f0f2f5;
   border-top: 1px solid #f0f2f5;
-  margin: 0;             /* 清除可能的外边距 */
 }
 }
-.detail { padding: 8px 0; }
-.block-title { margin: 14px 0 8px; font-weight: 600; }
+.detail {
+  padding: 8px 0;
+}
+.block-title {
+  margin: 14px 0 8px;
+  font-weight: 600;
+}
 .code-block {
 .code-block {
-  background: #0b1021; color: #d1e7ff; padding: 12px;
-  border-radius: 8px; overflow: auto; line-height: 1.5;
+  background: #0b1021;
+  color: #d1e7ff;
+  padding: 12px;
+  border-radius: 8px;
+  overflow: auto;
+  line-height: 1.5;
+}
+.ml-8 {
+  margin-left: 8px;
 }
 }
-.ml-8 { margin-left: 8px; }
-</style>
+</style>

+ 140 - 0
ui/src/views/TokenManager.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="token-page">
+    <div class="top-bar">
+      <el-button type="info" plain @click="goBack">🏠 返回首页</el-button>
+    </div>
+
+    <el-card class="token-card">
+      <h2>🔑 更新 Token / Cookie</h2>
+
+      <el-form :model="form" label-width="100px">
+        <el-form-item label="gh_id">
+          <el-input
+            v-model="form.gzh_id"
+            placeholder="请输入公众号 ID"
+            clearable
+          />
+        </el-form-item>
+
+        <el-form-item label="Token">
+          <el-input
+            v-model="form.token"
+            placeholder="请输入 Token"
+            clearable
+          />
+        </el-form-item>
+
+        <el-form-item label="Cookie">
+          <el-input
+            v-model="form.cookie"
+            placeholder="请输入 Cookie"
+            type="textarea"
+            rows="4"
+            clearable
+          />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button
+            type="primary"
+            @click="onSave"
+            :loading="loading"
+            style="width: 120px"
+          >
+            💾 保存
+          </el-button>
+          <el-button @click="onReset" :disabled="loading" style="width: 120px">
+            🔄 重置
+          </el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import axios from 'axios'
+
+const router = useRouter()
+
+const form = ref({
+  gzh_id: '',
+  token: '',
+  cookie: '',
+})
+
+const loading = ref(false)
+
+const onSave = async () => {
+  if (!form.value.gzh_id || !form.value.token || !form.value.cookie) {
+    ElMessage.warning('请填写完整信息(gh_id / Token / Cookie)')
+    return
+  }
+
+  loading.value = true
+  try {
+    const res = await axios.post('http://127.0.0.1:6060/api/save_token', {
+      gzh_id: form.value.gzh_id,
+      token: form.value.token,
+      cookie: form.value.cookie,
+    })
+
+    if (res.data && res.data.success) {
+      ElMessage.success('更新成功 ✅')
+      form.value = { gzh_id: '', token: '', cookie: '' }
+    } else {
+      ElMessage.error(res.data.error || '更新失败 ❌')
+    }
+  } catch (err) {
+    console.error('保存请求出错:', err)
+    ElMessage.error('请求异常,请稍后重试')
+  } finally {
+    loading.value = false
+  }
+}
+
+const onReset = () => {
+  form.value = { gzh_id: '', token: '', cookie: '' }
+}
+
+const goBack = () => {
+  router.push('/welcome')
+}
+</script>
+
+<style scoped>
+.token-page {
+  background: linear-gradient(180deg, #f6f8fb, #e8ecf3);
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+}
+
+.top-bar {
+  padding: 12px 16px;
+  background: #fff;
+  border-bottom: 1px solid #f0f2f5;
+  display: flex;
+  justify-content: flex-start;
+}
+
+.token-card {
+  width: 520px;
+  margin: 80px auto 0;
+  padding: 40px 30px 30px;
+  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
+  border-radius: 14px;
+  background: #fff;
+}
+
+h2 {
+  text-align: center;
+  font-size: 22px;
+  margin-bottom: 30px;
+  color: #333;
+  font-weight: 600;
+}
+</style>