/** * Browser half of dsh-marketing-toolkit — runs inside the dsh web GUI. * * v0.2.2: the entry row is injected as a SIBLING of the dsh-web family rows * (任务看板 / SSH / 技能中心), using the exact same sidebar-entry-core pattern * as @linxin666/dsh-client-ui-skill-explorer: DOM row between the New Session * button and the workspace browser, familySelectors keep the rows ordered, * MutationObserver self-heals after React re-renders. Clicking it opens the * marketing launcher panel (pure DOM overlay, no React needed). * * Failure policy: DOM problems are logged, never thrown (a plugin must never * take the GUI boot down). * * TO ADD A NEW TOOL: append one row to the CHANNELS array below. The panel * grid and the launch flow are data-driven — nothing else changes. */ window.__ModuleLoader__.load({ id: "dsh-marketing-toolkit", factory: (require) => { var module = { exports: {} }; var exports = module.exports; const PKG = "dsh-marketing-toolkit"; const SKILL = "marketing-toolkit"; /** * 营销工具箱通道表 —— 新增工具就在这里加一行: * { id, icon, title, desc, kickoff } */ const CHANNELS = [ { id: 'cover', icon: '🖼️', title: '封面图生成', desc: '公众号 / 视频号 / 小红书高点击率封面', kickoff: '请先加载技能(skill)marketing-toolkit,然后进入其中的「🖼️ 封面图生成」通道为我工作。', }, { id: 'moments', icon: '📱', title: '朋友圈文案', desc: '痛点 / 故事 / 数据三种风格 + 评论区话术', kickoff: '请先加载技能(skill)marketing-toolkit,然后进入其中的「📱 朋友圈文案」通道为我工作。', }, { id: 'video-script', icon: '🎬', title: '短视频脚本', desc: '分镜表:画面 + 台词 + 时长 + BGM', kickoff: '请先加载技能(skill)marketing-toolkit,然后进入其中的「🎬 短视频脚本」通道为我工作。', }, { id: 'hook-check', icon: '⚡', title: '开头黄金3秒检验', desc: '完播率诊断 + 3 个改写方案', kickoff: '请先加载技能(skill)marketing-toolkit,然后进入其中的「⚡ 开头黄金3秒检验」通道为我工作。', }, ]; function buildPrompt(channel, brief) { const trimmed = brief.trim(); const briefBlock = trimmed === '' ? '【我的产品/主题】(我还没有提供,请先向我提问确认:产品/主题、投放平台/场景、核心卖点)' : '【我的产品/主题】' + trimmed; return channel.kickoff + '\n\n' + briefBlock + '\n\n请严格按照该通道的规范执行,并在每个阶段结束后询问我是否需要切换到其他营销工具。'; } /** Megaphone/broadcast glyph, normalized to the shell's 18px nav icon size. */ const ICON = ''; /** * Sidebar row + panel styles, aligned with the dsh-web family rows: * 36px row, 18px glyph, hover token; [data-sidebar-collapsed] turns the * row into the circular rail icon exactly like the family rows. */ const STYLE_ID = "dsh-marketing-toolkit-style"; const CSS_TEXT = [ /* ---- sidebar entry row (family-consistent) ---- */ ".dshmt-entry{box-sizing:border-box;width:100%;height:36px;color:var(--dsw-alias-label-secondary,#5f6672);cursor:pointer;white-space:nowrap;background:0 0;border:none;border-radius:8px;align-items:center;gap:8px;padding:0 10px;font-size:13px;display:flex;font-family:inherit;text-align:left}", ".dshmt-entry:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(20,20,20,0.06));color:var(--dsw-alias-label-primary,#1c1e26)}", ".dshmt-entry[data-active]{background:var(--dsw-alias-interactive-bg-hover,rgba(20,20,20,0.06));color:var(--dsw-alias-label-primary,#1c1e26)}", ".dshmt-entryIcon{flex:none;justify-content:center;align-items:center;width:24px;height:24px;display:inline-flex}", ".dshmt-entryIcon svg{width:18px;height:18px;display:block}", ".dshmt-entryLabel{text-overflow:ellipsis;overflow:hidden}", "[data-dsh-frame][data-sidebar-collapsed] .dshmt-entry,[data-sidebar-collapsed] .dshmt-entry{border-radius:50%;justify-content:center;width:36px;height:36px;margin:0 auto 12px;padding:0}", "[data-dsh-frame][data-sidebar-collapsed] .dshmt-entryLabel,[data-sidebar-collapsed] .dshmt-entryLabel{display:none}", /* ---- launcher panel ---- */ ".dshmt-overlay{position:fixed;inset:0;z-index:9999;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-2,rgba(8,10,16,0.45));font-family:inherit}", ".dshmt-card{width:min(460px,92vw);max-height:84vh;overflow:auto;box-sizing:border-box;border-radius:12px;border:1px solid var(--dsw-alias-border-l2,rgba(20,20,20,0.08));background:var(--dsw-alias-bg-overlay,#fdfdfd);color:var(--dsw-alias-label-primary,#1c1e26);box-shadow:0 18px 60px rgba(0,0,0,0.35);padding:16px 18px 12px}", ".dshmt-head{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:2px}", ".dshmt-title{font-size:15px;font-weight:600;display:flex;align-items:center;gap:8px}", ".dshmt-subtitle{font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280);margin:0 0 12px}", ".dshmt-close{border:none;background:var(--dsw-alias-bg-layer-1,#f2f3f5);color:var(--dsw-alias-label-primary,#3a3f4b);font-size:13px;line-height:1;width:26px;height:26px;border-radius:6px;cursor:pointer;flex:none}", ".dshmt-close:hover{background:var(--dsw-alias-interactive-bg-hover,#e7e8ea)}", ".dshmt-brief-label{display:block;font-size:12px;color:var(--dsw-alias-label-secondary,#5f6672);margin:0 0 6px}", ".dshmt-brief{display:block;width:100%;box-sizing:border-box;resize:vertical;min-height:56px;max-height:120px;margin:0 0 12px;padding:8px 10px;border-radius:6px;border:1px solid transparent;background:var(--dsw-alias-bg-layer-1,#f7f8fa);color:var(--dsw-alias-label-primary,#1c1e26);font-family:inherit;font-size:12px;line-height:1.5}", ".dshmt-brief:focus{outline:none;border-color:var(--dsw-alias-brand-primary,#4c6ef5)}", ".dshmt-list{display:flex;flex-direction:column;gap:8px}", ".dshmt-channel{display:flex;flex-direction:row;align-items:center;gap:12px;width:100%;box-sizing:border-box;text-align:left;padding:10px 14px;border-radius:8px;border:1px solid var(--dsw-alias-border-l1,#e5e7eb);background:var(--dsw-alias-bg-base,#fff);color:var(--dsw-alias-label-primary,#1c1e26);font-family:inherit;cursor:pointer;transition:border-color .15s ease,background .15s ease}", ".dshmt-channel:hover{border-color:var(--dsw-alias-brand-primary,#4c6ef5);background:var(--dsw-alias-interactive-bg-hover,rgba(20,20,20,0.04))}", ".dshmt-channel:disabled{opacity:.55;cursor:default}", ".dshmt-channel-icon{flex:none;width:30px;height:30px;display:flex;align-items:center;justify-content:center;border-radius:8px;background:var(--dsw-alias-bg-layer-1,#f2f3f5);font-size:15px}", ".dshmt-channel-text{display:flex;flex-direction:column;gap:2px;min-width:0;flex:1 1 auto}", ".dshmt-channel-head{font-size:13px;font-weight:600;line-height:1.4}", ".dshmt-channel-desc{font-size:12px;color:var(--dsw-alias-label-secondary,#8a8f9c);line-height:1.45}", ".dshmt-placeholder{display:flex;align-items:center;justify-content:center;text-align:center;min-height:44px;padding:10px;border-radius:8px;border:1.5px dashed var(--dsw-alias-border-l2,rgba(20,20,20,0.14));color:var(--dsw-alias-label-tertiary,#a2a7b3);font-size:12px;line-height:1.5}", ".dshmt-status{margin-top:10px;min-height:18px;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}", ".dshmt-status[data-kind='ok']{color:var(--dsw-alias-state-success-primary,#0f9d6e)}", ".dshmt-status[data-kind='error']{color:var(--dsw-alias-state-error-primary,#b42318)}", ".dshmt-foot{margin-top:6px;font-size:11px;color:var(--dsw-alias-label-tertiary,#a0a5b1);text-align:right}", /* ---- dark theme adjustments (family precedent) ---- */ "body[data-ds-dark-theme]:not([data-dsh-skin]) .dshmt-card{background:#2c2c2e}", "body[data-ds-dark-theme]:not([data-dsh-skin]) .dshmt-channel{background:#48484a;border-color:rgba(255,255,255,0.08)}", "body[data-ds-dark-theme]:not([data-dsh-skin]) .dshmt-close{background:rgba(255,255,255,0.1);color:rgba(255,255,255,0.85)}", "body[data-ds-dark-theme]:not([data-dsh-skin]) .dshmt-brief{background:#1c1c1e;color:#f2f2f4;border-color:rgba(255,255,255,0.06)}", ].join("\n"); function injectStyles() { if (document.getElementById(STYLE_ID) !== null) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = CSS_TEXT; document.head.appendChild(style); } /* ===================== sidebar entry core ===================== * Same pattern as @linxin666/dsh-client-ui-skill-explorer * sidebar-entry-core.ts: place the row as a sibling of the family * rows, self-heal on shell re-renders, dispose cleanly. * ============================================================= */ const ENTRY_SELECTOR = "[data-dsh-marketing-toolkit-entry]"; function sidebarRoot() { const column = document.querySelector('[data-pane="sidebar"], [class*="sidebarCol"]'); if (column === null) return void 0; return column.querySelector('[class*="logoRow"]')?.parentElement ?? column.firstElementChild; } function newSessionButton(root) { const nested = root.querySelector('button[class*="newSession"]'); if (nested !== null) return nested; for (const child of root.children) if (child.tagName === "BUTTON") return child; return void 0; } function createEntry(options) { const entry = document.createElement("button"); entry.type = "button"; entry.setAttribute(options.rowAttribute, ""); entry.setAttribute("data-dsh-plugin", "marketing-toolkit"); entry.setAttribute("data-dsh-part", "sidebar-entry"); entry.className = options.css["entry"]; const labelSpan = document.createElement("span"); labelSpan.className = options.css["entryLabel"]; const iconSpan = document.createElement("span"); iconSpan.className = options.css["entryIcon"]; iconSpan.innerHTML = options.icon; entry.append(iconSpan, labelSpan); const applyLabel = () => { entry.setAttribute("aria-label", options.label()); entry.setAttribute("title", options.tooltip()); labelSpan.textContent = options.label(); }; applyLabel(); entry.addEventListener("click", options.onToggle); return { entry, applyLabel }; } /** Re-insert the row after the last family member (below 技能中心). */ function placeEntry(root, entry, options) { const button = newSessionButton(root); if (button === void 0) return false; if (entry.parentElement !== root) { const row = button.closest('[class*="logoRow"]'); const base = row !== null && row.parentElement === root ? row : button; const family = Array.from(root.children).filter((el) => el instanceof HTMLElement && el.matches(options.familySelectors.join(", "))); const anchor = options.position === "before" ? (family.length > 0 ? family[0] : base.nextElementSibling) : (family.length > 0 ? family[family.length - 1].nextElementSibling : base.nextElementSibling); root.insertBefore(entry, anchor); } return true; } function mountSidebarEntry(options) { if (typeof document !== "undefined" && document.querySelector(options.rowSelector) !== null) return () => {}; const { entry } = createEntry(options); let root; let placed = false; let rootObserver; const tryPlace = () => { if (root !== void 0 && !root.isConnected) { rootObserver.disconnect(); root = void 0; placed = false; } if (placed) { if (document.body.contains(entry)) return; rootObserver.disconnect(); root = void 0; placed = false; } root ??= sidebarRoot(); if (root === void 0) return; placed = placeEntry(root, entry, options); if (placed) rootObserver.observe(root, { childList: true, subtree: true }); }; const waitObserver = new MutationObserver(() => { try { tryPlace(); } catch (e) { console.warn("[" + PKG + "] tryPlace:", e); } }); waitObserver.observe(document.body, { childList: true, subtree: true }); rootObserver = new MutationObserver(() => { try { if (root === void 0 || !root.isConnected) { placed = false; tryPlace(); return; } if (!root.contains(entry)) placed = placeEntry(root, entry, options); } catch (e) { console.warn("[" + PKG + "] heal:", e); } }); const syncActive = () => { if (options.active.isOpen()) entry.dataset.active = "true"; else delete entry.dataset.active; }; const unsubscribeActive = options.active.subscribe(syncActive); syncActive(); tryPlace(); return () => { waitObserver.disconnect(); rootObserver.disconnect(); unsubscribeActive(); entry.remove(); }; } /* ===================== launcher panel (pure DOM) ===================== */ function buildPanel(hooks) { const overlay = document.createElement("div"); overlay.className = "dshmt-overlay"; overlay.setAttribute("data-dshmt-overlay", ""); overlay.style.display = "none"; const card = document.createElement("div"); card.className = "dshmt-card"; card.setAttribute("role", "dialog"); card.setAttribute("aria-label", "营销工具箱"); const head = document.createElement("div"); head.className = "dshmt-head"; const title = document.createElement("div"); title.className = "dshmt-title"; title.textContent = "🎯 营销工具箱"; const close = document.createElement("button"); close.type = "button"; close.className = "dshmt-close"; close.setAttribute("aria-label", "关闭"); close.textContent = "✕"; head.append(title, close); const subtitle = document.createElement("p"); subtitle.className = "dshmt-subtitle"; subtitle.textContent = "选择一个工具,将创建新会话并自动下达任务(均基于 marketing-toolkit 技能)"; const briefLabel = document.createElement("label"); briefLabel.className = "dshmt-brief-label"; briefLabel.textContent = "产品 / 主题简介(可选,填了会随任务一起发送)"; const brief = document.createElement("textarea"); brief.className = "dshmt-brief"; brief.placeholder = "例如:夏季新款防晒霜,主打敏感肌可用,客单价 89 元…"; briefLabel.appendChild(brief); const list = document.createElement("div"); list.className = "dshmt-list"; const buttons = []; for (const channel of CHANNELS) { const row = document.createElement("button"); row.type = "button"; row.className = "dshmt-channel"; const icon = document.createElement("span"); icon.className = "dshmt-channel-icon"; icon.setAttribute("aria-hidden", "true"); icon.textContent = channel.icon; const text = document.createElement("span"); text.className = "dshmt-channel-text"; const head = document.createElement("span"); head.className = "dshmt-channel-head"; head.textContent = channel.title; const desc = document.createElement("span"); desc.className = "dshmt-channel-desc"; desc.textContent = channel.desc; text.append(head, desc); row.append(icon, text); row.addEventListener("click", () => { hooks.onLaunch(channel, brief.value); }); buttons.push(row); list.appendChild(row); } const placeholder = document.createElement("div"); placeholder.className = "dshmt-placeholder"; placeholder.textContent = "+ 更多工具陆续上线(在此处扩展)"; list.appendChild(placeholder); const status = document.createElement("div"); status.className = "dshmt-status"; status.setAttribute("role", "status"); const foot = document.createElement("div"); foot.className = "dshmt-foot"; foot.textContent = "添加新工具:编辑插件 lib/client.js 顶部 CHANNELS 数组"; card.append(head, subtitle, briefLabel, list, status, foot); overlay.appendChild(card); overlay.addEventListener("mousedown", (event) => { if (event.target === overlay) hooks.onClose(); }); close.addEventListener("click", () => { hooks.onClose(); }); return { element: overlay, setBusy: (busy) => { for (const b of buttons) b.disabled = busy; brief.disabled = busy; close.disabled = busy; }, setStatus: (text, kind) => { status.textContent = text; if (kind === undefined) status.removeAttribute("data-kind"); else status.setAttribute("data-kind", kind); }, focusBrief: () => { brief.focus(); }, }; } /* ===================== channel launch ===================== */ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** * Resolve the target workspace the way New Session does: * the current session's workspace, else the most recently updated one. */ function resolveWorkspaceId(sessions, workspaces) { if (workspaces === undefined) return undefined; try { const snapshot = workspaces.list.getSnapshot(); const current = sessions.list.getSnapshot().current; if (current !== undefined) { const currentWs = snapshot.items.find((item) => item.sessionIds.includes(current)); if (currentWs !== undefined) return currentWs.workspaceId; } const sorted = [...snapshot.items].sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? ""))); return sorted[0]?.workspaceId; } catch (error) { console.warn("[" + PKG + "] workspace resolve failed:", error); return undefined; } } async function launchChannel(ctx, channel, brief) { const sessions = ctx.get("sessions"); const conversation = ctx.get("conversation"); const uiWorkspace = ctx.get("uiWorkspace"); const workspaces = ctx.get("workspaces"); if (sessions === undefined) throw new Error("sessions 服务不可用,请刷新页面重试"); const workspaceId = resolveWorkspaceId(sessions, workspaces); let sessionId; if (workspaceId !== undefined && uiWorkspace !== undefined && typeof uiWorkspace.connectWorkspace === "function") { sessionId = await uiWorkspace.connectWorkspace(workspaceId); } else { sessionId = await sessions.create(workspaceId === undefined ? {} : { workspaceId }); } if (typeof sessionId !== "string" || sessionId === "") throw new Error("会话创建失败"); sessions.open(sessionId); await sleep(400); const text = buildPrompt(channel, brief); const binding = typeof sessions.binding === "function" ? sessions.binding(sessionId) : undefined; if (conversation !== undefined && binding?.session !== undefined && typeof conversation.sendSession === "function") { const outcome = await conversation.sendSession(binding.session, text, [], "queue"); if (outcome !== undefined && outcome !== null && typeof outcome === "object" && outcome.kind === "error") { throw new Error("消息发送失败,请在新会话中手动输入需求"); } return; } throw new Error("未找到可用的消息通道,请在新会话中手动输入需求"); } /* ===================== apply ===================== */ const inject = ["sessions", "conversation", "workspaces"]; function apply(ctx) { try { ctx.effect(() => { injectStyles(); let panel; let overlayHosted = false; let launching = false; const isOpen = () => panel !== undefined && overlayHosted && panel.element.style.display !== "none"; const active = (() => { const listeners = new Set(); return { isOpen, notify: () => { for (const fn of [...listeners]) fn(); }, subscribe: (fn) => { listeners.add(fn); return () => { listeners.delete(fn); }; }, }; })(); const open = () => { if (panel === undefined || launching) return; if (!overlayHosted) { document.body.appendChild(panel.element); overlayHosted = true; } panel.element.style.display = ""; panel.setStatus("选择工具开始;Esc 或点击空白处关闭", undefined); panel.focusBrief(); active.notify(); }; const close = () => { if (panel === undefined || launching) return; panel.element.style.display = "none"; active.notify(); }; const onKeydown = (event) => { if (event.key === "Escape" && isOpen()) close(); }; const onSidebarNavigate = (event) => { if (!isOpen()) return; const t = event.target; if (t instanceof Element && t.closest('[data-dsh-skill-explorer-entry], [data-dsh-ssh-entry], [data-dsh-taskboard-entry], [class*="sessionRow"], [class*="newSession"]') !== null) close(); }; const onLaunch = async (channel, brief) => { if (launching || panel === undefined) return; launching = true; panel.setBusy(true); panel.setStatus("正在创建新会话并下达「" + channel.title + "」任务…", "busy"); try { await launchChannel(ctx, channel, brief); panel.setStatus("✓ 已创建会话并发送任务,正在为你跳转…", "ok"); await sleep(500); launching = false; panel.setBusy(false); panel.element.style.display = "none"; } catch (error) { launching = false; panel.setBusy(false); panel.setStatus("✕ " + (error instanceof Error ? error.message : String(error)), "error"); console.warn("[" + PKG + "] launch failed:", error); } }; panel = buildPanel({ onLaunch, onClose: close }); const disposeEntry = mountSidebarEntry({ rowAttribute: "data-dsh-marketing-toolkit-entry", rowSelector: ENTRY_SELECTOR, icon: ICON, css: { entry: "dshmt-entry", entryLabel: "dshmt-entryLabel", entryIcon: "dshmt-entryIcon" }, label: () => "营销工具箱", tooltip: () => "营销工具箱:封面图 / 朋友圈文案 / 短视频脚本 / 黄金3秒", onToggle: () => { if (isOpen()) close(); else { open(); } }, active, position: "after", familySelectors: [ "[data-dsh-taskboard-entry]", "[data-dsh-ssh-entry]", "[data-dsh-skill-explorer-entry]", "[data-dsh-marketing-toolkit-entry]", ], }); document.addEventListener("keydown", onKeydown); document.addEventListener("click", onSidebarNavigate, true); return () => { document.removeEventListener("keydown", onKeydown); document.removeEventListener("click", onSidebarNavigate, true); disposeEntry(); panel?.element.remove(); panel = undefined; overlayHosted = false; document.getElementById(STYLE_ID)?.remove(); }; }, PKG + ": ui"); } catch (error) { console.warn("[" + PKG + "] apply failed:", error); } } exports.apply = apply; exports.inject = inject; return module.exports; } });