create-dwy 0.9.1 → 0.11.0

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 (75) hide show
  1. package/package.json +3 -2
  2. package/src/create.js +5 -2
  3. package/src/prompts/grouped-checkbox.js +223 -0
  4. package/src/sync.js +45 -19
  5. package/templates/claude-global/hooks/pre-git-commit-sensitive-check.sh +0 -0
  6. package/templates/claude-global/rules/dwy-android-core.md +1 -0
  7. package/templates/claude-global/rules/dwy-db-migration.md +1 -0
  8. package/templates/claude-global/rules/dwy-flutter-core.md +1 -0
  9. package/templates/claude-global/rules/dwy-flutter-network.md +1 -0
  10. package/templates/claude-global/rules/dwy-flutter-router.md +1 -0
  11. package/templates/claude-global/rules/dwy-flutter-testing.md +1 -0
  12. package/templates/claude-global/rules/dwy-postgres.md +1 -0
  13. package/templates/claude-global/rules/dwy-python-backend.md +44 -3
  14. package/templates/claude-global/rules/dwy-python-core.md +36 -2
  15. package/templates/claude-global/rules/dwy-python-testing.md +1 -0
  16. package/templates/claude-global/rules/dwy-redis.md +1 -0
  17. package/templates/claude-global/rules/dwy-tdd-development.md +1 -0
  18. package/templates/claude-global/rules/dwy-vue-core.md +3 -43
  19. package/templates/claude-global/rules/dwy-vue-pinia.md +1 -0
  20. package/templates/claude-global/rules/dwy-vue-testing.md +1 -0
  21. package/templates/claude-global/skills/dwy-cibuildwheel/SKILL.md +355 -0
  22. package/templates/claude-global/skills/dwy-cibuildwheel/references/release-workflow-template.yml +99 -0
  23. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +1 -0
  24. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +0 -0
  25. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +0 -0
  26. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +0 -0
  27. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +0 -0
  28. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +0 -0
  29. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +0 -0
  30. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +0 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +0 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +0 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +0 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +0 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +0 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +0 -0
  37. package/templates/claude-global/skills/dwy-dev-qa/SKILL.md +1 -0
  38. package/templates/claude-global/skills/dwy-docker/SKILL.md +1 -0
  39. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +0 -0
  40. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +0 -0
  41. package/templates/claude-global/skills/dwy-dolphindb/SKILL.md +123 -76
  42. package/templates/claude-global/skills/dwy-dolphindb/references/create-database-table.md +219 -0
  43. package/templates/claude-global/skills/dwy-dolphindb/references/crud-operations.md +281 -0
  44. package/templates/claude-global/skills/dwy-dolphindb/references/data-import.md +271 -0
  45. package/templates/claude-global/skills/dwy-dolphindb/references/docker-deployment.md +63 -0
  46. package/templates/claude-global/skills/dwy-dolphindb/references/performance-tuning.md +223 -0
  47. package/templates/claude-global/skills/dwy-dolphindb/references/pitfalls-and-best-practices.md +352 -0
  48. package/templates/claude-global/skills/dwy-dolphindb/references/python-api.md +306 -0
  49. package/templates/claude-global/skills/dwy-dolphindb/references/setup-and-connection.md +143 -0
  50. package/templates/claude-global/skills/dwy-dolphindb/references/sources.md +46 -0
  51. package/templates/claude-global/skills/dwy-dolphindb/references/sql-batch-compute.md +206 -0
  52. package/templates/claude-global/skills/dwy-eapi/SKILL.md +134 -402
  53. package/templates/claude-global/skills/dwy-ekit/SKILL.md +1 -0
  54. package/templates/claude-global/skills/dwy-eui/SKILL.md +1 -0
  55. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +14 -7
  56. package/templates/claude-global/skills/dwy-git-commit/SKILL.md +1 -0
  57. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +1 -0
  58. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +0 -0
  59. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +0 -0
  60. package/templates/claude-global/skills/dwy-pentest/SKILL.md +1 -0
  61. package/templates/claude-global/skills/dwy-publish/SKILL.md +1 -0
  62. package/templates/claude-global/skills/dwy-tech-stack/SKILL.md +1 -0
  63. package/templates/project/fullstack-monorepo/CLAUDE.md.hbs +21 -9
  64. package/templates/project/fullstack-monorepo/backend/Dockerfile.prod +40 -0
  65. package/templates/project/fullstack-monorepo/backend/_env.example.hbs +26 -0
  66. package/templates/project/fullstack-monorepo/docker-compose.dev.yml.hbs +4 -4
  67. package/templates/project/fullstack-monorepo/docker-compose.prod.yml.hbs +116 -0
  68. package/templates/project/fullstack-monorepo/docs/.gitkeep +0 -0
  69. package/templates/project/fullstack-monorepo/frontend/_env.example.hbs +19 -0
  70. package/templates/project/fullstack-monorepo/{dev.sh.hbs → scripts/dev.sh.hbs} +2 -2
  71. package/templates/claude-global/skills/dwy-dolphindb/evals/evals.json +0 -67
  72. package/templates/claude-global/skills/dwy-dolphindb/references/dolphindb-rules.md +0 -441
  73. package/templates/claude-global/skills/dwy-eapi/references/health.md +0 -47
  74. package/templates/claude-global/skills/dwy-eapi/references/logger.md +0 -60
  75. package/templates/project/fullstack-monorepo/_env.example.hbs +0 -24
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "项目脚手架与 Claude Code 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "license": "MIT",
21
21
  "author": "dwydev",
22
22
  "dependencies": {
23
+ "@inquirer/core": "^10.0.0",
23
24
  "commander": "^12.0.0",
24
25
  "inquirer": "^9.0.0",
25
26
  "handlebars": "^4.7.0",
@@ -31,4 +32,4 @@
31
32
  "registry": "https://registry.npmjs.org/",
32
33
  "access": "public"
33
34
  }
34
- }
35
+ }
package/src/create.js CHANGED
@@ -154,11 +154,14 @@ export async function createProject(name) {
154
154
  }
155
155
 
156
156
  if (context.template === 'fullstack-monorepo') {
157
- console.log(chalk.gray(' cp .env.example .env'))
157
+ console.log(chalk.gray(' cp backend/.env.example backend/.env'))
158
+ if (context.includeFrontend) {
159
+ console.log(chalk.gray(' cp frontend/.env.example frontend/.env'))
160
+ }
158
161
  console.log(chalk.gray(' cd backend && uv sync --dev && cd ..'))
159
162
  if (context.includeFrontend) {
160
163
  console.log(chalk.gray(' cd frontend && pnpm install && cd ..'))
161
164
  }
162
- console.log(chalk.gray(' ./dev.sh'))
165
+ console.log(chalk.gray(' ./scripts/dev.sh'))
163
166
  }
164
167
  }
@@ -0,0 +1,223 @@
1
+ import {
2
+ createPrompt,
3
+ useState,
4
+ useMemo,
5
+ useKeypress,
6
+ usePrefix,
7
+ usePagination,
8
+ isUpKey,
9
+ isDownKey,
10
+ isSpaceKey,
11
+ isBackspaceKey,
12
+ isEnterKey,
13
+ makeTheme,
14
+ } from '@inquirer/core'
15
+ import chalk from 'chalk'
16
+
17
+ const OTHER = '其他'
18
+
19
+ function sortCategories(cats) {
20
+ const others = cats.includes(OTHER) ? [OTHER] : []
21
+ const rest = cats.filter(c => c !== OTHER).sort((a, b) => a.localeCompare(b, 'zh'))
22
+ return [...rest, ...others]
23
+ }
24
+
25
+ function buildVisibleRows(items, filter, selected) {
26
+ const kw = filter.toLowerCase()
27
+ const filtered = kw
28
+ ? items.filter(it => it.name.toLowerCase().includes(kw)
29
+ || (it.description || '').toLowerCase().includes(kw))
30
+ : items
31
+ const groups = new Map()
32
+ for (const it of filtered) {
33
+ const cat = it.category || OTHER
34
+ if (!groups.has(cat)) groups.set(cat, [])
35
+ groups.get(cat).push(it)
36
+ }
37
+ const rows = []
38
+ for (const cat of sortCategories([...groups.keys()])) {
39
+ const groupItems = groups.get(cat).sort((a, b) => a.name.localeCompare(b.name))
40
+ const selectedInGroup = groupItems.filter(it => selected.has(it.name)).length
41
+ rows.push({ type: 'header', category: cat, total: groupItems.length, selectedInGroup, items: groupItems })
42
+ for (const it of groupItems) rows.push({ type: 'item', item: it, category: cat })
43
+ }
44
+ return rows
45
+ }
46
+
47
+ function isPrintable(key) {
48
+ return typeof key.sequence === 'string'
49
+ && key.sequence.length === 1
50
+ && key.sequence >= ' '
51
+ && key.sequence <= '~'
52
+ && !key.ctrl
53
+ && !key.meta
54
+ && key.name !== 'space'
55
+ && key.name !== 'return'
56
+ && key.name !== 'enter'
57
+ }
58
+
59
+ function isEscapeKey(key) {
60
+ return key.name === 'escape'
61
+ }
62
+
63
+ function isCtrlA(key) {
64
+ return key.ctrl && key.name === 'a'
65
+ }
66
+
67
+ function nextCursor(rows, cursor, delta) {
68
+ if (rows.length === 0) return 0
69
+ const n = rows.length
70
+ let i = ((cursor + delta) % n + n) % n
71
+ // 防止死循环(理论上 rows 永不为空时不会触发)
72
+ return i
73
+ }
74
+
75
+ export const groupedCheckbox = createPrompt((config, done) => {
76
+ const theme = makeTheme(config.theme)
77
+ const pageSize = config.pageSize ?? 15
78
+ const allItems = config.items
79
+ const totalCount = allItems.length
80
+
81
+ const [filter, setFilter] = useState('')
82
+ const [cursor, setCursor] = useState(0)
83
+ const [selected, setSelected] = useState(new Set(config.defaultSelected ?? []))
84
+ const [status, setStatus] = useState('idle') // 'idle' | 'done'
85
+
86
+ const rows = useMemo(
87
+ () => buildVisibleRows(allItems, filter, selected),
88
+ [filter, selected],
89
+ )
90
+
91
+ // cursor 越界保护
92
+ const safeCursor = rows.length === 0 ? 0 : Math.min(cursor, rows.length - 1)
93
+
94
+ useKeypress((key) => {
95
+ if (status === 'done') return
96
+
97
+ if (isEnterKey(key)) {
98
+ const result = allItems.filter(it => selected.has(it.name))
99
+ setStatus('done')
100
+ done(result)
101
+ return
102
+ }
103
+
104
+ if (isEscapeKey(key)) {
105
+ if (filter) {
106
+ setFilter('')
107
+ setCursor(0)
108
+ } else {
109
+ setStatus('done')
110
+ done([])
111
+ }
112
+ return
113
+ }
114
+
115
+ if (isUpKey(key)) {
116
+ setCursor(nextCursor(rows, safeCursor, -1))
117
+ return
118
+ }
119
+ if (isDownKey(key)) {
120
+ setCursor(nextCursor(rows, safeCursor, +1))
121
+ return
122
+ }
123
+
124
+ if (isCtrlA(key)) {
125
+ // 全选/全不选当前可见 item
126
+ const visibleItems = rows.filter(r => r.type === 'item').map(r => r.item)
127
+ if (visibleItems.length === 0) return
128
+ const allSelected = visibleItems.every(it => selected.has(it.name))
129
+ const next = new Set(selected)
130
+ if (allSelected) visibleItems.forEach(it => next.delete(it.name))
131
+ else visibleItems.forEach(it => next.add(it.name))
132
+ setSelected(next)
133
+ return
134
+ }
135
+
136
+ if (isSpaceKey(key)) {
137
+ if (rows.length === 0) return
138
+ const row = rows[safeCursor]
139
+ const next = new Set(selected)
140
+ if (row.type === 'item') {
141
+ if (next.has(row.item.name)) next.delete(row.item.name)
142
+ else next.add(row.item.name)
143
+ } else {
144
+ // header:组内全选/全不选
145
+ const allSelected = row.items.every(it => next.has(it.name))
146
+ if (allSelected) row.items.forEach(it => next.delete(it.name))
147
+ else row.items.forEach(it => next.add(it.name))
148
+ }
149
+ setSelected(next)
150
+ return
151
+ }
152
+
153
+ if (isBackspaceKey(key)) {
154
+ if (filter) {
155
+ setFilter(filter.slice(0, -1))
156
+ setCursor(0)
157
+ }
158
+ return
159
+ }
160
+
161
+ if (isPrintable(key)) {
162
+ setFilter(filter + key.sequence)
163
+ setCursor(0)
164
+ return
165
+ }
166
+ })
167
+
168
+ const prefix = usePrefix({ theme })
169
+
170
+ if (status === 'done') {
171
+ const picked = allItems.filter(it => selected.has(it.name))
172
+ const summary = picked.length === 0
173
+ ? chalk.gray('(未选)')
174
+ : chalk.green(`已选 ${picked.length} 项`)
175
+ return `${prefix} ${theme.style.message(config.message, 'done')} ${summary}`
176
+ }
177
+
178
+ const filterLine = filter
179
+ ? `${chalk.cyan('🔍')} ${chalk.yellow(filter)}${chalk.gray('▏')} ${chalk.gray(`${rows.filter(r => r.type === 'item').length}/${totalCount} 匹配`)}`
180
+ : `${chalk.gray('🔍 输入即过滤 ')}${chalk.gray(`${totalCount} 项`)}`
181
+
182
+ const hint = chalk.gray('↑↓ 移动 · space 切换/组全选 · ctrl+a 全选可见 · enter 确认 · esc 清搜索/退出')
183
+
184
+ const renderItem = ({ item: row, isActive }) => {
185
+ if (row.type === 'header') {
186
+ const fullSelected = row.selectedInGroup === row.total && row.total > 0
187
+ const partial = row.selectedInGroup > 0 && !fullSelected
188
+ const marker = fullSelected ? chalk.green('✓') : partial ? chalk.yellow('◐') : chalk.gray('○')
189
+ const arrow = isActive ? chalk.cyan('▶') : ' '
190
+ const label = chalk.bold(`${arrow} ${marker} ${row.category}`)
191
+ const count = chalk.gray(`[${row.selectedInGroup}/${row.total}]`)
192
+ return isActive ? chalk.bgGray.white(`${label} ${count}`) : `${label} ${count}`
193
+ }
194
+ const checked = selected.has(row.item.name)
195
+ const box = checked ? chalk.green('◉') : chalk.gray('◯')
196
+ const cursorMark = isActive ? chalk.cyan('❯') : ' '
197
+ const name = checked ? chalk.green(row.item.name) : chalk.cyan(row.item.name)
198
+ const desc = row.item.description || ''
199
+ const descShort = desc.length > 60 ? desc.slice(0, 60) + '…' : desc
200
+ const line = ` ${cursorMark} ${box} ${name} ${chalk.gray('- ' + descShort)}`
201
+ return isActive ? chalk.bgBlackBright(line) : line
202
+ }
203
+
204
+ const paginated = usePagination({
205
+ items: rows.length > 0 ? rows : [{ type: 'empty' }],
206
+ active: safeCursor,
207
+ renderItem: (ctx) => ctx.item.type === 'empty'
208
+ ? chalk.gray(' (无匹配项,按 esc 清空搜索)')
209
+ : renderItem(ctx),
210
+ pageSize,
211
+ loop: false,
212
+ })
213
+ const page = paginated
214
+
215
+ const message = theme.style.message(config.message, 'idle')
216
+
217
+ return [
218
+ `${prefix} ${message}`,
219
+ filterLine,
220
+ page,
221
+ hint,
222
+ ].join('\n')
223
+ })
package/src/sync.js CHANGED
@@ -3,6 +3,7 @@ import path from 'path'
3
3
  import os from 'os'
4
4
  import inquirer from 'inquirer'
5
5
  import { ensureRepoCache, chalk } from './utils.js'
6
+ import { groupedCheckbox } from './prompts/grouped-checkbox.js'
6
7
 
7
8
  const CATEGORIES = [
8
9
  { key: 'skills', label: 'Skills' },
@@ -11,12 +12,38 @@ const CATEGORIES = [
11
12
  { key: 'hooks', label: 'Hooks' },
12
13
  ]
13
14
 
14
- function extractDescription(content) {
15
+ function extractFrontmatter(content) {
15
16
  const match = content.match(/^---\s*\n([\s\S]*?)\n---/)
16
- if (!match) return ''
17
- const frontmatter = match[1]
18
- const descMatch = frontmatter.match(/description:\s*(.+)/)
19
- return descMatch ? descMatch[1].trim().replace(/^["']|["']$/g, '') : ''
17
+ if (!match) return { description: '', category: '其他' }
18
+ const lines = match[1].split('\n')
19
+ const result = { description: '', category: '其他' }
20
+ let i = 0
21
+ while (i < lines.length) {
22
+ const kv = lines[i].match(/^([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/)
23
+ if (!kv) { i++; continue }
24
+ const [, key, value] = kv
25
+ if (key === 'category') {
26
+ result.category = value.trim().replace(/^["']|["']$/g, '') || '其他'
27
+ i++
28
+ } else if (key === 'description') {
29
+ const trimmed = value.trim()
30
+ if (trimmed === '>' || trimmed === '|') {
31
+ i++
32
+ const parts = []
33
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === '')) {
34
+ parts.push(lines[i].trim())
35
+ i++
36
+ }
37
+ result.description = parts.filter(Boolean).join(' ')
38
+ } else {
39
+ result.description = trimmed.replace(/^["']|["']$/g, '')
40
+ i++
41
+ }
42
+ } else {
43
+ i++
44
+ }
45
+ }
46
+ return result
20
47
  }
21
48
 
22
49
  async function scanSkills(sourceDir) {
@@ -31,9 +58,11 @@ async function scanSkills(sourceDir) {
31
58
  const skillMd = path.join(skillPath, 'SKILL.md')
32
59
  if (!await fs.pathExists(skillMd)) continue
33
60
  const content = await fs.readFile(skillMd, 'utf-8')
61
+ const fm = extractFrontmatter(content)
34
62
  skills.push({
35
63
  name: entry.name,
36
- description: extractDescription(content) || '(无描述)',
64
+ description: fm.description || '(无描述)',
65
+ category: fm.category,
37
66
  sourcePath: skillPath,
38
67
  type: 'skill',
39
68
  })
@@ -51,9 +80,11 @@ async function scanRules(sourceDir) {
51
80
  if (!entry.isFile() || !entry.name.endsWith('.md')) continue
52
81
  const rulePath = path.join(rulesDir, entry.name)
53
82
  const content = await fs.readFile(rulePath, 'utf-8')
83
+ const fm = extractFrontmatter(content)
54
84
  rules.push({
55
85
  name: entry.name,
56
- description: extractDescription(content) || '(无描述)',
86
+ description: fm.description || '(无描述)',
87
+ category: fm.category,
57
88
  sourcePath: rulePath,
58
89
  type: 'rule',
59
90
  })
@@ -71,6 +102,7 @@ async function scanCommands(sourceDir) {
71
102
  .map(e => ({
72
103
  name: e.name,
73
104
  description: e.isDirectory() ? '命令目录' : '命令文件',
105
+ category: '其他',
74
106
  sourcePath: path.join(commandsDir, e.name),
75
107
  type: 'command',
76
108
  }))
@@ -87,6 +119,7 @@ async function scanHooks(sourceDir) {
87
119
  .map(e => ({
88
120
  name: e.name,
89
121
  description: e.isDirectory() ? '钩子目录' : '钩子脚本',
122
+ category: '其他',
90
123
  sourcePath: path.join(hooksDir, e.name),
91
124
  type: 'hook',
92
125
  }))
@@ -102,19 +135,12 @@ async function scanExisting(projectTargetDir, typePlural) {
102
135
 
103
136
  async function promptSelection(items, label, defaultNames) {
104
137
  if (items.length === 0) return []
105
- const { selected } = await inquirer.prompt([{
106
- type: 'checkbox',
107
- name: 'selected',
108
- message: `选择 ${label}(空格勾选,回车确认):`,
109
- choices: items.map(item => ({
110
- name: `${chalk.cyan(item.name)} ${chalk.gray('- ' + item.description.slice(0, 60))}${item.description.length > 60 ? '...' : ''}`,
111
- value: item,
112
- short: item.name,
113
- checked: defaultNames.has(item.name),
114
- })),
138
+ return groupedCheckbox({
139
+ message: `选择 ${label}`,
140
+ items,
141
+ defaultSelected: defaultNames,
115
142
  pageSize: 15,
116
- }])
117
- return selected
143
+ })
118
144
  }
119
145
 
120
146
  /**
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Android (Java + Kotlin + XML) 代码注释与基础风格规范
3
+ category: Android
3
4
  paths:
4
5
  - "**/*.java"
5
6
  - "**/*.kt"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Alembic 数据库迁移规范
3
+ category: 数据库
3
4
  paths:
4
5
  - "**/alembic/**"
5
6
  - "**/alembic.ini"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Flutter + Dart 基础风格(结构/Riverpod/命名/模型/Dart 语言/Lint)
3
+ category: Flutter
3
4
  paths:
4
5
  - "**/*.dart"
5
6
  - "**/pubspec.yaml"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Flutter Dio 网络层 + 拦截器 + 错误处理
3
+ category: Flutter
3
4
  paths:
4
5
  - "**/core/network/**/*.dart"
5
6
  - "**/dio_client.dart"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Flutter GoRouter 路由规范
3
+ category: Flutter
3
4
  paths:
4
5
  - "**/app.dart"
5
6
  - "**/router/**/*.dart"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Flutter / Dart 测试规范(mocktail + ProviderContainer + Widget 测试)
3
+ category: Flutter
3
4
  paths:
4
5
  - "**/test/**/*.dart"
5
6
  - "**/*_test.dart"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: PostgreSQL 安全与配置规范(参数化查询、应用用户最小权限、连接池上限、SSL、敏感字段加密)
3
+ category: 数据库
3
4
  ---
4
5
 
5
6
  # PostgreSQL 安全与配置规范
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Python 后端(FastAPI + SQLAlchemy)应用层开发规范 — 路由 / Schema / 依赖注入 / ORM / 安全编码 / 注入防护 / 限流 / 审计 / 违规检测清单
3
+ category: Python
3
4
  paths:
4
5
  - "**/*.py"
5
6
  ---
@@ -200,7 +201,7 @@ class UserBase(BaseModel):
200
201
  email: EmailStr
201
202
 
202
203
  class UserCreate(UserBase):
203
- password: str = Field(min_length=8, max_length=128)
204
+ password: str = Field(min_length=8, max_length=72) # 72 = bcrypt 输入上限
204
205
 
205
206
  class UserUpdate(BaseModel):
206
207
  name: str | None = Field(default=None, max_length=50)
@@ -330,15 +331,55 @@ user = result.scalar_one_or_none()
330
331
  |------|------|
331
332
  | 存储算法 | **只允许 bcrypt**,必须走 dwyeapi 安全模块,**禁止**手写 bcrypt 调用或使用 MD5 / SHA / 明文 |
332
333
  | 接口返回 | **永远不返回**密码字段,Response Schema 中不包含 |
333
- | 密码强度 | 用户密码最少 8 位,管理员密码最少 12 位 |
334
+ | 密码强度 | **长度 ≥ 8 字符**,且**必须**包含 **大写字母、小写字母、数字、特殊符号** 中的至少 **3 类** |
335
+ | 密码长度上限 | Schema 必须 `max_length=72`(bcrypt 输入硬上限,超过会被静默截断),除非 dwyeapi 已做 SHA-256 预哈希 |
334
336
  | 登录错误 | 返回"用户名或密码错误",**不区分**是用户名不存在还是密码错误 |
337
+ | 强度校验位置 | Pydantic Schema 层用 `field_validator` 校验复杂度,**禁止**只校长度 |
338
+
339
+ **密码复杂度校验示例**:
340
+
341
+ ```python
342
+ import re
343
+ from pydantic import BaseModel, Field, field_validator
344
+
345
+ PASSWORD_PATTERNS = [
346
+ re.compile(r"[A-Z]"), # 大写
347
+ re.compile(r"[a-z]"), # 小写
348
+ re.compile(r"\d"), # 数字
349
+ re.compile(r"[^A-Za-z0-9]"), # 特殊符号
350
+ ]
351
+
352
+ class UserCreate(BaseModel):
353
+ password: str = Field(min_length=8, max_length=72)
354
+
355
+ @field_validator("password")
356
+ @classmethod
357
+ def check_complexity(cls, v: str) -> str:
358
+ """密码必须包含大写/小写/数字/特殊符号中至少 3 类。"""
359
+ matched = sum(1 for p in PASSWORD_PATTERNS if p.search(v))
360
+ if matched < 3:
361
+ raise ValueError("密码必须包含大写、小写、数字、特殊符号中至少 3 类")
362
+ return v
363
+ ```
364
+
365
+ > 该 validator 建议下沉到 dwyeapi 提供统一的 `PasswordStr` 类型,避免每个项目重复实现。
335
366
 
336
367
  ### 7.3 接口认证与授权
337
368
 
338
369
  - 所有 API **必须** JWT 认证(白名单除外:`/health` / `/auth/login` / `/auth/register`)
339
370
  - JWT 创建与解码**必须**走 dwyeapi 安全模块,**禁止**直接调用 PyJWT
340
371
  - Token 黑名单存 Redis(登出后失效,TTL = 剩余有效期)
341
- - Secret Key 必须从环境变量读取,最少 32 位随机字符串,**禁止**硬编码
372
+ - Secret Key 必须从环境变量读取,**禁止**硬编码
373
+ - Secret Key 长度与生成方式(按字节算,不按字符算):
374
+
375
+ | JWT 算法 | 最小长度 | 推荐生成命令 |
376
+ |---------|---------|------------|
377
+ | HS256 | **32 字节 / 256 bit** | `openssl rand -hex 32`(输出 64 个十六进制字符 = 32 字节) |
378
+ | HS512 | **64 字节 / 512 bit** | `openssl rand -hex 64`(输出 128 个十六进制字符 = 64 字节) |
379
+ | Python 等价 | — | `python -c "import secrets; print(secrets.token_urlsafe(32))"` |
380
+
381
+ - **禁止**使用:`uuid.uuid4().hex`(熵不足且非密码学 PRNG 推荐用途)、`random` 模块、手写字符串拼接
382
+ - 轮换策略:每 90 天 / 安全事件后立即轮换;新旧 Key 共存 1 个 Access Token 有效期窗口,避免在线用户被强制登出
342
383
 
343
384
  | Token 类型 | 有效期 |
344
385
  |------------|--------|
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Python 通用代码规范
3
+ category: Python
3
4
  paths:
4
5
  - "**/*.py"
5
6
  - "**/pyproject.toml"
@@ -18,7 +19,10 @@ paths:
18
19
  ### 环境变量
19
20
 
20
21
  - **禁止**在 shell 前台 `export FOO=...` 注入环境变量
21
- - **禁止**在代码中硬编码配置值(数据库地址、密钥、端口等)
22
+ - **禁止**在代码中硬编码以下配置值:
23
+ - **基础设施**:数据库地址、Redis / MQ 地址、密钥、端口、外部 API 地址
24
+ - **业务参数**:缓存 TTL、HTTP 超时、重试次数 / 间隔、批处理大小、限流阈值(rate / burst)、分页默认值与上限、文件大小上限、Token 有效期、定时任务周期
25
+ - 判定标准:**任何"运维 / 业务可能想调整、且不希望发版"的值,都必须走配置**。代码中只允许出现:算法常量(PI、HTTP 状态码)、协议规定的固定值、纯类型枚举(如 `Status.ACTIVE = 1`)
22
26
  - 环境变量统一通过 `.env` 文件加载,由 Pydantic Settings 读取(见「配置管理」节)
23
27
  - `.env` 必须列入 `.gitignore`,提交 `.env.example` 作为模板
24
28
 
@@ -149,6 +153,36 @@ class Settings(BaseSettings):
149
153
  | 只有 1 个字段 | 保持顶层,不必嵌套 |
150
154
  | 3 个以上同前缀字段 | 必须嵌套 |
151
155
 
156
+ ### 什么必须走配置(判定表)
157
+
158
+ 写代码时遇到字面量数字 / 字符串,先对照此表判断是否抽到 Settings:
159
+
160
+ | 类别 | 示例 | 是否走配置 |
161
+ |------|------|-----------|
162
+ | 基础设施地址 | `postgresql://...` / `redis://...` / `https://api.xxx.com` | ✅ 必须 |
163
+ | 凭证 | SECRET_KEY / API_KEY / DB 密码 / OSS AK·SK | ✅ 必须(致命) |
164
+ | 缓存 TTL | `redis.setex(key, 43200, v)` 中的 `43200` | ✅ 必须 → `settings.cache.user_token_ttl` |
165
+ | HTTP 超时 / 重试 | `httpx.AsyncClient(timeout=30)` 中的 `30` | ✅ 必须 |
166
+ | 限流阈值 | `@rate_limit(100, per=60)` 中的 `100` / `60` | ✅ 必须 |
167
+ | 分页默认 / 上限 | `page_size: int = 20` / `le=100` 的 `100` | ✅ 必须 |
168
+ | 文件大小上限 | `if size > 10 * 1024 * 1024` 的 `10MB` | ✅ 必须 |
169
+ | Token 有效期 | JWT `exp` 的 7 天 / 30 天 | ✅ 必须 |
170
+ | 定时任务周期 | Celery / APScheduler 的 cron / interval | ✅ 必须 |
171
+ | 算法常量 | `PI = 3.14159` / `EARTH_RADIUS = 6371` | ❌ 不走配置 |
172
+ | 协议规定值 | HTTP 状态码 `200` / `404` | ❌ 不走配置 |
173
+ | 纯枚举值 | `class Status: ACTIVE = 1` | ❌ 不走配置 |
174
+
175
+ ```python
176
+ # ❌ 错误:缓存 TTL 硬编码
177
+ await redis.setex(f"user:{uid}", 43200, json.dumps(user))
178
+
179
+ # ✅ 正确:走 Settings
180
+ class CacheConfig(BaseModel):
181
+ user_ttl: int = 43200 # 默认 12h,可通过 CACHE__USER_TTL 覆盖
182
+
183
+ await redis.setex(f"user:{uid}", settings.cache.user_ttl, json.dumps(user))
184
+ ```
185
+
152
186
  ## 六、函数与类设计
153
187
 
154
188
  ### 强制规则
@@ -290,6 +324,6 @@ user = cache.get_user(user_id)
290
324
  | 8 | 嵌套不超过 3 层,函数体不超过 50 行 |
291
325
  | 9 | 所有模块/类/函数有 docstring(Google 风格) |
292
326
  | 10 | 外部输入用 Pydantic 校验,不手动校验 dict |
293
- | 11 | 配置必须 Pydantic Settings 嵌套分组,不在代码硬编码 |
327
+ | 11 | 配置必须 Pydantic Settings 嵌套分组;缓存 TTL / 超时 / 重试 / 限流 / 分页上限 / 文件上限 / Token 有效期等业务参数禁止硬编码 |
294
328
 
295
329
  **不执行自检就提交代码 = 违规。**
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Python pytest 测试规范(目录组织、fixture、异步、AAA 结构)
3
+ category: Python
3
4
  paths:
4
5
  - "**/tests/**/*.py"
5
6
  - "**/test_*.py"
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Redis 安全与使用规范(必须用 dwyeapi 客户端、密码、绑定、危险命令禁用、key TTL 与前缀、敏感数据禁存)
3
+ category: 数据库
3
4
  ---
4
5
 
5
6
  # Redis 安全与使用规范
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: TDD 开发流程(新功能/bug 修复/重构的测试先行与回归策略)
3
+ category: 开发流程
3
4
  ---
4
5
 
5
6
  # TDD 开发规则
@@ -1,7 +1,9 @@
1
1
  ---
2
2
  description: Vue 3 + TS + Vite + Tailwind 应用层规范(强制使用 eui/ekit、业务聚合结构、组件 / TS / 路由 / API / 模板 / 样式 / 错误处理 / 注释)
3
+ category: Vue
3
4
  paths:
4
5
  - "**/*.vue"
6
+ - "**/*.ts"
5
7
  - "**/vite.config.*"
6
8
  ---
7
9
 
@@ -108,12 +110,6 @@ src/
108
110
  - 单文件超过约 400 行时再考虑拆分,**禁止**未到规模就预拆分
109
111
  - 模块间引用通过明确 import,**禁止**循环依赖
110
112
 
111
- ### 例外
112
-
113
- - 小型项目(< 3 个业务模块)允许扁平结构(`src/views/` + `src/components/`),但**不得**长大后保持扁平
114
-
115
- ---
116
-
117
113
  ## 二、组件规范
118
114
 
119
115
  ### 强制使用 Composition API + `<script setup>`
@@ -550,43 +546,7 @@ export function formatMoney(value: number, currency = '¥'): string {
550
546
 
551
547
  ---
552
548
 
553
- ## 十四、代码简约原则
554
-
555
- 代码只写**必要的逻辑**,不写"以防万一"的冗余代码。信任 TypeScript 类型系统与框架保证,只在系统边界(用户输入、外部 API)做校验。
556
-
557
- ### 禁止的冗余模式
558
-
559
- ```typescript
560
- // 反例:类型已保证非空,多余 fallback
561
- const name = props.title ?? '' // props.title: string
562
-
563
- // 正例
564
- const name = props.title
565
-
566
- // 反例:值一定存在
567
- const id = user?.id // user 来自必传 prop
568
-
569
- // 正例
570
- const id = user.id
571
-
572
- // 反例:空 catch 吞异常
573
- try { await api.getUsers() } catch {}
574
-
575
- // 正例:让异常暴露或给用户反馈
576
- try { await api.getUsers() } catch { toast.error('加载失败') }
577
- ```
578
-
579
- ### 判断标准
580
-
581
- 写每行防御代码前自问:**这个情况在当前上下文真的会发生吗?**
582
-
583
- - 会 → 写防御,注释说明触发条件
584
- - 不会 → 不写,信任上游保证
585
- - 不确定 → 查类型定义与调用链确认
586
-
587
- ---
588
-
589
- ## 十五、违规检测清单
549
+ ## 十四、违规检测清单
590
550
 
591
551
  AI 编写或审查 Vue 代码时,**必须**检查以下违规模式:
592
552
 
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  description: Pinia Setup Store 状态管理规范(强制 Setup Store、storeToRefs、按业务聚合)
3
+ category: Vue
3
4
  paths:
4
5
  - "**/store.ts"
5
6
  - "**/stores/**/*.ts"