baseline: dsh 工作区基线快照(2026-09-10,全新历史起点)
本提交为 dsh_mcn_workshop 仓库的首个提交(旧仓 dsh_MCNProject 已停用), 完整固化 C:/Users/Administrator/.dsh 当前磁盘状态。 1) 临时数据清理:移出 sessions/(81M)、logs/、skill-backup-20260906/(14M)、 scripts/__pycache__、storages/workspace.json.bak —— 合计 94M(.dsh 162M → 68M) 2) 凭据脱敏:.credentials.yaml(含明文 DEEPSEEK_API_KEY)移出 git 跟踪并加入 .gitignore;mcn-data-insight/scripts/setup_redfox_key.py 中真实 key 示例改为占位符 3) .gitignore 增补 logs/、skill-backup-*/ 规则 4) 技能结构以当前磁盘状态为准:skills/ = mcn-short-video / impeccable / taste-skill / dsh-multi-user-migration;旧技能目录(mcn-dou-analysis、storyboard-prompt、 short-video-script、browser-harness、mcn-data-insight)与 profiles/web/node_modules/dsh-vision-router 本地已不存在,本提交记为删除
This commit is contained in:
commit
856e632807
880 files changed
+225426
No files matched your search
+299
@@ -0,0 +1,299 @@
|
||||
// dsh-plugin-mcn 本地 SQLite 缓存(node:sqlite,Node 24 内置;拆分自 index.js)
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
import { join } from "node:path";
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { DB_PATH } from "./config.js";
|
||||
import { normTitle, titleDistance, findVideoByFolder, resolveAccountDirs } from "./video-tools.js";
|
||||
|
||||
let db;
|
||||
|
||||
/** 从 AI 写作脚本正文提取标题(兼容现有多种首行格式):
|
||||
* - 去掉 markdown 标题标记(# 前缀)
|
||||
* - 去掉「S9 故事脚本」等前缀
|
||||
* - 优先取《…》内内容,否则用整行
|
||||
* - 限制长度 80 字符,空则返回 null
|
||||
*/
|
||||
function extractScriptTitle(scriptText) {
|
||||
if (typeof scriptText !== "string" || !scriptText.trim()) return null;
|
||||
let line = scriptText.replace(/\r\n/g, "\n").split("\n").find((l) => l.trim()) || "";
|
||||
let t = line.trim();
|
||||
t = t.replace(/^#+\s*/, "").trim(); // 去 markdown 标题标记
|
||||
t = t.replace(/^S\d+\s*(故事)?(脚|剧)?本\s*/, "").trim(); // 去「S9 故事脚本」等前缀
|
||||
const m = t.match(/《([^》]+)》/);
|
||||
if (m && m[1].trim()) t = m[1].trim();
|
||||
t = t.replace(/^#+\s*/, "").trim();
|
||||
return t ? t.slice(0, 80) : null;
|
||||
}
|
||||
|
||||
function initDb() {
|
||||
if (db) return db;
|
||||
db = new DatabaseSync(DB_PATH);
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS hot_accounts (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_name TEXT NOT NULL,
|
||||
content TEXT,
|
||||
track TEXT,
|
||||
tags TEXT,
|
||||
masterpiece TEXT,
|
||||
created_by TEXT,
|
||||
updated_by TEXT,
|
||||
created_time TEXT,
|
||||
updated_time TEXT,
|
||||
sort INTEGER DEFAULT 0,
|
||||
del_flag INTEGER DEFAULT 0,
|
||||
account_type TEXT DEFAULT 'signed',
|
||||
favorite INTEGER DEFAULT 0
|
||||
);
|
||||
`);
|
||||
// 存量数据库迁移:旧表可能缺 account_type 列(CREATE TABLE IF NOT EXISTS 不会为已存在表加列)
|
||||
const cols = db.prepare(`PRAGMA table_info(hot_accounts)`).all().map((c) => c.name);
|
||||
if (!cols.includes("account_type")) {
|
||||
db.exec(`ALTER TABLE hot_accounts ADD COLUMN account_type TEXT DEFAULT 'signed'`);
|
||||
}
|
||||
if (!cols.includes("favorite")) {
|
||||
db.exec(`ALTER TABLE hot_accounts ADD COLUMN favorite INTEGER DEFAULT 0`);
|
||||
}
|
||||
// 外部账号扩展列(幂等迁移)
|
||||
const extCols = {
|
||||
douyin_id: "TEXT",
|
||||
sec_uid: "TEXT",
|
||||
followers: "TEXT",
|
||||
total_likes: "TEXT",
|
||||
works_count: "INTEGER",
|
||||
ip_location: "TEXT",
|
||||
location: "TEXT",
|
||||
age: "INTEGER",
|
||||
bio: "TEXT",
|
||||
source_dir: "TEXT",
|
||||
};
|
||||
for (const [name, type] of Object.entries(extCols)) {
|
||||
if (!cols.includes(name)) {
|
||||
db.exec(`ALTER TABLE hot_accounts ADD COLUMN ${name} ${type}`);
|
||||
}
|
||||
}
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_hot_accounts_name ON hot_accounts(account_name);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_hot_accounts_track ON hot_accounts(track);`);
|
||||
db.exec(`CREATE INDEX IF NOT EXISTS idx_hot_accounts_type ON hot_accounts(account_type);`);
|
||||
// 存量数据兼容:无 account_type 的按 signed 处理
|
||||
try { db.exec(`UPDATE hot_accounts SET account_type='signed' WHERE account_type IS NULL OR account_type=''`); } catch (e) {}
|
||||
// 外部账号关联表(见 Architecture/外部账号数据结构设计.md)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS account_videos (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_id INTEGER,
|
||||
aweme_id TEXT,
|
||||
video_title TEXT,
|
||||
video_url TEXT,
|
||||
like_count INTEGER DEFAULT 0,
|
||||
like_display TEXT,
|
||||
comment_count INTEGER DEFAULT 0,
|
||||
share_count INTEGER DEFAULT 0,
|
||||
collect_count INTEGER DEFAULT 0,
|
||||
play_count INTEGER DEFAULT 0,
|
||||
duration TEXT,
|
||||
publish_time TEXT,
|
||||
tags TEXT,
|
||||
collected_time TEXT,
|
||||
source_dir TEXT,
|
||||
topic TEXT,
|
||||
UNIQUE(account_id, aweme_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS account_video_analysis (
|
||||
id INTEGER PRIMARY KEY,
|
||||
video_id INTEGER,
|
||||
aweme_id TEXT,
|
||||
analysis_time TEXT,
|
||||
content_json TEXT,
|
||||
summary TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS account_video_source (
|
||||
id INTEGER PRIMARY KEY,
|
||||
video_id INTEGER,
|
||||
analysis_time TEXT,
|
||||
content_json TEXT,
|
||||
summary TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS account_persona (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_id INTEGER,
|
||||
analysis_time TEXT,
|
||||
content_json TEXT,
|
||||
summary TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS account_analysis (
|
||||
id INTEGER PRIMARY KEY,
|
||||
account_id INTEGER,
|
||||
analysis_time TEXT,
|
||||
content_json TEXT,
|
||||
summary TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS creative_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
created_time TEXT,
|
||||
source TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS rewrite_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
video_id INTEGER,
|
||||
aweme_id TEXT,
|
||||
account_id INTEGER,
|
||||
script_text TEXT,
|
||||
topic TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_time TEXT,
|
||||
updated_time TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS storyboard_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
video_id INTEGER,
|
||||
rewrite_id INTEGER,
|
||||
script_text TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_time TEXT,
|
||||
updated_time TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS script_review (
|
||||
id INTEGER PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
rewrite_id INTEGER,
|
||||
video_id INTEGER,
|
||||
dims_json TEXT,
|
||||
total TEXT,
|
||||
verdict TEXT,
|
||||
issues_json TEXT,
|
||||
content TEXT,
|
||||
summary TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_time TEXT,
|
||||
updated_time TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_script_review_type ON script_review(type);
|
||||
CREATE INDEX IF NOT EXISTS idx_script_review_rewrite ON script_review(rewrite_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_script_review_video ON script_review(video_id);
|
||||
`);
|
||||
// 存量迁移:老库 account_videos 缺 topic 列(选题,从源视频解析 json 提取)
|
||||
try {
|
||||
const tcols = db.prepare(`PRAGMA table_info(account_videos)`).all().map((c) => c.name);
|
||||
if (!tcols.includes("topic")) db.exec(`ALTER TABLE account_videos ADD COLUMN topic TEXT`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:rewrite_log 缺 title 列(AI 写作脚本标题,从脚本首行提取)
|
||||
try {
|
||||
const rwcols = db.prepare(`PRAGMA table_info(rewrite_log)`).all().map((c) => c.name);
|
||||
if (!rwcols.includes("title")) db.exec(`ALTER TABLE rewrite_log ADD COLUMN title TEXT`);
|
||||
// 存量回填:仅 title 为空且脚本非空时,从脚本首行提取(幂等)
|
||||
for (const row of db.prepare(`SELECT id, script_text FROM rewrite_log WHERE (title IS NULL OR title = '') AND script_text IS NOT NULL AND script_text <> ''`).all()) {
|
||||
const title = extractScriptTitle(row.script_text);
|
||||
if (title) db.prepare(`UPDATE rewrite_log SET title=? WHERE id=?`).run(title, row.id);
|
||||
}
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:account_video_analysis 缺 aweme_id 列(拆解分析关联视频ID,业务主键)
|
||||
try {
|
||||
const acols = db.prepare(`PRAGMA table_info(account_video_analysis)`).all().map((c) => c.name);
|
||||
if (!acols.includes("aweme_id")) db.exec(`ALTER TABLE account_video_analysis ADD COLUMN aweme_id TEXT`);
|
||||
db.exec(`UPDATE account_video_analysis SET aweme_id = (SELECT aweme_id FROM account_videos WHERE id = account_video_analysis.video_id) WHERE aweme_id IS NULL AND video_id IS NOT NULL`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:account_video_source 缺 aweme_id 列(原视频解析关联视频ID,业务主键)
|
||||
try {
|
||||
const scols = db.prepare(`PRAGMA table_info(account_video_source)`).all().map((c) => c.name);
|
||||
if (!scols.includes("aweme_id")) db.exec(`ALTER TABLE account_video_source ADD COLUMN aweme_id TEXT`);
|
||||
if (!scols.includes("analysis_json")) db.exec(`ALTER TABLE account_video_source ADD COLUMN analysis_json TEXT`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:hot_accounts 缺 is_ai 列(是否AI账号,默认「否」,赛道字段后展示)
|
||||
try {
|
||||
const hcols = db.prepare(`PRAGMA table_info(hot_accounts)`).all().map((c) => c.name);
|
||||
if (!hcols.includes("is_ai")) db.exec(`ALTER TABLE hot_accounts ADD COLUMN is_ai TEXT DEFAULT '否'`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:hot_accounts 缺 del_flag 列(软删标记,同步 V1.0 工作台:批量删除账号=软删+连坐清关联)
|
||||
try {
|
||||
const hdcols = db.prepare(`PRAGMA table_info(hot_accounts)`).all().map((c) => c.name);
|
||||
if (!hdcols.includes("del_flag")) db.exec(`ALTER TABLE hot_accounts ADD COLUMN del_flag INTEGER DEFAULT 0`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量迁移:rewrite_log 缺 gen_method 列(生成方式:ref=参考选题创作 / new=原创选题创作 / abc=ABC融合创作;
|
||||
// 同步 V1.0 工作台 v25 三 tab 口径;历史行按 video_id 是否为空回填)
|
||||
try {
|
||||
const gwcols = db.prepare(`PRAGMA table_info(rewrite_log)`).all().map((c) => c.name);
|
||||
if (!gwcols.includes("gen_method")) db.exec(`ALTER TABLE rewrite_log ADD COLUMN gen_method TEXT DEFAULT 'ref'`);
|
||||
db.exec(`UPDATE rewrite_log SET gen_method='new' WHERE (video_id IS NULL OR video_id = 0) AND (gen_method IS NULL OR gen_method = '')`);
|
||||
db.exec(`UPDATE rewrite_log SET gen_method='ref' WHERE (video_id IS NOT NULL AND video_id > 0) AND (gen_method IS NULL OR gen_method = '')`);
|
||||
} catch (e) { /* 迁移失败不阻塞 */ }
|
||||
// 存量清洗:赛道值去掉「赛道」后缀(如「生活vlog赛道」→「生活vlog」,全库统一;幂等)
|
||||
try {
|
||||
db.exec(`UPDATE hot_accounts SET track = REPLACE(track, '赛道', '') WHERE track LIKE '%赛道%'`);
|
||||
} catch (e) { /* 清洗失败不阻塞 */ }
|
||||
// 存量修复:source 按 content_json 标题匹配当前 account_videos,回填 aweme_id 并修正错位 video_id
|
||||
// (早期视频重建后自增 id 失效,旧记录指向已删除视频;aweme_id 为业务主键不受重建影响)
|
||||
try { backfillSourceAwemeIds(db); } catch (e) { console.warn(`[dsh-plugin-mcn] source 回填失败: ${e.message}`); }
|
||||
return db;
|
||||
}
|
||||
|
||||
function backfillSourceAwemeIds(d) {
|
||||
// 补 analysis_json(每次启动执行,不依赖 aweme_id 回填):文件系统里存在 analysis.json 的记录(按账号内标题匹配)
|
||||
try {
|
||||
for (const acc of d.prepare(`SELECT id, account_name, source_dir FROM hot_accounts`).all()) {
|
||||
for (const dir of resolveAccountDirs(acc)) {
|
||||
const vsDir = join(dir, "视频对标");
|
||||
if (!existsSync(vsDir)) continue;
|
||||
for (const e of readdirSync(vsDir, { withFileTypes: true })) {
|
||||
if (!e.isDirectory()) continue;
|
||||
const vDir = join(vsDir, e.name);
|
||||
const ap = join(vDir, "analysis.json");
|
||||
if (!existsSync(ap)) continue;
|
||||
const vd = findVideoByFolder(d, acc.id, vDir, e.name);
|
||||
if (!vd) continue;
|
||||
const row = d.prepare(`SELECT id FROM account_video_source WHERE aweme_id=? OR video_id=?`).get(vd.aweme_id, vd.id);
|
||||
if (row) {
|
||||
const aj = readFileSync(ap, "utf8");
|
||||
d.prepare(`UPDATE account_video_source SET analysis_json=? WHERE id=?`).run(aj, row.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn(`[dsh-plugin-mcn] analysis_json 补录失败: ${e.message}`); }
|
||||
const rows = d.prepare(`SELECT id, video_id, aweme_id, content_json FROM account_video_source`).all();
|
||||
const missing = rows.filter((r) => !r.aweme_id);
|
||||
if (missing.length === 0) return 0;
|
||||
const all = d.prepare(`SELECT id, aweme_id, video_title FROM account_videos`).all();
|
||||
const find = (title) => {
|
||||
const t = String(title || "").trim();
|
||||
if (!t) return null;
|
||||
const exact = all.find((v) => String(v.video_title || "") === t);
|
||||
if (exact) return exact;
|
||||
const tn = normTitle(t);
|
||||
if (!tn) return null;
|
||||
let best = null;
|
||||
for (const v of all) {
|
||||
const cn = normTitle(v.video_title);
|
||||
if (!cn || cn.length < 6 || tn.length < 6) continue;
|
||||
if (tn.includes(cn) || cn.includes(tn)) {
|
||||
if (!best || tn.length > best._len) { best = { ...v, _len: tn.length }; }
|
||||
}
|
||||
}
|
||||
if (best) return best;
|
||||
for (const v of all) {
|
||||
const cn = normTitle(v.video_title);
|
||||
if (!cn) continue;
|
||||
const ratio = 1 - titleDistance(tn, cn) / Math.max(tn.length, cn.length, 1);
|
||||
if (ratio > 0.8 && (!best || ratio > best._ratio)) { best = { ...v, _ratio: ratio }; }
|
||||
}
|
||||
return best;
|
||||
};
|
||||
let fixed = 0;
|
||||
for (const r of missing) {
|
||||
let title = null;
|
||||
try {
|
||||
const jd = JSON.parse(r.content_json);
|
||||
title = jd && jd.title != null ? String(jd.title) : null;
|
||||
} catch (e) { /* 单个解析失败跳过 */ }
|
||||
const hit = title ? find(title) : null;
|
||||
if (hit && hit.aweme_id) {
|
||||
d.prepare(`UPDATE account_video_source SET aweme_id=?, video_id=? WHERE id=?`).run(hit.aweme_id, hit.id, r.id);
|
||||
fixed++;
|
||||
}
|
||||
}
|
||||
// 补 analysis_json 完成(已上移至函数开头执行,保证每次启动都补录)
|
||||
if (fixed > 0) console.log(`[dsh-plugin-mcn] source 表回填 ${fixed}/${missing.length} 条 aweme_id(标题匹配修复错位)`);
|
||||
return fixed;
|
||||
}
|
||||
|
||||
export { initDb, backfillSourceAwemeIds, extractScriptTitle };
|
||||
Reference in new issue
Block a user