vibego 0.2.53__py3-none-any.whl → 0.2.55__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.
Potentially problematic release.
This version of vibego might be problematic. Click here for more details.
- bot.py +487 -443
- {vibego-0.2.53.dist-info → vibego-0.2.55.dist-info}/METADATA +1 -1
- {vibego-0.2.53.dist-info → vibego-0.2.55.dist-info}/RECORD +7 -7
- vibego_cli/__init__.py +1 -1
- {vibego-0.2.53.dist-info → vibego-0.2.55.dist-info}/WHEEL +0 -0
- {vibego-0.2.53.dist-info → vibego-0.2.55.dist-info}/entry_points.txt +0 -0
- {vibego-0.2.53.dist-info → vibego-0.2.55.dist-info}/top_level.txt +0 -0
bot.py
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
-
import asyncio, os, sys, time, uuid, shlex, subprocess, socket, re, json, shutil, hashlib, html
|
|
10
|
+
import asyncio, os, sys, time, uuid, shlex, subprocess, socket, re, json, shutil, hashlib, html, mimetypes
|
|
11
11
|
from datetime import datetime, UTC
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
from typing import Any, Dict, Optional, Sequence, Tuple, List, Callable, Awaitable, Literal
|
|
@@ -37,8 +37,6 @@ from aiogram.types import (
|
|
|
37
37
|
ReplyKeyboardRemove,
|
|
38
38
|
Update,
|
|
39
39
|
User,
|
|
40
|
-
PhotoSize,
|
|
41
|
-
Document,
|
|
42
40
|
)
|
|
43
41
|
from aiogram.client.session.aiohttp import AiohttpSession
|
|
44
42
|
from aiogram.enums import ParseMode
|
|
@@ -87,15 +85,6 @@ def load_env(p: str = ".env"):
|
|
|
87
85
|
|
|
88
86
|
load_env()
|
|
89
87
|
|
|
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
|
-
|
|
99
88
|
# --- 日志 & 上下文 ---
|
|
100
89
|
PROJECT_NAME = os.environ.get("PROJECT_NAME", "").strip()
|
|
101
90
|
ACTIVE_MODEL = (os.environ.get("ACTIVE_MODEL") or os.environ.get("MODEL_NAME") or "").strip()
|
|
@@ -119,6 +108,19 @@ def _env_int(name: str, default: int) -> int:
|
|
|
119
108
|
worker_log.warning("环境变量 %s=%r 解析为整数失败,已使用默认值 %s", name, raw, default)
|
|
120
109
|
return default
|
|
121
110
|
|
|
111
|
+
|
|
112
|
+
def _env_float(name: str, default: float) -> float:
|
|
113
|
+
"""读取浮点型环境变量,解析失败时回退默认值。"""
|
|
114
|
+
|
|
115
|
+
raw = os.environ.get(name)
|
|
116
|
+
if raw is None or not raw.strip():
|
|
117
|
+
return default
|
|
118
|
+
try:
|
|
119
|
+
return float(raw.strip())
|
|
120
|
+
except ValueError:
|
|
121
|
+
worker_log.warning("环境变量 %s=%r 解析为浮点数失败,已使用默认值 %s", name, raw, default)
|
|
122
|
+
return default
|
|
123
|
+
|
|
122
124
|
_PARSE_MODE_CANDIDATES: Dict[str, Optional[ParseMode]] = {
|
|
123
125
|
"": None,
|
|
124
126
|
"none": None,
|
|
@@ -1171,7 +1173,6 @@ async def _push_task_to_model(
|
|
|
1171
1173
|
supplement: Optional[str],
|
|
1172
1174
|
actor: Optional[str],
|
|
1173
1175
|
is_bug_report: bool = False,
|
|
1174
|
-
media_files: Optional[List[Dict[str, Any]]] = None,
|
|
1175
1176
|
) -> tuple[bool, str, Optional[Path]]:
|
|
1176
1177
|
"""推送任务信息到模型,并附带补充描述。
|
|
1177
1178
|
|
|
@@ -1182,30 +1183,17 @@ async def _push_task_to_model(
|
|
|
1182
1183
|
supplement: 补充描述
|
|
1183
1184
|
actor: 操作者
|
|
1184
1185
|
is_bug_report: 是否为缺陷报告推送
|
|
1185
|
-
media_files: 媒体文件列表
|
|
1186
1186
|
"""
|
|
1187
1187
|
|
|
1188
1188
|
history_text, history_count = await _build_history_context_for_model(task.id)
|
|
1189
1189
|
notes = await TASK_SERVICE.list_notes(task.id)
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
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
|
-
)
|
|
1190
|
+
prompt = _build_model_push_payload(
|
|
1191
|
+
task,
|
|
1192
|
+
supplement=supplement,
|
|
1193
|
+
history=history_text,
|
|
1194
|
+
notes=notes,
|
|
1195
|
+
is_bug_report=is_bug_report,
|
|
1196
|
+
)
|
|
1209
1197
|
success, session_path = await _dispatch_prompt_to_model(
|
|
1210
1198
|
chat_id,
|
|
1211
1199
|
prompt,
|
|
@@ -1315,6 +1303,439 @@ PROJECT_SLUG = (PROJECT_NAME or "default").replace("/", "-") or "default"
|
|
|
1315
1303
|
TASK_DB_PATH = DATA_ROOT / f"{PROJECT_SLUG}.db"
|
|
1316
1304
|
TASK_SERVICE = TaskService(TASK_DB_PATH, PROJECT_SLUG)
|
|
1317
1305
|
|
|
1306
|
+
ATTACHMENT_STORAGE_ROOT = (DATA_ROOT / "telegram").expanduser()
|
|
1307
|
+
ATTACHMENT_STORAGE_ROOT.mkdir(parents=True, exist_ok=True)
|
|
1308
|
+
_ATTACHMENT_TOTAL_MB = max(_env_int("TELEGRAM_ATTACHMENT_MAX_TOTAL_MB", 512), 16)
|
|
1309
|
+
ATTACHMENT_TOTAL_LIMIT_BYTES = _ATTACHMENT_TOTAL_MB * 1024 * 1024
|
|
1310
|
+
MEDIA_GROUP_AGGREGATION_DELAY = max(_env_float("TELEGRAM_MEDIA_GROUP_DELAY", 0.8), 0.1)
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
@dataclass
|
|
1314
|
+
class TelegramSavedAttachment:
|
|
1315
|
+
"""记录单个附件的落地信息,便于提示模型读取。"""
|
|
1316
|
+
|
|
1317
|
+
kind: str
|
|
1318
|
+
display_name: str
|
|
1319
|
+
mime_type: str
|
|
1320
|
+
absolute_path: Path
|
|
1321
|
+
relative_path: str
|
|
1322
|
+
|
|
1323
|
+
|
|
1324
|
+
@dataclass
|
|
1325
|
+
class PendingMediaGroupState:
|
|
1326
|
+
"""聚合 Telegram 媒体组的临时缓存。"""
|
|
1327
|
+
|
|
1328
|
+
chat_id: int
|
|
1329
|
+
origin_message: Message
|
|
1330
|
+
attachment_dir: Path
|
|
1331
|
+
attachments: list[TelegramSavedAttachment]
|
|
1332
|
+
captions: list[str]
|
|
1333
|
+
finalize_task: Optional[asyncio.Task] = None
|
|
1334
|
+
|
|
1335
|
+
|
|
1336
|
+
MEDIA_GROUP_STATE: dict[str, PendingMediaGroupState] = {}
|
|
1337
|
+
MEDIA_GROUP_LOCK = asyncio.Lock()
|
|
1338
|
+
|
|
1339
|
+
ATTACHMENT_USAGE_HINT = (
|
|
1340
|
+
"请按需读取附件:图片可使用 Codex 的 view_image 功能或 Claude Code 的文件引用能力;"
|
|
1341
|
+
"文本/日志可直接通过 @<路径> 打开;若需其他处理请说明。"
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
_FS_SAFE_PATTERN = re.compile(r"[^A-Za-z0-9._-]")
|
|
1345
|
+
|
|
1346
|
+
|
|
1347
|
+
def _sanitize_fs_component(value: str, fallback: str) -> str:
|
|
1348
|
+
"""清理路径片段中的特殊字符,避免越权访问。"""
|
|
1349
|
+
|
|
1350
|
+
stripped = (value or "").strip()
|
|
1351
|
+
cleaned = _FS_SAFE_PATTERN.sub("_", stripped)
|
|
1352
|
+
cleaned = cleaned.strip("._")
|
|
1353
|
+
return cleaned or fallback
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
def _format_relative_path(path: Path) -> str:
|
|
1357
|
+
"""将绝对路径转换为模型更易识别的相对路径。"""
|
|
1358
|
+
|
|
1359
|
+
try:
|
|
1360
|
+
rel = path.relative_to(ROOT_DIR_PATH)
|
|
1361
|
+
rel_str = rel.as_posix()
|
|
1362
|
+
if not rel_str.startswith("."):
|
|
1363
|
+
return f"./{rel_str}"
|
|
1364
|
+
return rel_str
|
|
1365
|
+
except ValueError:
|
|
1366
|
+
return path.resolve().as_posix()
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _directory_size(path: Path) -> int:
|
|
1370
|
+
"""计算目录占用的总字节数。"""
|
|
1371
|
+
|
|
1372
|
+
total = 0
|
|
1373
|
+
if not path.exists():
|
|
1374
|
+
return 0
|
|
1375
|
+
for entry in path.rglob("*"):
|
|
1376
|
+
try:
|
|
1377
|
+
if entry.is_file():
|
|
1378
|
+
total += entry.stat().st_size
|
|
1379
|
+
except FileNotFoundError:
|
|
1380
|
+
continue
|
|
1381
|
+
return total
|
|
1382
|
+
|
|
1383
|
+
|
|
1384
|
+
def _cleanup_attachment_storage() -> None:
|
|
1385
|
+
"""控制附件目录容量,避免磁盘被占满。"""
|
|
1386
|
+
|
|
1387
|
+
if ATTACHMENT_TOTAL_LIMIT_BYTES <= 0:
|
|
1388
|
+
return
|
|
1389
|
+
total = _directory_size(ATTACHMENT_STORAGE_ROOT)
|
|
1390
|
+
if total <= ATTACHMENT_TOTAL_LIMIT_BYTES:
|
|
1391
|
+
return
|
|
1392
|
+
candidates = sorted(
|
|
1393
|
+
(p for p in ATTACHMENT_STORAGE_ROOT.iterdir() if p.is_dir()),
|
|
1394
|
+
key=lambda item: item.stat().st_mtime,
|
|
1395
|
+
)
|
|
1396
|
+
for folder in candidates:
|
|
1397
|
+
try:
|
|
1398
|
+
shutil.rmtree(folder, ignore_errors=True)
|
|
1399
|
+
except Exception as exc: # noqa: BLE001
|
|
1400
|
+
worker_log.warning(
|
|
1401
|
+
"清理旧附件目录失败:%s",
|
|
1402
|
+
exc,
|
|
1403
|
+
extra=_session_extra(path=folder),
|
|
1404
|
+
)
|
|
1405
|
+
if _directory_size(ATTACHMENT_STORAGE_ROOT) <= ATTACHMENT_TOTAL_LIMIT_BYTES:
|
|
1406
|
+
break
|
|
1407
|
+
|
|
1408
|
+
|
|
1409
|
+
def _guess_extension(mime_type: Optional[str], fallback: str = ".bin") -> str:
|
|
1410
|
+
"""根据 MIME 类型推断扩展名。"""
|
|
1411
|
+
|
|
1412
|
+
if mime_type:
|
|
1413
|
+
guessed = mimetypes.guess_extension(mime_type, strict=False)
|
|
1414
|
+
if guessed:
|
|
1415
|
+
return guessed
|
|
1416
|
+
return fallback
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
def _attachment_dir_for_message(message: Message, media_group_id: Optional[str] = None) -> Path:
|
|
1420
|
+
"""为当前消息(或媒体组)生成稳定的附件目录。"""
|
|
1421
|
+
|
|
1422
|
+
chat_part = _sanitize_fs_component(str(message.chat.id), "chat")
|
|
1423
|
+
base = ATTACHMENT_STORAGE_ROOT / chat_part
|
|
1424
|
+
if media_group_id:
|
|
1425
|
+
group_part = _sanitize_fs_component(media_group_id, str(message.message_id))
|
|
1426
|
+
target = base / f"group_{group_part}"
|
|
1427
|
+
else:
|
|
1428
|
+
timestamp = int((message.date or datetime.now(UTC)).timestamp())
|
|
1429
|
+
target = base / f"msg_{timestamp}_{message.message_id}"
|
|
1430
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
1431
|
+
return target
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
async def _download_telegram_file(
|
|
1435
|
+
message: Message,
|
|
1436
|
+
*,
|
|
1437
|
+
file_id: str,
|
|
1438
|
+
file_name_hint: str,
|
|
1439
|
+
mime_type: Optional[str],
|
|
1440
|
+
target_dir: Path,
|
|
1441
|
+
) -> Path:
|
|
1442
|
+
"""从 Telegram 下载文件并返回本地路径。"""
|
|
1443
|
+
|
|
1444
|
+
bot = message.bot or current_bot()
|
|
1445
|
+
telegram_file = await bot.get_file(file_id)
|
|
1446
|
+
stem = _sanitize_fs_component(Path(file_name_hint).stem, "file")
|
|
1447
|
+
extension = Path(file_name_hint).suffix or _guess_extension(mime_type, ".bin")
|
|
1448
|
+
if not extension.startswith("."):
|
|
1449
|
+
extension = f".{extension}"
|
|
1450
|
+
filename = f"{stem}{extension}"
|
|
1451
|
+
destination = target_dir / filename
|
|
1452
|
+
counter = 1
|
|
1453
|
+
while destination.exists():
|
|
1454
|
+
destination = target_dir / f"{stem}_{counter}{extension}"
|
|
1455
|
+
counter += 1
|
|
1456
|
+
await bot.download_file(telegram_file.file_path, destination=destination)
|
|
1457
|
+
return destination
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
async def _collect_saved_attachments(message: Message, target_dir: Path) -> list[TelegramSavedAttachment]:
|
|
1461
|
+
"""下载消息中的所有附件,并返回保存记录。"""
|
|
1462
|
+
|
|
1463
|
+
saved: list[TelegramSavedAttachment] = []
|
|
1464
|
+
|
|
1465
|
+
if message.photo:
|
|
1466
|
+
photo = message.photo[-1]
|
|
1467
|
+
path = await _download_telegram_file(
|
|
1468
|
+
message,
|
|
1469
|
+
file_id=photo.file_id,
|
|
1470
|
+
file_name_hint=f"photo_{photo.file_unique_id}.jpg",
|
|
1471
|
+
mime_type="image/jpeg",
|
|
1472
|
+
target_dir=target_dir,
|
|
1473
|
+
)
|
|
1474
|
+
saved.append(
|
|
1475
|
+
TelegramSavedAttachment(
|
|
1476
|
+
kind="photo",
|
|
1477
|
+
display_name=path.name,
|
|
1478
|
+
mime_type="image/jpeg",
|
|
1479
|
+
absolute_path=path,
|
|
1480
|
+
relative_path=_format_relative_path(path),
|
|
1481
|
+
)
|
|
1482
|
+
)
|
|
1483
|
+
|
|
1484
|
+
document = message.document
|
|
1485
|
+
if document:
|
|
1486
|
+
file_name = document.file_name or f"document_{document.file_unique_id}"
|
|
1487
|
+
path = await _download_telegram_file(
|
|
1488
|
+
message,
|
|
1489
|
+
file_id=document.file_id,
|
|
1490
|
+
file_name_hint=file_name,
|
|
1491
|
+
mime_type=document.mime_type or "application/octet-stream",
|
|
1492
|
+
target_dir=target_dir,
|
|
1493
|
+
)
|
|
1494
|
+
saved.append(
|
|
1495
|
+
TelegramSavedAttachment(
|
|
1496
|
+
kind="document",
|
|
1497
|
+
display_name=file_name,
|
|
1498
|
+
mime_type=document.mime_type or "application/octet-stream",
|
|
1499
|
+
absolute_path=path,
|
|
1500
|
+
relative_path=_format_relative_path(path),
|
|
1501
|
+
)
|
|
1502
|
+
)
|
|
1503
|
+
|
|
1504
|
+
video = message.video
|
|
1505
|
+
if video:
|
|
1506
|
+
file_name = video.file_name or f"video_{video.file_unique_id}"
|
|
1507
|
+
path = await _download_telegram_file(
|
|
1508
|
+
message,
|
|
1509
|
+
file_id=video.file_id,
|
|
1510
|
+
file_name_hint=file_name,
|
|
1511
|
+
mime_type=video.mime_type or "video/mp4",
|
|
1512
|
+
target_dir=target_dir,
|
|
1513
|
+
)
|
|
1514
|
+
saved.append(
|
|
1515
|
+
TelegramSavedAttachment(
|
|
1516
|
+
kind="video",
|
|
1517
|
+
display_name=file_name,
|
|
1518
|
+
mime_type=video.mime_type or "video/mp4",
|
|
1519
|
+
absolute_path=path,
|
|
1520
|
+
relative_path=_format_relative_path(path),
|
|
1521
|
+
)
|
|
1522
|
+
)
|
|
1523
|
+
|
|
1524
|
+
audio = message.audio
|
|
1525
|
+
if audio:
|
|
1526
|
+
file_name = audio.file_name or f"audio_{audio.file_unique_id}"
|
|
1527
|
+
path = await _download_telegram_file(
|
|
1528
|
+
message,
|
|
1529
|
+
file_id=audio.file_id,
|
|
1530
|
+
file_name_hint=file_name,
|
|
1531
|
+
mime_type=audio.mime_type or "audio/mpeg",
|
|
1532
|
+
target_dir=target_dir,
|
|
1533
|
+
)
|
|
1534
|
+
saved.append(
|
|
1535
|
+
TelegramSavedAttachment(
|
|
1536
|
+
kind="audio",
|
|
1537
|
+
display_name=file_name,
|
|
1538
|
+
mime_type=audio.mime_type or "audio/mpeg",
|
|
1539
|
+
absolute_path=path,
|
|
1540
|
+
relative_path=_format_relative_path(path),
|
|
1541
|
+
)
|
|
1542
|
+
)
|
|
1543
|
+
|
|
1544
|
+
voice = message.voice
|
|
1545
|
+
if voice:
|
|
1546
|
+
file_name = f"voice_{voice.file_unique_id}.ogg"
|
|
1547
|
+
path = await _download_telegram_file(
|
|
1548
|
+
message,
|
|
1549
|
+
file_id=voice.file_id,
|
|
1550
|
+
file_name_hint=file_name,
|
|
1551
|
+
mime_type=voice.mime_type or "audio/ogg",
|
|
1552
|
+
target_dir=target_dir,
|
|
1553
|
+
)
|
|
1554
|
+
saved.append(
|
|
1555
|
+
TelegramSavedAttachment(
|
|
1556
|
+
kind="voice",
|
|
1557
|
+
display_name=file_name,
|
|
1558
|
+
mime_type=voice.mime_type or "audio/ogg",
|
|
1559
|
+
absolute_path=path,
|
|
1560
|
+
relative_path=_format_relative_path(path),
|
|
1561
|
+
)
|
|
1562
|
+
)
|
|
1563
|
+
|
|
1564
|
+
animation = message.animation
|
|
1565
|
+
if animation:
|
|
1566
|
+
file_name = animation.file_name or f"animation_{animation.file_unique_id}"
|
|
1567
|
+
path = await _download_telegram_file(
|
|
1568
|
+
message,
|
|
1569
|
+
file_id=animation.file_id,
|
|
1570
|
+
file_name_hint=file_name,
|
|
1571
|
+
mime_type=animation.mime_type or "video/mp4",
|
|
1572
|
+
target_dir=target_dir,
|
|
1573
|
+
)
|
|
1574
|
+
saved.append(
|
|
1575
|
+
TelegramSavedAttachment(
|
|
1576
|
+
kind="animation",
|
|
1577
|
+
display_name=file_name,
|
|
1578
|
+
mime_type=animation.mime_type or "video/mp4",
|
|
1579
|
+
absolute_path=path,
|
|
1580
|
+
relative_path=_format_relative_path(path),
|
|
1581
|
+
)
|
|
1582
|
+
)
|
|
1583
|
+
|
|
1584
|
+
video_note = message.video_note
|
|
1585
|
+
if video_note:
|
|
1586
|
+
file_name = f"video_note_{video_note.file_unique_id}.mp4"
|
|
1587
|
+
path = await _download_telegram_file(
|
|
1588
|
+
message,
|
|
1589
|
+
file_id=video_note.file_id,
|
|
1590
|
+
file_name_hint=file_name,
|
|
1591
|
+
mime_type=video_note.mime_type or "video/mp4",
|
|
1592
|
+
target_dir=target_dir,
|
|
1593
|
+
)
|
|
1594
|
+
saved.append(
|
|
1595
|
+
TelegramSavedAttachment(
|
|
1596
|
+
kind="video_note",
|
|
1597
|
+
display_name=file_name,
|
|
1598
|
+
mime_type=video_note.mime_type or "video/mp4",
|
|
1599
|
+
absolute_path=path,
|
|
1600
|
+
relative_path=_format_relative_path(path),
|
|
1601
|
+
)
|
|
1602
|
+
)
|
|
1603
|
+
|
|
1604
|
+
if saved:
|
|
1605
|
+
_cleanup_attachment_storage()
|
|
1606
|
+
return saved
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
def _build_prompt_with_attachments(
|
|
1610
|
+
text_part: Optional[str],
|
|
1611
|
+
attachments: Sequence[TelegramSavedAttachment],
|
|
1612
|
+
) -> str:
|
|
1613
|
+
"""将文字与附件描述拼接成模型可读的提示。"""
|
|
1614
|
+
|
|
1615
|
+
sections: list[str] = []
|
|
1616
|
+
base_text = (text_part or "").strip()
|
|
1617
|
+
if base_text:
|
|
1618
|
+
sections.append(base_text)
|
|
1619
|
+
if attachments:
|
|
1620
|
+
lines = ["附件列表(文件位于项目工作目录,可直接读取):"]
|
|
1621
|
+
for idx, item in enumerate(attachments, 1):
|
|
1622
|
+
lines.append(
|
|
1623
|
+
f"{idx}. {item.display_name}({item.mime_type})→ {item.relative_path}"
|
|
1624
|
+
)
|
|
1625
|
+
lines.append("")
|
|
1626
|
+
lines.append(ATTACHMENT_USAGE_HINT)
|
|
1627
|
+
sections.append("\n".join(lines))
|
|
1628
|
+
if not sections:
|
|
1629
|
+
fallback = [
|
|
1630
|
+
"收到一条仅包含附件的消息,没有额外文字说明。",
|
|
1631
|
+
"请直接阅读列出的附件并给出观察结果或结论。",
|
|
1632
|
+
]
|
|
1633
|
+
sections.append("\n".join(fallback))
|
|
1634
|
+
return "\n\n".join(sections).strip()
|
|
1635
|
+
|
|
1636
|
+
|
|
1637
|
+
async def _finalize_media_group_after_delay(media_group_id: str) -> None:
|
|
1638
|
+
"""在短暂延迟后合并媒体组消息,确保 Telegram 全部照片到齐。"""
|
|
1639
|
+
|
|
1640
|
+
try:
|
|
1641
|
+
await asyncio.sleep(MEDIA_GROUP_AGGREGATION_DELAY)
|
|
1642
|
+
except asyncio.CancelledError:
|
|
1643
|
+
return
|
|
1644
|
+
|
|
1645
|
+
async with MEDIA_GROUP_LOCK:
|
|
1646
|
+
state = MEDIA_GROUP_STATE.pop(media_group_id, None)
|
|
1647
|
+
|
|
1648
|
+
if state is None:
|
|
1649
|
+
return
|
|
1650
|
+
|
|
1651
|
+
text_block = "\n".join(state.captions).strip()
|
|
1652
|
+
prompt = _build_prompt_with_attachments(text_block, state.attachments)
|
|
1653
|
+
try:
|
|
1654
|
+
await _handle_prompt_dispatch(state.origin_message, prompt)
|
|
1655
|
+
except Exception as exc: # noqa: BLE001
|
|
1656
|
+
worker_log.exception(
|
|
1657
|
+
"媒体组消息推送模型失败:%s",
|
|
1658
|
+
exc,
|
|
1659
|
+
extra=_session_extra(media_group=media_group_id),
|
|
1660
|
+
)
|
|
1661
|
+
|
|
1662
|
+
|
|
1663
|
+
async def _enqueue_media_group_message(message: Message, text_part: Optional[str]) -> None:
|
|
1664
|
+
"""收集媒体组中的每一条消息,统一延迟推送。"""
|
|
1665
|
+
|
|
1666
|
+
media_group_id = message.media_group_id
|
|
1667
|
+
if not media_group_id:
|
|
1668
|
+
return
|
|
1669
|
+
|
|
1670
|
+
async with MEDIA_GROUP_LOCK:
|
|
1671
|
+
state = MEDIA_GROUP_STATE.get(media_group_id)
|
|
1672
|
+
if state is None:
|
|
1673
|
+
attachment_dir = _attachment_dir_for_message(message, media_group_id=media_group_id)
|
|
1674
|
+
state = PendingMediaGroupState(
|
|
1675
|
+
chat_id=message.chat.id,
|
|
1676
|
+
origin_message=message,
|
|
1677
|
+
attachment_dir=attachment_dir,
|
|
1678
|
+
attachments=[],
|
|
1679
|
+
captions=[],
|
|
1680
|
+
)
|
|
1681
|
+
MEDIA_GROUP_STATE[media_group_id] = state
|
|
1682
|
+
else:
|
|
1683
|
+
attachment_dir = state.attachment_dir
|
|
1684
|
+
|
|
1685
|
+
attachments = await _collect_saved_attachments(message, attachment_dir)
|
|
1686
|
+
caption = (text_part or "").strip()
|
|
1687
|
+
|
|
1688
|
+
async with MEDIA_GROUP_LOCK:
|
|
1689
|
+
state = MEDIA_GROUP_STATE.get(media_group_id)
|
|
1690
|
+
if state is None:
|
|
1691
|
+
# 若期间被清理,重新创建并继续积累,避免丢失后续内容。
|
|
1692
|
+
state = PendingMediaGroupState(
|
|
1693
|
+
chat_id=message.chat.id,
|
|
1694
|
+
origin_message=message,
|
|
1695
|
+
attachment_dir=attachment_dir,
|
|
1696
|
+
attachments=[],
|
|
1697
|
+
captions=[],
|
|
1698
|
+
)
|
|
1699
|
+
MEDIA_GROUP_STATE[media_group_id] = state
|
|
1700
|
+
state.attachments.extend(attachments)
|
|
1701
|
+
if caption:
|
|
1702
|
+
state.captions.append(caption)
|
|
1703
|
+
# 使用首条消息作为引用对象,便于 Telegram 回复。
|
|
1704
|
+
if state.origin_message.message_id > message.message_id:
|
|
1705
|
+
state.origin_message = message
|
|
1706
|
+
if state.finalize_task and not state.finalize_task.done():
|
|
1707
|
+
state.finalize_task.cancel()
|
|
1708
|
+
state.finalize_task = asyncio.create_task(_finalize_media_group_after_delay(media_group_id))
|
|
1709
|
+
|
|
1710
|
+
|
|
1711
|
+
async def _handle_prompt_dispatch(message: Message, prompt: str) -> None:
|
|
1712
|
+
"""统一封装向模型推送提示词的流程。"""
|
|
1713
|
+
|
|
1714
|
+
if ENV_ISSUES:
|
|
1715
|
+
message_text = _format_env_issue_message()
|
|
1716
|
+
worker_log.warning(
|
|
1717
|
+
"拒绝处理消息,环境异常: %s",
|
|
1718
|
+
message_text,
|
|
1719
|
+
extra={**_session_extra(), "chat": message.chat.id},
|
|
1720
|
+
)
|
|
1721
|
+
await message.answer(message_text)
|
|
1722
|
+
return
|
|
1723
|
+
|
|
1724
|
+
bot = current_bot()
|
|
1725
|
+
await bot.send_chat_action(message.chat.id, "typing")
|
|
1726
|
+
|
|
1727
|
+
if MODE == "A":
|
|
1728
|
+
if not AGENT_CMD:
|
|
1729
|
+
await message.answer("AGENT_CMD 未配置(.env)")
|
|
1730
|
+
return
|
|
1731
|
+
rc, out = run_subprocess_capture(AGENT_CMD, input_text=prompt)
|
|
1732
|
+
out = out or ""
|
|
1733
|
+
out = out + ("" if rc == 0 else f"\n(exit={rc})")
|
|
1734
|
+
await reply_large_text(message.chat.id, out)
|
|
1735
|
+
return
|
|
1736
|
+
|
|
1737
|
+
await _dispatch_prompt_to_model(message.chat.id, prompt, reply_to=message)
|
|
1738
|
+
|
|
1318
1739
|
BOT_COMMANDS: list[tuple[str, str]] = [
|
|
1319
1740
|
("help", "查看全部命令"),
|
|
1320
1741
|
("tasks", "任务命令清单"),
|
|
@@ -2023,57 +2444,6 @@ def _build_model_push_payload(
|
|
|
2023
2444
|
return _strip_legacy_bug_header(result or body)
|
|
2024
2445
|
|
|
2025
2446
|
|
|
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
|
-
|
|
2077
2447
|
try:
|
|
2078
2448
|
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
|
|
2079
2449
|
except ZoneInfoNotFoundError:
|
|
@@ -3051,61 +3421,6 @@ def _collect_message_payload(message: Message) -> str:
|
|
|
3051
3421
|
return "\n".join(parts).strip()
|
|
3052
3422
|
|
|
3053
3423
|
|
|
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
|
-
|
|
3109
3424
|
def _summarize_note_text(value: str) -> str:
|
|
3110
3425
|
"""压缩备注内容,维持主要信息并控制长度。"""
|
|
3111
3426
|
|
|
@@ -3562,9 +3877,6 @@ def tmux_capture_since(log_path: Path | str, start_pos: int, idle: float = 2.0,
|
|
|
3562
3877
|
return "".join(buf)
|
|
3563
3878
|
|
|
3564
3879
|
|
|
3565
|
-
# 全局媒体处理器
|
|
3566
|
-
media_handler: Optional[MediaHandler] = None
|
|
3567
|
-
|
|
3568
3880
|
SESSION_OFFSETS: Dict[str, int] = {}
|
|
3569
3881
|
CHAT_SESSION_MAP: Dict[int, str] = {}
|
|
3570
3882
|
CHAT_WATCHERS: Dict[int, asyncio.Task] = {}
|
|
@@ -5311,7 +5623,6 @@ async def on_help_command(message: Message) -> None:
|
|
|
5311
5623
|
"*指令总览*\n"
|
|
5312
5624
|
"- /help — 查看全部命令\n"
|
|
5313
5625
|
"- /tasks — 任务管理命令清单\n"
|
|
5314
|
-
"- /media — 查看媒体文件统计和管理\n"
|
|
5315
5626
|
"- /task_new — 创建任务(交互式或附带参数)\n"
|
|
5316
5627
|
"- /task_list — 查看任务列表,支持 status/limit/offset\n"
|
|
5317
5628
|
"- /task_show — 查看某个任务详情\n"
|
|
@@ -5340,158 +5651,6 @@ async def on_tasks_help(message: Message) -> None:
|
|
|
5340
5651
|
await _answer_with_markdown(message, text)
|
|
5341
5652
|
|
|
5342
5653
|
|
|
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
|
-
|
|
5495
5654
|
def _normalize_status(value: Optional[str]) -> Optional[str]:
|
|
5496
5655
|
if not value:
|
|
5497
5656
|
return None
|
|
@@ -7172,40 +7331,21 @@ async def on_task_bug_report(callback: CallbackQuery, state: FSMContext) -> None
|
|
|
7172
7331
|
|
|
7173
7332
|
@router.message(TaskBugReportStates.waiting_description)
|
|
7174
7333
|
async def on_task_bug_description(message: Message, state: FSMContext) -> None:
|
|
7175
|
-
"""
|
|
7334
|
+
"""处理缺陷描述输入。"""
|
|
7176
7335
|
|
|
7177
7336
|
if _is_cancel_message(message.text):
|
|
7178
7337
|
await state.clear()
|
|
7179
7338
|
await message.answer("已取消缺陷上报。", reply_markup=_build_worker_main_keyboard())
|
|
7180
7339
|
return
|
|
7181
|
-
|
|
7182
|
-
|
|
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:
|
|
7340
|
+
content = _collect_message_payload(message)
|
|
7341
|
+
if not content:
|
|
7200
7342
|
await message.answer(
|
|
7201
7343
|
"缺陷描述不能为空,请重新输入:",
|
|
7202
7344
|
reply_markup=_build_description_keyboard(),
|
|
7203
7345
|
)
|
|
7204
7346
|
return
|
|
7205
|
-
|
|
7206
7347
|
await state.update_data(
|
|
7207
7348
|
description=content,
|
|
7208
|
-
media_files=media_files,
|
|
7209
7349
|
reporter=_actor_from_message(message),
|
|
7210
7350
|
)
|
|
7211
7351
|
await state.set_state(TaskBugReportStates.waiting_reproduction)
|
|
@@ -7214,36 +7354,18 @@ async def on_task_bug_description(message: Message, state: FSMContext) -> None:
|
|
|
7214
7354
|
|
|
7215
7355
|
@router.message(TaskBugReportStates.waiting_reproduction)
|
|
7216
7356
|
async def on_task_bug_reproduction(message: Message, state: FSMContext) -> None:
|
|
7217
|
-
"""
|
|
7357
|
+
"""处理复现步骤输入。"""
|
|
7218
7358
|
|
|
7219
7359
|
if _is_cancel_message(message.text):
|
|
7220
7360
|
await state.clear()
|
|
7221
7361
|
await message.answer("已取消缺陷上报。", reply_markup=_build_worker_main_keyboard())
|
|
7222
7362
|
return
|
|
7223
|
-
|
|
7224
7363
|
options = [SKIP_TEXT, "取消"]
|
|
7225
7364
|
resolved = _resolve_reply_choice(message.text or "", options=options)
|
|
7226
7365
|
reproduction = ""
|
|
7227
|
-
reproduction_media = []
|
|
7228
|
-
|
|
7229
7366
|
if resolved not in {SKIP_TEXT, "取消"}:
|
|
7230
|
-
|
|
7231
|
-
|
|
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
|
-
)
|
|
7367
|
+
reproduction = _collect_message_payload(message)
|
|
7368
|
+
await state.update_data(reproduction=reproduction)
|
|
7247
7369
|
await state.set_state(TaskBugReportStates.waiting_logs)
|
|
7248
7370
|
await message.answer(_build_bug_log_prompt(), reply_markup=_build_description_keyboard())
|
|
7249
7371
|
|
|
@@ -7321,21 +7443,11 @@ async def on_task_bug_confirm(message: Message, state: FSMContext) -> None:
|
|
|
7321
7443
|
reproduction = data.get("reproduction", "")
|
|
7322
7444
|
logs = data.get("logs", "")
|
|
7323
7445
|
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
|
-
|
|
7332
7446
|
payload = {
|
|
7333
7447
|
"action": "bug_report",
|
|
7334
7448
|
"description_length": len(description),
|
|
7335
7449
|
"has_reproduction": bool(reproduction.strip()),
|
|
7336
7450
|
"has_logs": bool(logs.strip()),
|
|
7337
|
-
"has_media": len(all_media_files) > 0,
|
|
7338
|
-
"media_count": len(all_media_files),
|
|
7339
7451
|
"description": description,
|
|
7340
7452
|
"reproduction": reproduction,
|
|
7341
7453
|
"logs": logs,
|
|
@@ -7349,12 +7461,7 @@ async def on_task_bug_confirm(message: Message, state: FSMContext) -> None:
|
|
|
7349
7461
|
payload=payload,
|
|
7350
7462
|
)
|
|
7351
7463
|
await state.clear()
|
|
7352
|
-
await _auto_push_after_bug_report(
|
|
7353
|
-
task,
|
|
7354
|
-
message=message,
|
|
7355
|
-
actor=reporter,
|
|
7356
|
-
media_files=all_media_files
|
|
7357
|
-
)
|
|
7464
|
+
await _auto_push_after_bug_report(task, message=message, actor=reporter)
|
|
7358
7465
|
|
|
7359
7466
|
|
|
7360
7467
|
@router.callback_query(F.data.startswith("task:add_note:"))
|
|
@@ -7729,6 +7836,28 @@ async def on_edit_new_value(message: Message, state: FSMContext) -> None:
|
|
|
7729
7836
|
await _answer_with_markdown(message, f"任务已更新:\n{detail_text}", reply_markup=markup)
|
|
7730
7837
|
|
|
7731
7838
|
|
|
7839
|
+
@router.message(
|
|
7840
|
+
F.photo | F.document | F.video | F.audio | F.voice | F.animation | F.video_note
|
|
7841
|
+
)
|
|
7842
|
+
async def on_media_message(message: Message) -> None:
|
|
7843
|
+
"""处理带附件的普通消息,将附件下载并拼接提示词。"""
|
|
7844
|
+
|
|
7845
|
+
_auto_record_chat_id(message.chat.id)
|
|
7846
|
+
text_part = (message.caption or message.text or "").strip()
|
|
7847
|
+
|
|
7848
|
+
if message.media_group_id:
|
|
7849
|
+
await _enqueue_media_group_message(message, text_part)
|
|
7850
|
+
return
|
|
7851
|
+
|
|
7852
|
+
attachment_dir = _attachment_dir_for_message(message)
|
|
7853
|
+
attachments = await _collect_saved_attachments(message, attachment_dir)
|
|
7854
|
+
if not attachments and not text_part:
|
|
7855
|
+
await message.answer("未检测到可处理的附件或文字内容。")
|
|
7856
|
+
return
|
|
7857
|
+
prompt = _build_prompt_with_attachments(text_part, attachments)
|
|
7858
|
+
await _handle_prompt_dispatch(message, prompt)
|
|
7859
|
+
|
|
7860
|
+
|
|
7732
7861
|
@router.message(CommandStart())
|
|
7733
7862
|
async def on_start(m: Message):
|
|
7734
7863
|
# 首次收到消息时自动记录 chat_id 到 state 文件
|
|
@@ -7747,76 +7876,21 @@ async def on_start(m: Message):
|
|
|
7747
7876
|
if ENV_ISSUES:
|
|
7748
7877
|
await m.answer(_format_env_issue_message())
|
|
7749
7878
|
|
|
7750
|
-
@router.message(F.text
|
|
7879
|
+
@router.message(F.text)
|
|
7751
7880
|
async def on_text(m: Message):
|
|
7752
|
-
"""
|
|
7753
|
-
处理用户消息:支持纯文本、带图片和文档的消息
|
|
7754
|
-
- 纯文本消息:使用 m.text
|
|
7755
|
-
- 带媒体消息:使用 m.caption,并下载媒体文件
|
|
7756
|
-
"""
|
|
7757
7881
|
# 首次收到消息时自动记录 chat_id 到 state 文件
|
|
7758
7882
|
_auto_record_chat_id(m.chat.id)
|
|
7759
7883
|
|
|
7760
|
-
|
|
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
|
-
|
|
7884
|
+
prompt = (m.text or "").strip()
|
|
7783
7885
|
if not prompt:
|
|
7784
7886
|
return await m.answer("请输入非空提示词")
|
|
7785
|
-
|
|
7786
|
-
# 快捷查询任务详情(如输入 TASK_0001)
|
|
7787
7887
|
task_id_candidate = _normalize_task_id(prompt)
|
|
7788
7888
|
if task_id_candidate:
|
|
7789
7889
|
await _reply_task_detail_message(m, task_id_candidate)
|
|
7790
7890
|
return
|
|
7791
|
-
|
|
7792
|
-
# 忽略以 / 开头的未识别命令
|
|
7793
7891
|
if prompt.startswith("/"):
|
|
7794
7892
|
return
|
|
7795
|
-
|
|
7796
|
-
# 环境异常检查
|
|
7797
|
-
if ENV_ISSUES:
|
|
7798
|
-
message = _format_env_issue_message()
|
|
7799
|
-
worker_log.warning(
|
|
7800
|
-
"拒绝处理消息,环境异常: %s",
|
|
7801
|
-
message,
|
|
7802
|
-
extra={**_session_extra(), "chat": m.chat.id},
|
|
7803
|
-
)
|
|
7804
|
-
await m.answer(message)
|
|
7805
|
-
return
|
|
7806
|
-
|
|
7807
|
-
bot = current_bot()
|
|
7808
|
-
await bot.send_chat_action(m.chat.id, "typing") # "正在输入"提示
|
|
7809
|
-
|
|
7810
|
-
if MODE == "A":
|
|
7811
|
-
if not AGENT_CMD:
|
|
7812
|
-
return await m.answer("AGENT_CMD 未配置(.env)")
|
|
7813
|
-
rc, out = run_subprocess_capture(AGENT_CMD, input_text=prompt)
|
|
7814
|
-
out = out or ""
|
|
7815
|
-
out = out + ("" if rc == 0 else f"\n(exit={rc})")
|
|
7816
|
-
await reply_large_text(m.chat.id, out)
|
|
7817
|
-
|
|
7818
|
-
else:
|
|
7819
|
-
await _dispatch_prompt_to_model(m.chat.id, prompt, reply_to=m)
|
|
7893
|
+
await _handle_prompt_dispatch(m, prompt)
|
|
7820
7894
|
|
|
7821
7895
|
|
|
7822
7896
|
async def ensure_telegram_connectivity(bot: Bot, timeout: float = 30.0):
|
|
@@ -7888,28 +7962,8 @@ async def _ensure_worker_menu_button(bot: Bot) -> None:
|
|
|
7888
7962
|
extra={**_session_extra(), "text": WORKER_MENU_BUTTON_TEXT},
|
|
7889
7963
|
)
|
|
7890
7964
|
|
|
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
|
-
|
|
7911
7965
|
async def main():
|
|
7912
|
-
global _bot, CHAT_LONG_POLL_LOCK
|
|
7966
|
+
global _bot, CHAT_LONG_POLL_LOCK
|
|
7913
7967
|
# 初始化长轮询锁
|
|
7914
7968
|
CHAT_LONG_POLL_LOCK = asyncio.Lock()
|
|
7915
7969
|
_bot = build_bot()
|
|
@@ -7927,16 +7981,6 @@ async def main():
|
|
|
7927
7981
|
if _bot:
|
|
7928
7982
|
await _bot.session.close()
|
|
7929
7983
|
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())
|
|
7940
7984
|
await _ensure_bot_commands(_bot)
|
|
7941
7985
|
await _ensure_worker_menu_button(_bot)
|
|
7942
7986
|
await _broadcast_worker_keyboard(_bot)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
bot.py,sha256=
|
|
1
|
+
bot.py,sha256=hjebHpgW7tR9fNxkdbgNLFNYaz4MNgtsq_Sci2VljPM,289835
|
|
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=asbT5tFazlPcS692PW0b6GBg_fWkYpf-mZn02hiG-p4,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.55.dist-info/METADATA,sha256=McMxELuZfQ_ByCmOD3gRCEIqec4IHYK1pPNjMpn8xxM,10519
|
|
436
|
+
vibego-0.2.55.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
437
|
+
vibego-0.2.55.dist-info/entry_points.txt,sha256=Lsy_zm-dlyxt8-9DL9blBReIwU2k22c8-kifr46ND1M,48
|
|
438
|
+
vibego-0.2.55.dist-info/top_level.txt,sha256=R56CT3nW5H5v3ce0l3QDN4-C4qxTrNWzRTwrxnkDX4U,69
|
|
439
|
+
vibego-0.2.55.dist-info/RECORD,,
|
vibego_cli/__init__.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|