账号列表重置按钮改更新按钮(⭐已关注批量重抓,确认弹窗+逐账号串行任务);AI写脚本页tab与搜索筛选移至标题下同一行(还原R26误并入标题行);状态列已解析去图标改浅绿
This commit is contained in:
1 parent
d874dcc386
commit
1a68637de8
5 files changed
+187
-17
No files matched your search
@@ -207,3 +207,17 @@
|
||||
- data-pages.js:.rw-tabs div 从 toolbar 下独立行移入 .page-toolbar(返回|标题|tabs|toolbar-right 筛选右),加 rw-toolbar-tabs 类;rw-note 留在 tab 行下方表上方
|
||||
- style.css:.page-toolbar .rw-tabs 行内化(border-bottom none/margin 0/padding 0/center),tab 压缩 13px;tab-active 保留下划线条
|
||||
- 纯静态改动无需重启;git 待查;版本 ?v=20260903h
|
||||
|
||||
## R27a AI写脚本页布局修正(09-03,R26 返工)
|
||||
- 用户澄清:R26「tab 与筛选同行」≠ 并入标题行;且追加要求筛选搜索也移下标题行
|
||||
- 终局结构:page-toolbar 只留 返回|标题「AI写脚本」;其下 .rw-tabs 单行 = 左三个 tab + 右 toolbar-right(搜索/时间/状态,margin-left:auto),rw-tabs 加 align-items:center
|
||||
- 还原教训:R26 把 tabs 并入 toolbar 标题行属误读「放一行」,用户两次纠正(先 tab 放下、再筛选也放下)
|
||||
- 状态列:ref tab「✅已解析」去图标改浅绿文字(.st-parsed #52b788,font-weight 500);「未解析」保持默认
|
||||
|
||||
## R27 账号列表 重置按钮 → 更新按钮(09-03,口径已确认)
|
||||
- 口径(AskUserQuestion 确认):范围=⭐ 已关注(favorite=1 共 3 个:做夢/平凡的胡德禄/旧梦留声机);交互=确认弹窗后执行
|
||||
- data-pages.js:L700 accReset(纯 refresh 图标)→ accUpdate「refresh 图标 + 更新」;L733 重置绑定 → updateFollowedAccounts(btn)
|
||||
- 新函数 updateFollowedAccounts(renderAccounts 后,文件级):GET /api/dsh/accounts pageSize=100 → filter favorite → 自建 modal 确认(confirm-target 列名单)→ 逐账号串行 POST /api/run(prompt=SKILL_HINT_ACCOUNT +「更新已登记账号」语义:浏览器重抓账号信息+视频列表覆盖更新、身份标识禁库中旧值回填;name='更新账号·X';skills mcn-dou-analysis)→ 轮询 /api/run/status(每 5s,单任务 20min 上限;done/error/review/notfound/超时 五终态)→ 按钮「更新中 i/N…」→ 汇总 toast + refreshCurrentPage
|
||||
- 串行理由:多会话并发会抢浏览器 9223 调试实例;executeTask 单按钮封装不适配 N 任务,自写轮询
|
||||
- 未验证:首次实点后需核对任务侧能否覆盖更新 hot_accounts 主表(server.js importAccountIntoDb 不写 hot_accounts 粉丝/获赞/作品数——只 upsert 视频解析/人设/视频列表/拆解;技能侧登记链路待实测)
|
||||
- 纯静态改动无需重启;版本 ?v=20260903j
|
||||
@@ -0,0 +1,87 @@
|
||||
// R27 冒烟 v3:AI写脚本页布局(标题行干净/tab+筛选下行)+ 状态列浅绿 + 账号列表「更新」按钮弹窗
|
||||
// 注意:hash 不能带 ?query(会被路由当作账号名 scope 过滤)→ 切页用 location.hash
|
||||
const list = await fetch('http://localhost:9225/json').then((r) => r.json());
|
||||
const page = list.find((t) => t.type === 'page');
|
||||
if (!page) { console.log('ERR: no page'); process.exit(1); }
|
||||
const ws = new WebSocket(page.webSocketDebuggerUrl);
|
||||
let id = 0; const pending = new Map();
|
||||
const send = (method, params = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method, params })); });
|
||||
ws.onmessage = (e) => { const m = JSON.parse(e.data); if (m.id && pending.has(m.id)) { const p = pending.get(m.id); pending.delete(m.id); m.error ? p.rej(new Error(m.error.message)) : p.res(m.result); } };
|
||||
await new Promise((r) => (ws.onopen = r));
|
||||
const ev = async (expr) => { const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true }); if (r.exceptionDetails) return { __err: (r.exceptionDetails.exception?.description || r.exceptionDetails.text).slice(0, 200) }; return r.result.value; };
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
await send('Page.enable'); await send('Runtime.enable');
|
||||
const waitFor = async (expr, ms = 12000) => { const t0 = Date.now(); while (Date.now() - t0 < ms) { if (await ev(expr)) return true; await sleep(300); } return false; };
|
||||
const fail = (m) => { console.log('FAIL@' + m); process.exit(1); };
|
||||
const goto = async (hash) => { await ev(`location.hash = ${JSON.stringify(hash)}`); await sleep(600); };
|
||||
|
||||
/* ============ 0) 先加载应用 ============ */
|
||||
await send('Page.navigate', { url: 'http://localhost:8900/#/' });
|
||||
if (!(await waitFor(`!!document.querySelector('#view')`))) fail('app boot');
|
||||
|
||||
/* ============ 1) AI写脚本页布局 + 状态列 ============ */
|
||||
await goto('#/rewrites');
|
||||
if (!(await waitFor(`!!document.querySelector('#view .rw-tabs')`))) fail('rw-tabs render');
|
||||
if (!(await waitFor(`!!document.querySelector('#view table tbody tr')`))) fail('table rows: ' + JSON.stringify(await ev(`document.querySelector('#view .table-wrap') ? document.querySelector('#view .table-wrap').textContent.slice(0,80) : 'no wrap'`)));
|
||||
const rw = await ev(`(() => {
|
||||
const tb = document.querySelector('#view .page-toolbar');
|
||||
const tabs = document.querySelector('#view .rw-tabs');
|
||||
const tr = tabs.querySelector('.toolbar-right');
|
||||
return {
|
||||
toolbarChildren: [...tb.children].map((c) => (c.className && String(c.className)) || c.tagName),
|
||||
toolbarHasFilter: !!tb.querySelector('.toolbar-right'),
|
||||
toolbarHasTabs: !!tb.querySelector('.rw-tabs'),
|
||||
tabsIsNextSibling: tb.nextElementSibling === tabs,
|
||||
tabsCount: tabs.querySelectorAll('button[data-tab]').length,
|
||||
tabsRowRight: !!tr,
|
||||
rightInputs: tr ? [...tr.querySelectorAll('input,select')].map((x) => x.id) : [],
|
||||
parsedCells: [...document.querySelectorAll('#view tbody td')].filter((td) => td.textContent.trim() === '已解析').slice(0, 3).map((td) => {
|
||||
const sp = td.querySelector('span.st-parsed');
|
||||
return { hasSpan: !!sp, color: sp ? getComputedStyle(sp).color : null };
|
||||
}),
|
||||
hasEmojiParsed: document.querySelector('#view tbody').textContent.includes('✅已解析'),
|
||||
};
|
||||
})()`);
|
||||
|
||||
/* ============ 2) 账号列表「更新」按钮 + 弹窗 ============ */
|
||||
await goto('#/accounts');
|
||||
// 全新测试 profile 无 localStorage:应用可能自动弹出「设置产出根目录」弹窗 → 先清掉杂项弹窗,避免误匹配
|
||||
await ev(`document.querySelectorAll('.modal-mask').forEach((m) => m.remove())`);
|
||||
if (!(await waitFor(`!!document.querySelector('#accUpdate')`))) fail('accUpdate render');
|
||||
const acc = await ev(`(() => {
|
||||
const b = document.querySelector('#accUpdate');
|
||||
const r = { txt: b.textContent.trim(), hasIcon: !!b.querySelector('svg'), title: b.title };
|
||||
b.click();
|
||||
return r;
|
||||
})()`);
|
||||
await sleep(300);
|
||||
if (!(await waitFor(`[...document.querySelectorAll('.modal-mask .modal')].some((m) => m.textContent.includes('更新关注账号'))`))) fail('update modal open');
|
||||
const md = await ev(`(() => {
|
||||
const m = [...document.querySelectorAll('.modal-mask .modal')].find((x) => x.textContent.includes('更新关注账号'));
|
||||
const r = {
|
||||
title: m.querySelector('h3').textContent.trim(),
|
||||
target: m.querySelector('.confirm-target') ? m.querySelector('.confirm-target').textContent.trim() : null,
|
||||
desc: m.querySelector('p.dim').textContent.trim(),
|
||||
hasCancel: !!m.querySelector('[data-cancel]'),
|
||||
hasOk: !!m.querySelector('[data-ok]'),
|
||||
maskCount: document.querySelectorAll('.modal-mask').length,
|
||||
};
|
||||
m.querySelector('[data-cancel]').click();
|
||||
return r;
|
||||
})()`);
|
||||
await sleep(400);
|
||||
const closed = await ev(`![...document.querySelectorAll('.modal-mask')].some((m) => m.textContent.includes('更新关注账号'))`);
|
||||
|
||||
console.log(JSON.stringify({ rw, acc, md, closed }, null, 1));
|
||||
const ok = rw && !rw.__err
|
||||
&& rw.toolbarHasFilter === false && rw.toolbarHasTabs === false
|
||||
&& rw.tabsIsNextSibling === true && rw.tabsCount === 3
|
||||
&& rw.tabsRowRight === true && rw.rightInputs.length >= 2
|
||||
&& rw.hasEmojiParsed === false
|
||||
&& rw.parsedCells.length > 0 && rw.parsedCells.every((c) => c.hasSpan && c.color === 'rgb(82, 183, 136)')
|
||||
&& acc && !acc.__err && acc.txt === '更新' && acc.hasIcon
|
||||
&& md && !md.__err && md.title.includes('更新关注账号') && md.target.includes('做夢') && md.target.includes('胡德禄') && md.target.includes('旧梦')
|
||||
&& md.hasCancel && md.hasOk && closed === true;
|
||||
console.log(ok ? '\n=== PASS ===' : '\n=== CHECK ===');
|
||||
ws.close();
|
||||
process.exit(ok ? 0 : 1);
|
||||
@@ -697,7 +697,7 @@ async function renderAccounts(view) {
|
||||
<select id="accTrack" class="input-select"><option value="">全部赛道</option>${tracks.map((t) => `<option value="${esc(t)}" ${t === state.track ? 'selected' : ''}>${esc(t)}</option>`).join('')}</select>
|
||||
<input id="accSearch" class="input" placeholder="搜索账号名 / 内容 / 赛道" value="${esc(state.search)}">
|
||||
<button class="btn" id="accImport" title="添加账号数据(抖音账号信息 / 账号地址)">${svgIcon('user-plus', 15, 'currentColor')} 添加</button>
|
||||
<button class="btn" id="accReset" title="重置筛选">${svgIcon('refresh', 15, 'currentColor')}</button>
|
||||
<button class="btn" id="accUpdate" title="重新抓取所有已关注账号(⭐)的最新账号数据与视频列表(后台任务逐个执行)">${svgIcon('refresh', 15, 'currentColor')} 更新</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
@@ -730,7 +730,7 @@ async function renderAccounts(view) {
|
||||
view.querySelector('#accType').addEventListener('change', (e) => { state.type = e.target.value; state.page = 1; load(); });
|
||||
view.querySelector('#accSearch').addEventListener('keydown', (e) => { if (e.key === 'Enter') { state.search = e.target.value; state.page = 1; load(); } });
|
||||
view.querySelector('#accTrack').addEventListener('change', (e) => { state.track = e.target.value; state.page = 1; load(); });
|
||||
view.querySelector('#accReset').addEventListener('click', () => { state = { type: 'all', search: '', track: '', page: 1, pageSize: 20, batch: false }; load(); });
|
||||
view.querySelector('#accUpdate').addEventListener('click', () => updateFollowedAccounts(view.querySelector('#accUpdate')));
|
||||
view.querySelector('#accImport').addEventListener('click', (e) => openImportAccounts(e.currentTarget));
|
||||
bindPager(view, (p) => { state.page = p; load(); });
|
||||
bindBatchBar(view, {
|
||||
@@ -746,6 +746,76 @@ async function renderAccounts(view) {
|
||||
await load();
|
||||
}
|
||||
|
||||
/* ===== 账号列表「更新」按钮(09-03 R27):重新抓取所有已关注账号的最新账号数据与视频列表 =====
|
||||
范围 = ⭐ 已关注(favorite=1);后台任务逐账号串行执行(多会话并发会抢浏览器调试实例);
|
||||
每个任务复用 mcn-dou-analysis 技能流程(SKILL_HINT_ACCOUNT 同款技能加载+红线),任务语义改为「更新已登记账号」:
|
||||
身份标识(sec_uid/抖音号/地址)一律以本次浏览器抓取为准,禁止沿用库中旧值回填 */
|
||||
function updateFollowedAccounts(btn) {
|
||||
(async () => {
|
||||
try {
|
||||
const d = await api('/api/dsh/accounts?type=all&search=&track=&page=1&pageSize=100');
|
||||
const favs = ((d && d.items) || []).filter((a) => a.favorite);
|
||||
if (!favs.length) { showToast('⭐ 暂无已关注账号(先点击列表中的 ☆ 关注)'); return; }
|
||||
const names = favs.map((a) => String(a.account_name || '').trim()).filter(Boolean);
|
||||
if (!names.length) { showToast('已关注账号缺少账号名,无法更新'); return; }
|
||||
const mask = document.createElement('div'); mask.className = 'modal-mask';
|
||||
mask.innerHTML = `
|
||||
<div class="modal" style="max-width:540px">
|
||||
<h3>${svgIcon('refresh', 18)} 更新关注账号</h3>
|
||||
<p class="dim">将重新抓取 ${names.length} 个已关注账号的最新账号数据与视频列表(后台任务逐个执行,每个需数分钟,请耐心等待,可到左侧会话栏查看进度)。确认更新?</p>
|
||||
<div class="confirm-target">${names.map((n) => esc(n)).join('、')}</div>
|
||||
<div class="modal-actions">
|
||||
<button class="ghost" data-cancel>取消</button>
|
||||
<button class="primary" data-ok>确认更新</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(mask);
|
||||
const close = () => mask.remove();
|
||||
mask.addEventListener('click', (e) => { if (e.target === mask) close(); });
|
||||
mask.querySelector('[data-cancel]').addEventListener('click', close);
|
||||
mask.querySelector('[data-ok]').addEventListener('click', async () => {
|
||||
close();
|
||||
const oldHtml = btn ? btn.innerHTML : '';
|
||||
const restore = () => { if (btn) { btn.disabled = false; btn.innerHTML = oldHtml; btn.classList.remove('btn-busy'); } };
|
||||
if (btn) { btn.disabled = true; btn.classList.add('btn-busy'); }
|
||||
const done = [], failed = [];
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
const name = names[i];
|
||||
if (btn) btn.innerHTML = `更新中 ${i + 1}/${names.length}…`;
|
||||
const prompt = `${SKILL_HINT_ACCOUNT}\n请更新抖音账号「${name}」的数据:该账号已在本工作台账号库中登记,属数据更新场景(非新增)。通过抖音网页重新获取该账号最新的账号信息(粉丝/获赞/作品数/简介等)与最新视频列表,并覆盖更新该账号的登记数据与视频列表记录。账号身份标识(sec_uid/抖音号/抖音地址)一律以本次浏览器抓取到的为准,禁止沿用库中旧值回填。`;
|
||||
try {
|
||||
const r = await api('/api/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt, name: '更新账号·' + name, skills: ['mcn-dou-analysis'] }),
|
||||
});
|
||||
if (r.error) throw new Error(r.error);
|
||||
const taskId = r.id;
|
||||
const deadline = Date.now() + 20 * 60 * 1000;
|
||||
let state = 'running';
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((res) => setTimeout(res, 5000));
|
||||
let s = null;
|
||||
try { s = await api('/api/run/status?id=' + encodeURIComponent(taskId)); } catch (e) { continue; }
|
||||
if (!s || s.state === 'notfound') { state = 'done'; break; }
|
||||
if (s.state === 'done') { state = 'done'; break; }
|
||||
if (s.state === 'error' || s.state === 'review') { state = s.state; break; }
|
||||
}
|
||||
if (state === 'done') done.push(name);
|
||||
else failed.push(name + (state === 'error' ? '(任务失败)' : (state === 'review' ? '(待确认)' : '(超时)')));
|
||||
} catch (e) { failed.push(name + '(' + e.message + ')'); }
|
||||
}
|
||||
restore();
|
||||
if (btn) btn.blur();
|
||||
showToast(done.length ? `✅ 已更新 ${done.length} 个关注账号${failed.length ? `;${failed.length} 个未完成:${failed.join('、')}` : ''}` : '❌ 更新未完成:' + failed.join('、'));
|
||||
refreshCurrentPage();
|
||||
});
|
||||
} catch (e) {
|
||||
showToast('❌ 获取账号列表失败: ' + e.message);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/* ================= 账号详情 ================= */
|
||||
async function renderAccountDetail(view, id) {
|
||||
const accountId = Number(id);
|
||||
@@ -1077,7 +1147,7 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
|
||||
<td>${fmtWan(v.share_count)}</td>
|
||||
<td>${fmtDuration(v.duration)}</td>
|
||||
<td>${fmt(v.publish_time)}</td>
|
||||
<td>${(v.has_source || v.has_analysis) ? '✅已解析' : '未解析'}</td>
|
||||
<td>${(v.has_source || v.has_analysis) ? '<span class="st-parsed">已解析</span>' : '未解析'}</td>
|
||||
<td>${v.rewrite_count > 0 ? `<button class="btn btn-sm btn-view" data-detail="${v.id}" title="查看该视频下的 AI 脚本(源脚本对比/分镜)">${v.rewrite_count} 查看</button>` : '—'}</td>
|
||||
<td class="cell-act"><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: cleanTitle(v.video_title), aweme: v.aweme_id || '', accountId: v.account_id, parsed: !!(v.has_source || v.has_analysis), url: v.video_url || '' }))}'>AI创作</button></td>
|
||||
</tr>`).join('');
|
||||
@@ -1129,12 +1199,12 @@ async function renderRewrites(view, initAccount) { /* v22:initAccount=账号
|
||||
<div class="page-toolbar">
|
||||
${toolbarBack()}
|
||||
<span class="page-title">AI写脚本</span>
|
||||
<div class="rw-tabs rw-toolbar-tabs">
|
||||
<button class="${tabCls('ref')}" data-tab="ref" title="有原视频:以视频行为选题来源创作,脚本保存后挂在该视频下">参考选题创作<span class="rw-tab-badge">${state.stats.videoScripts}</span></button>
|
||||
<button class="${tabCls('custom')}" data-tab="custom" title="无原视频:账号列表「AI创作」弹窗等创作,脚本保存后自动入库">自定义选题创作<span class="rw-tab-badge">${state.stats.customScripts}</span></button>
|
||||
<button class="${tabCls('abc')}" data-tab="abc" title="多视频爆款元素融合创作(开发中)">ABC融合创作</button>
|
||||
</div>
|
||||
<div class="toolbar-right" style="margin-right:10px">${toolbarCtrl}</div>
|
||||
</div>
|
||||
<div class="rw-tabs">
|
||||
<button class="${tabCls('ref')}" data-tab="ref" title="有原视频:以视频行为选题来源创作,脚本保存后挂在该视频下">参考选题创作<span class="rw-tab-badge">${state.stats.videoScripts}</span></button>
|
||||
<button class="${tabCls('custom')}" data-tab="custom" title="无原视频:账号列表「AI创作」弹窗等创作,脚本保存后自动入库">自定义选题创作<span class="rw-tab-badge">${state.stats.customScripts}</span></button>
|
||||
<button class="${tabCls('abc')}" data-tab="abc" title="多视频爆款元素融合创作(开发中)">ABC融合创作</button>
|
||||
<div class="toolbar-right" style="margin-left:auto">${toolbarCtrl}</div>
|
||||
</div>
|
||||
<div class="rw-note">${note}</div>
|
||||
${bodyHtml}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MCN工作台</title>
|
||||
<link rel="stylesheet" href="style.css?v=20260903h">
|
||||
<link rel="stylesheet" href="style.css?v=20260903j">
|
||||
<script src="vendor/echarts.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -41,7 +41,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js?v=20260903h"></script>
|
||||
<script src="data-pages.js?v=20260903h"></script>
|
||||
<script src="app.js?v=20260903j"></script>
|
||||
<script src="data-pages.js?v=20260903j"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -650,15 +650,14 @@ td.td-title { max-width: 240px; }
|
||||
.cb-cell input { width: 15px; height: 15px; cursor: pointer; vertical-align: middle; accent-color: var(--primary); }
|
||||
|
||||
/* ---- AI写脚本 三 tab(09-02 v25:参考选题创作 / 自定义选题创作 / ABC融合创作) ---- */
|
||||
.rw-tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin: -12px 0 12px; padding: 0 4px; }
|
||||
/* 标题行(返回|标题)下方独立一行:左=tab 切换,右=搜索/时间/状态筛选(tab 与筛选同一行,均不与标题/返回同行) */
|
||||
.rw-tabs { display: flex; align-items: center; gap: 2px; border-bottom: 1px solid var(--border); margin: -12px 0 12px; padding: 0 4px; }
|
||||
.rw-tabs .tab { font-size: 15px; padding: 9px 14px; }
|
||||
/* AI写脚本页:tab 并入 page-toolbar 同一行(与搜索/筛选同行),去整行分隔条 */
|
||||
.page-toolbar .rw-tabs { border-bottom: none; margin: 0; padding: 0; align-self: center; }
|
||||
.page-toolbar .rw-tabs .tab { padding: 4px 10px 3px; font-size: 13px; margin-bottom: 0; }
|
||||
.page-toolbar .rw-tabs .tab-active { border-bottom: 2px solid var(--primary); }
|
||||
.rw-tab-badge { display: inline-block; min-width: 18px; margin-left: 6px; padding: 0 6px; border-radius: 10px; background: var(--primary-soft); color: var(--primary); font-size: 12px; line-height: 18px; font-weight: 600; text-align: center; }
|
||||
.tab-active .rw-tab-badge { background: var(--primary); color: #fff; }
|
||||
.rw-note { font-size: 12px; color: var(--dim); margin: 0 0 12px; }
|
||||
/* 视频状态「已解析」:浅绿色文字(去图标),区别于默认灰「未解析」 */
|
||||
.st-parsed { color: #52b788; font-weight: 500; }
|
||||
.rw-chip { display: inline-flex; align-items: center; gap: 2px; max-width: 260px; padding: 2px 8px 2px 12px; background: var(--primary-soft); color: var(--primary); border-radius: 999px; font-size: 13px; }
|
||||
.rw-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rw-chip button { border: none; background: none; color: inherit; cursor: pointer; font-size: 15px; line-height: 1; padding: 0 3px; border-radius: 50%; }
|
||||
|
||||
Reference in new issue
Block a user