feat(dnfs): 前端代码节点支持完整HTML/CSS/JS并注入当前页面

将内联 JS 节点扩展为内联前端代码节点:内容存完整 HTML 文档,
运行后解析注入当前页面——style 注入 head、script 在全局作用域执行
可操作整页加特效、body 渲染到节点下方容器,console 输出捕获到下方。
This commit is contained in:
qing committed 2026-08-04 22:24:33 +08:00
1 parent ec3df34b43
commit 30360cb106
6 files changed
+62 -20

No files matched your search

+1 -1
View File
@@ -59,7 +59,7 @@ Authorization: Bearer <base64_payload>_<rsa_sign>
| `_sp` | 信封字段 | 前端表现 |
|-------|---------|---------|
| `link` | `url`(http/https,≤2048)、`note?` | favicon 图标(经 static `/api/favicon` 代理拉取)+ 可点击链接 |
| `js` | `code`(≤512KB)、`note?` | "审计源码"(纯文本展示)+ "运行"(在当前页面上下文执行,可施加特效;console 输出捕获到下方)按钮 |
| `js` | `code`(完整 HTML/CSS/JS 文档,≤512KB)、`note?` | "审计源码"(纯文本展示)+ "运行"(解析完整 HTML 文档注入当前页面:style 注入 head、script 在全局作用域执行可加特效、body 渲染到节点下方;console 输出捕获到下方)按钮 |
| `image` | `name`、`data`(`data:image/` DataURL,≤3.5M 字符)、`note?` | 图片直接显示 |
| `file` | `name`、`mime`、`data`(Base64,≤3.5M 字符)、`note?` | 下载按钮(Base64→Blob→下载) |
+2 -2
View File
@@ -248,9 +248,9 @@ def _validate_sp(sp):
elif t == 'js':
code = sp.get('code', '')
if not isinstance(code, str) or not code.strip():
raise DnfsExp('JS 代码不能为空', 400)
raise DnfsExp('前端代码不能为空', 400)
if len(code) > _SP_MAX_CODE:
raise DnfsExp('JS 代码过大(上限 512KB)', 400)
raise DnfsExp('前端代码过大(上限 512KB)', 400)
elif t == 'image':
data = sp.get('data', '')
if not isinstance(data, str) or not data.startswith('data:image/'):
+1 -1
View File
@@ -81,7 +81,7 @@ static :9200 (Flask + CORS)
- 流媒体:视频(MP4/WebM/OGG/AVI/MOV/MKV)与音频(MP3/WAV/OGG/FLAC/AAC/M4A)在线播放,支持进度拖动
- 深色模式:`css/theme.css` 统一主题,自动适配系统偏好
- 表单组件:注册页图形/邮箱验证码统一 `.code-box` 行内布局(文字标签 + 输入框 + 控件同排,窄屏自动换行),验证码图片样式复用 `theme.css` 的 `.captcha-img` 类
- 特殊节点(dnfs 管理员专属):信封 JSON `{"_sp":...}` 内容差异化渲染 —— 链接(favicon 卡片)/ JS 代码(可审计/可运行,在当前页面执行可加特效)/ 图片(直显)/ 文件(下载);发布权限由 dnfs 后端按 `is_admin` 强制
- 特殊节点(dnfs 管理员专属):信封 JSON `{"_sp":...}` 内容差异化渲染 —— 链接(favicon 卡片)/ 前端代码(完整 HTML/CSS/JS,可审计/可运行,注入当前页面可加特效)/ 图片(直显)/ 文件(下载);发布权限由 dnfs 后端按 `is_admin` 强制
- aibox Web 客户端:`aibox.html` 移动端适配聊天页,经 gateway `[WsProxy]` 反向代理(如 `/ws/aibox → ws://127.0.0.1:8765`)与 aibox 智能体通信,握手由 gateway 强制 `is_admin` 验签;协议与 BotOps/client.py 兼容(chunk 流式 / reasoning 思考区 / task 任务面板 / 指数退避重连)
- 路径遍历防护:`safe_path_join` 限制访问范围在 `directory.root` 内
- Nuitka 编译:可打包为独立二进制 `fsys.bin`(`sys.frozen` 检测 + BASE_DIR 锚定)
+8 -1
View File
@@ -527,10 +527,17 @@ input[type="button"]:disabled {
}
.sp-image { display: block; max-width: 100%; border-radius: var(--radius-sm); }
.sp-run {
margin-top: 0.5rem;
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
}
.sp-log {
margin-top: 0.5rem;
max-height: 320px;
overflow: auto;
padding: 0.75rem;
padding: 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
+1 -1
View File
@@ -27,7 +27,7 @@
<select id="sp-type">
<option value="text">普通文本</option>
<option value="link">链接(自动显示站点图标)</option>
<option value="js">JS 代码(可审计/可运行)</option>
<option value="js">前端代码(可审计/可运行)</option>
<option value="image">图片(自动显示)</option>
<option value="file">文件(可下载)</option>
</select>
+49 -14
View File
@@ -492,10 +492,12 @@ function renderSpLink(container, sp, appendNote) {
appendNote();
}
/* JS 代码节点:审计(展开源码,textContent 防注入)+ 运行(注入 <script> 在当前页面上下文执行,可直接操作页面加特效)。
* 不使用 new Function/eval(CSP 未开 unsafe-eval);改为注入内联 <script>(CSP 允许 'unsafe-inline')执行。
* 代码运行在全局作用域,可访问 document/window 对当前页面施加特效;console.log/error/warn 输出捕获到下方输出区。
* 仅管理员可发布(后端强制)。 */
/* 前端代码节点:审计(展开源码,textContent 防注入)+ 运行(注入当前页面执行,支持完整 HTML/CSS/JS)。
* 不使用 new Function/eval(CSP 未开 unsafe-eval);解析完整 HTML 文档后注入当前页面:
* - <style> 注入到页面 head
* - <script> 在当前页面全局作用域执行(可操作整页 DOM 加特效)
* - <body> 内容渲染到节点下方容器
* console.log/error/warn 输出捕获到下方输出区。仅管理员可发布(后端强制)。 */
function renderSpJs(container, sp, appendNote) {
var bar = document.createElement('div');
bar.className = 'sp-actions';
@@ -543,15 +545,48 @@ function renderSpJs(container, sp, appendNote) {
window.__spLines.push('Error: ' + ev.message);
});
}
/* 注入内联 <script> 在当前页面全局作用域执行,可操作页面加特效 */
var s = document.createElement('script');
s.textContent = sp.code;
document.head.appendChild(s);
out.textContent = window.__spLines.join('\n') || '执行完成';
/* 异步输出(setTimeout 等)轮询刷新 */
var timer = setInterval(function() {
/* 解析完整 HTML 文档 */
var doc;
try {
doc = new DOMParser().parseFromString(sp.code, 'text/html');
} catch (err) {
window.__spLines.push('Error: 解析 HTML 失败: ' + (err.message || err));
out.textContent = window.__spLines.join('\n');
}, 400);
return;
}
/* 注入 <style> 到页面 head */
Array.prototype.forEach.call(doc.querySelectorAll('style'), function(st) {
var s = document.createElement('style');
s.textContent = st.textContent;
document.head.appendChild(s);
});
/* 注入 <script> 在当前页面全局作用域执行(可操作整页加特效) */
Array.prototype.forEach.call(doc.querySelectorAll('script'), function(sc) {
var s = document.createElement('script');
if (sc.getAttribute('src')) {
s.src = sc.getAttribute('src');
} else {
s.textContent = sc.textContent;
}
document.head.appendChild(s);
});
/* 渲染 <body> 内容到节点下方容器 */
if (doc.body) {
Array.prototype.forEach.call(doc.body.childNodes, function(node) {
out.appendChild(node);
});
}
/* console 输出区 */
var logBox = document.createElement('div');
logBox.className = 'sp-log';
out.appendChild(logBox);
var render = function() {
var lines = window.__spLines.slice();
logBox.textContent = lines.join('\n') || '执行完成';
};
render();
/* 异步输出(setTimeout 等)轮询刷新 */
var timer = setInterval(render, 400);
});
bar.appendChild(audit_btn);
bar.appendChild(run_btn);
@@ -680,7 +715,7 @@ sp_type.addEventListener('change', function() {
sp_edit_original = null;
}
if (t === 'link') input_note_content.placeholder = '请输入链接 URL(https://…)';
else if (t === 'js') input_note_content.placeholder = '请输入 JS 代码';
else if (t === 'js') input_note_content.placeholder = '请输入完整前端代码(可含 HTML/CSS/JS)';
else input_note_content.placeholder = '';
});
@@ -705,7 +740,7 @@ async function buildSubmitContent() {
}
if (t === 'js') {
var code = input_note_content.value;
if (!code.trim()) { alert('请输入 JS 代码'); return null; }
if (!code.trim()) { alert('请输入前端代码'); return null; }
return JSON.stringify({ _sp: 'js', code: code });
}
/* image / file:优先使用新选择的文件;编辑模式下未选新文件则保留原信封 */