fix: 对齐配置策略并加固书签导入

1) src/bookmark-processor.js:加固书签导入
- config/user/ 不存在时先从 config/_default/ 初始化一套完整用户配置(符合“完全替换”策略)
- 优先更新 config/user/site.yml 的 navigation,确保包含 id: bookmarks;失败再兼容旧 navigation.yml
- 无 .html 书签文件时由报错改为警告并跳过,不阻塞流程
- 导航更新日志按结果输出,不再无条件“已更新”

2) config/README.md:修正文档
- 明确配置加载为“完全替换、不合并”
- 补充首次使用建议:复制 config/_default/ 到 config/user/

3) package.json:许可证对齐
- license 从 MIT 改为 AGPL-3.0-only(与 LICENSE/README 对齐)

4) .gitignore:忽略工具目录
- 新增忽略 .serena/
This commit is contained in:
rbetree
2025-12-22 00:19:44 +08:00
parent b95a39db1d
commit 7a7bf361e3
4 changed files with 159 additions and 39 deletions

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ dist/
.cursorindexingignore
.cursor
.spec-workflow/
.serena/
# 系统文件
.DS_Store

View File

@@ -39,14 +39,12 @@ config/
## 配置加载机制
MeNav 配置系统使用深度合并机制,按以下顺序加载和合并配置
MeNav 配置系统采用“完全替换”策略(不合并),按以下优先级选择**唯一**的一套配置目录
1. 加载 `_default` 目录中的所有配置(基础层)
2. 加载 `user` 目录中的配置(如果存在,覆盖同名配置项)
3. 深度合并所有配置,确保用户配置优先级高于默认配置
4. 应用最终合并后的配置生成网站
1. 若存在 `config/user/`,则只加载该目录下的配置,并**完全忽略** `config/_default/`
2. 否则加载 `config/_default/` 作为默认配置
这种机制使用户只需配置想要自定义的部分,其余部分默认配置提供
也就是说:`config/user/` 一旦存在,就需要包含一套完整的配置(例如 `site.yml` 与必要的 `pages/*.yml`),系统不会把缺失部分默认配置补齐
## 模块化配置文件
@@ -170,7 +168,7 @@ categories:
3. **配置管理**:
- 利用模块化结构分类管理配置
- 只覆盖需要自定义的配置项
- 首次使用建议先完整复制 `config/_default/``config/user/`,再按需修改
- 定期备份您的用户配置
4. **配置验证**:

View File

@@ -17,7 +17,7 @@
"personal"
],
"author": "Your Name",
"license": "MIT",
"license": "AGPL-3.0-only",
"dependencies": {
"js-yaml": "^4.1.0",
"handlebars": "^4.7.8",

View File

@@ -6,6 +6,8 @@ const yaml = require('js-yaml');
const BOOKMARKS_DIR = 'bookmarks';
// 模块化配置目录
const CONFIG_USER_DIR = 'config/user';
// 模块化默认配置目录
const CONFIG_DEFAULT_DIR = 'config/_default';
// 模块化页面配置目录
const CONFIG_USER_PAGES_DIR = path.join(CONFIG_USER_DIR, 'pages');
// 模块化输出配置文件路径
@@ -13,6 +15,109 @@ const MODULAR_OUTPUT_FILE = path.join(CONFIG_USER_PAGES_DIR, 'bookmarks.yml');
// 模块化默认书签配置文件路径
const MODULAR_DEFAULT_BOOKMARKS_FILE = 'config/_default/pages/bookmarks.yml';
const USER_SITE_YML = path.join(CONFIG_USER_DIR, 'site.yml');
const DEFAULT_SITE_YML = path.join(CONFIG_DEFAULT_DIR, 'site.yml');
const LEGACY_USER_NAV_YML = path.join(CONFIG_USER_DIR, 'navigation.yml');
const LEGACY_DEFAULT_NAV_YML = path.join(CONFIG_DEFAULT_DIR, 'navigation.yml');
function ensureUserConfigInitialized() {
if (fs.existsSync(CONFIG_USER_DIR)) {
return { initialized: false, source: 'existing' };
}
if (fs.existsSync(CONFIG_DEFAULT_DIR)) {
fs.cpSync(CONFIG_DEFAULT_DIR, CONFIG_USER_DIR, { recursive: true });
console.log('[INFO] 检测到 config/user 不存在,已从 config/_default 初始化用户配置(完全替换策略需要完整配置)。');
return { initialized: true, source: '_default' };
}
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
console.log('[WARN] 未找到默认配置目录 config/_default已创建空的 config/user 目录;建议补齐 site.yml 与 pages/*.yml。');
return { initialized: true, source: 'empty' };
}
function ensureUserSiteYmlExists() {
if (fs.existsSync(USER_SITE_YML)) {
return true;
}
if (fs.existsSync(DEFAULT_SITE_YML)) {
if (!fs.existsSync(CONFIG_USER_DIR)) {
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
}
fs.copyFileSync(DEFAULT_SITE_YML, USER_SITE_YML);
console.log('[INFO] 未找到 config/user/site.yml已从 config/_default/site.yml 复制一份。');
return true;
}
console.log('[WARN] 未找到可用的 site.yml无法自动更新导航请手动在 config/user/site.yml 添加 navigation含 id: bookmarks。');
return false;
}
function upsertBookmarksNavInSiteYml(siteYmlPath) {
try {
const raw = fs.readFileSync(siteYmlPath, 'utf8');
const loaded = yaml.load(raw);
if (!loaded || typeof loaded !== 'object') {
return { updated: false, reason: 'site_yml_not_object' };
}
const navigation = loaded.navigation;
if (Array.isArray(navigation) && navigation.some(item => item && item.id === 'bookmarks')) {
return { updated: false, reason: 'already_present' };
}
if (navigation !== undefined && !Array.isArray(navigation)) {
return { updated: false, reason: 'navigation_not_array' };
}
const lines = raw.split(/\r?\n/);
const navLineIndex = lines.findIndex(line => /^navigation\s*:/.test(line));
const itemIndent = ' ';
const propIndent = `${itemIndent} `;
const snippet = [
`${itemIndent}- name: 书签`,
`${propIndent}icon: fas fa-bookmark`,
`${propIndent}id: bookmarks`,
];
if (navLineIndex === -1) {
// 不存在 navigation 段:直接追加一个新的块(尽量不破坏原文件结构)
const normalized = raw.endsWith('\n') ? raw : `${raw}\n`;
const spacer = normalized.trim().length === 0 ? '' : '\n';
const updatedRaw = `${normalized}${spacer}navigation:\n${snippet.join('\n')}\n`;
fs.writeFileSync(siteYmlPath, updatedRaw, 'utf8');
return { updated: true, reason: 'added_navigation_block' };
}
// 找到 navigation 块末尾(遇到下一个顶层 key 时结束)
let insertAt = lines.length;
for (let i = navLineIndex + 1; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '' || /^\s*#/.test(line)) continue;
if (/^[A-Za-z0-9_-]+\s*:/.test(line)) {
insertAt = i;
break;
}
}
const updatedLines = [...lines];
// 在块末尾插入,确保块内至少有一个空行分隔更易读
if (insertAt > 0 && updatedLines[insertAt - 1].trim() !== '') {
snippet.unshift('');
}
updatedLines.splice(insertAt, 0, ...snippet);
fs.writeFileSync(siteYmlPath, `${updatedLines.join('\n')}\n`, 'utf8');
return { updated: true, reason: 'updated_navigation_block' };
} catch (error) {
return { updated: false, reason: 'error', error };
}
}
// 图标映射根据URL关键字匹配合适的图标
const ICON_MAPPING = {
'github.com': 'fab fa-github',
@@ -627,40 +732,42 @@ ${yamlString}`;
// 更新导航以包含书签页面
function updateNavigationWithBookmarks() {
// 模块化配置文件
const modularUserNavFile = path.join(CONFIG_USER_DIR, 'navigation.yml');
const modularDefaultNavFile = 'config/_default/navigation.yml';
let navigationUpdated = false;
// 按优先级顺序尝试更新导航配置
// 1. 首选: 模块化用户导航配置
if (fs.existsSync(modularUserNavFile)) {
navigationUpdated = updateNavigationFile(modularUserNavFile);
// 1) 优先更新 site.yml当前推荐方式
if (ensureUserSiteYmlExists()) {
const result = upsertBookmarksNavInSiteYml(USER_SITE_YML);
if (result.updated) {
return { updated: true, target: 'site.yml', reason: result.reason };
}
if (result.reason === 'already_present') {
return { updated: false, target: 'site.yml', reason: 'already_present' };
}
if (result.reason === 'error') {
return { updated: false, target: 'site.yml', reason: 'error', error: result.error };
}
// 如果 site.yml 无法更新(如 navigation 格式异常),继续尝试旧版 navigation.yml
}
// 2. 其次: 模块化默认导航配置
else if (fs.existsSync(modularDefaultNavFile)) {
// 如果用户导航文件不存在,我们需要先创建它,然后基于默认文件更新
try {
// 读取默认导航文件
const defaultNavContent = fs.readFileSync(modularDefaultNavFile, 'utf8');
const defaultNav = yaml.load(defaultNavContent);
// 确保目录存在
// 2) 兼容旧版:独立 navigation.yml
if (fs.existsSync(LEGACY_USER_NAV_YML)) {
const updated = updateNavigationFile(LEGACY_USER_NAV_YML);
return { updated, target: 'navigation.yml', reason: updated ? 'updated' : 'already_present' };
}
if (fs.existsSync(LEGACY_DEFAULT_NAV_YML)) {
try {
const defaultNavContent = fs.readFileSync(LEGACY_DEFAULT_NAV_YML, 'utf8');
if (!fs.existsSync(CONFIG_USER_DIR)) {
fs.mkdirSync(CONFIG_USER_DIR, { recursive: true });
}
// 写入用户导航文件
fs.writeFileSync(modularUserNavFile, defaultNavContent, 'utf8');
// 更新新创建的文件
navigationUpdated = updateNavigationFile(modularUserNavFile);
fs.writeFileSync(LEGACY_USER_NAV_YML, defaultNavContent, 'utf8');
const updated = updateNavigationFile(LEGACY_USER_NAV_YML);
return { updated, target: 'navigation.yml', reason: updated ? 'updated' : 'already_present' };
} catch (error) {
console.error(`Error creating user navigation file:`, error);
return { updated: false, target: 'navigation.yml', reason: 'error', error };
}
}
return { updated: false, target: null, reason: 'no_navigation_config' };
}
// 更新单个导航配置文件
@@ -716,7 +823,7 @@ async function main() {
console.log('[步骤 1/5] 查找书签文件...');
const bookmarkFile = getLatestBookmarkFile();
if (!bookmarkFile) {
console.log('[ERROR] 未找到书签文件,处理终止');
console.log('[WARN] 未找到书签文件,已跳过处理将HTML书签文件放入 bookmarks/ 后再运行)。');
return;
}
console.log('[SUCCESS] 找到书签文件\n');
@@ -748,6 +855,9 @@ async function main() {
// 保存文件
console.log('[步骤 5/5] 保存配置文件...');
try {
// 完全替换策略:若尚未初始化用户配置,则先从默认配置初始化一份完整配置
ensureUserConfigInitialized();
// 确保目标目录存在
if (!fs.existsSync(CONFIG_USER_PAGES_DIR)) {
fs.mkdirSync(CONFIG_USER_PAGES_DIR, { recursive: true });
@@ -767,8 +877,19 @@ async function main() {
// 更新导航
console.log('[附加步骤] 更新导航配置...');
updateNavigationWithBookmarks();
console.log('[SUCCESS] 导航配置已更新\n');
const navUpdateResult = updateNavigationWithBookmarks();
if (navUpdateResult.updated) {
console.log(`[SUCCESS] 导航配置已更新(${navUpdateResult.target}\n`);
} else if (navUpdateResult.reason === 'already_present') {
console.log('[INFO] 导航配置已包含书签入口,无需更新\n');
} else if (navUpdateResult.reason === 'no_navigation_config') {
console.log('[WARN] 未找到可更新的导航配置文件;请确认 config/user/site.yml 存在且包含 navigation\n');
} else if (navUpdateResult.reason === 'error') {
console.log('[WARN] 导航更新失败,请手动检查配置文件格式(详见错误信息)\n');
console.error(navUpdateResult.error);
} else {
console.log('[INFO] 导航配置无需更新\n');
}
} catch (writeError) {
console.error(`[ERROR] 写入文件时出错:`, writeError);