create-dwy 0.12.0 → 0.12.2

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 (23) hide show
  1. package/package.json +1 -1
  2. package/src/sync.js +29 -17
  3. package/templates/claude-global/rules/Python/dwy-python-core.md +105 -7
  4. package/templates/claude-global/skills//345/205/203/345/267/245/345/205/267/dwy-shared/SKILL.md +255 -0
  5. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/SKILL.md +232 -0
  6. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/files-whitelist-snippet.json +22 -0
  7. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/js-commercial.npmignore +76 -0
  8. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial-MANIFEST.in +37 -0
  9. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/assets/python-commercial.gitignore +62 -0
  10. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/js-commercial-guide.md +133 -0
  11. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/publish-blacklist.md +106 -0
  12. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/python-commercial-guide.md +147 -0
  13. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/references/sensitive-patterns.md +112 -0
  14. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_comments.py +260 -0
  15. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_js_publish.sh +110 -0
  16. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/check_python_publish.sh +171 -0
  17. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/detect_sdk.sh +71 -0
  18. package/templates/claude-global/skills//345/256/211/345/205/250/dwy-sdk-spec/scripts/init_config.sh +97 -0
  19. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/SKILL.md +170 -0
  20. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/examples//351/205/215/351/242/235/350/256/241/347/256/227.md +64 -0
  21. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/INDEX.md +7 -0
  22. package/templates/claude-global/skills//345/274/200/345/217/221/346/265/201/347/250/213/dwy-code-flow-docs/templates/logic-doc.md +49 -0
  23. /package/templates/claude-global/hooks/{pre-git-commit-sensitive-check.sh → Git/pre-git-commit-sensitive-check.sh} +0 -0
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bash
2
+ # Python SDK 发布前文件清单审计(不实际发布)
3
+ # 用法: check_python_publish.sh <python_sdk_root> --edition commercial|opensource
4
+
5
+ set -euo pipefail
6
+
7
+ if [[ $# -lt 3 ]]; then
8
+ echo "Usage: $0 <python_sdk_root> --edition commercial|opensource" >&2
9
+ exit 2
10
+ fi
11
+
12
+ PKG_ROOT="$1"
13
+ shift
14
+
15
+ EDITION=""
16
+ while [[ $# -gt 0 ]]; do
17
+ case "$1" in
18
+ --edition) EDITION="$2"; shift 2 ;;
19
+ *) echo "ERROR: unknown arg: $1" >&2; exit 2 ;;
20
+ esac
21
+ done
22
+
23
+ if [[ "$EDITION" != "commercial" && "$EDITION" != "opensource" ]]; then
24
+ echo "ERROR: --edition must be 'commercial' or 'opensource'" >&2
25
+ exit 2
26
+ fi
27
+
28
+ if [[ ! -d "$PKG_ROOT" ]]; then
29
+ echo "ERROR: not a directory: $PKG_ROOT" >&2
30
+ exit 2
31
+ fi
32
+
33
+ if ! command -v python >/dev/null 2>&1 && ! command -v python3 >/dev/null 2>&1; then
34
+ echo "ERROR: python not found in PATH" >&2
35
+ exit 2
36
+ fi
37
+ PY=$(command -v python || command -v python3)
38
+
39
+ # 临时构建目录
40
+ TMPDIR=$(mktemp -d -t dwy-sdk-spec-py-XXXX)
41
+ trap 'rm -rf "$TMPDIR"' EXIT
42
+
43
+ echo "[1/3] Building wheel to $TMPDIR ..."
44
+ (cd "$PKG_ROOT" && "$PY" -m build --outdir "$TMPDIR") >&2
45
+
46
+ WHEEL=$(find "$TMPDIR" -name '*.whl' -type f | head -1)
47
+ if [[ -z "$WHEEL" ]]; then
48
+ echo "ERROR: no wheel produced" >&2
49
+ exit 2
50
+ fi
51
+ echo "[2/3] Listing wheel contents: $(basename "$WHEEL")"
52
+
53
+ # 列文件(剥掉 .dist-info 路径前缀,便于审计)
54
+ FILES=$(unzip -Z1 "$WHEEL")
55
+
56
+ # 致命级(任何版本都禁)
57
+ declare -a CRITICAL_PATTERNS=(
58
+ '(^|/)\.env(\..+)?$'
59
+ '(^|/)\.npmrc$'
60
+ '(^|/)\.pypirc$'
61
+ '(^|/)\.git-credentials$'
62
+ '(^|/)id_rsa$'
63
+ '(^|/)id_ed25519$'
64
+ '\.pem$'
65
+ '\.key$'
66
+ '\.p12$'
67
+ '(^|/)secrets\.json$'
68
+ '(^|/)credentials\.json$'
69
+ )
70
+
71
+ # 开源版黑名单
72
+ declare -a OPENSOURCE_DENY=(
73
+ '__pycache__/'
74
+ '\.pyc$'
75
+ '\.pyo$'
76
+ '\.egg-info/'
77
+ '\.mypy_cache/'
78
+ '\.pytest_cache/'
79
+ '\.ruff_cache/'
80
+ '\.pdb$'
81
+ '(^|/)\.git/'
82
+ '(^|/)\.gitignore$'
83
+ '(^|/)\.DS_Store$'
84
+ '(^|/)\.vscode/'
85
+ '(^|/)\.idea/'
86
+ '\.bak$'
87
+ '\.swp$'
88
+ )
89
+
90
+ # 商业版白名单(其他视为违规)
91
+ declare -a COMMERCIAL_ALLOW=(
92
+ '\.so$'
93
+ '\.pyi$'
94
+ '(^|/)py\.typed$'
95
+ '\.dist-info/'
96
+ '(^|/)LICEN[SC]E(\..+)?$'
97
+ '(^|/)README(\..+)?$'
98
+ '\.json$'
99
+ '\.txt$'
100
+ )
101
+
102
+ violations=()
103
+ critical=()
104
+
105
+ while IFS= read -r f; do
106
+ [[ -z "$f" ]] && continue
107
+
108
+ # 致命级
109
+ for pat in "${CRITICAL_PATTERNS[@]}"; do
110
+ if [[ "$f" =~ $pat ]]; then
111
+ critical+=("$f")
112
+ continue 2
113
+ fi
114
+ done
115
+
116
+ if [[ "$EDITION" == "opensource" ]]; then
117
+ for pat in "${OPENSOURCE_DENY[@]}"; do
118
+ if [[ "$f" =~ $pat ]]; then
119
+ violations+=("$f [denied: $pat]")
120
+ break
121
+ fi
122
+ done
123
+ else
124
+ # commercial: 白名单检查
125
+ # 排除目录条目(以 / 结尾)
126
+ [[ "$f" == */ ]] && continue
127
+ matched=0
128
+ for pat in "${COMMERCIAL_ALLOW[@]}"; do
129
+ if [[ "$f" =~ $pat ]]; then
130
+ matched=1
131
+ break
132
+ fi
133
+ done
134
+ # 单独允许:包根的 __init__.py(公开入口)/ 不以 _ 开头的 .py
135
+ if [[ $matched -eq 0 ]] && [[ "$f" =~ \.py$ ]]; then
136
+ base=$(basename "$f")
137
+ if [[ "$base" == "__init__.py" ]] || [[ ! "$base" =~ ^_ ]]; then
138
+ matched=1
139
+ fi
140
+ fi
141
+ if [[ $matched -eq 0 ]]; then
142
+ violations+=("$f [not in commercial whitelist]")
143
+ fi
144
+ fi
145
+ done <<< "$FILES"
146
+
147
+ echo "[3/3] Audit result (edition=$EDITION):"
148
+ echo "----------------------------------------"
149
+
150
+ if [[ ${#critical[@]} -gt 0 ]]; then
151
+ echo "[CRITICAL] secret-like files in wheel — DO NOT PUBLISH:"
152
+ for f in "${critical[@]}"; do
153
+ echo " $f"
154
+ done
155
+ echo ""
156
+ fi
157
+
158
+ if [[ ${#violations[@]} -gt 0 ]]; then
159
+ echo "[VIOLATION] ${#violations[@]} file(s) violate $EDITION rules:"
160
+ for v in "${violations[@]}"; do
161
+ echo " $v"
162
+ done
163
+ echo ""
164
+ exit 1
165
+ fi
166
+
167
+ if [[ ${#critical[@]} -gt 0 ]]; then
168
+ exit 1
169
+ fi
170
+
171
+ echo "OK: wheel passes $EDITION audit ($(echo "$FILES" | wc -l | tr -d ' ') file(s))"
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # 探测项目内的 Python / JS SDK 子包
3
+ # 用法: detect_sdk.sh <project_root>
4
+ # 输出: JSON {"python": [...], "js": [...]} 路径相对项目根
5
+
6
+ set -euo pipefail
7
+
8
+ PROJECT_ROOT="${1:-$(pwd)}"
9
+
10
+ if [[ ! -d "$PROJECT_ROOT" ]]; then
11
+ echo "ERROR: not a directory: $PROJECT_ROOT" >&2
12
+ exit 2
13
+ fi
14
+
15
+ # 找 Python SDK:pyproject.toml 含 [project] 段,或 setup.py
16
+ find_python_sdks() {
17
+ local results=()
18
+ # 项目根
19
+ if [[ -f "$PROJECT_ROOT/pyproject.toml" ]] && grep -q '^\[project\]' "$PROJECT_ROOT/pyproject.toml" 2>/dev/null; then
20
+ results+=(".")
21
+ elif [[ -f "$PROJECT_ROOT/setup.py" ]]; then
22
+ results+=(".")
23
+ fi
24
+ # 子目录(限 2 层深度,排除 node_modules / .venv / venv / dist / build / .git)
25
+ while IFS= read -r f; do
26
+ local d
27
+ d=$(dirname "$f")
28
+ local rel="${d#"$PROJECT_ROOT"/}"
29
+ # 验证含 [project] 段
30
+ if grep -q '^\[project\]' "$f" 2>/dev/null; then
31
+ results+=("$rel")
32
+ fi
33
+ done < <(find "$PROJECT_ROOT" -mindepth 2 -maxdepth 3 -name pyproject.toml -type f \
34
+ -not -path "*/node_modules/*" \
35
+ -not -path "*/.venv/*" \
36
+ -not -path "*/venv/*" \
37
+ -not -path "*/dist/*" \
38
+ -not -path "*/build/*" \
39
+ -not -path "*/.git/*" 2>/dev/null)
40
+
41
+ # 去重
42
+ printf '%s\n' "${results[@]}" | awk 'NF && !seen[$0]++'
43
+ }
44
+
45
+ # 找 JS SDK:package.json 含 main / exports / module 字段
46
+ find_js_sdks() {
47
+ local results=()
48
+ if [[ -f "$PROJECT_ROOT/package.json" ]] && jq -e '.main // .exports // .module' "$PROJECT_ROOT/package.json" >/dev/null 2>&1; then
49
+ results+=(".")
50
+ fi
51
+ while IFS= read -r f; do
52
+ local d
53
+ d=$(dirname "$f")
54
+ local rel="${d#"$PROJECT_ROOT"/}"
55
+ if jq -e '.main // .exports // .module' "$f" >/dev/null 2>&1; then
56
+ results+=("$rel")
57
+ fi
58
+ done < <(find "$PROJECT_ROOT" -mindepth 2 -maxdepth 3 -name package.json -type f \
59
+ -not -path "*/node_modules/*" \
60
+ -not -path "*/.venv/*" \
61
+ -not -path "*/dist/*" \
62
+ -not -path "*/build/*" \
63
+ -not -path "*/.git/*" 2>/dev/null)
64
+
65
+ printf '%s\n' "${results[@]}" | awk 'NF && !seen[$0]++'
66
+ }
67
+
68
+ python_list=$(find_python_sdks | jq -R . | jq -s .)
69
+ js_list=$(find_js_sdks | jq -R . | jq -s .)
70
+
71
+ jq -n --argjson py "$python_list" --argjson js "$js_list" '{python: $py, js: $js}'
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # dwy-sdk-spec 配置缓存读写
3
+ # 用法:
4
+ # init_config.sh get <project_path>
5
+ # init_config.sh set <project_path> <edition> <languages_json> <sdk_paths_json>
6
+ # init_config.sh reset <project_path>
7
+ # init_config.sh list
8
+
9
+ set -euo pipefail
10
+
11
+ CONFIG_DIR="${HOME}/.config/dwy-sdk-spec"
12
+ CONFIG_FILE="${CONFIG_DIR}/projects.json"
13
+
14
+ ensure_config() {
15
+ mkdir -p "$CONFIG_DIR"
16
+ if [[ ! -f "$CONFIG_FILE" ]]; then
17
+ echo '{"version":"1","projects":{}}' > "$CONFIG_FILE"
18
+ fi
19
+ # 检测损坏
20
+ if ! jq empty "$CONFIG_FILE" 2>/dev/null; then
21
+ echo "ERROR: $CONFIG_FILE is corrupted. Back it up and delete to reset." >&2
22
+ exit 2
23
+ fi
24
+ }
25
+
26
+ cmd_get() {
27
+ local project_path="$1"
28
+ ensure_config
29
+ jq --arg p "$project_path" '.projects[$p] // null' "$CONFIG_FILE"
30
+ }
31
+
32
+ cmd_set() {
33
+ local project_path="$1"
34
+ local edition="$2"
35
+ local languages_json="$3"
36
+ local sdk_paths_json="$4"
37
+
38
+ if [[ "$edition" != "commercial" && "$edition" != "opensource" ]]; then
39
+ echo "ERROR: edition must be 'commercial' or 'opensource', got: $edition" >&2
40
+ exit 2
41
+ fi
42
+
43
+ ensure_config
44
+ local now
45
+ now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
46
+
47
+ local tmp
48
+ tmp=$(mktemp)
49
+ jq \
50
+ --arg p "$project_path" \
51
+ --arg e "$edition" \
52
+ --argjson l "$languages_json" \
53
+ --argjson s "$sdk_paths_json" \
54
+ --arg t "$now" \
55
+ '.projects[$p] = {edition: $e, languages: $l, sdk_paths: $s, configured_at: $t}' \
56
+ "$CONFIG_FILE" > "$tmp"
57
+ mv "$tmp" "$CONFIG_FILE"
58
+
59
+ echo "OK: cached $project_path -> $edition"
60
+ }
61
+
62
+ cmd_reset() {
63
+ local project_path="$1"
64
+ ensure_config
65
+ local tmp
66
+ tmp=$(mktemp)
67
+ jq --arg p "$project_path" 'del(.projects[$p])' "$CONFIG_FILE" > "$tmp"
68
+ mv "$tmp" "$CONFIG_FILE"
69
+ echo "OK: removed $project_path from cache"
70
+ }
71
+
72
+ cmd_list() {
73
+ ensure_config
74
+ jq '.projects | to_entries | map({path: .key, edition: .value.edition, languages: .value.languages})' "$CONFIG_FILE"
75
+ }
76
+
77
+ main() {
78
+ if [[ $# -lt 1 ]]; then
79
+ echo "Usage: $0 {get|set|reset|list} [args...]" >&2
80
+ exit 2
81
+ fi
82
+ local action="$1"
83
+ shift
84
+
85
+ case "$action" in
86
+ get) cmd_get "$@" ;;
87
+ set) cmd_set "$@" ;;
88
+ reset) cmd_reset "$@" ;;
89
+ list) cmd_list ;;
90
+ *)
91
+ echo "ERROR: unknown action: $action" >&2
92
+ exit 2
93
+ ;;
94
+ esac
95
+ }
96
+
97
+ main "$@"
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: dwy-code-flow-docs
3
+ description: 代码逻辑梳理文档。把指定的代码流程整理成非 IT 人员能看懂的 Markdown 文档(Mermaid 流程图 + 概述 + 关键配置 + 依赖关系)。触发场景:用户说"梳理 XX 逻辑"/"整理 XX 流程"/"帮我梳理一下 XXX"(创建)、"更新 XX 逻辑"/"刷新 XX 文档"(更新)、"现在梳理了哪些逻辑"/"列出已梳理的流程"(查询)、"强制重新梳理 XX"/"重扫 XX"(重扫)。文档存在用户指定根目录,扁平结构+中文文件名,YAML frontmatter 记录 sources + git commit,更新时先用 git diff 判断有无变化,无变化跳过、有变化只读 diff,大幅节省 token。
4
+ ---
5
+
6
+ # dwy-code-flow-docs
7
+
8
+ 把代码流程梳理成非 IT 人员能看懂的 Markdown 文档。Claude 主动读代码,套统一模板,放在用户指定根目录。
9
+
10
+ ---
11
+
12
+ ## 何时触发
13
+
14
+ | 用户原话样例 | 操作类型 |
15
+ |---|---|
16
+ | 梳理 XX 逻辑 / 整理 XX 流程 / 帮我梳理一下 XXX | **创建** |
17
+ | 更新 XX 逻辑 / 刷新 XX 文档 / 重新梳理 XX(无"强制") | **更新** |
18
+ | 现在梳理了哪些逻辑 / 列出已梳理的流程 / 已经梳理过哪些 | **查询** |
19
+ | 强制重新梳理 XX / 重扫 XX / rescan XX | **重扫** |
20
+
21
+ ---
22
+
23
+ ## 根目录配置
24
+
25
+ 每个项目第一次使用时,先确认存储根目录。
26
+
27
+ ### 读取顺序
28
+ 1. 读项目根 `.claude/code-flow-docs.config`,格式 `root=<绝对路径>`
29
+ 2. 文件不存在 → 问用户:"代码逻辑文档存到哪里?给我一个绝对路径"
30
+ 3. 拿到路径后:
31
+ - 检查路径父目录存在;不存在则提示用户先创建
32
+ - 写入 `.claude/code-flow-docs.config`,内容仅一行 `root=<绝对路径>`
33
+ - 在该路径下创建 `INDEX.md`(若不存在)
34
+
35
+ ### 配置文件示例
36
+ ```
37
+ root=/Users/chances/WebstormProjects/ai-quant/docs/code-flows
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 操作:创建新文档
43
+
44
+ 1. 从用户原话提取**逻辑名称**(如"配额计算"、"因子流式查询")
45
+ 2. 目标路径 = `<root>/<逻辑名称>.md`
46
+ 3. 文件已存在 → 提示"该逻辑已梳理过,是否要更新?",改走**更新**流程
47
+ 4. 文件不存在 → 进入读代码阶段:
48
+ - 用 Grep 工具按关键词扫描(中英混合,如 `quota|配额|billing|deduct`)
49
+ - 读相关文件,弄清楚流程
50
+ - 按 `templates/logic-doc.md` 套模板生成文档
51
+ - 写质量参照 `examples/配额计算.md`
52
+ 5. **写 YAML frontmatter(必填,不可省略)**:
53
+ - `title`: 逻辑名称
54
+ - `updated`: 今天日期 (YYYY-MM-DD)
55
+ - `last_commit`: `git rev-parse HEAD` 的完整 hash
56
+ - `sources`: 实际读过的文件**相对项目根**的路径列表
57
+ - `scan_keywords`: 这次 grep 用过的关键词列表
58
+ 6. 更新 INDEX.md(不存在则按 `templates/INDEX.md` 创建),新增一行
59
+ 7. 输出文件**完整绝对路径**给用户
60
+
61
+ ---
62
+
63
+ ## 操作:更新已有文档
64
+
65
+ **核心:先 git diff 判断有无变化,无变化直接退出,不读代码。**
66
+
67
+ 1. 读 `<root>/<逻辑名称>.md` 的 YAML frontmatter,拿 `last_commit` 和 `sources`
68
+ 2. 跑命令(**不带 HEAD,含未提交改动**):
69
+ ```bash
70
+ git diff --stat <last_commit> -- <sources 列表展开>
71
+ ```
72
+ 3. 输出为空 → 告诉用户:
73
+ > "自 `<last_commit 短 hash>` 以来,相关代码无变化。文档已是最新,无需更新。"
74
+ 退出,不做任何写入。
75
+ 4. 有输出 → 跑 `git diff <last_commit> -- <sources>` 看具体 diff hunk
76
+ 5. 只读 diff 涉及的代码片段(不读完整文件,除非 diff 涉及的函数依赖外部上下文)
77
+ 6. 重写整个文档(整篇重写策略),更新 frontmatter:
78
+ - `updated`: 今天日期
79
+ - `last_commit`: 当前 `git rev-parse HEAD` 的完整 hash
80
+ - `sources`: 如果有新增相关文件,加入列表
81
+ 7. 更新 INDEX.md 对应行的"更新时间"
82
+ 8. 输出:"已更新 `<完整绝对路径>`,本次变化:`<git diff --stat 摘要>`"
83
+
84
+ ---
85
+
86
+ ## 操作:查询梳理过的逻辑
87
+
88
+ 1. 读 `<root>/INDEX.md`
89
+ 2. 直接把表格输出给用户
90
+
91
+ INDEX.md 不存在 → "还没梳理过任何逻辑,说'梳理 XX 逻辑'开始第一个。"
92
+
93
+ ---
94
+
95
+ ## 操作:强制重扫
96
+
97
+ 用户说"强制重新梳理 XX" / "重扫 XX" / "rescan XX":
98
+
99
+ 1. 读现有文档的 `scan_keywords`
100
+ 2. 用这些关键词重新 grep,允许找到新文件(扩大 sources)
101
+ 3. 走"创建新文档"完整流程,覆盖原文件
102
+ 4. 更新 `sources`(可能扩大)和 `last_commit` 和 `updated`
103
+ 5. 更新 INDEX.md 对应行
104
+
105
+ ---
106
+
107
+ ## 写作规范(产出文档质量底线)
108
+
109
+ ### 通用
110
+ - 面向**非 IT 读者**。避免行话(如 channel、pub-sub、middleware、callback)。必须用专业词时,后面括号补一句白话解释。
111
+ - 时间统一 `YYYY-MM-DD`,不要 `2026/5/17` 或 `5月17日`。
112
+
113
+ ### 1. 概述 (1 段, 3-5 句)
114
+ 讲清楚"这段代码是做什么的、解决什么问题、什么时候被触发"。
115
+
116
+ ### 2. 实现逻辑
117
+
118
+ #### 流程图 (Mermaid `flowchart`,**只用 flowchart,禁止 sequenceDiagram**)
119
+ - 节点用中文标签
120
+ - 决策点用 `{}` 菱形
121
+ - 主路径 + 异常路径都画
122
+ - 节点数 5-15 个;超 15 个考虑拆子流程
123
+
124
+ #### 关键步骤 (3-7 条有序列表)
125
+ 每条:**步骤名**: 一句话解释**做什么** + 一句话解释**为什么这么做**
126
+
127
+ ### 3. 关键配置 (表格)
128
+ - 列:`配置项 | 默认值 | 说明 | 位置`
129
+ - 位置格式 `` `相对路径:行号` ``
130
+ - **只列有人会改的配置**(超时、TTL、批大小、开关、阈值、Cron 表达式),硬编码常量不列
131
+
132
+ ### 4. 依赖关系 (bullet)
133
+ - 每个依赖:**名称**: 用途 + 涉及的 key/表/接口
134
+ - **最后一行必须明确写"不依赖什么"**(常见组件:PostgreSQL/Redis/DolphinDB/MinIO/外部 API),避免读者猜测
135
+
136
+ ---
137
+
138
+ ## 文件结构
139
+
140
+ ### Skill 端
141
+ ```
142
+ .claude/skills/dwy-code-flow-docs/
143
+ ├── SKILL.md ← 本文件
144
+ ├── templates/
145
+ │ ├── logic-doc.md ← 单文档模板
146
+ │ └── INDEX.md ← 总目录模板
147
+ └── examples/
148
+ └── 配额计算.md ← 质量样本(参考它的详尽程度)
149
+ ```
150
+
151
+ ### 产出端(用户指定的 root)
152
+ ```
153
+ <root>/
154
+ ├── INDEX.md
155
+ ├── 配额计算.md
156
+ ├── 因子流式查询.md
157
+ └── 用户鉴权.md
158
+ ```
159
+
160
+ ---
161
+
162
+ ## 禁止
163
+
164
+ - 禁止把代码原文大段贴进文档(读者是非 IT 人员)
165
+ - 禁止用 ASCII 流程图(必须用 Mermaid)
166
+ - 禁止时序图 `sequenceDiagram`(只用 `flowchart`)
167
+ - 禁止时间格式不一致
168
+ - 禁止省略 frontmatter(下次更新就丢失增量优化能力)
169
+ - 禁止不更新 INDEX.md(总目录必须和实际文件同步)
170
+ - 禁止跳过 git diff 直接重读所有代码(违反 token 优化原则)
@@ -0,0 +1,64 @@
1
+ ---
2
+ title: 配额计算
3
+ updated: 2026-05-17
4
+ last_commit: a3f8c2d4e5f6a1b2c3d4e5f6a7b8c9d0e1f2a3b4
5
+ sources:
6
+ - quant-cloud/backend/packages/quant-billing/src/quant_billing/quota.py
7
+ - quant-cloud/backend/packages/quant-billing/src/quant_billing/models.py
8
+ - quant-cloud/backend/packages/quant-billing/src/quant_billing/tasks/reset.py
9
+ scan_keywords: [quota, 配额, billing, deduct, plan_quota]
10
+ ---
11
+
12
+ # 配额计算
13
+
14
+ **更新时间**: 2026-05-17
15
+
16
+ ## 1. 概述
17
+
18
+ 用户每次调用数据查询 API(如查因子、查 Tick)时,系统都会先检查这个用户的剩余配额够不够。
19
+ 够 → 扣掉本次消耗,放行业务逻辑;不够 → 返回 429 错误,告诉用户超限。
20
+ 配额按月重置,套餐升级立即生效新配额。整个判断走 Redis 缓存,保证高并发下不卡顿。
21
+
22
+ ## 2. 实现逻辑
23
+
24
+ ### 2.1 流程图
25
+
26
+ ```mermaid
27
+ flowchart TD
28
+ A[用户 API 请求] --> B{Redis 有缓存?}
29
+ B -->|有| C[读取剩余配额]
30
+ B -->|无| D[查 PostgreSQL]
31
+ D --> E[写入 Redis 缓存 5 分钟]
32
+ E --> C
33
+ C --> F{剩余配额 ≥ 本次消耗?}
34
+ F -->|是| G[Redis 原子扣减]
35
+ F -->|否| H[返回 429 配额不足]
36
+ G --> I[异步任务同步到 PostgreSQL]
37
+ I --> J[放行业务逻辑]
38
+ ```
39
+
40
+ ### 2.2 关键步骤
41
+
42
+ 1. **读取配额**: 先查 Redis,5 分钟没人查就过期。为什么用 Redis: 配额查询是高频操作,直接打 DB 扛不住。
43
+ 2. **原子扣减**: 用 Redis 的 `DECRBY` 指令保证并发安全。为什么不用 DB 行锁: Redis 内存操作快几十倍,而且并发请求多时行锁会排队。
44
+ 3. **异步落盘**: Redis 扣减成功后,丢一个异步任务把最新值同步回 PostgreSQL。为什么异步: 接口响应不能等 DB 写完,异步保证用户体验。
45
+ 4. **月度重置**: 每月 1 日 0 点定时任务把所有用户的"已用配额"清零。为什么定时: 套餐周期是按月计的,统一时间点重置便于对账。
46
+ 5. **套餐升级**: 用户升级套餐时,服务会主动清除该用户的 Redis 缓存,下次请求会从 DB 重新拉取新配额上限。
47
+
48
+ ## 3. 关键配置
49
+
50
+ | 配置项 | 默认值 | 说明 | 位置 |
51
+ |--------|--------|------|------|
52
+ | Redis 缓存 TTL | 300 秒 | 配额数据在 Redis 里保留多久 | `quant_billing/quota.py:23` |
53
+ | 月度重置时间 | 每月 1 日 00:00 | Cron 表达式 `0 0 1 * *` | `quant_billing/tasks/reset.py:8` |
54
+ | 套餐配额表名 | `plan_quotas` | PostgreSQL 表,存每个套餐对应的月度配额 | `quant_billing/models.py:45` |
55
+ | 异步落盘队列 | `quota_sync` | ARQ 队列名,负责同步 Redis → PostgreSQL | `quant_billing/worker.py:12` |
56
+ | 超限返回码 | 429 | 配额不足时返回的 HTTP 状态码 | `quant_billing/quota.py:67` |
57
+
58
+ ## 4. 依赖关系
59
+
60
+ - **PostgreSQL**: 读写 `quotas` 表(每个用户的当月已用量)和 `plan_quotas` 表(每个套餐的配额上限)
61
+ - **Redis**: 缓存 + 原子扣减,key 格式 `quant:quota:{user_id}`,TTL 5 分钟
62
+ - **ARQ (异步任务框架)**: `quota_sync` 队列负责把 Redis 的扣减结果异步同步回 PostgreSQL,接口不等待
63
+ - **dwyeapi 框架**: 走统一异常体系,配额不足时抛 `BusinessError(code="QUOTA_EXCEEDED")`,前端按 code 显示提示
64
+ - **不依赖**: DolphinDB、MinIO、外部支付接口
@@ -0,0 +1,7 @@
1
+ # 代码逻辑梳理总目录
2
+
3
+ 最后更新: {{YYYY-MM-DD}}
4
+
5
+ | # | 标题 | 概述 | 更新时间 | 文档 |
6
+ |---|------|------|----------|------|
7
+ | 1 | {{标题}} | {{一句话概述, 30 字内}} | {{YYYY-MM-DD}} | [{{标题}}.md](./{{标题}}.md) |
@@ -0,0 +1,49 @@
1
+ ---
2
+ title: {{逻辑标题}}
3
+ updated: {{YYYY-MM-DD}}
4
+ last_commit: {{git_full_hash}}
5
+ sources:
6
+ - {{relative/path/to/file1.py}}
7
+ - {{relative/path/to/file2.py}}
8
+ scan_keywords: [{{kw1}}, {{kw2}}, {{kw3}}]
9
+ ---
10
+
11
+ # {{逻辑标题}}
12
+
13
+ **更新时间**: {{YYYY-MM-DD}}
14
+
15
+ ## 1. 概述
16
+
17
+ {{1 段, 3-5 句。讲清楚"这段代码是做什么的、解决什么问题、什么时候被触发"。
18
+ 面向非 IT 读者,避免行话。}}
19
+
20
+ ## 2. 实现逻辑
21
+
22
+ ### 2.1 流程图
23
+
24
+ ```mermaid
25
+ flowchart TD
26
+ A[起点] --> B{判断条件}
27
+ B -->|条件1| C[动作1]
28
+ B -->|条件2| D[动作2]
29
+ C --> E[终点]
30
+ D --> E
31
+ ```
32
+
33
+ ### 2.2 关键步骤
34
+
35
+ 1. **步骤名一**: 做什么 / 为什么这么做
36
+ 2. **步骤名二**: 做什么 / 为什么这么做
37
+ 3. **步骤名三**: 做什么 / 为什么这么做
38
+
39
+ ## 3. 关键配置
40
+
41
+ | 配置项 | 默认值 | 说明 | 位置 |
42
+ |--------|--------|------|------|
43
+ | {{配置项名}} | {{默认值}} | {{用途说明}} | `{{相对路径:行号}}` |
44
+
45
+ ## 4. 依赖关系
46
+
47
+ - **{{依赖名}}**: {{用途, 涉及的 key/表/接口}}
48
+ - **{{依赖名}}**: {{用途, 涉及的 key/表/接口}}
49
+ - **不依赖**: {{明确列出常见但不涉及的组件,如 DolphinDB / MinIO / 外部 API}}