@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.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/src/70-docs.js ADDED
@@ -0,0 +1,1226 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 70-docs.js — 原理页
3
+ *
4
+ * 中文教学长文 + 纯 CSS 图解。组件由 90-page.js 直接调用:
5
+ * function DocsPage(props)
6
+ * 可选 props.onNavigate(pageId):页面外壳若提供,本页会渲染跳转按钮;
7
+ * 未提供时自动退化为文字提示,不依赖外壳实现。
8
+ *
9
+ * 约束:普通 JS,无 import/export/JSX;React / e / hooks 由外层提供。
10
+ * 所有 class 使用 al-doc- 前缀;颜色优先使用 --dsw-alias-* token。
11
+ * ───────────────────────────────────────────────────────────── */
12
+
13
+ const DOC_CSS = `
14
+ .al-doc-page {
15
+ --al-doc-bg: var(--dsw-alias-bg-base, #ffffff);
16
+ --al-doc-panel: var(--dsw-alias-bg-layer-1, #ffffff);
17
+ --al-doc-panel-2: var(--dsw-alias-bg-layer-2, #f6f7f9);
18
+ --al-doc-panel-3: var(--dsw-alias-bg-layer-3, #eceff3);
19
+ --al-doc-text: var(--dsw-alias-label-primary, #171a21);
20
+ --al-doc-sub: var(--dsw-alias-label-secondary, #5b6472);
21
+ --al-doc-ter: var(--dsw-alias-label-tertiary, #8b93a1);
22
+ --al-doc-border: var(--dsw-alias-border-l1, rgba(15, 23, 42, 0.1));
23
+ --al-doc-border-2: var(--dsw-alias-border-l2, rgba(15, 23, 42, 0.18));
24
+ --al-doc-brand: var(--dsw-alias-brand-primary, #4d6bfe);
25
+ --al-doc-tint: color-mix(in srgb, var(--al-doc-brand) 10%, transparent);
26
+ --al-doc-tint-2: color-mix(in srgb, var(--al-doc-brand) 18%, transparent);
27
+ --al-doc-brand-text: color-mix(in srgb, var(--al-doc-brand) 76%, var(--al-doc-text));
28
+ --al-doc-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
29
+ --al-doc-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
30
+ box-sizing: border-box;
31
+ width: 100%;
32
+ max-width: 1180px;
33
+ margin: 0 auto;
34
+ padding: 4px 4px 72px;
35
+ color: var(--al-doc-text);
36
+ font-family: var(--al-doc-sans);
37
+ font-size: 14px;
38
+ line-height: 1.7;
39
+ -webkit-font-smoothing: antialiased;
40
+ }
41
+ .al-doc-page * { box-sizing: border-box; }
42
+
43
+ /* ── 头部 ── */
44
+ .al-doc-hero { padding: 16px 2px 24px; border-bottom: 1px solid var(--al-doc-border); margin-bottom: 26px; }
45
+ .al-doc-eyebrow { font-family: var(--al-doc-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--al-doc-ter); }
46
+ .al-doc-title { margin: 10px 0 12px; font-size: clamp(24px, 3.4vw, 34px); line-height: 1.24; font-weight: 700; letter-spacing: -0.01em; color: var(--al-doc-text); }
47
+ .al-doc-lead { margin: 0; max-width: 80ch; color: var(--al-doc-sub); font-size: 14.5px; line-height: 1.85; }
48
+ .al-doc-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
49
+ .al-doc-chip { display: inline-flex; align-items: center; gap: 7px; padding: 3px 10px; border-radius: 999px; border: 1px solid var(--al-doc-border); background: var(--al-doc-panel-2); color: var(--al-doc-sub); font-size: 12px; line-height: 1.6; }
50
+ .al-doc-chip-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--al-doc-brand); opacity: 0.85; }
51
+
52
+ /* ── 双栏:目录 + 正文 ── */
53
+ .al-doc-layout { display: grid; grid-template-columns: 196px minmax(0, 1fr); gap: 34px; align-items: start; }
54
+ .al-doc-toc { position: sticky; top: 16px; }
55
+ .al-doc-toc-title { font-family: var(--al-doc-mono); font-size: 11px; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; color: var(--al-doc-ter); margin-bottom: 10px; }
56
+ .al-doc-toc-list { display: flex; flex-direction: column; gap: 2px; border-left: 1px solid var(--al-doc-border); }
57
+ .al-doc-toc-item { appearance: none; display: flex; align-items: baseline; gap: 7px; width: 100%; margin-left: -1px; padding: 6px 10px; border: 0; border-left: 2px solid transparent; border-radius: 0 6px 6px 0; background: transparent; color: var(--al-doc-sub); font: inherit; font-size: 12.5px; line-height: 1.5; text-align: left; cursor: pointer; }
58
+ .al-doc-toc-item:hover { color: var(--al-doc-text); background: var(--al-doc-panel-2); border-left-color: var(--al-doc-border-2); }
59
+ .al-doc-toc-no { flex: none; font-family: var(--al-doc-mono); font-size: 10px; color: var(--al-doc-ter); }
60
+ .al-doc-main { min-width: 0; }
61
+
62
+ /* ── 章节 ── */
63
+ .al-doc-sec-wrap { scroll-margin-top: 20px; }
64
+ .al-doc-sec { padding: 26px 0 30px; border-bottom: 1px dashed var(--al-doc-border); }
65
+ .al-doc-sec:last-child { border-bottom: 0; }
66
+ .al-doc-sec-head { display: flex; flex-direction: column; gap: 7px; margin-bottom: 16px; }
67
+ .al-doc-sec-kicker { display: flex; align-items: center; gap: 9px; }
68
+ .al-doc-sec-no { font-family: var(--al-doc-mono); font-size: 11px; font-weight: 600; color: var(--al-doc-brand-text); background: var(--al-doc-tint); padding: 3px 7px; border-radius: 6px; line-height: 1.3; }
69
+ .al-doc-sec-title { margin: 0; font-size: 19px; line-height: 1.35; font-weight: 650; letter-spacing: -0.005em; color: var(--al-doc-text); }
70
+ .al-doc-sec-lead { margin: 0; max-width: 76ch; color: var(--al-doc-sub); font-size: 13.5px; line-height: 1.85; }
71
+
72
+ /* ── 正文元素 ── */
73
+ .al-doc-p { margin: 12px 0; max-width: 76ch; color: var(--al-doc-sub); font-size: 13.5px; line-height: 1.85; }
74
+ .al-doc-p strong, .al-doc-list strong, .al-doc-ordered strong { color: var(--al-doc-text); font-weight: 650; }
75
+ .al-doc-list, .al-doc-ordered { margin: 10px 0 14px; padding-left: 20px; display: flex; flex-direction: column; gap: 6px; color: var(--al-doc-sub); font-size: 13.5px; line-height: 1.8; }
76
+ .al-doc-list li::marker { color: var(--al-doc-ter); }
77
+ .al-doc-ordered li::marker { color: var(--al-doc-ter); font-family: var(--al-doc-mono); font-size: 12px; }
78
+ .al-doc-inline { font-family: var(--al-doc-mono); font-size: 12px; padding: 1px 5px; border-radius: 5px; border: 1px solid var(--al-doc-border); background: var(--al-doc-panel-3); color: var(--al-doc-text); white-space: nowrap; }
79
+
80
+ /* ── 卡片 / 网格 ── */
81
+ .al-doc-grid { display: grid; gap: 12px; margin: 16px 0; }
82
+ .al-doc-grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
83
+ .al-doc-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
84
+ .al-doc-grid-auto { grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); }
85
+ .al-doc-card { padding: 14px 15px; border: 1px solid var(--al-doc-border); border-radius: 12px; background: var(--al-doc-panel); }
86
+ .al-doc-card-icon { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; margin-bottom: 8px; border-radius: 8px; background: var(--al-doc-tint); color: var(--al-doc-brand-text); font-family: var(--al-doc-mono); font-size: 13px; font-weight: 700; }
87
+ .al-doc-card-title { margin-bottom: 4px; color: var(--al-doc-text); font-size: 13.5px; font-weight: 650; }
88
+ .al-doc-card-body { color: var(--al-doc-sub); font-size: 12.5px; line-height: 1.75; }
89
+
90
+ /* ── 公式条 ── */
91
+ .al-doc-formula { display: flex; flex-wrap: wrap; align-items: center; gap: 9px; margin: 14px 0 4px; padding: 16px; border: 1px solid var(--al-doc-border); border-radius: 12px; background: linear-gradient(180deg, var(--al-doc-panel), var(--al-doc-panel-2)); }
92
+ .al-doc-formula-term { padding: 5px 11px; border: 1px solid var(--al-doc-border); border-radius: 8px; background: var(--al-doc-panel); color: var(--al-doc-text); font-family: var(--al-doc-mono); font-size: 13px; font-weight: 650; }
93
+ .al-doc-formula-op { color: var(--al-doc-ter); font-family: var(--al-doc-mono); font-size: 13px; }
94
+ .al-doc-formula-note { margin: 6px 0 0; color: var(--al-doc-ter); font-size: 12px; line-height: 1.7; }
95
+
96
+ /* ── 最小循环图 ── */
97
+ .al-doc-loop { margin: 16px 0 6px; padding: 18px 16px 12px; border: 1px solid var(--al-doc-border); border-radius: 14px; background: var(--al-doc-panel-2); overflow-x: auto; }
98
+ .al-doc-loop-row { display: flex; align-items: center; gap: 9px; min-width: max-content; padding-bottom: 4px; }
99
+ .al-doc-loop-node { flex: none; min-width: 108px; padding: 9px 12px; border: 1px solid var(--al-doc-border); border-radius: 10px; background: var(--al-doc-panel); }
100
+ .al-doc-loop-node-input { border-color: color-mix(in srgb, var(--al-doc-brand) 42%, var(--al-doc-border)); }
101
+ .al-doc-loop-node-title { color: var(--al-doc-text); font-size: 12.5px; font-weight: 650; }
102
+ .al-doc-loop-node-sub { margin-top: 2px; max-width: 150px; color: var(--al-doc-ter); font-size: 10.5px; line-height: 1.45; }
103
+ .al-doc-loop-arrow { flex: none; color: var(--al-doc-ter); font-size: 15px; }
104
+ .al-doc-loop-core { flex: none; display: flex; align-items: stretch; gap: 8px; padding: 7px; border: 1px dashed color-mix(in srgb, var(--al-doc-brand) 45%, var(--al-doc-border-2)); border-radius: 12px; background: var(--al-doc-tint); animation: al-doc-breathe 3.2s ease-in-out infinite; }
105
+ .al-doc-loop-core-node { padding: 7px 11px; border: 1px solid var(--al-doc-border); border-radius: 8px; background: var(--al-doc-panel); color: var(--al-doc-text); font-size: 12px; font-weight: 600; white-space: nowrap; }
106
+ .al-doc-loop-core-swap { display: flex; flex-direction: column; align-items: center; justify-content: center; color: var(--al-doc-brand-text); font-size: 15px; line-height: 1; }
107
+ .al-doc-loop-core-swap small { margin-top: 3px; color: var(--al-doc-ter); font-size: 9.5px; white-space: nowrap; }
108
+ .al-doc-loop-foot { margin-top: 10px; color: var(--al-doc-ter); font-size: 11.5px; line-height: 1.7; text-align: center; }
109
+ @keyframes al-doc-breathe {
110
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(0, 0, 0, 0); }
111
+ 50% { box-shadow: 0 0 0 4px var(--al-doc-tint); }
112
+ }
113
+
114
+ /* ── 流转时间线 ── */
115
+ .al-doc-journey { list-style: none; margin: 16px 0 8px; padding: 0; }
116
+ .al-doc-journey-item { position: relative; margin-left: 7px; padding: 0 0 16px 30px; border-left: 1px solid var(--al-doc-border); }
117
+ .al-doc-journey-item:last-child { padding-bottom: 0; border-left-color: transparent; }
118
+ .al-doc-journey-dot { position: absolute; left: -6.5px; top: 4px; width: 12px; height: 12px; border-radius: 50%; background: var(--al-doc-brand); box-shadow: 0 0 0 3px var(--al-doc-panel-2); opacity: 0.9; }
119
+ .al-doc-journey-head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
120
+ .al-doc-stage { padding: 3px 6px; border-radius: 5px; background: var(--al-doc-tint); color: var(--al-doc-brand-text); font-family: var(--al-doc-mono); font-size: 10.5px; font-weight: 600; line-height: 1.2; }
121
+ .al-doc-journey-title { color: var(--al-doc-text); font-size: 13.5px; font-weight: 650; }
122
+ .al-doc-journey-text { margin: 3px 0 0; color: var(--al-doc-sub); font-size: 12.5px; line-height: 1.7; }
123
+ .al-doc-journey-where { margin-top: 5px; color: var(--al-doc-ter); font-size: 11.5px; line-height: 1.6; }
124
+ .al-doc-journey-where b { color: var(--al-doc-sub); font-weight: 600; }
125
+
126
+ /* ── 流程链 ── */
127
+ .al-doc-flowchain { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; margin: 14px 0; padding: 12px 14px; border: 1px dashed var(--al-doc-border-2); border-radius: 10px; background: var(--al-doc-panel-2); font-size: 12px; color: var(--al-doc-sub); }
128
+ .al-doc-flowchain-step { padding: 3px 8px; border: 1px solid var(--al-doc-border); border-radius: 6px; background: var(--al-doc-panel); color: var(--al-doc-text); font-family: var(--al-doc-mono); font-size: 11px; white-space: nowrap; }
129
+ .al-doc-flowchain-arrow { color: var(--al-doc-ter); }
130
+
131
+ /* ── 上下文预算条 ── */
132
+ .al-doc-budget { margin: 16px 0 4px; padding: 14px 15px 12px; border: 1px solid var(--al-doc-border); border-radius: 12px; background: var(--al-doc-panel-2); }
133
+ .al-doc-budget-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 12px; margin-bottom: 9px; color: var(--al-doc-sub); font-size: 12px; }
134
+ .al-doc-budget-used { color: var(--al-doc-text); font-family: var(--al-doc-mono); font-size: 11.5px; }
135
+ .al-doc-bar { position: relative; display: flex; height: 22px; overflow: hidden; border-radius: 6px; background: var(--al-doc-panel-3); }
136
+ .al-doc-bar-seg { height: 100%; }
137
+ .al-doc-bar-seg-system { background: #4d6bfe; }
138
+ .al-doc-bar-seg-summary { background: #a371f7; }
139
+ .al-doc-bar-seg-memory { background: #2da44e; }
140
+ .al-doc-bar-seg-recent { background: #d4a72c; }
141
+ .al-doc-bar-seg-input { background: #f85149; }
142
+ .al-doc-bar-marker { position: absolute; top: -3px; bottom: -3px; width: 2px; background: var(--al-doc-text); opacity: 0.5; }
143
+ .al-doc-legend { display: flex; flex-wrap: wrap; gap: 5px 15px; margin-top: 9px; color: var(--al-doc-sub); font-size: 11.5px; }
144
+ .al-doc-legend-item { display: inline-flex; align-items: center; gap: 6px; }
145
+ .al-doc-legend-dot { width: 8px; height: 8px; border-radius: 2px; }
146
+ .al-doc-legend-tok { color: var(--al-doc-ter); font-family: var(--al-doc-mono); font-size: 10.5px; }
147
+
148
+ /* ── 提示条 ── */
149
+ .al-doc-note { display: flex; gap: 10px; margin: 14px 0; padding: 12px 14px; border: 1px solid var(--al-doc-border); border-radius: 10px; background: var(--al-doc-panel-2); color: var(--al-doc-sub); font-size: 12.5px; line-height: 1.75; }
150
+ .al-doc-note-icon { flex: none; width: 20px; height: 20px; margin-top: 1px; border-radius: 50%; background: var(--al-doc-tint); color: var(--al-doc-brand-text); font-family: var(--al-doc-mono); font-size: 11px; font-weight: 700; line-height: 20px; text-align: center; }
151
+ .al-doc-note-body { min-width: 0; }
152
+ .al-doc-note-title { display: block; margin-bottom: 2px; color: var(--al-doc-text); font-weight: 650; }
153
+ .al-doc-note-warn { border-color: color-mix(in srgb, #f85149 32%, var(--al-doc-border)); background: color-mix(in srgb, #f85149 7%, var(--al-doc-panel)); }
154
+ .al-doc-note-warn .al-doc-note-icon { background: color-mix(in srgb, #f85149 16%, transparent); color: color-mix(in srgb, #f85149 72%, var(--al-doc-text)); }
155
+ .al-doc-note-tip { border-color: color-mix(in srgb, #2da44e 30%, var(--al-doc-border)); background: color-mix(in srgb, #2da44e 7%, var(--al-doc-panel)); }
156
+ .al-doc-note-tip .al-doc-note-icon { background: color-mix(in srgb, #2da44e 16%, transparent); color: color-mix(in srgb, #2da44e 72%, var(--al-doc-text)); }
157
+
158
+ /* ── 表格 ── */
159
+ .al-doc-table-wrap { margin: 14px 0; overflow-x: auto; border: 1px solid var(--al-doc-border); border-radius: 10px; }
160
+ .al-doc-table { width: 100%; min-width: 520px; border-collapse: collapse; font-size: 12.5px; }
161
+ .al-doc-table th { padding: 8px 12px; border-bottom: 1px solid var(--al-doc-border); background: var(--al-doc-panel-2); color: var(--al-doc-sub); font-weight: 600; text-align: left; white-space: nowrap; }
162
+ .al-doc-table td { padding: 9px 12px; border-bottom: 1px solid var(--al-doc-border); color: var(--al-doc-sub); line-height: 1.65; vertical-align: top; }
163
+ .al-doc-table tr:last-child td { border-bottom: 0; }
164
+ .al-doc-table td:first-child { color: var(--al-doc-text); font-weight: 600; }
165
+
166
+ /* ── 代码块 ── */
167
+ .al-doc-code { margin: 14px 0; overflow: hidden; border: 1px solid var(--al-doc-border); border-radius: 10px; background: var(--al-doc-panel-2); }
168
+ .al-doc-code-head { display: flex; align-items: center; gap: 7px; padding: 7px 12px; border-bottom: 1px solid var(--al-doc-border); background: var(--al-doc-panel); color: var(--al-doc-ter); font-family: var(--al-doc-mono); font-size: 11px; }
169
+ .al-doc-code-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--al-doc-brand); opacity: 0.7; }
170
+ .al-doc-pre { margin: 0; padding: 12px 14px; overflow-x: auto; color: var(--al-doc-text); font-family: var(--al-doc-mono); font-size: 12px; line-height: 1.7; tab-size: 2; white-space: pre; }
171
+
172
+ /* ── 编号步骤 ── */
173
+ .al-doc-steps { counter-reset: al-doc-step; list-style: none; margin: 14px 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
174
+ .al-doc-step { position: relative; padding: 10px 12px 10px 44px; border: 1px solid var(--al-doc-border); border-radius: 10px; background: var(--al-doc-panel); color: var(--al-doc-sub); font-size: 12.5px; line-height: 1.7; }
175
+ .al-doc-step::before { counter-increment: al-doc-step; content: counter(al-doc-step); position: absolute; left: 13px; top: 10px; width: 20px; height: 20px; border-radius: 50%; background: var(--al-doc-tint); color: var(--al-doc-brand-text); font-family: var(--al-doc-mono); font-size: 11px; font-weight: 700; line-height: 20px; text-align: center; }
176
+ .al-doc-step strong { color: var(--al-doc-text); font-weight: 650; }
177
+
178
+ /* ── 页脚 ── */
179
+ .al-doc-foot { margin-top: 10px; padding: 22px 0 0; color: var(--al-doc-ter); font-size: 12.5px; line-height: 1.8; }
180
+ .al-doc-foot p { margin: 0 0 10px; }
181
+ .al-doc-btn { appearance: none; display: inline-flex; align-items: center; gap: 6px; padding: 7px 14px; border: 1px solid var(--al-doc-border-2); border-radius: 8px; background: var(--al-doc-tint); color: var(--al-doc-brand-text); font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; }
182
+ .al-doc-btn:hover { background: var(--al-doc-tint-2); }
183
+
184
+ /* ── 响应式 ── */
185
+ @media (max-width: 900px) {
186
+ .al-doc-layout { grid-template-columns: minmax(0, 1fr); gap: 10px; }
187
+ .al-doc-toc { position: static; }
188
+ .al-doc-toc-list { flex-direction: row; gap: 4px; overflow-x: auto; padding-bottom: 6px; border-left: 0; }
189
+ .al-doc-toc-item { flex: none; width: auto; margin-left: 0; border-left: 0; border-bottom: 2px solid transparent; border-radius: 6px; white-space: nowrap; }
190
+ .al-doc-toc-item:hover { border-bottom-color: var(--al-doc-border-2); }
191
+ .al-doc-grid-2, .al-doc-grid-3 { grid-template-columns: minmax(0, 1fr); }
192
+ .al-doc-sec { padding: 22px 0 26px; }
193
+ }
194
+ @media (max-width: 560px) {
195
+ .al-doc-page { padding-bottom: 48px; }
196
+ .al-doc-title { font-size: 23px; }
197
+ .al-doc-loop { padding: 14px 12px 10px; }
198
+ }
199
+ @media (prefers-reduced-motion: reduce) {
200
+ .al-doc-loop-core { animation: none; }
201
+ }
202
+ `;
203
+
204
+ /* ── 小型展示原语 ─────────────────────────────────────────── */
205
+
206
+ function DocInline(props) {
207
+ return e('code', { className: 'al-doc-inline' }, props.children);
208
+ }
209
+
210
+ function DocP(props) {
211
+ return e('p', { className: 'al-doc-p' }, props.children);
212
+ }
213
+
214
+ function DocList(props) {
215
+ const items = props.items || [];
216
+ return e('ul', { className: 'al-doc-list' }, items.map(function (item, index) {
217
+ return e('li', { key: index }, item);
218
+ }));
219
+ }
220
+
221
+ function DocOrdered(props) {
222
+ const items = props.items || [];
223
+ return e('ol', { className: 'al-doc-ordered' }, items.map(function (item, index) {
224
+ return e('li', { key: index }, item);
225
+ }));
226
+ }
227
+
228
+ function DocCode(props) {
229
+ return e('div', { className: 'al-doc-code' },
230
+ props.label ? e('div', { className: 'al-doc-code-head' },
231
+ e('span', { className: 'al-doc-code-dot' }),
232
+ e('span', null, props.label)
233
+ ) : null,
234
+ e('pre', { className: 'al-doc-pre' }, props.code)
235
+ );
236
+ }
237
+
238
+ function DocNote(props) {
239
+ const kind = props.kind || 'info';
240
+ return e('div', { className: 'al-doc-note al-doc-note-' + kind },
241
+ e('span', { className: 'al-doc-note-icon' }, props.icon || 'i'),
242
+ e('div', { className: 'al-doc-note-body' },
243
+ props.title ? e('strong', { className: 'al-doc-note-title' }, props.title) : null,
244
+ props.children
245
+ )
246
+ );
247
+ }
248
+
249
+ function DocCard(props) {
250
+ return e('div', { className: 'al-doc-card' },
251
+ props.icon ? e('span', { className: 'al-doc-card-icon' }, props.icon) : null,
252
+ e('div', { className: 'al-doc-card-title' }, props.title),
253
+ e('div', { className: 'al-doc-card-body' }, props.children)
254
+ );
255
+ }
256
+
257
+ function DocGrid(props) {
258
+ const cols = props.cols || 2;
259
+ const modifier = cols === 'auto' ? 'al-doc-grid-auto' : 'al-doc-grid-' + cols;
260
+ return e('div', { className: 'al-doc-grid ' + modifier }, props.children);
261
+ }
262
+
263
+ function DocTable(props) {
264
+ const head = props.head || [];
265
+ const rows = props.rows || [];
266
+ return e('div', { className: 'al-doc-table-wrap' },
267
+ e('table', { className: 'al-doc-table' },
268
+ e('thead', null,
269
+ e('tr', null, head.map(function (cell, index) {
270
+ return e('th', { key: index }, cell);
271
+ }))
272
+ ),
273
+ e('tbody', null, rows.map(function (row, rowIndex) {
274
+ return e('tr', { key: rowIndex }, row.map(function (cell, cellIndex) {
275
+ return e('td', { key: cellIndex }, cell);
276
+ }));
277
+ }))
278
+ )
279
+ );
280
+ }
281
+
282
+ function DocSec(props) {
283
+ return e('section', { className: 'al-doc-sec' },
284
+ e('div', { className: 'al-doc-sec-head' },
285
+ e('div', { className: 'al-doc-sec-kicker' },
286
+ e('span', { className: 'al-doc-sec-no' }, props.no),
287
+ e('h2', { className: 'al-doc-sec-title' }, props.title)
288
+ ),
289
+ props.lead ? e('p', { className: 'al-doc-sec-lead' }, props.lead) : null
290
+ ),
291
+ props.children
292
+ );
293
+ }
294
+
295
+ function DocFlowChain(props) {
296
+ const steps = props.steps || [];
297
+ const nodes = [];
298
+ steps.forEach(function (step, index) {
299
+ if (index > 0) {
300
+ nodes.push(e('span', { key: 'arrow-' + index, className: 'al-doc-flowchain-arrow' }, '→'));
301
+ }
302
+ nodes.push(e('span', { key: 'step-' + index, className: 'al-doc-flowchain-step' }, step));
303
+ });
304
+ return e('div', { className: 'al-doc-flowchain' }, nodes);
305
+ }
306
+
307
+ function DocSteps(props) {
308
+ const items = props.items || [];
309
+ return e('ol', { className: 'al-doc-steps' }, items.map(function (item, index) {
310
+ return e('li', { key: index, className: 'al-doc-step' }, item);
311
+ }));
312
+ }
313
+
314
+ function DocChip(props) {
315
+ return e('span', { className: 'al-doc-chip' },
316
+ e('span', { className: 'al-doc-chip-dot' }),
317
+ props.children
318
+ );
319
+ }
320
+
321
+ /* ── 图解:最小循环 ──────────────────────────────────────── */
322
+
323
+ function DocLoopDiagram() {
324
+ function node(title, sub, extraClass) {
325
+ return e('div', { className: 'al-doc-loop-node' + (extraClass ? ' ' + extraClass : '') },
326
+ e('div', { className: 'al-doc-loop-node-title' }, title),
327
+ e('div', { className: 'al-doc-loop-node-sub' }, sub)
328
+ );
329
+ }
330
+
331
+ return e('div', { className: 'al-doc-loop' },
332
+ e('div', { className: 'al-doc-loop-row' },
333
+ node('用户输入', '一句话进入插件', 'al-doc-loop-node-input'),
334
+ e('span', { className: 'al-doc-loop-arrow' }, '→'),
335
+ node('组装上下文', 'system + 摘要 + 记忆 + 历史 + 输入'),
336
+ e('span', { className: 'al-doc-loop-arrow' }, '→'),
337
+ e('div', { className: 'al-doc-loop-core' },
338
+ e('div', { className: 'al-doc-loop-core-node' }, '模型决策'),
339
+ e('div', { className: 'al-doc-loop-core-swap' }, '⇄', e('small', null, '结果回填')),
340
+ e('div', { className: 'al-doc-loop-core-node' }, '执行工具')
341
+ ),
342
+ e('span', { className: 'al-doc-loop-arrow' }, '→'),
343
+ node('最终回答', '流式输出,turn 结束')
344
+ ),
345
+ e('div', { className: 'al-doc-loop-foot' },
346
+ '模型与工具之间的这一对箭头,就是 Agent 的心脏:模型只会「提议」调用,真正执行并回填结果的是宿主代码;循环最多 ',
347
+ e(DocInline, null, 'LAB_MAX_STEPS'),
348
+ ' 步。'
349
+ )
350
+ );
351
+ }
352
+
353
+ /* ── 图解:上下文预算 ────────────────────────────────────── */
354
+
355
+ const DOC_CONTEXT_SEGMENTS = [
356
+ { id: 'system', label: 'system', tokens: 118, note: '角色与规则' },
357
+ { id: 'summary', label: 'summary', tokens: 62, note: '历史压缩摘要' },
358
+ { id: 'memory', label: 'memory', tokens: 34, note: '本轮召回的长记忆(≤ 3 条)' },
359
+ { id: 'recent', label: 'recent', tokens: 152, note: '最近的消息' },
360
+ { id: 'input', label: 'input', tokens: 20, note: '本轮输入' },
361
+ ];
362
+
363
+ function DocBudgetBar() {
364
+ const limit = 1000;
365
+ const used = DOC_CONTEXT_SEGMENTS.reduce(function (sum, item) {
366
+ return sum + item.tokens;
367
+ }, 0);
368
+ const markerLeft = 90;
369
+
370
+ return e('div', { className: 'al-doc-budget' },
371
+ e('div', { className: 'al-doc-budget-head' },
372
+ e('span', null, '本轮上下文拼装示例(教学尺度)'),
373
+ e('span', { className: 'al-doc-budget-used' },
374
+ used + ' / ' + limit + ' tok · ' + Math.round((used / limit) * 100) + '%'
375
+ )
376
+ ),
377
+ e('div', { className: 'al-doc-bar' },
378
+ DOC_CONTEXT_SEGMENTS.map(function (segment) {
379
+ return e('div', {
380
+ key: segment.id,
381
+ className: 'al-doc-bar-seg al-doc-bar-seg-' + segment.id,
382
+ style: { width: ((segment.tokens / limit) * 100).toFixed(2) + '%' },
383
+ title: segment.note,
384
+ });
385
+ }),
386
+ e('div', { className: 'al-doc-bar-marker', style: { left: markerLeft + '%' }, title: '压缩阈值:90% 预算' })
387
+ ),
388
+ e('div', { className: 'al-doc-legend' }, DOC_CONTEXT_SEGMENTS.map(function (segment) {
389
+ return e('span', { key: segment.id, className: 'al-doc-legend-item' },
390
+ e('span', {
391
+ className: 'al-doc-legend-dot',
392
+ style: { background: 'var(--al-doc-bar-seg-' + segment.id + ', var(--al-doc-brand))' },
393
+ }),
394
+ e('span', null, segment.label),
395
+ e('span', { className: 'al-doc-legend-tok' }, segment.tokens + ' tok'),
396
+ e('span', { className: 'al-doc-legend-tok' }, '· ' + segment.note)
397
+ );
398
+ })),
399
+ e('p', { className: 'al-doc-formula-note' },
400
+ '竖线是 90% 压缩阈值:示例中的 ',
401
+ used,
402
+ ' tok 已越过 90%(378 tok),下一次模型调用前会触发压缩。数字仅用于演示,实际值随对话变化。'
403
+ )
404
+ );
405
+ }
406
+
407
+ /* ── 第 01 节:什么是 Agent ─────────────────────────────── */
408
+
409
+ function DocSectionWhat(props) {
410
+ return e(DocSec, {
411
+ no: props.no,
412
+ title: '一句话:Agent = LLM + 工具 + 循环 + 记忆',
413
+ lead: '去掉所有产品包装,一个 agent 就是四个部件的组合:模型负责决策,工具负责行动,循环负责推进,记忆负责延续。缺一件,能力就退化一次。',
414
+ },
415
+ e('div', { className: 'al-doc-formula' },
416
+ e('span', { className: 'al-doc-formula-term' }, 'Agent'),
417
+ e('span', { className: 'al-doc-formula-op' }, '='),
418
+ e('span', { className: 'al-doc-formula-term' }, 'LLM'),
419
+ e('span', { className: 'al-doc-formula-op' }, '×'),
420
+ e('span', { className: 'al-doc-formula-term' }, '工具'),
421
+ e('span', { className: 'al-doc-formula-op' }, '×'),
422
+ e('span', { className: 'al-doc-formula-term' }, '循环'),
423
+ e('span', { className: 'al-doc-formula-op' }, '×'),
424
+ e('span', { className: 'al-doc-formula-term' }, '记忆')
425
+ ),
426
+ e('p', { className: 'al-doc-formula-note' },
427
+ '为什么是乘而不是加:任何一项缺失,整体都会明显退化 —— 下面的表格就是每个部件缺席时的样子。'
428
+ ),
429
+ e('p', { className: 'al-doc-p' },
430
+ '在真实产品里,这四个部件还会配上权限、沙箱、日志、计费等等,但那些是工程外壳。理解 agent,先把这四个词放在心里。'
431
+ ),
432
+ e(DocGrid, { cols: 'auto' },
433
+ e(DocCard, { icon: 'LLM', title: '模型:决策' },
434
+ '读入上下文,输出下一步:一句回答,或者一次工具调用。本插件用规则驱动的模拟模型(',
435
+ e(DocInline, null, '30-llm.js'),
436
+ ')作为默认模型,离线、可复现;也可以在终端右上角切到「真实」,调用 DSH 当前登录的 DeepSeek 模型。'
437
+ ),
438
+ e(DocCard, { icon: 'Fn', title: '工具:行动' },
439
+ '模型自己不能查时间、不能算数、更写不了记忆。工具是它对外部世界唯一的通道。本插件有 8 个:calculator、clock、dice_roll、random_pick、password_generate、unit_convert、memory_write、memory_search。'
440
+ ),
441
+ e(DocCard, { icon: '↻', title: '循环:推进' },
442
+ '工具结果写回消息后再次调用模型,直到拿到最终回答。本插件封顶 ',
443
+ e(DocInline, null, 'LAB_MAX_STEPS = 4'),
444
+ ' 步,超过就报错,避免「死循环烧钱」。'
445
+ ),
446
+ e(DocCard, { icon: 'Mem', title: '记忆:延续' },
447
+ '模型每次调用都从零开始,记忆由宿主在调用前注入上下文。本插件的长期记忆存在 localStorage,跨会话共享。'
448
+ )
449
+ ),
450
+ e(DocTable, {
451
+ head: ['缺哪一块', '会变成什么'],
452
+ rows: [
453
+ ['只有 LLM', '会聊天但不能做事 —— 普通聊天机器人。'],
454
+ ['有工具但没有循环', '只能调用一次,无法根据结果继续 —— 函数调用演示。'],
455
+ ['没有记忆', '每次对话都从零开始 —— 失忆的助手。'],
456
+ ['没有 LLM(纯脚本)', '行为完全可预测,但没有泛化能力 —— 自动化脚本。'],
457
+ ],
458
+ }),
459
+ e(DocNote, { kind: 'tip', icon: '✓', title: '带着问题往下读' },
460
+ '接下来每一节都对应一个部件:最小循环(循环)、一次输入的旅程(四个部件如何串起来)、上下文与压缩、记忆、会话、工具协议。读完后,去「体验」页跑一句 ',
461
+ e(DocInline, null, '算一下 (12+30)*3 然后记住结果'),
462
+ ',你会看到两次工具调用和一次记忆写入。'
463
+ )
464
+ );
465
+ }
466
+
467
+ /* ── 第 02 节:最小循环 ─────────────────────────────────── */
468
+
469
+ const DOC_LOOP_PSEUDO = [
470
+ 'messages = [system, ...history, input]',
471
+ '',
472
+ 'for (let step = 1; step <= LAB_MAX_STEPS; step++) {',
473
+ ' const reply = await model(messages, LAB_TOOLS); // ① 决策',
474
+ ' if (reply.type === "final") return reply.text; // ② 收敛,结束',
475
+ ' const result = await runTool(reply.name, reply.args); // ③ 执行工具',
476
+ ' messages.push({ role: "tool", ...result }); // ④ 回填,进入下一圈',
477
+ '}',
478
+ '',
479
+ 'throw new Error("超过步数上限仍未收敛"); // 兜底:不会无限循环',
480
+ ].join('\n');
481
+
482
+ function DocSectionLoop(props) {
483
+ return e(DocSec, {
484
+ no: props.no,
485
+ title: '最小循环:一个环,两个出口',
486
+ lead: '把产品细节全部拿掉,agent 的核心就是下面这个环。本插件每一轮对话都是它的展开,右侧检查器的「流转」页签会逐帧记录它。',
487
+ },
488
+ e(DocLoopDiagram),
489
+ e('p', { className: 'al-doc-p' },
490
+ '循环的出口只有两个:模型返回 ',
491
+ e(DocInline, null, 'final'),
492
+ '(正常结束),或者超过 ',
493
+ e(DocInline, null, 'LAB_MAX_STEPS'),
494
+ '(错误结束)。除此之外没有「悄悄停下」的路径 —— 每一轮都留下可检查的轨迹。'
495
+ ),
496
+ e(DocCode, { label: '伪代码 · 最小 agent 循环', code: DOC_LOOP_PSEUDO }),
497
+ e(DocNote, { icon: 'i', title: '三个必须分清的词' },
498
+ '模型这一步的输出只有两种:调工具(',
499
+ e(DocInline, null, "type: 'tool_call'"),
500
+ ')或最终回答(',
501
+ e(DocInline, null, "type: 'final'"),
502
+ ')。本插件把「计划」压缩成这两种形状,引擎才能用同一个循环处理所有输入。'
503
+ ),
504
+ e('p', { className: 'al-doc-p' },
505
+ '真实 LLM 的循环与此完全同构,区别只在 ',
506
+ e(DocInline, null, 'model()'),
507
+ ' 内部是一次网络请求(可能是流式 SSE),而不是本地规则函数。'
508
+ )
509
+ );
510
+ }
511
+
512
+ /* ── 第 03 节:一次输入的旅程 ───────────────────────────── */
513
+
514
+ const DOC_JOURNEY = [
515
+ {
516
+ id: 'input',
517
+ title: '记录输入',
518
+ text: '把用户输入变成一条 user 消息,创建本轮 turn,标记开始时间。',
519
+ where: '终端出现用户行;检查器「流转」出现 input 节点。',
520
+ },
521
+ {
522
+ id: 'context',
523
+ title: '组装上下文',
524
+ text: '按 system → summary → memory(≤ 3 条召回)→ recent → input 的顺序拼出这一轮真正发给模型的内容。',
525
+ where: '检查器「上下文」的堆叠条与各段 token 数更新。',
526
+ },
527
+ {
528
+ id: 'compress',
529
+ title: '需要时压缩',
530
+ text: '若组装后已用 token 超过预算的 90%,把除最近 4 条之外的消息折叠进累计摘要,再重新组装。',
531
+ where: '终端出现 ⇣ 压缩行;「上下文」的 summary 与 used 发生变化。',
532
+ },
533
+ {
534
+ id: 'model',
535
+ title: '模型决策',
536
+ text: '把 messages 和工具 schema 一起发给模型。模型返回一次工具调用,或者最终回答;这一步可能发生多次(最多 4 步)。',
537
+ where: '「流转」的 model 节点可展开看到 messages、toolSchemas 与 response。',
538
+ },
539
+ {
540
+ id: 'tool',
541
+ title: '执行工具',
542
+ text: '宿主在注册表里按名字找到函数,校验参数后执行,把 { ok, result, display } 写回一条 tool 消息。',
543
+ where: '终端出现 ⚙ 调用行与 ← 结果行;「流转」多一个 tool 节点。',
544
+ },
545
+ {
546
+ id: 'memory',
547
+ title: '写入记忆(可选)',
548
+ text: '如果本轮调用了 memory_write,长期记忆会新增或更新一条;下一轮组装上下文时可能被关键词召回。',
549
+ where: '「记忆」页签出现新条目;终端出现 ◈ 记忆已保存。',
550
+ },
551
+ {
552
+ id: 'output',
553
+ title: '输出回答',
554
+ text: '模型给出最终回答,按字符流式播报;turn 结束,统计本轮 tokens、工具次数,并持久化会话。',
555
+ where: '终端输出 agent 行;「流转」所有节点变为完成态。',
556
+ },
557
+ ];
558
+
559
+ function DocSectionJourney(props) {
560
+ return e(DocSec, {
561
+ no: props.no,
562
+ title: '一次输入在插件里经历了什么',
563
+ lead: '每个阶段都对应 turn 里的一个 stage 节点。把它们和右侧检查器的三个页签对上号,你就拿到了这个插件的调试地图。',
564
+ },
565
+ e('ol', { className: 'al-doc-journey' }, DOC_JOURNEY.map(function (stage, index) {
566
+ return e('li', { key: stage.id, className: 'al-doc-journey-item' },
567
+ e('span', { className: 'al-doc-journey-dot' }),
568
+ e('div', { className: 'al-doc-journey-head' },
569
+ e('span', { className: 'al-doc-stage' }, stage.id),
570
+ e('span', { className: 'al-doc-journey-title' }, (index + 1) + '. ' + stage.title)
571
+ ),
572
+ e('p', { className: 'al-doc-journey-text' }, stage.text),
573
+ e('p', { className: 'al-doc-journey-where' },
574
+ e('b', null, '去看:'),
575
+ stage.where
576
+ )
577
+ );
578
+ })),
579
+ e('div', { className: 'al-doc-grid al-doc-grid-3' },
580
+ e(DocCard, { icon: 'msg', title: 'messages:消息' },
581
+ '真正的对话记录(role + content),会持久化,是「给模型的原件」。user / assistant / tool 三种角色交替出现。'
582
+ ),
583
+ e(DocCard, { icon: 'ctx', title: 'context:上下文' },
584
+ '本轮真正发出去的拼装结果,用完即弃;同一份消息在不同预算与记忆下会拼出不同的上下文。'
585
+ ),
586
+ e(DocCard, { icon: 'ui', title: 'transcript:终端行' },
587
+ '给人看的展示层(banner / user / llm / tool / compress / agent…),不会发给模型,只负责让过程可读。'
588
+ )
589
+ ),
590
+ e(DocNote, { icon: 'i', title: '单向数据流' },
591
+ '引擎从不直接改 UI:它只广播事件(',
592
+ e(DocInline, null, 'stage'),
593
+ ' / ',
594
+ e(DocInline, null, 'token'),
595
+ ' / ',
596
+ e(DocInline, null, 'memory'),
597
+ ' / ',
598
+ e(DocInline, null, 'compression'),
599
+ ' / ',
600
+ e(DocInline, null, 'turn_end'),
601
+ ' …),界面收到后重新读取 ',
602
+ e(DocInline, null, 'snapshot()'),
603
+ ' 渲染。所以 UI 永远不会「自己编造」状态,看到的都是引擎的真实快照。'
604
+ )
605
+ );
606
+ }
607
+
608
+ /* ── 第 04 节:上下文与预算 ─────────────────────────────── */
609
+
610
+ function DocSectionContext(props) {
611
+ return e(DocSec, {
612
+ no: props.no,
613
+ title: '上下文:一份有预算的临时视图',
614
+ lead: '上下文不是「聊天记录」,而是每次调用前重新拼出来的一份视图。它的大小有预算,超了就要压缩 —— 这是所有 agent 框架最关键的工程问题之一。',
615
+ },
616
+ e('p', { className: 'al-doc-p' },
617
+ '每次模型调用前,引擎按固定顺序拼装上下文,并同时更新检查器里的分段统计:'
618
+ ),
619
+ e(DocFlowChain, { steps: ['system', 'summary', 'memory', 'recent', 'input'] }),
620
+ e(DocList, {
621
+ items: [
622
+ [e('strong', null, 'system:'), '固定的角色与规则(', e(DocInline, null, 'LAB_SYSTEM_PROMPT'), '),告诉模型它是谁、能做什么。'],
623
+ [e('strong', null, 'summary:'), '历史消息被压缩后的累计摘要,只有发生过压缩才存在。'],
624
+ [e('strong', null, 'memory:'), '按本轮输入关键词召回的长期记忆,最多 3 条,作为 system 消息注入。'],
625
+ [e('strong', null, 'recent:'), '当前会话最近的消息;压缩后「新」消息与旧摘要并存。'],
626
+ [e('strong', null, 'input:'), '本轮用户输入。'],
627
+ ],
628
+ }),
629
+ e(DocBudgetBar),
630
+ e('p', { className: 'al-doc-p' },
631
+ '本插件用 ',
632
+ e(DocInline, null, 'estimateTokens'),
633
+ ' 估算 token:CJK 约 1 token/字,其他字符约 4 字符/token,向上取整。真实模型用 BPE 分词,数字会不同,但用来建立「预算」直觉足够了。教学预算 ',
634
+ e(DocInline, null, 'LAB_BUDGET = 1000'),
635
+ ' 是等比缩小过的;真实模型通常从几万到上百万 token。'
636
+ ),
637
+ e('p', { className: 'al-doc-p' },
638
+ e('strong', null, '为什么要在意预算?'),
639
+ '窗口有硬上限,超了直接报错;token 就是钱,输入越长越贵;无关内容越多,模型越容易抓错重点(真实系统里也存在长上下文中段信息被忽略的现象);输入越长,首字延迟也越高。'
640
+ ),
641
+ e('DocCode', {
642
+ label: 'turn.stages 里的一个节点(示意)',
643
+ code: [
644
+ '{',
645
+ ' id: "stage_3",',
646
+ ' kind: "tool",',
647
+ ' title: "调用 calculator",',
648
+ ' meta: "1 次工具调用 · 12 tok",',
649
+ ' status: "done",',
650
+ ' detail: { name: "calculator", args: { expression: "(12+30)*3" }, ok: true, display: "126" },',
651
+ ' at: 1762400000000,',
652
+ '}',
653
+ ].join('\n'),
654
+ }),
655
+ e(DocNote, { kind: 'tip', icon: '⇣', title: '压缩:预算快满时的折叠' },
656
+ '触发条件:模型调用前 ',
657
+ e(DocInline, null, 'used > limit * 0.9'),
658
+ '(默认 1000 的 90% 触发;页面可调 1K / 2K / 8K / 32K / 128K / 1M;一轮结束时若已超过 90% 也会立即压缩,不必等下一条消息)。做法:把除最近 ',
659
+ e(DocInline, null, 'LAB_RECENT_KEEP = 4'),
660
+ ' 条之外的消息折叠成结构化摘要:', e('strong', null, '关键内容'), '(用户意图 / 助手结论 / 工具结果)、', e('strong', null, '未完成任务清单'), '、', e('strong', null, '下一个任务'), '。同时记录压缩次数与省下的 token。终端会看到形如 ',
661
+ e(DocInline, null, '⇣ 上下文已压缩 · 186 → 92 tok'),
662
+ ' 的行。手动触发(',
663
+ e(DocInline, null, 'compact()'),
664
+ ')则无条件压缩;消息不足 3 条时只加一行提示。'
665
+ ),
666
+ e(DocNote, { kind: 'warn', icon: '!', title: '压缩是有损的' },
667
+ '摘要会丢掉细节。所以真正重要的事实(「项目代号是 Orion」)应该写进长期记忆,而不是指望它活过下一次压缩。这也是真实 agent 产品里「记忆」和「压缩」要分开设计的原因。'
668
+ )
669
+ );
670
+ }
671
+
672
+ /* ── 第 05 节:记忆 ─────────────────────────────────────── */
673
+
674
+ function DocSectionMemory(props) {
675
+ return e(DocSec, {
676
+ no: props.no,
677
+ title: '记忆:给无状态的模型装外挂',
678
+ lead: '模型本身不记得任何事,每次调用都是全新开始。所谓记忆,是宿主替它保存、并在调用前重新注入的信息。本插件把它分成两层。',
679
+ },
680
+ e(DocGrid, { cols: 2 },
681
+ e(DocCard, { icon: '短', title: '短期记忆:当前会话的消息' },
682
+ '就是 messages 里那段历史。它随会话生灭,受上下文预算和压缩影响:压缩后旧消息变成摘要,细节不再完整。'
683
+ ),
684
+ e(DocCard, { icon: '长', title: '长期记忆:跨会话的键值事实' },
685
+ '存在 localStorage,结构是 { id, key, value, source, sessionId, createdAt, updatedAt }。它独立于会话,删会话不会删记忆。'
686
+ )
687
+ ),
688
+ e(DocFlowChain, { steps: ['用户输入', '关键词召回 ≤ 3 条', '注入 system', '模型据此回答'] }),
689
+ e(DocP, null,
690
+ '长期记忆有两条读写路径:',
691
+ e('strong', null, '自动召回'),
692
+ '(引擎在每轮组装上下文时按关键词匹配,最多 3 条)和',
693
+ e('strong', null, '工具读写'),
694
+ '(模型通过 ',
695
+ e(DocInline, null, 'memory_write'),
696
+ ' / ',
697
+ e(DocInline, null, 'memory_search'),
698
+ ' 主动存取)。前者保证「该记得的会被想起」,后者让模型能判断「我现在需要查记忆」。'
699
+ ),
700
+ e(DocCode, {
701
+ label: '一条长期记忆(示意)',
702
+ code: [
703
+ '{',
704
+ ' id: "mem_1",',
705
+ ' key: "项目代号",',
706
+ ' value: "Orion",',
707
+ ' source: "memory_write",',
708
+ ' sessionId: "s_1",',
709
+ ' createdAt: 1762400000000,',
710
+ ' updatedAt: 1762400000000,',
711
+ '}',
712
+ ].join('\n'),
713
+ }),
714
+ e(DocNote, { kind: 'tip', icon: '✦', title: '试一试' },
715
+ '在「体验」页输入 ',
716
+ e(DocInline, null, '记住 我的项目代号是 Orion'),
717
+ ',然后新建一个会话,问 ',
718
+ e(DocInline, null, '我的项目代号是什么'),
719
+ '。会话是新的、消息是空的,但记忆还在 —— 这就是短期记忆与长期记忆的区别。'
720
+ ),
721
+ e(DocP, null,
722
+ '本插件的关键词召回简单、可解释,适合教学。真实系统通常升级为向量检索 / 知识库(RAG),还要考虑去重、时效、权限与隐私。但接口位置是一样的:查询 → 检索 → 注入。'
723
+ ),
724
+ );
725
+ }
726
+
727
+ /* ── 第 06 节:会话 ─────────────────────────────────────── */
728
+
729
+ function DocSectionSession(props) {
730
+ return e(DocSec, {
731
+ no: props.no,
732
+ title: '会话:换上下文,不换记忆',
733
+ lead: '会话是一段独立的消息历史。新建会话 = 把上下文清空重来;长期记忆则继续共享。这个划分决定了「什么该隔离、什么该延续」。',
734
+ },
735
+ e(DocList, {
736
+ items: [
737
+ [e('strong', null, '为什么要分会话:'), '不同任务互不污染;上下文更短更省 token;随时可以回到旧线索继续。'],
738
+ [e('strong', null, '生命周期:'), '新建 → 切换 → 删除。删除当前会话时,自动切到下一个或新建一个,不会把你留在空白状态。'],
739
+ [e('strong', null, '隔离与共享:'), '消息、transcript 按会话隔离;长期记忆全局共享。'],
740
+ [e('strong', null, '持久化:'), 'turn 结束、记忆变化、会话操作后写入 storage;刷新页面或重启后仍在。'],
741
+ ],
742
+ }),
743
+ e(DocP, null, '本插件为了保证浏览器里也稳定可控,设了几道上限:最多保留 20 个会话;每个会话最多 120 条消息(超出丢最旧的);每个会话 transcript 最多 200 行。存储键是 ',
744
+ e(DocInline, null, 'dsh.agent-lab.v1'),
745
+ ',在「会话」栏的入口里可以清空并重建。'
746
+ ),
747
+ e(DocCode, {
748
+ label: '会话对象(示意)',
749
+ code: [
750
+ '{',
751
+ ' id: "s_1",',
752
+ ' title: "算一下 (12+30)*3",',
753
+ ' createdAt: 1762400000000,',
754
+ ' updatedAt: 1762400001200,',
755
+ ' messageCount: 6,',
756
+ ' tokens: 214,',
757
+ ' compressions: 0,',
758
+ ' turns: 1,',
759
+ '}',
760
+ ].join('\n'),
761
+ }),
762
+ e(DocNote, { icon: 'i', title: '真实系统里的会话' },
763
+ '生产环境还要处理:多端同步、并发 turn、会话分享/导出、审计与合规、跨设备记忆同步。这些是工程问题,不改变「会话隔离上下文」的基本模型。'
764
+ )
765
+ );
766
+ }
767
+
768
+ /* ── 第 07 节:工具协议 ─────────────────────────────────── */
769
+
770
+ const DOC_TOOL_SHAPE = [
771
+ 'const LAB_TOOLS = [{',
772
+ ' name: "calculator",',
773
+ ' description: "安全计算四则/幂/括号表达式",',
774
+ ' parameters: {',
775
+ ' type: "object",',
776
+ ' properties: { expression: { type: "string", description: "要计算的表达式" } },',
777
+ ' required: ["expression"],',
778
+ ' },',
779
+ ' // 可以是 async;出错不要 throw,返回 { ok: false } 让模型看到错误',
780
+ ' run(args, ctx) {',
781
+ ' return { ok: true, result: 126, display: "126" };',
782
+ ' },',
783
+ '}];',
784
+ ].join('\n');
785
+
786
+ const DOC_TOOL_FLOW = [
787
+ '// ① 模型的这一步输出(本插件由 30-llm.js 产生,真实 LLM 由 API 返回)',
788
+ '{ type: "tool_call", name: "calculator", args: { expression: "(12+30)*3" } }',
789
+ '',
790
+ '// ② 宿主在注册表里查找、执行,并把结果写回一条 tool 消息',
791
+ 'const tool = LAB_TOOLS.find((item) => item.name === call.name);',
792
+ 'const outcome = await tool.run(call.args, ctx);',
793
+ 'messages.push({ role: "tool", toolName: call.name, content: outcome.display });',
794
+ '',
795
+ '// ③ 带着工具结果进入下一圈循环,直到模型返回 final',
796
+ ].join('\n');
797
+
798
+ function DocSectionProtocol(props) {
799
+ return e(DocSec, {
800
+ no: props.no,
801
+ title: '工具调用协议:工具只是函数',
802
+ lead: '工具调用不神秘:模型说出「想调哪个函数、参数是什么」,宿主照着执行,再把结果贴回对话。所有 agent 框架的工具协议都是这句话的不同写法。',
803
+ },
804
+ e(DocSteps, {
805
+ items: [
806
+ [e('strong', null, '声明:'), '宿主把工具名、描述与 JSON Schema 随请求一起发给模型,让模型知道「有什么、怎么调」。'],
807
+ [e('strong', null, '决策:'), '模型输出一次工具调用(name + JSON 参数),或者直接给出最终回答。'],
808
+ [e('strong', null, '校验:'), '宿主检查参数能否解析、字段类型是否符合 schema。模型给的参数永远不可信。'],
809
+ [e('strong', null, '执行:'), '按名字找到注册表里的函数,调用 ', e(DocInline, null, 'run(args, ctx)'), '。'],
810
+ [e('strong', null, '回填:'), '把 ', e(DocInline, null, '{ ok, result, display }'), ' 作为 tool 消息追加进对话;display 给终端看,result 给程序用。'],
811
+ [e('strong', null, '循环:'), '回到第 1 步再次调用模型,直到它给出 final。'],
812
+ ],
813
+ }),
814
+ e(DocCode, { label: 'JavaScript · 工具注册的形状', code: DOC_TOOL_SHAPE }),
815
+ e(DocCode, { label: 'JavaScript · 模型提议 → 宿主执行', code: DOC_TOOL_FLOW }),
816
+ e('p', { className: 'al-doc-p' },
817
+ e('strong', null, '为什么偏偏是「函数 + JSON Schema」?'),
818
+ '因为模型只会输出文本,而 JSON 是它最擅长的结构化文本;schema 让「能调什么、参数长什么样」变成可机器校验的契约。函数则是最小、可测试、可 mock、可替换的单元。'
819
+ ),
820
+ e(DocList, {
821
+ items: [
822
+ [e('strong', null, '安全边界:'), '模型只「提议」,执行权始终在宿主。危险工具要加权限与沙箱 —— 弹窗确认不是可靠的安全模型,隔离执行环境才是。'],
823
+ [e('strong', null, '错误即数据:'), '工具失败返回 ok:false 而不是抛异常,模型能看到错误并自行纠正。本插件的工具实现都遵守这一点。'],
824
+ [e('strong', null, '可组合:'), '新增工具 = 注册一个函数 + 写清描述,循环和协议不用改。'],
825
+ ],
826
+ }),
827
+ e(DocTable, {
828
+ head: ['工具', '作用', '关键参数'],
829
+ rows: [
830
+ ['calculator', '安全计算四则 / 幂 / 括号表达式', 'expression(字符串)'],
831
+ ['clock', '返回当前时间/日期', '无'],
832
+ ['dice_roll', '掷骰子,返回点数与合计', 'sides, count'],
833
+ ['random_pick', '从候选项里随机选一个', 'items(数组)'],
834
+ ['password_generate', '生成随机密码', 'length, numbers, symbols'],
835
+ ['unit_convert', '长度/重量/温度/体积/速度/数据换算', 'value, from, to'],
836
+ ['memory_write', '写入或更新一条长期记忆', 'key, value'],
837
+ ['memory_search', '按关键词检索长期记忆', 'query'],
838
+ ],
839
+ }),
840
+ e(DocNote, { icon: 'i', title: '和真实 API 的关系' },
841
+ 'OpenAI、Anthropic 等模型的 tools / function calling 字段,就是这套协议的标准版本:请求带工具声明,响应里带 name + JSON 参数。本插件把它收敛成 ',
842
+ e(DocInline, null, "type: 'tool_call'"),
843
+ ' 和 ',
844
+ e(DocInline, null, "type: 'final'"),
845
+ ' 两种形状,方便教学。'
846
+ )
847
+ );
848
+ }
849
+
850
+ /* ── 第 08 节:pi 的极简哲学 ────────────────────────────── */
851
+
852
+ function DocSectionPi(props) {
853
+ return e(DocSec, {
854
+ no: props.no,
855
+ title: 'pi 的极简哲学:小核心,大扩展',
856
+ lead: 'pi(pi.dev)是一个刻意做小的 coding agent harness。它的价值主张不是功能多,而是可理解、可修改、可观测 —— 这正好是本插件的设计参考。',
857
+ },
858
+ e(DocList, {
859
+ items: [
860
+ [e('strong', null, '小核心:'), '系统提示很短,内置工具只有 read / write / edit / bash 四个;MCP、子 agent、计划 UI、待办等交给扩展和用户习惯,而不是默认塞进来。'],
861
+ [e('strong', null, '上下文优先:'), '作者认为 harness 最关键的部件是上下文管理与压缩,而不是继续叠加功能 —— 上下文的质量直接决定模型表现。'],
862
+ [e('strong', null, '最大可扩展:'), '自定义工具、自定义 UI、自定义压缩、权限门、模型 provider、树状会话都留成接口。用一句话概括:让系统更容易改,而不是更容易欣赏。'],
863
+ [e('strong', null, '可观测、可控:'), '反对黑箱;对「审批弹窗 = 安全」持怀疑态度,认为容器 / 沙箱是更可靠的边界。'],
864
+ ],
865
+ }),
866
+ e(DocP, null,
867
+ '本插件借了它三点:',
868
+ e('strong', null, '工具克制'),
869
+ '(只保留四个能讲清楚的能力)、',
870
+ e('strong', null, '全部源码可读'),
871
+ '(构建时内嵌 ',
872
+ e(DocInline, null, 'SOURCES'),
873
+ ',「源码」页所见即所跑)、',
874
+ e('strong', null, '复杂度放在看得见的地方'),
875
+ '(常量集中在 ',
876
+ e(DocInline, null, '00-core.js'),
877
+ ',循环、预算、压缩都在一个文件里)。'
878
+ ),
879
+ e(DocNote, { kind: 'warn', icon: '!', title: '极简不是免费' },
880
+ '它把复杂度转移给了模型和用户。真实生产环境仍然需要权限、沙箱、审计、限流与可观测性;教学版有意省略这些外壳,只保留主干,以便看清 agent 到底在做什么。'
881
+ ),
882
+ e(DocNote, { icon: '⌘', title: '出处说明' },
883
+ '以上是对 pi 作者 Mario Zechner 公开分享的设计取向的提炼(pi.dev),不是 pi 的文档,也没有复刻其代码。本插件的工具与 pi 的工具完全不同,相同的只是「少而可读」的取向。'
884
+ )
885
+ );
886
+ }
887
+
888
+ /* ── 第 09 节:对照真实 DSH ─────────────────────────────── */
889
+
890
+ function DocSectionHarness(props) {
891
+ return e(DocSec, {
892
+ no: props.no,
893
+ title: '本插件与真实 DSH Agent 的对应',
894
+ lead: '这个页面本身就是一个真实的 DSH Web 客户端插件。把教学插件和真实 DSH Agent 摆在一起,会发现骨架完全一样,差别主要在规模与工程保障。',
895
+ },
896
+ e(DocTable, {
897
+ head: ['环节', '本插件(教学版)', '真实 DSH Agent'],
898
+ rows: [
899
+ ['决策者', '模拟模型(30-llm.js,离线可复现)或真实 DeepSeek(45-real-client.js + Host 路由)', '云端或本地大模型,流式返回并支持工具调用'],
900
+ ['工具', 'calculator / clock / dice_roll / random_pick / password_generate / unit_convert / memory_write / memory_search', '终端命令、读写文件、搜索、网页等真实工具,带权限与沙箱'],
901
+ ['循环', 'createLab 的 turn 循环,LAB_MAX_STEPS = 4', 'Agent 运行时驱动同一套循环,步数由配置与预算约束'],
902
+ ['上下文', 'system + summary + memory + recent + input,默认 1000 token(可调)', 'system prompt + 技能 + 工具说明 + 历史,以万计的 token 窗口'],
903
+ ['压缩', '90% 阈值触发,折叠旧消息进摘要', '窗口将满时摘要/裁剪,并持久化'],
904
+ ['记忆', 'localStorage 键值 + 关键词召回', '会话存储、记忆机制与检索'],
905
+ ['会话', '最多 20 个,每会话 120 条消息', '持久化会话、并发与多端'],
906
+ ['界面', '本插件注册到 DSH 的 Web UI', '同一套 DSH Web 客户端插件系统'],
907
+ ],
908
+ }),
909
+ e('p', { className: 'al-doc-p' }, e('strong', null, '这个插件是怎么跑进 DSH 的:')),
910
+ e(DocSteps, {
911
+ items: [
912
+ ['package.json', ' 声明 ',
913
+ e(DocInline, null, 'dsh.bundle.patch'),
914
+ ' 与 ',
915
+ e(DocInline, null, 'dsh.client'),
916
+ ',告诉 DSH 这是一个 Web 客户端 bundle。'],
917
+ ['build.mjs', ' 把 ',
918
+ e(DocInline, null, 'src/*.js'),
919
+ ' 按编号拼成一个 Client factory,并把每个源文件以 JSON 字符串注入为 ',
920
+ e(DocInline, null, 'SOURCES'),
921
+ '。'],
922
+ ['DSH Web', ' 通过 ',
923
+ e(DocInline, null, 'window.__ModuleLoader__'),
924
+ ' 加载这个 factory,并提供 React 等依赖。'],
925
+ ['95-apply.js', ' 把「Agent 实验室」注册到左侧栏与主区域 slot。'],
926
+ ['90-page.js', ' 组装页面外壳与会话栏;「原理」「源码」页签分别渲染本文件与 80-source.js。'],
927
+ ],
928
+ }),
929
+ e(DocNote, { kind: 'tip', icon: '◎', title: '所见即所跑' },
930
+ '你在「源码」页看到的字符串,就是构建时注入的 ',
931
+ e(DocInline, null, 'SOURCES'),
932
+ ' —— 不是从服务器另取的文档,也不是压缩后的产物。构建产物 ',
933
+ e(DocInline, null, 'client.js'),
934
+ ' 与源码页展示的内容是同一份代码的两种形态。'
935
+ ),
936
+ e(DocP, null,
937
+ '真实的 DSH Agent 还要处理:真实的模型鉴权与计费、工具权限与沙箱、流式输出的取消、会话落盘、上下文窗口的真实 token 计数。本插件为了离线与可读性,把这些等比缩小了 —— 但每一块的接口位置都能在下面的源码阅读顺序里找到。'
938
+ )
939
+ );
940
+ }
941
+
942
+ /* ── 第 10 节:真实 DeepSeek 模式 ───────────────────────── */
943
+
944
+ const DOC_REAL_CLIENT = [
945
+ '// 45-real-client.js(简化):把引擎上下文交给 Host 路由',
946
+ 'async function realModelAdapter({ messages, tools }) {',
947
+ ' const res = await fetch("/agent-lab/complete", {',
948
+ ' method: "POST",',
949
+ ' headers: { "content-type": "application/json" },',
950
+ ' body: JSON.stringify({ messages, tools, thinking: false }),',
951
+ ' });',
952
+ ' const data = await res.json();',
953
+ ' if (!data.ok) throw new Error(data.error.message);',
954
+ ' if (data.toolCalls.length > 0) {',
955
+ ' return { type: "tool_call", name: data.toolCalls[0].name, args: data.toolCalls[0].args };',
956
+ ' }',
957
+ ' return { type: "final", text: data.text };',
958
+ '}',
959
+ ].join('\n');
960
+
961
+ const DOC_REAL_HOST = [
962
+ '// index.js(Host 半侧,简化):用 DSH 当前账号模型发一次请求',
963
+ 'const { provider, model } = ctx.agentDefaultModel.currentSelection();',
964
+ 'for await (const chunk of ctx.llm.stream({',
965
+ ' provider, model,',
966
+ ' system, messages, tools,',
967
+ ' maxTokens: 900,',
968
+ ' reasoningEffort: "off", // 教学用:关闭 thinking,聚焦原生 tool-call',
969
+ '})) {',
970
+ ' if (chunk.type === "text-delta") text += chunk.text;',
971
+ ' if (chunk.type === "tool-call-delta") collectToolCall(chunk);',
972
+ '}',
973
+ '// 汇总为 { text, toolCalls, usage, latencyMs } 返回给浏览器',
974
+ ].join('\n');
975
+
976
+ function DocSectionRealLLM(props) {
977
+ return e(DocSec, {
978
+ no: props.no,
979
+ title: '真实 DeepSeek 模式:同一个循环,真模型决策',
980
+ lead: '体验页终端右上角可以在「模拟 / 真实」之间切换。真实模式调用你 DSH 当前选中的 DeepSeek 模型(本机为 deepseek-account / deepseek-flash),但循环、工具、记忆、压缩仍由本插件自己执行 —— 换掉的只有「模型决策」这一层。',
981
+ },
982
+ e(DocFlowChain, {
983
+ steps: ['Client 适配器', '同源 POST /agent-lab/complete', 'Host:ctx.llm.stream', '当前 DeepSeek 模型', 'tool_calls', '引擎执行工具 → 回填 → 下一轮'],
984
+ }),
985
+ e(DocP, null,
986
+ '为什么不让浏览器直接请求模型 API:',
987
+ e('strong', null, '密钥不能下发。 '),
988
+ '浏览器请求只发到同源路由 ',
989
+ e(DocInline, null, '/agent-lab/complete'),
990
+ ',由 DSH Connection 校验登录态与 Origin;DeepSeek 账号凭证由 DSH 的 provider adapter 在 Host 侧解析,页面与构建产物里都不含 token 或 API key。'
991
+ ),
992
+ e(DocSteps, {
993
+ items: [
994
+ ['浏览器', '只调用同源接口;拿不到账号 token,也绕不过 DSH 的认证与 Host/Origin 检查。'],
995
+ ['Host', 'index.js 通过 ',
996
+ e(DocInline, null, 'agentDefaultModel.currentSelection()'),
997
+ ' 读取当前 DSH 模型,再用 ',
998
+ e(DocInline, null, 'ctx.llm.stream()'),
999
+ ' 请求;账号鉴权由 adapter 完成。'],
1000
+ ['模型', '请求携带全部 8 个工具的 schema;模型返回原生 tool_calls,或直接返回 final 文本。'],
1001
+ ['引擎', '10-engine.js 执行工具,把 assistant(tool_call) 与 tool(result) 写回上下文,再发起下一次请求,直到 final。'],
1002
+ ],
1003
+ }),
1004
+ e(DocCode, { label: 'JavaScript · Client 适配器(45-real-client.js 的简化)', code: DOC_REAL_CLIENT }),
1005
+ e(DocCode, { label: 'JavaScript · Host 路由(index.js 的简化)', code: DOC_REAL_HOST }),
1006
+ e(DocNote, { icon: 'i', title: '教学取舍:关闭 thinking' },
1007
+ 'DeepSeek 的 thinking 模式要求把 reasoning 块原样带回下一轮工具调用;为了让教学循环保持最小可解释,真实模式显式传入 ',
1008
+ e(DocInline, null, 'reasoningEffort: "off"'),
1009
+ ',只保留原生 tool-call 协议。真实产品需要保留 reasoning 与 replay 状态,DSH 的 Agent Loop 已经处理了这些。'
1010
+ ),
1011
+ e(DocNote, { kind: 'warn', icon: '!', title: '真实模式会消耗账号额度' },
1012
+ '一次工具调用至少产生两次模型请求:决定调用工具一次、拿到工具结果后生成回答再一次。当前模型跟随 DSH 设置 → 模型;切换模型后下一次请求生效。终端会显示每次请求的模型、耗时与 token 用量。'
1013
+ ),
1014
+ e(DocP, null,
1015
+ '当前版本是「完整响应后回传」:Host 聚齐 text / tool_calls / usage 再返回,终端仍用本地打字动画呈现。要做到逐 token 流式,可以把这条路由换成 SSE,把 text-delta 直接转给引擎的 token 事件 —— 循环与工具层不需要改。'
1016
+ ),
1017
+ e(DocNote, { icon: 'i', title: '失败时的表现' },
1018
+ '未登录、额度不足、限流、上下文超窗都会以中文错误行出现在终端,并保留在「流转」的模型阶段里;如果 Host 路由没有加载,真实模式按钮会禁用并显示原因。'
1019
+ )
1020
+ );
1021
+ }
1022
+
1023
+ /* ── 第 11 节:新增一个工具 ─────────────────────────────── */
1024
+
1025
+ const DOC_NEW_TOOL = [
1026
+ '// 20-tools.js:把这样一个对象加进 LAB_TOOLS 数组即可',
1027
+ '{',
1028
+ ' name: "dice_roll",',
1029
+ ' description: "掷一个 n 面骰子,返回 1..n 的随机整数;用户说「掷骰子/随机数」时使用",',
1030
+ ' parameters: {',
1031
+ ' type: "object",',
1032
+ ' properties: {',
1033
+ ' sides: { type: "number", description: "骰子面数,默认 6,范围 2..100" },',
1034
+ ' },',
1035
+ ' required: [],',
1036
+ ' },',
1037
+ ' run(args) {',
1038
+ ' const raw = Number(args && args.sides);',
1039
+ ' const sides = Number.isFinite(raw) ? Math.max(2, Math.min(100, Math.round(raw))) : 6;',
1040
+ ' const value = 1 + Math.floor(Math.random() * sides);',
1041
+ ' return { ok: true, result: { sides, value }, display: "🎲 " + value + " / " + sides };',
1042
+ ' },',
1043
+ '}',
1044
+ ].join('\n');
1045
+
1046
+ function DocSectionNewTool(props) {
1047
+ return e(DocSec, {
1048
+ no: props.no,
1049
+ title: '如何新增一个工具',
1050
+ lead: '加工具只需要两步:注册一个函数,然后让模型知道它。规则模型靠一条意图规则,真实 LLM 靠随请求发出的 schema。',
1051
+ },
1052
+ e(DocSteps, {
1053
+ items: [
1054
+ [e('strong', null, '写工具:'), '按 ',
1055
+ e(DocInline, null, 'name / description / parameters / run'),
1056
+ ' 的形状写一个函数。'],
1057
+ [e('strong', null, '注册:'), '把它加进 ',
1058
+ e(DocInline, null, '20-tools.js'),
1059
+ ' 的 ',
1060
+ e(DocInline, null, 'LAB_TOOLS'),
1061
+ ' 数组。'],
1062
+ [e('strong', null, '让模型会调用:'), '本插件的模拟模型(',
1063
+ e(DocInline, null, '30-llm.js'),
1064
+ ')需要加一条识别「掷骰子」的意图规则并返回 ',
1065
+ e(DocInline, null, "type: 'tool_call'"),
1066
+ ';换成真实 LLM 后不需要写规则,schema 随请求发过去,模型会自己决定。'],
1067
+ [e('strong', null, '验证:'), '在「体验」页输入「掷一个 20 面骰子」,检查器「流转」应出现 tool 阶段、终端出现 ⚙ 与 ← 行;跑一遍无头测试;重新构建后「源码」页会自动包含新代码。'],
1068
+ ],
1069
+ }),
1070
+ e(DocCode, { label: 'JavaScript · 新增 dice_roll 工具', code: DOC_NEW_TOOL }),
1071
+ e(DocNote, { kind: 'tip', icon: '✓', title: '好工具的五个习惯' },
1072
+ '① description 写清「何时用 + 参数含义」,这是模型选择工具的唯一依据;② 参数永远校验与兜底,不信任模型;③ 失败返回 ',
1073
+ e(DocInline, null, 'ok: false'),
1074
+ ' 而不是抛异常;④ ',
1075
+ e(DocInline, null, 'display'),
1076
+ ' 面向人、',
1077
+ e(DocInline, null, 'result'),
1078
+ ' 面向程序;⑤ 像普通函数一样单测边界情况。'
1079
+ ),
1080
+ e(DocP, null,
1081
+ '注意:',
1082
+ e(DocInline, null, 'run'),
1083
+ ' 可以是 async,引擎会 await;真实工具(读写文件、执行命令)必须加权限与沙箱约束,不能把模型给的参数直接丢给系统执行。'
1084
+ )
1085
+ );
1086
+ }
1087
+
1088
+ /* ── 第 12 节:源码阅读顺序 ─────────────────────────────── */
1089
+
1090
+ function DocSectionReading(props) {
1091
+ return e(DocSec, {
1092
+ no: props.no,
1093
+ title: '建议的源码阅读顺序',
1094
+ lead: '按「从最小单元到整体」的顺序读,每一站都配一次体验页操作。源码页里所有文件都是普通 JavaScript,没有构建魔法。',
1095
+ },
1096
+ e(DocOrdered, {
1097
+ items: [
1098
+ [e(DocInline, null, '00-core.js'), ' —— 先建立「预算」直觉:常量、token 估算、时间与截断工具。'],
1099
+ [e(DocInline, null, '20-tools.js'), ' —— 工具就是函数的最短完整例子;看懂它,后面所有「模型能做什么」都顺了。'],
1100
+ [e(DocInline, null, '30-llm.js'), ' —— 规则模型如何把一句话变成 tool_call 或 final;对照体验页的多次工具调用看。'],
1101
+ [e(DocInline, null, '10-engine.js'), ' —— 核心:会话、上下文组装、压缩、turn 循环、持久化、事件与 snapshot。'],
1102
+ [e(DocInline, null, '45-real-client.js → index.js(Host 半侧)'), ' —— 真实模型模式:同源适配器 + Host 的 ctx.llm 路由;先看状态接口,再看 complete 如何聚合并返回 tool_calls。'],
1103
+ [e(DocInline, null, '40-ui-kit.js → 50-tui.js → 60-inspector.js'), ' —— 界面如何把状态渲染成终端与三个检查器页签(可按兴趣略读)。'],
1104
+ [e(DocInline, null, '70-docs.js → 80-source.js'), ' —— 你正在读的原理页与源码浏览器;看看讲解如何与实现一一对应。'],
1105
+ [e(DocInline, null, '90-page.js → 95-apply.js'), ' —— 最后看外壳与 DSH slot 注册,「插件是怎么变成页面的」在这里收尾。'],
1106
+ ],
1107
+ }),
1108
+ e(DocNote, { icon: 'i', title: '读源码的正确姿势' },
1109
+ '打开「源码」页,左边选文件、右边读代码;遇到看不懂的常量就回「体验」页触发一次对应行为,再回「流转」页签看它产生了什么。读代码 + 跑行为 + 看数据,比只看代码快得多。'
1110
+ )
1111
+ );
1112
+ }
1113
+
1114
+ /* ── 页面注册表 ─────────────────────────────────────────── */
1115
+
1116
+ const DOC_PAGE_SECTIONS = [
1117
+ { id: 'what', no: '01', title: '什么是 Agent', Comp: DocSectionWhat },
1118
+ { id: 'loop', no: '02', title: '最小循环', Comp: DocSectionLoop },
1119
+ { id: 'journey', no: '03', title: '一次输入的旅程', Comp: DocSectionJourney },
1120
+ { id: 'context', no: '04', title: '上下文与压缩', Comp: DocSectionContext },
1121
+ { id: 'memory', no: '05', title: '记忆', Comp: DocSectionMemory },
1122
+ { id: 'session', no: '06', title: '会话', Comp: DocSectionSession },
1123
+ { id: 'protocol', no: '07', title: '工具调用协议', Comp: DocSectionProtocol },
1124
+ { id: 'pi', no: '08', title: 'pi 的极简哲学', Comp: DocSectionPi },
1125
+ { id: 'harness', no: '09', title: '对照真实 DSH', Comp: DocSectionHarness },
1126
+ { id: 'real-llm', no: '10', title: '真实 DeepSeek 模式', Comp: DocSectionRealLLM },
1127
+ { id: 'new-tool', no: '11', title: '新增工具', Comp: DocSectionNewTool },
1128
+ { id: 'reading', no: '12', title: '源码阅读顺序', Comp: DocSectionReading },
1129
+ ];
1130
+
1131
+ /* ── 页面组件 ───────────────────────────────────────────── */
1132
+
1133
+ function DocsPage(props) {
1134
+ const handlers = props || {};
1135
+ const onNavigate = typeof handlers.onNavigate === 'function' ? handlers.onNavigate : null;
1136
+ const sectionRefs = useRef({});
1137
+
1138
+ const registerRef = useCallback(function (id) {
1139
+ return function (node) {
1140
+ sectionRefs.current[id] = node;
1141
+ };
1142
+ }, []);
1143
+
1144
+ const jumpTo = useCallback(function (id) {
1145
+ const node = sectionRefs.current[id];
1146
+ if (node && typeof node.scrollIntoView === 'function') {
1147
+ try {
1148
+ node.scrollIntoView({ behavior: 'smooth', block: 'start' });
1149
+ } catch (error) {
1150
+ /* 旧浏览器不支持平滑滚动时静默降级 */
1151
+ }
1152
+ }
1153
+ }, []);
1154
+
1155
+ const sourceStats = useMemo(function () {
1156
+ if (typeof SOURCES === 'undefined' || !Array.isArray(SOURCES)) return null;
1157
+ let lines = 0;
1158
+ SOURCES.forEach(function (source) {
1159
+ lines += String((source && source.code) || '').split('\n').length;
1160
+ });
1161
+ return { files: SOURCES.length, lines: lines };
1162
+ }, []);
1163
+
1164
+ return e('div', { className: 'al-doc-page' },
1165
+ e('style', { dangerouslySetInnerHTML: { __html: DOC_CSS } }),
1166
+
1167
+ e('header', { className: 'al-doc-hero' },
1168
+ e('div', { className: 'al-doc-eyebrow' }, 'AGENT LAB · 原理'),
1169
+ e('h1', { className: 'al-doc-title' }, 'Agent 是怎样工作的'),
1170
+ e('p', { className: 'al-doc-lead' },
1171
+ '一个 Agent 不是魔法,而是「模型 + 工具 + 循环 + 记忆」的组合。这一页用当前插件作为活体样本,拆开一次输入从进入、拼装、决策、调工具到写回记忆的完整旅程 —— 默认用可复现的模拟模型,也可以一键切到真实 DeepSeek 模型。你可以在「体验」页亲手跑一遍,再到「源码」页核对每一行实现。'
1172
+ ),
1173
+ e('div', { className: 'al-doc-chips' },
1174
+ e(DocChip, null, '模拟模式可离线 · 真实模式联网'),
1175
+ e(DocChip, null, '模拟 / 真实双模式'),
1176
+ e(DocChip, null, '源码全部内嵌可读'),
1177
+ sourceStats ? e(DocChip, null, sourceStats.files + ' 个源文件 · 约 ' + sourceStats.lines + ' 行') : null
1178
+ )
1179
+ ),
1180
+
1181
+ e('div', { className: 'al-doc-layout' },
1182
+ e('aside', { className: 'al-doc-toc' },
1183
+ e('div', { className: 'al-doc-toc-title' }, '本文目录'),
1184
+ e('nav', { className: 'al-doc-toc-list' },
1185
+ DOC_PAGE_SECTIONS.map(function (section) {
1186
+ return e('button', {
1187
+ key: section.id,
1188
+ type: 'button',
1189
+ className: 'al-doc-toc-item',
1190
+ onClick: function () { jumpTo(section.id); },
1191
+ },
1192
+ e('span', { className: 'al-doc-toc-no' }, section.no),
1193
+ e('span', null, section.title)
1194
+ );
1195
+ })
1196
+ )
1197
+ ),
1198
+
1199
+ e('main', { className: 'al-doc-main' },
1200
+ DOC_PAGE_SECTIONS.map(function (section) {
1201
+ return e('div', { key: section.id, className: 'al-doc-sec-wrap', ref: registerRef(section.id) },
1202
+ e(section.Comp, { no: section.no })
1203
+ );
1204
+ }),
1205
+
1206
+ e('footer', { className: 'al-doc-foot' },
1207
+ e('p', null,
1208
+ '本页由 Agent 实验室插件的 ',
1209
+ e(DocInline, null, '70-docs.js'),
1210
+ ' 渲染 —— 它和「体验」「源码」页是同一个 Client 里的普通 JavaScript,没有外部文档或网络资源。'
1211
+ ),
1212
+ e('p', null,
1213
+ '提醒:默认的模拟模型是规则驱动实现,用来把 agent 的骨架讲清楚、离线可复现;切到真实模式后,回答来自你 DSH 当前选中的 DeepSeek 模型,质量与额度都遵循该模型。'
1214
+ ),
1215
+ onNavigate
1216
+ ? e('button', {
1217
+ type: 'button',
1218
+ className: 'al-doc-btn',
1219
+ onClick: function () { onNavigate('source'); },
1220
+ }, '去「源码」页看真实代码 →')
1221
+ : e('p', null, '提示:切到顶部「源码」页,可以看到本页正在运行的每一行代码。')
1222
+ )
1223
+ )
1224
+ )
1225
+ );
1226
+ }