1.4.9-Fix

This commit is contained in:
RachelOS
2026-03-26 16:42:09 +08:00
parent f37b7db7e3
commit bc448af97d
16 changed files with 1312 additions and 12 deletions
+2 -1
View File
@@ -22,4 +22,5 @@ demo.txt
test.yaml
.venv
.idea
.trae
.trae
test.py
+384
View File
@@ -0,0 +1,384 @@
from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session
from core.auth import get_current_user_or_ak
from core.database import get_db
from core.db import DB
from core.models.filter_rule import FilterRule
from core.models.base import DATA_STATUS
from .base import success_response, error_response
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel
import json
router = APIRouter(prefix="/filter-rules", tags=["过滤规则管理"])
class FilterRuleCreate(BaseModel):
mp_id: str # JSON字符串,存储多个公众号ID数组
rule_name: str
remove_ids: Optional[List[str]] = None
remove_classes: Optional[List[str]] = None
remove_selectors: Optional[List[str]] = None
remove_attributes: Optional[List[dict]] = None
remove_regex: Optional[List[str]] = None
remove_normal_tag: Optional[int] = 0
priority: Optional[int] = 0
class FilterRuleUpdate(BaseModel):
rule_name: Optional[str] = None
remove_ids: Optional[List[str]] = None
remove_classes: Optional[List[str]] = None
remove_selectors: Optional[List[str]] = None
remove_attributes: Optional[List[dict]] = None
remove_regex: Optional[List[str]] = None
remove_normal_tag: Optional[int] = None
status: Optional[int] = None
priority: Optional[int] = None
@router.get("", summary="获取过滤规则列表")
async def get_filter_rules(
mp_id: str = Query(None, description="公众号ID,不传则返回所有"),
limit: int = Query(20, ge=1, le=100),
offset: int = Query(0, ge=0),
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""获取过滤规则列表,支持按公众号筛选"""
try:
query = db.query(FilterRule)
if mp_id:
query = query.filter(FilterRule.mp_id == mp_id)
total = query.count()
rules = query.order_by(FilterRule.priority.desc(), FilterRule.created_at.desc()).limit(limit).offset(offset).all()
rules_list = []
for rule in rules:
# 解析 mp_id JSON 字符串为数组
mp_ids = []
try:
if rule.mp_id:
mp_ids = json.loads(rule.mp_id) if rule.mp_id.startswith('[') else [rule.mp_id]
except:
mp_ids = [rule.mp_id] if rule.mp_id else []
rules_list.append({
"id": rule.id,
"mp_id": rule.mp_id,
"mp_ids": mp_ids,
"is_global": len(mp_ids) == 0, # 标记是否为全局规则
"rule_name": rule.rule_name,
"remove_ids": rule.remove_ids or [],
"remove_classes": rule.remove_classes or [],
"remove_selectors": rule.remove_selectors or [],
"remove_attributes": rule.remove_attributes or [],
"remove_regex": rule.remove_regex or [],
"remove_normal_tag": rule.remove_normal_tag or 0,
"status": rule.status,
"priority": rule.priority,
"created_at": rule.created_at.isoformat() if rule.created_at else None,
"updated_at": rule.updated_at.isoformat() if rule.updated_at else None
})
return success_response(data={
"list": rules_list,
"page": {
"limit": limit,
"offset": offset,
"total": total
}
})
except Exception as e:
import traceback
traceback.print_exc()
print(f"获取过滤规则列表错误: {str(e)}")
# 如果表不存在,返回空列表
if "no such table" in str(e).lower() or "filter_rules" in str(e).lower():
return success_response(data={
"list": [],
"page": {"limit": limit, "offset": offset, "total": 0}
})
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message=f"获取过滤规则列表失败: {str(e)}")
)
@router.get("/{rule_id}", summary="获取过滤规则详情")
async def get_filter_rule(
rule_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""获取单个过滤规则详情"""
try:
rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
if not rule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_response(code=40401, message="过滤规则不存在")
)
# 解析 mp_id JSON 字符串为数组
mp_ids = []
try:
if rule.mp_id:
mp_ids = json.loads(rule.mp_id) if rule.mp_id.startswith('[') else [rule.mp_id]
except:
mp_ids = [rule.mp_id] if rule.mp_id else []
return success_response(data={
"id": rule.id,
"mp_id": rule.mp_id,
"mp_ids": mp_ids,
"is_global": len(mp_ids) == 0, # 标记是否为全局规则
"rule_name": rule.rule_name,
"remove_ids": rule.remove_ids or [],
"remove_classes": rule.remove_classes or [],
"remove_selectors": rule.remove_selectors or [],
"remove_attributes": rule.remove_attributes or [],
"remove_regex": rule.remove_regex or [],
"remove_normal_tag": rule.remove_normal_tag or 0,
"status": rule.status,
"priority": rule.priority,
"created_at": rule.created_at.isoformat() if rule.created_at else None,
"updated_at": rule.updated_at.isoformat() if rule.updated_at else None
})
except HTTPException:
raise
except Exception as e:
print(f"获取过滤规则详情错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message="获取过滤规则详情失败")
)
@router.post("", summary="创建过滤规则")
async def create_filter_rule(
rule: FilterRuleCreate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""为指定公众号创建过滤规则,支持多公众号"""
try:
new_rule = FilterRule(
mp_id=rule.mp_id,
rule_name=rule.rule_name,
remove_ids=rule.remove_ids,
remove_classes=rule.remove_classes,
remove_selectors=rule.remove_selectors,
remove_attributes=rule.remove_attributes,
remove_regex=rule.remove_regex,
remove_normal_tag=rule.remove_normal_tag or 0,
priority=rule.priority or 0,
status=DATA_STATUS.ACTIVE,
created_at=datetime.now(),
updated_at=datetime.now()
)
db.add(new_rule)
db.commit()
db.refresh(new_rule)
return success_response(data={
"id": new_rule.id,
"mp_id": new_rule.mp_id,
"rule_name": new_rule.rule_name,
"message": "过滤规则创建成功"
})
except HTTPException:
raise
except Exception as e:
db.rollback()
print(f"创建过滤规则错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message="创建过滤规则失败")
)
@router.put("/{rule_id}", summary="更新过滤规则")
async def update_filter_rule(
rule_id: int,
rule: FilterRuleUpdate,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""更新过滤规则"""
try:
existing_rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
if not existing_rule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_response(code=40401, message="过滤规则不存在")
)
update_data = rule.dict(exclude_unset=True)
for key, value in update_data.items():
setattr(existing_rule, key, value)
existing_rule.updated_at = datetime.now()
db.commit()
return success_response(data={
"id": existing_rule.id,
"message": "过滤规则更新成功"
})
except HTTPException:
raise
except Exception as e:
db.rollback()
print(f"更新过滤规则错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message="更新过滤规则失败")
)
@router.delete("/{rule_id}", summary="删除过滤规则")
async def delete_filter_rule(
rule_id: int,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""删除过滤规则"""
try:
rule = db.query(FilterRule).filter(FilterRule.id == rule_id).first()
if not rule:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=error_response(code=40401, message="过滤规则不存在")
)
db.delete(rule)
db.commit()
return success_response(data={
"id": rule_id,
"message": "过滤规则删除成功"
})
except HTTPException:
raise
except Exception as e:
db.rollback()
print(f"删除过滤规则错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message="删除过滤规则失败")
)
@router.get("/mp/{mp_id}/active", summary="获取公众号的启用规则")
async def get_active_rules_for_mp(
mp_id: str,
db: Session = Depends(get_db),
current_user: dict = Depends(get_current_user_or_ak)
):
"""获取指定公众号的所有启用的过滤规则(支持多公众号匹配和全局规则)"""
try:
# 获取所有启用的规则,然后在 Python 层面过滤
rules = db.query(FilterRule).filter(
FilterRule.status == DATA_STATUS.ACTIVE
).order_by(FilterRule.priority.desc()).all()
rules_list = []
for rule in rules:
# 解析 mp_id JSON,检查是否包含指定的 mp_id 或者是全局规则(空数组)
try:
mp_ids = json.loads(rule.mp_id) if rule.mp_id and rule.mp_id.startswith('[') else ([rule.mp_id] if rule.mp_id else [])
except:
mp_ids = [rule.mp_id] if rule.mp_id else []
# 匹配条件:mp_id 在列表中,或者是全局规则(空数组)
if not mp_ids or mp_id in mp_ids:
rules_list.append({
"id": rule.id,
"rule_name": rule.rule_name,
"remove_ids": rule.remove_ids or [],
"remove_classes": rule.remove_classes or [],
"remove_selectors": rule.remove_selectors or [],
"remove_attributes": rule.remove_attributes or [],
"remove_regex": rule.remove_regex or [],
"remove_normal_tag": rule.remove_normal_tag or 0,
"priority": rule.priority
})
return success_response(data=rules_list)
except Exception as e:
print(f"获取公众号过滤规则错误: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=error_response(code=50001, message="获取公众号过滤规则失败")
)
def get_filter_rules_for_mp(mp_id: str) -> list:
"""
获取指定公众号的所有启用过滤规则(供内部调用)
返回规则列表,用于HTML过滤
支持多公众号匹配和全局规则(空mp_id数组)
"""
session = DB.get_session()
try:
rules = session.query(FilterRule).filter(
FilterRule.status == DATA_STATUS.ACTIVE
).order_by(FilterRule.priority.desc()).all()
# 在 Python 层面过滤匹配的规则
matched_rules = []
for rule in rules:
try:
mp_ids = json.loads(rule.mp_id) if rule.mp_id and rule.mp_id.startswith('[') else ([rule.mp_id] if rule.mp_id else [])
except:
mp_ids = [rule.mp_id] if rule.mp_id else []
# 匹配条件:mp_id 在列表中,或者是全局规则(空数组)
if not mp_ids or mp_id in mp_ids:
matched_rules.append(rule)
return matched_rules
except Exception as e:
print(f"获取过滤规则失败: {str(e)}")
return []
def apply_filter_rules(html_content: str, mp_id: str) -> str:
"""
对HTML内容应用指定公众号的过滤规则
Args:
html_content: 原始HTML内容
mp_id: 公众号ID
Returns:
过滤后的HTML内容
"""
if not html_content or not mp_id:
return html_content
rules = get_filter_rules_for_mp(mp_id)
if not rules:
return html_content
from tools.htmltools import htmltools
filtered_content = html_content
for rule in rules:
try:
filtered_content = htmltools.clean_html(
filtered_content,
remove_ids=rule.remove_ids or [],
remove_classes=rule.remove_classes or [],
remove_selectors=rule.remove_selectors or [],
remove_attributes=rule.remove_attributes or [],
remove_regx=rule.remove_regex or [],
remove_normal_tag=bool(rule.remove_normal_tag)
)
except Exception as e:
print(f"应用过滤规则失败 (规则ID: {rule.id}): {str(e)}")
continue
return filtered_content
+3 -1
View File
@@ -11,11 +11,13 @@ server:
#启动JOB定时任务,默认为True
enable_job: ${ENABLE_JOB:-True}
#代码修改自动重启服务,默认为False
auto_reload: ${AUTO_RELOAD:-True}
auto_reload: ${AUTO_RELOAD:-False}
#最大线程数 默认2个线程,不建议超过4个线程
threads: ${THREADS:-1}
#通过web方式授权二维码 默认False
auth_web: ${WERSS_AUTH_WEB:-True}
#是否发送授权二维通知 默认True
send_code: ${WERSS_SEND_CODE:-True}
#数据库连接 例如db: mysql+pymysql://<username>:<password>@<host>/we-rss?charset=utf8mb4
+12 -4
View File
@@ -144,16 +144,15 @@ class Db:
except ValueError:
return now_ts
return now_ts
def _to_unix_millis(value, fallback_seconds) -> int:
now_ts = int(datetime.now().timestamp())
now_ts = datetime.now().timestamp() # 保留小数精度
# 确保 fallback_seconds 是有效的秒级时间戳
if fallback_seconds is None:
fallback_seconds = now_ts
if isinstance(fallback_seconds, (int, float)):
fallback_seconds = int(fallback_seconds)
if fallback_seconds > 1_000_000_000_000:
fallback_seconds = int(fallback_seconds / 1000)
fallback_seconds = fallback_seconds / 1000
# 不转换为int,保留小数部分
else:
fallback_seconds = now_ts
@@ -205,6 +204,15 @@ class Db:
if art.content_html is None:
from tools.fix import fix_html
art.content_html = fix_html(art.content)
# 应用过滤规则
try:
from apis.filter_rule import apply_filter_rules
if art.mp_id and art.content_html:
art.content_html = apply_filter_rules(art.content_html, art.mp_id)
except Exception as e:
print_warning(f"应用过滤规则失败: {e}")
from core.models.base import DATA_STATUS
art.status=DATA_STATUS.ACTIVE
session.add(art)
+2
View File
@@ -14,5 +14,7 @@ from .access_key import AccessKey
from .cascade_node import CascadeNode, CascadeSyncLog
# 导入级联任务分配模型
from .cascade_task_allocation import CascadeTaskAllocation
# 导入过滤规则模型
from .filter_rule import FilterRule
# 导入基础模型
from .base import *
+27
View File
@@ -0,0 +1,27 @@
from .base import Base, Column, String, Integer, DateTime, Text, JSON
from datetime import datetime
class FilterRule(Base):
"""HTML过滤规则模型 - 为指定公众号配置HTML过滤规则"""
__tablename__ = 'filter_rules'
id = Column(Integer, primary_key=True, autoincrement=True)
mp_id = Column(String(255), nullable=False, index=True, comment='公众号ID')
rule_name = Column(String(255), nullable=False, comment='规则名称')
# 过滤规则配置
remove_ids = Column(JSON, nullable=True, comment='要移除的ID列表')
remove_classes = Column(JSON, nullable=True, comment='要移除的class列表')
remove_selectors = Column(JSON, nullable=True, comment='CSS选择器列表')
remove_attributes = Column(JSON, nullable=True, comment='属性过滤列表 [{"name": "attr_name", "value": "attr_value", "eq": true}]')
remove_regex = Column(JSON, nullable=True, comment='正则表达式列表')
remove_normal_tag = Column(Integer, default=0, comment='是否移除常见HTML元素(0-否, 1-是)')
status = Column(Integer, default=1, comment='状态(1-启用, 2-禁用)')
priority = Column(Integer, default=0, comment='优先级,数字越大优先级越高')
created_at = Column(DateTime, default=datetime.now, comment='创建时间')
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, comment='更新时间')
from_attributes = True
+5 -1
View File
@@ -1,3 +1,5 @@
from nt import environ
import requests
import json
import re
@@ -277,7 +279,9 @@ class WxGather:
setStatus(False)
from core.queue import TaskQueue
TaskQueue.clear_queue()
threading.Thread(target=send_wx_code,args=(f"公众号平台登录失效,请重新登录",)).start()
import os
if str(os.getenv('WE_RSS.AUTH',False))!="True" and cfg.get("server.send_code")=="True":
threading.Thread(target=send_wx_code,args=(f"公众号平台登录失效,请重新登录",)).start()
# send_wx_code(f"公众号平台登录失效,请重新登录")
raise Exception(error)
# raise Exception(error)
+1
View File
@@ -5,6 +5,7 @@ from core.models.feed import Feed
from core.models.message_task import MessageTask
from core.models.cascade_node import CascadeNode, CascadeSyncLog
from core.models.cascade_task_allocation import CascadeTaskAllocation
from core.models.filter_rule import FilterRule
from core.db import Db,DB
from core.config import cfg
from core.auth import pwd_context
+2
View File
@@ -21,6 +21,7 @@ from apis.tools import router as tools_router
from apis.github_update import router as github_router
from apis.cascade import router as cascade_router
from apis.env_exception import router as env_exception_router
from apis.filter_rule import router as filter_rule_router
from views import router as views_router
import apis
import os
@@ -93,6 +94,7 @@ api_router.include_router(tools_router)
api_router.include_router(github_router)
api_router.include_router(cascade_router)
api_router.include_router(env_exception_router)
api_router.include_router(filter_rule_router)
resource_router = APIRouter(prefix="/static")
resource_router.include_router(res_router)
+85
View File
@@ -0,0 +1,85 @@
import http from './http'
export interface FilterRule {
id: number
mp_id: string // JSON字符串,存储多个公众号ID
mp_ids: string[] // 解析后的公众号ID数组
is_global: boolean // 是否为全局规则
rule_name: string
remove_ids: string[]
remove_classes: string[]
remove_selectors: string[]
remove_attributes: Array<{ name: string; value?: string; eq?: boolean }>
remove_regex: string[]
remove_normal_tag: number
status: number
priority: number
created_at: string
updated_at: string
}
export interface FilterRuleCreateParams {
mp_id: string // JSON字符串,存储多个公众号ID
rule_name: string
remove_ids?: string[]
remove_classes?: string[]
remove_selectors?: string[]
remove_attributes?: Array<{ name: string; value?: string; eq?: boolean }>
remove_regex?: string[]
remove_normal_tag?: number
priority?: number
}
export interface FilterRuleUpdateParams {
rule_name?: string
remove_ids?: string[]
remove_classes?: string[]
remove_selectors?: string[]
remove_attributes?: Array<{ name: string; value?: string; eq?: boolean }>
remove_regex?: string[]
remove_normal_tag?: number
status?: number
priority?: number
}
export interface FilterRuleListResult {
code: number
data: {
list: FilterRule[]
page: {
limit: number
offset: number
total: number
}
}
}
// 获取过滤规则列表
export const getFilterRules = (params?: { mp_id?: string; limit?: number; offset?: number }) => {
return http.get<FilterRuleListResult>('/wx/filter-rules', { params })
}
// 获取单个过滤规则详情
export const getFilterRule = (ruleId: number) => {
return http.get<{ code: number; data: FilterRule }>(`/wx/filter-rules/${ruleId}`)
}
// 创建过滤规则
export const createFilterRule = (data: FilterRuleCreateParams) => {
return http.post<{ code: number; data: { id: number; message: string } }>('/wx/filter-rules', data)
}
// 更新过滤规则
export const updateFilterRule = (ruleId: number, data: FilterRuleUpdateParams) => {
return http.put<{ code: number; data: { id: number; message: string } }>(`/wx/filter-rules/${ruleId}`, data)
}
// 删除过滤规则
export const deleteFilterRule = (ruleId: number) => {
return http.delete<{ code: number; data: { id: number; message: string } }>(`/wx/filter-rules/${ruleId}`)
}
// 获取公众号的启用规则
export const getActiveRulesForMp = (mpId: string) => {
return http.get<{ code: number; data: FilterRule[] }>(`/wx/filter-rules/mp/${mpId}/active`)
}
+6
View File
@@ -29,6 +29,12 @@
</template>
消息任务
</a-menu-item>
<a-menu-item key="/filter-rules">
<template #icon>
<icon-filter />
</template>
过滤规则
</a-menu-item>
<a-menu-item key="/configs">
<template #icon>
<icon-settings />
+15 -5
View File
@@ -25,8 +25,9 @@ const loading = ref(false)
const mpList = ref<MpItem[]>([])
const selectedMps = ref<MpItem[]>([])
const currentPage = ref(0)
const totalCount = ref(0)
const hasMore = ref(true)
const pageSize = 10
const pageSize = 20
const filteredMps = computed(() => {
return mpList.value.filter(mp =>
@@ -65,8 +66,9 @@ const fetchMps = async (reset = true) => {
mpList.value = [...mpList.value, ...newMps]
}
// 判断是否还有更多数据
hasMore.value = res.list.length === pageSize
// 更新总数并判断是否还有更多数据
totalCount.value = res.total || 0
hasMore.value = mpList.value.length < totalCount.value
} finally {
loading.value = false
@@ -193,10 +195,13 @@ onMounted(() => {
</div>
<div v-if="hasMore" class="load-more">
<a-button type="text" @click="loadMore" :loading="loading">
加载更多
<a-button type="text" @click="loadMore" :loading="loading" :disabled="loading">
加载更多 ({{ mpList.length }}/{{ totalCount }})
</a-button>
</div>
<div v-else-if="mpList.length > 0" class="load-more no-more">
已加载全部 {{ totalCount }} 个公众号
</div>
</a-spin>
</a-space>
</a-card>
@@ -251,4 +256,9 @@ h4 {
justify-content: center;
margin-top: 16px;
}
.no-more {
color: var(--color-text-3);
font-size: 12px;
}
</style>
+30
View File
@@ -12,6 +12,8 @@ import ConfigDetail from '../views/ConfigDetail.vue'
import MessageTaskList from '../views/MessageTaskList.vue'
import MessageTaskForm from '../views/MessageTaskForm.vue'
import NovelReader from '../views/NovelReader.vue'
import FilterRuleList from '../views/FilterRuleList.vue'
import FilterRuleForm from '../views/FilterRuleForm.vue'
const routes = [
{
@@ -172,6 +174,34 @@ const routes = [
permissions: ['admin']
}
},
{
path: 'filter-rules',
name: 'FilterRuleList',
component: FilterRuleList,
meta: {
requiresAuth: true,
permissions: ['wechat:manage']
}
},
{
path: 'filter-rules/add',
name: 'FilterRuleAdd',
component: FilterRuleForm,
meta: {
requiresAuth: true,
permissions: ['wechat:manage']
}
},
{
path: 'filter-rules/edit/:id',
name: 'FilterRuleEdit',
component: FilterRuleForm,
props: true,
meta: {
requiresAuth: true,
permissions: ['wechat:manage']
}
},
]
},
{
+343
View File
@@ -0,0 +1,343 @@
<script setup lang="ts">
import { ref, onMounted, computed, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Message } from '@arco-design/web-vue'
import {
getFilterRule,
createFilterRule,
updateFilterRule,
type FilterRule,
type FilterRuleCreateParams,
type FilterRuleUpdateParams
} from '@/api/filterRule'
import MpMultiSelect from '@/components/MpMultiSelect.vue'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const submitting = ref(false)
const isEdit = computed(() => !!route.params.id)
const showMpSelector = ref(false)
const mpSelectorRef = ref<InstanceType<typeof MpMultiSelect> | null>(null)
// 表单数据
const formData = ref<{
mps_id: any[] // 选中的公众号数组
rule_name: string
remove_ids: string
remove_classes: string
remove_selectors: string
remove_attributes: Array<{ name: string; value: string; eq: boolean }>
remove_regex: string
remove_normal_tag: boolean
priority: number
}>({
mps_id: [],
rule_name: '',
remove_ids: '',
remove_classes: '',
remove_selectors: '',
remove_attributes: [],
remove_regex: '',
remove_normal_tag: false,
priority: 0
})
// 新增属性行
const addAttributeRow = () => {
formData.value.remove_attributes.push({ name: '', value: '', eq: false })
}
// 删除属性行
const removeAttributeRow = (index: number) => {
formData.value.remove_attributes.splice(index, 1)
}
// 获取规则详情
const fetchRuleDetail = async (id: number) => {
loading.value = true
try {
const res = await getFilterRule(id)
const rule = res
// 解析 mp_id JSON 字符串
let mpIds: any[] = []
try {
if (rule.mp_id) {
mpIds = rule.mp_id.startsWith('[')
? JSON.parse(rule.mp_id)
: rule.mp_id.split(',').map((id: string) => ({ id: id.trim() }))
}
} catch {
mpIds = rule.mp_id ? [{ id: rule.mp_id }] : []
}
formData.value = {
mps_id: mpIds,
rule_name: rule.rule_name,
remove_ids: (rule.remove_ids || []).join('\n'),
remove_classes: (rule.remove_classes || []).join('\n'),
remove_selectors: (rule.remove_selectors || []).join('\n'),
remove_attributes: rule.remove_attributes || [],
remove_regex: (rule.remove_regex || []).join('\n'),
remove_normal_tag: !!rule.remove_normal_tag,
priority: rule.priority || 0
}
// 初始化选择器数据
nextTick(() => {
if (mpSelectorRef.value && mpIds.length > 0) {
mpSelectorRef.value.parseSelected(mpIds)
}
})
} catch (error) {
Message.error('获取规则详情失败')
router.back()
} finally {
loading.value = false
}
}
// 提交表单
const handleSubmit = async () => {
if (!formData.value.rule_name.trim()) {
Message.warning('请输入规则名称')
return
}
submitting.value = true
try {
// 提取公众号ID数组并转为JSON字符串,如果为空则传空数组
const mpIdsArray = formData.value.mps_id.map((mp: any) => mp.id?.toString() || mp.toString())
const mpIdJson = mpIdsArray.length > 0 ? JSON.stringify(mpIdsArray) : '[]'
const data: FilterRuleCreateParams | FilterRuleUpdateParams = {
mp_id: mpIdJson,
rule_name: formData.value.rule_name.trim(),
remove_ids: formData.value.remove_ids
.split('\n')
.map(s => s.trim())
.filter(Boolean),
remove_classes: formData.value.remove_classes
.split('\n')
.map(s => s.trim())
.filter(Boolean),
remove_selectors: formData.value.remove_selectors
.split('\n')
.map(s => s.trim())
.filter(Boolean),
remove_attributes: formData.value.remove_attributes.filter(a => a.name.trim()),
remove_regex: formData.value.remove_regex
.split('\n')
.map(s => s.trim())
.filter(Boolean),
remove_normal_tag: formData.value.remove_normal_tag ? 1 : 0,
priority: formData.value.priority
}
if (isEdit.value) {
await updateFilterRule(Number(route.params.id), data)
Message.success('更新成功')
} else {
await createFilterRule(data as FilterRuleCreateParams)
Message.success('创建成功')
}
router.push('/filter-rules')
} catch (error: any) {
Message.error(error?.response?.data?.message || '操作失败')
} finally {
submitting.value = false
}
}
// 取消
const handleCancel = () => {
router.back()
}
onMounted(async () => {
if (isEdit.value) {
fetchRuleDetail(Number(route.params.id))
} else if (route.query.mp_id) {
// 从URL参数获取预选公众号
formData.value.mps_id = [{ id: route.query.mp_id as string }]
}
})
</script>
<template>
<a-spin :loading="loading">
<div class="filter-rule-form">
<a-page-header
:title="isEdit ? '编辑过滤规则' : '添加过滤规则'"
subtitle="配置HTML内容过滤规则"
@back="handleCancel"
/>
<a-card :loading="loading">
<a-form :model="formData" layout="vertical" @submit-success="handleSubmit">
<a-form-item label="选择公众号" field="mps_id">
<a-space>
<a-input
:model-value="(formData.mps_id||[]).map((mp: any) => mp.id?.toString() || mp.toString()).join(',')"
placeholder="不选择则对所有公众号生效"
readonly
style="width: 300px"
/>
<a-button @click="showMpSelector = true">选择</a-button>
</a-space>
<template #extra>
<span class="form-tip">可选择多个公众号,不选择则作为全局规则对所有公众号生效</span>
</template>
</a-form-item>
<a-form-item label="规则名称" field="rule_name" :rules="[{ required: true, message: '请输入规则名称' }]">
<a-input v-model="formData.rule_name" placeholder="例如移除广告元素" />
</a-form-item>
<a-form-item label="优先级" field="priority">
<a-input-number v-model="formData.priority" :min="0" :max="100" placeholder="数字越大优先级越高" />
<template #extra>
<span class="form-tip">数字越大优先级越高,同一公众号的规则按优先级依次执行</span>
</template>
</a-form-item>
<a-divider>过滤规则配置</a-divider>
<a-form-item label="移除ID元素">
<a-textarea
v-model="formData.remove_ids"
placeholder="每行一个ID例如: ad-banner, footer-nav"
:auto-size="{ minRows: 2, maxRows: 6 }"
/>
<template #extra>
<span class="form-tip">按元素ID移除,每行一个</span>
</template>
</a-form-item>
<a-form-item label="移除Class元素">
<a-textarea
v-model="formData.remove_classes"
placeholder="每行一个class名称例如: ad-container, recommend-box"
:auto-size="{ minRows: 2, maxRows: 6 }"
/>
<template #extra>
<span class="form-tip">按CSS class移除,每行一个</span>
</template>
</a-form-item>
<a-form-item label="CSS选择器">
<a-textarea
v-model="formData.remove_selectors"
placeholder="每行一个CSS选择器例如: div.ad-wrapper, .recommend-list > li"
:auto-size="{ minRows: 2, maxRows: 6 }"
/>
<template #extra>
<span class="form-tip">使用CSS选择器精确定位元素,每行一个</span>
</template>
</a-form-item>
<a-form-item label="属性过滤">
<div class="attribute-list">
<div v-for="(attr, index) in formData.remove_attributes" :key="index" class="attribute-row">
<a-input v-model="attr.name" placeholder="属性名" style="width: 120px" />
<a-input v-model="attr.value" placeholder="属性值(可选)" style="flex: 1" />
<a-checkbox v-model="attr.eq">精确匹配</a-checkbox>
<a-button type="text" status="danger" @click="removeAttributeRow(index)">
<template #icon><icon-delete /></template>
</a-button>
</div>
<a-button type="dashed" long @click="addAttributeRow">
<template #icon><icon-plus /></template>
添加属性条件
</a-button>
</div>
<template #extra>
<span class="form-tip">根据元素属性过滤,如 data-type="ad"</span>
</template>
</a-form-item>
<a-form-item label="正则表达式">
<a-textarea
v-model="formData.remove_regex"
placeholder="每行一个正则表达式例如: &lt;div class=&quot;ad&quot;&gt;.*?&lt;/div&gt;"
:auto-size="{ minRows: 2, maxRows: 6 }"
/>
<template #extra>
<span class="form-tip">使用正则表达式移除内容每行一个谨慎使用确保表达式正确</span>
</template>
</a-form-item>
<a-form-item label="移除常见HTML元素">
<a-switch v-model="formData.remove_normal_tag" />
<template #extra>
<span class="form-tip">自动移除 scriptstyle 标签和 HTML 注释等常见元素</span>
</template>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" html-type="submit" :loading="submitting">
{{ isEdit ? '保存修改' : '创建规则' }}
</a-button>
<a-button @click="handleCancel">取消</a-button>
</a-space>
</a-form-item>
</a-form>
</a-card>
<!-- 公众号选择器模态框 -->
<a-modal
v-model:visible="showMpSelector"
title="选择公众号"
:footer="false"
width="800px"
>
<MpMultiSelect
ref="mpSelectorRef"
v-model="formData.mps_id"
/>
<template #footer>
<a-button type="primary" @click="showMpSelector = false">确定</a-button>
</template>
</a-modal>
</div>
</a-spin>
</template>
<style scoped>
.filter-rule-form {
padding: 16px;
margin: 0 auto;
}
.form-tip {
color: var(--color-text-3);
font-size: 12px;
}
.attribute-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.attribute-row {
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 768px) {
.filter-rule-form {
padding: 12px;
}
.attribute-row {
flex-wrap: wrap;
}
.attribute-row .arco-input {
width: 100% !important;
}
}
</style>
+380
View File
@@ -0,0 +1,380 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { Message, Modal } from '@arco-design/web-vue'
import {
getFilterRules,
deleteFilterRule,
updateFilterRule,
type FilterRule
} from '@/api/filterRule'
import { getSubscriptions, type Subscription } from '@/api/subscription'
const router = useRouter()
const route = useRoute()
const loading = ref(false)
const ruleList = ref<FilterRule[]>([])
const mpList = ref<Subscription[]>([])
const selectedMpId = ref<string>('')
const pagination = ref({
current: 1,
pageSize: 20,
total: 0
})
const isMobile = ref(window.innerWidth < 768)
// 获取公众号列表
const fetchMpList = async () => {
try {
const res = await getSubscriptions({ page: 0, pageSize: 100 })
mpList.value = res.list || []
} catch (error) {
console.error('获取公众号列表失败:', error)
}
}
// 获取规则列表
const fetchRuleList = async () => {
loading.value = true
try {
const params: { mp_id?: string; limit: number; offset: number } = {
limit: pagination.value.pageSize,
offset: (pagination.value.current - 1) * pagination.value.pageSize
}
if (selectedMpId.value) {
params.mp_id = selectedMpId.value
}
const res = await getFilterRules(params)
ruleList.value = res.list || []
pagination.value.total = res.page?.total || 0
} catch (error) {
console.error('获取规则列表失败:', error)
Message.error('获取规则列表失败')
} finally {
loading.value = false
}
}
// 获取公众号名称(支持单个或多个)
const getMpNames = (mpIds: string[] | string) => {
if (!mpIds) return '全局规则'
const ids = Array.isArray(mpIds) ? mpIds : [mpIds]
if (ids.length === 0) return '全局规则'
const names = ids.map(id => {
const mp = mpList.value.find(m => m.id === id)
return mp?.mp_name || id
})
return names.join(', ')
}
// 获取第一个公众号名称(用于头像显示)
const getFirstMpName = (mpIds: string[] | string) => {
if (!mpIds) return '全'
const ids = Array.isArray(mpIds) ? mpIds : [mpIds]
if (ids.length === 0) return '全'
const mp = mpList.value.find(m => m.id === ids[0])
return mp?.mp_name?.charAt(0) || ids[0]?.charAt(0) || '?'
}
// 处理筛选变化
const handleFilterChange = () => {
pagination.value.current = 1
fetchRuleList()
}
// 分页变化
const handlePageChange = (page: number) => {
pagination.value.current = page
fetchRuleList()
}
// 加载更多(移动端)
const handleLoadMore = async () => {
loading.value = true
try {
pagination.value.current += 1
const params: { mp_id?: string; limit: number; offset: number } = {
limit: pagination.value.pageSize,
offset: (pagination.value.current - 1) * pagination.value.pageSize
}
if (selectedMpId.value) {
params.mp_id = selectedMpId.value
}
const res = await getFilterRules(params)
ruleList.value = [...ruleList.value, ...res.list]
pagination.value.total = res.page?.total || 0
} finally {
loading.value = false
}
}
// 添加规则
const handleAdd = () => {
router.push('/filter-rules/add')
}
// 编辑规则
const handleEdit = (id: number) => {
router.push(`/filter-rules/edit/${id}`)
}
// 切换状态
const handleToggleStatus = async (rule: FilterRule) => {
const newStatus = rule.status === 1 ? 2 : 1
try {
await updateFilterRule(rule.id, { status: newStatus })
Message.success(newStatus === 1 ? '规则已启用' : '规则已禁用')
fetchRuleList()
} catch (error) {
Message.error('操作失败')
}
}
// 删除规则
const handleDelete = (id: number) => {
Modal.confirm({
title: '确认删除',
content: '确定要删除这条过滤规则吗?删除后无法恢复',
okText: '确认',
cancelText: '取消',
onOk: async () => {
try {
await deleteFilterRule(id)
Message.success('删除成功')
fetchRuleList()
} catch (error) {
Message.error('删除失败')
}
}
})
}
// 预览规则配置
const previewRule = (rule: FilterRule) => {
const config: string[] = []
if (rule.remove_ids?.length) config.push(`移除ID: ${rule.remove_ids.join(', ')}`)
if (rule.remove_classes?.length) config.push(`移除Class: ${rule.remove_classes.join(', ')}`)
if (rule.remove_selectors?.length) config.push(`CSS选择器: ${rule.remove_selectors.join(', ')}`)
if (rule.remove_regex?.length) config.push(`正则: ${rule.remove_regex.length}`)
if (rule.remove_normal_tag) config.push('移除常见元素')
return config.length ? config.join(' | ') : '无过滤配置'
}
onMounted(() => {
fetchMpList()
// 从路由参数获取mp_id
if (route.query.mp_id) {
selectedMpId.value = route.query.mp_id as string
}
fetchRuleList()
})
</script>
<template>
<a-spin :loading="loading">
<div class="filter-rule-list">
<div class="header">
<h2>HTML过滤规则</h2>
<a-select
v-model="selectedMpId"
placeholder="选择公众号筛选"
allow-clear
style="width: 200px; margin-right: 10px;"
@change="handleFilterChange"
>
<a-option value="">全部公众号</a-option>
<a-option
v-for="mp in mpList"
:key="mp.id"
:value="mp.id"
:label="mp.mp_name"
/>
</a-select>
<a-button type="primary" @click="handleAdd">
<template #icon><icon-plus /></template>
添加规则
</a-button>
</div>
<a-alert type="info" closable style="margin-bottom: 16px">
过滤规则用于在采集文章后自动清理HTML内容可移除广告无关元素等每个公众号可配置多条规则按优先级依次执行
</a-alert>
<div v-if="!isMobile" class="table-wrapper">
<a-table
:data="ruleList"
:pagination="pagination"
:loading="loading"
@page-change="handlePageChange"
>
<template #columns>
<a-table-column title="公众号" :width="200">
<template #cell="{ record }">
<div class="mp-info">
<a-avatar :size="24" :style="{ backgroundColor: '#3370ff' }">
{{ getFirstMpName(record.mp_ids) }}
</a-avatar>
<a-tooltip :content="getMpNames(record.mp_ids)">
<span class="mp-name">{{ getMpNames(record.mp_ids) }}</span>
</a-tooltip>
</div>
</template>
</a-table-column>
<a-table-column title="规则名称" data-index="rule_name" :width="180" />
<a-table-column title="过滤配置" ellipsis>
<template #cell="{ record }">
<a-tooltip :content="previewRule(record)">
<span class="rule-preview">{{ previewRule(record) }}</span>
</a-tooltip>
</template>
</a-table-column>
<a-table-column title="优先级" data-index="priority" :width="80" align="center" />
<a-table-column title="状态" :width="100" align="center">
<template #cell="{ record }">
<a-tag :color="record.status === 1 ? 'green' : 'red'">
{{ record.status === 1 ? '启用' : '禁用' }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="操作" :width="200" align="center">
<template #cell="{ record }">
<a-space>
<a-button size="mini" type="primary" @click="handleEdit(record.id)">编辑</a-button>
<a-button
size="mini"
:type="record.status === 1 ? 'outline' : 'primary'"
:status="record.status === 1 ? 'warning' : 'success'"
@click="handleToggleStatus(record)"
>
{{ record.status === 1 ? '禁用' : '启用' }}
</a-button>
<a-button size="mini" status="danger" @click="handleDelete(record.id)">删除</a-button>
</a-space>
</template>
</a-table-column>
</template>
</a-table>
</div>
<a-list v-else :bordered="false">
<a-list-item v-for="item in ruleList" :key="item.id">
<a-list-item-meta>
<template #title>
<div class="mobile-title">
<span>{{ item.rule_name }}</span>
<a-tag :color="item.status === 1 ? 'green' : 'red'" size="small">
{{ item.status === 1 ? '启用' : '禁用' }}
</a-tag>
</div>
</template>
<template #description>
<div class="mobile-desc">
<div>公众号: {{ getMpNames(item.mp_ids) }}</div>
<div class="rule-preview">{{ previewRule(item) }}</div>
<div>优先级: {{ item.priority }}</div>
</div>
</template>
</a-list-item-meta>
<template #actions>
<a-space direction="vertical" size="mini">
<a-button size="mini" type="primary" long @click="handleEdit(item.id)">编辑</a-button>
<a-button
size="mini"
:type="item.status === 1 ? 'outline' : 'primary'"
:status="item.status === 1 ? 'warning' : 'success'"
long
@click="handleToggleStatus(item)"
>
{{ item.status === 1 ? '禁用' : '启用' }}
</a-button>
<a-button size="mini" status="danger" long @click="handleDelete(item.id)">删除</a-button>
</a-space>
</template>
</a-list-item>
<template #footer>
<div v-if="ruleList.length < pagination.total" class="load-more">
<a-button long @click="handleLoadMore" :loading="loading">加载更多</a-button>
</div>
</template>
</a-list>
</div>
</a-spin>
</template>
<style scoped>
.filter-rule-list {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header h2 {
flex: 1;
}
.header .arco-btn {
margin-left: 10px;
}
h2 {
margin: 0;
color: var(--color-text-1);
}
.table-wrapper {
width: 100%;
}
.table-wrapper :deep(.arco-table-container) {
width: 100%;
}
.mp-info {
display: flex;
align-items: center;
gap: 8px;
}
.mp-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rule-preview {
color: var(--color-text-3);
font-size: 12px;
}
.mobile-title {
display: flex;
justify-content: space-between;
align-items: center;
}
.mobile-desc {
font-size: 12px;
color: var(--color-text-3);
}
.mobile-desc div {
margin-bottom: 4px;
}
.load-more {
width: 120px;
margin: 0 auto;
text-align: center;
}
@media (max-width: 768px) {
.filter-rule-list {
padding: 12px;
}
}
</style>
+15
View File
@@ -18,6 +18,13 @@
<template #action="{ record }">
<a-space>
<a-button size="mini" @click="editMp(record)">编辑</a-button>
<a-button
size="mini"
type="outline"
@click="goToFilterRules(record.id)"
>
规则
</a-button>
<a-button
size="mini"
status="danger"
@@ -63,8 +70,12 @@
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { getSubscriptions, addSubscription, updateSubscription, deleteSubscription } from '@/api/subscription'
import { getToken } from '@/utils/auth'
import { Message } from '@arco-design/web-vue'
const router = useRouter()
const headers = { Authorization: `Bearer ${getToken()}` }
@@ -145,6 +156,10 @@ const deleteMp = async (id) => {
loadData()
}
const goToFilterRules = (mpId: string) => {
router.push({ path: '/filter-rules', query: { mp_id: mpId } })
}
const handlePageChange = (page) => {
pagination.current = page
loadData()