AstrBot 4.10.1__py3-none-any.whl → 4.10.3__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.
Files changed (65) hide show
  1. astrbot/builtin_stars/astrbot/long_term_memory.py +186 -0
  2. astrbot/builtin_stars/astrbot/main.py +128 -0
  3. astrbot/builtin_stars/astrbot/metadata.yaml +4 -0
  4. astrbot/builtin_stars/astrbot/process_llm_request.py +245 -0
  5. astrbot/builtin_stars/builtin_commands/commands/__init__.py +31 -0
  6. astrbot/builtin_stars/builtin_commands/commands/admin.py +77 -0
  7. astrbot/builtin_stars/builtin_commands/commands/alter_cmd.py +173 -0
  8. astrbot/builtin_stars/builtin_commands/commands/conversation.py +366 -0
  9. astrbot/builtin_stars/builtin_commands/commands/help.py +88 -0
  10. astrbot/builtin_stars/builtin_commands/commands/llm.py +20 -0
  11. astrbot/builtin_stars/builtin_commands/commands/persona.py +142 -0
  12. astrbot/builtin_stars/builtin_commands/commands/plugin.py +120 -0
  13. astrbot/builtin_stars/builtin_commands/commands/provider.py +329 -0
  14. astrbot/builtin_stars/builtin_commands/commands/setunset.py +36 -0
  15. astrbot/builtin_stars/builtin_commands/commands/sid.py +36 -0
  16. astrbot/builtin_stars/builtin_commands/commands/t2i.py +23 -0
  17. astrbot/builtin_stars/builtin_commands/commands/tool.py +31 -0
  18. astrbot/builtin_stars/builtin_commands/commands/tts.py +36 -0
  19. astrbot/builtin_stars/builtin_commands/commands/utils/rst_scene.py +26 -0
  20. astrbot/builtin_stars/builtin_commands/main.py +237 -0
  21. astrbot/builtin_stars/builtin_commands/metadata.yaml +4 -0
  22. astrbot/builtin_stars/python_interpreter/main.py +537 -0
  23. astrbot/builtin_stars/python_interpreter/metadata.yaml +4 -0
  24. astrbot/builtin_stars/python_interpreter/requirements.txt +1 -0
  25. astrbot/builtin_stars/python_interpreter/shared/api.py +22 -0
  26. astrbot/builtin_stars/reminder/main.py +266 -0
  27. astrbot/builtin_stars/reminder/metadata.yaml +4 -0
  28. astrbot/builtin_stars/session_controller/main.py +114 -0
  29. astrbot/builtin_stars/session_controller/metadata.yaml +5 -0
  30. astrbot/builtin_stars/web_searcher/engines/__init__.py +111 -0
  31. astrbot/builtin_stars/web_searcher/engines/bing.py +30 -0
  32. astrbot/builtin_stars/web_searcher/engines/sogo.py +52 -0
  33. astrbot/builtin_stars/web_searcher/main.py +436 -0
  34. astrbot/builtin_stars/web_searcher/metadata.yaml +4 -0
  35. astrbot/cli/__init__.py +1 -1
  36. astrbot/core/agent/message.py +9 -0
  37. astrbot/core/agent/runners/tool_loop_agent_runner.py +2 -1
  38. astrbot/core/backup/__init__.py +26 -0
  39. astrbot/core/backup/constants.py +77 -0
  40. astrbot/core/backup/exporter.py +476 -0
  41. astrbot/core/backup/importer.py +761 -0
  42. astrbot/core/config/default.py +1 -1
  43. astrbot/core/log.py +1 -1
  44. astrbot/core/pipeline/process_stage/method/agent_sub_stages/internal.py +1 -1
  45. astrbot/core/pipeline/waking_check/stage.py +2 -1
  46. astrbot/core/provider/entities.py +32 -9
  47. astrbot/core/provider/provider.py +3 -1
  48. astrbot/core/provider/sources/anthropic_source.py +80 -27
  49. astrbot/core/provider/sources/fishaudio_tts_api_source.py +14 -6
  50. astrbot/core/provider/sources/gemini_source.py +75 -26
  51. astrbot/core/provider/sources/openai_source.py +68 -25
  52. astrbot/core/star/command_management.py +45 -4
  53. astrbot/core/star/context.py +1 -1
  54. astrbot/core/star/star_manager.py +11 -13
  55. astrbot/core/utils/astrbot_path.py +34 -0
  56. astrbot/dashboard/routes/__init__.py +2 -0
  57. astrbot/dashboard/routes/backup.py +589 -0
  58. astrbot/dashboard/routes/command.py +2 -1
  59. astrbot/dashboard/routes/log.py +44 -10
  60. astrbot/dashboard/server.py +8 -1
  61. {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/METADATA +2 -2
  62. {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/RECORD +65 -26
  63. {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/WHEEL +0 -0
  64. {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/entry_points.txt +0 -0
  65. {astrbot-4.10.1.dist-info → astrbot-4.10.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,589 @@
1
+ """备份管理 API 路由"""
2
+
3
+ import asyncio
4
+ import os
5
+ import re
6
+ import traceback
7
+ import uuid
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ from quart import request, send_file
12
+
13
+ from astrbot.core import logger
14
+ from astrbot.core.backup.exporter import AstrBotExporter
15
+ from astrbot.core.backup.importer import AstrBotImporter
16
+ from astrbot.core.core_lifecycle import AstrBotCoreLifecycle
17
+ from astrbot.core.db import BaseDatabase
18
+ from astrbot.core.utils.astrbot_path import (
19
+ get_astrbot_backups_path,
20
+ get_astrbot_data_path,
21
+ )
22
+
23
+ from .route import Response, Route, RouteContext
24
+
25
+
26
+ def secure_filename(filename: str) -> str:
27
+ """清洗文件名,移除路径遍历字符和危险字符
28
+
29
+ Args:
30
+ filename: 原始文件名
31
+
32
+ Returns:
33
+ 安全的文件名
34
+ """
35
+ # 跨平台处理:先将反斜杠替换为正斜杠,再取文件名
36
+ filename = filename.replace("\\", "/")
37
+ # 仅保留文件名部分,移除路径
38
+ filename = os.path.basename(filename)
39
+
40
+ # 替换路径遍历字符
41
+ filename = filename.replace("..", "_")
42
+
43
+ # 仅保留字母、数字、下划线、连字符、点
44
+ filename = re.sub(r"[^\w\-.]", "_", filename)
45
+
46
+ # 移除前导点(隐藏文件)和尾部点
47
+ filename = filename.strip(".")
48
+
49
+ # 如果文件名为空或只包含下划线,生成一个默认名称
50
+ if not filename or filename.replace("_", "") == "":
51
+ filename = "backup"
52
+
53
+ return filename
54
+
55
+
56
+ def generate_unique_filename(original_filename: str) -> str:
57
+ """生成唯一的文件名,添加时间戳前缀
58
+
59
+ Args:
60
+ original_filename: 原始文件名(已清洗)
61
+
62
+ Returns:
63
+ 唯一的文件名
64
+ """
65
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
66
+ name, ext = os.path.splitext(original_filename)
67
+ return f"uploaded_{timestamp}_{name}{ext}"
68
+
69
+
70
+ class BackupRoute(Route):
71
+ """备份管理路由
72
+
73
+ 提供备份导出、导入、列表等 API 接口
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ context: RouteContext,
79
+ db: BaseDatabase,
80
+ core_lifecycle: AstrBotCoreLifecycle,
81
+ ) -> None:
82
+ super().__init__(context)
83
+ self.db = db
84
+ self.core_lifecycle = core_lifecycle
85
+ self.backup_dir = get_astrbot_backups_path()
86
+ self.data_dir = get_astrbot_data_path()
87
+
88
+ # 任务状态跟踪
89
+ self.backup_tasks: dict[str, dict] = {}
90
+ self.backup_progress: dict[str, dict] = {}
91
+
92
+ # 注册路由
93
+ self.routes = {
94
+ "/backup/list": ("GET", self.list_backups),
95
+ "/backup/export": ("POST", self.export_backup),
96
+ "/backup/upload": ("POST", self.upload_backup), # 上传文件
97
+ "/backup/check": ("POST", self.check_backup), # 预检查
98
+ "/backup/import": ("POST", self.import_backup), # 确认导入
99
+ "/backup/progress": ("GET", self.get_progress),
100
+ "/backup/download": ("GET", self.download_backup),
101
+ "/backup/delete": ("POST", self.delete_backup),
102
+ }
103
+ self.register_routes()
104
+
105
+ def _init_task(self, task_id: str, task_type: str, status: str = "pending") -> None:
106
+ """初始化任务状态"""
107
+ self.backup_tasks[task_id] = {
108
+ "type": task_type,
109
+ "status": status,
110
+ "result": None,
111
+ "error": None,
112
+ }
113
+ self.backup_progress[task_id] = {
114
+ "status": status,
115
+ "stage": "waiting",
116
+ "current": 0,
117
+ "total": 100,
118
+ "message": "",
119
+ }
120
+
121
+ def _set_task_result(
122
+ self,
123
+ task_id: str,
124
+ status: str,
125
+ result: dict | None = None,
126
+ error: str | None = None,
127
+ ) -> None:
128
+ """设置任务结果"""
129
+ if task_id in self.backup_tasks:
130
+ self.backup_tasks[task_id]["status"] = status
131
+ self.backup_tasks[task_id]["result"] = result
132
+ self.backup_tasks[task_id]["error"] = error
133
+ if task_id in self.backup_progress:
134
+ self.backup_progress[task_id]["status"] = status
135
+
136
+ def _update_progress(
137
+ self,
138
+ task_id: str,
139
+ *,
140
+ status: str | None = None,
141
+ stage: str | None = None,
142
+ current: int | None = None,
143
+ total: int | None = None,
144
+ message: str | None = None,
145
+ ) -> None:
146
+ """更新任务进度"""
147
+ if task_id not in self.backup_progress:
148
+ return
149
+ p = self.backup_progress[task_id]
150
+ if status is not None:
151
+ p["status"] = status
152
+ if stage is not None:
153
+ p["stage"] = stage
154
+ if current is not None:
155
+ p["current"] = current
156
+ if total is not None:
157
+ p["total"] = total
158
+ if message is not None:
159
+ p["message"] = message
160
+
161
+ def _make_progress_callback(self, task_id: str):
162
+ """创建进度回调函数"""
163
+
164
+ async def _callback(stage: str, current: int, total: int, message: str = ""):
165
+ self._update_progress(
166
+ task_id,
167
+ status="processing",
168
+ stage=stage,
169
+ current=current,
170
+ total=total,
171
+ message=message,
172
+ )
173
+
174
+ return _callback
175
+
176
+ async def list_backups(self):
177
+ """获取备份列表
178
+
179
+ Query 参数:
180
+ - page: 页码 (默认 1)
181
+ - page_size: 每页数量 (默认 20)
182
+ """
183
+ try:
184
+ page = request.args.get("page", 1, type=int)
185
+ page_size = request.args.get("page_size", 20, type=int)
186
+
187
+ # 确保备份目录存在
188
+ Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
189
+
190
+ # 获取所有备份文件
191
+ backup_files = []
192
+ for filename in os.listdir(self.backup_dir):
193
+ if filename.endswith(".zip") and filename.startswith("astrbot_backup_"):
194
+ file_path = os.path.join(self.backup_dir, filename)
195
+ stat = os.stat(file_path)
196
+ backup_files.append(
197
+ {
198
+ "filename": filename,
199
+ "size": stat.st_size,
200
+ "created_at": stat.st_mtime,
201
+ }
202
+ )
203
+
204
+ # 按创建时间倒序排序
205
+ backup_files.sort(key=lambda x: x["created_at"], reverse=True)
206
+
207
+ # 分页
208
+ start = (page - 1) * page_size
209
+ end = start + page_size
210
+ items = backup_files[start:end]
211
+
212
+ return (
213
+ Response()
214
+ .ok(
215
+ {
216
+ "items": items,
217
+ "total": len(backup_files),
218
+ "page": page,
219
+ "page_size": page_size,
220
+ }
221
+ )
222
+ .__dict__
223
+ )
224
+ except Exception as e:
225
+ logger.error(f"获取备份列表失败: {e}")
226
+ logger.error(traceback.format_exc())
227
+ return Response().error(f"获取备份列表失败: {e!s}").__dict__
228
+
229
+ async def export_backup(self):
230
+ """创建备份
231
+
232
+ 返回:
233
+ - task_id: 任务ID,用于查询导出进度
234
+ """
235
+ try:
236
+ # 生成任务ID
237
+ task_id = str(uuid.uuid4())
238
+
239
+ # 初始化任务状态
240
+ self._init_task(task_id, "export", "pending")
241
+
242
+ # 启动后台导出任务
243
+ asyncio.create_task(self._background_export_task(task_id))
244
+
245
+ return (
246
+ Response()
247
+ .ok(
248
+ {
249
+ "task_id": task_id,
250
+ "message": "export task created, processing in background",
251
+ }
252
+ )
253
+ .__dict__
254
+ )
255
+ except Exception as e:
256
+ logger.error(f"创建备份失败: {e}")
257
+ logger.error(traceback.format_exc())
258
+ return Response().error(f"创建备份失败: {e!s}").__dict__
259
+
260
+ async def _background_export_task(self, task_id: str):
261
+ """后台导出任务"""
262
+ try:
263
+ self._update_progress(task_id, status="processing", message="正在初始化...")
264
+
265
+ # 获取知识库管理器
266
+ kb_manager = getattr(self.core_lifecycle, "kb_manager", None)
267
+
268
+ exporter = AstrBotExporter(
269
+ main_db=self.db,
270
+ kb_manager=kb_manager,
271
+ config_path=os.path.join(self.data_dir, "cmd_config.json"),
272
+ )
273
+
274
+ # 创建进度回调
275
+ progress_callback = self._make_progress_callback(task_id)
276
+
277
+ # 执行导出
278
+ zip_path = await exporter.export_all(
279
+ output_dir=self.backup_dir,
280
+ progress_callback=progress_callback,
281
+ )
282
+
283
+ # 设置成功结果
284
+ self._set_task_result(
285
+ task_id,
286
+ "completed",
287
+ result={
288
+ "filename": os.path.basename(zip_path),
289
+ "path": zip_path,
290
+ "size": os.path.getsize(zip_path),
291
+ },
292
+ )
293
+ except Exception as e:
294
+ logger.error(f"后台导出任务 {task_id} 失败: {e}")
295
+ logger.error(traceback.format_exc())
296
+ self._set_task_result(task_id, "failed", error=str(e))
297
+
298
+ async def upload_backup(self):
299
+ """上传备份文件
300
+
301
+ 将备份文件上传到服务器,返回保存的文件名。
302
+ 上传后应调用 check_backup 进行预检查。
303
+
304
+ Form Data:
305
+ - file: 备份文件 (.zip)
306
+
307
+ 返回:
308
+ - filename: 保存的文件名
309
+ """
310
+ try:
311
+ files = await request.files
312
+ if "file" not in files:
313
+ return Response().error("缺少备份文件").__dict__
314
+
315
+ file = files["file"]
316
+ if not file.filename or not file.filename.endswith(".zip"):
317
+ return Response().error("请上传 ZIP 格式的备份文件").__dict__
318
+
319
+ # 清洗文件名并生成唯一名称,防止路径遍历和覆盖
320
+ safe_filename = secure_filename(file.filename)
321
+ unique_filename = generate_unique_filename(safe_filename)
322
+
323
+ # 保存上传的文件
324
+ Path(self.backup_dir).mkdir(parents=True, exist_ok=True)
325
+ zip_path = os.path.join(self.backup_dir, unique_filename)
326
+ await file.save(zip_path)
327
+
328
+ logger.info(
329
+ f"上传的备份文件已保存: {unique_filename} (原始名称: {file.filename})"
330
+ )
331
+
332
+ return (
333
+ Response()
334
+ .ok(
335
+ {
336
+ "filename": unique_filename,
337
+ "original_filename": file.filename,
338
+ "size": os.path.getsize(zip_path),
339
+ }
340
+ )
341
+ .__dict__
342
+ )
343
+ except Exception as e:
344
+ logger.error(f"上传备份文件失败: {e}")
345
+ logger.error(traceback.format_exc())
346
+ return Response().error(f"上传备份文件失败: {e!s}").__dict__
347
+
348
+ async def check_backup(self):
349
+ """预检查备份文件
350
+
351
+ 检查备份文件的版本兼容性,返回确认信息。
352
+ 用户确认后调用 import_backup 执行导入。
353
+
354
+ JSON Body:
355
+ - filename: 已上传的备份文件名
356
+
357
+ 返回:
358
+ - ImportPreCheckResult: 预检查结果
359
+ """
360
+ try:
361
+ data = await request.json
362
+ filename = data.get("filename")
363
+ if not filename:
364
+ return Response().error("缺少 filename 参数").__dict__
365
+
366
+ # 安全检查 - 防止路径遍历
367
+ if ".." in filename or "/" in filename or "\\" in filename:
368
+ return Response().error("无效的文件名").__dict__
369
+
370
+ zip_path = os.path.join(self.backup_dir, filename)
371
+ if not os.path.exists(zip_path):
372
+ return Response().error(f"备份文件不存在: {filename}").__dict__
373
+
374
+ # 获取知识库管理器(用于构造 importer)
375
+ kb_manager = getattr(self.core_lifecycle, "kb_manager", None)
376
+
377
+ importer = AstrBotImporter(
378
+ main_db=self.db,
379
+ kb_manager=kb_manager,
380
+ config_path=os.path.join(self.data_dir, "cmd_config.json"),
381
+ )
382
+
383
+ # 执行预检查
384
+ check_result = importer.pre_check(zip_path)
385
+
386
+ return Response().ok(check_result.to_dict()).__dict__
387
+ except Exception as e:
388
+ logger.error(f"预检查备份文件失败: {e}")
389
+ logger.error(traceback.format_exc())
390
+ return Response().error(f"预检查备份文件失败: {e!s}").__dict__
391
+
392
+ async def import_backup(self):
393
+ """执行备份导入
394
+
395
+ 在用户确认后执行实际的导入操作。
396
+ 需要先调用 upload_backup 上传文件,再调用 check_backup 预检查。
397
+
398
+ JSON Body:
399
+ - filename: 已上传的备份文件名(必填)
400
+ - confirmed: 用户已确认(必填,必须为 true)
401
+
402
+ 返回:
403
+ - task_id: 任务ID,用于查询导入进度
404
+ """
405
+ try:
406
+ data = await request.json
407
+ filename = data.get("filename")
408
+ confirmed = data.get("confirmed", False)
409
+
410
+ if not filename:
411
+ return Response().error("缺少 filename 参数").__dict__
412
+
413
+ if not confirmed:
414
+ return (
415
+ Response()
416
+ .error("请先确认导入。导入将会清空并覆盖现有数据,此操作不可撤销。")
417
+ .__dict__
418
+ )
419
+
420
+ # 安全检查 - 防止路径遍历
421
+ if ".." in filename or "/" in filename or "\\" in filename:
422
+ return Response().error("无效的文件名").__dict__
423
+
424
+ zip_path = os.path.join(self.backup_dir, filename)
425
+ if not os.path.exists(zip_path):
426
+ return Response().error(f"备份文件不存在: {filename}").__dict__
427
+
428
+ # 生成任务ID
429
+ task_id = str(uuid.uuid4())
430
+
431
+ # 初始化任务状态
432
+ self._init_task(task_id, "import", "pending")
433
+
434
+ # 启动后台导入任务
435
+ asyncio.create_task(self._background_import_task(task_id, zip_path))
436
+
437
+ return (
438
+ Response()
439
+ .ok(
440
+ {
441
+ "task_id": task_id,
442
+ "message": "import task created, processing in background",
443
+ }
444
+ )
445
+ .__dict__
446
+ )
447
+ except Exception as e:
448
+ logger.error(f"导入备份失败: {e}")
449
+ logger.error(traceback.format_exc())
450
+ return Response().error(f"导入备份失败: {e!s}").__dict__
451
+
452
+ async def _background_import_task(self, task_id: str, zip_path: str):
453
+ """后台导入任务"""
454
+ try:
455
+ self._update_progress(task_id, status="processing", message="正在初始化...")
456
+
457
+ # 获取知识库管理器
458
+ kb_manager = getattr(self.core_lifecycle, "kb_manager", None)
459
+
460
+ importer = AstrBotImporter(
461
+ main_db=self.db,
462
+ kb_manager=kb_manager,
463
+ config_path=os.path.join(self.data_dir, "cmd_config.json"),
464
+ )
465
+
466
+ # 创建进度回调
467
+ progress_callback = self._make_progress_callback(task_id)
468
+
469
+ # 执行导入
470
+ result = await importer.import_all(
471
+ zip_path=zip_path,
472
+ mode="replace",
473
+ progress_callback=progress_callback,
474
+ )
475
+
476
+ # 设置结果
477
+ if result.success:
478
+ self._set_task_result(
479
+ task_id,
480
+ "completed",
481
+ result=result.to_dict(),
482
+ )
483
+ else:
484
+ self._set_task_result(
485
+ task_id,
486
+ "failed",
487
+ error="; ".join(result.errors),
488
+ )
489
+ except Exception as e:
490
+ logger.error(f"后台导入任务 {task_id} 失败: {e}")
491
+ logger.error(traceback.format_exc())
492
+ self._set_task_result(task_id, "failed", error=str(e))
493
+
494
+ async def get_progress(self):
495
+ """获取任务进度
496
+
497
+ Query 参数:
498
+ - task_id: 任务 ID (必填)
499
+ """
500
+ try:
501
+ task_id = request.args.get("task_id")
502
+ if not task_id:
503
+ return Response().error("缺少参数 task_id").__dict__
504
+
505
+ if task_id not in self.backup_tasks:
506
+ return Response().error("找不到该任务").__dict__
507
+
508
+ task_info = self.backup_tasks[task_id]
509
+ status = task_info["status"]
510
+
511
+ response_data = {
512
+ "task_id": task_id,
513
+ "type": task_info["type"],
514
+ "status": status,
515
+ }
516
+
517
+ # 如果任务正在处理,返回进度信息
518
+ if status == "processing" and task_id in self.backup_progress:
519
+ response_data["progress"] = self.backup_progress[task_id]
520
+
521
+ # 如果任务完成,返回结果
522
+ if status == "completed":
523
+ response_data["result"] = task_info["result"]
524
+
525
+ # 如果任务失败,返回错误信息
526
+ if status == "failed":
527
+ response_data["error"] = task_info["error"]
528
+
529
+ return Response().ok(response_data).__dict__
530
+ except Exception as e:
531
+ logger.error(f"获取任务进度失败: {e}")
532
+ logger.error(traceback.format_exc())
533
+ return Response().error(f"获取任务进度失败: {e!s}").__dict__
534
+
535
+ async def download_backup(self):
536
+ """下载备份文件
537
+
538
+ Query 参数:
539
+ - filename: 备份文件名 (必填)
540
+ """
541
+ try:
542
+ filename = request.args.get("filename")
543
+ if not filename:
544
+ return Response().error("缺少参数 filename").__dict__
545
+
546
+ # 安全检查 - 防止路径遍历
547
+ if ".." in filename or "/" in filename or "\\" in filename:
548
+ return Response().error("无效的文件名").__dict__
549
+
550
+ file_path = os.path.join(self.backup_dir, filename)
551
+ if not os.path.exists(file_path):
552
+ return Response().error("备份文件不存在").__dict__
553
+
554
+ return await send_file(
555
+ file_path,
556
+ as_attachment=True,
557
+ attachment_filename=filename,
558
+ )
559
+ except Exception as e:
560
+ logger.error(f"下载备份失败: {e}")
561
+ logger.error(traceback.format_exc())
562
+ return Response().error(f"下载备份失败: {e!s}").__dict__
563
+
564
+ async def delete_backup(self):
565
+ """删除备份文件
566
+
567
+ Body:
568
+ - filename: 备份文件名 (必填)
569
+ """
570
+ try:
571
+ data = await request.json
572
+ filename = data.get("filename")
573
+ if not filename:
574
+ return Response().error("缺少参数 filename").__dict__
575
+
576
+ # 安全检查 - 防止路径遍历
577
+ if ".." in filename or "/" in filename or "\\" in filename:
578
+ return Response().error("无效的文件名").__dict__
579
+
580
+ file_path = os.path.join(self.backup_dir, filename)
581
+ if not os.path.exists(file_path):
582
+ return Response().error("备份文件不存在").__dict__
583
+
584
+ os.remove(file_path)
585
+ return Response().ok(message="删除备份成功").__dict__
586
+ except Exception as e:
587
+ logger.error(f"删除备份失败: {e}")
588
+ logger.error(traceback.format_exc())
589
+ return Response().error(f"删除备份失败: {e!s}").__dict__
@@ -61,12 +61,13 @@ class CommandRoute(Route):
61
61
  data = await request.get_json()
62
62
  handler_full_name = data.get("handler_full_name")
63
63
  new_name = data.get("new_name")
64
+ aliases = data.get("aliases")
64
65
 
65
66
  if not handler_full_name or not new_name:
66
67
  return Response().error("handler_full_name 与 new_name 均为必填。").__dict__
67
68
 
68
69
  try:
69
- await rename_command_service(handler_full_name, new_name)
70
+ await rename_command_service(handler_full_name, new_name, aliases=aliases)
70
71
  except ValueError as exc:
71
72
  return Response().error(str(exc)).__dict__
72
73
 
@@ -1,15 +1,26 @@
1
1
  import asyncio
2
2
  import json
3
+ import time
4
+ from collections.abc import AsyncGenerator
3
5
  from typing import cast
4
6
 
5
7
  from quart import Response as QuartResponse
6
- from quart import make_response
8
+ from quart import make_response, request
7
9
 
8
10
  from astrbot.core import LogBroker, logger
9
11
 
10
12
  from .route import Response, Route, RouteContext
11
13
 
12
14
 
15
+ def _format_log_sse(log: dict, ts: float) -> str:
16
+ """辅助函数:格式化 SSE 消息"""
17
+ payload = {
18
+ "type": "log",
19
+ **log,
20
+ }
21
+ return f"id: {ts}\ndata: {json.dumps(payload, ensure_ascii=False)}\n\n"
22
+
23
+
13
24
  class LogRoute(Route):
14
25
  def __init__(self, context: RouteContext, log_broker: LogBroker) -> None:
15
26
  super().__init__(context)
@@ -21,21 +32,44 @@ class LogRoute(Route):
21
32
  methods=["GET"],
22
33
  )
23
34
 
24
- async def log(self):
35
+ async def _replay_cached_logs(
36
+ self, last_event_id: str
37
+ ) -> AsyncGenerator[str, None]:
38
+ """辅助生成器:重放缓存的日志"""
39
+ try:
40
+ last_ts = float(last_event_id)
41
+ cached_logs = list(self.log_broker.log_cache)
42
+
43
+ for log_item in cached_logs:
44
+ log_ts = float(log_item.get("time", 0))
45
+
46
+ if log_ts > last_ts:
47
+ yield _format_log_sse(log_item, log_ts)
48
+
49
+ except ValueError:
50
+ pass
51
+ except Exception as e:
52
+ logger.error(f"Log SSE 补发历史错误: {e}")
53
+
54
+ async def log(self) -> QuartResponse:
55
+ last_event_id = request.headers.get("Last-Event-ID")
56
+
25
57
  async def stream():
26
58
  queue = None
27
59
  try:
60
+ if last_event_id:
61
+ async for event in self._replay_cached_logs(last_event_id):
62
+ yield event
63
+
28
64
  queue = self.log_broker.register()
29
65
  while True:
30
66
  message = await queue.get()
31
- payload = {
32
- "type": "log",
33
- **message, # see astrbot/core/log.py
34
- }
35
- yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
67
+ current_ts = message.get("time", time.time())
68
+ yield _format_log_sse(message, current_ts)
69
+
36
70
  except asyncio.CancelledError:
37
71
  pass
38
- except BaseException as e:
72
+ except Exception as e:
39
73
  logger.error(f"Log SSE 连接错误: {e}")
40
74
  finally:
41
75
  if queue:
@@ -53,7 +87,7 @@ class LogRoute(Route):
53
87
  },
54
88
  ),
55
89
  )
56
- response.timeout = None
90
+ response.timeout = None # type: ignore
57
91
  return response
58
92
 
59
93
  async def log_history(self):
@@ -69,6 +103,6 @@ class LogRoute(Route):
69
103
  )
70
104
  .__dict__
71
105
  )
72
- except BaseException as e:
106
+ except Exception as e:
73
107
  logger.error(f"获取日志历史失败: {e}")
74
108
  return Response().error(f"获取日志历史失败: {e}").__dict__