/** * 神之一手(one_divine_lot)客户端插件 * * 注册: * - conversation.view tabs:全部持仓 / 交易记录 / 关注列表(通用,按 tabConfig 显隐)+ 策略 tabs(按策略配置) * - settings.section:神之一手设置菜单(通用设置 + 策略分组) * * 迭代 02: * - O2 策略 tab 动态化:策略配置驱动注册 * - 设置 tab 化扩展:三个通用 tab(全部持仓/交易记录/关注列表)由 tabConfig 控制显隐 * - 刷新页面 = 插件重载 = 按最新配置重新注册(无事件机制) */ import { createElement } from 'react'; import { AllPositionsTab } from './views/AllPositionsTab.jsx'; import { StrategyTab } from './views/StrategyTab.jsx'; import { PlaceholderTab } from './views/PlaceholderTab.jsx'; import { TradeRecordsTab } from './views/TradeRecordsTab.jsx'; import { SettingsSection } from './views/SettingsSection.jsx'; import { QmtConnectionChip } from './views/QmtConnectionChip.jsx'; import { ConnectionProvider } from './views/connection.jsx'; import { MarketDataProvider } from './market/MarketDataProvider.jsx'; export const inject = ['slots', 'connection']; async function fetchTabs() { try { const res = await fetch('/odl/api/tabs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const data = await res.json(); if (data && data.ok && Array.isArray(data.value)) return data.value; } catch (e) { /* ignore */ } return null; } function strategyTabId(strategyId) { return 'odl-strategy-' + strategyId; } // R-011:内置 tab 渲染映射(refKey → render);order 由 settings.tabs 统一管理 const GENERAL_RENDER = { allPositions: (withConnection) => withConnection((props) => createElement(AllPositionsTab, props)), tradeRecords: (withConnection) => withConnection((props) => createElement(TradeRecordsTab, props)), watchlist: (withConnection) => withConnection((props) => createElement(PlaceholderTab, { ...props, title: '关注列表' })), }; export function apply(ctx) { const connection = ctx.get('connection'); if (!connection) { ctx.logger?.warn?.('[one-divine-lot] connection 服务不可用'); } // 神之一手 tab 激活时隐藏会话输入框(composer) // 借鉴 dsh-context 的纯 CSS 方案::has(.odl-root) 命中 → 隐藏 [data-composer-seat] // 我们的 tab 根元素带 odl-root class;chat/其他 tab 无此 class → 输入框正常显示 const ODL_HIDE_COMPOSER_CSS = ` [data-conversation-scroll]:has(.odl-root)>[data-composer-seat]{display:none} `; if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="one-divine-lot/hide-composer"]')) { const tag = document.createElement('style'); tag.dataset.pluginCss = 'one-divine-lot/hide-composer'; tag.textContent = ODL_HIDE_COMPOSER_CSS; document.head.appendChild(tag); } const withConnection = (render) => (props) => createElement(ConnectionProvider, { connection }, createElement(MarketDataProvider, null, render(props))); ctx.slots.register({ name: 'settings.section', id: 'one-divine-lot', order: 50, label: () => '神之一手', }, withConnection((props) => createElement(SettingsSection, props))); let tabDisposers = []; let disposed = false; const disposeAllTabs = () => { for (const d of tabDisposers) { try { d(); } catch (e) { /* ignore */ } } tabDisposers = []; }; // R-011:统一注册 —— 读 settings.tabs 有序数组,按 order 排序、过滤 visible // builtin 走内置 render(GENERAL_RENDER),strategy 走 StrategyTab const registerAllTabs = async () => { const tabs = await fetchTabs(); if (disposed) return; disposeAllTabs(); if (!tabs) return; const sorted = tabs.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0)); let n = 0; for (const t of sorted) { if (t.visible === false) continue; const render = t.kind === 'builtin' ? GENERAL_RENDER[t.refKey] : (withConnection) => withConnection((props) => createElement(StrategyTab, { ...props, strategyId: t.refId, strategyName: t.name })); if (!render) continue; const dispose = ctx.slots.register({ name: 'conversation.view', id: t.kind === 'builtin' ? t.id : strategyTabId(t.refId), order: t.order ?? 0, label: () => t.name ?? t.id, }, render(withConnection)); tabDisposers.push(dispose); n++; } ctx.logger?.info?.('[one-divine-lot] 已注册 ' + n + ' 个会话 tab'); }; registerAllTabs(); // R-004 Q9:会话头部快捷切换 chip(与 DSH 内置 PTC 标签 order=-10 并排,技术约束-009) ctx.slots.register({ name: 'conversation.session.header.actions', id: 'odl-qmt-switch', order: -9, }, withConnection((props) => createElement(QmtConnectionChip, props))); ctx.effect(() => { return () => { disposed = true; disposeAllTabs(); }; }, 'one-divine-lot.client'); }