<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Codebuddy Web Console · 设计说明</title>
<style>
+ :root {
+ --ink: #1f2328;
+ --ink-soft: #444b52;
+ --muted: #6b7280;
+ --line: #d8dde3;
+ --line-soft: #eceff2;
+ --paper: #ffffff;
+ --bg: #f4f5f7;
+ --accent: #2f5d8a;
+ --accent-soft: #eef2f7;
+ --code-bg: #f3f4f6;
+ }
* { margin: 0; padding: 0; box-sizing: border-box; }
- body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
- color: #222; line-height: 1.7; background: #f5f6fa; padding: 32px 16px; }
- .wrap { max-width: 920px; margin: 0 auto; background: #fff; border-radius: 14px; padding: 36px 40px; box-shadow: 0 4px 24px rgba(0,0,0,0.06); }
- header { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; border-bottom: 2px solid #4ecca3; padding-bottom: 16px; margin-bottom: 8px; }
- h1 { font-size: 24px; color: #1a1a2e; }
- .sub { color: #888; font-size: 13px; margin-top: 4px; }
- .btn-pdf { flex-shrink: 0; background: #4ecca3; color: #06281f; border: none; border-radius: 8px; padding: 8px 14px; cursor: pointer; font-size: 13px; font-weight: 600; }
- .btn-pdf:hover { background: #6fd9b4; }
- h2 { font-size: 19px; color: #1a1a2e; margin: 28px 0 10px; padding-left: 10px; border-left: 4px solid #4ecca3; }
- h3 { font-size: 16px; color: #157a5e; margin: 18px 0 6px; }
- p { margin: 6px 0; color: #333; }
- ul, ol { margin: 6px 0 6px 22px; }
- li { margin: 4px 0; }
- code { background: #eef6f2; color: #157a5e; padding: 1px 6px; border-radius: 5px; font-size: 13px; font-family: "Cascadia Code", Menlo, Consolas, monospace; }
- .tag { display: inline-block; background: #1a1a2e; color: #4ecca3; border-radius: 6px; padding: 2px 8px; font-size: 12px; margin: 2px 4px 2px 0; }
- table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 14px; }
- th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid #e3e6ee; vertical-align: top; }
- th { background: #f0f3f8; color: #1a1a2e; }
- .note { background: #fff8e6; border: 1px solid #ffe08a; border-radius: 8px; padding: 10px 14px; margin: 10px 0; font-size: 13px; color: #6b5a1f; }
- .page { background: #f0f3ff; border: 1px solid #c9d4ff; border-radius: 8px; padding: 12px 16px; margin: 10px 0; }
- footer { margin-top: 32px; padding-top: 14px; border-top: 1px solid #eee; color: #aaa; font-size: 12px; text-align: center; }
- a { color: #157a5e; }
+ html { scroll-behavior: smooth; }
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
+ color: var(--ink); background: var(--bg); line-height: 1.75; font-size: 15px;
+ }
+
+ /* ---------- 布局:左侧目录 + 右侧正文 ---------- */
+ .layout { display: flex; align-items: flex-start; max-width: 1180px; margin: 0 auto; }
+
+ .toc {
+ position: sticky; top: 0; align-self: flex-start;
+ width: 260px; flex-shrink: 0; height: 100vh; overflow-y: auto;
+ padding: 28px 18px 40px; border-right: 1px solid var(--line);
+ background: #fbfcfd;
+ }
+ .toc-title {
+ font-size: 12px; letter-spacing: .12em; text-transform: uppercase;
+ color: var(--muted); margin-bottom: 12px; font-weight: 600;
+ }
+ .toc ol { list-style: none; counter-reset: toc; }
+ .toc > ol > li { margin: 2px 0; }
+ .toc a {
+ display: block; padding: 6px 10px; border-radius: 6px;
+ color: var(--ink-soft); text-decoration: none; font-size: 13.5px;
+ border-left: 2px solid transparent;
+ }
+ .toc a:hover { background: var(--accent-soft); color: var(--accent); }
+ .toc a.active { background: var(--accent-soft); color: var(--accent); border-left-color: var(--accent); font-weight: 600; }
+ .toc .sub { padding-left: 14px; }
+ .toc .sub a { font-size: 12.5px; color: var(--muted); }
+
+ .content { flex: 1; min-width: 0; background: var(--paper); border: 1px solid var(--line); border-top: none; border-bottom: none; }
+ .doc { padding: 40px 52px 64px; }
+
+ /* ---------- 顶部条 ---------- */
+ .topbar {
+ display: flex; justify-content: space-between; align-items: flex-start; gap: 16px;
+ padding: 22px 52px; border-bottom: 1px solid var(--line); background: var(--paper);
+ }
+ .topbar h1 { font-size: 22px; font-weight: 700; letter-spacing: .01em; }
+ .topbar .sub { color: var(--muted); font-size: 13px; margin-top: 6px; }
+ .btn-pdf, .btn-md {
+ flex-shrink: 0; background: #fff; color: var(--accent); border: 1px solid var(--accent);
+ border-radius: 6px; padding: 7px 14px; cursor: pointer; font-size: 13px; font-weight: 600;
+ }
+ .btn-pdf:hover, .btn-md:hover { background: var(--accent-soft); }
+ .btn-md { margin-right: 8px; }
+
+ /* ---------- 排版 ---------- */
+ h2 {
+ font-size: 19px; font-weight: 700; color: var(--ink);
+ margin: 38px 0 14px; padding-bottom: 8px; border-bottom: 2px solid var(--line);
+ counter-increment: sec;
+ }
+ h2::before { content: counter(sec) ". "; color: var(--accent); }
+ h3 { font-size: 15.5px; font-weight: 700; color: var(--ink); margin: 22px 0 8px; }
+ h3::before { content: "§ "; color: var(--muted); font-weight: 400; }
+ p { margin: 8px 0; color: var(--ink-soft); }
+ ul, ol { margin: 8px 0 8px 24px; }
+ li { margin: 5px 0; color: var(--ink-soft); }
+ a { color: var(--accent); }
+ code {
+ background: var(--code-bg); color: #9a3412; padding: 1px 6px; border-radius: 4px;
+ font-size: 13px; font-family: "Cascadia Code", Menlo, Consolas, monospace;
+ }
+ kbd {
+ background: #fff; border: 1px solid var(--line); border-bottom-width: 2px;
+ border-radius: 4px; padding: 1px 6px; font-size: 12px; font-family: inherit;
+ }
+
+ .chip {
+ display: inline-block; background: #fff; color: var(--ink); border: 1px solid var(--line);
+ border-radius: 5px; padding: 1px 8px; font-size: 12.5px; margin: 2px 3px 2px 0;
+ font-family: "Cascadia Code", Menlo, Consolas, monospace;
+ }
+ table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: 13.5px; }
+ th, td { text-align: left; padding: 9px 12px; border: 1px solid var(--line); vertical-align: top; }
+ th { background: #f8fafc; color: var(--ink); font-weight: 600; }
+ tbody tr:nth-child(even) { background: #fcfcfd; }
+
+ .note {
+ background: #fbfaf5; border: 1px solid #e8e2cf; border-left: 3px solid #c9a227;
+ border-radius: 4px; padding: 12px 16px; margin: 14px 0; font-size: 13.5px; color: #5c4d1f;
+ }
+ .note b { color: #4a3d12; }
+
+ footer { margin-top: 40px; padding-top: 16px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12.5px; }
+
+ /* ---------- 移动端目录开关 ---------- */
+ .toc-toggle {
+ display: none; position: sticky; top: 0; z-index: 50;
+ width: 100%; text-align: left; background: #fbfcfd; color: var(--ink);
+ border: none; border-bottom: 1px solid var(--line); padding: 12px 16px; font-size: 14px; font-weight: 600; cursor: pointer;
+ }
+
+ @media (max-width: 900px) {
+ .layout { display: block; }
+ .toc-toggle { display: block; }
+ .toc {
+ position: static; width: 100%; height: auto; border-right: none;
+ border-bottom: 1px solid var(--line); display: none;
+ }
+ .toc.open { display: block; }
+ .content { border: none; }
+ .topbar, .doc { padding-left: 20px; padding-right: 20px; }
+ }
+
+ /* ---------- 打印 ---------- */
@media print {
- body { background: #fff; padding: 0; }
- .wrap { box-shadow: none; max-width: 100%; padding: 0 8px; }
- .btn-pdf { display: none; }
+ body { background: #fff; }
+ .layout { display: block; max-width: 100%; }
+ .toc-toggle, .toc, .btn-pdf { display: none !important; }
+ .content { border: none; }
+ .doc { padding: 0; }
h2 { page-break-after: avoid; }
- .page, .note { break-inside: avoid; }
+ .note, table, tr { break-inside: avoid; }
+ a { color: var(--ink); text-decoration: none; }
}
@page { margin: 16mm; }
</style>
</head>
<body>
-<div class="wrap">
- <header>
- <div>
- <h1>Codebuddy Web Console · 设计说明</h1>
- <div class="sub">通过浏览器远程使用 Codebuddy CLI 的 Web 控制台 · 多会话 / 全局技能 / 文件上传 / 移动端适配</div>
- </div>
- <button class="btn-pdf" onclick="window.print()">⬇ 下载 PDF</button>
- </header>
-
- <h2>一、整体架构</h2>
- <ul>
- <li><b>后端</b>:FastAPI + WebSocket PTY。每个“任务”对应一个独立运行的 <code>codebuddy</code> CLI 进程。</li>
- <li><b>前端</b>:单文件原生 HTML(无前端框架)。通过 URL 参数区分页面:<code>/</code> 为任务列表页,<code>/?task=<任务ID></code> 为对应任务的终端页。</li>
- <li><b>技能</b>:存储于全局目录 <code>~/.codebuddy/skills</code>。上传后对所有会话可见;运行中的会话可“热加载”即时生效,否则下次打开会话时由 codebuddy 自动发现。</li>
- <li><b>上传文件</b>:统一存于服务端的 <code>data/pastes/</code> 目录,并可在“已上传文件”面板中集中管理。</li>
- </ul>
-
- <h2>二、页面与功能</h2>
-
- <div class="page">
- <h3>1. 登录页</h3>
- <ul>
- <li>输入访问密码进入控制台(密码保存在浏览器 <code>localStorage</code>)。</li>
- </ul>
- </div>
- <div class="page">
- <h3>2. 任务列表页(首页)</h3>
- <p>默认展示“启用中”的任务,可勾选“显示全部任务”查看包括已暂停的全部任务。</p>
- <h3>左侧 · 技能侧边栏(🧩 技能 全局)</h3>
- <ul>
- <li><b>上传</b>:选择 skill 的 zip(须以单个 skill 目录为顶层,内含 <code>SKILL.md</code>),上传后对所有会话全局可见。</li>
- <li><b>技能列表卡片</b>:每张卡片提供 <span class="tag">更新</span><span class="tag">编辑</span><span class="tag">加载</span><span class="tag">删除</span>。
- <ul>
- <li><b>更新</b>:用新 zip 覆盖同名 skill(按 zip 内声明名匹配,避免产生重复目录)。</li>
- <li><b>编辑</b>:直接修改 <code>SKILL.md</code> 内容。</li>
- <li><b>加载</b>:选择“运行中的会话”注入热加载通知(可多选,或一键“全部运行中的会话”);若无运行会话则技能已全局就位,下次打开自动发现。</li>
- <li><b>删除</b>:移除该全局 skill。</li>
- </ul>
+<button class="toc-toggle" id="toc-toggle" onclick="document.getElementById('toc').classList.toggle('open')">☰ 目录</button>
+
+<div class="layout">
+ <nav class="toc" id="toc">
+ <div class="toc-title">目录</div>
+ <ol>
+ <li><a href="#sec-arch" class="nav-link">整体架构</a></li>
+ <li><a href="#sec-login" class="nav-link">登录页</a></li>
+ <li><a href="#sec-list" class="nav-link">任务列表页</a>
+ <ol class="sub">
+ <li><a href="#sec-list-head" class="nav-link">页面头部</a></li>
+ <li><a href="#sec-list-skills" class="nav-link">技能侧边栏</a></li>
+ <li><a href="#sec-list-tasks" class="nav-link">任务卡片区</a></li>
+ <li><a href="#sec-list-del" class="nav-link">删除确认弹窗</a></li>
+ </ol>
</li>
- <li><b>📂 已上传文件</b>:打开“已上传文件”管理弹窗(详见第 4 节)。</li>
- </ul>
- <h3>主区 · 任务卡片</h3>
- <ul>
- <li>展示:任务名、工作目录、状态、最近使用时间。</li>
- <li>状态指示:<span class="tag">● 前台已登录</span><span class="tag">⚙ 后台运行中</span><span class="tag">⏸ 后台等待中</span><span class="tag">⏳ 后台任务待确认</span><span class="tag">⏸ 已暂停</span><span class="tag">○ 空闲</span>。</li>
- <li>操作:打开终端、重启会话、暂停/恢复、删除(可同时删除历史会话记录与工作目录)。</li>
- </ul>
- <p class="note">列表页每 5 秒自动刷新一次,实时反映各会话状态。</p>
- </div>
+ <li><a href="#sec-terminal" class="nav-link">终端页</a>
+ <ol class="sub">
+ <li><a href="#sec-term-top" class="nav-link">顶部操作栏</a></li>
+ <li><a href="#sec-term-out" class="nav-link">终端输出区</a></li>
+ <li><a href="#sec-term-paste" class="nav-link">粘贴弹窗</a></li>
+ <li><a href="#sec-term-skills" class="nav-link">技能弹窗</a></li>
+ </ol>
+ </li>
+ <li><a href="#sec-uploads" class="nav-link">已上传文件弹窗</a></li>
+ <li><a href="#sec-mobile" class="nav-link">移动端适配</a></li>
+ <li><a href="#sec-shortcut" class="nav-link">快捷操作</a></li>
+ </ol>
+ </nav>
- <div class="page">
- <h3>3. 终端页(<code>/?task=<ID></code>)</h3>
- <h3>顶部栏</h3>
- <ul>
- <li>连接状态指示灯 + 文本、任务名、工作目录。</li>
- <li>模型切换下拉(切换后重启会话以应用)。</li>
- <li>按钮:<span class="tag">↻ 重启会话</span><span class="tag">≡ 列表</span><span class="tag">📋 粘贴</span><span class="tag">🧩 技能</span><span class="tag">❓ 帮助</span><span class="tag">✕ 关闭</span>。</li>
- </ul>
- <h3>终端输出区(xterm)</h3>
- <ul>
- <li>实时显示 CLI 输出;新输出自动滚动贴底。</li>
- <li>手动上滑查看历史时,右下角出现 <span class="tag">↓ 回到底部</span> 按钮,点击即回到最新。</li>
- </ul>
- <h3>粘贴框(📋)</h3>
- <ul>
- <li>文本直接输入;<code>Ctrl+Enter</code> 发送到终端输入行(不自动提交,需自行回车)。</li>
- <li><b>📎 附件</b>:支持上传 <b>图片 / 视频 / zip / 任意格式</b> 文件,单文件上限 <b>5MB</b>。</li>
- <li>待发送文件以 chip 展示(图标区分 🖼 图片 / 🎬 视频 / 📦 压缩包 / 📄 其他)。</li>
- <li>发送规则:图片包装为 <code><image_local_path>路径</image_local_path></code>;其它文件发送其本地路径文本,供命令行引用。</li>
- <li>粘贴框内“已上传文件”区:列出本次/历史上传文件,可单独删除。</li>
- </ul>
- <h3>技能弹窗(🧩,终端页内)</h3>
- <ul>
- <li>功能与列表页左侧技能侧边栏一致(上传/更新/编辑/加载/删除)。</li>
- </ul>
- </div>
+ <div class="content">
+ <div class="topbar">
+ <div>
+ <h1>Codebuddy Web Console · 设计说明</h1>
+ <div class="sub">通过浏览器远程使用 Codebuddy CLI 的 Web 控制台 — 多会话 / 全局技能 / 文件上传 / 移动端适配</div>
+ </div>
+ <button class="btn-md" onclick="downloadMd()">下载 MD</button>
+ <button class="btn-pdf" onclick="window.print()">下载 PDF</button>
+ </div>
- <div class="page">
- <h3>4. 已上传文件弹窗</h3>
- <p>从列表页「📂 已上传文件」按钮打开,以表格管理所有上传文件:</p>
- <table>
- <thead><tr><th>列</th><th>说明</th></tr></thead>
- <tbody>
- <tr><td>选择框</td><td>每行可勾选;表头“全选”一键勾选全部。</td></tr>
- <tr><td>文件名</td><td>服务端存储的文件名。</td></tr>
- <tr><td>上传时间</td><td>文件写入时间(本地时间)。</td></tr>
- <tr><td>所属会话任务</td><td>上传时所在的终端会话名;历史文件(升级前上传)显示 <code>—</code>。</td></tr>
- <tr><td>文件类型</td><td>按扩展名/格式显示,如 <code>.png</code> / <code>.zip</code> / <code>.mp4</code>。</td></tr>
- <tr><td>操作</td><td>单条“删除”。</td></tr>
- </tbody>
- </table>
- <ul>
- <li><b>批量删除</b>:勾选多个后点“删除所选”(带二次确认)。</li>
- <li>删除同时从服务端磁盘移除文件,并清理其会话归属索引。</li>
- </ul>
- </div>
+ <div class="doc" style="counter-reset: sec;">
- <h2>三、移动端(iPad / 手机)适配</h2>
- <ul>
- <li><b>顶栏固定</b>:顶部菜单栏锁定在视口顶部,不随页面滚动;整页只有终端区内部滚动。</li>
- <li><b>键盘适配</b>:软键盘弹起时,终端区自动缩小到键盘上方,输入内容始终可见。</li>
- <li><b>自动滚动</b>:CLI 新输出自动贴底,无需手动下拉。</li>
- <li><b>标签页标题</b>:浏览器标签页显示为 <code>会话名 - CLI</code>(如 <code>Words - CLI</code>),便于多标签区分。</li>
- <li><b>安全区域</b>:适配刘海与底部指示条(<code>env(safe-area-inset)</code>)。</li>
- </ul>
-
- <h2>四、快捷操作</h2>
- <ul>
- <li><code>Ctrl+Enter</code>:在粘贴框发送内容。</li>
- <li><code>Esc</code>:关闭弹窗(粘贴框 / 技能框 / 已上传文件框)。</li>
- <li>多会话选择 + “全部运行中的会话”:一键把 skill 热加载到多个会话。</li>
- </ul>
-
- <h2>五、常见问题</h2>
- <div class="note">
- <b>Q:上传视频/zip 提示不支持或过大?</b><br>
- 服务端已内置“任意格式 + 5MB”支持;若仍报错,多为服务端尚未重启生效,重启 <code>codebuddy-web.service</code> 即可。
- </div>
- <div class="note">
- <b>Q:已上传文件的“所属会话任务”是空/—?</b><br>
- 仅“升级后、从终端页新上传”的文件会记录所属会话;升级前上传的历史文件因当时未记录归属,故显示 <code>—</code>,无法反查。
- </div>
+ <h2 id="sec-arch">整体架构</h2>
+ <ul>
+ <li><b>后端</b>:FastAPI + WebSocket PTY。每个「任务」对应一个独立运行的 <code>codebuddy</code> CLI 进程,经由 PTY 与前端双向通信。</li>
+ <li><b>前端</b>:单文件原生 HTML(无前端框架)。通过 URL 参数区分页面:<code>/</code> 为任务列表页,<code>/?task=<任务ID></code> 为对应任务的终端页。</li>
+ <li><b>技能</b>:存储于全局目录 <code>~/.codebuddy/skills</code>。上传后对所有会话可见;运行中的会话可「热加载」即时生效,否则下次打开会话时由 codebuddy 自动发现。</li>
+ <li><b>上传文件</b>:统一存于服务端的 <code>data/pastes/</code> 目录,并在「已上传文件」面板中集中管理(含所属会话关联)。</li>
+ <li><b>鉴权</b>:通过访问密码进入,令牌保存于浏览器 <code>localStorage</code>,后续请求经请求头携带。</li>
+ </ul>
+
+ <h2 id="sec-login">登录页</h2>
+ <ul>
+ <li><b>密码输入框</b>:聚焦态自动获得焦点;输入访问密码。</li>
+ <li><b>进入控制台</b>:提交密码进行鉴权;成功后进入任务列表页,并将令牌写入 <code>localStorage</code> 以便后续自动携带。</li>
+ <li><b>错误提示</b>:鉴权失败时显示错误文案,不写入令牌。</li>
+ </ul>
+
+ <h2 id="sec-list">任务列表页</h2>
+
+ <h3 id="sec-list-head">页面头部</h3>
+ <ul>
+ <li><b>标题与计数</b>:显示「任务清单」及当前筛选下的任务数量、暂停数量统计。</li>
+ <li><b>显示全部任务</b>(复选框):默认仅列出「启用中」任务;勾选后展示包含已暂停的全部任务。</li>
+ <li><b>❓ 帮助</b>:新标签页打开本设计说明(<code>/help</code>)。</li>
+ <li><b>退出登录</b>:清除本地令牌并返回登录页。</li>
+ </ul>
+
+ <h3 id="sec-list-skills">技能侧边栏(🧩 技能 · 全局)</h3>
+ <ul>
+ <li><b>上传</b>:选择 skill 的 zip(须以单个 skill 目录为顶层,内含 <code>SKILL.md</code>),上传后对所有会话全局可见。</li>
+ <li><b>技能列表卡片</b>:每张卡片提供 <span class="chip">更新</span><span class="chip">编辑</span><span class="chip">加载</span><span class="chip">删除</span>。
+ <ul>
+ <li><b>更新</b>:用新 zip 覆盖同名 skill(按 zip 内声明名匹配,避免产生重复目录)。</li>
+ <li><b>编辑</b>:直接修改 <code>SKILL.md</code> 内容并保存。</li>
+ <li><b>加载</b>:选择「运行中的会话」注入热加载通知(可多选,或勾选「全部运行中的会话」);若无运行会话则技能已全局就位,下次打开自动发现。</li>
+ <li><b>删除</b>:移除该全局 skill。</li>
+ </ul>
+ </li>
+ <li><b>📂 已上传文件</b>:打开「已上传文件」管理弹窗(见第 5 节)。</li>
+ </ul>
+
+ <h3 id="sec-list-tasks">任务卡片区</h3>
+ <ul>
+ <li><b>卡片字段</b>:任务名、工作目录、最近使用时间、实时状态。</li>
+ <li><b>状态指示</b>:<span class="chip">● 前台已登录</span><span class="chip">⚙ 后台运行中</span><span class="chip">⏸ 后台等待中</span><span class="chip">⏳ 后台任务待确认</span><span class="chip">⏸ 已暂停</span><span class="chip">○ 空闲</span>。</li>
+ <li><b>进入</b>:打开该任务的终端页(<code>/?task=<ID></code>)。</li>
+ <li><b>暂停 / 恢复</b>:运行中显示「暂停」(⏸),已暂停显示「恢复」(▶);暂停后需满 24 小时才允许删除。</li>
+ <li><b>删除</b>(✕):仅对暂停满 24 小时的任务出现,触发删除确认弹窗。</li>
+ <li><b>+ 新建任务</b>:输入新任务名称(将作为 <code>/home/<名称></code> 工作目录)以创建会话。</li>
+ </ul>
+ <p class="note">列表页每 5 秒自动刷新一次,实时反映各会话状态。</p>
+
+ <h3 id="sec-list-del">删除确认弹窗</h3>
+ <ul>
+ <li>二次确认后方可删除;「确认删除」按钮初始禁用,需勾选至少一项附加操作后才可点击。</li>
+ <li><b>同时删除历史会话</b>:勾选后清理对话记录。</li>
+ <li><b>同时删除工作目录</b>:勾选后移除 <code>/home/<名称></code> 目录(显示其路径供核对)。</li>
+ </ul>
+
+ <h2 id="sec-terminal">终端页</h2>
+
+ <h3 id="sec-term-top">顶部操作栏</h3>
+ <ul>
+ <li><b>连接状态</b>:状态指示灯 + 文本,反映当前 PTY 连接状态。</li>
+ <li><b>任务名 / 工作目录</b>:显示当前会话标识。</li>
+ <li><b>模型切换下拉</b>:切换后重启会话以应用新模型。</li>
+ <li><span class="chip">↻ 重启会话</span>:重启当前 PTY 并续接上次历史。</li>
+ <li><span class="chip">≡ 列表</span>:返回任务列表页。</li>
+ <li><span class="chip">📋 粘贴</span>:打开粘贴弹窗。</li>
+ <li><span class="chip">🧩 技能</span>:打开技能弹窗(同列表页技能侧栏功能)。</li>
+ <li><span class="chip">❓ 帮助</span>:新标签页打开本设计说明。</li>
+ <li><span class="chip">✕ 关闭</span>:关闭窗口。</li>
+ </ul>
+
+ <h3 id="sec-term-out">终端输出区(xterm)</h3>
+ <ul>
+ <li>实时显示 CLI 输出;新输出默认自动滚动贴底。</li>
+ <li>手动上滑查看历史时,右下角出现 <span class="chip">↓ 回到底部</span> 按钮,点击即回到最新输出。</li>
+ </ul>
+
+ <h3 id="sec-term-paste">粘贴弹窗(📋)</h3>
+ <ul>
+ <li><b>文本输入</b>:直接输入;<kbd>Ctrl</kbd>+<kbd>Enter</kbd> 发送到终端输入行(不自动提交,需自行回车)。</li>
+ <li><b>📎 附件</b>:支持上传 图片 / 视频 / zip / 任意格式 文件,单文件上限 <b>20MB</b>。</li>
+ <li><b>✕ 清空</b>:清空当前输入与待发送文件。</li>
+ <li><b>发送</b>:将文本与待发送文件一并提交。</li>
+ <li><b>待发送文件 chip</b>:图标区分 🖼 图片 / 🎬 视频 / 📦 压缩包 / 📄 其他。</li>
+ <li><b>发送规则</b>:图片包装为 <code><image_local_path>路径</image_local_path></code>;其它文件发送其本地路径文本,供命令行引用。</li>
+ <li><b>已上传文件区</b>:列出历史/本次上传文件,可单独删除;⟳ 按钮刷新列表。</li>
+ </ul>
- <footer>
- Codebuddy Web Console · 设计说明(随仓库 <code>frontend/help.html</code> 提供) · 在应用内点「❓ 帮助」可随时打开
- </footer>
+ <h3 id="sec-term-skills">技能弹窗(🧩,终端页内)</h3>
+ <ul>
+ <li>功能与列表页左侧技能侧边栏一致:上传 / 更新 / 编辑 / 加载 / 删除。</li>
+ </ul>
+
+ <h2 id="sec-uploads">已上传文件弹窗</h2>
+ <p>从列表页「📂 已上传文件」按钮打开,以表格管理所有上传文件:</p>
+ <table>
+ <thead><tr><th>列</th><th>说明</th></tr></thead>
+ <tbody>
+ <tr><td>选择框</td><td>每行可勾选;表头「全选」一键勾选全部。</td></tr>
+ <tr><td>文件名</td><td>服务端存储的文件名。</td></tr>
+ <tr><td>上传时间</td><td>文件写入时间(本地时间)。</td></tr>
+ <tr><td>所属会话任务</td><td>上传时所在的终端会话名;历史文件(升级前上传)显示 <code>—</code>。</td></tr>
+ <tr><td>文件类型</td><td>按扩展名/格式显示,如 <code>.png</code> / <code>.zip</code> / <code>.mp4</code>。</td></tr>
+ <tr><td>操作</td><td>单条「删除」。</td></tr>
+ </tbody>
+ </table>
+ <ul>
+ <li><b>删除所选</b>:勾选多个后点击(带二次确认);初始禁用,至少勾选一项后启用。</li>
+ <li>删除同时从服务端磁盘移除文件,并清理其会话归属索引。</li>
+ </ul>
+
+ <h2 id="sec-mobile">移动端(iPad / 手机)适配</h2>
+ <ul>
+ <li><b>顶栏固定</b>:顶部菜单栏锁定在视口顶部,不随页面滚动;整页只有终端区内部滚动。</li>
+ <li><b>键盘适配</b>:软键盘弹起时,终端区自动缩小到键盘上方,输入内容始终可见。</li>
+ <li><b>自动滚动</b>:CLI 新输出自动贴底,无需手动下拉。</li>
+ <li><b>标签页标题</b>:浏览器标签页显示为 <code>会话名 - CLI</code>(如 <code>Words - CLI</code>),便于多标签区分。</li>
+ <li><b>安全区域</b>:适配刘海与底部指示条(<code>env(safe-area-inset)</code>)。</li>
+ </ul>
+
+ <h2 id="sec-shortcut">快捷操作</h2>
+ <ul>
+ <li><kbd>Ctrl</kbd>+<kbd>Enter</kbd>:在粘贴框发送内容。</li>
+ <li><kbd>Esc</kbd>:关闭弹窗(粘贴框 / 技能框 / 已上传文件框)。</li>
+ <li>多会话选择 + 「全部运行中的会话」:一键把 skill 热加载到多个会话。</li>
+ </ul>
+
+ <footer>
+ Codebuddy Web Console · 设计说明(随仓库 <code>frontend/help.html</code> 提供) · 在应用内点「❓ 帮助」可随时打开
+ </footer>
+ </div>
+ </div>
</div>
+
+<script>
+ // ---------- DOM → Markdown 转换(单一数据源,避免与正文脱节) ----------
+ function mdInline(node) {
+ var s = '';
+ node.childNodes.forEach(function (n) {
+ if (n.nodeType === 3) { s += n.textContent; return; }
+ if (n.nodeType !== 1) return;
+ var t = n.tagName.toLowerCase();
+ var txt = mdInline(n);
+ if (t === 'code' || t === 'kbd') s += '`' + txt + '`';
+ else if (t === 'span' && n.classList.contains('chip')) s += '`' + txt + '`';
+ else if (t === 'b' || t === 'strong') s += '**' + txt + '**';
+ else if (t === 'a') s += '[' + txt + '](' + (n.getAttribute('href') || '') + ')';
+ else if (t === 'br') s += '\n';
+ else s += txt;
+ });
+ return s;
+ }
+
+ function mdTable(table) {
+ var rows = Array.prototype.slice.call(table.querySelectorAll('tr'));
+ if (!rows.length) return '';
+ var head = Array.prototype.slice.call(rows[0].querySelectorAll('th')).map(function (c) { return mdInline(c).trim(); });
+ var out = '| ' + head.join(' | ') + ' |\n';
+ out += '| ' + head.map(function () { return '---'; }).join(' | ') + ' |\n';
+ for (var i = 1; i < rows.length; i++) {
+ var cells = Array.prototype.slice.call(rows[i].querySelectorAll('td')).map(function (c) {
+ return mdInline(c).trim().replace(/\n/g, ' ');
+ });
+ out += '| ' + cells.join(' | ') + ' |\n';
+ }
+ return out;
+ }
+
+ function docToMd() {
+ var root = document.querySelector('.doc');
+ var lines = [];
+ var h2n = 0;
+ Array.prototype.slice.call(root.children).forEach(function (node) {
+ var tag = node.tagName.toLowerCase();
+ if (tag === 'h2') {
+ h2n++;
+ var title = mdInline(node).replace(/^\s*[\d.]+\.?\s*/, '').trim();
+ lines.push('## ' + h2n + '. ' + title, '');
+ } else if (tag === 'h3') {
+ lines.push('### ' + mdInline(node).replace(/^§\s*/, '').trim(), '');
+ } else if (tag === 'p') {
+ if (node.classList.contains('note')) lines.push('> ' + mdInline(node).replace(/\n+/g, ' ').trim(), '');
+ else lines.push(mdInline(node).trim(), '');
+ } else if (tag === 'div' && node.classList.contains('note')) {
+ mdInline(node).split('\n').forEach(function (l) { lines.push('> ' + l.trim()); });
+ lines.push('');
+ } else if (tag === 'ul') {
+ Array.prototype.slice.call(node.children).forEach(function (li) { lines.push('- ' + mdInline(li).replace(/\n+/g, ' ').trim()); });
+ lines.push('');
+ } else if (tag === 'ol') {
+ var k = 1;
+ Array.prototype.slice.call(node.children).forEach(function (li) { lines.push(k + '. ' + mdInline(li).replace(/\n+/g, ' ').trim()); k++; });
+ lines.push('');
+ } else if (tag === 'table') {
+ lines.push(mdTable(node), '');
+ } else if (tag === 'footer') {
+ lines.push('---', mdInline(node).trim(), '');
+ }
+ });
+ return '# Codebuddy Web Console · 设计说明\n\n' + lines.join('\n');
+ }
+
+ function downloadMd() {
+ var md = docToMd();
+ var blob = new Blob([md], { type: 'text/markdown;charset=utf-8' });
+ var url = URL.createObjectURL(blob);
+ var a = document.createElement('a');
+ a.href = url;
+ a.download = 'Codebuddy-Web-Console-设计说明.md';
+ document.body.appendChild(a);
+ a.click();
+ a.remove();
+ URL.revokeObjectURL(url);
+ }
+
+ // 滚动高亮当前章节
+ (function () {
+ var links = Array.prototype.slice.call(document.querySelectorAll('.nav-link'));
+ var map = {};
+ links.forEach(function (a) {
+ var id = a.getAttribute('href').slice(1);
+ var el = document.getElementById(id);
+ if (el) map[id] = a;
+ });
+ var targets = Object.keys(map).map(function (id) { return document.getElementById(id); });
+ if (!('IntersectionObserver' in window) || !targets.length) return;
+ var observer = new IntersectionObserver(function (entries) {
+ entries.forEach(function (e) {
+ if (e.isIntersecting) {
+ links.forEach(function (l) { l.classList.remove('active'); });
+ var a = map[e.target.id];
+ if (a) a.classList.add('active');
+ }
+ });
+ }, { rootMargin: '0px 0px -70% 0px', threshold: 0 });
+ targets.forEach(function (t) { observer.observe(t); });
+ })();
+</script>
</body>
</html>