Files
auto_agent/README.html

1913 lines
121 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="minor Agent — 一个可本地运行的多模态 Agent 桌面应用:基于 LangGraph 构建,具备 GUI 自动化、浏览器控制、终端执行、RAG 知识库、邮件收发、图像生成、语音对话、数据库操作、定时任务等 20 项工具,并提供 Electron 桌面端一键安装体验。">
<meta name="author" content="minorQAQ">
<meta name="theme-color" content="#faf6ef">
<meta property="og:title" content="minor Agent · 可本地运行的多模态 Agent 桌面应用">
<meta property="og:description" content="真正能「动手」操作你电脑的 AI 助手 —— LangGraph + Electron 本地多模态 Agent">
<title>minor Agent · 可本地运行的多模态 Agent 桌面应用</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Ma+Shan+Zheng&family=Nunito:wght@400;600;700;800;900&family=Quicksand:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
/* ============================================================
minor Agent README · 单文件互动故事书版
主题:温暖 · 柔和 · 治愈系动画 / 绘本风格
============================================================ */
:root{
--bg:#faf6ef;
--bg-soft:#f7f2e9;
--card:rgba(255,255,255,.58);
--green-1:#c8d6b9;
--green-2:#b5c99a;
--green-3:#a3b18a;
--accent:#e9c46a;
--coral:#f2a68d;
--apricot:#f8dda4;
--ink:#4a4238;
--ink-soft:#6b6258;
--ink-faint:#8a7f72;
--line:rgba(163,177,138,.35);
--shadow:0 4px 20px rgba(163,177,138,.15);
--shadow-lg:0 14px 44px rgba(163,177,138,.28);
--ease:cubic-bezier(.16,1,.3,1);
--spring:cubic-bezier(.34,1.56,.64,1);
--radius:16px;
--mono:'JetBrains Mono',ui-monospace,Consolas,'Courier New',monospace;
--hand:'ZCOOL KuaiLe','Ma Shan Zheng','Microsoft YaHei',sans-serif;
--sans:'Nunito','Quicksand','PingFang SC','Microsoft YaHei','Segoe UI',sans-serif;
--nav-h:64px;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
body{
background:var(--bg);
color:var(--ink);
font-family:var(--sans);
font-size:16.5px;
line-height:1.78;
overflow-x:hidden;
-webkit-font-smoothing:antialiased;
}
::selection{background:rgba(233,196,106,.45);color:var(--ink)}
/* 极淡纸张纹理 */
body::before{
content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
background-image:radial-gradient(rgba(74,66,56,.045) 1px,transparent 1.4px);
background-size:22px 22px;
}
img{max-width:100%;display:block}
h1,h2,h3,h4{font-family:var(--hand);line-height:1.35;font-weight:400;letter-spacing:.5px}
a{color:var(--green-3);text-decoration:none;transition:color .3s var(--ease)}
a:hover{color:var(--accent)}
code{
background:rgba(163,177,138,.2);
color:#4a6b3f;
padding:.1em .45em;
border-radius:6px;
font-family:var(--mono);
font-size:.86em;
word-break:break-word;
}
strong{color:#3f3a32}
/* ---------- 阅读进度条 ---------- */
.progress{
position:fixed;top:0;left:0;right:0;height:3px;z-index:1000;
background:linear-gradient(90deg,var(--green-1),var(--green-3),var(--accent));
transform-origin:0 50%;transform:scaleX(0);
border-radius:0 3px 3px 0;
box-shadow:0 0 10px rgba(163,177,138,.55);
}
/* ---------- 顶部导航(毛玻璃) ---------- */
.nav{
position:fixed;top:0;left:0;right:0;z-index:900;
height:var(--nav-h);
background:rgba(250,246,239,.72);
backdrop-filter:blur(16px) saturate(1.5);
-webkit-backdrop-filter:blur(16px) saturate(1.5);
border-bottom:1px solid rgba(163,177,138,.22);
transition:transform .5s var(--ease),box-shadow .5s var(--ease);
}
.nav.nav-hidden{transform:translateY(-100%)}
.nav-inner{
max-width:1160px;margin:0 auto;height:100%;
display:flex;align-items:center;justify-content:space-between;
padding:0 22px;
}
.nav-brand{
font-family:var(--hand);font-size:1.35rem;color:var(--ink);
display:flex;align-items:center;gap:8px;
}
.nav-brand:hover{color:var(--green-3)}
.nav-links{display:flex;gap:6px;align-items:center}
.nav-links a{
color:var(--ink-soft);font-size:.92rem;font-weight:700;
padding:6px 13px;border-radius:99px;
transition:all .3s var(--ease);
}
.nav-links a:hover{background:rgba(233,196,106,.25);color:var(--ink);transform:translateY(-1px)}
/* ---------- 背景漂浮小元素 ---------- */
.decor{position:fixed;inset:0;z-index:0;pointer-events:none;overflow:hidden}
.decor i{
position:absolute;display:block;opacity:.28;
animation:drift1 var(--t,18s) ease-in-out infinite alternate;
}
.decor i:nth-child(1){top:12%;left:6%;--t:19s;width:26px;height:26px}
.decor i:nth-child(2){top:30%;right:8%;--t:23s;width:18px;height:18px}
.decor i:nth-child(3){top:64%;left:10%;--t:21s;width:14px;height:14px}
.decor i:nth-child(4){top:78%;right:12%;--t:26s;width:24px;height:24px}
.decor i:nth-child(5){top:46%;left:3%;--t:28s;width:12px;height:12px}
.decor i:nth-child(6){top:8%;right:22%;--t:24s;width:20px;height:20px}
.decor i:nth-child(7){top:88%;left:46%;--t:22s;width:16px;height:16px}
.decor i:nth-child(8){top:20%;left:40%;--t:30s;width:10px;height:10px}
.decor svg{width:100%;height:100%;overflow:visible}
@keyframes drift1{
0%{transform:translate(0,0) rotate(0deg)}
50%{transform:translate(26px,-30px) rotate(18deg)}
100%{transform:translate(-14px,18px) rotate(-12deg)}
}
/* ---------- Hero ---------- */
.hero{
position:relative;z-index:1;
min-height:100svh;
display:flex;flex-direction:column;align-items:center;justify-content:center;
text-align:center;padding:120px 22px 90px;
overflow:hidden;
}
.hero-blob{position:absolute;z-index:0;pointer-events:none}
.hero-blob.b1{top:-12%;left:-8%;width:46vw;height:46vw}
.hero-blob.b2{bottom:-16%;right:-10%;width:40vw;height:40vw}
.hb-inner{position:absolute;inset:0;border-radius:50%;filter:blur(70px);opacity:.55}
.b1 .hb-inner{background:radial-gradient(circle at 35% 40%,var(--green-1),rgba(200,214,185,0) 70%);animation:drift2 17s ease-in-out infinite alternate}
.b2 .hb-inner{background:radial-gradient(circle at 60% 50%,var(--apricot),rgba(248,221,164,0) 70%);animation:drift2 21s ease-in-out infinite alternate-reverse}
@keyframes drift2{
0%{transform:translate(0,0) scale(1)}
100%{transform:translate(40px,-34px) scale(1.08)}
}
.hero-inner{position:relative;z-index:2;width:100%;max-width:1080px;will-change:transform,opacity}
.hero-eyebrow{
display:inline-flex;align-items:center;gap:8px;
font-size:.85rem;font-weight:800;letter-spacing:2px;color:var(--green-3);
background:rgba(163,177,138,.14);border:1px dashed var(--line);
padding:6px 16px;border-radius:99px;margin-bottom:22px;
}
.hero-title{
font-family:var(--hand);
font-size:clamp(2.6rem,7.5vw,4.6rem);
color:var(--ink);line-height:1.15;
margin-bottom:14px;
}
.hero-title .hc{
display:inline-block;opacity:0;transform:translateY(44px);
transition:opacity .7s var(--ease) var(--d,0ms),transform .7s var(--ease) var(--d,0ms);
}
.hero-title.visible .hc{opacity:1;transform:none}
.hero-title .hc.space{width:.28em}
.hero-title .accent-letter{color:var(--green-3)}
.hero-sub{
opacity:0;transform:translateY(40px);
transition:opacity .9s var(--ease) .18s,transform .9s var(--ease) .18s;
font-size:clamp(1.02rem,2.4vw,1.3rem);font-weight:800;color:var(--ink-soft);
max-width:760px;margin:0 auto 16px;
}
.hero-desc{
opacity:0;transform:translateY(30px);
transition:opacity .9s var(--ease) .32s,transform .9s var(--ease) .32s;
max-width:820px;margin:0 auto 30px;color:var(--ink-soft);font-size:1.02rem;
}
.hero-sub.visible,.hero-desc.visible{opacity:1;transform:none}
.hero-badges{display:flex;flex-wrap:wrap;gap:12px;justify-content:center;margin-bottom:42px}
.badge{
opacity:0;transform:scale(.8);
transition:opacity .55s var(--spring) var(--d,0ms),transform .55s var(--spring) var(--d,0ms);
display:inline-flex;align-items:center;gap:6px;
background:rgba(255,255,255,.7);border:1.5px solid var(--line);
color:var(--ink-soft);font-weight:800;font-size:.95rem;
padding:9px 20px;border-radius:99px;
box-shadow:var(--shadow);
}
.badge:hover{color:var(--ink);border-color:var(--accent);transform:translateY(-3px) scale(1.05)!important}
.hero-badges.visible .badge{opacity:1;transform:scale(1)}
.hero-img-card{
opacity:0;transform:scale(.92) translateY(24px);
transition:opacity 1s var(--ease) .5s,transform 1s var(--ease) .5s;
width:min(1000px,96%);margin:0 auto 26px;
}
.hero-img-card.visible{opacity:1;transform:none}
.hero-banner{
opacity:0;transform:translateY(40px) rotate(-.5deg);
transition:opacity 1s var(--ease) .68s,transform 1s var(--ease) .68s;
width:min(920px,96%);margin:0 auto;
}
.hero-banner.visible{opacity:1;transform:none}
.scroll-hint{
position:absolute;bottom:22px;left:50%;transform:translateX(-50%);
z-index:3;color:var(--green-3);
animation:hintFloat 2.2s ease-in-out infinite;
display:flex;flex-direction:column;align-items:center;gap:2px;
}
.scroll-hint span{font-size:.72rem;letter-spacing:2px;color:var(--ink-faint)}
@keyframes hintFloat{0%,100%{transform:translateX(-50%) translateY(0)}50%{transform:translateX(-50%) translateY(10px)}}
/* ---------- 通用布局 ---------- */
main{position:relative;z-index:1}
.section{
max-width:1160px;margin:0 auto;padding:72px 22px;
scroll-margin-top:calc(var(--nav-h) + 18px);
}
.section-head{text-align:center;margin-bottom:44px}
.section-head .sec-emoji{
font-size:2.6rem;display:inline-block;
animation:headFloat 5s ease-in-out infinite;
}
@keyframes headFloat{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(-8px) rotate(3deg)}}
.section-head h2{
font-size:clamp(1.7rem,4vw,2.5rem);color:var(--ink);
margin:6px 0 10px;
}
.section-head .sec-sub{color:var(--ink-soft);max-width:720px;margin:0 auto;font-size:1rem}
.section-divider{
border:0;height:2px;max-width:70%;margin:8px auto;
background:linear-gradient(90deg,transparent,var(--green-2),transparent);
border-radius:2px;
}
.lead{
background:var(--card);
border:1px solid var(--line);
border-radius:20px;
padding:30px 34px;
box-shadow:var(--shadow);
}
.lead p{margin-bottom:14px}
.lead p:last-child{margin-bottom:0}
.chips{display:flex;flex-wrap:wrap;gap:10px;margin-top:8px}
.chip{
background:rgba(255,255,255,.72);border:1px solid var(--line);
border-radius:99px;padding:8px 16px;font-size:.9rem;
display:inline-flex;gap:8px;align-items:flex-start;
color:var(--ink-soft);box-shadow:var(--shadow);
}
.chip b{color:var(--ink)}
/* ---------- 引用块:左侧渐变流动边框 ---------- */
.quote{
position:relative;
background:rgba(255,255,255,.55);
border:1px solid var(--line);
border-radius:12px;
padding:14px 20px 14px 24px;
margin:18px 0;
overflow:hidden;
color:var(--ink-soft);
}
.quote::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:4px;
background:linear-gradient(180deg,var(--accent),var(--green-3),var(--coral),var(--accent));
background-size:100% 300%;
opacity:0;transition:opacity .8s var(--ease);
}
.quote.visible::before{opacity:1;animation:borderFlow 6s linear infinite}
@keyframes borderFlow{to{background-position-y:300%}}
.quote .q-emoji{font-style:normal;margin-right:2px}
/* ---------- 列表项进入 ---------- */
ul,ol{padding-left:1.4em}
li{margin:6px 0}
.li-rv{
opacity:0;transition:opacity .65s var(--ease) var(--d,0ms),transform .65s var(--ease) var(--d,0ms);
}
.li-rv-left{transform:translateX(-20px)}
.li-rv-right{transform:translateX(20px)}
.li-rv-up{transform:translateY(18px)}
.li-rv-pop{transform:scale(.92)}
.li-rv.visible{opacity:1;transform:none}
/* ---------- 通用 reveal 体系 ---------- */
.reveal,.reveal-left,.reveal-right,.reveal-scale,.reveal-rotate,.reveal-up{
opacity:0;
transition:all .9s var(--ease);
transition-property:opacity,transform,filter;
}
.reveal{transform:translateY(60px)}
.reveal-left{transform:translateX(-46px)}
.reveal-right{transform:translateX(46px)}
.reveal-scale{transform:scale(.94) translateY(20px)}
.reveal-rotate{transform:perspective(900px) rotateX(10deg) translateY(60px)}
.reveal-up{transform:translateY(34px)}
.reveal.visible,.reveal-left.visible,.reveal-right.visible,
.reveal-scale.visible,.reveal-rotate.visible,.reveal-up.visible{opacity:1;transform:none}
/* 弹性入场(Contributors 等) */
.reveal-pop{
opacity:0;transform:scale(.8);
transition:opacity .8s var(--spring),transform .8s var(--spring);
}
.reveal-pop.visible{opacity:1;transform:none}
[data-d]{transition-delay:var(--d,0ms)}
/* ---------- 图片卡片 ---------- */
.img-card{
position:relative;
border:1px solid var(--line);
border-radius:18px;
overflow:hidden;
background:linear-gradient(160deg,#fffefb,#f3ecd8);
box-shadow:var(--shadow);
transition:box-shadow .5s var(--ease);
}
.img-card:hover{box-shadow:var(--shadow-lg)}
.img-card img{
width:100%;height:auto;
transition:transform .55s var(--ease),filter .55s var(--ease);
}
.img-card:hover img{transform:scale(1.03)}
.img-card::after{
content:'🖼️ ' attr(data-path);
display:none;
padding:26px 20px;text-align:center;color:var(--ink-faint);
font-family:var(--mono);font-size:.82rem;min-height:120px;
align-items:center;justify-content:center;
}
.img-card.img-missing::after{display:flex}
.img-path{
position:absolute;bottom:10px;right:12px;z-index:2;
font-family:var(--mono);font-size:.68rem;color:#fff;
background:rgba(63,58,50,.55);backdrop-filter:blur(4px);
padding:2px 10px;border-radius:99px;
opacity:0;transition:opacity .35s var(--ease);
pointer-events:none;
}
.img-card:hover .img-path{opacity:1}
.shot-cap{
text-align:center;color:var(--ink-soft);
font-size:.92rem;margin-top:14px;margin-bottom:10px;
}
/* ---------- 表格 ---------- */
.table-scroll{
overflow-x:auto;
border:1px solid var(--line);
border-radius:var(--radius);
box-shadow:inset 0 1px 0 rgba(255,255,255,.65),var(--shadow);
background:rgba(255,255,255,.55);
margin:18px 0;
}
table{width:100%;border-collapse:collapse;font-size:.93rem;min-width:640px}
thead th{
background:linear-gradient(135deg,#cdd9bf,var(--green-2));
color:#3a352c;font-weight:800;
padding:12px 15px;text-align:left;white-space:nowrap;
border-bottom:2px solid rgba(163,177,138,.5);
font-family:var(--sans);
}
tbody td{padding:10px 15px;border-bottom:1px dashed rgba(163,177,138,.28);vertical-align:top}
tbody tr:last-child td{border-bottom:none}
tbody tr:hover td{background:rgba(233,196,106,.07)}
/* 行进入动画 */
.trow{opacity:0;transition:opacity .7s var(--ease),transform .7s var(--ease);transition-delay:var(--d,0ms)}
.trow-rv-right{transform:translateX(24px)}
.trow-rv-up{transform:translateY(22px)}
.trow-rv-alt:nth-child(odd){transform:translateX(-22px)}
.trow-rv-alt:nth-child(even){transform:translateX(22px)}
.trow.visible{opacity:1;transform:none}
/* 图标弹跳 */
.trow.visible .tool-emoji{
display:inline-block;
animation:emojiPop .6s var(--spring) var(--d,0ms) both;
}
@keyframes emojiPop{
0%{transform:rotate(-16deg) scale(.4)}
60%{transform:rotate(9deg) scale(1.22)}
100%{transform:rotate(0) scale(1)}
}
.trow-rv-up.visible td:first-child{animation:iconPulse .65s var(--ease) var(--d,0ms) both}
@keyframes iconPulse{0%{transform:scale(1)}45%{transform:scale(1.38)}100%{transform:scale(1)}}
/* 对比表:左右交错滑入 */
.trow-alt-col td{
opacity:0;
transition:opacity .7s var(--ease),transform .7s var(--ease);
transition-delay:var(--d,0ms);
}
.trow-alt-col td:nth-child(2){transform:translateX(-26px)}
.trow-alt-col td:nth-child(3){transform:translateX(26px)}
.trow-alt-col.visible td{opacity:1;transform:none}
/* ---------- 代码 / 终端 ---------- */
.pre-card,.arch-card pre,.line-cnt pre{
background:linear-gradient(165deg,#fffdf8,#f3ecd8);
border:1px solid var(--line);
border-radius:14px;
padding:18px 20px;
overflow-x:auto;
box-shadow:var(--shadow);
color:var(--ink-soft);
font-family:var(--mono);
font-size:13.4px;
line-height:1.66;
white-space:pre;
}
.term{
margin:18px 0;
border-radius:14px;
overflow:hidden;
border:1px solid var(--line);
box-shadow:var(--shadow);
background:#3a342b;
position:relative;
}
/* 左侧竖向奶油绿渐变细线 */
.term::before{
content:'';position:absolute;left:0;top:0;bottom:0;width:3px;z-index:2;
background:linear-gradient(180deg,var(--green-1),var(--green-3),var(--accent));
}
.term-bar{
display:flex;align-items:center;gap:7px;
background:rgba(255,255,255,.06);
padding:9px 14px;border-bottom:1px solid rgba(255,255,255,.08);
transform:scaleY(0);transform-origin:top;
}
.term.visible .term-bar{transform:scaleY(1);transition:transform .5s var(--ease) .05s}
.dot{width:11px;height:11px;border-radius:50%}
.dot.r{background:#ff5f57}.dot.y{background:#febc2e}.dot.g{background:#28c840}
.term-title{margin-left:8px;font-size:.72rem;color:rgba(255,255,255,.5);font-family:var(--mono);letter-spacing:1px}
.term-body{
background:transparent;
color:#e8e0d0;
font-family:var(--mono);
font-size:13px;
line-height:1.7;
padding:16px 18px;
overflow-x:auto;
white-space:pre;
opacity:0;transform:translateY(8px);
}
.term.visible .term-body{
opacity:1;transform:none;
transition:opacity .8s var(--ease) .25s,transform .8s var(--ease) .25s;
}
.term-body .cmt{color:#9aa284}
.term-body .pwd{color:var(--green-1)}
.term-body .cmd{color:#f5d78e}
/* ---------- 架构图卡片:光带扫过 + 中央点亮 ---------- */
.arch-card{position:relative;overflow:hidden;border-radius:16px;margin:18px 0}
.arch-card::after{
content:'';position:absolute;top:0;bottom:0;left:-30%;width:26%;z-index:2;pointer-events:none;
background:linear-gradient(100deg,transparent,rgba(233,196,106,.32),transparent);
transform:translateX(-120%);
}
.arch-card.visible::after{animation:sweep 1.1s var(--ease) .25s forwards}
@keyframes sweep{to{transform:translateX(560%)}}
.arch-card pre{opacity:0;transform:translateY(10px)}
.arch-card.visible pre{opacity:1;transform:none;transition:opacity .9s var(--ease) .4s,transform .9s var(--ease) .4s}
.arch-beam{
position:absolute;top:0;bottom:0;left:46%;width:9%;z-index:1;pointer-events:none;
background:linear-gradient(180deg,transparent,rgba(233,196,106,.28) 25%,rgba(233,196,106,.5) 50%,rgba(233,196,106,.28) 75%,transparent);
filter:blur(3px);opacity:0;
}
.arch-card.visible .arch-beam{animation:beamDown 1.5s var(--ease) .5s forwards}
@keyframes beamDown{
0%{transform:scaleY(0);opacity:0;transform-origin:top}
20%{opacity:.9}
55%{transform:scaleY(1);opacity:.9}
100%{transform:scaleY(1);opacity:0}
}
/* ---------- 逐行显现(打字机 / 树) ---------- */
.tline{
opacity:0;transform:translateY(4px);
transition:opacity .45s ease,transform .45s ease;
white-space:pre;
}
.tline.vis{opacity:1;transform:none}
.tl{
opacity:0;transform:translateY(4px);
transition:opacity .55s var(--ease),transform .55s var(--ease);
white-space:pre;
}
.tl.vis{opacity:1;transform:none}
.tl .tdir{color:var(--green-3);font-weight:700}
.tl .tfile{color:var(--ink);font-weight:600}
.tl .tcmt{color:var(--ink-faint);font-style:italic}
/* ---------- 实现细节:数字弹出 + 涟漪 ---------- */
.detail-head{display:flex;align-items:center;gap:16px;margin:44px 0 16px}
.detail-num{
flex:none;position:relative;
width:52px;height:52px;border-radius:16px;
display:flex;align-items:center;justify-content:center;
font-family:var(--hand);font-size:1.6rem;color:#fff;
background:linear-gradient(135deg,var(--green-2),var(--green-3));
box-shadow:var(--shadow);
}
.detail-num::after{
content:'';position:absolute;inset:0;border-radius:16px;
border:2.5px solid rgba(233,196,106,.85);
transform:scale(.35);opacity:0;
}
.detail-head.visible .detail-num{animation:numPop .65s var(--spring) both}
.detail-head.visible .detail-num::after{animation:ripple .9s var(--ease) .12s forwards}
@keyframes numPop{0%{transform:scale(.45)}70%{transform:scale(1.16)}100%{transform:scale(1)}}
@keyframes ripple{to{transform:scale(1.9);opacity:0}}
.detail-head h3{font-size:clamp(1.25rem,2.6vw,1.6rem);color:var(--ink);margin:0}
.detail-head .detail-tag{color:var(--ink-faint);font-size:.85rem}
/* ---------- details 折叠 ---------- */
details.pack{
margin:18px 0;border:1px dashed var(--line);border-radius:14px;
background:rgba(255,255,255,.5);padding:0;
box-shadow:var(--shadow);
}
details.pack summary{
cursor:pointer;padding:14px 18px;font-weight:800;color:var(--ink);
list-style:none;display:flex;align-items:center;gap:8px;
transition:color .3s;
}
details.pack summary::-webkit-details-marker{display:none}
details.pack summary::after{content:'+';margin-left:auto;color:var(--green-3);font-weight:900;transition:transform .3s var(--ease)}
details.pack[open] summary::after{transform:rotate(45deg)}
details.pack summary:hover{color:var(--green-3)}
details.pack .pack-body{padding:4px 18px 16px;color:var(--ink-soft);font-size:.95rem}
details.pack .pack-body li{margin:7px 0}
/* ---------- Contributors ---------- */
.contrib-card{
display:flex;gap:18px;align-items:flex-start;
background:var(--card);
border:1px solid var(--line);
border-radius:20px;
padding:26px 30px;
box-shadow:var(--shadow);
transition:box-shadow .5s var(--ease),transform .5s var(--ease);
}
.contrib-card:hover{box-shadow:var(--shadow-lg);transform:translateY(-4px)}
.contrib-avatar{
flex:none;width:64px;height:64px;border-radius:50%;
background:linear-gradient(135deg,var(--green-1),var(--accent));
display:flex;align-items:center;justify-content:center;
font-family:var(--hand);font-size:1.7rem;color:#fff;
box-shadow:var(--shadow);
}
.contrib-card h3{margin-bottom:6px}
.contrib-card .contrib-role{color:var(--ink-faint);font-size:.85rem;margin-bottom:8px}
/* ---------- Footer ---------- */
footer{
position:relative;z-index:1;
background:linear-gradient(180deg,transparent,rgba(200,214,185,.22));
padding:70px 22px 40px;
text-align:center;color:var(--ink-soft);
border-top:1px dashed var(--line);
}
.footer-line{
height:3px;width:0;margin:0 auto 34px;border-radius:3px;
background:linear-gradient(90deg,var(--green-1),var(--green-3),var(--accent));
}
footer.visible .footer-line{width:min(560px,80%);transition:width 1.3s var(--ease) .2s}
.footer-star{font-size:2rem;margin-bottom:10px}
.footer-license{font-family:var(--hand);font-size:1.3rem;color:var(--ink);margin-bottom:8px}
.footer-meta{font-size:.9rem}
/* ---------- 响应式 ---------- */
@media (max-width:820px){
body{font-size:15.5px}
.section{padding:54px 16px}
.nav-links{display:none}
.table-scroll table{min-width:560px}
.hero{padding-top:100px}
.lead{padding:22px 20px}
.arch-beam{left:43%;width:14%}
}
@media (max-width:520px){
.table-scroll table{min-width:480px}
.contrib-card{flex-direction:column;align-items:center;text-align:center}
}
/* ---------- 减少动效 ---------- */
@media (prefers-reduced-motion:reduce){
html{scroll-behavior:auto}
*,*::before,*::after{
animation-duration:.01ms!important;
animation-iteration-count:1!important;
transition-duration:.01ms!important;
transition-delay:0ms!important;
}
}
</style>
<noscript>
<style>
.reveal,.reveal-left,.reveal-right,.reveal-scale,.reveal-rotate,.reveal-up,.reveal-pop,
.trow,.trow-alt-col td,.li-rv,.tline,.tl,.hero-title .hc,.hero-sub,.hero-desc,.badge,
.hero-img-card,.hero-banner,.term-bar,.term-body,.arch-card pre,.quote,
.detail-head .detail-num,.detail-head .detail-num::after{opacity:1!important;transform:none!important}
</style>
</noscript>
</head>
<body>
<!-- 阅读进度条 -->
<div class="progress" id="progress" aria-hidden="true"></div>
<!-- 顶部导航 -->
<header class="nav" id="nav">
<div class="nav-inner">
<a class="nav-brand" href="#top">🍃 minor&nbsp;Agent</a>
<nav class="nav-links" aria-label="主导航">
<a href="#features">功能亮点</a>
<a href="#quickstart">快速开始</a>
<a href="#architecture">项目架构</a>
<a href="#dev-guide">开发手册</a>
<a href="#llm">LLM 推理服务</a>
</nav>
</div>
</header>
<!-- 漂浮小装饰 -->
<div class="decor" aria-hidden="true">
<i><svg viewBox="0 0 24 24" fill="#b5c99a"><path d="M17 8C8 10 5.9 16.17 3.82 21.34l1.89.66.95-2.3c.48.17.98.3 1.34.3C19 20 22 3 22 3c-1 2-8 2.25-13 3.25S2 11.5 2 13.5s1.75 3.75 1.75 3.75C7 8 17 8 17 8z"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#e9c46a"><circle cx="12" cy="12" r="7"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#f2a68d"><path d="M12 2l2.9 6.26L21 9.27l-4.5 4.38L17.8 20 12 16.77 6.2 20l1.3-6.35L3 9.27l6.1-1.01z"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#a3b18a"><path d="M12 3c1.8 3.2 4.5 4 4.5 4S15 9 13 10.5V21h-2V10.5C9 9 7.5 7 7.5 7s2.7-.8 4.5-4z"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#c8d6b9"><circle cx="12" cy="12" r="4.5"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#f8dda4"><path d="M12 3l1.8 6.2L20 11l-6.2 1.8L12 19l-1.8-6.2L4 11l6.2-1.8z"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#b5c99a"><circle cx="12" cy="12" r="6" fill="none" stroke="#b5c99a" stroke-width="3"/></svg></i>
<i><svg viewBox="0 0 24 24" fill="#e9c46a"><circle cx="12" cy="12" r="2.6"/></svg></i>
</div>
<main id="top">
<!-- ===================== HERO ===================== -->
<section class="hero" id="hero" aria-label="项目标题">
<div class="hero-blob b1"><span class="hb-inner"></span></div>
<div class="hero-blob b2"><span class="hb-inner"></span></div>
<div class="hero-inner">
<p class="hero-eyebrow observe">🍃 一本可以滚动的互动故事书 · README</p>
<h1 class="hero-title observe" data-stagger="60" aria-label="minor Agent">
<span class="hc">m</span><span class="hc">i</span><span class="hc">n</span><span class="hc">o</span><span class="hc">r</span><span class="hc space">&nbsp;</span><span class="hc accent-letter">A</span><span class="hc">g</span><span class="hc">e</span><span class="hc">n</span><span class="hc">t</span>
</h1>
<p class="hero-sub observe">一个可本地运行的多模态 Agent 桌面应用 —— 真正能「动手」操作你电脑的 AI 助手</p>
<p class="hero-desc observe">基于 <strong>LangGraph</strong> 构建,具备 GUI 自动化、浏览器控制、终端执行、RAG 知识库、邮件收发、图像生成、语音对话、数据库操作、定时任务等 20 项工具,并提供 Electron 桌面端一键安装体验。</p>
<div class="hero-badges observe" data-stagger="90">
<a class="badge" href="#features">✨ 功能亮点</a>
<a class="badge" href="#quickstart">🚀 快速开始</a>
<a class="badge" href="#architecture">🏗️ 项目架构</a>
<a class="badge" href="#dev-guide">🛠️ 开发手册</a>
<a class="badge" href="#llm">🔌 LLM 推理服务</a>
</div>
<figure class="hero-img-card observe">
<div class="img-card" data-path="./git_image/标题.png">
<img src="./git_image/标题.png" alt="minor Agent" loading="eager"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/标题.png</span>
</div>
</figure>
<figure class="hero-banner observe">
<div class="img-card" data-path="./git_image/banner.png">
<img src="./git_image/banner.png" alt="minor Agent 主界面" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/banner.png</span>
</div>
</figure>
</div>
<a class="scroll-hint" href="#toc" aria-label="向下滚动">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#a3b18a" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>
<span>SCROLL</span>
</a>
</section>
<!-- ===================== 目录 ===================== -->
<section class="section" id="toc" aria-label="目录">
<div class="section-head">
<span class="sec-emoji">🧭</span>
<h2>目录</h2>
<p class="sec-sub">像翻书一样,选择你想阅读的章节 —— 点击平滑滚动并自动触发该章节动画。</p>
</div>
<div class="lead reveal" data-reveal-list="up" data-stagger="50">
<a class="chip" href="#intro"><b>📖</b> 项目简介</a>
<a class="chip" href="#features"><b>✨</b> 功能亮点 · 20 项工具</a>
<a class="chip" href="#architecture"><b>🏗️</b> 项目架构</a>
<a class="chip" href="#structure"><b>📂</b> 项目结构</a>
<a class="chip" href="#quickstart"><b>🚀</b> 快速开始</a>
<a class="chip" href="#config"><b>⚙️</b> 配置说明</a>
<a class="chip" href="#details"><b>🧠</b> 实现细节</a>
<a class="chip" href="#llm"><b>🔌</b> LLM 推理服务</a>
<a class="chip" href="#dev-guide"><b>🛠️</b> 开发手册</a>
<a class="chip" href="#notes"><b>⚠️</b> 注意事项</a>
<a class="chip" href="#contributors"><b>👥</b> Contributors</a>
<a class="chip" href="#license"><b>📄</b> License</a>
</div>
</section>
<!-- ===================== 项目简介 ===================== -->
<section class="section" id="intro" aria-label="项目简介">
<div class="section-head">
<span class="sec-emoji">📖</span>
<h2>项目简介</h2>
</div>
<div class="lead reveal-left">
<p><strong>minor Agent</strong> 最初按 Web 在线应用开发,后转为本地桌面应用开源。它不是又一个「对话框 + 联网搜索」的套壳 Agent,而是一个<strong>以「工具执行 + 视觉闭环」为核心</strong>的通用智能体:</p>
<blockquote class="quote reveal">
<span class="q-emoji">🎓</span><strong>项目初衷</strong>:这是一个 LangChain + LangGraph 的学习项目。基于两者构建 Agent 工作流,结构清晰、上手容易、扩展方便,适合作为 Agent 开发的参考实现。
</blockquote>
<div class="chips" data-reveal-list="left" data-stagger="70">
<span class="chip"><span>🖥️</span><span><b>真能操作桌面</b> — 截图 → 视觉定位 → 键鼠模拟,闭环完成 ERP 录入、表单填写、软件操控</span></span>
<span class="chip"><span>🧩</span><span><b>20 项内置工具 + 单文件插件化</b> — 从终端命令到邮件收发,从 PPT 制作到图像生成;新增工具 = 一个文件 + 协议接入,前端面板自动生成,无需大面积改动</span></span>
<span class="chip"><span>🏠</span><span><b>完全本地运行</b> — Electron 打包,数据不出本机;后端 FastAPI + 前端零延迟直读磁盘</span></span>
<span class="chip"><span>🎨</span><span><b>可深度定制</b> — 主题 / 字体 / 字号 / 背景 / 工作空间 / 提示词分层架构,全部可调</span></span>
<span class="chip"><span>🔁</span><span><b>工程级稳定性</b> — 循环检测、上下文压缩、阻塞式人工确认、轨迹回放,对抗 LLM 的「幻觉操作」</span></span>
<span class="chip"><span>💾</span><span><b>存储多后端(接口隔离)</b> — 会话与定时任务存储统一接口驱动,<b>JSON / MySQL / SQLite</b> 一键切换、不双写;存储数据库与工具数据库配置隔离;新增存储方式只需实现接口,消费方无感知后端</span></span>
</div>
<blockquote class="quote reveal">
<span class="q-emoji">💡</span>项目演进:<code>Web 在线应用</code> → <code>Electron 桌面应用</code>。源码同一套 <code>src/</code>,既可由 <code>uvicorn</code> 作 Web 服务启动,也可由 Electron 主进程拉起,<strong>双模式共用</strong>。
</blockquote>
<p style="text-align:center;margin-top:22px" class="reveal">
<span class="img-card" style="display:block;width:min(860px,100%);margin:0 auto" data-path="./git_image/architecture.png">
<img src="./git_image/architecture.png" alt="系统架构总览" style="width:100%" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/architecture.png</span>
</span>
</p>
</div>
</section>
<hr class="section-divider">
<!-- ===================== 功能亮点 ===================== -->
<section class="section" id="features" aria-label="功能亮点">
<div class="section-head">
<span class="sec-emoji">✨</span>
<h2>功能亮点</h2>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin-bottom:16px">🛠️ 20 项工具能力</h3>
<div class="table-scroll reveal-scale">
<table data-row-anim="right">
<thead>
<tr><th>工具</th><th>文件</th><th>能力</th></tr>
</thead>
<tbody>
<tr><td><span class="tool-emoji">🖱️</span> <strong>gui</strong></td><td><code>tools/gui.py</code></td><td>桌面 GUI 自动化:截图、视觉批量定位、点击/输入/快捷键/滚动(桌面/任意应用轨)</td></tr>
<tr><td><span class="tool-emoji">🌐</span> <strong>browser_control</strong></td><td><code>tools/browser_control.py</code></td><td>浏览器打开/关闭、地址导航、标签页交互(Playwright + CDP 分离进程,Profile 默认一次性临时目录,可配置持久)</td></tr>
<tr><td><span class="tool-emoji">🧭</span> <strong>web_page</strong></td><td><code>tools/web_page.py</code></td><td>浏览器页面 <strong>DOM 级精准操作</strong>(浏览器轨):snapshot 元素清单、click/fill/scroll/press/extract/screenshot/eval_js 等,毫秒级、优于 GUI 视觉定位;典型流程 browser_control 打开 → web_page 操作</td></tr>
<tr><td><span class="tool-emoji">💻</span> <strong>terminal_execute</strong></td><td><code>tools/terminal_execute.py</code></td><td>执行任意终端命令(含目录/文件操作,可替代 directory_listing),支持工作目录与超时</td></tr>
<tr><td><span class="tool-emoji">🪟</span> <strong>software_control</strong></td><td><code>tools/software_control.py</code></td><td>Win32 API 启停软件、注册表、<code>os.startfile</code></td></tr>
<tr><td><span class="tool-emoji">📧</span> <strong>email</strong></td><td><code>tools/email.py</code></td><td>IMAP 收邮件 + SMTP 发邮件(含附件)</td></tr>
<tr><td><span class="tool-emoji">📄</span> <strong>doc</strong></td><td><code>tools/doc.py</code></td><td>创建/读取/编辑 docx / xlsx / pptx / pdf / txt</td></tr>
<tr><td><span class="tool-emoji">📨</span> <strong>send_file</strong></td><td><code>tools/send_file.py</code></td><td>以邮件附件形式发送本地文件</td></tr>
<tr><td><span class="tool-emoji">🔍</span> <strong>web_search</strong></td><td><code>tools/web_search.py</code></td><td>联网搜索(Tavily / Firecrawl / 火山引擎)</td></tr>
<tr><td><span class="tool-emoji">🗄️</span> <strong>text2sql</strong></td><td><code>tools/text2sql.py</code></td><td>自然语言/SQL 操作数据库(MySQL / SQLite,工具数据库配置):查表结构、查询、增删改、建库建表</td></tr>
<tr><td><span class="tool-emoji">📚</span> <strong>rag</strong></td><td><code>tools/rag.py</code></td><td>ChromaDB 向量检索 + Reranker 重排序</td></tr>
<tr><td><span class="tool-emoji">🎨</span> <strong>image_gen</strong></td><td><code>tools/image_gen.py</code></td><td>调用 Z-Image-Turbo 生成图片</td></tr>
<tr><td><span class="tool-emoji">✅</span> <strong>todo_list</strong></td><td><code>tools/todo_list.py</code></td><td>多步任务规划与进度跟踪</td></tr>
<tr><td><span class="tool-emoji">🤝</span> <strong>human_interaction</strong></td><td><code>tools/human_interaction.py</code></td><td>阻塞式询问用户:收集信息 / 多选一,答案作为普通工具返回值</td></tr>
<tr><td><span class="tool-emoji">🤖</span> <strong>agent_call</strong></td><td><code>tools/agent_call.py</code></td><td>委派子 Agent 处理子任务</td></tr>
<tr><td><span class="tool-emoji">🧭</span> <strong>skill_router</strong></td><td><code>tools/skill_router.py</code></td><td>匹配并路由到预定义 Skill 流程</td></tr>
<tr><td><span class="tool-emoji">📅</span> <strong>date_query</strong></td><td><code>tools/date_query.py</code></td><td>当前日期时间查询</td></tr>
<tr><td><span class="tool-emoji">⏰</span> <strong>cron_manager</strong></td><td><code>tools/cron_manager.py</code></td><td>定时任务管理:查看/新增/修改/删除定时任务,支持 cron/间隔/一次性触发</td></tr>
<tr><td><span class="tool-emoji">📊</span> <strong>hard_excel_read</strong></td><td><code>tools/hard_excel_read.py</code></td><td>复杂 Excel 表结构读取:前 N 行原始数据 + 维度/合并区/数据区范围建议,供大模型解析结构</td></tr>
<tr><td><span class="tool-emoji">📥</span> <strong>excel2sql</strong></td><td><code>tools/excel2sql.py</code></td><td>按大模型给定的数据行列范围,将 Excel 数据区流式批量入库(MySQL / SQLite,自动建库建表)</td></tr>
</tbody>
</table>
</div>
<p class="reveal-up" style="color:var(--ink-soft);font-size:.92rem;margin-top:10px">🔒 <strong>main Agent 默认挂载</strong>(代码强制注入,不可通过配置移除,前端显示为锁定项):<code>call_subagent</code>(子 Agent 委派)、<code>doc_tool</code>(文档读写)、<code>human_interaction</code>(人机交互审查)、<code>todo_list</code>(任务规划)、<code>terminal_execute</code>(终端命令)。其余工具按 <code>config.json</code> 中 main agent 的 tools 列表自行增删;子 Agent 无此限制。</p>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">🎯 区别于其他 Agent 项目的核心设计</h3>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt-col">
<thead>
<tr><th>设计点</th><th>多数 Agent 项目</th><th>minor Agent</th></tr>
</thead>
<tbody>
<tr><td><strong>GUI 操作粒度</strong></td><td>一步一调用,N 个动作 = 2N+1 次 LLM 调用</td><td><strong>动作序列合并</strong>:同页多操作打包为一次 <code>gui</code> 调用,N 个动作仅 2 次 LLM 调用</td></tr>
<tr><td><strong>UI 自动化双轨</strong></td><td>单轨(截图 + 坐标模拟)</td><td><strong>Playwright + GUI 双轨兼顾容错与效率</strong>:浏览器内走 DOM 级精准操作(<code>web_page</code> 毫秒级、定位可靠);桌面/任意应用走 GUI 视觉定位(<code>gui_tool</code> 截图视觉闭环、可处理任意界面)——浏览器能 DOM 就不截图,桌面无 DOM 就走视觉</td></tr>
<tr><td><strong>视觉反馈</strong></td><td>仅返回坐标文本</td><td><strong>截图作为视觉闭环</strong>注入上下文,多模态 LLM 看到真实屏幕状态</td></tr>
<tr><td><strong>坐标定位</strong></td><td>逐元素请求</td><td><strong>批量定位</strong> <code>_batch_ground_elements</code>:N 个元素 1 次子模型调用</td></tr>
<tr><td><strong>循环失控</strong></td><td>无防护,token 烧穿</td><td><strong>两级循环检测</strong>:连续 3 次同工具警告反思 → 连续 5 次强制终止</td></tr>
<tr><td><strong>上下文膨胀</strong></td><td>简单截断</td><td><strong>图内分级压缩</strong>:token 超 <code>窗口×COMPRESS_RATE</code> 触发,压缩历史工具调用为累积摘要,每步 LLM 调用后检查</td></tr>
<tr><td><strong>上下文连续性</strong></td><td>思考链跨轮丢失</td><td><strong>历史思考/反思注入</strong>:压缩游标后的思考、反思、工具调用按因果顺序交错注入(稳定 id、确定性构建,前缀缓存友好);深度思考与反思分离存储</td></tr>
<tr><td><strong>部署形态</strong></td><td>仅 Web / 仅 API</td><td><strong>同一套源码双模式</strong>:<code>uvicorn</code> Web 服务 ↔ Electron 桌面应用</td></tr>
<tr><td><strong>文件操作</strong></td><td>HTTP 上传下载</td><td>桌面端走 <strong>IPC 直读磁盘</strong>,零延迟;Web 端走 <code>/api/fs/*</code></td></tr>
<tr><td><strong>提示词</strong></td><td>单层 system prompt</td><td><strong>四层提示词架构</strong>:系统级 / 工具描述 / 运行时注入 / 内部子模型</td></tr>
<tr><td><strong>插件化</strong></td><td>注册式插件表(适配新插件需改框架主流程)</td><td><strong>前后端单文件插件化</strong>:后端按 <code>BaseTool</code> 协议单文件实现、前端按 <code>setXxxDeps</code> 依赖注入协议接入,<strong>监听事件 / 回调驱动而非注册式</strong>——新增或替换一个插件只需保持协议签名,宿主装配零改动</td></tr>
</tbody>
</table>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:48px 0 10px">🖼️ 运行界面</h3>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:26px">Agent 运行过程中的关键交互界面,支持文件从任意位置<strong>拖拽到聊天区或编辑区</strong>,灵活组织工作流:</p>
<figure class="shot-card reveal-rotate" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px;justify-items:center;margin-bottom:6px">
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-人机交互.png">
<img src="./git_image/运行-人机交互.png" alt="人机交互确认" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-人机交互.png</span>
</div>
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-todolist.png">
<img src="./git_image/运行-todolist.png" alt="任务规划 TodoList" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-todolist.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>人机交互确认</b> — 敏感操作(终端执行、文件写入等)与信息收集以「输入区上方通知条」形式实时弹出:只显示标题与数量角标,点击展开为完整卡片(标题/内容/选项胶囊/补充输入框),选择或补充后作为工具返回值,Agent 继续执行;<b>拒绝也只是一次工具返回值</b>,Agent 自行调整方案继续,不会结束本轮&emsp;|&emsp;<b>任务规划 TodoList</b> — 多步任务自动拆解与进度跟踪,同样以通知条展示(最新步骤 + 数量角标,可展开全部步骤),Todo 状态<b>会话级存储</b>(chat 与 cron 各自独立,切换会话自动恢复)</sub></p>
<figure class="shot-card reveal-rotate" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px;justify-items:center;margin:24px 0 6px">
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-文件修改.png">
<img src="./git_image/运行-文件修改.png" alt="文件编辑模式" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-文件修改.png</span>
</div>
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-工具调用1.png">
<img src="./git_image/运行-工具调用1.png" alt="工具调用记录" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-工具调用1.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>文件编辑模式</b> — 内置 Monaco 编辑器,支持代码高亮、差异对比、文件树拖拽组织&emsp;|&emsp;<b>工具调用记录</b> — 实时展示每轮 ReAct 的工具调用链、参数及返回值;思考(reasoning_content)与反思(content)分离显示为独立的 Thought / Reflection 行</sub></p>
<figure class="shot-card reveal-rotate" style="margin:24px 0 6px;text-align:center">
<div class="img-card" style="width:min(560px,100%);margin:0 auto" data-path="./git_image/运行-工具调用2.png">
<img src="./git_image/运行-工具调用2.png" alt="子 Agent 嵌套调用" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-工具调用2.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>子 Agent 嵌套调用</b> — 主 Agent 委派子 Agent 执行子任务,工具调用以嵌套结构展示,思考过程穿插显示,支持展开/折叠查看详情</sub></p>
<figure class="shot-card reveal-rotate" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px;justify-items:center;margin:24px 0 6px">
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-定时任务.png">
<img src="./git_image/运行-定时任务.png" alt="定时任务创建" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-定时任务.png</span>
</div>
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/运行-edit模式.png">
<img src="./git_image/运行-edit模式.png" alt="Edit 编辑模式" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/运行-edit模式.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>定时任务管理</b> — 支持 cron / 间隔 / 一次性三种触发方式;任务按<b>工作区分组</b>展示;任务会话与聊天同构(点击会话式查看历史、多轮对话、流式渲染),任务在<b>独立子进程</b>中执行;模式切换锁:chat 运行时可查看 cron、cron 运行时可查看 chat,双容器渲染不打断流式输出&emsp;|&emsp;<b>Edit 编辑模式</b> — 独立编辑区,Monaco 编辑器 + 文档树侧栏,支持拖拽文件/文件夹到附件区发送给 Agent</sub></p>
<figure class="shot-card reveal-rotate" style="margin:24px 0 6px;text-align:center">
<div class="img-card" style="width:min(560px,100%);margin:0 auto" data-path="./git_image/修改图片.png">
<img src="./git_image/修改图片.png" alt="图片修改画板" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/修改图片.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>图片修改画板</b> — 附件图片缩略图 hover 显示删除按钮,点击图片打开预览浮窗,支持下载/修改/关闭;点击「修改」进入画板编辑器,在底图上使用画笔/矩形/圆形/直线/文字/橡皮擦进行标注编辑,Ctrl+滚轮缩放,中键拖动平移,支持撤销/重做,确定后替换原图</sub></p>
<figure class="shot-card reveal-rotate" style="margin:24px 0 6px;text-align:center">
<div class="img-card" style="width:min(560px,100%);margin:0 auto" data-path="./git_image/画板.png">
<img src="./git_image/画板.png" alt="空白画板" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/画板.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>空白画板</b> — 输入区画板按钮一键打开 1536×768 空白画布,支持画笔/矩形/圆形/直线/文字/橡皮擦绘图,颜色与粗细可调,文字对象可拖拽/缩放/旋转,Ctrl+滚轮缩放画布,中键平移,完成导出为 PNG 自动添至附件区</sub></p>
</section>
<hr class="section-divider">
<!-- ===================== 项目架构 ===================== -->
<section class="section" id="architecture" aria-label="项目架构">
<div class="section-head">
<span class="sec-emoji">🏗️</span>
<h2>项目架构</h2>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin-bottom:16px">整体分层</h3>
<div class="arch-card reveal">
<div class="arch-beam" aria-hidden="true"></div>
<pre>┌──────────────────────────────────────────────────────────────┐
│ Electron 主进程 (main.js) │
│ 窗口管理 · 首次配置向导 · Python 生命周期 · 端口清理 · IPC │
└───────────────────────────────┬──────────────────────────────┘
│ spawn uvicorn (127.0.0.1:8765)
▼
┌──────────────────────────────────────────────────────────────┐
│ FastAPI 后端 (web/server.py) │
│ 会话管理 · 多模态收发 · SSE 流式 · 文件服务 · 工具调用推送 │
└───────────────────────────────┬──────────────────────────────┘
│ turn_runner.start_turn()
│(每会话一个后台线程,HTTP 请求
│ 挂载等待;断连不影响执行)
▼
┌──────────────────────────────────────────────────────────────┐
│ Agent 编排层 (agents/agent_runtime.py) │
│ 消息拼装 · 历史持久化 · 跨轮工具上下文 · 实时推送 │
└───────────────────────────────┬──────────────────────────────┘
│ graph.invoke()
▼
┌──────────────────────────────────────────────────────────────┐
│ LangGraph ReAct 图 (core/graph.py) │
│ agent 节点 → tools 节点 → process_tool_artifact 节点 │
│ 内含: 动态尾部注入 · 压缩 · 循环检测 · 路由 │
│ 人机交互: 阻塞式普通工具(core/human_request.py) │
└───────────────────────────────┬──────────────────────────────┘
│ 调用
▼
┌──────────────────────────────────────────────────────────────┐
│ 20 项工具 (tools/*.py) · 5 项技能 (skills/) │
└──────────────────────────────────────────────────────────────┘</pre>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">ReAct 循环(图内)</h3>
<div class="line-cnt reveal" data-line="type" data-interval="60">
<pre>START → [agent: call_model] ──有 tool_calls──▶ [tools: 并行工具节点] ──▶ [process_tool_artifact] ──▶ [compress]
▲ ▲ │ │
│ 无 tool_calls │ ◀────────────────────────────┘
▼ └── 子 Agent 结果 / 工具返回值注入 ◀── 并行工具节点(线程池)
END</pre>
</div>
<div class="lead reveal" style="margin-top:26px">
<ul data-reveal-list="left" data-stagger="60">
<li class="li-rv"><strong><code>call_model</code></strong>(<code>core/nodes.py</code>):本轮回插思考/反思(<code>[思考]</code> / <code>[反思]</code> 合成消息)→ 追加动态尾部(TodoList 状态 + 循环提醒;固定反思引导已并入系统提示词前缀)→ LLM 调用 → 每步检查压缩(token 超阈值标记)→ 提取 thinking / reflection 独立记录</li>
<li class="li-rv"><strong><code>should_continue</code></strong>(<code>core/routing.py</code>):含 tool_calls → 循环检测 → 路由到 <code>tools</code> 或 <code>END</code>(需人工确认的工具不在此暂停,而是在工具执行钳点阻塞征求用户意见,见「阻塞式人机交互」)</li>
<li class="li-rv"><strong><code>process_tool_artifact</code></strong>:gui 截图作为 synthetic HumanMessage 注入,实现视觉闭环</li>
<li class="li-rv"><strong><code>compress</code></strong>(<code>core/nodes.py</code>):token 超 <code>窗口×COMPRESS_RATE</code> 时,将历史工具调用压缩为累积摘要(见「实现细节」)</li>
</ul>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">多 Agent 模式</h3>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:18px">
多 Agent 并非独立图,而是<strong>同一张 react 图内的工具级并行</strong>:<code>tools</code> 节点由 <code>make_parallel_tool_node</code>(线程池)取代 ToolNode,一轮内的多个工具调用并行执行;其中 <code>agent_call</code> 工具会以主 Agent 当前消息快照启动<strong>子 Agent 的独立图实例</strong>执行子任务,结果以工具返回值注入主 Agent 上下文。子 Agent 内的人机交互同样为阻塞式——在子图线程中等待通知条应答,完成后自然回到主图:
</p>
<div class="line-cnt reveal" data-line="type" data-interval="55">
<pre>[agent] ──有 agent_call──▶ [tools: parallel_tool_node(线程池)]
│
├─ 普通工具:直接并行执行
└─ agent_call:spawn 子 Agent Runtime → 子图 invoke
│(子图内 human_interaction 阻塞等待
│ 通知条应答,答案作为工具返回值)
└─ 结果作为 ToolMessage 注入主 Agent ◄┐
▲ │
└──────────────── 有 agent_call → 继续循环;无 → END ──────────────────────┘</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===================== 项目结构 ===================== -->
<section class="section" id="structure" aria-label="项目结构">
<div class="section-head">
<span class="sec-emoji">📂</span>
<h2>项目结构</h2>
<p class="sec-sub">进入视口时逐层「生长」:先出根目录,再一层一层展开枝叶 —— 目录奶油绿,注释暖灰斜体。</p>
</div>
<div class="line-cnt reveal" data-line="tree">
<pre>Agent/
├── electron/ # 🖥️ Electron 桌面端
│ ├── main.js # 主进程:窗口/IPC/Python生命周期/端口清理
│ ├── preload.js # 安全桥接层:fs/dialog/window/Python setup API
│ └── setup.html # 首次配置向导(3 步:Python环境 → LLM → 高级参数)
│
├── src/
│ ├── agent/ # 🧠 Agent 核心
│ │ ├── agents/
│ │ │ ├── agent_runtime.py # 编排层入口:execute_agent(动态 Agent 工厂,main 默认挂载工具强制注入)
│ │ │ ├── config.json # ★ 合并配置(agents/tools/env/gui/workspace/theme 分区,含密钥)
│ │ │ └── config_dist.json # 配置模板(不含密钥)
│ │ ├── core/
│ │ │ ├── graph.py # LangGraph 构建(单 Agent + 多 Agent)
│ │ │ ├── nodes.py # ReAct 节点:call_model(思考/反思提取与本轮回插)/ process_tool_artifact / 压缩
│ │ │ ├── runtime.py # 图执行器:消息拼装、历史加载、轨迹落盘
│ │ │ ├── routing.py # should_continue 路由 + 强制终止
│ │ │ ├── state.py # Graph 状态定义(messages / agent_mode)
│ │ │ ├── loop_detector.py # 循环检测:重复工具调用,两级响应
│ │ │ ├── human_request.py # ★ 人工请求注册表:交互类工具的通用阻塞通道(ask_human)
│ │ │ ├── turn_runner.py # ★ 后台线程轮次执行器(解耦 HTTP 与图执行生命周期)
│ │ │ ├── llm.py # ChatQwen 多模态封装(图像/附件/非标准 tool_call)
│ │ │ ├── tool_policy.py # 工具执行策略(直执 / 需确认)
│ │ │ ├── db.py # ★ 数据库基础设施:配置加载(存储库/工具库隔离)、连接、幂等建库建表(MySQL/SQLite)
│ │ │ ├── storage.py # ★ 统一存储层:接口 + JSON/MySQL/SQLite 三实现 + 工厂 + scope
│ │ │ └── config_manager.py# 配置读写:agent/tool/env/gui/model/theme + MAIN_DEFAULT_TOOLS
│ │ ├── memory/
│ │ │ └── system_prompt.py # ★ 四层提示词:MAIN / PLAN / REFLECTION / VISION...
│ │ ├── tools/ # 20 项工具(见上表)
│ │ │ ├── ...(gui / browser_control / terminal_execute / email / doc / web_search / text2sql / rag / cron_manager 等)
│ │ ├── cron/ # ⏰ 定时任务引擎
│ │ │ ├── models.py # 数据模型(CronTask / Trigger,含 workspace 字段)
│ │ │ ├── storage.py # 转发层(实现统一在 core/storage.py)
│ │ │ ├── scheduler.py # 调度计算与触发扫描
│ │ │ ├── runner.py # 任务执行器(独立子进程,切换存储 cron 作用域)
│ │ │ └── live.py # 常驻调度循环
│ │ ├── skills/ # 5 项技能(skill.json + skill.md)
│ │ │ ├── netease_mail_read/
│ │ │ ├── netease_mail_send/
│ │ │ ├── ppt_maker/ # HTML 模板 → PPT(含 9 个附件文档 + 模板)
│ │ │ ├── taste/ # 代码生成界面美化
│ │ │ └── wechat_send_message/
│ │ ├── tts/
│ │ │ └── streaming_client.py # 流式 TTS 客户端(SSE 收音频块)
│ │ ├── history/
│ │ │ ├── session_storage.py # 转发层(实现统一在 core/storage.py)
│ │ │ ├── tool_call_recorder.py # 每轮工具调用轨迹记录(含思考/反思双字段,tool_{turn_id}.json)
│ │ │ └── sqlite/ # SQLite 存储默认目录(storage.sqlite / tool/ 每库一文件)
│ │ ├── utils/ # env_utils / agent_utils / image_utils / ppt_utils / tool_call_utils
│ │ └── data/ # 运行时数据(浏览器 Profile 等,不入库)
│ │
│ └── web/ # 🎨 前端 UI
│ ├── server.py # FastAPI 后端(75+ 路由)
│ ├── ui_session.py # UI 会话状态管理
│ ├── index.html # 主页面(侧边栏/聊天/编辑/Agent面板/操作栏)
│ ├── app.js / app.css # 入口
│ ├── js/ # 30 个前端模块(见开发手册)
│ ├── css/ # 18 个样式模块 + variables.css 主题变量
│ ├── packages/ # Monaco Editor 本地 npm 包
│ └── image/ # SVG 图标资源
│
├── llm_server/ # 🧠 LLM 推理服务(独立部署,详见该目录 README)
├── git_image/ # 📸 README 展示图片
├── package.json # Electron + electron-builder 配置
├── requirements.txt # Python 依赖
└── README.md # 本文件</pre>
</div>
</section>
<hr class="section-divider">
<!-- ===================== 快速开始 ===================== -->
<section class="section" id="quickstart" aria-label="快速开始">
<div class="section-head">
<span class="sec-emoji">🚀</span>
<h2>快速开始</h2>
<p class="sec-sub">minor Agent 提供两种运行方式:<strong>桌面应用(推荐)</strong> 与 <strong>Web 开发模式</strong>。两者共用同一套 <code>src/</code> 源码。</p>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin-bottom:18px">方式一:桌面应用(推荐体验)</h3>
<h4 class="reveal-up" style="font-size:1.1rem;margin:22px 0 4px">1. 安装依赖</h4>
<div class="term reveal">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body"><span class="pwd">cd C:\Users\86166\Desktop\Agent_Learning_minor\Agent</span>
<span class="cmt"># 设置国内镜像(可选,加速 Electron 下载)</span>
$env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
npm install</pre>
</div>
<h4 class="reveal-up" style="font-size:1.1rem;margin:22px 0 4px">2. 启动应用</h4>
<div class="term reveal">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body">npm start</pre>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin:14px 0 18px">
首次运行会弹出<strong>配置向导</strong>(3 步),写入 <code>src/agent/agents/config.json</code> 后自动启动:
</p>
<div class="table-scroll reveal-scale">
<table data-row-anim="right">
<thead><tr><th>步骤</th><th>配置内容</th><th>说明</th></tr></thead>
<tbody>
<tr><td><strong>Step 1</strong> Python 环境</td><td>选择 / 自动检测系统 Python</td><td>主进程会调用系统 Python 启动 uvicorn;可选自动 <code>pip install</code> 依赖</td></tr>
<tr><td><strong>Step 2</strong> LLM 设置</td><td><code>base_url</code> / <code>api_key</code> / <code>model</code></td><td>必填;可填任何 OpenAI 兼容端点</td></tr>
<tr><td><strong>Step 3</strong> 高级参数</td><td>TTS / ASR / RAG / 搜索 / 压缩 / 循环阈值</td><td>可选;不填则对应能力禁用</td></tr>
</tbody>
</table>
</div>
<blockquote class="quote reveal">💡 之后启动直接进入主界面,不再弹向导。如需重配,删除 <code>src/agent/agents/config.json</code>(或复制 <code>config_dist.json</code> 为新配置)即可重触首次流程。</blockquote>
<h4 class="reveal-up" style="font-size:1.1rem;margin:22px 0 4px">3. 打包为安装程序</h4>
<div class="term reveal">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body"><span class="cmt"># 设置镜像 + 自定义缓存目录后构建</span>
$env:ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
$env:ELECTRON_BUILDER_CACHE="$PWD\electron-builder-cache"
npm run build</pre>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin:14px 0">
输出 <code>dist/minor Agent Setup 1.0.0.exe</code>,双击安装即可。安装包使用<strong>系统 Python + 运行时安装依赖</strong>方案,体积远小于打包完整 venv 的方案。
</p>
<details class="pack reveal">
<summary>📦 打包原理(点击展开)</summary>
<div class="pack-body">
<ul data-reveal-list="left" data-stagger="50">
<li class="li-rv"><code>package.json</code> 的 <code>build.files</code> 仅打包 <code>electron/</code>、<code>src/</code>、<code>requirements.txt</code>,<strong>排除</strong> <code>config.json</code>(每个用户独立生成,模板 <code>config_dist.json</code> 随包分发)</li>
<li class="li-rv"><code>asar: false</code>:源码不加密,便于审计与二次开发</li>
<li class="li-rv">NSIS 安装器:允许改安装目录、创建桌面/开始菜单快捷方式、卸载时清理 AppData</li>
<li class="li-rv">首次启动由 <code>setup.html</code> 向导引导:检测 Python → 配置 LLM → 写 <code>config.json</code> → 启动 FastAPI → 加载 UI</li>
<li class="li-rv">主进程 <code>findSystemPython()</code> 依次检查:用户配置路径 → 常见 Python 安装位置 → PATH 中的 <code>python</code></li>
</ul>
</div>
</details>
<hr class="section-divider">
<h3 class="reveal" style="font-size:1.4rem;margin:8px 0 12px">方式二:Web 开发模式(调试用)</h3>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:18px">适合前端开发与快速迭代,无需 Electron。</p>
<div class="term reveal">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body"><span class="pwd">cd C:\Users\86166\Desktop\Agent_Learning_minor\Agent</span>
<span class="cmt"># 1. 创建并激活虚拟环境</span>
python -m venv myenv
.\myenv\Scripts\Activate.ps1 <span class="cmt"># Windows</span>
<span class="cmt"># source myenv/bin/activate # macOS / Linux</span>
<span class="cmt"># 2. 安装依赖</span>
pip install --upgrade pip
pip install -r requirements.txt
<span class="cmt"># 3. 配置环境(首次)</span>
<span class="cmt"># 复制模板并填写 LLM 凭证</span>
Copy-Item src\agent\agents\config_dist.json src\agent\agents\config.json
<span class="cmt"># 编辑 config.json,至少填写 env.models[].base_url / api_key / model</span>
<span class="cmt"># 4. 启动 Web 服务</span>
$env:PYTHONPATH="src;$env:PYTHONPATH"
uvicorn web.server:app --host 127.0.0.1 --port 8765 --reload
<span class="cmt"># 5. 浏览器打开</span>
<span class="cmt"># http://127.0.0.1:8765</span></pre>
</div>
<details class="pack reveal">
<summary>🌐 (可选)公网暴露</summary>
<div class="pack-body">
<div class="term reveal" style="margin-top:6px">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body"><span class="cmt"># 通过 Cloudflare Tunnel 临时暴露</span>
cloudflared tunnel --url http://localhost:8765</pre>
</div>
</div>
</details>
</section>
<hr class="section-divider">
<!-- ===================== 配置说明 ===================== -->
<section class="section" id="config" aria-label="配置说明">
<div class="section-head">
<span class="sec-emoji">⚙️</span>
<h2>配置说明</h2>
<p class="sec-sub">所有运行时配置集中在 <code>src/agent/agents/config.json</code>(合并单文件,含 <code>agents / tools / env / gui / workspace / theme</code> 分区),模板见 <code>config_dist.json</code>,首次向导自动生成。</p>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:12px">env 分区关键字段:</p>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>字段</th><th>默认值</th><th>说明</th></tr></thead>
<tbody>
<tr><td><code>models[]</code></td><td><code>SETUP_REQUIRED</code></td><td>LLM 列表:<code>{id, name, model, api_key, base_url, timeout, max_retries}</code>;ASR / TTS / RAG / ImageGen 四个本地服务也在此注册(按 <code>model</code> 字段匹配,<code>api_key</code> 留空)</td></tr>
<tr><td><code>gui_model_id</code></td><td><code>""</code></td><td>GUI 视觉定位专用模型(留空则回退主模型)</td></tr>
<tr><td><code>USER_PYTHON_PATH</code></td><td><code>""</code></td><td>指定 Python 解释器路径</td></tr>
<tr><td><code>LLM_CONTEXT_WINDOW</code></td><td><code>262144</code></td><td>上下文窗口大小(token)</td></tr>
<tr><td><code>COMPRESS_RATE</code></td><td><code>0.6</code></td><td>上下文压缩阈值比例(0~1),token 用量超过 <code>窗口×该值</code> 时触发压缩</td></tr>
<tr><td><code>IMG_SIZE</code></td><td><code>768</code></td><td>截图短边尺寸</td></tr>
<tr><td><code>GROUNDING_WIDTH/HEIGHT</code></td><td><code>1000</code></td><td>视觉定位输入分辨率</td></tr>
<tr><td><code>LOOP_DETECT_REPEATED_TOOL_WARN</code></td><td><code>3</code></td><td>连续同工具同参多少次警告</td></tr>
<tr><td><code>LOOP_DETECT_REPEATED_TOOL_END</code></td><td><code>5</code></td><td>连续同工具同参多少次强制终止</td></tr>
<tr><td><code>RAG_CHUNK_SIZE / OVERLAP</code></td><td><code>500 / 50</code></td><td>RAG 分块大小与重叠</td></tr>
<tr><td><code>SEND_FILE_SIZE_LIMIT</code></td><td><code>30</code></td><td>发送文件大小上限(MB)</td></tr>
<tr><td><code>WEB_SEARCH_API_KEY</code></td><td><code>""</code></td><td>联网搜索 API Key</td></tr>
<tr><td><code>WEB_SEARCH_ENGINE</code></td><td><code>"tavily"</code></td><td>搜索引擎选择(tavily / firecrawl / volcengine)</td></tr>
<tr><td><code>STORAGE_BACKEND</code></td><td><code>"json"</code></td><td>存储后端:json(本地文件)/ mysql / sqlite</td></tr>
<tr><td><code>STORAGE_DB_TYPE</code> + <code>STORAGE_DB_HOST/PORT/USER/PASSWORD/DATABASE/PATH</code></td><td><code>""</code></td><td><strong>存储数据库</strong>配置(mysql 用 host/port/user/password/database;sqlite 用 path,留空默认 <code>history/sqlite/storage.sqlite</code>);缺省回退旧 <code>MYSQL_*</code>(零迁移)</td></tr>
<tr><td><code>TOOL_DB_TYPE</code> + <code>TOOL_DB_HOST/PORT/USER/PASSWORD/DATABASE/PATH</code></td><td><code>"mysql"</code></td><td><strong>工具数据库</strong>配置——数据库操作工具(text2sql / excel2sql)专用,<strong>与存储后端完全隔离</strong>;sqlite path 留空默认 <code>history/sqlite/tool</code> 目录,每个库名一个 <code>.sqlite</code> 文件</td></tr>
<tr><td><code>MYSQL_*</code></td><td><code>""</code></td><td><strong>旧键</strong>:存储数据库 MySQL 参数(已被 <code>STORAGE_DB_*</code> 取代,仅作回退兼容)</td></tr>
<tr><td><code>BROWSER_PROFILE_DIR</code></td><td><code>""</code></td><td>浏览器控制工具持久 Profile 目录;留空 = 每次启动一次性临时 Profile(登录态不持久、不留盘)</td></tr>
<tr><td><code>TOOL_TIMEOUT</code></td><td><code>300</code></td><td>工具执行超时时间(秒)</td></tr>
<tr><td><code>CRON_TIME_PERIOD_MINUTES</code></td><td><code>30</code></td><td>定时任务时段长度(分钟),同一时段全局并发=1</td></tr>
<tr><td><code>EMAIL_ADDRESS / EMAIL_AUTH_CODE</code></td><td><code>""</code></td><td>邮箱凭证(IMAP/SMTP)</td></tr>
</tbody>
</table>
</div>
<blockquote class="quote reveal">⚙️ 思考档位(low / high / xhigh / max / ultra)为<strong>会话级</strong>配置(存于各会话 <code>session_meta.json</code>);以上参数均可在主界面「设置」面板热修改,或通过 <code>/api/config/*</code> 接口程序化调整。</blockquote>
<p class="reveal-up" style="color:var(--ink-soft);margin:16px 0">设置面板包含 11 个分组(常规 / 外观 / 模型 / Agent / 工具 / Skills / GUI / 网络搜索 / 邮件 / 存储与数据库 / 运行时参数),覆盖 Agent 运行的全部可调参数:</p>
<figure class="shot-card reveal-rotate" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px;justify-items:center;margin-bottom:6px">
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/5.png">
<img src="./git_image/5.png" alt="Agent 配置" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/5.png</span>
</div>
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/1.png">
<img src="./git_image/1.png" alt="工具配置" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/1.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>Agent 配置</b> — 管理多个 AI 角色,各自绑定独立模型与工具集(main Agent 默认挂载 5 个工具为锁定项,不可移除)&emsp;|&emsp;<b>工具配置</b> — 启用/禁用工具、调整参数及执行策略</sub></p>
<figure class="shot-card reveal-rotate" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:18px;justify-items:center;margin:24px 0 6px">
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/2.png">
<img src="./git_image/2.png" alt="环境变量" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/2.png</span>
</div>
<div class="img-card" style="width:min(360px,100%)" data-path="./git_image/3.png">
<img src="./git_image/3.png" alt="GUI 设置" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/3.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>环境变量</b> — LLM 密钥、路径、压缩阈值等全局参数&emsp;|&emsp;<b>GUI 设置</b> — 选择 GUI 自动化操作的目标显示器</sub></p>
<figure class="shot-card reveal-rotate" style="margin:24px 0 6px;text-align:center">
<div class="img-card" style="width:min(360px,100%);margin:0 auto" data-path="./git_image/4.png">
<img src="./git_image/4.png" alt="主题配置" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/4.png</span>
</div>
</figure>
<p class="shot-cap reveal" style="transition-delay:.1s"><sub><b>主题配置</b> — 12 套预设主题 + 自定义背景 / 模糊度 / 暗角 / 字体;另含 常规 / 模型 / Skills / 网络搜索 / 邮件 / 存储与数据库 / 运行时参数 等分组</sub></p>
</section>
<hr class="section-divider">
<!-- ===================== 实现细节 ===================== -->
<section class="section" id="details" aria-label="实现细节">
<div class="section-head">
<span class="sec-emoji">🧠</span>
<h2>实现细节</h2>
</div>
<!-- 1. 四层提示词架构 -->
<div class="detail-head reveal">
<span class="detail-num">1</span>
<h3>四层提示词架构</h3>
</div>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>层级</th><th>位置</th><th>注入时机</th><th>作用</th></tr></thead>
<tbody>
<tr><td><strong>L1 系统级</strong></td><td><code>memory/system_prompt.py</code></td><td>会话开始</td><td><code>MAIN_SYSTEM_PROMPT</code> 定义角色;<code>PLAN_MODE_PROMPT</code> 强制先规划;固定反思引导 <code>REFLECTION_PROMPT</code> 并入系统提示词前缀(思考关闭档位,前缀缓存友好)</td></tr>
<tr><td><strong>L2 工具描述</strong></td><td>各 <code>tools/*.py</code> 的 <code>description</code></td><td>工具绑定时</td><td>指导 LLM 正确调用(含 one-shot 示例)</td></tr>
<tr><td><strong>L3 运行时动态尾部</strong></td><td><code>nodes.py</code> / <code>loop_detector.py</code></td><td>每轮 ReAct</td><td>变化部分(TodoList 状态 + 循环警告)作为动态尾部追加到消息<strong>末尾</strong>,并持久化为长期记忆(<code>session_meta.dynamic_tail_history</code>)供后续轮次查看</td></tr>
<tr><td><strong>L4 内部子模型</strong></td><td><code>gui.py</code> <code>_build_batch_prompt</code></td><td>工具内部</td><td>批量坐标定位,one-shot 强制纯坐标输出</td></tr>
</tbody>
</table>
</div>
<div class="detail-head reveal" style="margin-top:20px">
<span class="detail-num" style="font-size:1.15rem">1.1</span>
<h3>前缀缓存(Prompt Caching)</h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:14px">上下文按「前缀稳定性」布局,最大化利用 LLM 服务端的前缀缓存:</p>
<div class="pre-card reveal">
[系统提示词(含固定反思引导,稳定)] + [压缩摘要] + [对话历史] + [本轮回插的思考/反思]
+ [历史思考/反思/工具调用(压缩游标后)] + [用户消息] + [动态尾部(每轮变化)]
</div>
<div class="lead reveal" style="margin-top:18px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><strong>固定部分放前缀</strong>:反思引导等不变提示文本并入系统提示词、位于上下文最前 —— 前缀稳定即可命中缓存;避免把固定文本逐轮塞在消息中间导致整段缓存失效。</li>
<li class="li-rv"><strong>历史思考/反思/工具调用注入</strong>(<code>build_tool_history_messages</code>):压缩游标之后的所有轮次,按因果顺序交错注入 <code>[历史思考] → [历史反思] → [历史工具调用]</code>;思考/反思<strong>独立注入</strong>(工具无结果文本时仍保留),尾部未被工具消费的思考/反思去重后追加在轮末。所有消息从磁盘<strong>确定性构建、id 稳定</strong>(<code>think_ctx_{idx}</code> / <code>reflect_ctx_{idx}</code> / <code>tool_ctx_{idx}</code>),只追加在末尾 —— 满足前缀缓存命中条件;压缩节点按 id 精确移除。</li>
<li class="li-rv"><strong>思考与反思分离</strong>:深度思考(<code>reasoning_content</code>)与反思(<code>content</code>)<strong>独立存储</strong>(工具记录的 <code>thinking</code> / <code>reflection</code> 字段 + 轮末 <code>reflections</code> 列表)并独立注入上下文,二者可同时存在,确保注入内容与真实消息一一对应、前缀缓存准确命中。</li>
<li class="li-rv"><strong>本轮回插</strong>(<code>_inject_turn_thinking</code>):带工具调用的 AIMessage 前,将其携带的思考/反思以 <code>[思考]</code> / <code>[反思]</code> 合成消息插回本次调用的上下文(<code>reasoning_content</code> 不被 LLM API 回传),使模型在后续步骤中仍能看到自己的思考链;仅注入 <code>messages_for_llm</code>、不进图状态、id 稳定,不破坏前缀缓存。</li>
<li class="li-rv"><strong>变化部分放末尾</strong>:TodoList 状态、循环警告等每轮变化的内容追加在消息列表<strong>末尾</strong>,变化只影响尾部、不破坏前缀缓存;无 TodoList 且无循环提醒时<strong>不注入任何尾部提示</strong>(省 token)。</li>
<li class="li-rv"><strong>动态尾部作为长期记忆</strong>:每轮尾部文本持久化到 <code>session_meta</code> 的 <code>dynamic_tail_history</code>(连续相同去重),后续轮次把全部历史尾部注入上下文,Agent 能看到之前所有轮的 TodoList 状态与循环提醒。</li>
</ul>
</div>
<!-- 2. 上下文分级压缩 -->
<div class="detail-head reveal">
<span class="detail-num">2</span>
<h3>上下文分级压缩(图内)</h3>
</div>
<p style="text-align:center;margin-bottom:18px" class="reveal">
<span class="img-card" style="display:block;width:min(780px,100%);margin:0 auto" data-path="./git_image/compress.png">
<img src="./git_image/compress.png" alt="多级压缩机制" style="width:100%" loading="lazy"
onerror="this.closest('.img-card').classList.add('img-missing');this.remove()">
<span class="img-path">./git_image/compress.png</span>
</span>
</p>
<div class="line-cnt reveal" data-line="type" data-interval="60">
<pre>每次 LLM 调用后读取 response.usage_metadata.total_tokens
├─ 未超阈值 → 不处理,继续下一轮
├─ 主 Agent 超阈值 → 标记压缩,由 compress 节点统一执行:
│ 将注入的历史工具调用与返回压缩为累积摘要(RemoveMessage 移除已覆盖消息)
└─ 子 Agent 超阈值 → 注入整理提示词,整理进度后直接返回主 Agent(子 Agent 不压缩)</pre>
</div>
<div class="lead reveal" style="margin-top:18px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><strong>触发</strong>:<code>call_model</code> 内每步检查 <code>total_tokens</code>,超过 <code>LLM_CONTEXT_WINDOW × COMPRESS_RATE</code>(默认 0.6)即标记压缩</li>
<li class="li-rv"><strong>执行</strong>:压缩发生在图内 <code>compress</code> 节点(<code>process_tool_artifact</code> 之后),只压缩注入的历史工具上下文,系统提示词与对话历史保留,摘要按压缩游标累积</li>
<li class="li-rv"><strong>子 Agent</strong>:不做压缩,超限时整理任务进度直接返回主 Agent,避免上下文继续膨胀</li>
</ul>
</div>
<!-- 3. 循环检测 -->
<div class="detail-head reveal">
<span class="detail-num">3</span>
<h3>循环检测两级响应</h3>
</div>
<div class="table-scroll reveal-scale">
<table data-row-anim="right">
<thead><tr><th>级别</th><th>触发条件</th><th>响应</th></tr></thead>
<tbody>
<tr><td>⚠️ 警告</td><td>连续 3 次同工具同参</td><td>注入反思提示引导模型自我纠偏</td></tr>
<tr><td>🛑 终止</td><td>连续 5 次同工具同参</td><td><code>should_force_end()</code> 返回 END,向前端输出错误报告</td></tr>
</tbody>
</table>
</div>
<!-- 4. 阻塞式人机交互 -->
<div class="detail-head reveal">
<span class="detail-num">4</span>
<h3>阻塞式人机交互<span class="detail-tag">(交互类 / 展示类工具的统一模式)</span></h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:16px">
人机交互不再「暂停图 + 存快照 + 续跑」,而是<strong>阻塞式普通工具</strong>:工具在图上线程中等待前端应答,答案(同意 / 拒绝 / 跳过 / 补充信息)只是普通工具返回值,图自然继续——<strong>拒绝不会结束整轮</strong>,Agent 自行调整方案。
</p>
<div class="line-cnt reveal" data-line="type" data-interval="55">
<pre>LLM 发出 human_interaction / 需确认工具调用
→ tools 节点执行工具
→ human_request.ask_human() 阻塞等待(注册到请求表,随 live snapshot 推送前端)
→ 前端"通知条"出现(输入区上方,标题 + 数量角标),工具调用行显示"运行中"
→ 用户点击展开 → 选项胶囊 / 补充输入框 → 提交(POST /api/human-action)→ 写入答案并唤醒
→ 工具返回答案文本 → ToolMessage → 图继续(无快照重放、无续跑状态机)</pre>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin:14px 0 4px"><strong>通知条(notify-strip)</strong>:todo 与人机交互不再使用浮窗,改为输入区上方的水平通知条——收起态只显示标题/最新步骤 + 数量角标,点击上下展开完整卡片;通知条左侧按钮可收起为输入区按钮,再次点击恢复;todo 与人机交互的挂起状态<strong>会话级存储</strong>(chat 与 cron 各自独立,切换会话自动保存/恢复)。</p>
<div class="table-scroll reveal-scale" style="margin-top:18px">
<table data-row-anim="alt">
<thead><tr><th>设计点</th><th>传统做法(本项目旧版)</th><th>阻塞式(当前)</th></tr></thead>
<tbody>
<tr><td>图执行</td><td>请求线程内同步 invoke</td><td>后台线程(<code>core/turn_runner.py</code>),断连/关页不影响执行,双发请求挂同一轮</td></tr>
<tr><td>等待答案</td><td>图 END + 消息快照存 <code>PENDING_TOOL_APPROVALS</code></td><td>工具内 <code>threading.Event</code> 阻塞(<code>core/human_request.py</code>)</td></tr>
<tr><td>决策处理</td><td><code>continue_after_human_action</code> 重放快照续跑</td><td>答案即返回值,无续跑路径</td></tr>
<tr><td>拒绝</td><td>结束整轮(等同手动暂停)</td><td>仅返回值,Agent 调整后继续</td></tr>
<tr><td>工具列表</td><td>确认时清空重建、续跑时重放</td><td>自然展示:等待中 <code>running</code> → 应答后 <code>done</code></td></tr>
<tr><td>子 Agent 交互</td><td>异常冒泡(<code>SubAgentPendingError</code>)+ 恢复状态机</td><td>子图线程内直接阻塞,完成自然回主图</td></tr>
<tr><td>无人值守(cron)</td><td>悬死靠超时兜底</td><td><code>ask_human</code> 检测 headless 立即返回默认答案</td></tr>
</tbody>
</table>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin:18px 0 8px"><strong>两类工具的扩展模式</strong>(新增能力零框架改动):</p>
<div class="lead reveal">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><strong>交互类工具</strong>(如 <code>human_interaction</code>):工具内调一次 <code>ask_human(meta)</code> 阻塞,自己格式化返回值</li>
<li class="li-rv"><strong>展示类工具</strong>(如 <code>todo_list</code>):纯文本返回、不维护全局 store,前端从通用 live 记录(工具调用的 args/结果)按工具名写一个小 adapter 渲染通知条(如 <code>js/notify-strip.js</code>)</li>
</ul>
</div>
<!-- 5. GUI 动作序列化 -->
<div class="detail-head reveal">
<span class="detail-num">5</span>
<h3>GUI 动作序列化<span class="detail-tag">(性能关键)</span></h3>
</div>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>模式</th><th>N 个动作的 LLM 调用次数</th></tr></thead>
<tbody>
<tr><td>单步执行(多数项目)</td><td>2N + 1</td></tr>
<tr><td><strong>序列合并(本项目)</strong></td><td><strong>2</strong>(1 次批量定位 + 1 次决策)</td></tr>
</tbody>
</table>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-top:14px">通过系统提示词前缀中的固定反思引导(<code>REFLECTION_PROMPT</code>)+ <code>MAIN_SYSTEM_PROMPT</code> 双重强调「同页操作必须合并为一次 <code>gui</code> 调用」实现。</p>
<!-- 5.1 UI 自动化双轨 -->
<div class="detail-head reveal">
<span class="detail-num">5.1</span>
<h3>UI 自动化双轨<span class="detail-tag">(Playwright + GUI,兼顾容错与效率)</span></h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:12px">UI 操作按目标介质分两条轨道,由系统提示词引导 Agent 按「能 DOM 不截图」的原则选择:</p>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>轨道</th><th>工具</th><th>机制</th><th>适用场景</th><th>特点</th></tr></thead>
<tbody>
<tr><td><strong>浏览器轨</strong></td><td><code>browser_control</code> + <code>web_page</code></td><td>Playwright 会话管理器(CDP 分离进程,Agent 退出后浏览器保留、重启自动重连)+ DOM 级操作</td><td>浏览器内的网页操作</td><td><strong>毫秒级、定位可靠</strong>(snapshot 元素清单 → ref/selector 精准操作,不依赖视觉模型)</td></tr>
<tr><td><strong>桌面轨</strong></td><td><code>gui</code></td><td>截图 + 视觉批量定位(<code>_batch_ground_elements</code>)+ 键鼠模拟</td><td>桌面应用 / 任意界面(含浏览器无 DOM 场景)</td><td><strong>通用容错</strong>:截图视觉闭环,多模态 LLM 看到真实屏幕状态,可处理任何界面</td></tr>
</tbody>
</table>
</div>
<div class="lead reveal" style="margin-top:14px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><strong>典型流程</strong>:<code>browser_control</code> 打开 URL → <code>web_page</code> snapshot 获取元素清单 → 按 ref/selector 批量 click/fill/scroll → 页面跳转后重新 snapshot;浏览器不适用(如系统对话框、桌面软件)时回退 <code>gui</code> 视觉定位</li>
<li class="li-rv"><strong>两轨均遵循</strong>「连续操作合并为一次调用」(actions 列表批量传入)的性能原则</li>
</ul>
</div>
<!-- 6. 双模式源码共用 -->
<div class="detail-head reveal">
<span class="detail-num">6</span>
<h3>双模式源码共用</h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:12px"><code>src/web/</code> 同一套代码两种部署:</p>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th></th><th>Web 模式</th><th>桌面模式</th></tr></thead>
<tbody>
<tr><td>启动</td><td><code>uvicorn web.server:app</code></td><td>Electron <code>spawn</code> uvicorn</td></tr>
<tr><td>文件操作</td><td><code>/api/fs/*</code> HTTP</td><td><code>electron-api.js</code> → IPC 直读磁盘</td></tr>
<tr><td>对话框</td><td>浏览器原生</td><td>Electron <code>dialog</code> 模块</td></tr>
<tr><td>加载协议</td><td><code>http://</code></td><td><code>file://</code> + IPC</td></tr>
</tbody>
</table>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-top:12px">前端通过 <code>window.electronAPI?.isElectron</code> 自动适配。</p>
<!-- 7. 存储多后端 -->
<div class="detail-head reveal">
<span class="detail-num">7</span>
<h3>存储多后端<span class="detail-tag">(JSON / MySQL / SQLite,统一接口隔离)</span></h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:16px">
会话历史与定时任务存储统一在 <strong><code>agent/core/storage.py</code></strong>:接口(Protocol)驱动、多后端实现、<strong>消费方零感知后端选择</strong>(不判断 <code>STORAGE_BACKEND</code>)。
</p>
<p class="reveal-up" style="color:var(--ink);font-weight:800">接口与实现</p>
<div class="lead reveal" style="margin-top:8px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><code>ConversationRecordRepository</code>(对话消息 / 工具调用 / 轮次元数据 / 会话生命周期,16 方法)与 <code>TaskConfigRepository</code>(cron 任务配置,6 方法)两个 Protocol</li>
<li class="li-rv">每后端一个实现类:<code>JsonRecordRepository</code> / <code>MysqlRecordRepository</code> / <code>SqliteRecordRepository</code>(含会话与 cron 双作用域)、<code>JsonTaskConfigRepository</code> / <code>MysqlTaskConfigRepository</code> / <code>SqliteTaskConfigRepository</code></li>
<li class="li-rv"><strong>新增存储后端只需实现上述接口并在工厂注册一行</strong>;消费方新增功能/字段无需改动存储层(消息原样 JSON 存储、工具记录 meta 全量透传)</li>
</ul>
</div>
<p class="reveal-up" style="color:var(--ink);font-weight:800;margin-top:22px">作用域(scope)</p>
<div class="lead reveal" style="margin-top:8px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><code>session</code>(默认):主进程普通会话,JSON 落盘 <code>history/sessions/{session_id}/</code>,MySQL/SQLite 用 <code>sessions / session_messages / session_tool_calls</code> 三表</li>
<li class="li-rv"><code>cron</code>:定时任务,JSON 落盘 <code>history/cron/{task_id}/</code>,MySQL/SQLite 用 <code>cron_messages / cron_tool_calls</code> 两表;cron 子进程切换存储作用域后<strong>直写</strong>记录,主进程经 cron 作用域仓库读取</li>
</ul>
</div>
<div class="table-scroll reveal-scale" style="margin-top:18px">
<table data-row-anim="alt">
<thead><tr><th>后端</th><th>存储位置</th><th>说明</th></tr></thead>
<tbody>
<tr><td><strong>json</strong>(默认)</td><td><code>history/sessions/{session_id}/</code></td><td>消息与工具调用以人类可读 JSON 文件落盘(<code>turn_{turn_id}.json</code> / <code>tool_{turn_id}.json</code> / <code>session_meta.json</code>),可直接打开查看/修改,便于审计与调试</td></tr>
<tr><td><strong>mysql</strong></td><td>session 三表 / cron 两表</td><td>消息与工具调用记录原样 JSON 入库、读取只走数据库;二进制产物(图片/附件等)所有后端均落盘</td></tr>
<tr><td><strong>sqlite</strong></td><td><code>history/sqlite/storage.sqlite</code>(单文件,session+cron 共用)</td><td>内置 <code>sqlite3</code> 零依赖,JSON 存 TEXT 列;<code>cron_tool_calls</code> 含 meta 列,尾部思考可完整落库;WAL 模式支持多进程并发</td></tr>
</tbody>
</table>
</div>
<div class="lead reveal" style="margin-top:18px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv">由 <code>STORAGE_BACKEND</code>(json / mysql / sqlite)+ 存储数据库配置驱动;<strong>存储数据库与工具数据库完全隔离</strong>:存储用 <code>STORAGE_DB_*</code>(缺省回退旧 <code>MYSQL_*</code>,零迁移),数据库操作工具(text2sql / excel2sql)用 <code>TOOL_DB_*</code>(<strong>不回退存储配置</strong>);SQLite 工具库目录模式下每个库名 = 一个 <code>.sqlite</code> 文件</li>
<li class="li-rv">两类数据库均在设置 → 存储与数据库页配置(类型可选择 MySQL / SQLite);<code>agent/core/db.py</code> 提供统一连接(<code>connect_db</code>)与幂等建库建表(<code>ensure_database</code> / <code>ensure_sqlite_database</code> —— 库不存在时先建库再按表结构建表)</li>
<li class="li-rv"><strong>不双写</strong>:选哪种后端,读写就只走该后端;写入幂等(同一 <code>(session_id, turn_id)</code> 先 DELETE 再 INSERT);DB 异常仅打印日志,不阻断主流程</li>
<li class="li-rv">分支(<code>/api/sessions/branch</code>)与回滚等会话功能均基于存储接口实现,各后端一致可用</li>
</ul>
</div>
<p class="reveal-up" style="color:var(--ink);font-weight:800;margin:22px 0 4px">后端行为差异(设计内)</p>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>场景</th><th>json</th><th>mysql / sqlite</th></tr></thead>
<tbody>
<tr><td>轮次中实时工具记录</td><td>每次工具结果后<strong>增量写盘</strong> <code>tool_*.json</code>,进程崩溃后重启可回退恢复最近记录</td><td>不增量入库,由轮次结束 <code>save_tool_calls</code> 统一落库(无额外磁盘 IO,但崩溃时该轮记录丢失)</td></tr>
<tr><td>服务重启后运行中轮次</td><td><code>get_live_tool_calls</code> 回退读取增量文件,可继续展示</td><td>内存记录丢失、数据库无该轮记录,实时面板为空</td></tr>
<tr><td>cron 历史读取</td><td>直接读 <code>history/cron/</code> 下文件</td><td>子进程直写数据库表,历史可正常读取</td></tr>
</tbody>
</table>
</div>
<!-- 8. 前后端单文件插件化 -->
<div class="detail-head reveal">
<span class="detail-num">8</span>
<h3>前后端单文件插件化<span class="detail-tag">(协议驱动,非注册式)</span></h3>
</div>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:14px"><strong>核心思想</strong>:一个插件 = 后端一个文件 + 前端一个文件(或 adapter),通过<strong>协议</strong>接入;框架与插件之间靠<strong>事件 / 回调监听</strong>通信,而不是集中式注册表 + 修改主流程——新增或替换插件只需保持协议签名,<strong>宿主装配零改动</strong>。</p>
<p class="reveal-up" style="color:var(--ink);font-weight:800">后端:<code>BaseTool</code> 协议(<code>src/agent/tools/*.py</code>)</p>
<div class="lead reveal" style="margin-top:8px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv">每个工具是独立单文件,继承 LangChain <code>BaseTool</code>,实现 <code>name</code> / <code>description</code> / <code>args_schema</code> / <code>_run</code> 四要素即完成协议;<code>tools/__init__.py</code> 注册表一行挂载</li>
<li class="li-rv"><strong>前端零改动</strong>:工具配置面板(工具列表、参数表单、权限/自动执行开关、main 默认挂载)全部由 <code>/api/config/tools</code> 的 <code>registered_tool_names</code> / <code>tool_parameters</code> / <code>forced_permissions</code> <strong>自动生成</strong>——新增工具自动出现在设置面板与 Agent 工具选择器</li>
<li class="li-rv">展示类工具(如 <code>todo_list</code>)只需保持「纯文本返回」协议,前端按工具名写一个小 adapter 即可渲染通知条</li>
</ul>
</div>
<p class="reveal-up" style="color:var(--ink);font-weight:800;margin-top:20px">前端:<code>setXxxDeps / setXxxFn</code> 依赖注入协议(<code>src/web/js/*.js</code>)</p>
<div class="lead reveal" style="margin-top:8px">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv">每个功能模块独立单文件,只导出 setter(协议接口)与自身功能函数,<strong>模块间零直接引用</strong>;宿主 <code>app.js</code> 作为装配中心做一次性连线(依赖注入):<code>setSendDeps({...})</code> / <code>setLiveUiDeps({...})</code> / <code>setRenderPersistentToolCalls(fn)</code> / <code>setRenderSkills(fn)</code></li>
<li class="li-rv"><strong>事件监听式数据流</strong>:<code>toolcalls.js</code> 收到 live snapshot(SSE/轮询)后,通过 <code>setLiveUiDeps</code> 注入的回调分发给各展示组件——组件是被回调通知(监听事件),而非向全局注册表登记</li>
<li class="li-rv"><strong>替换/新增模块零宿主改动</strong>:旧模块(如 <code>pending-overlay.js</code>)被新模块(<code>notify-strip.js</code>)替换时,只需保持协议导出签名(<code>setLiveUiDeps</code> / <code>updateTodoOverlayFromRecords</code> 等),宿主连线无需修改</li>
</ul>
</div>
</section>
<hr class="section-divider">
<!-- ===================== LLM 推理服务 ===================== -->
<section class="section" id="llm" aria-label="LLM 推理服务">
<div class="section-head">
<span class="sec-emoji">🔌</span>
<h2>LLM 推理服务</h2>
<p class="sec-sub">项目自带一套 6 服务推理后端(<code>llm_server/</code>),可独立部署在 GPU 服务器上,为 Agent 提供 LLM / ASR / TTS / RAG / 图像生成能力。</p>
</div>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>服务</th><th>端口</th><th>模型</th><th>脚本</th></tr></thead>
<tbody>
<tr><td>LLM</td><td>8900</td><td>Qwen3.6-35B-A3B-FP8</td><td><code>start_llm.sh</code></td></tr>
<tr><td>ASR</td><td>8901</td><td>Qwen3-ASR-1.7B</td><td><code>start_asr.sh</code></td></tr>
<tr><td>TTS</td><td>8902</td><td>VoxCPM</td><td><code>start_streaming_tts.sh</code></td></tr>
<tr><td>RAG</td><td>8903</td><td>Qwen3-Embedding-0.6B + Reranker-4B</td><td><code>start_rag_server.sh</code></td></tr>
<tr><td>Image Gen</td><td>8904</td><td>Z-Image-Turbo</td><td><code>start_image_gen.sh</code></td></tr>
</tbody>
</table>
</div>
<blockquote class="quote reveal">📄 完整部署、模型下载、测试方法见 <a href="./llm_server/README.md"><strong>llm_server/README.md</strong></a>。</blockquote>
<blockquote class="quote reveal">💡 也可不自建推理服务,直接在配置向导填入任何 OpenAI 兼容 API(如云端 Qwen / DeepSeek / OpenAI)。</blockquote>
</section>
<hr class="section-divider">
<!-- ===================== 开发手册 ===================== -->
<section class="section" id="dev-guide" aria-label="开发手册">
<div class="section-head">
<span class="sec-emoji">🛠️</span>
<h2>开发手册</h2>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin-bottom:16px">前端模块职责(<code>src/web/js/</code>)</h3>
<div class="table-scroll reveal-scale">
<table data-row-anim="alt">
<thead><tr><th>模块</th><th>职责</th></tr></thead>
<tbody>
<tr><td><code>app.js</code></td><td>应用入口,初始化与事件绑定</td></tr>
<tr><td><code>api.js</code></td><td>封装所有后端 API 请求</td></tr>
<tr><td><code>state.js</code></td><td>全局状态管理</td></tr>
<tr><td><code>send.js</code></td><td>消息发送逻辑</td></tr>
<tr><td><code>chat-render.js</code></td><td>聊天消息渲染(双容器:chat / cron 各自独立,运行时增量渲染、空闲全量重渲染)</td></tr>
<tr><td><code>cron.js</code></td><td>定时任务面板(按工作区分组列表、类 chat 会话式历史、模式切换锁、实时运行)</td></tr>
<tr><td><code>sessions.js</code></td><td>会话列表管理(工作区分组,切换会话恢复会话级通知条状态)</td></tr>
<tr><td><code>toolcalls.js</code></td><td>工具调用实时面板(思考 Thought / 反思 Reflection 分离显示,rAF 合并高频快照)</td></tr>
<tr><td><code>notify-strip.js</code></td><td>★ 通知条系统:todo 与人机交互的输入区上方长条(收起/展开、按钮切换、会话级存储)</td></tr>
<tr><td><code>edit-mode.js</code></td><td>编辑模式(Monaco)</td></tr>
<tr><td><code>doc-tree.js</code> / <code>doc-mod-panel.js</code></td><td>文档树与修改面板</td></tr>
<tr><td><code>action-bar.js</code></td><td>底部操作栏</td></tr>
<tr><td><code>skills.js</code></td><td>技能管理</td></tr>
<tr><td><code>settings.js</code></td><td>设置面板</td></tr>
<tr><td><code>themes.js</code></td><td>主题切换</td></tr>
<tr><td><code>layout.js</code></td><td>布局与分隔条拖拽</td></tr>
<tr><td><code>electron-api.js</code></td><td>★ Electron IPC 适配层</td></tr>
<tr><td><code>file-preview.js</code></td><td>文件预览(图片/HTML/PDF)</td></tr>
<tr><td><code>canvas-editor.js</code></td><td>画板编辑器(画笔/形状/文字/橡皮擦,支持缩放与图片修改)</td></tr>
<tr><td><code>audio.js</code></td><td>语音录制与播放</td></tr>
<tr><td><code>i18n.js</code></td><td>国际化</td></tr>
<tr><td><code>token-ring.js</code></td><td>上下文 Token 用量环形指示器(含压缩阈值刻度与三类占比)</td></tr>
<tr><td><code>access-mode.js</code></td><td>工作空间访问模式切换(限制访问 / 权限审查 / 完全访问)</td></tr>
<tr><td><code>think-level.js</code></td><td>思考模式档位选择(low / high / xhigh / max / ultra)</td></tr>
<tr><td><code>pending.js</code></td><td>人工请求决策提交(数据源为 live snapshot 的 <code>human_requests</code>,配合 notify-strip 渲染)</td></tr>
<tr><td><code>rollback.js</code></td><td>消息回滚</td></tr>
<tr><td><code>workspace.js</code></td><td>工作空间切换</td></tr>
<tr><td><code>dialog.js</code></td><td>对话框</td></tr>
<tr><td><code>utils.js</code></td><td>通用工具</td></tr>
</tbody>
</table>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">CSS 架构(<code>src/web/css/</code>)</h3>
<p class="reveal-up" style="color:var(--ink-soft)"><code>variables.css</code> 定义主题变量,其余按模块拆分:<code>base / layout / chat / composer / cron / dialog / settings / skills / toolcalls / notify-strip / edit-mode / canvas-editor / audio / animations / toast / responsive</code>。</p>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">添加自定义工具</h3>
<div class="lead reveal">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv">在 <code>src/agent/tools/</code> 新建 <code>my_tool.py</code>,继承 LangChain <code>BaseTool</code>,实现 <code>_run</code>,填写 <code>name</code> / <code>description</code> / <code>args_schema</code>(协议四要素)</li>
<li class="li-rv">在 <code>config.json</code> 的 <code>tools</code> 分区注册并配置启用状态与参数(权限设为 <code>confirm</code> 时,工具执行前会自动弹出确认通知条,同意后执行、拒绝/跳过作为返回值继续);<strong>前端无需改动</strong>——设置面板工具列表 / 参数表单 / Agent 工具选择器由 API 自动生成</li>
<li class="li-rv">交互类工具(需要向用户提问)在 <code>_run</code> 中调用 <code>core/human_request.ask_human(meta)</code> 阻塞等待答案;展示类工具(如进度面板)返回纯文本即可,前端从 live 工具记录按工具名写 adapter 渲染通知条</li>
<li class="li-rv">重启后端,工具自动绑定到 LLM;若希望 main Agent 默认挂载,加入 <code>config_manager.MAIN_DEFAULT_TOOLS</code></li>
</ul>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">添加自定义技能</h3>
<div class="lead reveal">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv">在 <code>src/agent/skills/</code> 新建 <code>my_skill/</code> 目录</li>
<li class="li-rv">编写 <code>skill.json</code>(名称、描述、标签)与 <code>skill.md</code>(操作流程,支持 <code>{SKILL_DIR}</code> 占位)</li>
<li class="li-rv">可放 <code>attachments/</code> 附件(模板、脚本)</li>
<li class="li-rv"><code>skill_router</code> 会自动匹配并路由</li>
</ul>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 12px">同步 Web ↔ 桌面端</h3>
<p class="reveal-up" style="color:var(--ink-soft);margin-bottom:16px">桌面端与 Web 端共用 <code>src/web/</code>。UI 改动无需同步;仅当分叉修改时手动合并。常用同步命令:</p>
<div class="term reveal">
<div class="term-bar"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span><span class="term-title">PowerShell</span></div>
<pre class="term-body"><span class="cmt"># 桌面端 → Web 端(按需)</span>
Copy-Item "src\web\index.html" "src\web\index.html" -Force
Copy-Item "src\web\css\*.css" "src\web\css\" -Force
Copy-Item "src\web\js\themes.js" "src\web\js\themes.js" -Force</pre>
</div>
<h3 class="reveal" style="font-size:1.4rem;margin:44px 0 16px">调试技巧</h3>
<div class="lead reveal">
<ul data-reveal-list="left" data-stagger="40">
<li class="li-rv"><strong>后端日志</strong>:桌面端 <code>npm start</code> 会在终端打印 <code>[FastAPI]</code> 日志</li>
<li class="li-rv"><strong>端口占用</strong>:<code>main.js</code> 的 <code>killPortProcess(8765)</code> 启动前自动清理</li>
<li class="li-rv"><strong>重置配置</strong>:删除 <code>src/agent/agents/config.json</code> 重触首次向导(或复制 <code>config_dist.json</code> 为新配置)</li>
<li class="li-rv"><strong>工具调用轨迹</strong>:<code>history/sessions/{session_id}/tool_{turn_id}.json</code></li>
</ul>
</div>
</section>
<hr class="section-divider">
<!-- ===================== 注意事项 ===================== -->
<section class="section" id="notes" aria-label="注意事项">
<div class="section-head">
<span class="sec-emoji">⚠️</span>
<h2>注意事项</h2>
</div>
<div class="table-scroll reveal-scale">
<table data-row-anim="up">
<thead><tr><th>事项</th><th>说明</th></tr></thead>
<tbody>
<tr><td>🖥️ <strong>平台</strong></td><td>桌面自动化工具(<code>gui</code> / <code>software_control</code>)依赖 Win32 / pywin32 / pyautogui,<strong>目前仅支持 Windows</strong>;Web 模式下这些工具不可用</td></tr>
<tr><td>🐍 <strong>Python 版本</strong></td><td>建议 Python 3.11 / 3.12 / 3.13;首次向导会自动检测</td></tr>
<tr><td>🔐 <strong>未签名安装包</strong></td><td><code>.exe</code> 可能被 Windows Defender 拦截,点「更多信息 → 仍要运行」</td></tr>
<tr><td>🌐 <strong>首次启动联网</strong></td><td>加载 Google Fonts 需联网;离线环境字体选择器有 HTML 硬编码兜底</td></tr>
<tr><td>🔑 <strong>API Key 安全</strong></td><td><code>src/agent/agents/config.json</code> 含明文凭证,<strong>勿提交到公开仓库</strong>(已在 <code>.gitignore</code> 排除,仅保留 <code>config_dist.json</code> 模板)</td></tr>
<tr><td>🎨 <strong>图标</strong></td><td>当前为 SVG,NSIS 打包后若显示默认图标,需准备 <code>.ico</code> 文件替换 <code>src/web/image/icon.ico</code></td></tr>
<tr><td>💾 <strong>历史存储</strong></td><td>默认会话历史与工具轨迹以 JSON 落盘在 <code>history/</code>(可切换 MySQL 后端),长期使用注意清理</td></tr>
<tr><td>⚡ <strong>GUI 性能</strong></td><td>同页多操作务必合并为一次 <code>gui</code> 调用,否则延迟显著(见「实现细节」)</td></tr>
</tbody>
</table>
</div>
</section>
<hr class="section-divider">
<!-- ===================== Contributors ===================== -->
<section class="section" id="contributors" aria-label="Contributors">
<div class="section-head">
<span class="sec-emoji">👥</span>
<h2>Contributors</h2>
<p class="sec-sub">新增贡献者请按此格式在下方追加一行:<code>- [用户名](GitHub 个人链接):贡献内容</code></p>
</div>
<div class="contrib-card reveal-pop" style="max-width:780px;margin:0 auto">
<div class="contrib-avatar">m</div>
<div>
<h3><a href="https://github.com/minorQAQ" target="_blank" rel="noopener">minor</a></h3>
<p class="contrib-role">项目话事人</p>
<p style="color:var(--ink-soft)">主导整体架构设计与核心实现(LangGraph 编排、20 项工具体系、多 Agent 模式、上下文分级压缩、循环检测、Electron 桌面端等),完成大部分的开发工作。</p>
</div>
</div>
</section>
</main>
<!-- ===================== Footer ===================== -->
<footer id="license" class="reveal">
<div class="footer-line" aria-hidden="true"></div>
<div class="footer-star">🍃</div>
<p class="footer-license">📄 License</p>
<p style="margin-bottom:10px">MIT License — 详见 <a href="./LICENSE" target="_blank" rel="noopener">LICENSE</a>。</p>
<p class="footer-meta">如果这个项目对你有帮助,欢迎 ⭐ Star 支持一下!</p>
</footer>
<script>
(function(){
'use strict';
var reduced = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var hasIO = 'IntersectionObserver' in window;
/* ---------- 工具 ---------- */
function $(s, c){ return (c || document).querySelector(s); }
function $$(s, c){ return Array.prototype.slice.call((c || document).querySelectorAll(s)); }
function escapeHtml(s){
return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
/* ---------- 自动 stagger:为 [data-stagger] 的直接子元素设置 --d ---------- */
$$('[data-stagger]').forEach(function(cont){
var base = parseFloat(cont.getAttribute('data-stagger')) || 50;
Array.prototype.forEach.call(cont.children, function(el, i){
el.style.setProperty('--d', Math.round(base * i) + 'ms');
});
});
/* ---------- 列表项动画:为 [data-reveal-list] 的子项打上 li-rv 类 ---------- */
$$('[data-reveal-list]').forEach(function(list){
var mode = list.getAttribute('data-reveal-list') || 'left';
var base = parseFloat(list.getAttribute('data-stagger')) || 40;
Array.prototype.forEach.call(list.children, function(item, i){
item.classList.add('li-rv', 'li-rv-' + mode);
item.style.setProperty('--d', Math.round(base * i) + 'ms');
});
});
/* ---------- 表格行动画 ---------- */
$$('table[data-row-anim]').forEach(function(t){
var mode = t.getAttribute('data-row-anim');
$$('tbody tr', t).forEach(function(tr, i){
tr.classList.add('trow');
if (mode === 'alt-col') tr.classList.add('trow-alt-col');
else tr.classList.add('trow-rv-' + mode);
tr.style.setProperty('--d', Math.round(i * 50) + 'ms');
});
});
/* ---------- 逐行显现(打字机) ---------- */
function initLineReveal(pre, interval){
var lines = pre.textContent.split('\n').map(function(l){ return l.replace(/[ \t]+$/, ''); });
pre.innerHTML = '';
lines.forEach(function(l){
var d = document.createElement('div');
d.className = 'tline';
d.textContent = l;
pre.appendChild(d);
});
var all = pre.children;
var i = 0;
(function step(){
if (i >= all.length) return;
all[i].classList.add('vis');
i++;
setTimeout(step, interval);
})();
}
/* ---------- 项目结构树:逐层生长 + 着色 ---------- */
function treeDepth(line){
var d = 0, s = line;
for(;;){
if (s.indexOf('│ ') === 0 || s.indexOf(' ') === 0){ s = s.slice(4); d++; }
else if (s.indexOf('├── ') === 0 || s.indexOf('└── ') === 0){ d++; break; }
else break;
}
return d;
}
function initTree(pre){
var lines = pre.textContent.split('\n');
pre.innerHTML = '';
var els = [];
lines.forEach(function(raw){
var line = raw.replace(/[ \t]+$/, '');
var d = document.createElement('div');
d.className = 'tl';
if (!line.trim()){ pre.appendChild(d); els.push(d); return; }
var hIdx = line.indexOf('#');
var codeFull = hIdx >= 0 ? line.slice(0, hIdx) : line;
var cmt = hIdx >= 0 ? line.slice(hIdx) : '';
var m = codeFull.match(/([^\s]+)(\s*)$/);
if (m && !/^[│├└─]*$/.test(m[1]) && (m[1].indexOf('/') >= 0 || m[1].indexOf('.') >= 0)){
var prefix = codeFull.slice(0, m.index);
var cls = m[1].charAt(m[1].length - 1) === '/' ? 'tdir' : 'tfile';
d.innerHTML = escapeHtml(prefix) + '<span class="' + cls + '">' + escapeHtml(m[1]) + '</span>' + escapeHtml(m[2]) + (cmt ? '<span class="tcmt">' + escapeHtml(cmt) + '</span>' : '');
} else {
d.innerHTML = escapeHtml(codeFull) + (cmt ? '<span class="tcmt">' + escapeHtml(cmt) + '</span>' : '');
}
d.setAttribute('data-depth', treeDepth(codeFull));
pre.appendChild(d);
els.push(d);
});
/* 按深度分层显现(覆盖任意深度,最深 4 级:config/ 下的文件等) */
var byDepth = {};
els.forEach(function(el){
var dep = el.getAttribute('data-depth') || '0';
(byDepth[dep] = byDepth[dep] || []).push(el);
});
var t = 0;
Object.keys(byDepth).map(Number).sort(function(a, b){ return a - b; }).forEach(function(dep){
(byDepth[String(dep)] || []).forEach(function(el, i){
el.style.transitionDelay = (t + i * 22) + 'ms';
el.classList.add('vis');
});
t += 300;
});
}
/* ---------- 动态 threshold 的 IntersectionObserver ---------- */
function clamp(v, a, b){ return Math.min(b, Math.max(a, v)); }
/* 收集需要观察的元素 */
var observed = $$('.reveal, .reveal-left, .reveal-right, .reveal-scale, .reveal-rotate, .reveal-up, .reveal-pop, .observe, .arch-card, .line-cnt, .term');
observed = observed.concat($$('.trow'));
observed = observed.concat($$('[data-reveal-list]'));
/* 去重 */
observed = observed.filter(function(el, idx){ return observed.indexOf(el) === idx; });
function revealChildren(cont){
if (cont.hasAttribute('data-reveal-list')){
Array.prototype.forEach.call(cont.children, function(c){ c.classList.add('visible'); });
}
}
if (hasIO && !reduced){
var buckets = {};
observed.forEach(function(el){
var h = el.getBoundingClientRect().height || 400;
var t = clamp(Math.round(120 / h * 100) / 100, 0.1, 0.3);
var key = Math.round(t * 100);
(buckets[key] = buckets[key] || []).push(el);
});
Object.keys(buckets).forEach(function(key){
var threshold = parseInt(key, 10) / 100;
var io = new IntersectionObserver(function(entries){
entries.forEach(function(entry){
if (!entry.isIntersecting) return;
var t = entry.target;
t.classList.add('visible');
revealChildren(t);
if (t.classList.contains('line-cnt')){
var pre = $('pre', t);
var mode = t.getAttribute('data-line');
var interval = parseInt(t.getAttribute('data-interval') || '60', 10);
if (mode === 'tree') initTree(pre);
else initLineReveal(pre, interval);
}
io.unobserve(t);
});
}, { threshold: threshold, rootMargin: '0px 0px -8% 0px' });
buckets[key].forEach(function(el){ io.observe(el); });
});
} else {
/* 无 IO 或用户偏好减少动效:直接全部显示 */
observed.forEach(function(el){
el.classList.add('visible');
revealChildren(el);
});
$$('.line-cnt').forEach(function(t){
var pre = $('pre', t);
if (t.getAttribute('data-line') === 'tree') initTree(pre);
else initLineReveal(pre, 0);
});
}
/* ---------- 滚动:进度条 / 导航隐藏 / Hero 视差 ---------- */
var progress = $('#progress');
var nav = $('#nav');
var hero = $('#hero');
var heroInner = $('.hero-inner', hero);
var ticking = false;
var lastY = window.pageYOffset;
function onScroll(){
var y = window.pageYOffset || document.documentElement.scrollTop;
var doc = document.documentElement;
var max = doc.scrollHeight - window.innerHeight;
var pct = max > 0 ? y / max : 0;
progress.style.transform = 'scaleX(' + pct + ')';
/* Hero 0.8 倍速视差 + 淡出 */
if (heroInner && y < hero.offsetHeight){
heroInner.style.transform = 'translate3d(0,' + Math.round(y * 0.8) + 'px,0)';
var p = clamp(y / (hero.offsetHeight * 0.9), 0, 1);
heroInner.style.opacity = String(1 - p);
heroInner.style.filter = 'blur(' + (p * 3).toFixed(1) + 'px)';
}
/* 导航:向下隐藏 / 向上浮现 */
if (!reduced){
if (y > lastY && y > 140 && !nav.classList.contains('nav-hidden')){
nav.classList.add('nav-hidden');
} else if (y < lastY && nav.classList.contains('nav-hidden')){
nav.classList.remove('nav-hidden');
}
}
lastY = y;
ticking = false;
}
window.addEventListener('scroll', function(){
if (!ticking){
requestAnimationFrame(onScroll);
ticking = true;
}
}, { passive: true });
onScroll();
/* ---------- Hero 鼠标视差(装饰光晕) ---------- */
if (hero && !reduced && window.matchMedia('(pointer:fine)').matches){
var blobs = $$('.hero-blob', hero);
hero.addEventListener('mousemove', function(e){
var r = hero.getBoundingClientRect();
var cx = (e.clientX - r.left) / r.width - 0.5;
var cy = (e.clientY - r.top) / r.height - 0.5;
blobs.forEach(function(b, i){
var f = (i % 2 ? 34 : -28) * (i + 1) * 0.4;
b.style.transform = 'translate(' + (cx * f).toFixed(1) + 'px,' + (cy * f * 0.7).toFixed(1) + 'px)';
});
});
}
})();
</script>
</body>
</html>