create-dwy 0.5.0 → 0.6.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 (156) hide show
  1. package/bin/index.js +1 -1
  2. package/package.json +2 -2
  3. package/src/create.js +61 -37
  4. package/src/sync.js +213 -53
  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} +314 -357
  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} +116 -79
  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} +361 -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/settings.json +20 -0
  31. package/templates/claude-global/skills/dwy-deploy-audit/SKILL.md +481 -0
  32. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-data.md +117 -0
  33. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-network.md +74 -0
  34. package/templates/claude-global/skills/dwy-deploy-audit/references/checks-runtime.md +184 -0
  35. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_base.sh +39 -0
  36. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_capacity.sh +278 -0
  37. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_db.sh +122 -0
  38. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_docker.sh +249 -0
  39. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_env.sh +64 -0
  40. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_https.sh +60 -0
  41. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_logs.sh +209 -0
  42. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_nginx.sh +92 -0
  43. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_resilience.sh +157 -0
  44. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_secrets.sh +327 -0
  45. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_services.sh +77 -0
  46. package/templates/claude-global/skills/dwy-deploy-audit/scripts/check_ssh.sh +56 -0
  47. package/templates/claude-global/skills/dwy-deploy-audit/scripts/run_all.sh +105 -0
  48. package/templates/claude-global/skills/dwy-docker/SKILL.md +187 -0
  49. package/templates/claude-global/skills/dwy-docker/references/ask-templates.md +142 -0
  50. package/templates/claude-global/skills/dwy-docker/references/templates.md +479 -0
  51. package/templates/claude-global/skills/dwy-docker/references/version-rules.md +76 -0
  52. package/templates/claude-global/skills/dwy-docker/scripts/query_dockerhub.py +204 -0
  53. package/templates/claude-global/skills/dwy-docker/scripts/scan_images.sh +185 -0
  54. package/templates/claude-global/skills/dwy-eapi/SKILL.md +433 -0
  55. package/templates/claude-global/skills/dwy-eapi/references/tasks-integration-guide.md +387 -0
  56. package/templates/claude-global/skills/dwy-ekit/SKILL.md +85 -0
  57. package/templates/claude-global/skills/dwy-ekit/references/data.md +135 -0
  58. package/templates/claude-global/skills/dwy-ekit/references/form.md +68 -0
  59. package/templates/claude-global/skills/dwy-ekit/references/request.md +183 -0
  60. package/templates/claude-global/skills/dwy-ekit/references/ui.md +109 -0
  61. package/templates/claude-global/skills/{dwy-frontend-eui → dwy-eui}/SKILL.md +129 -15
  62. package/templates/claude-global/skills/dwy-eui/references/eui-design-guide.md +257 -0
  63. package/templates/claude-global/skills/dwy-eui/references/eui-integration-guide.md +302 -0
  64. package/templates/claude-global/skills/dwy-eui/references/eui-landing-design-guide.md +437 -0
  65. package/templates/claude-global/skills/dwy-fullstack-scaffold/SKILL.md +214 -0
  66. package/templates/claude-global/skills/dwy-mirror-source/SKILL.md +275 -0
  67. package/templates/claude-global/skills/dwy-mirror-source/references/dockerfile-snippets.md +228 -0
  68. package/templates/claude-global/skills/dwy-mirror-source/references/mirror-providers.md +150 -0
  69. package/templates/claude-global/skills/dwy-mirror-source/scripts/__pycache__/check_mirrors.cpython-313.pyc +0 -0
  70. package/templates/claude-global/skills/dwy-mirror-source/scripts/apply_mirrors.py +666 -0
  71. package/templates/claude-global/skills/dwy-mirror-source/scripts/check_mirrors.py +868 -0
  72. package/templates/claude-global/skills/dwy-pentest/SKILL.md +344 -0
  73. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__init__.py +1 -0
  74. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/__init__.cpython-311.pyc +0 -0
  75. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/config.cpython-311.pyc +0 -0
  76. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/executor.cpython-311.pyc +0 -0
  77. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/severity.cpython-311.pyc +0 -0
  78. package/templates/claude-global/skills/dwy-pentest/scripts/lib/__pycache__/validator.cpython-311.pyc +0 -0
  79. package/templates/claude-global/skills/dwy-pentest/scripts/lib/config.py +109 -0
  80. package/templates/claude-global/skills/dwy-pentest/scripts/lib/executor.py +157 -0
  81. package/templates/claude-global/skills/dwy-pentest/scripts/lib/severity.py +96 -0
  82. package/templates/claude-global/skills/dwy-pentest/scripts/lib/validator.py +113 -0
  83. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_auth.py +289 -0
  84. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_recon.py +217 -0
  85. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_report.py +343 -0
  86. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_scan.py +205 -0
  87. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_ssl.py +208 -0
  88. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_vuln.py +205 -0
  89. package/templates/claude-global/skills/dwy-pentest/scripts/pentest_web.py +330 -0
  90. package/templates/claude-global/skills/dwy-pentest/scripts/pyproject.toml +14 -0
  91. package/templates/claude-global/skills/dwy-pentest/scripts/setup_tools.sh +97 -0
  92. package/templates/claude-global/skills/dwy-pentest/scripts/uv.lock +415 -0
  93. package/templates/claude-global/skills/dwy-publish/SKILL.md +135 -0
  94. package/templates/claude-global/skills/dwy-vue-core/SKILL.md +890 -0
  95. package/templates/project/backend/.dwy.hbs +5 -0
  96. package/templates/project/backend/.python-version +1 -0
  97. package/templates/project/backend/CLAUDE.md.hbs +40 -0
  98. package/templates/project/{web/backend → backend}/alembic/script.py.mako +2 -0
  99. package/templates/project/{web/backend → backend}/alembic.ini.hbs +0 -1
  100. package/templates/project/backend/deploy.sh.hbs +18 -0
  101. package/templates/project/{web → backend}/docker-compose.prod.yml.hbs +1 -1
  102. package/templates/project/{web/backend → backend}/pyproject.toml.hbs +4 -5
  103. package/templates/project/{web/backend → backend}/src/app/config.py.hbs +1 -1
  104. package/templates/project/backend/src/app/database.py +10 -0
  105. package/templates/project/backend/src/app/dependencies.py +5 -0
  106. package/templates/project/{web/backend → backend}/src/app/main.py.hbs +3 -3
  107. package/templates/project/backend/src/app/models/base.py +5 -0
  108. package/templates/project/web/CLAUDE.md.hbs +29 -36
  109. package/templates/project/web/{frontend → apps/web}/index.html.hbs +1 -0
  110. package/templates/project/web/{frontend → apps/web}/package.json.hbs +4 -3
  111. package/templates/project/web/apps/web/src/App.vue +9 -0
  112. package/templates/project/web/apps/web/src/api/client.ts +5 -0
  113. package/templates/project/web/apps/web/src/composables/.gitkeep +0 -0
  114. package/templates/project/web/apps/web/src/layouts/DefaultLayout.vue +8 -0
  115. package/templates/project/web/{frontend → apps/web}/src/router/index.ts +1 -1
  116. package/templates/project/web/{frontend → apps/web}/src/stores/auth.ts +4 -2
  117. package/templates/project/web/apps/web/src/style.css +2 -0
  118. package/templates/project/web/apps/web/src/types/router.d.ts +8 -0
  119. package/templates/project/web/apps/web/src/views/HomeView.vue +8 -0
  120. package/templates/project/web/apps/web/tsconfig.app.json +18 -0
  121. package/templates/project/web/apps/web/tsconfig.json +6 -0
  122. package/templates/project/web/{frontend → apps/web}/vite.config.ts.hbs +7 -1
  123. package/templates/project/web/package.json.hbs +16 -0
  124. package/templates/project/web/packages/.gitkeep +0 -0
  125. package/templates/project/web/pnpm-workspace.yaml +3 -0
  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-security.md +0 -144
  129. package/templates/claude-global/skills/dwy-auto-sync/SKILL.md +0 -52
  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,278 @@
1
+ #!/usr/bin/env bash
2
+ # 硬件规格识别 + 当前服务资源配置盘点 (只读)
3
+ #
4
+ # 输出 raw data, 由主 Claude 对照 SKILL.md 4.10 的推荐分级表生成
5
+ # "推荐 vs 当前" 对比报告。
6
+ #
7
+ # 检查范围:
8
+ # - 宿主硬件: CPU 核数 / 总内存 / 根盘容量 / swap
9
+ # - 当前容器: mem_limit / cpus / shm_size / 实际占用
10
+ # - Postgres 启动参数: shared_buffers / effective_cache_size / work_mem 等
11
+ # - Redis 启动参数: maxmemory / maxmemory-policy / appendonly / save
12
+ # - compose 文件中的资源声明 (供 grep 比对)
13
+
14
+ TARGET="$1"; shift
15
+ SSH_OPTS=("$@")
16
+
17
+ ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
18
+ echo "--- 宿主硬件规格 ---"
19
+ CPU_CORES=$(nproc 2>/dev/null || echo "?")
20
+ CPU_MODEL=$(LC_ALL=C lscpu 2>/dev/null | awk -F: '/^Model name/ {gsub(/^[ \t]+/,"",$2); print $2; exit}')
21
+ [[ -z "$CPU_MODEL" ]] && CPU_MODEL=$(awk -F: '/model name/ {gsub(/^[ \t]+/,"",$2); print $2; exit}' /proc/cpuinfo 2>/dev/null)
22
+ MEM_TOTAL_KB=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)
23
+ MEM_AVAIL_KB=$(awk '/MemAvailable/ {print $2}' /proc/meminfo 2>/dev/null)
24
+ SWAP_TOTAL_KB=$(awk '/SwapTotal/ {print $2}' /proc/meminfo 2>/dev/null)
25
+ MEM_TOTAL_MB=$(( ${MEM_TOTAL_KB:-0} / 1024 ))
26
+ MEM_AVAIL_MB=$(( ${MEM_AVAIL_KB:-0} / 1024 ))
27
+ SWAP_TOTAL_MB=$(( ${SWAP_TOTAL_KB:-0} / 1024 ))
28
+
29
+ # 推荐分档(对齐 SKILL.md 4.10 的 2/4/8/16 GB 表)
30
+ if [[ $MEM_TOTAL_MB -ge 14000 ]]; then TIER="16GB"
31
+ elif [[ $MEM_TOTAL_MB -ge 7000 ]]; then TIER="8GB"
32
+ elif [[ $MEM_TOTAL_MB -ge 3500 ]]; then TIER="4GB"
33
+ elif [[ $MEM_TOTAL_MB -ge 1500 ]]; then TIER="2GB"
34
+ else TIER="<2GB"
35
+ fi
36
+
37
+ echo "CPU 核数: $CPU_CORES"
38
+ echo "CPU 型号: ${CPU_MODEL:-unknown}"
39
+ echo "总内存: ${MEM_TOTAL_MB} MB"
40
+ echo "可用内存: ${MEM_AVAIL_MB} MB"
41
+ echo "Swap: ${SWAP_TOTAL_MB} MB"
42
+ echo "推荐分档: ${TIER} (主 Claude 据此查推荐表)"
43
+
44
+ echo ""
45
+ echo "--- 根盘容量 ---"
46
+ df -h / 2>/dev/null
47
+ ROOT_FREE_MB=$(df -BM --output=avail / 2>/dev/null | tail -1 | tr -d ' M')
48
+ echo "根盘可用: ${ROOT_FREE_MB:-?} MB"
49
+
50
+ if ! command -v docker >/dev/null 2>&1; then
51
+ echo ""
52
+ echo "[i] Docker 未安装,跳过容器资源检查"
53
+ exit 0
54
+ fi
55
+
56
+ echo ""
57
+ echo "--- 当前运行容器资源限制 (raw, 0=无限制) ---"
58
+ SUM_MEM_LIMIT=0
59
+ HAS_UNLIMITED_DB=0
60
+ docker ps -q 2>/dev/null | while read c; do
61
+ name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
62
+ image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
63
+ mem=$(docker inspect -f '{{.HostConfig.Memory}}' "$c" 2>/dev/null)
64
+ cpu_q=$(docker inspect -f '{{.HostConfig.CpuQuota}}' "$c" 2>/dev/null)
65
+ cpu_p=$(docker inspect -f '{{.HostConfig.CpuPeriod}}' "$c" 2>/dev/null)
66
+ shm=$(docker inspect -f '{{.HostConfig.ShmSize}}' "$c" 2>/dev/null)
67
+ pids=$(docker inspect -f '{{.HostConfig.PidsLimit}}' "$c" 2>/dev/null)
68
+
69
+ mem_mb=$(( mem / 1024 / 1024 ))
70
+ shm_mb=$(( shm / 1024 / 1024 ))
71
+ if [[ -n "$cpu_q" && "$cpu_q" != "0" && -n "$cpu_p" && "$cpu_p" != "0" ]]; then
72
+ cpus=$(awk -v q="$cpu_q" -v p="$cpu_p" 'BEGIN{printf "%.2f", q/p}')
73
+ else
74
+ cpus="unlimited"
75
+ fi
76
+
77
+ printf " %-32s image=%-40s mem_limit=%s cpus=%s shm=%sm pids=%s\n" \
78
+ "$name" "$image" \
79
+ "$([ "$mem" = "0" ] && echo 'unlimited' || echo "${mem_mb}m")" \
80
+ "$cpus" "$shm_mb" "${pids:-unlimited}"
81
+
82
+ # 关键服务无 mem_limit
83
+ if [[ "$mem" == "0" ]] && echo "$image" | grep -qiE 'redis|postgres|mysql|mariadb|mongo|clickhouse|elasticsearch'; then
84
+ echo " [!!] HIGH: $name (image=$image) 未设 mem_limit, 异常时可能耗尽宿主内存"
85
+ fi
86
+ done
87
+
88
+ echo ""
89
+ echo "--- 容器实际内存占用 (docker stats 单帧) ---"
90
+ timeout 8 docker stats --no-stream --format \
91
+ 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}\t{{.CPUPerc}}' 2>/dev/null \
92
+ || echo "[i] docker stats 超时或不可用"
93
+
94
+ echo ""
95
+ echo "--- Postgres 启动参数 (从 docker inspect Args 提取) ---"
96
+ PG_FOUND=0
97
+ for c in $(docker ps -q 2>/dev/null); do
98
+ image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
99
+ if echo "$image" | grep -qi "postgres"; then
100
+ PG_FOUND=1
101
+ name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
102
+ # Cmd / Args 拼成空格分隔字符串
103
+ cmd_str=$(docker inspect -f '{{range .Config.Cmd}}{{.}} {{end}}' "$c" 2>/dev/null)
104
+ args_str=$(docker inspect -f '{{range .Args}}{{.}} {{end}}' "$c" 2>/dev/null)
105
+ full="$cmd_str $args_str"
106
+
107
+ echo ""
108
+ echo "[$name] image=$image"
109
+ # 从 -c key=value 提取 Postgres 调优参数
110
+ for key in shared_buffers effective_cache_size work_mem maintenance_work_mem max_connections \
111
+ wal_buffers max_wal_size random_page_cost log_min_duration_statement; do
112
+ val=$(echo "$full" | grep -oE "${key}=[^ ]+" | head -1)
113
+ printf " %-30s %s\n" "$key" "${val:-(默认)}"
114
+ done
115
+
116
+ # 从 docker inspect 取 shm_size (Postgres 关键)
117
+ shm=$(docker inspect -f '{{.HostConfig.ShmSize}}' "$c" 2>/dev/null)
118
+ shm_mb=$(( shm / 1024 / 1024 ))
119
+ if [[ "$shm_mb" -lt 128 ]]; then
120
+ echo " [!] MEDIUM: shm_size=${shm_mb}m 偏低 (大查询可能 'no space left on /dev/shm', 推荐 >=256m)"
121
+ else
122
+ echo " [OK] shm_size=${shm_mb}m"
123
+ fi
124
+
125
+ # 校验 shared_buffers 是否过大
126
+ sb_val=$(echo "$full" | grep -oE 'shared_buffers=[^ ]+' | head -1 | cut -d= -f2)
127
+ if [[ -n "$sb_val" && -n "$MEM_TOTAL_MB" ]]; then
128
+ sb_mb=$(echo "$sb_val" | awk '
129
+ /[Gg][Bb]?$/ { sub(/[Gg][Bb]?$/,""); print $0*1024; next }
130
+ /[Mm][Bb]?$/ { sub(/[Mm][Bb]?$/,""); print $0; next }
131
+ /[Kk][Bb]?$/ { sub(/[Kk][Bb]?$/,""); print $0/1024; next }
132
+ { print $0/1024/1024 }
133
+ ')
134
+ sb_mb=${sb_mb%.*}
135
+ if [[ -n "$sb_mb" && "$sb_mb" -gt $((MEM_TOTAL_MB / 2)) ]]; then
136
+ echo " [!!] HIGH: shared_buffers=${sb_val} 已超过宿主总内存 50%, 易触发 OOM"
137
+ fi
138
+ fi
139
+ fi
140
+ done
141
+ [[ "$PG_FOUND" == "0" ]] && echo "[i] 未发现 Postgres 容器"
142
+
143
+ echo ""
144
+ echo "--- Redis 启动参数 (从 docker inspect Args 提取) ---"
145
+ REDIS_FOUND=0
146
+ for c in $(docker ps -q 2>/dev/null); do
147
+ image=$(docker inspect -f '{{.Config.Image}}' "$c" 2>/dev/null)
148
+ if echo "$image" | grep -qi "redis"; then
149
+ REDIS_FOUND=1
150
+ name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
151
+ cmd_str=$(docker inspect -f '{{range .Config.Cmd}}{{.}} {{end}}' "$c" 2>/dev/null)
152
+ args_str=$(docker inspect -f '{{range .Args}}{{.}} {{end}}' "$c" 2>/dev/null)
153
+ full="$cmd_str $args_str"
154
+
155
+ echo ""
156
+ echo "[$name] image=$image"
157
+ # Redis 启动参数格式: --key value
158
+ for key in maxmemory maxmemory-policy appendonly appendfsync save tcp-keepalive timeout; do
159
+ # 把密码字段脱敏
160
+ val=$(echo "$full" | grep -oE -- "--${key} [^ ]+" | head -1 | sed "s/^--${key} //")
161
+ printf " %-25s %s\n" "$key" "${val:-(默认/未设)}"
162
+ done
163
+
164
+ # 是否设了密码 (脱敏只看是否存在)
165
+ if echo "$full" | grep -qE -- '--requirepass [^ ]+'; then
166
+ echo " requirepass <REDACTED, 已设>"
167
+ else
168
+ echo " requirepass (未设, 见 check_db.sh CRITICAL)"
169
+ fi
170
+
171
+ # maxmemory 二元判定
172
+ mm=$(echo "$full" | grep -oE -- '--maxmemory [^ ]+' | head -1 | awk '{print $2}')
173
+ if [[ -z "$mm" || "$mm" == "0" ]]; then
174
+ echo " [!!!] CRITICAL: maxmemory 未设置或为 0, Redis 可能耗尽宿主内存"
175
+ fi
176
+
177
+ # appendonly + 检查 volume 是否挂载
178
+ if ! echo "$full" | grep -qE -- '--appendonly +yes'; then
179
+ echo " [!] MEDIUM: 未启用 AOF, 重启会丢最近 RDB save 间隔内的数据"
180
+ fi
181
+ fi
182
+ done
183
+ [[ "$REDIS_FOUND" == "0" ]] && echo "[i] 未发现 Redis 容器"
184
+
185
+ echo ""
186
+ echo "--- compose 文件中的资源声明 (供人工核对) ---"
187
+ COMPOSE_FILES=$(sudo -n find /home /opt /srv /root -maxdepth 5 \
188
+ \( -name "docker-compose*.yml" -o -name "compose*.yml" \) 2>/dev/null \
189
+ | grep -vE '\.bak|\.old' | head -10)
190
+ [[ -z "$COMPOSE_FILES" ]] && COMPOSE_FILES=$(find /home /opt /srv /root -maxdepth 5 \
191
+ \( -name "docker-compose*.yml" -o -name "compose*.yml" \) 2>/dev/null \
192
+ | grep -vE '\.bak|\.old' | head -10)
193
+
194
+ for f in $COMPOSE_FILES; do
195
+ echo ""
196
+ echo "[$f]"
197
+ grep -nE '^[[:space:]]+(mem_limit|mem_reservation|cpus|shm_size|memory|cpu_quota|--maxmemory|--maxmemory-policy|--appendonly|shared_buffers|effective_cache_size|work_mem)[[:space:]:]' \
198
+ "$f" 2>/dev/null | head -30 \
199
+ || echo " (无资源声明)"
200
+ done
201
+
202
+ echo ""
203
+ echo "--- 日志兜底推荐档 (基于根盘 + 容器规模, 对照 SKILL.md 4.10) ---"
204
+ ROOT_TOTAL_MB=$(df -BM --output=size / 2>/dev/null | tail -1 | tr -d ' M')
205
+ ROOT_TOTAL_GB=$(( ${ROOT_TOTAL_MB:-0} / 1024 ))
206
+ RUNNING_COUNT=$(docker ps -q 2>/dev/null | wc -l | tr -d ' ')
207
+ echo "根盘容量: ${ROOT_TOTAL_GB} GB"
208
+ echo "运行容器数: ${RUNNING_COUNT}"
209
+
210
+ # 推荐档分级
211
+ if [[ $ROOT_TOTAL_GB -lt 50 ]]; then REC_TIER="入门"; REC_SIZE="10m"; REC_FILE="3"
212
+ elif [[ $ROOT_TOTAL_GB -lt 150 ]]; then
213
+ REC_TIER="标准"
214
+ if [[ $RUNNING_COUNT -gt 5 ]]; then REC_SIZE="20m"; REC_FILE="5"
215
+ else REC_SIZE="50m"; REC_FILE="5"; fi
216
+ else REC_TIER="大型"; REC_SIZE="100m"; REC_FILE="5"
217
+ fi
218
+ echo "推荐档: ${REC_TIER}"
219
+ echo "推荐 daemon log-opts: max-size=${REC_SIZE}, max-file=${REC_FILE}, compress=true"
220
+
221
+ # 拿当前 daemon.json log-opts (复用 check_docker.sh 的逻辑)
222
+ DAEMON_JSON=$(sudo -n cat /etc/docker/daemon.json 2>/dev/null \
223
+ || cat /etc/docker/daemon.json 2>/dev/null || echo "")
224
+ CUR_SIZE=$(echo "$DAEMON_JSON" | grep -oE '"max-size"[[:space:]]*:[[:space:]]*"[^"]+"' \
225
+ | head -1 | grep -oE '"[^"]+"$' | tr -d '"')
226
+ CUR_FILE=$(echo "$DAEMON_JSON" | grep -oE '"max-file"[[:space:]]*:[[:space:]]*"[^"]+"' \
227
+ | head -1 | grep -oE '"[^"]+"$' | tr -d '"')
228
+ echo "当前 daemon.json: max-size=${CUR_SIZE:-(未配置)}, max-file=${CUR_FILE:-(未配置)}"
229
+
230
+ # 简单对比标记
231
+ if [[ -z "$CUR_SIZE" ]]; then
232
+ echo "[!!!] CRITICAL: daemon 无 log-opts.max-size, 长跑容器日志会无限增长"
233
+ elif [[ "$CUR_SIZE" == "$REC_SIZE" ]]; then
234
+ echo "[OK] max-size 与推荐档一致"
235
+ else
236
+ echo "[i] 当前 max-size=${CUR_SIZE} 与推荐档 ${REC_SIZE} 不同 (主 Claude 据 SKILL.md 4.10 判定是否偏离 50%)"
237
+ fi
238
+
239
+ # 估算所有容器日志合计 quota = max-size × max-file × 容器数, 看占根盘比
240
+ # 不依赖 bc(部分镜像/最小系统不带), 用 awk 解析单位
241
+ if [[ -n "$CUR_SIZE" ]]; then
242
+ size_mb=$(echo "$CUR_SIZE" | awk '
243
+ BEGIN{IGNORECASE=1}
244
+ /[gG]$/ { sub(/[gG]$/,""); print int($0*1024); exit }
245
+ /[mM]$/ { sub(/[mM]$/,""); print int($0); exit }
246
+ /[kK]$/ { sub(/[kK]$/,""); print int($0/1024); exit }
247
+ { print int($0); exit }
248
+ ')
249
+ file_n=${CUR_FILE:-1}
250
+ total_quota_mb=$(( ${size_mb:-0} * file_n * RUNNING_COUNT ))
251
+ if [[ "$ROOT_TOTAL_MB" -gt 0 ]]; then
252
+ pct=$(( total_quota_mb * 100 / ROOT_TOTAL_MB ))
253
+ echo "所有容器最大日志 quota 合计: ${total_quota_mb} MB (占根盘 ${pct}%, 期望 < 5%)"
254
+ if [[ "$pct" -gt 5 ]]; then
255
+ echo "[!!] HIGH: 日志 quota 合计 ${pct}% 已超根盘 5% 上限"
256
+ fi
257
+ fi
258
+ fi
259
+
260
+ echo ""
261
+ echo "--- 资源占比汇总 (供 Claude 校验 65% 上限规则) ---"
262
+ echo "宿主总内存: ${MEM_TOTAL_MB} MB"
263
+ TOTAL_LIMIT_MB=$(docker ps -q 2>/dev/null \
264
+ | xargs -I{} docker inspect -f '{{.HostConfig.Memory}}' {} 2>/dev/null \
265
+ | awk 'BEGIN{s=0} {s+=$1} END{print int(s/1024/1024)}')
266
+ echo "所有容器 mem_limit 合计: ${TOTAL_LIMIT_MB} MB"
267
+ if [[ "$MEM_TOTAL_MB" -gt 0 ]]; then
268
+ pct=$(( TOTAL_LIMIT_MB * 100 / MEM_TOTAL_MB ))
269
+ echo "占宿主总内存: ${pct}%"
270
+ if [[ "$pct" -gt 75 ]]; then
271
+ echo "[!!] HIGH: 容器硬限合计 > 75% 宿主内存, OS/nginx/监控 留 buffer 不足"
272
+ elif [[ "$pct" -lt 30 ]]; then
273
+ echo "[i] INFO: 容器硬限合计 < 30%, 可能资源利用不足"
274
+ else
275
+ echo "[OK] 容器硬限合计在 30%-75% 区间"
276
+ fi
277
+ fi
278
+ REMOTE
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # 数据库 (PostgreSQL / Redis) 暴露检查
3
+ TARGET="$1"; shift
4
+ SSH_OPTS=("$@")
5
+
6
+ ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
7
+ echo "########## PostgreSQL ##########"
8
+ if ! command -v psql >/dev/null 2>&1 && ! pgrep -f "postgres" >/dev/null 2>&1; then
9
+ echo "[i] PostgreSQL 未安装/未运行,跳过"
10
+ else
11
+ echo "--- 进程 ---"
12
+ pgrep -af postgres | head -5
13
+
14
+ echo ""
15
+ echo "--- 监听端口 (期望: 仅 127.0.0.1 或内网) ---"
16
+ (ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(5432|5433)" || echo "[i] 未发现 5432/5433 监听"
17
+
18
+ echo ""
19
+ echo "--- 配置文件位置 ---"
20
+ PG_CONF=$(sudo -n -u postgres psql -tAc "SHOW config_file" 2>/dev/null || \
21
+ find /etc/postgresql /var/lib/pgsql /var/lib/postgresql -name postgresql.conf 2>/dev/null | head -1)
22
+ PG_HBA=$(sudo -n -u postgres psql -tAc "SHOW hba_file" 2>/dev/null || \
23
+ find /etc/postgresql /var/lib/pgsql /var/lib/postgresql -name pg_hba.conf 2>/dev/null | head -1)
24
+ echo "postgresql.conf: ${PG_CONF:-未找到}"
25
+ echo "pg_hba.conf: ${PG_HBA:-未找到}"
26
+
27
+ echo ""
28
+ echo "--- postgresql.conf 关键项 ---"
29
+ if [[ -r "$PG_CONF" ]]; then
30
+ grep -E "^[[:space:]]*(listen_addresses|port|password_encryption|ssl|log_connections|log_disconnections|log_min_duration_statement)[[:space:]]*=" "$PG_CONF" 2>/dev/null
31
+ else
32
+ sudo -n cat "$PG_CONF" 2>/dev/null | grep -E "^[[:space:]]*(listen_addresses|password_encryption|ssl|log_connections)" || echo "[!] 配置不可读"
33
+ fi
34
+
35
+ echo ""
36
+ echo "--- pg_hba.conf 认证方式 (期望: 无 trust) ---"
37
+ if [[ -r "$PG_HBA" ]]; then
38
+ grep -vE "^[[:space:]]*#|^[[:space:]]*$" "$PG_HBA" 2>/dev/null
39
+ else
40
+ sudo -n cat "$PG_HBA" 2>/dev/null | grep -vE "^[[:space:]]*#|^[[:space:]]*$" || echo "[!] pg_hba.conf 不可读"
41
+ fi
42
+
43
+ echo ""
44
+ echo "--- PG 版本 & 数据库列表 ---"
45
+ sudo -n -u postgres psql -tAc "SELECT version()" 2>/dev/null | head -1 || echo "[i] 无法连接 PG"
46
+ sudo -n -u postgres psql -tAc "SELECT datname FROM pg_database WHERE datistemplate=false" 2>/dev/null || true
47
+ fi
48
+
49
+ echo ""
50
+ echo "########## Redis ##########"
51
+ if ! command -v redis-cli >/dev/null 2>&1 && ! pgrep -f "redis-server" >/dev/null 2>&1; then
52
+ echo "[i] Redis 未安装/未运行,跳过"
53
+ else
54
+ echo "--- 进程 ---"
55
+ pgrep -af redis-server | head -5
56
+
57
+ echo ""
58
+ echo "--- 监听端口 (期望: 仅 127.0.0.1 或内网) ---"
59
+ (ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(6379|6380)" || echo "[i] 未发现 6379/6380 监听"
60
+
61
+ echo ""
62
+ echo "--- redis.conf 位置与关键项 ---"
63
+ REDIS_CONF=$(ps -eo cmd | grep "[r]edis-server" | grep -oE "/[^ ]+\.conf" | head -1)
64
+ REDIS_CONF=${REDIS_CONF:-/etc/redis/redis.conf}
65
+ echo "redis.conf: $REDIS_CONF"
66
+ if [[ -r "$REDIS_CONF" ]]; then
67
+ grep -E "^[[:space:]]*(bind|port|protected-mode|requirepass|rename-command|tls-port|maxmemory|maxmemory-policy)" "$REDIS_CONF" 2>/dev/null \
68
+ | sed -E 's/(requirepass[[:space:]]+).*/\1<REDACTED>/'
69
+ else
70
+ sudo -n grep -E "^[[:space:]]*(bind|port|protected-mode|requirepass|rename-command|tls-port)" "$REDIS_CONF" 2>/dev/null \
71
+ | sed -E 's/(requirepass[[:space:]]+).*/\1<REDACTED>/' || echo "[!] redis.conf 不可读"
72
+ fi
73
+
74
+ echo ""
75
+ echo "--- maxmemory 判定 (运行时取值,优于 conf 文件) ---"
76
+ MAXMEM=$(timeout 3 redis-cli -h 127.0.0.1 CONFIG GET maxmemory 2>/dev/null | tail -1)
77
+ MAXMEM_POLICY=$(timeout 3 redis-cli -h 127.0.0.1 CONFIG GET maxmemory-policy 2>/dev/null | tail -1)
78
+ if [[ -z "$MAXMEM" ]]; then
79
+ echo "[i] redis-cli 无密码连接失败,从 conf 文件读取"
80
+ MAXMEM=$(grep -E "^[[:space:]]*maxmemory[[:space:]]" "$REDIS_CONF" 2>/dev/null | awk '{print $2}' | tail -1)
81
+ MAXMEM_POLICY=$(grep -E "^[[:space:]]*maxmemory-policy[[:space:]]" "$REDIS_CONF" 2>/dev/null | awk '{print $2}' | tail -1)
82
+ fi
83
+ echo "maxmemory: ${MAXMEM:-(未取得)}"
84
+ echo "maxmemory-policy: ${MAXMEM_POLICY:-(未取得)}"
85
+
86
+ TOTAL_MEM=$(awk '/MemTotal/ {print $2}' /proc/meminfo 2>/dev/null)
87
+ if [[ -n "$TOTAL_MEM" ]]; then
88
+ TOTAL_MEM_BYTES=$((TOTAL_MEM * 1024))
89
+ echo "服务器总内存: $((TOTAL_MEM / 1024)) MB ($TOTAL_MEM_BYTES bytes)"
90
+ fi
91
+
92
+ if [[ "$MAXMEM" == "0" || -z "$MAXMEM" ]]; then
93
+ echo "[!!!] HIGH: maxmemory=0(无上限),Redis 可能耗尽服务器内存触发 OOM kill"
94
+ echo " 建议: 设置为服务器物理内存的 50%-70% (CONFIG SET maxmemory <bytes>)"
95
+ else
96
+ echo "[OK] maxmemory 已设置"
97
+ if [[ "$MAXMEM_POLICY" == "noeviction" ]]; then
98
+ echo "[!] INFO: maxmemory-policy=noeviction,达到上限时拒写但不淘汰,业务方需感知"
99
+ fi
100
+ fi
101
+
102
+ echo ""
103
+ echo "--- 尝试无密码连接 (如能成功 → CRITICAL) ---"
104
+ if timeout 3 redis-cli -h 127.0.0.1 ping 2>/dev/null | grep -q PONG; then
105
+ echo "[!!!] CRITICAL: Redis 127.0.0.1 无密码可连!"
106
+ else
107
+ echo "[OK] 无密码连接被拒绝"
108
+ fi
109
+
110
+ echo ""
111
+ echo "--- Redis INFO server (需 auth, 跳过若失败) ---"
112
+ timeout 3 redis-cli -h 127.0.0.1 INFO server 2>/dev/null | grep -E "redis_version|os|arch_bits" | head -5 || echo "[i] 需要密码或无法连接"
113
+ fi
114
+
115
+ echo ""
116
+ echo "########## 公网可达性自测 ##########"
117
+ PUB_IP=$(curl -fsS --max-time 5 https://api.ipify.org 2>/dev/null)
118
+ echo "公网 IP (best effort): ${PUB_IP:-unknown}"
119
+ if [[ -n "$PUB_IP" ]]; then
120
+ echo "[i] 注意: 真实公网可达性需从外网测试,此处仅本机视角"
121
+ fi
122
+ REMOTE
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env bash
2
+ # Docker 安全检查
3
+ TARGET="$1"; shift
4
+ SSH_OPTS=("$@")
5
+
6
+ ssh "${SSH_OPTS[@]}" "${TARGET}" bash -s <<'REMOTE'
7
+ if ! command -v docker >/dev/null 2>&1; then
8
+ echo "[i] Docker 未安装,跳过"
9
+ exit 0
10
+ fi
11
+
12
+ echo "--- docker version ---"
13
+ docker version --format '{{.Server.Version}} (client: {{.Client.Version}})' 2>/dev/null || docker --version
14
+
15
+ echo ""
16
+ echo "--- docker daemon 监听 ---"
17
+ (ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null) | grep -E ":(2375|2376)" \
18
+ && echo "[!!!] CRITICAL: Docker daemon 暴露 TCP 端口" \
19
+ || echo "[OK] Docker daemon 未暴露 TCP 端口"
20
+
21
+ echo ""
22
+ echo "--- 运行中容器列表 ---"
23
+ docker ps --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null
24
+
25
+ echo ""
26
+ echo "--- 容器端口绑定 (期望: 内部服务不暴露 0.0.0.0) ---"
27
+ docker ps --format "{{.Names}}|{{.Ports}}" 2>/dev/null | while IFS='|' read -r name ports; do
28
+ if echo "$ports" | grep -qE "0\.0\.0\.0:(5432|6379|3306|27017|9200|11211)"; then
29
+ echo " [!!!] $name 暴露内部服务端口到 0.0.0.0: $ports"
30
+ else
31
+ echo " [OK] $name $ports"
32
+ fi
33
+ done
34
+
35
+ echo ""
36
+ echo "--- 镜像 tag 分级检查 (生产期望: 固定到具体 patch, 跑 /dwy-docker-image 选 N-1 minor) ---"
37
+ # 浮动 tag 黑名单 (无版本号或仅描述类的)
38
+ FLOATING_TAGS='^(latest|stable|mainline|edge|current|alpine|slim|bookworm|bullseye|buster|jammy|focal|noble|nightly)$'
39
+ docker ps --format "{{.Names}}|{{.Image}}" 2>/dev/null | while IFS='|' read -r name image; do
40
+ # 拆 image 与 tag (注意 image 可能含 registry 域名如 ghcr.io/foo/bar:tag)
41
+ if [[ "$image" == *"@sha256:"* ]]; then
42
+ # tag@digest 形式 → digest 钉死, 最严格
43
+ echo " [OK+] $name digest 钉死: $image"
44
+ continue
45
+ fi
46
+ # 取最后一个冒号后内容当 tag (port 形式 registry:5000/foo:tag 也只取最后)
47
+ base="${image%:*}"
48
+ tag="${image##*:}"
49
+ if [[ "$base" == "$image" ]]; then
50
+ # 没有冒号 → 无 tag (等同 :latest)
51
+ echo " [!!!] CRITICAL $name $image 省略 tag (等同 :latest), 不可重现"
52
+ continue
53
+ fi
54
+ # 提取 tag 起始的语义版本前缀, 支持 "X" / "X.Y" / "X.Y.Z" / "X.Y.Z-suffix"
55
+ ver_prefix=$(echo "$tag" | grep -oE '^[0-9]+(\.[0-9]+){0,3}')
56
+ if [[ -n "$ver_prefix" ]]; then
57
+ seg_count=$(echo "$ver_prefix" | awk -F. '{print NF}')
58
+ else
59
+ seg_count=0
60
+ fi
61
+
62
+ if [[ "$tag" == "latest" ]]; then
63
+ echo " [!!!] CRITICAL $name $image 使用 :latest"
64
+ elif echo "$tag" | grep -qE "$FLOATING_TAGS"; then
65
+ echo " [!!] HIGH $name $image 浮动 tag :$tag (随上游漂移)"
66
+ elif [[ "$seg_count" == 0 ]]; then
67
+ # 非数字开头, 可能是 commit hash / build id / 自定义 tag
68
+ echo " [i] $name $image 非 semver tag :$tag (确认是否 immutable; 如自建镜像走构建 id 视为 OK)"
69
+ elif [[ "$seg_count" == 1 ]]; then
70
+ echo " [!!] HIGH $name $image 仅 major :$tag, 应固定到 minor 或 patch"
71
+ elif [[ "$seg_count" == 2 ]]; then
72
+ # 两段: postgres/mysql/mariadb/alpine/debian/ubuntu 已是 patch; nginx/redis/node 等三段 semver 则需升 patch
73
+ echo " [i] $name $image 两段 :$ver_prefix (postgres/mysql/alpine 视为 patch OK; redis/nginx/node 等需固定到三段)"
74
+ else
75
+ # 三段及以上 (含 -alpine 等后缀)
76
+ echo " [OK] $name $image"
77
+ fi
78
+ done
79
+ echo ""
80
+ echo "[i] 修复:对每条 [!!!] / [!!] / [!] 的镜像,跑 /dwy-docker-image 用 query_dockerhub.py 拿 N-1 minor 推荐版本"
81
+
82
+ echo ""
83
+ echo "--- 容器是否以 root 运行 ---"
84
+ for c in $(docker ps -q 2>/dev/null); do
85
+ name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
86
+ user=$(docker inspect -f '{{.Config.User}}' "$c")
87
+ user="${user:-root}"
88
+ if [[ "$user" == "root" || "$user" == "0" || "$user" == "0:0" ]]; then
89
+ echo " [!] $name 以 root 运行 (User=${user})"
90
+ else
91
+ echo " [OK] $name user=$user"
92
+ fi
93
+ done
94
+
95
+ echo ""
96
+ echo "--- 特权容器检查 ---"
97
+ for c in $(docker ps -q 2>/dev/null); do
98
+ name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
99
+ priv=$(docker inspect -f '{{.HostConfig.Privileged}}' "$c")
100
+ caps=$(docker inspect -f '{{.HostConfig.CapAdd}}' "$c")
101
+ if [[ "$priv" == "true" ]]; then
102
+ echo " [!!!] $name --privileged"
103
+ fi
104
+ if [[ "$caps" != "[]" && "$caps" != "<no value>" ]]; then
105
+ echo " [!] $name 额外 capabilities: $caps"
106
+ fi
107
+ done
108
+
109
+ echo ""
110
+ echo "--- docker.sock 挂载检查 ---"
111
+ for c in $(docker ps -q 2>/dev/null); do
112
+ name=$(docker inspect -f '{{.Name}}' "$c" | sed 's|^/||')
113
+ mounts=$(docker inspect -f '{{range .Mounts}}{{.Source}}->{{.Destination}} {{end}}' "$c")
114
+ if echo "$mounts" | grep -q "docker.sock"; then
115
+ echo " [!!!] $name 挂载了 docker.sock: $mounts"
116
+ fi
117
+ done
118
+
119
+ echo ""
120
+ echo "--- docker daemon 配置 ---"
121
+ DAEMON_JSON=$(sudo -n cat /etc/docker/daemon.json 2>/dev/null || cat /etc/docker/daemon.json 2>/dev/null || echo "")
122
+ if [[ -z "$DAEMON_JSON" ]]; then
123
+ echo "[i] 无 daemon.json 或不可读"
124
+ else
125
+ echo "$DAEMON_JSON"
126
+ fi
127
+
128
+ echo ""
129
+ echo "--- 容器 RestartPolicy (服务器重启后能否自动起来) ---"
130
+ for c in $(docker ps -aq 2>/dev/null); do
131
+ name=$(docker inspect -f '{{.Name}}' "$c" 2>/dev/null | sed 's|^/||')
132
+ policy=$(docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$c" 2>/dev/null)
133
+ state=$(docker inspect -f '{{.State.Status}}' "$c" 2>/dev/null)
134
+ case "$policy" in
135
+ always|unless-stopped)
136
+ echo " [OK] $name state=$state policy=$policy"
137
+ ;;
138
+ on-failure)
139
+ echo " [!] $name state=$state policy=on-failure (手动 stop / OOM 后不会重启)"
140
+ ;;
141
+ no|"")
142
+ [[ "$state" == "running" ]] \
143
+ && echo " [!!!] CRITICAL: $name state=running policy=${policy:-no} (服务器重启后不会自动启动)" \
144
+ || echo " [i] $name state=$state policy=${policy:-no}"
145
+ ;;
146
+ *)
147
+ echo " [?] $name policy=$policy (未知)"
148
+ ;;
149
+ esac
150
+ done
151
+
152
+ echo ""
153
+ echo "--- 日志驱动配置 (防容器日志写满磁盘) ---"
154
+ LOG_DRIVER=$(echo "$DAEMON_JSON" | grep -oE '"log-driver"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
155
+ LOG_MAX_SIZE=$(echo "$DAEMON_JSON" | grep -oE '"max-size"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
156
+ LOG_MAX_FILE=$(echo "$DAEMON_JSON" | grep -oE '"max-file"[[:space:]]*:[[:space:]]*"[^"]+"' | head -1)
157
+ echo "log-driver: ${LOG_DRIVER:-(默认 json-file)}"
158
+ echo "max-size: ${LOG_MAX_SIZE:-(未配置, 单容器日志可无限增长)}"
159
+ echo "max-file: ${LOG_MAX_FILE:-(未配置)}"
160
+ if [[ -z "$LOG_MAX_SIZE" ]]; then
161
+ echo "[!!] HIGH: 未限制单容器日志大小,长跑容器可能写满磁盘"
162
+ echo " 建议: daemon.json 加 \"log-opts\": {\"max-size\": \"100m\", \"max-file\": \"3\"}"
163
+ fi
164
+
165
+ echo ""
166
+ echo "--- docker compose 文件位置 (用于人工核查) ---"
167
+ sudo -n find /home /opt /srv /root -maxdepth 5 \( -name "docker-compose*.yml" -o -name "compose.yml" \) 2>/dev/null | head -10
168
+
169
+ echo ""
170
+ echo "--- 镜像源加速检查 (国内服务器拉境外镜像必装) ---"
171
+ # a. 时区判定 (粗判境内)
172
+ TZ_VAL=$(timedatectl show -p Timezone --value 2>/dev/null || readlink -f /etc/localtime 2>/dev/null | sed 's|.*/zoneinfo/||')
173
+ echo "服务器时区: ${TZ_VAL:-unknown}"
174
+ LIKELY_CN=0
175
+ case "$TZ_VAL" in
176
+ Asia/Shanghai|Asia/Chongqing|Asia/Urumqi|Asia/Harbin|Asia/Hong_Kong|PRC) LIKELY_CN=1 ;;
177
+ esac
178
+ [[ "$LIKELY_CN" == "1" ]] && echo "→ 时区在 PRC, 拉境外 registry 不加速会非常慢" \
179
+ || echo "→ 时区非 PRC, 加速可选"
180
+
181
+ # b. daemon.json registry-mirrors
182
+ echo ""
183
+ MIRRORS=$(echo "$DAEMON_JSON" | grep -oE '"registry-mirrors"[[:space:]]*:[[:space:]]*\[[^]]*\]' | head -1)
184
+ if [[ -n "$MIRRORS" ]]; then
185
+ echo "daemon registry-mirrors:"
186
+ echo " $MIRRORS"
187
+ # 简单识别推荐源
188
+ echo "$MIRRORS" | grep -oE 'https?://[a-zA-Z0-9.-]+' | while read u; do
189
+ case "$u" in
190
+ *daocloud*|*aliyun*|*aliyuncs*|*ustc*|*tsinghua*|*163*|*tencent*|*huaweicloud*)
191
+ echo " [OK] $u (国内加速源)" ;;
192
+ *)
193
+ echo " [i] $u (非已知国内主流源, 确认是否仍可用)" ;;
194
+ esac
195
+ done
196
+ else
197
+ if [[ "$LIKELY_CN" == "1" ]]; then
198
+ echo "[!!] HIGH: daemon.json 未配 registry-mirrors, 国内服务器拉 docker.io 镜像会非常慢"
199
+ else
200
+ echo "[i] daemon.json 未配 registry-mirrors (境外服务器一般不需要)"
201
+ fi
202
+ echo " 修复: 跑 /dwy-mirror-source 自动写入阿里云/中科大/daocloud 镜像源"
203
+ fi
204
+
205
+ # c. 当前运行中容器使用的境外 registry (registry-mirrors 仅对 docker.io 生效)
206
+ echo ""
207
+ echo "--- 运行容器 image registry 来源 (registry-mirrors 仅救 docker.io, 其他要改前缀) ---"
208
+ docker ps --format "{{.Names}}|{{.Image}}" 2>/dev/null | while IFS='|' read -r name image; do
209
+ # 拆 registry: 必须含 "/" 且首段含 "." (域名) 或 ":" (port) 才算独立 registry
210
+ # 否则视为隐式 docker.io (单段名 redis 或 namespace/name 格式 bitnami/redis)
211
+ if [[ "$image" != */* ]]; then
212
+ registry="docker.io"
213
+ else
214
+ first_seg="${image%%/*}"
215
+ if [[ "$first_seg" == *.* || "$first_seg" == *:* ]]; then
216
+ registry="$first_seg"
217
+ else
218
+ registry="docker.io"
219
+ fi
220
+ fi
221
+
222
+ case "$registry" in
223
+ docker.io)
224
+ tag="OK 走 daemon registry-mirrors 加速"
225
+ [[ -z "$MIRRORS" && "$LIKELY_CN" == "1" ]] && tag="[!!] 未配 mirror 国内拉取慢"
226
+ ;;
227
+ gcr.io|ghcr.io|k8s.gcr.io|registry.k8s.io|quay.io|mcr.microsoft.com|nvcr.io|docker.elastic.co)
228
+ if [[ "$LIKELY_CN" == "1" ]]; then
229
+ tag="[!!] HIGH 境外 registry, registry-mirrors 不生效, 必须把 image 名改 *.m.daocloud.io 前缀"
230
+ else
231
+ tag="[i] 境外 registry"
232
+ fi
233
+ ;;
234
+ *daocloud*|*aliyun*|*aliyuncs*|*ustc*)
235
+ tag="[OK] 已用国内镜像前缀"
236
+ ;;
237
+ *)
238
+ tag="[i] 自建/私有 registry"
239
+ ;;
240
+ esac
241
+ printf " %-32s registry=%-32s %s\n" "$name" "$registry" "$tag"
242
+ done
243
+
244
+ echo ""
245
+ echo "[i] 修复路径:"
246
+ echo " 1. docker.io 加速 → /dwy-mirror-source 自动配 daemon registry-mirrors"
247
+ echo " 2. gcr/ghcr/k8s → 把 image 改 <registry>.m.daocloud.io 前缀, 见 dwy-mirror-source 镜像替换表"
248
+ echo " 3. 镜像版本不固定 → /dwy-docker-image 选 N-1 minor 重新固定"
249
+ REMOTE