vibego 0.2.50__py3-none-any.whl → 0.2.52__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bot.py +419 -18
- {vibego-0.2.50.dist-info → vibego-0.2.52.dist-info}/METADATA +1 -1
- {vibego-0.2.50.dist-info → vibego-0.2.52.dist-info}/RECORD +7 -7
- vibego_cli/__init__.py +1 -1
- {vibego-0.2.50.dist-info → vibego-0.2.52.dist-info}/WHEEL +0 -0
- {vibego-0.2.50.dist-info → vibego-0.2.52.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.50.dist-info → vibego-0.2.52.dist-info}/top_level.txt +0 -0
bot.py
CHANGED
|
@@ -37,6 +37,8 @@ from aiogram.types import (
|
|
|
37
37
|
ReplyKeyboardRemove,
|
|
38
38
|
Update,
|
|
39
39
|
User,
|
|
40
|
+
PhotoSize,
|
|
41
|
+
Document,
|
|
40
42
|
)
|
|
41
43
|
from aiogram.client.session.aiohttp import AiohttpSession
|
|
42
44
|
from aiogram.enums import ParseMode
|
|
@@ -85,6 +87,15 @@ def load_env(p: str = ".env"):
|
|
|
85
87
|
|
|
86
88
|
load_env()
|
|
87
89
|
|
|
90
|
+
# 导入媒体处理器
|
|
91
|
+
try:
|
|
92
|
+
from media_handler import MediaHandler, MediaConfig
|
|
93
|
+
HAS_MEDIA_HANDLER = True
|
|
94
|
+
except ImportError:
|
|
95
|
+
HAS_MEDIA_HANDLER = False
|
|
96
|
+
MediaHandler = None
|
|
97
|
+
MediaConfig = None
|
|
98
|
+
|
|
88
99
|
# --- 日志 & 上下文 ---
|
|
89
100
|
PROJECT_NAME = os.environ.get("PROJECT_NAME", "").strip()
|
|
90
101
|
ACTIVE_MODEL = (os.environ.get("ACTIVE_MODEL") or os.environ.get("MODEL_NAME") or "").strip()
|
|
@@ -1160,6 +1171,7 @@ async def _push_task_to_model(
|
|
|
1160
1171
|
supplement: Optional[str],
|
|
1161
1172
|
actor: Optional[str],
|
|
1162
1173
|
is_bug_report: bool = False,
|
|
1174
|
+
media_files: Optional[List[Dict[str, Any]]] = None,
|
|
1163
1175
|
) -> tuple[bool, str, Optional[Path]]:
|
|
1164
1176
|
"""推送任务信息到模型,并附带补充描述。
|
|
1165
1177
|
|
|
@@ -1170,17 +1182,30 @@ async def _push_task_to_model(
|
|
|
1170
1182
|
supplement: 补充描述
|
|
1171
1183
|
actor: 操作者
|
|
1172
1184
|
is_bug_report: 是否为缺陷报告推送
|
|
1185
|
+
media_files: 媒体文件列表
|
|
1173
1186
|
"""
|
|
1174
1187
|
|
|
1175
1188
|
history_text, history_count = await _build_history_context_for_model(task.id)
|
|
1176
1189
|
notes = await TASK_SERVICE.list_notes(task.id)
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1190
|
+
|
|
1191
|
+
# 使用增强版函数(如果有媒体文件)
|
|
1192
|
+
if media_files:
|
|
1193
|
+
prompt = _build_model_push_payload_with_media(
|
|
1194
|
+
task,
|
|
1195
|
+
supplement=supplement,
|
|
1196
|
+
history=history_text,
|
|
1197
|
+
notes=notes,
|
|
1198
|
+
media_files=media_files,
|
|
1199
|
+
is_bug_report=is_bug_report,
|
|
1200
|
+
)
|
|
1201
|
+
else:
|
|
1202
|
+
prompt = _build_model_push_payload(
|
|
1203
|
+
task,
|
|
1204
|
+
supplement=supplement,
|
|
1205
|
+
history=history_text,
|
|
1206
|
+
notes=notes,
|
|
1207
|
+
is_bug_report=is_bug_report,
|
|
1208
|
+
)
|
|
1184
1209
|
success, session_path = await _dispatch_prompt_to_model(
|
|
1185
1210
|
chat_id,
|
|
1186
1211
|
prompt,
|
|
@@ -1998,6 +2023,57 @@ def _build_model_push_payload(
|
|
|
1998
2023
|
return _strip_legacy_bug_header(result or body)
|
|
1999
2024
|
|
|
2000
2025
|
|
|
2026
|
+
def _build_model_push_payload_with_media(
|
|
2027
|
+
task: TaskRecord,
|
|
2028
|
+
supplement: Optional[str] = None,
|
|
2029
|
+
history: Optional[str] = None,
|
|
2030
|
+
notes: Optional[Sequence[TaskNoteRecord]] = None,
|
|
2031
|
+
media_files: Optional[List[Dict[str, Any]]] = None,
|
|
2032
|
+
is_bug_report: bool = False,
|
|
2033
|
+
) -> str:
|
|
2034
|
+
"""构造包含媒体文件引用的模型推送 payload"""
|
|
2035
|
+
|
|
2036
|
+
# 先构造基础 payload
|
|
2037
|
+
base_payload = _build_model_push_payload(
|
|
2038
|
+
task=task,
|
|
2039
|
+
supplement=supplement,
|
|
2040
|
+
history=history,
|
|
2041
|
+
notes=notes,
|
|
2042
|
+
is_bug_report=is_bug_report
|
|
2043
|
+
)
|
|
2044
|
+
|
|
2045
|
+
# 如果没有媒体文件,直接返回
|
|
2046
|
+
if not media_files:
|
|
2047
|
+
return base_payload
|
|
2048
|
+
|
|
2049
|
+
# 添加媒体文件信息
|
|
2050
|
+
lines = [base_payload]
|
|
2051
|
+
lines.append("\n相关文件:")
|
|
2052
|
+
|
|
2053
|
+
for idx, media in enumerate(media_files, 1):
|
|
2054
|
+
file_type = media["type"]
|
|
2055
|
+
file_path = media["file_path"]
|
|
2056
|
+
|
|
2057
|
+
if file_type == "image":
|
|
2058
|
+
width = media.get("width", "?")
|
|
2059
|
+
height = media.get("height", "?")
|
|
2060
|
+
lines.append(f"{idx}. 图片 ({width}×{height}): {file_path}")
|
|
2061
|
+
elif file_type == "document":
|
|
2062
|
+
original_name = media.get("original_name", "未命名文档")
|
|
2063
|
+
size_mb = media.get("size", 0) / 1024 / 1024
|
|
2064
|
+
lines.append(f"{idx}. 文档 [{original_name}] ({size_mb:.1f}MB): {file_path}")
|
|
2065
|
+
|
|
2066
|
+
# 为不同模型添加特定指令
|
|
2067
|
+
if _is_claudecode_model():
|
|
2068
|
+
lines.append("\n请使用 Read 工具查看上述文件以理解其内容。")
|
|
2069
|
+
elif MODEL_CANONICAL_NAME == "codex":
|
|
2070
|
+
lines.append("\n请分析上述文件并结合任务要求进行处理。")
|
|
2071
|
+
elif "gemini" in MODEL_CANONICAL_NAME:
|
|
2072
|
+
lines.append("\n请查看并分析上述文件。")
|
|
2073
|
+
|
|
2074
|
+
return "\n".join(lines)
|
|
2075
|
+
|
|
2076
|
+
|
|
2001
2077
|
try:
|
|
2002
2078
|
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
|
|
2003
2079
|
except ZoneInfoNotFoundError:
|
|
@@ -2975,6 +3051,61 @@ def _collect_message_payload(message: Message) -> str:
|
|
|
2975
3051
|
return "\n".join(parts).strip()
|
|
2976
3052
|
|
|
2977
3053
|
|
|
3054
|
+
async def _collect_message_payload_with_media(
|
|
3055
|
+
message: Message,
|
|
3056
|
+
task_id: Optional[str] = None
|
|
3057
|
+
) -> Tuple[str, List[Dict[str, Any]], Optional[str]]:
|
|
3058
|
+
"""
|
|
3059
|
+
收集消息内容和媒体文件(增强版)
|
|
3060
|
+
返回: (文本内容, 媒体文件列表, 用户提示消息)
|
|
3061
|
+
"""
|
|
3062
|
+
parts = []
|
|
3063
|
+
media_files = []
|
|
3064
|
+
user_feedback = None
|
|
3065
|
+
|
|
3066
|
+
# 处理文本
|
|
3067
|
+
text = _normalize_choice_token(message.text or message.caption)
|
|
3068
|
+
if text:
|
|
3069
|
+
parts.append(text)
|
|
3070
|
+
|
|
3071
|
+
# 处理媒体文件
|
|
3072
|
+
if media_handler and (message.photo or message.document):
|
|
3073
|
+
media_info, feedback_msg = await media_handler.process_message_media(
|
|
3074
|
+
message, task_id
|
|
3075
|
+
)
|
|
3076
|
+
|
|
3077
|
+
if media_info["type"] not in ["none", "error"]:
|
|
3078
|
+
media_files.append(media_info)
|
|
3079
|
+
# 添加文件引用到文本
|
|
3080
|
+
if media_info["type"] == "image":
|
|
3081
|
+
parts.append(f"[图片: {media_info['file_path']}]")
|
|
3082
|
+
elif media_info["type"] == "document":
|
|
3083
|
+
parts.append(f"[文档: {media_info['file_path']}]")
|
|
3084
|
+
elif media_info["type"] == "error":
|
|
3085
|
+
# 添加错误信息到文本
|
|
3086
|
+
parts.append(f"[媒体处理失败: {media_info.get('error', '未知错误')}]")
|
|
3087
|
+
|
|
3088
|
+
# 设置用户反馈
|
|
3089
|
+
user_feedback = feedback_msg
|
|
3090
|
+
else:
|
|
3091
|
+
# 没有媒体处理器时,使用原有逻辑
|
|
3092
|
+
if message.photo:
|
|
3093
|
+
file_id = message.photo[-1].file_id
|
|
3094
|
+
parts.append(f"[图片:{file_id}]")
|
|
3095
|
+
if message.document:
|
|
3096
|
+
doc = message.document
|
|
3097
|
+
name = doc.file_name or doc.file_id
|
|
3098
|
+
parts.append(f"[文件:{name}]")
|
|
3099
|
+
|
|
3100
|
+
# 处理其他媒体类型(保持原有逻辑)
|
|
3101
|
+
if message.voice:
|
|
3102
|
+
parts.append(f"[语音: {message.voice.file_id}]")
|
|
3103
|
+
if message.video:
|
|
3104
|
+
parts.append(f"[视频: {message.video.file_id}]")
|
|
3105
|
+
|
|
3106
|
+
return "\n".join(parts).strip(), media_files, user_feedback
|
|
3107
|
+
|
|
3108
|
+
|
|
2978
3109
|
def _summarize_note_text(value: str) -> str:
|
|
2979
3110
|
"""压缩备注内容,维持主要信息并控制长度。"""
|
|
2980
3111
|
|
|
@@ -3431,6 +3562,9 @@ def tmux_capture_since(log_path: Path | str, start_pos: int, idle: float = 2.0,
|
|
|
3431
3562
|
return "".join(buf)
|
|
3432
3563
|
|
|
3433
3564
|
|
|
3565
|
+
# 全局媒体处理器
|
|
3566
|
+
media_handler: Optional[MediaHandler] = None
|
|
3567
|
+
|
|
3434
3568
|
SESSION_OFFSETS: Dict[str, int] = {}
|
|
3435
3569
|
CHAT_SESSION_MAP: Dict[int, str] = {}
|
|
3436
3570
|
CHAT_WATCHERS: Dict[int, asyncio.Task] = {}
|
|
@@ -5177,6 +5311,7 @@ async def on_help_command(message: Message) -> None:
|
|
|
5177
5311
|
"*指令总览*\n"
|
|
5178
5312
|
"- /help — 查看全部命令\n"
|
|
5179
5313
|
"- /tasks — 任务管理命令清单\n"
|
|
5314
|
+
"- /media — 查看媒体文件统计和管理\n"
|
|
5180
5315
|
"- /task_new — 创建任务(交互式或附带参数)\n"
|
|
5181
5316
|
"- /task_list — 查看任务列表,支持 status/limit/offset\n"
|
|
5182
5317
|
"- /task_show — 查看某个任务详情\n"
|
|
@@ -5205,6 +5340,158 @@ async def on_tasks_help(message: Message) -> None:
|
|
|
5205
5340
|
await _answer_with_markdown(message, text)
|
|
5206
5341
|
|
|
5207
5342
|
|
|
5343
|
+
@router.message(Command("media"))
|
|
5344
|
+
async def on_media_command(message: Message) -> None:
|
|
5345
|
+
"""处理 /media 命令,显示媒体文件统计和管理选项"""
|
|
5346
|
+
global media_handler
|
|
5347
|
+
|
|
5348
|
+
if not media_handler:
|
|
5349
|
+
await message.reply("❌ 媒体处理功能未初始化")
|
|
5350
|
+
return
|
|
5351
|
+
|
|
5352
|
+
try:
|
|
5353
|
+
# 获取媒体文件统计
|
|
5354
|
+
stats = await media_handler.get_stats()
|
|
5355
|
+
|
|
5356
|
+
# 构建统计信息文本
|
|
5357
|
+
lines = [
|
|
5358
|
+
"*📊 媒体文件统计*",
|
|
5359
|
+
"",
|
|
5360
|
+
f"📁 存储位置: `{stats['storage_path']}`",
|
|
5361
|
+
f"📷 图片文件: {stats['image_count']} 个",
|
|
5362
|
+
f"📄 文档文件: {stats['document_count']} 个",
|
|
5363
|
+
f"💾 总占用空间: {stats['total_size_mb']:.2f} MB",
|
|
5364
|
+
"",
|
|
5365
|
+
f"⏰ 普通文件保留: {stats['normal_retention_days']} 天",
|
|
5366
|
+
f"🎯 任务文件保留: {stats['task_retention_days']} 天",
|
|
5367
|
+
"",
|
|
5368
|
+
f"🗑️ 待清理文件: {stats['expired_count']} 个",
|
|
5369
|
+
]
|
|
5370
|
+
|
|
5371
|
+
if stats['expired_count'] > 0:
|
|
5372
|
+
lines.append(f" ({stats['expired_size_mb']:.2f} MB 可释放)")
|
|
5373
|
+
|
|
5374
|
+
# 最近的文件
|
|
5375
|
+
if stats['recent_files']:
|
|
5376
|
+
lines.extend([
|
|
5377
|
+
"",
|
|
5378
|
+
"*📝 最近下载的文件:*"
|
|
5379
|
+
])
|
|
5380
|
+
for file_info in stats['recent_files'][:5]:
|
|
5381
|
+
file_type = "📷" if file_info['type'] == 'image' else "📄"
|
|
5382
|
+
lines.append(f"{file_type} {file_info['name']} ({file_info['size_mb']:.2f} MB)")
|
|
5383
|
+
|
|
5384
|
+
text = "\n".join(lines)
|
|
5385
|
+
|
|
5386
|
+
# 创建管理按钮
|
|
5387
|
+
keyboard = InlineKeyboardMarkup(
|
|
5388
|
+
inline_keyboard=[
|
|
5389
|
+
[
|
|
5390
|
+
InlineKeyboardButton(text="🗑️ 立即清理过期文件", callback_data="media_cleanup"),
|
|
5391
|
+
InlineKeyboardButton(text="🔄 刷新统计", callback_data="media_refresh")
|
|
5392
|
+
]
|
|
5393
|
+
]
|
|
5394
|
+
)
|
|
5395
|
+
|
|
5396
|
+
await _answer_with_markdown(message, text, reply_markup=keyboard)
|
|
5397
|
+
|
|
5398
|
+
except Exception as e:
|
|
5399
|
+
worker_log.error(f"获取媒体统计失败: {e}", exc_info=True)
|
|
5400
|
+
await message.reply(f"❌ 获取媒体统计失败: {e}")
|
|
5401
|
+
|
|
5402
|
+
|
|
5403
|
+
@router.callback_query(F.data == "media_cleanup")
|
|
5404
|
+
async def on_media_cleanup_callback(callback: CallbackQuery) -> None:
|
|
5405
|
+
"""处理清理过期文件的回调"""
|
|
5406
|
+
global media_handler
|
|
5407
|
+
|
|
5408
|
+
if not media_handler:
|
|
5409
|
+
await callback.answer("❌ 媒体处理功能未初始化", show_alert=True)
|
|
5410
|
+
return
|
|
5411
|
+
|
|
5412
|
+
try:
|
|
5413
|
+
# 执行清理
|
|
5414
|
+
cleaned_count = await media_handler.cleanup_old_files()
|
|
5415
|
+
|
|
5416
|
+
if cleaned_count > 0:
|
|
5417
|
+
await callback.answer(f"✅ 已清理 {cleaned_count} 个过期文件", show_alert=True)
|
|
5418
|
+
else:
|
|
5419
|
+
await callback.answer("📭 没有需要清理的文件", show_alert=True)
|
|
5420
|
+
|
|
5421
|
+
# 更新统计信息
|
|
5422
|
+
await on_media_refresh_callback(callback)
|
|
5423
|
+
|
|
5424
|
+
except Exception as e:
|
|
5425
|
+
worker_log.error(f"清理媒体文件失败: {e}", exc_info=True)
|
|
5426
|
+
await callback.answer(f"❌ 清理失败: {e}", show_alert=True)
|
|
5427
|
+
|
|
5428
|
+
|
|
5429
|
+
@router.callback_query(F.data == "media_refresh")
|
|
5430
|
+
async def on_media_refresh_callback(callback: CallbackQuery) -> None:
|
|
5431
|
+
"""处理刷新统计的回调"""
|
|
5432
|
+
global media_handler
|
|
5433
|
+
|
|
5434
|
+
if not media_handler:
|
|
5435
|
+
await callback.answer("❌ 媒体处理功能未初始化", show_alert=True)
|
|
5436
|
+
return
|
|
5437
|
+
|
|
5438
|
+
try:
|
|
5439
|
+
# 获取最新统计
|
|
5440
|
+
stats = await media_handler.get_stats()
|
|
5441
|
+
|
|
5442
|
+
# 构建更新的文本
|
|
5443
|
+
lines = [
|
|
5444
|
+
"*📊 媒体文件统计*",
|
|
5445
|
+
"",
|
|
5446
|
+
f"📁 存储位置: `{stats['storage_path']}`",
|
|
5447
|
+
f"📷 图片文件: {stats['image_count']} 个",
|
|
5448
|
+
f"📄 文档文件: {stats['document_count']} 个",
|
|
5449
|
+
f"💾 总占用空间: {stats['total_size_mb']:.2f} MB",
|
|
5450
|
+
"",
|
|
5451
|
+
f"⏰ 普通文件保留: {stats['normal_retention_days']} 天",
|
|
5452
|
+
f"🎯 任务文件保留: {stats['task_retention_days']} 天",
|
|
5453
|
+
"",
|
|
5454
|
+
f"🗑️ 待清理文件: {stats['expired_count']} 个",
|
|
5455
|
+
]
|
|
5456
|
+
|
|
5457
|
+
if stats['expired_count'] > 0:
|
|
5458
|
+
lines.append(f" ({stats['expired_size_mb']:.2f} MB 可释放)")
|
|
5459
|
+
|
|
5460
|
+
# 最近的文件
|
|
5461
|
+
if stats['recent_files']:
|
|
5462
|
+
lines.extend([
|
|
5463
|
+
"",
|
|
5464
|
+
"*📝 最近下载的文件:*"
|
|
5465
|
+
])
|
|
5466
|
+
for file_info in stats['recent_files'][:5]:
|
|
5467
|
+
file_type = "📷" if file_info['type'] == 'image' else "📄"
|
|
5468
|
+
lines.append(f"{file_type} {file_info['name']} ({file_info['size_mb']:.2f} MB)")
|
|
5469
|
+
|
|
5470
|
+
text = "\n".join(lines)
|
|
5471
|
+
|
|
5472
|
+
# 保持按钮
|
|
5473
|
+
keyboard = InlineKeyboardMarkup(
|
|
5474
|
+
inline_keyboard=[
|
|
5475
|
+
[
|
|
5476
|
+
InlineKeyboardButton(text="🗑️ 立即清理过期文件", callback_data="media_cleanup"),
|
|
5477
|
+
InlineKeyboardButton(text="🔄 刷新统计", callback_data="media_refresh")
|
|
5478
|
+
]
|
|
5479
|
+
]
|
|
5480
|
+
)
|
|
5481
|
+
|
|
5482
|
+
await callback.message.edit_text(
|
|
5483
|
+
text,
|
|
5484
|
+
parse_mode="MarkdownV2" if _IS_MARKDOWN_V2 else "Markdown",
|
|
5485
|
+
reply_markup=keyboard
|
|
5486
|
+
)
|
|
5487
|
+
|
|
5488
|
+
await callback.answer("✅ 统计已更新")
|
|
5489
|
+
|
|
5490
|
+
except Exception as e:
|
|
5491
|
+
worker_log.error(f"刷新媒体统计失败: {e}", exc_info=True)
|
|
5492
|
+
await callback.answer(f"❌ 刷新失败: {e}", show_alert=True)
|
|
5493
|
+
|
|
5494
|
+
|
|
5208
5495
|
def _normalize_status(value: Optional[str]) -> Optional[str]:
|
|
5209
5496
|
if not value:
|
|
5210
5497
|
return None
|
|
@@ -6885,21 +7172,40 @@ async def on_task_bug_report(callback: CallbackQuery, state: FSMContext) -> None
|
|
|
6885
7172
|
|
|
6886
7173
|
@router.message(TaskBugReportStates.waiting_description)
|
|
6887
7174
|
async def on_task_bug_description(message: Message, state: FSMContext) -> None:
|
|
6888
|
-
"""
|
|
7175
|
+
"""处理缺陷描述输入(支持图片和文档)。"""
|
|
6889
7176
|
|
|
6890
7177
|
if _is_cancel_message(message.text):
|
|
6891
7178
|
await state.clear()
|
|
6892
7179
|
await message.answer("已取消缺陷上报。", reply_markup=_build_worker_main_keyboard())
|
|
6893
7180
|
return
|
|
6894
|
-
|
|
6895
|
-
|
|
7181
|
+
|
|
7182
|
+
# 获取任务 ID
|
|
7183
|
+
data = await state.get_data()
|
|
7184
|
+
task_id = data.get("task_id")
|
|
7185
|
+
|
|
7186
|
+
# 使用增强版收集函数(如果有媒体处理器)
|
|
7187
|
+
if media_handler:
|
|
7188
|
+
content, media_files, feedback = await _collect_message_payload_with_media(
|
|
7189
|
+
message, task_id
|
|
7190
|
+
)
|
|
7191
|
+
# 发送用户反馈
|
|
7192
|
+
if feedback:
|
|
7193
|
+
await message.answer(feedback)
|
|
7194
|
+
else:
|
|
7195
|
+
# 使用原有函数
|
|
7196
|
+
content = _collect_message_payload(message)
|
|
7197
|
+
media_files = []
|
|
7198
|
+
|
|
7199
|
+
if not content and not media_files:
|
|
6896
7200
|
await message.answer(
|
|
6897
7201
|
"缺陷描述不能为空,请重新输入:",
|
|
6898
7202
|
reply_markup=_build_description_keyboard(),
|
|
6899
7203
|
)
|
|
6900
7204
|
return
|
|
7205
|
+
|
|
6901
7206
|
await state.update_data(
|
|
6902
7207
|
description=content,
|
|
7208
|
+
media_files=media_files,
|
|
6903
7209
|
reporter=_actor_from_message(message),
|
|
6904
7210
|
)
|
|
6905
7211
|
await state.set_state(TaskBugReportStates.waiting_reproduction)
|
|
@@ -6908,18 +7214,36 @@ async def on_task_bug_description(message: Message, state: FSMContext) -> None:
|
|
|
6908
7214
|
|
|
6909
7215
|
@router.message(TaskBugReportStates.waiting_reproduction)
|
|
6910
7216
|
async def on_task_bug_reproduction(message: Message, state: FSMContext) -> None:
|
|
6911
|
-
"""
|
|
7217
|
+
"""处理复现步骤输入(支持图片)。"""
|
|
6912
7218
|
|
|
6913
7219
|
if _is_cancel_message(message.text):
|
|
6914
7220
|
await state.clear()
|
|
6915
7221
|
await message.answer("已取消缺陷上报。", reply_markup=_build_worker_main_keyboard())
|
|
6916
7222
|
return
|
|
7223
|
+
|
|
6917
7224
|
options = [SKIP_TEXT, "取消"]
|
|
6918
7225
|
resolved = _resolve_reply_choice(message.text or "", options=options)
|
|
6919
7226
|
reproduction = ""
|
|
7227
|
+
reproduction_media = []
|
|
7228
|
+
|
|
6920
7229
|
if resolved not in {SKIP_TEXT, "取消"}:
|
|
6921
|
-
|
|
6922
|
-
|
|
7230
|
+
# 获取任务 ID
|
|
7231
|
+
data = await state.get_data()
|
|
7232
|
+
task_id = data.get("task_id")
|
|
7233
|
+
|
|
7234
|
+
if media_handler:
|
|
7235
|
+
reproduction, reproduction_media, feedback = await _collect_message_payload_with_media(
|
|
7236
|
+
message, task_id
|
|
7237
|
+
)
|
|
7238
|
+
if feedback:
|
|
7239
|
+
await message.answer(feedback)
|
|
7240
|
+
else:
|
|
7241
|
+
reproduction = _collect_message_payload(message)
|
|
7242
|
+
|
|
7243
|
+
await state.update_data(
|
|
7244
|
+
reproduction=reproduction,
|
|
7245
|
+
reproduction_media=reproduction_media
|
|
7246
|
+
)
|
|
6923
7247
|
await state.set_state(TaskBugReportStates.waiting_logs)
|
|
6924
7248
|
await message.answer(_build_bug_log_prompt(), reply_markup=_build_description_keyboard())
|
|
6925
7249
|
|
|
@@ -6997,11 +7321,21 @@ async def on_task_bug_confirm(message: Message, state: FSMContext) -> None:
|
|
|
6997
7321
|
reproduction = data.get("reproduction", "")
|
|
6998
7322
|
logs = data.get("logs", "")
|
|
6999
7323
|
reporter = data.get("reporter") or _actor_from_message(message)
|
|
7324
|
+
|
|
7325
|
+
# 收集所有媒体文件
|
|
7326
|
+
all_media_files = []
|
|
7327
|
+
if "media_files" in data:
|
|
7328
|
+
all_media_files.extend(data["media_files"])
|
|
7329
|
+
if "reproduction_media" in data:
|
|
7330
|
+
all_media_files.extend(data["reproduction_media"])
|
|
7331
|
+
|
|
7000
7332
|
payload = {
|
|
7001
7333
|
"action": "bug_report",
|
|
7002
7334
|
"description_length": len(description),
|
|
7003
7335
|
"has_reproduction": bool(reproduction.strip()),
|
|
7004
7336
|
"has_logs": bool(logs.strip()),
|
|
7337
|
+
"has_media": len(all_media_files) > 0,
|
|
7338
|
+
"media_count": len(all_media_files),
|
|
7005
7339
|
"description": description,
|
|
7006
7340
|
"reproduction": reproduction,
|
|
7007
7341
|
"logs": logs,
|
|
@@ -7015,7 +7349,12 @@ async def on_task_bug_confirm(message: Message, state: FSMContext) -> None:
|
|
|
7015
7349
|
payload=payload,
|
|
7016
7350
|
)
|
|
7017
7351
|
await state.clear()
|
|
7018
|
-
await _auto_push_after_bug_report(
|
|
7352
|
+
await _auto_push_after_bug_report(
|
|
7353
|
+
task,
|
|
7354
|
+
message=message,
|
|
7355
|
+
actor=reporter,
|
|
7356
|
+
media_files=all_media_files
|
|
7357
|
+
)
|
|
7019
7358
|
|
|
7020
7359
|
|
|
7021
7360
|
@router.callback_query(F.data.startswith("task:add_note:"))
|
|
@@ -7408,21 +7747,53 @@ async def on_start(m: Message):
|
|
|
7408
7747
|
if ENV_ISSUES:
|
|
7409
7748
|
await m.answer(_format_env_issue_message())
|
|
7410
7749
|
|
|
7411
|
-
@router.message(F.text)
|
|
7750
|
+
@router.message(F.text | F.photo | F.document | F.caption)
|
|
7412
7751
|
async def on_text(m: Message):
|
|
7752
|
+
"""
|
|
7753
|
+
处理用户消息:支持纯文本、带图片和文档的消息
|
|
7754
|
+
- 纯文本消息:使用 m.text
|
|
7755
|
+
- 带媒体消息:使用 m.caption,并下载媒体文件
|
|
7756
|
+
"""
|
|
7413
7757
|
# 首次收到消息时自动记录 chat_id 到 state 文件
|
|
7414
7758
|
_auto_record_chat_id(m.chat.id)
|
|
7415
7759
|
|
|
7416
|
-
|
|
7760
|
+
# 提取文本内容(优先使用 caption,用于带媒体的消息)
|
|
7761
|
+
prompt = (m.caption or m.text or "").strip()
|
|
7762
|
+
|
|
7763
|
+
# 如果有媒体文件,使用增强收集函数
|
|
7764
|
+
has_media = m.photo or m.document
|
|
7765
|
+
if media_handler and has_media:
|
|
7766
|
+
try:
|
|
7767
|
+
content, media_files, feedback = await _collect_message_payload_with_media(m, None)
|
|
7768
|
+
# 发送用户反馈(下载成功/失败提示)
|
|
7769
|
+
if feedback:
|
|
7770
|
+
await m.answer(feedback)
|
|
7771
|
+
# 使用包含媒体路径引用的完整内容
|
|
7772
|
+
prompt = content
|
|
7773
|
+
except Exception as e:
|
|
7774
|
+
worker_log.error(
|
|
7775
|
+
"处理媒体文件失败: %s",
|
|
7776
|
+
e,
|
|
7777
|
+
exc_info=True,
|
|
7778
|
+
extra={**_session_extra(), "chat": m.chat.id}
|
|
7779
|
+
)
|
|
7780
|
+
# 降级处理:继续使用文本部分
|
|
7781
|
+
await m.answer(f"⚠️ 媒体文件处理失败,仅使用文本内容:{str(e)}")
|
|
7782
|
+
|
|
7417
7783
|
if not prompt:
|
|
7418
7784
|
return await m.answer("请输入非空提示词")
|
|
7785
|
+
|
|
7786
|
+
# 快捷查询任务详情(如输入 TASK_0001)
|
|
7419
7787
|
task_id_candidate = _normalize_task_id(prompt)
|
|
7420
7788
|
if task_id_candidate:
|
|
7421
7789
|
await _reply_task_detail_message(m, task_id_candidate)
|
|
7422
7790
|
return
|
|
7791
|
+
|
|
7792
|
+
# 忽略以 / 开头的未识别命令
|
|
7423
7793
|
if prompt.startswith("/"):
|
|
7424
7794
|
return
|
|
7425
7795
|
|
|
7796
|
+
# 环境异常检查
|
|
7426
7797
|
if ENV_ISSUES:
|
|
7427
7798
|
message = _format_env_issue_message()
|
|
7428
7799
|
worker_log.warning(
|
|
@@ -7434,7 +7805,7 @@ async def on_text(m: Message):
|
|
|
7434
7805
|
return
|
|
7435
7806
|
|
|
7436
7807
|
bot = current_bot()
|
|
7437
|
-
await bot.send_chat_action(m.chat.id, "typing") #
|
|
7808
|
+
await bot.send_chat_action(m.chat.id, "typing") # "正在输入"提示
|
|
7438
7809
|
|
|
7439
7810
|
if MODE == "A":
|
|
7440
7811
|
if not AGENT_CMD:
|
|
@@ -7517,8 +7888,28 @@ async def _ensure_worker_menu_button(bot: Bot) -> None:
|
|
|
7517
7888
|
extra={**_session_extra(), "text": WORKER_MENU_BUTTON_TEXT},
|
|
7518
7889
|
)
|
|
7519
7890
|
|
|
7891
|
+
async def _media_cleanup_worker():
|
|
7892
|
+
"""媒体文件定期清理任务"""
|
|
7893
|
+
if not media_handler or not MediaConfig:
|
|
7894
|
+
return
|
|
7895
|
+
|
|
7896
|
+
while True:
|
|
7897
|
+
try:
|
|
7898
|
+
await asyncio.sleep(MediaConfig.AUTO_CLEANUP_INTERVAL_HOURS * 3600)
|
|
7899
|
+
|
|
7900
|
+
if media_handler:
|
|
7901
|
+
cleaned = await media_handler.cleanup_old_files()
|
|
7902
|
+
worker_log.info(
|
|
7903
|
+
"定期清理完成,删除了 %d 个过期文件",
|
|
7904
|
+
cleaned,
|
|
7905
|
+
extra=_session_extra()
|
|
7906
|
+
)
|
|
7907
|
+
|
|
7908
|
+
except Exception as e:
|
|
7909
|
+
worker_log.error("媒体清理任务失败: %s", e, extra=_session_extra())
|
|
7910
|
+
|
|
7520
7911
|
async def main():
|
|
7521
|
-
global _bot, CHAT_LONG_POLL_LOCK
|
|
7912
|
+
global _bot, CHAT_LONG_POLL_LOCK, media_handler
|
|
7522
7913
|
# 初始化长轮询锁
|
|
7523
7914
|
CHAT_LONG_POLL_LOCK = asyncio.Lock()
|
|
7524
7915
|
_bot = build_bot()
|
|
@@ -7536,6 +7927,16 @@ async def main():
|
|
|
7536
7927
|
if _bot:
|
|
7537
7928
|
await _bot.session.close()
|
|
7538
7929
|
raise SystemExit(1)
|
|
7930
|
+
|
|
7931
|
+
# 初始化媒体处理器
|
|
7932
|
+
if HAS_MEDIA_HANDLER and _bot:
|
|
7933
|
+
try:
|
|
7934
|
+
media_handler = MediaHandler(_bot)
|
|
7935
|
+
# 启动定期清理任务
|
|
7936
|
+
asyncio.create_task(_media_cleanup_worker())
|
|
7937
|
+
worker_log.info("媒体处理器已初始化", extra=_session_extra())
|
|
7938
|
+
except Exception as exc:
|
|
7939
|
+
worker_log.error("媒体处理器初始化失败:%s", exc, extra=_session_extra())
|
|
7539
7940
|
await _ensure_bot_commands(_bot)
|
|
7540
7941
|
await _ensure_worker_menu_button(_bot)
|
|
7541
7942
|
await _broadcast_worker_keyboard(_bot)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
bot.py,sha256=
|
|
1
|
+
bot.py,sha256=CesIqaHDsd3Y17WifLCXuVBIzNHM4PNNokdS67HfNFc,288481
|
|
2
2
|
logging_setup.py,sha256=gvxHi8mUwK3IhXJrsGNTDo-DR6ngkyav1X-tvlBF_IE,4613
|
|
3
3
|
master.py,sha256=Jwxf6I94jOADzb9Xio1wb-tWy5wgQ9PlmdpKW4mhQMg,117114
|
|
4
4
|
project_repository.py,sha256=UcthtSGOJK0cTE5bQCneo3xkomRG-kyc1N1QVqxeHIs,17577
|
|
@@ -426,14 +426,14 @@ tasks/constants.py,sha256=tS1kZxBIUm3JJUMHm25XI-KHNUZl5NhbbuzjzL_rF-c,299
|
|
|
426
426
|
tasks/fsm.py,sha256=rKXXLEieQQU4r2z_CZUvn1_70FXiZXBBugF40gpe_tQ,1476
|
|
427
427
|
tasks/models.py,sha256=N_qqRBo9xMSV0vbn4k6bLBXT8C_dp_oTFUxvdx16ZQM,2459
|
|
428
428
|
tasks/service.py,sha256=w_S_aWiVqRXzXEpimLDsuCCCX2lB5uDkff9aKThBw9c,41916
|
|
429
|
-
vibego_cli/__init__.py,sha256=
|
|
429
|
+
vibego_cli/__init__.py,sha256=kuc7b-RJFkC5SgVLXUeh4Tu-RWIEyya6_BIrgSzP1wI,311
|
|
430
430
|
vibego_cli/__main__.py,sha256=qqTrYmRRLe4361fMzbI3-CqpZ7AhTofIHmfp4ykrrBY,158
|
|
431
431
|
vibego_cli/config.py,sha256=VxkPJMq01tA3h3cOkH-z_tiP7pMgfSGGicRvUnCWkhI,3054
|
|
432
432
|
vibego_cli/deps.py,sha256=1nRXI7Dd-S1hYE8DligzK5fIluQWETRUj4_OKL0DikQ,1419
|
|
433
433
|
vibego_cli/main.py,sha256=X__NXwZnIDIFbdKSTbNyZgZHKcPlN0DQz9sqTI1aQ9E,12158
|
|
434
434
|
vibego_cli/data/worker_requirements.txt,sha256=QSt30DSSSHtfucTFPpc7twk9kLS5rVLNTcvDiagxrZg,62
|
|
435
|
-
vibego-0.2.
|
|
436
|
-
vibego-0.2.
|
|
437
|
-
vibego-0.2.
|
|
438
|
-
vibego-0.2.
|
|
439
|
-
vibego-0.2.
|
|
435
|
+
vibego-0.2.52.dist-info/METADATA,sha256=C1pP4zQ47vlK54F9e3Ix_vkG37BrCAsKFuxSBBsvwM8,10519
|
|
436
|
+
vibego-0.2.52.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
437
|
+
vibego-0.2.52.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
|
|
438
|
+
vibego-0.2.52.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
|
|
439
|
+
vibego-0.2.52.dist-info/RECORD,,
|
vibego_cli/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|