09-10 工作台:配置收敛、复盘字段契约修复、原创选题分镜支持、榜单期号区间显示

配置收敛:
- 端口/DB名/产出根/榜单目录单源化到 mcn-work-shop/config.json,代码层统一经 load-config.js 读取
- 新增 get-port.js 供 start.bat 读端口(bat 内嵌 node -e 会被 cmd 截断,读到的是兜底值)
- start.bat 修复失效的 managed node 路径(原写死 22.22.2 已不存在),改为遍历 versions 自动探测

复盘诊断页(真 bug 修复):
- 问题清单严重度全部显示成绿色 + 修改建议不显示:模型输出 {sev:'high',fix} 与前端读取 {severity:'高',suggestion} 字段名与取值均不匹配
- 前端新增 normIssue 归一化,兼容 sev/severity/level 与中英取值,并回补所属维度显示
- 在 submit-review-tasks 与诊断/复盘 prompt 中固化 issues_json 字段契约,从源头防 schema 漂移

分镜提示词:
- 原创选题(video_id 为空)原本无生成入口(分镜按 video_id 查询)
- listStoryboards 支持按 rewrite_id 查询,前端放开入口、切版本重查,并补落库指令(此前入库靠模型自由发挥)

榜单展示:
- 期号改为显示区间「08/31 ~ 09/06」,避免被误读为单日(期号=该周周一)
- 后端新增 collectRanges,从落盘文件读真实 dateStart/dateEnd 随 API 返回(日榜 start=end 不显区间)

其他:
- 清理 dsh 环境废弃后的历史叙述类废话(16 文件约 18 处),文末变更记录区保留
- 新增 docs/工作台UI规范.md(设计 token / 布局 / 8 页面路由 / 12 类组件 / 9 条已知坑)
This commit is contained in:
maogeigei committed 2026-09-10 18:28:42 +08:00
1 parent 6cb29a9877
commit c48902f063
58 files changed
+895 -263

No files matched your search

@@ -1,6 +1,6 @@
// 短视频工作台 - 本地 Web 服务(零依赖 Node 实现)
// 启动: node server.js [端口] → http://localhost:8900(默认;可传参指定端口,如 node server.js 9000)
// 功能: 技能导航 + 产出内容浏览(默认读桌面 MCNSkill项目)+ dsh 数据功能页(账号/视频/脚本/复盘/周榜,只读)
// 功能: 技能导航 + 产出内容浏览(默认读桌面 <outputRoot>,根目录名由 config.json 决定)+ dsh 数据功能页(账号/视频/脚本/复盘/周榜,只读)
'use strict';
const http = require('http');
@@ -11,14 +11,24 @@ const { execFileSync } = require('child_process');
const { URL } = require('url');
const dsh = require('./dsh-data');
const { parseAttachment } = require('./scripts/attachment-parser');
const { loadConfig } = require('./load-config');
// 09-01 默认端口由 8899 改为 8900:本机 8899 被小米 PC 管家 MiPCAudio.exe 系统服务占用(0.0.0.0 监听且自动复活),
// 此时 127.0.0.1:8899 绑定不生效 → 默认直接起在 8900;仍可 `node server.js <端口>` 显式换端口
const PORT_BASE = parseInt(process.argv[2], 10) || 8900;
const ROOT = __dirname;
const PUBLIC_DIR = path.join(ROOT, 'public');
const CONFIG_FILE = path.join(ROOT, 'config.json');
// 09-10 配置收敛:工作台设定(端口/DB名/产出根/榜单目录)统一收进 config.json,代码层一律从 CFG 读,
// 避免调整时到处改(config 缺失/损坏时回退 load-config.cjs 默认值)。argv 端口优先级仍高于 config。
const CFG = loadConfig();
const PORT_BASE = parseInt(process.argv[2], 10) || CFG.port;
// 09-10 路径治本(配套「仓库特征判定」):工作台运行时路径一律从 ROOT 动态推导,禁止写死盘符。
// 根因:原写死盘符指向仓库外的固定目录,仓库迁移后该目录不存在 → 浏览器锁失效、会话 cwd 悬空。
const BROWSER_LOCK_FILE = path.join(ROOT, '.browser-lock'); // 浏览器互斥锁文件(与工作台同目录)
const SESSION_CWD = ROOT; // 工作台触发的 AI 会话 cwd(分组名 = mcn-work-shop)
// 09-07:prompt 模板文件化(prompts/*.md)——规则迭代只改文件不碰代码;文件缺失/损坏回退 null,调用方自行兜底。
// 模板正文取 ==PROMPT== 与 ==CHANGELOG== 之间(头部说明/文末变更记录不发给模型)
const PROMPTS_DIR = path.join(ROOT, 'prompts');
@@ -45,22 +55,14 @@ function loadPrompt(name) {
} catch (e) { return null; }
}
// ---- 默认根目录:桌面/MCNSkill项目 ----
const DEFAULT_ROOT = path.join(os.homedir(), 'Desktop', 'MCNSkill项目');
// ---- 默认根目录:桌面/<CFG.outputRoot> ----
const DEFAULT_ROOT = path.join(os.homedir(), 'Desktop', CFG.outputRoot);
// ---- 配置读写(自定义根目录列表)----
function loadConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
const c = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
if (Array.isArray(c.extraRoots)) return { extraRoots: c.extraRoots };
}
} catch (e) { /* 忽略损坏配置 */ }
return { extraRoots: [] };
}
function saveConfig(cfg) {
fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), 'utf8');
// ---- 配置读写(自定义根目录列表;loadConfig 来自 load-config.js,每次读盘取最新值)----
function saveConfig(patch) {
// 合并写盘:保留 config.json 里的其它字段(port/dbName/outputRoot/businessDataDir),只更新传入的键
const merged = Object.assign({}, loadConfig(), patch);
fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf8');
}
// ---- 允许读取的根目录白名单 ----
@@ -338,7 +340,7 @@ function createOnceAutomation(name, prompt, skillsArr, connectorIdsArr) {
try {
const now = Date.now();
const id = 'automation-' + now;
const cwd = 'D:\\AgentSkill\\mcn-workshop'; // 工作台触发的会话归入 mcn-workshop 空间分组
const cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
const d = new Date(now + 5 * 1000);
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())}:${pad(d.getSeconds())}`;
@@ -490,7 +492,7 @@ const server = http.createServer((req, res) => {
// ---------- 数据功能(只读工作台自有数据库副本) ----------
const dshOk = (code, obj) => sendJSON(code, { ok: true, ...obj });
const dshGuard = (res2) => { if (!dsh.dbAvailable()) { sendJSON(200, { ok: false, error: '未检测到工作台数据库(mcn-plugin.db),本功能不可用' }); return true; } return false; };
const dshGuard = (res2) => { if (!dsh.dbAvailable()) { sendJSON(200, { ok: false, error: '未检测到工作台数据库(' + CFG.dbName + '),本功能不可用' }); return true; } return false; };
const qInt = (k, def) => { const v = Number(q.get(k)); return Number.isFinite(v) ? v : def; };
if (p === '/api/dsh/meta') {
@@ -627,7 +629,7 @@ const server = http.createServer((req, res) => {
}
if (p === '/api/dsh/storyboards') {
if (dshGuard()) return;
return dshOk(200, { items: dsh.listStoryboards(qInt('videoId', 0)) });
return dshOk(200, { items: dsh.listStoryboards(qInt('videoId', 0), qInt('rewriteId', 0)) });
}
if (p === '/api/dsh/review-detail') {
if (dshGuard()) return;
@@ -717,7 +719,7 @@ const server = http.createServer((req, res) => {
// 09-01 并发方案A:浏览器类任务(账号信息/视频列表/采集/导入)自动注入浏览器锁约束——
// 多个 AI 会话任务并行共享同一 9223 Chrome 实例,必须互斥使用,否则标签页互相导航抢占
const BROWSER_HINT = /账号信息|视频列表|保存并分析|采集|导入账号|网页采集|浏览器/;
const BROWSER_LOCK = 'D:\\\\AgentSkill\\\\mcn-workshop\\\\.browser-lock';
const BROWSER_LOCK = BROWSER_LOCK_FILE;
let finalPrompt = prompt.trim();
if (BROWSER_HINT.test(finalPrompt)) {
finalPrompt += `
@@ -733,7 +735,7 @@ const server = http.createServer((req, res) => {
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 cwd = SESSION_CWD; // 工作台触发的会话归入 mcn-work-shop 空间分组(由 ROOT 动态推导)
const d = new Date(Date.now() + 5 * 1000); // 未来 5 秒(08-31 根因:客户端只对未来 scheduledAt 补算 next_run_at;写 now=过去时间→不补算→调度器扫不到→卡死)
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())}:${pad(d.getSeconds())}`;
@@ -798,7 +800,7 @@ const server = http.createServer((req, res) => {
let contentText = '', personaText = '', videoRefText = '';
try {
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'), { readOnly: true });
const db = new DatabaseSync(path.join(ROOT, CFG.dbName), { readOnly: true });
const acc = db.prepare('SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1').get(accountName);
if (acc) {
contentText = String(acc.content || '').slice(0, 500);
@@ -873,7 +875,7 @@ const server = http.createServer((req, res) => {
let personaText = '', contentText = '';
try {
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'), { readOnly: true });
const db = new DatabaseSync(path.join(ROOT, CFG.dbName), { readOnly: true });
const acc = db.prepare('SELECT * FROM hot_accounts WHERE account_name=? LIMIT 1').get(accountName);
if (acc) {
contentText = String(acc.content || '').slice(0, 300);
@@ -1019,7 +1021,7 @@ const server = http.createServer((req, res) => {
const accountDir = resolveAccountDir(account, root);
if (!accountDir) return sendErr(404, '账号产出目录不存在: ' + account);
const { DatabaseSync } = require('node:sqlite');
const db = new DatabaseSync(path.join(ROOT, 'mcn-plugin.db'));
const db = new DatabaseSync(path.join(ROOT, CFG.dbName));
const res = importAccountIntoDb(db, account, accountDir);
db.close();
log('分析产物入库: ' + account + ' → ' + JSON.stringify(res));
@@ -1042,12 +1044,14 @@ const server = http.createServer((req, res) => {
});
});
// ---- 会话自动清理:mcn-workshop 空间超 6 小时无活动的已结束会话自动清除 ----
// 09-01 用户需求:工作台触发(cwd=D:\AgentSkill\mcn-workshop)的 AI 会话任务,超过 6 小时自动清除
// 判定:cwd 含 mcn-workshop + status=completed(执行中一律不删)+ 未删过 + 最后活动时间超过 6 小时
// ---- 会话自动清理:mcn-work-shop 空间超 6 小时无活动的已结束会话自动清除 ----
// 09-01 用户需求:工作台触发(cwd = 工作台目录 SESSION_CWD)的 AI 会话任务,超过 6 小时自动清除
// 09-10 路径治本:cwd 由写死盘符改为 ROOT 动态推导(<技能根>/mcn-work-shop);
// 清理模式放宽为 '%mcn-work%',同时覆盖历史会话(旧 mcn-workshop)与新建会话(mcn-work-shop)
// 判定:cwd 含 mcn-work + status=completed(执行中一律不删)+ 未删过 + 最后活动时间超过 6 小时
// 方式:软删除(置 deleted_at),客户端左侧会话栏按 deleted_at 过滤即不再显示;可逆安全
const SESSION_TTL_MS = 6 * 3600 * 1000; // 6 小时
const SESSION_CWD_PATTERN = '%mcn-workshop%'; // 与 /api/run 写入的 cwd 一致
const SESSION_CWD_PATTERN = '%mcn-work%'; // 兼容 SESSION_CWD(mcn-work-shop)与历史 cwd(mcn-workshop)
const SESSION_CLEAN_INTERVAL_MS = 60 * 60 * 1000; // 每小时检查一次
function cleanupExpiredSessions() {
@@ -1064,7 +1068,7 @@ function cleanupExpiredSessions() {
AND COALESCE(last_activity_at, updated_at, created_at, 0) < ?
`).run(now, SESSION_CWD_PATTERN, cutoff);
db.close();
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-workshop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
if (Number(r.changes) > 0) log(`会话自动清理:已清除 mcn-work-shop 超 6 小时无活动的已完成会话 ${r.changes} 条`);
} catch (e) {
console.error('会话自动清理失败:', e.message);
}