feat(MCN工作台): 数据功能页+图标扁平化+多项UI优化
- 工作台大版本升级:hash 路由 + 6 数据功能页(周榜/热门账号/账号详情/视频详情/AI写脚本/AI写作复盘),只读复用 dsh 库副本 + 抖音榜单 JSON - 数据库隔离:复制 dsh 库到 mcn-work-shop/,与 dsh 原库各访问各的(已 gitignore) - 图标全面扁平化:Lucide 线性 SVG,功能图标亮色区分,文字标题去图标 - 首页重构:功能入口卡 + AI写剧本快捷卡片(2列/8个/5行预览) + 独立帮助页 + header 精简 - 新增设计技能 impeccable + taste-skill(生成网页时参考) - 修复:fmtTime 跨 script 全局冲突(P0)、弹窗关闭、外链新标签打开等
This commit is contained in:
1 parent
e1dc14e8a3
commit
d3f98dda77
120 files changed
+44528
-1
No files matched your search
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Shared helpers for the pending-manual-edits buffer on disk.
|
||||
*
|
||||
* Location: .impeccable/live/pending-manual-edits.json (project-local).
|
||||
* Schema: { version: 1, entries: [{ id, pageUrl, element, ops, stagedAt }] }
|
||||
*
|
||||
* Each entry corresponds to one Save action from the browser. Ops merge by
|
||||
* (pageUrl, ref): if the user re-edits the same element before committing, the
|
||||
* existing entry's `newText` is replaced and `originalText` is kept (it holds
|
||||
* the real source state).
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getLiveDir } from '../lib/impeccable-paths.mjs';
|
||||
|
||||
const BUFFER_VERSION = 1;
|
||||
const BUFFER_FILENAME = 'pending-manual-edits.json';
|
||||
|
||||
export function getBufferPath(cwd = process.cwd()) {
|
||||
return path.join(getLiveDir(cwd), BUFFER_FILENAME);
|
||||
}
|
||||
|
||||
export function readBuffer(cwd = process.cwd()) {
|
||||
return readBufferInternal(cwd, { strict: false });
|
||||
}
|
||||
|
||||
export function readBufferStrict(cwd = process.cwd()) {
|
||||
return readBufferInternal(cwd, { strict: true });
|
||||
}
|
||||
|
||||
function readBufferInternal(cwd, { strict }) {
|
||||
const filePath = getBufferPath(cwd);
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, 'utf-8');
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.entries)) {
|
||||
if (strict) throw new Error('manual_edit_buffer_invalid_schema');
|
||||
return { version: BUFFER_VERSION, entries: [] };
|
||||
}
|
||||
return { version: BUFFER_VERSION, entries: parsed.entries };
|
||||
} catch (err) {
|
||||
if (strict && err?.code !== 'ENOENT') {
|
||||
throw new Error('manual_edit_buffer_unreadable: ' + (err.message || String(err)));
|
||||
}
|
||||
return { version: BUFFER_VERSION, entries: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export function writeBuffer(cwd, buffer) {
|
||||
const filePath = getBufferPath(cwd);
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify({ version: BUFFER_VERSION, entries: buffer.entries }, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a new entry into the buffer. For each op in the new entry, if there's
|
||||
* already a buffered op for the same (pageUrl, ref), update that op's newText
|
||||
* and keep its original originalText (the true source state). Otherwise add
|
||||
* the op (creating an entry if needed).
|
||||
*
|
||||
* Multiple ops in one Save are allowed; each is keyed by (pageUrl, ref).
|
||||
*/
|
||||
export function stageEntry(cwd, newEntry) {
|
||||
const buf = readBufferStrict(cwd);
|
||||
const pageUrl = newEntry.pageUrl;
|
||||
for (const newOp of newEntry.ops) {
|
||||
let mergedIntoExisting = false;
|
||||
for (const existing of buf.entries) {
|
||||
if (existing.pageUrl !== pageUrl) continue;
|
||||
const existingOpIdx = existing.ops.findIndex((op) => op.ref === newOp.ref);
|
||||
if (existingOpIdx >= 0) {
|
||||
// Keep the original source text but refresh the latest DOM/source evidence.
|
||||
existing.ops[existingOpIdx] = {
|
||||
...newOp,
|
||||
originalText: existing.ops[existingOpIdx].originalText,
|
||||
newText: newOp.newText,
|
||||
deleted: newOp.deleted || false,
|
||||
};
|
||||
if (newEntry.element) existing.element = newEntry.element;
|
||||
existing.stagedAt = new Date().toISOString();
|
||||
mergedIntoExisting = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mergedIntoExisting) continue;
|
||||
// No existing op for this (pageUrl, ref). Find or create an entry to hold it.
|
||||
let entry = buf.entries.find((e) => e.pageUrl === pageUrl && e.id === newEntry.id);
|
||||
if (!entry) {
|
||||
entry = {
|
||||
id: newEntry.id,
|
||||
pageUrl,
|
||||
element: newEntry.element,
|
||||
ops: [],
|
||||
stagedAt: new Date().toISOString(),
|
||||
};
|
||||
buf.entries.push(entry);
|
||||
}
|
||||
entry.ops.push(newOp);
|
||||
entry.stagedAt = new Date().toISOString();
|
||||
}
|
||||
writeBuffer(cwd, buf);
|
||||
return buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entries matching a predicate. Returns count of removed *ops* (not
|
||||
* entries) so callers report a unit consistent with truncateBuffer and the
|
||||
* pill's per-page op count. Empty entries (no ops left) are also pruned.
|
||||
*/
|
||||
export function removeEntries(cwd, predicate) {
|
||||
const buf = readBuffer(cwd);
|
||||
let removedOps = 0;
|
||||
const kept = [];
|
||||
for (const entry of buf.entries) {
|
||||
if (predicate(entry)) {
|
||||
removedOps += entry.ops?.length || 0;
|
||||
} else if (entry.ops && entry.ops.length > 0) {
|
||||
kept.push(entry);
|
||||
}
|
||||
}
|
||||
buf.entries = kept;
|
||||
writeBuffer(cwd, buf);
|
||||
return removedOps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count by page for the counter UI. Returns { totalCount, perPage: {[pageUrl]: count} }.
|
||||
*/
|
||||
export function countByPage(cwd = process.cwd()) {
|
||||
const buf = readBuffer(cwd);
|
||||
const perPage = {};
|
||||
let totalCount = 0;
|
||||
for (const entry of buf.entries) {
|
||||
const n = entry.ops.length;
|
||||
perPage[entry.pageUrl] = (perPage[entry.pageUrl] || 0) + n;
|
||||
totalCount += n;
|
||||
}
|
||||
return { totalCount, perPage };
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate the buffer to empty (used by discard-all). Returns the count of
|
||||
* removed ops.
|
||||
*/
|
||||
export function truncateBuffer(cwd) {
|
||||
const buf = readBuffer(cwd);
|
||||
let removed = 0;
|
||||
for (const entry of buf.entries) removed += entry.ops.length;
|
||||
writeBuffer(cwd, { version: BUFFER_VERSION, entries: [] });
|
||||
return removed;
|
||||
}
|
||||
Reference in new issue
Block a user