feat: add dedicated douyin workbench entry

This commit is contained in:
kris
2026-03-21 04:57:18 +08:00
parent 9f921fff94
commit 7171dae91c
5 changed files with 148 additions and 17 deletions

View File

@@ -22,23 +22,45 @@ cd /Users/kris/code/StoryForge-gitea/android-app
## Douyin Browser Capture
```bash
cd /Users/kris/code/StoryForge-gitea/scripts/douyin-browser-capture
npm install
npx playwright install chromium
npm run control-panel
cd /Users/kris/code/StoryForge-gitea
./scripts/start_douyin_workbench.sh
```
打开
业务页
```text
http://127.0.0.1:3618/workbench
```
完整采集控制台:
```text
http://127.0.0.1:3618
```
这个本地页面现在包含两部分
常用脚本
- 上半部分是浏览器辅助采集控制台
- 下半部分是 `Douyin Workbench`,可直接查看账号列表、商业化账号分析、快照详情、相似账号和对标关系
- 作品工作台支持按 `高分作品 / 最新作品 / 全部作品` 切换,并可按综合分、商业价值、发布时间、播放、点赞、分享、评论排序
```bash
./scripts/start_douyin_workbench.sh
./scripts/status_douyin_workbench.sh
./scripts/stop_douyin_workbench.sh
./scripts/cleanup_debug_ui.sh
```
如果第一次使用,还需要先安装浏览器依赖:
```bash
cd /Users/kris/code/StoryForge-gitea/scripts/douyin-browser-capture
npm install
npx playwright install chromium
```
当前本地页面已经拆成两个入口:
- `/workbench`:业务优先的 `Douyin Workbench`,可直接查看账号列表、商业化账号分析、快照详情、相似账号和对标关系
- `/`:完整浏览器辅助采集控制台,同时保留工作台能力
- 作品工作台支持按 `高分作品 / 最新作品 / 全部作品` 切换,并可按综合分、受欢迎程度、商业价值、发布时间、播放、点赞、分享、评论排序
- 作品列表支持 `视频 / 图文` 类型筛选,并可直接打开原作品链接
- 高分作品支持自动化分析,每条作品卡片下都会展示商业判断、复刻计划、运营动作和风险提醒
或者继续用命令行:

View File

@@ -342,7 +342,8 @@ function sendHtml(res, html) {
res.end(html);
}
function renderPage() {
function renderPage(mode = "full") {
const isWorkbenchMode = mode === "workbench";
return `<!doctype html>
<html lang="zh-CN">
<head>
@@ -375,6 +376,36 @@ function renderPage() {
padding: 32px 20px 48px;
}
h1, h2, h3 { margin: 0; }
.topbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.topbar-links {
display: inline-flex;
gap: 10px;
flex-wrap: wrap;
}
.nav-link {
display: inline-flex;
align-items: center;
padding: 9px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.78);
border: 1px solid rgba(22, 49, 61, 0.1);
color: var(--ink);
text-decoration: none;
font-size: 13px;
font-weight: 600;
}
.nav-link.active {
background: rgba(31, 110, 95, 0.12);
color: var(--accent);
border-color: rgba(31, 110, 95, 0.24);
}
.hero {
background: linear-gradient(135deg, #0b3c5d, #1f6e5f 58%, #b97524);
color: white;
@@ -827,20 +858,31 @@ function renderPage() {
background: rgba(22, 49, 61, 0.1);
margin: 4px 0;
}
.business-mode .capture-grid,
.business-mode .recent-runs-section,
.business-mode .capture-footnote {
display: none;
}
@media (max-width: 900px) {
.grid, .row, .checks, .workbench-layout, .metric-grid, .two-col, .analysis-grid, .video-grid, .video-layout, .toolbar-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>
<body class="${isWorkbenchMode ? "business-mode" : ""}">
<main>
<div class="topbar">
<span class="pill">${isWorkbenchMode ? "StoryForge / Douyin Workbench" : "StoryForge / Douyin Browser Assist"}</span>
<div class="topbar-links">
<a class="nav-link ${isWorkbenchMode ? "active" : ""}" href="/workbench">业务工作台</a>
<a class="nav-link ${isWorkbenchMode ? "" : "active"}" href="/">采集控制台</a>
</div>
</div>
<section class="hero">
<span class="pill">StoryForge / Douyin Browser Assist</span>
<h1 style="margin-top: 14px;">用网页点按钮,驱动真实浏览器采集抖音账号</h1>
<p>这不是无头绕反爬,而是一个可控的半自动流程。你点击“开始采集”后,脚本会打开真实 Chromium会话沿用同一份登录态。你在浏览器里登录或过滑块后回到这里点“已完成登录继续采集”系统就会继续抓取主页、creator-center并按安全规则同步进 StoryForge。</p>
<h1 style="margin-top: 2px;">${isWorkbenchMode ? "用业务工作台直接查看账号结论、作品榜单和运营分析" : "用网页点按钮,驱动真实浏览器采集抖音账号"}</h1>
<p>${isWorkbenchMode ? "这是面向日常运营的业务页。登录后即可查看账号列表、Agent 结论、完整作品工作台、快照和对标结果。只有在需要抓取新账号时,再切到“采集控制台”。" : "这不是无头绕反爬,而是一个可控的半自动流程。你点击“开始采集”后,脚本会打开真实 Chromium会话沿用同一份登录态。你在浏览器里登录或过滑块后回到这里点“已完成登录继续采集”系统就会继续抓取主页、creator-center并按安全规则同步进 StoryForge。"}</p>
</section>
<div class="grid">
<div class="grid capture-grid">
<section class="card stack">
<div>
<h2>开始新采集</h2>
@@ -923,7 +965,7 @@ function renderPage() {
</section>
</div>
<section class="card" style="margin-top: 18px;">
<section class="card recent-runs-section" style="margin-top: 18px;">
<div style="display: flex; justify-content: space-between; gap: 12px; align-items: center;">
<div>
<h2>最近运行</h2>
@@ -1096,6 +1138,7 @@ function renderPage() {
</section>
</div>
</section>
<p class="hint capture-footnote" style="margin-top:14px;">如果你主要是在做账号分析和作品运营,建议直接使用上面的业务工作台;只有在补采新账号时再回到采集控制台。</p>
</main>
<script>
@@ -2188,7 +2231,11 @@ const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "/", "http://127.0.0.1");
try {
if (req.method === "GET" && url.pathname === "/") {
sendHtml(res, renderPage());
sendHtml(res, renderPage("full"));
return;
}
if (req.method === "GET" && url.pathname === "/workbench") {
sendHtml(res, renderPage("workbench"));
return;
}
if (req.method === "GET" && url.pathname === "/api/status") {

View File

@@ -0,0 +1,35 @@
#!/bin/sh
set -eu
ROOT="$(CDPATH= cd -- "$(dirname "$0")/.." && pwd)"
PORT="${DOUYIN_WORKBENCH_PORT:-3618}"
SCRIPT="$ROOT/scripts/douyin-browser-capture/control_panel.mjs"
LOG_FILE="${DOUYIN_WORKBENCH_LOG:-/tmp/storyforge-douyin-workbench.log}"
if lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
echo "douyin workbench already running: http://127.0.0.1:$PORT/workbench"
exit 0
fi
nohup env PORT="$PORT" node "$SCRIPT" >"$LOG_FILE" 2>&1 &
python3 - <<'PY'
import os
import time
import urllib.request
port = os.environ.get("PORT", "3618")
url = f"http://127.0.0.1:{port}/workbench"
deadline = time.time() + 15
last_error = ""
while time.time() < deadline:
try:
with urllib.request.urlopen(url, timeout=3) as resp:
print(f"douyin workbench ready: {resp.status} {url}")
raise SystemExit(0)
except Exception as exc:
last_error = str(exc)
time.sleep(0.5)
print(f"douyin workbench start timeout: {last_error}")
raise SystemExit(1)
PY

View File

@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
PORT="${DOUYIN_WORKBENCH_PORT:-3618}"
if ! lsof -nP -iTCP:"$PORT" -sTCP:LISTEN >/dev/null 2>&1; then
echo "douyin workbench stopped"
exit 1
fi
python3 - <<'PY'
import os
import urllib.request
port = os.environ.get("PORT", "3618")
for path in ("/workbench", "/"):
url = f"http://127.0.0.1:{port}{path}"
with urllib.request.urlopen(url, timeout=5) as resp:
print(f"{path}: {resp.status}")
PY

View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -eu
PORT="${DOUYIN_WORKBENCH_PORT:-3618}"
lsof -tiTCP:"$PORT" -sTCP:LISTEN | xargs -r kill
echo "douyin workbench stopped: $PORT"