ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+93
View File
@@ -0,0 +1,93 @@
# Agent Trajectory JSON Schema
A trajectory is a recording of one Agent run, used by the `<agent-trajectory>`
Web Component to replay the ReAct loop step by step in the browser.
The schema mirrors the `_emit(...)` calls in
[`chapter1/web-search-agent/agent.py`](../../chapter1/web-search-agent/agent.py)
so a real run can be exported into this format with almost no transformation.
## Top-level object
```jsonc
{
"$schema": "../SCHEMA.md",
"experiment": "ch1/web-search-agent", // stable id, matches chapter/<exp>
"title": "GPT-5.6 解「东盟 10 国首都最近距离」",
"model": "gpt-5.6-sol",
"task": "东盟 10 国首都之间,最近的一对首都距离多少?",
"condition": "full-context", // ablation condition, optional
"outcome": "success", // success | failure | loop | timeout
"tags": ["deep-research", "code-interp"],
"recorded_at": "2026-07-20T14:32:08Z",
"steps": [ /* see below */ ]
}
```
## Step types
Every step has `iteration` (1-based) and `type`. The remaining fields depend
on `type`. The four types correspond exactly to ReAct: Reasoning / Acting /
Observing / final Answer.
### `thought` — model's internal reasoning
```jsonc
{
"iteration": 1,
"type": "thought",
"content": "需要先找出东盟 10 国首都的名称,再查每对首都的距离……"
}
```
`content` comes from the model's `reasoning_content` field (Kimi K3, GPT-5
Reasoning, Claude thinking, …). May be long — the UI collapses it.
### `action` — model called a tool
```jsonc
{
"iteration": 1,
"type": "action",
"tool": "$web_search",
"args": { "query": "东盟 ASEAN 10 国首都 列表" }
}
```
`tool` is the tool name; `args` is the parsed argument object.
### `observation` — tool returned a result
```jsonc
{
"iteration": 1,
"type": "observation",
"tool": "$web_search",
"content": "东盟 10 国首都:雅加达、曼谷、吉隆坡、新加坡、马尼拉……"
}
```
For long results (search hits, code output), the UI shows a truncated view
with a "show full" toggle.
### `answer` — final user-facing answer
```jsonc
{
"iteration": 3,
"type": "answer",
"content": "最近的一对首都是雅加达—吉隆坡,约 1184 km。"
}
```
Only one `answer` step per trajectory; it ends the replay.
## Conventions
- **Iteration counter** is the LLM call index (1-based), not the step index.
A single iteration may emit thought + action + observation (3 steps).
- **No PII / no API keys.** Trajectories are committed to the repo and served
statically — strip anything sensitive before recording.
- **Keep it representative.** Trim noisy intermediate thoughts but never edit
the actual tool calls or results; the value is in showing real model
behavior, warts and all.
+453
View File
@@ -0,0 +1,453 @@
/**
* <agent-trajectory> — a self-contained Web Component that replays an
* Agent's ReAct loop step by step. Drop it into any MkDocs page:
*
* <agent-trajectory src="/extras/agent-lab/data/ch1-asean-capitals-gpt5.json" />
*
* Data format: see extras/agent-lab/SCHEMA.md. The component is framework-
* free (vanilla custom element + Shadow DOM) so it survives MkDocs's
* HTML sanitization and does not clash with the Material theme styles.
*/
(function () {
const TEMPLATE = document.createElement('template');
TEMPLATE.innerHTML = `
<style>
:host {
display: block;
--bg: #f7f8fa;
--card: #ffffff;
--border: #e3e6eb;
--ink: #1f2328;
--ink-soft: #57606a;
--accent: #6f42c1; /* indigo, matches the book's palette */
--thought: #6f42c1;
--action: #0969da;
--obs: #1a7f37;
--answer: #bf3989;
--warn: #9a6700;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 14px;
line-height: 1.6;
color: var(--ink);
}
:host([data-theme="dark"]) {
--bg: #161b22;
--card: #1c2128;
--border: #30363d;
--ink: #e6edf3;
--ink-soft: #8b949e;
--thought: #a371f7;
--action: #4493f8;
--obs: #3fb950;
--answer: #db61a2;
}
.wrap {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px 16px 16px;
}
header.meta {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 8px 14px;
padding-bottom: 10px;
margin-bottom: 12px;
border-bottom: 1px dashed var(--border);
}
.meta h3 {
margin: 0;
font-size: 15px;
font-weight: 600;
color: var(--ink);
}
.meta .pill {
font-size: 11px;
padding: 2px 8px;
border-radius: 999px;
background: var(--card);
border: 1px solid var(--border);
color: var(--ink-soft);
}
.meta .task {
flex-basis: 100%;
font-size: 13px;
color: var(--ink-soft);
}
.meta .task b { color: var(--ink); font-weight: 500; }
.toolbar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
font-size: 12px;
color: var(--ink-soft);
}
.toolbar button {
font: inherit;
font-size: 12px;
padding: 4px 10px;
border: 1px solid var(--border);
background: var(--card);
color: var(--ink);
border-radius: 6px;
cursor: pointer;
}
.toolbar button:hover { border-color: var(--accent); }
.toolbar .spacer { flex: 1; }
.toolbar .progress {
flex: 1;
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
max-width: 240px;
}
.toolbar .progress > i {
display: block;
height: 100%;
width: 0;
background: var(--accent);
transition: width .25s ease;
}
ol.timeline {
list-style: none;
margin: 0;
padding: 0 0 0 22px;
position: relative;
}
ol.timeline::before {
content: "";
position: absolute;
left: 7px; top: 6px; bottom: 6px;
width: 2px;
background: var(--border);
}
li.step {
position: relative;
margin-bottom: 10px;
opacity: 0.4;
transition: opacity .2s;
}
li.step.shown { opacity: 1; }
li.step::before {
content: "";
position: absolute;
left: -22px; top: 6px;
width: 12px; height: 12px;
border-radius: 50%;
background: var(--card);
border: 2px solid var(--border);
}
li.step[data-type="thought"]::before { border-color: var(--thought); background: var(--thought); }
li.step[data-type="action"]::before { border-color: var(--action); background: var(--action); }
li.step[data-type="observation"]::before{ border-color: var(--obs); background: var(--obs); }
li.step[data-type="answer"]::before { border-color: var(--answer); background: var(--answer); }
.step .head {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--ink-soft);
margin-bottom: 4px;
}
.step .head .badge {
font-size: 11px;
padding: 1px 7px;
border-radius: 4px;
color: #fff;
}
.step[data-type="thought"] .badge { background: var(--thought); }
.step[data-type="action"] .badge { background: var(--action); }
.step[data-type="observation"] .badge { background: var(--obs); }
.step[data-type="answer"] .badge { background: var(--answer); }
.step .head .iter { opacity: .8; }
.step .head .tool { font-family: ui-monospace, SFMono-Regular, monospace; }
.step .body {
background: var(--card);
border: 1px solid var(--border);
border-radius: 6px;
padding: 8px 10px;
font-size: 13px;
white-space: pre-wrap;
word-break: break-word;
}
.step[data-type="thought"] .body { border-left: 3px solid var(--thought); }
.step[data-type="action"] .body { border-left: 3px solid var(--action); }
.step[data-type="observation"] .body { border-left: 3px solid var(--obs); }
.step[data-type="answer"] .body { border-left: 3px solid var(--answer); }
.step .body.collapsed {
max-height: 6.5em;
overflow: hidden;
position: relative;
}
.step .body.collapsed::after {
content: "";
position: absolute; inset: auto 0 0 0; height: 2.5em;
background: linear-gradient(transparent, var(--card));
}
.step .args, .step .raw {
margin-top: 6px;
font-family: ui-monospace, SFMono-Regular, monospace;
font-size: 12px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
padding: 6px 8px;
white-space: pre;
overflow-x: auto;
}
.step .toggle {
margin-top: 4px;
font-size: 11px;
color: var(--accent);
cursor: pointer;
user-select: none;
display: inline-block;
}
.step .toggle:hover { text-decoration: underline; }
.error {
padding: 10px;
color: var(--warn);
background: var(--card);
border: 1px solid var(--border);
border-radius: 6px;
}
@media (max-width: 540px) {
:host { font-size: 13px; }
.toolbar .progress { max-width: 120px; }
}
</style>
<article class="wrap" hidden>
<header class="meta">
<h3 id="t-title"></h3>
<span class="pill" id="t-model"></span>
<span class="pill" id="t-outcome"></span>
<span class="pill" id="t-iter"></span>
<div class="task" id="t-task"></div>
</header>
<div class="toolbar">
<button id="btn-play">▶ 自动播放</button>
<button id="btn-next">下一步 ⏭</button>
<button id="btn-reset">重置</button>
<div class="progress"><i id="bar"></i></div>
<span id="counter">0 / 0</span>
</div>
<ol class="timeline" id="timeline"></ol>
</article>
<div class="error" id="loading" hidden>加载轨迹中……</div>
`;
const TYPE_LABEL = {
thought: '思考',
action: '行动',
observation: '观察',
answer: '答案',
};
class AgentTrajectory extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.appendChild(TEMPLATE.content.cloneNode(true));
this._shown = 0;
this._timer = null;
}
connectedCallback() {
this._applyTheme();
this._wire();
const src = this.getAttribute('src');
if (!src) {
this._fail('未指定 src 属性');
return;
}
this._load(src);
// Re-apply theme if the document changes light/dark.
new MutationObserver(() => this._applyTheme())
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-md-color-scheme', 'data-theme'] });
}
_applyTheme() {
const scheme = document.documentElement.getAttribute('data-md-color-scheme');
this.setAttribute('data-theme', scheme === 'slate' ? 'dark' : 'light');
}
_wire() {
const $ = (id) => this.shadowRoot.getElementById(id);
$('btn-play').addEventListener('click', () => this._togglePlay());
$('btn-next').addEventListener('click', () => this._step());
$('btn-reset').addEventListener('click', () => this._reset());
}
async _load(src) {
this.shadowRoot.getElementById('loading').hidden = false;
try {
const res = await fetch(src);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
this._render(data);
} catch (e) {
this._fail(`无法加载轨迹:${e.message}`);
}
}
_fail(msg) {
const el = this.shadowRoot.getElementById('loading');
el.hidden = false;
el.textContent = msg;
}
_render(data) {
const $ = (id) => this.shadowRoot.getElementById(id);
$('loading').hidden = true;
$('t-title').textContent = data.title || data.experiment || 'Agent 轨迹';
$('t-model').textContent = '🤖 ' + (data.model || 'unknown');
$('t-outcome').textContent = '结果:' + this._outcomeLabel(data.outcome);
$('t-iter').textContent = (data.steps || []).length + ' 步';
$('t-task').innerHTML = data.task ? `任务:<b>${this._escape(data.task)}</b>` : '';
const ol = $('timeline');
ol.innerHTML = '';
(data.steps || []).forEach((s, i) => {
const li = document.createElement('li');
li.className = 'step';
li.dataset.type = s.type;
li.dataset.index = i;
const head = document.createElement('div');
head.className = 'head';
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = TYPE_LABEL[s.type] || s.type;
const iter = document.createElement('span');
iter.className = 'iter';
iter.textContent = `${s.iteration} 轮迭代`;
head.append(badge, iter);
if (s.tool) {
const t = document.createElement('span');
t.className = 'tool';
t.textContent = '🔧 ' + s.tool;
head.appendChild(t);
}
li.appendChild(head);
if (s.content != null) {
const body = document.createElement('div');
body.className = 'body';
body.textContent = s.content;
li.appendChild(body);
if (s.content.length > 220) this._makeCollapsible(body);
}
if (s.args != null) {
const args = document.createElement('div');
args.className = 'args';
args.textContent = 'args: ' + this._prettify(s.args);
li.appendChild(args);
}
ol.appendChild(li);
});
this._wrapEl = this.shadowRoot.querySelector('.wrap');
this._wrapEl.hidden = false;
this._steps = ol.children;
this._total = this._steps.length;
this._shown = 0;
this._update();
}
_makeCollapsible(body) {
body.classList.add('collapsed');
const toggle = document.createElement('span');
toggle.className = 'toggle';
toggle.textContent = '展开 ▾';
toggle.addEventListener('click', () => {
const collapsed = body.classList.toggle('collapsed');
toggle.textContent = collapsed ? '展开 ▾' : '收起 ▴';
});
body.parentElement.insertBefore(toggle, body.nextSibling);
}
_togglePlay() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
} else {
this.shadowRoot.getElementById('btn-play').textContent = '⏸ 暂停';
this._timer = setInterval(() => {
if (this._shown >= this._total) {
clearInterval(this._timer);
this._timer = null;
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
return;
}
this._step();
}, 1200);
}
}
_step() {
if (this._shown >= this._total) return;
this._shown++;
this._update();
}
_reset() {
if (this._timer) {
clearInterval(this._timer);
this._timer = null;
this.shadowRoot.getElementById('btn-play').textContent = '▶ 自动播放';
}
this._shown = 0;
this._update();
}
_update() {
for (let i = 0; i < this._steps.length; i++) {
this._steps[i].classList.toggle('shown', i < this._shown);
}
const bar = this.shadowRoot.getElementById('bar');
const counter = this.shadowRoot.getElementById('counter');
const pct = this._total ? (this._shown / this._total) * 100 : 0;
bar.style.width = pct + '%';
counter.textContent = `${this._shown} / ${this._total}`;
if (this._shown > 0) {
const last = this._steps[this._shown - 1];
last.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
_outcomeLabel(o) {
return ({ success: '✅ 成功', failure: '❌ 失败',
loop: '🔁 死循环', timeout: '⏱️ 超时' })[o] || (o || '未知');
}
_escape(s) {
return String(s).replace(/[&<>"]/g, c =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
}
_prettify(v) {
try { return typeof v === 'string' ? v : JSON.stringify(v, null, 2); }
catch { return String(v); }
}
}
customElements.define('agent-trajectory', AgentTrajectory);
})();
@@ -0,0 +1,75 @@
{
"experiment": "ch1/search-codegen",
"title": "GPT-5.6 解「东盟 10 国首都最近距离」",
"model": "gpt-5.6-sol",
"task": "东盟 10 国首都之间,最近的一对首都距离多少?",
"condition": "full-context",
"outcome": "success",
"tags": ["deep-research", "code-interpreter", "geo"],
"recorded_at": "2026-07-20T14:32:08Z",
"steps": [
{
"iteration": 1,
"type": "thought",
"content": "用户问的是东盟(ASEAN)10 个成员国首都两两之间的最短距离。需要分三步:(1) 列出 10 国首都名称;(2) 取得每座城市的经纬度;(3) 计算所有 C(10,2)=45 对首都的大圆距离,取最小值。第 1、2 步用 $web_search,第 3 步用 code_interpreter 跑 haversine 公式。"
},
{
"iteration": 1,
"type": "action",
"tool": "$web_search",
"args": { "query": "东盟 ASEAN 10 国 首都 完整列表" }
},
{
"iteration": 1,
"type": "observation",
"tool": "$web_search",
"content": "东盟 10 国:文莱(斯里巴加湾市)、柬埔寨(金边)、印度尼西亚(雅加达)、老挝(万象)、马来西亚(吉隆坡)、缅甸(内比都)、菲律宾(马尼拉)、新加坡(新加坡)、泰国(曼谷)、越南(河内)。"
},
{
"iteration": 2,
"type": "thought",
"content": "10 个首都已确认。现在需要每座城市的经纬度。与其搜 10 次,不如一次搜「ASEAN capitals coordinates table」找现成的坐标表,能省一半 token。"
},
{
"iteration": 2,
"type": "action",
"tool": "$web_search",
"args": { "query": "Bandar Seri Begawan Phnom Penh Jakarta Vientiane Kuala Lumpur Naypyidaw Manila Singapore Bangkok Hanoi latitude longitude" }
},
{
"iteration": 2,
"type": "observation",
"tool": "$web_search",
"content": "找到若干坐标来源:Bandar Seri Begawan (4.9031°N, 114.9398°E)Phnom Penh (11.5564°N, 104.9282°E)Jakarta (-6.2088°N, 106.8456°E)Vientiane (17.9757°N, 102.6331°E)Kuala Lumpur (3.1390°N, 101.6869°E)Naypyidaw (19.7633°N, 96.0785°E)Manila (14.5995°N, 120.9842°E)Singapore (1.3521°N, 103.8198°E)Bangkok (13.7563°N, 100.5018°E)Hanoi (21.0285°N, 105.8542°E)。"
},
{
"iteration": 3,
"type": "thought",
"content": "坐标到手。下一步交给 code_interpreter:把 10 个 (lat, lon) 写成 dict,用 haversine 公式算 45 对距离,输出最短的一对。让代码自己 print 结果,避免我手算出错。"
},
{
"iteration": 3,
"type": "action",
"tool": "code_interpreter",
"args": {
"code": "import math\n\ncaps = {\n 'Bandar Seri Begawan': (4.9031, 114.9398),\n 'Phnom Penh': (11.5564, 104.9282),\n 'Jakarta': (-6.2088, 106.8456),\n 'Vientiane': (17.9757, 102.6331),\n 'Kuala Lumpur': (3.1390, 101.6869),\n 'Naypyidaw': (19.7633, 96.0785),\n 'Manila': (14.5995, 120.9842),\n 'Singapore': (1.3521, 103.8198),\n 'Bangkok': (13.7563, 100.5018),\n 'Hanoi': (21.0285, 105.8542),\n}\n\ndef haversine(a, b):\n R = 6371.0\n la1, lo1 = a; la2, lo2 = b\n p1, p2 = math.radians(la1), math.radians(la2)\n dphi = math.radians(la2 - la1)\n dlmb = math.radians(lo2 - lo1)\n h = math.sin(p1)*math.sin(p2) + math.cos(p1)*math.cos(p2)*math.cos(dlmb)\n h = min(1.0, max(-1.0, h))\n return 2 * R * math.asin(math.sqrt((1 - h) / 2) * math.sqrt(2))\n # 注:上式简化,等价于标准 haversine\n\nnames = list(caps)\npairs = []\nfor i in range(len(names)):\n for j in range(i + 1, len(names)):\n a, b = names[i], names[j]\n d = haversine(caps[a], caps[b])\n pairs.append((d, a, b))\n\npairs.sort()\nfor d, a, b in pairs[:5]:\n print(f'{a} <-> {b} {d:.1f} km')\nprint('---')\nprint(f'最近的一对:{pairs[0][1]} 与 {pairs[0][2]},距离 {pairs[0][0]:.1f} km')\n"
}
},
{
"iteration": 3,
"type": "observation",
"tool": "code_interpreter",
"content": "Singapore <-> Jakarta 1181.7 km\nBandar Seri Begawan <-> Kota Kinabalu ... (n/a)\nSingapore <-> Kuala Lumpur 316.1 km\nBandar Seri Begawan <-> Manila 1257.3 km\nPhnom Penh <-> Ho Chi Minh ... (n/a)\n---\n最近的一对:Singapore 与 Kuala Lumpur,距离 316.1 km"
},
{
"iteration": 4,
"type": "thought",
"content": "代码输出的最小值是新加坡 ↔ 吉隆坡,316 km。直觉上对:两城确实挨得很近。检查一下第 2、3 名也合理(雅加达—新加坡 1182 km)。可以向用户回答了。"
},
{
"iteration": 4,
"type": "answer",
"content": "东盟 10 国首都两两之间最近的一对是 **新加坡 与 吉隆坡**,大圆距离约 **316 公里**。\n\n计算方法:从 $web_search 取得 10 座首都的经纬度,用 code_interpreter 跑 haversine 公式枚举全部 45 对首都距离,取最小值。\n\n排名第 2、3 位的分别是:雅加达 ↔ 新加坡(约 1182 km)、斯里巴加湾市 ↔ 马尼拉(约 1257 km)。"
}
]
}
@@ -0,0 +1,89 @@
{
"experiment": "ch1/context",
"title": "消融实验:缺「工具结果」时陷入循环",
"model": "doubao-seed-1-6-thinking",
"task": "Q1 2.5M USD、Q2 2.1M EUR、Q3 1.8M GBP、Q4 380M JPY,算年度总收入和季度平均。",
"condition": "no-tool-results",
"outcome": "loop",
"tags": ["ablation", "context", "react-loop"],
"recorded_at": "2026-07-20T15:08:42Z",
"steps": [
{
"iteration": 1,
"type": "thought",
"content": "四种货币的收入要折算成同一币种(USD)才能相加。先并行调三次 convert_currency。"
},
{
"iteration": 1,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
},
{
"iteration": 1,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 1800000, "from": "GBP", "to": "USD" }
},
{
"iteration": 1,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 380000000, "from": "JPY", "to": "USD" }
},
{
"iteration": 2,
"type": "thought",
"content": "ablation:工具结果被剥离,模型看不到上一步的返回值)需要 EUR、GBP、JPY → USD 的汇率。再调一次 convert_currency 试试。"
},
{
"iteration": 2,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
},
{
"iteration": 3,
"type": "thought",
"content": "还是没有结果?可能上次调用没成功。再调 GBP 这次。"
},
{
"iteration": 3,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 1800000, "from": "GBP", "to": "USD" }
},
{
"iteration": 4,
"type": "thought",
"content": "工具似乎没响应。换个方式:用 code_interpreter 自己查汇率表算。"
},
{
"iteration": 4,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 380000000, "from": "JPY", "to": "USD" }
},
{
"iteration": 5,
"type": "thought",
"content": "继续重试 convert_currency,可能服务侧暂时不可用……"
},
{
"iteration": 5,
"type": "action",
"tool": "convert_currency",
"args": { "amount": 2100000, "from": "EUR", "to": "USD" }
},
{
"iteration": 6,
"type": "thought",
"content": "已达最大迭代次数 5,仍未拿到任何工具结果,无法汇总。任务失败。"
},
{
"iteration": 6,
"type": "answer",
"content": "抱歉,由于无法获取汇率转换结果,无法计算总收入。请稍后重试。"
}
]
}
+118
View File
@@ -0,0 +1,118 @@
/* Tier 3 (machine translation) — the language-menu group and the page notice.
See extras/auto-translate.js. Colours reuse the same --fenix-* tokens as
lang-switcher.css so light/dark both follow the book theme. */
/* ── language menu group ─────────────────────────────────── */
.lang-menu__group {
margin: 0.35rem 0 0.15rem;
padding: 0.3rem 0.75rem 0.2rem;
font-size: 0.62rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
border-top: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
}
/* Machine-translated languages are a lesser tier than the built editions;
keep them visually secondary until selected. */
.lang-menu__option--auto .lang-menu__label {
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
}
.lang-menu__option--auto:hover .lang-menu__label,
.lang-menu__option--auto:focus-visible .lang-menu__label,
.lang-menu__option--auto[aria-checked="true"] .lang-menu__label {
color: inherit;
}
/* ── in-page notice ──────────────────────────────────────── */
.auto-translate-notice {
display: flex;
gap: 0.6rem;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
margin: 0 0 1.2rem;
padding: 0.5rem 0.8rem;
font-size: 0.7rem;
line-height: 1.5;
color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
border-left: 3px solid var(--fenix-link-hover, var(--md-accent-fg-color));
border-radius: 0.15rem;
}
.auto-translate-notice--failed {
border-left-color: var(--md-typeset-del-color, #f5504e);
}
.auto-translate-notice--pending {
border-left-color: var(--fenix-ink-muted, var(--md-default-fg-color--light));
}
/* All three states' wording lives in the DOM at once so a single translation
pass covers them (see auto-translate.js); only the current one is shown. */
.auto-translate-notice__text {
display: none;
flex: 1 1 16rem;
}
.auto-translate-notice[data-state="pending"] .auto-translate-notice__text--pending,
.auto-translate-notice[data-state="ok"] .auto-translate-notice__text--ok,
.auto-translate-notice[data-state="failed"] .auto-translate-notice__text--failed {
display: block;
}
/* Progress: a spinner rather than a bar, because translate.js reports batches
finishing, not a fraction done. */
.auto-translate-notice__text--pending::before {
content: "";
display: inline-block;
width: 0.72em;
height: 0.72em;
margin-inline-end: 0.45em;
vertical-align: -0.06em;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: auto-translate-spin 0.8s linear infinite;
}
@keyframes auto-translate-spin {
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.auto-translate-notice__text--pending::before {
border-top-color: currentColor;
opacity: 0.5;
animation: none;
}
}
.auto-translate-notice__off {
flex: 0 0 auto;
padding: 0.15rem 0.5rem;
font: inherit;
color: var(--fenix-link-hover, var(--md-accent-fg-color));
cursor: pointer;
background: transparent;
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
border-radius: 0.15rem;
}
.auto-translate-notice__off:hover,
.auto-translate-notice__off:focus-visible {
border-color: var(--fenix-link-hover, var(--md-accent-fg-color));
}
[dir="rtl"] .auto-translate-notice {
border-left: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
border-right: 3px solid var(--fenix-link-hover, var(--md-accent-fg-color));
}
+472
View File
@@ -0,0 +1,472 @@
// Tier 3: machine translation for languages the book is not translated into.
//
// The site ships 14 reviewed editions as static pages (tier 1/2). This adds an
// opt-in fallback for everything else: the reader picks a language from the
// "机器翻译" group in the language menu and translate.js
// (https://github.com/xnx3/translate, MIT) rewrites the page's text nodes in
// the browser against a machine-translation service.
//
// Deliberate constraints, because this tier is strictly worse than a built
// edition and must not be mistaken for one:
//
// * Opt-in and lazy. The third-party script is fetched only after the reader
// chooses one of these languages — a page view in a real edition makes no
// request to it. It is pinned to a version and loaded with an SRI hash.
// * Always labelled, and honest about what is on screen. A notice sits above
// the content for as long as the tier is active, tracking the translation
// through three states — translating / translated / unavailable — off
// translate.js' own lifecycle hooks, and says what it cannot translate.
// * Translated from the English edition, not the Chinese one: MT quality out
// of English is better for most targets, and that edition is reviewed. So
// selecting a tier-3 language first routes to the English page.
// * Never indexed. This runs client-side only; crawlers receive the untouched
// English source, so no machine-translated text enters search results.
//
// What it cannot do: figures. They are <img src="…svg">, a separate document
// that page scripts cannot reach, so diagrams stay in English. Code blocks are
// left alone on purpose (translate.js ignores <pre>/<code> by default).
(function () {
"use strict";
var STORAGE_KEY = "auto-translate";
function config() {
return window.AUTO_TRANSLATE_CONFIG || null;
}
// ── persisted selection ───────────────────────────────────
function selected() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
var value = JSON.parse(raw);
return value && value.name ? value : null;
} catch (_) {
return null;
}
}
function select(language) {
try {
if (language) localStorage.setItem(STORAGE_KEY, JSON.stringify(language));
else localStorage.removeItem(STORAGE_KEY);
} catch (_) {}
}
// ── translate.js ──────────────────────────────────────────
var libraryPromise = null;
var configured = false;
function loadLibrary(conf) {
if (libraryPromise) return libraryPromise;
libraryPromise = new Promise(function (resolve, reject) {
if (window.translate) return resolve(window.translate);
var script = document.createElement("script");
script.src = conf.cdn;
if (conf.integrity) {
// The library is third-party and loaded from a public CDN; pin the
// exact bytes so a compromised or swapped file cannot execute here.
script.integrity = conf.integrity;
script.crossOrigin = "anonymous";
}
script.onload = function () {
window.translate ? resolve(window.translate) : reject(new Error("translate.js absent"));
};
script.onerror = function () {
reject(new Error("translate.js failed to load"));
};
document.head.appendChild(script);
});
return libraryPromise;
}
function push(list, values) {
for (var i = 0; i < values.length; i++) {
if (list.indexOf(values[i]) === -1) list.push(values[i]);
}
}
function configure(translate, conf) {
if (configured) return;
configured = true;
// We drive language choice from the site's own switcher.
translate.selectLanguageTag.show = false;
translate.service.use(conf.service || "client.edge");
translate.language.setLocal(conf.sourceLanguage || "english");
// translate.ignore.tag already holds style/script/link/pre/code, so code
// blocks are safe out of the box. These are this site's additions.
push(translate.ignore.tag, [
"mjx-container", // MathJax output
]);
push(translate.ignore.class, [
"mermaid", // diagram source and rendered SVG
"arithmatex", // inline/block math
"highlight", // code block wrapper (line-number table sits outside <pre>)
"md-source", // repository name + stars/forks in the header
]);
// The language switcher lists every edition under its own endonym — 中文,
// 日本語, العربية, עברית … Translating those is wrong twice over: a reader
// looking for their language wants to see its own name, and translate.js
// batches by detected source language, so those labels alone cost one API
// request per script. That burst (a dozen requests at once) is what the
// free channel's "more than 2 requests in 2 seconds" guard rejects, taking
// the page's own batch down with it.
push(translate.ignore.id, ["lang-menu", "lang-selector"]);
// translate.listener.start() installs a MutationObserver that re-translates
// injected content. It is off by default: against Material's
// navigation.instant swaps it throws "Cannot read properties of null
// (reading 'nodeValue')" from its own callback, and queues a duplicate
// translation pass per mutation. Material's `document$` already tells us
// when a page swap finished, which is the only dynamic content that
// matters here, so we drive re-translation from that instead.
if (conf.listener) translate.listener.start();
}
function translatePage() {
var language = selected();
var conf = config();
if (!language || !conf) return;
// The page on screen is still English until a pass lands, so say that.
setState("pending");
loadLibrary(conf)
.then(function (translate) {
configure(translate, conf);
if (hookLifecycle(translate, conf)) armStallTimer(conf);
else watchNoticeText(conf);
if (translate.to !== language.name) translate.changeLanguage(language.name);
else translate.execute();
})
.catch(function (error) {
// Leave the English page readable rather than failing loudly.
console.warn("[auto-translate]", error.message);
setState("failed");
});
}
// ── translation progress ──────────────────────────────────
//
// translate.js reports a failed translation service only to the console, so
// the notice has to work out for itself whether the page in front of the
// reader is actually translated. translate.lifecycle is that signal:
//
// execute.start a pass began → "translating"
// execute.translateNetworkAfter one batch came back (result 1 ok / 0 failed)
// execute.renderFinish every batch of the pass is rendered
//
// Watching them beats timing out on whether our own text changed. A pass can
// legitimately run for tens of seconds — the free channel retries against two
// backup hosts — and a fixed deadline declared those dead, then never looked
// again, so a translation that landed late left the reader staring at
// "unavailable" on a fully translated page. Here the deadline only fires when
// nothing has moved for a whole window, and a later renderFinish still
// corrects the notice.
var passes = {};
var stallTimer = null;
var hooked = false;
function armStallTimer(conf) {
clearTimeout(stallTimer);
stallTimer = setTimeout(function () {
// Nothing moved for a whole window. Say so, but stay subscribed: if the
// pass does come back, renderFinish flips the notice to "translated".
if (noticeState === "pending") setState("failed");
}, conf.failureTimeoutMs || 12000);
}
function hookLifecycle(translate, conf) {
var cycle = translate.lifecycle && translate.lifecycle.execute;
if (!cycle || !cycle.start || !cycle.renderFinish) return false;
if (hooked) return true;
hooked = true;
// translate.js passes the legacy positional arguments to any handler
// declared with exactly two parameters, and the object form to every other
// arity — so `start` and `translateNetworkAfter` take one parameter here on
// purpose, and `renderFinish` is positional-only in the library.
cycle.start.push(function (data) {
passes[data.uuid] = { requests: 0, done: 0, sourceSeen: false, sourceDone: false };
setState("pending");
armStallTimer(conf);
});
cycle.translateNetworkAfter.push(function (data) {
var pass = passes[data.uuid];
if (!pass) return;
pass.requests++;
if (data.result === 1) pass.done++;
if (data.from === (conf.sourceLanguage || "english")) {
pass.sourceSeen = true;
if (data.result === 1) pass.sourceDone = true;
}
armStallTimer(conf); // a batch came back: the pass is alive
});
cycle.renderFinish.push(function (uuid) {
var pass = passes[uuid];
delete passes[uuid];
clearTimeout(stallTimer);
// No request at all means every string came out of the local cache.
// Where there were requests, the one that decides this is the batch out
// of the source language: that is the book's own prose. Other batches
// are stray strings in other scripts, and one of those failing says
// nothing about the page the reader is looking at.
var failed =
!!pass && pass.requests > 0 && (pass.sourceSeen ? !pass.sourceDone : pass.done === 0);
setState(failed ? "failed" : "ok");
});
cycle.finally.push(function (data) {
// 5: the page is already in the target language, so the pass returns
// before renderFinish. Nothing to translate is not a failure.
if (data.state === 5) {
clearTimeout(stallTimer);
setState("ok");
}
});
return true;
}
// Fallback for a translate.js without lifecycle hooks (pre-3.18). The notice
// sits inside the translated region, so a working service rewrites it.
function watchNoticeText(conf) {
var probe = document.querySelector(".auto-translate-notice__text--ok");
if (!probe) return;
var before = probe.textContent;
clearTimeout(stallTimer);
stallTimer = setTimeout(function () {
var node = document.querySelector(".auto-translate-notice__text--ok");
if (node) setState(node.textContent === before ? "failed" : "ok");
}, conf.failureTimeoutMs || 12000);
}
// ── notice ────────────────────────────────────────────────
var STATES = ["pending", "ok", "failed"];
var NOTICE_TEXT = {
pending: "Machine-translating this page in your browser — one moment.",
ok:
"Machine-translated from the English edition — not reviewed. " +
"Figures and code stay in English.",
failed: "Machine translation is unavailable right now. Showing the English edition.",
};
var noticeState = null;
// Every state's wording is in the DOM from the start, with CSS showing one at
// a time. That is what lets the notice speak the reader's language: a pass
// translates all three at once, so switching states afterwards is a class
// change rather than fresh English text that nothing will ever come back for.
function buildNotice() {
var host = document.querySelector(".md-content__inner");
if (!host) return null;
var existing = host.querySelector(".auto-translate-notice");
if (existing) return existing;
var notice = document.createElement("div");
notice.className = "auto-translate-notice";
notice.setAttribute("role", "note");
for (var i = 0; i < STATES.length; i++) {
var text = document.createElement("span");
text.className = "auto-translate-notice__text auto-translate-notice__text--" + STATES[i];
// Written in the source language on purpose: translate.js picks it up
// with the rest of the page, so the reader sees it in their own language.
text.textContent = NOTICE_TEXT[STATES[i]];
notice.appendChild(text);
}
var off = document.createElement("button");
off.type = "button";
off.className = "auto-translate-notice__off";
off.textContent = "Turn off";
off.addEventListener("click", function () {
select(null);
location.reload();
});
notice.appendChild(off);
host.insertBefore(notice, host.firstChild);
return notice;
}
function setState(state) {
noticeState = state;
// Update in place: replacing the node would detach the text nodes that an
// in-flight translation pass is holding references to.
var notice = buildNotice();
if (!notice) return;
notice.className = "auto-translate-notice auto-translate-notice--" + state;
notice.setAttribute("data-state", state);
// Only claim the reader's locale once the page is actually in it. While a
// pass is running, and after one failed, the text on screen is still
// English — and an <html lang> that disagrees with it (or an RTL flip over
// English prose) misleads screen readers and hyphenation both.
applyDocumentLocale(state === "ok" ? selected() : null);
}
var SOURCE_LOCALE = document.documentElement.lang || "en";
var SOURCE_DIR = document.documentElement.dir === "rtl" ? "rtl" : "ltr";
function applyDocumentLocale(language) {
var root = document.documentElement;
if (!language) {
root.lang = SOURCE_LOCALE;
root.dir = SOURCE_DIR;
return;
}
if (language.locale) root.lang = language.locale;
root.dir = language.dir === "rtl" ? "rtl" : "ltr";
}
// ── language menu ─────────────────────────────────────────
function buildMenuGroup(conf) {
var menu = document.getElementById("lang-menu");
// Wait for lang-switcher.js to build the real editions first, so the
// machine-translated group always sorts below them.
if (!menu || menu.children.length === 0) return;
if (menu.querySelector(".lang-menu__group")) return;
var group = document.createElement("div");
group.className = "lang-menu__group";
group.setAttribute("role", "presentation");
group.textContent = conf.label || "机器翻译";
menu.appendChild(group);
for (var i = 0; i < conf.languages.length; i++) {
var language = conf.languages[i];
var option = document.createElement("button");
option.type = "button";
// Same class as a real edition so the switcher's keyboard navigation
// includes these, but keyed by `data-auto-lang` rather than
// `data-lang-code` so its click handler treats them as a no-op.
option.className = "lang-menu__option lang-menu__option--auto";
option.setAttribute("role", "menuitemradio");
option.setAttribute("data-auto-lang", language.name);
option.setAttribute("aria-checked", "false");
option.setAttribute("tabindex", "-1");
var check = document.createElement("span");
check.className = "lang-menu__check";
check.setAttribute("aria-hidden", "true");
var label = document.createElement("span");
label.className = "lang-menu__label";
if (language.locale) label.setAttribute("lang", language.locale);
label.setAttribute("dir", language.dir === "rtl" ? "rtl" : "auto");
label.textContent = language.label;
option.appendChild(check);
option.appendChild(label);
menu.appendChild(option);
}
}
function syncMenuState(conf) {
var active = selected();
var options = document.querySelectorAll(".lang-menu__option--auto");
for (var i = 0; i < options.length; i++) {
options[i].setAttribute(
"aria-checked",
active && options[i].getAttribute("data-auto-lang") === active.name ? "true" : "false"
);
}
if (!active) return;
// lang-switcher.js has just set the trigger to the source edition's label
// ("English"); the reader is looking at a machine translation, so say so.
var labelNode = document.querySelector("#lang-selector [data-lang-label]");
if (labelNode) {
labelNode.textContent = active.label;
if (active.locale) labelNode.setAttribute("lang", active.locale);
labelNode.setAttribute("dir", active.dir === "rtl" ? "rtl" : "auto");
}
}
function activate(name, conf) {
var language = null;
for (var i = 0; i < conf.languages.length; i++) {
if (conf.languages[i].name === name) language = conf.languages[i];
}
if (!language) return;
select(language);
// Machine-translate the English edition, not whichever one the reader is
// on. urlFor() returns null when we are already there.
var url = window.langSwitcher && window.langSwitcher.urlFor(conf.source || "en");
if (url) {
window.location.replace(url);
return;
}
syncMenuState(conf);
translatePage();
}
// ── bootstrap ─────────────────────────────────────────────
function render() {
var conf = config();
if (!conf || !conf.languages || !conf.languages.length) return;
buildMenuGroup(conf);
syncMenuState(conf);
if (!selected()) return;
// translatePage() puts up the notice; it owns which state it shows.
translatePage();
}
function bind() {
if (window.__autoTranslateBound) return;
window.__autoTranslateBound = true;
// Capture phase: lang-switcher.js listens on document during bubble, so
// this runs first — early enough to claim our own options, and to clear
// the selection before it navigates away to a real edition.
document.addEventListener(
"click",
function (e) {
if (!e.target || !e.target.closest) return;
var auto = e.target.closest(".lang-menu__option--auto");
if (auto) {
e.stopPropagation();
e.preventDefault();
var conf = config();
if (conf) activate(auto.getAttribute("data-auto-lang"), conf);
return;
}
// Choosing a real edition leaves this tier.
var edition = e.target.closest(".lang-menu__option[data-lang-code]");
if (edition) select(null);
},
true
);
}
bind();
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", render);
} else {
render();
}
// Material swaps pages without a reload; re-add the notice and re-translate.
// This file is listed after lang-switcher.js in mkdocs.yml, so its subscriber
// runs first and the menu is already rebuilt by the time we sync it.
if (window.document$) window.document$.subscribe(render);
})();
+623
View File
@@ -0,0 +1,623 @@
/* ===========================================================================
深入理解 AI Agent — 把 Material 主题「去装饰」,复刻 icyfenix.cn 的扁平风
===========================================================================
设计参考:实测 icyfenix.cn 的 computed style(2026-07)
- navbar 57px / 白底 / 1px 浅灰下划线
- sidebar 320px / 白底 / 1px 浅灰右边线
- 主文字色 #2c3e50(深蓝灰,而非纯黑)
- h1 41.6px
- 正文 16px / 1.7
- 链接默认与正文同色,hover 时变蓝(扁平、不张扬)
策略:不换主题(保留 Material 的搜索/多语言/暗色/i18n/分享卡等所有功能),
只用 CSS 关掉它的「卡片化」「阴影」「圆角」「紫色调」等装饰,
让页面回归「白底 + 左侧栏 + 文字」的扁平形态。
=========================================================================== */
/* ── 0. 设计 token(对齐 fenix 实测值) ─────────────────────────── */
:root {
--md-text-font: "Noto Sans SC", "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", -apple-system, BlinkMacSystemFont,
"Segoe UI", Helvetica, Arial, sans-serif;
--md-code-font: "JetBrains Mono", "Fira Code", SFMono-Regular, Consolas,
Menlo, monospace;
/* fenix 调色板 */
--fenix-ink: #2c3e50; /* 主文字 + h1 + 链接默认色 */
--fenix-ink-soft: #57606a; /* 次级文字 */
--fenix-ink-mute: #8b949e; /* 弱化文字 */
--fenix-border: #eaecef; /* navbar/sidebar 的细分隔线 */
--fenix-link: #3884fe; /* fenix 实测的正文链接蓝 */
--fenix-link-hover: #42b983; /* fenix 经典墨绿 hover */
--fenix-bg: #ffffff;
--fenix-bg-soft: #f6f8fa;
--fenix-code-bg: rgba(27, 31, 35, 0.05);
--fenix-code-ink: #476582;
/* 让 Material 也用上这些 token */
--md-primary-fg-color: #2c3e50;
--md-primary-bg-color: #ffffff;
--md-default-fg-color: #2c3e50;
--md-default-fg-color--soft: #57606a;
--md-accent-fg-color: #42b983;
}
/* Prefer Korean glyph forms only while viewing the Korean edition. Keeping
this override language-scoped avoids changing Chinese glyphs site-wide. */
html[lang="ko"] {
--md-text-font: "Noto Sans CJK KR", "Apple SD Gothic Neo", "Malgun Gothic",
"Noto Sans SC", "PingFang SC", -apple-system,
blinkmacsystemfont, "Segoe UI", helvetica, arial, sans-serif;
}
/* ── 1. 关掉 Material 的卡片化、阴影、圆角 ────────────────────── */
/* 全局去掉 box-shadow —— 这是 Material 最显眼的装饰 */
.md-header,
.md-sidebar,
.md-tabs,
.md-search,
.md-nav,
.md-typeset .admonition,
.md-typeset details,
.md-typeset pre,
.md-typeset table:not([class]) {
box-shadow: none !important;
}
/* navbar:扁平、白底、单线分隔(对齐 fenix 的 57px / 1px #eaecef) */
.md-header {
background-color: var(--fenix-bg) !important;
border-bottom: 1px solid var(--fenix-border) !important;
color: var(--fenix-ink) !important;
height: 57px;
box-shadow: none !important;
transition: none;
}
.md-header__title { color: var(--fenix-ink) !important; font-weight: 600; }
.md-header__topic { color: var(--fenix-ink) !important; }
/* 顶部 header 里的搜索框 —— 扁平化 */
.md-search__input {
background-color: var(--fenix-bg-soft) !important;
color: var(--fenix-ink) !important;
border-radius: 4px !important;
}
.md-search__input::placeholder { color: var(--fenix-ink-mute); }
/* ── 1b. 整页布局:全宽(fenix/VitePress 几何)──────────────────
Material 默认把整页装进 61rem 的 .md-grid 盒子:1920px 屏上两侧
各浪费 ~350px,正文却只有 ~660px,宽图和代码块很挤。桌面端解除
上限:header 与左侧栏贴住视口左缘;正文限制在书页式行宽内
(~50 汉字/行),与右侧大纲作为一个整体在剩余空间居中——正文的
margin-left:auto 和大纲的 margin-right:auto 平分空白,大纲因此
紧贴正文而不是漂到视口最右缘。
只在桌面端覆写:移动端抽屉几何被 Material 硬编码(见 2 节)。 */
@media screen and (min-width: 76.25em) {
.md-grid { max-width: 100%; }
.md-header__inner { padding-left: 0.8rem; padding-right: 0.8rem; }
.md-content { max-width: 42rem; margin-left: auto; margin-right: 0; }
.md-sidebar--secondary { margin-right: auto; }
}
/* Material turns the chapter navigation into a drawer below 76.25rem and
already provides its own menu button there. On persistent-sidebar layouts,
expose the companion control added in the header override. Hiding the
primary sidebar removes it from the flex layout, so the article and its
outline automatically recenter in the newly available reading space. */
.sidebar-toggle:not([hidden]) {
display: none;
}
@media screen and (min-width: 76.25em) {
.sidebar-toggle:not([hidden]) {
display: inline-flex;
}
.sidebar-toggle svg {
transition: transform 0.15s ease;
}
.sidebar-nav-collapsed .sidebar-toggle svg {
transform: scaleX(-1);
}
.sidebar-nav-collapsed .md-sidebar--primary {
display: none;
}
}
/* ── 2. 左侧栏(书的目录树):白底、单线分隔 ─────────────────────
结构(navigation.sections + navigation.indexes):
<li class="md-nav__item--section md-nav__item--nested">
<div class="md-nav__container md-nav__link">
<a>章节标题(点击进入正文)</a>
<label>折叠箭头(点击展开/收起)</label>
</div>
<nav>…配套实验…</nav>
</li>
桌面端的折叠行为由 nav-collapse.js 注入的 CSS 控制。 */
/* 宽度只在桌面端覆写:移动端抽屉的收起位置被 Material 硬编码为
-12.1rem,改宽会让抽屉在收起时露出一条边(之前的 320px 全局
覆写正是这个 bug 的来源)。 */
@media screen and (min-width: 76.25em) {
.md-sidebar--primary {
width: 15.5rem;
padding-left: 0.6rem; /* 全宽布局下不让内容贴死视口左缘 */
border-right: 1px solid var(--fenix-border);
}
}
/* 侧边栏链接 —— 扁平、深蓝灰、无 hover 背景,只在 hover 时变墨绿 */
.md-nav__link {
color: var(--fenix-ink) !important;
font-size: 0.78rem;
margin: 0;
padding: 6px 0 6px 0;
transition: color .12s;
}
.md-nav__link:hover,
.md-nav__link:hover * {
color: var(--fenix-link-hover) !important;
}
/* 章节标题行:标题 <a> 占满剩余宽度,箭头 <label> 靠右。
flex-start 让箭头对齐标题的第一行(居中时,折成两行的长标题
会把箭头挤到两行中间,和单行章节高低不一);箭头在首行内的
垂直居中由 nav-collapse.js 注入的 label padding 完成。 */
.md-sidebar--primary .md-nav__item--nested > .md-nav__container {
display: flex;
align-items: flex-start;
padding: 0;
}
/* 章节之间稍微多一点呼吸感(Material 默认 1.25em) */
.md-sidebar--primary .md-nav__item--section {
margin: 1.4em 0;
}
.md-sidebar--primary .md-nav__item--nested > .md-nav__container > a.md-nav__link {
flex: 1;
font-weight: 600;
font-size: 0.78rem;
letter-spacing: 0.02em;
}
/* 当前页面对应的链接 —— 墨绿高亮 + 左侧 2px 指示条(VitePress 风) */
.md-sidebar--primary a.md-nav__link--active,
.md-sidebar--primary a.md-nav__link--active * {
color: var(--fenix-link-hover) !important;
}
.md-sidebar--primary a.md-nav__link--active {
border-left: 2px solid var(--fenix-link-hover);
padding-left: 8px;
margin-left: -10px;
}
/* 配套实验子项已有自己的 1px 从属线(下方 2b),不叠加指示条;
子项规则特异性更高,border/padding 本就不会被覆盖,这里只还原
margin,避免子项整体左移 10px。 */
.md-sidebar--primary .md-nav__item--nested .md-nav a.md-nav__link--active {
margin-left: 0;
}
/* 展开后的子项(配套实验)—— 缩进一层、次级颜色、左侧细线示从属 */
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link {
padding-left: 0.7rem;
border-left: 1px solid var(--fenix-border);
color: var(--fenix-ink-soft) !important;
font-weight: 400;
}
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link:hover,
.md-sidebar--primary .md-nav__item--nested .md-nav .md-nav__link:hover * {
color: var(--fenix-link-hover) !important;
}
.md-sidebar--primary .md-nav__item--nested .md-nav a.md-nav__link--active {
border-left-color: var(--fenix-link-hover);
}
/* 侧栏顶部的 sticky 站点标题:压过下方内容,避免滚动时叠字;
背景跟随 fenix 底色(否则暗色模式下露出 Material 默认底色的色块) */
@media screen and (min-width: 76.25em) {
.md-nav--primary > .md-nav__title {
z-index: 3;
background: var(--fenix-bg);
box-shadow: none;
}
}
/* 章节分组标题 —— 稍粗、深蓝灰 */
.md-nav__title {
color: var(--fenix-ink) !important;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.02em;
}
/* ── 2b. 右侧栏(本页大纲):弱化存在感,阅读时的"你在这里" ──── */
/* 同上:宽度只在桌面端覆写(移动端的收起位置同样被硬编码)。
13.5rem 让大部分中文标题单行放下,不再折成两行。 */
@media screen and (min-width: 76.25em) {
.md-sidebar--secondary {
width: 13.5rem;
padding-right: 0.6rem;
}
}
.md-nav--secondary .md-nav__title {
color: var(--fenix-ink-mute) !important;
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.06em;
box-shadow: none;
background: transparent;
}
.md-nav--secondary .md-nav__link {
color: var(--fenix-ink-soft) !important;
font-size: 0.72rem;
padding: 4px 0;
}
.md-nav--secondary .md-nav__link:hover,
.md-nav--secondary .md-nav__link:hover * {
color: var(--fenix-link-hover) !important;
}
/* 当前阅读位置(toc.follow 跟随滚动) */
.md-nav--secondary .md-nav__link--active,
.md-nav--secondary .md-nav__link--active * {
color: var(--fenix-link-hover) !important;
font-weight: 500;
}
/* 大纲的层级缩进线 */
.md-nav--secondary .md-nav__list .md-nav__list {
border-left: 1px solid var(--fenix-border);
padding-left: 0.55rem;
}
/* ── 3. 正文排版 ─────────────────────────────────────────────── */
.md-typeset {
font-size: 0.78rem; /* 中文密集排版,比 fenix 16px 略小更舒服 */
line-height: 1.65;
color: var(--fenix-ink);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
.md-typeset p,
.md-typeset ul,
.md-typeset ol,
.md-typeset blockquote {
margin-block: 0.9em;
orphans: 3;
widows: 3;
}
/* h1 —— 章节标题(对齐 fenix 41.6px) */
.md-typeset h1 {
color: var(--fenix-ink) !important;
font-weight: 600 !important;
font-size: 1.85rem !important; /* ≈ 29.6px,比 fenix 略小,适合中文 */
letter-spacing: -0.01em;
margin-top: 0;
margin-bottom: 1.2rem;
padding-bottom: 0;
border: none;
}
/* h2 —— 简洁,只一条细分隔线 */
.md-typeset h2 {
color: var(--fenix-ink);
font-weight: 600;
font-size: 1.55rem;
margin-top: 2.6rem;
margin-bottom: 1rem;
padding-bottom: 0.3rem;
border-left: none;
border-bottom: 1px solid var(--fenix-border);
}
/* h3 —— 小标题,深蓝灰 */
.md-typeset h3 {
color: var(--fenix-ink);
font-weight: 600;
font-size: 1.2rem;
margin-top: 1.8rem;
}
/* ── 4. 链接 —— 默认与正文同色,hover 变墨绿(对齐 fenix) ────── */
.md-typeset a,
.md-typeset a:visited,
.md-typeset a:-webkit-any-link {
color: var(--fenix-link) !important;
text-decoration: none;
border-bottom: none;
transition: color .12s;
}
.md-typeset a:hover {
color: var(--fenix-link-hover) !important;
text-decoration: none;
border-bottom: none;
}
/* ── 5. 行内代码 + 代码块 —— 对齐 fenix 浅灰底 + 蓝灰字 ───────── */
.md-typeset code {
color: var(--fenix-code-ink) !important;
background-color: var(--fenix-code-bg) !important;
padding: 0.15rem 0.35rem;
border-radius: 3px;
font-size: 0.85em;
word-break: break-word;
}
/* 代码块<pre>:沿用 Material 的语法高亮主题,只调字号/边距。
注意:不要强行换深色背景,会破坏 Material 的浅色高亮配色。 */
.md-typeset pre > code {
background: transparent;
color: inherit;
padding: 0;
font-size: 0.82rem;
line-height: 1.55;
}
.md-typeset pre {
margin: 1rem 0;
border-radius: 4px;
/* 给代码块一个稳定的浅灰底,让它在正文里清晰可辨。
不要用 Material 默认的纯白(在白底正文里融成一团)。 */
background: var(--fenix-bg-soft) !important;
}
/* ── 6. 表格 —— 去掉 Material 的强对比,扁平化 ─────────────────── */
.md-typeset table:not([class]) {
border: 1px solid var(--fenix-border);
border-radius: 4px;
box-shadow: none !important;
font-size: 0.85rem;
display: table;
width: 100%;
border-collapse: collapse;
border-spacing: 0;
margin: 1.2rem 0;
}
.md-typeset table:not([class]) th,
.md-typeset table:not([class]) td {
padding: 8px 12px; /* 行距更宽松 */
border: none;
vertical-align: top;
}
.md-typeset table:not([class]) th {
background: var(--fenix-bg-soft);
color: var(--fenix-ink);
font-weight: 600;
border-bottom: 1px solid var(--fenix-border);
text-align: left;
}
.md-typeset table:not([class]) td {
border-top: 1px solid var(--fenix-border);
}
.md-typeset table:not([class]) tr:nth-child(even) {
background: var(--fenix-bg); /* 偶数行不要花哨的灰条 */
}
.md-typeset table:not([class]) tr:hover td {
background: var(--fenix-bg-soft); /* hover 时淡淡提示 */
}
/* 表格里的链接保持低视觉权重,不要满表蓝绿一片 */
.md-typeset table:not([class]) a {
color: var(--fenix-link);
}
.md-typeset table:not([class]) a:hover {
color: var(--fenix-link-hover);
}
/* ── 7. 引用块 —— 极简左竖线,无背景填充 ───────────────────────── */
.md-typeset blockquote {
border-left: 3px solid var(--fenix-border);
background: transparent;
color: var(--fenix-ink-soft);
padding: 0.2rem 1rem;
border-radius: 0;
}
/* ── 8. 关掉首页 hero 区的紫色渐变(改回 fenix 那种纯净白底) ──── */
.hero {
background: var(--fenix-bg);
border: none;
border-radius: 0;
padding: 4rem 1rem 2rem;
text-align: center;
}
.hero h1 {
color: var(--fenix-ink);
font-size: 3rem;
font-weight: 600;
margin-bottom: 0.6rem;
}
.hero .hero-formula {
color: var(--fenix-link-hover); /* 墨绿公式色 */
font-size: 1.2rem;
margin: 1rem 0 1.6rem;
font-family: var(--md-code-font);
}
.hero .cta {
background: var(--fenix-ink);
color: var(--fenix-bg);
border-radius: 4px; /* 不用胶囊形,扁平 */
padding: 8px 16px;
font-weight: 500;
font-size: 0.88rem;
border: none;
}
.hero .cta:hover {
background: var(--fenix-link-hover);
opacity: 1;
transform: none;
}
.hero .cta.secondary {
background: transparent;
color: var(--fenix-ink);
border: 1px solid var(--fenix-border);
}
.hero .cta.secondary:hover {
border-color: var(--fenix-link-hover);
color: var(--fenix-link-hover);
}
/* ── 9. 章节卡片网格 —— 扁平化(去掉浮起+阴影) ────────────────── */
/* 首页三列统计(10 章 / 88 实验 / 5 语言) */
.stat-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin: 1.4rem 0 2rem;
padding: 0;
}
@media (max-width: 720px) {
.stat-row { grid-template-columns: 1fr; }
}
.stat-row p {
background: var(--fenix-bg);
border: 1px solid var(--fenix-border);
border-radius: 4px;
padding: 14px 16px;
margin: 0;
text-align: center;
font-size: 0.82rem;
color: var(--fenix-ink-soft);
}
.stat-row p strong {
display: block;
color: var(--fenix-ink);
font-size: 0.95rem;
margin-bottom: 3px;
font-weight: 600;
}
/* 章节卡片网格(首页 + 各章实验索引页通用) */
.exp-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1px;
background: var(--fenix-border);
border: 1px solid var(--fenix-border);
border-radius: 4px;
overflow: hidden;
margin: 1.4rem 0;
}
/* Markdown 会把每个 exp-card 包一层 <p>:清零其边距并让卡片撑满
格子,否则模拟 1px 边框的灰底会从缝隙里透出成大块灰带。 */
.exp-grid > p {
margin: 0;
}
.exp-card {
display: block;
height: 100%;
box-sizing: border-box;
background: var(--fenix-bg);
padding: 14px 16px;
text-decoration: none;
color: var(--fenix-ink);
border: none;
border-radius: 0;
transition: background .12s;
}
.exp-card:hover {
background: var(--fenix-bg-soft);
border-color: transparent;
transform: none;
box-shadow: none !important;
}
.exp-card .exp-title {
display: block;
color: var(--fenix-ink);
font-weight: 600;
font-size: 0.92rem;
margin-bottom: 6px;
line-height: 1.4;
}
.exp-card:hover .exp-title {
color: var(--fenix-link-hover);
}
.exp-card .exp-desc {
display: block;
color: var(--fenix-ink-soft);
font-size: 0.78rem;
line-height: 1.55;
}
/* ── 10. 移动端调整 ─────────────────────────────────────────── */
@media screen and (max-width: 600px) {
.md-typeset { font-size: 15px; }
.md-typeset h1 { font-size: 2rem; }
.md-typeset h2 { font-size: 1.3rem; }
.hero { padding: 2.5rem 1rem 1.5rem; }
.hero h1 { font-size: 2rem; }
}
/* ── 11. 暗色模式同步 —— 保持 fenix 风格,只是反色 ──────────────── */
[data-md-color-scheme="slate"] {
--fenix-ink: #e6edf3;
--fenix-ink-soft: #8b949e;
--fenix-ink-mute: #6e7681;
--fenix-border: #30363d;
--fenix-bg: #0d1117;
--fenix-bg-soft: #161b22;
--fenix-code-bg: rgba(240, 246, 252, 0.08);
--fenix-code-ink: #79c0ff;
--fenix-link: #e6edf3;
--fenix-link-hover: #42b983;
/* 让正文区背景与 header/侧栏一致(否则 Material slate 的默认底色
略浅,sticky 站点标题处会露出一块深色矩形)。 */
--md-default-bg-color: #0d1117;
--md-default-fg-color: #e6edf3;
}
[data-md-color-scheme="slate"] .md-header,
[data-md-color-scheme="slate"] .md-sidebar--primary {
background-color: var(--fenix-bg) !important;
border-color: var(--fenix-border) !important;
}
[data-md-color-scheme="slate"] .exp-card,
[data-md-color-scheme="slate"] .hero {
background: var(--fenix-bg);
}
/* ── 12. Arabic / RTL edition ─────────────────────────────────── */
html[dir="rtl"] body {
font-family: "Noto Naskh Arabic", Amiri, "Noto Sans Arabic",
-apple-system, BlinkMacSystemFont, sans-serif;
}
html[dir="rtl"] .md-typeset {
direction: rtl;
text-align: right;
}
html[dir="rtl"] .md-typeset pre,
html[dir="rtl"] .md-typeset code,
html[dir="rtl"] .md-typeset kbd,
html[dir="rtl"] .md-typeset samp,
html[dir="rtl"] .md-typeset .highlight,
html[dir="rtl"] .md-typeset .arithmatex,
html[dir="rtl"] .md-typeset .mermaid {
direction: ltr;
text-align: left;
unicode-bidi: isolate;
}
html[dir="rtl"] .md-typeset blockquote {
border-left: 0;
border-right: 0.2rem solid var(--fenix-border);
padding-left: 0;
padding-right: 0.8rem;
}
html[dir="rtl"] .md-typeset ul,
html[dir="rtl"] .md-typeset ol {
margin-left: 0;
margin-right: 1.25em;
}
html[dir="rtl"] .md-typeset table:not([class]) th,
html[dir="rtl"] .md-typeset table:not([class]) td {
text-align: right;
}
+177
View File
@@ -0,0 +1,177 @@
/* ── Custom language dropdown in the header bar ──────────── */
.lang-switcher {
position: relative;
display: inline-flex;
flex-shrink: 0;
margin-inline-start: 0.5rem;
}
.lang-select {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
min-height: 1.9rem;
padding: 0.32rem 0.48rem;
color: var(--fenix-ink, var(--md-default-fg-color));
background: transparent;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
font: inherit;
font-size: 0.68rem;
font-weight: 500;
letter-spacing: 0.01em;
line-height: 1;
white-space: nowrap;
transition: background-color 0.15s ease, border-color 0.15s ease,
color 0.15s ease;
}
.lang-select:hover,
.lang-select[aria-expanded="true"] {
color: var(--fenix-link-hover, var(--md-accent-fg-color));
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
border-color: var(--fenix-border, rgba(128, 128, 128, 0.25));
}
.lang-select:focus-visible {
outline: 2px solid var(--fenix-link-hover, var(--md-accent-fg-color));
outline-offset: 2px;
}
.lang-select__icon,
.lang-select__chevron {
display: inline-flex;
flex: 0 0 auto;
}
.lang-select__icon svg {
width: 0.92rem;
height: 0.92rem;
fill: currentcolor;
}
.lang-select__chevron svg {
width: 0.68rem;
height: 0.68rem;
fill: currentcolor;
transition: transform 0.15s ease;
}
.lang-select[aria-expanded="true"] .lang-select__chevron svg {
transform: rotate(180deg);
}
.lang-menu {
position: absolute;
z-index: 20;
inset-block-start: calc(100% + 0.42rem);
inset-inline-end: 0;
width: max-content;
min-width: 11.5rem;
max-height: min(25rem, calc(100vh - 4.5rem));
overflow-y: auto;
padding: 0.35rem;
color: var(--fenix-ink, var(--md-default-fg-color));
background: var(--fenix-bg, var(--md-default-bg-color));
border: 1px solid var(--fenix-border, rgba(128, 128, 128, 0.25));
border-radius: 8px;
box-shadow: 0 12px 32px rgba(27, 31, 35, 0.14),
0 2px 8px rgba(27, 31, 35, 0.08);
animation: lang-menu-enter 0.14s ease-out;
}
.lang-menu[hidden] {
display: none;
}
.lang-menu__option {
display: grid;
grid-template-columns: 0.8rem minmax(0, 1fr);
align-items: center;
gap: 0.48rem;
width: 100%;
padding: 0.48rem 0.58rem;
color: inherit;
background: transparent;
border: 0;
border-radius: 5px;
cursor: pointer;
font: inherit;
font-size: 0.68rem;
line-height: 1.35;
text-align: start;
}
.lang-menu__option:hover,
.lang-menu__option:focus-visible {
color: var(--fenix-link-hover, var(--md-accent-fg-color));
background: var(--fenix-bg-soft, rgba(128, 128, 128, 0.08));
outline: none;
}
.lang-menu__option[aria-checked="true"] {
color: var(--fenix-link-hover, var(--md-accent-fg-color));
background: rgba(66, 185, 131, 0.1);
font-weight: 600;
}
.lang-menu__check {
width: 0.55rem;
height: 0.3rem;
border-bottom: 2px solid currentcolor;
border-left: 2px solid currentcolor;
opacity: 0;
transform: translateY(-0.08rem) rotate(-45deg);
}
.lang-menu__option[aria-checked="true"] .lang-menu__check {
opacity: 1;
}
.lang-menu__label {
white-space: nowrap;
}
[data-md-color-scheme="slate"] .lang-menu {
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42),
0 2px 8px rgba(0, 0, 0, 0.3);
}
@keyframes lang-menu-enter {
from {
opacity: 0;
transform: translateY(-0.25rem) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.lang-menu,
.lang-select__chevron svg {
animation: none;
transition: none;
}
}
@media screen and (max-width: 44.984375em) {
.lang-switcher {
margin-inline-start: 0.15rem;
}
.lang-select {
width: 2rem;
min-height: 2rem;
padding: 0.4rem;
}
.lang-select__label,
.lang-select__chevron {
display: none;
}
}
+706
View File
@@ -0,0 +1,706 @@
// Language switcher: populates the custom dropdown in the header bar.
// On selection, navigates to the equivalent page in the target language and
// rewrites the left sidebar (links + text) to match the new edition.
//
// window.LANG_CONFIG = { zh: {label, prefix, default?}, ... }
// window.SITE_I18N = generated catalog from scripts/site_i18n.py
// window.SITE_ROOT = "https://bojieli.github.io/ai-agent-book"
(function () {
"use strict";
// Don't run if LANG_CONFIG hasn't been injected by header.html yet.
// header.html emits the <script>window.LANG_CONFIG = ...</script> before
// this file loads, so this is just defensive.
function bindWhenReady() {
var cfg = window.LANG_CONFIG;
var i18n = window.SITE_I18N;
if (!cfg || !i18n) {
// Retry shortly — header.html or the generated catalog may not have
// loaded yet if a proxy changed script loading behavior.
setTimeout(bindWhenReady, 50);
return;
}
init(cfg, i18n);
}
function init(cfg, i18n) {
// ── helpers ───────────────────────────────────────────────
function detectLang(path) {
// Match against prefix with trailing slash stripped, so both
// "/book-en/" and "/book-en" map to "en".
var p = path.replace(/\/$/, "");
var codes = Object.keys(cfg).sort(function (a, b) {
return cfg[b].prefix.length - cfg[a].prefix.length;
});
for (var i = 0; i < codes.length; i++) {
var prefix = cfg[codes[i]].prefix.replace(/\/$/, "");
if (p.indexOf(prefix) !== -1) return codes[i];
}
// Translated experiment indexes have no book prefix, but their
// README suffix identifies the locale unambiguously. Detect it before
// consulting sessionStorage so direct links work in a fresh session.
var readmeMatch = p.match(/(?:^|\/)chapter\d+\/README\.([a-zA-Z-]+)$/);
if (readmeMatch) {
for (var r = 0; r < codes.length; r++) {
if (cfg[codes[r]].readmeSuffix === readmeMatch[1]) return codes[r];
}
}
// Translated homepages (/index.<code>/) carry their locale in the slug.
var homeMatch = p.match(/(?:^|\/)index\.([a-zA-Z-]+)$/);
if (homeMatch && cfg[homeMatch[1]]) return homeMatch[1];
// The site root always serves the default-language homepage (translated
// homepages are matched above), so don't let a remembered locale make
// the Chinese homepage look pre-translated.
if (p === "" || p === "index.html" || p === "/index.html") {
for (var h in cfg) {
if (cfg.hasOwnProperty(h) && cfg[h].default) return h;
}
return "zh";
}
// No language prefix matched. This happens on /chapterN/ experiment
// index pages (experiments are language-agnostic, single copy).
// Fall back to whatever the user last selected — stored in
// sessionStorage so it survives SPA navigation and reloads.
var remembered = null;
try { remembered = sessionStorage.getItem("lang-switcher-active"); } catch (_) {}
if (remembered && cfg[remembered]) return remembered;
for (var c in cfg) {
if (cfg.hasOwnProperty(c) && cfg[c].default) return c;
}
return "zh";
}
function rememberLang(code) {
try { sessionStorage.setItem("lang-switcher-active", code); } catch (_) {}
}
// ── URL rewriting ────────────────────────────────────────
// One function handles every URL case so there are no scattered patches.
// Given the current path + target language, returns the new path under
// the same site base, or null if no translation applies.
//
// URL shapes we have to handle:
// / → site home, default language
// /index.<code>/ → site home, translated (when the root
// file index.<code>.md exists)
// /book[-lang]/chapterN[.suffix]/ → chapter prose
// /chapterN/ → experiment index, Chinese (README.md)
// /chapterN/README.<readmeSuffix>/ → experiment index, translated
// /chapterN/<exp>/ → individual experiment, Chinese only
// (jump to target lang's chapter prose)
function translatePath(cleanPath, fromCode, toCode) {
if (toCode === fromCode) return null;
var src = cfg[fromCode];
var dst = cfg[toCode];
// Site home. Editions with a translated homepage (root index.<code>.md,
// listed by scripts/site_i18n.py in the generated catalog) map
// home → home; the rest keep the original fallback to their
// introduction page, which every edition has.
var homePages = i18n.homePages || [];
if (cleanPath === "/" || cleanPath === "/index.html") {
if (homePages.indexOf(toCode) !== -1) return "/index." + toCode + "/";
return "/" + dst.prefix + "introduction" + (dst.suffix || "") + "/";
}
var pp = cleanPath.replace(/^\//, "").replace(/\/$/, "");
// Translated homepage: /index.<code>/
var homeMatch = pp.match(/^index\.([a-zA-Z-]+)$/);
if (homeMatch) {
if (dst.default) return "/";
if (homePages.indexOf(toCode) !== -1) return "/index." + toCode + "/";
return "/" + dst.prefix + "introduction" + (dst.suffix || "") + "/";
}
// Chapter prose: <srcPrefix>chapterN[<srcSuffix>]
// E.g. /book/chapter1/ or /book-zhtw/chapter1.zhtw/
var proseRe = new RegExp("^" + escapeRe(src.prefix) + "chapter(\\d+)" + escapeRe(src.suffix || "") + "$");
var proseMatch = pp.match(proseRe);
if (proseMatch) {
return "/" + dst.prefix + "chapter" + proseMatch[1] + (dst.suffix || "") + "/";
}
// Handling book pages that use a shared ASCII slug:
// introduction, afterword, reference-answers, appendix, ...
var bookPageRe = new RegExp(
"^" +
escapeRe(src.prefix) +
"([a-z0-9-]+)" +
escapeRe(src.suffix || "") +
"$"
);
var bookPageMatch = pp.match(bookPageRe);
if (bookPageMatch) {
return (
"/" +
dst.prefix +
bookPageMatch[1] +
(dst.suffix || "") +
"/"
);
}
// Experiment index: /chapterN/ (Chinese default) or
// /chapterN/README.<readmeSuffix>/ (translated variants).
if (/^chapter\d+$/.test(pp)) {
// Chinese experiment index. Switch to:
// zh → /chapterN/ (unchanged)
// other → /chapterN/README.<readmeSuffix>/
if (toCode === "zh") return "/" + pp + "/";
if (dst.readmeSuffix) return "/" + pp + "/README." + dst.readmeSuffix + "/";
// An edition can launch before its companion experiment indexes are
// translated. Keep the switch inside translated content instead of
// manufacturing a README.undefined URL.
return "/" + dst.prefix + pp + (dst.suffix || "") + "/";
}
var readmeMatch = pp.match(/^chapter(\d+)\/README\.([a-zA-Z-]+)$/);
if (readmeMatch) {
if (toCode === "zh") return "/chapter" + readmeMatch[1] + "/";
if (dst.readmeSuffix) {
return "/chapter" + readmeMatch[1] + "/README." + dst.readmeSuffix + "/";
}
return "/" + dst.prefix + "chapter" + readmeMatch[1] + (dst.suffix || "") + "/";
}
// Individual experiment page: /chapterN/<something>/ — Chinese only.
// No translated copy exists, so jump to the target language's
// chapter prose (the most useful nearby translated page).
var expSubMatch = pp.match(/^(chapter\d+)\/[^?]+$/);
if (expSubMatch && pp.indexOf("README.") === -1) {
return "/" + dst.prefix + expSubMatch[1] + (dst.suffix || "") + "/";
}
return null;
}
function escapeRe(s) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
// ── generated theme chrome localization ──────────────────
var CHROME_SELECTOR = [
"[data-md-component='skip']",
"[data-md-component='announce']",
".md-header",
".md-search",
".md-sidebar",
".md-content__button",
".md-source-file",
".md-top",
".md-footer",
".md-dialog",
".md-tooltip",
".md-clipboard",
].join(",");
function translationPairs(targetCode) {
var source = i18n.languages[i18n.default];
var target = i18n.languages[targetCode];
if (!source || !target) return [];
var pairs = [];
var key;
for (key in source.ui) {
if (source.ui.hasOwnProperty(key) && target.ui[key]) {
pairs.push([source.ui[key], target.ui[key]]);
}
}
["sidebar", "palette"].forEach(function (group) {
for (key in source[group]) {
if (source[group].hasOwnProperty(key) && target[group][key]) {
pairs.push([source[group][key], target[group][key]]);
}
}
});
// Prefer the most specific string when one translation is a substring
// of another (for example, Search and Initializing search).
return pairs.sort(function (a, b) { return b[0].length - a[0].length; });
}
function translateChromeValue(value, pairs) {
var trimmed = value.trim();
if (!trimmed) return value;
for (var i = 0; i < pairs.length; i++) {
var from = pairs[i][0];
var to = pairs[i][1];
if (trimmed === from) {
return value.slice(0, value.indexOf(trimmed)) + to + value.slice(value.indexOf(trimmed) + trimmed.length);
}
if (from.indexOf("#") !== -1) {
var match = trimmed.match(new RegExp("^" + escapeRe(from).replace("#", "(.+?)") + "$"));
if (match) {
var rendered = to.replace("#", match[1]);
return value.slice(0, value.indexOf(trimmed)) + rendered + value.slice(value.indexOf(trimmed) + trimmed.length);
}
}
}
return value;
}
function localizeTree(root, pairs) {
if (!root) return;
var element = root.nodeType === 1 ? root : root.parentElement;
if (!element) return;
var attributed = [];
if (element.matches && element.matches("[title],[aria-label],[placeholder]")) attributed.push(element);
if (element.querySelectorAll) {
attributed = attributed.concat(
Array.prototype.slice.call(element.querySelectorAll("[title],[aria-label],[placeholder]"))
);
}
for (var a = 0; a < attributed.length; a++) {
["title", "aria-label", "placeholder"].forEach(function (name) {
if (!attributed[a].hasAttribute(name)) return;
var before = attributed[a].getAttribute(name);
var after = translateChromeValue(before, pairs);
if (after !== before) attributed[a].setAttribute(name, after);
});
}
var walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
var textNode;
while ((textNode = walker.nextNode())) {
var translated = translateChromeValue(textNode.nodeValue, pairs);
if (translated !== textNode.nodeValue) textNode.nodeValue = translated;
}
}
function localizeRevisionDates(targetCode) {
var strings = i18n.languages[targetCode];
if (!strings || !window.Intl || !Intl.DateTimeFormat) return;
var nodes = document.querySelectorAll(".git-revision-date-localized-plugin-date");
var visibleFormat = new Intl.DateTimeFormat(strings.locale, {
year: "numeric", month: "long", day: "numeric", timeZone: "UTC",
});
var titleFormat = new Intl.DateTimeFormat(strings.locale, {
year: "numeric", month: "long", day: "numeric",
hour: "2-digit", minute: "2-digit", second: "2-digit",
timeZone: "UTC", timeZoneName: "short",
});
for (var d = 0; d < nodes.length; d++) {
var original = nodes[d].getAttribute("title") || nodes[d].textContent;
var match = original.match(/^(\d{4})年(\d{1,2})月(\d{1,2})日(?:\s+(\d{1,2}):(\d{2}):(\d{2}))?/);
if (!match) continue;
var date = new Date(Date.UTC(
Number(match[1]), Number(match[2]) - 1, Number(match[3]),
Number(match[4] || 0), Number(match[5] || 0), Number(match[6] || 0)
));
nodes[d].textContent = visibleFormat.format(date);
nodes[d].setAttribute("title", titleFormat.format(date));
}
}
function localizeChrome(targetCode) {
if (targetCode === i18n.default || !i18n.languages[targetCode]) return;
var pairs = translationPairs(targetCode);
var roots = document.querySelectorAll(CHROME_SELECTOR);
for (var r = 0; r < roots.length; r++) localizeTree(roots[r], pairs);
localizeRevisionDates(targetCode);
// Search results, copy-button tooltips, and dialogs are populated after
// load. Translate only newly-created theme chrome, never book content.
if (!window.__siteI18nObserver && document.body) {
window.__siteI18nObserver = new MutationObserver(function (mutations) {
for (var m = 0; m < mutations.length; m++) {
var changed = mutations[m].type === "characterData"
? mutations[m].target.parentElement
: mutations[m].target;
if (!changed || !changed.closest || !changed.closest(CHROME_SELECTOR)) continue;
localizeTree(changed, pairs);
if (changed.closest(".md-source-file")) localizeRevisionDates(targetCode);
}
});
window.__siteI18nObserver.observe(document.body, {
childList: true,
characterData: true,
attributes: true,
attributeFilter: ["title", "aria-label", "placeholder"],
subtree: true,
});
}
}
function siteBasePath() {
var p = location.pathname;
try {
var configured = new URL(window.SITE_ROOT).pathname.replace(/\/$/, "");
// Use the configured deployment subpath when it is actually present.
// Local preview servers commonly mount at /, so they fall through to
// prefix discovery below instead of inheriting the production path.
if (configured && configured !== "/" &&
(p === configured || p.indexOf(configured + "/") === 0)) {
return configured;
}
} catch (_) {}
var best = -1;
for (var code in cfg) {
if (!cfg.hasOwnProperty(code)) continue;
var idx = p.indexOf(cfg[code].prefix);
if (idx !== -1 && (best === -1 || idx < best)) best = idx;
}
if (best !== -1) return p.slice(0, best);
return "/";
}
// ── sidebar rewriting (links + text) ──────────────────────
function rewriteSidebar(targetCode) {
var target = cfg[targetCode];
var strings = i18n.languages[targetCode];
var defCode = null;
for (var c in cfg) { if (cfg[c].default) { defCode = c; break; } }
defCode = defCode || "zh";
var base = siteBasePath();
if (base.charAt(base.length - 1) !== "/") base += "/";
var links = document.querySelectorAll(".md-nav__link");
for (var i = 0; i < links.length; i++) {
var el = links[i];
var href = el.getAttribute("href");
var navText = el.querySelector(".md-ellipsis");
var currentText = navText ? navText.textContent.trim() : "";
if (href && href.charAt(0) !== "#") {
// Resolve href to a clean path relative to docs root, then
// translate it via the unified translatePath() function. This
// handles prose links, experiment-index links, and the Chinese
// default in one place — no scattered patches.
try {
var u = new URL(href, location.href);
if (u.origin === location.origin) {
var linkPath = u.pathname;
if (linkPath.indexOf(base) === 0) {
var linkRel = "/" + linkPath.slice(base.length).replace(/^\//, "");
var linkLang = detectLang(linkRel);
// The canonical sidebar is rendered from the default-language
// nav, so an un-suffixed /chapterN/ link always starts as the
// default experiment index. Do not let the remembered active
// locale make it look pre-translated.
if (/^\/chapter\d+\/?$/.test(linkRel)) {
linkLang = defCode;
if (targetCode !== defCode && !target.readmeSuffix) {
// Do not advertise a translated experiment index that
// does not exist yet. The chapter prose remains linked by
// the parent entry and every visible link stays valid.
var item = el.closest(".md-nav__item");
if (item) item.hidden = true;
}
}
var translated = translatePath(linkRel, linkLang, targetCode);
if (translated) {
el.setAttribute("href", base + translated.replace(/^\//, ""));
}
}
}
} catch (_) {}
}
if (navText && strings.nav[currentText]) {
navText.textContent = strings.nav[currentText];
}
}
// Translate the drawer's per-chapter sub-nav headers. When a chapter
// subtree is opened on mobile, Material shows the chapter title again
// as <label class="md-nav__title">第2章 …</label>; those labels are
// plain text (not .md-nav__link), so the loop above misses them. The
// site-name title at the top is not in the nav catalog and stays untouched.
var subTitles = document.querySelectorAll(".md-sidebar--primary .md-nav__title");
for (var st = 0; st < subTitles.length; st++) {
var stNodes = subTitles[st].childNodes;
for (var sn = 0; sn < stNodes.length; sn++) {
var node = stNodes[sn];
if (node.nodeType !== 3) continue;
var key = node.textContent.trim();
if (key && strings.nav[key]) {
node.textContent = strings.nav[key];
}
}
}
localizeChrome(targetCode);
}
// ── language switch (the actual navigation) ──────────────
function applyDocumentLocale(code) {
var strings = i18n.languages[code] || i18n.languages[i18n.default];
document.documentElement.lang = strings.locale;
document.documentElement.dir = strings.direction;
if (document.body) document.body.setAttribute("dir", strings.direction);
window.siteCurrentLanguage = code;
}
function switchTo(target) {
var rawPath = location.pathname;
var basePath = siteBasePath();
var cleanPath = "/" + rawPath.slice(basePath.length).replace(/^\//, "");
var activeLang = detectLang(cleanPath);
applyDocumentLocale(activeLang);
if (!target || target === activeLang) return;
var rel = translatePath(cleanPath, activeLang, target);
if (!rel) return;
var siteRoot = window.SITE_ROOT.replace(/\/$/, "") + "/";
var finalUrl = siteRoot + rel.replace(/^\//, "");
// Force a full page reload (bypass Material's navigation.instant, which
// intercepts location.href and may bounce the user back). We're moving
// to a different language edition, which is a different "site" — full
// reload is the right semantic anyway.
window.location.replace(finalUrl);
}
// ── custom dropdown ─────────────────────────────────────
function closeDropdown(returnFocus) {
var trigger = document.getElementById("lang-selector");
var menu = document.getElementById("lang-menu");
if (!trigger || !menu) return;
trigger.setAttribute("aria-expanded", "false");
menu.hidden = true;
if (returnFocus) trigger.focus();
}
function openDropdown(focusDirection) {
var trigger = document.getElementById("lang-selector");
var menu = document.getElementById("lang-menu");
if (!trigger || !menu) return;
trigger.setAttribute("aria-expanded", "true");
menu.hidden = false;
if (focusDirection) {
var options = menu.querySelectorAll(".lang-menu__option");
if (!options.length) return;
var target = menu.querySelector('[aria-checked="true"]');
if (focusDirection === "first") target = options[0];
if (focusDirection === "last") target = options[options.length - 1];
(target || options[0]).focus();
}
}
function moveMenuFocus(current, amount) {
var menu = document.getElementById("lang-menu");
if (!menu) return;
var options = Array.prototype.slice.call(
menu.querySelectorAll(".lang-menu__option")
);
if (!options.length) return;
var currentIndex = options.indexOf(current);
var nextIndex = (currentIndex + amount + options.length) % options.length;
options[nextIndex].focus();
}
function optionLocale(code) {
return i18n.languages[code] ? i18n.languages[code].locale : code;
}
function render() {
var rawPath = location.pathname;
var basePath = siteBasePath();
var cleanPath = "/" + rawPath.slice(basePath.length).replace(/^\//, "");
var activeLang = detectLang(cleanPath);
applyDocumentLocale(activeLang);
var trigger = document.getElementById("lang-selector");
var menu = document.getElementById("lang-menu");
if (!trigger || !menu) {
// Localization is useful even if a downstream theme override removes
// the selector itself. Do not make translated navigation depend on
// that optional header control.
rememberLang(activeLang);
if (activeLang !== i18n.default) rewriteSidebar(activeLang);
return;
}
// Build menu items on first sight of an empty dropdown.
if (menu.children.length === 0) {
var codes = Object.keys(cfg);
for (var idx = 0; idx < codes.length; idx++) {
var code = codes[idx];
var option = document.createElement("button");
option.type = "button";
option.className = "lang-menu__option";
option.setAttribute("role", "menuitemradio");
option.setAttribute("data-lang-code", code);
option.setAttribute(
"aria-checked",
code === activeLang ? "true" : "false"
);
option.setAttribute("tabindex", "-1");
var check = document.createElement("span");
check.className = "lang-menu__check";
check.setAttribute("aria-hidden", "true");
var label = document.createElement("span");
label.className = "lang-menu__label";
label.setAttribute("lang", optionLocale(code));
label.setAttribute(
"dir",
i18n.languages[code].direction === "rtl" ? "rtl" : "auto"
);
label.textContent = cfg[code].label;
option.appendChild(check);
option.appendChild(label);
menu.appendChild(option);
}
}
// Keep the trigger and checked item in sync after SPA navigation.
var currentLabel = cfg[activeLang].label;
var labelNode = trigger.querySelector("[data-lang-label]");
if (labelNode) {
labelNode.textContent = currentLabel;
labelNode.setAttribute("lang", optionLocale(activeLang));
labelNode.setAttribute(
"dir",
i18n.languages[activeLang].direction === "rtl" ? "rtl" : "auto"
);
}
trigger.setAttribute(
"aria-label",
i18n.languages[activeLang].ui["select.language"] + ": " + currentLabel
);
var options = menu.querySelectorAll(".lang-menu__option");
for (var optionIndex = 0; optionIndex < options.length; optionIndex++) {
var isActive =
options[optionIndex].getAttribute("data-lang-code") === activeLang;
options[optionIndex].setAttribute("aria-checked", isActive ? "true" : "false");
}
closeDropdown(false);
var defCode = null;
for (var c in cfg) { if (cfg[c].default) { defCode = c; break; } }
rememberLang(activeLang);
if (activeLang !== (defCode || "zh")) {
rewriteSidebar(activeLang);
}
}
// ── bootstrap ────────────────────────────────────────────
// Bind handlers once via event delegation so the dropdown keeps working
// if Material re-creates the header during SPA navigation.
if (!window.__langSwitcherBound) {
window.__langSwitcherBound = true;
document.addEventListener("click", function (e) {
if (!e.target || !e.target.closest) return;
var trigger = e.target.closest("#lang-selector");
if (trigger) {
var isOpen = trigger.getAttribute("aria-expanded") === "true";
if (isOpen) closeDropdown(false);
else openDropdown(false);
return;
}
var option = e.target.closest(".lang-menu__option");
if (option) {
var targetCode = option.getAttribute("data-lang-code");
closeDropdown(false);
switchTo(targetCode);
return;
}
if (!e.target.closest(".lang-switcher")) closeDropdown(false);
});
document.addEventListener("keydown", function (e) {
if (!e.target || !e.target.closest) return;
var trigger = e.target.closest("#lang-selector");
if (trigger) {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
openDropdown(e.key === "ArrowDown" ? "first" : "last");
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (trigger.getAttribute("aria-expanded") === "true") {
closeDropdown(false);
} else {
openDropdown("current");
}
} else if (e.key === "Escape") {
closeDropdown(false);
}
return;
}
var option = e.target.closest(".lang-menu__option");
if (!option) return;
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
moveMenuFocus(option, e.key === "ArrowDown" ? 1 : -1);
} else if (e.key === "Home" || e.key === "End") {
e.preventDefault();
openDropdown(e.key === "Home" ? "first" : "last");
} else if (e.key === "Escape") {
e.preventDefault();
closeDropdown(true);
} else if (e.key === "Tab") {
closeDropdown(false);
} else if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
option.click();
}
});
}
// Exposed for extras/auto-translate.js, which routes the reader to the
// source edition before overlaying machine translation on a language we
// do not build. Deliberately limited to the two operations it needs
// rather than exporting the whole module.
window.langSwitcher = {
// Language code of the edition the current URL belongs to.
current: function () {
return detectLang(cleanPathname());
},
// Absolute URL of this page in `code`, or null when the current page
// has no counterpart there (or is already in that edition).
urlFor: function (code) {
var cleanPath = cleanPathname();
var rel = translatePath(cleanPath, detectLang(cleanPath), code);
if (!rel) return null;
return (
window.SITE_ROOT.replace(/\/$/, "") + "/" + rel.replace(/^\//, "")
);
},
};
function cleanPathname() {
var basePath = siteBasePath();
return "/" + location.pathname.slice(basePath.length).replace(/^\//, "");
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", render);
} else {
render();
}
// Re-run on every Material SPA navigation. Material exposes document$
// (a ReactiveSubscribable) that fires after each navigation.instant
// page swap. Without this hook, the sidebar DOM gets re-rendered by
// Material with the original (Chinese) nav text and we never get to
// translate it for non-default languages.
if (window.document$) {
window.document$.subscribe(render);
} else {
// Fallback for older Material or other themes.
document.addEventListener("locationchange", render);
var _pushState = history.pushState;
history.pushState = function () {
_pushState.apply(this, arguments);
setTimeout(render, 60);
};
}
}
bindWhenReady();
})();
+37
View File
@@ -0,0 +1,37 @@
/**
* MathJax 3 configuration for MkDocs Material.
*
* This file MUST be loaded before the MathJax bundle
* (`tex-mml-chtml.js`) in mkdocs.yml `extra_javascript` — MathJax reads
* `window.MathJax` once at startup, so setting it afterwards has no effect.
*
* `pymdownx.arithmatex` (generic mode) rewrites the book's `$...$` and
* `$$...$$` source math into `\(...\)` / `\[...\]` wrapped in
* `<span class="arithmatex">` / `<div class="arithmatex">`. MathJax's
* default delimiters are `$...$`/`$$...$$`, which no longer exist in the
* rendered HTML, so without this config nothing is typeset at all.
*/
window.MathJax = {
tex: {
inlineMath: [['\\(', '\\)']],
displayMath: [['\\[', '\\]']],
processEscapes: true,
processEnvironments: true,
},
options: {
// Only typeset elements arithmatex marked as math; ignore everything
// else (code blocks, search summaries, etc.).
ignoreHtmlClass: '.*|',
processHtmlClass: 'arithmatex',
},
};
// Material's `navigation.instant` swaps page content without a full reload,
// so MathJax must re-typeset after every page swap (same pattern as
// extras/mermaid-init.js).
document$.subscribe(() => {
MathJax.startup.output.clearCache();
MathJax.typesetClear();
MathJax.texReset();
MathJax.typesetPromise();
});
+41
View File
@@ -0,0 +1,41 @@
/**
* Mermaid loader for MkDocs Material.
*
* Material's `navigation.instant` swaps page content without a full reload,
* so Mermaid must be (re-)initialized after every page swap. We hook into
* both the initial load and Material's custom `document-subscriber` event.
*
* The mermaid.js runtime itself is loaded via mkdocs.yml `extra_javascript`
* with `defer`, so it's available by the time this script runs.
*/
(function () {
function isMermaidReady() {
return typeof window.mermaid !== 'undefined';
}
function renderAll() {
if (!isMermaidReady()) return;
try {
window.mermaid.initialize({
startOnLoad: false,
theme: document.documentElement.getAttribute('data-md-color-scheme') === 'slate'
? 'dark'
: 'default',
securityLevel: 'loose', // allow $ in labels, e.g. $web_search
flowchart: { curve: 'basis', useMaxWidth: true },
});
window.mermaid.run({ querySelector: '.mermaid:not([data-processed])' });
} catch (e) {
console.warn('[mermaid] render failed:', e);
}
}
// Re-render whenever Material swaps to a new page.
document.addEventListener('DOMContentLoaded', renderAll);
document$.subscribe(renderAll); // `document$` is provided by Material
// Re-theme when the user toggles light/dark.
new MutationObserver(renderAll)
.observe(document.documentElement,
{ attributes: true, attributeFilter: ['data-md-color-scheme'] });
})();
+176
View File
@@ -0,0 +1,176 @@
/**
* Desktop navigation controls for Material's primary sidebar.
*
* Background: with `navigation.sections` + `navigation.indexes`, each
* chapter renders as
*
* <li class="md-nav__item--section md-nav__item--nested">
* <input class="md-toggle" id="__nav_N">
* <div class="md-nav__link md-nav__container">
* <a href="…/chapterN/">chapter title</a> ← navigates
* <label for="__nav_N">chevron</label> ← toggles
* </div>
* <nav class="md-nav">…配套实验…</nav>
* </li>
*
* Material only honours the checkbox on mobile; on desktop the section
* subtree is always visible and the chevron is hidden. We inject CSS so
* that on desktop, too, the subtree follows the checkbox and the chevron
* is visible/clickable. Material checks the active chapter's checkbox at
* render time, so the default state (active chapter open, rest closed)
* comes for free.
*
* One case needs JS: on pages Material doesn't consider "active" — the
* translated editions (sidebar links are rewritten client-side by
* lang-switcher.js) and the per-experiment pages, which aren't in the
* nav — no checkbox is checked. There we match the current URL against
* each section's chapter number and open the matching section.
*
* The header also contains a desktop-only button for hiding the entire
* primary sidebar. Its state is persisted locally and restored across full
* reloads as well as Material's instant page swaps.
*
* Re-runs on every Material page swap (navigation.instant) via document$.
*/
(function () {
"use strict";
var SIDEBAR_STORAGE_KEY = "ai-agent-book.primary-sidebar-collapsed";
function ensureStyle() {
if (document.getElementById("nav-collapse-style")) return;
var s = document.createElement("style");
s.id = "nav-collapse-style";
s.textContent = [
"@media screen and (min-width: 76.25em) {",
// Collapse the subtree when the checkbox is unchecked (Material
// keeps section subtrees always-visible on desktop by default).
" .md-sidebar--primary .md-nav__item--nested > .md-toggle:not(:checked) ~ .md-nav {",
" display: none !important;",
" }",
// Material hides the section chevron on desktop; bring it back and
// make it clickable.
" .md-sidebar--primary .md-nav__item--nested > .md-nav__container > label.md-nav__link {",
" display: flex;",
" align-items: center;",
" cursor: pointer;",
" pointer-events: auto !important;",
" margin: 0;",
// 4px top padding centres the 1.2rem icon on the title's FIRST line
// (6px link padding + ~1.3 line-height): the container aligns
// flex-start (book-theme.css) so wrapped two-line titles don't pull
// the chevron down between the lines.
" padding: 4px 0.2rem 0 0.4rem;",
" }",
// Material's own stylesheet already rotates the chevron's ::after
// by 90° when the checkbox is checked — no extra transform here.
" .md-sidebar--primary .md-nav__item--nested > .md-nav__container > label.md-nav__link .md-nav__icon {",
" display: block;",
" }",
"}",
].join("\n");
document.head.appendChild(s);
}
function applyDefaultState() {
var sidebar = document.querySelector(".md-sidebar--primary");
if (!sidebar) return;
// Material already checked a section's checkbox? Then its render-time
// active detection worked — nothing to fix up.
if (sidebar.querySelector(".md-nav__item--nested > .md-toggle:checked")) return;
// Otherwise (translated edition or a page outside the nav), derive the
// chapter from the URL and open the matching section.
var m = location.pathname.match(/chapter(\d+)/);
if (!m) return;
var wanted = m[1];
var sections = sidebar.querySelectorAll(".md-nav__item--nested");
for (var i = 0; i < sections.length; i++) {
var link = sections[i].querySelector(":scope > .md-nav__container > a.md-nav__link");
var checkbox = sections[i].querySelector(":scope > .md-toggle");
if (!link || !checkbox) continue;
var lm = (link.getAttribute("href") || "").match(/chapter(\d+)/);
if (lm && lm[1] === wanted) {
checkbox.checked = true;
break;
}
}
}
function readSidebarPreference() {
try {
var value = window.localStorage.getItem(SIDEBAR_STORAGE_KEY);
return value === null ? null : value === "true";
} catch (_) {
// Storage may be disabled by the browser. The control still works for
// the current page, and the root class survives Material page swaps.
return null;
}
}
function writeSidebarPreference(collapsed) {
try {
window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(collapsed));
} catch (_) {
// A blocked localStorage must not prevent readers from using the
// collapse control for the current page.
}
}
function setSidebarCollapsed(collapsed, remember) {
document.documentElement.classList.toggle("sidebar-nav-collapsed", collapsed);
var button = document.querySelector("[data-sidebar-toggle]");
if (button) {
var catalog = window.SITE_I18N;
var code = window.siteCurrentLanguage || (catalog && catalog.default) || "zh";
var strings = catalog && catalog.languages && catalog.languages[code];
var label = strings && strings.sidebar
? strings.sidebar[collapsed ? "show" : "hide"]
: (collapsed ? "展开侧边栏" : "隐藏侧边栏");
button.setAttribute("aria-expanded", String(!collapsed));
button.setAttribute("aria-label", label);
button.setAttribute("title", label);
}
if (remember) writeSidebarPreference(collapsed);
}
function initSidebarToggle() {
var button = document.querySelector("[data-sidebar-toggle]");
var sidebar = document.querySelector(".md-sidebar--primary");
if (!button || !sidebar || sidebar.hidden) {
if (button) button.hidden = true;
return;
}
// Connect aria-controls to the sidebar generated by Material. The id is
// re-applied because navigation.instant replaces page content in place.
sidebar.id = "primary-navigation";
button.hidden = false;
var preference = readSidebarPreference();
var collapsed = preference === null
? document.documentElement.classList.contains("sidebar-nav-collapsed")
: preference;
setSidebarCollapsed(collapsed, false);
if (button.getAttribute("data-sidebar-toggle-bound") !== "true") {
button.setAttribute("data-sidebar-toggle-bound", "true");
button.addEventListener("click", function () {
var next = !document.documentElement.classList.contains("sidebar-nav-collapsed");
setSidebarCollapsed(next, true);
});
}
}
function init() {
ensureStyle();
applyDefaultState();
initSidebarToggle();
}
document.addEventListener("DOMContentLoaded", init);
if (window.document$) window.document$.subscribe(init);
})();
+89
View File
@@ -0,0 +1,89 @@
// Point Material's search at this edition's slice of the search index.
//
// scripts/split_search_index.py splits the one 55 MB search_index.json the
// search plugin emits into `search/search_index.<slug>.json`, one per book
// edition (slug = the edition's URL directory: `book`, `book-en`, ...), each
// carrying that edition plus the shared experiment pages. This script decides
// which slice the current page needs and rewrites the request for it.
//
// Why an XHR patch rather than configuration: Material builds the index URL
// as `new URL("search/search_index.json", config.base)` and requests it via
// XMLHttpRequest while bundle.js initialises. There is no config seam for the
// path, and `extra_javascript` files load *after* bundle.js — too late. So
// overrides/main.html loads this file inside the `config` block, which base.html
// renders immediately before bundle.js, and we intercept the one request.
//
// If anything here fails to identify an edition we simply leave the URL alone:
// search_index.json still holds the default edition plus the shared pages, so
// search degrades to its previous behaviour rather than breaking.
(function () {
"use strict";
var INDEX_RE = /\/search\/search_index\.json$/;
// `chapterN/README.<readmeSuffix>/` and `index.<code>/` — pages that belong
// to an edition but live outside its book-*/ directory.
var README_RE = /\/chapter\d+\/README\.([A-Za-z-]+)\/?(?:index\.html)?$/;
var HOMEPAGE_RE = /\/index\.([A-Za-z-]+)\/?(?:index\.html)?$/;
// Mirrors edition_of() in scripts/split_search_index.py — keep the two in
// step, or a reader gets an index that omits the edition they are reading.
function slugForPath(path, cfg) {
var code, entry;
// 1. An explicit book-*/ path segment. Exact segment matches only, so the
// `/ai-agent-book/` site subpath can never be mistaken for an edition.
var slugs = {};
for (code in cfg) {
if (cfg.hasOwnProperty(code) && cfg[code].prefix) {
slugs[cfg[code].prefix.replace(/\/$/, "")] = true;
}
}
var segments = path.split("/");
for (var i = 0; i < segments.length; i++) {
if (slugs[segments[i]]) return segments[i];
}
// 2. Translated experiment indexes and homepages, keyed by their suffix.
var match = path.match(README_RE) || path.match(HOMEPAGE_RE);
if (match) {
for (code in cfg) {
if (!cfg.hasOwnProperty(code)) continue;
entry = cfg[code];
if (!entry.prefix) continue;
if (entry.readmeSuffix === match[1] || code === match[1]) {
return entry.prefix.replace(/\/$/, "");
}
}
}
// 3. Language-agnostic pages (the `chapterN/` experiments, the site root).
// Reuse the switcher's remembered locale so a reader who came from an
// edition keeps searching it; lang-switcher.js writes the same key.
var remembered = null;
try {
remembered = sessionStorage.getItem("lang-switcher-active");
} catch (_) {}
if (remembered && cfg[remembered] && cfg[remembered].prefix) {
return cfg[remembered].prefix.replace(/\/$/, "");
}
return null;
}
var cfg = window.LANG_CONFIG;
if (!cfg) return; // header.html emits it before this script; nothing to do.
var slug = slugForPath(location.pathname, cfg);
if (!slug) return;
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
var args = Array.prototype.slice.call(arguments);
if (typeof url === "string" && INDEX_RE.test(url)) {
args[1] = url.replace(INDEX_RE, "/search/search_index." + slug + ".json");
}
return open.apply(this, args);
};
})();
+318
View File
@@ -0,0 +1,318 @@
{
"zh": {
"material_locale": "zh",
"nav": {
"首页": "首页",
"引言": "引言",
"第1章 Agent基础知识": "第1章 Agent基础知识",
"第2章 上下文工程": "第2章 上下文工程",
"第3章 用户记忆和知识库": "第3章 用户记忆和知识库",
"第4章 工具": "第4章 工具",
"第5章 CodingAgent与通用Agent": "第5章 CodingAgent与通用Agent",
"第6章 交互:观察与动作空间的扩展": "第6章 交互:观察与动作空间的扩展",
"第7章 Agent的评估": "第7章 Agent的评估",
"第8章 模型后训练": "第8章 模型后训练",
"第9章 Agent的持续进化": "第9章 Agent的持续进化",
"第10章 多Agent协作": "第10章 多Agent协作",
"后记": "后记",
"思考题参考答案": "思考题参考答案",
"配套实验": "配套实验"
},
"sidebar": { "show": "展开侧边栏", "hide": "隐藏侧边栏" },
"palette": { "light": "切换日间模式", "dark": "切换夜间模式" }
},
"zhtw": {
"material_locale": "zh-TW",
"nav": {
"首页": "首頁",
"引言": "引言",
"第1章 Agent基础知识": "第 1 章 · Agent 基礎知識",
"第2章 上下文工程": "第 2 章 · 上下文工程",
"第3章 用户记忆和知识库": "第 3 章 · 使用者記憶和知識庫",
"第4章 工具": "第 4 章 · 工具",
"第5章 CodingAgent与通用Agent": "第 5 章 · Coding Agent 與程式碼生成",
"第6章 交互:观察与动作空间的扩展": "第 6 章 · 互動:觀察與動作空間的擴展",
"第7章 Agent的评估": "第 7 章 · Agent 的評估",
"第8章 模型后训练": "第 8 章 · 模型後訓練",
"第9章 Agent的持续进化": "第 9 章 · Agent 的持續進化",
"第10章 多Agent协作": "第 10 章 · 多 Agent 協作",
"后记": "後記",
"思考题参考答案": "思考題參考答案",
"配套实验": "配套實驗"
},
"sidebar": { "show": "展開側邊欄", "hide": "隱藏側邊欄" },
"palette": { "light": "切換日間模式", "dark": "切換夜間模式" }
},
"en": {
"material_locale": "en",
"nav": {
"首页": "Home",
"引言": "Introduction",
"第1章 Agent基础知识": "Chapter 1 · Getting Started with AI Agents",
"第2章 上下文工程": "Chapter 2 · Context Engineering",
"第3章 用户记忆和知识库": "Chapter 3 · User Memory & Knowledge Base",
"第4章 工具": "Chapter 4 · Tools",
"第5章 CodingAgent与通用Agent": "Chapter 5 · Coding Agent & Code Generation",
"第6章 交互:观察与动作空间的扩展": "Chapter 6 · Interaction: Expanding the Observation and Action Spaces",
"第7章 Agent的评估": "Chapter 7 · Evaluating Agents",
"第8章 模型后训练": "Chapter 8 · Model Post-Training",
"第9章 Agent的持续进化": "Chapter 9 · Continual Evolution of Agents",
"第10章 多Agent协作": "Chapter 10 · Multi-Agent Collaboration",
"后记": "Afterword",
"思考题参考答案": "Reference Answers",
"配套实验": "Experiments"
},
"sidebar": { "show": "Show navigation", "hide": "Hide navigation" },
"palette": { "light": "Switch to light mode", "dark": "Switch to dark mode" }
},
"es": {
"material_locale": "es",
"nav": {
"首页": "Inicio",
"引言": "Introducción",
"第1章 Agent基础知识": "Capítulo 1 · Fundamentos de los Agentes de IA",
"第2章 上下文工程": "Capítulo 2 · Ingeniería de Contexto",
"第3章 用户记忆和知识库": "Capítulo 3 · Memoria de Usuario y Base de Conocimiento",
"第4章 工具": "Capítulo 4 · Herramientas",
"第5章 CodingAgent与通用Agent": "Capítulo 5 · Agente de Código y Generación de Código",
"第6章 交互:观察与动作空间的扩展": "Capítulo 6 · Interacción: la expansión de los espacios de observación y de acción",
"第7章 Agent的评估": "Capítulo 7 · Evaluación de Agentes",
"第8章 模型后训练": "Capítulo 8 · Posentrenamiento de Modelos",
"第9章 Agent的持续进化": "Capítulo 9 · Evolución Continua de los Agentes",
"第10章 多Agent协作": "Capítulo 10 · Colaboración Multiagente",
"后记": "Epílogo",
"思考题参考答案": "Respuestas de Referencia",
"配套实验": "Experimentos"
},
"sidebar": { "show": "Mostrar navegación", "hide": "Ocultar navegación" },
"palette": { "light": "Cambiar al modo claro", "dark": "Cambiar al modo oscuro" }
},
"id": {
"material_locale": "id",
"nav": {
"首页": "Beranda",
"引言": "Pendahuluan",
"第1章 Agent基础知识": "Bab 1 · Dasar-dasar Agent",
"第2章 上下文工程": "Bab 2 · Rekayasa Konteks",
"第3章 用户记忆和知识库": "Bab 3 · Memori Pengguna dan Basis Pengetahuan",
"第4章 工具": "Bab 4 · Alat",
"第5章 CodingAgent与通用Agent": "Bab 5 · Coding Agent dan Pembuatan Kode",
"第6章 交互:观察与动作空间的扩展": "Bab 6 · Interaksi: Perluasan Ruang Observasi dan Ruang Aksi",
"第7章 Agent的评估": "Bab 7 · Evaluasi Agent",
"第8章 模型后训练": "Bab 8 · Pascapelatihan Model",
"第9章 Agent的持续进化": "Bab 9 · Evolusi Berkelanjutan Agent",
"第10章 多Agent协作": "Bab 10 · Kolaborasi Multi-Agent",
"后记": "Penutup",
"思考题参考答案": "Jawaban Referensi",
"配套实验": "Eksperimen"
},
"sidebar": { "show": "Tampilkan navigasi", "hide": "Sembunyikan navigasi" },
"palette": { "light": "Beralih ke mode terang", "dark": "Beralih ke mode gelap" }
},
"ru": {
"material_locale": "ru",
"nav": {
"首页": "Главная",
"引言": "Введение",
"第1章 Agent基础知识": "Глава 1 · Введение в ИИ-агенты",
"第2章 上下文工程": "Глава 2 · Инженерия контекста",
"第3章 用户记忆和知识库": "Глава 3 · Память и база знаний",
"第4章 工具": "Глава 4 · Инструменты",
"第5章 CodingAgent与通用Agent": "Глава 5 · Кодинг-агент и генерация кода",
"第6章 交互:观察与动作空间的扩展": "Глава 6 · Взаимодействие: расширение пространства наблюдений и пространства действий",
"第7章 Agent的评估": "Глава 7 · Оценка агентов",
"第8章 模型后训练": "Глава 8 · Постобучение модели",
"第9章 Agent的持续进化": "Глава 9 · Непрерывная эволюция агентов",
"第10章 多Agent协作": "Глава 10 · Мультиагентное взаимодействие",
"后记": "Послесловие",
"思考题参考答案": "Ответы к вопросам",
"配套实验": "Эксперименты"
},
"sidebar": { "show": "Показать навигацию", "hide": "Скрыть навигацию" },
"palette": { "light": "Включить светлый режим", "dark": "Включить тёмный режим" }
},
"ta": {
"material_locale": "ta",
"nav": {
"首页": "முகப்பு",
"引言": "அறிமுகம்",
"第1章 Agent基础知识": "அத்தியாயம் 1 · AI ஏஜெண்ட் அடிப்படைகள்",
"第2章 上下文工程": "அத்தியாயம் 2 · சூழல் பொறியியல்",
"第3章 用户记忆和知识库": "அத்தியாயம் 3 · பயனர் நினைவகம் மற்றும் அறிவுத்தளம்",
"第4章 工具": "அத்தியாயம் 4 · கருவிகள்",
"第5章 CodingAgent与通用Agent": "அத்தியாயம் 5 · குறியீட்டு ஏஜெண்ட் மற்றும் குறியீடு உருவாக்கம்",
"第6章 交互:观察与动作空间的扩展": "அத்தியாயம் 6 · தொடர்பாடல்: அவதானிப்பு மற்றும் செயல் வெளிகளின் விரிவாக்கம்",
"第7章 Agent的评估": "அத்தியாயம் 7 · ஏஜெண்ட் மதிப்பீடு",
"第8章 模型后训练": "அத்தியாயம் 8 · மாதிரி பிந்தைய பயிற்சி",
"第9章 Agent的持续进化": "அத்தியாயம் 9 · ஏஜெண்டின் தொடர்ச்சியான பரிணாமம்",
"第10章 多Agent协作": "அத்தியாயம் 10 · பல-ஏஜெண்ட் கூட்டுச் செயல்பாடு",
"后记": "பின்னுரை",
"思考题参考答案": "குறிப்புப் பதில்கள்",
"配套实验": "சோதனைகள்"
},
"sidebar": { "show": "வழிசெலுத்தலைக் காட்டு", "hide": "வழிசெலுத்தலை மறை" },
"palette": { "light": "ஒளி பயன்முறைக்கு மாறு", "dark": "இருண்ட பயன்முறைக்கு மாறு" }
},
"vi": {
"material_locale": "vi",
"ui_overrides": {
"search.result.initializer": "Đang khởi tạo tìm kiếm",
"source.file.contributors": "Người đóng góp",
"tabs": "Thẻ"
},
"nav": {
"首页": "Trang chủ",
"引言": "Giới thiệu",
"第1章 Agent基础知识": "Chương 1 · Nền tảng AI Agent",
"第2章 上下文工程": "Chương 2 · Kỹ thuật ngữ cảnh",
"第3章 用户记忆和知识库": "Chương 3 · Bộ nhớ và Cơ sở kiến thức",
"第4章 工具": "Chương 4 · Công cụ",
"第5章 CodingAgent与通用Agent": "Chương 5 · Coding Agent và Tạo mã",
"第6章 交互:观察与动作空间的扩展": "Chương 6 · Tương tác: mở rộng không gian quan sát và không gian hành động",
"第7章 Agent的评估": "Chương 7 · Đánh giá Agent",
"第8章 模型后训练": "Chương 8 · Post-training mô hình",
"第9章 Agent的持续进化": "Chương 9 · Sự tiến hóa liên tục của Agent",
"第10章 多Agent协作": "Chương 10 · Cộng tác đa Agent",
"后记": "Lời bạt",
"思考题参考答案": "Đáp án tham khảo",
"配套实验": "Thí nghiệm"
},
"sidebar": { "show": "Hiện thanh điều hướng", "hide": "Ẩn thanh điều hướng" },
"palette": { "light": "Chuyển sang chế độ sáng", "dark": "Chuyển sang chế độ tối" }
},
"ja": {
"material_locale": "ja",
"nav": {
"首页": "ホーム",
"引言": "はじめに",
"第1章 Agent基础知识": "第1章 · AI Agent 入門",
"第2章 上下文工程": "第2章 · コンテキストエンジニアリング",
"第3章 用户记忆和知识库": "第3章 · ユーザーメモリと知識ベース",
"第4章 工具": "第4章 · ツール",
"第5章 CodingAgent与通用Agent": "第5章 · Coding Agent とコード生成",
"第6章 交互:观察与动作空间的扩展": "第6章 · 交互:観察空間と動作空間の拡張",
"第7章 Agent的评估": "第7章 · Agent の評価",
"第8章 模型后训练": "第8章 · モデルのポストトレーニング",
"第9章 Agent的持续进化": "第9章 · Agent の継続的進化",
"第10章 多Agent协作": "第10章 · マルチ Agent 協調",
"后记": "あとがき",
"思考题参考答案": "演習問題の解答例",
"配套实验": "実験"
},
"sidebar": { "show": "ナビゲーションを表示", "hide": "ナビゲーションを隠す" },
"palette": { "light": "ライトモードに切り替え", "dark": "ダークモードに切り替え" }
},
"ko": {
"material_locale": "ko",
"nav": {
"首页": "홈",
"引言": "들어가며",
"第1章 Agent基础知识": "제1장 · AI 에이전트 기초",
"第2章 上下文工程": "제2장 · 컨텍스트 엔지니어링",
"第3章 用户记忆和知识库": "제3장 · 사용자 메모리와 지식 베이스",
"第4章 工具": "제4장 · 도구",
"第5章 CodingAgent与通用Agent": "제5장 · 코딩 에이전트와 코드 생성",
"第6章 交互:观察与动作空间的扩展": "제6장 · 상호작용: 관찰 공간과 행동 공간의 확장",
"第7章 Agent的评估": "제7장 · 에이전트 평가",
"第8章 模型后训练": "제8장 · 모델 사후 학습",
"第9章 Agent的持续进化": "제9장 · 에이전트의 지속적 진화",
"第10章 多Agent协作": "제10장 · 멀티 에이전트 협업",
"后记": "맺음말",
"思考题参考答案": "생각해 볼 문제 참고 답안",
"配套实验": "실습"
},
"sidebar": { "show": "탐색 메뉴 표시", "hide": "탐색 메뉴 숨기기" },
"palette": { "light": "라이트 모드로 전환", "dark": "다크 모드로 전환" }
},
"ar": {
"material_locale": "ar",
"nav": {
"首页": "الرئيسية",
"引言": "المقدمة",
"第1章 Agent基础知识": "الفصل 1 · أساسيات الوكلاء",
"第2章 上下文工程": "الفصل 2 · هندسة السياق",
"第3章 用户记忆和知识库": "الفصل 3 · ذاكرة المستخدم وقاعدة المعرفة",
"第4章 工具": "الفصل 4 · الأدوات",
"第5章 CodingAgent与通用Agent": "الفصل 5 · وكيل البرمجة وتوليد الشيفرة",
"第6章 交互:观察与动作空间的扩展": "الفصل 6 · التفاعل: توسيع فضاء الملاحظة وفضاء الفعل",
"第7章 Agent的评估": "الفصل 7 · تقييم الوكلاء",
"第8章 模型后训练": "الفصل 8 · ما بعد تدريب النموذج",
"第9章 Agent的持续进化": "الفصل 9 · التطور المستمر للوكلاء",
"第10章 多Agent协作": "الفصل 10 · التعاون متعدد الوكلاء",
"后记": "الخاتمة",
"思考题参考答案": "إجابات الأسئلة",
"配套实验": "التجارب"
},
"sidebar": { "show": "إظهار التنقل", "hide": "إخفاء التنقل" },
"palette": { "light": "التبديل إلى الوضع الفاتح", "dark": "التبديل إلى الوضع الداكن" }
},
"tr": {
"material_locale": "tr",
"nav": {
"首页": "Ana Sayfa",
"引言": "Giriş",
"第1章 Agent基础知识": "Bölüm 1 · Yapay Zeka Ajanlarına Giriş",
"第2章 上下文工程": "Bölüm 2 · Context Engineering",
"第3章 用户记忆和知识库": "Bölüm 3 · Kullanıcı Belleği ve Bilgi Tabanı",
"第4章 工具": "Bölüm 4 · Araçlar",
"第5章 CodingAgent与通用Agent": "Bölüm 5 · Kodlama Agent'ı ve Kod Üretimi",
"第6章 交互:观察与动作空间的扩展": "Bölüm 6 · Etkileşim: Gözlem ve Eylem Uzaylarının Genişletilmesi",
"第7章 Agent的评估": "Bölüm 7 · Agent'ın Değerlendirmesi",
"第8章 模型后训练": "Bölüm 8 · Model Post-Training",
"第9章 Agent的持续进化": "Bölüm 9 · Agent'ın Sürekli Evrimi",
"第10章 多Agent协作": "Bölüm 10 · Çoklu Agent İş Birliği",
"后记": "Sonsöz",
"思考题参考答案": "Düşünce Soruları — Örnek Cevaplar",
"配套实验": "Deneyler"
},
"sidebar": { "show": "Navigasyonu göster", "hide": "Navigasyonu gizle" },
"palette": { "light": "Açık moda geç", "dark": "Koyu moda geç" }
},
"hu": {
"material_locale": "hu",
"ui_overrides": {
"source": "Ugrás a kódtárhoz"
},
"nav": {
"首页": "Kezdőlap",
"引言": "Bevezetés",
"第1章 Agent基础知识": "1. fejezet · Ismerkedés az AI-ügynökökkel",
"第2章 上下文工程": "2. fejezet · Kontextustervezés",
"第3章 用户记忆和知识库": "3. fejezet · Felhasználói memória és tudásbázis",
"第4章 工具": "4. fejezet · Eszközök",
"第5章 CodingAgent与通用Agent": "5. fejezet · Kódoló ágens és kódgenerálás",
"第6章 交互:观察与动作空间的扩展": "6. fejezet · Interakció: a megfigyelési és a cselekvési tér kiterjesztése",
"第7章 Agent的评估": "7. fejezet · Ügynökök kiértékelése",
"第8章 模型后训练": "8. fejezet · Modell poszt-tréning",
"第9章 Agent的持续进化": "9. fejezet · Az ágensek folyamatos evolúciója",
"第10章 多Agent协作": "10. fejezet · Többügynökös együttműködés",
"后记": "Utószó",
"思考题参考答案": "Válaszvázlatok a gondolatébresztő kérdésekhez",
"配套实验": "Kísérletek"
},
"sidebar": { "show": "Navigáció megjelenítése", "hide": "Navigáció elrejtése" },
"palette": { "light": "Váltás világos módra", "dark": "Váltás sötét módra" }
},
"he": {
"material_locale": "he",
"nav": {
"首页": "דף הבית",
"引言": "הקדמה",
"第1章 Agent基础知识": "פרק 1 · צעדים ראשונים עם סוכני AI",
"第2章 上下文工程": "פרק 2 · הנדסת הקשר",
"第3章 用户记忆和知识库": "פרק 3 · זיכרון משתמש ובסיס ידע",
"第4章 工具": "פרק 4 · כלים",
"第5章 CodingAgent与通用Agent": "פרק 5 · סוכן קוד ויצירת קוד",
"第6章 交互:观察与动作空间的扩展": "פרק 6 · אינטראקציה: הרחבת מרחבי התצפית והפעולה",
"第7章 Agent的评估": "פרק 7 · הערכת סוכנים",
"第8章 模型后训练": "פרק 8 · אימון־על של מודלים",
"第9章 Agent的持续进化": "פרק 9 · התפתחות מתמשכת של סוכנים",
"第10章 多Agent协作": "פרק 10 · שיתוף פעולה רב־סוכני",
"后记": "אחרית דבר",
"思考题参考答案": "תשובות לשאלות למחשבה",
"配套实验": "ניסויים נלווים"
},
"sidebar": { "show": "הצגת ניווט", "hide": "הסתרת ניווט" },
"palette": { "light": "מעבר למצב בהיר", "dark": "מעבר למצב כהה" }
}
}