diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c171442 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# 依赖 / 构建产物 +node_modules/ +dist/ +dist-hot/ +output/ +.claude/ + +# sing-box 引擎二进制(太大,用 scripts/fetch-singbox.sh 拉取) +resources/singbox/*/sing-box +resources/singbox/*/sing-box.exe + +# 杂项 +.DS_Store +*.log +*.map diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b2b9a50 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,36 @@ + +## 项目记忆(Codex 插件市场自动维护 · 回答前据此 · 勿手动编辑本段) +**当前状态**:用户在询问 Windows 下 Codex 内容存储路径后,提供启动报错截图并问原因;当前需要检查截图所指的 .codex 数据库及备份文件状态,判断是自动修复后的提示还是会反复损坏。 +**已定决策**: +- 全量会话读取包含归档和 subagent +- 媒体预览保留dataUrl和原始url +- Grok多图参考不支持 +- Grok作为新增节点,不改Seedance +- 导入后保持当前全量/默认模式 +- 同步目标为当前账号 +- 默认刷新仍保持轻量列表 +- 密钥不写入项目源码 +- 使用 /v1/videos/{id} 查询 +- 使用 /v1/videos/generations 创建 +- 默认模型为 grok-imagine-video +- 需按截图路径检查数据库状态 +**下一步**: +- 查看截图中的具体报错路径 +- 检查 .codex 数据库和备份文件 +- 判断是否需恢复或清理损坏文件 +**关键文件**:/var/folders/xk/qrv10_rx12jg91fn322xlwr00000gn/T/codex-clipboard-41e58e05-0211-4045-8cc5-1084d4f7f471.png、%USERPROFILE%\.codex、%USERPROFILE%\.codex\sessions、%USERPROFILE%\.codex\archived_sessions、%USERPROFILE%\.codex\history.jsonl、%USERPROFILE%\.codex\config.toml、%USERPROFILE%\.codex\auth.json +**历史话题(需其中细节时用下面命令检索)**: +- 2026-07-15 工具箱新增Grok视频与全量会话同步 +- 2026-07-15 生成魔兽世界宣传海报 +- 2026-07-15 Codex skill软链改造 +- 2026-07-14 列出本地 skills +- 2026-07-14 Claude CLI 中转配置排查 +- 2026-07-14 3x-ui订阅分流修复 +- 2026-07-14 图片生成与规格调整 +- 2026-07-14 生成蝴蝶图片并保存 +- 2026-07-13 项目与画布功能分析 +- 2026-07-13 网站安装数据库连接失败 +- 2026-07-12 服务器BBR已开启 +- 2026-07-12 服务器服务与域名配置交接 +> 回答涉及本段未列出的历史细节前,先运行 `rg -n "关键词" /Users/mac/.codex/memory/31c67c70b625.md` 查证再答。 + diff --git a/CANVAS_SPEC.md b/CANVAS_SPEC.md new file mode 100644 index 0000000..516e15d --- /dev/null +++ b/CANVAS_SPEC.md @@ -0,0 +1,154 @@ +# 创意画布(Canvas Studio)集成契约 —— 冻结版 + +主控已勘定的集成事实,技术员必须严格遵守。目标:在插件市场旁新增「创意画布」, +Drawflow 卡片式节点画布,第一版跑通 **文生图 → 图生视频 → TTS配音** 链路, +全部走 tokendun 中转,**key 只在主进程**。 + +## 已知事实(不要重复调研) +- Electron 应用,**纯原生 JS,无 React/Vue,无构建步骤**:`renderer/index.html` 直接 ` + + + diff --git a/renderer/logo.png b/renderer/logo.png new file mode 100644 index 0000000..fc00940 Binary files /dev/null and b/renderer/logo.png differ diff --git a/renderer/plugins.json b/renderer/plugins.json new file mode 100644 index 0000000..715a8ea --- /dev/null +++ b/renderer/plugins.json @@ -0,0 +1 @@ +{"source": "tokendun 官方插件", "repo": "https://up.tokendun.cc/mhgmh/codex-market-app", "plugins": [{"name": "presentations", "displayName": "演示文稿", "source": "openai-primary-runtime", "rel": "plugins/cache/openai-primary-runtime/presentations", "description": "用一句话创建或编辑 PowerPoint / Google Slides 演示文稿,支持新建、改版、渲染导出。", "platform": "", "size": 7833601, "zipName": "presentations.zip", "url": "https://up.tokendun.cc/mhgmh/codex-market-app/releases/download/plugins/presentations.zip"}, {"name": "product-design", "displayName": "产品设计", "source": "openai-curated-remote", "rel": "plugins/cache/openai-curated-remote/product-design", "description": "完整的产品与 UX 设计工作流:用户研究、产品审计、视觉创意,从想法/网址/图片/Figma 生成可运行原型,再到设计 QA 与分享。", "platform": "", "size": 238496, "zipName": "product-design.zip", "url": "https://up.tokendun.cc/mhgmh/codex-market-app/releases/download/plugins/product-design.zip"}, {"name": "hyperframes", "displayName": "HTML 视频", "source": "openai-api-curated", "rel": "plugins/cache/openai-api-curated/hyperframes", "description": "用写 HTML 的方式做视频:合成动画、字幕、配音、音频可视化、转场,一键渲染成 MP4,还能把网站直接变成宣传视频。", "platform": "", "size": 371425, "zipName": "hyperframes.zip", "url": "https://up.tokendun.cc/mhgmh/codex-market-app/releases/download/plugins/hyperframes.zip"}, {"name": "computer-use", "displayName": "电脑操控", "source": "openai-bundled", "rel": "plugins/cache/openai-bundled/computer-use", "description": "让 Codex 直接操控本机 Mac 应用:自动点击、输入、滚动、拖拽、按键,完成需要动手操作 App 界面的任务(附带 HyperFrames 视频插件)。", "platform": "macos", "size": 60690310, "zipName": "computer-use.zip", "url": "https://up.tokendun.cc/mhgmh/codex-market-app/releases/download/plugins/computer-use.zip"}, {"name": "creative-production", "displayName": "创意创作", "source": "openai-curated-remote", "rel": "plugins/cache/openai-curated-remote/creative-production", "description": "从想法到成图的商业视觉创作插件:一站式探索定位、情绪板、场景、广告、Logo、分镜,并把选中的图精修成可发布成品。", "platform": "", "size": 2640342, "zipName": "creative-production.zip", "url": "https://up.tokendun.cc/mhgmh/codex-market-app/releases/download/plugins/creative-production.zip"}]} diff --git a/renderer/styles.css b/renderer/styles.css new file mode 100644 index 0000000..98aaac3 --- /dev/null +++ b/renderer/styles.css @@ -0,0 +1,653 @@ +/* Codex 插件市场 —— 深色为主,支持浅色。无外部 CDN,系统字体栈。 */ +:root { + --grad: linear-gradient(135deg, #5B6CFF 0%, #A855F7 100%); + --accent: #7c85ff; + --bg: #0f1117; + --bg-2: #161923; + --card: #1b1f2b; + --card-2: #212636; + --border: #2a3042; + --text: #e8eaf2; + --muted: #9aa2b8; + --danger: #ff6b6b; + --ok: #4ade80; + --radius: 14px; + --shadow: 0 8px 30px rgba(0, 0, 0, 0.35); +} +@media (prefers-color-scheme: light) { + :root { + --bg: #f5f6fb; + --bg-2: #eceef6; + --card: #ffffff; + --card-2: #f3f4fa; + --border: #e2e5ef; + --text: #1b1e29; + --muted: #6b7186; + --shadow: 0 8px 24px rgba(60, 60, 120, 0.12); + } +} + +* { box-sizing: border-box; } +html, body { height: 100%; margin: 0; } +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Microsoft YaHei", Roboto, Helvetica, Arial, sans-serif; + background: var(--bg); + color: var(--text); + display: flex; + flex-direction: column; + overflow: hidden; + font-size: 14px; +} +code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; + background: var(--card-2); padding: 1px 6px; border-radius: 6px; } + +/* 顶栏 */ +.topbar { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 20px; background: var(--bg-2); + border-bottom: 1px solid var(--border); + /* 窗口用系统原生标题栏(main.js 未设 frame:false),不需要自绘拖拽区。 + -webkit-app-region: drag 会让浏览器进程在 OS 层拦截该区域的鼠标事件用于「拖窗」, + 真实鼠标点击到导航(含标签间隙/品牌区,以及 no-drag 命中不稳的边角)会被吞掉, + 而 CDP Input.dispatchMouseEvent 直接注入渲染进程、绕过该拦截,所以静态与 CDP 都看不出。 + 原生边框已提供拖窗,这里移除以杜绝该类「点不动」。 */ +} +.brand { display: flex; align-items: center; gap: 12px; } +.logo { width: 40px; height: 40px; display: grid; place-items: center; + border-radius: 11px; box-shadow: var(--shadow); } +.brand-name { font-weight: 700; font-size: 15px; letter-spacing: .2px; } +.brand-sub { color: var(--muted); font-size: 12px; margin-top: 1px; } + +/* —— 顶部广告:官方推荐中转 —— */ +.promo { + -webkit-app-region: no-drag; + display: flex; align-items: center; gap: 8px; + padding: 6px 12px; border-radius: 10px; text-decoration: none; + color: var(--text); font-size: 12px; white-space: nowrap; + background: linear-gradient(90deg, rgba(91,108,255,.14), rgba(168,85,247,.14)); + border: 1px solid rgba(124,108,255,.35); + transition: .15s; +} +.promo:hover { border-color: rgba(124,108,255,.7); + background: linear-gradient(90deg, rgba(91,108,255,.22), rgba(168,85,247,.22)); } +.promo-badge { font-size: 11px; font-weight: 700; color: #fff; + background: var(--grad); padding: 2px 7px; border-radius: 6px; letter-spacing: .2px; } +.promo-text { color: var(--muted); } +.promo-text b { color: var(--text); font-weight: 600; } +.promo-arrow { color: var(--muted); font-weight: 700; } + +.tabs { display: flex; gap: 4px; background: var(--card); padding: 4px; + border-radius: 12px; border: 1px solid var(--border); } +.tab { border: 0; background: transparent; color: var(--muted); + padding: 7px 16px; border-radius: 9px; cursor: pointer; font-size: 13px; + font-weight: 600; transition: .15s; } +.tab:hover { color: var(--text); } +.tab.active { color: #fff; background: var(--grad); box-shadow: var(--shadow); } + +/* 内容区 */ +.content { flex: 1; overflow-y: auto; padding: 22px; } +.panel { display: none; animation: fade .2s ease; } +.panel.active { display: block; } +@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; } } + +.panel-head { display: flex; align-items: flex-end; justify-content: space-between; + gap: 16px; margin-bottom: 16px; flex-wrap: wrap; } +h2 { margin: 0; font-size: 20px; } +h3 { margin: 0; font-size: 16px; } +.muted { color: var(--muted); margin: 4px 0 0; } +.small { font-size: 12px; } +.head-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } + +.search { background: var(--card); border: 1px solid var(--border); color: var(--text); + border-radius: 10px; padding: 9px 12px; width: 220px; outline: none; } +.search:focus { border-color: var(--accent); } + +.segmented { display: flex; background: var(--card); border: 1px solid var(--border); + border-radius: 10px; padding: 3px; } +.seg { border: 0; background: transparent; color: var(--muted); padding: 6px 12px; + border-radius: 7px; cursor: pointer; font-size: 12px; font-weight: 600; } +.seg.active { background: var(--card-2); color: var(--text); } + +/* 按钮 */ +.btn { border: 1px solid var(--border); background: var(--card); color: var(--text); + padding: 9px 16px; border-radius: 10px; cursor: pointer; font-size: 13px; + font-weight: 600; transition: .15s; } +.btn:hover { border-color: var(--accent); } +.btn.primary { background: var(--grad); border: 0; color: #fff; box-shadow: var(--shadow); } +.btn.primary:hover { filter: brightness(1.07); } +.btn.ghost { background: transparent; } +.btn:disabled { opacity: .5; cursor: not-allowed; } +.btn.small { padding: 6px 12px; font-size: 12px; } +.btn.danger { border-color: var(--danger); color: var(--danger); } +.btn.danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); } +.btn-group { display: flex; gap: 10px; } +.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; opacity: .7; word-break: break-all; } + +/* 市场网格 */ +.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 14px; } +.plugin-card { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; + gap: 10px; transition: .15s; } +.plugin-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); + border-color: var(--accent); } +.pc-top { display: flex; align-items: center; gap: 10px; } +.pc-badge { font-size: 11px; font-weight: 700; padding: 3px 9px; border-radius: 999px; } +.pc-badge.plugin { background: rgba(124,133,255,.16); color: var(--accent); } +.pc-badge.skill { background: rgba(74,222,128,.15); color: var(--ok); } +.pc-badge.plat { background: rgba(245,158,11,.16); color: #f59e0b; } +.pc-badge.on { background: rgba(74,222,128,.18); color: var(--ok); } +/* 全局代理:两种模式说明块 */ +.proxy-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 14px; } +.pm-item { display: flex; flex-direction: column; gap: 5px; padding: 12px 14px; + border: 1px solid var(--border); border-radius: 10px; background: rgba(255,255,255,.02); } +.pm-item b { font-size: 14px; } +@media (max-width: 760px) { .proxy-modes { grid-template-columns: 1fr; } } +.pc-name { font-weight: 700; font-size: 15px; word-break: break-all; } +.pc-meta { color: var(--muted); font-size: 12px; display: flex; gap: 10px; + flex-wrap: wrap; } +.pc-desc { color: var(--muted); font-size: 13px; line-height: 1.5; + min-height: 20px; flex: 1; } +.pc-foot { display: flex; align-items: center; justify-content: space-between; + gap: 10px; margin-top: 2px; } +.link { color: var(--accent); cursor: pointer; font-size: 12px; text-decoration: none; } +.link:hover { text-decoration: underline; } + +/* 勾选列表(备份/还原) */ +.checklist { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; } +.cl-group { font-size: 12px; font-weight: 700; color: var(--muted); + text-transform: uppercase; letter-spacing: .5px; margin: 10px 2px 2px; } +.cl-item { display: flex; align-items: center; gap: 12px; background: var(--card); + border: 1px solid var(--border); border-radius: 11px; padding: 11px 14px; + cursor: pointer; transition: .12s; } +.cl-item:hover { border-color: var(--accent); } +.cl-item input { width: 17px; height: 17px; accent-color: var(--accent); } +.cl-item .cl-name { font-weight: 600; } +.cl-item .cl-src { color: var(--muted); font-size: 12px; } +.cl-item .cl-size { margin-left: auto; color: var(--muted); font-size: 12px; } + +/* 子标签 */ +.subtabs { display: flex; gap: 6px; margin-bottom: 18px; } +.subtab { border: 1px solid var(--border); background: var(--card); color: var(--muted); + padding: 8px 16px; border-radius: 10px; cursor: pointer; font-weight: 600; font-size: 13px; } +.subtab.active { color: var(--text); border-color: var(--accent); background: var(--card-2); } +.subpanel { display: none; } +.subpanel.active { display: block; } + +/* 表单卡片 */ +.card { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 16px; } +.action-card { margin-top: 8px; display: flex; flex-direction: column; gap: 14px; } +.row { display: flex; gap: 14px; align-items: flex-end; flex-wrap: wrap; } +.row.between { justify-content: space-between; align-items: center; } +.field { display: flex; flex-direction: column; gap: 6px; } +.field.grow { flex: 1; min-width: 200px; } +.field span { font-size: 12px; color: var(--muted); font-weight: 600; } +.field input { background: var(--bg-2); border: 1px solid var(--border); color: var(--text); + border-radius: 10px; padding: 10px 12px; outline: none; } +.field input:focus { border-color: var(--accent); } +.check-inline { display: flex; align-items: center; gap: 8px; color: var(--muted); + font-size: 13px; cursor: pointer; } +.check-inline input { accent-color: var(--accent); width: 16px; height: 16px; } + +/* 空 / 加载态 */ +.empty { text-align: center; color: var(--muted); padding: 50px 20px; + border: 1px dashed var(--border); border-radius: var(--radius); } +.spinner { width: 26px; height: 26px; border: 3px solid var(--border); + border-top-color: var(--accent); border-radius: 50%; animation: spin .8s linear infinite; + margin: 0 auto 12px; } +@keyframes spin { to { transform: rotate(360deg); } } +.hidden { display: none !important; } + +/* 日志抽屉 */ +.logbar { border-top: 1px solid var(--border); background: var(--bg-2); } +.log-toggle { width: 100%; text-align: left; border: 0; background: transparent; + color: var(--muted); padding: 8px 20px; cursor: pointer; font-size: 12px; + font-weight: 600; display: flex; justify-content: space-between; } +.log-panel { max-height: 190px; overflow-y: auto; padding: 0 20px 12px; } +.log-panel pre { margin: 0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 12px; line-height: 1.6; color: var(--muted); white-space: pre-wrap; + word-break: break-word; } + +/* Toast */ +.toast { position: fixed; bottom: 60px; left: 50%; transform: translateX(-50%); + background: var(--card); border: 1px solid var(--border); color: var(--text); + padding: 12px 20px; border-radius: 12px; box-shadow: var(--shadow); z-index: 50; + max-width: 80%; font-size: 13px; } +.toast.ok { border-color: var(--ok); } +.toast.err { border-color: var(--danger); } + +/* 卡片内按钮忙碌态 */ +.installing { pointer-events: none; opacity: .7; } + +/* 会话记录:同步横幅 */ +.sync-banner { display: flex; align-items: center; justify-content: space-between; + gap: 14px; background: linear-gradient(135deg, rgba(124,133,255,.12), rgba(168,85,247,.10)); + border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 16px; + margin-bottom: 12px; } +.sync-banner .sync-text { display: flex; flex-direction: column; gap: 3px; } +.sync-banner .sync-text strong { font-size: 14px; } +.sync-banner .sync-text b { color: var(--danger); } +.sync-progress { display: flex; align-items: center; gap: 10px; margin-top: 6px; } +.sync-progress-bar { flex: 1; max-width: 320px; height: 7px; background: var(--card-2); + border-radius: 999px; overflow: hidden; border: 1px solid var(--border); } +.sync-progress-fill { height: 100%; width: 0%; background: var(--grad); + border-radius: 999px; transition: width .2s ease; } +.sync-progress-label { white-space: nowrap; } + +/* 会话记录:左右两栏 */ +.sess-layout { display: grid; grid-template-columns: 300px 1fr; gap: 14px; + height: calc(100vh - 240px); min-height: 320px; } +.sess-list { overflow-y: auto; display: flex; flex-direction: column; gap: 6px; + padding-right: 4px; } +.sess-item { background: var(--card); border: 1px solid var(--border); + border-radius: 11px; padding: 10px 12px; cursor: pointer; transition: .12s; } +.sess-item:hover { border-color: var(--accent); } +.sess-item.active { border-color: var(--accent); background: var(--card-2); } +.sess-item .si-title { font-weight: 600; font-size: 13px; margin-bottom: 3px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.sess-item .si-meta { color: var(--muted); font-size: 11px; display: flex; + gap: 8px; flex-wrap: wrap; } +.si-badge { border: 1px solid var(--border); border-radius: 999px; padding: 1px 6px; + color: var(--text); background: var(--card-2); font-weight: 700; } +.sess-reader { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); overflow: hidden; display: flex; flex-direction: column; } +.sess-conv { display: flex; flex-direction: column; height: 100%; } +.sess-conv-head { display: flex; align-items: center; justify-content: space-between; + gap: 10px; padding: 12px 16px; border-bottom: 1px solid var(--border); } +.sess-conv-title { font-weight: 700; font-size: 14px; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } +.sess-messages { overflow-y: auto; padding: 16px; display: flex; + flex-direction: column; gap: 14px; } +.msg { display: flex; flex-direction: column; gap: 4px; } +.msg .who { font-size: 11px; font-weight: 700; color: var(--muted); } +.msg .bubble { border-radius: 12px; padding: 10px 13px; font-size: 13px; + line-height: 1.6; white-space: pre-wrap; word-break: break-word; } +.msg.user { align-items: flex-end; } +.msg.user .bubble { background: var(--grad); color: #fff; max-width: 85%; } +.msg.assistant .bubble { background: var(--card-2); color: var(--text); + border: 1px solid var(--border); max-width: 92%; } +.msg.tool .bubble { background: transparent; color: var(--muted); font-size: 12px; + border: 1px dashed var(--border); padding: 5px 10px; } + +/* 常驻页脚:版权 / 使用说明 */ +.footer-notice { display: flex; align-items: center; justify-content: space-between; + gap: 12px; padding: 7px 20px; background: var(--bg-2); + border-top: 1px solid var(--border); font-size: 11px; color: var(--muted); } +.footer-notice .fn-text { flex: 1; min-width: 0; overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } +.footer-notice .fn-links { display: flex; gap: 14px; flex-shrink: 0; } +.fn-link { border: 0; background: transparent; color: var(--accent); cursor: pointer; + font-size: 11px; font-weight: 600; padding: 0; } +.fn-link:hover { text-decoration: underline; } + +/* 说明弹窗 */ +.modal-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, .5); + display: flex; align-items: center; justify-content: center; z-index: 100; + padding: 24px; animation: fade .15s ease; } +.modal { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); box-shadow: var(--shadow); max-width: 640px; + width: 100%; max-height: 82vh; display: flex; flex-direction: column; } +.modal-head { display: flex; align-items: center; justify-content: space-between; + padding: 15px 20px; border-bottom: 1px solid var(--border); } +.modal-head h3 { font-size: 16px; } +.modal-close { border: 0; background: transparent; color: var(--muted); font-size: 17px; + cursor: pointer; line-height: 1; padding: 3px 7px; border-radius: 8px; } +.modal-close:hover { color: var(--text); background: var(--card-2); } +.modal-body { padding: 18px 22px; overflow-y: auto; line-height: 1.7; + font-size: 13px; color: var(--text); } +.modal-body h4 { margin: 18px 0 6px; font-size: 14px; } +.modal-body h4:first-child { margin-top: 0; } +.modal-body p { margin: 6px 0; color: var(--muted); } +.modal-body ul { margin: 6px 0; padding-left: 20px; color: var(--muted); } +.modal-body li { margin: 4px 0; } +.modal-body strong { color: var(--text); } +.modal-body .step { display: flex; gap: 10px; margin: 8px 0; align-items: baseline; } +.modal-body .step .n { flex-shrink: 0; width: 20px; height: 20px; border-radius: 999px; + background: var(--grad); color: #fff; font-size: 11px; font-weight: 700; + display: grid; place-items: center; } +.cv-media-modal { max-width: min(1120px, 94vw); } +.cv-media-modal .modal-body { padding: 10px; overflow: hidden; } +.cv-modal-media { display: block; width: 100%; max-height: 76vh; object-fit: contain; + background: #000; border-radius: 8px; } +.grp-select { margin-top: 8px; width: 100%; background: var(--bg-2); + border: 1px solid var(--border); color: var(--text); border-radius: 10px; + padding: 9px 12px; outline: none; font-size: 13px; } +.grp-select:focus { border-color: var(--accent); } + +/* —— 会话记忆 —— */ +.mem-item { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 13px 16px; margin-bottom: 10px; } +.mem-item:hover { border-color: var(--accent); } +.mem-top { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.mem-topic { font-weight: 600; } +.mem-actions { margin-left: auto; display: flex; gap: 6px; } +.mem-sum { margin-top: 8px; white-space: pre-wrap; font-size: 13px; color: var(--text); opacity: .88; } +.mem-line { margin-top: 6px; font-size: 12px; color: var(--muted); } +.mem-line b { color: var(--text); margin-right: 8px; font-weight: 600; } + +/* =========================================================================== + 技能商店(cocoloop 镜像)—— 侧栏分类 + 精致卡片 + =========================================================================== */ +.store { display: grid; grid-template-columns: 236px 1fr; gap: 18px; + height: calc(100vh - 150px); min-height: 420px; } + +/* 左侧分类导航 */ +.store-nav { display: flex; flex-direction: column; background: var(--bg-2); + border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; } +.store-nav-head { display: flex; align-items: baseline; justify-content: space-between; + padding: 13px 15px 9px; border-bottom: 1px solid var(--border); } +.store-nav-title { font-size: 12px; font-weight: 700; color: var(--muted); + letter-spacing: .5px; } +.store-nav-total { font-size: 11px; color: var(--accent); font-weight: 700; } +.store-nav-list { flex: 1; overflow-y: auto; padding: 8px; display: flex; + flex-direction: column; gap: 2px; } +.store-nav-list::-webkit-scrollbar { width: 8px; } +.store-nav-list::-webkit-scrollbar-thumb { background: var(--border); + border-radius: 8px; } +.store-nav-group { font-size: 10px; font-weight: 700; color: var(--muted); + text-transform: uppercase; letter-spacing: .6px; padding: 10px 10px 4px; opacity: .7; } +.nav-item { display: flex; align-items: center; gap: 9px; padding: 8px 10px; + border-radius: 9px; cursor: pointer; border: 1px solid transparent; + transition: .12s; color: var(--text); } +.nav-item:hover { background: var(--card); } +.nav-item.active { background: var(--grad); color: #fff; box-shadow: var(--shadow); } +.nav-item.active .nav-count { background: rgba(255,255,255,.22); color: #fff; } +.nav-ico { width: 18px; text-align: center; font-size: 14px; flex-shrink: 0; } +.nav-name { flex: 1; font-size: 13px; font-weight: 600; white-space: nowrap; + overflow: hidden; text-overflow: ellipsis; } +.nav-count { font-size: 11px; font-weight: 700; color: var(--muted); + background: var(--card-2); padding: 1px 8px; border-radius: 999px; flex-shrink: 0; } + +/* 右侧主区 */ +.store-main { display: flex; flex-direction: column; min-width: 0; } +.store-head { display: flex; align-items: center; justify-content: space-between; + gap: 16px; flex-wrap: wrap; } +.store-crumb { display: flex; align-items: baseline; gap: 8px; font-size: 20px; + font-weight: 700; } +.store-crumb .muted { font-size: 13px; font-weight: 600; } +.store-crumb-sep { color: var(--muted); font-weight: 400; } +.store-tools { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.store-sub { margin: 6px 0 14px; font-size: 12.5px; } +.store-grid { overflow-y: auto; align-content: start; padding: 2px 4px 4px 2px; + flex: 1; grid-template-columns: repeat(auto-fill, minmax(310px, 1fr)); } +.store-grid::-webkit-scrollbar { width: 9px; } +.store-grid::-webkit-scrollbar-thumb { background: var(--border); border-radius: 9px; } + +/* 技能卡片 */ +.skill-card { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 15px; display: flex; flex-direction: column; + gap: 11px; transition: .15s; position: relative; overflow: hidden; } +.skill-card::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 3px; + background: var(--grad); opacity: 0; transition: .15s; } +.skill-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); + border-color: var(--accent); } +.skill-card:hover::before { opacity: 1; } +.sk-top { display: flex; align-items: flex-start; gap: 11px; } +.sk-ico { width: 42px; height: 42px; flex-shrink: 0; display: grid; place-items: center; + font-size: 22px; background: var(--card-2); border: 1px solid var(--border); + border-radius: 11px; } +.sk-headings { min-width: 0; flex: 1; } +.sk-name { font-weight: 700; font-size: 15px; line-height: 1.25; word-break: break-word; } +.sk-author { color: var(--muted); font-size: 12px; margin-top: 2px; } +.sk-sec { flex-shrink: 0; width: 24px; height: 24px; display: grid; place-items: center; + border-radius: 7px; font-size: 11px; font-weight: 800; } +.sk-sec.S { background: linear-gradient(135deg,#f59e0b,#f97316); color: #fff; } +.sk-sec.A { background: rgba(74,222,128,.16); color: var(--ok); } +.sk-sec.B { background: var(--card-2); color: var(--muted); } +.sk-subtitle { font-weight: 600; font-size: 13.5px; line-height: 1.4; color: var(--text); } +.sk-brief { color: var(--muted); font-size: 12.5px; line-height: 1.55; flex: 1; + display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; + overflow: hidden; } +.sk-agents { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } +.sk-agents .lab { font-size: 11px; color: var(--muted); margin-right: 2px; } +.agent-badge { font-size: 11px; font-weight: 700; padding: 2px 9px; border-radius: 999px; + display: inline-flex; align-items: center; gap: 5px; background: var(--card-2); + border: 1px solid var(--border); } +.agent-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; } +.agent-badge.claude::before { background: #d97757; } +.agent-badge.codex::before { background: #5b6cff; } +.agent-badge.openclaw::before { background: #a855f7; } +.agent-badge.mm { background: rgba(245,158,11,.14); border-color: transparent; color: #f59e0b; } +.agent-badge.mm::before { display: none; } +.sk-meta { display: flex; align-items: center; gap: 13px; padding-top: 9px; + border-top: 1px solid var(--border); color: var(--muted); font-size: 12px; + flex-wrap: wrap; } +.sk-meta .cred { margin-left: auto; font-size: 10px; font-weight: 700; padding: 1px 7px; + border: 1px solid var(--border); border-radius: 6px; letter-spacing: .3px; } +.sk-foot { display: flex; align-items: center; gap: 9px; } +.sk-foot .btn.primary { flex: 1; } + +/* 加载更多 */ +.store-more { display: flex; align-items: center; justify-content: center; gap: 12px; + padding: 14px; color: var(--muted); font-size: 13px; } + +/* 技能详情弹窗内的排版 */ +.sd-hero { display: flex; align-items: flex-start; gap: 14px; margin-bottom: 4px; } +.sd-hero .sk-ico { width: 52px; height: 52px; font-size: 27px; } +.sd-hero h3 { font-size: 18px; } +.sd-stats { display: flex; gap: 16px; flex-wrap: wrap; margin: 12px 0; color: var(--muted); + font-size: 13px; } +.sd-tags { display: flex; gap: 6px; flex-wrap: wrap; margin: 8px 0; } +.sd-tag { font-size: 11px; font-weight: 600; padding: 3px 10px; border-radius: 999px; + background: var(--card-2); color: var(--muted); border: 1px solid var(--border); } +.sd-install-row { display: flex; gap: 10px; margin-top: 8px; } + +@media (max-width: 820px) { + .store { grid-template-columns: 1fr; height: auto; } + .store-nav { max-height: 200px; } +} + +/* 品牌 logo 图片 */ +.logo img { width: 100%; height: 100%; border-radius: 10px; display: block; object-fit: cover; } + +/* =========================================================================== + 更新弹窗 + =========================================================================== */ +.update-modal { max-width: 480px; padding: 0; } +.update-head { display: flex; align-items: center; gap: 13px; padding: 20px 22px 14px; } +.update-ico { width: 44px; height: 44px; flex-shrink: 0; display: grid; place-items: center; + font-size: 24px; border-radius: 12px; background: var(--grad); } +.update-head h3 { font-size: 17px; margin: 0; } +.update-badge { margin-left: auto; font-size: 11px; font-weight: 700; padding: 3px 10px; + border-radius: 999px; } +.update-badge.force { background: rgba(255,107,107,.16); color: var(--danger); } +.update-badge.normal { background: rgba(74,222,128,.15); color: var(--ok); } +.update-notes { margin: 0 22px; padding: 14px 16px; background: var(--bg-2); + border: 1px solid var(--border); border-radius: 12px; max-height: 300px; overflow-y: auto; + font-size: 13px; line-height: 1.65; color: var(--text); white-space: pre-wrap; + word-break: break-word; } +.update-notes:empty::before { content: "本次更新暂无详细说明。"; color: var(--muted); } +.update-foot { display: flex; gap: 10px; justify-content: flex-end; padding: 16px 22px 20px; } +.update-foot .btn { min-width: 96px; } + +/* =========================================================================== + 插件市场 —— 内置工具卡片 + 分区标题 + 返回按钮 + =========================================================================== */ +.tools-title { font-size: 13px; font-weight: 700; color: var(--muted); + letter-spacing: .5px; margin: 6px 2px 12px; } +.tools-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 14px; margin-bottom: 26px; } +.tool-card { background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 16px; display: flex; flex-direction: column; + gap: 10px; transition: .15s; } +.tool-card:hover { transform: translateY(-2px); box-shadow: var(--shadow); border-color: var(--accent); } +.tool-top { display: flex; align-items: center; gap: 11px; } +.tool-ico { width: 40px; height: 40px; flex-shrink: 0; display: grid; place-items: center; + font-size: 21px; background: var(--card-2); border: 1px solid var(--border); border-radius: 11px; } +.tool-name { font-weight: 700; font-size: 15px; } +.tool-desc { color: var(--muted); font-size: 13px; line-height: 1.6; flex: 1; } +.tool-foot { display: flex; justify-content: flex-end; } +.back-btn { margin-right: 8px; vertical-align: middle; padding: 4px 10px; } + +/* ============================================================ + 创意画布(Drawflow 节点画布) + 复用 :root 设计变量,覆写 Drawflow 默认外观,明/暗色协调。 + ============================================================ */ +/* 画布面板占满内容区高度:工具条固定 + 画布撑满剩余空间 */ +/* 画布面板占满内容区高度:抵消 .content 的 22px padding,铺满更大气 */ +#tab-canvas.active { display: flex; flex-direction: column; min-height: 0; + margin: -22px; height: calc(100% + 44px); padding: 22px; } +.cv-toolbar { display: flex; align-items: center; justify-content: space-between; + gap: 12px; flex-wrap: wrap; margin-bottom: 12px; } +.cv-toolbar-left { display: flex; align-items: center; gap: 12px; } +.cv-title { font-size: 18px; } +.cv-status { border: 1px solid var(--border); background: var(--card-2); color: var(--muted); } +.cv-status.on { background: rgba(74,222,128,.18); color: var(--ok); border-color: transparent; } +.cv-status.off { background: rgba(255,107,107,.16); color: var(--danger); border-color: transparent; } +.cv-toolbar-right { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; } +.cv-add-label { color: var(--muted); font-size: 12px; font-weight: 600; margin-right: 2px; } + +/* 画布舞台:明确高度(Drawflow 容器需要),点阵底纹 */ +.cv-stage { position: relative; flex: 1; min-height: 560px; + border: 1px solid var(--border); border-radius: var(--radius); overflow: hidden; + box-shadow: var(--shadow); } +#drawflow { position: absolute; inset: 0; width: 100%; height: 100%; + background-color: var(--bg-2); + background-image: radial-gradient(var(--border) 1px, transparent 1px); + background-size: 22px 22px; } + +/* 悬浮缩放控件(右下角,覆盖在画布上) */ +.cv-zoom { position: absolute; right: 14px; bottom: 14px; z-index: 15; + display: flex; flex-direction: column; gap: 1px; border-radius: 10px; overflow: hidden; + border: 1px solid var(--border); box-shadow: var(--shadow); } +.cv-zoom-btn { border: 0; background: var(--card); color: var(--text); + width: 38px; height: 34px; cursor: pointer; font-size: 17px; font-weight: 700; + line-height: 1; display: flex; align-items: center; justify-content: center; + transition: .12s; user-select: none; } +.cv-zoom-btn:hover { background: var(--accent); color: #fff; } +.cv-zoom-btn:active { transform: scale(.94); } +.cv-zoom-level { font-size: 11px; font-weight: 600; color: var(--muted); + border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); } + +/* 未就绪遮罩 */ +.cv-mask { position: absolute; inset: 0; z-index: 20; display: grid; place-items: center; + background: color-mix(in srgb, var(--bg) 78%, transparent); + backdrop-filter: blur(2px); } +.cv-mask-box { text-align: center; background: var(--card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 28px 32px; box-shadow: var(--shadow); max-width: 380px; } +.cv-mask-ico { font-size: 34px; } +.cv-mask-title { font-weight: 700; font-size: 16px; margin: 8px 0 4px; } +.cv-mask-text { color: var(--muted); font-size: 13px; margin: 0 0 14px; line-height: 1.6; } + +/* —— 覆写 Drawflow 节点为卡片风 —— */ +.drawflow .drawflow-node { background: transparent; border: 0; padding: 0; + border-radius: 16px; box-shadow: none; width: 250px; } +.drawflow .drawflow-node.selected { box-shadow: none; } +.drawflow .drawflow-node .drawflow_content_node { width: 100%; } +.cv-node { background: var(--card); border: 1px solid var(--border); + border-radius: 16px; overflow: hidden; box-shadow: var(--shadow); + transition: border-color .15s; } +.drawflow-node:hover .cv-node { border-color: var(--accent); } +.drawflow-node.selected .cv-node { border-color: transparent; + box-shadow: 0 0 0 2px var(--accent), var(--shadow); } +.cv-node-head { display: flex; align-items: center; gap: 8px; + padding: 9px 12px; background: var(--card-2); border-bottom: 1px solid var(--border); } +.cv-node-ico { font-size: 15px; } +.cv-node-name { font-weight: 700; font-size: 13px; color: var(--text); } +.cv-node-tag { margin-left: auto; font-size: 10px; font-weight: 700; color: #f59e0b; + background: rgba(245,158,11,.16); padding: 2px 7px; border-radius: 999px; } +.cv-node-body { display: flex; flex-direction: column; gap: 8px; padding: 11px 12px; } +.cv-node-hint { color: var(--muted); font-size: 11px; line-height: 1.5; } + +/* 节点内表单控件 */ +.cv-field { width: 100%; background: var(--bg-2); border: 1px solid var(--border); + color: var(--text); border-radius: 9px; padding: 8px 10px; font-size: 12px; + font-family: inherit; outline: none; resize: vertical; } +.cv-field:focus { border-color: var(--accent); } +select.cv-field { resize: none; cursor: pointer; } + +/* 并排两个下拉(比例 + 清晰度 / 音色 + 格式 等) */ +.cv-row2 { display: flex; gap: 8px; } +.cv-row2 .cv-field { flex: 1; min-width: 0; } + +/* 模型输入框 + 每节点「拉取」小按钮 */ +.cv-model-row { display: flex; gap: 6px; align-items: stretch; } +.cv-model-row .cv-field { flex: 1; min-width: 0; } +.cv-fetch-models { flex: none; border: 1px solid var(--border); background: var(--card-2); + color: var(--muted); border-radius: 9px; padding: 0 10px; cursor: pointer; + font-size: 11px; font-weight: 700; transition: .15s; } +.cv-fetch-models:hover { border-color: var(--accent); color: var(--accent); } +.cv-fetch-models:disabled { opacity: .5; cursor: not-allowed; } + +/* 素材上传控件(图片节点参考图 / 视频节点首帧·尾帧·多参考图)——走自建图床 */ +.cv-refimg-row, .cv-vmat { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; } +.cv-refimg-pick { display: inline-flex; align-items: center; gap: 5px; cursor: pointer; + border: 1px dashed var(--accent); color: var(--accent); background: var(--card-2); + border-radius: 9px; padding: 7px 12px; font-size: 11px; font-weight: 700; + transition: .15s; white-space: nowrap; user-select: none; } +.cv-refimg-pick:hover { background: var(--accent); color: #fff; } +.cv-refimg-thumb, .cv-vmat-thumb { border: 1px solid var(--border); } +.cv-refimg-clear, .cv-vmat-clear { flex: none; border: 1px solid var(--border); + background: var(--card-2); color: var(--muted); border-radius: 7px; cursor: pointer; + width: 24px; height: 24px; font-size: 12px; line-height: 1; padding: 0; transition: .15s; } +.cv-refimg-clear:hover, .cv-vmat-clear:hover { border-color: var(--danger); color: var(--danger); } +/* URL 输入 / 提示 / 多图缩略图 各自独占一行 */ +.cv-vmat-url { flex: 1 1 100%; } +.cv-refimg-row .cv-node-hint, .cv-vmat .cv-node-hint { flex: 1 1 100%; } +.cv-vmat-refs-thumbs { display: flex; flex-wrap: wrap; gap: 2px; flex: 1 1 100%; } +/* 复选标签(生成音频 / 水印 / 固定镜头)对齐 */ +.cv-check { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; + color: var(--muted); cursor: pointer; user-select: none; } + +/* 折叠「高级」区(接口地址覆盖),别把节点撑太高 */ +.cv-adv { border-top: 1px dashed var(--border); padding-top: 6px; } +.cv-adv > summary { cursor: pointer; color: var(--muted); font-size: 11px; + font-weight: 600; list-style: none; user-select: none; } +.cv-adv > summary::-webkit-details-marker { display: none; } +.cv-adv > summary::before { content: "▸ "; } +.cv-adv[open] > summary::before { content: "▾ "; } +.cv-adv[open] > summary { margin-bottom: 6px; } + +/* 运行按钮(沿用主色渐变观感) */ +.cv-run { align-self: stretch; border: 0; background: var(--grad); color: #fff; + padding: 8px 14px; border-radius: 9px; cursor: pointer; font-size: 12px; + font-weight: 700; transition: .15s; } +.cv-run:hover { filter: brightness(1.08); } +.cv-run:disabled { opacity: .6; cursor: not-allowed; } + +/* 错误红字 */ +.cv-node-err { color: var(--danger); font-size: 11px; line-height: 1.5; + background: rgba(255,107,107,.1); border-radius: 8px; padding: 6px 9px; } + +/* 预览区(img/video/audio)圆角、限高、加载骨架 */ +.cv-node-preview { border-radius: 10px; overflow: hidden; background: var(--bg-2); + border: 1px solid var(--border); } +.cv-preview-shell { position: relative; background: #000; } +.cv-preview-actions { position: absolute; top: 6px; right: 6px; display: flex; gap: 5px; + z-index: 2; opacity: .92; } +.cv-preview-actions button { width: 26px; height: 26px; display: grid; place-items: center; + border: 1px solid rgba(255,255,255,.22); border-radius: 7px; background: rgba(15,17,23,.78); + color: #fff; cursor: pointer; font-size: 13px; line-height: 1; padding: 0; } +.cv-preview-actions button:hover { background: rgba(90,103,216,.92); border-color: rgba(255,255,255,.4); } +.cv-media { display: block; width: 100%; max-height: 220px; object-fit: contain; + background: #000; } +.cv-media-audio { display: block; width: 100%; } +.cv-meta { font-size: 10px; color: var(--muted); padding: 5px 8px; + border-top: 1px solid var(--border); } +.cv-skeleton { display: flex; align-items: center; justify-content: center; gap: 8px; + height: 120px; color: var(--muted); font-size: 12px; + background: linear-gradient(100deg, var(--bg-2) 30%, var(--card-2) 50%, var(--bg-2) 70%); + background-size: 200% 100%; animation: cv-shimmer 1.2s linear infinite; } +@keyframes cv-shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } } +.cv-spin { width: 13px; height: 13px; border-radius: 50%; + border: 2px solid var(--border); border-top-color: var(--accent); + animation: cv-rot .7s linear infinite; } +@keyframes cv-rot { to { transform: rotate(360deg); } } + +/* —— 端口与连线用主题色 —— */ +.drawflow .drawflow-node .input, +.drawflow .drawflow-node .output { background: var(--card); + border: 2px solid var(--accent); width: 13px; height: 13px; } +.drawflow .drawflow-node .output:hover, +.drawflow .drawflow-node .input:hover { background: var(--accent); } +.drawflow .connection .main-path { stroke: var(--accent); stroke-width: 2.4px; } +.drawflow .connection .main-path:hover { stroke: var(--text); } +.drawflow .connection .point { stroke: var(--accent); fill: var(--card); } + +/* —— 沉浸式全屏:fixed 覆盖整个应用窗口(含顶部导航) —— */ +body.cv-noscroll { overflow: hidden; } +#tab-canvas.cv-fullscreen { position: fixed; inset: 0; z-index: 9999; margin: 0; + height: auto; padding: 14px 16px; background: var(--bg); } +#tab-canvas.cv-fullscreen .cv-stage { min-height: 0; border-radius: 12px; } diff --git a/renderer/vendor/drawflow.min.css b/renderer/vendor/drawflow.min.css new file mode 100644 index 0000000..c9c9442 --- /dev/null +++ b/renderer/vendor/drawflow.min.css @@ -0,0 +1 @@ +.drawflow,.drawflow .parent-node{position:relative}.parent-drawflow{display:flex;overflow:hidden;touch-action:none;outline:0}.drawflow{width:100%;height:100%;user-select:none;perspective:0}.drawflow .drawflow-node{display:flex;align-items:center;position:absolute;background:#0ff;width:160px;min-height:40px;border-radius:4px;border:2px solid #000;color:#000;z-index:2;padding:15px}.drawflow .drawflow-node.selected{background:red}.drawflow .drawflow-node:hover{cursor:move}.drawflow .drawflow-node .inputs,.drawflow .drawflow-node .outputs{width:0}.drawflow .drawflow-node .drawflow_content_node{width:100%;display:block}.drawflow .drawflow-node .input,.drawflow .drawflow-node .output{position:relative;width:20px;height:20px;background:#fff;border-radius:50%;border:2px solid #000;cursor:crosshair;z-index:1;margin-bottom:5px}.drawflow .drawflow-node .input{left:-27px;top:2px;background:#ff0}.drawflow .drawflow-node .output{right:-3px;top:2px}.drawflow svg{z-index:0;position:absolute;overflow:visible!important}.drawflow .connection{position:absolute;pointer-events:none;aspect-ratio:1/1}.drawflow .connection .main-path{fill:none;stroke-width:5px;stroke:#4682b4;pointer-events:all}.drawflow .connection .main-path:hover{stroke:#1266ab;cursor:pointer}.drawflow .connection .main-path.selected{stroke:#43b993}.drawflow .connection .point{cursor:move;stroke:#000;stroke-width:2;fill:#fff;pointer-events:all}.drawflow .connection .point.selected,.drawflow .connection .point:hover{fill:#1266ab}.drawflow .main-path{fill:none;stroke-width:5px;stroke:#4682b4}.drawflow-delete{position:absolute;display:block;width:30px;height:30px;background:#000;color:#fff;z-index:4;border:2px solid #fff;line-height:30px;font-weight:700;text-align:center;border-radius:50%;font-family:monospace;cursor:pointer}.drawflow>.drawflow-delete{margin-left:-15px;margin-top:15px}.parent-node .drawflow-delete{right:-15px;top:-15px} \ No newline at end of file diff --git a/renderer/vendor/drawflow.min.js b/renderer/vendor/drawflow.min.js new file mode 100644 index 0000000..ebdcbdd --- /dev/null +++ b/renderer/vendor/drawflow.min.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Drawflow=t():e.Drawflow=t()}("undefined"!=typeof self?self:this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var s=t[i]={i:i,l:!1,exports:{}};return e[i].call(s.exports,s,s.exports,n),s.l=!0,s.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(i,s,function(t){return e[t]}.bind(null,s));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){"use strict";n.r(t),n.d(t,"default",(function(){return i}));class i{constructor(e,t=null,n=null){this.events={},this.container=e,this.precanvas=null,this.nodeId=1,this.ele_selected=null,this.node_selected=null,this.drag=!1,this.reroute=!1,this.reroute_fix_curvature=!1,this.curvature=.5,this.reroute_curvature_start_end=.5,this.reroute_curvature=.5,this.reroute_width=6,this.drag_point=!1,this.editor_selected=!1,this.connection=!1,this.connection_ele=null,this.connection_selected=null,this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_x_start=0,this.pos_y=0,this.pos_y_start=0,this.mouse_x=0,this.mouse_y=0,this.line_path=5,this.first_click=null,this.force_first_input=!1,this.draggable_inputs=!0,this.useuuid=!1,this.parent=n,this.noderegister={},this.render=t,this.drawflow={drawflow:{Home:{data:{}}}},this.module="Home",this.editor_mode="edit",this.zoom=1,this.zoom_max=1.6,this.zoom_min=.5,this.zoom_value=.1,this.zoom_last_value=1,this.evCache=new Array,this.prevDiff=-1}start(){this.container.classList.add("parent-drawflow"),this.container.tabIndex=0,this.precanvas=document.createElement("div"),this.precanvas.classList.add("drawflow"),this.container.appendChild(this.precanvas),this.container.addEventListener("mouseup",this.dragEnd.bind(this)),this.container.addEventListener("mousemove",this.position.bind(this)),this.container.addEventListener("mousedown",this.click.bind(this)),this.container.addEventListener("touchend",this.dragEnd.bind(this)),this.container.addEventListener("touchmove",this.position.bind(this)),this.container.addEventListener("touchstart",this.click.bind(this)),this.container.addEventListener("contextmenu",this.contextmenu.bind(this)),this.container.addEventListener("keydown",this.key.bind(this)),this.container.addEventListener("wheel",this.zoom_enter.bind(this)),this.container.addEventListener("input",this.updateNodeValue.bind(this)),this.container.addEventListener("dblclick",this.dblclick.bind(this)),this.container.onpointerdown=this.pointerdown_handler.bind(this),this.container.onpointermove=this.pointermove_handler.bind(this),this.container.onpointerup=this.pointerup_handler.bind(this),this.container.onpointercancel=this.pointerup_handler.bind(this),this.container.onpointerout=this.pointerup_handler.bind(this),this.container.onpointerleave=this.pointerup_handler.bind(this),this.load()}pointerdown_handler(e){this.evCache.push(e)}pointermove_handler(e){for(var t=0;t100&&(n>this.prevDiff&&this.zoom_in(),n=n&&(n=parseInt(e)+1)}))})),this.nodeId=n}removeReouteConnectionSelected(){this.dispatch("connectionUnselected",!0),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})}click(e){if(this.dispatch("click",e),"fixed"===this.editor_mode){if("parent-drawflow"!==e.target.classList[0]&&"drawflow"!==e.target.classList[0])return!1;this.ele_selected=e.target.closest(".parent-drawflow"),e.preventDefault()}else"view"===this.editor_mode?(null!=e.target.closest(".drawflow")||e.target.matches(".parent-drawflow"))&&(this.ele_selected=e.target.closest(".parent-drawflow"),e.preventDefault()):(this.first_click=e.target,this.ele_selected=e.target,0===e.button&&this.contextmenuDel(),null!=e.target.closest(".drawflow_content_node")&&(this.ele_selected=e.target.closest(".drawflow_content_node").parentElement));switch(this.ele_selected.classList[0]){case"drawflow-node":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected!=this.ele_selected&&this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.node_selected!=this.ele_selected&&this.dispatch("nodeSelected",this.ele_selected.id.slice(5)),this.node_selected=this.ele_selected,this.node_selected.classList.add("selected"),this.draggable_inputs?"SELECT"!==e.target.tagName&&(this.drag=!0):"INPUT"!==e.target.tagName&&"TEXTAREA"!==e.target.tagName&&"SELECT"!==e.target.tagName&&!0!==e.target.hasAttribute("contenteditable")&&(this.drag=!0);break;case"output":this.connection=!0,null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.drawConnection(e.target);break;case"parent-drawflow":case"drawflow":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.editor_selected=!0;break;case"main-path":null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null),this.connection_selected=this.ele_selected,this.connection_selected.classList.add("selected");const t=this.connection_selected.parentElement.classList;t.length>1&&(this.dispatch("connectionSelected",{output_id:t[2].slice(14),input_id:t[1].slice(13),output_class:t[3],input_class:t[4]}),this.reroute_fix_curvature&&this.connection_selected.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.add("selected")}));break;case"point":this.drag_point=!0,this.ele_selected.classList.add("selected");break;case"drawflow-delete":this.node_selected&&this.removeNodeId(this.node_selected.id),this.connection_selected&&this.removeConnection(),null!=this.node_selected&&(this.node_selected.classList.remove("selected"),this.node_selected=null,this.dispatch("nodeUnselected",!0)),null!=this.connection_selected&&(this.connection_selected.classList.remove("selected"),this.removeReouteConnectionSelected(),this.connection_selected=null)}"touchstart"===e.type?(this.pos_x=e.touches[0].clientX,this.pos_x_start=e.touches[0].clientX,this.pos_y=e.touches[0].clientY,this.pos_y_start=e.touches[0].clientY,this.mouse_x=e.touches[0].clientX,this.mouse_y=e.touches[0].clientY):(this.pos_x=e.clientX,this.pos_x_start=e.clientX,this.pos_y=e.clientY,this.pos_y_start=e.clientY),["input","output","main-path"].includes(this.ele_selected.classList[0])&&e.preventDefault(),this.dispatch("clickEnd",e)}position(e){if("touchmove"===e.type)var t=e.touches[0].clientX,n=e.touches[0].clientY;else t=e.clientX,n=e.clientY;if(this.connection&&this.updateConnection(t,n),this.editor_selected&&(i=this.canvas_x+-(this.pos_x-t),s=this.canvas_y+-(this.pos_y-n),this.dispatch("translate",{x:i,y:s}),this.precanvas.style.transform="translate("+i+"px, "+s+"px) scale("+this.zoom+")"),this.drag){e.preventDefault();var i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n,this.ele_selected.style.top=this.ele_selected.offsetTop-s+"px",this.ele_selected.style.left=this.ele_selected.offsetLeft-i+"px",this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_x=this.ele_selected.offsetLeft-i,this.drawflow.drawflow[this.module].data[this.ele_selected.id.slice(5)].pos_y=this.ele_selected.offsetTop-s,this.updateConnectionNodes(this.ele_selected.id)}if(this.drag_point){i=(this.pos_x-t)*this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom),s=(this.pos_y-n)*this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom);this.pos_x=t,this.pos_y=n;var o=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),l=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));this.ele_selected.setAttributeNS(null,"cx",o),this.ele_selected.setAttributeNS(null,"cy",l);const e=this.ele_selected.parentElement.classList[2].slice(9),c=this.ele_selected.parentElement.classList[1].slice(13),d=this.ele_selected.parentElement.classList[3],a=this.ele_selected.parentElement.classList[4];let r=Array.from(this.ele_selected.parentElement.children).indexOf(this.ele_selected)-1;if(this.reroute_fix_curvature){r-=this.ele_selected.parentElement.querySelectorAll(".main-path").length-1,r<0&&(r=0)}const h=e.slice(5),u=this.drawflow.drawflow[this.module].data[h].outputs[d].connections.findIndex((function(e,t){return e.node===c&&e.output===a}));this.drawflow.drawflow[this.module].data[h].outputs[d].connections[u].points[r]={pos_x:o,pos_y:l};const p=this.ele_selected.parentElement.classList[2].slice(9);this.updateConnectionNodes(p)}"touchmove"===e.type&&(this.mouse_x=t,this.mouse_y=n),this.dispatch("mouseMove",{x:t,y:n})}dragEnd(e){if("touchend"===e.type)var t=this.mouse_x,n=this.mouse_y,i=document.elementFromPoint(t,n);else t=e.clientX,n=e.clientY,i=e.target;if(this.drag&&(this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("nodeMoved",this.ele_selected.id.slice(5))),this.drag_point&&(this.ele_selected.classList.remove("selected"),this.pos_x_start==t&&this.pos_y_start==n||this.dispatch("rerouteMoved",this.ele_selected.parentElement.classList[2].slice(14))),this.editor_selected&&(this.canvas_x=this.canvas_x+-(this.pos_x-t),this.canvas_y=this.canvas_y+-(this.pos_y-n),this.editor_selected=!1),!0===this.connection)if("input"===i.classList[0]||this.force_first_input&&(null!=i.closest(".drawflow_content_node")||"drawflow-node"===i.classList[0])){if(!this.force_first_input||null==i.closest(".drawflow_content_node")&&"drawflow-node"!==i.classList[0])s=i.parentElement.parentElement.id,o=i.classList[1];else{if(null!=i.closest(".drawflow_content_node"))var s=i.closest(".drawflow_content_node").parentElement.id;else var s=i.id;if(0===Object.keys(this.getNodeFromId(s.slice(5)).inputs).length)var o=!1;else var o="input_1"}var l=this.ele_selected.parentElement.parentElement.id,c=this.ele_selected.classList[1];if(l!==s&&!1!==o){if(0===this.container.querySelectorAll(".connection.node_in_"+s+".node_out_"+l+"."+c+"."+o).length){this.connection_ele.classList.add("node_in_"+s),this.connection_ele.classList.add("node_out_"+l),this.connection_ele.classList.add(c),this.connection_ele.classList.add(o);var d=s.slice(5),a=l.slice(5);this.drawflow.drawflow[this.module].data[a].outputs[c].connections.push({node:d,output:o}),this.drawflow.drawflow[this.module].data[d].inputs[o].connections.push({node:a,input:c}),this.updateConnectionNodes("node-"+a),this.updateConnectionNodes("node-"+d),this.dispatch("connectionCreated",{output_id:a,input_id:d,output_class:c,input_class:o})}else this.dispatch("connectionCancel",!0),this.connection_ele.remove();this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null}else this.dispatch("connectionCancel",!0),this.connection_ele.remove(),this.connection_ele=null;this.drag=!1,this.drag_point=!1,this.connection=!1,this.ele_selected=null,this.editor_selected=!1,this.dispatch("mouseUp",e)}contextmenu(e){if(this.dispatch("contextmenu",e),e.preventDefault(),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;if(this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove(),this.node_selected||this.connection_selected){var t=document.createElement("div");t.classList.add("drawflow-delete"),t.innerHTML="x",this.node_selected&&this.node_selected.appendChild(t),this.connection_selected&&this.connection_selected.parentElement.classList.length>1&&(t.style.top=e.clientY*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))+"px",t.style.left=e.clientX*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))+"px",this.precanvas.appendChild(t))}}contextmenuDel(){this.precanvas.getElementsByClassName("drawflow-delete").length&&this.precanvas.getElementsByClassName("drawflow-delete")[0].remove()}key(e){if(this.dispatch("keydown",e),"fixed"===this.editor_mode||"view"===this.editor_mode)return!1;("Delete"===e.key||"Backspace"===e.key&&e.metaKey)&&(null!=this.node_selected&&"INPUT"!==this.first_click.tagName&&"TEXTAREA"!==this.first_click.tagName&&!0!==this.first_click.hasAttribute("contenteditable")&&this.removeNodeId(this.node_selected.id),null!=this.connection_selected&&this.removeConnection())}zoom_enter(e,t){e.ctrlKey&&(e.preventDefault(),e.deltaY>0?this.zoom_out():this.zoom_in())}zoom_refresh(){this.dispatch("zoom",this.zoom),this.canvas_x=this.canvas_x/this.zoom_last_value*this.zoom,this.canvas_y=this.canvas_y/this.zoom_last_value*this.zoom,this.zoom_last_value=this.zoom,this.precanvas.style.transform="translate("+this.canvas_x+"px, "+this.canvas_y+"px) scale("+this.zoom+")"}zoom_in(){this.zoomthis.zoom_min&&(this.zoom-=this.zoom_value,this.zoom_refresh())}zoom_reset(){1!=this.zoom&&(this.zoom=1,this.zoom_refresh())}createCurvature(e,t,n,i,s,o){var l=e,c=t,d=n,a=i,r=s;switch(o){case"open":if(e>=n)var h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"close":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*r;else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;case"other":if(e>=n)h=l+Math.abs(d-l)*(-1*r),u=d-Math.abs(d-l)*(-1*r);else h=l+Math.abs(d-l)*r,u=d-Math.abs(d-l)*r;return" M "+l+" "+c+" C "+h+" "+c+" "+u+" "+a+" "+d+" "+a;default:return" M "+l+" "+c+" C "+(h=l+Math.abs(d-l)*r)+" "+c+" "+(u=d-Math.abs(d-l)*r)+" "+a+" "+d+" "+a}}drawConnection(e){var t=document.createElementNS("http://www.w3.org/2000/svg","svg");this.connection_ele=t;var n=document.createElementNS("http://www.w3.org/2000/svg","path");n.classList.add("main-path"),n.setAttributeNS(null,"d",""),t.classList.add("connection"),t.appendChild(n),this.precanvas.appendChild(t);var i=e.parentElement.parentElement.id.slice(5),s=e.classList[1];this.dispatch("connectionStart",{output_id:i,output_class:s})}updateConnection(e,t){const n=this.precanvas,i=this.zoom;let s=n.clientWidth/(n.clientWidth*i);s=s||0;let o=n.clientHeight/(n.clientHeight*i);o=o||0;var l=this.connection_ele.children[0],c=this.ele_selected.offsetWidth/2+(this.ele_selected.getBoundingClientRect().x-n.getBoundingClientRect().x)*s,d=this.ele_selected.offsetHeight/2+(this.ele_selected.getBoundingClientRect().y-n.getBoundingClientRect().y)*o,a=e*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),r=t*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom)),h=this.curvature,u=this.createCurvature(c,d,a,r,h,"openclose");l.setAttributeNS(null,"d",u)}addConnection(e,t,n,i){var s=this.getModuleFromNodeId(e);if(s===this.getModuleFromNodeId(t)){var o=this.getNodeFromId(e),l=!1;for(var c in o.outputs[n].connections){var d=o.outputs[n].connections[c];d.node==t&&d.output==i&&(l=!0)}if(!1===l){if(this.drawflow.drawflow[s].data[e].outputs[n].connections.push({node:t.toString(),output:i}),this.drawflow.drawflow[s].data[t].inputs[i].connections.push({node:e.toString(),input:n}),this.module===s){var a=document.createElementNS("http://www.w3.org/2000/svg","svg"),r=document.createElementNS("http://www.w3.org/2000/svg","path");r.classList.add("main-path"),r.setAttributeNS(null,"d",""),a.classList.add("connection"),a.classList.add("node_in_node-"+t),a.classList.add("node_out_node-"+e),a.classList.add(n),a.classList.add(i),a.appendChild(r),this.precanvas.appendChild(a),this.updateConnectionNodes("node-"+e),this.updateConnectionNodes("node-"+t)}this.dispatch("connectionCreated",{output_id:e,input_id:t,output_class:n,input_class:i})}}}updateConnectionNodes(e){const t="node_in_"+e,n="node_out_"+e;this.line_path;const i=this.container,s=this.precanvas,o=this.curvature,l=this.createCurvature,c=this.reroute_curvature,d=this.reroute_curvature_start_end,a=this.reroute_fix_curvature,r=this.reroute_width,h=this.zoom;let u=s.clientWidth/(s.clientWidth*h);u=u||0;let p=s.clientHeight/(s.clientHeight*h);p=p||0;const f=i.querySelectorAll("."+n);Object.keys(f).map((function(t,n){if(null===f[t].querySelector(".point")){var m=i.querySelector("#"+e),g=f[t].classList[1].replace("node_in_",""),_=i.querySelector("#"+g).querySelectorAll("."+f[t].classList[4])[0],w=_.offsetWidth/2+(_.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=_.offsetHeight/2+(_.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=m.querySelectorAll("."+f[t].classList[3])[0],C=y.offsetWidth/2+(y.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,x=y.offsetHeight/2+(y.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(C,x,w,v,o,"openclose");f[t].children[0].setAttributeNS(null,"d",n)}else{const n=f[t].querySelectorAll(".point");let o="";const m=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),g=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,g,_,d,"open");o+=y,m.push(y);f=t;var C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0];g=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,g,_,d,"close");o+=y,m.push(y)}else if(0===a){var L;f=i.querySelector("#"+e),g=((x=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(L=f.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(L.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,v=L.offsetHeight/2+(L.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,y=l(w,v,g,_,d,"open");o+=y,m.push(y);f=t,g=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,y=l(w,v,g,_,c,"other");o+=y,m.push(y)}else if(a===n.length-1){var E,R;f=t,C=t.parentElement.classList[1].replace("node_in_",""),x=(E=i.querySelector("#"+C)).querySelectorAll("."+t.parentElement.classList[4])[0],g=(R=E.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(R.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,_=R.offsetHeight/2+(R.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,g,_,d,"close");o+=y,m.push(y)}else{f=t,g=((x=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,_=(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,w=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*(s.clientWidth/(s.clientWidth*h))+r,v=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*(s.clientHeight/(s.clientHeight*h))+r,y=l(w,v,g,_,c,"other");o+=y,m.push(y)}}),a?m.forEach((e,n)=>{f[t].children[n].setAttributeNS(null,"d",e)}):f[t].children[0].setAttributeNS(null,"d",o)}}));const m=i.querySelectorAll("."+t);Object.keys(m).map((function(t,n){if(null===m[t].querySelector(".point")){var h=i.querySelector("#"+e),f=m[t].classList[2].replace("node_out_",""),g=i.querySelector("#"+f).querySelectorAll("."+m[t].classList[3])[0],_=g.offsetWidth/2+(g.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=g.offsetHeight/2+(g.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=(h=h.querySelectorAll("."+m[t].classList[4])[0]).offsetWidth/2+(h.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,y=h.offsetHeight/2+(h.getBoundingClientRect().y-s.getBoundingClientRect().y)*p;const n=l(_,w,v,y,o,"openclose");m[t].children[0].setAttributeNS(null,"d",n)}else{const n=m[t].querySelectorAll(".point");let o="";const h=[];n.forEach((t,a)=>{if(0===a&&n.length-1==0){var f=i.querySelector("#"+e),m=((C=t).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,_=(E=f.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,v=l(m,g,_,w,d,"close");o+=v,h.push(v);f=t;var y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0];m=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,g=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"open");o+=v,h.push(v)}else if(0===a){var x;f=t,y=t.parentElement.classList[2].replace("node_out_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[3])[0],m=(x=L.querySelectorAll("."+t.parentElement.classList[3])[0]).offsetWidth/2+(x.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,g=x.offsetHeight/2+(x.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,_=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"open");o+=v,h.push(v);f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,c,"other");o+=v,h.push(v)}else if(a===n.length-1){var L,E;f=t,y=t.parentElement.classList[1].replace("node_in_",""),C=(L=i.querySelector("#"+y)).querySelectorAll("."+t.parentElement.classList[4])[0],_=(E=L.querySelectorAll("."+t.parentElement.classList[4])[0]).offsetWidth/2+(E.getBoundingClientRect().x-s.getBoundingClientRect().x)*u,w=E.offsetHeight/2+(E.getBoundingClientRect().y-s.getBoundingClientRect().y)*p,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,d,"close");o+=v,h.push(v)}else{f=t,_=((C=n[a+1]).getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,w=(C.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,m=(f.getBoundingClientRect().x-s.getBoundingClientRect().x)*u+r,g=(f.getBoundingClientRect().y-s.getBoundingClientRect().y)*p+r,v=l(m,g,_,w,c,"other");o+=v,h.push(v)}}),a?h.forEach((e,n)=>{m[t].children[n].setAttributeNS(null,"d",e)}):m[t].children[0].setAttributeNS(null,"d",o)}}))}dblclick(e){null!=this.connection_selected&&this.reroute&&this.createReroutePoint(this.connection_selected),"point"===e.target.classList[0]&&this.removeReroutePoint(e.target)}createReroutePoint(e){this.connection_selected.classList.remove("selected");const t=this.connection_selected.parentElement.classList[2].slice(9),n=this.connection_selected.parentElement.classList[1].slice(13),i=this.connection_selected.parentElement.classList[3],s=this.connection_selected.parentElement.classList[4];this.connection_selected=null;const o=document.createElementNS("http://www.w3.org/2000/svg","circle");o.classList.add("point");var l=this.pos_x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom))-this.precanvas.getBoundingClientRect().x*(this.precanvas.clientWidth/(this.precanvas.clientWidth*this.zoom)),c=this.pos_y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom))-this.precanvas.getBoundingClientRect().y*(this.precanvas.clientHeight/(this.precanvas.clientHeight*this.zoom));o.setAttributeNS(null,"cx",l),o.setAttributeNS(null,"cy",c),o.setAttributeNS(null,"r",this.reroute_width);let d=0;if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;var a=document.createElementNS("http://www.w3.org/2000/svg","path");if(a.classList.add("main-path"),a.setAttributeNS(null,"d",""),e.parentElement.insertBefore(a,e.parentElement.children[t]),1===t)e.parentElement.appendChild(o);else{const n=Array.from(e.parentElement.children).indexOf(e);d=n,e.parentElement.insertBefore(o,e.parentElement.children[n+t+1])}}else e.parentElement.appendChild(o);const r=t.slice(5),h=this.drawflow.drawflow[this.module].data[r].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));void 0===this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points&&(this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points=[]),this.reroute_fix_curvature?(d>0||this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points!==[]?this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.splice(d,0,{pos_x:l,pos_y:c}):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),e.parentElement.querySelectorAll(".main-path").forEach((e,t)=>{e.classList.remove("selected")})):this.drawflow.drawflow[this.module].data[r].outputs[i].connections[h].points.push({pos_x:l,pos_y:c}),this.dispatch("addReroute",r),this.updateConnectionNodes(t)}removeReroutePoint(e){const t=e.parentElement.classList[2].slice(9),n=e.parentElement.classList[1].slice(13),i=e.parentElement.classList[3],s=e.parentElement.classList[4];let o=Array.from(e.parentElement.children).indexOf(e);const l=t.slice(5),c=this.drawflow.drawflow[this.module].data[l].outputs[i].connections.findIndex((function(e,t){return e.node===n&&e.output===s}));if(this.reroute_fix_curvature){const t=e.parentElement.querySelectorAll(".main-path").length;e.parentElement.children[t-1].remove(),o-=t,o<0&&(o=0)}else o--;this.drawflow.drawflow[this.module].data[l].outputs[i].connections[c].points.splice(o,1),e.remove(),this.dispatch("removeReroute",l),this.updateConnectionNodes(t)}registerNode(e,t,n=null,i=null){this.noderegister[e]={html:t,props:n,options:i}}getNodeFromId(e){var t=this.getModuleFromNodeId(e);return JSON.parse(JSON.stringify(this.drawflow.drawflow[t].data[e]))}getNodesFromName(e){var t=[];const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){for(var o in n[i].data)n[i].data[o].name==e&&t.push(n[i].data[o].id)})),t}addNode(e,t,n,i,s,o,l,c,d=!1){if(this.useuuid)var a=this.getUuid();else a=this.nodeId;const r=document.createElement("div");r.classList.add("parent-node");const h=document.createElement("div");h.innerHTML="",h.setAttribute("id","node-"+a),h.classList.add("drawflow-node"),""!=o&&h.classList.add(...o.split(" "));const u=document.createElement("div");u.classList.add("inputs");const p=document.createElement("div");p.classList.add("outputs");const f={};for(var m=0;me(this.noderegister[c].html,{props:this.noderegister[c].props}),...this.noderegister[c].options}).$mount();_.appendChild(e.$el)}Object.entries(l).forEach((function(e,t){if("object"==typeof e[1])!function e(t,n,i){if(null===t)t=l[n];else t=t[n];null!==t&&Object.entries(t).forEach((function(n,s){if("object"==typeof n[1])e(t,n[0],i+"-"+n[0]);else for(var o=_.querySelectorAll("[df-"+i+"-"+n[0]+"]"),l=0;lt(this.noderegister[e.html].html,{props:this.noderegister[e.html].props}),...this.noderegister[e.html].options}).$mount();c.appendChild(t.$el)}Object.entries(e.data).forEach((function(t,n){if("object"==typeof t[1])!function t(n,i,s){if(null===n)n=e.data[i];else n=n[i];null!==n&&Object.entries(n).forEach((function(e,i){if("object"==typeof e[1])t(n,e[0],s+"-"+e[0]);else for(var o=c.querySelectorAll("[df-"+s+"-"+e[0]+"]"),l=0;l{const a=e.outputs[s].connections[o].node,r=e.outputs[s].connections[o].output,h=i.querySelector(".connection.node_in_node-"+a+".node_out_node-"+e.id+"."+s+"."+r);if(n&&0===d)for(var u=0;u{this.removeSingleConnection(e.id_output,e.id,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].inputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].inputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].inputs={};const c=t.slice(6);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push(e)}),this.drawflow.drawflow[n].data[e].inputs["input_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .inputs .input").forEach((e,t)=>{const n=e.classList[1].slice(6);parseInt(c){this.drawflow.drawflow[n].data[t.node].outputs[t.input].connections.forEach((i,s)=>{if(i.node==e){const o=i.output.slice(6);if(parseInt(c){this.removeSingleConnection(e.id,e.id_input,e.output_class,e.input_class)}),delete this.drawflow.drawflow[n].data[e].outputs[t];const o=[],l=this.drawflow.drawflow[n].data[e].outputs;Object.keys(l).map((function(e,t){o.push(l[e])})),this.drawflow.drawflow[n].data[e].outputs={};const c=t.slice(7);let d=[];if(o.forEach((t,i)=>{t.connections.forEach((e,t)=>{d.push({node:e.node,output:e.output})}),this.drawflow.drawflow[n].data[e].outputs["output_"+(i+1)]=t}),d=new Set(d.map(e=>JSON.stringify(e))),d=Array.from(d).map(e=>JSON.parse(e)),this.module===n){this.container.querySelectorAll("#node-"+e+" .outputs .output").forEach((e,t)=>{const n=e.classList[1].slice(7);parseInt(c){this.drawflow.drawflow[n].data[t.node].inputs[t.output].connections.forEach((i,s)=>{if(i.node==e){const o=i.input.slice(7);if(parseInt(c)-1){this.module===s&&this.container.querySelector(".connection.node_in_node-"+t+".node_out_node-"+e+"."+n+"."+i).remove();var o=this.drawflow.drawflow[s].data[e].outputs[n].connections.findIndex((function(e,n){return e.node==t&&e.output===i}));this.drawflow.drawflow[s].data[e].outputs[n].connections.splice(o,1);var l=this.drawflow.drawflow[s].data[t].inputs[i].connections.findIndex((function(t,i){return t.node==e&&t.input===n}));return this.drawflow.drawflow[s].data[t].inputs[i].connections.splice(l,1),this.dispatch("connectionRemoved",{output_id:e,input_id:t,output_class:n,input_class:i}),!0}return!1}return!1}removeConnectionNodeId(e){const t="node_in_"+e,n="node_out_"+e,i=this.container.querySelectorAll("."+n);for(var s=i.length-1;s>=0;s--){var o=i[s].classList,l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1);var c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1),i[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}const d=this.container.querySelectorAll("."+t);for(s=d.length-1;s>=0;s--){o=d[s].classList,c=this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.findIndex((function(e,t){return e.node===o[1].slice(13)&&e.output===o[4]}));this.drawflow.drawflow[this.module].data[o[2].slice(14)].outputs[o[3]].connections.splice(c,1);l=this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.findIndex((function(e,t){return e.node===o[2].slice(14)&&e.input===o[3]}));this.drawflow.drawflow[this.module].data[o[1].slice(13)].inputs[o[4]].connections.splice(l,1),d[s].remove(),this.dispatch("connectionRemoved",{output_id:o[2].slice(14),input_id:o[1].slice(13),output_class:o[3],input_class:o[4]})}}getModuleFromNodeId(e){var t;const n=this.drawflow.drawflow;return Object.keys(n).map((function(i,s){Object.keys(n[i].data).map((function(n,s){n==e&&(t=i)}))})),t}addModule(e){this.drawflow.drawflow[e]={data:{}},this.dispatch("moduleCreated",e)}changeModule(e){this.dispatch("moduleChanged",e),this.module=e,this.precanvas.innerHTML="",this.canvas_x=0,this.canvas_y=0,this.pos_x=0,this.pos_y=0,this.mouse_x=0,this.mouse_y=0,this.zoom=1,this.zoom_last_value=1,this.precanvas.style.transform="",this.import(this.drawflow,!1)}removeModule(e){this.module===e&&this.changeModule("Home"),delete this.drawflow.drawflow[e],this.dispatch("moduleRemoved",e)}clearModuleSelected(){this.precanvas.innerHTML="",this.drawflow.drawflow[this.module]={data:{}}}clear(){this.precanvas.innerHTML="",this.drawflow={drawflow:{Home:{data:{}}}}}export(){const e=JSON.parse(JSON.stringify(this.drawflow));return this.dispatch("export",e),e}import(e,t=!0){this.clear(),this.drawflow=JSON.parse(JSON.stringify(e)),this.load(),t&&this.dispatch("import","import")}on(e,t){return"function"!=typeof t?(console.error("The listener callback must be a function, the given type is "+typeof t),!1):"string"!=typeof e?(console.error("The event name must be a string, the given type is "+typeof e),!1):(void 0===this.events[e]&&(this.events[e]={listeners:[]}),void this.events[e].listeners.push(t))}removeListener(e,t){if(!this.events[e])return!1;const n=this.events[e].listeners,i=n.indexOf(t);i>-1&&n.splice(i,1)}dispatch(e,t){if(void 0===this.events[e])return!1;this.events[e].listeners.forEach(e=>{e(t)})}getUuid(){for(var e=[],t=0;t<36;t++)e[t]="0123456789abcdef".substr(Math.floor(16*Math.random()),1);return e[14]="4",e[19]="0123456789abcdef".substr(3&e[19]|8,1),e[8]=e[13]=e[18]=e[23]="-",e.join("")}}}]).default})); \ No newline at end of file diff --git a/resources/singbox/README.md b/resources/singbox/README.md new file mode 100644 index 0000000..f752d35 --- /dev/null +++ b/resources/singbox/README.md @@ -0,0 +1,17 @@ +# sing-box 引擎(全局代理功能) + +「全局代理」功能内置 [sing-box](https://github.com/SagerNet/sing-box) 做本地中转 / TUN。 +这些二进制体积较大(每个约 45–50MB),**不纳入 git**。 + +打包 / 运行前先拉取一次: + +```bash +bash scripts/fetch-singbox.sh +``` + +会下载到: +- `darwin-arm64/sing-box` +- `darwin-x64/sing-box` +- `windows-x64/sing-box.exe` + +electron-builder 会按目标架构把对应二进制打进安装包(见 `electron-builder.yml` 的 `extraResources`)。 diff --git a/scripts/afterPack.js b/scripts/afterPack.js new file mode 100644 index 0000000..e9d8a50 --- /dev/null +++ b/scripts/afterPack.js @@ -0,0 +1,26 @@ +// afterPack 钩子:未购买 Apple 证书时,electron-builder(24) 会「跳过签名」, +// 导致 .app 的 bundle 封印(seal)残缺(codesign --verify 报 "code has no resources +// but signature indicates they must be present")。这里在打 DMG 前,用 ad-hoc 身份 +// 对整个 app(含内置 sing-box)做一次完整深度重新签名,得到有效封印—— +// 用户去掉隔离标记后即可稳定运行。 +const { execFileSync } = require('child_process'); +const path = require('path'); + +exports.default = async function afterPack(context) { + if (context.electronPlatformName !== 'darwin') return; + + const appName = `${context.packager.appInfo.productFilename}.app`; + const appPath = path.join(context.appOutDir, appName); + + console.log(` • ad-hoc 深度重签名 app=${appPath}`); + // --force 覆盖已有签名,--deep 递归签所有嵌套二进制(sing-box 等),-s - 表示 ad-hoc + execFileSync('codesign', ['--force', '--deep', '--sign', '-', appPath], { + stdio: 'inherit', + }); + + // 校验封印是否完整(失败则中断构建,避免发出坏包) + execFileSync('codesign', ['--verify', '--deep', '--strict', '--verbose=2', appPath], { + stdio: 'inherit', + }); + console.log(' • ad-hoc 签名校验通过 ✔'); +}; diff --git a/scripts/fetch-singbox.sh b/scripts/fetch-singbox.sh new file mode 100644 index 0000000..cf618ff --- /dev/null +++ b/scripts/fetch-singbox.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +# 下载全局代理功能所需的 sing-box 引擎二进制(macOS arm64/x64 + Windows x64)。 +# 这些二进制不进 git(太大),打包前跑一次即可。用法: bash scripts/fetch-singbox.sh +set -euo pipefail +VER="${SINGBOX_VERSION:-1.13.14}" +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +DEST="$ROOT/resources/singbox" +mkdir -p "$DEST" +cd "$DEST" + +fetch() { # $1=平台目录 $2=资源名(不含扩展) $3=压缩类型 $4=包内二进制名 + local dir="$1" asset="$2" kind="$3" bin="$4" + echo "▶ $dir …" + if [ -f "$dir/$bin" ]; then echo " 已存在,跳过"; return; fi + mkdir -p "$dir" + local url="https://github.com/SagerNet/sing-box/releases/download/v${VER}/${asset}.${kind}" + if [ "$kind" = "tar.gz" ]; then + curl -fsSL "$url" -o t.tgz && tar xzf t.tgz && mv "${asset}/${bin}" "$dir/" && rm -rf t.tgz "$asset" + else + curl -fsSL "$url" -o t.zip && unzip -qo t.zip && mv "${asset}/${bin}" "$dir/" && rm -rf t.zip "$asset" + fi + chmod +x "$dir/$bin" 2>/dev/null || true + echo " ✓ $dir/$bin" +} + +fetch "darwin-arm64" "sing-box-${VER}-darwin-arm64" "tar.gz" "sing-box" +fetch "darwin-x64" "sing-box-${VER}-darwin-amd64" "tar.gz" "sing-box" +fetch "windows-x64" "sing-box-${VER}-windows-amd64" "zip" "sing-box.exe" +echo "全部就位 ✅ (sing-box v${VER})" diff --git a/scripts/publish-hot.js b/scripts/publish-hot.js new file mode 100644 index 0000000..4897723 --- /dev/null +++ b/scripts/publish-hot.js @@ -0,0 +1,124 @@ +#!/usr/bin/env node +// publish-hot.js —— 发布热更新包到自建 gitea。 +// +// 用法: +// GIT_TOKEN=xxx node scripts/publish-hot.js --version 1.5.1 [--min 1.5.0] \ +// [--notes "更新说明"] [--mandatory] [--requires-full] +// token 也可放在 ~/.tokendun-git-token 文件里(优先用环境变量 GIT_TOKEN)。 +// +// 流程:打包 electron/+renderer/ 成 zip → sha256(+可选签名) → 写 hotupdate.json +// → gitea 的 tag=hot release:删旧 bundle/manifest、传新的。 +"use strict"; +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const crypto = require("crypto"); +const { execFileSync } = require("child_process"); + +const ROOT = path.join(__dirname, ".."); +const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, "package.json"), "utf8")); + +// —— 参数 —— +const argv = process.argv.slice(2); +function arg(name, def) { + const i = argv.indexOf(name); + return i >= 0 && argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[i + 1] : def; +} +const has = (name) => argv.includes(name); +const version = arg("--version", pkg.hotContentVersion || pkg.version); +const minAppVersion = arg("--min", pkg.version); +const notes = arg("--notes", ""); +const mandatory = has("--mandatory"); +const requiresFull = has("--requires-full"); + +// —— token —— +let token = process.env.GIT_TOKEN || ""; +if (!token) { + try { token = fs.readFileSync(path.join(os.homedir(), ".tokendun-git-token"), "utf8").trim(); } catch {} +} +if (!token) { console.error("缺少 gitea token:设置 GIT_TOKEN 或写入 ~/.tokendun-git-token"); process.exit(1); } + +const OWNER = "backup-bot", REPO = "backup", TAG = "hot"; +const API = `https://up.tokendun.cc/api/v1/repos/${OWNER}/${REPO}`; +const DL = `https://up.tokendun.cc/${OWNER}/${REPO}/releases/download/${TAG}`; + +const OUT = path.join(ROOT, "dist-hot"); +fs.mkdirSync(OUT, { recursive: true }); +const bundleName = `bundle-${version}.zip`; +const bundlePath = path.join(OUT, bundleName); + +// —— 1) 打包(zip,deflate,可被 lib/zip.js 解压)—— +fs.rmSync(bundlePath, { force: true }); +console.log(`打包 electron/ + renderer/ → ${bundleName} …`); +execFileSync("zip", ["-r", "-X", "-q", bundlePath, "electron", "renderer", + "-x", "*.map", "-x", "*/node_modules/*"], { cwd: ROOT, stdio: "inherit" }); + +// —— 2) sha256(+ 可选 ed25519 签名)—— +const buf = fs.readFileSync(bundlePath); +const sha256 = crypto.createHash("sha256").update(buf).digest("hex"); +let signature = ""; +try { + const keyPath = path.join(os.homedir(), ".codex-hot-key.pem"); // 私钥(不入库) + if (fs.existsSync(keyPath)) { + const key = fs.readFileSync(keyPath); + signature = crypto.sign(null, Buffer.from(`${sha256}:${version}`), key).toString("base64"); + console.log("已用本地私钥签名"); + } +} catch (e) { console.warn("签名跳过:" + e.message); } + +// —— 3) manifest —— +const manifest = { + contentVersion: version, + minAppVersion, + requiresFull, + bundleUrl: `${DL}/${bundleName}`, + size: buf.length, + sha256, + signature, + notes, + mandatory, +}; +const manifestPath = path.join(OUT, "hotupdate.json"); +fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2)); +console.log(`sha256=${sha256} size=${(buf.length / 1048576).toFixed(2)}MB`); + +// —— 4) gitea API —— +function api(method, urlPath, body) { + const args = ["-s", "-X", method, "-H", `Authorization: token ${token}`, `${API}${urlPath}`]; + if (body !== undefined) { args.push("-H", "Content-Type: application/json", "-d", JSON.stringify(body)); } + return execFileSync("curl", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); +} +function apiJson(method, urlPath, body) { + const out = api(method, urlPath, body); + try { return JSON.parse(out); } catch { return null; } +} + +// 确保 hot release 存在 +let rel = apiJson("GET", `/releases/tags/${TAG}`); +if (!rel || !rel.id) { + console.log("创建 hot release …"); + rel = apiJson("POST", `/releases`, { tag_name: TAG, target_commitish: "main", name: "热更新通道", body: "asar 热更新包(自动发布)", draft: false, prerelease: false }); +} +if (!rel || !rel.id) { console.error("无法创建/获取 hot release"); process.exit(1); } + +// 删除同名旧 asset +for (const a of rel.assets || []) { + if (a.name === bundleName || a.name === "hotupdate.json" || /^bundle-.*\.zip$/.test(a.name)) { + api("DELETE", `/releases/${rel.id}/assets/${a.id}`); + console.log("删除旧 asset:", a.name); + } +} +// 上传 bundle + manifest +function upload(file, name) { + const code = execFileSync("curl", ["-s", "-o", "/dev/null", "-w", "%{http_code}", + "-X", "POST", "-H", `Authorization: token ${token}`, + "-F", `attachment=@${file};type=application/octet-stream`, + `${API}/releases/${rel.id}/assets?name=${encodeURIComponent(name)}`], { encoding: "utf8" }); + console.log(`上传 ${name} → HTTP ${code}`); +} +upload(bundlePath, bundleName); +upload(manifestPath, "hotupdate.json"); + +console.log(`\n✅ 热更 v${version} 已发布。`); +console.log(` minAppVersion=${minAppVersion} mandatory=${mandatory} requiresFull=${requiresFull}`); +console.log(` 客户端将从 ${API}/releases/tags/${TAG} 检测到。`);