create-dwy 0.5.1 → 0.6.1

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 (156) hide show
  1. package/bin/index.js +6 -15
  2. package/package.json +3 -3
  3. package/src/create.js +61 -37
  4. package/src/sync.js +206 -61
  5. package/templates/claude-global/CLAUDE.md +92 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +610 -0
  7. package/templates/claude-global/rules/dwy-api-design.md +110 -0
  8. package/templates/claude-global/rules/dwy-db-migration.md +46 -0
  9. package/templates/claude-global/rules/dwy-flutter-core.md +564 -0
  10. package/templates/claude-global/rules/dwy-flutter-network.md +222 -0
  11. package/templates/claude-global/rules/dwy-flutter-router.md +97 -0
  12. package/templates/claude-global/rules/dwy-flutter-testing.md +94 -0
  13. package/templates/claude-global/rules/dwy-git-commit-convention.md +82 -0
  14. package/templates/claude-global/rules/dwy-git-security.md +53 -0
  15. package/templates/claude-global/rules/dwy-payload-limits.md +178 -0
  16. package/templates/claude-global/rules/{prefer-open-source.md → dwy-prefer-open-source.md} +4 -0
  17. package/templates/claude-global/rules/dwy-project-structure.md +181 -0
  18. package/templates/claude-global/rules/{python-code-style.md → dwy-python-core.md} +292 -358
  19. package/templates/claude-global/rules/dwy-python-fastapi.md +272 -0
  20. package/templates/claude-global/rules/dwy-python-orm.md +61 -0
  21. package/templates/claude-global/rules/{backend-security.md → dwy-python-security.md} +99 -72
  22. package/templates/claude-global/rules/dwy-python-testing.md +53 -0
  23. package/templates/claude-global/rules/{server-security.md → dwy-server-security.md} +1 -2
  24. package/templates/claude-global/rules/dwy-tdd-development.md +70 -0
  25. package/templates/claude-global/rules/dwy-tech-stack.md +60 -0
  26. package/templates/claude-global/rules/dwy-vue-api.md +146 -0
  27. package/templates/claude-global/rules/{vue-code-style.md → dwy-vue-core.md} +338 -152
  28. package/templates/claude-global/rules/dwy-vue-pinia.md +75 -0
  29. package/templates/claude-global/rules/dwy-vue-testing.md +102 -0
  30. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  47. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  48. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  49. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  50. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  51. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  53. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  54. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  55. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  56. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  60. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  61. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  62. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  64. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  65. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  71. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  72. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  92. package/templates/claude-global/skills/{dwy-release → dwy-publish}/SKILL.md +42 -18
  93. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  94. package/templates/project/backend/.dwy.hbs +5 -0
  95. package/templates/project/backend/.python-version +1 -0
  96. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  97. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  98. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  99. package/templates/project/backend/deploy.sh.hbs +18 -0
  100. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  101. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  102. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  103. package/templates/project/backend/src/app/database.py +10 -0
  104. package/templates/project/backend/src/app/dependencies.py +5 -0
  105. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  106. package/templates/project/backend/src/app/models/base.py +5 -0
  107. package/templates/project/web/CLAUDE.md.hbs +29 -36
  108. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  109. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  110. package/templates/project/web/apps/web/src/App.vue +9 -0
  111. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  112. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  113. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  114. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  115. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  116. package/templates/project/web/apps/web/src/style.css +2 -0
  117. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  118. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  119. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  120. package/templates/project/web/apps/web/tsconfig.json +6 -0
  121. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  122. package/templates/project/web/package.json.hbs +16 -0
  123. package/templates/project/web/packages/.gitkeep +0 -0
  124. package/templates/project/web/pnpm-workspace.yaml +3 -0
  125. package/templates/claude-global/hooks/post-sync-claude-global.sh +0 -72
  126. package/templates/claude-global/rules/agent-team-execution.md +0 -138
  127. package/templates/claude-global/rules/agent-team-patterns.md +0 -157
  128. package/templates/claude-global/rules/git-commit-convention.md +0 -106
  129. package/templates/claude-global/rules/git-security.md +0 -144
  130. package/templates/claude-global/skills/dwy-backend-base/SKILL.md +0 -279
  131. package/templates/claude-global/skills/dwy-frontend-core/SKILL.md +0 -181
  132. package/templates/project/web/.claude/commands/deploy.md +0 -4
  133. package/templates/project/web/backend/src/app/models/base.py +0 -5
  134. package/templates/project/web/deploy.sh.hbs +0 -113
  135. package/templates/project/web/frontend/src/App.vue +0 -3
  136. package/templates/project/web/frontend/src/api/client.ts +0 -13
  137. package/templates/project/web/frontend/src/style.css +0 -1
  138. package/templates/project/web/frontend/src/views/HomeView.vue +0 -5
  139. package/templates/project/web/frontend/tsconfig.app.json +0 -14
  140. package/templates/project/web/frontend/tsconfig.json +0 -4
  141. /package/templates/{project/web/backend → claude-global/skills/dwy-pentest/scripts}/.python-version +0 -0
  142. /package/templates/project/{web/backend → backend}/Dockerfile +0 -0
  143. /package/templates/project/{web/backend → backend}/alembic/env.py.hbs +0 -0
  144. /package/templates/{claude-global/skills → project/backend/alembic/versions}/.gitkeep +0 -0
  145. /package/templates/project/{web → backend}/docker-compose.dev.yml.hbs +0 -0
  146. /package/templates/project/{web/backend → backend}/src/app/__init__.py +0 -0
  147. /package/templates/project/{web/backend → backend}/src/app/models/__init__.py +0 -0
  148. /package/templates/project/{web/backend → backend}/src/app/routers/__init__.py +0 -0
  149. /package/templates/project/{web/backend → backend}/src/app/routers/health.py +0 -0
  150. /package/templates/project/{web/backend → backend}/src/app/schemas/__init__.py +0 -0
  151. /package/templates/project/{web/backend → backend}/src/app/services/__init__.py +0 -0
  152. /package/templates/project/{web/backend → backend}/tests/__init__.py +0 -0
  153. /package/templates/project/{web/backend → backend}/tests/conftest.py.hbs +0 -0
  154. /package/templates/project/{web/backend/alembic/versions → mobile}/.gitkeep +0 -0
  155. /package/templates/project/web/{frontend → apps/web}/src/components/.gitkeep +0 -0
  156. /package/templates/project/web/{frontend → apps/web}/src/main.ts +0 -0
@@ -0,0 +1,387 @@
1
+ # dwyeapi Tasks 集成指南
2
+
3
+ 基于 ARQ 的全异步耗时任务处理系统。开箱即用,3 步接入。
4
+
5
+ ## 1. 安装
6
+
7
+ ```bash
8
+ pip install dwyeapi[tasks]
9
+ # 或
10
+ uv add "dwyeapi[tasks]"
11
+ ```
12
+
13
+ 自动安装 `arq` 异步任务队列依赖。需要运行中的 Redis 实例。
14
+
15
+ ## 2. 快速接入
16
+
17
+ ### 第一步:写任务函数
18
+
19
+ ```python
20
+ # app/tasks.py
21
+ from dwyeapi.tasks import register, TaskContext
22
+
23
+
24
+ @register("process_data")
25
+ async def process_data(ctx: TaskContext, params: dict):
26
+ """数据处理任务。"""
27
+ file_path = params["file"]
28
+ await ctx.log(f"开始处理文件: {file_path}")
29
+
30
+ async with ctx.db() as session:
31
+ # 查询数据库
32
+ result = await session.execute(select(RawData).where(...))
33
+ rows = result.scalars().all()
34
+
35
+ for i, row in enumerate(rows):
36
+ # 业务处理...
37
+ await ctx.update_progress(int((i + 1) / len(rows) * 100))
38
+
39
+ if await ctx.is_cancelled():
40
+ await ctx.log("用户取消, 已停止")
41
+ return
42
+
43
+ return {"processed": len(rows)}
44
+
45
+
46
+ @register("export_file")
47
+ async def export_file(ctx: TaskContext, params: dict):
48
+ """文件导出任务。"""
49
+ await ctx.log("开始导出")
50
+ # ... 业务逻辑 ...
51
+ return {"file_url": "/files/report.csv"}
52
+ ```
53
+
54
+ ### 第二步:FastAPI 接入
55
+
56
+ ```python
57
+ # app/main.py
58
+ from contextlib import asynccontextmanager
59
+
60
+ from fastapi import FastAPI
61
+
62
+ from dwyeapi.database import create_async_engine_factory, create_session_factory
63
+ from dwyeapi.exceptions import register_exception_handlers
64
+ from dwyeapi.tasks import setup_tasks, task_router
65
+
66
+ from app.config import settings
67
+
68
+ import app.tasks # noqa: 触发 @register 装饰器
69
+
70
+ engine = create_async_engine_factory(settings.database_url)
71
+ session_factory = create_session_factory(engine)
72
+
73
+
74
+ @asynccontextmanager
75
+ async def lifespan(app: FastAPI):
76
+ await setup_tasks(app, settings, session_factory)
77
+ yield
78
+
79
+
80
+ app = FastAPI(lifespan=lifespan)
81
+ register_exception_handlers(app)
82
+ app.include_router(task_router) # 挂载 /tasks API
83
+ ```
84
+
85
+ ### 第三步:启动 Worker
86
+
87
+ ```python
88
+ # app/worker.py
89
+ from dwyeapi.database import create_async_engine_factory, create_session_factory
90
+ from dwyeapi.tasks import create_worker_settings
91
+
92
+ from app.config import settings
93
+
94
+ import app.tasks # noqa: 触发 @register 装饰器
95
+
96
+ engine = create_async_engine_factory(settings.database_url)
97
+ session_factory = create_session_factory(engine)
98
+ WorkerSettings = create_worker_settings(settings, session_factory=session_factory)
99
+ ```
100
+
101
+ ```bash
102
+ # 终端 1: FastAPI
103
+ uvicorn app.main:app --reload
104
+
105
+ # 终端 2: Worker
106
+ arq app.worker.WorkerSettings
107
+
108
+ # 终端 3 (可选): Redis
109
+ redis-server
110
+ ```
111
+
112
+ ## 3. .env 配置
113
+
114
+ ```env
115
+ DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydb
116
+ REDIS_URL=redis://localhost:6379/0
117
+ SECRET_KEY=your-secret-key
118
+
119
+ # 任务模块配置 (均有默认值, 可不配)
120
+ TASK_MAX_JOBS=5 # Worker 最大并发任务数 (默认 5)
121
+ TASK_JOB_TIMEOUT=3600 # 单个任务超时秒数 (默认 3600)
122
+ TASK_FAILURE_TTL=86400 # 失败任务在 Redis 中保留秒数 (默认 86400)
123
+ ```
124
+
125
+ ## 4. API 接口
126
+
127
+ 挂载 `task_router` 后自动获得以下接口:
128
+
129
+ | 方法 | 路径 | 说明 |
130
+ |------|------|------|
131
+ | POST | `/tasks` | 提交任务 |
132
+ | GET | `/tasks/{task_id}` | 查询单个任务状态 |
133
+ | GET | `/tasks` | 任务列表 (分页 + 筛选) |
134
+ | POST | `/tasks/{task_id}/cancel` | 取消任务 |
135
+
136
+ ### 提交任务
137
+
138
+ ```bash
139
+ curl -X POST http://localhost:8000/tasks \
140
+ -H "Content-Type: application/json" \
141
+ -d '{"task_type": "process_data", "params": {"file": "data.csv"}}'
142
+ ```
143
+
144
+ ```json
145
+ {
146
+ "code": 200,
147
+ "message": "success",
148
+ "data": {
149
+ "id": "task_a1b2c3d4...",
150
+ "task_type": "process_data",
151
+ "status": "pending",
152
+ "progress": 0,
153
+ "logs": "[2026-04-05 10:00:00] 任务已创建, 等待执行\n",
154
+ "result": null,
155
+ "params": {"file": "data.csv"},
156
+ "created_at": "2026-04-05T10:00:00",
157
+ "updated_at": "2026-04-05T10:00:00"
158
+ },
159
+ "timestamp": 1775000000
160
+ }
161
+ ```
162
+
163
+ ### 查询状态 (轮询)
164
+
165
+ ```bash
166
+ curl http://localhost:8000/tasks/task_a1b2c3d4...
167
+ ```
168
+
169
+ `status` 会从 `pending` → `running` → `success`/`failed`/`canceled` 变化,`progress` 从 0 递增到 100。
170
+
171
+ ### 任务列表
172
+
173
+ ```bash
174
+ # 分页 + 状态筛选
175
+ curl "http://localhost:8000/tasks?page=1&page_size=20&status=running"
176
+
177
+ # 按类型筛选
178
+ curl "http://localhost:8000/tasks?task_type=process_data"
179
+ ```
180
+
181
+ ### 取消任务
182
+
183
+ ```bash
184
+ curl -X POST http://localhost:8000/tasks/task_a1b2c3d4.../cancel
185
+ ```
186
+
187
+ 仅 `pending` 和 `running` 状态的任务可取消。已结束的任务返回 400。
188
+
189
+ ### 自定义路由前缀
190
+
191
+ ```python
192
+ app.include_router(task_router, prefix="/api/jobs") # /api/jobs/*
193
+ ```
194
+
195
+ ## 5. TaskContext API
196
+
197
+ 任务函数收到的 `ctx` 对象,提供以下方法:
198
+
199
+ | 方法 | 签名 | 说明 |
200
+ |------|------|------|
201
+ | ctx.log | `async (message: str) -> None` | 追加带时间戳的日志 |
202
+ | ctx.update_progress | `async (value: int, message: str = "") -> None` | 更新进度 0-100, 可选附日志 |
203
+ | ctx.is_cancelled | `async () -> bool` | 检查是否被取消 (读 Redis 标记) |
204
+ | ctx.db | `() -> AsyncContextManager[AsyncSession]` | 获取数据库 session |
205
+
206
+ ### 属性
207
+
208
+ | 属性 | 类型 | 说明 |
209
+ |------|------|------|
210
+ | ctx.task_id | str | 当前任务唯一 ID |
211
+ | ctx.task_type | str | 任务类型 |
212
+ | ctx.params | dict | 提交时的入参 |
213
+
214
+ ### 生命周期 (框架自动管理)
215
+
216
+ ```
217
+ 任务入队
218
+ → 状态设为 RUNNING + 记日志 "任务开始执行"
219
+ → 调用业务函数 func(ctx, params)
220
+ → 正常返回 dict → 状态设为 SUCCESS, result = 返回值, progress = 100
221
+ → 抛异常 → 状态设为 FAILED, result = {"error": "..."}
222
+ → 取消退出 → 状态设为 CANCELED
223
+ ```
224
+
225
+ 业务代码**不需要**手动管理 RUNNING/SUCCESS/FAILED 状态转换。
226
+
227
+ ## 6. 任务注册
228
+
229
+ ### @register 装饰器
230
+
231
+ ```python
232
+ from dwyeapi.tasks import register, TaskContext
233
+
234
+ @register("unique_task_type")
235
+ async def my_task(ctx: TaskContext, params: dict):
236
+ ...
237
+ ```
238
+
239
+ - `task_type` 全局唯一,重复注册抛 `ValueError`
240
+ - 必须是 `async def`
241
+ - 签名固定为 `(ctx: TaskContext, params: dict)`
242
+ - 返回 `dict` 或 `None`
243
+
244
+ ### 确保装饰器被执行
245
+
246
+ 任务模块必须被导入才能触发 `@register`:
247
+
248
+ ```python
249
+ # main.py 和 worker.py 中都要加
250
+ import app.tasks # noqa
251
+ ```
252
+
253
+ ## 7. 任务取消机制
254
+
255
+ 协作式取消 — 框架设置 Redis 标记,任务函数自行检查并退出。
256
+
257
+ ```python
258
+ @register("batch_import")
259
+ async def batch_import(ctx: TaskContext, params: dict):
260
+ for batch in batches:
261
+ # 每批处理前检查
262
+ if await ctx.is_cancelled():
263
+ await ctx.log("用户取消, 已导入到第 N 批")
264
+ return # 框架自动设为 CANCELED
265
+
266
+ await process_batch(batch)
267
+ await ctx.update_progress(...)
268
+ ```
269
+
270
+ **注意:** 取消不会回滚已执行的操作。已插入的数据不会被撤销。如需回滚,在检测到取消后执行自定义清理逻辑。
271
+
272
+ ## 8. 任务状态
273
+
274
+ | 状态 | 值 | 说明 |
275
+ |------|------|------|
276
+ | PENDING | `"pending"` | 已创建, 等待 Worker 执行 |
277
+ | RUNNING | `"running"` | 正在执行中 |
278
+ | SUCCESS | `"success"` | 执行成功 |
279
+ | FAILED | `"failed"` | 执行失败 (异常) |
280
+ | CANCELED | `"canceled"` | 已取消 |
281
+
282
+ 终态:SUCCESS / FAILED / CANCELED,不可再变更。
283
+
284
+ ## 9. Task 数据模型
285
+
286
+ Task 表自动随业务数据库创建 (复用 eapi 的 `Base`)。
287
+
288
+ | 字段 | 类型 | 说明 |
289
+ |------|------|------|
290
+ | id | String(64) | 主键, 自动生成 `task_{uuid}` |
291
+ | task_type | String(50) | 任务类型, 有索引 |
292
+ | status | Enum(TaskStatus) | 任务状态 |
293
+ | params | JSON | 提交时的入参 |
294
+ | result | JSON, nullable | 执行结果 |
295
+ | progress | Integer | 进度 0-100 |
296
+ | logs | Text | 带时间戳的执行日志 |
297
+ | created_at | DateTime | 创建时间 (TimestampMixin) |
298
+ | updated_at | DateTime | 更新时间 (TimestampMixin) |
299
+
300
+ ## 10. Worker 配置
301
+
302
+ ### create_worker_settings
303
+
304
+ 从 eapi 的 `BaseSettings` 自动生成 ARQ Worker 配置:
305
+
306
+ ```python
307
+ from dwyeapi.tasks import create_worker_settings
308
+
309
+ WorkerSettings = create_worker_settings(settings, session_factory=session_factory)
310
+ ```
311
+
312
+ 自动映射:
313
+ - `settings.redis_url` → ARQ `RedisSettings`
314
+ - `settings.task_max_jobs` → `max_jobs` (默认 5)
315
+ - `settings.task_job_timeout` → `job_timeout` (默认 3600s)
316
+
317
+ ### 并发控制
318
+
319
+ ```env
320
+ # 单 Worker, 5 个协程并发
321
+ TASK_MAX_JOBS=5
322
+
323
+ # 多 Worker 实例: 总并发 = max_jobs x Worker 数
324
+ # 终端 1: arq app.worker.WorkerSettings
325
+ # 终端 2: arq app.worker.WorkerSettings
326
+ # 总并发 = 5 + 5 = 10
327
+ ```
328
+
329
+ IO 密集型任务 (文件导出、第三方 API) 可设 5-10;CPU 密集型任务 (AI 推理) 建议等于 CPU 核心数。
330
+
331
+ ## 11. 公共 API 速查
332
+
333
+ ```python
334
+ from dwyeapi.tasks import (
335
+ setup_tasks, # 一站式初始化
336
+ task_router, # 开箱即用 APIRouter
337
+ register, # @register 装饰器
338
+ TaskContext, # 任务上下文类型
339
+ TaskStatus, # 状态枚举
340
+ create_worker_settings, # Worker 工厂
341
+ )
342
+ ```
343
+
344
+ | 导出项 | 用途 |
345
+ |--------|------|
346
+ | `setup_tasks(app, settings, session_factory)` | lifespan 中调用, 初始化 ARQ pool |
347
+ | `task_router` | APIRouter, include_router 挂载 |
348
+ | `register(task_type)` | 装饰器, 注册异步任务函数 |
349
+ | `TaskContext` | 任务函数第一个参数的类型 |
350
+ | `TaskStatus` | 枚举: PENDING/RUNNING/SUCCESS/FAILED/CANCELED |
351
+ | `create_worker_settings(settings, session_factory)` | 生成 ARQ WorkerSettings 类 |
352
+
353
+ ## 12. 完整最小示例
354
+
355
+ ```
356
+ myproject/
357
+ ├── app/
358
+ │ ├── config.py # Settings(BaseSettings)
359
+ │ ├── main.py # FastAPI + setup_tasks + task_router
360
+ │ ├── tasks.py # @register 任务函数
361
+ │ └── worker.py # WorkerSettings
362
+ ├── .env
363
+ └── pyproject.toml
364
+ ```
365
+
366
+ ```toml
367
+ # pyproject.toml
368
+ [project]
369
+ dependencies = ["dwyeapi[tasks]", "asyncpg"]
370
+ ```
371
+
372
+ ```env
373
+ # .env
374
+ DATABASE_URL=postgresql+asyncpg://user:pass@localhost/mydb
375
+ REDIS_URL=redis://localhost:6379/0
376
+ SECRET_KEY=your-secret-key
377
+ ```
378
+
379
+ ```bash
380
+ # 启动
381
+ uvicorn app.main:app --reload # API
382
+ arq app.worker.WorkerSettings # Worker
383
+
384
+ # 测试
385
+ curl -X POST http://localhost:8000/tasks \
386
+ -d '{"task_type": "process_data", "params": {"file": "test.csv"}}'
387
+ ```
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: dwy-ekit
3
+ description: "@dwydev/ekit 前端工具库速查(v0.7.0)。触发条件:HTTP 请求、dwyeapi 响应契约对接、localStorage/Cookie、表单校验、日期/时区、剪贴板、查询字符串、文件下载、PII 脱敏、Vue composable 工具。"
4
+ ---
5
+
6
+ # @dwydev/ekit 工具库速查(v0.7.0)
7
+
8
+ Vue 3 项目通用工具库,**薄封装层**:底层用 axios / dayjs / zod / js-cookie / qs / file-saver / @vueuse/core,**对外只暴露 ekit 自有契约**,不泄露底层库类型。
9
+
10
+ 包含 10 个模块:`request` / `storage` / `cookie` / `date` / `validators` / `copy` / `qs` / `file` / `hooks` / `masking`。
11
+
12
+ ## 安装
13
+
14
+ ```bash
15
+ pnpm add @dwydev/ekit
16
+ # peerDependencies: vue ^3.5.0, axios ^1.0.0, dayjs ^1.11.0, zod ^3.25.0, @vueuse/core ^14.2.1
17
+ ```
18
+
19
+ ```ts
20
+ import {
21
+ createRequest, useStorage, useCookie,
22
+ formatDateTime, isPhone, copyText, stringify, downloadFile,
23
+ useDebounce, maskPhone,
24
+ } from '@dwydev/ekit'
25
+ ```
26
+
27
+ ## 防腐层硬性约束(写代码前必读)
28
+
29
+ ekit 对外类型 **不允许** 出现底层库类名。下面这些导入 **会失败** —— 因为 ekit 不再 re-export:
30
+
31
+ ```ts
32
+ // ❌ 错误:ekit 不导出底层类型
33
+ import type { AxiosInstance, IStringifyOptions, CookieAttributes } from '@dwydev/ekit'
34
+ import { dayjs } from '@dwydev/ekit'
35
+
36
+ // ✅ 正确:用 ekit 自有契约
37
+ import type { HttpClient, StringifyOptions, CookieOptions } from '@dwydev/ekit'
38
+ import { now, formatTimestamp } from '@dwydev/ekit' // 不需要 dayjs 实例
39
+ ```
40
+
41
+ 业务代码也 **禁止** 直接调底层 API:
42
+
43
+ | 禁止 | 改用 |
44
+ |------|------|
45
+ | `new Date()` / `toLocaleString()` | `now()` / `formatTimestamp()` / `formatDateTime()` |
46
+ | `localStorage.setItem(...)` | `storage.set()` 或 `useStorage()` |
47
+ | `navigator.clipboard.writeText(...)` | `copyText()` 或 `useClipboard()` |
48
+ | `document.cookie = ...` | `cookie.set()` 或 `useCookie()` |
49
+ | `document.addEventListener('click', ...)` | `useEventListener()` |
50
+ | 手写 axios 实例 | `createRequest()` |
51
+
52
+ ## 查阅源码
53
+
54
+ 每个模块源码在 `/Users/chances/WebstormProjects/dwy-shared/frontend/ekit/src/{module}/`,函数都很短可以直接读。
55
+
56
+ ## 模块索引
57
+
58
+ 按需读取对应 references 文件,避免一次性加载全部 API 文档。
59
+
60
+ | 模块 | 用途 | 详见 |
61
+ |---|---|---|
62
+ | `request` | HTTP 客户端(防腐层 + 4 个内置插件 + dwyeapi 响应契约 + 业务错误捕获) | `references/request.md` |
63
+ | `storage` | localStorage 封装(@vueuse/core 的 useStorage + 静态 storage 对象) | `references/data.md` § storage |
64
+ | `cookie` | Cookie 读写(js-cookie 薄封装 + useCookie) | `references/data.md` § cookie |
65
+ | `date` | 日期 / 时间 / 时区格式化(基于 dayjs,不暴露实例) | `references/data.md` § date |
66
+ | `qs` | 查询字符串序列化 / 解析(基于 qs) | `references/data.md` § qs |
67
+ | `validators` | 表单校验(布尔函数 + zod schema 配合 vee-validate) | `references/form.md` § validators |
68
+ | `masking` | PII 数据脱敏(手机/邮箱/身份证/银行卡/姓名/地址等) | `references/form.md` § masking |
69
+ | `copy` | 剪贴板(copyText + @vueuse/core 的 useClipboard) | `references/ui.md` § copy |
70
+ | `file` | 文件下载 / Blob / 文件大小格式化 | `references/ui.md` § file |
71
+ | `hooks` | Vue Composables(全部再导出 @vueuse/core) | `references/ui.md` § hooks |
72
+
73
+ ## v0.6.0 迁移要点
74
+
75
+ | 旧 | 新 |
76
+ |----|----|
77
+ | `createRequest(): AxiosInstance` | `createRequest(): HttpClient` |
78
+ | `RequestPlugin` | `HttpPlugin` |
79
+ | `import { dayjs } from '@dwydev/ekit'` | 用 `now / formatTimestamp / formatInTimezone / formatDateTime` 等函数 |
80
+ | `CookieAttributes` | `CookieOptions` |
81
+ | `IStringifyOptions / IParseOptions` | `StringifyOptions / ParseOptions` |
82
+ | `DownloadOptions.requestInstance: AxiosInstance` | `DownloadOptions.requester: FileRequester` |
83
+ | 自写 `useDebounce` 返回可写 `Ref` | VueUse `refDebounced` 返回 `Readonly<Ref>` |
84
+ | 自写 `useClipboard` 返回 `{ text, copy, copied, isSupported }`(isSupported 是 boolean) | VueUse 版(isSupported 是 `ComputedRef<boolean>`) |
85
+ | `refreshTokenPlugin` 自动重放 | 必须传 `retry: (cfg) => client.request(cfg)` |
@@ -0,0 +1,135 @@
1
+ # 数据与查询字符串(storage / cookie / date / qs)
2
+
3
+ > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(localStorage 读写、Cookie 读写、日期/时间/时区格式化、查询字符串序列化/解析)。
4
+
5
+ ## storage — localStorage
6
+
7
+ ```ts
8
+ import { useStorage, storage } from '@dwydev/ekit'
9
+ ```
10
+
11
+ ### useStorage(key, defaultValue) — 直接再导出 @vueuse/core
12
+
13
+ 跨标签页同步、SSR-safe、自动 JSON 序列化。**API 与 VueUse 一致**(不再是 ekit 自写)。
14
+
15
+ ```ts
16
+ const token = useStorage<string>('token', '')
17
+ const config = useStorage('config', { theme: 'light' }, localStorage, { mergeDefaults: true })
18
+ ```
19
+
20
+ ### storage — 静态对象(非组件场景)
21
+
22
+ | 方法 | 签名 |
23
+ |------|------|
24
+ | `get<T>(key, defaultValue?)` | 返回 `T \| undefined`,JSON 解析失败按字符串返回 |
25
+ | `set(key, value)` | 自动 `JSON.stringify` |
26
+ | `remove(key)` | 删除 |
27
+ | `clear()` | 清空全部(慎用) |
28
+
29
+ ---
30
+
31
+ ## cookie — Cookie 读写
32
+
33
+ ```ts
34
+ import { useCookie, cookie } from '@dwydev/ekit'
35
+ import type { CookieOptions } from '@dwydev/ekit' // 旧名 CookieAttributes 已废弃
36
+ ```
37
+
38
+ ### CookieOptions
39
+
40
+ ```ts
41
+ interface CookieOptions {
42
+ expires?: number | Date // 天数或 Date;不传 = 会话级
43
+ path?: string // 默认 '/'
44
+ domain?: string
45
+ secure?: boolean // 仅 HTTPS
46
+ sameSite?: 'strict' | 'lax' | 'none'
47
+ }
48
+ ```
49
+
50
+ ### cookie(静态)
51
+
52
+ ```ts
53
+ cookie.get<User>('user')
54
+ cookie.set('user', { id: 1, name: 'Alice' }, { expires: 7, secure: true })
55
+ cookie.remove('user', { path: '/' }) // 删除时 path/domain 必须与写入一致
56
+ ```
57
+
58
+ ### useCookie(key, defaultValue?) → Ref
59
+
60
+ ```ts
61
+ const lang = useCookie('lang', 'zh-CN')
62
+ lang.value = 'en-US' // 自动写入 cookie
63
+ lang.value = undefined // 自动删除
64
+ ```
65
+
66
+ ---
67
+
68
+ ## date — 日期 / 时间 / 时区
69
+
70
+ ```ts
71
+ import {
72
+ now, formatTimestamp, formatInTimezone,
73
+ formatDate, formatDateTime, formatTime, formatRelativeTime, formatBy,
74
+ } from '@dwydev/ekit'
75
+ ```
76
+
77
+ 底层是 dayjs(含 utc + timezone + relativeTime + zh-cn locale 插件),**不导出 dayjs 实例**。所有函数对外用 `string | number | Date`。
78
+
79
+ ### 时间戳工具(v0.6.0 新增)
80
+
81
+ | 函数 | 签名 | 说明 |
82
+ |------|------|------|
83
+ | `now()` | `(): number` | 当前 UTC Unix 毫秒戳,等价于 `Date.now()` |
84
+ | `formatTimestamp(ts, fmt?)` | `(ts: number, fmt?: string): string` | 把毫秒戳格式化为本地时区字符串。默认 `'YYYY-MM-DD HH:mm:ss'` |
85
+ | `formatInTimezone(tz, ts?, fmt?)` | `(tz: string, ts?: number, fmt?: string): string` | 在指定 IANA 时区下格式化(如 `'Asia/Shanghai'` / `'America/New_York'` / `'UTC'`),ts 不传用当前时间 |
86
+
87
+ ```ts
88
+ const ts = now() // 1745318445000
89
+ formatTimestamp(ts) // '2026-04-22 18:40:45'
90
+ formatTimestamp(ts, 'YYYY/MM/DD') // '2026/04/22'
91
+ formatInTimezone('America/New_York', ts) // '2026-04-22 06:40:45'
92
+ formatInTimezone('UTC', ts, 'HH:mm') // '10:40'
93
+ ```
94
+
95
+ ### 普通格式化
96
+
97
+ | 函数 | 输出 | 输入 |
98
+ |------|------|------|
99
+ | `formatDate(input)` | `YYYY-MM-DD` | `string \| number \| Date` |
100
+ | `formatDateTime(input)` | `YYYY-MM-DD HH:mm:ss` | 同上 |
101
+ | `formatTime(input)` | `HH:mm` | 同上 |
102
+ | `formatRelativeTime(input)` | `刚刚` / `N 分钟前` / `N 小时前` / `N 天前`;> 30 天退化为 `YYYY-MM-DD` | 同上 |
103
+ | `formatBy(input, template)` | 自定义 dayjs 模板 | `formatBy(ts, 'YYYY年MM月DD日')` → `2026年04月22日` |
104
+
105
+ ---
106
+
107
+ ## qs — 查询字符串
108
+
109
+ ```ts
110
+ import { stringify, parse } from '@dwydev/ekit'
111
+ import type { StringifyOptions, ParseOptions } from '@dwydev/ekit' // 旧名 IStringifyOptions / IParseOptions 已废弃
112
+ ```
113
+
114
+ ```ts
115
+ stringify({ ids: [1, 2], name: 'a' }) // 'ids=1&ids=2&name=a' 默认 arrayFormat: 'repeat'
116
+ stringify({ ids: [1, 2] }, { arrayFormat: 'brackets' }) // 'ids[]=1&ids[]=2'
117
+ stringify({ a: 1 }, { addQueryPrefix: true }) // '?a=1'
118
+
119
+ parse('?a=1&b=2') // { a: '1', b: '2' } 默认 ignoreQueryPrefix: true
120
+ parse('a.b=1', { allowDots: true }) // { a: { b: '1' } }
121
+ ```
122
+
123
+ ### StringifyOptions / ParseOptions
124
+
125
+ ```ts
126
+ interface StringifyOptions {
127
+ arrayFormat?: 'repeat' | 'brackets' | 'indices' | 'comma'
128
+ addQueryPrefix?: boolean
129
+ skipNulls?: boolean
130
+ }
131
+ interface ParseOptions {
132
+ ignoreQueryPrefix?: boolean
133
+ allowDots?: boolean
134
+ }
135
+ ```
@@ -0,0 +1,68 @@
1
+ # 表单与脱敏(validators / masking)
2
+
3
+ > 何时读这份:当用户使用本模块的 API 或问法涉及本模块功能时读取(表单校验、zod schema、vee-validate 集成、PII 数据脱敏)。
4
+
5
+ ## validators — 表单校验
6
+
7
+ ```ts
8
+ import {
9
+ isPhone, isEmail, isIdCard, isUrl, isRequired, minLength, maxLength,
10
+ // 也可用 zod schema 配合 vee-validate
11
+ phoneSchema, emailSchema, idCardSchema, urlSchema,
12
+ requiredSchema, minLengthSchema, maxLengthSchema,
13
+ } from '@dwydev/ekit'
14
+ ```
15
+
16
+ ### 布尔校验函数
17
+
18
+ | 函数 | 签名 | 规则 |
19
+ |------|------|------|
20
+ | `isPhone(value)` | `string => boolean` | 中国手机号 1[3-9] 开头 11 位 |
21
+ | `isEmail(value)` | `string => boolean` | 基础邮箱 |
22
+ | `isIdCard(value)` | `string => boolean` | 18 位身份证(末位允许 X/x) |
23
+ | `isUrl(value)` | `string => boolean` | 合法 URL |
24
+ | `isRequired(value)` | `any => boolean` | 非空(0 / false 视为有效) |
25
+ | `minLength(value, n)` | `(string, number) => boolean` | 最小长度 |
26
+ | `maxLength(value, n)` | `(string, number) => boolean` | 最大长度 |
27
+
28
+ ### Zod schema(配合 vee-validate)
29
+
30
+ ```ts
31
+ import { useForm } from 'vee-validate'
32
+ import { toTypedSchema } from '@vee-validate/zod'
33
+ import { z } from 'zod'
34
+ import { phoneSchema, emailSchema } from '@dwydev/ekit'
35
+
36
+ const schema = toTypedSchema(z.object({
37
+ phone: phoneSchema,
38
+ email: emailSchema,
39
+ }))
40
+ const { defineField, errors } = useForm({ validationSchema: schema })
41
+ ```
42
+
43
+ > `minLengthSchema(n)` / `maxLengthSchema(n)` 是工厂函数,调用后返回 schema。
44
+
45
+ ---
46
+
47
+ ## masking — PII 脱敏
48
+
49
+ ```ts
50
+ import {
51
+ maskPhone, maskEmail, maskIdCard, maskBankCard,
52
+ maskName, maskAddress, maskIp, maskLicensePlate, maskText,
53
+ } from '@dwydev/ekit'
54
+ ```
55
+
56
+ 纯函数,无外部依赖。**输入不符合格式时原样返回,不抛异常**。
57
+
58
+ | 函数 | 规则 | 示例 |
59
+ |------|------|------|
60
+ | `maskPhone(phone)` | 前 3 后 4 | `'138****5678'` |
61
+ | `maskEmail(email)` | 首字符 + `***` + @域名 | `'z***@gmail.com'` |
62
+ | `maskIdCard(id)` | 前 3 后 4(18 位) | `'420***********1234'` |
63
+ | `maskBankCard(card)` | 前 4 后 4(13~19 位) | `'6222********1234'` |
64
+ | `maskName(name)` | 2 字 → 姓+`*`;3+ 字 → 姓+`*`×(n-2)+末字 | `'张*'` / `'张*明'` |
65
+ | `maskAddress(addr)` | 保留省/市/区/县/镇/旗前缀 + `****` | `'浙江省杭州市西湖区****'` |
66
+ | `maskIp(ip)` | IPv4 末段替换 `*` | `'192.168.1.*'` |
67
+ | `maskLicensePlate(plate)` | 前 2 后 1,中间 `***` | `'浙A***8'` |
68
+ | `maskText(text, start=1, end=1, ch='*')` | 通用:保留首尾 N 位 | `maskText('1234567890', 2, 3)` → `'12*****890'` |