// 认证改为 HttpOnly Cookie(gateway 下发 q4_token),浏览器同源请求自动携带,JS 不再经手 Token /* by qing. * * 用于获取一个节点下的所有子节点信息,0为根节点。 */ async function getNode(id) { try { const obj = { "pid": id } const jsonBody = JSON.stringify(obj) console.log(jsonBody) const res = await fetch('/get_notes', { method: 'POST', headers: { 'Content-Type': 'application/json', // 告诉服务器内容为json 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); // WAF 拦截:弹提示并中止,不能落入 catch(其会向上抛出) if (window.WafTip && WafTip.isBlocked(res, responseText)) { alert(WafTip.MESSAGE); return; } let result; try { result = JSON.parse(responseText); } catch { console.error(`获取笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),仅此一种情况跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(result.message || `获取节点失败(HTTP ${res.status})`); } //if (result.success) { console.log(`获取节点成功`); return result //... //} else { // alert(result.message || '获取节点失败'); //} } catch (err) { // 非 401 失败(网络异常 / 5xx / 非 JSON 响应):提示真实原因并向上抛出, // 调用方 .then(load_notes/loadMap) 不再执行,当前页面内容保持不变; // 不再把任意错误误判为凭证失效而跳登录页 console.error('获取节点请求异常:', err); alert(err.message || '获取节点失败,请检查网络后重试'); throw err; } } /* by qing * * 用于向dnfs添加节点 */ async function addNode(pid, title, content) { try { obj = { "pid": pid, "title": title, "content": content } jsonBody = JSON.stringify(obj) console.log(jsonBody) const res = await fetch('/add_note', { method: 'POST', headers: { 'Content-Type': 'application/json', // 告诉服务器内容为json 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE); let result; try { result = JSON.parse(responseText); } catch { console.error(`添加笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(`添加节点失败(HTTP ${res.status})`); } input_note_title.value = ""; input_note_content.value = ""; cancelEdit(); /* 退出编辑模式并重置高度 */ getNode(stack[stack.length - 1]).then(load_notes); getNode(stack[stack.length - 1]).then(loadMap) //if (result.success) { alert(`添加节点成功`, result); return result //... //} else { // alert(result.message || '获取节点失败'); //} } catch (err) { alert(err.message || '接口请求失败,请检查后端服务是否启动'); console.error('添加节点请求异常:', err); } finally { } } async function deleteNode(id) { if (confirm("确认删除此节点?") == false) { return; } try { obj = {"id": id}; jsonBody = JSON.stringify(obj); console.log(jsonBody); const res = await fetch('/delete_note', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE); let result; try { result = JSON.parse(responseText); } catch { console.error(`删除笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(`删除节点失败(HTTP ${res.status})`); } stack.pop(); getNode(stack[stack.length - 1]).then(load_notes); getNode(stack[stack.length - 1]).then(loadMap) console.log(`删除节点成功`); return result } catch (err) { alert(err.message || '接口请求失败,请检查后端服务是否启动'); console.error('删除节点请求异常:', err); } } /* by qing — 卡片上的"删除":删除子节点后停留在当前层,仅刷新列表(不弹栈) */ async function deleteChild(id) { if (confirm("确认删除此节点?") == false) { return; } try { obj = {"id": id}; jsonBody = JSON.stringify(obj); const res = await fetch('/delete_note', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE); let result; try { result = JSON.parse(responseText); } catch { console.error(`删除笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(result.error === "It's not your note" ? '只能删除自己的节点' : (result.error || `删除节点失败(HTTP ${res.status})`)); } // 若删掉的正是编辑中的节点,顺带退出编辑模式 if (editing_id == id) { cancelEdit(); input_note_title.value = ""; input_note_content.value = ""; } var cur = (stack.length == 0) ? 0 : stack[stack.length - 1]; getNode(cur).then(load_notes); getNode(cur).then(loadMap); console.log(`删除节点成功`); return result; } catch (err) { alert(err.message || '接口请求失败,请检查后端服务是否启动'); console.error('删除节点请求异常:', err); } } /* by qing — 保存编辑:调用 /update_note,成功后刷新当前层并退出编辑模式 */ async function updateNode(id, pid, title, content) { try { obj = { "id": Number(id), "pid": pid, "title": title, "content": content } jsonBody = JSON.stringify(obj) const res = await fetch('/update_note', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE); let result; try { result = JSON.parse(responseText); } catch { console.error(`更新笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(result.error === "It's not your note" ? '只能编辑自己的节点' : (result.error || `更新节点失败(HTTP ${res.status})`)); } input_note_title.value = ""; input_note_content.value = ""; cancelEdit(); var cur = (stack.length == 0) ? 0 : stack[stack.length - 1]; getNode(cur).then(load_notes); getNode(cur).then(loadMap); alert('修改成功'); return result; } catch (err) { alert(err.message || '接口请求失败,请检查后端服务是否启动'); console.error('更新节点请求异常:', err); } } /* by qing — 节点移动:本质是 pid 变更的 update_note,title/content 原样携带 */ async function moveNode(id, pid, title, content) { try { obj = { "id": Number(id), "pid": pid, "title": title, "content": content } jsonBody = JSON.stringify(obj) const res = await fetch('/update_note', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Target-Url': 'http://127.0.0.1:8006' }, body: jsonBody }); const responseText = await res.text(); if (window.WafTip && WafTip.isBlocked(res, responseText)) throw new Error(WafTip.MESSAGE); let result; try { result = JSON.parse(responseText); } catch { console.error(`更新笔记接口返回非 JSON 内容(HTTP ${res.status}):`, responseText); throw new Error('服务器响应格式异常,请稍后重试'); } if (res.status === 401) { // 凭证失效(过期或网关重启),跳转重新登录 location.href = '/login.html'; return; } if (!res.ok) { throw new Error(result.error === "It's not your note" ? '只能移动自己的节点' : (result.error || `移动节点失败(HTTP ${res.status})`)); } var cur = (stack.length == 0) ? 0 : stack[stack.length - 1]; getNode(cur).then(load_notes); getNode(cur).then(loadMap); alert('移动成功'); return result; } catch (err) { alert(err.message || '接口请求失败,请检查后端服务是否启动'); console.error('移动节点请求异常:', err); } } /* by qing — 点击卡片"移动":暂存节点信息,底部弹出移动条 */ function startMove(item) { moving_node = { id: Number(item.id), pid: item.pid, title: item.title, content: item.content }; renderMoveBar(); } /* by qing — 渲染底部移动条:显示待移动节点,提供"移动到当前层"和"取消" */ function renderMoveBar() { movebar.replaceChildren(); if (moving_node == null) { return; } let tip = document.createElement('span'); tip.className = "move-title"; tip.textContent = "移动中:" + moving_node.title; let ok_btn = document.createElement('button'); ok_btn.className = "act-btn act-edit"; ok_btn.textContent = "移动到当前层"; ok_btn.addEventListener('click', confirmMove); let cancel_btn = document.createElement('button'); cancel_btn.className = "act-btn act-del"; cancel_btn.textContent = "取消"; cancel_btn.addEventListener('click', function() { moving_node = null; renderMoveBar(); }); movebar.appendChild(tip); movebar.appendChild(ok_btn); movebar.appendChild(cancel_btn); } /* by qing — 执行移动:把待移动节点挂到当前层(stack 顶,根为 0)。 * 后端 update_note 会走 AI 内容审核,耗时较长,期间禁用移动条与表单按钮防止重复提交 */ async function confirmMove() { if (moving_node == null) { return; } var target_pid = (stack.length == 0) ? 0 : stack[stack.length - 1]; if (target_pid == moving_node.id) { alert('不能移动到节点自身内部'); return; } if (target_pid == moving_node.pid) { alert('该节点已在当前层'); return; } // 防重复点击:请求进行中禁用按钮(成功/失败都在 finally 恢复) movebar.querySelectorAll('button').forEach(function(b) { b.disabled = true; }); input_note_submit.disabled = true; input_note_clear.disabled = true; delete_note.disabled = true; try { var result = await moveNode(moving_node.id, target_pid, moving_node.title, moving_node.content); if (result != null) { moving_node = null; renderMoveBar(); } } finally { movebar.querySelectorAll('button').forEach(function(b) { b.disabled = false; }); input_note_submit.disabled = false; input_note_clear.disabled = false; delete_note.disabled = false; } } /* by shufeng2012 */ function renderMarkdown(element, content) { /* Markdown渲染 */ const md = window.markdownit({ html: false, highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return `
${hljs.highlight(code, {language: lang}).value}`
} else {
return `${md.utils.escapeHtml(code)}`
}
}
});
element.innerHTML = md.render(content);
}
/* ===== by qing — 特殊节点(管理员专属)=====
* 信封:content 为 {"_sp":"link|js|image|file", ...} JSON,普通内容走 Markdown。
* 发布权限由后端 dnfs 强制(is_admin),前端仅做展示与表单切换。 */
/* 与后端 base64 上限(3.5M 字符)对齐:原始文件约 2.5MB */
var SP_MAX_FILE_BYTES = 2.5 * 1024 * 1024;
function parseSp(content) {
if (typeof content !== 'string') return null;
var s = content.trim();
if (s.indexOf('{"_sp"') !== 0) return null;
try {
var obj = JSON.parse(s);
} catch (e) {
return null; // 非法信封按普通文本展示(后端发布时已拦截,这里是兜底)
}
if (obj && typeof obj === 'object' && ['link', 'js', 'image', 'file'].indexOf(obj._sp) !== -1) return obj;
return null;
}
/* 内容渲染分发:特殊节点按类型渲染,否则 Markdown */
function renderContent(container, item) {
var sp = parseSp(item.content);
if (!sp) {
renderMarkdown(container, item.content);
return;
}
function appendNote() {
if (typeof sp.note === 'string' && sp.note.trim()) {
var note = document.createElement('div');
note.className = 'sp-note';
note.textContent = sp.note;
container.appendChild(note);
}
}
if (sp._sp === 'link') renderSpLink(container, sp, appendNote);
else if (sp._sp === 'js') renderSpJs(container, sp, appendNote);
else if (sp._sp === 'image') renderSpImage(container, sp, appendNote);
else if (sp._sp === 'file') renderSpFile(container, sp, appendNote);
}
/* 链接节点:服务端代理拉取的 favicon + 可点击链接(卡片内交互需 stopPropagation) */
function renderSpLink(container, sp, appendNote) {
var box = document.createElement('div');
box.className = 'sp-link';
var icon = document.createElement('img');
icon.className = 'sp-favicon';
icon.alt = '';
icon.src = '/api/favicon?url=' + encodeURIComponent(sp.url);
icon.addEventListener('error', function() { icon.style.display = 'none'; });
var anchor = document.createElement('a');
anchor.className = 'sp-anchor';
anchor.href = sp.url;
anchor.target = '_blank';
anchor.rel = 'noopener noreferrer';
anchor.textContent = sp.url;
anchor.addEventListener('click', function(e) { e.stopPropagation(); });
box.appendChild(icon);
box.appendChild(anchor);
container.appendChild(box);
appendNote();
}
/* 前端代码节点:含前端内容的节点在渲染到当前页面时自动执行(隔离 iframe 沙箱)。
* 采用 iframe 沙箱而非注入父页面全局作用域,原因:
* - 特效脚本运行在独立 iframe 内,无法访问父页面 DOM/JS,任何报错、死循环、变量覆盖
* 都不会污染父页面,从而保证"运行特效后卡片点击/节点导航依然正常";
* - iframe 全屏铺底且 pointer-events:none,不拦截父页面点击,视觉特效悬浮展示在页面上方;
* - 不使用 new Function/eval(CSP 未开 unsafe-eval),srcdoc 内联脚本由 CSP 'unsafe-inline' 放行。
* 仅管理员可发布(后端强制)。 */
/* 无特效模式:true 为关闭自动执行(用户面板开关控制,localStorage 持久化) */
var sp_effects_disabled = (function() {
try { return localStorage.getItem('sp_effects_disabled') === '1'; } catch (e) { return false; }
})();
function renderSpJs(container, sp, appendNote) {
var bar = document.createElement('div');
bar.className = 'sp-actions';
var audit_btn = document.createElement('button');
audit_btn.className = 'act-btn act-edit';
audit_btn.textContent = '审计源码';
var toggle_btn = document.createElement('button');
toggle_btn.className = 'act-btn act-move';
toggle_btn.textContent = '停止特效';
var pre = document.createElement('pre');
pre.className = 'sp-code';
pre.style.display = 'none';
pre.textContent = sp.code;
var out = document.createElement('div');
out.className = 'sp-run';
out.style.display = 'none';
var frame = null; /* 当前特效 iframe 沙箱实例,null 表示未运行 */
audit_btn.addEventListener('click', function(e) {
e.stopPropagation();
var hidden = (pre.style.display === 'none');
pre.style.display = hidden ? 'block' : 'none';
audit_btn.textContent = hidden ? '收起源码' : '审计源码';
});
/* 启动特效:创建隔离 iframe 沙箱,完整前端代码(含