本提交为 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 本地已不存在,本提交记为删除
108 lines
3.6 KiB
JavaScript
108 lines
3.6 KiB
JavaScript
// dsh-plugin-mcn 视频/账号匹配工具(纯函数,拆分自 index.js)
|
||
import { homedir } from "node:os";
|
||
import { join } from "node:path";
|
||
import { readFileSync, existsSync } from "node:fs";
|
||
|
||
function normTitle(s) {
|
||
return String(s || "").toLowerCase().replace(/[\s#,。!?、,.!?·\-_~~::"“”()()【】]+/g, "");
|
||
}
|
||
|
||
function extractTopic(content) {
|
||
const s = String(content || "");
|
||
if (!s.trim()) return "";
|
||
const lines = s.replace(/\r\n/g, "\n").split("\n");
|
||
let start = -1;
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const t = lines[i].trim();
|
||
if (/^#{1,4}\s*选题\s*$/.test(t)) { start = i + 1; break; }
|
||
}
|
||
if (start < 0) return "";
|
||
const out = [];
|
||
for (let i = start; i < lines.length; i++) {
|
||
const t = lines[i].trim();
|
||
if (/^#{1,6}\s/.test(t) && !/^#{1,6}\s*选题\s*$/.test(t)) break;
|
||
if (t) out.push(lines[i].trim());
|
||
}
|
||
return out.join("\n").trim();
|
||
}
|
||
|
||
function titleDistance(a, b) {
|
||
const m = a.length, n = b.length;
|
||
if (!m) return n;
|
||
if (!n) return m;
|
||
let prev = new Array(n + 1), curr = new Array(n + 1);
|
||
for (let j = 0; j <= n; j++) prev[j] = j;
|
||
for (let i = 1; i <= m; i++) {
|
||
curr[0] = i;
|
||
for (let j = 1; j <= n; j++) {
|
||
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
||
}
|
||
[prev, curr] = [curr, prev];
|
||
}
|
||
return prev[n];
|
||
}
|
||
|
||
function findVideoByFolder(d, accountId, vDir, vName) {
|
||
const rows = d.prepare(`SELECT id, aweme_id, video_title FROM account_videos WHERE account_id=?`).all(accountId);
|
||
const pick = (title) => {
|
||
const t = String(title || "").trim();
|
||
if (!t) return undefined;
|
||
const exact = rows.find((r) => String(r.video_title || "") === t);
|
||
if (exact) return exact;
|
||
const tn = normTitle(t);
|
||
if (!tn) return undefined;
|
||
let best = null;
|
||
for (const r of rows) {
|
||
const cn = normTitle(r.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 = { id: r.id, aweme_id: r.aweme_id, len: tn.length };
|
||
}
|
||
}
|
||
if (best) return best;
|
||
let bestSim = null;
|
||
for (const r of rows) {
|
||
const cn = normTitle(r.video_title);
|
||
if (!cn) continue;
|
||
const ratio = 1 - titleDistance(tn, cn) / Math.max(tn.length, cn.length, 1);
|
||
if (ratio > 0.8 && (!bestSim || ratio > bestSim.ratio)) bestSim = { id: r.id, aweme_id: r.aweme_id, ratio };
|
||
}
|
||
return bestSim ? bestSim : undefined;
|
||
};
|
||
for (const jf of ["原视频解析.json", "content.json"]) {
|
||
const jp = join(vDir, jf);
|
||
if (!existsSync(jp)) continue;
|
||
try {
|
||
const jd = JSON.parse(readFileSync(jp, "utf8"));
|
||
const hit = pick(jd.title);
|
||
if (hit) return hit;
|
||
} catch (e) { /* 单个 json 解析失败不影响后续匹配 */ }
|
||
}
|
||
const first = String(vName || "").split("_")[0].trim();
|
||
if (first) {
|
||
const hit = pick(first);
|
||
if (hit) return hit;
|
||
}
|
||
return undefined;
|
||
}
|
||
|
||
function resolveAccountDirs(acc) {
|
||
const out = [];
|
||
if (acc && typeof acc.source_dir === "string" && acc.source_dir && existsSync(acc.source_dir)) out.push(acc.source_dir);
|
||
const legacy = acc && acc.account_name ? join(homedir(), "Desktop", "账号对标", acc.account_name) : null;
|
||
if (legacy && existsSync(legacy)) out.push(legacy);
|
||
return out;
|
||
}
|
||
|
||
function resolveAccountDir(acc) {
|
||
return resolveAccountDirs(acc)[0] || null;
|
||
}
|
||
|
||
/** 数值化(非法输入返回 0) */
|
||
function num(v) {
|
||
const n = Number(v);
|
||
return Number.isFinite(n) ? n : 0;
|
||
}
|
||
|
||
export { num, normTitle, extractTopic, titleDistance, findVideoByFolder, resolveAccountDirs, resolveAccountDir };
|