# 拆书面板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 = '
加载失败,请检查服务器
'; } } 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 = '
📚 还没有拆书记录~启动流水线开始拆吧!
'; } 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 = '
' + '
' + '
' + '
' + pct + '% · ' + b.analyzed + '/' + b.total + '章
'; } booksHtml += '
' + '
' + '' + si + '' + '《' + esc(b.name) + '》' + '' + (b.updated_at||'') + '' + '
' + '
' + esc(b.progress) + '
' + bar + '
'; }); } var selectOpts = bookList.map(function(b){ return ''; }).join('') || ''; el.innerHTML = '
' + '
' + totalBooks + '
已拆书籍
' + '
' + totalChapters + '
总章节数
' + '
' + '
' + '' + '' + '
' + '
📚 书库(最近10本)
' + '
' + '
📖 当前进度
' + '
' + booksHtml + '
' + '
' + '
🤖 自动拆书流水线
' + '
' + '' + '' + '' + '
' + '' + '
' + '
' + '
' + '
💡 快速记录规律
' + '' + '' + '' + '
'; 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 '
' + '' + ic + '' + '《' + esc(b.name) + '》' + '' + b.analyzed + '章' + '
'; }).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 两个字就进行下一步 💛 >