Merge branch 'master' of https://github.com/MaiBot-Plus/MaiMbot-Pro-Max
This commit is contained in:
78
src/common/database/db_migration.py
Normal file
78
src/common/database/db_migration.py
Normal file
@@ -0,0 +1,78 @@
|
||||
# mmc/src/common/database/db_migration.py
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.engine import Engine
|
||||
from src.common.database.sqlalchemy_models import Base, get_engine
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("db_migration")
|
||||
|
||||
|
||||
def check_and_migrate_database():
|
||||
"""
|
||||
检查数据库结构并自动迁移(添加缺失的表和列)。
|
||||
"""
|
||||
logger.info("正在检查数据库结构并执行自动迁移...")
|
||||
engine = get_engine()
|
||||
inspector = inspect(engine)
|
||||
|
||||
# 1. 获取数据库中所有已存在的表名
|
||||
db_table_names = set(inspector.get_table_names())
|
||||
|
||||
# 2. 遍历所有在代码中定义的模型
|
||||
for table_name, table in Base.metadata.tables.items():
|
||||
logger.debug(f"正在检查表: {table_name}")
|
||||
|
||||
# 3. 如果表不存在,则创建它
|
||||
if table_name not in db_table_names:
|
||||
logger.info(f"表 '{table_name}' 不存在,正在创建...")
|
||||
try:
|
||||
table.create(engine)
|
||||
logger.info(f"表 '{table_name}' 创建成功。")
|
||||
except Exception as e:
|
||||
logger.error(f"创建表 '{table_name}' 失败: {e}")
|
||||
continue
|
||||
|
||||
# 4. 如果表已存在,则检查并添加缺失的列
|
||||
db_columns = {col["name"] for col in inspector.get_columns(table_name)}
|
||||
model_columns = {col.name for col in table.c}
|
||||
|
||||
missing_columns = model_columns - db_columns
|
||||
if not missing_columns:
|
||||
logger.debug(f"表 '{table_name}' 结构一致,无需修改。")
|
||||
continue
|
||||
|
||||
logger.info(f"在表 '{table_name}' 中发现缺失的列: {', '.join(missing_columns)}")
|
||||
with engine.connect() as connection:
|
||||
trans = connection.begin()
|
||||
try:
|
||||
for column_name in missing_columns:
|
||||
column = table.c[column_name]
|
||||
|
||||
# 构造并执行 ALTER TABLE 语句
|
||||
try:
|
||||
column_type = column.type.compile(engine.dialect)
|
||||
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
|
||||
|
||||
# 添加默认值和非空约束的处理
|
||||
if column.default is not None:
|
||||
default_value = column.default.arg
|
||||
if isinstance(default_value, str):
|
||||
sql += f" DEFAULT '{default_value}'"
|
||||
else:
|
||||
sql += f" DEFAULT {default_value}"
|
||||
|
||||
if not column.nullable:
|
||||
sql += " NOT NULL"
|
||||
|
||||
connection.execute(text(sql))
|
||||
logger.info(f"成功向表 '{table_name}' 添加列 '{column_name}'。")
|
||||
except Exception as e:
|
||||
logger.error(f"向表 '{table_name}' 添加列 '{column_name}' 失败: {e}")
|
||||
|
||||
trans.commit()
|
||||
except Exception as e:
|
||||
logger.error(f"在表 '{table_name}' 添加列时发生错误,事务已回滚: {e}")
|
||||
trans.rollback()
|
||||
|
||||
logger.info("数据库结构检查与自动迁移完成。")
|
||||
138
src/common/database/db_migration_plan.md
Normal file
138
src/common/database/db_migration_plan.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# 数据库自动迁移模块 (`db_migration.py`) 设计文档
|
||||
|
||||
## 1. 目标
|
||||
|
||||
创建一个自动化的数据库迁移模块,用于在应用启动时检查数据库结构,并自动进行以下修复:
|
||||
|
||||
1. **创建缺失的表**:如果代码模型中定义的表在数据库中不存在,则自动创建。
|
||||
2. **添加缺失的列**:如果数据库中的某个表现有的列比代码模型中定义的少,则自动添加缺失的列。
|
||||
|
||||
## 2. 实现思路
|
||||
|
||||
我们将使用 SQLAlchemy 的 `Inspector` 来获取数据库的元数据(即实际的表和列信息),并将其与 `SQLAlchemy` 模型(`Base.metadata`)中定义的结构进行比较。
|
||||
|
||||
核心逻辑分为以下几个步骤:
|
||||
|
||||
1. **获取数据库引擎**:从现有代码中获取已初始化的 SQLAlchemy 引擎实例。
|
||||
2. **获取 Inspector**:通过引擎创建一个 `Inspector` 对象。
|
||||
3. **获取所有模型**:遍历 `Base.metadata.tables`,获取所有在代码中定义的表模型。
|
||||
4. **获取数据库中所有表名**:使用 `inspector.get_table_names()` 获取数据库中实际存在的所有表名。
|
||||
5. **创建缺失的表**:通过比较模型表名和数据库表名,找出所有缺失的表,并使用 `table.create(engine)` 来创建它们。
|
||||
6. **检查并添加缺失的列**:
|
||||
* 遍历每一个代码中定义的表模型。
|
||||
* 使用 `inspector.get_columns(table_name)` 获取数据库中该表的实际列。
|
||||
* 比较模型列和实际列,找出所有缺失的列。
|
||||
* 对于每一个缺失的列,生成一个 `ALTER TABLE ... ADD COLUMN ...` 的 SQL 语句,并执行它。
|
||||
|
||||
## 3. 伪代码实现
|
||||
|
||||
```python
|
||||
# mmc/src/common/database/db_migration.py
|
||||
|
||||
from sqlalchemy import inspect, text
|
||||
from sqlalchemy.engine import Engine
|
||||
from src.common.database.sqlalchemy_models import Base, get_engine
|
||||
from src.common.logger import get_logger
|
||||
|
||||
logger = get_logger("db_migration")
|
||||
|
||||
def check_and_migrate_database():
|
||||
"""
|
||||
检查数据库结构并自动迁移(添加缺失的表和列)。
|
||||
"""
|
||||
logger.info("正在检查数据库结构并执行自动迁移...")
|
||||
engine = get_engine()
|
||||
inspector = inspect(engine)
|
||||
|
||||
# 1. 获取数据库中所有已存在的表名
|
||||
db_table_names = set(inspector.get_table_names())
|
||||
|
||||
# 2. 遍历所有在代码中定义的模型
|
||||
for table_name, table in Base.metadata.tables.items():
|
||||
logger.debug(f"正在检查表: {table_name}")
|
||||
|
||||
# 3. 如果表不存在,则创建它
|
||||
if table_name not in db_table_names:
|
||||
logger.info(f"表 '{table_name}' 不存在,正在创建...")
|
||||
try:
|
||||
table.create(engine)
|
||||
logger.info(f"表 '{table_name}' 创建成功。")
|
||||
except Exception as e:
|
||||
logger.error(f"创建表 '{table_name}' 失败: {e}")
|
||||
continue
|
||||
|
||||
# 4. 如果表已存在,则检查并添加缺失的列
|
||||
db_columns = {col['name'] for col in inspector.get_columns(table_name)}
|
||||
model_columns = {col.name for col in table.c}
|
||||
|
||||
missing_columns = model_columns - db_columns
|
||||
if not missing_columns:
|
||||
logger.debug(f"表 '{table_name}' 结构一致,无需修改。")
|
||||
continue
|
||||
|
||||
logger.info(f"在表 '{table_name}' 中发现缺失的列: {', '.join(missing_columns)}")
|
||||
with engine.connect() as connection:
|
||||
for column_name in missing_columns:
|
||||
column = table.c[column_name]
|
||||
|
||||
# 构造并执行 ALTER TABLE 语句
|
||||
# 注意:这里的实现需要考虑不同数据库(SQLite, MySQL)的语法差异
|
||||
# 为了简化,我们先使用一个通用的格式,后续可以根据需要进行扩展
|
||||
try:
|
||||
column_type = column.type.compile(engine.dialect)
|
||||
sql = f"ALTER TABLE {table_name} ADD COLUMN {column_name} {column_type}"
|
||||
|
||||
# 可以在这里添加对默认值、非空约束等的处理
|
||||
|
||||
connection.execute(text(sql))
|
||||
logger.info(f"成功向表 '{table_name}' 添加列 '{column_name}'。")
|
||||
except Exception as e:
|
||||
logger.error(f"向表 '{table_name}' 添加列 '{column_name}' 失败: {e}")
|
||||
|
||||
# 提交事务
|
||||
if connection.in_transaction():
|
||||
connection.commit()
|
||||
|
||||
logger.info("数据库结构检查与自动迁移完成。")
|
||||
|
||||
```
|
||||
|
||||
## 4. 集成到启动流程
|
||||
|
||||
为了让这个迁移模块在应用启动时自动运行,我们需要在 `mmc/src/common/database/sqlalchemy_models.py` 的 `initialize_database` 函数中调用它。
|
||||
|
||||
修改后的 `initialize_database` 函数将如下所示:
|
||||
|
||||
```python
|
||||
# mmc/src/common/database/sqlalchemy_models.py
|
||||
|
||||
# ... (其他 import)
|
||||
from src.common.database.db_migration import check_and_migrate_database # 导入新函数
|
||||
|
||||
# ... (代码)
|
||||
|
||||
def initialize_database():
|
||||
"""初始化数据库引擎和会话"""
|
||||
global _engine, _SessionLocal
|
||||
|
||||
if _engine is not None:
|
||||
return _engine, _SessionLocal
|
||||
|
||||
# ... (数据库连接和引擎创建逻辑)
|
||||
|
||||
_engine = create_engine(database_url, **engine_kwargs)
|
||||
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
|
||||
|
||||
# 在这里,我们不再直接调用 create_all
|
||||
# Base.metadata.create_all(bind=_engine)
|
||||
|
||||
# 而是调用新的迁移函数,它会处理表的创建和列的添加
|
||||
check_and_migrate_database()
|
||||
|
||||
logger.info(f"SQLAlchemy数据库初始化成功: {config.database_type}")
|
||||
return _engine, _SessionLocal
|
||||
|
||||
# ... (其他代码)
|
||||
```
|
||||
|
||||
通过这样的修改,我们就可以在不改变现有初始化流程入口的情况下,无缝地集成自动化的数据库结构检查和修复功能。
|
||||
@@ -525,8 +525,9 @@ def initialize_database():
|
||||
_engine = create_engine(database_url, **engine_kwargs)
|
||||
_SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
|
||||
|
||||
# 创建所有表
|
||||
Base.metadata.create_all(bind=_engine)
|
||||
# 调用新的迁移函数,它会处理表的创建和列的添加
|
||||
from src.common.database.db_migration import check_and_migrate_database
|
||||
check_and_migrate_database()
|
||||
|
||||
logger.info(f"SQLAlchemy数据库初始化成功: {config.database_type}")
|
||||
return _engine, _SessionLocal
|
||||
@@ -540,7 +541,7 @@ def get_db_session():
|
||||
_, SessionLocal = initialize_database()
|
||||
session = SessionLocal()
|
||||
yield session
|
||||
# session.commit()
|
||||
#session.commit()
|
||||
except Exception:
|
||||
if session:
|
||||
session.rollback()
|
||||
|
||||
@@ -114,6 +114,9 @@ class ModelTaskConfig(ConfigBase):
|
||||
replyer_2: TaskConfig
|
||||
"""normal_chat次要回复模型配置"""
|
||||
|
||||
maizone : TaskConfig
|
||||
"""maizone专用模型"""
|
||||
|
||||
emotion: TaskConfig
|
||||
"""情绪模型配置"""
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ class MaiZoneRefactoredPlugin(BasePlugin):
|
||||
config_schema: dict = {
|
||||
"plugin": {"enable": ConfigField(type=bool, default=True, description="是否启用插件")},
|
||||
"models": {
|
||||
"text_model": ConfigField(type=str, default="replyer_1", description="生成文本的模型名称"),
|
||||
"text_model": ConfigField(type=str, default="maizone", description="生成文本的模型名称"),
|
||||
"siliconflow_apikey": ConfigField(type=str, default="", description="硅基流动AI生图API密钥"),
|
||||
},
|
||||
"send": {
|
||||
|
||||
@@ -55,7 +55,6 @@ class MonitorService:
|
||||
|
||||
interval_minutes = self.get_config("monitor.interval_minutes", 10)
|
||||
|
||||
logger.info("开始执行好友动态监控...")
|
||||
await self.qzone_service.monitor_feeds()
|
||||
|
||||
logger.info(f"本轮监控完成,将在 {interval_minutes} 分钟后进行下一次检查。")
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
QQ空间服务模块
|
||||
封装了所有与QQ空间API的直接交互,是插件的核心业务逻辑层。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
@@ -29,6 +30,7 @@ class QZoneService:
|
||||
"""
|
||||
QQ空间服务类,负责所有API交互和业务流程编排。
|
||||
"""
|
||||
|
||||
# --- API Endpoints ---
|
||||
ZONE_LIST_URL = "https://user.qzone.qq.com/proxy/domain/ic2.qzone.qq.com/cgi-bin/feeds/feeds3_html_more"
|
||||
EMOTION_PUBLISH_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_publish_v6"
|
||||
@@ -37,8 +39,13 @@ class QZoneService:
|
||||
LIST_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qq.com/cgi-bin/emotion_cgi_msglist_v6"
|
||||
REPLY_URL = "https://user.qzone.qq.com/proxy/domain/taotao.qzone.qq.com/cgi-bin/emotion_cgi_re_feeds"
|
||||
|
||||
|
||||
def __init__(self, get_config: Callable, content_service: ContentService, image_service: ImageService, cookie_service: CookieService):
|
||||
def __init__(
|
||||
self,
|
||||
get_config: Callable,
|
||||
content_service: ContentService,
|
||||
image_service: ImageService,
|
||||
cookie_service: CookieService,
|
||||
):
|
||||
self.get_config = get_config
|
||||
self.content_service = content_service
|
||||
self.image_service = image_service
|
||||
@@ -53,7 +60,7 @@ class QZoneService:
|
||||
return {"success": False, "message": "生成说说内容失败"}
|
||||
|
||||
await self.image_service.generate_images_for_story(story)
|
||||
|
||||
|
||||
qq_account = config_api.get_global_config("bot.qq_account", "")
|
||||
api_client = await self._get_api_client(qq_account, stream_id)
|
||||
if not api_client:
|
||||
@@ -78,7 +85,7 @@ class QZoneService:
|
||||
return {"success": False, "message": "根据活动生成说说内容失败"}
|
||||
|
||||
await self.image_service.generate_images_for_story(story)
|
||||
|
||||
|
||||
qq_account = config_api.get_global_config("bot.qq_account", "")
|
||||
# 注意:定时任务通常在后台运行,没有特定的用户会话,因此 stream_id 为 None
|
||||
api_client = await self._get_api_client(qq_account, stream_id=None)
|
||||
@@ -116,7 +123,7 @@ class QZoneService:
|
||||
feeds = await api_client["list_feeds"](target_qq, num_to_read)
|
||||
if not feeds:
|
||||
return {"success": True, "message": f"没有从'{target_name}'的空间获取到新说说。"}
|
||||
|
||||
|
||||
for feed in feeds:
|
||||
await self._process_single_feed(feed, api_client, target_qq, target_name)
|
||||
await asyncio.sleep(random.uniform(3, 7))
|
||||
@@ -136,11 +143,11 @@ class QZoneService:
|
||||
return
|
||||
|
||||
try:
|
||||
feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态
|
||||
feeds = await api_client["monitor_list_feeds"](20) # 监控时检查最近20条动态
|
||||
if not feeds:
|
||||
logger.info("监控完成:未发现新说说")
|
||||
return
|
||||
|
||||
|
||||
logger.info(f"监控任务: 发现 {len(feeds)} 条新动态,准备处理...")
|
||||
for feed in feeds:
|
||||
target_qq = feed.get("target_qq")
|
||||
@@ -153,7 +160,7 @@ class QZoneService:
|
||||
await self._reply_to_own_feed_comments(feed, api_client)
|
||||
else:
|
||||
await self._process_single_feed(feed, api_client, target_qq, target_qq)
|
||||
|
||||
|
||||
await asyncio.sleep(random.uniform(5, 10))
|
||||
except Exception as e:
|
||||
logger.error(f"监控好友动态时发生异常: {e}", exc_info=True)
|
||||
@@ -171,24 +178,31 @@ class QZoneService:
|
||||
return
|
||||
|
||||
# 筛选出未被自己回复过的主评论
|
||||
my_comment_tids = {c['parent_tid'] for c in comments if c.get('parent_tid') and c.get('qq_account') == qq_account}
|
||||
comments_to_reply = [c for c in comments if not c.get('parent_tid') and c.get('comment_tid') not in my_comment_tids]
|
||||
my_comment_tids = {
|
||||
c["parent_tid"] for c in comments if c.get("parent_tid") and c.get("qq_account") == qq_account
|
||||
}
|
||||
comments_to_reply = [
|
||||
c for c in comments if not c.get("parent_tid") and c.get("comment_tid") not in my_comment_tids
|
||||
]
|
||||
|
||||
if not comments_to_reply:
|
||||
return
|
||||
|
||||
logger.info(f"发现自己说说下的 {len(comments_to_reply)} 条新评论,准备回复...")
|
||||
for comment in comments_to_reply:
|
||||
reply_content = await self.content_service.generate_comment_reply(content, comment.get('content', ''), comment.get('nickname', ''))
|
||||
reply_content = await self.content_service.generate_comment_reply(
|
||||
content, comment.get("content", ""), comment.get("nickname", "")
|
||||
)
|
||||
if reply_content:
|
||||
success = await api_client["reply"](fid, qq_account, comment.get('nickname', ''), reply_content, comment.get('comment_tid'))
|
||||
success = await api_client["reply"](
|
||||
fid, qq_account, comment.get("nickname", ""), reply_content, comment.get("comment_tid")
|
||||
)
|
||||
if success:
|
||||
logger.info(f"成功回复'{comment.get('nickname', '')}'的评论: '{reply_content}'")
|
||||
else:
|
||||
logger.error(f"回复'{comment.get('nickname', '')}'的评论失败")
|
||||
await asyncio.sleep(random.uniform(10, 20))
|
||||
|
||||
|
||||
async def _process_single_feed(self, feed: Dict, api_client: Dict, target_qq: str, target_name: str):
|
||||
"""处理单条说说,决定是否评论和点赞"""
|
||||
content = feed.get("content", "")
|
||||
@@ -207,7 +221,7 @@ class QZoneService:
|
||||
images = []
|
||||
if not os.path.exists(image_dir):
|
||||
return images
|
||||
|
||||
|
||||
try:
|
||||
files = sorted([f for f in os.listdir(image_dir) if os.path.isfile(os.path.join(image_dir, f))])
|
||||
for filename in files:
|
||||
@@ -228,21 +242,26 @@ class QZoneService:
|
||||
|
||||
async def _get_api_client(self, qq_account: str, stream_id: Optional[str]) -> Optional[Dict]:
|
||||
cookies = await self.cookie_service.get_cookies(qq_account, stream_id)
|
||||
if not cookies: return None
|
||||
|
||||
p_skey = cookies.get('p_skey') or cookies.get('p_skey'.upper())
|
||||
if not p_skey: return None
|
||||
|
||||
if not cookies:
|
||||
return None
|
||||
|
||||
p_skey = cookies.get("p_skey") or cookies.get("p_skey".upper())
|
||||
if not p_skey:
|
||||
return None
|
||||
|
||||
gtk = self._generate_gtk(p_skey)
|
||||
uin = cookies.get('uin', '').lstrip('o')
|
||||
|
||||
uin = cookies.get("uin", "").lstrip("o")
|
||||
|
||||
async def _request(method, url, params=None, data=None, headers=None):
|
||||
final_headers = {'referer': f'https://user.qzone.qq.com/{uin}', 'origin': 'https://user.qzone.qq.com'}
|
||||
if headers: final_headers.update(headers)
|
||||
|
||||
final_headers = {"referer": f"https://user.qzone.qq.com/{uin}", "origin": "https://user.qzone.qq.com"}
|
||||
if headers:
|
||||
final_headers.update(headers)
|
||||
|
||||
async with aiohttp.ClientSession(cookies=cookies) as session:
|
||||
timeout = aiohttp.ClientTimeout(total=20)
|
||||
async with session.request(method, url, params=params, data=data, headers=final_headers, timeout=timeout) as response:
|
||||
async with session.request(
|
||||
method, url, params=params, data=data, headers=final_headers, timeout=timeout
|
||||
) as response:
|
||||
response.raise_for_status()
|
||||
return await response.text()
|
||||
|
||||
@@ -250,11 +269,18 @@ class QZoneService:
|
||||
"""发布说说"""
|
||||
try:
|
||||
post_data = {
|
||||
"syn_tweet_verson": "1", "paramstr": "1", "who": "1",
|
||||
"con": content, "feedversion": "1", "ver": "1",
|
||||
"ugc_right": "1", "to_sign": "0", "hostuin": uin,
|
||||
"code_version": "1", "format": "json",
|
||||
"qzreferrer": f"https://user.qzone.qq.com/{uin}"
|
||||
"syn_tweet_verson": "1",
|
||||
"paramstr": "1",
|
||||
"who": "1",
|
||||
"con": content,
|
||||
"feedversion": "1",
|
||||
"ver": "1",
|
||||
"ugc_right": "1",
|
||||
"to_sign": "0",
|
||||
"hostuin": uin,
|
||||
"code_version": "1",
|
||||
"format": "json",
|
||||
"qzreferrer": f"https://user.qzone.qq.com/{uin}",
|
||||
}
|
||||
if images:
|
||||
pic_bos, richvals = [], []
|
||||
@@ -265,17 +291,17 @@ class QZoneService:
|
||||
# This is a placeholder for the actual image upload logic which is quite complex.
|
||||
# In a real scenario, you would call a dedicated `_upload_image` method here.
|
||||
# For now, we assume the upload is successful and we get back dummy data.
|
||||
pass # Simplified for this example
|
||||
pass # Simplified for this example
|
||||
|
||||
# Dummy data for illustration
|
||||
if images:
|
||||
post_data['pic_bo'] = 'dummy_pic_bo'
|
||||
post_data['richtype'] = '1'
|
||||
post_data['richval'] = 'dummy_rich_val'
|
||||
post_data["pic_bo"] = "dummy_pic_bo"
|
||||
post_data["richtype"] = "1"
|
||||
post_data["richval"] = "dummy_rich_val"
|
||||
|
||||
res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={'g_tk': gtk}, data=post_data)
|
||||
res_text = await _request("POST", self.EMOTION_PUBLISH_URL, params={"g_tk": gtk}, data=post_data)
|
||||
result = json.loads(res_text)
|
||||
tid = result.get('tid', '')
|
||||
tid = result.get("tid", "")
|
||||
return bool(tid), tid
|
||||
except Exception as e:
|
||||
logger.error(f"发布说说异常: {e}", exc_info=True)
|
||||
@@ -285,27 +311,44 @@ class QZoneService:
|
||||
"""获取指定用户说说列表"""
|
||||
try:
|
||||
params = {
|
||||
'g_tk': gtk, "uin": t_qq, "ftype": 0, "sort": 0, "pos": 0,
|
||||
"num": num, "replynum": 100, "callback": "_preloadCallback",
|
||||
"code_version": 1, "format": "jsonp", "need_comment": 1
|
||||
"g_tk": gtk,
|
||||
"uin": t_qq,
|
||||
"ftype": 0,
|
||||
"sort": 0,
|
||||
"pos": 0,
|
||||
"num": num,
|
||||
"replynum": 100,
|
||||
"callback": "_preloadCallback",
|
||||
"code_version": 1,
|
||||
"format": "jsonp",
|
||||
"need_comment": 1,
|
||||
}
|
||||
res_text = await _request("GET", self.LIST_URL, params=params)
|
||||
json_str = res_text[len('_preloadCallback('):-2]
|
||||
json_str = res_text[len("_preloadCallback(") : -2]
|
||||
json_data = json.loads(json_str)
|
||||
|
||||
if json_data.get('code') != 0: return []
|
||||
if json_data.get("code") != 0:
|
||||
return []
|
||||
|
||||
feeds_list = []
|
||||
my_name = json_data.get('logininfo', {}).get('name', '')
|
||||
my_name = json_data.get("logininfo", {}).get("name", "")
|
||||
for msg in json_data.get("msglist", []):
|
||||
is_commented = any(c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict))
|
||||
is_commented = any(
|
||||
c.get("name") == my_name for c in msg.get("commentlist", []) if isinstance(c, dict)
|
||||
)
|
||||
if not is_commented:
|
||||
feeds_list.append({
|
||||
"tid": msg.get("tid", ""),
|
||||
"content": msg.get("content", ""),
|
||||
"created_time": time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(msg.get("created_time", 0))),
|
||||
"rt_con": msg.get("rt_con", {}).get("content", "") if isinstance(msg.get("rt_con"), dict) else ""
|
||||
})
|
||||
feeds_list.append(
|
||||
{
|
||||
"tid": msg.get("tid", ""),
|
||||
"content": msg.get("content", ""),
|
||||
"created_time": time.strftime(
|
||||
"%Y-%m-%d %H:%M:%S", time.localtime(msg.get("created_time", 0))
|
||||
),
|
||||
"rt_con": msg.get("rt_con", {}).get("content", "")
|
||||
if isinstance(msg.get("rt_con"), dict)
|
||||
else "",
|
||||
}
|
||||
)
|
||||
return feeds_list
|
||||
except Exception as e:
|
||||
logger.error(f"获取说说列表失败: {e}", exc_info=True)
|
||||
@@ -315,9 +358,15 @@ class QZoneService:
|
||||
"""评论说说"""
|
||||
try:
|
||||
data = {
|
||||
"topicId": f'{t_qq}_{feed_id}__1', "uin": uin, "hostUin": t_qq,
|
||||
"content": text, "format": "fs", "plat": "qzone", "source": "ic",
|
||||
"platformid": 52, "ref": "feeds"
|
||||
"topicId": f"{t_qq}_{feed_id}__1",
|
||||
"uin": uin,
|
||||
"hostUin": t_qq,
|
||||
"content": text,
|
||||
"format": "fs",
|
||||
"plat": "qzone",
|
||||
"source": "ic",
|
||||
"platformid": 52,
|
||||
"ref": "feeds",
|
||||
}
|
||||
await _request("POST", self.COMMENT_URL, params={"g_tk": gtk}, data=data)
|
||||
return True
|
||||
@@ -329,17 +378,24 @@ class QZoneService:
|
||||
"""点赞说说"""
|
||||
try:
|
||||
data = {
|
||||
'opuin': uin, 'unikey': f'http://user.qzone.qq.com/{t_qq}/mood/{feed_id}',
|
||||
'curkey': f'http://user.qzone.qq.com/{t_qq}/mood/{feed_id}',
|
||||
'from': 1, 'appid': 311, 'typeid': 0, 'abstime': int(time.time()),
|
||||
'fid': feed_id, 'active': 0, 'format': 'json', 'fupdate': 1
|
||||
"opuin": uin,
|
||||
"unikey": f"http://user.qzone.qq.com/{t_qq}/mood/{feed_id}",
|
||||
"curkey": f"http://user.qzone.qq.com/{t_qq}/mood/{feed_id}",
|
||||
"from": 1,
|
||||
"appid": 311,
|
||||
"typeid": 0,
|
||||
"abstime": int(time.time()),
|
||||
"fid": feed_id,
|
||||
"active": 0,
|
||||
"format": "json",
|
||||
"fupdate": 1,
|
||||
}
|
||||
await _request("POST", self.DOLIKE_URL, params={'g_tk': gtk}, data=data)
|
||||
await _request("POST", self.DOLIKE_URL, params={"g_tk": gtk}, data=data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"点赞说说异常: {e}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
async def _reply(fid, host_qq, target_name, content, comment_tid):
|
||||
"""回复评论"""
|
||||
try:
|
||||
@@ -355,7 +411,7 @@ class QZoneService:
|
||||
"ref": "feeds",
|
||||
"richtype": "",
|
||||
"richval": "",
|
||||
"paramstr": f"@{target_name} {content}"
|
||||
"paramstr": f"@{target_name} {content}",
|
||||
}
|
||||
await _request("POST", self.REPLY_URL, params={"g_tk": gtk}, data=data)
|
||||
return True
|
||||
@@ -367,38 +423,55 @@ class QZoneService:
|
||||
"""监控好友动态"""
|
||||
try:
|
||||
params = {
|
||||
"uin": uin, "scope": 0, "view": 1, "filter": "all", "flag": 1,
|
||||
"applist": "all", "pagenum": 1, "count": num, "format": "json",
|
||||
"g_tk": gtk, "useutf8": 1, "outputhtmlfeed": 1
|
||||
"uin": uin,
|
||||
"scope": 0,
|
||||
"view": 1,
|
||||
"filter": "all",
|
||||
"flag": 1,
|
||||
"applist": "all",
|
||||
"pagenum": 1,
|
||||
"count": num,
|
||||
"format": "json",
|
||||
"g_tk": gtk,
|
||||
"useutf8": 1,
|
||||
"outputhtmlfeed": 1,
|
||||
}
|
||||
res_text = await _request("GET", self.ZONE_LIST_URL, params=params)
|
||||
json_str = res_text[len('_Callback('):-2].replace('undefined', 'null')
|
||||
|
||||
# 增加对返回内容的校验
|
||||
if not res_text.startswith("_Callback(") or not res_text.endswith(");"):
|
||||
logger.warning(f"监控好友动态返回格式异常: {res_text}")
|
||||
return []
|
||||
|
||||
json_str = res_text[len("_Callback(") : -2].replace("undefined", "null")
|
||||
json_data = json5.loads(json_str)
|
||||
feeds_data = []
|
||||
if isinstance(json_data, dict):
|
||||
data_level1 = json_data.get('data')
|
||||
data_level1 = json_data.get("data")
|
||||
if isinstance(data_level1, dict):
|
||||
feeds_data = data_level1.get('data', [])
|
||||
|
||||
feeds_data = data_level1.get("data", [])
|
||||
|
||||
feeds_list = []
|
||||
for feed in feeds_data:
|
||||
if str(feed.get('appid', '')) != '311' or str(feed.get('uin', '')) == str(uin):
|
||||
continue
|
||||
|
||||
html_content = feed.get('html', '')
|
||||
soup = bs4.BeautifulSoup(html_content, 'html.parser')
|
||||
like_btn = soup.find('a', class_='qz_like_btn_v3')
|
||||
if isinstance(like_btn, bs4.element.Tag) and like_btn.get('data-islike') == '1':
|
||||
if str(feed.get("appid", "")) != "311" or str(feed.get("uin", "")) == str(uin):
|
||||
continue
|
||||
|
||||
text_div = soup.find('div', class_='f-info')
|
||||
html_content = feed.get("html", "")
|
||||
soup = bs4.BeautifulSoup(html_content, "html.parser")
|
||||
like_btn = soup.find("a", class_="qz_like_btn_v3")
|
||||
if isinstance(like_btn, bs4.element.Tag) and like_btn.get("data-islike") == "1":
|
||||
continue
|
||||
|
||||
text_div = soup.find("div", class_="f-info")
|
||||
text = text_div.get_text(strip=True) if text_div else ""
|
||||
|
||||
feeds_list.append({
|
||||
'target_qq': feed.get('uin'),
|
||||
'tid': feed.get('key'),
|
||||
'content': text,
|
||||
})
|
||||
|
||||
feeds_list.append(
|
||||
{
|
||||
"target_qq": feed.get("uin"),
|
||||
"tid": feed.get("key"),
|
||||
"content": text,
|
||||
}
|
||||
)
|
||||
return feeds_list
|
||||
except Exception as e:
|
||||
logger.error(f"监控好友动态失败: {e}", exc_info=True)
|
||||
@@ -411,4 +484,4 @@ class QZoneService:
|
||||
"like": _like,
|
||||
"reply": _reply,
|
||||
"monitor_list_feeds": _monitor_list_feeds,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[inner]
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
|
||||
# 配置文件版本号迭代规则同bot_config.toml
|
||||
|
||||
@@ -145,6 +145,11 @@ model_list = ["siliconflow-deepseek-v3"]
|
||||
temperature = 0.3
|
||||
max_tokens = 800
|
||||
|
||||
[model_task_config.maizone] # maizone模型
|
||||
model_list = ["siliconflow-deepseek-v3"]
|
||||
temperature = 0.7
|
||||
max_tokens = 800
|
||||
|
||||
[model_task_config.vlm] # 图像识别模型
|
||||
model_list = ["qwen2.5-vl-72b"]
|
||||
max_tokens = 800
|
||||
|
||||
Reference in New Issue
Block a user