feat: 新增启动欢迎通知、流式预览节点及多项功能更新
- 新增启动弹窗通知(绿色主题,支持关闭) - 新增 StreamPreview 流式文本预览节点 - 新增 fileUpload、updateNotifier 前端 JS 模块 - 重构多个 client,统一错误处理 - 删除废弃节点 batch_nano_banana_v2、quan_neng_sheng_tu 等 - 将 .config 纳入版本控制(已清空密钥)
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js";
|
||||
|
||||
// ── marked.js 懒加载 ──────────────────────────────────────────────────────────
|
||||
let markedReady = null;
|
||||
function loadMarked() {
|
||||
if (markedReady) return markedReady;
|
||||
markedReady = new Promise((resolve) => {
|
||||
if (window.marked) { resolve(window.marked); return; }
|
||||
const s = document.createElement("script");
|
||||
s.src = "https://cdn.jsdelivr.net/npm/marked/marked.min.js";
|
||||
s.onload = () => resolve(window.marked);
|
||||
s.onerror = () => resolve(null);
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
return markedReady;
|
||||
}
|
||||
|
||||
// ── 节点 UI 构建 ──────────────────────────────────────────────────────────────
|
||||
function buildUI(node) {
|
||||
if (node._spContainer) return;
|
||||
|
||||
const container = document.createElement("div");
|
||||
container.style.cssText =
|
||||
"width:100%;height:100%;box-sizing:border-box;padding:6px;" +
|
||||
"display:flex;flex-direction:column;gap:4px;";
|
||||
|
||||
const toolbar = document.createElement("div");
|
||||
toolbar.style.cssText =
|
||||
"display:flex;justify-content:flex-end;gap:6px;align-items:center;";
|
||||
|
||||
const mdToggle = document.createElement("button");
|
||||
mdToggle.textContent = "MD";
|
||||
mdToggle.title = "切换 Markdown / 纯文本";
|
||||
mdToggle.style.cssText =
|
||||
"font-size:10px;padding:2px 6px;border-radius:3px;cursor:pointer;" +
|
||||
"background:#2a5a2a;color:#ccc;border:1px solid #666;";
|
||||
|
||||
const copyBtn = document.createElement("button");
|
||||
copyBtn.textContent = "复制";
|
||||
copyBtn.style.cssText =
|
||||
"font-size:10px;padding:2px 6px;border-radius:3px;cursor:pointer;" +
|
||||
"background:#444;color:#ccc;border:1px solid #666;";
|
||||
|
||||
toolbar.appendChild(mdToggle);
|
||||
toolbar.appendChild(copyBtn);
|
||||
|
||||
const isDedicatedPreview = node.comfyClass === "StreamPreview" || node.type === "StreamPreview";
|
||||
|
||||
const content = document.createElement("div");
|
||||
if (isDedicatedPreview) {
|
||||
content.style.cssText =
|
||||
"flex:1;min-height:0;overflow:hidden;" +
|
||||
"background:#1a1a1a;border:1px solid #444;border-radius:4px;" +
|
||||
"padding:8px;box-sizing:border-box;font-size:13px;line-height:1.6;" +
|
||||
"color:#ddd;white-space:pre-wrap;word-break:break-word;";
|
||||
} else {
|
||||
content.style.cssText =
|
||||
"width:100%;min-height:60px;max-height:480px;overflow-y:auto;" +
|
||||
"background:#1a1a1a;border:1px solid #444;border-radius:4px;" +
|
||||
"padding:8px;box-sizing:border-box;font-size:13px;line-height:1.6;" +
|
||||
"color:#ddd;white-space:pre-wrap;word-break:break-word;";
|
||||
}
|
||||
|
||||
const status = document.createElement("div");
|
||||
status.style.cssText =
|
||||
"font-size:10px;color:#888;text-align:right;min-height:14px;";
|
||||
|
||||
container.appendChild(toolbar);
|
||||
container.appendChild(content);
|
||||
container.appendChild(status);
|
||||
|
||||
node._spContainer = container;
|
||||
node._spContent = content;
|
||||
node._spStatus = status;
|
||||
node._spMdToggle = mdToggle;
|
||||
node._spRawText = "";
|
||||
node._spMarkdown = true;
|
||||
|
||||
mdToggle.addEventListener("click", () => {
|
||||
node._spMarkdown = !node._spMarkdown;
|
||||
mdToggle.style.background = node._spMarkdown ? "#2a5a2a" : "#444";
|
||||
renderContent(node);
|
||||
});
|
||||
|
||||
copyBtn.addEventListener("click", () => {
|
||||
navigator.clipboard.writeText(node._spRawText).then(() => {
|
||||
copyBtn.textContent = "已复制";
|
||||
setTimeout(() => { copyBtn.textContent = "复制"; }, 1500);
|
||||
});
|
||||
});
|
||||
|
||||
const widget = node.addDOMWidget("stream_preview_widget", "preview", container, {
|
||||
getValue() { return node._spRawText; },
|
||||
setValue(v) { },
|
||||
});
|
||||
widget.computeSize = (width) => {
|
||||
const isDedicatedPreview = node.comfyClass === "StreamPreview" || node.type === "StreamPreview";
|
||||
if (isDedicatedPreview) {
|
||||
const nodeHeight = node.size?.[1] ?? 320;
|
||||
const overhead = 60;
|
||||
return [width, Math.max(120, nodeHeight - overhead)];
|
||||
}
|
||||
return [width, 320];
|
||||
};
|
||||
|
||||
loadMarked();
|
||||
}
|
||||
|
||||
async function renderContent(node) {
|
||||
const text = node._spRawText;
|
||||
const el = node._spContent;
|
||||
if (!text) { el.innerHTML = ""; return; }
|
||||
|
||||
if (node._spMarkdown) {
|
||||
const marked = await loadMarked();
|
||||
if (marked) {
|
||||
el.style.whiteSpace = "normal";
|
||||
el.innerHTML = marked.parse(text);
|
||||
} else {
|
||||
el.style.whiteSpace = "pre-wrap";
|
||||
el.textContent = text;
|
||||
}
|
||||
} else {
|
||||
el.style.whiteSpace = "pre-wrap";
|
||||
el.textContent = text;
|
||||
}
|
||||
const isPreview = node.comfyClass === "StreamPreview" || node.type === "StreamPreview";
|
||||
if (!isPreview) el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
|
||||
// ── 流式事件监听 ──────────────────────────────────────────────────────────────
|
||||
api.addEventListener("o1key.stream_token", (event) => {
|
||||
const { node_id, token, done } = event.detail;
|
||||
const node = app.graph.getNodeById(parseInt(node_id));
|
||||
if (!node) return;
|
||||
|
||||
buildUI(node);
|
||||
|
||||
if (done) {
|
||||
node._spStreaming = false;
|
||||
node._spStatus.textContent = "生成完成";
|
||||
node._spStatus.style.color = "#4a4";
|
||||
return;
|
||||
}
|
||||
|
||||
// 第一个 token 到来时清空上一次内容
|
||||
if (!node._spStreaming) {
|
||||
node._spStreaming = true;
|
||||
node._spRawText = "";
|
||||
}
|
||||
|
||||
node._spRawText += token;
|
||||
node._spStatus.textContent = "生成中…";
|
||||
node._spStatus.style.color = "#a84";
|
||||
renderContent(node);
|
||||
});
|
||||
|
||||
// ── 节点注册 ──────────────────────────────────────────────────────────────────
|
||||
app.registerExtension({
|
||||
name: "comfyui_o1key.streamPreview",
|
||||
|
||||
async beforeRegisterNodeDef(nodeType, nodeData) {
|
||||
if (nodeData.name !== "StreamPreview") return;
|
||||
|
||||
const origOnNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
if (origOnNodeCreated) origOnNodeCreated.apply(this, arguments);
|
||||
buildUI(this);
|
||||
};
|
||||
|
||||
nodeType.prototype.onResize = function () {
|
||||
this.setDirtyCanvas(true, false);
|
||||
};
|
||||
|
||||
const origOnExecuted = nodeType.prototype.onExecuted;
|
||||
nodeType.prototype.onExecuted = function (message) {
|
||||
if (origOnExecuted) origOnExecuted.apply(this, arguments);
|
||||
buildUI(this);
|
||||
|
||||
const texts = message?.text;
|
||||
if (!texts || texts.length === 0) return;
|
||||
|
||||
this._spRawText = texts[0];
|
||||
this._spStatus.textContent = "完成";
|
||||
this._spStatus.style.color = "#4a4";
|
||||
renderContent(this);
|
||||
this.setDirtyCanvas(true, true);
|
||||
};
|
||||
|
||||
const origOnSerialize = nodeType.prototype.onSerialize;
|
||||
nodeType.prototype.onSerialize = function (o) {
|
||||
if (origOnSerialize) origOnSerialize.apply(this, arguments);
|
||||
o.sp_text = this._spRawText || "";
|
||||
o.sp_markdown = this._spMarkdown !== false;
|
||||
};
|
||||
|
||||
const origOnConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function (o) {
|
||||
if (origOnConfigure) origOnConfigure.apply(this, arguments);
|
||||
buildUI(this);
|
||||
if (o.sp_text) {
|
||||
this._spRawText = o.sp_text;
|
||||
this._spMarkdown = o.sp_markdown !== false;
|
||||
this._spMdToggle.style.background = this._spMarkdown ? "#2a5a2a" : "#444";
|
||||
renderContent(this);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user