agentic-flow-kit 0.2.0 → 0.3.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/README.md +4 -2
- package/modules/gates/dotnet-ca/check-architecture.sh +14 -10
- package/modules/hosts/trae/commands/wf-build.md +14 -14
- package/modules/hosts/trae/commands/wf-deploy.md +13 -13
- package/modules/hosts/trae/commands/wf-design.md +13 -13
- package/modules/hosts/trae/commands/wf-maintain.md +13 -13
- package/modules/hosts/trae/commands/wf-new-task.md +13 -13
- package/modules/hosts/trae/commands/wf-plan.md +13 -13
- package/modules/hosts/trae/commands/wf-review.md +13 -13
- package/modules/hosts/trae/commands/wf-test.md +14 -14
- package/modules/hosts/trae/hooks/post-edit-check.cjs +10 -0
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +14 -4
- package/modules/hosts/trae/hooks/resolve-shell.cjs +30 -21
- package/modules/hosts/trae/rules/beautify-input-chinese.md +6 -6
- package/modules/hosts/trae/rules/git-commit-message.md +10 -10
- package/package.json +10 -2
- package/src/add-gate.mjs +8 -1
- package/src/add-host.mjs +4 -0
- package/src/cli.mjs +8 -5
- package/src/doctor.mjs +9 -2
- package/src/init.mjs +96 -12
- package/src/init.test.mjs +56 -0
- package/src/profiles.mjs +8 -0
- package/src/render.mjs +8 -9
- package/src/run-tests.mjs +1 -0
- package/src/sync.mjs +20 -4
- package/src/sync.test.mjs +40 -1
- package/templates/AGENTS.md +3 -2
- package/templates/_agents/board/index.html +449 -382
- package/templates/_agents/board/marked.min.js +6 -6
- package/templates/_agents/cache/kb-index.json +1 -1
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/design.md +1 -1
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +73 -73
- package/templates/_agents/commands/plan.md +2 -2
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +30 -29
- package/templates/_agents/hooks/commit-check.cjs +6 -1
- package/templates/_agents/scripts/agg-delegations.cjs +35 -18
- package/templates/_agents/scripts/agg-delegations.test.mjs +95 -0
- package/templates/_agents/scripts/check-loop.sh +6 -0
- package/templates/_agents/scripts/check-loop.test.sh +1042 -1042
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +94 -0
- package/templates/_agents/scripts/ensure-board.ps1 +1 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +13 -3
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +22 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +7 -2
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +27 -0
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +17 -3
- package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +21 -0
- package/templates/_agents/scripts/kb-search.mjs +2 -1
- package/templates/_agents/scripts/rule-budget.sh +8 -2
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +6 -1
- package/templates/_agents/scripts/verify.mjs +36 -0
- package/templates/_agents/scripts/verify.test.mjs +77 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +245 -244
- package/templates/_githooks/commit-msg +4 -2
- package/templates/_githooks/pre-push +32 -32
- package/templates/wiki/INDEX.md +9 -2
- package/templates/wiki//344/272/247/345/223/201/351/234/200/346/261/202/README.md +3 -0
- package/templates/wiki//345/274/200/345/217/221/346/226/271/346/241/210/README.md +3 -0
- package/templates/wiki//346/225/260/346/215/256/347/273/264/346/212/244/README.md +3 -0
- package/templates/wiki//346/265/213/350/257/225/346/212/245/345/221/212/README.md +3 -0
- package/templates/wiki//347/224/250/346/210/267/345/212/237/350/203/275/README.md +3 -0
- package/templates/wiki//351/241/271/347/233/256/350/247/204/350/214/203/README.md +3 -0
- package/templates/wiki//351/241/271/347/233/256/350/256/241/345/210/222/README.md +3 -0
- package/templates/workflow/README.md +2 -2
- package/templates/workflow/intents/_TEMPLATE.md +40 -40
- package/templates/workflow/specs/_TEMPLATE.md +41 -41
|
@@ -1,382 +1,449 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="zh-CN">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8">
|
|
5
|
-
<
|
|
6
|
-
<
|
|
7
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.
|
|
53
|
-
.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
78
|
-
.
|
|
79
|
-
.
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
.
|
|
85
|
-
.
|
|
86
|
-
.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
{
|
|
124
|
-
{
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if (
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<meta name="color-scheme" content="light">
|
|
7
|
+
<title>Workflow 看板 — AI 执行流程</title>
|
|
8
|
+
<script src="/marked.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
/* ============ 定调:纸墨工业(极致极简 × Industrial) ============
|
|
11
|
+
暖纸底 + 暖墨字;四类文档各一枚同重量低饱和标签色,只出现在小徽标与卡片左缘;
|
|
12
|
+
朱红是唯一强调色,只留给告警 / 老化 / 挂起等必须被看见的状态。
|
|
13
|
+
标题/数字/徽标用本机等宽字体栈(JetBrains Mono→Cascadia→Consolas),正文中文用雅黑;
|
|
14
|
+
零外部字体请求——看板是 127.0.0.1 本地工具,不引网络资源。 */
|
|
15
|
+
:root {
|
|
16
|
+
--bg: #f2f0e9; /* 暖纸 */
|
|
17
|
+
--surface: #f7f5ee; /* 顶栏 / 抽屉面板 */
|
|
18
|
+
--card: #fdfcf8; /* 卡片近白 */
|
|
19
|
+
--card-hover: #ffffff;
|
|
20
|
+
--border: #ddd7c8; /* 发丝线 */
|
|
21
|
+
--border-strong: #b5ad99;
|
|
22
|
+
--text: #26241d; /* 暖墨 */
|
|
23
|
+
--muted: #6c675a;
|
|
24
|
+
--faint: #99937f; /* 仅装饰:滚动条 / 占位符 */
|
|
25
|
+
--accent: #bc4327; /* 朱 — 唯一强调色 */
|
|
26
|
+
--intent: #3f6288; --incident: #a04b35; --spec: #6f5a88; --plan: #3e7157;
|
|
27
|
+
--ok: #3e7157; --warn: #7d6224; --bad: #a04b35;
|
|
28
|
+
--shadow-1: 0 1px 2px rgba(38,36,29,.05), 0 3px 10px rgba(38,36,29,.07);
|
|
29
|
+
--shadow-2: -16px 0 40px rgba(38,36,29,.16);
|
|
30
|
+
--ease: cubic-bezier(.16, 1, .3, 1);
|
|
31
|
+
--mono: "JetBrains Mono", "Cascadia Code", Consolas, "Courier New", monospace;
|
|
32
|
+
}
|
|
33
|
+
* { box-sizing: border-box; margin: 0; }
|
|
34
|
+
::selection { background: rgba(188,67,39,.18); }
|
|
35
|
+
::-webkit-scrollbar { width: 10px; height: 10px; }
|
|
36
|
+
::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 5px; border: 2px solid var(--bg); }
|
|
37
|
+
::-webkit-scrollbar-track { background: transparent; }
|
|
38
|
+
body { background: var(--bg); color: var(--text); font: 14px/1.7 "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif; letter-spacing: .01em; }
|
|
39
|
+
/* 纸张颗粒:5% 噪点叠层让纸面有物理质感;pointer-events 关闭不挡交互 */
|
|
40
|
+
body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-events: none; opacity: .05;
|
|
41
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); }
|
|
42
|
+
header, #board { position: relative; z-index: 1; }
|
|
43
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
44
|
+
|
|
45
|
+
/* ---- 顶栏:墨字标题 + 朱印章方块(记忆点) ---- */
|
|
46
|
+
header { display: flex; align-items: center; gap: 14px; height: 58px; padding: 0 22px; background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
47
|
+
header h1 { font: 700 15px/1 var(--mono); letter-spacing: .02em; }
|
|
48
|
+
header h1::after { content: ""; display: inline-block; width: 7px; height: 7px; background: var(--accent); margin-left: 9px; }
|
|
49
|
+
.chip { font: 500 11px/1 var(--mono); color: var(--muted); background: transparent; border: 1px solid var(--border); border-radius: 3px; padding: 4px 8px; font-variant-numeric: tabular-nums; }
|
|
50
|
+
#sse { display: inline-flex; align-items: center; gap: 6px; font: 11px/1 var(--mono); color: var(--muted); margin-left: auto; }
|
|
51
|
+
#sse i { width: 7px; height: 7px; border-radius: 50%; background: var(--bad); }
|
|
52
|
+
#sse.on i { background: var(--ok); }
|
|
53
|
+
#sse.poll i { background: var(--warn); }
|
|
54
|
+
#q { background: var(--card); border: 1px solid var(--border); color: var(--text); border-radius: 3px; padding: 6px 10px; font: 12px/1.4 var(--mono); width: 200px; transition: border-color .15s ease-out; }
|
|
55
|
+
#q::placeholder { color: var(--faint); }
|
|
56
|
+
#q:focus { outline: none; border-color: var(--border-strong); }
|
|
57
|
+
|
|
58
|
+
/* ---- 看板:去列盒子,发丝竖线分栏的编辑式构图 ---- */
|
|
59
|
+
#board { display: flex; overflow-x: auto; height: calc(100vh - 58px); }
|
|
60
|
+
.col { flex: 1 1 0; min-width: 240px; display: flex; flex-direction: column; border-left: 1px solid var(--border); }
|
|
61
|
+
.col:first-child { border-left: none; }
|
|
62
|
+
.col h2 { display: flex; align-items: baseline; gap: 8px; font: 600 11px/1 var(--mono); letter-spacing: .12em; color: var(--muted); padding: 16px 16px 12px; border-bottom: 1px solid var(--border); }
|
|
63
|
+
.col h2 b { color: var(--text); font-weight: 600; }
|
|
64
|
+
.col .list { flex: 1; overflow-y: auto; padding: 14px 14px 20px; display: flex; flex-direction: column; gap: 10px; }
|
|
65
|
+
|
|
66
|
+
/* ---- 卡片:近白纸片,色彩只出现在左缘 3px 与小徽标 ---- */
|
|
67
|
+
.card { background: var(--card); border: 1px solid var(--border); border-left: 3px solid var(--border-strong); border-radius: 4px; padding: 12px 14px; cursor: pointer; transition: background .15s ease-out, border-color .15s ease-out, transform .15s ease-out, box-shadow .15s ease-out; }
|
|
68
|
+
.card:hover { background: var(--card-hover); border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-1); }
|
|
69
|
+
.row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin-bottom: 7px; }
|
|
70
|
+
.type { font: 700 9.5px/1 var(--mono); letter-spacing: .06em; color: var(--card); border-radius: 2px; padding: 3px 5px; }
|
|
71
|
+
.type.mini { font-size: 9px; padding: 2px 4px; }
|
|
72
|
+
.lv { font: 700 10px/1 var(--mono); border: 1px solid var(--border-strong); color: var(--muted); border-radius: 2px; padding: 2px 4px; }
|
|
73
|
+
.lv.hi { border-color: var(--warn); color: var(--warn); }
|
|
74
|
+
.date { font: 11px/1 var(--mono); color: var(--muted); margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
75
|
+
.acpt { font: 600 10px/1 var(--mono); border: 1px solid var(--border-strong); color: var(--muted); border-radius: 2px; padding: 2px 4px; font-variant-numeric: tabular-nums; }
|
|
76
|
+
.acpt.full { color: var(--ok); border-color: var(--ok); }
|
|
77
|
+
.age { font: 700 10px/1 var(--mono); border-radius: 2px; padding: 2px 4px; }
|
|
78
|
+
.age.a1 { color: var(--warn); border: 1px solid var(--warn); }
|
|
79
|
+
.age.a2 { color: var(--card); background: var(--accent); border: 1px solid var(--accent); }
|
|
80
|
+
.card.age1 { outline: 1px solid var(--warn); outline-offset: -1px; }
|
|
81
|
+
.card.age2 { outline: 1px solid var(--accent); outline-offset: -1px; }
|
|
82
|
+
.alrt { font: 700 11px/1 var(--mono); color: var(--accent); cursor: help; }
|
|
83
|
+
.hold { font: 700 10px/1 var(--mono); color: var(--accent); border: 1px solid var(--accent); border-radius: 2px; padding: 1px 5px; margin-right: 6px; }
|
|
84
|
+
.auth { font: 10.5px/1 var(--mono); color: var(--muted); background: var(--surface); border: 1px solid var(--border); border-radius: 2px; padding: 2px 5px; }
|
|
85
|
+
.card .title { font-size: 13px; line-height: 1.6; }
|
|
86
|
+
.ct { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 6px; vertical-align: 1px; }
|
|
87
|
+
|
|
88
|
+
/* ---- 任务卡展开区:同族文档清单 ---- */
|
|
89
|
+
.docs { display: none; border-top: 1px dashed var(--border); margin-top: 10px; padding-top: 8px; flex-direction: column; gap: 4px; }
|
|
90
|
+
.card.open .docs { display: flex; }
|
|
91
|
+
.doc-item { display: flex; align-items: center; gap: 6px; padding: 6px 7px; border-radius: 3px; cursor: pointer; font-size: 12px; transition: background .15s ease-out; }
|
|
92
|
+
.doc-item:hover { background: var(--bg); }
|
|
93
|
+
.doc-item .doc-title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
94
|
+
.doc-item .chip { font: 500 10px/1 var(--mono); padding: 2px 5px; flex-shrink: 0; }
|
|
95
|
+
.empty { color: var(--muted); font: 11px/1 var(--mono); text-align: center; padding: 28px 0; }
|
|
96
|
+
|
|
97
|
+
/* ---- 抽屉:纸面 transform 右滑入(不触发布局) ---- */
|
|
98
|
+
#drawer { position: fixed; top: 0; right: 0; width: min(760px, 92vw); height: 100vh; background: var(--surface); border-left: 1px solid var(--border-strong); transform: translateX(calc(100% + 24px)); transition: transform .3s var(--ease); display: flex; flex-direction: column; box-shadow: var(--shadow-2); z-index: 10; }
|
|
99
|
+
#drawer.open { transform: none; }
|
|
100
|
+
#d-head { padding: 22px 26px 10px; }
|
|
101
|
+
#d-badges { display: flex; align-items: center; flex-wrap: wrap; gap: 7px; }
|
|
102
|
+
#d-title { font-size: 18px; font-weight: 700; line-height: 1.45; margin: 12px 0; }
|
|
103
|
+
#d-close { float: right; width: 32px; height: 32px; background: none; border: none; border-radius: 3px; color: var(--muted); font-family: inherit; font-size: 18px; line-height: 1; cursor: pointer; transition: background .15s ease-out, color .15s ease-out; }
|
|
104
|
+
#d-close:hover { color: var(--text); background: var(--bg); }
|
|
105
|
+
#d-related { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
106
|
+
#d-related a { font: 500 11px/1 var(--mono); color: var(--text); background: var(--card); border: 1px solid var(--border); border-radius: 3px; padding: 5px 9px; cursor: pointer; text-decoration: none; transition: border-color .15s ease-out, background .15s ease-out; }
|
|
107
|
+
#d-related a:hover { border-color: var(--border-strong); background: var(--card-hover); }
|
|
108
|
+
#d-tabs { display: flex; gap: 4px; padding: 0 26px; border-bottom: 1px solid var(--border); }
|
|
109
|
+
#d-tabs button { position: relative; background: none; border: none; color: var(--muted); font-family: inherit; font-size: 13px; letter-spacing: .02em; padding: 11px 14px; cursor: pointer; }
|
|
110
|
+
#d-tabs button::after { content: ""; position: absolute; left: 14px; right: 14px; bottom: -1px; height: 2px; background: var(--text); transform: scaleX(0); transition: transform .2s var(--ease); }
|
|
111
|
+
#d-tabs button.on { color: var(--text); font-weight: 600; }
|
|
112
|
+
#d-tabs button.on::after { transform: scaleX(1); }
|
|
113
|
+
#d-body { flex: 1; overflow-y: auto; padding: 26px 30px 56px; }
|
|
114
|
+
|
|
115
|
+
/* ---- md 渲染 ---- */
|
|
116
|
+
.md > :first-child { margin-top: 0; }
|
|
117
|
+
.md h1, .md h2, .md h3 { margin: 26px 0 10px; line-height: 1.4; letter-spacing: .01em; } .md h1 { font-size: 18px; } .md h2 { font-size: 15.5px; } .md h3 { font-size: 14px; }
|
|
118
|
+
.md p, .md ul, .md ol { margin: 10px 0; } .md li { margin: 5px 0; }
|
|
119
|
+
.md code { background: var(--bg); border-radius: 2px; padding: 1px 5px; font: 12px/1.4 var(--mono); }
|
|
120
|
+
.md pre { background: var(--card); border: 1px solid var(--border); border-radius: 4px; padding: 14px 16px; overflow-x: auto; margin: 12px 0; } .md pre code { background: none; padding: 0; }
|
|
121
|
+
.md table { border-collapse: collapse; margin: 12px 0; font-size: 12.5px; } .md th, .md td { border: 1px solid var(--border); padding: 6px 12px; } .md th { background: var(--bg); font-weight: 600; }
|
|
122
|
+
.md blockquote { border-left: 3px solid var(--border-strong); color: var(--muted); padding-left: 14px; margin: 12px 0; }
|
|
123
|
+
.md a { color: var(--intent); text-underline-offset: 3px; }
|
|
124
|
+
.md hr { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
|
|
125
|
+
|
|
126
|
+
/* ---- 历史时间线 ---- */
|
|
127
|
+
.tl { position: relative; padding-left: 22px; }
|
|
128
|
+
.tl::before { content: ""; position: absolute; left: 6px; top: 6px; bottom: 6px; width: 1px; background: var(--border); }
|
|
129
|
+
.tl-item { position: relative; padding: 6px 0 16px; }
|
|
130
|
+
.tl-item::before { content: ""; position: absolute; left: -20px; top: 10px; width: 8px; height: 8px; border-radius: 50%; background: var(--surface); border: 2px solid var(--border-strong); }
|
|
131
|
+
.tl-sub { color: var(--text); font-size: 13px; }
|
|
132
|
+
.tl-meta { color: var(--muted); font: 12px/1.6 var(--mono); }
|
|
133
|
+
.tl-meta code { cursor: pointer; background: var(--card); border: 1px solid var(--border); border-radius: 2px; padding: 0 4px; } .tl-meta code:hover { color: var(--text); border-color: var(--border-strong); }
|
|
134
|
+
.none { color: var(--muted); font-size: 13px; }
|
|
135
|
+
|
|
136
|
+
/* ---- toast:墨底纸字,像盖下去的一方印 ---- */
|
|
137
|
+
#toast { position: fixed; top: 70px; right: 22px; background: var(--text); color: var(--bg); border-radius: 3px; padding: 9px 14px; font-size: 12px; z-index: 99; opacity: 0; transform: translateY(-6px); transition: opacity .2s ease-out, transform .2s ease-out; pointer-events: none; box-shadow: var(--shadow-1); }
|
|
138
|
+
#toast.show { opacity: 1; transform: none; }
|
|
139
|
+
|
|
140
|
+
/* ---- 动效:一次编排好的入场(列错峰 60ms),SSE 重渲染不重播 ---- */
|
|
141
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
142
|
+
header { animation: rise .4s var(--ease) both; }
|
|
143
|
+
#board.boot .col { animation: rise .45s var(--ease) both; }
|
|
144
|
+
#board.boot .col:nth-child(2) { animation-delay: 60ms; }
|
|
145
|
+
#board.boot .col:nth-child(3) { animation-delay: 120ms; }
|
|
146
|
+
#board.boot .col:nth-child(4) { animation-delay: 180ms; }
|
|
147
|
+
#board.boot .col:nth-child(5) { animation-delay: 240ms; }
|
|
148
|
+
#board.boot .col:nth-child(6) { animation-delay: 300ms; }
|
|
149
|
+
#sse.on i { animation: breath 2.4s ease-in-out infinite; }
|
|
150
|
+
}
|
|
151
|
+
@keyframes rise { from { opacity: 0; transform: translateY(10px); } }
|
|
152
|
+
@keyframes breath { 50% { opacity: .45; } }
|
|
153
|
+
@media (prefers-reduced-motion: reduce) {
|
|
154
|
+
*, *::before, *::after { transition: none !important; animation: none !important; }
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<header>
|
|
160
|
+
<h1>Workflow 看板</h1>
|
|
161
|
+
<span class="chip" id="c-intents"></span><span class="chip" id="c-incidents"></span>
|
|
162
|
+
<span class="chip" id="c-plans"></span><span class="chip" id="c-specs"></span>
|
|
163
|
+
<span class="chip" id="c-alerts" style="display:none"></span>
|
|
164
|
+
<input id="q" placeholder="过滤 标题 / slug / 文件…" spellcheck="false">
|
|
165
|
+
<span id="sse"><i></i><span>连接断开</span></span>
|
|
166
|
+
</header>
|
|
167
|
+
<div id="board"></div>
|
|
168
|
+
|
|
169
|
+
<div id="drawer">
|
|
170
|
+
<div id="d-head">
|
|
171
|
+
<button id="d-close" title="关闭 (Esc)">×</button>
|
|
172
|
+
<div id="d-badges"></div>
|
|
173
|
+
<div id="d-title"></div>
|
|
174
|
+
<div id="d-related"></div>
|
|
175
|
+
</div>
|
|
176
|
+
<div id="d-tabs">
|
|
177
|
+
<button data-tab="doc" class="on">文档</button>
|
|
178
|
+
<button data-tab="hist">修改历史</button>
|
|
179
|
+
</div>
|
|
180
|
+
<div id="d-body"></div>
|
|
181
|
+
</div>
|
|
182
|
+
<div id="toast"></div>
|
|
183
|
+
|
|
184
|
+
<script>
|
|
185
|
+
const COLS = [
|
|
186
|
+
{ key: 'draft', name: '起草', statuses: ['draft'] },
|
|
187
|
+
{ key: 'active', name: '进行中', statuses: ['approved', 'open'] },
|
|
188
|
+
{ key: 'done', name: '完成', statuses: ['done', 'fixed'] },
|
|
189
|
+
{ key: 'closed', name: '关闭', statuses: ['closed'] },
|
|
190
|
+
{ key: 'gone', name: '废弃', statuses: ['superseded', 'cancelled'] },
|
|
191
|
+
];
|
|
192
|
+
const TYPE_LABEL = { intents: 'INTENT', incidents: 'INCIDENT', plans: 'PLAN', specs: 'SPEC' };
|
|
193
|
+
const TYPE_COLOR = { intents: 'var(--intent)', incidents: 'var(--incident)', plans: 'var(--plan)', specs: 'var(--spec)' };
|
|
194
|
+
|
|
195
|
+
let CARDS = []; // 最近一次 /api/board 的卡片
|
|
196
|
+
let CURRENT = null; // 抽屉当前打开的卡片
|
|
197
|
+
let TAB = 'doc';
|
|
198
|
+
let retryTimer = null; // /api/board 失败后的重试定时器
|
|
199
|
+
let ROOT = ''; // 仓库根(/api/board 返回,内链 vscode 协议用)
|
|
200
|
+
let Q = ''; // 搜索过滤关键字
|
|
201
|
+
|
|
202
|
+
const $ = (s) => document.querySelector(s);
|
|
203
|
+
const esc = (s) => s.replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
204
|
+
// —— 精细化徽标:验收进度 / 配对断裂 ⚠ / 进行中老化 ——
|
|
205
|
+
const ACTIVE_STATUSES = ['draft', 'approved', 'open'];
|
|
206
|
+
const ageClass = (c) => {
|
|
207
|
+
if (!ACTIVE_STATUSES.includes(c.status)) return '';
|
|
208
|
+
const d = new Date(c.date);
|
|
209
|
+
if (isNaN(d)) return '';
|
|
210
|
+
const days = Math.floor((Date.now() - d.getTime()) / 864e5);
|
|
211
|
+
return days >= 14 ? 'age2' : days >= 7 ? 'age1' : '';
|
|
212
|
+
};
|
|
213
|
+
const ageBadge = (c) => {
|
|
214
|
+
const cls = ageClass(c);
|
|
215
|
+
if (!cls) return '';
|
|
216
|
+
const days = Math.floor((Date.now() - new Date(c.date).getTime()) / 864e5);
|
|
217
|
+
return `<span class="age ${cls === 'age2' ? 'a2' : 'a1'}" title="进行中已 ${days} 天">${days}天</span>`;
|
|
218
|
+
};
|
|
219
|
+
const acceptBadge = (c) =>
|
|
220
|
+
!c.accept ? '' :
|
|
221
|
+
`<span class="acpt ${c.accept.done === c.accept.total ? 'full' : ''}" title="验收标准勾验 ${c.accept.done}/${c.accept.total}">☑${c.accept.done}/${c.accept.total}</span>`;
|
|
222
|
+
const alertBadge = (c) =>
|
|
223
|
+
!c.alerts?.length ? '' :
|
|
224
|
+
`<span class="alrt" title="${esc(c.alerts.join(';'))}">⚠${c.alerts.length > 1 ? c.alerts.length : ''}</span>`;
|
|
225
|
+
|
|
226
|
+
const badge = (c) =>
|
|
227
|
+
(TYPE_LABEL[c.type]
|
|
228
|
+
? `<span class="type" style="background:${TYPE_COLOR[c.type]}">${TYPE_LABEL[c.type]}</span>`
|
|
229
|
+
: `<span class="type" style="background:var(--dim)">MD</span>`) +
|
|
230
|
+
(c.level ? `<span class="lv ${['L2','L3'].includes(c.level) ? 'hi' : ''}">${esc(c.level)}</span>` : '') +
|
|
231
|
+
acceptBadge(c) + alertBadge(c) + ageBadge(c) +
|
|
232
|
+
`<span class="date">${esc(c.date)}</span>`;
|
|
233
|
+
|
|
234
|
+
// —— 任务族聚合:同 slug 一卡;head 取入口文档(intent 优先、其次 incident),孤儿用首文档 ——
|
|
235
|
+
const TYPE_ORDER = { intents: 0, incidents: 1, plans: 2, specs: 3 };
|
|
236
|
+
const TYPE_ABBR = { intents: 'IN', incidents: 'IC', plans: 'PL', specs: 'SP' };
|
|
237
|
+
const EXPANDED = new Set(); // 展开中的任务 slug(SSE 刷新后保持)
|
|
238
|
+
const familyOf = (cards) => {
|
|
239
|
+
const bySlug = new Map();
|
|
240
|
+
for (const c of cards) {
|
|
241
|
+
if (!bySlug.has(c.slug)) bySlug.set(c.slug, []);
|
|
242
|
+
bySlug.get(c.slug).push(c);
|
|
243
|
+
}
|
|
244
|
+
const fams = [];
|
|
245
|
+
for (const docs of bySlug.values()) {
|
|
246
|
+
const head = docs.find((d) => d.type === 'intents') || docs.find((d) => d.type === 'incidents') || docs[0];
|
|
247
|
+
docs.sort((a, b) => (TYPE_ORDER[a.type] - TYPE_ORDER[b.type]) || a.date.localeCompare(b.date));
|
|
248
|
+
fams.push({
|
|
249
|
+
slug: docs[0].slug, head, docs,
|
|
250
|
+
alerts: docs.flatMap((d) => d.alerts || []),
|
|
251
|
+
author: head.author || docs.map((d) => d.author).find(Boolean) || '',
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return fams.sort((a, b) => b.head.date.localeCompare(a.head.date));
|
|
255
|
+
};
|
|
256
|
+
const famTypeChips = (fam) =>
|
|
257
|
+
[...new Set(fam.docs.map((d) => d.type))].map((t) =>
|
|
258
|
+
`<span class="type mini" style="background:${TYPE_COLOR[t]}" title="${TYPE_LABEL[t]}">${TYPE_ABBR[t]}</span>`).join('');
|
|
259
|
+
|
|
260
|
+
async function refreshBoard() {
|
|
261
|
+
let data;
|
|
262
|
+
try {
|
|
263
|
+
const resp = await fetch('/api/board');
|
|
264
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
265
|
+
data = await resp.json();
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// 服务不可达:头部提示 + 3s 重试(避免静默保留旧内容而不提示)
|
|
268
|
+
const chip = $('#c-alerts');
|
|
269
|
+
chip.style.display = ''; chip.textContent = '看板服务不可达,重试中'; chip.style.color = 'var(--bad)';
|
|
270
|
+
clearTimeout(retryTimer);
|
|
271
|
+
retryTimer = setTimeout(refreshBoard, 3000);
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
CARDS = data.cards;
|
|
275
|
+
ROOT = data.root || ROOT;
|
|
276
|
+
CURRENT = CARDS.find((c) => c.file === CURRENT?.file) ?? CURRENT; // 重定位抽屉卡片,避免刷新后徽标/状态陈旧
|
|
277
|
+
for (const t of Object.keys(TYPE_LABEL)) $(`#c-${t}`).textContent = `${TYPE_LABEL[t]} ${data.counts[t] ?? 0}`;
|
|
278
|
+
const groups = Object.fromEntries(COLS.map((c) => [c.key, []]));
|
|
279
|
+
const rest = [];
|
|
280
|
+
const q = Q.trim().toLowerCase();
|
|
281
|
+
// 过滤在族级做:任一成员命中即整卡保留;有过滤词时全部展开便于定位命中文档
|
|
282
|
+
const fams = familyOf(CARDS).filter((f) =>
|
|
283
|
+
!q || f.docs.some((d) => (d.title + ' ' + d.name + ' ' + d.file).toLowerCase().includes(q)));
|
|
284
|
+
for (const fam of fams) {
|
|
285
|
+
const col = COLS.find((c) => c.statuses.includes(fam.head.status));
|
|
286
|
+
(col ? groups[col.key] : rest).push(fam);
|
|
287
|
+
}
|
|
288
|
+
const nAlerts = CARDS.filter((c) => c.alerts?.length).length;
|
|
289
|
+
const ca = $('#c-alerts');
|
|
290
|
+
ca.style.display = nAlerts ? '' : 'none';
|
|
291
|
+
if (nAlerts) { ca.textContent = `⚠ 断档 ${nAlerts}`; ca.style.color = 'var(--bad)'; }
|
|
292
|
+
const cardHtml = (fam, extra = '') => {
|
|
293
|
+
const h = fam.head;
|
|
294
|
+
return `<div class="card ${ageClass(h)}${EXPANDED.has(fam.slug) || q ? ' open' : ''}" data-slug="${esc(fam.slug)}" style="border-left-color:${TYPE_COLOR[h.type]}">` +
|
|
295
|
+
`<div class="row">${famTypeChips(fam)}` +
|
|
296
|
+
(h.level ? `<span class="lv ${['L2','L3'].includes(h.level) ? 'hi' : ''}">${esc(h.level)}</span>` : '') +
|
|
297
|
+
acceptBadge(h) + alertBadge(fam) + ageBadge(h) + extra +
|
|
298
|
+
(fam.author ? `<span class="auth" title="作者(git 最早提交)">${esc(fam.author)}</span>` : '') +
|
|
299
|
+
`<span class="date">${esc(h.date)}</span></div>` +
|
|
300
|
+
`<div class="title">${h.status === 'open' ? '<span class="hold">挂起</span>' : ''}${esc(h.title)}</div>` +
|
|
301
|
+
`<div class="docs">${fam.docs.map((d) =>
|
|
302
|
+
`<div class="doc-item" data-file="${esc(d.file)}" title="打开文档">` +
|
|
303
|
+
`<span class="type" style="background:${TYPE_COLOR[d.type]}">${TYPE_LABEL[d.type]}</span>` +
|
|
304
|
+
`<span class="chip">${esc(d.status)}</span>${alertBadge(d)}` +
|
|
305
|
+
`<span class="doc-title">${esc(d.title)}</span></div>`).join('')}</div></div>`;
|
|
306
|
+
};
|
|
307
|
+
let html = COLS.map((c) => {
|
|
308
|
+
const items = groups[c.key].map((fam) => cardHtml(fam)).join('');
|
|
309
|
+
return `<div class="col"><h2><b>${c.name}</b>${groups[c.key].length}</h2><div class="list">${
|
|
310
|
+
items || '<div class="empty">—</div>'}</div></div>`;
|
|
311
|
+
}).join('');
|
|
312
|
+
if (rest.length) {
|
|
313
|
+
html += `<div class="col"><h2><b>其他</b>${rest.length}</h2><div class="list">${rest.map((fam) =>
|
|
314
|
+
cardHtml(fam, `<span class="chip">${esc(fam.head.status)}</span>`)).join('')}</div></div>`;
|
|
315
|
+
}
|
|
316
|
+
$('#board').innerHTML = html;
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderDrawer() {
|
|
321
|
+
if (!CURRENT) return;
|
|
322
|
+
$('#d-badges').innerHTML = badge(CURRENT) +
|
|
323
|
+
(CURRENT.author ? ` <span class="chip" title="git 最早提交">✎ ${esc(CURRENT.author)}</span>` : '') +
|
|
324
|
+
` <span class="chip">${esc(CURRENT.status)}</span>`;
|
|
325
|
+
$('#d-title').textContent = CURRENT.title;
|
|
326
|
+
const related = CARDS.filter((c) => c.slug === CURRENT.slug && c.file !== CURRENT.file);
|
|
327
|
+
$('#d-related').innerHTML = related.map((c) =>
|
|
328
|
+
`<a data-file="${esc(c.file)}">${TYPE_LABEL[c.type]}${c.status !== CURRENT.status ? ' · ' + esc(c.status) : ''}</a>`).join('');
|
|
329
|
+
document.querySelectorAll('#d-tabs button').forEach((b) => b.classList.toggle('on', b.dataset.tab === TAB));
|
|
330
|
+
if (TAB === 'doc') loadDoc(); else loadHist();
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function loadDoc() {
|
|
334
|
+
$('#d-body').innerHTML = '<div class="none">加载中…</div>';
|
|
335
|
+
const { text, error } = await (await fetch(`/api/doc?file=${encodeURIComponent(CURRENT.file)}`)).json();
|
|
336
|
+
if (error) { $('#d-body').innerHTML = `<div class="none">${esc(error)}</div>`; return; }
|
|
337
|
+
// 裸 .md 相对路径引用 → 可点链接(workflow 文档互引多为纯文本路径而非 md 链接语法;
|
|
338
|
+
// 已有 ](path) 链接语法因前缀为 ] 不会被重复包裹)
|
|
339
|
+
const linked = text.replace(
|
|
340
|
+
/(^|[\s((【>])((?:\.\.\/|\.\/)?(?:intents|incidents|plans|specs)\/[A-Za-z0-9_\-./]+\.md)/g,
|
|
341
|
+
'$1[$2]($2)'
|
|
342
|
+
);
|
|
343
|
+
$('#d-body').innerHTML = `<div class="md">${marked.parse(linked)}</div>`;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// —— commit 类型色点(Conventional Commits 首词)——
|
|
347
|
+
const CT_COLOR = { feat: 'var(--ok)', fix: 'var(--bad)', docs: 'var(--dim)', test: 'var(--spec)', refactor: 'var(--intent)', chore: 'var(--warn)', style: 'var(--spec)', perf: 'var(--warn)' };
|
|
348
|
+
const ctDot = (subject) => {
|
|
349
|
+
const t = (subject.match(/^(\w+)[(:]/) || [])[1];
|
|
350
|
+
return `<span class="ct" style="background:${CT_COLOR[t] || 'var(--dim)'}" title="${t || 'commit'}"></span>`;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
async function loadHist() {
|
|
354
|
+
$('#d-body').innerHTML = '<div class="none">加载中…</div>';
|
|
355
|
+
const { commits } = await (await fetch(`/api/history?file=${encodeURIComponent(CURRENT.file)}`)).json();
|
|
356
|
+
$('#d-body').innerHTML = commits.length
|
|
357
|
+
? `<div class="tl">${commits.map((c) =>
|
|
358
|
+
`<div class="tl-item"><div class="tl-sub">${ctDot(c.subject)}${esc(c.subject)}</div>` +
|
|
359
|
+
`<div class="tl-meta">${esc(c.date)} · <code class="hash" data-h="${esc(c.hash)}" title="点击复制完整 hash">${esc(c.hash.slice(0, 9))}</code></div></div>`).join('')}</div>`
|
|
360
|
+
: '<div class="none">暂无提交记录(文件尚未 commit;git 历史只包含已提交的修改)</div>';
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function openCard(card) { CURRENT = card; TAB = 'doc'; $('#drawer').classList.add('open'); renderDrawer(); }
|
|
364
|
+
|
|
365
|
+
// —— toast 反馈 ——
|
|
366
|
+
let toastTimer = null;
|
|
367
|
+
function toast(msg) {
|
|
368
|
+
const t = $('#toast');
|
|
369
|
+
t.textContent = msg; t.classList.add('show');
|
|
370
|
+
clearTimeout(toastTimer);
|
|
371
|
+
toastTimer = setTimeout(() => t.classList.remove('show'), 1800);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// —— 抽屉内文档内链:md 相对链接 → 抽屉内打开;外链新窗口;仓库代码路径 → vscode 协议 ——
|
|
375
|
+
function viewFile(rel) {
|
|
376
|
+
const hit = CARDS.find((c) => c.file === rel);
|
|
377
|
+
if (hit) { openCard(hit); return; }
|
|
378
|
+
// 自由文档(不在看板卡范围,如 workflow/README.md、同名以外的引用):伪卡视图
|
|
379
|
+
CURRENT = { file: rel, name: rel, slug: rel, title: rel, type: 'link', status: '', level: '', date: '' };
|
|
380
|
+
TAB = 'doc';
|
|
381
|
+
$('#drawer').classList.add('open');
|
|
382
|
+
renderDrawer();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function onDocLink(e, a) {
|
|
386
|
+
const href = a.getAttribute('href');
|
|
387
|
+
if (!href || href.startsWith('#')) return; // 页内锚点走默认行为
|
|
388
|
+
if (/^https?:/i.test(href)) { e.preventDefault(); window.open(href, '_blank', 'noopener'); return; }
|
|
389
|
+
e.preventDefault();
|
|
390
|
+
const dir = CURRENT.file.includes('/') ? CURRENT.file.replace(/[^/]*$/, '') : '';
|
|
391
|
+
let rel;
|
|
392
|
+
try { rel = decodeURIComponent(new URL(href, 'http://x/' + dir).pathname.slice(1)); } catch { return; }
|
|
393
|
+
if (/\.md$/i.test(rel)) { viewFile(rel); return; }
|
|
394
|
+
if (ROOT && !/^[a-z]+:/i.test(rel)) { window.open(`vscode://file/${ROOT}/${rel}`); toast('已用编辑器打开 ' + rel); }
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
document.addEventListener('click', (e) => {
|
|
398
|
+
const dLink = e.target.closest('#d-body a');
|
|
399
|
+
if (dLink) { onDocLink(e, dLink); return; }
|
|
400
|
+
// 展开区文档条目 → 开抽屉(先于卡片命中,条目在卡内)
|
|
401
|
+
const item = e.target.closest('.doc-item');
|
|
402
|
+
if (item) {
|
|
403
|
+
const hit = CARDS.find((c) => c.file === item.dataset.file);
|
|
404
|
+
if (hit) openCard(hit);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// 卡片点击 → 就地展开/收起该任务
|
|
408
|
+
const card = e.target.closest('.card');
|
|
409
|
+
if (card) {
|
|
410
|
+
const slug = card.dataset.slug;
|
|
411
|
+
if (EXPANDED.has(slug)) EXPANDED.delete(slug); else EXPANDED.add(slug);
|
|
412
|
+
card.classList.toggle('open');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const rel = e.target.closest('#d-related a');
|
|
416
|
+
if (rel) {
|
|
417
|
+
const hit = CARDS.find((c) => c.file === rel.dataset.file);
|
|
418
|
+
if (hit) { CURRENT = hit; renderDrawer(); }
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const hash = e.target.closest('.hash');
|
|
422
|
+
if (hash) navigator.clipboard.writeText(hash.dataset.h)
|
|
423
|
+
.then(() => toast('已复制 ' + hash.dataset.h.slice(0, 9)))
|
|
424
|
+
.catch(() => toast('复制失败'));
|
|
425
|
+
});
|
|
426
|
+
$('#d-close').onclick = () => { $('#drawer').classList.remove('open'); };
|
|
427
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') $('#drawer').classList.remove('open'); });
|
|
428
|
+
document.querySelectorAll('#d-tabs button').forEach((b) => b.onclick = () => { TAB = b.dataset.tab; renderDrawer(); });
|
|
429
|
+
|
|
430
|
+
// SSE:服务端 fs.watch 防抖后推送 changed → 看板与打开中的抽屉自动刷新
|
|
431
|
+
const sse = new EventSource('/api/events');
|
|
432
|
+
sse.addEventListener('changed', () => { refreshBoard().then((ok) => { if (ok) renderDrawer(); }); });
|
|
433
|
+
sse.addEventListener('watchdead', () => { $('#sse').classList.remove('on'); $('#sse').classList.add('poll'); $('#sse span:last-child').textContent = '监听失效 · 60s 轮询'; });
|
|
434
|
+
sse.onopen = () => { $('#sse').classList.add('on'); $('#sse').classList.remove('poll'); $('#sse span:last-child').textContent = '实时'; };
|
|
435
|
+
sse.onerror = () => { $('#sse').classList.remove('on'); $('#sse span:last-child').textContent = '连接断开,自动重连中'; };
|
|
436
|
+
|
|
437
|
+
// 60s 兜底轮询:SSE/watch 任何环节失效时数据仍保鲜(正常时多拉一次无害)
|
|
438
|
+
setInterval(() => refreshBoard(), 60000);
|
|
439
|
+
|
|
440
|
+
// 搜索过滤:本地过滤标题/slug/文件,SSE 刷新时保持关键字
|
|
441
|
+
$('#q').addEventListener('input', (e) => { Q = e.target.value; refreshBoard(); });
|
|
442
|
+
|
|
443
|
+
// 首次入场:列错峰上浮一次(.boot 稍后摘除,SSE / 过滤触发的重渲染不再重播)
|
|
444
|
+
$('#board').classList.add('boot');
|
|
445
|
+
refreshBoard();
|
|
446
|
+
setTimeout(() => $('#board').classList.remove('boot'), 1500);
|
|
447
|
+
</script>
|
|
448
|
+
</body>
|
|
449
|
+
</html>
|