visualknowledge 0.2.4 → 0.2.6

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.
@@ -38,7 +38,7 @@ export function HtmlWidget({ html: htmlContent, onFullscreen }) {
38
38
  <iframe
39
39
  ref=${iframeRef}
40
40
  sandbox="allow-scripts allow-same-origin"
41
- style=${{ width: '100%', height: hasContent ? undefined : '0px', border: 'none', display: 'block' }}
41
+ style=${{ width: '100%', height: '0px', border: 'none', display: 'block', overflow: 'hidden' }}
42
42
  />
43
43
  </${WidgetContainer}>
44
44
  `;
@@ -52,10 +52,17 @@ function _writeHtml(iframe, content) {
52
52
  doc.write(content);
53
53
  doc.close();
54
54
 
55
- // 根据实际内容调整高度,随内容增长逐步扩大
55
+ // 紧凑高度:移除 body 默认 margin,只保留最小 padding
56
56
  try {
57
+ doc.body.style.margin = '0';
58
+ doc.body.style.padding = '0';
57
59
  const h = Math.max(doc.body.scrollHeight, doc.documentElement.scrollHeight, 0);
58
- iframe.style.height = Math.min(h + 16, 1200) + 'px';
60
+ // 只在内容更高时才增大,不缩小(避免抖动)
61
+ const current = parseInt(iframe.style.height) || 0;
62
+ const target = Math.min(h + 4, 1200);
63
+ if (target > current || target < current - 20) {
64
+ iframe.style.height = target + 'px';
65
+ }
59
66
  } catch (_) {}
60
67
  } catch (e) {
61
68
  console.warn('[HtmlWidget] write failed:', e);
@@ -23,7 +23,7 @@
23
23
  .widget-container .widget-header .btn:hover{border-color:var(--accent);color:var(--accent)}
24
24
  .widget-container .widget-body{
25
25
  padding:0;display:flex;justify-content:center;overflow:auto;
26
- min-height:100px;
26
+ min-height:0;
27
27
  }
28
28
  .widget-container .widget-body svg{max-width:100%;height:auto}
29
29
  .widget-container .widget-loading{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "visualknowledge",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "Interactive AI Chat with Visualization - one-click launch via npx",
5
5
  "bin": {
6
6
  "visualknowledge": "./bin/visualknowledge.js"
@@ -15,9 +15,9 @@ SKILLS = {
15
15
  "description": "详细展示 Q/K/V 计算过程"
16
16
  },
17
17
  "ring_graph": {
18
- "name": "环形/链表可视化",
18
+ "name": "环形/链表可视化(JS动态计算)",
19
19
  "trigger": ["环形", "链表", "环", "cycle", "ring", "floyd", "判圈", "龟兔赛跑", "快慢指针", "循环链表", "环形链表", "约瑟夫"],
20
- "description": "展示环形链表、Floyd判圈算法、循环队列等需要闭合回路的图"
20
+ "description": "用 JS 动态计算坐标画环形链表/Floyd判圈/循环队列,绝不手算坐标"
21
21
  },
22
22
  "generic_pipeline": {
23
23
  "name": "通用流程图",
@@ -162,60 +162,28 @@ def get_skill_prompt():
162
162
 
163
163
  ---
164
164
 
165
- ### ⚠️ 环形/链表可视化规范(ring_graph
165
+ ### ⚠️ 环形/链表可视化规范(ring_graph)— 矩形环 + JS 动态计算
166
166
 
167
- 环形链表、判圈算法、循环队列等**必须用内嵌 SVG**,不要用 flex+文字箭头,因为环路无法用 → ↓ 闭合。
167
+ **核心原则:用矩形画环,不用圆形!** 环形结构画成矩形路径(上排→右下→下排←左上闭合),坐标由 JS 自动计算。
168
168
 
169
- #### 核心原则
170
- 1. **节点位置用三角函数计算**:把环上的节点均匀放在圆上,用 `(cx + r*cos(θ), cy + r*sin(θ))` 定位
171
- 2. **非环节点排成一条直线**(tail 部分水平排列),最后一个非环节点指向环节点
172
- 3. **箭头用 SVG `<line>` + `<marker>` 箭头标记**
173
- 4. **整个 SVG 放在一个 HTML 容器 `<div>` 中**,配合标题和图例
169
+ #### 为什么用矩形?
170
+ 1. 矩形只需要水平/垂直线,无需三角函数,坐标100%正确
171
+ 2. 圆形需要 sin/cos 计算角度,AI 极易算错导致节点错位、箭头断开
172
+ 3. 矩形环更清晰:上排→右转→下排→左转→回到入口,首尾相连就是环
174
173
 
175
- #### 布局方法
174
+ #### 矩形环布局原理
176
175
  ```
177
- tail 节点(水平直线) 环上的节点(圆形排列)
178
- 3 → 2 → 1 → 5 ← 4
179
- ↓ ↑
180
- 6 → 7 → 8
176
+ [tail...] → [5] → [4] → [6] ← 上排(环的前半,从左到右)
177
+ ↑ ↓
178
+ [8] ← [7] ← 下排(环的后半,从右到左,最终连回入口)
181
179
  ```
182
180
 
183
- - tail 部分:从左到右水平排列,间距固定
184
- - 环部分:用圆排列,节点均匀分布在圆周上
185
- - 连线:SVG `<line>` 元素 + 箭头 marker
181
+ - 上排 = ringNodes 的前半部分,从左到右,用 → 连接
182
+ - 下排 = ringNodes 的后半部分,从右到左,用 ← 连接
183
+ - 右侧 = 上排末尾 连到下排首个
184
+ - 左侧 = 下排末尾 ↑ 连回入口(环闭合!)
186
185
 
187
- #### 节点和箭头样式
188
- ```css
189
- /* 节点 */
190
- .node {
191
- fill: #1e293b; /* 深色填充 */
192
- stroke: #64748b; /* 边框 */
193
- stroke-width: 2;
194
- r: 22; /* 半径 */
195
- }
196
- .node-label {
197
- fill: #f1f5f9; /* 白色文字 */
198
- font-size: 14px;
199
- font-weight: bold;
200
- text-anchor: middle;
201
- dominant-baseline: central;
202
- }
203
-
204
- /* 箭头连线 */
205
- .edge {
206
- stroke: #94a3b8;
207
- stroke-width: 2;
208
- marker-end: url(#arrow);
209
- }
210
-
211
- /* 特殊高亮 */
212
- .node-entry { fill: #16a34a20; stroke: #16a34a; stroke-width: 3; } /* 环入口 - 绿 */
213
- .node-meet { fill: #e11d4820; stroke: #e11d48; stroke-width: 3; } /* 相遇点 - 红 */
214
- .node-slow { stroke: #d97706; stroke-width: 2.5; stroke-dasharray: 5,3; } /* 慢指针 - 橙 */
215
- .node-fast { stroke: #2563eb; stroke-width: 2.5; stroke-dasharray: 8,4; } /* 快指针 - 蓝 */
216
- ```
217
-
218
- #### 高质量示例:Floyd 判圈算法
186
+ #### 必须照搬的 JS 模板(直接复制,只改数据部分)
219
187
 
220
188
  ```html
221
189
  <div style="max-width:860px;margin:0 auto;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;color:inherit;background:transparent;padding:20px">
@@ -228,75 +196,167 @@ tail 节点(水平直线) 环上的节点(圆形排列)
228
196
  <h3 style="text-align:center;font-size:16px;margin-bottom:4px">Floyd 判圈算法 — 龟兔赛跑</h3>
229
197
  <p style="text-align:center;font-size:12px;opacity:0.5;margin-bottom:16px">slow 走 1 步 · fast 走 2 步 · 必在环内相遇</p>
230
198
 
231
- <svg viewBox="0 0 700 380" style="width:100%;max-width:700px;margin:0 auto;display:block">
232
- <defs>
233
- <marker id="arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
234
- <path d="M0,0 L8,3 L0,6" fill="#94a3b8"/>
235
- </marker>
236
- <marker id="arrow-green" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
237
- <path d="M0,0 L8,3 L0,6" fill="#16a34a"/>
238
- </marker>
239
- </defs>
240
-
241
- <!-- Tail 部分 (非环节点) -->
242
- <circle cx="80" cy="80" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
243
- <text x="80" y="80" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">3</text>
244
- <line x1="102" y1="80" x2="148" y2="80" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
245
-
246
- <circle cx="170" cy="80" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
247
- <text x="170" y="80" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">2</text>
248
- <line x1="192" y1="80" x2="238" y2="80" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
249
-
250
- <circle cx="260" cy="80" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
251
- <text x="260" y="80" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">1</text>
252
- <line x1="282" y1="80" x2="418" y2="240" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
253
-
254
- <!-- Tail → Entry 标注 -->
255
- <text x="360" y="140" fill="#16a34a" font-size="11" text-anchor="middle">a 步(到入口)</text>
256
-
257
- <!-- 环上的节点 (圆形排列,圆心 440,260 半径 100) -->
258
- <!-- 5: θ=270° (440, 160) -->
259
- <circle cx="440" cy="160" r="22" fill="#16a34a20" stroke="#16a34a" stroke-width="3"/>
260
- <text x="440" y="160" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">5</text>
261
- <text x="440" y="130" fill="#16a34a" font-size="10" text-anchor="middle">入口</text>
262
-
263
- <!-- 4: θ=342° → (535, 229) -->
264
- <circle cx="535" cy="229" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
265
- <text x="535" y="229" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">4</text>
266
-
267
- <!-- 6: θ=54° → (499, 341) -->
268
- <circle cx="499" cy="341" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
269
- <text x="499" y="341" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">6</text>
270
-
271
- <!-- 7: θ=126° (381, 341) -->
272
- <circle cx="381" cy="341" r="22" fill="#e11d4820" stroke="#e11d48" stroke-width="3"/>
273
- <text x="381" y="341" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">7</text>
274
- <text x="381" y="374" fill="#e11d48" font-size="10" text-anchor="middle">相遇点</text>
275
-
276
- <!-- 8: θ=198° → (345, 229) -->
277
- <circle cx="345" cy="229" r="22" fill="#1e293b" stroke="#64748b" stroke-width="2"/>
278
- <text x="345" y="229" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">8</text>
279
-
280
- <!-- 环上的箭头连线 (5→4→6→7→8→5) -->
281
- <line x1="461" y1="152" x2="514" y2="219" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
282
- <line x1="555" y1="238" x2="518" y2="328" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
283
- <line x1="489" y1="357" x2="395" y2="353" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
284
- <line x1="363" y1="333" x2="328" y2="241" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
285
- <line x1="348" y1="211" x2="418" y2="171" stroke="#94a3b8" stroke-width="2" marker-end="url(#arrow)"/>
286
-
287
- <!-- 距离标注 -->
288
- <text x="388" y="310" fill="#94a3b8" font-size="10" text-anchor="middle">b</text>
289
- <text x="330" y="276" fill="#94a3b8" font-size="10" text-anchor="middle">c</text>
290
-
291
- <!-- 图例说明 -->
292
- <text x="440" y="260" fill="#94a3b8" font-size="11" text-anchor="middle" font-style="italic">环长 c = b + 剩余</text>
293
-
294
- <!-- 指针 -->
295
- <text x="100" y="110" fill="#d97706" font-size="11" font-weight="bold">🐢 slow</text>
296
- <text x="100" y="124" fill="#2563eb" font-size="11" font-weight="bold">🐇 fast</text>
297
- <line x1="140" y1="107" x2="170" y2="90" stroke="#d97706" stroke-width="1.5" stroke-dasharray="4,2"/>
298
- <line x1="140" y1="120" x2="170" y2="90" stroke="#2563eb" stroke-width="1.5" stroke-dasharray="6,3"/>
299
- </svg>
199
+ <div id="ring-container"></div>
200
+
201
+ <script>
202
+ (function() {
203
+ // ===== 只需修改这里的数据 =====
204
+ var tailNodes = [3, 2, 1]; // tail 部分(从左到右)
205
+ var ringNodes = [5, 4, 6, 7, 8]; // 环部分(顺序排列,第一个是入口)
206
+ var entryIdx = 0; // 环入口在 ringNodes 中的下标
207
+ var meetIdx = 3; // 相遇点下标(-1=无)
208
+ // ==============================
209
+
210
+ var NR = 24; // 节点半径
211
+ var GAP = 90; // 同行节点间距
212
+ var V_GAP = 110; // 上下排间距
213
+ var PAD = 50; // 左右边距
214
+ var ARROW_PAD = 6; // 箭头离节点边缘的间隙
215
+
216
+ var ringLen = ringNodes.length;
217
+ var tailLen = tailNodes.length;
218
+
219
+ // 将环节点分成上排和下排
220
+ var topCount = Math.ceil(ringLen / 2);
221
+ var botCount = ringLen - topCount;
222
+
223
+ // 计算上排起始 X(让上排在 tail 右侧)
224
+ var tailEndX = tailLen > 0 ? PAD + (tailLen - 1) * GAP : PAD - GAP;
225
+ var ringStartX = tailEndX + GAP;
226
+ var topY = PAD + NR;
227
+ var botY = topY + V_GAP;
228
+
229
+ // 所有节点坐标(统一索引)
230
+ var nodes = []; // { x, y, val, role }
231
+
232
+ // Tail 节点
233
+ for (var i = 0; i < tailLen; i++) {
234
+ nodes.push({ x: PAD + i * GAP, y: topY, val: tailNodes[i], role: 'tail' });
235
+ }
236
+
237
+ // 上排环节点
238
+ var topStart = nodes.length;
239
+ for (var i = 0; i < topCount; i++) {
240
+ var ri = i;
241
+ nodes.push({ x: ringStartX + i * GAP, y: topY, val: ringNodes[ri], role: 'ring', ringIdx: ri });
242
+ }
243
+
244
+ // 下排环节点(从右到左排列,所以 X 从上排末尾开始递减)
245
+ var botStart = nodes.length;
246
+ for (var i = 0; i < botCount; i++) {
247
+ var ri = topCount + i;
248
+ nodes.push({ x: ringStartX + (topCount - 1) * GAP - i * GAP, y: botY, val: ringNodes[ri], role: 'ring', ringIdx: ri });
249
+ }
250
+
251
+ // SVG 尺寸
252
+ var allX = nodes.map(function(n){ return n.x; });
253
+ var W = Math.max.apply(null, allX) + PAD + NR + 10;
254
+ var H = botY + NR + PAD;
255
+
256
+ // 边缘点计算(从圆心出发,沿方向偏移半径)
257
+ function edgePt(x1, y1, x2, y2, r) {
258
+ var dx = x2 - x1, dy = y2 - y1;
259
+ var len = Math.sqrt(dx * dx + dy * dy);
260
+ if (len < 1) return { x: x1, y: y1 };
261
+ return { x: x1 + dx / len * r, y: y1 + dy / len * r };
262
+ }
263
+
264
+ // 画箭头连线
265
+ function line(x1, y1, x2, y2, color, marker) {
266
+ var p1 = edgePt(x1, y1, x2, y2, NR + ARROW_PAD);
267
+ var p2 = edgePt(x2, y2, x1, y1, NR + ARROW_PAD);
268
+ return '<line x1="' + p1.x.toFixed(1) + '" y1="' + p1.y.toFixed(1) +
269
+ '" x2="' + p2.x.toFixed(1) + '" y2="' + p2.y.toFixed(1) +
270
+ '" stroke="' + color + '" stroke-width="2" marker-end="url(#' + marker + ')"/>';
271
+ }
272
+
273
+ // 画节点
274
+ function circle(n, fill, stroke, sw) {
275
+ var s = '<circle cx="' + n.x.toFixed(1) + '" cy="' + n.y.toFixed(1) + '" r="' + NR +
276
+ '" fill="' + fill + '" stroke="' + stroke + '" stroke-width="' + sw + '"/>';
277
+ s += '<text x="' + n.x.toFixed(1) + '" y="' + n.y.toFixed(1) +
278
+ '" fill="#f1f5f9" font-size="14" font-weight="bold" text-anchor="middle" dominant-baseline="central">' + n.val + '</text>';
279
+ return s;
280
+ }
281
+
282
+ var svg = '<svg viewBox="0 0 ' + W + ' ' + H + '" style="width:100%;max-width:' + W + 'px;margin:0 auto;display:block">';
283
+ svg += '<defs>';
284
+ svg += '<marker id="arr" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#94a3b8"/></marker>';
285
+ svg += '<marker id="arr-g" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#16a34a"/></marker>';
286
+ svg += '<marker id="arr-r" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e11d48"/></marker>';
287
+ svg += '</defs>';
288
+
289
+ // ---- Step 1: 画连线(先画线,节点覆盖在上面)----
290
+
291
+ // Tail → Tail 箭头
292
+ for (var i = 0; i < tailLen - 1; i++) {
293
+ svg += line(nodes[i].x, nodes[i].y, nodes[i+1].x, nodes[i+1].y, '#94a3b8', 'arr');
294
+ }
295
+
296
+ // Tail 末尾 → 环入口(上排第一个)
297
+ if (tailLen > 0) {
298
+ svg += line(nodes[tailLen-1].x, nodes[tailLen-1].y, nodes[topStart].x, nodes[topStart].y, '#16a34a', 'arr-g');
299
+ // "a 步" 标注
300
+ var mx = (nodes[tailLen-1].x + nodes[topStart].x) / 2;
301
+ var my = nodes[topStart].y - NR - 10;
302
+ svg += '<text x="' + mx.toFixed(0) + '" y="' + my.toFixed(0) + '" fill="#16a34a" font-size="11" text-anchor="middle">a 步</text>';
303
+ }
304
+
305
+ // 上排 → 箭头(从左到右)
306
+ for (var i = 0; i < topCount - 1; i++) {
307
+ svg += line(nodes[topStart+i].x, nodes[topStart+i].y, nodes[topStart+i+1].x, nodes[topStart+i+1].y, '#94a3b8', 'arr');
308
+ }
309
+
310
+ // 上排末尾 ↓ 下排首个(右侧垂直箭头)
311
+ if (botCount > 0) {
312
+ svg += line(nodes[topStart+topCount-1].x, nodes[topStart+topCount-1].y,
313
+ nodes[botStart].x, nodes[botStart].y, '#94a3b8', 'arr');
314
+ }
315
+
316
+ // 下排 ← 箭头(从右到左)
317
+ for (var i = 0; i < botCount - 1; i++) {
318
+ svg += line(nodes[botStart+i].x, nodes[botStart+i].y, nodes[botStart+i+1].x, nodes[botStart+i+1].y, '#94a3b8', 'arr');
319
+ }
320
+
321
+ // 下排末尾 ↑ 回到环入口(左侧闭合箭头!这是环的关键!)
322
+ var closeFrom = nodes[botStart + botCount - 1];
323
+ var closeTo = nodes[topStart]; // 入口 = 上排第一个
324
+ svg += line(closeFrom.x, closeFrom.y, closeTo.x, closeTo.y, '#16a34a', 'arr-g');
325
+
326
+ // ---- Step 2: 画节点(覆盖在连线上面)----
327
+
328
+ // Tail 节点
329
+ for (var i = 0; i < tailLen; i++) {
330
+ svg += circle(nodes[i], '#1e293b', '#64748b', 2);
331
+ }
332
+
333
+ // 环节点
334
+ for (var i = 0; i < ringLen; i++) {
335
+ var n = nodes[topStart + i];
336
+ var fill = '#1e293b', stroke = '#64748b', sw = 2, label = '';
337
+ if (i === entryIdx) {
338
+ fill = '#16a34a20'; stroke = '#16a34a'; sw = 3; label = '入口';
339
+ } else if (i === meetIdx) {
340
+ fill = '#e11d4820'; stroke = '#e11d48'; sw = 3; label = '相遇点';
341
+ }
342
+ svg += circle(n, fill, stroke, sw);
343
+ if (label) {
344
+ var ly = n.y < (topY + botY) / 2 ? n.y - NR - 10 : n.y + NR + 14;
345
+ svg += '<text x="' + n.x.toFixed(1) + '" y="' + ly.toFixed(1) + '" fill="' + stroke + '" font-size="10" text-anchor="middle">' + label + '</text>';
346
+ }
347
+ }
348
+
349
+ // 起点指针标注
350
+ if (tailLen > 0) {
351
+ var p = nodes[0];
352
+ svg += '<text x="' + p.x.toFixed(0) + '" y="' + (p.y + NR + 16).toFixed(0) + '" fill="#d97706" font-size="11" font-weight="bold" text-anchor="middle">🐢 slow</text>';
353
+ svg += '<text x="' + p.x.toFixed(0) + '" y="' + (p.y + NR + 30).toFixed(0) + '" fill="#2563eb" font-size="11" font-weight="bold" text-anchor="middle">🐇 fast</text>';
354
+ }
355
+
356
+ svg += '</svg>';
357
+ document.getElementById('ring-container').innerHTML = svg;
358
+ })();
359
+ </script>
300
360
 
301
361
  <div class="ring-legend">
302
362
  <span><span class="ring-dot" style="background:#16a34a"></span> 环入口</span>
@@ -312,14 +372,15 @@ tail 节点(水平直线) 环上的节点(圆形排列)
312
372
  </div>
313
373
  ```
314
374
 
315
- #### 环形图绘制要点(必读)
316
- 1. **先算坐标再画**:确定圆心 `(cx, cy)` 和半径 `r`,用 `θ = i × 360°/n` 计算每个节点位置
317
- 2. **箭头要指向圆心方向偏移**:`marker-end` 自动朝向,但起点要从节点边缘出发(不是圆心),终点停在目标节点边缘
318
- 3. **Tail 节点水平排列**,最后一个 tail 节点画一条指向环节点的箭头线
319
- 4. **闭合箭头**:最后一个环节点必须画箭头回到第一个环节点(这是关键!)
320
- 5. **节点数量 8**:太多节点看不清,适当合并或简化
321
- 6. **用 `<text>` 标注距离**(a, b, c)和关键位置(入口、相遇点)
322
- 7. **viewBox 要足够大**:确保所有节点和标注都在可视区域内
375
+ #### 使用规则(必读)
376
+ 1. **直接复制上面的模板**,只修改 `tailNodes`、`ringNodes`、`entryIdx`、`meetIdx` 这4个数据变量
377
+ 2. **绝不手写 SVG 坐标** — JS 自动计算所有矩形布局位置
378
+ 3. **绝不手算三角函数** 矩形布局只需要水平/垂直间距
379
+ 4. 环的闭合由代码自动处理:下排末尾 ↑ 连回上排入口(入口 = `ringNodes[entryIdx]`)
380
+ 5. `entryIdx` = 环入口在 `ringNodes` 中的下标,`meetIdx` = 相遇点下标(无则设 -1)
381
+ 6. 可根据具体算法修改标题、副标题、图例和底部公式
382
+ 7. 节点数量建议 ≤ 8,太多可适当合并
383
+ 8. **分步确认流程**:① 确认 tail→entry 连线正确 ② 确认上排→右转→下排方向正确 ③ 确认下排末尾连回入口形成闭环
323
384
 
324
385
  ### 使用规则
325
386
  1. 涉及架构/流程/数据变换/神经网络结构/算法步骤 → **必须用 ```html**