create-dwy 0.19.0 → 0.19.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.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/prompt-style.js +82 -0
- package/src/searchable-select.js +12 -29
- package/src/tab-multiselect.js +7 -12
- package/templates/ai-tools/rules/Python/dwy-python-base-lib.md +1 -1
- package/templates/ai-tools/skills/Python/dwy-eapi/SKILL.md +59 -200
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/anticorruption.md +13 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/layering.md +5 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/response-envelope.md +20 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/shared/timezone.md +7 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/breaking.md +12 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/email-canonical.md +18 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/index.md +103 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/v0/providers-email.md +39 -0
- package/templates/ai-tools/skills/Python/dwy-eapi/versions.yml +13 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/SKILL.md +62 -306
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-design-guide.md +8 -1
- package/templates/ai-tools/skills/Vue/dwy-eui/references/eui-integration-guide.md +0 -31
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/config-theme.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/form.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/overlay.md +6 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/shared/reka.md +5 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v1/index.md +8 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/breaking.md +14 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/index.md +55 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/v2/lookup.md +29 -0
- package/templates/ai-tools/skills/Vue/dwy-eui/versions.yml +12 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI 列表行聚焦样式。
|
|
3
|
+
* 只靠 dim 在 JetBrains / 部分终端看不出光标;已勾选时勾选框又全是绿块。
|
|
4
|
+
* 指针字符不依赖颜色,反底在支持 ANSI 的终端再加强。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { styleText } from 'node:util'
|
|
8
|
+
import process from 'node:process'
|
|
9
|
+
|
|
10
|
+
/** 是否支持 unicode 符号(与 clack prompts 判定一致) */
|
|
11
|
+
function isUnicodeSupported() {
|
|
12
|
+
if (process.platform !== 'win32') {
|
|
13
|
+
return process.env.TERM !== 'linux'
|
|
14
|
+
}
|
|
15
|
+
return Boolean(process.env.CI)
|
|
16
|
+
|| Boolean(process.env.WT_SESSION)
|
|
17
|
+
|| Boolean(process.env.TERMINUS_SUBLIME)
|
|
18
|
+
|| process.env.ConEmuTask === '{cmd::Cmder}'
|
|
19
|
+
|| process.env.TERM_PROGRAM === 'Terminus-Sublime'
|
|
20
|
+
|| process.env.TERM_PROGRAM === 'vscode'
|
|
21
|
+
|| process.env.TERM === 'xterm-256color'
|
|
22
|
+
|| process.env.TERM === 'alacritty'
|
|
23
|
+
|| process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const unicode = isUnicodeSupported()
|
|
27
|
+
const unicodeOr = (c, fallback) => (unicode ? c : fallback)
|
|
28
|
+
|
|
29
|
+
/** 光标指针。无 unicode 时用 ASCII,保证去 ANSI 后仍能看出聚焦行。 */
|
|
30
|
+
const S_POINTER = unicodeOr('›', '>')
|
|
31
|
+
const S_RADIO_ACTIVE = unicodeOr('●', '>')
|
|
32
|
+
const S_RADIO_INACTIVE = unicodeOr('○', ' ')
|
|
33
|
+
const S_CHECKBOX_ACTIVE = unicodeOr('◻', '[•]')
|
|
34
|
+
const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]')
|
|
35
|
+
const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]')
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 聚焦文案:反底芯片。无色终端退回原文字,靠指针区分。
|
|
39
|
+
*
|
|
40
|
+
* @param {string} label
|
|
41
|
+
* @param {boolean} active
|
|
42
|
+
*/
|
|
43
|
+
function formatLabel(label, active) {
|
|
44
|
+
if (active) return styleText(['bgCyan', 'black'], ` ${label} `)
|
|
45
|
+
return styleText('dim', label)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 多选一行。
|
|
50
|
+
* selected=已勾;active=当前光标。disabled 时不响应聚焦样式。
|
|
51
|
+
*
|
|
52
|
+
* @param {{ label: string, selected?: boolean, active?: boolean, prefix?: string, disabled?: boolean }} opts
|
|
53
|
+
*/
|
|
54
|
+
export function formatCheckboxRow({ label, selected = false, active = false, prefix = '', disabled = false }) {
|
|
55
|
+
const prefixPart = prefix ? `${prefix} ` : ''
|
|
56
|
+
if (disabled) {
|
|
57
|
+
return ` ${styleText('dim', S_CHECKBOX_INACTIVE)} ${prefixPart}${styleText(['strikethrough', 'dim'], label)}`
|
|
58
|
+
}
|
|
59
|
+
const pointer = active ? styleText('cyan', S_POINTER) : ' '
|
|
60
|
+
const box = selected
|
|
61
|
+
? styleText('green', S_CHECKBOX_SELECTED)
|
|
62
|
+
: active
|
|
63
|
+
? styleText('cyan', S_CHECKBOX_ACTIVE)
|
|
64
|
+
: styleText('dim', S_CHECKBOX_INACTIVE)
|
|
65
|
+
return `${pointer} ${box} ${prefixPart}${formatLabel(label, active)}`
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 单选一行。disabled 画删除线,不画指针。
|
|
70
|
+
*
|
|
71
|
+
* @param {{ label: string, active?: boolean, disabled?: boolean }} opts
|
|
72
|
+
*/
|
|
73
|
+
export function formatRadioRow({ label, active = false, disabled = false }) {
|
|
74
|
+
if (disabled) {
|
|
75
|
+
return ` ${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`
|
|
76
|
+
}
|
|
77
|
+
const pointer = active ? styleText('cyan', S_POINTER) : ' '
|
|
78
|
+
const radio = active
|
|
79
|
+
? styleText('green', S_RADIO_ACTIVE)
|
|
80
|
+
: styleText('dim', S_RADIO_INACTIVE)
|
|
81
|
+
return `${pointer} ${radio} ${formatLabel(label, active)}`
|
|
82
|
+
}
|
package/src/searchable-select.js
CHANGED
|
@@ -8,6 +8,7 @@ import { AutocompletePrompt, getColumns, settings } from '@clack/core'
|
|
|
8
8
|
import { limitOptions } from '@clack/prompts'
|
|
9
9
|
import { styleText } from 'node:util'
|
|
10
10
|
import process from 'node:process'
|
|
11
|
+
import { formatCheckboxRow, formatRadioRow } from './prompt-style.js'
|
|
11
12
|
|
|
12
13
|
/** 是否支持 unicode 符号(与 clack prompts 判定一致) */
|
|
13
14
|
function isUnicodeSupported() {
|
|
@@ -34,10 +35,6 @@ const S_STEP_ERROR = unicodeOr('▲', 'x')
|
|
|
34
35
|
const S_STEP_SUBMIT = unicodeOr('◇', 'o')
|
|
35
36
|
const S_BAR = unicodeOr('│', '|')
|
|
36
37
|
const S_BAR_END = unicodeOr('└', '—')
|
|
37
|
-
const S_RADIO_ACTIVE = unicodeOr('●', '>')
|
|
38
|
-
const S_RADIO_INACTIVE = unicodeOr('○', ' ')
|
|
39
|
-
const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]')
|
|
40
|
-
const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]')
|
|
41
38
|
|
|
42
39
|
/** 底部说明区固定行数,避免列表高度随描述长短跳动 */
|
|
43
40
|
const DESC_PANEL_LINES = 3
|
|
@@ -256,21 +253,12 @@ export function searchableMultiselect(opts) {
|
|
|
256
253
|
maxItems: opts.maxItems,
|
|
257
254
|
output: opts.output,
|
|
258
255
|
rowPadding: header.length + footer.length,
|
|
259
|
-
style: (opt, active) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (opt.disabled) {
|
|
266
|
-
return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'dim'], label)}`
|
|
267
|
-
}
|
|
268
|
-
// 聚焦也不拼 hint,描述只在底部公共区
|
|
269
|
-
if (active) {
|
|
270
|
-
return `${box} ${label}`
|
|
271
|
-
}
|
|
272
|
-
return `${box} ${styleText('dim', label)}`
|
|
273
|
-
},
|
|
256
|
+
style: (opt, active) => formatCheckboxRow({
|
|
257
|
+
label: optionLabel(opt),
|
|
258
|
+
selected: this.selectedValues.includes(opt.value),
|
|
259
|
+
active,
|
|
260
|
+
disabled: opt.disabled,
|
|
261
|
+
}),
|
|
274
262
|
})
|
|
275
263
|
|
|
276
264
|
return [
|
|
@@ -396,16 +384,11 @@ export function searchableSelect(opts) {
|
|
|
396
384
|
output: opts.output,
|
|
397
385
|
columnPadding: withGuide ? 3 : 0,
|
|
398
386
|
rowPadding: header.length + footer.length,
|
|
399
|
-
style: (opt, active) => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (active) {
|
|
405
|
-
return `${styleText('green', S_RADIO_ACTIVE)} ${label}`
|
|
406
|
-
}
|
|
407
|
-
return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`
|
|
408
|
-
},
|
|
387
|
+
style: (opt, active) => formatRadioRow({
|
|
388
|
+
label: optionLabel(opt),
|
|
389
|
+
active,
|
|
390
|
+
disabled: opt.disabled,
|
|
391
|
+
}),
|
|
409
392
|
})
|
|
410
393
|
|
|
411
394
|
return [
|
package/src/tab-multiselect.js
CHANGED
|
@@ -8,6 +8,7 @@ import { Prompt, getColumns, settings } from '@clack/core'
|
|
|
8
8
|
import { limitOptions } from '@clack/prompts'
|
|
9
9
|
import { styleText } from 'node:util'
|
|
10
10
|
import process from 'node:process'
|
|
11
|
+
import { formatCheckboxRow } from './prompt-style.js'
|
|
11
12
|
|
|
12
13
|
/** 是否支持 unicode 符号(与 clack / searchable-select 判定一致) */
|
|
13
14
|
function isUnicodeSupported() {
|
|
@@ -35,8 +36,6 @@ const S_STEP_SUBMIT = unicodeOr('◇', 'o')
|
|
|
35
36
|
const S_BAR = unicodeOr('│', '|')
|
|
36
37
|
const S_BAR_END = unicodeOr('└', '—')
|
|
37
38
|
const S_BAR_H = () => unicodeOr('─', '-')
|
|
38
|
-
const S_CHECKBOX_SELECTED = unicodeOr('◼', '[+]')
|
|
39
|
-
const S_CHECKBOX_INACTIVE = unicodeOr('◻', '[ ]')
|
|
40
39
|
const S_TAB_MORE = unicodeOr('‹', '<')
|
|
41
40
|
const S_TAB_MORE_RIGHT = unicodeOr('›', '>')
|
|
42
41
|
|
|
@@ -418,16 +417,12 @@ export function tabMultiselect(opts) {
|
|
|
418
417
|
maxItems: opts.maxItems ?? 12,
|
|
419
418
|
output: opts.output,
|
|
420
419
|
rowPadding: header.length + footer.length,
|
|
421
|
-
style: (opt, active) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const prefix = formatItemTypePrefix(opt.type)
|
|
428
|
-
if (active) return `${box} ${prefix} ${label}`
|
|
429
|
-
return `${box} ${prefix} ${styleText('dim', label)}`
|
|
430
|
-
},
|
|
420
|
+
style: (opt, active) => formatCheckboxRow({
|
|
421
|
+
label: opt.label ?? String(opt.value ?? ''),
|
|
422
|
+
selected: selected.includes(opt.value),
|
|
423
|
+
active,
|
|
424
|
+
prefix: formatItemTypePrefix(opt.type),
|
|
425
|
+
}),
|
|
431
426
|
})
|
|
432
427
|
|
|
433
428
|
return [
|
|
@@ -33,7 +33,7 @@ paths:
|
|
|
33
33
|
|
|
34
34
|
### 查阅方式(关键)
|
|
35
35
|
|
|
36
|
-
**在编写任何后端代码前,以及调用任何 dwyeapi 提供的 API 之前,必须先用 `Skill` 工具加载 `dwy-eapi
|
|
36
|
+
**在编写任何后端代码前,以及调用任何 dwyeapi 提供的 API 之前,必须先用 `Skill` 工具加载 `dwy-eapi`。** skill 会按消费方**实际安装版本**选 `v0`/`v1` 文档线;不确定时先 clone `dwyeapi@x.y.z`,拿不到再读虚拟环境里的安装包。本文件**只规定约束与边界**,**不固化**模块路径、类名、函数签名。
|
|
37
37
|
|
|
38
38
|
```
|
|
39
39
|
Skill 工具调用:skill="dwy-eapi"
|
|
@@ -1,226 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dwy-eapi
|
|
3
|
-
description: "
|
|
4
|
-
eapi_baseline_version: "0.9.0"
|
|
3
|
+
description: "Use when building or changing FastAPI backends that use dwyeapi: Pydantic Settings, async SQLAlchemy, Redis, JWT/bcrypt, AppError/NotFoundError/BusinessError, ApiResponse/PageData, pagination, loguru, health checks, PII masking, timezone-aware dt, ARQ tasks, email verification codes, Gmail alias canonicalization. The single navigation skill for dwyeapi."
|
|
5
4
|
---
|
|
6
5
|
|
|
7
|
-
# dwyeapi
|
|
6
|
+
# dwyeapi
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
FastAPI 基础设施包。本文件只做**路由**:探测安装版本 → 选文档线 → 不够再读源码(先 clone tag,拿不到再读虚拟环境)。
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
对外只有这一个 skill。`v0/` `v1/` 不是独立 skill,禁止当多个入口用。
|
|
12
11
|
|
|
13
|
-
##
|
|
12
|
+
## 每次写 dwyeapi 代码前(强制)
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
| `security` | JWT 令牌 + bcrypt 密码哈希(无状态) |
|
|
22
|
-
| `cache` | 异步 Redis 连接管理(懒单例) |
|
|
23
|
-
| `response` | 统一 API 响应信封 `ApiResponse[T]` + `PageData[T]` |
|
|
24
|
-
| `pagination` | 分页参数 PaginationParams + offset/limit 计算 |
|
|
25
|
-
| `dt` | 中国时区敏感的全局时间工具(替代 datetime.now) |
|
|
26
|
-
| `logger` | loguru 全局日志(按天 + 按大小轮转、stdlib 拦截) |
|
|
27
|
-
| `masking` | PII 数据脱敏(手机/邮箱/身份证/银行卡/姓名/地址/IP/车牌) |
|
|
28
|
-
| `health` | 健康检查路由工厂(只探活、不探依赖) |
|
|
29
|
-
| `tasks` | 异步任务系统(基于 ARQ,需 `pip install dwyeapi[tasks]`) |
|
|
30
|
-
| `providers.email` | 邮件验证码发送(可插拔,默认 Resend,可继承注入腾讯 SES / SMTP 等) |
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 查 API 标准动作(核心:每次写 dwyeapi 代码前都做)
|
|
35
|
-
|
|
36
|
-
dwyeapi 的源码 docstring 写得非常详尽(含设计意图、边界行为、Args/Returns/Raises、典型陷阱)。**本文档不再镜像签名表格** —— 因为表格信息密度低于源码 docstring,且会随版本漂移。需要某个 API 的精确签名/语义时,按下面顺序拿:
|
|
37
|
-
|
|
38
|
-
### 第一步:定位 dwyeapi 真实版本和安装路径
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
# 拿版本
|
|
42
|
-
pip show dwyeapi | grep -i version
|
|
43
|
-
# 或:python -c "import dwyeapi; print(dwyeapi.__version__)"
|
|
44
|
-
|
|
45
|
-
# 拿包安装目录
|
|
46
|
-
python -c "import dwyeapi; print(dwyeapi.__file__)"
|
|
47
|
-
# 输出形如 /path/to/.venv/lib/python3.12/site-packages/dwyeapi/__init__.py
|
|
48
|
-
# dirname 后的目录就是 dwyeapi 包根,模块都在里面
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### 第二步:读模块源码(按存在性回退)
|
|
52
|
-
|
|
53
|
-
```
|
|
54
|
-
a. 优先:<site-packages>/dwyeapi/{module}.py
|
|
55
|
-
—— 下游消费方默认场景。每个模块单文件,含模块级 docstring + 所有公开函数/类的 Args/Returns/Raises
|
|
56
|
-
|
|
57
|
-
b. 次选:<dwy-shared-root>/backend/src/dwyeapi/{module}.py
|
|
58
|
-
—— 仅当在 dwy-shared monorepo 内或并列 clone 时可用,内容相同
|
|
59
|
-
|
|
60
|
-
c. 都拿不到 → 退到本文档下方的 [模块清单](#模块清单13-个)(粗略,仅做导航)
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 第三步:tasks 模块查详细文档
|
|
64
|
-
|
|
65
|
-
tasks 模块功能丰富(注册 / 提交 / 查询 / 取消 / 进度),单独维护一份完整集成指南:
|
|
66
|
-
|
|
67
|
-
```
|
|
68
|
-
references/tasks-integration-guide.md
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
只在用到 tasks 时读,平时不必加载。
|
|
72
|
-
|
|
73
|
-
### 第四步:冲突时永远信源码
|
|
74
|
-
|
|
75
|
-
本 SKILL.md 是导航 + 心智模型,**不是 API 真相**。当文档描述与 site-packages 中的 `.py` 不一致:无条件以源码为准。
|
|
76
|
-
|
|
77
|
-
---
|
|
14
|
+
1. **探测安装版本**(按顺序,命中即停)
|
|
15
|
+
```bash
|
|
16
|
+
python -c "import dwyeapi; print(dwyeapi.__version__)"
|
|
17
|
+
```
|
|
18
|
+
失败则读项目 `pyproject.toml` / `uv.lock` 里 `dwyeapi` 约束。
|
|
19
|
+
仍失败:按 `v0` 给脚手架,并声明「未探测到安装版本」。
|
|
78
20
|
|
|
79
|
-
|
|
21
|
+
2. **选线**:读同目录 `versions.yml`。`0.y.z` → `v0/`,`1.y.z` → `v1/`,`2.y.z` → `v2/`。目录不存在(例如装了 1.x 但还没有 `v1/`)→ 不要用 `v0` 冒充;按下面「读源码」拿该版本代码,并说明文档线未建。
|
|
80
22
|
|
|
81
|
-
|
|
82
|
-
- 消费方装的是不同版本(更新或更旧)时,**必须**按"查 API 标准动作"流程读 .py,不要默认本文档准确
|
|
83
|
-
- dwyeapi < 0.9.0 可能没有 dt / tasks / providers 模块:先 `ls <site-packages>/dwyeapi/` 确认存在再用
|
|
84
|
-
- 主版本号变更(0.x → 1.x)时,本文档的索引可能完全失效:先 `ls <site-packages>/dwyeapi/` 重建认知
|
|
23
|
+
3. **读文档**:先 `shared/`(跨大版本不变),再该线 `vN/`。能力标了版本下限(如 `canonicalize_email` ≥0.9.6)时,安装版本低于下限 → 当不存在。
|
|
85
24
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
源码读得出来但容易忽略的"踩坑经验",写后端代码时遵守:
|
|
91
|
-
|
|
92
|
-
### 业务层架构(service / router 分层)
|
|
93
|
-
|
|
94
|
-
- **service 层**抛业务异常(`NotFoundError("用户")` / `BusinessError("余额不足", code="INSUFFICIENT_BALANCE")` 等),不感知 HTTP 概念
|
|
95
|
-
- **router 层**不写 try/except,由 `register_exception_handlers(app)` 统一转成 `ApiResponse` 信封 JSON
|
|
96
|
-
- **禁止**在 service 层抛 `HTTPException` —— 会绕过统一 handler,破坏响应结构一致性
|
|
97
|
-
|
|
98
|
-
### 时间处理(避免时区 bug)
|
|
99
|
-
|
|
100
|
-
- 业务时间统一中国时区,**禁止 `datetime.now()`**,统一走 `dwyeapi.dt`
|
|
101
|
-
- `dt.now()` 返回 naive datetime(Asia/Shanghai,用于数据库存储)
|
|
102
|
-
- `dt.utc_now()` 返回 aware UTC datetime(用于 JWT exp 等协议字段)
|
|
103
|
-
- 这两个不能混用:数据库列存 naive 中国时间、JWT exp 存 UTC aware
|
|
104
|
-
|
|
105
|
-
### 配置
|
|
106
|
-
|
|
107
|
-
- **没有 `debug` 字段**,所有调试开关统一用 `is_dev()`:避免两个维度(debug + environment)冲突
|
|
108
|
-
- `environment` 默认 `"prod"`(误配置时保守):生产环境必须显式关闭 docs/redoc/openapi 三个端点(`docs_url="/docs" if is_dev() else None`),否则路由元信息会泄露给攻击者
|
|
109
|
-
- `BaseSettings` 必填三个字段:`database_url` / `redis_url` / `secret_key`(继承时不能给默认值)
|
|
110
|
-
|
|
111
|
-
### Redis 连接
|
|
112
|
-
|
|
113
|
-
- `cache.configure(redis_url)` 启动时调用一次(lifespan);`cache.get_redis()` 首次调用懒建立连接
|
|
114
|
-
- `close_redis()` 可重复调用(安全幂等),lifespan 关闭阶段调用
|
|
115
|
-
- 未 configure 直接 `get_redis()` 会抛 `RuntimeError`
|
|
116
|
-
|
|
117
|
-
### tasks 模块
|
|
118
|
-
|
|
119
|
-
- 需要 `pip install dwyeapi[tasks]` 单独装 extra(避免不用任务的项目背 ARQ 依赖)
|
|
120
|
-
- 任务函数签名固定 `async def task(ctx: TaskContext, params: dict)`,第一个参数 ctx 由框架注入
|
|
121
|
-
- 取消机制是协作式:任务函数内必须主动 `await ctx.is_cancelled()` 检查,否则取消信号无效
|
|
122
|
-
- 完整细节读 `references/tasks-integration-guide.md`
|
|
25
|
+
4. **文档不够 / 和记忆冲突 / 下限对不上** → 读代码,顺序如下(命中即停):
|
|
26
|
+
1. clone 安装版本对应 tag(见下)
|
|
27
|
+
2. tag 不存在 / clone 失败 → 读**当前虚拟环境**里已安装的 `dwyeapi` 源码
|
|
28
|
+
3. 虚拟环境也没有 → **停下来问人**,禁止猜 API,禁止拿旁边 dwy-shared master 冒充
|
|
123
29
|
|
|
124
|
-
|
|
30
|
+
## 读源码
|
|
125
31
|
|
|
126
|
-
|
|
127
|
-
- 验证码生成 + Redis 存储 + 品牌化模板 由基类全部复用,子类只管"发出去"
|
|
32
|
+
### 1. clone tag(优先)
|
|
128
33
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
## 典型项目接入流程
|
|
144
|
-
|
|
145
|
-
```python
|
|
146
|
-
# config.py
|
|
147
|
-
from dwyeapi.config import BaseSettings
|
|
148
|
-
|
|
149
|
-
class Settings(BaseSettings):
|
|
150
|
-
app_name: str = "My API"
|
|
151
|
-
service_name: str = "my-api"
|
|
152
|
-
|
|
153
|
-
settings = Settings() # 从 .env / 环境变量读 database_url、redis_url、secret_key
|
|
154
|
-
|
|
155
|
-
# database.py
|
|
156
|
-
from dwyeapi.database import create_async_engine_factory, create_session_factory
|
|
157
|
-
from dwyeapi.dependencies import create_get_db
|
|
158
|
-
from dwyeapi import is_dev
|
|
159
|
-
|
|
160
|
-
engine = create_async_engine_factory(settings.database_url, echo=is_dev())
|
|
161
|
-
session_factory = create_session_factory(engine)
|
|
162
|
-
get_db = create_get_db(session_factory)
|
|
163
|
-
|
|
164
|
-
# main.py
|
|
165
|
-
from contextlib import asynccontextmanager
|
|
166
|
-
from fastapi import FastAPI
|
|
167
|
-
from dwyeapi import cache, logger, is_dev
|
|
168
|
-
from dwyeapi.exceptions import register_exception_handlers
|
|
169
|
-
from dwyeapi.health import create_health_router
|
|
170
|
-
|
|
171
|
-
@asynccontextmanager
|
|
172
|
-
async def lifespan(app: FastAPI):
|
|
173
|
-
logger.configure(level="INFO", log_dir="./logs", filename="app")
|
|
174
|
-
cache.configure(settings.redis_url)
|
|
175
|
-
yield
|
|
176
|
-
await cache.close_redis()
|
|
177
|
-
logger.close()
|
|
178
|
-
|
|
179
|
-
app = FastAPI(
|
|
180
|
-
lifespan=lifespan,
|
|
181
|
-
docs_url="/docs" if is_dev() else None,
|
|
182
|
-
redoc_url="/redoc" if is_dev() else None,
|
|
183
|
-
openapi_url="/openapi.json" if is_dev() else None,
|
|
184
|
-
)
|
|
185
|
-
register_exception_handlers(app)
|
|
186
|
-
app.include_router(create_health_router(service_name=settings.service_name, version="1.0.0"))
|
|
34
|
+
```bash
|
|
35
|
+
VER=0.9.6 # 换成第 1 步探测到的版本
|
|
36
|
+
TAG="dwyeapi@${VER}"
|
|
37
|
+
DEST=".dwy/eapi-src/${VER}" # 项目内缓存,已 gitignore
|
|
38
|
+
|
|
39
|
+
if [ ! -d "$DEST/backend/src/dwyeapi" ]; then
|
|
40
|
+
git clone --depth 1 --branch "$TAG" \
|
|
41
|
+
--filter=blob:none --sparse \
|
|
42
|
+
https://github.com/danweiyuancircle/dwy-ai-experience.git \
|
|
43
|
+
"$DEST"
|
|
44
|
+
git -C "$DEST" sparse-checkout set backend/src/dwyeapi
|
|
45
|
+
fi
|
|
46
|
+
# 只读 $DEST/backend/src/dwyeapi/ 的模块 docstring
|
|
187
47
|
```
|
|
188
48
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
## 响应契约速查(前后端对齐用)
|
|
49
|
+
- `--branch` 必须是 `dwyeapi@x.y.z`,禁止 clone 默认 branch。
|
|
50
|
+
- 必须 sparse,只要 `backend/src/dwyeapi`。
|
|
192
51
|
|
|
193
|
-
|
|
52
|
+
### 2. 虚拟环境(clone 拿不到时)
|
|
194
53
|
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
"data": { ... },
|
|
200
|
-
"timestamp": 1775625000
|
|
201
|
-
}
|
|
54
|
+
```bash
|
|
55
|
+
python -c "import dwyeapi, os; print(os.path.dirname(dwyeapi.__file__))"
|
|
56
|
+
# 输出形如 .venv/lib/python3.11/site-packages/dwyeapi
|
|
57
|
+
# 读该目录下模块 docstring
|
|
202
58
|
```
|
|
203
59
|
|
|
204
|
-
|
|
205
|
-
- 失败:`code` 是业务错误码(如 `NOT_FOUND` / `BUSINESS_ERROR` / `VALIDATION_ERROR` 等),HTTP 状态保持语义(404 就是 404)
|
|
206
|
-
- 校验错误 `VALIDATION_ERROR`:`data.errors: [{field, message}]`,field 含 `body.` / `query.` / `path.` 前缀
|
|
207
|
-
|
|
208
|
-
构造响应:用 `ApiResponse.ok(data)` 或 `ApiResponse.page(items, total, page, page_size)`,不直接实例化 `ApiResponse`。
|
|
60
|
+
editable 安装时这个路径就是消费方正在跑的代码,当作安装版本真相。
|
|
209
61
|
|
|
210
|
-
|
|
62
|
+
文档与源码冲突:信源码。clone 与 venv 都读到且不一致:以 **venv** 为准(editable / 热修),回复里写明。
|
|
211
63
|
|
|
212
|
-
|
|
64
|
+
## 文档地图
|
|
213
65
|
|
|
214
|
-
|
|
66
|
+
| 路径 | 何时读 |
|
|
67
|
+
|------|--------|
|
|
68
|
+
| `shared/layering.md` | 分层、禁 HTTPException |
|
|
69
|
+
| `shared/timezone.md` | 禁 `datetime.now` |
|
|
70
|
+
| `shared/response-envelope.md` | ApiResponse 信封 |
|
|
71
|
+
| `shared/anticorruption.md` | 禁止直调 redis/jwt/logging |
|
|
72
|
+
| `v0/index.md` | 0.x 模块清单、接入、陷阱 |
|
|
73
|
+
| `v0/providers-email.md` | 邮件 Provider 注册表 |
|
|
74
|
+
| `v0/email-canonical.md` | Gmail 规范化(≥0.9.6) |
|
|
75
|
+
| `v0/breaking.md` | 0.x 内部 breaking |
|
|
76
|
+
| `references/tasks-integration-guide.md` | 只用 tasks 时读 |
|
|
215
77
|
|
|
216
|
-
|
|
78
|
+
## 禁止
|
|
217
79
|
|
|
218
|
-
|
|
|
80
|
+
| 借口 | 实际 |
|
|
219
81
|
|------|------|
|
|
220
|
-
|
|
|
221
|
-
|
|
|
222
|
-
|
|
|
223
|
-
|
|
|
224
|
-
| 直接 `raise HTTPException(404, ...)` | `raise NotFoundError("资源名")` |
|
|
225
|
-
| 自己写 `{"code": ..., "data": ...}` dict 当响应 | `ApiResponse.ok(data)` / `ApiResponse.page(...)` |
|
|
226
|
-
| 自己写 `?page=&page_size=` Query 参数 | `params: PaginationParams = Depends()` |
|
|
82
|
+
| 读旁边 dwy-shared master | 消费方版本可能不同。先 clone 安装版本的 tag,不行再读本项目 venv |
|
|
83
|
+
| 文档是 0.9,装的 0.9.6,差不多 | 有下限的能力必须对照版本 |
|
|
84
|
+
| tag 没有就猜 / 直接停 | clone 失败后读 venv;venv 也没有才停 |
|
|
85
|
+
| 再开一个 `dwy-eapi-v0` skill | 对外只有 `dwy-eapi` |
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# 防腐层
|
|
2
|
+
|
|
3
|
+
业务代码禁止直调底层 API:
|
|
4
|
+
|
|
5
|
+
| 禁止 | 改用 |
|
|
6
|
+
|------|------|
|
|
7
|
+
| `datetime.now()` / `datetime.utcnow()` | `dt.now()` / `dt.utc_now()` |
|
|
8
|
+
| `import logging; logging.getLogger()` | `from dwyeapi import logger; logger.get_logger()` |
|
|
9
|
+
| 手写 `from redis.asyncio import Redis` 全局变量 | `cache.configure()` + `cache.get_redis()` |
|
|
10
|
+
| 手写 JWT encode/decode(含 `import jwt`) | `security.create_token()` / `security.decode_token()` |
|
|
11
|
+
| 直接 `raise HTTPException(404, ...)` | `raise NotFoundError("资源名")` |
|
|
12
|
+
| 自己写 `{"code": ..., "data": ...}` dict | `ApiResponse.ok(data)` / `ApiResponse.page(...)` |
|
|
13
|
+
| 自己写 `?page=&page_size=` Query | `params: PaginationParams = Depends()` |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 响应信封
|
|
2
|
+
|
|
3
|
+
所有接口返回:
|
|
4
|
+
|
|
5
|
+
```json
|
|
6
|
+
{
|
|
7
|
+
"code": "SUCCESS",
|
|
8
|
+
"message": "success",
|
|
9
|
+
"data": {},
|
|
10
|
+
"timestamp": 1775625000
|
|
11
|
+
}
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- 成功:`code = "SUCCESS"`,`data` 是业务载荷(单体或 `PageData[T]`)
|
|
15
|
+
- 失败:`code` 是业务错误码(`NOT_FOUND` / `BUSINESS_ERROR` / `VALIDATION_ERROR`),HTTP 状态保持语义
|
|
16
|
+
- 校验错误 `VALIDATION_ERROR`:`data.errors: [{field, message}]`,field 含 `body.` / `query.` / `path.` 前缀
|
|
17
|
+
|
|
18
|
+
构造:`ApiResponse.ok(data)` 或 `ApiResponse.page(items, total, page, page_size)`,不直接实例化成功态。
|
|
19
|
+
|
|
20
|
+
错误响应不要业务代码拼信封 —— `raise NotFoundError("用户")`,handler 自动转。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# 0.x 内部 breaking
|
|
2
|
+
|
|
3
|
+
0.x 的 minor 也可以不兼容。整条线仍用 `v0/`,破坏点记在这里。细节 clone `dwyeapi@x.y.z`,不要另开 `v0.10/` 目录。
|
|
4
|
+
|
|
5
|
+
完整段落以该 tag 的 `backend/CHANGELOG.md` 为准。
|
|
6
|
+
|
|
7
|
+
| 版本 | 破坏点 |
|
|
8
|
+
|------|--------|
|
|
9
|
+
| 0.6.0 | 信封改为 `ApiResponse[T]` / `PageData[T]`;删除 `success` / `fail` / `paginated`;`code` 成功态从 int `200` 改为 `"SUCCESS"` |
|
|
10
|
+
| 0.9.0 | 删除 `MockEmailProvider`、`AliyunEmailProvider`、整个 `providers.sms`;`EMAIL__PROVIDER` 改为自由字符串;mock/阿里云改业务 `register_email_provider` |
|
|
11
|
+
|
|
12
|
+
0.9.6 起新增 `dwyeapi.email` 与 Redis key 规范化,对旧调用方兼容(发码 key 折到同一收件箱)。能力下限见 `email-canonical.md`。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# 邮箱规范化(≥0.9.6)
|
|
2
|
+
|
|
3
|
+
安装版本 `< 0.9.6`:没有 `dwyeapi.email`,不要 import。
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from dwyeapi.email import canonicalize_email, is_folded_alias, GMAIL_DOMAINS
|
|
7
|
+
|
|
8
|
+
canonicalize_email("Bai.Wen+1@Gmail.com") # baiwen@gmail.com
|
|
9
|
+
canonicalize_email("a.b@googlemail.com") # ab@gmail.com
|
|
10
|
+
canonicalize_email("a+x@icloud.com") # a+x@icloud.com
|
|
11
|
+
is_folded_alias("a+x@gmail.com") # True
|
|
12
|
+
is_folded_alias("A@gmail.com") # False
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- 注册查重、登录查找、用户表写入 **必须** 走 `canonicalize_email`
|
|
16
|
+
- Gmail 把 plus / 点号 / `googlemail.com` 当成同一收件箱,不折会开多个号、绕过发码冷却
|
|
17
|
+
- 其它域名只做 strip + lower,保留 `+` 与点
|
|
18
|
+
- `send_code` / `verify_code` 的 Redis key 已自动规范化;业务存库仍要自己调
|