aury-boot 0.0.4__py3-none-any.whl → 0.0.7__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 (122) hide show
  1. aury/boot/__init__.py +2 -2
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +60 -36
  4. aury/boot/application/adapter/__init__.py +112 -0
  5. aury/boot/application/adapter/base.py +511 -0
  6. aury/boot/application/adapter/config.py +242 -0
  7. aury/boot/application/adapter/decorators.py +259 -0
  8. aury/boot/application/adapter/exceptions.py +202 -0
  9. aury/boot/application/adapter/http.py +325 -0
  10. aury/boot/application/app/__init__.py +12 -8
  11. aury/boot/application/app/base.py +12 -0
  12. aury/boot/application/app/components.py +137 -44
  13. aury/boot/application/app/middlewares.py +9 -4
  14. aury/boot/application/app/startup.py +249 -0
  15. aury/boot/application/config/__init__.py +36 -1
  16. aury/boot/application/config/multi_instance.py +216 -0
  17. aury/boot/application/config/settings.py +398 -149
  18. aury/boot/application/constants/components.py +6 -0
  19. aury/boot/application/errors/handlers.py +17 -3
  20. aury/boot/application/middleware/logging.py +21 -120
  21. aury/boot/application/rpc/__init__.py +2 -2
  22. aury/boot/commands/__init__.py +30 -10
  23. aury/boot/commands/app.py +131 -1
  24. aury/boot/commands/docs.py +104 -17
  25. aury/boot/commands/generate.py +22 -22
  26. aury/boot/commands/init.py +68 -17
  27. aury/boot/commands/server/app.py +2 -3
  28. aury/boot/commands/templates/project/AGENTS.md.tpl +221 -0
  29. aury/boot/commands/templates/project/README.md.tpl +2 -2
  30. aury/boot/commands/templates/project/aury_docs/00-overview.md.tpl +59 -0
  31. aury/boot/commands/templates/project/aury_docs/01-model.md.tpl +184 -0
  32. aury/boot/commands/templates/project/aury_docs/02-repository.md.tpl +206 -0
  33. aury/boot/commands/templates/project/aury_docs/03-service.md.tpl +398 -0
  34. aury/boot/commands/templates/project/aury_docs/04-schema.md.tpl +95 -0
  35. aury/boot/commands/templates/project/aury_docs/05-api.md.tpl +116 -0
  36. aury/boot/commands/templates/project/aury_docs/06-exception.md.tpl +118 -0
  37. aury/boot/commands/templates/project/aury_docs/07-cache.md.tpl +122 -0
  38. aury/boot/commands/templates/project/aury_docs/08-scheduler.md.tpl +32 -0
  39. aury/boot/commands/templates/project/aury_docs/09-tasks.md.tpl +38 -0
  40. aury/boot/commands/templates/project/aury_docs/10-storage.md.tpl +115 -0
  41. aury/boot/commands/templates/project/aury_docs/11-logging.md.tpl +131 -0
  42. aury/boot/commands/templates/project/aury_docs/12-admin.md.tpl +56 -0
  43. aury/boot/commands/templates/project/aury_docs/13-channel.md.tpl +104 -0
  44. aury/boot/commands/templates/project/aury_docs/14-mq.md.tpl +102 -0
  45. aury/boot/commands/templates/project/aury_docs/15-events.md.tpl +147 -0
  46. aury/boot/commands/templates/project/aury_docs/16-adapter.md.tpl +403 -0
  47. aury/boot/commands/templates/project/{CLI.md.tpl → aury_docs/99-cli.md.tpl} +19 -19
  48. aury/boot/commands/templates/project/config.py.tpl +10 -10
  49. aury/boot/commands/templates/project/env_templates/_header.tpl +10 -0
  50. aury/boot/commands/templates/project/env_templates/admin.tpl +49 -0
  51. aury/boot/commands/templates/project/env_templates/cache.tpl +14 -0
  52. aury/boot/commands/templates/project/env_templates/database.tpl +22 -0
  53. aury/boot/commands/templates/project/env_templates/log.tpl +18 -0
  54. aury/boot/commands/templates/project/env_templates/messaging.tpl +46 -0
  55. aury/boot/commands/templates/project/env_templates/rpc.tpl +28 -0
  56. aury/boot/commands/templates/project/env_templates/scheduler.tpl +18 -0
  57. aury/boot/commands/templates/project/env_templates/service.tpl +18 -0
  58. aury/boot/commands/templates/project/env_templates/storage.tpl +38 -0
  59. aury/boot/commands/templates/project/env_templates/third_party.tpl +43 -0
  60. aury/boot/commands/templates/project/modules/tasks.py.tpl +1 -1
  61. aury/boot/common/logging/__init__.py +26 -674
  62. aury/boot/common/logging/context.py +132 -0
  63. aury/boot/common/logging/decorators.py +118 -0
  64. aury/boot/common/logging/format.py +315 -0
  65. aury/boot/common/logging/setup.py +214 -0
  66. aury/boot/contrib/admin_console/auth.py +2 -3
  67. aury/boot/contrib/admin_console/install.py +1 -1
  68. aury/boot/domain/models/mixins.py +48 -1
  69. aury/boot/domain/pagination/__init__.py +94 -0
  70. aury/boot/domain/repository/impl.py +1 -1
  71. aury/boot/domain/repository/interface.py +1 -1
  72. aury/boot/domain/transaction/__init__.py +8 -9
  73. aury/boot/infrastructure/__init__.py +86 -29
  74. aury/boot/infrastructure/cache/backends.py +102 -18
  75. aury/boot/infrastructure/cache/base.py +12 -0
  76. aury/boot/infrastructure/cache/manager.py +153 -91
  77. aury/boot/infrastructure/channel/__init__.py +24 -0
  78. aury/boot/infrastructure/channel/backends/__init__.py +9 -0
  79. aury/boot/infrastructure/channel/backends/memory.py +83 -0
  80. aury/boot/infrastructure/channel/backends/redis.py +88 -0
  81. aury/boot/infrastructure/channel/base.py +92 -0
  82. aury/boot/infrastructure/channel/manager.py +203 -0
  83. aury/boot/infrastructure/clients/__init__.py +22 -0
  84. aury/boot/infrastructure/clients/rabbitmq/__init__.py +9 -0
  85. aury/boot/infrastructure/clients/rabbitmq/config.py +46 -0
  86. aury/boot/infrastructure/clients/rabbitmq/manager.py +288 -0
  87. aury/boot/infrastructure/clients/redis/__init__.py +28 -0
  88. aury/boot/infrastructure/clients/redis/config.py +51 -0
  89. aury/boot/infrastructure/clients/redis/manager.py +264 -0
  90. aury/boot/infrastructure/database/config.py +7 -16
  91. aury/boot/infrastructure/database/manager.py +16 -38
  92. aury/boot/infrastructure/events/__init__.py +18 -21
  93. aury/boot/infrastructure/events/backends/__init__.py +11 -0
  94. aury/boot/infrastructure/events/backends/memory.py +86 -0
  95. aury/boot/infrastructure/events/backends/rabbitmq.py +193 -0
  96. aury/boot/infrastructure/events/backends/redis.py +162 -0
  97. aury/boot/infrastructure/events/base.py +127 -0
  98. aury/boot/infrastructure/events/manager.py +224 -0
  99. aury/boot/infrastructure/mq/__init__.py +24 -0
  100. aury/boot/infrastructure/mq/backends/__init__.py +9 -0
  101. aury/boot/infrastructure/mq/backends/rabbitmq.py +179 -0
  102. aury/boot/infrastructure/mq/backends/redis.py +167 -0
  103. aury/boot/infrastructure/mq/base.py +143 -0
  104. aury/boot/infrastructure/mq/manager.py +239 -0
  105. aury/boot/infrastructure/scheduler/manager.py +7 -3
  106. aury/boot/infrastructure/storage/__init__.py +9 -9
  107. aury/boot/infrastructure/storage/base.py +17 -5
  108. aury/boot/infrastructure/storage/factory.py +0 -1
  109. aury/boot/infrastructure/tasks/__init__.py +2 -2
  110. aury/boot/infrastructure/tasks/config.py +5 -13
  111. aury/boot/infrastructure/tasks/manager.py +55 -33
  112. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/METADATA +20 -2
  113. aury_boot-0.0.7.dist-info/RECORD +197 -0
  114. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +0 -1397
  115. aury/boot/commands/templates/project/env.example.tpl +0 -213
  116. aury/boot/infrastructure/events/bus.py +0 -362
  117. aury/boot/infrastructure/events/config.py +0 -52
  118. aury/boot/infrastructure/events/consumer.py +0 -134
  119. aury/boot/infrastructure/events/models.py +0 -63
  120. aury_boot-0.0.4.dist-info/RECORD +0 -137
  121. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/WHEEL +0 -0
  122. {aury_boot-0.0.4.dist-info → aury_boot-0.0.7.dist-info}/entry_points.txt +0 -0
@@ -35,6 +35,12 @@ class ComponentName(str, Enum):
35
35
  # 存储组件
36
36
  STORAGE = "storage"
37
37
 
38
+ # 消息队列组件
39
+ MESSAGE_QUEUE = "message_queue"
40
+
41
+ # 事件总线组件
42
+ EVENT_BUS = "event_bus"
43
+
38
44
  # 迁移组件
39
45
  MIGRATIONS = "migrations"
40
46
 
@@ -9,6 +9,7 @@ from abc import ABC, abstractmethod
9
9
  from typing import TYPE_CHECKING
10
10
 
11
11
  from fastapi import HTTPException, Request, status
12
+ from fastapi.exceptions import RequestValidationError
12
13
  from fastapi.responses import JSONResponse
13
14
  from pydantic import ValidationError
14
15
  from sqlalchemy.exc import IntegrityError, SQLAlchemyError
@@ -121,9 +122,19 @@ class BaseErrorHandler(ErrorHandler):
121
122
 
122
123
  errors = [detail.model_dump() for detail in exception.details] if exception.details else None
123
124
 
125
+ # 兼容 ErrorCode 枚举和字符串
126
+ code_value = exception.code.value if hasattr(exception.code, "value") else exception.code
127
+
128
+ # 尝试转换为 int,如果失败则使用 status_code
129
+ try:
130
+ code_int = int(code_value)
131
+ except (ValueError, TypeError):
132
+ # 非数字字符串(如 "TODO_ATTACHMENT_ERROR"),使用 HTTP 状态码作为 code
133
+ code_int = exception.status_code
134
+
124
135
  response = ResponseBuilder.fail(
125
136
  message=exception.message,
126
- code=int(exception.code.value),
137
+ code=code_int,
127
138
  errors=errors,
128
139
  )
129
140
 
@@ -160,11 +171,14 @@ class HTTPExceptionHandler(ErrorHandler):
160
171
 
161
172
 
162
173
  class ValidationErrorHandler(ErrorHandler):
163
- """Pydantic验证异常处理器。"""
174
+ """验证异常处理器。
175
+
176
+ 处理 Pydantic ValidationError 和 FastAPI RequestValidationError。
177
+ """
164
178
 
165
179
  def can_handle(self, exception: Exception) -> bool:
166
180
  """判断是否为验证异常。"""
167
- return isinstance(exception, ValidationError)
181
+ return isinstance(exception, ValidationError | RequestValidationError)
168
182
 
169
183
  async def handle(self, exception: Exception, request: Request) -> JSONResponse:
170
184
  """处理验证异常。"""
@@ -16,7 +16,8 @@ from starlette.middleware.base import BaseHTTPMiddleware
16
16
  from starlette.requests import Request
17
17
  from starlette.responses import Response
18
18
 
19
- from aury.boot.common.logging import get_trace_id, logger, set_trace_id
19
+ from aury.boot.application.errors import global_exception_handler
20
+ from aury.boot.common.logging import get_request_contexts, logger, set_trace_id
20
21
 
21
22
 
22
23
  def log_request[T](func: Callable[..., T]) -> Callable[..., T]:
@@ -107,10 +108,7 @@ def _should_log_body(content_type: str | None) -> bool:
107
108
  if not content_type:
108
109
  return True
109
110
  content_type = content_type.lower()
110
- for skip_type in SKIP_BODY_CONTENT_TYPES:
111
- if skip_type in content_type:
112
- return False
113
- return True
111
+ return all(skip_type not in content_type for skip_type in SKIP_BODY_CONTENT_TYPES)
114
112
 
115
113
 
116
114
  class RequestLoggingMiddleware(BaseHTTPMiddleware):
@@ -189,6 +187,12 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
189
187
  )
190
188
  logger.log(log_level.upper(), response_log)
191
189
 
190
+ # 记录请求上下文(user_id, tenant_id 等用户注册的字段)
191
+ request_contexts = get_request_contexts()
192
+ if request_contexts:
193
+ ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
194
+ logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
195
+
192
196
  # 写入 access 日志(简洁格式)
193
197
  logger.bind(access=True).info(
194
198
  f"{request.method} {request.url.path} {status_code} {duration:.3f}s"
@@ -211,7 +215,18 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware):
211
215
  f"请求处理失败: {request.method} {request.url.path} | "
212
216
  f"耗时: {duration:.3f}s | Trace-ID: {trace_id}"
213
217
  )
214
- raise
218
+
219
+ # 记录请求上下文(即使异常也要记录,便于追踪问题)
220
+ request_contexts = get_request_contexts()
221
+ if request_contexts:
222
+ ctx_str = " | ".join(f"{k}: {v}" for k, v in request_contexts.items())
223
+ logger.info(f"[REQUEST_CONTEXT] Trace-ID: {trace_id} | {ctx_str}")
224
+
225
+ # 使用全局异常处理器生成响应,而不是直接抛出异常
226
+ # BaseHTTPMiddleware 中直接 raise 会绕过 FastAPI 的异常处理器
227
+ response = await global_exception_handler(request, exc)
228
+ response.headers["x-trace-id"] = trace_id
229
+ return response
215
230
 
216
231
 
217
232
  class WebSocketLoggingMiddleware:
@@ -328,120 +343,6 @@ class WebSocketLoggingMiddleware:
328
343
  raise
329
344
 
330
345
 
331
- class WebSocketLoggingMiddleware:
332
- """WebSocket 日志中间件。
333
-
334
- 记录 WebSocket 连接生命周期和消息收发(可选)。
335
-
336
- 使用示例:
337
- from aury.boot.application.middleware.logging import WebSocketLoggingMiddleware
338
-
339
- app.add_middleware(WebSocketLoggingMiddleware, log_messages=True)
340
- """
341
-
342
- def __init__(
343
- self,
344
- app,
345
- *,
346
- log_messages: bool = False,
347
- max_message_length: int = 500,
348
- ) -> None:
349
- """初始化 WebSocket 日志中间件。
350
-
351
- Args:
352
- app: ASGI 应用
353
- log_messages: 是否记录消息内容(默认 False,注意性能和敏感数据)
354
- max_message_length: 消息内容最大记录长度
355
- """
356
- self.app = app
357
- self.log_messages = log_messages
358
- self.max_message_length = max_message_length
359
-
360
- async def __call__(self, scope, receive, send) -> None:
361
- if scope["type"] != "websocket":
362
- await self.app(scope, receive, send)
363
- return
364
-
365
- # 获取或生成 trace_id
366
- headers = dict(scope.get("headers", []))
367
- trace_id = (
368
- headers.get(b"x-trace-id", b"").decode() or
369
- headers.get(b"x-request-id", b"").decode() or
370
- str(uuid.uuid4())
371
- )
372
- set_trace_id(trace_id)
373
-
374
- path = scope.get("path", "/")
375
- client = scope.get("client")
376
- client_host = f"{client[0]}:{client[1]}" if client else "unknown"
377
-
378
- start_time = time.time()
379
- message_count = {"sent": 0, "received": 0}
380
-
381
- async def logging_receive():
382
- message = await receive()
383
- msg_type = message.get("type", "")
384
-
385
- if msg_type == "websocket.connect":
386
- logger.info(
387
- f"WS → 连接: {path} | "
388
- f"客户端: {client_host} | Trace-ID: {trace_id}"
389
- )
390
- elif msg_type == "websocket.disconnect":
391
- duration = time.time() - start_time
392
- logger.info(
393
- f"WS ← 断开: {path} | "
394
- f"时长: {duration:.1f}s | "
395
- f"收/发: {message_count['received']}/{message_count['sent']} | "
396
- f"Trace-ID: {trace_id}"
397
- )
398
- elif msg_type == "websocket.receive":
399
- message_count["received"] += 1
400
- if self.log_messages:
401
- text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
402
- if len(text) > self.max_message_length:
403
- text = text[:self.max_message_length] + "..."
404
- logger.debug(f"WS → 收: {path} | {text}")
405
-
406
- return message
407
-
408
- async def logging_send(message):
409
- msg_type = message.get("type", "")
410
-
411
- if msg_type == "websocket.send":
412
- message_count["sent"] += 1
413
- if self.log_messages:
414
- text = message.get("text") or message.get("bytes", b"").decode("utf-8", errors="replace")
415
- if len(text) > self.max_message_length:
416
- text = text[:self.max_message_length] + "..."
417
- logger.debug(f"WS ← 发: {path} | {text}")
418
- elif msg_type == "websocket.close":
419
- code = message.get("code", 1000)
420
- reason = message.get("reason", "")
421
- duration = time.time() - start_time
422
- log_level = "warning" if code != 1000 else "info"
423
- logger.log(
424
- log_level.upper(),
425
- f"WS × 关闭: {path} | "
426
- f"Code: {code}{' | 原因: ' + reason if reason else ''} | "
427
- f"时长: {duration:.1f}s | Trace-ID: {trace_id}"
428
- )
429
-
430
- await send(message)
431
-
432
- try:
433
- await self.app(scope, logging_receive, logging_send)
434
- except Exception as exc:
435
- duration = time.time() - start_time
436
- logger.exception(
437
- f"WS ✖ 异常: {path} | "
438
- f"时长: {duration:.1f}s | "
439
- f"收/发: {message_count['received']}/{message_count['sent']} | "
440
- f"Trace-ID: {trace_id}"
441
- )
442
- raise
443
-
444
-
445
346
  __all__ = [
446
347
  "RequestLoggingMiddleware",
447
348
  "WebSocketLoggingMiddleware",
@@ -51,13 +51,13 @@ __all__ = [
51
51
  "BaseRPCClient",
52
52
  "CompositeServiceDiscovery",
53
53
  "ConfigServiceDiscovery",
54
- "create_rpc_client",
55
54
  "DNSServiceDiscovery",
56
- "get_service_discovery",
57
55
  "RPCClient",
58
56
  "RPCError",
59
57
  "RPCResponse",
60
58
  "ServiceDiscovery",
59
+ "create_rpc_client",
60
+ "get_service_discovery",
61
61
  "set_service_discovery",
62
62
  ]
63
63
 
@@ -1,6 +1,21 @@
1
1
  """命令行工具模块。
2
2
 
3
- 统一入口: aum
3
+ 统一入口: aury
4
+
5
+ CLI 继承接口:
6
+ 子框架(如 aury-django、aury-cloud)可以通过以下接口继承基础命令:
7
+
8
+ - register_commands(app): 将所有命令注册到目标 app
9
+ - get_command_modules(): 获取所有命令模块,供进一步定制
10
+
11
+ 示例:
12
+ ```python
13
+ from typer import Typer
14
+ from aury.boot.commands import register_commands
15
+
16
+ app = Typer(name="aury-django")
17
+ register_commands(app) # 继承所有命令
18
+ ```
4
19
  """
5
20
 
6
21
  from __future__ import annotations
@@ -9,22 +24,27 @@ from __future__ import annotations
9
24
  from .config import ProjectConfig, get_project_config, save_project_config
10
25
 
11
26
 
12
- # 延迟导入 appmain,避免加载重型依赖
27
+ # 延迟导入 appmain、register_commands、get_command_modules,避免加载重型依赖
13
28
  def __getattr__(name: str):
14
- if name in ("app", "main"):
15
- from .app import main as _main
29
+ if name in ("app", "main", "register_commands", "get_command_modules"):
30
+ from .app import _get_app, get_command_modules, main, register_commands
16
31
  if name == "main":
17
- return _main
18
- # app 通过 app 模块的 __getattr__ 获取
19
- from . import app as app_module
20
- return app_module.app
32
+ return main
33
+ if name == "app":
34
+ return _get_app()
35
+ if name == "register_commands":
36
+ return register_commands
37
+ if name == "get_command_modules":
38
+ return get_command_modules
21
39
  raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
22
40
 
23
41
 
24
42
  __all__ = [
25
- "app",
26
- "main",
27
43
  "ProjectConfig",
44
+ "app",
45
+ "get_command_modules",
28
46
  "get_project_config",
47
+ "main",
48
+ "register_commands",
29
49
  "save_project_config",
30
50
  ]
aury/boot/commands/app.py CHANGED
@@ -18,6 +18,22 @@
18
18
  aury worker # 运行 Worker
19
19
  aury migrate up # 执行数据库迁移
20
20
  aury docs all --force # 更新所有文档
21
+
22
+ CLI 继承:
23
+ 子框架(如 aury-django、aury-cloud)可以通过 `register_commands` 继承所有基础命令:
24
+
25
+ ```python
26
+ from typer import Typer
27
+ from aury.boot.commands import register_commands
28
+
29
+ app = Typer(name="aury-django")
30
+ register_commands(app) # 继承所有 aury-boot 命令
31
+
32
+ # 添加 django 特有命令
33
+ @app.command()
34
+ def startapp(name: str):
35
+ ...
36
+ ```
21
37
  """
22
38
 
23
39
  from __future__ import annotations
@@ -92,7 +108,119 @@ def main() -> None:
92
108
  _get_app()()
93
109
 
94
110
 
95
- # 为了向后兼容,允许 `from .app import app`
111
+ def register_commands(
112
+ target_app: typer.Typer,
113
+ *,
114
+ include_init: bool = True,
115
+ include_add: bool = True,
116
+ include_generate: bool = True,
117
+ include_server: bool = True,
118
+ include_scheduler: bool = True,
119
+ include_worker: bool = True,
120
+ include_migrate: bool = True,
121
+ include_docker: bool = True,
122
+ include_docs: bool = True,
123
+ ) -> None:
124
+ """将 aury-boot 的所有命令注册到目标 Typer app。
125
+
126
+ 用于子框架(如 aury-django、aury-cloud)继承基础命令。
127
+
128
+ Args:
129
+ target_app: 目标 Typer 应用
130
+ include_*: 是否包含对应的命令组
131
+
132
+ 使用示例:
133
+ ```python
134
+ from typer import Typer
135
+ from aury.boot.commands import register_commands
136
+
137
+ app = Typer(name="aury-django")
138
+
139
+ # 继承所有 aury-boot 命令
140
+ register_commands(app)
141
+
142
+ # 或选择性继承
143
+ register_commands(app, include_docker=False)
144
+
145
+ # 添加 django 特有命令
146
+ django_app = Typer(name="django")
147
+
148
+ @django_app.command()
149
+ def startapp(name: str):
150
+ '''Django startapp'''
151
+ ...
152
+
153
+ app.add_typer(django_app, name="django")
154
+ ```
155
+ """
156
+ # 延迟导入子命令
157
+ if include_init:
158
+ from .init import init
159
+ target_app.command(name="init", help="🎯 初始化项目脚手架")(init)
160
+
161
+ if include_add:
162
+ from .add import app as add_app
163
+ target_app.add_typer(add_app, name="add", help="➕ 添加可选模块")
164
+
165
+ if include_generate:
166
+ from .generate import app as generate_app
167
+ target_app.add_typer(generate_app, name="generate", help="⚡ 代码生成器")
168
+
169
+ if include_server:
170
+ from .server import app as server_app
171
+ target_app.add_typer(server_app, name="server", help="🖥️ 服务器管理")
172
+
173
+ if include_scheduler:
174
+ from .scheduler import app as scheduler_app
175
+ target_app.add_typer(scheduler_app, name="scheduler", help="🕐 独立运行调度器")
176
+
177
+ if include_worker:
178
+ from .worker import app as worker_app
179
+ target_app.add_typer(worker_app, name="worker", help="⚙️ 运行任务队列 Worker")
180
+
181
+ if include_migrate:
182
+ from .migrate import app as migrate_app
183
+ target_app.add_typer(migrate_app, name="migrate", help="🗃️ 数据库迁移")
184
+
185
+ if include_docker:
186
+ from .docker import app as docker_app
187
+ target_app.add_typer(docker_app, name="docker", help="🐳 Docker 配置")
188
+
189
+ if include_docs:
190
+ from .docs import app as docs_app
191
+ target_app.add_typer(docs_app, name="docs", help="📚 生成/更新项目文档")
192
+
193
+
194
+ def get_command_modules() -> dict[str, type]:
195
+ """获取所有命令模块,供子框架进一步定制。
196
+
197
+ Returns:
198
+ dict: 命令名 -> 模块对象
199
+
200
+ 使用示例:
201
+ ```python
202
+ from aury.boot.commands import get_command_modules
203
+
204
+ modules = get_command_modules()
205
+ # {'init': <module>, 'add': <module>, 'server': <module>, ...}
206
+ ```
207
+ """
208
+ from . import add, docker, docs, generate, init, migrate, scheduler, server, worker
209
+
210
+ return {
211
+ "init": init,
212
+ "add": add,
213
+ "generate": generate,
214
+ "server": server,
215
+ "scheduler": scheduler,
216
+ "worker": worker,
217
+ "migrate": migrate,
218
+ "docker": docker,
219
+ "docs": docs,
220
+ }
221
+
222
+
223
+ # 允许 `from .app import app`
96
224
  def __getattr__(name: str):
97
225
  if name == "app":
98
226
  return _get_app()
@@ -101,5 +229,7 @@ def __getattr__(name: str):
101
229
 
102
230
  __all__ = [
103
231
  "app",
232
+ "get_command_modules",
104
233
  "main",
234
+ "register_commands",
105
235
  ]
@@ -1,13 +1,15 @@
1
1
  """文档生成命令。
2
2
 
3
3
  提供命令行工具用于在现有项目中生成/更新文档:
4
- - aury docs dev 生成/更新 DEVELOPMENT.md
4
+ - aury docs agents 生成/更新 AGENTS.md(AI 编程助手上下文)
5
+ - aury docs dev 生成/更新 docs/ 目录(开发文档包)
5
6
  - aury docs cli 生成/更新 CLI.md
6
7
  - aury docs env 生成/更新 .env.example
7
8
  - aury docs all 生成/更新所有文档
8
9
 
9
10
  使用示例:
10
- aury docs dev # 生成开发文档
11
+ aury docs agents # 生成 AI 编程助手上下文文档
12
+ aury docs dev # 生成 docs/ 开发文档包
11
13
  aury docs cli # 生成 CLI 文档
12
14
  aury docs env # 生成环境变量示例
13
15
  aury docs all # 生成所有文档
@@ -79,10 +81,17 @@ def _detect_project_info(project_dir: Path) -> dict[str, str]:
79
81
 
80
82
 
81
83
  def _render_template(template_name: str, context: dict[str, str]) -> str:
82
- """渲染模板。"""
84
+ """渲染模板。
85
+
86
+ 支持根目录模板和 aury_docs/ 子目录模板。
87
+ """
88
+ # 先在根目录找
83
89
  template_path = TEMPLATES_DIR / template_name
84
90
  if not template_path.exists():
85
- raise FileNotFoundError(f"模板文件不存在: {template_path}")
91
+ # 再在 aury_docs/ 子目录找
92
+ template_path = AURY_DOCS_TPL_DIR / template_name
93
+ if not template_path.exists():
94
+ raise FileNotFoundError(f"模板文件不存在: {template_name}")
86
95
 
87
96
  content = template_path.read_text(encoding="utf-8")
88
97
  return content.format(**context)
@@ -116,8 +125,8 @@ def _write_file(
116
125
  return True
117
126
 
118
127
 
119
- @app.command(name="dev")
120
- def generate_dev_doc(
128
+ @app.command(name="agents")
129
+ def generate_agents_doc(
121
130
  project_dir: Path = typer.Argument(
122
131
  Path("."),
123
132
  help="项目目录路径",
@@ -139,20 +148,84 @@ def generate_dev_doc(
139
148
  help="预览模式,不实际写入文件",
140
149
  ),
141
150
  ) -> None:
142
- """生成/更新 DEVELOPMENT.md 开发文档。"""
151
+ """生成/更新 AGENTS.md(AI 编程助手上下文文档)。"""
143
152
  context = _detect_project_info(project_dir)
144
153
 
145
154
  console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
146
155
 
147
156
  try:
148
- content = _render_template("DEVELOPMENT.md.tpl", context)
149
- output_path = project_dir / "DEVELOPMENT.md"
157
+ content = _render_template("AGENTS.md.tpl", context)
158
+ output_path = project_dir / "AGENTS.md"
150
159
  _write_file(output_path, content, force=force, dry_run=dry_run)
151
160
  except Exception as e:
152
161
  console.print(f"[red]❌ 生成失败: {e}[/red]")
153
162
  raise typer.Exit(1)
154
163
 
155
164
 
165
+ # aury_docs/ 模板目录
166
+ AURY_DOCS_TPL_DIR = TEMPLATES_DIR / "aury_docs"
167
+
168
+
169
+ def _get_aury_docs_templates() -> list[Path]:
170
+ """动态扫描 aury_docs/ 模板目录。"""
171
+ if not AURY_DOCS_TPL_DIR.exists():
172
+ return []
173
+ return sorted(AURY_DOCS_TPL_DIR.glob("*.md.tpl"))
174
+
175
+
176
+ @app.command(name="dev")
177
+ def generate_dev_doc(
178
+ project_dir: Path = typer.Argument(
179
+ Path("."),
180
+ help="项目目录路径",
181
+ exists=True,
182
+ file_okay=False,
183
+ dir_okay=True,
184
+ resolve_path=True,
185
+ ),
186
+ force: bool = typer.Option(
187
+ False,
188
+ "--force",
189
+ "-f",
190
+ help="强制覆盖已存在的文件",
191
+ ),
192
+ dry_run: bool = typer.Option(
193
+ False,
194
+ "--dry-run",
195
+ "-n",
196
+ help="预览模式,不实际写入文件",
197
+ ),
198
+ ) -> None:
199
+ """生成/更新 aury_docs/ 开发文档包。"""
200
+ context = _detect_project_info(project_dir)
201
+
202
+ console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
203
+ console.print()
204
+
205
+ # 确保输出目录存在
206
+ aury_docs_dir = project_dir / "aury_docs"
207
+ if not dry_run:
208
+ aury_docs_dir.mkdir(parents=True, exist_ok=True)
209
+
210
+ success_count = 0
211
+ for tpl_path in _get_aury_docs_templates():
212
+ try:
213
+ output_name = tpl_path.stem # 去掉 .tpl 后缀,保留 .md
214
+ output_path = aury_docs_dir / output_name
215
+ content = tpl_path.read_text(encoding="utf-8")
216
+ content = content.format(**context)
217
+ if _write_file(output_path, content, force=force, dry_run=dry_run):
218
+ success_count += 1
219
+ except Exception as e:
220
+ console.print(f"[red]❌ 生成 {tpl_path.name} 失败: {e}[/red]")
221
+
222
+ console.print()
223
+ if dry_run:
224
+ console.print(f"[dim]🔍 预览模式完成,将生成 {success_count} 个文档到 aury_docs/ 目录[/dim]")
225
+ else:
226
+ console.print(f"[green]✨ 完成!成功生成 {success_count} 个文档到 aury_docs/ 目录[/green]")
227
+
228
+
156
229
  @app.command(name="cli")
157
230
  def generate_cli_doc(
158
231
  project_dir: Path = typer.Argument(
@@ -176,14 +249,18 @@ def generate_cli_doc(
176
249
  help="预览模式,不实际写入文件",
177
250
  ),
178
251
  ) -> None:
179
- """生成/更新 CLI.md 命令行文档。"""
252
+ """生成/更新 aury_docs/99-cli.md 命令行文档。"""
180
253
  context = _detect_project_info(project_dir)
181
254
 
182
255
  console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
183
256
 
184
257
  try:
185
- content = _render_template("CLI.md.tpl", context)
186
- output_path = project_dir / "CLI.md"
258
+ tpl_path = AURY_DOCS_TPL_DIR / "99-cli.md.tpl"
259
+ content = tpl_path.read_text(encoding="utf-8")
260
+ content = content.format(**context)
261
+ output_path = project_dir / "aury_docs" / "99-cli.md"
262
+ if not dry_run:
263
+ output_path.parent.mkdir(parents=True, exist_ok=True)
187
264
  _write_file(output_path, content, force=force, dry_run=dry_run)
188
265
  except Exception as e:
189
266
  console.print(f"[red]❌ 生成失败: {e}[/red]")
@@ -250,20 +327,30 @@ def generate_all_docs(
250
327
  help="预览模式,不实际写入文件",
251
328
  ),
252
329
  ) -> None:
253
- """生成/更新所有文档(DEVELOPMENT.md, CLI.md, .env.example)。"""
330
+ """生成/更新所有文档(AGENTS.md, docs/, CLI.md, .env.example)。"""
254
331
  context = _detect_project_info(project_dir)
255
332
 
256
333
  console.print(f"[cyan]📚 检测到项目: {context['project_name']}[/cyan]")
257
334
  console.print()
258
335
 
259
- docs_to_generate = [
260
- ("DEVELOPMENT.md.tpl", "DEVELOPMENT.md", "开发文档"),
261
- ("CLI.md.tpl", "CLI.md", "CLI 文档"),
336
+ # 根目录文档
337
+ root_docs: list[tuple[str, str, str]] = [
338
+ ("AGENTS.md.tpl", "AGENTS.md", "AI 编程助手上下文"),
262
339
  ("env.example.tpl", ".env.example", "环境变量示例"),
263
340
  ]
264
341
 
342
+ # aury_docs/ 开发文档
343
+ aury_docs_templates = _get_aury_docs_templates()
344
+ dev_docs = [
345
+ (tpl.name, f"aury_docs/{tpl.stem}", f"开发文档: {tpl.stem}")
346
+ for tpl in aury_docs_templates
347
+ ]
348
+
349
+ # 合并所有文档
350
+ all_docs = root_docs + dev_docs
351
+
265
352
  success_count = 0
266
- for template_name, output_name, description in docs_to_generate:
353
+ for template_name, output_name, description in all_docs:
267
354
  try:
268
355
  content = _render_template(template_name, context)
269
356
  output_path = project_dir / output_name