fix(MCN工作台): 修复刷新回首页(P0) + 周榜页全面对齐首页交互
- 根因: app.js 先于 data-pages.js 加载,boot 立即 router() 时 DataPages 未定义 → 数据页路由被当未知路由 → location.hash='#/' 回首页 - 修复: boot 改 DOMContentLoaded + 50ms 轮询等 DataPages 就绪再路由;router 兜底保护(数据页名+DataPages未定义不跳首页) - 验证: browser-harness 真实验证(独立Chrome 9223调试实例) 直接访问/刷新/切换全部通过 - 首页周榜: 新增板块(前10+赛道日期筛选)、综合得分倒序排序、9列(新增粉丝/点赞/评论/分享+功能列)、导入按钮、去分页 - 完整周榜页(#/ranking): 对齐首页(赛道列前置、工具栏/分页右对齐、综合得分倒序+橙色、功能列居中、导入/查看按钮区分) - 路由记忆: localStorage+cookie 双写,刷新后恢复上次页面 - 其他: AI写剧本→AI生成脚本、全部字体按比例放大+2(取整无小数)、外链新标签打开、btn-import/btn-view 样式
This commit is contained in:
1 parent
63e8a66f4f
commit
79ae403b7b
9 files changed
+325
-102
No files matched your search
@@ -6,3 +6,5 @@ Thumbs.db
|
||||
.git_bak/
|
||||
# browser-harness 的 Windows conda 环境(191M,二进制环境文件)不提交,源码正常提交
|
||||
**/browser-harness/envs/
|
||||
# WorkBuddy 自动化任务运行数据(本地运行时生成,不提交)
|
||||
.workbuddy/automations/
|
||||
@@ -127,7 +127,39 @@
|
||||
- **验证**:vm 模拟跨 script 全局,确认无冲突;node -c 双文件语法过;curl 确认 fmtDate 3 处 / fmtTime 零残留 ✅
|
||||
- **教训**:多 script 文件共享全局作用域,`function 声明` 与 `const/let` 同名会跨文件冲突(node -c 单文件查不出);给全局工具函数命名前应 grep 全项目避免与其它文件撞名
|
||||
|
||||
## 19:25 工作台触发的 AI 会话归入 mcn-workshop 空间分组
|
||||
- **需求**:工作台功能创建的 AI 会话显示到 mcn-workshop 空间分组
|
||||
- **机制**:WorkBuddy 左侧会话栏按 cwd(工作目录)分组;workspaces 表每行一个空间(path+last_opened_at)
|
||||
- **实现**:①创建目录 `D:\AgentSkill\mcn-workshop` 并插入 workspaces 表(路径格式必须与其他行一致 `D:\AgentSkill\xxx`;bash MSYS 会把反斜杠转正斜杠,需用 Write 写 node 脚本执行避开);②server.js `/api/run` 的 cwd 改为 `D:\\AgentSkill\\mcn-workshop`(cwds JSON 数组)
|
||||
- **验证**:POST /api/run 后 cwds=`["D:\\AgentSkill\\mcn-workshop"]` ✅;WorkBuddy 消费后新会话 cwd=`D:\AgentSkill\mcn-workshop`、is_background_automation=1 ✅(会话 3b28ab3c)
|
||||
- **注意**:8899 端口曾被旧实例/多实例占用(避让到 8900/8901),按端口清理 node 进程后恢复单一实例;MiPCAudio 占 0.0.0.0:8899 需排除(PID 20604)
|
||||
|
||||
## 12:31 同步到远程仓库
|
||||
- 提交 5f5b4f1(120 文件,含工作台 mcn-work-shop/ + impeccable/taste-skill 设计技能 + 记忆/文档)
|
||||
- 修正 .gitignore:`mcn-work-shop/mcn-plugin.db` → `**/mcn-work-shop/mcn-plugin.db`(带斜杠规则会锚定根目录,未匹配到 project/ 子路径,导致数据库副本被误暂存,已 git rm --cached)
|
||||
- push 成功 f025ad3..5f5b4f1;origin/main 跟踪引用照旧被沙箱拦截丢失,已手动重建 refs/remotes/origin/main
|
||||
|
||||
## 12:38 browser-harness 源码纳入版本管理(仅排除 Windows 环境)
|
||||
- **用户需求**:仓库除 browser-harness 的 Windows 环境文件外,其余文件都要提交
|
||||
- **检查发现**:browser-harness 是嵌套 git 仓库(clone 自 browser-use/browser-harness,main@f5eaf90),根 .gitignore 的 `browser-harness/` 把整个目录都忽略了,导致 181 个源码文件(SKILL.md/README/src/skills/tests/docs,仅 460K)也没提交,不只是 envs 环境
|
||||
- **用户决策**:删除嵌套 .git,envs 环境继续忽略
|
||||
|
||||
## 20:2x-20:3x 工作台路由刷新回首页问题(P0 排查修复 + browser-harness 真实验证)
|
||||
- **症状**:访问 http://localhost:8899/#/ranking 刷新后 URL 变 #/ 回首页;外部浏览器/WorkBuddy 面板均复现;点击进入正常
|
||||
- **排查过程**:①怀疑 iframe/sessionStorage(排除,外部浏览器也一样)→ ②Edge headless 复现但 virtual-time 与 await 冲突(首页同步渲染正常、数据页 await 挂起空 view,误导)→ ③注入探针发现 headless 加载 #/ranking 时 location.hash 变 #/(headless 特性)→ ④**browser-harness 真实验证**(用户提示可用,环境在 mcn-dou-analysis/subskills/browser-harness,用 PYTHONPATH=src + python -m browser_harness.run + BU_CDP_URL=http://127.0.0.1:9223 独立 Chrome 调试实例)→ ⑤诊断 __diag 实锤:**setTimeout(0) 时 dp=undefined(data-pages.js 未加载完)**
|
||||
- **真正根因**:script 标签下载**并行**——app.js 先下载完执行,其末尾 boot 立即 router()(或 setTimeout(0))时 **window.DataPages 尚未定义**(data-pages.js 还在下载/未执行)→ 数据页路由被当未知路由 → `location.hash='#/'` 兜底回首页(URL 变 #/)。点击进入正常因页面运行中两 JS 已加载
|
||||
- **修复**:boot 改为 **DOMContentLoaded + 50ms 轮询等 DataPages 就绪再 router()**;router 兜底加保护(数据页名 + DataPages undefined 时不跳首页,等待下次)
|
||||
- **验证(browser-harness 实测)**:直接访问 #/ranking 渲染周榜页 ✅;#/ranking 刷新停留 ✅;首页↔周榜切换 ✅;周榜 20 行完整 ✅
|
||||
- **环境调用法(重要复用)**:BH=mcn-dou-analysis/subskills/browser-harness;`PYTHONPATH="$BH/src" "$BH/envs/browser-harness/python.exe" -m browser_harness.run`(heredoc 传 Python);独立调试 Chrome:`chrome.exe --remote-debugging-port=9223 --user-data-dir=...` + `BU_CDP_URL=http://127.0.0.1:9223`;PYTHONPATH 必须指向 src/(含 browser_harness 包)
|
||||
- **执行**:rm -rf browser-harness/.git(2.7M);.gitignore `browser-harness/` → `**/browser-harness/envs/`;git add -A 提交 175 源码 + .gitignore 共 176 文件
|
||||
- **继续忽略**:envs/(191M conda 环境)、src/browser_harness.egg-info/(Python 打包元数据,6 文件)、__pycache__/
|
||||
- **提交**:9248cbe,push 025d28f..9248cbe 成功,跟踪引用照旧重建,本地与远程一致
|
||||
|
||||
## 19:17 工作台按钮直接触发 WorkBuddy AI 会话(Automation 方案,推翻复制 prompt 结论)
|
||||
- **用户纠正**:之前结论「WorkBuddy 无网页→会话触发接口,按钮只能复制 prompt」有误。用户提供 POC 文档(桌面「新建 文本文档 (2).txt」)+ 明确要求用 **Automation 方案**(非 CLI),让任务在左侧会话栏可见
|
||||
- **dsh 插件参考**(D:/dshworkspace/plugin_package/dsh-plugin-mcn):按钮 → POST /mcn/api/creative {prompt} → `getOrCreateCreativeAgent(sid)` 固定会话 + `agent.followup(userMessage)` 触发执行;执行中 409 防重;用户到 AI会话视图查看
|
||||
- **WorkBuddy Automation 链路(实测验证)**:往 `~/.workbuddy/workbuddy.db` 的 `automations` 表写 once 任务(scheduled_at=now)→ WorkBuddy 运行时自动消费(19:16:22 实测)→ 创建会话(automation_runs.thread_id / runtime_state.running_conversation_id)→ 左侧会话栏可见 → 结果落 automation_runs(result_success=1)
|
||||
- **automations 表关键字段**(用 automation_update 工具创建样本反推):id=`automation-{毫秒}`、status=ACTIVE、schedule_type=once、next_run_at=毫秒、scheduled_at=`YYYY-MM-DDTHH:mm`(本地无秒)、cwds=JSON数组、created_at/updated_at=毫秒、skills_json/connector_ids_json='[]'、model_id=deepseek-v4-flash、permission_mode=fullAccess、owner_user_id=当前用户ID(sessions.user_id)、owner_status=confirmed
|
||||
- **实现**:server.js 加 `POST /api/run`(node:sqlite 写 automations,owner_user_id 从 sessions 最新取,cwd=D:/AgentSkill/mcn-video-script);data-pages.js firePrompt 从复制改为提交执行 + 5 秒防连点;SKILL.md 工作台章节 5 更新
|
||||
- **验证**:curl POST /api/run 成功(automation-1787829463480),WorkBuddy 8 秒内消费(run IN_PROGRESS + 新会话 a16583b8)✅;node:sqlite 写 workbuddy.db 用事务测试无污染
|
||||
- **注意**:CLI 方案(codebuddy -p)也验证可用但需 `--model auto`(默认模型 500),用户最终选用 Automation 方案
|
||||
@@ -11,7 +11,7 @@
|
||||
## 核心技能清单
|
||||
- **短视频脚本创作** (`project/短视频脚本创作/V1.0/` + `Lite1.0/`):11步流程+知识库12分类(66文件);V1.0 完整版含 MCP/Dify 接口,Lite1.0 去全部接口、搜索用模型自带 WebSearch。扁平化定式=去「技能名」层、留「项目名」层(`project/项目名/{版本}/`);08-26 曾误去项目名层已回改
|
||||
- **同步策略(08-26 17:34 用户定)**:后续所有改动只改 V1.0 源技能 + dsh 副本两处;**Lite1.0 默认不动**(有需要再单独处理,其特化差异=去MCP/拆解贴文案/无工具接口块)
|
||||
- **MCN工作台(08-27 新增,仅 V1.0)**:`project/短视频脚本创作/V1.0/mcn-work-shop/`(server.js+public/,零依赖 Node 服务,端口8899自动避让);触发语「打开工作台」→启动服务+内置浏览器打开 http://localhost:8899;两页签=🧭技能导航(5大功能块+典型问法+创作模式)+📁产出浏览(默认读桌面 MCNSkill项目/ 账号层直挂结构,⚙️设置可加其他目录如 D:\dshworkspace,md/json 在线预览、xlsx 下载打开);路径白名单防穿越;**用户明确不同步 dsh/Lite**(SKILL.md/帮助文档只改 V1.0,dsh 副本保持无此功能,属刻意 divergence)
|
||||
- **MCN工作台(08-27 新增,仅 V1.0)**:`project/短视频脚本创作/V1.0/mcn-work-shop/`(server.js+public/,零依赖 Node 服务,端口8899自动避让);触发语「打开工作台」→启动服务+内置浏览器打开 http://localhost:8899;hash 路由:首页(功能入口卡+AI写剧本快捷卡)+数据功能页(📈周榜/👥热门账号/账号详情/视频详情/AI写脚本/AI写作复盘,读工作台自有 mcn-plugin.db 副本+dshworkspace 抖音榜单 JSON)+📁产出浏览(桌面 MCNSkill项目/ 账号层直挂)+❓帮助页(5功能块问法复制);**AI 任务触发(08-27 Automation 方案定版;19:25 归 mcn-workshop 空间)**:数据页「导入/创作/解析/改写/提炼/复盘」按钮 → 后端 `POST /api/run` → 写 `~/.workbuddy/workbuddy.db` automations 表(once+now,cwds=`D:\AgentSkill\mcn-workshop`)→ WorkBuddy 自动执行创建会话(cwd=mcn-workshop)→ **左侧会话栏 mcn-workshop 空间分组可见**(左侧按 cwd 分组,workspaces 表注册空间);曾错误降级为复制 prompt 已推翻;参考 dsh-plugin-mcn /mcn/api/creative 的 followup 模式;路径白名单防穿越;**用户明确不同步 dsh/Lite**(SKILL.md/帮助文档只改 V1.0,dsh 副本保持无此功能,属刻意 divergence)
|
||||
- **抖音账号分析 mcn-dou-analysis(主版本,08-27 用户宣布)**:源技能=`project/短视频脚本创作/V1.0/subskill/mcn-dou-analysis/`(SKILL.md 19KB);dsh 副本=`C:\Users\maidou\.dsh\skills\mcn-dou-analysis\`;**旧路径 `project/短视频账号分析/case_1/1_账号分析方法/` 已废弃**(08-26 迁移整合,旧三模式/12维度框架已被五大功能体系取代)。五大功能:F1获取账号信息/F2获取视频列表(13列Excel)/F3提炼账号设定(人设卡统一12段)/F4分析视频数据(逐条拆解→汇总两段式)/F5解析视频(单视频MCP);红线要点:达人账号表15列(仅用户要求才增改)、TOP6筛选(点赞+分享降序,唯一权威参数在SKILL.md)、≥60条停止滚动、29赛道枚举、源技能只读单向同步、技能边界(脚本必须切换脚本创作技能S1-S11)、浏览器操作规范(9223调试实例+标签页复用)。后续所有改动以本主版本为准
|
||||
- **短视频分镜脚本** (`project/短视频提示词生成/V1.0/`,08-20 由"短视频分镜创作"改名,08-26 路径精简为现结构) — 提示词工程
|
||||
- 4功能:F1反推图片/F2反推视频/F3生成图片/F4生成分镜;参考 skills:seedance2.0/ShotDesign + seedance25 单文件
|
||||
|
||||
@@ -9,7 +9,7 @@ description: >
|
||||
诊断脚本质量、设计账号人设、选题策划、生成分镜、广告植入设计、生成创作回顾。
|
||||
version: "1.0"
|
||||
updated_at: "2026-08-26"
|
||||
last_change: "MCN工作台大版本升级:hash 路由 + 数据功能页(抖音周榜/热门账号/账号详情/视频详情/AI写脚本/AI写作复盘,只读复用 dsh 库 mcn-plugin.db + 抖音榜单 JSON,dsh-data.js 数据层);所有触发AI按钮降级为点一下复制 prompt;前端拆 app.js+data-pages.js"
|
||||
last_change: "MCN工作台 AI 任务触发升级:按钮直接提交 Automation 一次性任务(POST /api/run → 写 ~/.workbuddy/workbuddy.db automations 表 schedule_type=once scheduled_at=now,cwds=D:\\AgentSkill\\mcn-workshop → WorkBuddy 自动执行创建会话,归入 mcn-workshop 空间分组,左侧会话栏可见),取代原『复制 prompt 降级』方案;参考 dsh-plugin-mcn /mcn/api/creative 模式;5 秒防连点;前置:数据库副本隔离+数据功能页"
|
||||
deployment: "source"
|
||||
agent_created: true
|
||||
---
|
||||
@@ -64,13 +64,14 @@ agent_created: true
|
||||
- **首页(#/)**:数据功能卡片(抖音周榜/热门账号/AI写脚本/AI写作复盘)+ 5 大技能功能块(问法点击复制)+ 三种创作模式
|
||||
- **📈 抖音周榜(#/ranking)**:读 `D:\dshworkspace\抖音榜单\*.json`(环境变量 `MCN_RANKING_DIR` 可覆盖),赛道筛选 + 日期切换 + 数值排序
|
||||
- **👥 热门账号(#/accounts)**:读工作台自有数据库副本 `mcn-work-shop/mcn-plugin.db`(只读),分页/搜索/赛道/类型筛选,账号点击进详情
|
||||
- **账号详情(#/account/:id)**:信息卡 + 账号信息/账号设定/数据分析 3 tab + 视频列表分页;解析/改写/提炼按钮降级为「点一下复制 prompt」
|
||||
- **账号详情(#/account/:id)**:信息卡 + 账号信息/账号设定/数据分析 3 tab + 视频列表分页;解析/改写/提炼按钮直接提交 AI 任务执行
|
||||
- **视频详情(#/video/:id)**:信息卡 + 视频脚本/视频拆解/视频分析 3 tab
|
||||
- **✍️ AI写脚本(#/rewrites)**:爆款视频列表 + 源脚本 vs AI脚本双栏对比 + 分镜提示词分块渲染
|
||||
- **🧪 AI写作复盘(#/reviews)**:评分维度列表 + 复盘详情(评分条/问题清单/报告)
|
||||
- **📁 产出浏览(#/files)**:默认读桌面 `MCNSkill项目/`,支持 ⚙️ 设置添加其他项目地址;md/json 在线预览
|
||||
4. **数据边界**:数据库读工作台自有副本 `mcn-work-shop/mcn-plugin.db`(由 dsh 库复制而来,与 dsh 原库 `~/.dsh/mcn-plugin.db` 完全隔离,两边各访问各的互不影响);周榜读 `D:\dshworkspace\抖音榜单\*.json` 文件;所有「触发 AI」按钮因 WorkBuddy 无 AI 会话桥接,统一降级为「点一下复制 prompt 到剪贴板」,粘贴到对话框执行
|
||||
5. 本功能仅 V1.0 源技能环境提供,**不同步 dsh/Lite 副本**
|
||||
4. **数据边界**:数据库读工作台自有副本 `mcn-work-shop/mcn-plugin.db`(由 dsh 库复制而来,与 dsh 原库 `~/.dsh/mcn-plugin.db` 完全隔离,两边各访问各的互不影响);周榜读 `D:\dshworkspace\抖音榜单\*.json` 文件
|
||||
5. **AI 任务触发(Automation 方案)**:各数据页「导入/创作/解析/改写/提炼/复盘」按钮点击后 → 工作台后端 `POST /api/run` → 向 `~/.workbuddy/workbuddy.db` 的 `automations` 表写入一条**一次性任务**(`schedule_type='once'`、`scheduled_at=now`、`cwds=["D:\\AgentSkill\\mcn-workshop"]`)→ WorkBuddy 运行时自动消费执行(创建 AI 会话)→ **会话归入 `mcn-workshop` 空间分组**(左侧会话栏按 cwd 分组,可在该分组查看任务执行过程与结果)。参考 dsh 插件 `dsh-plugin-mcn` 的 `/mcn/api/creative` 模式(固定会话 followup 触发);前端 5 秒防连点
|
||||
6. 本功能仅 V1.0 源技能环境提供,**不同步 dsh/Lite 副本**
|
||||
|
||||
---
|
||||
## MCP 依赖(摘要)
|
||||
|
||||
@@ -271,12 +271,21 @@ function getRanking({ date = '', category = '', sortBy = 'rank', order = '', pag
|
||||
for (const f of selFiles) {
|
||||
try { items = items.concat(JSON.parse(fs.readFileSync(path.join(RANKING_DIR, f), 'utf8')).list || []); } catch (e) {}
|
||||
}
|
||||
const orderDir = order === 'asc' ? 'asc' : (order === 'desc' ? 'desc' : (sortBy === 'rank' ? 'asc' : 'desc'));
|
||||
items.sort((a, b) => {
|
||||
const va = sortBy === 'rank' ? num(a.rank) : sortBy === 'comprehensiveScore' ? num(a.comprehensiveScore) : parseNum(a[sortBy]);
|
||||
const vb = sortBy === 'rank' ? num(b.rank) : sortBy === 'comprehensiveScore' ? num(b.comprehensiveScore) : parseNum(b[sortBy]);
|
||||
return orderDir === 'asc' ? va - vb : vb - va;
|
||||
});
|
||||
// 默认排序:综合得分倒序,同分按排名升序(用户点表头排序时走下方 sortBy/order 逻辑)
|
||||
if (!order && (!sortBy || sortBy === 'rank')) {
|
||||
items.sort((a, b) => {
|
||||
const scA = num(a.comprehensiveScore), scB = num(b.comprehensiveScore);
|
||||
if (scA !== scB) return scB - scA;
|
||||
return num(a.rank) - num(b.rank);
|
||||
});
|
||||
} else {
|
||||
const orderDir = order === 'asc' ? 'asc' : (order === 'desc' ? 'desc' : (sortBy === 'rank' ? 'asc' : 'desc'));
|
||||
items.sort((a, b) => {
|
||||
const va = sortBy === 'rank' ? num(a.rank) : sortBy === 'comprehensiveScore' ? num(a.comprehensiveScore) : parseNum(a[sortBy]);
|
||||
const vb = sortBy === 'rank' ? num(b.rank) : sortBy === 'comprehensiveScore' ? num(b.comprehensiveScore) : parseNum(b[sortBy]);
|
||||
return orderDir === 'asc' ? va - vb : vb - va;
|
||||
});
|
||||
}
|
||||
const total = items.length;
|
||||
const limit = Math.max(1, Math.min(Number(pageSize) || 20, 100));
|
||||
const offset = (Math.max(1, Number(page) || 1) - 1) * limit;
|
||||
|
||||
@@ -36,7 +36,7 @@ const ICON_PATHS = {
|
||||
|
||||
// 功能图标亮色映射(不同功能不同亮色;未列出的保持 currentColor 中性)
|
||||
const ICON_COLORS = {
|
||||
'trending-up': '#e8590c', 'users': '#2f6fed', 'pen': '#8250df', 'flask': '#1a7f37',
|
||||
'trending-up': '#e8590c', 'users': '#2f6fed', 'pen': '#ec4899', 'flask': '#0ea5e9',
|
||||
'lightbulb': '#d99a2b', 'refresh': '#0f7f8f', 'chart': '#d6336c', 'wrench': '#5b6470',
|
||||
};
|
||||
|
||||
@@ -166,8 +166,20 @@ function renderHome() {
|
||||
<span class="data-card-desc">${c.desc}</span>
|
||||
</button>`).join('')}
|
||||
</div>
|
||||
<div class="rank-preview">
|
||||
<div class="recent-head">
|
||||
<h3>抖音周榜</h3>
|
||||
<div class="rank-tools">
|
||||
<select id="rankCat" class="input-select rank-filter" title="赛道筛选"></select>
|
||||
<button class="btn btn-sm" id="rankPrevDate" title="上一期">◀</button>
|
||||
<span id="rankDateLabel" class="rank-date-label"></span>
|
||||
<button class="btn btn-sm" id="rankNextDate" title="下一期">▶</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rankPreviewList" class="rank-preview-list"><span class="dim" style="padding:12px">加载中…</span></div>
|
||||
</div>
|
||||
<div class="recent-scripts">
|
||||
<div class="recent-head"><h3>AI写剧本(最近)</h3><a class="link" href="#/rewrites">查看全部 →</a></div>
|
||||
<div class="recent-head"><h3>AI生成脚本</h3><a class="link" href="#/rewrites">查看全部</a></div>
|
||||
<div id="recentScriptsList" class="recent-list"><span class="dim">加载中…</span></div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -243,6 +255,53 @@ async function loadRecentScripts() {
|
||||
}));
|
||||
} catch (e) { el.innerHTML = '<span class="dim">' + esc(e.message) + '</span>'; }
|
||||
}
|
||||
|
||||
/* ================= 首页 抖音周榜 预览(前10 + 赛道/日期筛选,无分页) ================= */
|
||||
let _rankPreview = { page: 1, pageSize: 10, dates: [], categories: [], date: '', category: '' };
|
||||
function renderRankTools() {
|
||||
const catSel = $('#rankCat');
|
||||
if (catSel) {
|
||||
catSel.innerHTML = '<option value="">全部赛道</option>' + _rankPreview.categories.map((c) => `<option value="${esc(c)}" ${c === _rankPreview.category ? 'selected' : ''}>${esc(c)}</option>`).join('');
|
||||
catSel.onchange = () => { _rankPreview.category = catSel.value; loadRankingPreview(); };
|
||||
}
|
||||
const dateLabel = $('#rankDateLabel');
|
||||
const di = _rankPreview.dates.indexOf(_rankPreview.date);
|
||||
if (dateLabel) dateLabel.textContent = _rankPreview.date ? _rankPreview.date.slice(5).replace('-', '-') : '';
|
||||
const prevBtn = $('#rankPrevDate'), nextBtn = $('#rankNextDate');
|
||||
if (prevBtn) { prevBtn.disabled = di <= 0; prevBtn.onclick = () => { if (di > 0) { _rankPreview.date = _rankPreview.dates[di - 1]; renderRankTools(); loadRankingPreview(); } }; }
|
||||
if (nextBtn) { nextBtn.disabled = di < 0 || di >= _rankPreview.dates.length - 1; nextBtn.onclick = () => { if (di >= 0 && di < _rankPreview.dates.length - 1) { _rankPreview.date = _rankPreview.dates[di + 1]; renderRankTools(); loadRankingPreview(); } }; }
|
||||
}
|
||||
async function loadRankingPreview() {
|
||||
const listEl = $('#rankPreviewList');
|
||||
if (!listEl) return;
|
||||
try {
|
||||
if (!_rankPreview.dates.length) {
|
||||
const m = await api('/api/dsh/ranking?page=1&pageSize=1');
|
||||
_rankPreview.dates = m.dates || [];
|
||||
_rankPreview.categories = m.categories || [];
|
||||
_rankPreview.date = m.date || (_rankPreview.dates[0] || '');
|
||||
_rankPreview.category = m.category || '';
|
||||
renderRankTools();
|
||||
}
|
||||
const d = await api('/api/dsh/ranking?page=1&pageSize=' + _rankPreview.pageSize + '&date=' + encodeURIComponent(_rankPreview.date) + '&category=' + encodeURIComponent(_rankPreview.category));
|
||||
const items = d.items || [];
|
||||
const head = '<div class="rank-row rank-head"><span class="rk-no">排名</span><span class="rk-cat">赛道</span><span class="rk-name">账号</span><span class="rk-score">综合得分</span><span class="rk-num">新增粉丝</span><span class="rk-num">新增点赞</span><span class="rk-num">新增评论</span><span class="rk-num">新增分享</span><span class="rk-act">功能</span></div>';
|
||||
listEl.innerHTML = items.length ? head + items.map((r) => `<div class="rank-row">
|
||||
<span class="rk-no">${r.rank ?? '-'}</span>
|
||||
<span class="rk-cat">${esc(r.category || '-')}</span>
|
||||
<span class="rk-name">${r.inDb && r.dbId ? `<a class="link" href="#/account/${r.dbId}">${esc(r.accountName)}</a>` : (r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}">${esc(r.accountName)}</a>` : esc(r.accountName))}</span>
|
||||
<span class="rk-score">${r.comprehensiveScore ?? '-'}</span>
|
||||
<span class="rk-num">${esc(r.newFans ?? '-')}</span>
|
||||
<span class="rk-num">${esc(r.newLikes ?? '-')}</span>
|
||||
<span class="rk-num">${esc(r.newComments ?? '-')}</span>
|
||||
<span class="rk-num">${esc(r.newShares ?? '-')}</span>
|
||||
<span class="rk-act">${r.inDb && r.dbId ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-rk-import data-rk-name="${esc(r.accountName)}" data-rk-url="${esc(r.profileUrl || '')}" title="导入并分析该账号">导入</button>`}</span>
|
||||
</div>`).join('') : '<span class="dim" style="padding:12px">暂无周榜数据</span>';
|
||||
listEl.querySelectorAll('[data-rk-import]').forEach((b) => b.addEventListener('click', () => {
|
||||
firePrompt(`请帮我把抖音账号「${b.dataset.rkName}」导入并分析(账号信息 + 视频列表):${b.dataset.rkUrl || ''}`, '导入指令');
|
||||
}));
|
||||
} catch (e) { listEl.innerHTML = '<span class="dim" style="padding:12px">' + esc(e.message) + '</span>'; }
|
||||
}
|
||||
function openRecentScript(s) {
|
||||
const title = s.title || s.video_title || 'AI 写剧本';
|
||||
const mask = document.createElement('div'); mask.className = 'modal-mask';
|
||||
@@ -334,7 +393,7 @@ function nodeHtml(n, depth) {
|
||||
return `<div class="tree-node"><div class="tree-row" data-path="${esc(n.path)}" data-type="dir"><span class="caret">${open ? '▾' : '▸'}</span><span class="icon">${svgIcon(open ? 'folder-open' : 'folder', 15)}</span><span class="fname">${esc(n.name)}</span></div>${open && children.length ? `<div class="tree-children">${children.map((c) => nodeHtml(c, depth + 1)).join('')}</div>` : ''}</div>`;
|
||||
}
|
||||
const ext = (n.ext || '').slice(1);
|
||||
return `<div class="tree-row" data-path="${esc(n.path)}" data-type="file"><span class="caret"></span><span class="icon">${svgIcon(EXT_ICON[ext] || 'file', 15)}</span><span class="fname ${'ext-' + (ext || 'txt')}">${esc(n.name)}</span><span style="margin-left:auto;font-size:11px;color:var(--dim)">${fmtSize(n.size)}</span></div>`;
|
||||
return `<div class="tree-row" data-path="${esc(n.path)}" data-type="file"><span class="caret"></span><span class="icon">${svgIcon(EXT_ICON[ext] || 'file', 15)}</span><span class="fname ${'ext-' + (ext || 'txt')}">${esc(n.name)}</span><span style="margin-left:auto;font-size:13px;color:var(--dim)">${fmtSize(n.size)}</span></div>`;
|
||||
}
|
||||
|
||||
function renderTree() {
|
||||
@@ -374,7 +433,7 @@ async function openFile(p) {
|
||||
function renderRootList() {
|
||||
const list = $('#rootList');
|
||||
const roots = fileState.roots;
|
||||
list.innerHTML = (roots.extraRoots || []).map((p) => `<div class="root-item"><span class="rp">${esc(p)}</span><button class="rm" data-rm="${esc(p)}" title="移除">${svgIcon('x', 13)}</button></div>`).join('') || '<div class="dim" style="font-size:12px">暂无自定义目录</div>';
|
||||
list.innerHTML = (roots.extraRoots || []).map((p) => `<div class="root-item"><span class="rp">${esc(p)}</span><button class="rm" data-rm="${esc(p)}" title="移除">${svgIcon('x', 13)}</button></div>`).join('') || '<div class="dim" style="font-size:14px">暂无自定义目录</div>';
|
||||
list.querySelectorAll('.rm').forEach((btn) => btn.addEventListener('click', async () => {
|
||||
const p = btn.dataset.rm;
|
||||
roots.extraRoots = roots.extraRoots.filter((x) => x !== p);
|
||||
@@ -416,6 +475,7 @@ async function router() {
|
||||
updateCrumbs('');
|
||||
view.innerHTML = renderHome();
|
||||
view.querySelectorAll('.data-card').forEach((b) => b.addEventListener('click', () => { location.hash = b.dataset.hash; }));
|
||||
loadRankingPreview();
|
||||
loadRecentScripts();
|
||||
return;
|
||||
}
|
||||
@@ -447,10 +507,19 @@ async function router() {
|
||||
};
|
||||
if (pages[name]) {
|
||||
updateCrumbs(name, titles[name]);
|
||||
await pages[name]();
|
||||
try {
|
||||
await pages[name]();
|
||||
} catch (err) {
|
||||
console.error('[router]', name, err);
|
||||
view.innerHTML = '<div class="empty">页面渲染失败:' + esc(err.message) + '</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 数据页脚本(data-pages.js)尚未加载完时,不要误跳首页——等待下次路由
|
||||
if (name && ['ranking', 'accounts', 'account', 'video', 'rewrites', 'reviews'].includes(name) && typeof window.DataPages === 'undefined') {
|
||||
return;
|
||||
}
|
||||
// 未知路由 → 首页
|
||||
location.hash = '#/';
|
||||
}
|
||||
@@ -487,4 +556,40 @@ document.addEventListener('click', (e) => {
|
||||
});
|
||||
|
||||
initIcons();
|
||||
router();
|
||||
|
||||
/* ================= 路由记忆(刷新/面板重载后恢复到上次页面) =================
|
||||
内置浏览器刷新 iframe 时可能丢失 URL hash → 回首页。
|
||||
localStorage + cookie 双写(任一可用),hash 丢失时自动恢复。 */
|
||||
function saveRoute() {
|
||||
const h = location.hash || '#/';
|
||||
try { localStorage.setItem('wb_route', h); } catch (e) {}
|
||||
try { document.cookie = 'wb_route=' + encodeURIComponent(h) + '; path=/; max-age=86400'; } catch (e) {}
|
||||
}
|
||||
function readRoute() {
|
||||
try { const v = localStorage.getItem('wb_route'); if (v) return v; } catch (e) {}
|
||||
try {
|
||||
const m = document.cookie.match(/(?:^|;\s*)wb_route=([^;]*)/);
|
||||
if (m) return decodeURIComponent(m[1]);
|
||||
} catch (e) {}
|
||||
return null;
|
||||
}
|
||||
window.addEventListener('hashchange', saveRoute);
|
||||
(function boot() {
|
||||
const saved = readRoute();
|
||||
if (saved && saved !== '#/' && saved !== '#' && (!location.hash || location.hash === '#/')) {
|
||||
location.hash = saved; // 刷新丢 hash → 恢复上次页面
|
||||
return;
|
||||
}
|
||||
saveRoute(); // 直接输入 URL/首次打开:立即记录当前路由,供下次刷新恢复
|
||||
// 关键:data-pages.js 在 app.js 之后的 script 标签,下载/执行可能未完成。
|
||||
// 须等其就绪(DataPages 存在)再路由,否则数据页会被当成未知路由。
|
||||
function bootRouter() {
|
||||
if (typeof window.DataPages === 'undefined') { setTimeout(bootRouter, 50); return; }
|
||||
router();
|
||||
}
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', bootRouter);
|
||||
} else {
|
||||
bootRouter();
|
||||
}
|
||||
})();
|
||||
@@ -22,15 +22,29 @@ function bindBack(container) {
|
||||
container.querySelectorAll('[data-back]').forEach((b) => b.addEventListener('click', () => { location.hash = '#/'; }));
|
||||
}
|
||||
|
||||
/* 生成「触发 AI」的 prompt(WorkBuddy 无 AI 桥接,降级为复制到剪贴板) */
|
||||
/* 触发 AI 任务:提交 WorkBuddy Automation 一次性任务,WorkBuddy 自动执行,左侧会话栏可见 */
|
||||
let _lastFireTs = 0;
|
||||
async function firePrompt(prompt, label) {
|
||||
const ok = await copyText(prompt);
|
||||
showToast(ok ? `✅ 已复制${label || '任务指令'},请粘贴到对话框执行` : '❌ 复制失败,请手动复制');
|
||||
const nowTs = Date.now();
|
||||
if (nowTs - _lastFireTs < 5000) { showToast('⏳ 任务刚提交,请在左侧会话栏查看执行'); return false; }
|
||||
_lastFireTs = nowTs;
|
||||
try {
|
||||
const d = await api('/api/run', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ prompt, name: label || '工作台任务' }),
|
||||
});
|
||||
showToast('✅ ' + (d.message || '任务已提交,请在左侧会话栏查看执行'));
|
||||
return true;
|
||||
} catch (e) {
|
||||
showToast('❌ 任务提交失败: ' + e.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ================= 周榜 ================= */
|
||||
async function renderRanking(view) {
|
||||
let state = { date: '', category: '', sortBy: 'rank', order: 'asc', page: 1, pageSize: 20, dates: [], categories: [] };
|
||||
let state = { date: '', category: '', sortBy: 'comprehensiveScore', order: 'desc', page: 1, pageSize: 20, dates: [], categories: [] };
|
||||
const data = await api('/api/dsh/ranking?pageSize=20').catch((e) => ({ ok: false, error: e.message }));
|
||||
if (!data.ok) { view.innerHTML = `<div class="page"><div class="page-toolbar">${toolbarBack()}<span class="page-title">抖音周榜</span></div><div class="empty">${esc(data.error || '加载失败')}</div></div>`; bindBack(view); return; }
|
||||
state.dates = data.dates || []; state.categories = data.categories || []; state.date = data.date; state.category = data.category;
|
||||
@@ -41,33 +55,35 @@ async function renderRanking(view) {
|
||||
const sortable = (key, label) => `<th class="sortable" data-sort="${key}">${label}${state.sortBy === key ? (state.order === 'asc' ? ' ↑' : ' ↓') : ''}</th>`;
|
||||
const rows = (d.items || []).map((r) => `<tr>
|
||||
<td>${r.rank ?? '-'}</td>
|
||||
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
|
||||
<td>${esc(r.category || '-')}</td>
|
||||
<td>${fmt(r.comprehensiveScore)}</td>
|
||||
<td>${r.profileUrl ? `<a class="link" data-ext="${esc(r.profileUrl)}" title="在新标签打开账号主页">${esc(r.accountName)}</a>` : esc(r.accountName)}</td>
|
||||
<td class="score-cell">${fmt(r.comprehensiveScore)}</td>
|
||||
<td>${fmt(r.followers)}</td>
|
||||
<td>${fmt(r.newFans)}</td>
|
||||
<td>${fmt(r.newLikes)}</td>
|
||||
<td>${fmt(r.newComments)}</td>
|
||||
<td>${fmt(r.newShares)}</td>
|
||||
<td>${r.inDb ? `<a class="link" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm" data-import='${esc(JSON.stringify(r))}'>导入</button>`}</td>
|
||||
<td class="cell-act">${r.inDb ? `<a class="btn btn-sm btn-view" href="#/account/${r.dbId}">查看</a>` : `<button class="btn btn-sm btn-import" data-import='${esc(JSON.stringify(r))}'>导入</button>`}</td>
|
||||
</tr>`).join('');
|
||||
view.innerHTML = `
|
||||
<div class="page">
|
||||
<div class="page-toolbar">
|
||||
${toolbarBack()}
|
||||
<span class="page-title">抖音周榜</span>
|
||||
<select id="rankCat" class="input-select" title="赛道筛选">
|
||||
<option value="">全部赛道</option>
|
||||
${state.categories.map((c) => `<option value="${esc(c)}" ${c === state.category ? 'selected' : ''}>${esc(c)}</option>`).join('')}
|
||||
</select>
|
||||
<button class="btn" id="rankPrev" ${di <= 0 ? 'disabled' : ''}>◀</button>
|
||||
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
|
||||
<button class="btn" id="rankNext" ${di < 0 || di >= state.dates.length - 1 ? 'disabled' : ''}>▶</button>
|
||||
<div class="toolbar-right">
|
||||
<select id="rankCat" class="input-select" title="赛道筛选">
|
||||
<option value="">全部赛道</option>
|
||||
${state.categories.map((c) => `<option value="${esc(c)}" ${c === state.category ? 'selected' : ''}>${esc(c)}</option>`).join('')}
|
||||
</select>
|
||||
<button class="btn" id="rankPrev" ${di <= 0 ? 'disabled' : ''}>◀</button>
|
||||
<span class="pager-info" title="选择日期">${esc(state.date)}</span>
|
||||
<button class="btn" id="rankNext" ${di < 0 || di >= state.dates.length - 1 ? 'disabled' : ''}>▶</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table class="table">
|
||||
<thead><tr>
|
||||
${sortable('rank', '排名')}<th>账号</th><th>赛道</th>${sortable('comprehensiveScore', '综合得分')}${sortable('followers', '粉丝')}${sortable('newFans', '新增粉丝')}${sortable('newLikes', '新增点赞')}${sortable('newComments', '新增评论')}${sortable('newShares', '新增分享')}<th>功能</th>
|
||||
${sortable('rank', '排名')}<th>赛道</th><th>账号</th>${sortable('comprehensiveScore', '综合得分')}${sortable('followers', '粉丝')}${sortable('newFans', '新增粉丝')}${sortable('newLikes', '新增点赞')}${sortable('newComments', '新增评论')}${sortable('newShares', '新增分享')}<th class="th-act">功能</th>
|
||||
</tr></thead>
|
||||
<tbody>${rows || '<tr><td colspan="10" class="empty">暂无周榜数据</td></tr>'}</tbody>
|
||||
</table>
|
||||
@@ -114,7 +130,7 @@ async function renderAccounts(view) {
|
||||
<td class="cell-content" title="${esc(a.bio || '')}">${esc((a.bio || '').slice(0, 50))}</td>
|
||||
<td class="cell-content" title="${esc(a.content || '')}">${esc((a.content || '').slice(0, 50))}</td>
|
||||
<td>${a.video_count ?? 0}</td>
|
||||
<td><button class="btn btn-sm" data-create='${esc(a.account_name)}'>创作</button></td>
|
||||
<td class="cell-act"><button class="btn btn-sm" data-create='${esc(a.account_name)}'>创作</button></td>
|
||||
</tr>`).join('');
|
||||
view.innerHTML = `
|
||||
<div class="page">
|
||||
@@ -180,7 +196,7 @@ async function renderAccountDetail(view, id) {
|
||||
<td>${fmt(v.share_count)}</td>
|
||||
<td>${fmtDuration(v.duration)}</td>
|
||||
<td>${fmt(v.publish_time)}</td>
|
||||
<td>${(v.has_source || v.has_analysis) ? `<button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>改写脚本</button>` : `<button class="btn btn-sm" data-analyze='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>解析</button>`}</td>
|
||||
<td class="cell-act">${(v.has_source || v.has_analysis) ? `<button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>改写脚本</button>` : `<button class="btn btn-sm" data-analyze='${esc(JSON.stringify({ name: acc.account_name, title: v.video_title, aweme: v.aweme_id }))}'>解析</button>`}</td>
|
||||
</tr>`).join('');
|
||||
const tabBtns = [['info', '账号信息'], ['persona', '账号设定'], ['analysis', '数据分析']].map(([k, label]) => `<button class="tab ${tab === k ? 'tab-active' : ''}" data-tab="${k}">${label}</button>`).join('');
|
||||
let panel = '';
|
||||
@@ -279,7 +295,7 @@ async function renderRewrites(view) {
|
||||
<td>${fmt(v.publish_time)}</td>
|
||||
<td>${(v.has_source || v.has_analysis) ? '✅已解析' : '未解析'}</td>
|
||||
<td>${v.rewrite_count > 0 ? '✅' + v.rewrite_count : '—'}</td>
|
||||
<td><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: v.video_title }))}'>改写</button></td>
|
||||
<td class="cell-act"><button class="btn btn-sm" data-rewrite='${esc(JSON.stringify({ id: v.id, name: v.account_name, title: v.video_title }))}'>改写</button></td>
|
||||
</tr>`).join('');
|
||||
view.innerHTML = `
|
||||
<div class="page">
|
||||
@@ -405,7 +421,7 @@ async function renderReviews(view) {
|
||||
<td class="cell-content">${row.title ? `<a class="link" data-open="${row.id}">${esc(row.title)}</a>` : '-'}</td>
|
||||
<td>${fmtTime(row.created_time)}</td>
|
||||
${dimsTd}
|
||||
<td><button class="btn btn-sm" data-review-row="${row.id}" ${row.review_running ? 'disabled' : ''}>${row.review_running ? '复盘运行中' : '复盘'}</button></td>
|
||||
<td class="cell-act"><button class="btn btn-sm" data-review-row="${row.id}" ${row.review_running ? 'disabled' : ''}>${row.review_running ? '复盘运行中' : '复盘'}</button></td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ body {
|
||||
font-family: "Microsoft YaHei", "PingFang SC", "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 14px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ---- 顶栏 ---- */
|
||||
@@ -31,13 +31,13 @@ body {
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 8px; }
|
||||
.logo { display: inline-flex; align-items: center; color: var(--primary); }
|
||||
.title { font-size: 18px; font-weight: 700; }
|
||||
.subtitle { font-size: 12px; color: var(--dim); }
|
||||
.title { font-size: 21px; font-weight: 700; }
|
||||
.subtitle { font-size: 14px; color: var(--dim); }
|
||||
|
||||
.tabs { display: flex; gap: 4px; margin-left: 20px; }
|
||||
.tab {
|
||||
border: none; background: none; padding: 8px 16px;
|
||||
font-size: 14px; color: var(--dim); cursor: pointer;
|
||||
font-size: 16px; color: var(--dim); cursor: pointer;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.tab:hover { background: var(--primary-soft); color: var(--primary); }
|
||||
@@ -47,13 +47,13 @@ body {
|
||||
.ghost {
|
||||
border: 1px solid var(--border); background: var(--panel);
|
||||
color: var(--text); padding: 6px 14px; border-radius: 8px;
|
||||
cursor: pointer; font-size: 13px;
|
||||
cursor: pointer; font-size: 15px;
|
||||
display: inline-flex; align-items: center; gap: 6px;
|
||||
}
|
||||
.ghost:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.primary {
|
||||
border: none; background: var(--primary); color: #fff;
|
||||
padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px;
|
||||
padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 15px;
|
||||
}
|
||||
|
||||
/* ---- 主体 ---- */
|
||||
@@ -72,27 +72,27 @@ main { height: calc(100vh - 54px); }
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 16px 18px; box-shadow: var(--shadow);
|
||||
}
|
||||
.func-card h3 { font-size: 16px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.func-card .desc { color: var(--dim); font-size: 13px; margin-bottom: 10px; }
|
||||
.func-card h3 { font-size: 18px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.func-card .desc { color: var(--dim); font-size: 15px; margin-bottom: 10px; }
|
||||
.func-card .qa { border-top: 1px dashed var(--border); padding-top: 8px; }
|
||||
.qa-row { display: flex; gap: 8px; padding: 4px 0; font-size: 13px; }
|
||||
.qa-row { display: flex; gap: 8px; padding: 4px 0; font-size: 15px; }
|
||||
.qa-row .q { color: var(--accent); white-space: nowrap; }
|
||||
.qa-row .a { color: var(--dim); }
|
||||
.qa-row.copyable { cursor: pointer; border-radius: 6px; padding: 4px 6px; margin: 0 -6px; transition: background .15s; }
|
||||
.qa-row.copyable:hover { background: var(--primary-soft); }
|
||||
.qa-row.copyable:hover .q::after { content: ' 📋'; font-size: 12px; opacity: .7; }
|
||||
.qa-row.copyable:hover .q::after { content: ' 📋'; font-size: 14px; opacity: .7; }
|
||||
|
||||
.modes {
|
||||
max-width: 900px; margin: 28px auto 0;
|
||||
background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 12px; padding: 16px 18px;
|
||||
}
|
||||
.modes h3 { font-size: 15px; margin-bottom: 10px; }
|
||||
.modes h3 { font-size: 17px; margin-bottom: 10px; }
|
||||
.mode-table { border-collapse: collapse; width: 100%; }
|
||||
.mode-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 13px; }
|
||||
.mode-table td { padding: 6px 10px; border-bottom: 1px solid var(--border); font-size: 15px; }
|
||||
.mode-table tr:last-child td { border-bottom: none; }
|
||||
.mode-name { font-weight: 600; width: 140px; display: flex; align-items: center; gap: 6px; }
|
||||
.mode-hint { margin-top: 8px; font-size: 12px; color: var(--dim); }
|
||||
.mode-hint { margin-top: 8px; font-size: 14px; color: var(--dim); }
|
||||
|
||||
/* ---- 产出浏览 ---- */
|
||||
.files-layout { display: flex; height: 100%; gap: 0; }
|
||||
@@ -104,11 +104,11 @@ main { height: calc(100vh - 54px); }
|
||||
.sidebar-top { padding: 12px; display: flex; flex-direction: column; gap: 8px; border-bottom: 1px solid var(--border); }
|
||||
.sidebar-top select, .sidebar-top input {
|
||||
padding: 7px 10px; border: 1px solid var(--border); border-radius: 8px;
|
||||
font-size: 13px; width: 100%; background: #fff; color: var(--text);
|
||||
font-size: 15px; width: 100%; background: #fff; color: var(--text);
|
||||
}
|
||||
.file-tree { flex: 1; overflow: auto; padding: 8px; }
|
||||
.tree-empty { color: var(--dim); padding: 12px; font-size: 13px; }
|
||||
.tree-node { font-size: 13px; }
|
||||
.tree-empty { color: var(--dim); padding: 12px; font-size: 15px; }
|
||||
.tree-node { font-size: 15px; }
|
||||
.tree-row {
|
||||
display: flex; align-items: center; gap: 6px;
|
||||
padding: 4px 8px; border-radius: 6px; cursor: pointer; user-select: none;
|
||||
@@ -116,7 +116,7 @@ main { height: calc(100vh - 54px); }
|
||||
}
|
||||
.tree-row:hover { background: var(--primary-soft); }
|
||||
.tree-row.selected { background: var(--primary); color: #fff; }
|
||||
.tree-row .caret { width: 14px; text-align: center; font-size: 10px; flex-shrink: 0; }
|
||||
.tree-row .caret { width: 14px; text-align: center; font-size: 11px; flex-shrink: 0; }
|
||||
.tree-row .icon { flex-shrink: 0; }
|
||||
.tree-row .fname { overflow: hidden; text-overflow: ellipsis; }
|
||||
.tree-children { margin-left: 18px; border-left: 1px dashed var(--border); padding-left: 4px; }
|
||||
@@ -130,33 +130,33 @@ main { height: calc(100vh - 54px); }
|
||||
height: 100%; display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; color: var(--dim); gap: 8px;
|
||||
}
|
||||
.preview-empty p { font-size: 15px; }
|
||||
.preview-empty .dim { font-size: 12px; }
|
||||
.preview-empty p { font-size: 17px; }
|
||||
.preview-empty .dim { font-size: 14px; }
|
||||
.preview { padding: 24px 32px; max-width: 900px; margin: 0 auto; }
|
||||
.preview .file-head {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
border-bottom: 1px solid var(--border); padding-bottom: 10px; margin-bottom: 16px;
|
||||
}
|
||||
.preview .file-head .fpath { font-size: 12px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.preview .file-head .fmeta { font-size: 12px; color: var(--dim); white-space: nowrap; }
|
||||
.preview .file-head .fpath { font-size: 14px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.preview .file-head .fmeta { font-size: 14px; color: var(--dim); white-space: nowrap; }
|
||||
.btn-download {
|
||||
border: 1px solid var(--primary); color: var(--primary); background: #fff;
|
||||
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;
|
||||
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 14px;
|
||||
margin-left: auto; text-decoration: none;
|
||||
}
|
||||
.btn-download:hover { background: var(--primary-soft); }
|
||||
|
||||
/* ---- Markdown 渲染 ---- */
|
||||
.md-body { line-height: 1.75; font-size: 14px; word-break: break-word; }
|
||||
.md-body { line-height: 1.75; font-size: 16px; word-break: break-word; }
|
||||
.md-body h1, .md-body h2, .md-body h3, .md-body h4 { margin: 18px 0 10px; line-height: 1.4; }
|
||||
.md-body h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
||||
.md-body h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
||||
.md-body h3 { font-size: 16px; }
|
||||
.md-body h1 { font-size: 25px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
||||
.md-body h2 { font-size: 21px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
|
||||
.md-body h3 { font-size: 18px; }
|
||||
.md-body p { margin: 8px 0; }
|
||||
.md-body strong { font-weight: 700; }
|
||||
.md-body code {
|
||||
background: #f0f2f5; border-radius: 4px; padding: 2px 5px;
|
||||
font-family: Consolas, monospace; font-size: 13px;
|
||||
font-family: Consolas, monospace; font-size: 15px;
|
||||
}
|
||||
.md-body pre {
|
||||
background: #f6f8fa; border: 1px solid var(--border); border-radius: 8px;
|
||||
@@ -168,7 +168,7 @@ main { height: calc(100vh - 54px); }
|
||||
padding: 8px 14px; margin: 10px 0; border-radius: 0 8px 8px 0; color: #333;
|
||||
}
|
||||
.md-body table { border-collapse: collapse; margin: 10px 0; width: 100%; display: block; overflow-x: auto; }
|
||||
.md-body th, .md-body td { border: 1px solid var(--border); padding: 6px 10px; font-size: 13px; }
|
||||
.md-body th, .md-body td { border: 1px solid var(--border); padding: 6px 10px; font-size: 15px; }
|
||||
.md-body th { background: #f6f8fa; font-weight: 600; }
|
||||
.md-body ul, .md-body ol { margin: 8px 0 8px 22px; }
|
||||
.md-body li { margin: 3px 0; }
|
||||
@@ -178,7 +178,7 @@ main { height: calc(100vh - 54px); }
|
||||
/* ---- JSON 预览 ---- */
|
||||
.json-view pre {
|
||||
background: #f6f8fa; border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 14px; overflow: auto; font-size: 13px; line-height: 1.6;
|
||||
padding: 14px; overflow: auto; font-size: 15px; line-height: 1.6;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
|
||||
@@ -193,21 +193,21 @@ main { height: calc(100vh - 54px); }
|
||||
width: 520px; max-width: 92vw; box-shadow: 0 8px 30px rgba(0,0,0,.18);
|
||||
}
|
||||
.modal h3 { margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
|
||||
.modal .dim { color: var(--dim); font-size: 13px; margin-bottom: 12px; }
|
||||
.modal code { background: #f0f2f5; padding: 1px 5px; border-radius: 4px; font-size: 12px; }
|
||||
.modal .dim { color: var(--dim); font-size: 15px; margin-bottom: 12px; }
|
||||
.modal code { background: #f0f2f5; padding: 1px 5px; border-radius: 4px; font-size: 14px; }
|
||||
.root-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; }
|
||||
.root-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; font-size: 13px;
|
||||
border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; font-size: 15px;
|
||||
}
|
||||
.root-item .rp { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.root-item .rm {
|
||||
border: none; background: none; color: #cf222e; cursor: pointer; font-size: 16px; line-height: 1;
|
||||
border: none; background: none; color: #cf222e; cursor: pointer; font-size: 18px; line-height: 1;
|
||||
}
|
||||
.root-item .rm:hover { opacity: .7; }
|
||||
.root-add { display: flex; gap: 8px; }
|
||||
.root-add input { flex: 1; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; }
|
||||
.root-msg { margin-top: 8px; font-size: 12px; color: var(--accent); min-height: 16px; }
|
||||
.root-add input { flex: 1; padding: 8px 10px; border: 1px solid var(--border); border-radius: 8px; font-size: 15px; }
|
||||
.root-msg { margin-top: 8px; font-size: 14px; color: var(--accent); min-height: 16px; }
|
||||
.modal-actions { text-align: right; margin-top: 14px; }
|
||||
|
||||
.dim { color: var(--dim); }
|
||||
@@ -216,7 +216,7 @@ main { height: calc(100vh - 54px); }
|
||||
.toast {
|
||||
position: fixed; left: 50%; bottom: 32px; transform: translateX(-50%) translateY(20px);
|
||||
background: #24292f; color: #fff; padding: 10px 18px; border-radius: 8px;
|
||||
font-size: 13px; opacity: 0; pointer-events: none; transition: all .25s;
|
||||
font-size: 15px; opacity: 0; pointer-events: none; transition: all .25s;
|
||||
max-width: 70vw; z-index: 200; box-shadow: 0 4px 16px rgba(0,0,0,.2);
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -224,10 +224,10 @@ main { height: calc(100vh - 54px); }
|
||||
|
||||
/* ================= 面包屑 ================= */
|
||||
.crumbs { display: flex; align-items: baseline; gap: 8px; margin-left: 12px; min-width: 0; }
|
||||
.crumb-home { font-size: 18px; font-weight: 700; color: var(--text); cursor: pointer; text-decoration: none; }
|
||||
.crumb-home { font-size: 21px; font-weight: 700; color: var(--text); cursor: pointer; text-decoration: none; }
|
||||
a.crumb-home:hover { color: var(--primary); }
|
||||
.crumb-sep { color: var(--dim); font-size: 14px; }
|
||||
.crumb-current { color: var(--text); font-weight: 600; font-size: 14px; }
|
||||
.crumb-sep { color: var(--dim); font-size: 16px; }
|
||||
.crumb-current { color: var(--text); font-weight: 600; font-size: 16px; }
|
||||
|
||||
main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.view-empty { padding: 40px; text-align: center; color: var(--dim); }
|
||||
@@ -238,26 +238,32 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.data-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px 18px; cursor: pointer; text-align: left; box-shadow: var(--shadow); transition: all .15s; }
|
||||
.data-card:hover { border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(47,111,237,.15); }
|
||||
.data-card-icon { color: var(--primary); display: inline-flex; }
|
||||
.data-card-title { font-size: 15px; font-weight: 600; color: var(--text); }
|
||||
.data-card-desc { font-size: 12px; color: var(--dim); }
|
||||
.data-card-title { font-size: 17px; font-weight: 600; color: var(--text); }
|
||||
.data-card-desc { font-size: 14px; color: var(--dim); }
|
||||
|
||||
/* ================= 功能页通用 ================= */
|
||||
.page { padding: 20px 24px; max-width: 1440px; margin: 0 auto; }
|
||||
.page-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 14px; }
|
||||
.page-title { font-size: 16px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; }
|
||||
.input { border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 13px; min-width: 200px; background: #fff; color: var(--text); }
|
||||
.input-select { border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; font-size: 13px; background: #fff; color: var(--text); }
|
||||
.btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
||||
.toolbar-right { display: flex; align-items: center; gap: 8px; margin-left: auto; }
|
||||
.page-title { font-size: 18px; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; }
|
||||
.input { border: 1px solid var(--border); border-radius: 8px; padding: 6px 10px; font-size: 15px; min-width: 200px; background: #fff; color: var(--text); }
|
||||
.input-select { border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; font-size: 15px; background: #fff; color: var(--text); }
|
||||
.btn { border: 1px solid var(--border); background: var(--panel); color: var(--text); padding: 6px 14px; border-radius: 8px; cursor: pointer; font-size: 15px; text-decoration: none; }
|
||||
.btn:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.btn:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.btn-sm { padding: 3px 10px; font-size: 12px; border-radius: 6px; }
|
||||
.btn-sm { padding: 3px 10px; font-size: 14px; border-radius: 6px; }
|
||||
.btn-primary { background: var(--primary); color: #fff; border-color: transparent; }
|
||||
.btn-primary:hover { background: #2a5fd0; color: #fff; }
|
||||
.empty { padding: 30px; text-align: center; color: var(--dim); font-size: 13px; }
|
||||
/* 功能列按钮区分:导入=浅灰底深字,查看=白底蓝字 */
|
||||
.btn-import { background: #f0f2f5; border-color: #e1e4e8; color: #495057; }
|
||||
.btn-import:hover { background: #e4e7eb; border-color: #d0d4da; color: #343a40; }
|
||||
.btn-view { background: #fff; border-color: var(--primary); color: var(--primary); }
|
||||
.btn-view:hover { background: var(--primary-soft); color: var(--primary); }
|
||||
.empty { padding: 30px; text-align: center; color: var(--dim); font-size: 15px; }
|
||||
|
||||
/* ================= 表格 ================= */
|
||||
.table-wrap { overflow: auto; border: 1px solid var(--border); border-radius: 8px; background: var(--panel); }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||
.table { width: 100%; border-collapse: collapse; font-size: 15px; }
|
||||
.table th { position: sticky; top: 0; background: #f6f8fa; text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--border); font-weight: 600; white-space: nowrap; z-index: 1; }
|
||||
.table td { padding: 7px 10px; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||||
.table tbody tr:hover { background: var(--primary-soft); }
|
||||
@@ -267,15 +273,15 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.link { color: var(--primary); cursor: pointer; text-decoration: none; }
|
||||
.link:hover { text-decoration: underline; }
|
||||
.link.strong { font-weight: 600; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 13px; }
|
||||
.badge-signed { background: var(--primary-soft); color: var(--primary); }
|
||||
.badge-ext { background: #fff4e5; color: var(--accent); }
|
||||
.pager { display: flex; align-items: center; gap: 12px; padding: 12px 4px; }
|
||||
.pager-total, .pager-info { color: var(--dim); font-size: 13px; }
|
||||
.pager { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 12px 4px; }
|
||||
.pager-total, .pager-info { color: var(--dim); font-size: 15px; }
|
||||
|
||||
/* ================= 数据页 tab ================= */
|
||||
.tabs { display: flex; gap: 4px; margin-bottom: 14px; border-bottom: 1px solid var(--border); }
|
||||
.tab { border: none; background: none; padding: 8px 16px; font-size: 14px; color: var(--dim); cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab { border: none; background: none; padding: 8px 16px; font-size: 16px; color: var(--dim); cursor: pointer; border-bottom: 2px solid transparent; }
|
||||
.tab:hover { color: var(--primary); }
|
||||
.tab-active { color: var(--primary); font-weight: 600; border-bottom-color: var(--primary); }
|
||||
|
||||
@@ -284,11 +290,11 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.info-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px 18px; }
|
||||
.info-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
|
||||
.info-wide { grid-column: 1/-1; }
|
||||
.info-label { font-size: 12px; color: var(--dim); }
|
||||
.info-value { font-size: 13px; color: var(--text); word-break: break-word; white-space: pre-wrap; line-height: 1.6; }
|
||||
.info-label { font-size: 14px; color: var(--dim); }
|
||||
.info-value { font-size: 15px; color: var(--text); word-break: break-word; white-space: pre-wrap; line-height: 1.6; }
|
||||
|
||||
/* ================= 对比弹窗 ================= */
|
||||
.modal-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; font-size: 16px; font-weight: 600; }
|
||||
.modal-title-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 14px; font-size: 18px; font-weight: 600; }
|
||||
.rw-modal { width: min(1100px, 95vw); max-height: 90vh; overflow: auto; }
|
||||
.rw-split { display: flex; gap: 14px; }
|
||||
.rw-pane { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
@@ -300,35 +306,56 @@ main#view { height: calc(100vh - 54px); overflow: auto; }
|
||||
.sb-block { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; overflow: hidden; background: var(--panel); }
|
||||
.sb-head { display: flex; align-items: center; gap: 10px; padding: 6px 12px; background: #f6f8fa; border-bottom: 1px solid var(--border); }
|
||||
.sb-title { font-weight: 600; color: var(--primary); }
|
||||
.sb-meta { margin-left: auto; font-size: 12px; color: var(--dim); }
|
||||
.sb-meta { margin-left: auto; font-size: 14px; color: var(--dim); }
|
||||
.sb-body { padding: 8px 12px; }
|
||||
|
||||
/* ================= 评分 ================= */
|
||||
.score-row { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
|
||||
.score-name { width: 88px; font-size: 12px; color: var(--dim); white-space: nowrap; flex-shrink: 0; }
|
||||
.score-name { width: 88px; font-size: 14px; color: var(--dim); white-space: nowrap; flex-shrink: 0; }
|
||||
.score-bar { flex: 1; height: 6px; border-radius: 3px; background: #eef1f5; overflow: hidden; }
|
||||
.score-fill { height: 100%; }
|
||||
.score-val { width: 36px; text-align: right; font-size: 12px; font-weight: 600; flex-shrink: 0; }
|
||||
.score-val { width: 36px; text-align: right; font-size: 14px; font-weight: 600; flex-shrink: 0; }
|
||||
.score-dual { display: flex; gap: 12px; margin-bottom: 12px; }
|
||||
.score-box { flex: 1; padding: 10px; border: 1px solid var(--border); border-radius: 8px; }
|
||||
.score-box-title { font-weight: 600; margin-bottom: 8px; }
|
||||
.review-issues { margin-bottom: 10px; }
|
||||
.issue-row { display: flex; gap: 6px; font-size: 12px; margin-bottom: 4px; }
|
||||
.review-summary { margin-bottom: 10px; font-size: 13px; }
|
||||
.issue-row { display: flex; gap: 6px; font-size: 14px; margin-bottom: 4px; }
|
||||
.review-summary { margin-bottom: 10px; font-size: 15px; }
|
||||
|
||||
/* ================= 首页 AI写剧本 快捷卡片 ================= */
|
||||
.recent-scripts { margin: 4px 0 24px; }
|
||||
.recent-head { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }
|
||||
.recent-head h3 { font-size: 15px; }
|
||||
.recent-head .link { font-size: 13px; }
|
||||
.recent-head h3 { font-size: 17px; }
|
||||
.recent-head .link { font-size: 13px; margin-left: auto; margin-right: 10px; }
|
||||
.recent-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 14px; }
|
||||
|
||||
/* ================= 首页 抖音周榜 预览 ================= */
|
||||
.rank-preview { margin: 4px 0 20px; }
|
||||
.rank-tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }
|
||||
.rank-filter { padding: 3px 6px; font-size: 14px; max-width: 110px; }
|
||||
.rank-date-label { font-size: 14px; color: var(--dim); min-width: 62px; text-align: center; white-space: nowrap; }
|
||||
.rank-preview-list { border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
|
||||
.rank-row { display: grid; grid-template-columns: 34px 84px minmax(80px, 1fr) 52px repeat(4, 58px) 72px; align-items: center; gap: 6px; padding: 6px 10px; font-size: 14px; border-bottom: 1px solid var(--border); }
|
||||
.rank-row:last-child { border-bottom: none; }
|
||||
.rank-head { background: var(--panel); color: var(--dim); font-weight: 600; font-size: 13px; }
|
||||
.rk-no { color: var(--dim); font-weight: 600; }
|
||||
.rk-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rk-cat { color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rk-score { text-align: right; font-weight: 600; color: var(--accent); }
|
||||
.rk-num { text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.rk-act { display: flex; align-items: center; justify-content: center; white-space: nowrap; }
|
||||
.cell-act { text-align: center; white-space: nowrap; }
|
||||
.table th.th-act { text-align: center; }
|
||||
.table td.score-cell { color: var(--accent); font-weight: 600; }
|
||||
.rank-pager { display: flex; align-items: center; justify-content: flex-end; gap: 8px; margin-top: 8px; }
|
||||
.rank-page-info { font-size: 14px; color: var(--dim); }
|
||||
.recent-card { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; cursor: pointer; text-align: left; transition: all .15s; min-height: 172px; }
|
||||
.recent-card:hover { border-color: var(--primary); box-shadow: 0 2px 8px rgba(47,111,237,.12); }
|
||||
.recent-card-title { font-size: 13px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; display: flex; align-items: center; gap: 6px; }
|
||||
.recent-card-title { font-size: 15px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; display: flex; align-items: center; gap: 6px; }
|
||||
.recent-card-preview { width: 100%; display: flex; flex-direction: column; gap: 2px; }
|
||||
.recent-preview-line { display: block; font-size: 12px; color: #57606a; line-height: 1.55; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; }
|
||||
.recent-card-meta { font-size: 12px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; margin-top: auto; }
|
||||
.recent-meta { font-size: 12px; color: var(--dim); margin-bottom: 12px; }
|
||||
.recent-preview-line { display: block; font-size: 14px; color: #57606a; line-height: 1.55; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; }
|
||||
.recent-card-meta { font-size: 14px; color: var(--dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%; margin-top: auto; }
|
||||
.recent-meta { font-size: 14px; color: var(--dim); margin-bottom: 12px; }
|
||||
|
||||
/* ================= 扁平化向量图标 ================= */
|
||||
.ic { display: inline-block; vertical-align: -0.18em; flex-shrink: 0; }
|
||||
|
||||
@@ -289,6 +289,37 @@ const server = http.createServer((req, res) => {
|
||||
} catch (e) { return sendErr(500, e.message); }
|
||||
}
|
||||
|
||||
// ---------- 提交 AI 任务(Automation 一次性任务,WorkBuddy 自动执行,左侧会话栏可见)----------
|
||||
if (p === '/api/run' && req.method === 'POST') {
|
||||
let body = '';
|
||||
req.on('data', (c) => { body += c; if (body.length > 1e6) req.destroy(); });
|
||||
req.on('end', () => {
|
||||
try {
|
||||
const { prompt, name } = JSON.parse(body || '{}');
|
||||
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) return sendErr(400, 'prompt 不能为空');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const db = new DatabaseSync(process.env.WORKBUDDY_DB || path.join(os.homedir(), '.workbuddy', 'workbuddy.db'));
|
||||
const now = Date.now();
|
||||
const id = 'automation-' + now;
|
||||
const cwd = 'D:\\AgentSkill\\mcn-workshop'; // 工作台触发的会话归入 mcn-workshop 空间分组
|
||||
const d = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
const scheduledAt = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
const uidRow = db.prepare("SELECT user_id FROM sessions WHERE user_id IS NOT NULL AND user_id <> '' ORDER BY created_at DESC LIMIT 1").get();
|
||||
const ownerUserId = uidRow ? uidRow.user_id : '';
|
||||
db.prepare(`INSERT INTO automations (id,name,prompt,status,schedule_type,next_run_at,scheduled_at,rrule,cwds,created_at,updated_at,skills_json,connector_ids_json,model_id,permission_mode,owner_user_id,owner_status)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`)
|
||||
.run(id, name || '工作台任务', prompt.trim(), 'ACTIVE', 'once', now, scheduledAt, '', JSON.stringify([cwd]), now, now, '[]', '[]', 'deepseek-v4-flash', 'fullAccess', ownerUserId, 'confirmed');
|
||||
db.close();
|
||||
log('已提交 AI 任务: ' + id + (name ? ' (' + name + ')' : ''));
|
||||
return sendJSON(200, { ok: true, id, message: '任务已提交,请在左侧会话栏查看执行' });
|
||||
} catch (e) {
|
||||
return sendErr(500, '任务提交失败: ' + e.message);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// ---------- 静态文件 ----------
|
||||
let filePath = path.join(PUBLIC_DIR, p === '/' ? 'index.html' : p);
|
||||
// 防穿越
|
||||
|
||||
Reference in new issue
Block a user