Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
17 KiB
17 KiB
拆书面板v2.0·三步安装·这次不会乱·2026-05-20
先 Ctrl+C 停服务器,然后按顺序执行三段
第一段:建表 + 改 Python 文件(约剠30秒)
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秒)
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秒)
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
最后:重新启动服务器
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 两个字就进行下一步 💛