@nathan33/nao-skill 0.1.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.
- package/.agents/checklists/architecture-designer.md +42 -0
- package/.agents/checklists/pm.md +64 -0
- package/.agents/checklists/qa.md +36 -0
- package/.agents/checklists/rd-be.md +36 -0
- package/.agents/checklists/rd-fe.md +63 -0
- package/.agents/common/intercom-protocol.md +125 -0
- package/.agents/common/output-format.md +51 -0
- package/.agents/prompts/architecture-designer.md +102 -0
- package/.agents/prompts/backend-developer.md +72 -0
- package/.agents/prompts/frontend-developer.md +63 -0
- package/.agents/prompts/product-manager.md +147 -0
- package/.agents/prompts/test-engineer.md +79 -0
- package/.agents/roles.yaml +37 -0
- package/.agents/scripts/nao-fleet.sh +588 -0
- package/.agents/scripts/ui-tokens-check.sh +51 -0
- package/.agents/skills/arch-patterns.md +44 -0
- package/.agents/skills/backend-ddd-details.md +108 -0
- package/.agents/skills/codegraph.md +52 -0
- package/.agents/skills/commit.md +68 -0
- package/.agents/skills/frontend-ddd-details.md +95 -0
- package/.agents/skills/frontend-design/SKILL.md +73 -0
- package/.agents/skills/pm-grill.md +26 -0
- package/.agents/skills/pm-rice.md +36 -0
- package/.agents/skills/test-design.md +52 -0
- package/.agents/templates/AGENTS.md.example +37 -0
- package/.agents/templates/frontend-ui/tokens.css.example +74 -0
- package/.agents/templates/frontend-ui/ux-playbook.md.example +36 -0
- package/.agents/templates/tasks-state.md.example +34 -0
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/bin/nao-skill.js +142 -0
- package/package.json +34 -0
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# nao-fleet.sh — 按角色一键拉起 pi 会话窗口(nao 团队工具箱)
|
|
4
|
+
#
|
|
5
|
+
# 用法
|
|
6
|
+
# nao-fleet.sh check [--strict] 静态体检:roles.yaml/角色卡/交叉引用/白名单/布局
|
|
7
|
+
# nao-fleet.sh status 角色会话在线状态(权威名单见 intercom list)
|
|
8
|
+
# nao-fleet.sh ensure <别名>[@<repo>] [更多...] 拉起角色窗口(默认工作区=roles.yaml workspace)
|
|
9
|
+
# nao-fleet.sh ensure -m <model> <别名>... 显式指定模型(须命中白名单)
|
|
10
|
+
# nao-fleet.sh ensure --force <别名>... 忽略"已在运行"判重
|
|
11
|
+
#
|
|
12
|
+
# 角色别名 → 角色卡:见 .agents/roles.yaml(单一事实来源)
|
|
13
|
+
# 当前:pm / arch-designer(arch) / rd-fe / rd-be / qa
|
|
14
|
+
#
|
|
15
|
+
# 环境变量
|
|
16
|
+
# NAO_TERMINAL=ghostty|ptyxis|tmux|screen 强制宿主
|
|
17
|
+
# NAO_TMUX_LAYOUT=main-row2|grid tmux 布局(默认 main-row2)
|
|
18
|
+
# NAO_TMUX_MAIN_WIDTH=<10..90> main-row2 主 pane 宽度百分比(默认 50)
|
|
19
|
+
# NAO_SKILLS=<dir> 角色卡根目录(默认 <脚本>/../..)
|
|
20
|
+
# NAO_MODEL_WHITELIST=<glob,...> -m 白名单(默认空=不校验,支持 glob)
|
|
21
|
+
#
|
|
22
|
+
# tmux 宿主行为
|
|
23
|
+
# - 已在 tmux 内($TMUX 存在):当前窗口分屏拉起,不新建窗口。
|
|
24
|
+
# - 不在 tmux 内:创建 detached 会话 nao-<角色>,需 tmux attach -t nao-<角色>。
|
|
25
|
+
# - main-row2:第 1 个 pane 全高占左,后续每角色往右开列、每列上下 2 个:
|
|
26
|
+
# 1 | 2 | 4
|
|
27
|
+
# 1 | 3 | 5
|
|
28
|
+
# - grid:所有 pane 等大网格(tmux 内建 tiled)。
|
|
29
|
+
# =============================================================================
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
33
|
+
SKILLS_DIR="${NAO_SKILLS:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
34
|
+
PROMPTS_DIR="$SKILLS_DIR/.agents/prompts"
|
|
35
|
+
COMMON_DIR="$SKILLS_DIR/.agents/common"
|
|
36
|
+
SKILLS_SUB="$SKILLS_DIR/.agents/skills"
|
|
37
|
+
|
|
38
|
+
MANIFEST="$SKILLS_DIR/.agents/roles.yaml"
|
|
39
|
+
CARD_MAX_LINES=200
|
|
40
|
+
ROLE_ORDER=()
|
|
41
|
+
declare -A ROLE_CARDS ROLE_WS ALIAS_ROLE
|
|
42
|
+
MODEL_WHITELIST="${NAO_MODEL_WHITELIST:-}"
|
|
43
|
+
TMUX_LAYOUT="${NAO_TMUX_LAYOUT:-main-row2}"
|
|
44
|
+
TMUX_MAIN_WIDTH="${NAO_TMUX_MAIN_WIDTH:-50}"
|
|
45
|
+
|
|
46
|
+
log() { printf '\033[1;32m[fleet]\033[0m %s\n' "$*"; }
|
|
47
|
+
warn() { printf '\033[1;33m[fleet]\033[0m %s\n' "$*" >&2; }
|
|
48
|
+
die() { printf '\033[1;31m[fleet]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
49
|
+
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
# roles.yaml 解析(严格子集:顶层 roles:,2 空格角色条目 / 4 空格字段,禁 Tab)
|
|
52
|
+
# 填充 ROLE_ORDER / ROLE_CARDS / ROLE_WS / ALIAS_ROLE;解析失败即 die
|
|
53
|
+
load_manifest() {
|
|
54
|
+
local f="$MANIFEST" out
|
|
55
|
+
[[ -f "$f" ]] || die "角色清单缺失: $f(请创建 .agents/roles.yaml)"
|
|
56
|
+
out="$(awk -F'\t' '
|
|
57
|
+
function die(msg) { print "ERR: " FNR ": " msg; bad=1 }
|
|
58
|
+
BEGIN { bad=0; in_roles=0; cur=""; n=0 }
|
|
59
|
+
/^[[:space:]]*#/ { next }
|
|
60
|
+
/^[[:space:]]*$/ { next }
|
|
61
|
+
{
|
|
62
|
+
if ($0 ~ /\t/) { die("禁止 Tab 缩进"); next }
|
|
63
|
+
if (!in_roles) {
|
|
64
|
+
if ($0 ~ /^roles:[[:space:]]*$/) { in_roles=1; next }
|
|
65
|
+
die("顶层仅允许 roles:,得到: " $0); next
|
|
66
|
+
}
|
|
67
|
+
if ($0 ~ /^ [^ ][^:]*:[[:space:]]*$/) {
|
|
68
|
+
cur=$0; sub(/^ /, "", cur); sub(/:[[:space:]]*$/, "", cur)
|
|
69
|
+
if (cur !~ /^[a-z0-9][a-z0-9-]*$/) { die("非法角色 id: " cur); next }
|
|
70
|
+
card[cur]=""; ws[cur]="."; na[cur]=0; order[++n]=cur; next
|
|
71
|
+
}
|
|
72
|
+
if (cur != "" && $0 ~ /^ [a-z_]+:[[:space:]]/) {
|
|
73
|
+
key=$0; sub(/^ /, "", key); sub(/:.*/, "", key)
|
|
74
|
+
val=$0; sub(/^ [a-z_]+:[[:space:]]*/, "", val)
|
|
75
|
+
if (key=="card") { card[cur]=val }
|
|
76
|
+
else if (key=="workspace") { ws[cur]=val }
|
|
77
|
+
else if (key=="aliases") {
|
|
78
|
+
if (val !~ /^\[[^]]*\]$/) { die(cur ".aliases 须为 [a, b] 列表: " $0); next }
|
|
79
|
+
gsub(/^\[|\]$/, "", val); m=split(val, arr, /,/)
|
|
80
|
+
for (i=1; i<=m; i++) { gsub(/^[[:space:]]+|[[:space:]]+$/, "", arr[i]); if (arr[i]!="") alias[cur,++na[cur]]=arr[i] }
|
|
81
|
+
if (na[cur]==0) die(cur ".aliases 为空")
|
|
82
|
+
}
|
|
83
|
+
else die(cur " 未知字段: " key)
|
|
84
|
+
next
|
|
85
|
+
}
|
|
86
|
+
die("无法解析: " $0)
|
|
87
|
+
}
|
|
88
|
+
END {
|
|
89
|
+
if (bad) exit 1
|
|
90
|
+
if (!in_roles) { print "ERR: 缺少 roles: 顶层键"; exit 1 }
|
|
91
|
+
for (i=1; i<=n; i++) { c=order[i]; if (card[c]=="") { print "ERR: " c " 缺少 card"; bad=1 } if (na[c]==0) { print "ERR: " c " 缺少 aliases"; bad=1 } }
|
|
92
|
+
if (bad) exit 1
|
|
93
|
+
for (i=1; i<=n; i++) { c=order[i]; print "ROLE\t" c "\t" card[c] "\t" ws[c]; for (j=1; j<=na[c]; j++) print "ALIAS\t" c "\t" alias[c,j] }
|
|
94
|
+
}
|
|
95
|
+
' "$f")" || { printf '%s\n' "$out" >&2; die "roles.yaml 解析失败: $f"; }
|
|
96
|
+
local k v w rest
|
|
97
|
+
while IFS=$'\t' read -r k v w rest; do
|
|
98
|
+
case "$k" in
|
|
99
|
+
ROLE) ROLE_ORDER+=("$v"); ROLE_CARDS["$v"]="$w"; ROLE_WS["$v"]="$rest" ;;
|
|
100
|
+
ALIAS) ALIAS_ROLE["$v"]="$w" ;;
|
|
101
|
+
esac
|
|
102
|
+
done <<< "$out"
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# 读取卡片 frontmatter 字段(--- 与 --- 之间)
|
|
106
|
+
card_field() {
|
|
107
|
+
awk -v k="$2" '
|
|
108
|
+
/^---$/ { c++; next }
|
|
109
|
+
c == 1 && $0 ~ "^" k ":" { sub("^" k ": *", ""); print; exit }
|
|
110
|
+
' "$1"
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# 校验卡片/公共规范/清单中的 @.agents/... 引用均存在(含 checklists/ 子目录;跨仓库会话能解析的关键保障)
|
|
114
|
+
check_cross_refs() {
|
|
115
|
+
local rc=0 src ref
|
|
116
|
+
local -a srcs=()
|
|
117
|
+
shopt -s nullglob
|
|
118
|
+
srcs+=("$PROMPTS_DIR"/*.md "$COMMON_DIR"/*.md "$SKILLS_SUB"/*.md "$SKILLS_DIR"/.agents/checklists/*.md)
|
|
119
|
+
shopt -u nullglob
|
|
120
|
+
for src in "${srcs[@]}"; do
|
|
121
|
+
while IFS= read -r ref; do
|
|
122
|
+
[[ -z "$ref" ]] && continue
|
|
123
|
+
case "$ref" in
|
|
124
|
+
common/*|skills/*|prompts/*|scripts/*|templates/*|checklists/*) ;;
|
|
125
|
+
*) continue ;;
|
|
126
|
+
esac
|
|
127
|
+
if [[ ! -e "$SKILLS_DIR/.agents/$ref" ]]; then
|
|
128
|
+
printf ' ✗ %s → @.agents/%s 缺失\n' "$(basename "$src")" "$ref"
|
|
129
|
+
rc=1
|
|
130
|
+
fi
|
|
131
|
+
done < <(grep -hoE '@\.agents/(common|skills|prompts|scripts|templates|checklists)/[A-Za-z0-9._/-]+' "$src" | sed 's/^@\.agents\///' | sort -u)
|
|
132
|
+
done
|
|
133
|
+
[[ $rc -eq 0 ]] && echo ' ✓ 全部引用文件存在'
|
|
134
|
+
return $rc
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# CodeGraph 索引健康(ensure 拉起前 / check 用;缺失或过期仅 warn,不阻塞)
|
|
138
|
+
check_codegraph() {
|
|
139
|
+
local repo="$1" name st
|
|
140
|
+
name="$(basename "$repo")"
|
|
141
|
+
command -v codegraph >/dev/null 2>&1 || { warn "$name codegraph 未安装(RD 定位将回退 grep,属预期)"; return 0; }
|
|
142
|
+
if [[ ! -d "$repo/.codegraph" ]]; then
|
|
143
|
+
warn "$name 无 CodeGraph 索引(RD 定位将回退 grep;建议 codegraph init)"
|
|
144
|
+
return 0
|
|
145
|
+
fi
|
|
146
|
+
st="$(cd "$repo" 2>/dev/null && codegraph status 2>&1)"
|
|
147
|
+
if [[ "$st" == *"Index is up to date"* ]]; then
|
|
148
|
+
log "$name CodeGraph 索引 ✓ up to date"
|
|
149
|
+
elif [[ "$st" == *"Pending Changes"* ]]; then
|
|
150
|
+
warn "$name CodeGraph 索引有未同步变更(建议 codegraph sync)"
|
|
151
|
+
elif [[ "$st" == *"Not initialized"* ]]; then
|
|
152
|
+
warn "$name .codegraph 存在但未初始化(建议 codegraph init)"
|
|
153
|
+
else
|
|
154
|
+
warn "$name codegraph status 输出不可解析"
|
|
155
|
+
fi
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------------------
|
|
159
|
+
resolve_role() {
|
|
160
|
+
local id="${ALIAS_ROLE[$1]:-}"
|
|
161
|
+
[[ -n "$id" ]] || die "未知角色: $1(可用: ${ROLE_ORDER[*]})"
|
|
162
|
+
NAME="$id"
|
|
163
|
+
PROMPT="${ROLE_CARDS[$id]}"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
detect_host() {
|
|
167
|
+
if [[ -n "${NAO_TERMINAL:-}" ]]; then echo "$NAO_TERMINAL"; return; fi
|
|
168
|
+
if [[ -n "${TMUX:-}" ]] && command -v tmux >/dev/null 2>&1; then echo tmux; return; fi
|
|
169
|
+
for h in ghostty ptyxis tmux; do
|
|
170
|
+
command -v "$h" >/dev/null 2>&1 && { echo "$h"; return; }
|
|
171
|
+
done
|
|
172
|
+
echo screen
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
running() { pgrep -f -- "pi[[:space:]].*--name $1([[:space:]]|$)" >/dev/null 2>&1; }
|
|
176
|
+
|
|
177
|
+
# 白名单命中返回 0,否则返回 1;未设白名单=放行
|
|
178
|
+
check_model() {
|
|
179
|
+
local m="$1" entry
|
|
180
|
+
local entries=()
|
|
181
|
+
[[ -z "$MODEL_WHITELIST" ]] && return 0
|
|
182
|
+
IFS=',' read -ra entries <<< "$MODEL_WHITELIST"
|
|
183
|
+
for entry in "${entries[@]}"; do
|
|
184
|
+
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
185
|
+
entry="${entry%"${entry##*[![:space:]]}"}"
|
|
186
|
+
[[ -z "$entry" ]] && continue
|
|
187
|
+
# shellcheck disable=SC2053
|
|
188
|
+
[[ "$m" == $entry ]] && return 0
|
|
189
|
+
done
|
|
190
|
+
return 1
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# 16 位 checksum,与 tmux 源码 layout_checksum() 一致
|
|
194
|
+
_layout_checksum() {
|
|
195
|
+
local s="$1" c=0 b
|
|
196
|
+
local -a bytes=()
|
|
197
|
+
# shellcheck disable=SC2207
|
|
198
|
+
bytes=($(printf '%s' "$s" | od -An -v -tu1 | tr -s ' \n' ' '))
|
|
199
|
+
for b in "${bytes[@]}"; do
|
|
200
|
+
c=$(( ((c >> 1) + ((c & 1) << 15) + b) & 0xFFFF ))
|
|
201
|
+
done
|
|
202
|
+
printf '%04x' "$c"
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# main-row2:主 pane 全高占左;往右每列 2 个上下堆叠
|
|
206
|
+
# 1 | 2 | 4
|
|
207
|
+
# 1 | 3 | 5
|
|
208
|
+
# tmux 规范:相邻子 pane 之间 1 格 gap;{} 左右排列,[] 上下排列
|
|
209
|
+
build_main_row2_layout() {
|
|
210
|
+
local win_w="$1" win_h="$2" pct="$3"
|
|
211
|
+
shift 3
|
|
212
|
+
local -a panes=("$@")
|
|
213
|
+
local n=${#panes[@]}
|
|
214
|
+
(( n >= 1 )) || return 1
|
|
215
|
+
if (( n == 1 )); then
|
|
216
|
+
printf '%sx%s,0,0,%s' "$win_w" "$win_h" "${panes[0]}"
|
|
217
|
+
return 0
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# 根:水平分割,主 pane + 右侧容器,中间 1 gap
|
|
221
|
+
local usable_w=$(( win_w - 1 ))
|
|
222
|
+
(( usable_w < 2 )) && usable_w=2
|
|
223
|
+
local main_w=$(( usable_w * pct / 100 ))
|
|
224
|
+
(( main_w < 1 )) && main_w=1
|
|
225
|
+
(( main_w > usable_w - 1 )) && main_w=$(( usable_w - 1 ))
|
|
226
|
+
local right_w=$(( usable_w - main_w ))
|
|
227
|
+
(( right_w < 1 )) && right_w=1
|
|
228
|
+
local right_x=$(( main_w + 1 ))
|
|
229
|
+
|
|
230
|
+
# 右侧 cols 列
|
|
231
|
+
local m=$(( n - 1 ))
|
|
232
|
+
local cols=$(( (m + 1) / 2 ))
|
|
233
|
+
(( cols < 1 )) && cols=1
|
|
234
|
+
local r_usable=$(( right_w - (cols - 1) ))
|
|
235
|
+
(( r_usable < 1 )) && r_usable=1
|
|
236
|
+
local cw_base=$(( r_usable / cols ))
|
|
237
|
+
(( cw_base < 1 )) && cw_base=1
|
|
238
|
+
local cw_last=$(( r_usable - cw_base * (cols - 1) ))
|
|
239
|
+
(( cw_last < 1 )) && cw_last=1
|
|
240
|
+
|
|
241
|
+
# 每列内部:上下 2 个,中间 1 gap
|
|
242
|
+
local v_usable=$(( win_h - 1 ))
|
|
243
|
+
(( v_usable < 2 )) && v_usable=2
|
|
244
|
+
local top_h=$(( v_usable / 2 ))
|
|
245
|
+
(( top_h < 1 )) && top_h=1
|
|
246
|
+
local bot_h=$(( v_usable - top_h ))
|
|
247
|
+
(( bot_h < 1 )) && bot_h=1
|
|
248
|
+
local bot_y=$(( top_h + 1 ))
|
|
249
|
+
|
|
250
|
+
local -a col_parts=()
|
|
251
|
+
local col col_x cw i_top i_bot
|
|
252
|
+
for (( col=0; col<cols; col++ )); do
|
|
253
|
+
col_x=$(( right_x + col * (cw_base + 1) ))
|
|
254
|
+
if (( col == cols - 1 )); then cw=$cw_last; else cw=$cw_base; fi
|
|
255
|
+
i_top=$(( col * 2 + 1 ))
|
|
256
|
+
i_bot=$(( col * 2 + 2 ))
|
|
257
|
+
if (( i_bot < n )); then
|
|
258
|
+
col_parts+=("${cw}x${win_h},${col_x},0[${cw}x${top_h},${col_x},0,${panes[$i_top]},${cw}x${bot_h},${col_x},${bot_y},${panes[$i_bot]}]")
|
|
259
|
+
elif (( i_top < n )); then
|
|
260
|
+
col_parts+=("${cw}x${win_h},${col_x},0,${panes[$i_top]}")
|
|
261
|
+
fi
|
|
262
|
+
done
|
|
263
|
+
|
|
264
|
+
local right_body
|
|
265
|
+
if (( cols == 1 )); then
|
|
266
|
+
right_body="${col_parts[0]}"
|
|
267
|
+
else
|
|
268
|
+
local IFS=','
|
|
269
|
+
right_body="${right_w}x${win_h},${right_x},0{${col_parts[*]}}"
|
|
270
|
+
unset IFS
|
|
271
|
+
fi
|
|
272
|
+
|
|
273
|
+
printf '%sx%s,0,0{%sx%s,0,0,%s,%s}' \
|
|
274
|
+
"$win_w" "$win_h" \
|
|
275
|
+
"$main_w" "$win_h" "${panes[0]}" \
|
|
276
|
+
"$right_body"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
apply_tmux_layout() {
|
|
280
|
+
[[ -n "${TMUX:-}" ]] || return 0
|
|
281
|
+
if [[ "$TMUX_LAYOUT" == "grid" ]]; then
|
|
282
|
+
tmux select-layout tiled >/dev/null 2>&1 || true
|
|
283
|
+
return
|
|
284
|
+
fi
|
|
285
|
+
# main-row2
|
|
286
|
+
local win_w win_h
|
|
287
|
+
read -r win_w win_h < <(tmux display-message -p '#{window_width} #{window_height}' 2>/dev/null)
|
|
288
|
+
[[ -n "$win_w" && -n "$win_h" ]] || return 0
|
|
289
|
+
|
|
290
|
+
local -a pane_ids=()
|
|
291
|
+
local line
|
|
292
|
+
while IFS= read -r line; do
|
|
293
|
+
[[ -n "$line" ]] && pane_ids+=("$line")
|
|
294
|
+
done < <(tmux list-panes -F '#{pane_id}' 2>/dev/null | sed 's/^%//')
|
|
295
|
+
(( ${#pane_ids[@]} >= 1 )) || return 0
|
|
296
|
+
|
|
297
|
+
local pct="$TMUX_MAIN_WIDTH"
|
|
298
|
+
[[ "$pct" =~ ^[0-9]+$ ]] || pct=50
|
|
299
|
+
(( pct < 10 )) && pct=10
|
|
300
|
+
(( pct > 90 )) && pct=90
|
|
301
|
+
|
|
302
|
+
local full; full="$(build_main_row2_layout "$win_w" "$win_h" "$pct" "${pane_ids[@]}")"
|
|
303
|
+
[[ -n "$full" ]] || return 0
|
|
304
|
+
|
|
305
|
+
local ck; ck="$(_layout_checksum "$full")"
|
|
306
|
+
local err
|
|
307
|
+
if ! err=$(tmux select-layout "${ck},${full}" 2>&1); then
|
|
308
|
+
warn "main-row2 布局应用失败,回退 tiled"
|
|
309
|
+
warn " tmux: ${err:-<no message>}"
|
|
310
|
+
warn " layout: $full"
|
|
311
|
+
tmux select-layout tiled >/dev/null 2>&1 || true
|
|
312
|
+
fi
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
spawn_tmux() {
|
|
316
|
+
local name="$1" inner="$2"
|
|
317
|
+
case "$TMUX_LAYOUT" in
|
|
318
|
+
main-row2|grid) ;;
|
|
319
|
+
*) die "NAO_TMUX_LAYOUT 无效: $TMUX_LAYOUT(可选 main-row2|grid)" ;;
|
|
320
|
+
esac
|
|
321
|
+
local wrapped="bash -lc $(printf %q "$inner")"
|
|
322
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
323
|
+
tmux split-window -h "$wrapped" >/dev/null
|
|
324
|
+
apply_tmux_layout
|
|
325
|
+
else
|
|
326
|
+
tmux new-session -d -s "nao-$name" "$wrapped"
|
|
327
|
+
log "tmux 会话 nao-$name 已创建(附加: tmux attach -t nao-$name)"
|
|
328
|
+
fi
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
# 组装系统提示文件:角色卡 + 环境锚点(本机绝对路径,供跨仓库会话解析 @ 引用失败时兜底)
|
|
332
|
+
build_system_prompt() {
|
|
333
|
+
local card="$1" f
|
|
334
|
+
f="$(mktemp "/tmp/nao-fleet-$(basename "$card" .md)-XXXXXX.md")"
|
|
335
|
+
{
|
|
336
|
+
cat "$card"
|
|
337
|
+
printf '\n## 环境锚点\n- NAO_SKILLS=%s(@.agents/... 引用以会话 cwd 解析;cwd 无 .agents 时以 NAO_SKILLS 为根拼接绝对路径)\n' "$SKILLS_DIR"
|
|
338
|
+
} > "$f"
|
|
339
|
+
echo "$f"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
spawn_one() {
|
|
343
|
+
local name="$1" repo="$2" model="$3" card prompt_file host inner
|
|
344
|
+
card="$PROMPTS_DIR/$PROMPT"
|
|
345
|
+
[[ -f "$card" ]] || die "角色卡不存在: $card"
|
|
346
|
+
[[ -d "$repo" ]] || die "工作区不存在: $repo"
|
|
347
|
+
|
|
348
|
+
prompt_file="$(build_system_prompt "$card")"
|
|
349
|
+
inner="cd $(printf %q "$repo") && exec pi --name $(printf %q "$name")"
|
|
350
|
+
[[ -n "$model" ]] && inner+=" --model $(printf %q "$model")"
|
|
351
|
+
inner+=" --append-system-prompt $(printf %q "$prompt_file")"
|
|
352
|
+
( sleep 60; rm -f -- "$prompt_file" ) & disown 2>/dev/null || true
|
|
353
|
+
|
|
354
|
+
host="$(detect_host)"
|
|
355
|
+
case "$host" in
|
|
356
|
+
ghostty) setsid -f ghostty -e bash -lc "$inner" >/dev/null 2>&1 ;;
|
|
357
|
+
ptyxis) setsid -f ptyxis -- bash -lc "$inner" >/dev/null 2>&1 ;;
|
|
358
|
+
tmux) spawn_tmux "$name" "$inner" ;;
|
|
359
|
+
screen) screen -dmS "nao-$name" bash -lc "$inner" ;;
|
|
360
|
+
*) die "不支持的宿主: $host" ;;
|
|
361
|
+
esac
|
|
362
|
+
log "[$host] 已拉起 $name @ $repo${model:+(model=$model)}$([[ "$host" == tmux ]] && echo "(layout=$TMUX_LAYOUT)")"
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
# ---------------------------------------------------------------------------
|
|
366
|
+
cmd_check() {
|
|
367
|
+
local strict="${1:-false}"
|
|
368
|
+
local rc=0 f lines d
|
|
369
|
+
local wl_problems=0
|
|
370
|
+
|
|
371
|
+
echo "== 目录 =="
|
|
372
|
+
for d in "$PROMPTS_DIR" "$COMMON_DIR" "$SKILLS_SUB"; do
|
|
373
|
+
if [[ -d "$d" ]]; then printf ' ✓ %s\n' "$d"
|
|
374
|
+
else printf ' ✗ 缺失: %s\n' "$d"; rc=1; fi
|
|
375
|
+
done
|
|
376
|
+
|
|
377
|
+
echo "== 角色清单 roles.yaml =="
|
|
378
|
+
printf ' ✓ 解析成功,%d 个角色: %s\n' "${#ROLE_ORDER[@]}" "${ROLE_ORDER[*]}"
|
|
379
|
+
|
|
380
|
+
echo "== 常驻角色卡(阈值 ${CARD_MAX_LINES} 行,frontmatter 校验)=="
|
|
381
|
+
local a role version updated
|
|
382
|
+
for a in "${ROLE_ORDER[@]}"; do
|
|
383
|
+
f="$PROMPTS_DIR/${ROLE_CARDS[$a]}"
|
|
384
|
+
if [[ ! -f "$f" ]]; then
|
|
385
|
+
printf ' ✗ %-26s 缺失\n' "${ROLE_CARDS[$a]}"; rc=1; continue
|
|
386
|
+
fi
|
|
387
|
+
lines=$(wc -l < "$f")
|
|
388
|
+
if (( lines > CARD_MAX_LINES )); then
|
|
389
|
+
printf ' ! %-26s %3d 行(超阈值,建议拆到 skills/)\n' "${ROLE_CARDS[$a]}" "$lines"
|
|
390
|
+
else
|
|
391
|
+
printf ' ✓ %-26s %3d 行\n' "${ROLE_CARDS[$a]}" "$lines"
|
|
392
|
+
fi
|
|
393
|
+
role="$(card_field "$f" role)"
|
|
394
|
+
version="$(card_field "$f" version)"
|
|
395
|
+
updated="$(card_field "$f" updated)"
|
|
396
|
+
if [[ "$role" != "$a" ]]; then
|
|
397
|
+
printf ' ✗ %s frontmatter role=%s 与 manifest 角色 %s 不一致\n' "${ROLE_CARDS[$a]}" "${role:-<缺失>}" "$a"; rc=1
|
|
398
|
+
fi
|
|
399
|
+
if [[ ! "$version" =~ ^[0-9]+$ ]]; then
|
|
400
|
+
printf ' ✗ %s frontmatter version=%s 缺失或非整数\n' "${ROLE_CARDS[$a]}" "${version:-<缺失>}"; rc=1
|
|
401
|
+
fi
|
|
402
|
+
if [[ -z "$updated" ]]; then
|
|
403
|
+
printf ' ✗ %s frontmatter updated 缺失(应如 2026-09-21)\n' "${ROLE_CARDS[$a]}"; rc=1
|
|
404
|
+
fi
|
|
405
|
+
done
|
|
406
|
+
|
|
407
|
+
echo "== prompts/ 孤儿检查 =="
|
|
408
|
+
local cf found c2 orphan=0
|
|
409
|
+
shopt -s nullglob
|
|
410
|
+
for cf in "$PROMPTS_DIR"/*.md; do
|
|
411
|
+
found=0
|
|
412
|
+
for c2 in "${ROLE_ORDER[@]}"; do
|
|
413
|
+
[[ "$(basename "$cf")" == "${ROLE_CARDS[$c2]}" ]] && found=1
|
|
414
|
+
done
|
|
415
|
+
if (( ! found )); then printf ' ! %s 未登记于 roles.yaml\n' "$(basename "$cf")"; orphan=1; fi
|
|
416
|
+
done
|
|
417
|
+
shopt -u nullglob
|
|
418
|
+
(( orphan )) || echo ' ✓ 无孤儿卡'
|
|
419
|
+
|
|
420
|
+
echo "== 公共规范 =="
|
|
421
|
+
for f in output-format.md intercom-protocol.md; do
|
|
422
|
+
if [[ -f "$COMMON_DIR/$f" ]]; then printf ' ✓ %s\n' "$f"
|
|
423
|
+
else printf ' ✗ %s 缺失\n' "$f"; rc=1; fi
|
|
424
|
+
done
|
|
425
|
+
|
|
426
|
+
echo "== 按需技能 =="
|
|
427
|
+
if [[ -d "$SKILLS_SUB" ]]; then
|
|
428
|
+
shopt -s nullglob
|
|
429
|
+
for f in "$SKILLS_SUB"/*.md; do printf ' · %s\n' "$(basename "$f")"; done
|
|
430
|
+
shopt -u nullglob
|
|
431
|
+
fi
|
|
432
|
+
|
|
433
|
+
echo "== 交叉引用(@.agents/common|skills|prompts|scripts/... 均须存在)=="
|
|
434
|
+
check_cross_refs || rc=1
|
|
435
|
+
|
|
436
|
+
echo "== CodeGraph 索引(业务 repo 的索引在 ensure 拉起时检查)=="
|
|
437
|
+
if command -v codegraph >/dev/null 2>&1; then
|
|
438
|
+
if [[ -d "$PWD/.codegraph" ]]; then
|
|
439
|
+
local cgst
|
|
440
|
+
cgst="$(codegraph status 2>&1)"
|
|
441
|
+
if [[ "$cgst" == *"up to date"* ]]; then
|
|
442
|
+
echo ' ✓ 当前目录索引 up to date'
|
|
443
|
+
elif [[ "$cgst" == *"Pending Changes"* ]]; then
|
|
444
|
+
echo ' ! 当前目录索引有未同步变更(codegraph sync)'; rc=1
|
|
445
|
+
else
|
|
446
|
+
echo ' · 当前目录有索引,状态见 codegraph status'
|
|
447
|
+
fi
|
|
448
|
+
else
|
|
449
|
+
echo ' · 当前目录无索引(业务 repo 的索引在 ensure 拉起时检查)'
|
|
450
|
+
fi
|
|
451
|
+
else
|
|
452
|
+
echo ' · codegraph 未安装(回退 grep 属预期行为)'
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
echo "== 模型白名单 =="
|
|
456
|
+
if [[ -z "$MODEL_WHITELIST" ]]; then
|
|
457
|
+
echo " (未设置 NAO_MODEL_WHITELIST,-m 不校验)"
|
|
458
|
+
else
|
|
459
|
+
local entry s dup kind tag
|
|
460
|
+
local entries=() seen=()
|
|
461
|
+
IFS=',' read -ra entries <<< "$MODEL_WHITELIST"
|
|
462
|
+
for entry in "${entries[@]}"; do
|
|
463
|
+
entry="${entry#"${entry%%[![:space:]]*}"}"
|
|
464
|
+
entry="${entry%"${entry##*[![:space:]]}"}"
|
|
465
|
+
if [[ -z "$entry" ]]; then
|
|
466
|
+
printf ' ! 空条目被忽略\n'
|
|
467
|
+
wl_problems=$((wl_problems + 1))
|
|
468
|
+
continue
|
|
469
|
+
fi
|
|
470
|
+
dup=0
|
|
471
|
+
for s in "${seen[@]}"; do [[ "$s" == "$entry" ]] && { dup=1; break; }; done
|
|
472
|
+
seen+=("$entry")
|
|
473
|
+
case "$entry" in
|
|
474
|
+
*'*'*|*'?'*|*'['*) kind="glob" ;;
|
|
475
|
+
*) kind="字面量" ;;
|
|
476
|
+
esac
|
|
477
|
+
tag=""
|
|
478
|
+
if (( dup )); then tag=" [重复]"; wl_problems=$((wl_problems + 1)); fi
|
|
479
|
+
if [[ "$entry" == "*" ]]; then tag+=" [过宽!]"; wl_problems=$((wl_problems + 1)); fi
|
|
480
|
+
printf ' · %-32s (%s)%s\n' "$entry" "$kind" "$tag"
|
|
481
|
+
done
|
|
482
|
+
fi
|
|
483
|
+
|
|
484
|
+
echo "== tmux 布局 =="
|
|
485
|
+
if [[ -n "${TMUX:-}" ]]; then
|
|
486
|
+
echo " 当前在 tmux 内(会话 $(tmux display-message -p '#S' 2>/dev/null || echo '?'))"
|
|
487
|
+
else
|
|
488
|
+
echo " (当前不在 tmux 内;若宿主命中 tmux 会创建 detached 会话)"
|
|
489
|
+
fi
|
|
490
|
+
case "$TMUX_LAYOUT" in
|
|
491
|
+
main-row2|grid)
|
|
492
|
+
printf ' ✓ NAO_TMUX_LAYOUT=%-10s 合法\n' "$TMUX_LAYOUT" ;;
|
|
493
|
+
*)
|
|
494
|
+
printf ' ✗ NAO_TMUX_LAYOUT=%-10s 非法(可选 main-row2|grid)\n' "$TMUX_LAYOUT"; rc=1 ;;
|
|
495
|
+
esac
|
|
496
|
+
if [[ "$TMUX_LAYOUT" == "main-row2" ]]; then
|
|
497
|
+
if [[ "$TMUX_MAIN_WIDTH" =~ ^[0-9]+$ ]] && (( TMUX_MAIN_WIDTH >= 10 && TMUX_MAIN_WIDTH <= 90 )); then
|
|
498
|
+
printf ' ✓ NAO_TMUX_MAIN_WIDTH=%-3s%% 合法\n' "$TMUX_MAIN_WIDTH"
|
|
499
|
+
else
|
|
500
|
+
printf ' ! NAO_TMUX_MAIN_WIDTH=%-3s 非法(10..90),运行时会回退 50\n' "$TMUX_MAIN_WIDTH"
|
|
501
|
+
fi
|
|
502
|
+
fi
|
|
503
|
+
|
|
504
|
+
if [[ "$strict" == "true" && $wl_problems -gt 0 ]]; then
|
|
505
|
+
warn "strict 模式:白名单发现 $wl_problems 项问题(空条目/重复/过宽)"
|
|
506
|
+
rc=1
|
|
507
|
+
fi
|
|
508
|
+
|
|
509
|
+
exit $rc
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
# ---------------------------------------------------------------------------
|
|
513
|
+
cmd_status() {
|
|
514
|
+
local a
|
|
515
|
+
echo "== 角色会话在线状态(权威名单以 intercom({action:'list'}) 为准)=="
|
|
516
|
+
for a in "${ROLE_ORDER[@]}"; do
|
|
517
|
+
if running "$a"; then
|
|
518
|
+
printf ' ✓ %-14s 在线(--name %s;卡片 %s)\n' "$a" "$a" "${ROLE_CARDS[$a]}"
|
|
519
|
+
else
|
|
520
|
+
printf ' · %-14s 未运行(ensure 拉起)\n' "$a"
|
|
521
|
+
fi
|
|
522
|
+
done
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
# ---------------------------------------------------------------------------
|
|
526
|
+
cmd_ensure() {
|
|
527
|
+
local force="$1" model="$2"; shift 2
|
|
528
|
+
local spec role repo key seen k
|
|
529
|
+
local -a cg_done=()
|
|
530
|
+
[[ $# -eq 0 ]] && die "ensure 需要至少一个角色,如: nao-fleet.sh ensure arch rd-fe"
|
|
531
|
+
for spec in "$@"; do
|
|
532
|
+
if [[ "$spec" == *"@"* ]]; then
|
|
533
|
+
role="${spec%%@*}"; repo="${spec#*@}"
|
|
534
|
+
else
|
|
535
|
+
role="$spec"; repo=""
|
|
536
|
+
fi
|
|
537
|
+
resolve_role "$role"
|
|
538
|
+
[[ -n "$repo" ]] || repo="${ROLE_WS[$NAME]:-$PWD}"
|
|
539
|
+
# CodeGraph 索引健康(同 repo 只查一次,不阻塞拉起)
|
|
540
|
+
key="$repo"; seen=0
|
|
541
|
+
for k in "${cg_done[@]:-}"; do [[ "$k" == "$key" ]] && seen=1; done
|
|
542
|
+
if (( ! seen )); then check_codegraph "$repo"; cg_done+=("$key"); fi
|
|
543
|
+
if [[ "$force" != "true" ]] && running "$NAME"; then
|
|
544
|
+
warn "$NAME 已在运行(--name 识别),跳过;确需重开请加 --force"
|
|
545
|
+
continue
|
|
546
|
+
fi
|
|
547
|
+
spawn_one "$NAME" "$repo" "$model"
|
|
548
|
+
done
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
# ---------------------------------------------------------------------------
|
|
552
|
+
usage() {
|
|
553
|
+
awk '
|
|
554
|
+
/^# =+$/ { c++; if (c==2) exit; next }
|
|
555
|
+
c==1 && /^#/ { sub(/^# ?/,""); print }
|
|
556
|
+
' "$0"
|
|
557
|
+
exit 0
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
# ---- 入口 ----
|
|
561
|
+
CMD=""; FORCE=false; STRICT=false; MODEL=""; TARGETS=()
|
|
562
|
+
while [[ $# -gt 0 ]]; do
|
|
563
|
+
case "$1" in
|
|
564
|
+
check) CMD="check"; shift ;;
|
|
565
|
+
status) CMD="status"; shift ;;
|
|
566
|
+
ensure) CMD="ensure"; shift ;;
|
|
567
|
+
-m|--model)
|
|
568
|
+
MODEL="${2:-}"
|
|
569
|
+
[[ -n "$MODEL" ]] || die "-m 需要模型参数"
|
|
570
|
+
check_model "$MODEL" || die "模型 '$MODEL' 不在白名单内。允许: $MODEL_WHITELIST(可通过 NAO_MODEL_WHITELIST 覆盖)"
|
|
571
|
+
shift 2 ;;
|
|
572
|
+
--force) FORCE=true; shift ;;
|
|
573
|
+
--strict) STRICT=true; shift ;;
|
|
574
|
+
-h|--help) usage ;;
|
|
575
|
+
*) TARGETS+=("$1"); shift ;;
|
|
576
|
+
esac
|
|
577
|
+
done
|
|
578
|
+
|
|
579
|
+
case "$CMD" in
|
|
580
|
+
check|ensure|status) load_manifest ;;
|
|
581
|
+
esac
|
|
582
|
+
|
|
583
|
+
case "$CMD" in
|
|
584
|
+
check) cmd_check "$STRICT" ;;
|
|
585
|
+
status) cmd_status ;;
|
|
586
|
+
ensure) cmd_ensure "$FORCE" "$MODEL" "${TARGETS[@]}" ;;
|
|
587
|
+
*) usage ;;
|
|
588
|
+
esac
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# =============================================================================
|
|
3
|
+
# ui-tokens-check.sh — 扫描 UI 硬编码颜色值(绕过 Design Tokens)
|
|
4
|
+
#
|
|
5
|
+
# 用法
|
|
6
|
+
# ui-tokens-check.sh [<repo>] [-x <额外排除模式>]
|
|
7
|
+
# 默认 $PWD;退出码 0=通过 1=存在违规
|
|
8
|
+
#
|
|
9
|
+
# 原理
|
|
10
|
+
# UI 视觉应收敛到 Design Tokens(tokens.css 的 --<prefix>-* 变量)。
|
|
11
|
+
# 组件内裸色值(#fff / rgba() / #d4a72c)会破坏换主题/暗色模式并造成
|
|
12
|
+
# 风格漂移。本脚本扫出这些绕过令牌的色值,供交付前自查或接 CI/lint-staged。
|
|
13
|
+
#
|
|
14
|
+
# 排除(默认)
|
|
15
|
+
# node_modules / .codegraph / .d.ts
|
|
16
|
+
# 令牌定义文件:tokens.css / theme.css / variables.css(-x 追加自定义)
|
|
17
|
+
# 任意 CSS 变量定义行(--xxx: <值>)
|
|
18
|
+
#
|
|
19
|
+
# 非代码内容(测试断言里的期望色值)命中后由人/agent 判断是否豁免。
|
|
20
|
+
# =============================================================================
|
|
21
|
+
set -uo pipefail
|
|
22
|
+
|
|
23
|
+
repo="$PWD"; extra=""
|
|
24
|
+
while [[ $# -gt 0 ]]; do
|
|
25
|
+
case "$1" in
|
|
26
|
+
-x) extra="${2:-}"; shift 2 ;;
|
|
27
|
+
*) repo="$1"; shift ;;
|
|
28
|
+
esac
|
|
29
|
+
done
|
|
30
|
+
|
|
31
|
+
cd "$repo" 2>/dev/null || { echo "✗ 目录不存在: $repo" >&2; exit 2; }
|
|
32
|
+
|
|
33
|
+
file_excl="node_modules|\.codegraph|\.d\.ts|tokens\.css|theme\.css|variables\.css"
|
|
34
|
+
[[ -n "$extra" ]] && file_excl+="|$extra"
|
|
35
|
+
|
|
36
|
+
hits="$(grep -rnoE --include='*.vue' --include='*.ts' --include='*.tsx' \
|
|
37
|
+
--include='*.js' --include='*.jsx' --include='*.css' \
|
|
38
|
+
-E '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' . 2>/dev/null \
|
|
39
|
+
| grep -vE "$file_excl" \
|
|
40
|
+
| grep -viE -- 'var\(--|--[a-z0-9-]+[[:space:]]*:[[:space:]]*(#|rgba?\(|hsla?\()' \
|
|
41
|
+
| head -40 || true)"
|
|
42
|
+
|
|
43
|
+
if [[ -n "$hits" ]]; then
|
|
44
|
+
echo "✗ UI 令牌违规($(basename "$repo"))— 以下色值应改为 var(--<prefix>-*) 令牌:"
|
|
45
|
+
echo "$hits"
|
|
46
|
+
if [[ $(grep -c . <<< "$hits") -ge 40 ]]; then echo " …(更多)"; fi
|
|
47
|
+
exit 1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
echo "✓ $(basename "$repo") 无硬编码色值(Design Tokens 一致)"
|
|
51
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 按需技能——架构模式速查 + ADR 模板
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# 架构模式速查
|
|
6
|
+
|
|
7
|
+
| 场景 | 推荐架构 | 关键技术 |
|
|
8
|
+
| :--- | :--- | :--- |
|
|
9
|
+
| 高并发交易 | 微服务 + 事件驱动 | Kubernetes, Kafka, Redis |
|
|
10
|
+
| 大数据处理 | Lambda 架构 | Spark, Flink, S3 |
|
|
11
|
+
| 多租户 SaaS | 多租户 + 模块化 | PostgreSQL schemas, API Gateway |
|
|
12
|
+
| 实时系统 | 事件溯源 + CQRS | Event Store, gRPC |
|
|
13
|
+
|
|
14
|
+
## 最小示例
|
|
15
|
+
|
|
16
|
+
```mermaid
|
|
17
|
+
graph TD
|
|
18
|
+
A[用户请求] --> B[API Gateway]
|
|
19
|
+
B --> C[服务注册发现]
|
|
20
|
+
C --> D[微服务集群]
|
|
21
|
+
D --> E[数据库集群]
|
|
22
|
+
D --> F[缓存集群]
|
|
23
|
+
D --> G[消息队列]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## ADR 模板
|
|
27
|
+
|
|
28
|
+
```md
|
|
29
|
+
# ADR-<编号>: <标题>
|
|
30
|
+
- 日期:YYYY-MM-DD
|
|
31
|
+
- 状态:提议 / 已接受 / 已废弃
|
|
32
|
+
- 背景:<问题与约束>
|
|
33
|
+
- 决策:<选择了什么>
|
|
34
|
+
- 理由:<四步法分析>
|
|
35
|
+
- 备选方案:<列表 + 否决理由>
|
|
36
|
+
- 后果:<正面 / 负面 / 风险>
|
|
37
|
+
- 相关:<PRD / 关联 ADR>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 误区
|
|
41
|
+
|
|
42
|
+
- 过度设计 / 追新 → 务实、成熟稳定优先
|
|
43
|
+
- 忽视 NFR → 性能/安全/可扩展作为核心设计目标
|
|
44
|
+
- 单点设计不考虑演进 → 预留扩展空间
|