# 拆书面板v2.0·三步安装·这次不会乱·2026-05-20

# 先 Ctrl+C 停服务器，然后按顺序执行三段

---

## 第一段：建表 + 改 Python 文件（约剠30秒）

```bash
python3 << 'STEP1'
from pathlib import Path
import sqlite3
B = Path.home() / 'chenxing-aircraft'

# 1. 建表
c = sqlite3.connect(str(B/'chenxing.db'))
c.execute("""CREATE TABLE IF NOT EXISTS book_progress (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  book_name VARCHAR(200) NOT NULL UNIQUE,
  total_chapters INTEGER DEFAULT 0,
  analyzed_chapters INTEGER DEFAULT 0,
  status VARCHAR(20) DEFAULT 'in_progress',
  last_chapter VARCHAR(100) DEFAULT '',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)""")
c.commit(); c.close()
print("1/4 book_progress表 OK")

# 2. models.py
mp = B/'backend'/'models.py'
mt = mp.read_text('utf-8')
if 'BookProgress' not in mt:
    mp.write_text(mt + """

class BookProgress(Base):
    __tablename__ = "book_progress"
    id = Column(Integer, primary_key=True)
    book_name = Column(String(200), nullable=False, unique=True)
    total_chapters = Column(Integer, default=0)
    analyzed_chapters = Column(Integer, default=0)
    status = Column(String(20), default="in_progress")
    last_chapter = Column(String(100), default="")
    created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
    updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
""", 'utf-8')
print("2/4 models.py OK")

# 3. main.py
mm = B/'backend'/'main.py'
mt2 = mm.read_text('utf-8')
if '/api/chaishu/progress' not in mt2:
    api = """

# ========== Chaishu v2.0 ==========
from .models import BookProgress as _BP

class _StartPipelineReq(BaseModel):
    book_name: str
    start_chapter: int
    end_chapter: int
    force: bool = False

@app.get("/api/chaishu/progress")
def _chaishu_prog(db: Session = Depends(get_db)):
    rows = db.query(_BP).order_by(_BP.updated_at.desc()).all()
    return {"data": {
        "total_books": len(rows),
        "total_chapters": sum(r.analyzed_chapters for r in rows),
        "books": [{"name":r.book_name,"icon":"leaf",
            "progress":("\u5df2\u5206\u6790\u81f3"+r.last_chapter) if r.status=="in_progress" else ("\u5168\u4e66"+str(r.analyzed_chapters)+"\u7ae0\u5df2\u5206\u6790"),
            "analyzed":r.analyzed_chapters,"total":r.total_chapters,
            "status":r.status,"updated_at":str(r.updated_at)[:10]} for r in rows]
    }}

@app.get("/api/chaishu/recent")
def _chaishu_recent(db: Session = Depends(get_db)):
    rows = db.query(_BP).order_by(_BP.updated_at.desc()).limit(10).all()
    return {"data":[{"name":r.book_name,"last_edited":str(r.updated_at)[:10],
        "status":r.status,"analyzed":r.analyzed_chapters} for r in rows]}

@app.get("/api/chaishu/export")
def _chaishu_export(db: Session = Depends(get_db)):
    from fastapi.responses import Response as FR
    rows = db.query(_BP).order_by(_BP.updated_at.desc()).all()
    lines = ["\u4e66\u540d,\u5df2\u5206\u6790\u7ae0\u8282,\u603b\u7ae0\u8282,\u72b6\u6001,\u6700\u540e\u66f4\u65b0"]
    for r in rows:
        s = "\u5df2\u5b8c\u6210" if r.status=="completed" else "\u8fdb\u884c\u4e2d"
        lines.append(str(r.book_name)+","+str(r.analyzed_chapters)+","+str(r.total_chapters)+","+s+","+str(r.updated_at)[:10])
    return FR(content="\\n".join(lines).encode("utf-8-sig"),media_type="text/csv",
        headers={"Content-Disposition":"attachment; filename=chaishu.csv"})

@app.post("/api/chaishu/pipeline/start")
async def _chaishu_start(req: _StartPipelineReq, db: Session = Depends(get_db)):
    import asyncio as _a, sys as _s, os as _o
    ex = db.query(_BP).filter(_BP.book_name==req.book_name).first()
    if ex and not req.force:
        return {"success":False,"duplicate":True,
            "created_at":str(ex.created_at)[:10],"analyzed":ex.analyzed_chapters}
    if not ex:
        db.add(_BP(book_name=req.book_name,status="in_progress",
            total_chapters=req.end_chapter-req.start_chapter+1))
        db.commit()
    sc = _o.path.join(_o.path.dirname(_o.path.dirname(__file__)),"tools","auto_dishu.py")
    if _o.path.exists(sc):
        _a.create_task(_a.create_subprocess_exec(_s.executable,sc,
            "--book",req.book_name,"--start_chapter",str(req.start_chapter),
            "--end_chapter",str(req.end_chapter)))
    return {"success":True}

@app.post("/api/chaishu/note")
async def _chaishu_note(request: Request, db: Session = Depends(get_db)):
    try:
        body = await request.json()
        note = body.get("note","").strip()
        book = body.get("book","")
        if not note: return {"success":False}
        import datetime as _dt
        db.add(Knowledge(user_id=1,
            title="\ud83d\udca1 "+book+"\u00b7\u89c4\u5f8b\u7b14\u8bb0\u00b7"+str(_dt.datetime.now())[:10],
            content=note,category="note",tags=book,source="\u62c6\u4e66\u5de5\u4f5c\u53f0"))
        db.commit()
        return {"success":True}
    except Exception as e: return {"success":False,"error":str(e)}
"""
    mm.write_text(mt2 + api, 'utf-8')
print("3/4 main.py OK")

# 4. tools/
(B/'tools').mkdir(exist_ok=True)
(B/'tools'/'__init__.py').touch()
print("4/4 tools/ OK")
print("\n\u7b2c\u4e00\u6b65\u5b8c\u6210\uff01\u7ee7\u7eed\u6267\u884c\u7b2c\u4e8c\u6b65\u3002")
STEP1
```

---

## 第二段：把 JS 面板写到临时文件（约剠10秒）

```bash
cat > /tmp/panel_v2.js << 'JSEOF'
// ==============================
// 拆书面板 v2.0
// ==============================
let chaishuScrollTimer = null;
let _chaishuBookCache = [];

function openChaishu() { openPanel('panelChaishu', loadChaishuData); }

async function loadChaishuData() {
  try {
    const [pr, rr] = await Promise.all([fetch('/api/chaishu/progress'), fetch('/api/chaishu/recent')]);
    const prog = await pr.json();
    const rec = await rr.json();
    _chaishuBookCache = rec.data || [];
    renderChaishuContent(prog.data, rec.data);
  } catch(e) {
    const el = document.getElementById('chaishuContent');
    if (el) el.innerHTML = '<div style="color:#ef4444;padding:20px;text-align:center;font-size:13px;">加载失败，请检查服务器</div>';
  }
}

function renderChaishuContent(progress, books) {
  var el = document.getElementById('chaishuContent');
  if (!el) return;
  if (chaishuScrollTimer) { clearInterval(chaishuScrollTimer); chaishuScrollTimer = null; }
  var totalBooks = (progress && progress.total_books) || 0;
  var totalChapters = (progress && progress.total_chapters) || 0;
  var bookList = (progress && progress.books) || [];
  var booksHtml = '';
  if (!bookList.length) {
    booksHtml = '<div style="text-align:center;padding:24px;color:var(--text-dim);font-size:13px;">📚 还没有拆书记录～启动流水线开始拆吧！</div>';
  } else {
    bookList.forEach(function(b) {
      var pct = b.total ? Math.round(b.analyzed / b.total * 100) : 0;
      var si = b.status === 'completed' ? '✅' : '🔄';
      var bar = '';
      if (b.total) {
        bar = '<div style="padding-left:20px;margin-top:6px;">'
          + '<div style="height:4px;background:var(--border);border-radius:2px;">'
          + '<div style="height:4px;width:' + pct + '%;background:var(--accent);border-radius:2px;"></div>'
          + '</div><div style="font-size:10px;color:var(--text-dim);margin-top:2px;">' + pct + '% · ' + b.analyzed + '/' + b.total + '章</div></div>';
      }
      booksHtml += '<div class="chaishu-book-card" data-name="' + esc(b.name) + '">'
        + '<div style="display:flex;align-items:center;gap:6px;margin-bottom:4px;">'
        + '<span>' + si + '</span>'
        + '<span style="font-size:13px;font-weight:700;color:var(--text);">《' + esc(b.name) + '》</span>'
        + '<span style="font-size:10px;color:var(--text-dim);margin-left:auto;">' + (b.updated_at||'') + '</span>'
        + '</div>'
        + '<div style="font-size:12px;color:var(--text-dim);padding-left:20px;">' + esc(b.progress) + '</div>'
        + bar + '</div>';
    });
  }
  var selectOpts = bookList.map(function(b){ return '<option value="'+esc(b.name)+'">《'+esc(b.name)+'》</option>'; }).join('') || '<option>（暂无书籍）</option>';
  el.innerHTML = '<div style="display:flex;gap:8px;padding:10px 12px;border-bottom:1px solid var(--border);">'
    + '<div style="flex:1;text-align:center;padding:8px 4px;background:var(--accent-bg);border-radius:10px;"><div style="font-size:22px;font-weight:800;color:var(--accent);">' + totalBooks + '</div><div style="font-size:11px;color:var(--text-dim);">已拆书籍</div></div>'
    + '<div style="flex:1;text-align:center;padding:8px 4px;background:var(--accent-bg);border-radius:10px;"><div style="font-size:22px;font-weight:800;color:var(--accent);">' + totalChapters + '</div><div style="font-size:11px;color:var(--text-dim);">总章节数</div></div>'
    + '</div>'
    + '<div style="padding:8px 12px;border-bottom:1px solid var(--border);display:flex;gap:6px;align-items:center;">'
    + '<input id="chaishuSearchInput" type="text" placeholder="搜索书名…" style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-size:12px;outline:none;" oninput="filterChaishuCards(this.value)"/>'
    + '<button onclick="exportChaishu()" style="padding:6px 10px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text-dim);font-size:11px;cursor:pointer;white-space:nowrap;">📊 导出</button>'
    + '</div>'
    + '<div style="padding:8px 12px 4px;font-size:11px;font-weight:800;color:var(--text-dim);">📚 书库（最近10本）</div>'
    + '<div style="padding:0 12px 8px;border-bottom:1px solid var(--border);"><div id="chaishuScrollList" style="display:flex;flex-direction:column;gap:4px;min-height:36px;"></div></div>'
    + '<div style="padding:8px 12px 4px;font-size:11px;font-weight:800;color:var(--text-dim);">📖 当前进度</div>'
    + '<div id="chaishuBookList" style="padding:0 12px 8px;">' + booksHtml + '</div>'
    + '<div style="padding:10px 12px;border-top:1px solid var(--border);">'
    + '<div style="font-size:11px;font-weight:800;color:var(--text-dim);margin-bottom:8px;">🤖 自动拆书流水线</div>'
    + '<div style="display:flex;gap:5px;margin-bottom:6px;">'
    + '<input id="pipelineBook" placeholder="书名" style="flex:1;padding:5px 8px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:11px;"/>'
    + '<input id="pipelineStart" placeholder="起始章" type="number" style="width:58px;padding:5px 4px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:11px;text-align:center;"/>'
    + '<input id="pipelineEnd" placeholder="结束章" type="number" style="width:58px;padding:5px 4px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);font-size:11px;text-align:center;"/>'
    + '</div>'
    + '<button onclick="startPipeline()" style="width:100%;padding:8px;border:none;border-radius:8px;background:var(--accent);color:#fff;font-size:12px;font-weight:700;cursor:pointer;">🚀 启动流水线</button>'
    + '<div id="pipelineStatus" style="margin-top:6px;font-size:11px;color:var(--text-dim);text-align:center;"></div>'
    + '</div>'
    + '<div style="padding:10px 12px;border-top:1px solid var(--border);">'
    + '<div style="font-size:11px;font-weight:800;color:var(--text-dim);margin-bottom:6px;">💡 快速记录规律</div>'
    + '<select id="chaishuBook" style="width:100%;padding:5px 8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-size:12px;margin-bottom:6px;">' + selectOpts + '</select>'
    + '<textarea id="chaishuNote" placeholder="记录一个规律发现…" style="width:100%;box-sizing:border-box;padding:8px;border:1px solid var(--border);border-radius:8px;background:var(--bg);color:var(--text);font-size:12px;resize:none;height:56px;margin-bottom:6px;"></textarea>'
    + '<button onclick="submitChaishuNote()" style="width:100%;padding:7px;border:none;border-radius:8px;background:var(--accent);color:#fff;font-size:12px;font-weight:700;cursor:pointer;">✅ 记录到 Notion</button>'
    + '</div>';
  startChaishuScroll(bookList);
}

function startChaishuScroll(books) {
  if (!books || !books.length) return;
  var recent = books.slice(0, 10);
  var idx = 0;
  function render() {
    var wrap = document.getElementById('chaishuScrollList');
    if (!wrap) { clearInterval(chaishuScrollTimer); return; }
    var visible = [];
    for (var i = 0; i < Math.min(3, recent.length); i++) visible.push(recent[(idx + i) % recent.length]);
    wrap.innerHTML = visible.map(function(b) {
      var ic = b.status === 'completed' ? '✅' : '🔄';
      return '<div style="padding:5px 8px;background:var(--bg-alt);border-radius:8px;display:flex;align-items:center;gap:6px;font-size:12px;">'
        + '<span>' + ic + '</span>'
        + '<span style="color:var(--text);font-weight:600;">《' + esc(b.name) + '》</span>'
        + '<span style="color:var(--text-dim);font-size:11px;margin-left:auto;">' + b.analyzed + '章</span>'
        + '</div>';
    }).join('');
    idx = (idx + 1) % recent.length;
  }
  render();
  if (recent.length > 3) chaishuScrollTimer = setInterval(render, 2200);
}

function filterChaishuCards(q) {
  document.querySelectorAll('.chaishu-book-card').forEach(function(c) {
    c.style.display = (q && c.getAttribute('data-name').indexOf(q) === -1) ? 'none' : '';
  });
}

async function exportChaishu() {
  try {
    var r = await fetch('/api/chaishu/export');
    if (!r.ok) { showToast('导出失败'); return; }
    var blob = await r.blob();
    var url = URL.createObjectURL(blob);
    var a = document.createElement('a');
    a.href = url; a.download = 'chaishu_progress.csv'; a.click();
    URL.revokeObjectURL(url);
    showToast('✅ 导出成功');
  } catch(e) { showToast('导出失败'); }
}

async function startPipeline() {
  var book = ($('pipelineBook') || {}).value;
  if (book) book = book.trim();
  var start = parseInt(($('pipelineStart') || {}).value);
  var end = parseInt(($('pipelineEnd') || {}).value);
  var st = $('pipelineStatus');
  if (!book) { showToast('请输入书名'); return; }
  if (!start || !end || start > end) { showToast('请填写正确章节范围'); return; }
  try {
    var r = await fetch('/api/chaishu/pipeline/start', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({book_name: book, start_chapter: start, end_chapter: end})
    });
    var d = await r.json();
    if (d.success) {
      if (st) st.textContent = '✅ 流水线已在后台启动！';
      showToast('🚀 已启动！');
    } else if (d.duplicate) {
      if (confirm('《' + book + '》已于 ' + d.created_at + ' 开始拆书，已分析 ' + d.analyzed + ' 章。
确认追加？')) {
        var r2 = await fetch('/api/chaishu/pipeline/start', {
          method: 'POST',
          headers: {'Content-Type': 'application/json'},
          body: JSON.stringify({book_name: book, start_chapter: start, end_chapter: end, force: true})
        });
        var d2 = await r2.json();
        if (d2.success && st) st.textContent = '✅ 已追加启动！';
      }
    } else {
      if (st) st.textContent = '❌ ' + (d.error || d.message || '');
    }
  } catch(e) { if (st) st.textContent = '❌ 网络错误'; }
}

async function submitChaishuNote() {
  var noteEl = document.getElementById('chaishuNote');
  var bookEl = document.getElementById('chaishuBook');
  var note = noteEl ? noteEl.value.trim() : '';
  var book = bookEl ? bookEl.value : '';
  if (!note) { showToast('请输入内容'); return; }
  try {
    var r = await fetch('/api/chaishu/note', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({note: note, book: book})
    });
    var data = await r.json();
    if (data.success) {
      showToast('✅ 已记录！');
      if (noteEl) noteEl.value = '';
    } else {
      showToast('记录失败：' + (data.error || ''));
    }
  } catch(e) { showToast('记录失败'); }
}
JSEOF
echo "✅ JS面板已写入 /tmp/panel_v2.js"
```

---

## 第三段：拼入 app.js（约剠10秒）

```bash
python3 << 'STEP3'
from pathlib import Path
B = Path.home() / 'chenxing-aircraft'
code = (B/'frontend'/'app.js').read_text('utf-8')
panel = open('/tmp/panel_v2.js').read()

marker1 = '// ==============================\n// \u62c6\u4e66\u4e13\u5c5e\u9762\u677f'
marker2 = '// \u2550'*15  # old style
marker3 = 'let chaishuPanelOpen'

pos = code.find(marker1)
if pos == -1: pos = code.find('// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u62c6\u4e66')
if pos == -1:
    pos = code.find(marker3)
    if pos != -1: pos = code.rfind('\n', 0, pos) + 1

if pos == -1:
    (B/'frontend'/'app.js').write_text(code + '\n' + panel, 'utf-8')
    print('⚠️  找不到标记，内容已追加到文件末尾')
else:
    (B/'frontend'/'app.js').write_text(code[:pos] + panel, 'utf-8')
    print('✅ app.js 替换完成！')
STEP3
```

---

## 最后：重新启动服务器

```bash
cd ~/chenxing-aircraft && python3 -m uvicorn backend.main:app --host 0.0.0.0 --port 8000
```

浏览器 `Cmd+Shift+R` 强刷，拆书面板就变新版了 🎉

---

> ⚡ 三步安装 · 拆书面板v2.0 · 霜砚出品 · 2026-05-20
> 

> 妈妈每步一段，眼到 OK 两个字就进行下一步 💛
>