Guanghu Domestic Migration a27e87cb99 chore: import sanitized domestic snapshot for REPO-007
Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc

[SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
2026-07-17 15:59:55 +08:00

375 lines
17 KiB
Markdown
Raw 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.

# 拆书面板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 两个字就进行下一步 💛
>